zip.test.ts
2,637 bytes
| 1 | import { describe, expect, it } from 'vitest' |
|---|---|
| 2 | import { createZip } from './zip' |
| 3 | |
| 4 | interface StoredEntry { |
| 5 | content: Buffer |
| 6 | localOffset: number |
| 7 | } |
| 8 | |
| 9 | function readStoredEntries(zip: Buffer): Map<string, StoredEntry> { |
| 10 | const eocdOffset = zip.length - 22 |
| 11 | expect(zip.readUInt32LE(eocdOffset)).toBe(0x06054b50) |
| 12 | |
| 13 | const entryCount = zip.readUInt16LE(eocdOffset + 10) |
| 14 | const centralSize = zip.readUInt32LE(eocdOffset + 12) |
| 15 | const centralOffset = zip.readUInt32LE(eocdOffset + 16) |
| 16 | expect(centralOffset + centralSize).toBe(eocdOffset) |
| 17 | |
| 18 | const entries = new Map<string, StoredEntry>() |
| 19 | let cursor = centralOffset |
| 20 | |
| 21 | for (let index = 0; index < entryCount; index++) { |
| 22 | expect(zip.readUInt32LE(cursor)).toBe(0x02014b50) |
| 23 | expect(zip.readUInt16LE(cursor + 10)).toBe(0) |
| 24 | |
| 25 | const storedSize = zip.readUInt32LE(cursor + 24) |
| 26 | const nameLength = zip.readUInt16LE(cursor + 28) |
| 27 | const extraLength = zip.readUInt16LE(cursor + 30) |
| 28 | const commentLength = zip.readUInt16LE(cursor + 32) |
| 29 | const localOffset = zip.readUInt32LE(cursor + 42) |
| 30 | const name = zip.subarray(cursor + 46, cursor + 46 + nameLength).toString('utf8') |
| 31 | |
| 32 | expect(zip.readUInt32LE(localOffset)).toBe(0x04034b50) |
| 33 | expect(zip.readUInt16LE(localOffset + 8)).toBe(0) |
| 34 | |
| 35 | const localSize = zip.readUInt32LE(localOffset + 22) |
| 36 | const localNameLength = zip.readUInt16LE(localOffset + 26) |
| 37 | const localExtraLength = zip.readUInt16LE(localOffset + 28) |
| 38 | const localName = zip |
| 39 | .subarray(localOffset + 30, localOffset + 30 + localNameLength) |
| 40 | .toString('utf8') |
| 41 | const contentOffset = localOffset + 30 + localNameLength + localExtraLength |
| 42 | |
| 43 | expect(localName).toBe(name) |
| 44 | expect(localSize).toBe(storedSize) |
| 45 | entries.set(name, { |
| 46 | content: zip.subarray(contentOffset, contentOffset + storedSize), |
| 47 | localOffset, |
| 48 | }) |
| 49 | |
| 50 | cursor += 46 + nameLength + extraLength + commentLength |
| 51 | } |
| 52 | |
| 53 | expect(cursor).toBe(eocdOffset) |
| 54 | return entries |
| 55 | } |
| 56 | |
| 57 | describe('createZip', () => { |
| 58 | it('produces a valid stored zip with byte-for-byte content', () => { |
| 59 | const entries = [ |
| 60 | { name: 'SPEC.md', content: Buffer.from('# SPEC\n\nsome content [S1]\n', 'utf8') }, |
| 61 | { name: 'nested/HANDOFF.md', content: Buffer.from('claude "do the thing"\n', 'utf8') }, |
| 62 | { name: 'empty.txt', content: Buffer.alloc(0) }, |
| 63 | ] |
| 64 | |
| 65 | const zipBuffer = createZip(entries, new Date('2026-01-15T10:30:00')) |
| 66 | const stored = readStoredEntries(zipBuffer) |
| 67 | |
| 68 | for (const entry of entries) { |
| 69 | expect(stored.get(entry.name)?.content.equals(entry.content)).toBe(true) |
| 70 | } |
| 71 | expect([...stored.keys()]).toEqual(entries.map((entry) => entry.name)) |
| 72 | }) |
| 73 | }) |
| 74 | |