diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-14 13:55:41 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-09-15 17:24:42 -0300 |
| commit | 95681ff7017b8a9e4c8f9fa6a7371d1233432f2f (patch) | |
| tree | 34787d6bb62944cc778624685387db7376ccf71b /src | |
| parent | 682e237df7e8b22f820d14a4adee58e6e2f84268 (diff) | |
| download | pardes-95681ff7017b8a9e4c8f9fa6a7371d1233432f2f.tar.gz pardes-95681ff7017b8a9e4c8f9fa6a7371d1233432f2f.zip | |
9p: use cloud9 protocol sessions and transports
Diffstat (limited to 'src')
| -rw-r--r-- | src/9p.zig | 1676 | ||||
| -rw-r--r-- | src/9p_io.zig | 230 | ||||
| -rw-r--r-- | src/9p_quic.zig | 565 | ||||
| -rw-r--r-- | src/builtins.zig | 1 | ||||
| -rw-r--r-- | src/config.zig | 69 | ||||
| -rw-r--r-- | src/esp32p4_9p.zig | 2 | ||||
| -rw-r--r-- | src/gui/gui.zig | 242 |
7 files changed, 436 insertions, 2349 deletions
@@ -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); } diff --git a/src/9p_io.zig b/src/9p_io.zig index 89611c2b..943561eb 100644 --- a/src/9p_io.zig +++ b/src/9p_io.zig @@ -2,6 +2,7 @@ const std = @import("std"); const libc = std.c; const builtin = @import("builtin"); const ninep = @import("9p.zig"); +const transport = @import("cloud9").transport; const pardes = @import("pardes.zig"); const limits = @import("memory.zig").limits; pub const quic_enabled = @import("9p_options").quic; @@ -93,21 +94,6 @@ fn canonicalIp(address: std.Io.net.IpAddress) std.Io.net.IpAddress { return address; } -fn ipSockaddr(address: std.Io.net.IpAddress, out: *libc.sockaddr.storage) libc.socklen_t { - return switch (address) { - .ip4 => |ip| blk: { - const addr: *libc.sockaddr.in = @ptrCast(out); - addr.* = .{ .port = std.mem.nativeToBig(u16, ip.port), .addr = @bitCast(ip.bytes) }; - break :blk @sizeOf(libc.sockaddr.in); - }, - .ip6 => |ip| blk: { - const addr: *libc.sockaddr.in6 = @ptrCast(out); - addr.* = .{ .port = std.mem.nativeToBig(u16, ip.port), .addr = ip.bytes, .flowinfo = 0, .scope_id = 0 }; - break :blk @sizeOf(libc.sockaddr.in6); - }, - }; -} - fn sockaddrIp(address: *const libc.sockaddr) ?std.Io.net.IpAddress { return switch (address.family) { libc.AF.INET => blk: { @@ -207,25 +193,11 @@ pub const Listener = struct { } fn listenTcp(l: *Listener, address: std.Io.net.IpAddress) !void { + const fd = try transport.listenFd(.{ .tcp = address }, max_conns); + errdefer transport.close(fd); var addr: libc.sockaddr.storage = undefined; - const addr_len = ipSockaddr(address, &addr); - const fd = libc.socket(addr.family, libc.SOCK.STREAM, 0); - if (fd < 0) return error.SocketFailed; - errdefer _ = libc.close(fd); - setCloexec(fd); - setNonblock(fd); - const on: c_int = 1; - if (libc.setsockopt(fd, libc.SOL.SOCKET, libc.SO.REUSEADDR, &on, @sizeOf(c_int)) != 0) - return error.SocketOptionFailed; - if (address == .ip6) { - const v6only = if (darwin) 27 else std.os.linux.IPV6.V6ONLY; - if (libc.setsockopt(fd, libc.IPPROTO.IPV6, v6only, &on, @sizeOf(c_int)) != 0) - return error.SocketOptionFailed; - } - if (libc.bind(fd, @ptrCast(&addr), addr_len) != 0) return error.BindFailed; - if (libc.listen(fd, max_conns) != 0) return error.ListenFailed; - var actual_len: libc.socklen_t = @sizeOf(libc.sockaddr.storage); - if (libc.getsockname(fd, @ptrCast(&addr), &actual_len) != 0) return error.SocketAddressFailed; + var len: libc.socklen_t = @sizeOf(@TypeOf(addr)); + if (libc.getsockname(fd, @ptrCast(&addr), &len) != 0) return error.SocketAddressFailed; l.tcp_address = sockaddrIp(@ptrCast(&addr)) orelse return error.SocketAddressFailed; l.tcp_fd = fd; log.info("serving 9P2000 over TCP on {f}", .{l.tcp_address.?}); @@ -236,26 +208,11 @@ pub const Listener = struct { for ([_]c_int{ l.fd, l.tcp_fd }) |listener_fd| { if (listener_fd < 0) continue; for (0..max_conns + 1) |_| { - const fd = libc.accept(listener_fd, null, null); - if (fd < 0) switch (libc.errno(fd)) { - .AGAIN => break, - .INTR, .CONNABORTED => continue, - else => { - l.paused_ms = nowMs() +| accept_pause_ms; - log.warn("accept failed; pausing the listener for {d} ms", .{accept_pause_ms}); - return; - }, - }; - setCloexec(fd); - setNonblock(fd); - if (listener_fd == l.tcp_fd) { - const on: c_int = 1; - _ = libc.setsockopt(fd, libc.IPPROTO.TCP, libc.TCP.NODELAY, &on, @sizeOf(c_int)); - } - if (comptime darwin) { - const on: c_int = 1; - _ = libc.setsockopt(fd, libc.SOL.SOCKET, libc.SO.NOSIGPIPE, &on, @sizeOf(c_int)); - } + const fd = (transport.acceptFd(listener_fd, listener_fd == l.tcp_fd) catch { + l.paused_ms = nowMs() +| accept_pause_ms; + log.warn("accept failed; pausing the listener for {d} ms", .{accept_pause_ms}); + return; + }) orelse break; const c = for (&l.conns, 0..) |*cand, i| { if (!l.live(@intCast(i)) and !cand.draining) break cand; } else { @@ -301,7 +258,7 @@ pub const Listener = struct { const now = nowMs(); if (now == 0) return; for (&l.conns, 0..) |*c, i| { - if (!l.live(@intCast(i)) or c.srv.msize != 0) continue; + if (!l.live(@intCast(i)) or c.srv.protocol.msize != 0) continue; if (now - c.accepted_ms < greet_deadline_ms) continue; log.debug("slot {d} never sent Tversion; taking it back", .{i}); l.drop(@intCast(i)); @@ -333,7 +290,7 @@ pub const Listener = struct { if (comptime quic_enabled) { if (c.quic) |*connection| if (connection.pending()) return 0; } - if (c.srv.msize != 0) continue; + if (c.srv.protocol.msize != 0) continue; const at = c.accepted_ms + greet_deadline_ms; due = if (due) |d| @min(d, at) else at; } @@ -341,30 +298,22 @@ pub const Listener = struct { return @intCast(@max(0, at - now)); } - fn nowMs() i64 { - var ts: libc.timespec = undefined; - if (libc.clock_gettime(.MONOTONIC, &ts) != 0) return 0; - return @as(i64, ts.sec) * std.time.ms_per_s + @divTrunc(ts.nsec, std.time.ns_per_ms); - } + const nowMs = transport.nowMs; pub fn fill(l: *Listener, i: u8) void { if (comptime !supported) return; const c = &l.conns[i]; - if (c.srv.dead) return l.drop(i); - const room = c.srv.in.len - c.srv.in_len; + if (c.srv.protocol.dead) return l.drop(i); + const room = c.srv.protocol.in.len - c.srv.protocol.in_len; if (room == 0) return; var buf: [msize]u8 = undefined; - const got: isize = if (quic_enabled and c.quic != null) - @intCast((c.quic.?.read(buf[0..@min(room, buf.len)]) catch return l.drop(i)) orelse return) + const got = if (quic_enabled and c.quic != null) + (c.quic.?.read(buf[0..@min(room, buf.len)]) catch return l.drop(i)) orelse return else - libc.read(c.fd, &buf, @min(room, buf.len)); + (transport.read(c.fd, buf[0..@min(room, buf.len)]) catch return l.drop(i)) orelse return; if (got == 0) return l.drop(i); - if (got < 0) return switch (libc.errno(got)) { - .INTR, .AGAIN => {}, - else => l.drop(i), - }; const n = c.srv.push(buf[0..@intCast(got)]); - if (c.srv.dead) return l.drop(i); + if (c.srv.protocol.dead) return l.drop(i); std.debug.assert(n == @as(usize, @intCast(got))); } @@ -375,15 +324,10 @@ pub const Listener = struct { while (true) { const bytes = c.srv.output(); if (bytes.len == 0) return; - const n: isize = if (quic_enabled and c.quic != null) - @intCast(c.quic.?.write(bytes) catch return l.drop(i)) + const n = if (quic_enabled and c.quic != null) + c.quic.?.write(bytes) catch return l.drop(i) else - libc.send(c.fd, bytes.ptr, bytes.len, nosignal); - if (n < 0) switch (libc.errno(n)) { - .INTR => continue, - .AGAIN => return, - else => return l.drop(i), - }; + (transport.write(c.fd, bytes) catch return l.drop(i)) orelse return; if (n == 0) return; c.srv.wrote(@intCast(n)); } @@ -610,37 +554,29 @@ pub fn listen(gpa: std.mem.Allocator, named: []const u8, fallback: []const u8, t gpa.destroy(l); return null; }; - var addr: libc.sockaddr.un = .{ .path = @splat(0) }; - @memcpy(addr.path[0 .. p.len + 1], p[0 .. p.len + 1]); - const fd = libc.socket(libc.AF.UNIX, libc.SOCK.STREAM, 0); - if (fd < 0) { - gpa.destroy(l); - return null; - } - setCloexec(fd); - if (libc.bind(fd, @ptrCast(&addr), @sizeOf(@TypeOf(addr))) != 0) { - const bind_error = libc.errno(-1); + const fd = transport.listenFd(.{ .unix = p }, max_conns) catch |err| retry: { const io = std.Io.Threaded.global_single_threaded.io(); const existing = std.Io.Dir.cwd().statFile(io, p, .{ .follow_symlinks = false }) catch null; - if (bind_error != .ADDRINUSE or existing == null or existing.?.kind != .unix_domain_socket or alive(p)) { + if (err != error.Bind or existing == null or existing.?.kind != .unix_domain_socket or alive(p)) { log.warn("something is already listening on {s}", .{p}); - _ = libc.close(fd); gpa.destroy(l); return null; } - if (libc.unlink(p) != 0 or libc.bind(fd, @ptrCast(&addr), @sizeOf(@TypeOf(addr))) != 0) { - _ = libc.close(fd); + if (libc.unlink(p) != 0) { gpa.destroy(l); return null; } - } - if (libc.chmod(p, 0o600) != 0 or libc.listen(fd, max_conns) != 0) { - _ = libc.close(fd); + break :retry transport.listenFd(.{ .unix = p }, max_conns) catch { + gpa.destroy(l); + return null; + }; + }; + if (libc.chmod(p, 0o600) != 0) { + transport.close(fd); _ = libc.unlink(p); gpa.destroy(l); return null; } - setNonblock(fd); l.fd = fd; l.path_len = p.len; if (tcp_dial) |dial| { @@ -687,18 +623,7 @@ pub fn listen(gpa: std.mem.Allocator, named: []const u8, fallback: []const u8, t return l; } -fn alive(path: [:0]const u8) bool { - var addr: libc.sockaddr.un = .{ .path = @splat(0) }; - if (path.len + 1 > sun_path_len) return true; // cannot ask; assume occupied - @memcpy(addr.path[0 .. path.len + 1], path[0 .. path.len + 1]); - const fd = libc.socket(libc.AF.UNIX, libc.SOCK.STREAM, 0); - if (fd < 0) return true; - defer _ = libc.close(fd); - setCloexec(fd); - setNonblock(fd); - if (libc.connect(fd, @ptrCast(&addr), @sizeOf(@TypeOf(addr))) == 0) return true; - return libc.errno(-1) != .CONNREFUSED; -} +const alive = transport.isListening; fn setNonblock(fd: c_int) void { const flags = libc.fcntl(fd, libc.F.GETFL, @as(c_int, 0)); @@ -708,8 +633,6 @@ fn setNonblock(fd: c_int) void { _ = libc.fcntl(fd, libc.F.SETFL, @as(c_int, @bitCast(@as(u32, @bitCast(o))))); } -const nosignal: u32 = if (darwin) 0 else libc.MSG.NOSIGNAL; - const testing = std.testing; test "the socket name is a third prefix in the shared directory" { @@ -1115,15 +1038,10 @@ pub const Client = struct { return; } } - var fds = [1]libc.pollfd{.{ .fd = s.fd, .events = events, .revents = 0 }}; - const ready = libc.poll(&fds, 1, @intCast(@min(left, budget_ms))); - if (ready < 0) { - if (libc.errno(ready) == .INTR) continue; - return Error.Hangup; - } - if (ready == 0 or nowMs() >= s.deadline) return Error.Timeout; - if (fds[0].revents & events != 0) return; - return Error.Hangup; + return transport.wait(s.fd, events, s.deadline) catch |err| switch (err) { + error.Timeout => Error.Timeout, + else => Error.Hangup, + }; } } @@ -1143,12 +1061,7 @@ pub const Client = struct { } } try s.wait(poll_out); - const sent = libc.send(s.fd, bytes.ptr, bytes.len, nosignal); - if (sent < 0) switch (libc.errno(sent)) { - .INTR, .AGAIN => continue, - else => return Error.Hangup, - }; - if (sent == 0) return Error.Hangup; + const sent = (transport.write(s.fd, bytes) catch return Error.Hangup) orelse continue; s.cl.wrote(@intCast(sent)); } } @@ -1174,12 +1087,8 @@ pub const Client = struct { } } try s.wait(poll_in); - const got = libc.read(s.fd, &s.stage, @min(room, s.stage.len)); + const got = (transport.read(s.fd, s.stage[0..@min(room, s.stage.len)]) catch return Error.Hangup) orelse continue; if (got == 0) return Error.Hangup; - if (got < 0) switch (libc.errno(got)) { - .INTR, .AGAIN => continue, - else => return Error.Hangup, - }; const n = s.cl.push(s.stage[0..@intCast(got)]); std.debug.assert(n == @as(usize, @intCast(got))); } @@ -1317,62 +1226,15 @@ pub const Client = struct { } fn connect(sock: Dial, deadline: i64) Error!c_int { - var addr: libc.sockaddr.storage = undefined; - const addr_len: libc.socklen_t = switch (sock) { - .unix => |path| blk: { - if (path.len + 1 > sun_path_len) return Error.BadDial; - const un: *libc.sockaddr.un = @ptrCast(&addr); - un.* = .{ .path = @splat(0) }; - @memcpy(un.path[0 .. path.len + 1], path[0 .. path.len + 1]); - break :blk @sizeOf(libc.sockaddr.un); - }, - .tcp => |ip| ipSockaddr(ip, &addr), + const address: transport.Address = switch (sock) { + .unix => |path| .{ .unix = path }, + .tcp => |ip| .{ .tcp = ip }, .quic => return Error.QuicUnavailable, }; - const fd = libc.socket(addr.family, libc.SOCK.STREAM, 0); - if (fd < 0) return Error.Dial; - setCloexec(fd); - setNonblock(fd); - errdefer _ = libc.close(fd); - if (sock == .tcp) { - const on: c_int = 1; - _ = libc.setsockopt(fd, libc.IPPROTO.TCP, libc.TCP.NODELAY, &on, @sizeOf(c_int)); - } - while (true) { - if (libc.connect(fd, @ptrCast(&addr), addr_len) == 0) break; - switch (libc._errno().*) { - @intFromEnum(libc.E.AGAIN), @intFromEnum(libc.E.INTR) => { - if (nowMs() >= deadline) return Error.Dial; - nap(2); - }, - @intFromEnum(libc.E.INPROGRESS), @intFromEnum(libc.E.ALREADY) => { - const left = deadline - nowMs(); - if (left <= 0) return Error.Dial; - var pfd: [1]libc.pollfd = .{.{ .fd = fd, .events = poll_out, .revents = 0 }}; - if (libc.poll(&pfd, 1, @intCast(@min(left, 1000))) <= 0) continue; - var err: c_int = 0; - var len: libc.socklen_t = @sizeOf(c_int); - if (libc.getsockopt(fd, libc.SOL.SOCKET, libc.SO.ERROR, @ptrCast(&err), &len) != 0) - return Error.Dial; - if (err == 0) break; - return Error.Dial; - }, - @intFromEnum(libc.E.ISCONN) => break, - else => return Error.Dial, - } - } - if (comptime darwin) { - const on: c_int = 1; - _ = libc.setsockopt(fd, libc.SOL.SOCKET, libc.SO.NOSIGPIPE, &on, @sizeOf(c_int)); - } - return fd; + return transport.connectFd(address, deadline) catch return Error.Dial; } - fn nowMs() i64 { - var ts: libc.timespec = undefined; - if (libc.clock_gettime(.MONOTONIC, &ts) != 0) return std.math.maxInt(i64); - return @as(i64, ts.sec) * std.time.ms_per_s + @divTrunc(ts.nsec, std.time.ns_per_ms); - } + const nowMs = transport.nowMs; const poll_in: i16 = @intCast(libc.POLL.IN); const poll_out: i16 = @intCast(libc.POLL.OUT); @@ -1436,8 +1298,8 @@ pub const Client = struct { const transport_bytes = if (quic_enabled) @sizeOf(?quic.Connection) else 0; try testing.expectEqual(@as(usize, msize), @as(usize, (Session{ .fd = -1, .deadline = 0, .display_path = "" }).in.len)); try testing.expect(transport_bytes <= 64); - try testing.expect(@sizeOf(Session) <= 3 * msize + 256 + transport_bytes); - try testing.expect(@sizeOf(ninep.Client) <= 256); + try testing.expect(@sizeOf(Session) <= 3 * msize + @sizeOf(ninep.Client) + 128 + transport_bytes); + try testing.expect(@sizeOf(ninep.Client) <= 512); } test "expired sessions do not send or consume buffered protocol work" { diff --git a/src/9p_quic.zig b/src/9p_quic.zig index 3f12d8bc..12912085 100644 --- a/src/9p_quic.zig +++ b/src/9p_quic.zig @@ -1,559 +1,6 @@ -const std = @import("std"); -const libc = std.c; -const ssl = @import("openssl"); - -comptime { - if (ssl.OPENSSL_VERSION_NUMBER < 0x30600000) - @compileError("9P over QUIC requires OpenSSL 3.6 or newer"); -} - -pub const alpn = "pardes-9p"; -pub const Error = error{ Tls, Socket, SocketFlags, SocketOption, Bind, Address, Closed, InvalidWrite }; - -pub const Listener = struct { - fd: c_int, - handle: *ssl.SSL, - address: std.Io.net.IpAddress, - - pub fn init(address: std.Io.net.IpAddress) Error!Listener { - ssl.ERR_clear_error(); - const ctx = ssl.SSL_CTX_new(ssl.OSSL_QUIC_server_method()) orelse return error.Tls; - defer ssl.SSL_CTX_free(ctx); - const key = ssl.EVP_PKEY_Q_keygen(null, null, "EC", @as([*:0]const u8, "prime256v1")) orelse return error.Tls; - defer ssl.EVP_PKEY_free(key); - const cert = ssl.X509_new() orelse return error.Tls; - defer ssl.X509_free(cert); - if (ssl.X509_set_version(cert, 2) != 1 or - ssl.ASN1_INTEGER_set(ssl.X509_get_serialNumber(cert), 1) != 1 or - ssl.X509_gmtime_adj(ssl.X509_getm_notBefore(cert), -60) == null or - ssl.X509_gmtime_adj(ssl.X509_getm_notAfter(cert), 365 * 24 * 60 * 60) == null or - ssl.X509_set_pubkey(cert, key) != 1) return error.Tls; - const name = ssl.X509_get_subject_name(cert) orelse return error.Tls; - if (ssl.X509_NAME_add_entry_by_txt(name, "CN", ssl.MBSTRING_ASC, "pardes", -1, -1, 0) != 1 or - ssl.X509_set_issuer_name(cert, name) != 1 or - ssl.X509_sign(cert, key, ssl.EVP_sha256()) <= 0 or - ssl.SSL_CTX_use_certificate(ctx, cert) != 1 or - ssl.SSL_CTX_use_PrivateKey(ctx, key) != 1) return error.Tls; - ssl.SSL_CTX_set_verify(ctx, ssl.SSL_VERIFY_NONE, null); - ssl.SSL_CTX_set_alpn_select_cb(ctx, selectAlpn, null); - var addr: libc.sockaddr.storage = undefined; - const addr_len = sockaddr(address, &addr); - const fd = try udp(addr.family); - errdefer _ = libc.close(fd); - if (libc.bind(fd, @ptrCast(&addr), addr_len) != 0) return error.Bind; - var actual_len: libc.socklen_t = @sizeOf(@TypeOf(addr)); - if (libc.getsockname(fd, @ptrCast(&addr), &actual_len) != 0) return error.Address; - var actual = address; - actual.setPort(switch (address) { - .ip4 => std.mem.bigToNative(u16, @as(*const libc.sockaddr.in, @ptrCast(&addr)).port), - .ip6 => std.mem.bigToNative(u16, @as(*const libc.sockaddr.in6, @ptrCast(&addr)).port), - }); - const handle = ssl.SSL_new_listener(ctx, 0) orelse return error.Tls; - errdefer ssl.SSL_free(handle); - if (ssl.SSL_set_fd(handle, fd) != 1 or ssl.SSL_set_blocking_mode(handle, 0) != 1 or - ssl.SSL_listen(handle) != 1) return error.Tls; - return .{ .fd = fd, .handle = handle, .address = actual }; - } - - pub fn accept(l: *Listener) Error!?Connection { - ssl.ERR_clear_error(); - const handle = ssl.SSL_accept_connection(l.handle, ssl.SSL_ACCEPT_CONNECTION_NO_BLOCK) orelse { - if (ssl.ERR_peek_error() != 0) return error.Tls; - return null; - }; - errdefer ssl.SSL_free(handle); - if (ssl.SSL_set_default_stream_mode(handle, ssl.SSL_DEFAULT_STREAM_MODE_NONE) != 1 or - ssl.SSL_set_blocking_mode(handle, 0) != 1) return error.Tls; - return .{ .handle = handle }; - } - - pub fn events(l: *Listener) Error!void { - ssl.ERR_clear_error(); - if (ssl.SSL_handle_events(l.handle) != 1) return error.Tls; - } - - pub fn poll(l: *const Listener) libc.pollfd { - return pollFd(l.handle, l.fd); - } - - pub fn nextDue(l: *const Listener) ?i32 { - return due(l.handle); - } - - // Accepted connections must be released before the shared UDP socket. - pub fn deinit(l: *Listener) void { - ssl.SSL_free(l.handle); - _ = libc.close(l.fd); - l.* = undefined; - } -}; - -pub const Connection = struct { - handle: *ssl.SSL, - stream: ?*ssl.SSL = null, - fd: c_int = -1, - pending_write_len: usize = 0, - - pub fn dial(address: std.Io.net.IpAddress) Error!Connection { - ssl.ERR_clear_error(); - const ctx = ssl.SSL_CTX_new(ssl.OSSL_QUIC_client_method()) orelse return error.Tls; - defer ssl.SSL_CTX_free(ctx); - ssl.SSL_CTX_set_verify(ctx, ssl.SSL_VERIFY_NONE, null); - const fd = try udp(if (address == .ip4) libc.AF.INET else libc.AF.INET6); - errdefer _ = libc.close(fd); - const handle = ssl.SSL_new(ctx) orelse return error.Tls; - errdefer ssl.SSL_free(handle); - if (ssl.SSL_set_fd(handle, fd) != 1 or ssl.SSL_set_blocking_mode(handle, 0) != 1 or - ssl.SSL_set_default_stream_mode(handle, ssl.SSL_DEFAULT_STREAM_MODE_NONE) != 1) return error.Tls; - const protocols = [_]u8{alpn.len} ++ alpn.*; - if (ssl.SSL_set_alpn_protos(handle, &protocols, protocols.len) != 0) return error.Tls; - const peer = ssl.BIO_ADDR_new() orelse return error.Tls; - defer ssl.BIO_ADDR_free(peer); - const made = switch (address) { - .ip4 => |ip| ssl.BIO_ADDR_rawmake(peer, libc.AF.INET, &ip.bytes, ip.bytes.len, std.mem.nativeToBig(u16, ip.port)), - .ip6 => |ip| ssl.BIO_ADDR_rawmake(peer, libc.AF.INET6, &ip.bytes, ip.bytes.len, std.mem.nativeToBig(u16, ip.port)), - }; - if (made != 1 or ssl.SSL_set1_initial_peer_addr(handle, peer) != 1) return error.Tls; - return .{ .handle = handle, .fd = fd }; - } - - pub fn handshake(c: *Connection) Error!bool { - ssl.ERR_clear_error(); - var close_info: ssl.SSL_CONN_CLOSE_INFO = undefined; - if (ssl.SSL_get_conn_close_info(c.handle, &close_info, @sizeOf(@TypeOf(close_info))) == 1) - return error.Closed; - if (ssl.SSL_is_init_finished(c.handle) == 1) return true; - const rc = if (c.fd >= 0) ssl.SSL_connect(c.handle) else ssl.SSL_accept(c.handle); - if (rc == 1) return true; - try retry(c.handle, rc); - return false; - } - - fn ready(c: *Connection) Error!bool { - if (!try c.handshake()) return false; - if (c.stream != null) return true; - ssl.ERR_clear_error(); - const stream = if (c.fd >= 0) - ssl.SSL_new_stream(c.handle, ssl.SSL_STREAM_FLAG_NO_BLOCK) - else - ssl.SSL_accept_stream(c.handle, ssl.SSL_ACCEPT_STREAM_NO_BLOCK); - if (stream == null) { - if (ssl.ERR_peek_error() != 0) return error.Tls; - return false; - } - errdefer ssl.SSL_free(stream); - if (ssl.SSL_set_blocking_mode(stream, 0) != 1 or - ssl.SSL_get_stream_id(stream) != 0 or - ssl.SSL_set_incoming_stream_policy(c.handle, ssl.SSL_INCOMING_STREAM_POLICY_REJECT, 0) != 1) - return error.Tls; - _ = ssl.SSL_set_mode(stream, ssl.SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); - c.stream = stream; - return true; - } - - pub fn read(c: *Connection, bytes: []u8) Error!?usize { - if (!try c.ready()) return null; - ssl.ERR_clear_error(); - var len: usize = 0; - const rc = ssl.SSL_read_ex(c.stream, bytes.ptr, bytes.len, &len); - if (rc == 1) return len; - if (ssl.SSL_get_error(c.stream, rc) == ssl.SSL_ERROR_ZERO_RETURN) return 0; - try retry(c.stream.?, rc); - return null; - } - - pub fn pending(c: *const Connection) bool { - var close_info: ssl.SSL_CONN_CLOSE_INFO = undefined; - if (ssl.SSL_get_conn_close_info(c.handle, &close_info, @sizeOf(@TypeOf(close_info))) == 1) return true; - if (c.stream) |stream| { - var item: ssl.SSL_POLL_ITEM = .{ .desc = ssl.SSL_as_poll_descriptor(stream), .events = ssl.SSL_POLL_EVENT_RE, .revents = 0 }; - const timeout: ssl.struct_timeval = .{ .tv_sec = 0, .tv_usec = 0 }; - if (ssl.SSL_poll(&item, 1, @sizeOf(@TypeOf(item)), &timeout, ssl.SSL_POLL_FLAG_NO_HANDLE_EVENTS, null) != 1) return true; - return item.revents != 0; - } - return ssl.SSL_get_accept_stream_queue_len(c.handle) != 0; - } - - pub fn write(c: *Connection, bytes: []const u8) Error!usize { - if (!try c.ready()) return 0; - if (bytes.len < c.pending_write_len) return error.InvalidWrite; - const requested = if (c.pending_write_len != 0) c.pending_write_len else bytes.len; - if (requested == 0) return 0; - ssl.ERR_clear_error(); - var len: usize = 0; - const rc = ssl.SSL_write_ex(c.stream, bytes.ptr, requested, &len); - if (rc == 1) { - c.pending_write_len = 0; - return len; - } - try retry(c.stream.?, rc); - c.pending_write_len = requested; - return 0; - } - - pub fn conclude(c: *Connection) Error!void { - if (c.pending_write_len != 0) return error.InvalidWrite; - if (!try c.ready()) return error.Closed; - ssl.ERR_clear_error(); - if (ssl.SSL_stream_conclude(c.stream, 0) != 1) return error.Tls; - } - - pub fn events(c: *Connection) Error!void { - if (c.fd < 0) return; - ssl.ERR_clear_error(); - if (ssl.SSL_handle_events(c.handle) != 1) return error.Tls; - } - - pub fn poll(c: *const Connection) ?libc.pollfd { - return if (c.fd >= 0) pollFd(c.handle, c.fd) else null; - } - - pub fn nextDue(c: *const Connection) ?i32 { - return if (c.fd >= 0) due(c.handle) else null; - } - - pub fn deinit(c: *Connection) void { - ssl.ERR_clear_error(); - _ = ssl.SSL_shutdown_ex(c.handle, ssl.SSL_SHUTDOWN_FLAG_RAPID | ssl.SSL_SHUTDOWN_FLAG_NO_STREAM_FLUSH | ssl.SSL_SHUTDOWN_FLAG_NO_BLOCK, null, 0); - ssl.SSL_free(c.stream); - ssl.SSL_free(c.handle); - if (c.fd >= 0) _ = libc.close(c.fd); - c.* = undefined; - } -}; - -fn udp(family: u16) Error!c_int { - const fd = libc.socket(family, libc.SOCK.DGRAM, 0); - if (fd < 0) return error.Socket; - errdefer _ = libc.close(fd); - const flags = libc.fcntl(fd, libc.F.GETFL, @as(c_int, 0)); - if (flags < 0) return error.SocketFlags; - var options: libc.O = @bitCast(@as(u32, @bitCast(flags))); - options.NONBLOCK = true; - if (libc.fcntl(fd, libc.F.SETFL, @as(c_int, @bitCast(@as(u32, @bitCast(options))))) != 0 or - libc.fcntl(fd, libc.F.SETFD, @as(c_int, libc.FD_CLOEXEC)) != 0) return error.SocketFlags; - if (family == libc.AF.INET6) { - const enabled: c_int = 1; - const v6only = if (@import("builtin").os.tag.isDarwin()) 27 else libc.IPV6.V6ONLY; - if (libc.setsockopt(fd, libc.IPPROTO.IPV6, v6only, &enabled, @sizeOf(c_int)) != 0) return error.SocketOption; - } - return fd; -} - -fn sockaddr(address: std.Io.net.IpAddress, out: *libc.sockaddr.storage) libc.socklen_t { - switch (address) { - .ip4 => |ip| { - const addr: *libc.sockaddr.in = @ptrCast(out); - addr.* = .{ .port = std.mem.nativeToBig(u16, ip.port), .addr = @bitCast(ip.bytes) }; - return @sizeOf(libc.sockaddr.in); - }, - .ip6 => |ip| { - const addr: *libc.sockaddr.in6 = @ptrCast(out); - addr.* = .{ .port = std.mem.nativeToBig(u16, ip.port), .addr = ip.bytes, .flowinfo = 0, .scope_id = 0 }; - return @sizeOf(libc.sockaddr.in6); - }, - } -} - -fn pollFd(handle: *ssl.SSL, fd: c_int) libc.pollfd { - var result: libc.pollfd = .{ .fd = fd, .events = 0, .revents = 0 }; - if (ssl.SSL_net_read_desired(handle) == 1) result.events |= libc.POLL.IN; - if (ssl.SSL_net_write_desired(handle) == 1) result.events |= libc.POLL.OUT; - return result; -} - -fn due(handle: *ssl.SSL) ?i32 { - var tv: ssl.struct_timeval = undefined; - var infinite: c_int = undefined; - if (ssl.SSL_get_event_timeout(handle, &tv, &infinite) != 1) return 0; - if (infinite != 0) return null; - const ms = @as(i128, tv.tv_sec) * 1000 + @divFloor(@as(i128, tv.tv_usec) + 999, 1000); - return @intCast(std.math.clamp(ms, 0, std.math.maxInt(i32))); -} - -fn retry(handle: *ssl.SSL, rc: c_int) Error!void { - switch (ssl.SSL_get_error(handle, rc)) { - ssl.SSL_ERROR_WANT_READ, ssl.SSL_ERROR_WANT_WRITE => {}, - ssl.SSL_ERROR_ZERO_RETURN => return error.Closed, - else => return error.Tls, - } -} - -fn selectAlpn(_: ?*ssl.SSL, out: [*c][*c]const u8, outlen: [*c]u8, input: [*c]const u8, len: c_uint, _: ?*anyopaque) callconv(.c) c_int { - var offset: usize = 0; - while (offset < len) { - const size = input[offset]; - offset += 1; - if (size > len - offset) return ssl.SSL_TLSEXT_ERR_ALERT_FATAL; - if (std.mem.eql(u8, input[offset..][0..size], alpn)) { - out.* = input + offset; - outlen.* = size; - return ssl.SSL_TLSEXT_ERR_OK; - } - offset += size; - } - return ssl.SSL_TLSEXT_ERR_ALERT_FATAL; -} - -const TestPair = struct { - listener: *Listener, - client: Connection, - server: ?Connection = null, - - fn init(listener: *Listener) !TestPair { - var p: TestPair = .{ .listener = listener, .client = try .dial(listener.address) }; - errdefer p.deinit(); - const deadline = testNow() + 3000; - while (true) { - try listener.events(); - try p.client.events(); - if (p.server == null) p.server = try listener.accept(); - const connected = try p.client.handshake(); - if (p.server) |*server| if (connected and try server.handshake()) return p; - try p.wait(deadline); - } - } - - fn wait(p: *TestPair, deadline: i64) !void { - const remaining = deadline - testNow(); - if (remaining <= 0) return error.Deadline; - var timeout: i32 = @intCast(@min(remaining, std.math.maxInt(i32))); - if (p.listener.nextDue()) |ms| timeout = @min(timeout, ms); - if (p.client.nextDue()) |ms| timeout = @min(timeout, ms); - var fds = [_]libc.pollfd{ p.listener.poll(), p.client.poll().? }; - const rc = libc.poll(&fds, fds.len, timeout); - if (rc < 0 and libc.errno(rc) != .INTR) return error.Poll; - if (testNow() >= deadline) return error.Deadline; - try p.listener.events(); - try p.client.events(); - } - - fn transfer(p: *TestPair, from_client: bool, bytes: []const u8, fragment: usize) !void { - const writer = if (from_client) &p.client else &p.server.?; - const reader = if (from_client) &p.server.? else &p.client; - var sent: usize = 0; - var received: usize = 0; - var buffer: [8192]u8 = undefined; - const deadline = testNow() + 3000; - while (received < bytes.len) { - const written = if (sent != bytes.len) try writer.write(bytes[sent..][0..@min(fragment, bytes.len - sent)]) else 0; - sent += written; - const count = try reader.read(buffer[0..@min(fragment, buffer.len)]); - if (count) |n| { - try std.testing.expect(n > 0 and n <= bytes.len - received); - try std.testing.expectEqualSlices(u8, bytes[received..][0..n], buffer[0..n]); - received += n; - } - if (testNow() >= deadline) return error.Deadline; - if (received != bytes.len and written == 0 and count == null) { - try p.wait(deadline); - } else { - try p.listener.events(); - try p.client.events(); - } - } - try std.testing.expectEqual(bytes.len, sent); - } - - fn finish(p: *TestPair) !void { - try p.client.conclude(); - try p.server.?.conclude(); - var buffer: [16]u8 = undefined; - var a = false; - var b = false; - const deadline = testNow() + 3000; - while (!a or !b) { - if (!a) if (try p.client.read(&buffer)) |n| { - try std.testing.expectEqual(@as(usize, 0), n); - a = true; - }; - if (!b) if (try p.server.?.read(&buffer)) |n| { - try std.testing.expectEqual(@as(usize, 0), n); - b = true; - }; - if (!a or !b) try p.wait(deadline); - } - } - - fn deinit(p: *TestPair) void { - if (p.server) |*server| server.deinit(); - p.client.deinit(); - } -}; - -fn testNow() i64 { - var ts: libc.timespec = undefined; - std.debug.assert(libc.clock_gettime(.MONOTONIC, &ts) == 0); - return @as(i64, @intCast(ts.sec)) * 1000 + @divFloor(@as(i64, @intCast(ts.nsec)), 1_000_000); -} - -test "QUIC fragmented 9P frames reconnect and stream EOF over IPv4 and IPv6" { - const request = "\x13\x00\x00\x00\x64\xff\xff\x00\x20\x00\x00\x06\x00" ++ "9P2000"; - const response = "\x13\x00\x00\x00\x65\xff\xff\x00\x20\x00\x00\x06\x00" ++ "9P2000"; - const read_request = "\x17\x00\x00\x00\x74\x01\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0b\x00\x00\x00"; - const read_response = "\x16\x00\x00\x00\x75\x01\x00\x0b\x00\x00\x00" ++ "hello ninep"; - for ([_][]const u8{ "127.0.0.1", "::1" }) |host| { - var listener = try Listener.init(try .parse(host, 0)); - defer listener.deinit(); - try std.testing.expect(listener.address.getPort() != 0); - if (listener.address == .ip6) { - var enabled: c_int = 0; - var len: libc.socklen_t = @sizeOf(c_int); - const v6only = if (@import("builtin").os.tag.isDarwin()) 27 else libc.IPV6.V6ONLY; - try std.testing.expectEqual(@as(c_int, 0), libc.getsockopt(listener.fd, libc.IPPROTO.IPV6, v6only, @ptrCast(&enabled), &len)); - try std.testing.expectEqual(@as(c_int, 1), enabled); - } - for (0..3) |_| { - var pair = try TestPair.init(&listener); - defer pair.deinit(); - try std.testing.expect(pair.server.?.poll() == null); - try std.testing.expect(pair.server.?.nextDue() == null); - for ([_]usize{ 1, 2, 7, 64 }) |fragment| { - try pair.transfer(true, request, fragment); - try pair.transfer(false, response, fragment); - try pair.transfer(true, read_request, fragment); - try pair.transfer(false, read_response, fragment); - } - try pair.finish(); - } - } -} - -test "QUIC backpressure retries a moved prefix while new replies are appended" { - var listener = try Listener.init(try .parse("127.0.0.1", 0)); - defer listener.deinit(); - var pair = try TestPair.init(&listener); - defer pair.deinit(); - try pair.transfer(true, "hello", 1); - const bytes: [8192]u8 = @splat(0x5a); - var total: usize = 0; - const deadline = testNow() + 3000; - while (total < 64 * 1024 * 1024) { - const written = try pair.client.write(&bytes); - total += written; - if (written == 0) break; - try pair.listener.events(); - try pair.client.events(); - if (testNow() >= deadline) return error.Deadline; - } - try std.testing.expect(total > 0 and total < 64 * 1024 * 1024); - try std.testing.expectEqual(bytes.len, pair.client.pending_write_len); - try std.testing.expectError(error.InvalidWrite, pair.client.write(bytes[0..1])); - var buffer: [8192]u8 = undefined; - var received: usize = 0; - // A pending SSL write may already have delivered a prefix before it - // reports completion. Leave that prefix for the retry check below. - while (received < total) { - if (try pair.server.?.read(buffer[0..@min(buffer.len, total - received)])) |n| { - try std.testing.expect(n > 0); - try std.testing.expect(std.mem.allEqual(u8, buffer[0..n], 0x5a)); - received += n; - } else try pair.wait(deadline); - } - try std.testing.expectEqual(total, received); - var moved: [8192 + 7]u8 = undefined; - @memcpy(moved[0..bytes.len], &bytes); - @memset(moved[bytes.len..], 0x6b); - while (true) { - const n = try pair.client.write(&moved); - if (n != 0) { - try std.testing.expectEqual(bytes.len, n); - break; - } - try pair.wait(deadline); - } - received = 0; - while (received < bytes.len) { - if (try pair.server.?.read(&buffer)) |n| { - try std.testing.expect(n > 0); - try std.testing.expect(std.mem.allEqual(u8, buffer[0..n], 0x5a)); - received += n; - } else try pair.wait(deadline); - } - try std.testing.expectEqual(bytes.len, received); - try pair.transfer(true, moved[bytes.len..], 7); - try pair.finish(); -} - -test "QUIC owner can enforce handshake and read deadlines without busy polling" { - var listener = try Listener.init(try .parse("127.0.0.1", 0)); - defer listener.deinit(); - var client = try Connection.dial(listener.address); - defer client.deinit(); - const handshake_deadline = testNow() + 100; - var turns: usize = 0; - while (testNow() < handshake_deadline) { - try std.testing.expect(!try client.handshake()); - try client.events(); - var timeout: i32 = @intCast(@max(0, handshake_deadline - testNow())); - if (client.nextDue()) |ms| timeout = @min(timeout, ms); - var fds = [_]libc.pollfd{client.poll().?}; - const rc = libc.poll(&fds, fds.len, timeout); - if (rc < 0 and libc.errno(rc) != .INTR) return error.Poll; - turns += 1; - } - try std.testing.expect(turns < 100); - var serving = try Listener.init(try .parse("127.0.0.1", 0)); - defer serving.deinit(); - var pair = try TestPair.init(&serving); - defer pair.deinit(); - try pair.transfer(true, "request", 2); - var buffer: [32]u8 = undefined; - const read_deadline = testNow() + 100; - turns = 0; - while (true) { - try std.testing.expect(try pair.client.read(&buffer) == null); - pair.wait(read_deadline) catch |err| { - try std.testing.expectEqual(error.Deadline, err); - break; - }; - turns += 1; - } - try std.testing.expect(turns < 100); -} - -test "QUIC bind failure leaves the existing listener usable" { - var listener = try Listener.init(try .parse("127.0.0.1", 0)); - defer listener.deinit(); - for (0..8) |_| try std.testing.expectError(error.Bind, Listener.init(listener.address)); - var pair = try TestPair.init(&listener); - defer pair.deinit(); - try pair.transfer(true, "still listening", 3); - try pair.transfer(false, "still serving", 2); - try pair.finish(); -} - -test "QUIC pending reports buffered bytes and EOF without UDP readiness" { - var listener = try Listener.init(try .parse("127.0.0.1", 0)); - defer listener.deinit(); - var pair = try TestPair.init(&listener); - defer pair.deinit(); - try std.testing.expect(!pair.server.?.pending()); - try std.testing.expectEqual(@as(usize, 3), try pair.client.write("abc")); - const deadline = testNow() + 3000; - while (!pair.server.?.pending()) try pair.wait(deadline); - var buffer: [3]u8 = undefined; - try std.testing.expectEqual(@as(?usize, 1), try pair.server.?.read(buffer[0..1])); - try std.testing.expectEqual(@as(u8, 'a'), buffer[0]); - try listener.events(); - try pair.client.events(); - try std.testing.expect(pair.server.?.pending()); - try std.testing.expectEqual(@as(?usize, 2), try pair.server.?.read(buffer[1..])); - try std.testing.expectEqualStrings("abc", &buffer); - try std.testing.expect(!pair.server.?.pending()); - try pair.client.conclude(); - while (!pair.server.?.pending()) try pair.wait(deadline); - try std.testing.expectEqual(@as(?usize, 0), try pair.server.?.read(&buffer)); -} - -test "QUIC moved listener retains its in-memory identity" { - var original = try Listener.init(try .parse("127.0.0.1", 0)); - var listener = original; - original = undefined; - defer listener.deinit(); - var pair = try TestPair.init(&listener); - defer pair.deinit(); - try pair.transfer(true, "moved listener", 2); - try pair.transfer(false, "same identity", 3); - try pair.finish(); -} +//! Pardes selects its existing ALPN; transport implementation lives in cloud9. +const transport = @import("cloud9").Quic(@import("openssl"), "pardes-9p"); +pub const Listener = transport.Listener; +pub const Connection = transport.Connection; +pub const Error = transport.Error; +pub const alpn = transport.alpn; diff --git a/src/builtins.zig b/src/builtins.zig index 9208b9f8..a9728fd9 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -17,6 +17,7 @@ pub const capabilities: config.Runtime.Capabilities = .{ // The tty's font belongs to its emulator, and the P4 firmware's belongs to // whatever terminal is on the other end of the serial line. .tagline_font_size = pardes.platform != .tty and pardes.platform != .esp32p4, + .window_opacity = pardes.platform == .gui, }; pub const Ctx = struct { diff --git a/src/config.zig b/src/config.zig index 282e63da..865e2474 100644 --- a/src/config.zig +++ b/src/config.zig @@ -98,6 +98,7 @@ pub const leader_path = paths: { table.set(.TaglineSize, null); } if (pardes.platform == .gui) table.set(.Pet, null); + if (builtins.capabilities.window_opacity) table.set(.WindowOpacity, null); if (builtins.capabilities.panel_transitions) { table.set(.PanelSlide, "as"); table.set(.PanelZoom, "az"); @@ -537,6 +538,8 @@ pub const Runtime = struct { focus_tint: bool = true, column_tags: bool = true, syntax_bold: bool = false, + window_opacity: u8 = 100, + window_opacity_pending: bool = true, pet: enum { off, cat, frog } = .off, // Effective values change only after a host acknowledges the request. @@ -625,6 +628,7 @@ pub const Runtime = struct { panel_transitions: bool, scene_shaders: bool, tagline_font_size: bool, + window_opacity: bool = false, }; pub const Capability = std.meta.FieldEnum(Capabilities); @@ -638,6 +642,7 @@ pub const Runtime = struct { theme, font, tagline_size, + window_opacity, transition: layout.Transition, scene: Scene, }; @@ -649,7 +654,7 @@ pub const Runtime = struct { pub fn takesArg(setting: Setting) bool { return switch (setting.action) { - .shell, .theme, .font, .tagline_size => true, + .shell, .theme, .font, .tagline_size, .window_opacity => true, else => false, }; } @@ -675,6 +680,7 @@ pub const Runtime = struct { .{ .word = "Shell", .action = .shell }, .{ .word = "Font", .action = .font, .availability = .font_picker }, .{ .word = "TaglineSize", .action = .tagline_size, .availability = .font_picker }, + .{ .word = "WindowOpacity", .action = .window_opacity, .availability = .window_opacity }, .{ .word = "PanelSlide", .action = .{ .transition = .slide }, .availability = .panel_transitions }, .{ .word = "PanelZoom", .action = .{ .transition = .zoom }, .availability = .panel_transitions }, .{ .word = "PanelDissolve", .action = .{ .transition = .dissolve }, .availability = .panel_transitions }, @@ -716,6 +722,8 @@ pub const Runtime = struct { switch (setting.action) { .font, .tagline_size => if (setting.availability != .font_picker) @compileError("native font settings must use the font-picker capability"), + .window_opacity => if (setting.availability != .window_opacity) + @compileError("window opacity must use the window-opacity capability"), .transition => if (setting.availability != .panel_transitions) @compileError("panel effects must use the panel-transition capability"), .scene => if (setting.availability != .scene_shaders) @@ -727,8 +735,8 @@ pub const Runtime = struct { for (std.enums.values(Toggle)) |field| if (actionCount(.{ .toggle = field }) != 1) @compileError("runtime toggle must occur exactly once: " ++ @tagName(field)); if (actionCount(.shell) != 1 or actionCount(.theme) != 1 or actionCount(.font) != 1 or - actionCount(.tagline_size) != 1) - @compileError("Shell, Theme, Font, and TaglineSize actions must each occur exactly once"); + actionCount(.tagline_size) != 1 or actionCount(.window_opacity) != 1) + @compileError("Shell, Theme, Font, TaglineSize, and WindowOpacity actions must each occur exactly once"); for (std.enums.values(layout.Transition)) |effect| { const expected: comptime_int = @intFromBool(effect != .off); if (actionCount(.{ .transition = effect }) != expected) @@ -764,6 +772,14 @@ pub const Runtime = struct { if (percent < tagline_percent_min or percent > tagline_percent_max) return false; state.font.tagline_percent = @intCast(percent); }, + .window_opacity => { + const text = std.mem.trim(u8, argument orelse return false, " \t\r\n"); + for (text) |byte| if (!std.ascii.isDigit(byte)) return false; + const percent = std.fmt.parseInt(u8, text, 10) catch return false; + if (percent > 100) return false; + state.window_opacity = percent; + state.window_opacity_pending = true; + }, .transition => |effect| state.toggleTransition(effect), .scene => |effect| switch (effect) { inline else => |field| { @@ -819,6 +835,16 @@ pub const Runtime = struct { try out.print("{s}: {s}\n", .{ setting.word, onOff(value) }); }, .theme => try out.print("{s}: {s}\n", .{ setting.word, context.theme_name }), + .window_opacity => { + if (setting.enabled(context.capabilities)) + try out.print("{s}: {d}%{s}\n", .{ + setting.word, + state.window_opacity, + if (state.window_opacity_pending) " (pending)" else "", + }) + else + try out.print("{s}: unsupported\n", .{setting.word}); + }, .shell => { const chosen = state.shell.requested.get(); try out.print( @@ -917,7 +943,7 @@ pub const Runtime = struct { for (settings[i + 1 ..]) |later| try std.testing.expect(!std.mem.eql(u8, setting.word, later.word)); try std.testing.expectEqual(switch (setting.action) { - .shell, .theme, .font, .tagline_size => true, + .shell, .theme, .font, .tagline_size, .window_opacity => true, else => false, }, setting.takesArg()); } @@ -954,6 +980,33 @@ pub const Runtime = struct { } } + test "window opacity defaults to opaque validates atomically and retries" { + const setting = find("WindowOpacity").?; + var state: Runtime = .{}; + try std.testing.expectEqual(@as(u8, 100), state.window_opacity); + try std.testing.expect(state.window_opacity_pending); + try std.testing.expect(setting.takesArg()); + try std.testing.expectEqual(Capability.window_opacity, setting.availability.?); + + for ([_][]const u8{ "0", " 85\n", "100" }, [_]u8{ 0, 85, 100 }) |argument, expected| { + state.window_opacity_pending = false; + try std.testing.expect(apply(&state, setting, argument)); + try std.testing.expectEqual(expected, state.window_opacity); + try std.testing.expect(state.window_opacity_pending); + } + + state.window_opacity = 85; + state.window_opacity_pending = false; + for ([_]?[]const u8{ null, "", " \t\n", "101", "-1", "+85", "85%", "0.5", "85.0", "85 20", "8_5", "0x55", "999999999999999999999" }) |argument| { + try std.testing.expect(!apply(&state, setting, argument)); + try std.testing.expectEqual(@as(u8, 85), state.window_opacity); + try std.testing.expect(!state.window_opacity_pending); + } + // Reissuing the same request still asks the host to apply it again. + try std.testing.expect(apply(&state, setting, "85")); + try std.testing.expect(state.window_opacity_pending); + } + test "font request tuple rejects atomically" { var state: Runtime = .{}; try std.testing.expect(requestFont(&state, "/fonts/old.ttf", "Old", 1200)); @@ -994,6 +1047,7 @@ pub const Runtime = struct { .panel_transitions = true, .scene_shaders = true, .tagline_font_size = true, + .window_opacity = true, }, .state = &state, }; @@ -1006,6 +1060,7 @@ pub const Runtime = struct { const argument: ?[]const u8 = switch (setting.action) { .shell => "fish", .tagline_size => "73", + .window_opacity => "85", else => null, }; try std.testing.expect(apply(&state, setting, argument)); @@ -1025,6 +1080,7 @@ pub const Runtime = struct { }, .shell => "Shell requested (new panes): fish\n", .tagline_size => "TaglineSize: 73%\n", + .window_opacity => "WindowOpacity: 85% (pending)\n", .transition => |transition| switch (transition) { .off => unreachable, .slide => "Panel transition: PanelSlide\n", @@ -1059,10 +1115,12 @@ pub const Runtime = struct { state.font.tagline_percent = 82; var out: std.Io.Writer = .fixed(&storage); + state.window_opacity_pending = false; try writeReport(&out, context); const report = storage[0..out.end]; for ([_][]const u8{ "Theme: acme\n", + "WindowOpacity: 85%\n", "Font requested size: 18.50\n", "Font requested: Wanted Mono\n", "Font requested path: /fonts/wanted.ttf\n", @@ -1095,6 +1153,9 @@ pub const Runtime = struct { out = .fixed(&storage); try writeReport(&out, defaults_context); const defaults_report = storage[0..out.end]; + try std.testing.expect(std.mem.indexOf(u8, defaults_report, "WindowOpacity: unsupported\n") != null); + try std.testing.expect(!find("WindowOpacity").?.enabled(defaults_context.capabilities)); + try std.testing.expect(find("WindowOpacity").?.enabled(context.capabilities)); try std.testing.expect(std.mem.indexOf(u8, defaults_report, "Shell requested (new panes): /bin/sh (default)\n") != null); try std.testing.expect(std.mem.indexOf(u8, defaults_report, "Shell effective (last spawn): (none)\n") != null); try std.testing.expect(std.mem.indexOf(u8, defaults_report, "Shell pending: on\n") != null); diff --git a/src/esp32p4_9p.zig b/src/esp32p4_9p.zig index 4aa559a1..f6f66c15 100644 --- a/src/esp32p4_9p.zig +++ b/src/esp32p4_9p.zig @@ -84,7 +84,7 @@ export fn zig_main() noreturn { const queued = srv.output(); if (queued.len != 0) srv.wrote(uart.writeSome(queued)); - if (srv.dead) { + if (srv.protocol.dead) { srv.hangup(); while (srv.next()) |req| { const a = fsys.handle(req); diff --git a/src/gui/gui.zig b/src/gui/gui.zig index 0b2bdd18..082c8159 100644 --- a/src/gui/gui.zig +++ b/src/gui/gui.zig @@ -433,7 +433,16 @@ const CellInstance = extern struct { cell_coord: u32 = 0, }; -const OverlayVertex = extern struct { x: f32, y: f32, r: f32, g: f32, b: f32, a: f32 }; +const OverlayVertex = extern struct { + x: f32, + y: f32, + r: f32, + g: f32, + b: f32, + a: f32, + // CPU draw grouping metadata; not a shader attribute. + ink: u32 = 0, +}; const OverlayColor = struct { r: f32, g: f32, b: f32, a: f32 }; const ImageInstance = extern struct { @@ -462,6 +471,7 @@ const ImageInstance = extern struct { const initial_image_capacity: u32 = pardes.MAX_PANES; const old_layer_bit: u32 = 0x8000_0000; const clear_bg_bit: u32 = 0x4000_0000; +const opaque_bg_bit: u32 = 0x2000_0000; const SavedImagePlace = struct { key: pardes.ImageCacheKey, @@ -1799,6 +1809,8 @@ const Gui = struct { soft_present: bool = false, transparent: bool = false, + surface_opacity: bool = false, + applied_window_opacity: u8 = 100, soft_renderer: ?*c.SDL_Renderer = null, soft_texture: ?*c.SDL_Texture = null, soft_tex_w: u32 = 0, @@ -2091,11 +2103,16 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u } var win_flags: c.SDL_WindowFlags = c.SDL_WINDOW_RESIZABLE; if (!test_mode) win_flags |= c.SDL_WINDOW_HIGH_PIXEL_DENSITY; - if (config.gui_transparent) win_flags |= c.SDL_WINDOW_TRANSPARENT; + const native_wayland = !test_mode and std.mem.eql(u8, std.mem.span(c.SDL_GetCurrentVideoDriver()), "wayland"); + // Background-only opacity needs per-pixel alpha; whole-window compositor + // opacity would dim the text as well. SDL requires this flag at creation. + const surface_opacity = config.gui_transparent or native_wayland; + if (surface_opacity) win_flags |= c.SDL_WINDOW_TRANSPARENT; const window = c.SDL_CreateWindow("pardes", 1120, 720, win_flags) orelse { log.err("SDL_CreateWindow: {s}", .{c.SDL_GetError()}); return error.SdlInit; }; + if (native_wayland) log.info("Wayland opacity: background-only transparent surface", .{}); if (c.SDL_CreateCursor(&p9_arrow_set, &p9_arrow_mask, 16, 16, 1, 1)) |cur| { _ = c.SDL_SetCursor(cur); } else log.err("SDL_CreateCursor: {s}", .{c.SDL_GetError()}); @@ -2109,8 +2126,8 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u !(std.mem.eql(u8, raw, "0") or raw.len == 0) else false; - if (config.gui_transparent or force_soft or !c.SDL_ClaimWindowForGPUDevice(device, window)) { - if (config.gui_transparent) + if (surface_opacity or force_soft or !c.SDL_ClaimWindowForGPUDevice(device, window)) { + if (surface_opacity) log.info("transparent window: the GPU API will not claim one, presenting through SDL_Renderer", .{}) else if (force_soft) log.info("PARDES_SOFT_PRESENT set: presenting in software", .{}) @@ -2254,6 +2271,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u .capture_dir = capture_dir orelse "", .soft_present = soft_present, .transparent = config.gui_transparent, + .surface_opacity = surface_opacity, .soft_renderer = soft_renderer, }; setGuiFontName(&g, "Adwaita Mono"); @@ -3688,7 +3706,14 @@ fn pollFrame(ctx: ?*anyopaque) void { s.reconcilePtys(); const core = s.core; if (s.fs) |f| if (f.tick(core).pending) s.queue.push(.fs_ready); - const g = s.gui orelse return; + const g = s.gui orelse { + if (core.settings.window_opacity_pending) { + var unchanged_opacity: u8 = 100; + finishWindowOpacity(core, &unchanged_opacity, if (core.settings.window_opacity == 100) null else "requires a native SDL window"); + } + return; + }; + syncWindowOpacity(g, core); syncTaglineFont(g, core); if (core.takeFontRequest()) |path| blk: { const bytes = filesystem.readFile(s.gpa, path) catch { @@ -3717,6 +3742,56 @@ fn pollFrame(ctx: ?*anyopaque) void { stepScroll(g, core, s.gpa); } +fn finishWindowOpacity(core: *pardes.Pardes, applied: *u8, failure: ?[]const u8) void { + core.settings.window_opacity_pending = false; + if (failure) |reason| { + // Do not claim a value the compositor refused; an explicit retry of + // the same command will arm a new request. + core.settings.window_opacity = applied.*; + var buf: [256]u8 = undefined; + core.setMessage(core.active, std.fmt.bufPrint(&buf, "WindowOpacity: {s}", .{reason[0..@min(reason.len, 240)]}) catch "WindowOpacity: compositor rejected the request"); + } else { + applied.* = core.settings.window_opacity; + } +} + +fn syncWindowOpacity(g: *Gui, core: *pardes.Pardes) void { + if (!core.settings.window_opacity_pending) return; + // Offscreen captures also support per-pixel alpha. Never approximate this + // with SDL_SetWindowOpacity: that would make foreground text translucent. + finishWindowOpacity(core, &g.applied_window_opacity, if (g.surface_opacity or g.capture or core.settings.window_opacity == 100) + null + else + "background transparency requires a native Wayland or alpha-capable SDL window"); +} + +test "background opacity acknowledges success and rolls back unsupported surfaces" { + const core = try pardes.Pardes.init(std.testing.allocator, .{ .tty_only = true }); + defer core.deinit(); + var applied: u8 = 100; + try std.testing.expect(core.executeBuiltinLine(0, "WindowOpacity 70")); + try std.testing.expect(core.settings.window_opacity_pending); + finishWindowOpacity(core, &applied, null); + try std.testing.expectEqual(@as(u8, 70), applied); + try std.testing.expect(!core.settings.window_opacity_pending); + + try std.testing.expect(core.executeBuiltinLine(0, "WindowOpacity 40")); + finishWindowOpacity(core, &applied, "background transparency requires an alpha-capable SDL window"); + try std.testing.expectEqual(@as(u8, 70), applied); + try std.testing.expectEqual(applied, core.settings.window_opacity); + try std.testing.expect(!core.settings.window_opacity_pending); + const pane = core.panes[0].?; + try std.testing.expectEqualStrings("WindowOpacity: background transparency requires an alpha-capable SDL window", pane.msg[0..pane.msg_len]); + + try std.testing.expect(core.executeBuiltinLine(0, "WindowOpacity 40")); + try std.testing.expect(core.settings.window_opacity_pending); + finishWindowOpacity(core, &applied, null); + try std.testing.expectEqual(@as(u8, 40), applied); + try std.testing.expect(core.executeBuiltinLine(0, "WindowOpacity 100")); + finishWindowOpacity(core, &applied, null); + try std.testing.expectEqual(@as(u8, 100), applied); +} + fn present(ctx: ?*anyopaque, surface: *const pardes.Surface) void { const s = shellOf(ctx); const core = s.core; @@ -4746,6 +4821,7 @@ fn renderFrame( const cells: u32 = @as(u32, surface.cols) * surface.rows; const page = ground(theme_bg, g.transparent); + const background_opacity = backgroundOpacity(g.applied_window_opacity); const tagline_base = pardes.Cell{ .style = .{ .bg = .{ .rgb = tagline_rgb }, .font_role = .tagline }, .default = false, @@ -4753,10 +4829,10 @@ fn renderFrame( var color_target = std.mem.zeroes(c.SDL_GPUColorTargetInfo); color_target.texture = scene; color_target.clear_color = if (page.clear) .{ .r = 0, .g = 0, .b = 0, .a = 0 } else .{ - .r = @as(f32, @floatFromInt(page.rgb[0])) / 255.0, - .g = @as(f32, @floatFromInt(page.rgb[1])) / 255.0, - .b = @as(f32, @floatFromInt(page.rgb[2])) / 255.0, - .a = 1.0, + .r = @as(f32, @floatFromInt(page.rgb[0])) / 255.0 * background_opacity, + .g = @as(f32, @floatFromInt(page.rgb[1])) / 255.0 * background_opacity, + .b = @as(f32, @floatFromInt(page.rgb[2])) / 255.0 * background_opacity, + .a = background_opacity, }; color_target.load_op = c.SDL_GPU_LOADOP_CLEAR; color_target.store_op = c.SDL_GPU_STOREOP_STORE; @@ -4919,6 +4995,7 @@ fn renderFrame( } const rp = c.SDL_BeginGPURenderPass(cmd, &color_target, 1, null); + pushLayerOpacity(cmd, rp, background_opacity); const whole = c.SDL_Rect{ .x = 0, .y = 0, .w = @intCast(sw), .h = @intCast(sh) }; for (paint_plan.batches[0..paint_plan.len], 0..) |batch, batch_index| { const has_shifted = batch_index == 0 and shifted != 0; @@ -4955,7 +5032,7 @@ fn renderFrame( if (batch.image_count != 0) drawNativeImagesGpu(g, rp, surface, &paint_plan, batch_index); } - drawOverlayGpu(g, rp, overlay_count); + drawOverlayGpu(g, cmd, rp, overlay_count); c.SDL_EndGPURenderPass(rp); var rendered_scene: crt.Frame = .{}; @@ -5086,7 +5163,9 @@ fn resolveCell(g: *Gui, cell: *const pardes.Cell, role: pardes.FontRole, is_curs if (st.reverse) reverse = !reverse; if (st.invisible) { fg = bg; - blank = clear_bg; + // With a translucent background, painting bg-colored glyphs at + // full opacity would expose text that the terminal concealed. + blank = true; } if (st.dim) for (&fg) |*ch| { ch.* = @intCast(@as(u16, ch.*) * 6 / 10); @@ -5112,6 +5191,18 @@ fn cellFontRole(cell: *const pardes.Cell) pardes.FontRole { return if (cell.default) .body else cell.style.font_role; } +test "concealed terminal text emits no ink on opaque or transparent ground" { + var g: Gui = undefined; + g.space_slot = .{ .u = 0, .v = 0 }; + var cell: pardes.Cell = .{ .default = false, .style = .{ .invisible = true, .ul = .single } }; + cell.text[0] = 'X'; + for ([_]Ground{ .opaqueRgb(bg_default), .{ .rgb = bg_default, .clear = true } }) |page| { + const resolved = resolveCell(&g, &cell, .body, false, page); + try std.testing.expectEqualDeep(g.space_slot, resolved.slot); + try std.testing.expectEqual(page.rgb, resolved.bg); + } +} + fn drawnFontRole(core: ?*const pardes.Pardes, cell: *const pardes.Cell, row: u16) pardes.FontRole { const role = cellFontRole(cell); if (role != .tagline or core != null or row < pardes.TOPBAR_H) return role; @@ -5288,6 +5379,22 @@ fn emitInstance( ); if (old_layer) instances[idx].effect |= old_layer_bit; if (resolved.clear_bg) instances[idx].effect |= clear_bg_bit; + // A block cursor is foreground UI ink, even though its color is carried + // in the reversed cell background. Keep it visible at WindowOpacity 0. + if (is_cursor) instances[idx].effect |= opaque_bg_bit; +} + +test "block cursor alone overrides transparent cell background" { + var g: Gui = undefined; + g.cell_w = 10; + g.cell_h = 20; + g.space_slot = .{ .u = 0, .v = 0 }; + const cell: pardes.Cell = .{}; + var instances: [2]CellInstance = undefined; + for ([_]bool{ false, true }, 0..) |cursor, i| { + emitInstance(&g, &instances, @intCast(i), 0, 0, fixedCellLayout(&g), 800, 480, null, .body, &cell, false, cursor, Ground.opaqueRgb(bg_default)); + try std.testing.expectEqual(cursor, instances[i].effect & opaque_bg_bit != 0); + } } fn cellCodepoint(cell: *const pardes.Cell) u32 { @@ -5319,6 +5426,8 @@ test "insert cursor overlays without replacing the character beneath it" { try std.testing.expectApproxEqAbs(@as(f32, -0.32), vertices[0].y, 0.0001); try std.testing.expectApproxEqAbs(@as(f32, -0.48), vertices[2].y, 0.0001); try std.testing.expectApproxEqAbs(@as(f32, 32.0 / 255.0), vertices[0].r, 0.0001); + for (vertices) |vertex| try std.testing.expectEqual(@as(u32, 1), vertex.ink); + try std.testing.expect(!builder.ink); } fn palColor(idx: u8) [3]u8 { @@ -5512,7 +5621,7 @@ fn ensureVbuf(g: *Gui, cells: u32) !void { fn makePipeline(device: *c.SDL_GPUDevice, color_format: c.SDL_GPUTextureFormat) !*c.SDL_GPUGraphicsPipeline { const vs = try makeShader(device, vert_spv, c.SDL_GPU_SHADERSTAGE_VERTEX, 0, 0); defer c.SDL_ReleaseGPUShader(device, vs); - const fs = try makeShader(device, frag_spv, c.SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0); + const fs = try makeShader(device, frag_spv, c.SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1); defer c.SDL_ReleaseGPUShader(device, fs); var vbuf_desc = c.SDL_GPUVertexBufferDescription{ @@ -5549,10 +5658,35 @@ fn makePipeline(device: *c.SDL_GPUDevice, color_format: c.SDL_GPUTextureFormat) return c.SDL_CreateGPUGraphicsPipeline(device, &info) orelse error.GpuCreate; } +fn backgroundLayerBlend() c.SDL_GPUColorTargetBlendState { + var blend = std.mem.zeroes(c.SDL_GPUColorTargetBlendState); + blend.src_color_blendfactor = c.SDL_GPU_BLENDFACTOR_SRC_ALPHA; + blend.dst_color_blendfactor = c.SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA; + blend.color_blend_op = c.SDL_GPU_BLENDOP_ADD; + blend.src_alpha_blendfactor = c.SDL_GPU_BLENDFACTOR_CONSTANT_COLOR; + blend.dst_alpha_blendfactor = c.SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA; + blend.alpha_blend_op = c.SDL_GPU_BLENDOP_ADD; + blend.enable_blend = true; + return blend; +} + +test "overlapping background layers keep one uniform opacity" { + const blend = backgroundLayerBlend(); + try std.testing.expectEqual(@as(c.SDL_GPUBlendFactor, c.SDL_GPU_BLENDFACTOR_CONSTANT_COLOR), blend.src_alpha_blendfactor); + try std.testing.expectEqual(@as(c.SDL_GPUBlendFactor, c.SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA), blend.dst_alpha_blendfactor); + for ([_]f32{ 0, 0.25, 0.7, 1 }) |opacity| { + var alpha = opacity; + for ([_]f32{ 1, 0.5, 0.88, 0, 1 }) |coverage| { + alpha = opacity * coverage + alpha * (1 - coverage); + try std.testing.expectApproxEqAbs(opacity, alpha, 0.00001); + } + } +} + fn makeOverlayPipeline(device: *c.SDL_GPUDevice, color_format: c.SDL_GPUTextureFormat) !*c.SDL_GPUGraphicsPipeline { const vs = try makeShader(device, overlay_vert_spv, c.SDL_GPU_SHADERSTAGE_VERTEX, 0, 0); defer c.SDL_ReleaseGPUShader(device, vs); - const fs = try makeShader(device, overlay_frag_spv, c.SDL_GPU_SHADERSTAGE_FRAGMENT, 0, 0); + const fs = try makeShader(device, overlay_frag_spv, c.SDL_GPU_SHADERSTAGE_FRAGMENT, 0, 1); defer c.SDL_ReleaseGPUShader(device, fs); var vbuf_desc = c.SDL_GPUVertexBufferDescription{ @@ -5565,14 +5699,7 @@ fn makeOverlayPipeline(device: *c.SDL_GPUDevice, color_format: c.SDL_GPUTextureF .{ .location = 0, .buffer_slot = 0, .format = c.SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2, .offset = @offsetOf(OverlayVertex, "x") }, .{ .location = 1, .buffer_slot = 0, .format = c.SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4, .offset = @offsetOf(OverlayVertex, "r") }, }; - var blend = std.mem.zeroes(c.SDL_GPUColorTargetBlendState); - blend.src_color_blendfactor = c.SDL_GPU_BLENDFACTOR_SRC_ALPHA; - blend.dst_color_blendfactor = c.SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA; - blend.color_blend_op = c.SDL_GPU_BLENDOP_ADD; - blend.src_alpha_blendfactor = c.SDL_GPU_BLENDFACTOR_ONE; - blend.dst_alpha_blendfactor = c.SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA; - blend.alpha_blend_op = c.SDL_GPU_BLENDOP_ADD; - blend.enable_blend = true; + const blend = backgroundLayerBlend(); var col_desc = c.SDL_GPUColorTargetDescription{ .format = color_format, .blend_state = blend }; var info = std.mem.zeroes(c.SDL_GPUGraphicsPipelineCreateInfo); @@ -5595,7 +5722,7 @@ fn makeOverlayPipeline(device: *c.SDL_GPUDevice, color_format: c.SDL_GPUTextureF fn makeImagePipeline(device: *c.SDL_GPUDevice, color_format: c.SDL_GPUTextureFormat) !*c.SDL_GPUGraphicsPipeline { const vs = try makeShader(device, image_vert_spv, c.SDL_GPU_SHADERSTAGE_VERTEX, 0, 0); defer c.SDL_ReleaseGPUShader(device, vs); - const fs = try makeShader(device, image_frag_spv, c.SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0); + const fs = try makeShader(device, image_frag_spv, c.SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1); defer c.SDL_ReleaseGPUShader(device, fs); var vbuf_desc = c.SDL_GPUVertexBufferDescription{ @@ -5611,14 +5738,7 @@ fn makeImagePipeline(device: *c.SDL_GPUDevice, color_format: c.SDL_GPUTextureFor .{ .location = 3, .buffer_slot = 0, .format = c.SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4, .offset = @offsetOf(ImageInstance, "present_x0") }, .{ .location = 4, .buffer_slot = 0, .format = c.SDL_GPU_VERTEXELEMENTFORMAT_UINT4, .offset = @offsetOf(ImageInstance, "effect") }, }; - var blend = std.mem.zeroes(c.SDL_GPUColorTargetBlendState); - blend.src_color_blendfactor = c.SDL_GPU_BLENDFACTOR_SRC_ALPHA; - blend.dst_color_blendfactor = c.SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA; - blend.color_blend_op = c.SDL_GPU_BLENDOP_ADD; - blend.src_alpha_blendfactor = c.SDL_GPU_BLENDFACTOR_ONE; - blend.dst_alpha_blendfactor = c.SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA; - blend.alpha_blend_op = c.SDL_GPU_BLENDOP_ADD; - blend.enable_blend = true; + const blend = backgroundLayerBlend(); var col_desc = c.SDL_GPUColorTargetDescription{ .format = color_format, .blend_state = blend }; var info = std.mem.zeroes(c.SDL_GPUGraphicsPipelineCreateInfo); info.vertex_shader = vs; @@ -5788,13 +5908,29 @@ fn softPresentFrame(g: *Gui, cmd: *c.SDL_GPUCommandBuffer, target: *c.SDL_GPUTex const texture = try ensureSoftTexture(g, sw, sh); const renderer = g.soft_renderer orelse return error.GpuCreate; if (!c.SDL_UpdateTexture(texture, null, mapped, @intCast(sw * bpp))) return error.GpuMap; + // The cell shader already produced premultiplied backgrounds with opaque + // foreground ink. Copy that alpha unchanged; never modulate the full frame. _ = c.SDL_SetRenderDrawBlendMode(renderer, c.SDL_BLENDMODE_NONE); - _ = c.SDL_SetRenderDrawColor(renderer, 0, 0, 0, if (g.transparent) 0 else 255); + _ = c.SDL_SetRenderDrawColor(renderer, 0, 0, 0, if (g.transparent or g.surface_opacity) 0 else 255); _ = c.SDL_RenderClear(renderer); _ = c.SDL_RenderTexture(renderer, texture, null, null); _ = c.SDL_RenderPresent(renderer); } +fn backgroundOpacity(percent: u8) f32 { + std.debug.assert(percent <= 100); + return @as(f32, @floatFromInt(percent)) / 100; +} + +test "background opacity retains exact endpoints and percentage precision" { + try std.testing.expectEqual(@as(f32, 0), backgroundOpacity(0)); + try std.testing.expectEqual(@as(f32, 0.7), backgroundOpacity(70)); + try std.testing.expectEqual(@as(f32, 1), backgroundOpacity(100)); + for (0..100) |percent| { + try std.testing.expect(backgroundOpacity(@intCast(percent)) < backgroundOpacity(@intCast(percent + 1))); + } +} + fn captureFrame(g: *Gui, gpa: std.mem.Allocator, cmd: *c.SDL_GPUCommandBuffer, target: *c.SDL_GPUTexture, sw: u32, sh: u32) !void { const size = c.SDL_CalculateGPUTextureFormatSize(g.swapchain_format, sw, sh, 1); if (size == 0) { @@ -5878,6 +6014,9 @@ fn addCursorBar( rgb: [3]u8, ) void { if (!bar) return; + const previous_ink = builder.ink; + builder.ink = true; + defer builder.ink = previous_ink; const x0 = layout.x_off + @as(f32, @floatFromInt(x)) * layout.w; const height = std.math.clamp(visual_height, 1.0, layout.h); const y0 = layout.y_off + @as(f32, @floatFromInt(y)) * layout.h + @@ -6263,13 +6402,30 @@ fn uploadOverlayGpu(g: *Gui, cmd: *c.SDL_GPUCommandBuffer, vertex_count: u32) bo return true; } -fn drawOverlayGpu(g: *Gui, rp: ?*c.SDL_GPURenderPass, vertex_count: u32) void { +fn pushLayerOpacity(cmd: *c.SDL_GPUCommandBuffer, pass: ?*c.SDL_GPURenderPass, opacity: f32) void { + const uniforms = [4]f32{ opacity, 0, 0, 0 }; + c.SDL_PushGPUFragmentUniformData(cmd, 0, &uniforms, @sizeOf(@TypeOf(uniforms))); + // The source alpha is geometric/image coverage, not window opacity. Its + // alpha contribution is multiplied by this constant separately, so a + // rail repainted over a gutter still has alpha p, not p + p*(1-p). + c.SDL_SetGPUBlendConstants(pass, .{ .r = opacity, .g = opacity, .b = opacity, .a = opacity }); +} + +fn drawOverlayGpu(g: *Gui, cmd: *c.SDL_GPUCommandBuffer, rp: ?*c.SDL_GPURenderPass, vertex_count: u32) void { if (vertex_count == 0) return; const pass = rp orelse return; c.SDL_BindGPUGraphicsPipeline(pass, g.overlay_pipeline); const binding = c.SDL_GPUBufferBinding{ .buffer = g.overlay_vbuf, .offset = 0 }; c.SDL_BindGPUVertexBuffers(pass, 0, &binding, 1); - c.SDL_DrawGPUPrimitives(pass, vertex_count, 1, 0, 0); + var first: u32 = 0; + while (first < vertex_count) { + const ink = g.overlay_vertices[first].ink; + var end = first + 1; + while (end < vertex_count and g.overlay_vertices[end].ink == ink) : (end += 1) {} + pushLayerOpacity(cmd, pass, if (ink != 0) 1 else backgroundOpacity(g.applied_window_opacity)); + c.SDL_DrawGPUPrimitives(pass, end - first, 1, first, 0); + first = end; + } } fn touchColor(id: u64) OverlayColor { @@ -6291,6 +6447,7 @@ const OverlayBuilder = struct { len: usize = 0, win_w: f32, win_h: f32, + ink: bool = false, fn addVertex(b: *OverlayBuilder, px: f32, py: f32, color: OverlayColor) void { if (b.len >= b.vertices.len) return; @@ -6301,6 +6458,7 @@ const OverlayBuilder = struct { .g = color.g, .b = color.b, .a = color.a, + .ink = @intFromBool(b.ink), }; b.len += 1; } @@ -6354,6 +6512,9 @@ const OverlayBuilder = struct { } fn addMiniText(b: *OverlayBuilder, text: []const u8, x: f32, y: f32, scale: f32, color: OverlayColor) void { + const previous_ink = b.ink; + b.ink = true; + defer b.ink = previous_ink; var cx = x; for (text) |ch| { const rows = miniGlyph(ch); @@ -6371,6 +6532,21 @@ const OverlayBuilder = struct { } }; +test "overlay labels preserve ink without exempting surrounding chrome" { + var vertices: [192]OverlayVertex = undefined; + var builder: OverlayBuilder = .{ .vertices = &vertices, .win_w = 100, .win_h = 100 }; + const color: OverlayColor = .{ .r = 1, .g = 1, .b = 1, .a = 1 }; + builder.addRect(0, 0, 10, 10, color); + const text_start = builder.len; + builder.addMiniText("1", 0, 0, 1, color); + const text_end = builder.len; + try std.testing.expect(text_end > text_start); + builder.addRect(20, 20, 30, 30, color); + for (vertices[0..builder.len], 0..) |vertex, i| + try std.testing.expectEqual(@as(u32, @intFromBool(i >= text_start and i < text_end)), vertex.ink); + try std.testing.expect(!builder.ink); +} + const circle_points = [_][2]f32{ .{ 1.0000000, 0.0000000 }, .{ 0.9238795, 0.3826834 }, .{ 0.7071068, 0.7071068 }, .{ 0.3826834, 0.9238795 }, .{ 0.0000000, 1.0000000 }, .{ -0.3826834, 0.9238795 }, .{ -0.7071068, 0.7071068 }, .{ -0.9238795, 0.3826834 }, |
