@openclaw/fs-safe 0.6.0 → 0.7.1
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 +79 -0
- package/README.md +7 -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-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 +24 -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 +69 -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 +104 -35
- package/dist/archive-tar-runtime.d.ts +3 -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-names.js +2 -2
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +38 -27
- 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/directory-guard.d.ts +2 -1
- package/dist/directory-guard.d.ts.map +1 -1
- package/dist/directory-guard.js +10 -0
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +81 -30
- package/dist/file-observation.d.ts +9 -0
- package/dist/file-observation.d.ts.map +1 -0
- package/dist/file-observation.js +22 -0
- 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 +32 -13
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +4 -2
- package/dist/native-binding.d.ts +12 -3
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +1 -1
- package/dist/opened-file-failure.d.ts +7 -0
- package/dist/opened-file-failure.d.ts.map +1 -0
- package/dist/opened-file-failure.js +41 -0
- package/dist/opened-realpath.d.ts.map +1 -1
- package/dist/opened-realpath.js +8 -2
- 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/pinned-open.d.ts.map +1 -1
- package/dist/pinned-open.js +27 -48
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +10 -10
- 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 +113 -82
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +6 -3
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +6 -5
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +17 -13
- 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 +91 -37
- 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 +16 -5
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +60 -28
- package/dist/sidecar-lock-root.d.ts +3 -0
- package/dist/sidecar-lock-root.d.ts.map +1 -0
- package/dist/sidecar-lock-root.js +76 -0
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +28 -16
- package/dist/strict-file-identity.d.ts.map +1 -1
- package/dist/strict-file-identity.js +4 -1
- 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/docs/advanced.md +8 -1
- package/docs/archive.md +202 -26
- package/docs/atomic.md +11 -2
- package/docs/durability.md +1 -1
- package/docs/file-store.md +2 -0
- package/docs/install.md +8 -4
- package/docs/json.md +11 -4
- package/docs/native-helper.md +15 -3
- package/docs/native.md +33 -6
- package/docs/output.md +16 -3
- 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 +2 -0
- package/docs/secret-file.md +5 -1
- package/docs/secure-file.md +4 -2
- package/docs/security-model.md +18 -0
- package/docs/sidecar-lock.md +60 -4
- package/docs/store.md +13 -0
- package/docs/temp.md +128 -14
- package/docs/writing.md +6 -0
- package/package.json +10 -10
|
@@ -1,6 +1,34 @@
|
|
|
1
|
+
import { Transform } from "node:stream";
|
|
2
|
+
import { type TarMeterLimits } from "./archive-limits.js";
|
|
3
|
+
import type { TarEntryInfo } from "./archive-tar.js";
|
|
4
|
+
export declare class TarMetadataMeter extends Transform {
|
|
5
|
+
private readonly limits;
|
|
6
|
+
private readonly onMember?;
|
|
7
|
+
private readonly block;
|
|
8
|
+
private blockLength;
|
|
9
|
+
private state;
|
|
10
|
+
private pendingPax;
|
|
11
|
+
private readonly pendingGnu;
|
|
12
|
+
private pendingGnuPath;
|
|
13
|
+
private zeroBlocks;
|
|
14
|
+
private entries;
|
|
15
|
+
private remainingDecodedBytes;
|
|
16
|
+
private manifestBytes;
|
|
17
|
+
constructor(limits: TarMeterLimits, onMember?: ((entry: TarEntryInfo) => void) | undefined);
|
|
18
|
+
private countMember;
|
|
19
|
+
private invalid;
|
|
20
|
+
private paddedSize;
|
|
21
|
+
private parseSize;
|
|
22
|
+
private finishHeader;
|
|
23
|
+
private finishSparseHeader;
|
|
24
|
+
private meter;
|
|
25
|
+
_transform(chunk: Buffer, _encoding: BufferEncoding, callback: (error?: Error | null, data?: Buffer) => void): void;
|
|
26
|
+
_flush(callback: (error?: Error | null) => void): void;
|
|
27
|
+
}
|
|
1
28
|
export declare function preflightTarMetadata(params: {
|
|
2
29
|
archivePath: string;
|
|
3
|
-
|
|
30
|
+
limits: TarMeterLimits;
|
|
4
31
|
signal?: AbortSignal;
|
|
32
|
+
onMember?: (entry: TarEntryInfo) => void;
|
|
5
33
|
}): Promise<void>;
|
|
6
34
|
//# sourceMappingURL=archive-tar-meta.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive-tar-meta.d.ts","sourceRoot":"","sources":["../src/archive-tar-meta.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"archive-tar-meta.d.ts","sourceRoot":"","sources":["../src/archive-tar-meta.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAY,MAAM,aAAa,CAAC;AAIlD,OAAO,EAAqE,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAI7H,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAWrD,qBAAa,gBAAiB,SAAQ,SAAS;IAYjC,OAAO,CAAC,QAAQ,CAAC,MAAM;IAAkB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;IAX/E,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqB;IAC3C,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,KAAK,CAAkC;IAC/C,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAwB;IACnD,OAAO,CAAC,cAAc,CAAqB;IAC3C,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,OAAO,CAAK;IACpB,OAAO,CAAC,qBAAqB,CAAS;IACtC,OAAO,CAAC,aAAa,CAAK;IAE1B,YAA6B,MAAM,EAAE,cAAc,EAAmB,QAAQ,CAAC,GAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,aAAA,EAM7G;IAED,OAAO,CAAC,WAAW;IAOnB,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,UAAU;IAMlB,OAAO,CAAC,SAAS;IA4BjB,OAAO,CAAC,YAAY;IA6EpB,OAAO,CAAC,kBAAkB;IAgB1B,OAAO,CAAC,KAAK;IAyCJ,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,QAAQ,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAc3H;IAEQ,MAAM,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI,GAAG,IAAI,CAM9D;CACF;AAaD,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;CAC1C,GAAG,OAAO,CAAC,IAAI,CAAC,CAShB"}
|
package/dist/archive-tar-meta.js
CHANGED
|
@@ -3,22 +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";
|
|
6
|
+
import { ARCHIVE_LIMIT_ERROR_CODE, ArchiveLimitError, tarManifestEntryCost } from "./archive-limits.js";
|
|
7
7
|
import { parseLocalPax, paxMemberSize } from "./archive-tar-pax.js";
|
|
8
|
-
|
|
9
|
-
|
|
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;
|
|
10
15
|
block = Buffer.alloc(512);
|
|
11
16
|
blockLength = 0;
|
|
12
17
|
state = { kind: "header" };
|
|
13
18
|
pendingPax;
|
|
14
|
-
pendingGnu =
|
|
15
|
-
|
|
19
|
+
pendingGnu = new Set();
|
|
20
|
+
pendingGnuPath;
|
|
21
|
+
zeroBlocks = 0;
|
|
22
|
+
entries = 0;
|
|
23
|
+
remainingDecodedBytes;
|
|
24
|
+
manifestBytes = 0;
|
|
25
|
+
constructor(limits, onMember) {
|
|
16
26
|
super();
|
|
17
|
-
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;
|
|
18
39
|
}
|
|
19
40
|
invalid(message) {
|
|
20
41
|
return new ArchiveFormatError(`invalid TAR header: ${message}`);
|
|
21
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
|
+
}
|
|
22
49
|
parseSize() {
|
|
23
50
|
const field = this.block.subarray(124, 136);
|
|
24
51
|
if ((field[0] ?? 0) & 0x80) {
|
|
@@ -35,10 +62,13 @@ class TarMetadataMeter extends Transform {
|
|
|
35
62
|
return Number(value);
|
|
36
63
|
}
|
|
37
64
|
const zero = field.indexOf(0);
|
|
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
|
+
}
|
|
38
68
|
const bytes = field.subarray(0, zero < 0 ? field.length : zero);
|
|
39
69
|
if (bytes.some((byte) => byte > 0x7f))
|
|
40
70
|
throw this.invalid("size is not ASCII octal");
|
|
41
|
-
const text = bytes.toString("ascii").
|
|
71
|
+
const text = bytes.toString("ascii").replace(/^ +| +$/g, "");
|
|
42
72
|
if (!text || !/^[0-7]+$/.test(text))
|
|
43
73
|
throw this.invalid("size is not valid octal");
|
|
44
74
|
const value = Number.parseInt(text, 8);
|
|
@@ -50,54 +80,56 @@ class TarMetadataMeter extends Transform {
|
|
|
50
80
|
if (this.block.every((byte) => byte === 0)) {
|
|
51
81
|
if (this.pendingPax)
|
|
52
82
|
throw this.invalid("dangling PAX metadata");
|
|
53
|
-
this.pendingGnu
|
|
83
|
+
if (this.pendingGnu.size)
|
|
84
|
+
throw this.invalid("dangling GNU metadata");
|
|
54
85
|
this.blockLength = 0;
|
|
55
|
-
this.
|
|
86
|
+
this.zeroBlocks += 1;
|
|
87
|
+
this.state = { kind: this.zeroBlocks === 2 ? "eof" : "header" };
|
|
56
88
|
return;
|
|
57
89
|
}
|
|
90
|
+
if (this.zeroBlocks !== 0)
|
|
91
|
+
throw this.invalid("nonzero header after one TAR zero block");
|
|
92
|
+
validateTarHeader(this.block);
|
|
58
93
|
const nameEnd = this.block.subarray(0, 100).indexOf(0);
|
|
59
94
|
const name = this.block.subarray(0, nameEnd < 0 ? 100 : nameEnd);
|
|
60
95
|
if (name.length === 0) {
|
|
61
96
|
throw this.invalid("entry path is empty");
|
|
62
97
|
}
|
|
63
|
-
if (this.block[156]
|
|
98
|
+
if (![0x35, 0x44].includes(this.block[156]) && name.at(-1) === 0x2f) {
|
|
64
99
|
throw this.invalid("non-directory entry path ends with a separator");
|
|
65
100
|
}
|
|
66
101
|
let size = this.parseSize();
|
|
102
|
+
let padded = this.paddedSize(size);
|
|
67
103
|
const type = this.block[156];
|
|
68
|
-
|
|
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) {
|
|
69
109
|
throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.META_ENTRY_SIZE_EXCEEDS_LIMIT);
|
|
70
110
|
}
|
|
71
111
|
if (type === 0x67 || type === 0x58 || type === 0x4e) {
|
|
72
112
|
throw this.invalid("global/old PAX and old GNU metadata are not supported");
|
|
73
113
|
}
|
|
74
|
-
if (type === 0x78) {
|
|
75
|
-
|
|
76
|
-
|
|
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
|
+
}
|
|
77
119
|
const magic = this.block.subarray(257, 265).toString("latin1");
|
|
78
120
|
if (magic !== "ustar\0" + "00" && magic !== "ustar \0")
|
|
79
|
-
throw this.invalid("unrecognized PAX header format");
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
this.state = { kind: "pax", body: Buffer.alloc(size), used: 0, padding: padded - size };
|
|
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 };
|
|
84
125
|
this.blockLength = 0;
|
|
85
126
|
return;
|
|
86
127
|
}
|
|
87
128
|
// Sparse headers retain their metadata-limit-before-format-error ordering.
|
|
88
129
|
if (this.pendingPax && type !== 0x53) {
|
|
89
130
|
size = paxMemberSize(this.pendingPax, type, size, this.block);
|
|
90
|
-
this.pendingPax = undefined;
|
|
91
|
-
}
|
|
92
|
-
if (type === 0x4c || type === 0x4b) {
|
|
93
|
-
this.pendingGnu = true;
|
|
94
131
|
}
|
|
95
|
-
|
|
96
|
-
this.pendingGnu = false;
|
|
97
|
-
}
|
|
98
|
-
const padded = Math.ceil(size / 512) * 512;
|
|
99
|
-
if (!Number.isSafeInteger(padded))
|
|
100
|
-
throw this.invalid("entry padding exceeds the safe integer range");
|
|
132
|
+
padded = this.paddedSize(size);
|
|
101
133
|
if (type === 0x53) {
|
|
102
134
|
if (this.block[482] !== 0 && this.block[482] !== 1) {
|
|
103
135
|
throw this.invalid("GNU sparse extension flag is not 0 or 1");
|
|
@@ -108,13 +140,27 @@ class TarMetadataMeter extends Transform {
|
|
|
108
140
|
this.state = { kind: "sparse", dataRemaining: padded, metaBytes: 0 };
|
|
109
141
|
}
|
|
110
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;
|
|
111
157
|
this.state = padded === 0 ? { kind: "header" } : { kind: "data", remaining: padded };
|
|
112
158
|
}
|
|
113
159
|
this.blockLength = 0;
|
|
114
160
|
}
|
|
115
161
|
finishSparseHeader(state) {
|
|
116
162
|
const metaBytes = state.metaBytes + 512;
|
|
117
|
-
if (metaBytes > this.maxMetaEntryBytes) {
|
|
163
|
+
if (metaBytes > this.limits.maxMetaEntryBytes) {
|
|
118
164
|
throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.META_ENTRY_SIZE_EXCEEDS_LIMIT);
|
|
119
165
|
}
|
|
120
166
|
if (this.block[504] !== 0 && this.block[504] !== 1) {
|
|
@@ -131,14 +177,25 @@ class TarMetadataMeter extends Transform {
|
|
|
131
177
|
meter(chunk) {
|
|
132
178
|
let offset = 0;
|
|
133
179
|
while (offset < chunk.length) {
|
|
134
|
-
if (this.state.kind === "
|
|
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") {
|
|
135
186
|
const state = this.state;
|
|
136
187
|
const take = Math.min(state.body.length - state.used, chunk.length - offset);
|
|
137
188
|
chunk.copy(state.body, state.used, offset, offset + take);
|
|
138
189
|
state.used += take;
|
|
139
190
|
offset += take;
|
|
140
191
|
if (state.used === state.body.length) {
|
|
141
|
-
|
|
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
|
+
}
|
|
142
199
|
this.state = state.padding === 0 ? { kind: "header" } : { kind: "data", remaining: state.padding };
|
|
143
200
|
}
|
|
144
201
|
continue;
|
|
@@ -164,7 +221,15 @@ class TarMetadataMeter extends Transform {
|
|
|
164
221
|
}
|
|
165
222
|
_transform(chunk, _encoding, callback) {
|
|
166
223
|
try {
|
|
167
|
-
|
|
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
|
+
}
|
|
168
233
|
callback(null, chunk);
|
|
169
234
|
}
|
|
170
235
|
catch (error) {
|
|
@@ -174,8 +239,12 @@ class TarMetadataMeter extends Transform {
|
|
|
174
239
|
_flush(callback) {
|
|
175
240
|
if (this.pendingPax)
|
|
176
241
|
callback(this.invalid("dangling PAX metadata"));
|
|
177
|
-
else if (this.
|
|
242
|
+
else if (this.pendingGnu.size)
|
|
243
|
+
callback(this.invalid("dangling GNU metadata"));
|
|
244
|
+
else if (this.state.kind === "eof")
|
|
178
245
|
callback();
|
|
246
|
+
else if (this.state.kind === "header" && this.blockLength === 0)
|
|
247
|
+
callback(this.invalid("missing two-block TAR EOF"));
|
|
179
248
|
else
|
|
180
249
|
callback(this.invalid(this.state.kind === "header" ? "truncated TAR header" : "truncated TAR entry"));
|
|
181
250
|
}
|
|
@@ -193,7 +262,7 @@ async function isGzip(filePath) {
|
|
|
193
262
|
}
|
|
194
263
|
export async function preflightTarMetadata(params) {
|
|
195
264
|
const input = fs.createReadStream(params.archivePath);
|
|
196
|
-
const meter = new TarMetadataMeter(params.
|
|
265
|
+
const meter = new TarMetadataMeter(params.limits, params.onMember);
|
|
197
266
|
const sink = new Writable({ write(_chunk, _encoding, callback) { callback(); } });
|
|
198
267
|
if (await isGzip(params.archivePath)) {
|
|
199
268
|
await pipeline(input, createGunzip(), meter, sink, { signal: params.signal });
|
|
@@ -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;
|
|
@@ -28,6 +29,7 @@ export type TarModule = {
|
|
|
28
29
|
noMtime: true;
|
|
29
30
|
strict: true;
|
|
30
31
|
maxMetaEntrySize: number;
|
|
32
|
+
maxDecompressionRatio: number;
|
|
31
33
|
filter?(this: TarParser, entryPath: string, entry: unknown): boolean;
|
|
32
34
|
onReadEntry(this: unknown, entry: unknown): void;
|
|
33
35
|
}): TarParser;
|
|
@@ -35,6 +37,7 @@ export type TarModule = {
|
|
|
35
37
|
file: string;
|
|
36
38
|
strict: true;
|
|
37
39
|
maxMetaEntrySize: number;
|
|
40
|
+
maxDecompressionRatio: number;
|
|
38
41
|
onReadEntry(entry: AsyncIterable<unknown> & {
|
|
39
42
|
resume(): void;
|
|
40
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;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ArchiveFormatError, ArchiveSecurityError } from "./archive-errors.js";
|
|
2
|
-
import { validateArchiveEntryPath } from "./archive-entry.js";
|
|
2
|
+
import { stripArchivePath, validateArchiveEntryPath } from "./archive-entry.js";
|
|
3
3
|
import { updateCrc32 } from "./archive-crc32.js";
|
|
4
4
|
export function zipFormat(message) {
|
|
5
5
|
throw new ArchiveFormatError(`invalid ZIP metadata: ${message}`);
|
|
@@ -40,7 +40,7 @@ function utf8(bytes) {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
function key(name) {
|
|
43
|
-
return name
|
|
43
|
+
return stripArchivePath(name, 0) ?? "";
|
|
44
44
|
}
|
|
45
45
|
function originalName(name, flags) {
|
|
46
46
|
// ASCII path syntax is encoding-independent. Do not reinterpret legacy bytes
|
package/dist/archive.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive.d.ts","sourceRoot":"","sources":["../src/archive.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"archive.d.ts","sourceRoot":"","sources":["../src/archive.ts"],"names":[],"mappings":"AA+DA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAElE,YAAY,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AACjF,YAAY,EACV,kBAAkB,EAClB,gBAAgB,EAChB,sBAAsB,EACtB,0BAA0B,GAC3B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,kBAAkB,EAClB,yBAAyB,EACzB,wBAAwB,EACxB,gBAAgB,EAChB,wBAAwB,GACzB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC/F,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EACL,wBAAwB,EACxB,iBAAiB,EACjB,6BAA6B,EAC7B,mBAAmB,EACnB,2BAA2B,EAC3B,uBAAuB,EACvB,4BAA4B,EAC5B,iCAAiC,EACjC,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,GAC3B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,kBAAkB,EAAE,KAAK,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AACtF,OAAO,EAAE,oBAAoB,EAAE,KAAK,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAC3F,OAAO,EACL,kCAAkC,EAClC,iCAAiC,EACjC,4BAA4B,EAC5B,wBAAwB,EACxB,4BAA4B,GAC7B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,8BAA8B,EAAE,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrF,OAAO,EACL,2BAA2B,EAC3B,iCAAiC,EACjC,KAAK,mBAAmB,GACzB,MAAM,4BAA4B,CAAC;AAyNpC,wBAAsB,cAAc,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6KjF"}
|
package/dist/archive.js
CHANGED
|
@@ -5,7 +5,7 @@ import { Readable } from "node:stream";
|
|
|
5
5
|
import { pipeline } from "node:stream/promises";
|
|
6
6
|
import { createArchiveOutputPathTracker, resolveArchiveOutputPath, stripArchivePath, validateArchiveEntryPath, } from "./archive-entry.js";
|
|
7
7
|
import { createPipelineTimeoutError, waitForDeadline, withExtractionDeadline, } from "./archive-deadline.js";
|
|
8
|
-
import { assertArchiveEntryCountWithinLimit, assertArchiveEntryPathComponentsWithinLimit, createByteBudgetTracker, createExtractBudgetTransform, resolveExtractLimits, } from "./archive-limits.js";
|
|
8
|
+
import { assertArchiveEntryCountWithinLimit, assertArchiveEntryPathComponentsWithinLimit, createByteBudgetTracker, createExtractBudgetTransform, resolveExtractLimits, resolveTarMeterLimits, } from "./archive-limits.js";
|
|
9
9
|
import { resolveArchiveKind } from "./archive-kind.js";
|
|
10
10
|
import { mergeExtractedTreeIntoDestination, prepareArchiveDestinationDir, prepareArchiveOutputPath, withStagedArchiveDestination, } from "./archive-staging.js";
|
|
11
11
|
import { createTarEntryPreflightChecker, readTarEntryInfo, } from "./archive-tar.js";
|
|
@@ -17,9 +17,10 @@ import { ArchiveSecurityError } from "./archive-errors.js";
|
|
|
17
17
|
import { extractNativeArchive } from "./archive-native.js";
|
|
18
18
|
import { stageArchiveFileForExtraction } from "./archive-input.js";
|
|
19
19
|
import { getNativeBinding } from "./native.js";
|
|
20
|
-
import { resolveArchiveEntryMode, shouldExtractArchiveEntry, } from "./archive-policy.js";
|
|
21
|
-
import { importOptionalTar, normalizeTarParserError } from "./archive-tar-runtime.js";
|
|
20
|
+
import { resolveArchiveEntryMode, resolveArchiveFilteredEntryPolicy, shouldExtractArchiveEntry, } from "./archive-policy.js";
|
|
21
|
+
import { importOptionalTar, NODE_TAR_RATIO_DISABLED, normalizeTarParserError } from "./archive-tar-runtime.js";
|
|
22
22
|
import { preflightTarMetadata } from "./archive-tar-meta.js";
|
|
23
|
+
import { createTarAdmissionPlan } from "./archive-tar-admission.js";
|
|
23
24
|
import { writeSiblingTempFile } from "./sibling-temp.js";
|
|
24
25
|
export { isWindowsDrivePath, normalizeArchiveEntryPath, resolveArchiveOutputPath, stripArchivePath, validateArchiveEntryPath, } from "./archive-entry.js";
|
|
25
26
|
export { resolveArchiveKind, resolvePackedRootDir } from "./archive-kind.js";
|
|
@@ -45,12 +46,16 @@ async function readZipEntryStream(entry) {
|
|
|
45
46
|
}
|
|
46
47
|
function resolveZipOutputPath(params) {
|
|
47
48
|
validateArchiveEntryPath(params.entryPath);
|
|
48
|
-
const
|
|
49
|
+
const canonicalPath = stripArchivePath(params.entryPath, 0);
|
|
50
|
+
if (!canonicalPath)
|
|
51
|
+
return null;
|
|
52
|
+
const relPath = stripArchivePath(canonicalPath, params.strip);
|
|
49
53
|
if (!relPath) {
|
|
50
54
|
return null;
|
|
51
55
|
}
|
|
52
56
|
validateArchiveEntryPath(relPath);
|
|
53
57
|
return {
|
|
58
|
+
canonicalPath,
|
|
54
59
|
relPath,
|
|
55
60
|
outPath: resolveArchiveOutputPath({
|
|
56
61
|
rootDir: params.destinationDir,
|
|
@@ -72,7 +77,6 @@ async function writeZipFileEntry(params) {
|
|
|
72
77
|
try {
|
|
73
78
|
await writeSiblingTempFile({
|
|
74
79
|
dir: path.dirname(destinationPath),
|
|
75
|
-
tempPrefix: `.${path.basename(destinationPath)}.fs-safe-archive`,
|
|
76
80
|
chmodDir: false,
|
|
77
81
|
mode: params.mode,
|
|
78
82
|
writeTemp: async (tempPath) => {
|
|
@@ -151,7 +155,7 @@ async function extractZip(params) {
|
|
|
151
155
|
if (!shouldExtractArchiveEntry({
|
|
152
156
|
filter: params.entryFilter,
|
|
153
157
|
onFiltered: params.onFiltered,
|
|
154
|
-
entry: { path:
|
|
158
|
+
entry: { path: output.canonicalPath, kind: entryKind, size: entrySize },
|
|
155
159
|
})) {
|
|
156
160
|
continue;
|
|
157
161
|
}
|
|
@@ -166,6 +170,7 @@ async function extractZip(params) {
|
|
|
166
170
|
outPath: output.outPath,
|
|
167
171
|
originalPath: entry.name,
|
|
168
172
|
isDirectory: entry.dir,
|
|
173
|
+
deadline: params.deadline,
|
|
169
174
|
});
|
|
170
175
|
if (entry.dir) {
|
|
171
176
|
await fs.chmod(output.outPath, mode);
|
|
@@ -184,6 +189,7 @@ async function extractZip(params) {
|
|
|
184
189
|
sourceDir: stagingRealDir,
|
|
185
190
|
destinationDir: params.destDir,
|
|
186
191
|
destinationRealDir,
|
|
192
|
+
deadline: params.deadline,
|
|
187
193
|
});
|
|
188
194
|
params.deadline.check();
|
|
189
195
|
},
|
|
@@ -194,11 +200,14 @@ async function extractZip(params) {
|
|
|
194
200
|
}
|
|
195
201
|
}
|
|
196
202
|
export async function extractArchive(params) {
|
|
203
|
+
const onFiltered = resolveArchiveFilteredEntryPolicy(params.onFiltered);
|
|
197
204
|
const kind = params.kind ?? resolveArchiveKind(params.archivePath);
|
|
198
205
|
if (!kind) {
|
|
199
206
|
throw new Error(`unsupported archive: ${params.archivePath}`);
|
|
200
207
|
}
|
|
201
208
|
const label = kind === "zip" ? "extract zip" : "extract tar";
|
|
209
|
+
const limits = resolveExtractLimits(params.limits);
|
|
210
|
+
const tarLimits = resolveTarMeterLimits(limits);
|
|
202
211
|
const native = getNativeBinding();
|
|
203
212
|
if (native) {
|
|
204
213
|
await withExtractionDeadline(params.timeoutMs, label, async (deadline) => extractNativeArchive({
|
|
@@ -207,11 +216,12 @@ export async function extractArchive(params) {
|
|
|
207
216
|
destDir: params.destDir,
|
|
208
217
|
kind,
|
|
209
218
|
stripComponents: params.stripComponents,
|
|
210
|
-
limits
|
|
219
|
+
limits,
|
|
220
|
+
tarLimits,
|
|
211
221
|
deadline,
|
|
212
222
|
entryModes: params.entryModes,
|
|
213
223
|
entryFilter: params.entryFilter,
|
|
214
|
-
onFiltered
|
|
224
|
+
onFiltered,
|
|
215
225
|
}));
|
|
216
226
|
return;
|
|
217
227
|
}
|
|
@@ -222,17 +232,18 @@ export async function extractArchive(params) {
|
|
|
222
232
|
if (kind === "tar") {
|
|
223
233
|
await withExtractionDeadline(params.timeoutMs, label, async (deadline) => {
|
|
224
234
|
const tar = await importOptionalTar();
|
|
225
|
-
const limits = resolveExtractLimits(params.limits);
|
|
226
235
|
const stagedArchive = await stageArchiveFileForExtraction({
|
|
227
236
|
archivePath: params.archivePath,
|
|
228
237
|
limits,
|
|
229
238
|
deadline,
|
|
230
239
|
});
|
|
231
240
|
try {
|
|
241
|
+
const manifest = [];
|
|
232
242
|
await preflightTarMetadata({
|
|
233
243
|
archivePath: stagedArchive.path,
|
|
234
|
-
|
|
244
|
+
limits: tarLimits,
|
|
235
245
|
signal: deadline.signal,
|
|
246
|
+
onMember: (entry) => { manifest.push(entry); },
|
|
236
247
|
});
|
|
237
248
|
deadline.check();
|
|
238
249
|
const destinationRealDir = await prepareArchiveDestinationDir(params.destDir);
|
|
@@ -246,13 +257,14 @@ export async function extractArchive(params) {
|
|
|
246
257
|
stripComponents: params.stripComponents,
|
|
247
258
|
limits,
|
|
248
259
|
entryFilter: params.entryFilter,
|
|
249
|
-
onFiltered
|
|
260
|
+
onFiltered,
|
|
250
261
|
});
|
|
262
|
+
const admission = createTarAdmissionPlan(manifest, (entry) => {
|
|
263
|
+
deadline.check();
|
|
264
|
+
return checkTarEntrySafety(entry);
|
|
265
|
+
}, strip);
|
|
251
266
|
const acceptedEntries = [];
|
|
252
|
-
//
|
|
253
|
-
// pre-existing child symlinks in the live destination tree.
|
|
254
|
-
// Extract into a private staging dir first, then merge through
|
|
255
|
-
// the same safe-open boundary checks used by direct file writes.
|
|
267
|
+
// Extract privately, then merge through the safe-open boundary.
|
|
256
268
|
const extractor = tar.x({
|
|
257
269
|
cwd: stagingDir,
|
|
258
270
|
// fs-safe owns stripping: node-tar counts the `.` and empty path
|
|
@@ -265,20 +277,16 @@ export async function extractArchive(params) {
|
|
|
265
277
|
preserveOwner: false,
|
|
266
278
|
noMtime: true,
|
|
267
279
|
strict: true,
|
|
268
|
-
maxMetaEntrySize:
|
|
280
|
+
maxMetaEntrySize: tarLimits.maxMetaEntryBytes,
|
|
281
|
+
maxDecompressionRatio: NODE_TAR_RATIO_DISABLED,
|
|
269
282
|
filter(_entryPath, entry) {
|
|
270
283
|
try {
|
|
271
284
|
const info = readTarEntryInfo(entry);
|
|
272
|
-
|
|
273
|
-
return false;
|
|
274
|
-
}
|
|
275
|
-
const relPath = stripArchivePath(info.path, strip);
|
|
285
|
+
const relPath = admission.consume(info);
|
|
276
286
|
if (!relPath) {
|
|
277
287
|
return false;
|
|
278
288
|
}
|
|
279
|
-
//
|
|
280
|
-
// limited and collision-checked under, and that the mode
|
|
281
|
-
// pass below resolves after extraction.
|
|
289
|
+
// Writes and the mode pass use the admitted output identity.
|
|
282
290
|
entry.path = relPath;
|
|
283
291
|
acceptedEntries.push({
|
|
284
292
|
path: relPath,
|
|
@@ -306,8 +314,7 @@ export async function extractArchive(params) {
|
|
|
306
314
|
}
|
|
307
315
|
catch (err) {
|
|
308
316
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
309
|
-
//
|
|
310
|
-
// emitter (tar.Unpack), which exposes Parser.abort().
|
|
317
|
+
// EventEmitter binds `this` to tar.Unpack, exposing abort().
|
|
311
318
|
const emitter = this;
|
|
312
319
|
emitter.abort?.(error);
|
|
313
320
|
}
|
|
@@ -321,19 +328,23 @@ export async function extractArchive(params) {
|
|
|
321
328
|
catch (error) {
|
|
322
329
|
throw normalizeTarParserError(createPipelineTimeoutError(error, deadline));
|
|
323
330
|
}
|
|
331
|
+
admission.finish();
|
|
324
332
|
for (const accepted of acceptedEntries) {
|
|
333
|
+
deadline.check();
|
|
325
334
|
const outputPath = resolveArchiveOutputPath({
|
|
326
335
|
rootDir: stagingDir,
|
|
327
336
|
relPath: accepted.path,
|
|
328
337
|
originalPath: accepted.path,
|
|
329
338
|
});
|
|
330
339
|
await fs.chmod(outputPath, accepted.mode);
|
|
340
|
+
deadline.check();
|
|
331
341
|
}
|
|
332
342
|
deadline.check();
|
|
333
343
|
await mergeExtractedTreeIntoDestination({
|
|
334
344
|
sourceDir: stagingDir,
|
|
335
345
|
destinationDir: params.destDir,
|
|
336
346
|
destinationRealDir,
|
|
347
|
+
deadline,
|
|
337
348
|
});
|
|
338
349
|
deadline.check();
|
|
339
350
|
},
|
|
@@ -349,10 +360,10 @@ export async function extractArchive(params) {
|
|
|
349
360
|
archivePath: params.archivePath,
|
|
350
361
|
destDir: params.destDir,
|
|
351
362
|
stripComponents: params.stripComponents,
|
|
352
|
-
limits
|
|
363
|
+
limits,
|
|
353
364
|
deadline,
|
|
354
365
|
entryModes: params.entryModes,
|
|
355
366
|
entryFilter: params.entryFilter,
|
|
356
|
-
onFiltered
|
|
367
|
+
onFiltered,
|
|
357
368
|
}));
|
|
358
369
|
}
|
package/dist/atomic.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { replaceFileAtomic, replaceFileAtomicSync, type ReplaceFileAtomicFileSystem, type ReplaceFileAtomicOptions, type ReplaceFileAtomicRestoreCleanup, type ReplaceFileAtomicRestoreFailureDetails, type ReplaceFileAtomicResult, type ReplaceFileCopyFallbackRestorePolicy, type ReplaceFileDestinationHardlinkPolicy, type ReplaceFileAtomicSyncFileSystem, type ReplaceFileAtomicSyncOptions, } from "./replace-file.js";
|
|
1
|
+
export { replaceFileAtomic, replaceFileAtomicSync, type RenameIdentityPolicy, type ReplaceFileAtomicFileSystem, type ReplaceFileAtomicOptions, type ReplaceFileAtomicRestoreCleanup, type ReplaceFileAtomicRestoreFailureDetails, type ReplaceFileAtomicResult, type ReplaceFileCopyFallbackRestorePolicy, type ReplaceFileDestinationHardlinkPolicy, type ReplaceFileAtomicSyncFileSystem, type ReplaceFileAtomicSyncOptions, } from "./replace-file.js";
|
|
2
2
|
export { writeTextAtomic, type WriteTextAtomicOptions } from "./text-atomic.js";
|
|
3
3
|
export { replaceDirectoryAtomic, type ReplaceDirectoryAtomicOptions } from "./replace-directory.js";
|
|
4
4
|
export { movePathWithCopyFallback, type MovePathWithCopyFallbackOptions } from "./move-path.js";
|
package/dist/atomic.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"atomic.d.ts","sourceRoot":"","sources":["../src/atomic.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,KAAK,2BAA2B,EAChC,KAAK,wBAAwB,EAC7B,KAAK,+BAA+B,EACpC,KAAK,sCAAsC,EAC3C,KAAK,uBAAuB,EAC5B,KAAK,oCAAoC,EACzC,KAAK,oCAAoC,EACzC,KAAK,+BAA+B,EACpC,KAAK,4BAA4B,GAClC,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAChF,OAAO,EAAE,sBAAsB,EAAE,KAAK,6BAA6B,EAAE,MAAM,wBAAwB,CAAC;AACpG,OAAO,EAAE,wBAAwB,EAAE,KAAK,+BAA+B,EAAE,MAAM,gBAAgB,CAAC"}
|
|
1
|
+
{"version":3,"file":"atomic.d.ts","sourceRoot":"","sources":["../src/atomic.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,KAAK,oBAAoB,EACzB,KAAK,2BAA2B,EAChC,KAAK,wBAAwB,EAC7B,KAAK,+BAA+B,EACpC,KAAK,sCAAsC,EAC3C,KAAK,uBAAuB,EAC5B,KAAK,oCAAoC,EACzC,KAAK,oCAAoC,EACzC,KAAK,+BAA+B,EACpC,KAAK,4BAA4B,GAClC,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAChF,OAAO,EAAE,sBAAsB,EAAE,KAAK,6BAA6B,EAAE,MAAM,wBAAwB,CAAC;AACpG,OAAO,EAAE,wBAAwB,EAAE,KAAK,+BAA+B,EAAE,MAAM,gBAAgB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bounded-read-stream.d.ts","sourceRoot":"","sources":["../src/bounded-read-stream.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,SAAS,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"bounded-read-stream.d.ts","sourceRoot":"","sources":["../src/bounded-read-stream.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,SAAS,EAAE,MAAM,aAAa,CAAC;AAIlD,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAmBnE;AAED,wBAAgB,uBAAuB,CACrC,MAAM,EAAE;IAAE,MAAM,EAAE;QAAE,gBAAgB,IAAI,MAAM,CAAC,cAAc,CAAA;KAAE,CAAA;CAAE,EACjE,QAAQ,EAAE,MAAM,GAAG,SAAS,GAC3B,MAAM,CAAC,cAAc,CAWvB"}
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { pipeline, Transform } from "node:stream";
|
|
2
|
+
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
2
3
|
import { FsSafeError } from "./errors.js";
|
|
3
4
|
export function createMaxBytesTransform(maxBytes) {
|
|
4
|
-
|
|
5
|
-
(!Number.isSafeInteger(maxBytes) || maxBytes < 0)) {
|
|
6
|
-
throw new RangeError("maxBytes must be a non-negative safe integer or Infinity");
|
|
7
|
-
}
|
|
5
|
+
normalizeMaxBytes(maxBytes);
|
|
8
6
|
let bytes = 0;
|
|
9
7
|
return new Transform({
|
|
10
8
|
transform(chunk, _encoding, callback) {
|