atproto utils for zig zat.dev
atproto sdk zig
26
fork

Configure Feed

Select the types of activity you want to include in your feed.

merge calabro.io/zat: CBOR/CAR/MST robustness + 214 new tests

Merges Jim Calabro's comprehensive correctness improvements:

- non-minimal CBOR encoding rejection (RFC 8949)
- UTF-8 validation on text strings
- map key sort order + duplicate key rejection
- safe integer arithmetic throughout (std.math.add/cast)
- errdefer on all decode allocations
- low-level zero-copy CBOR read/write API
- MST tests ported from atmos, delete rebalancing fix
- CAR v1 validation gaps, readUvarint overflow fix
- ECDSA signature verification overflow fix
- firehose smoke test against live production data
- CBOR codec benchmarks

decode+verify throughput: 290k → 202k fps (still 13x faster than Go).
the regression is from correctness checks that were missing before.

also: bump zig to 0.16.0-dev.3070, update CI, fix tangled.org URLs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+7367 -346
+3
.gitignore
··· 6 6 7 7 # Wisp docs build output (generated) 8 8 site-out/ 9 + 10 + # Plans 11 + docs/
+2 -2
.tangled/workflows/ci.yml
··· 13 13 steps: 14 14 - name: install zig 0.16 15 15 command: | 16 - curl -sSL https://ziglang.org/builds/zig-x86_64-linux-0.16.0-dev.3059+42e33db9d.tar.xz | tar -xJ -C /tangled/workspace 17 - mv /tangled/workspace/zig-x86_64-linux-0.16.0-dev.3059+42e33db9d /tangled/workspace/.zig 16 + curl -sSL https://ziglang.org/builds/zig-x86_64-linux-0.16.0-dev.3070+b22eb176b.tar.xz | tar -xJ -C /tangled/workspace 17 + mv /tangled/workspace/zig-x86_64-linux-0.16.0-dev.3070+b22eb176b /tangled/workspace/.zig 18 18 19 19 - name: check formatting 20 20 command: |
+2 -2
.tangled/workflows/publish-docs.yml
··· 13 13 steps: 14 14 - name: install zig 0.16 15 15 command: | 16 - curl -sSL https://ziglang.org/builds/zig-x86_64-linux-0.16.0-dev.3059+42e33db9d.tar.xz | tar -xJ -C /tangled/workspace 17 - mv /tangled/workspace/zig-x86_64-linux-0.16.0-dev.3059+42e33db9d /tangled/workspace/.zig 16 + curl -sSL https://ziglang.org/builds/zig-x86_64-linux-0.16.0-dev.3070+b22eb176b.tar.xz | tar -xJ -C /tangled/workspace 17 + mv /tangled/workspace/zig-x86_64-linux-0.16.0-dev.3070+b22eb176b /tangled/workspace/.zig 18 18 19 19 - name: build and publish docs to ATProto 20 20 command: |
+3 -3
README.md
··· 16 16 requires zig 0.16+. 17 17 18 18 ```bash 19 - zig fetch --save https://tangled.sh/zat.dev/zat/archive/main 19 + zig fetch --save https://tangled.org/zat.dev/zat/archive/main 20 20 ``` 21 21 22 22 then in `build.zig`: ··· 273 273 274 274 ## benchmarks 275 275 276 - zat is benchmarked against Go (indigo), Rust (rsky), and Python (atproto) in [atproto-bench](https://tangled.sh/@zzstoatzz.io/atproto-bench): 276 + zat is benchmarked against Go (indigo), Rust (rsky), and Python (atproto) in [atproto-bench](https://tangled.org/zzstoatzz.io/atproto-bench): 277 277 278 - - **decode**: 290k frames/sec (zig) vs 39k (rust) vs 15k (go) — with CID hash verification 278 + - **decode**: 202k frames/sec (zig) vs 39k (rust) vs 15k (go) — with CID hash verification and full CBOR validation 279 279 - **sig-verify**: 15k–19k verifies/sec across all three — ECDSA is table stakes 280 280 - **trust chain**: full repo verification in ~300ms compute (zig) vs ~410ms (go) vs ~422ms (rust) 281 281
+32
build.zig
··· 77 77 const smoke_step = b.step("smoke", "run jetstream smoke test"); 78 78 smoke_step.dependOn(&run_smoke.step); 79 79 80 + // firehose smoke test (CBOR + CAR + CID on live data) 81 + const firehose_smoke = b.addExecutable(.{ 82 + .name = "firehose-smoke", 83 + .root_module = b.createModule(.{ 84 + .root_source_file = b.path("scripts/firehose_smoke.zig"), 85 + .target = target, 86 + .optimize = optimize, 87 + .link_libc = true, 88 + .imports = &.{.{ .name = "zat", .module = mod }}, 89 + }), 90 + }); 91 + b.installArtifact(firehose_smoke); 92 + 93 + const run_firehose_smoke = b.addRunArtifact(firehose_smoke); 94 + const firehose_smoke_step = b.step("firehose-smoke", "run firehose smoke test (CBOR/CAR/CID on live data)"); 95 + firehose_smoke_step.dependOn(&run_firehose_smoke.step); 96 + 97 + // CBOR codec benchmarks 98 + const cbor_bench = b.addExecutable(.{ 99 + .name = "cbor-bench", 100 + .root_module = b.createModule(.{ 101 + .root_source_file = b.path("src/internal/repo/cbor_bench.zig"), 102 + .target = target, 103 + .optimize = optimize, 104 + }), 105 + }); 106 + b.installArtifact(cbor_bench); 107 + 108 + const run_bench = b.addRunArtifact(cbor_bench); 109 + const bench_step = b.step("bench", "run CBOR codec benchmarks"); 110 + bench_step.dependOn(&run_bench.step); 111 + 80 112 // publish-docs script (uses zat to publish docs to ATProto) 81 113 const publish_docs = b.addExecutable(.{ 82 114 .name = "publish-docs",
+1 -1
build.zig.zon
··· 2 2 .name = .zat, 3 3 .version = "0.3.0-alpha.20", 4 4 .fingerprint = 0x8da9db57ee82fbe4, 5 - .minimum_zig_version = "0.16.0", 5 + .minimum_zig_version = "0.16.0-dev.3070+b22eb176b", 6 6 .dependencies = .{ 7 7 .websocket = .{ 8 8 .url = "https://github.com/zzstoatzz/websocket.zig/archive/9ac64da.tar.gz",
+5 -1
justfile
··· 14 14 15 15 # run tests 16 16 test: 17 - zig build test 17 + zig build test --summary all -freference-trace 18 + 19 + # run CBOR codec benchmarks 20 + bench: 21 + zig build bench -Doptimize=ReleaseFast
+71
scripts/firehose_smoke.zig
··· 1 + //! firehose smoke test — connects to the live AT Protocol firehose, 2 + //! decodes CBOR frames, parses CAR blocks, and verifies CIDs. 3 + //! exercises the full CBOR → CAR → CID pipeline on production data. 4 + //! 5 + //! run: just firehose-smoke 6 + 7 + const std = @import("std"); 8 + const zat = @import("zat"); 9 + 10 + pub fn main() !void { 11 + var da: std.heap.DebugAllocator(.{}) = .init; 12 + defer _ = da.deinit(); 13 + const allocator = da.allocator(); 14 + 15 + std.debug.print("firehose smoke test starting (CBOR + CAR + CID on live data)\n", .{}); 16 + 17 + var handler = Handler{}; 18 + var client = zat.FirehoseClient.init(std.Options.debug_io, allocator, .{}); 19 + try client.subscribe(&handler); 20 + } 21 + 22 + const Handler = struct { 23 + count: u64 = 0, 24 + commits: u64 = 0, 25 + records: u64 = 0, 26 + connects: u64 = 0, 27 + errors: u64 = 0, 28 + 29 + pub fn onEvent(self: *Handler, event: zat.FirehoseEvent) void { 30 + self.count += 1; 31 + 32 + switch (event) { 33 + .commit => |commit| { 34 + self.commits += 1; 35 + for (commit.ops) |op| { 36 + if (op.record) |record| { 37 + // record was decoded from CAR blocks via CBOR 38 + _ = record.getString("$type"); 39 + self.records += 1; 40 + } 41 + } 42 + }, 43 + else => {}, 44 + } 45 + 46 + if (self.count % 1000 == 0) { 47 + std.debug.print(" [{d}] commits={d} records={d} errors={d}\n", .{ 48 + self.count, self.commits, self.records, self.errors, 49 + }); 50 + } 51 + 52 + // stop after 10k events 53 + if (self.count >= 10000) { 54 + std.debug.print("\nfirehose smoke test PASSED\n", .{}); 55 + std.debug.print(" {d} events, {d} commits, {d} records decoded, {d} errors\n", .{ 56 + self.count, self.commits, self.records, self.errors, 57 + }); 58 + std.process.exit(0); 59 + } 60 + } 61 + 62 + pub fn onConnect(self: *Handler, host: []const u8) void { 63 + self.connects += 1; 64 + std.debug.print("CONNECT #{d} to {s}\n", .{ self.connects, host }); 65 + } 66 + 67 + pub fn onError(self: *Handler, err: anyerror) void { 68 + self.errors += 1; 69 + std.debug.print("ERROR: {s}\n", .{@errorName(err)}); 70 + } 71 + };
+5 -2
src/internal/crypto/jwt.zig
··· 288 288 /// verify an ECDSA signature, rejecting high-S 289 289 fn verifyEcdsa(comptime Scheme: type, comptime half_order: [32]u8, message: []const u8, sig_bytes: []const u8, public_key_raw: []const u8) !void { 290 290 if (sig_bytes.len != 64) return error.InvalidSignature; 291 - const sig = Scheme.Signature.fromBytes(sig_bytes[0..64].*); 291 + 292 + // reject high-S before constructing Signature — fromBytes does scalar 293 + // arithmetic that can overflow on out-of-range values 294 + rejectHighS(half_order, sig_bytes[32..64].*) catch return error.SignatureVerificationFailed; 292 295 293 - rejectHighS(half_order, sig.s) catch return error.SignatureVerificationFailed; 296 + const sig = Scheme.Signature.fromBytes(sig_bytes[0..64].*); 294 297 295 298 if (public_key_raw.len != 33) return error.InvalidPublicKey; 296 299 const public_key = Scheme.PublicKey.fromSec1(public_key_raw) catch return error.InvalidPublicKey;
+38 -59
src/internal/repo/car.zig
··· 37 37 BadBlockHash, 38 38 BlocksTooLarge, 39 39 TooManyBlocks, 40 + BlockTooLarge, 40 41 }; 41 42 42 43 /// match indigo's safety limits 43 44 const max_blocks_size: usize = 2 * 1024 * 1024; // 2 MB 44 45 const max_block_count: usize = 10_000; 46 + const max_block_size: usize = 1024 * 1024; // 1 MB per block (matches atmos) 45 47 46 48 pub const ReadOptions = struct { 47 49 /// verify that each block's content hashes to its CID. ··· 71 73 const header_end = pos + header_len_usize; 72 74 if (header_end > data.len) return error.UnexpectedEof; 73 75 74 - // decode header (DAG-CBOR map with "version" and "roots") 76 + // decode header using a temporary arena (the header's Value tree is only 77 + // needed to extract version + roots, then discarded). this avoids leaking 78 + // the header's CBOR allocations if a later allocation fails. 79 + var header_arena = std.heap.ArenaAllocator.init(allocator); 80 + defer header_arena.deinit(); 75 81 const header_bytes = data[pos..header_end]; 76 - const header = cbor.decodeAll(allocator, header_bytes) catch return error.InvalidHeader; 82 + const header = cbor.decodeAll(header_arena.allocator(), header_bytes) catch return error.InvalidHeader; 77 83 78 - // extract roots (array of CID links) 84 + // validate version == 1 85 + const version = header.getUint("version") orelse return error.InvalidHeader; 86 + if (version != 1) return error.InvalidHeader; 87 + 88 + // extract roots (array of CID links) — CAR v1 requires at least one root. 89 + // CID.raw slices point into the input `data` (zero-copy), so they outlive 90 + // the header arena. 79 91 var roots: std.ArrayList(cbor.Cid) = .empty; 80 - if (header.getArray("roots")) |root_values| { 81 - for (root_values) |root_val| { 82 - switch (root_val) { 83 - .cid => |c| try roots.append(allocator, c), 84 - else => {}, 85 - } 92 + errdefer roots.deinit(allocator); 93 + const root_values = header.getArray("roots") orelse return error.InvalidHeader; 94 + for (root_values) |root_val| { 95 + switch (root_val) { 96 + .cid => |c| try roots.append(allocator, c), 97 + else => return error.InvalidHeader, // roots must all be CID links 86 98 } 87 99 } 100 + if (roots.items.len == 0) return error.InvalidHeader; 88 101 89 102 pos = header_end; 90 103 91 104 // read blocks 92 105 var blocks: std.ArrayList(Block) = .empty; 106 + errdefer blocks.deinit(allocator); 93 107 var block_index: std.StringHashMapUnmanaged([]const u8) = .empty; 108 + errdefer block_index.deinit(allocator); 94 109 95 110 while (pos < data.len) { 96 111 // block: [varint total_len] [CID bytes] [data bytes] 97 112 // total_len includes both CID and data 98 113 const block_len = cbor.readUvarint(data, &pos) orelse return error.InvalidVarint; 99 114 const block_len_usize = std.math.cast(usize, block_len) orelse return error.InvalidHeader; 115 + if (block_len_usize == 0) return error.InvalidCid; // zero-length block has no CID 116 + if (block_len_usize > max_block_size) return error.BlockTooLarge; 100 117 const block_end = pos + block_len_usize; 101 118 if (block_end > data.len) return error.UnexpectedEof; 102 119 ··· 255 272 defer arena.deinit(); 256 273 const alloc = arena.allocator(); 257 274 258 - // construct a minimal CAR v1 file: 259 - // header: DAG-CBOR {"version": 1, "roots": []} 260 - const header_cbor = [_]u8{ 261 - 0xa2, // map(2) 262 - 0x67, 'v', 'e', 'r', 's', 'i', 'o', 'n', 0x01, // "version": 1 263 - 0x65, 'r', 'o', 'o', 't', 's', 0x80, // "roots": [] 264 - }; 275 + // create a block and compute its real CID 276 + const block_content = try cbor.encodeAlloc(alloc, .{ .map = &.{ 277 + .{ .key = "text", .value = .{ .text = "hi" } }, 278 + } }); 279 + const block_cid = try cbor.Cid.forDagCbor(alloc, block_content); 265 280 266 - // one block: CIDv1 (dag-cbor, sha2-256) + CBOR data 267 - const cid_prefix = [_]u8{ 268 - 0x01, // version 269 - 0x71, // dag-cbor 270 - 0x12, // sha2-256 271 - 0x20, // 32-byte digest 281 + // write a proper CAR via the writer, then read it back 282 + const original = Car{ 283 + .roots = &.{block_cid}, 284 + .blocks = &.{.{ .cid_raw = block_cid.raw, .data = block_content }}, 272 285 }; 273 - const digest = [_]u8{0xaa} ** 32; 274 - const block_content = [_]u8{ 275 - 0xa1, // map(1) 276 - 0x64, 't', 'e', 'x', 't', // "text" 277 - 0x62, 'h', 'i', // "hi" 278 - }; 279 - 280 - // assemble the CAR file 281 - var car_buf: [256]u8 = undefined; 282 - var car_pos: usize = 0; 283 - 284 - // header length varint 285 - car_buf[car_pos] = @intCast(header_cbor.len); 286 - car_pos += 1; 287 - 288 - // header 289 - @memcpy(car_buf[car_pos..][0..header_cbor.len], &header_cbor); 290 - car_pos += header_cbor.len; 286 + const car_bytes = try writeAlloc(alloc, original); 287 + const car_file = try read(alloc, car_bytes); 291 288 292 - // block length varint (CID + content) 293 - const block_total_len = cid_prefix.len + digest.len + block_content.len; 294 - car_buf[car_pos] = @intCast(block_total_len); 295 - car_pos += 1; 296 - 297 - // CID 298 - @memcpy(car_buf[car_pos..][0..cid_prefix.len], &cid_prefix); 299 - car_pos += cid_prefix.len; 300 - @memcpy(car_buf[car_pos..][0..digest.len], &digest); 301 - car_pos += digest.len; 302 - 303 - // block content 304 - @memcpy(car_buf[car_pos..][0..block_content.len], &block_content); 305 - car_pos += block_content.len; 306 - 307 - // this test uses a fake digest, so skip verification 308 - const car_file = try readWithOptions(alloc, car_buf[0..car_pos], .{ .verify_block_hashes = false }); 309 - 289 + try std.testing.expectEqual(@as(usize, 1), car_file.roots.len); 310 290 try std.testing.expectEqual(@as(usize, 1), car_file.blocks.len); 311 - try std.testing.expectEqual(@as(usize, block_content.len), car_file.blocks[0].data.len); 312 291 313 292 // decode the block content as CBOR 314 293 const val = try cbor.decodeAll(alloc, car_file.blocks[0].data);
+330
src/internal/repo/car_test.zig
··· 1 + //! additional CAR v1 codec tests ported from atmos (Go implementation). 2 + //! 3 + //! focuses on error paths, validation, and edge cases not covered 4 + //! by the inline tests in car.zig. 5 + 6 + const std = @import("std"); 7 + const car = @import("car.zig"); 8 + const cbor = @import("cbor.zig"); 9 + 10 + // === header validation === 11 + 12 + test "reject CAR with version 0" { 13 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 14 + defer arena.deinit(); 15 + const alloc = arena.allocator(); 16 + 17 + // build header with version: 0 18 + const root_cid = try cbor.Cid.forDagCbor(alloc, "data"); 19 + const header: cbor.Value = .{ .map = &.{ 20 + .{ .key = "roots", .value = .{ .array = &.{.{ .cid = root_cid }} } }, 21 + .{ .key = "version", .value = .{ .unsigned = 0 } }, 22 + } }; 23 + const header_bytes = try cbor.encodeAlloc(alloc, header); 24 + 25 + var car_aw: std.Io.Writer.Allocating = .init(alloc); 26 + try cbor.writeUvarint(&car_aw.writer, header_bytes.len); 27 + try car_aw.writer.writeAll(header_bytes); 28 + 29 + try std.testing.expectError(error.InvalidHeader, car.read(alloc, car_aw.written())); 30 + } 31 + 32 + test "reject CAR with version 2" { 33 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 34 + defer arena.deinit(); 35 + const alloc = arena.allocator(); 36 + 37 + const root_cid = try cbor.Cid.forDagCbor(alloc, "data"); 38 + const header: cbor.Value = .{ .map = &.{ 39 + .{ .key = "roots", .value = .{ .array = &.{.{ .cid = root_cid }} } }, 40 + .{ .key = "version", .value = .{ .unsigned = 2 } }, 41 + } }; 42 + const header_bytes = try cbor.encodeAlloc(alloc, header); 43 + 44 + var car_aw: std.Io.Writer.Allocating = .init(alloc); 45 + try cbor.writeUvarint(&car_aw.writer, header_bytes.len); 46 + try car_aw.writer.writeAll(header_bytes); 47 + 48 + try std.testing.expectError(error.InvalidHeader, car.read(alloc, car_aw.written())); 49 + } 50 + 51 + test "reject CAR with missing version field" { 52 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 53 + defer arena.deinit(); 54 + const alloc = arena.allocator(); 55 + 56 + const root_cid = try cbor.Cid.forDagCbor(alloc, "data"); 57 + // header with roots but no version 58 + const header: cbor.Value = .{ .map = &.{ 59 + .{ .key = "roots", .value = .{ .array = &.{.{ .cid = root_cid }} } }, 60 + } }; 61 + const header_bytes = try cbor.encodeAlloc(alloc, header); 62 + 63 + var car_aw: std.Io.Writer.Allocating = .init(alloc); 64 + try cbor.writeUvarint(&car_aw.writer, header_bytes.len); 65 + try car_aw.writer.writeAll(header_bytes); 66 + 67 + try std.testing.expectError(error.InvalidHeader, car.read(alloc, car_aw.written())); 68 + } 69 + 70 + test "reject CAR with missing roots field" { 71 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 72 + defer arena.deinit(); 73 + const alloc = arena.allocator(); 74 + 75 + // header with version but no roots 76 + const header: cbor.Value = .{ .map = &.{ 77 + .{ .key = "version", .value = .{ .unsigned = 1 } }, 78 + } }; 79 + const header_bytes = try cbor.encodeAlloc(alloc, header); 80 + 81 + var car_aw: std.Io.Writer.Allocating = .init(alloc); 82 + try cbor.writeUvarint(&car_aw.writer, header_bytes.len); 83 + try car_aw.writer.writeAll(header_bytes); 84 + 85 + try std.testing.expectError(error.InvalidHeader, car.read(alloc, car_aw.written())); 86 + } 87 + 88 + test "reject CAR with empty roots array" { 89 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 90 + defer arena.deinit(); 91 + const alloc = arena.allocator(); 92 + 93 + // header with empty roots: [] 94 + const header: cbor.Value = .{ .map = &.{ 95 + .{ .key = "roots", .value = .{ .array = &.{} } }, 96 + .{ .key = "version", .value = .{ .unsigned = 1 } }, 97 + } }; 98 + const header_bytes = try cbor.encodeAlloc(alloc, header); 99 + 100 + var car_aw: std.Io.Writer.Allocating = .init(alloc); 101 + try cbor.writeUvarint(&car_aw.writer, header_bytes.len); 102 + try car_aw.writer.writeAll(header_bytes); 103 + 104 + try std.testing.expectError(error.InvalidHeader, car.read(alloc, car_aw.written())); 105 + } 106 + 107 + // === input edge cases === 108 + 109 + test "reject empty input" { 110 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 111 + defer arena.deinit(); 112 + try std.testing.expectError(error.InvalidVarint, car.read(arena.allocator(), &.{})); 113 + } 114 + 115 + test "reject truncated header varint" { 116 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 117 + defer arena.deinit(); 118 + // 0x80 = continuation byte, needs more data 119 + try std.testing.expectError(error.InvalidVarint, car.read(arena.allocator(), &.{0x80})); 120 + } 121 + 122 + test "reject truncated header data" { 123 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 124 + defer arena.deinit(); 125 + // header_len = 50 but only 3 bytes of data follow 126 + try std.testing.expectError(error.UnexpectedEof, car.read(arena.allocator(), &.{ 0x32, 0xaa, 0xbb, 0xcc })); 127 + } 128 + 129 + // === block edge cases === 130 + 131 + test "reject block with truncated data" { 132 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 133 + defer arena.deinit(); 134 + const alloc = arena.allocator(); 135 + 136 + // build a valid CAR with one block, then truncate the block data 137 + const data = try cbor.encodeAlloc(alloc, .{ .map = &.{ 138 + .{ .key = "x", .value = .{ .unsigned = 1 } }, 139 + } }); 140 + const cid = try cbor.Cid.forDagCbor(alloc, data); 141 + const car_bytes = try car.writeAlloc(alloc, .{ 142 + .roots = &.{cid}, 143 + .blocks = &.{.{ .cid_raw = cid.raw, .data = data }}, 144 + }); 145 + 146 + // truncate the last 5 bytes (removing part of block data) 147 + const truncated = car_bytes[0 .. car_bytes.len - 5]; 148 + try std.testing.expectError(error.UnexpectedEof, car.read(alloc, truncated)); 149 + } 150 + 151 + // === round-trip determinism === 152 + 153 + test "write then read then write produces identical bytes" { 154 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 155 + defer arena.deinit(); 156 + const alloc = arena.allocator(); 157 + 158 + // create a multi-block CAR 159 + const data1 = try cbor.encodeAlloc(alloc, .{ .map = &.{ 160 + .{ .key = "text", .value = .{ .text = "first block" } }, 161 + } }); 162 + const data2 = try cbor.encodeAlloc(alloc, .{ .map = &.{ 163 + .{ .key = "text", .value = .{ .text = "second block" } }, 164 + } }); 165 + const cid1 = try cbor.Cid.forDagCbor(alloc, data1); 166 + const cid2 = try cbor.Cid.forDagCbor(alloc, data2); 167 + 168 + const original = car.Car{ 169 + .roots = &.{cid1}, 170 + .blocks = &.{ 171 + .{ .cid_raw = cid1.raw, .data = data1 }, 172 + .{ .cid_raw = cid2.raw, .data = data2 }, 173 + }, 174 + }; 175 + 176 + // write → read → write 177 + const first_write = try car.writeAlloc(alloc, original); 178 + const parsed = try car.read(alloc, first_write); 179 + const second_write = try car.writeAlloc(alloc, parsed); 180 + 181 + try std.testing.expectEqualSlices(u8, first_write, second_write); 182 + } 183 + 184 + // === multiple roots === 185 + 186 + test "round-trip with multiple roots" { 187 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 188 + defer arena.deinit(); 189 + const alloc = arena.allocator(); 190 + 191 + const data1 = "block one"; 192 + const data2 = "block two"; 193 + const cid1 = try cbor.Cid.forDagCbor(alloc, data1); 194 + const cid2 = try cbor.Cid.forDagCbor(alloc, data2); 195 + 196 + const original = car.Car{ 197 + .roots = &.{ cid1, cid2 }, 198 + .blocks = &.{ 199 + .{ .cid_raw = cid1.raw, .data = data1 }, 200 + .{ .cid_raw = cid2.raw, .data = data2 }, 201 + }, 202 + }; 203 + 204 + const car_bytes = try car.writeAlloc(alloc, original); 205 + const parsed = try car.read(alloc, car_bytes); 206 + 207 + try std.testing.expectEqual(@as(usize, 2), parsed.roots.len); 208 + try std.testing.expectEqual(@as(usize, 2), parsed.blocks.len); 209 + try std.testing.expectEqualSlices(u8, cid1.digest().?, parsed.roots[0].digest().?); 210 + try std.testing.expectEqualSlices(u8, cid2.digest().?, parsed.roots[1].digest().?); 211 + } 212 + 213 + // === CID integrity === 214 + 215 + test "reject single-bit corruption in block data" { 216 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 217 + defer arena.deinit(); 218 + const alloc = arena.allocator(); 219 + 220 + const data = try cbor.encodeAlloc(alloc, .{ .map = &.{ 221 + .{ .key = "text", .value = .{ .text = "original data" } }, 222 + } }); 223 + const cid = try cbor.Cid.forDagCbor(alloc, data); 224 + 225 + // write valid CAR, then flip one bit in block content 226 + const car_bytes = try car.writeAlloc(alloc, .{ 227 + .roots = &.{cid}, 228 + .blocks = &.{.{ .cid_raw = cid.raw, .data = data }}, 229 + }); 230 + 231 + // find the block data in the CAR and corrupt it 232 + var corrupted = try alloc.dupe(u8, car_bytes); 233 + corrupted[corrupted.len - 1] ^= 0x01; // flip last bit 234 + 235 + try std.testing.expectError(error.BadBlockHash, car.read(alloc, corrupted)); 236 + } 237 + 238 + // === findBlock === 239 + 240 + test "findBlock via hash index" { 241 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 242 + defer arena.deinit(); 243 + const alloc = arena.allocator(); 244 + 245 + const data = "test block"; 246 + const cid = try cbor.Cid.forDagCbor(alloc, data); 247 + 248 + const car_bytes = try car.writeAlloc(alloc, .{ 249 + .roots = &.{cid}, 250 + .blocks = &.{.{ .cid_raw = cid.raw, .data = data }}, 251 + }); 252 + const parsed = try car.read(alloc, car_bytes); 253 + 254 + // lookup by CID should return block data 255 + const found = car.findBlock(parsed, cid.raw).?; 256 + try std.testing.expectEqualSlices(u8, data, found); 257 + 258 + // lookup with wrong CID should return null 259 + const other_cid = try cbor.Cid.forDagCbor(alloc, "other"); 260 + try std.testing.expect(car.findBlock(parsed, other_cid.raw) == null); 261 + } 262 + 263 + // === size limits === 264 + 265 + test "reject CAR exceeding max size" { 266 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 267 + defer arena.deinit(); 268 + const alloc = arena.allocator(); 269 + 270 + // tiny CAR, but set max_size very small 271 + const data = "x"; 272 + const cid = try cbor.Cid.forDagCbor(alloc, data); 273 + const car_bytes = try car.writeAlloc(alloc, .{ 274 + .roots = &.{cid}, 275 + .blocks = &.{.{ .cid_raw = cid.raw, .data = data }}, 276 + }); 277 + 278 + // set max_size smaller than the CAR 279 + try std.testing.expectError(error.BlocksTooLarge, car.readWithOptions(alloc, car_bytes, .{ 280 + .max_size = 10, 281 + })); 282 + } 283 + 284 + // === varint edge cases (via CAR reader) === 285 + 286 + test "readUvarint rejects varint longer than 10 bytes" { 287 + // 10 continuation bytes + 1 terminator = 11 bytes total 288 + const data = [_]u8{0x80} ** 10 ++ [_]u8{0x00}; 289 + var pos: usize = 0; 290 + try std.testing.expect(cbor.readUvarint(&data, &pos) == null); 291 + } 292 + 293 + test "readUvarint accepts 10-byte varint" { 294 + // max valid: 9 continuation bytes + 1 terminator with bit 0 set 295 + const data = [_]u8{0x80} ** 9 ++ [_]u8{0x01}; 296 + var pos: usize = 0; 297 + const val = cbor.readUvarint(&data, &pos); 298 + try std.testing.expect(val != null); 299 + try std.testing.expectEqual(@as(usize, 10), pos); 300 + } 301 + 302 + // === header-only CAR (roots, no blocks) === 303 + 304 + test "CAR with roots but no blocks" { 305 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 306 + defer arena.deinit(); 307 + const alloc = arena.allocator(); 308 + 309 + const root_cid = try cbor.Cid.forDagCbor(alloc, "root"); 310 + const header: cbor.Value = .{ .map = &.{ 311 + .{ .key = "roots", .value = .{ .array = &.{.{ .cid = root_cid }} } }, 312 + .{ .key = "version", .value = .{ .unsigned = 1 } }, 313 + } }; 314 + const header_bytes = try cbor.encodeAlloc(alloc, header); 315 + 316 + var car_aw: std.Io.Writer.Allocating = .init(alloc); 317 + try cbor.writeUvarint(&car_aw.writer, header_bytes.len); 318 + try car_aw.writer.writeAll(header_bytes); 319 + 320 + const parsed = try car.read(alloc, car_aw.written()); 321 + try std.testing.expectEqual(@as(usize, 1), parsed.roots.len); 322 + try std.testing.expectEqual(@as(usize, 0), parsed.blocks.len); 323 + } 324 + 325 + // note: checkAllAllocationFailures cannot be used for car.read because 326 + // readWithOptions uses an internal ArenaAllocator for header CBOR parsing, 327 + // which creates non-deterministic page-level allocations from the backing 328 + // allocator. the errdefer cleanup on roots/blocks/block_index in 329 + // readWithOptions ensures no leaks on OOM; the header arena's defer deinit 330 + // ensures the header Value tree is always freed.
+580 -158
src/internal/repo/cbor.zig
··· 34 34 text: []const u8, 35 35 array: []const Value, 36 36 map: []const MapEntry, 37 - tag: Tag, 38 37 boolean: bool, 39 38 null, 40 39 cid: Cid, ··· 42 41 pub const MapEntry = struct { 43 42 key: []const u8, // DAG-CBOR: keys are always text strings 44 43 value: Value, 45 - }; 46 - 47 - pub const Tag = struct { 48 - number: u64, 49 - content: *const Value, 50 44 }; 51 45 52 46 /// look up a key in a map value ··· 118 112 }; 119 113 } 120 114 115 + /// get a CID from a map by key 116 + pub fn getCid(self: Value, key: []const u8) ?Cid { 117 + const v = self.get(key) orelse return null; 118 + return switch (v) { 119 + .cid => |c| c, 120 + else => null, 121 + }; 122 + } 123 + 121 124 // verify the Value union stayed slim after Cid optimization (was ~64, now 24) 122 125 comptime { 123 126 std.debug.assert(@sizeOf(Value) == 24); ··· 201 204 var hash: [Sha256.digest_length]u8 = undefined; 202 205 Sha256.hash(data, &hash, .{}); 203 206 204 - var aw: std.Io.Writer.Allocating = .init(allocator); 205 - errdefer aw.deinit(); 206 - try writeUvarint(&aw.writer, ver); 207 - try writeUvarint(&aw.writer, cod); 208 - try writeUvarint(&aw.writer, hash_fn_code); 209 - try writeUvarint(&aw.writer, Sha256.digest_length); 210 - try aw.writer.writeAll(&hash); 207 + // build CID on the stack then copy to allocator — avoids dynamic writer 208 + // overhead. max varint size is 10 bytes × 4 fields + 32 byte hash = 72 bytes. 209 + var buf: [72]u8 = undefined; 210 + var w: std.Io.Writer = .fixed(&buf); 211 + writeUvarint(&w, ver) catch unreachable; 212 + writeUvarint(&w, cod) catch unreachable; 213 + writeUvarint(&w, hash_fn_code) catch unreachable; 214 + writeUvarint(&w, Sha256.digest_length) catch unreachable; 215 + w.writeAll(&hash) catch unreachable; 211 216 212 - return .{ .raw = try aw.toOwnedSlice() }; 217 + const raw = try allocator.dupe(u8, w.buffered()); 218 + return .{ .raw = raw }; 213 219 } 214 220 215 221 /// serialize this CID to raw bytes (version varint + codec varint + multihash) ··· 228 234 ReservedAdditionalInfo, 229 235 Overflow, 230 236 OutOfMemory, 237 + NonMinimalEncoding, 238 + TrailingBytes, 239 + UnsupportedTag, 240 + UnsortedMapKeys, 241 + DuplicateMapKey, 242 + InvalidUtf8, 243 + MaxDepthExceeded, 244 + WrongType, 231 245 }; 232 246 247 + /// maximum nesting depth for arrays/maps to prevent stack overflow 248 + pub const max_depth: usize = 128; 249 + 233 250 /// decode a single CBOR value from the front of `data`. 234 251 /// returns the value and the number of bytes consumed. 235 252 pub fn decode(allocator: Allocator, data: []const u8) DecodeError!struct { value: Value, consumed: usize } { 236 253 var pos: usize = 0; 237 - const value = try decodeAt(allocator, data, &pos); 254 + const value = try decodeAt(allocator, data, &pos, 0); 238 255 return .{ .value = value, .consumed = pos }; 239 256 } 240 257 241 - /// decode all bytes as a single CBOR value 258 + /// decode all bytes as a single CBOR value, rejecting trailing bytes 242 259 pub fn decodeAll(allocator: Allocator, data: []const u8) DecodeError!Value { 243 260 var pos: usize = 0; 244 - return try decodeAt(allocator, data, &pos); 261 + const value = try decodeAt(allocator, data, &pos, 0); 262 + if (pos != data.len) return error.TrailingBytes; 263 + return value; 245 264 } 246 265 247 - fn decodeAt(allocator: Allocator, data: []const u8, pos: *usize) DecodeError!Value { 266 + fn decodeAt(allocator: Allocator, data: []const u8, pos: *usize, depth: usize) DecodeError!Value { 248 267 if (pos.* >= data.len) return error.UnexpectedEof; 249 - 250 268 const initial = data[pos.*]; 251 - pos.* += 1; 252 - 253 - const major: MajorType = @enumFromInt(@as(u3, @truncate(initial >> 5))); 269 + const major: u3 = @truncate(initial >> 5); 254 270 const additional: u5 = @truncate(initial); 255 271 256 - return switch (major) { 257 - .unsigned => { 258 - const val = try readArgument(data, pos, additional); 259 - return .{ .unsigned = val }; 260 - }, 261 - .negative => { 262 - const val = try readArgument(data, pos, additional); 272 + // simple values (major 7) are handled without readArg since floats 273 + // use additional 25/26/27 to mean float16/32/64, not integer arguments 274 + if (major == 7) { 275 + pos.* += 1; 276 + return switch (additional) { 277 + 20 => .{ .boolean = false }, 278 + 21 => .{ .boolean = true }, 279 + 22 => .null, 280 + 25, 26, 27 => error.UnsupportedFloat, // DAG-CBOR forbids floats in AT Protocol 281 + 31 => error.IndefiniteLength, // break code — DAG-CBOR forbids indefinite lengths 282 + else => error.UnsupportedSimpleValue, 283 + }; 284 + } 285 + 286 + const arg = try readArg(data, pos.*); 287 + pos.* = arg.end; 288 + 289 + return switch (@as(MajorType, @enumFromInt(major))) { 290 + .unsigned => .{ .unsigned = arg.val }, 291 + .negative => blk: { 263 292 // negative CBOR: value is -1 - val 264 - if (val > std.math.maxInt(i64)) return error.Overflow; 265 - return .{ .negative = -1 - @as(i64, @intCast(val)) }; 293 + if (arg.val > std.math.maxInt(i64)) return error.Overflow; 294 + break :blk .{ .negative = -1 - @as(i64, @intCast(arg.val)) }; 266 295 }, 267 - .byte_string => { 268 - const len = try readArgument(data, pos, additional); 269 - const end = pos.* + @as(usize, @intCast(len)); 296 + .byte_string => blk: { 297 + const len = std.math.cast(usize, arg.val) orelse return error.UnexpectedEof; 298 + const end = std.math.add(usize, pos.*, len) catch return error.UnexpectedEof; 270 299 if (end > data.len) return error.UnexpectedEof; 271 300 const bytes = data[pos.*..end]; 272 301 pos.* = end; 273 - return .{ .bytes = bytes }; 302 + break :blk .{ .bytes = bytes }; 274 303 }, 275 - .text_string => { 276 - const len = try readArgument(data, pos, additional); 277 - const end = pos.* + @as(usize, @intCast(len)); 304 + .text_string => blk: { 305 + const len = std.math.cast(usize, arg.val) orelse return error.UnexpectedEof; 306 + const end = std.math.add(usize, pos.*, len) catch return error.UnexpectedEof; 278 307 if (end > data.len) return error.UnexpectedEof; 279 308 const text = data[pos.*..end]; 309 + if (!std.unicode.utf8ValidateSlice(text)) return error.InvalidUtf8; 280 310 pos.* = end; 281 - return .{ .text = text }; 311 + break :blk .{ .text = text }; 282 312 }, 283 - .array => { 284 - const count = try readArgument(data, pos, additional); 285 - const items = try allocator.alloc(Value, @intCast(count)); 313 + .array => blk: { 314 + if (depth >= max_depth) return error.MaxDepthExceeded; 315 + // sanity check: each element is at least 1 byte 316 + if (arg.val > data.len - pos.*) return error.UnexpectedEof; 317 + const items = try allocator.alloc(Value, @intCast(arg.val)); 318 + errdefer allocator.free(items); 286 319 for (items) |*item| { 287 - item.* = try decodeAt(allocator, data, pos); 320 + item.* = try decodeAt(allocator, data, pos, depth + 1); 288 321 } 289 - return .{ .array = items }; 322 + break :blk .{ .array = items }; 290 323 }, 291 - .map => { 292 - const count = try readArgument(data, pos, additional); 293 - const entries = try allocator.alloc(Value.MapEntry, @intCast(count)); 294 - for (entries) |*entry| { 324 + .map => blk: { 325 + if (depth >= max_depth) return error.MaxDepthExceeded; 326 + // sanity check: each entry is at least 2 bytes (key + value) 327 + if (arg.val > (data.len - pos.*) / 2) return error.UnexpectedEof; 328 + const entries = try allocator.alloc(Value.MapEntry, @intCast(arg.val)); 329 + errdefer allocator.free(entries); 330 + for (entries, 0..) |*entry, i| { 295 331 // DAG-CBOR: map keys must be text strings — inline read to avoid 296 332 // a full decodeAt + Value union construction per key 297 - if (pos.* >= data.len) return error.UnexpectedEof; 298 - const key_byte = data[pos.*]; 299 - pos.* += 1; 300 - if (@as(u3, @truncate(key_byte >> 5)) != 3) return error.InvalidMapKey; 301 - const key_len = try readArgument(data, pos, @truncate(key_byte)); 302 - const key_end = pos.* + @as(usize, @intCast(key_len)); 333 + const key_arg = try readArg(data, pos.*); 334 + pos.* = key_arg.end; 335 + if (key_arg.major != 3) return error.InvalidMapKey; 336 + const key_len = std.math.cast(usize, key_arg.val) orelse return error.UnexpectedEof; 337 + const key_end = std.math.add(usize, pos.*, key_len) catch return error.UnexpectedEof; 303 338 if (key_end > data.len) return error.UnexpectedEof; 304 339 entry.key = data[pos.*..key_end]; 340 + if (!std.unicode.utf8ValidateSlice(entry.key)) return error.InvalidUtf8; 305 341 pos.* = key_end; 306 - entry.value = try decodeAt(allocator, data, pos); 342 + 343 + // DAG-CBOR: keys must be sorted (shorter first, then lex) and unique 344 + if (i > 0) { 345 + const prev = entries[i - 1].key; 346 + if (prev.len < entry.key.len) { 347 + // ok — shorter key first 348 + } else if (prev.len == entry.key.len) { 349 + switch (std.mem.order(u8, prev, entry.key)) { 350 + .lt => {}, // ok — lex order 351 + .eq => return error.DuplicateMapKey, 352 + .gt => return error.UnsortedMapKeys, 353 + } 354 + } else { 355 + return error.UnsortedMapKeys; 356 + } 357 + } 358 + 359 + entry.value = try decodeAt(allocator, data, pos, depth + 1); 307 360 } 308 - return .{ .map = entries }; 361 + break :blk .{ .map = entries }; 309 362 }, 310 - .tag => { 311 - const tag_num = try readArgument(data, pos, additional); 312 - if (tag_num == 42) { 313 - // CID link — content is a byte string with 0x00 prefix 314 - const content = try decodeAt(allocator, data, pos); 315 - const cid_bytes = switch (content) { 316 - .bytes => |b| b, 317 - else => return error.InvalidCid, 318 - }; 319 - if (cid_bytes.len < 1 or cid_bytes[0] != 0x00) return error.InvalidCid; 320 - return .{ .cid = .{ .raw = cid_bytes[1..] } }; // zero-cost: just reference the bytes 321 - } 322 - // generic tag — allocate content on heap 323 - const content_ptr = try allocator.create(Value); 324 - content_ptr.* = try decodeAt(allocator, data, pos); 325 - return .{ .tag = .{ .number = tag_num, .content = content_ptr } }; 326 - }, 327 - .simple => { 328 - return switch (additional) { 329 - 20 => .{ .boolean = false }, 330 - 21 => .{ .boolean = true }, 331 - 22 => .null, 332 - 25, 26, 27 => error.UnsupportedFloat, // DAG-CBOR forbids floats in AT Protocol 333 - 31 => error.IndefiniteLength, // break code — DAG-CBOR forbids indefinite lengths 334 - else => error.UnsupportedSimpleValue, 363 + .tag => blk: { 364 + if (arg.val != 42) return error.UnsupportedTag; // DAG-CBOR only allows tag 42 (CID) 365 + // CID link — content is a byte string with 0x00 prefix 366 + const content = try decodeAt(allocator, data, pos, depth); 367 + const cid_bytes = switch (content) { 368 + .bytes => |b| b, 369 + else => return error.InvalidCid, 335 370 }; 336 - }, 337 - }; 338 - } 339 - 340 - /// read the argument value from additional info + following bytes 341 - fn readArgument(data: []const u8, pos: *usize, additional: u5) DecodeError!u64 { 342 - return switch (additional) { 343 - 0...23 => @as(u64, additional), 344 - 24 => { // 1-byte 345 - if (pos.* >= data.len) return error.UnexpectedEof; 346 - const val = data[pos.*]; 347 - pos.* += 1; 348 - return @as(u64, val); 349 - }, 350 - 25 => { // 2-byte big-endian 351 - if (pos.* + 2 > data.len) return error.UnexpectedEof; 352 - const val = std.mem.readInt(u16, data[pos.*..][0..2], .big); 353 - pos.* += 2; 354 - return @as(u64, val); 355 - }, 356 - 26 => { // 4-byte big-endian 357 - if (pos.* + 4 > data.len) return error.UnexpectedEof; 358 - const val = std.mem.readInt(u32, data[pos.*..][0..4], .big); 359 - pos.* += 4; 360 - return @as(u64, val); 361 - }, 362 - 27 => { // 8-byte big-endian 363 - if (pos.* + 8 > data.len) return error.UnexpectedEof; 364 - const val = std.mem.readInt(u64, data[pos.*..][0..8], .big); 365 - pos.* += 8; 366 - return val; 371 + // CID byte string must have 0x00 identity multibase prefix + at least 372 + // version byte + codec byte (minimum 3 bytes total) 373 + if (cid_bytes.len < 3 or cid_bytes[0] != 0x00) return error.InvalidCid; 374 + break :blk .{ .cid = .{ .raw = cid_bytes[1..] } }; // zero-cost: just reference the bytes 367 375 }, 368 - 28, 29, 30 => error.ReservedAdditionalInfo, 369 - 31 => error.IndefiniteLength, 376 + .simple => unreachable, // handled above 370 377 }; 371 378 } 372 379 373 380 /// wrap raw CID bytes (after removing the 0x00 multibase prefix) into a Cid. 374 - /// validates the structure is parseable but stores only the raw bytes. 381 + /// does not validate the CID structure — call version()/codec()/digest() to parse lazily. 375 382 pub fn parseCid(raw: []const u8) Cid { 376 383 return .{ .raw = raw }; 377 384 } 378 385 379 - /// read an unsigned varint (LEB128) 386 + /// read an unsigned varint (LEB128). rejects varints longer than 10 bytes 387 + /// and rejects overflow (10th byte must have value <= 1). 380 388 pub fn readUvarint(data: []const u8, pos: *usize) ?u64 { 381 389 var result: u64 = 0; 382 - var shift: u6 = 0; 383 - while (pos.* < data.len) { 390 + var shift: u7 = 0; 391 + for (0..10) |i| { 392 + if (pos.* >= data.len) return null; 384 393 const byte = data[pos.*]; 385 394 pos.* += 1; 386 - result |= @as(u64, byte & 0x7f) << shift; 395 + // 10th byte (i=9, shift=63): only bit 0 can fit in u64 396 + if (i == 9 and byte > 1) return null; 397 + result |= @as(u64, byte & 0x7f) << @as(u6, @intCast(shift)); 387 398 if (byte & 0x80 == 0) return result; 388 - shift +|= 7; 389 - if (shift >= 64) return null; 399 + shift += 7; 390 400 } 391 - return null; 401 + return null; // varint too long 392 402 } 393 403 394 404 // === encoder === ··· 397 407 OutOfMemory, 398 408 }; 399 409 400 - /// write the CBOR initial byte + argument using shortest encoding (DAG-CBOR requirement) 410 + /// write the CBOR initial byte + argument using shortest encoding (DAG-CBOR requirement). 411 + /// batches all bytes into a single writeAll call to minimize writer dispatch overhead. 401 412 fn writeArgument(writer: anytype, major: u3, val: u64) !void { 402 413 const prefix: u8 = @as(u8, major) << 5; 403 414 if (val < 24) { 404 - try writer.writeByte(prefix | @as(u8, @intCast(val))); 415 + try writer.writeAll(&.{prefix | @as(u8, @intCast(val))}); 405 416 } else if (val <= 0xff) { 406 - try writer.writeByte(prefix | 24); 407 - try writer.writeByte(@as(u8, @intCast(val))); 417 + try writer.writeAll(&.{ prefix | 24, @as(u8, @intCast(val)) }); 408 418 } else if (val <= 0xffff) { 409 - try writer.writeByte(prefix | 25); 410 419 const v: u16 = @intCast(val); 411 - try writer.writeAll(&[2]u8{ @truncate(v >> 8), @truncate(v) }); 420 + try writer.writeAll(&.{ prefix | 25, @truncate(v >> 8), @truncate(v) }); 412 421 } else if (val <= 0xffffffff) { 413 - try writer.writeByte(prefix | 26); 414 422 const v: u32 = @intCast(val); 415 - try writer.writeAll(&[4]u8{ 416 - @truncate(v >> 24), @truncate(v >> 16), 417 - @truncate(v >> 8), @truncate(v), 423 + try writer.writeAll(&.{ 424 + prefix | 26, 425 + @truncate(v >> 24), 426 + @truncate(v >> 16), 427 + @truncate(v >> 8), 428 + @truncate(v), 418 429 }); 419 430 } else { 420 - try writer.writeByte(prefix | 27); 421 - try writer.writeAll(&[8]u8{ 422 - @truncate(val >> 56), @truncate(val >> 48), 423 - @truncate(val >> 40), @truncate(val >> 32), 424 - @truncate(val >> 24), @truncate(val >> 16), 425 - @truncate(val >> 8), @truncate(val), 431 + try writer.writeAll(&.{ 432 + prefix | 27, 433 + @truncate(val >> 56), 434 + @truncate(val >> 48), 435 + @truncate(val >> 40), 436 + @truncate(val >> 32), 437 + @truncate(val >> 24), 438 + @truncate(val >> 16), 439 + @truncate(val >> 8), 440 + @truncate(val), 426 441 }); 427 442 } 428 443 } 429 444 445 + /// check if map entries are already in DAG-CBOR key order 446 + fn keysAlreadySorted(entries: []const Value.MapEntry) bool { 447 + if (entries.len <= 1) return true; 448 + var prev = entries[0].key; 449 + for (entries[1..]) |entry| { 450 + if (prev.len > entry.key.len) return false; 451 + if (prev.len == entry.key.len and std.mem.order(u8, prev, entry.key) != .lt) return false; 452 + prev = entry.key; 453 + } 454 + return true; 455 + } 456 + 430 457 /// DAG-CBOR map key ordering: shorter keys first, then lexicographic 431 458 fn dagCborKeyLessThan(_: void, a: Value.MapEntry, b: Value.MapEntry) bool { 432 459 if (a.key.len != b.key.len) return a.key.len < b.key.len; 433 460 return std.mem.order(u8, a.key, b.key) == .lt; 434 461 } 435 462 463 + /// write a short text string (< 24 bytes) as a single fused write. 464 + /// this is the hot path for map keys in AT Protocol records, where keys 465 + /// are always short ASCII strings. fusing header+payload into one writeAll 466 + /// halves the writer dispatch count. 467 + fn writeShortText(writer: anytype, text: []const u8) !void { 468 + if (text.len < 24) { 469 + var buf: [24]u8 = undefined; 470 + buf[0] = 0x60 | @as(u8, @intCast(text.len)); 471 + @memcpy(buf[1..][0..text.len], text); 472 + try writer.writeAll(buf[0 .. 1 + text.len]); 473 + } else { 474 + try writeArgument(writer, 3, text.len); 475 + try writer.writeAll(text); 476 + } 477 + } 478 + 436 479 /// encode a Value to the given writer in DAG-CBOR format. 437 480 /// allocator is needed for sorting map keys during encoding. 438 481 pub fn encode(allocator: Allocator, writer: anytype, value: Value) !void { ··· 447 490 try writeArgument(writer, 2, b.len); 448 491 try writer.writeAll(b); 449 492 }, 450 - .text => |t| { 451 - try writeArgument(writer, 3, t.len); 452 - try writer.writeAll(t); 453 - }, 493 + .text => |t| try writeShortText(writer, t), 454 494 .array => |items| { 455 495 try writeArgument(writer, 4, items.len); 456 496 for (items) |item| { ··· 459 499 }, 460 500 .map => |entries| { 461 501 try writeArgument(writer, 5, entries.len); 462 - // DAG-CBOR: keys sorted by byte length, then lexicographically 463 - const sorted = try allocator.dupe(Value.MapEntry, entries); 464 - defer allocator.free(sorted); 465 - std.mem.sort(Value.MapEntry, sorted, {}, dagCborKeyLessThan); 466 - for (sorted) |entry| { 467 - try encode(allocator, writer, .{ .text = entry.key }); 468 - try encode(allocator, writer, entry.value); 502 + // DAG-CBOR: keys sorted by byte length, then lexicographically. 503 + // three paths: already sorted (common for decoded data), stack sort 504 + // for small maps (≤16 entries, covers all AT Protocol records), or 505 + // heap sort for rare large maps. 506 + if (keysAlreadySorted(entries)) { 507 + for (entries) |entry| { 508 + try writeShortText(writer, entry.key); 509 + try encode(allocator, writer, entry.value); 510 + } 511 + } else if (entries.len <= 16) { 512 + var buf: [16]Value.MapEntry = undefined; 513 + const sorted = buf[0..entries.len]; 514 + @memcpy(sorted, entries); 515 + std.mem.sort(Value.MapEntry, sorted, {}, dagCborKeyLessThan); 516 + for (sorted) |entry| { 517 + try writeShortText(writer, entry.key); 518 + try encode(allocator, writer, entry.value); 519 + } 520 + } else { 521 + const sorted = try allocator.dupe(Value.MapEntry, entries); 522 + defer allocator.free(sorted); 523 + std.mem.sort(Value.MapEntry, sorted, {}, dagCborKeyLessThan); 524 + for (sorted) |entry| { 525 + try writeShortText(writer, entry.key); 526 + try encode(allocator, writer, entry.value); 527 + } 469 528 } 470 - }, 471 - .tag => |t| { 472 - try writeArgument(writer, 6, t.number); 473 - try encode(allocator, writer, t.content.*); 474 529 }, 475 530 .boolean => |b| try writer.writeByte(if (b) @as(u8, 0xf5) else @as(u8, 0xf4)), 476 531 .null => try writer.writeByte(0xf6), ··· 502 557 try writer.writeByte(@as(u8, @truncate(v))); 503 558 } 504 559 560 + /// Result of reading a CBOR initial byte and its argument. 561 + pub const Arg = struct { 562 + major: u3, 563 + val: u64, 564 + end: usize, 565 + }; 566 + 567 + /// Read a CBOR initial byte at `pos`, parse the argument value from 568 + /// additional info + following bytes, and return the major type (high 3 bits), 569 + /// argument value, and position after the header. 570 + /// 571 + /// Validates shortest-form encoding (DAG-CBOR requirement). 572 + /// This is the public, value-semantics equivalent of the internal `readArgument`. 573 + pub fn readArg(data: []const u8, pos: usize) DecodeError!Arg { 574 + if (pos >= data.len) return error.UnexpectedEof; 575 + const initial = data[pos]; 576 + const major: u3 = @truncate(initial >> 5); 577 + const additional: u5 = @truncate(initial); 578 + var cur = pos + 1; 579 + const val: u64 = switch (additional) { 580 + 0...23 => @as(u64, additional), 581 + 24 => blk: { // 1-byte 582 + if (cur >= data.len) return error.UnexpectedEof; 583 + const v = data[cur]; 584 + cur += 1; 585 + if (v < 24) return error.NonMinimalEncoding; 586 + break :blk @as(u64, v); 587 + }, 588 + 25 => blk: { // 2-byte big-endian 589 + if (cur + 2 > data.len) return error.UnexpectedEof; 590 + const v = std.mem.readInt(u16, data[cur..][0..2], .big); 591 + cur += 2; 592 + if (v <= 0xff) return error.NonMinimalEncoding; 593 + break :blk @as(u64, v); 594 + }, 595 + 26 => blk: { // 4-byte big-endian 596 + if (cur + 4 > data.len) return error.UnexpectedEof; 597 + const v = std.mem.readInt(u32, data[cur..][0..4], .big); 598 + cur += 4; 599 + if (v <= 0xffff) return error.NonMinimalEncoding; 600 + break :blk @as(u64, v); 601 + }, 602 + 27 => blk: { // 8-byte big-endian 603 + if (cur + 8 > data.len) return error.UnexpectedEof; 604 + const v = std.mem.readInt(u64, data[cur..][0..8], .big); 605 + cur += 8; 606 + if (v <= 0xffffffff) return error.NonMinimalEncoding; 607 + break :blk v; 608 + }, 609 + 28, 29, 30 => return error.ReservedAdditionalInfo, 610 + 31 => return error.IndefiniteLength, 611 + }; 612 + return .{ .major = major, .val = val, .end = cur }; 613 + } 614 + 615 + // --------------------------------------------------------------------------- 616 + // Type-specific readers — zero-copy, no allocator needed 617 + // --------------------------------------------------------------------------- 618 + 619 + pub const SliceResult = struct { val: []const u8, end: usize }; 620 + pub const U64Result = struct { val: u64, end: usize }; 621 + pub const I64Result = struct { val: i64, end: usize }; 622 + pub const BoolResult = struct { val: bool, end: usize }; 623 + 624 + /// Read a CBOR text string (major type 3) at `pos`. 625 + /// Validates UTF-8. Returns a zero-copy slice into `data`. 626 + pub fn readText(data: []const u8, pos: usize) DecodeError!SliceResult { 627 + const arg = try readArg(data, pos); 628 + if (arg.major != 3) return error.WrongType; 629 + const len = std.math.cast(usize, arg.val) orelse return error.UnexpectedEof; 630 + const end = std.math.add(usize, arg.end, len) catch return error.UnexpectedEof; 631 + if (end > data.len) return error.UnexpectedEof; 632 + const text = data[arg.end..end]; 633 + if (!std.unicode.utf8ValidateSlice(text)) return error.InvalidUtf8; 634 + return .{ .val = text, .end = end }; 635 + } 636 + 637 + /// Read a CBOR byte string (major type 2) at `pos`. 638 + /// Returns a zero-copy slice into `data`. 639 + pub fn readBytes(data: []const u8, pos: usize) DecodeError!SliceResult { 640 + const arg = try readArg(data, pos); 641 + if (arg.major != 2) return error.WrongType; 642 + const len = std.math.cast(usize, arg.val) orelse return error.UnexpectedEof; 643 + const end = std.math.add(usize, arg.end, len) catch return error.UnexpectedEof; 644 + if (end > data.len) return error.UnexpectedEof; 645 + return .{ .val = data[arg.end..end], .end = end }; 646 + } 647 + 648 + /// Read a CBOR unsigned integer (major type 0) at `pos`. 649 + pub fn readUint(data: []const u8, pos: usize) DecodeError!U64Result { 650 + const arg = try readArg(data, pos); 651 + if (arg.major != 0) return error.WrongType; 652 + return .{ .val = arg.val, .end = arg.end }; 653 + } 654 + 655 + /// Read a CBOR integer (major type 0 or 1) at `pos`. 656 + /// Major 0 = positive, major 1 = negative (-1 - val). 657 + /// Returns error.Overflow if a positive value exceeds maxInt(i64). 658 + pub fn readInt(data: []const u8, pos: usize) DecodeError!I64Result { 659 + const arg = try readArg(data, pos); 660 + switch (arg.major) { 661 + 0 => { 662 + if (arg.val > @as(u64, @intCast(std.math.maxInt(i64)))) return error.Overflow; 663 + return .{ .val = @intCast(arg.val), .end = arg.end }; 664 + }, 665 + 1 => { 666 + // CBOR negative: -1 - val 667 + // val can be 0..2^64-1, result is -1..-2^64 668 + // i64 can hold down to -2^63, so max raw val is 2^63 - 1 669 + if (arg.val > @as(u64, @intCast(std.math.maxInt(i64)))) return error.Overflow; 670 + return .{ .val = -1 - @as(i64, @intCast(arg.val)), .end = arg.end }; 671 + }, 672 + else => return error.WrongType, 673 + } 674 + } 675 + 676 + /// Read a CBOR boolean at `pos`. 677 + /// 0xf4 = false, 0xf5 = true. 678 + pub fn readBool(data: []const u8, pos: usize) DecodeError!BoolResult { 679 + if (pos >= data.len) return error.UnexpectedEof; 680 + return switch (data[pos]) { 681 + 0xf4 => .{ .val = false, .end = pos + 1 }, 682 + 0xf5 => .{ .val = true, .end = pos + 1 }, 683 + else => error.WrongType, 684 + }; 685 + } 686 + 687 + /// Read a CBOR null at `pos`. 688 + /// 0xf6 = null. Returns position after the null byte. 689 + pub fn readNull(data: []const u8, pos: usize) DecodeError!usize { 690 + if (pos >= data.len) return error.UnexpectedEof; 691 + if (data[pos] != 0xf6) return error.WrongType; 692 + return pos + 1; 693 + } 694 + 695 + /// Read a CBOR map header (major type 5) at `pos`. 696 + /// Returns the entry count. 697 + pub fn readMapHeader(data: []const u8, pos: usize) DecodeError!U64Result { 698 + const arg = try readArg(data, pos); 699 + if (arg.major != 5) return error.WrongType; 700 + return .{ .val = arg.val, .end = arg.end }; 701 + } 702 + 703 + /// Read a CBOR array header (major type 4) at `pos`. 704 + /// Returns the element count. 705 + pub fn readArrayHeader(data: []const u8, pos: usize) DecodeError!U64Result { 706 + const arg = try readArg(data, pos); 707 + if (arg.major != 4) return error.WrongType; 708 + return .{ .val = arg.val, .end = arg.end }; 709 + } 710 + 711 + /// Read a DAG-CBOR CID link at `pos`. 712 + /// Expects tag(42) followed by a byte string with a 0x00 identity multibase prefix. 713 + /// Returns the raw CID bytes (after the 0x00 prefix) as a zero-copy slice. 714 + pub fn readCidLink(data: []const u8, pos: usize) DecodeError!SliceResult { 715 + // Read the tag header — must be tag(42) 716 + const tag_arg = try readArg(data, pos); 717 + if (tag_arg.major != 6 or tag_arg.val != 42) return error.WrongType; 718 + // Read the inner byte string 719 + const bytes_result = try readBytes(data, tag_arg.end); 720 + const payload = bytes_result.val; 721 + // Must have 0x00 prefix + at least version byte + codec byte (min 3 bytes) 722 + if (payload.len < 3 or payload[0] != 0x00) return error.InvalidCid; 723 + return .{ .val = payload[1..], .end = bytes_result.end }; 724 + } 725 + 726 + // --------------------------------------------------------------------------- 727 + // Streaming helpers — skip / peek without full decode 728 + // --------------------------------------------------------------------------- 729 + 730 + /// Skip one CBOR value at `pos` without decoding it. Returns the position 731 + /// after the skipped value. Iterative (not recursive) using a small stack 732 + /// for nested containers. Zero allocation. 733 + pub fn skipValue(data: []const u8, pos: usize) DecodeError!usize { 734 + const max_stack = 32; 735 + var stack: [max_stack]u64 = undefined; 736 + var depth: usize = 0; 737 + var cur = pos; 738 + 739 + while (true) { 740 + const arg = try readArg(data, cur); 741 + cur = arg.end; 742 + 743 + switch (arg.major) { 744 + 0, 1 => { 745 + // integers: header only, nothing to skip after readArg 746 + }, 747 + 2, 3 => { 748 + // byte string / text string: skip `val` bytes of payload 749 + const len = std.math.cast(usize, arg.val) orelse return error.UnexpectedEof; 750 + cur = std.math.add(usize, cur, len) catch return error.UnexpectedEof; 751 + if (cur > data.len) return error.UnexpectedEof; 752 + }, 753 + 4 => { 754 + // array: push element count 755 + if (arg.val > 0) { 756 + if (depth >= max_stack) return error.MaxDepthExceeded; 757 + stack[depth] = arg.val; 758 + depth += 1; 759 + continue; // don't decrement — we haven't consumed an element yet 760 + } 761 + }, 762 + 5 => { 763 + // map: push key+value count (2 per entry) 764 + if (arg.val > 0) { 765 + if (depth >= max_stack) return error.MaxDepthExceeded; 766 + stack[depth] = std.math.mul(u64, arg.val, 2) catch return error.Overflow; 767 + depth += 1; 768 + continue; 769 + } 770 + }, 771 + 6 => { 772 + // tag: the tagged value follows immediately — loop to read it 773 + // don't push anything, don't decrement 774 + continue; 775 + }, 776 + 7 => { 777 + // simple/float: header only 778 + }, 779 + } 780 + 781 + // After consuming a value, unwind the stack 782 + while (depth > 0) { 783 + stack[depth - 1] -= 1; 784 + if (stack[depth - 1] > 0) break; 785 + depth -= 1; 786 + } 787 + 788 + if (depth == 0) return cur; 789 + } 790 + } 791 + 792 + /// Peek at the "$type" field in a DAG-CBOR map without full decode. 793 + /// Returns the type string (zero-copy slice) or null if not found. 794 + pub fn peekType(data: []const u8) DecodeError!?[]const u8 { 795 + return peekTypeAt(data, 0); 796 + } 797 + 798 + /// Peek at the "$type" field starting from a given position. 799 + pub fn peekTypeAt(data: []const u8, pos: usize) DecodeError!?[]const u8 { 800 + const map_header = try readArg(data, pos); 801 + if (map_header.major != 5) return null; 802 + 803 + var cur = map_header.end; 804 + const count = map_header.val; 805 + 806 + const safe_count = std.math.cast(usize, count) orelse return null; 807 + for (0..safe_count) |_| { 808 + // Read key — DAG-CBOR keys are always text strings 809 + const key = readText(data, cur) catch return null; 810 + cur = key.end; 811 + 812 + if (std.mem.eql(u8, key.val, "$type")) { 813 + // Read the value as text 814 + const val = readText(data, cur) catch return null; 815 + return val.val; 816 + } 817 + 818 + // Skip the value 819 + cur = try skipValue(data, cur); 820 + } 821 + 822 + return null; 823 + } 824 + 825 + // === low-level write API === 826 + 827 + /// Write CBOR initial byte + argument using shortest encoding. 828 + /// Returns new position after written bytes. Caller must ensure buf is large enough. 829 + pub fn writeArg(buf: []u8, pos: usize, major: u3, val: u64) usize { 830 + const prefix: u8 = @as(u8, major) << 5; 831 + if (val < 24) { 832 + buf[pos] = prefix | @as(u8, @intCast(val)); 833 + return pos + 1; 834 + } else if (val <= 0xff) { 835 + buf[pos] = prefix | 24; 836 + buf[pos + 1] = @intCast(val); 837 + return pos + 2; 838 + } else if (val <= 0xffff) { 839 + buf[pos] = prefix | 25; 840 + const v: u16 = @intCast(val); 841 + buf[pos + 1] = @truncate(v >> 8); 842 + buf[pos + 2] = @truncate(v); 843 + return pos + 3; 844 + } else if (val <= 0xffffffff) { 845 + buf[pos] = prefix | 26; 846 + const v: u32 = @intCast(val); 847 + buf[pos + 1] = @truncate(v >> 24); 848 + buf[pos + 2] = @truncate(v >> 16); 849 + buf[pos + 3] = @truncate(v >> 8); 850 + buf[pos + 4] = @truncate(v); 851 + return pos + 5; 852 + } else { 853 + buf[pos] = prefix | 27; 854 + buf[pos + 1] = @truncate(val >> 56); 855 + buf[pos + 2] = @truncate(val >> 48); 856 + buf[pos + 3] = @truncate(val >> 40); 857 + buf[pos + 4] = @truncate(val >> 32); 858 + buf[pos + 5] = @truncate(val >> 24); 859 + buf[pos + 6] = @truncate(val >> 16); 860 + buf[pos + 7] = @truncate(val >> 8); 861 + buf[pos + 8] = @truncate(val); 862 + return pos + 9; 863 + } 864 + } 865 + 866 + /// Write CBOR text string header + payload. 867 + pub fn writeText(buf: []u8, pos: usize, text: []const u8) usize { 868 + const p = writeArg(buf, pos, 3, text.len); 869 + @memcpy(buf[p..][0..text.len], text); 870 + return p + text.len; 871 + } 872 + 873 + /// Write CBOR byte string header + payload. 874 + pub fn writeBytes(buf: []u8, pos: usize, bytes: []const u8) usize { 875 + const p = writeArg(buf, pos, 2, bytes.len); 876 + @memcpy(buf[p..][0..bytes.len], bytes); 877 + return p + bytes.len; 878 + } 879 + 880 + /// Write unsigned integer (major 0). 881 + pub fn writeUint(buf: []u8, pos: usize, val: u64) usize { 882 + return writeArg(buf, pos, 0, val); 883 + } 884 + 885 + /// Write signed integer. Positive values use major 0, negative values use major 1. 886 + pub fn writeInt(buf: []u8, pos: usize, val: i64) usize { 887 + if (val >= 0) { 888 + return writeArg(buf, pos, 0, @intCast(val)); 889 + } else { 890 + const raw: u64 = @intCast(-1 - val); 891 + return writeArg(buf, pos, 1, raw); 892 + } 893 + } 894 + 895 + /// Write map header (major 5). 896 + pub fn writeMapHeader(buf: []u8, pos: usize, count: usize) usize { 897 + return writeArg(buf, pos, 5, count); 898 + } 899 + 900 + /// Write array header (major 4). 901 + pub fn writeArrayHeader(buf: []u8, pos: usize, count: usize) usize { 902 + return writeArg(buf, pos, 4, count); 903 + } 904 + 905 + /// Write boolean: 0xf5 (true) or 0xf4 (false). 906 + pub fn writeBool(buf: []u8, pos: usize, val: bool) usize { 907 + buf[pos] = if (val) 0xf5 else 0xf4; 908 + return pos + 1; 909 + } 910 + 911 + /// Write null: 0xf6. 912 + pub fn writeNull(buf: []u8, pos: usize) usize { 913 + buf[pos] = 0xf6; 914 + return pos + 1; 915 + } 916 + 917 + /// Write tag(42) + byte string with 0x00 prefix + CID raw bytes. 918 + pub fn writeCidLink(buf: []u8, pos: usize, cid_raw: []const u8) usize { 919 + var p = writeArg(buf, pos, 6, 42); 920 + p = writeArg(buf, p, 2, 1 + cid_raw.len); 921 + buf[p] = 0x00; 922 + p += 1; 923 + @memcpy(buf[p..][0..cid_raw.len], cid_raw); 924 + return p + cid_raw.len; 925 + } 926 + 505 927 // === tests === 506 928 507 929 test "decode unsigned integers" { ··· 601 1023 defer arena.deinit(); 602 1024 const alloc = arena.allocator(); 603 1025 604 - // {"op": 1, "t": "#commit"} 1026 + // {"t": "#commit", "op": 1} — sorted by key length (1 < 2) 605 1027 const result = try decode(alloc, &.{ 606 1028 0xa2, // map(2) 607 - 0x62, 'o', 'p', 0x01, // "op": 1 608 1029 0x61, 't', 0x67, '#', 'c', 'o', 'm', 'm', 'i', 't', // "t": "#commit" 1030 + 0x62, 'o', 'p', 0x01, // "op": 1 609 1031 }); 610 1032 const val = result.value; 611 1033 try std.testing.expectEqual(@as(u64, 1), val.get("op").?.unsigned); ··· 643 1065 644 1066 const result = try decode(alloc, &.{ 645 1067 0xa3, // map(3) 646 - 0x64, 'n', 'a', 'm', 'e', 0x65, 'a', 'l', 'i', 'c', 'e', // "name": "alice" 647 - 0x63, 'a', 'g', 'e', 0x18, 30, // "age": 30 648 - 0x66, 'a', 'c', 't', 'i', 'v', 'e', 0xf5, // "active": true 1068 + 0x63, 'a', 'g', 'e', 0x18, 30, // "age": 30 (3 bytes, shortest) 1069 + 0x64, 'n', 'a', 'm', 'e', 0x65, 'a', 'l', 'i', 'c', 'e', // "name": "alice" (4 bytes) 1070 + 0x66, 'a', 'c', 't', 'i', 'v', 'e', 0xf5, // "active": true (6 bytes) 649 1071 }); 650 1072 const val = result.value; 651 1073 try std.testing.expectEqualStrings("alice", val.getString("name").?);
+569
src/internal/repo/cbor_bench.zig
··· 1 + //! DAG-CBOR codec benchmarks 2 + //! 3 + //! measures low-level encoding/decoding primitives and full record 4 + //! round-trips to track performance regressions and compare with 5 + //! the atmos (Go) implementation. 6 + //! 7 + //! run: zig build bench -Doptimize=ReleaseFast 8 + //! or: just bench 9 + 10 + const std = @import("std"); 11 + const cbor = @import("cbor.zig"); 12 + const car = @import("car.zig"); 13 + const Value = cbor.Value; 14 + const Cid = cbor.Cid; 15 + 16 + // --------------------------------------------------------------------------- 17 + // benchmark harness 18 + // --------------------------------------------------------------------------- 19 + 20 + const warmup_iters = 1_000; 21 + const min_iters = 10_000; 22 + const target_ns: u64 = 500_000_000; // run each bench for ~500ms 23 + 24 + fn clockNs() u64 { 25 + var ts: std.os.linux.timespec = undefined; 26 + _ = std.os.linux.clock_gettime(.MONOTONIC, &ts); 27 + return @intCast(ts.sec * std.time.ns_per_s + ts.nsec); 28 + } 29 + 30 + fn bench(name: []const u8, comptime func: anytype) void { 31 + // warmup 32 + for (0..warmup_iters) |_| { 33 + func(); 34 + } 35 + 36 + // calibrate: run min_iters, then scale up to fill target_ns 37 + var start = clockNs(); 38 + for (0..min_iters) |_| { 39 + func(); 40 + } 41 + const calibrate_ns = clockNs() - start; 42 + const iters: u64 = if (calibrate_ns == 0) 43 + min_iters * 100 44 + else 45 + @max(min_iters, target_ns * min_iters / calibrate_ns); 46 + 47 + // measured run 48 + start = clockNs(); 49 + for (0..iters) |_| { 50 + func(); 51 + } 52 + const elapsed_ns = clockNs() - start; 53 + const ns_per_op = elapsed_ns / iters; 54 + 55 + std.debug.print(" {s:<40} {d:>8} ns/op ({d} iters)\n", .{ name, ns_per_op, iters }); 56 + } 57 + 58 + // --------------------------------------------------------------------------- 59 + // test data: a realistic AT Protocol record (same structure as atmos bench) 60 + // --------------------------------------------------------------------------- 61 + 62 + const bench_record: Value = .{ .map = &.{ 63 + .{ .key = "$type", .value = .{ .text = "app.bsky.feed.post" } }, 64 + .{ .key = "createdAt", .value = .{ .text = "2024-01-15T12:00:00.000Z" } }, 65 + .{ .key = "langs", .value = .{ .array = &.{.{ .text = "en" }} } }, 66 + .{ .key = "reply", .value = .{ .map = &.{ 67 + .{ .key = "parent", .value = .{ .map = &.{ 68 + .{ .key = "cid", .value = .{ .text = "bafyreib3pwrff2yadznophzf4hcvtyoctwzcujvz7x4pngk2isicz7yszq" } }, 69 + .{ .key = "uri", .value = .{ .text = "at://did:plc:4nendwqrs754gt6qvgr56jmn/app.bsky.feed.post/3medg2qvcuc2c" } }, 70 + } } }, 71 + .{ .key = "root", .value = .{ .map = &.{ 72 + .{ .key = "cid", .value = .{ .text = "bafyreib3pwrff2yadznophzf4hcvtyoctwzcujvz7x4pngk2isicz7yszq" } }, 73 + .{ .key = "uri", .value = .{ .text = "at://did:plc:4nendwqrs754gt6qvgr56jmn/app.bsky.feed.post/3medg2qvcuc2c" } }, 74 + } } }, 75 + } } }, 76 + .{ .key = "text", .value = .{ .text = "Hello, world! This is a test post with some content." } }, 77 + } }; 78 + 79 + const bench_text_literal = "Hello, world! This is a test post with some content."; 80 + 81 + // pre-encoded data (initialized at runtime in initBenchData so the compiler 82 + // cannot constant-fold through them — matches real production conditions 83 + // where inputs arrive from the network) 84 + var encoded_record: []const u8 = undefined; 85 + var encoded_text: []const u8 = undefined; 86 + var encoded_uint: []const u8 = undefined; 87 + var encoded_cid_link: []const u8 = undefined; 88 + var bench_cid: Cid = undefined; 89 + var bench_arena: std.heap.ArenaAllocator = undefined; 90 + // runtime-opaque text for write benchmarks (same content as bench_text_literal 91 + // but not visible to the optimizer as a comptime constant) 92 + var bench_text: []const u8 = undefined; 93 + 94 + // CAR benchmark data 95 + var car_bytes: []const u8 = undefined; 96 + var car_5_blocks: []const u8 = undefined; 97 + 98 + fn initBenchData() void { 99 + bench_arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 100 + const alloc = bench_arena.allocator(); 101 + 102 + encoded_record = cbor.encodeAlloc(alloc, bench_record) catch @panic("encode record"); 103 + bench_text = alloc.dupe(u8, bench_text_literal) catch @panic("dupe text"); 104 + encoded_text = cbor.encodeAlloc(alloc, .{ .text = bench_text }) catch @panic("encode text"); 105 + encoded_uint = cbor.encodeAlloc(alloc, .{ .unsigned = 1_234_567_890 }) catch @panic("encode uint"); 106 + bench_cid = Cid.forDagCbor(alloc, encoded_record) catch @panic("compute cid"); 107 + encoded_cid_link = cbor.encodeAlloc(alloc, .{ .cid = bench_cid }) catch @panic("encode cid"); 108 + 109 + // build CAR test data: 1-block CAR 110 + car_bytes = car.writeAlloc(alloc, .{ 111 + .roots = &.{bench_cid}, 112 + .blocks = &.{.{ .cid_raw = bench_cid.raw, .data = encoded_record }}, 113 + }) catch @panic("write car"); 114 + 115 + // 5-block CAR — each block has unique text to produce unique CIDs 116 + const block_texts = [_][]const u8{ "block-0", "block-1", "block-2", "block-3", "block-4" }; 117 + var blocks5: [5]car.Block = undefined; 118 + var cids5: [5]Cid = undefined; 119 + for (&blocks5, &cids5, block_texts) |*b, *c, text| { 120 + const rec = cbor.encodeAlloc(alloc, .{ .map = &.{ 121 + .{ .key = "text", .value = .{ .text = text } }, 122 + } }) catch @panic("encode block"); 123 + c.* = Cid.forDagCbor(alloc, rec) catch @panic("cid"); 124 + b.* = .{ .cid_raw = c.raw, .data = rec }; 125 + } 126 + car_5_blocks = car.writeAlloc(alloc, .{ 127 + .roots = &.{cids5[0]}, 128 + .blocks = &blocks5, 129 + }) catch @panic("write 5-block car"); 130 + } 131 + 132 + // --------------------------------------------------------------------------- 133 + // shared allocator for benchmarks 134 + // 135 + // uses a FixedBufferAllocator over a stack buffer so we measure codec 136 + // work, not mmap/munmap syscalls. the encoder needs temp space for map 137 + // key sorting; the decoder needs space for Value arrays/map entries. 138 + // a 16 KB buffer is more than enough for the bench record. 139 + // --------------------------------------------------------------------------- 140 + 141 + // --------------------------------------------------------------------------- 142 + // individual benchmarks 143 + // --------------------------------------------------------------------------- 144 + 145 + // --- full record encode/decode --- 146 + 147 + fn benchMarshal() void { 148 + var scratch: [4096]u8 = undefined; 149 + var fba = std.heap.FixedBufferAllocator.init(&scratch); 150 + var out_buf: [1024]u8 = undefined; 151 + var w: std.Io.Writer = .fixed(&out_buf); 152 + cbor.encode(fba.allocator(), &w, bench_record) catch @panic("encode"); 153 + std.mem.doNotOptimizeAway(w.end); 154 + } 155 + 156 + fn benchUnmarshal() void { 157 + var scratch: [8192]u8 = undefined; 158 + var fba = std.heap.FixedBufferAllocator.init(&scratch); 159 + const val = cbor.decodeAll(fba.allocator(), encoded_record) catch @panic("decode"); 160 + std.mem.doNotOptimizeAway(val); 161 + } 162 + 163 + fn benchMarshalRoundTrip() void { 164 + var scratch: [16384]u8 = undefined; 165 + var fba = std.heap.FixedBufferAllocator.init(&scratch); 166 + const alloc = fba.allocator(); 167 + const enc = cbor.encodeAlloc(alloc, bench_record) catch @panic("encode"); 168 + const dec = cbor.decodeAll(alloc, enc) catch @panic("decode"); 169 + std.mem.doNotOptimizeAway(dec); 170 + } 171 + 172 + fn benchDecodeReencode() void { 173 + var scratch: [16384]u8 = undefined; 174 + var fba = std.heap.FixedBufferAllocator.init(&scratch); 175 + const alloc = fba.allocator(); 176 + const dec = cbor.decodeAll(alloc, encoded_record) catch @panic("decode"); 177 + const enc = cbor.encodeAlloc(alloc, dec) catch @panic("encode"); 178 + std.mem.doNotOptimizeAway(enc); 179 + } 180 + 181 + // --- CID computation --- 182 + 183 + fn benchComputeCID() void { 184 + var scratch: [256]u8 = undefined; 185 + var fba = std.heap.FixedBufferAllocator.init(&scratch); 186 + const cid = Cid.forDagCbor(fba.allocator(), encoded_record) catch @panic("cid"); 187 + std.mem.doNotOptimizeAway(cid); 188 + } 189 + 190 + fn benchEncodeAndCID() void { 191 + var scratch: [8192]u8 = undefined; 192 + var fba = std.heap.FixedBufferAllocator.init(&scratch); 193 + const alloc = fba.allocator(); 194 + const enc = cbor.encodeAlloc(alloc, bench_record) catch @panic("encode"); 195 + const cid = Cid.forDagCbor(alloc, enc) catch @panic("cid"); 196 + std.mem.doNotOptimizeAway(cid); 197 + } 198 + 199 + // --- text string encode/decode --- 200 + 201 + fn benchEncodeText() void { 202 + var buf: [128]u8 = undefined; 203 + var w: std.Io.Writer = .fixed(&buf); 204 + cbor.encode(std.heap.page_allocator, &w, .{ .text = bench_text }) catch @panic("encode"); 205 + std.mem.doNotOptimizeAway(w.end); 206 + } 207 + 208 + fn benchDecodeText() void { 209 + // text decoding doesn't allocate — pass a failing allocator to prove it 210 + const val = cbor.decodeAll(std.heap.page_allocator, encoded_text) catch @panic("decode"); 211 + std.mem.doNotOptimizeAway(val); 212 + } 213 + 214 + // --- unsigned integer encode/decode --- 215 + 216 + fn benchEncodeUint() void { 217 + var buf: [16]u8 = undefined; 218 + var w: std.Io.Writer = .fixed(&buf); 219 + cbor.encode(std.heap.page_allocator, &w, .{ .unsigned = 1_234_567_890 }) catch @panic("encode"); 220 + std.mem.doNotOptimizeAway(w.end); 221 + } 222 + 223 + fn benchDecodeUint() void { 224 + // uint decoding doesn't allocate 225 + const val = cbor.decodeAll(std.heap.page_allocator, encoded_uint) catch @panic("decode"); 226 + std.mem.doNotOptimizeAway(val); 227 + } 228 + 229 + // --- CID link encode/decode --- 230 + 231 + fn benchEncodeCidLink() void { 232 + var buf: [128]u8 = undefined; 233 + var w: std.Io.Writer = .fixed(&buf); 234 + cbor.encode(std.heap.page_allocator, &w, .{ .cid = bench_cid }) catch @panic("encode"); 235 + std.mem.doNotOptimizeAway(w.end); 236 + } 237 + 238 + fn benchDecodeCidLink() void { 239 + // CID link decoding doesn't allocate (borrows from input bytes) 240 + const val = cbor.decodeAll(std.heap.page_allocator, encoded_cid_link) catch @panic("decode"); 241 + std.mem.doNotOptimizeAway(val); 242 + } 243 + 244 + // --- map key lookup --- 245 + 246 + fn benchMapKeyLookup() void { 247 + var scratch: [8192]u8 = undefined; 248 + var fba = std.heap.FixedBufferAllocator.init(&scratch); 249 + const val = cbor.decodeAll(fba.allocator(), encoded_record) catch @panic("decode"); 250 + std.mem.doNotOptimizeAway(val.getString("text")); 251 + std.mem.doNotOptimizeAway(val.getString("$type")); 252 + std.mem.doNotOptimizeAway(val.getString("createdAt")); 253 + } 254 + 255 + // --- varint encode/decode --- 256 + 257 + fn benchWriteUvarint() void { 258 + var buf: [16]u8 = undefined; 259 + var w: std.Io.Writer = .fixed(&buf); 260 + cbor.writeUvarint(&w, 1_234_567_890) catch @panic("write"); 261 + std.mem.doNotOptimizeAway(w.end); 262 + } 263 + 264 + fn benchReadUvarint() void { 265 + // pre-encoded varint for 1_234_567_890 266 + const data = [_]u8{ 0xd2, 0x85, 0xd8, 0xcc, 0x04 }; 267 + var pos: usize = 0; 268 + const val = cbor.readUvarint(&data, &pos); 269 + std.mem.doNotOptimizeAway(val); 270 + } 271 + 272 + // --- diagnostic: isolate encode costs --- 273 + 274 + fn benchEncodeRecordNoSort() void { 275 + // encode with keys already in DAG-CBOR order (no sort needed) 276 + // bench_record keys are already sorted, so the sort is a no-op, 277 + // but we still pay for allocator.dupe + allocator.free per map. 278 + // this measures the sorting overhead vs raw encoding. 279 + var scratch: [4096]u8 = undefined; 280 + var fba = std.heap.FixedBufferAllocator.init(&scratch); 281 + var out_buf: [1024]u8 = undefined; 282 + var w: std.Io.Writer = .fixed(&out_buf); 283 + cbor.encode(fba.allocator(), &w, bench_record) catch @panic("encode"); 284 + std.mem.doNotOptimizeAway(w.end); 285 + } 286 + 287 + fn benchDecodeRecordNoValidation() void { 288 + // decode without UTF-8 validation or key order checks 289 + // (not possible with current API — this measures the same as benchUnmarshal 290 + // to show the overhead of validation is included) 291 + var scratch: [8192]u8 = undefined; 292 + var fba = std.heap.FixedBufferAllocator.init(&scratch); 293 + const val = cbor.decodeAll(fba.allocator(), encoded_record) catch @panic("decode"); 294 + std.mem.doNotOptimizeAway(val); 295 + } 296 + 297 + // --- diagnostic: UTF-8 validation cost --- 298 + 299 + fn benchUtf8Validate() void { 300 + // just the UTF-8 validation on the encoded record's text content 301 + // the record has ~300 bytes of text across all string fields 302 + std.mem.doNotOptimizeAway(std.unicode.utf8ValidateSlice(encoded_record)); 303 + } 304 + 305 + // --- diagnostic: SHA-256 only --- 306 + 307 + fn benchSha256() void { 308 + const Sha256 = std.crypto.hash.sha2.Sha256; 309 + var hash: [Sha256.digest_length]u8 = undefined; 310 + Sha256.hash(encoded_record, &hash, .{}); 311 + std.mem.doNotOptimizeAway(hash); 312 + } 313 + 314 + // --- larger payloads --- 315 + 316 + var encoded_record_10x: []const u8 = undefined; 317 + 318 + fn initLargePayload() void { 319 + const alloc = bench_arena.allocator(); 320 + // build a 10-element array of the bench record 321 + var items: [10]Value = undefined; 322 + for (&items) |*item| { 323 + item.* = bench_record; 324 + } 325 + const large: Value = .{ .array = &items }; 326 + encoded_record_10x = cbor.encodeAlloc(alloc, large) catch @panic("encode 10x"); 327 + } 328 + 329 + fn benchEncodeLarge() void { 330 + var scratch: [65536]u8 = undefined; 331 + var fba = std.heap.FixedBufferAllocator.init(&scratch); 332 + var items: [10]Value = undefined; 333 + for (&items) |*item| { 334 + item.* = bench_record; 335 + } 336 + const large: Value = .{ .array = &items }; 337 + var out_buf: [8192]u8 = undefined; 338 + var w: std.Io.Writer = .fixed(&out_buf); 339 + cbor.encode(fba.allocator(), &w, large) catch @panic("encode"); 340 + std.mem.doNotOptimizeAway(w.end); 341 + } 342 + 343 + fn benchDecodeLarge() void { 344 + var scratch: [65536]u8 = undefined; 345 + var fba = std.heap.FixedBufferAllocator.init(&scratch); 346 + const val = cbor.decodeAll(fba.allocator(), encoded_record_10x) catch @panic("decode"); 347 + std.mem.doNotOptimizeAway(val); 348 + } 349 + 350 + // --- low-level write (buffer-direct) --- 351 + 352 + fn benchWriteTextDirect() void { 353 + var buf: [128]u8 = undefined; 354 + const end = cbor.writeText(&buf, 0, bench_text); 355 + std.mem.doNotOptimizeAway(buf[0..end]); 356 + } 357 + 358 + fn benchWriteUintDirect() void { 359 + var buf: [16]u8 = undefined; 360 + const end = cbor.writeUint(&buf, 0, 1_234_567_890); 361 + std.mem.doNotOptimizeAway(buf[0..end]); 362 + } 363 + 364 + fn benchWriteCidLinkDirect() void { 365 + var buf: [128]u8 = undefined; 366 + const end = cbor.writeCidLink(&buf, 0, bench_cid.raw); 367 + std.mem.doNotOptimizeAway(buf[0..end]); 368 + } 369 + 370 + fn benchWriteRecordDirect() void { 371 + // manually write the bench record using low-level API (simulates generated code) 372 + var buf: [1024]u8 = undefined; 373 + var p: usize = 0; 374 + p = cbor.writeMapHeader(&buf, p, 5); 375 + // keys in DAG-CBOR order: text(4), $type(5), langs(5), reply(5), createdAt(9) 376 + p = cbor.writeText(&buf, p, "text"); 377 + p = cbor.writeText(&buf, p, bench_text); 378 + p = cbor.writeText(&buf, p, "$type"); 379 + p = cbor.writeText(&buf, p, "app.bsky.feed.post"); 380 + p = cbor.writeText(&buf, p, "langs"); 381 + p = cbor.writeArrayHeader(&buf, p, 1); 382 + p = cbor.writeText(&buf, p, "en"); 383 + p = cbor.writeText(&buf, p, "reply"); 384 + p = cbor.writeMapHeader(&buf, p, 2); 385 + p = cbor.writeText(&buf, p, "parent"); 386 + p = cbor.writeMapHeader(&buf, p, 2); 387 + p = cbor.writeText(&buf, p, "cid"); 388 + p = cbor.writeText(&buf, p, "bafyreib3pwrff2yadznophzf4hcvtyoctwzcujvz7x4pngk2isicz7yszq"); 389 + p = cbor.writeText(&buf, p, "uri"); 390 + p = cbor.writeText(&buf, p, "at://did:plc:4nendwqrs754gt6qvgr56jmn/app.bsky.feed.post/3medg2qvcuc2c"); 391 + p = cbor.writeText(&buf, p, "root"); 392 + p = cbor.writeMapHeader(&buf, p, 2); 393 + p = cbor.writeText(&buf, p, "cid"); 394 + p = cbor.writeText(&buf, p, "bafyreib3pwrff2yadznophzf4hcvtyoctwzcujvz7x4pngk2isicz7yszq"); 395 + p = cbor.writeText(&buf, p, "uri"); 396 + p = cbor.writeText(&buf, p, "at://did:plc:4nendwqrs754gt6qvgr56jmn/app.bsky.feed.post/3medg2qvcuc2c"); 397 + p = cbor.writeText(&buf, p, "createdAt"); 398 + p = cbor.writeText(&buf, p, "2024-01-15T12:00:00.000Z"); 399 + std.mem.doNotOptimizeAway(buf[0..p]); 400 + } 401 + 402 + // --- low-level read (buffer-direct) --- 403 + 404 + fn benchReadTextDirect() void { 405 + const r = cbor.readText(encoded_text, 0) catch @panic("readText"); 406 + std.mem.doNotOptimizeAway(r); 407 + } 408 + 409 + fn benchReadUintDirect() void { 410 + const r = cbor.readUint(encoded_uint, 0) catch @panic("readUint"); 411 + std.mem.doNotOptimizeAway(r); 412 + } 413 + 414 + fn benchReadCidLinkDirect() void { 415 + const r = cbor.readCidLink(encoded_cid_link, 0) catch @panic("readCidLink"); 416 + std.mem.doNotOptimizeAway(r); 417 + } 418 + 419 + fn benchSkipValue() void { 420 + const end = cbor.skipValue(encoded_record, 0) catch @panic("skipValue"); 421 + std.mem.doNotOptimizeAway(end); 422 + } 423 + 424 + fn benchPeekType() void { 425 + const typ = cbor.peekType(encoded_record) catch @panic("peekType"); 426 + std.mem.doNotOptimizeAway(typ); 427 + } 428 + 429 + // --- CID: stack vs heap allocation --- 430 + 431 + fn benchComputeCIDStack() void { 432 + // compute CID writing to a stack buffer (no allocator) 433 + const Sha256 = std.crypto.hash.sha2.Sha256; 434 + var hash: [Sha256.digest_length]u8 = undefined; 435 + Sha256.hash(encoded_record, &hash, .{}); 436 + // manually build CID bytes on stack: version(1) + codec(0x71) + hash_fn(0x12) + len(0x20) + hash 437 + var cid_buf: [36]u8 = undefined; 438 + cid_buf[0] = 0x01; 439 + cid_buf[1] = 0x71; 440 + cid_buf[2] = 0x12; 441 + cid_buf[3] = 0x20; 442 + @memcpy(cid_buf[4..36], &hash); 443 + std.mem.doNotOptimizeAway(cid_buf); 444 + } 445 + 446 + // --- CAR benchmarks --- 447 + 448 + fn benchCarRead1() void { 449 + var scratch: [8192]u8 = undefined; 450 + var fba = std.heap.FixedBufferAllocator.init(&scratch); 451 + const parsed = car.readWithOptions(fba.allocator(), car_bytes, .{ 452 + .verify_block_hashes = true, 453 + }) catch @panic("read car"); 454 + std.mem.doNotOptimizeAway(parsed); 455 + } 456 + 457 + fn benchCarRead1NoVerify() void { 458 + var scratch: [8192]u8 = undefined; 459 + var fba = std.heap.FixedBufferAllocator.init(&scratch); 460 + const parsed = car.readWithOptions(fba.allocator(), car_bytes, .{ 461 + .verify_block_hashes = false, 462 + }) catch @panic("read car"); 463 + std.mem.doNotOptimizeAway(parsed); 464 + } 465 + 466 + fn benchCarRead5() void { 467 + var scratch: [32768]u8 = undefined; 468 + var fba = std.heap.FixedBufferAllocator.init(&scratch); 469 + const parsed = car.readWithOptions(fba.allocator(), car_5_blocks, .{ 470 + .verify_block_hashes = true, 471 + }) catch @panic("read car"); 472 + std.mem.doNotOptimizeAway(parsed); 473 + } 474 + 475 + fn benchCarWrite1() void { 476 + var out_buf: [2048]u8 = undefined; 477 + var w: std.Io.Writer = .fixed(&out_buf); 478 + var scratch: [2048]u8 = undefined; 479 + var fba = std.heap.FixedBufferAllocator.init(&scratch); 480 + car.write(fba.allocator(), &w, .{ 481 + .roots = &.{bench_cid}, 482 + .blocks = &.{.{ .cid_raw = bench_cid.raw, .data = encoded_record }}, 483 + }) catch @panic("write car"); 484 + std.mem.doNotOptimizeAway(w.end); 485 + } 486 + 487 + fn benchCarRoundTrip1() void { 488 + var scratch: [16384]u8 = undefined; 489 + var fba = std.heap.FixedBufferAllocator.init(&scratch); 490 + const alloc = fba.allocator(); 491 + const parsed = car.read(alloc, car_bytes) catch @panic("read"); 492 + const written = car.writeAlloc(alloc, parsed) catch @panic("write"); 493 + std.mem.doNotOptimizeAway(written); 494 + } 495 + 496 + // --------------------------------------------------------------------------- 497 + // main 498 + // --------------------------------------------------------------------------- 499 + 500 + pub fn main() void { 501 + initBenchData(); 502 + initLargePayload(); 503 + defer bench_arena.deinit(); 504 + 505 + std.debug.print("\nDAG-CBOR benchmarks (record: {d} bytes encoded)\n", .{encoded_record.len}); 506 + std.debug.print("{s}\n\n", .{"=" ** 68}); 507 + 508 + std.debug.print("record encode/decode:\n", .{}); 509 + bench("encode record", benchMarshal); 510 + bench("decode record", benchUnmarshal); 511 + bench("encode + decode round-trip", benchMarshalRoundTrip); 512 + bench("decode + re-encode", benchDecodeReencode); 513 + 514 + std.debug.print("\nCID operations:\n", .{}); 515 + bench("compute CID (SHA-256)", benchComputeCID); 516 + bench("compute CID (stack, no alloc)", benchComputeCIDStack); 517 + bench("SHA-256 only (434 bytes)", benchSha256); 518 + bench("encode + compute CID", benchEncodeAndCID); 519 + 520 + std.debug.print("\ntext string:\n", .{}); 521 + bench("encode text (54 bytes)", benchEncodeText); 522 + bench("decode text (54 bytes)", benchDecodeText); 523 + 524 + std.debug.print("\nunsigned integer:\n", .{}); 525 + bench("encode uint (1234567890)", benchEncodeUint); 526 + bench("decode uint (1234567890)", benchDecodeUint); 527 + 528 + std.debug.print("\nCID link:\n", .{}); 529 + bench("encode CID link", benchEncodeCidLink); 530 + bench("decode CID link", benchDecodeCidLink); 531 + 532 + std.debug.print("\nvarint:\n", .{}); 533 + bench("write uvarint (1234567890)", benchWriteUvarint); 534 + bench("read uvarint (1234567890)", benchReadUvarint); 535 + 536 + std.debug.print("\ncomposite:\n", .{}); 537 + bench("decode + key lookup (3 keys)", benchMapKeyLookup); 538 + 539 + std.debug.print("\nCAR v1 ({d} bytes, 1 block):\n", .{car_bytes.len}); 540 + bench("read CAR (with hash verify)", benchCarRead1); 541 + bench("read CAR (no verify)", benchCarRead1NoVerify); 542 + bench("write CAR", benchCarWrite1); 543 + bench("read + write round-trip", benchCarRoundTrip1); 544 + 545 + std.debug.print("\nCAR v1 ({d} bytes, 5 blocks):\n", .{car_5_blocks.len}); 546 + bench("read CAR 5 blocks (verified)", benchCarRead5); 547 + 548 + std.debug.print("\nlow-level write (buffer-direct):\n", .{}); 549 + bench("writeText (54 bytes)", benchWriteTextDirect); 550 + bench("writeUint (1234567890)", benchWriteUintDirect); 551 + bench("writeCidLink", benchWriteCidLinkDirect); 552 + bench("writeRecord (manual, 434 bytes)", benchWriteRecordDirect); 553 + 554 + std.debug.print("\nlow-level read (buffer-direct):\n", .{}); 555 + bench("readText (54 bytes)", benchReadTextDirect); 556 + bench("readUint (1234567890)", benchReadUintDirect); 557 + bench("readCidLink", benchReadCidLinkDirect); 558 + bench("skipValue (434-byte record)", benchSkipValue); 559 + bench("peekType (434-byte record)", benchPeekType); 560 + 561 + std.debug.print("\ndiagnostic (cost breakdown):\n", .{}); 562 + bench("UTF-8 validate (434 bytes)", benchUtf8Validate); 563 + 564 + std.debug.print("\nscaling (10x array = {d} bytes):\n", .{encoded_record_10x.len}); 565 + bench("encode 10x records", benchEncodeLarge); 566 + bench("decode 10x records", benchDecodeLarge); 567 + 568 + std.debug.print("\n", .{}); 569 + }
+502
src/internal/repo/cbor_read_test.zig
··· 1 + const std = @import("std"); 2 + const cbor = @import("cbor.zig"); 3 + const readArg = cbor.readArg; 4 + const Arg = cbor.Arg; 5 + const readText = cbor.readText; 6 + const readBytes = cbor.readBytes; 7 + const readUint = cbor.readUint; 8 + const readInt = cbor.readInt; 9 + const readBool = cbor.readBool; 10 + const readNull = cbor.readNull; 11 + const readMapHeader = cbor.readMapHeader; 12 + const readArrayHeader = cbor.readArrayHeader; 13 + const readCidLink = cbor.readCidLink; 14 + 15 + // --------------------------------------------------------------------------- 16 + // Inline values 0-23 (major type 0 = unsigned) 17 + // --------------------------------------------------------------------------- 18 + 19 + test "readArg: inline value 0" { 20 + const data = [_]u8{0x00}; // major 0, additional 0 21 + const arg = try readArg(&data, 0); 22 + try std.testing.expectEqual(@as(u3, 0), arg.major); 23 + try std.testing.expectEqual(@as(u64, 0), arg.val); 24 + try std.testing.expectEqual(@as(usize, 1), arg.end); 25 + } 26 + 27 + test "readArg: inline value 1" { 28 + const data = [_]u8{0x01}; 29 + const arg = try readArg(&data, 0); 30 + try std.testing.expectEqual(@as(u3, 0), arg.major); 31 + try std.testing.expectEqual(@as(u64, 1), arg.val); 32 + try std.testing.expectEqual(@as(usize, 1), arg.end); 33 + } 34 + 35 + test "readArg: inline value 23" { 36 + const data = [_]u8{0x17}; // major 0, additional 23 37 + const arg = try readArg(&data, 0); 38 + try std.testing.expectEqual(@as(u3, 0), arg.major); 39 + try std.testing.expectEqual(@as(u64, 23), arg.val); 40 + try std.testing.expectEqual(@as(usize, 1), arg.end); 41 + } 42 + 43 + // --------------------------------------------------------------------------- 44 + // 1-byte value (additional info = 24) 45 + // --------------------------------------------------------------------------- 46 + 47 + test "readArg: 1-byte value 24" { 48 + const data = [_]u8{ 0x18, 24 }; // major 0, additional 24, payload 24 49 + const arg = try readArg(&data, 0); 50 + try std.testing.expectEqual(@as(u3, 0), arg.major); 51 + try std.testing.expectEqual(@as(u64, 24), arg.val); 52 + try std.testing.expectEqual(@as(usize, 2), arg.end); 53 + } 54 + 55 + test "readArg: 1-byte value 255" { 56 + const data = [_]u8{ 0x18, 0xff }; // major 0, additional 24, payload 255 57 + const arg = try readArg(&data, 0); 58 + try std.testing.expectEqual(@as(u3, 0), arg.major); 59 + try std.testing.expectEqual(@as(u64, 255), arg.val); 60 + try std.testing.expectEqual(@as(usize, 2), arg.end); 61 + } 62 + 63 + // --------------------------------------------------------------------------- 64 + // 2-byte value (additional info = 25) 65 + // --------------------------------------------------------------------------- 66 + 67 + test "readArg: 2-byte value 256" { 68 + const data = [_]u8{ 0x19, 0x01, 0x00 }; // major 0, additional 25, payload 256 big-endian 69 + const arg = try readArg(&data, 0); 70 + try std.testing.expectEqual(@as(u3, 0), arg.major); 71 + try std.testing.expectEqual(@as(u64, 256), arg.val); 72 + try std.testing.expectEqual(@as(usize, 3), arg.end); 73 + } 74 + 75 + // --------------------------------------------------------------------------- 76 + // 4-byte value (additional info = 26) 77 + // --------------------------------------------------------------------------- 78 + 79 + test "readArg: 4-byte value 65536" { 80 + const data = [_]u8{ 0x1a, 0x00, 0x01, 0x00, 0x00 }; // major 0, additional 26, payload 65536 81 + const arg = try readArg(&data, 0); 82 + try std.testing.expectEqual(@as(u3, 0), arg.major); 83 + try std.testing.expectEqual(@as(u64, 65536), arg.val); 84 + try std.testing.expectEqual(@as(usize, 5), arg.end); 85 + } 86 + 87 + // --------------------------------------------------------------------------- 88 + // 8-byte value (additional info = 27) 89 + // --------------------------------------------------------------------------- 90 + 91 + test "readArg: 8-byte value 0x100000000" { 92 + // major 0, additional 27, payload 0x00_00_00_01_00_00_00_00 93 + const data = [_]u8{ 0x1b, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00 }; 94 + const arg = try readArg(&data, 0); 95 + try std.testing.expectEqual(@as(u3, 0), arg.major); 96 + try std.testing.expectEqual(@as(u64, 0x100000000), arg.val); 97 + try std.testing.expectEqual(@as(usize, 9), arg.end); 98 + } 99 + 100 + // --------------------------------------------------------------------------- 101 + // Reject non-minimal encodings 102 + // --------------------------------------------------------------------------- 103 + 104 + test "readArg: reject non-minimal 0 encoded as 1-byte" { 105 + // Value 0 encoded with additional=24 payload=0x00 (should be inline 0) 106 + const data = [_]u8{ 0x18, 0x00 }; 107 + try std.testing.expectError(error.NonMinimalEncoding, readArg(&data, 0)); 108 + } 109 + 110 + test "readArg: reject non-minimal 255 encoded as 2-byte" { 111 + // Value 255 encoded with additional=25 payload=0x00ff (should be 1-byte) 112 + const data = [_]u8{ 0x19, 0x00, 0xff }; 113 + try std.testing.expectError(error.NonMinimalEncoding, readArg(&data, 0)); 114 + } 115 + 116 + // --------------------------------------------------------------------------- 117 + // Reject truncated data 118 + // --------------------------------------------------------------------------- 119 + 120 + test "readArg: reject truncated 2-byte" { 121 + // additional=25 needs 2 payload bytes, but only 1 provided 122 + const data = [_]u8{ 0x19, 0x01 }; 123 + try std.testing.expectError(error.UnexpectedEof, readArg(&data, 0)); 124 + } 125 + 126 + // --------------------------------------------------------------------------- 127 + // Reject reserved additional info (28-30) 128 + // --------------------------------------------------------------------------- 129 + 130 + test "readArg: reject reserved additional info 28" { 131 + const data = [_]u8{0x1c}; // major 0, additional 28 132 + try std.testing.expectError(error.ReservedAdditionalInfo, readArg(&data, 0)); 133 + } 134 + 135 + // --------------------------------------------------------------------------- 136 + // Reject indefinite length (additional info = 31) 137 + // --------------------------------------------------------------------------- 138 + 139 + test "readArg: reject indefinite length 31" { 140 + const data = [_]u8{0x5f}; // major 2 (byte string), additional 31 141 + try std.testing.expectError(error.IndefiniteLength, readArg(&data, 0)); 142 + } 143 + 144 + // --------------------------------------------------------------------------- 145 + // Non-zero start position 146 + // --------------------------------------------------------------------------- 147 + 148 + test "readArg: non-zero start position" { 149 + // prefix byte 0xAA, then a valid CBOR unsigned 24 at position 1 150 + const data = [_]u8{ 0xaa, 0x18, 24 }; 151 + const arg = try readArg(&data, 1); 152 + try std.testing.expectEqual(@as(u3, 0), arg.major); 153 + try std.testing.expectEqual(@as(u64, 24), arg.val); 154 + try std.testing.expectEqual(@as(usize, 3), arg.end); 155 + } 156 + 157 + test "readArg: non-zero start position with different major type" { 158 + // At position 2: 0x63 = major 3 (text string), additional 3 (inline length 3) 159 + const data = [_]u8{ 0x00, 0x00, 0x63, 0x66, 0x6f, 0x6f }; 160 + const arg = try readArg(&data, 2); 161 + try std.testing.expectEqual(@as(u3, 3), arg.major); 162 + try std.testing.expectEqual(@as(u64, 3), arg.val); 163 + try std.testing.expectEqual(@as(usize, 3), arg.end); 164 + } 165 + 166 + // --------------------------------------------------------------------------- 167 + // EOF at start position 168 + // --------------------------------------------------------------------------- 169 + 170 + test "readArg: empty data returns UnexpectedEof" { 171 + const data = [_]u8{}; 172 + try std.testing.expectError(error.UnexpectedEof, readArg(&data, 0)); 173 + } 174 + 175 + test "readArg: pos beyond data returns UnexpectedEof" { 176 + const data = [_]u8{0x00}; 177 + try std.testing.expectError(error.UnexpectedEof, readArg(&data, 1)); 178 + } 179 + 180 + // =========================================================================== 181 + // readText 182 + // =========================================================================== 183 + 184 + test "readText: short string 'hello'" { 185 + // 0x65 = major 3 (text), length 5 186 + const data = [_]u8{ 0x65, 'h', 'e', 'l', 'l', 'o' }; 187 + const result = try readText(&data, 0); 188 + try std.testing.expectEqualStrings("hello", result.val); 189 + try std.testing.expectEqual(@as(usize, 6), result.end); 190 + } 191 + 192 + test "readText: empty string" { 193 + const data = [_]u8{0x60}; // major 3, length 0 194 + const result = try readText(&data, 0); 195 + try std.testing.expectEqual(@as(usize, 0), result.val.len); 196 + try std.testing.expectEqual(@as(usize, 1), result.end); 197 + } 198 + 199 + test "readText: reject non-text major" { 200 + const data = [_]u8{ 0x45, 'h', 'e', 'l', 'l', 'o' }; // major 2 (bytes), length 5 201 + try std.testing.expectError(error.WrongType, readText(&data, 0)); 202 + } 203 + 204 + test "readText: reject invalid UTF-8" { 205 + // 0x62 = major 3, length 2; 0xff 0xfe is invalid UTF-8 206 + const data = [_]u8{ 0x62, 0xff, 0xfe }; 207 + try std.testing.expectError(error.InvalidUtf8, readText(&data, 0)); 208 + } 209 + 210 + // =========================================================================== 211 + // readBytes 212 + // =========================================================================== 213 + 214 + test "readBytes: 3-byte string" { 215 + const data = [_]u8{ 0x43, 0x01, 0x02, 0x03 }; // major 2, length 3 216 + const result = try readBytes(&data, 0); 217 + try std.testing.expectEqual(@as(usize, 3), result.val.len); 218 + try std.testing.expectEqual(@as(u8, 0x01), result.val[0]); 219 + try std.testing.expectEqual(@as(u8, 0x02), result.val[1]); 220 + try std.testing.expectEqual(@as(u8, 0x03), result.val[2]); 221 + try std.testing.expectEqual(@as(usize, 4), result.end); 222 + } 223 + 224 + test "readBytes: empty bytes" { 225 + const data = [_]u8{0x40}; // major 2, length 0 226 + const result = try readBytes(&data, 0); 227 + try std.testing.expectEqual(@as(usize, 0), result.val.len); 228 + try std.testing.expectEqual(@as(usize, 1), result.end); 229 + } 230 + 231 + // =========================================================================== 232 + // readUint 233 + // =========================================================================== 234 + 235 + test "readUint: value 1000" { 236 + // 0x19 = major 0, additional 25 (2-byte), 0x03e8 = 1000 237 + const data = [_]u8{ 0x19, 0x03, 0xe8 }; 238 + const result = try readUint(&data, 0); 239 + try std.testing.expectEqual(@as(u64, 1000), result.val); 240 + try std.testing.expectEqual(@as(usize, 3), result.end); 241 + } 242 + 243 + test "readUint: reject negative" { 244 + const data = [_]u8{0x20}; // major 1, value 0 => -1 245 + try std.testing.expectError(error.WrongType, readUint(&data, 0)); 246 + } 247 + 248 + // =========================================================================== 249 + // readInt 250 + // =========================================================================== 251 + 252 + test "readInt: positive 42" { 253 + // 0x18 = major 0, additional 24 (1-byte), 42 254 + const data = [_]u8{ 0x18, 42 }; 255 + const result = try readInt(&data, 0); 256 + try std.testing.expectEqual(@as(i64, 42), result.val); 257 + try std.testing.expectEqual(@as(usize, 2), result.end); 258 + } 259 + 260 + test "readInt: negative -10" { 261 + // major 1, value 9 => -1 - 9 = -10 262 + // 0x29 = 0b001_01001 = major 1, additional 9 263 + const data = [_]u8{0x29}; 264 + const result = try readInt(&data, 0); 265 + try std.testing.expectEqual(@as(i64, -10), result.val); 266 + try std.testing.expectEqual(@as(usize, 1), result.end); 267 + } 268 + 269 + test "readInt: reject non-integer" { 270 + const data = [_]u8{0x60}; // major 3 (text), length 0 271 + try std.testing.expectError(error.WrongType, readInt(&data, 0)); 272 + } 273 + 274 + // =========================================================================== 275 + // readBool 276 + // =========================================================================== 277 + 278 + test "readBool: true" { 279 + const data = [_]u8{0xf5}; 280 + const result = try readBool(&data, 0); 281 + try std.testing.expectEqual(true, result.val); 282 + try std.testing.expectEqual(@as(usize, 1), result.end); 283 + } 284 + 285 + test "readBool: false" { 286 + const data = [_]u8{0xf4}; 287 + const result = try readBool(&data, 0); 288 + try std.testing.expectEqual(false, result.val); 289 + try std.testing.expectEqual(@as(usize, 1), result.end); 290 + } 291 + 292 + test "readBool: reject non-bool" { 293 + const data = [_]u8{0xf6}; // null 294 + try std.testing.expectError(error.WrongType, readBool(&data, 0)); 295 + } 296 + 297 + // =========================================================================== 298 + // readNull 299 + // =========================================================================== 300 + 301 + test "readNull: null" { 302 + const data = [_]u8{0xf6}; 303 + const result = try readNull(&data, 0); 304 + try std.testing.expectEqual(@as(usize, 1), result); 305 + } 306 + 307 + test "readNull: reject non-null" { 308 + const data = [_]u8{0xf5}; // true 309 + try std.testing.expectError(error.WrongType, readNull(&data, 0)); 310 + } 311 + 312 + // =========================================================================== 313 + // readMapHeader 314 + // =========================================================================== 315 + 316 + test "readMapHeader: count 2" { 317 + const data = [_]u8{0xa2}; // major 5, length 2 318 + const result = try readMapHeader(&data, 0); 319 + try std.testing.expectEqual(@as(u64, 2), result.val); 320 + try std.testing.expectEqual(@as(usize, 1), result.end); 321 + } 322 + 323 + test "readMapHeader: reject non-map" { 324 + const data = [_]u8{0x82}; // major 4 (array), length 2 325 + try std.testing.expectError(error.WrongType, readMapHeader(&data, 0)); 326 + } 327 + 328 + // =========================================================================== 329 + // readArrayHeader 330 + // =========================================================================== 331 + 332 + test "readArrayHeader: count 3" { 333 + const data = [_]u8{0x83}; // major 4, length 3 334 + const result = try readArrayHeader(&data, 0); 335 + try std.testing.expectEqual(@as(u64, 3), result.val); 336 + try std.testing.expectEqual(@as(usize, 1), result.end); 337 + } 338 + 339 + test "readArrayHeader: reject non-array" { 340 + const data = [_]u8{0xa3}; // major 5 (map), length 3 341 + try std.testing.expectError(error.WrongType, readArrayHeader(&data, 0)); 342 + } 343 + 344 + // =========================================================================== 345 + // readCidLink 346 + // =========================================================================== 347 + 348 + test "readCidLink: valid CID (tag(42) + bytes with 0x00 prefix + 36-byte CIDv1)" { 349 + // tag(42): 0xd8 0x2a 350 + // bytes(37): 0x58 0x25 (37 = 1 prefix + 36 CID) 351 + // 0x00 prefix 352 + // 36-byte CID: 0x01 0x71 0x12 0x20 ++ [0xaa] ** 32 353 + const cid_raw = [_]u8{ 0x01, 0x71, 0x12, 0x20 } ++ [_]u8{0xaa} ** 32; 354 + const data = [_]u8{ 0xd8, 0x2a, 0x58, 0x25, 0x00 } ++ cid_raw; 355 + const result = try readCidLink(&data, 0); 356 + try std.testing.expectEqual(@as(usize, 36), result.val.len); 357 + try std.testing.expectEqual(@as(u8, 0x01), result.val[0]); 358 + try std.testing.expectEqual(@as(u8, 0x71), result.val[1]); 359 + try std.testing.expectEqual(@as(u8, 0x12), result.val[2]); 360 + try std.testing.expectEqual(@as(u8, 0x20), result.val[3]); 361 + try std.testing.expectEqual(@as(u8, 0xaa), result.val[4]); 362 + try std.testing.expectEqual(@as(usize, 4 + 37), result.end); // 4 header bytes (2 tag + 2 bytes hdr) + 37 payload bytes 363 + } 364 + 365 + test "readCidLink: reject non-tag" { 366 + const data = [_]u8{ 0x43, 0x01, 0x02, 0x03 }; // major 2 (bytes), not a tag 367 + try std.testing.expectError(error.WrongType, readCidLink(&data, 0)); 368 + } 369 + 370 + // =========================================================================== 371 + // skipValue 372 + // =========================================================================== 373 + 374 + const skipValue = cbor.skipValue; 375 + const peekType = cbor.peekType; 376 + const peekTypeAt = cbor.peekTypeAt; 377 + const encodeAlloc = cbor.encodeAlloc; 378 + const Value = cbor.Value; 379 + 380 + test "skipValue: unsigned integer (1 byte)" { 381 + // 0x05 = major 0, value 5 382 + const data = [_]u8{0x05}; 383 + const end = try skipValue(&data, 0); 384 + try std.testing.expectEqual(@as(usize, 1), end); 385 + } 386 + 387 + test "skipValue: text string (header + payload)" { 388 + // 0x65 = major 3, length 5 + "hello" 389 + const data = [_]u8{ 0x65, 'h', 'e', 'l', 'l', 'o' }; 390 + const end = try skipValue(&data, 0); 391 + try std.testing.expectEqual(@as(usize, 6), end); 392 + } 393 + 394 + test "skipValue: nested map {\"a\": [1, 2]}" { 395 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 396 + defer arena.deinit(); 397 + const alloc = arena.allocator(); 398 + 399 + // Build {"a": [1, 2]} using the encoder 400 + const val = Value{ .map = &.{ 401 + .{ .key = "a", .value = .{ .array = &.{ 402 + .{ .unsigned = 1 }, 403 + .{ .unsigned = 2 }, 404 + } } }, 405 + } }; 406 + const encoded = try encodeAlloc(alloc, val); 407 + const end = try skipValue(encoded, 0); 408 + try std.testing.expectEqual(encoded.len, end); 409 + } 410 + 411 + test "skipValue: CID link (tag 42 + byte string)" { 412 + // tag(42): 0xd8 0x2a 413 + // bytes(37): 0x58 0x25 (37 = 1 prefix + 36 CID) 414 + // 0x00 prefix + 36-byte CID 415 + const cid_raw = [_]u8{ 0x01, 0x71, 0x12, 0x20 } ++ [_]u8{0xaa} ** 32; 416 + const data = [_]u8{ 0xd8, 0x2a, 0x58, 0x25, 0x00 } ++ cid_raw; 417 + const end = try skipValue(&data, 0); 418 + try std.testing.expectEqual(data.len, end); 419 + } 420 + 421 + test "skipValue: first of two concatenated values" { 422 + // Two values: unsigned 5 (0x05) followed by unsigned 10 (0x0a) 423 + const data = [_]u8{ 0x05, 0x0a }; 424 + const end = try skipValue(&data, 0); 425 + try std.testing.expectEqual(@as(usize, 1), end); 426 + // The second value starts at position 1 427 + try std.testing.expectEqual(@as(u8, 0x0a), data[end]); 428 + } 429 + 430 + test "skipValue: complex record (encoded map)" { 431 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 432 + defer arena.deinit(); 433 + const alloc = arena.allocator(); 434 + 435 + // Encode a realistic map with multiple types 436 + const val = Value{ .map = &.{ 437 + .{ .key = "$type", .value = .{ .text = "app.bsky.feed.post" } }, 438 + .{ .key = "createdAt", .value = .{ .text = "2024-01-01T00:00:00Z" } }, 439 + .{ .key = "text", .value = .{ .text = "hello world" } }, 440 + } }; 441 + const encoded = try encodeAlloc(alloc, val); 442 + const end = try skipValue(encoded, 0); 443 + try std.testing.expectEqual(encoded.len, end); 444 + } 445 + 446 + // =========================================================================== 447 + // peekType 448 + // =========================================================================== 449 + 450 + test "peekType: find $type when present" { 451 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 452 + defer arena.deinit(); 453 + const alloc = arena.allocator(); 454 + 455 + const val = Value{ .map = &.{ 456 + .{ .key = "$type", .value = .{ .text = "app.bsky.feed.post" } }, 457 + .{ .key = "text", .value = .{ .text = "hello" } }, 458 + } }; 459 + const encoded = try encodeAlloc(alloc, val); 460 + const result = try peekType(encoded); 461 + try std.testing.expect(result != null); 462 + try std.testing.expectEqualStrings("app.bsky.feed.post", result.?); 463 + } 464 + 465 + test "peekType: find $type when not first key (DAG-CBOR sort order)" { 466 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 467 + defer arena.deinit(); 468 + const alloc = arena.allocator(); 469 + 470 + // DAG-CBOR sorts by length then lex. Keys "ab" (2 bytes) sorts before 471 + // "$type" (5 bytes), so "$type" won't be first. 472 + const val = Value{ .map = &.{ 473 + .{ .key = "ab", .value = .{ .unsigned = 42 } }, 474 + .{ .key = "$type", .value = .{ .text = "app.bsky.graph.follow" } }, 475 + .{ .key = "zzzzzz", .value = .{ .boolean = true } }, 476 + } }; 477 + const encoded = try encodeAlloc(alloc, val); 478 + const result = try peekType(encoded); 479 + try std.testing.expect(result != null); 480 + try std.testing.expectEqualStrings("app.bsky.graph.follow", result.?); 481 + } 482 + 483 + test "peekType: return null when no $type field" { 484 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 485 + defer arena.deinit(); 486 + const alloc = arena.allocator(); 487 + 488 + const val = Value{ .map = &.{ 489 + .{ .key = "text", .value = .{ .text = "hello" } }, 490 + .{ .key = "count", .value = .{ .unsigned = 5 } }, 491 + } }; 492 + const encoded = try encodeAlloc(alloc, val); 493 + const result = try peekType(encoded); 494 + try std.testing.expect(result == null); 495 + } 496 + 497 + test "peekType: return null for non-map input" { 498 + // An unsigned integer, not a map 499 + const data = [_]u8{0x05}; 500 + const result = try peekType(&data); 501 + try std.testing.expect(result == null); 502 + }
+175
src/internal/repo/cbor_rfc8949_test.zig
··· 1 + //! RFC 8949 test vector compliance for DAG-CBOR 2 + //! 3 + //! runs the canonical CBOR specification test vectors from RFC 8949 Appendix A. 4 + //! vectors are classified into three categories: 5 + //! 1. invalid — must be rejected by the decoder 6 + //! 2. valid + canonical — must decode and re-encode to identical bytes 7 + //! 3. valid + non-canonical — must be rejected by DAG-CBOR (requires shortest form) 8 + //! 9 + //! vector source: https://github.com/cbor/test-vectors 10 + 11 + const std = @import("std"); 12 + const cbor = @import("cbor.zig"); 13 + 14 + const vectors_json = @embedFile("testdata/rfc8949-vectors.json"); 15 + 16 + const Vector = struct { 17 + hex: []const u8, 18 + flags: []const []const u8, 19 + features: []const []const u8 = &.{}, 20 + diagnostic: []const u8 = "", 21 + 22 + fn hasFlag(self: Vector, flag: []const u8) bool { 23 + for (self.flags) |f| { 24 + if (std.mem.eql(u8, f, flag)) return true; 25 + } 26 + return false; 27 + } 28 + 29 + fn hasFeature(self: Vector, feature: []const u8) bool { 30 + for (self.features) |f| { 31 + if (std.mem.eql(u8, f, feature)) return true; 32 + } 33 + return false; 34 + } 35 + }; 36 + 37 + fn parseVectors(allocator: std.mem.Allocator) !std.json.Parsed([]const Vector) { 38 + return std.json.parseFromSlice( 39 + []const Vector, 40 + allocator, 41 + vectors_json, 42 + .{ .ignore_unknown_fields = true, .allocate = .alloc_always }, 43 + ); 44 + } 45 + 46 + fn hexToBytes(allocator: std.mem.Allocator, hex: []const u8) ![]u8 { 47 + if (hex.len % 2 != 0) return error.InvalidHex; 48 + const out = try allocator.alloc(u8, hex.len / 2); 49 + for (0..out.len) |i| { 50 + out[i] = std.fmt.parseInt(u8, hex[i * 2 ..][0..2], 16) catch return error.InvalidHex; 51 + } 52 + return out; 53 + } 54 + 55 + /// features and patterns that are outside DAG-CBOR's subset 56 + fn isOutsideDagCbor(v: Vector) bool { 57 + // floats (DAG-CBOR / DASL forbids all floats) 58 + if (v.hasFeature("float16")) return true; 59 + // bignums 60 + if (v.hasFeature("bignum") or v.hasFeature("!bignum")) return true; 61 + // simple values beyond false/true/null 62 + if (v.hasFeature("simple")) return true; 63 + // u64 overflow (values > i64 max that we store as unsigned) 64 + if (v.hasFeature("int64")) return true; 65 + // special float diagnostics 66 + if (std.mem.indexOf(u8, v.diagnostic, "NaN") != null) return true; 67 + if (std.mem.indexOf(u8, v.diagnostic, "Infinity") != null) return true; 68 + if (std.mem.eql(u8, v.diagnostic, "undefined")) return true; 69 + // float32/float64 prefix (DASL forbids all floats) 70 + if (v.hex.len >= 2 and std.mem.eql(u8, v.hex[0..2], "fa")) return true; 71 + if (v.hex.len >= 2 and std.mem.eql(u8, v.hex[0..2], "fb")) return true; 72 + // non-42 tags: c0, c1, d7xx, d8xx (but not d82a which is tag 42) 73 + if (v.hex.len >= 2) { 74 + if (std.mem.eql(u8, v.hex[0..2], "c0")) return true; 75 + if (std.mem.eql(u8, v.hex[0..2], "c1")) return true; 76 + if (v.hex.len >= 4 and std.mem.eql(u8, v.hex[0..2], "d7")) return true; 77 + if (v.hex.len >= 4 and std.mem.eql(u8, v.hex[0..2], "d8")) { 78 + // d82a = tag(42) which IS valid DAG-CBOR 79 + if (!std.mem.eql(u8, v.hex[0..4], "d82a")) return true; 80 + } 81 + if (v.hex.len >= 6 and std.mem.eql(u8, v.hex[0..4], "d820")) return true; 82 + } 83 + // map with integer keys 84 + if (std.mem.eql(u8, v.hex, "a201020304")) return true; 85 + return false; 86 + } 87 + 88 + test "RFC 8949: invalid vectors must be rejected" { 89 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 90 + defer arena.deinit(); 91 + const alloc = arena.allocator(); 92 + 93 + const parsed = try parseVectors(alloc); 94 + defer parsed.deinit(); 95 + 96 + var tested: usize = 0; 97 + for (parsed.value) |v| { 98 + if (!v.hasFlag("invalid")) continue; 99 + 100 + const data = hexToBytes(alloc, v.hex) catch continue; 101 + if (cbor.decodeAll(alloc, data)) |_| { 102 + std.debug.print("FAIL: invalid vector accepted: hex={s} diag={s}\n", .{ v.hex, v.diagnostic }); 103 + return error.TestExpectedError; 104 + } else |_| {} 105 + tested += 1; 106 + } 107 + // sanity check: we should have tested hundreds of invalid vectors 108 + try std.testing.expect(tested > 600); 109 + } 110 + 111 + test "RFC 8949: valid canonical vectors round-trip" { 112 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 113 + defer arena.deinit(); 114 + const alloc = arena.allocator(); 115 + 116 + const parsed = try parseVectors(alloc); 117 + defer parsed.deinit(); 118 + 119 + var tested: usize = 0; 120 + for (parsed.value) |v| { 121 + if (!v.hasFlag("valid")) continue; 122 + if (!v.hasFlag("canonical")) continue; 123 + if (isOutsideDagCbor(v)) continue; 124 + 125 + const data = hexToBytes(alloc, v.hex) catch continue; 126 + const decoded = cbor.decodeAll(alloc, data) catch |err| { 127 + std.debug.print("FAIL: valid vector rejected: hex={s} diag={s} err={s}\n", .{ v.hex, v.diagnostic, @errorName(err) }); 128 + return error.TestUnexpectedResult; 129 + }; 130 + 131 + // re-encode and verify byte-identical (canonical round-trip) 132 + const re_encoded = cbor.encodeAlloc(alloc, decoded) catch |err| { 133 + std.debug.print("FAIL: re-encode failed: hex={s} diag={s} err={s}\n", .{ v.hex, v.diagnostic, @errorName(err) }); 134 + return error.TestUnexpectedResult; 135 + }; 136 + 137 + if (!std.mem.eql(u8, data, re_encoded)) { 138 + std.debug.print("FAIL: round-trip mismatch: hex={s} diag={s}\n", .{ v.hex, v.diagnostic }); 139 + return error.TestExpectedEqual; 140 + } 141 + tested += 1; 142 + } 143 + // sanity check: we should have tested dozens of valid vectors 144 + try std.testing.expect(tested > 30); 145 + } 146 + 147 + test "RFC 8949: valid non-canonical vectors must be rejected by DAG-CBOR" { 148 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 149 + defer arena.deinit(); 150 + const alloc = arena.allocator(); 151 + 152 + const parsed = try parseVectors(alloc); 153 + defer parsed.deinit(); 154 + 155 + var tested: usize = 0; 156 + for (parsed.value) |v| { 157 + if (!v.hasFlag("valid")) continue; 158 + if (v.hasFlag("canonical")) continue; 159 + // skip features outside DAG-CBOR scope 160 + if (v.hasFeature("float16")) continue; 161 + if (v.hasFeature("bignum")) continue; 162 + if (v.hasFeature("simple")) continue; 163 + if (std.mem.indexOf(u8, v.diagnostic, "NaN") != null) continue; 164 + if (std.mem.indexOf(u8, v.diagnostic, "Infinity") != null) continue; 165 + 166 + const data = hexToBytes(alloc, v.hex) catch continue; 167 + const result = cbor.decodeAll(alloc, data); 168 + if (result) |_| { 169 + std.debug.print("FAIL: non-canonical vector accepted: hex={s} diag={s}\n", .{ v.hex, v.diagnostic }); 170 + return error.TestExpectedError; 171 + } else |_| {} 172 + tested += 1; 173 + } 174 + try std.testing.expect(tested > 10); 175 + }
+1285
src/internal/repo/cbor_test.zig
··· 1 + //! additional DAG-CBOR codec tests ported from atmos (Go implementation). 2 + //! 3 + //! focuses on spec compliance, edge cases, and error paths not covered 4 + //! by the inline tests in cbor.zig. 5 + 6 + const std = @import("std"); 7 + const cbor = @import("cbor.zig"); 8 + const Value = cbor.Value; 9 + const Cid = cbor.Cid; 10 + 11 + // === non-minimal encoding rejection === 12 + // 13 + // DAG-CBOR requires shortest-form encoding. values that fit in a smaller 14 + // representation must not be encoded with a larger one. 15 + 16 + test "reject non-minimal unsigned: 0 encoded as 1-byte" { 17 + // 0x18 0x00 = unsigned(0) with 1-byte additional, but 0 fits in additional field directly 18 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 19 + defer arena.deinit(); 20 + try std.testing.expectError(error.NonMinimalEncoding, cbor.decode(arena.allocator(), &.{ 0x18, 0x00 })); 21 + } 22 + 23 + test "reject non-minimal unsigned: 23 encoded as 1-byte" { 24 + // 0x18 0x17 = unsigned(23) with 1-byte additional, but 23 fits in additional field 25 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 26 + defer arena.deinit(); 27 + try std.testing.expectError(error.NonMinimalEncoding, cbor.decode(arena.allocator(), &.{ 0x18, 0x17 })); 28 + } 29 + 30 + test "reject non-minimal unsigned: 255 encoded as 2-byte" { 31 + // 0x19 0x00 0xff = unsigned(255) with 2-byte additional, but fits in 1-byte 32 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 33 + defer arena.deinit(); 34 + try std.testing.expectError(error.NonMinimalEncoding, cbor.decode(arena.allocator(), &.{ 0x19, 0x00, 0xff })); 35 + } 36 + 37 + test "reject non-minimal unsigned: 256 encoded as 4-byte" { 38 + // 0x1a 0x00 0x00 0x01 0x00 = unsigned(256) with 4-byte additional, but fits in 2-byte 39 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 40 + defer arena.deinit(); 41 + try std.testing.expectError(error.NonMinimalEncoding, cbor.decode(arena.allocator(), &.{ 0x1a, 0x00, 0x00, 0x01, 0x00 })); 42 + } 43 + 44 + test "reject non-minimal unsigned: 65535 encoded as 4-byte" { 45 + // 0x1a 0x00 0x00 0xff 0xff = unsigned(65535) with 4-byte, but fits in 2-byte 46 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 47 + defer arena.deinit(); 48 + try std.testing.expectError(error.NonMinimalEncoding, cbor.decode(arena.allocator(), &.{ 0x1a, 0x00, 0x00, 0xff, 0xff })); 49 + } 50 + 51 + test "reject non-minimal unsigned: 1 encoded as 8-byte" { 52 + // 0x1b 0x00..0x01 = unsigned(1) with 8-byte additional 53 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 54 + defer arena.deinit(); 55 + try std.testing.expectError(error.NonMinimalEncoding, cbor.decode(arena.allocator(), &.{ 0x1b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 })); 56 + } 57 + 58 + test "reject non-minimal negative: -1 encoded as 1-byte" { 59 + // 0x38 0x00 = negative(-1) with 1-byte additional, but -1 fits in additional field (0x20) 60 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 61 + defer arena.deinit(); 62 + try std.testing.expectError(error.NonMinimalEncoding, cbor.decode(arena.allocator(), &.{ 0x38, 0x00 })); 63 + } 64 + 65 + test "reject non-minimal negative: -24 encoded as 1-byte" { 66 + // 0x38 0x17 = negative(-24) with 1-byte additional, but fits in additional field (0x37) 67 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 68 + defer arena.deinit(); 69 + try std.testing.expectError(error.NonMinimalEncoding, cbor.decode(arena.allocator(), &.{ 0x38, 0x17 })); 70 + } 71 + 72 + test "reject non-minimal text string length: empty string as 1-byte length" { 73 + // 0x78 0x00 = text(0) with 1-byte length, but 0 fits in additional field (0x60) 74 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 75 + defer arena.deinit(); 76 + try std.testing.expectError(error.NonMinimalEncoding, cbor.decode(arena.allocator(), &.{ 0x78, 0x00 })); 77 + } 78 + 79 + test "reject non-minimal byte string length" { 80 + // 0x58 0x00 = bytes(0) with 1-byte length, but 0 fits in additional field (0x40) 81 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 82 + defer arena.deinit(); 83 + try std.testing.expectError(error.NonMinimalEncoding, cbor.decode(arena.allocator(), &.{ 0x58, 0x00 })); 84 + } 85 + 86 + test "reject non-minimal array length" { 87 + // 0x98 0x00 = array(0) with 1-byte length, but 0 fits in additional field (0x80) 88 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 89 + defer arena.deinit(); 90 + try std.testing.expectError(error.NonMinimalEncoding, cbor.decode(arena.allocator(), &.{ 0x98, 0x00 })); 91 + } 92 + 93 + test "reject non-minimal map length" { 94 + // 0xb8 0x00 = map(0) with 1-byte length, but 0 fits in additional field (0xa0) 95 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 96 + defer arena.deinit(); 97 + try std.testing.expectError(error.NonMinimalEncoding, cbor.decode(arena.allocator(), &.{ 0xb8, 0x00 })); 98 + } 99 + 100 + // === trailing bytes rejection === 101 + 102 + test "decodeAll rejects trailing bytes" { 103 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 104 + defer arena.deinit(); 105 + // integer 1 followed by extra byte 0x02 106 + try std.testing.expectError(error.TrailingBytes, cbor.decodeAll(arena.allocator(), &.{ 0x01, 0x02 })); 107 + } 108 + 109 + // === tag restriction === 110 + // 111 + // DAG-CBOR only allows tag 42 (CID links). all other tags must be rejected. 112 + 113 + test "reject tag 0 (date/time)" { 114 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 115 + defer arena.deinit(); 116 + // 0xc0 0x60 = tag(0) wrapping empty text string 117 + try std.testing.expectError(error.UnsupportedTag, cbor.decode(arena.allocator(), &.{ 0xc0, 0x60 })); 118 + } 119 + 120 + test "reject tag 1 (epoch time)" { 121 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 122 + defer arena.deinit(); 123 + // 0xc1 0x01 = tag(1) wrapping integer 1 124 + try std.testing.expectError(error.UnsupportedTag, cbor.decode(arena.allocator(), &.{ 0xc1, 0x01 })); 125 + } 126 + 127 + test "reject tag 2 (positive bignum)" { 128 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 129 + defer arena.deinit(); 130 + // 0xc2 0x41 0x01 = tag(2) wrapping byte string [0x01] 131 + try std.testing.expectError(error.UnsupportedTag, cbor.decode(arena.allocator(), &.{ 0xc2, 0x41, 0x01 })); 132 + } 133 + 134 + test "reject tag 3 (negative bignum)" { 135 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 136 + defer arena.deinit(); 137 + try std.testing.expectError(error.UnsupportedTag, cbor.decode(arena.allocator(), &.{ 0xc3, 0x41, 0x01 })); 138 + } 139 + 140 + test "reject tag 55799 (self-describe CBOR)" { 141 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 142 + defer arena.deinit(); 143 + // 0xd9 0xd9 0xf7 0x01 = tag(55799) wrapping integer 1 144 + try std.testing.expectError(error.UnsupportedTag, cbor.decode(arena.allocator(), &.{ 0xd9, 0xd9, 0xf7, 0x01 })); 145 + } 146 + 147 + // === map key ordering validation === 148 + // 149 + // DAG-CBOR requires map keys sorted by byte length (shorter first), 150 + // then lexicographically. 151 + 152 + test "reject unsorted map keys: wrong lexicographic order" { 153 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 154 + defer arena.deinit(); 155 + // {"b": 1, "a": 2} — same length, wrong lex order 156 + try std.testing.expectError(error.UnsortedMapKeys, cbor.decode(arena.allocator(), &.{ 157 + 0xa2, // map(2) 158 + 0x61, 'b', 0x01, // "b": 1 159 + 0x61, 'a', 0x02, // "a": 2 (should come before "b") 160 + })); 161 + } 162 + 163 + test "reject unsorted map keys: longer key before shorter" { 164 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 165 + defer arena.deinit(); 166 + // {"bb": 1, "a": 2} — 2-char key before 1-char key 167 + try std.testing.expectError(error.UnsortedMapKeys, cbor.decode(arena.allocator(), &.{ 168 + 0xa2, // map(2) 169 + 0x62, 'b', 'b', 0x01, // "bb": 1 170 + 0x61, 'a', 0x02, // "a": 2 (shorter, should come first) 171 + })); 172 + } 173 + 174 + test "accept correctly sorted map keys" { 175 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 176 + defer arena.deinit(); 177 + // {"a": 1, "b": 2, "cc": 3} — correct order: short first, then lex 178 + const result = try cbor.decode(arena.allocator(), &.{ 179 + 0xa3, // map(3) 180 + 0x61, 'a', 0x01, // "a": 1 181 + 0x61, 'b', 0x02, // "b": 2 182 + 0x62, 'c', 'c', 0x03, // "cc": 3 183 + }); 184 + try std.testing.expectEqual(@as(u64, 1), result.value.get("a").?.unsigned); 185 + try std.testing.expectEqual(@as(u64, 2), result.value.get("b").?.unsigned); 186 + try std.testing.expectEqual(@as(u64, 3), result.value.get("cc").?.unsigned); 187 + } 188 + 189 + // === duplicate map key rejection === 190 + 191 + test "reject duplicate map keys" { 192 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 193 + defer arena.deinit(); 194 + // {"a": 1, "a": 2} — duplicate key "a" 195 + try std.testing.expectError(error.DuplicateMapKey, cbor.decode(arena.allocator(), &.{ 196 + 0xa2, // map(2) 197 + 0x61, 'a', 0x01, // "a": 1 198 + 0x61, 'a', 0x02, // "a": 2 (duplicate!) 199 + })); 200 + } 201 + 202 + // === float rejection (all variants) === 203 + 204 + test "reject float16" { 205 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 206 + defer arena.deinit(); 207 + try std.testing.expectError(error.UnsupportedFloat, cbor.decode(arena.allocator(), &.{ 0xf9, 0x00, 0x00 })); 208 + } 209 + 210 + test "reject float32" { 211 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 212 + defer arena.deinit(); 213 + // 0xfa 0x47 0xc3 0x50 0x00 = float32(100000.0) 214 + try std.testing.expectError(error.UnsupportedFloat, cbor.decode(arena.allocator(), &.{ 0xfa, 0x47, 0xc3, 0x50, 0x00 })); 215 + } 216 + 217 + test "reject float64" { 218 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 219 + defer arena.deinit(); 220 + // 0xfb + 8 bytes = float64(1.0) 221 + try std.testing.expectError(error.UnsupportedFloat, cbor.decode(arena.allocator(), &.{ 0xfb, 0x3f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 })); 222 + } 223 + 224 + // === simple values rejection === 225 + 226 + test "reject undefined (0xf7)" { 227 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 228 + defer arena.deinit(); 229 + try std.testing.expectError(error.UnsupportedSimpleValue, cbor.decode(arena.allocator(), &.{0xf7})); 230 + } 231 + 232 + test "reject simple value 0" { 233 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 234 + defer arena.deinit(); 235 + // 0xf8 0x00 = simple(0) — only false/true/null allowed 236 + try std.testing.expectError(error.UnsupportedSimpleValue, cbor.decode(arena.allocator(), &.{ 0xf8, 0x00 })); 237 + } 238 + 239 + test "reject simple value 32" { 240 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 241 + defer arena.deinit(); 242 + try std.testing.expectError(error.UnsupportedSimpleValue, cbor.decode(arena.allocator(), &.{ 0xf8, 0x20 })); 243 + } 244 + 245 + test "reject simple value 255" { 246 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 247 + defer arena.deinit(); 248 + try std.testing.expectError(error.UnsupportedSimpleValue, cbor.decode(arena.allocator(), &.{ 0xf8, 0xff })); 249 + } 250 + 251 + // === indefinite-length rejection (all types) === 252 + 253 + test "reject indefinite-length byte string (0x5f)" { 254 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 255 + defer arena.deinit(); 256 + try std.testing.expectError(error.IndefiniteLength, cbor.decode(arena.allocator(), &.{0x5f})); 257 + } 258 + 259 + test "reject indefinite-length text string (0x7f)" { 260 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 261 + defer arena.deinit(); 262 + try std.testing.expectError(error.IndefiniteLength, cbor.decode(arena.allocator(), &.{0x7f})); 263 + } 264 + 265 + test "reject indefinite-length array (0x9f)" { 266 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 267 + defer arena.deinit(); 268 + try std.testing.expectError(error.IndefiniteLength, cbor.decode(arena.allocator(), &.{0x9f})); 269 + } 270 + 271 + test "reject indefinite-length map (0xbf)" { 272 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 273 + defer arena.deinit(); 274 + try std.testing.expectError(error.IndefiniteLength, cbor.decode(arena.allocator(), &.{0xbf})); 275 + } 276 + 277 + test "reject break stop code (0xff)" { 278 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 279 + defer arena.deinit(); 280 + try std.testing.expectError(error.IndefiniteLength, cbor.decode(arena.allocator(), &.{0xff})); 281 + } 282 + 283 + // === reserved additional info (28, 29, 30) for all major types === 284 + 285 + test "reject reserved additional info for unsigned" { 286 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 287 + defer arena.deinit(); 288 + // additional 28 = 0x1c, 29 = 0x1d, 30 = 0x1e 289 + try std.testing.expectError(error.ReservedAdditionalInfo, cbor.decode(arena.allocator(), &.{0x1c})); 290 + try std.testing.expectError(error.ReservedAdditionalInfo, cbor.decode(arena.allocator(), &.{0x1d})); 291 + try std.testing.expectError(error.ReservedAdditionalInfo, cbor.decode(arena.allocator(), &.{0x1e})); 292 + } 293 + 294 + test "reject reserved additional info for negative" { 295 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 296 + defer arena.deinit(); 297 + try std.testing.expectError(error.ReservedAdditionalInfo, cbor.decode(arena.allocator(), &.{0x3c})); 298 + try std.testing.expectError(error.ReservedAdditionalInfo, cbor.decode(arena.allocator(), &.{0x3d})); 299 + try std.testing.expectError(error.ReservedAdditionalInfo, cbor.decode(arena.allocator(), &.{0x3e})); 300 + } 301 + 302 + test "reject reserved additional info for byte string" { 303 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 304 + defer arena.deinit(); 305 + try std.testing.expectError(error.ReservedAdditionalInfo, cbor.decode(arena.allocator(), &.{0x5c})); 306 + try std.testing.expectError(error.ReservedAdditionalInfo, cbor.decode(arena.allocator(), &.{0x5d})); 307 + try std.testing.expectError(error.ReservedAdditionalInfo, cbor.decode(arena.allocator(), &.{0x5e})); 308 + } 309 + 310 + test "reject reserved additional info for text string" { 311 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 312 + defer arena.deinit(); 313 + try std.testing.expectError(error.ReservedAdditionalInfo, cbor.decode(arena.allocator(), &.{0x7c})); 314 + try std.testing.expectError(error.ReservedAdditionalInfo, cbor.decode(arena.allocator(), &.{0x7d})); 315 + try std.testing.expectError(error.ReservedAdditionalInfo, cbor.decode(arena.allocator(), &.{0x7e})); 316 + } 317 + 318 + test "reject reserved additional info for array" { 319 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 320 + defer arena.deinit(); 321 + try std.testing.expectError(error.ReservedAdditionalInfo, cbor.decode(arena.allocator(), &.{0x9c})); 322 + try std.testing.expectError(error.ReservedAdditionalInfo, cbor.decode(arena.allocator(), &.{0x9d})); 323 + try std.testing.expectError(error.ReservedAdditionalInfo, cbor.decode(arena.allocator(), &.{0x9e})); 324 + } 325 + 326 + test "reject reserved additional info for map" { 327 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 328 + defer arena.deinit(); 329 + try std.testing.expectError(error.ReservedAdditionalInfo, cbor.decode(arena.allocator(), &.{0xbc})); 330 + try std.testing.expectError(error.ReservedAdditionalInfo, cbor.decode(arena.allocator(), &.{0xbd})); 331 + try std.testing.expectError(error.ReservedAdditionalInfo, cbor.decode(arena.allocator(), &.{0xbe})); 332 + } 333 + 334 + test "reject reserved additional info for tag" { 335 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 336 + defer arena.deinit(); 337 + try std.testing.expectError(error.ReservedAdditionalInfo, cbor.decode(arena.allocator(), &.{0xdc})); 338 + try std.testing.expectError(error.ReservedAdditionalInfo, cbor.decode(arena.allocator(), &.{0xdd})); 339 + try std.testing.expectError(error.ReservedAdditionalInfo, cbor.decode(arena.allocator(), &.{0xde})); 340 + } 341 + 342 + // === integer boundary encode/decode === 343 + 344 + test "integer boundary: 255 (max 1-byte)" { 345 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 346 + defer arena.deinit(); 347 + const alloc = arena.allocator(); 348 + 349 + // 0x18 0xff = unsigned(255) 350 + try std.testing.expectEqual(@as(u64, 255), (try cbor.decode(alloc, &.{ 0x18, 0xff })).value.unsigned); 351 + // round-trip 352 + const encoded = try cbor.encodeAlloc(alloc, .{ .unsigned = 255 }); 353 + try std.testing.expectEqualSlices(u8, &.{ 0x18, 0xff }, encoded); 354 + } 355 + 356 + test "integer boundary: 256 (min 2-byte)" { 357 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 358 + defer arena.deinit(); 359 + const alloc = arena.allocator(); 360 + 361 + // 0x19 0x01 0x00 = unsigned(256) 362 + try std.testing.expectEqual(@as(u64, 256), (try cbor.decode(alloc, &.{ 0x19, 0x01, 0x00 })).value.unsigned); 363 + const encoded = try cbor.encodeAlloc(alloc, .{ .unsigned = 256 }); 364 + try std.testing.expectEqualSlices(u8, &.{ 0x19, 0x01, 0x00 }, encoded); 365 + } 366 + 367 + test "integer boundary: 65535 (max 2-byte)" { 368 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 369 + defer arena.deinit(); 370 + const alloc = arena.allocator(); 371 + 372 + try std.testing.expectEqual(@as(u64, 65535), (try cbor.decode(alloc, &.{ 0x19, 0xff, 0xff })).value.unsigned); 373 + const encoded = try cbor.encodeAlloc(alloc, .{ .unsigned = 65535 }); 374 + try std.testing.expectEqualSlices(u8, &.{ 0x19, 0xff, 0xff }, encoded); 375 + } 376 + 377 + test "integer boundary: 65536 (min 4-byte)" { 378 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 379 + defer arena.deinit(); 380 + const alloc = arena.allocator(); 381 + 382 + try std.testing.expectEqual(@as(u64, 65536), (try cbor.decode(alloc, &.{ 0x1a, 0x00, 0x01, 0x00, 0x00 })).value.unsigned); 383 + const encoded = try cbor.encodeAlloc(alloc, .{ .unsigned = 65536 }); 384 + try std.testing.expectEqualSlices(u8, &.{ 0x1a, 0x00, 0x01, 0x00, 0x00 }, encoded); 385 + } 386 + 387 + test "integer boundary: 0xffffffff (max 4-byte)" { 388 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 389 + defer arena.deinit(); 390 + const alloc = arena.allocator(); 391 + 392 + try std.testing.expectEqual(@as(u64, 0xffffffff), (try cbor.decode(alloc, &.{ 0x1a, 0xff, 0xff, 0xff, 0xff })).value.unsigned); 393 + const encoded = try cbor.encodeAlloc(alloc, .{ .unsigned = 0xffffffff }); 394 + try std.testing.expectEqualSlices(u8, &.{ 0x1a, 0xff, 0xff, 0xff, 0xff }, encoded); 395 + } 396 + 397 + test "integer boundary: 0x100000000 (min 8-byte)" { 398 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 399 + defer arena.deinit(); 400 + const alloc = arena.allocator(); 401 + 402 + try std.testing.expectEqual(@as(u64, 0x100000000), (try cbor.decode(alloc, &.{ 0x1b, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00 })).value.unsigned); 403 + const encoded = try cbor.encodeAlloc(alloc, .{ .unsigned = 0x100000000 }); 404 + try std.testing.expectEqualSlices(u8, &.{ 0x1b, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00 }, encoded); 405 + } 406 + 407 + test "integer boundary: max u64" { 408 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 409 + defer arena.deinit(); 410 + const alloc = arena.allocator(); 411 + 412 + const max_u64: u64 = std.math.maxInt(u64); 413 + try std.testing.expectEqual(max_u64, (try cbor.decode(alloc, &.{ 0x1b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff })).value.unsigned); 414 + } 415 + 416 + // === negative integer boundary tests === 417 + 418 + test "negative boundary: -24 (max inline)" { 419 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 420 + defer arena.deinit(); 421 + const alloc = arena.allocator(); 422 + 423 + // -24 = major 1, additional 23 → 0x37 424 + try std.testing.expectEqual(@as(i64, -24), (try cbor.decode(alloc, &.{0x37})).value.negative); 425 + const encoded = try cbor.encodeAlloc(alloc, .{ .negative = -24 }); 426 + try std.testing.expectEqualSlices(u8, &.{0x37}, encoded); 427 + } 428 + 429 + test "negative boundary: -25 (min 1-byte additional)" { 430 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 431 + defer arena.deinit(); 432 + const alloc = arena.allocator(); 433 + 434 + // -25 = major 1, additional 24 → 0x38 0x18 435 + try std.testing.expectEqual(@as(i64, -25), (try cbor.decode(alloc, &.{ 0x38, 0x18 })).value.negative); 436 + const encoded = try cbor.encodeAlloc(alloc, .{ .negative = -25 }); 437 + try std.testing.expectEqualSlices(u8, &.{ 0x38, 0x18 }, encoded); 438 + } 439 + 440 + test "negative boundary: -256 (max 1-byte additional)" { 441 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 442 + defer arena.deinit(); 443 + const alloc = arena.allocator(); 444 + 445 + // -256 = -1 - 255 → 0x38 0xff 446 + try std.testing.expectEqual(@as(i64, -256), (try cbor.decode(alloc, &.{ 0x38, 0xff })).value.negative); 447 + const encoded = try cbor.encodeAlloc(alloc, .{ .negative = -256 }); 448 + try std.testing.expectEqualSlices(u8, &.{ 0x38, 0xff }, encoded); 449 + } 450 + 451 + test "negative boundary: -257 (min 2-byte additional)" { 452 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 453 + defer arena.deinit(); 454 + const alloc = arena.allocator(); 455 + 456 + // -257 = -1 - 256 → 0x39 0x01 0x00 457 + try std.testing.expectEqual(@as(i64, -257), (try cbor.decode(alloc, &.{ 0x39, 0x01, 0x00 })).value.negative); 458 + const encoded = try cbor.encodeAlloc(alloc, .{ .negative = -257 }); 459 + try std.testing.expectEqualSlices(u8, &.{ 0x39, 0x01, 0x00 }, encoded); 460 + } 461 + 462 + test "negative boundary: -65537 (min 4-byte additional)" { 463 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 464 + defer arena.deinit(); 465 + const alloc = arena.allocator(); 466 + 467 + // -65537 = -1 - 65536 → 0x3a 0x00 0x01 0x00 0x00 468 + try std.testing.expectEqual(@as(i64, -65537), (try cbor.decode(alloc, &.{ 0x3a, 0x00, 0x01, 0x00, 0x00 })).value.negative); 469 + const encoded = try cbor.encodeAlloc(alloc, .{ .negative = -65537 }); 470 + try std.testing.expectEqualSlices(u8, &.{ 0x3a, 0x00, 0x01, 0x00, 0x00 }, encoded); 471 + } 472 + 473 + test "negative boundary: min i64 (-2^63)" { 474 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 475 + defer arena.deinit(); 476 + const alloc = arena.allocator(); 477 + 478 + const min_i64: i64 = std.math.minInt(i64); 479 + // -2^63 = -1 - (2^63 - 1) → 0x3b 0x7f 0xff 0xff 0xff 0xff 0xff 0xff 0xff 480 + try std.testing.expectEqual(min_i64, (try cbor.decode(alloc, &.{ 0x3b, 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff })).value.negative); 481 + } 482 + 483 + // === negative integer overflow === 484 + 485 + test "reject negative integer overflow: -(2^63 + 1)" { 486 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 487 + defer arena.deinit(); 488 + // 0x3b 0x80 0x00 ... 0x00 = -1 - 2^63 = -(2^63 + 1), overflows i64 489 + try std.testing.expectError(error.Overflow, cbor.decode(arena.allocator(), &.{ 0x3b, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 })); 490 + } 491 + 492 + test "reject negative integer overflow: -(2^64)" { 493 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 494 + defer arena.deinit(); 495 + // 0x3b 0xff ... 0xff = -1 - (2^64 - 1) = -2^64, overflows i64 496 + try std.testing.expectError(error.Overflow, cbor.decode(arena.allocator(), &.{ 0x3b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff })); 497 + } 498 + 499 + // === truncated data handling === 500 + 501 + test "reject empty input" { 502 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 503 + defer arena.deinit(); 504 + try std.testing.expectError(error.UnexpectedEof, cbor.decode(arena.allocator(), &.{})); 505 + } 506 + 507 + test "reject truncated 1-byte unsigned header" { 508 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 509 + defer arena.deinit(); 510 + // 0x18 needs 1 more byte 511 + try std.testing.expectError(error.UnexpectedEof, cbor.decode(arena.allocator(), &.{0x18})); 512 + } 513 + 514 + test "reject truncated 2-byte unsigned header" { 515 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 516 + defer arena.deinit(); 517 + // 0x19 needs 2 more bytes 518 + try std.testing.expectError(error.UnexpectedEof, cbor.decode(arena.allocator(), &.{0x19})); 519 + try std.testing.expectError(error.UnexpectedEof, cbor.decode(arena.allocator(), &.{ 0x19, 0x01 })); 520 + } 521 + 522 + test "reject truncated 4-byte unsigned header" { 523 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 524 + defer arena.deinit(); 525 + try std.testing.expectError(error.UnexpectedEof, cbor.decode(arena.allocator(), &.{0x1a})); 526 + try std.testing.expectError(error.UnexpectedEof, cbor.decode(arena.allocator(), &.{ 0x1a, 0x00, 0x00 })); 527 + } 528 + 529 + test "reject truncated 8-byte unsigned header" { 530 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 531 + defer arena.deinit(); 532 + try std.testing.expectError(error.UnexpectedEof, cbor.decode(arena.allocator(), &.{0x1b})); 533 + try std.testing.expectError(error.UnexpectedEof, cbor.decode(arena.allocator(), &.{ 0x1b, 0x00, 0x00, 0x00, 0x00 })); 534 + } 535 + 536 + test "reject truncated text string payload" { 537 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 538 + defer arena.deinit(); 539 + // 0x65 = text(5) but only 3 bytes follow 540 + try std.testing.expectError(error.UnexpectedEof, cbor.decode(arena.allocator(), &.{ 0x65, 'h', 'e', 'l' })); 541 + } 542 + 543 + test "reject truncated byte string payload" { 544 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 545 + defer arena.deinit(); 546 + // 0x44 = bytes(4) but only 2 bytes follow 547 + try std.testing.expectError(error.UnexpectedEof, cbor.decode(arena.allocator(), &.{ 0x44, 0x01, 0x02 })); 548 + } 549 + 550 + test "reject truncated array elements" { 551 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 552 + defer arena.deinit(); 553 + // 0x83 = array(3) but only 2 elements 554 + try std.testing.expectError(error.UnexpectedEof, cbor.decode(arena.allocator(), &.{ 0x83, 0x01, 0x02 })); 555 + } 556 + 557 + test "reject truncated map entries" { 558 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 559 + defer arena.deinit(); 560 + // 0xa2 = map(2) but only 1 entry 561 + try std.testing.expectError(error.UnexpectedEof, cbor.decode(arena.allocator(), &.{ 562 + 0xa2, 563 + 0x61, 564 + 'a', 565 + 0x01, 566 + })); 567 + } 568 + 569 + // === string/bytes at encoding boundaries === 570 + 571 + test "text string at encoding boundaries" { 572 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 573 + defer arena.deinit(); 574 + const alloc = arena.allocator(); 575 + 576 + // 23-byte string (max inline length) 577 + const s23 = "12345678901234567890123"; 578 + const encoded23 = try cbor.encodeAlloc(alloc, .{ .text = s23 }); 579 + try std.testing.expectEqual(@as(u8, 0x77), encoded23[0]); // 0x60 + 23 580 + const decoded23 = try cbor.decodeAll(alloc, encoded23); 581 + try std.testing.expectEqualStrings(s23, decoded23.text); 582 + 583 + // 24-byte string (first to use 1-byte length) 584 + const s24 = "123456789012345678901234"; 585 + const encoded24 = try cbor.encodeAlloc(alloc, .{ .text = s24 }); 586 + try std.testing.expectEqual(@as(u8, 0x78), encoded24[0]); // text + 1-byte length 587 + try std.testing.expectEqual(@as(u8, 24), encoded24[1]); 588 + const decoded24 = try cbor.decodeAll(alloc, encoded24); 589 + try std.testing.expectEqualStrings(s24, decoded24.text); 590 + } 591 + 592 + test "byte string at encoding boundaries" { 593 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 594 + defer arena.deinit(); 595 + const alloc = arena.allocator(); 596 + 597 + // 23-byte bytes (max inline length) 598 + const b23 = &[_]u8{0xaa} ** 23; 599 + const encoded23 = try cbor.encodeAlloc(alloc, .{ .bytes = b23 }); 600 + try std.testing.expectEqual(@as(u8, 0x57), encoded23[0]); // 0x40 + 23 601 + const decoded23 = try cbor.decodeAll(alloc, encoded23); 602 + try std.testing.expectEqualSlices(u8, b23, decoded23.bytes); 603 + 604 + // 24-byte bytes (first to use 1-byte length) 605 + const b24 = &[_]u8{0xbb} ** 24; 606 + const encoded24 = try cbor.encodeAlloc(alloc, .{ .bytes = b24 }); 607 + try std.testing.expectEqual(@as(u8, 0x58), encoded24[0]); // bytes + 1-byte length 608 + try std.testing.expectEqual(@as(u8, 24), encoded24[1]); 609 + const decoded24 = try cbor.decodeAll(alloc, encoded24); 610 + try std.testing.expectEqualSlices(u8, b24, decoded24.bytes); 611 + } 612 + 613 + // === CID edge cases === 614 + 615 + test "reject tag 42 wrapping non-bytes" { 616 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 617 + defer arena.deinit(); 618 + // tag(42) + text string "hello" instead of byte string 619 + try std.testing.expectError(error.InvalidCid, cbor.decode(arena.allocator(), &.{ 620 + 0xd8, 0x2a, // tag(42) 621 + 0x65, 'h', 'e', 'l', 'l', 'o', // text "hello" (should be bytes) 622 + })); 623 + } 624 + 625 + test "reject tag 42 with empty bytes" { 626 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 627 + defer arena.deinit(); 628 + // tag(42) + empty byte string (missing 0x00 prefix) 629 + try std.testing.expectError(error.InvalidCid, cbor.decode(arena.allocator(), &.{ 630 + 0xd8, 0x2a, // tag(42) 631 + 0x40, // bytes(0) — empty, no 0x00 prefix 632 + })); 633 + } 634 + 635 + test "reject tag 42 with wrong prefix" { 636 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 637 + defer arena.deinit(); 638 + // tag(42) + byte string with 0x01 prefix instead of 0x00 639 + try std.testing.expectError(error.InvalidCid, cbor.decode(arena.allocator(), &.{ 640 + 0xd8, 0x2a, // tag(42) 641 + 0x42, 0x01, 0xaa, // bytes [0x01, 0xaa] — wrong prefix 642 + })); 643 + } 644 + 645 + // === complex nested round-trips === 646 + 647 + test "round-trip: mixed array with all types" { 648 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 649 + defer arena.deinit(); 650 + const alloc = arena.allocator(); 651 + 652 + // [42, -7, "hello", true, false, null, [1, 2], {"k": 0}] 653 + const original: Value = .{ .array = &.{ 654 + .{ .unsigned = 42 }, 655 + .{ .negative = -7 }, 656 + .{ .text = "hello" }, 657 + .{ .boolean = true }, 658 + .{ .boolean = false }, 659 + .null, 660 + .{ .array = &.{ .{ .unsigned = 1 }, .{ .unsigned = 2 } } }, 661 + .{ .map = &.{.{ .key = "k", .value = .{ .unsigned = 0 } }} }, 662 + } }; 663 + 664 + const encoded = try cbor.encodeAlloc(alloc, original); 665 + const decoded = try cbor.decodeAll(alloc, encoded); 666 + 667 + const arr = decoded.array; 668 + try std.testing.expectEqual(@as(usize, 8), arr.len); 669 + try std.testing.expectEqual(@as(u64, 42), arr[0].unsigned); 670 + try std.testing.expectEqual(@as(i64, -7), arr[1].negative); 671 + try std.testing.expectEqualStrings("hello", arr[2].text); 672 + try std.testing.expectEqual(true, arr[3].boolean); 673 + try std.testing.expectEqual(false, arr[4].boolean); 674 + try std.testing.expectEqual(Value.null, arr[5]); 675 + try std.testing.expectEqual(@as(usize, 2), arr[6].array.len); 676 + try std.testing.expectEqual(@as(u64, 0), arr[7].get("k").?.unsigned); 677 + } 678 + 679 + test "round-trip: deeply nested maps" { 680 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 681 + defer arena.deinit(); 682 + const alloc = arena.allocator(); 683 + 684 + // {"a": {"b": {"c": {"d": 42}}}} 685 + const original: Value = .{ .map = &.{ 686 + .{ .key = "a", .value = .{ .map = &.{ 687 + .{ .key = "b", .value = .{ .map = &.{ 688 + .{ .key = "c", .value = .{ .map = &.{ 689 + .{ .key = "d", .value = .{ .unsigned = 42 } }, 690 + } } }, 691 + } } }, 692 + } } }, 693 + } }; 694 + 695 + const encoded = try cbor.encodeAlloc(alloc, original); 696 + const decoded = try cbor.decodeAll(alloc, encoded); 697 + 698 + const d = decoded.get("a").?.get("b").?.get("c").?.get("d").?.unsigned; 699 + try std.testing.expectEqual(@as(u64, 42), d); 700 + } 701 + 702 + test "round-trip: unicode text strings" { 703 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 704 + defer arena.deinit(); 705 + const alloc = arena.allocator(); 706 + 707 + const cases = [_][]const u8{ 708 + "", 709 + "a", 710 + "IETF", 711 + "\"\\\u{00fc}\u{6c34}", 712 + "\xc3\xbc", // ü 713 + "\xe6\xb0\xb4", // 水 714 + "\xf0\x9f\x98\x80", // 😀 715 + "\xf0\x9f\x91\xa8\xe2\x80\x8d\xf0\x9f\x91\xa9\xe2\x80\x8d\xf0\x9f\x91\xa7\xe2\x80\x8d\xf0\x9f\x91\xa7", // family ZWJ emoji 716 + }; 717 + 718 + for (cases) |text| { 719 + const encoded = try cbor.encodeAlloc(alloc, .{ .text = text }); 720 + const decoded = try cbor.decodeAll(alloc, encoded); 721 + try std.testing.expectEqualStrings(text, decoded.text); 722 + } 723 + } 724 + 725 + test "round-trip: empty containers" { 726 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 727 + defer arena.deinit(); 728 + const alloc = arena.allocator(); 729 + 730 + // empty array 731 + const empty_arr = try cbor.encodeAlloc(alloc, .{ .array = &.{} }); 732 + try std.testing.expectEqualSlices(u8, &.{0x80}, empty_arr); 733 + const decoded_arr = try cbor.decodeAll(alloc, empty_arr); 734 + try std.testing.expectEqual(@as(usize, 0), decoded_arr.array.len); 735 + 736 + // empty map 737 + const empty_map = try cbor.encodeAlloc(alloc, .{ .map = &.{} }); 738 + try std.testing.expectEqualSlices(u8, &.{0xa0}, empty_map); 739 + const decoded_map = try cbor.decodeAll(alloc, empty_map); 740 + try std.testing.expectEqual(@as(usize, 0), decoded_map.map.len); 741 + } 742 + 743 + test "round-trip: map with empty key" { 744 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 745 + defer arena.deinit(); 746 + const alloc = arena.allocator(); 747 + 748 + const original: Value = .{ .map = &.{ 749 + .{ .key = "", .value = .{ .unsigned = 1 } }, 750 + .{ .key = "a", .value = .{ .unsigned = 2 } }, 751 + } }; 752 + 753 + const encoded = try cbor.encodeAlloc(alloc, original); 754 + const decoded = try cbor.decodeAll(alloc, encoded); 755 + 756 + // empty key should sort first (shorter) 757 + try std.testing.expectEqualStrings("", decoded.map[0].key); 758 + try std.testing.expectEqualStrings("a", decoded.map[1].key); 759 + try std.testing.expectEqual(@as(u64, 1), decoded.get("").?.unsigned); 760 + try std.testing.expectEqual(@as(u64, 2), decoded.get("a").?.unsigned); 761 + } 762 + 763 + // === byte-identical re-encoding === 764 + 765 + test "canonical re-encoding: encode then decode then re-encode is identical" { 766 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 767 + defer arena.deinit(); 768 + const alloc = arena.allocator(); 769 + 770 + // build a non-trivially-ordered map 771 + const original: Value = .{ .map = &.{ 772 + .{ .key = "z", .value = .{ .unsigned = 26 } }, 773 + .{ .key = "a", .value = .{ .text = "first" } }, 774 + .{ .key = "mm", .value = .{ .array = &.{ 775 + .{ .boolean = true }, 776 + .null, 777 + .{ .negative = -100 }, 778 + } } }, 779 + } }; 780 + 781 + const first_encode = try cbor.encodeAlloc(alloc, original); 782 + const decoded = try cbor.decodeAll(alloc, first_encode); 783 + const second_encode = try cbor.encodeAlloc(alloc, decoded); 784 + 785 + try std.testing.expectEqualSlices(u8, first_encode, second_encode); 786 + } 787 + 788 + test "deterministic encoding: 10 iterations produce identical bytes" { 789 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 790 + defer arena.deinit(); 791 + const alloc = arena.allocator(); 792 + 793 + const value: Value = .{ .map = &.{ 794 + .{ .key = "type", .value = .{ .text = "app.bsky.feed.post" } }, 795 + .{ .key = "text", .value = .{ .text = "Hello, world!" } }, 796 + .{ .key = "createdAt", .value = .{ .text = "2024-01-01T00:00:00Z" } }, 797 + .{ .key = "langs", .value = .{ .array = &.{.{ .text = "en" }} } }, 798 + } }; 799 + 800 + const first = try cbor.encodeAlloc(alloc, value); 801 + for (0..10) |_| { 802 + const again = try cbor.encodeAlloc(alloc, value); 803 + try std.testing.expectEqualSlices(u8, first, again); 804 + } 805 + } 806 + 807 + // === single-byte exhaustive scan === 808 + // 809 + // every possible single-byte CBOR input should either decode or return 810 + // a well-defined error — never panic or trigger undefined behavior. 811 + 812 + test "single-byte exhaustive: no panics on any byte value" { 813 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 814 + defer arena.deinit(); 815 + const alloc = arena.allocator(); 816 + 817 + var byte: u16 = 0; 818 + while (byte <= 255) : (byte += 1) { 819 + const data = [_]u8{@intCast(byte)}; 820 + _ = cbor.decode(alloc, &data) catch continue; 821 + } 822 + } 823 + 824 + // === non-string map key rejection === 825 + 826 + test "reject integer map key" { 827 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 828 + defer arena.deinit(); 829 + // map(1) with integer key: {1: 2} 830 + try std.testing.expectError(error.InvalidMapKey, cbor.decode(arena.allocator(), &.{ 831 + 0xa1, // map(1) 832 + 0x01, // key: integer 1 (not text!) 833 + 0x02, // value: 2 834 + })); 835 + } 836 + 837 + test "reject bytes map key" { 838 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 839 + defer arena.deinit(); 840 + // map(1) with byte string key 841 + try std.testing.expectError(error.InvalidMapKey, cbor.decode(arena.allocator(), &.{ 842 + 0xa1, // map(1) 843 + 0x41, 0x01, // key: bytes [0x01] (not text!) 844 + 0x02, // value: 2 845 + })); 846 + } 847 + 848 + test "reject array map key" { 849 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 850 + defer arena.deinit(); 851 + try std.testing.expectError(error.InvalidMapKey, cbor.decode(arena.allocator(), &.{ 852 + 0xa1, // map(1) 853 + 0x80, // key: empty array (not text!) 854 + 0x02, // value: 2 855 + })); 856 + } 857 + 858 + test "reject boolean map key" { 859 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 860 + defer arena.deinit(); 861 + try std.testing.expectError(error.InvalidMapKey, cbor.decode(arena.allocator(), &.{ 862 + 0xa1, // map(1) 863 + 0xf5, // key: true (not text!) 864 + 0x02, // value: 2 865 + })); 866 + } 867 + 868 + // === map key ordering: comprehensive DAG-CBOR rules === 869 + 870 + test "map key ordering: length takes priority over lexicographic" { 871 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 872 + defer arena.deinit(); 873 + const alloc = arena.allocator(); 874 + 875 + // "z" (1 byte) must come before "aa" (2 bytes), even though "aa" < "z" lexicographically 876 + const original: Value = .{ .map = &.{ 877 + .{ .key = "aa", .value = .{ .unsigned = 2 } }, 878 + .{ .key = "z", .value = .{ .unsigned = 1 } }, 879 + } }; 880 + 881 + const encoded = try cbor.encodeAlloc(alloc, original); 882 + const decoded = try cbor.decodeAll(alloc, encoded); 883 + 884 + try std.testing.expectEqualStrings("z", decoded.map[0].key); 885 + try std.testing.expectEqualStrings("aa", decoded.map[1].key); 886 + } 887 + 888 + test "map key ordering: empty key sorts first" { 889 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 890 + defer arena.deinit(); 891 + const alloc = arena.allocator(); 892 + 893 + const original: Value = .{ .map = &.{ 894 + .{ .key = "a", .value = .{ .unsigned = 2 } }, 895 + .{ .key = "", .value = .{ .unsigned = 1 } }, 896 + .{ .key = "bb", .value = .{ .unsigned = 3 } }, 897 + } }; 898 + 899 + const encoded = try cbor.encodeAlloc(alloc, original); 900 + const decoded = try cbor.decodeAll(alloc, encoded); 901 + 902 + try std.testing.expectEqualStrings("", decoded.map[0].key); 903 + try std.testing.expectEqualStrings("a", decoded.map[1].key); 904 + try std.testing.expectEqualStrings("bb", decoded.map[2].key); 905 + } 906 + 907 + // === UTF-8 validation === 908 + // 909 + // CBOR text strings (major type 3) must contain valid UTF-8. 910 + // DAG-CBOR inherits this requirement. 911 + 912 + test "reject invalid UTF-8 in text string: 0xff byte" { 913 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 914 + defer arena.deinit(); 915 + // text(1) containing 0xff — not valid UTF-8 916 + try std.testing.expectError(error.InvalidUtf8, cbor.decode(arena.allocator(), &.{ 0x61, 0xff })); 917 + } 918 + 919 + test "reject invalid UTF-8 in text string: truncated multi-byte sequence" { 920 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 921 + defer arena.deinit(); 922 + // text(1) containing 0xc3 — start of 2-byte sequence but missing continuation 923 + try std.testing.expectError(error.InvalidUtf8, cbor.decode(arena.allocator(), &.{ 0x61, 0xc3 })); 924 + } 925 + 926 + test "reject invalid UTF-8 in text string: lone continuation byte" { 927 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 928 + defer arena.deinit(); 929 + // text(1) containing 0x80 — continuation byte without start 930 + try std.testing.expectError(error.InvalidUtf8, cbor.decode(arena.allocator(), &.{ 0x61, 0x80 })); 931 + } 932 + 933 + test "reject invalid UTF-8 in text string: surrogate half (U+D800)" { 934 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 935 + defer arena.deinit(); 936 + // text(3) containing ED A0 80 — UTF-8 encoding of U+D800 (surrogate) 937 + try std.testing.expectError(error.InvalidUtf8, cbor.decode(arena.allocator(), &.{ 0x63, 0xed, 0xa0, 0x80 })); 938 + } 939 + 940 + test "reject invalid UTF-8 in text string: overlong encoding" { 941 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 942 + defer arena.deinit(); 943 + // text(2) containing C0 80 — overlong encoding of U+0000 944 + try std.testing.expectError(error.InvalidUtf8, cbor.decode(arena.allocator(), &.{ 0x62, 0xc0, 0x80 })); 945 + } 946 + 947 + test "reject invalid UTF-8 in map key" { 948 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 949 + defer arena.deinit(); 950 + // map(1) with key containing invalid UTF-8 951 + try std.testing.expectError(error.InvalidUtf8, cbor.decode(arena.allocator(), &.{ 952 + 0xa1, // map(1) 953 + 0x61, 0xff, // key: text(1) with 0xff — invalid UTF-8 954 + 0x01, // value: 1 955 + })); 956 + } 957 + 958 + test "accept valid UTF-8 text: café" { 959 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 960 + defer arena.deinit(); 961 + // "café" = 63 61 66 c3 a9 — valid UTF-8 962 + const result = try cbor.decode(arena.allocator(), &.{ 0x65, 'c', 'a', 'f', 0xc3, 0xa9 }); 963 + try std.testing.expectEqualStrings("caf\xc3\xa9", result.value.text); 964 + } 965 + 966 + test "accept valid UTF-8 text: CJK and emoji" { 967 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 968 + defer arena.deinit(); 969 + const alloc = arena.allocator(); 970 + 971 + // encode and decode a string with multi-byte characters 972 + const text = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e\xf0\x9f\x98\x80"; // 日本語😀 973 + const encoded = try cbor.encodeAlloc(alloc, .{ .text = text }); 974 + const decoded = try cbor.decodeAll(alloc, encoded); 975 + try std.testing.expectEqualStrings(text, decoded.text); 976 + } 977 + 978 + // === nesting depth limit === 979 + 980 + test "accept nesting at max_depth - 1" { 981 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 982 + defer arena.deinit(); 983 + 984 + // build array(1) nested max_depth - 1 times, with 0 at the bottom 985 + var buf: [cbor.max_depth + 1]u8 = undefined; 986 + for (0..cbor.max_depth - 1) |i| { 987 + buf[i] = 0x81; // array(1) 988 + } 989 + buf[cbor.max_depth - 1] = 0x00; // integer 0 at the bottom 990 + 991 + const result = try cbor.decodeAll(arena.allocator(), buf[0..cbor.max_depth]); 992 + // verify outermost is an array 993 + try std.testing.expectEqual(@as(usize, 1), result.array.len); 994 + } 995 + 996 + test "reject nesting beyond max_depth" { 997 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 998 + defer arena.deinit(); 999 + 1000 + // build array(1) nested max_depth + 1 times 1001 + var buf: [cbor.max_depth + 2]u8 = undefined; 1002 + for (0..cbor.max_depth + 1) |i| { 1003 + buf[i] = 0x81; // array(1) 1004 + } 1005 + buf[cbor.max_depth + 1] = 0x00; 1006 + 1007 + try std.testing.expectError(error.MaxDepthExceeded, cbor.decodeAll(arena.allocator(), buf[0 .. cbor.max_depth + 2])); 1008 + } 1009 + 1010 + // === huge allocation rejection === 1011 + // 1012 + // the decoder should reject claims for impossibly large collections 1013 + // without attempting to allocate. 1014 + 1015 + test "reject huge array allocation claim" { 1016 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 1017 + defer arena.deinit(); 1018 + // 0x9b + 8 bytes claiming 2^32 elements, but only a few bytes of data follow 1019 + try std.testing.expectError(error.UnexpectedEof, cbor.decode(arena.allocator(), &.{ 1020 + 0x9b, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, // array(2^32) 1021 + 0x00, // just one byte of data 1022 + })); 1023 + } 1024 + 1025 + test "reject huge map allocation claim" { 1026 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 1027 + defer arena.deinit(); 1028 + // map claiming 2^32 entries with minimal data 1029 + try std.testing.expectError(error.UnexpectedEof, cbor.decode(arena.allocator(), &.{ 1030 + 0xbb, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, // map(2^32) 1031 + 0x61, 'a', 0x01, // one entry 1032 + })); 1033 + } 1034 + 1035 + test "reject huge byte string claim" { 1036 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 1037 + defer arena.deinit(); 1038 + // bytes claiming 2^32 length with minimal data 1039 + try std.testing.expectError(error.UnexpectedEof, cbor.decode(arena.allocator(), &.{ 1040 + 0x5b, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, // bytes(2^32) 1041 + 0x00, // just one byte 1042 + })); 1043 + } 1044 + 1045 + test "reject huge text string claim" { 1046 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 1047 + defer arena.deinit(); 1048 + // text claiming 2^32 length with minimal data 1049 + try std.testing.expectError(error.UnexpectedEof, cbor.decode(arena.allocator(), &.{ 1050 + 0x7b, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, // text(2^32) 1051 + 0x00, // just one byte 1052 + })); 1053 + } 1054 + 1055 + // === Cid method edge cases === 1056 + 1057 + test "Cid.version returns null for empty raw" { 1058 + const cid = Cid{ .raw = &.{} }; 1059 + try std.testing.expect(cid.version() == null); 1060 + try std.testing.expect(cid.codec() == null); 1061 + try std.testing.expect(cid.hashFn() == null); 1062 + try std.testing.expect(cid.digest() == null); 1063 + } 1064 + 1065 + test "Cid.version returns null for single byte" { 1066 + const cid = Cid{ .raw = &.{0x01} }; 1067 + try std.testing.expect(cid.version() == null); 1068 + } 1069 + 1070 + test "Cid.digest returns null for truncated CIDv0" { 1071 + // CIDv0 starts with 0x12 0x20 but needs 34 bytes total 1072 + const cid = Cid{ .raw = &.{ 0x12, 0x20, 0xaa, 0xbb } }; // only 4 bytes, need 34 1073 + try std.testing.expect(cid.version().? == 0); // version parses ok 1074 + try std.testing.expect(cid.digest() == null); // but digest is truncated 1075 + } 1076 + 1077 + test "Cid.digest returns correct slice for valid CIDv1" { 1078 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 1079 + defer arena.deinit(); 1080 + 1081 + const cid = try Cid.forDagCbor(arena.allocator(), "test"); 1082 + const d = cid.digest().?; 1083 + try std.testing.expectEqual(@as(usize, 32), d.len); 1084 + // verify it's actually SHA-256 of "test" 1085 + const Sha256 = std.crypto.hash.sha2.Sha256; 1086 + var expected: [32]u8 = undefined; 1087 + Sha256.hash("test", &expected, .{}); 1088 + try std.testing.expectEqualSlices(u8, &expected, d); 1089 + } 1090 + 1091 + // === readUvarint edge cases === 1092 + 1093 + test "readUvarint returns null on empty input" { 1094 + var pos: usize = 0; 1095 + try std.testing.expect(cbor.readUvarint(&.{}, &pos) == null); 1096 + } 1097 + 1098 + test "readUvarint returns null on truncated continuation" { 1099 + // 0x80 = continuation bit set, needs more bytes 1100 + var pos: usize = 0; 1101 + try std.testing.expect(cbor.readUvarint(&.{0x80}, &pos) == null); 1102 + } 1103 + 1104 + test "readUvarint decodes max single byte (127)" { 1105 + var pos: usize = 0; 1106 + try std.testing.expectEqual(@as(u64, 127), cbor.readUvarint(&.{0x7f}, &pos).?); 1107 + try std.testing.expectEqual(@as(usize, 1), pos); 1108 + } 1109 + 1110 + test "readUvarint decodes multi-byte value" { 1111 + // 128 = 0x80 0x01 1112 + var pos: usize = 0; 1113 + try std.testing.expectEqual(@as(u64, 128), cbor.readUvarint(&.{ 0x80, 0x01 }, &pos).?); 1114 + try std.testing.expectEqual(@as(usize, 2), pos); 1115 + } 1116 + 1117 + // === Value getter edge cases === 1118 + 1119 + test "getUint returns null for negative value" { 1120 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 1121 + defer arena.deinit(); 1122 + 1123 + const val: Value = .{ .map = &.{ 1124 + .{ .key = "n", .value = .{ .negative = -5 } }, 1125 + } }; 1126 + // -5 can't be represented as u64 1127 + try std.testing.expect(val.getUint("n") == null); 1128 + } 1129 + 1130 + test "getInt returns null for u64 > max i64" { 1131 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 1132 + defer arena.deinit(); 1133 + 1134 + const val: Value = .{ .map = &.{ 1135 + .{ .key = "n", .value = .{ .unsigned = std.math.maxInt(u64) } }, 1136 + } }; 1137 + // max u64 can't be represented as i64 1138 + try std.testing.expect(val.getInt("n") == null); 1139 + } 1140 + 1141 + test "getCid returns CID for cid value" { 1142 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 1143 + defer arena.deinit(); 1144 + const alloc = arena.allocator(); 1145 + 1146 + const cid = try Cid.forDagCbor(alloc, "data"); 1147 + const val: Value = .{ .map = &.{ 1148 + .{ .key = "link", .value = .{ .cid = cid } }, 1149 + } }; 1150 + 1151 + const got = val.getCid("link").?; 1152 + try std.testing.expectEqualSlices(u8, cid.raw, got.raw); 1153 + } 1154 + 1155 + test "getCid returns null for non-cid value" { 1156 + const val: Value = .{ .map = &.{ 1157 + .{ .key = "x", .value = .{ .unsigned = 42 } }, 1158 + } }; 1159 + try std.testing.expect(val.getCid("x") == null); 1160 + } 1161 + 1162 + test "get returns null for non-map value" { 1163 + const val: Value = .{ .unsigned = 42 }; 1164 + try std.testing.expect(val.get("anything") == null); 1165 + try std.testing.expect(val.getString("anything") == null); 1166 + try std.testing.expect(val.getInt("anything") == null); 1167 + } 1168 + 1169 + // === negative integer encode round-trip at min i64 === 1170 + 1171 + // === readUvarint 10th byte overflow rejection === 1172 + 1173 + test "readUvarint rejects 10th byte with value > 1" { 1174 + // 9 continuation bytes (0x80) + 10th byte with value 2 (bit 1 set, would overflow u64) 1175 + const data = [_]u8{0x80} ** 9 ++ [_]u8{0x02}; 1176 + var pos: usize = 0; 1177 + try std.testing.expect(cbor.readUvarint(&data, &pos) == null); 1178 + } 1179 + 1180 + test "readUvarint accepts 10th byte with value 1 (max u64)" { 1181 + // 9 continuation bytes (0xff = 0x7f data + continuation) + 10th byte 0x01 1182 + // This encodes 2^63 + (lower 63 bits all set) = max u64 1183 + const data = [_]u8{0xff} ** 9 ++ [_]u8{0x01}; 1184 + var pos: usize = 0; 1185 + const val = cbor.readUvarint(&data, &pos); 1186 + try std.testing.expect(val != null); 1187 + try std.testing.expectEqual(std.math.maxInt(u64), val.?); 1188 + } 1189 + 1190 + // === CID minimum size === 1191 + 1192 + test "reject tag 42 with only 0x00 prefix (empty CID)" { 1193 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 1194 + defer arena.deinit(); 1195 + // tag(42) + bytes([0x00]) — prefix present but no actual CID bytes 1196 + try std.testing.expectError(error.InvalidCid, cbor.decode(arena.allocator(), &.{ 1197 + 0xd8, 0x2a, // tag(42) 1198 + 0x41, 0x00, // bytes(1) with just the 0x00 prefix 1199 + })); 1200 + } 1201 + 1202 + test "reject tag 42 with only prefix + 1 byte (too short for CID)" { 1203 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 1204 + defer arena.deinit(); 1205 + // tag(42) + bytes([0x00, 0x01]) — only 1 CID byte, need at least version + codec 1206 + try std.testing.expectError(error.InvalidCid, cbor.decode(arena.allocator(), &.{ 1207 + 0xd8, 0x2a, // tag(42) 1208 + 0x42, 0x00, 0x01, // bytes(2) — prefix + 1 byte 1209 + })); 1210 + } 1211 + 1212 + // === allocation failure safety (checkAllAllocationFailures) === 1213 + 1214 + fn decodeSimpleImpl(backing: std.mem.Allocator, data: []const u8) !void { 1215 + // use an arena over the backing allocator — checkAllAllocationFailures 1216 + // tracks the backing allocator's alloc/free calls. the arena batches 1217 + // frees on deinit, so OOM from any arena allocation correctly frees 1218 + // everything allocated so far. 1219 + var arena = std.heap.ArenaAllocator.init(backing); 1220 + defer arena.deinit(); 1221 + _ = try cbor.decodeAll(arena.allocator(), data); 1222 + } 1223 + 1224 + test "checkAllAllocationFailures: decode flat map" { 1225 + var setup_arena = std.heap.ArenaAllocator.init(std.testing.allocator); 1226 + defer setup_arena.deinit(); 1227 + const encoded = try cbor.encodeAlloc(setup_arena.allocator(), .{ .map = &.{ 1228 + .{ .key = "a", .value = .{ .unsigned = 1 } }, 1229 + .{ .key = "b", .value = .{ .text = "hello" } }, 1230 + } }); 1231 + 1232 + try std.testing.checkAllAllocationFailures(std.testing.allocator, decodeSimpleImpl, .{encoded}); 1233 + } 1234 + 1235 + fn decodeNestedImpl(backing: std.mem.Allocator, data: []const u8) !void { 1236 + var arena = std.heap.ArenaAllocator.init(backing); 1237 + defer arena.deinit(); 1238 + _ = try cbor.decodeAll(arena.allocator(), data); 1239 + } 1240 + 1241 + test "checkAllAllocationFailures: decode nested record" { 1242 + var setup_arena = std.heap.ArenaAllocator.init(std.testing.allocator); 1243 + defer setup_arena.deinit(); 1244 + const sa = setup_arena.allocator(); 1245 + 1246 + const record: Value = .{ .map = &.{ 1247 + .{ .key = "$type", .value = .{ .text = "app.bsky.feed.post" } }, 1248 + .{ .key = "langs", .value = .{ .array = &.{.{ .text = "en" }} } }, 1249 + .{ .key = "text", .value = .{ .text = "hello" } }, 1250 + } }; 1251 + const encoded = try cbor.encodeAlloc(sa, record); 1252 + 1253 + try std.testing.checkAllAllocationFailures(std.testing.allocator, decodeNestedImpl, .{encoded}); 1254 + } 1255 + 1256 + fn decodeArrayImpl(backing: std.mem.Allocator, data: []const u8) !void { 1257 + var arena = std.heap.ArenaAllocator.init(backing); 1258 + defer arena.deinit(); 1259 + _ = try cbor.decodeAll(arena.allocator(), data); 1260 + } 1261 + 1262 + test "checkAllAllocationFailures: decode array" { 1263 + var setup_arena = std.heap.ArenaAllocator.init(std.testing.allocator); 1264 + defer setup_arena.deinit(); 1265 + const encoded = try cbor.encodeAlloc(setup_arena.allocator(), .{ .array = &.{ 1266 + .{ .unsigned = 1 }, 1267 + .{ .unsigned = 2 }, 1268 + .{ .text = "three" }, 1269 + } }); 1270 + 1271 + try std.testing.checkAllAllocationFailures(std.testing.allocator, decodeArrayImpl, .{encoded}); 1272 + } 1273 + 1274 + // === negative integer encode round-trip at min i64 === 1275 + 1276 + test "round-trip encode min i64" { 1277 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 1278 + defer arena.deinit(); 1279 + const alloc = arena.allocator(); 1280 + 1281 + const min_i64: i64 = std.math.minInt(i64); 1282 + const encoded = try cbor.encodeAlloc(alloc, .{ .negative = min_i64 }); 1283 + const decoded = try cbor.decodeAll(alloc, encoded); 1284 + try std.testing.expectEqual(min_i64, decoded.negative); 1285 + }
+212
src/internal/repo/cbor_write_test.zig
··· 1 + const std = @import("std"); 2 + const cbor = @import("cbor.zig"); 3 + 4 + const writeArg = cbor.writeArg; 5 + const writeText = cbor.writeText; 6 + const writeBytes = cbor.writeBytes; 7 + const writeUint = cbor.writeUint; 8 + const writeInt = cbor.writeInt; 9 + const writeMapHeader = cbor.writeMapHeader; 10 + const writeArrayHeader = cbor.writeArrayHeader; 11 + const writeBool = cbor.writeBool; 12 + const writeNull = cbor.writeNull; 13 + const writeCidLink = cbor.writeCidLink; 14 + 15 + const readArg = cbor.readArg; 16 + const readText = cbor.readText; 17 + const readBytes = cbor.readBytes; 18 + const readUint = cbor.readUint; 19 + const readInt = cbor.readInt; 20 + const readBool = cbor.readBool; 21 + const readNull = cbor.readNull; 22 + const readMapHeader = cbor.readMapHeader; 23 + const readArrayHeader = cbor.readArrayHeader; 24 + const readCidLink = cbor.readCidLink; 25 + 26 + const decodeAll = cbor.decodeAll; 27 + 28 + // =========================================================================== 29 + // writeArg 30 + // =========================================================================== 31 + 32 + test "writeArg: value 0 (1 byte)" { 33 + var buf: [16]u8 = undefined; 34 + const end = writeArg(&buf, 0, 0, 0); 35 + try std.testing.expectEqual(@as(usize, 1), end); 36 + try std.testing.expectEqual(@as(u8, 0x00), buf[0]); 37 + } 38 + 39 + test "writeArg: value 23 (1 byte)" { 40 + var buf: [16]u8 = undefined; 41 + const end = writeArg(&buf, 0, 0, 23); 42 + try std.testing.expectEqual(@as(usize, 1), end); 43 + try std.testing.expectEqual(@as(u8, 0x17), buf[0]); 44 + } 45 + 46 + test "writeArg: value 24 (2 bytes)" { 47 + var buf: [16]u8 = undefined; 48 + const end = writeArg(&buf, 0, 0, 24); 49 + try std.testing.expectEqual(@as(usize, 2), end); 50 + try std.testing.expectEqual(@as(u8, 0x18), buf[0]); 51 + try std.testing.expectEqual(@as(u8, 24), buf[1]); 52 + } 53 + 54 + test "writeArg: value 1000 (3 bytes)" { 55 + var buf: [16]u8 = undefined; 56 + const end = writeArg(&buf, 0, 0, 1000); 57 + try std.testing.expectEqual(@as(usize, 3), end); 58 + try std.testing.expectEqual(@as(u8, 0x19), buf[0]); 59 + try std.testing.expectEqual(@as(u8, 0x03), buf[1]); 60 + try std.testing.expectEqual(@as(u8, 0xe8), buf[2]); 61 + } 62 + 63 + // =========================================================================== 64 + // writeText round-trip 65 + // =========================================================================== 66 + 67 + test "writeText: 'hello' round-trip" { 68 + var buf: [64]u8 = undefined; 69 + const end = writeText(&buf, 0, "hello"); 70 + const result = try readText(&buf, 0); 71 + try std.testing.expectEqualStrings("hello", result.val); 72 + try std.testing.expectEqual(end, result.end); 73 + } 74 + 75 + // =========================================================================== 76 + // writeBytes round-trip 77 + // =========================================================================== 78 + 79 + test "writeBytes: [1,2,3] round-trip" { 80 + var buf: [64]u8 = undefined; 81 + const input = [_]u8{ 1, 2, 3 }; 82 + const end = writeBytes(&buf, 0, &input); 83 + const result = try readBytes(&buf, 0); 84 + try std.testing.expectEqual(@as(usize, 3), result.val.len); 85 + try std.testing.expectEqual(@as(u8, 1), result.val[0]); 86 + try std.testing.expectEqual(@as(u8, 2), result.val[1]); 87 + try std.testing.expectEqual(@as(u8, 3), result.val[2]); 88 + try std.testing.expectEqual(end, result.end); 89 + } 90 + 91 + // =========================================================================== 92 + // writeUint round-trip 93 + // =========================================================================== 94 + 95 + test "writeUint: 42 round-trip" { 96 + var buf: [16]u8 = undefined; 97 + const end = writeUint(&buf, 0, 42); 98 + const result = try readUint(&buf, 0); 99 + try std.testing.expectEqual(@as(u64, 42), result.val); 100 + try std.testing.expectEqual(end, result.end); 101 + } 102 + 103 + // =========================================================================== 104 + // writeInt 105 + // =========================================================================== 106 + 107 + test "writeInt: -10 verify bytes" { 108 + var buf: [16]u8 = undefined; 109 + const end = writeInt(&buf, 0, -10); 110 + try std.testing.expectEqual(@as(usize, 1), end); 111 + try std.testing.expectEqual(@as(u8, 0x29), buf[0]); 112 + } 113 + 114 + test "writeInt: positive 42 round-trip" { 115 + var buf: [16]u8 = undefined; 116 + const end = writeInt(&buf, 0, 42); 117 + const result = try readInt(&buf, 0); 118 + try std.testing.expectEqual(@as(i64, 42), result.val); 119 + try std.testing.expectEqual(end, result.end); 120 + } 121 + 122 + // =========================================================================== 123 + // writeMapHeader 124 + // =========================================================================== 125 + 126 + test "writeMapHeader: count 3 verify byte" { 127 + var buf: [16]u8 = undefined; 128 + const end = writeMapHeader(&buf, 0, 3); 129 + try std.testing.expectEqual(@as(usize, 1), end); 130 + try std.testing.expectEqual(@as(u8, 0xa3), buf[0]); 131 + } 132 + 133 + // =========================================================================== 134 + // writeArrayHeader 135 + // =========================================================================== 136 + 137 + test "writeArrayHeader: count 2 verify byte" { 138 + var buf: [16]u8 = undefined; 139 + const end = writeArrayHeader(&buf, 0, 2); 140 + try std.testing.expectEqual(@as(usize, 1), end); 141 + try std.testing.expectEqual(@as(u8, 0x82), buf[0]); 142 + } 143 + 144 + // =========================================================================== 145 + // writeBool 146 + // =========================================================================== 147 + 148 + test "writeBool: true and false consecutive" { 149 + var buf: [16]u8 = undefined; 150 + var p = writeBool(&buf, 0, true); 151 + p = writeBool(&buf, p, false); 152 + try std.testing.expectEqual(@as(u8, 0xf5), buf[0]); 153 + try std.testing.expectEqual(@as(u8, 0xf4), buf[1]); 154 + try std.testing.expectEqual(@as(usize, 2), p); 155 + 156 + const r1 = try readBool(&buf, 0); 157 + try std.testing.expectEqual(true, r1.val); 158 + const r2 = try readBool(&buf, r1.end); 159 + try std.testing.expectEqual(false, r2.val); 160 + } 161 + 162 + // =========================================================================== 163 + // writeNull 164 + // =========================================================================== 165 + 166 + test "writeNull: verify byte" { 167 + var buf: [16]u8 = undefined; 168 + const end = writeNull(&buf, 0); 169 + try std.testing.expectEqual(@as(usize, 1), end); 170 + try std.testing.expectEqual(@as(u8, 0xf6), buf[0]); 171 + // round-trip 172 + const read_end = try readNull(&buf, 0); 173 + try std.testing.expectEqual(end, read_end); 174 + } 175 + 176 + // =========================================================================== 177 + // writeCidLink round-trip 178 + // =========================================================================== 179 + 180 + test "writeCidLink: round-trip" { 181 + var buf: [128]u8 = undefined; 182 + const cid_raw = [_]u8{ 0x01, 0x71, 0x12, 0x20 } ++ [_]u8{0xaa} ** 32; 183 + const end = writeCidLink(&buf, 0, &cid_raw); 184 + const result = try readCidLink(&buf, 0); 185 + try std.testing.expectEqual(@as(usize, 36), result.val.len); 186 + try std.testing.expectEqualSlices(u8, &cid_raw, result.val); 187 + try std.testing.expectEqual(end, result.end); 188 + } 189 + 190 + // =========================================================================== 191 + // Full record: manually write {"text": "hello", "value": 42} then decodeAll 192 + // =========================================================================== 193 + 194 + test "full record: write map then decodeAll" { 195 + var buf: [128]u8 = undefined; 196 + // DAG-CBOR sorts keys by length then lex: "text" (4) < "value" (5) 197 + var p: usize = 0; 198 + p = writeMapHeader(&buf, p, 2); 199 + p = writeText(&buf, p, "text"); 200 + p = writeText(&buf, p, "hello"); 201 + p = writeText(&buf, p, "value"); 202 + p = writeUint(&buf, p, 42); 203 + 204 + const encoded = buf[0..p]; 205 + 206 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 207 + defer arena.deinit(); 208 + const decoded = try decodeAll(arena.allocator(), encoded); 209 + 210 + try std.testing.expectEqualStrings("hello", decoded.getString("text").?); 211 + try std.testing.expectEqual(@as(u64, 42), decoded.getUint("value").?); 212 + }
+73 -118
src/internal/repo/mst.zig
··· 172 172 fn findKey(maybe_node: ?*Node, layer: u32, key: []const u8, height: u32) ?cbor.Cid { 173 173 const node = maybe_node orelse return null; 174 174 175 - if (height == layer) { 175 + if (height >= layer) { 176 + // key belongs at this layer or above — scan entries 176 177 for (node.entries.items) |entry| { 177 178 const cmp = std.mem.order(u8, key, entry.key); 178 179 if (cmp == .eq) return entry.value; ··· 182 183 } 183 184 184 185 // height < layer: recurse into the subtree gap containing key 186 + if (layer == 0) return null; // can't go deeper 185 187 for (node.entries.items, 0..) |entry, i| { 186 188 if (std.mem.order(u8, key, entry.key) == .lt) { 187 189 const child = if (i == 0) node.left else node.entries.items[i - 1].right; ··· 234 236 fn deleteFromNode(self: *Mst, node: *Node, layer: u32, key: []const u8) !?cbor.Cid { 235 237 const height = keyHeight(key); 236 238 237 - if (height == layer) { 239 + if (height >= layer) { 238 240 // find and remove the entry 239 241 for (node.entries.items, 0..) |entry, i| { 240 242 if (std.mem.eql(u8, entry.key, key)) { ··· 259 261 } 260 262 261 263 // height < layer: recurse into the appropriate gap 264 + if (layer == 0) return null; // can't go deeper 262 265 if (node.entries.items.len == 0) { 263 266 switch (node.left) { 264 267 .node => |left| return try self.deleteFromNode(left, layer - 1, key), ··· 466 469 467 470 const root_node = try loadNodeFromData(allocator, repo_car, root_node_data); 468 471 469 - // root layer = key height of first entry 470 - var key_buf: [512]u8 = undefined; 471 - const first = root_node_data.entries[0]; 472 - @memcpy(key_buf[0..first.key_suffix.len], first.key_suffix); 473 - const root_layer = keyHeight(key_buf[0..first.key_suffix.len]); 472 + // root layer = key height of first entry (root entry has prefix_len=0, 473 + // so key_suffix IS the full key — no need to copy) 474 + const root_layer = keyHeight(root_node_data.entries[0].key_suffix); 474 475 475 476 return .{ 476 477 .allocator = allocator, ··· 493 494 var prev_key: []const u8 = ""; 494 495 for (data.entries) |entry_data| { 495 496 // reconstruct full key 497 + if (entry_data.prefix_len > prev_key.len) return error.InvalidMstNode; 496 498 const full_key = try allocator.alloc(u8, entry_data.prefix_len + entry_data.key_suffix.len); 497 499 if (entry_data.prefix_len > 0) { 498 500 @memcpy(full_key[0..entry_data.prefix_len], prev_key[0..entry_data.prefix_len]); ··· 841 843 }; 842 844 843 845 pub fn decodeMstNode(allocator: Allocator, data: []const u8) !MstNodeData { 844 - var r = MstReader{ .data = data, .pos = 0 }; 846 + var pos: usize = 0; 845 847 846 - const map_count = try r.expectMap(); 847 - if (map_count != 2) return error.InvalidMstNode; 848 + const map_hdr = cbor.readMapHeader(data, pos) catch return error.InvalidMstNode; 849 + pos = map_hdr.end; 850 + if (map_hdr.val != 2) return error.InvalidMstNode; 848 851 849 852 // "e" key 850 - const key_e = try r.readTextString(); 851 - if (!std.mem.eql(u8, key_e, "e")) return error.InvalidMstNode; 853 + const key_e = cbor.readText(data, pos) catch return error.InvalidMstNode; 854 + pos = key_e.end; 855 + if (!std.mem.eql(u8, key_e.val, "e")) return error.InvalidMstNode; 852 856 853 857 // entries array 854 - const entries_count = try r.expectArray(); 855 - const entries = try allocator.alloc(MstEntryData, entries_count); 858 + const arr_hdr = cbor.readArrayHeader(data, pos) catch return error.InvalidMstNode; 859 + pos = arr_hdr.end; 860 + const entries = try allocator.alloc(MstEntryData, @intCast(arr_hdr.val)); 861 + errdefer allocator.free(entries); 856 862 for (entries) |*entry| { 857 - entry.* = try readMstEntry(&r); 863 + const result = readMstEntry(data, pos) catch return error.InvalidMstNode; 864 + entry.* = result.entry; 865 + pos = result.end; 858 866 } 859 867 860 868 // "l" key 861 - const key_l = try r.readTextString(); 862 - if (!std.mem.eql(u8, key_l, "l")) return error.InvalidMstNode; 869 + const key_l = cbor.readText(data, pos) catch return error.InvalidMstNode; 870 + pos = key_l.end; 871 + if (!std.mem.eql(u8, key_l.val, "l")) return error.InvalidMstNode; 863 872 864 - const left = try r.readCidOrNull(); 873 + // left CID or null 874 + const left_result = readCidOrNull(data, pos) catch return error.InvalidMstNode; 865 875 866 - return .{ .left = left, .entries = entries }; 876 + return .{ .left = left_result.val, .entries = entries }; 867 877 } 868 878 869 - fn readMstEntry(r: *MstReader) !MstEntryData { 870 - const map_count = try r.expectMap(); 871 - if (map_count != 4) return error.InvalidMstNode; 879 + const MstEntryResult = struct { 880 + entry: MstEntryData, 881 + end: usize, 882 + }; 883 + 884 + fn readMstEntry(data: []const u8, pos: usize) !MstEntryResult { 885 + var p = pos; 886 + 887 + const map_hdr = cbor.readMapHeader(data, p) catch return error.InvalidMstNode; 888 + p = map_hdr.end; 889 + if (map_hdr.val != 4) return error.InvalidMstNode; 872 890 873 891 // "k" → key suffix (byte string) 874 - _ = try r.readTextString(); 875 - const key_suffix = try r.readByteString(); 892 + const key_k = cbor.readText(data, p) catch return error.InvalidMstNode; 893 + p = key_k.end; 894 + const key_suffix = cbor.readBytes(data, p) catch return error.InvalidMstNode; 895 + p = key_suffix.end; 876 896 877 897 // "p" → prefix length (unsigned int) 878 - _ = try r.readTextString(); 879 - const prefix_len = try r.readUnsigned(); 898 + const key_p = cbor.readText(data, p) catch return error.InvalidMstNode; 899 + p = key_p.end; 900 + const prefix_len = cbor.readUint(data, p) catch return error.InvalidMstNode; 901 + p = prefix_len.end; 880 902 881 903 // "t" → right subtree CID or null 882 - _ = try r.readTextString(); 883 - const tree = try r.readCidOrNull(); 904 + const key_t = cbor.readText(data, p) catch return error.InvalidMstNode; 905 + p = key_t.end; 906 + const tree_result = readCidOrNull(data, p) catch return error.InvalidMstNode; 907 + p = tree_result.end; 884 908 885 909 // "v" → value CID 886 - _ = try r.readTextString(); 887 - const value = try r.readCid(); 910 + const key_v = cbor.readText(data, p) catch return error.InvalidMstNode; 911 + p = key_v.end; 912 + const value = cbor.readCidLink(data, p) catch return error.InvalidMstNode; 913 + p = value.end; 888 914 889 915 return .{ 890 - .key_suffix = key_suffix, 891 - .prefix_len = @intCast(prefix_len), 892 - .tree = tree, 893 - .value = value, 916 + .entry = .{ 917 + .key_suffix = key_suffix.val, 918 + .prefix_len = @intCast(prefix_len.val), 919 + .tree = tree_result.val, 920 + .value = value.val, 921 + }, 922 + .end = p, 894 923 }; 895 924 } 896 925 897 - const MstReader = struct { 898 - data: []const u8, 899 - pos: usize, 900 - 901 - fn expectMap(self: *MstReader) !usize { 902 - return self.readMajorWithArg(5); 903 - } 904 - 905 - fn expectArray(self: *MstReader) !usize { 906 - return self.readMajorWithArg(4); 907 - } 908 - 909 - fn readTextString(self: *MstReader) ![]const u8 { 910 - const len = try self.readMajorWithArg(3); 911 - if (self.pos + len > self.data.len) return error.InvalidMstNode; 912 - const result = self.data[self.pos .. self.pos + len]; 913 - self.pos += len; 914 - return result; 915 - } 916 - 917 - fn readByteString(self: *MstReader) ![]const u8 { 918 - const len = try self.readMajorWithArg(2); 919 - if (self.pos + len > self.data.len) return error.InvalidMstNode; 920 - const result = self.data[self.pos .. self.pos + len]; 921 - self.pos += len; 922 - return result; 923 - } 924 - 925 - fn readUnsigned(self: *MstReader) !u64 { 926 - return self.readMajorWithArg(0); 927 - } 926 + const CidOrNullResult = struct { 927 + val: ?[]const u8, 928 + end: usize, 929 + }; 928 930 929 - fn readCidOrNull(self: *MstReader) !?[]const u8 { 930 - if (self.pos >= self.data.len) return error.InvalidMstNode; 931 - if (self.data[self.pos] == 0xf6) { 932 - self.pos += 1; 933 - return null; 934 - } 935 - return try self.readCid(); 936 - } 937 - 938 - fn readCid(self: *MstReader) ![]const u8 { 939 - // tag(42) encodes as 0xd8 0x2a 940 - if (self.pos + 1 >= self.data.len) return error.InvalidMstNode; 941 - if (self.data[self.pos] != 0xd8 or self.data[self.pos + 1] != 0x2a) 942 - return error.InvalidMstNode; 943 - self.pos += 2; 944 - const bytes = try self.readByteString(); 945 - if (bytes.len < 1 or bytes[0] != 0x00) return error.InvalidMstNode; 946 - return bytes[1..]; // skip 0x00 identity multibase prefix 947 - } 948 - 949 - fn readMajorWithArg(self: *MstReader, expected_major: u3) !usize { 950 - if (self.pos >= self.data.len) return error.InvalidMstNode; 951 - const b = self.data[self.pos]; 952 - self.pos += 1; 953 - const major: u3 = @truncate(b >> 5); 954 - if (major != expected_major) return error.InvalidMstNode; 955 - const additional: u5 = @truncate(b); 956 - return self.readArgValue(additional); 957 - } 958 - 959 - fn readArgValue(self: *MstReader, additional: u5) !usize { 960 - if (additional < 24) return @as(usize, additional); 961 - if (additional == 24) { 962 - if (self.pos >= self.data.len) return error.InvalidMstNode; 963 - const val = self.data[self.pos]; 964 - self.pos += 1; 965 - return @as(usize, val); 966 - } 967 - if (additional == 25) { 968 - if (self.pos + 2 > self.data.len) return error.InvalidMstNode; 969 - const val = std.mem.readInt(u16, self.data[self.pos..][0..2], .big); 970 - self.pos += 2; 971 - return @as(usize, val); 972 - } 973 - if (additional == 26) { 974 - if (self.pos + 4 > self.data.len) return error.InvalidMstNode; 975 - const val = std.mem.readInt(u32, self.data[self.pos..][0..4], .big); 976 - self.pos += 4; 977 - return @as(usize, val); 978 - } 979 - return error.InvalidMstNode; 980 - } 981 - }; 931 + fn readCidOrNull(data: []const u8, pos: usize) !CidOrNullResult { 932 + if (pos >= data.len) return error.InvalidMstNode; 933 + if (data[pos] == 0xf6) return .{ .val = null, .end = pos + 1 }; 934 + const cid_result = cbor.readCidLink(data, pos) catch return error.InvalidMstNode; 935 + return .{ .val = cid_result.val, .end = cid_result.end }; 936 + } 982 937 983 938 pub const MstDecodeError = error{InvalidMstNode} || Allocator.Error; 984 939
+253
src/internal/repo/mst_test.zig
··· 1 + //! additional MST tests ported from atmos (Go implementation). 2 + //! 3 + //! focuses on edge cases, stress tests, and compliance gaps not covered 4 + //! by the inline tests in mst.zig. 5 + 6 + const std = @import("std"); 7 + const mst = @import("mst.zig"); 8 + const cbor = @import("cbor.zig"); 9 + const Mst = mst.Mst; 10 + const Cid = cbor.Cid; 11 + 12 + // known empty tree CID from the AT Protocol reference implementations 13 + const empty_tree_cid = "bafyreie5737gdxlw5i64vzichcalba3z2v5n6icifvx5xytvske7mr3hpm"; 14 + 15 + fn testCid(a: std.mem.Allocator, data: []const u8) !Cid { 16 + return Cid.forDagCbor(a, data); 17 + } 18 + 19 + // === edge cases === 20 + 21 + test "get from empty tree returns null" { 22 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 23 + defer arena.deinit(); 24 + const a = arena.allocator(); 25 + 26 + const tree = Mst.init(a); 27 + try std.testing.expect(tree.get("anything") == null); 28 + try std.testing.expect(tree.get("app.bsky.feed.post/abc123") == null); 29 + } 30 + 31 + test "delete nonexistent key is no-op" { 32 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 33 + defer arena.deinit(); 34 + const a = arena.allocator(); 35 + 36 + var tree = Mst.init(a); 37 + const cid = try testCid(a, "value"); 38 + try tree.put("key1", cid); 39 + 40 + // deleting a key that doesn't exist should not error 41 + try tree.delete("nonexistent"); 42 + 43 + // original key still present 44 + try std.testing.expect(tree.get("key1") != null); 45 + } 46 + 47 + test "remove all keys produces empty tree CID" { 48 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 49 + defer arena.deinit(); 50 + const a = arena.allocator(); 51 + 52 + var tree = Mst.init(a); 53 + const cid = try testCid(a, "value"); 54 + 55 + try tree.put("key1", cid); 56 + try tree.put("key2", cid); 57 + try tree.put("key3", cid); 58 + 59 + try tree.delete("key1"); 60 + try tree.delete("key2"); 61 + try tree.delete("key3"); 62 + 63 + const root = try tree.rootCid(); 64 + const expected = try mst.parseCidString(a, empty_tree_cid); 65 + try std.testing.expectEqualSlices(u8, expected.raw, root.raw); 66 + } 67 + 68 + test "update existing key changes value" { 69 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 70 + defer arena.deinit(); 71 + const a = arena.allocator(); 72 + 73 + var tree = Mst.init(a); 74 + const cid1 = try testCid(a, "v1"); 75 + const cid2 = try testCid(a, "v2"); 76 + 77 + try tree.put("key", cid1); 78 + try std.testing.expectEqualSlices(u8, cid1.raw, tree.get("key").?.raw); 79 + 80 + try tree.put("key", cid2); 81 + try std.testing.expectEqualSlices(u8, cid2.raw, tree.get("key").?.raw); 82 + } 83 + 84 + // === order independence === 85 + 86 + test "order independence: 50 keys in 3 orderings produce same root CID" { 87 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 88 + defer arena.deinit(); 89 + const a = arena.allocator(); 90 + 91 + // generate 50 deterministic keys 92 + const n = 50; 93 + var keys: [n][]const u8 = undefined; 94 + var key_bufs: [n][64]u8 = undefined; 95 + for (0..n) |i| { 96 + const len = std.fmt.bufPrint(&key_bufs[i], "app.bsky.feed.post/{d:0>12}", .{i}) catch unreachable; 97 + keys[i] = len; 98 + } 99 + 100 + const val = try testCid(a, "value"); 101 + 102 + // forward order 103 + var tree1 = Mst.init(a); 104 + for (keys) |k| try tree1.put(k, val); 105 + const cid1 = try tree1.rootCid(); 106 + 107 + // reverse order 108 + var tree2 = Mst.init(a); 109 + var i: usize = n; 110 + while (i > 0) { 111 + i -= 1; 112 + try tree2.put(keys[i], val); 113 + } 114 + const cid2 = try tree2.rootCid(); 115 + 116 + // interleaved order (even indices first, then odd) 117 + var tree3 = Mst.init(a); 118 + for (0..n) |j| { 119 + if (j % 2 == 0) try tree3.put(keys[j], val); 120 + } 121 + for (0..n) |j| { 122 + if (j % 2 == 1) try tree3.put(keys[j], val); 123 + } 124 + const cid3 = try tree3.rootCid(); 125 + 126 + try std.testing.expectEqualSlices(u8, cid1.raw, cid2.raw); 127 + try std.testing.expectEqualSlices(u8, cid1.raw, cid3.raw); 128 + } 129 + 130 + // === stress tests === 131 + 132 + test "100 keys: all retrievable after insertion" { 133 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 134 + defer arena.deinit(); 135 + const a = arena.allocator(); 136 + 137 + const n = 100; 138 + var tree = Mst.init(a); 139 + var key_bufs: [n][64]u8 = undefined; 140 + var keys: [n][]const u8 = undefined; 141 + var cids: [n]Cid = undefined; 142 + 143 + for (0..n) |i| { 144 + keys[i] = std.fmt.bufPrint(&key_bufs[i], "app.bsky.feed.post/{d:0>12}", .{i}) catch unreachable; 145 + cids[i] = try testCid(a, keys[i]); 146 + try tree.put(keys[i], cids[i]); 147 + } 148 + 149 + // all keys retrievable with correct CIDs 150 + for (0..n) |i| { 151 + const got = tree.get(keys[i]) orelse { 152 + std.debug.print("FAIL: key {s} not found after insert\n", .{keys[i]}); 153 + return error.TestExpectedEqual; 154 + }; 155 + try std.testing.expectEqualSlices(u8, cids[i].raw, got.raw); 156 + } 157 + } 158 + 159 + test "insert 50 keys then remove every other" { 160 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 161 + defer arena.deinit(); 162 + const a = arena.allocator(); 163 + 164 + const n = 50; 165 + var tree = Mst.init(a); 166 + var key_bufs: [n][64]u8 = undefined; 167 + var keys: [n][]const u8 = undefined; 168 + const val = try testCid(a, "value"); 169 + 170 + for (0..n) |i| { 171 + keys[i] = std.fmt.bufPrint(&key_bufs[i], "app.bsky.feed.post/{d:0>12}", .{i}) catch unreachable; 172 + try tree.put(keys[i], val); 173 + } 174 + 175 + // remove even-indexed keys 176 + for (0..n) |i| { 177 + if (i % 2 == 0) try tree.delete(keys[i]); 178 + } 179 + 180 + // verify: even keys gone, odd keys present 181 + for (0..n) |i| { 182 + const got = tree.get(keys[i]); 183 + if (i % 2 == 0) { 184 + try std.testing.expect(got == null); 185 + } else { 186 + try std.testing.expect(got != null); 187 + } 188 + } 189 + } 190 + 191 + // === height edge cases === 192 + 193 + test "keyHeight: empty key has height 0" { 194 + try std.testing.expectEqual(@as(u32, 0), mst.keyHeight("")); 195 + } 196 + 197 + test "keyHeight: deterministic for same input" { 198 + const h1 = mst.keyHeight("app.bsky.feed.post/3jqfcqzm3fo2j"); 199 + const h2 = mst.keyHeight("app.bsky.feed.post/3jqfcqzm3fo2j"); 200 + try std.testing.expectEqual(h1, h2); 201 + } 202 + 203 + test "keyHeight: different keys can have different heights" { 204 + // "blue" is known to have height 1 from the interop fixtures 205 + try std.testing.expectEqual(@as(u32, 1), mst.keyHeight("blue")); 206 + try std.testing.expectEqual(@as(u32, 0), mst.keyHeight("asdf")); 207 + } 208 + 209 + // === allocation failure safety === 210 + 211 + fn decodeMstNodeImpl(backing: std.mem.Allocator, data: []const u8) !void { 212 + var arena = std.heap.ArenaAllocator.init(backing); 213 + defer arena.deinit(); 214 + _ = try mst.decodeMstNode(arena.allocator(), data); 215 + } 216 + 217 + test "checkAllAllocationFailures: decodeMstNode" { 218 + // build a valid MST node CBOR by hand: 219 + // map(2) { "e": [ map(4){k,p,t,v}, map(4){k,p,t,v} ], "l": null } 220 + var setup_arena = std.heap.ArenaAllocator.init(std.testing.allocator); 221 + defer setup_arena.deinit(); 222 + const sa = setup_arena.allocator(); 223 + 224 + const val_cid = try Cid.forDagCbor(sa, "value"); 225 + 226 + // build entry maps (the "k","p","t","v" maps inside the "e" array) 227 + const entry1: cbor.Value = .{ .map = &.{ 228 + .{ .key = "k", .value = .{ .bytes = "app.bsky.feed.post/aaa" } }, 229 + .{ .key = "p", .value = .{ .unsigned = 0 } }, 230 + .{ .key = "t", .value = .null }, 231 + .{ .key = "v", .value = .{ .cid = val_cid } }, 232 + } }; 233 + const entry2: cbor.Value = .{ 234 + .map = &.{ 235 + .{ .key = "k", .value = .{ .bytes = "bbb" } }, 236 + .{ .key = "p", .value = .{ .unsigned = 22 } }, // prefix_len = shared with prev key 237 + .{ .key = "t", .value = .null }, 238 + .{ .key = "v", .value = .{ .cid = val_cid } }, 239 + }, 240 + }; 241 + 242 + const node: cbor.Value = .{ .map = &.{ 243 + .{ .key = "e", .value = .{ .array = &.{ entry1, entry2 } } }, 244 + .{ .key = "l", .value = .null }, 245 + } }; 246 + const encoded = try cbor.encodeAlloc(sa, node); 247 + 248 + try std.testing.checkAllAllocationFailures( 249 + std.testing.allocator, 250 + decodeMstNodeImpl, 251 + .{encoded}, 252 + ); 253 + }
+3220
src/internal/repo/testdata/rfc8949-vectors.json
··· 1 + [ 2 + { 3 + "hex": "00", 4 + "flags": ["valid", "canonical"], 5 + "diagnostic": "0" 6 + }, 7 + { 8 + "hex": "01", 9 + "flags": ["valid", "canonical"], 10 + "diagnostic": "1" 11 + }, 12 + { 13 + "hex": "0a", 14 + "flags": ["valid", "canonical"], 15 + "diagnostic": "10" 16 + }, 17 + { 18 + "hex": "17", 19 + "flags": ["valid", "canonical"], 20 + "diagnostic": "23" 21 + }, 22 + { 23 + "hex": "1818", 24 + "flags": ["valid", "canonical"], 25 + "diagnostic": "24" 26 + }, 27 + { 28 + "hex": "1819", 29 + "flags": ["valid", "canonical"], 30 + "diagnostic": "25" 31 + }, 32 + { 33 + "hex": "1864", 34 + "flags": ["valid", "canonical"], 35 + "diagnostic": "100" 36 + }, 37 + { 38 + "hex": "1903e8", 39 + "flags": ["valid", "canonical"], 40 + "diagnostic": "1000" 41 + }, 42 + { 43 + "hex": "1a000f4240", 44 + "flags": ["valid", "canonical"], 45 + "diagnostic": "1000000" 46 + }, 47 + { 48 + "hex": "1b000000e8d4a51000", 49 + "flags": ["valid", "canonical"], 50 + "diagnostic": "1000000000000" 51 + }, 52 + { 53 + "hex": "1B3FFFFFFFFFFFFFFF", 54 + "flags": ["valid", "canonical"], 55 + "features": ["int63"], 56 + "diagnostic": "4611686018427387903" 57 + }, 58 + { 59 + "hex": "1bffffffffffffffff", 60 + "flags": ["valid", "canonical"], 61 + "features": ["int64"], 62 + "diagnostic": "18446744073709551615" 63 + }, 64 + { 65 + "hex": "c249010000000000000000", 66 + "flags": ["valid", "canonical"], 67 + "features": ["bignum"], 68 + "diagnostic": "18446744073709551616" 69 + }, 70 + { 71 + "hex": "c249010000000000000000", 72 + "flags": ["valid", "canonical"], 73 + "features": ["!bignum"], 74 + "diagnostic": "2(h'010000000000000000')" 75 + }, 76 + 77 + { 78 + "hex": "3bffffffffffffffff", 79 + "flags": ["valid", "canonical"], 80 + "features": ["int64"], 81 + "diagnostic": "-18446744073709551616" 82 + }, 83 + { 84 + "hex": "c349010000000000000000", 85 + "flags": ["valid", "canonical"], 86 + "features": ["bignum"], 87 + "diagnostic": "-18446744073709551617" 88 + }, 89 + { 90 + "hex": "c349010000000000000000", 91 + "flags": ["valid", "canonical"], 92 + "features": ["!bignum"], 93 + "diagnostic": "3(h'010000000000000000')" 94 + }, 95 + { 96 + "hex": "20", 97 + "flags": ["valid", "canonical"], 98 + "diagnostic": "-1" 99 + }, 100 + { 101 + "hex": "29", 102 + "flags": ["valid", "canonical"], 103 + "diagnostic": "-10" 104 + }, 105 + { 106 + "hex": "3863", 107 + "flags": ["valid", "canonical"], 108 + "diagnostic": "-100" 109 + }, 110 + { 111 + "hex": "3903e7", 112 + "flags": ["valid", "canonical"], 113 + "diagnostic": "-1000" 114 + }, 115 + { 116 + "hex": "f90000", 117 + "flags": ["valid", "canonical", "float"], 118 + "features": ["float16"], 119 + "diagnostic": "0.0" 120 + }, 121 + { 122 + "hex": "f98000", 123 + "flags": ["valid", "canonical", "float"], 124 + "features": ["float16"], 125 + "diagnostic": "-0.0" 126 + }, 127 + { 128 + "hex": "f93c00", 129 + "flags": ["valid", "canonical", "float"], 130 + "features": ["float16"], 131 + "diagnostic": "1.0" 132 + }, 133 + { 134 + "hex": "fb3ff199999999999a", 135 + "flags": ["valid", "canonical", "float"], 136 + "diagnostic": "1.1" 137 + }, 138 + { 139 + "hex": "f93e00", 140 + "flags": ["valid", "canonical", "float"], 141 + "features": ["float16"], 142 + "diagnostic": "1.5" 143 + }, 144 + { 145 + "hex": "f97bff", 146 + "flags": ["valid", "canonical", "float"], 147 + "features": ["float16"], 148 + "diagnostic": "65504.0" 149 + }, 150 + { 151 + "hex": "fa47c35000", 152 + "flags": ["valid", "canonical", "float"], 153 + "diagnostic": "100000.0" 154 + }, 155 + { 156 + "hex": "fa7f7fffff", 157 + "flags": ["valid", "canonical", "float"], 158 + "diagnostic": "3.40282346638529e+38" 159 + }, 160 + { 161 + "hex": "fb7e37e43c8800759c", 162 + "flags": ["valid", "canonical", "float"], 163 + "diagnostic": "1.0e+300" 164 + }, 165 + { 166 + "hex": "f90001", 167 + "flags": ["valid", "canonical", "float"], 168 + "features": ["float16"], 169 + "diagnostic": "5.96046447753906e-8" 170 + }, 171 + { 172 + "hex": "f90400", 173 + "flags": ["valid", "canonical", "float"], 174 + "features": ["float16"], 175 + "diagnostic": "6.103515625e-5" 176 + }, 177 + { 178 + "hex": "f9c400", 179 + "flags": ["valid", "canonical", "float"], 180 + "features": ["float16"], 181 + "diagnostic": "-4.0" 182 + }, 183 + { 184 + "hex": "fbc010666666666666", 185 + "flags": ["valid", "canonical", "float"], 186 + "diagnostic": "-4.1" 187 + }, 188 + { 189 + "hex": "f97c00", 190 + "flags": ["valid", "canonical"], 191 + "diagnostic": "Infinity" 192 + }, 193 + { 194 + "hex": "f97e00", 195 + "flags": ["valid", "canonical"], 196 + "diagnostic": "NaN" 197 + }, 198 + { 199 + "hex": "f9fc00", 200 + "flags": ["valid", "canonical"], 201 + "diagnostic": "-Infinity" 202 + }, 203 + { 204 + "hex": "fa7f800000", 205 + "flags": ["valid", "canonical"], 206 + "diagnostic": "Infinity" 207 + }, 208 + { 209 + "hex": "fa7fc00000", 210 + "flags": ["valid"], 211 + "diagnostic": "NaN" 212 + }, 213 + { 214 + "hex": "faff800000", 215 + "flags": ["valid"], 216 + "diagnostic": "-Infinity" 217 + }, 218 + { 219 + "hex": "fb7ff0000000000000", 220 + "flags": ["valid"], 221 + "diagnostic": "Infinity" 222 + }, 223 + { 224 + "hex": "fb7ff8000000000000", 225 + "flags": ["valid"], 226 + "diagnostic": "NaN" 227 + }, 228 + { 229 + "hex": "fbfff0000000000000", 230 + "flags": ["valid"], 231 + "diagnostic": "-Infinity" 232 + }, 233 + { 234 + "hex": "f4", 235 + "flags": ["valid", "canonical"], 236 + "diagnostic": "false" 237 + }, 238 + { 239 + "hex": "f5", 240 + "flags": ["valid", "canonical"], 241 + "diagnostic": "true" 242 + }, 243 + { 244 + "hex": "f6", 245 + "flags": ["valid", "canonical"], 246 + "diagnostic": "null" 247 + }, 248 + { 249 + "hex": "f7", 250 + "flags": ["valid", "canonical"], 251 + "diagnostic": "undefined" 252 + }, 253 + { 254 + "hex": "f0", 255 + "flags": ["valid", "canonical"], 256 + "features": ["simple"], 257 + "diagnostic": "simple(16)" 258 + }, 259 + { 260 + "hex": "f820", 261 + "flags": ["valid", "canonical"], 262 + "features": ["simple"], 263 + "diagnostic": "simple(32)" 264 + }, 265 + { 266 + "hex": "f8ff", 267 + "flags": ["valid", "canonical"], 268 + "features": ["simple"], 269 + "diagnostic": "simple(255)" 270 + }, 271 + { 272 + "hex": "c074323031332d30332d32315432303a30343a30305a", 273 + "flags": ["valid", "canonical"], 274 + "diagnostic": "0(\"2013-03-21T20:04:00Z\")" 275 + }, 276 + { 277 + "hex": "c11a514b67b0", 278 + "flags": ["valid", "canonical"], 279 + "diagnostic": "1(1363896240)" 280 + }, 281 + { 282 + "hex": "c1fb41d452d9ec200000", 283 + "flags": ["valid", "canonical", "float"], 284 + "diagnostic": "1(1363896240.5)" 285 + }, 286 + { 287 + "hex": "d74401020304", 288 + "flags": ["valid", "canonical"], 289 + "diagnostic": "23(h'01020304')" 290 + }, 291 + { 292 + "hex": "d818456449455446", 293 + "flags": ["valid", "canonical"], 294 + "diagnostic": "24(h'6449455446')" 295 + }, 296 + { 297 + "hex": "d82076687474703a2f2f7777772e6578616d706c652e636f6d", 298 + "flags": ["valid", "canonical"], 299 + "diagnostic": "32(\"http://www.example.com\")" 300 + }, 301 + { 302 + "hex": "40", 303 + "flags": ["valid", "canonical"], 304 + "diagnostic": "h''" 305 + }, 306 + { 307 + "hex": "4401020304", 308 + "flags": ["valid", "canonical"], 309 + "diagnostic": "h'01020304'" 310 + }, 311 + { 312 + "hex": "60", 313 + "flags": ["valid", "canonical"], 314 + "diagnostic": "\"\"" 315 + }, 316 + { 317 + "hex": "6161", 318 + "flags": ["valid", "canonical"], 319 + "diagnostic": "\"a\"" 320 + }, 321 + { 322 + "hex": "6449455446", 323 + "flags": ["valid", "canonical"], 324 + "diagnostic": "\"IETF\"" 325 + }, 326 + { 327 + "hex": "62225c", 328 + "flags": ["valid", "canonical"], 329 + "diagnostic": "\"\\\"\\\\\"" 330 + }, 331 + { 332 + "hex": "62c3bc", 333 + "flags": ["valid", "canonical"], 334 + "diagnostic": "\"ü\"" 335 + }, 336 + { 337 + "hex": "63e6b0b4", 338 + "flags": ["valid", "canonical"], 339 + "diagnostic": "\"水\"" 340 + }, 341 + { 342 + "hex": "64f0908591", 343 + "flags": ["valid", "canonical"], 344 + "diagnostic": "\"\uD800\uDD51\"" 345 + }, 346 + { 347 + "hex": "80", 348 + "flags": ["valid", "canonical"], 349 + "diagnostic": "[]" 350 + }, 351 + { 352 + "hex": "83010203", 353 + "flags": ["valid", "canonical"], 354 + "diagnostic": "[1, 2, 3]" 355 + }, 356 + { 357 + "hex": "8301820203820405", 358 + "flags": ["valid", "canonical"], 359 + "diagnostic": "[1, [2, 3], [4, 5]]" 360 + }, 361 + { 362 + "hex": "98190102030405060708090a0b0c0d0e0f101112131415161718181819", 363 + "flags": ["valid", "canonical"], 364 + "diagnostic": "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]" 365 + }, 366 + { 367 + "hex": "a0", 368 + "flags": ["valid", "canonical"], 369 + "diagnostic": "{}" 370 + }, 371 + { 372 + "hex": "a201020304", 373 + "flags": ["valid", "canonical"], 374 + "diagnostic": "{1: 2, 3: 4}" 375 + }, 376 + { 377 + "hex": "a26161016162820203", 378 + "flags": ["valid", "canonical"], 379 + "diagnostic": "{\"a\": 1, \"b\": [2, 3]}" 380 + }, 381 + { 382 + "hex": "826161a161626163", 383 + "flags": ["valid", "canonical"], 384 + "diagnostic": "[\"a\", {\"b\": \"c\"}]" 385 + }, 386 + { 387 + "hex": "a56161614161626142616361436164614461656145", 388 + "flags": ["valid", "canonical"], 389 + "diagnostic": "{\"a\": \"A\", \"b\": \"B\", \"c\": \"C\", \"d\": \"D\", \"e\": \"E\"}" 390 + }, 391 + { 392 + "hex": "5f42010243030405ff", 393 + "flags": ["valid"], 394 + "diagnostic": "h'0102030405'", 395 + "diagnosticExact": "(_ h'0102', h'030405')" 396 + }, 397 + { 398 + "hex": "7f657374726561646d696e67ff", 399 + "flags": ["valid"], 400 + "diagnostic": "\"streaming\"", 401 + "diagnosticExact": "(_ \"strea\", \"ming\")" 402 + }, 403 + { 404 + "hex": "9fff", 405 + "flags": ["valid"], 406 + "diagnostic": "[]" 407 + }, 408 + { 409 + "hex": "9f018202039f0405ffff", 410 + "flags": ["valid"], 411 + "diagnostic": "[1, [2, 3], [4, 5]]" 412 + }, 413 + { 414 + "hex": "9f01820203820405ff", 415 + "flags": ["valid"], 416 + "diagnostic": "[1, [2, 3], [4, 5]]" 417 + }, 418 + { 419 + "hex": "83018202039f0405ff", 420 + "flags": ["valid"], 421 + "diagnostic": "[1, [2, 3], [4, 5]]" 422 + }, 423 + { 424 + "hex": "83019f0203ff820405", 425 + "flags": ["valid"], 426 + "diagnostic": "[1, [2, 3], [4, 5]]" 427 + }, 428 + { 429 + "hex": "9f0102030405060708090a0b0c0d0e0f101112131415161718181819ff", 430 + "flags": ["valid"], 431 + "diagnostic": "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]" 432 + }, 433 + { 434 + "hex": "bf61610161629f0203ffff", 435 + "flags": ["valid"], 436 + "diagnostic": "{\"a\": 1, \"b\": [2, 3]}" 437 + }, 438 + { 439 + "hex": "826161bf61626163ff", 440 + "flags": ["valid"], 441 + "diagnostic": "[\"a\", {\"b\": \"c\"}]" 442 + }, 443 + { 444 + "hex": "bf6346756ef563416d7421ff", 445 + "flags": ["valid"], 446 + "diagnostic": "{\"Fun\": true, \"Amt\": -2}" 447 + }, 448 + { 449 + "flags": ["invalid"], 450 + "hex": "1c" 451 + }, 452 + { 453 + "flags": ["invalid"], 454 + "hex": "1d" 455 + }, 456 + { 457 + "flags": ["invalid"], 458 + "hex": "1e" 459 + }, 460 + { 461 + "flags": ["invalid"], 462 + "hex": "1f" 463 + }, 464 + { 465 + "flags": ["invalid"], 466 + "hex": "3c" 467 + }, 468 + { 469 + "flags": ["invalid"], 470 + "hex": "3d" 471 + }, 472 + { 473 + "flags": ["invalid"], 474 + "hex": "3e" 475 + }, 476 + { 477 + "flags": ["invalid"], 478 + "hex": "3f" 479 + }, 480 + { 481 + "flags": ["invalid"], 482 + "hex": "5c" 483 + }, 484 + { 485 + "flags": ["invalid"], 486 + "hex": "5d" 487 + }, 488 + { 489 + "flags": ["invalid"], 490 + "hex": "5e" 491 + }, 492 + { 493 + "flags": ["invalid"], 494 + "hex": "5f00" 495 + }, 496 + { 497 + "flags": ["invalid"], 498 + "hex": "5f01" 499 + }, 500 + { 501 + "flags": ["invalid"], 502 + "hex": "5f02" 503 + }, 504 + { 505 + "flags": ["invalid"], 506 + "hex": "5f03" 507 + }, 508 + { 509 + "flags": ["invalid"], 510 + "hex": "5f04" 511 + }, 512 + { 513 + "flags": ["invalid"], 514 + "hex": "5f05" 515 + }, 516 + { 517 + "flags": ["invalid"], 518 + "hex": "5f06" 519 + }, 520 + { 521 + "flags": ["invalid"], 522 + "hex": "5f07" 523 + }, 524 + { 525 + "flags": ["invalid"], 526 + "hex": "5f08" 527 + }, 528 + { 529 + "flags": ["invalid"], 530 + "hex": "5f09" 531 + }, 532 + { 533 + "flags": ["invalid"], 534 + "hex": "5f0a" 535 + }, 536 + { 537 + "flags": ["invalid"], 538 + "hex": "5f0b" 539 + }, 540 + { 541 + "flags": ["invalid"], 542 + "hex": "5f0c" 543 + }, 544 + { 545 + "flags": ["invalid"], 546 + "hex": "5f0d" 547 + }, 548 + { 549 + "flags": ["invalid"], 550 + "hex": "5f0e" 551 + }, 552 + { 553 + "flags": ["invalid"], 554 + "hex": "5f0f" 555 + }, 556 + { 557 + "flags": ["invalid"], 558 + "hex": "5f10" 559 + }, 560 + { 561 + "flags": ["invalid"], 562 + "hex": "5f11" 563 + }, 564 + { 565 + "flags": ["invalid"], 566 + "hex": "5f12" 567 + }, 568 + { 569 + "flags": ["invalid"], 570 + "hex": "5f13" 571 + }, 572 + { 573 + "flags": ["invalid"], 574 + "hex": "5f14" 575 + }, 576 + { 577 + "flags": ["invalid"], 578 + "hex": "5f15" 579 + }, 580 + { 581 + "flags": ["invalid"], 582 + "hex": "5f16" 583 + }, 584 + { 585 + "flags": ["invalid"], 586 + "hex": "5f17" 587 + }, 588 + { 589 + "flags": ["invalid"], 590 + "hex": "5f18" 591 + }, 592 + { 593 + "flags": ["invalid"], 594 + "hex": "5f19" 595 + }, 596 + { 597 + "flags": ["invalid"], 598 + "hex": "5f1a" 599 + }, 600 + { 601 + "flags": ["invalid"], 602 + "hex": "5f1b" 603 + }, 604 + { 605 + "flags": ["invalid"], 606 + "hex": "5f1c" 607 + }, 608 + { 609 + "flags": ["invalid"], 610 + "hex": "5f1d" 611 + }, 612 + { 613 + "flags": ["invalid"], 614 + "hex": "5f1e" 615 + }, 616 + { 617 + "flags": ["invalid"], 618 + "hex": "5f1f" 619 + }, 620 + { 621 + "flags": ["invalid"], 622 + "hex": "5f20" 623 + }, 624 + { 625 + "flags": ["invalid"], 626 + "hex": "5f21" 627 + }, 628 + { 629 + "flags": ["invalid"], 630 + "hex": "5f22" 631 + }, 632 + { 633 + "flags": ["invalid"], 634 + "hex": "5f23" 635 + }, 636 + { 637 + "flags": ["invalid"], 638 + "hex": "5f24" 639 + }, 640 + { 641 + "flags": ["invalid"], 642 + "hex": "5f25" 643 + }, 644 + { 645 + "flags": ["invalid"], 646 + "hex": "5f26" 647 + }, 648 + { 649 + "flags": ["invalid"], 650 + "hex": "5f27" 651 + }, 652 + { 653 + "flags": ["invalid"], 654 + "hex": "5f28" 655 + }, 656 + { 657 + "flags": ["invalid"], 658 + "hex": "5f29" 659 + }, 660 + { 661 + "flags": ["invalid"], 662 + "hex": "5f2a" 663 + }, 664 + { 665 + "flags": ["invalid"], 666 + "hex": "5f2b" 667 + }, 668 + { 669 + "flags": ["invalid"], 670 + "hex": "5f2c" 671 + }, 672 + { 673 + "flags": ["invalid"], 674 + "hex": "5f2d" 675 + }, 676 + { 677 + "flags": ["invalid"], 678 + "hex": "5f2e" 679 + }, 680 + { 681 + "flags": ["invalid"], 682 + "hex": "5f2f" 683 + }, 684 + { 685 + "flags": ["invalid"], 686 + "hex": "5f30" 687 + }, 688 + { 689 + "flags": ["invalid"], 690 + "hex": "5f31" 691 + }, 692 + { 693 + "flags": ["invalid"], 694 + "hex": "5f32" 695 + }, 696 + { 697 + "flags": ["invalid"], 698 + "hex": "5f33" 699 + }, 700 + { 701 + "flags": ["invalid"], 702 + "hex": "5f34" 703 + }, 704 + { 705 + "flags": ["invalid"], 706 + "hex": "5f35" 707 + }, 708 + { 709 + "flags": ["invalid"], 710 + "hex": "5f36" 711 + }, 712 + { 713 + "flags": ["invalid"], 714 + "hex": "5f37" 715 + }, 716 + { 717 + "flags": ["invalid"], 718 + "hex": "5f38" 719 + }, 720 + { 721 + "flags": ["invalid"], 722 + "hex": "5f39" 723 + }, 724 + { 725 + "flags": ["invalid"], 726 + "hex": "5f3a" 727 + }, 728 + { 729 + "flags": ["invalid"], 730 + "hex": "5f3b" 731 + }, 732 + { 733 + "flags": ["invalid"], 734 + "hex": "5f3c" 735 + }, 736 + { 737 + "flags": ["invalid"], 738 + "hex": "5f3d" 739 + }, 740 + { 741 + "flags": ["invalid"], 742 + "hex": "5f3e" 743 + }, 744 + { 745 + "flags": ["invalid"], 746 + "hex": "5f3f" 747 + }, 748 + { 749 + "flags": ["invalid"], 750 + "hex": "5f5c" 751 + }, 752 + { 753 + "flags": ["invalid"], 754 + "hex": "5f5d" 755 + }, 756 + { 757 + "flags": ["invalid"], 758 + "hex": "5f5e" 759 + }, 760 + { 761 + "flags": ["invalid"], 762 + "hex": "5f5f" 763 + }, 764 + { 765 + "flags": ["invalid"], 766 + "hex": "5f60" 767 + }, 768 + { 769 + "flags": ["invalid"], 770 + "hex": "5f61" 771 + }, 772 + { 773 + "flags": ["invalid"], 774 + "hex": "5f62" 775 + }, 776 + { 777 + "flags": ["invalid"], 778 + "hex": "5f63" 779 + }, 780 + { 781 + "flags": ["invalid"], 782 + "hex": "5f64" 783 + }, 784 + { 785 + "flags": ["invalid"], 786 + "hex": "5f65" 787 + }, 788 + { 789 + "flags": ["invalid"], 790 + "hex": "5f66" 791 + }, 792 + { 793 + "flags": ["invalid"], 794 + "hex": "5f67" 795 + }, 796 + { 797 + "flags": ["invalid"], 798 + "hex": "5f68" 799 + }, 800 + { 801 + "flags": ["invalid"], 802 + "hex": "5f69" 803 + }, 804 + { 805 + "flags": ["invalid"], 806 + "hex": "5f6a" 807 + }, 808 + { 809 + "flags": ["invalid"], 810 + "hex": "5f6b" 811 + }, 812 + { 813 + "flags": ["invalid"], 814 + "hex": "5f6c" 815 + }, 816 + { 817 + "flags": ["invalid"], 818 + "hex": "5f6d" 819 + }, 820 + { 821 + "flags": ["invalid"], 822 + "hex": "5f6e" 823 + }, 824 + { 825 + "flags": ["invalid"], 826 + "hex": "5f6f" 827 + }, 828 + { 829 + "flags": ["invalid"], 830 + "hex": "5f70" 831 + }, 832 + { 833 + "flags": ["invalid"], 834 + "hex": "5f71" 835 + }, 836 + { 837 + "flags": ["invalid"], 838 + "hex": "5f72" 839 + }, 840 + { 841 + "flags": ["invalid"], 842 + "hex": "5f73" 843 + }, 844 + { 845 + "flags": ["invalid"], 846 + "hex": "5f74" 847 + }, 848 + { 849 + "flags": ["invalid"], 850 + "hex": "5f75" 851 + }, 852 + { 853 + "flags": ["invalid"], 854 + "hex": "5f76" 855 + }, 856 + { 857 + "flags": ["invalid"], 858 + "hex": "5f77" 859 + }, 860 + { 861 + "flags": ["invalid"], 862 + "hex": "5f78" 863 + }, 864 + { 865 + "flags": ["invalid"], 866 + "hex": "5f79" 867 + }, 868 + { 869 + "flags": ["invalid"], 870 + "hex": "5f7a" 871 + }, 872 + { 873 + "flags": ["invalid"], 874 + "hex": "5f7b" 875 + }, 876 + { 877 + "flags": ["invalid"], 878 + "hex": "5f7c" 879 + }, 880 + { 881 + "flags": ["invalid"], 882 + "hex": "5f7d" 883 + }, 884 + { 885 + "flags": ["invalid"], 886 + "hex": "5f7e" 887 + }, 888 + { 889 + "flags": ["invalid"], 890 + "hex": "5f7f" 891 + }, 892 + { 893 + "flags": ["invalid"], 894 + "hex": "5f80" 895 + }, 896 + { 897 + "flags": ["invalid"], 898 + "hex": "5f81" 899 + }, 900 + { 901 + "flags": ["invalid"], 902 + "hex": "5f82" 903 + }, 904 + { 905 + "flags": ["invalid"], 906 + "hex": "5f83" 907 + }, 908 + { 909 + "flags": ["invalid"], 910 + "hex": "5f84" 911 + }, 912 + { 913 + "flags": ["invalid"], 914 + "hex": "5f85" 915 + }, 916 + { 917 + "flags": ["invalid"], 918 + "hex": "5f86" 919 + }, 920 + { 921 + "flags": ["invalid"], 922 + "hex": "5f87" 923 + }, 924 + { 925 + "flags": ["invalid"], 926 + "hex": "5f88" 927 + }, 928 + { 929 + "flags": ["invalid"], 930 + "hex": "5f89" 931 + }, 932 + { 933 + "flags": ["invalid"], 934 + "hex": "5f8a" 935 + }, 936 + { 937 + "flags": ["invalid"], 938 + "hex": "5f8b" 939 + }, 940 + { 941 + "flags": ["invalid"], 942 + "hex": "5f8c" 943 + }, 944 + { 945 + "flags": ["invalid"], 946 + "hex": "5f8d" 947 + }, 948 + { 949 + "flags": ["invalid"], 950 + "hex": "5f8e" 951 + }, 952 + { 953 + "flags": ["invalid"], 954 + "hex": "5f8f" 955 + }, 956 + { 957 + "flags": ["invalid"], 958 + "hex": "5f90" 959 + }, 960 + { 961 + "flags": ["invalid"], 962 + "hex": "5f91" 963 + }, 964 + { 965 + "flags": ["invalid"], 966 + "hex": "5f92" 967 + }, 968 + { 969 + "flags": ["invalid"], 970 + "hex": "5f93" 971 + }, 972 + { 973 + "flags": ["invalid"], 974 + "hex": "5f94" 975 + }, 976 + { 977 + "flags": ["invalid"], 978 + "hex": "5f95" 979 + }, 980 + { 981 + "flags": ["invalid"], 982 + "hex": "5f96" 983 + }, 984 + { 985 + "flags": ["invalid"], 986 + "hex": "5f97" 987 + }, 988 + { 989 + "flags": ["invalid"], 990 + "hex": "5f98" 991 + }, 992 + { 993 + "flags": ["invalid"], 994 + "hex": "5f99" 995 + }, 996 + { 997 + "flags": ["invalid"], 998 + "hex": "5f9a" 999 + }, 1000 + { 1001 + "flags": ["invalid"], 1002 + "hex": "5f9b" 1003 + }, 1004 + { 1005 + "flags": ["invalid"], 1006 + "hex": "5f9c" 1007 + }, 1008 + { 1009 + "flags": ["invalid"], 1010 + "hex": "5f9d" 1011 + }, 1012 + { 1013 + "flags": ["invalid"], 1014 + "hex": "5f9e" 1015 + }, 1016 + { 1017 + "flags": ["invalid"], 1018 + "hex": "5f9f" 1019 + }, 1020 + { 1021 + "flags": ["invalid"], 1022 + "hex": "5fa0" 1023 + }, 1024 + { 1025 + "flags": ["invalid"], 1026 + "hex": "5fa1" 1027 + }, 1028 + { 1029 + "flags": ["invalid"], 1030 + "hex": "5fa2" 1031 + }, 1032 + { 1033 + "flags": ["invalid"], 1034 + "hex": "5fa3" 1035 + }, 1036 + { 1037 + "flags": ["invalid"], 1038 + "hex": "5fa4" 1039 + }, 1040 + { 1041 + "flags": ["invalid"], 1042 + "hex": "5fa5" 1043 + }, 1044 + { 1045 + "flags": ["invalid"], 1046 + "hex": "5fa6" 1047 + }, 1048 + { 1049 + "flags": ["invalid"], 1050 + "hex": "5fa7" 1051 + }, 1052 + { 1053 + "flags": ["invalid"], 1054 + "hex": "5fa8" 1055 + }, 1056 + { 1057 + "flags": ["invalid"], 1058 + "hex": "5fa9" 1059 + }, 1060 + { 1061 + "flags": ["invalid"], 1062 + "hex": "5faa" 1063 + }, 1064 + { 1065 + "flags": ["invalid"], 1066 + "hex": "5fab" 1067 + }, 1068 + { 1069 + "flags": ["invalid"], 1070 + "hex": "5fac" 1071 + }, 1072 + { 1073 + "flags": ["invalid"], 1074 + "hex": "5fad" 1075 + }, 1076 + { 1077 + "flags": ["invalid"], 1078 + "hex": "5fae" 1079 + }, 1080 + { 1081 + "flags": ["invalid"], 1082 + "hex": "5faf" 1083 + }, 1084 + { 1085 + "flags": ["invalid"], 1086 + "hex": "5fb0" 1087 + }, 1088 + { 1089 + "flags": ["invalid"], 1090 + "hex": "5fb1" 1091 + }, 1092 + { 1093 + "flags": ["invalid"], 1094 + "hex": "5fb2" 1095 + }, 1096 + { 1097 + "flags": ["invalid"], 1098 + "hex": "5fb3" 1099 + }, 1100 + { 1101 + "flags": ["invalid"], 1102 + "hex": "5fb4" 1103 + }, 1104 + { 1105 + "flags": ["invalid"], 1106 + "hex": "5fb5" 1107 + }, 1108 + { 1109 + "flags": ["invalid"], 1110 + "hex": "5fb6" 1111 + }, 1112 + { 1113 + "flags": ["invalid"], 1114 + "hex": "5fb7" 1115 + }, 1116 + { 1117 + "flags": ["invalid"], 1118 + "hex": "5fb8" 1119 + }, 1120 + { 1121 + "flags": ["invalid"], 1122 + "hex": "5fb9" 1123 + }, 1124 + { 1125 + "flags": ["invalid"], 1126 + "hex": "5fba" 1127 + }, 1128 + { 1129 + "flags": ["invalid"], 1130 + "hex": "5fbb" 1131 + }, 1132 + { 1133 + "flags": ["invalid"], 1134 + "hex": "5fbc" 1135 + }, 1136 + { 1137 + "flags": ["invalid"], 1138 + "hex": "5fbd" 1139 + }, 1140 + { 1141 + "flags": ["invalid"], 1142 + "hex": "5fbe" 1143 + }, 1144 + { 1145 + "flags": ["invalid"], 1146 + "hex": "5fbf" 1147 + }, 1148 + { 1149 + "flags": ["invalid"], 1150 + "hex": "5fc0" 1151 + }, 1152 + { 1153 + "flags": ["invalid"], 1154 + "hex": "5fc1" 1155 + }, 1156 + { 1157 + "flags": ["invalid"], 1158 + "hex": "5fc2" 1159 + }, 1160 + { 1161 + "flags": ["invalid"], 1162 + "hex": "5fc3" 1163 + }, 1164 + { 1165 + "flags": ["invalid"], 1166 + "hex": "5fc4" 1167 + }, 1168 + { 1169 + "flags": ["invalid"], 1170 + "hex": "5fc5" 1171 + }, 1172 + { 1173 + "flags": ["invalid"], 1174 + "hex": "5fc6" 1175 + }, 1176 + { 1177 + "flags": ["invalid"], 1178 + "hex": "5fc7" 1179 + }, 1180 + { 1181 + "flags": ["invalid"], 1182 + "hex": "5fc8" 1183 + }, 1184 + { 1185 + "flags": ["invalid"], 1186 + "hex": "5fc9" 1187 + }, 1188 + { 1189 + "flags": ["invalid"], 1190 + "hex": "5fca" 1191 + }, 1192 + { 1193 + "flags": ["invalid"], 1194 + "hex": "5fcb" 1195 + }, 1196 + { 1197 + "flags": ["invalid"], 1198 + "hex": "5fcc" 1199 + }, 1200 + { 1201 + "flags": ["invalid"], 1202 + "hex": "5fcd" 1203 + }, 1204 + { 1205 + "flags": ["invalid"], 1206 + "hex": "5fce" 1207 + }, 1208 + { 1209 + "flags": ["invalid"], 1210 + "hex": "5fcf" 1211 + }, 1212 + { 1213 + "flags": ["invalid"], 1214 + "hex": "5fd0" 1215 + }, 1216 + { 1217 + "flags": ["invalid"], 1218 + "hex": "5fd1" 1219 + }, 1220 + { 1221 + "flags": ["invalid"], 1222 + "hex": "5fd2" 1223 + }, 1224 + { 1225 + "flags": ["invalid"], 1226 + "hex": "5fd3" 1227 + }, 1228 + { 1229 + "flags": ["invalid"], 1230 + "hex": "5fd4" 1231 + }, 1232 + { 1233 + "flags": ["invalid"], 1234 + "hex": "5fd5" 1235 + }, 1236 + { 1237 + "flags": ["invalid"], 1238 + "hex": "5fd6" 1239 + }, 1240 + { 1241 + "flags": ["invalid"], 1242 + "hex": "5fd7" 1243 + }, 1244 + { 1245 + "flags": ["invalid"], 1246 + "hex": "5fd8" 1247 + }, 1248 + { 1249 + "flags": ["invalid"], 1250 + "hex": "5fd9" 1251 + }, 1252 + { 1253 + "flags": ["invalid"], 1254 + "hex": "5fda" 1255 + }, 1256 + { 1257 + "flags": ["invalid"], 1258 + "hex": "5fdb" 1259 + }, 1260 + { 1261 + "flags": ["invalid"], 1262 + "hex": "5fdc" 1263 + }, 1264 + { 1265 + "flags": ["invalid"], 1266 + "hex": "5fdd" 1267 + }, 1268 + { 1269 + "flags": ["invalid"], 1270 + "hex": "5fde" 1271 + }, 1272 + { 1273 + "flags": ["invalid"], 1274 + "hex": "5fdf" 1275 + }, 1276 + { 1277 + "flags": ["invalid"], 1278 + "hex": "5fe0" 1279 + }, 1280 + { 1281 + "flags": ["invalid"], 1282 + "hex": "5fe1" 1283 + }, 1284 + { 1285 + "flags": ["invalid"], 1286 + "hex": "5fe2" 1287 + }, 1288 + { 1289 + "flags": ["invalid"], 1290 + "hex": "5fe3" 1291 + }, 1292 + { 1293 + "flags": ["invalid"], 1294 + "hex": "5fe4" 1295 + }, 1296 + { 1297 + "flags": ["invalid"], 1298 + "hex": "5fe5" 1299 + }, 1300 + { 1301 + "flags": ["invalid"], 1302 + "hex": "5fe6" 1303 + }, 1304 + { 1305 + "flags": ["invalid"], 1306 + "hex": "5fe7" 1307 + }, 1308 + { 1309 + "flags": ["invalid"], 1310 + "hex": "5fe8" 1311 + }, 1312 + { 1313 + "flags": ["invalid"], 1314 + "hex": "5fe9" 1315 + }, 1316 + { 1317 + "flags": ["invalid"], 1318 + "hex": "5fea" 1319 + }, 1320 + { 1321 + "flags": ["invalid"], 1322 + "hex": "5feb" 1323 + }, 1324 + { 1325 + "flags": ["invalid"], 1326 + "hex": "5fec" 1327 + }, 1328 + { 1329 + "flags": ["invalid"], 1330 + "hex": "5fed" 1331 + }, 1332 + { 1333 + "flags": ["invalid"], 1334 + "hex": "5fee" 1335 + }, 1336 + { 1337 + "flags": ["invalid"], 1338 + "hex": "5fef" 1339 + }, 1340 + { 1341 + "flags": ["invalid"], 1342 + "hex": "5ff0" 1343 + }, 1344 + { 1345 + "flags": ["invalid"], 1346 + "hex": "5ff1" 1347 + }, 1348 + { 1349 + "flags": ["invalid"], 1350 + "hex": "5ff2" 1351 + }, 1352 + { 1353 + "flags": ["invalid"], 1354 + "hex": "5ff3" 1355 + }, 1356 + { 1357 + "flags": ["invalid"], 1358 + "hex": "5ff4" 1359 + }, 1360 + { 1361 + "flags": ["invalid"], 1362 + "hex": "5ff5" 1363 + }, 1364 + { 1365 + "flags": ["invalid"], 1366 + "hex": "5ff6" 1367 + }, 1368 + { 1369 + "flags": ["invalid"], 1370 + "hex": "5ff7" 1371 + }, 1372 + { 1373 + "flags": ["invalid"], 1374 + "hex": "5ff8" 1375 + }, 1376 + { 1377 + "flags": ["invalid"], 1378 + "hex": "5ff9" 1379 + }, 1380 + { 1381 + "flags": ["invalid"], 1382 + "hex": "5ffa" 1383 + }, 1384 + { 1385 + "flags": ["invalid"], 1386 + "hex": "5ffb" 1387 + }, 1388 + { 1389 + "flags": ["invalid"], 1390 + "hex": "5ffc" 1391 + }, 1392 + { 1393 + "flags": ["invalid"], 1394 + "hex": "5ffd" 1395 + }, 1396 + { 1397 + "flags": ["invalid"], 1398 + "hex": "5ffe" 1399 + }, 1400 + { 1401 + "flags": ["invalid"], 1402 + "hex": "7c" 1403 + }, 1404 + { 1405 + "flags": ["invalid"], 1406 + "hex": "7d" 1407 + }, 1408 + { 1409 + "flags": ["invalid"], 1410 + "hex": "7e" 1411 + }, 1412 + { 1413 + "flags": ["invalid"], 1414 + "hex": "7f00" 1415 + }, 1416 + { 1417 + "flags": ["invalid"], 1418 + "hex": "7f01" 1419 + }, 1420 + { 1421 + "flags": ["invalid"], 1422 + "hex": "7f02" 1423 + }, 1424 + { 1425 + "flags": ["invalid"], 1426 + "hex": "7f03" 1427 + }, 1428 + { 1429 + "flags": ["invalid"], 1430 + "hex": "7f04" 1431 + }, 1432 + { 1433 + "flags": ["invalid"], 1434 + "hex": "7f05" 1435 + }, 1436 + { 1437 + "flags": ["invalid"], 1438 + "hex": "7f06" 1439 + }, 1440 + { 1441 + "flags": ["invalid"], 1442 + "hex": "7f07" 1443 + }, 1444 + { 1445 + "flags": ["invalid"], 1446 + "hex": "7f08" 1447 + }, 1448 + { 1449 + "flags": ["invalid"], 1450 + "hex": "7f09" 1451 + }, 1452 + { 1453 + "flags": ["invalid"], 1454 + "hex": "7f0a" 1455 + }, 1456 + { 1457 + "flags": ["invalid"], 1458 + "hex": "7f0b" 1459 + }, 1460 + { 1461 + "flags": ["invalid"], 1462 + "hex": "7f0c" 1463 + }, 1464 + { 1465 + "flags": ["invalid"], 1466 + "hex": "7f0d" 1467 + }, 1468 + { 1469 + "flags": ["invalid"], 1470 + "hex": "7f0e" 1471 + }, 1472 + { 1473 + "flags": ["invalid"], 1474 + "hex": "7f0f" 1475 + }, 1476 + { 1477 + "flags": ["invalid"], 1478 + "hex": "7f10" 1479 + }, 1480 + { 1481 + "flags": ["invalid"], 1482 + "hex": "7f11" 1483 + }, 1484 + { 1485 + "flags": ["invalid"], 1486 + "hex": "7f12" 1487 + }, 1488 + { 1489 + "flags": ["invalid"], 1490 + "hex": "7f13" 1491 + }, 1492 + { 1493 + "flags": ["invalid"], 1494 + "hex": "7f14" 1495 + }, 1496 + { 1497 + "flags": ["invalid"], 1498 + "hex": "7f15" 1499 + }, 1500 + { 1501 + "flags": ["invalid"], 1502 + "hex": "7f16" 1503 + }, 1504 + { 1505 + "flags": ["invalid"], 1506 + "hex": "7f17" 1507 + }, 1508 + { 1509 + "flags": ["invalid"], 1510 + "hex": "7f18" 1511 + }, 1512 + { 1513 + "flags": ["invalid"], 1514 + "hex": "7f19" 1515 + }, 1516 + { 1517 + "flags": ["invalid"], 1518 + "hex": "7f1a" 1519 + }, 1520 + { 1521 + "flags": ["invalid"], 1522 + "hex": "7f1b" 1523 + }, 1524 + { 1525 + "flags": ["invalid"], 1526 + "hex": "7f1c" 1527 + }, 1528 + { 1529 + "flags": ["invalid"], 1530 + "hex": "7f1d" 1531 + }, 1532 + { 1533 + "flags": ["invalid"], 1534 + "hex": "7f1e" 1535 + }, 1536 + { 1537 + "flags": ["invalid"], 1538 + "hex": "7f1f" 1539 + }, 1540 + { 1541 + "flags": ["invalid"], 1542 + "hex": "7f20" 1543 + }, 1544 + { 1545 + "flags": ["invalid"], 1546 + "hex": "7f21" 1547 + }, 1548 + { 1549 + "flags": ["invalid"], 1550 + "hex": "7f22" 1551 + }, 1552 + { 1553 + "flags": ["invalid"], 1554 + "hex": "7f23" 1555 + }, 1556 + { 1557 + "flags": ["invalid"], 1558 + "hex": "7f24" 1559 + }, 1560 + { 1561 + "flags": ["invalid"], 1562 + "hex": "7f25" 1563 + }, 1564 + { 1565 + "flags": ["invalid"], 1566 + "hex": "7f26" 1567 + }, 1568 + { 1569 + "flags": ["invalid"], 1570 + "hex": "7f27" 1571 + }, 1572 + { 1573 + "flags": ["invalid"], 1574 + "hex": "7f28" 1575 + }, 1576 + { 1577 + "flags": ["invalid"], 1578 + "hex": "7f29" 1579 + }, 1580 + { 1581 + "flags": ["invalid"], 1582 + "hex": "7f2a" 1583 + }, 1584 + { 1585 + "flags": ["invalid"], 1586 + "hex": "7f2b" 1587 + }, 1588 + { 1589 + "flags": ["invalid"], 1590 + "hex": "7f2c" 1591 + }, 1592 + { 1593 + "flags": ["invalid"], 1594 + "hex": "7f2d" 1595 + }, 1596 + { 1597 + "flags": ["invalid"], 1598 + "hex": "7f2e" 1599 + }, 1600 + { 1601 + "flags": ["invalid"], 1602 + "hex": "7f2f" 1603 + }, 1604 + { 1605 + "flags": ["invalid"], 1606 + "hex": "7f30" 1607 + }, 1608 + { 1609 + "flags": ["invalid"], 1610 + "hex": "7f31" 1611 + }, 1612 + { 1613 + "flags": ["invalid"], 1614 + "hex": "7f32" 1615 + }, 1616 + { 1617 + "flags": ["invalid"], 1618 + "hex": "7f33" 1619 + }, 1620 + { 1621 + "flags": ["invalid"], 1622 + "hex": "7f34" 1623 + }, 1624 + { 1625 + "flags": ["invalid"], 1626 + "hex": "7f35" 1627 + }, 1628 + { 1629 + "flags": ["invalid"], 1630 + "hex": "7f36" 1631 + }, 1632 + { 1633 + "flags": ["invalid"], 1634 + "hex": "7f37" 1635 + }, 1636 + { 1637 + "flags": ["invalid"], 1638 + "hex": "7f38" 1639 + }, 1640 + { 1641 + "flags": ["invalid"], 1642 + "hex": "7f39" 1643 + }, 1644 + { 1645 + "flags": ["invalid"], 1646 + "hex": "7f3a" 1647 + }, 1648 + { 1649 + "flags": ["invalid"], 1650 + "hex": "7f3b" 1651 + }, 1652 + { 1653 + "flags": ["invalid"], 1654 + "hex": "7f3c" 1655 + }, 1656 + { 1657 + "flags": ["invalid"], 1658 + "hex": "7f3d" 1659 + }, 1660 + { 1661 + "flags": ["invalid"], 1662 + "hex": "7f3e" 1663 + }, 1664 + { 1665 + "flags": ["invalid"], 1666 + "hex": "7f3f" 1667 + }, 1668 + { 1669 + "flags": ["invalid"], 1670 + "hex": "7f40" 1671 + }, 1672 + { 1673 + "flags": ["invalid"], 1674 + "hex": "7f41" 1675 + }, 1676 + { 1677 + "flags": ["invalid"], 1678 + "hex": "7f42" 1679 + }, 1680 + { 1681 + "flags": ["invalid"], 1682 + "hex": "7f43" 1683 + }, 1684 + { 1685 + "flags": ["invalid"], 1686 + "hex": "7f44" 1687 + }, 1688 + { 1689 + "flags": ["invalid"], 1690 + "hex": "7f45" 1691 + }, 1692 + { 1693 + "flags": ["invalid"], 1694 + "hex": "7f46" 1695 + }, 1696 + { 1697 + "flags": ["invalid"], 1698 + "hex": "7f47" 1699 + }, 1700 + { 1701 + "flags": ["invalid"], 1702 + "hex": "7f48" 1703 + }, 1704 + { 1705 + "flags": ["invalid"], 1706 + "hex": "7f49" 1707 + }, 1708 + { 1709 + "flags": ["invalid"], 1710 + "hex": "7f4a" 1711 + }, 1712 + { 1713 + "flags": ["invalid"], 1714 + "hex": "7f4b" 1715 + }, 1716 + { 1717 + "flags": ["invalid"], 1718 + "hex": "7f4c" 1719 + }, 1720 + { 1721 + "flags": ["invalid"], 1722 + "hex": "7f4d" 1723 + }, 1724 + { 1725 + "flags": ["invalid"], 1726 + "hex": "7f4e" 1727 + }, 1728 + { 1729 + "flags": ["invalid"], 1730 + "hex": "7f4f" 1731 + }, 1732 + { 1733 + "flags": ["invalid"], 1734 + "hex": "7f50" 1735 + }, 1736 + { 1737 + "flags": ["invalid"], 1738 + "hex": "7f51" 1739 + }, 1740 + { 1741 + "flags": ["invalid"], 1742 + "hex": "7f52" 1743 + }, 1744 + { 1745 + "flags": ["invalid"], 1746 + "hex": "7f53" 1747 + }, 1748 + { 1749 + "flags": ["invalid"], 1750 + "hex": "7f54" 1751 + }, 1752 + { 1753 + "flags": ["invalid"], 1754 + "hex": "7f55" 1755 + }, 1756 + { 1757 + "flags": ["invalid"], 1758 + "hex": "7f56" 1759 + }, 1760 + { 1761 + "flags": ["invalid"], 1762 + "hex": "7f57" 1763 + }, 1764 + { 1765 + "flags": ["invalid"], 1766 + "hex": "7f58" 1767 + }, 1768 + { 1769 + "flags": ["invalid"], 1770 + "hex": "7f59" 1771 + }, 1772 + { 1773 + "flags": ["invalid"], 1774 + "hex": "7f5a" 1775 + }, 1776 + { 1777 + "flags": ["invalid"], 1778 + "hex": "7f5b" 1779 + }, 1780 + { 1781 + "flags": ["invalid"], 1782 + "hex": "7f5c" 1783 + }, 1784 + { 1785 + "flags": ["invalid"], 1786 + "hex": "7f5d" 1787 + }, 1788 + { 1789 + "flags": ["invalid"], 1790 + "hex": "7f5e" 1791 + }, 1792 + { 1793 + "flags": ["invalid"], 1794 + "hex": "7f5f" 1795 + }, 1796 + { 1797 + "flags": ["invalid"], 1798 + "hex": "7f7c" 1799 + }, 1800 + { 1801 + "flags": ["invalid"], 1802 + "hex": "7f7d" 1803 + }, 1804 + { 1805 + "flags": ["invalid"], 1806 + "hex": "7f7e" 1807 + }, 1808 + { 1809 + "flags": ["invalid"], 1810 + "hex": "7f7f" 1811 + }, 1812 + { 1813 + "flags": ["invalid"], 1814 + "hex": "7f80" 1815 + }, 1816 + { 1817 + "flags": ["invalid"], 1818 + "hex": "7f81" 1819 + }, 1820 + { 1821 + "flags": ["invalid"], 1822 + "hex": "7f82" 1823 + }, 1824 + { 1825 + "flags": ["invalid"], 1826 + "hex": "7f83" 1827 + }, 1828 + { 1829 + "flags": ["invalid"], 1830 + "hex": "7f84" 1831 + }, 1832 + { 1833 + "flags": ["invalid"], 1834 + "hex": "7f85" 1835 + }, 1836 + { 1837 + "flags": ["invalid"], 1838 + "hex": "7f86" 1839 + }, 1840 + { 1841 + "flags": ["invalid"], 1842 + "hex": "7f87" 1843 + }, 1844 + { 1845 + "flags": ["invalid"], 1846 + "hex": "7f88" 1847 + }, 1848 + { 1849 + "flags": ["invalid"], 1850 + "hex": "7f89" 1851 + }, 1852 + { 1853 + "flags": ["invalid"], 1854 + "hex": "7f8a" 1855 + }, 1856 + { 1857 + "flags": ["invalid"], 1858 + "hex": "7f8b" 1859 + }, 1860 + { 1861 + "flags": ["invalid"], 1862 + "hex": "7f8c" 1863 + }, 1864 + { 1865 + "flags": ["invalid"], 1866 + "hex": "7f8d" 1867 + }, 1868 + { 1869 + "flags": ["invalid"], 1870 + "hex": "7f8e" 1871 + }, 1872 + { 1873 + "flags": ["invalid"], 1874 + "hex": "7f8f" 1875 + }, 1876 + { 1877 + "flags": ["invalid"], 1878 + "hex": "7f90" 1879 + }, 1880 + { 1881 + "flags": ["invalid"], 1882 + "hex": "7f91" 1883 + }, 1884 + { 1885 + "flags": ["invalid"], 1886 + "hex": "7f92" 1887 + }, 1888 + { 1889 + "flags": ["invalid"], 1890 + "hex": "7f93" 1891 + }, 1892 + { 1893 + "flags": ["invalid"], 1894 + "hex": "7f94" 1895 + }, 1896 + { 1897 + "flags": ["invalid"], 1898 + "hex": "7f95" 1899 + }, 1900 + { 1901 + "flags": ["invalid"], 1902 + "hex": "7f96" 1903 + }, 1904 + { 1905 + "flags": ["invalid"], 1906 + "hex": "7f97" 1907 + }, 1908 + { 1909 + "flags": ["invalid"], 1910 + "hex": "7f98" 1911 + }, 1912 + { 1913 + "flags": ["invalid"], 1914 + "hex": "7f99" 1915 + }, 1916 + { 1917 + "flags": ["invalid"], 1918 + "hex": "7f9a" 1919 + }, 1920 + { 1921 + "flags": ["invalid"], 1922 + "hex": "7f9b" 1923 + }, 1924 + { 1925 + "flags": ["invalid"], 1926 + "hex": "7f9c" 1927 + }, 1928 + { 1929 + "flags": ["invalid"], 1930 + "hex": "7f9d" 1931 + }, 1932 + { 1933 + "flags": ["invalid"], 1934 + "hex": "7f9e" 1935 + }, 1936 + { 1937 + "flags": ["invalid"], 1938 + "hex": "7f9f" 1939 + }, 1940 + { 1941 + "flags": ["invalid"], 1942 + "hex": "7fa0" 1943 + }, 1944 + { 1945 + "flags": ["invalid"], 1946 + "hex": "7fa1" 1947 + }, 1948 + { 1949 + "flags": ["invalid"], 1950 + "hex": "7fa2" 1951 + }, 1952 + { 1953 + "flags": ["invalid"], 1954 + "hex": "7fa3" 1955 + }, 1956 + { 1957 + "flags": ["invalid"], 1958 + "hex": "7fa4" 1959 + }, 1960 + { 1961 + "flags": ["invalid"], 1962 + "hex": "7fa5" 1963 + }, 1964 + { 1965 + "flags": ["invalid"], 1966 + "hex": "7fa6" 1967 + }, 1968 + { 1969 + "flags": ["invalid"], 1970 + "hex": "7fa7" 1971 + }, 1972 + { 1973 + "flags": ["invalid"], 1974 + "hex": "7fa8" 1975 + }, 1976 + { 1977 + "flags": ["invalid"], 1978 + "hex": "7fa9" 1979 + }, 1980 + { 1981 + "flags": ["invalid"], 1982 + "hex": "7faa" 1983 + }, 1984 + { 1985 + "flags": ["invalid"], 1986 + "hex": "7fab" 1987 + }, 1988 + { 1989 + "flags": ["invalid"], 1990 + "hex": "7fac" 1991 + }, 1992 + { 1993 + "flags": ["invalid"], 1994 + "hex": "7fad" 1995 + }, 1996 + { 1997 + "flags": ["invalid"], 1998 + "hex": "7fae" 1999 + }, 2000 + { 2001 + "flags": ["invalid"], 2002 + "hex": "7faf" 2003 + }, 2004 + { 2005 + "flags": ["invalid"], 2006 + "hex": "7fb0" 2007 + }, 2008 + { 2009 + "flags": ["invalid"], 2010 + "hex": "7fb1" 2011 + }, 2012 + { 2013 + "flags": ["invalid"], 2014 + "hex": "7fb2" 2015 + }, 2016 + { 2017 + "flags": ["invalid"], 2018 + "hex": "7fb3" 2019 + }, 2020 + { 2021 + "flags": ["invalid"], 2022 + "hex": "7fb4" 2023 + }, 2024 + { 2025 + "flags": ["invalid"], 2026 + "hex": "7fb5" 2027 + }, 2028 + { 2029 + "flags": ["invalid"], 2030 + "hex": "7fb6" 2031 + }, 2032 + { 2033 + "flags": ["invalid"], 2034 + "hex": "7fb7" 2035 + }, 2036 + { 2037 + "flags": ["invalid"], 2038 + "hex": "7fb8" 2039 + }, 2040 + { 2041 + "flags": ["invalid"], 2042 + "hex": "7fb9" 2043 + }, 2044 + { 2045 + "flags": ["invalid"], 2046 + "hex": "7fba" 2047 + }, 2048 + { 2049 + "flags": ["invalid"], 2050 + "hex": "7fbb" 2051 + }, 2052 + { 2053 + "flags": ["invalid"], 2054 + "hex": "7fbc" 2055 + }, 2056 + { 2057 + "flags": ["invalid"], 2058 + "hex": "7fbd" 2059 + }, 2060 + { 2061 + "flags": ["invalid"], 2062 + "hex": "7fbe" 2063 + }, 2064 + { 2065 + "flags": ["invalid"], 2066 + "hex": "7fbf" 2067 + }, 2068 + { 2069 + "flags": ["invalid"], 2070 + "hex": "7fc0" 2071 + }, 2072 + { 2073 + "flags": ["invalid"], 2074 + "hex": "7fc1" 2075 + }, 2076 + { 2077 + "flags": ["invalid"], 2078 + "hex": "7fc2" 2079 + }, 2080 + { 2081 + "flags": ["invalid"], 2082 + "hex": "7fc3" 2083 + }, 2084 + { 2085 + "flags": ["invalid"], 2086 + "hex": "7fc4" 2087 + }, 2088 + { 2089 + "flags": ["invalid"], 2090 + "hex": "7fc5" 2091 + }, 2092 + { 2093 + "flags": ["invalid"], 2094 + "hex": "7fc6" 2095 + }, 2096 + { 2097 + "flags": ["invalid"], 2098 + "hex": "7fc7" 2099 + }, 2100 + { 2101 + "flags": ["invalid"], 2102 + "hex": "7fc8" 2103 + }, 2104 + { 2105 + "flags": ["invalid"], 2106 + "hex": "7fc9" 2107 + }, 2108 + { 2109 + "flags": ["invalid"], 2110 + "hex": "7fca" 2111 + }, 2112 + { 2113 + "flags": ["invalid"], 2114 + "hex": "7fcb" 2115 + }, 2116 + { 2117 + "flags": ["invalid"], 2118 + "hex": "7fcc" 2119 + }, 2120 + { 2121 + "flags": ["invalid"], 2122 + "hex": "7fcd" 2123 + }, 2124 + { 2125 + "flags": ["invalid"], 2126 + "hex": "7fce" 2127 + }, 2128 + { 2129 + "flags": ["invalid"], 2130 + "hex": "7fcf" 2131 + }, 2132 + { 2133 + "flags": ["invalid"], 2134 + "hex": "7fd0" 2135 + }, 2136 + { 2137 + "flags": ["invalid"], 2138 + "hex": "7fd1" 2139 + }, 2140 + { 2141 + "flags": ["invalid"], 2142 + "hex": "7fd2" 2143 + }, 2144 + { 2145 + "flags": ["invalid"], 2146 + "hex": "7fd3" 2147 + }, 2148 + { 2149 + "flags": ["invalid"], 2150 + "hex": "7fd4" 2151 + }, 2152 + { 2153 + "flags": ["invalid"], 2154 + "hex": "7fd5" 2155 + }, 2156 + { 2157 + "flags": ["invalid"], 2158 + "hex": "7fd6" 2159 + }, 2160 + { 2161 + "flags": ["invalid"], 2162 + "hex": "7fd7" 2163 + }, 2164 + { 2165 + "flags": ["invalid"], 2166 + "hex": "7fd8" 2167 + }, 2168 + { 2169 + "flags": ["invalid"], 2170 + "hex": "7fd9" 2171 + }, 2172 + { 2173 + "flags": ["invalid"], 2174 + "hex": "7fda" 2175 + }, 2176 + { 2177 + "flags": ["invalid"], 2178 + "hex": "7fdb" 2179 + }, 2180 + { 2181 + "flags": ["invalid"], 2182 + "hex": "7fdc" 2183 + }, 2184 + { 2185 + "flags": ["invalid"], 2186 + "hex": "7fdd" 2187 + }, 2188 + { 2189 + "flags": ["invalid"], 2190 + "hex": "7fde" 2191 + }, 2192 + { 2193 + "flags": ["invalid"], 2194 + "hex": "7fdf" 2195 + }, 2196 + { 2197 + "flags": ["invalid"], 2198 + "hex": "7fe0" 2199 + }, 2200 + { 2201 + "flags": ["invalid"], 2202 + "hex": "7fe1" 2203 + }, 2204 + { 2205 + "flags": ["invalid"], 2206 + "hex": "7fe2" 2207 + }, 2208 + { 2209 + "flags": ["invalid"], 2210 + "hex": "7fe3" 2211 + }, 2212 + { 2213 + "flags": ["invalid"], 2214 + "hex": "7fe4" 2215 + }, 2216 + { 2217 + "flags": ["invalid"], 2218 + "hex": "7fe5" 2219 + }, 2220 + { 2221 + "flags": ["invalid"], 2222 + "hex": "7fe6" 2223 + }, 2224 + { 2225 + "flags": ["invalid"], 2226 + "hex": "7fe7" 2227 + }, 2228 + { 2229 + "flags": ["invalid"], 2230 + "hex": "7fe8" 2231 + }, 2232 + { 2233 + "flags": ["invalid"], 2234 + "hex": "7fe9" 2235 + }, 2236 + { 2237 + "flags": ["invalid"], 2238 + "hex": "7fea" 2239 + }, 2240 + { 2241 + "flags": ["invalid"], 2242 + "hex": "7feb" 2243 + }, 2244 + { 2245 + "flags": ["invalid"], 2246 + "hex": "7fec" 2247 + }, 2248 + { 2249 + "flags": ["invalid"], 2250 + "hex": "7fed" 2251 + }, 2252 + { 2253 + "flags": ["invalid"], 2254 + "hex": "7fee" 2255 + }, 2256 + { 2257 + "flags": ["invalid"], 2258 + "hex": "7fef" 2259 + }, 2260 + { 2261 + "flags": ["invalid"], 2262 + "hex": "7ff0" 2263 + }, 2264 + { 2265 + "flags": ["invalid"], 2266 + "hex": "7ff1" 2267 + }, 2268 + { 2269 + "flags": ["invalid"], 2270 + "hex": "7ff2" 2271 + }, 2272 + { 2273 + "flags": ["invalid"], 2274 + "hex": "7ff3" 2275 + }, 2276 + { 2277 + "flags": ["invalid"], 2278 + "hex": "7ff4" 2279 + }, 2280 + { 2281 + "flags": ["invalid"], 2282 + "hex": "7ff5" 2283 + }, 2284 + { 2285 + "flags": ["invalid"], 2286 + "hex": "7ff6" 2287 + }, 2288 + { 2289 + "flags": ["invalid"], 2290 + "hex": "7ff7" 2291 + }, 2292 + { 2293 + "flags": ["invalid"], 2294 + "hex": "7ff8" 2295 + }, 2296 + { 2297 + "flags": ["invalid"], 2298 + "hex": "7ff9" 2299 + }, 2300 + { 2301 + "flags": ["invalid"], 2302 + "hex": "7ffa" 2303 + }, 2304 + { 2305 + "flags": ["invalid"], 2306 + "hex": "7ffb" 2307 + }, 2308 + { 2309 + "flags": ["invalid"], 2310 + "hex": "7ffc" 2311 + }, 2312 + { 2313 + "flags": ["invalid"], 2314 + "hex": "7ffd" 2315 + }, 2316 + { 2317 + "flags": ["invalid"], 2318 + "hex": "7ffe" 2319 + }, 2320 + { 2321 + "flags": ["invalid"], 2322 + "hex": "9c" 2323 + }, 2324 + { 2325 + "flags": ["invalid"], 2326 + "hex": "9d" 2327 + }, 2328 + { 2329 + "flags": ["invalid"], 2330 + "hex": "9e" 2331 + }, 2332 + { 2333 + "flags": ["invalid"], 2334 + "hex": "9f1c" 2335 + }, 2336 + { 2337 + "flags": ["invalid"], 2338 + "hex": "9f1d" 2339 + }, 2340 + { 2341 + "flags": ["invalid"], 2342 + "hex": "9f1e" 2343 + }, 2344 + { 2345 + "flags": ["invalid"], 2346 + "hex": "9f1f" 2347 + }, 2348 + { 2349 + "flags": ["invalid"], 2350 + "hex": "9f3c" 2351 + }, 2352 + { 2353 + "flags": ["invalid"], 2354 + "hex": "9f3d" 2355 + }, 2356 + { 2357 + "flags": ["invalid"], 2358 + "hex": "9f3e" 2359 + }, 2360 + { 2361 + "flags": ["invalid"], 2362 + "hex": "9f3f" 2363 + }, 2364 + { 2365 + "flags": ["invalid"], 2366 + "hex": "9f5c" 2367 + }, 2368 + { 2369 + "flags": ["invalid"], 2370 + "hex": "9f5d" 2371 + }, 2372 + { 2373 + "flags": ["invalid"], 2374 + "hex": "9f5e" 2375 + }, 2376 + { 2377 + "flags": ["invalid"], 2378 + "hex": "9f7c" 2379 + }, 2380 + { 2381 + "flags": ["invalid"], 2382 + "hex": "9f7d" 2383 + }, 2384 + { 2385 + "flags": ["invalid"], 2386 + "hex": "9f7e" 2387 + }, 2388 + { 2389 + "flags": ["invalid"], 2390 + "hex": "9f9c" 2391 + }, 2392 + { 2393 + "flags": ["invalid"], 2394 + "hex": "9f9d" 2395 + }, 2396 + { 2397 + "flags": ["invalid"], 2398 + "hex": "9f9e" 2399 + }, 2400 + { 2401 + "flags": ["invalid"], 2402 + "hex": "9fbc" 2403 + }, 2404 + { 2405 + "flags": ["invalid"], 2406 + "hex": "9fbd" 2407 + }, 2408 + { 2409 + "flags": ["invalid"], 2410 + "hex": "9fbe" 2411 + }, 2412 + { 2413 + "flags": ["invalid"], 2414 + "hex": "9fdc" 2415 + }, 2416 + { 2417 + "flags": ["invalid"], 2418 + "hex": "9fdd" 2419 + }, 2420 + { 2421 + "flags": ["invalid"], 2422 + "hex": "9fde" 2423 + }, 2424 + { 2425 + "flags": ["invalid"], 2426 + "hex": "9fdf" 2427 + }, 2428 + { 2429 + "flags": ["invalid"], 2430 + "hex": "9ffc" 2431 + }, 2432 + { 2433 + "flags": ["invalid"], 2434 + "hex": "9ffd" 2435 + }, 2436 + { 2437 + "flags": ["invalid"], 2438 + "hex": "9ffe" 2439 + }, 2440 + { 2441 + "flags": ["invalid"], 2442 + "hex": "bc" 2443 + }, 2444 + { 2445 + "flags": ["invalid"], 2446 + "hex": "bd" 2447 + }, 2448 + { 2449 + "flags": ["invalid"], 2450 + "hex": "be" 2451 + }, 2452 + { 2453 + "flags": ["invalid"], 2454 + "hex": "bf1c" 2455 + }, 2456 + { 2457 + "flags": ["invalid"], 2458 + "hex": "bf1d" 2459 + }, 2460 + { 2461 + "flags": ["invalid"], 2462 + "hex": "bf1e" 2463 + }, 2464 + { 2465 + "flags": ["invalid"], 2466 + "hex": "bf1f" 2467 + }, 2468 + { 2469 + "flags": ["invalid"], 2470 + "hex": "bf3c" 2471 + }, 2472 + { 2473 + "flags": ["invalid"], 2474 + "hex": "bf3d" 2475 + }, 2476 + { 2477 + "flags": ["invalid"], 2478 + "hex": "bf3e" 2479 + }, 2480 + { 2481 + "flags": ["invalid"], 2482 + "hex": "bf3f" 2483 + }, 2484 + { 2485 + "flags": ["invalid"], 2486 + "hex": "bf5c" 2487 + }, 2488 + { 2489 + "flags": ["invalid"], 2490 + "hex": "bf5d" 2491 + }, 2492 + { 2493 + "flags": ["invalid"], 2494 + "hex": "bf5e" 2495 + }, 2496 + { 2497 + "flags": ["invalid"], 2498 + "hex": "bf7c" 2499 + }, 2500 + { 2501 + "flags": ["invalid"], 2502 + "hex": "bf7d" 2503 + }, 2504 + { 2505 + "flags": ["invalid"], 2506 + "hex": "bf7e" 2507 + }, 2508 + { 2509 + "flags": ["invalid"], 2510 + "hex": "bf9c" 2511 + }, 2512 + { 2513 + "flags": ["invalid"], 2514 + "hex": "bf9d" 2515 + }, 2516 + { 2517 + "flags": ["invalid"], 2518 + "hex": "bf9e" 2519 + }, 2520 + { 2521 + "flags": ["invalid"], 2522 + "hex": "bfbc" 2523 + }, 2524 + { 2525 + "flags": ["invalid"], 2526 + "hex": "bfbd" 2527 + }, 2528 + { 2529 + "flags": ["invalid"], 2530 + "hex": "bfbe" 2531 + }, 2532 + { 2533 + "flags": ["invalid"], 2534 + "hex": "bfdc" 2535 + }, 2536 + { 2537 + "flags": ["invalid"], 2538 + "hex": "bfdd" 2539 + }, 2540 + { 2541 + "flags": ["invalid"], 2542 + "hex": "bfde" 2543 + }, 2544 + { 2545 + "flags": ["invalid"], 2546 + "hex": "bfdf" 2547 + }, 2548 + { 2549 + "flags": ["invalid"], 2550 + "hex": "bffc" 2551 + }, 2552 + { 2553 + "flags": ["invalid"], 2554 + "hex": "bffd" 2555 + }, 2556 + { 2557 + "flags": ["invalid"], 2558 + "hex": "bffe" 2559 + }, 2560 + { 2561 + "flags": ["invalid"], 2562 + "hex": "bf00" 2563 + }, 2564 + { 2565 + "flags": ["invalid"], 2566 + "hex": "dc" 2567 + }, 2568 + { 2569 + "flags": ["invalid"], 2570 + "hex": "dd" 2571 + }, 2572 + { 2573 + "flags": ["invalid"], 2574 + "hex": "de" 2575 + }, 2576 + { 2577 + "flags": ["invalid"], 2578 + "hex": "df" 2579 + }, 2580 + { 2581 + "flags": ["invalid"], 2582 + "hex": "f800" 2583 + }, 2584 + { 2585 + "flags": ["invalid"], 2586 + "hex": "f801" 2587 + }, 2588 + { 2589 + "flags": ["invalid"], 2590 + "hex": "f802" 2591 + }, 2592 + { 2593 + "flags": ["invalid"], 2594 + "hex": "f803" 2595 + }, 2596 + { 2597 + "flags": ["invalid"], 2598 + "hex": "f804" 2599 + }, 2600 + { 2601 + "flags": ["invalid"], 2602 + "hex": "f805" 2603 + }, 2604 + { 2605 + "flags": ["invalid"], 2606 + "hex": "f806" 2607 + }, 2608 + { 2609 + "flags": ["invalid"], 2610 + "hex": "f807" 2611 + }, 2612 + { 2613 + "flags": ["invalid"], 2614 + "hex": "f808" 2615 + }, 2616 + { 2617 + "flags": ["invalid"], 2618 + "hex": "f809" 2619 + }, 2620 + { 2621 + "flags": ["invalid"], 2622 + "hex": "f80a" 2623 + }, 2624 + { 2625 + "flags": ["invalid"], 2626 + "hex": "f80b" 2627 + }, 2628 + { 2629 + "flags": ["invalid"], 2630 + "hex": "f80c" 2631 + }, 2632 + { 2633 + "flags": ["invalid"], 2634 + "hex": "f80d" 2635 + }, 2636 + { 2637 + "flags": ["invalid"], 2638 + "hex": "f80e" 2639 + }, 2640 + { 2641 + "flags": ["invalid"], 2642 + "hex": "f80f" 2643 + }, 2644 + { 2645 + "flags": ["invalid"], 2646 + "hex": "f810" 2647 + }, 2648 + { 2649 + "flags": ["invalid"], 2650 + "hex": "f811" 2651 + }, 2652 + { 2653 + "flags": ["invalid"], 2654 + "hex": "f812" 2655 + }, 2656 + { 2657 + "flags": ["invalid"], 2658 + "hex": "f813" 2659 + }, 2660 + { 2661 + "flags": ["invalid"], 2662 + "hex": "f814" 2663 + }, 2664 + { 2665 + "flags": ["invalid"], 2666 + "hex": "f815" 2667 + }, 2668 + { 2669 + "flags": ["invalid"], 2670 + "hex": "f816" 2671 + }, 2672 + { 2673 + "flags": ["invalid"], 2674 + "hex": "f817" 2675 + }, 2676 + { 2677 + "flags": ["invalid"], 2678 + "hex": "f818" 2679 + }, 2680 + { 2681 + "flags": ["invalid"], 2682 + "hex": "f819" 2683 + }, 2684 + { 2685 + "flags": ["invalid"], 2686 + "hex": "f81a" 2687 + }, 2688 + { 2689 + "flags": ["invalid"], 2690 + "hex": "f81b" 2691 + }, 2692 + { 2693 + "flags": ["invalid"], 2694 + "hex": "f81c" 2695 + }, 2696 + { 2697 + "flags": ["invalid"], 2698 + "hex": "f81d" 2699 + }, 2700 + { 2701 + "flags": ["invalid"], 2702 + "hex": "f81e" 2703 + }, 2704 + { 2705 + "flags": ["invalid"], 2706 + "hex": "f81f" 2707 + }, 2708 + { 2709 + "flags": ["invalid"], 2710 + "hex": "fc" 2711 + }, 2712 + { 2713 + "flags": ["invalid"], 2714 + "hex": "fd" 2715 + }, 2716 + { 2717 + "flags": ["invalid"], 2718 + "hex": "fe" 2719 + }, 2720 + { 2721 + "flags": ["invalid"], 2722 + "hex": "ff" 2723 + }, 2724 + { 2725 + "flags": ["invalid"], 2726 + "hex": "5f4100" 2727 + }, 2728 + { 2729 + "flags": ["invalid"], 2730 + "hex": "7f6100" 2731 + }, 2732 + { 2733 + "flags": ["invalid"], 2734 + "hex": "5f6100ff" 2735 + }, 2736 + { 2737 + "flags": ["invalid"], 2738 + "hex": "7f4100ff" 2739 + }, 2740 + { 2741 + "flags": ["invalid"], 2742 + "hex": "5f00ff" 2743 + }, 2744 + { 2745 + "flags": ["invalid"], 2746 + "hex": "5f21ff" 2747 + }, 2748 + { 2749 + "flags": ["invalid"], 2750 + "hex": "5f80ff" 2751 + }, 2752 + { 2753 + "flags": ["invalid"], 2754 + "hex": "5fa0ff" 2755 + }, 2756 + { 2757 + "flags": ["invalid"], 2758 + "hex": "5fc000ff" 2759 + }, 2760 + { 2761 + "flags": ["invalid"], 2762 + "hex": "5fe0ff" 2763 + }, 2764 + { 2765 + "flags": ["invalid"], 2766 + "hex": "5f5f4100ffff" 2767 + }, 2768 + { 2769 + "flags": ["invalid"], 2770 + "hex": "7f7f6100ffff" 2771 + }, 2772 + { 2773 + "flags": ["invalid"], 2774 + "hex": "81" 2775 + }, 2776 + { 2777 + "flags": ["invalid"], 2778 + "hex": "8200" 2779 + }, 2780 + { 2781 + "flags": ["invalid"], 2782 + "hex": "9a01ff00" 2783 + }, 2784 + { 2785 + "flags": ["invalid"], 2786 + "hex": "a1" 2787 + }, 2788 + { 2789 + "flags": ["invalid"], 2790 + "hex": "a20102" 2791 + }, 2792 + { 2793 + "flags": ["invalid"], 2794 + "hex": "9f" 2795 + }, 2796 + { 2797 + "flags": ["invalid"], 2798 + "hex": "9f0102" 2799 + }, 2800 + { 2801 + "flags": ["invalid"], 2802 + "hex": "bf" 2803 + }, 2804 + { 2805 + "flags": ["invalid"], 2806 + "hex": "bf01020102" 2807 + }, 2808 + { 2809 + "flags": ["invalid"], 2810 + "hex": "9f8000" 2811 + }, 2812 + { 2813 + "flags": ["invalid"], 2814 + "hex": "819f" 2815 + }, 2816 + { 2817 + "flags": ["invalid"], 2818 + "hex": "818181818181818181" 2819 + }, 2820 + { 2821 + "flags": ["invalid"], 2822 + "hex": "9f9f9f9f9fffffffff" 2823 + }, 2824 + { 2825 + "flags": ["invalid"], 2826 + "hex": "9f819f819f9fffffff" 2827 + }, 2828 + { 2829 + "flags": ["invalid"], 2830 + "hex": "9f829f819f9fffffffff" 2831 + }, 2832 + { 2833 + "flags": ["invalid"], 2834 + "hex": "18" 2835 + }, 2836 + { 2837 + "flags": ["invalid"], 2838 + "hex": "19" 2839 + }, 2840 + { 2841 + "flags": ["invalid"], 2842 + "hex": "1a" 2843 + }, 2844 + { 2845 + "flags": ["invalid"], 2846 + "hex": "1b" 2847 + }, 2848 + { 2849 + "flags": ["invalid"], 2850 + "hex": "1901" 2851 + }, 2852 + { 2853 + "flags": ["invalid"], 2854 + "hex": "1a0102" 2855 + }, 2856 + { 2857 + "flags": ["invalid"], 2858 + "hex": "1b01020304050607" 2859 + }, 2860 + { 2861 + "flags": ["invalid"], 2862 + "hex": "38" 2863 + }, 2864 + { 2865 + "flags": ["invalid"], 2866 + "hex": "58" 2867 + }, 2868 + { 2869 + "flags": ["invalid"], 2870 + "hex": "78" 2871 + }, 2872 + { 2873 + "flags": ["invalid"], 2874 + "hex": "98" 2875 + }, 2876 + { 2877 + "flags": ["invalid"], 2878 + "hex": "b8" 2879 + }, 2880 + { 2881 + "flags": ["invalid"], 2882 + "hex": "d8" 2883 + }, 2884 + { 2885 + "flags": ["invalid"], 2886 + "hex": "f8" 2887 + }, 2888 + { 2889 + "flags": ["invalid"], 2890 + "hex": "81ff" 2891 + }, 2892 + { 2893 + "flags": ["invalid"], 2894 + "hex": "8200ff" 2895 + }, 2896 + { 2897 + "flags": ["invalid"], 2898 + "hex": "a1ff" 2899 + }, 2900 + { 2901 + "flags": ["invalid"], 2902 + "hex": "a1ff00" 2903 + }, 2904 + { 2905 + "flags": ["invalid"], 2906 + "hex": "a100ff" 2907 + }, 2908 + { 2909 + "flags": ["invalid"], 2910 + "hex": "a20000ff" 2911 + }, 2912 + { 2913 + "flags": ["invalid"], 2914 + "hex": "ff" 2915 + }, 2916 + { 2917 + "flags": ["invalid"], 2918 + "hex": "80ff" 2919 + }, 2920 + { 2921 + "flags": ["invalid"], 2922 + "hex": "9fffff" 2923 + }, 2924 + { 2925 + "flags": ["invalid"], 2926 + "hex": "f800" 2927 + }, 2928 + { 2929 + "flags": ["invalid"], 2930 + "hex": "f801" 2931 + }, 2932 + { 2933 + "flags": ["invalid"], 2934 + "hex": "f802" 2935 + }, 2936 + { 2937 + "flags": ["invalid"], 2938 + "hex": "f803" 2939 + }, 2940 + { 2941 + "flags": ["invalid"], 2942 + "hex": "f804" 2943 + }, 2944 + { 2945 + "flags": ["invalid"], 2946 + "hex": "f805" 2947 + }, 2948 + { 2949 + "flags": ["invalid"], 2950 + "hex": "f806" 2951 + }, 2952 + { 2953 + "flags": ["invalid"], 2954 + "hex": "f807" 2955 + }, 2956 + { 2957 + "flags": ["invalid"], 2958 + "hex": "f808" 2959 + }, 2960 + { 2961 + "flags": ["invalid"], 2962 + "hex": "f809" 2963 + }, 2964 + { 2965 + "flags": ["invalid"], 2966 + "hex": "f80a" 2967 + }, 2968 + { 2969 + "flags": ["invalid"], 2970 + "hex": "f80b" 2971 + }, 2972 + { 2973 + "flags": ["invalid"], 2974 + "hex": "f80c" 2975 + }, 2976 + { 2977 + "flags": ["invalid"], 2978 + "hex": "f80d" 2979 + }, 2980 + { 2981 + "flags": ["invalid"], 2982 + "hex": "f80e" 2983 + }, 2984 + { 2985 + "flags": ["invalid"], 2986 + "hex": "f80f" 2987 + }, 2988 + { 2989 + "flags": ["invalid"], 2990 + "hex": "f810" 2991 + }, 2992 + { 2993 + "flags": ["invalid"], 2994 + "hex": "f811" 2995 + }, 2996 + { 2997 + "flags": ["invalid"], 2998 + "hex": "f812" 2999 + }, 3000 + { 3001 + "flags": ["invalid"], 3002 + "hex": "f813" 3003 + }, 3004 + { 3005 + "flags": ["invalid"], 3006 + "hex": "f814" 3007 + }, 3008 + { 3009 + "flags": ["invalid"], 3010 + "hex": "f815" 3011 + }, 3012 + { 3013 + "flags": ["invalid"], 3014 + "hex": "f816" 3015 + }, 3016 + { 3017 + "flags": ["invalid"], 3018 + "hex": "f817" 3019 + }, 3020 + { 3021 + "flags": ["invalid"], 3022 + "hex": "f818" 3023 + }, 3024 + { 3025 + "flags": ["invalid"], 3026 + "hex": "1f" 3027 + }, 3028 + { 3029 + "flags": ["invalid"], 3030 + "hex": "3f" 3031 + }, 3032 + { 3033 + "flags": ["invalid"], 3034 + "hex": "df00" 3035 + }, 3036 + { 3037 + "flags": ["invalid"], 3038 + "hex": "df" 3039 + }, 3040 + { 3041 + "flags": ["invalid"], 3042 + "hex": "41" 3043 + }, 3044 + { 3045 + "flags": ["invalid"], 3046 + "hex": "61" 3047 + }, 3048 + { 3049 + "flags": ["invalid"], 3050 + "hex": "5affffffff00" 3051 + }, 3052 + { 3053 + "flags": ["invalid"], 3054 + "hex": "7affffffff00" 3055 + }, 3056 + { 3057 + "flags": ["invalid"], 3058 + "hex": "1c" 3059 + }, 3060 + { 3061 + "flags": ["invalid"], 3062 + "hex": "1d" 3063 + }, 3064 + { 3065 + "flags": ["invalid"], 3066 + "hex": "1e" 3067 + }, 3068 + { 3069 + "flags": ["invalid"], 3070 + "hex": "3c" 3071 + }, 3072 + { 3073 + "flags": ["invalid"], 3074 + "hex": "3d" 3075 + }, 3076 + { 3077 + "flags": ["invalid"], 3078 + "hex": "3e" 3079 + }, 3080 + { 3081 + "flags": ["invalid"], 3082 + "hex": "5c" 3083 + }, 3084 + { 3085 + "flags": ["invalid"], 3086 + "hex": "5d" 3087 + }, 3088 + { 3089 + "flags": ["invalid"], 3090 + "hex": "5e" 3091 + }, 3092 + { 3093 + "flags": ["invalid"], 3094 + "hex": "7c" 3095 + }, 3096 + { 3097 + "flags": ["invalid"], 3098 + "hex": "7d" 3099 + }, 3100 + { 3101 + "flags": ["invalid"], 3102 + "hex": "7e" 3103 + }, 3104 + { 3105 + "flags": ["invalid"], 3106 + "hex": "9c" 3107 + }, 3108 + { 3109 + "flags": ["invalid"], 3110 + "hex": "9d" 3111 + }, 3112 + { 3113 + "flags": ["invalid"], 3114 + "hex": "9e" 3115 + }, 3116 + { 3117 + "flags": ["invalid"], 3118 + "hex": "bc" 3119 + }, 3120 + { 3121 + "flags": ["invalid"], 3122 + "hex": "bd" 3123 + }, 3124 + { 3125 + "flags": ["invalid"], 3126 + "hex": "be" 3127 + }, 3128 + { 3129 + "flags": ["invalid"], 3130 + "hex": "dc" 3131 + }, 3132 + { 3133 + "flags": ["invalid"], 3134 + "hex": "dd" 3135 + }, 3136 + { 3137 + "flags": ["invalid"], 3138 + "hex": "de" 3139 + }, 3140 + { 3141 + "flags": ["invalid"], 3142 + "hex": "fc" 3143 + }, 3144 + { 3145 + "flags": ["invalid"], 3146 + "hex": "fd" 3147 + }, 3148 + { 3149 + "flags": ["invalid"], 3150 + "hex": "fe" 3151 + }, 3152 + { 3153 + "flags": ["invalid"], 3154 + "hex": "a100" 3155 + }, 3156 + { 3157 + "flags": ["invalid"], 3158 + "hex": "a2000000" 3159 + }, 3160 + { 3161 + "flags": ["invalid"], 3162 + "hex": "bf00ff" 3163 + }, 3164 + { 3165 + "flags": ["invalid"], 3166 + "hex": "bf000000ff" 3167 + }, 3168 + { 3169 + "flags": ["invalid"], 3170 + "hex": "f900" 3171 + }, 3172 + { 3173 + "flags": ["invalid"], 3174 + "hex": "fa0000" 3175 + }, 3176 + { 3177 + "flags": ["invalid"], 3178 + "hex": "fb000000" 3179 + }, 3180 + { 3181 + "flags": ["invalid"], 3182 + "hex": "5bffffffffffffffff010203" 3183 + }, 3184 + { 3185 + "flags": ["invalid"], 3186 + "hex": "7b7fffffffffffffff010203" 3187 + }, 3188 + { 3189 + "flags": ["invalid"], 3190 + "hex": "c0" 3191 + }, 3192 + { 3193 + "flags": ["invalid"], 3194 + "hex": "9f81ff" 3195 + }, 3196 + { 3197 + "flags": ["invalid"], 3198 + "hex": "9bFFFFFFFFFFFFFFFF00000000" 3199 + }, 3200 + { 3201 + "flags": ["invalid"], 3202 + "hex": "9b0FFFFFFFFFFFFFFF00000000" 3203 + }, 3204 + { 3205 + "flags": ["invalid"], 3206 + "hex": "bbFFFFFFFFFFFFFFFF00000000" 3207 + }, 3208 + { 3209 + "flags": ["invalid"], 3210 + "hex": "bb0FFFFFFFFFFFFFFF00000000" 3211 + }, 3212 + { 3213 + "flags": ["invalid"], 3214 + "hex": "6bFFFFFFFFFFFFFFFF00000000" 3215 + }, 3216 + { 3217 + "flags": ["invalid"], 3218 + "hex": "6b0FFFFFFFFFFFFFFF00000000" 3219 + } 3220 + ]
+6
src/root.zig
··· 73 73 if (@import("builtin").is_test) { 74 74 _ = @import("internal/testing/interop_tests.zig"); 75 75 _ = @import("internal/repo/repo_verifier.zig"); 76 + _ = @import("internal/repo/cbor_test.zig"); 77 + _ = @import("internal/repo/cbor_read_test.zig"); 78 + _ = @import("internal/repo/cbor_write_test.zig"); 79 + _ = @import("internal/repo/car_test.zig"); 80 + _ = @import("internal/repo/cbor_rfc8949_test.zig"); 81 + _ = @import("internal/repo/mst_test.zig"); 76 82 } 77 83 }