archive-codec 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -4
- package/dist/cfb/detect.cjs +18 -0
- package/dist/cfb/detect.d.cts +4 -0
- package/dist/cfb/detect.d.ts +4 -0
- package/dist/cfb/detect.js +17 -0
- package/dist/cfb/ole-package.cjs +42 -0
- package/dist/cfb/ole-package.d.cts +13 -0
- package/dist/cfb/ole-package.d.ts +13 -0
- package/dist/cfb/ole-package.js +40 -0
- package/dist/cfb/read.cjs +206 -0
- package/dist/cfb/read.d.cts +15 -0
- package/dist/cfb/read.d.ts +15 -0
- package/dist/cfb/read.js +203 -0
- package/dist/index.cjs +20 -343
- package/dist/index.d.cts +6 -61
- package/dist/index.d.ts +6 -61
- package/dist/index.js +6 -329
- package/dist/magic.cjs +9 -0
- package/dist/magic.d.cts +4 -0
- package/dist/magic.d.ts +4 -0
- package/dist/magic.js +8 -0
- package/dist/zip/container.cjs +15 -0
- package/dist/zip/container.d.cts +9 -0
- package/dist/zip/container.d.ts +9 -0
- package/dist/zip/container.js +13 -0
- package/dist/zip/detect.cjs +26 -0
- package/dist/zip/detect.d.cts +6 -0
- package/dist/zip/detect.d.ts +6 -0
- package/dist/zip/detect.js +24 -0
- package/dist/zip/walk.cjs +41 -0
- package/dist/zip/walk.d.cts +20 -0
- package/dist/zip/walk.d.ts +20 -0
- package/dist/zip/walk.js +37 -0
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -16,18 +16,28 @@ Requires Node.js `>=20` and pnpm `11.6.0`.
|
|
|
16
16
|
|
|
17
17
|
```sh
|
|
18
18
|
pnpm install
|
|
19
|
-
pnpm build # tsdown -> dist/ (ESM + CJS + .d.ts)
|
|
19
|
+
pnpm build # tsdown -> dist/ (ESM + CJS + .d.ts, one file set per src module)
|
|
20
20
|
pnpm typecheck # tsc -p tsconfig.json && tsc -p tsconfig.node.json (dual tsconfig)
|
|
21
21
|
pnpm lint # eslint . --fix --cache --max-warnings 0
|
|
22
|
-
pnpm test # vitest run
|
|
23
|
-
pnpm test:watch # vitest
|
|
22
|
+
pnpm test # vitest run --project unit
|
|
23
|
+
pnpm test:watch # vitest --project unit
|
|
24
24
|
pnpm test:workers # vitest run --config vitest.workers.config.ts, inside a real Cloudflare Workers (workerd) isolate
|
|
25
|
+
pnpm test:smoke # builds dist/, then loads the built ESM and CJS barrels and every advertised deep import
|
|
25
26
|
```
|
|
26
27
|
|
|
27
28
|
To run a single test file, pass its path to vitest directly, e.g. `pnpm exec vitest run src/zip/walk.test.ts`.
|
|
28
29
|
|
|
29
30
|
## What it provides
|
|
30
31
|
|
|
32
|
+
Every module is importable by package-relative path as well as through the barrel — `tsdown` builds one dist file per src module (`root: 'src'`, the same layout ooxml.js ships), and `package.json`'s `./*` exports wildcard maps each subpath onto it:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { readCompoundFile } from 'archive-codec/cfb/read';
|
|
36
|
+
import { walkArchive } from 'archive-codec/zip/walk';
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The smoke suite (`test/smoke.test.mjs`) is the guard on that advertisement: it loads each module below from the built `dist/` in both module systems, so a build config that stops serving an advertised subpath fails the suite — neither publint nor `attw` catches a wildcard whose targets are missing.
|
|
40
|
+
|
|
31
41
|
| Module | Exports |
|
|
32
42
|
|---|---|
|
|
33
43
|
| `zip/container` | `zipPackage` (ordered-entries ZIP write with stored-uncompressed support), `unzipPackage`, `ZipEntry` |
|
|
@@ -96,7 +106,7 @@ npm install archive-codec
|
|
|
96
106
|
|
|
97
107
|
## Release and publishing
|
|
98
108
|
|
|
99
|
-
Release, CI, and commit-message conventions are all workspace-wide, not package-local — see the [monorepo root README](../../README.md#releases) for the mechanism (topological per-package `semantic-release` via `@exadev/semantic-release-workspace`, OIDC trusted npm publishing, automatic sibling dependency-range rewriting) and its [
|
|
109
|
+
Release, CI, and commit-message conventions are all workspace-wide, not package-local — see the [monorepo root README](../../README.md#releases) for the mechanism (topological per-package `semantic-release` via `@exadev/semantic-release-workspace`, OIDC trusted npm publishing, automatic sibling dependency-range rewriting) and its [post-release republishing and attestation](../../README.md#releases) note on the restored GitHub Packages mirrors, npm aliases, and SBOM/provenance signing.
|
|
100
110
|
|
|
101
111
|
## Contributing
|
|
102
112
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_magic = require("../magic.cjs");
|
|
3
|
+
//#region src/cfb/detect.ts
|
|
4
|
+
const COMPOUND_FILE_MAGIC = [
|
|
5
|
+
208,
|
|
6
|
+
207,
|
|
7
|
+
17,
|
|
8
|
+
224,
|
|
9
|
+
161,
|
|
10
|
+
177,
|
|
11
|
+
26,
|
|
12
|
+
225
|
|
13
|
+
];
|
|
14
|
+
function isCompoundFile(bytes) {
|
|
15
|
+
return require_magic.startsWithMagic(bytes, COMPOUND_FILE_MAGIC);
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
exports.isCompoundFile = isCompoundFile;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { startsWithMagic } from "../magic.js";
|
|
2
|
+
//#region src/cfb/detect.ts
|
|
3
|
+
const COMPOUND_FILE_MAGIC = [
|
|
4
|
+
208,
|
|
5
|
+
207,
|
|
6
|
+
17,
|
|
7
|
+
224,
|
|
8
|
+
161,
|
|
9
|
+
177,
|
|
10
|
+
26,
|
|
11
|
+
225
|
|
12
|
+
];
|
|
13
|
+
function isCompoundFile(bytes) {
|
|
14
|
+
return startsWithMagic(bytes, COMPOUND_FILE_MAGIC);
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { isCompoundFile };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/cfb/ole-package.ts
|
|
3
|
+
var OlePackageFormatError = class extends Error {
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "OlePackageFormatError";
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
const ANSI_DECODER = new TextDecoder("windows-1252");
|
|
10
|
+
function readZeroTerminated(bytes, offset, fieldName) {
|
|
11
|
+
let end = offset;
|
|
12
|
+
while (end < bytes.length && bytes[end] !== 0) end++;
|
|
13
|
+
if (end >= bytes.length) throw new OlePackageFormatError(`Package stream ends inside its ${fieldName} string with no terminator`);
|
|
14
|
+
return {
|
|
15
|
+
value: ANSI_DECODER.decode(bytes.subarray(offset, end)),
|
|
16
|
+
next: end + 1
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function readOlePackage(bytes) {
|
|
20
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
21
|
+
let offset = 2;
|
|
22
|
+
const label = readZeroTerminated(bytes, offset, "label");
|
|
23
|
+
offset = label.next;
|
|
24
|
+
const sourcePath = readZeroTerminated(bytes, offset, "source path");
|
|
25
|
+
offset = sourcePath.next;
|
|
26
|
+
offset += 8;
|
|
27
|
+
const tempPath = readZeroTerminated(bytes, offset, "temp path");
|
|
28
|
+
offset = tempPath.next;
|
|
29
|
+
if (offset + 4 > bytes.length) throw new OlePackageFormatError("Package stream ends before its packaged file size field");
|
|
30
|
+
const fileByteCount = view.getUint32(offset, true);
|
|
31
|
+
offset += 4;
|
|
32
|
+
if (offset + fileByteCount > bytes.length) throw new OlePackageFormatError(`Package stream declares ${fileByteCount} packaged-file bytes but holds only ${bytes.length - offset}`);
|
|
33
|
+
return {
|
|
34
|
+
label: label.value,
|
|
35
|
+
sourcePath: sourcePath.value,
|
|
36
|
+
tempPath: tempPath.value,
|
|
37
|
+
fileBytes: bytes.slice(offset, offset + fileByteCount)
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
exports.OlePackageFormatError = OlePackageFormatError;
|
|
42
|
+
exports.readOlePackage = readOlePackage;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/cfb/ole-package.d.ts
|
|
2
|
+
declare class OlePackageFormatError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
interface OlePackage {
|
|
6
|
+
readonly label: string;
|
|
7
|
+
readonly sourcePath: string;
|
|
8
|
+
readonly tempPath: string;
|
|
9
|
+
readonly fileBytes: Uint8Array<ArrayBuffer>;
|
|
10
|
+
}
|
|
11
|
+
declare function readOlePackage(bytes: Uint8Array<ArrayBuffer>): OlePackage;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { OlePackage, OlePackageFormatError, readOlePackage };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/cfb/ole-package.d.ts
|
|
2
|
+
declare class OlePackageFormatError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
interface OlePackage {
|
|
6
|
+
readonly label: string;
|
|
7
|
+
readonly sourcePath: string;
|
|
8
|
+
readonly tempPath: string;
|
|
9
|
+
readonly fileBytes: Uint8Array<ArrayBuffer>;
|
|
10
|
+
}
|
|
11
|
+
declare function readOlePackage(bytes: Uint8Array<ArrayBuffer>): OlePackage;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { OlePackage, OlePackageFormatError, readOlePackage };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//#region src/cfb/ole-package.ts
|
|
2
|
+
var OlePackageFormatError = class extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "OlePackageFormatError";
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
const ANSI_DECODER = new TextDecoder("windows-1252");
|
|
9
|
+
function readZeroTerminated(bytes, offset, fieldName) {
|
|
10
|
+
let end = offset;
|
|
11
|
+
while (end < bytes.length && bytes[end] !== 0) end++;
|
|
12
|
+
if (end >= bytes.length) throw new OlePackageFormatError(`Package stream ends inside its ${fieldName} string with no terminator`);
|
|
13
|
+
return {
|
|
14
|
+
value: ANSI_DECODER.decode(bytes.subarray(offset, end)),
|
|
15
|
+
next: end + 1
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function readOlePackage(bytes) {
|
|
19
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
20
|
+
let offset = 2;
|
|
21
|
+
const label = readZeroTerminated(bytes, offset, "label");
|
|
22
|
+
offset = label.next;
|
|
23
|
+
const sourcePath = readZeroTerminated(bytes, offset, "source path");
|
|
24
|
+
offset = sourcePath.next;
|
|
25
|
+
offset += 8;
|
|
26
|
+
const tempPath = readZeroTerminated(bytes, offset, "temp path");
|
|
27
|
+
offset = tempPath.next;
|
|
28
|
+
if (offset + 4 > bytes.length) throw new OlePackageFormatError("Package stream ends before its packaged file size field");
|
|
29
|
+
const fileByteCount = view.getUint32(offset, true);
|
|
30
|
+
offset += 4;
|
|
31
|
+
if (offset + fileByteCount > bytes.length) throw new OlePackageFormatError(`Package stream declares ${fileByteCount} packaged-file bytes but holds only ${bytes.length - offset}`);
|
|
32
|
+
return {
|
|
33
|
+
label: label.value,
|
|
34
|
+
sourcePath: sourcePath.value,
|
|
35
|
+
tempPath: tempPath.value,
|
|
36
|
+
fileBytes: bytes.slice(offset, offset + fileByteCount)
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
40
|
+
export { OlePackageFormatError, readOlePackage };
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_cfb_detect = require("./detect.cjs");
|
|
3
|
+
//#region src/cfb/read.ts
|
|
4
|
+
const ENDOFCHAIN = 4294967294;
|
|
5
|
+
const FREESECT = 4294967295;
|
|
6
|
+
const FATSECT = 4294967293;
|
|
7
|
+
const DIFSECT = 4294967292;
|
|
8
|
+
const NOSTREAM = 4294967295;
|
|
9
|
+
const HEADER_SIZE = 512;
|
|
10
|
+
const HEADER_DIFAT_ENTRIES = 109;
|
|
11
|
+
const OBJECT_TYPE_STORAGE = 1;
|
|
12
|
+
const OBJECT_TYPE_STREAM = 2;
|
|
13
|
+
const OBJECT_TYPE_ROOT = 5;
|
|
14
|
+
const MAX_CFB_TOTAL_STREAM_BYTES = 536870912;
|
|
15
|
+
var CompoundFileFormatError = class extends Error {
|
|
16
|
+
constructor(message) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "CompoundFileFormatError";
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
function u32(view, offset) {
|
|
22
|
+
return view.getUint32(offset, true);
|
|
23
|
+
}
|
|
24
|
+
function u16(view, offset) {
|
|
25
|
+
return view.getUint16(offset, true);
|
|
26
|
+
}
|
|
27
|
+
function readCompoundFile(bytes, options = {}) {
|
|
28
|
+
const maxTotalBytes = options.maxTotalBytes ?? 536870912;
|
|
29
|
+
if (!require_cfb_detect.isCompoundFile(bytes)) throw new CompoundFileFormatError("readCompoundFile input does not carry the compound-file signature (leading magic bytes are not D0 CF 11 E0 A1 B1 1A E1)");
|
|
30
|
+
if (bytes.length < HEADER_SIZE) throw new CompoundFileFormatError(`compound file is ${bytes.length} bytes, shorter than the fixed ${HEADER_SIZE}-byte header`);
|
|
31
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
32
|
+
const majorVersion = u16(view, 26);
|
|
33
|
+
if (majorVersion !== 3 && majorVersion !== 4) throw new CompoundFileFormatError(`compound file major version ${majorVersion} is not 3 or 4`);
|
|
34
|
+
if (u16(view, 28) !== 65534) throw new CompoundFileFormatError("compound file byte order is not little-endian");
|
|
35
|
+
const sectorShift = u16(view, 30);
|
|
36
|
+
if (majorVersion === 3 && sectorShift !== 9 || majorVersion === 4 && sectorShift !== 12) throw new CompoundFileFormatError(`compound file sector shift 2^${sectorShift} does not match major version ${majorVersion} (version 3 requires 512-byte sectors, version 4 requires 4096-byte)`);
|
|
37
|
+
const miniSectorShift = u16(view, 32);
|
|
38
|
+
if (miniSectorShift !== 6) throw new CompoundFileFormatError(`compound file mini sector shift 2^${miniSectorShift} is not the mandated 64-byte mini sector`);
|
|
39
|
+
const sectorSize = 1 << sectorShift;
|
|
40
|
+
const miniSectorSize = 1 << miniSectorShift;
|
|
41
|
+
const miniStreamCutoff = u32(view, 56);
|
|
42
|
+
if (miniStreamCutoff < miniSectorSize) throw new CompoundFileFormatError(`compound file mini stream cutoff ${miniStreamCutoff} is smaller than the ${miniSectorSize}-byte mini sector itself`);
|
|
43
|
+
const firstDirectorySector = u32(view, 48);
|
|
44
|
+
const firstMiniFatSector = u32(view, 60);
|
|
45
|
+
const firstDifatSector = u32(view, 68);
|
|
46
|
+
const sectorCount = Math.floor(bytes.length / sectorSize) - 1;
|
|
47
|
+
if (sectorCount < 1) throw new CompoundFileFormatError(`compound file holds no complete ${sectorSize}-byte sector after its header`);
|
|
48
|
+
const sectorOffset = (sector) => (sector + 1) * sectorSize;
|
|
49
|
+
const sectorBytes = (sector) => bytes.subarray(sectorOffset(sector), sectorOffset(sector) + sectorSize);
|
|
50
|
+
const fatSectorIds = [];
|
|
51
|
+
const acceptFatSector = (sector, provenance) => {
|
|
52
|
+
if (sector === FREESECT) return;
|
|
53
|
+
if (sector >= sectorCount) throw new CompoundFileFormatError(`${provenance} names FAT sector ${sector}, which is outside the file's ${sectorCount} sectors`);
|
|
54
|
+
fatSectorIds.push(sector);
|
|
55
|
+
};
|
|
56
|
+
for (let i = 0; i < HEADER_DIFAT_ENTRIES; i++) acceptFatSector(u32(view, 76 + i * 4), "the header DIFAT array");
|
|
57
|
+
let difatSector = firstDifatSector;
|
|
58
|
+
let difatSectorsWalked = 0;
|
|
59
|
+
while (difatSector !== ENDOFCHAIN) {
|
|
60
|
+
if (difatSector >= sectorCount) throw new CompoundFileFormatError(`the DIFAT chain names sector ${difatSector}, which is outside the file's ${sectorCount} sectors`);
|
|
61
|
+
if (++difatSectorsWalked > sectorCount) throw new CompoundFileFormatError("the DIFAT chain visits more sectors than the file holds, so it must cycle");
|
|
62
|
+
const difatView = new DataView(bytes.buffer, bytes.byteOffset + sectorOffset(difatSector), sectorSize);
|
|
63
|
+
const entriesPerDifatSector = sectorSize / 4 - 1;
|
|
64
|
+
for (let i = 0; i < entriesPerDifatSector; i++) acceptFatSector(u32(difatView, i * 4), "a DIFAT sector");
|
|
65
|
+
difatSector = u32(difatView, entriesPerDifatSector * 4);
|
|
66
|
+
}
|
|
67
|
+
if (fatSectorIds.length === 0) throw new CompoundFileFormatError("compound file declares no FAT sectors, so no sector chain can be walked");
|
|
68
|
+
const fatBytes = new Uint8Array(fatSectorIds.length * sectorSize);
|
|
69
|
+
for (let i = 0; i < fatSectorIds.length; i++) fatBytes.set(sectorBytes(fatSectorIds[i] ?? 0), i * sectorSize);
|
|
70
|
+
const fat = new DataView(fatBytes.buffer);
|
|
71
|
+
const fatEntry = (sector) => {
|
|
72
|
+
const offset = sector * 4;
|
|
73
|
+
if (offset < 0 || offset + 4 > fatBytes.length) throw new CompoundFileFormatError(`FAT entry for sector ${sector} lies beyond the sectors the DIFAT named`);
|
|
74
|
+
return fat.getUint32(offset, true);
|
|
75
|
+
};
|
|
76
|
+
const chainSectorIds = (start) => {
|
|
77
|
+
const ids = [];
|
|
78
|
+
let current = start;
|
|
79
|
+
while (current !== ENDOFCHAIN) {
|
|
80
|
+
if (current >= sectorCount) throw new CompoundFileFormatError(`a FAT chain steps to sector ${current}, which is outside the file's ${sectorCount} sectors`);
|
|
81
|
+
if (ids.length >= sectorCount) throw new CompoundFileFormatError("a FAT chain visits more sectors than the file holds, so it must cycle");
|
|
82
|
+
ids.push(current);
|
|
83
|
+
const next = fatEntry(current);
|
|
84
|
+
if (next === FREESECT || next === FATSECT || next === DIFSECT) throw new CompoundFileFormatError(`a FAT chain steps to sector ${current}'s entry ${next}, which is a sector-role marker, not a chain continuation`);
|
|
85
|
+
current = next;
|
|
86
|
+
}
|
|
87
|
+
return ids;
|
|
88
|
+
};
|
|
89
|
+
const chainBytes = (start) => {
|
|
90
|
+
const ids = chainSectorIds(start);
|
|
91
|
+
const out = new Uint8Array(ids.length * sectorSize);
|
|
92
|
+
for (let i = 0; i < ids.length; i++) out.set(sectorBytes(ids[i] ?? 0), i * sectorSize);
|
|
93
|
+
return out;
|
|
94
|
+
};
|
|
95
|
+
const directoryBytes = chainBytes(firstDirectorySector);
|
|
96
|
+
if (directoryBytes.length === 0) throw new CompoundFileFormatError("compound file has an empty directory chain");
|
|
97
|
+
const directoryView = new DataView(directoryBytes.buffer);
|
|
98
|
+
const entryCount = directoryBytes.length / 128;
|
|
99
|
+
const nameDecoder = new TextDecoder("utf-16le");
|
|
100
|
+
const entries = [];
|
|
101
|
+
for (let id = 0; id < entryCount; id++) {
|
|
102
|
+
const base = id * 128;
|
|
103
|
+
const nameLength = u16(directoryView, base + 64);
|
|
104
|
+
entries.push({
|
|
105
|
+
name: nameDecoder.decode(directoryBytes.subarray(base, base + Math.max(0, nameLength - 2))),
|
|
106
|
+
nameLength,
|
|
107
|
+
objectType: directoryView.getUint8(base + 66),
|
|
108
|
+
leftSibling: u32(directoryView, base + 68),
|
|
109
|
+
rightSibling: u32(directoryView, base + 72),
|
|
110
|
+
child: u32(directoryView, base + 76),
|
|
111
|
+
startSector: u32(directoryView, base + 116),
|
|
112
|
+
size: u32(directoryView, base + 120) + u32(directoryView, base + 124) * 4294967296
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
const root = entries[0];
|
|
116
|
+
if (root === void 0 || root.objectType !== OBJECT_TYPE_ROOT) throw new CompoundFileFormatError("the first directory entry is not the root storage entry (object type 5), as [MS-CFB] 2.6.1 requires");
|
|
117
|
+
const miniStream = chainBytes(root.startSector).subarray(0, root.size);
|
|
118
|
+
const miniSectorCount = Math.floor(miniStream.length / miniSectorSize);
|
|
119
|
+
const miniFatBytes = chainBytes(firstMiniFatSector);
|
|
120
|
+
const miniFat = new DataView(miniFatBytes.buffer);
|
|
121
|
+
const miniChainSectorIds = (start) => {
|
|
122
|
+
const ids = [];
|
|
123
|
+
let current = start;
|
|
124
|
+
while (current !== ENDOFCHAIN) {
|
|
125
|
+
if (current >= miniSectorCount) throw new CompoundFileFormatError(`a mini-FAT chain steps to mini sector ${current}, which is outside the mini stream's ${miniSectorCount} mini sectors`);
|
|
126
|
+
if (ids.length >= miniSectorCount) throw new CompoundFileFormatError("a mini-FAT chain visits more mini sectors than the mini stream holds, so it must cycle");
|
|
127
|
+
ids.push(current);
|
|
128
|
+
const next = miniFat.getUint32(current * 4, true);
|
|
129
|
+
if (next === FREESECT || next === FATSECT || next === DIFSECT) throw new CompoundFileFormatError(`a mini-FAT chain steps to mini sector ${current}'s entry ${next}, which is a sector-role marker, not a chain continuation`);
|
|
130
|
+
current = next;
|
|
131
|
+
}
|
|
132
|
+
return ids;
|
|
133
|
+
};
|
|
134
|
+
let totalExtractedBytes = 0;
|
|
135
|
+
const extractStream = (entry) => {
|
|
136
|
+
if (entry.size === 0) return /* @__PURE__ */ new Uint8Array(0);
|
|
137
|
+
if (entry.size > Number.MAX_SAFE_INTEGER) throw new CompoundFileFormatError(`stream '${entry.name}' declares a size beyond the integer range this reader addresses`);
|
|
138
|
+
let out;
|
|
139
|
+
if (entry.size < miniStreamCutoff) {
|
|
140
|
+
const ids = miniChainSectorIds(entry.startSector);
|
|
141
|
+
const raw = new Uint8Array(ids.length * miniSectorSize);
|
|
142
|
+
for (let i = 0; i < ids.length; i++) {
|
|
143
|
+
const miniSector = ids[i] ?? 0;
|
|
144
|
+
raw.set(miniStream.subarray(miniSector * miniSectorSize, (miniSector + 1) * miniSectorSize), i * miniSectorSize);
|
|
145
|
+
}
|
|
146
|
+
out = raw;
|
|
147
|
+
} else out = chainBytes(entry.startSector);
|
|
148
|
+
if (out.length < entry.size) throw new CompoundFileFormatError(`stream '${entry.name}' declares ${entry.size} bytes but its chain holds only ${out.length}`);
|
|
149
|
+
totalExtractedBytes += entry.size;
|
|
150
|
+
if (totalExtractedBytes > maxTotalBytes) throw new CompoundFileFormatError(`cumulative extracted stream size exceeded the ${maxTotalBytes}-byte budget at '${entry.name}'`);
|
|
151
|
+
return out.slice(0, entry.size);
|
|
152
|
+
};
|
|
153
|
+
const streams = [];
|
|
154
|
+
const visited = /* @__PURE__ */ new Set();
|
|
155
|
+
const stack = [{
|
|
156
|
+
id: root.child,
|
|
157
|
+
prefix: "",
|
|
158
|
+
stage: "descend"
|
|
159
|
+
}];
|
|
160
|
+
for (let frame = stack.pop(); frame !== void 0; frame = stack.pop()) if (frame.stage === "descend") {
|
|
161
|
+
const { id, prefix } = frame;
|
|
162
|
+
if (id === NOSTREAM) continue;
|
|
163
|
+
const entry = entries[id];
|
|
164
|
+
if (id >= entryCount || entry === void 0) throw new CompoundFileFormatError(`the directory tree links to entry ${id}, which is outside the directory's ${entryCount} entries`);
|
|
165
|
+
if (visited.has(id)) throw new CompoundFileFormatError(`the directory tree reaches entry ${id} twice, so its sibling and child links cycle`);
|
|
166
|
+
visited.add(id);
|
|
167
|
+
if (entry.nameLength < 2 || entry.nameLength > 64 || entry.nameLength % 2 === 1) throw new CompoundFileFormatError(`directory entry ${id} declares name length ${entry.nameLength}, which is not an even byte count between 2 and 64`);
|
|
168
|
+
if (entry.objectType !== OBJECT_TYPE_STORAGE && entry.objectType !== OBJECT_TYPE_STREAM && entry.objectType !== OBJECT_TYPE_ROOT) throw new CompoundFileFormatError(`directory entry ${id} ('${entry.name}') carries object type ${entry.objectType}, which is not a storage (1), stream (2), or root (5) entry`);
|
|
169
|
+
stack.push({
|
|
170
|
+
entry,
|
|
171
|
+
prefix,
|
|
172
|
+
stage: "self"
|
|
173
|
+
});
|
|
174
|
+
stack.push({
|
|
175
|
+
id: entry.leftSibling,
|
|
176
|
+
prefix,
|
|
177
|
+
stage: "descend"
|
|
178
|
+
});
|
|
179
|
+
} else if (frame.entry.objectType === OBJECT_TYPE_STREAM) {
|
|
180
|
+
streams.push({
|
|
181
|
+
path: frame.prefix + frame.entry.name,
|
|
182
|
+
bytes: extractStream(frame.entry)
|
|
183
|
+
});
|
|
184
|
+
stack.push({
|
|
185
|
+
id: frame.entry.rightSibling,
|
|
186
|
+
prefix: frame.prefix,
|
|
187
|
+
stage: "descend"
|
|
188
|
+
});
|
|
189
|
+
} else if (frame.entry.objectType === OBJECT_TYPE_STORAGE) {
|
|
190
|
+
stack.push({
|
|
191
|
+
id: frame.entry.rightSibling,
|
|
192
|
+
prefix: frame.prefix,
|
|
193
|
+
stage: "descend"
|
|
194
|
+
});
|
|
195
|
+
stack.push({
|
|
196
|
+
id: frame.entry.child,
|
|
197
|
+
prefix: `${frame.prefix}${frame.entry.name}/`,
|
|
198
|
+
stage: "descend"
|
|
199
|
+
});
|
|
200
|
+
} else throw new CompoundFileFormatError(`the directory tree reaches entry ${frame.entry.name}, which is not a storage or stream entry`);
|
|
201
|
+
return streams;
|
|
202
|
+
}
|
|
203
|
+
//#endregion
|
|
204
|
+
exports.CompoundFileFormatError = CompoundFileFormatError;
|
|
205
|
+
exports.MAX_CFB_TOTAL_STREAM_BYTES = MAX_CFB_TOTAL_STREAM_BYTES;
|
|
206
|
+
exports.readCompoundFile = readCompoundFile;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/cfb/read.d.ts
|
|
2
|
+
declare const MAX_CFB_TOTAL_STREAM_BYTES: number;
|
|
3
|
+
declare class CompoundFileFormatError extends Error {
|
|
4
|
+
constructor(message: string);
|
|
5
|
+
}
|
|
6
|
+
interface CompoundFileStream {
|
|
7
|
+
readonly path: string;
|
|
8
|
+
readonly bytes: Uint8Array<ArrayBuffer>;
|
|
9
|
+
}
|
|
10
|
+
interface ReadCompoundFileOptions {
|
|
11
|
+
readonly maxTotalBytes?: number;
|
|
12
|
+
}
|
|
13
|
+
declare function readCompoundFile(bytes: Uint8Array<ArrayBuffer>, options?: ReadCompoundFileOptions): CompoundFileStream[];
|
|
14
|
+
//#endregion
|
|
15
|
+
export { CompoundFileFormatError, CompoundFileStream, MAX_CFB_TOTAL_STREAM_BYTES, ReadCompoundFileOptions, readCompoundFile };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/cfb/read.d.ts
|
|
2
|
+
declare const MAX_CFB_TOTAL_STREAM_BYTES: number;
|
|
3
|
+
declare class CompoundFileFormatError extends Error {
|
|
4
|
+
constructor(message: string);
|
|
5
|
+
}
|
|
6
|
+
interface CompoundFileStream {
|
|
7
|
+
readonly path: string;
|
|
8
|
+
readonly bytes: Uint8Array<ArrayBuffer>;
|
|
9
|
+
}
|
|
10
|
+
interface ReadCompoundFileOptions {
|
|
11
|
+
readonly maxTotalBytes?: number;
|
|
12
|
+
}
|
|
13
|
+
declare function readCompoundFile(bytes: Uint8Array<ArrayBuffer>, options?: ReadCompoundFileOptions): CompoundFileStream[];
|
|
14
|
+
//#endregion
|
|
15
|
+
export { CompoundFileFormatError, CompoundFileStream, MAX_CFB_TOTAL_STREAM_BYTES, ReadCompoundFileOptions, readCompoundFile };
|
package/dist/cfb/read.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { isCompoundFile } from "./detect.js";
|
|
2
|
+
//#region src/cfb/read.ts
|
|
3
|
+
const ENDOFCHAIN = 4294967294;
|
|
4
|
+
const FREESECT = 4294967295;
|
|
5
|
+
const FATSECT = 4294967293;
|
|
6
|
+
const DIFSECT = 4294967292;
|
|
7
|
+
const NOSTREAM = 4294967295;
|
|
8
|
+
const HEADER_SIZE = 512;
|
|
9
|
+
const HEADER_DIFAT_ENTRIES = 109;
|
|
10
|
+
const OBJECT_TYPE_STORAGE = 1;
|
|
11
|
+
const OBJECT_TYPE_STREAM = 2;
|
|
12
|
+
const OBJECT_TYPE_ROOT = 5;
|
|
13
|
+
const MAX_CFB_TOTAL_STREAM_BYTES = 536870912;
|
|
14
|
+
var CompoundFileFormatError = class extends Error {
|
|
15
|
+
constructor(message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "CompoundFileFormatError";
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
function u32(view, offset) {
|
|
21
|
+
return view.getUint32(offset, true);
|
|
22
|
+
}
|
|
23
|
+
function u16(view, offset) {
|
|
24
|
+
return view.getUint16(offset, true);
|
|
25
|
+
}
|
|
26
|
+
function readCompoundFile(bytes, options = {}) {
|
|
27
|
+
const maxTotalBytes = options.maxTotalBytes ?? 536870912;
|
|
28
|
+
if (!isCompoundFile(bytes)) throw new CompoundFileFormatError("readCompoundFile input does not carry the compound-file signature (leading magic bytes are not D0 CF 11 E0 A1 B1 1A E1)");
|
|
29
|
+
if (bytes.length < HEADER_SIZE) throw new CompoundFileFormatError(`compound file is ${bytes.length} bytes, shorter than the fixed ${HEADER_SIZE}-byte header`);
|
|
30
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
31
|
+
const majorVersion = u16(view, 26);
|
|
32
|
+
if (majorVersion !== 3 && majorVersion !== 4) throw new CompoundFileFormatError(`compound file major version ${majorVersion} is not 3 or 4`);
|
|
33
|
+
if (u16(view, 28) !== 65534) throw new CompoundFileFormatError("compound file byte order is not little-endian");
|
|
34
|
+
const sectorShift = u16(view, 30);
|
|
35
|
+
if (majorVersion === 3 && sectorShift !== 9 || majorVersion === 4 && sectorShift !== 12) throw new CompoundFileFormatError(`compound file sector shift 2^${sectorShift} does not match major version ${majorVersion} (version 3 requires 512-byte sectors, version 4 requires 4096-byte)`);
|
|
36
|
+
const miniSectorShift = u16(view, 32);
|
|
37
|
+
if (miniSectorShift !== 6) throw new CompoundFileFormatError(`compound file mini sector shift 2^${miniSectorShift} is not the mandated 64-byte mini sector`);
|
|
38
|
+
const sectorSize = 1 << sectorShift;
|
|
39
|
+
const miniSectorSize = 1 << miniSectorShift;
|
|
40
|
+
const miniStreamCutoff = u32(view, 56);
|
|
41
|
+
if (miniStreamCutoff < miniSectorSize) throw new CompoundFileFormatError(`compound file mini stream cutoff ${miniStreamCutoff} is smaller than the ${miniSectorSize}-byte mini sector itself`);
|
|
42
|
+
const firstDirectorySector = u32(view, 48);
|
|
43
|
+
const firstMiniFatSector = u32(view, 60);
|
|
44
|
+
const firstDifatSector = u32(view, 68);
|
|
45
|
+
const sectorCount = Math.floor(bytes.length / sectorSize) - 1;
|
|
46
|
+
if (sectorCount < 1) throw new CompoundFileFormatError(`compound file holds no complete ${sectorSize}-byte sector after its header`);
|
|
47
|
+
const sectorOffset = (sector) => (sector + 1) * sectorSize;
|
|
48
|
+
const sectorBytes = (sector) => bytes.subarray(sectorOffset(sector), sectorOffset(sector) + sectorSize);
|
|
49
|
+
const fatSectorIds = [];
|
|
50
|
+
const acceptFatSector = (sector, provenance) => {
|
|
51
|
+
if (sector === FREESECT) return;
|
|
52
|
+
if (sector >= sectorCount) throw new CompoundFileFormatError(`${provenance} names FAT sector ${sector}, which is outside the file's ${sectorCount} sectors`);
|
|
53
|
+
fatSectorIds.push(sector);
|
|
54
|
+
};
|
|
55
|
+
for (let i = 0; i < HEADER_DIFAT_ENTRIES; i++) acceptFatSector(u32(view, 76 + i * 4), "the header DIFAT array");
|
|
56
|
+
let difatSector = firstDifatSector;
|
|
57
|
+
let difatSectorsWalked = 0;
|
|
58
|
+
while (difatSector !== ENDOFCHAIN) {
|
|
59
|
+
if (difatSector >= sectorCount) throw new CompoundFileFormatError(`the DIFAT chain names sector ${difatSector}, which is outside the file's ${sectorCount} sectors`);
|
|
60
|
+
if (++difatSectorsWalked > sectorCount) throw new CompoundFileFormatError("the DIFAT chain visits more sectors than the file holds, so it must cycle");
|
|
61
|
+
const difatView = new DataView(bytes.buffer, bytes.byteOffset + sectorOffset(difatSector), sectorSize);
|
|
62
|
+
const entriesPerDifatSector = sectorSize / 4 - 1;
|
|
63
|
+
for (let i = 0; i < entriesPerDifatSector; i++) acceptFatSector(u32(difatView, i * 4), "a DIFAT sector");
|
|
64
|
+
difatSector = u32(difatView, entriesPerDifatSector * 4);
|
|
65
|
+
}
|
|
66
|
+
if (fatSectorIds.length === 0) throw new CompoundFileFormatError("compound file declares no FAT sectors, so no sector chain can be walked");
|
|
67
|
+
const fatBytes = new Uint8Array(fatSectorIds.length * sectorSize);
|
|
68
|
+
for (let i = 0; i < fatSectorIds.length; i++) fatBytes.set(sectorBytes(fatSectorIds[i] ?? 0), i * sectorSize);
|
|
69
|
+
const fat = new DataView(fatBytes.buffer);
|
|
70
|
+
const fatEntry = (sector) => {
|
|
71
|
+
const offset = sector * 4;
|
|
72
|
+
if (offset < 0 || offset + 4 > fatBytes.length) throw new CompoundFileFormatError(`FAT entry for sector ${sector} lies beyond the sectors the DIFAT named`);
|
|
73
|
+
return fat.getUint32(offset, true);
|
|
74
|
+
};
|
|
75
|
+
const chainSectorIds = (start) => {
|
|
76
|
+
const ids = [];
|
|
77
|
+
let current = start;
|
|
78
|
+
while (current !== ENDOFCHAIN) {
|
|
79
|
+
if (current >= sectorCount) throw new CompoundFileFormatError(`a FAT chain steps to sector ${current}, which is outside the file's ${sectorCount} sectors`);
|
|
80
|
+
if (ids.length >= sectorCount) throw new CompoundFileFormatError("a FAT chain visits more sectors than the file holds, so it must cycle");
|
|
81
|
+
ids.push(current);
|
|
82
|
+
const next = fatEntry(current);
|
|
83
|
+
if (next === FREESECT || next === FATSECT || next === DIFSECT) throw new CompoundFileFormatError(`a FAT chain steps to sector ${current}'s entry ${next}, which is a sector-role marker, not a chain continuation`);
|
|
84
|
+
current = next;
|
|
85
|
+
}
|
|
86
|
+
return ids;
|
|
87
|
+
};
|
|
88
|
+
const chainBytes = (start) => {
|
|
89
|
+
const ids = chainSectorIds(start);
|
|
90
|
+
const out = new Uint8Array(ids.length * sectorSize);
|
|
91
|
+
for (let i = 0; i < ids.length; i++) out.set(sectorBytes(ids[i] ?? 0), i * sectorSize);
|
|
92
|
+
return out;
|
|
93
|
+
};
|
|
94
|
+
const directoryBytes = chainBytes(firstDirectorySector);
|
|
95
|
+
if (directoryBytes.length === 0) throw new CompoundFileFormatError("compound file has an empty directory chain");
|
|
96
|
+
const directoryView = new DataView(directoryBytes.buffer);
|
|
97
|
+
const entryCount = directoryBytes.length / 128;
|
|
98
|
+
const nameDecoder = new TextDecoder("utf-16le");
|
|
99
|
+
const entries = [];
|
|
100
|
+
for (let id = 0; id < entryCount; id++) {
|
|
101
|
+
const base = id * 128;
|
|
102
|
+
const nameLength = u16(directoryView, base + 64);
|
|
103
|
+
entries.push({
|
|
104
|
+
name: nameDecoder.decode(directoryBytes.subarray(base, base + Math.max(0, nameLength - 2))),
|
|
105
|
+
nameLength,
|
|
106
|
+
objectType: directoryView.getUint8(base + 66),
|
|
107
|
+
leftSibling: u32(directoryView, base + 68),
|
|
108
|
+
rightSibling: u32(directoryView, base + 72),
|
|
109
|
+
child: u32(directoryView, base + 76),
|
|
110
|
+
startSector: u32(directoryView, base + 116),
|
|
111
|
+
size: u32(directoryView, base + 120) + u32(directoryView, base + 124) * 4294967296
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
const root = entries[0];
|
|
115
|
+
if (root === void 0 || root.objectType !== OBJECT_TYPE_ROOT) throw new CompoundFileFormatError("the first directory entry is not the root storage entry (object type 5), as [MS-CFB] 2.6.1 requires");
|
|
116
|
+
const miniStream = chainBytes(root.startSector).subarray(0, root.size);
|
|
117
|
+
const miniSectorCount = Math.floor(miniStream.length / miniSectorSize);
|
|
118
|
+
const miniFatBytes = chainBytes(firstMiniFatSector);
|
|
119
|
+
const miniFat = new DataView(miniFatBytes.buffer);
|
|
120
|
+
const miniChainSectorIds = (start) => {
|
|
121
|
+
const ids = [];
|
|
122
|
+
let current = start;
|
|
123
|
+
while (current !== ENDOFCHAIN) {
|
|
124
|
+
if (current >= miniSectorCount) throw new CompoundFileFormatError(`a mini-FAT chain steps to mini sector ${current}, which is outside the mini stream's ${miniSectorCount} mini sectors`);
|
|
125
|
+
if (ids.length >= miniSectorCount) throw new CompoundFileFormatError("a mini-FAT chain visits more mini sectors than the mini stream holds, so it must cycle");
|
|
126
|
+
ids.push(current);
|
|
127
|
+
const next = miniFat.getUint32(current * 4, true);
|
|
128
|
+
if (next === FREESECT || next === FATSECT || next === DIFSECT) throw new CompoundFileFormatError(`a mini-FAT chain steps to mini sector ${current}'s entry ${next}, which is a sector-role marker, not a chain continuation`);
|
|
129
|
+
current = next;
|
|
130
|
+
}
|
|
131
|
+
return ids;
|
|
132
|
+
};
|
|
133
|
+
let totalExtractedBytes = 0;
|
|
134
|
+
const extractStream = (entry) => {
|
|
135
|
+
if (entry.size === 0) return /* @__PURE__ */ new Uint8Array(0);
|
|
136
|
+
if (entry.size > Number.MAX_SAFE_INTEGER) throw new CompoundFileFormatError(`stream '${entry.name}' declares a size beyond the integer range this reader addresses`);
|
|
137
|
+
let out;
|
|
138
|
+
if (entry.size < miniStreamCutoff) {
|
|
139
|
+
const ids = miniChainSectorIds(entry.startSector);
|
|
140
|
+
const raw = new Uint8Array(ids.length * miniSectorSize);
|
|
141
|
+
for (let i = 0; i < ids.length; i++) {
|
|
142
|
+
const miniSector = ids[i] ?? 0;
|
|
143
|
+
raw.set(miniStream.subarray(miniSector * miniSectorSize, (miniSector + 1) * miniSectorSize), i * miniSectorSize);
|
|
144
|
+
}
|
|
145
|
+
out = raw;
|
|
146
|
+
} else out = chainBytes(entry.startSector);
|
|
147
|
+
if (out.length < entry.size) throw new CompoundFileFormatError(`stream '${entry.name}' declares ${entry.size} bytes but its chain holds only ${out.length}`);
|
|
148
|
+
totalExtractedBytes += entry.size;
|
|
149
|
+
if (totalExtractedBytes > maxTotalBytes) throw new CompoundFileFormatError(`cumulative extracted stream size exceeded the ${maxTotalBytes}-byte budget at '${entry.name}'`);
|
|
150
|
+
return out.slice(0, entry.size);
|
|
151
|
+
};
|
|
152
|
+
const streams = [];
|
|
153
|
+
const visited = /* @__PURE__ */ new Set();
|
|
154
|
+
const stack = [{
|
|
155
|
+
id: root.child,
|
|
156
|
+
prefix: "",
|
|
157
|
+
stage: "descend"
|
|
158
|
+
}];
|
|
159
|
+
for (let frame = stack.pop(); frame !== void 0; frame = stack.pop()) if (frame.stage === "descend") {
|
|
160
|
+
const { id, prefix } = frame;
|
|
161
|
+
if (id === NOSTREAM) continue;
|
|
162
|
+
const entry = entries[id];
|
|
163
|
+
if (id >= entryCount || entry === void 0) throw new CompoundFileFormatError(`the directory tree links to entry ${id}, which is outside the directory's ${entryCount} entries`);
|
|
164
|
+
if (visited.has(id)) throw new CompoundFileFormatError(`the directory tree reaches entry ${id} twice, so its sibling and child links cycle`);
|
|
165
|
+
visited.add(id);
|
|
166
|
+
if (entry.nameLength < 2 || entry.nameLength > 64 || entry.nameLength % 2 === 1) throw new CompoundFileFormatError(`directory entry ${id} declares name length ${entry.nameLength}, which is not an even byte count between 2 and 64`);
|
|
167
|
+
if (entry.objectType !== OBJECT_TYPE_STORAGE && entry.objectType !== OBJECT_TYPE_STREAM && entry.objectType !== OBJECT_TYPE_ROOT) throw new CompoundFileFormatError(`directory entry ${id} ('${entry.name}') carries object type ${entry.objectType}, which is not a storage (1), stream (2), or root (5) entry`);
|
|
168
|
+
stack.push({
|
|
169
|
+
entry,
|
|
170
|
+
prefix,
|
|
171
|
+
stage: "self"
|
|
172
|
+
});
|
|
173
|
+
stack.push({
|
|
174
|
+
id: entry.leftSibling,
|
|
175
|
+
prefix,
|
|
176
|
+
stage: "descend"
|
|
177
|
+
});
|
|
178
|
+
} else if (frame.entry.objectType === OBJECT_TYPE_STREAM) {
|
|
179
|
+
streams.push({
|
|
180
|
+
path: frame.prefix + frame.entry.name,
|
|
181
|
+
bytes: extractStream(frame.entry)
|
|
182
|
+
});
|
|
183
|
+
stack.push({
|
|
184
|
+
id: frame.entry.rightSibling,
|
|
185
|
+
prefix: frame.prefix,
|
|
186
|
+
stage: "descend"
|
|
187
|
+
});
|
|
188
|
+
} else if (frame.entry.objectType === OBJECT_TYPE_STORAGE) {
|
|
189
|
+
stack.push({
|
|
190
|
+
id: frame.entry.rightSibling,
|
|
191
|
+
prefix: frame.prefix,
|
|
192
|
+
stage: "descend"
|
|
193
|
+
});
|
|
194
|
+
stack.push({
|
|
195
|
+
id: frame.entry.child,
|
|
196
|
+
prefix: `${frame.prefix}${frame.entry.name}/`,
|
|
197
|
+
stage: "descend"
|
|
198
|
+
});
|
|
199
|
+
} else throw new CompoundFileFormatError(`the directory tree reaches entry ${frame.entry.name}, which is not a storage or stream entry`);
|
|
200
|
+
return streams;
|
|
201
|
+
}
|
|
202
|
+
//#endregion
|
|
203
|
+
export { CompoundFileFormatError, MAX_CFB_TOTAL_STREAM_BYTES, readCompoundFile };
|