@patrickdk77/office-defuse 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +211 -43
  2. package/dist/cfb-writer.d.ts +25 -0
  3. package/dist/cfb-writer.js +314 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +357 -0
  6. package/dist/contained.d.ts +46 -0
  7. package/dist/contained.js +152 -0
  8. package/dist/context.d.ts +107 -12
  9. package/dist/context.js +101 -24
  10. package/dist/detect.d.ts +13 -0
  11. package/dist/detect.js +83 -0
  12. package/dist/engine.d.ts +66 -1
  13. package/dist/engine.js +290 -35
  14. package/dist/errors.d.ts +17 -0
  15. package/dist/errors.js +27 -1
  16. package/dist/fields.d.ts +20 -13
  17. package/dist/fields.js +65 -23
  18. package/dist/findings.d.ts +2 -1
  19. package/dist/findings.js +17 -1
  20. package/dist/flat.d.ts +33 -0
  21. package/dist/flat.js +555 -0
  22. package/dist/formats.d.ts +9 -0
  23. package/dist/formats.js +7 -1
  24. package/dist/formulas.d.ts +27 -0
  25. package/dist/formulas.js +157 -1
  26. package/dist/index.d.ts +4 -2
  27. package/dist/index.js +11 -1
  28. package/dist/index.mjs +1 -1
  29. package/dist/io.d.ts +62 -1
  30. package/dist/io.js +245 -1
  31. package/dist/links.d.ts +27 -0
  32. package/dist/links.js +106 -1
  33. package/dist/msg.d.ts +3 -2
  34. package/dist/msg.js +172 -18
  35. package/dist/objects.d.ts +8 -5
  36. package/dist/objects.js +90 -20
  37. package/dist/odf.d.ts +87 -1
  38. package/dist/odf.js +204 -76
  39. package/dist/offcrypto.d.ts +24 -1
  40. package/dist/offcrypto.js +308 -69
  41. package/dist/ole.d.ts +19 -2
  42. package/dist/ole.js +98 -58
  43. package/dist/oleobj.d.ts +11 -0
  44. package/dist/oleobj.js +20 -3
  45. package/dist/ooxml.d.ts +108 -2
  46. package/dist/ooxml.js +392 -109
  47. package/dist/package.d.ts +14 -0
  48. package/dist/package.js +23 -7
  49. package/dist/plan.d.ts +104 -0
  50. package/dist/plan.js +73 -0
  51. package/dist/plugins.d.ts +8 -0
  52. package/dist/plugins.js +35 -0
  53. package/dist/ppt.js +45 -5
  54. package/dist/rewrite-cfb.d.ts +14 -0
  55. package/dist/rewrite-cfb.js +52 -0
  56. package/dist/rewrite-rtf.d.ts +9 -0
  57. package/dist/rewrite-rtf.js +45 -0
  58. package/dist/rewrite-xml.d.ts +36 -0
  59. package/dist/rewrite-xml.js +341 -0
  60. package/dist/rewrite-zip.d.ts +8 -0
  61. package/dist/rewrite-zip.js +498 -0
  62. package/dist/rtf.js +216 -42
  63. package/dist/rtfcomp.d.ts +7 -0
  64. package/dist/rtfcomp.js +80 -0
  65. package/dist/score.js +1 -1
  66. package/dist/sniff.d.ts +5 -0
  67. package/dist/sniff.js +38 -0
  68. package/dist/types.d.ts +66 -6
  69. package/dist/types.js +14 -0
  70. package/dist/vsd.d.ts +13 -0
  71. package/dist/vsd.js +689 -0
  72. package/dist/word.d.ts +4 -4
  73. package/dist/word.js +70 -23
  74. package/dist/xls.d.ts +10 -4
  75. package/dist/xls.js +58 -10
  76. package/dist/xml.d.ts +30 -0
  77. package/dist/xml.js +45 -23
  78. package/dist/zip-writer.d.ts +4 -0
  79. package/dist/zip-writer.js +20 -7
  80. package/dist/zip.d.ts +11 -2
  81. package/dist/zip.js +67 -6
  82. package/package.json +4 -1
package/dist/package.d.ts CHANGED
@@ -1,3 +1,17 @@
1
1
  import type { ZipContext } from './context';
2
+ import type { Format } from './formats';
3
+ import type { ZipArchive, ZipEntry } from './zip';
4
+ /**
5
+ * The package a ZIP archive's markers name: OOXML by [Content_Types].xml, OpenDocument by a mimetype entry or a
6
+ * manifest. Throws for an archive with both or neither.
7
+ */
8
+ export declare function packageKind(zip: ZipArchive): {
9
+ kind: 'ooxml';
10
+ contentTypes: ZipEntry;
11
+ } | {
12
+ kind: 'odf';
13
+ };
2
14
  /** Inspects an OOXML or OpenDocument package, which its markers name. */
3
15
  export declare function inspectPackage(ctx: ZipContext): Promise<void>;
16
+ /** Names an OOXML or OpenDocument package's format as its inspection does, without the rules. */
17
+ export declare function namePackage(ctx: ZipContext): Promise<Format>;
package/dist/package.js CHANGED
@@ -1,21 +1,37 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.packageKind = packageKind;
3
4
  exports.inspectPackage = inspectPackage;
5
+ exports.namePackage = namePackage;
4
6
  const errors_1 = require("./errors");
5
7
  const odf_1 = require("./odf");
6
8
  const ooxml_1 = require("./ooxml");
7
9
  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;
10
+ /**
11
+ * The package a ZIP archive's markers name: OOXML by [Content_Types].xml, OpenDocument by a mimetype entry or a
12
+ * manifest. Throws for an archive with both or neither.
13
+ */
14
+ function packageKind(zip) {
11
15
  const contentTypes = zip.get('[Content_Types].xml');
12
16
  const odf = zip.entries.some(e => e.name === 'mimetype') || zip.get('META-INF/manifest.xml') !== undefined;
13
17
  if (contentTypes && odf)
14
18
  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
19
  if (contentTypes)
16
- await (0, ooxml_1.inspectOoxml)(ctx, contentTypes);
17
- else if (odf)
18
- await (0, odf_1.inspectOdf)(ctx);
20
+ return { kind: 'ooxml', contentTypes };
21
+ if (odf)
22
+ return { kind: 'odf' };
23
+ 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' });
24
+ }
25
+ /** Inspects an OOXML or OpenDocument package, which its markers name. */
26
+ async function inspectPackage(ctx) {
27
+ const k = packageKind(ctx.zip);
28
+ if (k.kind === 'ooxml')
29
+ await (0, ooxml_1.inspectOoxml)(ctx, k.contentTypes);
19
30
  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' });
31
+ await (0, odf_1.inspectOdf)(ctx);
32
+ }
33
+ /** Names an OOXML or OpenDocument package's format as its inspection does, without the rules. */
34
+ async function namePackage(ctx) {
35
+ const k = packageKind(ctx.zip);
36
+ return k.kind === 'ooxml' ? (0, ooxml_1.nameOoxml)(ctx, k.contentTypes) : (await (0, odf_1.nameOdf)(ctx)).format;
21
37
  }
package/dist/plan.d.ts ADDED
@@ -0,0 +1,104 @@
1
+ import type { CfbEntry, CompoundFile } from './cfb';
2
+ import type { SpillSink } from './io';
3
+ import type { OoxmlGraph } from './ooxml';
4
+ import type { OfficeFinding } from './types';
5
+ import type { ZipEntry } from './zip';
6
+ /** What the rewrite does to one element of an XML part. */
7
+ export interface ElementEdit {
8
+ /** The element goes, with everything inside it. */
9
+ remove?: boolean;
10
+ /** Its tags go and its content stays, as with a hyperlink around text. */
11
+ unwrap?: boolean;
12
+ /** Attributes left out, each as its namespace URI, a space and its local name. */
13
+ strip?: Set<string>;
14
+ /** Attribute values replaced, by qualified name. */
15
+ set?: Map<string, string>;
16
+ /** The element's content replaced with this text, as with base64 data a plugin rewrote. */
17
+ content?: string;
18
+ }
19
+ /** The key `ElementEdit.strip` takes for an attribute. */
20
+ export declare const attributeKey: (ns: string, local: string) => string;
21
+ /** A Word field the rewrite removes, by the element numbers of its marks. Its result stays. */
22
+ export interface FieldMarks {
23
+ begin: number;
24
+ separate?: number;
25
+ end?: number;
26
+ /** A w:fldSimple, whose tags go and whose result runs stay. */
27
+ simple: boolean;
28
+ }
29
+ /** What the rewrite does inside one XML part. */
30
+ export interface PartPlan {
31
+ elements: Map<number, ElementEdit>;
32
+ fields: FieldMarks[];
33
+ /** The part has an internal subset that goes. Its entity references are expanded and its defaults written out. */
34
+ subset: boolean;
35
+ /** References to external entities in the part go. */
36
+ external: boolean;
37
+ /** A document type declaration without an internal subset goes. */
38
+ doctype: boolean;
39
+ /** The part refers to an external entity. */
40
+ hasExternal: boolean;
41
+ }
42
+ /** An OpenDocument object element whose fate waits for the plugins' verdict on the entry it names. */
43
+ export interface DeferredObject {
44
+ part: string;
45
+ index: number;
46
+ /** The key of the entry it refers to. */
47
+ target: string;
48
+ finding?: OfficeFinding;
49
+ strips: boolean;
50
+ }
51
+ /** Everything a strip finding names in a ZIP package, for the rewrite. */
52
+ export declare class PackagePlan {
53
+ readonly parts: Map<string, PartPlan>;
54
+ /** Entries that go with every reference to them, by nameKey. */
55
+ readonly drop: Set<string>;
56
+ /** OpenDocument folders that go whole, as nameKey prefixes ending in "/". */
57
+ readonly folders: Set<string>;
58
+ /** Relationship Ids that go, by the name of their relationships part. */
59
+ readonly rels: Map<string, Set<string>>;
60
+ /** Excel 4.0 macro sheets that become empty worksheets, by nameKey. */
61
+ readonly sheets: Set<string>;
62
+ /** Entries whose bytes a plugin replaced, by nameKey. */
63
+ readonly replace: Map<string, SpillSink>;
64
+ /** OpenDocument object references, decided once the plugins have seen the entries they name. */
65
+ readonly objects: DeferredObject[];
66
+ /** What the OOXML rules read of the package's parts and relationships. */
67
+ graph?: OoxmlGraph;
68
+ /** What the OpenDocument rules read of the package's layout. */
69
+ odf?: OdfLayout;
70
+ part(name: string): PartPlan;
71
+ /** Adds an edit to an element. Removing it covers every other edit. */
72
+ element(part: string, index: number, edit: ElementEdit): void;
73
+ dropRelationship(relsPart: string, id: string): void;
74
+ }
75
+ /** The entries of a ZIP package the rewrite needs besides the plan: what the format rules found. */
76
+ export interface OdfLayout {
77
+ mimetype?: ZipEntry;
78
+ manifest?: ZipEntry;
79
+ /** The media type the package declares, which the output's mimetype entry holds. */
80
+ mediaType: string;
81
+ }
82
+ /** Everything a strip finding names in a compound file, for the rewrite. */
83
+ export declare class CompoundPlan {
84
+ /** Streams and storages that go, each with everything under it. */
85
+ readonly drop: Set<CfbEntry>;
86
+ /** Streams whose content a plugin replaced. */
87
+ readonly replace: Map<CfbEntry, SpillSink>;
88
+ /** Storages whose content a plugin replaced with a compound file of its own, read from what it wrote. */
89
+ readonly grafts: Map<CfbEntry, CompoundFile>;
90
+ }
91
+ /** A range of an RTF file the rewrite leaves out, or replaces with `text` and then the bytes of the file at `copy`. */
92
+ export interface RtfCut {
93
+ start: number;
94
+ end: number;
95
+ text?: Uint8Array;
96
+ copy?: [number, number];
97
+ }
98
+ /** What the output of a Flat OPC package keeps of the file besides its parts. */
99
+ export interface FlatOpcLayout {
100
+ /** The prolog's mso-application processing instructions, which name the application that opens the file. */
101
+ instructions: string[];
102
+ /** Each part's attributes besides its name and content type, such as pkg:compression, by key. */
103
+ extra: Map<string, Array<[string, string]>>;
104
+ }
package/dist/plan.js ADDED
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CompoundPlan = exports.PackagePlan = exports.attributeKey = void 0;
4
+ /** The key `ElementEdit.strip` takes for an attribute. */
5
+ const attributeKey = (ns, local) => `${ns} ${local}`;
6
+ exports.attributeKey = attributeKey;
7
+ /** Everything a strip finding names in a ZIP package, for the rewrite. */
8
+ class PackagePlan {
9
+ parts = new Map();
10
+ /** Entries that go with every reference to them, by nameKey. */
11
+ drop = new Set();
12
+ /** OpenDocument folders that go whole, as nameKey prefixes ending in "/". */
13
+ folders = new Set();
14
+ /** Relationship Ids that go, by the name of their relationships part. */
15
+ rels = new Map();
16
+ /** Excel 4.0 macro sheets that become empty worksheets, by nameKey. */
17
+ sheets = new Set();
18
+ /** Entries whose bytes a plugin replaced, by nameKey. */
19
+ replace = new Map();
20
+ /** OpenDocument object references, decided once the plugins have seen the entries they name. */
21
+ objects = [];
22
+ /** What the OOXML rules read of the package's parts and relationships. */
23
+ graph;
24
+ /** What the OpenDocument rules read of the package's layout. */
25
+ odf;
26
+ part(name) {
27
+ let p = this.parts.get(name);
28
+ if (!p) {
29
+ p = { elements: new Map(), fields: [], subset: false, external: false, doctype: false, hasExternal: false };
30
+ this.parts.set(name, p);
31
+ }
32
+ return p;
33
+ }
34
+ /** Adds an edit to an element. Removing it covers every other edit. */
35
+ element(part, index, edit) {
36
+ const elements = this.part(part).elements;
37
+ const had = elements.get(index);
38
+ if (!had) {
39
+ elements.set(index, edit);
40
+ return;
41
+ }
42
+ if (had.remove || edit.remove) {
43
+ elements.set(index, { remove: true });
44
+ return;
45
+ }
46
+ had.unwrap ||= edit.unwrap;
47
+ if (edit.strip)
48
+ had.strip = new Set([...(had.strip ?? []), ...edit.strip]);
49
+ if (edit.set)
50
+ had.set = new Map([...(had.set ?? []), ...edit.set]);
51
+ if (edit.content !== undefined)
52
+ had.content = edit.content;
53
+ }
54
+ dropRelationship(relsPart, id) {
55
+ let ids = this.rels.get(relsPart);
56
+ if (!ids) {
57
+ ids = new Set();
58
+ this.rels.set(relsPart, ids);
59
+ }
60
+ ids.add(id);
61
+ }
62
+ }
63
+ exports.PackagePlan = PackagePlan;
64
+ /** Everything a strip finding names in a compound file, for the rewrite. */
65
+ class CompoundPlan {
66
+ /** Streams and storages that go, each with everything under it. */
67
+ drop = new Set();
68
+ /** Streams whose content a plugin replaced. */
69
+ replace = new Map();
70
+ /** Storages whose content a plugin replaced with a compound file of its own, read from what it wrote. */
71
+ grafts = new Map();
72
+ }
73
+ exports.CompoundPlan = CompoundPlan;
@@ -0,0 +1,8 @@
1
+ import type { ContainedFilePlugin, OfficeOptions } from './types';
2
+ /**
3
+ * Defuses Office and OpenDocument files inside other files, with `options` when given. Without them, it takes the
4
+ * options of the office-defuse run that found the file, or none when another package found it. `limits.nestingDepth`
5
+ * applies to the depth in the context. The file's findings go into the containing file's report, and its output's
6
+ * findings into the output's report, each with its `weight`.
7
+ */
8
+ export declare function officePlugin(options?: OfficeOptions): ContainedFilePlugin;
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.officePlugin = officePlugin;
4
+ const contained_1 = require("./contained");
5
+ const detect_1 = require("./detect");
6
+ const engine_1 = require("./engine");
7
+ const score_1 = require("./score");
8
+ /**
9
+ * Defuses Office and OpenDocument files inside other files, with `options` when given. Without them, it takes the
10
+ * options of the office-defuse run that found the file, or none when another package found it. `limits.nestingDepth`
11
+ * applies to the depth in the context. The file's findings go into the containing file's report, and its output's
12
+ * findings into the output's report, each with its `weight`.
13
+ */
14
+ function officePlugin(options) {
15
+ return {
16
+ kind: 'file',
17
+ name: 'office',
18
+ // It takes what detectOffice names: the content decides, whatever the name says.
19
+ accepts: async (f) => (await (0, detect_1.detectOfficeSource)(f.source, { name: f.name, declaredType: f.declaredType })) !== undefined,
20
+ async process(file, sink, context) {
21
+ const run = options ?? context[contained_1.RUN_OPTIONS] ?? {};
22
+ const r = await (0, engine_1.disarmAtDepth)(file.source, sink, run, context.depth, context.deadline);
23
+ // A container in another package scores these by the weights this run used.
24
+ for (const f of [...r.before.findings, ...(r.after?.findings ?? [])])
25
+ f.weight = (0, score_1.weightOf)(f, run);
26
+ const findings = r.before.findings;
27
+ // The sink holds the rewritten file, also when nothing was removed.
28
+ if (r.status === 'clean')
29
+ return { result: 'passed', findings, outputFindings: r.after?.findings };
30
+ if (r.status === 'defused')
31
+ return { result: 'scrubbed', findings, outputFindings: r.after?.findings };
32
+ return { result: 'removed', findings };
33
+ },
34
+ };
35
+ }
package/dist/ppt.js CHANGED
@@ -36,11 +36,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.powerPointRules = powerPointRules;
37
37
  const zlib = __importStar(require("node:zlib"));
38
38
  const cfb_1 = require("./cfb");
39
+ const contained_1 = require("./contained");
39
40
  const context_1 = require("./context");
40
41
  const errors_1 = require("./errors");
41
42
  const io_1 = require("./io");
42
43
  const links_1 = require("./links");
43
44
  const objects_1 = require("./objects");
45
+ const offcrypto_1 = require("./offcrypto");
44
46
  const oleobj_1 = require("./oleobj");
45
47
  const types_1 = require("./types");
46
48
  /** Record types of the PowerPoint binary format (MS-PPT 2.13.24). */
@@ -56,6 +58,7 @@ const T = {
56
58
  cString: 0x0fba,
57
59
  exHyperlink: 0x0fd7,
58
60
  exHyperlinkAtom: 0x0fd3,
61
+ cryptSession: 0x2f14,
59
62
  };
60
63
  /** ExVideoContainer, ExMIDIAudioContainer and ExWAVAudioLinkContainer, whose path names a file outside the presentation. */
61
64
  const MEDIA_LINKS = new Set([0x1005, 0x100d, 0x1010]);
@@ -71,16 +74,18 @@ const header = (b) => ({ version: b[0] & 0x0f, instance: (b[0] | (b[1] << 8)) >>
71
74
  */
72
75
  async function powerPointRules(ctx, cfb, stream) {
73
76
  const location = stream.name;
74
- const source = cfb.source(stream);
75
- const reader = new io_1.Reader(source, stream.size, 16384, 8);
77
+ let source = cfb.source(stream);
78
+ let reader = new io_1.Reader(source, stream.size, 16384, 8);
76
79
  const persist = new Map();
77
80
  const current = cfb.child(cfb.root, 'Current User');
78
81
  if (current?.kind === 'stream') {
79
82
  const cu = await cfb.source(current).read(0, 24);
80
83
  if (cu.length >= 20 && header(cu).type === T.currentUserAtom) {
81
- if (u32(cu, 12) === ENCRYPTED_TOKEN)
82
- throw new errors_1.StopError(types_1.OfficeCategory.Encrypted, types_1.OfficeDetail.PasswordRequired, 'The presentation is encrypted', { stream: location });
83
84
  await readPersist(reader, stream.size, u32(cu, 16), persist, ctx.settings.deadline);
85
+ if (u32(cu, 12) === ENCRYPTED_TOKEN) {
86
+ source = await decryptPresentation(ctx, reader, stream, u32(cu, 16), persist);
87
+ reader = new io_1.Reader(source, stream.size, 16384, 8);
88
+ }
84
89
  }
85
90
  }
86
91
  const storages = new Map();
@@ -128,6 +133,37 @@ async function powerPointRules(ctx, cfb, stream) {
128
133
  await storageRules(ctx, source, s, role, `${location}, offset ${s.offset}`);
129
134
  }
130
135
  }
136
+ /**
137
+ * The PowerPoint Document stream of an encrypted presentation, decrypted (MS-PPT 2.3.7). The current edit names the
138
+ * persist object that holds the CryptSession10Container, whose data is an RC4 CryptoAPI EncryptionHeader. Each other
139
+ * persist object, its record header included, is RC4 with the key of block number its persist id. The edits and
140
+ * persist directories were never encrypted.
141
+ */
142
+ async function decryptPresentation(ctx, reader, stream, edit, persist) {
143
+ const required = (reason) => new errors_1.StopError(types_1.OfficeCategory.Encrypted, types_1.OfficeDetail.PasswordRequired, `The presentation is encrypted: ${reason}`, { stream: stream.name, reason });
144
+ const u = await reader.read(edit, 40);
145
+ const sessionAt = u.length >= 40 && header(u).type === T.userEditAtom ? persist.get(u32(u, 36)) : undefined;
146
+ if (sessionAt === undefined || sessionAt + 8 > stream.size)
147
+ throw required('the current edit names no CryptSession10Container');
148
+ const h = header(await reader.read(sessionAt, 8));
149
+ if (h.type !== T.cryptSession)
150
+ throw required('the current edit names no CryptSession10Container');
151
+ const keyOf = (0, offcrypto_1.unlockPresentation)(ctx, await reader.read(sessionAt + 8, Math.min(h.length, 65536)), stream.name);
152
+ if (stream.size > ctx.settings.decompressedBytes)
153
+ throw new errors_1.StopError(types_1.OfficeCategory.Limit, types_1.OfficeDetail.DecompressedSize, `The encrypted stream ${stream.name} is ${stream.size} bytes`, { name: stream.name, size: stream.size, limit: ctx.settings.decompressedBytes });
154
+ const copy = Buffer.from(await reader.read(0, stream.size));
155
+ for (const [id, offset] of persist) {
156
+ (0, errors_1.checkTime)(ctx.settings.deadline);
157
+ if (offset === sessionAt || offset + 8 > copy.length)
158
+ continue;
159
+ const rc4 = keyOf(id);
160
+ const head = rc4.crypt(copy.subarray(offset, offset + 8));
161
+ const length = Math.min(copy.length - offset - 8, u32(head, 4));
162
+ copy.set(head, offset);
163
+ copy.set(rc4.crypt(copy.subarray(offset + 8, offset + 8 + length)), offset + 8);
164
+ }
165
+ return (0, io_1.bufferSource)(copy);
166
+ }
131
167
  /** Reads the persist directories of the current edit and every edit before it. Newer entries win. */
132
168
  async function readPersist(reader, size, start, persist, deadline) {
133
169
  const seen = new Set();
@@ -193,7 +229,11 @@ async function storageRules(ctx, source, s, role, location) {
193
229
  (0, objects_1.nestedNotes)(ctx, cfb, location);
194
230
  if (role === 'control')
195
231
  ctx.report(types_1.OfficeCategory.Control, types_1.OfficeDetail.ActiveX, location, { storage: location, ...(cfb.root.clsid === '00000000-0000-0000-0000-000000000000' ? {} : { clsid: cfb.root.clsid }) });
196
- await (0, objects_1.compoundRules)(ctx, cfb, location, role === 'object');
232
+ // An object's storage goes to the plugins as the compound file it is. The rewrite cannot change the presentation's
233
+ // records, so only a plugin that passes the object keeps it in the output.
234
+ const size = await inner.size();
235
+ const decision = role === 'object' ? await (0, contained_1.decideContained)(ctx, { location, chunks: () => (0, io_1.chunksOf)(inner, size) }) : undefined;
236
+ await (0, objects_1.compoundRules)(ctx, cfb, location, role === 'object', undefined, decision);
197
237
  }
198
238
  function unreadable(ctx, role, location, reason) {
199
239
  if (role === 'vba')
@@ -0,0 +1,14 @@
1
+ import type { CfbEntry, CompoundFile } from './cfb';
2
+ import { type CfbTree } from './cfb-writer';
3
+ import type { RunState } from './context';
4
+ import { CompoundPlan } from './plan';
5
+ import type { ByteSink } from './types';
6
+ /**
7
+ * Writes a compound file without the streams and storages the plan drops. A stream a plugin replaced takes the bytes
8
+ * the plugin wrote, and an object's storage a plugin replaced takes the storages and streams of the compound file the
9
+ * plugin wrote. Every other stream is copied as it is. Sectors no stream holds, the rest of each stream's last sector
10
+ * and anything after the last sector are not written.
11
+ */
12
+ export declare function writeCompound(run: RunState, cfb: CompoundFile, plan: CompoundPlan, sink: ByteSink): Promise<void>;
13
+ /** A storage and everything under it, as the root of a compound file of its own. */
14
+ export declare function subtree(cfb: CompoundFile, storage: CfbEntry): CfbTree;
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.writeCompound = writeCompound;
4
+ exports.subtree = subtree;
5
+ const cfb_writer_1 = require("./cfb-writer");
6
+ const plan_1 = require("./plan");
7
+ /** The nodes of a storage's subtree, without what the plan drops, with what plugins returned in place of the rest. */
8
+ function nodes(cfb, storage, plan, opened) {
9
+ const out = [];
10
+ for (const c of storage.children) {
11
+ if (plan.drop.has(c))
12
+ continue;
13
+ const replaced = plan.replace.get(c);
14
+ if (c.kind === 'stream') {
15
+ if (!replaced) {
16
+ out.push({ kind: 'stream', name: c.name, size: c.size, source: cfb.source(c) });
17
+ continue;
18
+ }
19
+ const source = replaced.source();
20
+ opened.push(source);
21
+ out.push({ kind: 'stream', name: c.name, size: replaced.length, source });
22
+ continue;
23
+ }
24
+ const graft = plan.grafts.get(c);
25
+ if (graft)
26
+ out.push({ kind: 'storage', name: c.name, clsid: graft.root.clsid === '00000000-0000-0000-0000-000000000000' ? '' : graft.root.clsid, children: nodes(graft, graft.root, plan, opened) });
27
+ else
28
+ out.push({ kind: 'storage', name: c.name, clsid: c.clsid === '00000000-0000-0000-0000-000000000000' ? '' : c.clsid, children: nodes(cfb, c, plan, opened) });
29
+ }
30
+ return out;
31
+ }
32
+ /**
33
+ * Writes a compound file without the streams and storages the plan drops. A stream a plugin replaced takes the bytes
34
+ * the plugin wrote, and an object's storage a plugin replaced takes the storages and streams of the compound file the
35
+ * plugin wrote. Every other stream is copied as it is. Sectors no stream holds, the rest of each stream's last sector
36
+ * and anything after the last sector are not written.
37
+ */
38
+ async function writeCompound(run, cfb, plan, sink) {
39
+ const clsid = cfb.root.clsid === '00000000-0000-0000-0000-000000000000' ? '' : cfb.root.clsid;
40
+ const opened = [];
41
+ try {
42
+ await (0, cfb_writer_1.writeCfb)({ clsid, children: nodes(cfb, cfb.root, plan, opened) }, sink, run.deadline);
43
+ }
44
+ finally {
45
+ for (const s of opened)
46
+ await s.close?.();
47
+ }
48
+ }
49
+ /** A storage and everything under it, as the root of a compound file of its own. */
50
+ function subtree(cfb, storage) {
51
+ return { clsid: storage.clsid === '00000000-0000-0000-0000-000000000000' ? '' : storage.clsid, children: nodes(cfb, storage, new plan_1.CompoundPlan(), []) };
52
+ }
@@ -0,0 +1,9 @@
1
+ import type { RunState } from './context';
2
+ import type { RtfCut } from './plan';
3
+ import type { ByteSink, ByteSource } from './types';
4
+ /**
5
+ * Writes an RTF file without the ranges the cuts name: the groups of the destinations a strip finding names, and any
6
+ * bytes after the group that closes the document. A cut can put text in place of what it removes, such as an object's
7
+ * result group in place of the object. Everything else keeps its bytes.
8
+ */
9
+ export declare function writeRtf(run: RunState, source: ByteSource, size: number, cuts: RtfCut[], sink: ByteSink): Promise<void>;
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.writeRtf = writeRtf;
4
+ const errors_1 = require("./errors");
5
+ /** Sorts cuts and drops each one that lies inside another, so they apply in one pass. */
6
+ function normalize(cuts) {
7
+ const sorted = [...cuts].sort((a, b) => a.start - b.start || b.end - a.end);
8
+ const out = [];
9
+ let reach = 0;
10
+ for (const c of sorted) {
11
+ if (c.start < reach)
12
+ continue;
13
+ out.push(c);
14
+ reach = Math.max(reach, c.end);
15
+ }
16
+ return out;
17
+ }
18
+ /** Copies the bytes from `from` to `to` of the source. */
19
+ async function copyRange(run, source, from, to, sink) {
20
+ for (let at = from; at < to; at += 262144) {
21
+ (0, errors_1.checkTime)(run.deadline);
22
+ const want = Math.min(262144, to - at);
23
+ const b = await source.read(at, want);
24
+ if (b.length !== want)
25
+ throw Object.assign(new Error(`The source returned ${b.length} bytes at offset ${at} where ${want} were expected`), { code: 'EIO' });
26
+ await sink.write(b);
27
+ }
28
+ }
29
+ /**
30
+ * Writes an RTF file without the ranges the cuts name: the groups of the destinations a strip finding names, and any
31
+ * bytes after the group that closes the document. A cut can put text in place of what it removes, such as an object's
32
+ * result group in place of the object. Everything else keeps its bytes.
33
+ */
34
+ async function writeRtf(run, source, size, cuts, sink) {
35
+ let pos = 0;
36
+ for (const c of normalize(cuts)) {
37
+ await copyRange(run, source, pos, Math.min(c.start, size), sink);
38
+ if (c.text)
39
+ await sink.write(c.text);
40
+ if (c.copy)
41
+ await copyRange(run, source, c.copy[0], Math.min(c.copy[1], size), sink);
42
+ pos = Math.max(pos, c.end);
43
+ }
44
+ await copyRange(run, source, pos, size, sink);
45
+ }
@@ -0,0 +1,36 @@
1
+ import type { ElementEdit, PartPlan } from './plan';
2
+ import { type XmlElement, type XmlLimits } from './xml';
3
+ /** A range of a part's decoded text that the rewrite leaves out, or replaces with `text`. */
4
+ export interface Edit {
5
+ start: number;
6
+ end: number;
7
+ text?: string;
8
+ }
9
+ /** What the rewrite of one part does besides the plan: edits that follow from what was dropped elsewhere. */
10
+ export interface RewriteRules {
11
+ /** The edit an element gets, from the plan or from a reference to something that goes. */
12
+ element(el: XmlElement): ElementEdit | undefined;
13
+ /** Whether an element goes once every element in it went, as a list that would otherwise be left empty. */
14
+ collapses?(el: XmlElement): boolean;
15
+ /** Text to add before the root element's end tag, asked for when it is reached. */
16
+ append?(): string;
17
+ /** Whether an element belongs to WordprocessingML, whose fields the plan names. */
18
+ word?(el: XmlElement): boolean;
19
+ }
20
+ export interface PartRewrite {
21
+ edits: Edit[];
22
+ /** The plan removes the root element, so the part goes instead. */
23
+ rootRemoved: boolean;
24
+ }
25
+ /** A start tag written out from what the tokenizer read, with the edit's attributes left out or changed. */
26
+ export declare function startTag(el: XmlElement, edit?: ElementEdit): string;
27
+ /**
28
+ * Reads a part again with the tokenizer and returns the edits its rewrite makes. The part was read once already, by
29
+ * the same tokenizer with the same limits, so this reading sees the same elements in the same order.
30
+ */
31
+ export declare function partEdits(chunks: AsyncIterable<Uint8Array>, part: string, limits: XmlLimits, vml: boolean, plan: PartPlan | undefined, rules: RewriteRules): Promise<PartRewrite>;
32
+ /**
33
+ * Applies edits to a part as its bytes arrive, and yields the result in the part's encoding, its byte order mark
34
+ * included. Text the edits do not touch keeps its characters, so it keeps its bytes.
35
+ */
36
+ export declare function splice(chunks: AsyncIterable<Uint8Array>, edits: Edit[]): AsyncGenerator<Uint8Array>;