@office-kit/xlsx 0.13.0 → 0.15.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 (52) hide show
  1. package/README.md +26 -0
  2. package/dist/{cell-style-BFmJOmcx.mjs → cell-style-CNsET6WU.mjs} +2 -2
  3. package/dist/{cell-style-BFmJOmcx.mjs.map → cell-style-CNsET6WU.mjs.map} +1 -1
  4. package/dist/{differential-H9SjEeIU.mjs → differential-BPKa7wXi.mjs} +2 -2
  5. package/dist/{differential-H9SjEeIU.mjs.map → differential-BPKa7wXi.mjs.map} +1 -1
  6. package/dist/io.mjs +2 -2
  7. package/dist/iterparse-DZE0zy2M.mjs +187 -0
  8. package/dist/iterparse-DZE0zy2M.mjs.map +1 -0
  9. package/dist/{load-CJdGh50R.mjs → load-CspGc399.mjs} +5 -5
  10. package/dist/{load-CJdGh50R.mjs.map → load-CspGc399.mjs.map} +1 -1
  11. package/dist/node.mjs +8 -4
  12. package/dist/node.mjs.map +1 -1
  13. package/dist/{reader-BNaUTVCy.mjs → reader-DHMxLBQV.mjs} +81 -26
  14. package/dist/reader-DHMxLBQV.mjs.map +1 -0
  15. package/dist/{save-CjarR7Rp.mjs → save-DgPf1DyF.mjs} +84 -44
  16. package/dist/save-DgPf1DyF.mjs.map +1 -0
  17. package/dist/streaming.mjs +156 -62
  18. package/dist/streaming.mjs.map +1 -1
  19. package/dist/styles.mjs +3 -3
  20. package/dist/{stylesheet-writer-D7Uug85X.mjs → stylesheet-writer-TxsinwbO.mjs} +71 -67
  21. package/dist/stylesheet-writer-TxsinwbO.mjs.map +1 -0
  22. package/dist/{table-Ccro4rrz.mjs → table-BrP0RwUd.mjs} +2 -2
  23. package/dist/{table-Ccro4rrz.mjs.map → table-BrP0RwUd.mjs.map} +1 -1
  24. package/dist/utf8-OAkCDG5g.mjs +21 -0
  25. package/dist/utf8-OAkCDG5g.mjs.map +1 -0
  26. package/dist/{workbook-B15-T4cs.mjs → workbook-DQCslHzY.mjs} +3 -3
  27. package/dist/{workbook-B15-T4cs.mjs.map → workbook-DQCslHzY.mjs.map} +1 -1
  28. package/dist/workbook.mjs +1 -1
  29. package/dist/worksheet/index.d.ts +1 -1
  30. package/dist/worksheet/ref-batch.d.ts +15 -0
  31. package/dist/worksheet/worksheet.d.ts +33 -0
  32. package/dist/worksheet/writer.d.ts +23 -4
  33. package/dist/{worksheet-4xW-i8j9.mjs → worksheet-B2NOQ6dM.mjs} +100 -2
  34. package/dist/worksheet-B2NOQ6dM.mjs.map +1 -0
  35. package/dist/worksheet.mjs +3 -3
  36. package/dist/{writer-Z7cF_CBk.mjs → writer-C38RBZCy.mjs} +2 -2
  37. package/dist/writer-C38RBZCy.mjs.map +1 -0
  38. package/dist/xml.mjs +2 -1
  39. package/dist/xml.mjs.map +1 -1
  40. package/dist/zip/decompression-guard.d.ts +22 -6
  41. package/dist/zip/inflate-cache.d.ts +22 -0
  42. package/dist/zip/reader.d.ts +11 -6
  43. package/dist/zip/writer.d.ts +6 -2
  44. package/dist/zip.mjs +2 -2
  45. package/package.json +1 -1
  46. package/dist/reader-BNaUTVCy.mjs.map +0 -1
  47. package/dist/save-CjarR7Rp.mjs.map +0 -1
  48. package/dist/stylesheet-writer-D7Uug85X.mjs.map +0 -1
  49. package/dist/utf8-Tn3nzyik.mjs +0 -143
  50. package/dist/utf8-Tn3nzyik.mjs.map +0 -1
  51. package/dist/worksheet-4xW-i8j9.mjs.map +0 -1
  52. package/dist/writer-Z7cF_CBk.mjs.map +0 -1
package/dist/node.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"node.mjs","names":[],"sources":["../src/io/node.ts","../src/io/node-fs.ts","../src/io/node-save.ts"],"sourcesContent":["// In-memory Node helpers.\n//\n// `fromBuffer` / `toBuffer` rely only on the global `Buffer` symbol — no\n// `node:*` imports — so they're safe to ship through the `@office-kit/xlsx/streaming`\n// browser-targeted entry too. Filesystem + Readable / Writable helpers live in\n// `./node-fs.ts` (re-exported via `@office-kit/xlsx/node`) where the `node:fs` /\n// `node:stream` imports stay out of the browser-safe surface.\n\nimport { OpenXmlIoError } from '../utils/exceptions.js';\nimport type { BufferedSinkWriter, XlsxSink } from './sink.js';\nimport type { XlsxSource } from './source.js';\n\n/**\n * Wrap a Buffer or Uint8Array as an XlsxSource. The underlying bytes are\n * referenced — no copy — so callers must not mutate them while the source is in\n * use.\n */\nexport function fromBuffer(buf: Buffer | Uint8Array): XlsxSource {\n if (!(buf instanceof Uint8Array)) {\n throw new OpenXmlIoError('fromBuffer expects a Buffer or Uint8Array');\n }\n // Buffer is a subclass of Uint8Array, so a single normalisation suffices.\n const bytes: Uint8Array = buf instanceof Buffer ? new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength) : buf;\n return {\n async toBytes() {\n return bytes;\n },\n toStream() {\n return new ReadableStream<Uint8Array>({\n start(controller) {\n controller.enqueue(bytes);\n controller.close();\n },\n });\n },\n };\n}\n\n/**\n * In-memory Buffer sink. The buffered path concatenates appended chunks into a\n * single allocation when {@link BufferedSinkWriter.finish} resolves; the\n * convenience `result()` returns it as a Node Buffer.\n */\nexport function toBuffer(): XlsxSink & { toBytes(): BufferedSinkWriter; result(): Buffer } {\n const chunks: Uint8Array[] = [];\n let finalised: Uint8Array | undefined;\n\n const finalise = (): Uint8Array => {\n if (finalised !== undefined) return finalised;\n let total = 0;\n for (const c of chunks) total += c.byteLength;\n const out = new Uint8Array(total);\n let off = 0;\n for (const c of chunks) {\n out.set(c, off);\n off += c.byteLength;\n }\n finalised = out;\n chunks.length = 0;\n return out;\n };\n\n return {\n toBytes(): BufferedSinkWriter {\n return {\n write(chunk: Uint8Array): void {\n if (finalised !== undefined) {\n throw new OpenXmlIoError('toBuffer sink: write after finish');\n }\n if (!(chunk instanceof Uint8Array)) {\n throw new OpenXmlIoError('toBuffer sink: chunk is not a Uint8Array');\n }\n chunks.push(chunk);\n },\n async finish(): Promise<Uint8Array> {\n return finalise();\n },\n abort(): void {\n if (finalised !== undefined) return;\n finalised = new Uint8Array(0);\n chunks.length = 0;\n },\n };\n },\n result(): Buffer {\n const bytes = finalise();\n return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n },\n };\n}\n","// Node filesystem + Readable / Writable I/O helpers.\n//\n// Kept separate from `./node.ts` so the buffer-only entry stays free of\n// `node:fs` / `node:stream` imports — important for the `@office-kit/xlsx/streaming`\n// browser-targeted bundle, which can re-export `fromBuffer` / `toBuffer`\n// without dragging Node-only modules into the browser surface. Users who want\n// filesystem I/O reach this module directly (or through `@office-kit/xlsx/node` once\n// that subpath lands).\n\nimport { createReadStream, createWriteStream, readFileSync } from 'node:fs';\nimport { readFile, unlink } from 'node:fs/promises';\nimport { once } from 'node:events';\nimport { Readable, Writable } from 'node:stream';\nimport { OpenXmlIoError } from '../utils/exceptions.js';\nimport type { BufferedSinkWriter, XlsxSink } from './sink.js';\nimport type { XlsxSource } from './source.js';\n\nconst EMPTY_BYTES = new Uint8Array(0);\n\n/**\n * Wrap a filesystem path as an XlsxSource. `toBytes` reads the whole file into\n * memory; `toStream` opens a `fs.createReadStream` and bridges it to a Web\n * {@link ReadableStream} via `Readable.toWeb` so the ZIP reader can iterate\n * without loading the entire xlsx up front.\n */\nexport function fromFile(path: string): XlsxSource {\n if (typeof path !== 'string' || path.length === 0) {\n throw new OpenXmlIoError('fromFile expects a non-empty path string');\n }\n return {\n async toBytes() {\n try {\n return new Uint8Array(await readFile(path));\n } catch (cause) {\n throw new OpenXmlIoError(`fromFile: failed to read \"${path}\"`, { cause });\n }\n },\n toStream() {\n const nodeStream = createReadStream(path);\n return Readable.toWeb(nodeStream) as unknown as ReadableStream<Uint8Array>;\n },\n };\n}\n\n/**\n * Synchronous variant of {@link fromFile}. Convenience for tooling / scripts\n * where the cost of `await fs.readFile` outweighs the ergonomic gain. The\n * returned source's `toBytes` resolves immediately with the bytes already in\n * memory.\n */\nexport function fromFileSync(path: string): XlsxSource {\n if (typeof path !== 'string' || path.length === 0) {\n throw new OpenXmlIoError('fromFileSync expects a non-empty path string');\n }\n let bytes: Uint8Array;\n try {\n bytes = new Uint8Array(readFileSync(path));\n } catch (cause) {\n throw new OpenXmlIoError(`fromFileSync: failed to read \"${path}\"`, { cause });\n }\n return {\n async toBytes() {\n return bytes;\n },\n toStream() {\n return new ReadableStream<Uint8Array>({\n start(controller) {\n controller.enqueue(bytes);\n controller.close();\n },\n });\n },\n };\n}\n\n/**\n * Filesystem sink. Each `write(chunk)` call streams the bytes to disk via\n * `fs.createWriteStream`, honouring backpressure: the actual `writable.write`\n * for each chunk is queued behind any pending `drain`, so the writable's\n * internal buffer never grows past its `highWaterMark` (default 16 KB) no\n * matter how fast the producer hands chunks over.\n *\n * Note on the producer-side memory budget: the sink contract is\n * intentionally synchronous (`write(chunk): void`), so a producer that races\n * ahead without yielding will let chunk references pile up in the queue.\n * That keeps `writable`'s buffer bounded but does not bound the queue\n * itself. Producers that need a hard ceiling should yield between writes\n * (`await new Promise(setImmediate)` is enough) or use a sink with an async\n * write contract.\n *\n * `result()` returns the destination path; `finish()` resolves with an empty\n * `Uint8Array` once the stream has flushed. Callers that need the on-disk\n * bytes should `readFile()` the returned path themselves — re-reading inside\n * `finish()` would defeat the \"streamed to disk, never resident\" guarantee.\n */\nexport function toFile(path: string): XlsxSink & { toBytes(): BufferedSinkWriter; result(): string } {\n if (typeof path !== 'string' || path.length === 0) {\n throw new OpenXmlIoError('toFile expects a non-empty path string');\n }\n let stream: ReturnType<typeof createWriteStream> | undefined;\n let streamCreated = false;\n let finalised: Promise<Uint8Array> | undefined;\n let pendingError: Error | undefined;\n // Backpressure queue: every chunk's actual `writable.write` call is staged\n // behind the previous chunk's completion. When a write returns `false` the\n // queue parks on `drain` before the next chunk goes out, so the writable's\n // internal buffer stays within its highWaterMark.\n let writeQueue: Promise<void> = Promise.resolve();\n\n const ensureStream = (): NonNullable<typeof stream> => {\n if (!stream) {\n stream = createWriteStream(path);\n streamCreated = true;\n stream.on('error', (err) => {\n pendingError = err instanceof Error ? err : new Error(String(err));\n });\n }\n return stream;\n };\n\n // Best-effort: remove a half-written file when finish() fails so callers\n // don't mistake a corrupt artefact for a successful save. Swallows unlink\n // errors because we're already in a failure path and the original cause\n // is what the caller needs to see.\n const cleanupOnFailure = async (): Promise<void> => {\n if (!streamCreated) return;\n try {\n await unlink(path);\n } catch {\n // ignore — file may be gone, path may be on a read-only fs, etc.\n }\n };\n\n return {\n toBytes(): BufferedSinkWriter {\n return {\n write(chunk: Uint8Array): void {\n if (finalised !== undefined) throw new OpenXmlIoError(`toFile sink: write after finish (\"${path}\")`);\n if (!(chunk instanceof Uint8Array)) {\n throw new OpenXmlIoError(`toFile sink: chunk is not a Uint8Array (\"${path}\")`);\n }\n if (pendingError) throw new OpenXmlIoError(`toFile sink: write error on \"${path}\"`, { cause: pendingError });\n const s = ensureStream();\n writeQueue = writeQueue.then(async () => {\n // Skip remaining work once the stream has errored — the error\n // surfaces from `finish()` so callers see one consistent failure.\n if (pendingError) return;\n const ok = s.write(chunk);\n if (!ok) {\n // Writable's internal buffer is over highWaterMark; wait for it\n // to flush before the next queued chunk runs.\n await once(s, 'drain');\n }\n });\n },\n async finish(): Promise<Uint8Array> {\n if (finalised) return finalised;\n finalised = (async () => {\n const s = ensureStream();\n try {\n await writeQueue;\n await new Promise<void>((resolve, reject) => {\n s.end((err?: Error | null) => (err ? reject(err) : resolve()));\n });\n if (pendingError) {\n throw new OpenXmlIoError(`toFile sink: write error on \"${path}\"`, { cause: pendingError });\n }\n } catch (err) {\n await cleanupOnFailure();\n throw err;\n }\n return EMPTY_BYTES;\n })();\n return finalised;\n },\n abort(): void {\n // Idempotent: subsequent finish() / abort() calls become no-ops.\n if (finalised) return;\n finalised = Promise.resolve(EMPTY_BYTES);\n if (stream) {\n // destroy() releases the fd synchronously without flushing the\n // pending buffer — exactly what we want for an aborted save.\n stream.destroy();\n }\n // Fire-and-forget unlink — abort() is sync (void), and the caller\n // is already on a failure path so any unlink error is noise.\n void cleanupOnFailure();\n },\n };\n },\n result(): string {\n return path;\n },\n };\n}\n\n/**\n * Wrap a Node.js {@link Readable} as an XlsxSource. `toBytes` consumes the\n * entire stream synchronously (collecting chunks); `toStream` bridges to a Web\n * ReadableStream via `Readable.toWeb` so the ZIP reader can pull chunks lazily.\n */\nexport function fromReadable(readable: Readable): XlsxSource {\n if (!(readable instanceof Readable)) {\n throw new OpenXmlIoError('fromReadable expects a Node Readable');\n }\n let bytes: Promise<Uint8Array> | undefined;\n return {\n async toBytes() {\n if (bytes) return bytes;\n bytes = (async () => {\n const chunks: Uint8Array[] = [];\n for await (const c of readable) {\n chunks.push(c instanceof Uint8Array ? c : new Uint8Array(c));\n }\n let total = 0;\n for (const c of chunks) total += c.byteLength;\n const out = new Uint8Array(total);\n let off = 0;\n for (const c of chunks) {\n out.set(c, off);\n off += c.byteLength;\n }\n return out;\n })();\n return bytes;\n },\n toStream() {\n return Readable.toWeb(readable) as unknown as ReadableStream<Uint8Array>;\n },\n };\n}\n\n/**\n * Wrap a Node.js {@link Writable} as an XlsxSink. The actual\n * `writable.write` for each chunk is queued behind any pending `drain`, so\n * the writable's internal buffer never exceeds its `highWaterMark` regardless\n * of how fast the producer is. See {@link toFile} for the same caveat about\n * producer-side memory: the synchronous `write(chunk)` API does not let\n * backpressure flow back to the caller, so a tight non-yielding producer can\n * still let chunk references accumulate in the queue.\n *\n * `result()` returns the writable itself for downstream chaining.\n */\nexport function toWritable(writable: Writable): XlsxSink & { toBytes(): BufferedSinkWriter; result(): Writable } {\n if (!(writable instanceof Writable)) {\n throw new OpenXmlIoError('toWritable expects a Node Writable');\n }\n let finalised: Promise<Uint8Array> | undefined;\n let pendingError: Error | undefined;\n let writeQueue: Promise<void> = Promise.resolve();\n writable.on('error', (err) => {\n pendingError = err instanceof Error ? err : new Error(String(err));\n });\n\n return {\n toBytes(): BufferedSinkWriter {\n return {\n write(chunk: Uint8Array): void {\n if (finalised !== undefined) throw new OpenXmlIoError('toWritable sink: write after finish');\n if (!(chunk instanceof Uint8Array)) throw new OpenXmlIoError('toWritable sink: chunk is not a Uint8Array');\n if (pendingError) throw new OpenXmlIoError('toWritable sink: write error', { cause: pendingError });\n writeQueue = writeQueue.then(async () => {\n if (pendingError) return;\n const ok = writable.write(chunk);\n if (!ok) {\n await once(writable, 'drain');\n }\n });\n },\n async finish(): Promise<Uint8Array> {\n if (finalised) return finalised;\n finalised = (async () => {\n await writeQueue;\n await new Promise<void>((resolve, reject) => {\n writable.end((err?: Error | null) => (err ? reject(err) : resolve()));\n });\n if (pendingError) throw new OpenXmlIoError('toWritable sink: write error', { cause: pendingError });\n return EMPTY_BYTES;\n })();\n return finalised;\n },\n abort(cause?: unknown): void {\n if (finalised) return;\n finalised = Promise.resolve(EMPTY_BYTES);\n // Pass the cause to destroy() so downstream `error` listeners can\n // distinguish a deliberate abort from spontaneous fs/network errors.\n writable.destroy(cause instanceof Error ? cause : undefined);\n },\n };\n },\n result(): Writable {\n return writable;\n },\n };\n}\n","// Node-only counterpart to `workbookToBytes` in `./save`. Returns a Buffer\n// directly so Node consumers don't pay a `Buffer.from(uint8Array)` copy.\n\nimport type { Workbook } from '../workbook/workbook.js';\nimport { toBuffer } from './node.js';\nimport { saveWorkbook, type SaveOptions } from './save.js';\n\nexport async function workbookToBuffer(wb: Workbook, opts?: SaveOptions): Promise<Buffer> {\n const sink = toBuffer();\n await saveWorkbook(wb, sink, opts);\n return sink.result();\n}\n"],"mappings":";;;;;;;;;;;;AAiBA,SAAgB,WAAW,KAAsC;CAC/D,IAAI,EAAE,eAAe,aACnB,MAAM,IAAI,eAAe,2CAA2C;CAGtE,MAAM,QAAoB,eAAe,SAAS,IAAI,WAAW,IAAI,QAAQ,IAAI,YAAY,IAAI,UAAU,IAAI;CAC/G,OAAO;EACL,MAAM,UAAU;GACd,OAAO;EACT;EACA,WAAW;GACT,OAAO,IAAI,eAA2B,EACpC,MAAM,YAAY;IAChB,WAAW,QAAQ,KAAK;IACxB,WAAW,MAAM;GACnB,EACF,CAAC;EACH;CACF;AACF;;;;;;AAOA,SAAgB,WAA2E;CACzF,MAAM,SAAuB,CAAC;CAC9B,IAAI;CAEJ,MAAM,iBAA6B;EACjC,IAAI,cAAc,KAAA,GAAW,OAAO;EACpC,IAAI,QAAQ;EACZ,KAAK,MAAM,KAAK,QAAQ,SAAS,EAAE;EACnC,MAAM,MAAM,IAAI,WAAW,KAAK;EAChC,IAAI,MAAM;EACV,KAAK,MAAM,KAAK,QAAQ;GACtB,IAAI,IAAI,GAAG,GAAG;GACd,OAAO,EAAE;EACX;EACA,YAAY;EACZ,OAAO,SAAS;EAChB,OAAO;CACT;CAEA,OAAO;EACL,UAA8B;GAC5B,OAAO;IACL,MAAM,OAAyB;KAC7B,IAAI,cAAc,KAAA,GAChB,MAAM,IAAI,eAAe,mCAAmC;KAE9D,IAAI,EAAE,iBAAiB,aACrB,MAAM,IAAI,eAAe,0CAA0C;KAErE,OAAO,KAAK,KAAK;IACnB;IACA,MAAM,SAA8B;KAClC,OAAO,SAAS;IAClB;IACA,QAAc;KACZ,IAAI,cAAc,KAAA,GAAW;KAC7B,4BAAY,IAAI,WAAW,CAAC;KAC5B,OAAO,SAAS;IAClB;GACF;EACF;EACA,SAAiB;GACf,MAAM,QAAQ,SAAS;GACvB,OAAO,OAAO,KAAK,MAAM,QAAQ,MAAM,YAAY,MAAM,UAAU;EACrE;CACF;AACF;;;ACxEA,MAAM,8BAAc,IAAI,WAAW,CAAC;;;;;;;AAQpC,SAAgB,SAAS,MAA0B;CACjD,IAAI,OAAO,SAAS,YAAY,KAAK,WAAW,GAC9C,MAAM,IAAI,eAAe,0CAA0C;CAErE,OAAO;EACL,MAAM,UAAU;GACd,IAAI;IACF,OAAO,IAAI,WAAW,MAAM,SAAS,IAAI,CAAC;GAC5C,SAAS,OAAO;IACd,MAAM,IAAI,eAAe,6BAA6B,KAAK,IAAI,EAAE,MAAM,CAAC;GAC1E;EACF;EACA,WAAW;GACT,MAAM,aAAa,iBAAiB,IAAI;GACxC,OAAO,SAAS,MAAM,UAAU;EAClC;CACF;AACF;;;;;;;AAQA,SAAgB,aAAa,MAA0B;CACrD,IAAI,OAAO,SAAS,YAAY,KAAK,WAAW,GAC9C,MAAM,IAAI,eAAe,8CAA8C;CAEzE,IAAI;CACJ,IAAI;EACF,QAAQ,IAAI,WAAW,aAAa,IAAI,CAAC;CAC3C,SAAS,OAAO;EACd,MAAM,IAAI,eAAe,iCAAiC,KAAK,IAAI,EAAE,MAAM,CAAC;CAC9E;CACA,OAAO;EACL,MAAM,UAAU;GACd,OAAO;EACT;EACA,WAAW;GACT,OAAO,IAAI,eAA2B,EACpC,MAAM,YAAY;IAChB,WAAW,QAAQ,KAAK;IACxB,WAAW,MAAM;GACnB,EACF,CAAC;EACH;CACF;AACF;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,OAAO,MAA8E;CACnG,IAAI,OAAO,SAAS,YAAY,KAAK,WAAW,GAC9C,MAAM,IAAI,eAAe,wCAAwC;CAEnE,IAAI;CACJ,IAAI,gBAAgB;CACpB,IAAI;CACJ,IAAI;CAKJ,IAAI,aAA4B,QAAQ,QAAQ;CAEhD,MAAM,qBAAiD;EACrD,IAAI,CAAC,QAAQ;GACX,SAAS,kBAAkB,IAAI;GAC/B,gBAAgB;GAChB,OAAO,GAAG,UAAU,QAAQ;IAC1B,eAAe,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;GACnE,CAAC;EACH;EACA,OAAO;CACT;CAMA,MAAM,mBAAmB,YAA2B;EAClD,IAAI,CAAC,eAAe;EACpB,IAAI;GACF,MAAM,OAAO,IAAI;EACnB,QAAQ,CAER;CACF;CAEA,OAAO;EACL,UAA8B;GAC5B,OAAO;IACL,MAAM,OAAyB;KAC7B,IAAI,cAAc,KAAA,GAAW,MAAM,IAAI,eAAe,qCAAqC,KAAK,GAAG;KACnG,IAAI,EAAE,iBAAiB,aACrB,MAAM,IAAI,eAAe,4CAA4C,KAAK,GAAG;KAE/E,IAAI,cAAc,MAAM,IAAI,eAAe,gCAAgC,KAAK,IAAI,EAAE,OAAO,aAAa,CAAC;KAC3G,MAAM,IAAI,aAAa;KACvB,aAAa,WAAW,KAAK,YAAY;MAGvC,IAAI,cAAc;MAElB,IAAI,CADO,EAAE,MAAM,KACb,GAGJ,MAAM,KAAK,GAAG,OAAO;KAEzB,CAAC;IACH;IACA,MAAM,SAA8B;KAClC,IAAI,WAAW,OAAO;KACtB,aAAa,YAAY;MACvB,MAAM,IAAI,aAAa;MACvB,IAAI;OACF,MAAM;OACN,MAAM,IAAI,SAAe,SAAS,WAAW;QAC3C,EAAE,KAAK,QAAwB,MAAM,OAAO,GAAG,IAAI,QAAQ,CAAE;OAC/D,CAAC;OACD,IAAI,cACF,MAAM,IAAI,eAAe,gCAAgC,KAAK,IAAI,EAAE,OAAO,aAAa,CAAC;MAE7F,SAAS,KAAK;OACZ,MAAM,iBAAiB;OACvB,MAAM;MACR;MACA,OAAO;KACT,EAAA,CAAG;KACH,OAAO;IACT;IACA,QAAc;KAEZ,IAAI,WAAW;KACf,YAAY,QAAQ,QAAQ,WAAW;KACvC,IAAI,QAGF,OAAO,QAAQ;KAIjB,iBAAsB;IACxB;GACF;EACF;EACA,SAAiB;GACf,OAAO;EACT;CACF;AACF;;;;;;AAOA,SAAgB,aAAa,UAAgC;CAC3D,IAAI,EAAE,oBAAoB,WACxB,MAAM,IAAI,eAAe,sCAAsC;CAEjE,IAAI;CACJ,OAAO;EACL,MAAM,UAAU;GACd,IAAI,OAAO,OAAO;GAClB,SAAS,YAAY;IACnB,MAAM,SAAuB,CAAC;IAC9B,WAAW,MAAM,KAAK,UACpB,OAAO,KAAK,aAAa,aAAa,IAAI,IAAI,WAAW,CAAC,CAAC;IAE7D,IAAI,QAAQ;IACZ,KAAK,MAAM,KAAK,QAAQ,SAAS,EAAE;IACnC,MAAM,MAAM,IAAI,WAAW,KAAK;IAChC,IAAI,MAAM;IACV,KAAK,MAAM,KAAK,QAAQ;KACtB,IAAI,IAAI,GAAG,GAAG;KACd,OAAO,EAAE;IACX;IACA,OAAO;GACT,EAAA,CAAG;GACH,OAAO;EACT;EACA,WAAW;GACT,OAAO,SAAS,MAAM,QAAQ;EAChC;CACF;AACF;;;;;;;;;;;;AAaA,SAAgB,WAAW,UAAsF;CAC/G,IAAI,EAAE,oBAAoB,WACxB,MAAM,IAAI,eAAe,oCAAoC;CAE/D,IAAI;CACJ,IAAI;CACJ,IAAI,aAA4B,QAAQ,QAAQ;CAChD,SAAS,GAAG,UAAU,QAAQ;EAC5B,eAAe,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;CACnE,CAAC;CAED,OAAO;EACL,UAA8B;GAC5B,OAAO;IACL,MAAM,OAAyB;KAC7B,IAAI,cAAc,KAAA,GAAW,MAAM,IAAI,eAAe,qCAAqC;KAC3F,IAAI,EAAE,iBAAiB,aAAa,MAAM,IAAI,eAAe,4CAA4C;KACzG,IAAI,cAAc,MAAM,IAAI,eAAe,gCAAgC,EAAE,OAAO,aAAa,CAAC;KAClG,aAAa,WAAW,KAAK,YAAY;MACvC,IAAI,cAAc;MAElB,IAAI,CADO,SAAS,MAAM,KACpB,GACJ,MAAM,KAAK,UAAU,OAAO;KAEhC,CAAC;IACH;IACA,MAAM,SAA8B;KAClC,IAAI,WAAW,OAAO;KACtB,aAAa,YAAY;MACvB,MAAM;MACN,MAAM,IAAI,SAAe,SAAS,WAAW;OAC3C,SAAS,KAAK,QAAwB,MAAM,OAAO,GAAG,IAAI,QAAQ,CAAE;MACtE,CAAC;MACD,IAAI,cAAc,MAAM,IAAI,eAAe,gCAAgC,EAAE,OAAO,aAAa,CAAC;MAClG,OAAO;KACT,EAAA,CAAG;KACH,OAAO;IACT;IACA,MAAM,OAAuB;KAC3B,IAAI,WAAW;KACf,YAAY,QAAQ,QAAQ,WAAW;KAGvC,SAAS,QAAQ,iBAAiB,QAAQ,QAAQ,KAAA,CAAS;IAC7D;GACF;EACF;EACA,SAAmB;GACjB,OAAO;EACT;CACF;AACF;;;AC/RA,eAAsB,iBAAiB,IAAc,MAAqC;CACxF,MAAM,OAAO,SAAS;CACtB,MAAM,aAAa,IAAI,MAAM,IAAI;CACjC,OAAO,KAAK,OAAO;AACrB"}
1
+ {"version":3,"file":"node.mjs","names":[],"sources":["../src/io/node.ts","../src/io/node-fs.ts","../src/io/node-save.ts"],"sourcesContent":["// In-memory Node helpers.\n//\n// `fromBuffer` / `toBuffer` rely only on the global `Buffer` symbol — no\n// `node:*` imports — so they're safe to ship through the `@office-kit/xlsx/streaming`\n// browser-targeted entry too. Filesystem + Readable / Writable helpers live in\n// `./node-fs.ts` (re-exported via `@office-kit/xlsx/node`) where the `node:fs` /\n// `node:stream` imports stay out of the browser-safe surface.\n\nimport { OpenXmlIoError } from '../utils/exceptions.js';\nimport type { BufferedSinkWriter, XlsxSink } from './sink.js';\nimport type { XlsxSource } from './source.js';\n\n/**\n * Wrap a Buffer or Uint8Array as an XlsxSource. The underlying bytes are\n * referenced — no copy — so callers must not mutate them while the source is in\n * use.\n */\nexport function fromBuffer(buf: Buffer | Uint8Array): XlsxSource {\n if (!(buf instanceof Uint8Array)) {\n throw new OpenXmlIoError('fromBuffer expects a Buffer or Uint8Array');\n }\n // Buffer is a subclass of Uint8Array, so a single normalisation suffices.\n const bytes: Uint8Array = buf instanceof Buffer ? new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength) : buf;\n return {\n async toBytes() {\n return bytes;\n },\n toStream() {\n return new ReadableStream<Uint8Array>({\n start(controller) {\n controller.enqueue(bytes);\n controller.close();\n },\n });\n },\n };\n}\n\n/**\n * In-memory Buffer sink. The buffered path concatenates appended chunks into a\n * single allocation when {@link BufferedSinkWriter.finish} resolves; the\n * convenience `result()` returns it as a Node Buffer.\n */\nexport function toBuffer(): XlsxSink & { toBytes(): BufferedSinkWriter; result(): Buffer } {\n const chunks: Uint8Array[] = [];\n let finalised: Uint8Array | undefined;\n\n const finalise = (): Uint8Array => {\n if (finalised !== undefined) return finalised;\n let total = 0;\n for (const c of chunks) total += c.byteLength;\n const out = new Uint8Array(total);\n let off = 0;\n for (const c of chunks) {\n out.set(c, off);\n off += c.byteLength;\n }\n finalised = out;\n chunks.length = 0;\n return out;\n };\n\n return {\n toBytes(): BufferedSinkWriter {\n return {\n write(chunk: Uint8Array): void {\n if (finalised !== undefined) {\n throw new OpenXmlIoError('toBuffer sink: write after finish');\n }\n if (!(chunk instanceof Uint8Array)) {\n throw new OpenXmlIoError('toBuffer sink: chunk is not a Uint8Array');\n }\n chunks.push(chunk);\n },\n async finish(): Promise<Uint8Array> {\n return finalise();\n },\n abort(): void {\n if (finalised !== undefined) return;\n finalised = new Uint8Array(0);\n chunks.length = 0;\n },\n };\n },\n result(): Buffer {\n const bytes = finalise();\n return Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n },\n };\n}\n","// Node filesystem + Readable / Writable I/O helpers.\n//\n// Kept separate from `./node.ts` so the buffer-only entry stays free of\n// `node:fs` / `node:stream` imports — important for the `@office-kit/xlsx/streaming`\n// browser-targeted bundle, which can re-export `fromBuffer` / `toBuffer`\n// without dragging Node-only modules into the browser surface. Users who want\n// filesystem I/O reach this module directly (or through `@office-kit/xlsx/node` once\n// that subpath lands).\n\nimport { createReadStream, createWriteStream, readFileSync } from 'node:fs';\nimport { readFile, unlink } from 'node:fs/promises';\nimport { once } from 'node:events';\nimport { Readable, Writable } from 'node:stream';\nimport { OpenXmlIoError } from '../utils/exceptions.js';\nimport type { BufferedSinkWriter, XlsxSink } from './sink.js';\nimport type { XlsxSource } from './source.js';\n\nconst EMPTY_BYTES = new Uint8Array(0);\n\n/**\n * Wrap a filesystem path as an XlsxSource. `toBytes` reads the whole file into\n * memory; `toStream` opens a `fs.createReadStream` and bridges it to a Web\n * {@link ReadableStream} via `Readable.toWeb` so the ZIP reader can iterate\n * without loading the entire xlsx up front.\n */\nexport function fromFile(path: string): XlsxSource {\n if (typeof path !== 'string' || path.length === 0) {\n throw new OpenXmlIoError('fromFile expects a non-empty path string');\n }\n return {\n async toBytes() {\n try {\n return new Uint8Array(await readFile(path));\n } catch (cause) {\n throw new OpenXmlIoError(`fromFile: failed to read \"${path}\"`, { cause });\n }\n },\n toStream() {\n const nodeStream = createReadStream(path);\n return Readable.toWeb(nodeStream) as unknown as ReadableStream<Uint8Array>;\n },\n };\n}\n\n/**\n * Synchronous variant of {@link fromFile}. Convenience for tooling / scripts\n * where the cost of `await fs.readFile` outweighs the ergonomic gain. The\n * returned source's `toBytes` resolves immediately with the bytes already in\n * memory.\n */\nexport function fromFileSync(path: string): XlsxSource {\n if (typeof path !== 'string' || path.length === 0) {\n throw new OpenXmlIoError('fromFileSync expects a non-empty path string');\n }\n let bytes: Uint8Array;\n try {\n bytes = new Uint8Array(readFileSync(path));\n } catch (cause) {\n throw new OpenXmlIoError(`fromFileSync: failed to read \"${path}\"`, { cause });\n }\n return {\n async toBytes() {\n return bytes;\n },\n toStream() {\n return new ReadableStream<Uint8Array>({\n start(controller) {\n controller.enqueue(bytes);\n controller.close();\n },\n });\n },\n };\n}\n\n/**\n * Filesystem sink. Each `write(chunk)` call streams the bytes to disk via\n * `fs.createWriteStream`, honouring backpressure: the actual `writable.write`\n * for each chunk is queued behind any pending `drain`, so the writable's\n * internal buffer never grows past its `highWaterMark` (default 16 KB) no\n * matter how fast the producer hands chunks over.\n *\n * Note on the producer-side memory budget: the sink contract is\n * intentionally synchronous (`write(chunk): void`), so a producer that races\n * ahead without yielding will let chunk references pile up in the queue.\n * That keeps `writable`'s buffer bounded but does not bound the queue\n * itself. Producers that need a hard ceiling should yield between writes\n * (`await new Promise(setImmediate)` is enough) or use a sink with an async\n * write contract.\n *\n * `result()` returns the destination path; `finish()` resolves with an empty\n * `Uint8Array` once the stream has flushed. Callers that need the on-disk\n * bytes should `readFile()` the returned path themselves — re-reading inside\n * `finish()` would defeat the \"streamed to disk, never resident\" guarantee.\n */\nexport function toFile(path: string): XlsxSink & { toBytes(): BufferedSinkWriter; result(): string } {\n if (typeof path !== 'string' || path.length === 0) {\n throw new OpenXmlIoError('toFile expects a non-empty path string');\n }\n let stream: ReturnType<typeof createWriteStream> | undefined;\n let streamCreated = false;\n let finalised: Promise<Uint8Array> | undefined;\n let pendingError: Error | undefined;\n // Backpressure queue: every chunk's actual `writable.write` call is staged\n // behind the previous chunk's completion. When a write returns `false` the\n // queue parks on `drain` before the next chunk goes out, so the writable's\n // internal buffer stays within its highWaterMark.\n let writeQueue: Promise<void> = Promise.resolve();\n\n const ensureStream = (): NonNullable<typeof stream> => {\n if (!stream) {\n stream = createWriteStream(path);\n streamCreated = true;\n stream.on('error', (err) => {\n pendingError = err instanceof Error ? err : new Error(String(err));\n });\n }\n return stream;\n };\n\n // Best-effort: remove a half-written file when finish() fails so callers\n // don't mistake a corrupt artefact for a successful save. Swallows unlink\n // errors because we're already in a failure path and the original cause\n // is what the caller needs to see.\n const cleanupOnFailure = async (): Promise<void> => {\n if (!streamCreated) return;\n try {\n await unlink(path);\n } catch {\n // ignore — file may be gone, path may be on a read-only fs, etc.\n }\n };\n\n return {\n toBytes(): BufferedSinkWriter {\n return {\n write(chunk: Uint8Array): void {\n if (finalised !== undefined) throw new OpenXmlIoError(`toFile sink: write after finish (\"${path}\")`);\n if (!(chunk instanceof Uint8Array)) {\n throw new OpenXmlIoError(`toFile sink: chunk is not a Uint8Array (\"${path}\")`);\n }\n if (pendingError) throw new OpenXmlIoError(`toFile sink: write error on \"${path}\"`, { cause: pendingError });\n const s = ensureStream();\n writeQueue = writeQueue.then(async () => {\n // Skip remaining work once the stream has errored — the error\n // surfaces from `finish()` so callers see one consistent failure.\n if (pendingError) return;\n const ok = s.write(chunk);\n if (!ok) {\n // Writable's internal buffer is over highWaterMark; wait for it\n // to flush before the next queued chunk runs.\n await once(s, 'drain');\n }\n });\n },\n async finish(): Promise<Uint8Array> {\n if (finalised) return finalised;\n finalised = (async () => {\n const s = ensureStream();\n try {\n await writeQueue;\n await new Promise<void>((resolve, reject) => {\n s.end((err?: Error | null) => (err ? reject(err) : resolve()));\n });\n if (pendingError) {\n throw new OpenXmlIoError(`toFile sink: write error on \"${path}\"`, { cause: pendingError });\n }\n } catch (err) {\n await cleanupOnFailure();\n throw err;\n }\n return EMPTY_BYTES;\n })();\n return finalised;\n },\n async abort(): Promise<void> {\n // Idempotent: subsequent finish() / abort() calls become no-ops.\n if (finalised) return;\n finalised = Promise.resolve(EMPTY_BYTES);\n const s = stream;\n if (s && !s.closed) {\n // destroy() drops the pending buffer but only queues the fd close,\n // so unlinking straight after it races that close and fails with\n // EBUSY on Windows, leaving the partial file exactly where the\n // caller was told it would not be. Wait for 'close' first.\n await new Promise<void>((resolve) => {\n s.once('close', resolve);\n s.destroy();\n });\n }\n await cleanupOnFailure();\n },\n };\n },\n result(): string {\n return path;\n },\n };\n}\n\n/**\n * Wrap a Node.js {@link Readable} as an XlsxSource. `toBytes` consumes the\n * entire stream synchronously (collecting chunks); `toStream` bridges to a Web\n * ReadableStream via `Readable.toWeb` so the ZIP reader can pull chunks lazily.\n */\nexport function fromReadable(readable: Readable): XlsxSource {\n if (!(readable instanceof Readable)) {\n throw new OpenXmlIoError('fromReadable expects a Node Readable');\n }\n let bytes: Promise<Uint8Array> | undefined;\n return {\n async toBytes() {\n if (bytes) return bytes;\n bytes = (async () => {\n const chunks: Uint8Array[] = [];\n for await (const c of readable) {\n chunks.push(c instanceof Uint8Array ? c : new Uint8Array(c));\n }\n let total = 0;\n for (const c of chunks) total += c.byteLength;\n const out = new Uint8Array(total);\n let off = 0;\n for (const c of chunks) {\n out.set(c, off);\n off += c.byteLength;\n }\n return out;\n })();\n return bytes;\n },\n toStream() {\n return Readable.toWeb(readable) as unknown as ReadableStream<Uint8Array>;\n },\n };\n}\n\n/**\n * Wrap a Node.js {@link Writable} as an XlsxSink. The actual\n * `writable.write` for each chunk is queued behind any pending `drain`, so\n * the writable's internal buffer never exceeds its `highWaterMark` regardless\n * of how fast the producer is. See {@link toFile} for the same caveat about\n * producer-side memory: the synchronous `write(chunk)` API does not let\n * backpressure flow back to the caller, so a tight non-yielding producer can\n * still let chunk references accumulate in the queue.\n *\n * `result()` returns the writable itself for downstream chaining.\n */\nexport function toWritable(writable: Writable): XlsxSink & { toBytes(): BufferedSinkWriter; result(): Writable } {\n if (!(writable instanceof Writable)) {\n throw new OpenXmlIoError('toWritable expects a Node Writable');\n }\n let finalised: Promise<Uint8Array> | undefined;\n let pendingError: Error | undefined;\n let writeQueue: Promise<void> = Promise.resolve();\n writable.on('error', (err) => {\n pendingError = err instanceof Error ? err : new Error(String(err));\n });\n\n return {\n toBytes(): BufferedSinkWriter {\n return {\n write(chunk: Uint8Array): void {\n if (finalised !== undefined) throw new OpenXmlIoError('toWritable sink: write after finish');\n if (!(chunk instanceof Uint8Array)) throw new OpenXmlIoError('toWritable sink: chunk is not a Uint8Array');\n if (pendingError) throw new OpenXmlIoError('toWritable sink: write error', { cause: pendingError });\n writeQueue = writeQueue.then(async () => {\n if (pendingError) return;\n const ok = writable.write(chunk);\n if (!ok) {\n await once(writable, 'drain');\n }\n });\n },\n async finish(): Promise<Uint8Array> {\n if (finalised) return finalised;\n finalised = (async () => {\n await writeQueue;\n await new Promise<void>((resolve, reject) => {\n writable.end((err?: Error | null) => (err ? reject(err) : resolve()));\n });\n if (pendingError) throw new OpenXmlIoError('toWritable sink: write error', { cause: pendingError });\n return EMPTY_BYTES;\n })();\n return finalised;\n },\n abort(cause?: unknown): void {\n if (finalised) return;\n finalised = Promise.resolve(EMPTY_BYTES);\n // Pass the cause to destroy() so downstream `error` listeners can\n // distinguish a deliberate abort from spontaneous fs/network errors.\n writable.destroy(cause instanceof Error ? cause : undefined);\n },\n };\n },\n result(): Writable {\n return writable;\n },\n };\n}\n","// Node-only counterpart to `workbookToBytes` in `./save`. Returns a Buffer\n// directly so Node consumers don't pay a `Buffer.from(uint8Array)` copy.\n\nimport type { Workbook } from '../workbook/workbook.js';\nimport { toBuffer } from './node.js';\nimport { saveWorkbook, type SaveOptions } from './save.js';\n\nexport async function workbookToBuffer(wb: Workbook, opts?: SaveOptions): Promise<Buffer> {\n const sink = toBuffer();\n await saveWorkbook(wb, sink, opts);\n return sink.result();\n}\n"],"mappings":"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"}
@@ -29,7 +29,35 @@ function resolveDecompressionLimits(input) {
29
29
  function createBudget(limits) {
30
30
  return {
31
31
  limits,
32
- totalInflated: 0
32
+ totalInflated: 0,
33
+ chargedByPath: /* @__PURE__ */ new Map()
34
+ };
35
+ }
36
+ /**
37
+ * Start inflating `path`, returning the function every chunk of it must be
38
+ * recorded through. The recorder throws {@link OpenXmlDecompressionBombError}
39
+ * once the archive total is exceeded, naming `path`.
40
+ *
41
+ * The total bounds how much distinct payload an archive expands to, which is
42
+ * what `checkDeclaredTotals` enforces from the central directory by counting
43
+ * each entry once. An entry can be inflated more than once though: a
44
+ * `readStream` and a `read` of the same part, or a re-read of one the inflate
45
+ * cache turned away. So each path is charged its high-water mark rather than
46
+ * the sum of its reads, and a second inflate only adds what it pushes that mark
47
+ * past. Reads that overlap in any interleaving stay correct for the same
48
+ * reason. The per-entry cap is independent of this and is re-evaluated from
49
+ * scratch on every read.
50
+ */
51
+ function beginEntryInflate(budget, path) {
52
+ let inflated = 0;
53
+ return (bytes) => {
54
+ inflated += bytes;
55
+ const charged = budget.chargedByPath.get(path) ?? 0;
56
+ if (inflated > charged) {
57
+ budget.totalInflated += inflated - charged;
58
+ budget.chargedByPath.set(path, inflated);
59
+ }
60
+ if (budget.totalInflated > budget.limits.maxTotalUncompressedBytes) throw new OpenXmlDecompressionBombError(`openZip: archive-wide inflated size exceeded ${budget.limits.maxTotalUncompressedBytes} bytes while reading "${path}" (decompression-bomb guard).`);
33
61
  };
34
62
  }
35
63
  /**
@@ -53,19 +81,37 @@ function checkDeclaredTotals(budget, declaredEntries) {
53
81
  }
54
82
  if (declaredTotal > budget.limits.maxTotalUncompressedBytes) throw new OpenXmlDecompressionBombError(`openZip: declared total uncompressed size ${declaredTotal} bytes exceeds the ${budget.limits.maxTotalUncompressedBytes}-byte archive limit (decompression-bomb guard).`);
55
83
  }
56
- /**
57
- * Record `bytes` against the global budget. Throws when the running total
58
- * crosses {@link ResolvedDecompressionLimits.maxTotalUncompressedBytes}. Called
59
- * from both sync and streaming inflate code paths.
60
- */
61
- function recordInflated(budget, path, bytes) {
62
- budget.totalInflated += bytes;
63
- if (budget.totalInflated > budget.limits.maxTotalUncompressedBytes) throw new OpenXmlDecompressionBombError(`openZip: archive-wide inflated size exceeded ${budget.limits.maxTotalUncompressedBytes} bytes while reading "${path}" (decompression-bomb guard).`);
64
- }
65
84
  /** Build the message thrown when a single entry exceeds its cap mid-inflate. */
66
85
  function entryOverflowError(path, cap) {
67
86
  return new OpenXmlDecompressionBombError(`openZip: inflated size of "${path}" exceeded ${cap} bytes (decompression-bomb guard).`);
68
87
  }
88
+ function createInflateCache() {
89
+ const held = /* @__PURE__ */ new Map();
90
+ let total = 0;
91
+ return {
92
+ get(path) {
93
+ const hit = held.get(path);
94
+ if (!hit) return void 0;
95
+ held.delete(path);
96
+ held.set(path, hit);
97
+ return hit.slice();
98
+ },
99
+ set(path, bytes) {
100
+ if (bytes.byteLength > 65536) return;
101
+ total += bytes.byteLength - (held.get(path)?.byteLength ?? 0);
102
+ held.set(path, bytes.slice());
103
+ for (const [lru, entry] of held) {
104
+ if (total <= 4194304) break;
105
+ held.delete(lru);
106
+ total -= entry.byteLength;
107
+ }
108
+ },
109
+ clear() {
110
+ held.clear();
111
+ total = 0;
112
+ }
113
+ };
114
+ }
69
115
  //#endregion
70
116
  //#region src/zip/random-access-reader.ts
71
117
  /**
@@ -74,6 +120,11 @@ function entryOverflowError(path, cap) {
74
120
  * keeps peak transient memory bounded.
75
121
  */
76
122
  const INFLATE_CHUNK_BYTES = 64 * 1024;
123
+ /**
124
+ * `decode()` without `{ stream: true }` resets the decoder's state on every
125
+ * call, so one instance is safe to share across entries and across archives.
126
+ */
127
+ const CD_NAME_DECODER = new TextDecoder("utf-8");
77
128
  const singleChunkStream = (bytes) => new ReadableStream({ start(controller) {
78
129
  if (bytes.byteLength > 0) controller.enqueue(bytes);
79
130
  controller.close();
@@ -189,7 +240,7 @@ function parseCentralDirectory(b, cdOffset, expectedCount) {
189
240
  const commentLen = u16(b, p + 32);
190
241
  let lfhOffset = u32(b, p + 42);
191
242
  const nameBytes = b.subarray(p + 46, p + 46 + nameLen);
192
- const path = new TextDecoder("utf-8").decode(nameBytes);
243
+ const path = CD_NAME_DECODER.decode(nameBytes);
193
244
  const wantsUncomp = uncompSize === ZIP32_MAX_U32;
194
245
  const wantsComp = compSize === ZIP32_MAX_U32;
195
246
  const wantsOffset = lfhOffset === ZIP32_MAX_U32;
@@ -263,7 +314,7 @@ function openRandomAccessArchive(bytes, decompressionLimits) {
263
314
  for (const e of entries) byPath.set(e.path, e);
264
315
  const budget = resolvedLimits ? createBudget(resolvedLimits) : null;
265
316
  if (budget) checkDeclaredTotals(budget, entries);
266
- const inflateCache = /* @__PURE__ */ new Map();
317
+ const inflateCache = createInflateCache();
267
318
  let live = true;
268
319
  let archiveBytes = bytes;
269
320
  const ensureLive = () => {
@@ -281,7 +332,7 @@ function openRandomAccessArchive(bytes, decompressionLimits) {
281
332
  if (entry.compMethod === COMP_STORE) {
282
333
  if (budget) {
283
334
  if (compressed.byteLength > budget.limits.maxEntryUncompressedBytes) throw entryOverflowError(path, budget.limits.maxEntryUncompressedBytes);
284
- recordInflated(budget, path, compressed.byteLength);
335
+ beginEntryInflate(budget, path)(compressed.byteLength);
285
336
  }
286
337
  out = compressed.slice();
287
338
  } else if (entry.compMethod === COMP_DEFLATE) out = inflateBounded(path, compressed, budget);
@@ -299,12 +350,13 @@ function openRandomAccessArchive(bytes, decompressionLimits) {
299
350
  if (entry.compMethod === COMP_STORE) {
300
351
  if (budget) {
301
352
  if (compressed.byteLength > budget.limits.maxEntryUncompressedBytes) throw entryOverflowError(path, budget.limits.maxEntryUncompressedBytes);
302
- recordInflated(budget, path, compressed.byteLength);
353
+ beginEntryInflate(budget, path)(compressed.byteLength);
303
354
  }
304
355
  return singleChunkStream(compressed.slice());
305
356
  }
306
357
  if (entry.compMethod !== COMP_DEFLATE) throw new OpenXmlIoError(`openZip: unsupported compression method ${entry.compMethod} for "${path}"`);
307
358
  const entryCap = budget ? entryInflateCap(budget, compressed.byteLength) : Number.POSITIVE_INFINITY;
359
+ const record = budget ? beginEntryInflate(budget, path) : null;
308
360
  let entryEmitted = 0;
309
361
  const pending = [];
310
362
  let pushedOffset = 0;
@@ -318,8 +370,8 @@ function openRandomAccessArchive(bytes, decompressionLimits) {
318
370
  inflateError = entryOverflowError(path, entryCap);
319
371
  return;
320
372
  }
321
- if (budget) try {
322
- recordInflated(budget, path, chunk.byteLength);
373
+ if (record) try {
374
+ record(chunk.byteLength);
323
375
  } catch (err) {
324
376
  inflateError = err;
325
377
  return;
@@ -390,6 +442,7 @@ function openRandomAccessArchive(bytes, decompressionLimits) {
390
442
  live = false;
391
443
  archiveBytes = void 0;
392
444
  inflateCache.clear();
445
+ budget?.chargedByPath.clear();
393
446
  byPath.clear();
394
447
  }
395
448
  };
@@ -402,6 +455,7 @@ function openRandomAccessArchive(bytes, decompressionLimits) {
402
455
  */
403
456
  function inflateBounded(path, compressed, budget) {
404
457
  const cap = budget ? entryInflateCap(budget, compressed.byteLength) : Number.POSITIVE_INFINITY;
458
+ const record = budget ? beginEntryInflate(budget, path) : null;
405
459
  const acc = [];
406
460
  let emitted = 0;
407
461
  let aborted;
@@ -413,8 +467,8 @@ function inflateBounded(path, compressed, budget) {
413
467
  aborted = entryOverflowError(path, cap);
414
468
  return;
415
469
  }
416
- if (budget) try {
417
- recordInflated(budget, path, chunk.byteLength);
470
+ if (record) try {
471
+ record(chunk.byteLength);
418
472
  } catch (err) {
419
473
  aborted = err;
420
474
  return;
@@ -461,7 +515,7 @@ function openViaUnzipSync(bytes, limits) {
461
515
  const budget = createBudget(limits);
462
516
  for (const [path, payload] of Object.entries(entries)) {
463
517
  if (payload.byteLength > limits.maxEntryUncompressedBytes) throw new OpenXmlDecompressionBombError(`openZip: entry "${path}" inflated to ${payload.byteLength} bytes, exceeding the ${limits.maxEntryUncompressedBytes}-byte per-entry limit (decompression-bomb guard).`);
464
- recordInflated(budget, path, payload.byteLength);
518
+ beginEntryInflate(budget, path)(payload.byteLength);
465
519
  }
466
520
  }
467
521
  let live = true;
@@ -478,7 +532,7 @@ function openViaUnzipSync(bytes, limits) {
478
532
  if (!live || !entries) throw new OpenXmlIoError("openZip: archive is closed");
479
533
  const e = entries[path];
480
534
  if (!e) throw new OpenXmlIoError(`openZip: no entry at "${path}"`);
481
- return e;
535
+ return e.slice();
482
536
  },
483
537
  async readAsync(path) {
484
538
  return this.read(path);
@@ -513,11 +567,12 @@ const isCfbCompoundDocument = (bytes) => {
513
567
  /**
514
568
  * Open a zip archive from any {@link XlsxSource}. The source is fully
515
569
  * materialised in memory, the central directory is parsed once, and each
516
- * entry is inflated on demand by {@link openRandomAccessArchive} peak memory
517
- * stays at compressed-archive size plus per-entry inflate scratch rather than
518
- * holding every uncompressed entry resident. The fflate `unzipSync` fallback
519
- * is preserved internally for ZIP64 / non-standard archives the random-access
520
- * reader rejects.
570
+ * entry is inflated on demand by {@link openRandomAccessArchive}: peak memory
571
+ * stays at compressed-archive size, plus per-entry inflate scratch, plus a few
572
+ * MB at most of small entries kept for re-reads, rather than holding every
573
+ * uncompressed entry resident. The fflate `unzipSync` fallback is preserved
574
+ * internally for ZIP64 / non-standard archives the random-access reader
575
+ * rejects, and it does hold every entry inflated.
521
576
  */
522
577
  async function openZip(source, opts = {}) {
523
578
  let bytes;
@@ -532,4 +587,4 @@ async function openZip(source, opts = {}) {
532
587
  //#endregion
533
588
  export { DEFAULT_DECOMPRESSION_LIMITS as n, openZip as t };
534
589
 
535
- //# sourceMappingURL=reader-BNaUTVCy.mjs.map
590
+ //# sourceMappingURL=reader-DHMxLBQV.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reader-DHMxLBQV.mjs","names":[],"sources":["../src/zip/decompression-guard.ts","../src/zip/inflate-cache.ts","../src/zip/random-access-reader.ts","../src/zip/reader.ts"],"sourcesContent":["// Decompression-bomb safeguards for the ZIP reader.\n//\n// Zip-bombs encode pathological compression ratios (e.g. 100 B → 1 GB) so that\n// a naive `inflate` exhausts memory before the caller can react. The guards\n// here apply three orthogonal bounds:\n//\n// 1. Per-entry uncompressed cap — hard ceiling on a single payload.\n// 2. Total-archive uncompressed cap — hard ceiling summed across the archive.\n// 3. Per-entry compression ratio — bounds the *amplification factor* so that\n// even small compressed entries can't decompress into hundreds of MB.\n//\n// Defaults are sized to admit any legitimate xlsx (xml compresses well but\n// rarely exceeds the per-entry cap; office templates with embedded media stay\n// well under the total cap) while rejecting plausible bombs. Trusted callers\n// (e.g. a backend that only ever loads xlsx it generated itself) can pass\n// `false` to disable the guard.\n\nimport { OpenXmlDecompressionBombError, OpenXmlError } from '../utils/exceptions.js';\n\n/** Per-archive limits enforced during {@link openZip}. */\nexport interface DecompressionLimits {\n /**\n * Maximum decompressed bytes for a single archive entry. Default 512 MiB.\n * Legitimate xlsx sheets stay well below this even with millions of cells.\n */\n maxEntryUncompressedBytes?: number;\n /**\n * Maximum decompressed bytes summed across every entry the caller reads.\n * Default 1 GiB.\n */\n maxTotalUncompressedBytes?: number;\n /**\n * Maximum allowed `uncompressed / compressed` ratio for a single entry.\n * Default 1000. xml usually compresses 5–20×, but highly repetitive payloads\n * (long runs of zeros, sparse worksheets) can hit several hundred ×\n * legitimately. Classic zip-bombs run 10 000× and up, so a 1000× ceiling\n * still catches them while admitting realistic content. The implementation\n * treats compressed sizes below 64 B as exempt — there's no amplification\n * budget worth policing on such small entries, and the absolute per-entry /\n * archive limits already cover them.\n */\n maxCompressionRatio?: number;\n}\n\n/** Resolved limits with no `undefined` fields. */\nexport interface ResolvedDecompressionLimits {\n readonly maxEntryUncompressedBytes: number;\n readonly maxTotalUncompressedBytes: number;\n readonly maxCompressionRatio: number;\n}\n\n/** Default safeguards applied when the caller doesn't override them. */\nexport const DEFAULT_DECOMPRESSION_LIMITS: ResolvedDecompressionLimits = {\n maxEntryUncompressedBytes: 512 * 1024 * 1024,\n maxTotalUncompressedBytes: 1024 * 1024 * 1024,\n maxCompressionRatio: 1000,\n};\n\n/** Below this compressed size, the ratio check is skipped — see {@link DecompressionLimits}. */\nconst RATIO_CHECK_MIN_COMPRESSED_BYTES = 64;\n\n/**\n * `DecompressionLimits` plus the sentinel `false` to disable the guard.\n * Anything else is treated as \"use defaults\".\n */\nexport type DecompressionLimitsInput = DecompressionLimits | false | undefined;\n\n// Each limit must be a positive finite number to be meaningful; NaN / Infinity\n// / 0 / negatives silently turn `emitted > cap` and `totalInflated > cap` into\n// no-ops and disable the guard. Reject those at the boundary so the only way\n// the guard is off is the explicit `false` sentinel.\nconst requirePositiveFinite = (field: string, value: number): void => {\n if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {\n throw new OpenXmlError(\n `resolveDecompressionLimits: ${field} must be a positive finite number; got ${String(value)}`,\n );\n }\n};\n\n/** Returns null when the guard is disabled. */\nexport function resolveDecompressionLimits(\n input: DecompressionLimitsInput,\n): ResolvedDecompressionLimits | null {\n if (input === false) return null;\n if (!input) return DEFAULT_DECOMPRESSION_LIMITS;\n const resolved: ResolvedDecompressionLimits = {\n maxEntryUncompressedBytes:\n input.maxEntryUncompressedBytes ?? DEFAULT_DECOMPRESSION_LIMITS.maxEntryUncompressedBytes,\n maxTotalUncompressedBytes:\n input.maxTotalUncompressedBytes ?? DEFAULT_DECOMPRESSION_LIMITS.maxTotalUncompressedBytes,\n maxCompressionRatio:\n input.maxCompressionRatio ?? DEFAULT_DECOMPRESSION_LIMITS.maxCompressionRatio,\n };\n requirePositiveFinite('maxEntryUncompressedBytes', resolved.maxEntryUncompressedBytes);\n requirePositiveFinite('maxTotalUncompressedBytes', resolved.maxTotalUncompressedBytes);\n requirePositiveFinite('maxCompressionRatio', resolved.maxCompressionRatio);\n return resolved;\n}\n\n/**\n * Per-archive byte accounting shared across every read of an archive opened\n * with the given limits. Tracks total inflated bytes so the cap is enforced\n * even when individual entries stay below the per-entry ceiling.\n */\nexport interface DecompressionBudget {\n readonly limits: ResolvedDecompressionLimits;\n totalInflated: number;\n /**\n * Largest number of bytes any single inflate of an entry has charged to the\n * archive total, keyed by path. {@link beginEntryInflate} keeps\n * {@link DecompressionBudget.totalInflated} equal to the sum of these.\n */\n readonly chargedByPath: Map<string, number>;\n}\n\nexport function createBudget(limits: ResolvedDecompressionLimits): DecompressionBudget {\n return { limits, totalInflated: 0, chargedByPath: new Map() };\n}\n\n/**\n * Start inflating `path`, returning the function every chunk of it must be\n * recorded through. The recorder throws {@link OpenXmlDecompressionBombError}\n * once the archive total is exceeded, naming `path`.\n *\n * The total bounds how much distinct payload an archive expands to, which is\n * what `checkDeclaredTotals` enforces from the central directory by counting\n * each entry once. An entry can be inflated more than once though: a\n * `readStream` and a `read` of the same part, or a re-read of one the inflate\n * cache turned away. So each path is charged its high-water mark rather than\n * the sum of its reads, and a second inflate only adds what it pushes that mark\n * past. Reads that overlap in any interleaving stay correct for the same\n * reason. The per-entry cap is independent of this and is re-evaluated from\n * scratch on every read.\n */\nexport function beginEntryInflate(\n budget: DecompressionBudget,\n path: string,\n): (bytes: number) => void {\n let inflated = 0;\n return (bytes: number): void => {\n inflated += bytes;\n const charged = budget.chargedByPath.get(path) ?? 0;\n if (inflated > charged) {\n budget.totalInflated += inflated - charged;\n budget.chargedByPath.set(path, inflated);\n }\n // A failed inflate also advances the high-water mark. Check the total\n // even when this read adds nothing, or retrying a rejected entry bypasses\n // the archive limit using the charge left by the first attempt.\n if (budget.totalInflated > budget.limits.maxTotalUncompressedBytes) {\n throw new OpenXmlDecompressionBombError(\n `openZip: archive-wide inflated size exceeded ${budget.limits.maxTotalUncompressedBytes} bytes` +\n ` while reading \"${path}\" (decompression-bomb guard).`,\n );\n }\n };\n}\n\n/**\n * Per-entry inflate cap, accounting for both the absolute per-entry bound and\n * the ratio-based bound. Returns the smaller of the two — whichever fires\n * first stops the inflate loop.\n */\nexport function entryInflateCap(budget: DecompressionBudget, compressedSize: number): number {\n const { maxEntryUncompressedBytes, maxCompressionRatio } = budget.limits;\n if (compressedSize < RATIO_CHECK_MIN_COMPRESSED_BYTES) {\n return maxEntryUncompressedBytes;\n }\n const ratioCap = compressedSize * maxCompressionRatio;\n return Math.min(maxEntryUncompressedBytes, ratioCap);\n}\n\n/** Throw if the declared central-directory totals already exceed the limits. */\nexport function checkDeclaredTotals(\n budget: DecompressionBudget,\n declaredEntries: ReadonlyArray<{ path: string; compSize: number; uncompSize: number }>,\n): void {\n let declaredTotal = 0;\n for (const entry of declaredEntries) {\n declaredTotal += entry.uncompSize;\n if (entry.uncompSize > budget.limits.maxEntryUncompressedBytes) {\n throw new OpenXmlDecompressionBombError(\n `openZip: entry \"${entry.path}\" declares ${entry.uncompSize} uncompressed bytes,` +\n ` exceeding the ${budget.limits.maxEntryUncompressedBytes}-byte per-entry limit` +\n ` (decompression-bomb guard).`,\n );\n }\n if (\n entry.compSize >= RATIO_CHECK_MIN_COMPRESSED_BYTES &&\n entry.uncompSize > entry.compSize * budget.limits.maxCompressionRatio\n ) {\n throw new OpenXmlDecompressionBombError(\n `openZip: entry \"${entry.path}\" declares ratio ${(entry.uncompSize / entry.compSize).toFixed(1)}x` +\n ` (${entry.uncompSize}/${entry.compSize}), exceeding the ${budget.limits.maxCompressionRatio}x` +\n ` per-entry limit (decompression-bomb guard).`,\n );\n }\n }\n if (declaredTotal > budget.limits.maxTotalUncompressedBytes) {\n throw new OpenXmlDecompressionBombError(\n `openZip: declared total uncompressed size ${declaredTotal} bytes exceeds the` +\n ` ${budget.limits.maxTotalUncompressedBytes}-byte archive limit (decompression-bomb guard).`,\n );\n }\n}\n\n/** Build the message thrown when a single entry exceeds its cap mid-inflate. */\nexport function entryOverflowError(path: string, cap: number): OpenXmlDecompressionBombError {\n return new OpenXmlDecompressionBombError(\n `openZip: inflated size of \"${path}\" exceeded ${cap} bytes (decompression-bomb guard).`,\n );\n}\n","// Bounded cache of inflated ZIP entries.\n//\n// A load comes back to a handful of parts more than once (the `.rels` files the\n// relationship walk revisits, above all), and inflating those again is waste.\n// Keeping every entry is worse than that waste: it puts the whole uncompressed\n// package back in memory, which is the cost the random-access reader exists to\n// avoid. So the cache takes small entries only, holds a few MB of them at most,\n// and drops the least recently used first. Anything it turns away or evicts\n// inflates again on the next read.\n\n/**\n * Largest entry the cache accepts. Sized for the parts a load revisits, which\n * are small; a sheet or a media blob is normally read once, so keeping one buys\n * nothing and costs its full inflated size for the life of the archive.\n */\nexport const CACHE_MAX_ENTRY_BYTES = 64 * 1024;\n\n/**\n * Ceiling on everything the cache holds, so an archive with thousands of small\n * parts cannot accumulate without limit.\n */\nexport const CACHE_MAX_TOTAL_BYTES = 4 * 1024 * 1024;\n\nexport interface InflateCache {\n /**\n * Cached bytes for `path`, or `undefined`. The caller owns the array it gets\n * back and may mutate it. A hit counts as a use for eviction order.\n */\n get(path: string): Uint8Array | undefined;\n /** Offer `bytes` for later re-reads. Entries above the size ceiling are ignored. */\n set(path: string, bytes: Uint8Array): void;\n clear(): void;\n}\n\nexport function createInflateCache(): InflateCache {\n // Insertion-ordered, so the first key `keys()` yields is the least recently\n // used for as long as every hit re-inserts.\n const held = new Map<string, Uint8Array>();\n let total = 0;\n\n return {\n get(path) {\n const hit = held.get(path);\n if (!hit) return undefined;\n // `set` on an existing key leaves it where it was, so a hit has to delete\n // first. Without this the entries being re-read are the first evicted.\n held.delete(path);\n held.set(path, hit);\n return hit.slice();\n },\n set(path, bytes) {\n if (bytes.byteLength > CACHE_MAX_ENTRY_BYTES) return;\n total += bytes.byteLength - (held.get(path)?.byteLength ?? 0);\n // Hold a copy, so the array the caller was handed stays theirs to mutate.\n held.set(path, bytes.slice());\n for (const [lru, entry] of held) {\n if (total <= CACHE_MAX_TOTAL_BYTES) break;\n held.delete(lru);\n total -= entry.byteLength;\n }\n },\n clear() {\n held.clear();\n total = 0;\n },\n };\n}\n","// Random-access ZIP reader.\n//\n// Handing every entry to `fflate.unzipSync` materialises every uncompressed\n// payload at once, so a 100 MB xlsx with 500 MB of decompressed sheet data\n// spikes the resident set accordingly. This reader keeps only the compressed\n// archive bytes resident, parses the central directory once (cheap, about 46 B\n// per entry plus the filename), and inflates each entry lazily on `read(path)`.\n//\n// Small inflated payloads are kept for re-reads (see ./inflate-cache.ts).\n// Keeping every one of them would put the whole uncompressed package back in\n// memory, which is the cost this reader exists to avoid, so that cache is\n// bounded in both entry size and total and anything outside those bounds\n// inflates again on the next read.\n//\n// Limitations:\n// - ZIP64 reads only when the standard ZIP32 fields fit. EOCD with\n// sentinel values (0xFFFF / 0xFFFFFFFF) falls back to fflate's `unzipSync` so\n// external ZIP64 archives still load (the writer side has its own ZIP32 cap\n// guard).\n// - Compression methods: STORE (0) and DEFLATE (8). Anything else\n// throws OpenXmlIoError.\n\nimport { Inflate, unzipSync } from 'fflate';\nimport { OpenXmlDecompressionBombError, OpenXmlIoError } from '../utils/exceptions.js';\nimport {\n beginEntryInflate,\n checkDeclaredTotals,\n createBudget,\n type DecompressionBudget,\n type DecompressionLimitsInput,\n entryInflateCap,\n entryOverflowError,\n resolveDecompressionLimits,\n} from './decompression-guard.js';\nimport { createInflateCache } from './inflate-cache.js';\nimport type { ZipArchive } from './reader.js';\n\n/**\n * Chunk size used when feeding compressed bytes into fflate's `Inflate` for\n * streaming reads. 64 KB matches the saxes/SAX consumer's typical batch and\n * keeps peak transient memory bounded.\n */\nconst INFLATE_CHUNK_BYTES = 64 * 1024;\n\n/**\n * `decode()` without `{ stream: true }` resets the decoder's state on every\n * call, so one instance is safe to share across entries and across archives.\n */\nconst CD_NAME_DECODER = new TextDecoder('utf-8');\n\nconst singleChunkStream = (bytes: Uint8Array): ReadableStream<Uint8Array> =>\n new ReadableStream<Uint8Array>({\n start(controller) {\n if (bytes.byteLength > 0) controller.enqueue(bytes);\n controller.close();\n },\n });\n\nconst SIG_EOCD = 0x06054b50;\nconst SIG_CD = 0x02014b50;\nconst SIG_LFH = 0x04034b50;\nconst SIG_ZIP64_EOCD = 0x06064b50;\nconst SIG_ZIP64_EOCD_LOCATOR = 0x07064b50;\nconst ZIP32_MAX_U16 = 0xffff;\nconst ZIP32_MAX_U32 = 0xffffffff;\nconst ZIP64_LOCATOR_SIZE = 20;\nconst ZIP64_EXTRA_HEADER_ID = 0x0001;\nconst COMP_STORE = 0;\nconst COMP_DEFLATE = 8;\n\ninterface CdEntry {\n path: string;\n lfhOffset: number;\n compMethod: number;\n compSize: number;\n uncompSize: number;\n /** General-purpose bit flag, used to detect bit-3 (data descriptor) and bit-11 (UTF-8). */\n gpFlag: number;\n}\n\nconst u16 = (b: Uint8Array, off: number): number => (b[off] ?? 0) | ((b[off + 1] ?? 0) << 8);\nconst u32 = (b: Uint8Array, off: number): number => {\n const v0 = b[off] ?? 0;\n const v1 = b[off + 1] ?? 0;\n const v2 = b[off + 2] ?? 0;\n const v3 = b[off + 3] ?? 0;\n return (v0 | (v1 << 8) | (v2 << 16) | (v3 << 24)) >>> 0;\n};\n\n// Read a little-endian 64-bit value as a JS Number. Safe for values up to\n// 2^53-1 (Number.MAX_SAFE_INTEGER ~ 9 PiB), which is well past any realistic\n// xlsx archive — we throw if a parsed value exceeds the safe-integer range.\nconst u64 = (b: Uint8Array, off: number): number => {\n const lo = u32(b, off);\n const hi = u32(b, off + 4);\n if (hi > 0x1fffff) {\n throw new OpenXmlIoError(\n `openZip: ZIP64 field at byte ${off} exceeds the safe-integer range (${hi}*2^32 + ${lo})`,\n );\n }\n return hi * 0x100000000 + lo;\n};\n\n/** Find the End-of-Central-Directory record by scanning backwards from EOF. */\nfunction findEocd(b: Uint8Array): number {\n const minStart = Math.max(0, b.length - 22 - 0xffff);\n for (let i = b.length - 22; i >= minStart; i--) {\n if (u32(b, i) === SIG_EOCD) return i;\n }\n throw new OpenXmlIoError('openZip: no End-of-Central-Directory signature found');\n}\n\ninterface CdSummary {\n totalEntries: number;\n cdSize: number;\n cdOffset: number;\n}\n\n/**\n * Resolve the central-directory totals. When the EOCD carries ZIP64 sentinel\n * values (0xFFFF / 0xFFFFFFFF) the real totals live in a ZIP64 EOCD record\n * located via the ZIP64 EOCD locator just before the regular EOCD. ECMA-376\n * xlsx archives stay in ZIP32 territory; the ZIP64 path exists so the\n * decompression-bomb guards still apply to large external archives instead of\n * falling all the way back to `unzipSync` (which produces every entry's bytes\n * before any cap can fire).\n */\nfunction readCdSummary(b: Uint8Array, eocdOff: number): CdSummary {\n let totalEntries = u16(b, eocdOff + 10);\n let cdSize = u32(b, eocdOff + 12);\n let cdOffset = u32(b, eocdOff + 16);\n\n const usesZip64Eocd =\n totalEntries === ZIP32_MAX_U16 || cdSize === ZIP32_MAX_U32 || cdOffset === ZIP32_MAX_U32;\n if (!usesZip64Eocd) {\n return { totalEntries, cdSize, cdOffset };\n }\n\n // Locate the ZIP64 EOCD locator. It sits immediately before the regular\n // EOCD when one is present.\n const locatorOff = eocdOff - ZIP64_LOCATOR_SIZE;\n if (locatorOff < 0 || u32(b, locatorOff) !== SIG_ZIP64_EOCD_LOCATOR) {\n throw new OpenXmlIoError('openZip: ZIP64 EOCD locator missing despite EOCD sentinel values');\n }\n const zip64EocdOff = u64(b, locatorOff + 8);\n if (zip64EocdOff < 0 || zip64EocdOff + 56 > b.length) {\n throw new OpenXmlIoError(`openZip: ZIP64 EOCD offset ${zip64EocdOff} out of bounds`);\n }\n if (u32(b, zip64EocdOff) !== SIG_ZIP64_EOCD) {\n throw new OpenXmlIoError(`openZip: ZIP64 EOCD signature missing at byte ${zip64EocdOff}`);\n }\n totalEntries = u64(b, zip64EocdOff + 32);\n cdSize = u64(b, zip64EocdOff + 40);\n cdOffset = u64(b, zip64EocdOff + 48);\n return { totalEntries, cdSize, cdOffset };\n}\n\n/** Walk the ZIP64 Extended Information extra field for sentinel-valued sizes/offset. */\nfunction readZip64Extra(\n b: Uint8Array,\n extraStart: number,\n extraLen: number,\n wantsUncompSize: boolean,\n wantsCompSize: boolean,\n wantsLfhOffset: boolean,\n entryPath: string,\n): { uncompSize?: number; compSize?: number; lfhOffset?: number } {\n let p = extraStart;\n const end = extraStart + extraLen;\n while (p + 4 <= end) {\n const id = u16(b, p);\n const size = u16(b, p + 2);\n const dataStart = p + 4;\n const next = dataStart + size;\n if (next > end) break;\n if (id === ZIP64_EXTRA_HEADER_ID) {\n // Sanity-check that the declared extra-field size actually covers every\n // u64 the CD asked us to read. A truncated / mis-sized ZIP64 extra\n // field would otherwise let u64() interpret bytes belonging to the\n // next extra record (or the next CD entry) as a size/offset, producing\n // bogus values that downstream bomb checks then trust. Fail closed.\n const needed = (wantsUncompSize ? 8 : 0) + (wantsCompSize ? 8 : 0) + (wantsLfhOffset ? 8 : 0);\n if (size < needed) {\n throw new OpenXmlIoError(\n `openZip: ZIP64 extended-info field for \"${entryPath}\" declares ${size} bytes` +\n ` but the central directory sentinels require ${needed}`,\n );\n }\n let q = dataStart;\n const result: { uncompSize?: number; compSize?: number; lfhOffset?: number } = {};\n if (wantsUncompSize) {\n result.uncompSize = u64(b, q);\n q += 8;\n }\n if (wantsCompSize) {\n result.compSize = u64(b, q);\n q += 8;\n }\n if (wantsLfhOffset) {\n result.lfhOffset = u64(b, q);\n q += 8;\n }\n return result;\n }\n p = next;\n }\n return {};\n}\n\n/** Parse the central directory into an array of entry descriptors. */\nfunction parseCentralDirectory(b: Uint8Array, cdOffset: number, expectedCount: number): CdEntry[] {\n const entries: CdEntry[] = [];\n let p = cdOffset;\n for (let i = 0; i < expectedCount; i++) {\n if (u32(b, p) !== SIG_CD) {\n throw new OpenXmlIoError(`openZip: malformed central directory at byte ${p}`);\n }\n const gpFlag = u16(b, p + 8);\n const compMethod = u16(b, p + 10);\n let compSize = u32(b, p + 20);\n let uncompSize = u32(b, p + 24);\n const nameLen = u16(b, p + 28);\n const extraLen = u16(b, p + 30);\n const commentLen = u16(b, p + 32);\n let lfhOffset = u32(b, p + 42);\n const nameBytes = b.subarray(p + 46, p + 46 + nameLen);\n // Bit 11 (0x0800) signals UTF-8 filename. xlsx archives are almost always\n // UTF-8 already; treat bit-0 as UTF-8 too since CP437 ⊃ ASCII and xlsx uses\n // ASCII paths.\n const path = CD_NAME_DECODER.decode(nameBytes);\n\n // ZIP64 Extended Information rewrites whichever of {uncompSize, compSize,\n // lfhOffset} are 0xFFFFFFFF sentinels in the canonical fields. The extra\n // field stores them in the order listed in the spec (and omits any that\n // weren't sentinels), so we have to track which slots to read.\n const wantsUncomp = uncompSize === ZIP32_MAX_U32;\n const wantsComp = compSize === ZIP32_MAX_U32;\n const wantsOffset = lfhOffset === ZIP32_MAX_U32;\n if (wantsUncomp || wantsComp || wantsOffset) {\n const extra = readZip64Extra(b, p + 46 + nameLen, extraLen, wantsUncomp, wantsComp, wantsOffset, path);\n if (extra.uncompSize !== undefined) uncompSize = extra.uncompSize;\n if (extra.compSize !== undefined) compSize = extra.compSize;\n if (extra.lfhOffset !== undefined) lfhOffset = extra.lfhOffset;\n }\n entries.push({ path, lfhOffset, compMethod, compSize, uncompSize, gpFlag });\n p += 46 + nameLen + extraLen + commentLen;\n }\n return entries;\n}\n\n/** Read the compressed bytes for a CD entry by walking its local file header. */\nfunction readCompressedBytes(b: Uint8Array, entry: CdEntry): Uint8Array {\n if (u32(b, entry.lfhOffset) !== SIG_LFH) {\n throw new OpenXmlIoError(`openZip: malformed local file header for \"${entry.path}\"`);\n }\n const nameLen = u16(b, entry.lfhOffset + 26);\n const extraLen = u16(b, entry.lfhOffset + 28);\n const dataStart = entry.lfhOffset + 30 + nameLen + extraLen;\n return b.subarray(dataStart, dataStart + entry.compSize);\n}\n\n/**\n * Open a buffered xlsx archive in random-access mode. The archive bytes stay\n * resident; entries inflate on demand inside `read(path)`.\n *\n * Falls back to `fflate.unzipSync` when the central directory uses ZIP64\n * sentinel values (entry count == 0xFFFF or any size field == 0xFFFFFFFF) so\n * external ZIP64 archives still load. xlsx files in the wild fit comfortably in\n * ZIP32; the fallback exists for safety.\n *\n * `decompressionLimits` opts the archive into the zip-bomb safeguards\n * documented on {@link DecompressionLimits}; pass `false` to disable. Defaults\n * fit any legitimate xlsx.\n */\nexport function openRandomAccessArchive(\n bytes: Uint8Array,\n decompressionLimits?: DecompressionLimitsInput,\n): ZipArchive {\n // Quick sanity on min archive size.\n if (bytes.length < 22) {\n throw new OpenXmlIoError('openZip: archive is shorter than the minimum EOCD size (22 bytes)');\n }\n\n let eocdOff: number;\n try {\n eocdOff = findEocd(bytes);\n } catch (cause) {\n throw new OpenXmlIoError('openZip: archive is not a valid zip', { cause });\n }\n\n const resolvedLimits = resolveDecompressionLimits(decompressionLimits);\n\n // Detect ZIP64 up front so a malformed ZIP64 record fails closed instead of\n // falling back to `unzipSync`. The fallback inflates every entry before any\n // bomb cap runs; the whole point of the ZIP64 path is to keep the streaming\n // caps in play, so we must not silently downgrade when ZIP64 is in use.\n const eocdTotalEntries = u16(bytes, eocdOff + 10);\n const eocdCdSize = u32(bytes, eocdOff + 12);\n const eocdCdOffset = u32(bytes, eocdOff + 16);\n const claimsZip64 =\n eocdTotalEntries === ZIP32_MAX_U16 ||\n eocdCdSize === ZIP32_MAX_U32 ||\n eocdCdOffset === ZIP32_MAX_U32;\n\n let summary: CdSummary;\n try {\n summary = readCdSummary(bytes, eocdOff);\n } catch (cause) {\n if (claimsZip64) {\n // ZIP64 sentinels are set but the matching record / locator is missing\n // or invalid. Fail closed — `unzipSync` would inflate every entry.\n throw cause instanceof OpenXmlIoError\n ? cause\n : new OpenXmlIoError('openZip: ZIP64 central directory is malformed', { cause });\n }\n // Plain ZIP32 with an unparseable EOCD — fflate's `unzipSync` is more\n // tolerant of quirky archives; the post-hoc bomb check still applies.\n return openViaUnzipSync(bytes, resolvedLimits);\n }\n\n let entries: CdEntry[];\n try {\n entries = parseCentralDirectory(bytes, summary.cdOffset, summary.totalEntries);\n } catch (cause) {\n if (claimsZip64) {\n throw cause instanceof OpenXmlIoError\n ? cause\n : new OpenXmlIoError('openZip: ZIP64 central directory is malformed', { cause });\n }\n // Malformed ZIP32 CD — fall back to fflate which is more tolerant.\n return openViaUnzipSync(bytes, resolvedLimits);\n }\n\n const byPath = new Map<string, CdEntry>();\n for (const e of entries) byPath.set(e.path, e);\n\n const budget: DecompressionBudget | null = resolvedLimits ? createBudget(resolvedLimits) : null;\n if (budget) {\n // Declared CD totals are cheap to inspect — reject obvious bombs before\n // wiring up the inflate state machine.\n checkDeclaredTotals(budget, entries);\n }\n\n // Re-read cache for small entries, so the `.rels` parts the passthrough walk\n // revisits do not inflate twice.\n const inflateCache = createInflateCache();\n let live = true;\n let archiveBytes: Uint8Array | undefined = bytes;\n\n const ensureLive = (): Uint8Array => {\n if (!live || !archiveBytes) {\n throw new OpenXmlIoError('openZip: archive is closed');\n }\n return archiveBytes;\n };\n\n const readEntry = (path: string): Uint8Array => {\n const buf = ensureLive();\n const cached = inflateCache.get(path);\n if (cached) return cached;\n const entry = byPath.get(path);\n if (!entry) {\n throw new OpenXmlIoError(`openZip: no entry at \"${path}\"`);\n }\n const compressed = readCompressedBytes(buf, entry);\n let out: Uint8Array;\n if (entry.compMethod === COMP_STORE) {\n // STORE has ratio 1, so the only thing left to check is the absolute\n // size cap (and the running archive total).\n if (budget) {\n if (compressed.byteLength > budget.limits.maxEntryUncompressedBytes) {\n throw entryOverflowError(path, budget.limits.maxEntryUncompressedBytes);\n }\n const record = beginEntryInflate(budget, path);\n record(compressed.byteLength);\n }\n // Copy so callers can safely mutate the returned bytes without perturbing\n // the underlying archive view.\n out = compressed.slice();\n } else if (entry.compMethod === COMP_DEFLATE) {\n out = inflateBounded(path, compressed, budget);\n } else {\n throw new OpenXmlIoError(`openZip: unsupported compression method ${entry.compMethod} for \"${path}\"`);\n }\n inflateCache.set(path, out);\n return out;\n };\n\n const readEntryStream = (path: string): ReadableStream<Uint8Array> => {\n const buf = ensureLive();\n const cached = inflateCache.get(path);\n if (cached) return singleChunkStream(cached);\n const entry = byPath.get(path);\n if (!entry) {\n throw new OpenXmlIoError(`openZip: no entry at \"${path}\"`);\n }\n const compressed = readCompressedBytes(buf, entry);\n if (entry.compMethod === COMP_STORE) {\n // STORE means the bytes on disk are the bytes the caller wants; no need\n // to involve the inflate state machine. Run the same caps the sync\n // `read()` STORE branch applies — otherwise an uncompressed bomb entry\n // reached via `readStream()` would bypass the per-entry / archive-total\n // accounting. Ratio is fixed at 1, so only the absolute bounds apply.\n if (budget) {\n if (compressed.byteLength > budget.limits.maxEntryUncompressedBytes) {\n throw entryOverflowError(path, budget.limits.maxEntryUncompressedBytes);\n }\n const record = beginEntryInflate(budget, path);\n record(compressed.byteLength);\n }\n // Copy because callers may mutate the returned bytes.\n return singleChunkStream(compressed.slice());\n }\n if (entry.compMethod !== COMP_DEFLATE) {\n throw new OpenXmlIoError(`openZip: unsupported compression method ${entry.compMethod} for \"${path}\"`);\n }\n // DEFLATE: drive fflate's `Inflate` from `pull()` so the consumer's\n // demand controls how much we inflate. Each `pull()` either emits one\n // already-buffered inflated chunk or pushes one more block of compressed\n // input — never both — so the ReadableStream internal queue stays at\n // depth 1 and the producer can't race ahead of the consumer.\n const entryCap = budget ? entryInflateCap(budget, compressed.byteLength) : Number.POSITIVE_INFINITY;\n const record = budget ? beginEntryInflate(budget, path) : null;\n let entryEmitted = 0;\n const pending: Uint8Array[] = [];\n let pushedOffset = 0;\n let inflaterFinal = false;\n let inflateError: Error | undefined;\n const inflater = new Inflate((chunk, final) => {\n if (inflateError) return;\n if (chunk.byteLength > 0) {\n entryEmitted += chunk.byteLength;\n if (entryEmitted > entryCap) {\n inflateError = entryOverflowError(path, entryCap);\n return;\n }\n if (record) {\n try {\n record(chunk.byteLength);\n } catch (err) {\n inflateError = err as Error;\n return;\n }\n }\n pending.push(chunk);\n }\n if (final) inflaterFinal = true;\n });\n return new ReadableStream<Uint8Array>({\n pull(controller) {\n if (inflateError) {\n controller.error(inflateError);\n return;\n }\n // Emit at most one already-buffered inflated chunk per pull; subsequent\n // pulls drain the rest. This caps the stream's internal queue at one\n // chunk regardless of how many ondata callbacks fflate fired off the\n // most recent push.\n const buffered = pending.shift();\n if (buffered) {\n controller.enqueue(buffered);\n if (inflaterFinal && pending.length === 0 && pushedOffset >= compressed.byteLength) {\n controller.close();\n }\n return;\n }\n // No buffered output: push one block of compressed input and let\n // inflate's ondata fill `pending`. We stop pushing the moment we have\n // something to emit so the next pull can return it without racing\n // further inflation. `inflaterFinal` is set inside the ondata callback\n // during the same `push` call that sets `isLast`, so reaching\n // `pushedOffset >= compressed.byteLength` always ends the loop too.\n while (pending.length === 0 && pushedOffset < compressed.byteLength) {\n const end = Math.min(pushedOffset + INFLATE_CHUNK_BYTES, compressed.byteLength);\n const slice = compressed.subarray(pushedOffset, end);\n const isLast = end >= compressed.byteLength;\n try {\n inflater.push(slice, isLast);\n } catch (cause) {\n if (!inflateError) {\n inflateError = new OpenXmlIoError(`openZip: failed to inflate \"${path}\"`, { cause });\n }\n controller.error(inflateError);\n return;\n }\n // The ondata callback may have set inflateError (decompression-bomb\n // guard tripping mid-inflate). Surface it to the consumer before\n // continuing — otherwise we'd loop forever on a CD-lying bomb whose\n // chunks all land beyond the cap and never make it into `pending`.\n if (inflateError) {\n controller.error(inflateError);\n return;\n }\n pushedOffset = end;\n }\n const next = pending.shift();\n if (next) {\n controller.enqueue(next);\n }\n if (inflaterFinal && pending.length === 0 && pushedOffset >= compressed.byteLength) {\n controller.close();\n }\n },\n cancel() {\n // Consumer abandoned the stream early — drop buffered chunks and\n // advance the cursor past the end so the inflater and the compressed\n // slice are eligible for GC. fflate's `Inflate` has no terminate API\n // but losing the only reference is enough.\n pending.length = 0;\n pushedOffset = compressed.byteLength;\n inflaterFinal = true;\n },\n });\n };\n\n return {\n list(): string[] {\n ensureLive();\n return [...byPath.keys()].sort();\n },\n has(path: string): boolean {\n if (!live) return false;\n return byPath.has(path);\n },\n read(path: string): Uint8Array {\n return readEntry(path);\n },\n async readAsync(path: string): Promise<Uint8Array> {\n return readEntry(path);\n },\n readStream(path: string): ReadableStream<Uint8Array> {\n return readEntryStream(path);\n },\n close(): void {\n live = false;\n archiveBytes = undefined;\n inflateCache.clear();\n budget?.chargedByPath.clear();\n byPath.clear();\n },\n };\n}\n\n/**\n * Inflate `compressed` into a single `Uint8Array`, abort if it crosses the\n * configured per-entry cap or the archive-wide budget. Uses fflate's streaming\n * `Inflate` so the abort can fire on any internal block boundary rather than\n * after fflate has materialised the entire payload.\n */\nfunction inflateBounded(\n path: string,\n compressed: Uint8Array,\n budget: DecompressionBudget | null,\n): Uint8Array {\n const cap = budget ? entryInflateCap(budget, compressed.byteLength) : Number.POSITIVE_INFINITY;\n const record = budget ? beginEntryInflate(budget, path) : null;\n const acc: Uint8Array[] = [];\n let emitted = 0;\n let aborted: Error | undefined;\n const inflater = new Inflate((chunk) => {\n if (aborted) return;\n if (chunk.byteLength === 0) return;\n emitted += chunk.byteLength;\n if (emitted > cap) {\n aborted = entryOverflowError(path, cap);\n return;\n }\n if (record) {\n try {\n record(chunk.byteLength);\n } catch (err) {\n aborted = err as Error;\n return;\n }\n }\n acc.push(chunk);\n });\n let off = 0;\n while (off < compressed.byteLength) {\n const end = Math.min(off + INFLATE_CHUNK_BYTES, compressed.byteLength);\n const isLast = end >= compressed.byteLength;\n try {\n inflater.push(compressed.subarray(off, end), isLast);\n } catch (cause) {\n if (aborted) throw aborted;\n throw new OpenXmlIoError(`openZip: failed to inflate \"${path}\"`, { cause });\n }\n if (aborted) throw aborted;\n off = end;\n }\n const out = new Uint8Array(emitted);\n let cursor = 0;\n for (const chunk of acc) {\n out.set(chunk, cursor);\n cursor += chunk.byteLength;\n }\n return out;\n}\n\n/**\n * Fallback for archives we can't parse ourselves. The random-access reader\n * now understands ZIP64 EOCD + the Zip64 Extended Information extra field, so\n * the only way to reach this path is a malformed central directory. fflate's\n * `unzipSync` is more forgiving but inflates every entry up front; for\n * adversarial archives we still rely on a post-hoc cap check (the only one\n * available without our own streaming decoder).\n */\nfunction openViaUnzipSync(\n bytes: Uint8Array,\n limits: ReturnType<typeof resolveDecompressionLimits>,\n): ZipArchive {\n let entries: Record<string, Uint8Array> | undefined;\n try {\n entries = unzipSync(bytes);\n } catch (cause) {\n throw new OpenXmlIoError('openZip: archive is not a valid zip', { cause });\n }\n // fflate's `unzipSync` returns already-inflated bytes — we can't abort the\n // inflate mid-flight here, but a post-hoc check still rejects a malicious\n // archive *before* any caller-level code touches the bytes. The peak memory\n // spike is bounded by what fflate just produced; the random-access path\n // catches the common cases (ZIP64 + malformed-but-parseable CDs) up front.\n if (limits) {\n const budget = createBudget(limits);\n for (const [path, payload] of Object.entries(entries)) {\n if (payload.byteLength > limits.maxEntryUncompressedBytes) {\n throw new OpenXmlDecompressionBombError(\n `openZip: entry \"${path}\" inflated to ${payload.byteLength} bytes,` +\n ` exceeding the ${limits.maxEntryUncompressedBytes}-byte per-entry limit` +\n ` (decompression-bomb guard).`,\n );\n }\n const record = beginEntryInflate(budget, path);\n record(payload.byteLength);\n }\n }\n let live = true;\n return {\n list(): string[] {\n if (!live || !entries) throw new OpenXmlIoError('openZip: archive is closed');\n return Object.keys(entries).sort();\n },\n has(path: string): boolean {\n if (!live || !entries) return false;\n return Object.hasOwn(entries, path);\n },\n read(path: string): Uint8Array {\n if (!live || !entries) throw new OpenXmlIoError('openZip: archive is closed');\n const e = entries[path];\n if (!e) throw new OpenXmlIoError(`openZip: no entry at \"${path}\"`);\n // Copy, as the random-access path does: `read` hands the caller an array\n // of its own, and this one holds every entry for the archive's lifetime.\n return e.slice();\n },\n async readAsync(path: string): Promise<Uint8Array> {\n return this.read(path);\n },\n readStream(path: string): ReadableStream<Uint8Array> {\n // The unzipSync fallback path already has the entry fully inflated\n // (that's the price of dropping back from random-access). Hand it out as\n // a single-chunk stream so callers using the streaming reader don't have\n // to branch on the implementation.\n const inflated = this.read(path);\n return singleChunkStream(inflated);\n },\n close(): void {\n live = false;\n entries = undefined;\n },\n };\n}\n","// ZIP read layer.\n//\n// `openZip(source)` walks the central directory once and inflates each entry on\n// demand inside `read(path)` (see `./random-access-reader.ts`). That keeps peak\n// memory at compressed-archive size + per-entry inflate scratch + the bounded\n// cache of small re-read entries (`./inflate-cache.ts`), instead of holding\n// every uncompressed entry resident at once the way the old `unzipSync`\n// shortcut did. The fallback path through fflate's `unzipSync` is preserved for\n// ZIP64 / non-standard archives.\n\nimport type { XlsxSource } from '../io/source.js';\nimport { OpenXmlIoError, OpenXmlNotImplementedError } from '../utils/exceptions.js';\nimport type { DecompressionLimits } from './decompression-guard.js';\nimport { openRandomAccessArchive } from './random-access-reader.js';\n\nconst CFB_MAGIC = [0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1];\n\nconst isCfbCompoundDocument = (bytes: Uint8Array): boolean => {\n if (bytes.length < CFB_MAGIC.length) return false;\n for (let i = 0; i < CFB_MAGIC.length; i++) {\n if (bytes[i] !== CFB_MAGIC[i]) return false;\n }\n return true;\n};\n\nexport interface ZipArchive {\n /** Sorted list of all entry paths in the archive. */\n list(): string[];\n /**\n * Synchronous read; throws OpenXmlIoError when the path is unknown. Each\n * call returns an array the caller owns: mutating it changes neither the\n * archive nor what a later read of the same path returns.\n */\n read(path: string): Uint8Array;\n /** Promise variant for symmetry with the future streaming reader. */\n readAsync(path: string): Promise<Uint8Array>;\n /**\n * Streaming read: returns the entry's inflated bytes as a Web\n * `ReadableStream<Uint8Array>` chunk-by-chunk. Lets callers (the streaming\n * worksheet iterator, in particular) push the inflated payload through a SAX\n * parser without first materialising it in full — peak memory for a sheet\n * walk drops to the inflate window + SAX state instead of the entire\n * uncompressed worksheet body. Throws OpenXmlIoError when the path is\n * unknown.\n */\n readStream(path: string): ReadableStream<Uint8Array>;\n /** Whether the archive holds an entry at the given path. */\n has(path: string): boolean;\n /** Release the in-memory entry table. Subsequent reads throw. */\n close(): void;\n}\n\n/** Options for {@link openZip}. */\nexport interface OpenZipOptions {\n /**\n * Decompression-bomb safeguards applied while inflating archive entries. The\n * default limits admit any legitimate xlsx and reject pathological archives\n * (extreme compression ratios, gigabyte-scale entries). Pass `false` to\n * disable the guard entirely — only safe when the source is fully trusted.\n * See {@link DecompressionLimits} for the individual knobs.\n */\n decompressionLimits?: DecompressionLimits | false;\n}\n\n/**\n * Open a zip archive from any {@link XlsxSource}. The source is fully\n * materialised in memory, the central directory is parsed once, and each\n * entry is inflated on demand by {@link openRandomAccessArchive}: peak memory\n * stays at compressed-archive size, plus per-entry inflate scratch, plus a few\n * MB at most of small entries kept for re-reads, rather than holding every\n * uncompressed entry resident. The fflate `unzipSync` fallback is preserved\n * internally for ZIP64 / non-standard archives the random-access reader\n * rejects, and it does hold every entry inflated.\n */\nexport async function openZip(source: XlsxSource, opts: OpenZipOptions = {}): Promise<ZipArchive> {\n let bytes: Uint8Array;\n try {\n bytes = await source.toBytes();\n } catch (cause) {\n throw new OpenXmlIoError('openZip: failed to read source bytes', { cause });\n }\n\n // Encrypted xlsx files (Excel 2007+ password protection) wrap the real\n // package inside an OLE Compound File Binary container with the magic\n // signature `D0 CF 11 E0 A1 B1 1A E1`. Detect that early and surface a clear\n // \"decrypt first\" error rather than letting fflate fail with a generic\n // invalid-zip message.\n if (isCfbCompoundDocument(bytes)) {\n throw new OpenXmlNotImplementedError(\n 'Encrypted xlsx is not supported. Decrypt with msoffcrypto-tool first.',\n );\n }\n\n return openRandomAccessArchive(bytes, opts.decompressionLimits);\n}\n"],"mappings":"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"}