A minimal AT Protocol Personal Data Server written in JavaScript.
atproto
pds
1// @pds/core/car tests - CAR file parser
2import { describe, it, expect } from 'vitest';
3import {
4 readVarint,
5 parseCarFile,
6 iterateCarBlocks,
7 getCarHeader,
8} from '../packages/core/src/car.js';
9import {
10 buildCarFile,
11 cborEncodeDagCbor,
12 createCid,
13 cidToString,
14} from '../packages/core/src/repo.js';
15
16describe('readVarint', () => {
17 it('should decode single-byte varints', () => {
18 // 0x00 = 0
19 expect(readVarint(new Uint8Array([0x00]), 0)).toEqual([0, 1]);
20 // 0x01 = 1
21 expect(readVarint(new Uint8Array([0x01]), 0)).toEqual([1, 1]);
22 // 0x7f = 127
23 expect(readVarint(new Uint8Array([0x7f]), 0)).toEqual([127, 1]);
24 });
25
26 it('should decode multi-byte varints', () => {
27 // 0x80 0x01 = 128
28 expect(readVarint(new Uint8Array([0x80, 0x01]), 0)).toEqual([128, 2]);
29 // 0xff 0x01 = 255
30 expect(readVarint(new Uint8Array([0xff, 0x01]), 0)).toEqual([255, 2]);
31 // 0xac 0x02 = 300
32 expect(readVarint(new Uint8Array([0xac, 0x02]), 0)).toEqual([300, 2]);
33 });
34
35 it('should decode varint at offset', () => {
36 const bytes = new Uint8Array([0xff, 0x7f, 0xac, 0x02, 0x00]);
37 expect(readVarint(bytes, 2)).toEqual([300, 4]);
38 });
39
40 it('should throw on unterminated varint', () => {
41 expect(() => readVarint(new Uint8Array([0x80]), 0)).toThrow(
42 'Unexpected end of varint',
43 );
44 });
45});
46
47describe('parseCarFile', () => {
48 it('should roundtrip with buildCarFile - single block', async () => {
49 // Create a test block
50 const record = { $type: 'app.bsky.feed.post', text: 'Hello World' };
51 const recordBytes = cborEncodeDagCbor(record);
52 const recordCid = cidToString(await createCid(recordBytes));
53
54 // Build CAR file
55 const car = buildCarFile(recordCid, [{ cid: recordCid, data: recordBytes }]);
56
57 // Parse it back
58 const parsed = parseCarFile(car);
59
60 expect(parsed.roots).toHaveLength(1);
61 expect(parsed.roots[0]).toBe(recordCid);
62 expect(parsed.blocks.size).toBe(1);
63 expect(parsed.blocks.has(recordCid)).toBe(true);
64 expect(parsed.blocks.get(recordCid)).toEqual(recordBytes);
65 });
66
67 it('should roundtrip with buildCarFile - multiple blocks', async () => {
68 // Create multiple test blocks
69 const block1 = cborEncodeDagCbor({ text: 'Block 1' });
70 const cid1 = cidToString(await createCid(block1));
71
72 const block2 = cborEncodeDagCbor({ text: 'Block 2' });
73 const cid2 = cidToString(await createCid(block2));
74
75 const block3 = cborEncodeDagCbor({ text: 'Block 3' });
76 const cid3 = cidToString(await createCid(block3));
77
78 // Build CAR file with first block as root
79 const car = buildCarFile(cid1, [
80 { cid: cid1, data: block1 },
81 { cid: cid2, data: block2 },
82 { cid: cid3, data: block3 },
83 ]);
84
85 // Parse it back
86 const parsed = parseCarFile(car);
87
88 expect(parsed.roots).toHaveLength(1);
89 expect(parsed.roots[0]).toBe(cid1);
90 expect(parsed.blocks.size).toBe(3);
91 expect(parsed.blocks.get(cid1)).toEqual(block1);
92 expect(parsed.blocks.get(cid2)).toEqual(block2);
93 expect(parsed.blocks.get(cid3)).toEqual(block3);
94 });
95
96 it('should throw on invalid CAR version', () => {
97 // Create a malformed CAR with version 2
98 const invalidHeader = cborEncodeDagCbor({ version: 2, roots: [] });
99 const headerLenBytes = new Uint8Array([invalidHeader.length]);
100 const car = new Uint8Array(
101 headerLenBytes.length + invalidHeader.length,
102 );
103 car.set(headerLenBytes, 0);
104 car.set(invalidHeader, headerLenBytes.length);
105
106 expect(() => parseCarFile(car)).toThrow('Unsupported CAR version: 2');
107 });
108});
109
110describe('iterateCarBlocks', () => {
111 it('should iterate blocks in order', async () => {
112 // Create test blocks
113 const block1 = cborEncodeDagCbor({ index: 1 });
114 const cid1 = cidToString(await createCid(block1));
115
116 const block2 = cborEncodeDagCbor({ index: 2 });
117 const cid2 = cidToString(await createCid(block2));
118
119 // Build CAR file
120 const car = buildCarFile(cid1, [
121 { cid: cid1, data: block1 },
122 { cid: cid2, data: block2 },
123 ]);
124
125 // Iterate blocks
126 const blocks = [];
127 for (const block of iterateCarBlocks(car)) {
128 blocks.push(block);
129 }
130
131 expect(blocks).toHaveLength(2);
132 expect(blocks[0].cid).toBe(cid1);
133 expect(blocks[0].data).toEqual(block1);
134 expect(blocks[1].cid).toBe(cid2);
135 expect(blocks[1].data).toEqual(block2);
136 });
137});
138
139describe('getCarHeader', () => {
140 it('should extract header without parsing blocks', async () => {
141 // Create a large block (to verify we don't parse it)
142 const largeData = cborEncodeDagCbor({ data: 'x'.repeat(10000) });
143 const largeCid = cidToString(await createCid(largeData));
144
145 // Build CAR file
146 const car = buildCarFile(largeCid, [{ cid: largeCid, data: largeData }]);
147
148 // Get header only
149 const header = getCarHeader(car);
150
151 expect(header.version).toBe(1);
152 expect(header.roots).toHaveLength(1);
153 expect(header.roots[0]).toBe(largeCid);
154 });
155});