lib/quic/src/connection/storage.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const alloc_phase = @import("alloc_phase");
  2 const std = @import("std");
  3 const quic = @import("../root.zig");
  4 
  5 pub const key_slot_count: usize = 8;
  6 
  7 pub const KeySlot = enum(u3) {
  8     initial_write,
  9     initial_read,
 10     handshake_write,
 11     handshake_read,
 12     application_write,
 13     application_read_previous,
 14     application_read_current,
 15     application_read_next,
 16 };
 17 
 18 const StorageLimits = quic.connection.Limits;
 19 
 20 const CapacityDeriveError = error{
 21     CapacityOverflow,
 22     LimitsEmpty,
 23     DatagramTooSmall,
 24     DatagramTooLarge,
 25     RangeLimitExceeded,
 26 };
 27 
 28 /// Capacity works out, from the limits alone, where each buffer sits in the block and how many
 29 /// bytes the block needs in total. A caller derives one to learn how large a byte array to hand
 30 /// `Storage.init`. Its `storage_bytes` field is the figure a caller sizes its array by. Derivation
 31 /// fails with `LimitsEmpty`, `RangeLimitExceeded`, `DatagramTooSmall`, `DatagramTooLarge`, or
 32 /// `CapacityOverflow`, which puts invalid limits out before any storage is touched. The layout
 33 /// covers the sent records and received ranges of all three spaces, the TLS storage, the CRYPTO
 34 /// lanes, the packet, scratch, and reason bytes, the eight key slots, the encoded transport
 35 /// parameters, and the stream 0 buffers and tables.
 36 pub const Capacity = struct {
 37     sent_offset: usize,
 38     sent_count: usize,
 39     sent_bytes: usize,
 40     ranges_offset: usize,
 41     range_count: usize,
 42     range_bytes: usize,
 43     tls_offset: usize,
 44     tls_bytes: usize,
 45     crypto_offset: usize,
 46     crypto_lane_bytes: usize,
 47     packet_offset: usize,
 48     scratch_offset: usize,
 49     reason_offset: usize,
 50     keys_offset: usize,
 51     keys_bytes: usize,
 52     transport_offset: usize,
 53     transport_bytes: usize,
 54     stream_sent_offset: usize,
 55     stream_range_offset: usize,
 56     stream_send_offset: usize,
 57     stream_receive_offset: usize,
 58     storage_bytes: usize,
 59 
 60     pub const DeriveError: type = CapacityDeriveError;
 61 
 62     pub fn derive(limits: StorageLimits) DeriveError!Capacity {
 63         try validateLimits(limits);
 64         const sent_count = try multiplied(limits.sent_records, 3);
 65         const sent = try placed(quic.connection.SentRecord, 0, sent_count);
 66         const range_count = try multiplied(limits.received_ranges, 3);
 67         const ranges = try placed(quic.connection.Range, sent.end, range_count);
 68         const tls_offset = try aligned(ranges.end, quic.tls.Storage.storage_alignment);
 69         const tls_capacity = quic.tls.Capacity.derive(.{
 70             .max_message = limits.tls_message_max,
 71         }) catch return error.CapacityOverflow;
 72         const crypto_offset = try added(tls_offset, tls_capacity.storage_bytes);
 73         const crypto_lane_bytes: usize = limits.crypto_buffer_bytes;
 74         const crypto_bytes = try multiplied(crypto_lane_bytes, 6);
 75         const packet_offset = try added(crypto_offset, crypto_bytes);
 76         const scratch_offset = try added(packet_offset, limits.datagram_bytes);
 77         const reason_offset = try added(scratch_offset, limits.datagram_bytes);
 78         const reason_end = try added(reason_offset, limits.datagram_bytes);
 79         const keys_offset = try aligned(reason_end, quic.crypto.Keys.storage_alignment);
 80         const keys_bytes = try multiplied(key_slot_count, quic.crypto.Keys.storage_bytes_max);
 81         const transport_offset = try added(keys_offset, keys_bytes);
 82         const transport_end = try added(transport_offset, limits.tls_message_max);
 83         const streams = try placeStreams(limits, transport_end);
 84         return .{
 85             .sent_offset = sent.start,
 86             .sent_count = sent_count,
 87             .sent_bytes = sent.bytes,
 88             .ranges_offset = ranges.start,
 89             .range_count = range_count,
 90             .range_bytes = ranges.bytes,
 91             .tls_offset = tls_offset,
 92             .tls_bytes = tls_capacity.storage_bytes,
 93             .crypto_offset = crypto_offset,
 94             .crypto_lane_bytes = crypto_lane_bytes,
 95             .packet_offset = packet_offset,
 96             .scratch_offset = scratch_offset,
 97             .reason_offset = reason_offset,
 98             .keys_offset = keys_offset,
 99             .keys_bytes = keys_bytes,
100             .transport_offset = transport_offset,
101             .transport_bytes = limits.tls_message_max,
102             .stream_sent_offset = streams.sent.start,
103             .stream_range_offset = streams.ranges.start,
104             .stream_send_offset = streams.send.start,
105             .stream_receive_offset = streams.receive.start,
106             .storage_bytes = streams.receive.end,
107         };
108     }
109 };
110 
111 const StorageCapacity = Capacity;
112 
113 const Region = struct { start: usize, bytes: usize, end: usize };
114 
115 const StreamRegions = struct { sent: Region, ranges: Region, send: Region, receive: Region };
116 
117 fn validateLimits(limits: StorageLimits) Capacity.DeriveError!void {
118     if (limits.tls_message_max == 0) return error.LimitsEmpty;
119     if (limits.crypto_buffer_bytes == 0) return error.LimitsEmpty;
120     if (limits.sent_records == 0) return error.LimitsEmpty;
121     if (limits.received_ranges == 0) return error.LimitsEmpty;
122     if (limits.stream_send_bytes == 0) return error.LimitsEmpty;
123     if (limits.stream_receive_bytes == 0) return error.LimitsEmpty;
124     if (limits.stream_receive_ranges == 0) return error.LimitsEmpty;
125     if (limits.stream_sent_ranges == 0) return error.LimitsEmpty;
126     if (limits.received_ranges > quic.frame.ack_ranges_max) {
127         return error.RangeLimitExceeded;
128     }
129     if (limits.datagram_bytes < 1200) return error.DatagramTooSmall;
130     if (limits.datagram_bytes > quic.crypto.packet.packet_bytes_max) {
131         return error.DatagramTooLarge;
132     }
133 }
134 
135 fn added(left: anytype, right: anytype) Capacity.DeriveError!usize {
136     const bounded_left: usize = @intCast(left);
137     const bounded_right: usize = @intCast(right);
138     return std.math.add(usize, bounded_left, bounded_right) catch error.CapacityOverflow;
139 }
140 
141 fn multiplied(left: anytype, right: anytype) Capacity.DeriveError!usize {
142     const bounded_left: usize = @intCast(left);
143     const bounded_right: usize = @intCast(right);
144     return std.math.mul(usize, bounded_left, bounded_right) catch error.CapacityOverflow;
145 }
146 
147 fn aligned(value: usize, alignment: usize) Capacity.DeriveError!usize {
148     const mask = alignment - 1;
149     return (try added(value, mask)) & ~mask;
150 }
151 
152 fn placed(comptime T: type, offset: usize, count: usize) Capacity.DeriveError!Region {
153     const start = try aligned(offset, @alignOf(T));
154     const bytes = try multiplied(count, @sizeOf(T));
155     return .{ .start = start, .bytes = bytes, .end = try added(start, bytes) };
156 }
157 
158 fn placeStreams(limits: StorageLimits, offset: usize) Capacity.DeriveError!StreamRegions {
159     const sent = try placed(quic.connection.stream.SentRange, offset, limits.stream_sent_ranges);
160     const ranges = try placed(quic.connection.stream.Range, sent.end, limits.stream_receive_ranges);
161     const send = try placed(u8, ranges.end, limits.stream_send_bytes);
162     const receive = try placed(u8, send.end, limits.stream_receive_bytes);
163     return .{ .sent = sent, .ranges = ranges, .send = send, .receive = receive };
164 }
165 
166 /// Storage cuts the caller's block into every typed buffer one connection works out of. A caller
167 /// sets one up over its own bytes and hands a pointer to `Connection.init`, which keeps that
168 /// pointer. The block and the connection both have to stay put for the connection's life, because
169 /// the connection and the key state each hold a pointer to it. A block shorter than the derived
170 /// total is refused with `StorageTooShort`, and nothing else happens. `activate` moves it from
171 /// setup into steady use, and `Connection.init` does that once the connection stands. `deinit`
172 /// wipes every byte and hands the block back, and it requires that the connection has already
173 /// released the TLS storage nested inside. `records`, `ranges`, and `keys` hand out the per-space
174 /// and per-slot views of the block.
175 pub const Storage = struct {
176     phase: alloc_phase.capacity.Phase,
177     capacity: StorageCapacity,
178     storage: []align(quic.tls.Storage.storage_alignment) u8,
179     sent_records: []quic.connection.SentRecord,
180     received_ranges: []quic.connection.Range,
181     tls_storage: quic.tls.Storage,
182     crypto_bytes: [3][]u8,
183     crypto_present: [3][]u8,
184     packet: []u8,
185     scratch: []u8,
186     reason: []u8,
187     key_bytes: []u8,
188     transport: []u8,
189     stream_sent_ranges: []quic.connection.stream.SentRange,
190     stream_receive_ranges: []quic.connection.stream.Range,
191     stream_send_bytes: []u8,
192     stream_receive_bytes: []u8,
193     nested_released: bool = false,
194 
195     pub const storage_alignment: usize = quic.tls.Storage.storage_alignment;
196     pub const Storage = []align(storage_alignment) u8;
197     pub const Limits: type = StorageLimits;
198     pub const Capacity: type = StorageCapacity;
199     pub const InitError = StorageCapacity.DeriveError || quic.tls.Storage.InitError ||
200         error{StorageTooShort};
201     pub const work_limits: alloc_phase.capacity.WorkLimits = .{
202         .transition_steps_max = 1,
203         .cleanup_steps_per_call_max = 0,
204         .cleanup_calls_at_capacity_max = 0,
205     };
206 
207     pub const claim: alloc_phase.capacity.Declaration = .{
208         .source = .{
209             .id = "quic.connection_storage",
210             .kind = .startup_static,
211             .limit_source = .caller,
212             .storage = .{
213                 .covered = &.{
214                     .{
215                         .id = "packet_spaces",
216                         .lifetime = .steady,
217                         .detail = "three bounded sent-record and received-range lanes",
218                     },
219                     .{
220                         .id = "crypto_and_tls",
221                         .lifetime = .steady,
222                         .detail = "TLS storage, CRYPTO reassembly, and packet protection keys",
223                     },
224                     .{
225                         .id = "datagram_work",
226                         .lifetime = .steady,
227                         .detail = "packet input, open scratch, and close reason bytes",
228                     },
229                     .{
230                         .id = "stream_buffers",
231                         .lifetime = .steady,
232                         .detail = "stream 0 send and receive rings with sent and received ranges",
233                     },
234                 },
235                 .excluded = &.{
236                     "caller configuration slices and identity key storage",
237                     "caller datagram input and output slices",
238                     "standard cryptographic primitive stack storage",
239                 },
240             },
241             .capacity = .{
242                 .inputs = &.{
243                     alloc_phase.capacity.bindInput(
244                         StorageLimits,
245                         "tls_message_max",
246                         "tls_message_max",
247                     ),
248                     alloc_phase.capacity.bindInput(
249                         StorageLimits,
250                         "crypto_buffer_bytes",
251                         "crypto_buffer_bytes",
252                     ),
253                     alloc_phase.capacity.bindInput(
254                         StorageLimits,
255                         "sent_records",
256                         "sent_records",
257                     ),
258                     alloc_phase.capacity.bindInput(
259                         StorageLimits,
260                         "received_ranges",
261                         "received_ranges",
262                     ),
263                     alloc_phase.capacity.bindInput(
264                         StorageLimits,
265                         "datagram_bytes",
266                         "datagram_bytes",
267                     ),
268                     alloc_phase.capacity.bindInput(
269                         StorageLimits,
270                         "stream_send_bytes",
271                         "stream_send_bytes",
272                     ),
273                     alloc_phase.capacity.bindInput(
274                         StorageLimits,
275                         "stream_receive_bytes",
276                         "stream_receive_bytes",
277                     ),
278                     alloc_phase.capacity.bindInput(
279                         StorageLimits,
280                         "stream_receive_ranges",
281                         "stream_receive_ranges",
282                     ),
283                     alloc_phase.capacity.bindInput(
284                         StorageLimits,
285                         "stream_sent_ranges",
286                         "stream_sent_ranges",
287                     ),
288                 },
289                 .type_selectors = &.{
290                     alloc_phase.capacity.bindType(
291                         quic.connection.SentRecord,
292                         "sent_record",
293                     ),
294                     alloc_phase.capacity.bindType(
295                         quic.connection.Range,
296                         "received_range",
297                     ),
298                     alloc_phase.capacity.bindType(
299                         quic.connection.stream.SentRange,
300                         "stream_sent_range",
301                     ),
302                     alloc_phase.capacity.bindType(
303                         quic.connection.stream.Range,
304                         "stream_received_range",
305                     ),
306                 },
307                 .nodes = &.{
308                     .{ .input = 2 },
309                     .{ .scale = .{ .node = 0, .coefficient = .{ .literal = 3 } } },
310                     .{ .scale = .{
311                         .node = 1,
312                         .coefficient = .{ .size_of_concrete_type = 0 },
313                     } },
314                     .{ .alignment = .{
315                         .node = 2,
316                         .alignment = .{ .concrete_type = 1 },
317                     } },
318                     .{ .input = 3 },
319                     .{ .scale = .{ .node = 4, .coefficient = .{ .literal = 3 } } },
320                     .{ .scale = .{
321                         .node = 5,
322                         .coefficient = .{ .size_of_concrete_type = 1 },
323                     } },
324                     .{ .add = .{ .left = 3, .right = 6 } },
325                     .{ .alignment = .{ .node = 7, .alignment = .{ .literal = 16 } } },
326                     .{ .input = 0 },
327                     .{ .scale = .{ .node = 9, .coefficient = .{ .literal = 8 } } },
328                     .{ .constant = 28 },
329                     .{ .add = .{ .left = 10, .right = 11 } },
330                     .{ .add = .{ .left = 8, .right = 12 } },
331                     .{ .input = 1 },
332                     .{ .scale = .{ .node = 14, .coefficient = .{ .literal = 6 } } },
333                     .{ .add = .{ .left = 13, .right = 15 } },
334                     .{ .input = 4 },
335                     .{ .scale = .{ .node = 17, .coefficient = .{ .literal = 3 } } },
336                     .{ .add = .{ .left = 16, .right = 18 } },
337                     .{ .constant = 608 },
338                     .{ .add = .{ .left = 19, .right = 20 } },
339                     .{ .add = .{ .left = 21, .right = 9 } },
340                     .{ .alignment = .{
341                         .node = 22,
342                         .alignment = .{ .concrete_type = 2 },
343                     } },
344                     .{ .input = 8 },
345                     .{ .scale = .{
346                         .node = 24,
347                         .coefficient = .{ .size_of_concrete_type = 2 },
348                     } },
349                     .{ .add = .{ .left = 23, .right = 25 } },
350                     .{ .alignment = .{
351                         .node = 26,
352                         .alignment = .{ .concrete_type = 3 },
353                     } },
354                     .{ .input = 7 },
355                     .{ .scale = .{
356                         .node = 28,
357                         .coefficient = .{ .size_of_concrete_type = 3 },
358                     } },
359                     .{ .add = .{ .left = 27, .right = 29 } },
360                     .{ .input = 5 },
361                     .{ .add = .{ .left = 30, .right = 31 } },
362                     .{ .input = 6 },
363                     .{ .add = .{ .left = 32, .right = 33 } },
364                 },
365                 .assertions = &.{.{
366                     .scope = .closure_total,
367                     .measure = .retained,
368                     .relation = .exact,
369                     .expression = 34,
370                 }},
371             },
372             .overload = .{
373                 .kind = .reject_before_seal,
374                 .detail = "invalid limits and short caller storage reject before activation",
375             },
376             .risks = .{
377                 .transitive = .{
378                     .status = .witnessed,
379                     .detail = "packet, frame, TLS, and crypto paths retain only provisioned slices",
380                 },
381                 .foreign = .{
382                     .status = .excluded,
383                     .detail = "the sans-I/O connection crosses no socket or foreign boundary",
384                 },
385             },
386             .work = .{
387                 .equation = "each operation scans caller limits or one datagram at most",
388             },
389             .obligations = &.{
390                 .{ .key = "quic_connection_capacity", .role = .capacity_model },
391                 .{ .key = "quic_connection_overload", .role = .overload },
392                 .{ .key = "quic_connection_transitive", .role = .transitive_risk },
393                 .{ .key = "quic_connection_work", .role = .work_bound },
394                 .{ .key = "quic_connection_root", .role = .custom },
395             },
396         },
397         .bindings = .{
398             .owner = @This(),
399             .seal = .{
400                 .family = alloc_phase.capacity.selector(@This().activate),
401                 .premise = .{ .class = .checked_semantic_fact, .authority = .checker },
402             },
403             .teardown = .{
404                 .family = alloc_phase.capacity.selector(@This().deinit),
405                 .premise = .{ .class = .checked_semantic_fact, .authority = .checker },
406             },
407         },
408     };
409 
410     pub fn init(bytes: @This().Storage, limits: StorageLimits) InitError!@This() {
411         const capacity = try StorageCapacity.derive(limits);
412         if (bytes.len < capacity.storage_bytes) return error.StorageTooShort;
413         const owned = bytes[0..capacity.storage_bytes];
414         const sent_slice = typedSlice(
415             quic.connection.SentRecord,
416             owned,
417             capacity.sent_offset,
418             capacity.sent_count,
419         );
420         const range_slice = typedSlice(
421             quic.connection.Range,
422             owned,
423             capacity.ranges_offset,
424             capacity.range_count,
425         );
426         const stream_sent_slice = typedSlice(
427             quic.connection.stream.SentRange,
428             owned,
429             capacity.stream_sent_offset,
430             limits.stream_sent_ranges,
431         );
432         const stream_range_slice = typedSlice(
433             quic.connection.stream.Range,
434             owned,
435             capacity.stream_range_offset,
436             limits.stream_receive_ranges,
437         );
438         const send_start = capacity.stream_send_offset;
439         const receive_start = capacity.stream_receive_offset;
440         const tls_bytes: []align(quic.tls.Storage.storage_alignment) u8 = @alignCast(
441             owned[capacity.tls_offset..][0..capacity.tls_bytes],
442         );
443         var result = @This(){
444             .phase = .initialization,
445             .capacity = capacity,
446             .storage = owned,
447             .sent_records = sent_slice,
448             .received_ranges = range_slice,
449             .tls_storage = try quic.tls.Storage.init(tls_bytes, .{
450                 .max_message = limits.tls_message_max,
451             }),
452             .crypto_bytes = undefined,
453             .crypto_present = undefined,
454             .packet = owned[capacity.packet_offset..][0..limits.datagram_bytes],
455             .scratch = owned[capacity.scratch_offset..][0..limits.datagram_bytes],
456             .reason = owned[capacity.reason_offset..][0..limits.datagram_bytes],
457             .key_bytes = owned[capacity.keys_offset..][0..capacity.keys_bytes],
458             .transport = owned[capacity.transport_offset..][0..capacity.transport_bytes],
459             .stream_sent_ranges = stream_sent_slice,
460             .stream_receive_ranges = stream_range_slice,
461             .stream_send_bytes = owned[send_start..][0..limits.stream_send_bytes],
462             .stream_receive_bytes = owned[receive_start..][0..limits.stream_receive_bytes],
463         };
464         result.partitionCrypto();
465         return result;
466     }
467 
468     pub fn activate(self: *@This()) void {
469         std.debug.assert(self.phase == .initialization);
470         self.phase = .steady;
471     }
472 
473     pub fn deinit(self: *@This()) @This().Storage {
474         std.debug.assert(self.phase == .steady);
475         std.debug.assert(self.nested_released);
476         std.crypto.secureZero(u8, self.storage);
477         self.phase = .teardown;
478         const bytes = self.storage;
479         self.* = undefined;
480         return bytes;
481     }
482 
483     pub fn records(self: *@This(), index: usize) []quic.connection.SentRecord {
484         std.debug.assert(index < 3);
485         const width = self.capacity.sent_count / 3;
486         return self.sent_records[index * width ..][0..width];
487     }
488 
489     pub fn ranges(self: *@This(), index: usize) []quic.connection.Range {
490         std.debug.assert(index < 3);
491         const width = self.capacity.range_count / 3;
492         return self.received_ranges[index * width ..][0..width];
493     }
494 
495     pub fn keys(self: *@This(), slot: KeySlot) quic.crypto.Keys.Storage {
496         const width = quic.crypto.Keys.storage_bytes_max;
497         const start = @as(usize, @backingInt(slot)) * width;
498         return @alignCast(self.key_bytes[start..][0..width]);
499     }
500 
501     fn partitionCrypto(self: *@This()) void {
502         const width = self.capacity.crypto_lane_bytes;
503         for (0..3) |index| {
504             const data_start = self.capacity.crypto_offset + index * width;
505             const present_start = self.capacity.crypto_offset + (3 + index) * width;
506             self.crypto_bytes[index] = self.storage[data_start..][0..width];
507             self.crypto_present[index] = self.storage[present_start..][0..width];
508         }
509     }
510 };
511 
512 fn typedSlice(
513     comptime T: type,
514     bytes: []u8,
515     offset: usize,
516     count: usize,
517 ) []T {
518     const byte_count = count * @sizeOf(T);
519     const region: []align(@alignOf(T)) u8 = @alignCast(bytes[offset..][0..byte_count]);
520     return std.mem.bytesAsSlice(T, region);
521 }
522 
523 comptime {
524     std.debug.assert(key_slot_count * quic.crypto.Keys.storage_bytes_max == 608);
525 }
526 
527 comptime {
528     alloc_phase.capacity.requireProvisionedExactOwnerShape(Storage);
529 }
530 
531 test "connection storage accepts exact capacity and rejects one byte less" {
532     comptime {
533         @stardustClaim(
534             alloc_phase.capacity.witness(Storage, "quic_connection_capacity"),
535             null,
536             null,
537             null,
538             null,
539             null,
540             null,
541         );
542         @stardustClaim(
543             alloc_phase.capacity.witness(Storage, "quic_connection_overload"),
544             null,
545             null,
546             null,
547             null,
548             null,
549             null,
550         );
551         @stardustClaim(
552             alloc_phase.capacity.witness(Storage, "quic_connection_transitive"),
553             null,
554             null,
555             null,
556             null,
557             null,
558             null,
559         );
560         @stardustClaim(
561             alloc_phase.capacity.witness(Storage, "quic_connection_work"),
562             null,
563             null,
564             null,
565             null,
566             null,
567             null,
568         );
569         @stardustClaim(
570             alloc_phase.capacity.witness(Storage, "quic_connection_root"),
571             null,
572             null,
573             null,
574             null,
575             null,
576             null,
577         );
578     }
579     const limits = comptime testLimits();
580     const capacity = comptime Capacity.derive(limits) catch unreachable;
581     var bytes: [capacity.storage_bytes]u8 align(Storage.storage_alignment) = undefined;
582     try std.testing.expectError(
583         error.StorageTooShort,
584         Storage.init(@alignCast(bytes[0 .. bytes.len - 1]), limits),
585     );
586     var storage = try Storage.init(&bytes, limits);
587     storage.tls_storage.activate();
588     _ = storage.tls_storage.deinit();
589     storage.nested_released = true;
590     storage.activate();
591     _ = storage.deinit();
592 }
593 
594 test "RFC 9000 section 19.3 received ACK range capacity maximum and maximum plus one" {
595     var limits = testLimits();
596     limits.received_ranges = quic.frame.ack_ranges_max;
597     _ = try Capacity.derive(limits);
598     limits.received_ranges += 1;
599     try std.testing.expectError(error.RangeLimitExceeded, Capacity.derive(limits));
600 }
601 
602 test "RFC 9000 section 18.2 datagram capacity maximum and maximum plus one" {
603     var limits = testLimits();
604     limits.datagram_bytes = 65_527;
605     _ = try Capacity.derive(limits);
606     limits.datagram_bytes += 1;
607     try std.testing.expectError(error.DatagramTooLarge, Capacity.derive(limits));
608 }
609 
610 test "connection storage rejects empty stream limits" {
611     const base = testLimits();
612     _ = try Capacity.derive(base);
613     var limits = base;
614     limits.stream_send_bytes = 0;
615     try std.testing.expectError(error.LimitsEmpty, Capacity.derive(limits));
616     limits = base;
617     limits.stream_receive_bytes = 0;
618     try std.testing.expectError(error.LimitsEmpty, Capacity.derive(limits));
619     limits = base;
620     limits.stream_receive_ranges = 0;
621     try std.testing.expectError(error.LimitsEmpty, Capacity.derive(limits));
622     limits = base;
623     limits.stream_sent_ranges = 0;
624     try std.testing.expectError(error.LimitsEmpty, Capacity.derive(limits));
625 }
626 
627 fn testLimits() StorageLimits {
628     return .{
629         .tls_message_max = 256,
630         .crypto_buffer_bytes = 512,
631         .sent_records = 4,
632         .received_ranges = 4,
633         .datagram_bytes = 1200,
634         .stream_send_bytes = 64,
635         .stream_receive_bytes = 64,
636         .stream_receive_ranges = 4,
637         .stream_sent_ranges = 4,
638     };
639 }