@openclaw/fs-safe 0.4.7 → 0.5.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 +58 -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/device-path.d.ts.map +1 -1
- package/dist/device-path.js +24 -2
- 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/path.d.ts.map +1 -1
- package/dist/path.js +3 -2
- 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/safe-path-segment.d.ts.map +1 -1
- package/dist/safe-path-segment.js +16 -4
- 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-target.d.ts.map +1 -1
- package/dist/temp-target.js +51 -3
- 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 +10 -4
- 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
package/dist/move-path.js
CHANGED
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { constants as fsConstants } from "node:fs";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { FsSafeError } from "./errors.js";
|
|
5
6
|
import { guardedRename } from "./guarded-mutation.js";
|
|
6
7
|
export function moveCopyFallbackReasonForRenameError(error, platform = process.platform) {
|
|
7
8
|
const code = error?.code;
|
|
@@ -13,6 +14,49 @@ export function moveCopyFallbackReasonForRenameError(error, platform = process.p
|
|
|
13
14
|
}
|
|
14
15
|
return undefined;
|
|
15
16
|
}
|
|
17
|
+
const MAX_HARDLINK_PREFLIGHT_ENTRIES = 50_000;
|
|
18
|
+
function hardlinkedSourceError(sourcePath) {
|
|
19
|
+
return new FsSafeError("hardlink", `Refusing to move hardlinked file: ${sourcePath}`);
|
|
20
|
+
}
|
|
21
|
+
function hardlinkWalkTooLargeError() {
|
|
22
|
+
return new FsSafeError("too-large", `Source hardlink preflight exceeds ${MAX_HARDLINK_PREFLIGHT_ENTRIES} entries`);
|
|
23
|
+
}
|
|
24
|
+
async function preflightSourceHardlinks(sourcePath) {
|
|
25
|
+
const pending = [sourcePath];
|
|
26
|
+
let discovered = 1;
|
|
27
|
+
while (pending.length > 0) {
|
|
28
|
+
const current = pending.pop();
|
|
29
|
+
const stat = await fs.lstat(current);
|
|
30
|
+
if (stat.isFile() && stat.nlink > 1) {
|
|
31
|
+
throw hardlinkedSourceError(current);
|
|
32
|
+
}
|
|
33
|
+
if (!stat.isDirectory()) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const directory = await fs.opendir(current);
|
|
37
|
+
for await (const entry of directory) {
|
|
38
|
+
discovered += 1;
|
|
39
|
+
if (discovered > MAX_HARDLINK_PREFLIGHT_ENTRIES) {
|
|
40
|
+
throw hardlinkWalkTooLargeError();
|
|
41
|
+
}
|
|
42
|
+
pending.push(path.join(current, entry.name));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function isSameOrDescendant(parentPath, candidatePath) {
|
|
47
|
+
const relative = path.relative(parentPath, candidatePath);
|
|
48
|
+
return (relative === "" ||
|
|
49
|
+
(!path.isAbsolute(relative) && !relative.startsWith(`..${path.sep}`) && relative !== ".."));
|
|
50
|
+
}
|
|
51
|
+
async function assertCopyDestinationOutsideSource(sourcePath, targetPath) {
|
|
52
|
+
const sourceReal = await fs.realpath(sourcePath);
|
|
53
|
+
const normalizedTarget = path.resolve(targetPath);
|
|
54
|
+
const targetParentReal = await fs.realpath(path.dirname(normalizedTarget));
|
|
55
|
+
const targetCandidate = path.join(targetParentReal, path.basename(normalizedTarget));
|
|
56
|
+
if (isSameOrDescendant(sourceReal, targetCandidate)) {
|
|
57
|
+
throw new FsSafeError("invalid-path", "Move destination must not be inside the source");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
16
60
|
function entryIdentity(stat) {
|
|
17
61
|
return {
|
|
18
62
|
ctimeMs: stat.ctimeMs,
|
|
@@ -20,6 +64,7 @@ function entryIdentity(stat) {
|
|
|
20
64
|
ino: stat.ino,
|
|
21
65
|
mode: stat.mode,
|
|
22
66
|
mtimeMs: stat.mtimeMs,
|
|
67
|
+
nlink: stat.nlink,
|
|
23
68
|
size: stat.size,
|
|
24
69
|
};
|
|
25
70
|
}
|
|
@@ -27,6 +72,7 @@ function sameIdentity(a, b) {
|
|
|
27
72
|
return (a.dev === b.dev &&
|
|
28
73
|
a.ino === b.ino &&
|
|
29
74
|
a.mode === b.mode &&
|
|
75
|
+
a.nlink === b.nlink &&
|
|
30
76
|
a.size === b.size &&
|
|
31
77
|
a.mtimeMs === b.mtimeMs &&
|
|
32
78
|
a.ctimeMs === b.ctimeMs);
|
|
@@ -51,6 +97,9 @@ function regularReadFlags() {
|
|
|
51
97
|
return (fsConstants.O_RDONLY |
|
|
52
98
|
(typeof fsConstants.O_NOFOLLOW === "number" && process.platform !== "win32"
|
|
53
99
|
? fsConstants.O_NOFOLLOW
|
|
100
|
+
: 0) |
|
|
101
|
+
(typeof fsConstants.O_NONBLOCK === "number" && process.platform !== "win32"
|
|
102
|
+
? fsConstants.O_NONBLOCK
|
|
54
103
|
: 0));
|
|
55
104
|
}
|
|
56
105
|
async function writeAll(handle, buffer, bytesRead) {
|
|
@@ -75,6 +124,9 @@ async function copyRegularFilePinned(params) {
|
|
|
75
124
|
}
|
|
76
125
|
try {
|
|
77
126
|
const openedStat = await sourceHandle.stat();
|
|
127
|
+
if (params.rejectHardlinks && openedStat.nlink > 1) {
|
|
128
|
+
throw hardlinkedSourceError(params.from);
|
|
129
|
+
}
|
|
78
130
|
if (!openedStat.isFile() || !sameIdentity(params.identity, entryIdentity(openedStat))) {
|
|
79
131
|
throw sourceChangedError(params.from);
|
|
80
132
|
}
|
|
@@ -95,7 +147,11 @@ async function copyRegularFilePinned(params) {
|
|
|
95
147
|
}
|
|
96
148
|
// Re-check the opened handle before the staged tree can be committed. If
|
|
97
149
|
// the source changed while we copied, the caller should retry the move.
|
|
98
|
-
|
|
150
|
+
const finalSourceStat = await sourceHandle.stat();
|
|
151
|
+
if (params.rejectHardlinks && finalSourceStat.nlink > 1) {
|
|
152
|
+
throw hardlinkedSourceError(params.from);
|
|
153
|
+
}
|
|
154
|
+
if (!sameIdentity(params.identity, entryIdentity(finalSourceStat))) {
|
|
99
155
|
throw sourceChangedError(params.from);
|
|
100
156
|
}
|
|
101
157
|
await fs.chmod(params.to, modeBits(params.mode)).catch(() => undefined);
|
|
@@ -123,7 +179,15 @@ async function copyEntryWithManifest(from, to, options) {
|
|
|
123
179
|
if (sourceStat.isDirectory()) {
|
|
124
180
|
await fs.mkdir(to, { mode: modeBits(sourceStat.mode) || 0o755 });
|
|
125
181
|
const children = [];
|
|
126
|
-
|
|
182
|
+
const childNames = [];
|
|
183
|
+
const directory = await fs.opendir(from);
|
|
184
|
+
for await (const entry of directory) {
|
|
185
|
+
if (options.budget && ++options.budget.discovered > MAX_HARDLINK_PREFLIGHT_ENTRIES) {
|
|
186
|
+
throw hardlinkWalkTooLargeError();
|
|
187
|
+
}
|
|
188
|
+
childNames.push(entry.name);
|
|
189
|
+
}
|
|
190
|
+
for (const child of childNames) {
|
|
127
191
|
children.push({
|
|
128
192
|
name: child,
|
|
129
193
|
manifest: await copyEntryWithManifest(path.join(from, child), path.join(to, child), options),
|
|
@@ -141,9 +205,15 @@ async function copyEntryWithManifest(from, to, options) {
|
|
|
141
205
|
throw new Error(`Refusing to move non-file path with copy fallback: ${from}`);
|
|
142
206
|
}
|
|
143
207
|
if (options.sourceHardlinks === "reject" && sourceStat.nlink > 1) {
|
|
144
|
-
throw
|
|
208
|
+
throw hardlinkedSourceError(from);
|
|
145
209
|
}
|
|
146
|
-
await copyRegularFilePinned({
|
|
210
|
+
await copyRegularFilePinned({
|
|
211
|
+
from,
|
|
212
|
+
identity,
|
|
213
|
+
mode: sourceStat.mode,
|
|
214
|
+
rejectHardlinks: options.sourceHardlinks === "reject",
|
|
215
|
+
to,
|
|
216
|
+
});
|
|
147
217
|
return { ...identity, kind: "leaf" };
|
|
148
218
|
}
|
|
149
219
|
function mergeCleanupResults(a, b) {
|
|
@@ -189,27 +259,41 @@ async function cleanupCopiedEntry(sourcePath, manifest) {
|
|
|
189
259
|
return "removed";
|
|
190
260
|
}
|
|
191
261
|
export async function movePathWithCopyFallback(options) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
262
|
+
const sourcePath = path.resolve(options.from);
|
|
263
|
+
const targetPath = path.resolve(options.to);
|
|
264
|
+
const rejectHardlinks = options.sourceHardlinks === "reject";
|
|
265
|
+
if (rejectHardlinks) {
|
|
266
|
+
await preflightSourceHardlinks(sourcePath);
|
|
196
267
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
268
|
+
if (!rejectHardlinks) {
|
|
269
|
+
try {
|
|
270
|
+
await guardedRename({ from: sourcePath, to: targetPath });
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
if (!moveCopyFallbackReasonForRenameError(error)) {
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
201
277
|
}
|
|
202
278
|
}
|
|
203
|
-
|
|
279
|
+
else {
|
|
280
|
+
// A pathname preflight cannot make nlink and rename one atomic operation.
|
|
281
|
+
// Commit a fresh inode through the copy path so a post-scan hardlink can
|
|
282
|
+
// never become the published target; the copy loop fences nlink again.
|
|
283
|
+
}
|
|
284
|
+
await assertCopyDestinationOutsideSource(sourcePath, targetPath);
|
|
285
|
+
const targetDir = path.dirname(targetPath);
|
|
204
286
|
const staged = path.join(targetDir, `.fs-safe-move-${process.pid}-${randomUUID()}.tmp`);
|
|
205
287
|
try {
|
|
206
|
-
const manifest = await copyEntryWithManifest(
|
|
288
|
+
const manifest = await copyEntryWithManifest(sourcePath, staged, {
|
|
207
289
|
sourceHardlinks: options.sourceHardlinks ?? "allow",
|
|
290
|
+
...(rejectHardlinks ? { budget: { discovered: 1 } } : {}),
|
|
208
291
|
});
|
|
209
|
-
await
|
|
210
|
-
|
|
292
|
+
await assertCopyDestinationOutsideSource(sourcePath, targetPath);
|
|
293
|
+
await guardedRename({ from: staged, to: targetPath });
|
|
294
|
+
const cleanupResult = await cleanupCopiedEntry(sourcePath, manifest);
|
|
211
295
|
if (cleanupResult === "stale") {
|
|
212
|
-
throw sourceChangedError(
|
|
296
|
+
throw sourceChangedError(sourcePath);
|
|
213
297
|
}
|
|
214
298
|
}
|
|
215
299
|
finally {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export interface NativeFileHash {
|
|
2
|
+
bytes: number;
|
|
3
|
+
digest: string;
|
|
4
|
+
}
|
|
5
|
+
export interface NativeFileIdentity {
|
|
6
|
+
dev: number;
|
|
7
|
+
ino: number;
|
|
8
|
+
mode: number;
|
|
9
|
+
nlink: number;
|
|
10
|
+
size: number;
|
|
11
|
+
isFile: boolean;
|
|
12
|
+
isDirectory: boolean;
|
|
13
|
+
isSymbolicLink: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface NativeArchiveEntry {
|
|
16
|
+
index: number;
|
|
17
|
+
path: string;
|
|
18
|
+
kind: string;
|
|
19
|
+
size: number;
|
|
20
|
+
mode: number;
|
|
21
|
+
}
|
|
22
|
+
export interface NativeArchivePlanEntry extends NativeArchiveEntry {
|
|
23
|
+
}
|
|
24
|
+
export interface NativeCopyResult {
|
|
25
|
+
fd: number;
|
|
26
|
+
bytes: number;
|
|
27
|
+
errorCode?: string;
|
|
28
|
+
errorMessage?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface NativeOpenBeneathResult {
|
|
31
|
+
fd: number;
|
|
32
|
+
containment: "kernel-atomic" | "best-effort";
|
|
33
|
+
}
|
|
34
|
+
export interface NativeWindowsAccessControlEntry {
|
|
35
|
+
sid: string;
|
|
36
|
+
mask: number;
|
|
37
|
+
aceType: string;
|
|
38
|
+
flags: {
|
|
39
|
+
raw: number;
|
|
40
|
+
objectInherit: boolean;
|
|
41
|
+
containerInherit: boolean;
|
|
42
|
+
noPropagateInherit: boolean;
|
|
43
|
+
inheritOnly: boolean;
|
|
44
|
+
inherited: boolean;
|
|
45
|
+
successfulAccess: boolean;
|
|
46
|
+
failedAccess: boolean;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export interface NativeWindowsSecurityFacts {
|
|
50
|
+
ownerSid: string;
|
|
51
|
+
currentUserSid: string;
|
|
52
|
+
ownerClass: string;
|
|
53
|
+
worldWritable: boolean;
|
|
54
|
+
groupWritable: boolean;
|
|
55
|
+
worldReadable: boolean;
|
|
56
|
+
groupReadable: boolean;
|
|
57
|
+
fallbackRequired: boolean;
|
|
58
|
+
daclPresent: boolean;
|
|
59
|
+
isLocal: boolean;
|
|
60
|
+
aceListComplete: boolean;
|
|
61
|
+
unsupportedAceTypes: number[];
|
|
62
|
+
aces: NativeWindowsAccessControlEntry[];
|
|
63
|
+
}
|
|
64
|
+
export interface NativeBinding {
|
|
65
|
+
cloneFileExclusive(sourceFd: number, targetRootFd: number, targetRelPath: string): number;
|
|
66
|
+
copyFileRangeExclusive(sourceFd: number, targetRootFd: number, targetRelPath: string): Promise<NativeCopyResult>;
|
|
67
|
+
createPrivateDirectory(path: string): void;
|
|
68
|
+
extractArchiveNative(path: string, kind: string, rootFd: number, plan: NativeArchivePlanEntry[], maxMetaEntryBytes: number, signal: AbortSignal): Promise<void>;
|
|
69
|
+
fstatIdentity(fd: number): NativeFileIdentity;
|
|
70
|
+
inspectArchiveNative(path: string, kind: string, maxEntries: number, maxMetaEntryBytes: number, maxManifestBytes: number, signal: AbortSignal): Promise<NativeArchiveEntry[]>;
|
|
71
|
+
linkBeneath(sourceRootFd: number, sourceRelPath: string, targetRootFd: number, targetRelPath: string): void;
|
|
72
|
+
mkdirBeneath(rootFd: number, relPath: string, mode: number): void;
|
|
73
|
+
openBeneath(rootFd: number, relPath: string, flags: number): NativeOpenBeneathResult;
|
|
74
|
+
readArchiveEntryNative(path: string, kind: string, requested: string, maxBytes: number, maxEntries: number, maxMetaEntryBytes: number, signal: AbortSignal): Promise<Buffer>;
|
|
75
|
+
readOwnerAndDacl(path: string): NativeWindowsSecurityFacts;
|
|
76
|
+
renameNoReplace(sourceRootFd: number, sourceRelPath: string, targetRootFd: number, targetRelPath: string): void;
|
|
77
|
+
sha256File(fd: number): Promise<NativeFileHash>;
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=native-binding.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-binding.d.ts","sourceRoot":"","sources":["../src/native-binding.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type FsSafeNativeMode = "auto" | "off" | "require";
|
|
2
|
+
export type FsSafeNativeConfig = {
|
|
3
|
+
mode: FsSafeNativeMode;
|
|
4
|
+
};
|
|
5
|
+
/** @deprecated Compatibility bridge for 0.4 upgrades. Use {@link FsSafeNativeConfig}. */
|
|
6
|
+
export type FsSafePythonConfig = {
|
|
7
|
+
mode: FsSafeNativeMode;
|
|
8
|
+
pythonPath?: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function configureFsSafeNative(config: Partial<FsSafeNativeConfig>): void;
|
|
11
|
+
/**
|
|
12
|
+
* @deprecated Compatibility bridge for 0.4 upgrades. Use configureFsSafeNative.
|
|
13
|
+
*/
|
|
14
|
+
export declare function configureFsSafePython(config: Partial<FsSafePythonConfig>): void;
|
|
15
|
+
export declare function getFsSafeNativeConfig(): FsSafeNativeConfig;
|
|
16
|
+
export declare function __resetFsSafeNativeConfigForTest(): void;
|
|
17
|
+
//# sourceMappingURL=native-config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-config.d.ts","sourceRoot":"","sources":["../src/native-config.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,KAAK,GAAG,SAAS,CAAC;AAE1D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,gBAAgB,CAAC;CACxB,CAAC;AAEF,yFAAyF;AACzF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,gBAAgB,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AA8BF,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,OAAO,CAAC,kBAAkB,CAAC,GAAG,IAAI,CAE/E;AA4BD;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,OAAO,CAAC,kBAAkB,CAAC,GAAG,IAAI,CAM/E;AAED,wBAAgB,qBAAqB,IAAI,kBAAkB,CAU1D;AAED,wBAAgB,gCAAgC,IAAI,IAAI,CAGvD"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
let overrideConfig = {};
|
|
2
|
+
let legacyWarningEmitted = false;
|
|
3
|
+
const legacyModeEnvKeys = ["FS_SAFE_PYTHON_MODE", "OPENCLAW_FS_SAFE_PYTHON_MODE"];
|
|
4
|
+
const legacyPathEnvKeys = [
|
|
5
|
+
"FS_SAFE_PYTHON",
|
|
6
|
+
"OPENCLAW_FS_SAFE_PYTHON",
|
|
7
|
+
"OPENCLAW_PINNED_PYTHON",
|
|
8
|
+
"OPENCLAW_PINNED_WRITE_PYTHON",
|
|
9
|
+
];
|
|
10
|
+
function parseMode(value) {
|
|
11
|
+
if (!value) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
const normalized = value.trim().toLowerCase();
|
|
15
|
+
if (normalized === "0" || normalized === "false" || normalized === "off" || normalized === "never") {
|
|
16
|
+
return "off";
|
|
17
|
+
}
|
|
18
|
+
if (normalized === "1" || normalized === "true" || normalized === "on" || normalized === "auto") {
|
|
19
|
+
return "auto";
|
|
20
|
+
}
|
|
21
|
+
if (normalized === "required" || normalized === "require") {
|
|
22
|
+
return "require";
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
export function configureFsSafeNative(config) {
|
|
27
|
+
overrideConfig = { ...overrideConfig, ...config };
|
|
28
|
+
}
|
|
29
|
+
function warnLegacyPythonConfiguration(source, mappedMode) {
|
|
30
|
+
if (legacyWarningEmitted) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
legacyWarningEmitted = true;
|
|
34
|
+
process.emitWarning(`${source} is a deprecated 0.4 compatibility bridge; mapped to native mode ` +
|
|
35
|
+
`"${mappedMode}". Use configureFsSafeNative({ mode: "${mappedMode}" }) or ` +
|
|
36
|
+
`FS_SAFE_NATIVE_MODE=${mappedMode}. Python interpreter paths are no longer used.`, { code: "FS_SAFE_PYTHON_DEPRECATED", type: "DeprecationWarning" });
|
|
37
|
+
}
|
|
38
|
+
function readLegacyPythonMode() {
|
|
39
|
+
const configuredKeys = [...legacyModeEnvKeys, ...legacyPathEnvKeys].filter((key) => process.env[key] !== undefined);
|
|
40
|
+
if (configuredKeys.length === 0) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
const rawMode = legacyModeEnvKeys.map((key) => process.env[key]).find((value) => value !== undefined);
|
|
44
|
+
const mappedMode = parseMode(rawMode) ?? "auto";
|
|
45
|
+
warnLegacyPythonConfiguration(`Legacy ${configuredKeys.join(", ")}`, mappedMode);
|
|
46
|
+
return mappedMode;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* @deprecated Compatibility bridge for 0.4 upgrades. Use configureFsSafeNative.
|
|
50
|
+
*/
|
|
51
|
+
export function configureFsSafePython(config) {
|
|
52
|
+
const mappedMode = config.mode ?? "auto";
|
|
53
|
+
warnLegacyPythonConfiguration("configureFsSafePython()", mappedMode);
|
|
54
|
+
if (config.mode !== undefined) {
|
|
55
|
+
configureFsSafeNative({ mode: config.mode });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export function getFsSafeNativeConfig() {
|
|
59
|
+
const legacyMode = readLegacyPythonMode();
|
|
60
|
+
return {
|
|
61
|
+
mode: overrideConfig.mode ??
|
|
62
|
+
parseMode(process.env.FS_SAFE_NATIVE_MODE) ??
|
|
63
|
+
parseMode(process.env.OPENCLAW_FS_SAFE_NATIVE_MODE) ??
|
|
64
|
+
legacyMode ??
|
|
65
|
+
"auto",
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export function __resetFsSafeNativeConfigForTest() {
|
|
69
|
+
overrideConfig = {};
|
|
70
|
+
legacyWarningEmitted = false;
|
|
71
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type Stats } from "node:fs";
|
|
2
|
+
import type { ContainmentGuarantee } from "./containment.js";
|
|
3
|
+
import { type NativeBinding } from "./native.js";
|
|
4
|
+
export type NativeFileHandle = {
|
|
5
|
+
readonly fd: number;
|
|
6
|
+
readonly containment: ContainmentGuarantee;
|
|
7
|
+
close(): Promise<void>;
|
|
8
|
+
stat(): Promise<Stats>;
|
|
9
|
+
writeFile(data: string | Buffer, encoding?: BufferEncoding): Promise<void>;
|
|
10
|
+
};
|
|
11
|
+
export declare function removeNativeCreatedFileIfStillPinned(params: {
|
|
12
|
+
binding: NativeBinding;
|
|
13
|
+
parentPath: string;
|
|
14
|
+
parentFd: number;
|
|
15
|
+
basename: string;
|
|
16
|
+
created?: Stats;
|
|
17
|
+
}): void;
|
|
18
|
+
export declare function createNativeExclusiveFile(targetPath: string, mode: number): Promise<NativeFileHandle | undefined>;
|
|
19
|
+
export declare function syncNativeFileBestEffort(fd: number): void;
|
|
20
|
+
export declare function writeNativeFd(fd: number, data: Buffer): void;
|
|
21
|
+
//# sourceMappingURL=native-operations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-operations.d.ts","sourceRoot":"","sources":["../src/native-operations.ts"],"names":[],"mappings":"AAAA,OAAe,EAAE,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAG7C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAE7D,OAAO,EAAoB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAEnE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,WAAW,EAAE,oBAAoB,CAAC;IAC3C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC;IACvB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,QAAQ,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5E,CAAC;AA0CF,wBAAgB,oCAAoC,CAAC,MAAM,EAAE;IAC3D,OAAO,EAAE,aAAa,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,KAAK,CAAC;CACjB,GAAG,IAAI,CAsBP;AAED,wBAAsB,yBAAyB,CAC7C,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CA6CvC;AAED,wBAAgB,wBAAwB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAQzD;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAE5D"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import fsSync, {} from "node:fs";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { sameFileIdentity } from "./file-identity.js";
|
|
5
|
+
import { getNativeBinding } from "./native.js";
|
|
6
|
+
function nativeOpenFlags(flags) {
|
|
7
|
+
const closeOnExec = fsSync.constants.O_CLOEXEC;
|
|
8
|
+
return (flags |
|
|
9
|
+
(closeOnExec ?? 0) |
|
|
10
|
+
(typeof fsSync.constants.O_NOFOLLOW === "number" ? fsSync.constants.O_NOFOLLOW : 0));
|
|
11
|
+
}
|
|
12
|
+
function writeAll(fd, data) {
|
|
13
|
+
let offset = 0;
|
|
14
|
+
while (offset < data.byteLength) {
|
|
15
|
+
const written = fsSync.writeSync(fd, data, offset, data.byteLength - offset);
|
|
16
|
+
if (written <= 0) {
|
|
17
|
+
throw Object.assign(new Error("native file write made no progress"), { code: "EIO" });
|
|
18
|
+
}
|
|
19
|
+
offset += written;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function wrapNativeFd(fd, containment) {
|
|
23
|
+
let open = true;
|
|
24
|
+
return {
|
|
25
|
+
fd,
|
|
26
|
+
containment,
|
|
27
|
+
async close() {
|
|
28
|
+
if (open) {
|
|
29
|
+
open = false;
|
|
30
|
+
fsSync.closeSync(fd);
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
async stat() {
|
|
34
|
+
return fsSync.fstatSync(fd);
|
|
35
|
+
},
|
|
36
|
+
async writeFile(data, encoding) {
|
|
37
|
+
writeAll(fd, Buffer.isBuffer(data) ? data : Buffer.from(data, encoding ?? "utf8"));
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function removeNativeCreatedFileIfStillPinned(params) {
|
|
42
|
+
if (!params.created) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const parentPathStat = fsSync.lstatSync(params.parentPath);
|
|
47
|
+
const parentFdStat = params.binding.fstatIdentity(params.parentFd);
|
|
48
|
+
const targetPath = path.join(params.parentPath, params.basename);
|
|
49
|
+
const target = fsSync.lstatSync(targetPath);
|
|
50
|
+
if (!parentPathStat.isSymbolicLink() &&
|
|
51
|
+
parentPathStat.dev === parentFdStat.dev &&
|
|
52
|
+
parentPathStat.ino === parentFdStat.ino &&
|
|
53
|
+
!target.isSymbolicLink() &&
|
|
54
|
+
sameFileIdentity(target, params.created)) {
|
|
55
|
+
fsSync.rmSync(targetPath);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// Failed cleanup leaves a fail-closed artifact instead of deleting an
|
|
60
|
+
// unverified pathname.
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export async function createNativeExclusiveFile(targetPath, mode) {
|
|
64
|
+
const binding = getNativeBinding();
|
|
65
|
+
if (!binding) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
const parentPath = path.dirname(targetPath);
|
|
69
|
+
const basename = path.basename(targetPath);
|
|
70
|
+
const parent = await fs.open(parentPath, fsSync.constants.O_RDONLY |
|
|
71
|
+
(typeof fsSync.constants.O_DIRECTORY === "number" ? fsSync.constants.O_DIRECTORY : 0));
|
|
72
|
+
let fd;
|
|
73
|
+
let created;
|
|
74
|
+
try {
|
|
75
|
+
const opened = binding.openBeneath(parent.fd, basename, nativeOpenFlags(fsSync.constants.O_WRONLY | fsSync.constants.O_CREAT | fsSync.constants.O_EXCL));
|
|
76
|
+
fd = opened.fd;
|
|
77
|
+
fsSync.fchmodSync(fd, mode);
|
|
78
|
+
created = fsSync.fstatSync(fd);
|
|
79
|
+
return wrapNativeFd(fd, opened.containment);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (fd !== undefined) {
|
|
83
|
+
try {
|
|
84
|
+
fsSync.closeSync(fd);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Preserve the original error.
|
|
88
|
+
}
|
|
89
|
+
removeNativeCreatedFileIfStillPinned({
|
|
90
|
+
binding,
|
|
91
|
+
parentPath,
|
|
92
|
+
parentFd: parent.fd,
|
|
93
|
+
basename,
|
|
94
|
+
created,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
await parent.close().catch(() => undefined);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export function syncNativeFileBestEffort(fd) {
|
|
104
|
+
try {
|
|
105
|
+
fsSync.fsyncSync(fd);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (error.code !== "EPERM") {
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
export function writeNativeFd(fd, data) {
|
|
114
|
+
writeAll(fd, data);
|
|
115
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { FileIdentityStat } from "./file-identity.js";
|
|
2
|
+
import type { NativeBinding } from "./native.js";
|
|
3
|
+
import type { PinnedWriteParams } from "./pinned-write.js";
|
|
4
|
+
export declare function runPinnedWriteNative(binding: NativeBinding, params: PinnedWriteParams): Promise<FileIdentityStat>;
|
|
5
|
+
//# sourceMappingURL=native-pinned-write.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-pinned-write.d.ts","sourceRoot":"","sources":["../src/native-pinned-write.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAM3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAoB,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAiD7E,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,aAAa,EACtB,MAAM,EAAE,iBAAiB,GACxB,OAAO,CAAC,gBAAgB,CAAC,CA4F3B"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import fsSync, {} from "node:fs";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { FsSafeError } from "./errors.js";
|
|
6
|
+
import { removeNativeCreatedFileIfStillPinned, syncNativeFileBestEffort, writeNativeFd, } from "./native-operations.js";
|
|
7
|
+
function assertWithinMaxBytes(bytes, maxBytes) {
|
|
8
|
+
if (maxBytes !== undefined && bytes > maxBytes) {
|
|
9
|
+
throw new FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
async function inputToBuffer(input, maxBytes) {
|
|
13
|
+
if (input.kind === "buffer") {
|
|
14
|
+
const data = typeof input.data === "string"
|
|
15
|
+
? Buffer.from(input.data, input.encoding ?? "utf8")
|
|
16
|
+
: Buffer.from(input.data);
|
|
17
|
+
assertWithinMaxBytes(data.byteLength, maxBytes);
|
|
18
|
+
return data;
|
|
19
|
+
}
|
|
20
|
+
const chunks = [];
|
|
21
|
+
let bytes = 0;
|
|
22
|
+
for await (const chunk of input.stream) {
|
|
23
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
24
|
+
bytes += buffer.byteLength;
|
|
25
|
+
assertWithinMaxBytes(bytes, maxBytes);
|
|
26
|
+
chunks.push(buffer);
|
|
27
|
+
}
|
|
28
|
+
return Buffer.concat(chunks, bytes);
|
|
29
|
+
}
|
|
30
|
+
function nativeOpenFlags(flags) {
|
|
31
|
+
const closeOnExec = fsSync.constants.O_CLOEXEC;
|
|
32
|
+
return (flags |
|
|
33
|
+
(closeOnExec ?? 0) |
|
|
34
|
+
(typeof fsSync.constants.O_NOFOLLOW === "number" ? fsSync.constants.O_NOFOLLOW : 0));
|
|
35
|
+
}
|
|
36
|
+
function sameNativeIdentity(left, right) {
|
|
37
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
38
|
+
}
|
|
39
|
+
export async function runPinnedWriteNative(binding, params) {
|
|
40
|
+
const data = await inputToBuffer(params.input, params.maxBytes);
|
|
41
|
+
const root = await fs.open(params.rootPath, fsSync.constants.O_RDONLY |
|
|
42
|
+
(typeof fsSync.constants.O_DIRECTORY === "number" ? fsSync.constants.O_DIRECTORY : 0));
|
|
43
|
+
let parentFd;
|
|
44
|
+
let tempFd;
|
|
45
|
+
let targetFd;
|
|
46
|
+
let tempIdentity;
|
|
47
|
+
let parentPath = params.rootPath;
|
|
48
|
+
const tempName = `.${params.basename}.${randomUUID()}.native.tmp`;
|
|
49
|
+
let renamed = false;
|
|
50
|
+
try {
|
|
51
|
+
const rootIdentity = binding.fstatIdentity(root.fd);
|
|
52
|
+
if (params.rootIdentity && !sameNativeIdentity(params.rootIdentity, rootIdentity)) {
|
|
53
|
+
throw new FsSafeError("path-mismatch", "root path changed during native write");
|
|
54
|
+
}
|
|
55
|
+
if (params.mkdir) {
|
|
56
|
+
binding.mkdirBeneath(root.fd, params.relativeParentPath, 0o777);
|
|
57
|
+
}
|
|
58
|
+
const parentFlags = fsSync.constants.O_RDONLY |
|
|
59
|
+
(typeof fsSync.constants.O_DIRECTORY === "number" ? fsSync.constants.O_DIRECTORY : 0);
|
|
60
|
+
parentFd = binding.openBeneath(root.fd, params.relativeParentPath, parentFlags).fd;
|
|
61
|
+
parentPath = await fs.realpath(params.relativeParentPath
|
|
62
|
+
? path.join(params.rootPath, ...params.relativeParentPath.split("/"))
|
|
63
|
+
: params.rootPath);
|
|
64
|
+
const parentPathStat = await fs.lstat(parentPath);
|
|
65
|
+
const parentIdentity = binding.fstatIdentity(parentFd);
|
|
66
|
+
if (parentPathStat.isSymbolicLink() ||
|
|
67
|
+
!sameNativeIdentity(parentPathStat, parentIdentity)) {
|
|
68
|
+
throw new FsSafeError("path-mismatch", "native write parent changed during resolution");
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
await fs.lstat(path.join(parentPath, params.basename));
|
|
72
|
+
throw Object.assign(new Error("destination already exists"), { code: "EEXIST" });
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (error.code !== "ENOENT") {
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
tempFd = binding.openBeneath(parentFd, tempName, nativeOpenFlags(fsSync.constants.O_WRONLY | fsSync.constants.O_CREAT | fsSync.constants.O_EXCL)).fd;
|
|
80
|
+
fsSync.fchmodSync(tempFd, params.mode || 0o600);
|
|
81
|
+
writeNativeFd(tempFd, data);
|
|
82
|
+
syncNativeFileBestEffort(tempFd);
|
|
83
|
+
tempIdentity = fsSync.fstatSync(tempFd);
|
|
84
|
+
binding.renameNoReplace(parentFd, tempName, parentFd, params.basename);
|
|
85
|
+
renamed = true;
|
|
86
|
+
targetFd = binding.openBeneath(parentFd, params.basename, nativeOpenFlags(fsSync.constants.O_RDONLY)).fd;
|
|
87
|
+
const targetIdentity = binding.fstatIdentity(targetFd);
|
|
88
|
+
if (!targetIdentity.isFile || !sameNativeIdentity(tempIdentity, targetIdentity)) {
|
|
89
|
+
throw new FsSafeError("path-mismatch", "native write target changed after rename");
|
|
90
|
+
}
|
|
91
|
+
syncNativeFileBestEffort(parentFd);
|
|
92
|
+
return { dev: targetIdentity.dev, ino: targetIdentity.ino };
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
if (targetFd !== undefined) {
|
|
96
|
+
fsSync.closeSync(targetFd);
|
|
97
|
+
}
|
|
98
|
+
if (tempFd !== undefined) {
|
|
99
|
+
fsSync.closeSync(tempFd);
|
|
100
|
+
}
|
|
101
|
+
if (!renamed && parentFd !== undefined) {
|
|
102
|
+
removeNativeCreatedFileIfStillPinned({
|
|
103
|
+
binding,
|
|
104
|
+
parentPath,
|
|
105
|
+
parentFd,
|
|
106
|
+
basename: tempName,
|
|
107
|
+
created: tempIdentity,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
if (parentFd !== undefined) {
|
|
111
|
+
fsSync.closeSync(parentFd);
|
|
112
|
+
}
|
|
113
|
+
await root.close().catch(() => undefined);
|
|
114
|
+
}
|
|
115
|
+
}
|