@openclaw/fs-safe 0.4.7 → 0.5.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 +45 -0
- package/README.md +82 -43
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +5 -4
- package/dist/archive-errors.d.ts +11 -0
- package/dist/archive-errors.d.ts.map +1 -0
- package/dist/archive-errors.js +16 -0
- package/dist/archive-input.d.ts +12 -0
- package/dist/archive-input.d.ts.map +1 -0
- package/dist/archive-input.js +72 -0
- package/dist/archive-kind.d.ts +1 -1
- package/dist/archive-kind.d.ts.map +1 -1
- package/dist/archive-kind.js +18 -0
- package/dist/archive-limits.d.ts +10 -0
- package/dist/archive-limits.d.ts.map +1 -1
- package/dist/archive-limits.js +18 -0
- package/dist/archive-native.d.ts +18 -0
- package/dist/archive-native.d.ts.map +1 -0
- package/dist/archive-native.js +115 -0
- package/dist/archive-options.d.ts +21 -0
- package/dist/archive-options.d.ts.map +1 -0
- package/dist/archive-options.js +1 -0
- package/dist/archive-policy.d.ts +20 -0
- package/dist/archive-policy.d.ts.map +1 -0
- package/dist/archive-policy.js +29 -0
- package/dist/archive-read.d.ts +6 -0
- package/dist/archive-read.d.ts.map +1 -0
- package/dist/archive-read.js +200 -0
- package/dist/archive-staging.d.ts +2 -5
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +19 -10
- package/dist/archive-tar-meta.d.ts +6 -0
- package/dist/archive-tar-meta.d.ts.map +1 -0
- package/dist/archive-tar-meta.js +143 -0
- package/dist/archive-tar-runtime.d.ts +43 -0
- package/dist/archive-tar-runtime.d.ts.map +1 -0
- package/dist/archive-tar-runtime.js +8 -0
- package/dist/archive-tar.d.ts +5 -1
- package/dist/archive-tar.d.ts.map +1 -1
- package/dist/archive-tar.js +24 -6
- package/dist/archive-zip-entry.d.ts +15 -0
- package/dist/archive-zip-entry.d.ts.map +1 -0
- package/dist/archive-zip-entry.js +17 -0
- package/dist/archive.d.ts +7 -17
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +111 -108
- package/dist/atomic.d.ts +1 -1
- package/dist/atomic.d.ts.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -1
- package/dist/containment.d.ts +2 -0
- package/dist/containment.d.ts.map +1 -0
- package/dist/containment.js +1 -0
- package/dist/durability.d.ts +2 -0
- package/dist/durability.d.ts.map +1 -1
- package/dist/durability.js +2 -0
- package/dist/errors.d.ts +4 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +2 -0
- package/dist/file-hash.d.ts +10 -0
- package/dist/file-hash.d.ts.map +1 -0
- package/dist/file-hash.js +73 -0
- package/dist/file-lock-sync.d.ts +35 -0
- package/dist/file-lock-sync.d.ts.map +1 -0
- package/dist/file-lock-sync.js +233 -0
- package/dist/file-lock.d.ts +2 -0
- package/dist/file-lock.d.ts.map +1 -1
- package/dist/file-lock.js +1 -0
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +4 -1
- package/dist/guarded-mkdir.d.ts +7 -1
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +45 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/json-document-store.d.ts.map +1 -1
- package/dist/json-document-store.js +30 -15
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +101 -17
- package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
- package/dist/native/darwin-x64/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
- package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
- package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
- package/dist/native-binding.d.ts +79 -0
- package/dist/native-binding.d.ts.map +1 -0
- package/dist/native-binding.js +1 -0
- package/dist/native-config.d.ts +17 -0
- package/dist/native-config.d.ts.map +1 -0
- package/dist/native-config.js +71 -0
- package/dist/native-operations.d.ts +21 -0
- package/dist/native-operations.d.ts.map +1 -0
- package/dist/native-operations.js +115 -0
- package/dist/native-pinned-write.d.ts +5 -0
- package/dist/native-pinned-write.d.ts.map +1 -0
- package/dist/native-pinned-write.js +115 -0
- package/dist/native.d.ts +14 -0
- package/dist/native.d.ts.map +1 -0
- package/dist/native.js +204 -0
- package/dist/output-sibling.d.ts +8 -0
- package/dist/output-sibling.d.ts.map +1 -0
- package/dist/output-sibling.js +122 -0
- package/dist/output.d.ts +2 -0
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +32 -5
- package/dist/owner-dacl.d.ts +31 -0
- package/dist/owner-dacl.d.ts.map +1 -0
- package/dist/owner-dacl.js +27 -0
- package/dist/path-policy.js +1 -1
- package/dist/permissions-public.d.ts +2 -0
- package/dist/permissions-public.d.ts.map +1 -1
- package/dist/permissions-public.js +2 -0
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +6 -0
- package/dist/pinned-operation.d.ts +2 -0
- package/dist/pinned-operation.d.ts.map +1 -0
- package/dist/pinned-operation.js +35 -0
- package/dist/pinned-write.d.ts +3 -16
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +20 -85
- package/dist/private-directory.d.ts +5 -0
- package/dist/private-directory.d.ts.map +1 -0
- package/dist/private-directory.js +13 -0
- package/dist/private-temp-workspace.d.ts +6 -2
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +40 -13
- package/dist/publish-file-failure.d.ts +27 -0
- package/dist/publish-file-failure.d.ts.map +1 -0
- package/dist/publish-file-failure.js +21 -0
- package/dist/publish-file.d.ts +21 -0
- package/dist/publish-file.d.ts.map +1 -0
- package/dist/publish-file.js +351 -0
- package/dist/read-opened-file.d.ts +3 -0
- package/dist/read-opened-file.d.ts.map +1 -1
- package/dist/read-opened-file.js +1 -0
- package/dist/replace-file-copy-fallback.d.ts +34 -0
- package/dist/replace-file-copy-fallback.d.ts.map +1 -0
- package/dist/replace-file-copy-fallback.js +339 -0
- package/dist/replace-file.d.ts +6 -2
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +39 -90
- package/dist/root-context.d.ts +4 -1
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +20 -1
- package/dist/root-impl.d.ts +5 -0
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +128 -225
- package/dist/root-path-existing.d.ts +3 -0
- package/dist/root-path-existing.d.ts.map +1 -0
- package/dist/root-path-existing.js +69 -0
- package/dist/root-path.d.ts +1 -2
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +67 -81
- package/dist/root-walk.d.ts +41 -0
- package/dist/root-walk.d.ts.map +1 -0
- package/dist/root-walk.js +114 -0
- package/dist/root.d.ts +2 -0
- package/dist/root.d.ts.map +1 -1
- package/dist/secret-file.d.ts +5 -2
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +20 -2
- package/dist/secret-read-async.d.ts +4 -0
- package/dist/secret-read-async.d.ts.map +1 -0
- package/dist/secret-read-async.js +95 -0
- package/dist/secret.d.ts +2 -1
- package/dist/secret.d.ts.map +1 -1
- package/dist/secret.js +2 -1
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +1 -0
- package/dist/sidecar-lock-handle.d.ts +20 -0
- package/dist/sidecar-lock-handle.d.ts.map +1 -0
- package/dist/sidecar-lock-handle.js +28 -0
- package/dist/sidecar-lock-policy.d.ts +10 -0
- package/dist/sidecar-lock-policy.d.ts.map +1 -0
- package/dist/sidecar-lock-policy.js +29 -0
- package/dist/sidecar-lock-reclaim.d.ts +22 -6
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +106 -20
- package/dist/sidecar-lock-types.d.ts +56 -0
- package/dist/sidecar-lock-types.d.ts.map +1 -0
- package/dist/sidecar-lock-types.js +1 -0
- package/dist/sidecar-lock.d.ts +2 -45
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +94 -55
- package/dist/temp-cleanup.d.ts +7 -1
- package/dist/temp-cleanup.d.ts.map +1 -1
- package/dist/temp-cleanup.js +35 -5
- package/dist/temp.d.ts +2 -1
- package/dist/temp.d.ts.map +1 -1
- package/dist/test-hooks.d.ts +3 -0
- package/dist/test-hooks.d.ts.map +1 -1
- package/dist/windows-permissions-native.d.ts +9 -0
- package/dist/windows-permissions-native.d.ts.map +1 -0
- package/dist/windows-permissions-native.js +31 -0
- package/docs/archive.md +131 -13
- package/docs/atomic.md +30 -1
- package/docs/config.md +51 -21
- package/docs/durability.md +211 -1
- package/docs/errors.md +36 -6
- package/docs/filename.md +0 -0
- package/docs/index.md +11 -8
- package/docs/install.md +27 -26
- package/docs/json-store.md +18 -3
- package/docs/migrating-to-0.5.md +196 -0
- package/docs/native-helper.md +85 -0
- package/docs/native.md +134 -0
- package/docs/output.md +37 -11
- package/docs/path.md +1 -1
- package/docs/permissions.md +84 -1
- package/docs/quickstart.md +5 -2
- package/docs/reading.md +4 -4
- package/docs/root.md +31 -15
- package/docs/secret-file.md +50 -1
- package/docs/security-model.md +29 -8
- package/docs/sidecar-lock.md +92 -7
- package/docs/temp.md +24 -1
- package/docs/test-hooks.md +22 -2
- package/docs/testing.md +18 -7
- package/docs/types.md +3 -1
- package/docs/walk.md +55 -0
- package/docs/writing.md +3 -3
- package/package.json +8 -2
- package/dist/pinned-helper.d.ts +0 -9
- package/dist/pinned-helper.d.ts.map +0 -1
- package/dist/pinned-helper.js +0 -19
- package/dist/pinned-path.d.ts +0 -7
- package/dist/pinned-path.d.ts.map +0 -1
- package/dist/pinned-path.js +0 -21
- package/dist/pinned-python-config.d.ts +0 -9
- package/dist/pinned-python-config.d.ts.map +0 -1
- package/dist/pinned-python-config.js +0 -38
- package/dist/pinned-python.d.ts +0 -12
- package/dist/pinned-python.d.ts.map +0 -1
- package/dist/pinned-python.js +0 -688
- package/docs/python-helper.md +0 -103
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ExtractionDeadline } from "./archive-deadline.js";
|
|
2
|
+
import type { ArchiveKind } from "./archive-kind.js";
|
|
3
|
+
import { type ArchiveExtractLimits } from "./archive-limits.js";
|
|
4
|
+
import type { ExtractArchiveOptions } from "./archive-options.js";
|
|
5
|
+
import type { NativeBinding } from "./native.js";
|
|
6
|
+
export declare function extractNativeArchive(params: {
|
|
7
|
+
binding: NativeBinding;
|
|
8
|
+
archivePath: string;
|
|
9
|
+
destDir: string;
|
|
10
|
+
kind: ArchiveKind;
|
|
11
|
+
stripComponents?: number;
|
|
12
|
+
limits?: ArchiveExtractLimits;
|
|
13
|
+
deadline: ExtractionDeadline;
|
|
14
|
+
entryModes?: ExtractArchiveOptions["entryModes"];
|
|
15
|
+
entryFilter?: ExtractArchiveOptions["entryFilter"];
|
|
16
|
+
onFiltered?: ExtractArchiveOptions["onFiltered"];
|
|
17
|
+
}): Promise<void>;
|
|
18
|
+
//# sourceMappingURL=archive-native.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive-native.d.ts","sourceRoot":"","sources":["../src/archive-native.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAEhE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAOL,KAAK,oBAAoB,EAC1B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAOlE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAoBjD,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,OAAO,EAAE,aAAa,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,WAAW,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,UAAU,CAAC,EAAE,qBAAqB,CAAC,YAAY,CAAC,CAAC;IACjD,WAAW,CAAC,EAAE,qBAAqB,CAAC,aAAa,CAAC,CAAC;IACnD,UAAU,CAAC,EAAE,qBAAqB,CAAC,YAAY,CAAC,CAAC;CAClD,GAAG,OAAO,CAAC,IAAI,CAAC,CAsHhB"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { constants as fsConstants } from "node:fs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { ArchiveFormatError, ArchiveSecurityError } from "./archive-errors.js";
|
|
4
|
+
import { resolveArchiveOutputPath, stripArchivePath, validateArchiveEntryPath } from "./archive-entry.js";
|
|
5
|
+
import { stageArchiveFileForExtraction } from "./archive-input.js";
|
|
6
|
+
import { ARCHIVE_LIMIT_ERROR_CODE, ArchiveLimitError, assertArchiveEntryCountWithinLimit, assertArchiveEntryPathComponentsWithinLimit, createByteBudgetTracker, resolveExtractLimits, } from "./archive-limits.js";
|
|
7
|
+
import { resolveArchiveEntryMode, shouldExtractArchiveEntry } from "./archive-policy.js";
|
|
8
|
+
import { mergeExtractedTreeIntoDestination, prepareArchiveDestinationDir, withStagedArchiveDestination, } from "./archive-staging.js";
|
|
9
|
+
function policyKind(kind) {
|
|
10
|
+
if (kind === "file" || kind === "directory")
|
|
11
|
+
return kind;
|
|
12
|
+
if (kind === "symlink" || kind === "hardlink")
|
|
13
|
+
return "symlink";
|
|
14
|
+
return "other";
|
|
15
|
+
}
|
|
16
|
+
function throwMappedNativeError(error) {
|
|
17
|
+
if (error instanceof Error) {
|
|
18
|
+
for (const code of Object.values(ARCHIVE_LIMIT_ERROR_CODE)) {
|
|
19
|
+
if (error.message.includes(code))
|
|
20
|
+
throw new ArchiveLimitError(code);
|
|
21
|
+
}
|
|
22
|
+
if (error.message.includes("archive-header-invalid")) {
|
|
23
|
+
throw new ArchiveFormatError(error.message, { cause: error });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
export async function extractNativeArchive(params) {
|
|
29
|
+
const limits = resolveExtractLimits(params.limits);
|
|
30
|
+
const stagedArchive = await stageArchiveFileForExtraction({
|
|
31
|
+
archivePath: params.archivePath,
|
|
32
|
+
limits,
|
|
33
|
+
deadline: params.deadline,
|
|
34
|
+
});
|
|
35
|
+
try {
|
|
36
|
+
const destinationRealDir = await prepareArchiveDestinationDir(params.destDir);
|
|
37
|
+
await withStagedArchiveDestination({
|
|
38
|
+
destinationRealDir,
|
|
39
|
+
run: async (stagingDir) => {
|
|
40
|
+
params.deadline.check();
|
|
41
|
+
const manifest = await params.binding
|
|
42
|
+
.inspectArchiveNative(stagedArchive.path, params.kind, limits.maxEntries, limits.maxMetaEntryBytes, limits.maxArchiveBytes, params.deadline.signal)
|
|
43
|
+
.catch(throwMappedNativeError);
|
|
44
|
+
params.deadline.check();
|
|
45
|
+
assertArchiveEntryCountWithinLimit(manifest.length, limits);
|
|
46
|
+
const strip = Math.max(0, Math.floor(params.stripComponents ?? 0));
|
|
47
|
+
const budget = createByteBudgetTracker(limits);
|
|
48
|
+
const plan = [];
|
|
49
|
+
for (const entry of manifest) {
|
|
50
|
+
params.deadline.check();
|
|
51
|
+
validateArchiveEntryPath(entry.path);
|
|
52
|
+
const relPath = stripArchivePath(entry.path, strip);
|
|
53
|
+
if (!relPath)
|
|
54
|
+
continue;
|
|
55
|
+
validateArchiveEntryPath(relPath);
|
|
56
|
+
assertArchiveEntryPathComponentsWithinLimit(relPath, limits);
|
|
57
|
+
resolveArchiveOutputPath({ rootDir: stagingDir, relPath, originalPath: entry.path });
|
|
58
|
+
const kind = policyKind(entry.kind);
|
|
59
|
+
if (!shouldExtractArchiveEntry({
|
|
60
|
+
filter: params.entryFilter,
|
|
61
|
+
onFiltered: params.onFiltered,
|
|
62
|
+
entry: { path: entry.path, kind, size: entry.size },
|
|
63
|
+
})) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (entry.kind === "sparse") {
|
|
67
|
+
throw new ArchiveFormatError(`GNU sparse archive entry is not supported: ${entry.path}`);
|
|
68
|
+
}
|
|
69
|
+
if (kind === "symlink") {
|
|
70
|
+
const label = params.kind === "zip" ? "zip" : "tar";
|
|
71
|
+
throw new ArchiveSecurityError("entry-link", `${label} entry is a link: ${entry.path}`);
|
|
72
|
+
}
|
|
73
|
+
if (!Number.isSafeInteger(entry.size) || entry.size < 0) {
|
|
74
|
+
throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT);
|
|
75
|
+
}
|
|
76
|
+
if (kind === "file") {
|
|
77
|
+
budget.startEntry();
|
|
78
|
+
budget.addEntrySize(entry.size);
|
|
79
|
+
}
|
|
80
|
+
if (kind === "file" || kind === "directory") {
|
|
81
|
+
plan.push({
|
|
82
|
+
index: entry.index,
|
|
83
|
+
path: relPath,
|
|
84
|
+
kind,
|
|
85
|
+
size: entry.size,
|
|
86
|
+
mode: resolveArchiveEntryMode({
|
|
87
|
+
kind,
|
|
88
|
+
archivedMode: entry.mode,
|
|
89
|
+
policy: params.entryModes,
|
|
90
|
+
}),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const directory = await fs.open(stagingDir, fsConstants.O_RDONLY |
|
|
95
|
+
(typeof fsConstants.O_DIRECTORY === "number" ? fsConstants.O_DIRECTORY : 0));
|
|
96
|
+
try {
|
|
97
|
+
params.deadline.check();
|
|
98
|
+
await params.binding.extractArchiveNative(stagedArchive.path, params.kind, directory.fd, plan, limits.maxMetaEntryBytes, params.deadline.signal);
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
await directory.close().catch(() => undefined);
|
|
102
|
+
}
|
|
103
|
+
params.deadline.check();
|
|
104
|
+
await mergeExtractedTreeIntoDestination({
|
|
105
|
+
sourceDir: stagingDir,
|
|
106
|
+
destinationDir: params.destDir,
|
|
107
|
+
destinationRealDir,
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
await stagedArchive.cleanup();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ArchiveKind } from "./archive-kind.js";
|
|
2
|
+
import type { ArchiveExtractLimits } from "./archive-limits.js";
|
|
3
|
+
import type { ArchiveEntryFilter, ArchiveEntryModePolicy, ArchiveFilteredEntryPolicy } from "./archive-policy.js";
|
|
4
|
+
export type ArchiveLogger = {
|
|
5
|
+
info?: (message: string) => void;
|
|
6
|
+
warn?: (message: string) => void;
|
|
7
|
+
};
|
|
8
|
+
export type ExtractArchiveOptions = {
|
|
9
|
+
archivePath: string;
|
|
10
|
+
destDir: string;
|
|
11
|
+
timeoutMs: number;
|
|
12
|
+
kind?: ArchiveKind;
|
|
13
|
+
stripComponents?: number;
|
|
14
|
+
tarGzip?: boolean;
|
|
15
|
+
limits?: ArchiveExtractLimits;
|
|
16
|
+
logger?: ArchiveLogger;
|
|
17
|
+
entryModes?: ArchiveEntryModePolicy;
|
|
18
|
+
entryFilter?: ArchiveEntryFilter;
|
|
19
|
+
onFiltered?: ArchiveFilteredEntryPolicy;
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=archive-options.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive-options.d.ts","sourceRoot":"","sources":["../src/archive-options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAChE,OAAO,KAAK,EACV,kBAAkB,EAClB,sBAAsB,EACtB,0BAA0B,EAC3B,MAAM,qBAAqB,CAAC;AAE7B,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,UAAU,CAAC,EAAE,sBAAsB,CAAC;IACpC,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,UAAU,CAAC,EAAE,0BAA0B,CAAC;CACzC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type ArchiveEntryKind = "file" | "directory" | "symlink" | "other";
|
|
2
|
+
export type ArchiveEntryModePolicy = "clamp" | "preserve";
|
|
3
|
+
export type ArchiveFilteredEntryPolicy = "reject-archive" | "skip-entry";
|
|
4
|
+
export type ArchiveEntryFilter = (entry: {
|
|
5
|
+
path: string;
|
|
6
|
+
kind: ArchiveEntryKind;
|
|
7
|
+
size: number;
|
|
8
|
+
}) => "extract" | "skip";
|
|
9
|
+
export declare function archiveEntryKindFromTarType(type: string): ArchiveEntryKind;
|
|
10
|
+
export declare function resolveArchiveEntryMode(params: {
|
|
11
|
+
kind: "file" | "directory";
|
|
12
|
+
archivedMode?: number;
|
|
13
|
+
policy?: ArchiveEntryModePolicy;
|
|
14
|
+
}): number;
|
|
15
|
+
export declare function shouldExtractArchiveEntry(params: {
|
|
16
|
+
filter?: ArchiveEntryFilter;
|
|
17
|
+
onFiltered?: ArchiveFilteredEntryPolicy;
|
|
18
|
+
entry: Parameters<ArchiveEntryFilter>[0];
|
|
19
|
+
}): boolean;
|
|
20
|
+
//# sourceMappingURL=archive-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive-policy.d.ts","sourceRoot":"","sources":["../src/archive-policy.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC;AAC1E,MAAM,MAAM,sBAAsB,GAAG,OAAO,GAAG,UAAU,CAAC;AAC1D,MAAM,MAAM,0BAA0B,GAAG,gBAAgB,GAAG,YAAY,CAAC;AACzE,MAAM,MAAM,kBAAkB,GAAG,CAAC,KAAK,EAAE;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,gBAAgB,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;CACd,KAAK,SAAS,GAAG,MAAM,CAAC;AAEzB,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,CAK1E;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,sBAAsB,CAAC;CACjC,GAAG,MAAM,CAST;AAED,wBAAgB,yBAAyB,CAAC,MAAM,EAAE;IAChD,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,UAAU,CAAC,EAAE,0BAA0B,CAAC;IACxC,KAAK,EAAE,UAAU,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,CAAC;CAC1C,GAAG,OAAO,CAWV"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ArchiveSecurityError } from "./archive-errors.js";
|
|
2
|
+
export function archiveEntryKindFromTarType(type) {
|
|
3
|
+
if (type === "Directory" || type === "GNUDumpDir")
|
|
4
|
+
return "directory";
|
|
5
|
+
if (type === "File" || type === "OldFile" || type === "ContiguousFile")
|
|
6
|
+
return "file";
|
|
7
|
+
if (type === "SymbolicLink" || type === "Link")
|
|
8
|
+
return "symlink";
|
|
9
|
+
return "other";
|
|
10
|
+
}
|
|
11
|
+
export function resolveArchiveEntryMode(params) {
|
|
12
|
+
const archivedMode = (params.archivedMode ?? 0) & 0o777;
|
|
13
|
+
if (params.policy === "preserve") {
|
|
14
|
+
return archivedMode || (params.kind === "directory" ? 0o755 : 0o644);
|
|
15
|
+
}
|
|
16
|
+
if (params.kind === "directory") {
|
|
17
|
+
return 0o755;
|
|
18
|
+
}
|
|
19
|
+
return archivedMode & 0o100 ? 0o755 : 0o644;
|
|
20
|
+
}
|
|
21
|
+
export function shouldExtractArchiveEntry(params) {
|
|
22
|
+
if (!params.filter || params.filter(params.entry) === "extract") {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
if ((params.onFiltered ?? "reject-archive") === "reject-archive") {
|
|
26
|
+
throw new ArchiveSecurityError("entry-filtered", `archive entry rejected by filter: ${params.entry.path}`);
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive-read.d.ts","sourceRoot":"","sources":["../src/archive-read.ts"],"names":[],"mappings":"AASA,OAAO,EAAsB,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAkLzE,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,WAAW,CAAA;CAAE,GAChD,OAAO,CAAC,MAAM,CAAC,CAwEjB"}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import fsSync from "node:fs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { Readable } from "node:stream";
|
|
4
|
+
import { readFileHandleBounded } from "./bounded-read.js";
|
|
5
|
+
import { ArchiveFormatError } from "./archive-errors.js";
|
|
6
|
+
import { normalizeArchiveEntryPath, validateArchiveEntryPath, } from "./archive-entry.js";
|
|
7
|
+
import { resolveArchiveKind } from "./archive-kind.js";
|
|
8
|
+
import { DEFAULT_MAX_ARCHIVE_BYTES_ZIP, DEFAULT_MAX_ENTRIES, DEFAULT_MAX_META_ENTRY_BYTES, ArchiveLimitError, ARCHIVE_LIMIT_ERROR_CODE, } from "./archive-limits.js";
|
|
9
|
+
import { readTarEntryInfo } from "./archive-tar.js";
|
|
10
|
+
import { preflightTarMetadata } from "./archive-tar-meta.js";
|
|
11
|
+
import { importOptionalTar } from "./archive-tar-runtime.js";
|
|
12
|
+
import { loadZipArchiveWithPreflight } from "./archive-zip-preflight.js";
|
|
13
|
+
import { FsSafeError } from "./errors.js";
|
|
14
|
+
import { sameFileIdentity } from "./file-identity.js";
|
|
15
|
+
import { getNativeBinding } from "./native.js";
|
|
16
|
+
import { tempFile } from "./temp-target.js";
|
|
17
|
+
const ZIP_UNIX_FILE_TYPE_MASK = 0o170000;
|
|
18
|
+
const ZIP_UNIX_SYMLINK_TYPE = 0o120000;
|
|
19
|
+
function normalizedRequestedEntry(entryPath) {
|
|
20
|
+
validateArchiveEntryPath(entryPath, { escapeLabel: "archive root" });
|
|
21
|
+
const normalized = normalizeArchiveEntryPath(entryPath).replace(/^\.\//, "");
|
|
22
|
+
if (!normalized || normalized.endsWith("/")) {
|
|
23
|
+
throw new Error(`archive entry is not a file: ${entryPath}`);
|
|
24
|
+
}
|
|
25
|
+
return normalized;
|
|
26
|
+
}
|
|
27
|
+
async function readStreamBounded(stream, maxBytes) {
|
|
28
|
+
if (!(Symbol.asyncIterator in Object(stream))) {
|
|
29
|
+
return await new Promise((resolve, reject) => {
|
|
30
|
+
const readable = stream;
|
|
31
|
+
const chunks = [];
|
|
32
|
+
let total = 0;
|
|
33
|
+
readable.on("data", (chunk) => {
|
|
34
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
35
|
+
total += buffer.length;
|
|
36
|
+
if (total > maxBytes) {
|
|
37
|
+
readable.pause();
|
|
38
|
+
reject(new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
chunks.push(buffer);
|
|
42
|
+
});
|
|
43
|
+
readable.once("end", () => resolve(Buffer.concat(chunks, total)));
|
|
44
|
+
readable.once("error", reject);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const chunks = [];
|
|
48
|
+
let total = 0;
|
|
49
|
+
for await (const chunk of stream) {
|
|
50
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
51
|
+
total += buffer.length;
|
|
52
|
+
if (total > maxBytes) {
|
|
53
|
+
throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT);
|
|
54
|
+
}
|
|
55
|
+
chunks.push(buffer);
|
|
56
|
+
}
|
|
57
|
+
return Buffer.concat(chunks, total);
|
|
58
|
+
}
|
|
59
|
+
async function stageArchiveInput(archivePath) {
|
|
60
|
+
const resolved = await fs.realpath(archivePath);
|
|
61
|
+
const before = await fs.lstat(archivePath);
|
|
62
|
+
if (before.isSymbolicLink() || !before.isFile()) {
|
|
63
|
+
throw new Error(`archive is not a regular file: ${archivePath}`);
|
|
64
|
+
}
|
|
65
|
+
const noFollow = process.platform !== "win32" && typeof fsSync.constants.O_NOFOLLOW === "number"
|
|
66
|
+
? fsSync.constants.O_NOFOLLOW
|
|
67
|
+
: 0;
|
|
68
|
+
const handle = await fs.open(resolved, fsSync.constants.O_RDONLY | noFollow);
|
|
69
|
+
const staged = await tempFile({ prefix: "fs-safe-archive-read", fileName: "archive.bin" });
|
|
70
|
+
try {
|
|
71
|
+
const opened = await handle.stat();
|
|
72
|
+
const current = await fs.lstat(resolved);
|
|
73
|
+
if (!opened.isFile() ||
|
|
74
|
+
!current.isFile() ||
|
|
75
|
+
!sameFileIdentity(before, opened) ||
|
|
76
|
+
!sameFileIdentity(current, opened)) {
|
|
77
|
+
throw new Error("archive changed during validation");
|
|
78
|
+
}
|
|
79
|
+
const buffer = await readFileHandleBounded(handle, DEFAULT_MAX_ARCHIVE_BYTES_ZIP);
|
|
80
|
+
await fs.writeFile(staged.path, buffer, { flag: "wx", mode: 0o600 });
|
|
81
|
+
return { path: staged.path, buffer, cleanup: staged.cleanup };
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
await staged.cleanup().catch(() => undefined);
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
await handle.close().catch(() => undefined);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function readZipEntry(buffer, entryPath, maxBytes) {
|
|
92
|
+
const archive = await loadZipArchiveWithPreflight(buffer, {
|
|
93
|
+
maxArchiveBytes: DEFAULT_MAX_ARCHIVE_BYTES_ZIP,
|
|
94
|
+
maxEntryBytes: maxBytes,
|
|
95
|
+
maxExtractedBytes: maxBytes,
|
|
96
|
+
});
|
|
97
|
+
const entry = archive.files[entryPath];
|
|
98
|
+
if (!entry || entry.dir) {
|
|
99
|
+
throw new Error(`archive entry not found: ${entryPath}`);
|
|
100
|
+
}
|
|
101
|
+
if (typeof entry.unixPermissions === "number" &&
|
|
102
|
+
(entry.unixPermissions & ZIP_UNIX_FILE_TYPE_MASK) === ZIP_UNIX_SYMLINK_TYPE) {
|
|
103
|
+
throw new Error(`archive entry is a link: ${entryPath}`);
|
|
104
|
+
}
|
|
105
|
+
const stream = typeof entry.nodeStream === "function"
|
|
106
|
+
? entry.nodeStream()
|
|
107
|
+
: Readable.from(await entry.async("nodebuffer"));
|
|
108
|
+
return await readStreamBounded(stream, maxBytes);
|
|
109
|
+
}
|
|
110
|
+
async function readTarEntry(archivePath, entryPath, maxBytes) {
|
|
111
|
+
const tar = await importOptionalTar();
|
|
112
|
+
await preflightTarMetadata({
|
|
113
|
+
archivePath,
|
|
114
|
+
maxMetaEntryBytes: DEFAULT_MAX_META_ENTRY_BYTES,
|
|
115
|
+
});
|
|
116
|
+
let matched;
|
|
117
|
+
await tar.t({
|
|
118
|
+
file: archivePath,
|
|
119
|
+
strict: true,
|
|
120
|
+
maxMetaEntrySize: DEFAULT_MAX_META_ENTRY_BYTES,
|
|
121
|
+
onReadEntry(entry) {
|
|
122
|
+
const info = readTarEntryInfo(entry);
|
|
123
|
+
validateArchiveEntryPath(info.path, { escapeLabel: "archive root" });
|
|
124
|
+
const normalized = normalizeArchiveEntryPath(info.path).replace(/^\.\//, "");
|
|
125
|
+
if (normalized !== entryPath) {
|
|
126
|
+
entry.resume();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (info.type !== "File" && info.type !== "OldFile" && info.type !== "ContiguousFile") {
|
|
130
|
+
matched = Promise.reject(new Error(`archive entry is not a file: ${entryPath}`));
|
|
131
|
+
entry.resume();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (info.size > maxBytes) {
|
|
135
|
+
matched = Promise.reject(new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT));
|
|
136
|
+
entry.resume();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
matched = readStreamBounded(entry, maxBytes);
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
if (!matched) {
|
|
143
|
+
throw new Error(`archive entry not found: ${entryPath}`);
|
|
144
|
+
}
|
|
145
|
+
return await matched;
|
|
146
|
+
}
|
|
147
|
+
export async function readArchiveEntry(archivePath, entryPath, options) {
|
|
148
|
+
if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 0) {
|
|
149
|
+
throw new RangeError("maxBytes must be a non-negative safe integer");
|
|
150
|
+
}
|
|
151
|
+
const kind = options.kind ?? resolveArchiveKind(archivePath);
|
|
152
|
+
if (!kind) {
|
|
153
|
+
throw new Error(`unsupported archive: ${archivePath}`);
|
|
154
|
+
}
|
|
155
|
+
const requestedEntry = normalizedRequestedEntry(entryPath);
|
|
156
|
+
const staged = await stageArchiveInput(archivePath);
|
|
157
|
+
try {
|
|
158
|
+
const native = getNativeBinding();
|
|
159
|
+
if (native) {
|
|
160
|
+
try {
|
|
161
|
+
const signal = new AbortController().signal;
|
|
162
|
+
const manifest = await native.inspectArchiveNative(staged.path, kind, DEFAULT_MAX_ENTRIES, DEFAULT_MAX_META_ENTRY_BYTES, DEFAULT_MAX_ARCHIVE_BYTES_ZIP, signal);
|
|
163
|
+
let rawEntryPath;
|
|
164
|
+
for (const entry of manifest) {
|
|
165
|
+
validateArchiveEntryPath(entry.path, { escapeLabel: "archive root" });
|
|
166
|
+
const normalized = normalizeArchiveEntryPath(entry.path).replace(/^\.\//, "");
|
|
167
|
+
if (normalized === requestedEntry) {
|
|
168
|
+
if (entry.kind !== "file") {
|
|
169
|
+
throw new Error(`archive entry is not a file: ${entryPath}`);
|
|
170
|
+
}
|
|
171
|
+
rawEntryPath = entry.path;
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (!rawEntryPath)
|
|
176
|
+
throw new Error(`archive entry not found: ${entryPath}`);
|
|
177
|
+
return await native.readArchiveEntryNative(staged.path, kind, rawEntryPath, options.maxBytes, DEFAULT_MAX_ENTRIES, DEFAULT_MAX_META_ENTRY_BYTES, signal);
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
if (error instanceof Error &&
|
|
181
|
+
error.message.includes(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT)) {
|
|
182
|
+
throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT);
|
|
183
|
+
}
|
|
184
|
+
if (error instanceof Error && error.message.includes("archive-header-invalid")) {
|
|
185
|
+
throw new ArchiveFormatError(error.message, { cause: error });
|
|
186
|
+
}
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (kind === "tar-zstd" || kind === "tar-bzip2") {
|
|
191
|
+
throw new FsSafeError("helper-unavailable", `${kind} archives require a supported bundled native binding`);
|
|
192
|
+
}
|
|
193
|
+
return kind === "zip"
|
|
194
|
+
? await readZipEntry(staged.buffer, requestedEntry, options.maxBytes)
|
|
195
|
+
: await readTarEntry(staged.path, requestedEntry, options.maxBytes);
|
|
196
|
+
}
|
|
197
|
+
finally {
|
|
198
|
+
await staged.cleanup();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
code: ArchiveSecurityErrorCode;
|
|
4
|
-
constructor(code: ArchiveSecurityErrorCode, message: string, options?: ErrorOptions);
|
|
5
|
-
}
|
|
1
|
+
import { ArchiveSecurityError } from "./archive-errors.js";
|
|
2
|
+
export { ArchiveSecurityError, type ArchiveSecurityErrorCode } from "./archive-errors.js";
|
|
6
3
|
export declare function prepareArchiveDestinationDir(destDir: string): Promise<string>;
|
|
7
4
|
export declare function prepareArchiveOutputPath(params: {
|
|
8
5
|
destinationDir: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive-staging.d.ts","sourceRoot":"","sources":["../src/archive-staging.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"archive-staging.d.ts","sourceRoot":"","sources":["../src/archive-staging.ts"],"names":[],"mappings":"AAQA,OAAO,EACL,oBAAoB,EAErB,MAAM,qBAAqB,CAAC;AAU7B,OAAO,EAAE,oBAAoB,EAAE,KAAK,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAkC1F,wBAAsB,4BAA4B,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA4BnF;AA4DD,wBAAsB,wBAAwB,CAAC,MAAM,EAAE;IACrD,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,OAAO,CAAC;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuChB;AAyHD,wBAAsB,4BAA4B,CAAC,CAAC,EAAE,MAAM,EAAE;IAC5D,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,GAAG,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;CACzC,GAAG,OAAO,CAAC,CAAC,CAAC,CA6Bb;AAED,wBAAsB,iCAAiC,CAAC,MAAM,EAAE;IAC9D,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;CAC5B,GAAG,OAAO,CAAC,IAAI,CAAC,CAgFhB;AAED,wBAAgB,kCAAkC,CAAC,YAAY,EAAE,MAAM,GAAG,oBAAoB,CAE7F"}
|
package/dist/archive-staging.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import fsSync from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, } from "./directory-guard.js";
|
|
5
|
+
import { ArchiveSecurityError, } from "./archive-errors.js";
|
|
5
6
|
import { FsSafeError } from "./errors.js";
|
|
6
7
|
import { resolveOpenedFileRealPathForHandle, root } from "./root.js";
|
|
7
8
|
import { isNotFoundPathError, isPathInside } from "./path.js";
|
|
@@ -9,14 +10,7 @@ import { resolveSecureTempRoot } from "./secure-temp-dir.js";
|
|
|
9
10
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
10
11
|
const ERROR_ARCHIVE_ENTRY_TRAVERSES_SYMLINK = "archive entry traverses symlink in destination";
|
|
11
12
|
const ARCHIVE_STAGING_MODE = 0o700;
|
|
12
|
-
export
|
|
13
|
-
code;
|
|
14
|
-
constructor(code, message, options) {
|
|
15
|
-
super(message, options);
|
|
16
|
-
this.code = code;
|
|
17
|
-
this.name = "ArchiveSecurityError";
|
|
18
|
-
}
|
|
19
|
-
}
|
|
13
|
+
export { ArchiveSecurityError } from "./archive-errors.js";
|
|
20
14
|
function symlinkTraversalError(originalPath) {
|
|
21
15
|
return new ArchiveSecurityError("destination-symlink-traversal", `${ERROR_ARCHIVE_ENTRY_TRAVERSES_SYMLINK}: ${originalPath}`);
|
|
22
16
|
}
|
|
@@ -98,6 +92,17 @@ async function assertResolvedInsideDestination(params) {
|
|
|
98
92
|
throw symlinkTraversalError(params.originalPath);
|
|
99
93
|
}
|
|
100
94
|
}
|
|
95
|
+
async function mkdirArchiveOutput(params) {
|
|
96
|
+
try {
|
|
97
|
+
await params.targetRoot.mkdir(params.relativePath);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (error instanceof FsSafeError) {
|
|
101
|
+
throw symlinkTraversalError(params.originalPath);
|
|
102
|
+
}
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
101
106
|
export async function prepareArchiveOutputPath(params) {
|
|
102
107
|
const targetRoot = await root(params.destinationRealDir);
|
|
103
108
|
const destinationGuard = await createDirectoryIdentityGuard(params.destinationRealDir);
|
|
@@ -110,7 +115,7 @@ export async function prepareArchiveOutputPath(params) {
|
|
|
110
115
|
if (params.isDirectory) {
|
|
111
116
|
await getFsSafeTestHooks()?.beforeArchiveOutputMutation?.("mkdir", params.outPath);
|
|
112
117
|
await assertDirectoryIdentityGuard(destinationGuard);
|
|
113
|
-
await targetRoot.
|
|
118
|
+
await mkdirArchiveOutput({ targetRoot, relativePath: relPath, originalPath: params.originalPath });
|
|
114
119
|
await assertDirectoryIdentityGuard(destinationGuard);
|
|
115
120
|
await assertResolvedInsideDestination({
|
|
116
121
|
destinationRealDir: params.destinationRealDir,
|
|
@@ -123,7 +128,11 @@ export async function prepareArchiveOutputPath(params) {
|
|
|
123
128
|
if (parentRel !== ".") {
|
|
124
129
|
await getFsSafeTestHooks()?.beforeArchiveOutputMutation?.("mkdir", path.dirname(params.outPath));
|
|
125
130
|
await assertDirectoryIdentityGuard(destinationGuard);
|
|
126
|
-
await
|
|
131
|
+
await mkdirArchiveOutput({
|
|
132
|
+
targetRoot,
|
|
133
|
+
relativePath: parentRel,
|
|
134
|
+
originalPath: params.originalPath,
|
|
135
|
+
});
|
|
127
136
|
await assertDirectoryIdentityGuard(destinationGuard);
|
|
128
137
|
}
|
|
129
138
|
await assertResolvedInsideDestination({
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive-tar-meta.d.ts","sourceRoot":"","sources":["../src/archive-tar-meta.ts"],"names":[],"mappings":"AAuIA,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAShB"}
|