@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/oleobj.js ADDED
@@ -0,0 +1,198 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.utf16 = exports.latin1 = exports.ByteReader = exports.Truncated = void 0;
4
+ exports.bounded = bounded;
5
+ exports.parseCompObj = parseCompObj;
6
+ exports.readMoniker = readMoniker;
7
+ exports.parseOleStream = parseOleStream;
8
+ exports.parsePackage = parsePackage;
9
+ exports.parseOle1 = parseOle1;
10
+ const cfb_1 = require("./cfb");
11
+ /** Thrown by ByteReader when a structure runs past the end of its bytes. Parsers catch it and keep what they read. */
12
+ class Truncated extends Error {
13
+ }
14
+ exports.Truncated = Truncated;
15
+ /** Runs `read`. A structure that runs past its data gives what `partial` returns instead, and any other error goes up. */
16
+ function bounded(read, partial) {
17
+ try {
18
+ return read();
19
+ }
20
+ catch (e) {
21
+ if (e instanceof Truncated)
22
+ return partial();
23
+ throw e;
24
+ }
25
+ }
26
+ /** Little-endian reads over bytes in memory, bounded by their end. */
27
+ class ByteReader {
28
+ b;
29
+ pos = 0;
30
+ constructor(b) {
31
+ this.b = b;
32
+ }
33
+ get left() {
34
+ return this.b.length - this.pos;
35
+ }
36
+ need(n) {
37
+ if (n < 0 || this.pos + n > this.b.length)
38
+ throw new Truncated('The structure runs past its data');
39
+ const at = this.pos;
40
+ this.pos += n;
41
+ return at;
42
+ }
43
+ u8() {
44
+ return this.b[this.need(1)];
45
+ }
46
+ u16() {
47
+ const at = this.need(2);
48
+ return this.b[at] | (this.b[at + 1] << 8);
49
+ }
50
+ u32() {
51
+ const at = this.need(4);
52
+ return (this.b[at] | (this.b[at + 1] << 8) | (this.b[at + 2] << 16) | (this.b[at + 3] << 24)) >>> 0;
53
+ }
54
+ bytes(n) {
55
+ const at = this.need(n);
56
+ return this.b.subarray(at, at + n);
57
+ }
58
+ skip(n) {
59
+ this.need(n);
60
+ }
61
+ /** An ANSI string ended by a NUL within `max` bytes, read as Latin-1. */
62
+ ansiz(max = 1024) {
63
+ const end = this.b.indexOf(0, this.pos);
64
+ if (end < 0 || end - this.pos >= max)
65
+ throw new Truncated('No NUL ends the string');
66
+ const s = (0, exports.latin1)(this.b.subarray(this.pos, end));
67
+ this.pos = end + 1;
68
+ return s;
69
+ }
70
+ /** A 32-bit byte count, then that many bytes of ANSI text whose last is a NUL. */
71
+ lengthPrefixedAnsi(max = 65536) {
72
+ const n = this.u32();
73
+ if (n > max)
74
+ throw new Truncated('A string length past any real string');
75
+ if (n === 0)
76
+ return '';
77
+ const s = this.bytes(n);
78
+ return (0, exports.latin1)(s[n - 1] === 0 ? s.subarray(0, n - 1) : s);
79
+ }
80
+ /** A 32-bit count of UTF-16 code units, then the units, the last of which may be a NUL. */
81
+ lengthPrefixedUnicode(max = 32768) {
82
+ const n = this.u32();
83
+ if (n > max)
84
+ throw new Truncated('A string length past any real string');
85
+ return (0, exports.utf16)(this.bytes(n * 2)).replace(/\0+$/, '');
86
+ }
87
+ }
88
+ exports.ByteReader = ByteReader;
89
+ const latin1 = (b) => Buffer.from(b.buffer, b.byteOffset, b.byteLength).toString('latin1');
90
+ exports.latin1 = latin1;
91
+ const utf16 = (b) => Buffer.from(b.buffer, b.byteOffset, b.byteLength - (b.byteLength % 2)).toString('utf16le');
92
+ exports.utf16 = utf16;
93
+ function parseCompObj(b) {
94
+ const r = new ByteReader(b);
95
+ const out = {};
96
+ return bounded(() => {
97
+ r.skip(28);
98
+ out.userType = r.lengthPrefixedAnsi();
99
+ const marker = r.u32();
100
+ if (marker === 0xffffffff || marker === 0xfffffffe)
101
+ r.skip(4);
102
+ else if (marker !== 0)
103
+ r.skip(marker);
104
+ // MS-OLEDS calls it Reserved1. Windows writes the ProgID there.
105
+ out.progId = r.lengthPrefixedAnsi();
106
+ return out;
107
+ }, () => out);
108
+ }
109
+ /** URL moniker, file moniker and composite moniker class ids (MS-OSHARED 2.3.7). */
110
+ const URL_MONIKER = '79EAC9E0-BAF9-11CE-8C82-00AA004BA90B';
111
+ const FILE_MONIKER = '00000303-0000-0000-C000-000000000046';
112
+ const COMPOSITE_MONIKER = '00000309-0000-0000-C000-000000000046';
113
+ /**
114
+ * The address a moniker names: a URL moniker's URL, a file moniker's path, or the first of a composite's that names
115
+ * one. Other monikers name nothing this reads, and give ''. The reader stops after the moniker it reads.
116
+ */
117
+ function readMoniker(r, depth = 0) {
118
+ const clsid = (0, cfb_1.guid)(r.bytes(16));
119
+ if (clsid === URL_MONIKER) {
120
+ const n = r.u32();
121
+ const raw = r.bytes(Math.min(n, r.left));
122
+ return { target: (0, exports.utf16)(raw).split('\0')[0], url: true };
123
+ }
124
+ if (clsid === FILE_MONIKER) {
125
+ const anti = r.u16();
126
+ const ansi = r.lengthPrefixedAnsi();
127
+ r.skip(2 + 2 + 16);
128
+ const unicodeSize = r.u32();
129
+ let path = ansi;
130
+ if (unicodeSize > 0) {
131
+ const bytes = r.u32();
132
+ r.skip(2);
133
+ path = (0, exports.utf16)(r.bytes(bytes));
134
+ }
135
+ return { target: '..\\'.repeat(Math.min(anti, 64)) + path, url: false };
136
+ }
137
+ if (clsid === COMPOSITE_MONIKER && depth < 8) {
138
+ const count = r.u32();
139
+ for (let i = 0; i < Math.min(count, 64); i++) {
140
+ const found = readMoniker(r, depth + 1);
141
+ if (found.target)
142
+ return found;
143
+ }
144
+ }
145
+ return { target: '', url: false };
146
+ }
147
+ function parseOleStream(b) {
148
+ const r = new ByteReader(b);
149
+ const out = { linked: false, target: '', url: false };
150
+ return bounded(() => {
151
+ r.skip(4);
152
+ out.linked = (r.u32() & 1) === 1;
153
+ if (!out.linked)
154
+ return out;
155
+ r.skip(8);
156
+ r.skip(r.u32());
157
+ // The relative moniker comes first, then the absolute one, which Office prefers when both resolve.
158
+ const monikers = [];
159
+ for (let i = 0; i < 2; i++) {
160
+ const size = r.u32();
161
+ if (size > 0)
162
+ monikers.push(readMoniker(new ByteReader(r.bytes(size))));
163
+ }
164
+ return Object.assign(out, monikers.reverse().find(m => m.target));
165
+ }, () => out);
166
+ }
167
+ /**
168
+ * Packager's native data, as oleobj reads it: a 16-bit type, the label and source path ended by NULs, eight bytes,
169
+ * the temporary path ended by a NUL, and the size of the file's data. Without the size it is a link. Undefined when it
170
+ * does not read that far.
171
+ */
172
+ function parsePackage(b) {
173
+ const r = new ByteReader(b);
174
+ return bounded(() => {
175
+ r.skip(2);
176
+ const fileName = r.ansiz();
177
+ const sourcePath = r.ansiz();
178
+ r.skip(8);
179
+ const tempPath = r.ansiz();
180
+ return bounded(() => ({ fileName, sourcePath, tempPath, embedded: true, size: r.u32() }), () => ({ fileName, sourcePath, tempPath, embedded: false, size: 0 }));
181
+ }, () => undefined);
182
+ }
183
+ function parseOle1(b) {
184
+ const r = new ByteReader(b);
185
+ return bounded(() => {
186
+ r.skip(4);
187
+ const formatId = r.u32();
188
+ if (formatId !== 1 && formatId !== 2)
189
+ return { formatId, className: '', topicName: '', itemName: '' };
190
+ const className = r.lengthPrefixedAnsi();
191
+ const topicName = r.lengthPrefixedAnsi();
192
+ const itemName = r.lengthPrefixedAnsi();
193
+ if (formatId === 1)
194
+ return { formatId, className, topicName, itemName };
195
+ const size = r.u32();
196
+ return { formatId, className, topicName, itemName, data: r.bytes(Math.min(size, r.left)) };
197
+ }, () => undefined);
198
+ }
@@ -0,0 +1,8 @@
1
+ import { type ZipContext } from './context';
2
+ import { type ZipEntry } from './zip';
3
+ /**
4
+ * Inspects an OOXML package. The package relationships come first, since they name the main part and so the format.
5
+ * Then each part is read in archive order, followed by its relationships, which are reported as they are read and
6
+ * leave behind only which parts they reach and how.
7
+ */
8
+ export declare function inspectOoxml(ctx: ZipContext, contentTypes: ZipEntry): Promise<void>;
package/dist/ooxml.js ADDED
@@ -0,0 +1,468 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.inspectOoxml = inspectOoxml;
4
+ const biff_1 = require("./biff");
5
+ const context_1 = require("./context");
6
+ const errors_1 = require("./errors");
7
+ const fields_1 = require("./fields");
8
+ const formats_1 = require("./formats");
9
+ const formulas_1 = require("./formulas");
10
+ const links_1 = require("./links");
11
+ const types_1 = require("./types");
12
+ const xlsb_1 = require("./xlsb");
13
+ const xml_1 = require("./xml");
14
+ const zip_1 = require("./zip");
15
+ const CONTENT_TYPES_NS = 'http://schemas.openxmlformats.org/package/2006/content-types';
16
+ const RELS_NS = 'http://schemas.openxmlformats.org/package/2006/relationships';
17
+ const T = 'http://schemas.openxmlformats.org/officedocument/2006/relationships/';
18
+ const S = 'http://purl.oclc.org/ooxml/officedocument/relationships/';
19
+ const MS = 'http://schemas.microsoft.com/office/2006/relationships/';
20
+ const both = (name) => [T + name, S + name];
21
+ /** Relationship types, lowercased, as OPC compares them without regard to case. */
22
+ const REL = {
23
+ main: new Set([...both('officedocument'), 'http://schemas.microsoft.com/visio/2010/relationships/document']),
24
+ hyperlink: new Set(both('hyperlink')),
25
+ template: new Set(both('attachedtemplate')),
26
+ image: new Set([...both('image'), 'http://schemas.microsoft.com/office/2007/relationships/hdphoto']),
27
+ frame: new Set(both('frame')),
28
+ subDocument: new Set(both('subdocument')),
29
+ workbook: new Set([
30
+ ...both('externallinkpath'),
31
+ `${MS}xlexternallinkpath/xlpathmissing`,
32
+ `${MS}xlexternallinkpath/xlstartup`,
33
+ `${MS}xlexternallinkpath/xlalternatestartup`,
34
+ `${MS}xlexternallinkpath/xllibrary`,
35
+ ]),
36
+ };
37
+ /** Relationship types that say what the part they reach is. Each is a bit of Part.reachedAs. */
38
+ const AS = { vba: 1, macrosheet: 2, ole: 4, package: 8, control: 16, controlBinary: 32 };
39
+ const REACHED_AS = new Map([
40
+ [`${MS}vbaproject`, AS.vba],
41
+ [`${MS}xlmacrosheet`, AS.macrosheet],
42
+ [`${MS}xlintlmacrosheet`, AS.macrosheet],
43
+ ...both('oleobject').map((t) => [t, AS.ole]),
44
+ ...both('package').map((t) => [t, AS.package]),
45
+ ...both('control').map((t) => [t, AS.control]),
46
+ [`${MS}activexcontrolbinary`, AS.controlBinary],
47
+ ]);
48
+ /** Content types, lowercased. */
49
+ const TYPE = {
50
+ vba: new Set(['application/vnd.ms-office.vbaproject']),
51
+ macrosheet: new Set(['application/vnd.ms-excel.macrosheet+xml', 'application/vnd.ms-excel.intlmacrosheet+xml', 'application/vnd.ms-excel.macrosheet', 'application/vnd.ms-excel.intlmacrosheet']),
52
+ ole: new Set(['application/vnd.openxmlformats-officedocument.oleobject']),
53
+ control: new Set(['application/vnd.ms-office.activex+xml']),
54
+ controlBinary: new Set(['application/vnd.ms-office.activex']),
55
+ vml: 'application/vnd.openxmlformats-officedocument.vmldrawing',
56
+ xlsbExternalLink: 'application/vnd.ms-excel.externallink',
57
+ };
58
+ /** Relationship types that leave the package, by the finding they make. Any other is EXTERNAL/OTHER. */
59
+ const EXTERNAL_KINDS = [
60
+ [REL.template, types_1.OfficeDetail.Template],
61
+ [new Set([...both('oleobject'), ...both('package')]), types_1.OfficeDetail.Object],
62
+ [REL.image, types_1.OfficeDetail.Image],
63
+ [REL.frame, types_1.OfficeDetail.Frame],
64
+ [REL.subDocument, types_1.OfficeDetail.Include],
65
+ [REL.workbook, types_1.OfficeDetail.Workbook],
66
+ ];
67
+ const WORD_NS = new Set(['http://schemas.openxmlformats.org/wordprocessingml/2006/main', 'http://purl.oclc.org/ooxml/wordprocessingml/main']);
68
+ const SHEET_NS = new Set(['http://schemas.openxmlformats.org/spreadsheetml/2006/main', 'http://purl.oclc.org/ooxml/spreadsheetml/main']);
69
+ const XDR_NS = new Set(['http://schemas.openxmlformats.org/drawingml/2006/spreadsheetdrawing', 'http://purl.oclc.org/ooxml/drawingml/spreadsheetdrawing']);
70
+ const R_NS = new Set(['http://schemas.openxmlformats.org/officedocument/2006/relationships', 'http://purl.oclc.org/ooxml/officedocument/relationships']);
71
+ const CUSTOM_UI_NS = new Set(['http://schemas.microsoft.com/office/2006/01/customui', 'http://schemas.microsoft.com/office/2009/07/customui']);
72
+ const VML_EXCEL_NS = 'urn:schemas-microsoft-com:office:excel';
73
+ const VISIO_NS = 'http://schemas.microsoft.com/office/visio/2012/main';
74
+ const VISIO_CODE = /\b(RUNADDON|RUNADDONWARGS|CALLTHIS|RUNMACRO|OPENFILE|DOOLEVERB)\s*\(/i;
75
+ /** A Word attribute, qualified as Word writes it or unqualified. */
76
+ const wordAttr = (el, local) => el.attributes.find(a => a.local === local && (a.ns === '' || WORD_NS.has(a.ns.toLowerCase())))?.value;
77
+ /** The key of the part a relationships part belongs to, '' for the package, or undefined for any other part. */
78
+ const relsSource = (relsName) => {
79
+ const m = /^(.*?)_rels\/([^/]*)\.rels$/i.exec(relsName);
80
+ return m ? (0, zip_1.nameKey)(m[1] + m[2]) : undefined;
81
+ };
82
+ /**
83
+ * Resolves an internal target against the folder of the part it belongs to, as RFC 3986 resolves a relative
84
+ * reference: a ".." at the root of the package stays at the root. LibreOffice writes "../customXml/item1.xml" in the
85
+ * package relationships of workbooks it saves.
86
+ */
87
+ function resolveTarget(relsName, target) {
88
+ const folder = relsName.slice(0, relsName.toLowerCase().indexOf('_rels/'));
89
+ const path = (target.startsWith('/') ? target.slice(1) : folder + target).split('#')[0];
90
+ const out = [];
91
+ for (const seg of path.split('/')) {
92
+ if (seg === '..')
93
+ out.pop();
94
+ else if (seg !== '.' && seg !== '')
95
+ out.push(seg);
96
+ }
97
+ return (0, zip_1.nameKey)(out.join('/'));
98
+ }
99
+ const duplicate = (ctx, location, kind, name) => ctx.report(types_1.OfficeCategory.Package, types_1.OfficeDetail.DuplicateEntry, location, { kind, name: (0, context_1.clip)(name) });
100
+ /**
101
+ * The defaults and overrides of [Content_Types].xml. Only those that type an entry of the archive are kept, so a
102
+ * part full of declarations costs no memory. Two that type the same entry make PACKAGE/DUPLICATE_ENTRY.
103
+ */
104
+ async function readContentTypes(ctx, entry) {
105
+ const keys = new Set(ctx.zip.entries.map(e => (0, zip_1.nameKey)(e.name)));
106
+ const extensions = new Set(ctx.zip.entries.map(e => /\.([^./]*)$/.exec(e.name)?.[1]?.toLowerCase() ?? ''));
107
+ const defaults = new Map();
108
+ const overrides = new Map();
109
+ await ctx.parse(entry, {
110
+ start(el) {
111
+ if (el.ns.toLowerCase() !== CONTENT_TYPES_NS)
112
+ return;
113
+ const type = (0, xml_1.attr)(el, '', 'ContentType')?.trim().toLowerCase();
114
+ if (type === undefined)
115
+ return;
116
+ if (el.local === 'Default') {
117
+ const ext = (0, xml_1.attr)(el, '', 'Extension')?.toLowerCase();
118
+ if (ext === undefined || !extensions.has(ext))
119
+ return;
120
+ if (defaults.has(ext))
121
+ duplicate(ctx, entry.name, 'content type default', ext);
122
+ defaults.set(ext, type);
123
+ }
124
+ else if (el.local === 'Override') {
125
+ const name = (0, xml_1.attr)(el, '', 'PartName');
126
+ if (name === undefined)
127
+ return;
128
+ const key = (0, zip_1.nameKey)(name.replace(/^\//, ''));
129
+ if (!keys.has(key))
130
+ return;
131
+ if (overrides.has(key))
132
+ duplicate(ctx, entry.name, 'content type override', name);
133
+ overrides.set(key, type);
134
+ }
135
+ },
136
+ });
137
+ return { defaults, overrides };
138
+ }
139
+ /** Reads a relationships part and hands each relationship to `each` as it is read. Ids given twice are reported. */
140
+ async function readRelationships(ctx, entry, each) {
141
+ const ids = new Set();
142
+ await ctx.parse(entry, {
143
+ start(el) {
144
+ if (el.ns.toLowerCase() !== RELS_NS || el.local !== 'Relationship')
145
+ return;
146
+ const id = (0, xml_1.attr)(el, '', 'Id') ?? '';
147
+ if (ids.has(id))
148
+ duplicate(ctx, entry.name, 'relationship Id', id);
149
+ ids.add(id);
150
+ const target = (0, xml_1.attr)(el, '', 'Target');
151
+ if (target === undefined)
152
+ return;
153
+ const mode = ((0, xml_1.attr)(el, '', 'TargetMode') ?? 'Internal').trim().toLowerCase();
154
+ each({ id, type: ((0, xml_1.attr)(el, '', 'Type') ?? '').trim().toLowerCase(), target, external: mode !== 'internal' || (0, links_1.isAbsoluteTarget)(target) });
155
+ },
156
+ });
157
+ }
158
+ /** Word field codes in WordprocessingML: fldSimple, and fldChar marks around instrText. */
159
+ class Fields {
160
+ stack;
161
+ capture = false;
162
+ constructor(ctx, part) {
163
+ this.stack = new fields_1.FieldStack(ctx, part);
164
+ }
165
+ start(el) {
166
+ if (el.local === 'fldSimple')
167
+ this.stack.simple(wordAttr(el, 'instr') ?? '');
168
+ else if (el.local === 'fldChar') {
169
+ const kind = wordAttr(el, 'fldCharType');
170
+ if (kind === 'begin')
171
+ this.stack.begin();
172
+ else if (kind === 'separate')
173
+ this.stack.separate();
174
+ else if (kind === 'end')
175
+ this.stack.end();
176
+ }
177
+ else if (el.local === 'instrText' || el.local === 'delInstrText')
178
+ this.capture = true;
179
+ }
180
+ end(el) {
181
+ if (el.local === 'instrText' || el.local === 'delInstrText')
182
+ this.capture = false;
183
+ }
184
+ text(t) {
185
+ if (this.capture)
186
+ this.stack.text(t);
187
+ }
188
+ finish() {
189
+ this.stack.finish();
190
+ }
191
+ }
192
+ /** Text that may run long. Detection needs only whether it holds more than white space; the report shows its start. */
193
+ class Shown {
194
+ text = '';
195
+ content = false;
196
+ add(t) {
197
+ if (/\S/.test(t))
198
+ this.content = true;
199
+ if (this.text.length < 200)
200
+ this.text = this.text ? this.text + t : t.trimStart();
201
+ }
202
+ }
203
+ /** The rules that read inside one XML part of an OOXML package. */
204
+ class PartRules {
205
+ ctx;
206
+ part;
207
+ fields;
208
+ /**
209
+ * The formula being read, with a check for DDE that sees all of it however long it is, and the first external
210
+ * entity whose content stands in it, which could hold the DDE reference or move the quotes around one.
211
+ */
212
+ formula;
213
+ macroText;
214
+ /** The last namespace seen and its lowercase form. Neighboring elements share one, and this spares a copy each. */
215
+ ns = '';
216
+ lower = '';
217
+ constructor(ctx, part) {
218
+ this.ctx = ctx;
219
+ this.part = part;
220
+ this.fields = new Fields(ctx, part);
221
+ }
222
+ lowerNs(el) {
223
+ if (el.ns !== this.ns) {
224
+ this.ns = el.ns;
225
+ this.lower = el.ns.toLowerCase();
226
+ }
227
+ return this.lower;
228
+ }
229
+ start(el) {
230
+ const ns = this.lowerNs(el);
231
+ if (CUSTOM_UI_NS.has(ns)) {
232
+ if (el.depth === 1)
233
+ this.ctx.report(types_1.OfficeCategory.Ui, types_1.OfficeDetail.Ribbon, this.part);
234
+ for (const a of el.attributes) {
235
+ if (a.ns === '' && a.value.trim() && /^(on[A-Z]|get[A-Z]|loadImage$)/.test(a.local))
236
+ this.ctx.report(types_1.OfficeCategory.Ui, types_1.OfficeDetail.Callback, this.part, { callback: `${a.local}=${(0, context_1.clip)(a.value)}` });
237
+ }
238
+ return;
239
+ }
240
+ if (WORD_NS.has(ns))
241
+ this.fields.start(el);
242
+ else if (SHEET_NS.has(ns)) {
243
+ if (el.local === 'f' || el.local === 'definedName')
244
+ this.formula = { shown: new Shown(), quotes: new formulas_1.Quotes(), ddeBar: false };
245
+ if (el.local === 'definedName') {
246
+ // Excel runs the macro a name like _xlnm.Auto_Open refers to, as it does in a binary workbook.
247
+ const name = (0, xml_1.attr)(el, '', 'name') ?? '';
248
+ const auto = (0, biff_1.autoName)(name);
249
+ if (auto)
250
+ this.ctx.report(types_1.OfficeCategory.Macro, auto.onOpen ? types_1.OfficeDetail.EventOnOpen : types_1.OfficeDetail.Event, this.part, { event: auto.event, name: (0, context_1.clip)(name) });
251
+ }
252
+ else if (el.local === 'ddeLink')
253
+ this.ctx.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Link, this.part, { application: (0, context_1.clip)((0, xml_1.attr)(el, '', 'ddeService') ?? ''), topic: (0, context_1.clip)((0, xml_1.attr)(el, '', 'ddeTopic') ?? '') });
254
+ }
255
+ else if (XDR_NS.has(ns)) {
256
+ const macro = (0, xml_1.attr)(el, '', 'macro');
257
+ if (macro?.trim())
258
+ this.ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Link, this.part, { macro: (0, context_1.clip)(macro) });
259
+ }
260
+ else if (ns === VML_EXCEL_NS && el.local === 'FmlaMacro')
261
+ this.macroText = new Shown();
262
+ else if (ns === VISIO_NS && el.local === 'Cell')
263
+ this.visioCell(el);
264
+ if (el.local === 'hlinkClick' || el.local === 'hlinkHover' || el.local === 'hlinkMouseOver')
265
+ this.drawingAction(el);
266
+ }
267
+ text(t) {
268
+ this.fields.text(t);
269
+ const f = this.formula;
270
+ if (f) {
271
+ f.shown.add(t);
272
+ // Excel writes a DDE reference as application|topic!item, and "|" means nothing else outside quotes.
273
+ for (const c of t)
274
+ if (f.quotes.outside(c) && c === '|')
275
+ f.ddeBar = true;
276
+ if (f.entity === undefined && t.includes(xml_1.UNKNOWN))
277
+ f.entity = (0, xml_1.unknownEntity)(t);
278
+ }
279
+ this.macroText?.add(t);
280
+ }
281
+ end(el) {
282
+ const ns = this.lowerNs(el);
283
+ if (WORD_NS.has(ns))
284
+ this.fields.end(el);
285
+ if (SHEET_NS.has(ns) && (el.local === 'f' || el.local === 'definedName') && this.formula) {
286
+ const { ddeBar, entity, shown } = this.formula;
287
+ if (ddeBar)
288
+ this.ctx.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Formula, this.part, { formula: (0, context_1.clip)(shown.text) });
289
+ else if (entity !== undefined)
290
+ this.ctx.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Formula, this.part, { formula: (0, context_1.clip)(shown.text), entity: (0, context_1.clip)(entity) });
291
+ this.formula = undefined;
292
+ }
293
+ if (ns === VML_EXCEL_NS && el.local === 'FmlaMacro' && this.macroText) {
294
+ if (this.macroText.content)
295
+ this.ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Link, this.part, { macro: (0, context_1.clip)(this.macroText.text.trim()) });
296
+ this.macroText = undefined;
297
+ }
298
+ if (el.depth === 1)
299
+ this.fields.finish();
300
+ }
301
+ /** PowerPoint action settings, in a DrawingML hyperlink's action attribute. The relationship it names is reported on its own. */
302
+ drawingAction(el) {
303
+ const written = ((0, xml_1.attr)(el, '', 'action') ?? '').trim();
304
+ const action = written.toLowerCase();
305
+ if (!action)
306
+ return;
307
+ const id = el.attributes.find(a => a.local === 'id' && R_NS.has(a.ns.toLowerCase()))?.value;
308
+ const data = { trigger: el.local, action: (0, context_1.clip)(written), ...(id === undefined ? {} : { relationship: (0, context_1.clip)(id) }) };
309
+ if (action.startsWith('ppaction://program'))
310
+ this.ctx.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.RunProgram, this.part, data);
311
+ else if (action.startsWith('ppaction://macro'))
312
+ this.ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Link, this.part, data);
313
+ else if (action.startsWith('ppaction://ole'))
314
+ this.ctx.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.OleVerb, this.part, data);
315
+ }
316
+ visioCell(el) {
317
+ const n = (0, xml_1.attr)(el, '', 'N') ?? '';
318
+ const formula = (0, xml_1.attr)(el, '', 'F') ?? '';
319
+ if (VISIO_CODE.test(formula)) {
320
+ if (n.startsWith('Event'))
321
+ this.ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Event, this.part, { event: (0, context_1.clip)(n), formula: (0, context_1.clip)(formula) });
322
+ else
323
+ this.ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Link, this.part, { cell: (0, context_1.clip)(n), formula: (0, context_1.clip)(formula) });
324
+ }
325
+ const v = (0, xml_1.attr)(el, '', 'V');
326
+ if (n === 'Address' && v?.trim() && !(0, links_1.isFragment)(v))
327
+ this.ctx.link(v, this.part, { source: 'Visio hyperlink' });
328
+ }
329
+ }
330
+ /**
331
+ * Inspects an OOXML package. The package relationships come first, since they name the main part and so the format.
332
+ * Then each part is read in archive order, followed by its relationships, which are reported as they are read and
333
+ * leave behind only which parts they reach and how.
334
+ */
335
+ async function inspectOoxml(ctx, contentTypes) {
336
+ const { defaults, overrides } = await readContentTypes(ctx, contentTypes);
337
+ const parts = new Map();
338
+ const relsOf = new Map();
339
+ for (const entry of ctx.zip.entries) {
340
+ if (entry === contentTypes)
341
+ continue;
342
+ // OPC has no folders. A directory entry is still inflated once, so its bytes are checked like any other.
343
+ if (entry.directory) {
344
+ await ctx.drain(entry);
345
+ continue;
346
+ }
347
+ const source = relsSource(entry.name);
348
+ if (source !== undefined) {
349
+ relsOf.set(source, entry);
350
+ continue;
351
+ }
352
+ const key = (0, zip_1.nameKey)(entry.name);
353
+ const ext = /\.([^./]*)$/.exec(entry.name)?.[1]?.toLowerCase();
354
+ parts.set(key, { entry, contentType: overrides.get(key) ?? (ext === undefined ? undefined : defaults.get(ext)), reachedAs: 0, claimed: false, targets: new Set(), ole: false });
355
+ }
356
+ const roots = new Set();
357
+ const mains = [];
358
+ const relate = (rels, from, r) => {
359
+ const location = `${rels.name}, ${(0, context_1.clip)(r.id)}`;
360
+ const part = r.external ? undefined : parts.get(resolveTarget(rels.name, r.target));
361
+ if (part) {
362
+ part.reachedAs |= REACHED_AS.get(r.type) ?? 0;
363
+ if (TYPE.control.has(from?.contentType ?? ''))
364
+ part.claimed = true;
365
+ (from?.targets ?? roots).add(part);
366
+ }
367
+ // A hyperlink to a part of the package, or to a place in the document, goes nowhere else.
368
+ if (REL.hyperlink.has(r.type)) {
369
+ if (!part && !(0, links_1.isFragment)(r.target))
370
+ ctx.link(r.target, location);
371
+ }
372
+ else if (r.external) {
373
+ const kind = EXTERNAL_KINDS.find(([types]) => types.has(r.type))?.[1] ?? types_1.OfficeDetail.Other;
374
+ ctx.external(kind, r.target, location, { type: (0, context_1.clip)(r.type) }, (0, context_1.clip)(r.type.slice(r.type.lastIndexOf('/') + 1)));
375
+ }
376
+ };
377
+ const packageRels = relsOf.get('');
378
+ if (packageRels) {
379
+ await readRelationships(ctx, packageRels, r => {
380
+ if (REL.main.has(r.type))
381
+ mains.push(r.external ? undefined : parts.get(resolveTarget(packageRels.name, r.target)));
382
+ relate(packageRels, undefined, r);
383
+ });
384
+ }
385
+ if (mains.length > 1)
386
+ throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.AmbiguousFormat, 'The package names more than one main part', { reason: 'more than one main part' });
387
+ const main = mains[0];
388
+ if (!main)
389
+ throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The package has no main part', { reason: 'no main document part' });
390
+ const format = formats_1.OOXML_FORMATS.get(main.contentType ?? '');
391
+ if (!format)
392
+ throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The main part has an unknown content type', { contentType: (0, context_1.clip)(main.contentType ?? '') });
393
+ ctx.format = format;
394
+ for (const [key, p] of parts) {
395
+ const ct = p.contentType;
396
+ if (ct === undefined) {
397
+ ctx.report(types_1.OfficeCategory.Package, types_1.OfficeDetail.UntypedPart, p.entry.name, { part: p.entry.name });
398
+ p.ole = await ctx.drain(p.entry);
399
+ }
400
+ else if (ct.endsWith('+xml') || ct === 'application/xml' || ct === 'text/xml' || ct === TYPE.vml) {
401
+ await ctx.parse(p.entry, new PartRules(ctx, p.entry.name), ct === TYPE.vml);
402
+ }
403
+ else if (ct === TYPE.xlsbExternalLink) {
404
+ // The binary form of an Excel ddeLink.
405
+ const links = new xlsb_1.XlsbLinks();
406
+ p.ole = await ctx.drain(p.entry, c => links.write(c));
407
+ for (const l of links.dde)
408
+ ctx.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Link, p.entry.name, { application: (0, context_1.clip)(l.application), topic: (0, context_1.clip)(l.topic) });
409
+ }
410
+ else
411
+ p.ole = await ctx.drain(p.entry);
412
+ const rels = relsOf.get(key);
413
+ if (rels)
414
+ await readRelationships(ctx, rels, r => relate(rels, p, r));
415
+ }
416
+ // Relationships of parts that are not there still say what the parts they reach are.
417
+ for (const [source, rels] of relsOf)
418
+ if (source !== '' && !parts.has(source))
419
+ await readRelationships(ctx, rels, r => relate(rels, undefined, r));
420
+ for (const p of parts.values())
421
+ partRules(ctx, p);
422
+ unreferenced(ctx, parts, relsOf, roots);
423
+ }
424
+ /** Findings about a whole part, from its content type and the relationships that reach it. */
425
+ function partRules(ctx, p) {
426
+ const ct = p.contentType ?? '';
427
+ const loc = p.entry.name;
428
+ const data = { part: loc, contentType: (0, context_1.clip)(ct) };
429
+ const as = (bit) => (p.reachedAs & bit) !== 0;
430
+ const vba = TYPE.vba.has(ct) || as(AS.vba);
431
+ const ole = TYPE.ole.has(ct) || as(AS.ole);
432
+ const control = TYPE.control.has(ct) || as(AS.control);
433
+ const controlBinary = TYPE.controlBinary.has(ct) || as(AS.controlBinary);
434
+ const pkg = as(AS.package);
435
+ if (vba)
436
+ ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.VbaProject, loc, data);
437
+ if (TYPE.macrosheet.has(ct) || as(AS.macrosheet))
438
+ ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.XlmMacrosheet, loc, data);
439
+ if (ole)
440
+ ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleObject, loc, data);
441
+ if (pkg)
442
+ ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Package, loc, data);
443
+ if (control || (controlBinary && !p.claimed))
444
+ ctx.report(types_1.OfficeCategory.Control, types_1.OfficeDetail.ActiveX, loc, data);
445
+ if (p.ole && !vba && !ole && !control && !controlBinary && !pkg)
446
+ ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleFile, loc, data);
447
+ }
448
+ /** Parts that no chain of internal relationships reaches from the package. */
449
+ function unreferenced(ctx, parts, relsOf, roots) {
450
+ const reached = new Set(roots);
451
+ const queue = [...roots];
452
+ for (let p = queue.pop(); p; p = queue.pop()) {
453
+ for (const t of p.targets) {
454
+ if (reached.has(t))
455
+ continue;
456
+ reached.add(t);
457
+ queue.push(t);
458
+ }
459
+ }
460
+ for (const p of parts.values())
461
+ if (!reached.has(p))
462
+ ctx.report(types_1.OfficeCategory.Package, types_1.OfficeDetail.UnreferencedPart, p.entry.name, { part: p.entry.name });
463
+ for (const [source, rels] of relsOf) {
464
+ const owner = parts.get(source);
465
+ if (source !== '' && !(owner && reached.has(owner)))
466
+ ctx.report(types_1.OfficeCategory.Package, types_1.OfficeDetail.UnreferencedPart, rels.name, { part: rels.name });
467
+ }
468
+ }
@@ -0,0 +1,3 @@
1
+ import type { ZipContext } from './context';
2
+ /** Inspects an OOXML or OpenDocument package, which its markers name. */
3
+ export declare function inspectPackage(ctx: ZipContext): Promise<void>;