epubcheck-standalone-cli 0.0.0-oidc-bootstrap → 5.3.0-build2

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,37 @@
1
+ BSD 3-Clause License
2
+
3
+ This package (epubcheck-standalone-cli) is a command-line interface that mirrors the
4
+ official epubcheck (w3c/epubcheck) CLI, implemented on top of the
5
+ epubcheck-standalone package (which bundles and is derived from epubcheck).
6
+ epubcheck is distributed under the BSD 3-Clause License; this CLI is
7
+ released under the same license.
8
+
9
+ Copyright © 2007 Adobe Systems Incorporated
10
+ Copyright © 2008 IDPF
11
+ Copyright © 2017 W3C (MIT, ERCIM, Keio, Beihang)
12
+ Copyright © 2026 John L. Armstrong IV
13
+
14
+ Redistribution and use in source and binary forms, with or without
15
+ modification, are permitted provided that the following conditions are met:
16
+
17
+ 1. Redistributions of source code must retain the above copyright notice, this
18
+ list of conditions and the following disclaimer.
19
+
20
+ 2. Redistributions in binary form must reproduce the above copyright notice,
21
+ this list of conditions and the following disclaimer in the documentation
22
+ and/or other materials provided with the distribution.
23
+
24
+ 3. Neither the name of the copyright holder nor the names of its
25
+ contributors may be used to endorse or promote products derived from
26
+ this software without specific prior written permission.
27
+
28
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
29
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
30
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
31
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
32
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
33
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
34
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
35
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
36
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
37
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # epubcheck-standalone-cli
2
+
3
+ [![npm version](https://img.shields.io/npm/v/epubcheck-standalone-cli)](https://www.npmjs.com/package/epubcheck-standalone-cli) [![npm downloads](https://img.shields.io/npm/dm/epubcheck-standalone-cli)](https://www.npmjs.com/package/epubcheck-standalone-cli)
4
+
5
+ The official EPUBCheck command line, with no Java to install. It mirrors EPUBCheck's own CLI: the same flags, console text, JSON, XML, and XMP report files, and exit codes. Under the hood it runs the [epubcheck-standalone](https://github.com/jlarmstrongiv/epubcheck-standalone/blob/main/packages/epubcheck-standalone/README.md) engine, EPUBCheck compiled to plain JavaScript with TeaVM, so output matches the Java tool byte for byte.
6
+
7
+ - Validate an EPUB from your terminal with one command
8
+ - No Java, no install: run it straight from `npx`
9
+ - Same flags and same output as the official epubcheck CLI
10
+ - Writes the same JSON, XML, and XMP report files
11
+ - Your ebook never leaves your machine
12
+
13
+ ## Quickstart
14
+
15
+ ```sh
16
+ npx epubcheck-standalone-cli book.epub
17
+ ```
18
+
19
+ That prints the same report the official EPUBCheck prints:
20
+
21
+ ```
22
+ Validating using EPUB version 3.3 rules.
23
+ No errors or warnings detected.
24
+ Messages: 0 fatals / 0 errors / 0 warnings / 0 infos
25
+
26
+ EPUBCheck completed
27
+ ```
28
+
29
+ Install it in a project and call it by its bin name `epubcheck-standalone-cli`. It needs Node.js LTS or newer.
30
+
31
+ Validate a remote book by URL:
32
+
33
+ ```sh
34
+ npx epubcheck-standalone-cli https://example.com/book.epub
35
+ ```
36
+
37
+ The URL itself is the input, exactly like the official tool given a URL, so message locations reference the URL.
38
+
39
+ ## Flags
40
+
41
+ Same short and long forms as the official EPUBCheck CLI.
42
+
43
+ | Flag | What it does |
44
+ | --- | --- |
45
+ | `-j`, `--json <file>` | Write a JSON report to `<file>`. Use `-` for the console. |
46
+ | `-o`, `--out <file>` | Write an XML report to `<file>`. Use `-` for the console. |
47
+ | `-x`, `--xmp <file>` | Write an XMP report to `<file>`. Use `-` for the console. |
48
+ | `-q`, `--quiet` | Silence normal stdout. Message lines, the summary, and any requested report still print. |
49
+ | `-f`, `--fatal` | Show fatal messages only. |
50
+ | `-e`, `--error` | Show error and fatal messages. |
51
+ | `-w`, `--warn` | Show warning, error, and fatal messages. |
52
+ | `-i`, `--info` | Show messages down to info level (the default). |
53
+ | `-u`, `--usage` | Show usage-level messages too, the most detailed level. |
54
+ | `-m`, `--mode <type>` | Validate a single file of the given type: `xhtml`, `opf`, `svg`, `mo`, `nav`. Pair with `-v` to set the EPUB version. |
55
+ | `-p`, `--profile <name>` | Validate against a profile: `default`, `dict`, `edupub`, `idx`, `preview`. |
56
+ | `-s`, `--save` | With `--mode exp`, keep the packaged `.epub` the check builds, written next to the input directory. A check that finds errors deletes it again and says so, like the official tool. |
57
+ | `--locale <tag>` | Show messages in the given language (see below). |
58
+ | `--failonwarnings` | Exit `1` on warnings, not just errors. |
59
+ | `-l`, `--listChecks [<file>]` | Write message ids and severities to `<file>` or the console. Does not validate a book. |
60
+ | `-c`, `--customMessages <file>` | Override message severities from a tab-separated file, to promote, demote, suppress, or reword messages. Pass `none` to turn overrides off. |
61
+ | `--version` | Print the EPUBCheck version, in the `EPUBCheck v<version>` format. |
62
+ | `-h`, `-?`, `--help` | Print the help text. |
63
+
64
+ Only one report format can be requested per run. Asking for two is an error, the same as the official tool.
65
+
66
+ `-v <2.0|3.0>` is accepted so existing scripts keep working, but it does nothing for an `.epub` file: EPUBCheck reads the version from the book itself and the official tool ignores it too. It is separate from `--version`.
67
+
68
+ The saved `.epub` from `--save` matches the official tool's structurally: the same files in the same order, the same checksums, sizes, and timestamps, and `mimetype` stored first. The compressed bytes inside differ, because Node.js and Java ship different compressors. A directory whose packaged form passes 4 GB is written as a valid ZIP64 `.epub`, the same archive the official tool produces.
69
+
70
+ ## Languages
71
+
72
+ `--locale` takes any tag from this list. Untranslated message text falls back to English, exactly as in the official tool.
73
+
74
+ | Tag | Language |
75
+ | --- | --- |
76
+ | `da` | Danish |
77
+ | `de` | German |
78
+ | `en` | English (the default) |
79
+ | `es` | Spanish |
80
+ | `fr` | French |
81
+ | `it` | Italian |
82
+ | `ja` | Japanese |
83
+ | `ko-KR` | Korean |
84
+ | `nl` | Dutch |
85
+ | `pt-BR` | Portuguese (Brazil) |
86
+ | `zh-TW` | Chinese (Traditional) |
87
+
88
+ A region added to one of these languages works too and behaves exactly like the official tool: it resolves to the language above by dropping the region. So `en-US` and `en-GB` show English, `fr-FR` shows French, `de-DE` shows German. Case and separator do not matter (`fr_FR`, `FR-fr`, and `fr-FR` are the same tag).
89
+
90
+ These tags are refused (exit `2`): a bare `ko`, `pt`, or `zh` (those languages ship only with a region), and any language not in the list, such as `pl` or `ru`. The official tool falls back to English for those, but that fallback depends on the host machine's default language, so its result is not guaranteed to be the same on every machine. This tool refuses those tags rather than risk output that differs from the official tool. The `--listChecks` list is the one exception: like the official tool it never refuses a locale there, and an unsupported tag simply falls back to English.
91
+
92
+ ## Exit codes
93
+
94
+ | Code | Meaning |
95
+ | --- | --- |
96
+ | `0` | Valid. No errors, no fatals. Also `0` when the only messages are warnings, unless you passed `--failonwarnings`. |
97
+ | `1` | Errors or fatals found. Also `1` for a bad argument, a missing file, or a validation failure. |
98
+ | `2` | You passed a valid EPUBCheck flag this engine cannot support. The tool stops with a clear message rather than print output that would differ from the official tool. |
99
+
100
+ Codes `0` and `1` match the official EPUBCheck. Code `2` is specific to this tool, and the only flag that triggers it is a `--locale` tag outside the list above (see Languages). Everything else works: single-file `--mode` checks, expanded directories (`--mode exp`), all profiles, every shipped language, custom message overrides, and URL inputs. Validating a plain `.epub` needs none of these flags.
101
+
102
+ ## Relation to the library
103
+
104
+ This tool is a thin command-line front end over the [epubcheck-standalone](https://github.com/jlarmstrongiv/epubcheck-standalone/blob/main/packages/epubcheck-standalone/README.md) library. The library is the engine; this package reproduces EPUBCheck's argument handling, console text, and exit codes on top of it. To validate EPUBs from your own code, use the library directly.
105
+
106
+ ## Versioning
107
+
108
+ The version is the upstream EPUBCheck release baked into the engine, then a build number for this packaging. Same scheme as the library, explained in its [versioning section](https://github.com/jlarmstrongiv/epubcheck-standalone/blob/main/packages/epubcheck-standalone/README.md#versioning).
109
+
110
+ ## License
111
+
112
+ BSD-3-Clause, the same license as EPUBCheck.
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Thresholds at which ZIP64 kicks in. They default to the real 32-bit/16-bit
3
+ * limits (so production output is unchanged), but are overridable so the ZIP64
4
+ * code path can be exercised on tiny inputs in tests. `size` governs the local
5
+ * size fields, the central size/offset fields, and the central-directory
6
+ * size/offset in the EOCD; `entries` governs the entry-count fields. A field
7
+ * uses ZIP64 (and gets the sentinel) exactly when its value is `>= threshold`,
8
+ * matching commons-compress's `>= ZIP64_MAGIC` / `>= ZIP64_MAGIC_SHORT`.
9
+ */
10
+ export interface Zip64Thresholds {
11
+ size?: number;
12
+ entries?: number;
13
+ }
14
+ /**
15
+ * Package `baseDir` (an existing directory) into `<parent>/<name>.epub` beside
16
+ * it, overwriting any existing file there, and return the written path. The
17
+ * base directory is canonicalized first (Archive's makeCanonical), so a
18
+ * symlinked input saves next to the real directory. Throws on any failure; the
19
+ * caller maps that onto the jar's packaging-failure behavior.
20
+ *
21
+ * `thresholds` lets tests lower the ZIP64 trigger so the ZIP64 path runs on
22
+ * tiny inputs; it defaults to the real 32-bit/16-bit limits, keeping production
23
+ * output byte-identical to the pre-ZIP64 writer below 4 GB.
24
+ */
25
+ export declare function createArchive(baseDir: string, thresholds?: Zip64Thresholds): Promise<string>;
@@ -0,0 +1,340 @@
1
+ // Port of com.adobe.epubcheck.util.Archive (epubcheck 5.3.0) for the CLI's
2
+ // `-s`/`--save` flag: package an expanded EPUB directory into
3
+ // `<canonical-parent>/<directory-name>.epub`, laid out the way the jar's
4
+ // commons-compress ZipArchiveOutputStream writes it.
5
+ //
6
+ // Faithful to the jar's observed output (verified against the real jar):
7
+ // - Files are collected in RAW readdir order (Java's File.listFiles order).
8
+ // Node's fs.readdir would NOT match: libuv sorts scandir results, so this
9
+ // walks with the promises fs.opendir (uv_fs_opendir/readdir, NOT scandir),
10
+ // which preserves the on-disk order the jar sees -- the same raw order the old
11
+ // opendirSync gave, since async opendir uses the identical libuv directory API.
12
+ // - `.DS_Store`, `._DS_Store`, `Thumbs.db`, `ehthumbs.db` files and `.svn`/
13
+ // `.git` directories are skipped (Archive.collectFiles, epubcheck issue 256).
14
+ // - A root-level `mimetype` is moved to the front and STORED (method 0) with a
15
+ // precomputed CRC and NO extra fields; every other entry is DEFLATED.
16
+ // - Entry names are UTF-8 with the language-encoding flag (0x0800) set, no
17
+ // unicode-path extra fields (setCreateUnicodeExtraFields(NEVER)).
18
+ // - Deflated entries carry the same timestamp extra fields commons-compress
19
+ // emits from the file's real attributes: extended-timestamp 0x5455 (local:
20
+ // mtime/atime/birthtime seconds; central: mtime only) and NTFS 0x000a
21
+ // (mtime/atime/birthtime as 100 ns FILETIME values).
22
+ // - Symlinks are followed (Java's File.isFile/isDirectory semantics); an entry
23
+ // that is neither a regular file nor a directory is skipped.
24
+ //
25
+ // One deliberate divergence, impossible to close from Node: the DEFLATED
26
+ // streams differ byte-for-byte from the jar's because Node's bundled zlib and
27
+ // the JDK's zlib produce different (both valid) output for the same input.
28
+ // Entry order, names, methods, CRCs, sizes, and timestamps all match.
29
+ //
30
+ // ZIP64 (APPNOTE 6.3.x), emitted the way commons-compress's default
31
+ // Zip64Mode.AsNeeded does on seekable output: classic 32-bit headers are used
32
+ // until a field would actually overflow, and only then are ZIP64 structures
33
+ // added -- per entry and at the end -- so a packaged form that needs more than
34
+ // 4 GB of offset, a 4 GB+ entry, or 65535+ files produces a VALID archive
35
+ // instead of throwing. The trigger crosses over when a size/offset reaches
36
+ // 0xFFFFFFFF or the entry count reaches 0xFFFF (both overridable for tests via
37
+ // the `createArchive` options, which default to the real limits):
38
+ // - Per-entry ZIP64 extended-information extra field (id 0x0001), added only
39
+ // for the entry that needs it, placed FIRST among its extra fields
40
+ // (commons-compress's addAsFirstExtraField). The local header carries it when
41
+ // the entry's own size overflows and always writes BOTH 8-byte sizes
42
+ // (uncompressed then compressed); the central header carries only the values
43
+ // that overflow, in APPNOTE order (uncompressed size, compressed size,
44
+ // local-header offset), with the overflowing 32-bit fields set to the
45
+ // 0xFFFFFFFF sentinel and version-needed bumped to 45.
46
+ // - A ZIP64 end-of-central-directory record (0x06064b50, 56 bytes) + locator
47
+ // (0x07064b50, 20 bytes) before the classic EOCD once any entry used ZIP64 or
48
+ // the central-directory offset/size/entry-count would overflow; the classic
49
+ // EOCD then carries 0xFFFF / 0xFFFFFFFF sentinels in the overflowing fields.
50
+ //
51
+ // Standard for the ZIP64 path: STRUCTURE / field placement mirrors
52
+ // commons-compress AsNeeded, NOT byte-identical output. Byte parity above 4 GB
53
+ // was never achievable anyway -- our DEFLATE stream already differs from the
54
+ // JDK's (see above) -- so the goal is a valid archive whose ZIP64 layout
55
+ // matches the jar's, verified by round-tripping through an independent
56
+ // ZIP64-aware reader. Below the threshold NO ZIP64 structures appear, so
57
+ // sub-4 GB packaging stays byte-identical to the pre-ZIP64 output.
58
+ import { opendir, readFile, realpath, stat, open } from "node:fs/promises";
59
+ import { basename, dirname, join } from "node:path";
60
+ import { crc32, deflateRaw } from "node:zlib";
61
+ import { promisify } from "node:util";
62
+ // zlib exposes only a callback deflateRaw; promisify it so packaging runs on the
63
+ // libuv thread pool without blocking the event loop (async law). Its output is
64
+ // byte-identical to the old deflateRawSync -- same zlib, same default options, a
65
+ // deterministic deflate stream -- which the save-parity structural test confirms.
66
+ const deflateRawAsync = promisify(deflateRaw);
67
+ const IGNORED_FILES = new Set([".DS_Store", "._DS_Store", "Thumbs.db", "ehthumbs.db"]);
68
+ const IGNORED_DIRS = new Set([".svn", ".git"]);
69
+ // Sentinel values written into the classic 32-bit / 16-bit fields to say "the
70
+ // real value lives in a ZIP64 structure" (APPNOTE 4.3.9.2, 4.4.1.4). These are
71
+ // ALSO the true overflow limits commons-compress uses as its AsNeeded trigger.
72
+ const ZIP64_MAGIC = 0xffffffff;
73
+ const ZIP64_MAGIC_SHORT = 0xffff;
74
+ const ZIP64_VERSION = 45; // version-needed-to-extract for a ZIP64 entry/record
75
+ /**
76
+ * ZIP64 local extended-information extra field (id 0x0001): commons-compress
77
+ * always writes BOTH the 8-byte uncompressed and 8-byte compressed size in the
78
+ * local extra when it adds one.
79
+ */
80
+ function zip64LocalExtra(uncompressedSize, compressedSize) {
81
+ const b = Buffer.alloc(4 + 16);
82
+ b.writeUInt16LE(0x0001, 0);
83
+ b.writeUInt16LE(16, 2);
84
+ b.writeBigUInt64LE(BigInt(uncompressedSize), 4);
85
+ b.writeBigUInt64LE(BigInt(compressedSize), 12);
86
+ return b;
87
+ }
88
+ /**
89
+ * ZIP64 central extended-information extra field (id 0x0001): carries ONLY the
90
+ * values that overflow, in the fixed APPNOTE order (uncompressed size,
91
+ * compressed size, local-header offset), matching commons-compress's
92
+ * getCentralDirectoryData(). The two sizes are emitted together (both or
93
+ * neither) when either overflows; the offset is emitted when it overflows. The
94
+ * disk-start-number never overflows here (single disk), so it is never present.
95
+ */
96
+ function zip64CentralExtra(needSizes, uncompressedSize, compressedSize, needOffset, offset) {
97
+ const dataLen = (needSizes ? 16 : 0) + (needOffset ? 8 : 0);
98
+ const b = Buffer.alloc(4 + dataLen);
99
+ b.writeUInt16LE(0x0001, 0);
100
+ b.writeUInt16LE(dataLen, 2);
101
+ let o = 4;
102
+ if (needSizes) {
103
+ b.writeBigUInt64LE(BigInt(uncompressedSize), o);
104
+ b.writeBigUInt64LE(BigInt(compressedSize), o + 8);
105
+ o += 16;
106
+ }
107
+ if (needOffset) {
108
+ b.writeBigUInt64LE(BigInt(offset), o);
109
+ }
110
+ return b;
111
+ }
112
+ /** Archive.collectFiles: depth-first, raw readdir order, junk entries skipped. */
113
+ async function collectFiles(dir, dirName, out) {
114
+ // Drain the whole directory first (for-await auto-closes the Dir on
115
+ // completion), so only one directory handle is open at a time down the
116
+ // recursion. `for await` reads entries via the same libuv opendir/readdir the
117
+ // sync path used, preserving the raw (unsorted) order the jar's File.listFiles
118
+ // sees -- the byte-order the saved zip depends on.
119
+ const names = [];
120
+ const d = await opendir(dir);
121
+ for await (const ent of d)
122
+ names.push(ent.name);
123
+ for (const name of names) {
124
+ const full = join(dir, name);
125
+ let st;
126
+ try {
127
+ st = await stat(full); // follows symlinks, like java.io.File
128
+ }
129
+ catch {
130
+ continue; // dangling symlink: neither isFile nor isDirectory to Java
131
+ }
132
+ if (st.isFile()) {
133
+ if (!IGNORED_FILES.has(name))
134
+ out.push({ name: dirName + name, path: full });
135
+ }
136
+ else if (st.isDirectory()) {
137
+ if (!IGNORED_DIRS.has(name))
138
+ await collectFiles(full, dirName + name + "/", out);
139
+ }
140
+ }
141
+ }
142
+ /** MS-DOS date+time (local time, 2 s resolution) as (date << 16) | time. */
143
+ function dosDateTime(ms) {
144
+ const d = new Date(ms);
145
+ if (d.getFullYear() < 1980)
146
+ return 0x0021 << 16; // 1980-01-01 00:00:00
147
+ const date = ((d.getFullYear() - 1980) << 9) | ((d.getMonth() + 1) << 5) | d.getDate();
148
+ const time = (d.getHours() << 11) | (d.getMinutes() << 5) | (d.getSeconds() >> 1);
149
+ return ((date << 16) | time) >>> 0;
150
+ }
151
+ /** Windows FILETIME (100 ns intervals since 1601-01-01) from unix nanoseconds. */
152
+ function toFiletime(ns) {
153
+ return ns / 100n + 116444736000000000n;
154
+ }
155
+ /** Unix seconds as the unsigned 32-bit value the 0x5455 field stores. */
156
+ function toUnixSec(ns) {
157
+ return Number(BigInt.asUintN(32, ns / 1000000000n));
158
+ }
159
+ /** NTFS extra field 0x000a (36 bytes): mtime, atime, creation as FILETIME. */
160
+ function writeNtfsExtra(b, off, st) {
161
+ b.writeUInt16LE(0x000a, off);
162
+ b.writeUInt16LE(32, off + 2);
163
+ b.writeUInt32LE(0, off + 4); // reserved
164
+ b.writeUInt16LE(0x0001, off + 8); // tag: times
165
+ b.writeUInt16LE(24, off + 10);
166
+ b.writeBigUInt64LE(toFiletime(st.mtimeNs), off + 12);
167
+ b.writeBigUInt64LE(toFiletime(st.atimeNs), off + 20);
168
+ b.writeBigUInt64LE(toFiletime(st.birthtimeNs), off + 28);
169
+ }
170
+ /** Local-header extra: 0x5455 (flags 7: mtime/atime/creation) + NTFS. */
171
+ function localExtra(st) {
172
+ const b = Buffer.alloc(17 + 36);
173
+ b.writeUInt16LE(0x5455, 0);
174
+ b.writeUInt16LE(13, 2);
175
+ b.writeUInt8(0x07, 4);
176
+ b.writeUInt32LE(toUnixSec(st.mtimeNs), 5);
177
+ b.writeUInt32LE(toUnixSec(st.atimeNs), 9);
178
+ b.writeUInt32LE(toUnixSec(st.birthtimeNs), 13);
179
+ writeNtfsExtra(b, 17, st);
180
+ return b;
181
+ }
182
+ /** Central-directory extra: 0x5455 (flags 7, mtime only) + NTFS. */
183
+ function centralExtra(st) {
184
+ const b = Buffer.alloc(9 + 36);
185
+ b.writeUInt16LE(0x5455, 0);
186
+ b.writeUInt16LE(5, 2);
187
+ b.writeUInt8(0x07, 4);
188
+ b.writeUInt32LE(toUnixSec(st.mtimeNs), 5);
189
+ writeNtfsExtra(b, 9, st);
190
+ return b;
191
+ }
192
+ /**
193
+ * Package `baseDir` (an existing directory) into `<parent>/<name>.epub` beside
194
+ * it, overwriting any existing file there, and return the written path. The
195
+ * base directory is canonicalized first (Archive's makeCanonical), so a
196
+ * symlinked input saves next to the real directory. Throws on any failure; the
197
+ * caller maps that onto the jar's packaging-failure behavior.
198
+ *
199
+ * `thresholds` lets tests lower the ZIP64 trigger so the ZIP64 path runs on
200
+ * tiny inputs; it defaults to the real 32-bit/16-bit limits, keeping production
201
+ * output byte-identical to the pre-ZIP64 writer below 4 GB.
202
+ */
203
+ export async function createArchive(baseDir, thresholds = {}) {
204
+ const sizeThreshold = thresholds.size ?? ZIP64_MAGIC;
205
+ const entryThreshold = thresholds.entries ?? ZIP64_MAGIC_SHORT;
206
+ const canonical = await realpath(baseDir);
207
+ const epubFile = join(dirname(canonical), basename(canonical) + ".epub");
208
+ const entries = [];
209
+ await collectFiles(canonical, "", entries);
210
+ // Make a root-level mimetype the first entry; only then is one STORED.
211
+ const mimetypeIndex = entries.findIndex((e) => e.name === "mimetype");
212
+ if (mimetypeIndex > -1) {
213
+ const [m] = entries.splice(mimetypeIndex, 1);
214
+ entries.unshift(m);
215
+ }
216
+ const fd = await open(epubFile, "w");
217
+ try {
218
+ let offset = 0;
219
+ const central = [];
220
+ // Set once any entry emits a ZIP64 extra: it forces the ZIP64 EOCD, exactly
221
+ // like commons-compress's hasUsedZip64 flag (independent of whether the
222
+ // central-directory offset/size/count themselves overflow).
223
+ let usedZip64 = false;
224
+ for (let i = 0; i < entries.length; i++) {
225
+ const e = entries[i];
226
+ const st = await stat(e.path, { bigint: true }); // attrs BEFORE the read, like the jar
227
+ const data = await readFile(e.path);
228
+ const stored = i === 0 && mimetypeIndex > -1;
229
+ const comp = stored ? data : await deflateRawAsync(data);
230
+ // crc32 is a synchronous pure-CPU checksum with no async equivalent
231
+ // (async law's pure-CPU exemption); it runs on the already-in-memory buffer.
232
+ const crc = crc32(data);
233
+ const name = Buffer.from(e.name, "utf8");
234
+ const dos = dosDateTime(Number(st.mtimeNs / 1000000n));
235
+ const method = stored ? 0 : 8;
236
+ // AsNeeded ZIP64 decision, per field. The local header carries a ZIP64
237
+ // extra (both sizes) when the entry's own size overflows; the central
238
+ // header additionally carries the local-header offset when THAT overflows.
239
+ // The local header has no offset field, so an offset-only overflow leaves
240
+ // the local header classic (version 20, real 32-bit sizes) -- exactly what
241
+ // commons-compress does, since it only learns the offset overflow while
242
+ // building the central header.
243
+ const needSizes = data.length >= sizeThreshold || comp.length >= sizeThreshold;
244
+ const needOffset = offset >= sizeThreshold;
245
+ if (needSizes || needOffset)
246
+ usedZip64 = true;
247
+ const baseExtraL = stored ? Buffer.alloc(0) : localExtra(st);
248
+ const baseExtraC = stored ? Buffer.alloc(0) : centralExtra(st);
249
+ const extraL = needSizes
250
+ ? Buffer.concat([zip64LocalExtra(data.length, comp.length), baseExtraL])
251
+ : baseExtraL;
252
+ const extraC = needSizes || needOffset
253
+ ? Buffer.concat([
254
+ zip64CentralExtra(needSizes, data.length, comp.length, needOffset, offset),
255
+ baseExtraC,
256
+ ])
257
+ : baseExtraC;
258
+ const classicVersion = stored ? 10 : 20;
259
+ const localVersion = needSizes ? ZIP64_VERSION : classicVersion;
260
+ const centralVersion = needSizes || needOffset ? ZIP64_VERSION : classicVersion;
261
+ const lfhComp = needSizes ? ZIP64_MAGIC : comp.length;
262
+ const lfhUncomp = needSizes ? ZIP64_MAGIC : data.length;
263
+ const lfh = Buffer.alloc(30);
264
+ lfh.writeUInt32LE(0x04034b50, 0);
265
+ lfh.writeUInt16LE(localVersion, 4);
266
+ lfh.writeUInt16LE(0x0800, 6); // UTF-8 names
267
+ lfh.writeUInt16LE(method, 8);
268
+ lfh.writeUInt32LE(dos, 10);
269
+ lfh.writeUInt32LE(crc, 14);
270
+ lfh.writeUInt32LE(lfhComp, 18);
271
+ lfh.writeUInt32LE(lfhUncomp, 22);
272
+ lfh.writeUInt16LE(name.length, 26);
273
+ lfh.writeUInt16LE(extraL.length, 28);
274
+ const cen = Buffer.alloc(46);
275
+ cen.writeUInt32LE(0x02014b50, 0);
276
+ cen.writeUInt16LE(20, 4); // version made by: 2.0, FAT
277
+ cen.writeUInt16LE(centralVersion, 6);
278
+ cen.writeUInt16LE(0x0800, 8);
279
+ cen.writeUInt16LE(method, 10);
280
+ cen.writeUInt32LE(dos, 12);
281
+ cen.writeUInt32LE(crc, 16);
282
+ cen.writeUInt32LE(needSizes ? ZIP64_MAGIC : comp.length, 20);
283
+ cen.writeUInt32LE(needSizes ? ZIP64_MAGIC : data.length, 24);
284
+ cen.writeUInt16LE(name.length, 28);
285
+ cen.writeUInt16LE(extraC.length, 30);
286
+ // comment length, disk start, internal and external attributes: all 0
287
+ cen.writeUInt32LE(needOffset ? ZIP64_MAGIC : offset, 42);
288
+ central.push(cen, name, extraC);
289
+ await fd.write(lfh);
290
+ await fd.write(name);
291
+ await fd.write(extraL);
292
+ await fd.write(comp);
293
+ offset += lfh.length + name.length + extraL.length + comp.length;
294
+ }
295
+ const cdOffset = offset;
296
+ let cdSize = 0;
297
+ for (const b of central) {
298
+ await fd.write(b);
299
+ cdSize += b.length;
300
+ }
301
+ // ZIP64 EOCD record + locator (commons-compress writeZip64CentralDirectory):
302
+ // written when any entry used ZIP64 or the central-directory offset/size or
303
+ // entry count would overflow the classic 32-bit/16-bit EOCD fields.
304
+ const eocdOffsetOverflow = cdOffset >= sizeThreshold;
305
+ const eocdSizeOverflow = cdSize >= sizeThreshold;
306
+ const eocdCountOverflow = entries.length >= entryThreshold;
307
+ if (usedZip64 || eocdOffsetOverflow || eocdSizeOverflow || eocdCountOverflow) {
308
+ const z64eocd = Buffer.alloc(56);
309
+ z64eocd.writeUInt32LE(0x06064b50, 0);
310
+ z64eocd.writeBigUInt64LE(44n, 4); // size of the remainder of this record (56 - 12)
311
+ z64eocd.writeUInt16LE(ZIP64_VERSION, 12); // version made by
312
+ z64eocd.writeUInt16LE(ZIP64_VERSION, 14); // version needed to extract
313
+ z64eocd.writeUInt32LE(0, 16); // number of this disk
314
+ z64eocd.writeUInt32LE(0, 20); // disk with the start of the central directory
315
+ z64eocd.writeBigUInt64LE(BigInt(entries.length), 24); // entries on this disk
316
+ z64eocd.writeBigUInt64LE(BigInt(entries.length), 32); // total entries
317
+ z64eocd.writeBigUInt64LE(BigInt(cdSize), 40); // size of the central directory
318
+ z64eocd.writeBigUInt64LE(BigInt(cdOffset), 48); // offset of the central directory
319
+ await fd.write(z64eocd);
320
+ const z64loc = Buffer.alloc(20);
321
+ z64loc.writeUInt32LE(0x07064b50, 0);
322
+ z64loc.writeUInt32LE(0, 4); // disk with the ZIP64 EOCD record
323
+ z64loc.writeBigUInt64LE(BigInt(cdOffset + cdSize), 8); // offset of the ZIP64 EOCD record
324
+ z64loc.writeUInt32LE(1, 16); // total number of disks
325
+ await fd.write(z64loc);
326
+ }
327
+ const eocdCount = eocdCountOverflow ? ZIP64_MAGIC_SHORT : entries.length;
328
+ const eocd = Buffer.alloc(22);
329
+ eocd.writeUInt32LE(0x06054b50, 0);
330
+ eocd.writeUInt16LE(eocdCount, 8);
331
+ eocd.writeUInt16LE(eocdCount, 10);
332
+ eocd.writeUInt32LE(eocdSizeOverflow ? ZIP64_MAGIC : cdSize, 12);
333
+ eocd.writeUInt32LE(eocdOffsetOverflow ? ZIP64_MAGIC : cdOffset, 16);
334
+ await fd.write(eocd);
335
+ }
336
+ finally {
337
+ await fd.close();
338
+ }
339
+ return epubFile;
340
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export declare function run(args: string[]): Promise<number>;