OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-overlay / lib / manifest.js

80 lines · javascript ref:
/**
 * 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 }
}