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
@@ -68,10 +68,17 @@ export class Crc32MismatchError extends ArchiveError {
68
68
  * This is a security feature to detect ZIP bombs and corrupted archives.
69
69
  */
70
70
  export class EntrySizeMismatchError extends ArchiveError {
71
- constructor(path, expected, actual, reason, options) {
72
- const msg = reason === "too-many-bytes"
73
- ? `Entry "${path}" produced more bytes than declared: expected ${expected}, got at least ${actual}`
74
- : `Entry "${path}" produced fewer bytes than declared: expected ${expected}, got ${actual}`;
71
+ constructor(path, expected,
72
+ /**
73
+ * Bytes produced. `undefined` when the inflater was stopped at the
74
+ * declared size, so no count beyond it exists.
75
+ */
76
+ actual, reason, options) {
77
+ const msg = reason === "too-few-bytes"
78
+ ? `Entry "${path}" produced fewer bytes than declared: expected ${expected}, got ${actual}`
79
+ : actual === undefined
80
+ ? `Entry "${path}" exceeds its declared ${expected} bytes`
81
+ : `Entry "${path}" produced more bytes than declared: expected ${expected}, got at least ${actual}`;
75
82
  super(msg, options);
76
83
  this.path = path;
77
84
  this.expected = expected;
@@ -92,6 +99,21 @@ export class EntrySizeMismatchError extends ArchiveError {
92
99
  return this.reason === "too-few-bytes";
93
100
  }
94
101
  }
102
+ /**
103
+ * Error thrown when decompressing or extracting would exceed a configured limit.
104
+ */
105
+ export class ArchiveLimitError extends ArchiveError {
106
+ constructor(limit, allowed, options) {
107
+ super(limit === "maxOutputLength"
108
+ ? `Decompressed output exceeds maxOutputLength (${allowed} bytes)`
109
+ : limit === "maxEntries"
110
+ ? `Archive entry count exceeds maxEntries (${allowed})`
111
+ : `Archive uncompressed size exceeds maxTotalUncompressedSize (${allowed} bytes)`, options);
112
+ this.limit = limit;
113
+ this.allowed = allowed;
114
+ this.name = "ArchiveLimitError";
115
+ }
116
+ }
95
117
  // -----------------------------------------------------------------------------
96
118
  // Encryption Errors
97
119
  // -----------------------------------------------------------------------------
@@ -37,4 +37,4 @@ export {
37
37
  // Abort error class
38
38
  AbortError,
39
39
  // Error classes
40
- ArchiveError, ZipParseError, InvalidZipSignatureError, EocdNotFoundError, Crc32MismatchError, DecryptionError, PasswordRequiredError, RangeNotSupportedError, HttpRangeError, FileTooLargeError, UnsupportedCompressionError, EntrySizeMismatchError } from "./core/errors.js";
40
+ ArchiveError, ZipParseError, InvalidZipSignatureError, EocdNotFoundError, Crc32MismatchError, DecryptionError, PasswordRequiredError, RangeNotSupportedError, HttpRangeError, FileTooLargeError, UnsupportedCompressionError, EntrySizeMismatchError, ArchiveLimitError } from "./core/errors.js";
@@ -12,7 +12,7 @@
12
12
  import { decompress, decompressSync } from "#platform/modules/archive/compression/compress";
13
13
  import { crc32, crc32Finalize, crc32Update } from "#platform/modules/archive/compression/crc32";
14
14
  import { createInflateStream } from "#platform/modules/archive/compression/streaming-compress";
15
- import { ArchiveError, Crc32MismatchError, DecryptionError, EntrySizeMismatchError, FileTooLargeError, PasswordRequiredError, UnsupportedCompressionError, throwIfAborted, toError } from "../core/errors.js";
15
+ import { ArchiveError, ArchiveLimitError, Crc32MismatchError, DecryptionError, EntrySizeMismatchError, FileTooLargeError, PasswordRequiredError, UnsupportedCompressionError, throwIfAborted, toError } from "../core/errors.js";
16
16
  import { ZIP_CRYPTO_HEADER_SIZE, aesDecrypt, zipCryptoDecrypt, zipCryptoDecryptByte, zipCryptoInitKeys } from "../crypto/index.js";
17
17
  import { collect } from "../io/archive-sink.js";
18
18
  import { BinaryReader } from "../zip-spec/binary.js";
@@ -25,11 +25,45 @@ import { concatUint8Arrays } from "../../../utils/binary.js";
25
25
  export const LOCAL_HEADER_FIXED_SIZE = 30;
26
26
  /**
27
27
  * Maximum allowed uncompressed entry size for non-streaming extraction (512 MB).
28
- * This is a pre-decompression check based on the declared size in the ZIP metadata.
29
- * It prevents memory exhaustion from archives that declare very large output sizes.
28
+ *
29
+ * Enforced twice: against the *declared* size before anything is inflated, and
30
+ * as the inflater's output bound, so an entry that lies about its size is
31
+ * stopped as soon as it overruns rather than after it has been materialised.
30
32
  * The streaming extraction path (processEntryDataStream) has its own byte-level checks.
31
33
  */
32
34
  const DEFAULT_MAX_ENTRY_SIZE = 512 * 1024 * 1024;
35
+ /**
36
+ * Reject an entry whose declared size exceeds `maxEntrySize`, and fix the
37
+ * output bound to inflate it under.
38
+ *
39
+ * With size validation on, the bound is the declared size exactly: any byte
40
+ * past it already makes the entry invalid, so there is no reason to produce
41
+ * it. With validation off the declared size is not trusted for anything, and
42
+ * `maxEntrySize` alone bounds the in-memory result.
43
+ */
44
+ function planEntryInflation(entry, validateEntrySizes, maxEntrySize) {
45
+ if (validateEntrySizes && entry.uncompressedSize > maxEntrySize) {
46
+ throw new FileTooLargeError(entry.path, `declares uncompressed size of ${entry.uncompressedSize} bytes, ` +
47
+ `which exceeds the maximum allowed size of ${maxEntrySize} bytes. ` +
48
+ "Use the streaming API for large entries.");
49
+ }
50
+ const bound = validateEntrySizes ? entry.uncompressedSize : maxEntrySize;
51
+ return { entry, validateEntrySizes, bound };
52
+ }
53
+ /**
54
+ * Translate an inflater limit hit into the error that names the entry.
55
+ */
56
+ function translateOutputLimit(err, plan) {
57
+ if (!(err instanceof ArchiveLimitError)) {
58
+ return err;
59
+ }
60
+ const { entry, bound } = plan;
61
+ return plan.validateEntrySizes
62
+ ? new EntrySizeMismatchError(entry.path, entry.uncompressedSize, undefined, "too-many-bytes", {
63
+ cause: err
64
+ })
65
+ : new FileTooLargeError(entry.path, `inflates beyond ${bound} bytes`, { cause: err });
66
+ }
33
67
  /**
34
68
  * Process compressed (and possibly encrypted) entry data to get the final content.
35
69
  *
@@ -40,19 +74,14 @@ const DEFAULT_MAX_ENTRY_SIZE = 512 * 1024 * 1024;
40
74
  * @param password - Optional password for decryption
41
75
  * @param checkCrc32 - Whether to validate CRC32 checksum (default: false)
42
76
  * @param validateEntrySizes - Whether to validate decompressed size matches declared size (default: true)
77
+ * @param maxEntrySize - Largest entry that may be inflated into memory (default: 512 MiB)
43
78
  * @returns Decompressed entry content
44
79
  */
45
- export async function processEntryData(entry, compressedData, password, checkCrc32 = false, validateEntrySizes = true) {
80
+ export async function processEntryData(entry, compressedData, password, checkCrc32 = false, validateEntrySizes = true, maxEntrySize = DEFAULT_MAX_ENTRY_SIZE) {
46
81
  let result;
47
- // Pre-decompression size check: reject entries whose *declared* uncompressed size
48
- // exceeds the limit. This catches archives that honestly declare very large entries
49
- // but does NOT protect against ZIP bombs that lie about their size (for that, use
50
- // the streaming path processEntryDataStream which validates actual output bytes).
51
- if (validateEntrySizes && entry.uncompressedSize > DEFAULT_MAX_ENTRY_SIZE) {
52
- throw new FileTooLargeError(entry.path, `declares uncompressed size of ${entry.uncompressedSize} bytes, ` +
53
- `which exceeds the maximum allowed size of ${DEFAULT_MAX_ENTRY_SIZE} bytes. ` +
54
- "Use the streaming API for large entries.");
55
- }
82
+ // Rejects an over-large declared size up front, and bounds the inflater so an
83
+ // entry that lies about its size stops as soon as it overruns.
84
+ const plan = planEntryInflation(entry, validateEntrySizes, maxEntrySize);
56
85
  // Handle encrypted entries
57
86
  if (entry.isEncrypted) {
58
87
  if (!password) {
@@ -65,7 +94,7 @@ export async function processEntryData(entry, compressedData, password, checkCrc
65
94
  throw new DecryptionError(entry.path);
66
95
  }
67
96
  // Decompress if needed (use original compression method)
68
- result = await decompressData(decrypted, entry.originalCompressionMethod ?? COMPRESSION_STORE, entry.path);
97
+ result = await decompressData(decrypted, entry.originalCompressionMethod ?? COMPRESSION_STORE, plan);
69
98
  }
70
99
  else if (entry.encryptionMethod === "zipcrypto") {
71
100
  // ZipCrypto decryption
@@ -73,7 +102,7 @@ export async function processEntryData(entry, compressedData, password, checkCrc
73
102
  if (!decrypted) {
74
103
  throw new DecryptionError(entry.path);
75
104
  }
76
- result = await decompressData(decrypted, entry.compressionMethod, entry.path);
105
+ result = await decompressData(decrypted, entry.compressionMethod, plan);
77
106
  }
78
107
  else {
79
108
  throw new DecryptionError(entry.path, "Unsupported encryption method");
@@ -81,7 +110,7 @@ export async function processEntryData(entry, compressedData, password, checkCrc
81
110
  }
82
111
  else {
83
112
  // Non-encrypted entry
84
- result = await decompressData(compressedData, entry.compressionMethod, entry.path);
113
+ result = await decompressData(compressedData, entry.compressionMethod, plan);
85
114
  }
86
115
  // Validate entry size (ZIP bomb protection)
87
116
  if (validateEntrySizes && result.length !== entry.uncompressedSize) {
@@ -111,17 +140,14 @@ export async function processEntryData(entry, compressedData, password, checkCrc
111
140
  * @param compressedData - Raw compressed (and possibly encrypted) data from the ZIP
112
141
  * @param password - Optional password for decryption
113
142
  * @param validateEntrySizes - Whether to validate decompressed size matches declared size (default: true)
143
+ * @param maxEntrySize - Largest entry that may be inflated into memory (default: 512 MiB)
114
144
  * @returns Decompressed entry content
115
145
  * @throws Error if the entry uses AES encryption
116
146
  */
117
- export function processEntryDataSync(entry, compressedData, password, validateEntrySizes = true) {
147
+ export function processEntryDataSync(entry, compressedData, password, validateEntrySizes = true, maxEntrySize = DEFAULT_MAX_ENTRY_SIZE) {
118
148
  let result;
119
- // Pre-decompression size check (same as async version)
120
- if (validateEntrySizes && entry.uncompressedSize > DEFAULT_MAX_ENTRY_SIZE) {
121
- throw new FileTooLargeError(entry.path, `declares uncompressed size of ${entry.uncompressedSize} bytes, ` +
122
- `which exceeds the maximum allowed size of ${DEFAULT_MAX_ENTRY_SIZE} bytes. ` +
123
- "Use the streaming API for large entries.");
124
- }
149
+ // Same up-front check and inflater bound as the async version.
150
+ const plan = planEntryInflation(entry, validateEntrySizes, maxEntrySize);
125
151
  // Handle encrypted entries
126
152
  if (entry.isEncrypted) {
127
153
  if (!password) {
@@ -137,7 +163,7 @@ export function processEntryDataSync(entry, compressedData, password, validateEn
137
163
  if (!decrypted) {
138
164
  throw new DecryptionError(entry.path);
139
165
  }
140
- result = decompressDataSync(decrypted, entry.compressionMethod, entry.path);
166
+ result = decompressDataSync(decrypted, entry.compressionMethod, plan);
141
167
  // Always verify CRC32 for ZipCrypto because header verification only checks 1 byte
142
168
  // (1/256 false positive rate with wrong password per ZIP spec)
143
169
  const actualCrc = crc32(result);
@@ -151,7 +177,7 @@ export function processEntryDataSync(entry, compressedData, password, validateEn
151
177
  }
152
178
  else {
153
179
  // Non-encrypted entry
154
- result = decompressDataSync(compressedData, entry.compressionMethod, entry.path);
180
+ result = decompressDataSync(compressedData, entry.compressionMethod, plan);
155
181
  }
156
182
  // Validate entry size (ZIP bomb protection)
157
183
  if (validateEntrySizes && result.length !== entry.uncompressedSize) {
@@ -161,14 +187,20 @@ export function processEntryDataSync(entry, compressedData, password, validateEn
161
187
  return result;
162
188
  }
163
189
  /**
164
- * Decompress data based on compression method (async).
190
+ * Decompress an entry's data based on compression method (async), under the
191
+ * entry's output bound.
165
192
  */
166
- async function decompressData(data, compressionMethod, path) {
193
+ async function decompressData(data, compressionMethod, plan) {
167
194
  if (compressionMethod === COMPRESSION_STORE) {
168
195
  return data;
169
196
  }
170
197
  if (compressionMethod === COMPRESSION_DEFLATE) {
171
- return decompress(data);
198
+ try {
199
+ return await decompress(data, { maxOutputLength: plan.bound });
200
+ }
201
+ catch (err) {
202
+ throw translateOutputLimit(err, plan);
203
+ }
172
204
  }
173
205
  throw new UnsupportedCompressionError(compressionMethod);
174
206
  }
@@ -429,7 +461,7 @@ function applyValidationStreams(entry, source, options) {
429
461
  * requires the full ciphertext; this function falls back to buffering for AES.
430
462
  */
431
463
  export function processEntryDataStream(entry, compressedData, options = {}) {
432
- const { password, checkCrc32 = false, validateEntrySizes = true, signal } = options;
464
+ const { password, checkCrc32 = false, validateEntrySizes = true, maxEntrySize = DEFAULT_MAX_ENTRY_SIZE, signal } = options;
433
465
  async function* run() {
434
466
  throwIfAborted(signal);
435
467
  if (entry.type === "directory") {
@@ -441,11 +473,13 @@ export function processEntryDataStream(entry, compressedData, options = {}) {
441
473
  throw new PasswordRequiredError(entry.path);
442
474
  }
443
475
  if (entry.encryptionMethod === "aes" && entry.aesKeyStrength) {
476
+ // Reject an over-large declared size before buffering the ciphertext.
477
+ const plan = planEntryInflation(entry, validateEntrySizes, maxEntrySize);
444
478
  // AES requires full ciphertext to verify HMAC.
445
479
  const encrypted = await collect(compressedData);
446
480
  const decrypted = await aesDecrypt(encrypted, password, entry.aesKeyStrength);
447
481
  const method = entry.originalCompressionMethod ?? COMPRESSION_STORE;
448
- const out = await decompressData(decrypted, method, entry.path);
482
+ const out = await decompressData(decrypted, method, plan);
449
483
  // Validate size for AES entries (already fully buffered)
450
484
  if (validateEntrySizes && out.length !== entry.uncompressedSize) {
451
485
  const reason = out.length > entry.uncompressedSize ? "too-many-bytes" : "too-few-bytes";
@@ -483,7 +517,7 @@ export function processEntryDataStream(entry, compressedData, options = {}) {
483
517
  // Fallback for other encryption methods.
484
518
  const encrypted = await collect(compressedData);
485
519
  // processEntryData already handles size validation internally
486
- const out = await processEntryData(entry, encrypted, password, checkCrc32, validateEntrySizes);
520
+ const out = await processEntryData(entry, encrypted, password, checkCrc32, validateEntrySizes, maxEntrySize);
487
521
  if (out.length) {
488
522
  yield out;
489
523
  }
@@ -513,14 +547,20 @@ export function processEntryDataStream(entry, compressedData, options = {}) {
513
547
  return run();
514
548
  }
515
549
  /**
516
- * Decompress data based on compression method (sync).
550
+ * Decompress an entry's data based on compression method (sync), under the
551
+ * entry's output bound.
517
552
  */
518
- function decompressDataSync(data, compressionMethod, path) {
553
+ function decompressDataSync(data, compressionMethod, plan) {
519
554
  if (compressionMethod === COMPRESSION_STORE) {
520
555
  return data;
521
556
  }
522
557
  if (compressionMethod === COMPRESSION_DEFLATE) {
523
- return decompressSync(data);
558
+ try {
559
+ return decompressSync(data, { maxOutputLength: plan.bound });
560
+ }
561
+ catch (err) {
562
+ throw translateOutputLimit(err, plan);
563
+ }
524
564
  }
525
565
  throw new UnsupportedCompressionError(compressionMethod);
526
566
  }
@@ -4,9 +4,10 @@
4
4
  * No dependency on Node.js stream module
5
5
  */
6
6
  import { EMPTY_UINT8ARRAY } from "../core/bytes.js";
7
- import { FileTooLargeError } from "../core/errors.js";
7
+ import { ArchiveLimitError, FileTooLargeError } from "../core/errors.js";
8
8
  import { processEntryData, processEntryDataSync, readEntryCompressedData } from "./zip-extract-core.js";
9
9
  import { parseZipArchiveFromBuffer } from "../zip-spec/zip-parser-core.js";
10
+ import { assertLimitOption } from "../../../utils/limits.js";
10
11
  const MAX_SAFE_INTEGER_BIGINT = BigInt(Number.MAX_SAFE_INTEGER);
11
12
  function assertEntryExtractableInMemory(entry) {
12
13
  // This parser extracts into memory. If ZIP64 values exceed JS safe integers,
@@ -29,7 +30,8 @@ function assertEntryExtractableInMemory(entry) {
29
30
  function parseZipArchive(data, options = {}) {
30
31
  return parseZipArchiveFromBuffer(data, {
31
32
  decodeStrings: options.decodeStrings,
32
- encoding: options.encoding
33
+ encoding: options.encoding,
34
+ maxEntries: options.maxEntries
33
35
  });
34
36
  }
35
37
  /**
@@ -41,7 +43,7 @@ async function extractEntryData(data, entry, options = {}) {
41
43
  }
42
44
  assertEntryExtractableInMemory(entry);
43
45
  const compressedData = readEntryCompressedData(data, entry);
44
- return processEntryData(entry, compressedData, options.password);
46
+ return processEntryData(entry, compressedData, options.password, false, true, options.maxEntrySize);
45
47
  }
46
48
  /**
47
49
  * Extract file data synchronously (only supports ZipCrypto, not AES)
@@ -52,19 +54,39 @@ function extractEntryDataSync(data, entry, options = {}) {
52
54
  }
53
55
  assertEntryExtractableInMemory(entry);
54
56
  const compressedData = readEntryCompressedData(data, entry);
55
- return processEntryDataSync(entry, compressedData, options.password);
57
+ return processEntryDataSync(entry, compressedData, options.password, true, options.maxEntrySize);
56
58
  }
57
59
  /**
58
60
  * High-level ZIP parser class
59
61
  */
60
62
  export class ZipParser {
61
63
  constructor(data, options = {}) {
64
+ assertLimitOption("maxEntrySize", options.maxEntrySize);
65
+ assertLimitOption("maxTotalUncompressedSize", options.maxTotalUncompressedSize);
62
66
  this.data = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
63
67
  const result = parseZipArchive(this.data, options);
64
68
  this.entries = result.entries;
65
69
  this.archiveComment = result.comment;
66
70
  this.entryMap = new Map(this.entries.map(e => [e.path, e]));
67
71
  this.password = options.password;
72
+ this.maxEntrySize = options.maxEntrySize;
73
+ this.maxTotalUncompressedSize = options.maxTotalUncompressedSize ?? Infinity;
74
+ }
75
+ /**
76
+ * Reject a bulk extraction whose declared sizes exceed the total limit.
77
+ * Each entry is inflated under a bound of its declared size, so the declared
78
+ * sum is an upper bound on what extraction can actually produce.
79
+ */
80
+ assertTotalWithinLimit() {
81
+ let total = 0;
82
+ for (const entry of this.entries) {
83
+ if (entry.type !== "directory") {
84
+ total += entry.uncompressedSize;
85
+ }
86
+ }
87
+ if (total > this.maxTotalUncompressedSize) {
88
+ throw new ArchiveLimitError("maxTotalUncompressedSize", this.maxTotalUncompressedSize);
89
+ }
68
90
  }
69
91
  /**
70
92
  * Set the password for encrypted entries.
@@ -179,7 +201,10 @@ export class ZipParser {
179
201
  if (!entry) {
180
202
  return null;
181
203
  }
182
- return extractEntryData(this.data, entry, { password: password ?? this.password });
204
+ return extractEntryData(this.data, entry, {
205
+ password: password ?? this.password,
206
+ maxEntrySize: this.maxEntrySize
207
+ });
183
208
  }
184
209
  /**
185
210
  * Extract a single file (sync)
@@ -195,7 +220,10 @@ export class ZipParser {
195
220
  if (!entry) {
196
221
  return null;
197
222
  }
198
- return extractEntryDataSync(this.data, entry, { password: password ?? this.password });
223
+ return extractEntryDataSync(this.data, entry, {
224
+ password: password ?? this.password,
225
+ maxEntrySize: this.maxEntrySize
226
+ });
199
227
  }
200
228
  /**
201
229
  * Extract all files (async)
@@ -204,8 +232,12 @@ export class ZipParser {
204
232
  async extractAll(password) {
205
233
  const result = new Map();
206
234
  const pw = password ?? this.password;
235
+ this.assertTotalWithinLimit();
207
236
  for (const entry of this.entries) {
208
- const data = await extractEntryData(this.data, entry, { password: pw });
237
+ const data = await extractEntryData(this.data, entry, {
238
+ password: pw,
239
+ maxEntrySize: this.maxEntrySize
240
+ });
209
241
  result.set(entry.path, data);
210
242
  }
211
243
  return result;
@@ -222,8 +254,12 @@ export class ZipParser {
222
254
  extractAllSync(password) {
223
255
  const result = {};
224
256
  const pw = password ?? this.password;
257
+ this.assertTotalWithinLimit();
225
258
  for (const entry of this.entries) {
226
- result[entry.path] = extractEntryDataSync(this.data, entry, { password: pw });
259
+ result[entry.path] = extractEntryDataSync(this.data, entry, {
260
+ password: pw,
261
+ maxEntrySize: this.maxEntrySize
262
+ });
227
263
  }
228
264
  return result;
229
265
  }
@@ -234,11 +270,21 @@ export class ZipParser {
234
270
  */
235
271
  async forEach(callback, password) {
236
272
  const pw = password ?? this.password;
273
+ let produced = 0;
237
274
  for (const entry of this.entries) {
238
275
  let dataPromise = null;
239
276
  const getData = () => {
240
277
  if (!dataPromise) {
241
- dataPromise = extractEntryData(this.data, entry, { password: pw });
278
+ dataPromise = extractEntryData(this.data, entry, {
279
+ password: pw,
280
+ maxEntrySize: this.maxEntrySize
281
+ }).then(bytes => {
282
+ produced += bytes.length;
283
+ if (produced > this.maxTotalUncompressedSize) {
284
+ throw new ArchiveLimitError("maxTotalUncompressedSize", this.maxTotalUncompressedSize);
285
+ }
286
+ return bytes;
287
+ });
242
288
  }
243
289
  return dataPromise;
244
290
  };
@@ -4,12 +4,12 @@
4
4
  * This module contains pure functions for parsing EOCD, ZIP64 EOCD, and Central Directory.
5
5
  * Used by both `ZipParser` (in-memory) and `RemoteZipReader` (random-access).
6
6
  */
7
- import { ArchiveError, EocdNotFoundError } from "../core/errors.js";
7
+ import { ArchiveError, ArchiveLimitError, EocdNotFoundError } from "../core/errors.js";
8
8
  import { decodeZipPath, decodeZipComment, resolveZipStringCodec } from "../core/text.js";
9
9
  import { BinaryReader } from "./binary.js";
10
10
  import { resolveZipLastModifiedDateFromUnixSeconds } from "./timestamps.js";
11
11
  import { parseZipExtraFields } from "./zip-extra-fields.js";
12
- import { CENTRAL_DIR_HEADER_SIG, COMPRESSION_AES, UINT16_MAX, UINT32_MAX, ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIG, ZIP64_END_OF_CENTRAL_DIR_SIG, ZIP_OS_MSDOS, getUnixModeFromExternalAttributes, isSymlinkMode, isDirectoryMode } from "./zip-records.js";
12
+ import { CENTRAL_DIR_HEADER_SIG, COMPRESSION_AES, ZIP_CENTRAL_DIR_HEADER_FIXED_SIZE, UINT16_MAX, UINT32_MAX, ZIP64_END_OF_CENTRAL_DIR_LOCATOR_SIG, ZIP64_END_OF_CENTRAL_DIR_SIG, ZIP_OS_MSDOS, getUnixModeFromExternalAttributes, isSymlinkMode, isDirectoryMode } from "./zip-records.js";
13
13
  import { uint8ArrayToString as decodeUtf8 } from "../../../utils/binary.js";
14
14
  // -----------------------------------------------------------------------------
15
15
  // Constants
@@ -323,6 +323,18 @@ export function parseCentralDirectoryAt(data, offset, totalEntries, options = {}
323
323
  if (totalEntries === 0) {
324
324
  return [];
325
325
  }
326
+ // Every record is at least 46 bytes, so a count the buffer cannot hold is a
327
+ // lie — reject it before sizing an array from it.
328
+ if (totalEntries * ZIP_CENTRAL_DIR_HEADER_FIXED_SIZE > data.length - offset) {
329
+ throw new ArchiveError(`Central Directory declares ${totalEntries} entries, more than its ${data.length - offset} bytes can hold`);
330
+ }
331
+ // The 46-byte check only proves the count is *possible*; a genuine central
332
+ // directory of many tiny entries still costs one JS object (plus path
333
+ // strings and extra-field maps) per record, so the count needs its own bound.
334
+ const maxEntries = options.maxEntries ?? Infinity;
335
+ if (totalEntries > maxEntries) {
336
+ throw new ArchiveLimitError("maxEntries", maxEntries);
337
+ }
326
338
  const entries = new Array(totalEntries);
327
339
  const reader = new BinaryReader(data, offset);
328
340
  for (let i = 0; i < totalEntries; i++) {
@@ -1051,6 +1051,9 @@ export function createScanner(config) {
1051
1051
  },
1052
1052
  getBuffer() {
1053
1053
  return state.buffer.slice(state.position);
1054
+ },
1055
+ pendingLength() {
1056
+ return state.buffer.length - state.position;
1054
1057
  }
1055
1058
  };
1056
1059
  }
@@ -5,6 +5,7 @@
5
5
  * Provides parseCsv function and low-level parsing generators.
6
6
  */
7
7
  import { getUtf8ByteLength } from "../constants.js";
8
+ import { CsvError } from "../errors.js";
8
9
  import { resolveParseConfig, toScannerConfig } from "./config.js";
9
10
  import { filterValidHeaders } from "./helpers.js";
10
11
  import { splitLinesWithEndings } from "./lines.js";
@@ -95,7 +96,7 @@ export function* parseFastMode(input, config, state, errors) {
95
96
  if (config.maxRowBytes !== undefined) {
96
97
  const lineBytes = getUtf8ByteLength(line);
97
98
  if (lineBytes > config.maxRowBytes) {
98
- throw new Error(`Row exceeds the maximum size of ${config.maxRowBytes} bytes`);
99
+ throw new CsvError(`Row exceeds the maximum size of ${config.maxRowBytes} bytes`);
99
100
  }
100
101
  }
101
102
  if (config.infoOption) {
@@ -203,7 +204,7 @@ export function* parseWithScanner(input, config, state, errors) {
203
204
  const rawRow = input.slice(scanResult.rawStart, rawEndPos);
204
205
  const rowBytes = getUtf8ByteLength(rawRow);
205
206
  if (rowBytes > config.maxRowBytes) {
206
- throw new Error(`Row exceeds the maximum size of ${config.maxRowBytes} bytes`);
207
+ throw new CsvError(`Row exceeds the maximum size of ${config.maxRowBytes} bytes`);
207
208
  }
208
209
  }
209
210
  // Skip comment lines
@@ -5,6 +5,7 @@
5
5
  * Works identically in both Node.js and Browser environments.
6
6
  */
7
7
  import { DEFAULT_LINEBREAK_REGEX, getUtf8ByteLength } from "../constants.js";
8
+ import { CsvError } from "../errors.js";
8
9
  import { createParseConfig, toScannerConfig } from "../parse/config.js";
9
10
  import { createDelimiterDetector } from "../parse/delimiter-detector.js";
10
11
  import { convertRowToObject, filterValidHeaders } from "../parse/helpers.js";
@@ -444,6 +445,36 @@ export class CsvParserStream extends Transform {
444
445
  callback();
445
446
  }
446
447
  }
448
+ /**
449
+ * Fail early when the unfinished row already exceeds `maxRowBytes`.
450
+ *
451
+ * The completed-row check alone cannot bound memory: an unclosed quote (or a line that never
452
+ * ends) is never completed, so it would be buffered forever. Every UTF-16 code unit encodes
453
+ * to at least one UTF-8 byte, so a pending length above the limit proves the row is too
454
+ * large without measuring it — reading `.length` does not flatten the appended rope. Two
455
+ * code units of slack cover a line ending that has only partly arrived.
456
+ */
457
+ pendingRowLimitError(pendingLength) {
458
+ const limit = this.parseConfig.maxRowBytes;
459
+ if (limit === undefined || pendingLength <= limit + 2) {
460
+ return null;
461
+ }
462
+ return new CsvError(`Row exceeds the maximum size of ${limit} bytes`);
463
+ }
464
+ /**
465
+ * Emit the rows this chunk completed, then fail if the unfinished row left behind is
466
+ * already over the limit. The completed rows go out first: they are valid, and the
467
+ * error belongs to the row after them. `callback` is called exactly once.
468
+ */
469
+ finishChunk(rows, pendingLength, callback) {
470
+ const limitError = this.pendingRowLimitError(pendingLength);
471
+ if (!limitError) {
472
+ this.processPendingRows(rows, callback);
473
+ return;
474
+ }
475
+ // A consumer that aborted from its chunk callback asked for no more rows, not an error.
476
+ this.processPendingRows(rows, err => callback(err ?? (this.chunkAborted ? null : limitError)));
477
+ }
447
478
  /**
448
479
  * Reset info state for next row (used when skipping rows or after processing)
449
480
  */
@@ -497,7 +528,7 @@ export class CsvParserStream extends Transform {
497
528
  }
498
529
  // Scanner internally tracks unconsumed data - no need to reset
499
530
  // It will continue from where it left off on the next feed()
500
- this.processPendingRows(pendingRows, callback);
531
+ this.finishChunk(pendingRows, this.scanner.pendingLength(), callback);
501
532
  }
502
533
  /**
503
534
  * Run the hook and BOM strip on the first text, exactly as `Csv.parse` does.
@@ -712,12 +743,17 @@ export class CsvParserStream extends Transform {
712
743
  // Nothing has arrived that could end a line, so the buffer is left alone entirely —
713
744
  // touching it would flatten the rope built by appending. See fastModeLineEndPending.
714
745
  if (!this.fastModeLineEndPending) {
715
- callback();
746
+ callback(this.pendingRowLimitError(this.buffer.length));
716
747
  return;
717
748
  }
718
749
  const completeEnd = this.getFastModeCompleteDataEnd(this.buffer);
719
750
  // If no complete line, wait for more data
720
751
  if (completeEnd === -1) {
752
+ const limitError = this.pendingRowLimitError(this.buffer.length);
753
+ if (limitError) {
754
+ callback(limitError);
755
+ return;
756
+ }
721
757
  // The buffer stays, so record that it holds no line ending and spare the next
722
758
  // chunk from searching it again. Backing off by one character short of the
723
759
  // longest line ending keeps a line ending that straddles the mark reachable —
@@ -768,7 +804,7 @@ export class CsvParserStream extends Transform {
768
804
  return;
769
805
  }
770
806
  }
771
- this.processPendingRows(pendingRows, callback);
807
+ this.finishChunk(pendingRows, this.buffer.length, callback);
772
808
  }
773
809
  buildRow(rawRow, info) {
774
810
  const { dynamicTyping, castDate, groupColumnsByName = false } = this.options;
@@ -879,7 +915,7 @@ export class CsvParserStream extends Transform {
879
915
  if (raw !== undefined && this.parseConfig.maxRowBytes !== undefined) {
880
916
  const rawBytes = getUtf8ByteLength(raw);
881
917
  if (rawBytes > this.parseConfig.maxRowBytes) {
882
- callback(new Error(`Row exceeds the maximum size of ${this.parseConfig.maxRowBytes} bytes`));
918
+ callback(new CsvError(`Row exceeds the maximum size of ${this.parseConfig.maxRowBytes} bytes`));
883
919
  return "error";
884
920
  }
885
921
  }