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; pub const min_msize: u32 = 4096; pub const iohdrsz: u32 = 24; pub const errmax: usize = 128; 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 2 + self.name.len + 2 + self.uid.len + 2 + self.gid.len + 2 + self.muid.len; 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); } fn totalLen(msg: Msg) Error!usize { const body: usize = 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: { assert(m.nwname <= max_welem); var n: usize = 4 + 4 + 2; for (m.wname[0..m.nwname]) |name| n += try stringLen(name); break :blk n; }, .rwalk => |m| blk: { assert(m.nwqid <= max_welem); 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 + 2 + @as(usize, try m.stat.size()), .twstat => |m| 4 + 2 + 2 + @as(usize, try m.stat.size()), .rwstat => 0, }; const total = header_len + body; if (total > std.math.maxInt(u32)) return error.Overlong; return total; } 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!usize { if (d.len > std.math.maxInt(u32)) return error.Overlong; return 4 + d.len; } pub fn encode(msg: Msg, tag: u16, buf: []u8) Error![]u8 { const total = try totalLen(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 { @memcpy(try w.room(v.len), 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 = min_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); } pub const e_unknown_fid = "fid unknown or out of range"; pub const e_fid_in_use = "fid already in use"; pub const e_bad_use = "bad use of fid"; pub const e_bad_offset = "bad offset in directory read"; pub const e_perm = "permission denied"; pub const e_not_dir = "not a directory"; pub const e_already_open = "file already open for I/O"; pub const e_illegal_name = "illegal name"; pub const e_too_many_fids = "Too many open files in system"; pub const e_botch = "protocol botch"; pub const e_interrupted = "Interrupted system call"; pub const e_trunc_only = "only support truncation to zero length"; pub const e_wstat = "wstat prohibited"; pub const e_again = "Resource temporarily unavailable"; pub const e_count_small = "Invalid argument"; pub const e_no_tree = "No such file or directory"; pub const e_no_auth = "authentication not required"; pub const e_small_msize = "Invalid argument"; pub fn errString(errno: u16) []const u8 { return switch (errno) { 1 => "Operation not permitted", // E.PERM, EPERM 2 => "No such file or directory", // E.NOENT, ENOENT 5 => "Input/output error", // E.IO, EIO 12 => "Cannot allocate memory", // E.NOMEM, ENOMEM 20 => "Not a directory", // E.NOTDIR, ENOTDIR 22 => "Invalid argument", // E.INVAL, EINVAL 23 => "Too many open files in system", // E.NFILE, ENFILE 28 => "No space left on device", // E.NOSPC, ENOSPC 38 => "Function not implemented", // E.NOSYS, ENOSYS else => "Input/output error", }; } pub const oread: u8 = 0; pub const owrite: u8 = 1; pub const ordwr: u8 = 2; pub const oexec: u8 = 3; pub const otrunc: u8 = 16; pub const ocexec: u8 = 32; pub const orclose: u8 = 64; pub const max_fids: usize = 256; pub const board_fids: usize = 32; pub const max_slots: usize = 32; pub const park_data_max: usize = 128; pub const board_name_capacity: usize = 28; const username_capacity: usize = 28; pub const msize_min: u32 = header_len + 2 + max_welem * qid_len; comptime { assert(msize_min == 217); assert(header_len + 2 + stat_fixed + board_name_capacity + 3 * username_capacity <= msize_min); assert(header_len + 4 + stat_fixed + board_name_capacity + 3 * username_capacity <= msize_min); assert(username_capacity >= 20); assert(oread | owrite | ordwr | oexec == 3); assert(otrunc | ocexec | orclose == 112); } fn qidOf(node: u64, dir: bool) Qid { return .{ .type = if (dir) qtdir else qtfile, .version = 0, .path = node }; } pub const dirent_dir_perm: u16 = 0o500; pub const dirent_file_perm: u16 = 0o600; pub fn Server(comptime fs: type, comptime fid_capacity: usize) type { if (fid_capacity == 0) @compileError("9P server needs at least one fid"); const name_capacity = if (@hasDecl(fs, "name_capacity")) fs.name_capacity else board_name_capacity; if (name_capacity == 0 or name_capacity > 255) @compileError("9P backend name capacity must fit a directory entry"); return struct { const Self = @This(); in: []u8, out: []u8, root: u64, in_len: usize = 0, frame: u32 = 0, out_len: usize = 0, out_off: usize = 0, msize: u32 = 0, dead: bool = false, uname: [username_capacity]u8 = @splat(0), uname_len: u8 = 0, fids: [fid_capacity]Fid = @splat(.{}), slots: [max_slots]Slot = @splat(.{}), job: Job = .{}, seq: u64 = 0, const Kind = enum { none, attach, walk, open, read, readdir, write, clunk, remove, stat, wstat }; const Fid = struct { used: bool = false, fid: u32 = 0, node: u64 = 0, dir: bool = false, perm: u16 = 0, open: bool = false, omode: u8 = 0, handle: u32 = 0, diroff: u64 = 0, dirindex: u32 = 0, orphan: bool = false, name: [name_capacity]u8 = @splat(0), name_len: u8 = 0, }; const Slot = struct { used: bool = false, parked: bool = false, retried: bool = false, copied: bool = false, seq: u64 = 0, tag: u16 = 0, kind: Kind = .none, fid: u32 = 0, count: u32 = 0, req: fs.Req = undefined, data: [park_data_max]u8 = undefined, }; const Job = struct { kind: Kind = .none, tag: u16 = 0, req_tag: u64 = 0, req: fs.Req = undefined, step: u8 = 0, fid: u32 = 0, newfid: u32 = 0, count: u32 = 0, offset: u64 = 0, omode: u8 = 0, node: u64 = 0, dir: bool = false, perm: u16 = 0, name: [name_capacity]u8 = @splat(0), name_len: u8 = 0, nwname: u8 = 0, nwqid: u8 = 0, wqid: [max_welem]Qid = @splat(.{ .type = 0, .version = 0, .path = 0 }), msg: Msg = .rflush, }; pub const Options = struct { in: []u8, out: []u8, root: u64, }; pub fn init(opts: Options) Self { assert(opts.in.len >= msize_min); assert(opts.out.len >= 2 * msize_min); assert(opts.root != 0); return .{ .in = opts.in, .out = opts.out, .root = opts.root }; } pub fn references(s: *const Self, node: u64) bool { for (s.fids) |fid| if (fid.used and fid.node == node) return true; if (s.job.kind != .none and s.job.node == node) return true; for (s.slots) |slot| if (slot.used and slot.req.node == node) return true; return false; } pub fn hangup(s: *Self) void { s.reset(); s.dead = true; s.in_len = 0; s.frame = 0; s.out_len = 0; s.out_off = 0; } fn reset(s: *Self) void { for (&s.fids) |*f| { if (!f.used) continue; if (f.open) f.orphan = true else f.* = .{}; } for (&s.slots) |*sl| sl.* = .{}; s.job = .{}; } pub fn push(s: *Self, bytes: []const u8) usize { if (s.dead) return 0; const n = @min(bytes.len, s.in.len - s.in_len); @memcpy(s.in[s.in_len..][0..n], bytes[0..n]); s.in_len += n; return n; } pub fn output(s: *const Self) []const u8 { return s.out[s.out_off..s.out_len]; } pub fn wrote(s: *Self, n: usize) void { assert(n <= s.out_len - s.out_off); s.out_off += n; if (s.out_off == s.out_len) { s.out_off = 0; s.out_len = 0; } } fn compact(s: *Self) void { assert(s.out_off <= s.out_len); const n = s.out_len - s.out_off; std.mem.copyForwards(u8, s.out[0..n], s.out[s.out_off..s.out_len]); s.out_off = 0; s.out_len = n; } fn hasRoom(s: *Self) bool { if (s.out_off != 0) s.compact(); return s.out.len - s.out_len >= @max(s.msize, msize_min); } fn emit(s: *Self, tag: u16, msg: Msg) void { const bytes = encode(msg, tag, s.out[s.out_len..]) catch { s.dead = true; return; }; s.out_len += bytes.len; } fn fail(s: *Self, tag: u16, ename: []const u8) void { assert(ename.len <= errmax); s.emit(tag, .{ .rerror = .{ .ename = ename } }); } fn tick(s: *Self) u64 { s.seq += 1; return s.seq; } fn findFid(s: *Self, fid: u32) ?usize { for (&s.fids, 0..) |*f, i| if (f.used and !f.orphan and f.fid == fid) return i; return null; } fn freeFid(s: *Self) ?usize { for (&s.fids, 0..) |*f, i| if (!f.used) return i; return null; } fn dropFid(s: *Self, fid: u32) void { if (s.findFid(fid)) |i| s.fids[i] = .{}; } fn findSlot(s: *Self, req_tag: u64) ?usize { for (&s.slots, 0..) |*sl, i| if (sl.used and sl.req.tag == req_tag) return i; return null; } fn freeSlot(s: *Self) ?usize { for (&s.slots, 0..) |*sl, i| if (!sl.used) return i; return null; } fn findTag(s: *Self, tag: u16) ?usize { for (&s.slots, 0..) |*sl, i| if (sl.used and sl.tag == tag) return i; return null; } fn setUname(s: *Self, uname: []const u8) void { const n = @min(uname.len, username_capacity); @memcpy(s.uname[0..n], uname[0..n]); s.uname_len = @intCast(n); } pub fn retry(s: *Self) ?fs.Req { var best: ?usize = null; for (&s.slots, 0..) |*sl, i| { if (!sl.used or !sl.parked or sl.retried) continue; if (best == null or sl.seq < s.slots[best.?].seq) best = i; } const i = best orelse { for (&s.slots) |*sl| sl.retried = false; return null; }; if (!s.hasRoom()) { for (&s.slots) |*sl| sl.retried = false; return null; } s.slots[i].retried = true; s.slots[i].parked = false; return s.slots[i].req; } pub fn next(s: *Self) ?fs.Req { while (true) { if (s.job.kind != .none) { if (s.job.req_tag != 0) return null; if (s.stepJob()) |req| return req; assert(s.job.kind == .none); continue; } if (s.orphan()) |req| return req; if (!s.hasRoom()) return null; if (!s.startFrame()) return null; } } pub fn reply(s: *Self, r: *const fs.Reply, bytes: []const u8) void { if (s.job.kind != .none and s.job.req_tag == r.tag) return s.jobReply(r, bytes); if (s.findSlot(r.tag)) |i| return s.slotReply(i, r, bytes); } fn orphan(s: *Self) ?fs.Req { for (&s.fids) |*f| { if (!f.used or !f.orphan) continue; assert(f.open); const req: fs.Req = .{ .tag = s.tick(), .op = .release, .node = f.node, .handle = f.handle, }; f.* = .{}; return req; } return null; } fn startFrame(s: *Self) bool { assert(s.job.kind == .none); assert(s.frame == 0); if (s.dead) return false; const len = frameLen(s.in[0..s.in_len]) orelse return false; if (len < header_len or len > s.in.len) { s.dead = true; return false; } if (len > s.in_len) return false; s.frame = len; defer if (s.job.kind == .none) s.dropFrame(); const got = decode(s.in[0..len]) catch { s.fail(std.mem.readInt(u16, s.in[5..7], .little), e_botch); return true; }; if (!isT(got.msg.msgType())) { s.fail(got.tag, e_botch); return true; } if (s.msize == 0 and got.msg != .tversion) { s.fail(got.tag, e_botch); return true; } if (s.msize != 0 and len > s.msize) { s.fail(got.tag, e_botch); return true; } s.dispatch(got); return true; } fn dropFrame(s: *Self) void { assert(s.frame != 0); assert(s.frame <= s.in_len); const n = s.frame; std.mem.copyForwards(u8, s.in[0 .. s.in_len - n], s.in[n..s.in_len]); s.in_len -= n; s.frame = 0; } fn dispatch(s: *Self, got: Decoded) void { switch (got.msg) { .tversion => |m| s.version(got.tag, m.msize, m.version), .tauth => s.fail(got.tag, e_no_auth), .tcreate => s.fail(got.tag, e_perm), .tattach => |m| s.attach(got.tag, m.fid, m.uname, m.aname), .tflush => |m| s.flush(got.tag, m.oldtag), .twalk => |m| s.walk(got, m.fid, m.newfid, @intCast(m.nwname)), .topen => |m| s.open(got.tag, m.fid, m.mode), .tread => |m| s.read(got.tag, m.fid, m.offset, m.count), .twrite => |m| s.write(got, m.fid, m.offset, m.data.len), .tclunk => |m| s.clunk(got.tag, m.fid, .clunk), .tremove => |m| s.clunk(got.tag, m.fid, .remove), .tstat => |m| s.stat(got.tag, m.fid), .twstat => |m| s.wstat(got.tag, m.fid, m.stat), else => s.fail(got.tag, e_botch), } } fn version(s: *Self, tag: u16, want: u32, ver: []const u8) void { const cap: u32 = @intCast(@min(s.in.len, s.out.len / 2)); const m = @min(want, cap); if (m < msize_min) return s.fail(tag, e_small_msize); const known = std.mem.startsWith(u8, ver, "9P"); s.reset(); s.msize = if (known) m else 0; s.emit(tag, .{ .rversion = .{ .msize = m, .version = if (known) "9P2000" else "unknown" } }); } fn attach(s: *Self, tag: u16, fid: u32, uname: []const u8, aname: []const u8) void { if (aname.len != 0) return s.fail(tag, e_no_tree); if (fid == nofid) return s.fail(tag, e_unknown_fid); if (s.findFid(fid) != null) return s.fail(tag, e_fid_in_use); if (s.freeFid() == null) return s.fail(tag, e_too_many_fids); s.setUname(uname); s.job = .{ .kind = .attach, .tag = tag, .fid = fid, .node = s.root }; } fn walk(s: *Self, got: Decoded, fid: u32, newfid: u32, nwname: u8) void { const tag = got.tag; const i = s.findFid(fid) orelse return s.fail(tag, e_unknown_fid); if (s.fids[i].open) return s.fail(tag, e_bad_use); if (newfid == nofid) return s.fail(tag, e_unknown_fid); if (newfid != fid) { if (s.findFid(newfid) != null) return s.fail(tag, e_fid_in_use); if (s.freeFid() == null) return s.fail(tag, e_too_many_fids); } if (nwname == 0) { if (newfid != fid) { const j = s.freeFid().?; s.fids[j] = s.fids[i]; s.fids[j].fid = newfid; s.fids[j].diroff = 0; s.fids[j].dirindex = 0; } s.emit(tag, .{ .rwalk = .{ .nwqid = 0 } }); return; } if (!s.fids[i].dir) return s.fail(tag, e_not_dir); s.job = .{ .kind = .walk, .tag = tag, .fid = fid, .newfid = newfid, .nwname = nwname, .node = s.fids[i].node, .dir = s.fids[i].dir, .perm = s.fids[i].perm, .name = s.fids[i].name, .name_len = s.fids[i].name_len, .msg = got.msg, }; } fn open(s: *Self, tag: u16, fid: u32, mode: u8) void { const i = s.findFid(fid) orelse return s.fail(tag, e_unknown_fid); const f = &s.fids[i]; if (f.open) return s.fail(tag, e_already_open); if (mode & orclose != 0) return s.fail(tag, e_perm); const rw = mode & 3; if (rw == oexec) return s.fail(tag, e_perm); if (f.dir and (rw != oread or mode & otrunc != 0)) return s.fail(tag, e_perm); var need: u16 = 0; if (rw == oread or rw == ordwr) need |= 0o400; if (rw == owrite or rw == ordwr or mode & otrunc != 0) need |= 0o200; if (f.perm & need != need) return s.fail(tag, e_perm); s.job = .{ .kind = .open, .tag = tag, .fid = fid, .omode = mode }; } fn read(s: *Self, tag: u16, fid: u32, offset: u64, count: u32) void { const i = s.findFid(fid) orelse return s.fail(tag, e_unknown_fid); const f = &s.fids[i]; if (!f.open or (f.omode & 3) == owrite) return s.fail(tag, e_bad_use); const want = @min(count, s.msize - header_len - 4); if (!f.dir) { s.job = .{ .kind = .read, .tag = tag, .fid = fid, .offset = offset, .count = want }; return; } if (offset != f.diroff) { if (offset != 0) return s.fail(tag, e_bad_offset); f.diroff = 0; f.dirindex = 0; } s.job = .{ .kind = .readdir, .tag = tag, .fid = fid, .offset = offset, .count = want }; } fn write(s: *Self, got: Decoded, fid: u32, offset: u64, len: usize) void { const tag = got.tag; const i = s.findFid(fid) orelse return s.fail(tag, e_unknown_fid); const f = &s.fids[i]; if (!f.open or (f.omode & 3) == oread) return s.fail(tag, e_bad_use); s.job = .{ .kind = .write, .tag = tag, .fid = fid, .offset = offset, .count = @intCast(len), .msg = got.msg, }; } fn clunk(s: *Self, tag: u16, fid: u32, kind: Kind) void { assert(kind == .clunk or kind == .remove); const i = s.findFid(fid) orelse return s.fail(tag, e_unknown_fid); if (s.fids[i].open) { s.job = .{ .kind = kind, .tag = tag, .fid = fid }; return; } s.fids[i] = .{}; if (kind == .remove) s.fail(tag, e_perm) else s.emit(tag, .rclunk); } fn stat(s: *Self, tag: u16, fid: u32) void { if (s.findFid(fid) == null) return s.fail(tag, e_unknown_fid); s.job = .{ .kind = .stat, .tag = tag, .fid = fid }; } fn wstat(s: *Self, tag: u16, fid: u32, st: Stat) void { if (s.findFid(fid) == null) return s.fail(tag, e_unknown_fid); if (st.type != std.math.maxInt(u16) or st.dev != std.math.maxInt(u32) or st.qid.type != std.math.maxInt(u8) or st.qid.version != std.math.maxInt(u32) or st.qid.path != std.math.maxInt(u64) or st.mode != std.math.maxInt(u32) or st.atime != std.math.maxInt(u32) or st.mtime != std.math.maxInt(u32) or st.name.len != 0 or st.uid.len != 0 or st.gid.len != 0 or st.muid.len != 0) return s.fail(tag, e_wstat); if (st.length == std.math.maxInt(u64)) { s.emit(tag, .rwstat); return; } if (st.length != 0) return s.fail(tag, e_trunc_only); s.job = .{ .kind = .wstat, .tag = tag, .fid = fid }; } fn flush(s: *Self, tag: u16, oldtag: u16) void { if (s.findTag(oldtag)) |i| { s.fail(s.slots[i].tag, e_interrupted); s.slots[i] = .{}; } s.emit(tag, .rflush); } fn ask(s: *Self, req: fs.Req) fs.Req { assert(req.tag != 0); s.job.req = req; s.job.req_tag = req.tag; return req; } fn jobFid(s: *Self) ?*Fid { const i = s.findFid(s.job.fid) orelse { s.fail(s.job.tag, e_unknown_fid); s.finishJob(); return null; }; return &s.fids[i]; } fn stepJob(s: *Self) ?fs.Req { const j = &s.job; assert(j.kind != .none); assert(j.req_tag == 0); switch (j.kind) { .none => unreachable, .attach => return s.ask(.{ .tag = s.tick(), .op = .getattr, .node = s.root }), .walk => return s.stepWalk(), .open => { const f = s.jobFid() orelse return null; if (j.step == 0 and j.omode & otrunc != 0) return s.ask(.{ .tag = s.tick(), .op = .setattr, .node = f.node, .truncate = true, }); return s.ask(.{ .tag = s.tick(), .op = .open, .node = f.node }); }, .read => { const f = s.jobFid() orelse return null; return s.ask(.{ .tag = s.tick(), .op = .read, .node = f.node, .handle = f.handle, .off = j.offset, .size = j.count, }); }, .readdir => { const f = s.jobFid() orelse return null; return s.ask(.{ .tag = s.tick(), .op = .readdir, .node = f.node, .handle = f.handle, .off = f.dirindex, .size = j.count, }); }, .write => { const f = s.jobFid() orelse return null; return s.ask(.{ .tag = s.tick(), .op = .write, .node = f.node, .handle = f.handle, .off = j.offset, .size = j.count, .data = j.msg.twrite.data, }); }, .clunk, .remove => { const f = s.jobFid() orelse return null; return s.ask(.{ .tag = s.tick(), .op = .release, .node = f.node, .handle = f.handle, }); }, .stat => { const f = s.jobFid() orelse return null; return s.ask(.{ .tag = s.tick(), .op = .getattr, .node = f.node }); }, .wstat => { const f = s.jobFid() orelse return null; return s.ask(.{ .tag = s.tick(), .op = .setattr, .node = f.node, .truncate = true }); }, } } fn stepWalk(s: *Self) ?fs.Req { const j = &s.job; while (j.step < j.nwname) { const name = j.msg.twalk.wname[j.step]; if (name.len > name_capacity) { s.stopWalk(e_illegal_name); return null; } if (std.mem.eql(u8, name, ".")) { j.wqid[j.nwqid] = qidOf(j.node, j.dir); j.nwqid += 1; j.step += 1; continue; } @memcpy(j.name[0..name.len], name); j.name_len = @intCast(name.len); return s.ask(.{ .tag = s.tick(), .op = .lookup, .node = j.node, .data = name }); } const dst = pick: { if (j.newfid == j.fid) break :pick s.findFid(j.fid) orelse { s.fail(j.tag, e_unknown_fid); s.finishJob(); return null; }; break :pick s.freeFid() orelse { s.fail(j.tag, e_too_many_fids); s.finishJob(); return null; }; }; s.fids[dst] = .{ .used = true, .fid = j.newfid, .node = j.node, .dir = j.dir, .perm = j.perm, .name = j.name, .name_len = j.name_len, }; s.emit(j.tag, .{ .rwalk = .{ .nwqid = j.nwqid, .wqid = j.wqid } }); s.finishJob(); return null; } fn stopWalk(s: *Self, ename: []const u8) void { const j = &s.job; if (j.nwqid == 0) s.fail(j.tag, ename) else s.emit(j.tag, .{ .rwalk = .{ .nwqid = j.nwqid, .wqid = j.wqid } }); s.finishJob(); } fn finishJob(s: *Self) void { s.job = .{}; if (s.frame != 0) s.dropFrame(); } fn jobReply(s: *Self, r: *const fs.Reply, bytes: []const u8) void { const j = &s.job; assert(j.kind != .none); assert(j.req_tag == r.tag); j.req_tag = 0; if (j.kind == .clunk or j.kind == .remove) { s.dropFid(j.fid); if (j.kind == .remove) s.fail(j.tag, e_perm) else s.emit(j.tag, .rclunk); s.finishJob(); return; } if (r.status == .again) return s.parkJob(); if (r.status == .err) { const ename = errString(r.errno); if (j.kind == .walk) return s.stopWalk(ename); s.fail(j.tag, ename); s.finishJob(); return; } switch (j.kind) { .none, .clunk, .remove => unreachable, .attach => { const i = s.freeFid() orelse { s.fail(j.tag, e_too_many_fids); s.finishJob(); return; }; const node = if (r.attr.node != 0) r.attr.node else s.root; s.fids[i] = .{ .used = true, .fid = j.fid, .node = node, .dir = r.attr.dir, .perm = r.attr.mode, }; s.fids[i].name[0] = '/'; s.fids[i].name_len = 1; s.emit(j.tag, .{ .rattach = .{ .qid = qidOf(node, r.attr.dir) } }); s.finishJob(); }, .walk => { if (r.attr.node != 0) j.node = r.attr.node; if (comptime @hasField(@TypeOf(r.attr), "name")) { if (r.attr.name.len != 0) { j.name_len = @intCast(@min(r.attr.name.len, j.name.len)); @memcpy(j.name[0..j.name_len], r.attr.name[0..j.name_len]); } } j.dir = r.attr.dir; j.perm = r.attr.mode; j.wqid[j.nwqid] = qidOf(j.node, j.dir); j.nwqid += 1; j.step += 1; }, .open => { if (j.step == 0 and j.omode & otrunc != 0) { j.step = 1; return; } const f = s.jobFid() orelse return; f.open = true; f.omode = j.omode; f.handle = r.handle; f.diroff = 0; f.dirindex = 0; s.emit(j.tag, .{ .ropen = .{ .qid = qidOf(f.node, f.dir), .iounit = s.msize - iohdrsz, } }); s.finishJob(); }, .read => { s.emit(j.tag, .{ .rread = .{ .data = bytes[0..@min(bytes.len, j.count)] } }); s.finishJob(); }, .readdir => { s.emitDirRead(j.tag, j.fid, bytes, j.count); s.finishJob(); }, .write => { s.emit(j.tag, .{ .rwrite = .{ .count = @min(r.written, j.count) } }); s.finishJob(); }, .stat => { const f = s.jobFid() orelse return; f.perm = r.attr.mode; f.dir = r.attr.dir; const response: Msg = .{ .rstat = .{ .stat = s.statOf(f, r.attr) } }; if ((totalLen(response) catch unreachable) > s.msize) s.fail(j.tag, e_small_msize) else s.emit(j.tag, response); s.finishJob(); }, .wstat => { s.emit(j.tag, .rwstat); s.finishJob(); }, } } fn parkJob(s: *Self) void { const j = &s.job; switch (j.kind) { .read, .readdir, .write => {}, else => { s.fail(j.tag, e_again); s.finishJob(); return; }, } const i = s.freeSlot() orelse { s.fail(j.tag, e_again); s.finishJob(); return; }; const sl = &s.slots[i]; sl.* = .{ .used = true, .parked = true, .seq = s.tick(), .tag = j.tag, .kind = j.kind, .fid = j.fid, .count = j.count, .req = j.req, }; if (j.req.data.len != 0) { if (j.req.data.len > park_data_max) { sl.* = .{}; s.fail(j.tag, e_again); s.finishJob(); return; } @memcpy(sl.data[0..j.req.data.len], j.req.data); sl.copied = true; sl.req.data = sl.data[0..j.req.data.len]; } s.finishJob(); } fn slotReply(s: *Self, i: usize, r: *const fs.Reply, bytes: []const u8) void { const sl = &s.slots[i]; if (r.status == .again) { sl.parked = true; return; } if (r.status == .err) { s.fail(sl.tag, errString(r.errno)); sl.* = .{}; return; } switch (sl.kind) { .read => s.emit(sl.tag, .{ .rread = .{ .data = bytes[0..@min(bytes.len, sl.count)] } }), .readdir => s.emitDirRead(sl.tag, sl.fid, bytes, sl.count), .write => s.emit(sl.tag, .{ .rwrite = .{ .count = @min(r.written, sl.count) } }), else => s.fail(sl.tag, e_botch), } sl.* = .{}; } fn emitDirRead(s: *Self, tag: u16, fid: u32, staging: []const u8, count: u32) void { const buf = s.out[s.out_len..]; assert(buf.len > header_len + 4); const cap = @min(@as(usize, count), buf.len - header_len - 4); const who = s.uname[0..s.uname_len]; var n: usize = header_len + 4; var entries: u32 = 0; var i: usize = 0; while (i + 10 <= staging.len) { const nlen: usize = staging[i + 9]; if (i + 10 + nlen > staging.len) break; const dir = staging[i + 8] != 0; const rec: Stat = .{ .type = 0, .dev = 0, .qid = qidOf(std.mem.readInt(u64, staging[i..][0..8], .little), dir), .mode = (if (dir) dmdir else 0) | @as(u32, if (dir) dirent_dir_perm else dirent_file_perm), .atime = 0, .mtime = 0, .length = 0, .name = staging[i + 10 ..][0..nlen], .uid = who, .gid = who, .muid = who, }; const size = @as(usize, rec.size() catch break) + 2; if (n - header_len - 4 + size > cap) break; _ = rec.encode(buf[n..]) catch break; n += size; entries += 1; i += 10 + nlen; } if (entries == 0 and staging.len != 0) return s.fail(tag, e_count_small); const payload: u32 = @intCast(n - header_len - 4); comptime assert(header_len == 7); std.mem.writeInt(u32, buf[0..4], @intCast(n), .little); buf[4] = @intFromEnum(Type.rread); std.mem.writeInt(u16, buf[5..7], tag, .little); std.mem.writeInt(u32, buf[7..11], payload, .little); s.out_len += n; if (s.findFid(fid)) |k| { s.fids[k].diroff += payload; s.fids[k].dirindex += entries; } } fn statOf(s: *const Self, f: *const Fid, a: fs.Reply.Attr) Stat { const who = s.uname[0..s.uname_len]; return .{ .type = 0, .dev = 0, .qid = qidOf(if (a.node != 0) a.node else f.node, a.dir), .mode = (if (a.dir) dmdir else 0) | @as(u32, a.mode), .atime = 0, .mtime = 0, .length = a.size, .name = f.name[0..f.name_len], .uid = who, .gid = who, .muid = who, }; } }; } const StubFs = struct { pub const Op = enum(u8) { lookup, getattr, setattr, open, read, write, release, readdir }; pub const Status = enum(u8) { ok, again, err }; pub const Req = struct { tag: u64, op: Op, node: u64, handle: u32 = 0, off: u64 = 0, size: u32 = 0, data: []const u8 = &.{}, truncate: bool = false, }; pub const Reply = struct { tag: u64, status: Status = .ok, errno: u16 = 0, attr: Attr = .{}, handle: u32 = 0, written: u32 = 0, pub const Attr = struct { name: []const u8 = "", node: u64 = 0, dir: bool = false, size: u64 = 0, mode: u16 = 0o600, }; }; const Entry = struct { node: u64, parent: u64, name: []const u8, dir: bool, mode: u16 }; const tree = [_]Entry{ .{ .node = 1, .parent = 1, .name = "/", .dir = true, .mode = 0o500 }, .{ .node = 2, .parent = 1, .name = "index", .dir = false, .mode = 0o400 }, .{ .node = 3, .parent = 1, .name = "cons", .dir = false, .mode = 0o200 }, .{ .node = 4, .parent = 1, .name = "new", .dir = true, .mode = 0o500 }, .{ .node = 16, .parent = 1, .name = "1", .dir = true, .mode = 0o500 }, .{ .node = 32, .parent = 1, .name = "2", .dir = true, .mode = 0o500 }, .{ .node = 17, .parent = 16, .name = "addr", .dir = false, .mode = 0o600 }, .{ .node = 18, .parent = 16, .name = "body", .dir = false, .mode = 0o600 }, .{ .node = 19, .parent = 16, .name = "ctl", .dir = false, .mode = 0o600 }, .{ .node = 21, .parent = 16, .name = "errors", .dir = false, .mode = 0o200 }, .{ .node = 22, .parent = 16, .name = "event", .dir = false, .mode = 0o600 }, .{ .node = 23, .parent = 16, .name = "tag", .dir = false, .mode = 0o600 }, }; const body_node = 18; const index_node = 2; const event_node = 22; body: []const u8 = "hello, body\n", filler: [1024]u8 = @splat('x'), event: ?[]const u8 = null, park_writes: bool = false, releases: u32 = 0, calls: u32 = 0, writes: [128]u8 = undefined, writes_len: usize = 0, stage: [1024]u8 = undefined, const Answer = struct { reply: Reply, bytes: []const u8 = "" }; fn find(node: u64) ?usize { for (tree, 0..) |e, i| if (e.node == node) return i; return null; } fn sizeOf(st: *const StubFs, node: u64) u64 { return switch (node) { body_node => st.body.len, index_node => st.filler.len, else => 0, }; } fn contentOf(st: *const StubFs, node: u64) []const u8 { return switch (node) { body_node => st.body, index_node => &st.filler, else => "", }; } fn attrOf(st: *const StubFs, e: Entry) Reply.Attr { return .{ .name = e.name, .node = e.node, .dir = e.dir, .mode = e.mode, .size = st.sizeOf(e.node) }; } fn stageDir(st: *StubFs, node: u64, skip: u64) []const u8 { var n: usize = 0; var seen: u64 = 0; for (tree) |e| { if (e.parent != node or e.node == node) continue; if (seen < skip) { seen += 1; continue; } std.mem.writeInt(u64, st.stage[n..][0..8], e.node, .little); st.stage[n + 8] = @intFromBool(e.dir); st.stage[n + 9] = @intCast(e.name.len); @memcpy(st.stage[n + 10 ..][0..e.name.len], e.name); n += 10 + e.name.len; } return st.stage[0..n]; } fn handle(st: *StubFs, req: Req) Answer { st.calls += 1; const fail: Answer = .{ .reply = .{ .tag = req.tag, .status = .err, .errno = 2 } }; const i = find(req.node) orelse return fail; switch (req.op) { .lookup => { if (std.mem.eql(u8, req.data, "..")) return .{ .reply = .{ .tag = req.tag, .attr = st.attrOf(tree[find(tree[i].parent).?]) } }; for (tree) |e| { if (e.parent != req.node or e.node == req.node) continue; if (!std.mem.eql(u8, e.name, req.data)) continue; return .{ .reply = .{ .tag = req.tag, .attr = st.attrOf(e) } }; } return fail; }, .getattr => return .{ .reply = .{ .tag = req.tag, .attr = st.attrOf(tree[i]) } }, .setattr => { if (req.truncate and req.node == body_node) st.body = ""; return .{ .reply = .{ .tag = req.tag, .attr = st.attrOf(tree[i]) } }; }, .open => return .{ .reply = .{ .tag = req.tag, .handle = 7 } }, .release => { st.releases += 1; return .{ .reply = .{ .tag = req.tag } }; }, .readdir => { if (!tree[i].dir) return .{ .reply = .{ .tag = req.tag, .status = .err, .errno = 20 } }; return .{ .reply = .{ .tag = req.tag }, .bytes = st.stageDir(req.node, req.off) }; }, .read => { if (req.node == event_node) { const rec = st.event orelse return .{ .reply = .{ .tag = req.tag, .status = .again } }; st.event = null; return .{ .reply = .{ .tag = req.tag }, .bytes = rec }; } const all = st.contentOf(req.node); if (req.off >= all.len) return .{ .reply = .{ .tag = req.tag } }; const from = all[@intCast(req.off)..]; return .{ .reply = .{ .tag = req.tag }, .bytes = from[0..@min(from.len, req.size)] }; }, .write => { if (st.park_writes) return .{ .reply = .{ .tag = req.tag, .status = .again } }; const n = @min(req.data.len, st.writes.len - st.writes_len); @memcpy(st.writes[st.writes_len..][0..n], req.data[0..n]); st.writes_len += n; return .{ .reply = .{ .tag = req.tag, .written = @intCast(n) } }; }, } } }; const Srv = Server(StubFs, max_fids); const Harness = struct { in: [4096]u8 = undefined, out: [8192]u8 = undefined, fsys: StubFs = .{}, srv: Srv = undefined, fn start(h: *Harness) void { h.srv = Srv.init(.{ .in = &h.in, .out = &h.out, .root = 1 }); } fn answer(h: *Harness, req: StubFs.Req) void { const a = h.fsys.handle(req); h.srv.reply(&a.reply, a.bytes); } fn pump(h: *Harness) void { while (h.srv.retry()) |req| h.answer(req); while (h.srv.next()) |req| h.answer(req); } fn send(h: *Harness, tag: u16, msg: Msg) !void { var buf: [1024]u8 = undefined; const bytes = try encode(msg, tag, &buf); try testing.expectEqual(bytes.len, h.srv.push(bytes)); h.pump(); } fn reap(h: *Harness) !Decoded { const out = h.srv.output(); const len = frameLen(out) orelse return error.NoReply; if (len > out.len) return error.ShortReply; const got = try decode(out[0..len]); h.srv.wrote(len); return got; } fn quiet(h: *Harness) !void { try testing.expectEqual(@as(usize, 0), h.srv.output().len); } fn handshake(h: *Harness, msize: u32) !void { h.start(); try h.send(notag, .{ .tversion = .{ .msize = msize, .version = "9P2000" } }); const v = try h.reap(); try testing.expectEqualStrings("9P2000", v.msg.rversion.version); try h.send(0, .{ .tattach = .{ .fid = 0, .afid = nofid, .uname = "goblin", .aname = "" } }); const a = try h.reap(); try testing.expectEqual(@as(u64, 1), a.msg.rattach.qid.path); } fn walkTo(h: *Harness, tag: u16, newfid: u32, list: []const []const u8) !Decoded { try h.send(tag, .{ .twalk = .{ .fid = 0, .newfid = newfid, .nwname = @intCast(list.len), .wname = wnames(list), } }); return h.reap(); } }; fn wnames(list: []const []const u8) [max_welem][]const u8 { var out: [max_welem][]const u8 = @splat(""); for (list, 0..) |n, i| out[i] = n; return out; } fn dirNames(data: []const u8, out: [][]const u8) !usize { var n: usize = 0; var i: usize = 0; while (i < data.len) { const size = std.mem.readInt(u16, data[i..][0..2], .little); const st = try Stat.decode(data[i..][0 .. @as(usize, size) + 2]); out[n] = st.name; n += 1; i += @as(usize, size) + 2; } return n; } test "9p server: the version handshake clamps, falls back, and refuses" { var h: Harness = .{}; h.start(); try h.send(notag, .{ .tversion = .{ .msize = 1 << 20, .version = "9P2000" } }); var got = try h.reap(); try testing.expectEqual(notag, got.tag); try testing.expectEqual(@as(u32, 4096), got.msg.rversion.msize); try testing.expectEqualStrings("9P2000", got.msg.rversion.version); try h.send(notag, .{ .tversion = .{ .msize = 512, .version = "9P2000" } }); got = try h.reap(); try testing.expectEqual(@as(u32, 512), got.msg.rversion.msize); try h.send(notag, .{ .tversion = .{ .msize = 4096, .version = "9P2000.u" } }); got = try h.reap(); try testing.expectEqualStrings("9P2000", got.msg.rversion.version); try h.send(notag, .{ .tversion = .{ .msize = 4096, .version = "TCP/IP" } }); got = try h.reap(); try testing.expectEqualStrings("unknown", got.msg.rversion.version); try h.send(1, .{ .tattach = .{ .fid = 0, .afid = nofid, .uname = "goblin", .aname = "" } }); got = try h.reap(); try testing.expectEqualStrings(e_botch, got.msg.rerror.ename); try h.send(notag, .{ .tversion = .{ .msize = 64, .version = "9P2000" } }); got = try h.reap(); try testing.expectEqualStrings(e_small_msize, got.msg.rerror.ename); try testing.expectEqual(@as(u32, 216), msize_min - 1); } test "9p server: attach names the root, and the only tree there is" { var h: Harness = .{}; h.start(); try h.send(notag, .{ .tversion = .{ .msize = 4096, .version = "9P2000" } }); _ = try h.reap(); try h.send(1, .{ .tattach = .{ .fid = 0, .afid = nofid, .uname = "goblin", .aname = "work" } }); var got = try h.reap(); try testing.expectEqualStrings(e_no_tree, got.msg.rerror.ename); try h.send(2, .{ .tattach = .{ .fid = 0, .afid = nofid, .uname = "goblin", .aname = "" } }); got = try h.reap(); try testing.expectEqual(@as(u64, 1), got.msg.rattach.qid.path); try testing.expectEqual(qtdir, got.msg.rattach.qid.type); try testing.expectEqual(@as(u32, 0), got.msg.rattach.qid.version); try h.send(3, .{ .tattach = .{ .fid = 0, .afid = nofid, .uname = "goblin", .aname = "" } }); got = try h.reap(); try testing.expectEqualStrings(e_fid_in_use, got.msg.rerror.ename); try h.send(4, .{ .tauth = .{ .afid = 1, .uname = "goblin", .aname = "" } }); got = try h.reap(); try testing.expectEqualStrings(e_no_auth, got.msg.rerror.ename); try h.send(5, .{ .tstat = .{ .fid = 0 } }); got = try h.reap(); try testing.expectEqualStrings("/", got.msg.rstat.stat.name); try testing.expectEqualStrings("goblin", got.msg.rstat.stat.uid); try testing.expectEqualStrings("goblin", got.msg.rstat.stat.muid); try testing.expect(got.msg.rstat.stat.mode & dmdir != 0); } test "9p server: a three-element walk, and `..` with no kernel to resolve it" { var h: Harness = .{}; try h.handshake(4096); var got = try h.walkTo(5, 1, &.{ "..", "1", "body" }); try testing.expectEqual(@as(u16, 3), got.msg.rwalk.nwqid); try testing.expectEqual(@as(u64, 1), got.msg.rwalk.wqid[0].path); try testing.expectEqual(@as(u64, 16), got.msg.rwalk.wqid[1].path); try testing.expectEqual(@as(u64, 18), got.msg.rwalk.wqid[2].path); try testing.expectEqual(qtdir, got.msg.rwalk.wqid[1].type); try testing.expectEqual(qtfile, got.msg.rwalk.wqid[2].type); for (got.msg.rwalk.wqid[0..3]) |q| try testing.expectEqual(@as(u32, 0), q.version); got = try h.walkTo(6, 2, &.{ "..", "1", "..", "1", "body" }); try testing.expectEqual(@as(u16, 5), got.msg.rwalk.nwqid); try testing.expectEqual(@as(u64, 1), got.msg.rwalk.wqid[2].path); try testing.expectEqual(@as(u64, 18), got.msg.rwalk.wqid[4].path); try h.send(7, .{ .tstat = .{ .fid = 2 } }); got = try h.reap(); try testing.expectEqualStrings("body", got.msg.rstat.stat.name); try testing.expectEqual(@as(u64, 12), got.msg.rstat.stat.length); got = try h.walkTo(8, 3, &.{ "1", "body", ".." }); try testing.expectEqual(@as(u16, 3), got.msg.rwalk.nwqid); try testing.expectEqual(@as(u64, 16), got.msg.rwalk.wqid[2].path); try testing.expectEqual(qtdir, got.msg.rwalk.wqid[2].type); try h.send(9, .{ .tstat = .{ .fid = 3 } }); got = try h.reap(); try testing.expectEqualStrings("1", got.msg.rstat.stat.name); const before = h.fsys.calls; got = try h.walkTo(10, 4, &.{ ".", "." }); try testing.expectEqual(@as(u16, 2), got.msg.rwalk.nwqid); try testing.expectEqual(@as(u64, 1), got.msg.rwalk.wqid[1].path); try testing.expectEqual(before, h.fsys.calls); } test "9p server: a walk failing on the first element is Rerror, on the second a short Rwalk" { var h: Harness = .{}; try h.handshake(4096); var got = try h.walkTo(5, 1, &.{ "nope", "body" }); try testing.expectEqualStrings("No such file or directory", got.msg.rerror.ename); got = try h.walkTo(6, 1, &.{ "1", "nope" }); try testing.expectEqual(@as(u16, 1), got.msg.rwalk.nwqid); try testing.expectEqual(@as(u64, 16), got.msg.rwalk.wqid[0].path); try h.send(7, .{ .tstat = .{ .fid = 1 } }); got = try h.reap(); try testing.expectEqualStrings(e_unknown_fid, got.msg.rerror.ename); try h.send(8, .{ .twalk = .{ .fid = 0, .newfid = 1, .nwname = 0 } }); got = try h.reap(); try testing.expectEqual(@as(u16, 0), got.msg.rwalk.nwqid); try h.send(9, .{ .tstat = .{ .fid = 1 } }); got = try h.reap(); try testing.expectEqualStrings("/", got.msg.rstat.stat.name); got = try h.walkTo(10, 2, &.{"index"}); try testing.expectEqual(@as(u16, 1), got.msg.rwalk.nwqid); try h.send(11, .{ .twalk = .{ .fid = 2, .newfid = 3, .nwname = 1, .wname = wnames(&.{"body"}) } }); got = try h.reap(); try testing.expectEqualStrings(e_not_dir, got.msg.rerror.ename); got = try h.walkTo(12, 4, &.{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}); try testing.expectEqualStrings(e_illegal_name, got.msg.rerror.ename); try h.send(13, .{ .twalk = .{ .fid = 0, .newfid = 1, .nwname = 1, .wname = wnames(&.{"1"}) } }); got = try h.reap(); try testing.expectEqualStrings(e_fid_in_use, got.msg.rerror.ename); } test "9p server: backend-sized filenames survive walk and stat within negotiated msize" { const NativeFs = struct { pub const Req = StubFs.Req; pub const Reply = StubFs.Reply; pub const name_capacity: usize = 255; }; const Native = Server(NativeFs, 2); try testing.expectEqual(@as(usize, 28), @sizeOf(@FieldType(Srv.Fid, "name"))); try testing.expectEqual(@as(usize, 255), @sizeOf(@FieldType(Native.Fid, "name"))); const filename: [255]u8 = @splat('f'); for ([_]struct { length: usize, msize: u32 }{ .{ .length = 29, .msize = 512 }, .{ .length = 128, .msize = 512 }, .{ .length = 255, .msize = 512 }, .{ .length = 200, .msize = 256 }, }) |case| { var in: [1024]u8 = undefined; var out: [2048]u8 = undefined; var encoded: [1024]u8 = undefined; var server = Native.init(.{ .in = &in, .out = &out, .root = 1 }); server.msize = case.msize; server.setUname("u" ** username_capacity); server.fids[0] = .{ .used = true, .fid = 0, .node = 1, .dir = true, .perm = 0o500 }; const name = filename[0..case.length]; const walk = try encode(.{ .twalk = .{ .fid = 0, .newfid = 1, .nwname = 1, .wname = wnames(&.{name}), } }, 1, &encoded); try testing.expectEqual(walk.len, server.push(walk)); const lookup = server.next() orelse return error.MissingLookup; try testing.expectEqual(.lookup, lookup.op); try testing.expectEqualStrings(name, lookup.data); server.reply(&.{ .tag = lookup.tag, .attr = .{ .node = 2, .name = name, .size = 12 } }, ""); try testing.expectEqual(null, server.next()); var response = try decode(server.output()); try testing.expectEqual(@as(u16, 1), response.msg.rwalk.nwqid); server.wrote(server.output().len); const stat = try encode(.{ .tstat = .{ .fid = 1 } }, 2, &encoded); try testing.expectEqual(stat.len, server.push(stat)); const getattr = server.next() orelse return error.MissingGetattr; try testing.expectEqual(.getattr, getattr.op); server.reply(&.{ .tag = getattr.tag, .attr = .{ .node = 2, .name = name, .size = 12 } }, ""); try testing.expect(server.output().len <= case.msize); response = try decode(server.output()); if (case.msize == 256) { try testing.expectEqualStrings(e_small_msize, response.msg.rerror.ename); } else { try testing.expectEqualStrings(name, response.msg.rstat.stat.name); try testing.expectEqual(@as(u64, 12), response.msg.rstat.stat.length); } server.wrote(server.output().len); const clunk = try encode(.{ .tclunk = .{ .fid = 1 } }, 3, &encoded); try testing.expectEqual(clunk.len, server.push(clunk)); try testing.expectEqual(null, server.next()); response = try decode(server.output()); try testing.expect(response.msg == .rclunk); try testing.expect(!server.dead); } } test "9p server: open then read then clunk, and the release a clunk owes the core" { var h: Harness = .{}; try h.handshake(4096); _ = try h.walkTo(5, 1, &.{ "1", "body" }); try h.send(6, .{ .topen = .{ .fid = 1, .mode = oread } }); var got = try h.reap(); try testing.expectEqual(@as(u64, 18), got.msg.ropen.qid.path); try testing.expectEqual(@as(u32, 4096 - iohdrsz), got.msg.ropen.iounit); try h.send(7, .{ .topen = .{ .fid = 1, .mode = oread } }); got = try h.reap(); try testing.expectEqualStrings(e_already_open, got.msg.rerror.ename); try h.send(8, .{ .tread = .{ .fid = 1, .offset = 0, .count = 4096 } }); got = try h.reap(); try testing.expectEqualStrings("hello, body\n", got.msg.rread.data); try h.send(9, .{ .tread = .{ .fid = 1, .offset = 7, .count = 4096 } }); got = try h.reap(); try testing.expectEqualStrings("body\n", got.msg.rread.data); try h.send(10, .{ .tread = .{ .fid = 1, .offset = 99, .count = 16 } }); got = try h.reap(); try testing.expectEqual(@as(usize, 0), got.msg.rread.data.len); try testing.expectEqual(@as(u32, 0), h.fsys.releases); try h.send(11, .{ .tclunk = .{ .fid = 1 } }); got = try h.reap(); try testing.expect(got.msg == .rclunk); try testing.expectEqual(@as(u32, 1), h.fsys.releases); try h.send(12, .{ .tread = .{ .fid = 1, .offset = 0, .count = 16 } }); got = try h.reap(); try testing.expectEqualStrings(e_unknown_fid, got.msg.rerror.ename); try h.send(13, .{ .tclunk = .{ .fid = 1 } }); got = try h.reap(); try testing.expectEqualStrings(e_unknown_fid, got.msg.rerror.ename); _ = try h.walkTo(14, 2, &.{"index"}); try h.send(15, .{ .tclunk = .{ .fid = 2 } }); got = try h.reap(); try testing.expect(got.msg == .rclunk); try testing.expectEqual(@as(u32, 1), h.fsys.releases); } test "9p server: every Rread is clamped to the client's count and to the msize" { var h: Harness = .{}; try h.handshake(512); _ = try h.walkTo(5, 1, &.{"index"}); try h.send(6, .{ .topen = .{ .fid = 1, .mode = oread } }); _ = try h.reap(); try h.send(7, .{ .tread = .{ .fid = 1, .offset = 0, .count = 5 } }); var got = try h.reap(); try testing.expectEqual(@as(usize, 5), got.msg.rread.data.len); try h.send(8, .{ .tread = .{ .fid = 1, .offset = 0, .count = 1 << 20 } }); const out = h.srv.output(); try testing.expectEqual(@as(?u32, 512), frameLen(out)); got = try h.reap(); try testing.expectEqual(@as(usize, 512 - header_len - 4), got.msg.rread.data.len); try h.send(9, .{ .tread = .{ .fid = 1, .offset = 0, .count = 1 << 20 } }); got = try h.reap(); try testing.expectEqual(@as(usize, 501), got.msg.rread.data.len); } test "9p server: a directory read is whole stat records at a cursor the client cannot invent" { var h: Harness = .{}; try h.handshake(4096); _ = try h.walkTo(4, 1, &.{"index"}); try h.send(5, .{ .topen = .{ .fid = 0, .mode = oread } }); _ = try h.reap(); var found: [8][]const u8 = undefined; try h.send(6, .{ .tread = .{ .fid = 0, .offset = 0, .count = 150 } }); var got = try h.reap(); const first = got.msg.rread.data.len; try testing.expectEqual(@as(usize, 2), try dirNames(got.msg.rread.data, &found)); try testing.expectEqualStrings("index", found[0]); try testing.expectEqualStrings("cons", found[1]); try testing.expect(first <= 150); try h.send(7, .{ .tread = .{ .fid = 0, .offset = first, .count = 150 } }); got = try h.reap(); const second = got.msg.rread.data.len; try testing.expectEqual(@as(usize, 2), try dirNames(got.msg.rread.data, &found)); try testing.expectEqualStrings("new", found[0]); try testing.expectEqualStrings("1", found[1]); try h.send(8, .{ .tread = .{ .fid = 0, .offset = first + second + 1, .count = 150 } }); got = try h.reap(); try testing.expectEqualStrings(e_bad_offset, got.msg.rerror.ename); try h.send(9, .{ .tread = .{ .fid = 0, .offset = 3, .count = 150 } }); got = try h.reap(); try testing.expectEqualStrings(e_bad_offset, got.msg.rerror.ename); try h.send(10, .{ .tread = .{ .fid = 0, .offset = first + second, .count = 150 } }); got = try h.reap(); try testing.expectEqual(@as(usize, 1), try dirNames(got.msg.rread.data, &found)); try testing.expectEqualStrings("2", found[0]); try h.send(11, .{ .tread = .{ .fid = 0, .offset = first + second + got.msg.rread.data.len, .count = 150 } }); got = try h.reap(); try testing.expectEqual(@as(usize, 0), got.msg.rread.data.len); try h.send(12, .{ .tread = .{ .fid = 0, .offset = 0, .count = 150 } }); got = try h.reap(); try testing.expectEqual(@as(usize, 2), try dirNames(got.msg.rread.data, &found)); try testing.expectEqualStrings("index", found[0]); try h.send(13, .{ .tread = .{ .fid = 0, .offset = 0, .count = 40 } }); got = try h.reap(); try testing.expectEqualStrings(e_count_small, got.msg.rerror.ename); try h.send(14, .{ .tread = .{ .fid = 0, .offset = 0, .count = 150 } }); got = try h.reap(); const one = try Stat.decode(got.msg.rread.data[0 .. std.mem.readInt(u16, got.msg.rread.data[0..2], .little) + 2]); try testing.expectEqualStrings("index", one.name); try testing.expectEqual(@as(u32, dirent_file_perm), one.mode); try testing.expectEqual(@as(u64, 0), one.length); try testing.expectEqual(@as(u32, 0), one.qid.version); try h.send(16, .{ .tstat = .{ .fid = 1 } }); got = try h.reap(); try testing.expectEqual(@as(u32, 0o400), got.msg.rstat.stat.mode); try testing.expectEqual(@as(u64, 1024), got.msg.rstat.stat.length); } test "9p server: long directory names remain whole across pages" { var h: Harness = .{}; try h.handshake(4096); var entries: [10 + 255 + 10 + 4]u8 = @splat(0); std.mem.writeInt(u64, entries[0..8], 41, .little); entries[9] = 255; @memset(entries[10..265], 'f'); std.mem.writeInt(u64, entries[265..273], 42, .little); entries[274] = 4; @memcpy(entries[275..], "next"); var found: [2][]const u8 = undefined; h.srv.emitDirRead(5, 0, &entries, 330); var got = try h.reap(); const first = got.msg.rread.data.len; try testing.expectEqual(@as(usize, 1), try dirNames(got.msg.rread.data, &found)); try testing.expectEqualStrings(entries[10..265], found[0]); try testing.expectEqual(@as(u64, 1), h.srv.fids[0].dirindex); h.srv.emitDirRead(6, 0, entries[265..], 330); got = try h.reap(); try testing.expectEqual(@as(usize, 1), try dirNames(got.msg.rread.data, &found)); try testing.expectEqualStrings("next", found[0]); try testing.expectEqual(@as(u64, 2), h.srv.fids[0].dirindex); try testing.expectEqual(first + got.msg.rread.data.len, h.srv.fids[0].diroff); h.srv.emitDirRead(7, 0, &entries, 4096); got = try h.reap(); try testing.expectEqual(@as(usize, 2), try dirNames(got.msg.rread.data, &found)); try testing.expectEqualStrings(entries[10..265], found[0]); try testing.expectEqualStrings("next", found[1]); } test "9p server: a blocked read parks, and the connection keeps working" { var h: Harness = .{}; try h.handshake(4096); _ = try h.walkTo(5, 1, &.{ "1", "event" }); try h.send(6, .{ .topen = .{ .fid = 1, .mode = oread } }); _ = try h.reap(); try h.send(7, .{ .tread = .{ .fid = 1, .offset = 0, .count = 4096 } }); try h.quiet(); h.pump(); h.pump(); try h.quiet(); _ = try h.walkTo(8, 2, &.{ "1", "body" }); try h.send(9, .{ .tstat = .{ .fid = 2 } }); var got = try h.reap(); try testing.expectEqualStrings("body", got.msg.rstat.stat.name); h.fsys.event = "Kli7 7 0 0 hello\n"; h.pump(); got = try h.reap(); try testing.expectEqual(@as(u16, 7), got.tag); try testing.expectEqualStrings("Kli7 7 0 0 hello\n", got.msg.rread.data); try h.quiet(); _ = try h.walkTo(10, 3, &.{ "1", "ctl" }); try h.send(11, .{ .topen = .{ .fid = 3, .mode = owrite } }); _ = try h.reap(); h.fsys.park_writes = true; try h.send(12, .{ .twrite = .{ .fid = 3, .offset = 0, .data = "clean\n" } }); try h.quiet(); try h.send(13, .{ .tstat = .{ .fid = 2 } }); _ = try h.reap(); h.fsys.park_writes = false; h.pump(); got = try h.reap(); try testing.expectEqual(@as(u16, 12), got.tag); try testing.expectEqual(@as(u32, 6), got.msg.rwrite.count); try testing.expectEqualStrings("clean\n", h.fsys.writes[0..h.fsys.writes_len]); for (0..max_slots) |k| { try h.send(@intCast(100 + k), .{ .tread = .{ .fid = 1, .offset = 0, .count = 4096 } }); try h.quiet(); } try h.send(200, .{ .tread = .{ .fid = 1, .offset = 0, .count = 4096 } }); got = try h.reap(); try testing.expectEqualStrings(e_again, got.msg.rerror.ename); } test "9p server: the reply queue is a FIFO that survives a partial write" { var h: Harness = .{}; try h.handshake(4096); try h.send(5, .{ .tstat = .{ .fid = 0 } }); var saved: [256]u8 = undefined; const one = h.srv.output(); const n = one.len; @memcpy(saved[0..n], one); h.srv.wrote(3); try testing.expectEqual(n - 3, h.srv.output().len); try h.send(6, .{ .tstat = .{ .fid = 0 } }); const rest = h.srv.output(); try testing.expectEqualSlices(u8, saved[3..n], rest[0 .. n - 3]); const tail = rest[n - 3 ..]; const second = try decode(tail[0..frameLen(tail).?]); try testing.expectEqual(@as(u16, 6), second.tag); var flood: [8192]u8 = @splat(0); try testing.expectEqual(@as(usize, 4096), h.srv.push(&flood)); h.pump(); try testing.expect(h.srv.dead); try testing.expectEqual(@as(usize, 0), h.srv.push(&flood)); } test "9p server: Tflush answers the original first and the Rflush second" { var h: Harness = .{}; try h.handshake(4096); _ = try h.walkTo(5, 1, &.{ "1", "event" }); try h.send(6, .{ .topen = .{ .fid = 1, .mode = oread } }); _ = try h.reap(); try h.send(7, .{ .tread = .{ .fid = 1, .offset = 0, .count = 4096 } }); try h.quiet(); try h.send(8, .{ .tflush = .{ .oldtag = 7 } }); var got = try h.reap(); try testing.expectEqual(@as(u16, 7), got.tag); try testing.expectEqualStrings(e_interrupted, got.msg.rerror.ename); got = try h.reap(); try testing.expectEqual(@as(u16, 8), got.tag); try testing.expect(got.msg == .rflush); try h.quiet(); h.fsys.event = "Kli7 7 0 0 hello\n"; h.pump(); try h.quiet(); try h.send(9, .{ .tflush = .{ .oldtag = 99 } }); got = try h.reap(); try testing.expectEqual(@as(u16, 9), got.tag); try testing.expect(got.msg == .rflush); try h.quiet(); } test "9p server: the fid and permission refusals, each in a string Linux knows" { var h: Harness = .{}; try h.handshake(4096); for ([_]Msg{ .{ .tread = .{ .fid = 99, .offset = 0, .count = 16 } }, .{ .tstat = .{ .fid = 99 } }, .{ .tclunk = .{ .fid = 99 } }, .{ .topen = .{ .fid = 99, .mode = oread } }, .{ .twalk = .{ .fid = 99, .newfid = 98, .nwname = 0 } }, }, 20..) |msg, tag| { try h.send(@intCast(tag), msg); const got = try h.reap(); try testing.expectEqualStrings(e_unknown_fid, got.msg.rerror.ename); } _ = try h.walkTo(5, 1, &.{ "1", "body" }); try h.send(6, .{ .tread = .{ .fid = 1, .offset = 0, .count = 16 } }); var got = try h.reap(); try testing.expectEqualStrings(e_bad_use, got.msg.rerror.ename); try h.send(7, .{ .topen = .{ .fid = 1, .mode = oread } }); _ = try h.reap(); try h.send(8, .{ .twrite = .{ .fid = 1, .offset = 0, .data = "x" } }); got = try h.reap(); try testing.expectEqualStrings(e_bad_use, got.msg.rerror.ename); try h.send(9, .{ .twalk = .{ .fid = 1, .newfid = 2, .nwname = 0 } }); got = try h.reap(); try testing.expectEqualStrings(e_bad_use, got.msg.rerror.ename); _ = try h.walkTo(10, 3, &.{ "1", "errors" }); try h.send(11, .{ .topen = .{ .fid = 3, .mode = oread } }); got = try h.reap(); try testing.expectEqualStrings(e_perm, got.msg.rerror.ename); try h.send(12, .{ .topen = .{ .fid = 3, .mode = owrite } }); got = try h.reap(); try testing.expect(got.msg == .ropen); try h.send(13, .{ .topen = .{ .fid = 0, .mode = ordwr } }); got = try h.reap(); try testing.expectEqualStrings(e_perm, got.msg.rerror.ename); _ = try h.walkTo(14, 4, &.{ "1", "tag" }); for ([_]u8{ orclose, oexec, oread | orclose }, 30..) |mode, tag| { try h.send(@intCast(tag), .{ .topen = .{ .fid = 4, .mode = mode } }); got = try h.reap(); try testing.expectEqualStrings(e_perm, got.msg.rerror.ename); } } test "9p server: Twstat with a zero length is the truncate, and so is OTRUNC" { var h: Harness = .{}; try h.handshake(4096); _ = try h.walkTo(5, 1, &.{ "1", "body" }); const sentinel: Stat = .{ .type = std.math.maxInt(u16), .dev = std.math.maxInt(u32), .qid = .{ .type = 0xFF, .version = std.math.maxInt(u32), .path = std.math.maxInt(u64) }, .mode = std.math.maxInt(u32), .atime = std.math.maxInt(u32), .mtime = std.math.maxInt(u32), .length = std.math.maxInt(u64), .name = "", .uid = "", .gid = "", .muid = "", }; try h.send(6, .{ .twstat = .{ .fid = 1, .stat = sentinel } }); var got = try h.reap(); try testing.expect(got.msg == .rwstat); try testing.expectEqualStrings("hello, body\n", h.fsys.body); var five = sentinel; five.length = 5; try h.send(7, .{ .twstat = .{ .fid = 1, .stat = five } }); got = try h.reap(); try testing.expectEqualStrings(e_trunc_only, got.msg.rerror.ename); var renamed = sentinel; renamed.name = "other"; try h.send(8, .{ .twstat = .{ .fid = 1, .stat = renamed } }); got = try h.reap(); try testing.expectEqualStrings(e_wstat, got.msg.rerror.ename); try testing.expectEqualStrings("hello, body\n", h.fsys.body); var changes: [12]Stat = @splat(sentinel); changes[0].type = 0; changes[1].dev = 0; changes[2].qid.type = 0; changes[3].qid.version = 0; changes[4].qid.path = 0; changes[5].mode = 0o644; changes[6].atime = 0; changes[7].mtime = 0; changes[8].name = "renamed"; changes[9].uid = "owner"; changes[10].gid = "group"; changes[11].muid = "writer"; for (changes) |change| { for ([_]u64{ std.math.maxInt(u64), 0 }) |length| { var attributes = change; attributes.length = length; const calls = h.fsys.calls; try h.send(20, .{ .twstat = .{ .fid = 1, .stat = attributes } }); got = try h.reap(); try testing.expect(got.msg == .rerror); try testing.expectEqualStrings(e_wstat, got.msg.rerror.ename); try testing.expectEqual(calls, h.fsys.calls); try testing.expectEqualStrings("hello, body\n", h.fsys.body); } } var zero = sentinel; zero.length = 0; try h.send(9, .{ .twstat = .{ .fid = 1, .stat = zero } }); got = try h.reap(); try testing.expect(got.msg == .rwstat); try testing.expectEqualStrings("", h.fsys.body); h.fsys.body = "hello, body\n"; _ = try h.walkTo(10, 2, &.{"index"}); try h.send(11, .{ .topen = .{ .fid = 2, .mode = oread | otrunc } }); got = try h.reap(); try testing.expectEqualStrings(e_perm, got.msg.rerror.ename); try h.send(12, .{ .topen = .{ .fid = 1, .mode = owrite | otrunc } }); got = try h.reap(); try testing.expectEqual(@as(u64, 18), got.msg.ropen.qid.path); try testing.expectEqualStrings("", h.fsys.body); } test "9p server: create and remove are refused, and a remove clunks the fid anyway" { var h: Harness = .{}; try h.handshake(4096); try h.send(5, .{ .tcreate = .{ .fid = 0, .name = "thing", .perm = 0o600, .mode = owrite } }); var got = try h.reap(); try testing.expectEqualStrings(e_perm, got.msg.rerror.ename); _ = try h.walkTo(6, 1, &.{ "1", "body" }); try h.send(7, .{ .topen = .{ .fid = 1, .mode = ordwr } }); _ = try h.reap(); try h.send(8, .{ .tremove = .{ .fid = 1 } }); got = try h.reap(); try testing.expectEqualStrings(e_perm, got.msg.rerror.ename); try testing.expectEqual(@as(u32, 1), h.fsys.releases); try h.send(9, .{ .tstat = .{ .fid = 1 } }); got = try h.reap(); try testing.expectEqualStrings(e_unknown_fid, got.msg.rerror.ename); } test "9p server: a message arriving a byte at a time is served when its last byte lands" { var h: Harness = .{}; try h.handshake(4096); var buf: [64]u8 = undefined; const bytes = try encode(.{ .tstat = .{ .fid = 0 } }, 5, &buf); for (bytes[0 .. bytes.len - 1]) |b| { try testing.expectEqual(@as(usize, 1), h.srv.push(&.{b})); h.pump(); try h.quiet(); } try testing.expectEqual(@as(usize, 1), h.srv.push(bytes[bytes.len - 1 ..])); h.pump(); const got = try h.reap(); try testing.expectEqualStrings("/", got.msg.rstat.stat.name); var pair: [128]u8 = undefined; const a = try encode(.{ .tstat = .{ .fid = 0 } }, 6, &pair); const b = try encode(.{ .tstat = .{ .fid = 0 } }, 7, pair[a.len..]); try testing.expectEqual(a.len + b.len, h.srv.push(pair[0 .. a.len + b.len])); h.pump(); try testing.expectEqual(@as(u16, 6), (try h.reap()).tag); try testing.expectEqual(@as(u16, 7), (try h.reap()).tag); try h.quiet(); } test "9p server: what is not 9P2000 on this connection is refused, not guessed" { var h: Harness = .{}; try h.handshake(4096); var buf: [64]u8 = undefined; const good = try encode(.{ .tstat = .{ .fid = 0 } }, 5, &buf); var raw: [64]u8 = undefined; @memcpy(raw[0..good.len], good); raw[4] = @intFromEnum(Type.rstat); try testing.expectEqual(good.len, h.srv.push(raw[0..good.len])); h.pump(); var got = try h.reap(); try testing.expectEqual(@as(u16, 5), got.tag); try testing.expectEqualStrings(e_botch, got.msg.rerror.ename); @memcpy(raw[0..good.len], good); raw[4] = 8; _ = h.srv.push(raw[0..good.len]); h.pump(); got = try h.reap(); try testing.expectEqual(@as(u16, 5), got.tag); try testing.expectEqualStrings(e_botch, got.msg.rerror.ename); @memcpy(raw[0..good.len], good); std.mem.writeInt(u32, raw[0..4], 3, .little); _ = h.srv.push(raw[0..good.len]); h.pump(); try h.quiet(); try testing.expect(h.srv.dead); } test "9p server: a connection that drops still pays the core its releases" { var h: Harness = .{}; try h.handshake(4096); _ = try h.walkTo(5, 1, &.{ "1", "event" }); _ = try h.walkTo(6, 2, &.{ "1", "body" }); for ([_]u32{ 1, 2 }, 7..) |fid, tag| { try h.send(@intCast(tag), .{ .topen = .{ .fid = fid, .mode = oread } }); _ = try h.reap(); } try h.send(9, .{ .tread = .{ .fid = 1, .offset = 0, .count = 4096 } }); try h.quiet(); h.srv.hangup(); h.pump(); try testing.expectEqual(@as(u32, 2), h.fsys.releases); try h.quiet(); var g: Harness = .{}; try g.handshake(4096); _ = try g.walkTo(5, 1, &.{ "1", "event" }); try g.send(6, .{ .topen = .{ .fid = 1, .mode = oread } }); _ = try g.reap(); try g.send(7, .{ .tread = .{ .fid = 1, .offset = 0, .count = 4096 } }); try g.quiet(); try g.send(notag, .{ .tversion = .{ .msize = 4096, .version = "9P2000" } }); const v = try g.reap(); try testing.expectEqualStrings("9P2000", v.msg.rversion.version); try testing.expectEqual(@as(u32, 1), g.fsys.releases); g.fsys.event = "Kli7 7 0 0 hello\n"; g.pump(); try g.quiet(); try g.send(8, .{ .tstat = .{ .fid = 1 } }); const got = try g.reap(); try testing.expectEqualStrings(e_unknown_fid, got.msg.rerror.ename); } test "9p server: every errno the core can answer is a string Linux knows" { try testing.expectEqualStrings("Operation not permitted", errString(1)); try testing.expectEqualStrings("No such file or directory", errString(2)); try testing.expectEqualStrings("Input/output error", errString(5)); try testing.expectEqualStrings("Cannot allocate memory", errString(12)); try testing.expectEqualStrings("Not a directory", errString(20)); try testing.expectEqualStrings("Invalid argument", errString(22)); try testing.expectEqualStrings("Too many open files in system", errString(23)); try testing.expectEqualStrings("No space left on device", errString(28)); try testing.expectEqualStrings("Function not implemented", errString(38)); try testing.expectEqualStrings("Input/output error", errString(0)); try testing.expectEqualStrings("Input/output error", errString(999)); try testing.expectEqualStrings("fid unknown or out of range", e_unknown_fid); try testing.expectEqualStrings("fid already in use", e_fid_in_use); try testing.expectEqualStrings("bad use of fid", e_bad_use); try testing.expectEqualStrings("bad offset in directory read", e_bad_offset); try testing.expectEqualStrings("permission denied", e_perm); try testing.expectEqualStrings("not a directory", e_not_dir); try testing.expectEqualStrings("file already open for I/O", e_already_open); try testing.expectEqualStrings("illegal name", e_illegal_name); try testing.expectEqualStrings("Too many open files in system", e_too_many_fids); try testing.expectEqualStrings("protocol botch", e_botch); try testing.expectEqualStrings("Interrupted system call", e_interrupted); try testing.expectEqualStrings("only support truncation to zero length", e_trunc_only); try testing.expectEqualStrings("wstat prohibited", e_wstat); try testing.expectEqualStrings("Resource temporarily unavailable", e_again); for ([_][]const u8{ e_unknown_fid, e_fid_in_use, e_bad_use, e_bad_offset, e_perm, e_not_dir, e_already_open, e_botch, e_interrupted, e_trunc_only, e_wstat, e_again, e_no_tree, e_no_auth, e_count_small, e_illegal_name, e_too_many_fids, e_small_msize, }) |s| try testing.expect(s.len <= errmax); } test "9p server: board and native capacities size the actual fid storage" { const Board = Server(StubFs, board_fids); const Native = Server(StubFs, max_fids); const BoardFids = @FieldType(Board, "fids"); const NativeFids = @FieldType(Native, "fids"); try testing.expectEqual(32, @typeInfo(BoardFids).array.len); try testing.expectEqual(256, @typeInfo(NativeFids).array.len); try testing.expectEqual(32 * @sizeOf(Board.Fid), @sizeOf(BoardFids)); try testing.expectEqual(256 * @sizeOf(Native.Fid), @sizeOf(NativeFids)); try testing.expectEqual( @sizeOf(NativeFids) - @sizeOf(BoardFids), @sizeOf(Native) - @sizeOf(Board), ); try testing.expect(@sizeOf(BoardFids) <= 3 * 1024); try testing.expect(@sizeOf(NativeFids) <= 24 * 1024); try testing.expect(@sizeOf(@FieldType(Board, "slots")) <= 8 * 1024); try testing.expect(3 * 4096 + @sizeOf(Board) <= 24 * 1024); } pub const max_tags: usize = 16; const twrite_header: usize = header_len + 4 + 8 + 4; const rread_header: usize = header_len + 4; comptime { assert(twrite_header == 23); assert(rread_header == 11); assert(max_tags <= notag); assert(msize_min > rread_header); assert(msize_min > twrite_header); } pub const ClientError = error{ NoTags, NoSpace, TooLarge, Handshake, Dead, BadRequest, }; pub const Client = struct { in: []u8, out: []u8, in_len: usize = 0, frame: u32 = 0, out_len: usize = 0, out_off: usize = 0, msize: u32 = 0, asked: u32 = 0, versioning: bool = false, dead: bool = false, tags: [max_tags]Slot = @splat(.{}), const Slot = struct { op: ?Op = null, count: u32 = 0, }; pub const Op = enum { version, attach, walk, open, read, write, clunk, stat }; pub const Request = union(Op) { version: struct { msize: u32 = 0 }, attach: struct { fid: u32, uname: []const u8, aname: []const u8 = "" }, walk: struct { fid: u32, newfid: u32, names: []const []const u8 }, open: struct { fid: u32, mode: u8 }, read: struct { fid: u32, offset: u64, count: u32 }, write: struct { fid: u32, offset: u64, data: []const u8 }, clunk: struct { fid: u32 }, stat: struct { fid: u32 }, }; pub const Result = union(enum) { fail: []const u8, version: struct { msize: u32, version: []const u8 }, attach: Qid, walk: struct { nwqid: u16, wqid: [max_welem]Qid }, open: struct { qid: Qid, iounit: u32 }, read: []const u8, write: u32, clunk: void, stat: Stat, }; pub const Done = struct { tag: u16, op: Op, result: Result, }; pub const Options = struct { in: []u8, out: []u8, }; pub fn init(opts: Options) Client { assert(opts.in.len >= msize_min); assert(opts.out.len >= msize_min); return .{ .in = opts.in, .out = opts.out }; } pub fn hangup(c: *Client) void { c.dead = true; c.tags = @splat(.{}); c.versioning = false; c.msize = 0; c.asked = 0; c.in_len = 0; c.frame = 0; c.out_len = 0; c.out_off = 0; } pub fn push(c: *Client, bytes: []const u8) usize { if (c.dead) return 0; const n = @min(bytes.len, c.in.len - c.in_len); @memcpy(c.in[c.in_len..][0..n], bytes[0..n]); c.in_len += n; return n; } pub fn output(c: *const Client) []const u8 { return c.out[c.out_off..c.out_len]; } pub fn wrote(c: *Client, n: usize) void { assert(n <= c.out_len - c.out_off); c.out_off += n; if (c.out_off == c.out_len) { c.out_off = 0; c.out_len = 0; } } fn compact(c: *Client) void { assert(c.out_off <= c.out_len); const n = c.out_len - c.out_off; std.mem.copyForwards(u8, c.out[0..n], c.out[c.out_off..c.out_len]); c.out_off = 0; c.out_len = n; } fn dropFrame(c: *Client) void { assert(c.frame != 0); assert(c.frame <= c.in_len); const n = c.frame; std.mem.copyForwards(u8, c.in[0 .. c.in_len - n], c.in[n..c.in_len]); c.in_len -= n; c.frame = 0; } pub fn maxRead(c: *const Client) u32 { if (c.msize == 0) return 0; return c.msize - @as(u32, @intCast(rread_header)); } pub fn maxWrite(c: *const Client) u32 { if (c.msize == 0) return 0; return c.msize - @as(u32, @intCast(twrite_header)); } pub fn pending(c: *const Client) usize { var n: usize = @intFromBool(c.versioning); for (c.tags) |t| n += @intFromBool(t.op != null); return n; } pub fn submit(c: *Client, req: Request) ClientError!u16 { if (c.dead) return error.Dead; if (req == .version) return c.beginVersion(req.version.msize); if (c.msize == 0 or c.versioning) return error.Handshake; const msg: Msg = switch (req) { .version => unreachable, // handled above .attach => |m| blk: { if (m.fid == nofid) return error.BadRequest; break :blk .{ .tattach = .{ .fid = m.fid, .afid = nofid, .uname = m.uname, .aname = m.aname, } }; }, .walk => |m| blk: { if (m.fid == nofid or m.newfid == nofid) return error.BadRequest; if (m.names.len > max_welem) return error.BadRequest; var w: [max_welem][]const u8 = @splat(""); for (m.names, 0..) |n, i| { if (n.len == 0) return error.BadRequest; if (std.mem.indexOfAny(u8, n, "/\x00") != null) return error.BadRequest; w[i] = n; } break :blk .{ .twalk = .{ .fid = m.fid, .newfid = m.newfid, .nwname = @intCast(m.names.len), .wname = w, } }; }, .open => |m| blk: { if (m.fid == nofid) return error.BadRequest; break :blk .{ .topen = .{ .fid = m.fid, .mode = m.mode } }; }, .read => |m| blk: { if (m.fid == nofid) return error.BadRequest; if (m.count > c.maxRead()) return error.TooLarge; break :blk .{ .tread = .{ .fid = m.fid, .offset = m.offset, .count = m.count } }; }, .write => |m| blk: { if (m.fid == nofid) return error.BadRequest; break :blk .{ .twrite = .{ .fid = m.fid, .offset = m.offset, .data = m.data } }; }, .clunk => |m| blk: { if (m.fid == nofid) return error.BadRequest; break :blk .{ .tclunk = .{ .fid = m.fid } }; }, .stat => |m| blk: { if (m.fid == nofid) return error.BadRequest; break :blk .{ .tstat = .{ .fid = m.fid } }; }, }; const need = totalLen(msg) catch return error.TooLarge; if (need > c.msize) return error.TooLarge; const op = std.meta.activeTag(req); const tag = c.claim(op) orelse return error.NoTags; errdefer c.tags[tag] = .{}; try c.emit(tag, msg); if (op == .read) c.tags[tag].count = req.read.count; return tag; } fn beginVersion(c: *Client, want: u32) ClientError!u16 { if (c.pending() != 0) return error.Handshake; const cap: u32 = @intCast(@min(c.in.len, c.out.len, std.math.maxInt(u32))); const m = @min(if (want == 0) cap else want, cap); if (m < msize_min) return error.BadRequest; try c.emit(notag, .{ .tversion = .{ .msize = m, .version = "9P2000" } }); c.msize = 0; c.asked = m; c.versioning = true; return notag; } fn claim(c: *Client, op: Op) ?u16 { for (&c.tags, 0..) |*t, i| { if (t.op != null) continue; t.* = .{ .op = op }; return @intCast(i); } return null; } fn emit(c: *Client, tag: u16, msg: Msg) ClientError!void { if (c.out_off != 0) c.compact(); const bytes = encode(msg, tag, c.out[c.out_len..]) catch return error.NoSpace; c.out_len += bytes.len; } pub fn take(c: *Client) ?Done { if (c.frame != 0) c.dropFrame(); if (c.dead) return null; const len = frameLen(c.in[0..c.in_len]) orelse return null; if (len < header_len or len > c.in.len) return c.die(); if (c.msize != 0 and len > c.msize) return c.die(); if (len > c.in_len) return null; c.frame = len; const got = decode(c.in[0..len]) catch return c.die(); return c.consume(got); } fn die(c: *Client) ?Done { c.dead = true; return null; } fn consume(c: *Client, got: Decoded) ?Done { if (isT(got.msg.msgType())) return c.die(); if (got.msg == .rversion) return c.version(got); if (c.versioning or c.msize == 0) return c.die(); if (got.tag >= max_tags) return c.die(); const slot = &c.tags[got.tag]; const op = slot.op orelse return c.die(); const result: Result = switch (got.msg) { .rerror => |m| .{ .fail = m.ename }, .rattach => |m| if (op != .attach) return c.die() else .{ .attach = m.qid }, .rwalk => |m| if (op != .walk) return c.die() else .{ .walk = .{ .nwqid = m.nwqid, .wqid = m.wqid }, }, .ropen => |m| if (op != .open) return c.die() else .{ .open = .{ .qid = m.qid, .iounit = m.iounit }, }, .rread => |m| blk: { if (op != .read) return c.die(); if (m.data.len > slot.count) return c.die(); break :blk .{ .read = m.data }; }, .rwrite => |m| if (op != .write) return c.die() else .{ .write = m.count }, .rclunk => if (op != .clunk) return c.die() else .clunk, .rstat => |m| if (op != .stat) return c.die() else .{ .stat = m.stat }, else => return c.die(), }; slot.* = .{}; return .{ .tag = got.tag, .op = op, .result = result }; } fn version(c: *Client, got: Decoded) ?Done { if (!c.versioning) return c.die(); if (got.tag != notag) return c.die(); const m = got.msg.rversion; if (m.msize > c.asked or m.msize < msize_min) return c.die(); c.versioning = false; if (std.mem.eql(u8, m.version, "9P2000")) { c.msize = m.msize; } else if (!std.mem.eql(u8, m.version, "unknown")) { return c.die(); } return .{ .tag = notag, .op = .version, .result = .{ .version = .{ .msize = m.msize, .version = m.version }, } }; } }; const Pair = struct { srv_in: [4096]u8 = undefined, srv_out: [8192]u8 = undefined, cli_in: [4096]u8 = undefined, cli_out: [4096]u8 = undefined, fsys: StubFs = .{}, srv: Srv = undefined, cli: Client = undefined, fn start(p: *Pair) void { p.srv = Srv.init(.{ .in = &p.srv_in, .out = &p.srv_out, .root = 1 }); p.cli = Client.init(.{ .in = &p.cli_in, .out = &p.cli_out }); } fn answer(p: *Pair, req: StubFs.Req) void { const a = p.fsys.handle(req); p.srv.reply(&a.reply, a.bytes); } fn wire(p: *Pair) void { var moved = true; while (moved) { moved = false; while (p.cli.output().len != 0) { const n = p.srv.push(p.cli.output()); if (n == 0) break; p.cli.wrote(n); moved = true; } while (p.srv.retry()) |req| { p.answer(req); moved = true; } while (p.srv.next()) |req| { p.answer(req); moved = true; } while (p.srv.output().len != 0) { const n = p.cli.push(p.srv.output()); if (n == 0) break; p.srv.wrote(n); moved = true; } } } fn one(p: *Pair, req: Client.Request) !Client.Done { const tag = try p.cli.submit(req); p.wire(); const done = p.cli.take() orelse return error.NoReply; try testing.expectEqual(tag, done.tag); try testing.expectEqual(std.meta.activeTag(req), done.op); try testing.expectEqual(@as(usize, 0), p.cli.pending()); return done; } fn handshake(p: *Pair) !void { p.start(); const v = try p.one(.{ .version = .{} }); try testing.expectEqualStrings("9P2000", v.result.version.version); try testing.expectEqual(@as(u16, notag), v.tag); const a = try p.one(.{ .attach = .{ .fid = 0, .uname = "goblin" } }); try testing.expectEqual(@as(u64, 1), a.result.attach.path); try testing.expectEqual(qtdir, a.result.attach.type); } }; test "9p client: a whole session against the server in this file" { var p: Pair = .{}; try p.handshake(); try testing.expectEqual(@as(u32, 4096), p.cli.msize); try testing.expectEqual(@as(u32, 4096 - 11), p.cli.maxRead()); try testing.expectEqual(@as(u32, 4096 - 23), p.cli.maxWrite()); const w = try p.one(.{ .walk = .{ .fid = 0, .newfid = 1, .names = &.{ "1", "body" } } }); try testing.expectEqual(@as(u16, 2), w.result.walk.nwqid); try testing.expectEqual(@as(u64, 16), w.result.walk.wqid[0].path); try testing.expectEqual(@as(u64, 18), w.result.walk.wqid[1].path); try testing.expectEqual(qtfile, w.result.walk.wqid[1].type); const o = try p.one(.{ .open = .{ .fid = 1, .mode = ordwr } }); try testing.expectEqual(@as(u64, 18), o.result.open.qid.path); try testing.expectEqual(@as(u32, 4096 - iohdrsz), o.result.open.iounit); const r = try p.one(.{ .read = .{ .fid = 1, .offset = 0, .count = 64 } }); try testing.expectEqualStrings("hello, body\n", r.result.read); const eof = try p.one(.{ .read = .{ .fid = 1, .offset = 12, .count = 64 } }); try testing.expectEqual(@as(usize, 0), eof.result.read.len); const wr = try p.one(.{ .write = .{ .fid = 1, .offset = 0, .data = "abc" } }); try testing.expectEqual(@as(u32, 3), wr.result.write); try testing.expectEqualStrings("abc", p.fsys.writes[0..p.fsys.writes_len]); const st = try p.one(.{ .stat = .{ .fid = 1 } }); try testing.expectEqualStrings("body", st.result.stat.name); try testing.expectEqual(@as(u64, 12), st.result.stat.length); try testing.expectEqualStrings("goblin", st.result.stat.uid); _ = try p.one(.{ .clunk = .{ .fid = 1 } }); try testing.expectEqual(@as(u32, 1), p.fsys.releases); try testing.expectEqual(@as(usize, 0), p.cli.pending()); try testing.expectEqual(@as(usize, 0), p.cli.output().len); try testing.expect(p.cli.take() == null); try testing.expect(!p.cli.dead); } fn deliverReversed(p: *Pair) !void { var scratch: [4096]u8 = undefined; const out = p.srv.output(); try testing.expect(out.len <= scratch.len); @memcpy(scratch[0..out.len], out); const total = out.len; p.srv.wrote(total); var at: [max_tags]usize = undefined; var lens: [max_tags]u32 = undefined; var count: usize = 0; var i: usize = 0; while (i < total) { const len = frameLen(scratch[i..total]) orelse return error.ShortReply; at[count] = i; lens[count] = len; count += 1; i += len; } try testing.expect(count >= 2); var k = count; while (k > 0) { k -= 1; const f = scratch[at[k]..][0..lens[k]]; try testing.expectEqual(f.len, p.cli.push(f)); } } test "9p client: replies out of order are matched by tag and not by arrival" { var p: Pair = .{}; try p.handshake(); const w = try p.one(.{ .walk = .{ .fid = 0, .newfid = 1, .names = &.{"index"} } }); try testing.expectEqual(@as(u16, 1), w.result.walk.nwqid); const root_tag = try p.cli.submit(.{ .stat = .{ .fid = 0 } }); const index_tag = try p.cli.submit(.{ .stat = .{ .fid = 1 } }); try testing.expectEqual(@as(u16, 0), root_tag); try testing.expectEqual(@as(u16, 1), index_tag); try testing.expectEqual(@as(usize, 2), p.cli.pending()); while (p.cli.output().len != 0) { const n = p.srv.push(p.cli.output()); p.cli.wrote(n); } while (p.srv.next()) |req| p.answer(req); try deliverReversed(&p); const first = p.cli.take() orelse return error.NoReply; try testing.expectEqual(index_tag, first.tag); try testing.expectEqualStrings("index", first.result.stat.name); const second = p.cli.take() orelse return error.NoReply; try testing.expectEqual(root_tag, second.tag); try testing.expectEqualStrings("/", second.result.stat.name); try testing.expectEqual(@as(usize, 0), p.cli.pending()); try testing.expect(!p.cli.dead); } test "9p client: an Rerror answers one operation and the session carries on" { var p: Pair = .{}; try p.handshake(); const bad = try p.one(.{ .walk = .{ .fid = 0, .newfid = 1, .names = &.{"nope"} } }); try testing.expectEqual(Client.Op.walk, bad.op); try testing.expectEqualStrings(errString(2), bad.result.fail); try testing.expectEqual(@as(usize, 0), p.cli.pending()); try testing.expect(!p.cli.dead); const st = try p.one(.{ .stat = .{ .fid = 0 } }); try testing.expectEqualStrings("/", st.result.stat.name); const stale = try p.one(.{ .stat = .{ .fid = 9 } }); try testing.expectEqualStrings(e_unknown_fid, stale.result.fail); try testing.expect(!p.cli.dead); } test "9p client: a reply arriving a byte at a time is taken when its last byte lands" { var p: Pair = .{}; try p.handshake(); const tag = try p.cli.submit(.{ .stat = .{ .fid = 0 } }); while (p.cli.output().len != 0) { const n = p.srv.push(p.cli.output()); p.cli.wrote(n); } while (p.srv.next()) |req| p.answer(req); var scratch: [512]u8 = undefined; const out = p.srv.output(); try testing.expect(out.len > 4 and out.len <= scratch.len); @memcpy(scratch[0..out.len], out); const reply = scratch[0..out.len]; p.srv.wrote(reply.len); for (reply[0 .. reply.len - 1]) |b| { try testing.expectEqual(@as(usize, 1), p.cli.push(&.{b})); try testing.expect(p.cli.take() == null); try testing.expect(!p.cli.dead); } try testing.expectEqual(@as(usize, 1), p.cli.push(reply[reply.len - 1 ..])); const done = p.cli.take() orelse return error.NoReply; try testing.expectEqual(tag, done.tag); try testing.expectEqualStrings("/", done.result.stat.name); } test "9p client: sixteen tags outstanding, and the seventeenth is refused" { var p: Pair = .{}; try p.handshake(); var tags: [max_tags]u16 = undefined; for (&tags, 0..) |*t, i| { t.* = try p.cli.submit(.{ .stat = .{ .fid = 0 } }); try testing.expectEqual(@as(u16, @intCast(i)), t.*); } try testing.expectEqual(max_tags, p.cli.pending()); try testing.expectError(error.NoTags, p.cli.submit(.{ .stat = .{ .fid = 0 } })); const owed = p.cli.output().len; try testing.expectError(error.NoTags, p.cli.submit(.{ .clunk = .{ .fid = 0 } })); try testing.expectEqual(owed, p.cli.output().len); p.wire(); var seen: [max_tags]bool = @splat(false); for (0..max_tags) |_| { const done = p.cli.take() orelse return error.NoReply; try testing.expectEqual(Client.Op.stat, done.op); try testing.expect(!seen[done.tag]); seen[done.tag] = true; } for (seen) |s| try testing.expect(s); try testing.expectEqual(@as(usize, 0), p.cli.pending()); _ = try p.one(.{ .stat = .{ .fid = 0 } }); } test "9p client: what a caller may not ask for is refused before a tag is spent" { var p: Pair = .{}; p.start(); try testing.expectError(error.Handshake, p.cli.submit(.{ .stat = .{ .fid = 0 } })); try p.handshake(); try testing.expectError(error.BadRequest, p.cli.submit(.{ .stat = .{ .fid = nofid } })); try testing.expectError(error.BadRequest, p.cli.submit(.{ .clunk = .{ .fid = nofid } })); try testing.expectError(error.BadRequest, p.cli.submit(.{ .walk = .{ .fid = 0, .newfid = nofid, .names = &.{} } })); try testing.expectError(error.BadRequest, p.cli.submit(.{ .walk = .{ .fid = 0, .newfid = 1, .names = &.{"1/body"} } })); try testing.expectError(error.BadRequest, p.cli.submit(.{ .walk = .{ .fid = 0, .newfid = 1, .names = &.{""} } })); const seventeen: [max_welem + 1][]const u8 = @splat("x"); try testing.expectError(error.BadRequest, p.cli.submit(.{ .walk = .{ .fid = 0, .newfid = 1, .names = &seventeen } })); try testing.expectError(error.TooLarge, p.cli.submit(.{ .read = .{ .fid = 0, .offset = 0, .count = p.cli.maxRead() + 1, } })); var big: [4096]u8 = @splat('x'); try testing.expectError(error.TooLarge, p.cli.submit(.{ .write = .{ .fid = 0, .offset = 0, .data = big[0 .. p.cli.maxWrite() + 1], } })); p.cli.wrote(p.cli.output().len); _ = try p.cli.submit(.{ .read = .{ .fid = 0, .offset = 0, .count = p.cli.maxRead() } }); p.cli.wrote(p.cli.output().len); _ = try p.cli.submit(.{ .write = .{ .fid = 0, .offset = 0, .data = big[0..p.cli.maxWrite()] } }); try testing.expectError(error.Handshake, p.cli.submit(.{ .version = .{} })); try testing.expectError(error.NoSpace, p.cli.submit(.{ .stat = .{ .fid = 0 } })); } test "9p client: an msize below the floor, and one the server tried to raise" { var in: [512]u8 = undefined; var out: [512]u8 = undefined; var buf: [64]u8 = undefined; var c = Client.init(.{ .in = &in, .out = &out }); try testing.expectError(error.BadRequest, c.submit(.{ .version = .{ .msize = msize_min - 1 } })); _ = try c.submit(.{ .version = .{} }); try testing.expectEqual(@as(u32, 512), c.asked); _ = c.push(try encode(.{ .rversion = .{ .msize = 1024, .version = "9P2000" } }, notag, &buf)); try testing.expect(c.take() == null); try testing.expect(c.dead); var c2 = Client.init(.{ .in = &in, .out = &out }); _ = try c2.submit(.{ .version = .{} }); _ = c2.push(try encode(.{ .rversion = .{ .msize = 512, .version = "unknown" } }, notag, &buf)); const done = c2.take() orelse return error.NoReply; try testing.expectEqualStrings("unknown", done.result.version.version); try testing.expect(!c2.dead); try testing.expectEqual(@as(u32, 0), c2.msize); try testing.expectError(error.Handshake, c2.submit(.{ .stat = .{ .fid = 0 } })); var c3 = Client.init(.{ .in = &in, .out = &out }); _ = try c3.submit(.{ .version = .{} }); _ = c3.push(try encode(.{ .rversion = .{ .msize = 512, .version = "9P2000.u" } }, notag, &buf)); try testing.expect(c3.take() == null); try testing.expect(c3.dead); } test "9p client: what is not an answer to one of our requests ends the connection" { var buf: [64]u8 = undefined; const Case = struct { fn armed(in: []u8, out: []u8, scratch: []u8) !Client { var c = Client.init(.{ .in = in, .out = out }); _ = try c.submit(.{ .version = .{} }); c.wrote(c.output().len); _ = c.push(try encode(.{ .rversion = .{ .msize = 512, .version = "9P2000" } }, notag, scratch)); _ = c.take() orelse return error.NoReply; _ = try c.submit(.{ .stat = .{ .fid = 0 } }); c.wrote(c.output().len); return c; } }; var in: [512]u8 = undefined; var out: [512]u8 = undefined; { var c = try Case.armed(&in, &out, &buf); _ = c.push(try encode(.{ .tstat = .{ .fid = 0 } }, 0, &buf)); try testing.expect(c.take() == null); try testing.expect(c.dead); } { var c = try Case.armed(&in, &out, &buf); _ = c.push(try encode(.rclunk, 3, &buf)); try testing.expect(c.take() == null); try testing.expect(c.dead); } { var c = try Case.armed(&in, &out, &buf); _ = c.push(try encode(.rclunk, 900, &buf)); try testing.expect(c.take() == null); try testing.expect(c.dead); } { var c = try Case.armed(&in, &out, &buf); _ = c.push(try encode(.rclunk, 0, &buf)); try testing.expect(c.take() == null); try testing.expect(c.dead); } { var c = try Case.armed(&in, &out, &buf); _ = c.push(try encode(.rwstat, 0, &buf)); try testing.expect(c.take() == null); try testing.expect(c.dead); } { var c = try Case.armed(&in, &out, &buf); _ = c.push(&.{ 3, 0, 0, 0 }); try testing.expect(c.take() == null); try testing.expect(c.dead); } { var c = try Case.armed(&in, &out, &buf); _ = c.push(&.{ 0, 4, 0, 0 }); try testing.expect(c.take() == null); try testing.expect(c.dead); } { var c = try Case.armed(&in, &out, &buf); _ = c.push(&.{ 8, 0, 0, 0, @intFromEnum(Type.rstat), 0, 0, 0 }); try testing.expect(c.take() == null); try testing.expect(c.dead); } } test "9p client: an Rread longer than the Tread asked for is refused" { var p: Pair = .{}; try p.handshake(); _ = try p.one(.{ .walk = .{ .fid = 0, .newfid = 1, .names = &.{ "1", "body" } } }); _ = try p.one(.{ .open = .{ .fid = 1, .mode = oread } }); const tag = try p.cli.submit(.{ .read = .{ .fid = 1, .offset = 0, .count = 4 } }); p.cli.wrote(p.cli.output().len); var buf: [64]u8 = undefined; _ = p.cli.push(try encode(.{ .rread = .{ .data = "hello, body\n" } }, tag, &buf)); try testing.expect(p.cli.take() == null); try testing.expect(p.cli.dead); var q: Pair = .{}; try q.handshake(); _ = try q.one(.{ .walk = .{ .fid = 0, .newfid = 1, .names = &.{ "1", "body" } } }); _ = try q.one(.{ .open = .{ .fid = 1, .mode = oread } }); const short = try q.one(.{ .read = .{ .fid = 1, .offset = 0, .count = 4 } }); try testing.expectEqualStrings("hell", short.result.read); } test "9p client: hangup and a dead connection refuse everything after" { var p: Pair = .{}; try p.handshake(); p.cli.hangup(); try testing.expectEqual(@as(usize, 0), p.cli.pending()); try testing.expectEqual(@as(usize, 0), p.cli.output().len); try testing.expectEqual(@as(usize, 0), p.cli.push("anything")); try testing.expect(p.cli.take() == null); try testing.expectError(error.Dead, p.cli.submit(.{ .stat = .{ .fid = 0 } })); try testing.expectError(error.Dead, p.cli.submit(.{ .version = .{} })); } test "9p client: one session is a hundred and change bytes plus its buffers" { try testing.expect(@sizeOf(Client.Slot) <= 8); try testing.expect(@sizeOf(Client) <= 256); try testing.expect(2 * 8192 + @sizeOf(Client) <= 17 * 1024); try testing.expect(2 * msize_min + @sizeOf(Client) <= 700); }