@lousy-agents/agent-shell 5.19.1 → 5.19.3
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/dist/index.js +1171 -829
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { createRequire as __rspack_createRequire } from "node:module";
|
|
3
3
|
const __rspack_createRequire_require = __rspack_createRequire(import.meta.url);
|
|
4
4
|
var __webpack_modules__ = ({
|
|
5
|
-
|
|
5
|
+
983(__unused_rspack_module, __unused_rspack___webpack_exports__, __webpack_require__) {
|
|
6
6
|
|
|
7
7
|
;// CONCATENATED MODULE: external "node:child_process"
|
|
8
8
|
const external_node_child_process_namespaceObject = __rspack_createRequire_require("node:child_process");
|
|
@@ -81,6 +81,9 @@ function defaultExecutor(env) {
|
|
|
81
81
|
const OPERATIONAL_CODES = new Set([
|
|
82
82
|
"helper-failed",
|
|
83
83
|
"helper-unavailable",
|
|
84
|
+
"not-empty",
|
|
85
|
+
"not-found",
|
|
86
|
+
"not-removable",
|
|
84
87
|
"permission-unverified",
|
|
85
88
|
"timeout",
|
|
86
89
|
"unsupported-platform",
|
|
@@ -132,6 +135,7 @@ function file_identity_sameFileIdentity(left, right, platform = process.platform
|
|
|
132
135
|
|
|
133
136
|
|
|
134
137
|
|
|
138
|
+
|
|
135
139
|
const NOT_FOUND_CODES = new Set(["ENOENT", "ENOTDIR"]);
|
|
136
140
|
const SYMLINK_OPEN_CODES = new Set(["ELOOP", "EINVAL", "ENOTSUP"]);
|
|
137
141
|
const POSIX_SEPARATOR_CHAR_CODE = 0x2f;
|
|
@@ -249,6 +253,9 @@ function splitSafeRelativePath(relativePath) {
|
|
|
249
253
|
if (segment === "..") {
|
|
250
254
|
throw new FsSafeError("invalid-path", "relative path must not contain '..'");
|
|
251
255
|
}
|
|
256
|
+
if (isDriveRelativePath(segment)) {
|
|
257
|
+
throw new FsSafeError("invalid-path", "relative path must not contain a drive letter");
|
|
258
|
+
}
|
|
252
259
|
}
|
|
253
260
|
return segments;
|
|
254
261
|
}
|
|
@@ -261,6 +268,74 @@ function resolveSafeRelativePath(rootDir, relativePath) {
|
|
|
261
268
|
return target;
|
|
262
269
|
}
|
|
263
270
|
|
|
271
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/root-errors.js
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
const REMOVE_NOT_EMPTY_CODES = new Set(["ENOTEMPTY", "EEXIST"]);
|
|
275
|
+
function fileNotFoundError(cause) {
|
|
276
|
+
return cause === undefined
|
|
277
|
+
? new errors_FsSafeError("not-found", "file not found")
|
|
278
|
+
: new errors_FsSafeError("not-found", "file not found", { cause });
|
|
279
|
+
}
|
|
280
|
+
function outsideWorkspaceError() {
|
|
281
|
+
return new errors_FsSafeError("outside-workspace", "file is outside workspace root");
|
|
282
|
+
}
|
|
283
|
+
function root_errors_directoryComponentNotDirectoryError(cause) {
|
|
284
|
+
return cause === undefined
|
|
285
|
+
? new errors_FsSafeError("not-file", "directory component must be a directory")
|
|
286
|
+
: new errors_FsSafeError("not-file", "directory component must be a directory", { cause });
|
|
287
|
+
}
|
|
288
|
+
function hardlinkedPathNotAllowedError() {
|
|
289
|
+
return new errors_FsSafeError("hardlink", "hardlinked path not allowed");
|
|
290
|
+
}
|
|
291
|
+
function isAlreadyExistsError(error) {
|
|
292
|
+
return hasNodeErrorCode(error, "EEXIST") || /File exists|EEXIST/i.test(String(error));
|
|
293
|
+
}
|
|
294
|
+
function normalizePinnedWriteError(error) {
|
|
295
|
+
if (error instanceof errors_FsSafeError) {
|
|
296
|
+
return error;
|
|
297
|
+
}
|
|
298
|
+
if (path_isNotFoundPathError(error)) {
|
|
299
|
+
return fileNotFoundError(error instanceof Error ? error : undefined);
|
|
300
|
+
}
|
|
301
|
+
return new errors_FsSafeError("invalid-path", "path is not a regular file under root", {
|
|
302
|
+
cause: error instanceof Error ? error : undefined,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
function normalizePinnedPathError(error) {
|
|
306
|
+
if (error instanceof errors_FsSafeError) {
|
|
307
|
+
return error;
|
|
308
|
+
}
|
|
309
|
+
return new errors_FsSafeError("path-alias", "path is not under root", {
|
|
310
|
+
cause: error instanceof Error ? error : undefined,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
function normalizeRemoveGuardError(error) {
|
|
314
|
+
if (error instanceof errors_FsSafeError) {
|
|
315
|
+
return error;
|
|
316
|
+
}
|
|
317
|
+
if (path_isNotFoundPathError(error)) {
|
|
318
|
+
return fileNotFoundError(error instanceof Error ? error : undefined);
|
|
319
|
+
}
|
|
320
|
+
return normalizePinnedPathError(error);
|
|
321
|
+
}
|
|
322
|
+
function normalizeRemovePathError(error) {
|
|
323
|
+
if (error instanceof errors_FsSafeError) {
|
|
324
|
+
return error;
|
|
325
|
+
}
|
|
326
|
+
if (!isNodeError(error) || typeof error.code !== "string") {
|
|
327
|
+
return normalizePinnedPathError(error);
|
|
328
|
+
}
|
|
329
|
+
const cause = error instanceof Error ? error : undefined;
|
|
330
|
+
if (path_isNotFoundPathError(error)) {
|
|
331
|
+
return fileNotFoundError(cause);
|
|
332
|
+
}
|
|
333
|
+
if (REMOVE_NOT_EMPTY_CODES.has(error.code)) {
|
|
334
|
+
return new errors_FsSafeError("not-empty", "directory is not empty", { cause });
|
|
335
|
+
}
|
|
336
|
+
return new errors_FsSafeError("not-removable", "path could not be removed", { cause });
|
|
337
|
+
}
|
|
338
|
+
|
|
264
339
|
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/directory-guard.js
|
|
265
340
|
|
|
266
341
|
|
|
@@ -268,17 +343,18 @@ function resolveSafeRelativePath(rootDir, relativePath) {
|
|
|
268
343
|
|
|
269
344
|
|
|
270
345
|
|
|
346
|
+
|
|
271
347
|
async function directory_guard_createAsyncDirectoryGuard(dir) {
|
|
272
348
|
const stat = await promises_namespaceObject.lstat(dir);
|
|
273
349
|
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
274
|
-
throw
|
|
350
|
+
throw root_errors_directoryComponentNotDirectoryError();
|
|
275
351
|
}
|
|
276
352
|
return { dir, realPath: await promises_namespaceObject.realpath(dir), stat };
|
|
277
353
|
}
|
|
278
354
|
async function assertAsyncDirectoryGuard(guard) {
|
|
279
355
|
const stat = await promises_namespaceObject.lstat(guard.dir);
|
|
280
356
|
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
281
|
-
throw
|
|
357
|
+
throw root_errors_directoryComponentNotDirectoryError();
|
|
282
358
|
}
|
|
283
359
|
if (!file_identity_sameFileIdentity(stat, guard.stat) || (await promises_namespaceObject.realpath(guard.dir)) !== guard.realPath) {
|
|
284
360
|
throw new errors_FsSafeError("path-mismatch", "directory changed during operation");
|
|
@@ -287,14 +363,14 @@ async function assertAsyncDirectoryGuard(guard) {
|
|
|
287
363
|
function directory_guard_createSyncDirectoryGuard(dir) {
|
|
288
364
|
const stat = fsSync.lstatSync(dir);
|
|
289
365
|
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
290
|
-
throw
|
|
366
|
+
throw directoryComponentNotDirectoryError();
|
|
291
367
|
}
|
|
292
368
|
return { dir, realPath: fsSync.realpathSync(dir), stat };
|
|
293
369
|
}
|
|
294
370
|
function directory_guard_assertSyncDirectoryGuard(guard) {
|
|
295
371
|
const stat = fsSync.lstatSync(guard.dir);
|
|
296
372
|
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
297
|
-
throw
|
|
373
|
+
throw directoryComponentNotDirectoryError();
|
|
298
374
|
}
|
|
299
375
|
if (!sameFileIdentity(stat, guard.stat) || fsSync.realpathSync(guard.dir) !== guard.realPath) {
|
|
300
376
|
throw new FsSafeError("path-mismatch", "directory changed during operation");
|
|
@@ -640,6 +716,7 @@ async function ensureDurableDirectory(options) {
|
|
|
640
716
|
|
|
641
717
|
|
|
642
718
|
|
|
719
|
+
|
|
643
720
|
function isSameOrChildPath(candidate, parent) {
|
|
644
721
|
const parentPrefix = parent.endsWith(external_node_path_namespaceObject.sep) ? parent : `${parent}${external_node_path_namespaceObject.sep}`;
|
|
645
722
|
return candidate === parent || candidate.startsWith(parentPrefix);
|
|
@@ -652,9 +729,7 @@ async function realpathOrThrowNotFile(target) {
|
|
|
652
729
|
if (path_isNotFoundPathError(error)) {
|
|
653
730
|
// A dangling symlink (or a component removed between lstat and
|
|
654
731
|
// realpath) is not a usable directory component.
|
|
655
|
-
throw
|
|
656
|
-
cause: error instanceof Error ? error : undefined,
|
|
657
|
-
});
|
|
732
|
+
throw root_errors_directoryComponentNotDirectoryError(error instanceof Error ? error : undefined);
|
|
658
733
|
}
|
|
659
734
|
throw error;
|
|
660
735
|
}
|
|
@@ -677,8 +752,8 @@ async function mkdirPathComponentsWithGuards(params) {
|
|
|
677
752
|
for (const part of relative.split(external_node_path_namespaceObject.sep).filter(Boolean)) {
|
|
678
753
|
const next = external_node_path_namespaceObject.join(current, part);
|
|
679
754
|
const parentGuard = await directory_guard_createAsyncDirectoryGuard(current);
|
|
680
|
-
await params.beforeComponent?.(next);
|
|
681
755
|
await assertAsyncDirectoryGuard(parentGuard);
|
|
756
|
+
await params.beforeComponent?.(next);
|
|
682
757
|
try {
|
|
683
758
|
await promises_namespaceObject.mkdir(next);
|
|
684
759
|
}
|
|
@@ -689,7 +764,7 @@ async function mkdirPathComponentsWithGuards(params) {
|
|
|
689
764
|
}
|
|
690
765
|
const stat = await promises_namespaceObject.lstat(next);
|
|
691
766
|
if (!stat.isSymbolicLink() && !stat.isDirectory()) {
|
|
692
|
-
throw
|
|
767
|
+
throw root_errors_directoryComponentNotDirectoryError();
|
|
693
768
|
}
|
|
694
769
|
// Node's recursive mkdir follows symlinks in missing components. Build one
|
|
695
770
|
// segment at a time and realpath-check each segment before descending.
|
|
@@ -709,7 +784,7 @@ async function mkdirPathComponentsWithGuards(params) {
|
|
|
709
784
|
// the returned resolved path, not their own lexical parent path.
|
|
710
785
|
const targetStat = await promises_namespaceObject.stat(nextReal);
|
|
711
786
|
if (!targetStat.isDirectory()) {
|
|
712
|
-
throw
|
|
787
|
+
throw root_errors_directoryComponentNotDirectoryError();
|
|
713
788
|
}
|
|
714
789
|
await directory_guard_createAsyncDirectoryGuard(nextReal);
|
|
715
790
|
await assertAsyncDirectoryGuard(parentGuard);
|
|
@@ -801,30 +876,37 @@ function guardedRmSync(params) {
|
|
|
801
876
|
}), { verifyAfter: params.verifyAfter });
|
|
802
877
|
}
|
|
803
878
|
|
|
804
|
-
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/
|
|
879
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/root-path-existing.js
|
|
805
880
|
|
|
806
881
|
|
|
807
882
|
|
|
808
883
|
|
|
809
|
-
|
|
884
|
+
function isFilesystemRoot(candidate) {
|
|
885
|
+
return external_node_path_namespaceObject.parse(candidate).root === candidate;
|
|
886
|
+
}
|
|
887
|
+
async function pathExists(targetPath) {
|
|
810
888
|
try {
|
|
811
|
-
await promises_namespaceObject.lstat(
|
|
889
|
+
await promises_namespaceObject.lstat(targetPath);
|
|
812
890
|
return true;
|
|
813
891
|
}
|
|
814
|
-
catch (
|
|
815
|
-
if (
|
|
816
|
-
|
|
892
|
+
catch (error) {
|
|
893
|
+
if (path_isNotFoundPathError(error)) {
|
|
894
|
+
return false;
|
|
817
895
|
}
|
|
818
|
-
|
|
896
|
+
throw error;
|
|
819
897
|
}
|
|
820
898
|
}
|
|
821
899
|
async function resolvePathViaExistingAncestor(targetPath) {
|
|
822
900
|
const normalized = external_node_path_namespaceObject.resolve(targetPath);
|
|
823
901
|
let cursor = normalized;
|
|
824
902
|
const missingSuffix = [];
|
|
825
|
-
while (
|
|
903
|
+
while (!isFilesystemRoot(cursor) && !(await pathExists(cursor))) {
|
|
826
904
|
missingSuffix.unshift(external_node_path_namespaceObject.basename(cursor));
|
|
827
|
-
|
|
905
|
+
const parent = external_node_path_namespaceObject.dirname(cursor);
|
|
906
|
+
if (parent === cursor) {
|
|
907
|
+
break;
|
|
908
|
+
}
|
|
909
|
+
cursor = parent;
|
|
828
910
|
}
|
|
829
911
|
if (!(await pathExists(cursor))) {
|
|
830
912
|
return normalized;
|
|
@@ -839,7 +921,38 @@ async function resolvePathViaExistingAncestor(targetPath) {
|
|
|
839
921
|
return normalized;
|
|
840
922
|
}
|
|
841
923
|
}
|
|
842
|
-
|
|
924
|
+
function root_path_existing_resolvePathViaExistingAncestorSync(targetPath) {
|
|
925
|
+
const normalized = path.resolve(targetPath);
|
|
926
|
+
let cursor = normalized;
|
|
927
|
+
const missingSuffix = [];
|
|
928
|
+
while (!isFilesystemRoot(cursor) && !fs.existsSync(cursor)) {
|
|
929
|
+
missingSuffix.unshift(path.basename(cursor));
|
|
930
|
+
const parent = path.dirname(cursor);
|
|
931
|
+
if (parent === cursor) {
|
|
932
|
+
break;
|
|
933
|
+
}
|
|
934
|
+
cursor = parent;
|
|
935
|
+
}
|
|
936
|
+
if (!fs.existsSync(cursor)) {
|
|
937
|
+
return normalized;
|
|
938
|
+
}
|
|
939
|
+
try {
|
|
940
|
+
const resolvedAncestor = path.resolve(fs.realpathSync(cursor));
|
|
941
|
+
return missingSuffix.length === 0
|
|
942
|
+
? resolvedAncestor
|
|
943
|
+
: path.resolve(resolvedAncestor, ...missingSuffix);
|
|
944
|
+
}
|
|
945
|
+
catch {
|
|
946
|
+
return normalized;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/deny-mutations.js
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
async function resolveMutationComparablePaths(rawPath) {
|
|
843
956
|
path_assertNoNulPathInput(rawPath, "path contains a NUL byte");
|
|
844
957
|
const resolved = external_node_path_namespaceObject.resolve(rawPath);
|
|
845
958
|
return new Set([resolved, await resolvePathViaExistingAncestor(resolved)]);
|
|
@@ -868,9 +981,9 @@ async function assertMutationNotDenied(filePath, policy, options = {}) {
|
|
|
868
981
|
if (!hasPolicyEntries(policy)) {
|
|
869
982
|
return;
|
|
870
983
|
}
|
|
871
|
-
const targetPaths = await
|
|
984
|
+
const targetPaths = await resolveMutationComparablePaths(filePath);
|
|
872
985
|
for (const deniedPath of policyPathEntries(policy.paths)) {
|
|
873
|
-
const deniedPaths = await
|
|
986
|
+
const deniedPaths = await resolveMutationComparablePaths(deniedPath);
|
|
874
987
|
for (const target of targetPaths) {
|
|
875
988
|
for (const denied of deniedPaths) {
|
|
876
989
|
if (isSamePath(denied, target) ||
|
|
@@ -881,7 +994,7 @@ async function assertMutationNotDenied(filePath, policy, options = {}) {
|
|
|
881
994
|
}
|
|
882
995
|
}
|
|
883
996
|
for (const deniedPrefix of policyPathEntries(policy.prefixes)) {
|
|
884
|
-
const deniedPaths = await
|
|
997
|
+
const deniedPaths = await resolveMutationComparablePaths(deniedPrefix);
|
|
885
998
|
for (const target of targetPaths) {
|
|
886
999
|
for (const denied of deniedPaths) {
|
|
887
1000
|
if (path_isPathInside(denied, target) ||
|
|
@@ -1344,7 +1457,20 @@ async function createNativeExclusiveFile(targetPath, mode) {
|
|
|
1344
1457
|
let fd;
|
|
1345
1458
|
let created;
|
|
1346
1459
|
try {
|
|
1347
|
-
|
|
1460
|
+
let opened;
|
|
1461
|
+
try {
|
|
1462
|
+
opened = binding.openBeneath(parent.fd, basename, nativeOpenFlags(external_node_fs_namespaceObject.constants.O_WRONLY | external_node_fs_namespaceObject.constants.O_CREAT | external_node_fs_namespaceObject.constants.O_EXCL));
|
|
1463
|
+
}
|
|
1464
|
+
catch (error) {
|
|
1465
|
+
// The parent open above and every post-create operation remain untagged.
|
|
1466
|
+
// Only this exclusive-open failure has enough provenance for a caller to
|
|
1467
|
+
// classify a Windows lock-file denial without swallowing setup failures.
|
|
1468
|
+
const openError = error;
|
|
1469
|
+
if (process.platform === "win32" && openError.code === "EPERM") {
|
|
1470
|
+
openError.path = targetPath;
|
|
1471
|
+
}
|
|
1472
|
+
throw error;
|
|
1473
|
+
}
|
|
1348
1474
|
fd = opened.fd;
|
|
1349
1475
|
external_node_fs_namespaceObject.fchmodSync(fd, mode);
|
|
1350
1476
|
created = external_node_fs_namespaceObject.fstatSync(fd);
|
|
@@ -1398,23 +1524,22 @@ function assertWithinMaxBytes(bytes, maxBytes) {
|
|
|
1398
1524
|
throw new errors_FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`);
|
|
1399
1525
|
}
|
|
1400
1526
|
}
|
|
1401
|
-
async function
|
|
1527
|
+
async function writeNativeInput(fd, input, maxBytes) {
|
|
1402
1528
|
if (input.kind === "buffer") {
|
|
1403
1529
|
const data = typeof input.data === "string"
|
|
1404
1530
|
? Buffer.from(input.data, input.encoding ?? "utf8")
|
|
1405
1531
|
: Buffer.from(input.data);
|
|
1406
1532
|
assertWithinMaxBytes(data.byteLength, maxBytes);
|
|
1407
|
-
|
|
1533
|
+
writeNativeFd(fd, data);
|
|
1534
|
+
return;
|
|
1408
1535
|
}
|
|
1409
|
-
const chunks = [];
|
|
1410
1536
|
let bytes = 0;
|
|
1411
1537
|
for await (const chunk of input.stream) {
|
|
1412
1538
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
1413
1539
|
bytes += buffer.byteLength;
|
|
1414
1540
|
assertWithinMaxBytes(bytes, maxBytes);
|
|
1415
|
-
|
|
1541
|
+
writeNativeFd(fd, buffer);
|
|
1416
1542
|
}
|
|
1417
|
-
return Buffer.concat(chunks, bytes);
|
|
1418
1543
|
}
|
|
1419
1544
|
function native_pinned_write_nativeOpenFlags(flags) {
|
|
1420
1545
|
const closeOnExec = external_node_fs_namespaceObject.constants.O_CLOEXEC;
|
|
@@ -1426,7 +1551,6 @@ function sameNativeIdentity(left, right) {
|
|
|
1426
1551
|
return left.dev === right.dev && left.ino === right.ino;
|
|
1427
1552
|
}
|
|
1428
1553
|
async function runPinnedWriteNative(binding, params) {
|
|
1429
|
-
const data = await inputToBuffer(params.input, params.maxBytes);
|
|
1430
1554
|
const root = await promises_namespaceObject.open(params.rootPath, external_node_fs_namespaceObject.constants.O_RDONLY |
|
|
1431
1555
|
(typeof external_node_fs_namespaceObject.constants.O_DIRECTORY === "number" ? external_node_fs_namespaceObject.constants.O_DIRECTORY : 0));
|
|
1432
1556
|
let parentFd;
|
|
@@ -1456,27 +1580,56 @@ async function runPinnedWriteNative(binding, params) {
|
|
|
1456
1580
|
!sameNativeIdentity(parentPathStat, parentIdentity)) {
|
|
1457
1581
|
throw new errors_FsSafeError("path-mismatch", "native write parent changed during resolution");
|
|
1458
1582
|
}
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1583
|
+
if (params.overwrite === false) {
|
|
1584
|
+
try {
|
|
1585
|
+
await promises_namespaceObject.lstat(external_node_path_namespaceObject.join(parentPath, params.basename));
|
|
1586
|
+
throw Object.assign(new Error("destination already exists"), { code: "EEXIST" });
|
|
1587
|
+
}
|
|
1588
|
+
catch (error) {
|
|
1589
|
+
if (error.code !== "ENOENT") {
|
|
1590
|
+
throw error;
|
|
1591
|
+
}
|
|
1466
1592
|
}
|
|
1467
1593
|
}
|
|
1468
1594
|
tempFd = binding.openBeneath(parentFd, tempName, native_pinned_write_nativeOpenFlags(external_node_fs_namespaceObject.constants.O_WRONLY | external_node_fs_namespaceObject.constants.O_CREAT | external_node_fs_namespaceObject.constants.O_EXCL)).fd;
|
|
1469
|
-
external_node_fs_namespaceObject.fchmodSync(tempFd, params.mode || 0o600);
|
|
1470
|
-
writeNativeFd(tempFd, data);
|
|
1471
|
-
syncNativeFileBestEffort(tempFd);
|
|
1472
1595
|
tempIdentity = external_node_fs_namespaceObject.fstatSync(tempFd);
|
|
1473
|
-
binding
|
|
1596
|
+
// Creation is requested at 0600 in the binding, but a restrictive umask
|
|
1597
|
+
// can remove owner access. Keep the unpublished inode private and
|
|
1598
|
+
// reopenable until the published name has been identity-fenced.
|
|
1599
|
+
external_node_fs_namespaceObject.fchmodSync(tempFd, 0o600);
|
|
1600
|
+
await writeNativeInput(tempFd, params.input, params.maxBytes);
|
|
1601
|
+
syncNativeFileBestEffort(tempFd);
|
|
1602
|
+
if (params.overwrite === false) {
|
|
1603
|
+
binding.renameNoReplace(parentFd, tempName, parentFd, params.basename);
|
|
1604
|
+
}
|
|
1605
|
+
else {
|
|
1606
|
+
binding.renameReplace(parentFd, tempName, parentFd, params.basename);
|
|
1607
|
+
}
|
|
1474
1608
|
renamed = true;
|
|
1475
1609
|
targetFd = binding.openBeneath(parentFd, params.basename, native_pinned_write_nativeOpenFlags(external_node_fs_namespaceObject.constants.O_RDONLY)).fd;
|
|
1476
1610
|
const targetIdentity = binding.fstatIdentity(targetFd);
|
|
1477
1611
|
if (!targetIdentity.isFile || !sameNativeIdentity(tempIdentity, targetIdentity)) {
|
|
1478
1612
|
throw new errors_FsSafeError("path-mismatch", "native write target changed after rename");
|
|
1479
1613
|
}
|
|
1614
|
+
// Native exclusive creation starts at 0600. Apply the requested mode only
|
|
1615
|
+
// after reopening and fencing the published name, both so mode 000 stays
|
|
1616
|
+
// verifiable and so broader modes are never exposed before that fence.
|
|
1617
|
+
try {
|
|
1618
|
+
external_node_fs_namespaceObject.fchmodSync(targetFd, params.mode);
|
|
1619
|
+
syncNativeFileBestEffort(targetFd);
|
|
1620
|
+
}
|
|
1621
|
+
catch (error) {
|
|
1622
|
+
external_node_fs_namespaceObject.closeSync(targetFd);
|
|
1623
|
+
targetFd = undefined;
|
|
1624
|
+
removeNativeCreatedFileIfStillPinned({
|
|
1625
|
+
binding,
|
|
1626
|
+
parentPath,
|
|
1627
|
+
parentFd,
|
|
1628
|
+
basename: params.basename,
|
|
1629
|
+
created: tempIdentity,
|
|
1630
|
+
});
|
|
1631
|
+
throw error;
|
|
1632
|
+
}
|
|
1480
1633
|
syncNativeFileBestEffort(parentFd);
|
|
1481
1634
|
return { dev: targetIdentity.dev, ino: targetIdentity.ino };
|
|
1482
1635
|
}
|
|
@@ -1613,7 +1766,7 @@ async function readFileDescriptorBounded(fd, maxBytes) {
|
|
|
1613
1766
|
});
|
|
1614
1767
|
}
|
|
1615
1768
|
/** Sync bounded read from a numeric descriptor. The caller owns the descriptor. */
|
|
1616
|
-
function
|
|
1769
|
+
function bounded_read_readFileDescriptorBoundedSync(fd, maxBytes) {
|
|
1617
1770
|
assertMaxBytes(maxBytes);
|
|
1618
1771
|
const chunks = [];
|
|
1619
1772
|
const scratch = createScratchBuffer(maxBytes);
|
|
@@ -1628,6 +1781,21 @@ function readFileDescriptorBoundedSync(fd, maxBytes) {
|
|
|
1628
1781
|
}
|
|
1629
1782
|
}
|
|
1630
1783
|
|
|
1784
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/test-hooks.js
|
|
1785
|
+
let test_hooks_fsSafeTestHooks;
|
|
1786
|
+
function allowFsSafeTestHooks() {
|
|
1787
|
+
return false || process.env.VITEST === "true";
|
|
1788
|
+
}
|
|
1789
|
+
function getFsSafeTestHooks() {
|
|
1790
|
+
return test_hooks_fsSafeTestHooks;
|
|
1791
|
+
}
|
|
1792
|
+
function __setFsSafeTestHooksForTest(hooks) {
|
|
1793
|
+
if (hooks && !allowFsSafeTestHooks()) {
|
|
1794
|
+
throw new Error("__setFsSafeTestHooksForTest is only available in tests");
|
|
1795
|
+
}
|
|
1796
|
+
test_hooks_fsSafeTestHooks = hooks;
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1631
1799
|
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js
|
|
1632
1800
|
|
|
1633
1801
|
|
|
@@ -1636,6 +1804,7 @@ function readFileDescriptorBoundedSync(fd, maxBytes) {
|
|
|
1636
1804
|
|
|
1637
1805
|
|
|
1638
1806
|
|
|
1807
|
+
|
|
1639
1808
|
const MAX_LOCK_PAYLOAD_BYTES = 1024 * 1024;
|
|
1640
1809
|
const SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES = 16;
|
|
1641
1810
|
const SIDECAR_LOCK_OWNERSHIP_TOKEN_BITS = SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES * 8;
|
|
@@ -1686,6 +1855,7 @@ function parseSidecarLockPayload(raw, parser) {
|
|
|
1686
1855
|
}
|
|
1687
1856
|
}
|
|
1688
1857
|
async function readSidecarLockSnapshot(lockPath, options = {}) {
|
|
1858
|
+
let handle;
|
|
1689
1859
|
try {
|
|
1690
1860
|
if (options.lockRoot) {
|
|
1691
1861
|
const opened = await options.lockRoot.open(relativeSidecarLockPath(options.lockRoot, lockPath));
|
|
@@ -1701,9 +1871,44 @@ async function readSidecarLockSnapshot(lockPath, options = {}) {
|
|
|
1701
1871
|
await opened.handle.close().catch(() => undefined);
|
|
1702
1872
|
}
|
|
1703
1873
|
}
|
|
1704
|
-
const
|
|
1705
|
-
|
|
1706
|
-
|
|
1874
|
+
const before = await promises_namespaceObject.lstat(lockPath);
|
|
1875
|
+
if (!before.isFile() || before.isSymbolicLink()) {
|
|
1876
|
+
if (options.rejectNonFile) {
|
|
1877
|
+
throw new errors_FsSafeError("not-file", `sidecar lock is not a regular file: ${lockPath}`);
|
|
1878
|
+
}
|
|
1879
|
+
return null;
|
|
1880
|
+
}
|
|
1881
|
+
await getFsSafeTestHooks()?.beforeSidecarLockSnapshotOpen?.(lockPath);
|
|
1882
|
+
const noFollow = process.platform !== "win32" && typeof external_node_fs_namespaceObject.constants.O_NOFOLLOW === "number"
|
|
1883
|
+
? external_node_fs_namespaceObject.constants.O_NOFOLLOW
|
|
1884
|
+
: 0;
|
|
1885
|
+
try {
|
|
1886
|
+
handle = await promises_namespaceObject.open(lockPath, external_node_fs_namespaceObject.constants.O_RDONLY |
|
|
1887
|
+
noFollow |
|
|
1888
|
+
(typeof external_node_fs_namespaceObject.constants.O_NONBLOCK === "number" ? external_node_fs_namespaceObject.constants.O_NONBLOCK : 0));
|
|
1889
|
+
}
|
|
1890
|
+
catch (error) {
|
|
1891
|
+
if (options.rejectNonFile && error.code === "ELOOP") {
|
|
1892
|
+
throw new errors_FsSafeError("not-file", `sidecar lock is not a regular file: ${lockPath}`, {
|
|
1893
|
+
cause: error,
|
|
1894
|
+
});
|
|
1895
|
+
}
|
|
1896
|
+
throw error;
|
|
1897
|
+
}
|
|
1898
|
+
const opened = await handle.stat();
|
|
1899
|
+
if (!opened.isFile()) {
|
|
1900
|
+
if (options.rejectNonFile) {
|
|
1901
|
+
throw new errors_FsSafeError("not-file", `sidecar lock is not a regular file: ${lockPath}`);
|
|
1902
|
+
}
|
|
1903
|
+
return null;
|
|
1904
|
+
}
|
|
1905
|
+
if (!options.allowDescriptorIdentityDrift && !file_identity_sameFileIdentity(before, opened))
|
|
1906
|
+
return null;
|
|
1907
|
+
const raw = (await readFileHandleBounded(handle, MAX_LOCK_PAYLOAD_BYTES)).toString("utf8");
|
|
1908
|
+
const after = await promises_namespaceObject.lstat(lockPath);
|
|
1909
|
+
if (!after.isFile() || !file_identity_sameFileIdentity(before, after))
|
|
1910
|
+
return null;
|
|
1911
|
+
return { raw, payload: parseSidecarLockPayload(raw, options.parsePayload), stat: after };
|
|
1707
1912
|
}
|
|
1708
1913
|
catch (err) {
|
|
1709
1914
|
if (err.code === "ENOENT" ||
|
|
@@ -1712,19 +1917,26 @@ async function readSidecarLockSnapshot(lockPath, options = {}) {
|
|
|
1712
1917
|
}
|
|
1713
1918
|
throw err;
|
|
1714
1919
|
}
|
|
1920
|
+
finally {
|
|
1921
|
+
await handle?.close().catch(() => undefined);
|
|
1922
|
+
}
|
|
1715
1923
|
}
|
|
1716
|
-
function readSidecarLockSnapshotSync(lockPath, parsePayload) {
|
|
1924
|
+
function readSidecarLockSnapshotSync(lockPath, parsePayload, options = {}) {
|
|
1717
1925
|
let fd;
|
|
1718
1926
|
try {
|
|
1719
1927
|
const before = fsSync.lstatSync(lockPath);
|
|
1720
|
-
if (!before.isFile() || before.isSymbolicLink())
|
|
1928
|
+
if (!before.isFile() || before.isSymbolicLink()) {
|
|
1929
|
+
if (options.rejectNonFile) {
|
|
1930
|
+
throw new FsSafeError("not-file", `sidecar lock is not a regular file: ${lockPath}`);
|
|
1931
|
+
}
|
|
1721
1932
|
return null;
|
|
1933
|
+
}
|
|
1722
1934
|
const noFollow = process.platform !== "win32" && typeof fsSync.constants.O_NOFOLLOW === "number"
|
|
1723
1935
|
? fsSync.constants.O_NOFOLLOW
|
|
1724
1936
|
: 0;
|
|
1725
1937
|
fd = fsSync.openSync(lockPath, fsSync.constants.O_RDONLY | noFollow);
|
|
1726
1938
|
const opened = fsSync.fstatSync(fd);
|
|
1727
|
-
const raw =
|
|
1939
|
+
const raw = readFileDescriptorBoundedSync(fd, MAX_LOCK_PAYLOAD_BYTES).toString("utf8");
|
|
1728
1940
|
const after = fsSync.lstatSync(lockPath);
|
|
1729
1941
|
if (!sameFileIdentity(before, opened) || !sameFileIdentity(opened, after))
|
|
1730
1942
|
return null;
|
|
@@ -1770,7 +1982,10 @@ function sidecarLockSnapshotMatches(current, observed) {
|
|
|
1770
1982
|
return observed.stat !== undefined && current.stat !== undefined;
|
|
1771
1983
|
}
|
|
1772
1984
|
async function removeSidecarLockIfUnchanged(lockPath, observed, options = {}) {
|
|
1773
|
-
const current = await readSidecarLockSnapshot(lockPath,
|
|
1985
|
+
const current = await readSidecarLockSnapshot(lockPath, {
|
|
1986
|
+
...options,
|
|
1987
|
+
allowDescriptorIdentityDrift: observed?.ownershipToken !== undefined,
|
|
1988
|
+
});
|
|
1774
1989
|
if (!current || !observed || !sidecarLockSnapshotMatches(current, observed)) {
|
|
1775
1990
|
return false;
|
|
1776
1991
|
}
|
|
@@ -1783,7 +1998,10 @@ async function removeSidecarLockIfUnchanged(lockPath, observed, options = {}) {
|
|
|
1783
1998
|
return true;
|
|
1784
1999
|
}
|
|
1785
2000
|
async function sidecarLockSnapshotStillPresent(lockPath, observed, options = {}) {
|
|
1786
|
-
const current = await readSidecarLockSnapshot(lockPath,
|
|
2001
|
+
const current = await readSidecarLockSnapshot(lockPath, {
|
|
2002
|
+
...options,
|
|
2003
|
+
allowDescriptorIdentityDrift: observed?.ownershipToken !== undefined,
|
|
2004
|
+
});
|
|
1787
2005
|
return !!current && !!observed && sidecarLockSnapshotMatches(current, observed);
|
|
1788
2006
|
}
|
|
1789
2007
|
async function sidecarReclaimGuardExists(pathname) {
|
|
@@ -1851,36 +2069,6 @@ async function removeStaleSidecarLockIfAllowed(params) {
|
|
|
1851
2069
|
}
|
|
1852
2070
|
}
|
|
1853
2071
|
|
|
1854
|
-
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/sidecar-lock-handle.js
|
|
1855
|
-
|
|
1856
|
-
function createSidecarLockHandle(params) {
|
|
1857
|
-
let released = false;
|
|
1858
|
-
const release = async () => {
|
|
1859
|
-
if (released)
|
|
1860
|
-
return;
|
|
1861
|
-
released = true;
|
|
1862
|
-
await params.release();
|
|
1863
|
-
};
|
|
1864
|
-
return {
|
|
1865
|
-
lockPath: params.lockPath,
|
|
1866
|
-
normalizedTargetPath: params.normalizedTargetPath,
|
|
1867
|
-
verifyStillHeld: params.verifyStillHeld,
|
|
1868
|
-
release,
|
|
1869
|
-
[Symbol.asyncDispose]: release,
|
|
1870
|
-
};
|
|
1871
|
-
}
|
|
1872
|
-
function createHeldSidecarLockHandle(params) {
|
|
1873
|
-
return createSidecarLockHandle({
|
|
1874
|
-
lockPath: params.held.lockPath,
|
|
1875
|
-
normalizedTargetPath: params.normalizedTargetPath,
|
|
1876
|
-
verifyStillHeld: async () => await sidecarLockSnapshotStillPresent(params.held.lockPath, params.held.snapshot, {
|
|
1877
|
-
lockRoot: params.held.lockRoot,
|
|
1878
|
-
parsePayload: params.held.parsePayload,
|
|
1879
|
-
}),
|
|
1880
|
-
release: params.release,
|
|
1881
|
-
});
|
|
1882
|
-
}
|
|
1883
|
-
|
|
1884
2072
|
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/sidecar-lock-policy.js
|
|
1885
2073
|
|
|
1886
2074
|
function computeSidecarLockDelayMs(retry, attempt) {
|
|
@@ -1891,7 +2079,23 @@ function computeSidecarLockDelayMs(retry, attempt) {
|
|
|
1891
2079
|
const jitter = retry.randomize ? 1 + Math.random() : 1;
|
|
1892
2080
|
return Math.min(maxTimeout, Math.round(base * jitter));
|
|
1893
2081
|
}
|
|
2082
|
+
// Windows denies access to a lock file while a just-unlinked directory entry
|
|
2083
|
+
// is still being torn down, so a contended acquire sees EPERM on a name that is
|
|
2084
|
+
// already gone -- both when creating it exclusively and when reading the
|
|
2085
|
+
// holder's snapshot. The next attempt succeeds, so this is contention rather
|
|
2086
|
+
// than a permission failure. The error must name the lock file itself: the
|
|
2087
|
+
// exclusive-create helper opens the parent directory first, and a denial from
|
|
2088
|
+
// that setup step carries no teardown evidence and has to reach the caller.
|
|
2089
|
+
const maxTransientLockDenials = 8;
|
|
2090
|
+
function isTransientLockFileDenial(error, lockPath) {
|
|
2091
|
+
const denial = error;
|
|
2092
|
+
return process.platform === "win32" && denial?.code === "EPERM" && denial.path === lockPath;
|
|
2093
|
+
}
|
|
1894
2094
|
function sidecarLockPayloadIsStale(payload, staleMs, nowMs) {
|
|
2095
|
+
const createdAtMs = sidecarLockPayloadCreatedAtMs(payload);
|
|
2096
|
+
return createdAtMs !== null && nowMs - createdAtMs > staleMs;
|
|
2097
|
+
}
|
|
2098
|
+
function sidecarLockPayloadCreatedAtMs(payload) {
|
|
1895
2099
|
const createdAt = payload &&
|
|
1896
2100
|
typeof payload === "object" &&
|
|
1897
2101
|
"createdAt" in payload &&
|
|
@@ -1899,11 +2103,12 @@ function sidecarLockPayloadIsStale(payload, staleMs, nowMs) {
|
|
|
1899
2103
|
? payload.createdAt
|
|
1900
2104
|
: "";
|
|
1901
2105
|
const createdAtMs = Date.parse(createdAt);
|
|
1902
|
-
return Number.isFinite(createdAtMs)
|
|
2106
|
+
return Number.isFinite(createdAtMs) ? createdAtMs : null;
|
|
1903
2107
|
}
|
|
1904
2108
|
async function defaultSidecarLockShouldReclaim(params) {
|
|
1905
|
-
|
|
1906
|
-
|
|
2109
|
+
const createdAtMs = sidecarLockPayloadCreatedAtMs(params.payload);
|
|
2110
|
+
if (createdAtMs !== null)
|
|
2111
|
+
return params.nowMs - createdAtMs > params.staleMs;
|
|
1907
2112
|
try {
|
|
1908
2113
|
return params.nowMs - (await promises_namespaceObject.stat(params.lockPath)).mtimeMs > params.staleMs;
|
|
1909
2114
|
}
|
|
@@ -1912,57 +2117,347 @@ async function defaultSidecarLockShouldReclaim(params) {
|
|
|
1912
2117
|
}
|
|
1913
2118
|
}
|
|
1914
2119
|
|
|
1915
|
-
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/sidecar-lock.js
|
|
1916
|
-
|
|
2120
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/sidecar-lock-acquire.js
|
|
1917
2121
|
|
|
1918
2122
|
|
|
1919
2123
|
|
|
1920
2124
|
|
|
1921
2125
|
|
|
1922
2126
|
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
const
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
globalWithState[GLOBAL_STATE_KEY] = new Map();
|
|
1930
|
-
}
|
|
1931
|
-
return globalWithState[GLOBAL_STATE_KEY];
|
|
1932
|
-
}
|
|
1933
|
-
function resolveManagerState(key) {
|
|
1934
|
-
const managers = getGlobalManagers();
|
|
1935
|
-
let state = managers.get(key);
|
|
1936
|
-
if (!state) {
|
|
1937
|
-
state = {
|
|
1938
|
-
cleanupRegistered: false,
|
|
1939
|
-
held: new Map(),
|
|
1940
|
-
reclaimCleanupRegistered: false,
|
|
1941
|
-
reclaimGuards: new Set(),
|
|
1942
|
-
};
|
|
1943
|
-
managers.set(key, state);
|
|
2127
|
+
async function resolveNormalizedTargetPath(targetPath) {
|
|
2128
|
+
const resolved = external_node_path_namespaceObject.resolve(targetPath);
|
|
2129
|
+
const dir = external_node_path_namespaceObject.dirname(resolved);
|
|
2130
|
+
await promises_namespaceObject.mkdir(dir, { recursive: true });
|
|
2131
|
+
try {
|
|
2132
|
+
return external_node_path_namespaceObject.join(await promises_namespaceObject.realpath(dir), external_node_path_namespaceObject.basename(resolved));
|
|
1944
2133
|
}
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
// Backfill state created by fs-safe versions that predate reclaim guards.
|
|
1948
|
-
state.reclaimCleanupRegistered ??= false;
|
|
1949
|
-
state.reclaimGuards ??= new Set();
|
|
1950
|
-
for (const held of state.held.values()) {
|
|
1951
|
-
held.refCount ??= 1;
|
|
1952
|
-
}
|
|
2134
|
+
catch {
|
|
2135
|
+
return resolved;
|
|
1953
2136
|
}
|
|
1954
|
-
return state;
|
|
1955
2137
|
}
|
|
1956
|
-
function
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
2138
|
+
async function acquireSidecarLock(options, context) {
|
|
2139
|
+
context.ensureExitCleanupRegistered();
|
|
2140
|
+
const normalizedTargetPath = await resolveNormalizedTargetPath(options.targetPath);
|
|
2141
|
+
const lockPath = options.lockPath ?? `${normalizedTargetPath}.lock`;
|
|
2142
|
+
const held = context.held.get(normalizedTargetPath);
|
|
2143
|
+
if (held &&
|
|
2144
|
+
options.reentrantOwner !== undefined &&
|
|
2145
|
+
held.reentrantOwner !== undefined &&
|
|
2146
|
+
options.reentrantOwner === held.reentrantOwner) {
|
|
2147
|
+
held.refCount += 1;
|
|
2148
|
+
return context.handleForHeldLock(normalizedTargetPath, held);
|
|
2149
|
+
}
|
|
2150
|
+
const startedAt = Date.now();
|
|
2151
|
+
const retry = options.retry ?? {};
|
|
2152
|
+
const maxRetries = options.timeoutMs === Number.POSITIVE_INFINITY ? undefined : retry.retries;
|
|
2153
|
+
const reclaimGuardPath = `${lockPath}.reclaim`;
|
|
2154
|
+
let ownsReclaimGuard = false;
|
|
2155
|
+
let attempt = 0;
|
|
2156
|
+
// Bounded so a genuine denial still surfaces as EPERM, not a lock timeout.
|
|
2157
|
+
let transientDenials = 0;
|
|
2158
|
+
const withinDenialBudget = () => ++transientDenials <= (/* inlined export .maxTransientLockDenials */8);
|
|
2159
|
+
const waitForRetry = async () => {
|
|
2160
|
+
const elapsed = Date.now() - startedAt;
|
|
2161
|
+
if ((options.timeoutMs !== undefined &&
|
|
2162
|
+
options.timeoutMs !== Number.POSITIVE_INFINITY &&
|
|
2163
|
+
elapsed >= options.timeoutMs) ||
|
|
2164
|
+
(maxRetries !== undefined && attempt >= maxRetries)) {
|
|
2165
|
+
throw Object.assign(new Error(`file lock timeout for ${normalizedTargetPath}`), {
|
|
2166
|
+
code: "file_lock_timeout",
|
|
2167
|
+
lockPath,
|
|
2168
|
+
normalizedTargetPath,
|
|
2169
|
+
});
|
|
1962
2170
|
}
|
|
1963
|
-
const
|
|
1964
|
-
|
|
1965
|
-
|
|
2171
|
+
const remaining = options.timeoutMs === undefined || options.timeoutMs === Number.POSITIVE_INFINITY
|
|
2172
|
+
? Number.POSITIVE_INFINITY
|
|
2173
|
+
: Math.max(0, options.timeoutMs - elapsed);
|
|
2174
|
+
const delay = Math.min(computeSidecarLockDelayMs(retry, attempt), remaining);
|
|
2175
|
+
attempt += 1;
|
|
2176
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
2177
|
+
};
|
|
2178
|
+
// Waiting can fail on the caller's own retry or deadline limits. Classifying
|
|
2179
|
+
// a denial as contention must not cost them the original diagnosis, so hand
|
|
2180
|
+
// the denial back when no further attempt can be scheduled.
|
|
2181
|
+
const retryOrRethrowDenial = async (denial) => {
|
|
2182
|
+
try {
|
|
2183
|
+
await waitForRetry();
|
|
2184
|
+
}
|
|
2185
|
+
catch (waitError) {
|
|
2186
|
+
if (waitError.code === "file_lock_timeout")
|
|
2187
|
+
throw denial;
|
|
2188
|
+
throw waitError;
|
|
2189
|
+
}
|
|
2190
|
+
};
|
|
2191
|
+
try {
|
|
2192
|
+
while (true) {
|
|
2193
|
+
if (!ownsReclaimGuard && (await sidecarReclaimGuardExists(reclaimGuardPath))) {
|
|
2194
|
+
await waitForRetry();
|
|
2195
|
+
continue;
|
|
2196
|
+
}
|
|
2197
|
+
let handle = null;
|
|
2198
|
+
let createdSnapshot = null;
|
|
2199
|
+
let lockFileCreateDenied = false;
|
|
2200
|
+
try {
|
|
2201
|
+
const payload = await options.payload();
|
|
2202
|
+
const { raw, ownershipToken } = serializeSidecarLockPayload(payload);
|
|
2203
|
+
if (options.lockRoot) {
|
|
2204
|
+
const relativeLockPath = relativeSidecarLockPath(options.lockRoot, lockPath);
|
|
2205
|
+
try {
|
|
2206
|
+
await options.lockRoot.create(relativeLockPath, raw, { mkdir: true, mode: 0o600 });
|
|
2207
|
+
}
|
|
2208
|
+
catch (error) {
|
|
2209
|
+
if (error instanceof errors_FsSafeError && error.code === "already-exists") {
|
|
2210
|
+
throw Object.assign(new Error("sidecar lock exists"), { code: "EEXIST" });
|
|
2211
|
+
}
|
|
2212
|
+
throw error;
|
|
2213
|
+
}
|
|
2214
|
+
createdSnapshot = { raw, payload, ownershipToken };
|
|
2215
|
+
handle = (await options.lockRoot.open(relativeLockPath)).handle;
|
|
2216
|
+
}
|
|
2217
|
+
else {
|
|
2218
|
+
try {
|
|
2219
|
+
handle =
|
|
2220
|
+
(await createNativeExclusiveFile(lockPath, 0o600)) ??
|
|
2221
|
+
(await promises_namespaceObject.open(lockPath, "wx"));
|
|
2222
|
+
}
|
|
2223
|
+
catch (createError) {
|
|
2224
|
+
lockFileCreateDenied = isTransientLockFileDenial(createError, lockPath);
|
|
2225
|
+
throw createError;
|
|
2226
|
+
}
|
|
2227
|
+
await handle.writeFile(raw, "utf8");
|
|
2228
|
+
}
|
|
2229
|
+
const snapshot = { raw, payload, stat: await handle.stat(), ownershipToken };
|
|
2230
|
+
const createdHeld = {
|
|
2231
|
+
refCount: 1,
|
|
2232
|
+
reentrantOwner: options.reentrantOwner,
|
|
2233
|
+
handle,
|
|
2234
|
+
lockPath,
|
|
2235
|
+
snapshot,
|
|
2236
|
+
acquiredAt: Date.now(),
|
|
2237
|
+
metadata: options.metadata ?? {},
|
|
2238
|
+
lockRoot: options.lockRoot,
|
|
2239
|
+
parsePayload: options.parsePayload,
|
|
2240
|
+
};
|
|
2241
|
+
context.held.set(normalizedTargetPath, createdHeld);
|
|
2242
|
+
if (ownsReclaimGuard) {
|
|
2243
|
+
try {
|
|
2244
|
+
await releaseSidecarReclaimGuard(context.reclaimGuards, reclaimGuardPath);
|
|
2245
|
+
ownsReclaimGuard = false;
|
|
2246
|
+
}
|
|
2247
|
+
catch (err) {
|
|
2248
|
+
await context.releaseHeldLock(normalizedTargetPath, createdHeld, { force: true });
|
|
2249
|
+
throw err;
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
const returnedHandle = context.handleForHeldLock(normalizedTargetPath, createdHeld);
|
|
2253
|
+
const interval = options.compromiseCheckIntervalMs;
|
|
2254
|
+
if (options.onCompromised && interval !== undefined && interval > 0) {
|
|
2255
|
+
createdHeld.compromiseTimer = setInterval(() => {
|
|
2256
|
+
void returnedHandle.verifyStillHeld().then((stillHeld) => {
|
|
2257
|
+
if (!stillHeld && createdHeld.compromiseTimer) {
|
|
2258
|
+
clearInterval(createdHeld.compromiseTimer);
|
|
2259
|
+
createdHeld.compromiseTimer = undefined;
|
|
2260
|
+
options.onCompromised?.({ lockPath, normalizedTargetPath });
|
|
2261
|
+
}
|
|
2262
|
+
});
|
|
2263
|
+
}, interval);
|
|
2264
|
+
createdHeld.compromiseTimer.unref();
|
|
2265
|
+
}
|
|
2266
|
+
return returnedHandle;
|
|
2267
|
+
}
|
|
2268
|
+
catch (err) {
|
|
2269
|
+
if (handle) {
|
|
2270
|
+
const failedSnapshot = { payload: null };
|
|
2271
|
+
try {
|
|
2272
|
+
failedSnapshot.stat = await handle.stat();
|
|
2273
|
+
}
|
|
2274
|
+
catch {
|
|
2275
|
+
// Best-effort cleanup of a failed exclusive create.
|
|
2276
|
+
}
|
|
2277
|
+
const current = context.held.get(normalizedTargetPath);
|
|
2278
|
+
if (current?.handle === handle) {
|
|
2279
|
+
context.held.delete(normalizedTargetPath);
|
|
2280
|
+
}
|
|
2281
|
+
await handle.close().catch(() => undefined);
|
|
2282
|
+
// The file may be empty or partial JSON, so remove by the identity
|
|
2283
|
+
// captured from our exclusive handle rather than by pathname alone.
|
|
2284
|
+
await removeSidecarLockIfUnchanged(lockPath, failedSnapshot, {
|
|
2285
|
+
lockRoot: options.lockRoot,
|
|
2286
|
+
parsePayload: options.parsePayload,
|
|
2287
|
+
});
|
|
2288
|
+
}
|
|
2289
|
+
else if (createdSnapshot) {
|
|
2290
|
+
await removeSidecarLockIfUnchanged(lockPath, createdSnapshot, {
|
|
2291
|
+
lockRoot: options.lockRoot,
|
|
2292
|
+
parsePayload: options.parsePayload,
|
|
2293
|
+
});
|
|
2294
|
+
}
|
|
2295
|
+
if (lockFileCreateDenied && withinDenialBudget()) {
|
|
2296
|
+
await retryOrRethrowDenial(err);
|
|
2297
|
+
continue;
|
|
2298
|
+
}
|
|
2299
|
+
if (err.code !== "EEXIST") {
|
|
2300
|
+
throw err;
|
|
2301
|
+
}
|
|
2302
|
+
if (ownsReclaimGuard) {
|
|
2303
|
+
await releaseSidecarReclaimGuard(context.reclaimGuards, reclaimGuardPath);
|
|
2304
|
+
ownsReclaimGuard = false;
|
|
2305
|
+
continue;
|
|
2306
|
+
}
|
|
2307
|
+
const nowMs = Date.now();
|
|
2308
|
+
let snapshot;
|
|
2309
|
+
try {
|
|
2310
|
+
snapshot = await readSidecarLockSnapshot(lockPath, {
|
|
2311
|
+
lockRoot: options.lockRoot,
|
|
2312
|
+
parsePayload: options.parsePayload,
|
|
2313
|
+
rejectNonFile: true,
|
|
2314
|
+
});
|
|
2315
|
+
}
|
|
2316
|
+
catch (readErr) {
|
|
2317
|
+
if (!isTransientLockFileDenial(readErr, lockPath) || !withinDenialBudget())
|
|
2318
|
+
throw readErr;
|
|
2319
|
+
await retryOrRethrowDenial(readErr);
|
|
2320
|
+
continue;
|
|
2321
|
+
}
|
|
2322
|
+
if (!snapshot) {
|
|
2323
|
+
continue;
|
|
2324
|
+
}
|
|
2325
|
+
if (context.held.has(normalizedTargetPath)) {
|
|
2326
|
+
await waitForRetry();
|
|
2327
|
+
continue;
|
|
2328
|
+
}
|
|
2329
|
+
const shouldReclaim = options.shouldReclaim ?? defaultSidecarLockShouldReclaim;
|
|
2330
|
+
if (await shouldReclaim({
|
|
2331
|
+
lockPath,
|
|
2332
|
+
normalizedTargetPath,
|
|
2333
|
+
payload: snapshot?.payload ?? null,
|
|
2334
|
+
staleMs: options.staleMs,
|
|
2335
|
+
nowMs,
|
|
2336
|
+
heldByThisProcess: context.held.has(normalizedTargetPath),
|
|
2337
|
+
})) {
|
|
2338
|
+
if (!(await sidecarLockSnapshotStillPresent(lockPath, snapshot, {
|
|
2339
|
+
lockRoot: options.lockRoot,
|
|
2340
|
+
parsePayload: options.parsePayload,
|
|
2341
|
+
}))) {
|
|
2342
|
+
continue;
|
|
2343
|
+
}
|
|
2344
|
+
const staleRecovery = options.staleRecovery ?? "fail-closed";
|
|
2345
|
+
if (staleRecovery === "remove-if-unchanged") {
|
|
2346
|
+
if (!(await tryAcquireSidecarReclaimGuard(context.reclaimGuards, reclaimGuardPath))) {
|
|
2347
|
+
await waitForRetry();
|
|
2348
|
+
continue;
|
|
2349
|
+
}
|
|
2350
|
+
ownsReclaimGuard = true;
|
|
2351
|
+
const removal = await removeStaleSidecarLockIfAllowed({
|
|
2352
|
+
lockPath,
|
|
2353
|
+
normalizedTargetPath,
|
|
2354
|
+
snapshot,
|
|
2355
|
+
shouldRemoveStaleLock: options.shouldRemoveStaleLock,
|
|
2356
|
+
lockRoot: options.lockRoot,
|
|
2357
|
+
parsePayload: options.parsePayload,
|
|
2358
|
+
});
|
|
2359
|
+
if (removal === "removed" || removal === "changed") {
|
|
2360
|
+
continue;
|
|
2361
|
+
}
|
|
2362
|
+
await releaseSidecarReclaimGuard(context.reclaimGuards, reclaimGuardPath);
|
|
2363
|
+
ownsReclaimGuard = false;
|
|
2364
|
+
}
|
|
2365
|
+
throw Object.assign(new Error(`file lock stale for ${normalizedTargetPath}`), {
|
|
2366
|
+
code: "file_lock_stale",
|
|
2367
|
+
lockPath,
|
|
2368
|
+
normalizedTargetPath,
|
|
2369
|
+
});
|
|
2370
|
+
}
|
|
2371
|
+
await waitForRetry();
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
finally {
|
|
2376
|
+
if (ownsReclaimGuard) {
|
|
2377
|
+
await releaseSidecarReclaimGuard(context.reclaimGuards, reclaimGuardPath).catch(() => undefined);
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/sidecar-lock-handle.js
|
|
2383
|
+
|
|
2384
|
+
function createSidecarLockHandle(params) {
|
|
2385
|
+
let released = false;
|
|
2386
|
+
const release = async () => {
|
|
2387
|
+
if (released)
|
|
2388
|
+
return;
|
|
2389
|
+
released = true;
|
|
2390
|
+
await params.release();
|
|
2391
|
+
};
|
|
2392
|
+
return {
|
|
2393
|
+
lockPath: params.lockPath,
|
|
2394
|
+
normalizedTargetPath: params.normalizedTargetPath,
|
|
2395
|
+
verifyStillHeld: params.verifyStillHeld,
|
|
2396
|
+
release,
|
|
2397
|
+
[Symbol.asyncDispose]: release,
|
|
2398
|
+
};
|
|
2399
|
+
}
|
|
2400
|
+
function createHeldSidecarLockHandle(params) {
|
|
2401
|
+
return createSidecarLockHandle({
|
|
2402
|
+
lockPath: params.held.lockPath,
|
|
2403
|
+
normalizedTargetPath: params.normalizedTargetPath,
|
|
2404
|
+
verifyStillHeld: async () => await sidecarLockSnapshotStillPresent(params.held.lockPath, params.held.snapshot, {
|
|
2405
|
+
lockRoot: params.held.lockRoot,
|
|
2406
|
+
parsePayload: params.held.parsePayload,
|
|
2407
|
+
}),
|
|
2408
|
+
release: params.release,
|
|
2409
|
+
});
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/sidecar-lock.js
|
|
2413
|
+
|
|
2414
|
+
|
|
2415
|
+
|
|
2416
|
+
|
|
2417
|
+
|
|
2418
|
+
const GLOBAL_STATE_KEY = Symbol.for("fsSafe.sidecarLockManagers");
|
|
2419
|
+
const GLOBAL_CLEANUP_KEY = Symbol.for("fsSafe.sidecarLockCleanupRegistered");
|
|
2420
|
+
const GLOBAL_CLEANUP_HANDLER_KEY = Symbol.for("fsSafe.sidecarLockCleanupHandler");
|
|
2421
|
+
function getGlobalManagers() {
|
|
2422
|
+
const globalWithState = globalThis;
|
|
2423
|
+
if (!globalWithState[GLOBAL_STATE_KEY]) {
|
|
2424
|
+
globalWithState[GLOBAL_STATE_KEY] = new Map();
|
|
2425
|
+
}
|
|
2426
|
+
return globalWithState[GLOBAL_STATE_KEY];
|
|
2427
|
+
}
|
|
2428
|
+
function resolveManagerState(key) {
|
|
2429
|
+
const managers = getGlobalManagers();
|
|
2430
|
+
let state = managers.get(key);
|
|
2431
|
+
if (!state) {
|
|
2432
|
+
state = {
|
|
2433
|
+
cleanupRegistered: false,
|
|
2434
|
+
held: new Map(),
|
|
2435
|
+
reclaimCleanupRegistered: false,
|
|
2436
|
+
reclaimGuards: new Set(),
|
|
2437
|
+
};
|
|
2438
|
+
managers.set(key, state);
|
|
2439
|
+
}
|
|
2440
|
+
else {
|
|
2441
|
+
// The global manager symbol is shared across package copies and hot reloads.
|
|
2442
|
+
// Backfill state created by fs-safe versions that predate reclaim guards.
|
|
2443
|
+
state.reclaimCleanupRegistered ??= false;
|
|
2444
|
+
state.reclaimGuards ??= new Set();
|
|
2445
|
+
for (const held of state.held.values()) {
|
|
2446
|
+
held.refCount ??= 1;
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
return state;
|
|
2450
|
+
}
|
|
2451
|
+
function snapshotMatchesSync(lockPath, observed) {
|
|
2452
|
+
let fd;
|
|
2453
|
+
try {
|
|
2454
|
+
const beforeStat = external_node_fs_namespaceObject.lstatSync(lockPath);
|
|
2455
|
+
if (!beforeStat.isFile()) {
|
|
2456
|
+
return false;
|
|
2457
|
+
}
|
|
2458
|
+
const openFlags = external_node_fs_namespaceObject.constants.O_RDONLY |
|
|
2459
|
+
(process.platform !== "win32" && typeof external_node_fs_namespaceObject.constants.O_NOFOLLOW === "number"
|
|
2460
|
+
? external_node_fs_namespaceObject.constants.O_NOFOLLOW
|
|
1966
2461
|
: 0) |
|
|
1967
2462
|
(typeof external_node_fs_namespaceObject.constants.O_NONBLOCK === "number" ? external_node_fs_namespaceObject.constants.O_NONBLOCK : 0);
|
|
1968
2463
|
fd = external_node_fs_namespaceObject.openSync(lockPath, openFlags);
|
|
@@ -1994,17 +2489,6 @@ function snapshotMatchesSync(lockPath, observed) {
|
|
|
1994
2489
|
}
|
|
1995
2490
|
}
|
|
1996
2491
|
}
|
|
1997
|
-
async function resolveNormalizedTargetPath(targetPath) {
|
|
1998
|
-
const resolved = external_node_path_namespaceObject.resolve(targetPath);
|
|
1999
|
-
const dir = external_node_path_namespaceObject.dirname(resolved);
|
|
2000
|
-
await promises_namespaceObject.mkdir(dir, { recursive: true });
|
|
2001
|
-
try {
|
|
2002
|
-
return external_node_path_namespaceObject.join(await promises_namespaceObject.realpath(dir), external_node_path_namespaceObject.basename(resolved));
|
|
2003
|
-
}
|
|
2004
|
-
catch {
|
|
2005
|
-
return resolved;
|
|
2006
|
-
}
|
|
2007
|
-
}
|
|
2008
2492
|
function releaseAllReclaimGuardsSync(state) {
|
|
2009
2493
|
for (const reclaimGuardPath of state.reclaimGuards) {
|
|
2010
2494
|
try {
|
|
@@ -2031,6 +2515,19 @@ function releaseAllLocksSync(state) {
|
|
|
2031
2515
|
}
|
|
2032
2516
|
releaseAllReclaimGuardsSync(state);
|
|
2033
2517
|
}
|
|
2518
|
+
function ensureGlobalExitCleanupRegistered() {
|
|
2519
|
+
const globalWithCleanup = globalThis;
|
|
2520
|
+
if (globalWithCleanup[GLOBAL_CLEANUP_KEY])
|
|
2521
|
+
return;
|
|
2522
|
+
globalWithCleanup[GLOBAL_CLEANUP_KEY] = true;
|
|
2523
|
+
const cleanup = () => {
|
|
2524
|
+
for (const state of getGlobalManagers().values()) {
|
|
2525
|
+
releaseAllLocksSync(state);
|
|
2526
|
+
}
|
|
2527
|
+
};
|
|
2528
|
+
globalWithCleanup[GLOBAL_CLEANUP_HANDLER_KEY] = cleanup;
|
|
2529
|
+
process.on("exit", cleanup);
|
|
2530
|
+
}
|
|
2034
2531
|
async function releaseHeldLock(state, normalizedTargetPath, held, options = {}) {
|
|
2035
2532
|
const current = state.held.get(normalizedTargetPath);
|
|
2036
2533
|
if (current !== held) {
|
|
@@ -2079,217 +2576,18 @@ function handleForHeldLock(state, normalizedTargetPath, held) {
|
|
|
2079
2576
|
function createSidecarLockManager(key) {
|
|
2080
2577
|
const state = resolveManagerState(key);
|
|
2081
2578
|
function ensureExitCleanupRegistered() {
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
process.on("exit", () => releaseAllLocksSync(state));
|
|
2086
|
-
return;
|
|
2087
|
-
}
|
|
2088
|
-
if (!state.reclaimCleanupRegistered) {
|
|
2089
|
-
state.reclaimCleanupRegistered = true;
|
|
2090
|
-
process.on("exit", () => releaseAllReclaimGuardsSync(state));
|
|
2091
|
-
}
|
|
2579
|
+
state.cleanupRegistered = true;
|
|
2580
|
+
state.reclaimCleanupRegistered = true;
|
|
2581
|
+
ensureGlobalExitCleanupRegistered();
|
|
2092
2582
|
}
|
|
2093
2583
|
async function acquire(options) {
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
options.reentrantOwner === held.reentrantOwner) {
|
|
2102
|
-
held.refCount += 1;
|
|
2103
|
-
return handleForHeldLock(state, normalizedTargetPath, held);
|
|
2104
|
-
}
|
|
2105
|
-
const startedAt = Date.now();
|
|
2106
|
-
const retry = options.retry ?? {};
|
|
2107
|
-
const maxRetries = options.timeoutMs === Number.POSITIVE_INFINITY ? undefined : retry.retries;
|
|
2108
|
-
const reclaimGuardPath = `${lockPath}.reclaim`;
|
|
2109
|
-
let ownsReclaimGuard = false;
|
|
2110
|
-
let attempt = 0;
|
|
2111
|
-
const waitForRetry = async () => {
|
|
2112
|
-
const elapsed = Date.now() - startedAt;
|
|
2113
|
-
if ((options.timeoutMs !== undefined &&
|
|
2114
|
-
options.timeoutMs !== Number.POSITIVE_INFINITY &&
|
|
2115
|
-
elapsed >= options.timeoutMs) ||
|
|
2116
|
-
(maxRetries !== undefined && attempt >= maxRetries)) {
|
|
2117
|
-
throw Object.assign(new Error(`file lock timeout for ${normalizedTargetPath}`), {
|
|
2118
|
-
code: "file_lock_timeout",
|
|
2119
|
-
lockPath,
|
|
2120
|
-
normalizedTargetPath,
|
|
2121
|
-
});
|
|
2122
|
-
}
|
|
2123
|
-
const remaining = options.timeoutMs === undefined || options.timeoutMs === Number.POSITIVE_INFINITY
|
|
2124
|
-
? Number.POSITIVE_INFINITY
|
|
2125
|
-
: Math.max(0, options.timeoutMs - elapsed);
|
|
2126
|
-
const delay = Math.min(computeSidecarLockDelayMs(retry, attempt), remaining);
|
|
2127
|
-
attempt += 1;
|
|
2128
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
2129
|
-
};
|
|
2130
|
-
try {
|
|
2131
|
-
while (true) {
|
|
2132
|
-
if (!ownsReclaimGuard && (await sidecarReclaimGuardExists(reclaimGuardPath))) {
|
|
2133
|
-
await waitForRetry();
|
|
2134
|
-
continue;
|
|
2135
|
-
}
|
|
2136
|
-
let handle = null;
|
|
2137
|
-
try {
|
|
2138
|
-
const payload = await options.payload();
|
|
2139
|
-
const { raw, ownershipToken } = serializeSidecarLockPayload(payload);
|
|
2140
|
-
if (options.lockRoot) {
|
|
2141
|
-
const relativeLockPath = relativeSidecarLockPath(options.lockRoot, lockPath);
|
|
2142
|
-
try {
|
|
2143
|
-
await options.lockRoot.create(relativeLockPath, raw, { mkdir: true, mode: 0o600 });
|
|
2144
|
-
}
|
|
2145
|
-
catch (error) {
|
|
2146
|
-
if (error instanceof errors_FsSafeError && error.code === "already-exists") {
|
|
2147
|
-
throw Object.assign(new Error("sidecar lock exists"), { code: "EEXIST" });
|
|
2148
|
-
}
|
|
2149
|
-
throw error;
|
|
2150
|
-
}
|
|
2151
|
-
handle = (await options.lockRoot.open(relativeLockPath)).handle;
|
|
2152
|
-
}
|
|
2153
|
-
else {
|
|
2154
|
-
handle = (await createNativeExclusiveFile(lockPath, 0o600)) ?? await promises_namespaceObject.open(lockPath, "wx");
|
|
2155
|
-
await handle.writeFile(raw, "utf8");
|
|
2156
|
-
}
|
|
2157
|
-
const snapshot = { raw, payload, stat: await handle.stat(), ownershipToken };
|
|
2158
|
-
const createdHeld = {
|
|
2159
|
-
refCount: 1,
|
|
2160
|
-
reentrantOwner: options.reentrantOwner,
|
|
2161
|
-
handle,
|
|
2162
|
-
lockPath,
|
|
2163
|
-
snapshot,
|
|
2164
|
-
acquiredAt: Date.now(),
|
|
2165
|
-
metadata: options.metadata ?? {},
|
|
2166
|
-
lockRoot: options.lockRoot,
|
|
2167
|
-
parsePayload: options.parsePayload,
|
|
2168
|
-
};
|
|
2169
|
-
state.held.set(normalizedTargetPath, createdHeld);
|
|
2170
|
-
if (ownsReclaimGuard) {
|
|
2171
|
-
try {
|
|
2172
|
-
await releaseSidecarReclaimGuard(state.reclaimGuards, reclaimGuardPath);
|
|
2173
|
-
ownsReclaimGuard = false;
|
|
2174
|
-
}
|
|
2175
|
-
catch (err) {
|
|
2176
|
-
await releaseHeldLock(state, normalizedTargetPath, createdHeld, { force: true });
|
|
2177
|
-
throw err;
|
|
2178
|
-
}
|
|
2179
|
-
}
|
|
2180
|
-
const returnedHandle = handleForHeldLock(state, normalizedTargetPath, createdHeld);
|
|
2181
|
-
const interval = options.compromiseCheckIntervalMs;
|
|
2182
|
-
if (options.onCompromised && interval !== undefined && interval > 0) {
|
|
2183
|
-
createdHeld.compromiseTimer = setInterval(() => {
|
|
2184
|
-
void returnedHandle.verifyStillHeld().then((stillHeld) => {
|
|
2185
|
-
if (!stillHeld && createdHeld.compromiseTimer) {
|
|
2186
|
-
clearInterval(createdHeld.compromiseTimer);
|
|
2187
|
-
createdHeld.compromiseTimer = undefined;
|
|
2188
|
-
options.onCompromised?.({ lockPath, normalizedTargetPath });
|
|
2189
|
-
}
|
|
2190
|
-
});
|
|
2191
|
-
}, interval);
|
|
2192
|
-
createdHeld.compromiseTimer.unref();
|
|
2193
|
-
}
|
|
2194
|
-
return returnedHandle;
|
|
2195
|
-
}
|
|
2196
|
-
catch (err) {
|
|
2197
|
-
if (handle) {
|
|
2198
|
-
const failedSnapshot = { payload: null };
|
|
2199
|
-
try {
|
|
2200
|
-
failedSnapshot.stat = await handle.stat();
|
|
2201
|
-
}
|
|
2202
|
-
catch {
|
|
2203
|
-
// Best-effort cleanup of a failed exclusive create.
|
|
2204
|
-
}
|
|
2205
|
-
const current = state.held.get(normalizedTargetPath);
|
|
2206
|
-
if (current?.handle === handle) {
|
|
2207
|
-
state.held.delete(normalizedTargetPath);
|
|
2208
|
-
}
|
|
2209
|
-
// If payload serialization/write fails, the file may be empty or
|
|
2210
|
-
// partial JSON, so remove while our exclusive handle is still open.
|
|
2211
|
-
if (!options.lockRoot) {
|
|
2212
|
-
await promises_namespaceObject.rm(lockPath, { force: true }).catch(() => undefined);
|
|
2213
|
-
}
|
|
2214
|
-
await handle.close().catch(() => undefined);
|
|
2215
|
-
// Windows can refuse removing an open file; retry after close but
|
|
2216
|
-
// only if the path still points at the file identity we created.
|
|
2217
|
-
await removeSidecarLockIfUnchanged(lockPath, failedSnapshot, {
|
|
2218
|
-
lockRoot: options.lockRoot,
|
|
2219
|
-
parsePayload: options.parsePayload,
|
|
2220
|
-
});
|
|
2221
|
-
}
|
|
2222
|
-
if (err.code !== "EEXIST") {
|
|
2223
|
-
throw err;
|
|
2224
|
-
}
|
|
2225
|
-
if (ownsReclaimGuard) {
|
|
2226
|
-
await releaseSidecarReclaimGuard(state.reclaimGuards, reclaimGuardPath);
|
|
2227
|
-
ownsReclaimGuard = false;
|
|
2228
|
-
continue;
|
|
2229
|
-
}
|
|
2230
|
-
const nowMs = Date.now();
|
|
2231
|
-
const snapshot = await readSidecarLockSnapshot(lockPath, {
|
|
2232
|
-
lockRoot: options.lockRoot,
|
|
2233
|
-
parsePayload: options.parsePayload,
|
|
2234
|
-
});
|
|
2235
|
-
if (!snapshot) {
|
|
2236
|
-
continue;
|
|
2237
|
-
}
|
|
2238
|
-
if (state.held.has(normalizedTargetPath)) {
|
|
2239
|
-
await waitForRetry();
|
|
2240
|
-
continue;
|
|
2241
|
-
}
|
|
2242
|
-
const shouldReclaim = options.shouldReclaim ?? defaultSidecarLockShouldReclaim;
|
|
2243
|
-
if (await shouldReclaim({
|
|
2244
|
-
lockPath,
|
|
2245
|
-
normalizedTargetPath,
|
|
2246
|
-
payload: snapshot?.payload ?? null,
|
|
2247
|
-
staleMs: options.staleMs,
|
|
2248
|
-
nowMs,
|
|
2249
|
-
heldByThisProcess: state.held.has(normalizedTargetPath),
|
|
2250
|
-
})) {
|
|
2251
|
-
if (!(await sidecarLockSnapshotStillPresent(lockPath, snapshot, {
|
|
2252
|
-
lockRoot: options.lockRoot,
|
|
2253
|
-
parsePayload: options.parsePayload,
|
|
2254
|
-
}))) {
|
|
2255
|
-
continue;
|
|
2256
|
-
}
|
|
2257
|
-
const staleRecovery = options.staleRecovery ?? "fail-closed";
|
|
2258
|
-
if (staleRecovery === "remove-if-unchanged") {
|
|
2259
|
-
if (!(await tryAcquireSidecarReclaimGuard(state.reclaimGuards, reclaimGuardPath))) {
|
|
2260
|
-
await waitForRetry();
|
|
2261
|
-
continue;
|
|
2262
|
-
}
|
|
2263
|
-
ownsReclaimGuard = true;
|
|
2264
|
-
const removal = await removeStaleSidecarLockIfAllowed({
|
|
2265
|
-
lockPath,
|
|
2266
|
-
normalizedTargetPath,
|
|
2267
|
-
snapshot,
|
|
2268
|
-
shouldRemoveStaleLock: options.shouldRemoveStaleLock,
|
|
2269
|
-
lockRoot: options.lockRoot,
|
|
2270
|
-
parsePayload: options.parsePayload,
|
|
2271
|
-
});
|
|
2272
|
-
if (removal === "removed" || removal === "changed") {
|
|
2273
|
-
continue;
|
|
2274
|
-
}
|
|
2275
|
-
await releaseSidecarReclaimGuard(state.reclaimGuards, reclaimGuardPath);
|
|
2276
|
-
ownsReclaimGuard = false;
|
|
2277
|
-
}
|
|
2278
|
-
throw Object.assign(new Error(`file lock stale for ${normalizedTargetPath}`), {
|
|
2279
|
-
code: "file_lock_stale",
|
|
2280
|
-
lockPath,
|
|
2281
|
-
normalizedTargetPath,
|
|
2282
|
-
});
|
|
2283
|
-
}
|
|
2284
|
-
await waitForRetry();
|
|
2285
|
-
}
|
|
2286
|
-
}
|
|
2287
|
-
}
|
|
2288
|
-
finally {
|
|
2289
|
-
if (ownsReclaimGuard) {
|
|
2290
|
-
await releaseSidecarReclaimGuard(state.reclaimGuards, reclaimGuardPath).catch(() => undefined);
|
|
2291
|
-
}
|
|
2292
|
-
}
|
|
2584
|
+
return await acquireSidecarLock(options, {
|
|
2585
|
+
held: state.held,
|
|
2586
|
+
reclaimGuards: state.reclaimGuards,
|
|
2587
|
+
ensureExitCleanupRegistered,
|
|
2588
|
+
handleForHeldLock: (normalizedTargetPath, held) => handleForHeldLock(state, normalizedTargetPath, held),
|
|
2589
|
+
releaseHeldLock: async (normalizedTargetPath, held, releaseOptions) => await releaseHeldLock(state, normalizedTargetPath, held, releaseOptions),
|
|
2590
|
+
});
|
|
2293
2591
|
}
|
|
2294
2592
|
async function withLock(options, fn) {
|
|
2295
2593
|
const lock = await acquire(options);
|
|
@@ -2325,21 +2623,6 @@ async function withSidecarLock(targetPath, options, fn) {
|
|
|
2325
2623
|
return await manager.withLock({ ...acquireOptions, targetPath }, fn);
|
|
2326
2624
|
}
|
|
2327
2625
|
|
|
2328
|
-
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/test-hooks.js
|
|
2329
|
-
let test_hooks_fsSafeTestHooks;
|
|
2330
|
-
function allowFsSafeTestHooks() {
|
|
2331
|
-
return false || process.env.VITEST === "true";
|
|
2332
|
-
}
|
|
2333
|
-
function getFsSafeTestHooks() {
|
|
2334
|
-
return test_hooks_fsSafeTestHooks;
|
|
2335
|
-
}
|
|
2336
|
-
function __setFsSafeTestHooksForTest(hooks) {
|
|
2337
|
-
if (hooks && !allowFsSafeTestHooks()) {
|
|
2338
|
-
throw new Error("__setFsSafeTestHooksForTest is only available in tests");
|
|
2339
|
-
}
|
|
2340
|
-
test_hooks_fsSafeTestHooks = hooks;
|
|
2341
|
-
}
|
|
2342
|
-
|
|
2343
2626
|
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/pinned-write.js
|
|
2344
2627
|
|
|
2345
2628
|
|
|
@@ -2412,7 +2695,7 @@ async function runPinnedWriteHelper(params) {
|
|
|
2412
2695
|
return await runPinnedWriteFallback(params);
|
|
2413
2696
|
}
|
|
2414
2697
|
const native = getNativeBinding();
|
|
2415
|
-
if (native
|
|
2698
|
+
if (native) {
|
|
2416
2699
|
return await runPinnedWriteNative(native, params);
|
|
2417
2700
|
}
|
|
2418
2701
|
return await runPinnedWriteFallback(params);
|
|
@@ -2450,6 +2733,7 @@ async function runPinnedWriteFallback(params) {
|
|
|
2450
2733
|
parentPath = await mkdirPathComponentsWithGuards({
|
|
2451
2734
|
rootReal: params.rootPath,
|
|
2452
2735
|
targetPath: parentPath,
|
|
2736
|
+
beforeComponent: async (componentPath) => await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("mkdir", componentPath),
|
|
2453
2737
|
});
|
|
2454
2738
|
}
|
|
2455
2739
|
const parentGuard = params.mkdir
|
|
@@ -2589,77 +2873,135 @@ async function runPinnedWriteFallback(params) {
|
|
|
2589
2873
|
return { dev: targetStat.dev, ino: targetStat.ino };
|
|
2590
2874
|
}
|
|
2591
2875
|
|
|
2592
|
-
;// CONCATENATED MODULE:
|
|
2593
|
-
const
|
|
2594
|
-
|
|
2876
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/error-detail.js
|
|
2877
|
+
const UNSAFE_ERROR_DETAIL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/gu;
|
|
2878
|
+
function formatErrorDetail(value) {
|
|
2879
|
+
return value.replace(UNSAFE_ERROR_DETAIL_CHARACTERS, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
2880
|
+
}
|
|
2595
2881
|
|
|
2882
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/root-path-symlink.js
|
|
2596
2883
|
|
|
2597
2884
|
|
|
2598
2885
|
|
|
2599
|
-
|
|
2600
|
-
|
|
2886
|
+
|
|
2887
|
+
|
|
2888
|
+
|
|
2889
|
+
function normalizeSymlinkResolutionError(error) {
|
|
2890
|
+
if (isSymlinkOpenError(error)) {
|
|
2891
|
+
throw new errors_FsSafeError("symlink", "symlink path could not be resolved", {
|
|
2892
|
+
cause: error instanceof Error ? error : undefined,
|
|
2893
|
+
});
|
|
2894
|
+
}
|
|
2895
|
+
if (!path_isNotFoundPathError(error))
|
|
2896
|
+
throw error;
|
|
2601
2897
|
}
|
|
2602
|
-
async function
|
|
2898
|
+
async function resolveSymlinkHopPath(symlinkPath) {
|
|
2603
2899
|
try {
|
|
2604
|
-
await promises_namespaceObject.
|
|
2605
|
-
return true;
|
|
2900
|
+
return external_node_path_namespaceObject.resolve(await promises_namespaceObject.realpath(symlinkPath));
|
|
2606
2901
|
}
|
|
2607
2902
|
catch (error) {
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
throw error;
|
|
2903
|
+
normalizeSymlinkResolutionError(error);
|
|
2904
|
+
const linkTarget = await promises_namespaceObject.readlink(symlinkPath);
|
|
2905
|
+
return resolvePathViaExistingAncestor(external_node_path_namespaceObject.resolve(external_node_path_namespaceObject.dirname(symlinkPath), linkTarget));
|
|
2612
2906
|
}
|
|
2613
2907
|
}
|
|
2614
|
-
|
|
2615
|
-
const normalized = external_node_path_namespaceObject.resolve(targetPath);
|
|
2616
|
-
let cursor = normalized;
|
|
2617
|
-
const missingSuffix = [];
|
|
2618
|
-
while (!isFilesystemRoot(cursor) && !(await root_path_existing_pathExists(cursor))) {
|
|
2619
|
-
missingSuffix.unshift(external_node_path_namespaceObject.basename(cursor));
|
|
2620
|
-
const parent = external_node_path_namespaceObject.dirname(cursor);
|
|
2621
|
-
if (parent === cursor) {
|
|
2622
|
-
break;
|
|
2623
|
-
}
|
|
2624
|
-
cursor = parent;
|
|
2625
|
-
}
|
|
2626
|
-
if (!(await root_path_existing_pathExists(cursor))) {
|
|
2627
|
-
return normalized;
|
|
2628
|
-
}
|
|
2908
|
+
function root_path_symlink_resolveSymlinkHopPathSync(symlinkPath) {
|
|
2629
2909
|
try {
|
|
2630
|
-
|
|
2631
|
-
return missingSuffix.length === 0
|
|
2632
|
-
? resolvedAncestor
|
|
2633
|
-
: external_node_path_namespaceObject.resolve(resolvedAncestor, ...missingSuffix);
|
|
2910
|
+
return path.resolve(fs.realpathSync(symlinkPath));
|
|
2634
2911
|
}
|
|
2635
|
-
catch {
|
|
2636
|
-
|
|
2912
|
+
catch (error) {
|
|
2913
|
+
normalizeSymlinkResolutionError(error);
|
|
2914
|
+
const linkTarget = fs.readlinkSync(symlinkPath);
|
|
2915
|
+
return resolvePathViaExistingAncestorSync(path.resolve(path.dirname(symlinkPath), linkTarget));
|
|
2637
2916
|
}
|
|
2638
2917
|
}
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2918
|
+
|
|
2919
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/safe-path-segment.js
|
|
2920
|
+
|
|
2921
|
+
const SAFE_PATH_SEGMENT_PATTERN = /^[A-Za-z0-9_-][A-Za-z0-9._-]*$/;
|
|
2922
|
+
const SAFE_DOT_PREFIX_PATH_SEGMENT_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
2923
|
+
// Windows treats "C:name" as relative to the drive's current directory even
|
|
2924
|
+
// though path.win32.isAbsolute() reports false.
|
|
2925
|
+
const DRIVE_RELATIVE_PREFIX = /^[A-Za-z]:(?![\\/])/;
|
|
2926
|
+
const HYPHEN_CHAR_CODE = 0x2d;
|
|
2927
|
+
function safe_path_segment_isDriveRelativePath(value) {
|
|
2928
|
+
return DRIVE_RELATIVE_PREFIX.test(value);
|
|
2929
|
+
}
|
|
2930
|
+
function assertNoDriveRelativePathSegments(value, label) {
|
|
2931
|
+
if (value.split("/").some(safe_path_segment_isDriveRelativePath)) {
|
|
2932
|
+
throw new errors_FsSafeError("invalid-path", `${label} must not contain a drive letter`);
|
|
2650
2933
|
}
|
|
2651
|
-
|
|
2652
|
-
|
|
2934
|
+
return value;
|
|
2935
|
+
}
|
|
2936
|
+
function trimHyphenEdges(value) {
|
|
2937
|
+
let start = 0;
|
|
2938
|
+
let end = value.length;
|
|
2939
|
+
while (start < end && value.charCodeAt(start) === HYPHEN_CHAR_CODE) {
|
|
2940
|
+
start += 1;
|
|
2653
2941
|
}
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
return missingSuffix.length === 0
|
|
2657
|
-
? resolvedAncestor
|
|
2658
|
-
: path.resolve(resolvedAncestor, ...missingSuffix);
|
|
2942
|
+
while (end > start && value.charCodeAt(end - 1) === HYPHEN_CHAR_CODE) {
|
|
2943
|
+
end -= 1;
|
|
2659
2944
|
}
|
|
2660
|
-
|
|
2661
|
-
|
|
2945
|
+
return start === 0 && end === value.length ? value : value.slice(start, end);
|
|
2946
|
+
}
|
|
2947
|
+
function isSafePathSegment(segment, options = {}) {
|
|
2948
|
+
return (segment !== "" &&
|
|
2949
|
+
segment !== "." &&
|
|
2950
|
+
segment !== ".." &&
|
|
2951
|
+
!segment.includes("/") &&
|
|
2952
|
+
!segment.includes("\\") &&
|
|
2953
|
+
!segment.includes("\0") &&
|
|
2954
|
+
(options.allowDotPrefix === true || !segment.startsWith(".")) &&
|
|
2955
|
+
(options.allowDotPrefix === true
|
|
2956
|
+
? SAFE_DOT_PREFIX_PATH_SEGMENT_PATTERN.test(segment)
|
|
2957
|
+
: SAFE_PATH_SEGMENT_PATTERN.test(segment)));
|
|
2958
|
+
}
|
|
2959
|
+
function assertSafePathSegment(segment, options = {}) {
|
|
2960
|
+
// Validate the exact value callers will later join into paths; trimming here
|
|
2961
|
+
// would let whitespace-padded ids pass and then be used verbatim.
|
|
2962
|
+
if (!isSafePathSegment(segment, options)) {
|
|
2963
|
+
throw new FsSafeError("invalid-path", `${options.label ?? "path segment"} must be a safe path segment`);
|
|
2964
|
+
}
|
|
2965
|
+
return segment;
|
|
2966
|
+
}
|
|
2967
|
+
function sanitizeSafePathSegment(value, fallback, options = {}) {
|
|
2968
|
+
const sanitized = value
|
|
2969
|
+
.trim()
|
|
2970
|
+
.replace(/[\\/]+/g, "-")
|
|
2971
|
+
.replace(/\0/g, "")
|
|
2972
|
+
.replace(/[^A-Za-z0-9._-]+/g, "-");
|
|
2973
|
+
const trimmed = trimHyphenEdges(sanitized);
|
|
2974
|
+
if (isSafePathSegment(trimmed, options)) {
|
|
2975
|
+
return trimmed;
|
|
2976
|
+
}
|
|
2977
|
+
return assertSafePathSegment(fallback, { ...options, label: "fallback path segment" });
|
|
2978
|
+
}
|
|
2979
|
+
function assertSafePathPrefix(prefix, options = {}) {
|
|
2980
|
+
// Prefixes are often derived from safe filenames. Normalize harmless
|
|
2981
|
+
// filename characters first, but still reject real path-control bytes.
|
|
2982
|
+
if (prefix.includes("/") || prefix.includes("\\") || prefix.includes("\0")) {
|
|
2983
|
+
return assertSafePathSegment(prefix, {
|
|
2984
|
+
allowDotPrefix: true,
|
|
2985
|
+
...options,
|
|
2986
|
+
label: options.label ?? "path prefix",
|
|
2987
|
+
});
|
|
2662
2988
|
}
|
|
2989
|
+
return assertSafePathSegment(prefix.replace(/[^A-Za-z0-9._-]+/g, "-"), {
|
|
2990
|
+
allowDotPrefix: true,
|
|
2991
|
+
...options,
|
|
2992
|
+
label: options.label ?? "path prefix",
|
|
2993
|
+
});
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2996
|
+
;// CONCATENATED MODULE: external "node:os"
|
|
2997
|
+
const external_node_os_namespaceObject = __rspack_createRequire_require("node:os");
|
|
2998
|
+
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/short-path.js
|
|
2999
|
+
|
|
3000
|
+
|
|
3001
|
+
function shortPath(value) {
|
|
3002
|
+
const home = external_node_os_namespaceObject.homedir();
|
|
3003
|
+
const shortened = value.startsWith(home) ? `~${value.slice(home.length)}` : value;
|
|
3004
|
+
return formatErrorDetail(shortened);
|
|
2663
3005
|
}
|
|
2664
3006
|
|
|
2665
3007
|
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/root-path.js
|
|
@@ -2670,6 +3012,10 @@ function root_path_existing_resolvePathViaExistingAncestorSync(targetPath) {
|
|
|
2670
3012
|
|
|
2671
3013
|
|
|
2672
3014
|
|
|
3015
|
+
|
|
3016
|
+
|
|
3017
|
+
|
|
3018
|
+
|
|
2673
3019
|
const ROOT_PATH_ALIAS_POLICIES = {
|
|
2674
3020
|
strict: Object.freeze({
|
|
2675
3021
|
allowFinalSymlinkForUnlink: false,
|
|
@@ -2681,11 +3027,20 @@ const ROOT_PATH_ALIAS_POLICIES = {
|
|
|
2681
3027
|
}),
|
|
2682
3028
|
};
|
|
2683
3029
|
async function resolveRootPath(params) {
|
|
3030
|
+
try {
|
|
3031
|
+
return await resolveRootPathInternal(params);
|
|
3032
|
+
}
|
|
3033
|
+
catch (error) {
|
|
3034
|
+
throw sanitizeRootPathError(error);
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
async function resolveRootPathInternal(params) {
|
|
3038
|
+
assertValidRootPathInputs(params);
|
|
2684
3039
|
const rootPath = external_node_path_namespaceObject.resolve(params.rootPath);
|
|
2685
3040
|
const absolutePath = external_node_path_namespaceObject.resolve(params.absolutePath);
|
|
2686
3041
|
const rootCanonicalPath = params.rootCanonicalPath
|
|
2687
3042
|
? external_node_path_namespaceObject.resolve(params.rootCanonicalPath)
|
|
2688
|
-
: await
|
|
3043
|
+
: await resolvePathViaExistingAncestor(rootPath);
|
|
2689
3044
|
const context = createBoundaryResolutionContext({
|
|
2690
3045
|
resolveParams: params,
|
|
2691
3046
|
rootPath,
|
|
@@ -2711,6 +3066,15 @@ async function resolveRootPath(params) {
|
|
|
2711
3066
|
});
|
|
2712
3067
|
}
|
|
2713
3068
|
function resolveRootPathSync(params) {
|
|
3069
|
+
try {
|
|
3070
|
+
return resolveRootPathSyncInternal(params);
|
|
3071
|
+
}
|
|
3072
|
+
catch (error) {
|
|
3073
|
+
throw sanitizeRootPathError(error);
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
function resolveRootPathSyncInternal(params) {
|
|
3077
|
+
assertValidRootPathInputs(params);
|
|
2714
3078
|
const rootPath = path.resolve(params.rootPath);
|
|
2715
3079
|
const absolutePath = path.resolve(params.absolutePath);
|
|
2716
3080
|
const rootCanonicalPath = params.rootCanonicalPath
|
|
@@ -2740,6 +3104,29 @@ function resolveRootPathSync(params) {
|
|
|
2740
3104
|
rootCanonicalPath: context.rootCanonicalPath,
|
|
2741
3105
|
});
|
|
2742
3106
|
}
|
|
3107
|
+
function sanitizeRootPathError(error) {
|
|
3108
|
+
if (error instanceof Error) {
|
|
3109
|
+
error.message = formatErrorDetail(error.message);
|
|
3110
|
+
}
|
|
3111
|
+
return error;
|
|
3112
|
+
}
|
|
3113
|
+
function assertValidRootPathInputs(params) {
|
|
3114
|
+
path_assertNoNulPathInput(params.rootPath, "root path contains a NUL byte");
|
|
3115
|
+
path_assertNoNulPathInput(params.absolutePath, "absolute path contains a NUL byte");
|
|
3116
|
+
assertNoEmbeddedDriveRelativeSegment(params.rootPath, "root path");
|
|
3117
|
+
assertNoEmbeddedDriveRelativeSegment(params.absolutePath, "absolute path");
|
|
3118
|
+
if (params.rootCanonicalPath !== undefined) {
|
|
3119
|
+
path_assertNoNulPathInput(params.rootCanonicalPath, "canonical root path contains a NUL byte");
|
|
3120
|
+
assertNoEmbeddedDriveRelativeSegment(params.rootCanonicalPath, "canonical root path");
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
function assertNoEmbeddedDriveRelativeSegment(filePath, label) {
|
|
3124
|
+
if (process.platform !== "win32") {
|
|
3125
|
+
return;
|
|
3126
|
+
}
|
|
3127
|
+
const root = external_node_path_namespaceObject.parse(filePath).root;
|
|
3128
|
+
assertNoDriveRelativePathSegments(filePath.slice(root.length).replaceAll("\\", "/"), label);
|
|
3129
|
+
}
|
|
2743
3130
|
function isPromiseLike(value) {
|
|
2744
3131
|
return Boolean(value &&
|
|
2745
3132
|
(typeof value === "object" || typeof value === "function") &&
|
|
@@ -2758,6 +3145,15 @@ function createLexicalTraversalState(params) {
|
|
|
2758
3145
|
preserveFinalSymlink: false,
|
|
2759
3146
|
};
|
|
2760
3147
|
}
|
|
3148
|
+
function createLexicalTraversalContext(params) {
|
|
3149
|
+
return {
|
|
3150
|
+
state: createLexicalTraversalState(params),
|
|
3151
|
+
resolveParams: params.params,
|
|
3152
|
+
rootPath: params.rootPath,
|
|
3153
|
+
rootCanonicalPath: params.rootCanonicalPath,
|
|
3154
|
+
absolutePath: params.absolutePath,
|
|
3155
|
+
};
|
|
3156
|
+
}
|
|
2761
3157
|
function splitTraversalSegments(value) {
|
|
2762
3158
|
return value
|
|
2763
3159
|
.split(process.platform === "win32" ? /[\\/]+/ : /\/+/)
|
|
@@ -2781,149 +3177,97 @@ function rawPathRelativeToRoot(rootPath, candidatePath) {
|
|
|
2781
3177
|
: candidatePrefix === rootWithSep;
|
|
2782
3178
|
return prefixMatches ? candidate.slice(rootWithSep.length) : undefined;
|
|
2783
3179
|
}
|
|
2784
|
-
function assertLexicalCursorInsideBoundary(
|
|
3180
|
+
function assertLexicalCursorInsideBoundary(context, candidatePath) {
|
|
2785
3181
|
assertInsideBoundary({
|
|
2786
|
-
boundaryLabel:
|
|
2787
|
-
rootCanonicalPath:
|
|
2788
|
-
candidatePath
|
|
2789
|
-
absolutePath:
|
|
3182
|
+
boundaryLabel: context.resolveParams.boundaryLabel,
|
|
3183
|
+
rootCanonicalPath: context.rootCanonicalPath,
|
|
3184
|
+
candidatePath,
|
|
3185
|
+
absolutePath: context.absolutePath,
|
|
2790
3186
|
});
|
|
2791
3187
|
}
|
|
2792
|
-
function applyMissingSuffixToCanonicalCursor(
|
|
2793
|
-
const missingSuffix =
|
|
3188
|
+
function applyMissingSuffixToCanonicalCursor(context, missingFromIndex) {
|
|
3189
|
+
const missingSuffix = context.state.segments.slice(missingFromIndex);
|
|
2794
3190
|
for (const segment of missingSuffix) {
|
|
2795
|
-
advanceCanonicalCursorForSegment(
|
|
2796
|
-
state: params.state,
|
|
2797
|
-
segment,
|
|
2798
|
-
rootCanonicalPath: params.rootCanonicalPath,
|
|
2799
|
-
params: params.params,
|
|
2800
|
-
absolutePath: params.absolutePath,
|
|
2801
|
-
});
|
|
3191
|
+
advanceCanonicalCursorForSegment(context, segment);
|
|
2802
3192
|
}
|
|
2803
3193
|
}
|
|
2804
|
-
function advanceCanonicalCursorForSegment(
|
|
2805
|
-
|
|
2806
|
-
assertLexicalCursorInsideBoundary(
|
|
2807
|
-
params: params.params,
|
|
2808
|
-
rootCanonicalPath: params.rootCanonicalPath,
|
|
2809
|
-
candidatePath: params.state.canonicalCursor,
|
|
2810
|
-
absolutePath: params.absolutePath,
|
|
2811
|
-
});
|
|
3194
|
+
function advanceCanonicalCursorForSegment(context, segment) {
|
|
3195
|
+
context.state.canonicalCursor = external_node_path_namespaceObject.resolve(context.state.canonicalCursor, segment);
|
|
3196
|
+
assertLexicalCursorInsideBoundary(context, context.state.canonicalCursor);
|
|
2812
3197
|
}
|
|
2813
|
-
function finalizeLexicalResolution(
|
|
2814
|
-
assertLexicalCursorInsideBoundary(
|
|
2815
|
-
params: params.params,
|
|
2816
|
-
rootCanonicalPath: params.rootCanonicalPath,
|
|
2817
|
-
candidatePath: params.state.canonicalCursor,
|
|
2818
|
-
absolutePath: params.absolutePath,
|
|
2819
|
-
});
|
|
3198
|
+
function finalizeLexicalResolution(context, kind) {
|
|
3199
|
+
assertLexicalCursorInsideBoundary(context, context.state.canonicalCursor);
|
|
2820
3200
|
return buildResolvedRootPath({
|
|
2821
|
-
absolutePath:
|
|
2822
|
-
canonicalPath:
|
|
2823
|
-
rootPath:
|
|
2824
|
-
rootCanonicalPath:
|
|
2825
|
-
kind
|
|
3201
|
+
absolutePath: context.absolutePath,
|
|
3202
|
+
canonicalPath: context.state.canonicalCursor,
|
|
3203
|
+
rootPath: context.rootPath,
|
|
3204
|
+
rootCanonicalPath: context.rootCanonicalPath,
|
|
3205
|
+
kind,
|
|
2826
3206
|
});
|
|
2827
3207
|
}
|
|
2828
|
-
function handleLexicalLstatFailure(
|
|
2829
|
-
if (!path_isNotFoundPathError(
|
|
3208
|
+
function handleLexicalLstatFailure(context, error, missingFromIndex) {
|
|
3209
|
+
if (!path_isNotFoundPathError(error)) {
|
|
2830
3210
|
return false;
|
|
2831
3211
|
}
|
|
2832
|
-
applyMissingSuffixToCanonicalCursor(
|
|
2833
|
-
state: params.state,
|
|
2834
|
-
missingFromIndex: params.missingFromIndex,
|
|
2835
|
-
rootCanonicalPath: params.rootCanonicalPath,
|
|
2836
|
-
params: params.resolveParams,
|
|
2837
|
-
absolutePath: params.absolutePath,
|
|
2838
|
-
});
|
|
3212
|
+
applyMissingSuffixToCanonicalCursor(context, missingFromIndex);
|
|
2839
3213
|
return true;
|
|
2840
3214
|
}
|
|
2841
|
-
function handleLexicalStatReadFailure(
|
|
2842
|
-
if (handleLexicalLstatFailure({
|
|
2843
|
-
error: params.error,
|
|
2844
|
-
state: params.state,
|
|
2845
|
-
missingFromIndex: params.missingFromIndex,
|
|
2846
|
-
rootCanonicalPath: params.rootCanonicalPath,
|
|
2847
|
-
resolveParams: params.resolveParams,
|
|
2848
|
-
absolutePath: params.absolutePath,
|
|
2849
|
-
})) {
|
|
3215
|
+
function handleLexicalStatReadFailure(context, error, missingFromIndex) {
|
|
3216
|
+
if (handleLexicalLstatFailure(context, error, missingFromIndex)) {
|
|
2850
3217
|
return null;
|
|
2851
3218
|
}
|
|
2852
|
-
throw
|
|
3219
|
+
throw error;
|
|
2853
3220
|
}
|
|
2854
|
-
function handleLexicalStatDisposition(params) {
|
|
3221
|
+
function handleLexicalStatDisposition(context, params) {
|
|
2855
3222
|
if (!params.isSymbolicLink) {
|
|
2856
|
-
advanceCanonicalCursorForSegment(
|
|
2857
|
-
state: params.state,
|
|
2858
|
-
segment: params.segment,
|
|
2859
|
-
rootCanonicalPath: params.rootCanonicalPath,
|
|
2860
|
-
params: params.resolveParams,
|
|
2861
|
-
absolutePath: params.absolutePath,
|
|
2862
|
-
});
|
|
3223
|
+
advanceCanonicalCursorForSegment(context, params.segment);
|
|
2863
3224
|
return "continue";
|
|
2864
3225
|
}
|
|
2865
|
-
if (
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
params: params.resolveParams,
|
|
2872
|
-
absolutePath: params.absolutePath,
|
|
2873
|
-
});
|
|
3226
|
+
if (context.resolveParams.rejectSymlinks === true && params.isLast) {
|
|
3227
|
+
throw new errors_FsSafeError("symlink", "symlink path component not allowed");
|
|
3228
|
+
}
|
|
3229
|
+
if (context.state.allowFinalSymlink && params.isLast) {
|
|
3230
|
+
context.state.preserveFinalSymlink = true;
|
|
3231
|
+
advanceCanonicalCursorForSegment(context, params.segment);
|
|
2874
3232
|
return "break";
|
|
2875
3233
|
}
|
|
2876
3234
|
return "resolve-link";
|
|
2877
3235
|
}
|
|
2878
|
-
function applyResolvedSymlinkHop(
|
|
2879
|
-
if (!path_isPathInside(
|
|
3236
|
+
function applyResolvedSymlinkHop(context, linkCanonical) {
|
|
3237
|
+
if (!path_isPathInside(context.rootCanonicalPath, linkCanonical)) {
|
|
2880
3238
|
throw symlinkEscapeError({
|
|
2881
|
-
boundaryLabel:
|
|
2882
|
-
rootCanonicalPath:
|
|
2883
|
-
symlinkPath:
|
|
3239
|
+
boundaryLabel: context.resolveParams.boundaryLabel,
|
|
3240
|
+
rootCanonicalPath: context.rootCanonicalPath,
|
|
3241
|
+
symlinkPath: context.state.lexicalCursor,
|
|
2884
3242
|
});
|
|
2885
3243
|
}
|
|
2886
|
-
|
|
2887
|
-
|
|
3244
|
+
context.state.canonicalCursor = linkCanonical;
|
|
3245
|
+
context.state.lexicalCursor = linkCanonical;
|
|
2888
3246
|
}
|
|
2889
|
-
function readLexicalStat(params) {
|
|
3247
|
+
function readLexicalStat(context, params) {
|
|
2890
3248
|
try {
|
|
2891
|
-
const stat = params.read(
|
|
3249
|
+
const stat = params.read(context.state.lexicalCursor);
|
|
2892
3250
|
if (isPromiseLike(stat)) {
|
|
2893
|
-
return Promise.resolve(stat).catch((error) => handleLexicalStatReadFailure(
|
|
3251
|
+
return Promise.resolve(stat).catch((error) => handleLexicalStatReadFailure(context, error, params.missingFromIndex));
|
|
2894
3252
|
}
|
|
2895
3253
|
return stat;
|
|
2896
3254
|
}
|
|
2897
3255
|
catch (error) {
|
|
2898
|
-
return handleLexicalStatReadFailure(
|
|
3256
|
+
return handleLexicalStatReadFailure(context, error, params.missingFromIndex);
|
|
2899
3257
|
}
|
|
2900
3258
|
}
|
|
2901
|
-
function resolveAndApplySymlinkHop(params) {
|
|
2902
|
-
const linkCanonical = params.resolveLinkCanonical(
|
|
3259
|
+
function resolveAndApplySymlinkHop(context, params) {
|
|
3260
|
+
const linkCanonical = params.resolveLinkCanonical(context.state.lexicalCursor);
|
|
2903
3261
|
if (isPromiseLike(linkCanonical)) {
|
|
2904
|
-
return Promise.resolve(linkCanonical).then((value) =>
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
rootCanonicalPath: params.rootCanonicalPath,
|
|
2908
|
-
boundaryLabel: params.boundaryLabel,
|
|
2909
|
-
}));
|
|
3262
|
+
return Promise.resolve(linkCanonical).then((value) => {
|
|
3263
|
+
applyResolvedSymlinkHop(context, value);
|
|
3264
|
+
});
|
|
2910
3265
|
}
|
|
2911
|
-
applyResolvedSymlinkHop(
|
|
2912
|
-
state: params.state,
|
|
2913
|
-
linkCanonical,
|
|
2914
|
-
rootCanonicalPath: params.rootCanonicalPath,
|
|
2915
|
-
boundaryLabel: params.boundaryLabel,
|
|
2916
|
-
});
|
|
3266
|
+
applyResolvedSymlinkHop(context, linkCanonical);
|
|
2917
3267
|
}
|
|
2918
|
-
function applyParentTraversalStep(
|
|
2919
|
-
|
|
2920
|
-
advanceCanonicalCursorForSegment(
|
|
2921
|
-
state: params.state,
|
|
2922
|
-
segment: "..",
|
|
2923
|
-
rootCanonicalPath: params.rootCanonicalPath,
|
|
2924
|
-
params: params.resolveParams,
|
|
2925
|
-
absolutePath: params.absolutePath,
|
|
2926
|
-
});
|
|
3268
|
+
function applyParentTraversalStep(context) {
|
|
3269
|
+
context.state.lexicalCursor = external_node_path_namespaceObject.resolve(context.state.lexicalCursor, "..");
|
|
3270
|
+
advanceCanonicalCursorForSegment(context, "..");
|
|
2927
3271
|
}
|
|
2928
3272
|
function* iterateLexicalTraversal(state) {
|
|
2929
3273
|
for (let idx = 0; idx < state.segments.length; idx += 1) {
|
|
@@ -2933,32 +3277,22 @@ function* iterateLexicalTraversal(state) {
|
|
|
2933
3277
|
}
|
|
2934
3278
|
}
|
|
2935
3279
|
async function resolveRootPathLexicalAsync(params) {
|
|
2936
|
-
const
|
|
2937
|
-
const
|
|
2938
|
-
state,
|
|
2939
|
-
rootCanonicalPath: params.rootCanonicalPath,
|
|
2940
|
-
resolveParams: params.params,
|
|
2941
|
-
absolutePath: params.absolutePath,
|
|
2942
|
-
};
|
|
3280
|
+
const context = createLexicalTraversalContext(params);
|
|
3281
|
+
const { state } = context;
|
|
2943
3282
|
for (const { idx, segment, isLast } of iterateLexicalTraversal(state)) {
|
|
2944
3283
|
if (segment === "..") {
|
|
2945
|
-
applyParentTraversalStep(
|
|
2946
|
-
...sharedStepParams,
|
|
2947
|
-
resolveParams: params.params,
|
|
2948
|
-
});
|
|
3284
|
+
applyParentTraversalStep(context);
|
|
2949
3285
|
continue;
|
|
2950
3286
|
}
|
|
2951
3287
|
state.lexicalCursor = external_node_path_namespaceObject.join(state.lexicalCursor, segment);
|
|
2952
|
-
const stat = await readLexicalStat({
|
|
2953
|
-
...sharedStepParams,
|
|
3288
|
+
const stat = await readLexicalStat(context, {
|
|
2954
3289
|
missingFromIndex: idx,
|
|
2955
3290
|
read: (cursor) => promises_namespaceObject.lstat(cursor),
|
|
2956
3291
|
});
|
|
2957
3292
|
if (!stat) {
|
|
2958
3293
|
break;
|
|
2959
3294
|
}
|
|
2960
|
-
const disposition = handleLexicalStatDisposition({
|
|
2961
|
-
...sharedStepParams,
|
|
3295
|
+
const disposition = handleLexicalStatDisposition(context, {
|
|
2962
3296
|
isSymbolicLink: stat.isSymbolicLink(),
|
|
2963
3297
|
segment,
|
|
2964
3298
|
isLast,
|
|
@@ -2969,41 +3303,29 @@ async function resolveRootPathLexicalAsync(params) {
|
|
|
2969
3303
|
if (disposition === "break") {
|
|
2970
3304
|
break;
|
|
2971
3305
|
}
|
|
2972
|
-
await resolveAndApplySymlinkHop({
|
|
2973
|
-
state,
|
|
2974
|
-
rootCanonicalPath: params.rootCanonicalPath,
|
|
2975
|
-
boundaryLabel: params.params.boundaryLabel,
|
|
3306
|
+
await resolveAndApplySymlinkHop(context, {
|
|
2976
3307
|
resolveLinkCanonical: (cursor) => resolveSymlinkHopPath(cursor),
|
|
2977
3308
|
});
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
});
|
|
3309
|
+
if (context.resolveParams.rejectSymlinks === true) {
|
|
3310
|
+
throw new errors_FsSafeError("symlink", "symlink path component not allowed");
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
const kind = await getPathKind(state.canonicalCursor, state.preserveFinalSymlink);
|
|
3314
|
+
return finalizeLexicalResolution(context, kind);
|
|
2985
3315
|
}
|
|
2986
3316
|
function resolveRootPathLexicalSync(params) {
|
|
2987
|
-
const
|
|
3317
|
+
const context = createLexicalTraversalContext(params);
|
|
3318
|
+
const { state } = context;
|
|
2988
3319
|
for (let idx = 0; idx < state.segments.length; idx += 1) {
|
|
2989
3320
|
const segment = state.segments[idx] ?? "";
|
|
2990
3321
|
const isLast = idx === state.segments.length - 1;
|
|
2991
3322
|
if (segment === "..") {
|
|
2992
|
-
applyParentTraversalStep(
|
|
2993
|
-
state,
|
|
2994
|
-
rootCanonicalPath: params.rootCanonicalPath,
|
|
2995
|
-
resolveParams: params.params,
|
|
2996
|
-
absolutePath: params.absolutePath,
|
|
2997
|
-
});
|
|
3323
|
+
applyParentTraversalStep(context);
|
|
2998
3324
|
continue;
|
|
2999
3325
|
}
|
|
3000
3326
|
state.lexicalCursor = path.join(state.lexicalCursor, segment);
|
|
3001
|
-
const maybeStat = readLexicalStat({
|
|
3002
|
-
state,
|
|
3327
|
+
const maybeStat = readLexicalStat(context, {
|
|
3003
3328
|
missingFromIndex: idx,
|
|
3004
|
-
rootCanonicalPath: params.rootCanonicalPath,
|
|
3005
|
-
resolveParams: params.params,
|
|
3006
|
-
absolutePath: params.absolutePath,
|
|
3007
3329
|
read: (cursor) => fs.lstatSync(cursor),
|
|
3008
3330
|
});
|
|
3009
3331
|
if (isPromiseLike(maybeStat)) {
|
|
@@ -3013,14 +3335,10 @@ function resolveRootPathLexicalSync(params) {
|
|
|
3013
3335
|
if (!stat) {
|
|
3014
3336
|
break;
|
|
3015
3337
|
}
|
|
3016
|
-
const disposition = handleLexicalStatDisposition({
|
|
3017
|
-
state,
|
|
3338
|
+
const disposition = handleLexicalStatDisposition(context, {
|
|
3018
3339
|
isSymbolicLink: stat.isSymbolicLink(),
|
|
3019
3340
|
segment,
|
|
3020
3341
|
isLast,
|
|
3021
|
-
rootCanonicalPath: params.rootCanonicalPath,
|
|
3022
|
-
resolveParams: params.params,
|
|
3023
|
-
absolutePath: params.absolutePath,
|
|
3024
3342
|
});
|
|
3025
3343
|
if (disposition === "continue") {
|
|
3026
3344
|
continue;
|
|
@@ -3028,22 +3346,18 @@ function resolveRootPathLexicalSync(params) {
|
|
|
3028
3346
|
if (disposition === "break") {
|
|
3029
3347
|
break;
|
|
3030
3348
|
}
|
|
3031
|
-
const maybeApplied = resolveAndApplySymlinkHop({
|
|
3032
|
-
state,
|
|
3033
|
-
rootCanonicalPath: params.rootCanonicalPath,
|
|
3034
|
-
boundaryLabel: params.params.boundaryLabel,
|
|
3349
|
+
const maybeApplied = resolveAndApplySymlinkHop(context, {
|
|
3035
3350
|
resolveLinkCanonical: (cursor) => resolveSymlinkHopPathSync(cursor),
|
|
3036
3351
|
});
|
|
3037
3352
|
if (isPromiseLike(maybeApplied)) {
|
|
3038
3353
|
throw new Error("Unexpected async symlink resolution");
|
|
3039
3354
|
}
|
|
3355
|
+
if (context.resolveParams.rejectSymlinks === true) {
|
|
3356
|
+
throw new FsSafeError("symlink", "symlink path component not allowed");
|
|
3357
|
+
}
|
|
3040
3358
|
}
|
|
3041
3359
|
const kind = getPathKindSync(state.canonicalCursor, state.preserveFinalSymlink);
|
|
3042
|
-
return finalizeLexicalResolution(
|
|
3043
|
-
...params,
|
|
3044
|
-
state,
|
|
3045
|
-
kind,
|
|
3046
|
-
});
|
|
3360
|
+
return finalizeLexicalResolution(context, kind);
|
|
3047
3361
|
}
|
|
3048
3362
|
function resolveCanonicalOutsideLexicalPath(params) {
|
|
3049
3363
|
return params.outsideLexicalCanonicalPath ?? params.absolutePath;
|
|
@@ -3107,7 +3421,7 @@ async function resolveOutsideLexicalCanonicalPathAsync(params) {
|
|
|
3107
3421
|
if (path_isPathInside(params.rootPath, params.absolutePath)) {
|
|
3108
3422
|
return undefined;
|
|
3109
3423
|
}
|
|
3110
|
-
return await
|
|
3424
|
+
return await resolvePathViaExistingAncestor(params.absolutePath);
|
|
3111
3425
|
}
|
|
3112
3426
|
function resolveOutsideLexicalCanonicalPathSync(params) {
|
|
3113
3427
|
if (isPathInside(params.rootPath, params.absolutePath)) {
|
|
@@ -3214,39 +3528,6 @@ function pathEscapeError(params) {
|
|
|
3214
3528
|
function symlinkEscapeError(params) {
|
|
3215
3529
|
return new Error(`Symlink escapes ${params.boundaryLabel} (${shortPath(params.rootCanonicalPath)}): ${shortPath(params.symlinkPath)}`);
|
|
3216
3530
|
}
|
|
3217
|
-
function shortPath(value) {
|
|
3218
|
-
const home = external_node_os_namespaceObject.homedir();
|
|
3219
|
-
if (value.startsWith(home)) {
|
|
3220
|
-
return `~${value.slice(home.length)}`;
|
|
3221
|
-
}
|
|
3222
|
-
return value;
|
|
3223
|
-
}
|
|
3224
|
-
async function resolveSymlinkHopPath(symlinkPath) {
|
|
3225
|
-
try {
|
|
3226
|
-
return external_node_path_namespaceObject.resolve(await promises_namespaceObject.realpath(symlinkPath));
|
|
3227
|
-
}
|
|
3228
|
-
catch (error) {
|
|
3229
|
-
if (!path_isNotFoundPathError(error)) {
|
|
3230
|
-
throw error;
|
|
3231
|
-
}
|
|
3232
|
-
const linkTarget = await promises_namespaceObject.readlink(symlinkPath);
|
|
3233
|
-
const linkAbsolute = external_node_path_namespaceObject.resolve(external_node_path_namespaceObject.dirname(symlinkPath), linkTarget);
|
|
3234
|
-
return root_path_existing_resolvePathViaExistingAncestor(linkAbsolute);
|
|
3235
|
-
}
|
|
3236
|
-
}
|
|
3237
|
-
function resolveSymlinkHopPathSync(symlinkPath) {
|
|
3238
|
-
try {
|
|
3239
|
-
return path.resolve(fs.realpathSync(symlinkPath));
|
|
3240
|
-
}
|
|
3241
|
-
catch (error) {
|
|
3242
|
-
if (!isNotFoundPathError(error)) {
|
|
3243
|
-
throw error;
|
|
3244
|
-
}
|
|
3245
|
-
const linkTarget = fs.readlinkSync(symlinkPath);
|
|
3246
|
-
const linkAbsolute = path.resolve(path.dirname(symlinkPath), linkTarget);
|
|
3247
|
-
return resolvePathViaExistingAncestorSync(linkAbsolute);
|
|
3248
|
-
}
|
|
3249
|
-
}
|
|
3250
3531
|
|
|
3251
3532
|
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/path-policy.js
|
|
3252
3533
|
|
|
@@ -3290,14 +3571,8 @@ async function assertNoHardlinkedFinalPath(params) {
|
|
|
3290
3571
|
return;
|
|
3291
3572
|
}
|
|
3292
3573
|
if (stat.nlink > 1) {
|
|
3293
|
-
throw new Error(`Hardlinked path is not allowed under ${params.boundaryLabel} (${
|
|
3294
|
-
}
|
|
3295
|
-
}
|
|
3296
|
-
function path_policy_shortPath(value) {
|
|
3297
|
-
if (value.startsWith(external_node_os_namespaceObject.homedir())) {
|
|
3298
|
-
return `~${value.slice(external_node_os_namespaceObject.homedir().length)}`;
|
|
3574
|
+
throw new Error(`Hardlinked path is not allowed under ${params.boundaryLabel} (${shortPath(params.root)}): ${shortPath(params.filePath)}`);
|
|
3299
3575
|
}
|
|
3300
|
-
return value;
|
|
3301
3576
|
}
|
|
3302
3577
|
|
|
3303
3578
|
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/string-coerce.js
|
|
@@ -3379,6 +3654,10 @@ function hasNonEmptyString(value) {
|
|
|
3379
3654
|
|
|
3380
3655
|
|
|
3381
3656
|
const ENCODED_FILE_URL_SEPARATOR_RE = /%(?:2f|5c)/i;
|
|
3657
|
+
const FILE_URL_PREFIX_RE = /^file:\/\//i;
|
|
3658
|
+
function isFileUrl(input) {
|
|
3659
|
+
return FILE_URL_PREFIX_RE.test(input);
|
|
3660
|
+
}
|
|
3382
3661
|
function isLocalFileUrlHost(hostname) {
|
|
3383
3662
|
const normalized = normalizeLowercaseStringOrEmpty(hostname);
|
|
3384
3663
|
return normalized === "" || normalized === "localhost";
|
|
@@ -3409,7 +3688,7 @@ function assertNoWindowsNetworkPath(filePath, label = "Path") {
|
|
|
3409
3688
|
throw new Error(`${label} cannot use Windows network paths: ${filePath}`);
|
|
3410
3689
|
}
|
|
3411
3690
|
}
|
|
3412
|
-
function safeFileURLToPath(fileUrl) {
|
|
3691
|
+
function safeFileURLToPath(fileUrl, platform = process.platform) {
|
|
3413
3692
|
let parsed;
|
|
3414
3693
|
try {
|
|
3415
3694
|
parsed = new external_node_url_namespaceObject.URL(fileUrl);
|
|
@@ -3426,13 +3705,15 @@ function safeFileURLToPath(fileUrl) {
|
|
|
3426
3705
|
if (hasEncodedFileUrlSeparator(parsed.pathname)) {
|
|
3427
3706
|
throw new Error(`file:// URLs cannot encode path separators: ${fileUrl}`);
|
|
3428
3707
|
}
|
|
3429
|
-
const filePath = (0,external_node_url_namespaceObject.fileURLToPath)(parsed);
|
|
3430
|
-
|
|
3708
|
+
const filePath = (0,external_node_url_namespaceObject.fileURLToPath)(parsed, { windows: platform === "win32" });
|
|
3709
|
+
if (isWindowsNetworkPath(filePath, platform)) {
|
|
3710
|
+
throw new Error(`Local file URL cannot use Windows network paths: ${filePath}`);
|
|
3711
|
+
}
|
|
3431
3712
|
return filePath;
|
|
3432
3713
|
}
|
|
3433
|
-
function trySafeFileURLToPath(fileUrl) {
|
|
3714
|
+
function trySafeFileURLToPath(fileUrl, platform = process.platform) {
|
|
3434
3715
|
try {
|
|
3435
|
-
return safeFileURLToPath(fileUrl);
|
|
3716
|
+
return safeFileURLToPath(fileUrl, platform);
|
|
3436
3717
|
}
|
|
3437
3718
|
catch {
|
|
3438
3719
|
return undefined;
|
|
@@ -3442,7 +3723,7 @@ function basenameFromMediaSource(source) {
|
|
|
3442
3723
|
if (!source) {
|
|
3443
3724
|
return undefined;
|
|
3444
3725
|
}
|
|
3445
|
-
if (source
|
|
3726
|
+
if (isFileUrl(source)) {
|
|
3446
3727
|
const filePath = trySafeFileURLToPath(source);
|
|
3447
3728
|
return filePath ? path.basename(filePath) || undefined : undefined;
|
|
3448
3729
|
}
|
|
@@ -3527,11 +3808,11 @@ function trimTrailingWindowsIgnoredChars(value) {
|
|
|
3527
3808
|
}
|
|
3528
3809
|
return end === value.length ? value : value.slice(0, end);
|
|
3529
3810
|
}
|
|
3530
|
-
function candidateReadPaths(filePath) {
|
|
3531
|
-
if (!filePath
|
|
3811
|
+
function candidateReadPaths(filePath, platform) {
|
|
3812
|
+
if (!isFileUrl(filePath)) {
|
|
3532
3813
|
return [filePath];
|
|
3533
3814
|
}
|
|
3534
|
-
const parsed = trySafeFileURLToPath(filePath);
|
|
3815
|
+
const parsed = trySafeFileURLToPath(filePath, platform);
|
|
3535
3816
|
return parsed === undefined ? [filePath] : [filePath, parsed];
|
|
3536
3817
|
}
|
|
3537
3818
|
function normalizePosixPath(filePath, cwd) {
|
|
@@ -3574,7 +3855,7 @@ function matchWindowsDeviceReadPath(filePath) {
|
|
|
3574
3855
|
}
|
|
3575
3856
|
function matchUnsafeDeviceReadPath(filePath, options = {}) {
|
|
3576
3857
|
const platform = options.platform ?? process.platform;
|
|
3577
|
-
for (const candidate of candidateReadPaths(filePath)) {
|
|
3858
|
+
for (const candidate of candidateReadPaths(filePath, platform)) {
|
|
3578
3859
|
const match = platform === "win32"
|
|
3579
3860
|
? matchWindowsDeviceReadPath(candidate)
|
|
3580
3861
|
: matchPosixDeviceReadPath(candidate, options.cwd);
|
|
@@ -3611,23 +3892,6 @@ async function read_opened_file_readOpenedFileSafely(params) {
|
|
|
3611
3892
|
};
|
|
3612
3893
|
}
|
|
3613
3894
|
|
|
3614
|
-
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/path-stat.js
|
|
3615
|
-
function pathStatFromStats(stat) {
|
|
3616
|
-
return {
|
|
3617
|
-
dev: Number(stat.dev),
|
|
3618
|
-
gid: Number(stat.gid),
|
|
3619
|
-
ino: Number(stat.ino),
|
|
3620
|
-
isDirectory: stat.isDirectory(),
|
|
3621
|
-
isFile: stat.isFile(),
|
|
3622
|
-
isSymbolicLink: stat.isSymbolicLink(),
|
|
3623
|
-
mode: stat.mode,
|
|
3624
|
-
mtimeMs: stat.mtimeMs,
|
|
3625
|
-
nlink: stat.nlink,
|
|
3626
|
-
size: stat.size,
|
|
3627
|
-
uid: stat.uid,
|
|
3628
|
-
};
|
|
3629
|
-
}
|
|
3630
|
-
|
|
3631
3895
|
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/home-dir.js
|
|
3632
3896
|
|
|
3633
3897
|
|
|
@@ -3750,10 +4014,19 @@ function resolveOsHomeRelativePath(input, opts) {
|
|
|
3750
4014
|
|
|
3751
4015
|
|
|
3752
4016
|
|
|
4017
|
+
|
|
4018
|
+
|
|
4019
|
+
|
|
3753
4020
|
const ensureTrailingSep = (value) => value.endsWith(external_node_path_namespaceObject.sep) ? value : value + external_node_path_namespaceObject.sep;
|
|
3754
4021
|
function assertValidRootRelativePath(relativePath) {
|
|
3755
4022
|
path_assertNoNulPathInput(relativePath, "relative path contains a NUL byte");
|
|
3756
4023
|
}
|
|
4024
|
+
function assertValidRootDestinationPath(relativePath) {
|
|
4025
|
+
assertValidRootRelativePath(relativePath);
|
|
4026
|
+
if (safe_path_segment_isDriveRelativePath(relativePath)) {
|
|
4027
|
+
throw new errors_FsSafeError("invalid-path", "relative path must not start with a drive letter");
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
3757
4030
|
let cachedHomePath;
|
|
3758
4031
|
async function expandRelativePathWithHome(relativePath) {
|
|
3759
4032
|
const rawHome = process.env.HOME || process.env.USERPROFILE || external_node_os_namespaceObject.homedir();
|
|
@@ -3772,12 +4045,14 @@ async function expandRelativePathWithHome(relativePath) {
|
|
|
3772
4045
|
async function resolveRootContext(rootDir) {
|
|
3773
4046
|
path_assertNoNulPathInput(rootDir, "root dir contains a NUL byte");
|
|
3774
4047
|
let rootReal;
|
|
4048
|
+
let rootIdentity;
|
|
3775
4049
|
try {
|
|
3776
4050
|
rootReal = await promises_namespaceObject.realpath(rootDir);
|
|
3777
4051
|
const rootStat = await promises_namespaceObject.stat(rootReal);
|
|
3778
4052
|
if (!rootStat.isDirectory()) {
|
|
3779
4053
|
throw new errors_FsSafeError("invalid-path", "root dir is not a directory");
|
|
3780
4054
|
}
|
|
4055
|
+
rootIdentity = { dev: rootStat.dev, ino: rootStat.ino };
|
|
3781
4056
|
}
|
|
3782
4057
|
catch (err) {
|
|
3783
4058
|
if (err instanceof errors_FsSafeError) {
|
|
@@ -3790,16 +4065,37 @@ async function resolveRootContext(rootDir) {
|
|
|
3790
4065
|
}
|
|
3791
4066
|
return {
|
|
3792
4067
|
rootDir: external_node_path_namespaceObject.resolve(rootDir),
|
|
4068
|
+
rootIdentity,
|
|
3793
4069
|
rootReal,
|
|
3794
4070
|
rootWithSep: ensureTrailingSep(rootReal),
|
|
3795
4071
|
};
|
|
3796
4072
|
}
|
|
4073
|
+
async function assertRootIdentityCurrent(root) {
|
|
4074
|
+
let current;
|
|
4075
|
+
try {
|
|
4076
|
+
current = await promises_namespaceObject.lstat(root.rootReal);
|
|
4077
|
+
}
|
|
4078
|
+
catch (error) {
|
|
4079
|
+
throw new errors_FsSafeError("path-mismatch", "root path changed during operation", {
|
|
4080
|
+
cause: error instanceof Error ? error : undefined,
|
|
4081
|
+
});
|
|
4082
|
+
}
|
|
4083
|
+
if (current.isSymbolicLink() ||
|
|
4084
|
+
!current.isDirectory() ||
|
|
4085
|
+
!file_identity_sameFileIdentity(current, root.rootIdentity)) {
|
|
4086
|
+
throw new errors_FsSafeError("path-mismatch", "root path changed during operation");
|
|
4087
|
+
}
|
|
4088
|
+
}
|
|
3797
4089
|
async function resolvePathInRoot(root, relativePath, options) {
|
|
3798
4090
|
assertValidRootRelativePath(relativePath);
|
|
4091
|
+
await assertRootIdentityCurrent(root);
|
|
3799
4092
|
const expanded = await expandRelativePathWithHome(relativePath);
|
|
3800
4093
|
const resolved = external_node_path_namespaceObject.resolve(root.rootWithSep, expanded);
|
|
3801
4094
|
if (!path_isPathInside(root.rootWithSep, resolved)) {
|
|
3802
|
-
throw
|
|
4095
|
+
throw outsideWorkspaceError();
|
|
4096
|
+
}
|
|
4097
|
+
if (options?.rejectUnsafeDeviceReads === true) {
|
|
4098
|
+
assertNoUnsafeDeviceReadPath(resolved);
|
|
3803
4099
|
}
|
|
3804
4100
|
const rawAbsolutePath = external_node_path_namespaceObject.isAbsolute(expanded)
|
|
3805
4101
|
? expanded
|
|
@@ -3811,9 +4107,18 @@ async function resolvePathInRoot(root, relativePath, options) {
|
|
|
3811
4107
|
rootCanonicalPath: root.rootReal,
|
|
3812
4108
|
boundaryLabel: "root",
|
|
3813
4109
|
policy: options?.allowFinalSymlink ? ROOT_PATH_ALIAS_POLICIES.unlinkTarget : undefined,
|
|
4110
|
+
rejectSymlinks: options?.rejectSymlinks,
|
|
3814
4111
|
});
|
|
3815
4112
|
}
|
|
3816
4113
|
catch (error) {
|
|
4114
|
+
if (error instanceof errors_FsSafeError && error.code === "symlink") {
|
|
4115
|
+
throw error;
|
|
4116
|
+
}
|
|
4117
|
+
if (hasNodeErrorCode(error, "ENAMETOOLONG")) {
|
|
4118
|
+
throw new errors_FsSafeError("invalid-path", "relative path is too long", {
|
|
4119
|
+
cause: error instanceof Error ? error : undefined,
|
|
4120
|
+
});
|
|
4121
|
+
}
|
|
3817
4122
|
const code = options?.aliasErrorCode ?? "outside-workspace";
|
|
3818
4123
|
throw new errors_FsSafeError(code, code === "path-alias" ? "path alias escape blocked" : "file is outside workspace root", {
|
|
3819
4124
|
cause: error instanceof Error ? error : undefined,
|
|
@@ -3825,29 +4130,6 @@ async function resolvePathWithinRoot(params) {
|
|
|
3825
4130
|
return await resolvePathInRoot(await resolveRootContext(params.rootDir), params.relativePath);
|
|
3826
4131
|
}
|
|
3827
4132
|
|
|
3828
|
-
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/root-errors.js
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
function isAlreadyExistsError(error) {
|
|
3832
|
-
return hasNodeErrorCode(error, "EEXIST") || /File exists|EEXIST/i.test(String(error));
|
|
3833
|
-
}
|
|
3834
|
-
function normalizePinnedWriteError(error) {
|
|
3835
|
-
if (error instanceof errors_FsSafeError) {
|
|
3836
|
-
return error;
|
|
3837
|
-
}
|
|
3838
|
-
return new errors_FsSafeError("invalid-path", "path is not a regular file under root", {
|
|
3839
|
-
cause: error instanceof Error ? error : undefined,
|
|
3840
|
-
});
|
|
3841
|
-
}
|
|
3842
|
-
function normalizePinnedPathError(error) {
|
|
3843
|
-
if (error instanceof errors_FsSafeError) {
|
|
3844
|
-
return error;
|
|
3845
|
-
}
|
|
3846
|
-
return new errors_FsSafeError("path-alias", "path is not under root", {
|
|
3847
|
-
cause: error instanceof Error ? error : undefined,
|
|
3848
|
-
});
|
|
3849
|
-
}
|
|
3850
|
-
|
|
3851
4133
|
;// CONCATENATED MODULE: ../../node_modules/@openclaw/fs-safe/dist/json-stringify.js
|
|
3852
4134
|
function stringifyJsonDocument(value, replacer, space) {
|
|
3853
4135
|
const text = JSON.stringify(value, replacer, space);
|
|
@@ -3882,6 +4164,17 @@ function limitEntry(relativePath) {
|
|
|
3882
4164
|
return { relativePath, kind: "truncated", size: 0 };
|
|
3883
4165
|
}
|
|
3884
4166
|
async function* walkRoot(root, relativePath, options) {
|
|
4167
|
+
if (!["skip", "follow-within-root"].includes(options.symlinkPolicy)) {
|
|
4168
|
+
throw new TypeError(`invalid root walk symlink policy: ${String(options.symlinkPolicy)}`);
|
|
4169
|
+
}
|
|
4170
|
+
if (options.limitBehavior !== undefined &&
|
|
4171
|
+
!["truncate", "throw"].includes(options.limitBehavior)) {
|
|
4172
|
+
throw new TypeError(`invalid root walk limit behavior: ${String(options.limitBehavior)}`);
|
|
4173
|
+
}
|
|
4174
|
+
if (options.onDirectoryError !== undefined &&
|
|
4175
|
+
!["throw", "skip-and-report"].includes(options.onDirectoryError)) {
|
|
4176
|
+
throw new TypeError(`invalid root walk directory error behavior: ${String(options.onDirectoryError)}`);
|
|
4177
|
+
}
|
|
3885
4178
|
const maxDepth = validateBudget("maxDepth", options.maxDepth);
|
|
3886
4179
|
const maxEntries = validateBudget("maxEntries", options.maxEntries);
|
|
3887
4180
|
const visitedDirectories = new Set();
|
|
@@ -3909,17 +4202,21 @@ async function* walkRoot(root, relativePath, options) {
|
|
|
3909
4202
|
return;
|
|
3910
4203
|
}
|
|
3911
4204
|
visitedDirectories.add(resolvedDirectory.canonicalPath);
|
|
4205
|
+
options.signal?.throwIfAborted();
|
|
3912
4206
|
const listingDirectory = external_node_path_namespaceObject.relative(root.rootReal, resolvedDirectory.canonicalPath)
|
|
3913
4207
|
.split(external_node_path_namespaceObject.sep)
|
|
3914
4208
|
.join(external_node_path_namespaceObject.posix.sep);
|
|
3915
4209
|
entries = await root.list(listingDirectory, { withFileTypes: true });
|
|
3916
4210
|
}
|
|
3917
4211
|
catch (error) {
|
|
4212
|
+
// Cancellation is never a recoverable directory read failure.
|
|
4213
|
+
options.signal?.throwIfAborted();
|
|
3918
4214
|
if ((options.onDirectoryError ?? "throw") === "throw")
|
|
3919
4215
|
throw error;
|
|
3920
4216
|
yield { relativePath: directory, kind: "directory-error", size: 0, error };
|
|
3921
4217
|
return;
|
|
3922
4218
|
}
|
|
4219
|
+
options.signal?.throwIfAborted();
|
|
3923
4220
|
for (const entry of entries) {
|
|
3924
4221
|
options.signal?.throwIfAborted();
|
|
3925
4222
|
const child = directory
|
|
@@ -4100,11 +4397,11 @@ function logWarn(message) {
|
|
|
4100
4397
|
}
|
|
4101
4398
|
}
|
|
4102
4399
|
const SUPPORTS_NOFOLLOW = process.platform !== "win32" && "O_NOFOLLOW" in external_node_fs_namespaceObject.constants;
|
|
4103
|
-
const NONBLOCK_OPEN_FLAG = "O_NONBLOCK" in external_node_fs_namespaceObject.constants ? external_node_fs_namespaceObject.constants.O_NONBLOCK : 0;
|
|
4104
|
-
const OPEN_READ_FLAGS = external_node_fs_namespaceObject.constants.O_RDONLY |
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
const
|
|
4400
|
+
const NONBLOCK_OPEN_FLAG = process.platform !== "win32" && "O_NONBLOCK" in external_node_fs_namespaceObject.constants ? external_node_fs_namespaceObject.constants.O_NONBLOCK : 0;
|
|
4401
|
+
const OPEN_READ_FLAGS = external_node_fs_namespaceObject.constants.O_RDONLY |
|
|
4402
|
+
(SUPPORTS_NOFOLLOW ? external_node_fs_namespaceObject.constants.O_NOFOLLOW : 0) |
|
|
4403
|
+
NONBLOCK_OPEN_FLAG;
|
|
4404
|
+
const OPEN_READ_FOLLOW_FLAGS = external_node_fs_namespaceObject.constants.O_RDONLY | NONBLOCK_OPEN_FLAG;
|
|
4108
4405
|
const OPEN_WRITE_EXISTING_FLAGS = external_node_fs_namespaceObject.constants.O_WRONLY | (SUPPORTS_NOFOLLOW ? external_node_fs_namespaceObject.constants.O_NOFOLLOW : 0);
|
|
4109
4406
|
const OPEN_WRITE_CREATE_FLAGS = external_node_fs_namespaceObject.constants.O_WRONLY |
|
|
4110
4407
|
external_node_fs_namespaceObject.constants.O_CREAT |
|
|
@@ -4117,6 +4414,21 @@ const OPEN_APPEND_CREATE_FLAGS = external_node_fs_namespaceObject.constants.O_RD
|
|
|
4117
4414
|
external_node_fs_namespaceObject.constants.O_EXCL |
|
|
4118
4415
|
(SUPPORTS_NOFOLLOW ? external_node_fs_namespaceObject.constants.O_NOFOLLOW : 0);
|
|
4119
4416
|
const DEFAULT_ROOT_MAX_BYTES = 16 * 1024 * 1024;
|
|
4417
|
+
function pathStatFromStats(stat) {
|
|
4418
|
+
return {
|
|
4419
|
+
dev: Number(stat.dev),
|
|
4420
|
+
gid: Number(stat.gid),
|
|
4421
|
+
ino: Number(stat.ino),
|
|
4422
|
+
isDirectory: stat.isDirectory(),
|
|
4423
|
+
isFile: stat.isFile(),
|
|
4424
|
+
isSymbolicLink: stat.isSymbolicLink(),
|
|
4425
|
+
mode: stat.mode,
|
|
4426
|
+
mtimeMs: stat.mtimeMs,
|
|
4427
|
+
nlink: stat.nlink,
|
|
4428
|
+
size: stat.size,
|
|
4429
|
+
uid: stat.uid,
|
|
4430
|
+
};
|
|
4431
|
+
}
|
|
4120
4432
|
function openResult(params) {
|
|
4121
4433
|
return {
|
|
4122
4434
|
handle: params.handle,
|
|
@@ -4129,11 +4441,15 @@ function openResult(params) {
|
|
|
4129
4441
|
async function openVerifiedLocalFile(filePath, options) {
|
|
4130
4442
|
assertNoUnsafeDeviceReadPath(filePath);
|
|
4131
4443
|
const fsSafeTestHooks = getFsSafeTestHooks();
|
|
4444
|
+
let preOpenStat;
|
|
4132
4445
|
// Reject directories before opening so we never surface EISDIR to callers (e.g. tool
|
|
4133
4446
|
// results that get sent to messaging channels). See openclaw/openclaw#31186.
|
|
4134
4447
|
try {
|
|
4135
|
-
|
|
4136
|
-
if (
|
|
4448
|
+
preOpenStat = await promises_namespaceObject.lstat(filePath);
|
|
4449
|
+
if (preOpenStat.isSymbolicLink() && options?.symlinks !== "follow-within-root") {
|
|
4450
|
+
throw new errors_FsSafeError("symlink", "symlink not allowed");
|
|
4451
|
+
}
|
|
4452
|
+
if (!preOpenStat.isFile() && !preOpenStat.isSymbolicLink()) {
|
|
4137
4453
|
throw new errors_FsSafeError("not-file", "not a file");
|
|
4138
4454
|
}
|
|
4139
4455
|
await fsSafeTestHooks?.afterPreOpenLstat?.(filePath);
|
|
@@ -4147,12 +4463,8 @@ async function openVerifiedLocalFile(filePath, options) {
|
|
|
4147
4463
|
let handle;
|
|
4148
4464
|
try {
|
|
4149
4465
|
const openFlags = options?.symlinks === "follow-within-root"
|
|
4150
|
-
?
|
|
4151
|
-
|
|
4152
|
-
: OPEN_READ_FOLLOW_FLAGS
|
|
4153
|
-
: options?.nonBlockingRead
|
|
4154
|
-
? OPEN_READ_NONBLOCK_FLAGS
|
|
4155
|
-
: OPEN_READ_FLAGS;
|
|
4466
|
+
? OPEN_READ_FOLLOW_FLAGS
|
|
4467
|
+
: OPEN_READ_FLAGS;
|
|
4156
4468
|
await fsSafeTestHooks?.beforeOpen?.(filePath, openFlags);
|
|
4157
4469
|
handle = await promises_namespaceObject.open(filePath, openFlags);
|
|
4158
4470
|
try {
|
|
@@ -4165,7 +4477,7 @@ async function openVerifiedLocalFile(filePath, options) {
|
|
|
4165
4477
|
}
|
|
4166
4478
|
catch (err) {
|
|
4167
4479
|
if (path_isNotFoundPathError(err)) {
|
|
4168
|
-
throw
|
|
4480
|
+
throw fileNotFoundError();
|
|
4169
4481
|
}
|
|
4170
4482
|
if (isSymlinkOpenError(err)) {
|
|
4171
4483
|
throw new errors_FsSafeError("symlink", "symlink open blocked", { cause: err });
|
|
@@ -4181,8 +4493,13 @@ async function openVerifiedLocalFile(filePath, options) {
|
|
|
4181
4493
|
if (!stat.isFile()) {
|
|
4182
4494
|
throw new errors_FsSafeError("not-file", "not a file");
|
|
4183
4495
|
}
|
|
4496
|
+
if (preOpenStat &&
|
|
4497
|
+
!preOpenStat.isSymbolicLink() &&
|
|
4498
|
+
!file_identity_sameFileIdentity(stat, preOpenStat)) {
|
|
4499
|
+
throw new errors_FsSafeError("path-mismatch", "path changed before open");
|
|
4500
|
+
}
|
|
4184
4501
|
if (options?.hardlinks === "reject" && stat.nlink > 1) {
|
|
4185
|
-
throw
|
|
4502
|
+
throw hardlinkedPathNotAllowedError();
|
|
4186
4503
|
}
|
|
4187
4504
|
if (options?.symlinks === "follow-within-root") {
|
|
4188
4505
|
const pathStat = await promises_namespaceObject.stat(filePath);
|
|
@@ -4202,7 +4519,7 @@ async function openVerifiedLocalFile(filePath, options) {
|
|
|
4202
4519
|
const realPath = await resolveOpenedFileRealPathForHandle(handle, filePath);
|
|
4203
4520
|
const realStat = await promises_namespaceObject.stat(realPath);
|
|
4204
4521
|
if (options?.hardlinks === "reject" && realStat.nlink > 1) {
|
|
4205
|
-
throw
|
|
4522
|
+
throw hardlinkedPathNotAllowedError();
|
|
4206
4523
|
}
|
|
4207
4524
|
if (!file_identity_sameFileIdentity(stat, realStat)) {
|
|
4208
4525
|
throw new errors_FsSafeError("path-mismatch", "path mismatch");
|
|
@@ -4215,17 +4532,19 @@ async function openVerifiedLocalFile(filePath, options) {
|
|
|
4215
4532
|
throw err;
|
|
4216
4533
|
}
|
|
4217
4534
|
if (path_isNotFoundPathError(err)) {
|
|
4218
|
-
throw
|
|
4535
|
+
throw fileNotFoundError();
|
|
4219
4536
|
}
|
|
4220
4537
|
throw err;
|
|
4221
4538
|
}
|
|
4222
4539
|
}
|
|
4223
4540
|
class RootHandle {
|
|
4541
|
+
rootIdentity;
|
|
4224
4542
|
rootDir;
|
|
4225
4543
|
rootReal;
|
|
4226
4544
|
rootWithSep;
|
|
4227
4545
|
defaults;
|
|
4228
4546
|
constructor(context, defaults = {}) {
|
|
4547
|
+
this.rootIdentity = context.rootIdentity;
|
|
4229
4548
|
this.rootDir = context.rootDir;
|
|
4230
4549
|
this.rootReal = context.rootReal;
|
|
4231
4550
|
this.rootWithSep = context.rootWithSep;
|
|
@@ -4234,11 +4553,19 @@ class RootHandle {
|
|
|
4234
4553
|
get context() {
|
|
4235
4554
|
return {
|
|
4236
4555
|
rootDir: this.rootDir,
|
|
4556
|
+
rootIdentity: this.rootIdentity,
|
|
4237
4557
|
rootReal: this.rootReal,
|
|
4238
4558
|
rootWithSep: this.rootWithSep,
|
|
4239
4559
|
};
|
|
4240
4560
|
}
|
|
4561
|
+
mutationOptions(options) {
|
|
4562
|
+
return {
|
|
4563
|
+
...options,
|
|
4564
|
+
denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
|
|
4565
|
+
};
|
|
4566
|
+
}
|
|
4241
4567
|
async resolve(relativePath) {
|
|
4568
|
+
assertValidRootDestinationPath(relativePath);
|
|
4242
4569
|
return (await resolvePathInRoot(this.context, relativePath, { allowFinalSymlink: true })).resolved;
|
|
4243
4570
|
}
|
|
4244
4571
|
async open(relativePath, options = {}) {
|
|
@@ -4278,67 +4605,67 @@ class RootHandle {
|
|
|
4278
4605
|
};
|
|
4279
4606
|
}
|
|
4280
4607
|
async openWritable(relativePath, options = {}) {
|
|
4608
|
+
assertValidRootDestinationPath(relativePath);
|
|
4281
4609
|
const writeMode = options.writeMode ?? "replace";
|
|
4282
4610
|
return await openWritableFileInRoot(this.context, {
|
|
4283
4611
|
relativePath,
|
|
4284
4612
|
mkdir: this.defaults.mkdir,
|
|
4285
4613
|
mode: this.defaults.mode,
|
|
4286
|
-
...options,
|
|
4287
|
-
denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
|
|
4614
|
+
...this.mutationOptions(options),
|
|
4288
4615
|
append: writeMode === "append",
|
|
4289
4616
|
truncateExisting: writeMode === "replace",
|
|
4290
4617
|
});
|
|
4291
4618
|
}
|
|
4292
4619
|
async append(relativePath, data, options = {}) {
|
|
4620
|
+
assertValidRootDestinationPath(relativePath);
|
|
4293
4621
|
await appendFileInRoot(this.context, {
|
|
4294
4622
|
relativePath,
|
|
4295
4623
|
data,
|
|
4296
4624
|
mkdir: this.defaults.mkdir,
|
|
4297
4625
|
mode: this.defaults.mode,
|
|
4298
|
-
...options,
|
|
4299
|
-
denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
|
|
4626
|
+
...this.mutationOptions(options),
|
|
4300
4627
|
});
|
|
4301
4628
|
}
|
|
4302
4629
|
async remove(relativePath, options = {}) {
|
|
4303
4630
|
assertValidRootRelativePath(relativePath);
|
|
4304
4631
|
await removePathInRoot(this.context, {
|
|
4305
4632
|
relativePath,
|
|
4306
|
-
|
|
4633
|
+
...this.mutationOptions(options),
|
|
4307
4634
|
});
|
|
4308
4635
|
}
|
|
4309
4636
|
async mkdir(relativePath, options = {}) {
|
|
4310
|
-
|
|
4637
|
+
assertValidRootDestinationPath(relativePath);
|
|
4311
4638
|
await mkdirPathInRoot(this.context, {
|
|
4312
4639
|
relativePath,
|
|
4313
|
-
|
|
4640
|
+
...this.mutationOptions(options),
|
|
4314
4641
|
});
|
|
4315
4642
|
}
|
|
4316
4643
|
async ensureRoot(options = {}) {
|
|
4317
4644
|
await mkdirPathInRoot(this.context, {
|
|
4318
4645
|
relativePath: "",
|
|
4319
4646
|
allowRoot: true,
|
|
4320
|
-
|
|
4647
|
+
...this.mutationOptions(options),
|
|
4321
4648
|
});
|
|
4322
4649
|
}
|
|
4323
4650
|
async write(relativePath, data, options = {}) {
|
|
4651
|
+
assertValidRootDestinationPath(relativePath);
|
|
4324
4652
|
await writeFileInRoot(this.context, {
|
|
4325
4653
|
relativePath,
|
|
4326
4654
|
data,
|
|
4327
4655
|
mkdir: this.defaults.mkdir,
|
|
4328
4656
|
mode: this.defaults.mode,
|
|
4329
4657
|
renameIdentity: this.defaults.renameIdentity,
|
|
4330
|
-
...options,
|
|
4331
|
-
denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
|
|
4658
|
+
...this.mutationOptions(options),
|
|
4332
4659
|
});
|
|
4333
4660
|
}
|
|
4334
4661
|
async create(relativePath, data, options = {}) {
|
|
4662
|
+
assertValidRootDestinationPath(relativePath);
|
|
4335
4663
|
await writeFileInRoot(this.context, {
|
|
4336
4664
|
relativePath,
|
|
4337
4665
|
data,
|
|
4338
4666
|
mkdir: this.defaults.mkdir,
|
|
4339
4667
|
mode: this.defaults.mode,
|
|
4340
|
-
...options,
|
|
4341
|
-
denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
|
|
4668
|
+
...this.mutationOptions(options),
|
|
4342
4669
|
overwrite: false,
|
|
4343
4670
|
});
|
|
4344
4671
|
}
|
|
@@ -4353,15 +4680,14 @@ class RootHandle {
|
|
|
4353
4680
|
await this.create(relativePath, trailingNewline ? `${json}\n` : json, writeOptions);
|
|
4354
4681
|
}
|
|
4355
4682
|
async copyIn(relativePath, sourcePath, options = {}) {
|
|
4356
|
-
|
|
4683
|
+
assertValidRootDestinationPath(relativePath);
|
|
4357
4684
|
await copyFileInRoot(this.context, {
|
|
4358
4685
|
sourcePath,
|
|
4359
4686
|
relativePath,
|
|
4360
4687
|
maxBytes: this.defaults.maxBytes,
|
|
4361
4688
|
mkdir: this.defaults.mkdir,
|
|
4362
4689
|
mode: this.defaults.mode,
|
|
4363
|
-
...options,
|
|
4364
|
-
denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
|
|
4690
|
+
...this.mutationOptions(options),
|
|
4365
4691
|
});
|
|
4366
4692
|
}
|
|
4367
4693
|
async exists(relativePath) {
|
|
@@ -4388,9 +4714,9 @@ class RootHandle {
|
|
|
4388
4714
|
}
|
|
4389
4715
|
async move(fromRelative, toRelative, options = {}) {
|
|
4390
4716
|
assertValidRootRelativePath(fromRelative);
|
|
4391
|
-
|
|
4717
|
+
assertValidRootDestinationPath(toRelative);
|
|
4392
4718
|
validatePinnedOperationPayload({ from: fromRelative, to: toRelative });
|
|
4393
|
-
const denyMutations =
|
|
4719
|
+
const { denyMutations } = this.mutationOptions(options);
|
|
4394
4720
|
await assertMoveMutationAllowed(this.context, {
|
|
4395
4721
|
fromRelative,
|
|
4396
4722
|
toRelative,
|
|
@@ -4422,6 +4748,8 @@ async function root_impl_root(rootDir, defaults = {}) {
|
|
|
4422
4748
|
async function openFileInRoot(root, params) {
|
|
4423
4749
|
const { rootWithSep, resolved } = await resolvePathInRoot(root, params.relativePath, {
|
|
4424
4750
|
allowFinalSymlink: true,
|
|
4751
|
+
rejectUnsafeDeviceReads: true,
|
|
4752
|
+
rejectSymlinks: params.symlinks !== "follow-within-root",
|
|
4425
4753
|
});
|
|
4426
4754
|
let opened;
|
|
4427
4755
|
try {
|
|
@@ -4438,11 +4766,11 @@ async function openFileInRoot(root, params) {
|
|
|
4438
4766
|
}
|
|
4439
4767
|
if (params.hardlinks !== "allow" && opened.stat.nlink > 1) {
|
|
4440
4768
|
await opened.handle.close().catch(() => { });
|
|
4441
|
-
throw
|
|
4769
|
+
throw hardlinkedPathNotAllowedError();
|
|
4442
4770
|
}
|
|
4443
4771
|
if (!path_isPathInside(rootWithSep, opened.realPath)) {
|
|
4444
4772
|
await opened.handle.close().catch(() => { });
|
|
4445
|
-
throw
|
|
4773
|
+
throw outsideWorkspaceError();
|
|
4446
4774
|
}
|
|
4447
4775
|
return opened;
|
|
4448
4776
|
}
|
|
@@ -4524,33 +4852,47 @@ async function verifyAtomicWriteResult(params) {
|
|
|
4524
4852
|
throw new errors_FsSafeError("path-mismatch", "path changed during write");
|
|
4525
4853
|
}
|
|
4526
4854
|
if (!path_isPathInside(params.root.rootWithSep, opened.realPath)) {
|
|
4527
|
-
throw
|
|
4855
|
+
throw outsideWorkspaceError();
|
|
4528
4856
|
}
|
|
4529
4857
|
}
|
|
4530
4858
|
finally {
|
|
4531
4859
|
await opened.handle.close().catch(() => { });
|
|
4532
4860
|
}
|
|
4533
4861
|
}
|
|
4534
|
-
async function
|
|
4535
|
-
const
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4862
|
+
async function resolveGuardedWritePathInRoot(root, params) {
|
|
4863
|
+
const resolvedPath = await resolvePathInRoot(root, params.relativePath, {
|
|
4864
|
+
aliasErrorCode: "path-alias",
|
|
4865
|
+
allowFinalSymlink: params.allowFinalSymlink,
|
|
4866
|
+
});
|
|
4867
|
+
await assertMutationNotDenied(resolvedPath.resolved, params.denyMutations, params.protectDeniedAncestors ? { protectAncestors: true } : undefined);
|
|
4868
|
+
if (await (params.shouldAssertNoPathAlias?.(resolvedPath) ?? true)) {
|
|
4869
|
+
try {
|
|
4870
|
+
await assertNoPathAliasEscape({
|
|
4871
|
+
absolutePath: resolvedPath.resolved,
|
|
4872
|
+
rootPath: resolvedPath.rootReal,
|
|
4873
|
+
boundaryLabel: "root",
|
|
4874
|
+
});
|
|
4875
|
+
}
|
|
4876
|
+
catch (error) {
|
|
4877
|
+
throw new errors_FsSafeError("path-alias", "path alias escape blocked", {
|
|
4878
|
+
cause: error instanceof Error ? error : undefined,
|
|
4879
|
+
});
|
|
4880
|
+
}
|
|
4546
4881
|
}
|
|
4882
|
+
return resolvedPath;
|
|
4883
|
+
}
|
|
4884
|
+
async function openWritableFileInRoot(root, params) {
|
|
4885
|
+
const { rootReal, rootWithSep, resolved } = await resolveGuardedWritePathInRoot(root, {
|
|
4886
|
+
relativePath: params.relativePath,
|
|
4887
|
+
denyMutations: params.denyMutations,
|
|
4888
|
+
});
|
|
4547
4889
|
let ioPath = params.mkdir === false
|
|
4548
4890
|
? resolved
|
|
4549
4891
|
: await prepareRootWriteTarget(rootReal, resolved);
|
|
4550
4892
|
try {
|
|
4551
4893
|
const resolvedRealPath = await promises_namespaceObject.realpath(ioPath);
|
|
4552
4894
|
if (!path_isPathInside(rootWithSep, resolvedRealPath)) {
|
|
4553
|
-
throw
|
|
4895
|
+
throw outsideWorkspaceError();
|
|
4554
4896
|
}
|
|
4555
4897
|
ioPath = resolvedRealPath;
|
|
4556
4898
|
}
|
|
@@ -4581,7 +4923,7 @@ async function openWritableFileInRoot(root, params) {
|
|
|
4581
4923
|
}
|
|
4582
4924
|
catch (err) {
|
|
4583
4925
|
if (path_isNotFoundPathError(err)) {
|
|
4584
|
-
throw
|
|
4926
|
+
throw fileNotFoundError();
|
|
4585
4927
|
}
|
|
4586
4928
|
if (isSymlinkOpenError(err)) {
|
|
4587
4929
|
throw new errors_FsSafeError("symlink", "symlink open blocked", { cause: err });
|
|
@@ -4592,13 +4934,17 @@ async function openWritableFileInRoot(root, params) {
|
|
|
4592
4934
|
throw err;
|
|
4593
4935
|
}
|
|
4594
4936
|
let realPathForCleanup = null;
|
|
4937
|
+
let createdIdentity = null;
|
|
4595
4938
|
try {
|
|
4596
4939
|
const stat = await handle.stat();
|
|
4940
|
+
if (createdForWrite) {
|
|
4941
|
+
createdIdentity = stat;
|
|
4942
|
+
}
|
|
4597
4943
|
if (!stat.isFile()) {
|
|
4598
|
-
throw new errors_FsSafeError("
|
|
4944
|
+
throw new errors_FsSafeError("not-file", "path is not a regular file under root");
|
|
4599
4945
|
}
|
|
4600
4946
|
if (stat.nlink > 1) {
|
|
4601
|
-
throw
|
|
4947
|
+
throw hardlinkedPathNotAllowedError();
|
|
4602
4948
|
}
|
|
4603
4949
|
try {
|
|
4604
4950
|
const lstat = await promises_namespaceObject.lstat(ioPath);
|
|
@@ -4621,10 +4967,10 @@ async function openWritableFileInRoot(root, params) {
|
|
|
4621
4967
|
throw new errors_FsSafeError("path-mismatch", "path mismatch");
|
|
4622
4968
|
}
|
|
4623
4969
|
if (realStat.nlink > 1) {
|
|
4624
|
-
throw
|
|
4970
|
+
throw hardlinkedPathNotAllowedError();
|
|
4625
4971
|
}
|
|
4626
4972
|
if (!path_isPathInside(rootWithSep, realPath)) {
|
|
4627
|
-
throw
|
|
4973
|
+
throw outsideWorkspaceError();
|
|
4628
4974
|
}
|
|
4629
4975
|
// Truncate only after boundary and identity checks complete. This avoids
|
|
4630
4976
|
// irreversible side effects if a symlink target changes before validation.
|
|
@@ -4644,8 +4990,8 @@ async function openWritableFileInRoot(root, params) {
|
|
|
4644
4990
|
const cleanupCreatedPath = createdForWrite && err instanceof errors_FsSafeError;
|
|
4645
4991
|
const cleanupPath = realPathForCleanup ?? ioPath;
|
|
4646
4992
|
await handle.close().catch(() => { });
|
|
4647
|
-
if (cleanupCreatedPath) {
|
|
4648
|
-
await
|
|
4993
|
+
if (cleanupCreatedPath && createdIdentity) {
|
|
4994
|
+
await removePathIfIdentityUnchanged(cleanupPath, createdIdentity).catch(() => { });
|
|
4649
4995
|
}
|
|
4650
4996
|
throw err;
|
|
4651
4997
|
}
|
|
@@ -4690,7 +5036,11 @@ async function appendFileInRoot(root, params) {
|
|
|
4690
5036
|
}
|
|
4691
5037
|
async function removePathInRoot(root, params) {
|
|
4692
5038
|
validatePinnedOperationPayload({ relativePath: params.relativePath });
|
|
4693
|
-
const resolved = await
|
|
5039
|
+
const resolved = await resolvePinnedPathInRoot(root, {
|
|
5040
|
+
relativePath: params.relativePath,
|
|
5041
|
+
denyMutations: params.denyMutations,
|
|
5042
|
+
remove: true,
|
|
5043
|
+
});
|
|
4694
5044
|
try {
|
|
4695
5045
|
await removePathFallback(resolved);
|
|
4696
5046
|
}
|
|
@@ -4709,15 +5059,16 @@ async function mkdirPathInRoot(root, params) {
|
|
|
4709
5059
|
}
|
|
4710
5060
|
}
|
|
4711
5061
|
async function writeFileInRoot(root, params) {
|
|
4712
|
-
|
|
4713
|
-
|
|
5062
|
+
await serializePathWrite(rootWriteQueueKey(root, params.relativePath), async () => {
|
|
5063
|
+
if (process.platform === "win32" &&
|
|
5064
|
+
(params.renameIdentity === "verify-content-with-lock" || !getNativeBinding())) {
|
|
4714
5065
|
await writeFileFallback(root, params);
|
|
5066
|
+
return;
|
|
5067
|
+
}
|
|
5068
|
+
const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode, params.denyMutations);
|
|
5069
|
+
await serializePathWrite(pinned.targetPath, async () => {
|
|
5070
|
+
await commitPinnedWriteInRoot(root, pinned, params);
|
|
4715
5071
|
});
|
|
4716
|
-
return;
|
|
4717
|
-
}
|
|
4718
|
-
const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode, params.denyMutations);
|
|
4719
|
-
await serializePathWrite(pinned.targetPath, async () => {
|
|
4720
|
-
await commitPinnedWriteInRoot(root, pinned, params);
|
|
4721
5072
|
});
|
|
4722
5073
|
}
|
|
4723
5074
|
async function commitPinnedWriteInRoot(root, pinned, params) {
|
|
@@ -4733,6 +5084,7 @@ async function commitPinnedWriteInRoot(root, pinned, params) {
|
|
|
4733
5084
|
mode: params.mode ?? pinned.mode,
|
|
4734
5085
|
overwrite: params.overwrite,
|
|
4735
5086
|
input: { kind: "buffer", data: params.data, encoding: params.encoding },
|
|
5087
|
+
rootIdentity: root.rootIdentity,
|
|
4736
5088
|
});
|
|
4737
5089
|
}
|
|
4738
5090
|
catch (error) {
|
|
@@ -4770,26 +5122,35 @@ async function copyFileInRoot(root, params) {
|
|
|
4770
5122
|
throw new errors_FsSafeError("too-large", `file exceeds limit of ${params.maxBytes} bytes (got ${source.stat.size})`);
|
|
4771
5123
|
}
|
|
4772
5124
|
try {
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
await
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
5125
|
+
await serializePathWrite(rootWriteQueueKey(root, params.relativePath), async () => {
|
|
5126
|
+
const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode, params.denyMutations);
|
|
5127
|
+
await serializePathWrite(pinned.targetPath, async () => {
|
|
5128
|
+
await assertCopySourceCurrent(source);
|
|
5129
|
+
let identity;
|
|
5130
|
+
try {
|
|
5131
|
+
identity = await runPinnedWriteHelper({
|
|
5132
|
+
rootPath: pinned.rootReal,
|
|
5133
|
+
relativeParentPath: pinned.relativeParentPath,
|
|
5134
|
+
basename: pinned.basename,
|
|
5135
|
+
mkdir: params.mkdir !== false,
|
|
5136
|
+
mode: pinned.mode,
|
|
5137
|
+
overwrite: true,
|
|
5138
|
+
maxBytes: params.maxBytes,
|
|
5139
|
+
input: { kind: "stream", stream: source.handle.createReadStream() },
|
|
5140
|
+
rootIdentity: root.rootIdentity,
|
|
5141
|
+
});
|
|
5142
|
+
}
|
|
5143
|
+
catch (error) {
|
|
5144
|
+
throw normalizePinnedWriteError(error);
|
|
5145
|
+
}
|
|
5146
|
+
try {
|
|
5147
|
+
await assertCopySourcePathCurrent(source);
|
|
5148
|
+
}
|
|
5149
|
+
catch (error) {
|
|
5150
|
+
await removePathIfIdentityUnchanged(pinned.targetPath, identity).catch(() => undefined);
|
|
5151
|
+
throw error;
|
|
5152
|
+
}
|
|
4785
5153
|
});
|
|
4786
|
-
try {
|
|
4787
|
-
await assertCopySourcePathCurrent(source);
|
|
4788
|
-
}
|
|
4789
|
-
catch (error) {
|
|
4790
|
-
await removeCopyTargetIfUnchanged(pinned.targetPath, identity).catch(() => undefined);
|
|
4791
|
-
throw error;
|
|
4792
|
-
}
|
|
4793
5154
|
});
|
|
4794
5155
|
}
|
|
4795
5156
|
finally {
|
|
@@ -4809,7 +5170,7 @@ async function assertCopySourcePathCurrent(source) {
|
|
|
4809
5170
|
throw new errors_FsSafeError("path-mismatch", "copy source path changed");
|
|
4810
5171
|
}
|
|
4811
5172
|
}
|
|
4812
|
-
async function
|
|
5173
|
+
async function removePathIfIdentityUnchanged(targetPath, identity) {
|
|
4813
5174
|
const parentGuard = await directory_guard_createAsyncDirectoryGuard(external_node_path_namespaceObject.dirname(targetPath));
|
|
4814
5175
|
const current = await promises_namespaceObject.lstat(targetPath);
|
|
4815
5176
|
if (current.isSymbolicLink() || !file_identity_sameFileIdentity(current, identity)) {
|
|
@@ -4820,25 +5181,15 @@ async function removeCopyTargetIfUnchanged(targetPath, identity) {
|
|
|
4820
5181
|
});
|
|
4821
5182
|
}
|
|
4822
5183
|
async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode, denyMutations) {
|
|
4823
|
-
const { rootReal, rootWithSep, resolved } = await
|
|
4824
|
-
|
|
5184
|
+
const { rootReal, rootWithSep, resolved } = await resolveGuardedWritePathInRoot(root, {
|
|
5185
|
+
relativePath,
|
|
5186
|
+
denyMutations,
|
|
4825
5187
|
});
|
|
4826
|
-
await assertMutationNotDenied(resolved, denyMutations);
|
|
4827
|
-
try {
|
|
4828
|
-
await assertNoPathAliasEscape({
|
|
4829
|
-
absolutePath: resolved,
|
|
4830
|
-
rootPath: rootReal,
|
|
4831
|
-
boundaryLabel: "root",
|
|
4832
|
-
});
|
|
4833
|
-
}
|
|
4834
|
-
catch (err) {
|
|
4835
|
-
throw new errors_FsSafeError("path-alias", "path alias escape blocked", { cause: err });
|
|
4836
|
-
}
|
|
4837
5188
|
// resolvePathInRoot already enforces isPathInside, so any actual escape
|
|
4838
5189
|
// is rejected upstream.
|
|
4839
5190
|
const relativeResolved = external_node_path_namespaceObject.relative(rootReal, resolved);
|
|
4840
5191
|
if (external_node_path_namespaceObject.isAbsolute(relativeResolved)) {
|
|
4841
|
-
throw
|
|
5192
|
+
throw outsideWorkspaceError();
|
|
4842
5193
|
}
|
|
4843
5194
|
const relativePosix = relativeResolved
|
|
4844
5195
|
? relativeResolved.split(external_node_path_namespaceObject.sep).join(external_node_path_namespaceObject.posix.sep)
|
|
@@ -4853,11 +5204,12 @@ async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode,
|
|
|
4853
5204
|
relativePath,
|
|
4854
5205
|
hardlinks: "reject",
|
|
4855
5206
|
nonBlockingRead: true,
|
|
5207
|
+
symlinks: "follow-within-root",
|
|
4856
5208
|
});
|
|
4857
5209
|
try {
|
|
4858
5210
|
mode = requestedMode ?? (opened.stat.mode & 0o777);
|
|
4859
5211
|
if (!path_isPathInside(rootWithSep, opened.realPath)) {
|
|
4860
|
-
throw
|
|
5212
|
+
throw outsideWorkspaceError();
|
|
4861
5213
|
}
|
|
4862
5214
|
}
|
|
4863
5215
|
finally {
|
|
@@ -4881,17 +5233,9 @@ async function resolvePinnedPathInRoot(root, params) {
|
|
|
4881
5233
|
return await resolvePinnedOperationPathInRoot(root, {
|
|
4882
5234
|
allowRoot: params.allowRoot,
|
|
4883
5235
|
denyMutations: params.denyMutations,
|
|
4884
|
-
protectDenyMutationAncestors:
|
|
5236
|
+
protectDenyMutationAncestors: params.remove === true,
|
|
4885
5237
|
relativePath: params.relativePath,
|
|
4886
|
-
policy: PATH_ALIAS_POLICIES.strict,
|
|
4887
|
-
});
|
|
4888
|
-
}
|
|
4889
|
-
async function resolvePinnedRemovePathInRoot(root, relativePath, denyMutations) {
|
|
4890
|
-
return await resolvePinnedOperationPathInRoot(root, {
|
|
4891
|
-
denyMutations,
|
|
4892
|
-
protectDenyMutationAncestors: true,
|
|
4893
|
-
relativePath,
|
|
4894
|
-
policy: PATH_ALIAS_POLICIES.unlinkTarget,
|
|
5238
|
+
policy: params.remove ? PATH_ALIAS_POLICIES.unlinkTarget : PATH_ALIAS_POLICIES.strict,
|
|
4895
5239
|
});
|
|
4896
5240
|
}
|
|
4897
5241
|
async function resolvePinnedOperationPathInRoot(root, params) {
|
|
@@ -4909,11 +5253,11 @@ async function resolvePinnedOperationPathInRoot(root, params) {
|
|
|
4909
5253
|
relativeResolved === "." ||
|
|
4910
5254
|
firstSegment === ".." ||
|
|
4911
5255
|
external_node_path_namespaceObject.isAbsolute(relativeResolved)) {
|
|
4912
|
-
throw
|
|
5256
|
+
throw outsideWorkspaceError();
|
|
4913
5257
|
}
|
|
4914
5258
|
const relativePosix = relativeResolved.split(external_node_path_namespaceObject.sep).join(external_node_path_namespaceObject.posix.sep);
|
|
4915
5259
|
if (!path_isPathInside(resolved.rootWithSep, resolved.canonicalPath)) {
|
|
4916
|
-
throw
|
|
5260
|
+
throw outsideWorkspaceError();
|
|
4917
5261
|
}
|
|
4918
5262
|
await assertMutationNotDenied(resolved.canonicalPath, params.denyMutations, {
|
|
4919
5263
|
protectAncestors: params.protectDenyMutationAncestors,
|
|
@@ -4921,6 +5265,7 @@ async function resolvePinnedOperationPathInRoot(root, params) {
|
|
|
4921
5265
|
return { rootReal: resolved.rootReal, resolved: resolved.canonicalPath, relativePosix };
|
|
4922
5266
|
}
|
|
4923
5267
|
async function resolvePinnedRootPathInRoot(root, params) {
|
|
5268
|
+
await assertRootIdentityCurrent(root);
|
|
4924
5269
|
const rootReal = root.rootReal;
|
|
4925
5270
|
let resolved;
|
|
4926
5271
|
try {
|
|
@@ -4945,11 +5290,25 @@ async function resolvePinnedRootPathInRoot(root, params) {
|
|
|
4945
5290
|
canonicalPath: resolved.canonicalPath,
|
|
4946
5291
|
};
|
|
4947
5292
|
}
|
|
5293
|
+
async function prepareRemoveGuard(targetPath) {
|
|
5294
|
+
try {
|
|
5295
|
+
const guard = await directory_guard_createAsyncDirectoryGuard(external_node_path_namespaceObject.dirname(targetPath));
|
|
5296
|
+
await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("remove", targetPath);
|
|
5297
|
+
await assertAsyncDirectoryGuard(guard);
|
|
5298
|
+
return guard;
|
|
5299
|
+
}
|
|
5300
|
+
catch (error) {
|
|
5301
|
+
throw normalizeRemoveGuardError(error);
|
|
5302
|
+
}
|
|
5303
|
+
}
|
|
4948
5304
|
async function removePathFallback(resolved) {
|
|
4949
|
-
const guard = await
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
5305
|
+
const guard = await prepareRemoveGuard(resolved.resolved);
|
|
5306
|
+
try {
|
|
5307
|
+
await ((await promises_namespaceObject.lstat(resolved.resolved)).isDirectory() ? promises_namespaceObject.rmdir(resolved.resolved) : promises_namespaceObject.rm(resolved.resolved));
|
|
5308
|
+
}
|
|
5309
|
+
catch (error) {
|
|
5310
|
+
throw normalizeRemovePathError(error);
|
|
5311
|
+
}
|
|
4953
5312
|
await assertAsyncDirectoryGuard(guard).catch(() => undefined);
|
|
4954
5313
|
}
|
|
4955
5314
|
async function mkdirPathFallback(resolved) {
|
|
@@ -4961,13 +5320,13 @@ async function mkdirPathFallback(resolved) {
|
|
|
4961
5320
|
async function statPathFallback(root, relativePath) {
|
|
4962
5321
|
const resolved = await resolvePinnedPathInRoot(root, { relativePath, allowRoot: true });
|
|
4963
5322
|
try {
|
|
4964
|
-
|
|
5323
|
+
const stat = pathStatFromStats(await promises_namespaceObject.lstat(resolved.resolved));
|
|
5324
|
+
await assertRootIdentityCurrent(root);
|
|
5325
|
+
return stat;
|
|
4965
5326
|
}
|
|
4966
5327
|
catch (error) {
|
|
4967
5328
|
if (path_isNotFoundPathError(error)) {
|
|
4968
|
-
throw
|
|
4969
|
-
cause: error instanceof Error ? error : undefined,
|
|
4970
|
-
});
|
|
5329
|
+
throw fileNotFoundError(error instanceof Error ? error : undefined);
|
|
4971
5330
|
}
|
|
4972
5331
|
throw error;
|
|
4973
5332
|
}
|
|
@@ -4978,6 +5337,7 @@ async function listPathFallback(root, relativePath, withFileTypes) {
|
|
|
4978
5337
|
const names = await promises_namespaceObject.readdir(resolved.resolved);
|
|
4979
5338
|
const sortedNames = names.toSorted();
|
|
4980
5339
|
if (!withFileTypes) {
|
|
5340
|
+
await assertRootIdentityCurrent(root);
|
|
4981
5341
|
return sortedNames;
|
|
4982
5342
|
}
|
|
4983
5343
|
const entries = [];
|
|
@@ -4987,6 +5347,7 @@ async function listPathFallback(root, relativePath, withFileTypes) {
|
|
|
4987
5347
|
...pathStatFromStats(await promises_namespaceObject.lstat(external_node_path_namespaceObject.join(resolved.resolved, name))),
|
|
4988
5348
|
});
|
|
4989
5349
|
}
|
|
5350
|
+
await assertRootIdentityCurrent(root);
|
|
4990
5351
|
return entries;
|
|
4991
5352
|
}
|
|
4992
5353
|
catch (error) {
|
|
@@ -4999,6 +5360,8 @@ async function listPathFallback(root, relativePath, withFileTypes) {
|
|
|
4999
5360
|
}
|
|
5000
5361
|
}
|
|
5001
5362
|
async function assertMoveMutationAllowed(root, params) {
|
|
5363
|
+
// Keep this preflight separate from the pinned resolutions in movePathFallback:
|
|
5364
|
+
// mutation denials must take precedence over source alias or identity failures.
|
|
5002
5365
|
const source = await resolvePathInRoot(root, params.fromRelative, {
|
|
5003
5366
|
aliasErrorCode: "path-alias",
|
|
5004
5367
|
allowFinalSymlink: true,
|
|
@@ -5020,40 +5383,29 @@ async function movePathFallback(root, params) {
|
|
|
5020
5383
|
relativePath: params.fromRelative,
|
|
5021
5384
|
policy: PATH_ALIAS_POLICIES.strict,
|
|
5022
5385
|
});
|
|
5023
|
-
const target = await
|
|
5024
|
-
aliasErrorCode: "path-alias",
|
|
5025
|
-
allowFinalSymlink: true,
|
|
5026
|
-
});
|
|
5027
|
-
await assertMutationNotDenied(target.resolved, params.denyMutations, { protectAncestors: true });
|
|
5028
|
-
await resolvePinnedRootPathInRoot(root, {
|
|
5386
|
+
const target = await resolveGuardedWritePathInRoot(root, {
|
|
5029
5387
|
relativePath: params.toRelative,
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
absolutePath: target.resolved,
|
|
5038
|
-
rootPath: target.rootReal,
|
|
5039
|
-
boundaryLabel: "root",
|
|
5040
|
-
});
|
|
5041
|
-
}
|
|
5042
|
-
catch (error) {
|
|
5043
|
-
throw new errors_FsSafeError("path-alias", "path alias escape blocked", {
|
|
5044
|
-
cause: error instanceof Error ? error : undefined,
|
|
5388
|
+
denyMutations: params.denyMutations,
|
|
5389
|
+
allowFinalSymlink: true,
|
|
5390
|
+
protectDeniedAncestors: true,
|
|
5391
|
+
shouldAssertNoPathAlias: async (resolvedTarget) => {
|
|
5392
|
+
await resolvePinnedRootPathInRoot(root, {
|
|
5393
|
+
relativePath: params.toRelative,
|
|
5394
|
+
policy: PATH_ALIAS_POLICIES.unlinkTarget,
|
|
5045
5395
|
});
|
|
5046
|
-
|
|
5047
|
-
|
|
5396
|
+
const targetStat = await promises_namespaceObject.lstat(resolvedTarget.resolved).catch(() => undefined);
|
|
5397
|
+
return !(process.platform !== "win32" &&
|
|
5398
|
+
params.overwrite &&
|
|
5399
|
+
targetStat?.isSymbolicLink() === true);
|
|
5400
|
+
},
|
|
5401
|
+
});
|
|
5048
5402
|
let sourceStat;
|
|
5049
5403
|
try {
|
|
5050
5404
|
sourceStat = await promises_namespaceObject.lstat(source.resolved);
|
|
5051
5405
|
}
|
|
5052
5406
|
catch (error) {
|
|
5053
5407
|
if (path_isNotFoundPathError(error)) {
|
|
5054
|
-
throw
|
|
5055
|
-
cause: error instanceof Error ? error : undefined,
|
|
5056
|
-
});
|
|
5408
|
+
throw fileNotFoundError(error instanceof Error ? error : undefined);
|
|
5057
5409
|
}
|
|
5058
5410
|
throw error;
|
|
5059
5411
|
}
|
|
@@ -5061,7 +5413,7 @@ async function movePathFallback(root, params) {
|
|
|
5061
5413
|
throw new errors_FsSafeError("symlink", "symlink not allowed");
|
|
5062
5414
|
}
|
|
5063
5415
|
if (sourceStat.isFile() && sourceStat.nlink > 1) {
|
|
5064
|
-
throw
|
|
5416
|
+
throw hardlinkedPathNotAllowedError();
|
|
5065
5417
|
}
|
|
5066
5418
|
if (!params.overwrite && sourceStat.isDirectory()) {
|
|
5067
5419
|
throw new errors_FsSafeError("invalid-path", "directory moves require overwrite: true");
|
|
@@ -5090,9 +5442,7 @@ async function movePathFallback(root, params) {
|
|
|
5090
5442
|
}
|
|
5091
5443
|
catch (error) {
|
|
5092
5444
|
if (path_isNotFoundPathError(error)) {
|
|
5093
|
-
throw
|
|
5094
|
-
cause: error instanceof Error ? error : undefined,
|
|
5095
|
-
});
|
|
5445
|
+
throw fileNotFoundError(error instanceof Error ? error : undefined);
|
|
5096
5446
|
}
|
|
5097
5447
|
if (hasNodeErrorCode(error, "EEXIST")) {
|
|
5098
5448
|
throw new errors_FsSafeError("already-exists", "destination exists", {
|
|
@@ -5158,30 +5508,22 @@ async function writeFileFallback(root, params) {
|
|
|
5158
5508
|
}
|
|
5159
5509
|
}
|
|
5160
5510
|
async function writeMissingFileFallback(root, params) {
|
|
5161
|
-
const { rootReal, resolved } = await
|
|
5162
|
-
|
|
5511
|
+
const { rootReal, resolved } = await resolveGuardedWritePathInRoot(root, {
|
|
5512
|
+
relativePath: params.relativePath,
|
|
5513
|
+
denyMutations: params.denyMutations,
|
|
5163
5514
|
});
|
|
5164
|
-
await assertMutationNotDenied(resolved, params.denyMutations);
|
|
5165
|
-
try {
|
|
5166
|
-
await assertNoPathAliasEscape({
|
|
5167
|
-
absolutePath: resolved,
|
|
5168
|
-
rootPath: rootReal,
|
|
5169
|
-
boundaryLabel: "root",
|
|
5170
|
-
});
|
|
5171
|
-
}
|
|
5172
|
-
catch (err) {
|
|
5173
|
-
throw new errors_FsSafeError("path-alias", "path alias escape blocked", { cause: err });
|
|
5174
|
-
}
|
|
5175
5515
|
const targetPath = params.mkdir === false
|
|
5176
5516
|
? resolved
|
|
5177
5517
|
: await prepareRootWriteTarget(rootReal, resolved);
|
|
5178
5518
|
const parentGuard = await directory_guard_createAsyncDirectoryGuard(external_node_path_namespaceObject.dirname(targetPath));
|
|
5179
5519
|
let created = false;
|
|
5520
|
+
let createdIdentity;
|
|
5180
5521
|
try {
|
|
5181
5522
|
const { handle, writtenStat } = await withAsyncDirectoryGuards([parentGuard], async () => {
|
|
5182
5523
|
const handle = await promises_namespaceObject.open(targetPath, OPEN_WRITE_CREATE_FLAGS, params.mode ?? 0o600);
|
|
5183
5524
|
created = true;
|
|
5184
5525
|
try {
|
|
5526
|
+
createdIdentity = await handle.stat();
|
|
5185
5527
|
if (typeof params.data === "string") {
|
|
5186
5528
|
await handle.writeFile(params.data, params.encoding ?? "utf8");
|
|
5187
5529
|
}
|
|
@@ -5217,8 +5559,8 @@ async function writeMissingFileFallback(root, params) {
|
|
|
5217
5559
|
throw err;
|
|
5218
5560
|
}
|
|
5219
5561
|
finally {
|
|
5220
|
-
if (created) {
|
|
5221
|
-
await
|
|
5562
|
+
if (created && createdIdentity) {
|
|
5563
|
+
await removePathIfIdentityUnchanged(targetPath, createdIdentity).catch(() => undefined);
|
|
5222
5564
|
}
|
|
5223
5565
|
}
|
|
5224
5566
|
}
|
|
@@ -16388,4 +16730,4 @@ if (installedChunkData !== 0) { // 0 means "already installed".'
|
|
|
16388
16730
|
// module factories are used so entry inlining is disabled
|
|
16389
16731
|
// startup
|
|
16390
16732
|
// Load entry module and return exports
|
|
16391
|
-
var __webpack_exports__ = __webpack_require__(
|
|
16733
|
+
var __webpack_exports__ = __webpack_require__(983);
|