summaryrefslogtreecommitdiff
path: root/9player/src/fuse.zig
diff options
context:
space:
mode:
Diffstat (limited to '9player/src/fuse.zig')
-rw-r--r--9player/src/fuse.zig653
1 files changed, 0 insertions, 653 deletions
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));
-}