/**
* Manifest — ordered chunk list for large files
*
* Stores all chunk hashes + sizes for files with >64 chunks.
* Delta-encoded and DEFLATE compressed for efficient storage/transfer.
*
* Binary format:
* Header: version (uint8) + chunkCount (uint32BE) + fileHash (32 bytes)
* Body: for each chunk: hash (32 bytes) + size (uint16BE)
* Compressed: raw DEFLATE
*/
import { deflateRawSync, inflateRawSync } from '#zlib'
/**
* Encode a chunk list into a compressed manifest
*
* @param {Array<{hash: string, size: number}>} chunks - Ordered chunk list
* @param {string} fileHash - blake2b hex hash of the complete file
* @returns {Buffer} Compressed manifest
*/
export function encode (chunks, fileHash) {
// Header: version (1) + chunkCount (4) + fileHash (32) = 37 bytes
// Body: per chunk: hash (32) + size (2) = 34 bytes each
const headerSize = 37
const bodySize = chunks.length * 34
const raw = Buffer.alloc(headerSize + bodySize)
// Header
raw[0] = 1 // version
raw.writeUInt32BE(chunks.length, 1)
Buffer.from(fileHash, 'hex').copy(raw, 5, 0, 32)
// Body
let offset = headerSize
for (const chunk of chunks) {
Buffer.from(chunk.hash, 'hex').copy(raw, offset, 0, 32)
offset += 32
raw.writeUInt16BE(Math.min(chunk.size, 65535), offset)
offset += 2
}
return deflateRawSync(raw)
}
/**
* Decode a compressed manifest back to chunk list
*
* @param {Buffer} compressed - Compressed manifest
* @returns {{ fileHash: string, chunks: Array<{hash: string, size: number}> }}
*/
export function decode (compressed) {
const raw = inflateRawSync(compressed)
if (raw.length < 37) throw new Error('Manifest too short')
const version = raw[0]
if (version !== 1) throw new Error(`Unsupported manifest version: ${version}`)
const chunkCount = raw.readUInt32BE(1)
const fileHash = raw.subarray(5, 37).toString('hex')
const expectedSize = 37 + chunkCount * 34
if (raw.length < expectedSize) {
throw new Error(`Manifest truncated: expected ${expectedSize} bytes, got ${raw.length}`)
}
const chunks = []
let offset = 37
for (let i = 0; i < chunkCount; i++) {
const hash = raw.subarray(offset, offset + 32).toString('hex')
offset += 32
const size = raw.readUInt16BE(offset)
offset += 2
chunks.push({ hash, size })
}
return { fileHash, chunks }
}