Files
onenote-tools/src/onenote/onepkg/cabinet.ts

956 lines
30 KiB
TypeScript

/*
* 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<string>();
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<CabExtractionResult> {
const parsed = parseCabinetInternal(bytes, options);
const byFolder = new Map<number, CabFileEntry[]>();
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 };
}