diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-19 23:28:22 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-09-19 23:28:22 -0300 |
| commit | ba996acfcad1698adbf4a1834fe50e73b1c6cab9 (patch) | |
| tree | 282ba00ce5b10d7416aecb9f2f0f0a439340a57d /9ns/src/fuse.zig | |
| parent | b05abcba3ea09ea106ad28364c6e40a3ec31b890 (diff) | |
| download | cloud9-ba996acfcad1698adbf4a1834fe50e73b1c6cab9.tar.gz cloud9-ba996acfcad1698adbf4a1834fe50e73b1c6cab9.zip | |
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 <[email protected]>
Diffstat (limited to '9ns/src/fuse.zig')
| -rw-r--r-- | 9ns/src/fuse.zig | 653 |
1 files changed, 653 insertions, 0 deletions
diff --git a/9ns/src/fuse.zig b/9ns/src/fuse.zig new file mode 100644 index 0000000..682b3d5 --- /dev/null +++ b/9ns/src/fuse.zig @@ -0,0 +1,653 @@ +//! 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 9ns 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)); +} |
