A minimal AT Protocol Personal Data Server written in JavaScript.
atproto pds
at main 155 lines 5.0 kB view raw
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});