@stll/folio-core 0.64.1 → 0.65.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/headless.js +3 -3
- package/dist/compare/reproducible-package.d.ts +5 -0
- package/dist/compare/reproducible-package.js +10 -23
- package/dist/docx/archiveInflation.d.ts +8 -103
- package/dist/docx/archiveInflation.js +8 -145
- package/dist/docx/canonicalStoryRepack.js +2 -2
- package/dist/docx/ensureParaIds.js +7 -22
- package/dist/docx/metadataPrivacy.js +38 -48
- package/dist/docx/packageParts.d.ts +4 -4
- package/dist/docx/packageParts.js +17 -24
- package/dist/docx/parser.js +4 -5
- package/dist/docx/removeHeaderFooterParts.d.ts +2 -2
- package/dist/docx/removeHeaderFooterParts.js +19 -36
- package/dist/docx/rezip.d.ts +7 -7
- package/dist/docx/rezip.js +143 -531
- package/dist/docx/selectiveSave.js +37 -41
- package/dist/docx/selectiveSaveFlags.d.ts +1 -1
- package/dist/docx/selectiveSaveTripwire.js +5 -11
- package/dist/docx/server/applyDocxXmlPatchProposal.js +20 -41
- package/dist/docx/server/boundedArchive.d.ts +1 -2
- package/dist/docx/server/boundedArchive.js +77 -98
- package/dist/docx/server/inspectDocxPackage.d.ts +1 -1
- package/dist/docx/server/inspectDocxPackage.js +1 -1
- package/dist/docx/unzip.d.ts +2 -3
- package/dist/docx/unzip.js +126 -154
- package/package.json +3 -4
|
@@ -56,7 +56,7 @@ import { TaggedError, panic } from "better-result";
|
|
|
56
56
|
import { REVIEW_CARRIERS, finalizeParagraphFormatting, numberingLevelEntries, setDocumentBodyComments } from "@stll/docx-core/model";
|
|
57
57
|
import { reconcileDocumentPartRelationships } from "@stll/docx-core/ops";
|
|
58
58
|
import { EditorState } from "prosemirror-state";
|
|
59
|
-
import
|
|
59
|
+
import { ZipArchive } from "@stll/docx-core/zip";
|
|
60
60
|
// src/ai-edits/headless.ts
|
|
61
61
|
/**
|
|
62
62
|
* Headless `.docx` review path: buffer -> apply AI edits -> buffer, with
|
|
@@ -1990,8 +1990,8 @@ var FolioDocxReviewer = class FolioDocxReviewer {
|
|
|
1990
1990
|
if (xml === null) return null;
|
|
1991
1991
|
const extension = media.path.split(".").at(-1)?.toLowerCase();
|
|
1992
1992
|
if (!extension || !/^[a-z0-9]+$/u.test(extension)) return null;
|
|
1993
|
-
const originalZip = await
|
|
1994
|
-
const existingPaths = new Set(
|
|
1993
|
+
const originalZip = await ZipArchive.read(this.originalBuffer);
|
|
1994
|
+
const existingPaths = new Set(originalZip.paths().map((path) => path.toLowerCase()));
|
|
1995
1995
|
let suffix = 1;
|
|
1996
1996
|
let path = `word/media/folio-import-${suffix}.${extension}`;
|
|
1997
1997
|
while (existingPaths.has(path.toLowerCase()) || this.baseDocument.package.media?.has(path) || this.importedMedia.has(path)) path = `word/media/folio-import-${++suffix}.${extension}`;
|
|
@@ -1 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Date comparison output from the comparison's timestamp. Imported entries
|
|
3
|
+
* retain source dates, and saves can stamp `dcterms:modified` from the wall
|
|
4
|
+
* clock; restamping both makes repeated comparisons reproducible.
|
|
5
|
+
*/
|
|
1
6
|
export declare const withFixedPackageDates: (buffer: ArrayBuffer, date: Date) => Promise<ArrayBuffer>;
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import { decodeText
|
|
2
|
-
import
|
|
3
|
-
import { writeZipPart } from "@stll/docx-core/zip";
|
|
1
|
+
import { decodeText } from "../utils/textEncoding.js";
|
|
2
|
+
import { ZipArchive } from "@stll/docx-core/zip";
|
|
4
3
|
// src/compare/reproducible-package.ts
|
|
5
4
|
/**
|
|
6
5
|
* Date comparison output from the comparison's timestamp. Imported entries
|
|
7
6
|
* retain source dates, and saves can stamp `dcterms:modified` from the wall
|
|
8
7
|
* clock; restamping both makes repeated comparisons reproducible.
|
|
9
8
|
*/
|
|
10
|
-
/**
|
|
9
|
+
/** ZipArchive deflate level `repackDocx` writes DOCX parts at. */
|
|
11
10
|
const DOCX_COMPRESSION_LEVEL = 6;
|
|
12
11
|
const CORE_PROPERTIES_PATH = "docProps/core.xml";
|
|
13
12
|
const MODIFIED_ELEMENT = /<dcterms:modified[^<>]*>[^<]*<\/dcterms:modified>/u;
|
|
@@ -18,24 +17,12 @@ const MODIFIED_ELEMENT = /<dcterms:modified[^<>]*>[^<]*<\/dcterms:modified>/u;
|
|
|
18
17
|
*/
|
|
19
18
|
const withFixedModifiedDate = (corePropsXml, date) => corePropsXml.replace(MODIFIED_ELEMENT, `<dcterms:modified xsi:type="dcterms:W3CDTF">${date.toISOString()}</dcterms:modified>`);
|
|
20
19
|
const withFixedPackageDates = async (buffer, date) => {
|
|
21
|
-
const zip = await
|
|
22
|
-
const coreProps = zip.
|
|
23
|
-
if (coreProps)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
compression: "DEFLATE",
|
|
29
|
-
compressionOptions: { level: DOCX_COMPRESSION_LEVEL }
|
|
30
|
-
}
|
|
31
|
-
});
|
|
32
|
-
zip.forEach((_path, file) => {
|
|
33
|
-
file.date = date;
|
|
34
|
-
});
|
|
35
|
-
return await zip.generateAsync({
|
|
36
|
-
type: "arraybuffer",
|
|
37
|
-
compression: "DEFLATE",
|
|
38
|
-
compressionOptions: { level: DOCX_COMPRESSION_LEVEL }
|
|
39
|
-
});
|
|
20
|
+
const zip = await ZipArchive.read(buffer);
|
|
21
|
+
const coreProps = zip.entry(CORE_PROPERTIES_PATH);
|
|
22
|
+
if (coreProps) zip.set(CORE_PROPERTIES_PATH, withFixedModifiedDate(decodeText(await coreProps.bytes(), { ignoreBOM: true }), date));
|
|
23
|
+
return (await zip.generate({
|
|
24
|
+
level: DOCX_COMPRESSION_LEVEL,
|
|
25
|
+
modified: date
|
|
26
|
+
})).buffer;
|
|
40
27
|
};
|
|
41
28
|
export { withFixedPackageDates };
|
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* platform-neutral.
|
|
9
|
-
*/
|
|
10
|
-
internalStream(type: "uint8array"): JSZip.JSZipStreamHelper<Uint8Array>;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Compression bounds for DOCX packages.
|
|
3
|
+
*
|
|
4
|
+
* The archive reader (`@stll/docx-core/zip`) checks every size an archive
|
|
5
|
+
* declares before anything inflates, and stops each inflation at its entry's
|
|
6
|
+
* declared size. The values here are the DOCX policy it is given.
|
|
7
|
+
*/
|
|
13
8
|
/**
|
|
14
9
|
* Default ceiling on inflated bytes per compressed byte.
|
|
15
10
|
*
|
|
@@ -26,100 +21,10 @@ export declare const DOCX_MAX_COMPRESSION_RATIO = 200;
|
|
|
26
21
|
* matter. The byte caps still apply below this size.
|
|
27
22
|
*/
|
|
28
23
|
export declare const DOCX_COMPRESSION_RATIO_GRACE_BYTES: number;
|
|
29
|
-
/**
|
|
30
|
-
* Upper bound on the central-directory records in `bytes`, counted without
|
|
31
|
-
* parsing the archive and stopping once the count passes `stopAfter`.
|
|
32
|
-
*
|
|
33
|
-
* Every record JSZip reads starts with the record signature, so the number of
|
|
34
|
-
* signature occurrences cannot be lower than the number of entries JSZip would
|
|
35
|
-
* build, whatever the end-of-central-directory record claims.
|
|
36
|
-
*/
|
|
37
|
-
export declare const countCentralDirectoryRecords: (bytes: Uint8Array, stopAfter: number) => number;
|
|
38
|
-
/** Sizes a loaded entry declares in the central directory. */
|
|
39
|
-
export type ZipEntrySizes = {
|
|
40
|
-
readonly compressedBytes: number | null;
|
|
41
|
-
readonly uncompressedBytes: number | null;
|
|
42
|
-
};
|
|
43
|
-
/** Declared sizes of an entry JSZip loaded from an archive. */
|
|
44
|
-
export declare const getZipEntrySizes: (entry: JSZip.JSZipObject) => ZipEntrySizes;
|
|
45
|
-
/**
|
|
46
|
-
* Whether a loaded entry is stored rather than compressed. A stored entry
|
|
47
|
-
* reads back exactly the bytes the archive carries, so it cannot expand.
|
|
48
|
-
*/
|
|
49
|
-
export declare const isStoredZipEntry: (entry: JSZip.JSZipObject) => boolean;
|
|
50
24
|
/**
|
|
51
25
|
* Whether the per-entry ratio cap applies to a part. Markup and text parts
|
|
52
26
|
* never compress anywhere near the cap. Binary parts can: an uncompressed
|
|
53
27
|
* bitmap of one colour compresses like a run of one byte. Those stay bounded
|
|
54
28
|
* by the byte caps and the package-wide ratio instead.
|
|
55
29
|
*/
|
|
56
|
-
export declare const isRatioBoundedPart: (path: string) => boolean;
|
|
57
|
-
/** The per-entry ratio cap for `path`: `maxRatio`, or none for a binary part. */
|
|
58
|
-
export declare const compressionRatioLimitFor: (path: string, maxRatio: number) => number;
|
|
59
|
-
type CompressionRatioCheck = {
|
|
60
|
-
inflatedBytes: number;
|
|
61
|
-
compressedBytes: number | null;
|
|
62
|
-
maxRatio: number;
|
|
63
|
-
};
|
|
64
|
-
/**
|
|
65
|
-
* Whether `inflatedBytes` produced from `compressedBytes` passes the ratio cap.
|
|
66
|
-
* Sizes under {@link DOCX_COMPRESSION_RATIO_GRACE_BYTES} never do, and an
|
|
67
|
-
* unknown compressed size is left to the byte caps.
|
|
68
|
-
*/
|
|
69
|
-
export declare const exceedsCompressionRatio: ({ inflatedBytes, compressedBytes, maxRatio }: CompressionRatioCheck) => boolean;
|
|
70
|
-
/**
|
|
71
|
-
* Inflated bytes charged across one package. `aborted` stops every inflation
|
|
72
|
-
* sharing the budget at its next chunk, once one of them has failed the
|
|
73
|
-
* package.
|
|
74
|
-
*/
|
|
75
|
-
export type InflationBudget = {
|
|
76
|
-
readonly maxTotalBytes: number;
|
|
77
|
-
inflatedBytes: number;
|
|
78
|
-
aborted: boolean;
|
|
79
|
-
};
|
|
80
|
-
export declare const createInflationBudget: (maxTotalBytes: number) => InflationBudget;
|
|
81
|
-
/**
|
|
82
|
-
* The bound an inflation stopped at.
|
|
83
|
-
*
|
|
84
|
-
* - `declared-size` — the entry produced more than its declared size.
|
|
85
|
-
* - `compression-ratio` — the entry passed the ratio cap for its compressed size.
|
|
86
|
-
* - `entry` — the entry passed the per-entry cap the caller set.
|
|
87
|
-
* - `total` — the package passed its cumulative budget.
|
|
88
|
-
* - `aborted` — another inflation sharing the budget already failed.
|
|
89
|
-
*/
|
|
90
|
-
export type InflationLimit = "declared-size" | "compression-ratio" | "entry" | "total" | "aborted";
|
|
91
|
-
export type InflateEntryResult = {
|
|
92
|
-
readonly ok: true;
|
|
93
|
-
readonly bytes: Uint8Array<ArrayBuffer>;
|
|
94
|
-
} | {
|
|
95
|
-
readonly ok: false;
|
|
96
|
-
readonly limit: InflationLimit;
|
|
97
|
-
};
|
|
98
|
-
export type InflateEntryOptions = {
|
|
99
|
-
entry: JSZip.JSZipObject;
|
|
100
|
-
/** Most bytes this entry may inflate to. */
|
|
101
|
-
maxEntryBytes: number;
|
|
102
|
-
/** Ratio cap while streaming; see {@link compressionRatioLimitFor}. */
|
|
103
|
-
maxCompressionRatio: number;
|
|
104
|
-
budget: InflationBudget;
|
|
105
|
-
/**
|
|
106
|
-
* `false` inflates the entry only to prove it stays within its bounds; the
|
|
107
|
-
* chunks are dropped and the result carries no bytes. A package part that
|
|
108
|
-
* is never read here is still inflated by a later save, so the bound has
|
|
109
|
-
* to be established for it too.
|
|
110
|
-
*/
|
|
111
|
-
retain?: boolean;
|
|
112
|
-
};
|
|
113
|
-
/**
|
|
114
|
-
* Inflate one entry chunk by chunk, checking every bound before a chunk is
|
|
115
|
-
* kept. Pausing the stream abandons the rest of the entry, so a limit bounds
|
|
116
|
-
* memory rather than reporting an overrun afterwards.
|
|
117
|
-
*
|
|
118
|
-
* The budget is charged as chunks arrive, which keeps concurrent inflations
|
|
119
|
-
* of one package honest, and refunded when this entry stops at a limit or
|
|
120
|
-
* fails, since none of its bytes are kept. Stream errors (a corrupt entry)
|
|
121
|
-
* reject; limits resolve with `ok: false` for the caller to map onto its own
|
|
122
|
-
* error or skip policy.
|
|
123
|
-
*/
|
|
124
|
-
export declare const inflateEntryWithinLimits: ({ entry, maxEntryBytes, maxCompressionRatio, budget, retain }: InflateEntryOptions) => Promise<InflateEntryResult>;
|
|
125
|
-
export {};
|
|
30
|
+
export declare const isRatioBoundedPart: (path: string) => boolean;
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
// src/docx/archiveInflation.ts
|
|
2
2
|
/**
|
|
3
|
+
* Compression bounds for DOCX packages.
|
|
4
|
+
*
|
|
5
|
+
* The archive reader (`@stll/docx-core/zip`) checks every size an archive
|
|
6
|
+
* declares before anything inflates, and stops each inflation at its entry's
|
|
7
|
+
* declared size. The values here are the DOCX policy it is given.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
3
10
|
* Default ceiling on inflated bytes per compressed byte.
|
|
4
11
|
*
|
|
5
12
|
* Document parts in the repository's fixtures stay under 75:1, and parts over a
|
|
@@ -15,57 +22,6 @@ const DOCX_MAX_COMPRESSION_RATIO = 200;
|
|
|
15
22
|
* matter. The byte caps still apply below this size.
|
|
16
23
|
*/
|
|
17
24
|
const DOCX_COMPRESSION_RATIO_GRACE_BYTES = 4194304;
|
|
18
|
-
const CENTRAL_DIRECTORY_RECORD_SIGNATURE = [
|
|
19
|
-
80,
|
|
20
|
-
75,
|
|
21
|
-
1,
|
|
22
|
-
2
|
|
23
|
-
];
|
|
24
|
-
/**
|
|
25
|
-
* Upper bound on the central-directory records in `bytes`, counted without
|
|
26
|
-
* parsing the archive and stopping once the count passes `stopAfter`.
|
|
27
|
-
*
|
|
28
|
-
* Every record JSZip reads starts with the record signature, so the number of
|
|
29
|
-
* signature occurrences cannot be lower than the number of entries JSZip would
|
|
30
|
-
* build, whatever the end-of-central-directory record claims.
|
|
31
|
-
*/
|
|
32
|
-
const countCentralDirectoryRecords = (bytes, stopAfter) => {
|
|
33
|
-
const [first, second, third, fourth] = CENTRAL_DIRECTORY_RECORD_SIGNATURE;
|
|
34
|
-
let count = 0;
|
|
35
|
-
let offset = bytes.indexOf(first);
|
|
36
|
-
while (offset !== -1 && offset + 3 < bytes.length) {
|
|
37
|
-
if (bytes[offset + 1] === second && bytes[offset + 2] === third && bytes[offset + 3] === fourth) {
|
|
38
|
-
count += 1;
|
|
39
|
-
if (count > stopAfter) return count;
|
|
40
|
-
}
|
|
41
|
-
offset = bytes.indexOf(first, offset + 1);
|
|
42
|
-
}
|
|
43
|
-
return count;
|
|
44
|
-
};
|
|
45
|
-
const finiteSize = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
46
|
-
/** Declared sizes of an entry JSZip loaded from an archive. */
|
|
47
|
-
const getZipEntrySizes = (entry) => {
|
|
48
|
-
const data = "_data" in entry ? entry._data : void 0;
|
|
49
|
-
if (typeof data !== "object" || data === null) return {
|
|
50
|
-
compressedBytes: null,
|
|
51
|
-
uncompressedBytes: null
|
|
52
|
-
};
|
|
53
|
-
return {
|
|
54
|
-
compressedBytes: "compressedSize" in data ? finiteSize(data.compressedSize) : null,
|
|
55
|
-
uncompressedBytes: "uncompressedSize" in data ? finiteSize(data.uncompressedSize) : null
|
|
56
|
-
};
|
|
57
|
-
};
|
|
58
|
-
const STORED_COMPRESSION_MAGIC = "\0\0";
|
|
59
|
-
/**
|
|
60
|
-
* Whether a loaded entry is stored rather than compressed. A stored entry
|
|
61
|
-
* reads back exactly the bytes the archive carries, so it cannot expand.
|
|
62
|
-
*/
|
|
63
|
-
const isStoredZipEntry = (entry) => {
|
|
64
|
-
const data = "_data" in entry ? entry._data : void 0;
|
|
65
|
-
if (typeof data !== "object" || data === null || !("compression" in data)) return false;
|
|
66
|
-
const { compression } = data;
|
|
67
|
-
return typeof compression === "object" && compression !== null && "magic" in compression && compression.magic === STORED_COMPRESSION_MAGIC;
|
|
68
|
-
};
|
|
69
25
|
const RATIO_BOUNDED_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
70
26
|
"xml",
|
|
71
27
|
"rels",
|
|
@@ -84,97 +40,4 @@ const RATIO_BOUNDED_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
84
40
|
* by the byte caps and the package-wide ratio instead.
|
|
85
41
|
*/
|
|
86
42
|
const isRatioBoundedPart = (path) => RATIO_BOUNDED_EXTENSIONS.has(path.slice(path.lastIndexOf(".") + 1).toLowerCase());
|
|
87
|
-
|
|
88
|
-
const compressionRatioLimitFor = (path, maxRatio) => isRatioBoundedPart(path) ? maxRatio : Number.POSITIVE_INFINITY;
|
|
89
|
-
/**
|
|
90
|
-
* Whether `inflatedBytes` produced from `compressedBytes` passes the ratio cap.
|
|
91
|
-
* Sizes under {@link DOCX_COMPRESSION_RATIO_GRACE_BYTES} never do, and an
|
|
92
|
-
* unknown compressed size is left to the byte caps.
|
|
93
|
-
*/
|
|
94
|
-
const exceedsCompressionRatio = ({ inflatedBytes, compressedBytes, maxRatio }) => compressedBytes !== null && inflatedBytes > 4194304 && inflatedBytes > compressedBytes * maxRatio;
|
|
95
|
-
const createInflationBudget = (maxTotalBytes) => ({
|
|
96
|
-
maxTotalBytes,
|
|
97
|
-
inflatedBytes: 0,
|
|
98
|
-
aborted: false
|
|
99
|
-
});
|
|
100
|
-
const EMPTY_BYTES = /* @__PURE__ */ new Uint8Array(0);
|
|
101
|
-
const concatChunks = (chunks, totalBytes) => {
|
|
102
|
-
const merged = new Uint8Array(totalBytes);
|
|
103
|
-
let offset = 0;
|
|
104
|
-
for (const chunk of chunks) {
|
|
105
|
-
merged.set(chunk, offset);
|
|
106
|
-
offset += chunk.length;
|
|
107
|
-
}
|
|
108
|
-
return merged;
|
|
109
|
-
};
|
|
110
|
-
/**
|
|
111
|
-
* Inflate one entry chunk by chunk, checking every bound before a chunk is
|
|
112
|
-
* kept. Pausing the stream abandons the rest of the entry, so a limit bounds
|
|
113
|
-
* memory rather than reporting an overrun afterwards.
|
|
114
|
-
*
|
|
115
|
-
* The budget is charged as chunks arrive, which keeps concurrent inflations
|
|
116
|
-
* of one package honest, and refunded when this entry stops at a limit or
|
|
117
|
-
* fails, since none of its bytes are kept. Stream errors (a corrupt entry)
|
|
118
|
-
* reject; limits resolve with `ok: false` for the caller to map onto its own
|
|
119
|
-
* error or skip policy.
|
|
120
|
-
*/
|
|
121
|
-
const inflateEntryWithinLimits = async ({ entry, maxEntryBytes, maxCompressionRatio, budget, retain = true }) => {
|
|
122
|
-
const { compressedBytes, uncompressedBytes } = getZipEntrySizes(entry);
|
|
123
|
-
return await new Promise((resolve, reject) => {
|
|
124
|
-
const stream = entry.internalStream("uint8array");
|
|
125
|
-
const chunks = [];
|
|
126
|
-
let entryBytes = 0;
|
|
127
|
-
let settled = false;
|
|
128
|
-
const settle = () => {
|
|
129
|
-
if (settled) return false;
|
|
130
|
-
settled = true;
|
|
131
|
-
return true;
|
|
132
|
-
};
|
|
133
|
-
const refund = () => {
|
|
134
|
-
budget.inflatedBytes -= entryBytes;
|
|
135
|
-
};
|
|
136
|
-
const stop = (limit) => {
|
|
137
|
-
if (!settle()) return;
|
|
138
|
-
stream.pause();
|
|
139
|
-
refund();
|
|
140
|
-
resolve({
|
|
141
|
-
ok: false,
|
|
142
|
-
limit
|
|
143
|
-
});
|
|
144
|
-
};
|
|
145
|
-
const limitFor = () => {
|
|
146
|
-
if (budget.aborted) return "aborted";
|
|
147
|
-
if (uncompressedBytes !== null && entryBytes > uncompressedBytes) return "declared-size";
|
|
148
|
-
if (exceedsCompressionRatio({
|
|
149
|
-
inflatedBytes: entryBytes,
|
|
150
|
-
compressedBytes,
|
|
151
|
-
maxRatio: maxCompressionRatio
|
|
152
|
-
})) return "compression-ratio";
|
|
153
|
-
if (entryBytes > maxEntryBytes) return "entry";
|
|
154
|
-
if (budget.inflatedBytes > budget.maxTotalBytes) return "total";
|
|
155
|
-
return null;
|
|
156
|
-
};
|
|
157
|
-
stream.on("data", (chunk) => {
|
|
158
|
-
if (settled) return;
|
|
159
|
-
entryBytes += chunk.length;
|
|
160
|
-
budget.inflatedBytes += chunk.length;
|
|
161
|
-
const limit = limitFor();
|
|
162
|
-
if (limit !== null) {
|
|
163
|
-
stop(limit);
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
if (retain) chunks.push(chunk);
|
|
167
|
-
}).on("end", () => {
|
|
168
|
-
if (!settle()) return;
|
|
169
|
-
resolve({
|
|
170
|
-
ok: true,
|
|
171
|
-
bytes: retain ? concatChunks(chunks, entryBytes) : EMPTY_BYTES
|
|
172
|
-
});
|
|
173
|
-
}).on("error", (error) => {
|
|
174
|
-
if (!settle()) return;
|
|
175
|
-
refund();
|
|
176
|
-
reject(error);
|
|
177
|
-
}).resume();
|
|
178
|
-
});
|
|
179
|
-
};
|
|
180
|
-
export { DOCX_COMPRESSION_RATIO_GRACE_BYTES, DOCX_MAX_COMPRESSION_RATIO, compressionRatioLimitFor, countCentralDirectoryRecords, createInflationBudget, exceedsCompressionRatio, getZipEntrySizes, inflateEntryWithinLimits, isRatioBoundedPart, isStoredZipEntry };
|
|
43
|
+
export { DOCX_COMPRESSION_RATIO_GRACE_BYTES, DOCX_MAX_COMPRESSION_RATIO, isRatioBoundedPart };
|
|
@@ -5,7 +5,7 @@ import { readDocumentSectionFacts } from "./documentSectionFacts.js";
|
|
|
5
5
|
import { parseDocx } from "./parser.js";
|
|
6
6
|
import { assertSectionCarriersMatchModel } from "./rezip.js";
|
|
7
7
|
import { serializeDocument } from "./serializer/documentSerializer.js";
|
|
8
|
-
import
|
|
8
|
+
import { ZipArchive } from "@stll/docx-core/zip";
|
|
9
9
|
// src/docx/canonicalStoryRepack.ts
|
|
10
10
|
/** Canonical snapshots own explicit section references; the PM projection does not. */
|
|
11
11
|
const referenceKey = ({ element, type, rId }) => `${element}:${type}:${rId}`;
|
|
@@ -19,7 +19,7 @@ const xmlFingerprint = async (xml) => {
|
|
|
19
19
|
const repackWithCanonicalStoryRemovals = async ({ document, repack }) => {
|
|
20
20
|
const originalBuffer = document.originalBuffer;
|
|
21
21
|
if (!originalBuffer) return repack();
|
|
22
|
-
const xml = await (await
|
|
22
|
+
const xml = await (await ZipArchive.read(originalBuffer)).entry("word/document.xml")?.bytes().then((bytes) => decodeText(bytes, { ignoreBOM: true }));
|
|
23
23
|
if (!xml) return repack();
|
|
24
24
|
const before = readDocumentSectionFacts(xml);
|
|
25
25
|
const serializedXml = serializeDocument(document);
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { deterministicHexId } from "../utils/hexId.js";
|
|
2
|
-
import { encodeUtf8 } from "../utils/textEncoding.js";
|
|
3
2
|
import { resolvePackageRelationshipTarget } from "./packageParts.js";
|
|
4
3
|
import { spliceXml } from "./selectiveXmlPatch.js";
|
|
5
4
|
import { loadDocxArchive } from "./server/boundedArchive.js";
|
|
@@ -7,8 +6,7 @@ import { scanStreamingXmlElements } from "./streamingXmlParser.js";
|
|
|
7
6
|
import { resolveNamespaceUri } from "./xmlNamespaceContext.js";
|
|
8
7
|
import { OFFICE_RELATIONSHIP_NAMESPACE_URIS, WORDPROCESSINGML_NAMESPACE_URIS, getAttribute, getChildElements, getLocalName, getNamespaceUri, parseXmlDocument, resolveAttributeNamespaceUri } from "./xmlParser.js";
|
|
9
8
|
import { TaggedError } from "better-result";
|
|
10
|
-
import
|
|
11
|
-
import { writeZipPart } from "@stll/docx-core/zip";
|
|
9
|
+
import { ZipArchive } from "@stll/docx-core/zip";
|
|
12
10
|
// src/docx/ensureParaIds.ts
|
|
13
11
|
/**
|
|
14
12
|
* Headless `w14:paraId` normalization for a `.docx` buffer.
|
|
@@ -321,7 +319,7 @@ const mainDocumentPart = (rels, entries) => {
|
|
|
321
319
|
/** OPC part names are case-insensitive; compare lowercased. */
|
|
322
320
|
const isTargetPart = (path) => TARGET_PART_PATTERNS.some((pattern) => pattern.test(path.toLowerCase()));
|
|
323
321
|
const toUint8Array = (docx) => docx instanceof Uint8Array ? docx : new Uint8Array(docx);
|
|
324
|
-
const hasDigitalSignatureParts = (zip) =>
|
|
322
|
+
const hasDigitalSignatureParts = (zip) => zip.entries().some(({ path }) => path.toLowerCase().startsWith(DIGITAL_SIGNATURE_PART_PREFIX));
|
|
325
323
|
/**
|
|
326
324
|
* Backfill `w14:paraId` on every paragraph of a `.docx` buffer. See the
|
|
327
325
|
* module doc for the exact contract. Throws {@link EnsureParaIdsError} when
|
|
@@ -381,31 +379,18 @@ const ensureParaIdsInternal = async (docx, options) => {
|
|
|
381
379
|
alreadyComplete: true,
|
|
382
380
|
mintedParaIds: []
|
|
383
381
|
};
|
|
384
|
-
const zip = await
|
|
382
|
+
const zip = await ZipArchive.read(docx);
|
|
385
383
|
if (hasDigitalSignatureParts(zip) && options.allowSignedPackageMutation !== true) throw new EnsureParaIdsError({
|
|
386
384
|
reason: ENSURE_PARA_IDS_REASONS.SIGNED_PACKAGE,
|
|
387
385
|
message: "Refusing to normalize a digitally signed package because rewriting OOXML invalidates its signatures. Warn the user and pass allowSignedPackageMutation only if invalidation is acceptable."
|
|
388
386
|
});
|
|
389
387
|
for (const [partPath, content] of updates) {
|
|
390
|
-
const sourceEntry = zip.
|
|
391
|
-
if (sourceEntry ===
|
|
392
|
-
|
|
393
|
-
zip,
|
|
394
|
-
path: partPath,
|
|
395
|
-
data: encodeUtf8(content),
|
|
396
|
-
options: {
|
|
397
|
-
compression: "DEFLATE",
|
|
398
|
-
compressionOptions: { level: 6 },
|
|
399
|
-
date: sourceEntry.date
|
|
400
|
-
}
|
|
401
|
-
});
|
|
388
|
+
const sourceEntry = zip.entry(partPath);
|
|
389
|
+
if (sourceEntry === void 0) throw createEnsureParaIdsError(`Package part disappeared during normalization: ${partPath}`);
|
|
390
|
+
zip.set(partPath, content, { modified: sourceEntry.modified });
|
|
402
391
|
}
|
|
403
392
|
return {
|
|
404
|
-
docx: await zip.
|
|
405
|
-
type: "uint8array",
|
|
406
|
-
compression: "DEFLATE",
|
|
407
|
-
compressionOptions: { level: 6 }
|
|
408
|
-
}),
|
|
393
|
+
docx: await zip.generate({ level: 6 }),
|
|
409
394
|
assigned,
|
|
410
395
|
deduplicated,
|
|
411
396
|
alreadyComplete: false,
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { decodeText
|
|
2
|
-
import
|
|
1
|
+
import { decodeText } from "../utils/textEncoding.js";
|
|
2
|
+
import "./archiveInflation.js";
|
|
3
3
|
import { elementToXml, getLocalName, parseXmlDocument } from "./xmlParser.js";
|
|
4
|
-
import { TaggedError } from "better-result";
|
|
5
|
-
import
|
|
6
|
-
import { writeZipPart } from "@stll/docx-core/zip";
|
|
4
|
+
import { Result, TaggedError } from "better-result";
|
|
5
|
+
import { ZipArchive, ZipError } from "@stll/docx-core/zip";
|
|
7
6
|
// src/docx/metadataPrivacy.ts
|
|
8
7
|
const FOLIO_DOCUMENT_METADATA_PROPERTIES = Object.freeze([
|
|
9
8
|
"title",
|
|
@@ -57,40 +56,34 @@ const CORE_PROPERTIES_PATH_LOWER = "docprops/core.xml";
|
|
|
57
56
|
* found — otherwise the privacy rewrite silently no-ops and the unscrubbed
|
|
58
57
|
* buffer round-trips untouched.
|
|
59
58
|
*/
|
|
60
|
-
const findZipEntryCaseInsensitive = (zip, lowerPath) => {
|
|
61
|
-
const direct = zip.file(lowerPath);
|
|
62
|
-
if (direct) return direct;
|
|
63
|
-
for (const [path, file] of Object.entries(zip.files)) if (!file.dir && path.toLowerCase() === lowerPath) return file;
|
|
64
|
-
return null;
|
|
65
|
-
};
|
|
59
|
+
const findZipEntryCaseInsensitive = (zip, lowerPath) => zip.entry(lowerPath) ?? zip.entries().find(({ path }) => path.toLowerCase() === lowerPath) ?? null;
|
|
66
60
|
const loadPrivacyArchive = async (buffer) => {
|
|
67
61
|
if (buffer.byteLength > MAX_INPUT_BYTES) throw new FolioDocumentPrivacyArchiveError({
|
|
68
62
|
message: "Document privacy input exceeded the compressed-size limit",
|
|
69
63
|
reason: "input-too-large"
|
|
70
64
|
});
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
65
|
+
const read = await Result.tryPromise({
|
|
66
|
+
try: async () => await ZipArchive.read(buffer, { limits: {
|
|
67
|
+
maxEntries: MAX_ARCHIVE_ENTRIES,
|
|
68
|
+
maxCompressionRatio: 200
|
|
69
|
+
} }),
|
|
70
|
+
catch: (cause) => cause
|
|
74
71
|
});
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
72
|
+
if (Result.isError(read)) {
|
|
73
|
+
const cause = read.error;
|
|
74
|
+
if (cause instanceof ZipError && cause.reason === "too-many-entries") throw new FolioDocumentPrivacyArchiveError({
|
|
75
|
+
message: "Document privacy input exceeded the package-entry limit",
|
|
76
|
+
reason: "too-many-entries"
|
|
77
|
+
});
|
|
79
78
|
throw new FolioDocumentPrivacyArchiveError({
|
|
80
79
|
message: "Document privacy input is not a readable package",
|
|
81
80
|
reason: "load-failed",
|
|
82
81
|
cause
|
|
83
82
|
});
|
|
84
83
|
}
|
|
85
|
-
|
|
86
|
-
message: "Document privacy input exceeded the package-entry limit",
|
|
87
|
-
reason: "too-many-entries"
|
|
88
|
-
});
|
|
84
|
+
const zip = read.value;
|
|
89
85
|
const coreProperties = findZipEntryCaseInsensitive(zip, CORE_PROPERTIES_PATH_LOWER);
|
|
90
|
-
if (
|
|
91
|
-
const data = "_data" in coreProperties ? coreProperties._data : void 0;
|
|
92
|
-
const declaredBytes = typeof data === "object" && data !== null && "uncompressedSize" in data ? data.uncompressedSize : void 0;
|
|
93
|
-
if (typeof declaredBytes !== "number" || declaredBytes > MAX_CORE_PROPERTIES_BYTES) throw new FolioDocumentPrivacyArchiveError({
|
|
86
|
+
if (coreProperties && coreProperties.size > MAX_CORE_PROPERTIES_BYTES) throw new FolioDocumentPrivacyArchiveError({
|
|
94
87
|
message: "Document privacy core properties exceeded the part-size limit",
|
|
95
88
|
reason: "core-properties-too-large"
|
|
96
89
|
});
|
|
@@ -119,22 +112,26 @@ const rewriteCorePropertiesPrivacy = (xml, transforms) => {
|
|
|
119
112
|
};
|
|
120
113
|
};
|
|
121
114
|
/**
|
|
122
|
-
* Read the core-properties part
|
|
123
|
-
*
|
|
124
|
-
* the part is no larger than it claims.
|
|
115
|
+
* Read the core-properties part. Its declared size was checked when the
|
|
116
|
+
* archive loaded, and inflation stops at that size.
|
|
125
117
|
*/
|
|
126
118
|
const readCoreProperties = async (entry) => {
|
|
127
|
-
const
|
|
128
|
-
entry,
|
|
129
|
-
|
|
130
|
-
maxCompressionRatio: 200,
|
|
131
|
-
budget: createInflationBudget(MAX_CORE_PROPERTIES_BYTES)
|
|
132
|
-
});
|
|
133
|
-
if (!result.ok) throw new FolioDocumentPrivacyArchiveError({
|
|
134
|
-
message: "Document privacy core properties exceeded the part-size limit",
|
|
135
|
-
reason: "core-properties-too-large"
|
|
119
|
+
const read = await Result.tryPromise({
|
|
120
|
+
try: async () => await entry.bytes(),
|
|
121
|
+
catch: (cause) => cause
|
|
136
122
|
});
|
|
137
|
-
|
|
123
|
+
if (Result.isError(read)) {
|
|
124
|
+
const cause = read.error;
|
|
125
|
+
throw new FolioDocumentPrivacyArchiveError(cause instanceof ZipError && cause.reason === "declared-size-exceeded" ? {
|
|
126
|
+
message: "Document privacy core properties exceeded the part-size limit",
|
|
127
|
+
reason: "core-properties-too-large"
|
|
128
|
+
} : {
|
|
129
|
+
message: "Document privacy input is not a readable package",
|
|
130
|
+
reason: "load-failed",
|
|
131
|
+
cause
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return decodeText(read.value, { ignoreBOM: true });
|
|
138
135
|
};
|
|
139
136
|
/** Rewrite selected package metadata fields without changing other package parts. */
|
|
140
137
|
const rewriteDocxMetadataPrivacy = async (buffer, { transforms }) => {
|
|
@@ -156,16 +153,9 @@ const rewriteDocxMetadataPrivacy = async (buffer, { transforms }) => {
|
|
|
156
153
|
removedMetadataProperties: []
|
|
157
154
|
}
|
|
158
155
|
};
|
|
159
|
-
|
|
160
|
-
zip,
|
|
161
|
-
path: coreProperties.name,
|
|
162
|
-
data: encodeUtf8(rewritten.xml)
|
|
163
|
-
});
|
|
156
|
+
zip.set(coreProperties.path, rewritten.xml);
|
|
164
157
|
return {
|
|
165
|
-
buffer: await zip.
|
|
166
|
-
type: "arraybuffer",
|
|
167
|
-
compression: "DEFLATE"
|
|
168
|
-
}),
|
|
158
|
+
buffer: (await zip.generate()).buffer,
|
|
169
159
|
privacyReport: {
|
|
170
160
|
appliedTransforms,
|
|
171
161
|
removedMetadataProperties: rewritten.removedMetadataProperties
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { ZipArchive } from "@stll/docx-core/zip";
|
|
2
2
|
/** Resolve an internal URI reference once, before comparing it with ZIP entries. */
|
|
3
3
|
export declare const resolvePackageRelationshipTarget: (target: string, relsPath: string) => string | undefined;
|
|
4
4
|
/**
|
|
@@ -13,7 +13,7 @@ export declare const resolvePackageRelationshipTarget: (target: string, relsPath
|
|
|
13
13
|
*/
|
|
14
14
|
export declare const isUnsafePackagePath: (path: string) => boolean;
|
|
15
15
|
/** Drop the entries whose path a save refuses. Every other part survives. */
|
|
16
|
-
export declare const removeUnsafeEntries: (zip:
|
|
16
|
+
export declare const removeUnsafeEntries: (zip: ZipArchive) => void;
|
|
17
17
|
/** What reconciliation removed to make the package internally consistent. */
|
|
18
18
|
export type PackageReferenceRepair = {
|
|
19
19
|
/** Parts a relationship named that the package does not hold. */
|
|
@@ -41,7 +41,7 @@ export type PackageReferenceRepair = {
|
|
|
41
41
|
* already-reconciled package therefore reports nothing and changes nothing,
|
|
42
42
|
* which is what the repack tests assert about their own output.
|
|
43
43
|
*/
|
|
44
|
-
export declare const reconcilePackageReferences: (zip:
|
|
44
|
+
export declare const reconcilePackageReferences: (zip: ZipArchive, compressionLevel: number) => Promise<PackageReferenceRepair>;
|
|
45
45
|
/**
|
|
46
46
|
* What {@link checkPackageIntegrity} found wrong with a package. Both fields
|
|
47
47
|
* are empty for a package where every internal relationship resolves and
|
|
@@ -75,4 +75,4 @@ export type PackageIntegrityReport = {
|
|
|
75
75
|
* or a corpus-gate invariant: one pass over the `.rels` parts building an id
|
|
76
76
|
* census, one pass over the XML parts checking references against it.
|
|
77
77
|
*/
|
|
78
|
-
export declare const checkPackageIntegrity: (zip:
|
|
78
|
+
export declare const checkPackageIntegrity: (zip: ZipArchive) => Promise<PackageIntegrityReport>;
|