@stll/folio-core 0.12.0 → 0.14.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.d.ts +2 -1
- package/dist/ai-edits/apply.js +118 -40
- package/dist/ai-edits/blockRange.js +1 -1
- package/dist/ai-edits/clean-text.js +2 -1
- package/dist/ai-edits/headless.js +3 -3
- package/dist/ai-edits/snapshot.js +21 -3
- package/dist/ai-edits/table-cell-mutations.js +2 -1
- package/dist/ai-edits/table-row-column-mutations.d.ts +9 -1
- package/dist/ai-edits/table-row-column-mutations.js +1 -1
- package/dist/ai-edits/types.d.ts +28 -1
- package/dist/ai-edits/word-diff.js +21 -0
- package/dist/compat/eigenpal.d.ts +3 -3
- package/dist/compat/eigenpal.js +3 -3
- package/dist/controller/headerFooterEditorManager.js +1 -1
- package/dist/controller/hiddenEditorManager.js +12 -1
- package/dist/controller/layoutPipeline.js +1 -1
- package/dist/controller/noteEditorManager.js +1 -1
- package/dist/document-operations.d.ts +13 -13
- package/dist/document-operations.js +31 -21
- package/dist/docx/blockContentParser.js +19 -5
- package/dist/docx/commentRangeIntegrity.d.ts +6 -0
- package/dist/docx/commentRangeIntegrity.js +186 -0
- package/dist/docx/conformance.js +57 -3
- package/dist/docx/encryption/agileDecryption.js +12 -2
- package/dist/docx/encryption/compoundFile.js +6 -1
- package/dist/docx/encryption/encryptionInfo.d.ts +9 -1
- package/dist/docx/encryption/encryptionInfo.js +18 -2
- package/dist/docx/encryption/openEncryptedDocx.js +1 -1
- package/dist/docx/ensureParaIds.js +7 -6
- package/dist/docx/groupDrawingParser.js +10 -5
- package/dist/docx/hyperlinkParser.js +1 -1
- package/dist/docx/imageParser.js +10 -6
- package/dist/docx/metadataPrivacy.js +17 -3
- package/dist/docx/paragraphParser.js +7 -3
- package/dist/docx/rezip.js +19 -8
- package/dist/docx/runParser.js +13 -4
- package/dist/docx/sectionParser.js +17 -4
- package/dist/docx/selectiveSave.js +4 -0
- package/dist/docx/selectiveXmlPatch.d.ts +22 -1
- package/dist/docx/selectiveXmlPatch.js +61 -1
- package/dist/docx/serializer/blockSdtSerializer.js +3 -2
- package/dist/docx/serializer/commentSerializer.js +4 -4
- package/dist/docx/serializer/paragraphSerializer.js +16 -14
- package/dist/docx/serializer/runSerializer.js +21 -19
- package/dist/docx/serializer/sectionPropertiesSerializer.js +2 -1
- package/dist/docx/serializer/tableSerializer.js +9 -7
- package/dist/docx/serializer/xmlUtils.d.ts +13 -1
- package/dist/docx/serializer/xmlUtils.js +27 -1
- package/dist/docx/server/docxToMarkdown.d.ts +15 -0
- package/dist/docx/server/docxToMarkdown.js +17 -0
- package/dist/docx/server/extractDocxText.js +55 -8
- package/dist/docx/settingsParser.js +15 -2
- package/dist/docx/tableParser.d.ts +8 -1
- package/dist/docx/tableParser.js +29 -8
- package/dist/docx/unzip.js +1 -0
- package/dist/docx/vmlImageParser.js +11 -3
- package/dist/docx/xmlParser.js +11 -1
- package/dist/fonts/embeddedFonts.d.ts +33 -5
- package/dist/fonts/embeddedFonts.js +37 -8
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/layout-bridge/convert/toFlowBlocks.js +15 -0
- package/dist/layout-engine/index.js +19 -12
- package/dist/layout-engine/measure/cache.js +2 -0
- package/dist/layout-engine/measure/effectiveLineBreakPolicy.js +8 -2
- package/dist/layout-engine/measure/floatingTablePosition.d.ts +9 -0
- package/dist/layout-engine/measure/floatingTablePosition.js +17 -0
- package/dist/layout-engine/measure/index.d.ts +2 -1
- package/dist/layout-engine/measure/index.js +2 -1
- package/dist/layout-engine/measure/lineBreakProvider.d.ts +2 -2
- package/dist/layout-engine/measure/lineBreakProvider.js +3 -3
- package/dist/layout-engine/measure/measureBlocks.js +11 -8
- package/dist/layout-engine/measure/measureHelpers.d.ts +10 -1
- package/dist/layout-engine/measure/measureHelpers.js +12 -1
- package/dist/layout-engine/measure/measureParagraph.js +16 -0
- package/dist/layout-engine/types.d.ts +22 -2
- package/dist/layout-painter/renderImage.d.ts +12 -1
- package/dist/layout-painter/renderImage.js +21 -4
- package/dist/layout-painter/renderPage.js +5 -8
- package/dist/layout-painter/renderParagraph.js +33 -9
- package/dist/layout-painter/renderTable.js +2 -1
- package/dist/layout-painter/renderWatermark.d.ts +1 -1
- package/dist/layout-painter/renderWatermark.js +2 -1
- package/dist/markdown/fromMarkdown.js +1 -1
- package/dist/prosemirror/attrs/index.d.ts +1 -1
- package/dist/prosemirror/attrs/index.js +21 -0
- package/dist/prosemirror/commands/comments.d.ts +76 -1
- package/dist/prosemirror/commands/comments.js +401 -28
- package/dist/prosemirror/commands/hyperlink.js +15 -1
- package/dist/prosemirror/commands/image.js +7 -1
- package/dist/prosemirror/commands/index.d.ts +2 -2
- package/dist/prosemirror/commands/index.js +2 -2
- package/dist/prosemirror/commands/tableCellMergeResolution.js +1 -1
- package/dist/prosemirror/commentIdAllocator.js +13 -3
- package/dist/prosemirror/conversion/fromProseDoc.js +122 -5
- package/dist/prosemirror/conversion/index.js +1 -1
- package/dist/prosemirror/conversion/toProseDoc.js +20 -8
- package/dist/prosemirror/extensions/core/ParagraphExtension.js +2 -1
- package/dist/prosemirror/extensions/features/ImagePasteExtension.js +10 -2
- package/dist/prosemirror/extensions/features/pasteCleanup.js +15 -2
- package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +30 -4
- package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +49 -13
- package/dist/prosemirror/extensions/nodes/TableExtension.d.ts +1 -1
- package/dist/prosemirror/extensions/nodes/TableExtension.js +5 -3
- package/dist/prosemirror/index.d.ts +1 -1
- package/dist/prosemirror/index.js +1 -1
- package/dist/prosemirror/plugins/contentControlWidgets.d.ts +2 -2
- package/dist/prosemirror/plugins/contentControlWidgets.js +4 -4
- package/dist/prosemirror/plugins/revisionIds.d.ts +23 -0
- package/dist/prosemirror/plugins/revisionIds.js +57 -0
- package/dist/prosemirror/plugins/suggestionMode.js +4 -3
- package/dist/prosemirror/runFormattingMarkNames.d.ts +14 -0
- package/dist/prosemirror/runFormattingMarkNames.js +40 -0
- package/dist/prosemirror/schema/index.d.ts +1 -1
- package/dist/prosemirror/schema/marks.d.ts +27 -4
- package/dist/prosemirror/schema/marks.js +15 -0
- package/dist/prosemirror/schema/nodes.d.ts +50 -2
- package/dist/prosemirror/utils/tabCalculator.js +3 -3
- package/dist/render-dom/RemoteSelectionOverlay.js +2 -2
- package/dist/server.d.ts +3 -2
- package/dist/server.js +4 -3
- package/dist/utils/clipboard.d.ts +10 -1
- package/dist/utils/clipboard.js +33 -6
- package/dist/utils/colorResolver.d.ts +8 -1
- package/dist/utils/colorResolver.js +35 -5
- package/dist/utils/fontResolver.d.ts +2 -1
- package/dist/utils/fontResolver.js +9 -3
- package/dist/utils/hexId.d.ts +11 -1
- package/dist/utils/hexId.js +11 -1
- package/dist/utils/sanitizeImageSrc.d.ts +21 -0
- package/dist/utils/sanitizeImageSrc.js +30 -0
- package/dist/utils/stripXmlDeclarations.d.ts +11 -0
- package/dist/utils/stripXmlDeclarations.js +35 -0
- package/dist/version-comparison.d.ts +22 -2
- package/dist/version-comparison.js +41 -12
- package/dist/watermark/index.d.ts +11 -1
- package/dist/watermark/index.js +22 -1
- package/package.json +3 -2
|
@@ -25,6 +25,16 @@ import { EditorView } from "prosemirror-view";
|
|
|
25
25
|
const normalizeSeedState = (state) => ensureBaseDirectionInState(ensureParaIdsInState(state));
|
|
26
26
|
const normalizeLocalSeedState = (state) => ensureBaseDirectionInState(state);
|
|
27
27
|
const isObjectRecord = (value) => typeof value === "object" && value !== null;
|
|
28
|
+
/**
|
|
29
|
+
* A remote collaborator's awareness `user.color` is read from another
|
|
30
|
+
* peer's Yjs awareness state — effectively untrusted network input — and
|
|
31
|
+
* gets fed straight into `element.style.background`/`backgroundColor` (see
|
|
32
|
+
* `RemoteSelectionOverlay`). Restrict it to a plain 6-digit hex color before
|
|
33
|
+
* accepting it so a crafted value (e.g. a CSS `url(...)` payload) can't ride
|
|
34
|
+
* along as a CSS-injection / beacon vector; anything else falls back to the
|
|
35
|
+
* default selection color below.
|
|
36
|
+
*/
|
|
37
|
+
const AWARENESS_HEX_COLOR_PATTERN = /^#[0-9A-Fa-f]{6}$/u;
|
|
28
38
|
const isYSyncState = (value, yjs) => {
|
|
29
39
|
if (!isObjectRecord(value)) return false;
|
|
30
40
|
const binding = value["binding"];
|
|
@@ -48,8 +58,9 @@ const readAwarenessUser = (state, clientId) => {
|
|
|
48
58
|
name: `User ${clientId}`
|
|
49
59
|
};
|
|
50
60
|
const user = state["user"];
|
|
61
|
+
const rawColor = user["color"];
|
|
51
62
|
return {
|
|
52
|
-
color: typeof
|
|
63
|
+
color: typeof rawColor === "string" && AWARENESS_HEX_COLOR_PATTERN.test(rawColor) ? rawColor : "var(--doc-image-selection)",
|
|
53
64
|
name: typeof user["name"] === "string" ? user["name"] : `User ${clientId}`
|
|
54
65
|
};
|
|
55
66
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getDocumentWatermark } from "../watermark/index.js";
|
|
1
2
|
import "../layout-engine/types.js";
|
|
2
3
|
import { resolveEffectiveParagraphSpacingTree } from "../layout-engine/paragraphSpacing.js";
|
|
3
4
|
import { renderPages } from "../layout-painter/renderPage.js";
|
|
@@ -20,7 +21,6 @@ import { resolveJustificationCompatibility } from "../layout-engine/justificatio
|
|
|
20
21
|
import { measureBlocks, measureSingleBlockWithoutFloatingZones } from "../layout-engine/measure/measureBlocks.js";
|
|
21
22
|
import { tryBuildIncrementalMeasures } from "../paged-layout/incrementalMeasure.js";
|
|
22
23
|
import { computePerBlockMeasureInputs } from "../paged-layout/sectionBlockWidths.js";
|
|
23
|
-
import { getDocumentWatermark } from "../watermark/index.js";
|
|
24
24
|
//#region src/controller/layoutPipeline.ts
|
|
25
25
|
function bodyMarginsClearHeaderFooter({ authoredMargins, preparedHeader, preparedFooter }) {
|
|
26
26
|
const headerBottom = preparedHeader ? (authoredMargins.header ?? 0) + (preparedHeader.marginPushBottom ?? preparedHeader.height) : authoredMargins.top;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { proseDocToBlocks } from "../prosemirror/conversion/fromProseDoc.js";
|
|
2
1
|
import { ExtensionManager } from "../prosemirror/extensions/ExtensionManager.js";
|
|
3
2
|
import { ensureBaseDirectionInState } from "../prosemirror/extensions/features/AutoBidiDetectionExtension.js";
|
|
4
3
|
import { createDocumentStylesPlugin } from "../prosemirror/plugins/documentStyles.js";
|
|
@@ -6,6 +5,7 @@ import { ensureParaIdsInState } from "../prosemirror/extensions/features/ParaIdA
|
|
|
6
5
|
import { createStarterKit } from "../prosemirror/extensions/StarterKit.js";
|
|
7
6
|
import { schema } from "../prosemirror/schema/index.js";
|
|
8
7
|
import { footnoteToProseDoc } from "../prosemirror/conversion/toProseDoc.js";
|
|
8
|
+
import { proseDocToBlocks } from "../prosemirror/conversion/fromProseDoc.js";
|
|
9
9
|
import { isSeparatorEndnote, isSeparatorFootnote } from "../docx/footnoteParser.js";
|
|
10
10
|
import { EditorState } from "prosemirror-state";
|
|
11
11
|
import { EditorView } from "prosemirror-view";
|
|
@@ -4,7 +4,7 @@ import { FolioAIEditView } from "./ai-edits/apply.js";
|
|
|
4
4
|
//#region src/document-operations.d.ts
|
|
5
5
|
declare const FOLIO_DOCUMENT_OPERATION_CONTRACT_VERSION: 1;
|
|
6
6
|
declare const FOLIO_DOCUMENT_OPERATION_TYPES: readonly ["replaceInBlock", "replaceRange", "commentOnRange", "formatRange", "insertAfterBlock", "insertBeforeBlock", "replaceBlock", "deleteBlock", "commentOnBlock", "insertSignatureTable", "insertTableRow", "deleteTableRow", "insertTableColumn", "deleteTableColumn", "mergeTableCells", "splitTableCell"];
|
|
7
|
-
declare const FOLIO_DOCUMENT_OPERATION_MODES: readonly ["direct", "tracked-changes"];
|
|
7
|
+
declare const FOLIO_DOCUMENT_OPERATION_MODES: readonly ["direct", "tracked-changes", "suggested"];
|
|
8
8
|
declare const FOLIO_DOCUMENT_OPERATION_STORIES: readonly ["main", "header", "footer", "footnote", "endnote"];
|
|
9
9
|
declare const FOLIO_DOCUMENT_OPERATION_PRECONDITIONS: readonly ["blockTextHash"];
|
|
10
10
|
declare const FOLIO_DOCUMENT_OPERATION_BATCH_MODES: readonly ["best-effort", "atomic"];
|
|
@@ -13,20 +13,20 @@ type FolioDocumentOperationMode = FolioAIEditApplyMode;
|
|
|
13
13
|
type FolioDocumentOperationPrecondition = FolioAIEditPrecondition;
|
|
14
14
|
type FolioDocumentOperationType = FolioDocumentOperation["type"];
|
|
15
15
|
declare const FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE: Readonly<{
|
|
16
|
-
readonly replaceInBlock: readonly ["direct", "tracked-changes"];
|
|
17
|
-
readonly replaceRange: readonly ["direct", "tracked-changes"];
|
|
16
|
+
readonly replaceInBlock: readonly ["direct", "tracked-changes", "suggested"];
|
|
17
|
+
readonly replaceRange: readonly ["direct", "tracked-changes", "suggested"];
|
|
18
18
|
readonly commentOnRange: readonly ["direct", "tracked-changes"];
|
|
19
|
-
readonly formatRange: readonly ["direct", "tracked-changes"];
|
|
20
|
-
readonly insertAfterBlock: readonly ["direct", "tracked-changes"];
|
|
21
|
-
readonly insertBeforeBlock: readonly ["direct", "tracked-changes"];
|
|
22
|
-
readonly replaceBlock: readonly ["direct", "tracked-changes"];
|
|
23
|
-
readonly deleteBlock: readonly ["direct", "tracked-changes"];
|
|
19
|
+
readonly formatRange: readonly ["direct", "tracked-changes", "suggested"];
|
|
20
|
+
readonly insertAfterBlock: readonly ["direct", "tracked-changes", "suggested"];
|
|
21
|
+
readonly insertBeforeBlock: readonly ["direct", "tracked-changes", "suggested"];
|
|
22
|
+
readonly replaceBlock: readonly ["direct", "tracked-changes", "suggested"];
|
|
23
|
+
readonly deleteBlock: readonly ["direct", "tracked-changes", "suggested"];
|
|
24
24
|
readonly commentOnBlock: readonly ["direct", "tracked-changes"];
|
|
25
|
-
readonly insertSignatureTable: readonly ["direct"];
|
|
26
|
-
readonly insertTableRow: readonly ["direct", "tracked-changes"];
|
|
27
|
-
readonly deleteTableRow: readonly ["direct", "tracked-changes"];
|
|
28
|
-
readonly insertTableColumn: readonly ["direct", "tracked-changes"];
|
|
29
|
-
readonly deleteTableColumn: readonly ["direct", "tracked-changes"];
|
|
25
|
+
readonly insertSignatureTable: readonly ["direct", "suggested"];
|
|
26
|
+
readonly insertTableRow: readonly ["direct", "tracked-changes", "suggested"];
|
|
27
|
+
readonly deleteTableRow: readonly ["direct", "tracked-changes", "suggested"];
|
|
28
|
+
readonly insertTableColumn: readonly ["direct", "tracked-changes", "suggested"];
|
|
29
|
+
readonly deleteTableColumn: readonly ["direct", "tracked-changes", "suggested"];
|
|
30
30
|
readonly mergeTableCells: readonly ["direct", "tracked-changes"];
|
|
31
31
|
readonly splitTableCell: readonly ["direct", "tracked-changes"];
|
|
32
32
|
}>;
|
|
@@ -20,7 +20,11 @@ const FOLIO_DOCUMENT_OPERATION_TYPES = Object.freeze([
|
|
|
20
20
|
"mergeTableCells",
|
|
21
21
|
"splitTableCell"
|
|
22
22
|
]);
|
|
23
|
-
const FOLIO_DOCUMENT_OPERATION_MODES = Object.freeze([
|
|
23
|
+
const FOLIO_DOCUMENT_OPERATION_MODES = Object.freeze([
|
|
24
|
+
"direct",
|
|
25
|
+
"tracked-changes",
|
|
26
|
+
"suggested"
|
|
27
|
+
]);
|
|
24
28
|
const FOLIO_DOCUMENT_OPERATION_STORIES = Object.freeze([
|
|
25
29
|
"main",
|
|
26
30
|
"header",
|
|
@@ -30,23 +34,24 @@ const FOLIO_DOCUMENT_OPERATION_STORIES = Object.freeze([
|
|
|
30
34
|
]);
|
|
31
35
|
const FOLIO_DOCUMENT_OPERATION_PRECONDITIONS = Object.freeze(["blockTextHash"]);
|
|
32
36
|
const FOLIO_DOCUMENT_OPERATION_BATCH_MODES = Object.freeze(["best-effort", "atomic"]);
|
|
33
|
-
const DIRECT_AND_TRACKED_MODES =
|
|
34
|
-
const
|
|
37
|
+
const DIRECT_AND_TRACKED_MODES = Object.freeze(["direct", "tracked-changes"]);
|
|
38
|
+
const DIRECT_TRACKED_AND_SUGGESTED_MODES = FOLIO_DOCUMENT_OPERATION_MODES;
|
|
39
|
+
const DIRECT_AND_SUGGESTED_MODES = Object.freeze(["direct", "suggested"]);
|
|
35
40
|
const FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE = Object.freeze({
|
|
36
|
-
replaceInBlock:
|
|
37
|
-
replaceRange:
|
|
41
|
+
replaceInBlock: DIRECT_TRACKED_AND_SUGGESTED_MODES,
|
|
42
|
+
replaceRange: DIRECT_TRACKED_AND_SUGGESTED_MODES,
|
|
38
43
|
commentOnRange: DIRECT_AND_TRACKED_MODES,
|
|
39
|
-
formatRange:
|
|
40
|
-
insertAfterBlock:
|
|
41
|
-
insertBeforeBlock:
|
|
42
|
-
replaceBlock:
|
|
43
|
-
deleteBlock:
|
|
44
|
+
formatRange: DIRECT_TRACKED_AND_SUGGESTED_MODES,
|
|
45
|
+
insertAfterBlock: DIRECT_TRACKED_AND_SUGGESTED_MODES,
|
|
46
|
+
insertBeforeBlock: DIRECT_TRACKED_AND_SUGGESTED_MODES,
|
|
47
|
+
replaceBlock: DIRECT_TRACKED_AND_SUGGESTED_MODES,
|
|
48
|
+
deleteBlock: DIRECT_TRACKED_AND_SUGGESTED_MODES,
|
|
44
49
|
commentOnBlock: DIRECT_AND_TRACKED_MODES,
|
|
45
|
-
insertSignatureTable:
|
|
46
|
-
insertTableRow:
|
|
47
|
-
deleteTableRow:
|
|
48
|
-
insertTableColumn:
|
|
49
|
-
deleteTableColumn:
|
|
50
|
+
insertSignatureTable: DIRECT_AND_SUGGESTED_MODES,
|
|
51
|
+
insertTableRow: DIRECT_TRACKED_AND_SUGGESTED_MODES,
|
|
52
|
+
deleteTableRow: DIRECT_TRACKED_AND_SUGGESTED_MODES,
|
|
53
|
+
insertTableColumn: DIRECT_TRACKED_AND_SUGGESTED_MODES,
|
|
54
|
+
deleteTableColumn: DIRECT_TRACKED_AND_SUGGESTED_MODES,
|
|
50
55
|
mergeTableCells: DIRECT_AND_TRACKED_MODES,
|
|
51
56
|
splitTableCell: DIRECT_AND_TRACKED_MODES
|
|
52
57
|
});
|
|
@@ -63,8 +68,8 @@ const DOCUMENT_OPERATION_CAPABILITIES = Object.freeze({
|
|
|
63
68
|
const getFolioDocumentOperationCapabilities = () => DOCUMENT_OPERATION_CAPABILITIES;
|
|
64
69
|
const includesDocumentOperationMode = (supportedModes, mode) => supportedModes.includes(mode);
|
|
65
70
|
const isFolioDocumentOperationModeSupported = (operationType, mode) => {
|
|
71
|
+
if (!Object.hasOwn(FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE, operationType)) return false;
|
|
66
72
|
const supportedModes = FOLIO_DOCUMENT_OPERATION_MODES_BY_TYPE[operationType];
|
|
67
|
-
if (supportedModes === void 0) return false;
|
|
68
73
|
return includesDocumentOperationMode(supportedModes, mode);
|
|
69
74
|
};
|
|
70
75
|
const isSupportedFolioDocumentOperationVersion = (value) => value === 1;
|
|
@@ -204,7 +209,8 @@ const COMMON_OPERATION_KEYS = [
|
|
|
204
209
|
"blockId",
|
|
205
210
|
"severity",
|
|
206
211
|
"area",
|
|
207
|
-
"precondition"
|
|
212
|
+
"precondition",
|
|
213
|
+
"suggestionId"
|
|
208
214
|
];
|
|
209
215
|
const parseSignatureParties = (value, path) => {
|
|
210
216
|
const parties = value["parties"];
|
|
@@ -232,10 +238,12 @@ const parseDocumentOperation = (value, index) => {
|
|
|
232
238
|
const type = readString(value, "type", path);
|
|
233
239
|
const reviewMeta = readReviewMeta(value, path);
|
|
234
240
|
const precondition = readOptionalPrecondition(value, path);
|
|
241
|
+
const suggestionId = readOptionalString(value, "suggestionId", path);
|
|
235
242
|
const comment = readOptionalComment(value, path);
|
|
236
243
|
const operationMeta = {
|
|
237
244
|
...reviewMeta,
|
|
238
|
-
...precondition !== void 0 && { precondition }
|
|
245
|
+
...precondition !== void 0 && { precondition },
|
|
246
|
+
...suggestionId !== void 0 && { suggestionId }
|
|
239
247
|
};
|
|
240
248
|
if (type === "replaceRange") {
|
|
241
249
|
assertAllowedKeys(value, path, [
|
|
@@ -246,7 +254,8 @@ const parseDocumentOperation = (value, index) => {
|
|
|
246
254
|
"comment",
|
|
247
255
|
"severity",
|
|
248
256
|
"area",
|
|
249
|
-
"precondition"
|
|
257
|
+
"precondition",
|
|
258
|
+
"suggestionId"
|
|
250
259
|
]);
|
|
251
260
|
return {
|
|
252
261
|
...operationMeta,
|
|
@@ -284,7 +293,8 @@ const parseDocumentOperation = (value, index) => {
|
|
|
284
293
|
"formatting",
|
|
285
294
|
"severity",
|
|
286
295
|
"area",
|
|
287
|
-
"precondition"
|
|
296
|
+
"precondition",
|
|
297
|
+
"suggestionId"
|
|
288
298
|
]);
|
|
289
299
|
return {
|
|
290
300
|
...operationMeta,
|
|
@@ -510,7 +520,7 @@ const parseFolioDocumentOperationBatch = (value) => {
|
|
|
510
520
|
const operations = value["operations"];
|
|
511
521
|
if (!Array.isArray(operations)) return invalidBatch("$.operations", "expected an array");
|
|
512
522
|
const mode = value["mode"];
|
|
513
|
-
if (mode !== void 0 && mode !== "direct" && mode !== "tracked-changes") return invalidBatch("$.mode", "expected \"direct\"
|
|
523
|
+
if (mode !== void 0 && mode !== "direct" && mode !== "tracked-changes" && mode !== "suggested") return invalidBatch("$.mode", "expected \"direct\", \"tracked-changes\", or \"suggested\" when provided");
|
|
514
524
|
const atomic = readOptionalBoolean(value, "atomic", "$");
|
|
515
525
|
const dryRun = readOptionalBoolean(value, "dryRun", "$");
|
|
516
526
|
const parsedOperations = operations.map(parseDocumentOperation);
|
|
@@ -53,7 +53,7 @@ const formatNumber = (value, numFmt) => {
|
|
|
53
53
|
default: return String(value);
|
|
54
54
|
}
|
|
55
55
|
};
|
|
56
|
-
const computeListMarker = (paragraph, { numbering, listCounters, abstractCounters, restartedNumIds, previousList }) => {
|
|
56
|
+
const computeListMarker = (paragraph, { numbering, listCounters, abstractCounters, restartedNumIds, previousList, siblingNumIdsByAbstractNumId }) => {
|
|
57
57
|
const listRendering = paragraph.listRendering;
|
|
58
58
|
if (!listRendering || !numbering) {
|
|
59
59
|
previousList.abstractNumId = null;
|
|
@@ -73,6 +73,14 @@ const computeListMarker = (paragraph, { numbering, listCounters, abstractCounter
|
|
|
73
73
|
const counters = listCounters.get(numId);
|
|
74
74
|
if (!counters) return;
|
|
75
75
|
const abstractNumId = numbering.getAbstractNumId(numId);
|
|
76
|
+
if (firstEncounter && abstractNumId !== null) {
|
|
77
|
+
let siblingNumIds = siblingNumIdsByAbstractNumId.get(abstractNumId);
|
|
78
|
+
if (!siblingNumIds) {
|
|
79
|
+
siblingNumIds = /* @__PURE__ */ new Set();
|
|
80
|
+
siblingNumIdsByAbstractNumId.set(abstractNumId, siblingNumIds);
|
|
81
|
+
}
|
|
82
|
+
siblingNumIds.add(numId);
|
|
83
|
+
}
|
|
76
84
|
const styleNumbering = paragraph.formatting?.numPrFromStyle;
|
|
77
85
|
let resumedAbstractCounters;
|
|
78
86
|
const resumesRestartedInstance = firstEncounter && listRendering.startOverride === void 0 && abstractNumId !== null && previousList.abstractNumId === abstractNumId && previousList.numId !== null && previousList.numId !== numId && restartedNumIds.has(previousList.numId);
|
|
@@ -101,9 +109,13 @@ const computeListMarker = (paragraph, { numbering, listCounters, abstractCounter
|
|
|
101
109
|
counters[level + 1] = (childCounter === void 0 || Number.isNaN(childCounter) ? 0 : childCounter) + childAdvances;
|
|
102
110
|
}
|
|
103
111
|
if (abstractNumId !== null) {
|
|
104
|
-
if (resumedAbstractCounters)
|
|
105
|
-
|
|
106
|
-
for (
|
|
112
|
+
if (resumedAbstractCounters) {
|
|
113
|
+
const siblingNumIds = siblingNumIdsByAbstractNumId.get(abstractNumId);
|
|
114
|
+
for (const otherNumId of siblingNumIds ?? []) {
|
|
115
|
+
const otherCounters = listCounters.get(otherNumId);
|
|
116
|
+
if (otherNumId === numId || !otherCounters || !otherCounters.every((value, index) => Object.is(value, resumedAbstractCounters[index]))) continue;
|
|
117
|
+
for (let i = 0; i < otherCounters.length; i += 1) otherCounters[i] = counters[i] ?? NaN;
|
|
118
|
+
}
|
|
107
119
|
}
|
|
108
120
|
abstractCounters.set(abstractNumId, [...counters]);
|
|
109
121
|
}
|
|
@@ -140,6 +152,7 @@ const parseBlockContent = (parent, styles, theme, numbering, rels, media, option
|
|
|
140
152
|
fromStyle: false,
|
|
141
153
|
numId: null
|
|
142
154
|
},
|
|
155
|
+
siblingNumIdsByAbstractNumId: /* @__PURE__ */ new Map(),
|
|
143
156
|
options: {
|
|
144
157
|
...options,
|
|
145
158
|
rootXmlns: mergeXmlnsDeclarations(options?.rootXmlns ?? {}, parent)
|
|
@@ -160,7 +173,8 @@ const parseBlockContentWithState = (parent, styles, theme, numbering, rels, medi
|
|
|
160
173
|
listCounters: state.listCounters,
|
|
161
174
|
abstractCounters: state.abstractCounters,
|
|
162
175
|
restartedNumIds: state.restartedNumIds,
|
|
163
|
-
previousList: state.previousList
|
|
176
|
+
previousList: state.previousList,
|
|
177
|
+
siblingNumIdsByAbstractNumId: state.siblingNumIdsByAbstractNumId
|
|
164
178
|
});
|
|
165
179
|
content.push(paragraph);
|
|
166
180
|
continue;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { document_d_exports } from "../types/document.js";
|
|
2
|
+
|
|
3
|
+
//#region src/docx/commentRangeIntegrity.d.ts
|
|
4
|
+
declare const withoutOrphanCommentRanges: (doc: document_d_exports.Document) => document_d_exports.Document;
|
|
5
|
+
//#endregion
|
|
6
|
+
export { withoutOrphanCommentRanges };
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
//#region src/docx/commentRangeIntegrity.ts
|
|
2
|
+
const withoutOrphanCommentRanges = (doc) => {
|
|
3
|
+
const validCommentIds = new Set((doc.package.document.comments ?? []).map((comment) => comment.id));
|
|
4
|
+
const document = withoutOrphanDocumentBodyMarkers(doc.package.document, validCommentIds);
|
|
5
|
+
const headers = withoutOrphanHeaderFooterMapMarkers(doc.package.headers, validCommentIds);
|
|
6
|
+
const footers = withoutOrphanHeaderFooterMapMarkers(doc.package.footers, validCommentIds);
|
|
7
|
+
const footnotes = withoutOrphanNoteMarkers(doc.package.footnotes, validCommentIds);
|
|
8
|
+
const endnotes = withoutOrphanNoteMarkers(doc.package.endnotes, validCommentIds);
|
|
9
|
+
if (!document && !headers && !footers && !footnotes && !endnotes) return doc;
|
|
10
|
+
return {
|
|
11
|
+
...doc,
|
|
12
|
+
package: {
|
|
13
|
+
...doc.package,
|
|
14
|
+
...document ? { document } : {},
|
|
15
|
+
...headers ? { headers } : {},
|
|
16
|
+
...footers ? { footers } : {},
|
|
17
|
+
...footnotes ? { footnotes } : {},
|
|
18
|
+
...endnotes ? { endnotes } : {}
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
const withoutOrphanDocumentBodyMarkers = (document, validCommentIds) => {
|
|
23
|
+
const content = withoutOrphanBlockMarkers(document.content, validCommentIds);
|
|
24
|
+
const comments = withoutOrphanCommentBodyMarkers(document.comments, validCommentIds);
|
|
25
|
+
if (!content && !comments) return null;
|
|
26
|
+
return {
|
|
27
|
+
...document,
|
|
28
|
+
...content ? { content } : {},
|
|
29
|
+
...comments ? { comments } : {}
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
const withoutOrphanCommentBodyMarkers = (comments, validCommentIds) => {
|
|
33
|
+
if (!comments) return null;
|
|
34
|
+
let changed = false;
|
|
35
|
+
const next = [];
|
|
36
|
+
for (const comment of comments) {
|
|
37
|
+
const content = withoutOrphanParagraphListMarkers(comment.content, validCommentIds);
|
|
38
|
+
if (!content) {
|
|
39
|
+
next.push(comment);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
changed = true;
|
|
43
|
+
next.push({
|
|
44
|
+
...comment,
|
|
45
|
+
content
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return changed ? next : null;
|
|
49
|
+
};
|
|
50
|
+
const withoutOrphanParagraphListMarkers = (paragraphs, validCommentIds) => {
|
|
51
|
+
let changed = false;
|
|
52
|
+
const next = [];
|
|
53
|
+
for (const paragraph of paragraphs) {
|
|
54
|
+
const nextParagraph = withoutOrphanParagraphMarkers(paragraph, validCommentIds);
|
|
55
|
+
if (nextParagraph !== paragraph) changed = true;
|
|
56
|
+
next.push(nextParagraph);
|
|
57
|
+
}
|
|
58
|
+
return changed ? next : null;
|
|
59
|
+
};
|
|
60
|
+
const withoutOrphanHeaderFooterMapMarkers = (map, validCommentIds) => {
|
|
61
|
+
if (!map) return null;
|
|
62
|
+
let changed = false;
|
|
63
|
+
const next = /* @__PURE__ */ new Map();
|
|
64
|
+
for (const [rId, headerFooter] of map) {
|
|
65
|
+
const content = withoutOrphanBlockMarkers(headerFooter.content, validCommentIds);
|
|
66
|
+
if (!content) {
|
|
67
|
+
next.set(rId, headerFooter);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
changed = true;
|
|
71
|
+
next.set(rId, {
|
|
72
|
+
...headerFooter,
|
|
73
|
+
content
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return changed ? next : null;
|
|
77
|
+
};
|
|
78
|
+
const withoutOrphanNoteMarkers = (notes, validCommentIds) => {
|
|
79
|
+
if (!notes) return null;
|
|
80
|
+
let changed = false;
|
|
81
|
+
const next = [];
|
|
82
|
+
for (const note of notes) {
|
|
83
|
+
const content = withoutOrphanBlockMarkers(note.content, validCommentIds);
|
|
84
|
+
if (!content) {
|
|
85
|
+
next.push(note);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
changed = true;
|
|
89
|
+
next.push({
|
|
90
|
+
...note,
|
|
91
|
+
content
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return changed ? next : null;
|
|
95
|
+
};
|
|
96
|
+
const withoutOrphanBlockMarkers = (blocks, validCommentIds) => {
|
|
97
|
+
let changed = false;
|
|
98
|
+
const next = [];
|
|
99
|
+
for (const block of blocks) {
|
|
100
|
+
const nextBlock = withoutOrphanBlockMarker(block, validCommentIds);
|
|
101
|
+
if (nextBlock !== block) changed = true;
|
|
102
|
+
next.push(nextBlock);
|
|
103
|
+
}
|
|
104
|
+
return changed ? next : null;
|
|
105
|
+
};
|
|
106
|
+
const withoutOrphanBlockMarker = (block, validCommentIds) => {
|
|
107
|
+
if (block.type === "paragraph") return withoutOrphanParagraphMarkers(block, validCommentIds);
|
|
108
|
+
if (block.type === "table") return withoutOrphanTableMarkers(block, validCommentIds);
|
|
109
|
+
if (!("content" in block) || !block.content) return block;
|
|
110
|
+
const content = withoutOrphanBlockMarkers(block.content, validCommentIds);
|
|
111
|
+
if (!content) return block;
|
|
112
|
+
return {
|
|
113
|
+
...block,
|
|
114
|
+
content
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
const withoutOrphanTableMarkers = (table, validCommentIds) => {
|
|
118
|
+
let changed = false;
|
|
119
|
+
const rows = [];
|
|
120
|
+
for (const row of table.rows) {
|
|
121
|
+
const nextRow = withoutOrphanTableRowMarkers(row, validCommentIds);
|
|
122
|
+
if (nextRow !== row) changed = true;
|
|
123
|
+
rows.push(nextRow);
|
|
124
|
+
}
|
|
125
|
+
if (!changed) return table;
|
|
126
|
+
return {
|
|
127
|
+
...table,
|
|
128
|
+
rows
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
const withoutOrphanTableRowMarkers = (row, validCommentIds) => {
|
|
132
|
+
let changed = false;
|
|
133
|
+
const cells = [];
|
|
134
|
+
for (const cell of row.cells) {
|
|
135
|
+
const nextCell = withoutOrphanTableCellMarkers(cell, validCommentIds);
|
|
136
|
+
if (nextCell !== cell) changed = true;
|
|
137
|
+
cells.push(nextCell);
|
|
138
|
+
}
|
|
139
|
+
if (!changed) return row;
|
|
140
|
+
return {
|
|
141
|
+
...row,
|
|
142
|
+
cells
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
const withoutOrphanTableCellMarkers = (cell, validCommentIds) => {
|
|
146
|
+
const content = withoutOrphanTableCellBlockMarkers(cell.content, validCommentIds);
|
|
147
|
+
if (!content) return cell;
|
|
148
|
+
return {
|
|
149
|
+
...cell,
|
|
150
|
+
content
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
const withoutOrphanTableCellBlockMarkers = (blocks, validCommentIds) => {
|
|
154
|
+
let changed = false;
|
|
155
|
+
const next = [];
|
|
156
|
+
for (const block of blocks) {
|
|
157
|
+
const nextBlock = withoutOrphanTableCellBlockMarker(block, validCommentIds);
|
|
158
|
+
if (nextBlock !== block) changed = true;
|
|
159
|
+
next.push(nextBlock);
|
|
160
|
+
}
|
|
161
|
+
return changed ? next : null;
|
|
162
|
+
};
|
|
163
|
+
const withoutOrphanTableCellBlockMarker = (block, validCommentIds) => {
|
|
164
|
+
if (block.type === "paragraph") return withoutOrphanParagraphMarkers(block, validCommentIds);
|
|
165
|
+
if (block.type === "table") return withoutOrphanTableMarkers(block, validCommentIds);
|
|
166
|
+
return block;
|
|
167
|
+
};
|
|
168
|
+
const withoutOrphanParagraphMarkers = (paragraph, validCommentIds) => {
|
|
169
|
+
let changed = false;
|
|
170
|
+
const content = [];
|
|
171
|
+
for (const item of paragraph.content) {
|
|
172
|
+
if (isCommentMarker(item) && !validCommentIds.has(item.id)) {
|
|
173
|
+
changed = true;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
content.push(item);
|
|
177
|
+
}
|
|
178
|
+
if (!changed) return paragraph;
|
|
179
|
+
return {
|
|
180
|
+
...paragraph,
|
|
181
|
+
content
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
const isCommentMarker = (content) => content.type === "commentRangeStart" || content.type === "commentRangeEnd" || content.type === "commentReference";
|
|
185
|
+
//#endregion
|
|
186
|
+
export { withoutOrphanCommentRanges };
|
package/dist/docx/conformance.js
CHANGED
|
@@ -4,11 +4,65 @@ import { DOCX_CONFORMANCE_CLASSES } from "@stll/docx-core/model";
|
|
|
4
4
|
const STRICT_MAIN_NAMESPACE = "http://purl.oclc.org/ooxml/wordprocessingml/main";
|
|
5
5
|
const TRANSITIONAL_MAIN_NAMESPACE = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
6
6
|
const ROOT_START_TAG_SCAN_LIMIT = 64 * 1024;
|
|
7
|
-
const
|
|
7
|
+
const XML_WHITESPACE = /\s/u;
|
|
8
|
+
/**
|
|
9
|
+
* Find the root start tag within a bounded prefix of `document.xml`,
|
|
10
|
+
* without backtracking.
|
|
11
|
+
*
|
|
12
|
+
* Equivalent in captured-tag semantics to the previous
|
|
13
|
+
* `/^(?:\uFEFF|\s|<\?[\s\S]*?\?>|<!--[\s\S]*?-->)*(?<tag><[^\s/>]+(?:[^>"']|"[^"]*"|'[^']*')*>)/u`
|
|
14
|
+
* pattern, but as a linear hand-rolled scanner: the original alternation of
|
|
15
|
+
* greedy/lazy `[\s\S]*?` spans over an attacker-controlled prefix was
|
|
16
|
+
* susceptible to catastrophic backtracking on crafted input. This walks
|
|
17
|
+
* `scanTarget` once, tracking in-quote state, and returns `undefined` in
|
|
18
|
+
* every case the regex would have failed to match (unterminated PI/comment,
|
|
19
|
+
* no tag found before the scan limit, malformed tag open).
|
|
20
|
+
*/
|
|
21
|
+
function scanRootStartTag(scanTarget) {
|
|
22
|
+
const length = scanTarget.length;
|
|
23
|
+
let index = 0;
|
|
24
|
+
for (;;) {
|
|
25
|
+
if (index >= length) return;
|
|
26
|
+
const character = scanTarget[index];
|
|
27
|
+
if (character === "" || XML_WHITESPACE.test(character ?? "")) {
|
|
28
|
+
index += 1;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (scanTarget.startsWith("<?", index)) {
|
|
32
|
+
const closeIndex = scanTarget.indexOf("?>", index + 2);
|
|
33
|
+
if (closeIndex === -1) return;
|
|
34
|
+
index = closeIndex + 2;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (scanTarget.startsWith("<!--", index)) {
|
|
38
|
+
const closeIndex = scanTarget.indexOf("-->", index + 4);
|
|
39
|
+
if (closeIndex === -1) return;
|
|
40
|
+
index = closeIndex + 3;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
if (scanTarget[index] !== "<") return;
|
|
46
|
+
const nameStart = index + 1;
|
|
47
|
+
const firstNameChar = scanTarget[nameStart];
|
|
48
|
+
if (firstNameChar === void 0 || firstNameChar === "/" || firstNameChar === ">" || XML_WHITESPACE.test(firstNameChar)) return;
|
|
49
|
+
let quote;
|
|
50
|
+
for (let cursor = nameStart; cursor < length; cursor++) {
|
|
51
|
+
const character = scanTarget[cursor];
|
|
52
|
+
if (quote) {
|
|
53
|
+
if (character === quote) quote = void 0;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (character === "\"" || character === "'") {
|
|
57
|
+
quote = character;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (character === ">") return scanTarget.slice(index, cursor + 1);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
8
63
|
const detectDocxConformanceClass = (documentXml) => {
|
|
9
64
|
if (documentXml === null) return DOCX_CONFORMANCE_CLASSES.UNKNOWN;
|
|
10
|
-
const
|
|
11
|
-
const rootStartTag = ROOT_START_TAG_PATTERN.exec(scanTarget)?.groups?.["tag"];
|
|
65
|
+
const rootStartTag = scanRootStartTag(documentXml.length <= ROOT_START_TAG_SCAN_LIMIT ? documentXml : documentXml.slice(0, ROOT_START_TAG_SCAN_LIMIT));
|
|
12
66
|
const root = (rootStartTag === void 0 ? null : parseXmlDocument(rootStartTag.endsWith("/>") ? rootStartTag : `${rootStartTag.slice(0, -1)}/>`)) ?? parseXmlDocument(documentXml);
|
|
13
67
|
return detectDocxConformanceClassFromRoot(root);
|
|
14
68
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DOCX_ENCRYPTION_ERROR_CODES, DocxEncryptionError } from "./errors.js";
|
|
2
2
|
import { bytesEqual, joinBytes, padToBlock, passwordToUtf16Le, toArrayBuffer, writeUint32Le } from "./cryptoBytes.js";
|
|
3
|
+
import { MAX_SPIN_COUNT } from "./encryptionInfo.js";
|
|
3
4
|
//#region src/docx/encryption/agileDecryption.ts
|
|
4
5
|
/**
|
|
5
6
|
* Agile Encryption decryption (Office 2010+) using Web Crypto.
|
|
@@ -115,11 +116,20 @@ const decryptAesCbc = async (rawKey, iv, ciphertext) => {
|
|
|
115
116
|
}, key, toArrayBuffer(paddedCiphertext));
|
|
116
117
|
return new Uint8Array(decrypted).subarray(0, ciphertext.length);
|
|
117
118
|
};
|
|
118
|
-
/**
|
|
119
|
+
/**
|
|
120
|
+
* [MS-OFFCRYPTO] 2.3.4.9 — iterated hash of password + salt.
|
|
121
|
+
*
|
|
122
|
+
* `iterations` is expected to already be validated by
|
|
123
|
+
* `encryptionInfo.ts`'s `spinCount` parsing, but this loop drives real
|
|
124
|
+
* WebCrypto work per iteration, so it is clamped again here as well —
|
|
125
|
+
* this function must stay safe to call with an untrusted iteration count
|
|
126
|
+
* regardless of caller.
|
|
127
|
+
*/
|
|
119
128
|
const hashPassword = async (password, salt, iterations, hashName) => {
|
|
120
129
|
const algorithm = webHashName(hashName);
|
|
130
|
+
const boundedIterations = Math.min(Math.max(iterations, 0), MAX_SPIN_COUNT);
|
|
121
131
|
let state = await digest(algorithm, joinBytes([salt, passwordToUtf16Le(password)]));
|
|
122
|
-
for (let i = 0; i <
|
|
132
|
+
for (let i = 0; i < boundedIterations; i++) state = await digest(algorithm, joinBytes([writeUint32Le(i), state]));
|
|
123
133
|
return state;
|
|
124
134
|
};
|
|
125
135
|
/** [MS-OFFCRYPTO] 2.3.4.9 — finalize a derived key with a block key suffix. */
|
|
@@ -83,9 +83,14 @@ const loadSector = (file, header, sectorId) => {
|
|
|
83
83
|
};
|
|
84
84
|
const loadFatSectorIds = (file, header) => {
|
|
85
85
|
const ids = [...header.headerDifat];
|
|
86
|
+
const maxDifatSectors = Math.ceil(file.length / header.sectorBytes);
|
|
87
|
+
const stepLimit = Math.min(header.difatSectorCount, maxDifatSectors);
|
|
88
|
+
const seen = /* @__PURE__ */ new Set();
|
|
86
89
|
let chain = header.difatStart;
|
|
87
|
-
for (let step = 0; step <
|
|
90
|
+
for (let step = 0; step < stepLimit; step++) {
|
|
88
91
|
if (chain === SECTOR_END || chain === SECTOR_FREE) break;
|
|
92
|
+
if (seen.has(chain)) throw new Error(`DIFAT chain loop at sector ${chain}`);
|
|
93
|
+
seen.add(chain);
|
|
89
94
|
const sector = loadSector(file, header, chain);
|
|
90
95
|
const view = viewOf(sector);
|
|
91
96
|
const slotsPerSector = header.sectorBytes / 4 - 1;
|
|
@@ -27,6 +27,14 @@ type AgileEncryptionDescriptor = {
|
|
|
27
27
|
scheme: "agile";
|
|
28
28
|
material: AgileKeyMaterial;
|
|
29
29
|
};
|
|
30
|
+
/**
|
|
31
|
+
* Cap on `encryptedKey/@spinCount`. MS-OFFCRYPTO's documented default is
|
|
32
|
+
* 100,000; a hostile/corrupt value here would drive `hashPassword`'s
|
|
33
|
+
* per-iteration WebCrypto digest loop (agileDecryption.ts) an
|
|
34
|
+
* attacker-chosen number of times, so reject anything past a generous
|
|
35
|
+
* multiple of that default instead of trusting the file.
|
|
36
|
+
*/
|
|
37
|
+
declare const MAX_SPIN_COUNT = 200000;
|
|
30
38
|
declare const parseAgileEncryptionInfo: (stream: Uint8Array) => AgileEncryptionDescriptor;
|
|
31
39
|
//#endregion
|
|
32
|
-
export { AgileEncryptionDescriptor, AgileKeyMaterial, parseAgileEncryptionInfo };
|
|
40
|
+
export { AgileEncryptionDescriptor, AgileKeyMaterial, MAX_SPIN_COUNT, parseAgileEncryptionInfo };
|
|
@@ -68,6 +68,22 @@ const requireInt = (attrs, name, element) => {
|
|
|
68
68
|
return parsed;
|
|
69
69
|
};
|
|
70
70
|
const requireBase64 = (attrs, name, element) => decodeBase64(requireAttr(attrs, name, element));
|
|
71
|
+
/**
|
|
72
|
+
* Cap on `encryptedKey/@spinCount`. MS-OFFCRYPTO's documented default is
|
|
73
|
+
* 100,000; a hostile/corrupt value here would drive `hashPassword`'s
|
|
74
|
+
* per-iteration WebCrypto digest loop (agileDecryption.ts) an
|
|
75
|
+
* attacker-chosen number of times, so reject anything past a generous
|
|
76
|
+
* multiple of that default instead of trusting the file.
|
|
77
|
+
*/
|
|
78
|
+
const MAX_SPIN_COUNT = 2e5;
|
|
79
|
+
const requireSpinCount = (attrs, element) => {
|
|
80
|
+
const spinCount = requireInt(attrs, "spinCount", element);
|
|
81
|
+
if (spinCount < 0 || spinCount > 2e5) throw new DocxEncryptionError({
|
|
82
|
+
code: DOCX_ENCRYPTION_ERROR_CODES.DECRYPTION_FAILED,
|
|
83
|
+
message: `EncryptionInfo <${element}> attribute "spinCount" out of range: ${spinCount}`
|
|
84
|
+
});
|
|
85
|
+
return spinCount;
|
|
86
|
+
};
|
|
71
87
|
const parseAgileXml = (xmlBytes) => {
|
|
72
88
|
const xml = new TextDecoder("utf-8").decode(xmlBytes);
|
|
73
89
|
const keyDataTag = findElement(xml, "keyData");
|
|
@@ -88,7 +104,7 @@ const parseAgileXml = (xmlBytes) => {
|
|
|
88
104
|
cipherBlockBytes: requireInt(keyData, "blockSize", "keyData"),
|
|
89
105
|
cipherName: requireAttr(keyData, "cipherAlgorithm", "keyData"),
|
|
90
106
|
cipherMode: requireAttr(keyData, "cipherChaining", "keyData"),
|
|
91
|
-
passwordIterations:
|
|
107
|
+
passwordIterations: requireSpinCount(encryptedKey, "encryptedKey"),
|
|
92
108
|
passwordSalt: requireBase64(encryptedKey, "saltValue", "encryptedKey"),
|
|
93
109
|
passwordHash: requireAttr(encryptedKey, "hashAlgorithm", "encryptedKey"),
|
|
94
110
|
passwordKeyBits: requireInt(encryptedKey, "keyBits", "encryptedKey"),
|
|
@@ -125,4 +141,4 @@ const parseAgileEncryptionInfo = (stream) => {
|
|
|
125
141
|
});
|
|
126
142
|
};
|
|
127
143
|
//#endregion
|
|
128
|
-
export { parseAgileEncryptionInfo };
|
|
144
|
+
export { MAX_SPIN_COUNT, parseAgileEncryptionInfo };
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { DOCX_ENCRYPTION_ERROR_CODES, DocxEncryptionError } from "./errors.js";
|
|
2
2
|
import { toArrayBuffer } from "./cryptoBytes.js";
|
|
3
|
+
import { parseAgileEncryptionInfo } from "./encryptionInfo.js";
|
|
3
4
|
import { decryptAgileEncryptedPackage } from "./agileDecryption.js";
|
|
4
5
|
import { readEncryptedPackageStreams } from "./compoundFile.js";
|
|
5
6
|
import { DOCX_CONTAINER_TYPES, detectDocxContainerType } from "./containerFormat.js";
|
|
6
|
-
import { parseAgileEncryptionInfo } from "./encryptionInfo.js";
|
|
7
7
|
//#region src/docx/encryption/openEncryptedDocx.ts
|
|
8
8
|
/**
|
|
9
9
|
* Decrypt password-protected OOXML before ZIP extraction.
|