@patrickdk77/office-defuse 0.0.0-stage → 0.1.1

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.
Files changed (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +317 -2
  3. package/dist/biff.d.ts +62 -0
  4. package/dist/biff.js +95 -0
  5. package/dist/cfb.d.ts +58 -0
  6. package/dist/cfb.js +456 -0
  7. package/dist/context.d.ts +66 -0
  8. package/dist/context.js +121 -0
  9. package/dist/crc32.d.ts +8 -0
  10. package/dist/crc32.js +58 -0
  11. package/dist/engine.d.ts +5 -0
  12. package/dist/engine.js +200 -0
  13. package/dist/errors.d.ts +19 -0
  14. package/dist/errors.js +33 -0
  15. package/dist/fields.d.ts +29 -0
  16. package/dist/fields.js +97 -0
  17. package/dist/findings.d.ts +25 -0
  18. package/dist/findings.js +188 -0
  19. package/dist/formats.d.ts +10 -0
  20. package/dist/formats.js +46 -0
  21. package/dist/formulas.d.ts +22 -0
  22. package/dist/formulas.js +77 -0
  23. package/dist/index.d.ts +5 -0
  24. package/dist/index.js +30 -0
  25. package/dist/index.mjs +3 -0
  26. package/dist/io.d.ts +23 -0
  27. package/dist/io.js +139 -0
  28. package/dist/links.d.ts +26 -0
  29. package/dist/links.js +145 -0
  30. package/dist/msg.d.ts +7 -0
  31. package/dist/msg.js +97 -0
  32. package/dist/objects.d.ts +42 -0
  33. package/dist/objects.js +134 -0
  34. package/dist/odf.d.ts +3 -0
  35. package/dist/odf.js +297 -0
  36. package/dist/offcrypto.d.ts +31 -0
  37. package/dist/offcrypto.js +408 -0
  38. package/dist/ole.d.ts +12 -0
  39. package/dist/ole.js +127 -0
  40. package/dist/oleobj.d.ts +74 -0
  41. package/dist/oleobj.js +198 -0
  42. package/dist/ooxml.d.ts +8 -0
  43. package/dist/ooxml.js +468 -0
  44. package/dist/package.d.ts +3 -0
  45. package/dist/package.js +21 -0
  46. package/dist/ppt.d.ts +8 -0
  47. package/dist/ppt.js +241 -0
  48. package/dist/rtf.d.ts +8 -0
  49. package/dist/rtf.js +539 -0
  50. package/dist/score.d.ts +17 -0
  51. package/dist/score.js +63 -0
  52. package/dist/types.d.ts +274 -0
  53. package/dist/types.js +152 -0
  54. package/dist/word.d.ts +9 -0
  55. package/dist/word.js +160 -0
  56. package/dist/xls.d.ts +27 -0
  57. package/dist/xls.js +174 -0
  58. package/dist/xlsb.d.ts +19 -0
  59. package/dist/xlsb.js +101 -0
  60. package/dist/xml.d.ts +256 -0
  61. package/dist/xml.js +1105 -0
  62. package/dist/zip-writer.d.ts +38 -0
  63. package/dist/zip-writer.js +182 -0
  64. package/dist/zip.d.ts +55 -0
  65. package/dist/zip.js +535 -0
  66. package/package.json +96 -4
package/dist/cfb.js ADDED
@@ -0,0 +1,456 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CompoundFile = exports.showName = exports.CFB_SIGNATURE = void 0;
4
+ exports.guid = guid;
5
+ exports.entryPath = entryPath;
6
+ const errors_1 = require("./errors");
7
+ const io_1 = require("./io");
8
+ const types_1 = require("./types");
9
+ /** The first eight bytes of every compound file. */
10
+ exports.CFB_SIGNATURE = Uint8Array.of(0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1);
11
+ const MAX_REGULAR = 0xfffffffa;
12
+ const END_OF_CHAIN = 0xfffffffe;
13
+ const FREE = 0xffffffff;
14
+ const NO_STREAM = 0xffffffff;
15
+ const MINI_SECTOR = 64;
16
+ const MINI_CUTOFF = 4096;
17
+ /** The header lists the first 109 FAT sectors itself. */
18
+ const HEADER_FAT = 109;
19
+ /** Owners of a sector besides the streams, which take 1 + their position among the entries. */
20
+ const OWN = { fat: -1, difat: -2, directory: -3, miniFat: -4, miniStream: -5 };
21
+ const fail = (detail, message, data) => {
22
+ throw new errors_1.StopError(types_1.OfficeCategory.Container, detail, message, data);
23
+ };
24
+ const limit = (detail, message, data) => {
25
+ throw new errors_1.StopError(types_1.OfficeCategory.Limit, detail, message, data);
26
+ };
27
+ const u16 = (b, at) => b[at] | (b[at + 1] << 8);
28
+ const u32 = (b, at) => (b[at] | (b[at + 1] << 8) | (b[at + 2] << 16) | (b[at + 3] << 24)) >>> 0;
29
+ /** A GUID as Windows shows it: the first three fields little-endian, upper case, without braces. */
30
+ function guid(b, at = 0) {
31
+ const h = (i) => b[at + i].toString(16).padStart(2, '0');
32
+ const r = (...is) => is.map(h).join('');
33
+ return `${r(3, 2, 1, 0)}-${r(5, 4)}-${r(7, 6)}-${r(8, 9)}-${r(10, 11, 12, 13, 14, 15)}`.toUpperCase();
34
+ }
35
+ /** An entry name with control and format characters written as \xNN, so a location can show it. */
36
+ const showName = (name) => name.replace(/[\p{Cc}\p{Cf}]/gu, c => `\\x${c.charCodeAt(0).toString(16).padStart(2, '0')}`);
37
+ exports.showName = showName;
38
+ /** The entry's path from the root, its names joined by "/", which no name may hold. */
39
+ function entryPath(e) {
40
+ const names = [];
41
+ for (let p = e; p && p.kind !== 'root'; p = p.parent)
42
+ names.push((0, exports.showName)(p.name));
43
+ return names.reverse().join('/');
44
+ }
45
+ /** Names compare as MS-CFB compares them, ignoring case. Python's lower() and Windows' upper case both count. */
46
+ const nameKeys = (name) => [`u:${name.toUpperCase()}`, `l:${name.toLowerCase()}`];
47
+ /**
48
+ * A compound file (MS-CFB), read from its header, its allocation tables and its directory. Every chain is followed as
49
+ * far as its stream needs and every sector has one owner, so two readers cannot read the file differently. Reading a
50
+ * stream maps its offsets through its chain; nothing is held but the tables.
51
+ */
52
+ class CompoundFile {
53
+ reader;
54
+ sectorSize;
55
+ root;
56
+ entries;
57
+ notes;
58
+ miniStream;
59
+ deadline;
60
+ streamChains = new Map();
61
+ constructor(reader, sectorSize, root,
62
+ /** Every entry the directory tree reaches, the root first, each storage before its children. */
63
+ entries, notes, miniStream, deadline) {
64
+ this.reader = reader;
65
+ this.sectorSize = sectorSize;
66
+ this.root = root;
67
+ this.entries = entries;
68
+ this.notes = notes;
69
+ this.miniStream = miniStream;
70
+ this.deadline = deadline;
71
+ }
72
+ /** The child of a storage with this name, compared ignoring case. */
73
+ child(storage, name) {
74
+ const key = name.toUpperCase();
75
+ return storage.children.find(c => c.name.toUpperCase() === key);
76
+ }
77
+ /** Reads a compound file. The caller has seen its signature. */
78
+ static async open(source, limits, deadline) {
79
+ const size = await source.size();
80
+ const reader = new io_1.Reader(source, size);
81
+ if (size < 512)
82
+ fail(types_1.OfficeDetail.BadHeader, 'The file is too short for a compound file header');
83
+ const h = await reader.read(0, 512);
84
+ for (let i = 0; i < 8; i++)
85
+ if (h[i] !== exports.CFB_SIGNATURE[i])
86
+ fail(types_1.OfficeDetail.BadHeader, 'The file does not start with the compound file signature');
87
+ const major = u16(h, 0x1a);
88
+ const shift = u16(h, 0x1e);
89
+ // olefile reads any sector size the header gives, and Windows only the two that match their version.
90
+ if (!((major === 3 && shift === 9) || (major === 4 && shift === 12)))
91
+ fail(types_1.OfficeDetail.BadHeader, `Version ${major} with sectors of 2^${shift} bytes`, { reason: 'version and sector size' });
92
+ if (u16(h, 0x20) !== 6)
93
+ fail(types_1.OfficeDetail.BadHeader, 'Mini sectors other than 64 bytes', { reason: 'mini sector size' });
94
+ // olefile reads mini streams up to 4096 bytes whatever the header says, and other readers take the header's word.
95
+ if (u32(h, 0x38) !== MINI_CUTOFF)
96
+ fail(types_1.OfficeDetail.BadHeader, 'A mini stream cutoff other than 4096 bytes', { reason: 'mini stream cutoff' });
97
+ if (u16(h, 0x1c) !== 0xfffe)
98
+ fail(types_1.OfficeDetail.BadHeader, 'A byte order mark other than little-endian', { reason: 'byte order' });
99
+ const ss = 2 ** shift;
100
+ if (size < ss)
101
+ fail(types_1.OfficeDetail.BadHeader, 'The file is too short for its header sector');
102
+ const numFat = u32(h, 0x2c);
103
+ const firstDir = u32(h, 0x30);
104
+ const firstMiniFat = u32(h, 0x3c);
105
+ const numMiniFat = u32(h, 0x40);
106
+ const firstDifat = u32(h, 0x44);
107
+ const numDifat = u32(h, 0x48);
108
+ const notes = [];
109
+ /** Sectors that start inside the file. The last may be cut short. */
110
+ const fileSectors = Math.ceil((size - ss) / ss);
111
+ const fullSectors = Math.floor((size - ss) / ss);
112
+ const perSector = ss / 4;
113
+ if (numFat === 0)
114
+ fail(types_1.OfficeDetail.BadHeader, 'The header counts no FAT sectors', { reason: 'no FAT' });
115
+ if (numFat > fullSectors)
116
+ fail(types_1.OfficeDetail.OutOfRange, 'The header counts more FAT sectors than the file holds');
117
+ // The FAT sectors: the first 109 from the header, the rest from the DIFAT chain.
118
+ const fatSectors = [];
119
+ const listed = Math.min(numFat, HEADER_FAT);
120
+ for (let i = 0; i < listed; i++)
121
+ fatSectors.push(u32(h, 0x4c + 4 * i));
122
+ // olefile reads FAT sectors from the header until the first free or end mark, and Windows reads as many as the
123
+ // header counts. A sector listed past the count would be read by one and not the other.
124
+ if (numFat < HEADER_FAT) {
125
+ const next = u32(h, 0x4c + 4 * numFat);
126
+ if (next <= MAX_REGULAR)
127
+ fail(types_1.OfficeDetail.BadHeader, 'The header lists more FAT sectors than it counts', { reason: 'FAT sector count' });
128
+ }
129
+ const difatSectors = [];
130
+ if (numFat > HEADER_FAT) {
131
+ const per = perSector - 1;
132
+ const need = Math.ceil((numFat - HEADER_FAT) / per);
133
+ if (numDifat !== need)
134
+ fail(types_1.OfficeDetail.BadHeader, 'The DIFAT sector count does not fit the FAT sector count', { reason: 'DIFAT sector count' });
135
+ let s = firstDifat;
136
+ for (let k = 0; k < need; k++) {
137
+ if (s > MAX_REGULAR)
138
+ fail(types_1.OfficeDetail.BrokenChain, 'The DIFAT chain ends early');
139
+ if (s >= fullSectors)
140
+ fail(types_1.OfficeDetail.OutOfRange, 'A DIFAT sector lies outside the file');
141
+ if (difatSectors.includes(s))
142
+ fail(types_1.OfficeDetail.ChainLoop, 'The DIFAT chain loops');
143
+ difatSectors.push(s);
144
+ const d = await reader.read((s + 1) * ss, ss);
145
+ const take = Math.min(per, numFat - fatSectors.length);
146
+ for (let i = 0; i < take; i++)
147
+ fatSectors.push(u32(d, 4 * i));
148
+ if (take < per && u32(d, 4 * take) <= MAX_REGULAR)
149
+ fail(types_1.OfficeDetail.BadHeader, 'The DIFAT lists more FAT sectors than the header counts', { reason: 'FAT sector count' });
150
+ s = u32(d, 4 * per);
151
+ }
152
+ if (s !== END_OF_CHAIN && s !== FREE)
153
+ fail(types_1.OfficeDetail.BrokenChain, 'The DIFAT chain does not end after its last sector');
154
+ }
155
+ else if (numDifat !== 0)
156
+ fail(types_1.OfficeDetail.BadHeader, 'The header counts DIFAT sectors it does not need', { reason: 'DIFAT sector count' });
157
+ const fatLength = numFat * perSector;
158
+ /** Sectors the FAT describes that start inside the file: the only ones a chain may name. */
159
+ const sectors = Math.min(fatLength, fileSectors);
160
+ if (fatLength < fileSectors)
161
+ notes.push({ detail: types_1.OfficeDetail.TrailingBytes, bytes: size - (fatLength + 1) * ss });
162
+ const owners = new Int32Array(sectors);
163
+ const own = (s, owner, what) => {
164
+ if (s >= sectors)
165
+ fail(types_1.OfficeDetail.OutOfRange, `A sector of the ${what} lies outside the file`);
166
+ if (owners[s] === owner)
167
+ fail(types_1.OfficeDetail.ChainLoop, `The ${what} loops`);
168
+ if (owners[s] !== 0)
169
+ fail(types_1.OfficeDetail.Overlap, `The ${what} shares a sector with another stream`, { what });
170
+ owners[s] = owner;
171
+ };
172
+ for (const s of fatSectors) {
173
+ if (s > MAX_REGULAR)
174
+ fail(types_1.OfficeDetail.BrokenChain, 'A FAT sector number is a reserved value');
175
+ if (s >= fullSectors)
176
+ fail(types_1.OfficeDetail.OutOfRange, 'A FAT sector lies outside the file');
177
+ own(s, OWN.fat, 'FAT');
178
+ }
179
+ for (const s of difatSectors)
180
+ own(s, OWN.difat, 'DIFAT');
181
+ const fat = new Uint32Array(fatLength);
182
+ for (const [i, s] of fatSectors.entries()) {
183
+ if (i % 64 === 0)
184
+ (0, errors_1.checkTime)(deadline);
185
+ const b = await reader.read((s + 1) * ss, ss);
186
+ fat.set(new Uint32Array(b.buffer.slice(b.byteOffset, b.byteOffset + ss)), i * perSector);
187
+ }
188
+ /**
189
+ * Follows a chain of the FAT, owning each sector: to its end mark, or for a stream only as far as its size needs,
190
+ * since readers stop there. `full` sectors must lie wholly inside the file.
191
+ */
192
+ const chain = (start, owner, what, full, length = Number.POSITIVE_INFINITY) => {
193
+ const out = [];
194
+ for (let s = start; out.length < length; s = fat[s]) {
195
+ if (out.length % 4096 === 0)
196
+ (0, errors_1.checkTime)(deadline);
197
+ if (s === END_OF_CHAIN) {
198
+ if (length === Number.POSITIVE_INFINITY)
199
+ break;
200
+ fail(types_1.OfficeDetail.DataError, `The ${what} is shorter than its declared size`, { reason: 'a chain shorter than the stream' });
201
+ }
202
+ if (s > MAX_REGULAR)
203
+ fail(types_1.OfficeDetail.BrokenChain, `The ${what} runs into a free or reserved sector`, { what });
204
+ own(s, owner, what);
205
+ if (full && s >= fullSectors)
206
+ fail(types_1.OfficeDetail.OutOfRange, `A sector of the ${what} is cut short by the end of the file`);
207
+ out.push(s);
208
+ }
209
+ return Uint32Array.from(out);
210
+ };
211
+ const dir = chain(firstDir, OWN.directory, 'directory', true);
212
+ if (dir.length === 0)
213
+ fail(types_1.OfficeDetail.BadHeader, 'The directory is empty', { reason: 'no directory' });
214
+ const perDirSector = ss / 128;
215
+ const dirCount = dir.length * perDirSector;
216
+ const rawEntry = (id) => reader.read((dir[Math.floor(id / perDirSector)] + 1) * ss + (id % perDirSector) * 128, 128);
217
+ const v3 = major === 3;
218
+ const load = async (id, parent) => {
219
+ const b = await rawEntry(id);
220
+ const type = b[0x42];
221
+ const kind = type === 5 ? 'root' : type === 1 ? 'storage' : type === 2 ? 'stream' : undefined;
222
+ if (kind === undefined)
223
+ return fail(types_1.OfficeDetail.BadEntry, `Directory entry ${id} has type ${type}`, { reason: `entry type ${type}` });
224
+ if ((kind === 'root') !== (id === 0))
225
+ fail(types_1.OfficeDetail.BadEntry, 'The root entry is not the first entry of the directory, or there are two', { reason: 'root entry' });
226
+ const nameLength = u16(b, 0x40);
227
+ let name = '';
228
+ if (kind !== 'root') {
229
+ // olefile and LibreOffice take the name from its length field. An odd or empty length, or one past the field,
230
+ // makes them read different names, and a NUL inside one ends it for a reader that takes it as a C string.
231
+ if (nameLength < 2 || nameLength > 64 || nameLength % 2)
232
+ fail(types_1.OfficeDetail.BadEntry, `Directory entry ${id} has a name length of ${nameLength}`, { reason: 'name length' });
233
+ name = Buffer.from(b.subarray(0, nameLength - 2)).toString('utf16le');
234
+ if (u16(b, nameLength - 2) !== 0)
235
+ fail(types_1.OfficeDetail.BadEntry, `The name of directory entry ${id} is not terminated where its length says`, { reason: 'name terminator' });
236
+ if (name.includes('\0'))
237
+ fail(types_1.OfficeDetail.BadEntry, `The name of directory entry ${id} holds a NUL`, { reason: 'NUL in name' });
238
+ if (/[/\\:!]/.test(name))
239
+ fail(types_1.OfficeDetail.UnsafeName, `The name of directory entry ${id} holds a character MS-CFB forbids`, { name: (0, exports.showName)(name), reason: 'a character MS-CFB forbids' });
240
+ }
241
+ const low = u32(b, 0x78);
242
+ const high = u32(b, 0x7c);
243
+ // A version 3 file keeps its sizes in 32 bits. Readers ignore the high half, which some writers leave set.
244
+ let entrySize = low;
245
+ if (!v3) {
246
+ const big = high * 2 ** 32 + low;
247
+ if (big > Number.MAX_SAFE_INTEGER)
248
+ fail(types_1.OfficeDetail.OutOfRange, 'A stream size is beyond any file this reader can hold');
249
+ entrySize = big;
250
+ }
251
+ return {
252
+ entry: { id, name, kind, clsid: guid(b, 0x50), size: kind === 'storage' ? 0 : entrySize, parent, children: [] },
253
+ left: u32(b, 0x44),
254
+ right: u32(b, 0x48),
255
+ child: u32(b, 0x4c),
256
+ start: u32(b, 0x74),
257
+ };
258
+ };
259
+ const rootRaw = await load(0, undefined);
260
+ const root = rootRaw.entry;
261
+ const entries = [root];
262
+ /** The first sector of each entry, by its position in `entries`. */
263
+ const starts = [rootRaw.start];
264
+ const reached = new Uint8Array(dirCount);
265
+ reached[0] = 1;
266
+ const storages = [rootRaw];
267
+ while (storages.length) {
268
+ (0, errors_1.checkTime)(deadline);
269
+ const storage = storages.pop();
270
+ const seen = new Set();
271
+ // An in-order walk of the storage's tree of children.
272
+ const stack = [];
273
+ let at = storage.child;
274
+ while (at !== NO_STREAM || stack.length) {
275
+ while (at !== NO_STREAM) {
276
+ if (at >= dirCount)
277
+ fail(types_1.OfficeDetail.OutOfRange, 'A directory entry names an entry past the directory');
278
+ if (reached[at])
279
+ fail(types_1.OfficeDetail.DirectoryLoop, `Directory entry ${at} is reached twice`);
280
+ reached[at] = 1;
281
+ const r = await load(at, storage.entry);
282
+ if (entries.length >= limits.entries)
283
+ limit(types_1.OfficeDetail.EntryCount, `More than ${limits.entries} directory entries`, { limit: limits.entries });
284
+ stack.push(r);
285
+ at = r.left;
286
+ }
287
+ const r = stack.pop();
288
+ for (const k of nameKeys(r.entry.name)) {
289
+ if (seen.has(k))
290
+ fail(types_1.OfficeDetail.DuplicateName, `Two entries of one storage are named ${(0, exports.showName)(r.entry.name)}`, { name: (0, exports.showName)(r.entry.name) });
291
+ seen.add(k);
292
+ }
293
+ storage.entry.children.push(r.entry);
294
+ entries.push(r.entry);
295
+ starts.push(r.start);
296
+ if (r.entry.kind === 'storage')
297
+ storages.push(r);
298
+ else if (r.child !== NO_STREAM)
299
+ fail(types_1.OfficeDetail.BadEntry, `Stream ${(0, exports.showName)(r.entry.name)} has children`, { reason: 'a stream with children' });
300
+ at = r.right;
301
+ }
302
+ }
303
+ let total = 0;
304
+ const big = [];
305
+ /** File offsets and lengths of the bytes past a stream's end in its last sector. */
306
+ const slack = [];
307
+ const small = [];
308
+ for (const [i, e] of entries.entries()) {
309
+ if (e.kind === 'storage' || e.size === 0)
310
+ continue;
311
+ if (e.size > limits.decompressedBytes)
312
+ limit(types_1.OfficeDetail.DecompressedSize, `${entryPath(e) || 'The mini stream'} is ${e.size} bytes`, { name: entryPath(e), size: e.size, limit: limits.decompressedBytes });
313
+ total += e.size;
314
+ if (total > limits.totalDecompressedBytes)
315
+ limit(types_1.OfficeDetail.TotalDecompressedSize, `The streams hold more than ${limits.totalDecompressedBytes} bytes`, { limit: limits.totalDecompressedBytes });
316
+ if (e.kind === 'stream' && e.size < MINI_CUTOFF) {
317
+ small.push([e, i]);
318
+ continue;
319
+ }
320
+ const what = e.kind === 'root' ? 'mini stream' : `stream ${(0, exports.showName)(e.name)}`;
321
+ const need = Math.ceil(e.size / ss);
322
+ const c = chain(starts[i], e.kind === 'root' ? OWN.miniStream : i + 1, what, false, need);
323
+ for (let k = 0; k < need - 1; k++)
324
+ if (c[k] >= fullSectors)
325
+ fail(types_1.OfficeDetail.OutOfRange, `The ${what} is cut short by the end of the file`);
326
+ const tail = e.size - (need - 1) * ss;
327
+ if ((c[need - 1] + 1) * ss + tail > size)
328
+ fail(types_1.OfficeDetail.OutOfRange, `The ${what} is cut short by the end of the file`);
329
+ big.push([e, c]);
330
+ // The rest of the last sector belongs to no stream.
331
+ if (tail < ss)
332
+ slack.push([(c[need - 1] + 1) * ss + tail, Math.min(ss - tail, size - ((c[need - 1] + 1) * ss + tail))]);
333
+ }
334
+ const cf = new CompoundFile(reader, ss, root, entries, notes, big.find(([e]) => e.kind === 'root')?.[1] ?? new Uint32Array(0), deadline);
335
+ for (const [e, c] of big)
336
+ if (e.kind !== 'root')
337
+ cf.streamChains.set(e, c);
338
+ // The mini FAT, and the mini streams in the mini stream. olefile reads as many mini FAT sectors as the header
339
+ // counts, and other readers follow the chain, so both must cover every mini sector a stream can name.
340
+ const miniFatChain = firstMiniFat === END_OF_CHAIN ? new Uint32Array(0) : chain(firstMiniFat, OWN.miniFat, 'mini FAT', true);
341
+ const miniSectors = Math.ceil(root.size / MINI_SECTOR);
342
+ const miniOwners = new Int32Array(miniSectors);
343
+ /** Mini stream offsets and lengths of the bytes past a mini stream's end in its last mini sector. */
344
+ const miniSlack = [];
345
+ if (small.length) {
346
+ const needFat = Math.ceil(miniSectors / perSector);
347
+ if (miniFatChain.length < needFat || numMiniFat < needFat)
348
+ fail(types_1.OfficeDetail.BadHeader, 'The mini FAT is too short for the mini stream', { reason: 'mini FAT sector count' });
349
+ const miniFat = new Uint32Array(miniFatChain.length * perSector);
350
+ for (const [i, s] of miniFatChain.entries()) {
351
+ const b = await reader.read((s + 1) * ss, ss);
352
+ miniFat.set(new Uint32Array(b.buffer.slice(b.byteOffset, b.byteOffset + ss)), i * perSector);
353
+ }
354
+ for (const [e, i] of small) {
355
+ const what = `stream ${(0, exports.showName)(e.name)}`;
356
+ const out = [];
357
+ const owner = i + 1;
358
+ const need = Math.ceil(e.size / MINI_SECTOR);
359
+ for (let s = starts[i]; out.length < need; s = miniFat[s]) {
360
+ if (s === END_OF_CHAIN)
361
+ fail(types_1.OfficeDetail.DataError, `The ${what} is shorter than its declared size`, { name: entryPath(e), reason: 'a chain shorter than the stream' });
362
+ if (s > MAX_REGULAR)
363
+ fail(types_1.OfficeDetail.BrokenChain, `The ${what} runs into a free or reserved mini sector`, { what });
364
+ if (s >= miniSectors)
365
+ fail(types_1.OfficeDetail.OutOfRange, `A mini sector of the ${what} lies outside the mini stream`);
366
+ if (miniOwners[s] === owner)
367
+ fail(types_1.OfficeDetail.ChainLoop, `The ${what} loops`);
368
+ if (miniOwners[s] !== 0)
369
+ fail(types_1.OfficeDetail.Overlap, `The ${what} shares a mini sector with another stream`, { what });
370
+ miniOwners[s] = owner;
371
+ out.push(s);
372
+ }
373
+ cf.streamChains.set(e, Uint32Array.from(out));
374
+ const tail = e.size - (need - 1) * MINI_SECTOR;
375
+ if (tail < MINI_SECTOR)
376
+ miniSlack.push([out[need - 1] * MINI_SECTOR + tail, Math.min(MINI_SECTOR - tail, root.size - (out[need - 1] * MINI_SECTOR + tail))]);
377
+ }
378
+ }
379
+ // Bytes no stream covers: sectors nothing owns, mini sectors no stream owns, the rest of each stream's last
380
+ // sector, and directory entries the tree does not reach. Zeros are padding and count for nothing; anything else
381
+ // could carry content that a reader of the raw file finds and a rewrite leaves out.
382
+ let hidden = 0;
383
+ for (let s = 0; s < sectors; s++) {
384
+ if (owners[s] !== 0)
385
+ continue;
386
+ (0, errors_1.checkTime)(deadline);
387
+ hidden += content(await reader.read((s + 1) * ss, Math.min(ss, size - (s + 1) * ss)));
388
+ }
389
+ if (major === 4)
390
+ hidden += content((await reader.read(0, ss)).subarray(512));
391
+ for (const [at, n] of slack)
392
+ hidden += content(await reader.read(at, n));
393
+ for (let m = 0; m < miniSectors; m++) {
394
+ if (miniOwners[m] !== 0)
395
+ continue;
396
+ hidden += content(await cf.readMini(m * MINI_SECTOR, Math.min(MINI_SECTOR, root.size - m * MINI_SECTOR)));
397
+ }
398
+ for (const [at, n] of miniSlack)
399
+ hidden += content(await cf.readMini(at, n));
400
+ for (let id = 0; id < dirCount; id++)
401
+ if (!reached[id])
402
+ hidden += unusedEntry(await rawEntry(id));
403
+ if (hidden > 0)
404
+ notes.push({ detail: types_1.OfficeDetail.HiddenData, bytes: hidden });
405
+ return cf;
406
+ }
407
+ /** Bytes of the mini stream, by their offset in it. */
408
+ readMini(offset, length) {
409
+ return this.readChain(this.miniStream, this.sectorSize, this.root.size, offset, length, (s, at, n) => this.reader.read((s + 1) * this.sectorSize + at, n));
410
+ }
411
+ async readChain(chain, unit, size, offset, length, read) {
412
+ const end = Math.min(size, offset + length);
413
+ if (end <= offset)
414
+ return new Uint8Array(0);
415
+ const out = new Uint8Array(end - offset);
416
+ let pos = offset;
417
+ while (pos < end) {
418
+ const k = Math.floor(pos / unit);
419
+ const at = pos % unit;
420
+ const n = Math.min(unit - at, end - pos);
421
+ out.set(await read(chain[k], at, n), pos - offset);
422
+ pos += n;
423
+ }
424
+ return out;
425
+ }
426
+ /** A random-access source over a stream's content. */
427
+ source(e) {
428
+ const chain = this.streamChains.get(e) ?? new Uint32Array(0);
429
+ const mini = e.size < MINI_CUTOFF;
430
+ const read = mini ? (s, at, n) => this.readMini(s * MINI_SECTOR + at, n) : (s, at, n) => this.reader.read((s + 1) * this.sectorSize + at, n);
431
+ const unit = mini ? MINI_SECTOR : this.sectorSize;
432
+ return {
433
+ size: async () => e.size,
434
+ read: (offset, length) => {
435
+ (0, errors_1.checkTime)(this.deadline);
436
+ return this.readChain(chain, unit, e.size, offset, length, read);
437
+ },
438
+ };
439
+ }
440
+ /** A stream's whole content. Callers read only streams whose size they bounded. */
441
+ bytes(e) {
442
+ return this.source(e).read(0, e.size);
443
+ }
444
+ }
445
+ exports.CompoundFile = CompoundFile;
446
+ /**
447
+ * The length of a directory entry the tree does not reach, or 0 when it is unused as MS-CFB writes one: all zeros,
448
+ * with its sibling and child fields 0xFFFFFFFF or zero.
449
+ */
450
+ function unusedEntry(b) {
451
+ const links = b.subarray(0x44, 0x50);
452
+ const blank = links.every(x => x === 0xff) || links.every(x => x === 0);
453
+ return blank && content(b.subarray(0, 0x44)) === 0 && content(b.subarray(0x50)) === 0 ? 0 : b.length;
454
+ }
455
+ /** The length of `b` when it holds anything but zeros, and 0 when it does not. */
456
+ const content = (b) => (b.some(x => x !== 0) ? b.length : 0);
@@ -0,0 +1,66 @@
1
+ import type { FindingFactory } from './findings';
2
+ import type { Format } from './formats';
3
+ import { OfficeCategory as C, OfficeDetail as D, type DefuseFinding } from './types';
4
+ import { type EntityBudget, type XmlHandler, type XmlLimits } from './xml';
5
+ import type { ZipArchive, ZipEntry } from './zip';
6
+ /** Cuts text from the file to a length that fits a finding's data. A reference to an external entity shows as `&name;`. */
7
+ export declare const clip: (raw: string, n?: number) => string;
8
+ /** The limits that bound a container and what is decoded from it, and the caller's password. */
9
+ export interface RunSettings {
10
+ entries: number;
11
+ decompressedBytes: number;
12
+ totalDecompressedBytes: number;
13
+ /** Group nesting in an RTF file, and destinations open at once. */
14
+ rtfDepth: number;
15
+ deadline?: number;
16
+ /** Tried first on an encrypted file, before the password the format opens with by default. */
17
+ password?: string;
18
+ }
19
+ /** What every context of one inspection shares, so a package inside the file reports into the same list. */
20
+ interface Shared {
21
+ findings: DefuseFinding[];
22
+ perKind: Map<string, number>;
23
+ overflow: Map<string, Record<string, string | number>>;
24
+ /** Entity expansion in every part, which the entity limits bound. */
25
+ budget: EntityBudget;
26
+ format?: Format;
27
+ }
28
+ /** What one inspection holds while the format rules run. */
29
+ export declare class RunContext {
30
+ readonly factory: FindingFactory;
31
+ readonly xmlLimits: XmlLimits;
32
+ readonly settings: RunSettings;
33
+ protected readonly shared: Shared;
34
+ constructor(factory: FindingFactory, xmlLimits: XmlLimits, settings: RunSettings, shared?: Shared);
35
+ get findings(): DefuseFinding[];
36
+ /** Set as soon as the rules know it, so a run that stops later still names the format. */
37
+ get format(): Format | undefined;
38
+ set format(f: Format | undefined);
39
+ /** A context for a ZIP archive the rules read, reporting into the same findings. */
40
+ withZip(zip: ZipArchive): ZipContext;
41
+ /** Records a finding. Past 200 of one kind, as in pdf-defuse, one more finding counts the rest. */
42
+ report(category: C, detail: D, location?: string, data?: Record<string, string | number>): void;
43
+ /** Reports a link, classified by pdf-defuse's checks of its target. */
44
+ link(target: string, location: string, data?: Record<string, string>): void;
45
+ /**
46
+ * Reports a reference that loads from outside the file on its own. It is EXTERNAL/LOCAL_FILE, with `kind` saying
47
+ * what refers, when the target is on this machine, and `detail` otherwise.
48
+ */
49
+ external(detail: D, target: string, location: string, data: Record<string, string>, kind: string): void;
50
+ /**
51
+ * Tokenizes XML text, or Office's VML dialect, handing the events to `handler`. A document type declaration is
52
+ * XML/DOCTYPE without an internal subset and XML/INTERNAL_SUBSET with one. A reference to an external entity in
53
+ * content is XML/EXTERNAL_ENTITY.
54
+ */
55
+ parseXml(part: string, chunks: AsyncIterable<Uint8Array> | Iterable<Uint8Array>, handler: XmlHandler, vml?: boolean): Promise<void>;
56
+ }
57
+ /** The context of the rules that read a ZIP archive. */
58
+ export declare class ZipContext extends RunContext {
59
+ readonly zip: ZipArchive;
60
+ constructor(zip: ZipArchive, factory: FindingFactory, xmlLimits: XmlLimits, settings: RunSettings, shared: Shared);
61
+ /** Inflates an entry and tokenizes it as XML, or as Office's VML dialect, handing the events to `handler`. */
62
+ parse(entry: ZipEntry, handler: XmlHandler, vml?: boolean): Promise<void>;
63
+ /** Inflates an entry, which checks it, hands each chunk to `visit`, and tells whether its bytes are an OLE file. */
64
+ drain(entry: ZipEntry, visit?: (chunk: Uint8Array) => void): Promise<boolean>;
65
+ }
66
+ export {};
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ZipContext = exports.RunContext = exports.clip = void 0;
4
+ const links_1 = require("./links");
5
+ const types_1 = require("./types");
6
+ const xml_1 = require("./xml");
7
+ const OLE_SIGNATURE = [0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1];
8
+ /** Cuts text from the file to a length that fits a finding's data. A reference to an external entity shows as `&name;`. */
9
+ const clip = (raw, n = 200) => {
10
+ const s = (0, xml_1.showReferences)(raw);
11
+ return s.length > n ? `${s.slice(0, n - 3)}...` : s;
12
+ };
13
+ exports.clip = clip;
14
+ /** What one inspection holds while the format rules run. */
15
+ class RunContext {
16
+ factory;
17
+ xmlLimits;
18
+ settings;
19
+ shared;
20
+ constructor(factory, xmlLimits, settings, shared) {
21
+ this.factory = factory;
22
+ this.xmlLimits = xmlLimits;
23
+ this.settings = settings;
24
+ this.shared = shared ?? { findings: [], perKind: new Map(), overflow: new Map(), budget: { expansions: 0, size: 0 } };
25
+ }
26
+ get findings() {
27
+ return this.shared.findings;
28
+ }
29
+ /** Set as soon as the rules know it, so a run that stops later still names the format. */
30
+ get format() {
31
+ return this.shared.format;
32
+ }
33
+ set format(f) {
34
+ this.shared.format = f;
35
+ }
36
+ /** A context for a ZIP archive the rules read, reporting into the same findings. */
37
+ withZip(zip) {
38
+ return new ZipContext(zip, this.factory, this.xmlLimits, this.settings, this.shared);
39
+ }
40
+ /** Records a finding. Past 200 of one kind, as in pdf-defuse, one more finding counts the rest. */
41
+ report(category, detail, location, data) {
42
+ const k = `${category}/${detail}`;
43
+ const n = (this.shared.perKind.get(k) ?? 0) + 1;
44
+ this.shared.perKind.set(k, n);
45
+ if (n <= 200) {
46
+ this.shared.findings.push(this.factory.make(category, detail, location, data));
47
+ return;
48
+ }
49
+ let counted = this.shared.overflow.get(k);
50
+ if (!counted) {
51
+ // The first overflow fills the description's placeholders.
52
+ counted = { ...data, count: 0 };
53
+ this.shared.overflow.set(k, counted);
54
+ this.shared.findings.push(this.factory.make(category, detail, 'additional occurrences', counted));
55
+ }
56
+ counted.count = Number(counted.count) + 1;
57
+ }
58
+ /** Reports a link, classified by pdf-defuse's checks of its target. */
59
+ link(target, location, data = {}) {
60
+ const v = (0, links_1.checkLink)(target);
61
+ this.report(v.category, v.detail, location, { ...data, target: (0, exports.clip)(target, 2000), ...(v.host ? { host: (0, exports.clip)(v.host) } : {}) });
62
+ }
63
+ /**
64
+ * Reports a reference that loads from outside the file on its own. It is EXTERNAL/LOCAL_FILE, with `kind` saying
65
+ * what refers, when the target is on this machine, and `detail` otherwise.
66
+ */
67
+ external(detail, target, location, data, kind) {
68
+ const d = { ...data, target: (0, exports.clip)(target, 2000) };
69
+ if ((0, links_1.isLocalTarget)(target))
70
+ this.report(types_1.OfficeCategory.External, types_1.OfficeDetail.LocalFile, location, { ...d, kind });
71
+ else
72
+ this.report(types_1.OfficeCategory.External, detail, location, d);
73
+ }
74
+ /**
75
+ * Tokenizes XML text, or Office's VML dialect, handing the events to `handler`. A document type declaration is
76
+ * XML/DOCTYPE without an internal subset and XML/INTERNAL_SUBSET with one. A reference to an external entity in
77
+ * content is XML/EXTERNAL_ENTITY.
78
+ */
79
+ async parseXml(part, chunks, handler, vml = false) {
80
+ const notes = {
81
+ doctype: d => {
82
+ const ids = { ...(d.publicId === undefined ? {} : { publicId: (0, exports.clip)(d.publicId) }), ...(d.systemId === undefined ? {} : { systemId: (0, exports.clip)(d.systemId) }) };
83
+ if (!d.internalSubset)
84
+ this.report(types_1.OfficeCategory.Xml, types_1.OfficeDetail.Doctype, part, { part, name: (0, exports.clip)(d.name), ...ids });
85
+ else {
86
+ const counts = { entities: d.entities, externalEntities: d.externalEntities, attributeDefaults: d.attributeDefaults, skipped: d.skipped };
87
+ this.report(types_1.OfficeCategory.Xml, types_1.OfficeDetail.InternalSubset, part, { part, name: (0, exports.clip)(d.name), ...ids, ...counts });
88
+ }
89
+ },
90
+ externalEntity: r => this.report(types_1.OfficeCategory.Xml, types_1.OfficeDetail.ExternalEntity, part, { part, entity: (0, exports.clip)(r.name), target: (0, exports.clip)(r.systemId, 2000), ...(r.publicId === undefined ? {} : { publicId: (0, exports.clip)(r.publicId) }) }),
91
+ };
92
+ const t = new xml_1.XmlTokenizer(handler, this.xmlLimits, part, { vml, notes, budget: this.shared.budget });
93
+ for await (const chunk of chunks)
94
+ t.write(chunk);
95
+ t.end();
96
+ }
97
+ }
98
+ exports.RunContext = RunContext;
99
+ /** The context of the rules that read a ZIP archive. */
100
+ class ZipContext extends RunContext {
101
+ zip;
102
+ constructor(zip, factory, xmlLimits, settings, shared) {
103
+ super(factory, xmlLimits, settings, shared);
104
+ this.zip = zip;
105
+ }
106
+ /** Inflates an entry and tokenizes it as XML, or as Office's VML dialect, handing the events to `handler`. */
107
+ parse(entry, handler, vml = false) {
108
+ return this.parseXml(entry.name, this.zip.read(entry), handler, vml);
109
+ }
110
+ /** Inflates an entry, which checks it, hands each chunk to `visit`, and tells whether its bytes are an OLE file. */
111
+ async drain(entry, visit) {
112
+ const head = [];
113
+ for await (const chunk of this.zip.read(entry)) {
114
+ visit?.(chunk);
115
+ for (let i = 0; i < chunk.length && head.length < OLE_SIGNATURE.length; i++)
116
+ head.push(chunk[i]);
117
+ }
118
+ return head.length === OLE_SIGNATURE.length && head.every((b, i) => b === OLE_SIGNATURE[i]);
119
+ }
120
+ }
121
+ exports.ZipContext = ZipContext;
@@ -0,0 +1,8 @@
1
+ /** Continues a CRC-32, the one ZIP uses, over `bytes`, with a lookup table. Start from 0. */
2
+ export declare function tableCrc32(bytes: Uint8Array, crc?: number): number;
3
+ /**
4
+ * Continues a CRC-32, the one ZIP uses, over `bytes`. Start from 0. This is zlib.crc32 where Node has it, about 50
5
+ * times faster, and the table otherwise. Node 18 and early releases of Node 20 and 22 lack it. The choice is made
6
+ * once, when the module loads.
7
+ */
8
+ export declare const crc32: (bytes: Uint8Array, crc?: number) => number;