@patrickdk77/office-defuse 0.1.1 → 0.2.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 (82) hide show
  1. package/README.md +211 -43
  2. package/dist/cfb-writer.d.ts +25 -0
  3. package/dist/cfb-writer.js +314 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +357 -0
  6. package/dist/contained.d.ts +46 -0
  7. package/dist/contained.js +152 -0
  8. package/dist/context.d.ts +107 -12
  9. package/dist/context.js +101 -24
  10. package/dist/detect.d.ts +13 -0
  11. package/dist/detect.js +83 -0
  12. package/dist/engine.d.ts +66 -1
  13. package/dist/engine.js +290 -35
  14. package/dist/errors.d.ts +17 -0
  15. package/dist/errors.js +27 -1
  16. package/dist/fields.d.ts +20 -13
  17. package/dist/fields.js +65 -23
  18. package/dist/findings.d.ts +2 -1
  19. package/dist/findings.js +17 -1
  20. package/dist/flat.d.ts +33 -0
  21. package/dist/flat.js +555 -0
  22. package/dist/formats.d.ts +9 -0
  23. package/dist/formats.js +7 -1
  24. package/dist/formulas.d.ts +27 -0
  25. package/dist/formulas.js +157 -1
  26. package/dist/index.d.ts +4 -2
  27. package/dist/index.js +11 -1
  28. package/dist/index.mjs +1 -1
  29. package/dist/io.d.ts +62 -1
  30. package/dist/io.js +245 -1
  31. package/dist/links.d.ts +27 -0
  32. package/dist/links.js +106 -1
  33. package/dist/msg.d.ts +3 -2
  34. package/dist/msg.js +172 -18
  35. package/dist/objects.d.ts +8 -5
  36. package/dist/objects.js +90 -20
  37. package/dist/odf.d.ts +87 -1
  38. package/dist/odf.js +204 -76
  39. package/dist/offcrypto.d.ts +24 -1
  40. package/dist/offcrypto.js +308 -69
  41. package/dist/ole.d.ts +19 -2
  42. package/dist/ole.js +98 -58
  43. package/dist/oleobj.d.ts +11 -0
  44. package/dist/oleobj.js +20 -3
  45. package/dist/ooxml.d.ts +108 -2
  46. package/dist/ooxml.js +392 -109
  47. package/dist/package.d.ts +14 -0
  48. package/dist/package.js +23 -7
  49. package/dist/plan.d.ts +104 -0
  50. package/dist/plan.js +73 -0
  51. package/dist/plugins.d.ts +8 -0
  52. package/dist/plugins.js +35 -0
  53. package/dist/ppt.js +45 -5
  54. package/dist/rewrite-cfb.d.ts +14 -0
  55. package/dist/rewrite-cfb.js +52 -0
  56. package/dist/rewrite-rtf.d.ts +9 -0
  57. package/dist/rewrite-rtf.js +45 -0
  58. package/dist/rewrite-xml.d.ts +36 -0
  59. package/dist/rewrite-xml.js +341 -0
  60. package/dist/rewrite-zip.d.ts +8 -0
  61. package/dist/rewrite-zip.js +498 -0
  62. package/dist/rtf.js +216 -42
  63. package/dist/rtfcomp.d.ts +7 -0
  64. package/dist/rtfcomp.js +80 -0
  65. package/dist/score.js +1 -1
  66. package/dist/sniff.d.ts +5 -0
  67. package/dist/sniff.js +38 -0
  68. package/dist/types.d.ts +66 -6
  69. package/dist/types.js +14 -0
  70. package/dist/vsd.d.ts +13 -0
  71. package/dist/vsd.js +689 -0
  72. package/dist/word.d.ts +4 -4
  73. package/dist/word.js +70 -23
  74. package/dist/xls.d.ts +10 -4
  75. package/dist/xls.js +58 -10
  76. package/dist/xml.d.ts +30 -0
  77. package/dist/xml.js +45 -23
  78. package/dist/zip-writer.d.ts +4 -0
  79. package/dist/zip-writer.js +20 -7
  80. package/dist/zip.d.ts +11 -2
  81. package/dist/zip.js +67 -6
  82. package/package.json +4 -1
package/dist/word.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import type { CfbEntry, CompoundFile } from './cfb';
2
2
  import type { RunContext } from './context';
3
+ /** Whether a WordDocument stream holds a template: its FIB has fDot set. */
4
+ export declare function isWordTemplate(cfb: CompoundFile, stream: CfbEntry): Promise<boolean>;
3
5
  /**
4
6
  * Inspects a Word 6.0 to 2003 document: its FIB, whether it is encrypted, the fields in its text and the template it
5
- * attaches. Returns whether it is a template, or undefined for an `optional` stream that holds no FIB.
7
+ * attaches. An `optional` stream that holds no FIB is not read.
6
8
  */
7
- export declare function wordRules(ctx: RunContext, cfb: CompoundFile, stream: CfbEntry, optional?: boolean): Promise<{
8
- template: boolean;
9
- } | undefined>;
9
+ export declare function wordRules(ctx: RunContext, cfb: CompoundFile, stream: CfbEntry, optional?: boolean): Promise<void>;
package/dist/word.js CHANGED
@@ -1,9 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isWordTemplate = isWordTemplate;
3
4
  exports.wordRules = wordRules;
4
5
  const errors_1 = require("./errors");
5
6
  const fields_1 = require("./fields");
6
7
  const io_1 = require("./io");
8
+ const offcrypto_1 = require("./offcrypto");
7
9
  const oleobj_1 = require("./oleobj");
8
10
  const types_1 = require("./types");
9
11
  /** wIdent of Word 97 and later, and of Word 6.0 and 95 (MS-DOC 2.5.2). */
@@ -14,27 +16,56 @@ const FC_STTBF_ASSOC = 32;
14
16
  const FC_CLX = 33;
15
17
  /** In a Word 6.0 or 95 FIB, the length of the macro table that holds WordBasic macros. Word writes 2 for an empty one. */
16
18
  const WORD6_MACROS = 0x11c;
19
+ /** In a Word 6.0 or 95 FIB, fcClx and lcbClx: where the piece table of a file saved fast lies in the WordDocument stream. */
20
+ const WORD6_CLX = 0x160;
17
21
  /** Text is read in pieces of at most this many characters. */
18
22
  const PIECE = 65536;
19
23
  const u16 = (b, at) => b[at] | (b[at + 1] << 8);
24
+ /** A stream read through its decryption. */
25
+ const decrypted = (src, decrypt) => ({
26
+ size: () => src.size(),
27
+ read: async (offset, length) => decrypt(offset, await src.read(offset, length)),
28
+ });
20
29
  const u32 = (b, at) => (b[at] | (b[at + 1] << 8) | (b[at + 2] << 16) | (b[at + 3] << 24)) >>> 0;
30
+ /**
31
+ * The identifier and flags of the FIB a WordDocument stream starts with (MS-DOC 2.5.2), or undefined when it does not
32
+ * start with the FIB of Word 6.0 to 2003. The FIB's base is never encrypted.
33
+ */
34
+ function fibBase(fib) {
35
+ const ident = fib.length >= 32 ? u16(fib, 0) : 0;
36
+ return ident === WORD8 || ident === WORD6 ? { ident, flags: u16(fib, 10) } : undefined;
37
+ }
38
+ /** Whether a WordDocument stream holds a template: its FIB has fDot set. */
39
+ async function isWordTemplate(cfb, stream) {
40
+ const base = fibBase(await cfb.source(stream).read(0, 32));
41
+ return base !== undefined && (base.flags & 1) === 1;
42
+ }
21
43
  /**
22
44
  * Inspects a Word 6.0 to 2003 document: its FIB, whether it is encrypted, the fields in its text and the template it
23
- * attaches. Returns whether it is a template, or undefined for an `optional` stream that holds no FIB.
45
+ * attaches. An `optional` stream that holds no FIB is not read.
24
46
  */
25
47
  async function wordRules(ctx, cfb, stream, optional = false) {
26
- const source = cfb.source(stream);
27
- const fib = await source.read(0, 1024);
28
- const ident = fib.length >= 32 ? u16(fib, 0) : 0;
29
- if (ident !== WORD8 && ident !== WORD6) {
48
+ let source = cfb.source(stream);
49
+ let fib = await source.read(0, 1024);
50
+ const base = fibBase(fib);
51
+ if (!base) {
30
52
  if (optional)
31
- return undefined;
53
+ return;
32
54
  throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The WordDocument stream does not start with a Word FIB', { reason: 'a WordDocument stream without a FIB' });
33
55
  }
34
- const flags = u16(fib, 10);
35
- // fEncrypted, set for both RC4 encryption and XOR obfuscation.
36
- if (flags & 0x0100)
37
- throw new errors_1.StopError(types_1.OfficeCategory.Encrypted, types_1.OfficeDetail.PasswordRequired, 'The document is encrypted', { stream: stream.name });
56
+ const { ident, flags } = base;
57
+ const tableEntry = cfb.child(cfb.root, flags & 0x0200 ? '1Table' : '0Table');
58
+ let tableSource = tableEntry?.kind === 'stream' ? cfb.source(tableEntry) : undefined;
59
+ // fEncrypted, set for RC4, RC4 CryptoAPI and XOR obfuscation alike. Word 97 to 2003 documents are read through the
60
+ // decryption. A Word 6.0 or 95 document's encryption is another, which this reader does not undo.
61
+ if (flags & 0x0100) {
62
+ if (ident === WORD6 || !tableSource)
63
+ throw new errors_1.StopError(types_1.OfficeCategory.Encrypted, types_1.OfficeDetail.PasswordRequired, 'The document is encrypted', { stream: stream.name });
64
+ const key = await (0, offcrypto_1.unlockDocument)(ctx, fib, tableSource, stream.name);
65
+ source = decrypted(source, key.word);
66
+ tableSource = decrypted(tableSource, key.table);
67
+ fib = await source.read(0, 1024);
68
+ }
38
69
  const location = stream.name;
39
70
  const fields = new fields_1.FieldStack(ctx, location);
40
71
  // Word marks a field's start, separator and end with 0x13, 0x14 and 0x15 in the text. Other control characters in
@@ -47,28 +78,45 @@ async function wordRules(ctx, cfb, stream, optional = false) {
47
78
  fields.separate();
48
79
  else if (ch === '\x15')
49
80
  fields.end();
50
- else if (fields.inCode && (ch >= ' ' || ch === '\t'))
51
- fields.text(ch);
81
+ else if (ch >= ' ' || ch === '\t') {
82
+ if (fields.inCode)
83
+ fields.text(ch);
84
+ else
85
+ fields.result(ch);
86
+ }
52
87
  }
53
88
  };
54
89
  const reader = new io_1.Reader(source, stream.size, 65536, 8);
55
90
  if (ident === WORD6) {
56
- // Word 6.0 and 95 keep their text as 8-bit characters from fcMin to fcMac. A file saved fast adds pieces to the
57
- // end, and this reads every byte between, as msodde does.
58
- const from = u32(fib, 0x18);
59
- const to = Math.min(u32(fib, 0x1c), stream.size);
60
- for (let at = from; at < to; at += PIECE) {
61
- (0, errors_1.checkTime)(ctx.settings.deadline);
62
- text(latin(await reader.read(at, Math.min(PIECE, to - at))));
91
+ // Word 6.0 and 95 keep their text as 8-bit characters. A file saved fast (fComplex) adds pieces to the end, and its
92
+ // piece table, in the WordDocument stream itself at fcClx (FIB offset 0x160), gives their order. Any other file,
93
+ // or one whose piece table does not read, is read from fcMin to fcMac, every byte between, as msodde does.
94
+ const clxAt = u32(fib, WORD6_CLX);
95
+ const clxSize = u32(fib, WORD6_CLX + 4);
96
+ const list = flags & 0x0004 && clxSize > 0 && clxAt + clxSize <= stream.size ? pieces(await reader.read(clxAt, clxSize)).filter(p => !p.compressed && p.fc + p.length <= stream.size) : [];
97
+ if (list.length) {
98
+ for (const p of list) {
99
+ for (let cp = 0; cp < p.length; cp += PIECE) {
100
+ (0, errors_1.checkTime)(ctx.settings.deadline);
101
+ text(latin(await reader.read(p.fc + cp, Math.min(PIECE, p.length - cp))));
102
+ }
103
+ }
104
+ }
105
+ else {
106
+ const from = u32(fib, 0x18);
107
+ const to = Math.min(u32(fib, 0x1c), stream.size);
108
+ for (let at = from; at < to; at += PIECE) {
109
+ (0, errors_1.checkTime)(ctx.settings.deadline);
110
+ text(latin(await reader.read(at, Math.min(PIECE, to - at))));
111
+ }
63
112
  }
64
113
  if (fib.length >= WORD6_MACROS + 4 && u32(fib, WORD6_MACROS) > 2)
65
114
  ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.WordBasic, location, { stream: location, bytes: u32(fib, WORD6_MACROS) });
66
115
  }
67
116
  else {
68
117
  const pairs = fcLcb(fib);
69
- const table = cfb.child(cfb.root, flags & 0x0200 ? '1Table' : '0Table');
70
- if (table?.kind === 'stream') {
71
- const tableSource = cfb.source(table);
118
+ const table = tableEntry;
119
+ if (table?.kind === 'stream' && tableSource) {
72
120
  const clx = pairs[FC_CLX];
73
121
  if (clx && clx.lcb > 0 && clx.fc + clx.lcb <= table.size) {
74
122
  for (const p of pieces(await tableSource.read(clx.fc, clx.lcb))) {
@@ -92,7 +140,6 @@ async function wordRules(ctx, cfb, stream, optional = false) {
92
140
  }
93
141
  }
94
142
  fields.finish();
95
- return { template: (flags & 1) === 1 };
96
143
  }
97
144
  /** The fc and lcb pairs of a Word 97 FIB, after its variable-length arrays of 16- and 32-bit values. */
98
145
  function fcLcb(fib) {
package/dist/xls.d.ts CHANGED
@@ -8,16 +8,22 @@ export type Decrypt = (r: BiffRecord) => Uint8Array;
8
8
  * reads workbooks without knowing how they are encrypted.
9
9
  */
10
10
  export type Unlock = (ctx: RunContext, filePass: BiffRecord, stream: string) => Decrypt | Promise<Decrypt>;
11
+ /**
12
+ * Whether a workbook stream holds a template or an add-in: its globals substream, from the BOF that starts the stream
13
+ * to its EOF, holds a Template or an AddIn record. Record headers are never encrypted, so an encrypted workbook tells
14
+ * this without its password. A stream that does not start with a BOF record is neither.
15
+ */
16
+ export declare function workbookKind(cfb: CompoundFile, stream: CfbEntry, deadline?: number): Promise<{
17
+ template: boolean;
18
+ addIn: boolean;
19
+ }>;
11
20
  /**
12
21
  * Inspects an Excel 5.0 to 2003 workbook stream. Excel reads the globals substream from the start of the stream,
13
22
  * then each sheet from where its BoundSheet record says the sheet starts. The globals give the macro sheets, the
14
23
  * names Excel runs as macros, and the DDE, OLE and workbook links. The sheets give shapes that run macros and
15
24
  * hyperlinks. An encrypted workbook goes through `unlock`.
16
25
  */
17
- export declare function excelRules(ctx: RunContext, cfb: CompoundFile, stream: CfbEntry, unlock: Unlock): Promise<{
18
- template: boolean;
19
- addIn: boolean;
20
- }>;
26
+ export declare function excelRules(ctx: RunContext, cfb: CompoundFile, stream: CfbEntry, unlock: Unlock): Promise<void>;
21
27
  /**
22
28
  * A VirtualPath (MS-XLS 2.5.277) written out. One that starts with U+0001 is encoded: its control characters name a
23
29
  * volume, the root, a folder, a step up, a URL's volume or Excel's startup folders.
package/dist/xls.js CHANGED
@@ -1,15 +1,46 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.workbookKind = workbookKind;
3
4
  exports.excelRules = excelRules;
4
5
  exports.virtualPath = virtualPath;
5
6
  exports.hyperlink = hyperlink;
6
7
  const biff_1 = require("./biff");
7
8
  const context_1 = require("./context");
8
9
  const errors_1 = require("./errors");
10
+ const formulas_1 = require("./formulas");
9
11
  const links_1 = require("./links");
10
12
  const oleobj_1 = require("./oleobj");
11
13
  const types_1 = require("./types");
12
14
  const STATES = ['visible', 'hidden', 'very hidden', 'hidden'];
15
+ /** Record types of cell formulas (MS-XLS 2.4.127, 2.4.260 and 2.4.4): FORMULA, SHRFMLA and ARRAY. */
16
+ const FORMULA = 0x0006;
17
+ const SHRFMLA = 0x04bc;
18
+ const ARRAY = 0x0221;
19
+ /** Where the parsed formula starts in each: its cce, then its rgce. */
20
+ const FORMULA_AT = new Map([
21
+ [FORMULA, 20],
22
+ [SHRFMLA, 8],
23
+ [ARRAY, 12],
24
+ ]);
25
+ /**
26
+ * Whether a workbook stream holds a template or an add-in: its globals substream, from the BOF that starts the stream
27
+ * to its EOF, holds a Template or an AddIn record. Record headers are never encrypted, so an encrypted workbook tells
28
+ * this without its password. A stream that does not start with a BOF record is neither.
29
+ */
30
+ async function workbookKind(cfb, stream, deadline) {
31
+ const kind = { template: false, addIn: false };
32
+ const records = new biff_1.RecordReader(cfb, stream, 0, deadline);
33
+ const first = await records.next();
34
+ if (first?.type !== biff_1.R.bof || first.body.length < 4)
35
+ return kind;
36
+ for (let r = await records.next(); r && r.type !== biff_1.R.eof; r = await records.next()) {
37
+ if (r.type === biff_1.R.template)
38
+ kind.template = true;
39
+ else if (r.type === biff_1.R.addIn)
40
+ kind.addIn = true;
41
+ }
42
+ return kind;
43
+ }
13
44
  /**
14
45
  * Inspects an Excel 5.0 to 2003 workbook stream. Excel reads the globals substream from the start of the stream,
15
46
  * then each sheet from where its BoundSheet record says the sheet starts. The globals give the macro sheets, the
@@ -26,7 +57,9 @@ async function excelRules(ctx, cfb, stream, unlock) {
26
57
  let decrypt = r => r.body;
27
58
  const sheets = [];
28
59
  const books = [];
29
- const flags = { template: false, addIn: false };
60
+ const names = [];
61
+ /** The SupBook each XTI of the ExternSheet record names. */
62
+ const xti = [];
30
63
  for (let r = await records.next(); r && r.type !== biff_1.R.eof; r = await records.next()) {
31
64
  if (r.type === biff_1.R.filePass) {
32
65
  decrypt = await unlock(ctx, r, location);
@@ -35,21 +68,25 @@ async function excelRules(ctx, cfb, stream, unlock) {
35
68
  const body = decrypt(r);
36
69
  if (r.type === biff_1.R.boundSheet)
37
70
  sheets.push((0, biff_1.parseBoundSheet)(body, biff8));
38
- else if (r.type === biff_1.R.template)
39
- flags.template = true;
40
- else if (r.type === biff_1.R.addIn)
41
- flags.addIn = true;
42
71
  else if (r.type === biff_1.R.lbl) {
43
72
  const name = (0, biff_1.parseLbl)(body, biff8);
73
+ names.push(name.name);
44
74
  const auto = (0, biff_1.autoName)(name.name);
45
75
  if (auto)
46
76
  ctx.report(types_1.OfficeCategory.Macro, auto.onOpen ? types_1.OfficeDetail.EventOnOpen : types_1.OfficeDetail.Event, location, { event: auto.event, name: (0, context_1.clip)(name.name) });
47
77
  }
48
78
  else if (r.type === biff_1.R.supBook && body.length >= 4) {
49
79
  const cch = body[2] | (body[3] << 8);
50
- books.push({ ctab: body[0] | (body[1] << 8), cch, path: cch > 0 && cch <= 0xff ? (0, biff_1.xlString)(body, 4, cch).text : '', ole: false, dde: false });
80
+ books.push({ ctab: body[0] | (body[1] << 8), cch, path: cch > 0 && cch <= 0xff ? (0, biff_1.xlString)(body, 4, cch).text : '', ole: false, dde: false, names: [] });
81
+ }
82
+ else if (r.type === biff_1.R.externSheet && biff8 && body.length >= 2) {
83
+ const n = body[0] | (body[1] << 8);
84
+ for (let k = 0; k < n && 2 + k * 6 + 2 <= body.length; k++)
85
+ xti.push(body[2 + k * 6] | (body[3 + k * 6] << 8));
51
86
  }
52
87
  else if (r.type === biff_1.R.externName && body.length >= 2 && books.length) {
88
+ if (biff8 && body.length >= 7)
89
+ books[books.length - 1].names.push((0, biff_1.xlString)(body, 7, body[6]).text);
53
90
  // fOle and fOleLink mark an OLE object's name. Excel writes StdDocumentName with fOle beside the items of a DDE
54
91
  // link, so a link with any DDE item is a DDE link.
55
92
  if ((body[0] | (body[1] << 8)) & 0x18)
@@ -60,9 +97,9 @@ async function excelRules(ctx, cfb, stream, unlock) {
60
97
  }
61
98
  for (const b of books)
62
99
  linkRules(ctx, b, location);
100
+ const nameOf = (ref) => (ref.kind === 'name' ? names[ref.index - 1] : books[xti[ref.ixti] ?? -1]?.names[ref.index - 1]);
63
101
  for (const s of sheets)
64
- await sheetRules(ctx, cfb, stream, s, decrypt, location);
65
- return flags;
102
+ await sheetRules(ctx, cfb, stream, s, decrypt, location, biff8 ? nameOf : undefined);
66
103
  }
67
104
  /** A SupBook record, read as MS-XLS 2.4.271 and oletools tell its kinds apart. */
68
105
  function linkRules(ctx, b, location) {
@@ -115,14 +152,15 @@ function virtualPath(p) {
115
152
  return out;
116
153
  }
117
154
  /** One sheet's substream, with any chart substreams inside it: Obj records that run macros, and hyperlinks. */
118
- async function sheetRules(ctx, cfb, stream, s, decrypt, location) {
155
+ async function sheetRules(ctx, cfb, stream, s, decrypt, location, nameOf) {
119
156
  const records = new biff_1.RecordReader(cfb, stream, s.position, ctx.settings.deadline);
120
157
  const bof = await records.next();
121
158
  if (bof?.type !== biff_1.R.bof || bof.body.length < 4)
122
159
  return;
123
160
  const bofType = bof.body[2] | (bof.body[3] << 8);
124
161
  // The sheet's own BOF says what it is, and so does its BoundSheet record. Either one makes a macro sheet.
125
- if (s.type === 1 || bofType === 0x0040)
162
+ const macroSheet = s.type === 1 || bofType === 0x0040;
163
+ if (macroSheet)
126
164
  ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.XlmMacrosheet, location, { sheet: (0, context_1.clip)(s.name), state: STATES[s.state] });
127
165
  let depth = 1;
128
166
  for (let r = await records.next(); r && depth > 0; r = await records.next()) {
@@ -139,6 +177,16 @@ async function sheetRules(ctx, cfb, stream, s, decrypt, location) {
139
177
  if (target.trim() && !(0, links_1.isFragment)(target))
140
178
  ctx.link(target, location, { sheet: (0, context_1.clip)(s.name), source: 'HLink record' });
141
179
  }
180
+ else if (nameOf && !macroSheet && FORMULA_AT.has(r.type)) {
181
+ // A macro sheet's formulas are its macros, which its own finding covers.
182
+ const b = decrypt(r);
183
+ const at = FORMULA_AT.get(r.type) ?? 0;
184
+ if (b.length < at + 2)
185
+ continue;
186
+ const cce = b[at] | (b[at + 1] << 8);
187
+ for (const [fn, kind] of (0, formulas_1.biffCalls)(b.subarray(at + 2, at + 2 + cce), nameOf))
188
+ ctx.report(kind === 'fetch' ? types_1.OfficeCategory.External : types_1.OfficeCategory.Macro, types_1.OfficeDetail.Formula, location, { sheet: (0, context_1.clip)(s.name), function: (0, context_1.clip)(fn) });
189
+ }
142
190
  }
143
191
  }
144
192
  /** Whether an Obj record (MS-XLS 2.4.181) holds an FtMacro subrecord, the macro a click runs. */
package/dist/xml.d.ts CHANGED
@@ -32,6 +32,17 @@ export interface XmlElement {
32
32
  */
33
33
  start: number;
34
34
  end: number;
35
+ /** The element's position among the elements of the part, from 0, counting those in entity text. */
36
+ index: number;
37
+ /** The namespace declarations of the start tag, as prefix ('' for the default namespace) and URI, defaults included. */
38
+ namespaces: Array<[string, string]>;
39
+ /** Written as an empty-element tag. */
40
+ empty: boolean;
41
+ /**
42
+ * The start tag does not say on its own what the attributes are: a value refers to an entity the internal subset
43
+ * declares, or an attribute-list declaration added a default or normalized a value.
44
+ */
45
+ rewritten: boolean;
35
46
  }
36
47
  export interface XmlHandler {
37
48
  start?(el: XmlElement): void;
@@ -87,6 +98,12 @@ export interface XmlExternalReference {
87
98
  export interface XmlNotes {
88
99
  doctype?(d: XmlDoctype): void;
89
100
  externalEntity?(r: XmlExternalReference): void;
101
+ /**
102
+ * A reference to an internal entity in content, outside any other entity's text, at these offsets. The events of
103
+ * its text come between this call and `referenceEnd`.
104
+ */
105
+ referenceStart?(start: number, end: number): void;
106
+ referenceEnd?(): void;
90
107
  }
91
108
  export interface XmlOptions {
92
109
  /** Reads the dialect Office writes in VML drawing parts. See XmlTokenizer. */
@@ -99,6 +116,15 @@ export interface XmlOptions {
99
116
  export declare function attr(el: XmlElement, ns: string, local: string): string | undefined;
100
117
  /** An XML name without a colon. */
101
118
  export declare const isNcName: (s: string) => boolean;
119
+ export type XmlEncoding = 'utf-8' | 'utf-16le' | 'utf-16be';
120
+ /**
121
+ * The encoding of an XML part from its first four bytes, as XML 1.0 Appendix F tells it, and the length of its byte
122
+ * order mark. Undefined for UTF-32, which no Office application reads.
123
+ */
124
+ export declare function detectEncoding(b: Uint8Array): {
125
+ encoding: XmlEncoding;
126
+ bom: number;
127
+ } | undefined;
102
128
  /**
103
129
  * A streaming XML 1.0 tokenizer with namespaces. It refuses references to entities nothing declares, encodings other
104
130
  * than UTF-8 and UTF-16, and anything not well-formed. It holds one markup token at a time; text goes to the handler
@@ -133,6 +159,10 @@ export declare class XmlTokenizer {
133
159
  /** The XML declaration says standalone="yes". */
134
160
  private standalone;
135
161
  private encoding;
162
+ /** Start tags read so far, which numbers the elements. */
163
+ private elements;
164
+ /** An attribute value of the tag being read referred to an entity of the internal subset. */
165
+ private generalReference;
136
166
  /** The last text piece ended in CR, so an LF that starts the next piece belongs to the same line end. */
137
167
  private lastCr;
138
168
  private scan?;
package/dist/xml.js CHANGED
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.XmlTokenizer = exports.isNcName = exports.unknownEntity = exports.showReferences = exports.UNKNOWN = exports.XMLNS_NS = exports.XML_NS = void 0;
4
4
  exports.attr = attr;
5
+ exports.detectEncoding = detectEncoding;
5
6
  const node_util_1 = require("node:util");
6
7
  const errors_1 = require("./errors");
7
8
  const types_1 = require("./types");
@@ -53,6 +54,26 @@ const NAME_END = /[\s>[\]"'%;&<]/;
53
54
  /** Attribute types other than CDATA, whose values are normalized further. NOTATION and enumerations are read apart. */
54
55
  const TOKENIZED_TYPES = new Set(['ID', 'IDREF', 'IDREFS', 'ENTITY', 'ENTITIES', 'NMTOKEN', 'NMTOKENS']);
55
56
  const LIMIT_DETAILS = new Set([types_1.OfficeDetail.XmlDepth, types_1.OfficeDetail.XmlTokenSize, types_1.OfficeDetail.XmlEntityDepth, types_1.OfficeDetail.XmlEntityExpansions, types_1.OfficeDetail.XmlEntitySize]);
57
+ /**
58
+ * The encoding of an XML part from its first four bytes, as XML 1.0 Appendix F tells it, and the length of its byte
59
+ * order mark. Undefined for UTF-32, which no Office application reads.
60
+ */
61
+ function detectEncoding(b) {
62
+ const at = (i) => (i < b.length ? b[i] : -1);
63
+ if ((at(0) === 0 && at(1) === 0) || (at(0) === 0xff && at(1) === 0xfe && at(2) === 0 && at(3) === 0) || (at(0) === 0x3c && at(1) === 0 && at(2) === 0 && at(3) === 0))
64
+ return undefined;
65
+ if (at(0) === 0xef && at(1) === 0xbb && at(2) === 0xbf)
66
+ return { encoding: 'utf-8', bom: 3 };
67
+ if (at(0) === 0xff && at(1) === 0xfe)
68
+ return { encoding: 'utf-16le', bom: 2 };
69
+ if (at(0) === 0xfe && at(1) === 0xff)
70
+ return { encoding: 'utf-16be', bom: 2 };
71
+ if (at(0) === 0x3c && at(1) === 0 && at(2) === 0x3f && at(3) === 0)
72
+ return { encoding: 'utf-16le', bom: 0 };
73
+ if (at(0) === 0 && at(1) === 0x3c && at(2) === 0 && at(3) === 0x3f)
74
+ return { encoding: 'utf-16be', bom: 0 };
75
+ return { encoding: 'utf-8', bom: 0 };
76
+ }
56
77
  const isSpace = (c) => c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d;
57
78
  /** A character XML 1.0 allows. The decoders leave no lone surrogates, so the halves of a pair pass here. */
58
79
  const isXmlChar = (c) => c === 0x09 || c === 0x0a || c === 0x0d || (c >= 0x20 && c !== 0xfffe && c !== 0xffff);
@@ -169,6 +190,10 @@ class XmlTokenizer {
169
190
  /** The XML declaration says standalone="yes". */
170
191
  standalone = false;
171
192
  encoding = 'utf-8';
193
+ /** Start tags read so far, which numbers the elements. */
194
+ elements = 0;
195
+ /** An attribute value of the tag being read referred to an entity of the internal subset. */
196
+ generalReference = false;
172
197
  /** The last text piece ended in CR, so an LF that starts the next piece belongs to the same line end. */
173
198
  lastCr = false;
174
199
  scan;
@@ -227,29 +252,14 @@ class XmlTokenizer {
227
252
  /** Picks the encoding from the first bytes, as XML 1.0 Appendix F does, and reads the bytes held so far. */
228
253
  begin() {
229
254
  const b = this.head;
230
- const at = (i) => (i < b.length ? b[i] : -1);
231
- if ((at(0) === 0 && at(1) === 0) || (at(0) === 0xff && at(1) === 0xfe && at(2) === 0 && at(3) === 0) || (at(0) === 0x3c && at(1) === 0 && at(2) === 0 && at(3) === 0)) {
232
- this.stop(types_1.OfficeDetail.Encoding, 'UTF-32 is not an encoding Office reads');
233
- }
234
- let skip = 0;
235
- if (at(0) === 0xef && at(1) === 0xbb && at(2) === 0xbf)
236
- skip = 3;
237
- else if (at(0) === 0xff && at(1) === 0xfe) {
238
- this.encoding = 'utf-16le';
239
- skip = 2;
240
- }
241
- else if (at(0) === 0xfe && at(1) === 0xff) {
242
- this.encoding = 'utf-16be';
243
- skip = 2;
244
- }
245
- else if (at(0) === 0x3c && at(1) === 0 && at(2) === 0x3f && at(3) === 0)
246
- this.encoding = 'utf-16le';
247
- else if (at(0) === 0 && at(1) === 0x3c && at(2) === 0 && at(3) === 0x3f)
248
- this.encoding = 'utf-16be';
255
+ const found = detectEncoding(b);
256
+ if (!found)
257
+ return this.stop(types_1.OfficeDetail.Encoding, 'UTF-32 is not an encoding Office reads');
258
+ this.encoding = found.encoding;
249
259
  const decoder = new node_util_1.TextDecoder(this.encoding, { fatal: true, ignoreBOM: true });
250
260
  this.decoder = decoder;
251
261
  this.head = new Uint8Array(0);
252
- this.feed(this.decode(decoder, b.subarray(skip), true), false);
262
+ this.feed(this.decode(decoder, b.subarray(found.bom), true), false);
253
263
  return decoder;
254
264
  }
255
265
  decode(decoder, bytes, more) {
@@ -467,6 +477,8 @@ class XmlTokenizer {
467
477
  return;
468
478
  }
469
479
  this.enter(name, e.text);
480
+ if (!this.span)
481
+ this.notes.referenceStart?.(start, end);
470
482
  const { buf, pos, base, floor, span: outer } = this;
471
483
  this.buf = e.text;
472
484
  this.pos = 0;
@@ -482,6 +494,8 @@ class XmlTokenizer {
482
494
  this.floor = floor;
483
495
  this.span = outer;
484
496
  this.active.pop();
497
+ if (!outer)
498
+ this.notes.referenceEnd?.();
485
499
  }
486
500
  /** A reference to a general entity in an attribute value. Its text is normalized into the value. */
487
501
  attributeReference(name) {
@@ -489,6 +503,7 @@ class XmlTokenizer {
489
503
  if (!('text' in e))
490
504
  this.malformed(`A reference to external entity "${name}" in an attribute value`);
491
505
  this.enter(name, e.text);
506
+ this.generalReference = true;
492
507
  const value = this.attributeValue(e.text, true);
493
508
  this.active.pop();
494
509
  return value;
@@ -981,6 +996,7 @@ class XmlTokenizer {
981
996
  const qname = body.slice(0, i);
982
997
  const raw = [];
983
998
  let selfClosing = this.vml && qname.toLowerCase() === 'br';
999
+ this.generalReference = false;
984
1000
  for (;;) {
985
1001
  const before = i;
986
1002
  while (i < n && isSpace(body.charCodeAt(i)))
@@ -1017,18 +1033,24 @@ class XmlTokenizer {
1017
1033
  // Attribute-list declarations normalize the attributes they type, and add the defaults the tag leaves out. Each
1018
1034
  // default added counts against the entity limits, so declarations cannot multiply the work of a long part.
1019
1035
  const declared = this.declared.get(qname);
1036
+ let rewritten = this.generalReference;
1020
1037
  if (declared) {
1021
1038
  const given = new Set();
1022
1039
  for (const a of raw) {
1023
1040
  given.add(a[0]);
1024
- if (declared.tokenized.has(a[0]))
1025
- a[1] = tokenize(a[1]);
1041
+ if (!declared.tokenized.has(a[0]))
1042
+ continue;
1043
+ const v = tokenize(a[1]);
1044
+ if (v !== a[1])
1045
+ rewritten = true;
1046
+ a[1] = v;
1026
1047
  }
1027
1048
  for (const [name, value] of declared.defaults) {
1028
1049
  if (given.has(name))
1029
1050
  continue;
1030
1051
  this.charge(value.length);
1031
1052
  raw.push([name, value]);
1053
+ rewritten = true;
1032
1054
  }
1033
1055
  }
1034
1056
  const scope = new Map();
@@ -1061,7 +1083,7 @@ class XmlTokenizer {
1061
1083
  const depth = this.stack.length + 1;
1062
1084
  if (depth > this.limits.depth)
1063
1085
  this.stop(types_1.OfficeDetail.XmlDepth, `Elements nest deeper than ${this.limits.depth}`);
1064
- const el = { ns: '', local: '', qname, attributes: [], depth, start, end };
1086
+ const el = { ns: '', local: '', qname, attributes: [], depth, start, end, index: this.elements++, namespaces: [...scope], empty: selfClosing, rewritten };
1065
1087
  const open = { el, scope: scope.size ? scope : undefined };
1066
1088
  this.stack.push(open);
1067
1089
  const [prefix, local] = this.split(qname);
@@ -24,6 +24,8 @@ export declare class ZipWriter {
24
24
  constructor(sink: ByteSink, options?: {
25
25
  forceZip64?: boolean;
26
26
  });
27
+ /** Bytes written so far: where the next entry's local header goes. */
28
+ get position(): number;
27
29
  private put;
28
30
  /** Adds an entry from bytes in memory, deflated unless `compress` is false. A name ending in "/" is a directory. */
29
31
  add(name: string, data: Uint8Array, options?: {
@@ -31,6 +33,8 @@ export declare class ZipWriter {
31
33
  dosTime?: number;
32
34
  dosDate?: number;
33
35
  }): Promise<void>;
36
+ /** The local header `copy` writes for an entry. */
37
+ localHeader(info: CopiedEntry): Uint8Array;
34
38
  /** Adds an entry whose compressed bytes come from `chunks` unchanged, such as an entry of the archive being rewritten. */
35
39
  copy(info: CopiedEntry, chunks: AsyncIterable<Uint8Array>): Promise<void>;
36
40
  /** Writes the central directory and the end records. The caller closes the sink. */
@@ -76,6 +76,10 @@ class ZipWriter {
76
76
  this.sink = sink;
77
77
  this.options = options;
78
78
  }
79
+ /** Bytes written so far: where the next entry's local header goes. */
80
+ get position() {
81
+ return this.offset;
82
+ }
79
83
  async put(b) {
80
84
  await this.sink.write(b);
81
85
  this.offset += b.length;
@@ -89,15 +93,11 @@ class ZipWriter {
89
93
  yield body;
90
94
  })());
91
95
  }
92
- /** Adds an entry whose compressed bytes come from `chunks` unchanged, such as an entry of the archive being rewritten. */
93
- async copy(info, chunks) {
94
- if (this.names.has(info.name))
95
- throw new Error(`Entry ${info.name} is already in the archive`);
96
- this.names.add(info.name);
96
+ /** The local header `copy` writes for an entry. */
97
+ localHeader(info) {
97
98
  const name = Buffer.from(info.name, 'utf8');
98
99
  const flags = /^[\x20-\x7e]*$/.test(info.name) ? 0 : 0x0800;
99
100
  const big = this.options.forceZip64 === true || info.size >= MAX32 || info.compressedSize >= MAX32;
100
- const localOffset = this.offset;
101
101
  const time = info.dosTime ?? 0;
102
102
  const date = info.dosDate ?? DOS_DATE;
103
103
  const local = new Builder()
@@ -115,7 +115,20 @@ class ZipWriter {
115
115
  .bytes(name);
116
116
  if (big)
117
117
  local.u16(0x0001).u16(16).u64(info.size).u64(info.compressedSize);
118
- await this.put(local.build());
118
+ return local.build();
119
+ }
120
+ /** Adds an entry whose compressed bytes come from `chunks` unchanged, such as an entry of the archive being rewritten. */
121
+ async copy(info, chunks) {
122
+ if (this.names.has(info.name))
123
+ throw new Error(`Entry ${info.name} is already in the archive`);
124
+ this.names.add(info.name);
125
+ const name = Buffer.from(info.name, 'utf8');
126
+ const flags = /^[\x20-\x7e]*$/.test(info.name) ? 0 : 0x0800;
127
+ const big = this.options.forceZip64 === true || info.size >= MAX32 || info.compressedSize >= MAX32;
128
+ const localOffset = this.offset;
129
+ const time = info.dosTime ?? 0;
130
+ const date = info.dosDate ?? DOS_DATE;
131
+ await this.put(this.localHeader(info));
119
132
  let written = 0;
120
133
  for await (const c of chunks) {
121
134
  written += c.length;
package/dist/zip.d.ts CHANGED
@@ -27,10 +27,17 @@ export interface ZipEntry {
27
27
  }
28
28
  /** Something the reader saw that it does not refuse. Each is a strip finding, since the rewrite drops the bytes. */
29
29
  export interface ZipNote {
30
- detail: D.LeadingBytes | D.HiddenData | D.TrailingBytes;
30
+ detail: D.LeadingBytes | D.HiddenData | D.HiddenEntry | D.TrailingBytes;
31
31
  bytes: number;
32
+ /** For a hidden entry, where its local header sits. */
33
+ offset?: number;
32
34
  }
33
- /** The key two names collide on: percent-decoded, NFC and lowercase, without a directory's slash. */
35
+ /**
36
+ * The key two names collide on: percent-decoded, NFKC and case-folded, without a directory's slash, and without the
37
+ * dots and spaces Windows drops from the end of each segment. Upper then lower case is the full folding, which takes a
38
+ * sharp s to ss as macOS's case-insensitive file system does. NFKC also joins compatibility forms such as ligatures
39
+ * and full-width letters.
40
+ */
34
41
  export declare function nameKey(name: string): string;
35
42
  /**
36
43
  * A ZIP archive read from its central directory, after every local header was checked against it. Reading an entry
@@ -41,6 +48,8 @@ export declare class ZipArchive {
41
48
  readonly entries: ZipEntry[];
42
49
  readonly notes: ZipNote[];
43
50
  private readonly deadline;
51
+ /** Where the archive's data starts: the first local header, or the central directory of an archive without entries. */
52
+ readonly start: number;
44
53
  private readonly byKey;
45
54
  private constructor();
46
55
  /** The entry with this name, compared as entry names collide: ignoring case. */