@vibe-agent-toolkit/utils 0.2.0-rc.11 → 0.2.0-rc.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +111 -9
- package/dist/archive/archive.d.ts +129 -0
- package/dist/archive/archive.d.ts.map +1 -0
- package/dist/archive/archive.js +223 -0
- package/dist/archive/archive.js.map +1 -0
- package/dist/archive/entries.d.ts +132 -0
- package/dist/archive/entries.d.ts.map +1 -0
- package/dist/archive/entries.js +223 -0
- package/dist/archive/entries.js.map +1 -0
- package/dist/archive/limits.d.ts +39 -0
- package/dist/archive/limits.d.ts.map +1 -0
- package/dist/archive/limits.js +59 -0
- package/dist/archive/limits.js.map +1 -0
- package/dist/archive/records.d.ts +14 -0
- package/dist/archive/records.d.ts.map +1 -0
- package/dist/archive/records.js +3 -0
- package/dist/archive/records.js.map +1 -0
- package/dist/archive/tar-reader.d.ts +18 -0
- package/dist/archive/tar-reader.d.ts.map +1 -0
- package/dist/archive/tar-reader.js +173 -0
- package/dist/archive/tar-reader.js.map +1 -0
- package/dist/archive/zip-reader.d.ts +76 -0
- package/dist/archive/zip-reader.d.ts.map +1 -0
- package/dist/archive/zip-reader.js +376 -0
- package/dist/archive/zip-reader.js.map +1 -0
- package/dist/archive.d.ts +14 -0
- package/dist/archive.d.ts.map +1 -0
- package/dist/archive.js +12 -0
- package/dist/archive.js.map +1 -0
- package/dist/asset-reference.d.ts +17 -1
- package/dist/asset-reference.d.ts.map +1 -1
- package/dist/asset-reference.js +31 -7
- package/dist/asset-reference.js.map +1 -1
- package/dist/dirent-kind.d.ts +13 -40
- package/dist/dirent-kind.d.ts.map +1 -1
- package/dist/dirent-kind.js +21 -69
- package/dist/dirent-kind.js.map +1 -1
- package/dist/entrypoint.d.ts +2 -2
- package/dist/entrypoint.d.ts.map +1 -1
- package/dist/entrypoint.js +4 -3
- package/dist/entrypoint.js.map +1 -1
- package/dist/env-flag.d.ts +2 -0
- package/dist/env-flag.d.ts.map +1 -1
- package/dist/env-flag.js +2 -0
- package/dist/env-flag.js.map +1 -1
- package/dist/errors/confirmed-absent.d.ts +41 -0
- package/dist/errors/confirmed-absent.d.ts.map +1 -0
- package/dist/errors/confirmed-absent.js +43 -0
- package/dist/errors/confirmed-absent.js.map +1 -0
- package/dist/errors/errno-table.d.ts +177 -0
- package/dist/errors/errno-table.d.ts.map +1 -0
- package/dist/errors/errno-table.js +343 -0
- package/dist/errors/errno-table.js.map +1 -0
- package/dist/errors/fs-boundary.d.ts +36 -0
- package/dist/errors/fs-boundary.d.ts.map +1 -0
- package/dist/errors/fs-boundary.js +121 -0
- package/dist/errors/fs-boundary.js.map +1 -0
- package/dist/errors/fs-fault.d.ts +176 -0
- package/dist/errors/fs-fault.d.ts.map +1 -0
- package/dist/errors/fs-fault.js +215 -0
- package/dist/errors/fs-fault.js.map +1 -0
- package/dist/errors/git-errno.d.ts +13 -0
- package/dist/errors/git-errno.d.ts.map +1 -0
- package/dist/errors/git-errno.js +45 -0
- package/dist/errors/git-errno.js.map +1 -0
- package/dist/errors/path-present.d.ts +72 -0
- package/dist/errors/path-present.d.ts.map +1 -0
- package/dist/errors/path-present.js +110 -0
- package/dist/errors/path-present.js.map +1 -0
- package/dist/errors/rag-index-empty.d.ts +16 -0
- package/dist/errors/rag-index-empty.d.ts.map +1 -0
- package/dist/errors/rag-index-empty.js +16 -0
- package/dist/errors/rag-index-empty.js.map +1 -0
- package/dist/errors/suppressed-faults.d.ts +34 -0
- package/dist/errors/suppressed-faults.d.ts.map +1 -0
- package/dist/errors/suppressed-faults.js +66 -0
- package/dist/errors/suppressed-faults.js.map +1 -0
- package/dist/file-crawler.d.ts +13 -18
- package/dist/file-crawler.d.ts.map +1 -1
- package/dist/file-crawler.js +140 -24
- package/dist/file-crawler.js.map +1 -1
- package/dist/fs-utils.d.ts +75 -50
- package/dist/fs-utils.d.ts.map +1 -1
- package/dist/fs-utils.js +148 -158
- package/dist/fs-utils.js.map +1 -1
- package/dist/fs.d.ts +4 -4
- package/dist/fs.d.ts.map +1 -1
- package/dist/fs.js +4 -4
- package/dist/fs.js.map +1 -1
- package/dist/git-snapshot.d.ts +25 -17
- package/dist/git-snapshot.d.ts.map +1 -1
- package/dist/git-snapshot.js +65 -11
- package/dist/git-snapshot.js.map +1 -1
- package/dist/git-tracker.d.ts +11 -3
- package/dist/git-tracker.d.ts.map +1 -1
- package/dist/git-tracker.js +29 -6
- package/dist/git-tracker.js.map +1 -1
- package/dist/git-utils.d.ts.map +1 -1
- package/dist/git-utils.js +7 -35
- package/dist/git-utils.js.map +1 -1
- package/dist/git.d.ts +1 -1
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +1 -1
- package/dist/git.js.map +1 -1
- package/dist/gitignore-checker.d.ts.map +1 -1
- package/dist/gitignore-checker.js +5 -4
- package/dist/gitignore-checker.js.map +1 -1
- package/dist/in-order.d.ts +93 -0
- package/dist/in-order.d.ts.map +1 -0
- package/dist/in-order.js +165 -0
- package/dist/in-order.js.map +1 -0
- package/dist/index.d.ts +20 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +30 -4
- package/dist/index.js.map +1 -1
- package/dist/kill-process-tree.d.ts +43 -0
- package/dist/kill-process-tree.d.ts.map +1 -0
- package/dist/kill-process-tree.js +48 -0
- package/dist/kill-process-tree.js.map +1 -0
- package/dist/link-following.d.ts +115 -0
- package/dist/link-following.d.ts.map +1 -0
- package/dist/link-following.js +283 -0
- package/dist/link-following.js.map +1 -0
- package/dist/listing-refusal.d.ts +44 -0
- package/dist/listing-refusal.d.ts.map +1 -1
- package/dist/listing-refusal.js +79 -2
- package/dist/listing-refusal.js.map +1 -1
- package/dist/name-fold.d.ts +25 -0
- package/dist/name-fold.d.ts.map +1 -0
- package/dist/name-fold.js +40 -0
- package/dist/name-fold.js.map +1 -0
- package/dist/path-containment.d.ts +3 -1
- package/dist/path-containment.d.ts.map +1 -1
- package/dist/path-containment.js +13 -7
- package/dist/path-containment.js.map +1 -1
- package/dist/path-utils.d.ts +14 -17
- package/dist/path-utils.d.ts.map +1 -1
- package/dist/path-utils.js +37 -55
- package/dist/path-utils.js.map +1 -1
- package/dist/process.d.ts +2 -0
- package/dist/process.d.ts.map +1 -1
- package/dist/process.js +2 -0
- package/dist/process.js.map +1 -1
- package/dist/project-utils.d.ts.map +1 -1
- package/dist/project-utils.js +8 -9
- package/dist/project-utils.js.map +1 -1
- package/dist/safe-exec.d.ts +6 -2
- package/dist/safe-exec.d.ts.map +1 -1
- package/dist/safe-exec.js +1 -1
- package/dist/safe-exec.js.map +1 -1
- package/dist/skill-targets.d.ts.map +1 -1
- package/dist/skill-targets.js +3 -1
- package/dist/skill-targets.js.map +1 -1
- package/dist/skill-test/spawn-claude.d.ts +0 -15
- package/dist/skill-test/spawn-claude.d.ts.map +1 -1
- package/dist/skill-test/spawn-claude.js +11 -50
- package/dist/skill-test/spawn-claude.js.map +1 -1
- package/dist/special-file.d.ts +18 -0
- package/dist/special-file.d.ts.map +1 -0
- package/dist/special-file.js +21 -0
- package/dist/special-file.js.map +1 -0
- package/dist/terminating-signals.d.ts +129 -0
- package/dist/terminating-signals.d.ts.map +1 -0
- package/dist/terminating-signals.js +210 -0
- package/dist/terminating-signals.js.map +1 -0
- package/dist/test-helpers.d.ts +9 -5
- package/dist/test-helpers.d.ts.map +1 -1
- package/dist/test-helpers.js +18 -21
- package/dist/test-helpers.js.map +1 -1
- package/dist/test-scratch-guard.d.ts +34 -0
- package/dist/test-scratch-guard.d.ts.map +1 -0
- package/dist/test-scratch-guard.js +45 -0
- package/dist/test-scratch-guard.js.map +1 -0
- package/dist/testing/archive-fixtures.d.ts +147 -0
- package/dist/testing/archive-fixtures.d.ts.map +1 -0
- package/dist/testing/archive-fixtures.js +449 -0
- package/dist/testing/archive-fixtures.js.map +1 -0
- package/dist/testing/executables.d.ts +6 -0
- package/dist/testing/executables.d.ts.map +1 -1
- package/dist/testing/executables.js +20 -9
- package/dist/testing/executables.js.map +1 -1
- package/dist/testing/fault-fs-preload.d.ts +16 -0
- package/dist/testing/fault-fs-preload.d.ts.map +1 -0
- package/dist/testing/fault-fs-preload.js +22 -0
- package/dist/testing/fault-fs-preload.js.map +1 -0
- package/dist/testing/fault-fs.d.ts +115 -0
- package/dist/testing/fault-fs.d.ts.map +1 -0
- package/dist/testing/fault-fs.js +411 -0
- package/dist/testing/fault-fs.js.map +1 -0
- package/dist/testing/fault-spec.d.ts +83 -0
- package/dist/testing/fault-spec.d.ts.map +1 -0
- package/dist/testing/fault-spec.js +132 -0
- package/dist/testing/fault-spec.js.map +1 -0
- package/dist/testing/hostile-tree.d.ts.map +1 -1
- package/dist/testing/hostile-tree.js +3 -4
- package/dist/testing/hostile-tree.js.map +1 -1
- package/dist/testing/platform-gates.d.ts +8 -0
- package/dist/testing/platform-gates.d.ts.map +1 -1
- package/dist/testing/platform-gates.js +24 -0
- package/dist/testing/platform-gates.js.map +1 -1
- package/dist/testing/temp-dir.d.ts +31 -0
- package/dist/testing/temp-dir.d.ts.map +1 -1
- package/dist/testing/temp-dir.js +63 -0
- package/dist/testing/temp-dir.js.map +1 -1
- package/dist/testing/tier-timeout.d.ts +34 -0
- package/dist/testing/tier-timeout.d.ts.map +1 -0
- package/dist/testing/tier-timeout.js +45 -0
- package/dist/testing/tier-timeout.js.map +1 -0
- package/dist/testing/tree-snapshot.d.ts +71 -0
- package/dist/testing/tree-snapshot.d.ts.map +1 -0
- package/dist/testing/tree-snapshot.js +163 -0
- package/dist/testing/tree-snapshot.js.map +1 -0
- package/dist/testing.d.ts +6 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +8 -1
- package/dist/testing.js.map +1 -1
- package/dist/text-file.d.ts +14 -1
- package/dist/text-file.d.ts.map +1 -1
- package/dist/text-file.js +41 -8
- package/dist/text-file.js.map +1 -1
- package/dist/timing-dump.d.ts.map +1 -1
- package/dist/timing-dump.js +2 -10
- package/dist/timing-dump.js.map +1 -1
- package/dist/tree-change/apply-decisions.d.ts +101 -0
- package/dist/tree-change/apply-decisions.d.ts.map +1 -0
- package/dist/tree-change/apply-decisions.js +139 -0
- package/dist/tree-change/apply-decisions.js.map +1 -0
- package/dist/tree-change/apply.d.ts +98 -0
- package/dist/tree-change/apply.d.ts.map +1 -0
- package/dist/tree-change/apply.js +364 -0
- package/dist/tree-change/apply.js.map +1 -0
- package/dist/tree-change/copy-decisions.d.ts +58 -0
- package/dist/tree-change/copy-decisions.d.ts.map +1 -0
- package/dist/tree-change/copy-decisions.js +66 -0
- package/dist/tree-change/copy-decisions.js.map +1 -0
- package/dist/tree-change/copy-tree.d.ts +125 -0
- package/dist/tree-change/copy-tree.d.ts.map +1 -0
- package/dist/tree-change/copy-tree.js +279 -0
- package/dist/tree-change/copy-tree.js.map +1 -0
- package/dist/tree-change/files.d.ts +167 -0
- package/dist/tree-change/files.d.ts.map +1 -0
- package/dist/tree-change/files.js +423 -0
- package/dist/tree-change/files.js.map +1 -0
- package/dist/tree-change/followed-walk.d.ts +54 -0
- package/dist/tree-change/followed-walk.d.ts.map +1 -0
- package/dist/tree-change/followed-walk.js +81 -0
- package/dist/tree-change/followed-walk.js.map +1 -0
- package/dist/tree-change/host-chain.d.ts +48 -0
- package/dist/tree-change/host-chain.d.ts.map +1 -0
- package/dist/tree-change/host-chain.js +85 -0
- package/dist/tree-change/host-chain.js.map +1 -0
- package/dist/tree-change/identity-compare.d.ts +61 -0
- package/dist/tree-change/identity-compare.d.ts.map +1 -0
- package/dist/tree-change/identity-compare.js +115 -0
- package/dist/tree-change/identity-compare.js.map +1 -0
- package/dist/tree-change/identity.d.ts +62 -0
- package/dist/tree-change/identity.d.ts.map +1 -0
- package/dist/tree-change/identity.js +152 -0
- package/dist/tree-change/identity.js.map +1 -0
- package/dist/tree-change/index.d.ts +24 -0
- package/dist/tree-change/index.d.ts.map +1 -0
- package/dist/tree-change/index.js +14 -0
- package/dist/tree-change/index.js.map +1 -0
- package/dist/tree-change/link-chain.d.ts +180 -0
- package/dist/tree-change/link-chain.d.ts.map +1 -0
- package/dist/tree-change/link-chain.js +377 -0
- package/dist/tree-change/link-chain.js.map +1 -0
- package/dist/tree-change/link-containment.d.ts +105 -0
- package/dist/tree-change/link-containment.d.ts.map +1 -0
- package/dist/tree-change/link-containment.js +196 -0
- package/dist/tree-change/link-containment.js.map +1 -0
- package/dist/tree-change/link-walk.d.ts +49 -0
- package/dist/tree-change/link-walk.d.ts.map +1 -0
- package/dist/tree-change/link-walk.js +69 -0
- package/dist/tree-change/link-walk.js.map +1 -0
- package/dist/tree-change/plan.d.ts +206 -0
- package/dist/tree-change/plan.d.ts.map +1 -0
- package/dist/tree-change/plan.js +320 -0
- package/dist/tree-change/plan.js.map +1 -0
- package/dist/tree-change/readable-tree.d.ts +44 -0
- package/dist/tree-change/readable-tree.d.ts.map +1 -0
- package/dist/tree-change/readable-tree.js +52 -0
- package/dist/tree-change/readable-tree.js.map +1 -0
- package/dist/tree-change/rollback-error.d.ts +29 -0
- package/dist/tree-change/rollback-error.d.ts.map +1 -0
- package/dist/tree-change/rollback-error.js +31 -0
- package/dist/tree-change/rollback-error.js.map +1 -0
- package/dist/tree-change/staging-names.d.ts +27 -0
- package/dist/tree-change/staging-names.d.ts.map +1 -0
- package/dist/tree-change/staging-names.js +40 -0
- package/dist/tree-change/staging-names.js.map +1 -0
- package/dist/tree-change/tree-walk.d.ts +81 -0
- package/dist/tree-change/tree-walk.d.ts.map +1 -0
- package/dist/tree-change/tree-walk.js +212 -0
- package/dist/tree-change/tree-walk.js.map +1 -0
- package/dist/win32-namespace.d.ts +24 -0
- package/dist/win32-namespace.d.ts.map +1 -0
- package/dist/win32-namespace.js +31 -0
- package/dist/win32-namespace.js.map +1 -0
- package/dist/yaml/surgical-yaml.d.ts +12 -3
- package/dist/yaml/surgical-yaml.d.ts.map +1 -1
- package/dist/yaml/surgical-yaml.js +16 -6
- package/dist/yaml/surgical-yaml.js.map +1 -1
- package/eslint/README.md +12 -4
- package/eslint/rules/fs-binding-scan.cjs +278 -0
- package/eslint/rules/no-adhoc-errno.cjs +364 -0
- package/eslint/rules/no-bare-symlink-in-tests.cjs +2 -3
- package/eslint/rules/no-blind-catch.cjs +1 -1
- package/eslint/rules/no-destructive-fs.cjs +359 -0
- package/eslint/rules/no-existssync.cjs +70 -0
- package/eslint/rules/no-fragile-entrypoint-guard.cjs +3 -2
- package/eslint/rules/no-link-following-fs.cjs +68 -0
- package/eslint/rules/no-literal-process-exit.cjs +3 -16
- package/eslint/rules/no-literal-test-timeout.cjs +214 -0
- package/eslint/rules/no-raw-signal-listener.cjs +224 -0
- package/eslint/rules/no-stdout-outside-writer.cjs +135 -0
- package/package.json +10 -2
- package/dist/errors/errno.d.ts +0 -51
- package/dist/errors/errno.d.ts.map +0 -1
- package/dist/errors/errno.js +0 -112
- package/dist/errors/errno.js.map +0 -1
- package/eslint/rules/no-fs-promises-cp.cjs +0 -43
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tar reader: every entry of a tar stream (gzipped or plain), parsed in memory and bounded
|
|
3
|
+
* while it is parsed. One of the two files in VAT that import an archive library.
|
|
4
|
+
*/
|
|
5
|
+
import { constants as zlibConstants, gunzipSync } from 'node:zlib';
|
|
6
|
+
import { Parser } from 'tar';
|
|
7
|
+
import { isVatError } from '../errors/vat-error.js';
|
|
8
|
+
import { ARCHIVE_REFUSED_CODE, entryNameOf } from './entries.js';
|
|
9
|
+
import { archiveBytesCap, tooLarge, tooManyEntries, unreadableArchive, unsupportedEntry } from './limits.js';
|
|
10
|
+
/** The tar entry types that are a regular file, and the ones that are a directory. */
|
|
11
|
+
const FILE_TYPES = new Set(['File', 'OldFile', 'ContiguousFile']);
|
|
12
|
+
const DIRECTORY_TYPES = new Set(['Directory', 'GNUDumpDir']);
|
|
13
|
+
/**
|
|
14
|
+
* What an entry is. One the tar library would SKIP — a sparse file, a continuation, a tape volume
|
|
15
|
+
* header, a type byte no tar defines, a metadata record too large to read — is `other`: dropping
|
|
16
|
+
* it silently would install the archive without one of its entries, where `tar xf` writes a file.
|
|
17
|
+
*/
|
|
18
|
+
function kindOf(entry) {
|
|
19
|
+
if (entry.ignore)
|
|
20
|
+
return 'other';
|
|
21
|
+
const type = entry.type;
|
|
22
|
+
if (FILE_TYPES.has(type))
|
|
23
|
+
return 'file';
|
|
24
|
+
if (DIRECTORY_TYPES.has(type))
|
|
25
|
+
return 'directory';
|
|
26
|
+
if (type === 'SymbolicLink')
|
|
27
|
+
return 'link';
|
|
28
|
+
if (type === 'Link')
|
|
29
|
+
return 'hardlink';
|
|
30
|
+
return 'other';
|
|
31
|
+
}
|
|
32
|
+
/** Any of the owner, group or other execute bits. */
|
|
33
|
+
const EXECUTE_BITS = 0o111;
|
|
34
|
+
/** Whether zlib refused to inflate past the cap it was given. */
|
|
35
|
+
function exceededOutputCap(error) {
|
|
36
|
+
return error.code === 'ERR_BUFFER_TOO_LARGE';
|
|
37
|
+
}
|
|
38
|
+
/** Whether zlib ran out of input before the stream ended: a gzip stream cut short. */
|
|
39
|
+
function endedEarly(error) {
|
|
40
|
+
return error.code === 'Z_BUF_ERROR';
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Inflate `bytes` bounded by `limits`; a stream cut short inflates as far as it goes (a sync
|
|
44
|
+
* flush), so the tar layer can name the entry the cut fell in — the answer a plain tarball cut
|
|
45
|
+
* inside an entry gets. A gzip stream cut on an entry boundary is still refused, naming the last
|
|
46
|
+
* entry: gzip records where it ends. A PLAIN tar cut on an entry boundary is not — the reader does
|
|
47
|
+
* not require tar's end-of-archive blocks, so nothing marks it incomplete (documented under
|
|
48
|
+
* `INPUT_UNREADABLE` in `docs/validation-codes.md`).
|
|
49
|
+
*/
|
|
50
|
+
function inflated(bytes, archive, limits) {
|
|
51
|
+
const maxOutputLength = archiveBytesCap(limits);
|
|
52
|
+
try {
|
|
53
|
+
return { stream: gunzipSync(bytes, { maxOutputLength }), truncated: false };
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (exceededOutputCap(error))
|
|
57
|
+
throw tooLarge(archive, limits, error);
|
|
58
|
+
if (!endedEarly(error))
|
|
59
|
+
throw unreadableArchive(archive, `it is not a gzip stream that inflates (${String(error)})`, error);
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
return { stream: gunzipSync(bytes, { maxOutputLength, finishFlush: zlibConstants.Z_SYNC_FLUSH }), truncated: true };
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
throw exceededOutputCap(error) ? tooLarge(archive, limits, error) : unreadableArchive(archive, `it is not a gzip stream that inflates (${String(error)})`, error);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** The magic numbers a compressed stream begins with. */
|
|
69
|
+
const GZIP_MAGIC = [0x1f, 0x8b];
|
|
70
|
+
const ZSTD_MAGIC = [0x28, 0xb5, 0x2f, 0xfd];
|
|
71
|
+
const beginsWith = (bytes, magic) => magic.every((byte, index) => bytes[index] === byte);
|
|
72
|
+
/**
|
|
73
|
+
* The tar stream itself: `bytes` inflated when they are gzip (by their magic number), bounded by
|
|
74
|
+
* what an archive within `limits` can be — so a stream that inflates without end is refused at
|
|
75
|
+
* the bound, never held.
|
|
76
|
+
*
|
|
77
|
+
* ONE layer, and it is this one. The tar library sniffs whatever it is handed for a gzip or a zstd
|
|
78
|
+
* magic of its own and inflates it with no bound of VAT's, so a stream that is still compressed
|
|
79
|
+
* here — a tarball gzipped twice, a zstd stream — is refused, never passed on.
|
|
80
|
+
*/
|
|
81
|
+
function tarStream(bytes, archive, limits) {
|
|
82
|
+
let read = { stream: bytes, truncated: false };
|
|
83
|
+
if (beginsWith(bytes, GZIP_MAGIC)) {
|
|
84
|
+
read = inflated(bytes, archive, limits);
|
|
85
|
+
if (beginsWith(read.stream, GZIP_MAGIC))
|
|
86
|
+
throw unreadableArchive(archive, 'it is compressed more than once (a gzip stream inside a gzip stream), and VAT inflates one layer');
|
|
87
|
+
}
|
|
88
|
+
if (beginsWith(read.stream, ZSTD_MAGIC))
|
|
89
|
+
throw unreadableArchive(archive, 'it is a zstd stream, and VAT reads a tar archive plain or gzipped');
|
|
90
|
+
return read;
|
|
91
|
+
}
|
|
92
|
+
/** One parse of a tar stream, holding the entries and the bounds they are held to. */
|
|
93
|
+
class TarListing {
|
|
94
|
+
records = [];
|
|
95
|
+
/** The first thing the parser said is wrong with the stream. */
|
|
96
|
+
problem;
|
|
97
|
+
#bytes = 0;
|
|
98
|
+
#archive;
|
|
99
|
+
#limits;
|
|
100
|
+
constructor(archive, limits) {
|
|
101
|
+
this.#archive = archive;
|
|
102
|
+
this.#limits = limits;
|
|
103
|
+
}
|
|
104
|
+
/** Take one entry at its header — bounded there, before its body is read. */
|
|
105
|
+
take = (entry) => {
|
|
106
|
+
const kind = kindOf(entry);
|
|
107
|
+
const size = kind === 'file' ? entry.size : 0;
|
|
108
|
+
this.#bytes += size;
|
|
109
|
+
if (this.records.length >= this.#limits.maxEntries)
|
|
110
|
+
throw tooManyEntries(this.#archive, this.#limits);
|
|
111
|
+
if (this.#bytes > this.#limits.maxBytes)
|
|
112
|
+
throw tooLarge(this.#archive, this.#limits);
|
|
113
|
+
// The body arrives as slices of the stream this module holds: kept, not copied. A skipped entry's body is the library's to drain.
|
|
114
|
+
const chunks = [];
|
|
115
|
+
if (!entry.ignore)
|
|
116
|
+
entry.on('data', (chunk) => chunks.push(chunk));
|
|
117
|
+
this.records.push({
|
|
118
|
+
name: entryNameOf(entry.path),
|
|
119
|
+
kind,
|
|
120
|
+
size,
|
|
121
|
+
executable: ((entry.mode ?? 0) & EXECUTE_BITS) !== 0,
|
|
122
|
+
read: () => (chunks.length === 1 && chunks[0] !== undefined ? chunks[0] : Buffer.concat(chunks)),
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
warn = (_code, message) => {
|
|
126
|
+
this.problem ??= message;
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Every entry of the tar archive `bytes`, in archive order, each as the archive spells it ({@link entryNameOf}) — links,
|
|
131
|
+
* devices and unsafe names included: listing judges nothing. Synchronous and in memory.
|
|
132
|
+
*
|
|
133
|
+
* @param bytes - The archive, gzipped or plain
|
|
134
|
+
* @param archive - What the operator named, for a refusal's message
|
|
135
|
+
* @param limits - The bounds the archive is held to as it is parsed
|
|
136
|
+
* @throws ArchiveRefusedError `unreadable` for bytes that are no tar archive; `unsupported`, naming
|
|
137
|
+
* the entry, for one whose entry is cut short; `too-many-entries` or `too-large` for one past `limits`
|
|
138
|
+
*/
|
|
139
|
+
export function readTar(bytes, archive, limits) {
|
|
140
|
+
const listing = new TarListing(archive, limits);
|
|
141
|
+
const { stream, truncated } = tarStream(bytes, archive, limits);
|
|
142
|
+
// The library is told to inflate nothing: `tarStream` has refused every stream it would sniff as compressed.
|
|
143
|
+
const parser = new Parser({ onReadEntry: listing.take, onwarn: listing.warn, brotli: false, zstd: false });
|
|
144
|
+
parser.on('ignoredEntry', listing.take);
|
|
145
|
+
try {
|
|
146
|
+
// In memory the parse is synchronous: every entry has been taken, whole, when `end` returns.
|
|
147
|
+
parser.end(stream);
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
// A bound `take` raised is the refusal; anything else is the library giving up on the bytes.
|
|
151
|
+
if (isVatError(error, ARCHIVE_REFUSED_CODE))
|
|
152
|
+
throw error;
|
|
153
|
+
throw unreadableArchive(archive, `it is not a tar archive VAT can read (${String(error)})`, error);
|
|
154
|
+
}
|
|
155
|
+
// Before the parser's own complaint, which a cut-short body also raises: an entry whose header
|
|
156
|
+
// parsed is in a tarball, one that does not hold what it declares — `unsupported`, not `unreadable`.
|
|
157
|
+
for (const record of listing.records) {
|
|
158
|
+
if (record.kind === 'file' && record.read().length !== record.size) {
|
|
159
|
+
throw unsupportedEntry(archive, record.name, `declares ${record.size} bytes and is cut short at ${record.read().length}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Cut short in the gzip layer on an entry boundary: every entry parsed whole, and the stream
|
|
163
|
+
// still ended early — the tarball is cut short after its last entry.
|
|
164
|
+
const last = listing.records.at(-1);
|
|
165
|
+
if (truncated && last !== undefined)
|
|
166
|
+
throw unsupportedEntry(archive, last.name, 'is the last entry before the gzip stream is cut short');
|
|
167
|
+
if (truncated)
|
|
168
|
+
throw unreadableArchive(archive, 'it is a gzip stream cut short before any tar entry');
|
|
169
|
+
if (listing.problem !== undefined)
|
|
170
|
+
throw unreadableArchive(archive, `it is not a tar archive VAT can read (${listing.problem})`);
|
|
171
|
+
return listing.records;
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=tar-reader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tar-reader.js","sourceRoot":"","sources":["../../src/archive/tar-reader.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ZIP reader and writer: every entry of a ZIP's central directory, each inflated only when
|
|
3
|
+
* asked, and the one ZIP VAT writes. One of the two files in VAT that import an archive library.
|
|
4
|
+
*/
|
|
5
|
+
import { type ArchiveLimits } from './entries.js';
|
|
6
|
+
import type { ArchiveRecord } from './records.js';
|
|
7
|
+
/**
|
|
8
|
+
* One central-directory record as VAT reads it off the bytes: the name, and every field the ZIP
|
|
9
|
+
* library extracts the entry by — where its local header is, the two sizes, and the method.
|
|
10
|
+
*/
|
|
11
|
+
interface CentralRecord {
|
|
12
|
+
readonly name: Buffer;
|
|
13
|
+
readonly offset: number;
|
|
14
|
+
readonly compressedSize: number;
|
|
15
|
+
readonly size: number;
|
|
16
|
+
readonly method: number;
|
|
17
|
+
}
|
|
18
|
+
/** The fields of a {@link CentralRecord} beside its name, as the ZIP library's entry header carries them. */
|
|
19
|
+
declare const EXTENT_FIELDS: readonly ["offset", "compressedSize", "size", "method"];
|
|
20
|
+
/**
|
|
21
|
+
* The second lock on "which directory is this archive's": the ZIP library's OWN answer, held to
|
|
22
|
+
* VAT's. Whatever the two scans do, the entries the library built must be the entries VAT read
|
|
23
|
+
* off the bytes — the same count, the same names in the same order, and each extracted by the
|
|
24
|
+
* same local-header offset, sizes and method — or the archive is refused. A differential neither
|
|
25
|
+
* reading anticipated lands here instead of in an install: so does a per-entry ZIP64 extra field,
|
|
26
|
+
* which the library reads a size or an offset from wherever the central record holds 0xFFFFFFFF.
|
|
27
|
+
*
|
|
28
|
+
* @throws ArchiveRefusedError `unsupported`
|
|
29
|
+
*/
|
|
30
|
+
export declare function sameDirectory(archive: string, records: readonly CentralRecord[], entries: ReadonlyArray<{
|
|
31
|
+
readonly rawEntryName: Buffer;
|
|
32
|
+
readonly header: Readonly<Record<(typeof EXTENT_FIELDS)[number], number>>;
|
|
33
|
+
}>): void;
|
|
34
|
+
/**
|
|
35
|
+
* Every entry of the ZIP archive `bytes`, in central-directory order, each as the archive spells
|
|
36
|
+
* it — links, devices and unsafe names included: listing judges nothing. Nothing is inflated
|
|
37
|
+
* until a record is read, and the reader caps each inflation at the size the entry declares.
|
|
38
|
+
*
|
|
39
|
+
* @param bytes - The archive
|
|
40
|
+
* @param archive - What the operator named, for a refusal's message
|
|
41
|
+
* @param limits - The bounds the archive is held to, by what its entries declare
|
|
42
|
+
* @throws ArchiveRefusedError `unreadable` for bytes that are no ZIP archive at all (they hold
|
|
43
|
+
* neither a local-file-header nor an end-record signature anywhere); `unsupported` for a ZIP VAT
|
|
44
|
+
* will not take — no end record in the last 64 KiB, data before its first local header, ZIP64,
|
|
45
|
+
* an end record or a directory two readers would read differently, one the ZIP reader cannot parse; `name-clash` for one
|
|
46
|
+
* listing a name twice; `too-many-entries` or `too-large` for one past `limits`
|
|
47
|
+
*/
|
|
48
|
+
export declare function readZip(bytes: Buffer, archive: string, limits: ArchiveLimits): ArchiveRecord[];
|
|
49
|
+
/**
|
|
50
|
+
* The ZIP of a directory tree VAT built, in memory: every file under `sourceDir`, named relative
|
|
51
|
+
* to it, each entry made on Unix with a directory `0755` and a file `0644`, or `0755` when the host
|
|
52
|
+
* marks it executable ({@link bytesMadeOnUnix}).
|
|
53
|
+
*
|
|
54
|
+
* @param sourceDir - The directory to archive
|
|
55
|
+
* @returns The archive's bytes
|
|
56
|
+
* @throws the raw errno of a listing or a read the OS refuses, for the caller to classify
|
|
57
|
+
*/
|
|
58
|
+
export declare function zipOfDirectory(sourceDir: string): Buffer;
|
|
59
|
+
/** One entry of a ZIP VAT writes from entries it judged: a directory has no `data`. */
|
|
60
|
+
interface WrittenZipEntry {
|
|
61
|
+
/** Where the entry lands: its segments joined with `/`, no empty, `.` or `..` segment. */
|
|
62
|
+
readonly place: string;
|
|
63
|
+
readonly data: Buffer | undefined;
|
|
64
|
+
readonly executable: boolean;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The ZIP of `entries`, in order, in memory — each a directory or a file with mode `0755` or
|
|
68
|
+
* `0644` by its executable bit, made on Unix on every host ({@link bytesMadeOnUnix}), nothing else
|
|
69
|
+
* of what an archive it came from said.
|
|
70
|
+
*
|
|
71
|
+
* @param entries - The entries, each named by the place it was judged at
|
|
72
|
+
* @returns The archive's bytes
|
|
73
|
+
*/
|
|
74
|
+
export declare function zipOfEntries(entries: readonly WrittenZipEntry[]): Buffer;
|
|
75
|
+
export {};
|
|
76
|
+
//# sourceMappingURL=zip-reader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zip-reader.d.ts","sourceRoot":"","sources":["../../src/archive/zip-reader.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,OAAO,EAAyB,KAAK,aAAa,EAAoC,MAAM,cAAc,CAAC;AAE3G,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAqIlD;;;GAGG;AACH,UAAU,aAAa;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,6GAA6G;AAC7G,QAAA,MAAM,aAAa,yDAA0D,CAAC;AAqG9E;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,SAAS,aAAa,EAAE,EACjC,OAAO,EAAE,aAAa,CAAC;IAAE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAA;CAAE,CAAC,GACnI,IAAI,CAMN;AAsBD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,aAAa,EAAE,CAuB9F;AAkCD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAIxD;AAED,uFAAuF;AACvF,UAAU,eAAe;IACvB,0FAA0F;IAC1F,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;CAC9B;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,SAAS,eAAe,EAAE,GAAG,MAAM,CAQxE"}
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ZIP reader and writer: every entry of a ZIP's central directory, each inflated only when
|
|
3
|
+
* asked, and the one ZIP VAT writes. One of the two files in VAT that import an archive library.
|
|
4
|
+
*/
|
|
5
|
+
import AdmZip from 'adm-zip';
|
|
6
|
+
import { decodeTextContent } from '../text-content.js';
|
|
7
|
+
import { ArchiveRefusedError, entryNameOf } from './entries.js';
|
|
8
|
+
import { assertWithinLimits, tooManyEntries, unreadableArchive } from './limits.js';
|
|
9
|
+
/** The host a ZIP entry was made on, in the high byte of `version made by`; 3 is Unix. */
|
|
10
|
+
const MADE_ON_UNIX = 3;
|
|
11
|
+
/** The file-type bits of a Unix mode, and the types among them. */
|
|
12
|
+
const TYPE_MASK = 0o170000;
|
|
13
|
+
const TYPE_REGULAR = 0o100000;
|
|
14
|
+
const TYPE_DIRECTORY = 0o040000;
|
|
15
|
+
const TYPE_LINK = 0o120000;
|
|
16
|
+
/** Any of the owner, group or other execute bits. */
|
|
17
|
+
const EXECUTE_BITS = 0o111;
|
|
18
|
+
/** The permission bits a ZIP VAT writes gives a directory, an executable file and any other file. */
|
|
19
|
+
const DIRECTORY_MODE = 0o755;
|
|
20
|
+
const EXECUTABLE_MODE = 0o755;
|
|
21
|
+
const FILE_MODE = 0o644;
|
|
22
|
+
/** The Unix mode an entry made on Unix carries in its external attributes; `undefined` for any other host. */
|
|
23
|
+
function unixModeOf(entry) {
|
|
24
|
+
return entry.header.made >>> 8 === MADE_ON_UNIX ? entry.attr >>> 16 : undefined;
|
|
25
|
+
}
|
|
26
|
+
function kindOf(entry, mode) {
|
|
27
|
+
const type = (mode ?? 0) & TYPE_MASK;
|
|
28
|
+
if (type === TYPE_LINK)
|
|
29
|
+
return 'link';
|
|
30
|
+
if (entry.isDirectory || type === TYPE_DIRECTORY)
|
|
31
|
+
return 'directory';
|
|
32
|
+
// No type bits: a ZIP made where there are none, or by a writer that set only permissions.
|
|
33
|
+
return type === 0 || type === TYPE_REGULAR ? 'file' : 'other';
|
|
34
|
+
}
|
|
35
|
+
function recordOf(entry) {
|
|
36
|
+
const mode = unixModeOf(entry);
|
|
37
|
+
const kind = kindOf(entry, mode);
|
|
38
|
+
return {
|
|
39
|
+
name: entryNameOf(entry.entryName),
|
|
40
|
+
kind,
|
|
41
|
+
size: kind === 'file' ? entry.header.size : 0,
|
|
42
|
+
executable: ((mode ?? 0) & EXECUTE_BITS) !== 0,
|
|
43
|
+
read: () => entry.getData(),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/** The end-of-central-directory record and one central-directory record: their signatures and fixed sizes. */
|
|
47
|
+
const END_SIGNATURE = 0x06054b50;
|
|
48
|
+
const END_SIZE = 22;
|
|
49
|
+
const CENTRAL_SIGNATURE = 0x02014b50;
|
|
50
|
+
const CENTRAL_SIZE = 46;
|
|
51
|
+
/** A local file header's fixed size, before its name. */
|
|
52
|
+
const LOCAL_HEADER_SIZE = 30;
|
|
53
|
+
/** The longest comment that may follow the end record, so how far back from the end it can start. */
|
|
54
|
+
const MAX_COMMENT = 0xffff;
|
|
55
|
+
/** The 16-bit entry count a ZIP64 archive writes in place of its real one. */
|
|
56
|
+
const COUNT_IN_ZIP64 = 0xffff;
|
|
57
|
+
/** The ZIP64 end-of-central-directory locator and record: their signatures, and how far before the end record the ZIP library looks for either. */
|
|
58
|
+
const ZIP64_LOCATOR_SIGNATURE = 0x07064b50;
|
|
59
|
+
const ZIP64_RECORD_SIGNATURE = 0x06064b50;
|
|
60
|
+
const ZIP64_LOCATOR_SIZE = 20;
|
|
61
|
+
const NO_ZIP64 = 'which VAT does not read: no skill or plugin archive needs one';
|
|
62
|
+
/**
|
|
63
|
+
* The refusal of a ZIP VAT found the end of and will not take — `unsupported`, never `unreadable`:
|
|
64
|
+
* `unreadable` is kept for bytes that are no ZIP at all, the one thing a lane may choose to pass
|
|
65
|
+
* on to a service that is the authority on them. This is a ZIP, and VAT's answer to it is no.
|
|
66
|
+
*/
|
|
67
|
+
function unsupportedZip(archive, why, cause) {
|
|
68
|
+
return new ArchiveRefusedError(archive, { reason: 'unsupported', why }, { cause });
|
|
69
|
+
}
|
|
70
|
+
/** The two signatures that make bytes a ZIP, as bytes to search for anywhere in them: a local file header's, and the end record's. */
|
|
71
|
+
const LOCAL_SIGNATURE_BYTES = Buffer.from([0x50, 0x4b, 0x03, 0x04]);
|
|
72
|
+
const END_SIGNATURE_BYTES = Buffer.from([0x50, 0x4b, 0x05, 0x06]);
|
|
73
|
+
/**
|
|
74
|
+
* Whether `bytes` carry a ZIP signature at all: a local file header or an end record, ANYWHERE.
|
|
75
|
+
* Bytes with neither are no ZIP (`unreadable`); bytes with either and no end record a reader finds
|
|
76
|
+
* where every reader looks are a ZIP VAT will not take — one padded past that window, cut short,
|
|
77
|
+
* or behind other bytes, which a reader that searches further back, or scans forward for a local
|
|
78
|
+
* header, still takes.
|
|
79
|
+
*/
|
|
80
|
+
function holdsZipSignature(bytes) {
|
|
81
|
+
return bytes.includes(LOCAL_SIGNATURE_BYTES) || bytes.includes(END_SIGNATURE_BYTES);
|
|
82
|
+
}
|
|
83
|
+
/** Where the last end-of-central-directory record starts, searching back from the end as every reader does; `undefined` when there is none. */
|
|
84
|
+
function endRecordAt(bytes) {
|
|
85
|
+
const last = bytes.length - END_SIZE;
|
|
86
|
+
for (let at = last; at >= Math.max(0, last - MAX_COMMENT); at--) {
|
|
87
|
+
if (bytes.readUInt32LE(at) === END_SIGNATURE)
|
|
88
|
+
return at;
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
/** Why a signature at `at` is refused, or `undefined` for bytes that are none of the three a reader looks for. */
|
|
93
|
+
function signatureRefusal(bytes, at) {
|
|
94
|
+
const signature = bytes.readUInt32LE(at);
|
|
95
|
+
if (signature === ZIP64_LOCATOR_SIGNATURE || signature === ZIP64_RECORD_SIGNATURE)
|
|
96
|
+
return `it is a ZIP64 archive (a ZIP64 end-of-central-directory record or locator stands beside its end record), ${NO_ZIP64}`;
|
|
97
|
+
return signature === END_SIGNATURE ? 'it holds more than one end-of-central-directory record where two readers would choose differently' : undefined;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Refuse a ZIP whose end record a second reader could find differently — the first of the two
|
|
101
|
+
* locks on "which directory is this archive's" (the second is {@link sameDirectory}).
|
|
102
|
+
*
|
|
103
|
+
* The ZIP library scans back from 22 bytes before the end of the bytes and takes the FIRST
|
|
104
|
+
* signature it knows: an end record, or a ZIP64 record — which it meets first when one stands in
|
|
105
|
+
* the archive comment — and having found an end record it looks on through the 20 bytes before it
|
|
106
|
+
* for a ZIP64 locator or another end record. VAT's own reading is of the last end record. So the
|
|
107
|
+
* bytes around that record must hold nothing a scan could mistake for one:
|
|
108
|
+
*
|
|
109
|
+
* - the record is the END of the archive: it, and exactly the comment it declares, and nothing after;
|
|
110
|
+
* - no end-record, ZIP64-record or ZIP64-locator signature anywhere after the record's own
|
|
111
|
+
* first byte (its fields, its comment) nor in the 20 bytes before it;
|
|
112
|
+
* - the central directory it names ends exactly where it starts.
|
|
113
|
+
*
|
|
114
|
+
* No archive within this module's limits needs ZIP64, so its frame is refused outright.
|
|
115
|
+
*
|
|
116
|
+
* @throws ArchiveRefusedError `unsupported`
|
|
117
|
+
*/
|
|
118
|
+
function refuseAmbiguousEndRecord(bytes, archive, end) {
|
|
119
|
+
if (end + END_SIZE + bytes.readUInt16LE(end + 20) !== bytes.length) {
|
|
120
|
+
throw unsupportedZip(archive, 'its end-of-central-directory record is not the end of the archive: the comment it declares and the bytes that follow it disagree');
|
|
121
|
+
}
|
|
122
|
+
for (let at = Math.max(0, end - ZIP64_LOCATOR_SIZE); at <= bytes.length - 4; at++) {
|
|
123
|
+
const refusal = at === end ? undefined : signatureRefusal(bytes, at);
|
|
124
|
+
if (refusal !== undefined)
|
|
125
|
+
throw unsupportedZip(archive, refusal);
|
|
126
|
+
}
|
|
127
|
+
if (bytes.readUInt32LE(end + 16) + bytes.readUInt32LE(end + 12) !== end) {
|
|
128
|
+
throw unsupportedZip(archive, 'its central directory does not end where its end-of-central-directory record begins');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** The fields of a {@link CentralRecord} beside its name, as the ZIP library's entry header carries them. */
|
|
132
|
+
const EXTENT_FIELDS = ['offset', 'compressedSize', 'size', 'method'];
|
|
133
|
+
/**
|
|
134
|
+
* The records the central directory lists, read off the bytes — before the ZIP library builds
|
|
135
|
+
* a record for each, and whether or not it would accept them: the count is held to `limits`
|
|
136
|
+
* here, and a name listed twice is found here, by VAT's own reading and not the library's prose.
|
|
137
|
+
* `undefined` for bytes with no end record where a reader looks for one, for {@link readZip} to judge.
|
|
138
|
+
*
|
|
139
|
+
* The count read is the one the library loops over ("entries on this disk"); an end record whose
|
|
140
|
+
* two counts differ, or that defers them to ZIP64 fields, is refused, and so is a directory whose
|
|
141
|
+
* records do not run exactly from its start to the end record — so what is bounded and compared
|
|
142
|
+
* here is exactly what the library goes on to build ({@link refuseAmbiguousEndRecord}).
|
|
143
|
+
*
|
|
144
|
+
* @returns Each record, its name as the bytes the directory holds, in directory order; and where the directory starts
|
|
145
|
+
* @throws ArchiveRefusedError `too-many-entries`; `unsupported` for a ZIP64 archive, or an end
|
|
146
|
+
* record or directory two readers would read differently
|
|
147
|
+
*/
|
|
148
|
+
function directoryRecords(bytes, archive, limits) {
|
|
149
|
+
const end = endRecordAt(bytes);
|
|
150
|
+
if (end === undefined)
|
|
151
|
+
return undefined;
|
|
152
|
+
refuseAmbiguousEndRecord(bytes, archive, end);
|
|
153
|
+
const count = bytes.readUInt16LE(end + 8);
|
|
154
|
+
if (count !== bytes.readUInt16LE(end + 10))
|
|
155
|
+
throw unsupportedZip(archive, 'its end-of-central-directory record counts its entries two ways (on this disk, and in total)');
|
|
156
|
+
if (count === COUNT_IN_ZIP64)
|
|
157
|
+
throw unsupportedZip(archive, `it is a ZIP64 archive (its end record defers the entry count to ZIP64 fields), ${NO_ZIP64}`);
|
|
158
|
+
if (count > limits.maxEntries)
|
|
159
|
+
throw tooManyEntries(archive, limits);
|
|
160
|
+
const records = [];
|
|
161
|
+
let at = bytes.readUInt32LE(end + 16);
|
|
162
|
+
for (let index = 0; index < count; index++) {
|
|
163
|
+
if (at + CENTRAL_SIZE > end || bytes.readUInt32LE(at) !== CENTRAL_SIGNATURE)
|
|
164
|
+
throw unsupportedZip(archive, `its central directory holds no record where its entry ${index + 1} of ${count} should be`);
|
|
165
|
+
const nameEnd = at + CENTRAL_SIZE + bytes.readUInt16LE(at + 28);
|
|
166
|
+
records.push({
|
|
167
|
+
name: bytes.subarray(at + CENTRAL_SIZE, nameEnd),
|
|
168
|
+
method: bytes.readUInt16LE(at + 10),
|
|
169
|
+
compressedSize: bytes.readUInt32LE(at + 20),
|
|
170
|
+
size: bytes.readUInt32LE(at + 24),
|
|
171
|
+
offset: bytes.readUInt32LE(at + 42),
|
|
172
|
+
});
|
|
173
|
+
at = nameEnd + bytes.readUInt16LE(at + 30) + bytes.readUInt16LE(at + 32);
|
|
174
|
+
}
|
|
175
|
+
if (at !== end)
|
|
176
|
+
throw unsupportedZip(archive, `its central directory holds more than the ${count} entries its end record counts`);
|
|
177
|
+
return { records, directoryAt: bytes.readUInt32LE(end + 16) };
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Refuse a ZIP whose first byte is not the first local header its directory names (or, holding
|
|
181
|
+
* no entries, its directory): a self-extracting stub, or any other bytes put before the archive —
|
|
182
|
+
* whether or not its offsets were moved to step over them (`zip -A`): a ZIP no writer VAT takes
|
|
183
|
+
* from makes. ({@link refuseRecordWithoutItsLocalHeader} makes "the first local header its
|
|
184
|
+
* directory names" a header that is really there.)
|
|
185
|
+
*
|
|
186
|
+
* This closes unlisted bytes at the FRONT only: an unlisted local entry between listed ones, or
|
|
187
|
+
* after the last, is not refused. It is no defence on its own and need not be — no lane forwards
|
|
188
|
+
* an operator's ZIP bytes: every lane reads entries by the directory, and `zipOfArchive` re-writes
|
|
189
|
+
* only the entries it judged, so no unlisted entry leaves VAT.
|
|
190
|
+
*
|
|
191
|
+
* @throws ArchiveRefusedError `unsupported`
|
|
192
|
+
*/
|
|
193
|
+
function refuseDataBeforeFirstEntry(archive, { records, directoryAt }) {
|
|
194
|
+
const first = records.reduce((lowest, record) => Math.min(lowest, record.offset), directoryAt);
|
|
195
|
+
if (first !== 0)
|
|
196
|
+
throw unsupportedZip(archive, `it holds ${first} bytes before its first local file header (a self-extracting stub, or other data put before the archive), which a reader that reads local headers from the front would read`);
|
|
197
|
+
}
|
|
198
|
+
/** What a central record holds in a size or offset it defers to a ZIP64 extra field. */
|
|
199
|
+
const DEFERRED_TO_ZIP64 = 0xff_ff_ff_ff;
|
|
200
|
+
/**
|
|
201
|
+
* Refuse a ZIP one of whose central records holds {@link DEFERRED_TO_ZIP64} for a size or its
|
|
202
|
+
* offset. With a ZIP64 extra field the ZIP library reads another value than VAT and
|
|
203
|
+
* {@link sameDirectory} refuses it; with none both read `0xFFFFFFFF` and agree — and a directory,
|
|
204
|
+
* whose sizes are never used and whose data is never read, would pass. ZIP64 is refused outright.
|
|
205
|
+
*
|
|
206
|
+
* @throws ArchiveRefusedError `unsupported` (the archive as a whole; its message names the record)
|
|
207
|
+
*/
|
|
208
|
+
function refuseZip64Fields(archive, records) {
|
|
209
|
+
const deferred = records.find((record) => [record.compressedSize, record.size, record.offset].includes(DEFERRED_TO_ZIP64));
|
|
210
|
+
if (deferred !== undefined)
|
|
211
|
+
throw unsupportedZip(archive, `it is a ZIP64 archive (the central record of \`${decodeTextContent(deferred.name).text}\` holds 0xFFFFFFFF for a size or its offset), ${NO_ZIP64}`);
|
|
212
|
+
}
|
|
213
|
+
/** Whether a local file header for `record` — its signature, then its own name — starts at the offset the record names, before the directory. */
|
|
214
|
+
function holdsItsLocalHeader(bytes, record, directoryAt) {
|
|
215
|
+
const { offset, name } = record;
|
|
216
|
+
if (offset + LOCAL_HEADER_SIZE > directoryAt || !bytes.subarray(offset, offset + LOCAL_SIGNATURE_BYTES.length).equals(LOCAL_SIGNATURE_BYTES))
|
|
217
|
+
return false;
|
|
218
|
+
const nameAt = offset + LOCAL_HEADER_SIZE;
|
|
219
|
+
return nameAt + name.length <= directoryAt && bytes.readUInt16LE(offset + 26) === name.length && bytes.subarray(nameAt, nameAt + name.length).equals(name);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Refuse a ZIP one of whose central records names an offset that does not hold that entry's own
|
|
223
|
+
* local header. The ZIP library reads a local header only when it inflates a file — never for a
|
|
224
|
+
* directory — so without this a directory record "at byte 0" stands over whatever is there, and
|
|
225
|
+
* the directory would be one two readers read differently. Like {@link refuseDataBeforeFirstEntry}
|
|
226
|
+
* it refuses only what a record points at: no lane forwards the operator's bytes, so an unlisted
|
|
227
|
+
* entry elsewhere is dropped by the re-zip, not refused here.
|
|
228
|
+
*
|
|
229
|
+
* @throws ArchiveRefusedError `unsupported` (the archive as a whole; its message names the record)
|
|
230
|
+
*/
|
|
231
|
+
function refuseRecordWithoutItsLocalHeader(bytes, archive, { records, directoryAt }) {
|
|
232
|
+
const headerless = records.find((record) => !holdsItsLocalHeader(bytes, record, directoryAt));
|
|
233
|
+
if (headerless !== undefined)
|
|
234
|
+
throw unsupportedZip(archive, `its central directory says the local file header of \`${decodeTextContent(headerless.name).text}\` is at byte ${headerless.offset}, and that entry's local header does not start there`);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* The second lock on "which directory is this archive's": the ZIP library's OWN answer, held to
|
|
238
|
+
* VAT's. Whatever the two scans do, the entries the library built must be the entries VAT read
|
|
239
|
+
* off the bytes — the same count, the same names in the same order, and each extracted by the
|
|
240
|
+
* same local-header offset, sizes and method — or the archive is refused. A differential neither
|
|
241
|
+
* reading anticipated lands here instead of in an install: so does a per-entry ZIP64 extra field,
|
|
242
|
+
* which the library reads a size or an offset from wherever the central record holds 0xFFFFFFFF.
|
|
243
|
+
*
|
|
244
|
+
* @throws ArchiveRefusedError `unsupported`
|
|
245
|
+
*/
|
|
246
|
+
export function sameDirectory(archive, records, entries) {
|
|
247
|
+
const agree = entries.length === records.length && entries.every((entry, index) => {
|
|
248
|
+
const record = records[index];
|
|
249
|
+
return record?.name.equals(entry.rawEntryName) === true && EXTENT_FIELDS.every((field) => entry.header[field] === record[field]);
|
|
250
|
+
});
|
|
251
|
+
if (!agree)
|
|
252
|
+
throw unsupportedZip(archive, 'its central directory reads two ways: the entries the ZIP reader built are not the ones its end record names');
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
|
|
256
|
+
* Refuse a ZIP whose directory lists one name twice: the archive's own clash — two readers resolve
|
|
257
|
+
* the name to different bytes — and the same `name-clash` a tarball listing a name twice is.
|
|
258
|
+
*
|
|
259
|
+
* @throws ArchiveRefusedError `name-clash`, naming the entry
|
|
260
|
+
*/
|
|
261
|
+
function refuseNameListedTwice(archive, records) {
|
|
262
|
+
const seen = new Set();
|
|
263
|
+
for (const { name: raw } of records) {
|
|
264
|
+
// Compared byte for byte: two names that only DECODE alike are two names.
|
|
265
|
+
const key = raw.toString('hex');
|
|
266
|
+
if (seen.has(key)) {
|
|
267
|
+
const name = decodeTextContent(raw).text;
|
|
268
|
+
throw new ArchiveRefusedError(archive, { reason: 'name-clash', entry: name, why: `it lists \`${name}\` twice` });
|
|
269
|
+
}
|
|
270
|
+
seen.add(key);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Every entry of the ZIP archive `bytes`, in central-directory order, each as the archive spells
|
|
275
|
+
* it — links, devices and unsafe names included: listing judges nothing. Nothing is inflated
|
|
276
|
+
* until a record is read, and the reader caps each inflation at the size the entry declares.
|
|
277
|
+
*
|
|
278
|
+
* @param bytes - The archive
|
|
279
|
+
* @param archive - What the operator named, for a refusal's message
|
|
280
|
+
* @param limits - The bounds the archive is held to, by what its entries declare
|
|
281
|
+
* @throws ArchiveRefusedError `unreadable` for bytes that are no ZIP archive at all (they hold
|
|
282
|
+
* neither a local-file-header nor an end-record signature anywhere); `unsupported` for a ZIP VAT
|
|
283
|
+
* will not take — no end record in the last 64 KiB, data before its first local header, ZIP64,
|
|
284
|
+
* an end record or a directory two readers would read differently, one the ZIP reader cannot parse; `name-clash` for one
|
|
285
|
+
* listing a name twice; `too-many-entries` or `too-large` for one past `limits`
|
|
286
|
+
*/
|
|
287
|
+
export function readZip(bytes, archive, limits) {
|
|
288
|
+
const directory = bytes.length < END_SIZE ? undefined : directoryRecords(bytes, archive, limits);
|
|
289
|
+
if (directory === undefined) {
|
|
290
|
+
if (holdsZipSignature(bytes))
|
|
291
|
+
throw unsupportedZip(archive, 'it carries a ZIP signature and no end-of-central-directory record where every reader looks for one (in its last 64 KiB): it is padded, cut short, or buried under other bytes, and a reader that looks elsewhere would read it');
|
|
292
|
+
throw unreadableArchive(archive, 'it is not a ZIP archive (its bytes hold neither a local file header nor an end-of-central-directory record)');
|
|
293
|
+
}
|
|
294
|
+
const { records } = directory;
|
|
295
|
+
refuseNameListedTwice(archive, records);
|
|
296
|
+
let entries;
|
|
297
|
+
try {
|
|
298
|
+
// The central directory is parsed at the first read of the entries, not when the archive is opened.
|
|
299
|
+
entries = new AdmZip(bytes).getEntries();
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
throw unsupportedZip(archive, `the ZIP reader cannot read it (${String(error)})`, error);
|
|
303
|
+
}
|
|
304
|
+
sameDirectory(archive, records, entries);
|
|
305
|
+
refuseZip64Fields(archive, records);
|
|
306
|
+
// After the library's offsets are held to VAT's: an offset deferred to ZIP64 is that refusal, not this one.
|
|
307
|
+
refuseDataBeforeFirstEntry(archive, directory);
|
|
308
|
+
refuseRecordWithoutItsLocalHeader(bytes, archive, directory);
|
|
309
|
+
const listed = entries.map((entry) => recordOf(entry));
|
|
310
|
+
assertWithinLimits(archive, listed, limits);
|
|
311
|
+
return listed;
|
|
312
|
+
}
|
|
313
|
+
/** `version made by` in every entry VAT writes: host 3 (Unix), spec version 2.0. */
|
|
314
|
+
const VERSION_MADE_ON_UNIX = (MADE_ON_UNIX << 8) | 20;
|
|
315
|
+
/** The MS-DOS directory flag, in the low byte of an entry's external attributes. */
|
|
316
|
+
const DOS_DIRECTORY = 0x10;
|
|
317
|
+
/**
|
|
318
|
+
* The one mode policy of a ZIP VAT writes: a directory `0755`, a file `0755` when it is executable
|
|
319
|
+
* and `0644` otherwise — never a mode the host's `stat` made up.
|
|
320
|
+
*
|
|
321
|
+
* @returns The entry's external attributes: the Unix type and permission bits high, the DOS directory flag low
|
|
322
|
+
*/
|
|
323
|
+
function writtenAttributesOf(directory, executable) {
|
|
324
|
+
if (directory)
|
|
325
|
+
return (((TYPE_DIRECTORY | DIRECTORY_MODE) << 16) | DOS_DIRECTORY) >>> 0;
|
|
326
|
+
return ((TYPE_REGULAR | (executable ? EXECUTABLE_MODE : FILE_MODE)) << 16) >>> 0;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* The bytes of `zip`, every entry made on Unix and holding the mode {@link writtenAttributesOf}
|
|
330
|
+
* gives it. The ZIP library stamps the host it RUNS on (NTFS on Windows) and copies whatever mode
|
|
331
|
+
* `stat` reports — on Windows a directory `0o40666`, a file `0o100666`, never an execute bit. A
|
|
332
|
+
* host-aware reader (VAT's own {@link unixModeOf}, Info-ZIP) applies the Unix mode only under host
|
|
333
|
+
* 3, so both are fixed here together: one archive is the same bytes wherever VAT writes it, and no
|
|
334
|
+
* directory in it is one a Unix reader cannot enter.
|
|
335
|
+
*/
|
|
336
|
+
function bytesMadeOnUnix(zip) {
|
|
337
|
+
for (const entry of zip.getEntries()) {
|
|
338
|
+
entry.header.made = VERSION_MADE_ON_UNIX;
|
|
339
|
+
entry.attr = writtenAttributesOf(entry.isDirectory, ((entry.attr >>> 16) & EXECUTE_BITS) !== 0);
|
|
340
|
+
}
|
|
341
|
+
return zip.toBuffer();
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* The ZIP of a directory tree VAT built, in memory: every file under `sourceDir`, named relative
|
|
345
|
+
* to it, each entry made on Unix with a directory `0755` and a file `0644`, or `0755` when the host
|
|
346
|
+
* marks it executable ({@link bytesMadeOnUnix}).
|
|
347
|
+
*
|
|
348
|
+
* @param sourceDir - The directory to archive
|
|
349
|
+
* @returns The archive's bytes
|
|
350
|
+
* @throws the raw errno of a listing or a read the OS refuses, for the caller to classify
|
|
351
|
+
*/
|
|
352
|
+
export function zipOfDirectory(sourceDir) {
|
|
353
|
+
const zip = new AdmZip();
|
|
354
|
+
zip.addLocalFolder(sourceDir);
|
|
355
|
+
return bytesMadeOnUnix(zip);
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* The ZIP of `entries`, in order, in memory — each a directory or a file with mode `0755` or
|
|
359
|
+
* `0644` by its executable bit, made on Unix on every host ({@link bytesMadeOnUnix}), nothing else
|
|
360
|
+
* of what an archive it came from said.
|
|
361
|
+
*
|
|
362
|
+
* @param entries - The entries, each named by the place it was judged at
|
|
363
|
+
* @returns The archive's bytes
|
|
364
|
+
*/
|
|
365
|
+
export function zipOfEntries(entries) {
|
|
366
|
+
const zip = new AdmZip();
|
|
367
|
+
for (const { place, data, executable } of entries) {
|
|
368
|
+
// Only the execute bit is read from what is added here; the mode itself is {@link bytesMadeOnUnix}'s.
|
|
369
|
+
if (data === undefined)
|
|
370
|
+
zip.addFile(`${place}/`, Buffer.alloc(0), '', DIRECTORY_MODE);
|
|
371
|
+
else
|
|
372
|
+
zip.addFile(place, data, '', executable ? EXECUTABLE_MODE : FILE_MODE);
|
|
373
|
+
}
|
|
374
|
+
return bytesMadeOnUnix(zip);
|
|
375
|
+
}
|
|
376
|
+
//# sourceMappingURL=zip-reader.js.map
|