@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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/errors.ts","../src/gc/reachability.ts","../src/gc/collect.ts","../src/export/prepare.ts","../src/export/preserve.ts","../src/export/export.ts","../src/oracle/bisect.ts","../src/oracle/roundtrip.ts"],"sourcesContent":["/**\n * The writer's own failures.\n *\n * There are only three, and the smallness is deliberate. Almost everything that\n * can go wrong on an export already has an owner: `OpcError` for the container\n * (a part with no content type, a relationship pointing at nothing), and\n * `ValidateError` for the twenty-nine rules. Re-wrapping either would throw away\n * the detail a caller needs - `ValidateError` carries the whole `Report` on its\n * `detail` - in exchange for a uniform class name nobody dispatches on.\n *\n * So this file holds what is genuinely the writer's: a hook that threw, a\n * garbage collection that could not be shown to be safe, and the one assertion\n * the writer makes on its own output.\n */\n\nexport const WRITER_ERROR_CODES = [\n /** A `prepare` hook threw. The hook's name is in `detail.hook`. */\n 'ERR_PREPARE_FAILED',\n /**\n * The relationship graph could not be walked completely, so no part can be\n * shown to be unreferenced. See `collect.ts` - the sweep refuses rather than\n * guesses, because the guess deletes user data.\n */\n 'ERR_COLLECTION_UNSAFE',\n /**\n * A part nobody edited came out of the writer with different bytes.\n *\n * The one thing this package promises. It is checked against the archive we\n * actually emitted rather than against our intention, which is why it is an\n * error here and not a finding in the report.\n */\n 'ERR_PRESERVATION_BROKEN',\n] as const;\n\nexport type WriterErrorCode = (typeof WRITER_ERROR_CODES)[number];\n\nexport interface WriterErrorDetail {\n /** The prepare hook that threw, for `ERR_PREPARE_FAILED`. */\n readonly hook?: string;\n /** The part the failure is about, when it is about one. */\n readonly part?: string;\n /** Relationship parts that would not parse, for `ERR_COLLECTION_UNSAFE`. */\n readonly blockedBy?: readonly string[];\n}\n\nexport class WriterError extends Error {\n override readonly name = 'WriterError';\n readonly code: WriterErrorCode;\n readonly detail: WriterErrorDetail;\n\n constructor(\n code: WriterErrorCode,\n message: string,\n detail: WriterErrorDetail = {},\n options: { cause?: unknown } = {},\n ) {\n super(message, options);\n this.code = code;\n this.detail = detail;\n }\n}\n\nexport function isWriterError(value: unknown): value is WriterError {\n return value instanceof WriterError;\n}\n","import {\n normalizePartName,\n relsPartNameFor,\n ROOT_RELS_PART,\n type PartStore,\n type Relationships,\n} from '@pptx-studio/opc';\n\n/**\n * Which parts anything can still get to, and - the part that matters - whether\n * we are sure.\n *\n * `@pptx-studio/census` has a walk of its own and this is deliberately not it.\n * The two answer the same question with opposite failure modes, and the census\n * says so in its own comment: it swallows a relationship part it cannot parse\n * and carries on, because a census must never refuse. That is exactly right for\n * a report and exactly wrong here. A swallowed parse error means some edges are\n * invisible, invisible edges make their targets look unreferenced, and\n * unreferenced is what this walk is used to justify deleting. The census's\n * worst case is an inaccurate line of output; ours is a user's image gone.\n *\n * So every failure is recorded rather than skipped, and `complete` is false if\n * there was one. `collect.ts` does nothing at all unless it is true.\n *\n * ## Relationship parts are reachable when their source is\n *\n * Nothing ever *relates* to a `.rels` - the spec forbids it and PowerPoint\n * refuses a package that tries, which is one of the assertions in `PartStore`.\n * They are found by naming convention instead. Walking edges alone would\n * therefore report every relationship part in the package as unreachable, which\n * is true of the graph and false of the container. They are marked alongside\n * their source, so that \"unreachable\" keeps meaning \"nothing can find this\".\n */\n\nexport interface Reachability {\n /** Normalised names of every part something can still get to, including `.rels` parts. */\n readonly reachable: ReadonlySet<string>;\n /**\n * False when at least one relationship part could not be read.\n *\n * A caller that deletes on the strength of an incomplete walk is deleting on\n * the strength of a parse error.\n */\n readonly complete: boolean;\n /** The relationship parts that would not parse, and the root if it was unusable. */\n readonly blockedBy: readonly string[];\n}\n\n/** Every part reachable by following relationships from the package root. */\nexport function reachableParts(store: PartStore): Reachability {\n const reachable = new Set<string>();\n const blockedBy: string[] = [];\n const queue: string[] = [];\n\n const mark = (partName: string): void => {\n const key = normalizePartName(partName);\n if (reachable.has(key)) return;\n reachable.add(key);\n queue.push(partName);\n // Its relationship part comes with it, if it has one.\n const rels = relsPartNameFor(partName);\n if (store.has(rels)) reachable.add(normalizePartName(rels));\n };\n\n const follow = (rels: Relationships): void => {\n for (const rel of rels.all) {\n if (rel.targetMode === 'External') continue;\n let target: string;\n try {\n target = rels.resolve(rel);\n } catch {\n // A target that will not resolve names no part, so it can neither mark\n // one nor hide one. `V008` reports it; there is nothing to do here.\n continue;\n }\n if (store.has(target)) mark(target);\n }\n };\n\n let root: Relationships;\n try {\n root = store.rootRelationships();\n } catch {\n return { reachable, complete: false, blockedBy: [ROOT_RELS_PART] };\n }\n if (root.size === 0) {\n // Not a package we can reason about. Every part in it would look orphaned,\n // which is the one wrong answer with consequences. `PartStore.write` refuses\n // such a package outright a moment later; we simply decline to act first.\n return { reachable, complete: false, blockedBy: [ROOT_RELS_PART] };\n }\n if (store.has(ROOT_RELS_PART)) reachable.add(normalizePartName(ROOT_RELS_PART));\n follow(root);\n\n // Every reached part is asked for its relationships - including the ones with\n // no `.rels` part in the package.\n //\n // The census restricts this to parts that own a relationship part, and on a\n // package being read that is a free optimisation. Here it is a bug, and a\n // quiet one: a relationship collection created during this session exists\n // only as a parsed object until `PartStore.write` materialises it, so its\n // `.rels` is not among `partNames` yet. Keying the walk on existing `.rels`\n // parts therefore makes exactly the edges this session added invisible - and\n // those are the edges a writer is about. It cost one failing test to find,\n // and the optimisation it replaces was buying a `Map` lookup per part.\n for (let part = queue.pop(); part !== undefined; part = queue.pop()) {\n try {\n follow(store.relationships(part));\n } catch {\n blockedBy.push(relsPartNameFor(part));\n }\n }\n\n return { reachable, complete: blockedBy.length === 0, blockedBy };\n}\n\n/**\n * Parts nothing in the package can get to.\n *\n * Relationship parts are excluded by construction rather than by filter - see\n * the header. `[Content_Types].xml` never appears because it is not a part.\n */\nexport function orphanedParts(store: PartStore, walk: Reachability): string[] {\n return store.partNames.filter((name) => !walk.reachable.has(normalizePartName(name)));\n}\n","import { normalizePartName, type PartStore } from '@pptx-studio/opc';\nimport { WriterError } from '../errors.js';\nimport { orphanedParts, reachableParts, type Reachability } from './reachability.js';\n\n/**\n * Mark and sweep, for media.\n *\n * ## What it collects, and why so little\n *\n * The sweep set is `/ppt/media/` and nothing else. Not layouts, not masters,\n * not themes, not `ppt/embeddings/*.bin`, whatever the relationship graph says\n * about them. PowerPoint keeps slide layouts that no slide uses - that is how\n * the layout picker has anything to offer - so a package where an unused layout\n * has been tidied away is a package where \"change layout\" has quietly lost\n * options the user had a moment ago. There is no upside to weigh against that:\n * the layout part is a few kilobytes and the media is the megabytes.\n *\n * ## Where the walk is rooted is the same rule, made twice\n *\n * The obvious implementation asks \"which images do the slides use?\" and roots\n * the walk at the slide list. It is wrong, and wrong in the way that only shows\n * up weeks later: a layout no slide currently uses still has its background\n * picture, and switching a slide onto that layout has to still work. Rooting at\n * the package root instead gets this right with no special case - layouts hang\n * off the master, the master off `presentation.xml` - so every image a layout\n * or master references is live whether or not a slide has ever used it.\n *\n * ## It cannot break a relationship\n *\n * Worth stating as a property rather than a hope, because it is what makes this\n * safe to run without supervision. A part is collected only when **no**\n * relationship anywhere in the package resolves to it. So there is no edge left\n * to dangle - and the case that kills naive implementations, two slides sharing\n * one image with one of them deleted, cannot arise: the surviving slide's\n * relationship still resolves and still marks the image live.\n *\n * The corollary is a division of labour. This collects nothing until something\n * else has removed the relationship, and removing it is part of deleting a\n * picture, which is a document operation and belongs to Phase 5's command\n * layer rather than to the writer.\n *\n * ## By default it collects only what we orphaned\n *\n * The question `@pptx-studio/validate` asks about findings, asked about parts:\n * not \"is this unreferenced\" but \"did *we* leave it unreferenced\". A deck can\n * arrive with an orphaned image - decks that have been through several editors\n * often do - and collecting it would mean that merely opening and saving a file\n * changes it. That breaks the property the whole of Phase 1 exists to\n * establish, and it breaks it in the direction where nobody notices for months.\n *\n * So the default policy differences the reachability of the package we are\n * about to write against the package as it was opened, and collects only what\n * moved from reachable to unreachable. With no baseline there is no \"we\", and\n * it collects nothing.\n *\n * Measured, for scale: across the fifty-two committed decks there are 1474\n * parts, 24 of them under `/ppt/media/`, and every part of every deck is\n * reachable from the package root. On this corpus the two policies are the same\n * policy. The difference is a promise about the decks we have not seen.\n */\n\nexport type CollectionPolicy =\n /** Only parts this session left unreferenced. The default. */\n | 'orphaned-here'\n /** Every unreferenced part in the sweep set, however it got that way. */\n | 'every-orphan'\n /** Collect nothing. */\n | 'none';\n\n/** `/ppt/media/...` - the default sweep set. */\nexport function isMediaPart(partName: string): boolean {\n return /^\\/ppt\\/media\\//i.test(partName);\n}\n\nexport interface CollectOptions {\n /** The package about to be written. */\n readonly store: PartStore;\n /** The package as it was opened. Required by `orphaned-here`, ignored otherwise. */\n readonly baseline?: PartStore | null;\n /** Defaults to `orphaned-here`. */\n readonly policy?: CollectionPolicy;\n /**\n * Which parts the sweep may touch at all. Defaults to `isMediaPart`.\n *\n * Sub-phase 8.7 will pass one of its own, to collect a `ppt/fonts/*.fntdata`\n * whose typeface no run uses any more - PowerPoint requires every listed\n * embedded font to be in use. Anything wider than that wants the header above\n * read first.\n */\n readonly sweepable?: (partName: string) => boolean;\n}\n\n/** An orphan the sweep declined to collect, and why. */\nexport interface KeptPart {\n readonly part: string;\n readonly why: string;\n}\n\nexport interface CollectionPlan {\n /** Parts removed, in the order they were found. */\n readonly collect: readonly string[];\n /** Orphans left in place, each with a reason. Empty is the normal case. */\n readonly kept: readonly KeptPart[];\n /**\n * False when the relationship graph could not be walked completely.\n *\n * A caller that deletes on the strength of an incomplete walk is deleting on\n * the strength of a parse error.\n */\n readonly safe: boolean;\n /** Relationship parts that would not parse. Empty when `safe`. */\n readonly blockedBy: readonly string[];\n}\n\n/**\n * Plan and apply in one pass.\n *\n * Not split into a pure plan and a separate apply, and the reason is the fixed\n * point below: removing a part can orphan the parts *it* referenced, and the\n * only way to observe that is to remove and look again. A pure planner would\n * have to simulate the store to find the second round, which is a second\n * implementation of the store.\n *\n * Never throws. A package too broken to reason about yields a plan that\n * collects nothing and says why - refusing would make a deck with one\n * unparseable `.rels` permanently unsaveable, and that `.rels` may well have\n * arrived that way. `collectGarbage` is the variant that refuses, for callers\n * who asked for a sweep and would misread silence as \"nothing to do\".\n */\nexport function planCollection(options: CollectOptions): CollectionPlan {\n const policy = options.policy ?? 'orphaned-here';\n if (policy === 'none') return { collect: [], kept: [], safe: true, blockedBy: [] };\n\n const sweepable = options.sweepable ?? isMediaPart;\n const store = options.store;\n\n let before: Reachability | null = null;\n if (policy === 'orphaned-here') {\n const baseline = options.baseline ?? null;\n if (baseline === null) {\n const now = reachableParts(store);\n return {\n collect: [],\n kept: candidateOrphans(store, now, sweepable).map((part) => ({\n part,\n why:\n 'it is unreferenced, but with no baseline to compare against there is no way to tell ' +\n 'whether this session orphaned it or it arrived that way.',\n })),\n safe: now.complete,\n blockedBy: now.blockedBy,\n };\n }\n before = reachableParts(baseline);\n if (!before.complete)\n return { collect: [], kept: [], safe: false, blockedBy: before.blockedBy };\n }\n\n const collect: string[] = [];\n const kept: KeptPart[] = [];\n const decided = new Set<string>();\n\n // Runs to a fixed point rather than once, because a collected part can own a\n // `.rels` whose targets then become unreferenced. It terminates: every round\n // that continues removes at least one part from a finite set. On the default\n // sweep set the second round always finds nothing - media parts have no\n // relationships - so the loop costs one extra walk and buys correctness for\n // the sweep sets later sub-phases will pass.\n for (;;) {\n const now = reachableParts(store);\n if (!now.complete) {\n // On the first round nothing has been removed and the empty plan is the\n // honest answer. On a later round `collect` holds work already applied,\n // which is reported as done rather than silently dropped.\n return { collect, kept, safe: false, blockedBy: now.blockedBy };\n }\n\n const found = candidateOrphans(store, now, sweepable).filter(\n (part) => !decided.has(normalizePartName(part)),\n );\n if (found.length === 0) break;\n\n const removing: string[] = [];\n for (const part of found) {\n decided.add(normalizePartName(part));\n if (before !== null && !before.reachable.has(normalizePartName(part))) {\n kept.push({\n part,\n why: 'it was already unreferenced when this package was opened, so it is not ours to remove.',\n });\n continue;\n }\n removing.push(part);\n }\n if (removing.length === 0) break;\n\n for (const part of removing) {\n store.removePart(part);\n collect.push(part);\n }\n }\n\n return { collect, kept, safe: true, blockedBy: [] };\n}\n\nfunction candidateOrphans(\n store: PartStore,\n walk: Reachability,\n sweepable: (partName: string) => boolean,\n): string[] {\n if (!walk.complete) return [];\n return orphanedParts(store, walk).filter(sweepable);\n}\n\n/**\n * Collect, and refuse rather than guess.\n *\n * The entry point for a deliberate clean-up: a user asking for one, or a\n * command that knows it has just orphaned something. It throws when the graph\n * could not be walked completely, because a caller who asked for a sweep and\n * got silence would reasonably conclude there had been nothing to sweep.\n *\n * `exportPackage` deliberately does not use this. An export that failed because\n * some unrelated `.rels` will not parse would be refusing to save a file over a\n * defect it did not cause and cannot fix.\n */\nexport function collectGarbage(options: CollectOptions): CollectionPlan {\n const plan = planCollection(options);\n if (plan.safe) return plan;\n throw new WriterError(\n 'ERR_COLLECTION_UNSAFE',\n 'refusing to collect: ' +\n String(plan.blockedBy.length) +\n ' relationship part(s) would not parse, so no part in this package can be shown to be ' +\n 'unreferenced. First: ' +\n (plan.blockedBy[0] ?? '(unknown)') +\n '. An unreadable edge is an invisible edge, and an invisible edge makes its target look ' +\n 'like garbage.',\n { blockedBy: plan.blockedBy },\n );\n}\n","import type { PartStore } from '@pptx-studio/opc';\nimport { WriterError } from '../errors.js';\n\n/**\n * Work that has to happen at save time, contributed by whoever owns it.\n *\n * Several later sub-phases need to touch the package on the way out, and each\n * of them is somebody else's subject:\n *\n * - **8.7, embedded fonts.** Six artifacts that must all be present or\n * PowerPoint reports a problem: the `fntdata` parts, the `fntdata` content-type\n * `Default`, the font relationships on `presentation.xml`, the\n * `p:embeddedFont` entries, their PANOSE and charset attributes, and\n * `@embedTrueTypeFonts`. Plus a collection pass, because PowerPoint requires\n * every listed typeface to actually be used.\n * - **3.4, autofit.** A `txBody` whose text changed needs its computed\n * `@fontScale` and `@lnSpcReduction` written back - and one whose text did\n * not must be left exactly alone, or a view-only deck stops round-tripping.\n * - **10.8, media.** The `p14:media` and `a:videoFile` relationships are a\n * deliberate pair in the file and a duplicate on the way out.\n *\n * None of that is the writer's subject. Written inline it would become a list\n * of special cases in `export.ts` that grows by one every phase, that the\n * writer's own tests would have to know about, and that could not be tested\n * without standing up a font stack or a text engine. As hooks, each lands in\n * the package that owns it, ships with the sub-phase that needs it, and this\n * package keeps knowing nothing about fonts.\n *\n * ## Where they run, and why there\n *\n * Before collection and before validation, both deliberately.\n *\n * Before collection, because a hook is exactly the thing that changes what is\n * referenced - 8.7 both adds font parts and drops the ones nothing uses - and a\n * sweep that ran first would be answering a question about the wrong package.\n *\n * Before validation, because a hook writes markup, and markup this session\n * wrote is precisely what the twenty-nine rules exist to check. A hook that ran\n * after validation would be the one part of an export nothing checked, which is\n * the opposite of how it should be: it is newly synthesized markup, the\n * riskiest kind there is.\n */\n\nexport interface PrepareContext {\n /** The package being exported. Hooks mutate this. */\n readonly store: PartStore;\n /** The package as it was opened, when there is one. Read only; never mutate it. */\n readonly baseline: PartStore | null;\n /**\n * Record something worth reporting.\n *\n * Surfaced on `ExportResult.prepared`. A hook that did nothing should say\n * nothing - the common case for every hook on most exports is silence, and a\n * note per hook per save would bury the one that mattered.\n */\n note(message: string): void;\n}\n\nexport interface PrepareHook {\n /** Short and stable. It appears in `ExportResult` and in the error if this hook throws. */\n readonly name: string;\n run(context: PrepareContext): void;\n}\n\nexport interface PrepareRecord {\n readonly hook: string;\n readonly notes: readonly string[];\n}\n\n/**\n * Run the hooks in the order given.\n *\n * The order is the caller's, not ours, and it is not inferred from anything.\n * Some pairs genuinely depend on each other - autofit must settle before a font\n * pass decides which typefaces are used - and a writer that sorted hooks by\n * some notion of priority would be guessing at a dependency the caller knows\n * for certain.\n *\n * A hook that throws stops the export. That is the point: the alternative is\n * handing over a package with five of the six font artifacts in it, which\n * PowerPoint reports as a problem with the content and no way to find out\n * which.\n */\nexport function runPrepare(\n hooks: readonly PrepareHook[],\n store: PartStore,\n baseline: PartStore | null,\n): PrepareRecord[] {\n const records: PrepareRecord[] = [];\n for (const hook of hooks) {\n const notes: string[] = [];\n const context: PrepareContext = {\n store,\n baseline,\n note: (message: string) => notes.push(message),\n };\n try {\n hook.run(context);\n } catch (error) {\n throw new WriterError(\n 'ERR_PREPARE_FAILED',\n 'the prepare hook ' +\n hook.name +\n ' threw, so nothing was written: ' +\n (error instanceof Error ? error.message : String(error)),\n { hook: hook.name },\n { cause: error },\n );\n }\n if (notes.length > 0) records.push({ hook: hook.name, notes });\n }\n return records;\n}\n","import {\n CONTENT_TYPES_PART,\n isContentTypesStreamName,\n normalizePartName,\n readZip,\n type ReadZipOptions,\n type ZipEntry,\n} from '@pptx-studio/opc';\nimport { WriterError } from '../errors.js';\n\n/**\n * The one assertion the writer makes about its own output.\n *\n * A part nobody edited must come out with the same bytes it went in with. That\n * is the first architectural bet stated as a testable sentence, and it is worth\n * a check of its own even though `V027` is nominally about the same thing,\n * because the two are looking at different objects.\n *\n * `V027` asks the **store**: it compares what the store would hand you for a\n * part against what the baseline store would hand you, and for an untouched\n * part both answers come from the same archive. So it can catch a part that was\n * replaced without being marked edited, and it cannot - even in principle -\n * catch a writer that re-serialised a clean part on the way out, because by the\n * time the bytes exist the rule has already run against a store that knows\n * nothing about them.\n *\n * This check reads the archive we actually emitted. It compares the **stored**\n * bytes, still compressed, against the stored bytes of the source archive:\n * same DEFLATE stream, same CRC, same declared sizes. Comparing compressed\n * bytes is both cheaper - nothing is inflated on either side - and stricter,\n * because two different DEFLATE encodings of identical content would pass an\n * inflate-and-compare and fail this. Stricter is what is wanted: any difference\n * at all means something re-compressed, and re-compressing is the failure.\n *\n * It needs the bytes the package was opened from. Without them there is nothing\n * to compare against and the check reports itself as not run, which is not the\n * same as reporting that it passed.\n */\n\nexport interface PreservationCheck {\n /** Entries compared and found identical. */\n readonly checked: number;\n /** Entries deliberately not compared, because this session wrote them. */\n readonly rewritten: number;\n /** Why the check did not run, or `null` when it did. */\n readonly skipped: string | null;\n}\n\n/** The key an entry is matched on across the two archives. */\nfunction entryKey(entry: ZipEntry): string {\n // The content-type stream is located case-insensitively on the way in and\n // written under the canonical spelling on the way out, so an archive that\n // said `[content_types].xml` still matches the entry we emitted.\n return isContentTypesStreamName(entry.name)\n ? CONTENT_TYPES_PART\n : normalizePartName('/' + entry.name);\n}\n\n/**\n * Compare the emitted archive against the one it came from.\n *\n * Throws `ERR_PRESERVATION_BROKEN` on the first difference, naming the part.\n * There is no \"report and continue\" mode: one part quietly re-serialised is the\n * whole category of failure this package is built to make impossible, and every\n * later one is likely the same cause.\n */\nexport function assertPreserved(\n output: Uint8Array,\n original: Uint8Array | undefined,\n rewritten: ReadonlySet<string>,\n zip: ReadZipOptions = {},\n): PreservationCheck {\n if (original === undefined) {\n return {\n checked: 0,\n rewritten: rewritten.size,\n skipped:\n 'the bytes this package was opened from were not supplied, so there is nothing to compare ' +\n 'the output against.',\n };\n }\n\n let before, after;\n try {\n before = readZip(original, zip);\n after = readZip(output, zip);\n } catch (error) {\n return {\n checked: 0,\n rewritten: rewritten.size,\n skipped:\n 'one of the two archives could not be read: ' +\n (error instanceof Error ? error.message : String(error)),\n };\n }\n\n const source = new Map<string, ZipEntry>();\n for (const entry of before.entries) {\n if (entry.isDirectory) continue;\n source.set(entryKey(entry), entry);\n }\n\n let checked = 0;\n for (const entry of after.entries) {\n const key = entryKey(entry);\n // A part this session wrote is meant to differ, and a part that is not in\n // the source archive is one we added. Neither is evidence of anything.\n if (rewritten.has(key)) continue;\n const was = source.get(key);\n if (was === undefined) continue;\n\n const now = after.raw(entry);\n const then = before.raw(was);\n if (\n entry.method === was.method &&\n entry.crc32 === was.crc32 &&\n entry.uncompressedSize === was.uncompressedSize &&\n sameBytes(now, then)\n ) {\n checked++;\n continue;\n }\n\n throw new WriterError(\n 'ERR_PRESERVATION_BROKEN',\n entry.name +\n ' was not edited and came out different anyway (' +\n describe(was) +\n ' in, ' +\n describe(entry) +\n ' out). An untouched part is streamed through still compressed and is never ' +\n 're-serialised: the moment that stops being true, every feature we cannot parse is a ' +\n 'feature we can lose.',\n { part: '/' + entry.name },\n );\n }\n\n return { checked, rewritten: rewritten.size, skipped: null };\n}\n\nfunction describe(entry: ZipEntry): string {\n return (\n String(entry.compressedSize) +\n 'B method ' +\n String(entry.method) +\n ' crc ' +\n entry.crc32.toString(16)\n );\n}\n\nfunction sameBytes(a: Uint8Array, b: Uint8Array): boolean {\n if (a.byteLength !== b.byteLength) return false;\n for (let i = 0; i < a.byteLength; i++) if (a[i] !== b[i]) return false;\n return true;\n}\n","import {\n CONTENT_TYPES_PART,\n normalizePartName,\n PartStore,\n type ReadZipOptions,\n} from '@pptx-studio/opc';\nimport { assertValid, type Report, type RuleId } from '@pptx-studio/validate';\nimport { planCollection, type CollectionPlan, type CollectionPolicy } from '../gc/collect.js';\nimport { assertPreserved, type PreservationCheck } from './preserve.js';\nimport { runPrepare, type PrepareHook, type PrepareRecord } from './prepare.js';\n\n/**\n * Handing the package back.\n *\n * Four things happen, in an order that is load-bearing rather than tidy:\n *\n * 1. **Prepare hooks**, so that whatever later phases owe the file is in it.\n * 2. **Collect**, because a hook is the thing most likely to have orphaned\n * something.\n * 3. **Write**, which streams every untouched part still compressed.\n * 4. **Check**, twice: that nothing we did not edit changed, and that the\n * twenty-nine rules pass. Only then do the bytes leave the function.\n *\n * The check comes after the write and not before it, which is the one ordering\n * that might look backwards. It is the right way round because a check that\n * runs first is checking an intention, and what a user opens is an archive. All\n * three of `V003`, the duplicate half of `V002`, and the preservation check\n * below are questions about the emitted container that no amount of inspecting\n * the store can answer. Bytes are produced, then judged, then either returned\n * or thrown away - never returned unjudged.\n */\n\nexport interface ExportOptions {\n /** The package to write, with whatever edits have been made to it. */\n readonly store: PartStore;\n /**\n * The package as it was opened.\n *\n * Optional, and the three things it costs to leave out are worth knowing\n * before deciding to. Without it the three preservation rules are skipped\n * rather than passed; the media sweep collects nothing, because there is no\n * way to tell what this session orphaned; and `origin` is unavailable, so\n * every fatal finding counts as one we introduced. `openPackage` produces a\n * correct one in a line.\n */\n readonly baseline?: PartStore;\n /** The archive the baseline was opened from. Needed by `V003` and the preservation check. */\n readonly baselineBytes?: Uint8Array;\n /** Run in the order given, before everything else. See `prepare.ts`. */\n readonly prepare?: readonly PrepareHook[];\n /** Media garbage collection. Defaults to `orphaned-here`. See `collect.ts`. */\n readonly collect?: CollectionPolicy;\n /** Which parts the sweep may touch. Defaults to `/ppt/media/`. */\n readonly sweepable?: (partName: string) => boolean;\n /**\n * Set false to write without the firewall.\n *\n * There is no legitimate production use. It exists so that a test can build a\n * deliberately broken package and look at it, and so that `cli bisect` in 1.5\n * can emit the intermediate packages whose whole purpose is to be rejected.\n */\n readonly validate?: boolean;\n /** A subset of rules to run. Defaults to all twenty-nine. */\n readonly rules?: readonly RuleId[];\n /** Set false to skip comparing the output against the source archive. */\n readonly verifyPreservation?: boolean;\n /** Passed through to `PartStore.write`. */\n readonly normalizeEntryOrder?: boolean;\n /** Passed through to `PartStore.write`. */\n readonly deflateLevel?: number;\n /** Passed to `readZip` wherever this package re-opens an archive. */\n readonly zip?: ReadZipOptions;\n}\n\nexport interface ExportResult {\n /** The package. Only ever returned when every check above passed. */\n readonly bytes: Uint8Array;\n /** `null` when validation was turned off. */\n readonly report: Report | null;\n /** Hooks that had something to say. Silent hooks are not listed. */\n readonly prepared: readonly PrepareRecord[];\n readonly collection: CollectionPlan;\n /** Parts serialised afresh. Empty on a no-op export - that is the point. */\n readonly rewritten: readonly string[];\n /** Parts streamed out of the source archive without being decompressed. */\n readonly streamed: number;\n readonly preservation: PreservationCheck;\n}\n\n/**\n * A package and the untouched twin every check compares it against.\n *\n * Two independent `PartStore.open` calls over the same bytes. Independent\n * matters: the baseline has to keep answering questions about the file as it\n * arrived no matter what happens to the other one, and sharing anything mutable\n * between them would make it a record of the present rather than the past.\n *\n * Neither call inflates anything but `[Content_Types].xml`, so the second is\n * two central-directory parses and a small XML read. They do carry separate\n * inflation budgets, which means an adversarial archive gets two budgets rather\n * than one; that is the correct trade at the size these budgets are set to, and\n * it is written down here so it is a decision rather than an oversight.\n */\nexport interface OpenPackage {\n readonly store: PartStore;\n readonly baseline: PartStore;\n readonly baselineBytes: Uint8Array;\n}\n\n/** Open a package for editing, with the baseline an export will need. */\nexport function openPackage(bytes: Uint8Array, options: ReadZipOptions = {}): OpenPackage {\n return {\n store: PartStore.open(bytes, options),\n baseline: PartStore.open(bytes, options),\n baselineBytes: bytes,\n };\n}\n\nexport function exportPackage(options: ExportOptions): ExportResult {\n const store = options.store;\n const baseline = options.baseline ?? null;\n\n const prepared = runPrepare(options.prepare ?? [], store, baseline);\n\n const collection = planCollection({\n store,\n baseline,\n ...(options.collect === undefined ? {} : { policy: options.collect }),\n ...(options.sweepable === undefined ? {} : { sweepable: options.sweepable }),\n });\n\n // Settle the relationship parts before anything reads bytes.\n //\n // `relationships(...)` hands out a live object, so an edge added or removed\n // this session lives in the parsed form and not in the part's bytes until\n // this call. `PartStore.write` would do it a moment later anyway - but the\n // firewall below reads relationship markup as raw XML, deliberately, and\n // would otherwise be checking the `.rels` as it arrived rather than as it is\n // about to be written. The rules would then report an edge we removed as\n // still present, miss one we added, and not see a `.rels` created this\n // session at all. Which is precisely what happened, in the test that exports\n // a deck with one image unlinked.\n store.materializeRelationships();\n\n const rewritten = store.rewrittenParts();\n const streamed = store.partNames.length - rewritten.length;\n\n const bytes = store.write({\n ...(options.normalizeEntryOrder === undefined\n ? {}\n : { normalizeEntryOrder: options.normalizeEntryOrder }),\n ...(options.deflateLevel === undefined ? {} : { deflateLevel: options.deflateLevel }),\n });\n\n const preservation =\n options.verifyPreservation === false\n ? { checked: 0, rewritten: rewritten.length, skipped: 'turned off by the caller.' }\n : assertPreserved(bytes, options.baselineBytes, rewrittenKeys(store, rewritten), options.zip);\n\n const report =\n options.validate === false\n ? null\n : assertValid({\n store,\n bytes,\n ...(baseline === null ? {} : { baseline }),\n ...(options.baselineBytes === undefined ? {} : { baselineBytes: options.baselineBytes }),\n ...(options.rules === undefined ? {} : { rules: options.rules }),\n ...(options.zip === undefined ? {} : { zip: options.zip }),\n });\n\n return { bytes, report, prepared, collection, rewritten, streamed, preservation };\n}\n\n/**\n * The parts the preservation check must not compare, in its own key space.\n *\n * `[Content_Types].xml` is in the set whenever the map changed, and only then:\n * it is passed through untouched when nothing altered it, which is what lets a\n * deck that spells it `[content_types].xml` round-trip its original bytes under\n * the canonical entry name.\n */\nfunction rewrittenKeys(store: PartStore, rewritten: readonly string[]): ReadonlySet<string> {\n const keys = new Set(rewritten.map((name) => normalizePartName(name)));\n if (store.contentTypes.dirty) keys.add(CONTENT_TYPES_PART);\n return keys;\n}\n","import {\n deflatedEntry,\n passthroughEntry,\n readZip,\n writeZip,\n type ReadZipOptions,\n type ZipArchive,\n type ZipEntry,\n type ZipEntryInput,\n} from '@pptx-studio/opc';\nimport {\n encodeXmlSource,\n parseXml,\n type XAttribute,\n type XElement,\n type XNode,\n} from '@pptx-studio/xml';\n\n/**\n * Narrowing a package PowerPoint refuses down to the change that causes it.\n *\n * ## Why this is the debugger for this project\n *\n * PowerPoint emits no diagnostic log. A file it will not open produces one\n * sentence - \"PowerPoint found a problem with content in deck.pptx\" - naming no\n * part, no element and no reason, and a file it *repairs* produces a sentence\n * that does not even say what was repaired. `@pptx-studio/validate` covers the\n * twenty-nine failures we know how to describe; this covers the rest, which is\n * the interesting ones. The plan's own caveat: passing the rules is necessary\n * and not sufficient, because PowerPoint rejects some schema-legal markup for\n * reasons nobody has written down.\n *\n * So the only way to find out what a refusal is about is to ask PowerPoint\n * again with less of the change present, and keep asking. That is delta\n * debugging: Zeller and Hildebrandt's `ddmin` over a set of changes\n * (*Simplifying and Isolating Failure-Inducing Input*, IEEE TSE 2002), applied\n * level by level down a tree, which is Misherghi and Su's HDD (*HDD:\n * Hierarchical Delta Debugging*, ICSE 2006).\n *\n * ## This is the isolation problem, not the simplification one\n *\n * The distinction is Zeller's and it decides the whole design. Simplification\n * starts from one failing input and cuts it down; it has to guess what a\n * smaller input looks like, and most of its guesses are not even well formed.\n * Isolation starts from a **passing** configuration and a **failing** one and\n * reduces the *difference* between them.\n *\n * We are always in the second case. A deck that PowerPoint repairs came from a\n * deck that it opened, and we have both. So the atoms here are not elements of\n * the document: they are the **changes** between the two packages, and a\n * configuration is a subset of them applied to the original. That buys three\n * things a simplifier cannot have:\n *\n * - Every configuration is well formed by construction. The splice unit is a\n * whole node replaced by the whole node the other package has in that\n * position, so no configuration can invent unbalanced markup.\n * - The empty configuration is known to pass and the full one to fail, which\n * is exactly `ddmin`'s precondition - and both are *checked* here rather\n * than assumed, because when they do not hold the reason is the most useful\n * thing this command can say.\n * - The answer is \"these two changes, out of ninety-one\", which is a sentence\n * about your edit rather than about PowerPoint's file format.\n *\n * ## Entries, not parts\n *\n * The delta is taken over **ZIP entries**, not over `PartStore` parts, and the\n * difference is not academic: `[Content_Types].xml` is not a part. It is a\n * package-level stream, `PartStore.partNames` deliberately excludes it, and a\n * missing `<Default Extension=\"fntdata\"/>` in it is the canonical cause of\n * \"PowerPoint found a problem with content\" - the one this project's own plan\n * cites. A bisector that could only vary parts would be blind to the single\n * best-documented repair prompt there is.\n *\n * Working at the entry level also means nothing has to know what an entry\n * holds. Anything that parses as XML is descended into - parts, `.rels`,\n * `docProps` and the content-type stream alike - and anything that does not is\n * one atom, whether it is a PNG, an OLE2 compound file, or markup too damaged\n * to read. There is no content-type table to keep in step.\n */\n\n// -------------------------------------------------------------------- the delta\n\nexport interface Span {\n readonly start: number;\n readonly end: number;\n}\n\nexport type ChangeKind =\n /** The entry exists only in the broken package. */\n | 'entry-added'\n /** The entry exists only in the original. */\n | 'entry-removed'\n /** A whole entry. Binary, or markup whose two sides do not line up. */\n | 'entry'\n /** One element and everything under it. */\n | 'element'\n /** One text, CDATA, comment, PI or declaration node. */\n | 'node'\n /**\n * A run of children replaced by another run of a different length.\n *\n * What an insertion or a deletion looks like. `was` empty means children were\n * added; `text` empty means they were taken away.\n */\n | 'children'\n /** One element's start tag: its name, its attributes and their spacing. */\n | 'tag'\n /** One attribute, including the whitespace in front of it. */\n | 'attribute';\n\nexport interface Change {\n readonly id: number;\n /** ZIP entry name, without a leading slash: `ppt/slides/slide1.xml`. */\n readonly entry: string;\n readonly kind: ChangeKind;\n /**\n * Where in the entry, for a person to read.\n *\n * An XPath-ish path for markup - `/p:sld/p:cSld/p:spTree/p:sp[3]/@name` -\n * built from qualified names exactly as the file writes them, because a path\n * that renamed prefixes could not be pasted back into a search.\n */\n readonly where: string;\n /** Distance from the entry, so a report can indent. */\n readonly depth: number;\n /** Where this sits in the **original** entry's text. Null for a whole entry. */\n readonly span: Span | null;\n /** What the broken package has there. Null when the entry is only original. */\n readonly text: string | null;\n /** What the original has there. Null when the entry is only in the broken one. */\n readonly was: string | null;\n /**\n * Finer changes that together reproduce this one exactly.\n *\n * The exactness is what makes descending free: the regions between the\n * children are the regions that compared equal, so applying every child is\n * the same package as applying the parent. HDD relies on it to move down a\n * level without spending a run to re-establish that the configuration fails.\n */\n readonly children: readonly Change[];\n}\n\n/** Every change in the forest, parents before children. */\nexport function flattenChanges(changes: readonly Change[]): Change[] {\n const out: Change[] = [];\n const stack = [...changes].reverse();\n while (stack.length > 0) {\n const change = stack.pop()!;\n out.push(change);\n for (let i = change.children.length - 1; i >= 0; i -= 1) stack.push(change.children[i]!);\n }\n return out;\n}\n\n/** The leaves: the finest changes this delta can express. */\nexport function leafChanges(changes: readonly Change[]): Change[] {\n return flattenChanges(changes).filter((change) => change.children.length === 0);\n}\n\n// ------------------------------------------------------------------- the oracle\n\n/**\n * What the oracle says about one candidate package.\n *\n * Three values rather than two, and the third is Zeller's. `unresolved` is a\n * run that answered neither question - PowerPoint timed out, the harness would\n * not start, a subprocess was killed. Treating it as \"passes\" would let the\n * reducer discard the change that matters and then blame an innocent one;\n * treating it as \"fails\" would let it keep everything. It is counted and\n * reported instead, and never reduced towards.\n */\nexport type Verdict = 'fails' | 'passes' | 'unresolved';\n\n/**\n * Does this package still show the problem?\n *\n * Synchronous on purpose. Every oracle we have is either pure computation - the\n * twenty-nine rules, the round-trip comparison - or a subprocess the caller\n * blocks on anyway, and `spawnSync` blocks perfectly well. Making the reducer\n * asynchronous to serve a caller that does not exist would put a Promise in the\n * middle of a browser package for nothing.\n */\nexport type Oracle = (bytes: Uint8Array, label: string) => Verdict;\n\nexport interface RunEvent {\n readonly run: number;\n readonly applied: number;\n readonly total: number;\n readonly label: string;\n}\n\nexport interface BisectOptions {\n readonly oracle: Oracle;\n /**\n * Ceiling on oracle runs.\n *\n * `ddmin` is quadratic in the worst case and the interesting oracle takes\n * about a second and a half, so a run against a large delta can take a long\n * time. When the ceiling is hit the result carries `exhausted: true` and the\n * smallest failing configuration found so far - never a truncated answer\n * presented as a minimal one.\n */\n readonly maxRuns?: number;\n /** Called before each oracle run, for a progress line. */\n readonly onRun?: (run: RunEvent) => void;\n readonly zip?: ReadZipOptions;\n /**\n * Ceiling on the size of the delta tree.\n *\n * Two packages that share nothing produce a tree the size of the document.\n * Past this, subtrees stop being decomposed and stay atomic, which costs\n * precision and not correctness; `truncated` says when it happened.\n */\n readonly maxChanges?: number;\n}\n\nexport type BisectOutcome =\n /** A minimal failing set was found. */\n | 'localized'\n /** The two packages hold the same entries, byte for byte. */\n | 'identical'\n /** The broken package passes the oracle. There is nothing to look for. */\n | 'broken-passes'\n /** The original fails too, so the cause is not in the difference. */\n | 'original-fails';\n\nexport interface BisectResult {\n readonly outcome: BisectOutcome;\n /** The whole delta, as a forest. One root per differing entry. */\n readonly changes: readonly Change[];\n /** The 1-minimal failing subset: drop any one of these and it passes. */\n readonly minimal: readonly Change[];\n /** The smallest package still showing the problem. Ready to open, or to keep. */\n readonly bytes: Uint8Array;\n /** Oracle runs actually performed. */\n readonly runs: number;\n /** Configurations answered from the cache instead of the oracle. */\n readonly cached: number;\n /** Runs that answered neither question. */\n readonly unresolved: number;\n /** True when `maxRuns` stopped the reduction early. */\n readonly exhausted: boolean;\n /** True when `maxChanges` stopped the delta being decomposed further. */\n readonly truncated: boolean;\n}\n\n// --------------------------------------------------------------- building files\n\ninterface Entry {\n readonly archive: ZipArchive;\n readonly entry: ZipEntry;\n}\n\ninterface Context {\n readonly order: readonly string[];\n readonly original: ReadonlyMap<string, Entry>;\n readonly broken: ReadonlyMap<string, Entry>;\n /** Decoded text of every original entry we descended into. */\n readonly text: ReadonlyMap<string, string>;\n readonly byEntry: ReadonlyMap<string, readonly Change[]>;\n}\n\nfunction index(archive: ZipArchive): Map<string, Entry> {\n const map = new Map<string, Entry>();\n for (const entry of archive.entries) {\n if (entry.isDirectory) continue;\n map.set(entry.name, { archive, entry });\n }\n return map;\n}\n\nfunction equalBytes(a: Uint8Array, b: Uint8Array): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i += 1) if (a[i] !== b[i]) return false;\n return true;\n}\n\n/**\n * The package with exactly this subset of the changes applied to the original.\n *\n * Entries no change touches are passed through still compressed rather than\n * re-deflated - the same trick `PartStore.write` uses, and it matters more\n * here, because this runs once per oracle call rather than once per export.\n */\nfunction buildPackage(context: Context, selected: ReadonlySet<number>): Uint8Array {\n const applied = new Map<string, Change[]>();\n for (const [name, changes] of context.byEntry) {\n const chosen = changes.filter((change) => selected.has(change.id));\n if (chosen.length > 0) applied.set(name, chosen);\n }\n\n const entries: ZipEntryInput[] = [];\n for (const name of context.order) {\n const source = context.original.get(name);\n const chosen = applied.get(name);\n\n if (chosen === undefined) {\n // Untouched by this configuration: whatever the original had, or nothing\n // at all if this entry exists only in the broken package.\n if (source !== undefined) entries.push(passthroughEntry(source.archive, source.entry));\n continue;\n }\n\n const whole = chosen.find((change) => change.span === null);\n if (whole !== undefined) {\n if (whole.kind === 'entry-removed') continue;\n const target = context.broken.get(name)!;\n entries.push(passthroughEntry(target.archive, target.entry));\n continue;\n }\n\n // Splice the broken text into the original at each selected span, back to\n // front, so an earlier span's offsets are still the offsets of the string\n // being edited. The selected set is always an antichain - HDD expands a\n // change into its children and never keeps both - so no two spans overlap.\n let text = context.text.get(name)!;\n const ordered = [...chosen].sort((a, b) => b.span!.start - a.span!.start);\n for (const change of ordered) {\n text = text.slice(0, change.span!.start) + change.text! + text.slice(change.span!.end);\n }\n entries.push(deflatedEntry(name, encodeXmlSource(text)));\n }\n\n return writeZip(entries);\n}\n\n// ---------------------------------------------------------------- the delta walk\n\ninterface Builder {\n readonly next: () => number;\n readonly budget: { left: number };\n readonly truncated: { yes: boolean };\n}\n\nfunction atom(\n builder: Builder,\n entry: string,\n kind: ChangeKind,\n where: string,\n depth: number,\n span: Span | null,\n was: string | null,\n text: string | null,\n children: readonly Change[] = [],\n): Change {\n return { id: builder.next(), entry, kind, where, depth, span, text, was, children };\n}\n\nfunction slice(source: string, node: XNode): string {\n return source.slice(node.start, node.end);\n}\n\nfunction nameOf(node: XNode): string {\n switch (node.type) {\n case 'element':\n return node.qname;\n case 'text':\n return 'text()';\n case 'cdata':\n return 'cdata()';\n case 'comment':\n return 'comment()';\n case 'processingInstruction':\n return 'processing-instruction()';\n case 'declaration':\n return 'xml-declaration()';\n }\n}\n\n/** `/p:sld/p:cSld/p:spTree/p:sp[3]` - one-based, and indexed only where it must be. */\nfunction pathTo(parent: string, siblings: readonly XNode[], at: number): string {\n const name = nameOf(siblings[at]!);\n let position = 1;\n let total = 0;\n for (let i = 0; i < siblings.length; i += 1) {\n if (nameOf(siblings[i]!) !== name) continue;\n total += 1;\n if (i < at) position += 1;\n }\n return parent + '/' + name + (total > 1 ? '[' + String(position) + ']' : '');\n}\n\n/**\n * Attributes paired by position, or null when they do not line up.\n *\n * Null whenever anything outside the attributes themselves differs - the\n * element name, the number or order of attributes, the whitespace before `/>`.\n * Otherwise the attribute spans would not cover the whole difference, and\n * reverting every one of them would not reproduce the original tag, which is\n * the single property the descent is allowed to assume.\n */\nfunction pairAttributes(a: XElement, b: XElement): [XAttribute, XAttribute][] | null {\n if (a.attributes.length === 0 || a.attributes.length !== b.attributes.length) return null;\n // `<qname` on both sides, then the same run-up to the first attribute.\n if (a.nameEnd - a.start !== b.nameEnd - b.start) return null;\n if (a.attributes[0]!.start - a.start !== b.attributes[0]!.start - b.start) return null;\n // ...and the same tail: the spacing before `>` or `/>`, and nothing between\n // the last attribute and it.\n if (a.attributes.at(-1)!.end !== a.trailingSpaceStart) return null;\n if (b.attributes.at(-1)!.end !== b.trailingSpaceStart) return null;\n if (a.openTagEnd - a.trailingSpaceStart !== b.openTagEnd - b.trailingSpaceStart) return null;\n\n const pairs: [XAttribute, XAttribute][] = [];\n for (let i = 0; i < a.attributes.length; i += 1) {\n const left = a.attributes[i]!;\n const right = b.attributes[i]!;\n if (left.qname !== right.qname) return null;\n pairs.push([left, right]);\n }\n return pairs;\n}\n\n/** The start tag, decomposed into attributes when the two tags line up. */\nfunction tagChange(\n builder: Builder,\n entry: string,\n where: string,\n depth: number,\n a: XElement,\n b: XElement,\n ao: string,\n bo: string,\n): Change {\n const children: Change[] = [];\n const pairs = pairAttributes(a, b);\n if (pairs !== null) {\n for (const [left, right] of pairs) {\n const was = ao.slice(left.start, left.end);\n const text = bo.slice(right.start, right.end);\n if (was === text) continue;\n children.push(\n atom(\n builder,\n entry,\n 'attribute',\n where + '/@' + left.qname,\n depth + 1,\n { start: left.start, end: left.end },\n was,\n text,\n ),\n );\n }\n }\n\n return atom(\n builder,\n entry,\n 'tag',\n where,\n depth,\n { start: a.start, end: a.openTagEnd },\n ao.slice(a.start, a.openTagEnd),\n bo.slice(b.start, b.openTagEnd),\n children,\n );\n}\n\n/**\n * Line up two lists of children and describe the difference between them.\n *\n * The two lists are matched from both ends first, and only what is left in the\n * middle is treated as changed. That is what makes an insertion or a deletion\n * nameable: without it, any change to the *number* of children shifts every\n * position after it, nothing pairs, and the answer degrades to \"something in\n * this element\". The case that forced it is the headline one - a missing\n * `<Default Extension=\"fntdata\"/>` reduces `<Types>` from seventeen children to\n * sixteen, and positional pairing reported the whole content-type stream.\n *\n * When the middles are the same length they pair off one to one, which is the\n * ordinary edit-in-place case. When they are not, the whole middle is one\n * change: a run of children replaced by another run. That is coarse if a part\n * had several separate insertions, and it is exact in the common case of one -\n * which is what a writer produces.\n */\nfunction pairChildren(\n builder: Builder,\n entry: string,\n where: string,\n depth: number,\n a: XElement,\n b: XElement,\n ao: string,\n bo: string,\n): Change[] {\n const left = a.children;\n const right = b.children;\n\n let lo = 0;\n while (lo < left.length && lo < right.length && slice(ao, left[lo]!) === slice(bo, right[lo]!)) {\n lo += 1;\n }\n let hiA = left.length;\n let hiB = right.length;\n while (hiA > lo && hiB > lo && slice(ao, left[hiA - 1]!) === slice(bo, right[hiB - 1]!)) {\n hiA -= 1;\n hiB -= 1;\n }\n\n if (hiA === lo && hiB === lo) return [];\n\n const changes: Change[] = [];\n if (hiA - lo === hiB - lo) {\n for (let i = lo; i < hiA; i += 1) {\n builder.budget.left -= 1;\n changes.push(\n pairNodes(builder, entry, pathTo(where, left, i), depth + 1, left[i]!, right[i]!, ao, bo),\n );\n }\n return changes;\n }\n\n // The middles are different lengths, so this is an insertion, a deletion, or\n // a replacement of one run by another. The span is the original's middle -\n // empty, and therefore zero-width, when children were only added.\n const start = lo < hiA ? left[lo]!.start : (left[hiA]?.start ?? a.closeTagStart);\n const end = lo < hiA ? left[hiA - 1]!.end : start;\n const text = lo < hiB ? bo.slice(right[lo]!.start, right[hiB - 1]!.end) : '';\n const at = lo < hiA ? pathTo(where, left, lo) : where;\n\n builder.budget.left -= 1;\n changes.push(\n atom(builder, entry, 'children', at, depth + 1, { start, end }, ao.slice(start, end), text),\n );\n return changes;\n}\n\n/** One change turning the original's node into the broken package's node. */\nfunction pairNodes(\n builder: Builder,\n entry: string,\n where: string,\n depth: number,\n a: XNode,\n b: XNode,\n ao: string,\n bo: string,\n): Change {\n const span = { start: a.start, end: a.end };\n const was = slice(ao, a);\n const text = slice(bo, b);\n\n const aligned =\n a.type === 'element' &&\n b.type === 'element' &&\n a.qname === b.qname &&\n a.selfClosing === b.selfClosing;\n\n if (!aligned) {\n return atom(\n builder,\n entry,\n a.type === 'element' ? 'element' : 'node',\n where,\n depth,\n span,\n was,\n text,\n );\n }\n\n if (builder.budget.left <= 0) {\n builder.truncated.yes = true;\n return atom(builder, entry, 'element', where, depth, span, was, text);\n }\n\n const element = a;\n const other = b;\n const children: Change[] = [];\n\n if (ao.slice(element.start, element.openTagEnd) !== bo.slice(other.start, other.openTagEnd)) {\n builder.budget.left -= 1;\n children.push(tagChange(builder, entry, where, depth + 1, element, other, ao, bo));\n }\n\n children.push(...pairChildren(builder, entry, where, depth, element, other, ao, bo));\n\n // Same name, same shape, same children, same start tag - and yet the texts\n // differ. Only the end tag is left and it cannot differ, so this is\n // unreachable. Returning an atom rather than a childless parent keeps it\n // harmless if the node model ever grows a region this walk does not cover.\n if (children.length === 0) return atom(builder, entry, 'element', where, depth, span, was, text);\n\n return atom(builder, entry, 'element', where, depth, span, was, text, children);\n}\n\nfunction deltaForEntry(\n builder: Builder,\n name: string,\n originalBytes: Uint8Array,\n brokenBytes: Uint8Array,\n text: Map<string, string>,\n): Change {\n const whole = (): Change => atom(builder, name, 'entry', '/', 0, null, null, null);\n\n let a;\n let b;\n try {\n a = parseXml(originalBytes);\n b = parseXml(brokenBytes);\n } catch {\n // Not XML, or XML we cannot read. Either way this entry is one atom.\n return whole();\n }\n\n if (a.children.length !== b.children.length) return whole();\n\n const children: Change[] = [];\n for (let i = 0; i < a.children.length; i += 1) {\n const left = a.children[i]!;\n const right = b.children[i]!;\n if (slice(a.source, left) === slice(b.source, right)) continue;\n builder.budget.left -= 1;\n children.push(\n pairNodes(builder, name, pathTo('', a.children, i), 1, left, right, a.source, b.source),\n );\n }\n\n if (children.length === 0) return whole();\n text.set(name, a.source);\n return atom(builder, name, 'entry', '/', 0, null, null, null, children);\n}\n\n/** The forest of changes between two packages. One root per differing entry. */\nexport function collectDelta(\n original: ZipArchive,\n broken: ZipArchive,\n maxChanges = 20_000,\n): { changes: Change[]; text: Map<string, string>; truncated: boolean } {\n let counter = 0;\n const builder: Builder = {\n next: () => counter++,\n budget: { left: maxChanges },\n truncated: { yes: false },\n };\n\n const left = index(original);\n const right = index(broken);\n const text = new Map<string, string>();\n const changes: Change[] = [];\n\n for (const [name, source] of left) {\n const target = right.get(name);\n if (target === undefined) {\n changes.push(atom(builder, name, 'entry-removed', '/', 0, null, null, null));\n continue;\n }\n const a = source.archive.read(source.entry);\n const b = target.archive.read(target.entry);\n if (equalBytes(a, b)) continue;\n changes.push(deltaForEntry(builder, name, a, b, text));\n }\n\n for (const name of right.keys()) {\n if (left.has(name)) continue;\n changes.push(atom(builder, name, 'entry-added', '/', 0, null, null, null));\n }\n\n return { changes, text, truncated: builder.truncated.yes };\n}\n\n// --------------------------------------------------------------------- the search\n\n/** Internal. Never escapes `bisectPackages`; see the `maxRuns` handling there. */\nclass Exhausted extends Error {}\n\n/**\n * `ddmin`, reducing a set of changes to a 1-minimal failing subset.\n *\n * 1-minimal means every change left in the answer is load-bearing: drop any one\n * and the package stops failing. It does **not** mean no smaller failing set\n * exists - two changes may only fail together, and neither alone - and the\n * difference is worth keeping straight, because a report claiming a minimality\n * it does not have sends someone looking in the wrong place.\n */\nfunction ddmin(\n items: readonly Change[],\n test: (subset: readonly Change[]) => Verdict,\n /**\n * Called with every failing configuration on the way down.\n *\n * So that hitting `maxRuns` returns the smallest set reached rather than the\n * one this call started from. Without it a run that stopped one test short of\n * the answer reported the whole level, which is a true statement and a\n * useless one.\n */\n record: (current: readonly Change[]) => void = () => {},\n): readonly Change[] {\n let current = items;\n let n = 2;\n\n while (current.length >= 2) {\n const size = Math.ceil(current.length / n);\n const subsets: (readonly Change[])[] = [];\n for (let i = 0; i < current.length; i += size) subsets.push(current.slice(i, i + size));\n\n // Reduce to a subset: is one nth of what is left enough on its own?\n const failing = subsets.find((subset) => test(subset) === 'fails');\n if (failing !== undefined) {\n current = failing;\n record(current);\n n = 2;\n continue;\n }\n\n // Reduce to a complement: can one nth be dropped?\n let dropped = false;\n for (const subset of subsets) {\n const complement = current.filter((change) => !subset.includes(change));\n if (complement.length === 0 || complement.length === current.length) continue;\n if (test(complement) !== 'fails') continue;\n current = complement;\n record(current);\n n = Math.max(n - 1, 2);\n dropped = true;\n break;\n }\n if (dropped) continue;\n\n if (n >= current.length) break;\n n = Math.min(current.length, n * 2);\n }\n\n return current;\n}\n\nexport function bisectPackages(\n originalBytes: Uint8Array,\n brokenBytes: Uint8Array,\n options: BisectOptions,\n): BisectResult {\n const original = readZip(originalBytes, options.zip ?? {});\n const broken = readZip(brokenBytes, options.zip ?? {});\n const { changes, text, truncated } = collectDelta(original, broken, options.maxChanges);\n\n const order: string[] = [];\n for (const entry of original.entries) if (!entry.isDirectory) order.push(entry.name);\n for (const entry of broken.entries) {\n if (entry.isDirectory || order.includes(entry.name)) continue;\n order.push(entry.name);\n }\n\n const byEntry = new Map<string, Change[]>();\n for (const change of flattenChanges(changes)) {\n const list = byEntry.get(change.entry);\n if (list === undefined) byEntry.set(change.entry, [change]);\n else list.push(change);\n }\n\n const context: Context = {\n order,\n original: index(original),\n broken: index(broken),\n text,\n byEntry,\n };\n\n const maxRuns = options.maxRuns ?? 2000;\n let runs = 0;\n let cached = 0;\n let unresolved = 0;\n let exhausted = false;\n const seen = new Map<string, Verdict>();\n\n const test = (subset: readonly Change[]): Verdict => {\n const ids = new Set(subset.map((change) => change.id));\n const key = [...ids].sort((a, b) => a - b).join(',');\n const remembered = seen.get(key);\n if (remembered !== undefined) {\n cached += 1;\n return remembered;\n }\n if (runs >= maxRuns) throw new Exhausted();\n runs += 1;\n const label = String(subset.length) + '/' + String(changes.length) + ' change(s)';\n options.onRun?.({ run: runs, applied: subset.length, total: changes.length, label });\n const verdict = options.oracle(buildPackage(context, ids), label);\n if (verdict === 'unresolved') unresolved += 1;\n seen.set(key, verdict);\n return verdict;\n };\n\n const done = (outcome: BisectOutcome, minimal: readonly Change[]): BisectResult => ({\n outcome,\n changes,\n minimal,\n bytes: buildPackage(context, new Set(minimal.map((change) => change.id))),\n runs,\n cached,\n unresolved,\n exhausted,\n truncated,\n });\n\n if (changes.length === 0) return done('identical', []);\n\n let best: readonly Change[] = changes;\n try {\n // `ddmin` assumes the empty configuration passes and the full one fails.\n // Both are checked, because when either does not hold, saying so is the\n // most useful thing this can do - and reducing anyway would produce a\n // confident answer about nothing.\n if (test([]) === 'fails') return done('original-fails', []);\n if (test(changes) !== 'fails') return done('broken-passes', changes);\n\n let level: readonly Change[] = changes;\n for (;;) {\n best = ddmin(level, test, (current) => {\n best = current;\n });\n const expanded = best.flatMap((change) =>\n change.children.length > 0 ? [...change.children] : [change],\n );\n if (expanded.length === best.length && expanded.every((c, i) => c === best[i])) break;\n // Applying every child of a change is the same package as applying the\n // change itself, so the next level is known to fail and `ddmin` may start\n // again without spending a run to establish it.\n level = expanded;\n }\n } catch (error) {\n if (!(error instanceof Exhausted)) throw error;\n exhausted = true;\n }\n\n return done('localized', best);\n}\n\n// -------------------------------------------------------------------- reporting\n\n/** One line naming a change, without its text. */\nexport function describeChange(change: Change): string {\n const where = change.where === '/' ? '' : ' ' + change.where;\n switch (change.kind) {\n case 'entry-added':\n return 'added /' + change.entry;\n case 'entry-removed':\n return 'removed /' + change.entry;\n case 'entry':\n return 'entry /' + change.entry;\n case 'element':\n return 'element /' + change.entry + where;\n case 'node':\n return 'node /' + change.entry + where;\n case 'children':\n // The two directions read very differently to someone debugging, so they\n // are named rather than both being called \"children\".\n if (change.was === '') return 'added /' + change.entry + where;\n if (change.text === '') return 'removed /' + change.entry + where;\n return 'children /' + change.entry + where;\n case 'tag':\n return 'tag /' + change.entry + where;\n case 'attribute':\n return 'attr /' + change.entry + where;\n }\n}\n\n/** `2 change(s) in 1 entry(s), from a delta of 91, in 37 oracle run(s)`. */\nexport function summarizeBisect(result: BisectResult): string {\n const entries = new Set(result.minimal.map((change) => change.entry));\n return (\n String(result.minimal.length) +\n ' change(s) in ' +\n String(entries.size) +\n ' entry(s), from a delta of ' +\n String(flattenChanges(result.changes).length) +\n ', in ' +\n String(result.runs) +\n ' oracle run(s)'\n );\n}\n","import {\n isRelationshipPartName,\n isXmlContentType,\n normalizePartName,\n sha256Hex,\n sha256HexOfText,\n PartStore,\n sourcePartNameForRels,\n type Relationship,\n type Relationships,\n} from '@pptx-studio/opc';\nimport {\n canonicalXml,\n excerpt,\n firstDifference,\n isXmlError,\n NS,\n parseXml,\n type XmlParseLimits,\n} from '@pptx-studio/xml';\nimport {\n exportPackage,\n openPackage,\n type ExportOptions,\n type ExportResult,\n} from '../export/export.js';\n\n/**\n * Is the deck we wrote the deck we read?\n *\n * ## Why this is not a byte comparison\n *\n * The plan is explicit that raw ZIP byte equality is the wrong test and says\n * why in one line: entry order, deflate level, DOS timestamps and attribute\n * order all legitimately differ, so a byte-equality round-trip test is red on\n * day one and disabled on day two. A test nobody trusts is worse than no test,\n * because it also stops anyone writing the one that would have worked.\n *\n * `assertPreserved` in this same package *does* compare bytes, and the two are\n * not in competition. It asks a narrow question - did the entries we said we\n * would not touch come out with the identical stored bytes - and it is the\n * right question for a no-op export. This module asks the wide one: for a\n * package we edited, or one a different producer wrote, do the two archives\n * hold the same document? That question has to survive an entry moving, a rId\n * being renumbered, and a `Default` becoming an `Override`, none of which\n * changes anything a reader can observe.\n *\n * ## Three comparisons, because a package has three kinds of content\n *\n * **XML parts** compare by canonical form - see `canonicalXml` in\n * `@pptx-studio/xml` for what it normalises and, more importantly, what it\n * refuses to. Whitespace and prefixes are meaning, not formatting.\n *\n * **Relationship parts** compare as a graph with the ids treated as opaque\n * labels. Two `.rels` that disagree only about whether the theme is `rId1` or\n * `rId7` describe the same package, and PowerPoint renumbers on every save, so\n * a comparator that could not see past that would be useless the first time it\n * was pointed at a file PowerPoint had written. Ids are matched by what they\n * point at, and the resulting mapping is then applied to the referring markup:\n * an `r:embed=\"rId3\"` on one side and `r:embed=\"rId7\"` on the other are the\n * same reference exactly when rId3 and rId7 resolve to the same part.\n *\n * That relabelling is keyed on the **namespace** of the attribute rather than\n * on a list of names. Every attribute in the relationships namespace is a\n * relationship reference - `r:id`, `r:embed`, `r:link`, `r:pict`, `r:dm`,\n * `r:lo`, `r:qs`, `r:cs` - and a list would be a list that is one entry short\n * the first time we meet a part type nobody thought about.\n *\n * **Everything else** compares by SHA-256. The bytes are in hand on both sides\n * so the digest is not needed to decide equality; it is needed to *report*, and\n * to give `cli bisect` in 1.5 something to name a part by. CRC-32, which this\n * package already has for the archive, is a 32-bit error-detecting code and not\n * evidence that two files are the same.\n *\n * ## What it does not compare, and why that is not a hole\n *\n * `[Content_Types].xml` is never compared as a document. It is a map, and the\n * same map has many spellings: a `Default` for an extension and an `Override`\n * on every part with that extension mean the identical thing, and PowerPoint\n * chooses between them differently from us. What is compared instead is the\n * resolved content type of every part, which is the only thing the map is for.\n */\n\nexport type DifferenceKind =\n /** A part is in the original and not in what was written. */\n | 'part-removed'\n /** A part is in what was written and not in the original. */\n | 'part-added'\n /** Both have the part; the content types disagree. */\n | 'content-type'\n /** Both have the part; the canonical XML disagrees. */\n | 'xml'\n /** Both have the part; the bytes disagree. */\n | 'binary'\n /** The relationship graphs of one source part disagree. */\n | 'relationship'\n /** A part could not be read on one side or the other, so nothing can be said. */\n | 'unreadable';\n\nexport interface Difference {\n readonly kind: DifferenceKind;\n /** The part it is about. For a relationship difference, the *source* part. */\n readonly part: string;\n readonly detail: string;\n}\n\nexport type CompareHow = 'xml' | 'binary' | 'relationships';\n\nexport interface PartComparison {\n readonly part: string;\n readonly how: CompareHow;\n readonly same: boolean;\n /**\n * The digest of the compared form on the original side: the canonical XML for\n * an XML part, the bytes for anything else, and the relationship graph with\n * the ids left out for a `.rels`.\n *\n * Two parts with the same digest are the same part in the sense this module\n * means. That is the value worth writing into a report.\n */\n readonly digest: string;\n /** The same digest on the written side. Equal to `digest` whenever `same`. */\n readonly writtenDigest: string;\n}\n\n/** One relationship id that means the same thing under a different name. */\nexport interface Relabelling {\n /** The part whose `.rels` this is - `/` for the package root. */\n readonly source: string;\n readonly from: string;\n readonly to: string;\n /** What both ids point at, which is why they are the same label. */\n readonly target: string;\n}\n\nexport interface RoundTripReport {\n readonly ok: boolean;\n readonly parts: readonly PartComparison[];\n readonly differences: readonly Difference[];\n /**\n * Ids that moved. Empty for anything this package wrote, and the interesting\n * field when the comparison is against a file PowerPoint saved.\n */\n readonly relabelled: readonly Relabelling[];\n readonly counts: {\n readonly xml: number;\n readonly binary: number;\n readonly relationships: number;\n readonly same: number;\n };\n}\n\nexport interface CompareOptions {\n /** Passed to `parseXml` for both sides. */\n readonly limits?: XmlParseLimits;\n}\n\n/** The relationships namespace: every attribute in it is a relationship id. */\nconst R = NS.r;\n\n/**\n * What a relationship points at, as a string that does not mention its id.\n *\n * An internal target resolves to an absolute part name, so that `../media/a.png`\n * from a slide and `/ppt/media/a.png` written absolutely are one target. An\n * external one is compared verbatim: it is a URI, we do not own it, and\n * normalising somebody's `http://Example.COM/` would be inventing a rule.\n */\nfunction targetKey(rels: Relationships, rel: Relationship): string {\n if (rel.targetMode === 'External') return 'External ' + rel.target;\n try {\n return 'Internal ' + normalizePartName(rels.resolve(rel));\n } catch {\n // A target that will not resolve is still a target, and comparing the raw\n // string keeps two identically broken packages equal - which they are.\n return 'Internal? ' + rel.target;\n }\n}\n\nfunction relationshipKey(rels: Relationships, rel: Relationship): string {\n return rel.type + ' -> ' + targetKey(rels, rel);\n}\n\ninterface GraphMatch {\n /** Original id to written id, for every relationship that matched. */\n readonly relabel: ReadonlyMap<string, string>;\n readonly relabelled: readonly Relabelling[];\n readonly differences: readonly Difference[];\n /** The graph as a sorted list of keys, ids omitted. The digest is taken over this. */\n readonly signature: string;\n readonly writtenSignature: string;\n}\n\n/**\n * Match two relationship collections by what they point at.\n *\n * Duplicates are legal - a part may relate to the same target twice under two\n * ids, and `a03-fills` in the corpus has eight fills on one image - so this\n * matches within a key group by document order rather than assuming one\n * relationship per key. Order inside a group is arbitrary in principle; it is\n * the only tiebreak available, and picking the wrong pairing among identical\n * relationships cannot change the answer, because the two ids resolve to the\n * same place.\n */\nfunction matchGraphs(source: string, before: Relationships, after: Relationships): GraphMatch {\n const groupsBefore = new Map<string, Relationship[]>();\n const groupsAfter = new Map<string, Relationship[]>();\n const collect = (rels: Relationships, into: Map<string, Relationship[]>): string[] => {\n const keys: string[] = [];\n for (const rel of rels.all) {\n const key = relationshipKey(rels, rel);\n keys.push(key);\n const bucket = into.get(key);\n if (bucket === undefined) into.set(key, [rel]);\n else bucket.push(rel);\n }\n return keys.sort();\n };\n const signature = collect(before, groupsBefore).join('\\n');\n const writtenSignature = collect(after, groupsAfter).join('\\n');\n\n const relabel = new Map<string, string>();\n const relabelled: Relabelling[] = [];\n const differences: Difference[] = [];\n\n for (const [key, mine] of groupsBefore) {\n const theirs = groupsAfter.get(key) ?? [];\n const shared = Math.min(mine.length, theirs.length);\n for (let i = 0; i < shared; i++) {\n const from = mine[i]!.id;\n const to = theirs[i]!.id;\n relabel.set(from, to);\n if (from !== to) relabelled.push({ source, from, to, target: key });\n }\n for (let i = shared; i < mine.length; i++) {\n differences.push({\n kind: 'relationship',\n part: source,\n detail:\n 'the relationship ' +\n mine[i]!.id +\n ' (' +\n key +\n ') is in the original and not in what was written',\n });\n }\n }\n\n for (const [key, theirs] of groupsAfter) {\n const mine = groupsBefore.get(key) ?? [];\n for (let i = mine.length; i < theirs.length; i++) {\n differences.push({\n kind: 'relationship',\n part: source,\n detail:\n 'the relationship ' +\n theirs[i]!.id +\n ' (' +\n key +\n ') was written and is not in the original',\n });\n }\n }\n\n return { relabel, relabelled, differences, signature, writtenSignature };\n}\n\n/**\n * Every part that could own relationships, in either package, plus the root.\n *\n * Every part, and not only the ones with a `.rels` in `partNames` - which is\n * the shorter, faster and wrong version of this function, and the same trap\n * `reachability.ts` documents. A relationship collection created in this\n * session exists as a parsed object until `PartStore.write` materialises it, so\n * keying on existing relationship parts makes exactly the edges an editing\n * session added invisible. `PartStore.relationships` returns an empty\n * collection for a part that has none, so asking every part costs a lookup and\n * removes the whole class of miss.\n */\nfunction relationshipSources(original: PartStore, written: PartStore): string[] {\n const sources = new Set<string>(['/']);\n for (const store of [original, written]) {\n for (const partName of store.partNames) {\n if (!isRelationshipPartName(partName)) {\n sources.add(partName);\n continue;\n }\n // And the source a `.rels` names, even when that part is not in the\n // package. An orphan relationship part is a defect, but it is one this\n // comparison should be able to see rather than one it drops.\n try {\n sources.add(sourcePartNameForRels(partName));\n } catch {\n // A name that does not decode to a source is not a relationship part in\n // any useful sense; it is compared as bytes with everything else.\n }\n }\n }\n return [...sources].sort();\n}\n\nfunction readRelationships(store: PartStore, source: string): Relationships | Error {\n try {\n return store.relationships(source);\n } catch (error) {\n return error instanceof Error ? error : new Error(String(error));\n }\n}\n\n/** The content types are the same type if they differ only in case or spacing. */\nfunction sameContentType(a: string | undefined, b: string | undefined): boolean {\n return (a ?? '').trim().toLowerCase() === (b ?? '').trim().toLowerCase();\n}\n\n/**\n * Compare two packages.\n *\n * Never throws. Every failure - a part that will not parse, a `.rels` that will\n * not read - is a difference with a reason, because the caller of a comparison\n * wants the whole list and not the first item on it.\n */\nexport function comparePackages(\n original: PartStore,\n written: PartStore,\n options: CompareOptions = {},\n): RoundTripReport {\n const differences: Difference[] = [];\n const parts: PartComparison[] = [];\n const relabelled: Relabelling[] = [];\n\n // The relationship pass runs first, because the mapping it produces is what\n // makes the XML pass able to see past a renumbering.\n const relabelBySource = new Map<string, ReadonlyMap<string, string>>();\n const graphs = new Map<string, GraphMatch>();\n for (const source of relationshipSources(original, written)) {\n const before = readRelationships(original, source);\n const after = readRelationships(written, source);\n if (before instanceof Error || after instanceof Error) {\n const which = before instanceof Error ? 'the original' : 'what was written';\n const error = before instanceof Error ? before : (after as Error);\n differences.push({\n kind: 'unreadable',\n part: source,\n detail: 'the relationships of ' + which + ' would not parse: ' + error.message,\n });\n continue;\n }\n const match = matchGraphs(source, before, after);\n graphs.set(normalizePartName(source), match);\n relabelBySource.set(normalizePartName(source), match.relabel);\n differences.push(...match.differences);\n relabelled.push(...match.relabelled);\n }\n\n const names = new Map<string, string>();\n for (const store of [original, written]) {\n for (const partName of store.partNames) names.set(normalizePartName(partName), partName);\n }\n\n let xmlCount = 0;\n let binaryCount = 0;\n let relsCount = 0;\n\n for (const key of [...names.keys()].sort()) {\n const part = names.get(key)!;\n const inOriginal = original.has(part);\n const inWritten = written.has(part);\n\n if (!inOriginal || !inWritten) {\n differences.push({\n kind: inOriginal ? 'part-removed' : 'part-added',\n part,\n detail: inOriginal\n ? 'the original has this part and what was written does not'\n : 'this part was written and the original does not have it',\n });\n continue;\n }\n\n const typeBefore = original.contentTypeOf(part);\n const typeAfter = written.contentTypeOf(part);\n if (!sameContentType(typeBefore, typeAfter)) {\n differences.push({\n kind: 'content-type',\n part,\n detail:\n 'the content type was ' +\n (typeBefore ?? '(none)') +\n ' and is now ' +\n (typeAfter ?? '(none)'),\n });\n }\n\n if (isRelationshipPartName(part)) {\n relsCount++;\n parts.push(relationshipComparison(part, graphs));\n continue;\n }\n\n if (isXmlContentType(typeBefore) && isXmlContentType(typeAfter)) {\n xmlCount++;\n parts.push(\n compareXmlPart(part, original, written, relabelBySource, differences, options.limits),\n );\n continue;\n }\n\n binaryCount++;\n parts.push(compareBinaryPart(part, original, written, differences));\n }\n\n const same = parts.filter((entry) => entry.same).length;\n return {\n ok: differences.length === 0,\n parts,\n differences,\n relabelled,\n counts: { xml: xmlCount, binary: binaryCount, relationships: relsCount, same },\n };\n}\n\n/**\n * A relationship part's verdict, taken from the graph pass rather than from its\n * bytes.\n *\n * The digest is over the graph with the ids omitted, so it is the same on both\n * sides of a renumbering. That is the whole claim of this comparison stated as\n * a single value: two `.rels` with one digest describe one package.\n */\nfunction relationshipComparison(part: string, graphs: Map<string, GraphMatch>): PartComparison {\n let source: string;\n try {\n source = sourcePartNameForRels(part);\n } catch {\n return { part, how: 'relationships', same: true, digest: '', writtenDigest: '' };\n }\n const match = graphs.get(normalizePartName(source));\n if (match === undefined) {\n // Only reachable when the graph pass reported the pair unreadable, which is\n // already a difference; saying nothing further here avoids reporting the\n // same failure twice under two kinds.\n return { part, how: 'relationships', same: false, digest: '', writtenDigest: '' };\n }\n const digest = sha256HexOfText(match.signature);\n const writtenDigest = sha256HexOfText(match.writtenSignature);\n return { part, how: 'relationships', same: digest === writtenDigest, digest, writtenDigest };\n}\n\nfunction compareXmlPart(\n part: string,\n original: PartStore,\n written: PartStore,\n relabelBySource: Map<string, ReadonlyMap<string, string>>,\n differences: Difference[],\n limits: XmlParseLimits | undefined,\n): PartComparison {\n // The mapping that applies to *this* part's markup is the one derived from\n // this part's own `.rels`. An rId is scoped to one relationship collection -\n // treating them as global is the bug `PartStore` documents in its allocator -\n // so a slide's rId3 and a chart's rId3 are unrelated names.\n const relabel = relabelBySource.get(normalizePartName(part));\n const rewriteAttributeValue =\n relabel === undefined || relabel.size === 0\n ? undefined\n : (value: string, namespaceUri: string): string =>\n namespaceUri === R ? (relabel.get(value) ?? value) : value;\n\n // Applied to the original only, never to both. The mapping takes the\n // original's ids *into* the written package's naming, so running it on the\n // written side as well would map them a second time and the two would agree\n // again - including in the case that matters, where two ids swapped what they\n // point at and both documents still say `rId1`.\n const canonical = (\n store: PartStore,\n which: string,\n relabelThis: boolean,\n ): string | Difference => {\n try {\n const document = parseXml(store.read(part), limits);\n return canonicalXml(\n document,\n relabelThis && rewriteAttributeValue !== undefined ? { rewriteAttributeValue } : {},\n );\n } catch (error) {\n return {\n kind: 'unreadable',\n part,\n detail:\n which +\n ' would not parse as XML: ' +\n (isXmlError(error) ? error.code + ' - ' + error.message : String(error)),\n };\n }\n };\n\n const before = canonical(original, 'the original', true);\n const after = canonical(written, 'what was written', false);\n if (typeof before !== 'string' || typeof after !== 'string') {\n if (typeof before !== 'string') differences.push(before);\n if (typeof after !== 'string') differences.push(after);\n return { part, how: 'xml', same: false, digest: '', writtenDigest: '' };\n }\n\n const digest = sha256HexOfText(before);\n const writtenDigest = sha256HexOfText(after);\n if (digest !== writtenDigest) {\n const at = firstDifference(before, after);\n differences.push({\n kind: 'xml',\n part,\n detail:\n 'the canonical XML differs at character ' +\n String(at) +\n '\\n original: ' +\n excerpt(before, at) +\n '\\n written: ' +\n excerpt(after, at),\n });\n }\n return { part, how: 'xml', same: digest === writtenDigest, digest, writtenDigest };\n}\n\nfunction compareBinaryPart(\n part: string,\n original: PartStore,\n written: PartStore,\n differences: Difference[],\n): PartComparison {\n let digest: string;\n let writtenDigest: string;\n try {\n digest = sha256Hex(original.read(part));\n writtenDigest = sha256Hex(written.read(part));\n } catch (error) {\n differences.push({\n kind: 'unreadable',\n part,\n detail: 'the part could not be read: ' + (error instanceof Error ? error.message : ''),\n });\n return { part, how: 'binary', same: false, digest: '', writtenDigest: '' };\n }\n if (digest !== writtenDigest) {\n differences.push({\n kind: 'binary',\n part,\n detail:\n 'the bytes differ: sha256 was ' +\n digest.slice(0, 12) +\n ', is now ' +\n writtenDigest.slice(0, 12) +\n ' (' +\n String(original.read(part).length) +\n ' bytes then, ' +\n String(written.read(part).length) +\n ' now)',\n });\n }\n return { part, how: 'binary', same: digest === writtenDigest, digest, writtenDigest };\n}\n\n/** A one-line summary, for a CLI and for a test failure message. */\nexport function summarizeRoundTrip(report: RoundTripReport): string {\n const { xml, binary, relationships, same } = report.counts;\n const total = xml + binary + relationships;\n return (\n String(same) +\n '/' +\n String(total) +\n ' parts identical (' +\n String(xml) +\n ' xml, ' +\n String(relationships) +\n ' rels, ' +\n String(binary) +\n ' binary)' +\n (report.relabelled.length === 0\n ? ''\n : ', ' + String(report.relabelled.length) + ' relationship id(s) renamed') +\n (report.ok ? '' : ', ' + String(report.differences.length) + ' difference(s)')\n );\n}\n\n/**\n * Read a package, write it back, and compare the two.\n *\n * The whole of sub-phase 1.4 in one call, and the shape `cli roundtrip` and the\n * corpus gate both want. Every option `exportPackage` takes is accepted, so a\n * caller can round-trip through a different deflate level or a normalised entry\n * order - which is the point of a comparison that is not byte equality, and\n * worth having a test do deliberately.\n *\n * Throws whatever `exportPackage` throws. A package the firewall refuses is not\n * a package with a round-trip difference; it is one that never got written, and\n * flattening the two into one verdict would lose the distinction exactly where\n * it matters.\n */\nexport function roundTripPackage(\n bytes: Uint8Array,\n options: RoundTripOptions = {},\n): RoundTripResult {\n const { limits, ...exportOptions } = options;\n const opened = openPackage(bytes, options.zip ?? {});\n const exported = exportPackage({ ...exportOptions, ...opened });\n const written = PartStore.open(exported.bytes, options.zip ?? {});\n const comparison = comparePackages(\n opened.baseline,\n written,\n limits === undefined ? {} : { limits },\n );\n return { exported, written, comparison, ok: comparison.ok };\n}\n\nexport interface RoundTripOptions\n extends Omit<ExportOptions, 'store' | 'baseline' | 'baselineBytes'>, CompareOptions {}\n\nexport interface RoundTripResult {\n readonly exported: ExportResult;\n /** The package as re-opened from the bytes that were written. */\n readonly written: PartStore;\n readonly comparison: RoundTripReport;\n readonly ok: boolean;\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@pptx-studio/writer",
3
+ "version": "0.1.0",
4
+ "description": "Export a .pptx: dirty-part-only serialization, media GC, prepare hooks, the repair firewall",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Dewiride-Open-Source/Dewiride-PPTX-Studio.git",
11
+ "directory": "packages/writer"
12
+ },
13
+ "homepage": "https://github.com/Dewiride-Open-Source/Dewiride-PPTX-Studio/tree/main/packages/writer#readme",
14
+ "bugs": "https://github.com/Dewiride-Open-Source/Dewiride-PPTX-Studio/issues",
15
+ "keywords": [
16
+ "pptx",
17
+ "ooxml",
18
+ "export",
19
+ "powerpoint",
20
+ "round-trip"
21
+ ],
22
+ "engines": {
23
+ "node": ">=24.11.0"
24
+ },
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "files": [
29
+ "dist",
30
+ "README.md",
31
+ "CHANGELOG.md",
32
+ "LICENSE",
33
+ "NOTICE"
34
+ ],
35
+ "exports": {
36
+ ".": {
37
+ "types": "./dist/index.d.ts",
38
+ "default": "./dist/index.js"
39
+ },
40
+ "./package.json": "./package.json"
41
+ },
42
+ "dependencies": {
43
+ "@pptx-studio/xml": "^0.1.0",
44
+ "@pptx-studio/opc": "^0.1.0",
45
+ "@pptx-studio/validate": "^0.1.0"
46
+ },
47
+ "devDependencies": {
48
+ "@arethetypeswrong/cli": "0.18.5",
49
+ "publint": "0.3.24",
50
+ "tsdown": "0.22.14",
51
+ "typescript": "6.0.3"
52
+ },
53
+ "scripts": {
54
+ "build": "tsdown",
55
+ "pkg:qa": "publint --strict && attw --pack . --profile esm-only"
56
+ }
57
+ }