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); }