//! The VFS wire protocol — what a client (through the file API,
//! library/kernel/file-system.zig) says to a filesystem backend over IPC. Defined
//! through the envelope (docs/os-development/protocol-namespace.md), so every
//! packet begins with the folded `Header`: the verb in `Header.operation`, and
//! **the open node id in `Header.target`** — the field that used to be
//! `Request.node`. A path appears in the conversation once, at `open`; every
//! packet after it addresses that integer.
//!
//! This is a danos-native contract, so it uses danos names throughout. It is
//! user-space only — the kernel knows nothing of files or paths; it only routes
//! (`fs_resolve`) and moves the bytes. The backends that serve it today are the
//! FAT server (system/services/fat/) and the protocol registry inside PID 1
//! (system/services/init/), which is a *synthetic* backend: `/protocol` holds
//! contracts rather than files.
//!
//! **An `open` reply may carry a capability.** The `open` request rides
//! `ipc_call`, and the reply direction of a call can hand back an endpoint
//! (`ipc.callCap`'s `Reply.cap`). A file backend never uses it — FAT answers with
//! a node id and nothing else — but the registry does: opening a
//! `NodeKind.protocol` node under `/protocol` returns the provider's endpoint,
//! which is the channel. The convention is per-backend, not per-operation: a
//! client that did not ask a synthetic backend simply gets no capability back.

const envelope = @import("envelope");

/// The type of a filesystem node, aligned to the node-kind table
/// (docs/file-system-development/file-system-hierarchy.md). Fills `FileStatus.kind` and
/// `DirectoryEntry.kind`; `regular = 0` keeps the historical hardcoded value.
pub const NodeKind = enum(u32) {
    regular = 0,
    directory = 1,
    character_device = 2,
    block_device = 3,
    symbolic_link = 4,
    fifo = 5,
    socket = 6,
    /// A node that names a *contract*, not a file: opening it establishes a
    /// channel to whatever process currently provides that protocol, delivered
    /// as an endpoint capability in the reply rather than a node id. This is
    /// what lives under `/protocol`; `readdir` lists these like any other node,
    /// so the tree stays browsable for diagnosis.
    protocol = 7,
};

/// One directory entry: the fixed part of a `readdir` reply, followed inline by
/// `name_len` bytes of name. **A zero `name_len` is end of directory** — the
/// reply's own length cannot say so any more, because the envelope always sends
/// the fixed part.
pub const DirectoryEntry = extern struct {
    kind: u32 = 0, // a NodeKind
    name_len: u32 = 0,
    size: u64 = 0,
};

pub const directory_entry_size: usize = @sizeOf(DirectoryEntry);

/// A file's metadata (the danos-native answer to a `status` request). The POSIX
/// layer maps this onto `struct stat`.
pub const FileStatus = extern struct {
    size: u64,
    kind: u32,
    _padding: u32 = 0,
    /// Modification time — Unix epoch seconds, UTC. 0 if the backend has none (the
    /// flat ramfs). Filled from the FAT directory entry's write date/time.
    mtime: u64 = 0,
};

// --- the per-operation request and reply parts ------------------------------
//
// Each names the bytes AFTER the prefix. Nothing here carries an operation or a
// node id: those are the packet header's, folded in once.

/// `open(flags)` with the path as the packet's tail. The one verb that spends a
/// path; everything after it addresses the node id this returns.
pub const Open = extern struct { flags: u32 = 0 };

/// The node id an `open` established — the integer every later packet puts in
/// `Header.target`. Meaningful only between this client and this backend.
pub const Opened = extern struct { node: u64 };

/// `read(offset, len)` on `Header.target`; the bytes come back as the reply tail.
pub const Read = extern struct {
    offset: u64,
    len: u32,
    _padding: u32 = 0,
};

/// `write(offset, len)` on `Header.target`, with the data as the packet's tail.
pub const Write = extern struct {
    offset: u64,
    len: u32,
    _padding: u32 = 0,
};

/// How many bytes a `write` actually took — it may be short.
pub const Written = extern struct { count: u32 };

/// `readdir(cursor)` on `Header.target`: one entry per call, cursor-advanced.
pub const Readdir = extern struct { cursor: u64 };

/// The contract, whole. Verbs number from `envelope.first_protocol_operation`
/// (16) in this order; the reserved verbs below it mean what they mean
/// everywhere. `readdir` stays a protocol verb rather than folding into the
/// reserved `enumerate`: it enumerates the children of one *node*, where
/// `enumerate` names a provider's targets.
pub const Protocol = envelope.Define(.{
    .name = "vfs",
    .version = 1,
    .operations = &.{
        .{ .name = "open", .request = Open, .reply = Opened },
        .{ .name = "close" },
        .{ .name = "read", .request = Read },
        .{ .name = "write", .request = Write, .reply = Written },
        .{ .name = "status", .reply = FileStatus },
        .{ .name = "readdir", .request = Readdir, .reply = DirectoryEntry },
        // The mount router's two verbs. Path routing lives in the kernel now
        // (system/kernel/vfs.zig), so no backend implements either; they keep
        // their numbers so the vocabulary stays the one docs/vfs-protocol.md
        // describes.
        .{ .name = "mount" }, // tail = the prefix, capability = the backend's endpoint
        .{ .name = "unmount" }, // tail = the prefix
        // Filesystem mutation, path-based: the path is the packet's tail.
        .{ .name = "mkdir" },
        .{ .name = "unlink" },
        // rename: the tail is the old path, a single 0x00 separator, then the
        // new path. Same-directory rename only.
        .{ .name = "rename" },
        // The registry's claim verb (P2): the name is the tail and the
        // provider's endpoint rides the call as its capability. A file backend
        // refuses it; only init implements it.
        .{ .name = "bind" },
    },
});

pub const Operation = Protocol.Operation;

/// What a backend sizes its buffers to — the call floor, as every protocol does.
pub const message_maximum: usize = Protocol.message_maximum;

/// The most inline payload any request may carry: the floor less the header and
/// the widest fixed request part, so one bound serves every verb (a path, write
/// data, a read's answer).
pub const maximum_payload: usize = envelope.packet_maximum - Protocol.request_maximum;

/// Open flags (danos-native; `file_system.OpenOptions` maps its booleans onto these).
pub const create: u32 = 1;
/// Open a directory (for readdir) rather than a file. A mounted backend uses
/// this to open a directory node; the flat ramfs ignores it.
pub const directory: u32 = 2;
/// Truncate the file to zero length on open (O_TRUNC): replace its contents rather
/// than overwriting in place, so a shorter new file leaves no stale tail. A mounted
/// backend frees the old cluster chain; the flat ramfs ignores it.
pub const truncate: u32 = 4;

test "the stable wire values: node kinds, entry layout, and the verb numbering" {
    const std = @import("std");
    try std.testing.expectEqual(@as(u32, 0), @intFromEnum(NodeKind.regular));
    try std.testing.expectEqual(@as(u32, 1), @intFromEnum(NodeKind.directory));
    // Appended with the protocol namespace; every earlier value keeps its own.
    try std.testing.expectEqual(@as(u32, 6), @intFromEnum(NodeKind.socket));
    try std.testing.expectEqual(@as(u32, 7), @intFromEnum(NodeKind.protocol));
    try std.testing.expectEqual(@as(usize, 16), @sizeOf(DirectoryEntry));

    // The numbering the envelope gives this protocol. These are NEW values: the
    // rebase moved every verb above the reserved range, so the old 0..11 are
    // gone and 16..27 are what the wire carries. Pinned because both sides of a
    // flag-day have to agree on them, not because they may never change again.
    try std.testing.expectEqual(@as(u32, 16), @intFromEnum(Operation.open));
    try std.testing.expectEqual(@as(u32, 17), @intFromEnum(Operation.close));
    try std.testing.expectEqual(@as(u32, 18), @intFromEnum(Operation.read));
    try std.testing.expectEqual(@as(u32, 19), @intFromEnum(Operation.write));
    try std.testing.expectEqual(@as(u32, 20), @intFromEnum(Operation.status));
    try std.testing.expectEqual(@as(u32, 21), @intFromEnum(Operation.readdir));
    try std.testing.expectEqual(@as(u32, 26), @intFromEnum(Operation.rename));
    try std.testing.expectEqual(@as(u32, 27), @intFromEnum(Operation.bind));
    // The payload bound is what it always was, arrived at the other way round:
    // the header plus the widest fixed request part is 32 bytes of the floor.
    try std.testing.expectEqual(@as(usize, 224), maximum_payload);
}

test "the node id rides the header, and a path rides the tail" {
    const std = @import("std");
    var buffer: [message_maximum]u8 = undefined;

    const opening = Protocol.encodeRequest(.open, 0, .{ .flags = create }, "/a/b", &buffer).?;
    try std.testing.expectEqual(@as(u32, create), Protocol.decodeRequest(.open, opening).?.flags);
    try std.testing.expectEqualStrings("/a/b", Protocol.requestTail(.open, opening));
    try std.testing.expectEqual(@as(u64, 0), envelope.headerOf(opening).?.target);

    const reading = Protocol.encodeRequest(.read, 7, .{ .offset = 512, .len = 64 }, &.{}, &buffer).?;
    try std.testing.expectEqual(@as(u64, 7), envelope.headerOf(reading).?.target);
    try std.testing.expectEqual(@as(u64, 512), Protocol.decodeRequest(.read, reading).?.offset);
}
