Files
poshmanager/server/services/cfbReader.js
2026-08-09 17:33:11 -05:00

124 lines
5.2 KiB
JavaScript

// Minimal reader for the OLE2 / Compound File Binary (CFB) container format —
// the on-disk shape of an MSI database (and legacy Office docs). Dependency-free
// and read-only: it walks the FAT / mini-FAT and directory to expose each
// stream as a Buffer keyed by its (decoded) directory name. Just enough of
// [MS-CFB] to pull MSI tables; it does not write or modify compound files.
const SIG = Buffer.from([0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1]);
const ENDOFCHAIN = 0xfffffffe;
const FREESECT = 0xffffffff;
const MAX_CHAIN = 1 << 24; // chain-length guard against malformed/looping files
export function isCompoundFile(buffer) {
return Buffer.isBuffer(buffer) && buffer.length >= 512 && buffer.subarray(0, 8).equals(SIG);
}
function decodeName(buffer, nameLen) {
// nameLen includes the trailing UTF-16 null terminator.
if (!nameLen || nameLen < 2) return '';
return buffer.toString('utf16le', 0, nameLen - 2);
}
function followChain(start, fat, limit) {
const chain = [];
let sector = start;
let guard = 0;
while (sector !== ENDOFCHAIN && sector !== FREESECT && sector < limit) {
chain.push(sector);
if (++guard > MAX_CHAIN) throw new Error('CFB sector chain too long (malformed file)');
sector = fat[sector];
if (sector == null) break;
}
return chain;
}
export function readCompoundFile(buffer) {
if (!isCompoundFile(buffer)) throw new Error('Not a compound file (bad OLE2 signature)');
const sectorShift = buffer.readUInt16LE(30);
const miniSectorShift = buffer.readUInt16LE(32);
const sectorSize = 1 << sectorShift;
const miniSectorSize = 1 << miniSectorShift;
const numFatSectors = buffer.readUInt32LE(44);
const firstDirSector = buffer.readUInt32LE(48);
const miniCutoff = buffer.readUInt32LE(56);
const firstMiniFatSector = buffer.readUInt32LE(60);
const firstDifatSector = buffer.readUInt32LE(68);
const numDifatSectors = buffer.readUInt32LE(72);
const sectorOffset = (sector) => (sector + 1) * sectorSize;
const totalSectors = Math.floor(buffer.length / sectorSize);
// 1) Assemble the DIFAT (locations of every FAT sector): 109 from the header,
// then the DIFAT sector chain.
const fatSectorLocations = [];
for (let i = 0; i < 109 && fatSectorLocations.length < numFatSectors; i += 1) {
const loc = buffer.readUInt32LE(76 + i * 4);
if (loc !== FREESECT) fatSectorLocations.push(loc);
}
let difatSector = firstDifatSector;
const entriesPerDifat = sectorSize / 4 - 1;
for (let s = 0; s < numDifatSectors && difatSector !== ENDOFCHAIN && difatSector !== FREESECT; s += 1) {
const base = sectorOffset(difatSector);
for (let i = 0; i < entriesPerDifat; i += 1) {
const loc = buffer.readUInt32LE(base + i * 4);
if (loc !== FREESECT && fatSectorLocations.length < numFatSectors) fatSectorLocations.push(loc);
}
difatSector = buffer.readUInt32LE(base + entriesPerDifat * 4);
}
// 2) Build the FAT: a flat next-sector table.
const entriesPerSector = sectorSize / 4;
const fat = new Uint32Array(fatSectorLocations.length * entriesPerSector);
let fi = 0;
for (const loc of fatSectorLocations) {
const base = sectorOffset(loc);
for (let i = 0; i < entriesPerSector; i += 1) fat[fi++] = buffer.readUInt32LE(base + i * 4);
}
const readChain = (start) => {
const chain = followChain(start, fat, totalSectors);
const out = Buffer.allocUnsafe(chain.length * sectorSize);
chain.forEach((sector, idx) => buffer.copy(out, idx * sectorSize, sectorOffset(sector), sectorOffset(sector) + sectorSize));
return out;
};
// 3) Directory entries.
const dirBytes = readChain(firstDirSector);
const entries = [];
for (let off = 0; off + 128 <= dirBytes.length; off += 128) {
const type = dirBytes.readUInt8(off + 66);
if (type === 0) continue; // unused slot
entries.push({
name: decodeName(dirBytes.subarray(off, off + 64), dirBytes.readUInt16LE(off + 64)),
type,
startSector: dirBytes.readUInt32LE(off + 116),
size: Number(dirBytes.readBigUInt64LE(off + 120))
});
}
// 4) Mini stream lives in the root entry; small streams are read from it.
const root = entries.find((e) => e.type === 5);
const miniStream = root && root.size > 0 ? readChain(root.startSector).subarray(0, root.size) : Buffer.alloc(0);
const miniFatBytes = firstMiniFatSector === ENDOFCHAIN ? Buffer.alloc(0) : readChain(firstMiniFatSector);
const miniFat = new Uint32Array(miniFatBytes.length / 4);
for (let i = 0; i < miniFat.length; i += 1) miniFat[i] = miniFatBytes.readUInt32LE(i * 4);
const readMiniChain = (start, size) => {
const chain = followChain(start, miniFat, miniStream.length / miniSectorSize + 1);
const out = Buffer.allocUnsafe(chain.length * miniSectorSize);
chain.forEach((sector, idx) => miniStream.copy(out, idx * miniSectorSize, sector * miniSectorSize, sector * miniSectorSize + miniSectorSize));
return out.subarray(0, size);
};
const streams = new Map();
for (const entry of entries) {
if (entry.type !== 2) continue; // streams only
const data = entry.size < miniCutoff
? readMiniChain(entry.startSector, entry.size)
: readChain(entry.startSector).subarray(0, entry.size);
streams.set(entry.name, data);
}
return { streams };
}