summaryrefslogtreecommitdiff
path: root/src/wire.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/wire.zig')
-rw-r--r--src/wire.zig1153
1 files changed, 1153 insertions, 0 deletions
diff --git a/src/wire.zig b/src/wire.zig
new file mode 100644
index 0000000..e093450
--- /dev/null
+++ b/src/wire.zig
@@ -0,0 +1,1153 @@
+//! Base 9P2000 wire format. Decoded strings and data borrow the input frame.
+//! Encoding performs a complete size check before writing the caller-owned buffer.
+const std = @import("std");
+const assert = std.debug.assert;
+
+pub const Error = error{
+ Truncated,
+ Overlong,
+ BadTag,
+ BadValue,
+ Trailing,
+ NoSpace,
+};
+
+pub const Type = enum(u8) {
+ tversion = 100,
+ rversion = 101,
+ tauth = 102,
+ rauth = 103,
+ tattach = 104,
+ rattach = 105,
+ terror = 106,
+ rerror = 107,
+ tflush = 108,
+ rflush = 109,
+ twalk = 110,
+ rwalk = 111,
+ topen = 112,
+ ropen = 113,
+ tcreate = 114,
+ rcreate = 115,
+ tread = 116,
+ rread = 117,
+ twrite = 118,
+ rwrite = 119,
+ tclunk = 120,
+ rclunk = 121,
+ tremove = 122,
+ rremove = 123,
+ tstat = 124,
+ rstat = 125,
+ twstat = 126,
+ rwstat = 127,
+ _,
+};
+
+pub fn isT(t: Type) bool {
+ return @intFromEnum(t) % 2 == 0;
+}
+
+pub const header_len: usize = 4 + 1 + 2;
+
+pub const qid_len: usize = 1 + 4 + 8;
+
+pub const stat_fixed: usize = 2 + qid_len + 5 * 2 + 4 * 4 + 8;
+
+pub const notag: u16 = 0xFFFF;
+
+pub const nofid: u32 = 0xFFFF_FFFF;
+
+pub const max_welem: usize = 16;
+
+const test_msize: u32 = 4096;
+
+pub const iohdrsz: u32 = 24;
+
+pub const qtdir: u8 = 0x80;
+pub const qtappend: u8 = 0x40;
+pub const qtexcl: u8 = 0x20;
+pub const qtmount: u8 = 0x10;
+pub const qtauth: u8 = 0x08;
+pub const qttmp: u8 = 0x04;
+pub const qtfile: u8 = 0x00;
+
+pub const dmdir: u32 = 0x8000_0000;
+pub const dmappend: u32 = 0x4000_0000;
+pub const dmexcl: u32 = 0x2000_0000;
+pub const dmmount: u32 = 0x1000_0000;
+pub const dmauth: u32 = 0x0800_0000;
+pub const dmtmp: u32 = 0x0400_0000;
+pub const dmperm: u32 = 0o777;
+
+comptime {
+ assert(header_len == 7);
+ assert(qid_len == 13);
+ assert(stat_fixed == 49);
+ for (std.enums.values(Type)) |t| {
+ const even = @intFromEnum(t) % 2 == 0;
+ assert(isT(t) == even);
+ assert(std.mem.startsWith(u8, @tagName(t), if (even) "t" else "r"));
+ }
+}
+
+pub const Qid = struct {
+ type: u8,
+ version: u32,
+ path: u64,
+
+ pub fn encode(self: Qid, buf: []u8) Error![]u8 {
+ if (buf.len < qid_len) return error.NoSpace;
+ buf[0] = self.type;
+ std.mem.writeInt(u32, buf[1..5], self.version, .little);
+ std.mem.writeInt(u64, buf[5..13], self.path, .little);
+ return buf[0..qid_len];
+ }
+
+ pub fn decode(bytes: []const u8) Error!Qid {
+ if (bytes.len < qid_len) return error.Truncated;
+ return .{
+ .type = bytes[0],
+ .version = std.mem.readInt(u32, bytes[1..5], .little),
+ .path = std.mem.readInt(u64, bytes[5..13], .little),
+ };
+ }
+};
+
+pub const Stat = struct {
+ type: u16,
+ dev: u32,
+ qid: Qid,
+ mode: u32,
+ atime: u32,
+ mtime: u32,
+ length: u64,
+ name: []const u8,
+ uid: []const u8,
+ gid: []const u8,
+ muid: []const u8,
+
+ pub fn size(self: Stat) Error!u16 {
+ const n =
+ 2 + // type
+ 4 + // dev
+ qid_len + // qid: type[1] version[4] path[8]
+ 4 + // mode
+ 4 + // atime
+ 4 + // mtime
+ 8 + // length
+ try stringLen(self.name) +
+ try stringLen(self.uid) +
+ try stringLen(self.gid) +
+ try stringLen(self.muid);
+ assert(n >= stat_fixed - 2);
+ if (n > std.math.maxInt(u16)) return error.Overlong;
+ return @intCast(n);
+ }
+
+ pub fn encode(self: Stat, buf: []u8) Error![]u8 {
+ const n = try self.size();
+ const total = @as(usize, n) + 2;
+ if (buf.len < total) return error.NoSpace;
+ var w: Writer = .init(buf[0..total]);
+ try w.putU16(n);
+ try w.putU16(self.type);
+ try w.putU32(self.dev);
+ try w.putQid(self.qid);
+ try w.putU32(self.mode);
+ try w.putU32(self.atime);
+ try w.putU32(self.mtime);
+ try w.putU64(self.length);
+ try w.putString(self.name);
+ try w.putString(self.uid);
+ try w.putString(self.gid);
+ try w.putString(self.muid);
+ assert(w.n == total);
+ return buf[0..total];
+ }
+
+ pub fn decode(bytes: []const u8) Error!Stat {
+ var r: Reader = .init(bytes);
+ const n = try r.getU16();
+ const body = bytes.len - 2;
+ if (n > body) return error.Truncated;
+ if (n < body) return error.Trailing;
+ const self: Stat = .{
+ .type = try r.getU16(),
+ .dev = try r.getU32(),
+ .qid = try r.getQid(),
+ .mode = try r.getU32(),
+ .atime = try r.getU32(),
+ .mtime = try r.getU32(),
+ .length = try r.getU64(),
+ .name = try r.getString(),
+ .uid = try r.getString(),
+ .gid = try r.getString(),
+ .muid = try r.getString(),
+ };
+ try r.end();
+ return self;
+ }
+};
+
+pub const Msg = union(enum) {
+ tversion: struct { msize: u32, version: []const u8 },
+ rversion: struct { msize: u32, version: []const u8 },
+
+ tauth: struct { afid: u32, uname: []const u8, aname: []const u8 },
+ rauth: struct { aqid: Qid },
+
+ tattach: struct { fid: u32, afid: u32, uname: []const u8, aname: []const u8 },
+ rattach: struct { qid: Qid },
+
+ rerror: struct { ename: []const u8 },
+
+ tflush: struct { oldtag: u16 },
+ rflush: void,
+
+ twalk: struct {
+ fid: u32,
+ newfid: u32,
+ nwname: u16,
+ wname: [max_welem][]const u8 = @splat(""),
+ },
+ rwalk: struct {
+ nwqid: u16,
+ wqid: [max_welem]Qid = @splat(.{ .type = 0, .version = 0, .path = 0 }),
+ },
+
+ topen: struct { fid: u32, mode: u8 },
+ ropen: struct { qid: Qid, iounit: u32 },
+
+ tcreate: struct { fid: u32, name: []const u8, perm: u32, mode: u8 },
+ rcreate: struct { qid: Qid, iounit: u32 },
+
+ tread: struct { fid: u32, offset: u64, count: u32 },
+ rread: struct { data: []const u8 },
+
+ twrite: struct { fid: u32, offset: u64, data: []const u8 },
+ rwrite: struct { count: u32 },
+
+ tclunk: struct { fid: u32 },
+ rclunk: void,
+ tremove: struct { fid: u32 },
+ rremove: void,
+
+ tstat: struct { fid: u32 },
+ rstat: struct { stat: Stat },
+ twstat: struct { fid: u32, stat: Stat },
+ rwstat: void,
+
+ pub fn msgType(msg: Msg) Type {
+ return switch (msg) {
+ inline else => |_, t| @field(Type, @tagName(t)),
+ };
+ }
+};
+
+pub const Decoded = struct {
+ tag: u16,
+ msg: Msg,
+};
+
+pub fn frameLen(prefix: []const u8) ?u32 {
+ if (prefix.len < 4) return null;
+ return std.mem.readInt(u32, prefix[0..4], .little);
+}
+
+pub fn encodedLen(msg: Msg) Error!usize {
+ const body: u64 = switch (msg) {
+ .tversion => |m| 4 + try stringLen(m.version),
+ .rversion => |m| 4 + try stringLen(m.version),
+ .tauth => |m| 4 + try stringLen(m.uname) + try stringLen(m.aname),
+ .rauth => qid_len,
+ .tattach => |m| 4 + 4 + try stringLen(m.uname) + try stringLen(m.aname),
+ .rattach => qid_len,
+ .rerror => |m| try stringLen(m.ename),
+ .tflush => 2,
+ .rflush => 0,
+ .twalk => |m| blk: {
+ if (m.nwname > max_welem) return error.Overlong;
+ var n: usize = 4 + 4 + 2;
+ for (m.wname[0..m.nwname]) |name| n += try stringLen(name);
+ break :blk n;
+ },
+ .rwalk => |m| blk: {
+ if (m.nwqid > max_welem) return error.Overlong;
+ break :blk 2 + @as(usize, m.nwqid) * qid_len;
+ },
+ .topen => 4 + 1,
+ .ropen => qid_len + 4,
+ .tcreate => |m| 4 + try stringLen(m.name) + 4 + 1,
+ .rcreate => qid_len + 4,
+ .tread => 4 + 8 + 4,
+ .rread => |m| try dataLen(m.data),
+ .twrite => |m| 4 + 8 + try dataLen(m.data),
+ .rwrite => 4,
+ .tclunk => 4,
+ .rclunk => 0,
+ .tremove => 4,
+ .rremove => 0,
+ .tstat => 4,
+ .rstat => |m| 2 + try statLen(m.stat),
+ .twstat => |m| 4 + 2 + try statLen(m.stat),
+ .rwstat => 0,
+ };
+ const total = header_len + body;
+ if (total > std.math.maxInt(u32)) return error.Overlong;
+ return @intCast(total);
+}
+
+fn statLen(stat: Stat) Error!usize {
+ const n = @as(usize, try stat.size()) + 2;
+ if (n > std.math.maxInt(u16)) return error.Overlong;
+ return n;
+}
+
+fn stringLen(s: []const u8) Error!usize {
+ if (s.len > std.math.maxInt(u16)) return error.Overlong;
+ return 2 + s.len;
+}
+
+fn dataLen(d: []const u8) Error!u64 {
+ if (d.len > std.math.maxInt(u32)) return error.Overlong;
+ return 4 + @as(u64, d.len);
+}
+
+pub fn encode(msg: Msg, tag: u16, buf: []u8) Error![]u8 {
+ const total = try encodedLen(msg);
+ if (total > buf.len) return error.NoSpace;
+
+ var w: Writer = .init(buf[0..total]);
+ try w.putU32(@intCast(total));
+ try w.putByte(@intFromEnum(msg.msgType()));
+ try w.putU16(tag);
+
+ switch (msg) {
+ .tversion => |m| {
+ try w.putU32(m.msize);
+ try w.putString(m.version);
+ },
+ .rversion => |m| {
+ try w.putU32(m.msize);
+ try w.putString(m.version);
+ },
+ .tauth => |m| {
+ try w.putU32(m.afid);
+ try w.putString(m.uname);
+ try w.putString(m.aname);
+ },
+ .rauth => |m| try w.putQid(m.aqid),
+ .tattach => |m| {
+ try w.putU32(m.fid);
+ try w.putU32(m.afid);
+ try w.putString(m.uname);
+ try w.putString(m.aname);
+ },
+ .rattach => |m| try w.putQid(m.qid),
+ .rerror => |m| try w.putString(m.ename),
+ .tflush => |m| try w.putU16(m.oldtag),
+ .rflush => {},
+ .twalk => |m| {
+ try w.putU32(m.fid);
+ try w.putU32(m.newfid);
+ try w.putU16(m.nwname);
+ for (m.wname[0..m.nwname]) |name| try w.putString(name);
+ },
+ .rwalk => |m| {
+ try w.putU16(m.nwqid);
+ for (m.wqid[0..m.nwqid]) |qid| try w.putQid(qid);
+ },
+ .topen => |m| {
+ try w.putU32(m.fid);
+ try w.putByte(m.mode);
+ },
+ .ropen => |m| {
+ try w.putQid(m.qid);
+ try w.putU32(m.iounit);
+ },
+ .tcreate => |m| {
+ try w.putU32(m.fid);
+ try w.putString(m.name);
+ try w.putU32(m.perm);
+ try w.putByte(m.mode);
+ },
+ .rcreate => |m| {
+ try w.putQid(m.qid);
+ try w.putU32(m.iounit);
+ },
+ .tread => |m| {
+ try w.putU32(m.fid);
+ try w.putU64(m.offset);
+ try w.putU32(m.count);
+ },
+ .rread => |m| {
+ try w.putU32(@intCast(m.data.len));
+ try w.putBytes(m.data);
+ },
+ .twrite => |m| {
+ try w.putU32(m.fid);
+ try w.putU64(m.offset);
+ try w.putU32(@intCast(m.data.len));
+ try w.putBytes(m.data);
+ },
+ .rwrite => |m| try w.putU32(m.count),
+ .tclunk => |m| try w.putU32(m.fid),
+ .rclunk => {},
+ .tremove => |m| try w.putU32(m.fid),
+ .rremove => {},
+ .tstat => |m| try w.putU32(m.fid),
+ .rstat => |m| {
+ try w.putU16(try m.stat.size() + 2);
+ try w.putStat(m.stat);
+ },
+ .twstat => |m| {
+ try w.putU32(m.fid);
+ try w.putU16(try m.stat.size() + 2);
+ try w.putStat(m.stat);
+ },
+ .rwstat => {},
+ }
+
+ assert(w.n == total);
+ return buf[0..total];
+}
+
+pub fn decode(bytes: []const u8) Error!Decoded {
+ if (bytes.len < header_len) return error.Truncated;
+ const size = std.mem.readInt(u32, bytes[0..4], .little);
+ if (size < header_len) return error.BadValue;
+ if (size > bytes.len) return error.Truncated;
+ if (size < bytes.len) return error.Trailing;
+
+ const t: Type = @enumFromInt(bytes[4]);
+ const tag = std.mem.readInt(u16, bytes[5..7], .little);
+
+ var r: Reader = .init(bytes[header_len..size]);
+
+ const msg: Msg = switch (t) {
+ .tversion => .{ .tversion = .{ .msize = try r.getU32(), .version = try r.getString() } },
+ .rversion => .{ .rversion = .{ .msize = try r.getU32(), .version = try r.getString() } },
+ .tauth => .{ .tauth = .{
+ .afid = try r.getU32(),
+ .uname = try r.getString(),
+ .aname = try r.getString(),
+ } },
+ .rauth => .{ .rauth = .{ .aqid = try r.getQid() } },
+ .tattach => .{ .tattach = .{
+ .fid = try r.getU32(),
+ .afid = try r.getU32(),
+ .uname = try r.getString(),
+ .aname = try r.getString(),
+ } },
+ .rattach => .{ .rattach = .{ .qid = try r.getQid() } },
+ .rerror => .{ .rerror = .{ .ename = try r.getString() } },
+ .tflush => .{ .tflush = .{ .oldtag = try r.getU16() } },
+ .rflush => .rflush,
+ .twalk => blk: {
+ var m: Msg = .{ .twalk = .{
+ .fid = try r.getU32(),
+ .newfid = try r.getU32(),
+ .nwname = try r.getU16(),
+ } };
+ if (m.twalk.nwname > max_welem) return error.Overlong;
+ for (m.twalk.wname[0..m.twalk.nwname]) |*name| name.* = try r.getString();
+ break :blk m;
+ },
+ .rwalk => blk: {
+ var m: Msg = .{ .rwalk = .{ .nwqid = try r.getU16() } };
+ if (m.rwalk.nwqid > max_welem) return error.Overlong;
+ for (m.rwalk.wqid[0..m.rwalk.nwqid]) |*qid| qid.* = try r.getQid();
+ break :blk m;
+ },
+ .topen => .{ .topen = .{ .fid = try r.getU32(), .mode = try r.getByte() } },
+ .ropen => .{ .ropen = .{ .qid = try r.getQid(), .iounit = try r.getU32() } },
+ .tcreate => .{ .tcreate = .{
+ .fid = try r.getU32(),
+ .name = try r.getString(),
+ .perm = try r.getU32(),
+ .mode = try r.getByte(),
+ } },
+ .rcreate => .{ .rcreate = .{ .qid = try r.getQid(), .iounit = try r.getU32() } },
+ .tread => .{ .tread = .{
+ .fid = try r.getU32(),
+ .offset = try r.getU64(),
+ .count = try r.getU32(),
+ } },
+ .rread => .{ .rread = .{ .data = try r.getData() } },
+ .twrite => .{ .twrite = .{
+ .fid = try r.getU32(),
+ .offset = try r.getU64(),
+ .data = try r.getData(),
+ } },
+ .rwrite => .{ .rwrite = .{ .count = try r.getU32() } },
+ .tclunk => .{ .tclunk = .{ .fid = try r.getU32() } },
+ .rclunk => .rclunk,
+ .tremove => .{ .tremove = .{ .fid = try r.getU32() } },
+ .rremove => .rremove,
+ .tstat => .{ .tstat = .{ .fid = try r.getU32() } },
+ .rstat => .{ .rstat = .{ .stat = try Stat.decode(try r.getBlob16()) } },
+ .twstat => .{ .twstat = .{
+ .fid = try r.getU32(),
+ .stat = try Stat.decode(try r.getBlob16()),
+ } },
+ .rwstat => .rwstat,
+ .terror, _ => return error.BadTag,
+ };
+
+ try r.end();
+ return .{ .tag = tag, .msg = msg };
+}
+
+const Writer = struct {
+ buf: []u8,
+ n: usize = 0,
+
+ fn init(buf: []u8) Writer {
+ return .{ .buf = buf };
+ }
+
+ fn room(w: *Writer, k: usize) Error![]u8 {
+ if (w.buf.len - w.n < k) return error.NoSpace;
+ defer w.n += k;
+ return w.buf[w.n..][0..k];
+ }
+
+ fn putByte(w: *Writer, v: u8) Error!void {
+ (try w.room(1))[0] = v;
+ }
+
+ fn putU16(w: *Writer, v: u16) Error!void {
+ std.mem.writeInt(u16, (try w.room(2))[0..2], v, .little);
+ }
+
+ fn putU32(w: *Writer, v: u32) Error!void {
+ std.mem.writeInt(u32, (try w.room(4))[0..4], v, .little);
+ }
+
+ fn putU64(w: *Writer, v: u64) Error!void {
+ std.mem.writeInt(u64, (try w.room(8))[0..8], v, .little);
+ }
+
+ fn putBytes(w: *Writer, v: []const u8) Error!void {
+ const target = try w.room(v.len);
+ // Permit payloads staged at their final position in the output frame.
+ if (target.ptr != v.ptr) @memcpy(target, v);
+ }
+
+ fn putString(w: *Writer, v: []const u8) Error!void {
+ assert(v.len <= std.math.maxInt(u16));
+ try w.putU16(@intCast(v.len));
+ try w.putBytes(v);
+ }
+
+ fn putQid(w: *Writer, v: Qid) Error!void {
+ _ = try v.encode(try w.room(qid_len));
+ }
+
+ fn putStat(w: *Writer, v: Stat) Error!void {
+ const total = @as(usize, try v.size()) + 2;
+ _ = try v.encode(try w.room(total));
+ }
+};
+
+const Reader = struct {
+ bytes: []const u8,
+ i: usize = 0,
+
+ fn init(bytes: []const u8) Reader {
+ return .{ .bytes = bytes };
+ }
+
+ fn take(r: *Reader, n: usize) Error![]const u8 {
+ if (r.bytes.len - r.i < n) return error.Truncated;
+ defer r.i += n;
+ return r.bytes[r.i..][0..n];
+ }
+
+ fn getByte(r: *Reader) Error!u8 {
+ return (try r.take(1))[0];
+ }
+
+ fn getU16(r: *Reader) Error!u16 {
+ return std.mem.readInt(u16, (try r.take(2))[0..2], .little);
+ }
+
+ fn getU32(r: *Reader) Error!u32 {
+ return std.mem.readInt(u32, (try r.take(4))[0..4], .little);
+ }
+
+ fn getU64(r: *Reader) Error!u64 {
+ return std.mem.readInt(u64, (try r.take(8))[0..8], .little);
+ }
+
+ fn getString(r: *Reader) Error![]const u8 {
+ return r.take(try r.getU16());
+ }
+
+ fn getData(r: *Reader) Error![]const u8 {
+ return r.take(try r.getU32());
+ }
+
+ fn getBlob16(r: *Reader) Error![]const u8 {
+ return r.take(try r.getU16());
+ }
+
+ fn getQid(r: *Reader) Error!Qid {
+ return Qid.decode(try r.take(qid_len));
+ }
+
+ fn end(r: *Reader) Error!void {
+ if (r.i != r.bytes.len) return error.Trailing;
+ }
+};
+
+const testing = std.testing;
+
+fn roundTrip(buf: []u8, tag: u16, msg: Msg) !Msg {
+ const bytes = try encode(msg, tag, buf);
+ try testing.expectEqual(bytes.len, frameLen(bytes).?);
+ const got = try decode(bytes);
+ try testing.expectEqual(tag, got.tag);
+ try testing.expectEqual(msg.msgType(), got.msg.msgType());
+ try expectMsgEqual(msg, got.msg);
+ return got.msg;
+}
+
+fn expectStatEqual(want: Stat, have: Stat) !void {
+ try testing.expectEqual(want.type, have.type);
+ try testing.expectEqual(want.dev, have.dev);
+ try testing.expectEqual(want.qid, have.qid);
+ try testing.expectEqual(want.mode, have.mode);
+ try testing.expectEqual(want.atime, have.atime);
+ try testing.expectEqual(want.mtime, have.mtime);
+ try testing.expectEqual(want.length, have.length);
+ try testing.expectEqualStrings(want.name, have.name);
+ try testing.expectEqualStrings(want.uid, have.uid);
+ try testing.expectEqualStrings(want.gid, have.gid);
+ try testing.expectEqualStrings(want.muid, have.muid);
+}
+
+fn expectMsgEqual(want: Msg, have: Msg) !void {
+ switch (want) {
+ .tversion => |w| {
+ try testing.expectEqual(w.msize, have.tversion.msize);
+ try testing.expectEqualStrings(w.version, have.tversion.version);
+ },
+ .rversion => |w| {
+ try testing.expectEqual(w.msize, have.rversion.msize);
+ try testing.expectEqualStrings(w.version, have.rversion.version);
+ },
+ .tauth => |w| {
+ try testing.expectEqual(w.afid, have.tauth.afid);
+ try testing.expectEqualStrings(w.uname, have.tauth.uname);
+ try testing.expectEqualStrings(w.aname, have.tauth.aname);
+ },
+ .rauth => |w| try testing.expectEqual(w.aqid, have.rauth.aqid),
+ .tattach => |w| {
+ try testing.expectEqual(w.fid, have.tattach.fid);
+ try testing.expectEqual(w.afid, have.tattach.afid);
+ try testing.expectEqualStrings(w.uname, have.tattach.uname);
+ try testing.expectEqualStrings(w.aname, have.tattach.aname);
+ },
+ .rattach => |w| try testing.expectEqual(w.qid, have.rattach.qid),
+ .rerror => |w| try testing.expectEqualStrings(w.ename, have.rerror.ename),
+ .tflush => |w| try testing.expectEqual(w.oldtag, have.tflush.oldtag),
+ .rflush, .rclunk, .rremove, .rwstat => {},
+ .twalk => |w| {
+ try testing.expectEqual(w.fid, have.twalk.fid);
+ try testing.expectEqual(w.newfid, have.twalk.newfid);
+ try testing.expectEqual(w.nwname, have.twalk.nwname);
+ for (w.wname[0..w.nwname], have.twalk.wname[0..w.nwname]) |a, b|
+ try testing.expectEqualStrings(a, b);
+ },
+ .rwalk => |w| {
+ try testing.expectEqual(w.nwqid, have.rwalk.nwqid);
+ for (w.wqid[0..w.nwqid], have.rwalk.wqid[0..w.nwqid]) |a, b|
+ try testing.expectEqual(a, b);
+ },
+ .topen => |w| {
+ try testing.expectEqual(w.fid, have.topen.fid);
+ try testing.expectEqual(w.mode, have.topen.mode);
+ },
+ .ropen => |w| {
+ try testing.expectEqual(w.qid, have.ropen.qid);
+ try testing.expectEqual(w.iounit, have.ropen.iounit);
+ },
+ .tcreate => |w| {
+ try testing.expectEqual(w.fid, have.tcreate.fid);
+ try testing.expectEqualStrings(w.name, have.tcreate.name);
+ try testing.expectEqual(w.perm, have.tcreate.perm);
+ try testing.expectEqual(w.mode, have.tcreate.mode);
+ },
+ .rcreate => |w| {
+ try testing.expectEqual(w.qid, have.rcreate.qid);
+ try testing.expectEqual(w.iounit, have.rcreate.iounit);
+ },
+ .tread => |w| {
+ try testing.expectEqual(w.fid, have.tread.fid);
+ try testing.expectEqual(w.offset, have.tread.offset);
+ try testing.expectEqual(w.count, have.tread.count);
+ },
+ .rread => |w| try testing.expectEqualStrings(w.data, have.rread.data),
+ .twrite => |w| {
+ try testing.expectEqual(w.fid, have.twrite.fid);
+ try testing.expectEqual(w.offset, have.twrite.offset);
+ try testing.expectEqualStrings(w.data, have.twrite.data);
+ },
+ .rwrite => |w| try testing.expectEqual(w.count, have.rwrite.count),
+ .tclunk => |w| try testing.expectEqual(w.fid, have.tclunk.fid),
+ .tremove => |w| try testing.expectEqual(w.fid, have.tremove.fid),
+ .tstat => |w| try testing.expectEqual(w.fid, have.tstat.fid),
+ .rstat => |w| try expectStatEqual(w.stat, have.rstat.stat),
+ .twstat => |w| {
+ try testing.expectEqual(w.fid, have.twstat.fid);
+ try expectStatEqual(w.stat, have.twstat.stat);
+ },
+ }
+}
+
+const sample_qid: Qid = .{ .type = qtdir, .version = 3, .path = 0x0102_0304_0506_0708 };
+
+const sample_stat: Stat = .{
+ .type = 0,
+ .dev = 0,
+ .qid = sample_qid,
+ .mode = dmdir | 0o755,
+ .atime = 1,
+ .mtime = 2,
+ .length = 0,
+ .name = "body",
+ .uid = "goblin",
+ .gid = "goblin",
+ .muid = "goblin",
+};
+
+test "9p: the type numbers and their parity are the protocol's own" {
+ try testing.expectEqual(@as(u8, 100), @intFromEnum(Type.tversion));
+ try testing.expectEqual(@as(u8, 106), @intFromEnum(Type.terror));
+ try testing.expectEqual(@as(u8, 107), @intFromEnum(Type.rerror));
+ try testing.expectEqual(@as(u8, 126), @intFromEnum(Type.twstat));
+ try testing.expectEqual(@as(u8, 127), @intFromEnum(Type.rwstat));
+
+ try testing.expectEqual(@as(usize, 28), std.enums.values(Type).len);
+ for (std.enums.values(Type), 100..) |t, want| try testing.expectEqual(@as(u8, @intCast(want)), @intFromEnum(t));
+
+ try testing.expect(isT(.tversion));
+ try testing.expect(!isT(.rversion));
+ try testing.expect(isT(.twstat));
+ try testing.expect(!isT(.rwstat));
+
+ try testing.expectEqual(@as(u16, 0xFFFF), notag);
+ try testing.expectEqual(@as(u32, 0xFFFF_FFFF), nofid);
+ try testing.expectEqual(@as(usize, 16), max_welem);
+}
+
+test "9p: a qid is thirteen bytes" {
+ var buf: [32]u8 = undefined;
+ const bytes = try sample_qid.encode(&buf);
+ try testing.expectEqual(qid_len, bytes.len);
+ try testing.expectEqual(@as(usize, 13), bytes.len);
+ try testing.expectEqual(sample_qid, try Qid.decode(bytes));
+ try testing.expectError(error.Truncated, Qid.decode(bytes[0..12]));
+ try testing.expectError(error.NoSpace, sample_qid.encode(buf[0..12]));
+}
+
+test "9p: an encoded stat is size() + 2 bytes" {
+ var buf: [256]u8 = undefined;
+ const bytes = try sample_stat.encode(&buf);
+ const n = try sample_stat.size();
+ try testing.expectEqual(@as(usize, n) + 2, bytes.len);
+ try testing.expectEqual(@as(u16, 69), n);
+ try testing.expectEqual(stat_fixed - 2 + 22, n);
+ try testing.expectEqual(n, std.mem.readInt(u16, bytes[0..2], .little));
+ try expectStatEqual(sample_stat, try Stat.decode(bytes));
+
+ const bare: Stat = .{
+ .type = 0,
+ .dev = 0,
+ .qid = .{ .type = qtfile, .version = 0, .path = 0 },
+ .mode = 0,
+ .atime = 0,
+ .mtime = 0,
+ .length = 0,
+ .name = "",
+ .uid = "",
+ .gid = "",
+ .muid = "",
+ };
+ try testing.expectEqual(@as(u16, 47), try bare.size());
+ try testing.expectEqual(@as(usize, 49), (try bare.encode(&buf)).len);
+}
+
+test "9p: every message round-trips" {
+ var buf: [512]u8 = undefined;
+
+ _ = try roundTrip(&buf, notag, .{ .tversion = .{ .msize = 8192, .version = "9P2000" } });
+ _ = try roundTrip(&buf, notag, .{ .rversion = .{ .msize = 8192, .version = "9P2000" } });
+ _ = try roundTrip(&buf, notag, .{ .rversion = .{ .msize = test_msize, .version = "unknown" } });
+ _ = try roundTrip(&buf, 1, .{ .tauth = .{ .afid = 1, .uname = "goblin", .aname = "" } });
+ _ = try roundTrip(&buf, 1, .{ .rauth = .{ .aqid = .{ .type = qtauth, .version = 0, .path = 9 } } });
+ _ = try roundTrip(&buf, 2, .{ .tattach = .{ .fid = 0, .afid = nofid, .uname = "goblin", .aname = "" } });
+ _ = try roundTrip(&buf, 2, .{ .rattach = .{ .qid = sample_qid } });
+ _ = try roundTrip(&buf, 3, .{ .rerror = .{ .ename = "no such file" } });
+ _ = try roundTrip(&buf, 4, .{ .tflush = .{ .oldtag = 3 } });
+ _ = try roundTrip(&buf, 4, .rflush);
+ _ = try roundTrip(&buf, 5, .{ .twalk = .{ .fid = 0, .newfid = 1, .nwname = 2, .wname = .{ "7", "body" } ++ @as([max_welem - 2][]const u8, @splat("")) } });
+ _ = try roundTrip(&buf, 5, .{ .rwalk = .{ .nwqid = 2, .wqid = .{ sample_qid, sample_qid } ++ @as([max_welem - 2]Qid, @splat(sample_qid)) } });
+ _ = try roundTrip(&buf, 6, .{ .topen = .{ .fid = 1, .mode = 0 } });
+ _ = try roundTrip(&buf, 6, .{ .ropen = .{ .qid = sample_qid, .iounit = 8192 - iohdrsz } });
+ _ = try roundTrip(&buf, 7, .{ .tcreate = .{ .fid = 1, .name = "new", .perm = dmdir | 0o777, .mode = 2 } });
+ _ = try roundTrip(&buf, 7, .{ .rcreate = .{ .qid = sample_qid, .iounit = 0 } });
+ _ = try roundTrip(&buf, 8, .{ .tread = .{ .fid = 1, .offset = 0xdead_beef_cafe, .count = 4096 } });
+ _ = try roundTrip(&buf, 8, .{ .rread = .{ .data = "hello" } });
+ _ = try roundTrip(&buf, 8, .{ .rread = .{ .data = "" } });
+ _ = try roundTrip(&buf, 9, .{ .twrite = .{ .fid = 1, .offset = 0, .data = "Edit ,d" } });
+ _ = try roundTrip(&buf, 9, .{ .twrite = .{ .fid = 1, .offset = 0, .data = "" } });
+ _ = try roundTrip(&buf, 9, .{ .rwrite = .{ .count = 7 } });
+ _ = try roundTrip(&buf, 10, .{ .tclunk = .{ .fid = 1 } });
+ _ = try roundTrip(&buf, 10, .rclunk);
+ _ = try roundTrip(&buf, 11, .{ .tremove = .{ .fid = 1 } });
+ _ = try roundTrip(&buf, 11, .rremove);
+ _ = try roundTrip(&buf, 12, .{ .tstat = .{ .fid = 1 } });
+ _ = try roundTrip(&buf, 12, .{ .rstat = .{ .stat = sample_stat } });
+ _ = try roundTrip(&buf, 13, .{ .twstat = .{ .fid = 1, .stat = sample_stat } });
+ _ = try roundTrip(&buf, 13, .rwstat);
+
+ try testing.expectEqual(@as(usize, 27), @typeInfo(Msg).@"union".fields.len);
+ try testing.expectEqual(std.enums.values(Type).len - 1, @typeInfo(Msg).@"union".fields.len);
+}
+
+test "9p: empty and maximum-length strings survive the trip" {
+ var buf: [70_000]u8 = undefined;
+
+ const empty = try roundTrip(&buf, 1, .{ .tattach = .{ .fid = 0, .afid = nofid, .uname = "", .aname = "" } });
+ try testing.expectEqual(@as(usize, 0), empty.tattach.uname.len);
+ try testing.expectEqual(@as(usize, header_len + 4 + 4 + 2 + 2), (try encode(empty, 1, &buf)).len);
+
+ var big: [65_536]u8 = undefined;
+ @memset(&big, 'x');
+ const max = big[0..std.math.maxInt(u16)];
+ const got = try roundTrip(&buf, 1, .{ .rerror = .{ .ename = max } });
+ try testing.expectEqual(@as(usize, 65_535), got.rerror.ename.len);
+ try testing.expectError(error.Overlong, encode(.{ .rerror = .{ .ename = &big } }, 1, &buf));
+
+ var wide = sample_stat;
+ wide.name = max;
+ try testing.expectError(error.Overlong, wide.size());
+ try testing.expectError(error.Overlong, encode(.{ .rstat = .{ .stat = wide } }, 1, &buf));
+}
+
+test "9p: Twalk carries 0, 1 and 16 elements and refuses 17" {
+ var buf: [512]u8 = undefined;
+
+ const zero = try roundTrip(&buf, 1, .{ .twalk = .{ .fid = 0, .newfid = 1, .nwname = 0 } });
+ try testing.expectEqual(@as(u16, 0), zero.twalk.nwname);
+ try testing.expectEqual(@as(usize, header_len + 4 + 4 + 2), (try encode(zero, 1, &buf)).len);
+
+ _ = try roundTrip(&buf, 1, .{ .twalk = .{
+ .fid = 0,
+ .newfid = 1,
+ .nwname = 1,
+ .wname = .{"body"} ++ @as([max_welem - 1][]const u8, @splat("")),
+ } });
+
+ const names: [max_welem][]const u8 = .{ "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p" };
+ const full = try roundTrip(&buf, 1, .{ .twalk = .{ .fid = 0, .newfid = 1, .nwname = max_welem, .wname = names } });
+ try testing.expectEqual(@as(u16, 16), full.twalk.nwname);
+ for (names, full.twalk.wname[0..max_welem]) |a, b| try testing.expectEqualStrings(a, b);
+ _ = try roundTrip(&buf, 1, .{ .rwalk = .{ .nwqid = max_welem, .wqid = @splat(sample_qid) } });
+
+ var raw: [256]u8 = undefined;
+ const bad = blk: {
+ var w: Writer = .init(&raw);
+ try w.putU32(0); // patched below
+ try w.putByte(@intFromEnum(Type.twalk));
+ try w.putU16(1);
+ try w.putU32(0);
+ try w.putU32(1);
+ try w.putU16(17);
+ for (0..17) |i| try w.putString(&[_]u8{@intCast('a' + i)});
+ std.mem.writeInt(u32, raw[0..4], @intCast(w.n), .little);
+ break :blk raw[0..w.n];
+ };
+ try testing.expectEqual(@as(usize, header_len + 4 + 4 + 2 + 17 * 3), bad.len);
+ try testing.expectError(error.Overlong, decode(bad));
+
+ const bad_r = blk: {
+ var w: Writer = .init(&raw);
+ try w.putU32(0);
+ try w.putByte(@intFromEnum(Type.rwalk));
+ try w.putU16(1);
+ try w.putU16(17);
+ for (0..17) |_| try w.putQid(sample_qid);
+ std.mem.writeInt(u32, raw[0..4], @intCast(w.n), .little);
+ break :blk raw[0..w.n];
+ };
+ try testing.expectError(error.Overlong, decode(bad_r));
+}
+
+test "9p: the stat double length" {
+ var buf: [512]u8 = undefined;
+
+ var good: [512]u8 = undefined;
+ const n = blk: {
+ const bytes = try encode(.{ .rstat = .{ .stat = sample_stat } }, 1, &buf);
+ @memcpy(good[0..bytes.len], bytes);
+ break :blk bytes.len;
+ };
+ const inner = try sample_stat.size();
+ try testing.expectEqual(inner + 2, std.mem.readInt(u16, good[header_len..][0..2], .little));
+ try testing.expectEqual(inner, std.mem.readInt(u16, good[header_len + 2 ..][0..2], .little));
+ try testing.expectEqual(header_len + 2 + @as(usize, inner) + 2, n);
+
+ const w_bytes = try encode(.{ .twstat = .{ .fid = 7, .stat = sample_stat } }, 1, &buf);
+ try testing.expectEqual(inner + 2, std.mem.readInt(u16, w_bytes[header_len + 4 ..][0..2], .little));
+ try testing.expectEqual(inner, std.mem.readInt(u16, w_bytes[header_len + 6 ..][0..2], .little));
+
+ var off: [512]u8 = undefined;
+
+ @memcpy(off[0..n], good[0..n]);
+ std.mem.writeInt(u16, off[header_len..][0..2], inner, .little);
+ try testing.expectError(error.Truncated, decode(off[0..n]));
+
+ @memcpy(off[0..n], good[0..n]);
+ std.mem.writeInt(u16, off[header_len..][0..2], inner + 4, .little);
+ try testing.expectError(error.Truncated, decode(off[0..n]));
+
+ @memcpy(off[0..n], good[0..n]);
+ std.mem.writeInt(u16, off[header_len + 2 ..][0..2], inner + 2, .little);
+ try testing.expectError(error.Truncated, decode(off[0..n]));
+
+ @memcpy(off[0..n], good[0..n]);
+ std.mem.writeInt(u16, off[header_len + 2 ..][0..2], inner - 1, .little);
+ try testing.expectError(error.Trailing, decode(off[0..n]));
+}
+
+fn expectTruncatedAtEveryBoundary(full: []const u8) !void {
+ var scratch: [1024]u8 = undefined;
+ var n: usize = 0;
+ while (n < full.len) : (n += 1) {
+ try testing.expectError(error.Truncated, decode(full[0..n]));
+ if (n < header_len) continue;
+ @memcpy(scratch[0..n], full[0..n]);
+ std.mem.writeInt(u32, scratch[0..4], @intCast(n), .little);
+ try testing.expectError(error.Truncated, decode(scratch[0..n]));
+ }
+ _ = try decode(full);
+}
+
+test "9p: truncation at every field boundary is refused" {
+ var buf: [512]u8 = undefined;
+
+ try expectTruncatedAtEveryBoundary(try encode(
+ .{ .tversion = .{ .msize = 8192, .version = "9P2000" } },
+ notag,
+ &buf,
+ ));
+ try expectTruncatedAtEveryBoundary(try encode(.{ .twalk = .{
+ .fid = 1,
+ .newfid = 2,
+ .nwname = 3,
+ .wname = .{ "usr", "", "bin" } ++ @as([max_welem - 3][]const u8, @splat("")),
+ } }, 1, &buf));
+ try expectTruncatedAtEveryBoundary(try encode(
+ .{ .tread = .{ .fid = 1, .offset = 0x0102_0304_0506_0708, .count = 8168 } },
+ 1,
+ &buf,
+ ));
+ try expectTruncatedAtEveryBoundary(try encode(.{ .rstat = .{ .stat = sample_stat } }, 1, &buf));
+ try expectTruncatedAtEveryBoundary(try encode(.{ .rread = .{ .data = "12345678" } }, 1, &buf));
+ try expectTruncatedAtEveryBoundary(try encode(
+ .{ .rwalk = .{ .nwqid = 3, .wqid = @splat(sample_qid) } },
+ 1,
+ &buf,
+ ));
+ try expectTruncatedAtEveryBoundary(try encode(.{ .twstat = .{ .fid = 1, .stat = sample_stat } }, 1, &buf));
+}
+
+test "9p: a size field that disagrees with the buffer is refused" {
+ var buf: [512]u8 = undefined;
+ const bytes = try encode(.{ .tclunk = .{ .fid = 1 } }, 1, &buf);
+ try testing.expectEqual(@as(usize, 11), bytes.len);
+
+ var raw: [64]u8 = undefined;
+ @memcpy(raw[0..bytes.len], bytes);
+
+ for ([_]u32{ 12, 13, 64, 1 << 20, std.math.maxInt(u32) }) |claim| {
+ std.mem.writeInt(u32, raw[0..4], claim, .little);
+ try testing.expectError(error.Truncated, decode(raw[0..bytes.len]));
+ }
+
+ std.mem.writeInt(u32, raw[0..4], 10, .little);
+ try testing.expectError(error.Trailing, decode(raw[0..bytes.len]));
+
+ for ([_]u32{ 0, 1, 6 }) |claim| {
+ std.mem.writeInt(u32, raw[0..4], claim, .little);
+ try testing.expectError(error.BadValue, decode(raw[0..bytes.len]));
+ try testing.expectError(error.BadValue, decode(raw[0..header_len]));
+ }
+}
+
+test "9p: an unknown or illegal type byte is refused" {
+ var buf: [512]u8 = undefined;
+ const bytes = try encode(.{ .tclunk = .{ .fid = 1 } }, 1, &buf);
+ var raw: [64]u8 = undefined;
+ @memcpy(raw[0..bytes.len], bytes);
+
+ for ([_]u8{ 0, 1, 8, 12, 99, 106, 128, 255 }) |t| {
+ raw[4] = t;
+ try testing.expectError(error.BadTag, decode(raw[0..bytes.len]));
+ }
+
+ var t: u16 = 0;
+ while (t <= 255) : (t += 1) {
+ raw[4] = @intCast(t);
+ const defined = t >= 100 and t <= 127 and t != @intFromEnum(Type.terror);
+ if (decode(raw[0..bytes.len])) |got| {
+ try testing.expectEqual(@as(u8, @intCast(t)), @intFromEnum(got.msg.msgType()));
+ try testing.expect(t == @intFromEnum(Type.tclunk) or
+ t == @intFromEnum(Type.tremove) or
+ t == @intFromEnum(Type.tstat) or
+ t == @intFromEnum(Type.rwrite));
+ } else |err| {
+ if (!defined) try testing.expectEqual(Error.BadTag, err);
+ }
+ }
+}
+
+test "9p: trailing bytes inside the size are refused" {
+ var raw: [64]u8 = undefined;
+
+ var w: Writer = .init(&raw);
+ try w.putU32(12);
+ try w.putByte(@intFromEnum(Type.tclunk));
+ try w.putU16(1);
+ try w.putU32(7);
+ try w.putByte(0xAA);
+ try testing.expectEqual(@as(usize, 12), w.n);
+ try testing.expectError(error.Trailing, decode(raw[0..12]));
+
+ w = .init(&raw);
+ try w.putU32(header_len + 2 + 2);
+ try w.putByte(@intFromEnum(Type.tflush));
+ try w.putU16(1);
+ try w.putU16(3);
+ try w.putU16(3);
+ try testing.expectError(error.Trailing, decode(raw[0..w.n]));
+}
+
+test "9p: frameLen needs four bytes" {
+ var buf: [512]u8 = undefined;
+ const bytes = try encode(.{ .tread = .{ .fid = 1, .offset = 0, .count = 8168 } }, 1, &buf);
+ try testing.expectEqual(@as(usize, 23), bytes.len);
+
+ for (0..4) |n| try testing.expectEqual(@as(?u32, null), frameLen(bytes[0..n]));
+ try testing.expectEqual(@as(?u32, 23), frameLen(bytes[0..4]));
+ try testing.expectEqual(@as(?u32, 23), frameLen(bytes));
+
+ var raw: [4]u8 = .{ 0xFF, 0xFF, 0xFF, 0xFF };
+ try testing.expectEqual(@as(?u32, std.math.maxInt(u32)), frameLen(&raw));
+ raw = .{ 0, 0, 0, 0 };
+ try testing.expectEqual(@as(?u32, 0), frameLen(&raw));
+}
+
+test "9p: encode refuses a short buffer and writes nothing" {
+ var buf: [512]u8 = undefined;
+ const want = (try encode(.{ .rstat = .{ .stat = sample_stat } }, 1, &buf)).len;
+
+ var n: usize = 0;
+ while (n < want) : (n += 1) {
+ var scratch: [512]u8 = @splat(0xAA);
+ try testing.expectError(error.NoSpace, encode(.{ .rstat = .{ .stat = sample_stat } }, 1, scratch[0..n]));
+ for (scratch) |b| try testing.expectEqual(@as(u8, 0xAA), b);
+ }
+
+ var exact: [512]u8 = @splat(0xAA);
+ try testing.expectEqual(want, (try encode(.{ .rstat = .{ .stat = sample_stat } }, 1, exact[0..want])).len);
+ try testing.expectEqual(@as(u8, 0xAA), exact[want]);
+}
+
+test "9p: byte for byte against u9fs convS2M" {
+ var buf: [512]u8 = undefined;
+
+ try testing.expectEqualSlices(u8, &.{
+ 0x13, 0x00, 0x00, 0x00, // size = 19
+ 0x64, // Tversion = 100
+ 0xff, 0xff, // NOTAG
+ 0x00, 0x20, 0x00, 0x00, // msize = 8192
+ 0x06, 0x00, // n = 6
+ '9', 'P',
+ '2', '0',
+ '0', '0',
+ }, try encode(.{ .tversion = .{ .msize = 8192, .version = "9P2000" } }, notag, &buf));
+
+ try testing.expectEqualSlices(u8, &.{
+ 0x1b, 0x00, 0x00, 0x00, // size = 27
+ 0x6e, // Twalk = 110
+ 0x01, 0x00, // tag = 1
+ 0x01, 0x00, 0x00, 0x00, // fid = 1
+ 0x02, 0x00, 0x00, 0x00, // newfid = 2
+ 0x02, 0x00, // nwname = 2
+ 0x03, 0x00,
+ 'u', 's',
+ 'r', 0x03,
+ 0x00, 'b',
+ 'i', 'n',
+ }, try encode(.{ .twalk = .{
+ .fid = 1,
+ .newfid = 2,
+ .nwname = 2,
+ .wname = .{ "usr", "bin" } ++ @as([max_welem - 2][]const u8, @splat("")),
+ } }, 1, &buf));
+
+ try testing.expectEqualSlices(u8, &.{
+ 0x0e, 0x00, 0x00, 0x00, // size = 14
+ 0x75, // Rread = 117
+ 0x09, 0x00, // tag = 9
+ 0x03, 0x00, 0x00, 0x00, // count = 3
+ 'a', 'b', 'c',
+ }, try encode(.{ .rread = .{ .data = "abc" } }, 9, &buf));
+
+ const one: Stat = .{
+ .type = 0,
+ .dev = 0,
+ .qid = .{ .type = qtdir, .version = 1, .path = 2 },
+ .mode = dmdir | 0o755,
+ .atime = 3,
+ .mtime = 4,
+ .length = 0,
+ .name = "a",
+ .uid = "u",
+ .gid = "g",
+ .muid = "m",
+ };
+ try testing.expectEqual(@as(u16, 51), try one.size());
+ try testing.expectEqualSlices(u8, &.{
+ 0x3e, 0x00, 0x00, 0x00, // size = 62
+ 0x7d, // Rstat = 125
+ 0x07, 0x00, // tag = 7
+ 0x35, 0x00, // OUTER count = 53 = 51 + 2
+ 0x33, 0x00, // stat size = 51, excluding these two
+ 0x00, 0x00, // type
+ 0x00, 0x00, 0x00, 0x00, // dev
+ 0x80, // qid.type = QTDIR
+ 0x01, 0x00, 0x00, 0x00, // qid.version = 1
+ 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // qid.path = 2
+ 0xed, 0x01, 0x00, 0x80, // mode = DMDIR | 0755
+ 0x03, 0x00, 0x00, 0x00, // atime
+ 0x04, 0x00, 0x00, 0x00, // mtime
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // length
+ 0x01, 0x00, 'a', // name
+ 0x01, 0x00, 'u', // uid
+ 0x01, 0x00, 'g', // gid
+ 0x01, 0x00, 'm', // muid
+ }, try encode(.{ .rstat = .{ .stat = one } }, 7, &buf));
+
+ try testing.expectEqual(qtdir, @as(u8, @intCast(dmdir >> 24)));
+ try testing.expectEqual(qtappend, @as(u8, @intCast(dmappend >> 24)));
+ try testing.expectEqual(qtexcl, @as(u8, @intCast(dmexcl >> 24)));
+ try testing.expectEqual(qtauth, @as(u8, @intCast(dmauth >> 24)));
+ try testing.expectEqual(qttmp, @as(u8, @intCast(dmtmp >> 24)));
+ try testing.expectEqual(@as(u32, 0o777), dmperm);
+}