@emdzej/csfs-zip 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Michał Jaskólski
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # @emdzej/csfs-zip
2
+
3
+ A zip archive as a file system — read from any backend, by range, without
4
+ unpacking it.
5
+
6
+ _Part of [csfs](https://github.com/emdzej/csfs) — a **c**lient **s**ide **f**ile **s**ystem: one read API over static HTTP, a picked directory,
7
+ OPFS, and inside zip archives._
8
+
9
+ ```ts
10
+ import { zipFromBlob, withArchives } from "@emdzej/csfs-zip";
11
+ import { httpFileSystem } from "@emdzej/csfs-http";
12
+
13
+ // A picked or dropped file
14
+ const fs = withArchives(zipFromBlob(file));
15
+ await fs.read("/inside/photo.png");
16
+
17
+ // Or an archive sitting in another tree, read in place
18
+ const http = withArchives(httpFileSystem("https://example.test/data"));
19
+ await http.read("/media.zip#/photo.png"); // a few Range requests, not 945 MB
20
+ ```
21
+
22
+ Only the central directory and the requested entry are read. Which is the whole
23
+ point: a 945 MB archive on a static host becomes browsable in a tab.
24
+
25
+ ## `#` addressing
26
+
27
+ `withArchives(fs)` makes `#` work on any file system, and nests:
28
+
29
+ ```ts
30
+ await fs.read("/pack.zip#/inside.txt");
31
+ await fs.read("/outer.zip#/inner.zip#/deep.txt");
32
+ ```
33
+
34
+ Paths without a `#` are passed straight through, so wrapping a file system costs
35
+ nothing until someone uses the syntax. Archives are opened once and cached —
36
+ the expensive part is the central directory, not the reads — and cached as the
37
+ _promise_, so two concurrent lookups share one read of it.
38
+
39
+ ## Transparent mounts
40
+
41
+ `withTransparentArchives(fs, mounts)` makes an archive answer for a directory
42
+ that does not exist:
43
+
44
+ ```ts
45
+ const fs = withTransparentArchives(base, [
46
+ { archive: "/drawings.zip", serves: "/drawings", entry: "basename" },
47
+ ]);
48
+
49
+ await fs.read("/drawings/1132/1132C000.png"); // from a flat archive
50
+ ```
51
+
52
+ This is the case that motivated the package. A parts catalogue ships 38,488
53
+ drawings as one flat `drawings.zip` _and_ as a tree bucketed by name, and every
54
+ reference in the data uses the tree's shape. The archive and the extracted
55
+ layout are **different shapes**, and only the tree's author knows how one maps
56
+ onto the other — so a mount _declares_ it. `entry: "basename"` is a fact about
57
+ that archive, not a default.
58
+
59
+ Several archives may serve one directory, which is how a multi-disc data set
60
+ that ships three different `images_1.zip` files is read without renaming
61
+ anything.
62
+
63
+ **A real file always wins**, so a tree that _was_ extracted keeps working, and a
64
+ half-extracted one falls back file by file rather than failing.
65
+
66
+ ## Zip handling is not ours
67
+
68
+ The reader is [`@zip.js/zip.js`](https://github.com/gildas-lormeau/zip.js). The
69
+ format has enough corners to be worth a library: zip64 past 4 GB or 65,535
70
+ entries, data descriptors, cp437 entry names, and — the one that catches people
71
+ — archives whose _local_ headers carry zero sizes and a zero CRC while the
72
+ central directory holds the truth.
73
+
74
+ What this package supplies is `CsFileReader`, a `zip.js` `Reader` over a
75
+ `CsFile`. That one adapter is what lets an archive be read from HTTP, a picked
76
+ directory, OPFS, or `node:fs` without any of them knowing about zip.
77
+
78
+ ## Licence
79
+
80
+ **MIT** — see [LICENSE](https://github.com/emdzej/csfs/blob/main/LICENSE).
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Archives as part of the surrounding file system.
3
+ *
4
+ * Two ways in, because they answer different questions:
5
+ *
6
+ * **`withArchives(fs)`** makes `#` work. `/dessins/100.zip#/1132C000.png`
7
+ * resolves through the archive, and so does `/a.zip#/b.zip#/deep.txt`. The
8
+ * caller has to know the archive is there, which is the honest case when a
9
+ * path comes from a manifest or a link.
10
+ *
11
+ * **`withTransparentArchives(fs, mounts)`** makes an archive answer for a
12
+ * directory that does not exist. A tree may ship `dessins/100.zip` while every
13
+ * reference in the data says `dessins/100/1132/1132C000.png` — the archive and
14
+ * the extracted layout are *different shapes*, and only the tree's author
15
+ * knows how one maps onto the other. So a mount states it rather than guessing.
16
+ *
17
+ * Mounted archives are opened once and cached, because the expensive part is
18
+ * the central directory, not the reads.
19
+ */
20
+ import { dirname, formatPath, statVia, type CsFileSystem } from "@emdzej/csfs-core";
21
+ import { type ZipFileSystemOptions } from "./zip-fs.js";
22
+ /** Which archive stands in for which directory, and how names map. */
23
+ export interface ArchiveMount {
24
+ /** Path of the archive within the host file system. */
25
+ readonly archive: string;
26
+ /** The directory it answers for. */
27
+ readonly serves: string;
28
+ /**
29
+ * How to turn a requested path into an entry name.
30
+ *
31
+ * `"relative"` strips `serves` — the usual case, where the archive mirrors
32
+ * the directory. `"basename"` uses only the last segment, for a flat archive
33
+ * standing in for a nested tree; that is not a corner case, it is how parts
34
+ * catalogues ship their drawings.
35
+ */
36
+ readonly entry?: "relative" | "basename";
37
+ }
38
+ /**
39
+ * Resolve `#` fragments through archives.
40
+ *
41
+ * Read-only, and it adds no behaviour to paths without a `#` — so wrapping a
42
+ * file system costs nothing until someone uses the syntax.
43
+ */
44
+ export declare function withArchives(fs: CsFileSystem, opts?: ZipFileSystemOptions): CsFileSystem;
45
+ /**
46
+ * Make archives answer for directories, per a declared mapping.
47
+ *
48
+ * A real file always wins: a tree that was extracted keeps working, and a
49
+ * half-extracted one falls back file by file rather than failing. Except for
50
+ * *listing* — a directory that exists only inside an archive has no real
51
+ * counterpart to list, so both are merged.
52
+ */
53
+ export declare function withTransparentArchives(fs: CsFileSystem, mounts: readonly ArchiveMount[], opts?: ZipFileSystemOptions): CsFileSystem;
54
+ /** Re-exported for callers building paths. */
55
+ export { formatPath, dirname, statVia };
56
+ //# sourceMappingURL=archives.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archives.d.ts","sourceRoot":"","sources":["../src/archives.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAEL,OAAO,EACP,UAAU,EAGV,OAAO,EAIP,KAAK,YAAY,EAElB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAgC,KAAK,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEtF,sEAAsE;AACtE,MAAM,WAAW,YAAY;IAC3B,uDAAuD;IACvD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,oCAAoC;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;;;;;;;OAOG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,UAAU,GAAG,UAAU,CAAC;CAC1C;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,EAAE,EAAE,YAAY,EAAE,IAAI,CAAC,EAAE,oBAAoB,GAAG,YAAY,CA2ExF;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,EAAE,EAAE,YAAY,EAChB,MAAM,EAAE,SAAS,YAAY,EAAE,EAC/B,IAAI,CAAC,EAAE,oBAAoB,GAC1B,YAAY,CAqGd;AAqCD,8CAA8C;AAC9C,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC"}
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Archives as part of the surrounding file system.
3
+ *
4
+ * Two ways in, because they answer different questions:
5
+ *
6
+ * **`withArchives(fs)`** makes `#` work. `/dessins/100.zip#/1132C000.png`
7
+ * resolves through the archive, and so does `/a.zip#/b.zip#/deep.txt`. The
8
+ * caller has to know the archive is there, which is the honest case when a
9
+ * path comes from a manifest or a link.
10
+ *
11
+ * **`withTransparentArchives(fs, mounts)`** makes an archive answer for a
12
+ * directory that does not exist. A tree may ship `dessins/100.zip` while every
13
+ * reference in the data says `dessins/100/1132/1132C000.png` — the archive and
14
+ * the extracted layout are *different shapes*, and only the tree's author
15
+ * knows how one maps onto the other. So a mount states it rather than guessing.
16
+ *
17
+ * Mounted archives are opened once and cached, because the expensive part is
18
+ * the central directory, not the reads.
19
+ */
20
+ import { basename, dirname, formatPath, normalizePath, parsePath, statVia, } from "@emdzej/csfs-core";
21
+ import { ZipFileSystem, zipFileSystem } from "./zip-fs.js";
22
+ /**
23
+ * Resolve `#` fragments through archives.
24
+ *
25
+ * Read-only, and it adds no behaviour to paths without a `#` — so wrapping a
26
+ * file system costs nothing until someone uses the syntax.
27
+ */
28
+ export function withArchives(fs, opts) {
29
+ const cache = new Map();
30
+ const mount = (path) => {
31
+ const hit = cache.get(path);
32
+ if (hit)
33
+ return hit;
34
+ // Cached as the promise, so two concurrent lookups share one read of the
35
+ // central directory rather than both fetching it.
36
+ const promise = (async () => {
37
+ const file = await resolve(path);
38
+ return file ? zipFileSystem(file, opts) : null;
39
+ })();
40
+ cache.set(path, promise);
41
+ return promise;
42
+ };
43
+ /** Resolve a possibly-nested path down to the file it names. */
44
+ async function resolve(path) {
45
+ const { base, fragments } = parsePath(path);
46
+ if (fragments.length === 0)
47
+ return await fs.file(base);
48
+ let container = base;
49
+ for (let i = 0; i < fragments.length; i++) {
50
+ const inner = fragments[i];
51
+ const archive = await mount(container);
52
+ if (!archive)
53
+ return null;
54
+ if (i === fragments.length - 1)
55
+ return await archive.file(inner);
56
+ // Not the last hop: this fragment names another archive, so it becomes
57
+ // the container for the next one.
58
+ container = `${container}#${inner}`;
59
+ }
60
+ return null;
61
+ }
62
+ return {
63
+ kind: `${fs.kind}+zip`,
64
+ async file(path) {
65
+ return await resolve(path);
66
+ },
67
+ async directory(path) {
68
+ const { base, fragments } = parsePath(path);
69
+ if (fragments.length === 0)
70
+ return await fs.directory(base);
71
+ let container = base;
72
+ for (let i = 0; i < fragments.length - 1; i++) {
73
+ container = `${container}#${fragments[i]}`;
74
+ }
75
+ const archive = await mount(container);
76
+ return archive ? await archive.directory(fragments[fragments.length - 1]) : null;
77
+ },
78
+ async read(path) {
79
+ return (await resolve(path))?.bytes() ?? null;
80
+ },
81
+ async stat(path) {
82
+ const { base, fragments } = parsePath(path);
83
+ if (fragments.length === 0)
84
+ return await fs.stat(base);
85
+ let container = base;
86
+ for (let i = 0; i < fragments.length - 1; i++) {
87
+ container = `${container}#${fragments[i]}`;
88
+ }
89
+ const archive = await mount(container);
90
+ return archive ? await archive.stat(fragments[fragments.length - 1]) : null;
91
+ },
92
+ async directUrl(path) {
93
+ const { fragments } = parsePath(path);
94
+ // An entry inside an archive has no URL of its own: the bytes are
95
+ // compressed inside a larger file, so handing back the archive's URL
96
+ // would load the wrong thing entirely.
97
+ if (fragments.length > 0)
98
+ return null;
99
+ return (await fs.directUrl?.(path)) ?? null;
100
+ },
101
+ };
102
+ }
103
+ /**
104
+ * Make archives answer for directories, per a declared mapping.
105
+ *
106
+ * A real file always wins: a tree that was extracted keeps working, and a
107
+ * half-extracted one falls back file by file rather than failing. Except for
108
+ * *listing* — a directory that exists only inside an archive has no real
109
+ * counterpart to list, so both are merged.
110
+ */
111
+ export function withTransparentArchives(fs, mounts, opts) {
112
+ const normalized = mounts.map((m) => ({
113
+ archive: normalizePath(m.archive),
114
+ serves: normalizePath(m.serves),
115
+ entry: m.entry ?? "relative",
116
+ }));
117
+ const cache = new Map();
118
+ const mount = (archive) => {
119
+ const hit = cache.get(archive);
120
+ if (hit)
121
+ return hit;
122
+ const promise = (async () => {
123
+ const file = await fs.file(archive);
124
+ return file ? zipFileSystem(file, opts) : null;
125
+ })();
126
+ cache.set(archive, promise);
127
+ return promise;
128
+ };
129
+ /** Every mount that could answer for this path. */
130
+ function candidates(path) {
131
+ const p = normalizePath(path);
132
+ const out = [];
133
+ for (const m of normalized) {
134
+ if (p !== m.serves && !p.startsWith(`${m.serves}/`))
135
+ continue;
136
+ // Plural on purpose: several archives can stand in for one directory,
137
+ // which is how a data set that ships `images_1.zip` on three discs is
138
+ // read without renaming its contents.
139
+ const inner = m.entry === "basename" ? `/${basename(p)}` : p.slice(m.serves.length) || "/";
140
+ out.push({ archive: m.archive, inner });
141
+ }
142
+ return out;
143
+ }
144
+ async function fromArchives(path) {
145
+ for (const { archive, inner } of candidates(path)) {
146
+ const zip = await mount(archive);
147
+ const found = await zip?.file(inner);
148
+ if (found)
149
+ return found;
150
+ }
151
+ return null;
152
+ }
153
+ return {
154
+ kind: `${fs.kind}+mounted-zip`,
155
+ async file(path) {
156
+ return (await fs.file(path)) ?? (await fromArchives(path));
157
+ },
158
+ async read(path) {
159
+ return (await this.file(path))?.bytes() ?? null;
160
+ },
161
+ async directory(path) {
162
+ const real = await fs.directory(path);
163
+ const mounted = candidates(path);
164
+ if (mounted.length === 0)
165
+ return real;
166
+ const inners = [];
167
+ for (const { archive, inner } of mounted) {
168
+ // A `basename` mount has no directory structure to contribute: its
169
+ // entries are flat and its shape says nothing about the tree it stands
170
+ // in for, so listing it would invent paths that do not resolve.
171
+ const m = normalized.find((n) => n.archive === archive);
172
+ if (m?.entry === "basename")
173
+ continue;
174
+ const zip = await mount(archive);
175
+ const dir = await zip?.directory(inner);
176
+ if (dir)
177
+ inners.push(dir);
178
+ }
179
+ if (!real && inners.length === 0)
180
+ return null;
181
+ return new MergedDirectory(normalizePath(path), real, inners, this);
182
+ },
183
+ async stat(path) {
184
+ const direct = await fs.stat(path);
185
+ if (direct)
186
+ return direct;
187
+ const file = await fromArchives(path);
188
+ if (file)
189
+ return { kind: "file", name: basename(path), size: file.size };
190
+ const dir = await this.directory(path);
191
+ return dir ? { kind: "directory", name: basename(path), size: 0 } : null;
192
+ },
193
+ async directUrl(path) {
194
+ /*
195
+ * Only for a file that really exists.
196
+ *
197
+ * The inner backend decides, and its answer is already the right one:
198
+ * `HttpFileSystem.directUrl` returns null for a path its manifest does
199
+ * not list. So a path that only a mounted archive can serve gets null
200
+ * here and the caller falls back to a blob — which is correct, because
201
+ * the bytes are inside a zip and no URL addresses them.
202
+ *
203
+ * Asking the inner backend rather than checking `file()` first also
204
+ * keeps this to one lookup, and keeps "is there a real file" a question
205
+ * only the backend answers.
206
+ */
207
+ return (await fs.directUrl?.(path)) ?? null;
208
+ },
209
+ };
210
+ }
211
+ /** A directory whose children come from the real tree and from archives. */
212
+ class MergedDirectory {
213
+ path;
214
+ real;
215
+ inners;
216
+ owner;
217
+ name;
218
+ constructor(path, real, inners, owner) {
219
+ this.path = path;
220
+ this.real = real;
221
+ this.inners = inners;
222
+ this.owner = owner;
223
+ this.name = basename(path);
224
+ }
225
+ async entries() {
226
+ const byName = new Map();
227
+ // Real files first, so an extracted copy wins over an archived one and the
228
+ // sizes reported are the ones on disk.
229
+ for (const source of [this.real, ...this.inners]) {
230
+ if (!source)
231
+ continue;
232
+ for (const e of await source.entries()) {
233
+ if (!byName.has(e.name))
234
+ byName.set(e.name, e);
235
+ }
236
+ }
237
+ return [...byName.values()];
238
+ }
239
+ async file(name) {
240
+ return await this.owner.file(`${this.path}/${name}`);
241
+ }
242
+ async directory(name) {
243
+ return await this.owner.directory(`${this.path}/${name}`);
244
+ }
245
+ }
246
+ /** Re-exported for callers building paths. */
247
+ export { formatPath, dirname, statVia };
248
+ //# sourceMappingURL=archives.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archives.js","sourceRoot":"","sources":["../src/archives.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=blob-zip.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blob-zip.test.d.ts","sourceRoot":"","sources":["../src/blob-zip.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,75 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
6
+ import { BlobWriter, TextReader, Uint8ArrayReader, ZipWriter } from "@zip.js/zip.js";
7
+ import { zipFromBlob } from "./zip-fs.js";
8
+ import { withArchives } from "./archives.js";
9
+ async function makeZip(files) {
10
+ const w = new ZipWriter(new BlobWriter("application/zip"), { useWebWorkers: false });
11
+ for (const f of files) {
12
+ await w.add(f.name, f.bytes ? new Uint8ArrayReader(f.bytes) : new TextReader(f.text ?? ""));
13
+ }
14
+ return new Uint8Array(await (await w.close()).arrayBuffer());
15
+ }
16
+ let dir;
17
+ beforeAll(async () => {
18
+ dir = await mkdtemp(join(tmpdir(), "csfs-blob-"));
19
+ const inner = await makeZip([{ name: "buried.txt", text: "two levels down" }]);
20
+ await writeFile(join(dir, "outer.zip"), await makeZip([
21
+ { name: "top.txt", text: "at the top" },
22
+ { name: "deep/leaf.txt", text: "a leaf" },
23
+ { name: "inner.zip", bytes: inner },
24
+ ]));
25
+ });
26
+ afterAll(async () => {
27
+ await rm(dir, { recursive: true, force: true });
28
+ });
29
+ /** Exactly what a picker, an `<input type="file">` or a drop event hands over. */
30
+ async function pickedFile() {
31
+ const bytes = await readFile(join(dir, "outer.zip"));
32
+ return new File([bytes], "outer.zip", { type: "application/zip" });
33
+ }
34
+ describe("a picked archive as a file system", () => {
35
+ it("mounts a File with no adapter, because a File is a Blob", async () => {
36
+ // The claim the whole contract rests on. If this needed an adapter, the
37
+ // interface would be wrong.
38
+ const fs = zipFromBlob(await pickedFile());
39
+ expect(fs.kind).toBe("zip");
40
+ const root = await fs.directory("/");
41
+ expect((await root.entries()).map((e) => `${e.kind}:${e.name}`).sort()).toEqual([
42
+ "directory:deep",
43
+ "file:inner.zip",
44
+ "file:top.txt",
45
+ ]);
46
+ expect(await (await fs.file("/top.txt")).text()).toBe("at the top");
47
+ expect(await (await fs.file("/deep/leaf.txt")).text()).toBe("a leaf");
48
+ });
49
+ it("takes its path from the file's own name", async () => {
50
+ const fs = zipFromBlob(await pickedFile());
51
+ const file = await fs.file("/top.txt");
52
+ expect(file.path).toBe("/top.txt");
53
+ expect(file.type).toBe("text/plain; charset=utf-8");
54
+ });
55
+ it("reads an archive nested inside the picked one", async () => {
56
+ // Composition worth checking rather than assuming: the outer entry is
57
+ // decompressed to bytes, and those bytes then have to serve as a
58
+ // container in their own right.
59
+ const fs = withArchives(zipFromBlob(await pickedFile()));
60
+ const bytes = await fs.read("/inner.zip#/buried.txt");
61
+ expect(new TextDecoder().decode(bytes)).toBe("two levels down");
62
+ });
63
+ it("reports absence as null, from a blob as from anywhere else", async () => {
64
+ const fs = zipFromBlob(await pickedFile());
65
+ expect(await fs.file("/nope.txt")).toBeNull();
66
+ expect(await fs.directory("/nope")).toBeNull();
67
+ expect(await fs.stat("/nope")).toBeNull();
68
+ });
69
+ it("says what is wrong when the file is not an archive", async () => {
70
+ const notAZip = new File([new TextEncoder().encode("hello")], "x.zip");
71
+ const fs = zipFromBlob(notAZip);
72
+ await expect(fs.file("/anything")).rejects.toThrow(/not a readable zip archive/);
73
+ });
74
+ });
75
+ //# sourceMappingURL=blob-zip.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"blob-zip.test.js","sourceRoot":"","sources":["../src/blob-zip.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export * from "./reader.js";
2
+ export * from "./zip-fs.js";
3
+ export * from "./archives.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from "./reader.js";
2
+ export * from "./zip-fs.js";
3
+ export * from "./archives.js";
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * A `zip.js` reader over a `CsFile`.
3
+ *
4
+ * This is the whole trick behind mounting archives: `zip.js` reads through a
5
+ * `Reader` interface, and a `CsFile` already offers `size` and `slice`. So one
6
+ * small adapter lets an archive be read from *any* backend — over HTTP by
7
+ * range, out of a picked directory, out of OPFS, or out of another archive —
8
+ * without csfs implementing a single byte of the zip format.
9
+ *
10
+ * Zip handling is not reinvented here on purpose. The format has enough
11
+ * corners to be worth a library: zip64 for anything over 4 GB or 65,535
12
+ * entries, the data-descriptor case where sizes trail the data, cp437 versus
13
+ * UTF-8 entry names, and — the one that catches people — archives whose
14
+ * *local* headers carry zero sizes and a zero CRC while the central directory
15
+ * holds the truth. `zip.js` reads the central directory, which is what makes
16
+ * those readable at all.
17
+ */
18
+ import { Reader } from "@zip.js/zip.js";
19
+ import type { CsFile } from "@emdzej/csfs-core";
20
+ /**
21
+ * Presents a `CsFile` to `zip.js`.
22
+ *
23
+ * `readUint8Array` is the only method that touches data, and it slices rather
24
+ * than buffering, so reading a central directory out of a 945 MB archive costs
25
+ * two range reads instead of a download.
26
+ */
27
+ export declare class CsFileReader extends Reader<CsFile> {
28
+ private readonly file;
29
+ constructor(file: CsFile);
30
+ init(): Promise<void>;
31
+ readUint8Array(index: number, length: number): Promise<Uint8Array>;
32
+ }
33
+ //# sourceMappingURL=reader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reader.d.ts","sourceRoot":"","sources":["../src/reader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACxC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAEhD;;;;;;GAMG;AACH,qBAAa,YAAa,SAAQ,MAAM,CAAC,MAAM,CAAC;IAI9C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;gBAElB,IAAI,EAAE,MAAM;IAMT,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAIrB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;CAYlF"}
package/dist/reader.js ADDED
@@ -0,0 +1,51 @@
1
+ /**
2
+ * A `zip.js` reader over a `CsFile`.
3
+ *
4
+ * This is the whole trick behind mounting archives: `zip.js` reads through a
5
+ * `Reader` interface, and a `CsFile` already offers `size` and `slice`. So one
6
+ * small adapter lets an archive be read from *any* backend — over HTTP by
7
+ * range, out of a picked directory, out of OPFS, or out of another archive —
8
+ * without csfs implementing a single byte of the zip format.
9
+ *
10
+ * Zip handling is not reinvented here on purpose. The format has enough
11
+ * corners to be worth a library: zip64 for anything over 4 GB or 65,535
12
+ * entries, the data-descriptor case where sizes trail the data, cp437 versus
13
+ * UTF-8 entry names, and — the one that catches people — archives whose
14
+ * *local* headers carry zero sizes and a zero CRC while the central directory
15
+ * holds the truth. `zip.js` reads the central directory, which is what makes
16
+ * those readable at all.
17
+ */
18
+ import { Reader } from "@zip.js/zip.js";
19
+ /**
20
+ * Presents a `CsFile` to `zip.js`.
21
+ *
22
+ * `readUint8Array` is the only method that touches data, and it slices rather
23
+ * than buffering, so reading a central directory out of a 945 MB archive costs
24
+ * two range reads instead of a download.
25
+ */
26
+ export class CsFileReader extends Reader {
27
+ // Kept alongside rather than read back from the base class: `Reader.value`
28
+ // is not part of zip.js's published types, and relying on an internal would
29
+ // make a patch release able to break this.
30
+ file;
31
+ constructor(file) {
32
+ super(file);
33
+ this.file = file;
34
+ this.size = file.size;
35
+ }
36
+ async init() {
37
+ this.size = this.file.size;
38
+ }
39
+ async readUint8Array(index, length) {
40
+ if (length === 0)
41
+ return new Uint8Array(0);
42
+ const bytes = await this.file.slice(index, index + length).bytes();
43
+ // A short read means the backend clamped a range. Saying so beats handing
44
+ // back a truncated buffer that a parser will misread as corrupt data.
45
+ if (bytes.byteLength < length) {
46
+ throw new Error(`${this.file.path}: wanted ${length} bytes at ${index}, got ${bytes.byteLength}`);
47
+ }
48
+ return bytes;
49
+ }
50
+ }
51
+ //# sourceMappingURL=reader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reader.js","sourceRoot":"","sources":["../src/reader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAGxC;;;;;;GAMG;AACH,MAAM,OAAO,YAAa,SAAQ,MAAc;IAC9C,2EAA2E;IAC3E,4EAA4E;IAC5E,2CAA2C;IAC1B,IAAI,CAAS;IAE9B,YAAY,IAAY;QACtB,KAAK,CAAC,IAAI,CAAC,CAAC;QACZ,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;IACxB,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC;IAC7B,CAAC;IAEQ,KAAK,CAAC,cAAc,CAAC,KAAa,EAAE,MAAc;QACzD,IAAI,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC;QAC3C,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,CAAC,CAAC,KAAK,EAAE,CAAC;QACnE,0EAA0E;QAC1E,sEAAsE;QACtE,IAAI,KAAK,CAAC,UAAU,GAAG,MAAM,EAAE,CAAC;YAC9B,MAAM,IAAI,KAAK,CACb,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,YAAY,MAAM,aAAa,KAAK,SAAS,KAAK,CAAC,UAAU,EAAE,CACjF,CAAC;QACJ,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;CACF"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * An archive, as a file system.
3
+ *
4
+ * A zip stores a flat list of entries whose names happen to contain slashes;
5
+ * it has no directories in the sense a caller means. So the entry list is read
6
+ * once and a tree is synthesised from it — including directories that exist
7
+ * only implicitly, because plenty of archives never write directory entries at
8
+ * all and a tree that omitted them would lose every file inside.
9
+ *
10
+ * The central directory is read on first use and kept. That is one round trip
11
+ * of a couple of megabytes for a large archive — 2.2 MB for 38,488 entries —
12
+ * after which every lookup is a map hit and every read is one range request.
13
+ * Re-reading it per file would cost more than the files.
14
+ */
15
+ import { type FileEntry } from "@zip.js/zip.js";
16
+ import { dirname, type CsDirectory, type CsFile, type CsFileSystem, type CsStat, type BlobLike } from "@emdzej/csfs-core";
17
+ export interface ZipFileSystemOptions {
18
+ /**
19
+ * Match entry names without regard to case.
20
+ *
21
+ * Off by default. Archives built on Windows are inconsistent about case and
22
+ * some data sets rely on that, but two entries differing only in case then
23
+ * become ambiguous — so it is a choice the caller makes knowingly.
24
+ */
25
+ caseInsensitive?: boolean;
26
+ /** Password, for the archives that need one. */
27
+ password?: string;
28
+ }
29
+ interface Node {
30
+ readonly name: string;
31
+ /**
32
+ * Only a `FileEntry` can be read: `Entry` is a union with `DirectoryEntry`,
33
+ * which has no `getData`. Narrowing here rather than at each read means the
34
+ * tree cannot hold something unreadable in a file position.
35
+ */
36
+ readonly entry?: FileEntry;
37
+ readonly children: Map<string, Node>;
38
+ }
39
+ export declare class ZipFileSystem implements CsFileSystem {
40
+ private readonly archive;
41
+ private readonly opts;
42
+ readonly kind = "zip";
43
+ private tree?;
44
+ constructor(archive: CsFile, opts?: ZipFileSystemOptions);
45
+ /** Read the central directory once and build the tree from it. */
46
+ private load;
47
+ private nodeAt;
48
+ /** Read one entry's bytes. */
49
+ private readEntry;
50
+ file(path: string): Promise<CsFile | null>;
51
+ directory(path: string): Promise<CsDirectory | null>;
52
+ read(path: string): Promise<Uint8Array | null>;
53
+ stat(path: string): Promise<CsStat | null>;
54
+ /** Entry names as stored, for callers that want the flat view. */
55
+ names(): Promise<string[]>;
56
+ /** @internal — `ZipDirectory` reaches back for this. */
57
+ entryFile(node: Node, path: string): Promise<CsFile | null>;
58
+ }
59
+ /** Mount an archive. Nothing is read until something is asked for. */
60
+ export declare function zipFileSystem(archive: CsFile, opts?: ZipFileSystemOptions): ZipFileSystem;
61
+ /** Re-exported so callers can build a path without importing core directly. */
62
+ export { dirname };
63
+ /**
64
+ * Mount a `Blob` or a `File` as a file system.
65
+ *
66
+ * This is the whole answer to "can I open a zip the user picked?" — a `File`
67
+ * *is* a `Blob`, so it already satisfies the read contract and needs no
68
+ * adapter. It works from `showOpenFilePicker()`, from `<input type="file">`
69
+ * and from a drop event, and the latter two work in every browser rather than
70
+ * only in Chromium.
71
+ *
72
+ * One caveat worth passing on: a `File` is a snapshot of a path, not a lock on
73
+ * it. If the archive changes on disk after being picked, later reads throw
74
+ * rather than returning stale bytes — which is the right behaviour, but a
75
+ * long-lived page should be ready to ask for it again.
76
+ */
77
+ export declare function zipFromBlob(blob: BlobLike & {
78
+ name?: string;
79
+ }, opts?: ZipFileSystemOptions & {
80
+ path?: string;
81
+ }): ZipFileSystem;
82
+ //# sourceMappingURL=zip-fs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zip-fs.d.ts","sourceRoot":"","sources":["../src/zip-fs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAyB,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AACvE,OAAO,EAKL,OAAO,EAKP,KAAK,WAAW,EAEhB,KAAK,MAAM,EACX,KAAK,YAAY,EACjB,KAAK,MAAM,EACX,KAAK,QAAQ,EACd,MAAM,mBAAmB,CAAC;AAG3B,MAAM,WAAW,oBAAoB;IACnC;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,gDAAgD;IAChD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,IAAI;IACZ,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;CACtC;AAMD,qBAAa,aAAc,YAAW,YAAY;IAK9C,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,IAAI;IALvB,QAAQ,CAAC,IAAI,SAAS;IACtB,OAAO,CAAC,IAAI,CAAC,CAAgB;gBAGV,OAAO,EAAE,MAAM,EACf,IAAI,GAAE,oBAAyB;IAGlD,kEAAkE;IAClE,OAAO,CAAC,IAAI;YAwDE,MAAM;IAUpB,8BAA8B;YAChB,SAAS;IAsBjB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAY1C,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAMpD,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAI9C,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAOhD,kEAAkE;IAC5D,KAAK,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAahC,wDAAwD;IAClD,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;CAKlE;AA8BD,sEAAsE;AACtE,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,oBAAoB,GAAG,aAAa,CAEzF;AAED,+EAA+E;AAC/E,OAAO,EAAE,OAAO,EAAE,CAAC;AAEnB;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,QAAQ,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,EAClC,IAAI,GAAE,oBAAoB,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,GAClD,aAAa,CAGf"}
package/dist/zip-fs.js ADDED
@@ -0,0 +1,217 @@
1
+ /**
2
+ * An archive, as a file system.
3
+ *
4
+ * A zip stores a flat list of entries whose names happen to contain slashes;
5
+ * it has no directories in the sense a caller means. So the entry list is read
6
+ * once and a tree is synthesised from it — including directories that exist
7
+ * only implicitly, because plenty of archives never write directory entries at
8
+ * all and a tree that omitted them would lose every file inside.
9
+ *
10
+ * The central directory is read on first use and kept. That is one round trip
11
+ * of a couple of megabytes for a large archive — 2.2 MB for 38,488 entries —
12
+ * after which every lookup is a map hit and every read is one range request.
13
+ * Re-reading it per file would cost more than the files.
14
+ */
15
+ import { ZipReader } from "@zip.js/zip.js";
16
+ import { BackendError, basename, blobFile, bytesFile, dirname, mimeType, normalizePath, segments, statVia, } from "@emdzej/csfs-core";
17
+ import { CsFileReader } from "./reader.js";
18
+ function keyOf(name, caseInsensitive) {
19
+ return caseInsensitive ? name.toLowerCase() : name;
20
+ }
21
+ export class ZipFileSystem {
22
+ archive;
23
+ opts;
24
+ kind = "zip";
25
+ tree;
26
+ constructor(archive, opts = {}) {
27
+ this.archive = archive;
28
+ this.opts = opts;
29
+ }
30
+ /** Read the central directory once and build the tree from it. */
31
+ load() {
32
+ this.tree ??= (async () => {
33
+ const reader = new ZipReader(new CsFileReader(this.archive), {
34
+ // `zip.js` would otherwise spin up workers, which is wrong for a
35
+ // library: a consumer may already be inside one, and a page that reads
36
+ // three files should not start a worker pool to do it.
37
+ useWebWorkers: false,
38
+ });
39
+ let entries;
40
+ try {
41
+ entries = await reader.getEntries();
42
+ }
43
+ catch (e) {
44
+ throw new BackendError(`not a readable zip archive: ${e instanceof Error ? e.message : String(e)}`, this.archive.path);
45
+ }
46
+ const insensitive = this.opts.caseInsensitive ?? false;
47
+ const root = { name: "", children: new Map() };
48
+ for (const entry of entries) {
49
+ const parts = segments(entry.filename);
50
+ if (parts.length === 0)
51
+ continue;
52
+ let node = root;
53
+ // Every parent is created on the way down, whether or not the archive
54
+ // bothered to store a directory entry for it.
55
+ for (const part of parts.slice(0, -1)) {
56
+ const key = keyOf(part, insensitive);
57
+ let next = node.children.get(key);
58
+ if (!next) {
59
+ next = { name: part, children: new Map() };
60
+ node.children.set(key, next);
61
+ }
62
+ node = next;
63
+ }
64
+ const last = parts[parts.length - 1];
65
+ const key = keyOf(last, insensitive);
66
+ if (entry.directory) {
67
+ if (!node.children.has(key)) {
68
+ node.children.set(key, { name: last, children: new Map() });
69
+ }
70
+ }
71
+ else {
72
+ // A later entry with the same name replaces an earlier one, which is
73
+ // what every unzip does.
74
+ node.children.set(key, {
75
+ name: last,
76
+ entry: entry,
77
+ children: new Map(),
78
+ });
79
+ }
80
+ }
81
+ return root;
82
+ })();
83
+ return this.tree;
84
+ }
85
+ async nodeAt(path) {
86
+ const insensitive = this.opts.caseInsensitive ?? false;
87
+ let node = await this.load();
88
+ for (const part of segments(path)) {
89
+ node = node?.children.get(keyOf(part, insensitive));
90
+ if (!node)
91
+ return null;
92
+ }
93
+ return node ?? null;
94
+ }
95
+ /** Read one entry's bytes. */
96
+ async readEntry(entry, path) {
97
+ const chunks = [];
98
+ const sink = new WritableStream({
99
+ write(chunk) {
100
+ chunks.push(chunk);
101
+ },
102
+ });
103
+ await entry.getData(sink, {
104
+ ...(this.opts.password !== undefined ? { password: this.opts.password } : {}),
105
+ useWebWorkers: false,
106
+ });
107
+ let total = 0;
108
+ for (const c of chunks)
109
+ total += c.byteLength;
110
+ const out = new Uint8Array(total);
111
+ let at = 0;
112
+ for (const c of chunks) {
113
+ out.set(c, at);
114
+ at += c.byteLength;
115
+ }
116
+ return out;
117
+ }
118
+ async file(path) {
119
+ const node = await this.nodeAt(path);
120
+ if (!node?.entry)
121
+ return null;
122
+ const full = normalizePath(path);
123
+ // Decompressed eagerly, and only on request. A `CsFile` promises random
124
+ // access, and a deflated entry has no seekable form — offering a lazy
125
+ // slice would mean re-inflating from the start for every read, which is
126
+ // slower and more surprising than doing it once.
127
+ const bytes = await this.readEntry(node.entry, full);
128
+ return bytesFile(full, bytes, mimeType(full));
129
+ }
130
+ async directory(path) {
131
+ const node = await this.nodeAt(path);
132
+ if (!node || node.entry)
133
+ return null;
134
+ return new ZipDirectory(this, node, normalizePath(path));
135
+ }
136
+ async read(path) {
137
+ return (await this.file(path))?.bytes() ?? null;
138
+ }
139
+ async stat(path) {
140
+ const root = await this.directory("/");
141
+ if (!root)
142
+ return null;
143
+ if (segments(path).length === 0)
144
+ return { kind: "directory", name: "", size: 0 };
145
+ return await statVia(root, path);
146
+ }
147
+ /** Entry names as stored, for callers that want the flat view. */
148
+ async names() {
149
+ const out = [];
150
+ const visit = (node, prefix) => {
151
+ for (const child of node.children.values()) {
152
+ const p = `${prefix}/${child.name}`;
153
+ if (child.entry)
154
+ out.push(p);
155
+ else
156
+ visit(child, p);
157
+ }
158
+ };
159
+ visit(await this.load(), "");
160
+ return out;
161
+ }
162
+ /** @internal — `ZipDirectory` reaches back for this. */
163
+ async entryFile(node, path) {
164
+ if (!node.entry)
165
+ return null;
166
+ const bytes = await this.readEntry(node.entry, path);
167
+ return bytesFile(path, bytes, mimeType(path));
168
+ }
169
+ }
170
+ class ZipDirectory {
171
+ fs;
172
+ node;
173
+ path;
174
+ name;
175
+ constructor(fs, node, path) {
176
+ this.fs = fs;
177
+ this.node = node;
178
+ this.path = path;
179
+ this.name = basename(path);
180
+ }
181
+ async entries() {
182
+ return [...this.node.children.values()].map((child) => child.entry
183
+ ? { kind: "file", name: child.name, size: child.entry.uncompressedSize }
184
+ : { kind: "directory", name: child.name });
185
+ }
186
+ async file(name) {
187
+ return await this.fs.file(`${this.path}/${name}`);
188
+ }
189
+ async directory(name) {
190
+ return await this.fs.directory(`${this.path}/${name}`);
191
+ }
192
+ }
193
+ /** Mount an archive. Nothing is read until something is asked for. */
194
+ export function zipFileSystem(archive, opts) {
195
+ return new ZipFileSystem(archive, opts);
196
+ }
197
+ /** Re-exported so callers can build a path without importing core directly. */
198
+ export { dirname };
199
+ /**
200
+ * Mount a `Blob` or a `File` as a file system.
201
+ *
202
+ * This is the whole answer to "can I open a zip the user picked?" — a `File`
203
+ * *is* a `Blob`, so it already satisfies the read contract and needs no
204
+ * adapter. It works from `showOpenFilePicker()`, from `<input type="file">`
205
+ * and from a drop event, and the latter two work in every browser rather than
206
+ * only in Chromium.
207
+ *
208
+ * One caveat worth passing on: a `File` is a snapshot of a path, not a lock on
209
+ * it. If the archive changes on disk after being picked, later reads throw
210
+ * rather than returning stale bytes — which is the right behaviour, but a
211
+ * long-lived page should be ready to ask for it again.
212
+ */
213
+ export function zipFromBlob(blob, opts = {}) {
214
+ const { path, ...rest } = opts;
215
+ return new ZipFileSystem(blobFile(blob, path, "application/zip"), rest);
216
+ }
217
+ //# sourceMappingURL=zip-fs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zip-fs.js","sourceRoot":"","sources":["../src/zip-fs.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=zip-fs.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zip-fs.test.d.ts","sourceRoot":"","sources":["../src/zip-fs.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,159 @@
1
+ import { openAsBlob } from "node:fs";
2
+ import { mkdtemp, writeFile, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { describe, expect, it, beforeAll, afterAll } from "vitest";
6
+ import { BlobFile } from "@emdzej/csfs-core";
7
+ import { ZipWriter, BlobWriter, TextReader, Uint8ArrayReader } from "@zip.js/zip.js";
8
+ import { zipFileSystem } from "./zip-fs.js";
9
+ import { withArchives, withTransparentArchives } from "./archives.js";
10
+ import { nodeFileSystem } from "@emdzej/csfs-node";
11
+ /** Build a real archive with zip.js, so the fixture is not hand-rolled. */
12
+ async function makeZip(files) {
13
+ const writer = new ZipWriter(new BlobWriter("application/zip"), { useWebWorkers: false });
14
+ for (const f of files) {
15
+ await writer.add(f.name, f.bytes ? new Uint8ArrayReader(f.bytes) : new TextReader(f.text ?? ""));
16
+ }
17
+ const blob = await writer.close();
18
+ return new Uint8Array(await blob.arrayBuffer());
19
+ }
20
+ const fileOf = (path, bytes) => new BlobFile(path, new Blob([bytes]));
21
+ describe("ZipFileSystem", () => {
22
+ it("synthesises directories the archive never stored", async () => {
23
+ // Plenty of archives write no directory entries at all. A tree built only
24
+ // from stored directories would lose every file inside them.
25
+ const zip = await makeZip([
26
+ { name: "deep/nested/one.txt", text: "one" },
27
+ { name: "top.txt", text: "top" },
28
+ ]);
29
+ const fs = zipFileSystem(fileOf("/a.zip", zip));
30
+ const root = await fs.directory("/");
31
+ expect((await root.entries()).map((e) => `${e.kind}:${e.name}`).sort()).toEqual([
32
+ "directory:deep",
33
+ "file:top.txt",
34
+ ]);
35
+ expect(await fs.directory("/deep/nested")).not.toBeNull();
36
+ expect(await (await fs.file("/deep/nested/one.txt")).text()).toBe("one");
37
+ });
38
+ it("reports sizes and reads bytes back exactly", async () => {
39
+ const payload = new Uint8Array(5000).map((_, i) => i % 251);
40
+ const zip = await makeZip([{ name: "blob.bin", bytes: payload }]);
41
+ const fs = zipFileSystem(fileOf("/a.zip", zip));
42
+ const file = await fs.file("/blob.bin");
43
+ expect(file.size).toBe(5000);
44
+ expect(await file.bytes()).toEqual(payload);
45
+ // Slicing a decompressed entry has to work like any other file.
46
+ expect(await file.slice(10, 20).bytes()).toEqual(payload.subarray(10, 20));
47
+ });
48
+ it("returns null rather than throwing for anything absent", async () => {
49
+ const fs = zipFileSystem(fileOf("/a.zip", await makeZip([{ name: "a.txt", text: "a" }])));
50
+ expect(await fs.file("/nope.txt")).toBeNull();
51
+ expect(await fs.directory("/nope")).toBeNull();
52
+ // A directory is not a file and a file is not a directory.
53
+ expect(await fs.directory("/a.txt")).toBeNull();
54
+ });
55
+ it("says what is wrong when handed something that is not an archive", async () => {
56
+ const fs = zipFileSystem(fileOf("/a.zip", new TextEncoder().encode("not a zip")));
57
+ await expect(fs.file("/a.txt")).rejects.toThrow(/not a readable zip archive/);
58
+ });
59
+ it("can match names without regard to case, when asked", async () => {
60
+ const zip = await makeZip([{ name: "MS43.IPO", text: "x" }]);
61
+ expect(await zipFileSystem(fileOf("/a.zip", zip)).file("/ms43.ipo")).toBeNull();
62
+ const insensitive = zipFileSystem(fileOf("/a.zip", zip), { caseInsensitive: true });
63
+ expect(await insensitive.file("/ms43.ipo")).not.toBeNull();
64
+ });
65
+ });
66
+ describe("# addressing", () => {
67
+ let dir;
68
+ beforeAll(async () => {
69
+ dir = await mkdtemp(join(tmpdir(), "csfs-"));
70
+ const inner = await makeZip([{ name: "deep/file.txt", text: "from the inner zip" }]);
71
+ const outer = await makeZip([
72
+ { name: "inner.zip", bytes: inner },
73
+ { name: "plain.txt", text: "beside it" },
74
+ ]);
75
+ await writeFile(join(dir, "outer.zip"), outer);
76
+ await writeFile(join(dir, "loose.txt"), "on disk");
77
+ });
78
+ afterAll(async () => {
79
+ await rm(dir, { recursive: true, force: true });
80
+ });
81
+ it("reads through one archive", async () => {
82
+ const fs = withArchives(nodeFileSystem(dir));
83
+ expect(await fs.read("/outer.zip#/plain.txt").then((b) => new TextDecoder().decode(b))).toBe("beside it");
84
+ });
85
+ it("reads through nested archives", async () => {
86
+ // The reason nesting is supported at all: it costs one loop, and without
87
+ // it someone eventually hits the special case.
88
+ const fs = withArchives(nodeFileSystem(dir));
89
+ const bytes = await fs.read("/outer.zip#/inner.zip#/deep/file.txt");
90
+ expect(new TextDecoder().decode(bytes)).toBe("from the inner zip");
91
+ });
92
+ it("leaves ordinary paths alone", async () => {
93
+ const fs = withArchives(nodeFileSystem(dir));
94
+ expect(new TextDecoder().decode((await fs.read("/loose.txt")))).toBe("on disk");
95
+ expect(await fs.read("/absent.txt")).toBeNull();
96
+ });
97
+ it("lists a directory inside an archive", async () => {
98
+ const fs = withArchives(nodeFileSystem(dir));
99
+ const inside = await fs.directory("/outer.zip#/");
100
+ expect((await inside.entries()).map((e) => e.name).sort()).toEqual([
101
+ "inner.zip",
102
+ "plain.txt",
103
+ ]);
104
+ });
105
+ it("resolves .. without escaping the archive", async () => {
106
+ // Paths arrive from manifests and URLs, so this is untrusted input.
107
+ const fs = withArchives(nodeFileSystem(dir));
108
+ expect(await fs.read("/outer.zip#/../../../etc/passwd")).toBeNull();
109
+ });
110
+ });
111
+ describe("mounted archives", () => {
112
+ let dir;
113
+ beforeAll(async () => {
114
+ dir = await mkdtemp(join(tmpdir(), "csfs-mount-"));
115
+ // A flat archive standing in for a nested tree — the shape a parts
116
+ // catalogue ships its drawings in.
117
+ await writeFile(join(dir, "drawings.zip"), await makeZip([{ name: "1132C000.png", text: "a drawing" }]));
118
+ // Two archives serving one directory with no overlapping names, which is
119
+ // how a multi-disc set ships its illustrations.
120
+ await writeFile(join(dir, "img-1.zip"), await makeZip([{ name: "one.png", text: "first" }]));
121
+ await writeFile(join(dir, "img-2.zip"), await makeZip([{ name: "two.png", text: "second" }]));
122
+ });
123
+ afterAll(async () => {
124
+ await rm(dir, { recursive: true, force: true });
125
+ });
126
+ it("serves a flat archive under a nested path", async () => {
127
+ const fs = withTransparentArchives(nodeFileSystem(dir), [
128
+ { archive: "/drawings.zip", serves: "/drawings", entry: "basename" },
129
+ ]);
130
+ const bytes = await fs.read("/drawings/1132/1132C000.png");
131
+ expect(new TextDecoder().decode(bytes)).toBe("a drawing");
132
+ });
133
+ it("tries every archive that serves a directory", async () => {
134
+ const fs = withTransparentArchives(nodeFileSystem(dir), [
135
+ { archive: "/img-1.zip", serves: "/img", entry: "basename" },
136
+ { archive: "/img-2.zip", serves: "/img", entry: "basename" },
137
+ ]);
138
+ expect(new TextDecoder().decode((await fs.read("/img/one.png")))).toBe("first");
139
+ expect(new TextDecoder().decode((await fs.read("/img/two.png")))).toBe("second");
140
+ expect(await fs.read("/img/three.png")).toBeNull();
141
+ });
142
+ it("prefers a real file, so an extracted tree keeps working", async () => {
143
+ await writeFile(join(dir, "extracted.txt"), "the real one");
144
+ const fs = withTransparentArchives(nodeFileSystem(dir), [
145
+ { archive: "/drawings.zip", serves: "/", entry: "basename" },
146
+ ]);
147
+ expect(new TextDecoder().decode((await fs.read("/extracted.txt")))).toBe("the real one");
148
+ });
149
+ it("stats a file that exists only inside an archive", async () => {
150
+ const fs = withTransparentArchives(nodeFileSystem(dir), [
151
+ { archive: "/drawings.zip", serves: "/drawings", entry: "basename" },
152
+ ]);
153
+ const st = await fs.stat("/drawings/1132/1132C000.png");
154
+ expect(st).toEqual({ kind: "file", name: "1132C000.png", size: 9 });
155
+ });
156
+ });
157
+ /** Keep `openAsBlob` referenced: it is what makes the Node backend sliceable. */
158
+ void openAsBlob;
159
+ //# sourceMappingURL=zip-fs.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zip-fs.test.js","sourceRoot":"","sources":["../src/zip-fs.test.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@emdzej/csfs-zip",
3
+ "version": "0.1.0",
4
+ "description": "Mount a zip archive as a file system, read by range",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/emdzej/csfs.git",
9
+ "directory": "packages/zip"
10
+ },
11
+ "homepage": "https://github.com/emdzej/csfs/tree/main/packages/zip#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/emdzej/csfs/issues"
14
+ },
15
+ "keywords": [
16
+ "filesystem",
17
+ "browser",
18
+ "zip",
19
+ "range-requests",
20
+ "opfs",
21
+ "file-system-access",
22
+ "http"
23
+ ],
24
+ "type": "module",
25
+ "main": "dist/index.js",
26
+ "types": "dist/index.d.ts",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "default": "./dist/index.js"
31
+ }
32
+ },
33
+ "files": [
34
+ "dist"
35
+ ],
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "dependencies": {
40
+ "@zip.js/zip.js": "^2.10.0",
41
+ "@emdzej/csfs-core": "0.1.0"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^22.13.1",
45
+ "@emdzej/csfs-node": "0.1.0"
46
+ },
47
+ "scripts": {
48
+ "build": "tsc -b",
49
+ "typecheck": "tsc --noEmit",
50
+ "clean": "rm -rf dist .turbo *.tsbuildinfo"
51
+ }
52
+ }