| … | … | @@ -0,0 +1,55 @@ |
| 1 | // A path-keyed chunk map that no longer describes the file must not be served. |
| 2 | // |
| 3 | // 2026-10-11, tabertron serving hyper-tps texture packs: the stored map for |
| 4 | // `+0med_but1_normal.png` was ONE 191-byte chunk — the git POINTER the path held |
| 5 | // when it was first served — while the file is the real 6196-byte PNG with the |
| 6 | // same mtime. prepareFile trusted it (mtime matched), _onChunkNeed served by it, |
| 7 | // and stortron asked for chunk 0 forever: the bytes never matched the hash, and |
| 8 | // ~2.6k receives stayed open. Maps < 1 MB were never rewritten. |
| 9 | |
| 10 | import test from 'brittle' |
| 11 | import { tmpStore, tmpDir, fs, path } from './helpers.js' |
| 12 | import { FileIndex } from '../lib/file-index.js' |
| 13 | import { TransferManager } from '../lib/transfer.js' |
| 14 | import { hashChunk } from '../lib/chunker.js' |
| 15 | |
| 16 | test('prepareFile — a cached map whose chunks do not cover the file is rebuilt and replaced', async (t) => { |
| 17 | const index = new FileIndex(tmpStore('stale-map')) |
| 18 | await index.ready() |
| 19 | const transfer = new TransferManager(index) |
| 20 | |
| 21 | const pointer = Buffer.from('hyper-overlay v1\noid blake2b:' + 'a'.repeat(64) + '\nsize 6196\n' + 'x'.repeat(91)) |
| 22 | const real = Buffer.alloc(6196, 7) |
| 23 | const dir = tmpDir('stale-map') |
| 24 | const filePath = path.join(dir, 'tex.png') |
| 25 | fs.writeFileSync(filePath, real) |
| 26 | const st = fs.statSync(filePath) |
| 27 | |
| 28 | // The state tabertron was in: stale pointer-sized map, metadata of the real file. |
| 29 | await index.putChunkMap('/tex.png', [{ hash: hashChunk(pointer), offset: 0, length: pointer.length }]) |
| 30 | await index.putFile('/tex.png', { contentHash: 'b'.repeat(64), size: real.length, mtime: st.mtimeMs }) |
| 31 | |
| 32 | const res = await transfer.prepareFile(filePath, '/tex.png') |
| 33 | const total = res.chunks.reduce((n, c) => n + c.length, 0) |
| 34 | t.is(total, real.length, 'served map covers the whole file') |
| 35 | t.is(res.chunks[0].hash, hashChunk(real.subarray(0, res.chunks[0].length)), 'chunk hash is of the real bytes') |
| 36 | |
| 37 | const stored = await index.getChunkMap('/tex.png') |
| 38 | t.alike(stored, res.chunks, 'the path-keyed map (what _onChunkNeed serves by) is replaced') |
| 39 | }) |
| 40 | |
| 41 | test('prepareFile — a cached map that does cover the file is still reused', async (t) => { |
| 42 | const index = new FileIndex(tmpStore('good-map')) |
| 43 | await index.ready() |
| 44 | const transfer = new TransferManager(index) |
| 45 | const dir = tmpDir('good-map') |
| 46 | const filePath = path.join(dir, 'f.bin') |
| 47 | fs.writeFileSync(filePath, Buffer.alloc(3000, 1)) |
| 48 | const first = await transfer.prepareFile(filePath, '/f.bin') |
| 49 | await index.putChunkMap('/f.bin', first.chunks) |
| 50 | const marker = first.chunks.map(c => ({ ...c })) |
| 51 | marker[0].hash = 'c'.repeat(64) // same lengths, different hash: proves the cache path was taken |
| 52 | await index.putChunkMap('/f.bin', marker) |
| 53 | const again = await transfer.prepareFile(filePath, '/f.bin') |
| 54 | t.is(again.chunks[0].hash, 'c'.repeat(64), 'cache hit when the map covers the file size') |
| 55 | }) |