feat: add local OneNote and ONEPKG reader
This commit is contained in:
727
src/onenote/onepkg/cabinet.ts
Normal file
727
src/onenote/onepkg/cabinet.ts
Normal file
@@ -0,0 +1,727 @@
|
||||
/*
|
||||
* 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 { decodeCabFileName, normalizeCabPath } from './path.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;
|
||||
const FLAG_RESERVE_PRESENT = 0x0004;
|
||||
const ATTRIBUTE_NAME_IS_UTF8 = 0x0080;
|
||||
const MAX_CAB_BLOCK_OUTPUT = 32 * 1024;
|
||||
|
||||
interface CabDataBlock {
|
||||
headerOffset: number;
|
||||
checksum: number;
|
||||
compressedSize: number;
|
||||
uncompressedSize: number;
|
||||
reserve: Uint8Array;
|
||||
dataOffset: number;
|
||||
}
|
||||
|
||||
interface CabFolderInternal {
|
||||
index: number;
|
||||
firstDataBlockOffset: number;
|
||||
dataBlockCount: number;
|
||||
compression: CabCompressionMethod;
|
||||
blocks: CabDataBlock[];
|
||||
compressedSize: number;
|
||||
uncompressedSize: number;
|
||||
dataEnd: number;
|
||||
}
|
||||
|
||||
interface ParsedCabinet {
|
||||
bytes: Uint8Array;
|
||||
limits: OnePkgLimits;
|
||||
folders: CabFolderInternal[];
|
||||
result: CabEnumerationResult;
|
||||
}
|
||||
|
||||
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 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 (level < 1 || level > 7 || memoryBits < 10 || memoryBits > 21) {
|
||||
fail(
|
||||
'cab-invalid-compression',
|
||||
`Invalid Quantum type 0x${raw.toString(16)}.`,
|
||||
{
|
||||
offset,
|
||||
structure: 'CFFOLDER.typeCompress',
|
||||
}
|
||||
);
|
||||
}
|
||||
return { kind: 'quantum', raw, label: 'Quantum', level, memoryBits };
|
||||
}
|
||||
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',
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function parseCabinet(
|
||||
bytes: Uint8Array,
|
||||
options: CabOpenOptions
|
||||
): ParsedCabinet {
|
||||
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 ((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 folderReserveSize = 0;
|
||||
let dataReserveSize = 0;
|
||||
if ((flags & FLAG_RESERVE_PRESENT) !== 0) {
|
||||
const headerReserveSize = reader.readU16('CFHEADER.cbCFHeader');
|
||||
folderReserveSize = reader.readU8('CFHEADER.cbCFFolder');
|
||||
dataReserveSize = reader.readU8('CFHEADER.cbCFData');
|
||||
reader.skip(headerReserveSize, 'CFHEADER.abReserve');
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
reader.skip(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,
|
||||
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 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 folderIndex = fileReader.readU16('CFFILE.iFolder');
|
||||
fileReader.readU16('CFFILE.date');
|
||||
fileReader.readU16('CFFILE.time');
|
||||
const attributes = fileReader.readU16('CFFILE.attribs');
|
||||
const nameOffset = fileReader.offset;
|
||||
const nameBytes = fileReader.readNullTerminatedBytes(
|
||||
limits.maxNameBytes,
|
||||
'CFFILE.szName'
|
||||
);
|
||||
|
||||
if (folderIndex >= 0xfffd) {
|
||||
fail(
|
||||
'cab-multipart-file-unsupported',
|
||||
'CAB file spans another cabinet; multi-cabinet files are unsupported.',
|
||||
{ offset: entryOffset + 8, structure: 'CFFILE.iFolder' }
|
||||
);
|
||||
}
|
||||
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' }
|
||||
);
|
||||
}
|
||||
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);
|
||||
entries.push({
|
||||
index,
|
||||
path: normalized.path,
|
||||
normalizedPath: normalized.normalizedPath,
|
||||
folderIndex,
|
||||
uncompressedOffset,
|
||||
uncompressedSize,
|
||||
attributes,
|
||||
compression: folder.compression,
|
||||
});
|
||||
}
|
||||
const metadataEnd = fileReader.offset;
|
||||
|
||||
let totalFolderOutput = 0;
|
||||
let totalLzxOutput = 0;
|
||||
for (const folder of folders) {
|
||||
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 === 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 (
|
||||
folder.compression.kind === 'none' &&
|
||||
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,
|
||||
});
|
||||
}
|
||||
folder.dataEnd = blockReader.offset;
|
||||
if (
|
||||
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 (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' }
|
||||
);
|
||||
}
|
||||
|
||||
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',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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' }
|
||||
)
|
||||
);
|
||||
}
|
||||
for (const folder of folders) {
|
||||
if (
|
||||
folder.compression.kind === 'mszip' ||
|
||||
folder.compression.kind === 'quantum'
|
||||
) {
|
||||
diagnostics.push(
|
||||
warning(
|
||||
'cab-compression-unsupported',
|
||||
`${folder.compression.label} metadata can be listed, but this build cannot extract it.`,
|
||||
{ structure: `CFFOLDER[${folder.index}].typeCompress` }
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
bytes,
|
||||
limits,
|
||||
folders,
|
||||
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 parseCabinet(bytes, options).result;
|
||||
}
|
||||
|
||||
export async function extractCabinet(
|
||||
bytes: Uint8Array,
|
||||
options: CabOpenOptions = {}
|
||||
): Promise<CabExtractionResult> {
|
||||
const parsed = parseCabinet(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);
|
||||
if (
|
||||
folder.compression.kind === 'mszip' ||
|
||||
folder.compression.kind === 'quantum'
|
||||
) {
|
||||
fail(
|
||||
'cab-compression-unsupported',
|
||||
`${folder.compression.label} extraction is not implemented.`,
|
||||
{ structure: `CFFOLDER[${folder.index}].typeCompress` }
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
let outputOffset = 0;
|
||||
for (const block of folder.blocks) {
|
||||
checkCancellation(options.cancellation);
|
||||
const compressed = parsed.bytes.subarray(
|
||||
block.dataOffset,
|
||||
block.dataOffset + block.compressedSize
|
||||
);
|
||||
if (block.checksum !== 0) {
|
||||
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' }
|
||||
);
|
||||
}
|
||||
}
|
||||
const output =
|
||||
decoder === undefined
|
||||
? compressed
|
||||
: decoder.decompressNext(compressed, block.uncompressedSize);
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user