From 95681ff7017b8a9e4c8f9fa6a7371d1233432f2f Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 14 Sep 2026 13:55:41 -0300 Subject: 9p: use cloud9 protocol sessions and transports --- src/9p.zig | 1676 ++++-------------------------------------------------------- 1 file changed, 108 insertions(+), 1568 deletions(-) (limited to 'src/9p.zig') diff --git a/src/9p.zig b/src/9p.zig index 61a08b84..9513fdb5 100644 --- a/src/9p.zig +++ b/src/9p.zig @@ -1,704 +1,45 @@ +//! Pardes filesystem adapter. The 9P protocol and transports live in cloud9. 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 cloud9 = @import("cloud9"); +pub const Error = cloud9.Error; +pub const Type = cloud9.Type; +pub const isT = cloud9.isT; +pub const header_len = cloud9.header_len; +pub const qid_len = cloud9.qid_len; +pub const stat_fixed = cloud9.stat_fixed; +pub const notag = cloud9.notag; +pub const nofid = cloud9.nofid; +pub const max_welem = cloud9.max_welem; +pub const iohdrsz = cloud9.iohdrsz; +pub const Qid = cloud9.Qid; +pub const Stat = cloud9.Stat; +pub const Msg = cloud9.Msg; +pub const Decoded = cloud9.Decoded; +pub const frameLen = cloud9.frameLen; +pub const encode = cloud9.encode; +pub const decode = cloud9.decode; +pub const qtdir = cloud9.qtdir; +pub const qtappend = cloud9.qtappend; +pub const qtexcl = cloud9.qtexcl; +pub const qtmount = cloud9.qtmount; +pub const qtauth = cloud9.qtauth; +pub const qttmp = cloud9.qttmp; +pub const qtfile = cloud9.qtfile; +pub const dmdir = cloud9.dmdir; +pub const dmappend = cloud9.dmappend; +pub const dmexcl = cloud9.dmexcl; +pub const dmmount = cloud9.dmmount; +pub const dmauth = cloud9.dmauth; +pub const dmtmp = cloud9.dmtmp; +pub const dmperm = cloud9.dmperm; +pub const Client = cloud9.Client; +pub const ClientError = cloud9.ClientError; +pub const max_tags = cloud9.max_tags; +const totalLen = cloud9.encodedLen; +pub const errmax: usize = 128; +pub const min_msize: u32 = 4096; const sample_qid: Qid = .{ .type = qtdir, .version = 3, .path = 0x0102_0304_0506_0708 }; const sample_stat: Stat = .{ @@ -715,435 +56,6 @@ const sample_stat: Stat = .{ .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"; @@ -1222,18 +134,9 @@ pub fn Server(comptime fs: type, comptime fid_capacity: usize) type { return struct { const Self = @This(); - in: []u8, - out: []u8, + protocol: cloud9.Server, 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, @@ -1306,7 +209,7 @@ pub fn Server(comptime fs: type, comptime fid_capacity: usize) type { 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 }; + return .{ .protocol = .init(.{ .in = opts.in, .out = opts.out }), .root = opts.root }; } pub fn references(s: *const Self, node: u64) bool { @@ -1318,11 +221,7 @@ pub fn Server(comptime fs: type, comptime fid_capacity: usize) type { 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; + s.protocol.hangup(); } fn reset(s: *Self) void { @@ -1335,45 +234,25 @@ pub fn Server(comptime fs: type, comptime fid_capacity: usize) type { } 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; + return s.protocol.push(bytes); } pub fn output(s: *const Self) []const u8 { - return s.out[s.out_off..s.out_len]; + return s.protocol.output(); } 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; + s.protocol.wrote(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); + return s.protocol.hasRoom(); } 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.protocol.reply(tag, msg) catch { + s.protocol.dead = true; }; - s.out_len += bytes.len; } fn fail(s: *Self, tag: u16, ename: []const u8) void { @@ -1477,44 +356,14 @@ pub fn Server(comptime fs: type, comptime fid_capacity: usize) type { 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; + const got = (s.protocol.receive() catch return false) orelse return false; 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; + s.protocol.release(); } fn dispatch(s: *Self, got: Decoded) void { @@ -1537,13 +386,16 @@ pub fn Server(comptime fs: type, comptime fid_capacity: usize) type { } 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"); + assert(tag == notag); s.reset(); - s.msize = if (known) m else 0; - s.emit(tag, .{ .rversion = .{ .msize = m, .version = if (known) "9P2000" else "unknown" } }); + // The adapter's fixed directory staging requires this capacity. + if (want < msize_min) { + s.protocol.dead = true; + return; + } + s.protocol.negotiate(want, ver) catch { + s.protocol.dead = true; + }; } fn attach(s: *Self, tag: u16, fid: u32, uname: []const u8, aname: []const u8) void { @@ -1610,7 +462,7 @@ pub fn Server(comptime fs: type, comptime fid_capacity: usize) type { 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); + const want = @min(count, s.protocol.msize - header_len - 4); if (!f.dir) { s.job = .{ .kind = .read, .tag = tag, .fid = fid, .offset = offset, .count = want }; return; @@ -1821,7 +673,7 @@ pub fn Server(comptime fs: type, comptime fid_capacity: usize) type { fn finishJob(s: *Self) void { s.job = .{}; - if (s.frame != 0) s.dropFrame(); + if (s.protocol.frame != 0) s.dropFrame(); } fn jobReply(s: *Self, r: *const fs.Reply, bytes: []const u8) void { @@ -1893,7 +745,7 @@ pub fn Server(comptime fs: type, comptime fid_capacity: usize) type { f.dirindex = 0; s.emit(j.tag, .{ .ropen = .{ .qid = qidOf(f.node, f.dir), - .iounit = s.msize - iohdrsz, + .iounit = s.protocol.msize - iohdrsz, } }); s.finishJob(); }, @@ -1914,7 +766,7 @@ pub fn Server(comptime fs: type, comptime fid_capacity: usize) type { 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) + if ((totalLen(response) catch unreachable) > s.protocol.msize) s.fail(j.tag, e_small_msize) else s.emit(j.tag, response); @@ -1988,7 +840,7 @@ pub fn Server(comptime fs: type, comptime fid_capacity: usize) type { } fn emitDirRead(s: *Self, tag: u16, fid: u32, staging: []const u8, count: u32) void { - const buf = s.out[s.out_len..]; + const buf = s.protocol.out[s.protocol.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]; @@ -2022,11 +874,7 @@ pub fn Server(comptime fs: type, comptime fid_capacity: usize) type { 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; + s.emit(tag, .{ .rread = .{ .data = buf[header_len + 4 .. n] } }); if (s.findFid(fid)) |k| { s.fids[k].diroff += payload; s.fids[k].dirindex += entries; @@ -2315,12 +1163,10 @@ test "9p server: the version handshake clamps, falls back, and refuses" { 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 testing.expect(h.srv.protocol.dead); + h.start(); 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.expect(h.srv.protocol.dead); try testing.expectEqual(@as(u32, 216), msize_min - 1); } @@ -2450,7 +1296,7 @@ test "9p server: backend-sized filenames survive walk and stat within negotiated 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.protocol.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]; @@ -2489,7 +1335,7 @@ test "9p server: backend-sized filenames survive walk and stat within negotiated try testing.expectEqual(null, server.next()); response = try decode(server.output()); try testing.expect(response.msg == .rclunk); - try testing.expect(!server.dead); + try testing.expect(!server.protocol.dead); } } @@ -2636,6 +1482,13 @@ test "9p server: long directory names remain whole across pages" { @memcpy(entries[275..], "next"); var found: [2][]const u8 = undefined; + { + var request: [64]u8 = undefined; + const bytes = try encode(.{ .tread = .{ .fid = 0, .offset = 0, .count = 330 } }, 5, &request); + _ = h.srv.protocol.push(bytes); + _ = (try h.srv.protocol.receive()).?; + h.srv.protocol.release(); + } h.srv.emitDirRead(5, 0, &entries, 330); var got = try h.reap(); const first = got.msg.rread.data.len; @@ -2643,6 +1496,13 @@ test "9p server: long directory names remain whole across pages" { try testing.expectEqualStrings(entries[10..265], found[0]); try testing.expectEqual(@as(u64, 1), h.srv.fids[0].dirindex); + { + var request: [64]u8 = undefined; + const bytes = try encode(.{ .tread = .{ .fid = 0, .offset = 0, .count = 330 } }, 6, &request); + _ = h.srv.protocol.push(bytes); + _ = (try h.srv.protocol.receive()).?; + h.srv.protocol.release(); + } 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)); @@ -2650,6 +1510,13 @@ test "9p server: long directory names remain whole across pages" { 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); + { + var request: [64]u8 = undefined; + const bytes = try encode(.{ .tread = .{ .fid = 0, .offset = 0, .count = 4096 } }, 7, &request); + _ = h.srv.protocol.push(bytes); + _ = (try h.srv.protocol.receive()).?; + h.srv.protocol.release(); + } h.srv.emitDirRead(7, 0, &entries, 4096); got = try h.reap(); try testing.expectEqual(@as(usize, 2), try dirNames(got.msg.rread.data, &found)); @@ -2728,7 +1595,7 @@ test "9p server: the reply queue is a FIFO that survives a partial write" { 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.expect(h.srv.protocol.dead); try testing.expectEqual(@as(usize, 0), h.srv.push(&flood)); } @@ -2938,36 +1805,18 @@ test "9p server: a message arriving a byte at a time is served when its last byt 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: invalid framing and reply types terminate the connection" { + for ([_]u8{ @intFromEnum(Type.rstat), 8, 0 }) |kind| { + var h: Harness = .{}; + try h.handshake(4096); + var buf: [64]u8 = undefined; + const raw = try encode(.{ .tstat = .{ .fid = 0 } }, 5, &buf); + if (kind == 0) std.mem.writeInt(u32, raw[0..4], 3, .little) else raw[4] = kind; + _ = h.srv.push(raw); + h.pump(); + try h.quiet(); + try testing.expect(h.srv.protocol.dead); + } } test "9p server: a connection that drops still pays the core its releases" { @@ -3060,314 +1909,6 @@ test "9p server: board and native capacities size the actual fid storage" { 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, @@ -3647,7 +2188,7 @@ test "9p client: an msize below the floor, and one the server tried to raise" { 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 testing.expectError(error.BadRequest, c.submit(.{ .version = .{ .msize = cloud9.Server.msize_min - 1 } })); _ = try c.submit(.{ .version = .{} }); try testing.expectEqual(@as(u32, 512), c.asked); @@ -3771,9 +2312,8 @@ test "9p client: hangup and a dead connection refuse everything after" { 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); +test "9p client: session memory is bounded independently of input" { + try testing.expect(@sizeOf(Client) <= 512); try testing.expect(2 * 8192 + @sizeOf(Client) <= 17 * 1024); - try testing.expect(2 * msize_min + @sizeOf(Client) <= 700); + try testing.expect(2 * cloud9.Server.msize_min + @sizeOf(Client) <= 700); } -- cgit v1.3