@openclaw/fs-safe 0.6.0 → 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 +50 -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 -26
- 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-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +10 -5
- 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 +12 -3
- package/dist/native-binding.d.ts.map +1 -1
- 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 +8 -9
- 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 +40 -26
- 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 +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/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 +194 -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 +8 -4
- package/docs/native-helper.md +15 -3
- package/docs/native.md +33 -6
- package/docs/output.md +10 -0
- 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 +4 -0
- package/docs/store.md +13 -0
- package/docs/temp.md +128 -14
- 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;AA0NpC,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,
|
|
@@ -151,7 +156,7 @@ async function extractZip(params) {
|
|
|
151
156
|
if (!shouldExtractArchiveEntry({
|
|
152
157
|
filter: params.entryFilter,
|
|
153
158
|
onFiltered: params.onFiltered,
|
|
154
|
-
entry: { path:
|
|
159
|
+
entry: { path: output.canonicalPath, kind: entryKind, size: entrySize },
|
|
155
160
|
})) {
|
|
156
161
|
continue;
|
|
157
162
|
}
|
|
@@ -166,6 +171,7 @@ async function extractZip(params) {
|
|
|
166
171
|
outPath: output.outPath,
|
|
167
172
|
originalPath: entry.name,
|
|
168
173
|
isDirectory: entry.dir,
|
|
174
|
+
deadline: params.deadline,
|
|
169
175
|
});
|
|
170
176
|
if (entry.dir) {
|
|
171
177
|
await fs.chmod(output.outPath, mode);
|
|
@@ -184,6 +190,7 @@ async function extractZip(params) {
|
|
|
184
190
|
sourceDir: stagingRealDir,
|
|
185
191
|
destinationDir: params.destDir,
|
|
186
192
|
destinationRealDir,
|
|
193
|
+
deadline: params.deadline,
|
|
187
194
|
});
|
|
188
195
|
params.deadline.check();
|
|
189
196
|
},
|
|
@@ -194,11 +201,14 @@ async function extractZip(params) {
|
|
|
194
201
|
}
|
|
195
202
|
}
|
|
196
203
|
export async function extractArchive(params) {
|
|
204
|
+
const onFiltered = resolveArchiveFilteredEntryPolicy(params.onFiltered);
|
|
197
205
|
const kind = params.kind ?? resolveArchiveKind(params.archivePath);
|
|
198
206
|
if (!kind) {
|
|
199
207
|
throw new Error(`unsupported archive: ${params.archivePath}`);
|
|
200
208
|
}
|
|
201
209
|
const label = kind === "zip" ? "extract zip" : "extract tar";
|
|
210
|
+
const limits = resolveExtractLimits(params.limits);
|
|
211
|
+
const tarLimits = resolveTarMeterLimits(limits);
|
|
202
212
|
const native = getNativeBinding();
|
|
203
213
|
if (native) {
|
|
204
214
|
await withExtractionDeadline(params.timeoutMs, label, async (deadline) => extractNativeArchive({
|
|
@@ -207,11 +217,12 @@ export async function extractArchive(params) {
|
|
|
207
217
|
destDir: params.destDir,
|
|
208
218
|
kind,
|
|
209
219
|
stripComponents: params.stripComponents,
|
|
210
|
-
limits
|
|
220
|
+
limits,
|
|
221
|
+
tarLimits,
|
|
211
222
|
deadline,
|
|
212
223
|
entryModes: params.entryModes,
|
|
213
224
|
entryFilter: params.entryFilter,
|
|
214
|
-
onFiltered
|
|
225
|
+
onFiltered,
|
|
215
226
|
}));
|
|
216
227
|
return;
|
|
217
228
|
}
|
|
@@ -222,17 +233,18 @@ export async function extractArchive(params) {
|
|
|
222
233
|
if (kind === "tar") {
|
|
223
234
|
await withExtractionDeadline(params.timeoutMs, label, async (deadline) => {
|
|
224
235
|
const tar = await importOptionalTar();
|
|
225
|
-
const limits = resolveExtractLimits(params.limits);
|
|
226
236
|
const stagedArchive = await stageArchiveFileForExtraction({
|
|
227
237
|
archivePath: params.archivePath,
|
|
228
238
|
limits,
|
|
229
239
|
deadline,
|
|
230
240
|
});
|
|
231
241
|
try {
|
|
242
|
+
const manifest = [];
|
|
232
243
|
await preflightTarMetadata({
|
|
233
244
|
archivePath: stagedArchive.path,
|
|
234
|
-
|
|
245
|
+
limits: tarLimits,
|
|
235
246
|
signal: deadline.signal,
|
|
247
|
+
onMember: (entry) => { manifest.push(entry); },
|
|
236
248
|
});
|
|
237
249
|
deadline.check();
|
|
238
250
|
const destinationRealDir = await prepareArchiveDestinationDir(params.destDir);
|
|
@@ -246,13 +258,14 @@ export async function extractArchive(params) {
|
|
|
246
258
|
stripComponents: params.stripComponents,
|
|
247
259
|
limits,
|
|
248
260
|
entryFilter: params.entryFilter,
|
|
249
|
-
onFiltered
|
|
261
|
+
onFiltered,
|
|
250
262
|
});
|
|
263
|
+
const admission = createTarAdmissionPlan(manifest, (entry) => {
|
|
264
|
+
deadline.check();
|
|
265
|
+
return checkTarEntrySafety(entry);
|
|
266
|
+
}, strip);
|
|
251
267
|
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.
|
|
268
|
+
// Extract privately, then merge through the safe-open boundary.
|
|
256
269
|
const extractor = tar.x({
|
|
257
270
|
cwd: stagingDir,
|
|
258
271
|
// fs-safe owns stripping: node-tar counts the `.` and empty path
|
|
@@ -265,20 +278,16 @@ export async function extractArchive(params) {
|
|
|
265
278
|
preserveOwner: false,
|
|
266
279
|
noMtime: true,
|
|
267
280
|
strict: true,
|
|
268
|
-
maxMetaEntrySize:
|
|
281
|
+
maxMetaEntrySize: tarLimits.maxMetaEntryBytes,
|
|
282
|
+
maxDecompressionRatio: NODE_TAR_RATIO_DISABLED,
|
|
269
283
|
filter(_entryPath, entry) {
|
|
270
284
|
try {
|
|
271
285
|
const info = readTarEntryInfo(entry);
|
|
272
|
-
|
|
273
|
-
return false;
|
|
274
|
-
}
|
|
275
|
-
const relPath = stripArchivePath(info.path, strip);
|
|
286
|
+
const relPath = admission.consume(info);
|
|
276
287
|
if (!relPath) {
|
|
277
288
|
return false;
|
|
278
289
|
}
|
|
279
|
-
//
|
|
280
|
-
// limited and collision-checked under, and that the mode
|
|
281
|
-
// pass below resolves after extraction.
|
|
290
|
+
// Writes and the mode pass use the admitted output identity.
|
|
282
291
|
entry.path = relPath;
|
|
283
292
|
acceptedEntries.push({
|
|
284
293
|
path: relPath,
|
|
@@ -306,8 +315,7 @@ export async function extractArchive(params) {
|
|
|
306
315
|
}
|
|
307
316
|
catch (err) {
|
|
308
317
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
309
|
-
//
|
|
310
|
-
// emitter (tar.Unpack), which exposes Parser.abort().
|
|
318
|
+
// EventEmitter binds `this` to tar.Unpack, exposing abort().
|
|
311
319
|
const emitter = this;
|
|
312
320
|
emitter.abort?.(error);
|
|
313
321
|
}
|
|
@@ -321,19 +329,23 @@ export async function extractArchive(params) {
|
|
|
321
329
|
catch (error) {
|
|
322
330
|
throw normalizeTarParserError(createPipelineTimeoutError(error, deadline));
|
|
323
331
|
}
|
|
332
|
+
admission.finish();
|
|
324
333
|
for (const accepted of acceptedEntries) {
|
|
334
|
+
deadline.check();
|
|
325
335
|
const outputPath = resolveArchiveOutputPath({
|
|
326
336
|
rootDir: stagingDir,
|
|
327
337
|
relPath: accepted.path,
|
|
328
338
|
originalPath: accepted.path,
|
|
329
339
|
});
|
|
330
340
|
await fs.chmod(outputPath, accepted.mode);
|
|
341
|
+
deadline.check();
|
|
331
342
|
}
|
|
332
343
|
deadline.check();
|
|
333
344
|
await mergeExtractedTreeIntoDestination({
|
|
334
345
|
sourceDir: stagingDir,
|
|
335
346
|
destinationDir: params.destDir,
|
|
336
347
|
destinationRealDir,
|
|
348
|
+
deadline,
|
|
337
349
|
});
|
|
338
350
|
deadline.check();
|
|
339
351
|
},
|
|
@@ -349,10 +361,10 @@ export async function extractArchive(params) {
|
|
|
349
361
|
archivePath: params.archivePath,
|
|
350
362
|
destDir: params.destDir,
|
|
351
363
|
stripComponents: params.stripComponents,
|
|
352
|
-
limits
|
|
364
|
+
limits,
|
|
353
365
|
deadline,
|
|
354
366
|
entryModes: params.entryModes,
|
|
355
367
|
entryFilter: params.entryFilter,
|
|
356
|
-
onFiltered
|
|
368
|
+
onFiltered,
|
|
357
369
|
}));
|
|
358
370
|
}
|
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) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bounded-read.d.ts","sourceRoot":"","sources":["../src/bounded-read.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"bounded-read.d.ts","sourceRoot":"","sources":["../src/bounded-read.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAMnD,KAAK,kBAAkB,GAAG,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;AAmDnD;;;;GAIG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,kBAAkB,EAC1B,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,CAAC,CAIjB;AAcD,oFAAoF;AACpF,wBAAsB,yBAAyB,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAI7F;AAED,mFAAmF;AACnF,wBAAgB,6BAA6B,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAalF"}
|