zig

fork of https://codeberg.org/ziglang/zig
Log | Files | Refs | README | LICENSE

commit d4a295a9de6c2ac0cb76fa0fcfd5162cbb7031d5 (tree)
parent d534cfa787cfa077b24e949b19749bd5c6e89a80
Author: Andrew Kelley <andrew@ziglang.org>
Date:   Fri, 29 May 2026 05:53:30 +0200

Merge pull request 'std.crypto.codecs.asn1: fix. compilation after IO changes and improve correctness' (#35326) from jedisct1/zig:asn1der into master

Reviewed-on: https://codeberg.org/ziglang/zig/pulls/35326
Reviewed-by: Andrew Kelley <andrew@ziglang.org>

Diffstat:
Mlib/std/crypto/codecs.zig | 6++++++
Mlib/std/crypto/codecs/asn1.zig | 133+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Mlib/std/crypto/codecs/asn1/Oid.zig | 4++--
Mlib/std/crypto/codecs/asn1/der.zig | 42++++++++++++++++++++++++++++++++++++++++++
Mlib/std/crypto/codecs/asn1/der/Decoder.zig | 57++++++++++++++++++++++++++++++++++++++++++++-------------
Mlib/std/crypto/codecs/asn1/der/Encoder.zig | 139+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
6 files changed, 291 insertions(+), 90 deletions(-)

diff --git a/lib/std/crypto/codecs.zig b/lib/std/crypto/codecs.zig @@ -1,3 +1,9 @@ pub const asn1 = @import("codecs/asn1.zig"); pub const base64 = @import("codecs/base64_hex_ct.zig").base64; pub const hex = @import("codecs/base64_hex_ct.zig").hex; + +test { + _ = asn1; + _ = base64; + _ = hex; +} diff --git a/lib/std/crypto/codecs/asn1.zig b/lib/std/crypto/codecs/asn1.zig @@ -71,16 +71,18 @@ pub const Tag = struct { pub fn decode(reader: *std.Io.Reader) !Tag { const tag1: FirstTag = @bitCast(try reader.takeByte()); - var number: u14 = tag1.number; - - if (tag1.number == 31) { - const tag2: NextTag = @bitCast(try reader.takeByte()); - number = tag2.number; - if (tag2.continues) { - const tag3: NextTag = @bitCast(try reader.takeByte()); - number = (number << 7) + tag3.number; - if (tag3.continues) return error.EndOfStream; - } + var number: std.meta.Tag(Tag.Number) = tag1.number; + + if (tag1.number == high_tag_marker) { + number = 0; + for (0..max_continuations) |i| { + const next: NextTag = @bitCast(try reader.takeByte()); + if (i == 0 and next.number == 0) return error.InvalidEncoding; + number = std.math.shlExact(@TypeOf(number), number, 7) catch return error.InvalidEncoding; + number |= next.number; + if (!next.continues) break; + } else return error.InvalidEncoding; + if (number < high_tag_marker) return error.InvalidEncoding; } return Tag{ @@ -90,40 +92,51 @@ pub const Tag = struct { }; } - pub fn encode(self: Tag, writer: *std.Io.Writer) @TypeOf(writer).Error!void { - var tag1 = FirstTag{ + pub fn encodeToSlice(self: Tag, buf: *[max_encoded_len]u8) []const u8 { + const n = @intFromEnum(self.number); + var tag1: FirstTag = .{ .number = undefined, .constructed = self.constructed, .class = self.class, }; - var buffer: [3]u8 = undefined; - var writer2: std.Io.Writer = .init(&buffer); + if (n < high_tag_marker) { + tag1.number = @intCast(n); + buf[0] = @bitCast(tag1); + return buf[0..1]; + } - switch (@intFromEnum(self.number)) { - 0...std.math.maxInt(u5) => |n| { - tag1.number = @intCast(n); - writer2.writeByte(@bitCast(tag1)) catch unreachable; - }, - std.math.maxInt(u5) + 1...std.math.maxInt(u7) => |n| { - tag1.number = 15; - const tag2 = NextTag{ .number = @intCast(n), .continues = false }; - writer2.writeByte(@bitCast(tag1)) catch unreachable; - writer2.writeByte(@bitCast(tag2)) catch unreachable; - }, - else => |n| { - tag1.number = 15; - const tag2 = NextTag{ .number = @intCast(n >> 7), .continues = true }; - const tag3 = NextTag{ .number = @truncate(n), .continues = false }; - writer2.writeByte(@bitCast(tag1)) catch unreachable; - writer2.writeByte(@bitCast(tag2)) catch unreachable; - writer2.writeByte(@bitCast(tag3)) catch unreachable; - }, + tag1.number = high_tag_marker; + buf[0] = @bitCast(tag1); + + const bits_used = @bitSizeOf(@TypeOf(n)) - @clz(n); + const len = std.math.divCeil(usize, bits_used, 7) catch unreachable; + + var remaining = n; + var i = len; + while (i > 0) : (i -= 1) { + buf[i] = @bitCast(NextTag{ + .number = @truncate(remaining), + .continues = i != len, + }); + remaining >>= 7; } + return buf[0 .. 1 + len]; + } - _ = try writer.write(writer2.buffered()); + pub fn encode(self: Tag, writer: *std.Io.Writer) std.Io.Writer.Error!void { + var buf: [max_encoded_len]u8 = undefined; + try writer.writeAll(self.encodeToSlice(&buf)); } + pub const max_encoded_len = 1 + (std.math.divCeil( + comptime_int, + @bitSizeOf(std.meta.Tag(Tag.Number)), + 7, + ) catch unreachable); + const max_continuations = max_encoded_len - 1; + const high_tag_marker = std.math.maxInt(u5); + const FirstTag = packed struct(u8) { number: u5, constructed: bool, class: Tag.Class }; const NextTag = packed struct(u8) { number: u7, continues: bool }; @@ -165,6 +178,42 @@ test Tag { try std.testing.expectEqual(Tag.init(@enumFromInt(3), true, .context_specific), t); } +test "Tag.encode produces the exact bytes from X.690" { + const cases = [_]struct { number: u16, expected: []const u8 }{ + .{ .number = 0, .expected = &.{0x00} }, + .{ .number = 30, .expected = &.{0x1e} }, + .{ .number = 31, .expected = &.{ 0x1f, 0x1f } }, + .{ .number = 127, .expected = &.{ 0x1f, 0x7f } }, + .{ .number = 128, .expected = &.{ 0x1f, 0x81, 0x00 } }, + .{ .number = 16383, .expected = &.{ 0x1f, 0xff, 0x7f } }, + .{ .number = 16384, .expected = &.{ 0x1f, 0x81, 0x80, 0x00 } }, + .{ .number = 65535, .expected = &.{ 0x1f, 0x83, 0xff, 0x7f } }, + }; + for (cases) |c| { + const tag = Tag.init(@enumFromInt(c.number), false, .universal); + var buf: [Tag.max_encoded_len]u8 = undefined; + try std.testing.expectEqualSlices(u8, c.expected, tag.encodeToSlice(&buf)); + } +} + +test "Tag.encode/decode round trip" { + for ([_]u16{ 0, 30, 31, 32, 127, 128, 16383, 16384, 65535 }) |n| { + const tag = Tag.init(@enumFromInt(n), false, .universal); + var buf: [Tag.max_encoded_len]u8 = undefined; + const encoded = tag.encodeToSlice(&buf); + var reader: std.Io.Reader = .fixed(encoded); + try std.testing.expectEqual(tag, try Tag.decode(&reader)); + try std.testing.expectEqual(encoded.len, reader.seek); + } +} + +test "Tag.decode rejects non-minimal high-tag form" { + for ([_][]const u8{ &.{ 0x1f, 0x1e }, &.{ 0x1f, 0x80, 0x01 } }) |bytes| { + var reader: std.Io.Reader = .fixed(bytes); + try std.testing.expectError(error.InvalidEncoding, Tag.decode(&reader)); + } +} + /// A decoded view. pub const Element = struct { tag: Tag, @@ -183,13 +232,14 @@ pub const Element = struct { } }; - pub const DecodeError = error{EndOfStream}; + pub const DecodeError = error{ EndOfStream, InvalidEncoding }; /// Safely decode a DER/BER/CER element at `index`: /// - Ensures length uses shortest form /// - Ensures length is within `bytes` /// - Ensures length is less than `std.math.maxInt(Index)` pub fn decode(bytes: []const u8, index: Index) DecodeError!Element { + if (index > bytes.len) return error.EndOfStream; var reader: std.Io.Reader = .fixed(bytes[index..]); const tag = Tag.decode(&reader) catch |err| switch (err) { @@ -327,13 +377,22 @@ pub const BitString = struct { } pub fn encodeDer(self: BitString, encoder: *der.Encoder) !void { - try encoder.writer().writeAll(self.bytes); - try encoder.writer().writeByte(self.right_padding); + try encoder.prependBytes(self.bytes); + try encoder.prependBytes(&.{self.right_padding}); try encoder.length(self.bytes.len + 1); try encoder.tag(asn1_tag); } }; +test BitString { + const bs = BitString{ .bytes = &.{ 0x6e, 0x5d, 0xc0 }, .right_padding = 6 }; + const allocator = std.testing.allocator; + const buf = try der.encode(allocator, bs); + defer allocator.free(buf); + try std.testing.expectEqualSlices(u8, &.{ 0x03, 0x04, 0x06, 0x6e, 0x5d, 0xc0 }, buf); + try std.testing.expectEqualDeep(bs, try der.decode(BitString, buf)); +} + pub fn Opaque(comptime tag: Tag) type { return struct { bytes: []const u8, diff --git a/lib/std/crypto/codecs/asn1/Oid.zig b/lib/std/crypto/codecs/asn1/Oid.zig @@ -47,7 +47,7 @@ test fromDot { } } -pub fn toDot(self: Oid, writer: anytype) @TypeOf(writer).Error!void { +pub fn toDot(self: Oid, writer: *std.Io.Writer) std.Io.Writer.Error!void { const encoded = self.encoded; const first = @divTrunc(encoded[0], 40); const second = encoded[0] - first * 40; @@ -81,7 +81,7 @@ test toDot { for (test_cases) |t| { var stream: std.Io.Writer = .fixed(&buf); try toDot(Oid{ .encoded = t.encoded }, &stream); - try std.testing.expectEqualStrings(t.dot_notation, stream.written()); + try std.testing.expectEqualStrings(t.dot_notation, stream.buffered()); } } diff --git a/lib/std/crypto/codecs/asn1/der.zig b/lib/std/crypto/codecs/asn1/der.zig @@ -49,6 +49,48 @@ test decode { try std.testing.expectEqualDeep(test_case.value, decoded); } +test "integer round trip across signed and unsigned boundaries" { + const allocator = std.testing.allocator; + inline for (.{ u8, u16, u32, i8, i16, i32 }) |T| { + const cases = comptime blk: { + const min = std.math.minInt(T); + const max = std.math.maxInt(T); + break :blk [_]T{ 0, 1, max, min, @divTrunc(max, 2), @divTrunc(min, 2) }; + }; + for (cases) |value| { + const buf = try encode(allocator, value); + defer allocator.free(buf); + const decoded = try decode(T, buf); + try std.testing.expectEqual(value, decoded); + } + } +} + +test "encode skips null optional fields" { + const Value = struct { a: ?u8, b: u8 }; + const allocator = std.testing.allocator; + const actual = try encode(allocator, Value{ .a = null, .b = 5 }); + defer allocator.free(actual); + + try std.testing.expectEqualSlices(u8, &.{ 0x30, 0x03, 0x02, 0x01, 0x05 }, actual); +} + +test "encode preserves outer sequence tag after implicit field tags" { + const Value = struct { + a: u8, + b: u8, + + pub const asn1_tags = .{ + .a = asn1.FieldTag.initImplicit(0, .context_specific), + }; + }; + const allocator = std.testing.allocator; + const actual = try encode(allocator, Value{ .a = 1, .b = 2 }); + defer allocator.free(actual); + + try std.testing.expectEqualSlices(u8, &.{ 0x30, 0x06, 0x80, 0x01, 0x01, 0x02, 0x01, 0x02 }, actual); +} + test { _ = Decoder; _ = Encoder; diff --git a/lib/std/crypto/codecs/asn1/der/Decoder.zig b/lib/std/crypto/codecs/asn1/der/Decoder.zig @@ -111,21 +111,33 @@ pub fn view(self: Decoder, elem: Element) []const u8 { } fn int(comptime T: type, value: []const u8) error{ NonCanonical, LargeValue }!T { - if (@typeInfo(T).int.bits % 8 != 0) @compileError("T must be byte aligned"); - - var bytes = value; - if (bytes.len >= 2) { - if (bytes[0] == 0) { - if (@clz(bytes[1]) > 0) return error.NonCanonical; - bytes.ptr += 1; - } - if (bytes[0] == 0xff and @clz(bytes[1]) == 0) return error.NonCanonical; + const info = @typeInfo(T).int; + if (info.bits % 8 != 0) @compileError("T must be byte aligned"); + + if (value.len == 0) return error.NonCanonical; + if (value.len >= 2) { + if (value[0] == 0x00 and value[1] & 0x80 == 0) return error.NonCanonical; + if (value[0] == 0xff and value[1] & 0x80 != 0) return error.NonCanonical; } - if (bytes.len > @sizeOf(T)) return error.LargeValue; - if (@sizeOf(T) == 1) return @bitCast(bytes[0]); + const had_sign_byte = value.len >= 2 and value[0] == 0x00; + const bytes = if (had_sign_byte) value[1..] else value; + const der_negative = !had_sign_byte and bytes[0] & 0x80 != 0; + + switch (info.signedness) { + .unsigned => { + if (der_negative) return error.LargeValue; + if (bytes.len > @sizeOf(T)) return error.LargeValue; + }, + .signed => { + const max_len: usize = if (had_sign_byte) @sizeOf(T) - 1 else @sizeOf(T); + if (bytes.len > max_len) return error.LargeValue; + }, + } - return std.mem.readVarInt(T, bytes, .big); + var buf: [@sizeOf(T)]u8 = @splat(if (der_negative) 0xff else 0); + @memcpy(buf[buf.len - bytes.len ..], bytes); + return std.mem.readInt(T, &buf, .big); } test int { @@ -135,7 +147,26 @@ test int { const big = [_]u8{ 0xef, 0xff }; try expectError(error.LargeValue, int(u8, &big)); - try expectEqual(0xefff, int(u16, &big)); + try expectError(error.LargeValue, int(u16, &big)); + try expectEqual(@as(i16, -4097), try int(i16, &big)); + + try expectEqual(@as(u16, 255), try int(u16, &.{ 0x00, 0xff })); + try expectEqual(@as(u16, 0x8000), try int(u16, &.{ 0x00, 0x80, 0x00 })); + + try expectEqual(@as(i8, -1), try int(i8, &.{0xff})); + try expectEqual(@as(i16, -1), try int(i16, &.{0xff})); + try expectEqual(@as(i16, -128), try int(i16, &.{0x80})); + try expectEqual(@as(i16, -129), try int(i16, &.{ 0xff, 0x7f })); + try expectEqual(@as(i16, 255), try int(i16, &.{ 0x00, 0xff })); + try expectEqual(@as(i32, 0x7fffffff), try int(i32, &.{ 0x7f, 0xff, 0xff, 0xff })); + + try expectError(error.LargeValue, int(i8, &.{ 0x00, 0xff })); + try expectError(error.LargeValue, int(i16, &.{ 0x00, 0x80, 0x00 })); + try expectError(error.LargeValue, int(i32, &.{ 0x00, 0x80, 0x00, 0x00, 0x00 })); + + try expectError(error.LargeValue, int(u8, &.{0xff})); + try expectError(error.LargeValue, int(u16, &.{0x80})); + try expectError(error.LargeValue, int(u32, &.{ 0x80, 0x00, 0x00, 0x00 })); } test Decoder { diff --git a/lib/std/crypto/codecs/asn1/der/Encoder.zig b/lib/std/crypto/codecs/asn1/der/Encoder.zig @@ -24,6 +24,7 @@ pub fn any(self: *Encoder, val: anytype) !void { fn anyTag(self: *Encoder, tag_: Tag, val: anytype) !void { const T = @TypeOf(val); if (std.meta.hasFn(T, "encodeDer")) return try val.encodeDer(self); + const outer_field_tag = self.field_tag; const start = self.buffer.data.len; const merged_tag = self.mergedTag(tag_); @@ -42,8 +43,9 @@ fn anyTag(self: *Encoder, tag_: Tag, val: anytype) !void { const is_default = if (f_attrs.@"comptime") false else if (f_attrs.defaultValue(f_type)) |default_val| brk: { break :brk std.mem.eql(u8, std.mem.asBytes(&default_val), std.mem.asBytes(&field_val)); } else false; + const is_null_optional = if (@typeInfo(f_type) == .optional) field_val == null else false; - if (!is_default) { + if (!is_default and !is_null_optional) { const start2 = self.buffer.data.len; self.field_tag = field_tag; // will merge with self.field_tag. @@ -58,6 +60,7 @@ fn anyTag(self: *Encoder, tag_: Tag, val: anytype) !void { } } } + self.field_tag = outer_field_tag; }, .bool => try self.buffer.prependSlice(&[_]u8{if (val) 0xff else 0}), .int => try self.int(T, val), @@ -68,7 +71,7 @@ fn anyTag(self: *Encoder, tag_: Tag, val: anytype) !void { try self.int(e.tag_type, @intFromEnum(val)); } }, - .optional => if (val) |v| return try self.anyTag(tag_, v), + .optional => if (val) |v| return try self.anyTag(tag_, v) else return, .null => {}, else => @compileError("cannot encode type " ++ @typeName(T)), } @@ -80,7 +83,8 @@ fn anyTag(self: *Encoder, tag_: Tag, val: anytype) !void { /// Encode a tag. pub fn tag(self: *Encoder, tag_: Tag) !void { const t = self.mergedTag(tag_); - try t.encode(self.writer()); + var buf: [Tag.max_encoded_len]u8 = undefined; + try self.buffer.prependSlice(t.encodeToSlice(&buf)); } fn mergedTag(self: *Encoder, tag_: Tag) Tag { @@ -96,19 +100,14 @@ fn mergedTag(self: *Encoder, tag_: Tag) Tag { /// Encode a length. pub fn length(self: *Encoder, len: usize) !void { - const writer_ = self.writer(); - if (len < 128) { - try writer_.writeInt(u8, @intCast(len), .big); - return; - } - inline for ([_]type{ u8, u16, u32 }) |T| { - if (len < std.math.maxInt(T)) { - try writer_.writeInt(T, @intCast(len), .big); - try writer_.writeInt(u8, @sizeOf(T) | 0x80, .big); - return; - } - } - return error.InvalidLength; + if (len < 128) return self.buffer.prependSlice(&.{@intCast(len)}); + const len32 = std.math.cast(u32, len) orelse return error.InvalidLength; + var buf: [@sizeOf(u32) + 1]u8 = undefined; + std.mem.writeInt(u32, buf[1..], len32, .big); + var first: usize = 1; + while (buf[first] == 0) first += 1; + buf[first - 1] = @intCast((buf.len - first) | 0x80); + return self.buffer.prependSlice(buf[first - 1 ..]); } /// Encode a tag and length-prefixed bytes. @@ -118,28 +117,23 @@ pub fn tagBytes(self: *Encoder, tag_: Tag, bytes: []const u8) !void { try self.tag(tag_); } -/// Warning: This writer writes backwards. `fn print` will NOT work as expected. -pub fn writer(self: *Encoder) ArrayListReverse.Writer { - return self.buffer.writer(); +/// Write raw bytes. The encoder builds its output back-to-front, so chained +/// calls should be made in reverse of the desired on-wire order. +pub fn prependBytes(self: *Encoder, bytes: []const u8) !void { + return self.buffer.prependSlice(bytes); } fn int(self: *Encoder, comptime T: type, value: T) !void { - const big = std.mem.nativeTo(T, value, .big); - const big_bytes = std.mem.asBytes(&big); - - const bits_needed = @bitSizeOf(T) - @clz(value); - const needs_padding: u1 = if (value == 0) - 1 - else if (bits_needed > 8) brk: { - const RightShift = @Int(.unsigned, @bitSizeOf(@TypeOf(bits_needed)) - 1); - const right_shift: RightShift = @intCast(bits_needed - 9); - break :brk if (value >> right_shift == 0x1ff) 1 else 0; - } else 0; - const bytes_needed = try std.math.divCeil(usize, bits_needed, 8) + needs_padding; - - const writer_ = self.writer(); - for (0..bytes_needed - needs_padding) |i| try writer_.writeByte(big_bytes[big_bytes.len - i - 1]); - if (needs_padding == 1) try writer_.writeByte(0); + const info = @typeInfo(T).int; + const Unsigned = @Int(.unsigned, info.bits); + const pad: u8 = if (info.signedness == .signed and value < 0) 0xff else 0; + var buf: [@sizeOf(Unsigned) + 1]u8 = undefined; + buf[0] = pad; + std.mem.writeInt(Unsigned, buf[1..], @bitCast(value), .big); + + var first: usize = 0; + while (first + 1 < buf.len and buf[first] == pad and (buf[first + 1] ^ pad) & 0x80 == 0) first += 1; + try self.buffer.prependSlice(buf[first..]); } test int { @@ -148,15 +142,84 @@ test int { defer encoder.deinit(); try encoder.int(u8, 0); - try std.testing.expectEqualSlices(u8, &[_]u8{0}, encoder.buffer.data); + try std.testing.expectEqualSlices(u8, &.{0}, encoder.buffer.data); encoder.buffer.clearAndFree(); try encoder.int(u16, 0x00ff); - try std.testing.expectEqualSlices(u8, &[_]u8{0xff}, encoder.buffer.data); + try std.testing.expectEqualSlices(u8, &.{ 0, 0xff }, encoder.buffer.data); encoder.buffer.clearAndFree(); try encoder.int(u32, 0xffff); - try std.testing.expectEqualSlices(u8, &[_]u8{ 0, 0xff, 0xff }, encoder.buffer.data); + try std.testing.expectEqualSlices(u8, &.{ 0, 0xff, 0xff }, encoder.buffer.data); + + encoder.buffer.clearAndFree(); + try encoder.int(u32, 0x01020304); + try std.testing.expectEqualSlices(u8, &.{ 0x01, 0x02, 0x03, 0x04 }, encoder.buffer.data); + + encoder.buffer.clearAndFree(); + try encoder.int(u8, 127); + try std.testing.expectEqualSlices(u8, &.{0x7f}, encoder.buffer.data); + + encoder.buffer.clearAndFree(); + try encoder.int(u16, 128); + try std.testing.expectEqualSlices(u8, &.{ 0, 0x80 }, encoder.buffer.data); + + encoder.buffer.clearAndFree(); + try encoder.int(u16, 256); + try std.testing.expectEqualSlices(u8, &.{ 0x01, 0x00 }, encoder.buffer.data); + + encoder.buffer.clearAndFree(); + try encoder.int(u8, 128); + try std.testing.expectEqualSlices(u8, &.{ 0, 0x80 }, encoder.buffer.data); + + encoder.buffer.clearAndFree(); + try encoder.int(u8, 255); + try std.testing.expectEqualSlices(u8, &.{ 0, 0xff }, encoder.buffer.data); + + encoder.buffer.clearAndFree(); + try encoder.int(u16, 0x8000); + try std.testing.expectEqualSlices(u8, &.{ 0, 0x80, 0 }, encoder.buffer.data); + + encoder.buffer.clearAndFree(); + try encoder.int(i8, -1); + try std.testing.expectEqualSlices(u8, &.{0xff}, encoder.buffer.data); + + encoder.buffer.clearAndFree(); + try encoder.int(i8, -128); + try std.testing.expectEqualSlices(u8, &.{0x80}, encoder.buffer.data); + + encoder.buffer.clearAndFree(); + try encoder.int(i16, -129); + try std.testing.expectEqualSlices(u8, &.{ 0xff, 0x7f }, encoder.buffer.data); +} + +test length { + const allocator = std.testing.allocator; + var encoder = Encoder.init(allocator); + defer encoder.deinit(); + + try encoder.length(127); + try std.testing.expectEqualSlices(u8, &.{0x7f}, encoder.buffer.data); + + encoder.buffer.clearAndFree(); + try encoder.length(128); + try std.testing.expectEqualSlices(u8, &.{ 0x81, 0x80 }, encoder.buffer.data); + + encoder.buffer.clearAndFree(); + try encoder.length(255); + try std.testing.expectEqualSlices(u8, &.{ 0x81, 0xff }, encoder.buffer.data); + + encoder.buffer.clearAndFree(); + try encoder.length(256); + try std.testing.expectEqualSlices(u8, &.{ 0x82, 0x01, 0x00 }, encoder.buffer.data); + + encoder.buffer.clearAndFree(); + try encoder.length(65535); + try std.testing.expectEqualSlices(u8, &.{ 0x82, 0xff, 0xff }, encoder.buffer.data); + + encoder.buffer.clearAndFree(); + try encoder.length(65536); + try std.testing.expectEqualSlices(u8, &.{ 0x83, 0x01, 0x00, 0x00 }, encoder.buffer.data); } const std = @import("std");