@openclaw/fs-safe 0.5.6 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +89 -0
- package/README.md +23 -10
- package/dist/advanced.d.ts +2 -1
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +1 -0
- package/dist/archive-crc32.d.ts +2 -0
- package/dist/archive-crc32.d.ts.map +1 -0
- package/dist/archive-crc32.js +14 -0
- package/dist/archive-deadline.d.ts +3 -0
- package/dist/archive-deadline.d.ts.map +1 -1
- package/dist/archive-deadline.js +44 -8
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +1 -0
- package/dist/archive-errors.d.ts +1 -0
- package/dist/archive-errors.d.ts.map +1 -1
- package/dist/archive-errors.js +3 -0
- package/dist/archive-input.d.ts.map +1 -1
- package/dist/archive-input.js +26 -16
- package/dist/archive-kind.js +2 -2
- package/dist/archive-limits.d.ts +11 -3
- package/dist/archive-limits.d.ts.map +1 -1
- package/dist/archive-limits.js +24 -0
- package/dist/archive-native.d.ts +3 -2
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +31 -9
- package/dist/archive-policy.d.ts +2 -0
- package/dist/archive-policy.d.ts.map +1 -1
- package/dist/archive-policy.js +9 -1
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +88 -41
- package/dist/archive-staging.d.ts +3 -0
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +91 -43
- package/dist/archive-tar-admission.d.ts +7 -0
- package/dist/archive-tar-admission.d.ts.map +1 -0
- package/dist/archive-tar-admission.js +43 -0
- package/dist/archive-tar-gnu.d.ts +2 -0
- package/dist/archive-tar-gnu.d.ts.map +1 -0
- package/dist/archive-tar-gnu.js +20 -0
- package/dist/archive-tar-header.d.ts +8 -0
- package/dist/archive-tar-header.d.ts.map +1 -0
- package/dist/archive-tar-header.js +47 -0
- package/dist/archive-tar-meta.d.ts +29 -1
- package/dist/archive-tar-meta.d.ts.map +1 -1
- package/dist/archive-tar-meta.js +135 -19
- package/dist/archive-tar-pax.d.ts +8 -0
- package/dist/archive-tar-pax.d.ts.map +1 -0
- package/dist/archive-tar-pax.js +100 -0
- package/dist/archive-tar-runtime.d.ts +4 -0
- package/dist/archive-tar-runtime.d.ts.map +1 -1
- package/dist/archive-tar-runtime.js +3 -0
- package/dist/archive-tar.d.ts.map +1 -1
- package/dist/archive-tar.js +8 -2
- package/dist/archive-zip-admission.d.ts +7 -0
- package/dist/archive-zip-admission.d.ts.map +1 -0
- package/dist/archive-zip-admission.js +60 -0
- package/dist/archive-zip-count.d.ts +2 -0
- package/dist/archive-zip-count.d.ts.map +1 -0
- package/dist/archive-zip-count.js +140 -0
- package/dist/archive-zip-directory.d.ts +8 -0
- package/dist/archive-zip-directory.d.ts.map +1 -0
- package/dist/archive-zip-directory.js +224 -0
- package/dist/archive-zip-integrity.d.ts.map +1 -1
- package/dist/archive-zip-integrity.js +1 -14
- package/dist/archive-zip-names.d.ts +12 -0
- package/dist/archive-zip-names.d.ts.map +1 -0
- package/dist/archive-zip-names.js +99 -0
- package/dist/archive-zip-preflight.d.ts +1 -1
- package/dist/archive-zip-preflight.d.ts.map +1 -1
- package/dist/archive-zip-preflight.js +8 -145
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +63 -42
- package/dist/atomic.d.ts +1 -1
- package/dist/atomic.d.ts.map +1 -1
- package/dist/bounded-read-stream.d.ts.map +1 -1
- package/dist/bounded-read-stream.js +2 -4
- package/dist/bounded-read.d.ts.map +1 -1
- package/dist/bounded-read.js +3 -10
- package/dist/byte-budget.d.ts +5 -0
- package/dist/byte-budget.d.ts.map +1 -0
- package/dist/byte-budget.js +9 -0
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +25 -19
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +26 -9
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +9 -5
- package/dist/file-store-limit.d.ts +2 -0
- package/dist/file-store-limit.d.ts.map +1 -0
- package/dist/file-store-limit.js +8 -0
- package/dist/file-store-sync-write.d.ts.map +1 -1
- package/dist/file-store-sync-write.js +38 -8
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +26 -26
- package/dist/json-durable-queue-directory.d.ts +2 -0
- package/dist/json-durable-queue-directory.d.ts.map +1 -0
- package/dist/json-durable-queue-directory.js +20 -0
- package/dist/json-durable-queue-ownership.d.ts +14 -0
- package/dist/json-durable-queue-ownership.d.ts.map +1 -0
- package/dist/json-durable-queue-ownership.js +168 -0
- package/dist/json-durable-queue-retirement.d.ts +9 -0
- package/dist/json-durable-queue-retirement.d.ts.map +1 -0
- package/dist/json-durable-queue-retirement.js +126 -0
- package/dist/json-durable-queue-transfer-lock.d.ts +2 -0
- package/dist/json-durable-queue-transfer-lock.d.ts.map +1 -0
- package/dist/json-durable-queue-transfer-lock.js +19 -0
- package/dist/json-durable-queue.d.ts +1 -0
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +90 -57
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +27 -8
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +4 -2
- package/dist/native-binding.d.ts +15 -3
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-operations.d.ts +4 -1
- package/dist/native-operations.d.ts.map +1 -1
- package/dist/native-operations.js +22 -6
- package/dist/native-pinned-write-windows.d.ts +8 -0
- package/dist/native-pinned-write-windows.d.ts.map +1 -0
- package/dist/native-pinned-write-windows.js +92 -0
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +136 -127
- package/dist/native-staged-file.d.ts +24 -0
- package/dist/native-staged-file.d.ts.map +1 -0
- package/dist/native-staged-file.js +337 -0
- package/dist/native.d.ts.map +1 -1
- package/dist/native.js +4 -4
- package/dist/opened-realpath.d.ts +2 -0
- package/dist/opened-realpath.d.ts.map +1 -1
- package/dist/opened-realpath.js +12 -7
- package/dist/output-sibling.d.ts.map +1 -1
- package/dist/output-sibling.js +11 -110
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +4 -2
- package/dist/owner-dacl.d.ts.map +1 -1
- package/dist/owner-dacl.js +2 -1
- package/dist/permission-exec.d.ts +19 -0
- package/dist/permission-exec.d.ts.map +1 -1
- package/dist/permission-exec.js +57 -11
- package/dist/permissions-public.d.ts +1 -1
- package/dist/permissions-public.d.ts.map +1 -1
- package/dist/permissions-windows.d.ts +3 -0
- package/dist/permissions-windows.d.ts.map +1 -1
- package/dist/permissions-windows.js +16 -5
- package/dist/permissions.d.ts +5 -0
- package/dist/permissions.d.ts.map +1 -1
- package/dist/pinned-open.d.ts.map +1 -1
- package/dist/pinned-open.js +27 -48
- package/dist/pinned-write.d.ts +6 -0
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +26 -37
- package/dist/private-directory.d.ts.map +1 -1
- package/dist/private-directory.js +3 -2
- package/dist/private-temp-workspace.d.ts +3 -1
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +81 -56
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +2 -4
- package/dist/read-opened-file.d.ts.map +1 -1
- package/dist/read-opened-file.js +6 -4
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +136 -82
- package/dist/replace-file-copy-fallback.d.ts +3 -1
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +21 -36
- package/dist/replace-file-copy-source.d.ts +21 -0
- package/dist/replace-file-copy-source.d.ts.map +1 -0
- package/dist/replace-file-copy-source.js +112 -0
- package/dist/replace-file-descriptor.d.ts +13 -3
- package/dist/replace-file-descriptor.d.ts.map +1 -1
- package/dist/replace-file-descriptor.js +32 -7
- package/dist/replace-file-rename-policy.d.ts +7 -0
- package/dist/replace-file-rename-policy.d.ts.map +1 -0
- package/dist/replace-file-rename-policy.js +30 -0
- package/dist/replace-file-temp-owner.d.ts +46 -0
- package/dist/replace-file-temp-owner.d.ts.map +1 -0
- package/dist/replace-file-temp-owner.js +346 -0
- package/dist/replace-file.d.ts +6 -1
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +72 -58
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +118 -98
- package/dist/root-paths.d.ts +11 -14
- package/dist/root-paths.d.ts.map +1 -1
- package/dist/root-paths.js +36 -27
- package/dist/root-write-verification.d.ts +11 -0
- package/dist/root-write-verification.d.ts.map +1 -0
- package/dist/root-write-verification.js +91 -0
- package/dist/secret-file.d.ts +1 -6
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +49 -120
- package/dist/secret-read-async.d.ts +1 -1
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +51 -72
- package/dist/secret-read-policy.d.ts +13 -0
- package/dist/secret-read-policy.d.ts.map +1 -0
- package/dist/secret-read-policy.js +28 -0
- package/dist/secret.d.ts +2 -1
- package/dist/secret.d.ts.map +1 -1
- package/dist/secret.js +2 -1
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +42 -29
- package/dist/sibling-staged-file.d.ts +15 -0
- package/dist/sibling-staged-file.d.ts.map +1 -0
- package/dist/sibling-staged-file.js +148 -0
- package/dist/sibling-temp.d.ts +3 -0
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +30 -74
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +48 -27
- package/dist/sidecar-lock-handle.d.ts +6 -2
- package/dist/sidecar-lock-handle.d.ts.map +1 -1
- package/dist/sidecar-lock-handle.js +17 -3
- package/dist/sidecar-lock-policy.d.ts +2 -0
- package/dist/sidecar-lock-policy.d.ts.map +1 -1
- package/dist/sidecar-lock-policy.js +29 -0
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +29 -7
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +28 -16
- package/dist/staged-directory.d.ts +16 -0
- package/dist/staged-directory.d.ts.map +1 -0
- package/dist/staged-directory.js +60 -0
- package/dist/staged-file-types.d.ts +56 -0
- package/dist/staged-file-types.d.ts.map +1 -0
- package/dist/staged-file-types.js +1 -0
- package/dist/staged-file.d.ts +10 -0
- package/dist/staged-file.d.ts.map +1 -0
- package/dist/staged-file.js +15 -0
- package/dist/strict-file-identity.d.ts +6 -0
- package/dist/strict-file-identity.d.ts.map +1 -0
- package/dist/strict-file-identity.js +48 -0
- package/dist/suppressed-error.d.ts +6 -0
- package/dist/suppressed-error.d.ts.map +1 -0
- package/dist/suppressed-error.js +15 -0
- package/dist/temp-cleanup.d.ts +2 -0
- package/dist/temp-cleanup.d.ts.map +1 -1
- package/dist/temp-cleanup.js +25 -10
- package/dist/temp-workspace-owner.d.ts +23 -0
- package/dist/temp-workspace-owner.d.ts.map +1 -0
- package/dist/temp-workspace-owner.js +320 -0
- package/dist/temp.d.ts +1 -1
- package/dist/temp.d.ts.map +1 -1
- package/dist/test-hooks.d.ts +5 -0
- package/dist/test-hooks.d.ts.map +1 -1
- package/dist/windows-owner.d.ts +3 -0
- package/dist/windows-owner.d.ts.map +1 -1
- package/dist/windows-owner.js +10 -2
- package/docs/advanced.md +19 -2
- package/docs/archive.md +250 -35
- package/docs/atomic.md +11 -2
- package/docs/config.md +7 -0
- package/docs/contributing.md +45 -7
- package/docs/durability.md +18 -5
- package/docs/errors.md +16 -1
- package/docs/file-store.md +2 -0
- package/docs/index.md +3 -1
- package/docs/install.md +20 -8
- package/docs/json.md +8 -4
- package/docs/migrating-to-0.5.md +7 -7
- package/docs/migrating-to-0.6.md +43 -0
- package/docs/native-helper.md +30 -8
- package/docs/native.md +73 -17
- package/docs/output.md +10 -0
- package/docs/path-scope.md +28 -2
- package/docs/permissions.md +13 -2
- package/docs/public-api.md +6 -3
- package/docs/quickstart.md +1 -1
- package/docs/reading.md +1 -1
- package/docs/regular-file.md +9 -2
- package/docs/root.md +3 -1
- package/docs/secret-file.md +12 -0
- package/docs/secure-file.md +21 -3
- package/docs/security-model.md +33 -1
- package/docs/sidecar-lock.md +15 -1
- package/docs/staged-file.md +178 -0
- package/docs/store.md +13 -0
- package/docs/temp.md +128 -14
- package/docs/testing.md +1 -1
- package/docs/writing.md +19 -0
- package/package.json +16 -9
- package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
- package/dist/native/darwin-x64/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
- package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
package/dist/archive-tar-meta.js
CHANGED
|
@@ -3,19 +3,49 @@ import { Transform, Writable } from "node:stream";
|
|
|
3
3
|
import { pipeline } from "node:stream/promises";
|
|
4
4
|
import { createGunzip } from "node:zlib";
|
|
5
5
|
import { ArchiveFormatError } from "./archive-errors.js";
|
|
6
|
-
import { ARCHIVE_LIMIT_ERROR_CODE, ArchiveLimitError } from "./archive-limits.js";
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
import { ARCHIVE_LIMIT_ERROR_CODE, ArchiveLimitError, tarManifestEntryCost } from "./archive-limits.js";
|
|
7
|
+
import { parseLocalPax, paxMemberSize } from "./archive-tar-pax.js";
|
|
8
|
+
import { validateGnuMetadata } from "./archive-tar-gnu.js";
|
|
9
|
+
import { rawTarMember } from "./archive-tar-admission.js";
|
|
10
|
+
import { validateTarHeader } from "./archive-tar-header.js";
|
|
11
|
+
// Keep raw framing aligned with native/src/tar_meter.rs, before either parser.
|
|
12
|
+
export class TarMetadataMeter extends Transform {
|
|
13
|
+
limits;
|
|
14
|
+
onMember;
|
|
9
15
|
block = Buffer.alloc(512);
|
|
10
16
|
blockLength = 0;
|
|
11
17
|
state = { kind: "header" };
|
|
12
|
-
|
|
18
|
+
pendingPax;
|
|
19
|
+
pendingGnu = new Set();
|
|
20
|
+
pendingGnuPath;
|
|
21
|
+
zeroBlocks = 0;
|
|
22
|
+
entries = 0;
|
|
23
|
+
remainingDecodedBytes;
|
|
24
|
+
manifestBytes = 0;
|
|
25
|
+
constructor(limits, onMember) {
|
|
13
26
|
super();
|
|
14
|
-
this.
|
|
27
|
+
this.limits = limits;
|
|
28
|
+
this.onMember = onMember;
|
|
29
|
+
if (!Number.isSafeInteger(limits.maxDecodedBytes) || limits.maxDecodedBytes < 0) {
|
|
30
|
+
throw new RangeError("maxDecodedBytes must be a non-negative safe integer");
|
|
31
|
+
}
|
|
32
|
+
this.remainingDecodedBytes = limits.maxDecodedBytes;
|
|
33
|
+
}
|
|
34
|
+
countMember() {
|
|
35
|
+
if (this.entries >= this.limits.maxEntries) {
|
|
36
|
+
throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_COUNT_EXCEEDS_LIMIT);
|
|
37
|
+
}
|
|
38
|
+
this.entries += 1;
|
|
15
39
|
}
|
|
16
40
|
invalid(message) {
|
|
17
41
|
return new ArchiveFormatError(`invalid TAR header: ${message}`);
|
|
18
42
|
}
|
|
43
|
+
paddedSize(size) {
|
|
44
|
+
const padded = Math.ceil(size / 512) * 512;
|
|
45
|
+
if (!Number.isSafeInteger(padded))
|
|
46
|
+
throw this.invalid("entry padding exceeds the safe integer range");
|
|
47
|
+
return padded;
|
|
48
|
+
}
|
|
19
49
|
parseSize() {
|
|
20
50
|
const field = this.block.subarray(124, 136);
|
|
21
51
|
if ((field[0] ?? 0) & 0x80) {
|
|
@@ -32,7 +62,13 @@ class TarMetadataMeter extends Transform {
|
|
|
32
62
|
return Number(value);
|
|
33
63
|
}
|
|
34
64
|
const zero = field.indexOf(0);
|
|
35
|
-
|
|
65
|
+
if (zero >= 0 && field.subarray(zero).some((byte) => byte !== 0 && byte !== 0x20)) {
|
|
66
|
+
throw this.invalid("size has non-padding bytes after NUL");
|
|
67
|
+
}
|
|
68
|
+
const bytes = field.subarray(0, zero < 0 ? field.length : zero);
|
|
69
|
+
if (bytes.some((byte) => byte > 0x7f))
|
|
70
|
+
throw this.invalid("size is not ASCII octal");
|
|
71
|
+
const text = bytes.toString("ascii").replace(/^ +| +$/g, "");
|
|
36
72
|
if (!text || !/^[0-7]+$/.test(text))
|
|
37
73
|
throw this.invalid("size is not valid octal");
|
|
38
74
|
const value = Number.parseInt(text, 8);
|
|
@@ -42,29 +78,58 @@ class TarMetadataMeter extends Transform {
|
|
|
42
78
|
}
|
|
43
79
|
finishHeader() {
|
|
44
80
|
if (this.block.every((byte) => byte === 0)) {
|
|
81
|
+
if (this.pendingPax)
|
|
82
|
+
throw this.invalid("dangling PAX metadata");
|
|
83
|
+
if (this.pendingGnu.size)
|
|
84
|
+
throw this.invalid("dangling GNU metadata");
|
|
45
85
|
this.blockLength = 0;
|
|
46
|
-
this.
|
|
86
|
+
this.zeroBlocks += 1;
|
|
87
|
+
this.state = { kind: this.zeroBlocks === 2 ? "eof" : "header" };
|
|
47
88
|
return;
|
|
48
89
|
}
|
|
90
|
+
if (this.zeroBlocks !== 0)
|
|
91
|
+
throw this.invalid("nonzero header after one TAR zero block");
|
|
92
|
+
validateTarHeader(this.block);
|
|
49
93
|
const nameEnd = this.block.subarray(0, 100).indexOf(0);
|
|
50
94
|
const name = this.block.subarray(0, nameEnd < 0 ? 100 : nameEnd);
|
|
51
95
|
if (name.length === 0) {
|
|
52
96
|
throw this.invalid("entry path is empty");
|
|
53
97
|
}
|
|
54
|
-
if (this.block[156]
|
|
98
|
+
if (![0x35, 0x44].includes(this.block[156]) && name.at(-1) === 0x2f) {
|
|
55
99
|
throw this.invalid("non-directory entry path ends with a separator");
|
|
56
100
|
}
|
|
57
|
-
|
|
101
|
+
let size = this.parseSize();
|
|
102
|
+
let padded = this.paddedSize(size);
|
|
58
103
|
const type = this.block[156];
|
|
59
|
-
|
|
104
|
+
// Directory/link headers cannot carry bodies; PAX/GNU metadata can.
|
|
105
|
+
if ([0x31, 0x32, 0x35].includes(type) && size !== 0) {
|
|
106
|
+
throw this.invalid("directory or link has a nonzero body size");
|
|
107
|
+
}
|
|
108
|
+
if ([0x78, 0x67, 0x4c, 0x4b, 0x58, 0x4e].includes(type) && size > this.limits.maxMetaEntryBytes) {
|
|
60
109
|
throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.META_ENTRY_SIZE_EXCEEDS_LIMIT);
|
|
61
110
|
}
|
|
62
|
-
if (type ===
|
|
63
|
-
throw this.invalid("PAX
|
|
111
|
+
if (type === 0x67 || type === 0x58 || type === 0x4e) {
|
|
112
|
+
throw this.invalid("global/old PAX and old GNU metadata are not supported");
|
|
64
113
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
114
|
+
if (type === 0x78 || type === 0x4c || type === 0x4b) {
|
|
115
|
+
const metadataType = type === 0x78 ? "pax" : type === 0x4c ? "L" : "K";
|
|
116
|
+
if (this.pendingPax || size === 0 || (metadataType === "pax" ? this.pendingGnu.size : this.pendingGnu.has(metadataType))) {
|
|
117
|
+
throw this.invalid("empty, repeated or mixed PAX/GNU metadata");
|
|
118
|
+
}
|
|
119
|
+
const magic = this.block.subarray(257, 265).toString("latin1");
|
|
120
|
+
if (magic !== "ustar\0" + "00" && magic !== "ustar \0")
|
|
121
|
+
throw this.invalid("unrecognized PAX/GNU header format");
|
|
122
|
+
if (metadataType !== "pax")
|
|
123
|
+
this.pendingGnu.add(metadataType);
|
|
124
|
+
this.state = { kind: "metadata", type: metadataType, body: Buffer.alloc(size), used: 0, padding: padded - size };
|
|
125
|
+
this.blockLength = 0;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
// Sparse headers retain their metadata-limit-before-format-error ordering.
|
|
129
|
+
if (this.pendingPax && type !== 0x53) {
|
|
130
|
+
size = paxMemberSize(this.pendingPax, type, size, this.block);
|
|
131
|
+
}
|
|
132
|
+
padded = this.paddedSize(size);
|
|
68
133
|
if (type === 0x53) {
|
|
69
134
|
if (this.block[482] !== 0 && this.block[482] !== 1) {
|
|
70
135
|
throw this.invalid("GNU sparse extension flag is not 0 or 1");
|
|
@@ -75,13 +140,27 @@ class TarMetadataMeter extends Transform {
|
|
|
75
140
|
this.state = { kind: "sparse", dataRemaining: padded, metaBytes: 0 };
|
|
76
141
|
}
|
|
77
142
|
else {
|
|
143
|
+
this.countMember();
|
|
144
|
+
if (this.pendingGnuPath && /[\\/]$/.test(this.pendingGnuPath) && ![0x35, 0x44].includes(type)) {
|
|
145
|
+
throw this.invalid("GNU effective non-directory path ends with a separator");
|
|
146
|
+
}
|
|
147
|
+
const entry = rawTarMember(this.block, size, this.pendingPax?.path ?? this.pendingGnuPath);
|
|
148
|
+
const cost = tarManifestEntryCost(entry.path);
|
|
149
|
+
if (cost > this.limits.maxManifestBytes - this.manifestBytes) {
|
|
150
|
+
throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.MANIFEST_SIZE_EXCEEDS_LIMIT);
|
|
151
|
+
}
|
|
152
|
+
this.manifestBytes += cost;
|
|
153
|
+
this.onMember?.(entry);
|
|
154
|
+
this.pendingPax = undefined;
|
|
155
|
+
this.pendingGnu.clear();
|
|
156
|
+
this.pendingGnuPath = undefined;
|
|
78
157
|
this.state = padded === 0 ? { kind: "header" } : { kind: "data", remaining: padded };
|
|
79
158
|
}
|
|
80
159
|
this.blockLength = 0;
|
|
81
160
|
}
|
|
82
161
|
finishSparseHeader(state) {
|
|
83
162
|
const metaBytes = state.metaBytes + 512;
|
|
84
|
-
if (metaBytes > this.maxMetaEntryBytes) {
|
|
163
|
+
if (metaBytes > this.limits.maxMetaEntryBytes) {
|
|
85
164
|
throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.META_ENTRY_SIZE_EXCEEDS_LIMIT);
|
|
86
165
|
}
|
|
87
166
|
if (this.block[504] !== 0 && this.block[504] !== 1) {
|
|
@@ -98,6 +177,29 @@ class TarMetadataMeter extends Transform {
|
|
|
98
177
|
meter(chunk) {
|
|
99
178
|
let offset = 0;
|
|
100
179
|
while (offset < chunk.length) {
|
|
180
|
+
if (this.state.kind === "eof") {
|
|
181
|
+
if (chunk.subarray(offset).some((byte) => byte !== 0))
|
|
182
|
+
throw this.invalid("nonzero data after TAR EOF");
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (this.state.kind === "metadata") {
|
|
186
|
+
const state = this.state;
|
|
187
|
+
const take = Math.min(state.body.length - state.used, chunk.length - offset);
|
|
188
|
+
chunk.copy(state.body, state.used, offset, offset + take);
|
|
189
|
+
state.used += take;
|
|
190
|
+
offset += take;
|
|
191
|
+
if (state.used === state.body.length) {
|
|
192
|
+
if (state.type === "pax")
|
|
193
|
+
this.pendingPax = parseLocalPax(state.body);
|
|
194
|
+
else {
|
|
195
|
+
const name = validateGnuMetadata(state.body, state.type);
|
|
196
|
+
if (state.type === "L")
|
|
197
|
+
this.pendingGnuPath = name;
|
|
198
|
+
}
|
|
199
|
+
this.state = state.padding === 0 ? { kind: "header" } : { kind: "data", remaining: state.padding };
|
|
200
|
+
}
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
101
203
|
if (this.state.kind === "data") {
|
|
102
204
|
const take = Math.min(this.state.remaining, chunk.length - offset);
|
|
103
205
|
offset += take;
|
|
@@ -119,7 +221,15 @@ class TarMetadataMeter extends Transform {
|
|
|
119
221
|
}
|
|
120
222
|
_transform(chunk, _encoding, callback) {
|
|
121
223
|
try {
|
|
122
|
-
|
|
224
|
+
const admitted = Math.min(chunk.length, this.remainingDecodedBytes);
|
|
225
|
+
this.meter(chunk.subarray(0, admitted));
|
|
226
|
+
this.remainingDecodedBytes -= admitted;
|
|
227
|
+
if (admitted < chunk.length) {
|
|
228
|
+
// Inspect only the overflow probe, never scan an unbounded tail.
|
|
229
|
+
if (this.state.kind === "eof" && chunk[admitted] !== 0)
|
|
230
|
+
throw this.invalid("nonzero data after TAR EOF");
|
|
231
|
+
throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.DECODED_SIZE_EXCEEDS_LIMIT);
|
|
232
|
+
}
|
|
123
233
|
callback(null, chunk);
|
|
124
234
|
}
|
|
125
235
|
catch (error) {
|
|
@@ -127,8 +237,14 @@ class TarMetadataMeter extends Transform {
|
|
|
127
237
|
}
|
|
128
238
|
}
|
|
129
239
|
_flush(callback) {
|
|
130
|
-
if (this.
|
|
240
|
+
if (this.pendingPax)
|
|
241
|
+
callback(this.invalid("dangling PAX metadata"));
|
|
242
|
+
else if (this.pendingGnu.size)
|
|
243
|
+
callback(this.invalid("dangling GNU metadata"));
|
|
244
|
+
else if (this.state.kind === "eof")
|
|
131
245
|
callback();
|
|
246
|
+
else if (this.state.kind === "header" && this.blockLength === 0)
|
|
247
|
+
callback(this.invalid("missing two-block TAR EOF"));
|
|
132
248
|
else
|
|
133
249
|
callback(this.invalid(this.state.kind === "header" ? "truncated TAR header" : "truncated TAR entry"));
|
|
134
250
|
}
|
|
@@ -146,7 +262,7 @@ async function isGzip(filePath) {
|
|
|
146
262
|
}
|
|
147
263
|
export async function preflightTarMetadata(params) {
|
|
148
264
|
const input = fs.createReadStream(params.archivePath);
|
|
149
|
-
const meter = new TarMetadataMeter(params.
|
|
265
|
+
const meter = new TarMetadataMeter(params.limits, params.onMember);
|
|
150
266
|
const sink = new Writable({ write(_chunk, _encoding, callback) { callback(); } });
|
|
151
267
|
if (await isGzip(params.archivePath)) {
|
|
152
268
|
await pipeline(input, createGunzip(), meter, sink, { signal: params.signal });
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type LocalPax = {
|
|
2
|
+
path?: string;
|
|
3
|
+
linkpath?: string;
|
|
4
|
+
size?: number;
|
|
5
|
+
};
|
|
6
|
+
export declare function parseLocalPax(body: Buffer): LocalPax;
|
|
7
|
+
export declare function paxMemberSize(pax: LocalPax, type: number, rawSize: number, header: Buffer): number;
|
|
8
|
+
//# sourceMappingURL=archive-tar-pax.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive-tar-pax.d.ts","sourceRoot":"","sources":["../src/archive-tar-pax.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,QAAQ,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAqB3E,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAwCpD;AAQD,wBAAgB,aAAa,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAclG"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { ArchiveFormatError } from "./archive-errors.js";
|
|
2
|
+
function invalid() {
|
|
3
|
+
throw new ArchiveFormatError("invalid TAR header: unsupported or malformed PAX metadata");
|
|
4
|
+
}
|
|
5
|
+
function decimal(bytes) {
|
|
6
|
+
const text = bytes.toString("latin1");
|
|
7
|
+
if (!/^(0|[1-9][0-9]*)$/.test(text))
|
|
8
|
+
invalid();
|
|
9
|
+
const value = Number(text);
|
|
10
|
+
if (!Number.isSafeInteger(value))
|
|
11
|
+
invalid();
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
function ascii(bytes) {
|
|
15
|
+
if (bytes.length === 0 || bytes.some((byte) => byte < 0x20 || byte > 0x7e))
|
|
16
|
+
invalid();
|
|
17
|
+
return bytes.toString("ascii");
|
|
18
|
+
}
|
|
19
|
+
// Keep this grammar aligned with native/src/tar_pax.rs. Downstream parsers
|
|
20
|
+
// disagree on duplicates, coercions and UTF-8 chunk boundaries.
|
|
21
|
+
export function parseLocalPax(body) {
|
|
22
|
+
if (body.length === 0)
|
|
23
|
+
invalid();
|
|
24
|
+
const result = {};
|
|
25
|
+
const keys = new Set();
|
|
26
|
+
let offset = 0;
|
|
27
|
+
while (offset < body.length) {
|
|
28
|
+
const space = body.indexOf(0x20, offset);
|
|
29
|
+
if (space < 0)
|
|
30
|
+
invalid();
|
|
31
|
+
const length = decimal(body.subarray(offset, space));
|
|
32
|
+
if (length > body.length - offset || length <= space - offset + 3)
|
|
33
|
+
invalid();
|
|
34
|
+
const end = offset + length;
|
|
35
|
+
if (body[end - 1] !== 0x0a)
|
|
36
|
+
invalid();
|
|
37
|
+
const record = body.subarray(space + 1, end - 1);
|
|
38
|
+
if (record.includes(0x0a))
|
|
39
|
+
invalid();
|
|
40
|
+
const equals = record.indexOf(0x3d);
|
|
41
|
+
if (equals <= 0)
|
|
42
|
+
invalid();
|
|
43
|
+
const key = ascii(record.subarray(0, equals));
|
|
44
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(key) || keys.has(key))
|
|
45
|
+
invalid();
|
|
46
|
+
keys.add(key);
|
|
47
|
+
const value = record.subarray(equals + 1);
|
|
48
|
+
if (/^(LIBARCHIVE|SCHILY)\.xattr\..+$/.test(key)) {
|
|
49
|
+
// Inert bytes, never restored. Newlines would corrupt Rust's subsequent
|
|
50
|
+
// record lookup; NUL and non-UTF8 bytes in these values are harmless.
|
|
51
|
+
}
|
|
52
|
+
else if (key === "path" || key === "linkpath") {
|
|
53
|
+
result[key] = ascii(value);
|
|
54
|
+
}
|
|
55
|
+
else if (key === "size") {
|
|
56
|
+
result.size = decimal(value);
|
|
57
|
+
}
|
|
58
|
+
else if (key === "uid" || key === "gid") {
|
|
59
|
+
decimal(value);
|
|
60
|
+
}
|
|
61
|
+
else if (key === "uname" || key === "gname") {
|
|
62
|
+
ascii(value);
|
|
63
|
+
}
|
|
64
|
+
else if (key === "mtime" || key === "atime" || key === "ctime") {
|
|
65
|
+
const text = ascii(value);
|
|
66
|
+
if (!/^-?(0|[1-9][0-9]*)(\.[0-9]+)?$/.test(text) || Math.abs(Number(text)) > 8_640_000_000_000)
|
|
67
|
+
invalid();
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
invalid();
|
|
71
|
+
}
|
|
72
|
+
offset = end;
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
function rawText(field) {
|
|
77
|
+
const zero = field.indexOf(0);
|
|
78
|
+
const value = field.subarray(0, zero < 0 ? field.length : zero);
|
|
79
|
+
return value.length === 0 ? "" : ascii(value);
|
|
80
|
+
}
|
|
81
|
+
export function paxMemberSize(pax, type, rawSize, header) {
|
|
82
|
+
if (![0, 0x30, 0x31, 0x32, 0x35, 0x37].includes(type))
|
|
83
|
+
invalid();
|
|
84
|
+
const rawName = rawText(header.subarray(0, 100));
|
|
85
|
+
const rawLink = rawText(header.subarray(157, 257));
|
|
86
|
+
if (header.subarray(257, 265).equals(Buffer.from("ustar\0" + "00"))) {
|
|
87
|
+
rawText(header.subarray(345, 500));
|
|
88
|
+
}
|
|
89
|
+
const isLink = type === 0x31 || type === 0x32;
|
|
90
|
+
if (isLink !== (rawLink.length > 0))
|
|
91
|
+
invalid();
|
|
92
|
+
const size = pax.size ?? rawSize;
|
|
93
|
+
if ([0x31, 0x32, 0x35].includes(type) && (rawSize !== 0 || size !== 0))
|
|
94
|
+
invalid();
|
|
95
|
+
if (type !== 0x35 && (pax.path?.endsWith("/") || pax.path?.endsWith("\\") || rawName.endsWith("\\")))
|
|
96
|
+
invalid();
|
|
97
|
+
if (pax.linkpath !== undefined && !isLink)
|
|
98
|
+
invalid();
|
|
99
|
+
return size;
|
|
100
|
+
}
|
|
@@ -12,6 +12,7 @@ export type TarParser = NodeJS.WritableStream & {
|
|
|
12
12
|
on(event: "error", listener: (error: Error) => void): TarParser;
|
|
13
13
|
on(event: "end", listener: () => void): TarParser;
|
|
14
14
|
};
|
|
15
|
+
export declare const NODE_TAR_RATIO_DISABLED: number;
|
|
15
16
|
export type TarModule = {
|
|
16
17
|
Parser: new (options: {
|
|
17
18
|
strict: true;
|
|
@@ -25,8 +26,10 @@ export type TarModule = {
|
|
|
25
26
|
preservePaths: false;
|
|
26
27
|
noChmod: true;
|
|
27
28
|
preserveOwner: false;
|
|
29
|
+
noMtime: true;
|
|
28
30
|
strict: true;
|
|
29
31
|
maxMetaEntrySize: number;
|
|
32
|
+
maxDecompressionRatio: number;
|
|
30
33
|
filter?(this: TarParser, entryPath: string, entry: unknown): boolean;
|
|
31
34
|
onReadEntry(this: unknown, entry: unknown): void;
|
|
32
35
|
}): TarParser;
|
|
@@ -34,6 +37,7 @@ export type TarModule = {
|
|
|
34
37
|
file: string;
|
|
35
38
|
strict: true;
|
|
36
39
|
maxMetaEntrySize: number;
|
|
40
|
+
maxDecompressionRatio: number;
|
|
37
41
|
onReadEntry(entry: AsyncIterable<unknown> & {
|
|
38
42
|
resume(): void;
|
|
39
43
|
}): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive-tar-runtime.d.ts","sourceRoot":"","sources":["../src/archive-tar-runtime.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,IAAI,IAAI,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,cAAc,GAAG;IAC9C,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAC1B,EAAE,CAAC,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GAAG,SAAS,CAAC;IAChF,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GAAG,SAAS,CAAC;IACzE,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,GAAG,SAAS,CAAC;IACnE,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,SAAS,CAAC;IAChE,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,SAAS,CAAC;CACnD,CAAC;
|
|
1
|
+
{"version":3,"file":"archive-tar-runtime.d.ts","sourceRoot":"","sources":["../src/archive-tar-runtime.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,IAAI,IAAI,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,cAAc,GAAG;IAC9C,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAC1B,EAAE,CAAC,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GAAG,SAAS,CAAC;IAChF,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GAAG,SAAS,CAAC;IACzE,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,GAAG,SAAS,CAAC;IACnE,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,SAAS,CAAC;IAChE,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,SAAS,CAAC;CACnD,CAAC;AAIF,eAAO,MAAM,uBAAuB,QAA2B,CAAC;AAEhE,MAAM,MAAM,SAAS,GAAG;IACtB,MAAM,EAAE,KAAK,OAAO,EAAE;QAAE,MAAM,EAAE,IAAI,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAA;KAAE,KAAK,SAAS,CAAC;IAC/E,CAAC,CAAC,OAAO,EAAE;QACT,GAAG,EAAE,MAAM,CAAC;QACZ,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,OAAO,CAAC;QACf,MAAM,CAAC,EAAE,WAAW,CAAC;QACrB,aAAa,EAAE,KAAK,CAAC;QACrB,OAAO,EAAE,IAAI,CAAC;QACd,aAAa,EAAE,KAAK,CAAC;QACrB,OAAO,EAAE,IAAI,CAAC;QACd,MAAM,EAAE,IAAI,CAAC;QACb,gBAAgB,EAAE,MAAM,CAAC;QACzB,qBAAqB,EAAE,MAAM,CAAC;QAC9B,MAAM,CAAC,CAAC,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC;QACrE,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;KAClD,GAAG,SAAS,CAAC;IACd,CAAC,CAAC,OAAO,EAAE;QACT,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,IAAI,CAAC;QACb,gBAAgB,EAAE,MAAM,CAAC;QACzB,qBAAqB,EAAE,MAAM,CAAC;QAC9B,WAAW,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,GAAG;YAAE,MAAM,IAAI,IAAI,CAAA;SAAE,GAAG,IAAI,CAAC;KACvE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACtB,CAAC;AAEF,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,SAAS,CAAC,CAS5D;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAS/D"}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { ArchiveFormatError } from "./archive-errors.js";
|
|
2
|
+
// Both callers parse an immutable staged stream only after fs-safe's complete
|
|
3
|
+
// decoded-byte admission, so node-tar must not add backend-specific policy.
|
|
4
|
+
export const NODE_TAR_RATIO_DISABLED = Number.POSITIVE_INFINITY;
|
|
2
5
|
export async function importOptionalTar() {
|
|
3
6
|
try {
|
|
4
7
|
return await import("tar");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive-tar.d.ts","sourceRoot":"","sources":["../src/archive-tar.ts"],"names":[],"mappings":"AAMA,OAAO,EAKL,KAAK,oBAAoB,EAC1B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAGL,KAAK,kBAAkB,EACvB,KAAK,0BAA0B,EAChC,MAAM,qBAAqB,CAAC;AAI7B,MAAM,MAAM,YAAY,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAWvF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,YAAY,CAyB7D;AAED,wBAAgB,8BAA8B,CAAC,MAAM,EAAE;IACrD,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,UAAU,CAAC,EAAE,0BAA0B,CAAC;CACzC,GAAG,CAAC,KAAK,EAAE,YAAY,KAAK,OAAO,
|
|
1
|
+
{"version":3,"file":"archive-tar.d.ts","sourceRoot":"","sources":["../src/archive-tar.ts"],"names":[],"mappings":"AAMA,OAAO,EAKL,KAAK,oBAAoB,EAC1B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAGL,KAAK,kBAAkB,EACvB,KAAK,0BAA0B,EAChC,MAAM,qBAAqB,CAAC;AAI7B,MAAM,MAAM,YAAY,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAWvF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,YAAY,CAyB7D;AAED,wBAAgB,8BAA8B,CAAC,MAAM,EAAE;IACrD,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,UAAU,CAAC,EAAE,0BAA0B,CAAC;CACzC,GAAG,CAAC,KAAK,EAAE,YAAY,KAAK,OAAO,CAqDnC"}
|
package/dist/archive-tar.js
CHANGED
|
@@ -43,7 +43,10 @@ export function createTarEntryPreflightChecker(params) {
|
|
|
43
43
|
entryCount += 1;
|
|
44
44
|
assertArchiveEntryCountWithinLimit(entryCount, limits);
|
|
45
45
|
validateArchiveEntryPath(entry.path, { escapeLabel: params.escapeLabel });
|
|
46
|
-
const
|
|
46
|
+
const canonicalPath = stripArchivePath(entry.path, 0);
|
|
47
|
+
if (!canonicalPath)
|
|
48
|
+
return false;
|
|
49
|
+
const relPath = stripArchivePath(canonicalPath, strip);
|
|
47
50
|
if (!relPath) {
|
|
48
51
|
return false;
|
|
49
52
|
}
|
|
@@ -60,13 +63,16 @@ export function createTarEntryPreflightChecker(params) {
|
|
|
60
63
|
if (!shouldExtractArchiveEntry({
|
|
61
64
|
filter: params.entryFilter,
|
|
62
65
|
onFiltered: params.onFiltered,
|
|
63
|
-
entry: { path:
|
|
66
|
+
entry: { path: canonicalPath, kind, size: entry.size },
|
|
64
67
|
})) {
|
|
65
68
|
return false;
|
|
66
69
|
}
|
|
67
70
|
if (BLOCKED_TAR_ENTRY_TYPES.has(entry.type)) {
|
|
68
71
|
throw new ArchiveSecurityError("entry-link", `tar entry is a link: ${formatErrorDetail(entry.path)}`);
|
|
69
72
|
}
|
|
73
|
+
// Accepted unsupported records remain omitted, as in the native plan.
|
|
74
|
+
if (kind === "other")
|
|
75
|
+
return false;
|
|
70
76
|
budget.startEntry();
|
|
71
77
|
budget.addEntrySize(entry.size);
|
|
72
78
|
return true;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ExtractionDeadline } from "./archive-deadline.js";
|
|
2
|
+
import { type ResolvedArchiveExtractLimits } from "./archive-limits.js";
|
|
3
|
+
/** Admit physical records before a decoder can normalize or collapse names. */
|
|
4
|
+
export declare function admitZipBuffer(input: Uint8Array, limits: ResolvedArchiveExtractLimits): number;
|
|
5
|
+
/** The extraction input is already private and staged; read only bounded metadata. */
|
|
6
|
+
export declare function admitZipFile(archivePath: string, limits: ResolvedArchiveExtractLimits, deadline: ExtractionDeadline): Promise<number>;
|
|
7
|
+
//# sourceMappingURL=archive-zip-admission.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive-zip-admission.d.ts","sourceRoot":"","sources":["../src/archive-zip-admission.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAA+C,KAAK,4BAA4B,EAAE,MAAM,qBAAqB,CAAC;AAYrH,+EAA+E;AAC/E,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,4BAA4B,GAAG,MAAM,CAU9F;AAED,sFAAsF;AACtF,wBAAsB,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,4BAA4B,EAAE,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,MAAM,CAAC,CA+B3I"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { ARCHIVE_LIMIT_ERROR_CODE, ArchiveLimitError } from "./archive-limits.js";
|
|
4
|
+
import { openRootFile } from "./root-file.js";
|
|
5
|
+
import { scanZipDirectory } from "./archive-zip-directory.js";
|
|
6
|
+
import { zipFormat } from "./archive-zip-names.js";
|
|
7
|
+
function checkSize(size, limits) {
|
|
8
|
+
if (size > limits.maxArchiveBytes) {
|
|
9
|
+
throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ARCHIVE_SIZE_EXCEEDS_LIMIT);
|
|
10
|
+
}
|
|
11
|
+
if (!Number.isSafeInteger(size) || size < 0)
|
|
12
|
+
zipFormat("invalid archive size");
|
|
13
|
+
}
|
|
14
|
+
/** Admit physical records before a decoder can normalize or collapse names. */
|
|
15
|
+
export function admitZipBuffer(input, limits) {
|
|
16
|
+
checkSize(input.byteLength, limits);
|
|
17
|
+
const buffer = Buffer.from(input.buffer, input.byteOffset, input.byteLength);
|
|
18
|
+
const scan = scanZipDirectory(buffer.length, limits);
|
|
19
|
+
let step = scan.next();
|
|
20
|
+
while (!step.done) {
|
|
21
|
+
const { offset, length } = step.value;
|
|
22
|
+
step = scan.next(buffer.subarray(offset, offset + length));
|
|
23
|
+
}
|
|
24
|
+
return step.value;
|
|
25
|
+
}
|
|
26
|
+
/** The extraction input is already private and staged; read only bounded metadata. */
|
|
27
|
+
export async function admitZipFile(archivePath, limits, deadline) {
|
|
28
|
+
deadline.check();
|
|
29
|
+
const opened = await openRootFile({
|
|
30
|
+
absolutePath: archivePath, rootPath: path.dirname(archivePath),
|
|
31
|
+
boundaryLabel: "staged ZIP archive", rejectSymlinks: true, rejectHardlinks: true,
|
|
32
|
+
});
|
|
33
|
+
if (!opened.ok)
|
|
34
|
+
throw opened.error ?? new Error("cannot open staged ZIP archive");
|
|
35
|
+
try {
|
|
36
|
+
checkSize(opened.stat.size, limits);
|
|
37
|
+
const scan = scanZipDirectory(opened.stat.size, limits);
|
|
38
|
+
let step = scan.next();
|
|
39
|
+
while (!step.done) {
|
|
40
|
+
const { offset, length } = step.value;
|
|
41
|
+
const bytes = Buffer.alloc(length);
|
|
42
|
+
let received = 0;
|
|
43
|
+
while (received < length) {
|
|
44
|
+
deadline.check();
|
|
45
|
+
const count = await new Promise((resolve, reject) => {
|
|
46
|
+
fs.read(opened.fd, bytes, received, length - received, offset + received, (error, count) => error ? reject(error) : resolve(count));
|
|
47
|
+
});
|
|
48
|
+
deadline.check();
|
|
49
|
+
if (!count)
|
|
50
|
+
zipFormat("truncated staged record");
|
|
51
|
+
received += count;
|
|
52
|
+
}
|
|
53
|
+
step = scan.next(bytes);
|
|
54
|
+
}
|
|
55
|
+
return step.value;
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
fs.closeSync(opened.fd);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive-zip-count.d.ts","sourceRoot":"","sources":["../src/archive-zip-count.ts"],"names":[],"mappings":"AA4KA,wBAAgB,iCAAiC,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,GAAG,IAAI,CAU5F"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// Conservative public count hint; strict admission must not rely on its fallback.
|
|
2
|
+
const ZIP_EOCD_SIGNATURE = 0x06054b50;
|
|
3
|
+
const ZIP64_EOCD_SIGNATURE = 0x06064b50;
|
|
4
|
+
const ZIP64_EOCD_LOCATOR_SIGNATURE = 0x07064b50;
|
|
5
|
+
const ZIP_EOCD_MIN_BYTES = 22;
|
|
6
|
+
const ZIP_EOCD_MAX_COMMENT_BYTES = 0xffff;
|
|
7
|
+
const ZIP64_ENTRY_COUNT_SENTINEL = 0xffff;
|
|
8
|
+
const ZIP64_UINT32_SENTINEL = 0xffffffff;
|
|
9
|
+
const ZIP_CENTRAL_FILE_HEADER_SIGNATURE = 0x02014b50;
|
|
10
|
+
const ZIP_CENTRAL_FILE_HEADER_MIN_BYTES = 46;
|
|
11
|
+
const ZIP_CENTRAL_FILE_HEADER_NAME_LENGTH_OFFSET = 28;
|
|
12
|
+
const ZIP_CENTRAL_FILE_HEADER_EXTRA_LENGTH_OFFSET = 30;
|
|
13
|
+
const ZIP_CENTRAL_FILE_HEADER_COMMENT_LENGTH_OFFSET = 32;
|
|
14
|
+
const ZIP_EOCD_TOTAL_ENTRIES_OFFSET = 10;
|
|
15
|
+
const ZIP_EOCD_CENTRAL_DIRECTORY_SIZE_OFFSET = 12;
|
|
16
|
+
const ZIP_EOCD_CENTRAL_DIRECTORY_OFFSET_OFFSET = 16;
|
|
17
|
+
const ZIP_EOCD_COMMENT_LENGTH_OFFSET = 20;
|
|
18
|
+
const ZIP64_EOCD_LOCATOR_BYTES = 20;
|
|
19
|
+
const ZIP64_EOCD_OFFSET_OFFSET = 8;
|
|
20
|
+
const ZIP64_EOCD_TOTAL_ENTRIES_OFFSET = 32;
|
|
21
|
+
const ZIP64_EOCD_CENTRAL_DIRECTORY_SIZE_OFFSET = 40;
|
|
22
|
+
const ZIP64_EOCD_CENTRAL_DIRECTORY_OFFSET_OFFSET = 48;
|
|
23
|
+
function asBufferView(buffer) {
|
|
24
|
+
if (Buffer.isBuffer(buffer)) {
|
|
25
|
+
return buffer;
|
|
26
|
+
}
|
|
27
|
+
return Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
28
|
+
}
|
|
29
|
+
function readSafeUInt64LE(buffer, offset) {
|
|
30
|
+
const value = buffer.readBigUInt64LE(offset);
|
|
31
|
+
if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
32
|
+
return Number.MAX_SAFE_INTEGER;
|
|
33
|
+
}
|
|
34
|
+
return Number(value);
|
|
35
|
+
}
|
|
36
|
+
function findZipEndOfCentralDirectory(buffer) {
|
|
37
|
+
if (buffer.byteLength < ZIP_EOCD_MIN_BYTES) {
|
|
38
|
+
return -1;
|
|
39
|
+
}
|
|
40
|
+
const minOffset = Math.max(0, buffer.byteLength - ZIP_EOCD_MIN_BYTES - ZIP_EOCD_MAX_COMMENT_BYTES);
|
|
41
|
+
for (let offset = buffer.byteLength - ZIP_EOCD_MIN_BYTES; offset >= minOffset; offset -= 1) {
|
|
42
|
+
if (buffer.readUInt32LE(offset) !== ZIP_EOCD_SIGNATURE) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const commentLength = buffer.readUInt16LE(offset + ZIP_EOCD_COMMENT_LENGTH_OFFSET);
|
|
46
|
+
if (offset + ZIP_EOCD_MIN_BYTES + commentLength === buffer.byteLength) {
|
|
47
|
+
return offset;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return -1;
|
|
51
|
+
}
|
|
52
|
+
function readZip64CentralDirectoryInfo(buffer, eocdOffset) {
|
|
53
|
+
const locatorOffset = eocdOffset - ZIP64_EOCD_LOCATOR_BYTES;
|
|
54
|
+
if (locatorOffset < 0 || buffer.readUInt32LE(locatorOffset) !== ZIP64_EOCD_LOCATOR_SIGNATURE) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const zip64EocdOffset = readSafeUInt64LE(buffer, locatorOffset + ZIP64_EOCD_OFFSET_OFFSET);
|
|
58
|
+
if (zip64EocdOffset < 0 ||
|
|
59
|
+
zip64EocdOffset + ZIP64_EOCD_CENTRAL_DIRECTORY_OFFSET_OFFSET + 8 > buffer.byteLength ||
|
|
60
|
+
buffer.readUInt32LE(zip64EocdOffset) !== ZIP64_EOCD_SIGNATURE) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
declaredEntryCount: readSafeUInt64LE(buffer, zip64EocdOffset + ZIP64_EOCD_TOTAL_ENTRIES_OFFSET),
|
|
65
|
+
centralDirectorySize: readSafeUInt64LE(buffer, zip64EocdOffset + ZIP64_EOCD_CENTRAL_DIRECTORY_SIZE_OFFSET),
|
|
66
|
+
centralDirectoryOffset: readSafeUInt64LE(buffer, zip64EocdOffset + ZIP64_EOCD_CENTRAL_DIRECTORY_OFFSET_OFFSET),
|
|
67
|
+
endOfCentralDirectoryOffset: eocdOffset,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function readZipCentralDirectoryInfo(buffer) {
|
|
71
|
+
const eocdOffset = findZipEndOfCentralDirectory(buffer);
|
|
72
|
+
if (eocdOffset < 0) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const declaredEntryCount = buffer.readUInt16LE(eocdOffset + ZIP_EOCD_TOTAL_ENTRIES_OFFSET);
|
|
76
|
+
const centralDirectorySize = buffer.readUInt32LE(eocdOffset + ZIP_EOCD_CENTRAL_DIRECTORY_SIZE_OFFSET);
|
|
77
|
+
const centralDirectoryOffset = buffer.readUInt32LE(eocdOffset + ZIP_EOCD_CENTRAL_DIRECTORY_OFFSET_OFFSET);
|
|
78
|
+
const usesZip64 = declaredEntryCount === ZIP64_ENTRY_COUNT_SENTINEL ||
|
|
79
|
+
centralDirectorySize === ZIP64_UINT32_SENTINEL ||
|
|
80
|
+
centralDirectoryOffset === ZIP64_UINT32_SENTINEL;
|
|
81
|
+
if (usesZip64) {
|
|
82
|
+
return (readZip64CentralDirectoryInfo(buffer, eocdOffset) ?? {
|
|
83
|
+
declaredEntryCount,
|
|
84
|
+
centralDirectoryOffset,
|
|
85
|
+
centralDirectorySize,
|
|
86
|
+
endOfCentralDirectoryOffset: eocdOffset,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
declaredEntryCount,
|
|
91
|
+
centralDirectoryOffset,
|
|
92
|
+
centralDirectorySize,
|
|
93
|
+
endOfCentralDirectoryOffset: eocdOffset,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function countZipCentralDirectoryHeaders(buffer, info) {
|
|
97
|
+
const start = info.centralDirectoryOffset;
|
|
98
|
+
const declaredEnd = start + info.centralDirectorySize;
|
|
99
|
+
const scanEnd = info.endOfCentralDirectoryOffset;
|
|
100
|
+
if (!Number.isSafeInteger(start) ||
|
|
101
|
+
!Number.isSafeInteger(declaredEnd) ||
|
|
102
|
+
!Number.isSafeInteger(scanEnd) ||
|
|
103
|
+
start < 0 ||
|
|
104
|
+
declaredEnd < start ||
|
|
105
|
+
scanEnd < start ||
|
|
106
|
+
scanEnd > buffer.byteLength) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
let offset = start;
|
|
110
|
+
let count = 0;
|
|
111
|
+
while (offset < scanEnd) {
|
|
112
|
+
if (scanEnd - offset < ZIP_CENTRAL_FILE_HEADER_MIN_BYTES) {
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
if (buffer.readUInt32LE(offset) !== ZIP_CENTRAL_FILE_HEADER_SIGNATURE) {
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
const nameLength = buffer.readUInt16LE(offset + ZIP_CENTRAL_FILE_HEADER_NAME_LENGTH_OFFSET);
|
|
119
|
+
const extraLength = buffer.readUInt16LE(offset + ZIP_CENTRAL_FILE_HEADER_EXTRA_LENGTH_OFFSET);
|
|
120
|
+
const commentLength = buffer.readUInt16LE(offset + ZIP_CENTRAL_FILE_HEADER_COMMENT_LENGTH_OFFSET);
|
|
121
|
+
const nextOffset = offset + ZIP_CENTRAL_FILE_HEADER_MIN_BYTES + nameLength + extraLength + commentLength;
|
|
122
|
+
if (nextOffset <= offset || nextOffset > scanEnd) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
count += 1;
|
|
126
|
+
offset = nextOffset;
|
|
127
|
+
}
|
|
128
|
+
return count > 0 || info.declaredEntryCount === 0 ? count : null;
|
|
129
|
+
}
|
|
130
|
+
export function readZipCentralDirectoryEntryCount(buffer) {
|
|
131
|
+
const view = asBufferView(buffer);
|
|
132
|
+
const info = readZipCentralDirectoryInfo(view);
|
|
133
|
+
if (!info) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
const countedEntryCount = countZipCentralDirectoryHeaders(view, info);
|
|
137
|
+
return countedEntryCount === null
|
|
138
|
+
? info.declaredEntryCount
|
|
139
|
+
: Math.max(info.declaredEntryCount, countedEntryCount);
|
|
140
|
+
}
|