src/client/buffered_wire.zig
Ref: Size: 7.9 KiB History
const std = @import("std");
const client = @import("client.zig");
const proto = @import("term").protocol;
/// Opaque transport bytes held by a forwarding connection, across fragmented
/// frames and bursts. Frame parsing remains forwarding-only in this wrapper.
pub const transport_queue_max: usize = 1024 * 1024;
// Incremental reads and queued writes keep partial stream frames and a
// peer which stops reading from blocking the mailbox or stop(). QUIC keeps
// its existing framing and outgoing queue in Link.
pub const Wire = struct {
alloc: std.mem.Allocator,
tr: *client.Transport,
input: std.ArrayList(u8) = .empty,
output: std.ArrayList(u8) = .empty,
pub fn init(alloc: std.mem.Allocator, tr: *client.Transport) !Wire {
switch (tr.link) {
.fd => |fd| try nonblocking(fd),
.pipe => |p| {
try nonblocking(p.r);
try nonblocking(p.w);
},
.quic => {},
}
return .{ .alloc = alloc, .tr = tr };
}
pub fn deinit(self: *Wire) void {
self.input.deinit(self.alloc);
self.output.deinit(self.alloc);
}
pub fn writeFd(self: *Wire) std.posix.fd_t {
return switch (self.tr.link) {
.fd => |fd| fd,
.pipe => |p| p.w,
.quic => self.tr.pollFd(),
};
}
/// Stream backlog becomes writable through its output fd. QUIC backlog
/// advances only when service processes packets or a transport timer, so
/// polling its UDP socket for OUT would spin while stream credit is full.
pub fn pendingWriteFd(self: *Wire) ?std.posix.fd_t {
if (!self.pending()) return null;
return switch (self.tr.link) {
.fd => |fd| fd,
.pipe => |p| p.w,
.quic => null,
};
}
pub fn pending(self: *Wire) bool {
return if (self.tr.link == .quic) self.tr.link.quic.qout.items.len > 0 else self.output.items.len > 0;
}
pub fn pendingBytes(self: *const Wire) usize {
return if (self.tr.link == .quic) self.tr.link.quic.qout.items.len else self.output.items.len;
}
pub fn send(self: *Wire, kind: proto.MsgType, payload: []const u8) !void {
if (self.tr.link == .quic) return self.tr.writeFrame(kind, payload);
try proto.appendFrame(&self.output, self.alloc, kind, payload);
try self.flush();
}
pub fn flush(self: *Wire) !void {
if (self.tr.link == .quic) return self.tr.flushQuic();
if (self.output.items.len == 0) return;
const n = std.posix.write(self.writeFd(), self.output.items) catch |err| switch (err) {
error.WouldBlock => return,
else => return err,
};
self.output.replaceRangeAssumeCapacity(0, n, &.{});
}
pub fn read(self: *Wire) !client.Incoming {
return self.readLimited(proto.frame_header_len + proto.max_payload);
}
/// Forwarding peers accept much smaller frames than the terminal wire's
/// paste-sized maximum. Bound the partial frame too, rather than letting a
/// claimed terminal-sized payload quietly become a forwarding queue.
pub fn readLimited(self: *Wire, max_buffered: usize) !client.Incoming {
if (self.tr.link == .quic) {
if (self.tr.link.quic.cl.in.items.len > max_buffered) return error.FrameTooLarge;
return self.tr.readFrame(self.alloc);
}
var need: usize = proto.frame_header_len;
if (self.input.items.len >= proto.frame_header_len) {
const len = std.mem.readInt(u32, self.input.items[1..5], .little);
if (len > proto.max_payload) return error.FrameTooLarge;
need += len;
}
if (need > max_buffered) return error.FrameTooLarge;
if (self.input.items.len >= need) {
if (try proto.takeFrame(self.alloc, &self.input)) |frame| return .{ .frame = frame };
unreachable;
}
if (self.input.items.len >= max_buffered) return error.FrameTooLarge;
var buf: [64 * 1024]u8 = undefined;
const room = max_buffered - self.input.items.len;
const n = std.posix.read(self.tr.pollFd(), buf[0..@min(buf.len, @min(need - self.input.items.len, room))]) catch |err| switch (err) {
error.WouldBlock => return .incomplete,
else => return err,
};
if (n == 0) return .closed;
try self.input.appendSlice(self.alloc, buf[0..n]);
if (self.input.items.len >= proto.frame_header_len) {
const staged_len = std.mem.readInt(u32, self.input.items[1..5], .little);
if (staged_len > proto.max_payload or proto.frame_header_len + staged_len > max_buffered) return error.FrameTooLarge;
}
if (try proto.takeFrame(self.alloc, &self.input)) |frame| return .{ .frame = frame };
return .incomplete;
}
};
fn nonblocking(fd: std.posix.fd_t) !void {
const flags = try std.posix.fcntl(fd, std.posix.F.GETFL, 0);
const bits: u32 = @bitCast(std.posix.O{ .NONBLOCK = true });
_ = try std.posix.fcntl(fd, std.posix.F.SETFL, flags | bits);
}
test "limited wire rejects a claimed forwarding queue before reading its body" {
var pair: [2]std.posix.fd_t = undefined;
try std.testing.expectEqual(@as(c_int, 0), std.c.socketpair(std.posix.AF.UNIX, std.posix.SOCK.STREAM, 0, &pair));
defer std.posix.close(pair[1]);
var tr: client.Transport = .{ .link = .{ .fd = pair[0] } };
defer tr.close();
var wire = try Wire.init(std.testing.allocator, &tr);
defer wire.deinit();
const header = proto.encodeHeader(.forward_data, 1024);
try proto.writeAllFd(pair[1], &header);
try std.testing.expectError(error.FrameTooLarge, wire.readLimited(512));
}
test "forwarding budget admits a maximum legal forwarding frame" {
var pair: [2]std.posix.fd_t = undefined;
try std.testing.expectEqual(@as(c_int, 0), std.c.socketpair(std.posix.AF.UNIX, std.posix.SOCK.STREAM, 0, &pair));
defer std.posix.close(pair[1]);
var tr: client.Transport = .{ .link = .{ .fd = pair[0] } };
defer tr.close();
var wire = try Wire.init(std.testing.allocator, &tr);
defer wire.deinit();
var payload: [proto.forward_id_len + proto.forward_data_max]u8 = undefined;
try proto.writeFrame(pair[1], .forward_data, &payload);
const frame = while (true) switch (try wire.readLimited(transport_queue_max)) {
.frame => |frame| break frame,
.incomplete => continue,
.closed => return error.UnexpectedEof,
};
defer frame.deinit(std.testing.allocator);
try std.testing.expectEqual(proto.MsgType.forward_data, frame.type);
try std.testing.expectEqual(payload.len, frame.payload.len);
try std.testing.expect(proto.frame_header_len + frame.payload.len <= transport_queue_max);
}
test "only stream backlog requests write readiness" {
const alloc = std.testing.allocator;
const pair = blk: {
var fds: [2]std.posix.fd_t = undefined;
try std.testing.expectEqual(@as(c_int, 0), std.c.socketpair(std.posix.AF.UNIX, std.posix.SOCK.STREAM, 0, &fds));
break :blk fds;
};
defer std.posix.close(pair[0]);
defer std.posix.close(pair[1]);
var stream_tr: client.Transport = .{ .link = .{ .fd = pair[0] } };
var stream_wire: Wire = .{ .alloc = alloc, .tr = &stream_tr };
defer stream_wire.deinit();
try std.testing.expectEqual(@as(?std.posix.fd_t, null), stream_wire.pendingWriteFd());
try stream_wire.output.append(alloc, 1);
try std.testing.expectEqual(@as(?std.posix.fd_t, pair[0]), stream_wire.pendingWriteFd());
var quic_tr: client.Transport = .{ .link = .{ .quic = .{ .cl = undefined, .alloc = alloc } } };
defer quic_tr.link.quic.qout.deinit(alloc);
var quic_wire: Wire = .{ .alloc = alloc, .tr = &quic_tr };
defer quic_wire.deinit();
try quic_tr.link.quic.qout.append(alloc, 1);
try std.testing.expect(quic_wire.pending());
try std.testing.expectEqual(@as(?std.posix.fd_t, null), quic_wire.pendingWriteFd());
}