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 +37 -0
- package/README.md +112 -0
- package/dist/archive.d.ts +25 -0
- package/dist/archive.js +340 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +855 -0
- package/dist/help-text.d.ts +1 -0
- package/dist/help-text.js +12 -0
- package/dist/list-checks-data.d.ts +1 -0
- package/dist/list-checks-data.js +11 -0
- package/dist/list-checks-locale-data.d.ts +1 -0
- package/dist/list-checks-locale-data.js +22 -0
- package/dist/locale-messages.d.ts +3 -0
- package/dist/locale-messages.js +346 -0
- package/dist/messages.d.ts +93 -0
- package/dist/messages.js +136 -0
- package/package.json +64 -3
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
|
+
[](https://www.npmjs.com/package/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>;
|
package/dist/archive.js
ADDED
|
@@ -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