/* * SPDX-License-Identifier: MIT * * CAB structure parsing and extraction translated from rust-cab 0.6.0. * rust-cab commit c5839f5fdfa4c4e7cc9b22f570c79c96af0560e2 * Copyright (c) 2017 Matthew D. Steele, MIT License. * Modified for immutable browser Uint8Array input, strict archive-path handling, * explicit resource limits, cancellation, and whole-folder extraction. */ import { BinaryReader } from './binary-reader.js'; import { checkCancellation, yieldForCancellation } from './cancellation.js'; import { calculateDataBlockChecksum } from './checksum.js'; import { fail, warning } from './errors.js'; import { LzxDecoder } from './lzx/decoder.js'; import { MsZipDecoder } from './mszip/decoder.js'; import { decodeCabFileName, normalizeCabPath } from './path.js'; import { QuantumDecoder } from './quantum/decoder.js'; import { resolveLimits, type CabCompressionMethod, type CabEnumerationResult, type CabExtractionResult, type CabFileEntry, type CabOpenOptions, type OnePkgLimits, } from './types.js'; const CAB_SIGNATURE = 0x4643_534d; const FLAG_PREVIOUS_CABINET = 0x0001; const FLAG_NEXT_CABINET = 0x0002; export const FLAG_RESERVE_PRESENT = 0x0004; const ATTRIBUTE_NAME_IS_UTF8 = 0x0080; const MAX_CAB_BLOCK_OUTPUT = 32 * 1024; const MAX_CAB_BLOCK_INPUT = MAX_CAB_BLOCK_OUTPUT + 6 * 1024; const MAX_MSZIP_BLOCK_INPUT = 32 * 1024 + 12; export const FOLDER_CONTINUED_FROM_PREVIOUS = 0xfffd; export const FOLDER_CONTINUED_TO_NEXT = 0xfffe; export const FOLDER_CONTINUED_PREVIOUS_AND_NEXT = 0xffff; const STRICT_UTF8 = new TextDecoder('utf-8', { fatal: true }); export interface CabDataBlockInternal { headerOffset: number; checksum: number; compressedSize: number; uncompressedSize: number; reserve: Uint8Array; dataOffset: number; sourceBytes: Uint8Array; } export interface CabFolderInternal { index: number; firstDataBlockOffset: number; dataBlockCount: number; compression: CabCompressionMethod; reserve: Uint8Array; blocks: CabDataBlockInternal[]; compressedSize: number; uncompressedSize: number; dataEnd: number; } export interface CabFileRecordInternal { entry: CabFileEntry; rawFolderIndex: number; duplicateKey: string; date: number; time: number; entryOffset: number; } export interface ParsedCabinetInternal { bytes: Uint8Array; limits: OnePkgLimits; flags: number; previousCabinetName?: string; previousDiskName?: string; nextCabinetName?: string; nextDiskName?: string; headerReserveSize: number; folderReserveSize: number; dataReserveSize: number; headerReserve: Uint8Array; folders: CabFolderInternal[]; fileRecords: CabFileRecordInternal[]; result: CabEnumerationResult; } export function verifyDataBlockChecksumInternal( bytes: Uint8Array, block: CabDataBlockInternal ): void { if (block.checksum === 0) return; const compressed = bytes.subarray( block.dataOffset, block.dataOffset + block.compressedSize ); const actual = calculateDataBlockChecksum( block.reserve, compressed, block.compressedSize, block.uncompressedSize ); if (actual !== block.checksum) { fail( 'cab-checksum-mismatch', `CAB data block checksum mismatch: expected 0x${block.checksum.toString(16).padStart(8, '0')}, got 0x${actual.toString(16).padStart(8, '0')}.`, { offset: block.headerOffset, structure: 'CFDATA.csum' } ); } } function checkedAdd( left: number, right: number, code: string, structure: string ): number { const sum = left + right; if (!Number.isSafeInteger(sum)) { fail(code, `Integer overflow while reading ${structure}.`, { structure }); } return sum; } function readLinkedCabinetString( reader: BinaryReader, limits: OnePkgLimits, structure: string, allowEmpty: boolean ): string { const offset = reader.offset; const bytes = reader.readNullTerminatedBytes(limits.maxNameBytes, structure); if (!allowEmpty && bytes.length === 0) { fail('cab-linked-name-empty', 'Linked cabinet name cannot be empty.', { offset, structure, }); } try { return STRICT_UTF8.decode(bytes).normalize('NFC'); } catch (error) { fail( 'cab-linked-name-invalid-utf8', `Linked cabinet metadata is not valid UTF-8${error instanceof Error ? `: ${error.message}` : '.'}`, { offset, structure } ); } } function parseCompression( raw: number, limits: OnePkgLimits, offset: number ): CabCompressionMethod { const kind = raw & 0x000f; if (kind === 0) { if (raw !== 0) { fail( 'cab-invalid-compression', `Invalid uncompressed type 0x${raw.toString(16)}.`, { offset, structure: 'CFFOLDER.typeCompress', } ); } return { kind: 'none', raw, label: 'None' }; } if (kind === 1) { if (raw !== 1) { fail( 'cab-invalid-compression', `Invalid MSZIP type 0x${raw.toString(16)}.`, { offset, structure: 'CFFOLDER.typeCompress', } ); } return { kind: 'mszip', raw, label: 'MSZIP' }; } if (kind === 2) { const level = (raw & 0x00f0) >>> 4; const memoryBits = (raw & 0x1f00) >>> 8; if ( (raw & ~0x1ff2) !== 0 || level < 1 || level > 7 || memoryBits < 10 || memoryBits > 21 ) { fail( 'cab-invalid-compression', `Invalid Quantum type 0x${raw.toString(16)}.`, { offset, structure: 'CFFOLDER.typeCompress', } ); } const windowBytes = 2 ** memoryBits; if (windowBytes > limits.maxQuantumWindowBytes) { fail( 'cab-quantum-window-limit', `Quantum window ${windowBytes} exceeds the configured ${limits.maxQuantumWindowBytes}-byte limit.`, { offset, structure: 'CFFOLDER.typeCompress' } ); } return { kind: 'quantum', raw, label: 'Quantum', level, memoryBits, windowBytes, }; } if (kind === 3) { if ((raw & ~0x1f0f) !== 0) { fail( 'cab-invalid-compression', `Invalid LZX type 0x${raw.toString(16)}.`, { offset, structure: 'CFFOLDER.typeCompress', } ); } const windowBits = (raw & 0x1f00) >>> 8; if (windowBits < 15 || windowBits > 25) { fail('cab-invalid-lzx-window', `Invalid LZX window 2^${windowBits}.`, { offset, structure: 'CFFOLDER.typeCompress', }); } const windowBytes = 2 ** windowBits; if (windowBytes > limits.maxLzxWindowBytes) { fail( 'cab-lzx-window-limit', `LZX window ${windowBytes} exceeds the configured ${limits.maxLzxWindowBytes}-byte limit.`, { offset, structure: 'CFFOLDER.typeCompress' } ); } return { kind: 'lzx', raw, label: 'LZX', windowBits, windowBytes }; } fail( 'cab-invalid-compression', `Unknown CAB compression type 0x${raw.toString(16)}.`, { offset, structure: 'CFFOLDER.typeCompress', } ); } export function parseCabinetInternal( bytes: Uint8Array, options: CabOpenOptions, allowMultipart = false ): ParsedCabinetInternal { if (!(bytes instanceof Uint8Array)) { throw new TypeError('CAB input must be a Uint8Array'); } const limits = resolveLimits(options.limits); if (bytes.byteLength > limits.maxCabinetBytes) { fail( 'cab-input-limit', `Cabinet is ${bytes.byteLength} bytes; configured limit is ${limits.maxCabinetBytes}.`, { structure: 'CFHEADER' } ); } checkCancellation(options.cancellation); const header = new BinaryReader(bytes); if (header.readU32('CFHEADER.signature') !== CAB_SIGNATURE) { fail( 'cab-invalid-signature', 'Input does not begin with the MSCF CAB signature.', { offset: 0, structure: 'CFHEADER.signature', } ); } const reserved1 = header.readU32('CFHEADER.reserved1'); const declaredSize = header.readU32('CFHEADER.cbCabinet'); const reserved2 = header.readU32('CFHEADER.reserved2'); const firstFileOffset = header.readU32('CFHEADER.coffFiles'); const reserved3 = header.readU32('CFHEADER.reserved3'); const minorVersion = header.readU8('CFHEADER.versionMinor'); const majorVersion = header.readU8('CFHEADER.versionMajor'); const folderCount = header.readU16('CFHEADER.cFolders'); const fileCount = header.readU16('CFHEADER.cFiles'); const flags = header.readU16('CFHEADER.flags'); const cabinetSetId = header.readU16('CFHEADER.setID'); const cabinetSetIndex = header.readU16('CFHEADER.iCabinet'); if (declaredSize < 36 || declaredSize > bytes.byteLength) { fail( 'cab-invalid-size', `Declared cabinet size ${declaredSize} is outside the available ${bytes.byteLength} bytes.`, { offset: 8, structure: 'CFHEADER.cbCabinet' } ); } if (declaredSize > limits.maxCabinetBytes) { fail( 'cab-input-limit', 'Declared cabinet size exceeds the configured limit.', { offset: 8, structure: 'CFHEADER.cbCabinet', } ); } if (majorVersion > 1 || (majorVersion === 1 && minorVersion > 3)) { fail( 'cab-version-unsupported', `CAB version ${majorVersion}.${minorVersion} is not supported.`, { offset: 24, structure: 'CFHEADER.version' } ); } if ((flags & ~0x0007) !== 0) { fail( 'cab-invalid-flags', `Unknown CAB header flags 0x${flags.toString(16)}.`, { offset: 30, structure: 'CFHEADER.flags', } ); } if ( !allowMultipart && (flags & (FLAG_PREVIOUS_CABINET | FLAG_NEXT_CABINET)) !== 0 ) { fail( 'cab-multipart-unsupported', 'Multi-cabinet archives are not supported for local .onepkg files.', { offset: 30, structure: 'CFHEADER.flags' } ); } if (folderCount > limits.maxFolders || fileCount > limits.maxFiles) { fail( 'cab-entry-count-limit', `Cabinet contains ${folderCount} folders and ${fileCount} files, exceeding configured limits.`, { structure: 'CFHEADER' } ); } // Re-bind reads to the trusted cbCabinet boundary before parsing variable data. const reader = new BinaryReader(bytes, header.offset, declaredSize); let headerReserveSize = 0; let folderReserveSize = 0; let dataReserveSize = 0; let headerReserve: Uint8Array = new Uint8Array(0); if ((flags & FLAG_RESERVE_PRESENT) !== 0) { headerReserveSize = reader.readU16('CFHEADER.cbCFHeader'); folderReserveSize = reader.readU8('CFHEADER.cbCFFolder'); dataReserveSize = reader.readU8('CFHEADER.cbCFData'); if (headerReserveSize > 60_000) { fail( 'cab-header-reserve-limit', 'CAB header reserve area exceeds the 60,000-byte format limit.', { offset: header.offset, structure: 'CFHEADER.cbCFHeader' } ); } headerReserve = reader.readBytes(headerReserveSize, 'CFHEADER.abReserve'); } const previousCabinetName = (flags & FLAG_PREVIOUS_CABINET) !== 0 ? readLinkedCabinetString(reader, limits, 'CFHEADER.szCabinetPrev', false) : undefined; const previousDiskName = (flags & FLAG_PREVIOUS_CABINET) !== 0 ? readLinkedCabinetString(reader, limits, 'CFHEADER.szDiskPrev', true) : undefined; const nextCabinetName = (flags & FLAG_NEXT_CABINET) !== 0 ? readLinkedCabinetString(reader, limits, 'CFHEADER.szCabinetNext', false) : undefined; const nextDiskName = (flags & FLAG_NEXT_CABINET) !== 0 ? readLinkedCabinetString(reader, limits, 'CFHEADER.szDiskNext', true) : undefined; const folders: CabFolderInternal[] = []; let totalDataBlocks = 0; for (let index = 0; index < folderCount; index += 1) { if ((index & 0xff) === 0) checkCancellation(options.cancellation); const folderOffset = reader.offset; const firstDataBlockOffset = reader.readU32('CFFOLDER.coffCabStart'); const dataBlockCount = reader.readU16('CFFOLDER.cCFData'); const compressionRawOffset = reader.offset; const compression = parseCompression( reader.readU16('CFFOLDER.typeCompress'), limits, compressionRawOffset ); const reserve = reader.readBytes(folderReserveSize, 'CFFOLDER.abReserve'); totalDataBlocks = checkedAdd( totalDataBlocks, dataBlockCount, 'cab-data-block-count-overflow', 'CFFOLDER.cCFData' ); if (totalDataBlocks > limits.maxDataBlocks) { fail( 'cab-data-block-count-limit', `Cabinet exceeds the configured ${limits.maxDataBlocks}-block limit.`, { offset: folderOffset, structure: 'CFFOLDER.cCFData' } ); } folders.push({ index, firstDataBlockOffset, dataBlockCount, compression, reserve, blocks: [], compressedSize: 0, uncompressedSize: 0, dataEnd: firstDataBlockOffset, }); } if (firstFileOffset < reader.offset || firstFileOffset >= declaredSize) { fail('cab-invalid-file-table-offset', 'CAB file table offset is invalid.', { offset: 16, structure: 'CFHEADER.coffFiles', }); } const fileReader = new BinaryReader(bytes, firstFileOffset, declaredSize); const entries: CabFileEntry[] = []; const fileRecords: CabFileRecordInternal[] = []; const diagnostics = []; const duplicatePaths = new Set(); let extractedByteTotal = 0; for (let index = 0; index < fileCount; index += 1) { if ((index & 0xff) === 0) checkCancellation(options.cancellation); const entryOffset = fileReader.offset; const uncompressedSize = fileReader.readU32('CFFILE.cbFile'); const uncompressedOffset = fileReader.readU32('CFFILE.uoffFolderStart'); const rawFolderIndex = fileReader.readU16('CFFILE.iFolder'); const date = fileReader.readU16('CFFILE.date'); const time = fileReader.readU16('CFFILE.time'); const attributes = fileReader.readU16('CFFILE.attribs'); const nameOffset = fileReader.offset; const nameBytes = fileReader.readNullTerminatedBytes( limits.maxNameBytes, 'CFFILE.szName' ); let folderIndex = rawFolderIndex; if (rawFolderIndex >= FOLDER_CONTINUED_FROM_PREVIOUS) { if (!allowMultipart) { fail( 'cab-multipart-file-unsupported', 'CAB file spans another cabinet; multi-cabinet files are unsupported.', { offset: entryOffset + 8, structure: 'CFFILE.iFolder' } ); } const continuesFromPrevious = rawFolderIndex === FOLDER_CONTINUED_FROM_PREVIOUS || rawFolderIndex === FOLDER_CONTINUED_PREVIOUS_AND_NEXT; const continuesToNext = rawFolderIndex === FOLDER_CONTINUED_TO_NEXT || rawFolderIndex === FOLDER_CONTINUED_PREVIOUS_AND_NEXT; if ( (continuesFromPrevious && (flags & FLAG_PREVIOUS_CABINET) === 0) || (continuesToNext && (flags & FLAG_NEXT_CABINET) === 0) ) { fail( 'cab-spanning-file-flag-mismatch', 'CAB spanning file marker does not match its prev/next header flags.', { offset: entryOffset + 8, structure: 'CFFILE.iFolder' } ); } if ( rawFolderIndex === FOLDER_CONTINUED_PREVIOUS_AND_NEXT && folders.length !== 1 ) { fail( 'cab-spanning-folder-ambiguous', 'A folder continued from both cabinets must be the only folder in this cabinet.', { offset: entryOffset + 8, structure: 'CFFILE.iFolder' } ); } folderIndex = continuesFromPrevious ? 0 : folders.length - 1; } const folder = folders[folderIndex]; if (folder === undefined) { fail( 'cab-folder-index-out-of-range', 'CAB file folder index is out of range.', { offset: entryOffset + 8, structure: 'CFFILE.iFolder', } ); } if (uncompressedSize > limits.maxFileBytes) { fail( 'cab-file-size-limit', `CAB file size ${uncompressedSize} exceeds the configured ${limits.maxFileBytes}-byte limit.`, { offset: entryOffset, structure: 'CFFILE.cbFile' } ); } if (!allowMultipart) { extractedByteTotal = checkedAdd( extractedByteTotal, uncompressedSize, 'cab-extracted-size-overflow', 'CFFILE.cbFile' ); if (extractedByteTotal > limits.maxExtractedBytes) { fail( 'cab-extracted-size-limit', `Extracted files exceed the configured ${limits.maxExtractedBytes}-byte limit.`, { offset: entryOffset, structure: 'CFFILE.cbFile' } ); } } const path = decodeCabFileName( nameBytes, (attributes & ATTRIBUTE_NAME_IS_UTF8) !== 0, nameOffset ); if ( (attributes & ATTRIBUTE_NAME_IS_UTF8) === 0 && nameBytes.some((byte) => byte > 0x7f) ) { diagnostics.push( warning( 'cab-unmarked-utf8-name', `Decoded unmarked non-ASCII CAB filename as strict UTF-8: ${path}`, { offset: nameOffset, structure: 'CFFILE.szName' } ) ); } const normalized = normalizeCabPath(path, limits, nameOffset); if (duplicatePaths.has(normalized.duplicateKey)) { fail( 'cab-duplicate-path', `CAB contains a duplicate normalized path: ${normalized.normalizedPath}`, { offset: nameOffset, structure: 'CFFILE.szName' } ); } duplicatePaths.add(normalized.duplicateKey); const entry: CabFileEntry = { index, path: normalized.path, normalizedPath: normalized.normalizedPath, folderIndex, uncompressedOffset, uncompressedSize, attributes, compression: folder.compression, }; entries.push(entry); fileRecords.push({ entry, rawFolderIndex, duplicateKey: normalized.duplicateKey, date, time, entryOffset, }); } const metadataEnd = fileReader.offset; let totalFolderOutput = 0; let totalLzxOutput = 0; let totalQuantumOutput = 0; for (const folder of folders) { const folderContinuesToNext = fileRecords.some( (record) => record.entry.folderIndex === folder.index && (record.rawFolderIndex === FOLDER_CONTINUED_TO_NEXT || record.rawFolderIndex === FOLDER_CONTINUED_PREVIOUS_AND_NEXT) ); if (folder.firstDataBlockOffset > declaredSize) { fail( 'cab-invalid-data-offset', 'CAB folder data offset is beyond the declared cabinet.', { offset: folder.firstDataBlockOffset, structure: 'CFFOLDER.coffCabStart', } ); } if ( folder.dataBlockCount > 0 && folder.firstDataBlockOffset < metadataEnd ) { fail( 'cab-data-overlaps-metadata', 'CAB data blocks overlap the file table.', { offset: folder.firstDataBlockOffset, structure: 'CFFOLDER.coffCabStart', } ); } const blockReader = new BinaryReader( bytes, folder.firstDataBlockOffset, declaredSize ); for ( let blockIndex = 0; blockIndex < folder.dataBlockCount; blockIndex += 1 ) { if ((blockIndex & 0xff) === 0) checkCancellation(options.cancellation); const headerOffset = blockReader.offset; const checksum = blockReader.readU32('CFDATA.csum'); const compressedSize = blockReader.readU16('CFDATA.cbData'); const uncompressedSize = blockReader.readU16('CFDATA.cbUncomp'); const reserve = blockReader.readBytes( dataReserveSize, 'CFDATA.abReserve' ); const dataOffset = blockReader.offset; blockReader.skip(compressedSize, 'CFDATA.ab'); if (compressedSize === 0 || uncompressedSize > MAX_CAB_BLOCK_OUTPUT) { fail( 'cab-invalid-data-block-size', `Invalid CAB data block sizes ${compressedSize}/${uncompressedSize}.`, { offset: headerOffset + 4, structure: 'CFDATA' } ); } if ( uncompressedSize === 0 && (!allowMultipart || blockIndex !== folder.dataBlockCount - 1 || !folderContinuesToNext) ) { fail( 'cab-invalid-split-data-block', 'A zero-output CFDATA fragment must be the final block of a folder continued in the next cabinet.', { offset: headerOffset + 6, structure: 'CFDATA.cbUncomp' } ); } if (compressedSize > MAX_CAB_BLOCK_INPUT) { fail( 'cab-invalid-compressed-block-size', `CAB data block size ${compressedSize} exceeds ${MAX_CAB_BLOCK_INPUT} bytes.`, { offset: headerOffset + 4, structure: 'CFDATA.cbData' } ); } if ( folder.compression.kind === 'mszip' && compressedSize > MAX_MSZIP_BLOCK_INPUT ) { fail( 'cab-invalid-mszip-block-size', `MSZIP CAB data block size ${compressedSize} exceeds ${MAX_MSZIP_BLOCK_INPUT} bytes.`, { offset: headerOffset + 4, structure: 'CFDATA.cbData' } ); } if ( folder.compression.kind === 'none' && !allowMultipart && compressedSize !== uncompressedSize ) { fail( 'cab-uncompressed-size-mismatch', 'Uncompressed CAB block has different compressed and output sizes.', { offset: headerOffset + 4, structure: 'CFDATA' } ); } folder.compressedSize = checkedAdd( folder.compressedSize, compressedSize, 'cab-folder-size-overflow', 'CFDATA.cbData' ); folder.uncompressedSize = checkedAdd( folder.uncompressedSize, uncompressedSize, 'cab-folder-size-overflow', 'CFDATA.cbUncomp' ); if (folder.uncompressedSize > limits.maxFolderUncompressedBytes) { fail( 'cab-folder-size-limit', `CAB folder exceeds the configured ${limits.maxFolderUncompressedBytes}-byte output limit.`, { offset: headerOffset + 6, structure: 'CFDATA.cbUncomp' } ); } folder.blocks.push({ headerOffset, checksum, compressedSize, uncompressedSize, reserve, dataOffset, sourceBytes: bytes, }); } folder.dataEnd = blockReader.offset; if ( !allowMultipart && folder.uncompressedSize / folder.compressedSize > limits.maxCompressionRatio ) { fail( 'cab-compression-ratio-limit', `CAB folder compression ratio exceeds the configured ${limits.maxCompressionRatio}:1 limit.`, { offset: folder.firstDataBlockOffset, structure: 'CFFOLDER/CFDATA' } ); } totalFolderOutput = checkedAdd( totalFolderOutput, folder.uncompressedSize, 'cab-total-size-overflow', 'CFDATA.cbUncomp' ); if (folder.compression.kind === 'lzx') { totalLzxOutput = checkedAdd( totalLzxOutput, folder.uncompressedSize, 'lzx-operation-limit', 'CFDATA.cbUncomp' ); } if (folder.compression.kind === 'quantum') { totalQuantumOutput = checkedAdd( totalQuantumOutput, folder.uncompressedSize, 'quantum-operation-limit', 'CFDATA.cbUncomp' ); } } if (totalFolderOutput > limits.maxTotalUncompressedBytes) { fail( 'cab-total-size-limit', `CAB output exceeds the configured ${limits.maxTotalUncompressedBytes}-byte limit.`, { structure: 'CFDATA.cbUncomp' } ); } if (totalLzxOutput > limits.maxDecodeOperations) { fail( 'lzx-operation-limit', `LZX output exceeds the configured ${limits.maxDecodeOperations}-operation limit.`, { structure: 'CFDATA.cbUncomp' } ); } if (totalQuantumOutput > limits.maxDecodeOperations) { fail( 'quantum-operation-limit', `Quantum output exceeds the configured ${limits.maxDecodeOperations}-operation limit.`, { structure: 'CFDATA.cbUncomp' } ); } const dataRanges = folders .filter((folder) => folder.dataBlockCount > 0) .map((folder) => ({ start: folder.firstDataBlockOffset, end: folder.dataEnd, })) .sort((left, right) => left.start - right.start); for (let index = 1; index < dataRanges.length; index += 1) { if (dataRanges[index]!.start < dataRanges[index - 1]!.end) { fail('cab-overlapping-folders', 'CAB folder data ranges overlap.', { offset: dataRanges[index]!.start, structure: 'CFFOLDER.coffCabStart', }); } } if (!allowMultipart) { for (const entry of entries) { const folder = folders[entry.folderIndex]!; const fileEnd = checkedAdd( entry.uncompressedOffset, entry.uncompressedSize, 'cab-file-range-overflow', 'CFFILE' ); if (fileEnd > folder.uncompressedSize) { fail( 'cab-file-range-out-of-bounds', `CAB file ${entry.normalizedPath} extends beyond its folder data.`, { structure: 'CFFILE' } ); } } } if (bytes.byteLength > declaredSize) { diagnostics.push( warning( 'cab-trailing-data', `Ignored ${bytes.byteLength - declaredSize} bytes after the declared cabinet.`, { offset: declaredSize, structure: 'CFHEADER.cbCabinet' } ) ); } if (reserved1 !== 0 || reserved2 !== 0 || reserved3 !== 0) { diagnostics.push( warning( 'cab-reserved-header-data', 'CAB header contains non-zero reserved fields.', { structure: 'CFHEADER' } ) ); } return { bytes, limits, flags, previousCabinetName, previousDiskName, nextCabinetName, nextDiskName, headerReserveSize, folderReserveSize, dataReserveSize, headerReserve, folders, fileRecords, result: { cabinetSetId, cabinetSetIndex, declaredSize, entries, folders: folders.map((folder) => ({ index: folder.index, dataBlockCount: folder.dataBlockCount, compressedSize: folder.compressedSize, uncompressedSize: folder.uncompressedSize, compression: folder.compression, })), diagnostics, }, }; } export function enumerateCabinet( bytes: Uint8Array, options: CabOpenOptions = {} ): CabEnumerationResult { return parseCabinetInternal(bytes, options).result; } export async function extractCabinet( bytes: Uint8Array, options: CabOpenOptions = {} ): Promise { const parsed = parseCabinetInternal(bytes, options); const byFolder = new Map(); for (const entry of parsed.result.entries) { const current = byFolder.get(entry.folderIndex); if (current === undefined) byFolder.set(entry.folderIndex, [entry]); else current.push(entry); } const extractedEntries = []; for (const folder of parsed.folders) { checkCancellation(options.cancellation); const folderOutput = new Uint8Array(folder.uncompressedSize); const decoder = folder.compression.kind === 'lzx' ? new LzxDecoder(folder.compression.windowBits, { maxOperations: parsed.limits.maxDecodeOperations, checkCancellation: () => checkCancellation(options.cancellation), }) : undefined; const msZipDecoder = folder.compression.kind === 'mszip' ? new MsZipDecoder() : undefined; const quantumDecoder = folder.compression.kind === 'quantum' ? new QuantumDecoder(folder.compression.memoryBits, { maxOperations: parsed.limits.maxDecodeOperations, checkCancellation: () => checkCancellation(options.cancellation), }) : undefined; let quantumQueuedBlock = -1; let outputOffset = 0; for (const [blockIndex, block] of folder.blocks.entries()) { checkCancellation(options.cancellation); if (quantumDecoder !== undefined) { const lookahead = Math.min(blockIndex + 1, folder.blocks.length - 1); while (quantumQueuedBlock < lookahead) { quantumQueuedBlock += 1; const queued = folder.blocks[quantumQueuedBlock]!; verifyDataBlockChecksumInternal(parsed.bytes, queued); quantumDecoder.appendCabBlock( parsed.bytes.subarray( queued.dataOffset, queued.dataOffset + queued.compressedSize ), quantumQueuedBlock === folder.blocks.length - 1 ); } } const compressed = parsed.bytes.subarray( block.dataOffset, block.dataOffset + block.compressedSize ); if (quantumDecoder === undefined) { verifyDataBlockChecksumInternal(parsed.bytes, block); } const output = decoder !== undefined ? decoder.decompressNext(compressed, block.uncompressedSize) : msZipDecoder !== undefined ? msZipDecoder.decompressNext(compressed, block.uncompressedSize) : quantumDecoder !== undefined ? quantumDecoder.decompressNext( block.uncompressedSize, blockIndex === folder.blocks.length - 1 ) : compressed; if (output.length !== block.uncompressedSize) { fail( 'cab-output-size-mismatch', `CAB decoder returned ${output.length} bytes; expected ${block.uncompressedSize}.`, { offset: block.headerOffset + 6, structure: 'CFDATA.cbUncomp' } ); } folderOutput.set(output, outputOffset); outputOffset += output.length; await yieldForCancellation(options.cancellation); } decoder?.finish(); if (outputOffset !== folderOutput.length) { fail( 'cab-folder-output-size-mismatch', 'CAB folder output is incomplete.', { structure: `CFFOLDER[${folder.index}]`, } ); } for (const entry of byFolder.get(folder.index) ?? []) { const start = entry.uncompressedOffset; extractedEntries.push({ ...entry, bytes: folderOutput.slice(start, start + entry.uncompressedSize), }); } } extractedEntries.sort((left, right) => left.index - right.index); return { ...parsed.result, extractedEntries }; }