@phi-ag/rvt
Version:
Parse Revit file format
184 lines (183 loc) • 8.03 kB
JavaScript
import { Boundaries } from "./boundaries.js";
import { parseDirectory } from "./directory.js";
import { maxFatSectorsHeader, parseHeader } from "./header.js";
const endOfChain = 0xfffffffe;
const fatSectorChain = (name, fat, start, length) => {
const sectors = new Uint32Array(length);
for (let i = 0, current = start;; i++) {
if (current === endOfChain) {
if (i !== length)
throw Error(`${name} unexpected end of fat chain (${i} !== ${length})`);
break;
}
if (i >= length)
throw Error(`${name} fat chain too long (${i} >= ${length})`);
sectors[i] = current;
if (current >= fat.length)
throw Error(`${name} fat chain out-of-bounds (${current} >= ${fat.length})`);
current = fat[current];
}
return sectors;
};
const directorySectorChain = (header, fat) => fatSectorChain("Directory", fat, header.directoryStart, header.directorySectorCount);
const miniFatSectorChain = (header, fat) => fatSectorChain("MiniFat", fat, header.miniFatStart, header.miniFatSectorCount);
const miniStreamSectorChain = (header, fat, root) => {
const sectorCount = (root.size + header.sectorSize - 1) >> header.sectorSizeBits;
return fatSectorChain("MiniStream", fat, root.start, sectorCount);
};
const sectorBounds = (header, sectors, byteLength) => {
const bounds = new Boundaries();
const sectorSize = header.sectorSize;
for (let i = 0; i < sectors.length; i++) {
const start = (sectors[i] + 1) * sectorSize;
const size = byteLength
? Math.min(byteLength - i * sectorSize, sectorSize)
: sectorSize;
const end = start + size;
bounds.add([start, end]);
}
return bounds;
};
const boundsData = async (source, bounds) => {
const data = new Uint8Array(bounds.size());
for (const [start, end, offset] of bounds.items) {
const chunk = await source.sliceBytes(start, end);
data.set(chunk, offset);
}
return data;
};
const boundsEntries = async (source, bounds) => {
const entries = new Uint32Array(bounds.size() >> 2);
for (const [start, end, offset] of bounds.items) {
const view = await source.sliceView(start, end);
const chunkEntries = view.byteLength >> 2;
const chunkOffset = offset >> 2;
for (let i = 0; i < chunkEntries; i++) {
entries[i + chunkOffset] = view.getUint32(i * 4, true);
}
}
return entries;
};
const createDirectory = async (source, header, fat) => {
const sectors = directorySectorChain(header, fat);
const bounds = sectorBounds(header, sectors);
const data = await boundsData(source, bounds);
return parseDirectory(header, data);
};
const addDiFatEntries = async (source, header, fatSectors) => {
for (let i = 0, current = header.diFatStart;; i++) {
if (current === endOfChain) {
if (i !== header.diFatCount)
throw Error(`Unexpected end of diFat chain (${i} !== ${header.diFatCount})`);
break;
}
const diFatStart = (current + 1) * header.sectorSize;
const diFatEnd = diFatStart + header.sectorSize;
const diFat = await source.sliceView(diFatStart, diFatEnd);
const max = header.maxSectorEntries - 1;
const offset = i * max + maxFatSectorsHeader;
const remaining = Math.min(header.fatSectorCount - offset, max);
if (remaining < 0)
throw Error(`Unexpected remaining sectors in diFat chain (${remaining})`);
for (let j = 0; j < remaining; j++) {
fatSectors[j + offset] = diFat.getUint32(j * 4, true);
}
current = diFat.getUint32(max * 4, true);
}
};
const createFat = async (source, header, fatSectors) => {
const fatBounds = sectorBounds(header, fatSectors);
return await boundsEntries(source, fatBounds);
};
const createMiniFat = async (source, header, fat, root) => {
const miniFatSectors = miniFatSectorChain(header, fat);
const miniFatCount = (root.size + (1 << header.miniSectorSizeBits) - 1) >> header.miniSectorSizeBits;
const miniFatBounds = sectorBounds(header, miniFatSectors, miniFatCount * 4);
return await boundsEntries(source, miniFatBounds);
};
export class Cfb {
#source;
#header;
#directory;
#fat;
#miniFat;
#miniStreamSectors;
constructor(source, header, directory, fat, miniFat, miniStreamSectors) {
this.#source = source;
this.#header = header;
this.#directory = directory;
this.#fat = fat;
this.#miniFat = miniFat;
this.#miniStreamSectors = miniStreamSectors;
}
static initialize = async (source) => {
if (source.size < 512)
throw Error(`Compound file too small (${source.size})`);
const [header, fatSectors] = parseHeader(await source.sliceBytes(0, 512));
await addDiFatEntries(source, header, fatSectors);
const fat = await createFat(source, header, fatSectors);
const directory = await createDirectory(source, header, fat);
const root = directory.find((entry) => entry.type === 5);
if (!root)
throw Error("Compound file root not found");
const miniFat = await createMiniFat(source, header, fat, root);
const miniStreamSectors = miniStreamSectorChain(header, fat, root);
return new Cfb(source, header, directory, fat, miniFat, miniStreamSectors);
};
findEntry = (name) => this.#directory.find((entry) => entry.name === name);
fatBounds = (start, size) => {
const bounds = new Boundaries();
for (let i = 0, current = start, remaining = size;; i++) {
if (current === endOfChain) {
if (remaining !== 0)
throw Error("Fat bounds unexpected end of chain");
break;
}
const start = (current + 1) * this.#header.sectorSize;
const size = Math.min(remaining, this.#header.sectorSize);
remaining -= size;
bounds.add([start, start + size]);
if (current >= this.#fat.length)
throw Error(`Fat out-of-bounds (${current})`);
current = this.#fat[current];
}
return bounds;
};
miniStreamOffset = (start) => {
const subBits = this.#header.sectorSizeBits - this.#header.miniSectorSizeBits;
const sector = start >> subBits;
if (sector >= this.#miniStreamSectors.length)
throw Error(`MiniStream sector out-of-bounds (${sector})`);
const offset = ((this.#miniStreamSectors[sector] + 1) << subBits) + (start & ((1 << subBits) - 1));
return offset * this.#header.miniSectorSize;
};
miniStreamBounds = (start, size) => {
const bounds = new Boundaries();
for (let i = 0, current = start, remaining = size;; i++) {
if (current === endOfChain) {
if (remaining !== 0)
throw Error("MiniStream bounds unexpected end of chain");
break;
}
const start = this.miniStreamOffset(current);
const miniSectorSize = this.#header.miniSectorSize;
const size = remaining < miniSectorSize ? remaining : miniSectorSize;
remaining -= size;
bounds.add([start, start + size]);
if (current >= this.#miniFat.length)
throw Error(`MiniStream out-of-bounds (${current})`);
current = this.#miniFat[current];
}
return bounds;
};
miniStreamData = async (start, size) => {
const bounds = this.miniStreamBounds(start, size);
return await boundsData(this.#source, bounds);
};
entryData = async (entry) => {
if (entry.size < this.#header.miniFatCutoff)
return await this.miniStreamData(entry.start, entry.size);
const bounds = this.fatBounds(entry.start, entry.size);
return await boundsData(this.#source, bounds);
};
}