@patrickdk77/office-defuse 0.2.1 → 0.2.4

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/forms.js ADDED
@@ -0,0 +1,316 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WORD_CONTROL_STREAMS = exports.OOXML_CONTROL_STREAMS = exports.FORMS_PROGIDS = exports.FORMS_CONTROLS = void 0;
4
+ exports.classTableCount = classTableCount;
5
+ exports.designerProblem = designerProblem;
6
+ exports.controlProblem = controlProblem;
7
+ exports.bagProblem = bagProblem;
8
+ exports.controlStorageProblem = controlStorageProblem;
9
+ const cfb_1 = require("./cfb");
10
+ const oleobj_1 = require("./oleobj");
11
+ /**
12
+ * The designer storage of a VBA user form (MS-OVBA 2.2.10), which holds the form and its controls as Forms 2.0
13
+ * persists them (MS-OFORMS). A form site names its control's class by an index into Forms 2.0's own controls, or
14
+ * into the form's class table, which may name any ActiveX control. A form whose class tables are empty therefore
15
+ * holds Forms 2.0 controls only.
16
+ */
17
+ /** The Forms 2.0 form, frame and multipage, the classes a designer and the storages under it are written as. */
18
+ const FORM_CLASSES = new Set(['C62A69F0-16DC-11CE-9E98-00AA00574A4F', '6E182020-F460-11CE-9BCD-00AA00608E01', '46E31370-3F7A-11CE-BED6-00AA00611080']);
19
+ const FORM_PROGIDS = new Set(['forms.form.1', 'forms.frame.1', 'forms.multipage.1']);
20
+ /** The UserForm designer, which \x03VBFrame names. */
21
+ const USER_FORM = 'C62A69F0-16DC-11CE-9E98-00AA00574A4F';
22
+ const NULL_CLSID = '00000000-0000-0000-0000-000000000000';
23
+ /** StdFont's GUID, as GuidAndFont writes it, which a StdFont structure follows; any other is TextProps. */
24
+ const STD_FONT = '0352e30b918fce119de300aa004bb851';
25
+ /** FORM_FLAG_DONTSAVECLASSTABLE, in BooleanProperties. */
26
+ const NO_CLASS_TABLE = 0x8000;
27
+ class FormError extends Error {
28
+ }
29
+ /** The number of classes in the class table of a FormControl (MS-OFORMS 2.2.10.1), the `f` stream. */
30
+ function classTableCount(f) {
31
+ const b = Buffer.from(f.buffer, f.byteOffset, f.byteLength);
32
+ const need = (at, n) => {
33
+ if (at + n > b.length)
34
+ throw new FormError('a form stream cut short');
35
+ };
36
+ need(0, 8);
37
+ if (b[1] !== 0x04)
38
+ throw new FormError(`a form of version ${b[1]}.${b[0]}`);
39
+ const cbForm = b.readUInt16LE(2);
40
+ const mask = b.readUInt32LE(4);
41
+ // BooleanProperties follows BackColor, ForeColor and NextAvailableID, each 4 bytes when present.
42
+ let booleans = 0x04;
43
+ if (mask & (1 << 6)) {
44
+ let at = 8;
45
+ for (const bit of [1, 2, 3])
46
+ if (mask & (1 << bit))
47
+ at += 4;
48
+ need(at, 4);
49
+ booleans = b.readUInt32LE(at);
50
+ }
51
+ // cbForm counts the property mask and the data blocks; the stream data follows them.
52
+ let p = 4 + cbForm;
53
+ const picture = () => {
54
+ need(p, 24);
55
+ p += 16;
56
+ if (b.readUInt32LE(p) !== 0x746c)
57
+ throw new FormError('a form picture without its preamble');
58
+ p += 8 + b.readUInt32LE(p + 4);
59
+ };
60
+ if (mask & (1 << 15))
61
+ picture();
62
+ if (mask & (1 << 20)) {
63
+ need(p, 16);
64
+ const stdFont = b.subarray(p, p + 16).toString('hex') === STD_FONT;
65
+ p += 16;
66
+ if (stdFont) {
67
+ need(p, 11);
68
+ p += 11 + b[p + 10];
69
+ }
70
+ else {
71
+ need(p, 4);
72
+ p += 4 + b.readUInt16LE(p + 2);
73
+ }
74
+ }
75
+ if (mask & (1 << 21))
76
+ picture();
77
+ if (booleans & NO_CLASS_TABLE)
78
+ return 0;
79
+ need(p, 2);
80
+ return b.readUInt16LE(p);
81
+ }
82
+ /**
83
+ * Why a user form's designer storage may hold something other than Forms 2.0 controls, or undefined when it holds
84
+ * only those: the UserForm designer, storages of Forms 2.0 classes, and forms whose class tables are empty.
85
+ */
86
+ async function designerProblem(cfb, storage, limit) {
87
+ const frame = storage.children.find(c => c.kind === 'stream' && c.name === '\x03VBFrame');
88
+ if (!frame)
89
+ return 'it has no \\x03VBFrame stream';
90
+ if (frame.size > limit)
91
+ return 'its \\x03VBFrame stream is past the limit';
92
+ const designer = /Begin\s+\{([0-9A-Fa-f-]{36})\}/.exec(Buffer.from(await cfb.bytes(frame)).toString('latin1'))?.[1]?.toUpperCase();
93
+ if (designer !== USER_FORM)
94
+ return `its designer is ${designer ?? 'not named'}, not the UserForm designer`;
95
+ const walk = async (s, top) => {
96
+ for (const c of s.children) {
97
+ if (c.kind === 'storage') {
98
+ if (!/^i\d+$/.test(c.name))
99
+ return `it holds a storage named ${JSON.stringify(c.name)}`;
100
+ const cls = c.clsid === NULL_CLSID ? '' : c.clsid;
101
+ if (!cls) {
102
+ const compObj = c.children.find(x => x.kind === 'stream' && x.name === '\x01CompObj');
103
+ const progId = compObj && compObj.size <= 65536 ? (0, oleobj_1.parseCompObj)(await cfb.bytes(compObj)).progId : undefined;
104
+ if (!progId || !FORM_PROGIDS.has(progId.trim().toLowerCase()))
105
+ return `it holds a control of class ${progId ?? 'not named'}`;
106
+ }
107
+ else if (!FORM_CLASSES.has(cls))
108
+ return `it holds a control of class ${cls}`;
109
+ const inner = await walk(c, false);
110
+ if (inner)
111
+ return inner;
112
+ continue;
113
+ }
114
+ if (c.name === 'f') {
115
+ if (c.size > limit)
116
+ return 'a form stream is past the limit';
117
+ try {
118
+ if (classTableCount(await cfb.bytes(c)) > 0)
119
+ return 'a form names controls in its class table, which may be any ActiveX control';
120
+ }
121
+ catch (e) {
122
+ if (!(e instanceof FormError))
123
+ throw e;
124
+ return e.message;
125
+ }
126
+ }
127
+ else if (!['o', 'x', '\x01CompObj'].includes(c.name) && !(top && c.name === '\x03VBFrame'))
128
+ return `it holds a stream named ${JSON.stringify(c.name)}`;
129
+ }
130
+ return undefined;
131
+ };
132
+ return walk(storage, true);
133
+ }
134
+ const BUTTON = ['u32', 'u32', 'u32', 'string', 'u32', 'pair', 'u8', 'picture', 'u16', 'flag', 'picture'];
135
+ const LABEL = ['u32', 'u32', 'u32', 'string', 'u32', 'pair', 'u8', 'u32', 'u16', 'u16', 'picture', 'u16', 'picture'];
136
+ // TextBox, ListBox, ComboBox, CheckBox, OptionButton and ToggleButton. The 16th property counts the column infos.
137
+ const MORPH = [
138
+ ...['u32', 'u32', 'u32', 'u32', 'u8', 'u8', 'u8', 'u8', 'pair', 'u16', 'u32', 'u16', 'u16', 'u16', 'u16', 'u16'],
139
+ ...['u8', 'u8', 'u8', 'undefined', 'u8', 'u8', 'string', 'string', 'u32', 'u32', 'u32', 'picture', 'picture', 'u16', 'undefined', 'flag', 'string'],
140
+ ];
141
+ const MORPH_COLUMN_INFOS = 15;
142
+ const SPIN = ['u32', 'u32', 'u32', 'pair', 'u32', 'u32', 'u32', 'u32', 'u32', 'u32', 'u32', 'u32', 'u32', 'picture', 'u8'];
143
+ const SCROLL = ['u32', 'u32', 'u32', 'pair', 'u8', 'u32', 'u32', 'u32', 'u32', 'u32', 'u32', 'u32', 'u32', 'u32', 'u16', 'u32', 'picture'];
144
+ /** TextProps (MS-OFORMS 2.3.1), the font a control with text writes after its own properties. */
145
+ const TEXT_PROPS = ['string', 'u32', 'u32', 'u32', 'u8', 'u8', 'u8', 'u16'];
146
+ const COLUMN_INFO = ['u32'];
147
+ /** The Forms 2.0 controls office-defuse reads, by class id. Frames, multipages and tab strips hold other controls and are not here. */
148
+ exports.FORMS_CONTROLS = new Map([
149
+ ['D7053240-CE69-11CD-A777-00DD01143C57', { props: BUTTON, wide: false, text: true }],
150
+ ['978C9E23-D4B0-11CE-BF2D-00AA003F40D0', { props: LABEL, wide: false, text: true }],
151
+ ...['8BD21D10', '8BD21D20', '8BD21D30', '8BD21D40', '8BD21D50', '8BD21D60'].map((p) => [`${p}-EC42-11CE-9E0D-00AA006002F3`, { props: MORPH, wide: true, text: true }]),
152
+ ['79176FB0-B7F2-11CE-97EF-00AA006D2776', { props: SPIN, wide: false, text: false }],
153
+ ['DFD181E0-5E2F-11CE-A449-00AA004A803D', { props: SCROLL, wide: false, text: false }],
154
+ ]);
155
+ /** The ProgIDs of those controls, which a property bag or CompObj may name. */
156
+ exports.FORMS_PROGIDS = new Map([
157
+ ['forms.commandbutton.1', 'D7053240-CE69-11CD-A777-00DD01143C57'],
158
+ ['forms.label.1', '978C9E23-D4B0-11CE-BF2D-00AA003F40D0'],
159
+ ['forms.textbox.1', '8BD21D10-EC42-11CE-9E0D-00AA006002F3'],
160
+ ['forms.listbox.1', '8BD21D20-EC42-11CE-9E0D-00AA006002F3'],
161
+ ['forms.combobox.1', '8BD21D30-EC42-11CE-9E0D-00AA006002F3'],
162
+ ['forms.checkbox.1', '8BD21D40-EC42-11CE-9E0D-00AA006002F3'],
163
+ ['forms.optionbutton.1', '8BD21D50-EC42-11CE-9E0D-00AA006002F3'],
164
+ ['forms.togglebutton.1', '8BD21D60-EC42-11CE-9E0D-00AA006002F3'],
165
+ ['forms.spinbutton.1', '79176FB0-B7F2-11CE-97EF-00AA006D2776'],
166
+ ['forms.scrollbar.1', 'DFD181E0-5E2F-11CE-A449-00AA004A803D'],
167
+ ]);
168
+ /** Strings a control persists are at most this many bytes, as Forms 2.0 and LibreOffice read them. */
169
+ const MAX_STRING = 0x20000;
170
+ /**
171
+ * Reads one property block at `start`: version 2.0, the block size, the property mask and the properties it names.
172
+ * Returns where the block ends and the numbers read, by property index. Throws FormError on a picture, a bit of an
173
+ * undefined property, a bit past the last property, or a property that runs past the block.
174
+ */
175
+ function readBlock(d, start, props, wide) {
176
+ const need = (at, n, what) => {
177
+ if (at + n > d.length)
178
+ throw new FormError(`${what} cut short`);
179
+ };
180
+ need(start, wide ? 12 : 8, 'a control property block');
181
+ if (d[start] !== 0 || d[start + 1] !== 2)
182
+ throw new FormError(`a control property block of version ${d[start + 1]}.${d[start]}`);
183
+ const end = start + 4 + d.readUInt16LE(start + 2);
184
+ if (end > d.length)
185
+ throw new FormError('a control property block that runs past its stream');
186
+ let low = d.readUInt32LE(start + 4);
187
+ let high = wide ? d.readUInt32LE(start + 8) : 0;
188
+ let pos = start + (wide ? 12 : 8);
189
+ const align = (n) => {
190
+ pos += (n - ((pos - start) % n)) % n;
191
+ };
192
+ const values = new Map();
193
+ const extra = [];
194
+ props.forEach((p, i) => {
195
+ const has = i < 32 ? (low >>> i) & 1 : (high >>> (i - 32)) & 1;
196
+ if (i < 32)
197
+ low &= ~(1 << i);
198
+ else
199
+ high &= ~(1 << (i - 32));
200
+ if (!has || p === 'flag')
201
+ return;
202
+ if (p === 'undefined')
203
+ throw new FormError('a control property MS-OFORMS leaves undefined');
204
+ if (p === 'picture')
205
+ throw new FormError('a control that holds a picture');
206
+ if (p === 'pair')
207
+ extra.push({ kind: 'pair' });
208
+ else if (p === 'string') {
209
+ align(4);
210
+ need(pos, 4, 'a control string size');
211
+ extra.push({ kind: 'string', size: d.readUInt32LE(pos) });
212
+ pos += 4;
213
+ }
214
+ else {
215
+ const n = p === 'u8' ? 1 : p === 'u16' ? 2 : 4;
216
+ align(n);
217
+ need(pos, n, 'a control property');
218
+ values.set(i, n === 1 ? d[pos] : n === 2 ? d.readUInt16LE(pos) : d.readUInt32LE(pos));
219
+ pos += n;
220
+ }
221
+ });
222
+ if (low !== 0 || high !== 0)
223
+ throw new FormError('a control property mask with bits past its last property');
224
+ align(4);
225
+ for (const x of extra) {
226
+ const n = x.kind === 'pair' ? 8 : x.size & 0x7fffffff;
227
+ if (n > MAX_STRING)
228
+ throw new FormError('a control string past 128 KiB');
229
+ pos += n;
230
+ align(4);
231
+ }
232
+ if (pos > end)
233
+ throw new FormError('control properties that run past their block');
234
+ return { end, values };
235
+ }
236
+ /**
237
+ * Why the persisted data of a Forms 2.0 control may not be what Forms 2.0 writes, or undefined when it is: the
238
+ * control's property block, then its TextProps for a control with text, then the column infos a list control counts,
239
+ * and nothing after. A picture or a mouse icon counts as a problem, since its bytes are a picture file Office decodes
240
+ * and office-defuse cannot rewrite in place.
241
+ */
242
+ function controlProblem(classId, data) {
243
+ const layout = exports.FORMS_CONTROLS.get(classId.toUpperCase());
244
+ if (!layout)
245
+ return `its class ${classId} is not a Forms 2.0 control office-defuse reads`;
246
+ const d = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
247
+ try {
248
+ const own = readBlock(d, 0, layout.props, layout.wide);
249
+ let pos = own.end;
250
+ if (layout.text)
251
+ pos = readBlock(d, pos, TEXT_PROPS, false).end;
252
+ const columns = layout.props === MORPH ? (own.values.get(MORPH_COLUMN_INFOS) ?? 0) : 0;
253
+ for (let k = 0; k < columns; k++)
254
+ pos = readBlock(d, pos, COLUMN_INFO, false).end;
255
+ if (pos !== d.length)
256
+ return `${d.length - pos} bytes after the control's properties`;
257
+ return undefined;
258
+ }
259
+ catch (e) {
260
+ if (!(e instanceof FormError))
261
+ throw e;
262
+ return e.message;
263
+ }
264
+ }
265
+ /** Property bag names that load a picture, which a Forms 2.0 control takes from a part of its own. */
266
+ const BAG_PICTURES = new Set(['picture', 'mouseicon']);
267
+ /**
268
+ * Why a Forms 2.0 control persisted as a property bag in its activeX part may hold more than plain values, or
269
+ * undefined when it does not. `props` are the bag's name and value pairs, `nested` says ax:ocx held any other element,
270
+ * inside an ax:ocxPr or beside one, such as a font or a picture.
271
+ */
272
+ function bagProblem(classId, props, nested) {
273
+ if (!exports.FORMS_CONTROLS.has(classId.toUpperCase()))
274
+ return `its class ${classId} is not a Forms 2.0 control office-defuse reads`;
275
+ if (nested)
276
+ return 'it holds elements besides plain ax:ocxPr values, such as a font or a picture';
277
+ const picture = props.find(p => BAG_PICTURES.has(p.name.toLowerCase()));
278
+ if (picture)
279
+ return `its ${picture.name} property loads a picture`;
280
+ return undefined;
281
+ }
282
+ /** Streams a control's storage holds in an OOXML binary part: its class and its properties. */
283
+ exports.OOXML_CONTROL_STREAMS = new Set(['\x01CompObj', '\x03ObjInfo', 'contents']);
284
+ /** In a Word document, the storage also holds the control's name and its picture for printing. */
285
+ exports.WORD_CONTROL_STREAMS = new Set([...exports.OOXML_CONTROL_STREAMS, '\x03OCXNAME', '\x03PRINT']);
286
+ /** A Forms 2.0 control's contents stream, without pictures, is far smaller. */
287
+ const MAX_CONTENTS = 65536;
288
+ /**
289
+ * Why a control's storage may not hold a Forms 2.0 control as Office persists one, or undefined when it does. Its
290
+ * class is the storage's class id, or its CompObj's ProgID when that is null, and must be `classId` when given. It
291
+ * holds only the `allowed` streams and a contents stream that `controlProblem` passes.
292
+ */
293
+ async function controlStorageProblem(cfb, storage, allowed, classId) {
294
+ const stream = (name) => storage.children.find(c => c.kind === 'stream' && c.name === name);
295
+ let cls = storage.clsid === NULL_CLSID ? undefined : storage.clsid;
296
+ if (!cls) {
297
+ const compObj = stream('\x01CompObj');
298
+ const progId = compObj && compObj.size <= MAX_CONTENTS ? (0, oleobj_1.parseCompObj)(await cfb.bytes(compObj)).progId : undefined;
299
+ cls = progId ? exports.FORMS_PROGIDS.get(progId.trim().toLowerCase()) : undefined;
300
+ if (!cls)
301
+ return `its class ${progId ?? 'is not named'}${progId ? ' is not a Forms 2.0 control office-defuse reads' : ''}`;
302
+ }
303
+ if (classId && cls !== classId)
304
+ return 'its storage names another class';
305
+ if (!exports.FORMS_CONTROLS.has(cls))
306
+ return `its class ${cls} is not a Forms 2.0 control office-defuse reads`;
307
+ for (const c of storage.children)
308
+ if (c.kind !== 'stream' || !allowed.has(c.name))
309
+ return `it holds a ${c.kind} named ${JSON.stringify((0, cfb_1.showName)(c.name))}`;
310
+ const contents = stream('contents');
311
+ if (!contents)
312
+ return 'it has no contents stream';
313
+ if (contents.size > MAX_CONTENTS)
314
+ return 'its contents stream is larger than a Forms 2.0 control without pictures';
315
+ return controlProblem(cls, await cfb.bytes(contents));
316
+ }
@@ -0,0 +1,21 @@
1
+ import { type CfbEntry, type CompoundFile } from './cfb';
2
+ import { type RunContext } from './context';
3
+ import type { CompoundPlan } from './plan';
4
+ /** What the script plugins decided about a VBA project. */
5
+ export interface VbaDecision {
6
+ keep: boolean;
7
+ /** Why the project could not go to the plugins. */
8
+ reason?: string;
9
+ /** The designer storages of the project's user forms, which stay with it and get no object rules of their own. */
10
+ designers?: CfbEntry[];
11
+ }
12
+ /**
13
+ * Shows each module of the VBA project in `vba` to the script plugins. The first plugin that accepts a module decides
14
+ * it. The project stays when every module passed; `plan` then writes it without its compiled code, so Office compiles
15
+ * the source the plugins read, and the run's kept map remembers the verdict for the output check. A module no plugin
16
+ * takes, or one a plugin does not pass, leaves the project to the VBA_PROJECT finding, as without plugins.
17
+ *
18
+ * In the run that checks an output, the plugins are not asked again: a project whose sources match one the plugins
19
+ * passed keeps their findings, and a compiled code finding if the output still holds p-code.
20
+ */
21
+ export declare function decideVba(ctx: RunContext, cfb: CompoundFile, vba: CfbEntry, location: string, plan: CompoundPlan): Promise<VbaDecision>;
package/dist/macros.js ADDED
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.decideVba = decideVba;
4
+ const cfb_1 = require("./cfb");
5
+ const contained_1 = require("./contained");
6
+ const context_1 = require("./context");
7
+ const forms_1 = require("./forms");
8
+ const types_1 = require("./types");
9
+ const vba_1 = require("./vba");
10
+ /** Keys of the run's kept map for a VBA project, which never match the MD5 of a file. */
11
+ const keyOf = (project) => `vba:${project.digest}`;
12
+ /**
13
+ * Shows each module of the VBA project in `vba` to the script plugins. The first plugin that accepts a module decides
14
+ * it. The project stays when every module passed; `plan` then writes it without its compiled code, so Office compiles
15
+ * the source the plugins read, and the run's kept map remembers the verdict for the output check. A module no plugin
16
+ * takes, or one a plugin does not pass, leaves the project to the VBA_PROJECT finding, as without plugins.
17
+ *
18
+ * In the run that checks an output, the plugins are not asked again: a project whose sources match one the plugins
19
+ * passed keeps their findings, and a compiled code finding if the output still holds p-code.
20
+ */
21
+ async function decideVba(ctx, cfb, vba, location, plan) {
22
+ const run = ctx.run;
23
+ const plugins = run.options.scriptPlugins ?? [];
24
+ if (!plugins.length && !run.verify?.size)
25
+ return { keep: false };
26
+ let project;
27
+ try {
28
+ project = await (0, vba_1.readProject)(cfb, vba, ctx.settings.decompressedBytes);
29
+ }
30
+ catch (e) {
31
+ if (!(e instanceof vba_1.VbaError))
32
+ throw e;
33
+ return { keep: false, reason: (0, context_1.clip)(e.message) };
34
+ }
35
+ const expected = run.verify?.get(keyOf(project));
36
+ if (expected) {
37
+ ctx.push((expected.findings ?? []).map(f => ({ ...f, data: f.data && { ...f.data } })));
38
+ const designers = await formDesigners(ctx, cfb, project, location);
39
+ if (!designers)
40
+ return { keep: false };
41
+ if (!(await (0, vba_1.isPurged)(cfb, project)))
42
+ ctx.removal(types_1.OfficeCategory.Macro, types_1.OfficeDetail.CompiledCode, location, { storage: location });
43
+ run.macrosKept = true;
44
+ return { keep: true, designers };
45
+ }
46
+ if (!plugins.length)
47
+ return { keep: false };
48
+ const start = ctx.findings.length;
49
+ let keep = project.modules.length > 0;
50
+ const used = new Set();
51
+ for (const [i, m] of project.modules.entries()) {
52
+ const at = `${location}/${(0, cfb_1.showName)(m.stream.name)}`;
53
+ const script = {
54
+ text: m.source,
55
+ trigger: `vba-${m.kind}`,
56
+ location: at,
57
+ fieldName: m.name,
58
+ language: 'vba',
59
+ form: 'office',
60
+ ...(i === 0 ? { references: project.references.map(r => ({ ...r })) } : {}),
61
+ };
62
+ let decided = false;
63
+ for (const plugin of plugins) {
64
+ try {
65
+ if (!(await plugin.accepts(script)))
66
+ continue;
67
+ }
68
+ catch (e) {
69
+ (0, contained_1.passOn)(run, e);
70
+ continue;
71
+ }
72
+ decided = true;
73
+ used.add(plugin.name);
74
+ const data = { plugin: plugin.name, module: (0, context_1.clip)(m.name) };
75
+ try {
76
+ const r = await plugin.process(script);
77
+ if (r.result === 'passed')
78
+ ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.PluginPassed, at, data);
79
+ else {
80
+ keep = false;
81
+ ctx.removal(types_1.OfficeCategory.Macro, types_1.OfficeDetail.PluginRemoved, at, data);
82
+ }
83
+ pushScript(ctx, r.findings);
84
+ }
85
+ catch (e) {
86
+ (0, contained_1.passOn)(run, e);
87
+ keep = false;
88
+ ctx.removal(types_1.OfficeCategory.Macro, types_1.OfficeDetail.PluginFailed, at, data);
89
+ }
90
+ break;
91
+ }
92
+ if (!decided)
93
+ keep = false;
94
+ }
95
+ if (!keep)
96
+ return { keep: false };
97
+ const passed = { result: 'passed', plugin: [...used].join(', '), findings: ctx.findings.slice(start).map(f => ({ ...f, data: f.data && { ...f.data } })) };
98
+ const designers = await formDesigners(ctx, cfb, project, location);
99
+ if (!designers)
100
+ return { keep: false };
101
+ run.kept.set(keyOf(project), passed);
102
+ if (!(await (0, vba_1.isPurged)(cfb, project))) {
103
+ let streams;
104
+ try {
105
+ streams = (0, vba_1.purge)(project);
106
+ }
107
+ catch (e) {
108
+ if (!(e instanceof vba_1.VbaError))
109
+ throw e;
110
+ run.kept.delete(keyOf(project));
111
+ return { keep: false, reason: (0, context_1.clip)(e.message) };
112
+ }
113
+ for (const [entry, bytes] of streams.replace)
114
+ plan.replace.set(entry, await (0, contained_1.spillOf)(run, bytes));
115
+ for (const entry of streams.drop)
116
+ plan.drop.add(entry);
117
+ ctx.removal(types_1.OfficeCategory.Macro, types_1.OfficeDetail.CompiledCode, location, { storage: location });
118
+ }
119
+ run.macrosKept = true;
120
+ return { keep: true, designers };
121
+ }
122
+ /**
123
+ * The designer storages of the project's user forms, beside its VBA storage, named as their modules. Undefined when
124
+ * one may hold a control that is not a Forms 2.0 control, which CONTROL/ACTIVEX reports at the designer.
125
+ */
126
+ async function formDesigners(ctx, cfb, project, location) {
127
+ const holder = project.storage.parent;
128
+ if (!holder)
129
+ return [];
130
+ const base = location.slice(0, location.length - (0, cfb_1.showName)(project.storage.name).length);
131
+ const designers = [];
132
+ for (const m of project.modules) {
133
+ if (m.kind !== 'form')
134
+ continue;
135
+ const designer = holder.children.find(c => c.kind === 'storage' && c.name.toUpperCase() === m.name.toUpperCase());
136
+ if (!designer)
137
+ continue;
138
+ const problem = await (0, forms_1.designerProblem)(cfb, designer, ctx.settings.decompressedBytes);
139
+ const at = `${base}${(0, cfb_1.showName)(designer.name)}`;
140
+ if (problem && ctx.report(types_1.OfficeCategory.Control, types_1.OfficeDetail.ActiveX, at, { storage: at, reason: (0, context_1.clip)(problem) }).action === 'strip')
141
+ return undefined;
142
+ designers.push(designer);
143
+ }
144
+ return designers;
145
+ }
146
+ /** Adds a script plugin's findings, which already name the module. A reject removes the module, so it counts as strip. */
147
+ function pushScript(ctx, found) {
148
+ ctx.push((found ?? []).map(f => ({ ...f, action: f.action === 'reject' ? 'strip' : f.action, data: f.data && { ...f.data } })));
149
+ }
package/dist/objects.js CHANGED
@@ -11,7 +11,9 @@ const cfb_writer_1 = require("./cfb-writer");
11
11
  const contained_1 = require("./contained");
12
12
  const context_1 = require("./context");
13
13
  const errors_1 = require("./errors");
14
+ const forms_1 = require("./forms");
14
15
  const io_1 = require("./io");
16
+ const macros_1 = require("./macros");
15
17
  const oleobj_1 = require("./oleobj");
16
18
  const rewrite_cfb_1 = require("./rewrite-cfb");
17
19
  const types_1 = require("./types");
@@ -44,32 +46,57 @@ const at = (prefix, e) => [prefix, (0, cfb_1.entryPath)(e)].filter(Boolean).join
44
46
  */
45
47
  async function compoundRules(ctx, cfb, prefix, rootIsObject, plan, rootDecision) {
46
48
  const projects = new Set();
49
+ /** Storages of embedded objects, whose contents the plugins that take the object decide. */
50
+ const objects = new Set();
51
+ const inObject = (e) => {
52
+ for (let p = e.parent; p; p = p.parent)
53
+ if (objects.has(p))
54
+ return true;
55
+ return false;
56
+ };
57
+ /** User form designers of a VBA project that stays, which go with the project rather than as objects. */
58
+ const designers = new Set();
59
+ const inDesigner = (e) => {
60
+ for (let p = e; p; p = p.parent)
61
+ if (designers.has(p))
62
+ return true;
63
+ return false;
64
+ };
47
65
  const drop = (r, e) => {
48
66
  if (r.action === 'strip' && e.kind !== 'root')
49
67
  plan?.drop.add(e);
50
68
  };
51
69
  for (const e of cfb.entries) {
52
- if (e.kind === 'stream')
70
+ if (e.kind === 'stream' || inDesigner(e))
53
71
  continue;
54
72
  const child = (name) => e.children.find(c => upper(c) === name);
73
+ const names = new Set(e.children.map(upper));
74
+ const parent = e.parent ? upper(e.parent) : '';
75
+ const isObject = e.kind === 'root'
76
+ ? rootIsObject
77
+ : [...names].some(n => OBJECT_STREAMS.has(n) || CONTROL_STREAMS.has(n)) ||
78
+ (parent === 'OBJECTPOOL' && e.parent?.parent?.kind === 'root') ||
79
+ (e.parent?.kind === 'root' && /^MBD[0-9A-F]{8}$/i.test(e.name));
80
+ if (isObject)
81
+ objects.add(e);
55
82
  // A VBA project: a VBA storage with a dir stream, which names the modules (MS-OVBA 2.2). The storage that holds
56
- // the project goes, or the VBA storage when that is the root.
83
+ // the project goes, or the VBA storage when that is the root. With script plugins, the project stays when they
84
+ // pass every module and the rewrite can take out its compiled code: in the file the rewrite writes, and outside any
85
+ // embedded object, whose plugins decide what it holds.
57
86
  const vba = child('VBA');
58
87
  if (vba?.kind === 'storage' && vba.children.some(c => c.kind === 'stream' && upper(c) === 'DIR')) {
59
88
  projects.add(e);
60
- drop(ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.VbaProject, at(prefix, vba), { storage: at(prefix, vba) }), e.kind === 'root' ? vba : e);
89
+ const decision = plan && !objects.has(e) && !inObject(e) ? await (0, macros_1.decideVba)(ctx, cfb, vba, at(prefix, vba), plan) : undefined;
90
+ const data = { storage: at(prefix, vba), ...(decision?.reason ? { reason: decision.reason } : {}) };
91
+ if (decision?.keep)
92
+ for (const d of decision.designers ?? [])
93
+ designers.add(d);
94
+ drop(ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.VbaProject, at(prefix, vba), data, decision?.keep ? 'info' : undefined), e.kind === 'root' ? vba : e);
61
95
  }
62
96
  if (e.kind === 'storage' && (upper(e) === '_VBA_PROJECT_CUR' || upper(e) === 'MACROS') && e.children.length && !projects.has(e)) {
63
97
  // Excel's and Word's project storages, without the dir stream a whole project has.
64
98
  drop(ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.VbaProject, at(prefix, e), { storage: at(prefix, e) }), e);
65
99
  }
66
- const names = new Set(e.children.map(upper));
67
- const parent = e.parent ? upper(e.parent) : '';
68
- const isObject = e.kind === 'root'
69
- ? rootIsObject
70
- : [...names].some(n => OBJECT_STREAMS.has(n) || CONTROL_STREAMS.has(n)) ||
71
- (parent === 'OBJECTPOOL' && e.parent?.parent?.kind === 'root') ||
72
- (e.parent?.kind === 'root' && /^MBD[0-9A-F]{8}$/i.test(e.name));
73
100
  if (isObject)
74
101
  for (const r of await objectRules(ctx, cfb, e, at(prefix, e), plan, e.kind === 'root' ? rootDecision : undefined))
75
102
  drop(r, e);
@@ -169,8 +196,19 @@ async function objectRules(ctx, cfb, e, location, plan, given) {
169
196
  data.progId = (0, context_1.clip)(d.progId);
170
197
  if (d.userType)
171
198
  data.className = (0, context_1.clip)(d.userType);
172
- if (d.control)
173
- return [ctx.report(types_1.OfficeCategory.Control, types_1.OfficeDetail.ActiveX, location, d.controlName ? { ...data, name: (0, context_1.clip)(d.controlName) } : data)];
199
+ if (d.control) {
200
+ if (d.controlName)
201
+ data.name = (0, context_1.clip)(d.controlName);
202
+ // A Forms 2.0 control stays when its storage and properties are as Office writes them. Its picture for printing,
203
+ // a metafile beside them, goes on its own.
204
+ const problem = await (0, forms_1.controlStorageProblem)(cfb, e, forms_1.WORD_CONTROL_STREAMS);
205
+ if (problem)
206
+ return [ctx.report(types_1.OfficeCategory.Control, types_1.OfficeDetail.ActiveX, location, { ...data, reason: (0, context_1.clip)(problem) })];
207
+ const print = e.children.find(c => c.kind === 'stream' && c.name === '\x03PRINT');
208
+ if (print && ctx.report(types_1.OfficeCategory.Control, types_1.OfficeDetail.ControlPicture, `${location}/${(0, cfb_1.showName)(print.name)}`, { storage: location, bytes: print.size }).action === 'strip')
209
+ plan?.drop.add(print);
210
+ return [ctx.report(types_1.OfficeCategory.Control, types_1.OfficeDetail.FormsControl, location, data)];
211
+ }
174
212
  const found = [];
175
213
  if (d.pkg) {
176
214
  Object.assign(data, packageData(d.pkg));
package/dist/ole.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- import type { CompoundFile } from './cfb';
2
- import type { RunContext } from './context';
1
+ import { type CompoundFile } from './cfb';
2
+ import { type RunContext } from './context';
3
3
  import type { Format } from './formats';
4
4
  import { ZipArchive } from './zip';
5
5
  /** The families a compound file can hold, by the stream or storage that marks each. */
6
- export type Family = 'word' | 'excel' | 'powerpoint' | 'visio' | 'outlook' | 'publisher' | 'project' | 'encrypted';
6
+ export type Family = 'word' | 'excel' | 'powerpoint' | 'powerpoint-addin' | 'visio' | 'outlook' | 'publisher' | 'project' | 'encrypted';
7
7
  /** What the root of a compound file holds, read by the names of its children, ignoring case as MS-CFB does. */
8
8
  export declare function families(cfb: CompoundFile): Family[];
9
9
  /**