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:
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");