documonster 0.19.0 → 0.20.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.
Files changed (160) hide show
  1. package/README.md +1 -1
  2. package/README_zh.md +1 -1
  3. package/dist/esm/modules/archive/compression/compress.base.js +20 -6
  4. package/dist/esm/modules/archive/compression/compress.browser.js +45 -32
  5. package/dist/esm/modules/archive/compression/compress.js +74 -18
  6. package/dist/esm/modules/archive/compression/deflate-fallback.js +14 -3
  7. package/dist/esm/modules/archive/compression/worker-pool/pool.browser.js +9 -3
  8. package/dist/esm/modules/archive/compression/worker-pool/worker-script.js +35 -7
  9. package/dist/esm/modules/archive/core/errors.js +26 -4
  10. package/dist/esm/modules/archive/index.base.js +1 -1
  11. package/dist/esm/modules/archive/unzip/zip-extract-core.js +74 -34
  12. package/dist/esm/modules/archive/unzip/zip-parser.js +55 -9
  13. package/dist/esm/modules/archive/zip-spec/zip-parser-core.js +14 -2
  14. package/dist/esm/modules/csv/parse/scanner/scanner.js +3 -0
  15. package/dist/esm/modules/csv/parse/sync.js +3 -2
  16. package/dist/esm/modules/csv/stream/parser.js +40 -4
  17. package/dist/esm/modules/csv/worker/worker-script.generated.js +1 -1
  18. package/dist/esm/modules/excel/core/cell.js +22 -2
  19. package/dist/esm/modules/excel/core/table.js +39 -2
  20. package/dist/esm/modules/excel/core/worksheet-core.js +39 -0
  21. package/dist/esm/modules/excel/core/worksheet-load.js +3 -1
  22. package/dist/esm/modules/excel/stream/workbook-reader.browser.js +34 -11
  23. package/dist/esm/modules/excel/surface/cell.js +64 -23
  24. package/dist/esm/modules/excel/utils/cell-format.js +3 -2
  25. package/dist/esm/modules/excel/xlsb/read/package.js +3 -1
  26. package/dist/esm/modules/excel/xlsx/read/package.js +17 -1
  27. package/dist/esm/modules/pdf/builder/document-builder.js +3 -1
  28. package/dist/esm/modules/pdf/builder/pdf-editor.js +218 -58
  29. package/dist/esm/modules/pdf/core/pdf-kdf.browser.js +9 -1
  30. package/dist/esm/modules/pdf/core/pdf-kdf.js +8 -0
  31. package/dist/esm/modules/pdf/core/pdf-writer.js +159 -149
  32. package/dist/esm/modules/pdf/core/pdfa.js +7 -3
  33. package/dist/esm/modules/pdf/errors.js +25 -0
  34. package/dist/esm/modules/pdf/index.js +1 -1
  35. package/dist/esm/modules/pdf/reader/pdf-decrypt.js +136 -74
  36. package/dist/esm/modules/pdf/reader/pdf-document.js +314 -105
  37. package/dist/esm/modules/pdf/reader/pdf-parser.js +29 -11
  38. package/dist/esm/modules/pdf/reader/pdf-reader.js +8 -1
  39. package/dist/esm/modules/pdf/reader/stream-filters.js +139 -77
  40. package/dist/esm/modules/pdf/render/pdf-exporter.js +3 -1
  41. package/dist/esm/modules/pdf/word-bridge.js +22 -48
  42. package/dist/esm/modules/word/advanced/field-engine.js +75 -56
  43. package/dist/esm/modules/word/builder/document-handle.js +15 -1
  44. package/dist/esm/modules/word/convert/docx-to-semantic.js +84 -83
  45. package/dist/esm/modules/word/convert/html/html-renderer.js +107 -78
  46. package/dist/esm/modules/word/convert/markdown/markdown-import.js +43 -10
  47. package/dist/esm/modules/word/convert/markdown/markdown-renderer.js +98 -130
  48. package/dist/esm/modules/word/convert/odt/odt.js +149 -55
  49. package/dist/esm/modules/word/core/text-utils.js +9 -0
  50. package/dist/esm/modules/word/errors.js +1 -1
  51. package/dist/esm/modules/word/layout/layout-constants.js +20 -23
  52. package/dist/esm/modules/word/layout/layout-full.js +259 -280
  53. package/dist/esm/modules/word/layout/layout.js +140 -197
  54. package/dist/esm/modules/word/query/final-view.js +49 -0
  55. package/dist/esm/modules/word/query/heading.js +81 -0
  56. package/dist/esm/modules/word/query/merge.js +39 -22
  57. package/dist/esm/modules/word/query/numbering-counter.js +108 -0
  58. package/dist/esm/modules/word/query/page-field.js +212 -0
  59. package/dist/esm/modules/word/query/search.js +15 -14
  60. package/dist/esm/modules/word/query/split.js +15 -25
  61. package/dist/esm/modules/word/query/style-resolve.js +196 -53
  62. package/dist/esm/modules/word/query/style-resolver.js +24 -0
  63. package/dist/esm/modules/word/reader/chart-parser.js +17 -7
  64. package/dist/esm/modules/word/reader/comments-parser.js +4 -4
  65. package/dist/esm/modules/word/reader/doc-props-parsers.js +10 -9
  66. package/dist/esm/modules/word/reader/docx-reader.js +46 -32
  67. package/dist/esm/modules/word/reader/metadata-parsers.js +5 -5
  68. package/dist/esm/modules/word/reader/numbering-parser.js +4 -3
  69. package/dist/esm/modules/word/reader/part-xml.js +26 -0
  70. package/dist/esm/modules/word/reader/reader-context.js +3 -3
  71. package/dist/esm/modules/word/reader/settings-parser.js +3 -3
  72. package/dist/esm/modules/word/reader/styles-parser.js +3 -3
  73. package/dist/esm/modules/word/reader/theme-parser.js +4 -3
  74. package/dist/esm/modules/word/surface/query.js +1 -1
  75. package/dist/esm/modules/word/writer/docx-packager.js +4 -1
  76. package/dist/esm/modules/word/writer/streaming-writer.js +11 -2
  77. package/dist/esm/modules/xml/dom.js +4 -8
  78. package/dist/esm/modules/xml/errors.js +1 -3
  79. package/dist/esm/modules/xml/sax.js +8 -7
  80. package/dist/esm/modules/xml/to-object.js +4 -8
  81. package/dist/esm/utils/crypto.browser.js +147 -0
  82. package/dist/esm/utils/crypto.js +14 -0
  83. package/dist/esm/utils/limits.js +19 -0
  84. package/dist/iife/documonster.archive.iife.min.js +39 -11
  85. package/dist/iife/documonster.csv.iife.min.js +17 -17
  86. package/dist/iife/documonster.draw.iife.min.js +1 -1
  87. package/dist/iife/documonster.excel.iife.min.js +88 -60
  88. package/dist/iife/documonster.formula.iife.min.js +1 -1
  89. package/dist/iife/documonster.markdown.iife.min.js +1 -1
  90. package/dist/iife/documonster.mermaid.iife.min.js +1 -1
  91. package/dist/iife/documonster.pdf.iife.min.js +44 -42
  92. package/dist/iife/documonster.stream.iife.min.js +1 -1
  93. package/dist/iife/documonster.word.iife.min.js +95 -67
  94. package/dist/iife/documonster.xml.iife.min.js +6 -6
  95. package/dist/types/modules/archive/compression/compress.base.d.ts +12 -3
  96. package/dist/types/modules/archive/compression/compress.browser.d.ts +7 -4
  97. package/dist/types/modules/archive/compression/compress.d.ts +9 -4
  98. package/dist/types/modules/archive/compression/deflate-fallback.d.ts +3 -1
  99. package/dist/types/modules/archive/compression/worker-pool/pool.browser.d.ts +4 -1
  100. package/dist/types/modules/archive/compression/worker-pool/types.d.ts +4 -0
  101. package/dist/types/modules/archive/core/errors.d.ts +31 -2
  102. package/dist/types/modules/archive/index.base.d.ts +1 -1
  103. package/dist/types/modules/archive/unzip/zip-extract-core.d.ts +9 -2
  104. package/dist/types/modules/archive/unzip/zip-parser.d.ts +33 -0
  105. package/dist/types/modules/archive/zip-spec/zip-parser-core.d.ts +6 -0
  106. package/dist/types/modules/csv/parse/scanner/types.d.ts +5 -0
  107. package/dist/types/modules/csv/stream/parser.d.ts +16 -0
  108. package/dist/types/modules/csv/worker/worker-script.generated.d.ts +1 -1
  109. package/dist/types/modules/excel/core/table.d.ts +15 -0
  110. package/dist/types/modules/excel/core/worksheet-core.d.ts +26 -0
  111. package/dist/types/modules/excel/stream/workbook-reader.browser.d.ts +11 -0
  112. package/dist/types/modules/excel/surface/cell.d.ts +8 -8
  113. package/dist/types/modules/excel/xlsx/types.d.ts +16 -0
  114. package/dist/types/modules/pdf/builder/document-builder.d.ts +7 -0
  115. package/dist/types/modules/pdf/builder/pdf-editor.d.ts +49 -5
  116. package/dist/types/modules/pdf/core/pdf-kdf.browser.d.ts +4 -0
  117. package/dist/types/modules/pdf/core/pdf-kdf.d.ts +4 -0
  118. package/dist/types/modules/pdf/core/pdf-writer.d.ts +49 -10
  119. package/dist/types/modules/pdf/core/pdfa.d.ts +2 -0
  120. package/dist/types/modules/pdf/errors.d.ts +19 -0
  121. package/dist/types/modules/pdf/index.d.ts +2 -2
  122. package/dist/types/modules/pdf/reader/pdf-document.d.ts +83 -5
  123. package/dist/types/modules/pdf/reader/pdf-parser.d.ts +2 -2
  124. package/dist/types/modules/pdf/reader/pdf-reader.d.ts +7 -0
  125. package/dist/types/modules/pdf/reader/stream-filters.d.ts +5 -1
  126. package/dist/types/modules/pdf/types.d.ts +7 -0
  127. package/dist/types/modules/word/convert/conversion-ir.d.ts +2 -0
  128. package/dist/types/modules/word/core/text-utils.d.ts +7 -1
  129. package/dist/types/modules/word/errors.d.ts +3 -3
  130. package/dist/types/modules/word/index.base.d.ts +1 -1
  131. package/dist/types/modules/word/layout/layout-constants.d.ts +13 -9
  132. package/dist/types/modules/word/query/final-view.d.ts +26 -0
  133. package/dist/types/modules/word/query/heading.d.ts +30 -0
  134. package/dist/types/modules/word/query/numbering-counter.d.ts +47 -0
  135. package/dist/types/modules/word/query/page-field.d.ts +79 -0
  136. package/dist/types/modules/word/query/search.d.ts +8 -2
  137. package/dist/types/modules/word/query/split.d.ts +4 -1
  138. package/dist/types/modules/word/query/style-resolve.d.ts +58 -11
  139. package/dist/types/modules/word/query/style-resolver.d.ts +29 -0
  140. package/dist/types/modules/word/reader/chart-parser.d.ts +2 -2
  141. package/dist/types/modules/word/reader/comments-parser.d.ts +1 -1
  142. package/dist/types/modules/word/reader/doc-props-parsers.d.ts +4 -4
  143. package/dist/types/modules/word/reader/metadata-parsers.d.ts +2 -2
  144. package/dist/types/modules/word/reader/numbering-parser.d.ts +1 -1
  145. package/dist/types/modules/word/reader/part-xml.d.ts +14 -0
  146. package/dist/types/modules/word/reader/reader-context.d.ts +1 -1
  147. package/dist/types/modules/word/reader/settings-parser.d.ts +1 -1
  148. package/dist/types/modules/word/reader/styles-parser.d.ts +1 -1
  149. package/dist/types/modules/word/reader/theme-parser.d.ts +1 -1
  150. package/dist/types/modules/word/surface/query.d.ts +2 -1
  151. package/dist/types/modules/word/types.d.ts +5 -1
  152. package/dist/types/modules/word/writer/docx-packager.d.ts +7 -0
  153. package/dist/types/modules/word/writer/streaming-writer.d.ts +7 -0
  154. package/dist/types/modules/xml/errors.d.ts +9 -0
  155. package/dist/types/modules/xml/index.d.ts +1 -0
  156. package/dist/types/modules/xml/sax.d.ts +2 -1
  157. package/dist/types/utils/crypto.browser.d.ts +10 -0
  158. package/dist/types/utils/crypto.d.ts +10 -0
  159. package/dist/types/utils/limits.d.ts +12 -0
  160. package/package.json +1 -1
package/README.md CHANGED
@@ -211,7 +211,7 @@ const buffer = await Workbook.toBuffer(wb);
211
211
 
212
212
  ```html
213
213
  <!-- Script tag (no bundler) — one IIFE per module, each under the shared `Documonster` global -->
214
- <script src="https://unpkg.com/documonster@0.19.0/dist/iife/documonster.excel.iife.min.js"></script>
214
+ <script src="https://unpkg.com/documonster@0.20.0/dist/iife/documonster.excel.iife.min.js"></script>
215
215
  <script>
216
216
  const { Workbook, Cell } = Documonster.Excel;
217
217
  const wb = Workbook.create();
package/README_zh.md CHANGED
@@ -198,7 +198,7 @@ const buffer = await Workbook.toBuffer(wb);
198
198
 
199
199
  ```html
200
200
  <!-- Script 标签(无需打包工具)— 每个模块一个 IIFE,共享同一个 `Documonster` 全局 -->
201
- <script src="https://unpkg.com/documonster@0.19.0/dist/iife/documonster.excel.iife.min.js"></script>
201
+ <script src="https://unpkg.com/documonster@0.20.0/dist/iife/documonster.excel.iife.min.js"></script>
202
202
  <script>
203
203
  const { Workbook, Cell } = Documonster.Excel;
204
204
  const wb = Workbook.create();
@@ -9,7 +9,7 @@
9
9
  * see deflate-fallback.ts for pure JS implementation
10
10
  */
11
11
  import { ByteQueue } from "../core/byte-queue.js";
12
- import { ArchiveError } from "../core/errors.js";
12
+ import { ArchiveError, ArchiveLimitError } from "../core/errors.js";
13
13
  /**
14
14
  * Default threshold (in bytes) to choose the lower-overhead path.
15
15
  *
@@ -106,18 +106,23 @@ export function hasDeflateRawDecompressionStream() {
106
106
  export function hasDeflateRawWebStreams() {
107
107
  return hasDeflateRawCompressionStream() && hasDeflateRawDecompressionStream();
108
108
  }
109
- export async function streamToUint8Array(reader) {
109
+ export async function streamToUint8Array(reader, maxOutputLength) {
110
110
  const out = new ByteQueue();
111
111
  while (true) {
112
112
  const { done, value } = await reader.read();
113
113
  if (done) {
114
114
  break;
115
115
  }
116
+ if (maxOutputLength !== undefined && out.length + value.length > maxOutputLength) {
117
+ // Stop pulling: the transform is cancelled by the caller, so nothing
118
+ // beyond this chunk is ever produced or buffered.
119
+ throw new ArchiveLimitError("maxOutputLength", maxOutputLength);
120
+ }
116
121
  out.append(value);
117
122
  }
118
123
  return out.read(out.length);
119
124
  }
120
- export async function transformWithStream(data, stream) {
125
+ export async function transformWithStream(data, stream, maxOutputLength) {
121
126
  const writer = stream.writable.getWriter();
122
127
  const reader = stream.readable.getReader();
123
128
  // Start reading immediately to avoid potential backpressure deadlocks
@@ -132,7 +137,16 @@ export async function transformWithStream(data, stream) {
132
137
  // though the caller had caught the failure and recovered via the pure-JS
133
138
  // fallback. A promise with both handlers already attached can never become
134
139
  // one, so abandoning it is safe on every path below.
135
- const read = streamToUint8Array(reader).then(value => ({ ok: true, value }), (error) => ({ ok: false, error }));
140
+ const read = streamToUint8Array(reader, maxOutputLength).then(value => ({ ok: true, value }), (error) => {
141
+ // A limit hit leaves the transform mid-flight; cancel it so the
142
+ // pending write settles and no further output is produced.
143
+ if (error instanceof ArchiveLimitError) {
144
+ reader.cancel(error).catch(() => {
145
+ // ignore
146
+ });
147
+ }
148
+ return { ok: false, error };
149
+ });
136
150
  try {
137
151
  let writeError;
138
152
  try {
@@ -191,9 +205,9 @@ export async function compressWithStream(data) {
191
205
  * @param data - Compressed data (deflate-raw format)
192
206
  * @returns Decompressed data
193
207
  */
194
- export async function decompressWithStream(data) {
208
+ export async function decompressWithStream(data, maxOutputLength) {
195
209
  const ds = new DecompressionStream("deflate-raw");
196
- return transformWithStream(data, ds);
210
+ return transformWithStream(data, ds, maxOutputLength);
197
211
  }
198
212
  // =============================================================================
199
213
  // Adler-32 Checksum (RFC 1950)
@@ -16,9 +16,10 @@ import { crc32 } from "./crc32.browser.js";
16
16
  import { inflateRaw, deflateRawCompressed, deflateRawStore } from "./deflate-fallback.js";
17
17
  import { deflateWithPool, inflateWithPool, hasWorkerSupport } from "./worker-pool/index.browser.js";
18
18
  import { DEFAULT_COMPRESS_LEVEL } from "../core/defaults.js";
19
- import { ArchiveError, createAbortError, isAbortError, throwIfAborted } from "../core/errors.js";
19
+ import { ArchiveError, ArchiveLimitError, createAbortError, isAbortError, throwIfAborted } from "../core/errors.js";
20
20
  import { readUint32LE } from "../zip-spec/binary.js";
21
21
  import { concatUint8Arrays } from "../../../utils/binary.js";
22
+ import { assertLimitOption } from "../../../utils/limits.js";
22
23
  // Re-export shared types and GZIP utilities
23
24
  export {};
24
25
  export { hasCompressionStream, hasWorkerSupport,
@@ -59,15 +60,17 @@ function rethrowIfAborted(err, signal) {
59
60
  }
60
61
  const deflateStrategy = {
61
62
  hasNative: hasDeflateRawCompressionStream,
62
- native: compressWithStream,
63
- worker: deflateWithPool,
64
- jsFallback: deflateRawCompressed
63
+ native: data => compressWithStream(data),
64
+ worker: (data, { level, signal, allowTransfer }) => deflateWithPool(data, { level, signal, allowTransfer }),
65
+ jsFallback: (data, options) => deflateRawCompressed(data, options.level)
65
66
  };
66
67
  const inflateStrategy = {
67
68
  hasNative: hasDeflateRawDecompressionStream,
68
- native: decompressWithStream,
69
- worker: inflateWithPool,
70
- jsFallback: inflateRaw
69
+ native: (data, options) => decompressWithStream(data, options.maxOutputLength),
70
+ // The worker enforces the bound while it inflates and reports a hit as an
71
+ // ArchiveLimitError, exactly like the in-process paths.
72
+ worker: (data, { signal, allowTransfer, maxOutputLength }) => inflateWithPool(data, { signal, allowTransfer, maxOutputLength }),
73
+ jsFallback: (data, options) => inflateRaw(data, options.maxOutputLength)
71
74
  };
72
75
  /**
73
76
  * Unified compression/decompression with automatic strategy selection.
@@ -79,26 +82,31 @@ async function processWithStrategy(strategy, data, options) {
79
82
  // If the user explicitly requested workers, honor it.
80
83
  if (useWorker === true && workerSupported) {
81
84
  try {
82
- return await strategy.worker(data, {
83
- level: options.level,
84
- signal: options.signal,
85
- allowTransfer: options.allowTransfer
86
- });
85
+ return await strategy.worker(data, options);
87
86
  }
88
87
  catch (err) {
89
88
  // If the user aborts, do NOT fall back to main-thread work.
90
89
  rethrowIfAborted(err, options.signal);
90
+ // A limit hit is a verdict on the data; retrying elsewhere would only
91
+ // reach the same one.
92
+ if (err instanceof ArchiveLimitError) {
93
+ throw err;
94
+ }
91
95
  // Fall through to best available in-process path.
92
96
  }
93
97
  }
94
98
  // Default: use native stream if supported (fastest, no worker overhead).
95
99
  if (canUseNative) {
96
100
  try {
97
- return await strategy.native(data);
101
+ return await strategy.native(data, options);
98
102
  }
99
103
  catch (err) {
100
104
  // Respect aborts — never silently retry an aborted operation.
101
105
  rethrowIfAborted(err, options.signal);
106
+ // A limit hit is a verdict on the data, not a spurious native failure.
107
+ if (err instanceof ArchiveLimitError) {
108
+ throw err;
109
+ }
102
110
  // Native CompressionStream / DecompressionStream can intermittently
103
111
  // reject input that is in fact valid (observed in Chromium under heavy
104
112
  // concurrent stream creation: a `DecompressionStream` rejects a deflate
@@ -107,19 +115,15 @@ async function processWithStrategy(strategy, data, options) {
107
115
  // set" / corruption error, fall back to the deterministic pure-JS
108
116
  // implementation. If the data is genuinely corrupt the JS path throws
109
117
  // too, so this never masks a real error.
110
- return strategy.jsFallback(data, options.level);
118
+ return strategy.jsFallback(data, options);
111
119
  }
112
120
  }
113
121
  // Use worker in fallback environments (no native deflate-raw) when appropriate.
114
122
  if (useWorker !== true && shouldUseWorker(data, options)) {
115
- return strategy.worker(data, {
116
- level: options.level,
117
- signal: options.signal,
118
- allowTransfer: options.allowTransfer
119
- });
123
+ return strategy.worker(data, options);
120
124
  }
121
125
  // Fallback to pure JS implementation.
122
- return strategy.jsFallback(data, options.level);
126
+ return strategy.jsFallback(data, options);
123
127
  }
124
128
  // =============================================================================
125
129
  // Public API
@@ -150,13 +154,15 @@ export function compressSync(data, options = {}) {
150
154
  * Decompress data using browser's native DecompressionStream or JS fallback
151
155
  */
152
156
  export async function decompress(data, options = {}) {
157
+ assertLimitOption("maxOutputLength", options.maxOutputLength);
153
158
  return processWithStrategy(inflateStrategy, data, options);
154
159
  }
155
160
  /**
156
161
  * Decompress data synchronously using pure JS implementation
157
162
  */
158
- export function decompressSync(data) {
159
- return inflateRaw(data);
163
+ export function decompressSync(data, options = {}) {
164
+ assertLimitOption("maxOutputLength", options.maxOutputLength);
165
+ return inflateRaw(data, options.maxOutputLength);
160
166
  }
161
167
  // =============================================================================
162
168
  // GZIP API
@@ -269,10 +275,11 @@ export function gzipSync(data, options = {}) {
269
275
  * - Inherits Worker Pool support from decompress() for large files
270
276
  */
271
277
  export async function gunzip(data, options = {}) {
278
+ assertLimitOption("maxOutputLength", options.maxOutputLength);
272
279
  throwIfAborted(options.signal);
273
280
  if (hasGzipDecompressionStream()) {
274
281
  const ds = new DecompressionStream("gzip");
275
- const out = await transformWithStream(data, ds);
282
+ const out = await transformWithStream(data, ds, options.maxOutputLength);
276
283
  throwIfAborted(options.signal);
277
284
  return out;
278
285
  }
@@ -284,9 +291,10 @@ export async function gunzip(data, options = {}) {
284
291
  /**
285
292
  * Gunzip data synchronously using the JS fallback.
286
293
  */
287
- export function gunzipSync(data) {
294
+ export function gunzipSync(data, options = {}) {
295
+ assertLimitOption("maxOutputLength", options.maxOutputLength);
288
296
  const { deflateData, expectedCrc32, expectedSize } = parseGzipPayload(data);
289
- const out = inflateRaw(deflateData);
297
+ const out = inflateRaw(deflateData, options.maxOutputLength);
290
298
  verifyGzipOutput(out, expectedCrc32, expectedSize);
291
299
  return out;
292
300
  }
@@ -344,11 +352,12 @@ export function zlibSync(data, options = {}) {
344
352
  * 2. Fallback: parse header + decompress (inflate-raw) + verify Adler-32
345
353
  */
346
354
  export async function unzlib(data, options = {}) {
355
+ assertLimitOption("maxOutputLength", options.maxOutputLength);
347
356
  throwIfAborted(options.signal);
348
357
  // Native "deflate" format is Zlib
349
358
  if (hasDeflateDecompressionStream()) {
350
359
  const ds = new DecompressionStream("deflate");
351
- const out = await transformWithStream(data, ds);
360
+ const out = await transformWithStream(data, ds, options.maxOutputLength);
352
361
  throwIfAborted(options.signal);
353
362
  return out;
354
363
  }
@@ -359,10 +368,14 @@ export async function unzlib(data, options = {}) {
359
368
  }
360
369
  /**
361
370
  * Decompress Zlib data (sync)
371
+ *
372
+ * @param options - `maxOutputLength` bounds the output; decoding stops with a
373
+ * `ArchiveLimitError` as soon as it is exceeded
362
374
  */
363
- export function unzlibSync(data) {
375
+ export function unzlibSync(data, options = {}) {
376
+ assertLimitOption("maxOutputLength", options.maxOutputLength);
364
377
  const { deflateData, expectedAdler32 } = parseZlibPayload(data);
365
- const out = inflateRaw(deflateData);
378
+ const out = inflateRaw(deflateData, options.maxOutputLength);
366
379
  verifyAdler32(out, expectedAdler32);
367
380
  return out;
368
381
  }
@@ -397,14 +410,14 @@ export async function decompressAuto(data, options = {}) {
397
410
  /**
398
411
  * Decompress data synchronously, automatically detecting the format
399
412
  */
400
- export function decompressAutoSync(data) {
413
+ export function decompressAutoSync(data, options = {}) {
401
414
  const format = detectCompressionFormat(data);
402
415
  switch (format) {
403
416
  case "gzip":
404
- return gunzipSync(data);
417
+ return gunzipSync(data, options);
405
418
  case "zlib":
406
- return unzlibSync(data);
419
+ return unzlibSync(data, options);
407
420
  case "deflate-raw":
408
- return decompressSync(data);
421
+ return decompressSync(data, options);
409
422
  }
410
423
  }
@@ -5,9 +5,11 @@
5
5
  * - deflate-raw: Raw DEFLATE (for ZIP files)
6
6
  * - gzip: GZIP format (for tar.gz, HTTP compression)
7
7
  */
8
+ import { kMaxLength } from "buffer";
8
9
  import { promisify } from "util";
9
10
  import * as nodeZlib from "zlib";
10
11
  import { DEFAULT_COMPRESS_LEVEL } from "../core/defaults.js";
12
+ import { ArchiveLimitError } from "../core/errors.js";
11
13
  import { uint8ArrayToNodeBufferView } from "../../../utils/binary.js";
12
14
  // Re-export shared types and utilities
13
15
  export { hasCompressionStream,
@@ -35,6 +37,7 @@ export function bufferToUint8Array(buffer) {
35
37
  const deflateRawAsync = promisify(nodeZlib.deflateRaw);
36
38
  const inflateRawAsync = promisify(nodeZlib.inflateRaw);
37
39
  import { resolveCompressThresholdBytes, detectCompressionFormat } from "./compress.base.js";
40
+ import { assertLimitOption } from "../../../utils/limits.js";
38
41
  /**
39
42
  * Compress data using Node.js native zlib
40
43
  *
@@ -79,30 +82,75 @@ export function compressSync(data, options = {}) {
79
82
  const zlibOptions = { level };
80
83
  return bufferToUint8Array(nodeZlib.deflateRawSync(input, zlibOptions));
81
84
  }
85
+ /**
86
+ * zlib options for an optional output bound. zlib rejects a bound below 1, so
87
+ * a zero bound is requested as 1 and {@link inflateBounded} rejects any output.
88
+ */
89
+ function boundOptions(maxOutputLength) {
90
+ // Every decompressing entry point resolves its bound here first.
91
+ assertLimitOption("maxOutputLength", maxOutputLength);
92
+ return maxOutputLength === undefined
93
+ ? undefined
94
+ : { maxOutputLength: Math.max(1, Math.min(maxOutputLength, kMaxLength)) };
95
+ }
96
+ /**
97
+ * Settle a zlib inflate run with {@link boundOptions}: zlib stops as soon as
98
+ * the inflater crosses the bound and throws `ERR_BUFFER_TOO_LARGE`, which
99
+ * becomes the archive's own error.
100
+ */
101
+ function inflateBounded(inflate, maxOutputLength) {
102
+ let out;
103
+ try {
104
+ out = inflate();
105
+ }
106
+ catch (err) {
107
+ if (maxOutputLength !== undefined &&
108
+ err.code === "ERR_BUFFER_TOO_LARGE") {
109
+ throw new ArchiveLimitError("maxOutputLength", maxOutputLength, { cause: err });
110
+ }
111
+ throw err;
112
+ }
113
+ if (maxOutputLength !== undefined && out.byteLength > maxOutputLength) {
114
+ throw new ArchiveLimitError("maxOutputLength", maxOutputLength);
115
+ }
116
+ return bufferToUint8Array(out);
117
+ }
118
+ /** {@link inflateBounded} for a run settled on the thread pool. */
119
+ async function inflateBoundedAsync(pending, maxOutputLength) {
120
+ const settled = await pending.then(out => () => out, (err) => () => {
121
+ throw err;
122
+ });
123
+ return inflateBounded(settled, maxOutputLength);
124
+ }
82
125
  /**
83
126
  * Decompress data using Node.js native zlib
84
127
  *
85
128
  * @param data - Compressed data (deflate-raw format)
129
+ * @param options - `maxOutputLength` bounds the output; inflation stops early
130
+ * with an `ArchiveLimitError` when it is exceeded
86
131
  * @returns Decompressed data
87
132
  */
88
133
  export async function decompress(data, options = {}) {
89
- const thresholdBytes = resolveCompressThresholdBytes(options);
90
134
  const input = uint8ArrayToBuffer(data);
135
+ const { maxOutputLength } = options;
136
+ const zlibOptions = boundOptions(maxOutputLength);
91
137
  // Small-input fast path: avoid threadpool overhead.
92
- if (data.byteLength <= thresholdBytes) {
93
- return bufferToUint8Array(nodeZlib.inflateRawSync(input));
138
+ if (data.byteLength <= resolveCompressThresholdBytes(options)) {
139
+ return inflateBounded(() => nodeZlib.inflateRawSync(input, zlibOptions), maxOutputLength);
94
140
  }
95
- return bufferToUint8Array(await inflateRawAsync(input));
141
+ return inflateBoundedAsync(inflateRawAsync(input, zlibOptions), maxOutputLength);
96
142
  }
97
143
  /**
98
144
  * Decompress data synchronously using Node.js zlib
99
145
  *
100
146
  * @param data - Compressed data (deflate-raw format)
147
+ * @param options - `maxOutputLength` bounds the output (see {@link decompress})
101
148
  * @returns Decompressed data
102
149
  */
103
- export function decompressSync(data) {
150
+ export function decompressSync(data, options = {}) {
104
151
  const input = uint8ArrayToBuffer(data);
105
- return bufferToUint8Array(nodeZlib.inflateRawSync(input));
152
+ const { maxOutputLength } = options;
153
+ return inflateBounded(() => nodeZlib.inflateRawSync(input, boundOptions(maxOutputLength)), maxOutputLength);
106
154
  }
107
155
  // =============================================================================
108
156
  // GZIP API
@@ -129,11 +177,13 @@ export async function gzip(data, options = {}) {
129
177
  export async function gunzip(data, options = {}) {
130
178
  const thresholdBytes = resolveCompressThresholdBytes(options);
131
179
  const input = uint8ArrayToBuffer(data);
180
+ const { maxOutputLength } = options;
181
+ const zlibOptions = boundOptions(maxOutputLength);
132
182
  // Small-input fast path
133
183
  if (data.byteLength <= thresholdBytes) {
134
- return bufferToUint8Array(nodeZlib.gunzipSync(input));
184
+ return inflateBounded(() => nodeZlib.gunzipSync(input, zlibOptions), maxOutputLength);
135
185
  }
136
- return bufferToUint8Array(await gunzipAsync(input));
186
+ return inflateBoundedAsync(gunzipAsync(input, zlibOptions), maxOutputLength);
137
187
  }
138
188
  /**
139
189
  * Compress data with gzip (sync)
@@ -147,9 +197,10 @@ export function gzipSync(data, options = {}) {
147
197
  /**
148
198
  * Decompress gzip data (sync)
149
199
  */
150
- export function gunzipSync(data) {
200
+ export function gunzipSync(data, options = {}) {
151
201
  const input = uint8ArrayToBuffer(data);
152
- return bufferToUint8Array(nodeZlib.gunzipSync(input));
202
+ const { maxOutputLength } = options;
203
+ return inflateBounded(() => nodeZlib.gunzipSync(input, boundOptions(maxOutputLength)), maxOutputLength);
153
204
  }
154
205
  // =============================================================================
155
206
  // ZLIB API (RFC 1950: DEFLATE with zlib header/trailer + Adler-32)
@@ -179,11 +230,13 @@ export async function zlib(data, options = {}) {
179
230
  export async function unzlib(data, options = {}) {
180
231
  const thresholdBytes = resolveCompressThresholdBytes(options);
181
232
  const input = uint8ArrayToBuffer(data);
233
+ const { maxOutputLength } = options;
234
+ const zlibOptions = boundOptions(maxOutputLength);
182
235
  // Small-input fast path
183
236
  if (data.byteLength <= thresholdBytes) {
184
- return bufferToUint8Array(nodeZlib.inflateSync(input));
237
+ return inflateBounded(() => nodeZlib.inflateSync(input, zlibOptions), maxOutputLength);
185
238
  }
186
- return bufferToUint8Array(await unzlibAsync(input));
239
+ return inflateBoundedAsync(unzlibAsync(input, zlibOptions), maxOutputLength);
187
240
  }
188
241
  /**
189
242
  * Compress data with Zlib wrapper (sync)
@@ -196,10 +249,13 @@ export function zlibSync(data, options = {}) {
196
249
  }
197
250
  /**
198
251
  * Decompress Zlib data (sync)
252
+ *
253
+ * @param options - `maxOutputLength` bounds the output (see {@link decompress})
199
254
  */
200
- export function unzlibSync(data) {
255
+ export function unzlibSync(data, options = {}) {
201
256
  const input = uint8ArrayToBuffer(data);
202
- return bufferToUint8Array(nodeZlib.inflateSync(input));
257
+ const { maxOutputLength } = options;
258
+ return inflateBounded(() => nodeZlib.inflateSync(input, boundOptions(maxOutputLength)), maxOutputLength);
203
259
  }
204
260
  // =============================================================================
205
261
  // AUTO-DETECT DECOMPRESSION
@@ -232,14 +288,14 @@ export async function decompressAuto(data, options = {}) {
232
288
  /**
233
289
  * Decompress data synchronously, automatically detecting the format
234
290
  */
235
- export function decompressAutoSync(data) {
291
+ export function decompressAutoSync(data, options = {}) {
236
292
  const format = detectCompressionFormat(data);
237
293
  switch (format) {
238
294
  case "gzip":
239
- return gunzipSync(data);
295
+ return gunzipSync(data, options);
240
296
  case "zlib":
241
- return unzlibSync(data);
297
+ return unzlibSync(data, options);
242
298
  case "deflate-raw":
243
- return decompressSync(data);
299
+ return decompressSync(data, options);
244
300
  }
245
301
  }
@@ -10,8 +10,9 @@
10
10
  * - Safari < 16.4
11
11
  * - Chrome < 103
12
12
  */
13
- import { ArchiveError } from "../core/errors.js";
13
+ import { ArchiveError, ArchiveLimitError } from "../core/errors.js";
14
14
  import { concatUint8Arrays } from "../../../utils/binary.js";
15
+ import { assertLimitOption } from "../../../utils/limits.js";
15
16
  // ============================================================================
16
17
  // DEFLATE Decompression (Full implementation)
17
18
  // ============================================================================
@@ -199,19 +200,28 @@ class BitReader {
199
200
  * Decompress DEFLATE data (raw format, no zlib header)
200
201
  *
201
202
  * @param data - Compressed data in deflate-raw format
203
+ * @param maxOutputLength - Optional output bound; decoding stops with a
204
+ * `ArchiveLimitError` before the output buffer grows past it
202
205
  * @returns Decompressed data
203
206
  */
204
- export function inflateRaw(data) {
207
+ export function inflateRaw(data, maxOutputLength) {
205
208
  const reader = new BitReader(data);
206
209
  // Output accumulator. DEFLATE has no length prefix, so we grow a typed array
207
210
  // geometrically instead of pushing into a `number[]` (which boxes every byte
208
211
  // at ~8x the memory and forces a final full copy). Seed with a heuristic
209
212
  // based on the compressed size; deflate ratios on OOXML/text are commonly
210
213
  // 3-5x, so 4x is a reasonable starting capacity that avoids most regrowths.
211
- let output = new Uint8Array(Math.max(64, data.length * 4));
214
+ assertLimitOption("maxOutputLength", maxOutputLength);
215
+ const limit = maxOutputLength ?? Infinity;
216
+ // Never larger than the bound; with a positive bound never empty, so the
217
+ // doubling below always makes progress (a zero bound throws before growing).
218
+ let output = new Uint8Array(Math.min(Math.max(64, data.length * 4), limit));
212
219
  let outLen = 0;
213
220
  const ensureCapacity = (additional) => {
214
221
  const required = outLen + additional;
222
+ if (required > limit) {
223
+ throw new ArchiveLimitError("maxOutputLength", limit);
224
+ }
215
225
  if (required <= output.length) {
216
226
  return;
217
227
  }
@@ -219,6 +229,7 @@ export function inflateRaw(data) {
219
229
  while (capacity < required) {
220
230
  capacity *= 2;
221
231
  }
232
+ capacity = Math.min(capacity, limit);
222
233
  const grown = new Uint8Array(capacity);
223
234
  grown.set(output.subarray(0, outLen));
224
235
  output = grown;
@@ -14,7 +14,7 @@
14
14
  */
15
15
  import { resolvePoolOptions, getPriorityValue, hasWorkerSupport, createAbortError } from "./pool.base.js";
16
16
  import { getWorkerBlobUrl, releaseWorkerBlobUrl } from "./worker-script.js";
17
- import { ArchiveError } from "../../core/errors.js";
17
+ import { ArchiveError, ArchiveLimitError } from "../../core/errors.js";
18
18
  export { hasWorkerSupport };
19
19
  const EMPTY_CHUNK = new Uint8Array(0);
20
20
  const DEFAULT_MAX_INFLIGHT_STREAM_CHUNKS = 8;
@@ -153,6 +153,7 @@ export class WorkerPool {
153
153
  taskType,
154
154
  data,
155
155
  level: options?.level,
156
+ maxOutputLength: options?.maxOutputLength,
156
157
  priority,
157
158
  priorityValue,
158
159
  resolve,
@@ -362,7 +363,8 @@ export class WorkerPool {
362
363
  taskId: task.taskId,
363
364
  taskType: task.taskType,
364
365
  data,
365
- level: task.level
366
+ level: task.level,
367
+ maxOutputLength: task.maxOutputLength
366
368
  };
367
369
  // Use transferables for zero-copy
368
370
  if (shouldTransfer) {
@@ -485,7 +487,11 @@ export class WorkerPool {
485
487
  }
486
488
  else {
487
489
  this._failedTasks++;
488
- task.reject(new Error(message.error ?? "Unknown worker error"));
490
+ // An Error does not survive postMessage with its class, so a limit
491
+ // hit travels as the bound and is rebuilt as the archive's own error.
492
+ task.reject(message.limit !== undefined
493
+ ? new ArchiveLimitError("maxOutputLength", message.limit)
494
+ : new Error(message.error ?? "Unknown worker error"));
489
495
  }
490
496
  this._workerBecameIdle(poolWorker);
491
497
  return;
@@ -32,16 +32,25 @@ const hasDeflateRaw = (() => {
32
32
 
33
33
  const EMPTY_UINT8ARRAY = new Uint8Array(0);
34
34
 
35
+ /** Output exceeded the task's maxOutputLength; reported to the pool as \`limit\`. */
36
+ class LimitError extends Error {
37
+ constructor(limit) {
38
+ super('Decompressed output exceeds maxOutputLength (' + limit + ' bytes)');
39
+ this.limit = limit;
40
+ }
41
+ }
42
+
35
43
  /**
36
44
  * Process data through a TransformStream (compress or decompress)
37
45
  */
38
- async function processWithStream(stream, data) {
46
+ async function processWithStream(stream, data, maxOutputLength) {
39
47
  const writer = stream.writable.getWriter();
40
48
  const reader = stream.readable.getReader();
41
49
 
42
50
  // Start reading output
43
51
  let firstChunk = null;
44
52
  let chunks = null;
53
+ let produced = 0;
45
54
  const readPromise = (async () => {
46
55
  while (true) {
47
56
  const { done, value } = await reader.read();
@@ -49,6 +58,13 @@ async function processWithStream(stream, data) {
49
58
  if (!value) {
50
59
  continue;
51
60
  }
61
+ produced += value.length;
62
+ if (maxOutputLength !== undefined && produced > maxOutputLength) {
63
+ // Stop pulling so nothing past the bound is produced or buffered.
64
+ const limitError = new LimitError(maxOutputLength);
65
+ reader.cancel(limitError).catch(() => {});
66
+ throw limitError;
67
+ }
52
68
  if (firstChunk === null) {
53
69
  firstChunk = value;
54
70
  } else if (chunks === null) {
@@ -59,12 +75,19 @@ async function processWithStream(stream, data) {
59
75
  }
60
76
  })();
61
77
 
62
- // Write input and close
63
- await writer.write(data);
64
- await writer.close();
78
+ // Write input and close. Once the reader has failed the writes reject
79
+ // too; the read's verdict is the one to report.
80
+ const read = readPromise.then(() => null, err => err);
81
+ try {
82
+ await writer.write(data);
83
+ await writer.close();
84
+ } catch (err) {
85
+ throw (await read) ?? err;
86
+ }
65
87
 
66
88
  // Wait for all output
67
- await readPromise;
89
+ const readError = await read;
90
+ if (readError) throw readError;
68
91
 
69
92
  // Fast path for common cases
70
93
  if (firstChunk === null) return EMPTY_UINT8ARRAY;
@@ -121,6 +144,7 @@ function postError(taskId, err, startTime) {
121
144
  type: 'error',
122
145
  taskId,
123
146
  error,
147
+ limit: err instanceof LimitError ? err.limit : undefined,
124
148
  duration
125
149
  });
126
150
  }
@@ -274,7 +298,7 @@ self.onmessage = async function(event) {
274
298
 
275
299
  // Handle task request
276
300
  if (msg.type === 'task') {
277
- const { taskId, taskType, data } = msg;
301
+ const { taskId, taskType, data, maxOutputLength } = msg;
278
302
  const startTime = performance.now();
279
303
 
280
304
  try {
@@ -294,7 +318,11 @@ self.onmessage = async function(event) {
294
318
  throw new Error('Unknown task type: ' + taskType);
295
319
  }
296
320
 
297
- const result = await processWithStream(stream, data);
321
+ const result = await processWithStream(
322
+ stream,
323
+ data,
324
+ taskType === 'inflate' ? maxOutputLength : undefined
325
+ );
298
326
 
299
327
  // Transfer the result buffer for zero-copy
300
328
  postResult(taskId, result, startTime);