/** * A reader for OLE compound files ("structured storage", [MS-CFB]), the * container .mpp files use: a little file system of storages (folders) and * streams (files) inside one file, laid out in sectors chained by a FAT. * Written from the published specification; reading only. */ const SIGNATURE = [0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1]; const FREE = 0xffffffff; const END = 0xfffffffe; export interface CfbEntry { name: string; /** 1 storage, 2 stream, 5 root. */ type: number; start: number; size: number; children: Map; } export class Cfb { readonly root: CfbEntry; private sectorSize: number; private fat: Uint32Array; private miniFat: Uint32Array; private miniStream: Uint8Array; private cutoff: number; constructor(private buf: Uint8Array) { for (let i = 0; i < 8; i++) if (buf[i] !== SIGNATURE[i]) throw new Error('not an OLE compound file'); const v = new DataView(buf.buffer, buf.byteOffset, buf.byteLength); this.sectorSize = 1 << v.getUint16(0x1e, true); this.cutoff = v.getUint32(0x38, true) || 4096; // The FAT's own sectors are listed in the header (109) and then in a chain of DIFAT sectors. const fatSectors: number[] = []; for (let i = 0; i < 109; i++) { const s = v.getUint32(0x4c + i * 4, true); if (s !== FREE && s !== END) fatSectors.push(s); } let difat = v.getUint32(0x44, true); const perSector = this.sectorSize / 4; for (let guard = 0; difat !== END && difat !== FREE && guard < 100000; guard++) { const off = this.offset(difat); for (let i = 0; i < perSector - 1; i++) { const s = v.getUint32(off + i * 4, true); if (s !== FREE && s !== END) fatSectors.push(s); } difat = v.getUint32(off + (perSector - 1) * 4, true); } this.fat = new Uint32Array(fatSectors.length * perSector); fatSectors.forEach((s, i) => { const off = this.offset(s); for (let k = 0; k < perSector && off + k * 4 + 4 <= buf.length; k++) this.fat[i * perSector + k] = v.getUint32(off + k * 4, true); }); const dir = this.chain(v.getUint32(0x30, true)); const entries: (CfbEntry & { left: number; right: number; child: number })[] = []; for (let p = 0; p + 128 <= dir.length; p += 128) { const ev = new DataView(dir.buffer, dir.byteOffset + p, 128); const nameLen = Math.min(64, ev.getUint16(64, true)); let name = ''; for (let i = 0; i + 2 < nameLen; i += 2) name += String.fromCharCode(ev.getUint16(i, true)); entries.push({ name, type: ev.getUint8(66), left: ev.getInt32(68, true), right: ev.getInt32(72, true), child: ev.getInt32(76, true), start: ev.getUint32(116, true), size: ev.getUint32(120, true), children: new Map() }); } const root = entries[0]; if (!root || root.type !== 5) throw new Error('the compound file has no root entry'); // Children of a storage are a red-black tree hanging off its child pointer. const seen = new Set(); const attach = (parent: CfbEntry, id: number) => { if (id < 0 || id >= entries.length || seen.has(id)) return; seen.add(id); const e = entries[id]; parent.children.set(e.name, e); attach(parent, e.left); attach(parent, e.right); if (e.type === 1) attach(e, e.child); }; attach(root, root.child); this.root = root; this.miniStream = this.chain(root.start, root.size); const miniFatBytes = this.chain(v.getUint32(0x3c, true)); this.miniFat = new Uint32Array(miniFatBytes.length / 4); const mv = new DataView(miniFatBytes.buffer, miniFatBytes.byteOffset, miniFatBytes.byteLength); for (let i = 0; i < this.miniFat.length; i++) this.miniFat[i] = mv.getUint32(i * 4, true); } private offset(sector: number): number { return (sector + 1) * this.sectorSize; } /** The bytes of a FAT chain, cut to `size` when given. */ private chain(start: number, size = -1): Uint8Array { const parts: Uint8Array[] = []; let total = 0; const seen = new Set(); for (let s = start; s !== END && s !== FREE && s < this.fat.length && !seen.has(s); s = this.fat[s]) { seen.add(s); const off = this.offset(s); parts.push(this.buf.subarray(off, Math.min(off + this.sectorSize, this.buf.length))); total += this.sectorSize; if (size >= 0 && total >= size) break; } return join(parts, size >= 0 ? size : total); } private miniChain(start: number, size: number): Uint8Array { const parts: Uint8Array[] = []; let total = 0; const seen = new Set(); for (let s = start; s !== END && s !== FREE && s < this.miniFat.length && !seen.has(s) && total < size; s = this.miniFat[s]) { seen.add(s); parts.push(this.miniStream.subarray(s * 64, s * 64 + 64)); total += 64; } return join(parts, size); } /** An entry by path, e.g. get(' 114', 'TBkndTask', 'FixedData'). */ get(...path: string[]): CfbEntry | undefined { let e: CfbEntry | undefined = this.root; for (const name of path) e = e?.children.get(name); return e; } read(e: CfbEntry): Uint8Array { if (e.type !== 2) throw new Error(`${e.name} is not a stream`); return e.size < this.cutoff ? this.miniChain(e.start, e.size) : this.chain(e.start, e.size); } /** A stream by path, or null when it is not there. */ stream(...path: string[]): Uint8Array | null { const e = this.get(...path); return e && e.type === 2 ? this.read(e) : null; } } function join(parts: Uint8Array[], size: number): Uint8Array { const out = new Uint8Array(Math.max(0, size)); let at = 0; for (const p of parts) { if (at >= out.length) break; const n = Math.min(p.length, out.length - at); out.set(p.subarray(0, n), at); at += n; } return out; }