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.

fix CAR v1 validation gaps, fix readUvarint overflow, add 17 tests

CAR reader now validates version==1, requires non-empty roots, rejects
zero-length blocks, and enforces a 1 MiB per-block size limit (matching
atmos). Fix readUvarint where the overflow check was dead code — shift
was u6 (max 63) so the >= 64 check never triggered; replaced with a
bounded for(0..10) loop. Add 17 CAR tests covering header validation,
truncation, corruption, round-trip determinism, and size limits. Add 5
CAR benchmarks (read/write/round-trip with and without hash verification).

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

jcalabro 968d03a4 71c34577

+441 -61
+27 -57
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. ··· 75 77 const header_bytes = data[pos..header_end]; 76 78 const header = cbor.decodeAll(allocator, header_bytes) catch return error.InvalidHeader; 77 79 78 - // extract roots (array of CID links) 80 + // validate version == 1 81 + const version = header.getUint("version") orelse return error.InvalidHeader; 82 + if (version != 1) return error.InvalidHeader; 83 + 84 + // extract roots (array of CID links) — CAR v1 requires at least one root 79 85 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 - } 86 + const root_values = header.getArray("roots") orelse return error.InvalidHeader; 87 + for (root_values) |root_val| { 88 + switch (root_val) { 89 + .cid => |c| try roots.append(allocator, c), 90 + else => {}, 86 91 } 87 92 } 93 + if (roots.items.len == 0) return error.InvalidHeader; 88 94 89 95 pos = header_end; 90 96 ··· 97 103 // total_len includes both CID and data 98 104 const block_len = cbor.readUvarint(data, &pos) orelse return error.InvalidVarint; 99 105 const block_len_usize = std.math.cast(usize, block_len) orelse return error.InvalidHeader; 106 + if (block_len_usize == 0) return error.InvalidCid; // zero-length block has no CID 107 + if (block_len_usize > max_block_size) return error.BlockTooLarge; 100 108 const block_end = pos + block_len_usize; 101 109 if (block_end > data.len) return error.UnexpectedEof; 102 110 ··· 255 263 defer arena.deinit(); 256 264 const alloc = arena.allocator(); 257 265 258 - // construct a minimal CAR v1 file: 259 - // header: DAG-CBOR {"version": 1, "roots": []} 260 - const header_cbor = [_]u8{ 261 - 0xa2, // map(2) 262 - 0x65, 'r', 'o', 'o', 't', 's', 0x80, // "roots": [] (5 bytes, shorter) 263 - 0x67, 'v', 'e', 'r', 's', 'i', 'o', 'n', 0x01, // "version": 1 (7 bytes) 264 - }; 266 + // create a block and compute its real CID 267 + const block_content = try cbor.encodeAlloc(alloc, .{ .map = &.{ 268 + .{ .key = "text", .value = .{ .text = "hi" } }, 269 + } }); 270 + const block_cid = try cbor.Cid.forDagCbor(alloc, block_content); 265 271 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 272 + // write a proper CAR via the writer, then read it back 273 + const original = Car{ 274 + .roots = &.{block_cid}, 275 + .blocks = &.{.{ .cid_raw = block_cid.raw, .data = block_content }}, 272 276 }; 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; 277 + const car_bytes = try writeAlloc(alloc, original); 278 + const car_file = try read(alloc, car_bytes); 291 279 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 - 280 + try std.testing.expectEqual(@as(usize, 1), car_file.roots.len); 310 281 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 282 313 283 // decode the block content as CBOR 314 284 const val = try cbor.decodeAll(alloc, car_file.blocks[0].data);
+323
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 + }
+4 -4
src/internal/repo/cbor.zig
··· 420 420 return .{ .raw = raw }; 421 421 } 422 422 423 - /// read an unsigned varint (LEB128) 423 + /// read an unsigned varint (LEB128). rejects varints longer than 10 bytes. 424 424 pub fn readUvarint(data: []const u8, pos: *usize) ?u64 { 425 425 var result: u64 = 0; 426 426 var shift: u6 = 0; 427 - while (pos.* < data.len) { 427 + for (0..10) |_| { 428 + if (pos.* >= data.len) return null; 428 429 const byte = data[pos.*]; 429 430 pos.* += 1; 430 431 result |= @as(u64, byte & 0x7f) << shift; 431 432 if (byte & 0x80 == 0) return result; 432 433 shift +|= 7; 433 - if (shift >= 64) return null; 434 434 } 435 - return null; 435 + return null; // varint too long 436 436 } 437 437 438 438 // === encoder ===
+86
src/internal/repo/cbor_bench.zig
··· 9 9 10 10 const std = @import("std"); 11 11 const cbor = @import("cbor.zig"); 12 + const car = @import("car.zig"); 12 13 const Value = cbor.Value; 13 14 const Cid = cbor.Cid; 14 15 ··· 85 86 var bench_cid: Cid = undefined; 86 87 var bench_arena: std.heap.ArenaAllocator = undefined; 87 88 89 + // CAR benchmark data 90 + var car_bytes: []const u8 = undefined; 91 + var car_5_blocks: []const u8 = undefined; 92 + 88 93 fn initBenchData() void { 89 94 bench_arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 90 95 const alloc = bench_arena.allocator(); ··· 94 99 encoded_uint = cbor.encodeAlloc(alloc, .{ .unsigned = 1_234_567_890 }) catch @panic("encode uint"); 95 100 bench_cid = Cid.forDagCbor(alloc, encoded_record) catch @panic("compute cid"); 96 101 encoded_cid_link = cbor.encodeAlloc(alloc, .{ .cid = bench_cid }) catch @panic("encode cid"); 102 + 103 + // build CAR test data: 1-block CAR 104 + car_bytes = car.writeAlloc(alloc, .{ 105 + .roots = &.{bench_cid}, 106 + .blocks = &.{.{ .cid_raw = bench_cid.raw, .data = encoded_record }}, 107 + }) catch @panic("write car"); 108 + 109 + // 5-block CAR — each block has unique text to produce unique CIDs 110 + const block_texts = [_][]const u8{ "block-0", "block-1", "block-2", "block-3", "block-4" }; 111 + var blocks5: [5]car.Block = undefined; 112 + var cids5: [5]Cid = undefined; 113 + for (&blocks5, &cids5, block_texts) |*b, *c, text| { 114 + const rec = cbor.encodeAlloc(alloc, .{ .map = &.{ 115 + .{ .key = "text", .value = .{ .text = text } }, 116 + } }) catch @panic("encode block"); 117 + c.* = Cid.forDagCbor(alloc, rec) catch @panic("cid"); 118 + b.* = .{ .cid_raw = c.raw, .data = rec }; 119 + } 120 + car_5_blocks = car.writeAlloc(alloc, .{ 121 + .roots = &.{cids5[0]}, 122 + .blocks = &blocks5, 123 + }) catch @panic("write 5-block car"); 97 124 } 98 125 99 126 // --------------------------------------------------------------------------- ··· 331 358 std.mem.doNotOptimizeAway(cid_buf); 332 359 } 333 360 361 + // --- CAR benchmarks --- 362 + 363 + fn benchCarRead1() void { 364 + var scratch: [8192]u8 = undefined; 365 + var fba = std.heap.FixedBufferAllocator.init(&scratch); 366 + const parsed = car.readWithOptions(fba.allocator(), car_bytes, .{ 367 + .verify_block_hashes = true, 368 + }) catch @panic("read car"); 369 + std.mem.doNotOptimizeAway(parsed); 370 + } 371 + 372 + fn benchCarRead1NoVerify() void { 373 + var scratch: [8192]u8 = undefined; 374 + var fba = std.heap.FixedBufferAllocator.init(&scratch); 375 + const parsed = car.readWithOptions(fba.allocator(), car_bytes, .{ 376 + .verify_block_hashes = false, 377 + }) catch @panic("read car"); 378 + std.mem.doNotOptimizeAway(parsed); 379 + } 380 + 381 + fn benchCarRead5() void { 382 + var scratch: [32768]u8 = undefined; 383 + var fba = std.heap.FixedBufferAllocator.init(&scratch); 384 + const parsed = car.readWithOptions(fba.allocator(), car_5_blocks, .{ 385 + .verify_block_hashes = true, 386 + }) catch @panic("read car"); 387 + std.mem.doNotOptimizeAway(parsed); 388 + } 389 + 390 + fn benchCarWrite1() void { 391 + var out_buf: [2048]u8 = undefined; 392 + var w: std.Io.Writer = .fixed(&out_buf); 393 + var scratch: [2048]u8 = undefined; 394 + var fba = std.heap.FixedBufferAllocator.init(&scratch); 395 + car.write(fba.allocator(), &w, .{ 396 + .roots = &.{bench_cid}, 397 + .blocks = &.{.{ .cid_raw = bench_cid.raw, .data = encoded_record }}, 398 + }) catch @panic("write car"); 399 + std.mem.doNotOptimizeAway(w.end); 400 + } 401 + 402 + fn benchCarRoundTrip1() void { 403 + var scratch: [16384]u8 = undefined; 404 + var fba = std.heap.FixedBufferAllocator.init(&scratch); 405 + const alloc = fba.allocator(); 406 + const parsed = car.read(alloc, car_bytes) catch @panic("read"); 407 + const written = car.writeAlloc(alloc, parsed) catch @panic("write"); 408 + std.mem.doNotOptimizeAway(written); 409 + } 410 + 334 411 // --------------------------------------------------------------------------- 335 412 // main 336 413 // --------------------------------------------------------------------------- ··· 373 450 374 451 std.debug.print("\ncomposite:\n", .{}); 375 452 bench("decode + key lookup (3 keys)", benchMapKeyLookup); 453 + 454 + std.debug.print("\nCAR v1 ({d} bytes, 1 block):\n", .{car_bytes.len}); 455 + bench("read CAR (with hash verify)", benchCarRead1); 456 + bench("read CAR (no verify)", benchCarRead1NoVerify); 457 + bench("write CAR", benchCarWrite1); 458 + bench("read + write round-trip", benchCarRoundTrip1); 459 + 460 + std.debug.print("\nCAR v1 ({d} bytes, 5 blocks):\n", .{car_5_blocks.len}); 461 + bench("read CAR 5 blocks (verified)", benchCarRead5); 376 462 377 463 std.debug.print("\ndiagnostic (cost breakdown):\n", .{}); 378 464 bench("UTF-8 validate (434 bytes)", benchUtf8Validate);
+1
src/root.zig
··· 74 74 _ = @import("internal/testing/interop_tests.zig"); 75 75 _ = @import("internal/repo/repo_verifier.zig"); 76 76 _ = @import("internal/repo/cbor_test.zig"); 77 + _ = @import("internal/repo/car_test.zig"); 77 78 } 78 79 }