web-jszipp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,955 @@
1
+ # JSZipp
2
+
3
+ **The ZIP library for browser apps that need safety, streaming, and archive fidelity.**
4
+
5
+ JSZipp is a tiny, dependency-free ZIP reader and writer for modern browser apps.
6
+ It combines safe defaults, Web Streams integration, ZIP64, full archive metadata,
7
+ correct filename decoding, TypeScript types, and practical output shapes
8
+ (`Blob`, `Response`, `ReadableStream`, `Uint8Array`, `ArrayBuffer`) in one
9
+ focused package.
10
+
11
+ Reach for JSZipp when your app handles **ZIP archives in the browser** — file
12
+ uploads, downloadable exports, `.docx` / `.xlsx` / `.epub` inspection, plugin
13
+ bundles, templates, CI artifacts, generated reports, or package-like archives —
14
+ and you want the default path to be safe and productive.
15
+
16
+ ```ts
17
+ import { ZipWriter, openZip } from "web-jszipp";
18
+
19
+ // Create a browser-downloadable ZIP.
20
+ const writer = new ZipWriter({ outputAs: "blob" });
21
+ await writer.add({ path: "hello.txt", data: "Hello from JSZipp" });
22
+ const download = await writer.close();
23
+
24
+ // Open an untrusted upload with the strict package profile.
25
+ const zip = await openZip(fileInput.files![0], {
26
+ pathMode: "strict-package",
27
+ maxArchiveSize: 50 * 1024 * 1024,
28
+ maxEntrySize: 10 * 1024 * 1024
29
+ });
30
+
31
+ console.log(await zip.get("hello.txt")?.text());
32
+ await zip.close();
33
+ ```
34
+
35
+ ## Why JSZipp?
36
+
37
+ Most ZIP libraries make the happy path easy. JSZipp is designed to make the
38
+ **safe browser happy path** easy.
39
+
40
+ | General-user need | Why it matters | JSZipp's answer |
41
+ | --- | --- | --- |
42
+ | Accept user ZIP uploads | ZIP filenames are attacker-controlled paths, not harmless labels. | `openZip()` rejects unsafe paths by default; `strict-package` adds package-grade collision and local/central consistency checks. |
43
+ | Avoid zip-bomb surprises | A small upload can claim one size and expand into much more data. | `maxArchiveSize` bounds the archive and `maxEntrySize` is enforced while inflating. |
44
+ | Ship less JavaScript | Browser apps pay for every byte and every dependency. | Zero dependencies, native `DecompressionStream` for reading deflated entries, and tree-shakeable reader/writer entry points. |
45
+ | Work with real browser APIs | Downloads, fetch responses, service workers, and pipelines already speak Web APIs. | `ZipWriter` can return `Blob`, `Response`, `ReadableStream`, `Uint8Array`, or `ArrayBuffer`; `ZipTransformStream` is a native `TransformStream`. |
46
+ | Preserve real archive data | Archives are more than compressed bytes: comments, modes, timestamps, names, and ZIP64 matter. | ZIP64, comments, extra fields, Unix mode bits, DOS + UTC timestamps, CRC-32, CP437, `TextDecoder` fallbacks, and Info-ZIP Unicode Path support. |
47
+ | Keep the app code simple | Most teams do not want to write their own ZIP safety and metadata layer. | Random-access `entries` / `get(path)` plus `text()` / `bytes()` / `arrayBuffer()` / `stream()` helpers and full TypeScript types. |
48
+
49
+ The practical win is not that JSZipp beats every library at every benchmark. It
50
+ is that common browser ZIP tasks need fewer adapters, fewer security footguns,
51
+ and fewer project-specific validation rules.
52
+
53
+ ## Highlights
54
+
55
+ - **Safe by default.** `openZip` rejects Zip Slip (`..`), absolute, drive-letter,
56
+ drive-relative, backslash, and NUL-byte paths out of the box. It also
57
+ cross-checks each entry's local header against the central directory for
58
+ filename, security-flag, and reused-offset consistency, so a scanner and an
59
+ extractor cannot be shown two different file trees.
60
+ - **A stricter profile for untrusted packages.** `pathMode: "strict-package"`
61
+ adds local/central size cross-checks and rejects duplicate, case-only
62
+ (`Readme.txt` vs `README.TXT`), and Unicode NFC/NFD path collisions — the
63
+ parser-differential tricks that appear when one tool validates an archive and
64
+ another extracts it.
65
+ - **Anti-zip-bomb caps.** `maxArchiveSize` and `maxEntrySize` bound input and
66
+ per-entry output. `maxEntrySize` is enforced *during* inflate, so a header that
67
+ lies about its uncompressed size cannot expand past the cap before JSZipp
68
+ notices.
69
+ - **Browser-native output.** Create a ZIP byte stream by default, or ask for a
70
+ `Blob` for downloads, a `Response` for fetch-like APIs, or raw bytes for
71
+ storage and tests. `AbortSignal` and progress callbacks are first-class.
72
+ - **Stream-shaped APIs.** `ZipTransformStream` drops into Web Streams pipelines,
73
+ while `readZipStream` gives you a `for await...of` reader for archive entries.
74
+ - **Full-fidelity ZIP handling.** ZIP64 (auto/force/off), store + deflate,
75
+ per-file and archive comments, extra fields, Unix mode bits, DOS + UTC
76
+ (`0x5455`) timestamps, CRC-32 integrity, and EOCD-by-content detection that
77
+ resists comment/append forgery.
78
+ - **Correct filenames.** UTF-8 with the UTF-8 flag, a built-in CP437 decoder,
79
+ `TextDecoder` fallbacks (`shift_jis`, `gbk`, `big5`, …), and CRC-verified
80
+ Info-ZIP Unicode Path (`0x7075`) support.
81
+ - **Ergonomic and typed.** Random-access `entries` / `get(path)`, reusable
82
+ `text()` / `bytes()` / `arrayBuffer()` / `stream()` helpers, synchronous
83
+ in-memory writing, and full TypeScript types.
84
+
85
+ ## JSZipp vs JSZip vs fflate
86
+
87
+ There are excellent ZIP libraries already. The honest summary:
88
+
89
+ - **JSZip** is a mature, friendly, general-purpose ZIP toolkit with a large
90
+ ecosystem and a familiar API.
91
+ - **fflate** is a best-in-class JavaScript compression engine with fast raw
92
+ DEFLATE/GZIP/Zlib/ZIP primitives and callback-style streaming tools.
93
+ - **JSZipp** focuses on *safe, browser-native, full-fidelity ZIP archive handling*
94
+ for apps that read or write archives crossing a trust boundary.
95
+
96
+ | | **JSZipp** | JSZip | fflate |
97
+ | --- | --- | --- | --- |
98
+ | Best fit | Browser ZIP handling with safety defaults | Mature general ZIP toolkit | Fastest/smallest compression engine |
99
+ | Read unsafe paths | Rejects by default; `sanitize` / `unsafe` are opt-in | Sanitizes relative path traversal; strict rejection policy is app-defined | App-defined |
100
+ | Package hardening | `strict-package` collision + local/central checks | No strict-package profile | No strict-package profile |
101
+ | Parser-differential defenses | Filename, security flags, reused offsets; size checks in `strict-package` | Not the primary focus | Not the primary focus |
102
+ | Anti-zip-bomb caps | Built in (`maxArchiveSize`, bounded `maxEntrySize`) | App-defined | App-defined/filter-based |
103
+ | Browser Web Streams | Native `ReadableStream` + `TransformStream` shapes | Promise/StreamHelper/Node stream oriented | Callback stream APIs |
104
+ | Browser output targets | `ReadableStream`, `Blob`, `Response`, `Uint8Array`, `ArrayBuffer` | Common byte/blob outputs | Byte arrays/callback chunks |
105
+ | Random-access convenience | `entries`, `get(path)`, reusable entry readers | Yes, mature object API | Mostly lower-level ZIP primitives |
106
+ | Full archive metadata | Comments, extra fields, modes, timestamps, ZIP64 | Common metadata, but some input data is discarded on rewrite | Focused on compression/archive primitives |
107
+ | Filename encodings | UTF-8, CP437, `TextDecoder` fallbacks, Unicode Path extra | UTF-8 plus custom decode hooks | UTF-8-oriented API |
108
+ | Dependencies | None | None | None |
109
+ | Raw compression speed | Good | Moderate | Best-in-class |
110
+
111
+ Competitor cells are deliberately high-level and may change by version. Verify
112
+ library-specific behavior against the release you use.
113
+
114
+ ### Choosing between them
115
+
116
+ - **Pick JSZipp** when you read ZIP uploads, inspect package-like archives, create
117
+ downloadable ZIPs in a browser, need Web Streams or `Response` output, care
118
+ about metadata, or want safe defaults instead of writing your own path,
119
+ collision, and zip-bomb guardrails.
120
+ - **Pick JSZip** when you already rely on its API or ecosystem, want the most
121
+ familiar general-purpose ZIP object model, and do not need JSZipp's stricter
122
+ trust-boundary profile or browser-native stream shapes.
123
+ - **Pick fflate** when raw compression/decompression speed, worker-based
124
+ throughput, or the smallest compression-focused primitive is the deciding
125
+ factor, and you are comfortable building your own archive policy, metadata
126
+ layer, and app-specific validation.
127
+
128
+ If your decision question is **"can my browser app safely open this ZIP
129
+ upload?"**, JSZipp is built for that job: use `openZip` with
130
+ `pathMode: "strict-package"` plus explicit `maxArchiveSize` and `maxEntrySize`
131
+ caps, and reject archives that do not meet the profile.
132
+
133
+ ## Runtime
134
+
135
+ - ECMAScript 2019 output
136
+ - Modern browsers with `ReadableStream`, `TransformStream`, `Blob`, and
137
+ `DecompressionStream` for reading deflated entries
138
+ - Intended browser baseline: Chrome 80+ and Firefox 113+ class browsers
139
+ - Node.js can run the tests, but the library is designed for browser APIs
140
+
141
+ ## Error Messages
142
+
143
+ JSZipp keeps exception classes and DOMException names stable across builds
144
+ (`RangeError`, `TypeError`, `SecurityError`, `InvalidStateError`,
145
+ `NotSupportedError`, and so on). Production bundles shorten `error.message` to
146
+ codes such as `E_PATH`, `E_LIMIT`, and `E_STRUCTURE`; source/dev execution keeps
147
+ the detailed diagnostic messages used by the test suite.
148
+
149
+ ## Install
150
+
151
+ ```sh
152
+ pnpm add web-jszipp
153
+ ```
154
+
155
+ ```ts
156
+ import JSZipp, {
157
+ ZipWriter,
158
+ ZipTransformStream,
159
+ openZip,
160
+ readZipStream,
161
+ TimestampMode
162
+ } from "web-jszipp";
163
+ ```
164
+
165
+ `JSZipp` is the default namespace export and includes the same runtime values:
166
+ `ZipWriter`, `ZipTransformStream`, `openZip`, `readZipStream`, and
167
+ `TimestampMode`. Named exports are usually more convenient in application code.
168
+
169
+ Browser-legacy builds are opt-in npm subpaths for apps that must target older
170
+ browser pairs. They expose the same public API as the main entry point, but ship
171
+ extra compatibility code:
172
+
173
+ ```ts
174
+ import { ZipWriter, openZip } from "web-jszipp/browser-legacy/cr61ff58";
175
+ ```
176
+
177
+ ```ts
178
+ import { ZipWriter, openZip } from "web-jszipp/browser-legacy/cr86ff68";
179
+ ```
180
+
181
+ If you prefer CDN script tags, use one of the following UMD builds:
182
+
183
+ ```html
184
+ <!-- Modern UMD default -->
185
+
186
+ <script src="https://unpkg.com/web-jszipp"></script>
187
+
188
+ <script src="https://cdn.jsdelivr.net/npm/web-jszipp"></script>
189
+
190
+
191
+ <!-- Chrome 61 / Firefox 58 compatible UMD -->
192
+
193
+ <script src="https://unpkg.com/web-jszipp/dist/cr61ff58/jszipp.umd.js"></script>
194
+
195
+ <script src="https://cdn.jsdelivr.net/npm/web-jszipp/dist/cr61ff58/jszipp.umd.js"></script>
196
+
197
+
198
+ <!-- Chrome 86 / Firefox 68 compatible UMD -->
199
+
200
+ <script src="https://unpkg.com/jszipp/dist/cr86ff68/jszipp.umd.js"></script>
201
+
202
+ <script src="https://cdn.jsdelivr.net/npm/jszipp/dist/cr86ff68/jszipp.umd.js"></script>
203
+ ```
204
+
205
+ ## Which API Should I Use?
206
+
207
+ | Your app needs to | Use | Why |
208
+ | --- | --- | --- |
209
+ | Create a ZIP Blob for download or upload | `new ZipWriter({ outputAs: "blob" })` | Easiest option for most browser apps. |
210
+ | Create a ZIP HTTP response | `new ZipWriter({ outputAs: "response" })` | Returns a native `Response` wrapper. |
211
+ | Create a ZIP byte stream | `new ZipWriter()` | Default mode returns `ReadableStream<Uint8Array>`. |
212
+ | Create raw ZIP bytes | `new ZipWriter({ outputAs: "uint8array" })` | Returns browser byte containers directly. |
213
+ | Insert ZIP creation into an existing Web Streams pipeline | `ZipTransformStream` | It is a native `TransformStream`. |
214
+ | Open a user-selected `.zip` file and read files by name | `openZip` | Best random-access API for `Blob`, `File`, `Uint8Array`, or `ArrayBuffer`. |
215
+ | Open an untrusted upload or package | `openZip(file, { pathMode: "strict-package", maxArchiveSize, maxEntrySize })` | Applies the strongest reader policy with explicit size caps. |
216
+ | List every entry in archive order, including duplicate names from foreign archives | `openZip(...).entries` | Preserves the archive's true entry order. |
217
+ | Get JSZipp's selected file for a path when duplicates exist | `openZip(...).get(path)` | Returns the last matching central-directory entry; external extractors vary. |
218
+ | Consume a ZIP as an async iterator | `readZipStream` | Forward-style iteration with single-use entry tokens. |
219
+ | Read a file more than once or concurrently | `openZip` | Random-access entries create independent streams. |
220
+ | Create a small in-memory ZIP synchronously | `writer.writeSync()` + `writer.closeSync()` | Useful for tests, fixtures, and already-in-memory data. |
221
+
222
+ Most browser apps should use:
223
+
224
+ - `ZipWriter` for creating archives
225
+ - `openZip` for reading archives selected by the user
226
+
227
+ Use `ZipTransformStream` only when you already think in Web Streams. Use
228
+ `readZipStream` when async iteration is a better fit than path lookup.
229
+
230
+ ## Create A ZIP
231
+
232
+ `ZipWriter` is the simplest way to create an archive. For browser downloads,
233
+ ask it to return a `Blob`.
234
+
235
+ ```ts
236
+ import { ZipWriter } from "web-jszipp";
237
+
238
+ const writer = new ZipWriter({ level: 6, outputAs: "blob" });
239
+
240
+ await writer.add({ path: "hello.txt", data: "Hello from JSZipp" });
241
+ await writer.add({ path: "docs/readme.md", data: "# Readme\n" });
242
+ const zipBlob = await writer.close();
243
+ ```
244
+
245
+ Save it from the browser:
246
+
247
+ ```ts
248
+ const url = URL.createObjectURL(zipBlob);
249
+ const link = document.createElement("a");
250
+ link.href = url;
251
+ link.download = "archive.zip";
252
+ link.click();
253
+ URL.revokeObjectURL(url);
254
+ ```
255
+
256
+ ## Add Different Data Types
257
+
258
+ `ZipInputEntry.data` accepts `string`, `Uint8Array`, `ArrayBuffer`, `Blob`, or
259
+ `ReadableStream<Uint8Array>`.
260
+
261
+ ```ts
262
+ const writer = new ZipWriter({ level: 6, outputAs: "blob" });
263
+
264
+ await writer.add({ path: "text.txt", data: "plain text" });
265
+ await writer.add({ path: "bytes.bin", data: new Uint8Array([1, 2, 3]) });
266
+ await writer.add({ path: "buffer.bin", data: new Uint8Array([4, 5, 6]).buffer });
267
+ await writer.add({ path: "photo.jpg", data: fileInput.files![0] });
268
+ await writer.add({ path: "folder/", data: "" });
269
+ await writer.add({
270
+ path: "stream.txt",
271
+ data: new Blob(["streamed content"]).stream()
272
+ });
273
+
274
+ const zipBlob = await writer.close();
275
+ ```
276
+
277
+ ## Add Metadata
278
+
279
+ Each entry can include a comment, timestamps, Unix permissions, DOS attributes,
280
+ or low-level ZIP metadata. Writer options can also include an archive-level ZIP
281
+ comment.
282
+
283
+ ```ts
284
+ const writer = new ZipWriter({
285
+ outputAs: "blob",
286
+ comment: "Generated by JSZipp"
287
+ });
288
+
289
+ await writer.add({
290
+ path: "report.txt",
291
+ data: "Quarterly report",
292
+ meta: {
293
+ comment: "Generated in the browser",
294
+ modifiedAt: new Date("2026-05-31T12:00:00Z"),
295
+ unixPermissions: 0o644
296
+ }
297
+ });
298
+
299
+ await writer.add({
300
+ path: "scripts/build.sh",
301
+ data: "#!/bin/sh\npnpm build\n",
302
+ meta: { unixPermissions: 0o755 }
303
+ });
304
+ ```
305
+
306
+ ## Compression Options
307
+
308
+ ```ts
309
+ new ZipWriter({
310
+ level: 6,
311
+ zip64: "auto",
312
+ outputAs: "blob"
313
+ });
314
+ ```
315
+
316
+ `level`:
317
+
318
+ - `0`: store files without compression
319
+ - `1` to `9`: use DEFLATE compression with real level control
320
+ - default: `6`
321
+
322
+ `zip64`:
323
+
324
+ - `"auto"`: emit ZIP64 records only when standard ZIP limits are exceeded. This is the default.
325
+ - `"force"`: always emit ZIP64-compatible records.
326
+ - `"off"`: write standard ZIP records and throw if ZIP64 would be required.
327
+
328
+ `outputAs`:
329
+
330
+ - `"stream"`: `close()` returns `ReadableStream<Uint8Array>`. This is the default.
331
+ - `"blob"`: `close()` returns a native `Blob`.
332
+ - `"response"`: `close()` returns a native `Response`.
333
+ - `"uint8array"`: `close()` returns a `Uint8Array`.
334
+ - `"arraybuffer"`: `close()` returns an `ArrayBuffer`.
335
+
336
+ Use `level: 6` for text, JSON, CSV, HTML, and similar files. JSZipp will store
337
+ an entry automatically when the default DEFLATE attempt would not make it
338
+ smaller. Use `level: 0` or `method: "store"` when you want to skip compression
339
+ work entirely for already-compressed files such as JPEG, PNG, MP4, or PDF.
340
+
341
+ You can override compression per entry:
342
+
343
+ ```ts
344
+ await writer.add({ path: "photo.jpg", data: photoFile, method: "store" });
345
+ await writer.add({ path: "data/report.json", data: jsonText, method: "deflate" });
346
+ ```
347
+
348
+ `method: "store"` skips compression for that entry. `method: "deflate"` forces
349
+ JSZipp's in-repo raw DEFLATE writer. When no per-entry method is set, JSZipp
350
+ uses DEFLATE but stores the entry instead if the compressed payload would be no
351
+ smaller than the source. Entry-level `level` overrides the writer default for
352
+ that file, so you can use lower levels for faster files and higher levels for
353
+ deeper match search.
354
+
355
+ Generated archives use ZIP method `0x0000` for stored entries, ZIP method
356
+ `0x0008` for deflated entries, and general-purpose bit flags `0x0800` to mark
357
+ filenames/comments as UTF-8. For the ZIP-format distinction between compression
358
+ method values and general-purpose bit flags, see
359
+ [ZIP metadata traps](docs/zip-metadata-traps.md#compression-method-and-general-purpose-bit-flags).
360
+
361
+ ## Choose The Output Type
362
+
363
+ Default streaming output:
364
+
365
+ ```ts
366
+ const writer = new ZipWriter();
367
+
368
+ await writer.add({ path: "log.txt", data: "stream me" });
369
+ const stream = await writer.close();
370
+ ```
371
+
372
+ Blob output for downloads, file uploads, or `openZip`:
373
+
374
+ ```ts
375
+ const writer = new ZipWriter({ outputAs: "blob" });
376
+
377
+ await writer.add({ path: "report.txt", data: "download me" });
378
+ const blob = await writer.close();
379
+ ```
380
+
381
+ Response output for service workers, route handlers, and fetch-like APIs:
382
+
383
+ ```ts
384
+ const writer = new ZipWriter({ outputAs: "response" });
385
+
386
+ await writer.add({ path: "api.txt", data: "response body" });
387
+ const response = await writer.close();
388
+ ```
389
+
390
+ Custom response MIME type:
391
+
392
+ ```ts
393
+ const writer = new ZipWriter({
394
+ outputAs: "response",
395
+ mimeType: "application/x-zip-compressed"
396
+ });
397
+ ```
398
+
399
+ Raw byte output:
400
+
401
+ ```ts
402
+ const bytes = await new ZipWriter({ outputAs: "uint8array" }).close();
403
+ const buffer = await new ZipWriter({ outputAs: "arraybuffer" }).close();
404
+ ```
405
+
406
+ ## Synchronous In-Memory Writing
407
+
408
+ Use `writeSync()` / `closeSync()` for tests, fixtures, small generated archives,
409
+ or code paths where all entry data is already in memory. The synchronous API
410
+ accepts `string`, `Uint8Array`, and `ArrayBuffer` data. Use async `add()` for
411
+ `Blob` and `ReadableStream` input.
412
+
413
+ ```ts
414
+ const writer = new ZipWriter({ outputAs: "uint8array" });
415
+
416
+ writer.writeSync({ path: "manifest.json", data: JSON.stringify({ ok: true }) });
417
+ writer.writeSync({ path: "data.bin", data: new Uint8Array([1, 2, 3]) });
418
+
419
+ const zipBytes = writer.closeSync();
420
+ ```
421
+
422
+ Do not mix sync and async writes on the same writer. JSZipp rejects mixed usage
423
+ so entries are not accidentally routed to different output paths.
424
+
425
+ ## Read A ZIP By File Name
426
+
427
+ Use `openZip` when the ZIP is a `Blob`, `File`, `Uint8Array`, or `ArrayBuffer`,
428
+ such as a file chosen from an `<input type="file">`.
429
+
430
+ ```ts
431
+ import { openZip } from "web-jszipp";
432
+
433
+ const file = fileInput.files![0];
434
+ const reader = await openZip(file);
435
+
436
+ const readme = reader.get("docs/readme.md");
437
+ if (readme) {
438
+ console.log(await readme.text());
439
+ }
440
+
441
+ await reader.close();
442
+ ```
443
+
444
+ By default, `openZip()` rejects unsafe entry paths that could escape an
445
+ extraction root, including `..`, absolute paths, drive-letter paths (including
446
+ drive-relative names like `C:name`), backslash-separated paths, and paths
447
+ containing a NUL byte. Use `pathMode: "sanitize"` to normalize unsafe names
448
+ instead, or `pathMode: "unsafe"` only when you need raw archive names and will
449
+ handle extraction safety yourself.
450
+
451
+ ```ts
452
+ const reader = await openZip(file, { pathMode: "sanitize" });
453
+ ```
454
+
455
+ ### Strict Package Mode
456
+
457
+ For archives that cross a trust boundary — uploads, software packages, CI
458
+ artifacts, document bundles — use `pathMode: "strict-package"`. It applies all
459
+ the `"strict"` path checks above and adds two cross-entry checks the default
460
+ deliberately leaves off (so the default can preserve duplicate paths and defer
461
+ size integrity to read time):
462
+
463
+ - the local file header and central directory sizes must agree (for non-streaming
464
+ entries), and
465
+ - no two entries may collide after Unicode (NFC) and case normalization — this
466
+ rejects exact duplicates, case-only twins (`Readme.txt` vs `README.TXT`), and
467
+ NFC/NFD twins.
468
+
469
+ ```ts
470
+ try {
471
+ // A hostile package with duplicate, case-colliding, or size-spoofing entries
472
+ // throws here instead of being silently accepted.
473
+ const reader = await openZip(untrustedUpload, { pathMode: "strict-package" });
474
+ for (const entry of reader.entries) {
475
+ // ... safe to process
476
+ }
477
+ } catch (error) {
478
+ // Reject the upload: it does not meet the strict package profile.
479
+ }
480
+ ```
481
+
482
+ The default reader (`pathMode: "strict"`) is unchanged: it still preserves
483
+ duplicate paths and verifies size/CRC integrity at read time.
484
+
485
+ Writers reject duplicate normalized entry paths. If you need to replace an entry,
486
+ choose the final payload before calling `add()` or `writeSync()`.
487
+
488
+ ## List Every Entry
489
+
490
+ `reader.entries` preserves the real order inside the archive. This matters for
491
+ ZIP files from other tools that contain duplicate paths.
492
+
493
+ ```ts
494
+ const reader = await openZip(zipBlob);
495
+
496
+ for (const entry of reader.entries) {
497
+ console.log({
498
+ path: entry.path,
499
+ size: entry.size,
500
+ compressedSize: entry.compressedSize,
501
+ crc32: entry.crc32,
502
+ isDirectory: entry.isDirectory,
503
+ comment: entry.comment,
504
+ modifiedAt: entry.modifiedAt,
505
+ externalAttributes: entry.externalAttributes,
506
+ unixFileAttributes: entry.externalAttributes !== undefined ? entry.externalAttributes >>> 16 : undefined,
507
+ dosAttributeByte: entry.externalAttributes !== undefined ? entry.externalAttributes & 0xff : undefined
508
+ });
509
+ }
510
+ ```
511
+
512
+ ## Duplicate File Names
513
+
514
+ ZIP archives can contain the same path more than once. `entries` shows all of
515
+ them. `get(path)` returns the latest matching entry.
516
+
517
+ ```ts
518
+ const reader = await openZip(zipBlob);
519
+
520
+ const allCopies = reader.entries.filter((entry) => entry.path === "data.json");
521
+ const latest = reader.get("data.json");
522
+ ```
523
+
524
+ ## Read Entry Data
525
+
526
+ Random-access entries from `openZip` are reusable. You can call `stream()` or
527
+ `text()` many times.
528
+
529
+ ```ts
530
+ const entry = reader.get("data.json");
531
+
532
+ if (entry) {
533
+ const text = await entry.text();
534
+ const bytes = await entry.bytes();
535
+ const buffer = await entry.arrayBuffer();
536
+ }
537
+ ```
538
+
539
+ ## Read Legacy File Names
540
+
541
+ If an archive does not mark names as UTF-8, `openZip` can use a fallback
542
+ encoding.
543
+
544
+ ```ts
545
+ const reader = await openZip(file, {
546
+ filenameEncoding: "shift_jis",
547
+ pathMode: "strict"
548
+ });
549
+ ```
550
+
551
+ Supported fallback values:
552
+
553
+ - `"cp437"`
554
+ - any charset label supported by `TextDecoder`, such as `"utf-8"`,
555
+ `"shift_jis"`, or `"windows-1252"`
556
+
557
+ See [Filename Charset Handling](docs/charset.md) for
558
+ details on ZIP filename charset behavior and choosing a fallback.
559
+
560
+ ## Stream Pipeline Writing
561
+
562
+ Use `ZipTransformStream` when another part of your app already writes
563
+ `ZipInputEntry` objects into a stream.
564
+
565
+ ```ts
566
+ import { ZipTransformStream } from "web-jszipp";
567
+
568
+ const zipStream = new ZipTransformStream({ level: 6 });
569
+ const archivePromise = new Response(zipStream.readable).blob();
570
+ const writer = zipStream.writable.getWriter();
571
+
572
+ await writer.write({ path: "a.txt", data: "A" });
573
+ await writer.write({ path: "b.txt", data: "B" });
574
+ await writer.close();
575
+
576
+ const zipBlob = await archivePromise;
577
+ ```
578
+
579
+ ## Async Iterator Reading
580
+
581
+ Use `readZipStream` when you want a `for await...of` style reader.
582
+
583
+ ```ts
584
+ import { readZipStream } from "web-jszipp";
585
+
586
+ for await (const entry of readZipStream(zipBlob.stream())) {
587
+ if (entry.isDirectory) {
588
+ await entry.skip();
589
+ continue;
590
+ }
591
+
592
+ if (entry.path.endsWith(".txt")) {
593
+ console.log(entry.path, await entry.text());
594
+ } else {
595
+ await entry.skip();
596
+ }
597
+ }
598
+ ```
599
+
600
+ `ZipStreamEntry` payloads are single-use. For each entry, call exactly one of:
601
+
602
+ - `entry.stream()`
603
+ - `entry.text()`
604
+ - `entry.bytes()`
605
+ - `entry.arrayBuffer()`
606
+ - `entry.skip()`
607
+
608
+ If you need to read the same entry more than once, use `openZip` instead.
609
+
610
+ ## API Reference
611
+
612
+ ### `new ZipWriter(options?)`
613
+
614
+ High-level ZIP writer.
615
+
616
+ ```ts
617
+ const writer = new ZipWriter({
618
+ level: 6,
619
+ zip64: "auto",
620
+ outputAs: "blob"
621
+ });
622
+ ```
623
+
624
+ Properties and methods:
625
+
626
+ - `writer.output: ReadableStream<Uint8Array>`
627
+ - `writer.add(entry: ZipInputEntry): Promise<void>`
628
+ - `writer.writeSync(entry: ZipSyncInputEntry): void`
629
+ - `writer.close(): Promise<ReadableStream<Uint8Array> | Blob | Response | Uint8Array | ArrayBuffer>`
630
+ - `writer.closeSync(): ReadableStream<Uint8Array> | Blob | Response | Uint8Array | ArrayBuffer`
631
+
632
+ The writer rejects duplicate normalized entry paths. It does not emit archives
633
+ where two records target the same path.
634
+
635
+ `close()` returns a more specific type when `outputAs` is known:
636
+
637
+ ```ts
638
+ const stream = await new ZipWriter().close();
639
+ const blob = await new ZipWriter({ outputAs: "blob" }).close();
640
+ const response = await new ZipWriter({ outputAs: "response" }).close();
641
+ const bytes = await new ZipWriter({ outputAs: "uint8array" }).close();
642
+ ```
643
+
644
+ Options:
645
+
646
+ ```ts
647
+ interface ZipWriterOptions {
648
+ level?: number;
649
+ zip64?: "auto" | "force" | "off";
650
+ comment?: string;
651
+ timestamps?: number; // bitmask of TimestampMode flags (Dos=1, Unix=2, Ntfs=4)
652
+ pathMode?: "strict" | "sanitize" | "unsafe" | "strict-package";
653
+ signal?: AbortSignal;
654
+ onProgress?: (progress: ZipProgress) => void;
655
+ explicitDirectoryEntries?: boolean;
656
+ outputAs?: "stream" | "blob" | "response" | "uint8array" | "arraybuffer";
657
+ mimeType?: string;
658
+ }
659
+ ```
660
+
661
+ ### `new ZipTransformStream(options?)`
662
+
663
+ Native transform stream from `ZipInputEntry` objects to ZIP bytes.
664
+
665
+ ```ts
666
+ const stream = new ZipTransformStream({ level: 0, zip64: "off" });
667
+ ```
668
+
669
+ It extends:
670
+
671
+ ```ts
672
+ TransformStream<ZipInputEntry, Uint8Array>
673
+ ```
674
+
675
+ ### `openZip(source, options?)`
676
+
677
+ Random-access reader for `Blob`, `File`, `Uint8Array`, or `ArrayBuffer`.
678
+
679
+ ```ts
680
+ const reader = await openZip(file, {
681
+ filenameEncoding: "utf-8",
682
+ pathMode: "strict-package",
683
+ maxArchiveSize: 50 * 1024 * 1024,
684
+ maxEntrySize: 10 * 1024 * 1024
685
+ });
686
+ ```
687
+
688
+ Options:
689
+
690
+ ```ts
691
+ interface ZipReadOptions {
692
+ filenameEncoding?: "cp437" | StandardFilenameEncoding | {
693
+ encoding: string;
694
+ fatal: boolean;
695
+ ignoreBOM: boolean;
696
+ decode(bytes: Uint8Array): string;
697
+ };
698
+ pathMode?: "strict" | "sanitize" | "unsafe" | "strict-package";
699
+ maxArchiveSize?: number;
700
+ maxEntrySize?: number;
701
+ signal?: AbortSignal;
702
+ onProgress?: (progress: ZipProgress) => void;
703
+ }
704
+ ```
705
+
706
+ Returns:
707
+
708
+ ```ts
709
+ interface ZipRandomAccessReader {
710
+ readonly comment?: string;
711
+ readonly entries: readonly ZipRandomAccessEntry[];
712
+ get(path: string): ZipRandomAccessEntry | undefined;
713
+ close(): Promise<void>;
714
+ }
715
+ ```
716
+
717
+ ### `readZipStream(zipStream, options?)`
718
+
719
+ Async iterable reader.
720
+
721
+ ```ts
722
+ for await (const entry of readZipStream(zipBlob.stream())) {
723
+ await entry.skip();
724
+ }
725
+ ```
726
+
727
+ Returns:
728
+
729
+ ```ts
730
+ AsyncIterable<ZipStreamEntry>
731
+ ```
732
+
733
+ ### `ZipInputEntry`
734
+
735
+ ```ts
736
+ interface ZipInputEntry {
737
+ path: string;
738
+ data: string | Uint8Array | ArrayBuffer | Blob | ReadableStream<Uint8Array>;
739
+ method?: "store" | "deflate";
740
+ level?: number;
741
+ meta?: ZipEntryMeta;
742
+ }
743
+ ```
744
+
745
+ ### `ZipSyncInputEntry`
746
+
747
+ ```ts
748
+ interface ZipSyncInputEntry extends Omit<ZipInputEntry, "data"> {
749
+ data: string | Uint8Array | ArrayBuffer;
750
+ }
751
+ ```
752
+
753
+ ### `ZipEntryMeta`
754
+
755
+ ```ts
756
+ interface ZipEntryMeta {
757
+ comment?: string; // per-entry comment (informational)
758
+ extraField?: Uint8Array; // raw, well-formed ZIP extra-field bytes — ⚠ unchecked override
759
+ modifiedAt?: Date; // mtime; defaults to write time; must be a valid Date ≥ 1970
760
+ createdAt?: Date; // defaults to modifiedAt when timestamps includes TimestampMode.Ntfs
761
+ lastAccess?: Date; // defaults to modifiedAt when timestamps includes TimestampMode.Ntfs
762
+ unixPermissions?: number; // Unix permission bits 0o000–0o777; needs the Unix timestamp mode
763
+ dosAttributes?: number; // MS-DOS attribute byte 0x00–0xff; 0x10 must match entry kind; not allowed in Dos|Unix
764
+ externalAttributes?: number; // raw 32-bit external attributes — ⚠ unchecked override
765
+ }
766
+ ```
767
+
768
+ `comment` is an informational per-entry comment. It does not affect extraction.
769
+
770
+ `modifiedAt` is the main entry timestamp and defaults to the current write time
771
+ when omitted. `createdAt` and `lastAccess` are stored only when the `timestamps`
772
+ mode includes `TimestampMode.Ntfs`; in that mode, omitted creation/access times
773
+ default to `modifiedAt`.
774
+
775
+ `unixPermissions` stores the permission portion of a Unix mode, such as `0o644`
776
+ for a regular file or `0o755` for a script or directory. JSZipp adds the
777
+ file-type bits from the entry kind. Use `unixPermissions: 0o755` when that
778
+ permission should survive extraction.
779
+
780
+ `dosAttributes` stores the MS-DOS attribute byte, such as read-only, hidden,
781
+ archive, or directory flags. Use it when you need Windows/DOS-style attributes;
782
+ for ordinary Unix permission restoration, prefer `unixPermissions`.
783
+
784
+ `externalAttributes` is the raw 32-bit Central Directory attribute field behind
785
+ Unix permissions and DOS attributes. Set it only when you need to round-trip an
786
+ exact value from another archive; it overrides the higher-level permission fields.
787
+
788
+ `extraField` appends raw ZIP extra-field records for callers that already know
789
+ the ZIP extra format. It is useful for exact metadata preservation, but most
790
+ callers should leave it unset.
791
+
792
+ `externalAttributes` and `extraField` are unchecked manual overrides. JSZipp
793
+ writes them as supplied and cannot detect every conflict with the entry kind or
794
+ with generated metadata, so prefer `unixPermissions`, `dosAttributes`, and the
795
+ `timestamps` option for normal writes.
796
+
797
+ For field validation and timestamp-mode interactions, see the
798
+ [API reference](pages/api.html#entry-meta). For ZIP-format background on what
799
+ metadata adds bytes, see [ZIP optional metadata](docs/zip-optional-metadata.md).
800
+
801
+ ### `ZipRandomAccessEntry`
802
+
803
+ ```ts
804
+ interface ZipRandomAccessEntry extends ZipEntryMeta {
805
+ readonly path: string;
806
+ readonly size: number;
807
+ readonly compressedSize: number;
808
+ readonly crc32: number;
809
+ readonly isDirectory: boolean;
810
+ stream(): ReadableStream<Uint8Array>;
811
+ text(): Promise<string>;
812
+ bytes(): Promise<Uint8Array>;
813
+ arrayBuffer(): Promise<ArrayBuffer>;
814
+ }
815
+ ```
816
+
817
+ ### `ZipStreamEntry`
818
+
819
+ ```ts
820
+ interface ZipStreamEntry extends ZipEntryMeta {
821
+ readonly path: string;
822
+ readonly size: number | null;
823
+ readonly compressedSize: number | null;
824
+ readonly crc32: number | null;
825
+ readonly isDirectory: boolean;
826
+ stream(): ReadableStream<Uint8Array>;
827
+ text(): Promise<string>;
828
+ bytes(): Promise<Uint8Array>;
829
+ arrayBuffer(): Promise<ArrayBuffer>;
830
+ skip(): Promise<void>;
831
+ }
832
+ ```
833
+
834
+ ## Timestamp Modes and Archive Size
835
+
836
+ ZIP stores timestamps in more than one place, and JSZipp lets you choose which
837
+ with the `timestamps` bitmask (`TimestampMode.Dos` = 1, `Unix` = 2, `Ntfs` = 4;
838
+ default `Dos | Unix`; values outside `0`–`7` are rejected). The legacy MS-DOS
839
+ date/time pair lives in the normal ZIP headers and is **always** written.
840
+
841
+ Every ZIP entry already has two per-entry metadata locations: a local file header
842
+ before the file data, and a Central Directory header near the end of the archive.
843
+ The byte counts below are the **additional timestamp extra-field bytes** JSZipp
844
+ writes into those existing locations. They do not include the base local header,
845
+ Central Directory header, filename bytes, comments, ZIP64 records, EOCD records,
846
+ or compressed file data. For a broader breakdown of ZIP metadata size, see
847
+ [ZIP optional metadata](docs/zip-optional-metadata.md).
848
+
849
+ - **`Dos` (always on).** Two bytes of date plus two of time are already reserved
850
+ in every local header and Central Directory header, so it adds no extra bytes
851
+ beyond the normal per-entry ZIP headers. The tradeoff is fidelity: 2-second
852
+ granularity, no time zone (interpreted as local wall-clock), and a representable
853
+ range of 1980–2107. Dates before 1980 clamp upward; the writer rejects pre-1970
854
+ (negative) dates outright.
855
+ - **`Unix` (`0x5455` Extended Timestamp).** Whole-second UTC mtime. JSZipp writes
856
+ a 9-byte extra-field record in both the local header and Central Directory
857
+ header (**+18 timestamp bytes per entry**). It also lets you set
858
+ `unixPermissions` and makes the archive advertise the Unix host. Skipped for
859
+ dates outside the unsigned 32-bit Unix range (then only DOS applies).
860
+ - **`Ntfs` (`0x000a` NTFS extra).** 100-nanosecond UTC modification, access, and
861
+ creation times. JSZipp writes a 36-byte extra-field record in both headers
862
+ (**+72 timestamp bytes per entry**). When this flag is set, a missing
863
+ `createdAt` or `lastAccess` defaults to `modifiedAt`. It also lets you set
864
+ `dosAttributes`.
865
+
866
+ | `timestamps` | Extra timestamp bytes/entry | mtime precision | createdAt / lastAccess | `unixPermissions` | `dosAttributes` |
867
+ | --- | --- | --- | --- | --- | --- |
868
+ | `Dos` | 0 | 2 s, local | not stored | rejected | allowed |
869
+ | `Dos \| Unix` (default) | +18 | 1 s, UTC | not stored | allowed | rejected |
870
+ | `Dos \| Ntfs` | +72 | 100 ns, UTC | stored (default to mtime) | rejected | allowed |
871
+ | `Dos \| Unix \| Ntfs` | +90 | 100 ns, UTC | stored (default to mtime) | allowed | allowed |
872
+
873
+ `dosAttributes` is rejected for `Dos | Unix` specifically: a Unix-host archive
874
+ that also carried DOS attribute bits would confuse Unix-oriented tools. On read,
875
+ an NTFS extra carrying both creation and last-access times is authoritative;
876
+ otherwise JSZipp prefers the `0x5455` mtime and falls back to the DOS fields. For
877
+ the smallest archive use `Dos` alone; for portable UTC mtime use the default
878
+ `Dos | Unix`; reach for `Ntfs` only when you need sub-second or creation/access
879
+ times, since it is the largest of the three.
880
+
881
+ ## Important Notes
882
+
883
+ - `ZipWriter` defaults to `outputAs: "stream"`. Use `outputAs: "blob"` for the
884
+ easiest browser download flow.
885
+ - `writer.output` is still available for advanced streaming integrations, but
886
+ most apps should use the value returned by `writer.close()`.
887
+ - `ZipWriter`, `ZipTransformStream`, and `readZipStream` expose Web Streams
888
+ shapes but currently consume each entry payload, compression result, and read
889
+ archive into memory before emitting the next ZIP structure.
890
+ - `ZipWriter`, `openZip`, and `readZipStream` accept `AbortSignal`; large
891
+ operations can also report coarse progress with `onProgress`.
892
+ - Encrypted ZIP files are not supported.
893
+ - Unsupported compression methods are rejected.
894
+ - ZIP64 records are supported with JavaScript `number` precision limits.
895
+ - Modification times are always written to the legacy DOS fields. The
896
+ `timestamps` option controls which UTC timestamp extras are added; see
897
+ [Timestamp Modes and Archive Size](#timestamp-modes-and-archive-size) and
898
+ [docs/timezone.md](./docs/timezone.md) for the detailed timezone model.
899
+ - `explicitDirectoryEntries` (default `false`) controls whether the writer
900
+ materializes a standalone entry for every parent directory implied by an
901
+ entry's path (`a/b/c.txt` also emits `a/` and `a/b/`). The default keeps the
902
+ historical behavior — only the directory entries you add yourself are written.
903
+ JSZipp never scans for empty directories, so an empty folder must still be added
904
+ explicitly regardless of this flag.
905
+ - All options that affect the ZIP file specification itself — `level`, `zip64`,
906
+ `comment`, `timestamps`, `pathMode`, and `explicitDirectoryEntries` — live on
907
+ `ZipEncoderOptions`, shared by `ZipWriter` and `ZipTransformStream`. Only the
908
+ output-shaping options (`outputAs`, `mimeType`) are `ZipWriter`-specific.
909
+ - `openZip` and `readZipStream` reject a negative or non-finite `maxArchiveSize`
910
+ or `maxEntrySize`.
911
+ - `readZipStream` currently exposes the forward-iteration API by collecting the
912
+ input stream and parsing the Central Directory first.
913
+
914
+ See [CONTRACT.md](./CONTRACT.md) for the detailed implementation contract and
915
+ current runtime boundaries. See [docs/timezone.md](./docs/timezone.md) for the
916
+ timestamp timezone model.
917
+
918
+ ## Build
919
+
920
+ ```sh
921
+ pnpm install
922
+ pnpm test
923
+ pnpm build
924
+ ```
925
+
926
+ The npm package points at generated files under `dist/`. `prepack` runs the
927
+ build and test suite before `pnpm pack` / `pnpm publish`, so the published
928
+ tarball contains those generated artifacts even if the source repository omits
929
+ them.
930
+
931
+ Build output:
932
+
933
+ - `dist/jszipp.mjs`
934
+ - `dist/jszipp.cjs`
935
+ - `dist/jszipp.umd.js`
936
+ - `dist/jszipp.writer.umd.js`
937
+ - `dist/jszipp.reader.umd.js`
938
+ - `dist/cr61ff58/jszipp.mjs`
939
+ - `dist/cr61ff58/jszipp.cjs`
940
+ - `dist/cr61ff58/jszipp.umd.js`
941
+ - `dist/cr61ff58/jszipp.reader.umd.js`
942
+ - `dist/cr61ff58/jszipp.writer.umd.js`
943
+ - `dist/cr86ff68/jszipp.mjs`
944
+ - `dist/cr86ff68/jszipp.cjs`
945
+ - `dist/cr86ff68/jszipp.umd.js`
946
+ - `dist/cr86ff68/jszipp.reader.umd.js`
947
+ - `dist/cr86ff68/jszipp.writer.umd.js`
948
+ - `dist/index.d.ts`
949
+ - `dist/types.d.ts`
950
+ - `dist/writer.d.ts`
951
+ - `dist/reader.d.ts`
952
+
953
+ ## License
954
+
955
+ MIT