summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/9p.zig1676
-rw-r--r--src/9p_io.zig230
-rw-r--r--src/9p_quic.zig565
-rw-r--r--src/builtins.zig1
-rw-r--r--src/config.zig69
-rw-r--r--src/esp32p4_9p.zig2
-rw-r--r--src/gui/gui.zig242
7 files changed, 436 insertions, 2349 deletions
diff --git a/src/9p.zig b/src/9p.zig
index 61a08b84..9513fdb5 100644
--- a/src/9p.zig
+++ b/src/9p.zig
@@ -1,704 +1,45 @@
+//! Pardes filesystem adapter. The 9P protocol and transports live in cloud9.
const std = @import("std");
const assert = std.debug.assert;
-
-pub const Error = error{
- Truncated,
- Overlong,
- BadTag,
- BadValue,
- Trailing,
- NoSpace,
-};
-
-pub const Type = enum(u8) {
- tversion = 100,
- rversion = 101,
- tauth = 102,
- rauth = 103,
- tattach = 104,
- rattach = 105,
- terror = 106,
- rerror = 107,
- tflush = 108,
- rflush = 109,
- twalk = 110,
- rwalk = 111,
- topen = 112,
- ropen = 113,
- tcreate = 114,
- rcreate = 115,
- tread = 116,
- rread = 117,
- twrite = 118,
- rwrite = 119,
- tclunk = 120,
- rclunk = 121,
- tremove = 122,
- rremove = 123,
- tstat = 124,
- rstat = 125,
- twstat = 126,
- rwstat = 127,
- _,
-};
-
-pub fn isT(t: Type) bool {
- return @intFromEnum(t) % 2 == 0;
-}
-
-pub const header_len: usize = 4 + 1 + 2;
-
-pub const qid_len: usize = 1 + 4 + 8;
-
-pub const stat_fixed: usize = 2 + qid_len + 5 * 2 + 4 * 4 + 8;
-
-pub const notag: u16 = 0xFFFF;
-
-pub const nofid: u32 = 0xFFFF_FFFF;
-
-pub const max_welem: usize = 16;
-
-pub const min_msize: u32 = 4096;
-
-pub const iohdrsz: u32 = 24;
-
-pub const errmax: usize = 128;
-
-pub const qtdir: u8 = 0x80;
-pub const qtappend: u8 = 0x40;
-pub const qtexcl: u8 = 0x20;
-pub const qtmount: u8 = 0x10;
-pub const qtauth: u8 = 0x08;
-pub const qttmp: u8 = 0x04;
-pub const qtfile: u8 = 0x00;
-
-pub const dmdir: u32 = 0x8000_0000;
-pub const dmappend: u32 = 0x4000_0000;
-pub const dmexcl: u32 = 0x2000_0000;
-pub const dmmount: u32 = 0x1000_0000;
-pub const dmauth: u32 = 0x0800_0000;
-pub const dmtmp: u32 = 0x0400_0000;
-pub const dmperm: u32 = 0o777;
-
-comptime {
- assert(header_len == 7);
- assert(qid_len == 13);
- assert(stat_fixed == 49);
- for (std.enums.values(Type)) |t| {
- const even = @intFromEnum(t) % 2 == 0;
- assert(isT(t) == even);
- assert(std.mem.startsWith(u8, @tagName(t), if (even) "t" else "r"));
- }
-}
-
-pub const Qid = struct {
- type: u8,
- version: u32,
- path: u64,
-
- pub fn encode(self: Qid, buf: []u8) Error![]u8 {
- if (buf.len < qid_len) return error.NoSpace;
- buf[0] = self.type;
- std.mem.writeInt(u32, buf[1..5], self.version, .little);
- std.mem.writeInt(u64, buf[5..13], self.path, .little);
- return buf[0..qid_len];
- }
-
- pub fn decode(bytes: []const u8) Error!Qid {
- if (bytes.len < qid_len) return error.Truncated;
- return .{
- .type = bytes[0],
- .version = std.mem.readInt(u32, bytes[1..5], .little),
- .path = std.mem.readInt(u64, bytes[5..13], .little),
- };
- }
-};
-
-pub const Stat = struct {
- type: u16,
- dev: u32,
- qid: Qid,
- mode: u32,
- atime: u32,
- mtime: u32,
- length: u64,
- name: []const u8,
- uid: []const u8,
- gid: []const u8,
- muid: []const u8,
-
- pub fn size(self: Stat) Error!u16 {
- const n =
- 2 + // type
- 4 + // dev
- qid_len + // qid: type[1] version[4] path[8]
- 4 + // mode
- 4 + // atime
- 4 + // mtime
- 8 + // length
- 2 + self.name.len +
- 2 + self.uid.len +
- 2 + self.gid.len +
- 2 + self.muid.len;
- assert(n >= stat_fixed - 2);
- if (n > std.math.maxInt(u16)) return error.Overlong;
- return @intCast(n);
- }
-
- pub fn encode(self: Stat, buf: []u8) Error![]u8 {
- const n = try self.size();
- const total = @as(usize, n) + 2;
- if (buf.len < total) return error.NoSpace;
- var w: Writer = .init(buf[0..total]);
- try w.putU16(n);
- try w.putU16(self.type);
- try w.putU32(self.dev);
- try w.putQid(self.qid);
- try w.putU32(self.mode);
- try w.putU32(self.atime);
- try w.putU32(self.mtime);
- try w.putU64(self.length);
- try w.putString(self.name);
- try w.putString(self.uid);
- try w.putString(self.gid);
- try w.putString(self.muid);
- assert(w.n == total);
- return buf[0..total];
- }
-
- pub fn decode(bytes: []const u8) Error!Stat {
- var r: Reader = .init(bytes);
- const n = try r.getU16();
- const body = bytes.len - 2;
- if (n > body) return error.Truncated;
- if (n < body) return error.Trailing;
- const self: Stat = .{
- .type = try r.getU16(),
- .dev = try r.getU32(),
- .qid = try r.getQid(),
- .mode = try r.getU32(),
- .atime = try r.getU32(),
- .mtime = try r.getU32(),
- .length = try r.getU64(),
- .name = try r.getString(),
- .uid = try r.getString(),
- .gid = try r.getString(),
- .muid = try r.getString(),
- };
- try r.end();
- return self;
- }
-};
-
-pub const Msg = union(enum) {
- tversion: struct { msize: u32, version: []const u8 },
- rversion: struct { msize: u32, version: []const u8 },
-
- tauth: struct { afid: u32, uname: []const u8, aname: []const u8 },
- rauth: struct { aqid: Qid },
-
- tattach: struct { fid: u32, afid: u32, uname: []const u8, aname: []const u8 },
- rattach: struct { qid: Qid },
-
- rerror: struct { ename: []const u8 },
-
- tflush: struct { oldtag: u16 },
- rflush: void,
-
- twalk: struct {
- fid: u32,
- newfid: u32,
- nwname: u16,
- wname: [max_welem][]const u8 = @splat(""),
- },
- rwalk: struct {
- nwqid: u16,
- wqid: [max_welem]Qid = @splat(.{ .type = 0, .version = 0, .path = 0 }),
- },
-
- topen: struct { fid: u32, mode: u8 },
- ropen: struct { qid: Qid, iounit: u32 },
-
- tcreate: struct { fid: u32, name: []const u8, perm: u32, mode: u8 },
- rcreate: struct { qid: Qid, iounit: u32 },
-
- tread: struct { fid: u32, offset: u64, count: u32 },
- rread: struct { data: []const u8 },
-
- twrite: struct { fid: u32, offset: u64, data: []const u8 },
- rwrite: struct { count: u32 },
-
- tclunk: struct { fid: u32 },
- rclunk: void,
- tremove: struct { fid: u32 },
- rremove: void,
-
- tstat: struct { fid: u32 },
- rstat: struct { stat: Stat },
- twstat: struct { fid: u32, stat: Stat },
- rwstat: void,
-
- pub fn msgType(msg: Msg) Type {
- return switch (msg) {
- inline else => |_, t| @field(Type, @tagName(t)),
- };
- }
-};
-
-pub const Decoded = struct {
- tag: u16,
- msg: Msg,
-};
-
-pub fn frameLen(prefix: []const u8) ?u32 {
- if (prefix.len < 4) return null;
- return std.mem.readInt(u32, prefix[0..4], .little);
-}
-
-fn totalLen(msg: Msg) Error!usize {
- const body: usize = switch (msg) {
- .tversion => |m| 4 + try stringLen(m.version),
- .rversion => |m| 4 + try stringLen(m.version),
- .tauth => |m| 4 + try stringLen(m.uname) + try stringLen(m.aname),
- .rauth => qid_len,
- .tattach => |m| 4 + 4 + try stringLen(m.uname) + try stringLen(m.aname),
- .rattach => qid_len,
- .rerror => |m| try stringLen(m.ename),
- .tflush => 2,
- .rflush => 0,
- .twalk => |m| blk: {
- assert(m.nwname <= max_welem);
- var n: usize = 4 + 4 + 2;
- for (m.wname[0..m.nwname]) |name| n += try stringLen(name);
- break :blk n;
- },
- .rwalk => |m| blk: {
- assert(m.nwqid <= max_welem);
- break :blk 2 + @as(usize, m.nwqid) * qid_len;
- },
- .topen => 4 + 1,
- .ropen => qid_len + 4,
- .tcreate => |m| 4 + try stringLen(m.name) + 4 + 1,
- .rcreate => qid_len + 4,
- .tread => 4 + 8 + 4,
- .rread => |m| try dataLen(m.data),
- .twrite => |m| 4 + 8 + try dataLen(m.data),
- .rwrite => 4,
- .tclunk => 4,
- .rclunk => 0,
- .tremove => 4,
- .rremove => 0,
- .tstat => 4,
- .rstat => |m| 2 + 2 + @as(usize, try m.stat.size()),
- .twstat => |m| 4 + 2 + 2 + @as(usize, try m.stat.size()),
- .rwstat => 0,
- };
- const total = header_len + body;
- if (total > std.math.maxInt(u32)) return error.Overlong;
- return total;
-}
-
-fn stringLen(s: []const u8) Error!usize {
- if (s.len > std.math.maxInt(u16)) return error.Overlong;
- return 2 + s.len;
-}
-
-fn dataLen(d: []const u8) Error!usize {
- if (d.len > std.math.maxInt(u32)) return error.Overlong;
- return 4 + d.len;
-}
-
-pub fn encode(msg: Msg, tag: u16, buf: []u8) Error![]u8 {
- const total = try totalLen(msg);
- if (total > buf.len) return error.NoSpace;
-
- var w: Writer = .init(buf[0..total]);
- try w.putU32(@intCast(total));
- try w.putByte(@intFromEnum(msg.msgType()));
- try w.putU16(tag);
-
- switch (msg) {
- .tversion => |m| {
- try w.putU32(m.msize);
- try w.putString(m.version);
- },
- .rversion => |m| {
- try w.putU32(m.msize);
- try w.putString(m.version);
- },
- .tauth => |m| {
- try w.putU32(m.afid);
- try w.putString(m.uname);
- try w.putString(m.aname);
- },
- .rauth => |m| try w.putQid(m.aqid),
- .tattach => |m| {
- try w.putU32(m.fid);
- try w.putU32(m.afid);
- try w.putString(m.uname);
- try w.putString(m.aname);
- },
- .rattach => |m| try w.putQid(m.qid),
- .rerror => |m| try w.putString(m.ename),
- .tflush => |m| try w.putU16(m.oldtag),
- .rflush => {},
- .twalk => |m| {
- try w.putU32(m.fid);
- try w.putU32(m.newfid);
- try w.putU16(m.nwname);
- for (m.wname[0..m.nwname]) |name| try w.putString(name);
- },
- .rwalk => |m| {
- try w.putU16(m.nwqid);
- for (m.wqid[0..m.nwqid]) |qid| try w.putQid(qid);
- },
- .topen => |m| {
- try w.putU32(m.fid);
- try w.putByte(m.mode);
- },
- .ropen => |m| {
- try w.putQid(m.qid);
- try w.putU32(m.iounit);
- },
- .tcreate => |m| {
- try w.putU32(m.fid);
- try w.putString(m.name);
- try w.putU32(m.perm);
- try w.putByte(m.mode);
- },
- .rcreate => |m| {
- try w.putQid(m.qid);
- try w.putU32(m.iounit);
- },
- .tread => |m| {
- try w.putU32(m.fid);
- try w.putU64(m.offset);
- try w.putU32(m.count);
- },
- .rread => |m| {
- try w.putU32(@intCast(m.data.len));
- try w.putBytes(m.data);
- },
- .twrite => |m| {
- try w.putU32(m.fid);
- try w.putU64(m.offset);
- try w.putU32(@intCast(m.data.len));
- try w.putBytes(m.data);
- },
- .rwrite => |m| try w.putU32(m.count),
- .tclunk => |m| try w.putU32(m.fid),
- .rclunk => {},
- .tremove => |m| try w.putU32(m.fid),
- .rremove => {},
- .tstat => |m| try w.putU32(m.fid),
- .rstat => |m| {
- try w.putU16(try m.stat.size() + 2);
- try w.putStat(m.stat);
- },
- .twstat => |m| {
- try w.putU32(m.fid);
- try w.putU16(try m.stat.size() + 2);
- try w.putStat(m.stat);
- },
- .rwstat => {},
- }
-
- assert(w.n == total);
- return buf[0..total];
-}
-
-pub fn decode(bytes: []const u8) Error!Decoded {
- if (bytes.len < header_len) return error.Truncated;
- const size = std.mem.readInt(u32, bytes[0..4], .little);
- if (size < header_len) return error.BadValue;
- if (size > bytes.len) return error.Truncated;
- if (size < bytes.len) return error.Trailing;
-
- const t: Type = @enumFromInt(bytes[4]);
- const tag = std.mem.readInt(u16, bytes[5..7], .little);
-
- var r: Reader = .init(bytes[header_len..size]);
-
- const msg: Msg = switch (t) {
- .tversion => .{ .tversion = .{ .msize = try r.getU32(), .version = try r.getString() } },
- .rversion => .{ .rversion = .{ .msize = try r.getU32(), .version = try r.getString() } },
- .tauth => .{ .tauth = .{
- .afid = try r.getU32(),
- .uname = try r.getString(),
- .aname = try r.getString(),
- } },
- .rauth => .{ .rauth = .{ .aqid = try r.getQid() } },
- .tattach => .{ .tattach = .{
- .fid = try r.getU32(),
- .afid = try r.getU32(),
- .uname = try r.getString(),
- .aname = try r.getString(),
- } },
- .rattach => .{ .rattach = .{ .qid = try r.getQid() } },
- .rerror => .{ .rerror = .{ .ename = try r.getString() } },
- .tflush => .{ .tflush = .{ .oldtag = try r.getU16() } },
- .rflush => .rflush,
- .twalk => blk: {
- var m: Msg = .{ .twalk = .{
- .fid = try r.getU32(),
- .newfid = try r.getU32(),
- .nwname = try r.getU16(),
- } };
- if (m.twalk.nwname > max_welem) return error.Overlong;
- for (m.twalk.wname[0..m.twalk.nwname]) |*name| name.* = try r.getString();
- break :blk m;
- },
- .rwalk => blk: {
- var m: Msg = .{ .rwalk = .{ .nwqid = try r.getU16() } };
- if (m.rwalk.nwqid > max_welem) return error.Overlong;
- for (m.rwalk.wqid[0..m.rwalk.nwqid]) |*qid| qid.* = try r.getQid();
- break :blk m;
- },
- .topen => .{ .topen = .{ .fid = try r.getU32(), .mode = try r.getByte() } },
- .ropen => .{ .ropen = .{ .qid = try r.getQid(), .iounit = try r.getU32() } },
- .tcreate => .{ .tcreate = .{
- .fid = try r.getU32(),
- .name = try r.getString(),
- .perm = try r.getU32(),
- .mode = try r.getByte(),
- } },
- .rcreate => .{ .rcreate = .{ .qid = try r.getQid(), .iounit = try r.getU32() } },
- .tread => .{ .tread = .{
- .fid = try r.getU32(),
- .offset = try r.getU64(),
- .count = try r.getU32(),
- } },
- .rread => .{ .rread = .{ .data = try r.getData() } },
- .twrite => .{ .twrite = .{
- .fid = try r.getU32(),
- .offset = try r.getU64(),
- .data = try r.getData(),
- } },
- .rwrite => .{ .rwrite = .{ .count = try r.getU32() } },
- .tclunk => .{ .tclunk = .{ .fid = try r.getU32() } },
- .rclunk => .rclunk,
- .tremove => .{ .tremove = .{ .fid = try r.getU32() } },
- .rremove => .rremove,
- .tstat => .{ .tstat = .{ .fid = try r.getU32() } },
- .rstat => .{ .rstat = .{ .stat = try Stat.decode(try r.getBlob16()) } },
- .twstat => .{ .twstat = .{
- .fid = try r.getU32(),
- .stat = try Stat.decode(try r.getBlob16()),
- } },
- .rwstat => .rwstat,
- .terror, _ => return error.BadTag,
- };
-
- try r.end();
- return .{ .tag = tag, .msg = msg };
-}
-
-const Writer = struct {
- buf: []u8,
- n: usize = 0,
-
- fn init(buf: []u8) Writer {
- return .{ .buf = buf };
- }
-
- fn room(w: *Writer, k: usize) Error![]u8 {
- if (w.buf.len - w.n < k) return error.NoSpace;
- defer w.n += k;
- return w.buf[w.n..][0..k];
- }
-
- fn putByte(w: *Writer, v: u8) Error!void {
- (try w.room(1))[0] = v;
- }
-
- fn putU16(w: *Writer, v: u16) Error!void {
- std.mem.writeInt(u16, (try w.room(2))[0..2], v, .little);
- }
-
- fn putU32(w: *Writer, v: u32) Error!void {
- std.mem.writeInt(u32, (try w.room(4))[0..4], v, .little);
- }
-
- fn putU64(w: *Writer, v: u64) Error!void {
- std.mem.writeInt(u64, (try w.room(8))[0..8], v, .little);
- }
-
- fn putBytes(w: *Writer, v: []const u8) Error!void {
- @memcpy(try w.room(v.len), v);
- }
-
- fn putString(w: *Writer, v: []const u8) Error!void {
- assert(v.len <= std.math.maxInt(u16));
- try w.putU16(@intCast(v.len));
- try w.putBytes(v);
- }
-
- fn putQid(w: *Writer, v: Qid) Error!void {
- _ = try v.encode(try w.room(qid_len));
- }
-
- fn putStat(w: *Writer, v: Stat) Error!void {
- const total = @as(usize, try v.size()) + 2;
- _ = try v.encode(try w.room(total));
- }
-};
-
-const Reader = struct {
- bytes: []const u8,
- i: usize = 0,
-
- fn init(bytes: []const u8) Reader {
- return .{ .bytes = bytes };
- }
-
- fn take(r: *Reader, n: usize) Error![]const u8 {
- if (r.bytes.len - r.i < n) return error.Truncated;
- defer r.i += n;
- return r.bytes[r.i..][0..n];
- }
-
- fn getByte(r: *Reader) Error!u8 {
- return (try r.take(1))[0];
- }
-
- fn getU16(r: *Reader) Error!u16 {
- return std.mem.readInt(u16, (try r.take(2))[0..2], .little);
- }
-
- fn getU32(r: *Reader) Error!u32 {
- return std.mem.readInt(u32, (try r.take(4))[0..4], .little);
- }
-
- fn getU64(r: *Reader) Error!u64 {
- return std.mem.readInt(u64, (try r.take(8))[0..8], .little);
- }
-
- fn getString(r: *Reader) Error![]const u8 {
- return r.take(try r.getU16());
- }
-
- fn getData(r: *Reader) Error![]const u8 {
- return r.take(try r.getU32());
- }
-
- fn getBlob16(r: *Reader) Error![]const u8 {
- return r.take(try r.getU16());
- }
-
- fn getQid(r: *Reader) Error!Qid {
- return Qid.decode(try r.take(qid_len));
- }
-
- fn end(r: *Reader) Error!void {
- if (r.i != r.bytes.len) return error.Trailing;
- }
-};
-
const testing = std.testing;
-
-fn roundTrip(buf: []u8, tag: u16, msg: Msg) !Msg {
- const bytes = try encode(msg, tag, buf);
- try testing.expectEqual(bytes.len, frameLen(bytes).?);
- const got = try decode(bytes);
- try testing.expectEqual(tag, got.tag);
- try testing.expectEqual(msg.msgType(), got.msg.msgType());
- try expectMsgEqual(msg, got.msg);
- return got.msg;
-}
-
-fn expectStatEqual(want: Stat, have: Stat) !void {
- try testing.expectEqual(want.type, have.type);
- try testing.expectEqual(want.dev, have.dev);
- try testing.expectEqual(want.qid, have.qid);
- try testing.expectEqual(want.mode, have.mode);
- try testing.expectEqual(want.atime, have.atime);
- try testing.expectEqual(want.mtime, have.mtime);
- try testing.expectEqual(want.length, have.length);
- try testing.expectEqualStrings(want.name, have.name);
- try testing.expectEqualStrings(want.uid, have.uid);
- try testing.expectEqualStrings(want.gid, have.gid);
- try testing.expectEqualStrings(want.muid, have.muid);
-}
-
-fn expectMsgEqual(want: Msg, have: Msg) !void {
- switch (want) {
- .tversion => |w| {
- try testing.expectEqual(w.msize, have.tversion.msize);
- try testing.expectEqualStrings(w.version, have.tversion.version);
- },
- .rversion => |w| {
- try testing.expectEqual(w.msize, have.rversion.msize);
- try testing.expectEqualStrings(w.version, have.rversion.version);
- },
- .tauth => |w| {
- try testing.expectEqual(w.afid, have.tauth.afid);
- try testing.expectEqualStrings(w.uname, have.tauth.uname);
- try testing.expectEqualStrings(w.aname, have.tauth.aname);
- },
- .rauth => |w| try testing.expectEqual(w.aqid, have.rauth.aqid),
- .tattach => |w| {
- try testing.expectEqual(w.fid, have.tattach.fid);
- try testing.expectEqual(w.afid, have.tattach.afid);
- try testing.expectEqualStrings(w.uname, have.tattach.uname);
- try testing.expectEqualStrings(w.aname, have.tattach.aname);
- },
- .rattach => |w| try testing.expectEqual(w.qid, have.rattach.qid),
- .rerror => |w| try testing.expectEqualStrings(w.ename, have.rerror.ename),
- .tflush => |w| try testing.expectEqual(w.oldtag, have.tflush.oldtag),
- .rflush, .rclunk, .rremove, .rwstat => {},
- .twalk => |w| {
- try testing.expectEqual(w.fid, have.twalk.fid);
- try testing.expectEqual(w.newfid, have.twalk.newfid);
- try testing.expectEqual(w.nwname, have.twalk.nwname);
- for (w.wname[0..w.nwname], have.twalk.wname[0..w.nwname]) |a, b|
- try testing.expectEqualStrings(a, b);
- },
- .rwalk => |w| {
- try testing.expectEqual(w.nwqid, have.rwalk.nwqid);
- for (w.wqid[0..w.nwqid], have.rwalk.wqid[0..w.nwqid]) |a, b|
- try testing.expectEqual(a, b);
- },
- .topen => |w| {
- try testing.expectEqual(w.fid, have.topen.fid);
- try testing.expectEqual(w.mode, have.topen.mode);
- },
- .ropen => |w| {
- try testing.expectEqual(w.qid, have.ropen.qid);
- try testing.expectEqual(w.iounit, have.ropen.iounit);
- },
- .tcreate => |w| {
- try testing.expectEqual(w.fid, have.tcreate.fid);
- try testing.expectEqualStrings(w.name, have.tcreate.name);
- try testing.expectEqual(w.perm, have.tcreate.perm);
- try testing.expectEqual(w.mode, have.tcreate.mode);
- },
- .rcreate => |w| {
- try testing.expectEqual(w.qid, have.rcreate.qid);
- try testing.expectEqual(w.iounit, have.rcreate.iounit);
- },
- .tread => |w| {
- try testing.expectEqual(w.fid, have.tread.fid);
- try testing.expectEqual(w.offset, have.tread.offset);
- try testing.expectEqual(w.count, have.tread.count);
- },
- .rread => |w| try testing.expectEqualStrings(w.data, have.rread.data),
- .twrite => |w| {
- try testing.expectEqual(w.fid, have.twrite.fid);
- try testing.expectEqual(w.offset, have.twrite.offset);
- try testing.expectEqualStrings(w.data, have.twrite.data);
- },
- .rwrite => |w| try testing.expectEqual(w.count, have.rwrite.count),
- .tclunk => |w| try testing.expectEqual(w.fid, have.tclunk.fid),
- .tremove => |w| try testing.expectEqual(w.fid, have.tremove.fid),
- .tstat => |w| try testing.expectEqual(w.fid, have.tstat.fid),
- .rstat => |w| try expectStatEqual(w.stat, have.rstat.stat),
- .twstat => |w| {
- try testing.expectEqual(w.fid, have.twstat.fid);
- try expectStatEqual(w.stat, have.twstat.stat);
- },
- }
-}
-
+const cloud9 = @import("cloud9");
+pub const Error = cloud9.Error;
+pub const Type = cloud9.Type;
+pub const isT = cloud9.isT;
+pub const header_len = cloud9.header_len;
+pub const qid_len = cloud9.qid_len;
+pub const stat_fixed = cloud9.stat_fixed;
+pub const notag = cloud9.notag;
+pub const nofid = cloud9.nofid;
+pub const max_welem = cloud9.max_welem;
+pub const iohdrsz = cloud9.iohdrsz;
+pub const Qid = cloud9.Qid;
+pub const Stat = cloud9.Stat;
+pub const Msg = cloud9.Msg;
+pub const Decoded = cloud9.Decoded;
+pub const frameLen = cloud9.frameLen;
+pub const encode = cloud9.encode;
+pub const decode = cloud9.decode;
+pub const qtdir = cloud9.qtdir;
+pub const qtappend = cloud9.qtappend;
+pub const qtexcl = cloud9.qtexcl;
+pub const qtmount = cloud9.qtmount;
+pub const qtauth = cloud9.qtauth;
+pub const qttmp = cloud9.qttmp;
+pub const qtfile = cloud9.qtfile;
+pub const dmdir = cloud9.dmdir;
+pub const dmappend = cloud9.dmappend;
+pub const dmexcl = cloud9.dmexcl;
+pub const dmmount = cloud9.dmmount;
+pub const dmauth = cloud9.dmauth;
+pub const dmtmp = cloud9.dmtmp;
+pub const dmperm = cloud9.dmperm;
+pub const Client = cloud9.Client;
+pub const ClientError = cloud9.ClientError;
+pub const max_tags = cloud9.max_tags;
+const totalLen = cloud9.encodedLen;
+pub const errmax: usize = 128;
+pub const min_msize: u32 = 4096;
const sample_qid: Qid = .{ .type = qtdir, .version = 3, .path = 0x0102_0304_0506_0708 };
const sample_stat: Stat = .{
@@ -715,435 +56,6 @@ const sample_stat: Stat = .{
.muid = "goblin",
};
-test "9p: the type numbers and their parity are the protocol's own" {
- try testing.expectEqual(@as(u8, 100), @intFromEnum(Type.tversion));
- try testing.expectEqual(@as(u8, 106), @intFromEnum(Type.terror));
- try testing.expectEqual(@as(u8, 107), @intFromEnum(Type.rerror));
- try testing.expectEqual(@as(u8, 126), @intFromEnum(Type.twstat));
- try testing.expectEqual(@as(u8, 127), @intFromEnum(Type.rwstat));
-
- try testing.expectEqual(@as(usize, 28), std.enums.values(Type).len);
- for (std.enums.values(Type), 100..) |t, want| try testing.expectEqual(@as(u8, @intCast(want)), @intFromEnum(t));
-
- try testing.expect(isT(.tversion));
- try testing.expect(!isT(.rversion));
- try testing.expect(isT(.twstat));
- try testing.expect(!isT(.rwstat));
-
- try testing.expectEqual(@as(u16, 0xFFFF), notag);
- try testing.expectEqual(@as(u32, 0xFFFF_FFFF), nofid);
- try testing.expectEqual(@as(usize, 16), max_welem);
-}
-
-test "9p: a qid is thirteen bytes" {
- var buf: [32]u8 = undefined;
- const bytes = try sample_qid.encode(&buf);
- try testing.expectEqual(qid_len, bytes.len);
- try testing.expectEqual(@as(usize, 13), bytes.len);
- try testing.expectEqual(sample_qid, try Qid.decode(bytes));
- try testing.expectError(error.Truncated, Qid.decode(bytes[0..12]));
- try testing.expectError(error.NoSpace, sample_qid.encode(buf[0..12]));
-}
-
-test "9p: an encoded stat is size() + 2 bytes" {
- var buf: [256]u8 = undefined;
- const bytes = try sample_stat.encode(&buf);
- const n = try sample_stat.size();
- try testing.expectEqual(@as(usize, n) + 2, bytes.len);
- try testing.expectEqual(@as(u16, 69), n);
- try testing.expectEqual(stat_fixed - 2 + 22, n);
- try testing.expectEqual(n, std.mem.readInt(u16, bytes[0..2], .little));
- try expectStatEqual(sample_stat, try Stat.decode(bytes));
-
- const bare: Stat = .{
- .type = 0,
- .dev = 0,
- .qid = .{ .type = qtfile, .version = 0, .path = 0 },
- .mode = 0,
- .atime = 0,
- .mtime = 0,
- .length = 0,
- .name = "",
- .uid = "",
- .gid = "",
- .muid = "",
- };
- try testing.expectEqual(@as(u16, 47), try bare.size());
- try testing.expectEqual(@as(usize, 49), (try bare.encode(&buf)).len);
-}
-
-test "9p: every message round-trips" {
- var buf: [512]u8 = undefined;
-
- _ = try roundTrip(&buf, notag, .{ .tversion = .{ .msize = 8192, .version = "9P2000" } });
- _ = try roundTrip(&buf, notag, .{ .rversion = .{ .msize = 8192, .version = "9P2000" } });
- _ = try roundTrip(&buf, notag, .{ .rversion = .{ .msize = min_msize, .version = "unknown" } });
- _ = try roundTrip(&buf, 1, .{ .tauth = .{ .afid = 1, .uname = "goblin", .aname = "" } });
- _ = try roundTrip(&buf, 1, .{ .rauth = .{ .aqid = .{ .type = qtauth, .version = 0, .path = 9 } } });
- _ = try roundTrip(&buf, 2, .{ .tattach = .{ .fid = 0, .afid = nofid, .uname = "goblin", .aname = "" } });
- _ = try roundTrip(&buf, 2, .{ .rattach = .{ .qid = sample_qid } });
- _ = try roundTrip(&buf, 3, .{ .rerror = .{ .ename = "no such file" } });
- _ = try roundTrip(&buf, 4, .{ .tflush = .{ .oldtag = 3 } });
- _ = try roundTrip(&buf, 4, .rflush);
- _ = try roundTrip(&buf, 5, .{ .twalk = .{ .fid = 0, .newfid = 1, .nwname = 2, .wname = .{ "7", "body" } ++ @as([max_welem - 2][]const u8, @splat("")) } });
- _ = try roundTrip(&buf, 5, .{ .rwalk = .{ .nwqid = 2, .wqid = .{ sample_qid, sample_qid } ++ @as([max_welem - 2]Qid, @splat(sample_qid)) } });
- _ = try roundTrip(&buf, 6, .{ .topen = .{ .fid = 1, .mode = 0 } });
- _ = try roundTrip(&buf, 6, .{ .ropen = .{ .qid = sample_qid, .iounit = 8192 - iohdrsz } });
- _ = try roundTrip(&buf, 7, .{ .tcreate = .{ .fid = 1, .name = "new", .perm = dmdir | 0o777, .mode = 2 } });
- _ = try roundTrip(&buf, 7, .{ .rcreate = .{ .qid = sample_qid, .iounit = 0 } });
- _ = try roundTrip(&buf, 8, .{ .tread = .{ .fid = 1, .offset = 0xdead_beef_cafe, .count = 4096 } });
- _ = try roundTrip(&buf, 8, .{ .rread = .{ .data = "hello" } });
- _ = try roundTrip(&buf, 8, .{ .rread = .{ .data = "" } });
- _ = try roundTrip(&buf, 9, .{ .twrite = .{ .fid = 1, .offset = 0, .data = "Edit ,d" } });
- _ = try roundTrip(&buf, 9, .{ .twrite = .{ .fid = 1, .offset = 0, .data = "" } });
- _ = try roundTrip(&buf, 9, .{ .rwrite = .{ .count = 7 } });
- _ = try roundTrip(&buf, 10, .{ .tclunk = .{ .fid = 1 } });
- _ = try roundTrip(&buf, 10, .rclunk);
- _ = try roundTrip(&buf, 11, .{ .tremove = .{ .fid = 1 } });
- _ = try roundTrip(&buf, 11, .rremove);
- _ = try roundTrip(&buf, 12, .{ .tstat = .{ .fid = 1 } });
- _ = try roundTrip(&buf, 12, .{ .rstat = .{ .stat = sample_stat } });
- _ = try roundTrip(&buf, 13, .{ .twstat = .{ .fid = 1, .stat = sample_stat } });
- _ = try roundTrip(&buf, 13, .rwstat);
-
- try testing.expectEqual(@as(usize, 27), @typeInfo(Msg).@"union".fields.len);
- try testing.expectEqual(std.enums.values(Type).len - 1, @typeInfo(Msg).@"union".fields.len);
-}
-
-test "9p: empty and maximum-length strings survive the trip" {
- var buf: [70_000]u8 = undefined;
-
- const empty = try roundTrip(&buf, 1, .{ .tattach = .{ .fid = 0, .afid = nofid, .uname = "", .aname = "" } });
- try testing.expectEqual(@as(usize, 0), empty.tattach.uname.len);
- try testing.expectEqual(@as(usize, header_len + 4 + 4 + 2 + 2), (try encode(empty, 1, &buf)).len);
-
- var big: [65_536]u8 = undefined;
- @memset(&big, 'x');
- const max = big[0..std.math.maxInt(u16)];
- const got = try roundTrip(&buf, 1, .{ .rerror = .{ .ename = max } });
- try testing.expectEqual(@as(usize, 65_535), got.rerror.ename.len);
- try testing.expectError(error.Overlong, encode(.{ .rerror = .{ .ename = &big } }, 1, &buf));
-
- var wide = sample_stat;
- wide.name = max;
- try testing.expectError(error.Overlong, wide.size());
- try testing.expectError(error.Overlong, encode(.{ .rstat = .{ .stat = wide } }, 1, &buf));
-}
-
-test "9p: Twalk carries 0, 1 and 16 elements and refuses 17" {
- var buf: [512]u8 = undefined;
-
- const zero = try roundTrip(&buf, 1, .{ .twalk = .{ .fid = 0, .newfid = 1, .nwname = 0 } });
- try testing.expectEqual(@as(u16, 0), zero.twalk.nwname);
- try testing.expectEqual(@as(usize, header_len + 4 + 4 + 2), (try encode(zero, 1, &buf)).len);
-
- _ = try roundTrip(&buf, 1, .{ .twalk = .{
- .fid = 0,
- .newfid = 1,
- .nwname = 1,
- .wname = .{"body"} ++ @as([max_welem - 1][]const u8, @splat("")),
- } });
-
- const names: [max_welem][]const u8 = .{ "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p" };
- const full = try roundTrip(&buf, 1, .{ .twalk = .{ .fid = 0, .newfid = 1, .nwname = max_welem, .wname = names } });
- try testing.expectEqual(@as(u16, 16), full.twalk.nwname);
- for (names, full.twalk.wname[0..max_welem]) |a, b| try testing.expectEqualStrings(a, b);
- _ = try roundTrip(&buf, 1, .{ .rwalk = .{ .nwqid = max_welem, .wqid = @splat(sample_qid) } });
-
- var raw: [256]u8 = undefined;
- const bad = blk: {
- var w: Writer = .init(&raw);
- try w.putU32(0); // patched below
- try w.putByte(@intFromEnum(Type.twalk));
- try w.putU16(1);
- try w.putU32(0);
- try w.putU32(1);
- try w.putU16(17);
- for (0..17) |i| try w.putString(&[_]u8{@intCast('a' + i)});
- std.mem.writeInt(u32, raw[0..4], @intCast(w.n), .little);
- break :blk raw[0..w.n];
- };
- try testing.expectEqual(@as(usize, header_len + 4 + 4 + 2 + 17 * 3), bad.len);
- try testing.expectError(error.Overlong, decode(bad));
-
- const bad_r = blk: {
- var w: Writer = .init(&raw);
- try w.putU32(0);
- try w.putByte(@intFromEnum(Type.rwalk));
- try w.putU16(1);
- try w.putU16(17);
- for (0..17) |_| try w.putQid(sample_qid);
- std.mem.writeInt(u32, raw[0..4], @intCast(w.n), .little);
- break :blk raw[0..w.n];
- };
- try testing.expectError(error.Overlong, decode(bad_r));
-}
-
-test "9p: the stat double length" {
- var buf: [512]u8 = undefined;
-
- var good: [512]u8 = undefined;
- const n = blk: {
- const bytes = try encode(.{ .rstat = .{ .stat = sample_stat } }, 1, &buf);
- @memcpy(good[0..bytes.len], bytes);
- break :blk bytes.len;
- };
- const inner = try sample_stat.size();
- try testing.expectEqual(inner + 2, std.mem.readInt(u16, good[header_len..][0..2], .little));
- try testing.expectEqual(inner, std.mem.readInt(u16, good[header_len + 2 ..][0..2], .little));
- try testing.expectEqual(header_len + 2 + @as(usize, inner) + 2, n);
-
- const w_bytes = try encode(.{ .twstat = .{ .fid = 7, .stat = sample_stat } }, 1, &buf);
- try testing.expectEqual(inner + 2, std.mem.readInt(u16, w_bytes[header_len + 4 ..][0..2], .little));
- try testing.expectEqual(inner, std.mem.readInt(u16, w_bytes[header_len + 6 ..][0..2], .little));
-
- var off: [512]u8 = undefined;
-
- @memcpy(off[0..n], good[0..n]);
- std.mem.writeInt(u16, off[header_len..][0..2], inner, .little);
- try testing.expectError(error.Truncated, decode(off[0..n]));
-
- @memcpy(off[0..n], good[0..n]);
- std.mem.writeInt(u16, off[header_len..][0..2], inner + 4, .little);
- try testing.expectError(error.Truncated, decode(off[0..n]));
-
- @memcpy(off[0..n], good[0..n]);
- std.mem.writeInt(u16, off[header_len + 2 ..][0..2], inner + 2, .little);
- try testing.expectError(error.Truncated, decode(off[0..n]));
-
- @memcpy(off[0..n], good[0..n]);
- std.mem.writeInt(u16, off[header_len + 2 ..][0..2], inner - 1, .little);
- try testing.expectError(error.Trailing, decode(off[0..n]));
-}
-
-fn expectTruncatedAtEveryBoundary(full: []const u8) !void {
- var scratch: [1024]u8 = undefined;
- var n: usize = 0;
- while (n < full.len) : (n += 1) {
- try testing.expectError(error.Truncated, decode(full[0..n]));
- if (n < header_len) continue;
- @memcpy(scratch[0..n], full[0..n]);
- std.mem.writeInt(u32, scratch[0..4], @intCast(n), .little);
- try testing.expectError(error.Truncated, decode(scratch[0..n]));
- }
- _ = try decode(full);
-}
-
-test "9p: truncation at every field boundary is refused" {
- var buf: [512]u8 = undefined;
-
- try expectTruncatedAtEveryBoundary(try encode(
- .{ .tversion = .{ .msize = 8192, .version = "9P2000" } },
- notag,
- &buf,
- ));
- try expectTruncatedAtEveryBoundary(try encode(.{ .twalk = .{
- .fid = 1,
- .newfid = 2,
- .nwname = 3,
- .wname = .{ "usr", "", "bin" } ++ @as([max_welem - 3][]const u8, @splat("")),
- } }, 1, &buf));
- try expectTruncatedAtEveryBoundary(try encode(
- .{ .tread = .{ .fid = 1, .offset = 0x0102_0304_0506_0708, .count = 8168 } },
- 1,
- &buf,
- ));
- try expectTruncatedAtEveryBoundary(try encode(.{ .rstat = .{ .stat = sample_stat } }, 1, &buf));
- try expectTruncatedAtEveryBoundary(try encode(.{ .rread = .{ .data = "12345678" } }, 1, &buf));
- try expectTruncatedAtEveryBoundary(try encode(
- .{ .rwalk = .{ .nwqid = 3, .wqid = @splat(sample_qid) } },
- 1,
- &buf,
- ));
- try expectTruncatedAtEveryBoundary(try encode(.{ .twstat = .{ .fid = 1, .stat = sample_stat } }, 1, &buf));
-}
-
-test "9p: a size field that disagrees with the buffer is refused" {
- var buf: [512]u8 = undefined;
- const bytes = try encode(.{ .tclunk = .{ .fid = 1 } }, 1, &buf);
- try testing.expectEqual(@as(usize, 11), bytes.len);
-
- var raw: [64]u8 = undefined;
- @memcpy(raw[0..bytes.len], bytes);
-
- for ([_]u32{ 12, 13, 64, 1 << 20, std.math.maxInt(u32) }) |claim| {
- std.mem.writeInt(u32, raw[0..4], claim, .little);
- try testing.expectError(error.Truncated, decode(raw[0..bytes.len]));
- }
-
- std.mem.writeInt(u32, raw[0..4], 10, .little);
- try testing.expectError(error.Trailing, decode(raw[0..bytes.len]));
-
- for ([_]u32{ 0, 1, 6 }) |claim| {
- std.mem.writeInt(u32, raw[0..4], claim, .little);
- try testing.expectError(error.BadValue, decode(raw[0..bytes.len]));
- try testing.expectError(error.BadValue, decode(raw[0..header_len]));
- }
-}
-
-test "9p: an unknown or illegal type byte is refused" {
- var buf: [512]u8 = undefined;
- const bytes = try encode(.{ .tclunk = .{ .fid = 1 } }, 1, &buf);
- var raw: [64]u8 = undefined;
- @memcpy(raw[0..bytes.len], bytes);
-
- for ([_]u8{ 0, 1, 8, 12, 99, 106, 128, 255 }) |t| {
- raw[4] = t;
- try testing.expectError(error.BadTag, decode(raw[0..bytes.len]));
- }
-
- var t: u16 = 0;
- while (t <= 255) : (t += 1) {
- raw[4] = @intCast(t);
- const defined = t >= 100 and t <= 127 and t != @intFromEnum(Type.terror);
- if (decode(raw[0..bytes.len])) |got| {
- try testing.expectEqual(@as(u8, @intCast(t)), @intFromEnum(got.msg.msgType()));
- try testing.expect(t == @intFromEnum(Type.tclunk) or
- t == @intFromEnum(Type.tremove) or
- t == @intFromEnum(Type.tstat) or
- t == @intFromEnum(Type.rwrite));
- } else |err| {
- if (!defined) try testing.expectEqual(Error.BadTag, err);
- }
- }
-}
-
-test "9p: trailing bytes inside the size are refused" {
- var raw: [64]u8 = undefined;
-
- var w: Writer = .init(&raw);
- try w.putU32(12);
- try w.putByte(@intFromEnum(Type.tclunk));
- try w.putU16(1);
- try w.putU32(7);
- try w.putByte(0xAA);
- try testing.expectEqual(@as(usize, 12), w.n);
- try testing.expectError(error.Trailing, decode(raw[0..12]));
-
- w = .init(&raw);
- try w.putU32(header_len + 2 + 2);
- try w.putByte(@intFromEnum(Type.tflush));
- try w.putU16(1);
- try w.putU16(3);
- try w.putU16(3);
- try testing.expectError(error.Trailing, decode(raw[0..w.n]));
-}
-
-test "9p: frameLen needs four bytes" {
- var buf: [512]u8 = undefined;
- const bytes = try encode(.{ .tread = .{ .fid = 1, .offset = 0, .count = 8168 } }, 1, &buf);
- try testing.expectEqual(@as(usize, 23), bytes.len);
-
- for (0..4) |n| try testing.expectEqual(@as(?u32, null), frameLen(bytes[0..n]));
- try testing.expectEqual(@as(?u32, 23), frameLen(bytes[0..4]));
- try testing.expectEqual(@as(?u32, 23), frameLen(bytes));
-
- var raw: [4]u8 = .{ 0xFF, 0xFF, 0xFF, 0xFF };
- try testing.expectEqual(@as(?u32, std.math.maxInt(u32)), frameLen(&raw));
- raw = .{ 0, 0, 0, 0 };
- try testing.expectEqual(@as(?u32, 0), frameLen(&raw));
-}
-
-test "9p: encode refuses a short buffer and writes nothing" {
- var buf: [512]u8 = undefined;
- const want = (try encode(.{ .rstat = .{ .stat = sample_stat } }, 1, &buf)).len;
-
- var n: usize = 0;
- while (n < want) : (n += 1) {
- var scratch: [512]u8 = @splat(0xAA);
- try testing.expectError(error.NoSpace, encode(.{ .rstat = .{ .stat = sample_stat } }, 1, scratch[0..n]));
- for (scratch) |b| try testing.expectEqual(@as(u8, 0xAA), b);
- }
-
- var exact: [512]u8 = @splat(0xAA);
- try testing.expectEqual(want, (try encode(.{ .rstat = .{ .stat = sample_stat } }, 1, exact[0..want])).len);
- try testing.expectEqual(@as(u8, 0xAA), exact[want]);
-}
-
-test "9p: byte for byte against u9fs convS2M" {
- var buf: [512]u8 = undefined;
-
- try testing.expectEqualSlices(u8, &.{
- 0x13, 0x00, 0x00, 0x00, // size = 19
- 0x64, // Tversion = 100
- 0xff, 0xff, // NOTAG
- 0x00, 0x20, 0x00, 0x00, // msize = 8192
- 0x06, 0x00, // n = 6
- '9', 'P',
- '2', '0',
- '0', '0',
- }, try encode(.{ .tversion = .{ .msize = 8192, .version = "9P2000" } }, notag, &buf));
-
- try testing.expectEqualSlices(u8, &.{
- 0x1b, 0x00, 0x00, 0x00, // size = 27
- 0x6e, // Twalk = 110
- 0x01, 0x00, // tag = 1
- 0x01, 0x00, 0x00, 0x00, // fid = 1
- 0x02, 0x00, 0x00, 0x00, // newfid = 2
- 0x02, 0x00, // nwname = 2
- 0x03, 0x00,
- 'u', 's',
- 'r', 0x03,
- 0x00, 'b',
- 'i', 'n',
- }, try encode(.{ .twalk = .{
- .fid = 1,
- .newfid = 2,
- .nwname = 2,
- .wname = .{ "usr", "bin" } ++ @as([max_welem - 2][]const u8, @splat("")),
- } }, 1, &buf));
-
- try testing.expectEqualSlices(u8, &.{
- 0x0e, 0x00, 0x00, 0x00, // size = 14
- 0x75, // Rread = 117
- 0x09, 0x00, // tag = 9
- 0x03, 0x00, 0x00, 0x00, // count = 3
- 'a', 'b', 'c',
- }, try encode(.{ .rread = .{ .data = "abc" } }, 9, &buf));
-
- const one: Stat = .{
- .type = 0,
- .dev = 0,
- .qid = .{ .type = qtdir, .version = 1, .path = 2 },
- .mode = dmdir | 0o755,
- .atime = 3,
- .mtime = 4,
- .length = 0,
- .name = "a",
- .uid = "u",
- .gid = "g",
- .muid = "m",
- };
- try testing.expectEqual(@as(u16, 51), try one.size());
- try testing.expectEqualSlices(u8, &.{
- 0x3e, 0x00, 0x00, 0x00, // size = 62
- 0x7d, // Rstat = 125
- 0x07, 0x00, // tag = 7
- 0x35, 0x00, // OUTER count = 53 = 51 + 2
- 0x33, 0x00, // stat size = 51, excluding these two
- 0x00, 0x00, // type
- 0x00, 0x00, 0x00, 0x00, // dev
- 0x80, // qid.type = QTDIR
- 0x01, 0x00, 0x00, 0x00, // qid.version = 1
- 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // qid.path = 2
- 0xed, 0x01, 0x00, 0x80, // mode = DMDIR | 0755
- 0x03, 0x00, 0x00, 0x00, // atime
- 0x04, 0x00, 0x00, 0x00, // mtime
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // length
- 0x01, 0x00, 'a', // name
- 0x01, 0x00, 'u', // uid
- 0x01, 0x00, 'g', // gid
- 0x01, 0x00, 'm', // muid
- }, try encode(.{ .rstat = .{ .stat = one } }, 7, &buf));
-
- try testing.expectEqual(qtdir, @as(u8, @intCast(dmdir >> 24)));
- try testing.expectEqual(qtappend, @as(u8, @intCast(dmappend >> 24)));
- try testing.expectEqual(qtexcl, @as(u8, @intCast(dmexcl >> 24)));
- try testing.expectEqual(qtauth, @as(u8, @intCast(dmauth >> 24)));
- try testing.expectEqual(qttmp, @as(u8, @intCast(dmtmp >> 24)));
- try testing.expectEqual(@as(u32, 0o777), dmperm);
-}
-
pub const e_unknown_fid = "fid unknown or out of range";
pub const e_fid_in_use = "fid already in use";
pub const e_bad_use = "bad use of fid";
@@ -1222,18 +134,9 @@ pub fn Server(comptime fs: type, comptime fid_capacity: usize) type {
return struct {
const Self = @This();
- in: []u8,
- out: []u8,
+ protocol: cloud9.Server,
root: u64,
- in_len: usize = 0,
- frame: u32 = 0,
- out_len: usize = 0,
- out_off: usize = 0,
-
- msize: u32 = 0,
- dead: bool = false,
-
uname: [username_capacity]u8 = @splat(0),
uname_len: u8 = 0,
@@ -1306,7 +209,7 @@ pub fn Server(comptime fs: type, comptime fid_capacity: usize) type {
assert(opts.in.len >= msize_min);
assert(opts.out.len >= 2 * msize_min);
assert(opts.root != 0);
- return .{ .in = opts.in, .out = opts.out, .root = opts.root };
+ return .{ .protocol = .init(.{ .in = opts.in, .out = opts.out }), .root = opts.root };
}
pub fn references(s: *const Self, node: u64) bool {
@@ -1318,11 +221,7 @@ pub fn Server(comptime fs: type, comptime fid_capacity: usize) type {
pub fn hangup(s: *Self) void {
s.reset();
- s.dead = true;
- s.in_len = 0;
- s.frame = 0;
- s.out_len = 0;
- s.out_off = 0;
+ s.protocol.hangup();
}
fn reset(s: *Self) void {
@@ -1335,45 +234,25 @@ pub fn Server(comptime fs: type, comptime fid_capacity: usize) type {
}
pub fn push(s: *Self, bytes: []const u8) usize {
- if (s.dead) return 0;
- const n = @min(bytes.len, s.in.len - s.in_len);
- @memcpy(s.in[s.in_len..][0..n], bytes[0..n]);
- s.in_len += n;
- return n;
+ return s.protocol.push(bytes);
}
pub fn output(s: *const Self) []const u8 {
- return s.out[s.out_off..s.out_len];
+ return s.protocol.output();
}
pub fn wrote(s: *Self, n: usize) void {
- assert(n <= s.out_len - s.out_off);
- s.out_off += n;
- if (s.out_off == s.out_len) {
- s.out_off = 0;
- s.out_len = 0;
- }
- }
-
- fn compact(s: *Self) void {
- assert(s.out_off <= s.out_len);
- const n = s.out_len - s.out_off;
- std.mem.copyForwards(u8, s.out[0..n], s.out[s.out_off..s.out_len]);
- s.out_off = 0;
- s.out_len = n;
+ s.protocol.wrote(n);
}
fn hasRoom(s: *Self) bool {
- if (s.out_off != 0) s.compact();
- return s.out.len - s.out_len >= @max(s.msize, msize_min);
+ return s.protocol.hasRoom();
}
fn emit(s: *Self, tag: u16, msg: Msg) void {
- const bytes = encode(msg, tag, s.out[s.out_len..]) catch {
- s.dead = true;
- return;
+ s.protocol.reply(tag, msg) catch {
+ s.protocol.dead = true;
};
- s.out_len += bytes.len;
}
fn fail(s: *Self, tag: u16, ename: []const u8) void {
@@ -1477,44 +356,14 @@ pub fn Server(comptime fs: type, comptime fid_capacity: usize) type {
fn startFrame(s: *Self) bool {
assert(s.job.kind == .none);
- assert(s.frame == 0);
- if (s.dead) return false;
- const len = frameLen(s.in[0..s.in_len]) orelse return false;
- if (len < header_len or len > s.in.len) {
- s.dead = true;
- return false;
- }
- if (len > s.in_len) return false;
- s.frame = len;
+ const got = (s.protocol.receive() catch return false) orelse return false;
defer if (s.job.kind == .none) s.dropFrame();
-
- const got = decode(s.in[0..len]) catch {
- s.fail(std.mem.readInt(u16, s.in[5..7], .little), e_botch);
- return true;
- };
- if (!isT(got.msg.msgType())) {
- s.fail(got.tag, e_botch);
- return true;
- }
- if (s.msize == 0 and got.msg != .tversion) {
- s.fail(got.tag, e_botch);
- return true;
- }
- if (s.msize != 0 and len > s.msize) {
- s.fail(got.tag, e_botch);
- return true;
- }
s.dispatch(got);
return true;
}
fn dropFrame(s: *Self) void {
- assert(s.frame != 0);
- assert(s.frame <= s.in_len);
- const n = s.frame;
- std.mem.copyForwards(u8, s.in[0 .. s.in_len - n], s.in[n..s.in_len]);
- s.in_len -= n;
- s.frame = 0;
+ s.protocol.release();
}
fn dispatch(s: *Self, got: Decoded) void {
@@ -1537,13 +386,16 @@ pub fn Server(comptime fs: type, comptime fid_capacity: usize) type {
}
fn version(s: *Self, tag: u16, want: u32, ver: []const u8) void {
- const cap: u32 = @intCast(@min(s.in.len, s.out.len / 2));
- const m = @min(want, cap);
- if (m < msize_min) return s.fail(tag, e_small_msize);
- const known = std.mem.startsWith(u8, ver, "9P");
+ assert(tag == notag);
s.reset();
- s.msize = if (known) m else 0;
- s.emit(tag, .{ .rversion = .{ .msize = m, .version = if (known) "9P2000" else "unknown" } });
+ // The adapter's fixed directory staging requires this capacity.
+ if (want < msize_min) {
+ s.protocol.dead = true;
+ return;
+ }
+ s.protocol.negotiate(want, ver) catch {
+ s.protocol.dead = true;
+ };
}
fn attach(s: *Self, tag: u16, fid: u32, uname: []const u8, aname: []const u8) void {
@@ -1610,7 +462,7 @@ pub fn Server(comptime fs: type, comptime fid_capacity: usize) type {
const i = s.findFid(fid) orelse return s.fail(tag, e_unknown_fid);
const f = &s.fids[i];
if (!f.open or (f.omode & 3) == owrite) return s.fail(tag, e_bad_use);
- const want = @min(count, s.msize - header_len - 4);
+ const want = @min(count, s.protocol.msize - header_len - 4);
if (!f.dir) {
s.job = .{ .kind = .read, .tag = tag, .fid = fid, .offset = offset, .count = want };
return;
@@ -1821,7 +673,7 @@ pub fn Server(comptime fs: type, comptime fid_capacity: usize) type {
fn finishJob(s: *Self) void {
s.job = .{};
- if (s.frame != 0) s.dropFrame();
+ if (s.protocol.frame != 0) s.dropFrame();
}
fn jobReply(s: *Self, r: *const fs.Reply, bytes: []const u8) void {
@@ -1893,7 +745,7 @@ pub fn Server(comptime fs: type, comptime fid_capacity: usize) type {
f.dirindex = 0;
s.emit(j.tag, .{ .ropen = .{
.qid = qidOf(f.node, f.dir),
- .iounit = s.msize - iohdrsz,
+ .iounit = s.protocol.msize - iohdrsz,
} });
s.finishJob();
},
@@ -1914,7 +766,7 @@ pub fn Server(comptime fs: type, comptime fid_capacity: usize) type {
f.perm = r.attr.mode;
f.dir = r.attr.dir;
const response: Msg = .{ .rstat = .{ .stat = s.statOf(f, r.attr) } };
- if ((totalLen(response) catch unreachable) > s.msize)
+ if ((totalLen(response) catch unreachable) > s.protocol.msize)
s.fail(j.tag, e_small_msize)
else
s.emit(j.tag, response);
@@ -1988,7 +840,7 @@ pub fn Server(comptime fs: type, comptime fid_capacity: usize) type {
}
fn emitDirRead(s: *Self, tag: u16, fid: u32, staging: []const u8, count: u32) void {
- const buf = s.out[s.out_len..];
+ const buf = s.protocol.out[s.protocol.out_len..];
assert(buf.len > header_len + 4);
const cap = @min(@as(usize, count), buf.len - header_len - 4);
const who = s.uname[0..s.uname_len];
@@ -2022,11 +874,7 @@ pub fn Server(comptime fs: type, comptime fid_capacity: usize) type {
if (entries == 0 and staging.len != 0) return s.fail(tag, e_count_small);
const payload: u32 = @intCast(n - header_len - 4);
comptime assert(header_len == 7);
- std.mem.writeInt(u32, buf[0..4], @intCast(n), .little);
- buf[4] = @intFromEnum(Type.rread);
- std.mem.writeInt(u16, buf[5..7], tag, .little);
- std.mem.writeInt(u32, buf[7..11], payload, .little);
- s.out_len += n;
+ s.emit(tag, .{ .rread = .{ .data = buf[header_len + 4 .. n] } });
if (s.findFid(fid)) |k| {
s.fids[k].diroff += payload;
s.fids[k].dirindex += entries;
@@ -2315,12 +1163,10 @@ test "9p server: the version handshake clamps, falls back, and refuses" {
got = try h.reap();
try testing.expectEqualStrings("unknown", got.msg.rversion.version);
try h.send(1, .{ .tattach = .{ .fid = 0, .afid = nofid, .uname = "goblin", .aname = "" } });
- got = try h.reap();
- try testing.expectEqualStrings(e_botch, got.msg.rerror.ename);
-
+ try testing.expect(h.srv.protocol.dead);
+ h.start();
try h.send(notag, .{ .tversion = .{ .msize = 64, .version = "9P2000" } });
- got = try h.reap();
- try testing.expectEqualStrings(e_small_msize, got.msg.rerror.ename);
+ try testing.expect(h.srv.protocol.dead);
try testing.expectEqual(@as(u32, 216), msize_min - 1);
}
@@ -2450,7 +1296,7 @@ test "9p server: backend-sized filenames survive walk and stat within negotiated
var out: [2048]u8 = undefined;
var encoded: [1024]u8 = undefined;
var server = Native.init(.{ .in = &in, .out = &out, .root = 1 });
- server.msize = case.msize;
+ server.protocol.msize = case.msize;
server.setUname("u" ** username_capacity);
server.fids[0] = .{ .used = true, .fid = 0, .node = 1, .dir = true, .perm = 0o500 };
const name = filename[0..case.length];
@@ -2489,7 +1335,7 @@ test "9p server: backend-sized filenames survive walk and stat within negotiated
try testing.expectEqual(null, server.next());
response = try decode(server.output());
try testing.expect(response.msg == .rclunk);
- try testing.expect(!server.dead);
+ try testing.expect(!server.protocol.dead);
}
}
@@ -2636,6 +1482,13 @@ test "9p server: long directory names remain whole across pages" {
@memcpy(entries[275..], "next");
var found: [2][]const u8 = undefined;
+ {
+ var request: [64]u8 = undefined;
+ const bytes = try encode(.{ .tread = .{ .fid = 0, .offset = 0, .count = 330 } }, 5, &request);
+ _ = h.srv.protocol.push(bytes);
+ _ = (try h.srv.protocol.receive()).?;
+ h.srv.protocol.release();
+ }
h.srv.emitDirRead(5, 0, &entries, 330);
var got = try h.reap();
const first = got.msg.rread.data.len;
@@ -2643,6 +1496,13 @@ test "9p server: long directory names remain whole across pages" {
try testing.expectEqualStrings(entries[10..265], found[0]);
try testing.expectEqual(@as(u64, 1), h.srv.fids[0].dirindex);
+ {
+ var request: [64]u8 = undefined;
+ const bytes = try encode(.{ .tread = .{ .fid = 0, .offset = 0, .count = 330 } }, 6, &request);
+ _ = h.srv.protocol.push(bytes);
+ _ = (try h.srv.protocol.receive()).?;
+ h.srv.protocol.release();
+ }
h.srv.emitDirRead(6, 0, entries[265..], 330);
got = try h.reap();
try testing.expectEqual(@as(usize, 1), try dirNames(got.msg.rread.data, &found));
@@ -2650,6 +1510,13 @@ test "9p server: long directory names remain whole across pages" {
try testing.expectEqual(@as(u64, 2), h.srv.fids[0].dirindex);
try testing.expectEqual(first + got.msg.rread.data.len, h.srv.fids[0].diroff);
+ {
+ var request: [64]u8 = undefined;
+ const bytes = try encode(.{ .tread = .{ .fid = 0, .offset = 0, .count = 4096 } }, 7, &request);
+ _ = h.srv.protocol.push(bytes);
+ _ = (try h.srv.protocol.receive()).?;
+ h.srv.protocol.release();
+ }
h.srv.emitDirRead(7, 0, &entries, 4096);
got = try h.reap();
try testing.expectEqual(@as(usize, 2), try dirNames(got.msg.rread.data, &found));
@@ -2728,7 +1595,7 @@ test "9p server: the reply queue is a FIFO that survives a partial write" {
var flood: [8192]u8 = @splat(0);
try testing.expectEqual(@as(usize, 4096), h.srv.push(&flood));
h.pump();
- try testing.expect(h.srv.dead);
+ try testing.expect(h.srv.protocol.dead);
try testing.expectEqual(@as(usize, 0), h.srv.push(&flood));
}
@@ -2938,36 +1805,18 @@ test "9p server: a message arriving a byte at a time is served when its last byt
try h.quiet();
}
-test "9p server: what is not 9P2000 on this connection is refused, not guessed" {
- var h: Harness = .{};
- try h.handshake(4096);
-
- var buf: [64]u8 = undefined;
- const good = try encode(.{ .tstat = .{ .fid = 0 } }, 5, &buf);
-
- var raw: [64]u8 = undefined;
- @memcpy(raw[0..good.len], good);
- raw[4] = @intFromEnum(Type.rstat);
- try testing.expectEqual(good.len, h.srv.push(raw[0..good.len]));
- h.pump();
- var got = try h.reap();
- try testing.expectEqual(@as(u16, 5), got.tag);
- try testing.expectEqualStrings(e_botch, got.msg.rerror.ename);
-
- @memcpy(raw[0..good.len], good);
- raw[4] = 8;
- _ = h.srv.push(raw[0..good.len]);
- h.pump();
- got = try h.reap();
- try testing.expectEqual(@as(u16, 5), got.tag);
- try testing.expectEqualStrings(e_botch, got.msg.rerror.ename);
-
- @memcpy(raw[0..good.len], good);
- std.mem.writeInt(u32, raw[0..4], 3, .little);
- _ = h.srv.push(raw[0..good.len]);
- h.pump();
- try h.quiet();
- try testing.expect(h.srv.dead);
+test "9p server: invalid framing and reply types terminate the connection" {
+ for ([_]u8{ @intFromEnum(Type.rstat), 8, 0 }) |kind| {
+ var h: Harness = .{};
+ try h.handshake(4096);
+ var buf: [64]u8 = undefined;
+ const raw = try encode(.{ .tstat = .{ .fid = 0 } }, 5, &buf);
+ if (kind == 0) std.mem.writeInt(u32, raw[0..4], 3, .little) else raw[4] = kind;
+ _ = h.srv.push(raw);
+ h.pump();
+ try h.quiet();
+ try testing.expect(h.srv.protocol.dead);
+ }
}
test "9p server: a connection that drops still pays the core its releases" {
@@ -3060,314 +1909,6 @@ test "9p server: board and native capacities size the actual fid storage" {
try testing.expect(3 * 4096 + @sizeOf(Board) <= 24 * 1024);
}
-pub const max_tags: usize = 16;
-
-const twrite_header: usize = header_len + 4 + 8 + 4;
-
-const rread_header: usize = header_len + 4;
-
-comptime {
- assert(twrite_header == 23);
- assert(rread_header == 11);
- assert(max_tags <= notag);
- assert(msize_min > rread_header);
- assert(msize_min > twrite_header);
-}
-
-pub const ClientError = error{
- NoTags,
- NoSpace,
- TooLarge,
- Handshake,
- Dead,
- BadRequest,
-};
-
-pub const Client = struct {
- in: []u8,
- out: []u8,
-
- in_len: usize = 0,
- frame: u32 = 0,
- out_len: usize = 0,
- out_off: usize = 0,
-
- msize: u32 = 0,
- asked: u32 = 0,
- versioning: bool = false,
- dead: bool = false,
-
- tags: [max_tags]Slot = @splat(.{}),
-
- const Slot = struct {
- op: ?Op = null,
- count: u32 = 0,
- };
-
- pub const Op = enum { version, attach, walk, open, read, write, clunk, stat };
-
- pub const Request = union(Op) {
- version: struct { msize: u32 = 0 },
- attach: struct { fid: u32, uname: []const u8, aname: []const u8 = "" },
- walk: struct { fid: u32, newfid: u32, names: []const []const u8 },
- open: struct { fid: u32, mode: u8 },
- read: struct { fid: u32, offset: u64, count: u32 },
- write: struct { fid: u32, offset: u64, data: []const u8 },
- clunk: struct { fid: u32 },
- stat: struct { fid: u32 },
- };
-
- pub const Result = union(enum) {
- fail: []const u8,
- version: struct { msize: u32, version: []const u8 },
- attach: Qid,
- walk: struct { nwqid: u16, wqid: [max_welem]Qid },
- open: struct { qid: Qid, iounit: u32 },
- read: []const u8,
- write: u32,
- clunk: void,
- stat: Stat,
- };
-
- pub const Done = struct {
- tag: u16,
- op: Op,
- result: Result,
- };
-
- pub const Options = struct {
- in: []u8,
- out: []u8,
- };
-
- pub fn init(opts: Options) Client {
- assert(opts.in.len >= msize_min);
- assert(opts.out.len >= msize_min);
- return .{ .in = opts.in, .out = opts.out };
- }
-
- pub fn hangup(c: *Client) void {
- c.dead = true;
- c.tags = @splat(.{});
- c.versioning = false;
- c.msize = 0;
- c.asked = 0;
- c.in_len = 0;
- c.frame = 0;
- c.out_len = 0;
- c.out_off = 0;
- }
-
- pub fn push(c: *Client, bytes: []const u8) usize {
- if (c.dead) return 0;
- const n = @min(bytes.len, c.in.len - c.in_len);
- @memcpy(c.in[c.in_len..][0..n], bytes[0..n]);
- c.in_len += n;
- return n;
- }
-
- pub fn output(c: *const Client) []const u8 {
- return c.out[c.out_off..c.out_len];
- }
-
- pub fn wrote(c: *Client, n: usize) void {
- assert(n <= c.out_len - c.out_off);
- c.out_off += n;
- if (c.out_off == c.out_len) {
- c.out_off = 0;
- c.out_len = 0;
- }
- }
-
- fn compact(c: *Client) void {
- assert(c.out_off <= c.out_len);
- const n = c.out_len - c.out_off;
- std.mem.copyForwards(u8, c.out[0..n], c.out[c.out_off..c.out_len]);
- c.out_off = 0;
- c.out_len = n;
- }
-
- fn dropFrame(c: *Client) void {
- assert(c.frame != 0);
- assert(c.frame <= c.in_len);
- const n = c.frame;
- std.mem.copyForwards(u8, c.in[0 .. c.in_len - n], c.in[n..c.in_len]);
- c.in_len -= n;
- c.frame = 0;
- }
-
- pub fn maxRead(c: *const Client) u32 {
- if (c.msize == 0) return 0;
- return c.msize - @as(u32, @intCast(rread_header));
- }
-
- pub fn maxWrite(c: *const Client) u32 {
- if (c.msize == 0) return 0;
- return c.msize - @as(u32, @intCast(twrite_header));
- }
-
- pub fn pending(c: *const Client) usize {
- var n: usize = @intFromBool(c.versioning);
- for (c.tags) |t| n += @intFromBool(t.op != null);
- return n;
- }
-
- pub fn submit(c: *Client, req: Request) ClientError!u16 {
- if (c.dead) return error.Dead;
- if (req == .version) return c.beginVersion(req.version.msize);
- if (c.msize == 0 or c.versioning) return error.Handshake;
-
- const msg: Msg = switch (req) {
- .version => unreachable, // handled above
- .attach => |m| blk: {
- if (m.fid == nofid) return error.BadRequest;
- break :blk .{ .tattach = .{
- .fid = m.fid,
- .afid = nofid,
- .uname = m.uname,
- .aname = m.aname,
- } };
- },
- .walk => |m| blk: {
- if (m.fid == nofid or m.newfid == nofid) return error.BadRequest;
- if (m.names.len > max_welem) return error.BadRequest;
- var w: [max_welem][]const u8 = @splat("");
- for (m.names, 0..) |n, i| {
- if (n.len == 0) return error.BadRequest;
- if (std.mem.indexOfAny(u8, n, "/\x00") != null) return error.BadRequest;
- w[i] = n;
- }
- break :blk .{ .twalk = .{
- .fid = m.fid,
- .newfid = m.newfid,
- .nwname = @intCast(m.names.len),
- .wname = w,
- } };
- },
- .open => |m| blk: {
- if (m.fid == nofid) return error.BadRequest;
- break :blk .{ .topen = .{ .fid = m.fid, .mode = m.mode } };
- },
- .read => |m| blk: {
- if (m.fid == nofid) return error.BadRequest;
- if (m.count > c.maxRead()) return error.TooLarge;
- break :blk .{ .tread = .{ .fid = m.fid, .offset = m.offset, .count = m.count } };
- },
- .write => |m| blk: {
- if (m.fid == nofid) return error.BadRequest;
- break :blk .{ .twrite = .{ .fid = m.fid, .offset = m.offset, .data = m.data } };
- },
- .clunk => |m| blk: {
- if (m.fid == nofid) return error.BadRequest;
- break :blk .{ .tclunk = .{ .fid = m.fid } };
- },
- .stat => |m| blk: {
- if (m.fid == nofid) return error.BadRequest;
- break :blk .{ .tstat = .{ .fid = m.fid } };
- },
- };
- const need = totalLen(msg) catch return error.TooLarge;
- if (need > c.msize) return error.TooLarge;
-
- const op = std.meta.activeTag(req);
- const tag = c.claim(op) orelse return error.NoTags;
- errdefer c.tags[tag] = .{};
- try c.emit(tag, msg);
- if (op == .read) c.tags[tag].count = req.read.count;
- return tag;
- }
-
- fn beginVersion(c: *Client, want: u32) ClientError!u16 {
- if (c.pending() != 0) return error.Handshake;
- const cap: u32 = @intCast(@min(c.in.len, c.out.len, std.math.maxInt(u32)));
- const m = @min(if (want == 0) cap else want, cap);
- if (m < msize_min) return error.BadRequest;
- try c.emit(notag, .{ .tversion = .{ .msize = m, .version = "9P2000" } });
- c.msize = 0;
- c.asked = m;
- c.versioning = true;
- return notag;
- }
-
- fn claim(c: *Client, op: Op) ?u16 {
- for (&c.tags, 0..) |*t, i| {
- if (t.op != null) continue;
- t.* = .{ .op = op };
- return @intCast(i);
- }
- return null;
- }
-
- fn emit(c: *Client, tag: u16, msg: Msg) ClientError!void {
- if (c.out_off != 0) c.compact();
- const bytes = encode(msg, tag, c.out[c.out_len..]) catch return error.NoSpace;
- c.out_len += bytes.len;
- }
-
- pub fn take(c: *Client) ?Done {
- if (c.frame != 0) c.dropFrame();
- if (c.dead) return null;
- const len = frameLen(c.in[0..c.in_len]) orelse return null;
- if (len < header_len or len > c.in.len) return c.die();
- if (c.msize != 0 and len > c.msize) return c.die();
- if (len > c.in_len) return null;
- c.frame = len;
- const got = decode(c.in[0..len]) catch return c.die();
- return c.consume(got);
- }
-
- fn die(c: *Client) ?Done {
- c.dead = true;
- return null;
- }
-
- fn consume(c: *Client, got: Decoded) ?Done {
- if (isT(got.msg.msgType())) return c.die();
- if (got.msg == .rversion) return c.version(got);
- if (c.versioning or c.msize == 0) return c.die();
- if (got.tag >= max_tags) return c.die();
- const slot = &c.tags[got.tag];
- const op = slot.op orelse return c.die();
- const result: Result = switch (got.msg) {
- .rerror => |m| .{ .fail = m.ename },
- .rattach => |m| if (op != .attach) return c.die() else .{ .attach = m.qid },
- .rwalk => |m| if (op != .walk) return c.die() else .{
- .walk = .{ .nwqid = m.nwqid, .wqid = m.wqid },
- },
- .ropen => |m| if (op != .open) return c.die() else .{
- .open = .{ .qid = m.qid, .iounit = m.iounit },
- },
- .rread => |m| blk: {
- if (op != .read) return c.die();
- if (m.data.len > slot.count) return c.die();
- break :blk .{ .read = m.data };
- },
- .rwrite => |m| if (op != .write) return c.die() else .{ .write = m.count },
- .rclunk => if (op != .clunk) return c.die() else .clunk,
- .rstat => |m| if (op != .stat) return c.die() else .{ .stat = m.stat },
- else => return c.die(),
- };
- slot.* = .{};
- return .{ .tag = got.tag, .op = op, .result = result };
- }
-
- fn version(c: *Client, got: Decoded) ?Done {
- if (!c.versioning) return c.die();
- if (got.tag != notag) return c.die();
- const m = got.msg.rversion;
- if (m.msize > c.asked or m.msize < msize_min) return c.die();
- c.versioning = false;
- if (std.mem.eql(u8, m.version, "9P2000")) {
- c.msize = m.msize;
- } else if (!std.mem.eql(u8, m.version, "unknown")) {
- return c.die();
- }
- return .{ .tag = notag, .op = .version, .result = .{
- .version = .{ .msize = m.msize, .version = m.version },
- } };
- }
-};
-
const Pair = struct {
srv_in: [4096]u8 = undefined,
srv_out: [8192]u8 = undefined,
@@ -3647,7 +2188,7 @@ test "9p client: an msize below the floor, and one the server tried to raise" {
var buf: [64]u8 = undefined;
var c = Client.init(.{ .in = &in, .out = &out });
- try testing.expectError(error.BadRequest, c.submit(.{ .version = .{ .msize = msize_min - 1 } }));
+ try testing.expectError(error.BadRequest, c.submit(.{ .version = .{ .msize = cloud9.Server.msize_min - 1 } }));
_ = try c.submit(.{ .version = .{} });
try testing.expectEqual(@as(u32, 512), c.asked);
@@ -3771,9 +2312,8 @@ test "9p client: hangup and a dead connection refuse everything after" {
try testing.expectError(error.Dead, p.cli.submit(.{ .version = .{} }));
}
-test "9p client: one session is a hundred and change bytes plus its buffers" {
- try testing.expect(@sizeOf(Client.Slot) <= 8);
- try testing.expect(@sizeOf(Client) <= 256);
+test "9p client: session memory is bounded independently of input" {
+ try testing.expect(@sizeOf(Client) <= 512);
try testing.expect(2 * 8192 + @sizeOf(Client) <= 17 * 1024);
- try testing.expect(2 * msize_min + @sizeOf(Client) <= 700);
+ try testing.expect(2 * cloud9.Server.msize_min + @sizeOf(Client) <= 700);
}
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 },