@patrickdk77/office-defuse 0.1.0 → 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.
package/dist/ole.js ADDED
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.families = families;
4
+ exports.inspectCompound = inspectCompound;
5
+ const errors_1 = require("./errors");
6
+ const msg_1 = require("./msg");
7
+ const objects_1 = require("./objects");
8
+ const offcrypto_1 = require("./offcrypto");
9
+ const package_1 = require("./package");
10
+ const ppt_1 = require("./ppt");
11
+ const types_1 = require("./types");
12
+ const word_1 = require("./word");
13
+ const xls_1 = require("./xls");
14
+ const zip_1 = require("./zip");
15
+ const f = (format, mimeType) => ({ format, mimeType });
16
+ const upper = (e) => e.name.toUpperCase();
17
+ const PUBLISHER_CLSIDS = new Set(['00021200-0000-0000-00C0-000000000046', '00021201-0000-0000-00C0-000000000046']);
18
+ /** What the root of a compound file holds, read by the names of its children, ignoring case as MS-CFB does. */
19
+ function families(cfb) {
20
+ const names = new Map(cfb.root.children.map(c => [upper(c), c]));
21
+ const stream = (n) => names.get(n)?.kind === 'stream';
22
+ const out = [];
23
+ if (stream('ENCRYPTIONINFO') && stream('ENCRYPTEDPACKAGE'))
24
+ out.push('encrypted');
25
+ if (stream('WORDDOCUMENT'))
26
+ out.push('word');
27
+ if (stream('WORKBOOK') || stream('BOOK'))
28
+ out.push('excel');
29
+ if (stream('POWERPOINT DOCUMENT'))
30
+ out.push('powerpoint');
31
+ if (stream('VISIODOCUMENT'))
32
+ out.push('visio');
33
+ if (stream('__PROPERTIES_VERSION1.0'))
34
+ out.push('outlook');
35
+ // Publisher 98 and later keep their content in a Quill storage. Publisher 95 has none, and names itself by class id.
36
+ if (names.get('QUILL')?.kind === 'storage' || (PUBLISHER_CLSIDS.has(cfb.root.clsid) && stream('CONTENTS')))
37
+ out.push('publisher');
38
+ // Project keeps its data in a storage named by three spaces and a number, beside a Props stream.
39
+ if ([...names.keys()].some(n => /^PROPS\d*$/.test(n)) && cfb.root.children.some(c => c.kind === 'storage' && /^ {3}\d+$/.test(c.name)))
40
+ out.push('project');
41
+ return out;
42
+ }
43
+ /**
44
+ * Inspects a compound file: the rules every compound file gets, over its whole tree, and then those of the format
45
+ * its root names.
46
+ */
47
+ async function inspectCompound(ctx, cfb) {
48
+ const found = families(cfb);
49
+ // Word 2007 writes a Word document beside an encrypted package, for readers that cannot decrypt it.
50
+ const main = found.filter(x => !(x === 'word' && found.includes('encrypted')));
51
+ if (main.length > 1)
52
+ throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.AmbiguousFormat, `The compound file holds ${main.join(' and ')} streams`, { reason: `${main.join(' and ')} streams` });
53
+ if (main.length === 0)
54
+ throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The compound file holds no Office document stream', { reason: 'a compound file of another kind' });
55
+ const family = main[0];
56
+ ctx.format = formatOf(family, cfb);
57
+ await (0, objects_1.compoundRules)(ctx, cfb, '', false);
58
+ const stream = (name) => {
59
+ const e = cfb.child(cfb.root, name);
60
+ return e?.kind === 'stream' ? e : undefined;
61
+ };
62
+ // Beside an encrypted package, Word 2007 writes a WordDocument stream for older readers, of zeros in the files at
63
+ // hand. Those readers read it if it holds a document, so it gets the Word rules when it does.
64
+ const word = stream('WordDocument');
65
+ if (word) {
66
+ const r = await (0, word_1.wordRules)(ctx, cfb, word, family === 'encrypted');
67
+ if (r?.template && family === 'word')
68
+ ctx.format = f('dot', 'application/msword');
69
+ }
70
+ if (family === 'excel') {
71
+ // Excel 97 to 2003 reads Workbook, and Excel 5.0 and 95 read Book. A file can hold both.
72
+ let template = false;
73
+ let addIn = false;
74
+ for (const name of ['Workbook', 'Book']) {
75
+ const s = stream(name);
76
+ if (!s)
77
+ continue;
78
+ const r = await (0, xls_1.excelRules)(ctx, cfb, s, offcrypto_1.unlockWorkbook);
79
+ template ||= r.template;
80
+ addIn ||= r.addIn;
81
+ }
82
+ if (addIn)
83
+ ctx.format = f('xla', 'application/vnd.ms-excel');
84
+ else if (template)
85
+ ctx.format = f('xlt', 'application/vnd.ms-excel');
86
+ // Excel keeps the data of a workbook's ActiveX controls in one Ctls stream.
87
+ const controls = stream('Ctls');
88
+ if (controls && controls.size > 0)
89
+ ctx.report(types_1.OfficeCategory.Control, types_1.OfficeDetail.ActiveX, controls.name, { stream: controls.name });
90
+ }
91
+ if (family === 'outlook')
92
+ await (0, msg_1.messageRules)(ctx, cfb, cfb.root);
93
+ const ppt = stream('PowerPoint Document');
94
+ if (family === 'powerpoint' && ppt)
95
+ await (0, ppt_1.powerPointRules)(ctx, cfb, ppt);
96
+ const info = stream('EncryptionInfo');
97
+ const pkg = stream('EncryptedPackage');
98
+ if (family === 'encrypted' && info && pkg) {
99
+ // The package goes through the OOXML rules once a password opens it, and names the format.
100
+ const { source } = await (0, offcrypto_1.openPackage)(ctx, cfb, info, pkg);
101
+ const zip = await zip_1.ZipArchive.open(source, ctx.settings, ctx.settings.deadline);
102
+ for (const n of zip.notes)
103
+ ctx.report(types_1.OfficeCategory.Container, n.detail, pkg.name, { bytes: n.bytes });
104
+ await (0, package_1.inspectPackage)(ctx.withZip(zip));
105
+ }
106
+ }
107
+ function formatOf(family, cfb) {
108
+ switch (family) {
109
+ case 'word':
110
+ return f('doc', 'application/msword');
111
+ case 'excel':
112
+ return f('xls', 'application/vnd.ms-excel');
113
+ case 'powerpoint':
114
+ return f('ppt', 'application/vnd.ms-powerpoint');
115
+ case 'visio':
116
+ return f('vsd', 'application/vnd.visio');
117
+ case 'outlook':
118
+ return cfb.root.clsid === '0006F046-0000-0000-C000-000000000046' ? f('oft', 'application/vnd.ms-outlook') : f('msg', 'application/vnd.ms-outlook');
119
+ case 'publisher':
120
+ return f('pub', 'application/x-mspublisher');
121
+ case 'project':
122
+ return f('mpp', 'application/vnd.ms-project');
123
+ default:
124
+ // An encrypted package names its format only once it is decrypted.
125
+ return undefined;
126
+ }
127
+ }
@@ -0,0 +1,74 @@
1
+ /** Thrown by ByteReader when a structure runs past the end of its bytes. Parsers catch it and keep what they read. */
2
+ export declare class Truncated extends Error {
3
+ }
4
+ /** Runs `read`. A structure that runs past its data gives what `partial` returns instead, and any other error goes up. */
5
+ export declare function bounded<T>(read: () => T, partial: () => T): T;
6
+ /** Little-endian reads over bytes in memory, bounded by their end. */
7
+ export declare class ByteReader {
8
+ readonly b: Uint8Array;
9
+ pos: number;
10
+ constructor(b: Uint8Array);
11
+ get left(): number;
12
+ private need;
13
+ u8(): number;
14
+ u16(): number;
15
+ u32(): number;
16
+ bytes(n: number): Uint8Array;
17
+ skip(n: number): void;
18
+ /** An ANSI string ended by a NUL within `max` bytes, read as Latin-1. */
19
+ ansiz(max?: number): string;
20
+ /** A 32-bit byte count, then that many bytes of ANSI text whose last is a NUL. */
21
+ lengthPrefixedAnsi(max?: number): string;
22
+ /** A 32-bit count of UTF-16 code units, then the units, the last of which may be a NUL. */
23
+ lengthPrefixedUnicode(max?: number): string;
24
+ }
25
+ export declare const latin1: (b: Uint8Array) => string;
26
+ export declare const utf16: (b: Uint8Array) => string;
27
+ /** The class an OLE object names in its CompObj stream (MS-OLEDS 2.3.8): the user type and the ProgID. */
28
+ export interface CompObj {
29
+ userType?: string;
30
+ progId?: string;
31
+ }
32
+ export declare function parseCompObj(b: Uint8Array): CompObj;
33
+ /** What a moniker names: a URL moniker's URL, or a file moniker's path. */
34
+ export interface MonikerTarget {
35
+ target: string;
36
+ /** A URL moniker, whose target loads from wherever it names, however it is spelled. */
37
+ url: boolean;
38
+ }
39
+ /**
40
+ * The address a moniker names: a URL moniker's URL, a file moniker's path, or the first of a composite's that names
41
+ * one. Other monikers name nothing this reads, and give ''. The reader stops after the moniker it reads.
42
+ */
43
+ export declare function readMoniker(r: ByteReader, depth?: number): MonikerTarget;
44
+ /** What an OLE stream (MS-OLEDS 2.3.3) says: whether the object is a link, and what it links to. */
45
+ export interface OleStreamInfo extends MonikerTarget {
46
+ linked: boolean;
47
+ }
48
+ export declare function parseOleStream(b: Uint8Array): OleStreamInfo;
49
+ /** An embedded file in Packager's native data, which oleobj reads the same way. */
50
+ export interface PackagedFile {
51
+ fileName: string;
52
+ sourcePath: string;
53
+ tempPath: string;
54
+ /** False when the data names a file it does not hold, a link. */
55
+ embedded: boolean;
56
+ size: number;
57
+ }
58
+ /**
59
+ * Packager's native data, as oleobj reads it: a 16-bit type, the label and source path ended by NULs, eight bytes,
60
+ * 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
61
+ * does not read that far.
62
+ */
63
+ export declare function parsePackage(b: Uint8Array): PackagedFile | undefined;
64
+ /** An OLE 1.0 object (MS-OLEDS 2.2), as RTF's objdata holds one. */
65
+ export interface Ole1Object {
66
+ /** 1 linked, 2 embedded, 5 a static picture. */
67
+ formatId: number;
68
+ className: string;
69
+ topicName: string;
70
+ itemName: string;
71
+ /** The native data of an embedded object. */
72
+ data?: Uint8Array;
73
+ }
74
+ export declare function parseOle1(b: Uint8Array): Ole1Object | undefined;
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
+ }
package/dist/ooxml.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { type RunContext } from './context';
1
+ import { type ZipContext } from './context';
2
2
  import { type ZipEntry } from './zip';
3
3
  /**
4
4
  * Inspects an OOXML package. The package relationships come first, since they name the main part and so the format.
5
5
  * Then each part is read in archive order, followed by its relationships, which are reported as they are read and
6
6
  * leave behind only which parts they reach and how.
7
7
  */
8
- export declare function inspectOoxml(ctx: RunContext, contentTypes: ZipEntry): Promise<void>;
8
+ export declare function inspectOoxml(ctx: ZipContext, contentTypes: ZipEntry): Promise<void>;
package/dist/ooxml.js CHANGED
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.inspectOoxml = inspectOoxml;
4
+ const biff_1 = require("./biff");
4
5
  const context_1 = require("./context");
5
6
  const errors_1 = require("./errors");
7
+ const fields_1 = require("./fields");
6
8
  const formats_1 = require("./formats");
7
9
  const formulas_1 = require("./formulas");
8
10
  const links_1 = require("./links");
@@ -70,8 +72,6 @@ const CUSTOM_UI_NS = new Set(['http://schemas.microsoft.com/office/2006/01/custo
70
72
  const VML_EXCEL_NS = 'urn:schemas-microsoft-com:office:excel';
71
73
  const VISIO_NS = 'http://schemas.microsoft.com/office/visio/2012/main';
72
74
  const VISIO_CODE = /\b(RUNADDON|RUNADDONWARGS|CALLTHIS|RUNMACRO|OPENFILE|DOOLEVERB)\s*\(/i;
73
- /** How much of a field code is kept, counted from its first word, which names the field. */
74
- const MAX_FIELD_CODE = 4096;
75
75
  /** A Word attribute, qualified as Word writes it or unqualified. */
76
76
  const wordAttr = (el, local) => el.attributes.find(a => a.local === local && (a.ns === '' || WORD_NS.has(a.ns.toLowerCase())))?.value;
77
77
  /** The key of the part a relationships part belongs to, '' for the package, or undefined for any other part. */
@@ -155,43 +155,24 @@ async function readRelationships(ctx, entry, each) {
155
155
  },
156
156
  });
157
157
  }
158
- /** Word field codes, followed through nested fields, as one part's fields run. */
158
+ /** Word field codes in WordprocessingML: fldSimple, and fldChar marks around instrText. */
159
159
  class Fields {
160
- ctx;
161
- part;
162
- stack = [];
160
+ stack;
163
161
  capture = false;
164
162
  constructor(ctx, part) {
165
- this.ctx = ctx;
166
- this.part = part;
167
- }
168
- /** A field starts. One that starts before its parent's code has a word in it makes that code at run time. */
169
- nest() {
170
- const top = this.stack[this.stack.length - 1];
171
- if (top?.inCode && top.code === '')
172
- top.computed = true;
163
+ this.stack = new fields_1.FieldStack(ctx, part);
173
164
  }
174
165
  start(el) {
175
- if (el.local === 'fldSimple') {
176
- this.nest();
177
- this.evaluate(wordAttr(el, 'instr') ?? '', false);
178
- }
166
+ if (el.local === 'fldSimple')
167
+ this.stack.simple(wordAttr(el, 'instr') ?? '');
179
168
  else if (el.local === 'fldChar') {
180
169
  const kind = wordAttr(el, 'fldCharType');
181
- if (kind === 'begin') {
182
- this.nest();
183
- this.stack.push({ code: '', inCode: true, computed: false });
184
- }
185
- else if (kind === 'separate') {
186
- const top = this.stack[this.stack.length - 1];
187
- if (top)
188
- top.inCode = false;
189
- }
190
- else if (kind === 'end') {
191
- const f = this.stack.pop();
192
- if (f)
193
- this.evaluate(f.code, f.computed);
194
- }
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();
195
176
  }
196
177
  else if (el.local === 'instrText' || el.local === 'delInstrText')
197
178
  this.capture = true;
@@ -200,48 +181,12 @@ class Fields {
200
181
  if (el.local === 'instrText' || el.local === 'delInstrText')
201
182
  this.capture = false;
202
183
  }
203
- /** White space before the first word counts for nothing, so no amount of it can push the field's name out. */
204
184
  text(t) {
205
- const top = this.stack[this.stack.length - 1];
206
- if (this.capture && top?.inCode && top.code.length < MAX_FIELD_CODE)
207
- top.code = top.code ? top.code + t : t.trimStart();
185
+ if (this.capture)
186
+ this.stack.text(t);
208
187
  }
209
- /** A part can end inside a field. Word still reads the code it has. */
210
188
  finish() {
211
- for (let f = this.stack.pop(); f; f = this.stack.pop())
212
- this.evaluate(f.code, f.computed);
213
- }
214
- /**
215
- * Reports a field by its code. A field whose name holds the content of an external entity has a type the file does
216
- * not say, as one whose name another field builds does.
217
- */
218
- evaluate(code, computed) {
219
- const data = { code: (0, context_1.clip)(code.trim()) };
220
- const words = (0, formulas_1.fieldWords)(code);
221
- const first = words[0]?.text ?? '';
222
- const entity = (0, xml_1.unknownEntity)(first);
223
- if (computed || entity !== undefined)
224
- this.ctx.report(types_1.OfficeCategory.Field, types_1.OfficeDetail.Computed, this.part, entity === undefined ? data : { ...data, entity: (0, context_1.clip)(entity) });
225
- const name = first.toUpperCase();
226
- // An argument comes before any switch, and a switch is an unquoted word starting with a backslash.
227
- const switchAt = words.findIndex(w => !w.quoted && w.text.startsWith('\\'));
228
- const arg = (n) => (words[n] !== undefined && (switchAt < 0 || n < switchAt) ? words[n].text : undefined);
229
- if (name === 'DDE' || name === 'DDEAUTO')
230
- this.ctx.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Field, this.part, data);
231
- else if (name === 'MACROBUTTON')
232
- this.ctx.report(types_1.OfficeCategory.Field, types_1.OfficeDetail.Macrobutton, this.part, { ...data, macro: (0, context_1.clip)(words[1]?.text ?? '') });
233
- else if (name === 'INCLUDETEXT' || name === 'INCLUDEPICTURE' || name === 'IMPORT' || name === 'LINK') {
234
- const target = arg(name === 'LINK' ? 2 : 1);
235
- if (target === undefined)
236
- this.ctx.report(types_1.OfficeCategory.External, types_1.OfficeDetail.Include, this.part, data);
237
- else
238
- this.ctx.external(types_1.OfficeDetail.Include, target, this.part, data, 'field');
239
- }
240
- else if (name === 'HYPERLINK') {
241
- const target = arg(1);
242
- if (target !== undefined && !(0, links_1.isFragment)(target))
243
- this.ctx.link(target, this.part, { ...data, source: 'HYPERLINK field' });
244
- }
189
+ this.stack.finish();
245
190
  }
246
191
  }
247
192
  /** Text that may run long. Detection needs only whether it holds more than white space; the report shows its start. */
@@ -297,6 +242,13 @@ class PartRules {
297
242
  else if (SHEET_NS.has(ns)) {
298
243
  if (el.local === 'f' || el.local === 'definedName')
299
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
+ }
300
252
  else if (el.local === 'ddeLink')
301
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') ?? '') });
302
254
  }
@@ -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>;
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.inspectPackage = inspectPackage;
4
+ const errors_1 = require("./errors");
5
+ const odf_1 = require("./odf");
6
+ const ooxml_1 = require("./ooxml");
7
+ const types_1 = require("./types");
8
+ /** Inspects an OOXML or OpenDocument package, which its markers name. */
9
+ async function inspectPackage(ctx) {
10
+ const zip = ctx.zip;
11
+ const contentTypes = zip.get('[Content_Types].xml');
12
+ const odf = zip.entries.some(e => e.name === 'mimetype') || zip.get('META-INF/manifest.xml') !== undefined;
13
+ if (contentTypes && odf)
14
+ throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.AmbiguousFormat, 'The file has both [Content_Types].xml and an OpenDocument mimetype or manifest', { reason: 'OOXML and OpenDocument markers' });
15
+ if (contentTypes)
16
+ await (0, ooxml_1.inspectOoxml)(ctx, contentTypes);
17
+ else if (odf)
18
+ await (0, odf_1.inspectOdf)(ctx);
19
+ else
20
+ throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The archive has neither [Content_Types].xml nor an OpenDocument mimetype or manifest', { reason: 'a plain ZIP archive' });
21
+ }
package/dist/ppt.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ import { type CfbEntry, CompoundFile } from './cfb';
2
+ import { type RunContext } from './context';
3
+ /**
4
+ * Inspects a PowerPoint 97 to 2003 presentation. PowerPoint finds its objects through the persist directory that the
5
+ * current edit names. This reads every record at the top level of the stream as well as every object the persist
6
+ * directories name, so an object that one route hides is still read.
7
+ */
8
+ export declare function powerPointRules(ctx: RunContext, cfb: CompoundFile, stream: CfbEntry): Promise<void>;