/* This file is part of the Notesnook project (https://notesnook.com/) Copyright (C) 2023 Streetwriters (Private) Limited This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ /** * A minimal Microsoft Cabinet (CAB) file extractor supporting the formats * used by OneNote .onepkg packages (NONE, MSZIP and LZX/LZXD compression). * * Based on node-mscabinet (https://github.com/jc-lab/node-mscabinet, * Apache-2.0) with LZXD support via the lzxd WebAssembly module * (https://github.com/Lonami/lzxd, MIT OR Apache-2.0). */ import { inflate } from "fflate"; import { Lzxd } from "./lzxd"; const CAB_SIGNATURE = 0x4643534d; // "MSCF" type CabHeader = { headerEnd: number; cbCabinet: number; coffFiles: number; versionMinor: number; versionMajor: number; cFolders: number; cFiles: number; flags: number; setID: number; iCabinet: number; cbCFHeader?: number; cbCFFolder?: number; cbCFData?: number; abReserve?: Uint8Array; szCabinetPrev?: string; szDiskPrev?: string; szCabinetNext?: string; szDiskNext?: string; }; type CabFolder = { coffCabStart: number; cCFData: number; typeCompress: number; abReserve?: Uint8Array; }; type CabFile = { cbFile: number; uoffFolderStart: number; iFolder: number; date: number; time: number; attribs: number; szName: string; }; export type ExtractedFile = { name: string; data: Uint8Array; }; export function isOnepkg(data: Uint8Array): boolean { return data.length >= 8 && new DataView(data.buffer, data.byteOffset, 4).getUint32(0, true) === CAB_SIGNATURE; } /** * Extracts all files contained in a CAB archive (e.g. a .onepkg file). */ /** Extracts all files from a OneNote package (.onepkg). */ export async function extractOnepkg( data: Uint8Array ): Promise { return extractCab(data); } async function extractCab(data: Uint8Array): Promise { const header = parseHeader(data); const folders = parseFolders(data, header); const files = parseFiles(data, header); // Decompress each folder and slice out the contained files. const decompressed = await Promise.all( folders.map((folder) => decompressFolder(data, folder, header)) ); const result: ExtractedFile[] = []; for (const file of files) { const folderData = decompressed[file.iFolder]; if (!folderData) continue; result.push({ name: file.szName, data: folderData.slice( file.uoffFolderStart, file.uoffFolderStart + file.cbFile ) }); } return result; } function parseHeader(data: Uint8Array): CabHeader { const view = new DataView(data.buffer, data.byteOffset, data.byteLength); if (view.getUint32(0, true) !== CAB_SIGNATURE) throw new Error(`Invalid cabinet signature.`); const header: CabHeader = { headerEnd: 36, cbCabinet: view.getUint32(8, true), coffFiles: view.getUint32(16, true), versionMinor: view.getUint8(24), versionMajor: view.getUint8(25), cFolders: view.getUint16(26, true), cFiles: view.getUint16(28, true), flags: view.getUint16(30, true), setID: view.getUint16(32, true), iCabinet: view.getUint16(34, true) }; let offset = 36; if (header.flags & 0x4) { header.cbCFHeader = view.getUint16(offset, true); offset += 2; header.cbCFFolder = view.getUint8(offset); offset += 1; header.cbCFData = view.getUint8(offset); offset += 1; header.abReserve = data.slice(offset, offset + header.cbCFHeader); offset += header.cbCFHeader; } header.headerEnd = offset; return header; } function parseFolders(data: Uint8Array, header: CabHeader): CabFolder[] { const view = new DataView(data.buffer, data.byteOffset, data.byteLength); const folders: CabFolder[] = []; let offset = header.headerEnd; for (let i = 0; i < header.cFolders; ++i) { const folder: CabFolder = { coffCabStart: view.getUint32(offset, true), cCFData: view.getUint16(offset + 4, true), typeCompress: view.getUint16(offset + 6, true) }; offset += 8; if (header.flags & 0x4) { folder.abReserve = data.slice(offset, offset + (header.cbCFFolder ?? 0)); offset += header.cbCFFolder ?? 0; } folders.push(folder); } return folders; } function parseFiles(data: Uint8Array, header: CabHeader): CabFile[] { const view = new DataView(data.buffer, data.byteOffset, data.byteLength); const files: CabFile[] = []; let offset = header.headerEnd + header.cFolders * 8 + (header.flags & 0x4 ? header.cFolders * (header.cbCFFolder ?? 0) : 0); for (let i = 0; i < header.cFiles; ++i) { const cbFile = view.getUint32(offset, true); const uoffFolderStart = view.getUint32(offset + 4, true); const iFolder = view.getUint16(offset + 8, true); const date = view.getUint16(offset + 10, true); const time = view.getUint16(offset + 12, true); const attribs = view.getUint16(offset + 14, true); offset += 16; let nameEnd = data.indexOf(0, offset); if (nameEnd === -1) nameEnd = data.length; const szName = new TextDecoder("utf-8").decode( data.slice(offset, nameEnd) ); offset = nameEnd + 1; files.push({ cbFile, uoffFolderStart, iFolder, date, time, attribs, szName }); } return files; } type CfData = { cbData: number; cbUncomp: number; abReserve?: Uint8Array; compData: Uint8Array; }; function parseCfDataBlocks( data: Uint8Array, folder: CabFolder, header: CabHeader ): CfData[] { const view = new DataView(data.buffer, data.byteOffset, data.byteLength); const blocks: CfData[] = []; let offset = folder.coffCabStart; for (let i = 0; i < folder.cCFData; ++i) { const csum = view.getUint32(offset, true); const cbData = view.getUint16(offset + 4, true); const cbUncomp = view.getUint16(offset + 6, true); offset += 8; let abReserve: Uint8Array | undefined; if (header.flags & 0x4) { abReserve = data.slice(offset, offset + (header.cbCFData ?? 0)); offset += header.cbCFData ?? 0; } const compData = data.slice(offset, offset + cbData); offset += cbData; blocks.push({ cbData, cbUncomp, abReserve, compData }); void csum; } return blocks; } async function decompressFolder( data: Uint8Array, folder: CabFolder, header: CabHeader ): Promise { const blocks = parseCfDataBlocks(data, folder, header); const compressType = folder.typeCompress & 0x00ff; const output: Uint8Array[] = []; if (compressType === 0x0) { // NONE for (const block of blocks) output.push(block.compData); } else if (compressType === 0x1) { // MSZIP for (const block of blocks) { if (block.cbData === 0) continue; const sig = block.compData[0] | (block.compData[1] << 8); if (sig !== 0x4b43) throw new Error(`MSZIP signature error: ${sig}`); const inflated = await new Promise((resolve, reject) => { inflate(block.compData.slice(2), (err, result) => { if (err) return reject(err); resolve(result); }); }); output.push(inflated); } } else if (compressType === 0x3) { // LZX / LZXD const windowSize = (folder.typeCompress & 0x1f00) >> 8; const lzxd = await Lzxd.create(toWindowSize(windowSize)); try { for (const block of blocks) { if (block.cbData === 0) continue; const decompressed = lzxd.decompressNext(block.compData); if (decompressed) output.push(decompressed); } } finally { lzxd.free(); } } else { throw new Error(`Not supported compression type = ${folder.typeCompress}`); } const totalLength = output.reduce((sum, chunk) => sum + chunk.length, 0); const result = new Uint8Array(totalLength); let offset = 0; for (const chunk of output) { result.set(chunk, offset); offset += chunk.length; } return result; } function toWindowSize(num: number): number { switch (num) { case 15: return 32768; case 16: return 65536; case 17: return 131072; case 18: return 262144; case 19: return 524288; case 20: return 1048576; case 21: return 2097152; case 22: return 4194304; case 23: return 8388608; case 24: return 16777216; case 25: return 33554432; default: throw new Error(`Invalid window size: ${num}`); } }