@patrickdk77/office-defuse 0.2.2 → 0.3.0

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.
@@ -0,0 +1,51 @@
1
+ import { type CfbEntry, type CompoundFile } from './cfb';
2
+ /** The number of classes in the class table of a FormControl (MS-OFORMS 2.2.10.1), the `f` stream. */
3
+ export declare function classTableCount(f: Uint8Array): number;
4
+ /**
5
+ * Why a user form's designer storage may hold something other than Forms 2.0 controls, or undefined when it holds
6
+ * only those: the UserForm designer, storages of Forms 2.0 classes, and forms whose class tables are empty.
7
+ */
8
+ export declare function designerProblem(cfb: CompoundFile, storage: CfbEntry, limit: number): Promise<string | undefined>;
9
+ /**
10
+ * The properties a Forms 2.0 control persists, in the order of its property mask (MS-OFORMS 2.2). Each takes the
11
+ * next bit. A number sits in the data block, aligned to its size; a string or a pair in the extra data block; a
12
+ * picture in the stream data; a flag holds no data. A bit MS-OFORMS leaves undefined must be clear.
13
+ */
14
+ type Prop = 'u8' | 'u16' | 'u32' | 'pair' | 'string' | 'picture' | 'flag' | 'undefined';
15
+ interface ControlLayout {
16
+ props: Prop[];
17
+ /** The property mask is 64 bits wide. */
18
+ wide: boolean;
19
+ text: boolean;
20
+ }
21
+ /** The Forms 2.0 controls office-defuse reads, by class id. Frames, multipages and tab strips hold other controls and are not here. */
22
+ export declare const FORMS_CONTROLS: ReadonlyMap<string, ControlLayout>;
23
+ /** The ProgIDs of those controls, which a property bag or CompObj may name. */
24
+ export declare const FORMS_PROGIDS: ReadonlyMap<string, string>;
25
+ /**
26
+ * Why the persisted data of a Forms 2.0 control may not be what Forms 2.0 writes, or undefined when it is: the
27
+ * control's property block, then its TextProps for a control with text, then the column infos a list control counts,
28
+ * and nothing after. A picture or a mouse icon counts as a problem, since its bytes are a picture file Office decodes
29
+ * and office-defuse cannot rewrite in place.
30
+ */
31
+ export declare function controlProblem(classId: string, data: Uint8Array): string | undefined;
32
+ /**
33
+ * Why a Forms 2.0 control persisted as a property bag in its activeX part may hold more than plain values, or
34
+ * undefined when it does not. `props` are the bag's name and value pairs, `nested` says ax:ocx held any other element,
35
+ * inside an ax:ocxPr or beside one, such as a font or a picture.
36
+ */
37
+ export declare function bagProblem(classId: string, props: ReadonlyArray<{
38
+ name: string;
39
+ value: string;
40
+ }>, nested: boolean): string | undefined;
41
+ /** Streams a control's storage holds in an OOXML binary part: its class and its properties. */
42
+ export declare const OOXML_CONTROL_STREAMS: ReadonlySet<string>;
43
+ /** In a Word document, the storage also holds the control's name and its picture for printing. */
44
+ export declare const WORD_CONTROL_STREAMS: ReadonlySet<string>;
45
+ /**
46
+ * Why a control's storage may not hold a Forms 2.0 control as Office persists one, or undefined when it does. Its
47
+ * class is the storage's class id, or its CompObj's ProgID when that is null, and must be `classId` when given. It
48
+ * holds only the `allowed` streams and a contents stream that `controlProblem` passes.
49
+ */
50
+ export declare function controlStorageProblem(cfb: CompoundFile, storage: CfbEntry, allowed: ReadonlySet<string>, classId?: string): Promise<string | undefined>;
51
+ export {};
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,26 @@
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
+ * A plugin passed or removed every module, and the project stays or goes by those answers alone. They are final:
13
+ * the report holds them, and nothing of office-defuse's own about the project.
14
+ */
15
+ answered?: boolean;
16
+ }
17
+ /**
18
+ * Shows each module of the VBA project in `vba` to the script plugins. The first plugin that accepts a module decides
19
+ * it. The project stays when every module passed; `plan` then writes it without its compiled code, so Office compiles
20
+ * the source the plugins read, and the run's kept map remembers the verdict for the output check. A module no plugin
21
+ * takes, or one a plugin does not pass, leaves the project to the VBA_PROJECT finding, as without plugins.
22
+ *
23
+ * In the run that checks an output, the plugins are not asked again: a project whose sources match one the plugins
24
+ * passed keeps their findings, and a compiled code finding if the output still holds p-code.
25
+ */
26
+ export declare function decideVba(ctx: RunContext, cfb: CompoundFile, vba: CfbEntry, location: string, plan: CompoundPlan): Promise<VbaDecision>;
package/dist/macros.js ADDED
@@ -0,0 +1,160 @@
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
+ run.macrosAnswered = true;
45
+ return { keep: true, designers, answered: true };
46
+ }
47
+ if (!plugins.length)
48
+ return { keep: false };
49
+ const start = ctx.findings.length;
50
+ let keep = project.modules.length > 0;
51
+ let answered = keep;
52
+ const used = new Set();
53
+ for (const [i, m] of project.modules.entries()) {
54
+ const at = `${location}/${(0, cfb_1.showName)(m.stream.name)}`;
55
+ const script = {
56
+ text: m.source,
57
+ trigger: `vba-${m.kind}`,
58
+ location: at,
59
+ fieldName: m.name,
60
+ language: 'vba',
61
+ form: 'office',
62
+ ...(i === 0 ? { references: project.references.map(r => ({ ...r })) } : {}),
63
+ };
64
+ let decided = false;
65
+ for (const plugin of plugins) {
66
+ try {
67
+ if (!(await plugin.accepts(script)))
68
+ continue;
69
+ }
70
+ catch (e) {
71
+ (0, contained_1.passOn)(run, e);
72
+ continue;
73
+ }
74
+ decided = true;
75
+ used.add(plugin.name);
76
+ const data = { plugin: plugin.name, module: (0, context_1.clip)(m.name) };
77
+ try {
78
+ const r = await plugin.process(script);
79
+ if (r.result === 'passed')
80
+ ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.PluginPassed, at, data);
81
+ else {
82
+ keep = false;
83
+ if (r.result !== 'removed')
84
+ answered = false;
85
+ ctx.removal(types_1.OfficeCategory.Macro, types_1.OfficeDetail.PluginRemoved, at, data);
86
+ }
87
+ pushScript(ctx, r.findings);
88
+ }
89
+ catch (e) {
90
+ (0, contained_1.passOn)(run, e);
91
+ keep = false;
92
+ answered = false;
93
+ ctx.removal(types_1.OfficeCategory.Macro, types_1.OfficeDetail.PluginFailed, at, data);
94
+ }
95
+ break;
96
+ }
97
+ if (!decided) {
98
+ keep = false;
99
+ answered = false;
100
+ }
101
+ }
102
+ if (!keep) {
103
+ if (answered)
104
+ run.macrosAnswered = true;
105
+ return { keep: false, answered };
106
+ }
107
+ const passed = { result: 'passed', plugin: [...used].join(', '), findings: ctx.findings.slice(start).map(f => ({ ...f, data: f.data && { ...f.data } })) };
108
+ const designers = await formDesigners(ctx, cfb, project, location);
109
+ if (!designers)
110
+ return { keep: false };
111
+ run.kept.set(keyOf(project), passed);
112
+ if (!(await (0, vba_1.isPurged)(cfb, project))) {
113
+ let streams;
114
+ try {
115
+ streams = (0, vba_1.purge)(project);
116
+ }
117
+ catch (e) {
118
+ if (!(e instanceof vba_1.VbaError))
119
+ throw e;
120
+ run.kept.delete(keyOf(project));
121
+ return { keep: false, reason: (0, context_1.clip)(e.message) };
122
+ }
123
+ for (const [entry, bytes] of streams.replace)
124
+ plan.replace.set(entry, await (0, contained_1.spillOf)(run, bytes));
125
+ for (const entry of streams.drop)
126
+ plan.drop.add(entry);
127
+ ctx.removal(types_1.OfficeCategory.Macro, types_1.OfficeDetail.CompiledCode, location, { storage: location });
128
+ }
129
+ run.macrosKept = true;
130
+ run.macrosAnswered = true;
131
+ return { keep: true, designers, answered: true };
132
+ }
133
+ /**
134
+ * The designer storages of the project's user forms, beside its VBA storage, named as their modules. Undefined when
135
+ * one may hold a control that is not a Forms 2.0 control, which CONTROL/ACTIVEX reports at the designer.
136
+ */
137
+ async function formDesigners(ctx, cfb, project, location) {
138
+ const holder = project.storage.parent;
139
+ if (!holder)
140
+ return [];
141
+ const base = location.slice(0, location.length - (0, cfb_1.showName)(project.storage.name).length);
142
+ const designers = [];
143
+ for (const m of project.modules) {
144
+ if (m.kind !== 'form')
145
+ continue;
146
+ const designer = holder.children.find(c => c.kind === 'storage' && c.name.toUpperCase() === m.name.toUpperCase());
147
+ if (!designer)
148
+ continue;
149
+ const problem = await (0, forms_1.designerProblem)(cfb, designer, ctx.settings.decompressedBytes);
150
+ const at = `${base}${(0, cfb_1.showName)(designer.name)}`;
151
+ if (problem && ctx.report(types_1.OfficeCategory.Control, types_1.OfficeDetail.ActiveX, at, { storage: at, reason: (0, context_1.clip)(problem) }).action === 'strip')
152
+ return undefined;
153
+ designers.push(designer);
154
+ }
155
+ return designers;
156
+ }
157
+ /** Adds a script plugin's findings, which already name the module. A reject removes the module, so it counts as strip. */
158
+ function pushScript(ctx, found) {
159
+ ctx.push((found ?? []).map(f => ({ ...f, action: f.action === 'reject' ? 'strip' : f.action, data: f.data && { ...f.data } })));
160
+ }
package/dist/msg.js CHANGED
@@ -231,7 +231,7 @@ async function attachment(ctx, cfb, data, file, info, plan) {
231
231
  const decision = await (0, contained_1.decideContained)(ctx, { ...file, chunks: () => (0, io_1.chunksOf)(source, data.size) });
232
232
  if (decision?.replacement)
233
233
  plan?.replace.set(data, decision.replacement);
234
- const r = ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Attachment, file.location, { ...info, size: data.size }, (0, contained_1.presence)(decision));
234
+ const r = (0, contained_1.ownFinding)(ctx, decision, types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Attachment, file.location, { ...info, size: data.size });
235
235
  return decision && !decision.keep ? { ...r, action: 'strip' } : r;
236
236
  }
237
237
  /** The fixed-size values of a property stream (MS-OXMSG 2.4.2), by property id. An attachment's has an 8-byte header. */