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