From ba996acfcad1698adbf4a1834fe50e73b1c6cab9 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Sat, 19 Sep 2026 23:28:22 -0300 Subject: Rename programs: 9player -> 9ns, introspect -> 9proc, app -> web (9web) Directories, binaries, build options (-D9ns, -D9proc), step names, module name (9proc), thread and fs names, env var NINEPLAYER_MOUNT -> NINE_MOUNT, docs and test scripts. Browser assets move to web/static. Co-Authored-By: Claude Fable 5.1 --- 9player/src/fuse.zig | 653 --------------------------------------------------- 1 file changed, 653 deletions(-) delete mode 100644 9player/src/fuse.zig (limited to '9player/src/fuse.zig') diff --git a/9player/src/fuse.zig b/9player/src/fuse.zig deleted file mode 100644 index ef11873..0000000 --- a/9player/src/fuse.zig +++ /dev/null @@ -1,653 +0,0 @@ -//! Kernel FUSE protocol subset (no libfuse, no libc, no policy). -//! -//! Extern structs mirror `/usr/include/linux/fuse.h` (kernel header 7.45); -//! every layout is checked against the header's size at comptime. Only the -//! opcodes and structs 9player needs are here. The I/O helpers are blocking -//! and allocation-free: the caller owns a single request buffer. -//! -//! Wire rules worth remembering: -//! * The kernel delivers exactly one request per `read(2)` on `/dev/fuse`, -//! and a reply must be exactly one `write(2)`/`writev(2)`. -//! * Request bodies start right after the 40-byte `InHeader`; since every -//! in-struct is 8-byte aligned in the header, `body()` requires the caller's -//! buffer to be 8-byte aligned (`std.heap` page allocations and -//! `align(8)` arrays both qualify). -//! * A write that fails with `ENOENT` means the request was interrupted and -//! the kernel already forgot it: the reply is silently dropped. -//! * `ENODEV` on read means the filesystem was unmounted. - -const std = @import("std"); -const linux = std.os.linux; - -pub const kernel_version: u32 = 7; -/// The minor we answer; the kernel adapts to the lower of the two. -pub const kernel_minor: u32 = 31; -pub const root_id: u64 = 1; - -pub const FOPEN_DIRECT_IO: u32 = 1 << 0; -pub const FOPEN_KEEP_CACHE: u32 = 1 << 1; -pub const FOPEN_NONSEEKABLE: u32 = 1 << 2; - -pub const FUSE_ASYNC_READ: u32 = 1 << 0; -/// The kernel passes O_TRUNC in OPEN instead of a separate SETATTR(size=0); 9P has OTRUNC for exactly this. -pub const FUSE_ATOMIC_O_TRUNC: u32 = 1 << 3; -/// Without this the kernel's cached-write path (`--no-direct-io`) sends one 4 KiB WRITE per page. -pub const FUSE_BIG_WRITES: u32 = 1 << 5; -/// Re-fetch a cached inode's size/mtime and drop stale pages when they change. -/// Required for `--no-direct-io` correctness: 9P sizes change under us, and -/// without this the kernel trusts a stale cached size and truncates reads. -pub const FUSE_AUTO_INVAL_DATA: u32 = 1 << 12; -pub const FUSE_MAX_PAGES: u32 = 1 << 22; - -pub const FATTR_MODE: u32 = 1 << 0; -pub const FATTR_UID: u32 = 1 << 1; -pub const FATTR_GID: u32 = 1 << 2; -pub const FATTR_SIZE: u32 = 1 << 3; -pub const FATTR_ATIME: u32 = 1 << 4; -pub const FATTR_MTIME: u32 = 1 << 5; -pub const FATTR_FH: u32 = 1 << 6; -pub const FATTR_ATIME_NOW: u32 = 1 << 7; -pub const FATTR_MTIME_NOW: u32 = 1 << 8; -pub const FATTR_LOCKOWNER: u32 = 1 << 9; -pub const FATTR_CTIME: u32 = 1 << 10; - -/// File type bits for `Attr.mode` and `Dirent.type` (used by the bridge). -pub const S_IFDIR: u32 = linux.S.IFDIR; -pub const S_IFREG: u32 = linux.S.IFREG; -pub const DT_DIR: u32 = linux.DT.DIR; -pub const DT_REG: u32 = linux.DT.REG; - -pub const Opcode = enum(u32) { - lookup = 1, - forget = 2, - getattr = 3, - setattr = 4, - readlink = 5, - symlink = 6, - mknod = 8, - mkdir = 9, - unlink = 10, - rmdir = 11, - rename = 12, - link = 13, - open = 14, - read = 15, - write = 16, - statfs = 17, - release = 18, - fsync = 20, - setxattr = 21, - getxattr = 22, - listxattr = 23, - removexattr = 24, - flush = 25, - init = 26, - opendir = 27, - readdir = 28, - releasedir = 29, - fsyncdir = 30, - getlk = 31, - setlk = 32, - setlkw = 33, - access = 34, - create = 35, - interrupt = 36, - bmap = 37, - destroy = 38, - ioctl = 39, - poll = 40, - notify_reply = 41, - batch_forget = 42, - fallocate = 43, - readdirplus = 44, - rename2 = 45, - lseek = 46, - copy_file_range = 47, - setupmapping = 48, - removemapping = 49, - syncfs = 50, - tmpfile = 51, - statx = 52, - _, -}; - -// --------------------------------------------------------------------------- -// Structs (field order and widths follow linux/fuse.h exactly) -// --------------------------------------------------------------------------- - -pub const InHeader = extern struct { - len: u32, - opcode: u32, - unique: u64, - nodeid: u64, - uid: u32, - gid: u32, - pid: u32, - total_extlen: u16, - padding: u16, - - pub fn op(h: InHeader) Opcode { - return @enumFromInt(h.opcode); - } -}; - -pub const OutHeader = extern struct { - len: u32, - @"error": i32, - unique: u64, -}; - -pub const Attr = extern struct { - ino: u64 = 0, - size: u64 = 0, - blocks: u64 = 0, - atime: u64 = 0, - mtime: u64 = 0, - ctime: u64 = 0, - atimensec: u32 = 0, - mtimensec: u32 = 0, - ctimensec: u32 = 0, - mode: u32 = 0, - nlink: u32 = 0, - uid: u32 = 0, - gid: u32 = 0, - rdev: u32 = 0, - blksize: u32 = 0, - flags: u32 = 0, -}; - -pub const EntryOut = extern struct { - nodeid: u64 = 0, - generation: u64 = 0, - entry_valid: u64 = 0, - attr_valid: u64 = 0, - entry_valid_nsec: u32 = 0, - attr_valid_nsec: u32 = 0, - attr: Attr = .{}, -}; - -pub const AttrOut = extern struct { - attr_valid: u64 = 0, - attr_valid_nsec: u32 = 0, - dummy: u32 = 0, - attr: Attr = .{}, -}; - -pub const GetattrIn = extern struct { getattr_flags: u32, dummy: u32, fh: u64 }; - -pub const SetattrIn = extern struct { - valid: u32, - padding: u32, - fh: u64, - size: u64, - lock_owner: u64, - atime: u64, - mtime: u64, - ctime: u64, - atimensec: u32, - mtimensec: u32, - ctimensec: u32, - mode: u32, - unused4: u32, - uid: u32, - gid: u32, - unused5: u32, -}; - -pub const OpenIn = extern struct { flags: u32, open_flags: u32 }; -pub const OpenOut = extern struct { fh: u64 = 0, open_flags: u32 = 0, backing_id: i32 = 0 }; -pub const ReleaseIn = extern struct { fh: u64, flags: u32, release_flags: u32, lock_owner: u64 }; -pub const FlushIn = extern struct { fh: u64, unused: u32, padding: u32, lock_owner: u64 }; - -pub const ReadIn = extern struct { - fh: u64, - offset: u64, - size: u32, - read_flags: u32, - lock_owner: u64, - flags: u32, - padding: u32, -}; - -pub const WriteIn = extern struct { - fh: u64, - offset: u64, - size: u32, - write_flags: u32, - lock_owner: u64, - flags: u32, - padding: u32, -}; - -pub const WriteOut = extern struct { size: u32, padding: u32 = 0 }; -pub const CreateIn = extern struct { flags: u32, mode: u32, umask: u32, open_flags: u32 }; -pub const MkdirIn = extern struct { mode: u32, umask: u32 }; -pub const RenameIn = extern struct { newdir: u64 }; -pub const Rename2In = extern struct { newdir: u64, flags: u32, padding: u32 }; -pub const ForgetIn = extern struct { nlookup: u64 }; -pub const BatchForgetIn = extern struct { count: u32, dummy: u32 }; -pub const ForgetOne = extern struct { nodeid: u64, nlookup: u64 }; -pub const FsyncIn = extern struct { fh: u64, fsync_flags: u32, padding: u32 }; -pub const AccessIn = extern struct { mask: u32, padding: u32 }; -pub const InterruptIn = extern struct { unique: u64 }; -pub const LseekIn = extern struct { fh: u64, offset: u64, whence: u32, padding: u32 }; - -pub const Kstatfs = extern struct { - blocks: u64 = 0, - bfree: u64 = 0, - bavail: u64 = 0, - files: u64 = 0, - ffree: u64 = 0, - bsize: u32 = 0, - namelen: u32 = 0, - frsize: u32 = 0, - padding: u32 = 0, - spare: [6]u32 = [_]u32{0} ** 6, -}; - -pub const StatfsOut = extern struct { st: Kstatfs = .{} }; - -pub const InitIn = extern struct { - major: u32, - minor: u32, - max_readahead: u32, - flags: u32, - flags2: u32, - unused: [11]u32, -}; - -/// 64 bytes; the kernel accepts this size whenever the answered minor >= 23. -pub const InitOut = extern struct { - major: u32 = kernel_version, - minor: u32 = kernel_minor, - max_readahead: u32 = 0, - flags: u32 = 0, - max_background: u16 = 0, - congestion_threshold: u16 = 0, - max_write: u32 = 0, - time_gran: u32 = 0, - max_pages: u16 = 0, - map_alignment: u16 = 0, - flags2: u32 = 0, - max_stack_depth: u32 = 0, - request_timeout: u16 = 0, - unused: [11]u16 = [_]u16{0} ** 11, -}; - -/// Fixed 24-byte head of `fuse_dirent`; the name follows, padded to 8 bytes. -pub const Dirent = extern struct { ino: u64, off: u64, namelen: u32, type: u32 }; - -comptime { - std.debug.assert(@sizeOf(InHeader) == 40); - std.debug.assert(@sizeOf(OutHeader) == 16); - std.debug.assert(@sizeOf(Attr) == 88); - std.debug.assert(@sizeOf(EntryOut) == 128); - std.debug.assert(@sizeOf(AttrOut) == 104); - std.debug.assert(@sizeOf(GetattrIn) == 16); - std.debug.assert(@sizeOf(SetattrIn) == 88); - std.debug.assert(@sizeOf(OpenIn) == 8); - std.debug.assert(@sizeOf(OpenOut) == 16); - std.debug.assert(@sizeOf(ReleaseIn) == 24); - std.debug.assert(@sizeOf(FlushIn) == 24); - std.debug.assert(@sizeOf(ReadIn) == 40); - std.debug.assert(@sizeOf(WriteIn) == 40); - std.debug.assert(@sizeOf(WriteOut) == 8); - std.debug.assert(@sizeOf(CreateIn) == 16); - std.debug.assert(@sizeOf(MkdirIn) == 8); - std.debug.assert(@sizeOf(RenameIn) == 8); - std.debug.assert(@sizeOf(Rename2In) == 16); - std.debug.assert(@sizeOf(ForgetIn) == 8); - std.debug.assert(@sizeOf(BatchForgetIn) == 8); - std.debug.assert(@sizeOf(ForgetOne) == 16); - std.debug.assert(@sizeOf(FsyncIn) == 16); - std.debug.assert(@sizeOf(AccessIn) == 8); - std.debug.assert(@sizeOf(InterruptIn) == 8); - std.debug.assert(@sizeOf(Kstatfs) == 80); - std.debug.assert(@sizeOf(StatfsOut) == 80); - std.debug.assert(@sizeOf(InitIn) == 64); - std.debug.assert(@sizeOf(InitOut) == 64); - std.debug.assert(@sizeOf(Dirent) == 24); - std.debug.assert(@sizeOf(LseekIn) == 24); -} - -// --------------------------------------------------------------------------- -// Request / reply helpers -// --------------------------------------------------------------------------- - -pub const Error = error{ Protocol, Io, TooManyPayloads }; - -pub const Request = struct { - header: InHeader, - /// Bytes after the header; a slice into the caller's buffer. - body: []const u8, -}; - -/// Reads one kernel request with a single `read(2)`. Returns null on ENODEV -/// (unmounted). Retries EINTR/EAGAIN/ENOENT. `buf` should be at least -/// `max_write + 4096` bytes and 8-byte aligned so `body()` can view it. -pub fn readRequest(fd: i32, buf: []u8) Error!?Request { - while (true) { - const rc = linux.read(fd, buf.ptr, buf.len); - switch (linux.errno(rc)) { - .SUCCESS => { - const n: usize = rc; - if (n < @sizeOf(InHeader)) return error.Protocol; - const header = std.mem.bytesToValue(InHeader, buf[0..@sizeOf(InHeader)]); - if (header.len != n) return error.Protocol; - return .{ .header = header, .body = buf[@sizeOf(InHeader)..n] }; - }, - .INTR, .AGAIN, .NOENT => continue, - .NODEV => return null, - else => return error.Io, - } - } -} - -/// Maximum number of payload slices a single `reply` can carry. -pub const max_payloads = 7; - -/// Success reply: `OutHeader` followed by the concatenated `payloads`, sent in -/// one `writev(2)`. An ENOENT from the kernel means the request was -/// interrupted; the reply is dropped and this returns normally. -pub fn reply(fd: i32, unique: u64, payloads: []const []const u8) Error!void { - if (payloads.len > max_payloads) return error.TooManyPayloads; - var total: usize = @sizeOf(OutHeader); - for (payloads) |p| total += p.len; - if (total > std.math.maxInt(u32)) return error.Protocol; - const header = OutHeader{ .len = @intCast(total), .@"error" = 0, .unique = unique }; - var iov: [max_payloads + 1]std.posix.iovec_const = undefined; - iov[0] = .{ .base = @ptrCast(&header), .len = @sizeOf(OutHeader) }; - for (payloads, 1..) |p, i| iov[i] = .{ .base = p.ptr, .len = p.len }; - return writeAll(fd, &iov, payloads.len + 1, total); -} - -/// Error reply: an `OutHeader` carrying `-errno` and no payload. -pub fn replyError(fd: i32, unique: u64, err: linux.E) Error!void { - const code: i32 = @intCast(@intFromEnum(err)); - const header = OutHeader{ .len = @sizeOf(OutHeader), .@"error" = -code, .unique = unique }; - var iov = [_]std.posix.iovec_const{.{ .base = @ptrCast(&header), .len = @sizeOf(OutHeader) }}; - return writeAll(fd, &iov, 1, @sizeOf(OutHeader)); -} - -fn writeAll(fd: i32, iov: [*]const std.posix.iovec_const, count: usize, total: usize) Error!void { - while (true) { - const rc = linux.writev(fd, iov, count); - switch (linux.errno(rc)) { - .SUCCESS => return if (rc == total) {} else error.Protocol, - .INTR => continue, - .NOENT => return, // request was interrupted; reply dropped - else => return error.Io, - } - } -} - -/// Appends a `fuse_dirent` (head + name, padded to a multiple of 8) at -/// `buf[used.*..]`. Returns false and leaves `buf`/`used` unchanged if the -/// record does not fit. -pub fn addDirent(buf: []u8, used: *usize, ino: u64, off: u64, dtype: u32, name: []const u8) bool { - const raw = @sizeOf(Dirent) + name.len; - const rec = (raw + 7) & ~@as(usize, 7); - if (used.* > buf.len or buf.len - used.* < rec) return false; - const dst = buf[used.*..][0..rec]; - const head = Dirent{ .ino = ino, .off = off, .namelen = @intCast(name.len), .type = dtype }; - @memcpy(dst[0..@sizeOf(Dirent)], std.mem.asBytes(&head)); - @memcpy(dst[@sizeOf(Dirent)..raw], name); - @memset(dst[raw..rec], 0); - used.* += rec; - return true; -} - -/// Views the first `@sizeOf(T)` bytes of `req.body` as `T` (copy-free). -/// Fails with `error.Protocol` if the body is too short or misaligned. -pub fn body(comptime T: type, req: Request) Error!*const T { - if (req.body.len < @sizeOf(T)) return error.Protocol; - if (@intFromPtr(req.body.ptr) % @alignOf(T) != 0) return error.Protocol; - return @ptrCast(@alignCast(req.body.ptr)); -} - -/// The NUL-terminated string at `req.body[offset..]`, without the NUL. -pub fn nameAt(req: Request, offset: usize) Error![]const u8 { - if (offset > req.body.len) return error.Protocol; - const rest = req.body[offset..]; - const end = std.mem.indexOfScalar(u8, rest, 0) orelse return error.Protocol; - return rest[0..end]; -} - -/// The NUL-terminated string following a `T` body (or at offset 0 when -/// `T == void`), e.g. LOOKUP's name (`void`) or MKDIR's name (`MkdirIn`). -pub fn nameAfter(comptime T: type, req: Request) Error![]const u8 { - const offset = if (T == void) 0 else @sizeOf(T); - return nameAt(req, offset); -} - -/// The string that follows `first` (obtained via `nameAt(req, offset)`), -/// for "old\0new\0" pairs such as RENAME's. -pub fn secondName(req: Request, first: []const u8, offset: usize) Error![]const u8 { - return nameAt(req, offset + first.len + 1); -} - -/// Builds the INIT reply per docs/DESIGN.md. -pub fn initReply(in: *const InitIn, max_write: u32) InitOut { - var out = InitOut{ - .major = kernel_version, - .minor = @min(kernel_minor, in.minor), - .max_readahead = in.max_readahead, - .flags = FUSE_ASYNC_READ | FUSE_ATOMIC_O_TRUNC | FUSE_BIG_WRITES | FUSE_AUTO_INVAL_DATA, - .max_background = 16, - .congestion_threshold = 12, - .max_write = max_write, - .time_gran = 1, - }; - if (in.flags & FUSE_MAX_PAGES != 0) { - out.flags |= FUSE_MAX_PAGES; - out.max_pages = 256; - } - return out; -} - -// --------------------------------------------------------------------------- -// Tests -// --------------------------------------------------------------------------- - -const testing = std.testing; - -test "struct sizes match linux/fuse.h" { - // The comptime block above is the real check; this makes it run under - // `zig test` even if the module is otherwise unreferenced. - try testing.expectEqual(@as(usize, 40), @sizeOf(InHeader)); - try testing.expectEqual(@as(usize, 64), @sizeOf(InitOut)); - try testing.expectEqual(@as(usize, 24), @sizeOf(Dirent)); - try testing.expectEqual(@as(u32, 26), @intFromEnum(Opcode.init)); - try testing.expectEqual(Opcode.statx, @as(Opcode, @enumFromInt(52))); -} - -test "addDirent pads records to 8 bytes and refuses when full" { - var buf: [1024]u8 = undefined; - var used: usize = 0; - const name = "abcdefghijklmnopq"; // 17 chars - var expect_total: usize = 0; - var n: usize = 1; - while (n <= 17) : (n += 1) { - const before = used; - try testing.expect(addDirent(&buf, &used, n, n, DT_REG, name[0..n])); - const rec = used - before; - try testing.expectEqual(@as(usize, 0), rec % 8); - try testing.expectEqual((24 + n + 7) & ~@as(usize, 7), rec); - // check head fields and NUL padding - const head = std.mem.bytesToValue(Dirent, buf[before..][0..24]); - try testing.expectEqual(n, head.ino); - try testing.expectEqual(@as(u32, @intCast(n)), head.namelen); - try testing.expectEqualStrings(name[0..n], buf[before + 24 ..][0..n]); - for (buf[before + 24 + n .. used]) |b| try testing.expectEqual(@as(u8, 0), b); - expect_total += rec; - } - try testing.expectEqual(expect_total, used); - - // A record that does not fit leaves everything untouched. - var small: [40]u8 = undefined; - var used2: usize = 0; - try testing.expect(addDirent(&small, &used2, 1, 1, DT_DIR, "0123456789abcdef")); // 24+16 = 40 - try testing.expectEqual(@as(usize, 40), used2); - try testing.expect(!addDirent(&small, &used2, 2, 2, DT_DIR, "x")); - try testing.expectEqual(@as(usize, 40), used2); - var tight: [31]u8 = undefined; - var used3: usize = 0; - try testing.expect(!addDirent(&tight, &used3, 1, 1, DT_REG, "a")); // needs 32 - try testing.expectEqual(@as(usize, 0), used3); -} - -test "body/nameAfter/secondName on hand-built requests" { - var buf: [128]u8 align(8) = undefined; - // LOOKUP(parent=1, "hello") - const name = "hello"; - const hdr = InHeader{ - .len = @intCast(@sizeOf(InHeader) + name.len + 1), - .opcode = @intFromEnum(Opcode.lookup), - .unique = 7, - .nodeid = root_id, - .uid = 1000, - .gid = 1000, - .pid = 42, - .total_extlen = 0, - .padding = 0, - }; - @memcpy(buf[0..40], std.mem.asBytes(&hdr)); - @memcpy(buf[40..45], name); - buf[45] = 0; - const req = Request{ .header = hdr, .body = buf[40..hdr.len] }; - try testing.expectEqual(Opcode.lookup, req.header.op()); - try testing.expectEqualStrings("hello", try nameAfter(void, req)); - try testing.expectError(error.Protocol, body(MkdirIn, Request{ .header = hdr, .body = buf[40..44] })); - - // MKDIR(mode=0o755) + "dir" - const mk = MkdirIn{ .mode = 0o755, .umask = 0o22 }; - @memcpy(buf[40..48], std.mem.asBytes(&mk)); - @memcpy(buf[48..51], "dir"); - buf[51] = 0; - const mreq = Request{ .header = hdr, .body = buf[40..52] }; - const got = try body(MkdirIn, mreq); - try testing.expectEqual(@as(u32, 0o755), got.mode); - try testing.expectEqualStrings("dir", try nameAfter(MkdirIn, mreq)); - - // RENAME(newdir) + "old\0new\0" - const rn = RenameIn{ .newdir = 9 }; - @memcpy(buf[40..48], std.mem.asBytes(&rn)); - @memcpy(buf[48..56], "old\x00new\x00"); - const rreq = Request{ .header = hdr, .body = buf[40..56] }; - try testing.expectEqual(@as(u64, 9), (try body(RenameIn, rreq)).newdir); - const old = try nameAfter(RenameIn, rreq); - try testing.expectEqualStrings("old", old); - try testing.expectEqualStrings("new", try secondName(rreq, old, @sizeOf(RenameIn))); - try testing.expectError(error.Protocol, secondName(rreq, "new", @sizeOf(RenameIn) + 4)); - - // Missing NUL and misalignment are protocol errors. - try testing.expectError(error.Protocol, nameAt(Request{ .header = hdr, .body = buf[48..51] }, 0)); - try testing.expectError(error.Protocol, body(MkdirIn, Request{ .header = hdr, .body = buf[41..57] })); -} - -test "initReply fields" { - var in = InitIn{ .major = 7, .minor = 45, .max_readahead = 131072, .flags = 0, .flags2 = 0, .unused = [_]u32{0} ** 11 }; - const a = initReply(&in, 1 << 20); - try testing.expectEqual(@as(u32, 7), a.major); - try testing.expectEqual(@as(u32, 31), a.minor); - try testing.expectEqual(@as(u32, 131072), a.max_readahead); - try testing.expectEqual(FUSE_ASYNC_READ | FUSE_ATOMIC_O_TRUNC | FUSE_BIG_WRITES | FUSE_AUTO_INVAL_DATA, a.flags); - try testing.expectEqual(@as(u16, 0), a.max_pages); - try testing.expectEqual(@as(u16, 16), a.max_background); - try testing.expectEqual(@as(u16, 12), a.congestion_threshold); - try testing.expectEqual(@as(u32, 1 << 20), a.max_write); - try testing.expectEqual(@as(u32, 1), a.time_gran); - - in.flags = FUSE_MAX_PAGES | FUSE_ASYNC_READ; - in.minor = 27; - const b = initReply(&in, 4096); - try testing.expectEqual(@as(u32, 27), b.minor); - try testing.expectEqual(FUSE_ASYNC_READ | FUSE_ATOMIC_O_TRUNC | FUSE_BIG_WRITES | FUSE_AUTO_INVAL_DATA | FUSE_MAX_PAGES, b.flags); - try testing.expectEqual(@as(u16, 256), b.max_pages); - try testing.expectEqual(@as(u32, 4096), b.max_write); -} - -fn makePipe() ![2]i32 { - var fds: [2]i32 = undefined; - if (linux.errno(linux.pipe2(&fds, .{ .CLOEXEC = true })) != .SUCCESS) return error.Io; - return fds; -} - -fn readExact(fd: i32, out: []u8) !void { - var got: usize = 0; - while (got < out.len) { - const rc = linux.read(fd, out[got..].ptr, out.len - got); - if (linux.errno(rc) != .SUCCESS or rc == 0) return error.Io; - got += rc; - } -} - -test "reply writes header + payloads through a pipe" { - const fds = try makePipe(); - defer _ = linux.close(fds[0]); - defer _ = linux.close(fds[1]); - - const oo = OpenOut{ .fh = 0x1234, .open_flags = FOPEN_DIRECT_IO }; - try reply(fds[1], 99, &.{ std.mem.asBytes(&oo), "tail" }); - - var out: [16 + 16 + 4]u8 = undefined; - try readExact(fds[0], &out); - const h = std.mem.bytesToValue(OutHeader, out[0..16]); - try testing.expectEqual(@as(u32, 36), h.len); - try testing.expectEqual(@as(i32, 0), h.@"error"); - try testing.expectEqual(@as(u64, 99), h.unique); - try testing.expectEqualSlices(u8, std.mem.asBytes(&oo), out[16..32]); - try testing.expectEqualStrings("tail", out[32..36]); - - // Empty payload list: header only. - try reply(fds[1], 5, &.{}); - var only: [16]u8 = undefined; - try readExact(fds[0], &only); - try testing.expectEqual(@as(u32, 16), std.mem.bytesToValue(OutHeader, &only).len); - - var too_many: [max_payloads + 1][]const u8 = undefined; - for (&too_many) |*p| p.* = "x"; - try testing.expectError(error.TooManyPayloads, reply(fds[1], 1, &too_many)); -} - -test "replyError writes a negative errno" { - const fds = try makePipe(); - defer _ = linux.close(fds[0]); - defer _ = linux.close(fds[1]); - - try replyError(fds[1], 0xdead_beef, .NOENT); - var out: [16]u8 = undefined; - try readExact(fds[0], &out); - const h = std.mem.bytesToValue(OutHeader, &out); - try testing.expectEqual(@as(u32, 16), h.len); - try testing.expectEqual(@as(i32, -2), h.@"error"); - try testing.expectEqual(@as(u64, 0xdead_beef), h.unique); - - try replyError(fds[1], 1, .NOSYS); - try readExact(fds[0], &out); - try testing.expectEqual(-@as(i32, @intCast(@intFromEnum(linux.E.NOSYS))), std.mem.bytesToValue(OutHeader, &out).@"error"); -} - -test "readRequest parses one request from a pipe and rejects bad lengths" { - const fds = try makePipe(); - defer _ = linux.close(fds[0]); - defer _ = linux.close(fds[1]); - - var wire: [48]u8 align(8) = undefined; - const hdr = InHeader{ .len = 48, .opcode = @intFromEnum(Opcode.forget), .unique = 3, .nodeid = 2, .uid = 0, .gid = 0, .pid = 0, .total_extlen = 0, .padding = 0 }; - @memcpy(wire[0..40], std.mem.asBytes(&hdr)); - @memcpy(wire[40..48], std.mem.asBytes(&ForgetIn{ .nlookup = 11 })); - try testing.expectEqual(@as(usize, 48), linux.write(fds[1], &wire, wire.len)); - - var buf: [4096]u8 align(8) = undefined; - const req = (try readRequest(fds[0], &buf)) orelse return error.Io; - try testing.expectEqual(Opcode.forget, req.header.op()); - try testing.expectEqual(@as(u64, 2), req.header.nodeid); - try testing.expectEqual(@as(u64, 11), (try body(ForgetIn, req)).nlookup); - - // Header length disagreeing with what was read is a protocol error. - var bad = wire; - std.mem.bytesAsValue(InHeader, bad[0..40]).len = 40; - try testing.expectEqual(@as(usize, 48), linux.write(fds[1], &bad, bad.len)); - try testing.expectError(error.Protocol, readRequest(fds[0], &buf)); -} -- cgit v1.3