@stll/folio-core 0.10.0 → 0.12.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/dist/ai-edits/apply.js +602 -50
- package/dist/ai-edits/block-identity.d.ts +4 -0
- package/dist/ai-edits/block-identity.js +9 -0
- package/dist/ai-edits/blockRange.d.ts +28 -1
- package/dist/ai-edits/blockRange.js +67 -1
- package/dist/ai-edits/headless.d.ts +4 -4
- package/dist/ai-edits/headless.js +9 -9
- package/dist/ai-edits/index.d.ts +2 -2
- package/dist/ai-edits/index.js +2 -2
- package/dist/ai-edits/read.d.ts +7 -7
- package/dist/ai-edits/read.js +110 -3
- package/dist/ai-edits/table-cell-mutations.d.ts +48 -0
- package/dist/ai-edits/table-cell-mutations.js +237 -0
- package/dist/ai-edits/table-mutation-plan.d.ts +38 -0
- package/dist/ai-edits/table-mutation-plan.js +90 -0
- package/dist/ai-edits/table-row-column-mutations.d.ts +100 -0
- package/dist/ai-edits/table-row-column-mutations.js +300 -0
- package/dist/ai-edits/table-targets.d.ts +32 -0
- package/dist/ai-edits/table-targets.js +70 -0
- package/dist/ai-edits/types.d.ts +34 -0
- package/dist/compat/eigenpal.js +1 -1
- package/dist/content-controls/mutateContentControls.js +1 -1
- package/dist/controller/activeEditorStory.d.ts +36 -0
- package/dist/controller/activeEditorStory.js +30 -0
- package/dist/controller/collaborationModules.d.ts +7 -0
- package/dist/controller/collaborationModules.js +15 -0
- package/dist/controller/contentControlWidgetController.d.ts +55 -0
- package/dist/controller/contentControlWidgetController.js +141 -0
- package/dist/controller/folioEditor.d.ts +1 -1
- package/dist/controller/fontReadiness.js +2 -0
- package/dist/controller/headerFooterEditorManager.d.ts +41 -0
- package/dist/controller/headerFooterEditorManager.js +182 -0
- package/dist/controller/hiddenEditorApi.js +2 -2
- package/dist/controller/hiddenEditorManager.d.ts +13 -2
- package/dist/controller/hiddenEditorManager.js +32 -11
- package/dist/controller/layoutPipeline.d.ts +2 -0
- package/dist/controller/layoutPipeline.js +16 -1
- package/dist/controller/noteEditorManager.d.ts +36 -0
- package/dist/controller/noteEditorManager.js +210 -0
- package/dist/document-operations.d.ts +35 -3
- package/dist/document-operations.js +161 -3
- package/dist/docx/blockContentParser.js +2 -2
- package/dist/docx/commentParser.js +6 -1
- package/dist/docx/conformance.js +8 -1
- package/dist/docx/documentGrid.d.ts +8 -0
- package/dist/docx/documentGrid.js +8 -0
- package/dist/docx/drawingUtils.js +1 -1
- package/dist/docx/ensureParaIds.js +1 -1
- package/dist/docx/fieldParser.js +1 -1
- package/dist/docx/notePropertiesParser.js +1 -1
- package/dist/docx/numberingParser.js +1 -1
- package/dist/docx/paragraphParser.js +63 -58
- package/dist/docx/paragraphTextBoxEnrichment.d.ts +2 -1
- package/dist/docx/paragraphTextBoxEnrichment.js +67 -13
- package/dist/docx/paragraphTraversal.js +1 -1
- package/dist/docx/parser.js +3 -3
- package/dist/docx/rezip.js +1 -1
- package/dist/docx/runParser.js +4 -2
- package/dist/docx/sdtProperties.js +1 -1
- package/dist/docx/sectionParser.js +1 -1
- package/dist/docx/selectiveSave.js +1 -1
- package/dist/docx/serializer/commentSerializer.js +1 -2
- package/dist/docx/serializer/documentSerializer.js +2 -2
- package/dist/docx/serializer/headerFooterSerializer.js +2 -2
- package/dist/docx/serializer/noteSerializer.js +2 -2
- package/dist/docx/serializer/paragraphSerializer.js +7 -1
- package/dist/docx/serializer/runSerializer.js +4 -2
- package/dist/docx/serializer/stylesSerializer.js +1 -1
- package/dist/docx/serializer/tableSerializer.d.ts +5 -4
- package/dist/docx/serializer/tableSerializer.js +17 -13
- package/dist/docx/server/applyDocxXmlPatchProposal.d.ts +87 -0
- package/dist/docx/server/applyDocxXmlPatchProposal.js +204 -0
- package/dist/docx/server/boundedArchive.d.ts +14 -3
- package/dist/docx/server/boundedArchive.js +56 -11
- package/dist/docx/server/evaluateDocxXmlPatchProposal.d.ts +85 -0
- package/dist/docx/server/evaluateDocxXmlPatchProposal.js +361 -0
- package/dist/docx/server/inspectDocxPackage.d.ts +70 -0
- package/dist/docx/server/inspectDocxPackage.js +217 -0
- package/dist/docx/server/validateDocxConformance.d.ts +41 -0
- package/dist/docx/server/validateDocxConformance.js +336 -0
- package/dist/docx/settingsParser.js +11 -1
- package/dist/docx/shapeParser.js +1 -1
- package/dist/docx/styleParser.js +4 -2
- package/dist/docx/tableParser.js +18 -7
- package/dist/docx/textBoxParser.d.ts +3 -3
- package/dist/docx/textBoxParser.js +16 -12
- package/dist/docx/xmlSafety.d.ts +5 -0
- package/dist/docx/xmlSafety.js +9 -0
- package/dist/fields/resolveFieldValues.js +1 -3
- package/dist/i18n/messages/catalogs.gen.d.ts +17 -0
- package/dist/i18n/messages/catalogs.gen.js +17 -0
- package/dist/i18n/messages/messages.gen.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/layout-bridge/convert/footnoteLayout.d.ts +1 -0
- package/dist/layout-bridge/convert/footnoteLayout.js +47 -40
- package/dist/layout-bridge/convert/headerFooterLayout.d.ts +1 -0
- package/dist/layout-bridge/convert/headerFooterLayout.js +3 -0
- package/dist/layout-bridge/convert/templatePreviewFlow.js +3 -3
- package/dist/layout-bridge/convert/toFlowBlocks.d.ts +4 -2
- package/dist/layout-bridge/convert/toFlowBlocks.js +136 -20
- package/dist/layout-bridge/dom/noteStoryDom.d.ts +6 -0
- package/dist/layout-bridge/dom/noteStoryDom.js +17 -0
- package/dist/layout-bridge/engine/clickToPosition.js +2 -2
- package/dist/layout-engine/index.d.ts +2 -2
- package/dist/layout-engine/index.js +43 -28
- package/dist/layout-engine/justificationCompatibility.d.ts +6 -0
- package/dist/layout-engine/justificationCompatibility.js +8 -0
- package/dist/layout-engine/keep-together.js +11 -1
- package/dist/layout-engine/measure/cache.js +21 -5
- package/dist/layout-engine/measure/lineBreakProvider.js +53 -34
- package/dist/layout-engine/measure/measureBlocks.js +6 -3
- package/dist/layout-engine/measure/measureContainer.js +1 -1
- package/dist/layout-engine/measure/measureHelpers.d.ts +1 -1
- package/dist/layout-engine/measure/measureHelpers.js +3 -3
- package/dist/layout-engine/measure/measureParagraph.js +69 -25
- package/dist/layout-engine/measure/measureProvider.js +4 -3
- package/dist/layout-engine/measure/tableCellFlow.js +5 -2
- package/dist/layout-engine/measure/textBoxParagraphLayout.d.ts +9 -7
- package/dist/layout-engine/measure/textBoxParagraphLayout.js +22 -6
- package/dist/layout-engine/measure/textMeasurementPolicy.d.ts +3 -1
- package/dist/layout-engine/measure/textMeasurementPolicy.js +7 -1
- package/dist/layout-engine/paginator.js +5 -1
- package/dist/layout-engine/paragraphSpacing.d.ts +28 -4
- package/dist/layout-engine/paragraphSpacing.js +107 -13
- package/dist/layout-engine/renderedBreakReconciliation.js +1 -1
- package/dist/layout-engine/types.d.ts +26 -13
- package/dist/layout-painter/registry/modules/textBox.js +5 -1
- package/dist/layout-painter/renderPage.d.ts +5 -2
- package/dist/layout-painter/renderPage.js +56 -12
- package/dist/layout-painter/renderParagraph.js +22 -6
- package/dist/layout-painter/renderTable.d.ts +5 -1
- package/dist/layout-painter/renderTable.js +5 -2
- package/dist/layout-painter/renderTextBox.d.ts +2 -1
- package/dist/layout-painter/renderTextBox.js +25 -14
- package/dist/managers/DocumentLoaderManager.js +1 -1
- package/dist/managers/FindReplaceManager.d.ts +8 -0
- package/dist/managers/FindReplaceManager.js +18 -0
- package/dist/managers/TableSelectionManager.d.ts +6 -0
- package/dist/managers/TableSelectionManager.js +14 -0
- package/dist/model.js +1 -1
- package/dist/prosemirror/attrs/index.d.ts +4 -2
- package/dist/prosemirror/attrs/index.js +154 -18
- package/dist/prosemirror/commands/comments.d.ts +3 -3
- package/dist/prosemirror/commands/comments.js +248 -12
- package/dist/prosemirror/commands/contentControls.js +1 -1
- package/dist/prosemirror/commands/index.js +1 -1
- package/dist/prosemirror/commands/pageBreak.js +1 -1
- package/dist/prosemirror/commands/propertyChangeScope.d.ts +2 -2
- package/dist/prosemirror/commands/propertyChangeScope.js +3 -2
- package/dist/prosemirror/commands/tableCellMergeResolution.d.ts +9 -0
- package/dist/prosemirror/commands/tableCellMergeResolution.js +181 -0
- package/dist/prosemirror/conversion/fromProseDoc.d.ts +2 -1
- package/dist/prosemirror/conversion/fromProseDoc.js +286 -61
- package/dist/prosemirror/conversion/sdtAttrs.d.ts +9 -0
- package/dist/prosemirror/conversion/sdtAttrs.js +49 -0
- package/dist/prosemirror/conversion/toProseDoc.d.ts +2 -1
- package/dist/prosemirror/conversion/toProseDoc.js +347 -98
- package/dist/prosemirror/extensions/ExtensionManager.js +1 -1
- package/dist/prosemirror/extensions/StarterKit.js +6 -1
- package/dist/prosemirror/extensions/core/ParagraphExtension.js +4 -1
- package/dist/prosemirror/extensions/features/BaseKeymapExtension.js +1 -0
- package/dist/prosemirror/extensions/features/ListExtension.js +1 -1
- package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.d.ts +10 -1
- package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +93 -2
- package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.d.ts +2 -1
- package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +6 -2
- package/dist/prosemirror/extensions/marks/CharacterSpacingExtension.js +1 -1
- package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.d.ts +9 -1
- package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +24 -1
- package/dist/prosemirror/extensions/nodes/FieldExtension.js +1 -1
- package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.d.ts +7 -0
- package/dist/prosemirror/extensions/nodes/RenderedPageBreakExtension.js +22 -0
- package/dist/prosemirror/extensions/nodes/TableExtension.js +9 -3
- package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.d.ts +13 -0
- package/dist/prosemirror/extensions/nodes/TextBoxAnchorExtension.js +41 -0
- package/dist/prosemirror/extensions/nodes/TextBoxExtension.d.ts +5 -2
- package/dist/prosemirror/extensions/nodes/TextBoxExtension.js +4 -1
- package/dist/prosemirror/findReplaceSelection.d.ts +6 -1
- package/dist/prosemirror/findReplaceSelection.js +57 -13
- package/dist/prosemirror/plugins/contentControlWidgets.d.ts +34 -8
- package/dist/prosemirror/plugins/contentControlWidgets.js +125 -91
- package/dist/prosemirror/plugins/selectionTracker.js +1 -0
- package/dist/prosemirror/plugins/suggestionMode.js +1 -1
- package/dist/prosemirror/schema/index.d.ts +2 -2
- package/dist/prosemirror/schema/marks.d.ts +5 -1
- package/dist/prosemirror/schema/nodes.d.ts +64 -7
- package/dist/prosemirror/selectionState.js +1 -0
- package/dist/prosemirror/styles/resolvedStyleAttrs.js +1 -0
- package/dist/prosemirror/tableCellMergeRevision.d.ts +9 -0
- package/dist/prosemirror/tableCellMergeRevision.js +22 -0
- package/dist/prosemirror/textBoxAnchorAttrs.d.ts +9 -0
- package/dist/prosemirror/textBoxAnchorAttrs.js +33 -0
- package/dist/prosemirror/utils/extractTrackedChanges.d.ts +1 -1
- package/dist/prosemirror/utils/extractTrackedChanges.js +62 -14
- package/dist/prosemirror/validation.js +9 -1
- package/dist/render-dom/BodySelectionOverlay.d.ts +31 -0
- package/dist/render-dom/BodySelectionOverlay.js +76 -0
- package/dist/render-dom/HeaderFooterSelectionOverlay.d.ts +28 -0
- package/dist/render-dom/HeaderFooterSelectionOverlay.js +121 -0
- package/dist/render-dom/RemoteSelectionOverlay.d.ts +30 -0
- package/dist/render-dom/RemoteSelectionOverlay.js +67 -0
- package/dist/render-dom/RenderedDomContext.d.ts +40 -0
- package/dist/render-dom/RenderedDomContext.js +126 -0
- package/dist/render-dom/resolveSidebarItemPositions.d.ts +33 -0
- package/dist/render-dom/resolveSidebarItemPositions.js +45 -0
- package/dist/server.d.ts +6 -2
- package/dist/server.js +5 -1
- package/dist/utils/findReplace.d.ts +32 -0
- package/dist/utils/findReplace.js +118 -0
- package/dist/utils/fontFamilyMerge.js +9 -11
- package/dist/utils/fontLoader.js +3 -2
- package/dist/utils/fontResolver.js +22 -4
- package/dist/utils/fontWeights.d.ts +1 -1
- package/dist/utils/fontWeights.js +1 -1
- package/dist/utils/headerFooter.js +24 -1
- package/dist/utils/paragraphFormattingMerge.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { DocxArchiveError } from "./boundedArchive.js";
|
|
2
|
+
import { getDocxXmlSafetyIssue } from "../xmlSafety.js";
|
|
3
|
+
import { FolioDocxPackageInspectionError, inspectDocxPackage } from "./inspectDocxPackage.js";
|
|
4
|
+
import { TaggedError, panic } from "better-result";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
//#region src/docx/server/evaluateDocxXmlPatchProposal.ts
|
|
7
|
+
const FOLIO_DOCX_XML_PATCH_PROPOSAL_VERSION = 1;
|
|
8
|
+
const FOLIO_DOCX_XML_PATCH_PROPOSAL_PROFILE = "folio-xml-patch-proposal-v1";
|
|
9
|
+
const FOLIO_DOCX_XML_PATCH_PROPOSAL_DEFAULTS = Object.freeze({
|
|
10
|
+
maxReplacements: 16,
|
|
11
|
+
maxPartBytes: 8 * 1024 * 1024,
|
|
12
|
+
maxTotalBytes: 16 * 1024 * 1024
|
|
13
|
+
});
|
|
14
|
+
const FOLIO_DOCX_XML_PATCH_PROPOSAL_ISSUE_CODES = Object.freeze([
|
|
15
|
+
"invalid-proposal",
|
|
16
|
+
"unsupported-version",
|
|
17
|
+
"too-many-replacements",
|
|
18
|
+
"duplicate-part",
|
|
19
|
+
"invalid-part-path",
|
|
20
|
+
"invalid-base-sha256",
|
|
21
|
+
"part-not-allowed",
|
|
22
|
+
"part-not-found",
|
|
23
|
+
"part-not-xml",
|
|
24
|
+
"replacement-too-large",
|
|
25
|
+
"replacements-too-large",
|
|
26
|
+
"xml-doctype-forbidden",
|
|
27
|
+
"xml-not-well-formed",
|
|
28
|
+
"xml-encoding-mismatch",
|
|
29
|
+
"base-hash-mismatch",
|
|
30
|
+
"package-inspection-failed"
|
|
31
|
+
]);
|
|
32
|
+
var InvalidFolioDocxXmlPatchProposalError = class extends TaggedError("InvalidFolioDocxXmlPatchProposalError")() {};
|
|
33
|
+
var InvalidFolioDocxXmlPatchProposalOptionsError = class extends TaggedError("InvalidFolioDocxXmlPatchProposalOptionsError")() {};
|
|
34
|
+
const SHA256_PATTERN = /^[\da-f]{64}$/u;
|
|
35
|
+
const XML_DECLARED_ENCODING_PATTERN = /^\uFEFF?<\?xml[^>]*\bencoding\s*=\s*["']([^"']+)["'][^>]*\?>/iu;
|
|
36
|
+
const MAX_PACKAGE_PATH_CODE_UNITS = 1024;
|
|
37
|
+
const prepareXmlReplacement = ({ replacement, encodedByPath, inspectedByPath }) => {
|
|
38
|
+
const replacementBytes = encodedByPath.get(replacement.path);
|
|
39
|
+
const current = inspectedByPath.get(replacement.path);
|
|
40
|
+
if (replacementBytes === void 0 || current === void 0) return panic("Accepted XML replacement is missing prepared package data");
|
|
41
|
+
return {
|
|
42
|
+
path: replacement.path,
|
|
43
|
+
baseSha256: replacement.baseSha256,
|
|
44
|
+
currentSha256: current.sha256,
|
|
45
|
+
replacementSha256: createHash("sha256").update(replacementBytes).digest("hex"),
|
|
46
|
+
replacementByteLength: replacementBytes.byteLength,
|
|
47
|
+
encoding: "utf-8"
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
51
|
+
const invalidProposal = (path, reason) => {
|
|
52
|
+
throw new InvalidFolioDocxXmlPatchProposalError({
|
|
53
|
+
message: `Invalid XML patch proposal at ${path}: ${reason}.`,
|
|
54
|
+
path,
|
|
55
|
+
reason
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
const assertAllowedKeys = (value, path, allowedKeys) => {
|
|
59
|
+
const allowed = new Set(allowedKeys);
|
|
60
|
+
const unexpected = Object.keys(value).find((key) => !allowed.has(key));
|
|
61
|
+
if (unexpected !== void 0) invalidProposal(`${path}.${unexpected}`, "unexpected property");
|
|
62
|
+
};
|
|
63
|
+
const readString = (value, key, path) => {
|
|
64
|
+
const result = value[key];
|
|
65
|
+
if (typeof result !== "string") return invalidProposal(`${path}.${key}`, "expected a string");
|
|
66
|
+
return result;
|
|
67
|
+
};
|
|
68
|
+
const parseXmlReplacement = (value, index) => {
|
|
69
|
+
const path = `$.replacements[${index}]`;
|
|
70
|
+
if (!isPlainObject(value)) return invalidProposal(path, "expected an object");
|
|
71
|
+
assertAllowedKeys(value, path, [
|
|
72
|
+
"path",
|
|
73
|
+
"baseSha256",
|
|
74
|
+
"replacementXml"
|
|
75
|
+
]);
|
|
76
|
+
return {
|
|
77
|
+
path: readString(value, "path", path),
|
|
78
|
+
baseSha256: readString(value, "baseSha256", path),
|
|
79
|
+
replacementXml: readString(value, "replacementXml", path)
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
const parseFolioDocxXmlPatchProposal = (value) => {
|
|
83
|
+
if (!isPlainObject(value)) return invalidProposal("$", "expected an object");
|
|
84
|
+
assertAllowedKeys(value, "$", ["version", "replacements"]);
|
|
85
|
+
const version = value["version"];
|
|
86
|
+
if (typeof version !== "number" || !Number.isSafeInteger(version)) return invalidProposal("$.version", "expected an integer contract version");
|
|
87
|
+
if (version !== 1) return invalidProposal("$.version", "unsupported contract version");
|
|
88
|
+
const replacements = value["replacements"];
|
|
89
|
+
if (!Array.isArray(replacements) || replacements.length === 0) return invalidProposal("$.replacements", "expected a non-empty array");
|
|
90
|
+
if (replacements.length > FOLIO_DOCX_XML_PATCH_PROPOSAL_DEFAULTS.maxReplacements) return invalidProposal("$.replacements", `expected at most ${FOLIO_DOCX_XML_PATCH_PROPOSAL_DEFAULTS.maxReplacements} replacements`);
|
|
91
|
+
const firstReplacement = replacements.at(0);
|
|
92
|
+
if (firstReplacement === void 0) return invalidProposal("$.replacements", "expected a non-empty array");
|
|
93
|
+
return {
|
|
94
|
+
version: 1,
|
|
95
|
+
replacements: [parseXmlReplacement(firstReplacement, 0), ...replacements.slice(1).map((replacement, index) => parseXmlReplacement(replacement, index + 1))]
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
const resolveLimit = ({ value, fallback, path }) => {
|
|
99
|
+
const limit = value ?? fallback;
|
|
100
|
+
if (!Number.isSafeInteger(limit) || limit < 0) throw new InvalidFolioDocxXmlPatchProposalOptionsError({
|
|
101
|
+
message: `${path} must be a non-negative safe integer.`,
|
|
102
|
+
path
|
|
103
|
+
});
|
|
104
|
+
return limit;
|
|
105
|
+
};
|
|
106
|
+
const resolveLimits = (limits) => {
|
|
107
|
+
const maxReplacements = resolveLimit({
|
|
108
|
+
value: limits?.maxReplacements,
|
|
109
|
+
fallback: FOLIO_DOCX_XML_PATCH_PROPOSAL_DEFAULTS.maxReplacements,
|
|
110
|
+
path: "limits.maxReplacements"
|
|
111
|
+
});
|
|
112
|
+
if (maxReplacements > FOLIO_DOCX_XML_PATCH_PROPOSAL_DEFAULTS.maxReplacements) throw new InvalidFolioDocxXmlPatchProposalOptionsError({
|
|
113
|
+
message: `limits.maxReplacements must not exceed ${FOLIO_DOCX_XML_PATCH_PROPOSAL_DEFAULTS.maxReplacements}.`,
|
|
114
|
+
path: "limits.maxReplacements"
|
|
115
|
+
});
|
|
116
|
+
return {
|
|
117
|
+
maxReplacements,
|
|
118
|
+
maxPartBytes: resolveLimit({
|
|
119
|
+
value: limits?.maxPartBytes,
|
|
120
|
+
fallback: FOLIO_DOCX_XML_PATCH_PROPOSAL_DEFAULTS.maxPartBytes,
|
|
121
|
+
path: "limits.maxPartBytes"
|
|
122
|
+
}),
|
|
123
|
+
maxTotalBytes: resolveLimit({
|
|
124
|
+
value: limits?.maxTotalBytes,
|
|
125
|
+
fallback: FOLIO_DOCX_XML_PATCH_PROPOSAL_DEFAULTS.maxTotalBytes,
|
|
126
|
+
path: "limits.maxTotalBytes"
|
|
127
|
+
})
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
const isNormalizedPackagePath = (path) => path.length > 0 && path.length <= MAX_PACKAGE_PATH_CODE_UNITS && !path.startsWith("/") && !path.endsWith("/") && !path.includes("\\") && path.split("/").every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
|
|
131
|
+
const resolveAllowedParts = (allowedParts) => {
|
|
132
|
+
const resolved = /* @__PURE__ */ new Set();
|
|
133
|
+
for (const [index, path] of allowedParts.entries()) {
|
|
134
|
+
if (typeof path !== "string" || !isNormalizedPackagePath(path)) throw new InvalidFolioDocxXmlPatchProposalOptionsError({
|
|
135
|
+
message: `allowedParts[${index}] must be a normalized package path.`,
|
|
136
|
+
path: `allowedParts[${index}]`
|
|
137
|
+
});
|
|
138
|
+
if (resolved.has(path)) throw new InvalidFolioDocxXmlPatchProposalOptionsError({
|
|
139
|
+
message: `allowedParts[${index}] must be unique.`,
|
|
140
|
+
path: `allowedParts[${index}]`
|
|
141
|
+
});
|
|
142
|
+
resolved.add(path);
|
|
143
|
+
}
|
|
144
|
+
return resolved;
|
|
145
|
+
};
|
|
146
|
+
const rejectedEvaluation = ({ issues, limits }) => ({
|
|
147
|
+
version: 1,
|
|
148
|
+
profile: FOLIO_DOCX_XML_PATCH_PROPOSAL_PROFILE,
|
|
149
|
+
status: "rejected",
|
|
150
|
+
producesOutput: false,
|
|
151
|
+
issues,
|
|
152
|
+
replacements: [],
|
|
153
|
+
limits
|
|
154
|
+
});
|
|
155
|
+
const acceptedEvaluation = ({ replacements, limits }) => ({
|
|
156
|
+
version: 1,
|
|
157
|
+
profile: FOLIO_DOCX_XML_PATCH_PROPOSAL_PROFILE,
|
|
158
|
+
status: "accepted",
|
|
159
|
+
producesOutput: false,
|
|
160
|
+
issues: [],
|
|
161
|
+
replacements,
|
|
162
|
+
limits
|
|
163
|
+
});
|
|
164
|
+
const inspectionIssue = (error) => {
|
|
165
|
+
if (error.code === "part-not-found" || error.code === "part-not-xml") return {
|
|
166
|
+
code: error.code,
|
|
167
|
+
message: error.message,
|
|
168
|
+
...error.part !== void 0 && { part: error.part }
|
|
169
|
+
};
|
|
170
|
+
return {
|
|
171
|
+
code: "package-inspection-failed",
|
|
172
|
+
message: "The package could not be inspected within the configured limits.",
|
|
173
|
+
...error.part !== void 0 && { part: error.part }
|
|
174
|
+
};
|
|
175
|
+
};
|
|
176
|
+
const evaluateDocxXmlPatchProposal = async ({ bytes, proposal: rawProposal, allowedParts, archive, limits: rawLimits }) => {
|
|
177
|
+
const limits = resolveLimits(rawLimits);
|
|
178
|
+
const allowed = resolveAllowedParts(allowedParts);
|
|
179
|
+
const rawReplacements = isPlainObject(rawProposal) ? rawProposal["replacements"] : void 0;
|
|
180
|
+
if (isPlainObject(rawProposal) && rawProposal["version"] === 1 && Array.isArray(rawReplacements) && rawReplacements.length > limits.maxReplacements) return rejectedEvaluation({
|
|
181
|
+
issues: [{
|
|
182
|
+
code: "too-many-replacements",
|
|
183
|
+
message: `Proposal contains ${rawReplacements.length} replacements (max ${limits.maxReplacements}).`,
|
|
184
|
+
proposalPath: "$.replacements"
|
|
185
|
+
}],
|
|
186
|
+
limits
|
|
187
|
+
});
|
|
188
|
+
let proposal;
|
|
189
|
+
try {
|
|
190
|
+
proposal = parseFolioDocxXmlPatchProposal(rawProposal);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
if (!(error instanceof InvalidFolioDocxXmlPatchProposalError)) throw error;
|
|
193
|
+
return rejectedEvaluation({
|
|
194
|
+
issues: [{
|
|
195
|
+
code: error.path === "$.version" && isPlainObject(rawProposal) && typeof rawProposal["version"] === "number" && Number.isSafeInteger(rawProposal["version"]) ? "unsupported-version" : "invalid-proposal",
|
|
196
|
+
message: error.message,
|
|
197
|
+
proposalPath: error.path
|
|
198
|
+
}],
|
|
199
|
+
limits
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
const issues = [];
|
|
203
|
+
const seen = /* @__PURE__ */ new Set();
|
|
204
|
+
const encodedByPath = /* @__PURE__ */ new Map();
|
|
205
|
+
let encodedTotalBytes = 0;
|
|
206
|
+
let totalCodeUnits = 0;
|
|
207
|
+
let totalLimitReported = false;
|
|
208
|
+
for (const [index, replacement] of proposal.replacements.entries()) {
|
|
209
|
+
const proposalPath = `$.replacements[${index}]`;
|
|
210
|
+
if (seen.has(replacement.path)) issues.push({
|
|
211
|
+
code: "duplicate-part",
|
|
212
|
+
message: "Each package part may appear only once in a proposal.",
|
|
213
|
+
proposalPath: `${proposalPath}.path`,
|
|
214
|
+
part: replacement.path
|
|
215
|
+
});
|
|
216
|
+
seen.add(replacement.path);
|
|
217
|
+
if (!isNormalizedPackagePath(replacement.path)) issues.push({
|
|
218
|
+
code: "invalid-part-path",
|
|
219
|
+
message: "Replacement paths must be normalized package paths.",
|
|
220
|
+
proposalPath: `${proposalPath}.path`,
|
|
221
|
+
part: replacement.path
|
|
222
|
+
});
|
|
223
|
+
else if (!allowed.has(replacement.path)) issues.push({
|
|
224
|
+
code: "part-not-allowed",
|
|
225
|
+
message: "The server-side policy does not allow replacing this package part.",
|
|
226
|
+
proposalPath: `${proposalPath}.path`,
|
|
227
|
+
part: replacement.path
|
|
228
|
+
});
|
|
229
|
+
if (replacement.baseSha256.length !== 64 || !SHA256_PATTERN.test(replacement.baseSha256)) issues.push({
|
|
230
|
+
code: "invalid-base-sha256",
|
|
231
|
+
message: "baseSha256 must be a lowercase hexadecimal SHA-256 digest.",
|
|
232
|
+
proposalPath: `${proposalPath}.baseSha256`,
|
|
233
|
+
part: replacement.path
|
|
234
|
+
});
|
|
235
|
+
const replacementCodeUnits = replacement.replacementXml.length;
|
|
236
|
+
const exceedsPartLowerBound = replacementCodeUnits > limits.maxPartBytes;
|
|
237
|
+
if (exceedsPartLowerBound) issues.push({
|
|
238
|
+
code: "replacement-too-large",
|
|
239
|
+
message: `Replacement XML exceeds the ${limits.maxPartBytes}-byte per-part limit.`,
|
|
240
|
+
proposalPath: `${proposalPath}.replacementXml`,
|
|
241
|
+
part: replacement.path
|
|
242
|
+
});
|
|
243
|
+
totalCodeUnits += replacementCodeUnits;
|
|
244
|
+
if (totalCodeUnits > limits.maxTotalBytes && !totalLimitReported) {
|
|
245
|
+
totalLimitReported = true;
|
|
246
|
+
issues.push({
|
|
247
|
+
code: "replacements-too-large",
|
|
248
|
+
message: `Replacement XML exceeds the ${limits.maxTotalBytes}-byte cumulative limit.`,
|
|
249
|
+
proposalPath: "$.replacements"
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
if (exceedsPartLowerBound || totalLimitReported) continue;
|
|
253
|
+
const replacementBytes = new TextEncoder().encode(replacement.replacementXml);
|
|
254
|
+
const exceedsEncodedPart = replacementBytes.byteLength > limits.maxPartBytes;
|
|
255
|
+
const nextEncodedTotalBytes = encodedTotalBytes + replacementBytes.byteLength;
|
|
256
|
+
if (exceedsEncodedPart) issues.push({
|
|
257
|
+
code: "replacement-too-large",
|
|
258
|
+
message: `Replacement XML exceeds the ${limits.maxPartBytes}-byte per-part limit.`,
|
|
259
|
+
proposalPath: `${proposalPath}.replacementXml`,
|
|
260
|
+
part: replacement.path
|
|
261
|
+
});
|
|
262
|
+
if (nextEncodedTotalBytes > limits.maxTotalBytes) {
|
|
263
|
+
totalLimitReported = true;
|
|
264
|
+
issues.push({
|
|
265
|
+
code: "replacements-too-large",
|
|
266
|
+
message: `Replacement XML exceeds the ${limits.maxTotalBytes}-byte cumulative limit.`,
|
|
267
|
+
proposalPath: "$.replacements"
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
encodedTotalBytes = nextEncodedTotalBytes;
|
|
271
|
+
if (exceedsEncodedPart || totalLimitReported) continue;
|
|
272
|
+
encodedByPath.set(replacement.path, replacementBytes);
|
|
273
|
+
const safetyIssue = getDocxXmlSafetyIssue(replacement.replacementXml);
|
|
274
|
+
if (safetyIssue === "doctype-forbidden") issues.push({
|
|
275
|
+
code: "xml-doctype-forbidden",
|
|
276
|
+
message: "Replacement XML must not declare a document type.",
|
|
277
|
+
proposalPath: `${proposalPath}.replacementXml`,
|
|
278
|
+
part: replacement.path
|
|
279
|
+
});
|
|
280
|
+
else if (safetyIssue === "not-well-formed") issues.push({
|
|
281
|
+
code: "xml-not-well-formed",
|
|
282
|
+
message: "Replacement XML must be well formed.",
|
|
283
|
+
proposalPath: `${proposalPath}.replacementXml`,
|
|
284
|
+
part: replacement.path
|
|
285
|
+
});
|
|
286
|
+
else {
|
|
287
|
+
const declaredEncoding = replacement.replacementXml.match(XML_DECLARED_ENCODING_PATTERN)?.at(1);
|
|
288
|
+
if (declaredEncoding !== void 0 && declaredEncoding.toLowerCase() !== "utf-8" && declaredEncoding.toLowerCase() !== "utf8") issues.push({
|
|
289
|
+
code: "xml-encoding-mismatch",
|
|
290
|
+
message: "Replacement XML declarations must use UTF-8 encoding.",
|
|
291
|
+
proposalPath: `${proposalPath}.replacementXml`,
|
|
292
|
+
part: replacement.path
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (issues.length > 0) {
|
|
297
|
+
const firstIssue = issues.at(0);
|
|
298
|
+
if (firstIssue === void 0) return panic("Rejected XML patch proposal is missing an issue");
|
|
299
|
+
return rejectedEvaluation({
|
|
300
|
+
issues: [firstIssue, ...issues.slice(1)],
|
|
301
|
+
limits
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
let inspected;
|
|
305
|
+
try {
|
|
306
|
+
inspected = await inspectDocxPackage(bytes, {
|
|
307
|
+
...archive === void 0 ? {} : { archive },
|
|
308
|
+
xmlParts: proposal.replacements.map(({ path }) => path),
|
|
309
|
+
limits: {
|
|
310
|
+
maxXmlParts: limits.maxReplacements,
|
|
311
|
+
maxXmlPartBytes: limits.maxPartBytes,
|
|
312
|
+
maxXmlTotalBytes: limits.maxTotalBytes
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
} catch (error) {
|
|
316
|
+
if (error instanceof FolioDocxPackageInspectionError) return rejectedEvaluation({
|
|
317
|
+
issues: [inspectionIssue(error)],
|
|
318
|
+
limits
|
|
319
|
+
});
|
|
320
|
+
if (error instanceof DocxArchiveError) return rejectedEvaluation({
|
|
321
|
+
issues: [{
|
|
322
|
+
code: "package-inspection-failed",
|
|
323
|
+
message: "The package could not be inspected within the configured limits."
|
|
324
|
+
}],
|
|
325
|
+
limits
|
|
326
|
+
});
|
|
327
|
+
throw error;
|
|
328
|
+
}
|
|
329
|
+
const inspectedByPath = new Map(inspected.xmlParts.map((part) => [part.path, part]));
|
|
330
|
+
for (const [index, replacement] of proposal.replacements.entries()) {
|
|
331
|
+
const current = inspectedByPath.get(replacement.path);
|
|
332
|
+
if (current === void 0 || current.sha256 === replacement.baseSha256) continue;
|
|
333
|
+
issues.push({
|
|
334
|
+
code: "base-hash-mismatch",
|
|
335
|
+
message: "The package part changed after the proposal base was inspected.",
|
|
336
|
+
proposalPath: `$.replacements[${index}].baseSha256`,
|
|
337
|
+
part: replacement.path
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
if (issues.length > 0) {
|
|
341
|
+
const firstIssue = issues.at(0);
|
|
342
|
+
if (firstIssue === void 0) return panic("Rejected XML patch proposal is missing an issue");
|
|
343
|
+
return rejectedEvaluation({
|
|
344
|
+
issues: [firstIssue, ...issues.slice(1)],
|
|
345
|
+
limits
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
const preparedReplacements = proposal.replacements.map((replacement) => prepareXmlReplacement({
|
|
349
|
+
replacement,
|
|
350
|
+
encodedByPath,
|
|
351
|
+
inspectedByPath
|
|
352
|
+
}));
|
|
353
|
+
const firstPreparedReplacement = preparedReplacements.at(0);
|
|
354
|
+
if (firstPreparedReplacement === void 0) return panic("Accepted XML patch proposal is missing a prepared replacement");
|
|
355
|
+
return acceptedEvaluation({
|
|
356
|
+
replacements: [firstPreparedReplacement, ...preparedReplacements.slice(1)],
|
|
357
|
+
limits
|
|
358
|
+
});
|
|
359
|
+
};
|
|
360
|
+
//#endregion
|
|
361
|
+
export { FOLIO_DOCX_XML_PATCH_PROPOSAL_DEFAULTS, FOLIO_DOCX_XML_PATCH_PROPOSAL_ISSUE_CODES, FOLIO_DOCX_XML_PATCH_PROPOSAL_PROFILE, FOLIO_DOCX_XML_PATCH_PROPOSAL_VERSION, InvalidFolioDocxXmlPatchProposalError, InvalidFolioDocxXmlPatchProposalOptionsError, evaluateDocxXmlPatchProposal, parseFolioDocxXmlPatchProposal };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { DocxArchiveOptions } from "./boundedArchive.js";
|
|
2
|
+
|
|
3
|
+
//#region src/docx/server/inspectDocxPackage.d.ts
|
|
4
|
+
declare const FOLIO_DOCX_PACKAGE_INSPECTION_VERSION: 1;
|
|
5
|
+
declare const FOLIO_DOCX_PACKAGE_INSPECTION_DEFAULTS: Readonly<{
|
|
6
|
+
readonly maxXmlParts: 16;
|
|
7
|
+
readonly maxXmlPartBytes: number;
|
|
8
|
+
readonly maxXmlTotalBytes: number;
|
|
9
|
+
}>;
|
|
10
|
+
declare const FOLIO_DOCX_PACKAGE_INSPECTION_ERROR_CODES: readonly ["invalid-limits", "too-many-xml-parts", "duplicate-xml-part", "part-not-found", "part-not-xml", "xml-part-too-large", "xml-total-too-large", "xml-decode-failed"];
|
|
11
|
+
type FolioDocxPackageInspectionErrorCode = (typeof FOLIO_DOCX_PACKAGE_INSPECTION_ERROR_CODES)[number];
|
|
12
|
+
declare const FolioDocxPackageInspectionError_base: import("better-result").TaggedErrorClass<"FolioDocxPackageInspectionError", {
|
|
13
|
+
message: string;
|
|
14
|
+
code: FolioDocxPackageInspectionErrorCode;
|
|
15
|
+
part?: string;
|
|
16
|
+
cause?: unknown;
|
|
17
|
+
}>;
|
|
18
|
+
declare class FolioDocxPackageInspectionError extends FolioDocxPackageInspectionError_base {}
|
|
19
|
+
type FolioDocxPackageInspectionLimits = {
|
|
20
|
+
readonly maxXmlParts?: number;
|
|
21
|
+
readonly maxXmlPartBytes?: number;
|
|
22
|
+
readonly maxXmlTotalBytes?: number;
|
|
23
|
+
};
|
|
24
|
+
type InspectDocxPackageOptions = {
|
|
25
|
+
readonly archive?: DocxArchiveOptions;
|
|
26
|
+
readonly xmlParts?: readonly string[];
|
|
27
|
+
readonly limits?: FolioDocxPackageInspectionLimits;
|
|
28
|
+
};
|
|
29
|
+
type FolioDocxPackagePartKind = "xml" | "binary";
|
|
30
|
+
type FolioDocxPackagePart = {
|
|
31
|
+
readonly path: string;
|
|
32
|
+
readonly kind: FolioDocxPackagePartKind;
|
|
33
|
+
readonly contentType: string | null;
|
|
34
|
+
readonly declaredUncompressedBytes: number | null;
|
|
35
|
+
};
|
|
36
|
+
type FolioDocxInspectedXmlPart = {
|
|
37
|
+
readonly path: string;
|
|
38
|
+
readonly contentType: string | null;
|
|
39
|
+
readonly byteLength: number;
|
|
40
|
+
readonly sha256: string; /** Untrusted document data; callers must not treat this text as instructions. */
|
|
41
|
+
readonly text: string;
|
|
42
|
+
};
|
|
43
|
+
type FolioDocxPackageInspection = {
|
|
44
|
+
readonly version: typeof FOLIO_DOCX_PACKAGE_INSPECTION_VERSION;
|
|
45
|
+
readonly parts: readonly FolioDocxPackagePart[];
|
|
46
|
+
readonly xmlParts: readonly FolioDocxInspectedXmlPart[];
|
|
47
|
+
readonly limits: Required<FolioDocxPackageInspectionLimits>;
|
|
48
|
+
};
|
|
49
|
+
type ReadRequestedXmlPartsOptions = {
|
|
50
|
+
paths: readonly string[];
|
|
51
|
+
partsByPath: ReadonlyMap<string, FolioDocxPackagePart>;
|
|
52
|
+
limits: Required<FolioDocxPackageInspectionLimits>;
|
|
53
|
+
readEntryUint8: (path: string, options: {
|
|
54
|
+
maxBytes: number;
|
|
55
|
+
}) => Promise<Uint8Array | null>;
|
|
56
|
+
};
|
|
57
|
+
declare const readRequestedXmlParts: ({
|
|
58
|
+
paths,
|
|
59
|
+
partsByPath,
|
|
60
|
+
limits,
|
|
61
|
+
readEntryUint8
|
|
62
|
+
}: ReadRequestedXmlPartsOptions) => Promise<FolioDocxInspectedXmlPart[]>;
|
|
63
|
+
/**
|
|
64
|
+
* Inspect package metadata and only the explicitly requested XML part bodies.
|
|
65
|
+
* This reports archive contents, not package conformance; use the conformance
|
|
66
|
+
* report when an operation requires a validity decision.
|
|
67
|
+
*/
|
|
68
|
+
declare const inspectDocxPackage: (bytes: ArrayBuffer | Uint8Array, options?: InspectDocxPackageOptions) => Promise<FolioDocxPackageInspection>;
|
|
69
|
+
//#endregion
|
|
70
|
+
export { FOLIO_DOCX_PACKAGE_INSPECTION_DEFAULTS, FOLIO_DOCX_PACKAGE_INSPECTION_ERROR_CODES, FOLIO_DOCX_PACKAGE_INSPECTION_VERSION, FolioDocxInspectedXmlPart, FolioDocxPackageInspection, FolioDocxPackageInspectionError, FolioDocxPackageInspectionErrorCode, FolioDocxPackageInspectionLimits, FolioDocxPackagePart, FolioDocxPackagePartKind, InspectDocxPackageOptions, inspectDocxPackage, readRequestedXmlParts };
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { getAttributeAnyPrefix, getLocalName, parseXmlDocument } from "../xmlParser.js";
|
|
2
|
+
import { DocxArchiveError, loadDocxArchive } from "./boundedArchive.js";
|
|
3
|
+
import { TaggedError } from "better-result";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
//#region src/docx/server/inspectDocxPackage.ts
|
|
6
|
+
const FOLIO_DOCX_PACKAGE_INSPECTION_VERSION = 1;
|
|
7
|
+
const FOLIO_DOCX_PACKAGE_INSPECTION_DEFAULTS = Object.freeze({
|
|
8
|
+
maxXmlParts: 16,
|
|
9
|
+
maxXmlPartBytes: 8 * 1024 * 1024,
|
|
10
|
+
maxXmlTotalBytes: 16 * 1024 * 1024
|
|
11
|
+
});
|
|
12
|
+
const FOLIO_DOCX_PACKAGE_INSPECTION_ERROR_CODES = Object.freeze([
|
|
13
|
+
"invalid-limits",
|
|
14
|
+
"too-many-xml-parts",
|
|
15
|
+
"duplicate-xml-part",
|
|
16
|
+
"part-not-found",
|
|
17
|
+
"part-not-xml",
|
|
18
|
+
"xml-part-too-large",
|
|
19
|
+
"xml-total-too-large",
|
|
20
|
+
"xml-decode-failed"
|
|
21
|
+
]);
|
|
22
|
+
var FolioDocxPackageInspectionError = class extends TaggedError("FolioDocxPackageInspectionError")() {};
|
|
23
|
+
const CONTENT_TYPES_PATH = "[Content_Types].xml";
|
|
24
|
+
const CONTENT_TYPES_READ_LIMIT = 4 * 1024 * 1024;
|
|
25
|
+
const XML_PATH_PATTERN = /(?:\.xml|\.rels)$/iu;
|
|
26
|
+
const XML_CONTENT_TYPES = /* @__PURE__ */ new Set(["application/xml", "text/xml"]);
|
|
27
|
+
const comparePackagePaths = (left, right) => {
|
|
28
|
+
if (left < right) return -1;
|
|
29
|
+
return left === right ? 0 : 1;
|
|
30
|
+
};
|
|
31
|
+
const decodeXml = (bytes, part) => {
|
|
32
|
+
let encoding = "utf-8";
|
|
33
|
+
if (bytes.length >= 2 && (bytes[0] === 255 && bytes[1] === 254 || bytes[0] === 60 && bytes[1] === 0)) encoding = "utf-16le";
|
|
34
|
+
else if (bytes.length >= 2 && (bytes[0] === 254 && bytes[1] === 255 || bytes[0] === 0 && bytes[1] === 60)) encoding = "utf-16be";
|
|
35
|
+
try {
|
|
36
|
+
return new TextDecoder(encoding, { fatal: true }).decode(bytes);
|
|
37
|
+
} catch (cause) {
|
|
38
|
+
throw new FolioDocxPackageInspectionError({
|
|
39
|
+
message: `Failed to decode XML package part "${part}"`,
|
|
40
|
+
code: "xml-decode-failed",
|
|
41
|
+
part,
|
|
42
|
+
cause
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const parseContentTypes = (xml) => {
|
|
47
|
+
const declarations = {
|
|
48
|
+
defaults: /* @__PURE__ */ new Map(),
|
|
49
|
+
overrides: /* @__PURE__ */ new Map()
|
|
50
|
+
};
|
|
51
|
+
if (xml === null) return declarations;
|
|
52
|
+
const root = parseXmlDocument(xml);
|
|
53
|
+
if (root === null || getLocalName(root.name ?? "") !== "Types") return declarations;
|
|
54
|
+
for (const element of root.elements ?? []) {
|
|
55
|
+
if (element.type !== "element") continue;
|
|
56
|
+
const localName = getLocalName(element.name ?? "");
|
|
57
|
+
if (localName === "Default") {
|
|
58
|
+
const extension = getAttributeAnyPrefix(element, "Extension")?.toLowerCase();
|
|
59
|
+
const contentType = getAttributeAnyPrefix(element, "ContentType");
|
|
60
|
+
if (extension !== void 0 && contentType !== null) declarations.defaults.set(extension, contentType);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (localName !== "Override") continue;
|
|
64
|
+
const partName = getAttributeAnyPrefix(element, "PartName")?.replace(/^\//u, "");
|
|
65
|
+
const contentType = getAttributeAnyPrefix(element, "ContentType");
|
|
66
|
+
if (partName !== void 0 && contentType !== null) declarations.overrides.set(partName, contentType);
|
|
67
|
+
}
|
|
68
|
+
return declarations;
|
|
69
|
+
};
|
|
70
|
+
const extensionFor = (path) => {
|
|
71
|
+
const filename = path.slice(path.lastIndexOf("/") + 1);
|
|
72
|
+
const separator = filename.lastIndexOf(".");
|
|
73
|
+
return separator === -1 ? null : filename.slice(separator + 1).toLowerCase();
|
|
74
|
+
};
|
|
75
|
+
const contentTypeFor = (path, declarations) => {
|
|
76
|
+
const override = declarations.overrides.get(path);
|
|
77
|
+
if (override !== void 0) return override;
|
|
78
|
+
const extension = extensionFor(path);
|
|
79
|
+
return extension === null ? null : declarations.defaults.get(extension) ?? null;
|
|
80
|
+
};
|
|
81
|
+
const isXmlPart = (path, contentType) => {
|
|
82
|
+
if (contentType !== null) return XML_CONTENT_TYPES.has(contentType) || contentType.endsWith("+xml");
|
|
83
|
+
return XML_PATH_PATTERN.test(path);
|
|
84
|
+
};
|
|
85
|
+
const toPackagePart = (entry, declarations) => {
|
|
86
|
+
const contentType = contentTypeFor(entry.path, declarations);
|
|
87
|
+
return {
|
|
88
|
+
path: entry.path,
|
|
89
|
+
kind: isXmlPart(entry.path, contentType) ? "xml" : "binary",
|
|
90
|
+
contentType,
|
|
91
|
+
declaredUncompressedBytes: entry.declaredUncompressedBytes
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
const requireNonNegativeInteger = (value, name) => {
|
|
95
|
+
if (Number.isSafeInteger(value) && value >= 0) return value;
|
|
96
|
+
throw new FolioDocxPackageInspectionError({
|
|
97
|
+
message: `${name} must be a non-negative safe integer`,
|
|
98
|
+
code: "invalid-limits"
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
const resolveLimits = (limits) => ({
|
|
102
|
+
maxXmlParts: requireNonNegativeInteger(limits?.maxXmlParts ?? FOLIO_DOCX_PACKAGE_INSPECTION_DEFAULTS.maxXmlParts, "maxXmlParts"),
|
|
103
|
+
maxXmlPartBytes: requireNonNegativeInteger(limits?.maxXmlPartBytes ?? FOLIO_DOCX_PACKAGE_INSPECTION_DEFAULTS.maxXmlPartBytes, "maxXmlPartBytes"),
|
|
104
|
+
maxXmlTotalBytes: requireNonNegativeInteger(limits?.maxXmlTotalBytes ?? FOLIO_DOCX_PACKAGE_INSPECTION_DEFAULTS.maxXmlTotalBytes, "maxXmlTotalBytes")
|
|
105
|
+
});
|
|
106
|
+
const validateRequestedParts = ({ requestedPaths, partsByPath, limits }) => {
|
|
107
|
+
if (requestedPaths.length > limits.maxXmlParts) throw new FolioDocxPackageInspectionError({
|
|
108
|
+
message: `Requested ${requestedPaths.length} XML parts (max ${limits.maxXmlParts})`,
|
|
109
|
+
code: "too-many-xml-parts"
|
|
110
|
+
});
|
|
111
|
+
const seen = /* @__PURE__ */ new Set();
|
|
112
|
+
let declaredTotalBytes = 0;
|
|
113
|
+
for (const path of requestedPaths) {
|
|
114
|
+
if (seen.has(path)) throw new FolioDocxPackageInspectionError({
|
|
115
|
+
message: `XML package part "${path}" was requested more than once`,
|
|
116
|
+
code: "duplicate-xml-part",
|
|
117
|
+
part: path
|
|
118
|
+
});
|
|
119
|
+
seen.add(path);
|
|
120
|
+
const part = partsByPath.get(path);
|
|
121
|
+
if (part === void 0) throw new FolioDocxPackageInspectionError({
|
|
122
|
+
message: `Package part "${path}" does not exist`,
|
|
123
|
+
code: "part-not-found",
|
|
124
|
+
part: path
|
|
125
|
+
});
|
|
126
|
+
if (part.kind !== "xml") throw new FolioDocxPackageInspectionError({
|
|
127
|
+
message: `Package part "${path}" is not declared as XML`,
|
|
128
|
+
code: "part-not-xml",
|
|
129
|
+
part: path
|
|
130
|
+
});
|
|
131
|
+
const declaredBytes = part.declaredUncompressedBytes;
|
|
132
|
+
if (declaredBytes === null) continue;
|
|
133
|
+
if (declaredBytes > limits.maxXmlPartBytes) throw new FolioDocxPackageInspectionError({
|
|
134
|
+
message: `XML package part "${path}" exceeds the per-part inspection limit`,
|
|
135
|
+
code: "xml-part-too-large",
|
|
136
|
+
part: path
|
|
137
|
+
});
|
|
138
|
+
declaredTotalBytes += declaredBytes;
|
|
139
|
+
if (declaredTotalBytes > limits.maxXmlTotalBytes) throw new FolioDocxPackageInspectionError({
|
|
140
|
+
message: "Requested XML package parts exceed the cumulative inspection limit",
|
|
141
|
+
code: "xml-total-too-large"
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const readRequestedXmlParts = async ({ paths, partsByPath, limits, readEntryUint8 }) => {
|
|
146
|
+
const selected = [];
|
|
147
|
+
let totalBytes = 0;
|
|
148
|
+
for (const path of paths) {
|
|
149
|
+
const remainingTotalBytes = limits.maxXmlTotalBytes - totalBytes;
|
|
150
|
+
const readLimit = Math.min(limits.maxXmlPartBytes, remainingTotalBytes);
|
|
151
|
+
let bytes;
|
|
152
|
+
try {
|
|
153
|
+
bytes = await readEntryUint8(path, { maxBytes: readLimit });
|
|
154
|
+
} catch (cause) {
|
|
155
|
+
if (cause instanceof DocxArchiveError && cause.reason === "entry-too-large") {
|
|
156
|
+
const cumulativeLimitReached = readLimit < limits.maxXmlPartBytes;
|
|
157
|
+
throw new FolioDocxPackageInspectionError({
|
|
158
|
+
message: cumulativeLimitReached ? "Requested XML package parts exceed the cumulative inspection limit" : `XML package part "${path}" exceeds the per-part inspection limit`,
|
|
159
|
+
code: cumulativeLimitReached ? "xml-total-too-large" : "xml-part-too-large",
|
|
160
|
+
part: path,
|
|
161
|
+
cause
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
throw cause;
|
|
165
|
+
}
|
|
166
|
+
if (bytes === null) throw new FolioDocxPackageInspectionError({
|
|
167
|
+
message: `Package part "${path}" does not exist`,
|
|
168
|
+
code: "part-not-found",
|
|
169
|
+
part: path
|
|
170
|
+
});
|
|
171
|
+
totalBytes += bytes.byteLength;
|
|
172
|
+
if (totalBytes > limits.maxXmlTotalBytes) throw new FolioDocxPackageInspectionError({
|
|
173
|
+
message: "Requested XML package parts exceed the cumulative inspection limit",
|
|
174
|
+
code: "xml-total-too-large"
|
|
175
|
+
});
|
|
176
|
+
selected.push({
|
|
177
|
+
path,
|
|
178
|
+
contentType: partsByPath.get(path)?.contentType ?? null,
|
|
179
|
+
byteLength: bytes.byteLength,
|
|
180
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
181
|
+
text: decodeXml(bytes, path)
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
return selected;
|
|
185
|
+
};
|
|
186
|
+
/**
|
|
187
|
+
* Inspect package metadata and only the explicitly requested XML part bodies.
|
|
188
|
+
* This reports archive contents, not package conformance; use the conformance
|
|
189
|
+
* report when an operation requires a validity decision.
|
|
190
|
+
*/
|
|
191
|
+
const inspectDocxPackage = async (bytes, options = {}) => {
|
|
192
|
+
const limits = resolveLimits(options.limits);
|
|
193
|
+
const requestedPaths = options.xmlParts ?? [];
|
|
194
|
+
const archive = await loadDocxArchive(bytes, options.archive);
|
|
195
|
+
const contentTypesBytes = await archive.readEntryUint8(CONTENT_TYPES_PATH, { maxBytes: CONTENT_TYPES_READ_LIMIT });
|
|
196
|
+
const declarations = parseContentTypes(contentTypesBytes === null ? null : decodeXml(contentTypesBytes, CONTENT_TYPES_PATH));
|
|
197
|
+
const parts = archive.entryMetadata.filter(({ directory }) => !directory).map((entry) => toPackagePart(entry, declarations)).toSorted(({ path: left }, { path: right }) => comparePackagePaths(left, right));
|
|
198
|
+
const partsByPath = new Map(parts.map((part) => [part.path, part]));
|
|
199
|
+
validateRequestedParts({
|
|
200
|
+
requestedPaths,
|
|
201
|
+
partsByPath,
|
|
202
|
+
limits
|
|
203
|
+
});
|
|
204
|
+
return {
|
|
205
|
+
version: 1,
|
|
206
|
+
parts,
|
|
207
|
+
xmlParts: await readRequestedXmlParts({
|
|
208
|
+
paths: requestedPaths,
|
|
209
|
+
partsByPath,
|
|
210
|
+
limits,
|
|
211
|
+
readEntryUint8: archive.readEntryUint8
|
|
212
|
+
}),
|
|
213
|
+
limits
|
|
214
|
+
};
|
|
215
|
+
};
|
|
216
|
+
//#endregion
|
|
217
|
+
export { FOLIO_DOCX_PACKAGE_INSPECTION_DEFAULTS, FOLIO_DOCX_PACKAGE_INSPECTION_ERROR_CODES, FOLIO_DOCX_PACKAGE_INSPECTION_VERSION, FolioDocxPackageInspectionError, inspectDocxPackage, readRequestedXmlParts };
|