@stll/anonymize-docx 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ATTRIBUTION.md +0 -2
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +148 -861
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { SaxesParser } from "saxes";
|
|
1
|
+
import { loadNativeAnonymizeBinding } from "@stll/anonymize";
|
|
3
2
|
//#region src/types.ts
|
|
4
3
|
const DOCX_PART_TYPES = {
|
|
5
4
|
comments: "comments",
|
|
@@ -22,7 +21,8 @@ const DOCX_RESTORATION_ERROR_CODES = {
|
|
|
22
21
|
invalidPlaceholder: "invalid-placeholder",
|
|
23
22
|
invalidSession: "invalid-session",
|
|
24
23
|
restorationLimitExceeded: "restoration-limit-exceeded",
|
|
25
|
-
sessionMismatch: "session-mismatch"
|
|
24
|
+
sessionMismatch: "session-mismatch",
|
|
25
|
+
unsupportedDocument: "unsupported-document"
|
|
26
26
|
};
|
|
27
27
|
const DOCX_REWRITE_ERROR_CODES = {
|
|
28
28
|
invalidReplacement: "invalid-replacement",
|
|
@@ -45,47 +45,6 @@ const DOCX_ARCHIVE_MAX_BYTES = 64 * 1024 * 1024;
|
|
|
45
45
|
const DOCX_ENTRY_MAX_BYTES = 16 * 1024 * 1024;
|
|
46
46
|
const DOCX_UNCOMPRESSED_MAX_BYTES = 128 * 1024 * 1024;
|
|
47
47
|
const DOCX_XML_MAX_DEPTH = 256;
|
|
48
|
-
const DOCX_MAX_ENTRIES = 4096;
|
|
49
|
-
const DOCX_MAX_TEXT_BLOCKS = 1e5;
|
|
50
|
-
const DOCX_MAX_TEXT_SEGMENTS = 1e6;
|
|
51
|
-
const DOCX_MAX_INLINE_CONTEXT_SCAN_OPS = 2e7;
|
|
52
|
-
const CONTENT_TYPES_PATH = "[Content_Types].xml";
|
|
53
|
-
const ROOT_RELATIONSHIPS_PATH = "_rels/.rels";
|
|
54
|
-
const DOCX_CORE_PROPERTIES_PATH = "docProps/core.xml";
|
|
55
|
-
const DOCX_APP_PROPERTIES_PATH = "docProps/app.xml";
|
|
56
|
-
const DOCX_CUSTOM_PROPERTIES_PATH = "docProps/custom.xml";
|
|
57
|
-
const CUSTOM_XML_DIRECTORY_PREFIX = "customXml/";
|
|
58
|
-
const DOCPROPS_DIRECTORY_PREFIX = "docProps/";
|
|
59
|
-
const METADATA_CONTENT_TYPES = /* @__PURE__ */ new Set([
|
|
60
|
-
"application/vnd.openxmlformats-package.core-properties+xml",
|
|
61
|
-
"application/vnd.openxmlformats-officedocument.extended-properties+xml",
|
|
62
|
-
"application/vnd.openxmlformats-officedocument.custom-properties+xml"
|
|
63
|
-
]);
|
|
64
|
-
const ABSOLUTE_URI_PATTERN = /^[a-z][a-z0-9+.-]*:/iu;
|
|
65
|
-
const KNOWN_METADATA_CONTENT_TYPES = {
|
|
66
|
-
[DOCX_CORE_PROPERTIES_PATH]: "application/vnd.openxmlformats-package.core-properties+xml",
|
|
67
|
-
[DOCX_APP_PROPERTIES_PATH]: "application/vnd.openxmlformats-officedocument.extended-properties+xml",
|
|
68
|
-
[DOCX_CUSTOM_PROPERTIES_PATH]: "application/vnd.openxmlformats-officedocument.custom-properties+xml"
|
|
69
|
-
};
|
|
70
|
-
const GENERIC_XML_CONTENT_TYPE = "application/xml";
|
|
71
|
-
const GENERIC_BINARY_CONTENT_TYPE = "application/octet-stream";
|
|
72
|
-
const RELATIONSHIPS_CONTENT_TYPE = "application/vnd.openxmlformats-package.relationships+xml";
|
|
73
|
-
const PII_RELATIONSHIP_TARGET_SCHEMES = ["mailto:", "tel:"];
|
|
74
|
-
const CONTENT_TYPES_NAMESPACE = "http://schemas.openxmlformats.org/package/2006/content-types";
|
|
75
|
-
const PACKAGE_RELATIONSHIP_NAMESPACES = /* @__PURE__ */ new Set(["http://purl.oclc.org/ooxml/package/relationships", "http://schemas.openxmlformats.org/package/2006/relationships"]);
|
|
76
|
-
const WORDPROCESSING_CONTENT_TYPE_PREFIX = "application/vnd.openxmlformats-officedocument.wordprocessingml.";
|
|
77
|
-
const SUPPORTED_CONTENT_TYPE_SUFFIXES = {
|
|
78
|
-
"comments+xml": DOCX_PART_TYPES.comments,
|
|
79
|
-
"document.main+xml": DOCX_PART_TYPES.mainDocument,
|
|
80
|
-
"endnotes+xml": DOCX_PART_TYPES.endnotes,
|
|
81
|
-
"footer+xml": DOCX_PART_TYPES.footer,
|
|
82
|
-
"footnotes+xml": DOCX_PART_TYPES.footnotes,
|
|
83
|
-
"header+xml": DOCX_PART_TYPES.header
|
|
84
|
-
};
|
|
85
|
-
const WORDPROCESSING_NAMESPACES$1 = /* @__PURE__ */ new Set(["http://purl.oclc.org/ooxml/wordprocessingml/main", "http://schemas.openxmlformats.org/wordprocessingml/2006/main"]);
|
|
86
|
-
const RELATIONSHIP_NAMESPACES = /* @__PURE__ */ new Set(["http://purl.oclc.org/ooxml/officeDocument/relationships", "http://schemas.openxmlformats.org/officeDocument/2006/relationships"]);
|
|
87
|
-
const OFFICE_DOCUMENT_RELATIONSHIP_TYPES = new Set([...RELATIONSHIP_NAMESPACES].map((namespace) => `${namespace}/officeDocument`));
|
|
88
|
-
const MARKUP_COMPATIBILITY_NAMESPACES = /* @__PURE__ */ new Set(["http://purl.oclc.org/ooxml/markup-compatibility/main", "http://schemas.openxmlformats.org/markup-compatibility/2006"]);
|
|
89
48
|
var DocxExtractionError = class extends Error {
|
|
90
49
|
code;
|
|
91
50
|
constructor(code, message) {
|
|
@@ -94,526 +53,26 @@ var DocxExtractionError = class extends Error {
|
|
|
94
53
|
this.code = code;
|
|
95
54
|
}
|
|
96
55
|
};
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const isRelationshipsEntry = (name) => name === ROOT_RELATIONSHIPS_PATH || RELATIONSHIPS_ENTRY_PATTERN.test(name);
|
|
105
|
-
const isCustomXmlEntry = (name) => name.startsWith(CUSTOM_XML_DIRECTORY_PREFIX) && name.endsWith(".xml");
|
|
106
|
-
const archiveFilter = ({ budget, file, includeAllEntries }) => {
|
|
107
|
-
budget.entryCount += 1;
|
|
108
|
-
if (budget.entryCount > DOCX_MAX_ENTRIES) throw new DocxExtractionError(DOCX_EXTRACTION_ERROR_CODES.uncompressedLimitExceeded, `DOCX archives must contain at most ${DOCX_MAX_ENTRIES} entries`);
|
|
109
|
-
if (!safeEntryPath(file.name)) throw new DocxExtractionError(DOCX_EXTRACTION_ERROR_CODES.unsafeEntryPath, "DOCX archive contains an unsafe entry path");
|
|
110
|
-
if (file.originalSize > 16777216) throw new DocxExtractionError(DOCX_EXTRACTION_ERROR_CODES.uncompressedLimitExceeded, `DOCX entries must not exceed ${DOCX_ENTRY_MAX_BYTES} bytes`);
|
|
111
|
-
budget.uncompressedBytes += file.originalSize;
|
|
112
|
-
if (budget.uncompressedBytes > 134217728) throw new DocxExtractionError(DOCX_EXTRACTION_ERROR_CODES.uncompressedLimitExceeded, `DOCX archives must not exceed ${DOCX_UNCOMPRESSED_MAX_BYTES} uncompressed bytes`);
|
|
113
|
-
return includeAllEntries || file.name === CONTENT_TYPES_PATH || file.name.startsWith("word/") && file.name.endsWith(".xml") || isRelationshipsEntry(file.name) || file.name.startsWith(DOCPROPS_DIRECTORY_PREFIX) || isCustomXmlEntry(file.name);
|
|
114
|
-
};
|
|
115
|
-
const unzipDocxArchive = (archive, includeAllEntries = false, onSkippedEntry) => {
|
|
116
|
-
if (archive.byteLength > 67108864) throw new DocxExtractionError(DOCX_EXTRACTION_ERROR_CODES.archiveLimitExceeded, `DOCX archives must not exceed ${DOCX_ARCHIVE_MAX_BYTES} bytes`);
|
|
117
|
-
const budget = {
|
|
118
|
-
entryCount: 0,
|
|
119
|
-
uncompressedBytes: 0
|
|
120
|
-
};
|
|
121
|
-
try {
|
|
122
|
-
return unzipSync(archive, { filter: (file) => {
|
|
123
|
-
const keep = archiveFilter({
|
|
124
|
-
budget,
|
|
125
|
-
file,
|
|
126
|
-
includeAllEntries
|
|
127
|
-
});
|
|
128
|
-
if (!keep) onSkippedEntry?.(file.name);
|
|
129
|
-
return keep;
|
|
130
|
-
} });
|
|
131
|
-
} catch (error) {
|
|
132
|
-
if (error instanceof DocxExtractionError) throw error;
|
|
133
|
-
throw new DocxExtractionError(DOCX_EXTRACTION_ERROR_CODES.invalidArchive, "Input is not a valid bounded DOCX ZIP archive");
|
|
134
|
-
}
|
|
135
|
-
};
|
|
136
|
-
const decodeXml = (bytes, path) => {
|
|
137
|
-
try {
|
|
138
|
-
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
139
|
-
} catch {
|
|
140
|
-
throw new DocxExtractionError(DOCX_EXTRACTION_ERROR_CODES.invalidXml, `DOCX XML part is not valid UTF-8: ${path}`);
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
const attributeByLocalName = (tag, localName, namespaces) => {
|
|
144
|
-
for (const attribute of Object.values(tag.attributes)) if (attribute.local === localName && (namespaces === void 0 || namespaces.has(attribute.uri))) return attribute.value;
|
|
145
|
-
return null;
|
|
146
|
-
};
|
|
147
|
-
const parseContentTypes = (xml) => {
|
|
148
|
-
const parts = [];
|
|
149
|
-
const paths = /* @__PURE__ */ new Set();
|
|
150
|
-
const parser = new SaxesParser({ xmlns: true });
|
|
151
|
-
let parseError = null;
|
|
152
|
-
let depth = 0;
|
|
153
|
-
parser.on("error", (error) => {
|
|
154
|
-
parseError = error;
|
|
155
|
-
});
|
|
156
|
-
parser.on("doctype", () => {
|
|
157
|
-
throw invalidPackage("DOCX XML must not contain a document type declaration");
|
|
158
|
-
});
|
|
159
|
-
parser.on("opentag", (tag) => {
|
|
160
|
-
assertXmlDepth(depth);
|
|
161
|
-
depth += 1;
|
|
162
|
-
if (tag.local !== "Override" || tag.uri !== CONTENT_TYPES_NAMESPACE) return;
|
|
163
|
-
const rawPath = attributeByLocalName(tag, "PartName");
|
|
164
|
-
const contentType = attributeByLocalName(tag, "ContentType");
|
|
165
|
-
if (rawPath === null || contentType === null) throw invalidPackage("DOCX content-type override is incomplete");
|
|
166
|
-
const path = rawPath.startsWith("/") ? rawPath.slice(1) : rawPath;
|
|
167
|
-
if (!safeEntryPath(path)) throw invalidPackage("DOCX content-type override has an unsafe path");
|
|
168
|
-
if (paths.has(path)) throw invalidPackage("DOCX content-type overrides must have unique paths");
|
|
169
|
-
paths.add(path);
|
|
170
|
-
parts.push({
|
|
171
|
-
path,
|
|
172
|
-
contentType
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
parser.on("closetag", () => {
|
|
176
|
-
depth -= 1;
|
|
177
|
-
});
|
|
178
|
-
try {
|
|
179
|
-
parser.write(xml).close();
|
|
180
|
-
} catch (error) {
|
|
181
|
-
if (error instanceof DocxExtractionError) throw error;
|
|
182
|
-
parseError = error instanceof Error ? error : /* @__PURE__ */ new Error("invalid XML");
|
|
183
|
-
}
|
|
184
|
-
if (parseError !== null) throw new DocxExtractionError(DOCX_EXTRACTION_ERROR_CODES.invalidXml, "DOCX content types are not valid XML");
|
|
185
|
-
return parts;
|
|
186
|
-
};
|
|
187
|
-
const parseMainDocumentTarget = (xml) => {
|
|
188
|
-
const targets = [];
|
|
189
|
-
const parser = new SaxesParser({ xmlns: true });
|
|
190
|
-
let parseError = null;
|
|
191
|
-
let depth = 0;
|
|
192
|
-
parser.on("error", (error) => {
|
|
193
|
-
parseError = error;
|
|
194
|
-
});
|
|
195
|
-
parser.on("doctype", () => {
|
|
196
|
-
throw invalidPackage("DOCX XML must not contain a document type declaration");
|
|
197
|
-
});
|
|
198
|
-
parser.on("opentag", (tag) => {
|
|
199
|
-
assertXmlDepth(depth);
|
|
200
|
-
depth += 1;
|
|
201
|
-
if (tag.local !== "Relationship" || !PACKAGE_RELATIONSHIP_NAMESPACES.has(tag.uri)) return;
|
|
202
|
-
const type = attributeByLocalName(tag, "Type");
|
|
203
|
-
if (type === null || !OFFICE_DOCUMENT_RELATIONSHIP_TYPES.has(type)) return;
|
|
204
|
-
const targetMode = attributeByLocalName(tag, "TargetMode");
|
|
205
|
-
const rawTarget = attributeByLocalName(tag, "Target");
|
|
206
|
-
if (targetMode === "External" || rawTarget === null) throw invalidPackage("DOCX main-document relationship must be internal");
|
|
207
|
-
const target = rawTarget.startsWith("/") ? rawTarget.slice(1) : rawTarget;
|
|
208
|
-
if (!safeEntryPath(target) || target.includes(":")) throw invalidPackage("DOCX main-document relationship has an unsafe target");
|
|
209
|
-
targets.push(target);
|
|
210
|
-
});
|
|
211
|
-
parser.on("closetag", () => {
|
|
212
|
-
depth -= 1;
|
|
213
|
-
});
|
|
214
|
-
try {
|
|
215
|
-
parser.write(xml).close();
|
|
216
|
-
} catch (error) {
|
|
217
|
-
if (error instanceof DocxExtractionError) throw error;
|
|
218
|
-
parseError = error instanceof Error ? error : /* @__PURE__ */ new Error("invalid XML");
|
|
219
|
-
}
|
|
220
|
-
if (parseError !== null) throw new DocxExtractionError(DOCX_EXTRACTION_ERROR_CODES.invalidXml, "DOCX root relationships are not valid XML");
|
|
221
|
-
if (targets.length !== 1) throw invalidPackage("DOCX archive must contain exactly one main-document relationship");
|
|
222
|
-
const target = targets.at(0);
|
|
223
|
-
if (target === void 0) throw invalidPackage("DOCX main-document relationship is unavailable");
|
|
224
|
-
return target;
|
|
225
|
-
};
|
|
226
|
-
const hasPiiRelationshipTargetScheme = (target) => {
|
|
227
|
-
const normalized = target.trim().toLowerCase();
|
|
228
|
-
return PII_RELATIONSHIP_TARGET_SCHEMES.some((scheme) => normalized.startsWith(scheme));
|
|
229
|
-
};
|
|
230
|
-
const isExternalRelationshipTarget = (target, targetMode) => {
|
|
231
|
-
const normalized = target.trim();
|
|
232
|
-
return targetMode?.trim().toLowerCase() === "external" || ABSOLUTE_URI_PATTERN.test(normalized) || normalized.startsWith("//");
|
|
233
|
-
};
|
|
234
|
-
const PII_SCHEME_TARGET_REASON = "target uses a PII-bearing external scheme (mailto/tel) that anonymization does not redact";
|
|
235
|
-
const EXTERNAL_TARGET_REASON = "target is external and is not examined or redacted by anonymization";
|
|
236
|
-
const DANGLING_TARGET_REASON = "target does not resolve to a package part and is not examined or redacted by anonymization";
|
|
237
|
-
const relationshipBaseDirectory = (relsPath) => {
|
|
238
|
-
const marker = relsPath.lastIndexOf("_rels/");
|
|
239
|
-
return marker <= 0 ? "" : relsPath.slice(0, marker);
|
|
240
|
-
};
|
|
241
|
-
const decodeOpcTarget = (target) => {
|
|
242
|
-
try {
|
|
243
|
-
return decodeURIComponent(target);
|
|
244
|
-
} catch {
|
|
245
|
-
return null;
|
|
246
|
-
}
|
|
247
|
-
};
|
|
248
|
-
const normalizeOpcPath = (path) => {
|
|
249
|
-
const segments = [];
|
|
250
|
-
for (const segment of path.split("/")) {
|
|
251
|
-
if (segment === "" || segment === ".") continue;
|
|
252
|
-
if (segment === "..") {
|
|
253
|
-
if (segments.length === 0) return null;
|
|
254
|
-
segments.pop();
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
257
|
-
segments.push(segment);
|
|
258
|
-
}
|
|
259
|
-
return segments.length === 0 ? null : segments.join("/");
|
|
260
|
-
};
|
|
261
|
-
const resolveInternalRelationshipTarget = (target, relsPath) => {
|
|
262
|
-
const decoded = decodeOpcTarget(target.trim());
|
|
263
|
-
if (decoded === null || decoded === "") return null;
|
|
264
|
-
if (decoded.startsWith("/")) return normalizeOpcPath(decoded.slice(1));
|
|
265
|
-
return normalizeOpcPath(`${relationshipBaseDirectory(relsPath)}${decoded}`);
|
|
266
|
-
};
|
|
267
|
-
const parseUncoveredRelationshipTargets = ({ xml, relsPath: path, knownEntryPaths }) => {
|
|
268
|
-
const found = [];
|
|
269
|
-
const parser = new SaxesParser({ xmlns: true });
|
|
270
|
-
let parseError = null;
|
|
271
|
-
let depth = 0;
|
|
272
|
-
parser.on("error", (error) => {
|
|
273
|
-
parseError = error;
|
|
274
|
-
});
|
|
275
|
-
parser.on("doctype", () => {
|
|
276
|
-
throw invalidPackage("DOCX XML must not contain a document type declaration");
|
|
277
|
-
});
|
|
278
|
-
parser.on("opentag", (tag) => {
|
|
279
|
-
assertXmlDepth(depth);
|
|
280
|
-
depth += 1;
|
|
281
|
-
if (tag.local !== "Relationship" || !PACKAGE_RELATIONSHIP_NAMESPACES.has(tag.uri)) return;
|
|
282
|
-
const target = attributeByLocalName(tag, "Target");
|
|
283
|
-
if (target === null) return;
|
|
284
|
-
const targetMode = attributeByLocalName(tag, "TargetMode");
|
|
285
|
-
if (hasPiiRelationshipTargetScheme(target)) {
|
|
286
|
-
found.push({
|
|
287
|
-
relationshipId: attributeByLocalName(tag, "Id"),
|
|
288
|
-
reason: PII_SCHEME_TARGET_REASON
|
|
289
|
-
});
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
if (isExternalRelationshipTarget(target, targetMode)) {
|
|
293
|
-
found.push({
|
|
294
|
-
relationshipId: attributeByLocalName(tag, "Id"),
|
|
295
|
-
reason: EXTERNAL_TARGET_REASON
|
|
296
|
-
});
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
const resolved = resolveInternalRelationshipTarget(target, path);
|
|
300
|
-
if (resolved === null || !knownEntryPaths.has(resolved.toLowerCase())) found.push({
|
|
301
|
-
relationshipId: attributeByLocalName(tag, "Id"),
|
|
302
|
-
reason: DANGLING_TARGET_REASON
|
|
303
|
-
});
|
|
304
|
-
});
|
|
305
|
-
parser.on("closetag", () => {
|
|
306
|
-
depth -= 1;
|
|
307
|
-
});
|
|
308
|
-
try {
|
|
309
|
-
parser.write(xml).close();
|
|
310
|
-
} catch (error) {
|
|
311
|
-
if (error instanceof DocxExtractionError) throw error;
|
|
312
|
-
parseError = error instanceof Error ? error : /* @__PURE__ */ new Error("invalid XML");
|
|
313
|
-
}
|
|
314
|
-
if (parseError !== null) throw new DocxExtractionError(DOCX_EXTRACTION_ERROR_CODES.invalidXml, `DOCX relationships are not valid XML: ${path}`);
|
|
315
|
-
return found;
|
|
316
|
-
};
|
|
317
|
-
const classifyPart = ({ contentType, path }) => {
|
|
318
|
-
if (!contentType.startsWith(WORDPROCESSING_CONTENT_TYPE_PREFIX)) return null;
|
|
319
|
-
const suffix = contentType.slice(63);
|
|
320
|
-
const type = SUPPORTED_CONTENT_TYPE_SUFFIXES[suffix];
|
|
321
|
-
return type === void 0 ? null : {
|
|
322
|
-
type,
|
|
323
|
-
path
|
|
324
|
-
};
|
|
325
|
-
};
|
|
326
|
-
const isWordTag = (tag, local) => tag.local === local && WORDPROCESSING_NAMESPACES$1.has(tag.uri);
|
|
327
|
-
const frameByLocalName = (stack, local) => {
|
|
328
|
-
for (let index = stack.length - 1; index >= 0; index -= 1) {
|
|
329
|
-
const frame = stack.at(index);
|
|
330
|
-
if (frame !== void 0 && isWordTag(frame.tag, local)) return frame;
|
|
331
|
-
}
|
|
332
|
-
return null;
|
|
333
|
-
};
|
|
334
|
-
const blockLocation = (part, blockIndex, paragraphPath, stack) => {
|
|
335
|
-
const textBox = frameByLocalName(stack, "txbxContent");
|
|
336
|
-
if (textBox !== null) return {
|
|
337
|
-
type: "text-box-paragraph",
|
|
338
|
-
part,
|
|
339
|
-
blockIndex,
|
|
340
|
-
xmlPath: paragraphPath,
|
|
341
|
-
textBoxPath: textBox.path
|
|
342
|
-
};
|
|
343
|
-
const cell = frameByLocalName(stack, "tc");
|
|
344
|
-
const row = frameByLocalName(stack, "tr");
|
|
345
|
-
const table = frameByLocalName(stack, "tbl");
|
|
346
|
-
if (cell !== null && row !== null && table !== null) return {
|
|
347
|
-
type: "table-cell-paragraph",
|
|
348
|
-
part,
|
|
349
|
-
blockIndex,
|
|
350
|
-
xmlPath: paragraphPath,
|
|
351
|
-
tablePath: table.path,
|
|
352
|
-
rowPath: row.path,
|
|
353
|
-
cellPath: cell.path
|
|
354
|
-
};
|
|
355
|
-
return {
|
|
356
|
-
type: "paragraph",
|
|
357
|
-
part,
|
|
358
|
-
blockIndex,
|
|
359
|
-
xmlPath: paragraphPath
|
|
360
|
-
};
|
|
361
|
-
};
|
|
362
|
-
const revisionForTag = (tag) => {
|
|
363
|
-
if (!WORDPROCESSING_NAMESPACES$1.has(tag.uri)) return null;
|
|
364
|
-
const revision = {
|
|
365
|
-
del: "deletion",
|
|
366
|
-
ins: "insertion",
|
|
367
|
-
moveFrom: "move-from",
|
|
368
|
-
moveTo: "move-to"
|
|
369
|
-
}[tag.local];
|
|
370
|
-
return revision === void 0 ? null : {
|
|
371
|
-
type: "revision",
|
|
372
|
-
revision
|
|
373
|
-
};
|
|
374
|
-
};
|
|
375
|
-
const inlineContexts = (stack) => {
|
|
376
|
-
const contexts = [];
|
|
377
|
-
for (const { tag } of stack) {
|
|
378
|
-
if (isWordTag(tag, "hyperlink")) contexts.push({
|
|
379
|
-
type: "hyperlink",
|
|
380
|
-
relationshipId: attributeByLocalName(tag, "id", RELATIONSHIP_NAMESPACES),
|
|
381
|
-
anchor: attributeByLocalName(tag, "anchor", WORDPROCESSING_NAMESPACES$1)
|
|
382
|
-
});
|
|
383
|
-
const revision = revisionForTag(tag);
|
|
384
|
-
if (revision !== null) contexts.push(revision);
|
|
385
|
-
}
|
|
386
|
-
return contexts;
|
|
56
|
+
const nativeExtractionErrorCode = (message) => {
|
|
57
|
+
if (message.includes("unsafe entry path")) return DOCX_EXTRACTION_ERROR_CODES.unsafeEntryPath;
|
|
58
|
+
if (message.includes("valid bounded DOCX ZIP archive")) return DOCX_EXTRACTION_ERROR_CODES.invalidArchive;
|
|
59
|
+
if (message.includes("valid XML") || message.includes("valid UTF-8")) return DOCX_EXTRACTION_ERROR_CODES.invalidXml;
|
|
60
|
+
if (message.includes(`DOCX archives must not exceed 67108864 bytes`)) return DOCX_EXTRACTION_ERROR_CODES.archiveLimitExceeded;
|
|
61
|
+
if (message.includes("must not exceed") || message.includes("must not contain more than") || message.includes("at most")) return DOCX_EXTRACTION_ERROR_CODES.uncompressedLimitExceeded;
|
|
62
|
+
return DOCX_EXTRACTION_ERROR_CODES.invalidPackage;
|
|
387
63
|
};
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
if (
|
|
391
|
-
budget.segmentCount += 1;
|
|
392
|
-
if (budget.inlineContextScanOps + stack.length > DOCX_MAX_INLINE_CONTEXT_SCAN_OPS) throw new DocxExtractionError(DOCX_EXTRACTION_ERROR_CODES.uncompressedLimitExceeded, `DOCX archives must not require more than ${DOCX_MAX_INLINE_CONTEXT_SCAN_OPS} aggregate inline-context scan operations`);
|
|
393
|
-
budget.inlineContextScanOps += stack.length;
|
|
394
|
-
const start = block.text.length;
|
|
395
|
-
block.text += value;
|
|
396
|
-
block.segments.push({
|
|
397
|
-
start,
|
|
398
|
-
end: block.text.length,
|
|
399
|
-
source,
|
|
400
|
-
contexts: inlineContexts(stack),
|
|
401
|
-
xmlPath: path
|
|
402
|
-
});
|
|
403
|
-
};
|
|
404
|
-
const extractPart = (part, xml, textBudget) => {
|
|
405
|
-
const blocks = [];
|
|
406
|
-
const stack = [];
|
|
407
|
-
const blockStack = [];
|
|
408
|
-
let nextBlockIndex = 0;
|
|
409
|
-
let currentText = "";
|
|
410
|
-
let currentTextPath = null;
|
|
411
|
-
let parseError = null;
|
|
412
|
-
let unsupportedSymbolCount = 0;
|
|
413
|
-
let unsupportedFieldInstructionCount = 0;
|
|
414
|
-
let unsupportedAlternateContentCount = 0;
|
|
415
|
-
const parser = new SaxesParser({ xmlns: true });
|
|
416
|
-
parser.on("error", (error) => {
|
|
417
|
-
parseError = error;
|
|
418
|
-
});
|
|
419
|
-
parser.on("doctype", () => {
|
|
420
|
-
throw invalidPackage("DOCX XML must not contain a document type declaration");
|
|
421
|
-
});
|
|
422
|
-
parser.on("opentag", (tag) => {
|
|
423
|
-
assertXmlDepth(stack.length);
|
|
424
|
-
const parent = stack.at(-1);
|
|
425
|
-
const childIndex = parent?.nextChildIndex ?? 0;
|
|
426
|
-
if (parent !== void 0) parent.nextChildIndex += 1;
|
|
427
|
-
const path = [...parent?.path ?? [], childIndex];
|
|
428
|
-
if (isWordTag(tag, "p")) {
|
|
429
|
-
if (nextBlockIndex >= DOCX_MAX_TEXT_BLOCKS) throw new DocxExtractionError(DOCX_EXTRACTION_ERROR_CODES.uncompressedLimitExceeded, `DOCX parts must not contain more than ${DOCX_MAX_TEXT_BLOCKS} text blocks`);
|
|
430
|
-
blockStack.push({
|
|
431
|
-
text: "",
|
|
432
|
-
segments: [],
|
|
433
|
-
location: blockLocation(part, nextBlockIndex, path, stack)
|
|
434
|
-
});
|
|
435
|
-
nextBlockIndex += 1;
|
|
436
|
-
}
|
|
437
|
-
stack.push({
|
|
438
|
-
tag,
|
|
439
|
-
path,
|
|
440
|
-
nextChildIndex: 0
|
|
441
|
-
});
|
|
442
|
-
if (isWordTag(tag, "t") || isWordTag(tag, "delText")) {
|
|
443
|
-
currentText = "";
|
|
444
|
-
currentTextPath = path;
|
|
445
|
-
}
|
|
446
|
-
const currentBlock = blockStack.at(-1);
|
|
447
|
-
if (currentBlock !== void 0 && isWordTag(tag, "tab")) appendSegment(currentBlock, textBudget, " ", "tab", path, stack);
|
|
448
|
-
if (currentBlock !== void 0 && (isWordTag(tag, "br") || isWordTag(tag, "cr"))) appendSegment(currentBlock, textBudget, "\n", "break", path, stack);
|
|
449
|
-
if (isWordTag(tag, "sym")) unsupportedSymbolCount += 1;
|
|
450
|
-
if (isWordTag(tag, "instrText") || isWordTag(tag, "fldSimple")) unsupportedFieldInstructionCount += 1;
|
|
451
|
-
if (tag.local === "AlternateContent" && MARKUP_COMPATIBILITY_NAMESPACES.has(tag.uri)) unsupportedAlternateContentCount += 1;
|
|
452
|
-
});
|
|
453
|
-
parser.on("text", (text) => {
|
|
454
|
-
if (currentTextPath !== null) currentText += text;
|
|
455
|
-
});
|
|
456
|
-
parser.on("cdata", (text) => {
|
|
457
|
-
if (currentTextPath !== null) currentText += text;
|
|
458
|
-
});
|
|
459
|
-
parser.on("closetag", (tag) => {
|
|
460
|
-
const frame = stack.at(-1);
|
|
461
|
-
if (frame === void 0 || frame.tag !== tag) throw invalidPackage("DOCX XML element stack is inconsistent");
|
|
462
|
-
if (currentTextPath !== null && (isWordTag(tag, "t") || isWordTag(tag, "delText"))) {
|
|
463
|
-
const currentBlock = blockStack.at(-1);
|
|
464
|
-
if (currentBlock === void 0) {
|
|
465
|
-
if (currentText.length > 0) throw invalidPackage("DOCX text is outside a paragraph");
|
|
466
|
-
} else appendSegment(currentBlock, textBudget, currentText, "text", currentTextPath, stack);
|
|
467
|
-
currentText = "";
|
|
468
|
-
currentTextPath = null;
|
|
469
|
-
}
|
|
470
|
-
if (isWordTag(tag, "p")) {
|
|
471
|
-
const completedBlock = blockStack.pop();
|
|
472
|
-
if (completedBlock === void 0) throw invalidPackage("DOCX paragraph state is unavailable");
|
|
473
|
-
blocks.push(completedBlock);
|
|
474
|
-
}
|
|
475
|
-
stack.pop();
|
|
476
|
-
});
|
|
64
|
+
const extractDocxText = (archive) => {
|
|
65
|
+
const extract = loadNativeAnonymizeBinding().extractDocxTextJson;
|
|
66
|
+
if (extract === void 0) throw new DocxExtractionError(DOCX_EXTRACTION_ERROR_CODES.invalidPackage, "Native anonymize binding does not expose DOCX extraction");
|
|
477
67
|
try {
|
|
478
|
-
|
|
68
|
+
return JSON.parse(extract(archive));
|
|
479
69
|
} catch (error) {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
}
|
|
483
|
-
if (parseError !== null) throw new DocxExtractionError(DOCX_EXTRACTION_ERROR_CODES.invalidXml, `DOCX part is not valid XML: ${part.path}`);
|
|
484
|
-
blocks.sort((left, right) => left.location.blockIndex - right.location.blockIndex);
|
|
485
|
-
let hyperlinkTextSegmentCount = 0;
|
|
486
|
-
let revisionTextSegmentCount = 0;
|
|
487
|
-
for (const { segments } of blocks) for (const { contexts } of segments) {
|
|
488
|
-
if (contexts.some((context) => context.type === "hyperlink")) hyperlinkTextSegmentCount += 1;
|
|
489
|
-
if (contexts.some((context) => context.type === "revision")) revisionTextSegmentCount += 1;
|
|
490
|
-
}
|
|
491
|
-
return {
|
|
492
|
-
blocks,
|
|
493
|
-
hyperlinkTextSegmentCount,
|
|
494
|
-
revisionTextSegmentCount,
|
|
495
|
-
unsupportedAlternateContentCount,
|
|
496
|
-
unsupportedSymbolCount,
|
|
497
|
-
unsupportedFieldInstructionCount
|
|
498
|
-
};
|
|
499
|
-
};
|
|
500
|
-
const extractDocxText = (archive) => {
|
|
501
|
-
const skippedEntryPaths = [];
|
|
502
|
-
const entries = unzipDocxArchive(archive, false, (name) => {
|
|
503
|
-
skippedEntryPaths.push(name);
|
|
504
|
-
});
|
|
505
|
-
const contentTypesBytes = entries[CONTENT_TYPES_PATH];
|
|
506
|
-
if (contentTypesBytes === void 0) throw invalidPackage("DOCX archive is missing [Content_Types].xml");
|
|
507
|
-
const contentTypes = parseContentTypes(decodeXml(contentTypesBytes, CONTENT_TYPES_PATH));
|
|
508
|
-
const rootRelationshipsBytes = entries[ROOT_RELATIONSHIPS_PATH];
|
|
509
|
-
if (rootRelationshipsBytes === void 0) throw invalidPackage("DOCX archive is missing _rels/.rels");
|
|
510
|
-
const mainDocumentTarget = parseMainDocumentTarget(decodeXml(rootRelationshipsBytes, ROOT_RELATIONSHIPS_PATH));
|
|
511
|
-
const supportedParts = contentTypes.map(classifyPart).filter((part) => part !== null);
|
|
512
|
-
if (supportedParts.filter((part) => part.type === DOCX_PART_TYPES.mainDocument).length !== 1) throw invalidPackage("DOCX archive must contain exactly one main document");
|
|
513
|
-
if (supportedParts.find((part) => part.type === DOCX_PART_TYPES.mainDocument)?.path !== mainDocumentTarget) throw invalidPackage("DOCX main-document relationship and content type do not agree");
|
|
514
|
-
const blocks = [];
|
|
515
|
-
const coverageParts = [];
|
|
516
|
-
let hyperlinkTextSegmentCount = 0;
|
|
517
|
-
let revisionTextSegmentCount = 0;
|
|
518
|
-
let unsupportedSymbolCount = 0;
|
|
519
|
-
let unsupportedFieldInstructionCount = 0;
|
|
520
|
-
let unsupportedAlternateContentCount = 0;
|
|
521
|
-
const textBudget = {
|
|
522
|
-
segmentCount: 0,
|
|
523
|
-
inlineContextScanOps: 0
|
|
524
|
-
};
|
|
525
|
-
for (const part of supportedParts) {
|
|
526
|
-
const bytes = entries[part.path];
|
|
527
|
-
if (bytes === void 0) throw invalidPackage(`DOCX archive is missing declared part: ${part.path}`);
|
|
528
|
-
const extracted = extractPart(part, decodeXml(bytes, part.path), textBudget);
|
|
529
|
-
if (blocks.length + extracted.blocks.length > DOCX_MAX_TEXT_BLOCKS) throw new DocxExtractionError(DOCX_EXTRACTION_ERROR_CODES.uncompressedLimitExceeded, `DOCX archives must not contain more than ${DOCX_MAX_TEXT_BLOCKS} text blocks`);
|
|
530
|
-
blocks.push(...extracted.blocks);
|
|
531
|
-
coverageParts.push({
|
|
532
|
-
status: "extracted",
|
|
533
|
-
part,
|
|
534
|
-
blockCount: extracted.blocks.length
|
|
535
|
-
});
|
|
536
|
-
hyperlinkTextSegmentCount += extracted.hyperlinkTextSegmentCount;
|
|
537
|
-
revisionTextSegmentCount += extracted.revisionTextSegmentCount;
|
|
538
|
-
unsupportedSymbolCount += extracted.unsupportedSymbolCount;
|
|
539
|
-
unsupportedFieldInstructionCount += extracted.unsupportedFieldInstructionCount;
|
|
540
|
-
unsupportedAlternateContentCount += extracted.unsupportedAlternateContentCount;
|
|
541
|
-
}
|
|
542
|
-
const knownEntryPaths = new Set([...Object.keys(entries), ...skippedEntryPaths].map((name) => name.toLowerCase()));
|
|
543
|
-
for (const [relsPath, relsBytes] of Object.entries(entries)) {
|
|
544
|
-
if (!isRelationshipsEntry(relsPath)) continue;
|
|
545
|
-
const uncoveredTargets = parseUncoveredRelationshipTargets({
|
|
546
|
-
xml: decodeXml(relsBytes, relsPath),
|
|
547
|
-
relsPath,
|
|
548
|
-
knownEntryPaths
|
|
549
|
-
});
|
|
550
|
-
for (const uncovered of uncoveredTargets) coverageParts.push({
|
|
551
|
-
status: "unsupported",
|
|
552
|
-
path: relsPath,
|
|
553
|
-
contentType: RELATIONSHIPS_CONTENT_TYPE,
|
|
554
|
-
reason: uncovered.relationshipId === null ? `Relationship ${uncovered.reason}` : `Relationship "${uncovered.relationshipId}" ${uncovered.reason}`
|
|
555
|
-
});
|
|
556
|
-
}
|
|
557
|
-
const coveredPaths = /* @__PURE__ */ new Set([CONTENT_TYPES_PATH, ...supportedParts.map((part) => part.path)]);
|
|
558
|
-
const overrideContentTypes = new Map(contentTypes.map((entry) => [entry.path, entry.contentType]));
|
|
559
|
-
const markUnsupported = (path, contentType, reason) => {
|
|
560
|
-
if (coveredPaths.has(path)) return;
|
|
561
|
-
coveredPaths.add(path);
|
|
562
|
-
coverageParts.push({
|
|
563
|
-
status: "unsupported",
|
|
564
|
-
path,
|
|
565
|
-
contentType,
|
|
566
|
-
reason
|
|
567
|
-
});
|
|
568
|
-
};
|
|
569
|
-
for (const path of Object.keys(entries)) if (path.startsWith(DOCPROPS_DIRECTORY_PREFIX)) markUnsupported(path, overrideContentTypes.get(path) ?? KNOWN_METADATA_CONTENT_TYPES[path] ?? GENERIC_XML_CONTENT_TYPE, "Document metadata parts are not extracted or redacted");
|
|
570
|
-
else if (isCustomXmlEntry(path)) markUnsupported(path, overrideContentTypes.get(path) ?? GENERIC_XML_CONTENT_TYPE, "Custom XML parts are not extracted or redacted");
|
|
571
|
-
for (const { contentType, path } of contentTypes) {
|
|
572
|
-
if (contentType === RELATIONSHIPS_CONTENT_TYPE || isRelationshipsEntry(path)) continue;
|
|
573
|
-
if (METADATA_CONTENT_TYPES.has(contentType)) {
|
|
574
|
-
markUnsupported(path, contentType, "Document metadata parts are not extracted or redacted");
|
|
575
|
-
continue;
|
|
576
|
-
}
|
|
577
|
-
if (contentType.startsWith(WORDPROCESSING_CONTENT_TYPE_PREFIX)) {
|
|
578
|
-
markUnsupported(path, contentType, "WordprocessingML part type is not extracted");
|
|
579
|
-
continue;
|
|
580
|
-
}
|
|
581
|
-
markUnsupported(path, contentType, "Package part type is not extracted or redacted");
|
|
582
|
-
}
|
|
583
|
-
for (const path of [...Object.keys(entries), ...skippedEntryPaths]) {
|
|
584
|
-
if (path.endsWith("/") || isRelationshipsEntry(path)) continue;
|
|
585
|
-
markUnsupported(path, overrideContentTypes.get(path) ?? (path.endsWith(".xml") ? GENERIC_XML_CONTENT_TYPE : GENERIC_BINARY_CONTENT_TYPE), "Package part is not examined by anonymization");
|
|
70
|
+
const message = error instanceof Error ? error.message : "DOCX extraction failed";
|
|
71
|
+
throw new DocxExtractionError(nativeExtractionErrorCode(message), message);
|
|
586
72
|
}
|
|
587
|
-
return {
|
|
588
|
-
contractVersion: 1,
|
|
589
|
-
blocks,
|
|
590
|
-
coverage: {
|
|
591
|
-
parts: coverageParts,
|
|
592
|
-
hyperlinkTextSegmentCount,
|
|
593
|
-
revisionTextSegmentCount,
|
|
594
|
-
unsupportedAlternateContentCount,
|
|
595
|
-
unsupportedSymbolCount,
|
|
596
|
-
unsupportedFieldInstructionCount
|
|
597
|
-
}
|
|
598
|
-
};
|
|
599
73
|
};
|
|
600
74
|
//#endregion
|
|
601
|
-
//#region src/location.ts
|
|
602
|
-
const arraysEqual = (left, right) => left.length === right.length && left.every((value, index) => value === right.at(index));
|
|
603
|
-
const docxLocationsEqual = (left, right) => {
|
|
604
|
-
if (left.type !== right.type || left.part.type !== right.part.type || left.part.path !== right.part.path || left.blockIndex !== right.blockIndex || !arraysEqual(left.xmlPath, right.xmlPath)) return false;
|
|
605
|
-
if (left.type === "paragraph" && right.type === "paragraph") return true;
|
|
606
|
-
if (left.type === "table-cell-paragraph" && right.type === "table-cell-paragraph") return arraysEqual(left.tablePath, right.tablePath) && arraysEqual(left.rowPath, right.rowPath) && arraysEqual(left.cellPath, right.cellPath);
|
|
607
|
-
if (left.type === "text-box-paragraph" && right.type === "text-box-paragraph") return arraysEqual(left.textBoxPath, right.textBoxPath);
|
|
608
|
-
return false;
|
|
609
|
-
};
|
|
610
|
-
const docxLocationKey = ({ blockIndex, part }) => `${part.path}\0${blockIndex}`;
|
|
611
|
-
//#endregion
|
|
612
75
|
//#region src/rewrite.ts
|
|
613
|
-
const WORDPROCESSING_NAMESPACES = /* @__PURE__ */ new Set(["http://purl.oclc.org/ooxml/wordprocessingml/main", "http://schemas.openxmlformats.org/wordprocessingml/2006/main"]);
|
|
614
|
-
const XML_NAMESPACE = "http://www.w3.org/XML/1998/namespace";
|
|
615
|
-
const DOCX_MAX_REPLACEMENTS = 1e6;
|
|
616
|
-
const SIGNATURE_PART_PREFIX = "_xmlsignatures/";
|
|
617
76
|
var DocxRewriteError = class extends Error {
|
|
618
77
|
code;
|
|
619
78
|
constructor(code, message) {
|
|
@@ -622,261 +81,110 @@ var DocxRewriteError = class extends Error {
|
|
|
622
81
|
this.code = code;
|
|
623
82
|
}
|
|
624
83
|
};
|
|
625
|
-
const
|
|
626
|
-
const
|
|
627
|
-
const
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
const
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
const
|
|
642
|
-
let
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
if (
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
84
|
+
const REWRITE_ERROR_CODES = new Set(Object.values(DOCX_REWRITE_ERROR_CODES));
|
|
85
|
+
const EXTRACTION_ERROR_CODES = new Set(Object.values(DOCX_EXTRACTION_ERROR_CODES));
|
|
86
|
+
const DOCX_REWRITE_MAX_BLOCKS = 1e5;
|
|
87
|
+
const DOCX_REWRITE_MAX_REPLACEMENTS = 1e6;
|
|
88
|
+
const LOCATION_PATH_KEYS = [
|
|
89
|
+
"xmlPath",
|
|
90
|
+
"tablePath",
|
|
91
|
+
"rowPath",
|
|
92
|
+
"cellPath",
|
|
93
|
+
"textBoxPath"
|
|
94
|
+
];
|
|
95
|
+
const preflightRewritePlan = (rewrites) => {
|
|
96
|
+
const rewriteCount = rewrites.length;
|
|
97
|
+
if (rewriteCount > DOCX_REWRITE_MAX_BLOCKS) throw new DocxRewriteError(DOCX_REWRITE_ERROR_CODES.rewriteLimitExceeded, `DOCX rewrites must not contain more than ${DOCX_REWRITE_MAX_BLOCKS} blocks`);
|
|
98
|
+
let replacementCount = 0;
|
|
99
|
+
let estimatedBytes = rewriteCount * 256;
|
|
100
|
+
const serializableRewrites = [];
|
|
101
|
+
for (let rewriteIndex = 0; rewriteIndex < rewriteCount; rewriteIndex += 1) {
|
|
102
|
+
const rewrite = rewrites[rewriteIndex];
|
|
103
|
+
if (rewrite === void 0) throw new DocxRewriteError(DOCX_REWRITE_ERROR_CODES.invalidReplacement, "DOCX rewrite plans must not contain sparse blocks");
|
|
104
|
+
if (!Array.isArray(rewrite.replacements)) throw new DocxRewriteError(DOCX_REWRITE_ERROR_CODES.invalidReplacement, "DOCX block rewrite replacements must be an array");
|
|
105
|
+
const blockReplacementCount = rewrite.replacements.length;
|
|
106
|
+
replacementCount += blockReplacementCount;
|
|
107
|
+
if (replacementCount > DOCX_REWRITE_MAX_REPLACEMENTS) throw new DocxRewriteError(DOCX_REWRITE_ERROR_CODES.rewriteLimitExceeded, `DOCX rewrites must not contain more than ${DOCX_REWRITE_MAX_REPLACEMENTS} replacements`);
|
|
108
|
+
estimatedBytes += (typeof rewrite.expectedText === "string" ? rewrite.expectedText.length * 6 : 0) + blockReplacementCount * 96;
|
|
109
|
+
const serializableReplacements = [];
|
|
110
|
+
for (let replacementIndex = 0; replacementIndex < blockReplacementCount; replacementIndex += 1) {
|
|
111
|
+
const replacement = rewrite.replacements[replacementIndex];
|
|
112
|
+
if (replacement === void 0) throw new DocxRewriteError(DOCX_REWRITE_ERROR_CODES.invalidReplacement, "DOCX rewrite plans must not contain sparse replacements");
|
|
113
|
+
const value = replacement.replacement;
|
|
114
|
+
if (typeof value === "string") estimatedBytes += value.length * 6;
|
|
115
|
+
serializableReplacements.push({
|
|
116
|
+
start: typeof replacement.start === "number" ? replacement.start : null,
|
|
117
|
+
end: typeof replacement.end === "number" ? replacement.end : null,
|
|
118
|
+
replacement: typeof value === "string" ? value : null
|
|
119
|
+
});
|
|
659
120
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
121
|
+
const location = rewrite.location;
|
|
122
|
+
const part = location["part"];
|
|
123
|
+
for (const value of [
|
|
124
|
+
location["type"],
|
|
125
|
+
part?.["type"],
|
|
126
|
+
part?.["path"]
|
|
127
|
+
]) if (typeof value === "string") estimatedBytes += value.length * 6;
|
|
128
|
+
const serializableLocation = {
|
|
129
|
+
type: typeof location["type"] === "string" ? location["type"] : null,
|
|
130
|
+
part: {
|
|
131
|
+
type: typeof part?.["type"] === "string" ? part["type"] : null,
|
|
132
|
+
path: typeof part?.["path"] === "string" ? part["path"] : null
|
|
133
|
+
},
|
|
134
|
+
blockIndex: typeof location["blockIndex"] === "number" ? location["blockIndex"] : null
|
|
135
|
+
};
|
|
136
|
+
for (const key of LOCATION_PATH_KEYS) {
|
|
137
|
+
const path = location[key];
|
|
138
|
+
if (Array.isArray(path)) {
|
|
139
|
+
if (path.length > 256) throw new DocxRewriteError(DOCX_REWRITE_ERROR_CODES.invalidReplacement, `DOCX rewrite location paths must not exceed 256 entries`);
|
|
140
|
+
estimatedBytes += path.length * 24;
|
|
141
|
+
const serializablePath = [];
|
|
142
|
+
for (let pathIndex = 0; pathIndex < path.length; pathIndex += 1) {
|
|
143
|
+
const value = path[pathIndex];
|
|
144
|
+
serializablePath.push(typeof value === "number" ? value : null);
|
|
145
|
+
}
|
|
146
|
+
serializableLocation[key] = serializablePath;
|
|
147
|
+
}
|
|
663
148
|
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
else total += 4;
|
|
669
|
-
}
|
|
670
|
-
return total;
|
|
671
|
-
};
|
|
672
|
-
const replacementByteLength = (replacement) => escapedXmlTextByteLength(replacement.replacement);
|
|
673
|
-
const validateReplacement = (replacement, blockText) => {
|
|
674
|
-
if (!Number.isSafeInteger(replacement.start) || !Number.isSafeInteger(replacement.end) || replacement.start < 0 || replacement.start >= replacement.end || replacement.end > blockText.length || !isUtf16Boundary(blockText, replacement.start) || !isUtf16Boundary(blockText, replacement.end)) throw rewriteError(DOCX_REWRITE_ERROR_CODES.invalidReplacement, "DOCX replacement spans must be nonempty bounded integer ranges at UTF-16 boundaries");
|
|
675
|
-
if (!isValidXmlText(replacement.replacement)) throw rewriteError(DOCX_REWRITE_ERROR_CODES.invalidReplacement, "DOCX replacement text must contain only valid XML characters");
|
|
676
|
-
if (replacementByteLength(replacement) > 16777216) throw rewriteError(DOCX_REWRITE_ERROR_CODES.rewriteLimitExceeded, `DOCX replacement text must not exceed ${DOCX_ENTRY_MAX_BYTES} escaped UTF-8 bytes`);
|
|
677
|
-
};
|
|
678
|
-
const coveredTextSegments = (block, replacement) => {
|
|
679
|
-
const segments = block.segments.filter(({ end, start }) => start < replacement.end && end > replacement.start);
|
|
680
|
-
let cursor = replacement.start;
|
|
681
|
-
for (const segment of segments) {
|
|
682
|
-
if (segment.source !== "text" || segment.start > cursor || segment.contexts.some((context) => context.type === "revision")) throw rewriteError(DOCX_REWRITE_ERROR_CODES.unsupportedReplacement, "DOCX replacements must stay within contiguous non-revision text segments");
|
|
683
|
-
cursor = Math.min(replacement.end, segment.end);
|
|
684
|
-
}
|
|
685
|
-
if (segments.length === 0 || cursor !== replacement.end) throw rewriteError(DOCX_REWRITE_ERROR_CODES.unsupportedReplacement, "DOCX replacements must stay within contiguous non-revision text segments");
|
|
686
|
-
return segments;
|
|
687
|
-
};
|
|
688
|
-
const planBlockUpdates = (block, rewrite) => {
|
|
689
|
-
const replacements = [...rewrite.replacements].sort((left, right) => left.start - right.start);
|
|
690
|
-
for (const [index, replacement] of replacements.entries()) {
|
|
691
|
-
validateReplacement(replacement, block.text);
|
|
692
|
-
const previous = index === 0 ? void 0 : replacements.at(index - 1);
|
|
693
|
-
if (previous !== void 0 && previous.end > replacement.start) throw rewriteError(DOCX_REWRITE_ERROR_CODES.invalidReplacement, "DOCX replacement spans must not overlap");
|
|
694
|
-
}
|
|
695
|
-
const values = /* @__PURE__ */ new Map();
|
|
696
|
-
const originalValues = /* @__PURE__ */ new Map();
|
|
697
|
-
for (const segment of block.segments) {
|
|
698
|
-
if (segment.source !== "text") continue;
|
|
699
|
-
const original = block.text.slice(segment.start, segment.end);
|
|
700
|
-
values.set(pathKey(segment.xmlPath), {
|
|
701
|
-
path: segment.xmlPath,
|
|
702
|
-
value: original,
|
|
703
|
-
originalByteLength: utf8ByteLength(original)
|
|
149
|
+
serializableRewrites.push({
|
|
150
|
+
location: serializableLocation,
|
|
151
|
+
expectedText: typeof rewrite.expectedText === "string" ? rewrite.expectedText : null,
|
|
152
|
+
replacements: serializableReplacements
|
|
704
153
|
});
|
|
705
|
-
|
|
154
|
+
if (estimatedBytes > 134217728) throw new DocxRewriteError(DOCX_REWRITE_ERROR_CODES.rewriteLimitExceeded, `DOCX rewrite plans must not exceed ${DOCX_UNCOMPRESSED_MAX_BYTES} estimated serialized bytes`);
|
|
706
155
|
}
|
|
707
|
-
|
|
708
|
-
const segments = coveredTextSegments(block, replacement);
|
|
709
|
-
const first = segments.at(0);
|
|
710
|
-
const last = segments.at(-1);
|
|
711
|
-
if (first === void 0 || last === void 0) throw rewriteError(DOCX_REWRITE_ERROR_CODES.unsupportedReplacement, "DOCX replacement text segments are unavailable");
|
|
712
|
-
const firstUpdate = values.get(pathKey(first.xmlPath));
|
|
713
|
-
const lastUpdate = values.get(pathKey(last.xmlPath));
|
|
714
|
-
if (firstUpdate === void 0 || lastUpdate === void 0) throw rewriteError(DOCX_REWRITE_ERROR_CODES.unsupportedReplacement, "DOCX replacement text nodes are unavailable");
|
|
715
|
-
const firstStart = replacement.start - first.start;
|
|
716
|
-
const lastEnd = replacement.end - last.start;
|
|
717
|
-
if (first === last) {
|
|
718
|
-
firstUpdate.value = firstUpdate.value.slice(0, firstStart) + replacement.replacement + firstUpdate.value.slice(lastEnd);
|
|
719
|
-
continue;
|
|
720
|
-
}
|
|
721
|
-
firstUpdate.value = firstUpdate.value.slice(0, firstStart) + replacement.replacement;
|
|
722
|
-
for (const segment of segments.slice(1, -1)) {
|
|
723
|
-
const update = values.get(pathKey(segment.xmlPath));
|
|
724
|
-
if (update !== void 0) update.value = "";
|
|
725
|
-
}
|
|
726
|
-
lastUpdate.value = lastUpdate.value.slice(lastEnd);
|
|
727
|
-
}
|
|
728
|
-
return [...values.entries()].filter(([key, update]) => update.value !== originalValues.get(key)).map(([, update]) => update);
|
|
729
|
-
};
|
|
730
|
-
const requiresPreservedSpace = (value) => /^\s|\s$/u.test(value);
|
|
731
|
-
const isWordTextTag = (tag) => WORDPROCESSING_NAMESPACES.has(tag.uri) && (tag.local === "t" || tag.local === "delText");
|
|
732
|
-
const hasPreservedSpace = (tag) => Object.values(tag.attributes).some((attribute) => attribute.uri === XML_NAMESPACE && attribute.local === "space" && attribute.value === "preserve");
|
|
733
|
-
const findClosingTagStart = ({ xml, contentStart, parserPosition }) => {
|
|
734
|
-
for (let index = parserPosition - 1; index >= contentStart; index -= 1) if (xml[index] === "<" && xml[index + 1] === "/") return index;
|
|
735
|
-
throw rewriteError(DOCX_REWRITE_ERROR_CODES.staleExtraction, "DOCX text-node closing tag changed after extraction");
|
|
156
|
+
return serializableRewrites;
|
|
736
157
|
};
|
|
737
|
-
const
|
|
738
|
-
const
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
const stack = [];
|
|
742
|
-
let activeText;
|
|
743
|
-
let parseError = null;
|
|
744
|
-
const parser = new SaxesParser({ xmlns: true });
|
|
745
|
-
parser.on("error", (error) => {
|
|
746
|
-
parseError = error;
|
|
747
|
-
});
|
|
748
|
-
parser.on("opentag", (tag) => {
|
|
749
|
-
if (stack.length >= 256) throw rewriteError(DOCX_REWRITE_ERROR_CODES.rewriteLimitExceeded, `DOCX XML must not exceed 256 nested elements`);
|
|
750
|
-
const parent = stack.at(-1);
|
|
751
|
-
const childIndex = parent?.nextChildIndex ?? 0;
|
|
752
|
-
if (parent !== void 0) parent.nextChildIndex += 1;
|
|
753
|
-
const path = [...parent?.path ?? [], childIndex];
|
|
754
|
-
stack.push({
|
|
755
|
-
path,
|
|
756
|
-
nextChildIndex: 0
|
|
757
|
-
});
|
|
758
|
-
const key = pathKey(path);
|
|
759
|
-
if (isWordTextTag(tag) && updatesByPath.has(key)) {
|
|
760
|
-
if (tag.isSelfClosing) throw rewriteError(DOCX_REWRITE_ERROR_CODES.unsupportedReplacement, "DOCX self-closing text nodes cannot receive replacements");
|
|
761
|
-
activeText = {
|
|
762
|
-
key,
|
|
763
|
-
contentStart: parser.position,
|
|
764
|
-
tag
|
|
765
|
-
};
|
|
766
|
-
}
|
|
767
|
-
});
|
|
768
|
-
parser.on("closetag", (tag) => {
|
|
769
|
-
if (activeText?.tag === tag) {
|
|
770
|
-
const update = updatesByPath.get(activeText.key);
|
|
771
|
-
if (update !== void 0) {
|
|
772
|
-
const contentEnd = findClosingTagStart({
|
|
773
|
-
xml,
|
|
774
|
-
contentStart: activeText.contentStart,
|
|
775
|
-
parserPosition: parser.position
|
|
776
|
-
});
|
|
777
|
-
patches.push({
|
|
778
|
-
start: activeText.contentStart,
|
|
779
|
-
end: contentEnd,
|
|
780
|
-
value: escapeXmlText(update.value)
|
|
781
|
-
});
|
|
782
|
-
if (requiresPreservedSpace(update.value) && !hasPreservedSpace(tag)) patches.push({
|
|
783
|
-
start: activeText.contentStart - 1,
|
|
784
|
-
end: activeText.contentStart - 1,
|
|
785
|
-
value: " xml:space=\"preserve\""
|
|
786
|
-
});
|
|
787
|
-
foundPaths.add(activeText.key);
|
|
788
|
-
}
|
|
789
|
-
activeText = void 0;
|
|
790
|
-
}
|
|
791
|
-
stack.pop();
|
|
792
|
-
});
|
|
158
|
+
const rewriteDocxText = (archive, rewrites) => {
|
|
159
|
+
const rewrite = loadNativeAnonymizeBinding().rewriteDocxTextNative;
|
|
160
|
+
if (rewrite === void 0) throw new Error("The native anonymize binding does not expose DOCX rewriting");
|
|
161
|
+
let serializableRewrites;
|
|
793
162
|
try {
|
|
794
|
-
|
|
163
|
+
serializableRewrites = preflightRewritePlan(rewrites);
|
|
795
164
|
} catch (error) {
|
|
796
165
|
if (error instanceof DocxRewriteError) throw error;
|
|
797
|
-
|
|
166
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
167
|
+
throw new DocxRewriteError(DOCX_REWRITE_ERROR_CODES.invalidReplacement, `DOCX rewrite plan is invalid: ${message}`);
|
|
798
168
|
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
};
|
|
805
|
-
const assertArchiveBudgets = (entries) => {
|
|
806
|
-
let totalBytes = 0;
|
|
807
|
-
for (const bytes of Object.values(entries)) {
|
|
808
|
-
if (bytes.byteLength > 16777216) throw rewriteError(DOCX_REWRITE_ERROR_CODES.rewriteLimitExceeded, `Rewritten DOCX entries must not exceed ${DOCX_ENTRY_MAX_BYTES} bytes`);
|
|
809
|
-
totalBytes += bytes.byteLength;
|
|
810
|
-
}
|
|
811
|
-
if (totalBytes > 134217728) throw rewriteError(DOCX_REWRITE_ERROR_CODES.rewriteLimitExceeded, `Rewritten DOCX archives must not exceed ${DOCX_UNCOMPRESSED_MAX_BYTES} uncompressed bytes`);
|
|
812
|
-
};
|
|
813
|
-
const rewriteDocxText = (archive, rewrites) => {
|
|
814
|
-
const extraction = extractDocxText(archive);
|
|
815
|
-
if (rewrites.length === 0) return {
|
|
816
|
-
document: archive.slice(),
|
|
817
|
-
rewrittenBlockCount: 0,
|
|
818
|
-
appliedReplacementCount: 0
|
|
819
|
-
};
|
|
820
|
-
const blocksByLocation = new Map(extraction.blocks.map((block) => [docxLocationKey(block.location), block]));
|
|
821
|
-
const updatesByPart = /* @__PURE__ */ new Map();
|
|
822
|
-
const rewrittenLocations = /* @__PURE__ */ new Set();
|
|
823
|
-
const replacementBytesByPart = /* @__PURE__ */ new Map();
|
|
824
|
-
let appliedReplacementCount = 0;
|
|
825
|
-
let totalReplacementBytes = 0;
|
|
826
|
-
for (const rewrite of rewrites) {
|
|
827
|
-
const key = docxLocationKey(rewrite.location);
|
|
828
|
-
if (rewrittenLocations.has(key)) throw rewriteError(DOCX_REWRITE_ERROR_CODES.invalidReplacement, "Each DOCX block may appear in a rewrite plan only once");
|
|
829
|
-
rewrittenLocations.add(key);
|
|
830
|
-
const block = blocksByLocation.get(key);
|
|
831
|
-
if (block === void 0 || !docxLocationsEqual(block.location, rewrite.location) || block.text !== rewrite.expectedText) throw rewriteError(DOCX_REWRITE_ERROR_CODES.staleExtraction, "DOCX block location or expected text no longer matches");
|
|
832
|
-
if (rewrite.replacements.length === 0) throw rewriteError(DOCX_REWRITE_ERROR_CODES.invalidReplacement, "DOCX block rewrite plans must contain at least one replacement");
|
|
833
|
-
if (appliedReplacementCount + rewrite.replacements.length > DOCX_MAX_REPLACEMENTS) throw rewriteError(DOCX_REWRITE_ERROR_CODES.rewriteLimitExceeded, `DOCX rewrites must not contain more than ${DOCX_MAX_REPLACEMENTS} replacements`);
|
|
834
|
-
const rewriteReplacementBytes = rewrite.replacements.reduce((total, replacement) => total + replacementByteLength(replacement), 0);
|
|
835
|
-
totalReplacementBytes += rewriteReplacementBytes;
|
|
836
|
-
if (totalReplacementBytes > 134217728) throw rewriteError(DOCX_REWRITE_ERROR_CODES.rewriteLimitExceeded, `DOCX rewrite replacement text must not exceed ${DOCX_UNCOMPRESSED_MAX_BYTES} aggregate escaped UTF-8 bytes`);
|
|
837
|
-
const partReplacementBytes = (replacementBytesByPart.get(block.location.part.path) ?? 0) + rewriteReplacementBytes;
|
|
838
|
-
replacementBytesByPart.set(block.location.part.path, partReplacementBytes);
|
|
839
|
-
if (partReplacementBytes > 16777216) throw rewriteError(DOCX_REWRITE_ERROR_CODES.rewriteLimitExceeded, `DOCX rewrite replacement text for a single part must not exceed ${DOCX_ENTRY_MAX_BYTES} aggregate escaped UTF-8 bytes`);
|
|
840
|
-
const partUpdates = updatesByPart.get(block.location.part.path) ?? /* @__PURE__ */ new Map();
|
|
841
|
-
for (const update of planBlockUpdates(block, rewrite)) partUpdates.set(pathKey(update.path), update);
|
|
842
|
-
updatesByPart.set(block.location.part.path, partUpdates);
|
|
843
|
-
appliedReplacementCount += rewrite.replacements.length;
|
|
844
|
-
}
|
|
845
|
-
let totalUpdatedNodeBytes = 0;
|
|
846
|
-
for (const updates of updatesByPart.values()) {
|
|
847
|
-
let partUpdatedNodeBytes = 0;
|
|
848
|
-
for (const update of updates.values()) partUpdatedNodeBytes += escapedXmlTextByteLength(update.value);
|
|
849
|
-
if (partUpdatedNodeBytes > 16777216) throw rewriteError(DOCX_REWRITE_ERROR_CODES.rewriteLimitExceeded, `DOCX rewritten text nodes for a single part must not exceed ${DOCX_ENTRY_MAX_BYTES} escaped UTF-8 bytes`);
|
|
850
|
-
totalUpdatedNodeBytes += partUpdatedNodeBytes;
|
|
851
|
-
if (totalUpdatedNodeBytes > 134217728) throw rewriteError(DOCX_REWRITE_ERROR_CODES.rewriteLimitExceeded, `DOCX rewritten text nodes must not exceed ${DOCX_UNCOMPRESSED_MAX_BYTES} aggregate escaped UTF-8 bytes`);
|
|
852
|
-
}
|
|
853
|
-
const entries = unzipDocxArchive(archive, true);
|
|
854
|
-
if (Object.keys(entries).some((path) => path.toLowerCase().startsWith(SIGNATURE_PART_PREFIX))) throw rewriteError(DOCX_REWRITE_ERROR_CODES.unsupportedReplacement, "Digitally signed DOCX packages must be re-signed before rewriting");
|
|
855
|
-
let projectedTotalBytes = 0;
|
|
856
|
-
for (const [partPath, partBytes] of Object.entries(entries)) {
|
|
857
|
-
let projected = partBytes.byteLength;
|
|
858
|
-
const updates = updatesByPart.get(partPath);
|
|
859
|
-
if (updates !== void 0) {
|
|
860
|
-
for (const update of updates.values()) projected += escapedXmlTextByteLength(update.value) + XML_SPACE_INSERTION_BYTES - update.originalByteLength;
|
|
861
|
-
if (projected > 16777216) throw rewriteError(DOCX_REWRITE_ERROR_CODES.rewriteLimitExceeded, `Rewritten DOCX parts must not exceed ${DOCX_ENTRY_MAX_BYTES} projected bytes`);
|
|
862
|
-
}
|
|
863
|
-
projectedTotalBytes += projected;
|
|
864
|
-
if (projectedTotalBytes > 134217728) throw rewriteError(DOCX_REWRITE_ERROR_CODES.rewriteLimitExceeded, `Rewritten DOCX archives must not exceed ${DOCX_UNCOMPRESSED_MAX_BYTES} projected uncompressed bytes`);
|
|
169
|
+
let rewritesJson;
|
|
170
|
+
try {
|
|
171
|
+
rewritesJson = JSON.stringify(serializableRewrites);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
174
|
+
throw new DocxRewriteError(DOCX_REWRITE_ERROR_CODES.invalidReplacement, `DOCX rewrite plan is not serializable: ${message}`);
|
|
865
175
|
}
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
const
|
|
870
|
-
|
|
176
|
+
try {
|
|
177
|
+
return rewrite(archive, rewritesJson);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
180
|
+
const separator = message.indexOf(": ");
|
|
181
|
+
const rawCode = message.slice(0, separator);
|
|
182
|
+
const extractionCode = rawCode;
|
|
183
|
+
if (separator > 0 && EXTRACTION_ERROR_CODES.has(extractionCode)) throw new DocxExtractionError(extractionCode, message.slice(separator + 2));
|
|
184
|
+
const code = rawCode;
|
|
185
|
+
if (separator > 0 && REWRITE_ERROR_CODES.has(code)) throw new DocxRewriteError(code, message.slice(separator + 2));
|
|
186
|
+
throw error;
|
|
871
187
|
}
|
|
872
|
-
assertArchiveBudgets(entries);
|
|
873
|
-
const document = zipSync(entries);
|
|
874
|
-
if (document.byteLength > 67108864) throw rewriteError(DOCX_REWRITE_ERROR_CODES.rewriteLimitExceeded, `Rewritten DOCX archives must not exceed ${DOCX_ARCHIVE_MAX_BYTES} bytes`);
|
|
875
|
-
return {
|
|
876
|
-
document,
|
|
877
|
-
rewrittenBlockCount: rewrites.length,
|
|
878
|
-
appliedReplacementCount
|
|
879
|
-
};
|
|
880
188
|
};
|
|
881
189
|
//#endregion
|
|
882
190
|
//#region src/coverage.ts
|
|
@@ -901,8 +209,6 @@ const docxWorkflowCoverage = (coverage) => {
|
|
|
901
209
|
};
|
|
902
210
|
//#endregion
|
|
903
211
|
//#region src/restore.ts
|
|
904
|
-
const DOCX_RESTORE_MAX_PLACEHOLDER_UTF16 = 512;
|
|
905
|
-
const DOCX_RESTORE_MAX_CANDIDATES = 1e6;
|
|
906
212
|
var DocxRestorationError = class extends Error {
|
|
907
213
|
code;
|
|
908
214
|
constructor(code, message) {
|
|
@@ -912,53 +218,6 @@ var DocxRestorationError = class extends Error {
|
|
|
912
218
|
}
|
|
913
219
|
};
|
|
914
220
|
const restorationError = (code, message) => new DocxRestorationError(code, message);
|
|
915
|
-
const encodedSessionNamespace = (sessionId) => sessionId.replaceAll("_", "%5F");
|
|
916
|
-
const isOwnedPlaceholderCandidate = (value, encodedSessionId) => {
|
|
917
|
-
const inner = value.endsWith("]") ? value.slice(0, -1) : value;
|
|
918
|
-
const countSeparator = inner.lastIndexOf("_");
|
|
919
|
-
if (countSeparator <= 0) return false;
|
|
920
|
-
const prefix = inner.slice(0, countSeparator);
|
|
921
|
-
const namespaceSeparator = prefix.lastIndexOf("_");
|
|
922
|
-
if (namespaceSeparator <= 0) return false;
|
|
923
|
-
return prefix.slice(namespaceSeparator + 1) === encodedSessionId;
|
|
924
|
-
};
|
|
925
|
-
const planBlockRestoration = ({ text, encodedSessionId, restoreCandidate, budget }) => {
|
|
926
|
-
const replacements = [];
|
|
927
|
-
let start;
|
|
928
|
-
for (let cursor = 0; cursor < text.length; cursor += 1) {
|
|
929
|
-
const character = text.at(cursor);
|
|
930
|
-
if (character === "[") {
|
|
931
|
-
if (start !== void 0 && isOwnedPlaceholderCandidate(text.slice(start + 1, cursor), encodedSessionId)) throw restorationError(DOCX_RESTORATION_ERROR_CODES.invalidPlaceholder, "DOCX text contains an incomplete placeholder for the expected session");
|
|
932
|
-
start = cursor;
|
|
933
|
-
continue;
|
|
934
|
-
}
|
|
935
|
-
if (character !== "]" || start === void 0) continue;
|
|
936
|
-
const candidateEnd = cursor + 1;
|
|
937
|
-
const candidate = text.slice(start, candidateEnd);
|
|
938
|
-
budget.candidateCount += 1;
|
|
939
|
-
if (budget.candidateCount > DOCX_RESTORE_MAX_CANDIDATES) throw restorationError(DOCX_RESTORATION_ERROR_CODES.restorationLimitExceeded, `DOCX restoration must not inspect more than ${DOCX_RESTORE_MAX_CANDIDATES} placeholder candidates`);
|
|
940
|
-
const isOwned = isOwnedPlaceholderCandidate(candidate.slice(1), encodedSessionId);
|
|
941
|
-
if (candidate.length > DOCX_RESTORE_MAX_PLACEHOLDER_UTF16) {
|
|
942
|
-
if (isOwned) throw restorationError(DOCX_RESTORATION_ERROR_CODES.invalidPlaceholder, "DOCX session placeholder exceeds the maximum length");
|
|
943
|
-
start = void 0;
|
|
944
|
-
continue;
|
|
945
|
-
}
|
|
946
|
-
if (!isOwned) {
|
|
947
|
-
start = void 0;
|
|
948
|
-
continue;
|
|
949
|
-
}
|
|
950
|
-
const replacement = restoreCandidate(candidate);
|
|
951
|
-
if (replacement !== candidate) replacements.push({
|
|
952
|
-
start,
|
|
953
|
-
end: candidateEnd,
|
|
954
|
-
replacement
|
|
955
|
-
});
|
|
956
|
-
else throw restorationError(DOCX_RESTORATION_ERROR_CODES.invalidPlaceholder, "DOCX text contains an unknown placeholder for the expected session");
|
|
957
|
-
start = void 0;
|
|
958
|
-
}
|
|
959
|
-
if (start !== void 0 && isOwnedPlaceholderCandidate(text.slice(start + 1), encodedSessionId)) throw restorationError(DOCX_RESTORATION_ERROR_CODES.invalidPlaceholder, "DOCX text contains an incomplete placeholder for the expected session");
|
|
960
|
-
return replacements;
|
|
961
|
-
};
|
|
962
221
|
const restoreDocxText = ({ document, session, expectedSessionId, observedAtEpochSeconds }) => {
|
|
963
222
|
const sessionId = session.sessionId();
|
|
964
223
|
if (sessionId !== expectedSessionId) throw restorationError(DOCX_RESTORATION_ERROR_CODES.sessionMismatch, "DOCX restoration session does not match the expected session id");
|
|
@@ -974,23 +233,40 @@ const restoreDocxText = ({ document, session, expectedSessionId, observedAtEpoch
|
|
|
974
233
|
restoredCandidates.set(candidate, restored);
|
|
975
234
|
return restored;
|
|
976
235
|
};
|
|
977
|
-
const
|
|
978
|
-
|
|
236
|
+
const planRestoration = loadNativeAnonymizeBinding().planDocxRestorationJson;
|
|
237
|
+
if (planRestoration === void 0) throw restorationError(DOCX_RESTORATION_ERROR_CODES.invalidSession, "Native anonymize binding does not expose DOCX restoration planning");
|
|
238
|
+
let plan;
|
|
239
|
+
try {
|
|
240
|
+
plan = JSON.parse(planRestoration(document, sessionId));
|
|
241
|
+
} catch (error) {
|
|
242
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
243
|
+
const separator = message.indexOf(": ");
|
|
244
|
+
const code = message.slice(0, separator);
|
|
245
|
+
const knownCodes = /* @__PURE__ */ new Set([
|
|
246
|
+
DOCX_RESTORATION_ERROR_CODES.invalidPlaceholder,
|
|
247
|
+
DOCX_RESTORATION_ERROR_CODES.restorationLimitExceeded,
|
|
248
|
+
DOCX_RESTORATION_ERROR_CODES.unsupportedDocument
|
|
249
|
+
]);
|
|
250
|
+
if (separator > 0 && knownCodes.has(code)) throw restorationError(code, message.slice(separator + 2));
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
979
253
|
const rewrites = [];
|
|
980
|
-
const budget = { candidateCount: 0 };
|
|
981
254
|
let restoredPlaceholderCount = 0;
|
|
982
|
-
for (const block of
|
|
983
|
-
const replacements =
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
255
|
+
for (const block of plan.blocks) {
|
|
256
|
+
const replacements = block.candidates.map(({ candidate, end, start }) => {
|
|
257
|
+
const replacement = restoreCandidate(candidate);
|
|
258
|
+
if (replacement === candidate) throw restorationError(DOCX_RESTORATION_ERROR_CODES.invalidPlaceholder, "DOCX text contains an unknown placeholder for the expected session");
|
|
259
|
+
return {
|
|
260
|
+
start,
|
|
261
|
+
end,
|
|
262
|
+
replacement
|
|
263
|
+
};
|
|
988
264
|
});
|
|
989
265
|
if (replacements.length === 0) continue;
|
|
990
266
|
restoredPlaceholderCount += replacements.length;
|
|
991
267
|
rewrites.push({
|
|
992
268
|
location: block.location,
|
|
993
|
-
expectedText: block.
|
|
269
|
+
expectedText: block.expectedText,
|
|
994
270
|
replacements
|
|
995
271
|
});
|
|
996
272
|
}
|
|
@@ -1001,10 +277,21 @@ const restoreDocxText = ({ document, session, expectedSessionId, observedAtEpoch
|
|
|
1001
277
|
sessionId,
|
|
1002
278
|
restoredBlockCount: restored.rewrittenBlockCount,
|
|
1003
279
|
restoredPlaceholderCount,
|
|
1004
|
-
coverage: docxWorkflowCoverage(extraction.coverage)
|
|
280
|
+
coverage: docxWorkflowCoverage(plan.extraction.coverage)
|
|
1005
281
|
};
|
|
1006
282
|
};
|
|
1007
283
|
//#endregion
|
|
284
|
+
//#region src/location.ts
|
|
285
|
+
const arraysEqual = (left, right) => left.length === right.length && left.every((value, index) => value === right.at(index));
|
|
286
|
+
const docxLocationsEqual = (left, right) => {
|
|
287
|
+
if (left.type !== right.type || left.part.type !== right.part.type || left.part.path !== right.part.path || left.blockIndex !== right.blockIndex || !arraysEqual(left.xmlPath, right.xmlPath)) return false;
|
|
288
|
+
if (left.type === "paragraph" && right.type === "paragraph") return true;
|
|
289
|
+
if (left.type === "table-cell-paragraph" && right.type === "table-cell-paragraph") return arraysEqual(left.tablePath, right.tablePath) && arraysEqual(left.rowPath, right.rowPath) && arraysEqual(left.cellPath, right.cellPath);
|
|
290
|
+
if (left.type === "text-box-paragraph" && right.type === "text-box-paragraph") return arraysEqual(left.textBoxPath, right.textBoxPath);
|
|
291
|
+
return false;
|
|
292
|
+
};
|
|
293
|
+
const docxLocationKey = ({ blockIndex, part }) => `${part.path}\0${blockIndex}`;
|
|
294
|
+
//#endregion
|
|
1008
295
|
//#region src/anonymize.ts
|
|
1009
296
|
const DOCX_ANONYMIZATION_MAX_CALLER_DETECTIONS = 1e6;
|
|
1010
297
|
var DocxAnonymizationError = class extends Error {
|