lib/quic/src/connection/stream/send.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const quic = @import("../../root.zig");
  3 
  4 const stream = quic.connection.stream;
  5 
  6 /// One STREAM frame that has gone out, held until an acknowledgment or a loss settles it, so the
  7 /// caller can size the range table to hold one entry for each outstanding frame. The entry holds
  8 /// the offset, the byte count, whether that frame closed the stream, and whether the entry is
  9 /// taken.
 10 pub const SentRange = struct {
 11     offset: u62,
 12     length: u16,
 13     fin: bool,
 14     outstanding: bool,
 15 
 16     pub fn empty() SentRange {
 17         return .{ .offset = 0, .length = 0, .fin = false, .outstanding = false };
 18     }
 19 };
 20 
 21 /// The bytes and the closing flag picked out for one STREAM frame, so the packet builder can turn
 22 /// the chunk into a frame. The chunk holds the offset, those bytes, and that flag. The bytes point
 23 /// into the send ring.
 24 pub const Chunk = struct {
 25     offset: u62,
 26     data: []const u8,
 27     fin: bool,
 28 };
 29 
 30 /// The half of one stream that puts data out, working out of a ring and a range table the caller
 31 /// supplies, so the connection holds one for stream 0 and drives it from application writes and
 32 /// recovery. The half holds that ring and table, the peer's credit, the state, the offsets
 33 /// acknowledged, sent, and written, the final size, and the reset state. Those three offsets keep
 34 /// their order, acknowledged at or under sent and sent at or under written. `init` wants both the
 35 /// ring and the table nonempty.
 36 pub const Send = struct {
 37     bytes: []u8,
 38     ranges: []SentRange,
 39     credit: stream.Credit,
 40     state: stream.SendState = .ready,
 41     head: usize = 0,
 42     acknowledged: u62 = 0,
 43     sent: u62 = 0,
 44     written: u62 = 0,
 45     final_size: ?u62 = null,
 46     fin_sent: bool = false,
 47     fin_acknowledged: bool = false,
 48     /// The stretch of the stream that a lost frame put back in the queue, so the next chunk can
 49     /// start below the sent offset. Those bytes go out ahead of anything the application has newly
 50     /// written.
 51     lost: quic.connection.recovery.LostRange = .{},
 52     /// Flag set when a lost frame carried the closing flag before an acknowledgment covered it,
 53     /// because a lost closing flag carries no bytes of its own and needs a dedicated flag. The flag
 54     /// clears once a copy goes out, once an acknowledgment covers the flag, or once a reset
 55     /// abandons the stream.
 56     fin_lost: bool = false,
 57     reset_code: ?u62 = null,
 58     reset_pending: bool = false,
 59 
 60     pub fn init(bytes: []u8, ranges: []SentRange, limit: u62) Send {
 61         std.debug.assert(bytes.len > 0);
 62         std.debug.assert(ranges.len > 0);
 63         for (ranges) |*range| range.* = SentRange.empty();
 64         return .{ .bytes = bytes, .ranges = ranges, .credit = stream.Credit.init(limit) };
 65     }
 66 
 67     /// Counts what the ring is holding, measured from the oldest byte the peer has yet to
 68     /// acknowledge, so `write` accepts what room remains. The count fits inside the ring.
 69     pub fn buffered(self: *const Send) usize {
 70         std.debug.assert(self.acknowledged <= self.sent);
 71         std.debug.assert(self.sent <= self.written);
 72         const count: usize = @intCast(self.written - self.acknowledged);
 73         std.debug.assert(count <= self.bytes.len);
 74         return count;
 75     }
 76 
 77     /// Takes application bytes into the ring so they enter the stream, accepting as many as free
 78     /// room allows, and reports how many it took. The call accepts nothing once the final size is
 79     /// settled or a reset has abandoned the stream. The call also stops at the highest offset a
 80     /// stream can reach.
 81     pub fn write(self: *Send, input: []const u8) usize {
 82         if (!self.writable()) return 0;
 83         const free = self.bytes.len - self.buffered();
 84         const offset_room: u64 = std.math.maxInt(u62) - self.written;
 85         const count: usize = @intCast(@min(free, input.len, offset_room));
 86         if (count == 0) return 0;
 87         stream.ring.write(self.bytes, self.ringIndex(self.written), input[0..count]);
 88         self.written += @intCast(count);
 89         std.debug.assert(self.buffered() <= self.bytes.len);
 90         return count;
 91     }
 92 
 93     /// Settles the final size at the last byte the application wrote, so the application ends its
 94     /// side of the stream. The call leaves state unchanged when the final size is settled already
 95     /// or the stream stands reset.
 96     pub fn finish(self: *Send) void {
 97         if (!self.writable()) return;
 98         self.final_size = self.written;
 99     }
100 
101     /// Says whether this half has anything for the packet being built, so the send path decides
102     /// whether to request a frame. Lost bytes, a lost closing flag, bytes never sent, and a closing
103     /// flag never sent each count. A reset leaves nothing waiting, having abandoned what was there.
104     pub fn pending(self: *const Send) bool {
105         if (self.reset_code != null) return false;
106         if (!self.lost.isEmpty() or self.fin_lost) return true;
107         if (self.sent < self.written) return true;
108         const final = self.final_size orelse return false;
109         std.debug.assert(final == self.sent);
110         return !self.fin_sent;
111     }
112 
113     /// Gives the offset the next STREAM frame will start at, so the packet builder can size the
114     /// frame header against that offset before asking for a chunk. The start is the first byte
115     /// waiting to go a second time, or, with none waiting, the first byte never sent.
116     pub fn nextOffset(self: *const Send) u62 {
117         if (self.lost.isEmpty()) return self.sent;
118         std.debug.assert(self.lost.start >= self.acknowledged);
119         std.debug.assert(self.lost.end <= self.sent);
120         return self.lost.start;
121     }
122 
123     /// Picks out up to `data_max` bytes in order for the next STREAM frame, so the packet builder
124     /// receives the payload for that frame. Bytes going a second time come first, then a closing
125     /// flag on its own, and both travel free of credit. Bytes going out for the first time stop at
126     /// this stream's own credit and at `connection_credit`. The pick also stops at the buffer end
127     /// where the ring wraps, and at the longest payload a frame may carry. The closing flag travels
128     /// with the pick that reaches the final size. The call returns null when the half has nothing
129     /// waiting, and returns null when every entry in the table is taken.
130     pub fn nextChunk(self: *const Send, data_max: usize, connection_credit: u62) ?Chunk {
131         if (!self.pending()) return null;
132         if (self.freeRange() == null) return null;
133         if (!self.lost.isEmpty()) return self.lostChunk(data_max);
134         if (self.fin_lost) return self.lostFin();
135         const index = self.ringIndex(self.sent);
136         const credit: u64 = @min(self.credit.available(), connection_credit);
137         const waiting: u64 = self.written - self.sent;
138         const room: u64 = @min(data_max, self.bytes.len - index, std.math.maxInt(u16));
139         const count: usize = @intCast(@min(waiting, credit, room));
140         const end = self.sent + @as(u62, @intCast(count));
141         const fin = if (self.final_size) |final| end == final and !self.fin_sent else false;
142         if (count == 0 and !fin) return null;
143         return .{ .offset = self.sent, .data = self.bytes[index..][0..count], .fin = fin };
144     }
145 
146     /// Takes an entry in the table for the pick that has gone out, and reports how many of its
147     /// bytes had never gone before, for the packet builder to call once the packet holding the
148     /// frame is sealed and charge that count to connection credit. Only that figure is charged to
149     /// the connection credit, because bytes going a second time sit under the highest offset
150     /// already reached. A pick that had gone before reports zero and takes its bytes out of the
151     /// waiting stretch. A pick of fresh bytes moves the sent offset on, spends this stream's
152     /// credit, and moves the half's state.
153     pub fn commitChunk(self: *Send, offset: u62, length: u16, fin: bool) u16 {
154         const slot = self.freeRange() orelse unreachable;
155         self.ranges[slot] = .{
156             .offset = offset,
157             .length = length,
158             .fin = fin,
159             .outstanding = true,
160         };
161         if (offset < self.sent or (fin and self.fin_sent)) {
162             self.commitLost(offset, length, fin);
163             return 0;
164         }
165         std.debug.assert(offset == self.sent);
166         std.debug.assert(length <= self.written - self.sent);
167         self.sent += length;
168         self.credit.consume(length);
169         if (fin) {
170             std.debug.assert(self.sent == self.final_size.?);
171             self.fin_sent = true;
172         }
173         const event: stream.SendEvent = if (fin) .send_fin else .send_data;
174         self.state = stream.sendTransition(self.state, event) catch unreachable;
175         return length;
176     }
177 
178     /// Hands back the figure a STREAM_DATA_BLOCKED frame would report while this stream's own
179     /// credit holds the sender up, so the send path decides whether that frame belongs in the
180     /// packet being built. The sender counts as held up while the stream stands unreset and bytes
181     /// wait to go.
182     pub fn blocked(self: *const Send) ?u62 {
183         const waiting = self.reset_code == null and self.sent < self.written;
184         return self.credit.blocked(waiting);
185     }
186 
187     /// Notes that a STREAM_DATA_BLOCKED frame went out, for the send path to call once the frame is
188     /// in a sealed packet. That frame also starts the half moving, taking it out of Ready.
189     pub fn commitBlocked(self: *Send, limit: u62) void {
190         self.credit.markBlocked(limit);
191         if (self.state != .ready) return;
192         self.state = stream.sendTransition(.ready, .send_data) catch unreachable;
193     }
194 
195     /// Frees the ring bytes one acknowledged STREAM frame covered, and trims them off whichever end
196     /// of the waiting stretch they reach, so an acknowledged packet frees the send buffer. An
197     /// acknowledgment covering the closing flag clears a lost one. Bytes come free in offset order,
198     /// so an acknowledgment above a frame still outstanding frees nothing yet. The call passes over
199     /// a frame this half has already released. The closing flag acknowledged with every byte behind
200     /// it carries the half to Data Recvd.
201     pub fn acknowledge(self: *Send, offset: u62, length: u16, fin: bool) void {
202         const slot = self.findRange(offset, length, fin) orelse return;
203         self.ranges[slot] = SentRange.empty();
204         self.lost.acknowledge(offset, length);
205         if (fin) {
206             self.fin_acknowledged = true;
207             self.fin_lost = false;
208         }
209         self.releaseAcknowledged();
210         if (self.state != .data_sent) return;
211         if (!self.fin_acknowledged) return;
212         if (self.acknowledged != self.sent) return;
213         self.state = stream.sendTransition(.data_sent, .all_acknowledged) catch unreachable;
214     }
215 
216     /// Gives up the table entry a lost STREAM frame held, and puts its bytes, with a closing flag
217     /// no acknowledgment has covered, in the stretch that goes ahead of fresh bytes, so lost bytes
218     /// go out again ahead of new ones. A frame whose entry an acknowledgment or a reset has already
219     /// given up calls for nothing.
220     pub fn lose(self: *Send, offset: u62, length: u16, fin: bool) void {
221         const slot = self.findRange(offset, length, fin) orelse return;
222         std.debug.assert(offset >= self.acknowledged);
223         std.debug.assert(offset + length <= self.sent);
224         self.ranges[slot] = SentRange.empty();
225         self.lost.add(offset, length);
226         if (fin and !self.fin_acknowledged) self.fin_lost = true;
227     }
228 
229     /// Gives up on the bytes waiting, sent and unsent alike, and queues RESET_STREAM so an
230     /// application can abandon the stream, returning whether the reset was taken. What a reset
231     /// gives up on has no route back onto the wire. The call settles the final size at the highest
232     /// offset already sent. The call returns false on a second call, and false once the half holds
233     /// every acknowledgment or has sent its reset already.
234     pub fn reset(self: *Send, code: u62) bool {
235         if (self.reset_code != null) return false;
236         switch (self.state) {
237             .ready, .send, .data_sent => {},
238             .data_recvd, .reset_sent, .reset_recvd => return false,
239         }
240         self.reset_code = code;
241         self.reset_pending = true;
242         self.written = self.sent;
243         self.final_size = self.sent;
244         self.lost = .{};
245         self.fin_lost = false;
246         for (self.ranges) |*range| range.* = SentRange.empty();
247         self.releaseAcknowledged();
248         std.debug.assert(self.buffered() == 0);
249         return true;
250     }
251 
252     /// Hands back the RESET_STREAM frame that is waiting for a packet, so the send path can insert
253     /// that frame into the packet being built. That frame carries the error code, and the highest
254     /// offset sent as its final size. The call returns null when no reset waits.
255     pub fn resetFrame(self: *const Send, stream_id: u62) ?quic.frame.ResetStream {
256         if (!self.reset_pending) return null;
257         const code = self.reset_code.?;
258         return .{ .stream_id = stream_id, .error_code = code, .final_size = self.sent };
259     }
260 
261     /// Notes that a RESET_STREAM frame went out and carries the half into Reset Sent, for the send
262     /// path to call once the frame is in a sealed packet. When a loss has put one RESET_STREAM
263     /// frame out already, the half stands in Reset Sent as the copy goes, and stays there.
264     pub fn commitReset(self: *Send) void {
265         std.debug.assert(self.reset_pending);
266         self.reset_pending = false;
267         if (self.state == .reset_sent) return;
268         self.state = stream.sendTransition(self.state, .send_reset) catch unreachable;
269     }
270 
271     /// Puts RESET_STREAM back in the queue after a loss, while the half sits in Reset Sent, so a
272     /// lost RESET_STREAM goes out again. The copy carries the same code and final size, because
273     /// `resetFrame` reads both out of the same fields. Once an acknowledgment has carried the half
274     /// to Reset Recvd the peer knows, so nothing goes in the queue.
275     pub fn loseReset(self: *Send) void {
276         if (self.state != .reset_sent) return;
277         std.debug.assert(self.reset_code != null);
278         self.reset_pending = true;
279     }
280 
281     /// Notes the acknowledgment of the RESET_STREAM frame and carries the half into Reset Recvd, so
282     /// an acknowledged RESET_STREAM ends the sending part. Called from any state other than Reset
283     /// Sent, the call leaves state unchanged.
284     pub fn acknowledgeReset(self: *Send) void {
285         if (self.state != .reset_sent) return;
286         self.reset_pending = false;
287         self.state = stream.sendTransition(.reset_sent, .reset_acknowledged) catch unreachable;
288     }
289 
290     fn writable(self: *const Send) bool {
291         if (self.final_size != null) return false;
292         if (self.reset_code != null) return false;
293         std.debug.assert(self.state != .data_sent);
294         std.debug.assert(self.state != .data_recvd);
295         return true;
296     }
297 
298     fn lostChunk(self: *const Send, data_max: usize) ?Chunk {
299         std.debug.assert(!self.lost.isEmpty());
300         const offset = self.lost.start;
301         const index = self.ringIndex(offset);
302         const room: u64 = @min(data_max, self.bytes.len - index, std.math.maxInt(u16));
303         const count: usize = @intCast(@min(self.lost.length(), room));
304         if (count == 0) return null;
305         const end = offset + @as(u62, @intCast(count));
306         const fin = self.fin_lost and end == self.final_size.?;
307         return .{ .offset = offset, .data = self.bytes[index..][0..count], .fin = fin };
308     }
309 
310     fn lostFin(self: *const Send) Chunk {
311         std.debug.assert(self.fin_lost);
312         std.debug.assert(self.fin_sent);
313         std.debug.assert(self.final_size.? == self.sent);
314         const index = self.ringIndex(self.sent);
315         return .{ .offset = self.sent, .data = self.bytes[index..][0..0], .fin = true };
316     }
317 
318     fn commitLost(self: *Send, offset: u62, length: u16, fin: bool) void {
319         if (fin) {
320             std.debug.assert(self.fin_lost);
321             self.fin_lost = false;
322         }
323         if (length == 0) return;
324         std.debug.assert(offset == self.lost.start);
325         self.lost.advance(length);
326     }
327 
328     fn releaseAcknowledged(self: *Send) void {
329         var lowest = self.sent;
330         if (!self.lost.isEmpty()) lowest = @min(lowest, self.lost.start);
331         for (self.ranges) |range| {
332             if (range.outstanding and range.offset < lowest) lowest = range.offset;
333         }
334         std.debug.assert(lowest >= self.acknowledged);
335         const released: usize = @intCast(lowest - self.acknowledged);
336         std.debug.assert(released <= self.bytes.len);
337         self.head = (self.head + released) % self.bytes.len;
338         self.acknowledged = lowest;
339     }
340 
341     fn ringIndex(self: *const Send, offset: u62) usize {
342         std.debug.assert(offset >= self.acknowledged);
343         const distance: usize = @intCast(offset - self.acknowledged);
344         std.debug.assert(distance <= self.bytes.len);
345         return (self.head + distance) % self.bytes.len;
346     }
347 
348     fn freeRange(self: *const Send) ?usize {
349         for (self.ranges, 0..) |range, index| {
350             if (!range.outstanding) return index;
351         }
352         return null;
353     }
354 
355     fn findRange(self: *const Send, offset: u62, length: u16, fin: bool) ?usize {
356         for (self.ranges, 0..) |range, index| {
357             if (!range.outstanding) continue;
358             if (range.offset != offset) continue;
359             if (range.length == length and range.fin == fin) return index;
360         }
361         return null;
362     }
363 };
364 
365 /// Counts the bytes a STREAM frame spends ahead of `length` bytes of data, so the packet builder
366 /// knows the header size before determining how many data bytes fit. Those bytes carry the frame
367 /// type and the stream identifier always, the offset whenever it stands above zero, and the length
368 /// whenever the caller asks for one.
369 pub fn frameHeaderBytes(stream_id: u62, offset: u62, length: usize, length_present: bool) usize {
370     std.debug.assert(length <= std.math.maxInt(u62));
371     var bytes: usize = 1 + @as(usize, quic.varint.encodedLength(stream_id));
372     if (offset != 0) bytes += quic.varint.encodedLength(offset);
373     if (length_present) bytes += quic.varint.encodedLength(@intCast(length));
374     return bytes;
375 }
376 
377 /// Works out the most data a STREAM frame can carry inside `available` bytes, so the packet builder
378 /// turns the room left in a packet into a byte count it can ask the sending part for. The call
379 /// returns null when even a frame without data cannot fit. The length field widens as the data
380 /// grows, so the answer is settled in two passes.
381 pub fn frameDataCapacity(
382     stream_id: u62,
383     offset: u62,
384     available: usize,
385     length_present: bool,
386 ) ?usize {
387     const empty = frameHeaderBytes(stream_id, offset, 0, length_present);
388     if (empty > available) return null;
389     const first = available - empty;
390     const header = frameHeaderBytes(stream_id, offset, first, length_present);
391     if (header + first <= available) return first;
392     std.debug.assert(header <= available);
393     const second = available - header;
394     const second_header = frameHeaderBytes(stream_id, offset, second, length_present);
395     std.debug.assert(second_header + second <= available);
396     return second;
397 }
398 
399 /// Assembles the STREAM frame around one pick, so the packet builder turns the chunk into the frame
400 /// it encodes. The OFF bit goes in whenever the offset stands above zero.
401 pub fn frame(stream_id: u62, chunk: Chunk, length_present: bool) quic.frame.Frame {
402     return .{ .stream = .{
403         .stream_id = stream_id,
404         .offset = chunk.offset,
405         .offset_present = chunk.offset != 0,
406         .length_present = length_present,
407         .fin = chunk.fin,
408         .data = chunk.data,
409     } };
410 }
411 
412 fn commitNext(send: *Send, data_max: usize) !Chunk {
413     const chunk = send.nextChunk(data_max, std.math.maxInt(u62)) orelse return error.NoChunk;
414     _ = send.commitChunk(chunk.offset, @intCast(chunk.data.len), chunk.fin);
415     return chunk;
416 }
417 
418 test "RFC 9000 section 2.2 send buffer maximum and maximum plus one" {
419     var bytes: [8]u8 = undefined;
420     var ranges: [2]SentRange = undefined;
421     var send = Send.init(&bytes, &ranges, 64);
422     try std.testing.expectEqual(@as(usize, 8), send.write("abcdefgh"));
423     try std.testing.expectEqual(@as(usize, 0), send.write("i"));
424     try std.testing.expectEqual(@as(usize, 8), send.buffered());
425 }
426 
427 test "RFC 9000 section 4.1 chunks stop at stream and connection credit" {
428     var bytes: [8]u8 = undefined;
429     var ranges: [4]SentRange = undefined;
430     var send = Send.init(&bytes, &ranges, 3);
431     try std.testing.expectEqual(@as(usize, 6), send.write("abcdef"));
432     try std.testing.expectEqualStrings("abc", (try commitNext(&send, 64)).data);
433     try std.testing.expect(send.nextChunk(64, 64) == null);
434     try std.testing.expectEqual(@as(?u62, 3), send.blocked());
435     send.commitBlocked(3);
436     try std.testing.expectEqual(@as(?u62, null), send.blocked());
437     send.credit.raise(5);
438     const limited = send.nextChunk(64, 1) orelse return error.NoChunk;
439     try std.testing.expectEqualStrings("d", limited.data);
440     try std.testing.expect(send.nextChunk(64, 0) == null);
441 }
442 
443 test "RFC 9000 section 13.3 acknowledged STREAM frames release bytes in offset order" {
444     var bytes: [8]u8 = undefined;
445     var ranges: [4]SentRange = undefined;
446     var send = Send.init(&bytes, &ranges, 64);
447     _ = send.write("abcdefgh");
448     for (0..3) |_| _ = try commitNext(&send, 2);
449     send.acknowledge(2, 2, false);
450     try std.testing.expectEqual(@as(u62, 0), send.acknowledged);
451     try std.testing.expectEqual(@as(usize, 0), send.write("x"));
452     send.acknowledge(0, 2, false);
453     try std.testing.expectEqual(@as(u62, 4), send.acknowledged);
454     try std.testing.expectEqual(@as(usize, 4), send.write("wxyz"));
455     try std.testing.expectEqualStrings("gh", (try commitNext(&send, 8)).data);
456     try std.testing.expectEqualStrings("wxyz", (try commitNext(&send, 8)).data);
457     send.acknowledge(4, 2, false);
458     send.acknowledge(6, 2, false);
459     send.acknowledge(8, 4, false);
460     try std.testing.expectEqual(@as(usize, 0), send.buffered());
461 }
462 
463 test "RFC 9000 section 13.3 sent range records maximum and maximum plus one" {
464     var bytes: [8]u8 = undefined;
465     var ranges: [2]SentRange = undefined;
466     var send = Send.init(&bytes, &ranges, 64);
467     _ = send.write("abcd");
468     _ = try commitNext(&send, 1);
469     _ = try commitNext(&send, 1);
470     try std.testing.expect(send.nextChunk(1, 64) == null);
471     send.acknowledge(1, 1, false);
472     try std.testing.expectEqualStrings("c", (try commitNext(&send, 1)).data);
473 }
474 
475 test "RFC 9000 section 13.3 lost STREAM bytes go out again first without spending credit" {
476     var bytes: [8]u8 = undefined;
477     var ranges: [2]SentRange = undefined;
478     var send = Send.init(&bytes, &ranges, 8);
479     _ = send.write("abcdefgh");
480     _ = try commitNext(&send, 4);
481     _ = try commitNext(&send, 2);
482     send.lose(0, 4, false);
483     try std.testing.expectEqual(@as(u62, 0), send.nextOffset());
484     const resent = send.nextChunk(3, 0) orelse return error.NoChunk;
485     try std.testing.expectEqualStrings("abc", resent.data);
486     try std.testing.expectEqual(@as(u16, 0), send.commitChunk(0, 3, false));
487     try std.testing.expectEqual(@as(u62, 6), send.credit.used);
488     try std.testing.expect(send.nextChunk(8, 64) == null);
489     send.acknowledge(0, 3, false);
490     try std.testing.expectEqual(@as(u62, 3), send.acknowledged);
491     try std.testing.expectEqualStrings("d", (try commitNext(&send, 8)).data);
492     try std.testing.expectEqual(@as(u62, 6), send.nextOffset());
493     send.acknowledge(4, 2, false);
494     try std.testing.expectEqualStrings("gh", (try commitNext(&send, 8)).data);
495     try std.testing.expectEqual(@as(u62, 8), send.credit.used);
496     send.acknowledge(3, 1, false);
497     send.acknowledge(6, 2, false);
498     try std.testing.expectEqual(@as(usize, 0), send.buffered());
499 }
500 
501 test "RFC 9000 section 13.3 an acknowledged copy trims the lost bytes it delivered" {
502     var bytes: [8]u8 = undefined;
503     var ranges: [4]SentRange = undefined;
504     var send = Send.init(&bytes, &ranges, 64);
505     _ = send.write("abcdef");
506     for (0..3) |_| _ = try commitNext(&send, 2);
507     send.lose(0, 2, false);
508     send.lose(4, 2, false);
509     try std.testing.expectEqual(@as(u62, 6), send.lost.length());
510     try std.testing.expectEqualStrings("ab", (try commitNext(&send, 2)).data);
511     send.acknowledge(2, 2, false);
512     try std.testing.expectEqual(@as(u62, 4), send.nextOffset());
513     try std.testing.expectEqualStrings("ef", (try commitNext(&send, 8)).data);
514     try std.testing.expect(!send.pending());
515 }
516 
517 test "RFC 9000 section 13.3 a lost FIN goes out again alone or on the last lost byte" {
518     var bytes: [8]u8 = undefined;
519     var ranges: [2]SentRange = undefined;
520     var send = Send.init(&bytes, &ranges, 64);
521     _ = send.write("ab");
522     _ = try commitNext(&send, 8);
523     send.finish();
524     const fin_only = try commitNext(&send, 8);
525     try std.testing.expect(fin_only.fin);
526     try std.testing.expectEqual(@as(usize, 0), fin_only.data.len);
527     send.lose(2, 0, true);
528     try std.testing.expect(send.pending());
529     const alone = send.nextChunk(8, 0) orelse return error.NoChunk;
530     try std.testing.expect(alone.fin);
531     try std.testing.expectEqual(@as(u62, 2), alone.offset);
532     try std.testing.expectEqual(@as(usize, 0), alone.data.len);
533     try std.testing.expectEqual(@as(u16, 0), send.commitChunk(2, 0, true));
534     try std.testing.expect(!send.pending());
535     send.lose(0, 2, false);
536     send.lose(2, 0, true);
537     const last = send.nextChunk(8, 0) orelse return error.NoChunk;
538     try std.testing.expectEqualStrings("ab", last.data);
539     try std.testing.expect(last.fin);
540     try std.testing.expectEqual(@as(u16, 0), send.commitChunk(0, 2, true));
541     try std.testing.expect(!send.pending());
542     send.acknowledge(0, 2, true);
543     try std.testing.expectEqual(stream.SendState.data_recvd, send.state);
544     try std.testing.expectEqual(@as(usize, 0), send.buffered());
545 }
546 
547 test "RFC 9000 section 3.1 an acknowledged FIN reaches Data Recvd" {
548     var bytes: [8]u8 = undefined;
549     var ranges: [2]SentRange = undefined;
550     var send = Send.init(&bytes, &ranges, 64);
551     _ = send.write("ab");
552     send.finish();
553     try std.testing.expectEqual(@as(usize, 0), send.write("c"));
554     const chunk = try commitNext(&send, 8);
555     try std.testing.expect(chunk.fin);
556     try std.testing.expectEqual(stream.SendState.data_sent, send.state);
557     try std.testing.expect(!send.pending());
558     send.acknowledge(0, 2, true);
559     try std.testing.expectEqual(stream.SendState.data_recvd, send.state);
560     try std.testing.expectEqual(@as(usize, 0), send.buffered());
561 }
562 
563 test "RFC 9000 section 19.4 reset reports the sent final size and discards unsent bytes" {
564     var bytes: [8]u8 = undefined;
565     var ranges: [2]SentRange = undefined;
566     var send = Send.init(&bytes, &ranges, 64);
567     _ = send.write("abcdef");
568     _ = try commitNext(&send, 4);
569     try std.testing.expect(send.reset(0x11));
570     try std.testing.expect(!send.reset(0x12));
571     const reset_frame = send.resetFrame(0) orelse return error.NoReset;
572     try std.testing.expectEqual(@as(u62, 4), reset_frame.final_size);
573     try std.testing.expectEqual(@as(u62, 0x11), reset_frame.error_code);
574     try std.testing.expect(send.nextChunk(8, 64) == null);
575     try std.testing.expectEqual(@as(usize, 0), send.buffered());
576     try std.testing.expectEqual(@as(usize, 0), send.write("g"));
577     send.commitReset();
578     try std.testing.expectEqual(stream.SendState.reset_sent, send.state);
579     send.acknowledgeReset();
580     try std.testing.expectEqual(stream.SendState.reset_recvd, send.state);
581 }
582 
583 test "RFC 9000 section 13.3 a lost RESET_STREAM goes out again unchanged until acknowledged" {
584     var bytes: [8]u8 = undefined;
585     var ranges: [2]SentRange = undefined;
586     var send = Send.init(&bytes, &ranges, 64);
587     _ = send.write("abcdef");
588     _ = try commitNext(&send, 4);
589     send.lose(0, 4, false);
590     try std.testing.expect(send.reset(0x11));
591     try std.testing.expect(send.lost.isEmpty());
592     const first = send.resetFrame(0) orelse return error.NoReset;
593     send.commitReset();
594     send.lose(0, 4, false);
595     try std.testing.expect(!send.pending());
596     send.loseReset();
597     const again = send.resetFrame(0) orelse return error.NoReset;
598     try std.testing.expectEqual(first, again);
599     send.commitReset();
600     try std.testing.expectEqual(stream.SendState.reset_sent, send.state);
601     send.acknowledgeReset();
602     try std.testing.expectEqual(stream.SendState.reset_recvd, send.state);
603     send.loseReset();
604     try std.testing.expectEqual(@as(?quic.frame.ResetStream, null), send.resetFrame(0));
605 }
606 
607 test "RFC 9000 section 19.8 STREAM packing round trips every OFF, LEN, and FIN combination" {
608     const data = "stream-bytes";
609     for (0..8) |bits| {
610         const offset: u62 = if (bits & 4 != 0) 1_000 else 0;
611         const length_present = bits & 2 != 0;
612         const chunk = Chunk{ .offset = offset, .data = data, .fin = bits & 1 != 0 };
613         const available = frameHeaderBytes(4, offset, data.len, length_present) + data.len;
614         const capacity = frameDataCapacity(4, offset, available, length_present);
615         try std.testing.expectEqual(@as(?usize, data.len), capacity);
616         var packet: [32]u8 = undefined;
617         var output = quic.cursor.Write.init(packet[0..available]);
618         try quic.frame.encode(frame(4, chunk, length_present), &output);
619         try std.testing.expectEqual(available, output.index);
620         try std.testing.expectEqual(@as(u8, @intCast(0x08 | bits)), packet[0]);
621         var input = quic.cursor.Read.init(output.written());
622         const decoded = (try quic.frame.decode(&input)).stream;
623         try std.testing.expectEqual(@as(usize, 0), input.remaining());
624         try std.testing.expectEqual(@as(u62, 4), decoded.stream_id);
625         try std.testing.expectEqual(offset, decoded.offset);
626         try std.testing.expectEqual(chunk.fin, decoded.fin);
627         try std.testing.expectEqual(length_present, decoded.length_present);
628         try std.testing.expectEqualStrings(data, decoded.data);
629     }
630     try std.testing.expectEqual(@as(?usize, null), frameDataCapacity(4, 0, 1, false));
631     try std.testing.expectEqual(@as(?usize, 0), frameDataCapacity(4, 0, 2, false));
632     try std.testing.expectEqual(@as(?usize, 63), frameDataCapacity(4, 0, 66, true));
633     try std.testing.expectEqual(@as(?usize, 63), frameDataCapacity(4, 0, 67, true));
634 }