import { PACKER_CODES, PackerError } from "../../error-codes.js"; import { resolvePackerMessage } from "../../i18n/messages.js"; import { CHUNK_HEADER_SIZE, CHUNK_METADATA_SIZE, CHUNK_VERSION_V1, type ChunkV1Metadata, } from "../../types/chunked.types.js"; const FILE_ID_SIZE = 32; const NONCE_SIZE = 24; const AEAD_TAG_SIZE = 16; /** * Encode a {@link ChunkV1Metadata} into the locked 100-byte big-endian metadata header. * * Throws `PackerError(PACKER.INVALID_FORMAT)` on any field-size mismatch. */ export function encodeHeader(meta: ChunkV1Metadata): Uint8Array { if (meta.fileId.byteLength !== FILE_ID_SIZE) { throw new PackerError( PACKER_CODES.INVALID_FORMAT, `fileId must be ${FILE_ID_SIZE} bytes, got ${meta.fileId.byteLength}` ); } if (meta.nonce.byteLength !== NONCE_SIZE) { throw new PackerError( PACKER_CODES.INVALID_FORMAT, `nonce must be ${NONCE_SIZE} bytes, got ${meta.nonce.byteLength}` ); } if (meta.aeadTag.byteLength !== AEAD_TAG_SIZE) { throw new PackerError( PACKER_CODES.INVALID_FORMAT, `aeadTag must be ${AEAD_TAG_SIZE} bytes, got ${meta.aeadTag.byteLength}` ); } const out = new Uint8Array(CHUNK_METADATA_SIZE); const view = new DataView(out.buffer); view.setUint16(0, meta.version, false); out.set(meta.fileId, 2); view.setUint32(34, meta.chunkId, false); view.setUint32(38, meta.totalChunks, false); view.setBigUint64(42, meta.fileSize, false); view.setUint32(50, meta.chunkSize, false); view.setUint32(54, meta.payloadLen, false); view.setUint16(58, meta.flags, false); out.set(meta.nonce, 60); out.set(meta.aeadTag, 84); return out; } /** * Parse the first 100 bytes of `buf` as a v1 metadata header. * * Validates: buffer length, version=0x0001, flags=0x0000 (v1 reserved), * totalChunks ≥ 1, chunkId < totalChunks, chunkSize > 164, and the payloadLen * invariants for non-final and final chunks (PACKER-CHUNK-03). * * Field byte ranges are returned as zero-copy `Uint8Array.subarray` views into * `buf` — callers must not mutate. */ export function parseHeader(buf: Uint8Array): ChunkV1Metadata { if (buf.byteLength < CHUNK_METADATA_SIZE) { throw new PackerError( PACKER_CODES.INVALID_FORMAT, `buffer too short for metadata: ${buf.byteLength} < ${CHUNK_METADATA_SIZE}` ); } const view = new DataView(buf.buffer, buf.byteOffset, CHUNK_METADATA_SIZE); const version = view.getUint16(0, false); if (version !== CHUNK_VERSION_V1) { throw new PackerError( PACKER_CODES.VERSION_UNSUPPORTED, resolvePackerMessage( undefined, "packer.error.VERSION_UNSUPPORTED.unknown-version", { version: `0x${version.toString(16).padStart(4, "0")}` } ) ); } const flags = view.getUint16(58, false); if (flags !== 0) { throw new PackerError( PACKER_CODES.FLAGS_RESERVED_NONZERO, resolvePackerMessage( undefined, "packer.error.FLAGS_RESERVED_NONZERO.nonzero" ) ); } const fileId = buf.subarray(2, 34); const chunkId = view.getUint32(34, false); const totalChunks = view.getUint32(38, false); const fileSize = view.getBigUint64(42, false); const chunkSize = view.getUint32(50, false); const payloadLen = view.getUint32(54, false); const nonce = buf.subarray(60, 84); const aeadTag = buf.subarray(84, 100); if (totalChunks === 0) { throw new PackerError( PACKER_CODES.INVALID_FORMAT, "totalChunks must be >= 1" ); } if (chunkId >= totalChunks) { throw new PackerError( PACKER_CODES.INVALID_FORMAT, `chunkId=${chunkId} out of range; totalChunks=${totalChunks}` ); } if (chunkSize <= CHUNK_HEADER_SIZE) { throw new PackerError( PACKER_CODES.INVALID_FORMAT, `chunkSize=${chunkSize} must exceed header size ${CHUNK_HEADER_SIZE}` ); } const payloadPerChunk = chunkSize - CHUNK_HEADER_SIZE; if (chunkId < totalChunks - 1) { if (payloadLen !== payloadPerChunk) { throw new PackerError( PACKER_CODES.PAYLOAD_LEN_INVALID, resolvePackerMessage( undefined, "packer.error.PAYLOAD_LEN_INVALID.non-final", { actual: payloadLen, expected: payloadPerChunk } ) ); } } else { const expectedFinal = fileSize - BigInt(chunkId) * BigInt(payloadPerChunk); if (BigInt(payloadLen) !== expectedFinal) { throw new PackerError( PACKER_CODES.PAYLOAD_LEN_INVALID, resolvePackerMessage( undefined, "packer.error.PAYLOAD_LEN_INVALID.final", { actual: payloadLen, expected: expectedFinal.toString(), } ) ); } } return { version, fileId, chunkId, totalChunks, fileSize, chunkSize, payloadLen, flags, nonce, aeadTag, }; }