summaryrefslogtreecommitdiff
path: root/src/Server.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/Server.zig')
-rw-r--r--src/Server.zig194
1 files changed, 194 insertions, 0 deletions
diff --git a/src/Server.zig b/src/Server.zig
new file mode 100644
index 0000000..a652f99
--- /dev/null
+++ b/src/Server.zig
@@ -0,0 +1,194 @@
+//! A bounded, caller-driven 9P2000 server connection.
+//! The backend owns fids, authentication, permissions, and filesystem operations.
+//! receive() borrows one input frame until release(). Pending operations may outlive
+//! that frame only if the backend copies their strings/data. reply() copies output.
+const std = @import("std");
+const assert = std.debug.assert;
+const wire = @import("wire.zig");
+const Server = @This();
+
+in: []u8,
+out: []u8,
+in_len: usize = 0,
+frame: u32 = 0,
+out_len: usize = 0,
+out_off: usize = 0,
+msize: u32 = 0,
+dead: bool = false,
+pending: [65]Pending = @splat(.{}),
+versioning: bool = false,
+
+const Pending = struct {
+ request: ?wire.Type = null,
+ tag: u16 = 0,
+ count: u32 = 0,
+ oldtag: u16 = wire.notag,
+};
+
+pub const Error = wire.Error || error{ Protocol, NoTags, UnknownTag, WrongReply, TooLarge };
+pub const Options = struct { in: []u8, out: []u8 };
+/// Local resource floor, not a minimum imposed by the 9P specification.
+pub const msize_min: u32 = 24;
+
+pub fn init(options: Options) Server {
+ assert(options.in.len >= msize_min);
+ assert(options.out.len >= msize_min);
+ return .{ .in = options.in, .out = options.out };
+}
+
+pub fn push(s: *Server, 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;
+}
+
+pub fn output(s: *const Server) []const u8 {
+ return s.out[s.out_off..s.out_len];
+}
+
+pub fn wrote(s: *Server, n: usize) void {
+ assert(n <= s.output().len);
+ s.out_off += n;
+ if (s.out_off == s.out_len) {
+ s.out_off = 0;
+ s.out_len = 0;
+ }
+}
+
+pub fn hasRoom(s: *Server) bool {
+ if (s.out_off != 0) {
+ const n = s.output().len;
+ std.mem.copyForwards(u8, s.out[0..n], s.output());
+ s.out_off = 0;
+ s.out_len = n;
+ }
+ return s.out.len - s.out_len >= @max(s.msize, msize_min);
+}
+
+/// A null result means more input or output drainage is needed.
+/// Any receive error is terminal: the stream cannot be safely resynchronized.
+pub fn receive(s: *Server) Error!?wire.Decoded {
+ assert(s.frame == 0);
+ if (s.dead) return null;
+ errdefer s.dead = true;
+ const len = wire.frameLen(s.in[0..s.in_len]) orelse return null;
+ if (len < wire.header_len or len > s.in.len) return error.Protocol;
+ if (len > s.in_len) return null;
+ const got = try wire.decode(s.in[0..len]);
+ const kind = got.msg.msgType();
+ if (!wire.isT(kind)) return error.Protocol;
+ if (!s.hasRoom()) return null;
+ if (kind == .tversion) {
+ if (got.tag != wire.notag) return error.Protocol;
+ // Finish any partially transmitted response before starting a new session.
+ if (s.output().len != 0) return null;
+ s.pending = @splat(.{});
+ s.versioning = true;
+ s.msize = 0;
+ } else {
+ if (s.msize == 0 or s.versioning) return error.Protocol;
+ if (len > s.msize or got.tag == wire.notag) return error.Protocol;
+ if (s.find(got.tag) != null) return error.Protocol;
+ const slot = s.free(kind) orelse return error.NoTags;
+ slot.* = .{ .request = kind, .tag = got.tag, .count = switch (got.msg) {
+ .tread => |m| m.count,
+ .twrite => |m| @intCast(m.data.len),
+ .twalk => |m| m.nwname,
+ else => 0,
+ }, .oldtag = if (got.msg == .tflush) got.msg.tflush.oldtag else wire.notag };
+ }
+ s.frame = len;
+ return got;
+}
+
+pub fn release(s: *Server) void {
+ assert(s.frame != 0 and s.frame <= s.in_len);
+ const n = s.in_len - s.frame;
+ std.mem.copyForwards(u8, s.in[0..n], s.in[s.frame..s.in_len]);
+ s.in_len = n;
+ s.frame = 0;
+}
+
+/// Call only for a received Tversion, after aborting backend work and releasing fids.
+/// Suffixes may fall back to base 9P2000; arbitrary strings beginning with 9P may not.
+pub fn negotiate(s: *Server, want: u32, version: []const u8) Error!void {
+ assert(s.versioning);
+ const size: u32 = @intCast(@min(want, s.in.len, s.out.len, std.math.maxInt(u32)));
+ if (size < msize_min) return error.TooLarge;
+ const base = std.mem.sliceTo(version, '.');
+ const known = std.mem.eql(u8, base, "9P2000");
+ try s.append(wire.notag, .{ .rversion = .{
+ .msize = size,
+ .version = if (known) "9P2000" else "unknown",
+ } }, size);
+ s.msize = if (known) size else 0;
+ s.versioning = false;
+}
+
+/// An Rflush is a backend promise: no further response for oldtag will be sent.
+/// A canceled backend operation must be retired before its tag can be reused.
+pub fn reply(s: *Server, tag: u16, msg: wire.Msg) Error!void {
+ if (s.dead) return error.Protocol;
+ const slot = s.find(tag) orelse return error.UnknownTag;
+ const request = slot.request.?;
+ const kind = msg.msgType();
+ if (request == .tflush and kind != .rflush) return error.WrongReply;
+ if (kind != .rerror and @intFromEnum(kind) != @intFromEnum(request) + 1)
+ return error.WrongReply;
+ switch (msg) {
+ .rread => |m| if (m.data.len > slot.count) return error.WrongReply,
+ .rwrite => |m| if (m.count > slot.count) return error.WrongReply,
+ .rwalk => |m| {
+ if (m.nwqid > slot.count) return error.WrongReply;
+ if (m.nwqid == 0 and slot.count != 0) return error.WrongReply;
+ },
+ else => {},
+ }
+ var response = msg;
+ if (response == .rerror) {
+ const cap = @min(s.msize - wire.header_len - 2, std.math.maxInt(u16));
+ response.rerror.ename = response.rerror.ename[0..@min(response.rerror.ename.len, cap)];
+ }
+ try s.append(tag, response, s.msize);
+ const oldtag = slot.oldtag;
+ slot.* = .{};
+ if (kind == .rflush) {
+ if (s.find(oldtag)) |old| old.* = .{};
+ }
+}
+
+fn append(s: *Server, tag: u16, msg: wire.Msg, limit: u32) Error!void {
+ if (try wire.encodedLen(msg) > limit) return error.TooLarge;
+ _ = s.hasRoom();
+ const bytes = try wire.encode(msg, tag, s.out[s.out_len..]);
+ s.out_len += bytes.len;
+}
+
+fn find(s: *Server, tag: u16) ?*Pending {
+ for (&s.pending) |*slot| {
+ if (slot.request != null and slot.tag == tag) return slot;
+ }
+ return null;
+}
+
+fn free(s: *Server, request: wire.Type) ?*Pending {
+ for (&s.pending, 0..) |*slot, i| {
+ // A full ordinary request window must still admit cancellation.
+ if (i == s.pending.len - 1 and request != .tflush) continue;
+ if (slot.request == null) return slot;
+ }
+ return null;
+}
+
+pub fn hangup(s: *Server) void {
+ s.dead = true;
+ s.pending = @splat(.{});
+ s.in_len = 0;
+ s.frame = 0;
+ s.out_len = 0;
+ s.out_off = 0;
+ s.msize = 0;
+ s.versioning = false;
+}