summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/9p.zig2311
-rw-r--r--src/9p_io.zig2
-rw-r--r--src/esp32p4_9p.zig2
-rw-r--r--src/esp32p4_gpio.zig54
-rw-r--r--src/ninep/tree.zig100
-rw-r--r--src/pardes.zig2
6 files changed, 70 insertions, 2401 deletions
diff --git a/src/9p.zig b/src/9p.zig
index 3cffbffc..c0e7f61e 100644
--- a/src/9p.zig
+++ b/src/9p.zig
@@ -1,8 +1,10 @@
-//! Pardes filesystem adapter. The 9P protocol and transports live in cloud9.
-const std = @import("std");
-const assert = std.debug.assert;
-const testing = std.testing;
+//! The editor's 9P surface. The wire protocol, the client, the transports
+//! and the file-server engine (`cloud9.fs.Server`) live in cloud9; this
+//! module names the two engine configurations Pardes builds, the editor's
+//! and the board's, and re-exports what the transports and firmware use.
const cloud9 = @import("cloud9");
+pub const fs = cloud9.fs;
+pub const Server = cloud9.fs.Server;
pub const Error = cloud9.Error;
pub const Type = cloud9.Type;
pub const isT = cloud9.isT;
@@ -37,2299 +39,50 @@ 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 = .{
- .type = 0,
- .dev = 0,
- .qid = sample_qid,
- .mode = dmdir | 0o755,
- .atime = 1,
- .mtime = 2,
- .length = 0,
- .name = "body",
- .uid = "goblin",
- .gid = "goblin",
- .muid = "goblin",
-};
+pub const oread = cloud9.oread;
+pub const owrite = cloud9.owrite;
+pub const ordwr = cloud9.ordwr;
+pub const oexec = cloud9.oexec;
+pub const otrunc = cloud9.otrunc;
+pub const ocexec = cloud9.ocexec;
+pub const orclose = cloud9.orclose;
+pub const errmax = cloud9.fs.errmax;
+pub const errString = cloud9.fs.errString;
+pub const msize_min = cloud9.fs.msize_min;
-pub const e_unknown_fid = "fid unknown or out of range";
-pub const e_fid_in_use = "fid already in use";
-pub const e_bad_use = "bad use of fid";
-pub const e_bad_offset = "bad offset in directory read";
-pub const e_perm = "permission denied";
-pub const e_not_dir = "not a directory";
-pub const e_already_open = "file already open for I/O";
-pub const e_illegal_name = "illegal name";
-pub const e_too_many_fids = "Too many open files in system";
-pub const e_botch = "protocol botch";
-pub const e_interrupted = "Interrupted system call";
-pub const e_trunc_only = "only support truncation to zero length";
-pub const e_wstat = "wstat prohibited";
-pub const e_again = "Resource temporarily unavailable";
-pub const e_count_small = "Invalid argument";
-pub const e_no_tree = "No such file or directory";
-pub const e_no_auth = "authentication not required";
-pub const e_small_msize = "Invalid argument";
-
-pub fn errString(errno: u16) []const u8 {
- return switch (errno) {
- 1 => "Operation not permitted", // E.PERM, EPERM
- 2 => "No such file or directory", // E.NOENT, ENOENT
- 5 => "Input/output error", // E.IO, EIO
- 12 => "Cannot allocate memory", // E.NOMEM, ENOMEM
- 20 => "Not a directory", // E.NOTDIR, ENOTDIR
- 22 => "Invalid argument", // E.INVAL, EINVAL
- 23 => "Too many open files in system", // E.NFILE, ENFILE
- 28 => "No space left on device", // E.NOSPC, ENOSPC
- 38 => "Function not implemented", // E.NOSYS, ENOSYS
- else => "Input/output error",
- };
-}
+/// The smallest msize a native listener offers (src/9p_io.zig).
+pub const min_msize: u32 = 4096;
-pub const oread: u8 = 0;
-pub const owrite: u8 = 1;
-pub const ordwr: u8 = 2;
-pub const oexec: u8 = 3;
-pub const otrunc: u8 = 16;
-pub const ocexec: u8 = 32;
-pub const orclose: u8 = 64;
+/// The editor's engine: native fid count, native file names.
+pub const editor: cloud9.fs.Options = .{ .fid_capacity = max_fids, .name_capacity = 255 };
+/// The board's engine: the GPIO firmware's budget (src/esp32p4_9p.zig).
+pub const board: cloud9.fs.Options = .{ .fid_capacity = board_fids, .name_capacity = board_name_capacity };
pub const max_fids: usize = 256;
-
pub const board_fids: usize = 32;
-
-pub const max_slots: usize = 32;
-
-pub const park_data_max: usize = 128;
-
pub const board_name_capacity: usize = 28;
-const username_capacity: usize = 28;
-
-pub const msize_min: u32 = header_len + 2 + max_welem * qid_len;
-
-comptime {
- assert(msize_min == 217);
- assert(header_len + 2 + stat_fixed + board_name_capacity + 3 * username_capacity <= msize_min);
- assert(header_len + 4 + stat_fixed + board_name_capacity + 3 * username_capacity <= msize_min);
- assert(username_capacity >= 20);
- assert(oread | owrite | ordwr | oexec == 3);
- assert(otrunc | ocexec | orclose == 112);
-}
-
-fn qidOf(node: u64, dir: bool) Qid {
- return .{ .type = if (dir) qtdir else qtfile, .version = 0, .path = node };
-}
-
-pub const dirent_dir_perm: u16 = 0o500;
-pub const dirent_file_perm: u16 = 0o600;
-
-pub fn Server(comptime fs: type, comptime fid_capacity: usize) type {
- if (fid_capacity == 0) @compileError("9P server needs at least one fid");
- const name_capacity = if (@hasDecl(fs, "name_capacity")) fs.name_capacity else board_name_capacity;
- if (name_capacity == 0 or name_capacity > 255) @compileError("9P backend name capacity must fit a directory entry");
- return struct {
- const Self = @This();
-
- protocol: cloud9.Server,
- root: u64,
-
- uname: [username_capacity]u8 = @splat(0),
- uname_len: u8 = 0,
-
- fids: [fid_capacity]Fid = @splat(.{}),
- slots: [max_slots]Slot = @splat(.{}),
- job: Job = .{},
- seq: u64 = 0,
-
- const Kind = enum { none, attach, walk, open, read, readdir, write, clunk, remove, stat, wstat };
-
- const Fid = struct {
- used: bool = false,
- fid: u32 = 0,
- node: u64 = 0,
- dir: bool = false,
- perm: u16 = 0,
- open: bool = false,
- omode: u8 = 0,
- handle: u32 = 0,
- diroff: u64 = 0,
- dirindex: u32 = 0,
- orphan: bool = false,
- name: [name_capacity]u8 = @splat(0),
- name_len: u8 = 0,
- };
-
- const Slot = struct {
- used: bool = false,
- parked: bool = false,
- retried: bool = false,
- copied: bool = false,
- seq: u64 = 0,
- tag: u16 = 0,
- kind: Kind = .none,
- fid: u32 = 0,
- count: u32 = 0,
- req: fs.Req = undefined,
- data: [park_data_max]u8 = undefined,
- };
-
- const Job = struct {
- kind: Kind = .none,
- tag: u16 = 0,
- req_tag: u64 = 0,
- req: fs.Req = undefined,
- step: u8 = 0,
- fid: u32 = 0,
- newfid: u32 = 0,
- count: u32 = 0,
- offset: u64 = 0,
- omode: u8 = 0,
- node: u64 = 0,
- dir: bool = false,
- perm: u16 = 0,
- name: [name_capacity]u8 = @splat(0),
- name_len: u8 = 0,
- nwname: u8 = 0,
- nwqid: u8 = 0,
- wqid: [max_welem]Qid = @splat(.{ .type = 0, .version = 0, .path = 0 }),
- msg: Msg = .rflush,
- };
-
- pub const Options = struct {
- in: []u8,
- out: []u8,
- root: u64,
- };
-
- pub fn init(opts: Options) Self {
- assert(opts.in.len >= msize_min);
- assert(opts.out.len >= 2 * msize_min);
- assert(opts.root != 0);
- return .{ .protocol = .init(.{ .in = opts.in, .out = opts.out }), .root = opts.root };
- }
-
- pub fn references(s: *const Self, node: u64) bool {
- for (s.fids) |fid| if (fid.used and fid.node == node) return true;
- if (s.job.kind != .none and s.job.node == node) return true;
- for (s.slots) |slot| if (slot.used and slot.req.node == node) return true;
- return false;
- }
-
- pub fn hangup(s: *Self) void {
- s.reset();
- s.protocol.hangup();
- }
-
- fn reset(s: *Self) void {
- for (&s.fids) |*f| {
- if (!f.used) continue;
- if (f.open) f.orphan = true else f.* = .{};
- }
- for (&s.slots) |*sl| sl.* = .{};
- s.job = .{};
- }
-
- pub fn push(s: *Self, bytes: []const u8) usize {
- return s.protocol.push(bytes);
- }
-
- pub fn output(s: *const Self) []const u8 {
- return s.protocol.output();
- }
-
- pub fn wrote(s: *Self, n: usize) void {
- s.protocol.wrote(n);
- }
-
- fn hasRoom(s: *Self) bool {
- return s.protocol.hasRoom();
- }
-
- fn emit(s: *Self, tag: u16, msg: Msg) void {
- s.protocol.reply(tag, msg) catch {
- s.protocol.dead = true;
- };
- }
-
- fn fail(s: *Self, tag: u16, ename: []const u8) void {
- assert(ename.len <= errmax);
- s.emit(tag, .{ .rerror = .{ .ename = ename } });
- }
-
- fn tick(s: *Self) u64 {
- s.seq += 1;
- return s.seq;
- }
-
- fn findFid(s: *Self, fid: u32) ?usize {
- for (&s.fids, 0..) |*f, i| if (f.used and !f.orphan and f.fid == fid) return i;
- return null;
- }
-
- fn freeFid(s: *Self) ?usize {
- for (&s.fids, 0..) |*f, i| if (!f.used) return i;
- return null;
- }
-
- fn dropFid(s: *Self, fid: u32) void {
- if (s.findFid(fid)) |i| s.fids[i] = .{};
- }
-
- fn findSlot(s: *Self, req_tag: u64) ?usize {
- for (&s.slots, 0..) |*sl, i| if (sl.used and sl.req.tag == req_tag) return i;
- return null;
- }
-
- fn freeSlot(s: *Self) ?usize {
- for (&s.slots, 0..) |*sl, i| if (!sl.used) return i;
- return null;
- }
-
- fn findTag(s: *Self, tag: u16) ?usize {
- for (&s.slots, 0..) |*sl, i| if (sl.used and sl.tag == tag) return i;
- return null;
- }
-
- fn setUname(s: *Self, uname: []const u8) void {
- const n = @min(uname.len, username_capacity);
- @memcpy(s.uname[0..n], uname[0..n]);
- s.uname_len = @intCast(n);
- }
-
- pub fn retry(s: *Self) ?fs.Req {
- var best: ?usize = null;
- for (&s.slots, 0..) |*sl, i| {
- if (!sl.used or !sl.parked or sl.retried) continue;
- if (best == null or sl.seq < s.slots[best.?].seq) best = i;
- }
- const i = best orelse {
- for (&s.slots) |*sl| sl.retried = false;
- return null;
- };
- if (!s.hasRoom()) {
- for (&s.slots) |*sl| sl.retried = false;
- return null;
- }
- s.slots[i].retried = true;
- s.slots[i].parked = false;
- return s.slots[i].req;
- }
-
- pub fn next(s: *Self) ?fs.Req {
- while (true) {
- if (s.job.kind != .none) {
- if (s.job.req_tag != 0) return null;
- if (s.stepJob()) |req| return req;
- assert(s.job.kind == .none);
- continue;
- }
- if (s.orphan()) |req| return req;
- if (!s.hasRoom()) return null;
- if (!s.startFrame()) return null;
- }
- }
-
- pub fn reply(s: *Self, r: *const fs.Reply, bytes: []const u8) void {
- if (s.job.kind != .none and s.job.req_tag == r.tag) return s.jobReply(r, bytes);
- if (s.findSlot(r.tag)) |i| return s.slotReply(i, r, bytes);
- }
-
- fn orphan(s: *Self) ?fs.Req {
- for (&s.fids) |*f| {
- if (!f.used or !f.orphan) continue;
- assert(f.open);
- const req: fs.Req = .{
- .tag = s.tick(),
- .op = .release,
- .node = f.node,
- .handle = f.handle,
- };
- f.* = .{};
- return req;
- }
- return null;
- }
-
- fn startFrame(s: *Self) bool {
- assert(s.job.kind == .none);
- const got = (s.protocol.receive() catch return false) orelse return false;
- defer if (s.job.kind == .none) s.dropFrame();
- s.dispatch(got);
- return true;
- }
-
- fn dropFrame(s: *Self) void {
- s.protocol.release();
- }
-
- fn dispatch(s: *Self, got: Decoded) void {
- switch (got.msg) {
- .tversion => |m| s.version(got.tag, m.msize, m.version),
- .tauth => s.fail(got.tag, e_no_auth),
- .tcreate => s.fail(got.tag, e_perm),
- .tattach => |m| s.attach(got.tag, m.fid, m.uname, m.aname),
- .tflush => |m| s.flush(got.tag, m.oldtag),
- .twalk => |m| s.walk(got, m.fid, m.newfid, @intCast(m.nwname)),
- .topen => |m| s.open(got.tag, m.fid, m.mode),
- .tread => |m| s.read(got.tag, m.fid, m.offset, m.count),
- .twrite => |m| s.write(got, m.fid, m.offset, m.data.len),
- .tclunk => |m| s.clunk(got.tag, m.fid, .clunk),
- .tremove => |m| s.clunk(got.tag, m.fid, .remove),
- .tstat => |m| s.stat(got.tag, m.fid),
- .twstat => |m| s.wstat(got.tag, m.fid, m.stat),
- else => s.fail(got.tag, e_botch),
- }
- }
-
- fn version(s: *Self, tag: u16, want: u32, ver: []const u8) void {
- assert(tag == notag);
- s.reset();
- // 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 {
- if (aname.len != 0) return s.fail(tag, e_no_tree);
- if (fid == nofid) return s.fail(tag, e_unknown_fid);
- if (s.findFid(fid) != null) return s.fail(tag, e_fid_in_use);
- if (s.freeFid() == null) return s.fail(tag, e_too_many_fids);
- s.setUname(uname);
- s.job = .{ .kind = .attach, .tag = tag, .fid = fid, .node = s.root };
- }
-
- fn walk(s: *Self, got: Decoded, fid: u32, newfid: u32, nwname: u8) void {
- const tag = got.tag;
- const i = s.findFid(fid) orelse return s.fail(tag, e_unknown_fid);
- if (s.fids[i].open) return s.fail(tag, e_bad_use);
- if (newfid == nofid) return s.fail(tag, e_unknown_fid);
- if (newfid != fid) {
- if (s.findFid(newfid) != null) return s.fail(tag, e_fid_in_use);
- if (s.freeFid() == null) return s.fail(tag, e_too_many_fids);
- }
- if (nwname == 0) {
- if (newfid != fid) {
- const j = s.freeFid().?;
- s.fids[j] = s.fids[i];
- s.fids[j].fid = newfid;
- s.fids[j].diroff = 0;
- s.fids[j].dirindex = 0;
- }
- s.emit(tag, .{ .rwalk = .{ .nwqid = 0 } });
- return;
- }
- if (!s.fids[i].dir) return s.fail(tag, e_not_dir);
- s.job = .{
- .kind = .walk,
- .tag = tag,
- .fid = fid,
- .newfid = newfid,
- .nwname = nwname,
- .node = s.fids[i].node,
- .dir = s.fids[i].dir,
- .perm = s.fids[i].perm,
- .name = s.fids[i].name,
- .name_len = s.fids[i].name_len,
- .msg = got.msg,
- };
- }
-
- fn open(s: *Self, tag: u16, fid: u32, mode: u8) void {
- const i = s.findFid(fid) orelse return s.fail(tag, e_unknown_fid);
- const f = &s.fids[i];
- if (f.open) return s.fail(tag, e_already_open);
- if (mode & orclose != 0) return s.fail(tag, e_perm);
- const rw = mode & 3;
- if (rw == oexec) return s.fail(tag, e_perm);
- if (f.dir and (rw != oread or mode & otrunc != 0)) return s.fail(tag, e_perm);
- var need: u16 = 0;
- if (rw == oread or rw == ordwr) need |= 0o400;
- if (rw == owrite or rw == ordwr or mode & otrunc != 0) need |= 0o200;
- if (f.perm & need != need) return s.fail(tag, e_perm);
- s.job = .{ .kind = .open, .tag = tag, .fid = fid, .omode = mode };
- }
-
- fn read(s: *Self, tag: u16, fid: u32, offset: u64, count: u32) void {
- const i = s.findFid(fid) orelse return s.fail(tag, e_unknown_fid);
- const f = &s.fids[i];
- if (!f.open or (f.omode & 3) == owrite) return s.fail(tag, e_bad_use);
- const want = @min(count, s.protocol.msize - header_len - 4);
- if (!f.dir) {
- s.job = .{ .kind = .read, .tag = tag, .fid = fid, .offset = offset, .count = want };
- return;
- }
- if (offset != f.diroff) {
- if (offset != 0) return s.fail(tag, e_bad_offset);
- f.diroff = 0;
- f.dirindex = 0;
- }
- s.job = .{ .kind = .readdir, .tag = tag, .fid = fid, .offset = offset, .count = want };
- }
-
- fn write(s: *Self, got: Decoded, fid: u32, offset: u64, len: usize) void {
- const tag = got.tag;
- const i = s.findFid(fid) orelse return s.fail(tag, e_unknown_fid);
- const f = &s.fids[i];
- if (!f.open or (f.omode & 3) == oread) return s.fail(tag, e_bad_use);
- s.job = .{
- .kind = .write,
- .tag = tag,
- .fid = fid,
- .offset = offset,
- .count = @intCast(len),
- .msg = got.msg,
- };
- }
-
- fn clunk(s: *Self, tag: u16, fid: u32, kind: Kind) void {
- assert(kind == .clunk or kind == .remove);
- const i = s.findFid(fid) orelse return s.fail(tag, e_unknown_fid);
- if (s.fids[i].open) {
- s.job = .{ .kind = kind, .tag = tag, .fid = fid };
- return;
- }
- s.fids[i] = .{};
- if (kind == .remove) s.fail(tag, e_perm) else s.emit(tag, .rclunk);
- }
-
- fn stat(s: *Self, tag: u16, fid: u32) void {
- if (s.findFid(fid) == null) return s.fail(tag, e_unknown_fid);
- s.job = .{ .kind = .stat, .tag = tag, .fid = fid };
- }
-
- fn wstat(s: *Self, tag: u16, fid: u32, st: Stat) void {
- if (s.findFid(fid) == null) return s.fail(tag, e_unknown_fid);
- if (st.type != std.math.maxInt(u16) or st.dev != std.math.maxInt(u32) or
- st.qid.type != std.math.maxInt(u8) or st.qid.version != std.math.maxInt(u32) or
- st.qid.path != std.math.maxInt(u64) or st.mode != std.math.maxInt(u32) or
- st.atime != std.math.maxInt(u32) or
- // Linux v9fs follows O_TRUNC with Twstat(length=0, mtime=now).
- // Accept that timestamp hint with truncation; this control
- // filesystem does not persist caller-selected timestamps.
- (st.mtime != std.math.maxInt(u32) and st.length != 0) or
- st.name.len != 0 or st.uid.len != 0 or st.gid.len != 0 or st.muid.len != 0)
- return s.fail(tag, e_wstat);
- if (st.length == std.math.maxInt(u64)) {
- s.emit(tag, .rwstat);
- return;
- }
- if (st.length != 0) return s.fail(tag, e_trunc_only);
- s.job = .{ .kind = .wstat, .tag = tag, .fid = fid };
- }
-
- fn flush(s: *Self, tag: u16, oldtag: u16) void {
- if (s.findTag(oldtag)) |i| {
- s.fail(s.slots[i].tag, e_interrupted);
- s.slots[i] = .{};
- }
- s.emit(tag, .rflush);
- }
-
- fn ask(s: *Self, req: fs.Req) fs.Req {
- assert(req.tag != 0);
- s.job.req = req;
- s.job.req_tag = req.tag;
- return req;
- }
-
- fn jobFid(s: *Self) ?*Fid {
- const i = s.findFid(s.job.fid) orelse {
- s.fail(s.job.tag, e_unknown_fid);
- s.finishJob();
- return null;
- };
- return &s.fids[i];
- }
-
- fn stepJob(s: *Self) ?fs.Req {
- const j = &s.job;
- assert(j.kind != .none);
- assert(j.req_tag == 0);
- switch (j.kind) {
- .none => unreachable,
- .attach => return s.ask(.{ .tag = s.tick(), .op = .getattr, .node = s.root }),
- .walk => return s.stepWalk(),
- .open => {
- const f = s.jobFid() orelse return null;
- if (j.step == 0 and j.omode & otrunc != 0) return s.ask(.{
- .tag = s.tick(),
- .op = .setattr,
- .node = f.node,
- .truncate = true,
- });
- return s.ask(.{ .tag = s.tick(), .op = .open, .node = f.node });
- },
- .read => {
- const f = s.jobFid() orelse return null;
- return s.ask(.{
- .tag = s.tick(),
- .op = .read,
- .node = f.node,
- .handle = f.handle,
- .off = j.offset,
- .size = j.count,
- });
- },
- .readdir => {
- const f = s.jobFid() orelse return null;
- return s.ask(.{
- .tag = s.tick(),
- .op = .readdir,
- .node = f.node,
- .handle = f.handle,
- .off = f.dirindex,
- .size = j.count,
- });
- },
- .write => {
- const f = s.jobFid() orelse return null;
- return s.ask(.{
- .tag = s.tick(),
- .op = .write,
- .node = f.node,
- .handle = f.handle,
- .off = j.offset,
- .size = j.count,
- .data = j.msg.twrite.data,
- });
- },
- .clunk, .remove => {
- const f = s.jobFid() orelse return null;
- return s.ask(.{
- .tag = s.tick(),
- .op = .release,
- .node = f.node,
- .handle = f.handle,
- });
- },
- .stat => {
- const f = s.jobFid() orelse return null;
- return s.ask(.{ .tag = s.tick(), .op = .getattr, .node = f.node });
- },
- .wstat => {
- const f = s.jobFid() orelse return null;
- return s.ask(.{ .tag = s.tick(), .op = .setattr, .node = f.node, .truncate = true });
- },
- }
- }
-
- fn stepWalk(s: *Self) ?fs.Req {
- const j = &s.job;
- while (j.step < j.nwname) {
- const name = j.msg.twalk.wname[j.step];
- if (name.len > name_capacity) {
- s.stopWalk(e_illegal_name);
- return null;
- }
- if (std.mem.eql(u8, name, ".")) {
- j.wqid[j.nwqid] = qidOf(j.node, j.dir);
- j.nwqid += 1;
- j.step += 1;
- continue;
- }
- @memcpy(j.name[0..name.len], name);
- j.name_len = @intCast(name.len);
- return s.ask(.{ .tag = s.tick(), .op = .lookup, .node = j.node, .data = name });
- }
- const dst = pick: {
- if (j.newfid == j.fid) break :pick s.findFid(j.fid) orelse {
- s.fail(j.tag, e_unknown_fid);
- s.finishJob();
- return null;
- };
- break :pick s.freeFid() orelse {
- s.fail(j.tag, e_too_many_fids);
- s.finishJob();
- return null;
- };
- };
- s.fids[dst] = .{
- .used = true,
- .fid = j.newfid,
- .node = j.node,
- .dir = j.dir,
- .perm = j.perm,
- .name = j.name,
- .name_len = j.name_len,
- };
- s.emit(j.tag, .{ .rwalk = .{ .nwqid = j.nwqid, .wqid = j.wqid } });
- s.finishJob();
- return null;
- }
-
- fn stopWalk(s: *Self, ename: []const u8) void {
- const j = &s.job;
- if (j.nwqid == 0)
- s.fail(j.tag, ename)
- else
- s.emit(j.tag, .{ .rwalk = .{ .nwqid = j.nwqid, .wqid = j.wqid } });
- s.finishJob();
- }
-
- fn finishJob(s: *Self) void {
- s.job = .{};
- if (s.protocol.frame != 0) s.dropFrame();
- }
-
- fn jobReply(s: *Self, r: *const fs.Reply, bytes: []const u8) void {
- const j = &s.job;
- assert(j.kind != .none);
- assert(j.req_tag == r.tag);
- j.req_tag = 0;
-
- if (j.kind == .clunk or j.kind == .remove) {
- s.dropFid(j.fid);
- if (j.kind == .remove) s.fail(j.tag, e_perm) else s.emit(j.tag, .rclunk);
- s.finishJob();
- return;
- }
- if (r.status == .again) return s.parkJob();
- if (r.status == .err) {
- const ename = errString(r.errno);
- if (j.kind == .walk) return s.stopWalk(ename);
- s.fail(j.tag, ename);
- s.finishJob();
- return;
- }
-
- switch (j.kind) {
- .none, .clunk, .remove => unreachable,
- .attach => {
- const i = s.freeFid() orelse {
- s.fail(j.tag, e_too_many_fids);
- s.finishJob();
- return;
- };
- const node = if (r.attr.node != 0) r.attr.node else s.root;
- s.fids[i] = .{
- .used = true,
- .fid = j.fid,
- .node = node,
- .dir = r.attr.dir,
- .perm = r.attr.mode,
- };
- s.fids[i].name[0] = '/';
- s.fids[i].name_len = 1;
- s.emit(j.tag, .{ .rattach = .{ .qid = qidOf(node, r.attr.dir) } });
- s.finishJob();
- },
- .walk => {
- if (r.attr.node != 0) j.node = r.attr.node;
- if (comptime @hasField(@TypeOf(r.attr), "name")) {
- if (r.attr.name.len != 0) {
- j.name_len = @intCast(@min(r.attr.name.len, j.name.len));
- @memcpy(j.name[0..j.name_len], r.attr.name[0..j.name_len]);
- }
- }
- j.dir = r.attr.dir;
- j.perm = r.attr.mode;
- j.wqid[j.nwqid] = qidOf(j.node, j.dir);
- j.nwqid += 1;
- j.step += 1;
- },
- .open => {
- if (j.step == 0 and j.omode & otrunc != 0) {
- j.step = 1;
- return;
- }
- const f = s.jobFid() orelse return;
- if (r.attr.node != 0) f.node = r.attr.node;
- f.open = true;
- f.omode = j.omode;
- f.handle = r.handle;
- f.diroff = 0;
- f.dirindex = 0;
- s.emit(j.tag, .{ .ropen = .{
- .qid = qidOf(f.node, f.dir),
- .iounit = s.protocol.msize - iohdrsz,
- } });
- s.finishJob();
- },
- .read => {
- s.emit(j.tag, .{ .rread = .{ .data = bytes[0..@min(bytes.len, j.count)] } });
- s.finishJob();
- },
- .readdir => {
- s.emitDirRead(j.tag, j.fid, bytes, j.count);
- s.finishJob();
- },
- .write => {
- s.emit(j.tag, .{ .rwrite = .{ .count = @min(r.written, j.count) } });
- s.finishJob();
- },
- .stat => {
- const f = s.jobFid() orelse return;
- f.perm = r.attr.mode;
- f.dir = r.attr.dir;
- const response: Msg = .{ .rstat = .{ .stat = s.statOf(f, r.attr) } };
- if ((totalLen(response) catch unreachable) > s.protocol.msize)
- s.fail(j.tag, e_small_msize)
- else
- s.emit(j.tag, response);
- s.finishJob();
- },
- .wstat => {
- s.emit(j.tag, .rwstat);
- s.finishJob();
- },
- }
- }
- fn parkJob(s: *Self) void {
- const j = &s.job;
- switch (j.kind) {
- .read, .readdir, .write => {},
- else => {
- s.fail(j.tag, e_again);
- s.finishJob();
- return;
- },
- }
- const i = s.freeSlot() orelse {
- s.fail(j.tag, e_again);
- s.finishJob();
- return;
- };
- const sl = &s.slots[i];
- sl.* = .{
- .used = true,
- .parked = true,
- .seq = s.tick(),
- .tag = j.tag,
- .kind = j.kind,
- .fid = j.fid,
- .count = j.count,
- .req = j.req,
- };
- if (j.req.data.len != 0) {
- if (j.req.data.len > park_data_max) {
- sl.* = .{};
- s.fail(j.tag, e_again);
- s.finishJob();
- return;
- }
- @memcpy(sl.data[0..j.req.data.len], j.req.data);
- sl.copied = true;
- sl.req.data = sl.data[0..j.req.data.len];
- }
- s.finishJob();
- }
-
- fn slotReply(s: *Self, i: usize, r: *const fs.Reply, bytes: []const u8) void {
- const sl = &s.slots[i];
- if (r.status == .again) {
- sl.parked = true;
- return;
- }
- if (r.status == .err) {
- s.fail(sl.tag, errString(r.errno));
- sl.* = .{};
- return;
- }
- switch (sl.kind) {
- .read => s.emit(sl.tag, .{ .rread = .{ .data = bytes[0..@min(bytes.len, sl.count)] } }),
- .readdir => s.emitDirRead(sl.tag, sl.fid, bytes, sl.count),
- .write => s.emit(sl.tag, .{ .rwrite = .{ .count = @min(r.written, sl.count) } }),
- else => s.fail(sl.tag, e_botch),
- }
- sl.* = .{};
- }
-
- fn emitDirRead(s: *Self, tag: u16, fid: u32, staging: []const u8, count: u32) void {
- const buf = s.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];
- var n: usize = header_len + 4;
- var entries: u32 = 0;
- var i: usize = 0;
- while (i + 10 <= staging.len) {
- const nlen: usize = staging[i + 9];
- if (i + 10 + nlen > staging.len) break;
- const dir = staging[i + 8] != 0;
- const rec: Stat = .{
- .type = 0,
- .dev = 0,
- .qid = qidOf(std.mem.readInt(u64, staging[i..][0..8], .little), dir),
- .mode = (if (dir) dmdir else 0) | @as(u32, if (dir) dirent_dir_perm else dirent_file_perm),
- .atime = 0,
- .mtime = 0,
- .length = 0,
- .name = staging[i + 10 ..][0..nlen],
- .uid = who,
- .gid = who,
- .muid = who,
- };
- const size = @as(usize, rec.size() catch break) + 2;
- if (n - header_len - 4 + size > cap) break;
- _ = rec.encode(buf[n..]) catch break;
- n += size;
- entries += 1;
- i += 10 + nlen;
- }
- if (entries == 0 and staging.len != 0) return s.fail(tag, e_count_small);
- const payload: u32 = @intCast(n - header_len - 4);
- comptime assert(header_len == 7);
- 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;
- }
- }
-
- fn statOf(s: *const Self, f: *const Fid, a: fs.Reply.Attr) Stat {
- const who = s.uname[0..s.uname_len];
- return .{
- .type = 0,
- .dev = 0,
- .qid = qidOf(if (a.node != 0) a.node else f.node, a.dir),
- .mode = (if (a.dir) dmdir else 0) | @as(u32, a.mode),
- .atime = 0,
- .mtime = if (@hasField(@TypeOf(a), "mtime")) a.mtime else 0,
- .length = a.size,
- .name = f.name[0..f.name_len],
- .uid = who,
- .gid = who,
- .muid = who,
- };
- }
- };
-}
-
-const StubFs = struct {
- pub const Op = enum(u8) { lookup, getattr, setattr, open, read, write, release, readdir };
- pub const Status = enum(u8) { ok, again, err };
-
- pub const Req = struct {
- tag: u64,
- op: Op,
- node: u64,
- handle: u32 = 0,
- off: u64 = 0,
- size: u32 = 0,
- data: []const u8 = &.{},
- truncate: bool = false,
- };
-
- pub const Reply = struct {
- tag: u64,
- status: Status = .ok,
- errno: u16 = 0,
- attr: Attr = .{},
- handle: u32 = 0,
- written: u32 = 0,
-
- pub const Attr = struct {
- name: []const u8 = "",
- node: u64 = 0,
- dir: bool = false,
- size: u64 = 0,
- mode: u16 = 0o600,
- };
- };
-
- const Entry = struct { node: u64, parent: u64, name: []const u8, dir: bool, mode: u16 };
-
- const tree = [_]Entry{
- .{ .node = 1, .parent = 1, .name = "/", .dir = true, .mode = 0o500 },
- .{ .node = 2, .parent = 1, .name = "index", .dir = false, .mode = 0o400 },
- .{ .node = 3, .parent = 1, .name = "cons", .dir = false, .mode = 0o200 },
- .{ .node = 4, .parent = 1, .name = "new", .dir = true, .mode = 0o500 },
- .{ .node = 16, .parent = 1, .name = "1", .dir = true, .mode = 0o500 },
- .{ .node = 32, .parent = 1, .name = "2", .dir = true, .mode = 0o500 },
- .{ .node = 17, .parent = 16, .name = "addr", .dir = false, .mode = 0o600 },
- .{ .node = 18, .parent = 16, .name = "body", .dir = false, .mode = 0o600 },
- .{ .node = 19, .parent = 16, .name = "ctl", .dir = false, .mode = 0o600 },
- .{ .node = 21, .parent = 16, .name = "errors", .dir = false, .mode = 0o200 },
- .{ .node = 22, .parent = 16, .name = "event", .dir = false, .mode = 0o600 },
- .{ .node = 23, .parent = 16, .name = "tag", .dir = false, .mode = 0o600 },
- };
-
- const body_node = 18;
- const index_node = 2;
- const event_node = 22;
-
- body: []const u8 = "hello, body\n",
- filler: [1024]u8 = @splat('x'),
- event: ?[]const u8 = null,
- park_writes: bool = false,
- releases: u32 = 0,
- calls: u32 = 0,
- writes: [128]u8 = undefined,
- writes_len: usize = 0,
- stage: [1024]u8 = undefined,
-
- const Answer = struct { reply: Reply, bytes: []const u8 = "" };
-
- fn find(node: u64) ?usize {
- for (tree, 0..) |e, i| if (e.node == node) return i;
- return null;
- }
-
- fn sizeOf(st: *const StubFs, node: u64) u64 {
- return switch (node) {
- body_node => st.body.len,
- index_node => st.filler.len,
- else => 0,
- };
- }
-
- fn contentOf(st: *const StubFs, node: u64) []const u8 {
- return switch (node) {
- body_node => st.body,
- index_node => &st.filler,
- else => "",
- };
- }
-
- fn attrOf(st: *const StubFs, e: Entry) Reply.Attr {
- return .{ .name = e.name, .node = e.node, .dir = e.dir, .mode = e.mode, .size = st.sizeOf(e.node) };
- }
-
- fn stageDir(st: *StubFs, node: u64, skip: u64) []const u8 {
- var n: usize = 0;
- var seen: u64 = 0;
- for (tree) |e| {
- if (e.parent != node or e.node == node) continue;
- if (seen < skip) {
- seen += 1;
- continue;
- }
- std.mem.writeInt(u64, st.stage[n..][0..8], e.node, .little);
- st.stage[n + 8] = @intFromBool(e.dir);
- st.stage[n + 9] = @intCast(e.name.len);
- @memcpy(st.stage[n + 10 ..][0..e.name.len], e.name);
- n += 10 + e.name.len;
- }
- return st.stage[0..n];
- }
-
- fn handle(st: *StubFs, req: Req) Answer {
- st.calls += 1;
- const fail: Answer = .{ .reply = .{ .tag = req.tag, .status = .err, .errno = 2 } };
- const i = find(req.node) orelse return fail;
- switch (req.op) {
- .lookup => {
- if (std.mem.eql(u8, req.data, ".."))
- return .{ .reply = .{ .tag = req.tag, .attr = st.attrOf(tree[find(tree[i].parent).?]) } };
- for (tree) |e| {
- if (e.parent != req.node or e.node == req.node) continue;
- if (!std.mem.eql(u8, e.name, req.data)) continue;
- return .{ .reply = .{ .tag = req.tag, .attr = st.attrOf(e) } };
- }
- return fail;
- },
- .getattr => return .{ .reply = .{ .tag = req.tag, .attr = st.attrOf(tree[i]) } },
- .setattr => {
- if (req.truncate and req.node == body_node) st.body = "";
- return .{ .reply = .{ .tag = req.tag, .attr = st.attrOf(tree[i]) } };
- },
- .open => return .{ .reply = .{ .tag = req.tag, .handle = 7 } },
- .release => {
- st.releases += 1;
- return .{ .reply = .{ .tag = req.tag } };
- },
- .readdir => {
- if (!tree[i].dir) return .{ .reply = .{ .tag = req.tag, .status = .err, .errno = 20 } };
- return .{ .reply = .{ .tag = req.tag }, .bytes = st.stageDir(req.node, req.off) };
- },
- .read => {
- if (req.node == event_node) {
- const rec = st.event orelse return .{ .reply = .{ .tag = req.tag, .status = .again } };
- st.event = null;
- return .{ .reply = .{ .tag = req.tag }, .bytes = rec };
- }
- const all = st.contentOf(req.node);
- if (req.off >= all.len) return .{ .reply = .{ .tag = req.tag } };
- const from = all[@intCast(req.off)..];
- return .{ .reply = .{ .tag = req.tag }, .bytes = from[0..@min(from.len, req.size)] };
- },
- .write => {
- if (st.park_writes) return .{ .reply = .{ .tag = req.tag, .status = .again } };
- const n = @min(req.data.len, st.writes.len - st.writes_len);
- @memcpy(st.writes[st.writes_len..][0..n], req.data[0..n]);
- st.writes_len += n;
- return .{ .reply = .{ .tag = req.tag, .written = @intCast(n) } };
- },
- }
- }
-};
-
-const Srv = Server(StubFs, max_fids);
-
-const Harness = struct {
- in: [4096]u8 = undefined,
- out: [8192]u8 = undefined,
- fsys: StubFs = .{},
- srv: Srv = undefined,
-
- fn start(h: *Harness) void {
- h.srv = Srv.init(.{ .in = &h.in, .out = &h.out, .root = 1 });
- }
-
- fn answer(h: *Harness, req: StubFs.Req) void {
- const a = h.fsys.handle(req);
- h.srv.reply(&a.reply, a.bytes);
- }
-
- fn pump(h: *Harness) void {
- while (h.srv.retry()) |req| h.answer(req);
- while (h.srv.next()) |req| h.answer(req);
- }
-
- fn send(h: *Harness, tag: u16, msg: Msg) !void {
- var buf: [1024]u8 = undefined;
- const bytes = try encode(msg, tag, &buf);
- try testing.expectEqual(bytes.len, h.srv.push(bytes));
- h.pump();
- }
-
- fn reap(h: *Harness) !Decoded {
- const out = h.srv.output();
- const len = frameLen(out) orelse return error.NoReply;
- if (len > out.len) return error.ShortReply;
- const got = try decode(out[0..len]);
- h.srv.wrote(len);
- return got;
- }
-
- fn quiet(h: *Harness) !void {
- try testing.expectEqual(@as(usize, 0), h.srv.output().len);
- }
-
- fn handshake(h: *Harness, msize: u32) !void {
- h.start();
- try h.send(notag, .{ .tversion = .{ .msize = msize, .version = "9P2000" } });
- const v = try h.reap();
- try testing.expectEqualStrings("9P2000", v.msg.rversion.version);
- try h.send(0, .{ .tattach = .{ .fid = 0, .afid = nofid, .uname = "goblin", .aname = "" } });
- const a = try h.reap();
- try testing.expectEqual(@as(u64, 1), a.msg.rattach.qid.path);
- }
+const std = @import("std");
+const testing = std.testing;
- fn walkTo(h: *Harness, tag: u16, newfid: u32, list: []const []const u8) !Decoded {
- try h.send(tag, .{ .twalk = .{
- .fid = 0,
- .newfid = newfid,
- .nwname = @intCast(list.len),
- .wname = wnames(list),
- } });
- return h.reap();
- }
+const Stub = struct {
+ pub const Req = cloud9.fs.Req;
+ pub const Reply = cloud9.fs.Reply;
};
-fn wnames(list: []const []const u8) [max_welem][]const u8 {
- var out: [max_welem][]const u8 = @splat("");
- for (list, 0..) |n, i| out[i] = n;
- return out;
-}
-
-fn dirNames(data: []const u8, out: [][]const u8) !usize {
- var n: usize = 0;
- var i: usize = 0;
- while (i < data.len) {
- const size = std.mem.readInt(u16, data[i..][0..2], .little);
- const st = try Stat.decode(data[i..][0 .. @as(usize, size) + 2]);
- out[n] = st.name;
- n += 1;
- i += @as(usize, size) + 2;
- }
- return n;
-}
-
-test "9p server: the version handshake clamps, falls back, and refuses" {
- var h: Harness = .{};
- h.start();
-
- try h.send(notag, .{ .tversion = .{ .msize = 1 << 20, .version = "9P2000" } });
- var got = try h.reap();
- try testing.expectEqual(notag, got.tag);
- try testing.expectEqual(@as(u32, 4096), got.msg.rversion.msize);
- try testing.expectEqualStrings("9P2000", got.msg.rversion.version);
-
- try h.send(notag, .{ .tversion = .{ .msize = 512, .version = "9P2000" } });
- got = try h.reap();
- try testing.expectEqual(@as(u32, 512), got.msg.rversion.msize);
-
- try h.send(notag, .{ .tversion = .{ .msize = 4096, .version = "9P2000.u" } });
- got = try h.reap();
- try testing.expectEqualStrings("9P2000", got.msg.rversion.version);
-
- try h.send(notag, .{ .tversion = .{ .msize = 4096, .version = "TCP/IP" } });
- got = try h.reap();
- try testing.expectEqualStrings("unknown", got.msg.rversion.version);
- try h.send(1, .{ .tattach = .{ .fid = 0, .afid = nofid, .uname = "goblin", .aname = "" } });
- try testing.expect(h.srv.protocol.dead);
- h.start();
- try h.send(notag, .{ .tversion = .{ .msize = 64, .version = "9P2000" } });
- try testing.expect(h.srv.protocol.dead);
- try testing.expectEqual(@as(u32, 216), msize_min - 1);
-}
-
-test "9p server: attach names the root, and the only tree there is" {
- var h: Harness = .{};
- h.start();
- try h.send(notag, .{ .tversion = .{ .msize = 4096, .version = "9P2000" } });
- _ = try h.reap();
-
- try h.send(1, .{ .tattach = .{ .fid = 0, .afid = nofid, .uname = "goblin", .aname = "work" } });
- var got = try h.reap();
- try testing.expectEqualStrings(e_no_tree, got.msg.rerror.ename);
-
- try h.send(2, .{ .tattach = .{ .fid = 0, .afid = nofid, .uname = "goblin", .aname = "" } });
- got = try h.reap();
- try testing.expectEqual(@as(u64, 1), got.msg.rattach.qid.path);
- try testing.expectEqual(qtdir, got.msg.rattach.qid.type);
- try testing.expectEqual(@as(u32, 0), got.msg.rattach.qid.version);
-
- try h.send(3, .{ .tattach = .{ .fid = 0, .afid = nofid, .uname = "goblin", .aname = "" } });
- got = try h.reap();
- try testing.expectEqualStrings(e_fid_in_use, got.msg.rerror.ename);
-
- try h.send(4, .{ .tauth = .{ .afid = 1, .uname = "goblin", .aname = "" } });
- got = try h.reap();
- try testing.expectEqualStrings(e_no_auth, got.msg.rerror.ename);
-
- try h.send(5, .{ .tstat = .{ .fid = 0 } });
- got = try h.reap();
- try testing.expectEqualStrings("/", got.msg.rstat.stat.name);
- try testing.expectEqualStrings("goblin", got.msg.rstat.stat.uid);
- try testing.expectEqualStrings("goblin", got.msg.rstat.stat.muid);
- try testing.expect(got.msg.rstat.stat.mode & dmdir != 0);
-}
-
-test "9p server: a three-element walk, and `..` with no kernel to resolve it" {
- var h: Harness = .{};
- try h.handshake(4096);
-
- var got = try h.walkTo(5, 1, &.{ "..", "1", "body" });
- try testing.expectEqual(@as(u16, 3), got.msg.rwalk.nwqid);
- try testing.expectEqual(@as(u64, 1), got.msg.rwalk.wqid[0].path);
- try testing.expectEqual(@as(u64, 16), got.msg.rwalk.wqid[1].path);
- try testing.expectEqual(@as(u64, 18), got.msg.rwalk.wqid[2].path);
- try testing.expectEqual(qtdir, got.msg.rwalk.wqid[1].type);
- try testing.expectEqual(qtfile, got.msg.rwalk.wqid[2].type);
- for (got.msg.rwalk.wqid[0..3]) |q| try testing.expectEqual(@as(u32, 0), q.version);
-
- got = try h.walkTo(6, 2, &.{ "..", "1", "..", "1", "body" });
- try testing.expectEqual(@as(u16, 5), got.msg.rwalk.nwqid);
- try testing.expectEqual(@as(u64, 1), got.msg.rwalk.wqid[2].path);
- try testing.expectEqual(@as(u64, 18), got.msg.rwalk.wqid[4].path);
- try h.send(7, .{ .tstat = .{ .fid = 2 } });
- got = try h.reap();
- try testing.expectEqualStrings("body", got.msg.rstat.stat.name);
- try testing.expectEqual(@as(u64, 12), got.msg.rstat.stat.length);
-
- got = try h.walkTo(8, 3, &.{ "1", "body", ".." });
- try testing.expectEqual(@as(u16, 3), got.msg.rwalk.nwqid);
- try testing.expectEqual(@as(u64, 16), got.msg.rwalk.wqid[2].path);
- try testing.expectEqual(qtdir, got.msg.rwalk.wqid[2].type);
- try h.send(9, .{ .tstat = .{ .fid = 3 } });
- got = try h.reap();
- try testing.expectEqualStrings("1", got.msg.rstat.stat.name);
-
- const before = h.fsys.calls;
- got = try h.walkTo(10, 4, &.{ ".", "." });
- try testing.expectEqual(@as(u16, 2), got.msg.rwalk.nwqid);
- try testing.expectEqual(@as(u64, 1), got.msg.rwalk.wqid[1].path);
- try testing.expectEqual(before, h.fsys.calls);
-}
-
-test "9p server: a walk failing on the first element is Rerror, on the second a short Rwalk" {
- var h: Harness = .{};
- try h.handshake(4096);
-
- var got = try h.walkTo(5, 1, &.{ "nope", "body" });
- try testing.expectEqualStrings("No such file or directory", got.msg.rerror.ename);
-
- got = try h.walkTo(6, 1, &.{ "1", "nope" });
- try testing.expectEqual(@as(u16, 1), got.msg.rwalk.nwqid);
- try testing.expectEqual(@as(u64, 16), got.msg.rwalk.wqid[0].path);
-
- try h.send(7, .{ .tstat = .{ .fid = 1 } });
- got = try h.reap();
- try testing.expectEqualStrings(e_unknown_fid, got.msg.rerror.ename);
-
- try h.send(8, .{ .twalk = .{ .fid = 0, .newfid = 1, .nwname = 0 } });
- got = try h.reap();
- try testing.expectEqual(@as(u16, 0), got.msg.rwalk.nwqid);
- try h.send(9, .{ .tstat = .{ .fid = 1 } });
- got = try h.reap();
- try testing.expectEqualStrings("/", got.msg.rstat.stat.name);
-
- got = try h.walkTo(10, 2, &.{"index"});
- try testing.expectEqual(@as(u16, 1), got.msg.rwalk.nwqid);
- try h.send(11, .{ .twalk = .{ .fid = 2, .newfid = 3, .nwname = 1, .wname = wnames(&.{"body"}) } });
- got = try h.reap();
- try testing.expectEqualStrings(e_not_dir, got.msg.rerror.ename);
-
- got = try h.walkTo(12, 4, &.{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"});
- try testing.expectEqualStrings(e_illegal_name, got.msg.rerror.ename);
-
- try h.send(13, .{ .twalk = .{ .fid = 0, .newfid = 1, .nwname = 1, .wname = wnames(&.{"1"}) } });
- got = try h.reap();
- try testing.expectEqualStrings(e_fid_in_use, got.msg.rerror.ename);
-}
-
-test "9p server: backend-sized filenames survive walk and stat within negotiated msize" {
- const NativeFs = struct {
- pub const Req = StubFs.Req;
- pub const Reply = StubFs.Reply;
- pub const name_capacity: usize = 255;
- };
- const Native = Server(NativeFs, 2);
- try testing.expectEqual(@as(usize, 28), @sizeOf(@FieldType(Srv.Fid, "name")));
- try testing.expectEqual(@as(usize, 255), @sizeOf(@FieldType(Native.Fid, "name")));
-
- const filename: [255]u8 = @splat('f');
- for ([_]struct { length: usize, msize: u32 }{
- .{ .length = 29, .msize = 512 },
- .{ .length = 128, .msize = 512 },
- .{ .length = 255, .msize = 512 },
- .{ .length = 200, .msize = 256 },
- }) |case| {
- var in: [1024]u8 = undefined;
- var out: [2048]u8 = undefined;
- var encoded: [1024]u8 = undefined;
- var server = Native.init(.{ .in = &in, .out = &out, .root = 1 });
- server.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];
- const walk = try encode(.{ .twalk = .{
- .fid = 0,
- .newfid = 1,
- .nwname = 1,
- .wname = wnames(&.{name}),
- } }, 1, &encoded);
- try testing.expectEqual(walk.len, server.push(walk));
- const lookup = server.next() orelse return error.MissingLookup;
- try testing.expectEqual(.lookup, lookup.op);
- try testing.expectEqualStrings(name, lookup.data);
- server.reply(&.{ .tag = lookup.tag, .attr = .{ .node = 2, .name = name, .size = 12 } }, "");
- try testing.expectEqual(null, server.next());
- var response = try decode(server.output());
- try testing.expectEqual(@as(u16, 1), response.msg.rwalk.nwqid);
- server.wrote(server.output().len);
-
- const stat = try encode(.{ .tstat = .{ .fid = 1 } }, 2, &encoded);
- try testing.expectEqual(stat.len, server.push(stat));
- const getattr = server.next() orelse return error.MissingGetattr;
- try testing.expectEqual(.getattr, getattr.op);
- server.reply(&.{ .tag = getattr.tag, .attr = .{ .node = 2, .name = name, .size = 12 } }, "");
- try testing.expect(server.output().len <= case.msize);
- response = try decode(server.output());
- if (case.msize == 256) {
- try testing.expectEqualStrings(e_small_msize, response.msg.rerror.ename);
- } else {
- try testing.expectEqualStrings(name, response.msg.rstat.stat.name);
- try testing.expectEqual(@as(u64, 12), response.msg.rstat.stat.length);
- }
- server.wrote(server.output().len);
- const clunk = try encode(.{ .tclunk = .{ .fid = 1 } }, 3, &encoded);
- try testing.expectEqual(clunk.len, server.push(clunk));
- try testing.expectEqual(null, server.next());
- response = try decode(server.output());
- try testing.expect(response.msg == .rclunk);
- try testing.expect(!server.protocol.dead);
- }
-}
-
-test "9p server: open then read then clunk, and the release a clunk owes the core" {
- var h: Harness = .{};
- try h.handshake(4096);
- _ = try h.walkTo(5, 1, &.{ "1", "body" });
-
- try h.send(6, .{ .topen = .{ .fid = 1, .mode = oread } });
- var got = try h.reap();
- try testing.expectEqual(@as(u64, 18), got.msg.ropen.qid.path);
- try testing.expectEqual(@as(u32, 4096 - iohdrsz), got.msg.ropen.iounit);
-
- try h.send(7, .{ .topen = .{ .fid = 1, .mode = oread } });
- got = try h.reap();
- try testing.expectEqualStrings(e_already_open, got.msg.rerror.ename);
-
- try h.send(8, .{ .tread = .{ .fid = 1, .offset = 0, .count = 4096 } });
- got = try h.reap();
- try testing.expectEqualStrings("hello, body\n", got.msg.rread.data);
-
- try h.send(9, .{ .tread = .{ .fid = 1, .offset = 7, .count = 4096 } });
- got = try h.reap();
- try testing.expectEqualStrings("body\n", got.msg.rread.data);
-
- try h.send(10, .{ .tread = .{ .fid = 1, .offset = 99, .count = 16 } });
- got = try h.reap();
- try testing.expectEqual(@as(usize, 0), got.msg.rread.data.len);
-
- try testing.expectEqual(@as(u32, 0), h.fsys.releases);
- try h.send(11, .{ .tclunk = .{ .fid = 1 } });
- got = try h.reap();
- try testing.expect(got.msg == .rclunk);
- try testing.expectEqual(@as(u32, 1), h.fsys.releases);
-
- try h.send(12, .{ .tread = .{ .fid = 1, .offset = 0, .count = 16 } });
- got = try h.reap();
- try testing.expectEqualStrings(e_unknown_fid, got.msg.rerror.ename);
- try h.send(13, .{ .tclunk = .{ .fid = 1 } });
- got = try h.reap();
- try testing.expectEqualStrings(e_unknown_fid, got.msg.rerror.ename);
-
- _ = try h.walkTo(14, 2, &.{"index"});
- try h.send(15, .{ .tclunk = .{ .fid = 2 } });
- got = try h.reap();
- try testing.expect(got.msg == .rclunk);
- try testing.expectEqual(@as(u32, 1), h.fsys.releases);
-}
-
-test "9p server: every Rread is clamped to the client's count and to the msize" {
- var h: Harness = .{};
- try h.handshake(512);
- _ = try h.walkTo(5, 1, &.{"index"});
- try h.send(6, .{ .topen = .{ .fid = 1, .mode = oread } });
- _ = try h.reap();
-
- try h.send(7, .{ .tread = .{ .fid = 1, .offset = 0, .count = 5 } });
- var got = try h.reap();
- try testing.expectEqual(@as(usize, 5), got.msg.rread.data.len);
-
- try h.send(8, .{ .tread = .{ .fid = 1, .offset = 0, .count = 1 << 20 } });
- const out = h.srv.output();
- try testing.expectEqual(@as(?u32, 512), frameLen(out));
- got = try h.reap();
- try testing.expectEqual(@as(usize, 512 - header_len - 4), got.msg.rread.data.len);
-
- try h.send(9, .{ .tread = .{ .fid = 1, .offset = 0, .count = 1 << 20 } });
- got = try h.reap();
- try testing.expectEqual(@as(usize, 501), got.msg.rread.data.len);
-}
-
-test "9p server: a directory read is whole stat records at a cursor the client cannot invent" {
- var h: Harness = .{};
- try h.handshake(4096);
- _ = try h.walkTo(4, 1, &.{"index"});
- try h.send(5, .{ .topen = .{ .fid = 0, .mode = oread } });
- _ = try h.reap();
-
- var found: [8][]const u8 = undefined;
-
- try h.send(6, .{ .tread = .{ .fid = 0, .offset = 0, .count = 150 } });
- var got = try h.reap();
- const first = got.msg.rread.data.len;
- try testing.expectEqual(@as(usize, 2), try dirNames(got.msg.rread.data, &found));
- try testing.expectEqualStrings("index", found[0]);
- try testing.expectEqualStrings("cons", found[1]);
- try testing.expect(first <= 150);
-
- try h.send(7, .{ .tread = .{ .fid = 0, .offset = first, .count = 150 } });
- got = try h.reap();
- const second = got.msg.rread.data.len;
- try testing.expectEqual(@as(usize, 2), try dirNames(got.msg.rread.data, &found));
- try testing.expectEqualStrings("new", found[0]);
- try testing.expectEqualStrings("1", found[1]);
-
- try h.send(8, .{ .tread = .{ .fid = 0, .offset = first + second + 1, .count = 150 } });
- got = try h.reap();
- try testing.expectEqualStrings(e_bad_offset, got.msg.rerror.ename);
- try h.send(9, .{ .tread = .{ .fid = 0, .offset = 3, .count = 150 } });
- got = try h.reap();
- try testing.expectEqualStrings(e_bad_offset, got.msg.rerror.ename);
-
- try h.send(10, .{ .tread = .{ .fid = 0, .offset = first + second, .count = 150 } });
- got = try h.reap();
- try testing.expectEqual(@as(usize, 1), try dirNames(got.msg.rread.data, &found));
- try testing.expectEqualStrings("2", found[0]);
-
- try h.send(11, .{ .tread = .{ .fid = 0, .offset = first + second + got.msg.rread.data.len, .count = 150 } });
- got = try h.reap();
- try testing.expectEqual(@as(usize, 0), got.msg.rread.data.len);
-
- try h.send(12, .{ .tread = .{ .fid = 0, .offset = 0, .count = 150 } });
- got = try h.reap();
- try testing.expectEqual(@as(usize, 2), try dirNames(got.msg.rread.data, &found));
- try testing.expectEqualStrings("index", found[0]);
-
- try h.send(13, .{ .tread = .{ .fid = 0, .offset = 0, .count = 40 } });
- got = try h.reap();
- try testing.expectEqualStrings(e_count_small, got.msg.rerror.ename);
-
- try h.send(14, .{ .tread = .{ .fid = 0, .offset = 0, .count = 150 } });
- got = try h.reap();
- const one = try Stat.decode(got.msg.rread.data[0 .. std.mem.readInt(u16, got.msg.rread.data[0..2], .little) + 2]);
- try testing.expectEqualStrings("index", one.name);
- try testing.expectEqual(@as(u32, dirent_file_perm), one.mode);
- try testing.expectEqual(@as(u64, 0), one.length);
- try testing.expectEqual(@as(u32, 0), one.qid.version);
-
- try h.send(16, .{ .tstat = .{ .fid = 1 } });
- got = try h.reap();
- try testing.expectEqual(@as(u32, 0o400), got.msg.rstat.stat.mode);
- try testing.expectEqual(@as(u64, 1024), got.msg.rstat.stat.length);
-}
-
-test "9p server: long directory names remain whole across pages" {
- var h: Harness = .{};
- try h.handshake(4096);
- var entries: [10 + 255 + 10 + 4]u8 = @splat(0);
- std.mem.writeInt(u64, entries[0..8], 41, .little);
- entries[9] = 255;
- @memset(entries[10..265], 'f');
- std.mem.writeInt(u64, entries[265..273], 42, .little);
- entries[274] = 4;
- @memcpy(entries[275..], "next");
- var found: [2][]const u8 = undefined;
-
- {
- 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;
- try testing.expectEqual(@as(usize, 1), try dirNames(got.msg.rread.data, &found));
- try testing.expectEqualStrings(entries[10..265], found[0]);
- try testing.expectEqual(@as(u64, 1), h.srv.fids[0].dirindex);
-
- {
- 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));
- try testing.expectEqualStrings("next", found[0]);
- try testing.expectEqual(@as(u64, 2), h.srv.fids[0].dirindex);
- try testing.expectEqual(first + got.msg.rread.data.len, h.srv.fids[0].diroff);
-
- {
- 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));
- try testing.expectEqualStrings(entries[10..265], found[0]);
- try testing.expectEqualStrings("next", found[1]);
-}
-
-test "9p server: a blocked read parks, and the connection keeps working" {
- var h: Harness = .{};
- try h.handshake(4096);
- _ = try h.walkTo(5, 1, &.{ "1", "event" });
- try h.send(6, .{ .topen = .{ .fid = 1, .mode = oread } });
- _ = try h.reap();
-
- try h.send(7, .{ .tread = .{ .fid = 1, .offset = 0, .count = 4096 } });
- try h.quiet();
-
- h.pump();
- h.pump();
- try h.quiet();
-
- _ = try h.walkTo(8, 2, &.{ "1", "body" });
- try h.send(9, .{ .tstat = .{ .fid = 2 } });
- var got = try h.reap();
- try testing.expectEqualStrings("body", got.msg.rstat.stat.name);
-
- h.fsys.event = "Kli7 7 0 0 hello\n";
- h.pump();
- got = try h.reap();
- try testing.expectEqual(@as(u16, 7), got.tag);
- try testing.expectEqualStrings("Kli7 7 0 0 hello\n", got.msg.rread.data);
- try h.quiet();
-
- _ = try h.walkTo(10, 3, &.{ "1", "ctl" });
- try h.send(11, .{ .topen = .{ .fid = 3, .mode = owrite } });
- _ = try h.reap();
- h.fsys.park_writes = true;
- try h.send(12, .{ .twrite = .{ .fid = 3, .offset = 0, .data = "clean\n" } });
- try h.quiet();
- try h.send(13, .{ .tstat = .{ .fid = 2 } });
- _ = try h.reap();
- h.fsys.park_writes = false;
- h.pump();
- got = try h.reap();
- try testing.expectEqual(@as(u16, 12), got.tag);
- try testing.expectEqual(@as(u32, 6), got.msg.rwrite.count);
- try testing.expectEqualStrings("clean\n", h.fsys.writes[0..h.fsys.writes_len]);
- for (0..max_slots) |k| {
- try h.send(@intCast(100 + k), .{ .tread = .{ .fid = 1, .offset = 0, .count = 4096 } });
- try h.quiet();
- }
- try h.send(200, .{ .tread = .{ .fid = 1, .offset = 0, .count = 4096 } });
- got = try h.reap();
- try testing.expectEqualStrings(e_again, got.msg.rerror.ename);
-}
-
-test "9p server: the reply queue is a FIFO that survives a partial write" {
- var h: Harness = .{};
- try h.handshake(4096);
-
- try h.send(5, .{ .tstat = .{ .fid = 0 } });
- var saved: [256]u8 = undefined;
- const one = h.srv.output();
- const n = one.len;
- @memcpy(saved[0..n], one);
- h.srv.wrote(3);
- try testing.expectEqual(n - 3, h.srv.output().len);
-
- try h.send(6, .{ .tstat = .{ .fid = 0 } });
- const rest = h.srv.output();
- try testing.expectEqualSlices(u8, saved[3..n], rest[0 .. n - 3]);
- const tail = rest[n - 3 ..];
- const second = try decode(tail[0..frameLen(tail).?]);
- try testing.expectEqual(@as(u16, 6), second.tag);
-
- var flood: [8192]u8 = @splat(0);
- try testing.expectEqual(@as(usize, 4096), h.srv.push(&flood));
- h.pump();
- try testing.expect(h.srv.protocol.dead);
- try testing.expectEqual(@as(usize, 0), h.srv.push(&flood));
-}
-
-test "9p server: Tflush answers the original first and the Rflush second" {
- var h: Harness = .{};
- try h.handshake(4096);
- _ = try h.walkTo(5, 1, &.{ "1", "event" });
- try h.send(6, .{ .topen = .{ .fid = 1, .mode = oread } });
- _ = try h.reap();
- try h.send(7, .{ .tread = .{ .fid = 1, .offset = 0, .count = 4096 } });
- try h.quiet();
-
- try h.send(8, .{ .tflush = .{ .oldtag = 7 } });
- var got = try h.reap();
- try testing.expectEqual(@as(u16, 7), got.tag);
- try testing.expectEqualStrings(e_interrupted, got.msg.rerror.ename);
- got = try h.reap();
- try testing.expectEqual(@as(u16, 8), got.tag);
- try testing.expect(got.msg == .rflush);
- try h.quiet();
-
- h.fsys.event = "Kli7 7 0 0 hello\n";
- h.pump();
- try h.quiet();
-
- try h.send(9, .{ .tflush = .{ .oldtag = 99 } });
- got = try h.reap();
- try testing.expectEqual(@as(u16, 9), got.tag);
- try testing.expect(got.msg == .rflush);
- try h.quiet();
-}
-
-test "9p server: the fid and permission refusals, each in a string Linux knows" {
- var h: Harness = .{};
- try h.handshake(4096);
-
- for ([_]Msg{
- .{ .tread = .{ .fid = 99, .offset = 0, .count = 16 } },
- .{ .tstat = .{ .fid = 99 } },
- .{ .tclunk = .{ .fid = 99 } },
- .{ .topen = .{ .fid = 99, .mode = oread } },
- .{ .twalk = .{ .fid = 99, .newfid = 98, .nwname = 0 } },
- }, 20..) |msg, tag| {
- try h.send(@intCast(tag), msg);
- const got = try h.reap();
- try testing.expectEqualStrings(e_unknown_fid, got.msg.rerror.ename);
- }
-
- _ = try h.walkTo(5, 1, &.{ "1", "body" });
- try h.send(6, .{ .tread = .{ .fid = 1, .offset = 0, .count = 16 } });
- var got = try h.reap();
- try testing.expectEqualStrings(e_bad_use, got.msg.rerror.ename);
- try h.send(7, .{ .topen = .{ .fid = 1, .mode = oread } });
- _ = try h.reap();
- try h.send(8, .{ .twrite = .{ .fid = 1, .offset = 0, .data = "x" } });
- got = try h.reap();
- try testing.expectEqualStrings(e_bad_use, got.msg.rerror.ename);
-
- try h.send(9, .{ .twalk = .{ .fid = 1, .newfid = 2, .nwname = 0 } });
- got = try h.reap();
- try testing.expectEqualStrings(e_bad_use, got.msg.rerror.ename);
-
- _ = try h.walkTo(10, 3, &.{ "1", "errors" });
- try h.send(11, .{ .topen = .{ .fid = 3, .mode = oread } });
- got = try h.reap();
- try testing.expectEqualStrings(e_perm, got.msg.rerror.ename);
- try h.send(12, .{ .topen = .{ .fid = 3, .mode = owrite } });
- got = try h.reap();
- try testing.expect(got.msg == .ropen);
-
- try h.send(13, .{ .topen = .{ .fid = 0, .mode = ordwr } });
- got = try h.reap();
- try testing.expectEqualStrings(e_perm, got.msg.rerror.ename);
- _ = try h.walkTo(14, 4, &.{ "1", "tag" });
- for ([_]u8{ orclose, oexec, oread | orclose }, 30..) |mode, tag| {
- try h.send(@intCast(tag), .{ .topen = .{ .fid = 4, .mode = mode } });
- got = try h.reap();
- try testing.expectEqualStrings(e_perm, got.msg.rerror.ename);
- }
-}
-
-test "9p server: Twstat with a zero length is the truncate, and so is OTRUNC" {
- var h: Harness = .{};
- try h.handshake(4096);
- _ = try h.walkTo(5, 1, &.{ "1", "body" });
-
- const sentinel: Stat = .{
- .type = std.math.maxInt(u16),
- .dev = std.math.maxInt(u32),
- .qid = .{ .type = 0xFF, .version = std.math.maxInt(u32), .path = std.math.maxInt(u64) },
- .mode = std.math.maxInt(u32),
- .atime = std.math.maxInt(u32),
- .mtime = std.math.maxInt(u32),
- .length = std.math.maxInt(u64),
- .name = "",
- .uid = "",
- .gid = "",
- .muid = "",
- };
-
- try h.send(6, .{ .twstat = .{ .fid = 1, .stat = sentinel } });
- var got = try h.reap();
- try testing.expect(got.msg == .rwstat);
- try testing.expectEqualStrings("hello, body\n", h.fsys.body);
-
- var five = sentinel;
- five.length = 5;
- try h.send(7, .{ .twstat = .{ .fid = 1, .stat = five } });
- got = try h.reap();
- try testing.expectEqualStrings(e_trunc_only, got.msg.rerror.ename);
-
- var renamed = sentinel;
- renamed.name = "other";
- try h.send(8, .{ .twstat = .{ .fid = 1, .stat = renamed } });
- got = try h.reap();
- try testing.expectEqualStrings(e_wstat, got.msg.rerror.ename);
- try testing.expectEqualStrings("hello, body\n", h.fsys.body);
-
- var changes: [12]Stat = @splat(sentinel);
- changes[0].type = 0;
- changes[1].dev = 0;
- changes[2].qid.type = 0;
- changes[3].qid.version = 0;
- changes[4].qid.path = 0;
- changes[5].mode = 0o644;
- changes[6].atime = 0;
- changes[7].mtime = 0;
- changes[8].name = "renamed";
- changes[9].uid = "owner";
- changes[10].gid = "group";
- changes[11].muid = "writer";
- for (changes) |change| {
- for ([_]u64{ std.math.maxInt(u64), 0 }) |length| {
- if (change.mtime != std.math.maxInt(u32) and length == 0) continue;
- var attributes = change;
- attributes.length = length;
- const calls = h.fsys.calls;
- try h.send(20, .{ .twstat = .{ .fid = 1, .stat = attributes } });
- got = try h.reap();
- try testing.expect(got.msg == .rerror);
- try testing.expectEqualStrings(e_wstat, got.msg.rerror.ename);
- try testing.expectEqual(calls, h.fsys.calls);
- try testing.expectEqualStrings("hello, body\n", h.fsys.body);
- }
- }
-
- // Captured Linux v9fs O_TRUNC follow-up: length=0 plus current mtime.
- var linux_truncate = sentinel;
- linux_truncate.length = 0;
- linux_truncate.mtime = 1789432552;
- try h.send(21, .{ .twstat = .{ .fid = 1, .stat = linux_truncate } });
- got = try h.reap();
- try testing.expect(got.msg == .rwstat);
- try testing.expectEqualStrings("", h.fsys.body);
- h.fsys.body = "hello, body\n";
-
- var zero = sentinel;
- zero.length = 0;
- try h.send(9, .{ .twstat = .{ .fid = 1, .stat = zero } });
- got = try h.reap();
- try testing.expect(got.msg == .rwstat);
- try testing.expectEqualStrings("", h.fsys.body);
-
- h.fsys.body = "hello, body\n";
- _ = try h.walkTo(10, 2, &.{"index"});
- try h.send(11, .{ .topen = .{ .fid = 2, .mode = oread | otrunc } });
- got = try h.reap();
- try testing.expectEqualStrings(e_perm, got.msg.rerror.ename);
- try h.send(12, .{ .topen = .{ .fid = 1, .mode = owrite | otrunc } });
- got = try h.reap();
- try testing.expectEqual(@as(u64, 18), got.msg.ropen.qid.path);
- try testing.expectEqualStrings("", h.fsys.body);
-}
-
-test "9p server: create and remove are refused, and a remove clunks the fid anyway" {
- var h: Harness = .{};
- try h.handshake(4096);
-
- try h.send(5, .{ .tcreate = .{ .fid = 0, .name = "thing", .perm = 0o600, .mode = owrite } });
- var got = try h.reap();
- try testing.expectEqualStrings(e_perm, got.msg.rerror.ename);
-
- _ = try h.walkTo(6, 1, &.{ "1", "body" });
- try h.send(7, .{ .topen = .{ .fid = 1, .mode = ordwr } });
- _ = try h.reap();
- try h.send(8, .{ .tremove = .{ .fid = 1 } });
- got = try h.reap();
- try testing.expectEqualStrings(e_perm, got.msg.rerror.ename);
- try testing.expectEqual(@as(u32, 1), h.fsys.releases);
- try h.send(9, .{ .tstat = .{ .fid = 1 } });
- got = try h.reap();
- try testing.expectEqualStrings(e_unknown_fid, got.msg.rerror.ename);
-}
-
-test "9p server: a message arriving a byte at a time is served when its last byte lands" {
- var h: Harness = .{};
- try h.handshake(4096);
-
- var buf: [64]u8 = undefined;
- const bytes = try encode(.{ .tstat = .{ .fid = 0 } }, 5, &buf);
- for (bytes[0 .. bytes.len - 1]) |b| {
- try testing.expectEqual(@as(usize, 1), h.srv.push(&.{b}));
- h.pump();
- try h.quiet();
- }
- try testing.expectEqual(@as(usize, 1), h.srv.push(bytes[bytes.len - 1 ..]));
- h.pump();
- const got = try h.reap();
- try testing.expectEqualStrings("/", got.msg.rstat.stat.name);
-
- var pair: [128]u8 = undefined;
- const a = try encode(.{ .tstat = .{ .fid = 0 } }, 6, &pair);
- const b = try encode(.{ .tstat = .{ .fid = 0 } }, 7, pair[a.len..]);
- try testing.expectEqual(a.len + b.len, h.srv.push(pair[0 .. a.len + b.len]));
- h.pump();
- try testing.expectEqual(@as(u16, 6), (try h.reap()).tag);
- try testing.expectEqual(@as(u16, 7), (try h.reap()).tag);
- try h.quiet();
-}
-
-test "9p server: 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" {
- var h: Harness = .{};
- try h.handshake(4096);
- _ = try h.walkTo(5, 1, &.{ "1", "event" });
- _ = try h.walkTo(6, 2, &.{ "1", "body" });
- for ([_]u32{ 1, 2 }, 7..) |fid, tag| {
- try h.send(@intCast(tag), .{ .topen = .{ .fid = fid, .mode = oread } });
- _ = try h.reap();
- }
- try h.send(9, .{ .tread = .{ .fid = 1, .offset = 0, .count = 4096 } });
- try h.quiet();
-
- h.srv.hangup();
- h.pump();
- try testing.expectEqual(@as(u32, 2), h.fsys.releases);
- try h.quiet();
-
- var g: Harness = .{};
- try g.handshake(4096);
- _ = try g.walkTo(5, 1, &.{ "1", "event" });
- try g.send(6, .{ .topen = .{ .fid = 1, .mode = oread } });
- _ = try g.reap();
- try g.send(7, .{ .tread = .{ .fid = 1, .offset = 0, .count = 4096 } });
- try g.quiet();
- try g.send(notag, .{ .tversion = .{ .msize = 4096, .version = "9P2000" } });
- const v = try g.reap();
- try testing.expectEqualStrings("9P2000", v.msg.rversion.version);
- try testing.expectEqual(@as(u32, 1), g.fsys.releases);
- g.fsys.event = "Kli7 7 0 0 hello\n";
- g.pump();
- try g.quiet();
- try g.send(8, .{ .tstat = .{ .fid = 1 } });
- const got = try g.reap();
- try testing.expectEqualStrings(e_unknown_fid, got.msg.rerror.ename);
-}
-
-test "9p server: every errno the core can answer is a string Linux knows" {
- try testing.expectEqualStrings("Operation not permitted", errString(1));
- try testing.expectEqualStrings("No such file or directory", errString(2));
- try testing.expectEqualStrings("Input/output error", errString(5));
- try testing.expectEqualStrings("Cannot allocate memory", errString(12));
- try testing.expectEqualStrings("Not a directory", errString(20));
- try testing.expectEqualStrings("Invalid argument", errString(22));
- try testing.expectEqualStrings("Too many open files in system", errString(23));
- try testing.expectEqualStrings("No space left on device", errString(28));
- try testing.expectEqualStrings("Function not implemented", errString(38));
- try testing.expectEqualStrings("Input/output error", errString(0));
- try testing.expectEqualStrings("Input/output error", errString(999));
-
- try testing.expectEqualStrings("fid unknown or out of range", e_unknown_fid);
- try testing.expectEqualStrings("fid already in use", e_fid_in_use);
- try testing.expectEqualStrings("bad use of fid", e_bad_use);
- try testing.expectEqualStrings("bad offset in directory read", e_bad_offset);
- try testing.expectEqualStrings("permission denied", e_perm);
- try testing.expectEqualStrings("not a directory", e_not_dir);
- try testing.expectEqualStrings("file already open for I/O", e_already_open);
- try testing.expectEqualStrings("illegal name", e_illegal_name);
- try testing.expectEqualStrings("Too many open files in system", e_too_many_fids);
- try testing.expectEqualStrings("protocol botch", e_botch);
- try testing.expectEqualStrings("Interrupted system call", e_interrupted);
- try testing.expectEqualStrings("only support truncation to zero length", e_trunc_only);
- try testing.expectEqualStrings("wstat prohibited", e_wstat);
- try testing.expectEqualStrings("Resource temporarily unavailable", e_again);
- for ([_][]const u8{
- e_unknown_fid, e_fid_in_use, e_bad_use, e_bad_offset, e_perm,
- e_not_dir, e_already_open, e_botch, e_interrupted, e_trunc_only,
- e_wstat, e_again, e_no_tree, e_no_auth, e_count_small,
- e_illegal_name, e_too_many_fids, e_small_msize,
- }) |s| try testing.expect(s.len <= errmax);
-}
-
test "9p server: board and native capacities size the actual fid storage" {
- const Board = Server(StubFs, board_fids);
- const Native = Server(StubFs, max_fids);
+ const Board = Server(Stub, board);
+ const Native = Server(Stub, editor);
const BoardFids = @FieldType(Board, "fids");
const NativeFids = @FieldType(Native, "fids");
try testing.expectEqual(32, @typeInfo(BoardFids).array.len);
try testing.expectEqual(256, @typeInfo(NativeFids).array.len);
+ try testing.expectEqual(28, @sizeOf(@FieldType(Board.Fid, "name")));
+ try testing.expectEqual(255, @sizeOf(@FieldType(Native.Fid, "name")));
try testing.expectEqual(32 * @sizeOf(Board.Fid), @sizeOf(BoardFids));
try testing.expectEqual(256 * @sizeOf(Native.Fid), @sizeOf(NativeFids));
- try testing.expectEqual(
- @sizeOf(NativeFids) - @sizeOf(BoardFids),
- @sizeOf(Native) - @sizeOf(Board),
- );
try testing.expect(@sizeOf(BoardFids) <= 3 * 1024);
- try testing.expect(@sizeOf(NativeFids) <= 24 * 1024);
try testing.expect(@sizeOf(@FieldType(Board, "slots")) <= 8 * 1024);
try testing.expect(3 * 4096 + @sizeOf(Board) <= 24 * 1024);
-}
-
-const Pair = struct {
- srv_in: [4096]u8 = undefined,
- srv_out: [8192]u8 = undefined,
- cli_in: [4096]u8 = undefined,
- cli_out: [4096]u8 = undefined,
- fsys: StubFs = .{},
- srv: Srv = undefined,
- cli: Client = undefined,
-
- fn start(p: *Pair) void {
- p.srv = Srv.init(.{ .in = &p.srv_in, .out = &p.srv_out, .root = 1 });
- p.cli = Client.init(.{ .in = &p.cli_in, .out = &p.cli_out });
- }
-
- fn answer(p: *Pair, req: StubFs.Req) void {
- const a = p.fsys.handle(req);
- p.srv.reply(&a.reply, a.bytes);
- }
-
- fn wire(p: *Pair) void {
- var moved = true;
- while (moved) {
- moved = false;
- while (p.cli.output().len != 0) {
- const n = p.srv.push(p.cli.output());
- if (n == 0) break;
- p.cli.wrote(n);
- moved = true;
- }
- while (p.srv.retry()) |req| {
- p.answer(req);
- moved = true;
- }
- while (p.srv.next()) |req| {
- p.answer(req);
- moved = true;
- }
- while (p.srv.output().len != 0) {
- const n = p.cli.push(p.srv.output());
- if (n == 0) break;
- p.srv.wrote(n);
- moved = true;
- }
- }
- }
-
- fn one(p: *Pair, req: Client.Request) !Client.Done {
- const tag = try p.cli.submit(req);
- p.wire();
- const done = p.cli.take() orelse return error.NoReply;
- try testing.expectEqual(tag, done.tag);
- try testing.expectEqual(std.meta.activeTag(req), done.op);
- try testing.expectEqual(@as(usize, 0), p.cli.pending());
- return done;
- }
-
- fn handshake(p: *Pair) !void {
- p.start();
- const v = try p.one(.{ .version = .{} });
- try testing.expectEqualStrings("9P2000", v.result.version.version);
- try testing.expectEqual(@as(u16, notag), v.tag);
- const a = try p.one(.{ .attach = .{ .fid = 0, .uname = "goblin" } });
- try testing.expectEqual(@as(u64, 1), a.result.attach.path);
- try testing.expectEqual(qtdir, a.result.attach.type);
- }
-};
-
-test "9p client: a whole session against the server in this file" {
- var p: Pair = .{};
- try p.handshake();
- try testing.expectEqual(@as(u32, 4096), p.cli.msize);
- try testing.expectEqual(@as(u32, 4096 - 11), p.cli.maxRead());
- try testing.expectEqual(@as(u32, 4096 - 23), p.cli.maxWrite());
-
- const w = try p.one(.{ .walk = .{ .fid = 0, .newfid = 1, .names = &.{ "1", "body" } } });
- try testing.expectEqual(@as(u16, 2), w.result.walk.nwqid);
- try testing.expectEqual(@as(u64, 16), w.result.walk.wqid[0].path);
- try testing.expectEqual(@as(u64, 18), w.result.walk.wqid[1].path);
- try testing.expectEqual(qtfile, w.result.walk.wqid[1].type);
-
- const o = try p.one(.{ .open = .{ .fid = 1, .mode = ordwr } });
- try testing.expectEqual(@as(u64, 18), o.result.open.qid.path);
- try testing.expectEqual(@as(u32, 4096 - iohdrsz), o.result.open.iounit);
-
- const r = try p.one(.{ .read = .{ .fid = 1, .offset = 0, .count = 64 } });
- try testing.expectEqualStrings("hello, body\n", r.result.read);
- const eof = try p.one(.{ .read = .{ .fid = 1, .offset = 12, .count = 64 } });
- try testing.expectEqual(@as(usize, 0), eof.result.read.len);
-
- const wr = try p.one(.{ .write = .{ .fid = 1, .offset = 0, .data = "abc" } });
- try testing.expectEqual(@as(u32, 3), wr.result.write);
- try testing.expectEqualStrings("abc", p.fsys.writes[0..p.fsys.writes_len]);
-
- const st = try p.one(.{ .stat = .{ .fid = 1 } });
- try testing.expectEqualStrings("body", st.result.stat.name);
- try testing.expectEqual(@as(u64, 12), st.result.stat.length);
- try testing.expectEqualStrings("goblin", st.result.stat.uid);
-
- _ = try p.one(.{ .clunk = .{ .fid = 1 } });
- try testing.expectEqual(@as(u32, 1), p.fsys.releases);
- try testing.expectEqual(@as(usize, 0), p.cli.pending());
- try testing.expectEqual(@as(usize, 0), p.cli.output().len);
- try testing.expect(p.cli.take() == null);
- try testing.expect(!p.cli.dead);
-}
-
-fn deliverReversed(p: *Pair) !void {
- var scratch: [4096]u8 = undefined;
- const out = p.srv.output();
- try testing.expect(out.len <= scratch.len);
- @memcpy(scratch[0..out.len], out);
- const total = out.len;
- p.srv.wrote(total);
-
- var at: [max_tags]usize = undefined;
- var lens: [max_tags]u32 = undefined;
- var count: usize = 0;
- var i: usize = 0;
- while (i < total) {
- const len = frameLen(scratch[i..total]) orelse return error.ShortReply;
- at[count] = i;
- lens[count] = len;
- count += 1;
- i += len;
- }
- try testing.expect(count >= 2);
- var k = count;
- while (k > 0) {
- k -= 1;
- const f = scratch[at[k]..][0..lens[k]];
- try testing.expectEqual(f.len, p.cli.push(f));
- }
-}
-
-test "9p client: replies out of order are matched by tag and not by arrival" {
- var p: Pair = .{};
- try p.handshake();
- const w = try p.one(.{ .walk = .{ .fid = 0, .newfid = 1, .names = &.{"index"} } });
- try testing.expectEqual(@as(u16, 1), w.result.walk.nwqid);
-
- const root_tag = try p.cli.submit(.{ .stat = .{ .fid = 0 } });
- const index_tag = try p.cli.submit(.{ .stat = .{ .fid = 1 } });
- try testing.expectEqual(@as(u16, 0), root_tag);
- try testing.expectEqual(@as(u16, 1), index_tag);
- try testing.expectEqual(@as(usize, 2), p.cli.pending());
-
- while (p.cli.output().len != 0) {
- const n = p.srv.push(p.cli.output());
- p.cli.wrote(n);
- }
- while (p.srv.next()) |req| p.answer(req);
- try deliverReversed(&p);
-
- const first = p.cli.take() orelse return error.NoReply;
- try testing.expectEqual(index_tag, first.tag);
- try testing.expectEqualStrings("index", first.result.stat.name);
- const second = p.cli.take() orelse return error.NoReply;
- try testing.expectEqual(root_tag, second.tag);
- try testing.expectEqualStrings("/", second.result.stat.name);
- try testing.expectEqual(@as(usize, 0), p.cli.pending());
- try testing.expect(!p.cli.dead);
-}
-
-test "9p client: an Rerror answers one operation and the session carries on" {
- var p: Pair = .{};
- try p.handshake();
-
- const bad = try p.one(.{ .walk = .{ .fid = 0, .newfid = 1, .names = &.{"nope"} } });
- try testing.expectEqual(Client.Op.walk, bad.op);
- try testing.expectEqualStrings(errString(2), bad.result.fail);
-
- try testing.expectEqual(@as(usize, 0), p.cli.pending());
- try testing.expect(!p.cli.dead);
- const st = try p.one(.{ .stat = .{ .fid = 0 } });
- try testing.expectEqualStrings("/", st.result.stat.name);
-
- const stale = try p.one(.{ .stat = .{ .fid = 9 } });
- try testing.expectEqualStrings(e_unknown_fid, stale.result.fail);
- try testing.expect(!p.cli.dead);
-}
-
-test "9p client: a reply arriving a byte at a time is taken when its last byte lands" {
- var p: Pair = .{};
- try p.handshake();
- const tag = try p.cli.submit(.{ .stat = .{ .fid = 0 } });
-
- while (p.cli.output().len != 0) {
- const n = p.srv.push(p.cli.output());
- p.cli.wrote(n);
- }
- while (p.srv.next()) |req| p.answer(req);
- var scratch: [512]u8 = undefined;
- const out = p.srv.output();
- try testing.expect(out.len > 4 and out.len <= scratch.len);
- @memcpy(scratch[0..out.len], out);
- const reply = scratch[0..out.len];
- p.srv.wrote(reply.len);
-
- for (reply[0 .. reply.len - 1]) |b| {
- try testing.expectEqual(@as(usize, 1), p.cli.push(&.{b}));
- try testing.expect(p.cli.take() == null);
- try testing.expect(!p.cli.dead);
- }
- try testing.expectEqual(@as(usize, 1), p.cli.push(reply[reply.len - 1 ..]));
- const done = p.cli.take() orelse return error.NoReply;
- try testing.expectEqual(tag, done.tag);
- try testing.expectEqualStrings("/", done.result.stat.name);
-}
-
-test "9p client: sixteen tags outstanding, and the seventeenth is refused" {
- var p: Pair = .{};
- try p.handshake();
-
- var tags: [max_tags]u16 = undefined;
- for (&tags, 0..) |*t, i| {
- t.* = try p.cli.submit(.{ .stat = .{ .fid = 0 } });
- try testing.expectEqual(@as(u16, @intCast(i)), t.*);
- }
- try testing.expectEqual(max_tags, p.cli.pending());
- try testing.expectError(error.NoTags, p.cli.submit(.{ .stat = .{ .fid = 0 } }));
- const owed = p.cli.output().len;
- try testing.expectError(error.NoTags, p.cli.submit(.{ .clunk = .{ .fid = 0 } }));
- try testing.expectEqual(owed, p.cli.output().len);
-
- p.wire();
- var seen: [max_tags]bool = @splat(false);
- for (0..max_tags) |_| {
- const done = p.cli.take() orelse return error.NoReply;
- try testing.expectEqual(Client.Op.stat, done.op);
- try testing.expect(!seen[done.tag]);
- seen[done.tag] = true;
- }
- for (seen) |s| try testing.expect(s);
- try testing.expectEqual(@as(usize, 0), p.cli.pending());
- _ = try p.one(.{ .stat = .{ .fid = 0 } });
-}
-
-test "9p client: what a caller may not ask for is refused before a tag is spent" {
- var p: Pair = .{};
- p.start();
-
- try testing.expectError(error.Handshake, p.cli.submit(.{ .stat = .{ .fid = 0 } }));
- try p.handshake();
-
- try testing.expectError(error.BadRequest, p.cli.submit(.{ .stat = .{ .fid = nofid } }));
- try testing.expectError(error.BadRequest, p.cli.submit(.{ .clunk = .{ .fid = nofid } }));
- try testing.expectError(error.BadRequest, p.cli.submit(.{ .walk = .{ .fid = 0, .newfid = nofid, .names = &.{} } }));
-
- try testing.expectError(error.BadRequest, p.cli.submit(.{ .walk = .{ .fid = 0, .newfid = 1, .names = &.{"1/body"} } }));
- try testing.expectError(error.BadRequest, p.cli.submit(.{ .walk = .{ .fid = 0, .newfid = 1, .names = &.{""} } }));
- const seventeen: [max_welem + 1][]const u8 = @splat("x");
- try testing.expectError(error.BadRequest, p.cli.submit(.{ .walk = .{ .fid = 0, .newfid = 1, .names = &seventeen } }));
-
- try testing.expectError(error.TooLarge, p.cli.submit(.{ .read = .{
- .fid = 0,
- .offset = 0,
- .count = p.cli.maxRead() + 1,
- } }));
- var big: [4096]u8 = @splat('x');
- try testing.expectError(error.TooLarge, p.cli.submit(.{ .write = .{
- .fid = 0,
- .offset = 0,
- .data = big[0 .. p.cli.maxWrite() + 1],
- } }));
- p.cli.wrote(p.cli.output().len);
- _ = try p.cli.submit(.{ .read = .{ .fid = 0, .offset = 0, .count = p.cli.maxRead() } });
- p.cli.wrote(p.cli.output().len);
- _ = try p.cli.submit(.{ .write = .{ .fid = 0, .offset = 0, .data = big[0..p.cli.maxWrite()] } });
-
- try testing.expectError(error.Handshake, p.cli.submit(.{ .version = .{} }));
- try testing.expectError(error.NoSpace, p.cli.submit(.{ .stat = .{ .fid = 0 } }));
-}
-
-test "9p client: an msize below the floor, and one the server tried to raise" {
- var in: [512]u8 = undefined;
- var out: [512]u8 = undefined;
- var buf: [64]u8 = undefined;
-
- var c = Client.init(.{ .in = &in, .out = &out });
- try testing.expectError(error.BadRequest, c.submit(.{ .version = .{ .msize = cloud9.Server.msize_min - 1 } }));
- _ = try c.submit(.{ .version = .{} });
- try testing.expectEqual(@as(u32, 512), c.asked);
-
- _ = c.push(try encode(.{ .rversion = .{ .msize = 1024, .version = "9P2000" } }, notag, &buf));
- try testing.expect(c.take() == null);
- try testing.expect(c.dead);
-
- var c2 = Client.init(.{ .in = &in, .out = &out });
- _ = try c2.submit(.{ .version = .{} });
- _ = c2.push(try encode(.{ .rversion = .{ .msize = 512, .version = "unknown" } }, notag, &buf));
- const done = c2.take() orelse return error.NoReply;
- try testing.expectEqualStrings("unknown", done.result.version.version);
- try testing.expect(!c2.dead);
- try testing.expectEqual(@as(u32, 0), c2.msize);
- try testing.expectError(error.Handshake, c2.submit(.{ .stat = .{ .fid = 0 } }));
-
- var c3 = Client.init(.{ .in = &in, .out = &out });
- _ = try c3.submit(.{ .version = .{} });
- _ = c3.push(try encode(.{ .rversion = .{ .msize = 512, .version = "9P2000.u" } }, notag, &buf));
- try testing.expect(c3.take() == null);
- try testing.expect(c3.dead);
-}
-
-test "9p client: what is not an answer to one of our requests ends the connection" {
- var buf: [64]u8 = undefined;
- const Case = struct {
- fn armed(in: []u8, out: []u8, scratch: []u8) !Client {
- var c = Client.init(.{ .in = in, .out = out });
- _ = try c.submit(.{ .version = .{} });
- c.wrote(c.output().len);
- _ = c.push(try encode(.{ .rversion = .{ .msize = 512, .version = "9P2000" } }, notag, scratch));
- _ = c.take() orelse return error.NoReply;
- _ = try c.submit(.{ .stat = .{ .fid = 0 } });
- c.wrote(c.output().len);
- return c;
- }
- };
- var in: [512]u8 = undefined;
- var out: [512]u8 = undefined;
-
- {
- var c = try Case.armed(&in, &out, &buf);
- _ = c.push(try encode(.{ .tstat = .{ .fid = 0 } }, 0, &buf));
- try testing.expect(c.take() == null);
- try testing.expect(c.dead);
- }
- {
- var c = try Case.armed(&in, &out, &buf);
- _ = c.push(try encode(.rclunk, 3, &buf));
- try testing.expect(c.take() == null);
- try testing.expect(c.dead);
- }
- {
- var c = try Case.armed(&in, &out, &buf);
- _ = c.push(try encode(.rclunk, 900, &buf));
- try testing.expect(c.take() == null);
- try testing.expect(c.dead);
- }
- {
- var c = try Case.armed(&in, &out, &buf);
- _ = c.push(try encode(.rclunk, 0, &buf));
- try testing.expect(c.take() == null);
- try testing.expect(c.dead);
- }
- {
- var c = try Case.armed(&in, &out, &buf);
- _ = c.push(try encode(.rwstat, 0, &buf));
- try testing.expect(c.take() == null);
- try testing.expect(c.dead);
- }
- {
- var c = try Case.armed(&in, &out, &buf);
- _ = c.push(&.{ 3, 0, 0, 0 });
- try testing.expect(c.take() == null);
- try testing.expect(c.dead);
- }
- {
- var c = try Case.armed(&in, &out, &buf);
- _ = c.push(&.{ 0, 4, 0, 0 });
- try testing.expect(c.take() == null);
- try testing.expect(c.dead);
- }
- {
- var c = try Case.armed(&in, &out, &buf);
- _ = c.push(&.{ 8, 0, 0, 0, @intFromEnum(Type.rstat), 0, 0, 0 });
- try testing.expect(c.take() == null);
- try testing.expect(c.dead);
- }
-}
-
-test "9p client: an Rread longer than the Tread asked for is refused" {
- var p: Pair = .{};
- try p.handshake();
- _ = try p.one(.{ .walk = .{ .fid = 0, .newfid = 1, .names = &.{ "1", "body" } } });
- _ = try p.one(.{ .open = .{ .fid = 1, .mode = oread } });
-
- const tag = try p.cli.submit(.{ .read = .{ .fid = 1, .offset = 0, .count = 4 } });
- p.cli.wrote(p.cli.output().len);
- var buf: [64]u8 = undefined;
- _ = p.cli.push(try encode(.{ .rread = .{ .data = "hello, body\n" } }, tag, &buf));
- try testing.expect(p.cli.take() == null);
- try testing.expect(p.cli.dead);
-
- var q: Pair = .{};
- try q.handshake();
- _ = try q.one(.{ .walk = .{ .fid = 0, .newfid = 1, .names = &.{ "1", "body" } } });
- _ = try q.one(.{ .open = .{ .fid = 1, .mode = oread } });
- const short = try q.one(.{ .read = .{ .fid = 1, .offset = 0, .count = 4 } });
- try testing.expectEqualStrings("hell", short.result.read);
-}
-
-test "9p client: hangup and a dead connection refuse everything after" {
- var p: Pair = .{};
- try p.handshake();
- p.cli.hangup();
- try testing.expectEqual(@as(usize, 0), p.cli.pending());
- try testing.expectEqual(@as(usize, 0), p.cli.output().len);
- try testing.expectEqual(@as(usize, 0), p.cli.push("anything"));
- try testing.expect(p.cli.take() == null);
- try testing.expectError(error.Dead, p.cli.submit(.{ .stat = .{ .fid = 0 } }));
- try testing.expectError(error.Dead, p.cli.submit(.{ .version = .{} }));
-}
-
-test "9p client: 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 * cloud9.Server.msize_min + @sizeOf(Client) <= 700);
+ try testing.expect(min_msize >= msize_min);
}
diff --git a/src/9p_io.zig b/src/9p_io.zig
index d0db1c86..57ce67b5 100644
--- a/src/9p_io.zig
+++ b/src/9p_io.zig
@@ -138,7 +138,7 @@ fn localIp(address: std.Io.net.IpAddress) bool {
return false;
}
-const Srv = ninep.Server(pardes.ctlfs, ninep.max_fids);
+const Srv = ninep.Server(pardes.ctlfs, ninep.editor);
const Conn = struct {
fd: c_int = -1,
diff --git a/src/esp32p4_9p.zig b/src/esp32p4_9p.zig
index f6f66c15..e23235a0 100644
--- a/src/esp32p4_9p.zig
+++ b/src/esp32p4_9p.zig
@@ -26,7 +26,7 @@ comptime {
}
const Fs = gpio.Fs(Pads);
-const Server = ninep.Server(Fs, ninep.board_fids);
+const Server = ninep.Server(Fs, ninep.board);
comptime {
std.debug.assert(@typeInfo(@FieldType(Server, "fids")).array.len == 32);
diff --git a/src/esp32p4_gpio.zig b/src/esp32p4_gpio.zig
index 92468581..d6c29a36 100644
--- a/src/esp32p4_gpio.zig
+++ b/src/esp32p4_gpio.zig
@@ -1,5 +1,6 @@
//! ESP32-P4 JP1 GPIO data and its freestanding filesystem.
const std = @import("std");
+const ninep = @import("9p.zig");
// JP1: JC-ESP32P4-M3-DEV schematic, sheet 2 "Expand IO":
// 01-esp32p4-m3/docs/schematics/2_EXPAND_IO&BAT.png
@@ -107,11 +108,7 @@ pub const Header = struct {
}
};
-pub const E = struct {
- pub const NOENT: u16 = 2;
- pub const NOTDIR: u16 = 20;
- pub const INVAL: u16 = 22;
-};
+pub const E = ninep.fs.E;
pub const root: u64 = 1;
const gpio_node: u64 = 16;
@@ -167,43 +164,10 @@ const out_capacity = size: {
pub fn Fs(comptime Pads: type) type {
return struct {
const Self = @This();
- pub const name_capacity = 28;
-
- pub const Op = enum(u8) { lookup, getattr, setattr, open, read, write, release, readdir };
-
- pub const Status = enum(u8) { ok, again, err };
-
- pub const Req = struct {
- tag: u64,
- op: Op,
- node: u64,
- handle: u32 = 0,
- off: u64 = 0,
- size: u32 = 0,
- data: []const u8 = &.{},
- truncate: bool = false,
- };
-
- pub const Reply = struct {
- tag: u64,
- status: Status = .ok,
- errno: u16 = 0,
- attr: Attr = .{},
- handle: u32 = 0,
- written: u32 = 0,
-
- pub const Attr = struct {
- name: []const u8 = "",
- node: u64 = 0,
- dir: bool = false,
- size: u64 = 0,
- mode: u16 = 0o600,
- };
-
- pub fn fail(tag: u64, e: u16) Reply {
- return .{ .tag = tag, .status = .err, .errno = e };
- }
- };
+ pub const Op = ninep.fs.Op;
+ pub const Status = ninep.fs.Status;
+ pub const Req = ninep.fs.Req;
+ pub const Reply = ninep.fs.Reply;
// Reply bytes are borrowed until the next handle call.
pub const Answer = struct { reply: Reply, bytes: []const u8 = "" };
@@ -484,7 +448,7 @@ fn names(t: *Board, node: u64, skip: u64) ![][]const u8 {
test "GPIO: the whole tree costs one buffer, and the table says how big" {
try testing.expectEqual(@as(usize, 143), out_capacity);
try testing.expectEqual(@as(usize, 143), @sizeOf(Board));
- try testing.expectEqual(@as(usize, 28), Board.name_capacity);
+ try testing.expectEqual(@as(usize, 28), ninep.board.name_capacity);
try testing.expect(Header.text.len > out_capacity);
}
@@ -511,11 +475,9 @@ test "GPIO: stat and truncation preserve the register value and reads respect of
try testing.expectEqualStrings(Header.text[10..22], text.bytes);
}
-const ninep = @import("9p.zig");
-
test "GPIO: a real 9P client reads a pin's value off this tree" {
StubPads.reset();
- const Server = ninep.Server(Board, ninep.board_fids);
+ const Server = ninep.Server(Board, ninep.board);
var in: [1024]u8 = undefined;
var out: [2048]u8 = undefined;
var fsys: Board = .{};
diff --git a/src/ninep/tree.zig b/src/ninep/tree.zig
index f716bf5f..56c3112d 100644
--- a/src/ninep/tree.zig
+++ b/src/ninep/tree.zig
@@ -1,11 +1,14 @@
//! The control tree served over 9P: its nodes, lookup, readdir and the
-//! dispatch that answers the engine in src/9p.zig. The backend contract
-//! (Req, Reply, Op, Status, E) lives here; fs.zig keeps host access and mounts.
+//! dispatch that answers the engine (cloud9.fs.Server, configured in
+//! src/9p.zig). The backend contract (Req, Reply, Op, Status, E) is cloud9's,
+//! extended here with the editor's reply payload; fs.zig keeps host access
+//! and mounts.
//!
//! /README /index /ctl /new /log /screen /listeners
//! /pane/<serial>/{name,body,tag,ctl,addr,data,xdata,sel,errors,event,pty/}
//! /os/... the host filesystem /src/... embedded sources (opt-in)
const std = @import("std");
+const cloud9 = @import("cloud9");
const pardes = @import("../pardes.zig");
const fs = @import("../fs.zig");
pub const events = @import("events.zig");
@@ -22,84 +25,35 @@ const MAX_PANES = pardes.MAX_PANES;
pub const name_capacity: usize = 255;
-// ---- the backend contract ----
-
-pub const Op = enum(u8) {
- lookup,
- getattr,
- setattr,
- open,
- read,
- write,
- release,
- readdir,
-};
+comptime {
+ // The editor's engine (src/9p.zig) remembers names this long.
+ std.debug.assert(@import("../9p.zig").editor.name_capacity == name_capacity);
+}
-pub const Status = enum(u8) {
- ok,
- again,
- err,
-};
+// ---- the backend contract: cloud9.fs, with the editor's payload locator ----
-pub const Req = struct {
- tag: u64,
- op: Op,
- node: u64,
- handle: u32 = 0,
- off: u64 = 0,
- size: u32 = 0,
- data: []const u8 = &.{},
- truncate: bool = false,
+pub const Op = cloud9.fs.Op;
+pub const Status = cloud9.fs.Status;
+pub const Req = cloud9.fs.Req;
+pub const E = cloud9.fs.E;
- pub fn changesPane(req: Req) bool {
- return switch (req.op) {
- .write, .setattr => true,
- .open => req.node == @intFromEnum(TopFile.new),
- .lookup, .getattr, .read, .release, .readdir => false,
- };
- }
+/// Where a reply's bytes live. The engine ignores it; `Pardes.fsPayload`
+/// resolves it into the slice that travels beside the reply.
+pub const Payload = union(enum) {
+ none,
+ staged: u32,
+ region: struct { pane: u8, serial: u32, off: u32, len: u32 },
};
-pub const Reply = struct {
- tag: u64,
- status: Status = .ok,
- errno: u16 = 0,
- attr: Attr = .{},
- handle: u32 = 0,
- payload: Payload = .none,
- written: u32 = 0,
-
- pub const Attr = struct {
- name: []const u8 = "",
- node: u64 = 0,
- dir: bool = false,
- size: u64 = 0,
- mode: u16 = 0o644,
- mtime: u32 = 0,
- };
+pub const Reply = cloud9.fs.ReplyWith(Payload);
- pub const Payload = union(enum) {
- none,
- staged: u32,
- region: struct { pane: u8, serial: u32, off: u32, len: u32 },
+pub fn changesPane(req: Req) bool {
+ return switch (req.op) {
+ .write, .setattr => true,
+ .open => req.node == @intFromEnum(TopFile.new),
+ .lookup, .getattr, .read, .release, .readdir => false,
};
-
- pub fn fail(tag: u64, e: u16) Reply {
- return .{ .tag = tag, .status = .err, .errno = e };
- }
-};
-
-pub const E = struct {
- pub const PERM: u16 = 1;
- pub const NOENT: u16 = 2;
- pub const IO: u16 = 5;
- pub const NOMEM: u16 = 12;
- pub const NOTDIR: u16 = 20;
- pub const INVAL: u16 = 22;
- pub const NFILE: u16 = 23;
- pub const NOSPC: u16 = 28;
- pub const NOSYS: u16 = 38;
-};
+}
pub const out_reserve = 4 * 1024;
diff --git a/src/pardes.zig b/src/pardes.zig
index 88442542..ed71102e 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -7049,7 +7049,7 @@ pub const Pardes = struct {
p.shell_rows.sweep(p.gpa);
switch (ev) {
.tick => {},
- .fs_req => |req| if (req.changesPane()) {
+ .fs_req => |req| if (ctlfs.changesPane(req)) {
p.raw_hover_intent = false;
p.cancelLookHover();
},