From 5f24c4a2284a0af84fb9d116f7a39f6a58e42ae9 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 14 Sep 2026 14:10:28 -0300 Subject: Implement base 9P2000 sessions, shared transports, and conformance probes --- src/session_test.zig | 222 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 src/session_test.zig (limited to 'src/session_test.zig') diff --git a/src/session_test.zig b/src/session_test.zig new file mode 100644 index 0000000..8600d03 --- /dev/null +++ b/src/session_test.zig @@ -0,0 +1,222 @@ +const std = @import("std"); +const testing = std.testing; +const c9 = @import("root.zig"); +const Pair = struct { + client_in: [4096]u8 = undefined, + client_out: [4096]u8 = undefined, + server_in: [4096]u8 = undefined, + server_out: [8192]u8 = undefined, + client: c9.Client = undefined, + server: c9.Server = undefined, + + fn init(p: *Pair) !void { + p.client = .init(.{ .in = &p.client_in, .out = &p.client_out }); + p.server = .init(.{ .in = &p.server_in, .out = &p.server_out }); + _ = try p.client.submit(.{ .version = .{} }); + const request = try p.nextRequest(); + try p.server.negotiate(request.msg.tversion.msize, request.msg.tversion.version); + p.server.release(); + try testing.expectEqual(c9.Client.Op.version, (try p.nextResult()).op); + } + + fn nextRequest(p: *Pair) !c9.Decoded { + // Exercise every frame boundary through single-byte delivery. + while (p.client.output().len != 0) { + try testing.expectEqual(@as(usize, 1), p.server.push(p.client.output()[0..1])); + p.client.wrote(1); + if (try p.server.receive()) |request| return request; + } + return (try p.server.receive()) orelse error.NoRequest; + } + + fn nextResult(p: *Pair) !c9.Client.Done { + while (p.server.output().len != 0) { + try testing.expectEqual(@as(usize, 1), p.client.push(p.server.output()[0..1])); + p.server.wrote(1); + if (p.client.take()) |result| return result; + } + return p.client.take() orelse error.NoResult; + } + + fn exchange(p: *Pair, req: c9.Client.Request, response: c9.Msg) !void { + const tag = try p.client.submit(req); + const request = try p.nextRequest(); + try testing.expectEqual(tag, request.tag); + try p.server.reply(tag, response); + p.server.release(); + const result = try p.nextResult(); + try testing.expectEqual(std.meta.activeTag(req), result.op); + try testing.expectEqualStrings(@tagName(req), @tagName(result.result)); + } +}; + +const qid: c9.Qid = .{ .type = 0, .version = 1, .path = 2 }; +const stat: c9.Stat = .{ + .type = 0, + .dev = 0, + .qid = qid, + .mode = 0o600, + .atime = 0, + .mtime = 0, + .length = 0, + .name = "file", + .uid = "user", + .gid = "group", + .muid = "user", +}; + +test "all base 9P2000 client operations through fragmented server transport" { + var p: Pair = .{}; + try p.init(); + try p.exchange(.{ .auth = .{ .afid = 1, .uname = "user" } }, .{ .rauth = .{ .aqid = qid } }); + try p.exchange(.{ .attach = .{ .fid = 2, .afid = 1, .uname = "user" } }, .{ .rattach = .{ .qid = qid } }); + try p.exchange(.{ .walk = .{ .fid = 2, .newfid = 3, .names = &.{"file"} } }, .{ .rwalk = .{ .nwqid = 1, .wqid = @splat(qid) } }); + try p.exchange(.{ .open = .{ .fid = 3, .mode = 2 } }, .{ .ropen = .{ .qid = qid, .iounit = 0 } }); + try p.exchange(.{ .create = .{ .fid = 2, .name = "new", .perm = 0o600, .mode = 2 } }, .{ .rcreate = .{ .qid = qid, .iounit = 0 } }); + try p.exchange(.{ .read = .{ .fid = 3, .offset = 0, .count = 3 } }, .{ .rread = .{ .data = "abc" } }); + try p.exchange(.{ .write = .{ .fid = 3, .offset = 0, .data = "abc" } }, .{ .rwrite = .{ .count = 2 } }); + try p.exchange(.{ .stat = .{ .fid = 3 } }, .{ .rstat = .{ .stat = stat } }); + try p.exchange(.{ .wstat = .{ .fid = 3, .stat = stat } }, .rwstat); + try p.exchange(.{ .clunk = .{ .fid = 3 } }, .rclunk); + try p.exchange(.{ .remove = .{ .fid = 2 } }, .rremove); + try p.exchange(.{ .flush = .{ .oldtag = 42 } }, .rflush); +} + +test "flush holds oldtag until Rflush, including a completed original request" { + for ([_]bool{ false, true }) |complete| { + var p: Pair = .{}; + try p.init(); + const oldtag = try p.client.submit(.{ .read = .{ .fid = 1, .offset = 0, .count = 1 } }); + _ = try p.nextRequest(); + p.server.release(); + const flush = try p.client.submit(.{ .flush = .{ .oldtag = oldtag } }); + _ = try p.nextRequest(); + p.server.release(); + if (complete) { + try p.server.reply(oldtag, .{ .rread = .{ .data = "x" } }); + try testing.expectEqual(oldtag, (try p.nextResult()).tag); + try testing.expectEqual(@as(usize, 2), p.client.pending()); + } + const other = try p.client.submit(.{ .stat = .{ .fid = 1 } }); + try testing.expect(other != oldtag and other != flush); + try p.server.reply(flush, .rflush); + try testing.expectEqual(flush, (try p.nextResult()).tag); + try testing.expectEqual(@as(usize, 1), p.client.pending()); + try testing.expectError(error.UnknownTag, p.server.reply(oldtag, .{ .rread = .{ .data = "x" } })); + const reused = try p.client.submit(.{ .stat = .{ .fid = 1 } }); + try testing.expectEqual(oldtag, reused); + } +} + +test "response bounds and reply types are checked before output is changed" { + var p: Pair = .{}; + try p.init(); + const tag = try p.client.submit(.{ .write = .{ .fid = 1, .offset = 0, .data = "a" } }); + _ = try p.nextRequest(); + p.server.release(); + try testing.expectError(error.WrongReply, p.server.reply(tag, .{ .rwrite = .{ .count = 2 } })); + try testing.expectError(error.WrongReply, p.server.reply(tag, .rclunk)); + try testing.expectEqual(@as(usize, 0), p.server.output().len); + try p.server.reply(tag, .{ .rwrite = .{ .count = 1 } }); + _ = try p.nextResult(); +} + +test "stat outer length overflow is rejected without writing output" { + var name: [65535 - 48]u8 = @splat('a'); + var entry = stat; + entry.name = &name; + entry.uid = ""; + entry.gid = ""; + entry.muid = ""; + var buffer: [65550]u8 = @splat(0xaa); + try testing.expectError(error.Overlong, c9.encode(.{ .rstat = .{ .stat = entry } }, 1, &buffer)); + try testing.expect(std.mem.allEqual(u8, &buffer, 0xaa)); +} + +test "arbitrary input decoder round trip" { + try testing.fuzz({}, fuzzDecode, .{}); +} + +fn fuzzDecode(_: void, smith: *testing.Smith) !void { + var input_buffer: [65536]u8 = undefined; + const input = input_buffer[0..smith.slice(&input_buffer)]; + const decoded = c9.decode(input) catch return; + var buffer: [65536]u8 = undefined; + const encoded = try c9.encode(decoded.msg, decoded.tag, &buffer); + try testing.expectEqualSlices(u8, input, encoded); +} + +test "flush can cancel a full request window and reserves unused oldtags" { + var p: Pair = .{}; + try p.init(); + _ = try p.client.submit(.{ .flush = .{ .oldtag = 2 } }); + const next = try p.client.submit(.{ .stat = .{ .fid = 0 } }); + try testing.expect(next != 2); + const third = try p.client.submit(.{ .stat = .{ .fid = 0 } }); + try testing.expect(third != 2); + p.client.hangup(); + try p.init(); + for (0..c9.max_tags) |_| _ = try p.client.submit(.{ .stat = .{ .fid = 0 } }); + try testing.expectError(error.NoTags, p.client.submit(.{ .stat = .{ .fid = 0 } })); + const flush = try p.client.submit(.{ .flush = .{ .oldtag = 0 } }); + try testing.expectEqual(@as(u16, c9.max_tags), flush); +} + +test "multiple flushes retain reservations until each response and can themselves be flushed" { + var p: Pair = .{}; + try p.init(); + const oldtag = try p.client.submit(.{ .read = .{ .fid = 1, .offset = 0, .count = 1 } }); + _ = try p.nextRequest(); + p.server.release(); + const first = try p.client.submit(.{ .flush = .{ .oldtag = oldtag } }); + _ = try p.nextRequest(); + p.server.release(); + const second = try p.client.submit(.{ .flush = .{ .oldtag = oldtag } }); + _ = try p.nextRequest(); + p.server.release(); + try p.server.reply(first, .rflush); + _ = try p.nextResult(); + const held = try p.client.submit(.{ .stat = .{ .fid = 1 } }); + try testing.expect(held != oldtag); + try p.server.reply(second, .rflush); + _ = try p.nextResult(); + try testing.expectEqual(oldtag, try p.client.submit(.{ .stat = .{ .fid = 1 } })); + + try p.init(); + const read_tag = try p.client.submit(.{ .read = .{ .fid = 1, .offset = 0, .count = 1 } }); + _ = try p.nextRequest(); + p.server.release(); + const flush_tag = try p.client.submit(.{ .flush = .{ .oldtag = read_tag } }); + _ = try p.nextRequest(); + p.server.release(); + const cancel_flush = try p.client.submit(.{ .flush = .{ .oldtag = flush_tag } }); + _ = try p.nextRequest(); + p.server.release(); + try p.server.reply(read_tag, .{ .rread = .{ .data = "x" } }); + _ = try p.nextResult(); + try p.server.reply(cancel_flush, .rflush); + _ = try p.nextResult(); + try testing.expectEqual(@as(usize, 0), p.client.pending()); +} + +test "server reserves cancellation capacity and rejects duplicate pending tags" { + var input: [4096]u8 = undefined; + var output: [4096]u8 = undefined; + var server: c9.Server = .init(.{ .in = &input, .out = &output }); + server.msize = 4096; + var frame: [64]u8 = undefined; + for (0..64) |i| { + const bytes = try c9.encode(.{ .tstat = .{ .fid = 1 } }, @intCast(i), &frame); + _ = server.push(bytes); + _ = (try server.receive()).?; + server.release(); + } + _ = server.push(try c9.encode(.{ .tflush = .{ .oldtag = 0 } }, 100, &frame)); + _ = (try server.receive()).?; + server.release(); + try server.reply(100, .rflush); + server.wrote(server.output().len); + _ = server.push(try c9.encode(.{ .tstat = .{ .fid = 1 } }, 1, &frame)); + try testing.expectError(error.Protocol, server.receive()); + try testing.expect(server.dead); +} -- cgit v1.3