@pptx-studio/writer 0.1.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.
- package/CHANGELOG.md +24 -0
- package/LICENSE +202 -0
- package/NOTICE +43 -0
- package/README.md +184 -0
- package/dist/index.d.ts +818 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1165 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,818 @@
|
|
|
1
|
+
import { PartStore, ReadZipOptions, ZipArchive } from "@pptx-studio/opc";
|
|
2
|
+
import { Report, RuleId } from "@pptx-studio/validate";
|
|
3
|
+
import { XmlParseLimits } from "@pptx-studio/xml";
|
|
4
|
+
//#region src/errors.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* The writer's own failures.
|
|
7
|
+
*
|
|
8
|
+
* There are only three, and the smallness is deliberate. Almost everything that
|
|
9
|
+
* can go wrong on an export already has an owner: `OpcError` for the container
|
|
10
|
+
* (a part with no content type, a relationship pointing at nothing), and
|
|
11
|
+
* `ValidateError` for the twenty-nine rules. Re-wrapping either would throw away
|
|
12
|
+
* the detail a caller needs - `ValidateError` carries the whole `Report` on its
|
|
13
|
+
* `detail` - in exchange for a uniform class name nobody dispatches on.
|
|
14
|
+
*
|
|
15
|
+
* So this file holds what is genuinely the writer's: a hook that threw, a
|
|
16
|
+
* garbage collection that could not be shown to be safe, and the one assertion
|
|
17
|
+
* the writer makes on its own output.
|
|
18
|
+
*/
|
|
19
|
+
declare const WRITER_ERROR_CODES: readonly ["ERR_PREPARE_FAILED", "ERR_COLLECTION_UNSAFE", "ERR_PRESERVATION_BROKEN"];
|
|
20
|
+
type WriterErrorCode = (typeof WRITER_ERROR_CODES)[number];
|
|
21
|
+
interface WriterErrorDetail {
|
|
22
|
+
/** The prepare hook that threw, for `ERR_PREPARE_FAILED`. */
|
|
23
|
+
readonly hook?: string;
|
|
24
|
+
/** The part the failure is about, when it is about one. */
|
|
25
|
+
readonly part?: string;
|
|
26
|
+
/** Relationship parts that would not parse, for `ERR_COLLECTION_UNSAFE`. */
|
|
27
|
+
readonly blockedBy?: readonly string[];
|
|
28
|
+
}
|
|
29
|
+
declare class WriterError extends Error {
|
|
30
|
+
readonly name = "WriterError";
|
|
31
|
+
readonly code: WriterErrorCode;
|
|
32
|
+
readonly detail: WriterErrorDetail;
|
|
33
|
+
constructor(code: WriterErrorCode, message: string, detail?: WriterErrorDetail, options?: {
|
|
34
|
+
cause?: unknown;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
declare function isWriterError(value: unknown): value is WriterError;
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/gc/reachability.d.ts
|
|
40
|
+
/**
|
|
41
|
+
* Which parts anything can still get to, and - the part that matters - whether
|
|
42
|
+
* we are sure.
|
|
43
|
+
*
|
|
44
|
+
* `@pptx-studio/census` has a walk of its own and this is deliberately not it.
|
|
45
|
+
* The two answer the same question with opposite failure modes, and the census
|
|
46
|
+
* says so in its own comment: it swallows a relationship part it cannot parse
|
|
47
|
+
* and carries on, because a census must never refuse. That is exactly right for
|
|
48
|
+
* a report and exactly wrong here. A swallowed parse error means some edges are
|
|
49
|
+
* invisible, invisible edges make their targets look unreferenced, and
|
|
50
|
+
* unreferenced is what this walk is used to justify deleting. The census's
|
|
51
|
+
* worst case is an inaccurate line of output; ours is a user's image gone.
|
|
52
|
+
*
|
|
53
|
+
* So every failure is recorded rather than skipped, and `complete` is false if
|
|
54
|
+
* there was one. `collect.ts` does nothing at all unless it is true.
|
|
55
|
+
*
|
|
56
|
+
* ## Relationship parts are reachable when their source is
|
|
57
|
+
*
|
|
58
|
+
* Nothing ever *relates* to a `.rels` - the spec forbids it and PowerPoint
|
|
59
|
+
* refuses a package that tries, which is one of the assertions in `PartStore`.
|
|
60
|
+
* They are found by naming convention instead. Walking edges alone would
|
|
61
|
+
* therefore report every relationship part in the package as unreachable, which
|
|
62
|
+
* is true of the graph and false of the container. They are marked alongside
|
|
63
|
+
* their source, so that "unreachable" keeps meaning "nothing can find this".
|
|
64
|
+
*/
|
|
65
|
+
interface Reachability {
|
|
66
|
+
/** Normalised names of every part something can still get to, including `.rels` parts. */
|
|
67
|
+
readonly reachable: ReadonlySet<string>;
|
|
68
|
+
/**
|
|
69
|
+
* False when at least one relationship part could not be read.
|
|
70
|
+
*
|
|
71
|
+
* A caller that deletes on the strength of an incomplete walk is deleting on
|
|
72
|
+
* the strength of a parse error.
|
|
73
|
+
*/
|
|
74
|
+
readonly complete: boolean;
|
|
75
|
+
/** The relationship parts that would not parse, and the root if it was unusable. */
|
|
76
|
+
readonly blockedBy: readonly string[];
|
|
77
|
+
}
|
|
78
|
+
/** Every part reachable by following relationships from the package root. */
|
|
79
|
+
declare function reachableParts(store: PartStore): Reachability;
|
|
80
|
+
/**
|
|
81
|
+
* Parts nothing in the package can get to.
|
|
82
|
+
*
|
|
83
|
+
* Relationship parts are excluded by construction rather than by filter - see
|
|
84
|
+
* the header. `[Content_Types].xml` never appears because it is not a part.
|
|
85
|
+
*/
|
|
86
|
+
declare function orphanedParts(store: PartStore, walk: Reachability): string[];
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region src/gc/collect.d.ts
|
|
89
|
+
/**
|
|
90
|
+
* Mark and sweep, for media.
|
|
91
|
+
*
|
|
92
|
+
* ## What it collects, and why so little
|
|
93
|
+
*
|
|
94
|
+
* The sweep set is `/ppt/media/` and nothing else. Not layouts, not masters,
|
|
95
|
+
* not themes, not `ppt/embeddings/*.bin`, whatever the relationship graph says
|
|
96
|
+
* about them. PowerPoint keeps slide layouts that no slide uses - that is how
|
|
97
|
+
* the layout picker has anything to offer - so a package where an unused layout
|
|
98
|
+
* has been tidied away is a package where "change layout" has quietly lost
|
|
99
|
+
* options the user had a moment ago. There is no upside to weigh against that:
|
|
100
|
+
* the layout part is a few kilobytes and the media is the megabytes.
|
|
101
|
+
*
|
|
102
|
+
* ## Where the walk is rooted is the same rule, made twice
|
|
103
|
+
*
|
|
104
|
+
* The obvious implementation asks "which images do the slides use?" and roots
|
|
105
|
+
* the walk at the slide list. It is wrong, and wrong in the way that only shows
|
|
106
|
+
* up weeks later: a layout no slide currently uses still has its background
|
|
107
|
+
* picture, and switching a slide onto that layout has to still work. Rooting at
|
|
108
|
+
* the package root instead gets this right with no special case - layouts hang
|
|
109
|
+
* off the master, the master off `presentation.xml` - so every image a layout
|
|
110
|
+
* or master references is live whether or not a slide has ever used it.
|
|
111
|
+
*
|
|
112
|
+
* ## It cannot break a relationship
|
|
113
|
+
*
|
|
114
|
+
* Worth stating as a property rather than a hope, because it is what makes this
|
|
115
|
+
* safe to run without supervision. A part is collected only when **no**
|
|
116
|
+
* relationship anywhere in the package resolves to it. So there is no edge left
|
|
117
|
+
* to dangle - and the case that kills naive implementations, two slides sharing
|
|
118
|
+
* one image with one of them deleted, cannot arise: the surviving slide's
|
|
119
|
+
* relationship still resolves and still marks the image live.
|
|
120
|
+
*
|
|
121
|
+
* The corollary is a division of labour. This collects nothing until something
|
|
122
|
+
* else has removed the relationship, and removing it is part of deleting a
|
|
123
|
+
* picture, which is a document operation and belongs to Phase 5's command
|
|
124
|
+
* layer rather than to the writer.
|
|
125
|
+
*
|
|
126
|
+
* ## By default it collects only what we orphaned
|
|
127
|
+
*
|
|
128
|
+
* The question `@pptx-studio/validate` asks about findings, asked about parts:
|
|
129
|
+
* not "is this unreferenced" but "did *we* leave it unreferenced". A deck can
|
|
130
|
+
* arrive with an orphaned image - decks that have been through several editors
|
|
131
|
+
* often do - and collecting it would mean that merely opening and saving a file
|
|
132
|
+
* changes it. That breaks the property the whole of Phase 1 exists to
|
|
133
|
+
* establish, and it breaks it in the direction where nobody notices for months.
|
|
134
|
+
*
|
|
135
|
+
* So the default policy differences the reachability of the package we are
|
|
136
|
+
* about to write against the package as it was opened, and collects only what
|
|
137
|
+
* moved from reachable to unreachable. With no baseline there is no "we", and
|
|
138
|
+
* it collects nothing.
|
|
139
|
+
*
|
|
140
|
+
* Measured, for scale: across the fifty-two committed decks there are 1474
|
|
141
|
+
* parts, 24 of them under `/ppt/media/`, and every part of every deck is
|
|
142
|
+
* reachable from the package root. On this corpus the two policies are the same
|
|
143
|
+
* policy. The difference is a promise about the decks we have not seen.
|
|
144
|
+
*/
|
|
145
|
+
type CollectionPolicy =
|
|
146
|
+
/** Only parts this session left unreferenced. The default. */
|
|
147
|
+
'orphaned-here' |
|
|
148
|
+
/** Every unreferenced part in the sweep set, however it got that way. */
|
|
149
|
+
'every-orphan' |
|
|
150
|
+
/** Collect nothing. */
|
|
151
|
+
'none';
|
|
152
|
+
/** `/ppt/media/...` - the default sweep set. */
|
|
153
|
+
declare function isMediaPart(partName: string): boolean;
|
|
154
|
+
interface CollectOptions {
|
|
155
|
+
/** The package about to be written. */
|
|
156
|
+
readonly store: PartStore;
|
|
157
|
+
/** The package as it was opened. Required by `orphaned-here`, ignored otherwise. */
|
|
158
|
+
readonly baseline?: PartStore | null;
|
|
159
|
+
/** Defaults to `orphaned-here`. */
|
|
160
|
+
readonly policy?: CollectionPolicy;
|
|
161
|
+
/**
|
|
162
|
+
* Which parts the sweep may touch at all. Defaults to `isMediaPart`.
|
|
163
|
+
*
|
|
164
|
+
* Sub-phase 8.7 will pass one of its own, to collect a `ppt/fonts/*.fntdata`
|
|
165
|
+
* whose typeface no run uses any more - PowerPoint requires every listed
|
|
166
|
+
* embedded font to be in use. Anything wider than that wants the header above
|
|
167
|
+
* read first.
|
|
168
|
+
*/
|
|
169
|
+
readonly sweepable?: (partName: string) => boolean;
|
|
170
|
+
}
|
|
171
|
+
/** An orphan the sweep declined to collect, and why. */
|
|
172
|
+
interface KeptPart {
|
|
173
|
+
readonly part: string;
|
|
174
|
+
readonly why: string;
|
|
175
|
+
}
|
|
176
|
+
interface CollectionPlan {
|
|
177
|
+
/** Parts removed, in the order they were found. */
|
|
178
|
+
readonly collect: readonly string[];
|
|
179
|
+
/** Orphans left in place, each with a reason. Empty is the normal case. */
|
|
180
|
+
readonly kept: readonly KeptPart[];
|
|
181
|
+
/**
|
|
182
|
+
* False when the relationship graph could not be walked completely.
|
|
183
|
+
*
|
|
184
|
+
* A caller that deletes on the strength of an incomplete walk is deleting on
|
|
185
|
+
* the strength of a parse error.
|
|
186
|
+
*/
|
|
187
|
+
readonly safe: boolean;
|
|
188
|
+
/** Relationship parts that would not parse. Empty when `safe`. */
|
|
189
|
+
readonly blockedBy: readonly string[];
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Plan and apply in one pass.
|
|
193
|
+
*
|
|
194
|
+
* Not split into a pure plan and a separate apply, and the reason is the fixed
|
|
195
|
+
* point below: removing a part can orphan the parts *it* referenced, and the
|
|
196
|
+
* only way to observe that is to remove and look again. A pure planner would
|
|
197
|
+
* have to simulate the store to find the second round, which is a second
|
|
198
|
+
* implementation of the store.
|
|
199
|
+
*
|
|
200
|
+
* Never throws. A package too broken to reason about yields a plan that
|
|
201
|
+
* collects nothing and says why - refusing would make a deck with one
|
|
202
|
+
* unparseable `.rels` permanently unsaveable, and that `.rels` may well have
|
|
203
|
+
* arrived that way. `collectGarbage` is the variant that refuses, for callers
|
|
204
|
+
* who asked for a sweep and would misread silence as "nothing to do".
|
|
205
|
+
*/
|
|
206
|
+
declare function planCollection(options: CollectOptions): CollectionPlan;
|
|
207
|
+
/**
|
|
208
|
+
* Collect, and refuse rather than guess.
|
|
209
|
+
*
|
|
210
|
+
* The entry point for a deliberate clean-up: a user asking for one, or a
|
|
211
|
+
* command that knows it has just orphaned something. It throws when the graph
|
|
212
|
+
* could not be walked completely, because a caller who asked for a sweep and
|
|
213
|
+
* got silence would reasonably conclude there had been nothing to sweep.
|
|
214
|
+
*
|
|
215
|
+
* `exportPackage` deliberately does not use this. An export that failed because
|
|
216
|
+
* some unrelated `.rels` will not parse would be refusing to save a file over a
|
|
217
|
+
* defect it did not cause and cannot fix.
|
|
218
|
+
*/
|
|
219
|
+
declare function collectGarbage(options: CollectOptions): CollectionPlan;
|
|
220
|
+
//#endregion
|
|
221
|
+
//#region src/export/prepare.d.ts
|
|
222
|
+
/**
|
|
223
|
+
* Work that has to happen at save time, contributed by whoever owns it.
|
|
224
|
+
*
|
|
225
|
+
* Several later sub-phases need to touch the package on the way out, and each
|
|
226
|
+
* of them is somebody else's subject:
|
|
227
|
+
*
|
|
228
|
+
* - **8.7, embedded fonts.** Six artifacts that must all be present or
|
|
229
|
+
* PowerPoint reports a problem: the `fntdata` parts, the `fntdata` content-type
|
|
230
|
+
* `Default`, the font relationships on `presentation.xml`, the
|
|
231
|
+
* `p:embeddedFont` entries, their PANOSE and charset attributes, and
|
|
232
|
+
* `@embedTrueTypeFonts`. Plus a collection pass, because PowerPoint requires
|
|
233
|
+
* every listed typeface to actually be used.
|
|
234
|
+
* - **3.4, autofit.** A `txBody` whose text changed needs its computed
|
|
235
|
+
* `@fontScale` and `@lnSpcReduction` written back - and one whose text did
|
|
236
|
+
* not must be left exactly alone, or a view-only deck stops round-tripping.
|
|
237
|
+
* - **10.8, media.** The `p14:media` and `a:videoFile` relationships are a
|
|
238
|
+
* deliberate pair in the file and a duplicate on the way out.
|
|
239
|
+
*
|
|
240
|
+
* None of that is the writer's subject. Written inline it would become a list
|
|
241
|
+
* of special cases in `export.ts` that grows by one every phase, that the
|
|
242
|
+
* writer's own tests would have to know about, and that could not be tested
|
|
243
|
+
* without standing up a font stack or a text engine. As hooks, each lands in
|
|
244
|
+
* the package that owns it, ships with the sub-phase that needs it, and this
|
|
245
|
+
* package keeps knowing nothing about fonts.
|
|
246
|
+
*
|
|
247
|
+
* ## Where they run, and why there
|
|
248
|
+
*
|
|
249
|
+
* Before collection and before validation, both deliberately.
|
|
250
|
+
*
|
|
251
|
+
* Before collection, because a hook is exactly the thing that changes what is
|
|
252
|
+
* referenced - 8.7 both adds font parts and drops the ones nothing uses - and a
|
|
253
|
+
* sweep that ran first would be answering a question about the wrong package.
|
|
254
|
+
*
|
|
255
|
+
* Before validation, because a hook writes markup, and markup this session
|
|
256
|
+
* wrote is precisely what the twenty-nine rules exist to check. A hook that ran
|
|
257
|
+
* after validation would be the one part of an export nothing checked, which is
|
|
258
|
+
* the opposite of how it should be: it is newly synthesized markup, the
|
|
259
|
+
* riskiest kind there is.
|
|
260
|
+
*/
|
|
261
|
+
interface PrepareContext {
|
|
262
|
+
/** The package being exported. Hooks mutate this. */
|
|
263
|
+
readonly store: PartStore;
|
|
264
|
+
/** The package as it was opened, when there is one. Read only; never mutate it. */
|
|
265
|
+
readonly baseline: PartStore | null;
|
|
266
|
+
/**
|
|
267
|
+
* Record something worth reporting.
|
|
268
|
+
*
|
|
269
|
+
* Surfaced on `ExportResult.prepared`. A hook that did nothing should say
|
|
270
|
+
* nothing - the common case for every hook on most exports is silence, and a
|
|
271
|
+
* note per hook per save would bury the one that mattered.
|
|
272
|
+
*/
|
|
273
|
+
note(message: string): void;
|
|
274
|
+
}
|
|
275
|
+
interface PrepareHook {
|
|
276
|
+
/** Short and stable. It appears in `ExportResult` and in the error if this hook throws. */
|
|
277
|
+
readonly name: string;
|
|
278
|
+
run(context: PrepareContext): void;
|
|
279
|
+
}
|
|
280
|
+
interface PrepareRecord {
|
|
281
|
+
readonly hook: string;
|
|
282
|
+
readonly notes: readonly string[];
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Run the hooks in the order given.
|
|
286
|
+
*
|
|
287
|
+
* The order is the caller's, not ours, and it is not inferred from anything.
|
|
288
|
+
* Some pairs genuinely depend on each other - autofit must settle before a font
|
|
289
|
+
* pass decides which typefaces are used - and a writer that sorted hooks by
|
|
290
|
+
* some notion of priority would be guessing at a dependency the caller knows
|
|
291
|
+
* for certain.
|
|
292
|
+
*
|
|
293
|
+
* A hook that throws stops the export. That is the point: the alternative is
|
|
294
|
+
* handing over a package with five of the six font artifacts in it, which
|
|
295
|
+
* PowerPoint reports as a problem with the content and no way to find out
|
|
296
|
+
* which.
|
|
297
|
+
*/
|
|
298
|
+
declare function runPrepare(hooks: readonly PrepareHook[], store: PartStore, baseline: PartStore | null): PrepareRecord[];
|
|
299
|
+
//#endregion
|
|
300
|
+
//#region src/export/preserve.d.ts
|
|
301
|
+
/**
|
|
302
|
+
* The one assertion the writer makes about its own output.
|
|
303
|
+
*
|
|
304
|
+
* A part nobody edited must come out with the same bytes it went in with. That
|
|
305
|
+
* is the first architectural bet stated as a testable sentence, and it is worth
|
|
306
|
+
* a check of its own even though `V027` is nominally about the same thing,
|
|
307
|
+
* because the two are looking at different objects.
|
|
308
|
+
*
|
|
309
|
+
* `V027` asks the **store**: it compares what the store would hand you for a
|
|
310
|
+
* part against what the baseline store would hand you, and for an untouched
|
|
311
|
+
* part both answers come from the same archive. So it can catch a part that was
|
|
312
|
+
* replaced without being marked edited, and it cannot - even in principle -
|
|
313
|
+
* catch a writer that re-serialised a clean part on the way out, because by the
|
|
314
|
+
* time the bytes exist the rule has already run against a store that knows
|
|
315
|
+
* nothing about them.
|
|
316
|
+
*
|
|
317
|
+
* This check reads the archive we actually emitted. It compares the **stored**
|
|
318
|
+
* bytes, still compressed, against the stored bytes of the source archive:
|
|
319
|
+
* same DEFLATE stream, same CRC, same declared sizes. Comparing compressed
|
|
320
|
+
* bytes is both cheaper - nothing is inflated on either side - and stricter,
|
|
321
|
+
* because two different DEFLATE encodings of identical content would pass an
|
|
322
|
+
* inflate-and-compare and fail this. Stricter is what is wanted: any difference
|
|
323
|
+
* at all means something re-compressed, and re-compressing is the failure.
|
|
324
|
+
*
|
|
325
|
+
* It needs the bytes the package was opened from. Without them there is nothing
|
|
326
|
+
* to compare against and the check reports itself as not run, which is not the
|
|
327
|
+
* same as reporting that it passed.
|
|
328
|
+
*/
|
|
329
|
+
interface PreservationCheck {
|
|
330
|
+
/** Entries compared and found identical. */
|
|
331
|
+
readonly checked: number;
|
|
332
|
+
/** Entries deliberately not compared, because this session wrote them. */
|
|
333
|
+
readonly rewritten: number;
|
|
334
|
+
/** Why the check did not run, or `null` when it did. */
|
|
335
|
+
readonly skipped: string | null;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Compare the emitted archive against the one it came from.
|
|
339
|
+
*
|
|
340
|
+
* Throws `ERR_PRESERVATION_BROKEN` on the first difference, naming the part.
|
|
341
|
+
* There is no "report and continue" mode: one part quietly re-serialised is the
|
|
342
|
+
* whole category of failure this package is built to make impossible, and every
|
|
343
|
+
* later one is likely the same cause.
|
|
344
|
+
*/
|
|
345
|
+
declare function assertPreserved(output: Uint8Array, original: Uint8Array | undefined, rewritten: ReadonlySet<string>, zip?: ReadZipOptions): PreservationCheck;
|
|
346
|
+
//#endregion
|
|
347
|
+
//#region src/export/export.d.ts
|
|
348
|
+
/**
|
|
349
|
+
* Handing the package back.
|
|
350
|
+
*
|
|
351
|
+
* Four things happen, in an order that is load-bearing rather than tidy:
|
|
352
|
+
*
|
|
353
|
+
* 1. **Prepare hooks**, so that whatever later phases owe the file is in it.
|
|
354
|
+
* 2. **Collect**, because a hook is the thing most likely to have orphaned
|
|
355
|
+
* something.
|
|
356
|
+
* 3. **Write**, which streams every untouched part still compressed.
|
|
357
|
+
* 4. **Check**, twice: that nothing we did not edit changed, and that the
|
|
358
|
+
* twenty-nine rules pass. Only then do the bytes leave the function.
|
|
359
|
+
*
|
|
360
|
+
* The check comes after the write and not before it, which is the one ordering
|
|
361
|
+
* that might look backwards. It is the right way round because a check that
|
|
362
|
+
* runs first is checking an intention, and what a user opens is an archive. All
|
|
363
|
+
* three of `V003`, the duplicate half of `V002`, and the preservation check
|
|
364
|
+
* below are questions about the emitted container that no amount of inspecting
|
|
365
|
+
* the store can answer. Bytes are produced, then judged, then either returned
|
|
366
|
+
* or thrown away - never returned unjudged.
|
|
367
|
+
*/
|
|
368
|
+
interface ExportOptions {
|
|
369
|
+
/** The package to write, with whatever edits have been made to it. */
|
|
370
|
+
readonly store: PartStore;
|
|
371
|
+
/**
|
|
372
|
+
* The package as it was opened.
|
|
373
|
+
*
|
|
374
|
+
* Optional, and the three things it costs to leave out are worth knowing
|
|
375
|
+
* before deciding to. Without it the three preservation rules are skipped
|
|
376
|
+
* rather than passed; the media sweep collects nothing, because there is no
|
|
377
|
+
* way to tell what this session orphaned; and `origin` is unavailable, so
|
|
378
|
+
* every fatal finding counts as one we introduced. `openPackage` produces a
|
|
379
|
+
* correct one in a line.
|
|
380
|
+
*/
|
|
381
|
+
readonly baseline?: PartStore;
|
|
382
|
+
/** The archive the baseline was opened from. Needed by `V003` and the preservation check. */
|
|
383
|
+
readonly baselineBytes?: Uint8Array;
|
|
384
|
+
/** Run in the order given, before everything else. See `prepare.ts`. */
|
|
385
|
+
readonly prepare?: readonly PrepareHook[];
|
|
386
|
+
/** Media garbage collection. Defaults to `orphaned-here`. See `collect.ts`. */
|
|
387
|
+
readonly collect?: CollectionPolicy;
|
|
388
|
+
/** Which parts the sweep may touch. Defaults to `/ppt/media/`. */
|
|
389
|
+
readonly sweepable?: (partName: string) => boolean;
|
|
390
|
+
/**
|
|
391
|
+
* Set false to write without the firewall.
|
|
392
|
+
*
|
|
393
|
+
* There is no legitimate production use. It exists so that a test can build a
|
|
394
|
+
* deliberately broken package and look at it, and so that `cli bisect` in 1.5
|
|
395
|
+
* can emit the intermediate packages whose whole purpose is to be rejected.
|
|
396
|
+
*/
|
|
397
|
+
readonly validate?: boolean;
|
|
398
|
+
/** A subset of rules to run. Defaults to all twenty-nine. */
|
|
399
|
+
readonly rules?: readonly RuleId[];
|
|
400
|
+
/** Set false to skip comparing the output against the source archive. */
|
|
401
|
+
readonly verifyPreservation?: boolean;
|
|
402
|
+
/** Passed through to `PartStore.write`. */
|
|
403
|
+
readonly normalizeEntryOrder?: boolean;
|
|
404
|
+
/** Passed through to `PartStore.write`. */
|
|
405
|
+
readonly deflateLevel?: number;
|
|
406
|
+
/** Passed to `readZip` wherever this package re-opens an archive. */
|
|
407
|
+
readonly zip?: ReadZipOptions;
|
|
408
|
+
}
|
|
409
|
+
interface ExportResult {
|
|
410
|
+
/** The package. Only ever returned when every check above passed. */
|
|
411
|
+
readonly bytes: Uint8Array;
|
|
412
|
+
/** `null` when validation was turned off. */
|
|
413
|
+
readonly report: Report | null;
|
|
414
|
+
/** Hooks that had something to say. Silent hooks are not listed. */
|
|
415
|
+
readonly prepared: readonly PrepareRecord[];
|
|
416
|
+
readonly collection: CollectionPlan;
|
|
417
|
+
/** Parts serialised afresh. Empty on a no-op export - that is the point. */
|
|
418
|
+
readonly rewritten: readonly string[];
|
|
419
|
+
/** Parts streamed out of the source archive without being decompressed. */
|
|
420
|
+
readonly streamed: number;
|
|
421
|
+
readonly preservation: PreservationCheck;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* A package and the untouched twin every check compares it against.
|
|
425
|
+
*
|
|
426
|
+
* Two independent `PartStore.open` calls over the same bytes. Independent
|
|
427
|
+
* matters: the baseline has to keep answering questions about the file as it
|
|
428
|
+
* arrived no matter what happens to the other one, and sharing anything mutable
|
|
429
|
+
* between them would make it a record of the present rather than the past.
|
|
430
|
+
*
|
|
431
|
+
* Neither call inflates anything but `[Content_Types].xml`, so the second is
|
|
432
|
+
* two central-directory parses and a small XML read. They do carry separate
|
|
433
|
+
* inflation budgets, which means an adversarial archive gets two budgets rather
|
|
434
|
+
* than one; that is the correct trade at the size these budgets are set to, and
|
|
435
|
+
* it is written down here so it is a decision rather than an oversight.
|
|
436
|
+
*/
|
|
437
|
+
interface OpenPackage {
|
|
438
|
+
readonly store: PartStore;
|
|
439
|
+
readonly baseline: PartStore;
|
|
440
|
+
readonly baselineBytes: Uint8Array;
|
|
441
|
+
}
|
|
442
|
+
/** Open a package for editing, with the baseline an export will need. */
|
|
443
|
+
declare function openPackage(bytes: Uint8Array, options?: ReadZipOptions): OpenPackage;
|
|
444
|
+
declare function exportPackage(options: ExportOptions): ExportResult;
|
|
445
|
+
//#endregion
|
|
446
|
+
//#region src/oracle/bisect.d.ts
|
|
447
|
+
/**
|
|
448
|
+
* Narrowing a package PowerPoint refuses down to the change that causes it.
|
|
449
|
+
*
|
|
450
|
+
* ## Why this is the debugger for this project
|
|
451
|
+
*
|
|
452
|
+
* PowerPoint emits no diagnostic log. A file it will not open produces one
|
|
453
|
+
* sentence - "PowerPoint found a problem with content in deck.pptx" - naming no
|
|
454
|
+
* part, no element and no reason, and a file it *repairs* produces a sentence
|
|
455
|
+
* that does not even say what was repaired. `@pptx-studio/validate` covers the
|
|
456
|
+
* twenty-nine failures we know how to describe; this covers the rest, which is
|
|
457
|
+
* the interesting ones. The plan's own caveat: passing the rules is necessary
|
|
458
|
+
* and not sufficient, because PowerPoint rejects some schema-legal markup for
|
|
459
|
+
* reasons nobody has written down.
|
|
460
|
+
*
|
|
461
|
+
* So the only way to find out what a refusal is about is to ask PowerPoint
|
|
462
|
+
* again with less of the change present, and keep asking. That is delta
|
|
463
|
+
* debugging: Zeller and Hildebrandt's `ddmin` over a set of changes
|
|
464
|
+
* (*Simplifying and Isolating Failure-Inducing Input*, IEEE TSE 2002), applied
|
|
465
|
+
* level by level down a tree, which is Misherghi and Su's HDD (*HDD:
|
|
466
|
+
* Hierarchical Delta Debugging*, ICSE 2006).
|
|
467
|
+
*
|
|
468
|
+
* ## This is the isolation problem, not the simplification one
|
|
469
|
+
*
|
|
470
|
+
* The distinction is Zeller's and it decides the whole design. Simplification
|
|
471
|
+
* starts from one failing input and cuts it down; it has to guess what a
|
|
472
|
+
* smaller input looks like, and most of its guesses are not even well formed.
|
|
473
|
+
* Isolation starts from a **passing** configuration and a **failing** one and
|
|
474
|
+
* reduces the *difference* between them.
|
|
475
|
+
*
|
|
476
|
+
* We are always in the second case. A deck that PowerPoint repairs came from a
|
|
477
|
+
* deck that it opened, and we have both. So the atoms here are not elements of
|
|
478
|
+
* the document: they are the **changes** between the two packages, and a
|
|
479
|
+
* configuration is a subset of them applied to the original. That buys three
|
|
480
|
+
* things a simplifier cannot have:
|
|
481
|
+
*
|
|
482
|
+
* - Every configuration is well formed by construction. The splice unit is a
|
|
483
|
+
* whole node replaced by the whole node the other package has in that
|
|
484
|
+
* position, so no configuration can invent unbalanced markup.
|
|
485
|
+
* - The empty configuration is known to pass and the full one to fail, which
|
|
486
|
+
* is exactly `ddmin`'s precondition - and both are *checked* here rather
|
|
487
|
+
* than assumed, because when they do not hold the reason is the most useful
|
|
488
|
+
* thing this command can say.
|
|
489
|
+
* - The answer is "these two changes, out of ninety-one", which is a sentence
|
|
490
|
+
* about your edit rather than about PowerPoint's file format.
|
|
491
|
+
*
|
|
492
|
+
* ## Entries, not parts
|
|
493
|
+
*
|
|
494
|
+
* The delta is taken over **ZIP entries**, not over `PartStore` parts, and the
|
|
495
|
+
* difference is not academic: `[Content_Types].xml` is not a part. It is a
|
|
496
|
+
* package-level stream, `PartStore.partNames` deliberately excludes it, and a
|
|
497
|
+
* missing `<Default Extension="fntdata"/>` in it is the canonical cause of
|
|
498
|
+
* "PowerPoint found a problem with content" - the one this project's own plan
|
|
499
|
+
* cites. A bisector that could only vary parts would be blind to the single
|
|
500
|
+
* best-documented repair prompt there is.
|
|
501
|
+
*
|
|
502
|
+
* Working at the entry level also means nothing has to know what an entry
|
|
503
|
+
* holds. Anything that parses as XML is descended into - parts, `.rels`,
|
|
504
|
+
* `docProps` and the content-type stream alike - and anything that does not is
|
|
505
|
+
* one atom, whether it is a PNG, an OLE2 compound file, or markup too damaged
|
|
506
|
+
* to read. There is no content-type table to keep in step.
|
|
507
|
+
*/
|
|
508
|
+
interface Span {
|
|
509
|
+
readonly start: number;
|
|
510
|
+
readonly end: number;
|
|
511
|
+
}
|
|
512
|
+
type ChangeKind =
|
|
513
|
+
/** The entry exists only in the broken package. */
|
|
514
|
+
'entry-added' |
|
|
515
|
+
/** The entry exists only in the original. */
|
|
516
|
+
'entry-removed' |
|
|
517
|
+
/** A whole entry. Binary, or markup whose two sides do not line up. */
|
|
518
|
+
'entry' |
|
|
519
|
+
/** One element and everything under it. */
|
|
520
|
+
'element' |
|
|
521
|
+
/** One text, CDATA, comment, PI or declaration node. */
|
|
522
|
+
'node' |
|
|
523
|
+
/**
|
|
524
|
+
* A run of children replaced by another run of a different length.
|
|
525
|
+
*
|
|
526
|
+
* What an insertion or a deletion looks like. `was` empty means children were
|
|
527
|
+
* added; `text` empty means they were taken away.
|
|
528
|
+
*/
|
|
529
|
+
'children' |
|
|
530
|
+
/** One element's start tag: its name, its attributes and their spacing. */
|
|
531
|
+
'tag' |
|
|
532
|
+
/** One attribute, including the whitespace in front of it. */
|
|
533
|
+
'attribute';
|
|
534
|
+
interface Change {
|
|
535
|
+
readonly id: number;
|
|
536
|
+
/** ZIP entry name, without a leading slash: `ppt/slides/slide1.xml`. */
|
|
537
|
+
readonly entry: string;
|
|
538
|
+
readonly kind: ChangeKind;
|
|
539
|
+
/**
|
|
540
|
+
* Where in the entry, for a person to read.
|
|
541
|
+
*
|
|
542
|
+
* An XPath-ish path for markup - `/p:sld/p:cSld/p:spTree/p:sp[3]/@name` -
|
|
543
|
+
* built from qualified names exactly as the file writes them, because a path
|
|
544
|
+
* that renamed prefixes could not be pasted back into a search.
|
|
545
|
+
*/
|
|
546
|
+
readonly where: string;
|
|
547
|
+
/** Distance from the entry, so a report can indent. */
|
|
548
|
+
readonly depth: number;
|
|
549
|
+
/** Where this sits in the **original** entry's text. Null for a whole entry. */
|
|
550
|
+
readonly span: Span | null;
|
|
551
|
+
/** What the broken package has there. Null when the entry is only original. */
|
|
552
|
+
readonly text: string | null;
|
|
553
|
+
/** What the original has there. Null when the entry is only in the broken one. */
|
|
554
|
+
readonly was: string | null;
|
|
555
|
+
/**
|
|
556
|
+
* Finer changes that together reproduce this one exactly.
|
|
557
|
+
*
|
|
558
|
+
* The exactness is what makes descending free: the regions between the
|
|
559
|
+
* children are the regions that compared equal, so applying every child is
|
|
560
|
+
* the same package as applying the parent. HDD relies on it to move down a
|
|
561
|
+
* level without spending a run to re-establish that the configuration fails.
|
|
562
|
+
*/
|
|
563
|
+
readonly children: readonly Change[];
|
|
564
|
+
}
|
|
565
|
+
/** Every change in the forest, parents before children. */
|
|
566
|
+
declare function flattenChanges(changes: readonly Change[]): Change[];
|
|
567
|
+
/** The leaves: the finest changes this delta can express. */
|
|
568
|
+
declare function leafChanges(changes: readonly Change[]): Change[];
|
|
569
|
+
/**
|
|
570
|
+
* What the oracle says about one candidate package.
|
|
571
|
+
*
|
|
572
|
+
* Three values rather than two, and the third is Zeller's. `unresolved` is a
|
|
573
|
+
* run that answered neither question - PowerPoint timed out, the harness would
|
|
574
|
+
* not start, a subprocess was killed. Treating it as "passes" would let the
|
|
575
|
+
* reducer discard the change that matters and then blame an innocent one;
|
|
576
|
+
* treating it as "fails" would let it keep everything. It is counted and
|
|
577
|
+
* reported instead, and never reduced towards.
|
|
578
|
+
*/
|
|
579
|
+
type Verdict = 'fails' | 'passes' | 'unresolved';
|
|
580
|
+
/**
|
|
581
|
+
* Does this package still show the problem?
|
|
582
|
+
*
|
|
583
|
+
* Synchronous on purpose. Every oracle we have is either pure computation - the
|
|
584
|
+
* twenty-nine rules, the round-trip comparison - or a subprocess the caller
|
|
585
|
+
* blocks on anyway, and `spawnSync` blocks perfectly well. Making the reducer
|
|
586
|
+
* asynchronous to serve a caller that does not exist would put a Promise in the
|
|
587
|
+
* middle of a browser package for nothing.
|
|
588
|
+
*/
|
|
589
|
+
type Oracle = (bytes: Uint8Array, label: string) => Verdict;
|
|
590
|
+
interface RunEvent {
|
|
591
|
+
readonly run: number;
|
|
592
|
+
readonly applied: number;
|
|
593
|
+
readonly total: number;
|
|
594
|
+
readonly label: string;
|
|
595
|
+
}
|
|
596
|
+
interface BisectOptions {
|
|
597
|
+
readonly oracle: Oracle;
|
|
598
|
+
/**
|
|
599
|
+
* Ceiling on oracle runs.
|
|
600
|
+
*
|
|
601
|
+
* `ddmin` is quadratic in the worst case and the interesting oracle takes
|
|
602
|
+
* about a second and a half, so a run against a large delta can take a long
|
|
603
|
+
* time. When the ceiling is hit the result carries `exhausted: true` and the
|
|
604
|
+
* smallest failing configuration found so far - never a truncated answer
|
|
605
|
+
* presented as a minimal one.
|
|
606
|
+
*/
|
|
607
|
+
readonly maxRuns?: number;
|
|
608
|
+
/** Called before each oracle run, for a progress line. */
|
|
609
|
+
readonly onRun?: (run: RunEvent) => void;
|
|
610
|
+
readonly zip?: ReadZipOptions;
|
|
611
|
+
/**
|
|
612
|
+
* Ceiling on the size of the delta tree.
|
|
613
|
+
*
|
|
614
|
+
* Two packages that share nothing produce a tree the size of the document.
|
|
615
|
+
* Past this, subtrees stop being decomposed and stay atomic, which costs
|
|
616
|
+
* precision and not correctness; `truncated` says when it happened.
|
|
617
|
+
*/
|
|
618
|
+
readonly maxChanges?: number;
|
|
619
|
+
}
|
|
620
|
+
type BisectOutcome =
|
|
621
|
+
/** A minimal failing set was found. */
|
|
622
|
+
'localized' |
|
|
623
|
+
/** The two packages hold the same entries, byte for byte. */
|
|
624
|
+
'identical' |
|
|
625
|
+
/** The broken package passes the oracle. There is nothing to look for. */
|
|
626
|
+
'broken-passes' |
|
|
627
|
+
/** The original fails too, so the cause is not in the difference. */
|
|
628
|
+
'original-fails';
|
|
629
|
+
interface BisectResult {
|
|
630
|
+
readonly outcome: BisectOutcome;
|
|
631
|
+
/** The whole delta, as a forest. One root per differing entry. */
|
|
632
|
+
readonly changes: readonly Change[];
|
|
633
|
+
/** The 1-minimal failing subset: drop any one of these and it passes. */
|
|
634
|
+
readonly minimal: readonly Change[];
|
|
635
|
+
/** The smallest package still showing the problem. Ready to open, or to keep. */
|
|
636
|
+
readonly bytes: Uint8Array;
|
|
637
|
+
/** Oracle runs actually performed. */
|
|
638
|
+
readonly runs: number;
|
|
639
|
+
/** Configurations answered from the cache instead of the oracle. */
|
|
640
|
+
readonly cached: number;
|
|
641
|
+
/** Runs that answered neither question. */
|
|
642
|
+
readonly unresolved: number;
|
|
643
|
+
/** True when `maxRuns` stopped the reduction early. */
|
|
644
|
+
readonly exhausted: boolean;
|
|
645
|
+
/** True when `maxChanges` stopped the delta being decomposed further. */
|
|
646
|
+
readonly truncated: boolean;
|
|
647
|
+
}
|
|
648
|
+
/** The forest of changes between two packages. One root per differing entry. */
|
|
649
|
+
declare function collectDelta(original: ZipArchive, broken: ZipArchive, maxChanges?: number): {
|
|
650
|
+
changes: Change[];
|
|
651
|
+
text: Map<string, string>;
|
|
652
|
+
truncated: boolean;
|
|
653
|
+
};
|
|
654
|
+
declare function bisectPackages(originalBytes: Uint8Array, brokenBytes: Uint8Array, options: BisectOptions): BisectResult;
|
|
655
|
+
/** One line naming a change, without its text. */
|
|
656
|
+
declare function describeChange(change: Change): string;
|
|
657
|
+
/** `2 change(s) in 1 entry(s), from a delta of 91, in 37 oracle run(s)`. */
|
|
658
|
+
declare function summarizeBisect(result: BisectResult): string;
|
|
659
|
+
//#endregion
|
|
660
|
+
//#region src/oracle/roundtrip.d.ts
|
|
661
|
+
/**
|
|
662
|
+
* Is the deck we wrote the deck we read?
|
|
663
|
+
*
|
|
664
|
+
* ## Why this is not a byte comparison
|
|
665
|
+
*
|
|
666
|
+
* The plan is explicit that raw ZIP byte equality is the wrong test and says
|
|
667
|
+
* why in one line: entry order, deflate level, DOS timestamps and attribute
|
|
668
|
+
* order all legitimately differ, so a byte-equality round-trip test is red on
|
|
669
|
+
* day one and disabled on day two. A test nobody trusts is worse than no test,
|
|
670
|
+
* because it also stops anyone writing the one that would have worked.
|
|
671
|
+
*
|
|
672
|
+
* `assertPreserved` in this same package *does* compare bytes, and the two are
|
|
673
|
+
* not in competition. It asks a narrow question - did the entries we said we
|
|
674
|
+
* would not touch come out with the identical stored bytes - and it is the
|
|
675
|
+
* right question for a no-op export. This module asks the wide one: for a
|
|
676
|
+
* package we edited, or one a different producer wrote, do the two archives
|
|
677
|
+
* hold the same document? That question has to survive an entry moving, a rId
|
|
678
|
+
* being renumbered, and a `Default` becoming an `Override`, none of which
|
|
679
|
+
* changes anything a reader can observe.
|
|
680
|
+
*
|
|
681
|
+
* ## Three comparisons, because a package has three kinds of content
|
|
682
|
+
*
|
|
683
|
+
* **XML parts** compare by canonical form - see `canonicalXml` in
|
|
684
|
+
* `@pptx-studio/xml` for what it normalises and, more importantly, what it
|
|
685
|
+
* refuses to. Whitespace and prefixes are meaning, not formatting.
|
|
686
|
+
*
|
|
687
|
+
* **Relationship parts** compare as a graph with the ids treated as opaque
|
|
688
|
+
* labels. Two `.rels` that disagree only about whether the theme is `rId1` or
|
|
689
|
+
* `rId7` describe the same package, and PowerPoint renumbers on every save, so
|
|
690
|
+
* a comparator that could not see past that would be useless the first time it
|
|
691
|
+
* was pointed at a file PowerPoint had written. Ids are matched by what they
|
|
692
|
+
* point at, and the resulting mapping is then applied to the referring markup:
|
|
693
|
+
* an `r:embed="rId3"` on one side and `r:embed="rId7"` on the other are the
|
|
694
|
+
* same reference exactly when rId3 and rId7 resolve to the same part.
|
|
695
|
+
*
|
|
696
|
+
* That relabelling is keyed on the **namespace** of the attribute rather than
|
|
697
|
+
* on a list of names. Every attribute in the relationships namespace is a
|
|
698
|
+
* relationship reference - `r:id`, `r:embed`, `r:link`, `r:pict`, `r:dm`,
|
|
699
|
+
* `r:lo`, `r:qs`, `r:cs` - and a list would be a list that is one entry short
|
|
700
|
+
* the first time we meet a part type nobody thought about.
|
|
701
|
+
*
|
|
702
|
+
* **Everything else** compares by SHA-256. The bytes are in hand on both sides
|
|
703
|
+
* so the digest is not needed to decide equality; it is needed to *report*, and
|
|
704
|
+
* to give `cli bisect` in 1.5 something to name a part by. CRC-32, which this
|
|
705
|
+
* package already has for the archive, is a 32-bit error-detecting code and not
|
|
706
|
+
* evidence that two files are the same.
|
|
707
|
+
*
|
|
708
|
+
* ## What it does not compare, and why that is not a hole
|
|
709
|
+
*
|
|
710
|
+
* `[Content_Types].xml` is never compared as a document. It is a map, and the
|
|
711
|
+
* same map has many spellings: a `Default` for an extension and an `Override`
|
|
712
|
+
* on every part with that extension mean the identical thing, and PowerPoint
|
|
713
|
+
* chooses between them differently from us. What is compared instead is the
|
|
714
|
+
* resolved content type of every part, which is the only thing the map is for.
|
|
715
|
+
*/
|
|
716
|
+
type DifferenceKind =
|
|
717
|
+
/** A part is in the original and not in what was written. */
|
|
718
|
+
'part-removed' |
|
|
719
|
+
/** A part is in what was written and not in the original. */
|
|
720
|
+
'part-added' |
|
|
721
|
+
/** Both have the part; the content types disagree. */
|
|
722
|
+
'content-type' |
|
|
723
|
+
/** Both have the part; the canonical XML disagrees. */
|
|
724
|
+
'xml' |
|
|
725
|
+
/** Both have the part; the bytes disagree. */
|
|
726
|
+
'binary' |
|
|
727
|
+
/** The relationship graphs of one source part disagree. */
|
|
728
|
+
'relationship' |
|
|
729
|
+
/** A part could not be read on one side or the other, so nothing can be said. */
|
|
730
|
+
'unreadable';
|
|
731
|
+
interface Difference {
|
|
732
|
+
readonly kind: DifferenceKind;
|
|
733
|
+
/** The part it is about. For a relationship difference, the *source* part. */
|
|
734
|
+
readonly part: string;
|
|
735
|
+
readonly detail: string;
|
|
736
|
+
}
|
|
737
|
+
type CompareHow = 'xml' | 'binary' | 'relationships';
|
|
738
|
+
interface PartComparison {
|
|
739
|
+
readonly part: string;
|
|
740
|
+
readonly how: CompareHow;
|
|
741
|
+
readonly same: boolean;
|
|
742
|
+
/**
|
|
743
|
+
* The digest of the compared form on the original side: the canonical XML for
|
|
744
|
+
* an XML part, the bytes for anything else, and the relationship graph with
|
|
745
|
+
* the ids left out for a `.rels`.
|
|
746
|
+
*
|
|
747
|
+
* Two parts with the same digest are the same part in the sense this module
|
|
748
|
+
* means. That is the value worth writing into a report.
|
|
749
|
+
*/
|
|
750
|
+
readonly digest: string;
|
|
751
|
+
/** The same digest on the written side. Equal to `digest` whenever `same`. */
|
|
752
|
+
readonly writtenDigest: string;
|
|
753
|
+
}
|
|
754
|
+
/** One relationship id that means the same thing under a different name. */
|
|
755
|
+
interface Relabelling {
|
|
756
|
+
/** The part whose `.rels` this is - `/` for the package root. */
|
|
757
|
+
readonly source: string;
|
|
758
|
+
readonly from: string;
|
|
759
|
+
readonly to: string;
|
|
760
|
+
/** What both ids point at, which is why they are the same label. */
|
|
761
|
+
readonly target: string;
|
|
762
|
+
}
|
|
763
|
+
interface RoundTripReport {
|
|
764
|
+
readonly ok: boolean;
|
|
765
|
+
readonly parts: readonly PartComparison[];
|
|
766
|
+
readonly differences: readonly Difference[];
|
|
767
|
+
/**
|
|
768
|
+
* Ids that moved. Empty for anything this package wrote, and the interesting
|
|
769
|
+
* field when the comparison is against a file PowerPoint saved.
|
|
770
|
+
*/
|
|
771
|
+
readonly relabelled: readonly Relabelling[];
|
|
772
|
+
readonly counts: {
|
|
773
|
+
readonly xml: number;
|
|
774
|
+
readonly binary: number;
|
|
775
|
+
readonly relationships: number;
|
|
776
|
+
readonly same: number;
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
interface CompareOptions {
|
|
780
|
+
/** Passed to `parseXml` for both sides. */
|
|
781
|
+
readonly limits?: XmlParseLimits;
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Compare two packages.
|
|
785
|
+
*
|
|
786
|
+
* Never throws. Every failure - a part that will not parse, a `.rels` that will
|
|
787
|
+
* not read - is a difference with a reason, because the caller of a comparison
|
|
788
|
+
* wants the whole list and not the first item on it.
|
|
789
|
+
*/
|
|
790
|
+
declare function comparePackages(original: PartStore, written: PartStore, options?: CompareOptions): RoundTripReport;
|
|
791
|
+
/** A one-line summary, for a CLI and for a test failure message. */
|
|
792
|
+
declare function summarizeRoundTrip(report: RoundTripReport): string;
|
|
793
|
+
/**
|
|
794
|
+
* Read a package, write it back, and compare the two.
|
|
795
|
+
*
|
|
796
|
+
* The whole of sub-phase 1.4 in one call, and the shape `cli roundtrip` and the
|
|
797
|
+
* corpus gate both want. Every option `exportPackage` takes is accepted, so a
|
|
798
|
+
* caller can round-trip through a different deflate level or a normalised entry
|
|
799
|
+
* order - which is the point of a comparison that is not byte equality, and
|
|
800
|
+
* worth having a test do deliberately.
|
|
801
|
+
*
|
|
802
|
+
* Throws whatever `exportPackage` throws. A package the firewall refuses is not
|
|
803
|
+
* a package with a round-trip difference; it is one that never got written, and
|
|
804
|
+
* flattening the two into one verdict would lose the distinction exactly where
|
|
805
|
+
* it matters.
|
|
806
|
+
*/
|
|
807
|
+
declare function roundTripPackage(bytes: Uint8Array, options?: RoundTripOptions): RoundTripResult;
|
|
808
|
+
interface RoundTripOptions extends Omit<ExportOptions, 'store' | 'baseline' | 'baselineBytes'>, CompareOptions {}
|
|
809
|
+
interface RoundTripResult {
|
|
810
|
+
readonly exported: ExportResult;
|
|
811
|
+
/** The package as re-opened from the bytes that were written. */
|
|
812
|
+
readonly written: PartStore;
|
|
813
|
+
readonly comparison: RoundTripReport;
|
|
814
|
+
readonly ok: boolean;
|
|
815
|
+
}
|
|
816
|
+
//#endregion
|
|
817
|
+
export { type BisectOptions, type BisectOutcome, type BisectResult, type Change, type ChangeKind, type CollectOptions, type CollectionPlan, type CollectionPolicy, type CompareHow, type CompareOptions, type Difference, type DifferenceKind, type ExportOptions, type ExportResult, type KeptPart, type OpenPackage, type Oracle, type PartComparison, type PrepareContext, type PrepareHook, type PrepareRecord, type PreservationCheck, type Reachability, type Relabelling, type RoundTripOptions, type RoundTripReport, type RoundTripResult, type RunEvent, type Span, type Verdict, WRITER_ERROR_CODES, WriterError, type WriterErrorCode, type WriterErrorDetail, assertPreserved, bisectPackages, collectDelta, collectGarbage, comparePackages, describeChange, exportPackage, flattenChanges, isMediaPart, isWriterError, leafChanges, openPackage, orphanedParts, planCollection, reachableParts, roundTripPackage, runPrepare, summarizeBisect, summarizeRoundTrip };
|
|
818
|
+
//# sourceMappingURL=index.d.ts.map
|