//! 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; /// Without this the kernel takes the directory's inode lock around every /// LOOKUP and READDIR in it, so one walk parked on a server that never /// answers holds up every other name in the same directory — the whole /// registry root, for a mntgen mount. With it, walks into different names /// proceed side by side and only the parked one waits. pub const FUSE_PARALLEL_DIROPS: u32 = 1 << 18; 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, Retry }; 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) { return readRequestOnce(fd, buf) catch |e| switch (e) { error.Retry => continue, else => return e, }; } } /// One `read(2)` attempt: like `readRequest` but EINTR/EAGAIN/ENOENT surface as /// `error.Retry` instead of being retried, so a caller that only reads after /// `poll` (or on a non-blocking fd) never blocks in here. pub fn readRequestOnce(fd: i32, buf: []u8) Error!?Request { 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 => return error.Retry, .NODEV => return null, else => return error.Io, } } /// Sets O_NONBLOCK on `fd` so that a read after `poll` cannot block when the /// kernel withdrew the request in between (a killed waiter, for instance). pub fn setNonblocking(fd: i32) Error!void { const cur = linux.fcntl(fd, linux.F.GETFL, 0); if (linux.errno(cur) != .SUCCESS) return error.Io; const nonblock: u32 = @bitCast(linux.O{ .NONBLOCK = true }); const rc = linux.fcntl(fd, linux.F.SETFL, @as(usize, cur) | nonblock); if (linux.errno(rc) != .SUCCESS) 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)); } /// Notification codes, sent to the kernel unsolicited: they ride an /// `OutHeader` with `unique = 0` and the code (positive) in `error`. pub const notify_inval_entry: i32 = 3; /// The body of a `FUSE_NOTIFY_INVAL_ENTRY`, followed by the name and a NUL. pub const NotifyInvalEntryOut = extern struct { parent: u64, namelen: u32, padding: u32 = 0, }; /// Drops the kernel's cached dentry for `name` under `parent`, so the next /// access of that path comes back as a fresh LOOKUP instead of reusing a /// node id the server no longer knows. /// /// `unique = 0` marks the message as a notification rather than a reply, so /// it is safe to interleave with replies on the same fd, from any thread. /// /// Best-effort by nature: the kernel answers ENOENT when it had nothing /// cached under that name (`writeAll` already treats that as success) and /// EINVAL when it does not support the notification, which surfaces here as /// `error.Io`. Callers ignore both — a notification that does not land /// leaves them exactly where they were without it. pub fn notifyInvalEntry(fd: i32, parent: u64, name: []const u8) Error!void { if (name.len == 0 or name.len > std.math.maxInt(u32) - 1) return error.Protocol; const out = NotifyInvalEntryOut{ .parent = parent, .namelen = @intCast(name.len) }; const total = @sizeOf(OutHeader) + @sizeOf(NotifyInvalEntryOut) + name.len + 1; const header = OutHeader{ .len = @intCast(total), .@"error" = notify_inval_entry, .unique = 0 }; const nul = [_]u8{0}; var iov = [_]std.posix.iovec_const{ .{ .base = @ptrCast(&header), .len = @sizeOf(OutHeader) }, .{ .base = @ptrCast(&out), .len = @sizeOf(NotifyInvalEntryOut) }, .{ .base = name.ptr, .len = name.len }, .{ .base = &nul, .len = 1 }, }; return writeAll(fd, &iov, iov.len, total); } 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; } if (in.flags & FUSE_PARALLEL_DIROPS != 0) out.flags |= FUSE_PARALLEL_DIROPS; 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)); // Non-blocking and empty: readRequestOnce reports Retry instead of waiting. try setNonblocking(fds[0]); try testing.expectError(error.Retry, readRequestOnce(fds[0], &buf)); try testing.expectEqual(@as(usize, 48), linux.write(fds[1], &wire, wire.len)); const again = (try readRequestOnce(fds[0], &buf)) orelse return error.Io; try testing.expectEqual(@as(u64, 3), again.header.unique); try testing.expectError(error.Retry, readRequestOnce(fds[0], &buf)); }