/** * @module * * locates and reads the structural pieces of a 7z archive: the 32-byte * SignatureHeader at the start of the file, the StartHeader pointer it * carries to the NextHeader at the tail, and the NextHeader itself. * * the NextHeader is often stored compressed (an EncodedHeader); when that * happens the on-disk header is itself a single-folder packed stream whose * coder chain must be run before {@link parseHeader} can read the result. * decoding the EncodedHeader is deferred to the codec layer; this module * surfaces the raw structure so callers can branch on the kind. */ import { crc32 } from '@mary/crc32'; import { type DispatchOptions, rewrapDecodeError, runFolder } from './codec/folder.ts'; import { SIGNATURE, SIGNATURE_HEADER_SIZE } from './constants.ts'; import { ChecksumMismatchError, InvalidSignatureError, MalformedArchiveError, TruncatedArchiveError, } from './errors.ts'; import { type ArchiveHeader, type Folder, parseHeader, parseStreamsInfo } from './header.ts'; import { Property } from './constants.ts'; import type { Reader } from './reader/types.ts'; import { concat, readBytes } from './utils/buffer.ts'; import { Cursor } from './utils/cursor.ts'; // #region signature header /** the pointer carried by the StartHeader inside the signature header. */ export interface StartHeader { /** file format major version (always 0 in archives we support). */ versionMajor: number; /** file format minor version. */ versionMinor: number; /** offset from the end of the signature header to the NextHeader. */ nextHeaderOffset: number; /** length of the NextHeader region. */ nextHeaderSize: number; /** CRC32 over the NextHeader region. */ nextHeaderCrc: number; } /** * reads and validates the 32-byte signature header at the start of the * archive. throws {@link InvalidSignatureError} on a wrong signature and * {@link ChecksumMismatchError} on a StartHeader CRC mismatch. * * @param reader the random-access archive source * @returns the parsed StartHeader pointer to the NextHeader */ export const readSignatureHeader = async (reader: Reader): Promise => { if (reader.length < SIGNATURE_HEADER_SIZE) { throw new TruncatedArchiveError('source is shorter than the 7z signature header'); } const bytes = await readBytes(reader, 0, SIGNATURE_HEADER_SIZE); for (let i = 0; i < SIGNATURE.length; i++) { if (bytes[i] !== SIGNATURE[i]) { throw new InvalidSignatureError('source does not start with a 7z signature'); } } const versionMajor = bytes[6]; const versionMinor = bytes[7]; const startHeaderCrc = new DataView(bytes.buffer, bytes.byteOffset).getUint32(8, true); const startHeader = bytes.subarray(12, 32); const computedCrc = crc32(startHeader); if (computedCrc !== startHeaderCrc) { throw new ChecksumMismatchError( `StartHeader CRC32 mismatch: stored 0x${startHeaderCrc.toString(16).padStart(8, '0')}, ` + `computed 0x${computedCrc.toString(16).padStart(8, '0')}`, ); } const dv = new DataView(startHeader.buffer, startHeader.byteOffset, startHeader.byteLength); const offsetBig = dv.getBigUint64(0, true); const sizeBig = dv.getBigUint64(8, true); if (offsetBig > BigInt(Number.MAX_SAFE_INTEGER) || sizeBig > BigInt(Number.MAX_SAFE_INTEGER)) { throw new MalformedArchiveError('NextHeader offset or size exceeds safe integer range'); } return { nextHeaderCrc: dv.getUint32(16, true), nextHeaderOffset: Number(offsetBig), nextHeaderSize: Number(sizeBig), versionMajor, versionMinor, }; }; // #endregion // #region next header /** * raw NextHeader payload as it sits on disk: either a plain Header (parse * directly with {@link parseHeader}) or an EncodedHeader (the bytes belong * to a folder's packed stream and must be decoded first). */ export type NextHeaderRaw = | { kind: 'header'; bytes: Uint8Array } | { kind: 'encoded'; folder: Folder; packPos: number; packSize: number; unpackSize: number; storedCrc: number | undefined; }; /** * reads the NextHeader region and decides whether it is a plain Header or * an EncodedHeader. the bytes are CRC32-verified against the StartHeader's * `nextHeaderCrc` before further parsing. * * @param reader random-access archive source * @param start StartHeader returned by {@link readSignatureHeader} * @returns the raw NextHeader description; plain headers carry their bytes * directly, encoded headers carry the indirection that locates the packed * payload elsewhere in the file */ export const readNextHeader = async ( reader: Reader, start: StartHeader, ): Promise => { const absoluteOffset = SIGNATURE_HEADER_SIZE + start.nextHeaderOffset; if (absoluteOffset + start.nextHeaderSize > reader.length) { throw new TruncatedArchiveError( `NextHeader region runs past end of file: ` + `expected ${absoluteOffset + start.nextHeaderSize} bytes, got ${reader.length}`, ); } const bytes = await readBytes(reader, absoluteOffset, start.nextHeaderSize); const computedCrc = crc32(bytes); if (computedCrc !== start.nextHeaderCrc) { throw new ChecksumMismatchError( `NextHeader CRC32 mismatch: stored 0x${start.nextHeaderCrc.toString(16).padStart(8, '0')}, ` + `computed 0x${computedCrc.toString(16).padStart(8, '0')}`, ); } const cursor = new Cursor(bytes); const tag = cursor.peekU8(); if (tag === Property.header) { return { bytes, kind: 'header' }; } if (tag === Property.encodedHeader) { cursor.readU8(); const info = parseStreamsInfo(cursor); if (info.folders.length !== 1) { throw new MalformedArchiveError( `EncodedHeader must contain exactly one folder: got ${info.folders.length}`, ); } if (info.pack.packSizes.length < 1) { throw new MalformedArchiveError('EncodedHeader has no packed streams'); } const folder = info.folders[0]; const unpackSize = folder.unpackSizes.length > 0 ? lastUnboundUnpackSize(folder) : 0; return { folder, kind: 'encoded', packPos: info.pack.packPos, packSize: info.pack.packSizes[0], storedCrc: info.pack.packCrcs[0], unpackSize, }; } throw new MalformedArchiveError( `expected Header (0x01) or EncodedHeader (0x17): got 0x${tag.toString(16)}`, ); }; const lastUnboundUnpackSize = (folder: Folder): number => { for (let i = folder.unpackSizes.length - 1; i >= 0; i--) { if (!folder.bindPairs.some((bp) => bp.out === i)) { return folder.unpackSizes[i]; } } return 0; }; // #endregion // #region archive /** the fully-loaded result of reading an archive's signature + NextHeader. */ export interface ArchiveContainer { start: StartHeader; /** * the absolute byte offset where the packed data area begins * (`SIGNATURE_HEADER_SIZE` plus the header's PackInfo.packPos). */ packBase: number; header: ArchiveHeader; } /** * reads an archive's container layer: signature, StartHeader, NextHeader. * when the NextHeader is encoded (the common case), its single-folder * coder chain is run through the codec layer to recover the plain header * bytes before parsing. * * @param reader random-access archive source * @returns the parsed container */ export const readContainer = async ( reader: Reader, options: DispatchOptions = {}, ): Promise => { const start = await readSignatureHeader(reader); if (start.nextHeaderSize === 0) { const header: ArchiveHeader = { fileFolderIndex: [], fileSubStreamIndex: [], files: [], folders: [], pack: { packCrcs: [], packPos: 0, packSizes: [] }, subStreams: { crcs: [], numUnpackStreams: [], unpackSizes: [] }, }; return { header, packBase: SIGNATURE_HEADER_SIZE, start }; } const raw = await readNextHeader(reader, start); const headerBytes = raw.kind === 'header' ? raw.bytes : await decodeEncodedHeader(reader, raw, options); let header: ArchiveHeader; try { header = parseHeader(headerBytes); } catch (err) { if (raw.kind === 'encoded') { throw rewrapDecodeError(err, raw.folder, options); } throw err; } return { header, packBase: SIGNATURE_HEADER_SIZE + header.pack.packPos, start }; }; const decodeEncodedHeader = async ( reader: Reader, raw: Extract, options: DispatchOptions, ): Promise> => { const packedOffset = SIGNATURE_HEADER_SIZE + raw.packPos; try { const chunks: Uint8Array[] = []; let total = 0; for await ( const chunk of runFolder( reader, raw.folder, [packedOffset], [raw.packSize], raw.unpackSize, options, ) ) { chunks.push(chunk); total += chunk.length; } if (total !== raw.unpackSize) { throw new MalformedArchiveError( `EncodedHeader output size mismatch: expected ${raw.unpackSize} bytes, got ${total}`, ); } return concat(chunks, total); } catch (err) { throw rewrapDecodeError(err, raw.folder, options); } }; // #endregion