@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
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { escapeXml, intAttr } from "./xmlUtils.js";
|
|
2
2
|
import { serializeBorder } from "./borderSerializer.js";
|
|
3
|
-
import { serializeParagraph } from "./paragraphSerializer.js";
|
|
4
3
|
//#region src/docx/serializer/tableSerializer.ts
|
|
5
4
|
function normalizeTrackedChangeInfo(info) {
|
|
6
5
|
const normalizedId = Number.isInteger(info.id) && info.id >= 0 ? info.id : 0;
|
|
@@ -247,12 +246,17 @@ function serializeTableCellFormatting(formatting, propertyChanges, structuralCha
|
|
|
247
246
|
if (formatting.textDirection) parts.push(`<w:textDirection w:val="${formatting.textDirection}"/>`);
|
|
248
247
|
if (formatting.fitText) parts.push("<w:tcFitText/>");
|
|
249
248
|
if (formatting.verticalAlign) parts.push(`<w:vAlign w:val="${formatting.verticalAlign}"/>`);
|
|
250
|
-
if (formatting.hideMark) parts.push("<w:hideMark/>");
|
|
249
|
+
if (formatting.hideMark !== void 0) parts.push(formatting.hideMark ? "<w:hideMark/>" : "<w:hideMark w:val=\"false\"/>");
|
|
251
250
|
}
|
|
252
251
|
if (structuralChange) {
|
|
253
252
|
if (structuralChange.type === "tableCellInsertion") parts.push(`<w:cellIns ${serializeTrackedChangeAttributes(structuralChange.info)}/>`);
|
|
254
253
|
else if (structuralChange.type === "tableCellDeletion") parts.push(`<w:cellDel ${serializeTrackedChangeAttributes(structuralChange.info)}/>`);
|
|
255
|
-
else if (structuralChange.type === "tableCellMerge")
|
|
254
|
+
else if (structuralChange.type === "tableCellMerge") {
|
|
255
|
+
const attrs = [serializeTrackedChangeAttributes(structuralChange.info)];
|
|
256
|
+
if (structuralChange.verticalMerge) attrs.push(`w:vMerge="${structuralChange.verticalMerge === "continue" ? "cont" : "rest"}"`);
|
|
257
|
+
if (structuralChange.verticalMergeOriginal) attrs.push(`w:vMergeOrig="${structuralChange.verticalMergeOriginal === "continue" ? "cont" : "rest"}"`);
|
|
258
|
+
parts.push(`<w:cellMerge ${attrs.join(" ")}/>`);
|
|
259
|
+
}
|
|
256
260
|
}
|
|
257
261
|
if (propertyChanges && propertyChanges.length > 0) parts.push(...propertyChanges.map((change) => serializeTableCellPropertyChange(change)));
|
|
258
262
|
if (parts.length === 0) return "";
|
|
@@ -277,31 +281,31 @@ function serializeTableGrid(columnWidths) {
|
|
|
277
281
|
/**
|
|
278
282
|
* Serialize cell content (paragraphs, nested tables)
|
|
279
283
|
*/
|
|
280
|
-
function serializeCellContent(content) {
|
|
284
|
+
function serializeCellContent(content, serializeParagraph) {
|
|
281
285
|
const parts = [];
|
|
282
286
|
for (const item of content) if (item.type === "paragraph") parts.push(serializeParagraph(item));
|
|
283
|
-
else parts.push(serializeTable(item));
|
|
287
|
+
else parts.push(serializeTable(item, serializeParagraph));
|
|
284
288
|
if (parts.length === 0) parts.push("<w:p/>");
|
|
285
289
|
return parts.join("");
|
|
286
290
|
}
|
|
287
291
|
/**
|
|
288
292
|
* Serialize a table cell (w:tc)
|
|
289
293
|
*/
|
|
290
|
-
function serializeTableCell(cell) {
|
|
294
|
+
function serializeTableCell(cell, serializeParagraph) {
|
|
291
295
|
const parts = [];
|
|
292
296
|
const tcPrXml = serializeTableCellFormatting(cell.formatting, cell.propertyChanges, cell.structuralChange);
|
|
293
297
|
if (tcPrXml) parts.push(tcPrXml);
|
|
294
|
-
parts.push(serializeCellContent(cell.content));
|
|
298
|
+
parts.push(serializeCellContent(cell.content, serializeParagraph));
|
|
295
299
|
return `<w:tc>${parts.join("")}</w:tc>`;
|
|
296
300
|
}
|
|
297
301
|
/**
|
|
298
302
|
* Serialize a table row (w:tr)
|
|
299
303
|
*/
|
|
300
|
-
function serializeTableRow(row) {
|
|
304
|
+
function serializeTableRow(row, serializeParagraph) {
|
|
301
305
|
const parts = [];
|
|
302
306
|
const trPrXml = serializeTableRowFormatting(row.formatting, row.propertyChanges, row.structuralChange);
|
|
303
307
|
if (trPrXml) parts.push(trPrXml);
|
|
304
|
-
for (const cell of row.cells) parts.push(serializeTableCell(cell));
|
|
308
|
+
for (const cell of row.cells) parts.push(serializeTableCell(cell, serializeParagraph));
|
|
305
309
|
return `<w:tr>${parts.join("")}</w:tr>`;
|
|
306
310
|
}
|
|
307
311
|
/**
|
|
@@ -310,13 +314,13 @@ function serializeTableRow(row) {
|
|
|
310
314
|
* @param table - The table to serialize
|
|
311
315
|
* @returns XML string for the table
|
|
312
316
|
*/
|
|
313
|
-
function serializeTable(table) {
|
|
317
|
+
function serializeTable(table, serializeParagraph) {
|
|
314
318
|
const parts = [];
|
|
315
319
|
const tblPrXml = serializeTableFormatting(table.formatting, table.propertyChanges);
|
|
316
320
|
if (tblPrXml) parts.push(tblPrXml);
|
|
317
321
|
const tblGridXml = serializeTableGrid(table.columnWidths);
|
|
318
322
|
if (tblGridXml) parts.push(tblGridXml);
|
|
319
|
-
for (const row of table.rows) parts.push(serializeTableRow(row));
|
|
323
|
+
for (const row of table.rows) parts.push(serializeTableRow(row, serializeParagraph));
|
|
320
324
|
return `<w:tbl>${parts.join("")}</w:tbl>`;
|
|
321
325
|
}
|
|
322
326
|
/**
|
|
@@ -325,8 +329,8 @@ function serializeTable(table) {
|
|
|
325
329
|
* @param tables - The tables to serialize
|
|
326
330
|
* @returns XML string for all tables
|
|
327
331
|
*/
|
|
328
|
-
function serializeTables(tables) {
|
|
329
|
-
return tables.map(serializeTable).join("");
|
|
332
|
+
function serializeTables(tables, serializeParagraph) {
|
|
333
|
+
return tables.map((table) => serializeTable(table, serializeParagraph)).join("");
|
|
330
334
|
}
|
|
331
335
|
/**
|
|
332
336
|
* Check if a table has any rows
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { DocxArchiveOptions } from "./boundedArchive.js";
|
|
2
|
+
import { FOLIO_DOCX_XML_PATCH_PROPOSAL_PROFILE, FolioDocxPreparedXmlReplacement, FolioDocxXmlPatchProposalEvaluation, FolioDocxXmlPatchProposalLimits } from "./evaluateDocxXmlPatchProposal.js";
|
|
3
|
+
import { FOLIO_DOCX_CONFORMANCE_PROFILE, FolioDocxConformanceReport } from "./validateDocxConformance.js";
|
|
4
|
+
|
|
5
|
+
//#region src/docx/server/applyDocxXmlPatchProposal.d.ts
|
|
6
|
+
declare const FOLIO_DOCX_XML_PATCH_APPLICATION_VERSION: 1;
|
|
7
|
+
declare const FOLIO_DOCX_XML_PATCH_APPLICATION_PROFILE: "folio-xml-patch-application-v1";
|
|
8
|
+
type ApplyDocxXmlPatchProposalArgs = {
|
|
9
|
+
readonly bytes: ArrayBuffer | Uint8Array;
|
|
10
|
+
readonly proposal: unknown; /** Exact package paths authorized by the server-side caller. */
|
|
11
|
+
readonly allowedParts: readonly string[];
|
|
12
|
+
readonly validationProfile: typeof FOLIO_DOCX_CONFORMANCE_PROFILE;
|
|
13
|
+
readonly archive?: DocxArchiveOptions;
|
|
14
|
+
readonly limits?: FolioDocxXmlPatchProposalLimits;
|
|
15
|
+
};
|
|
16
|
+
type FolioDocxXmlPatchApplicationReceipt = {
|
|
17
|
+
readonly version: typeof FOLIO_DOCX_XML_PATCH_APPLICATION_VERSION;
|
|
18
|
+
readonly profile: typeof FOLIO_DOCX_XML_PATCH_APPLICATION_PROFILE;
|
|
19
|
+
readonly proposalProfile: typeof FOLIO_DOCX_XML_PATCH_PROPOSAL_PROFILE;
|
|
20
|
+
readonly validationProfile: typeof FOLIO_DOCX_CONFORMANCE_PROFILE;
|
|
21
|
+
readonly input: {
|
|
22
|
+
readonly sha256: string;
|
|
23
|
+
readonly byteLength: number;
|
|
24
|
+
};
|
|
25
|
+
readonly output: {
|
|
26
|
+
readonly sha256: string;
|
|
27
|
+
readonly byteLength: number;
|
|
28
|
+
};
|
|
29
|
+
readonly replacements: readonly [FolioDocxPreparedXmlReplacement, ...FolioDocxPreparedXmlReplacement[]];
|
|
30
|
+
};
|
|
31
|
+
type FolioDocxXmlPatchApplication = {
|
|
32
|
+
readonly version: typeof FOLIO_DOCX_XML_PATCH_APPLICATION_VERSION;
|
|
33
|
+
readonly profile: typeof FOLIO_DOCX_XML_PATCH_APPLICATION_PROFILE;
|
|
34
|
+
readonly status: "proposal-rejected";
|
|
35
|
+
readonly producesOutput: false;
|
|
36
|
+
readonly evaluation: Extract<FolioDocxXmlPatchProposalEvaluation, {
|
|
37
|
+
status: "rejected";
|
|
38
|
+
}>;
|
|
39
|
+
readonly conformance: null;
|
|
40
|
+
readonly receipt: null;
|
|
41
|
+
} | {
|
|
42
|
+
readonly version: typeof FOLIO_DOCX_XML_PATCH_APPLICATION_VERSION;
|
|
43
|
+
readonly profile: typeof FOLIO_DOCX_XML_PATCH_APPLICATION_PROFILE;
|
|
44
|
+
readonly status: "output-rejected";
|
|
45
|
+
readonly producesOutput: false;
|
|
46
|
+
readonly evaluation: Extract<FolioDocxXmlPatchProposalEvaluation, {
|
|
47
|
+
status: "accepted";
|
|
48
|
+
}>;
|
|
49
|
+
readonly conformance: FolioDocxConformanceReport;
|
|
50
|
+
readonly receipt: null;
|
|
51
|
+
} | {
|
|
52
|
+
readonly version: typeof FOLIO_DOCX_XML_PATCH_APPLICATION_VERSION;
|
|
53
|
+
readonly profile: typeof FOLIO_DOCX_XML_PATCH_APPLICATION_PROFILE;
|
|
54
|
+
readonly status: "applied";
|
|
55
|
+
readonly producesOutput: true;
|
|
56
|
+
readonly evaluation: Extract<FolioDocxXmlPatchProposalEvaluation, {
|
|
57
|
+
status: "accepted";
|
|
58
|
+
}>;
|
|
59
|
+
readonly conformance: FolioDocxConformanceReport & {
|
|
60
|
+
readonly status: "conformant";
|
|
61
|
+
};
|
|
62
|
+
readonly receipt: FolioDocxXmlPatchApplicationReceipt;
|
|
63
|
+
readonly bytes: Uint8Array;
|
|
64
|
+
};
|
|
65
|
+
declare const UnsupportedFolioDocxXmlPatchApplicationProfileError_base: import("better-result").TaggedErrorClass<"UnsupportedFolioDocxXmlPatchApplicationProfileError", {
|
|
66
|
+
message: string;
|
|
67
|
+
profile: unknown;
|
|
68
|
+
}>;
|
|
69
|
+
declare class UnsupportedFolioDocxXmlPatchApplicationProfileError extends UnsupportedFolioDocxXmlPatchApplicationProfileError_base {}
|
|
70
|
+
declare const FolioDocxXmlPatchApplicationError_base: import("better-result").TaggedErrorClass<"FolioDocxXmlPatchApplicationError", {
|
|
71
|
+
message: string;
|
|
72
|
+
stage: "verify" | "load" | "replace" | "generate";
|
|
73
|
+
part?: string;
|
|
74
|
+
cause?: unknown;
|
|
75
|
+
}>;
|
|
76
|
+
declare class FolioDocxXmlPatchApplicationError extends FolioDocxXmlPatchApplicationError_base {}
|
|
77
|
+
/** Apply a guarded XML proposal and return output only after full profile validation. */
|
|
78
|
+
declare const applyDocxXmlPatchProposal: ({
|
|
79
|
+
bytes,
|
|
80
|
+
proposal: rawProposal,
|
|
81
|
+
allowedParts,
|
|
82
|
+
validationProfile,
|
|
83
|
+
archive,
|
|
84
|
+
limits
|
|
85
|
+
}: ApplyDocxXmlPatchProposalArgs) => Promise<FolioDocxXmlPatchApplication>;
|
|
86
|
+
//#endregion
|
|
87
|
+
export { ApplyDocxXmlPatchProposalArgs, FOLIO_DOCX_XML_PATCH_APPLICATION_PROFILE, FOLIO_DOCX_XML_PATCH_APPLICATION_VERSION, FolioDocxXmlPatchApplication, FolioDocxXmlPatchApplicationError, FolioDocxXmlPatchApplicationReceipt, UnsupportedFolioDocxXmlPatchApplicationProfileError, applyDocxXmlPatchProposal };
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { loadDocxArchive } from "./boundedArchive.js";
|
|
2
|
+
import { FOLIO_DOCX_XML_PATCH_PROPOSAL_PROFILE, InvalidFolioDocxXmlPatchProposalError, evaluateDocxXmlPatchProposal, parseFolioDocxXmlPatchProposal } from "./evaluateDocxXmlPatchProposal.js";
|
|
3
|
+
import { FOLIO_DOCX_CONFORMANCE_PROFILE, validateDocxConformance } from "./validateDocxConformance.js";
|
|
4
|
+
import { TaggedError, panic } from "better-result";
|
|
5
|
+
import JSZip from "jszip";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
//#region src/docx/server/applyDocxXmlPatchProposal.ts
|
|
8
|
+
const FOLIO_DOCX_XML_PATCH_APPLICATION_VERSION = 1;
|
|
9
|
+
const FOLIO_DOCX_XML_PATCH_APPLICATION_PROFILE = "folio-xml-patch-application-v1";
|
|
10
|
+
const isConformantReport = (report) => report.status === "conformant";
|
|
11
|
+
var UnsupportedFolioDocxXmlPatchApplicationProfileError = class extends TaggedError("UnsupportedFolioDocxXmlPatchApplicationProfileError")() {};
|
|
12
|
+
var FolioDocxXmlPatchApplicationError = class extends TaggedError("FolioDocxXmlPatchApplicationError")() {};
|
|
13
|
+
const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
14
|
+
const snapshotInput = (bytes) => Uint8Array.from(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes));
|
|
15
|
+
const snapshotArchiveOptions = (archive) => {
|
|
16
|
+
if (archive === void 0) return;
|
|
17
|
+
return {
|
|
18
|
+
...archive.maxInputBytes === void 0 ? {} : { maxInputBytes: archive.maxInputBytes },
|
|
19
|
+
...archive.maxEntryBytes === void 0 ? {} : { maxEntryBytes: archive.maxEntryBytes },
|
|
20
|
+
...archive.maxTotalBytes === void 0 ? {} : { maxTotalBytes: archive.maxTotalBytes },
|
|
21
|
+
...archive.maxEntries === void 0 ? {} : { maxEntries: archive.maxEntries }
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
const snapshotProposalLimits = (limits) => {
|
|
25
|
+
if (limits === void 0) return;
|
|
26
|
+
return {
|
|
27
|
+
...limits.maxReplacements === void 0 ? {} : { maxReplacements: limits.maxReplacements },
|
|
28
|
+
...limits.maxPartBytes === void 0 ? {} : { maxPartBytes: limits.maxPartBytes },
|
|
29
|
+
...limits.maxTotalBytes === void 0 ? {} : { maxTotalBytes: limits.maxTotalBytes }
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
const evaluateProposal = async ({ bytes, proposal, allowedParts, archive, limits }) => await evaluateDocxXmlPatchProposal({
|
|
33
|
+
bytes,
|
|
34
|
+
proposal,
|
|
35
|
+
allowedParts,
|
|
36
|
+
...archive === void 0 ? {} : { archive },
|
|
37
|
+
...limits === void 0 ? {} : { limits }
|
|
38
|
+
});
|
|
39
|
+
const proposalRejected = ({ evaluation }) => ({
|
|
40
|
+
version: 1,
|
|
41
|
+
profile: FOLIO_DOCX_XML_PATCH_APPLICATION_PROFILE,
|
|
42
|
+
status: "proposal-rejected",
|
|
43
|
+
producesOutput: false,
|
|
44
|
+
evaluation,
|
|
45
|
+
conformance: null,
|
|
46
|
+
receipt: null
|
|
47
|
+
});
|
|
48
|
+
const verifyArchiveContents = async (bytes, options) => {
|
|
49
|
+
let archive;
|
|
50
|
+
try {
|
|
51
|
+
archive = await loadDocxArchive(bytes, options);
|
|
52
|
+
} catch (cause) {
|
|
53
|
+
throw new FolioDocxXmlPatchApplicationError({
|
|
54
|
+
message: "The evaluated package could not be verified within the archive limits.",
|
|
55
|
+
stage: "verify",
|
|
56
|
+
cause
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
for (const entry of archive.entryMetadata.toSorted(({ path: left }, { path: right }) => left < right ? -1 : Number(left > right))) {
|
|
60
|
+
if (entry.directory) continue;
|
|
61
|
+
try {
|
|
62
|
+
if (await archive.readEntryUint8(entry.path) === null) throw new FolioDocxXmlPatchApplicationError({
|
|
63
|
+
message: "An archive entry disappeared during verification.",
|
|
64
|
+
stage: "verify",
|
|
65
|
+
part: entry.path
|
|
66
|
+
});
|
|
67
|
+
} catch (cause) {
|
|
68
|
+
if (cause instanceof FolioDocxXmlPatchApplicationError) throw cause;
|
|
69
|
+
throw new FolioDocxXmlPatchApplicationError({
|
|
70
|
+
message: "An archive entry could not be verified within the configured limits.",
|
|
71
|
+
stage: "verify",
|
|
72
|
+
part: entry.path,
|
|
73
|
+
cause
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const applyReplacements = async ({ bytes, proposal }) => {
|
|
79
|
+
let zip;
|
|
80
|
+
try {
|
|
81
|
+
zip = await JSZip.loadAsync(bytes);
|
|
82
|
+
} catch (cause) {
|
|
83
|
+
throw new FolioDocxXmlPatchApplicationError({
|
|
84
|
+
message: "The evaluated package could not be loaded for replacement.",
|
|
85
|
+
stage: "load",
|
|
86
|
+
cause
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
for (const replacement of proposal.replacements) {
|
|
90
|
+
const current = zip.file(replacement.path);
|
|
91
|
+
if (current === null) throw new FolioDocxXmlPatchApplicationError({
|
|
92
|
+
message: "An evaluated package part was unavailable during replacement.",
|
|
93
|
+
stage: "replace"
|
|
94
|
+
});
|
|
95
|
+
zip.file(replacement.path, new TextEncoder().encode(replacement.replacementXml), {
|
|
96
|
+
binary: true,
|
|
97
|
+
date: current.date,
|
|
98
|
+
comment: current.comment,
|
|
99
|
+
createFolders: false,
|
|
100
|
+
unixPermissions: current.unixPermissions,
|
|
101
|
+
dosPermissions: current.dosPermissions,
|
|
102
|
+
compression: "DEFLATE",
|
|
103
|
+
compressionOptions: { level: 6 }
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
return await zip.generateAsync({
|
|
108
|
+
type: "uint8array",
|
|
109
|
+
compression: "DEFLATE",
|
|
110
|
+
compressionOptions: { level: 6 }
|
|
111
|
+
});
|
|
112
|
+
} catch (cause) {
|
|
113
|
+
throw new FolioDocxXmlPatchApplicationError({
|
|
114
|
+
message: "The replacement package could not be generated.",
|
|
115
|
+
stage: "generate",
|
|
116
|
+
cause
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
const outputRejected = ({ evaluation, conformance }) => ({
|
|
121
|
+
version: 1,
|
|
122
|
+
profile: FOLIO_DOCX_XML_PATCH_APPLICATION_PROFILE,
|
|
123
|
+
status: "output-rejected",
|
|
124
|
+
producesOutput: false,
|
|
125
|
+
evaluation,
|
|
126
|
+
conformance,
|
|
127
|
+
receipt: null
|
|
128
|
+
});
|
|
129
|
+
const applied = ({ input, output, evaluation, conformance }) => ({
|
|
130
|
+
version: 1,
|
|
131
|
+
profile: FOLIO_DOCX_XML_PATCH_APPLICATION_PROFILE,
|
|
132
|
+
status: "applied",
|
|
133
|
+
producesOutput: true,
|
|
134
|
+
evaluation,
|
|
135
|
+
conformance,
|
|
136
|
+
receipt: {
|
|
137
|
+
version: 1,
|
|
138
|
+
profile: FOLIO_DOCX_XML_PATCH_APPLICATION_PROFILE,
|
|
139
|
+
proposalProfile: FOLIO_DOCX_XML_PATCH_PROPOSAL_PROFILE,
|
|
140
|
+
validationProfile: FOLIO_DOCX_CONFORMANCE_PROFILE,
|
|
141
|
+
input: {
|
|
142
|
+
sha256: sha256(input),
|
|
143
|
+
byteLength: input.byteLength
|
|
144
|
+
},
|
|
145
|
+
output: {
|
|
146
|
+
sha256: sha256(output),
|
|
147
|
+
byteLength: output.byteLength
|
|
148
|
+
},
|
|
149
|
+
replacements: evaluation.replacements
|
|
150
|
+
},
|
|
151
|
+
bytes: output
|
|
152
|
+
});
|
|
153
|
+
/** Apply a guarded XML proposal and return output only after full profile validation. */
|
|
154
|
+
const applyDocxXmlPatchProposal = async ({ bytes, proposal: rawProposal, allowedParts, validationProfile, archive, limits }) => {
|
|
155
|
+
if (validationProfile !== "folio-supported-v1") throw new UnsupportedFolioDocxXmlPatchApplicationProfileError({
|
|
156
|
+
message: "The requested package validation profile is not supported.",
|
|
157
|
+
profile: validationProfile
|
|
158
|
+
});
|
|
159
|
+
const input = snapshotInput(bytes);
|
|
160
|
+
const policyAllowedParts = [...allowedParts];
|
|
161
|
+
const policyArchive = snapshotArchiveOptions(archive);
|
|
162
|
+
const policyLimits = snapshotProposalLimits(limits);
|
|
163
|
+
let proposal;
|
|
164
|
+
try {
|
|
165
|
+
proposal = parseFolioDocxXmlPatchProposal(rawProposal);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (!(error instanceof InvalidFolioDocxXmlPatchProposalError)) throw error;
|
|
168
|
+
const evaluation = await evaluateProposal({
|
|
169
|
+
bytes: input,
|
|
170
|
+
proposal: rawProposal,
|
|
171
|
+
allowedParts: policyAllowedParts,
|
|
172
|
+
archive: policyArchive,
|
|
173
|
+
limits: policyLimits
|
|
174
|
+
});
|
|
175
|
+
if (evaluation.status !== "rejected") return panic("An invalid XML proposal produced an accepted evaluation");
|
|
176
|
+
return proposalRejected({ evaluation });
|
|
177
|
+
}
|
|
178
|
+
const evaluation = await evaluateProposal({
|
|
179
|
+
bytes: input,
|
|
180
|
+
proposal,
|
|
181
|
+
allowedParts: policyAllowedParts,
|
|
182
|
+
archive: policyArchive,
|
|
183
|
+
limits: policyLimits
|
|
184
|
+
});
|
|
185
|
+
if (evaluation.status === "rejected") return proposalRejected({ evaluation });
|
|
186
|
+
await verifyArchiveContents(input, policyArchive);
|
|
187
|
+
const output = await applyReplacements({
|
|
188
|
+
bytes: input,
|
|
189
|
+
proposal
|
|
190
|
+
});
|
|
191
|
+
const conformance = await validateDocxConformance(output, { ...policyArchive === void 0 ? {} : { archive: policyArchive } });
|
|
192
|
+
if (!isConformantReport(conformance)) return outputRejected({
|
|
193
|
+
evaluation,
|
|
194
|
+
conformance
|
|
195
|
+
});
|
|
196
|
+
return applied({
|
|
197
|
+
input,
|
|
198
|
+
output,
|
|
199
|
+
evaluation,
|
|
200
|
+
conformance
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
//#endregion
|
|
204
|
+
export { FOLIO_DOCX_XML_PATCH_APPLICATION_PROFILE, FOLIO_DOCX_XML_PATCH_APPLICATION_VERSION, FolioDocxXmlPatchApplicationError, UnsupportedFolioDocxXmlPatchApplicationProfileError, applyDocxXmlPatchProposal };
|
|
@@ -2,23 +2,34 @@
|
|
|
2
2
|
declare const DOCX_MAX_ENTRY_BYTES: number;
|
|
3
3
|
declare const DOCX_MAX_TOTAL_BYTES: number;
|
|
4
4
|
declare const DOCX_MAX_ENTRIES = 4096;
|
|
5
|
+
declare const DOCX_MAX_INPUT_BYTES: number;
|
|
5
6
|
declare const DocxArchiveError_base: import("better-result").TaggedErrorClass<"DocxArchiveError", {
|
|
6
7
|
message: string;
|
|
7
|
-
reason: "load-failed" | "too-many-entries" | "entry-too-large" | "total-too-large";
|
|
8
|
+
reason: "load-failed" | "input-too-large" | "too-many-entries" | "entry-too-large" | "total-too-large" | "invalid-options";
|
|
8
9
|
cause?: unknown;
|
|
9
10
|
}>;
|
|
10
11
|
/** Error raised when a DOCX archive cannot be loaded within configured limits. */
|
|
11
12
|
declare class DocxArchiveError extends DocxArchiveError_base {}
|
|
12
13
|
type DocxArchiveOptions = {
|
|
14
|
+
maxInputBytes?: number;
|
|
13
15
|
maxEntryBytes?: number;
|
|
14
16
|
maxTotalBytes?: number;
|
|
15
17
|
maxEntries?: number;
|
|
16
18
|
};
|
|
19
|
+
type DocxArchiveEntry = {
|
|
20
|
+
readonly path: string;
|
|
21
|
+
readonly directory: boolean;
|
|
22
|
+
readonly declaredUncompressedBytes: number | null;
|
|
23
|
+
};
|
|
24
|
+
type DocxArchiveReadOptions = {
|
|
25
|
+
maxBytes?: number;
|
|
26
|
+
};
|
|
17
27
|
type DocxArchive = {
|
|
18
28
|
entries: readonly string[];
|
|
29
|
+
entryMetadata: readonly DocxArchiveEntry[];
|
|
19
30
|
readEntryString: (path: string) => Promise<string | null>;
|
|
20
|
-
readEntryUint8: (path: string) => Promise<Uint8Array | null>;
|
|
31
|
+
readEntryUint8: (path: string, options?: DocxArchiveReadOptions) => Promise<Uint8Array | null>;
|
|
21
32
|
};
|
|
22
33
|
declare const loadDocxArchive: (bytes: ArrayBuffer | Uint8Array, options?: DocxArchiveOptions) => Promise<DocxArchive>;
|
|
23
34
|
//#endregion
|
|
24
|
-
export { DOCX_MAX_ENTRIES, DOCX_MAX_ENTRY_BYTES, DOCX_MAX_TOTAL_BYTES, DocxArchive, DocxArchiveError, DocxArchiveOptions, loadDocxArchive };
|
|
35
|
+
export { DOCX_MAX_ENTRIES, DOCX_MAX_ENTRY_BYTES, DOCX_MAX_INPUT_BYTES, DOCX_MAX_TOTAL_BYTES, DocxArchive, DocxArchiveEntry, DocxArchiveError, DocxArchiveOptions, DocxArchiveReadOptions, loadDocxArchive };
|
|
@@ -4,6 +4,7 @@ import JSZip from "jszip";
|
|
|
4
4
|
const DOCX_MAX_ENTRY_BYTES = 128 * 1024 * 1024;
|
|
5
5
|
const DOCX_MAX_TOTAL_BYTES = 256 * 1024 * 1024;
|
|
6
6
|
const DOCX_MAX_ENTRIES = 4096;
|
|
7
|
+
const DOCX_MAX_INPUT_BYTES = 50 * 1024 * 1024;
|
|
7
8
|
/** Error raised when a DOCX archive cannot be loaded within configured limits. */
|
|
8
9
|
var DocxArchiveError = class extends TaggedError("DocxArchiveError")() {};
|
|
9
10
|
const collectStream = async ({ stream, maxEntryBytes, remainingBytes, maxTotalBytes, path }) => await new Promise((resolve, reject) => {
|
|
@@ -33,10 +34,44 @@ const collectStream = async ({ stream, maxEntryBytes, remainingBytes, maxTotalBy
|
|
|
33
34
|
stream.on("end", () => resolve(Buffer.concat(chunks)));
|
|
34
35
|
stream.on("error", reject);
|
|
35
36
|
});
|
|
37
|
+
const getDeclaredUncompressedBytes = (entry) => {
|
|
38
|
+
const data = "_data" in entry ? entry._data : void 0;
|
|
39
|
+
const declaredBytes = typeof data === "object" && data !== null && "uncompressedSize" in data ? data.uncompressedSize : void 0;
|
|
40
|
+
return typeof declaredBytes === "number" && Number.isFinite(declaredBytes) ? declaredBytes : null;
|
|
41
|
+
};
|
|
42
|
+
const resolveByteLimit = ({ value, fallback, name }) => {
|
|
43
|
+
const limit = value ?? fallback;
|
|
44
|
+
if (!Number.isSafeInteger(limit) || limit < 0) throw new DocxArchiveError({
|
|
45
|
+
message: `${name} must be a non-negative safe integer`,
|
|
46
|
+
reason: "invalid-options"
|
|
47
|
+
});
|
|
48
|
+
return limit;
|
|
49
|
+
};
|
|
36
50
|
const loadDocxArchive = async (bytes, options = {}) => {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
51
|
+
const maxInputBytes = resolveByteLimit({
|
|
52
|
+
value: options.maxInputBytes,
|
|
53
|
+
fallback: DOCX_MAX_INPUT_BYTES,
|
|
54
|
+
name: "DOCX input byte limit"
|
|
55
|
+
});
|
|
56
|
+
const maxEntryBytes = resolveByteLimit({
|
|
57
|
+
value: options.maxEntryBytes,
|
|
58
|
+
fallback: DOCX_MAX_ENTRY_BYTES,
|
|
59
|
+
name: "DOCX entry byte limit"
|
|
60
|
+
});
|
|
61
|
+
const maxTotalBytes = resolveByteLimit({
|
|
62
|
+
value: options.maxTotalBytes,
|
|
63
|
+
fallback: DOCX_MAX_TOTAL_BYTES,
|
|
64
|
+
name: "DOCX cumulative byte limit"
|
|
65
|
+
});
|
|
66
|
+
const maxEntries = resolveByteLimit({
|
|
67
|
+
value: options.maxEntries,
|
|
68
|
+
fallback: DOCX_MAX_ENTRIES,
|
|
69
|
+
name: "DOCX entry limit"
|
|
70
|
+
});
|
|
71
|
+
if (bytes.byteLength > maxInputBytes) throw new DocxArchiveError({
|
|
72
|
+
message: `DOCX input contains ${bytes.byteLength} bytes (max ${maxInputBytes})`,
|
|
73
|
+
reason: "input-too-large"
|
|
74
|
+
});
|
|
40
75
|
let zip;
|
|
41
76
|
try {
|
|
42
77
|
zip = await JSZip.loadAsync(bytes);
|
|
@@ -54,9 +89,9 @@ const loadDocxArchive = async (bytes, options = {}) => {
|
|
|
54
89
|
});
|
|
55
90
|
let declaredTotalBytes = 0;
|
|
56
91
|
for (const entry of archiveEntries) {
|
|
57
|
-
|
|
58
|
-
const declaredBytes =
|
|
59
|
-
if (
|
|
92
|
+
if (entry.dir) continue;
|
|
93
|
+
const declaredBytes = getDeclaredUncompressedBytes(entry);
|
|
94
|
+
if (declaredBytes === null) {
|
|
60
95
|
declaredTotalBytes = NaN;
|
|
61
96
|
break;
|
|
62
97
|
}
|
|
@@ -72,13 +107,18 @@ const loadDocxArchive = async (bytes, options = {}) => {
|
|
|
72
107
|
});
|
|
73
108
|
let totalBytesRead = 0;
|
|
74
109
|
let readChain = Promise.resolve();
|
|
75
|
-
const readEntry = async (path) => {
|
|
110
|
+
const readEntry = async (path, readOptions = {}) => {
|
|
111
|
+
const requestedMaxBytes = resolveByteLimit({
|
|
112
|
+
value: readOptions.maxBytes,
|
|
113
|
+
fallback: maxEntryBytes,
|
|
114
|
+
name: "DOCX entry read byte limit"
|
|
115
|
+
});
|
|
76
116
|
const work = async () => {
|
|
77
117
|
const entry = zip.file(path);
|
|
78
118
|
if (!entry) return null;
|
|
79
119
|
const buffer = await collectStream({
|
|
80
120
|
stream: entry.nodeStream("nodebuffer"),
|
|
81
|
-
maxEntryBytes,
|
|
121
|
+
maxEntryBytes: Math.min(requestedMaxBytes, maxEntryBytes),
|
|
82
122
|
remainingBytes: maxTotalBytes - totalBytesRead,
|
|
83
123
|
maxTotalBytes,
|
|
84
124
|
path
|
|
@@ -92,15 +132,20 @@ const loadDocxArchive = async (bytes, options = {}) => {
|
|
|
92
132
|
};
|
|
93
133
|
return {
|
|
94
134
|
entries: Object.freeze(archiveEntries.map(({ name }) => name)),
|
|
135
|
+
entryMetadata: Object.freeze(archiveEntries.map((entry) => ({
|
|
136
|
+
path: entry.name,
|
|
137
|
+
directory: entry.dir,
|
|
138
|
+
declaredUncompressedBytes: getDeclaredUncompressedBytes(entry)
|
|
139
|
+
}))),
|
|
95
140
|
async readEntryString(path) {
|
|
96
141
|
const buffer = await readEntry(path);
|
|
97
142
|
return buffer === null ? null : buffer.toString("utf-8");
|
|
98
143
|
},
|
|
99
|
-
async readEntryUint8(path) {
|
|
100
|
-
const buffer = await readEntry(path);
|
|
144
|
+
async readEntryUint8(path, readOptions) {
|
|
145
|
+
const buffer = await readEntry(path, readOptions);
|
|
101
146
|
return buffer === null ? null : new Uint8Array(buffer);
|
|
102
147
|
}
|
|
103
148
|
};
|
|
104
149
|
};
|
|
105
150
|
//#endregion
|
|
106
|
-
export { DOCX_MAX_ENTRIES, DOCX_MAX_ENTRY_BYTES, DOCX_MAX_TOTAL_BYTES, DocxArchiveError, loadDocxArchive };
|
|
151
|
+
export { DOCX_MAX_ENTRIES, DOCX_MAX_ENTRY_BYTES, DOCX_MAX_INPUT_BYTES, DOCX_MAX_TOTAL_BYTES, DocxArchiveError, loadDocxArchive };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { DocxArchiveOptions } from "./boundedArchive.js";
|
|
2
|
+
|
|
3
|
+
//#region src/docx/server/evaluateDocxXmlPatchProposal.d.ts
|
|
4
|
+
declare const FOLIO_DOCX_XML_PATCH_PROPOSAL_VERSION: 1;
|
|
5
|
+
declare const FOLIO_DOCX_XML_PATCH_PROPOSAL_PROFILE: "folio-xml-patch-proposal-v1";
|
|
6
|
+
declare const FOLIO_DOCX_XML_PATCH_PROPOSAL_DEFAULTS: Readonly<{
|
|
7
|
+
readonly maxReplacements: 16;
|
|
8
|
+
readonly maxPartBytes: number;
|
|
9
|
+
readonly maxTotalBytes: number;
|
|
10
|
+
}>;
|
|
11
|
+
declare const FOLIO_DOCX_XML_PATCH_PROPOSAL_ISSUE_CODES: readonly ["invalid-proposal", "unsupported-version", "too-many-replacements", "duplicate-part", "invalid-part-path", "invalid-base-sha256", "part-not-allowed", "part-not-found", "part-not-xml", "replacement-too-large", "replacements-too-large", "xml-doctype-forbidden", "xml-not-well-formed", "xml-encoding-mismatch", "base-hash-mismatch", "package-inspection-failed"];
|
|
12
|
+
type FolioDocxXmlPatchProposalIssueCode = (typeof FOLIO_DOCX_XML_PATCH_PROPOSAL_ISSUE_CODES)[number];
|
|
13
|
+
type FolioDocxXmlReplacement = {
|
|
14
|
+
readonly path: string;
|
|
15
|
+
readonly baseSha256: string;
|
|
16
|
+
readonly replacementXml: string;
|
|
17
|
+
};
|
|
18
|
+
type FolioDocxXmlPatchProposal = {
|
|
19
|
+
readonly version: typeof FOLIO_DOCX_XML_PATCH_PROPOSAL_VERSION;
|
|
20
|
+
readonly replacements: readonly [FolioDocxXmlReplacement, ...FolioDocxXmlReplacement[]];
|
|
21
|
+
};
|
|
22
|
+
type FolioDocxXmlPatchProposalLimits = {
|
|
23
|
+
readonly maxReplacements?: number;
|
|
24
|
+
readonly maxPartBytes?: number;
|
|
25
|
+
readonly maxTotalBytes?: number;
|
|
26
|
+
};
|
|
27
|
+
type EvaluateDocxXmlPatchProposalArgs = {
|
|
28
|
+
readonly bytes: ArrayBuffer | Uint8Array;
|
|
29
|
+
readonly proposal: unknown; /** Exact package paths authorized by the server-side caller. */
|
|
30
|
+
readonly allowedParts: readonly string[];
|
|
31
|
+
readonly archive?: DocxArchiveOptions;
|
|
32
|
+
readonly limits?: FolioDocxXmlPatchProposalLimits;
|
|
33
|
+
};
|
|
34
|
+
type FolioDocxXmlPatchProposalIssue = {
|
|
35
|
+
readonly code: FolioDocxXmlPatchProposalIssueCode;
|
|
36
|
+
readonly message: string;
|
|
37
|
+
readonly proposalPath?: string;
|
|
38
|
+
readonly part?: string;
|
|
39
|
+
};
|
|
40
|
+
type FolioDocxPreparedXmlReplacement = {
|
|
41
|
+
readonly path: string;
|
|
42
|
+
readonly baseSha256: string;
|
|
43
|
+
readonly currentSha256: string;
|
|
44
|
+
readonly replacementSha256: string;
|
|
45
|
+
readonly replacementByteLength: number;
|
|
46
|
+
readonly encoding: "utf-8";
|
|
47
|
+
};
|
|
48
|
+
type FolioDocxXmlPatchProposalEvaluation = {
|
|
49
|
+
readonly version: typeof FOLIO_DOCX_XML_PATCH_PROPOSAL_VERSION;
|
|
50
|
+
readonly profile: typeof FOLIO_DOCX_XML_PATCH_PROPOSAL_PROFILE;
|
|
51
|
+
readonly status: "accepted";
|
|
52
|
+
readonly producesOutput: false;
|
|
53
|
+
readonly issues: readonly [];
|
|
54
|
+
readonly replacements: readonly [FolioDocxPreparedXmlReplacement, ...FolioDocxPreparedXmlReplacement[]];
|
|
55
|
+
readonly limits: Required<FolioDocxXmlPatchProposalLimits>;
|
|
56
|
+
} | {
|
|
57
|
+
readonly version: typeof FOLIO_DOCX_XML_PATCH_PROPOSAL_VERSION;
|
|
58
|
+
readonly profile: typeof FOLIO_DOCX_XML_PATCH_PROPOSAL_PROFILE;
|
|
59
|
+
readonly status: "rejected";
|
|
60
|
+
readonly producesOutput: false;
|
|
61
|
+
readonly issues: readonly [FolioDocxXmlPatchProposalIssue, ...FolioDocxXmlPatchProposalIssue[]];
|
|
62
|
+
readonly replacements: readonly [];
|
|
63
|
+
readonly limits: Required<FolioDocxXmlPatchProposalLimits>;
|
|
64
|
+
};
|
|
65
|
+
declare const InvalidFolioDocxXmlPatchProposalError_base: import("better-result").TaggedErrorClass<"InvalidFolioDocxXmlPatchProposalError", {
|
|
66
|
+
message: string;
|
|
67
|
+
path: string;
|
|
68
|
+
reason: string;
|
|
69
|
+
}>;
|
|
70
|
+
declare class InvalidFolioDocxXmlPatchProposalError extends InvalidFolioDocxXmlPatchProposalError_base {}
|
|
71
|
+
declare const InvalidFolioDocxXmlPatchProposalOptionsError_base: import("better-result").TaggedErrorClass<"InvalidFolioDocxXmlPatchProposalOptionsError", {
|
|
72
|
+
message: string;
|
|
73
|
+
path: string;
|
|
74
|
+
}>;
|
|
75
|
+
declare class InvalidFolioDocxXmlPatchProposalOptionsError extends InvalidFolioDocxXmlPatchProposalOptionsError_base {}
|
|
76
|
+
declare const parseFolioDocxXmlPatchProposal: (value: unknown) => FolioDocxXmlPatchProposal;
|
|
77
|
+
declare const evaluateDocxXmlPatchProposal: ({
|
|
78
|
+
bytes,
|
|
79
|
+
proposal: rawProposal,
|
|
80
|
+
allowedParts,
|
|
81
|
+
archive,
|
|
82
|
+
limits: rawLimits
|
|
83
|
+
}: EvaluateDocxXmlPatchProposalArgs) => Promise<FolioDocxXmlPatchProposalEvaluation>;
|
|
84
|
+
//#endregion
|
|
85
|
+
export { EvaluateDocxXmlPatchProposalArgs, 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, FolioDocxPreparedXmlReplacement, FolioDocxXmlPatchProposal, FolioDocxXmlPatchProposalEvaluation, FolioDocxXmlPatchProposalIssue, FolioDocxXmlPatchProposalIssueCode, FolioDocxXmlPatchProposalLimits, FolioDocxXmlReplacement, InvalidFolioDocxXmlPatchProposalError, InvalidFolioDocxXmlPatchProposalOptionsError, evaluateDocxXmlPatchProposal, parseFolioDocxXmlPatchProposal };
|