@openclaw/fs-safe 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +4 -2
  3. package/dist/advanced.d.ts +1 -1
  4. package/dist/advanced.d.ts.map +1 -1
  5. package/dist/advanced.js +1 -1
  6. package/dist/archive-entry.d.ts.map +1 -1
  7. package/dist/archive-entry.js +4 -3
  8. package/dist/archive-gzip-tail.d.ts.map +1 -1
  9. package/dist/archive-gzip-tail.js +13 -9
  10. package/dist/archive-read.d.ts.map +1 -1
  11. package/dist/archive-read.js +37 -33
  12. package/dist/archive-zip-names.d.ts.map +1 -1
  13. package/dist/archive-zip-names.js +17 -5
  14. package/dist/bounded-read.js +2 -2
  15. package/dist/device-path.d.ts.map +1 -1
  16. package/dist/device-path.js +5 -3
  17. package/dist/file-hash.d.ts.map +1 -1
  18. package/dist/file-hash.js +9 -2
  19. package/dist/file-store-prune.d.ts.map +1 -1
  20. package/dist/file-store-prune.js +9 -1
  21. package/dist/filename.d.ts +1 -0
  22. package/dist/filename.d.ts.map +1 -1
  23. package/dist/filename.js +30 -13
  24. package/dist/guarded-mkdir.d.ts +2 -0
  25. package/dist/guarded-mkdir.d.ts.map +1 -1
  26. package/dist/guarded-mkdir.js +50 -15
  27. package/dist/guest.d.ts.map +1 -1
  28. package/dist/guest.js +12 -4
  29. package/dist/install-path.d.ts +6 -0
  30. package/dist/install-path.d.ts.map +1 -1
  31. package/dist/install-path.js +13 -0
  32. package/dist/json-durable-queue-ownership.d.ts +2 -0
  33. package/dist/json-durable-queue-ownership.d.ts.map +1 -1
  34. package/dist/json-durable-queue-ownership.js +47 -3
  35. package/dist/json-durable-queue-read.d.ts +6 -0
  36. package/dist/json-durable-queue-read.d.ts.map +1 -0
  37. package/dist/json-durable-queue-read.js +59 -0
  38. package/dist/json-durable-queue.d.ts +1 -1
  39. package/dist/json-durable-queue.d.ts.map +1 -1
  40. package/dist/json-durable-queue.js +37 -80
  41. package/dist/native-binding.d.ts +7 -0
  42. package/dist/native-binding.d.ts.map +1 -1
  43. package/dist/path-scope-lexical.d.ts +14 -0
  44. package/dist/path-scope-lexical.d.ts.map +1 -0
  45. package/dist/path-scope-lexical.js +27 -0
  46. package/dist/path.d.ts.map +1 -1
  47. package/dist/path.js +20 -2
  48. package/dist/pinned-write.js +1 -0
  49. package/dist/root-boundary.d.ts +25 -0
  50. package/dist/root-boundary.d.ts.map +1 -0
  51. package/dist/root-boundary.js +177 -0
  52. package/dist/root-context.d.ts.map +1 -1
  53. package/dist/root-context.js +32 -5
  54. package/dist/root-directory-list.d.ts.map +1 -1
  55. package/dist/root-directory-list.js +10 -2
  56. package/dist/root-impl.d.ts.map +1 -1
  57. package/dist/root-impl.js +53 -34
  58. package/dist/root-move-preflight.d.ts +8 -0
  59. package/dist/root-move-preflight.d.ts.map +1 -0
  60. package/dist/root-move-preflight.js +16 -0
  61. package/dist/root-path-existing.d.ts +2 -0
  62. package/dist/root-path-existing.d.ts.map +1 -1
  63. package/dist/root-path-existing.js +10 -2
  64. package/dist/root-path.d.ts +2 -0
  65. package/dist/root-path.d.ts.map +1 -1
  66. package/dist/root-path.js +54 -26
  67. package/dist/root-paths.d.ts +2 -6
  68. package/dist/root-paths.d.ts.map +1 -1
  69. package/dist/root-paths.js +14 -26
  70. package/dist/root-walk.d.ts.map +1 -1
  71. package/dist/root-walk.js +2 -1
  72. package/dist/root-write-mode.d.ts +2 -0
  73. package/dist/root-write-mode.d.ts.map +1 -1
  74. package/dist/root-write-mode.js +20 -7
  75. package/dist/root-write-verification.d.ts.map +1 -1
  76. package/dist/root-write-verification.js +12 -3
  77. package/dist/safe-path-segment.d.ts.map +1 -1
  78. package/dist/safe-path-segment.js +3 -1
  79. package/dist/secure-file-windows.d.ts +10 -0
  80. package/dist/secure-file-windows.d.ts.map +1 -0
  81. package/dist/secure-file-windows.js +186 -0
  82. package/dist/secure-file.d.ts.map +1 -1
  83. package/dist/secure-file.js +6 -3
  84. package/dist/sibling-staged-file.d.ts +1 -0
  85. package/dist/sibling-staged-file.d.ts.map +1 -1
  86. package/dist/sibling-staged-file.js +7 -0
  87. package/dist/sibling-temp.d.ts.map +1 -1
  88. package/dist/sibling-temp.js +2 -1
  89. package/dist/sidecar-lock-acquire.js +1 -1
  90. package/dist/sidecar-lock.d.ts.map +1 -1
  91. package/dist/sidecar-lock.js +2 -3
  92. package/dist/temp-target.d.ts.map +1 -1
  93. package/dist/temp-target.js +3 -1
  94. package/dist/walk.d.ts.map +1 -1
  95. package/dist/walk.js +11 -10
  96. package/docs/advanced.md +3 -1
  97. package/docs/archive.md +6 -0
  98. package/docs/contributing.md +4 -1
  99. package/docs/copy.md +1 -1
  100. package/docs/durability.md +4 -2
  101. package/docs/file-store.md +6 -0
  102. package/docs/filename.md +9 -2
  103. package/docs/guest.md +5 -0
  104. package/docs/install-path.md +59 -13
  105. package/docs/install.md +4 -1
  106. package/docs/native-helper.md +4 -2
  107. package/docs/native.md +14 -2
  108. package/docs/path.md +1 -1
  109. package/docs/permissions.md +26 -1
  110. package/docs/public-api.md +5 -0
  111. package/docs/root.md +3 -1
  112. package/docs/secure-file.md +15 -14
  113. package/docs/security-model.md +1 -1
  114. package/docs/store.md +22 -1
  115. package/docs/temp.md +4 -0
  116. package/package.json +8 -8
package/dist/root-impl.js CHANGED
@@ -69,15 +69,17 @@ import { runPinnedWriteHelper, runPinnedWriteWithRenamePolicy, } from "./pinned-
69
69
  import { getNativeBinding } from "./native.js";
70
70
  import { validatePinnedOperationPayload } from "./pinned-operation.js";
71
71
  import { assertNoPathAliasEscape, PATH_ALIAS_POLICIES } from "./path-policy.js";
72
- import { assertNoNulPathInput, assertNoUnsafeDeviceReadPath, hasNodeErrorCode, isNotFoundPathError, isPathInside, isSymlinkOpenError, } from "./path.js";
72
+ import { assertNoNulPathInput, assertNoUnsafeDeviceReadPath, hasNodeErrorCode, isNotFoundPathError, isSymlinkOpenError, } from "./path.js";
73
73
  import { readOpenedFileSafely } from "./read-opened-file.js";
74
74
  import { cleanupPinnedFilePath } from "./replace-file-temp-owner.js";
75
75
  import { resolveReadOpenFlags } from "./read-open-flags.js";
76
76
  import { realpathSync } from "./realpath.js";
77
77
  import { isNonRegularWriteOpenError, resolveNonblockingWriteFlag } from "./write-open-flags.js";
78
78
  import { resolveRootPath } from "./root-path.js";
79
+ import { admitPathInsideRoot } from "./root-boundary.js";
79
80
  import { openRootDirectoryListing, listDirectoryPath, pathStatFromStats } from "./root-directory-list.js";
80
81
  import { entriesInRoot } from "./root-entries.js";
82
+ import { assertMoveMutationAllowed } from "./root-move-preflight.js";
81
83
  import { assertRootIdentityCurrent, assertValidRootDestinationPath, assertValidRootRelativePath, ensureTrailingSep, expandRelativePathWithHome, resolvePathInRoot, resolveRootContext, rootRelativeReadPath, } from "./root-context.js";
82
84
  import { fileNotFoundError, hardlinkedPathNotAllowedError, isAlreadyExistsError, normalizePinnedPathError, normalizePinnedWriteError, outsideWorkspaceError, } from "./root-errors.js";
83
85
  import { getFsSafeTestHooks } from "./test-hooks.js";
@@ -478,7 +480,7 @@ export function rootFromDirectoryGuard(guard, defaults = {}) {
478
480
  }, defaults);
479
481
  }
480
482
  async function openFileInRoot(root, params) {
481
- const { rootWithSep, resolved } = await resolvePathInRoot(root, params.relativePath, {
483
+ const { resolved } = await resolvePathInRoot(root, params.relativePath, {
482
484
  allowFinalSymlink: true,
483
485
  rejectUnsafeDeviceReads: true,
484
486
  ...readSymlinkResolution(params.symlinks),
@@ -491,10 +493,16 @@ async function openFileInRoot(root, params) {
491
493
  await opened.handle.close().catch(() => { });
492
494
  throw hardlinkedPathNotAllowedError();
493
495
  }
494
- if (!isPathInside(rootWithSep, opened.realPath)) {
496
+ const admittedRealPath = admitPathInsideRoot({
497
+ rootPath: root.rootReal,
498
+ candidatePath: opened.realPath,
499
+ rootIdentity: root.rootIdentity,
500
+ });
501
+ if (!admittedRealPath) {
495
502
  await opened.handle.close().catch(() => { });
496
503
  throw outsideWorkspaceError();
497
504
  }
505
+ opened.realPath = admittedRealPath.path;
498
506
  return opened;
499
507
  }
500
508
  async function readFileInRoot(root, params) {
@@ -538,9 +546,10 @@ function buildAtomicWriteTempPath(targetPath) {
538
546
  function rootWriteQueueKey(root, relativePath) {
539
547
  return `${root.rootReal}\0${relativePath}`;
540
548
  }
541
- async function prepareRootWriteTarget(rootReal, targetPath, assertBeforeMutation) {
549
+ async function prepareRootWriteTarget(root, targetPath, assertBeforeMutation) {
542
550
  const parentPath = await mkdirPathComponentsWithGuards({
543
- rootReal,
551
+ rootReal: root.rootReal,
552
+ rootIdentity: root.rootIdentity,
544
553
  targetPath: path.dirname(targetPath),
545
554
  assertBeforeMutation,
546
555
  });
@@ -573,21 +582,26 @@ async function resolveGuardedWritePathInRoot(root, params) {
573
582
  return resolvedPath;
574
583
  }
575
584
  async function openWritableFileInRoot(root, params) {
576
- const { rootReal, rootWithSep, resolved } = await resolveGuardedWritePathInRoot(root, {
585
+ const { resolved } = await resolveGuardedWritePathInRoot(root, {
577
586
  relativePath: params.relativePath,
578
587
  denyMutations: params.denyMutations,
579
588
  mutationSymlinks: params.mutationSymlinks,
580
589
  });
581
590
  let ioPath = params.mkdir === false
582
591
  ? resolved
583
- : await prepareRootWriteTarget(rootReal, resolved, params.assertBeforeMutation);
592
+ : await prepareRootWriteTarget(root, resolved, params.assertBeforeMutation);
584
593
  try {
585
594
  assertFinalSymlinkRejected(ioPath, params.mutationSymlinks !== undefined);
586
595
  const resolvedRealPath = params.mutationSymlinks === undefined ? realpathSync.native(ioPath) : ioPath;
587
- if (!isPathInside(rootWithSep, resolvedRealPath)) {
596
+ const admittedRealPath = admitPathInsideRoot({
597
+ rootPath: root.rootReal,
598
+ candidatePath: resolvedRealPath,
599
+ rootIdentity: root.rootIdentity,
600
+ });
601
+ if (!admittedRealPath) {
588
602
  throw outsideWorkspaceError();
589
603
  }
590
- ioPath = resolvedRealPath;
604
+ ioPath = admittedRealPath.path;
591
605
  }
592
606
  catch (err) {
593
607
  if (err instanceof FsSafeError) {
@@ -657,8 +671,7 @@ async function openWritableFileInRoot(root, params) {
657
671
  throw err;
658
672
  }
659
673
  }
660
- const realPath = await resolveOpenedFileRealPathForHandle(handle, ioPath);
661
- realPathForCleanup = realPath;
674
+ let realPath = await resolveOpenedFileRealPathForHandle(handle, ioPath);
662
675
  const realStat = fsSync.statSync(realPath);
663
676
  if (!sameFileIdentity(stat, realStat)) {
664
677
  throw new FsSafeError("path-mismatch", "path mismatch");
@@ -666,9 +679,16 @@ async function openWritableFileInRoot(root, params) {
666
679
  if (realStat.nlink > 1) {
667
680
  throw hardlinkedPathNotAllowedError();
668
681
  }
669
- if (!isPathInside(rootWithSep, realPath)) {
682
+ const admittedRealPath = admitPathInsideRoot({
683
+ rootPath: root.rootReal,
684
+ candidatePath: realPath,
685
+ rootIdentity: root.rootIdentity,
686
+ });
687
+ if (!admittedRealPath) {
670
688
  throw outsideWorkspaceError();
671
689
  }
690
+ realPath = admittedRealPath.path;
691
+ realPathForCleanup = realPath;
672
692
  // Truncate only after boundary and identity checks complete. This avoids
673
693
  // irreversible side effects if a symlink target changes before validation.
674
694
  if (params.append !== true && params.truncateExisting !== false && !createdForWrite) {
@@ -785,7 +805,7 @@ async function mkdirPathInRoot(root, params) {
785
805
  validatePinnedOperationPayload({ relativePath: params.relativePath });
786
806
  const resolved = await resolvePinnedPathInRoot(root, params);
787
807
  try {
788
- await mkdirPathFallback(resolved, params.assertBeforeMutation, params.mutationSymlinks !== undefined);
808
+ await mkdirPathFallback(root, resolved, params.assertBeforeMutation, params.mutationSymlinks !== undefined);
789
809
  }
790
810
  catch (error) {
791
811
  throw normalizePinnedPathError(error);
@@ -948,8 +968,8 @@ async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode,
948
968
  denyMutations,
949
969
  mutationSymlinks,
950
970
  });
951
- // resolvePathInRoot already enforces isPathInside, so any actual escape
952
- // is rejected upstream.
971
+ // resolvePathInRoot already admits and rebases the path at the exact Root
972
+ // boundary, so any actual escape is rejected upstream.
953
973
  const relativeResolved = path.relative(rootReal, resolved);
954
974
  if (path.isAbsolute(relativeResolved)) {
955
975
  throw outsideWorkspaceError();
@@ -978,7 +998,12 @@ async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode,
978
998
  }
979
999
  }
980
1000
  const mode = overwrite
981
- ? await inheritWriteTargetMode({ targetPath: resolved, rootWithSep, requestedMode })
1001
+ ? await inheritWriteTargetMode({
1002
+ targetPath: resolved,
1003
+ rootWithSep,
1004
+ rootIdentity: root.rootIdentity,
1005
+ requestedMode,
1006
+ })
982
1007
  : requestedMode ?? 0o600;
983
1008
  return {
984
1009
  rootReal,
@@ -1017,9 +1042,15 @@ async function resolvePinnedOperationPathInRoot(root, params) {
1017
1042
  throw outsideWorkspaceError();
1018
1043
  }
1019
1044
  const relativePosix = relativeResolved.split(path.sep).join(path.posix.sep);
1020
- if (!isPathInside(resolved.rootWithSep, resolved.canonicalPath)) {
1045
+ const admittedCanonicalPath = admitPathInsideRoot({
1046
+ rootPath: resolved.rootReal,
1047
+ candidatePath: resolved.canonicalPath,
1048
+ rootIdentity: root.rootIdentity,
1049
+ });
1050
+ if (!admittedCanonicalPath) {
1021
1051
  throw outsideWorkspaceError();
1022
1052
  }
1053
+ resolved.canonicalPath = admittedCanonicalPath.path;
1023
1054
  await assertMutationNotDenied(resolved.canonicalPath, params.denyMutations, {
1024
1055
  protectAncestors: params.protectDenyMutationAncestors,
1025
1056
  });
@@ -1037,6 +1068,7 @@ async function resolvePinnedRootPathInRoot(root, params) {
1037
1068
  : `${ensureTrailingSep(rootReal)}${expandedPath}`,
1038
1069
  rootPath: rootReal,
1039
1070
  rootCanonicalPath: rootReal,
1071
+ rootIdentity: root.rootIdentity,
1040
1072
  boundaryLabel: "root",
1041
1073
  policy: params.policy,
1042
1074
  ...mutationSymlinkResolution(params.mutationSymlinks),
@@ -1054,9 +1086,10 @@ async function resolvePinnedRootPathInRoot(root, params) {
1054
1086
  canonicalPath: resolved.canonicalPath,
1055
1087
  };
1056
1088
  }
1057
- async function mkdirPathFallback(resolved, assertBeforeMutation, rejectSymlinks = false) {
1089
+ async function mkdirPathFallback(root, resolved, assertBeforeMutation, rejectSymlinks = false) {
1058
1090
  await mkdirPathComponentsWithGuards({
1059
1091
  rootReal: resolved.rootReal, targetPath: resolved.resolved, assertBeforeMutation,
1092
+ rootIdentity: root.rootIdentity,
1060
1093
  rejectSymlinks,
1061
1094
  beforeComponent: async (componentPath) => await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("mkdir", componentPath),
1062
1095
  });
@@ -1079,20 +1112,6 @@ async function listPathFallback(root, relativePath, withFileTypes) {
1079
1112
  const resolved = await resolvePinnedPathInRoot(root, { relativePath, allowRoot: true });
1080
1113
  return await listDirectoryPath(root, resolved.resolved, withFileTypes);
1081
1114
  }
1082
- async function assertMoveMutationAllowed(root, params) {
1083
- // Keep this preflight separate from the pinned resolutions in movePathFallback:
1084
- // mutation denials must take precedence over source alias or identity failures.
1085
- const source = await resolvePathInRoot(root, params.fromRelative, {
1086
- aliasErrorCode: "path-alias",
1087
- allowFinalSymlink: true,
1088
- });
1089
- await assertMutationNotDenied(source.resolved, params.denyMutations, { protectAncestors: true });
1090
- const target = await resolvePathInRoot(root, params.toRelative, {
1091
- aliasErrorCode: "path-alias",
1092
- allowFinalSymlink: true,
1093
- });
1094
- await assertMutationNotDenied(target.resolved, params.denyMutations, { protectAncestors: true });
1095
- }
1096
1115
  async function movePathFallback(root, params) {
1097
1116
  const source = await resolvePathInRoot(root, params.fromRelative, {
1098
1117
  aliasErrorCode: "path-alias",
@@ -1297,14 +1316,14 @@ async function writeFileFallback(root, params) {
1297
1316
  }
1298
1317
  }
1299
1318
  async function writeMissingFileFallback(root, params) {
1300
- const { rootReal, resolved } = await resolveGuardedWritePathInRoot(root, {
1319
+ const { resolved } = await resolveGuardedWritePathInRoot(root, {
1301
1320
  relativePath: params.relativePath,
1302
1321
  denyMutations: params.denyMutations,
1303
1322
  mutationSymlinks: params.mutationSymlinks,
1304
1323
  });
1305
1324
  const targetPath = params.mkdir === false
1306
1325
  ? resolved
1307
- : await prepareRootWriteTarget(rootReal, resolved, params.assertBeforeMutation);
1326
+ : await prepareRootWriteTarget(root, resolved, params.assertBeforeMutation);
1308
1327
  const parentGuard = await createAsyncDirectoryGuard(path.dirname(targetPath), { bigint: true });
1309
1328
  let created = false;
1310
1329
  let createdIdentity;
@@ -0,0 +1,8 @@
1
+ import { type DenyMutationPolicy } from "./deny-mutations.js";
2
+ import { type RootContext } from "./root-context.js";
3
+ export declare function assertMoveMutationAllowed(root: RootContext, params: {
4
+ fromRelative: string;
5
+ toRelative: string;
6
+ denyMutations?: DenyMutationPolicy;
7
+ }): Promise<void>;
8
+ //# sourceMappingURL=root-move-preflight.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"root-move-preflight.d.ts","sourceRoot":"","sources":["../src/root-move-preflight.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,kBAAkB,EACxB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAExE,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE;IACN,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,kBAAkB,CAAC;CACpC,GACA,OAAO,CAAC,IAAI,CAAC,CAaf"}
@@ -0,0 +1,16 @@
1
+ import { assertMutationNotDenied, } from "./deny-mutations.js";
2
+ import { resolvePathInRoot } from "./root-context.js";
3
+ export async function assertMoveMutationAllowed(root, params) {
4
+ // Keep this preflight separate from the pinned resolutions in movePathFallback:
5
+ // mutation denials must take precedence over source alias or identity failures.
6
+ const source = await resolvePathInRoot(root, params.fromRelative, {
7
+ aliasErrorCode: "path-alias",
8
+ allowFinalSymlink: true,
9
+ });
10
+ await assertMutationNotDenied(source.resolved, params.denyMutations, { protectAncestors: true });
11
+ const target = await resolvePathInRoot(root, params.toRelative, {
12
+ aliasErrorCode: "path-alias",
13
+ allowFinalSymlink: true,
14
+ });
15
+ await assertMutationNotDenied(target.resolved, params.denyMutations, { protectAncestors: true });
16
+ }
@@ -1,7 +1,9 @@
1
+ import { type RootBoundaryIdentity } from "./root-boundary.js";
1
2
  export declare function absolutePathWithRawSegments(candidate: string): string;
2
3
  export declare function rawPathRelativeToCanonicalRoot(candidate: string, rootCanonicalPath: string, options?: {
3
4
  rejectSymlinks?: boolean;
4
5
  rejectFinalSymlink?: boolean;
6
+ rootIdentity?: RootBoundaryIdentity;
5
7
  }): string | undefined;
6
8
  export declare function resolvePathViaExistingAncestor(targetPath: string): Promise<string>;
7
9
  export declare function resolvePathViaExistingAncestorSync(targetPath: string): string;
@@ -1 +1 @@
1
- {"version":3,"file":"root-path-existing.d.ts","sourceRoot":"","sources":["../src/root-path-existing.ts"],"names":[],"mappings":"AAOA,wBAAgB,2BAA2B,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAKrE;AAED,wBAAgB,8BAA8B,CAC5C,SAAS,EAAE,MAAM,EACjB,iBAAiB,EAAE,MAAM,EACzB,OAAO,GAAE;IAAE,cAAc,CAAC,EAAE,OAAO,CAAC;IAAC,kBAAkB,CAAC,EAAE,OAAO,CAAA;CAAO,GACvE,MAAM,GAAG,SAAS,CAuCpB;AAkBD,wBAAsB,8BAA8B,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA0BxF;AAED,wBAAgB,kCAAkC,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CA0B7E"}
1
+ {"version":3,"file":"root-path-existing.d.ts","sourceRoot":"","sources":["../src/root-path-existing.ts"],"names":[],"mappings":"AAMA,OAAO,EAAuB,KAAK,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAEpF,wBAAgB,2BAA2B,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAKrE;AAED,wBAAgB,8BAA8B,CAC5C,SAAS,EAAE,MAAM,EACjB,iBAAiB,EAAE,MAAM,EACzB,OAAO,GAAE;IACP,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,YAAY,CAAC,EAAE,oBAAoB,CAAC;CAChC,GACL,MAAM,GAAG,SAAS,CA8CpB;AAkBD,wBAAsB,8BAA8B,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA0BxF;AAED,wBAAgB,kCAAkC,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CA0B7E"}
@@ -4,6 +4,7 @@ import { FsSafeError } from "./errors.js";
4
4
  import { formatErrorDetail } from "./error-detail.js";
5
5
  import { isNotFoundPathError, isPathInside } from "./path.js";
6
6
  import { realpathSync } from "./realpath.js";
7
+ import { admitPathInsideRoot } from "./root-boundary.js";
7
8
  export function absolutePathWithRawSegments(candidate) {
8
9
  if (path.isAbsolute(candidate))
9
10
  return candidate;
@@ -19,6 +20,7 @@ export function rawPathRelativeToCanonicalRoot(candidate, rootCanonicalPath, opt
19
20
  const finalComponentIndex = segments.findLastIndex(segment => segment !== "" && segment !== ".");
20
21
  let prefix = filesystemRoot;
21
22
  let traversedSymlink = false;
23
+ const identityCache = process.platform === "win32" ? new Map() : undefined;
22
24
  for (let index = 0; index < segments.length; index += 1) {
23
25
  prefix += `${prefix.endsWith(path.sep) ? "" : path.sep}${segments[index]}`;
24
26
  let canonical;
@@ -47,12 +49,18 @@ export function rawPathRelativeToCanonicalRoot(candidate, rootCanonicalPath, opt
47
49
  return undefined;
48
50
  continue;
49
51
  }
50
- if (!isPathInside(rootCanonicalPath, canonical))
52
+ const admitted = admitPathInsideRoot({
53
+ rootPath: rootCanonicalPath,
54
+ candidatePath: canonical,
55
+ rootIdentity: options.rootIdentity,
56
+ identityCache,
57
+ });
58
+ if (!admitted)
51
59
  continue;
52
60
  if (options.rejectSymlinks && traversedSymlink) {
53
61
  throw new FsSafeError("symlink", "symlink path component not allowed");
54
62
  }
55
- return [path.relative(rootCanonicalPath, canonical), ...segments.slice(index + 1)]
63
+ return [admitted.relativePath, ...segments.slice(index + 1)]
56
64
  .filter(Boolean).join(path.sep);
57
65
  }
58
66
  return undefined;
@@ -1,3 +1,4 @@
1
+ import { type RootBoundaryIdentity } from "./root-boundary.js";
1
2
  export { resolvePathViaExistingAncestorSync } from "./root-path-existing.js";
2
3
  type RootPathIntent = "read" | "write" | "create" | "delete" | "stat";
3
4
  export type RootPathAliasPolicy = {
@@ -25,6 +26,7 @@ type ResolveRootPathParams = {
25
26
  rejectUnresolvedSymlinks?: boolean;
26
27
  skipLexicalRootCheck?: boolean;
27
28
  rootCanonicalPath?: string;
29
+ rootIdentity?: RootBoundaryIdentity;
28
30
  };
29
31
  type ResolvedRootPathKind = "missing" | "file" | "directory" | "symlink" | "other";
30
32
  export type ResolvedRootPath = {
@@ -1 +1 @@
1
- {"version":3,"file":"root-path.d.ts","sourceRoot":"","sources":["../src/root-path.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAE,kCAAkC,EAAE,MAAM,yBAAyB,CAAC;AAE7E,KAAK,cAAc,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEtE,MAAM,MAAM,mBAAmB,GAAG;IAChC,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC,CAAC;AAEF,eAAO,MAAM,wBAAwB;aACnC,MAAM;;;;aAIN,YAAY;;;;CAIJ,CAAC;AAEX,KAAK,qBAAqB,GAAG;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,KAAK,oBAAoB,GAAG,SAAS,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC;AAEnF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,oBAAoB,CAAC;CAC5B,CAAC;AAEF,wBAAsB,eAAe,CACnC,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,gBAAgB,CAAC,CAM3B;AAeD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,qBAAqB,GAAG,gBAAgB,CAMnF"}
1
+ {"version":3,"file":"root-path.d.ts","sourceRoot":"","sources":["../src/root-path.ts"],"names":[],"mappings":"AAeA,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,oBAAoB,CAAC;AAI5B,OAAO,EAAE,kCAAkC,EAAE,MAAM,yBAAyB,CAAC;AAE7E,KAAK,cAAc,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEtE,MAAM,MAAM,mBAAmB,GAAG;IAChC,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,2BAA2B,CAAC,EAAE,OAAO,CAAC;CACvC,CAAC;AAEF,eAAO,MAAM,wBAAwB;aACnC,MAAM;;;;aAIN,YAAY;;;;CAIJ,CAAC;AAEX,KAAK,qBAAqB,GAAG;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,YAAY,CAAC,EAAE,oBAAoB,CAAC;CACrC,CAAC;AAEF,KAAK,oBAAoB,GAAG,SAAS,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC;AAEnF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,oBAAoB,CAAC;CAC5B,CAAC;AAEF,wBAAsB,eAAe,CACnC,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,gBAAgB,CAAC,CAM3B;AAcD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,qBAAqB,GAAG,gBAAgB,CAMnF"}
package/dist/root-path.js CHANGED
@@ -2,8 +2,9 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { formatErrorDetail, shortPath } from "./error-detail.js";
4
4
  import { FsSafeError } from "./errors.js";
5
- import { assertNoNulPathInput, isNotFoundPathError, isPathInside, isPathRelativeEscape, } from "./path.js";
5
+ import { assertNoNulPathInput, isNotFoundPathError, isPathRelativeEscape, } from "./path.js";
6
6
  import { absolutePathWithRawSegments, rawPathRelativeToCanonicalRoot, resolvePathViaExistingAncestor, resolvePathViaExistingAncestorSync, } from "./root-path-existing.js";
7
+ import { admitPathInsideRoot, } from "./root-boundary.js";
7
8
  import { resolveSymlinkHopPath, resolveSymlinkHopPathSync } from "./root-path-symlink.js";
8
9
  import { assertNoDriveRelativePathSegments } from "./safe-path-segment.js";
9
10
  export { resolvePathViaExistingAncestorSync } from "./root-path-existing.js";
@@ -29,11 +30,10 @@ async function resolveRootPathInternal(params) {
29
30
  assertValidRootPathInputs(params);
30
31
  params = { ...params, absolutePath: absolutePathWithRawSegments(params.absolutePath) };
31
32
  const rootPath = path.resolve(params.rootPath);
32
- const absolutePath = path.resolve(params.absolutePath);
33
33
  const rootCanonicalPath = params.rootCanonicalPath
34
34
  ? path.resolve(params.rootCanonicalPath)
35
35
  : await resolvePathViaExistingAncestor(rootPath);
36
- return resolveRootPathLexicalAsync(prepareRootTraversal(params, rootPath, rootCanonicalPath, absolutePath));
36
+ return resolveRootPathLexicalAsync(prepareRootTraversal(params, rootPath, rootCanonicalPath));
37
37
  }
38
38
  export function resolveRootPathSync(params) {
39
39
  try {
@@ -47,25 +47,38 @@ function resolveRootPathSyncInternal(params) {
47
47
  assertValidRootPathInputs(params);
48
48
  params = { ...params, absolutePath: absolutePathWithRawSegments(params.absolutePath) };
49
49
  const rootPath = path.resolve(params.rootPath);
50
- const absolutePath = path.resolve(params.absolutePath);
51
50
  const rootCanonicalPath = params.rootCanonicalPath
52
51
  ? path.resolve(params.rootCanonicalPath)
53
52
  : resolvePathViaExistingAncestorSync(rootPath);
54
- return resolveRootPathLexicalSync(prepareRootTraversal(params, rootPath, rootCanonicalPath, absolutePath));
53
+ return resolveRootPathLexicalSync(prepareRootTraversal(params, rootPath, rootCanonicalPath));
55
54
  }
56
- function prepareRootTraversal(params, rootPath, rootCanonicalPath, absolutePath) {
55
+ function prepareRootTraversal(params, rootPath, rootCanonicalPath) {
56
+ const callerAbsolutePath = path.resolve(params.absolutePath);
57
57
  let raw = params.absolutePath;
58
- if (rawPathRelativeToRoot(rootPath, raw) === undefined) {
59
- const relative = rawPathRelativeToRoot(rootCanonicalPath, raw)
58
+ let trustedAbsolutePath = false;
59
+ const direct = admitRawPathInsideRoot(rootPath, raw, params.rootIdentity);
60
+ if (direct) {
61
+ raw = direct.path;
62
+ trustedAbsolutePath = direct.admission === "identity";
63
+ }
64
+ else {
65
+ const canonical = admitRawPathInsideRoot(rootCanonicalPath, raw, params.rootIdentity);
66
+ const relative = canonical?.relativePath
60
67
  ?? rawPathRelativeToCanonicalRoot(raw, rootCanonicalPath, params);
61
68
  if (relative === undefined) {
62
69
  throw pathEscapeError({ rootPath, absolutePath: raw, boundaryLabel: params.boundaryLabel });
63
70
  }
64
- raw = `${rootPath}${path.sep}${relative}`;
71
+ trustedAbsolutePath = canonical?.admission === "identity";
72
+ raw = relative === "" ? rootPath : `${rootPath}${path.sep}${relative}`;
65
73
  }
66
74
  return {
67
75
  params: raw === params.absolutePath ? params : { ...params, absolutePath: raw },
68
- rootPath, rootCanonicalPath, absolutePath,
76
+ rootPath,
77
+ rootCanonicalPath,
78
+ // Preserve the caller-spelling receipt for ordinary aliases. Only a
79
+ // Windows case-fold admission must return the identity-gated Root spelling,
80
+ // because downstream I/O must not reuse the ambiguous caller prefix.
81
+ absolutePath: trustedAbsolutePath ? path.resolve(raw) : callerAbsolutePath,
69
82
  };
70
83
  }
71
84
  function sanitizeRootPathError(error) {
@@ -93,7 +106,7 @@ function assertNoEmbeddedDriveRelativeSegment(filePath, label) {
93
106
  }
94
107
  function createLexicalTraversalState(params) {
95
108
  const rawAbsolutePath = params.params.absolutePath;
96
- const relative = rawPathRelativeToRoot(params.rootPath, rawAbsolutePath);
109
+ const relative = admitRawPathInsideRoot(params.rootPath, rawAbsolutePath, params.params.rootIdentity)?.relativePath;
97
110
  if (relative === undefined)
98
111
  throw new Error("Path traversal must begin at the root");
99
112
  const segments = splitTraversalSegments(relative);
@@ -124,7 +137,7 @@ function splitTraversalSegments(value) {
124
137
  segments.push(".");
125
138
  return segments;
126
139
  }
127
- function rawPathRelativeToRoot(rootPath, candidatePath) {
140
+ function admitRawPathInsideRoot(rootPath, candidatePath, rootIdentity) {
128
141
  if (!path.isAbsolute(candidatePath)) {
129
142
  return undefined;
130
143
  }
@@ -132,30 +145,36 @@ function rawPathRelativeToRoot(rootPath, candidatePath) {
132
145
  const candidate = process.platform === "win32"
133
146
  ? candidatePath.replaceAll("/", path.sep)
134
147
  : candidatePath;
148
+ if (process.platform === "win32") {
149
+ return admitPathInsideRoot({ rootPath: root, candidatePath: candidate, rootIdentity });
150
+ }
135
151
  if (candidate === root) {
136
- return "";
152
+ return { admission: "exact", path: candidate, relativePath: "" };
137
153
  }
138
154
  const rootWithSep = root.endsWith(path.sep) ? root : `${root}${path.sep}`;
139
155
  const candidatePrefix = candidate.slice(0, rootWithSep.length);
140
- const prefixMatches = process.platform === "win32"
141
- ? candidatePrefix.toLowerCase() === rootWithSep.toLowerCase()
142
- : candidatePrefix === rootWithSep;
143
- return prefixMatches ? candidate.slice(rootWithSep.length) : undefined;
156
+ return candidatePrefix === rootWithSep
157
+ ? {
158
+ admission: "exact",
159
+ path: candidate,
160
+ relativePath: candidate.slice(rootWithSep.length),
161
+ }
162
+ : undefined;
144
163
  }
145
164
  function assertLexicalCursorInsideBoundary(context, candidatePath) {
146
- assertInsideBoundary({
165
+ return assertInsideBoundary({
147
166
  boundaryLabel: context.resolveParams.boundaryLabel,
148
167
  rootCanonicalPath: context.rootCanonicalPath,
168
+ rootIdentity: context.resolveParams.rootIdentity,
149
169
  candidatePath,
150
170
  absolutePath: context.absolutePath,
151
171
  });
152
172
  }
153
173
  function advanceCanonicalCursorForSegment(context, segment) {
154
- context.state.canonicalCursor = path.resolve(context.state.canonicalCursor, segment);
155
- assertLexicalCursorInsideBoundary(context, context.state.canonicalCursor);
174
+ context.state.canonicalCursor = assertLexicalCursorInsideBoundary(context, path.resolve(context.state.canonicalCursor, segment));
156
175
  }
157
176
  function finalizeLexicalResolution(context, kind) {
158
- assertLexicalCursorInsideBoundary(context, context.state.canonicalCursor);
177
+ context.state.canonicalCursor = assertLexicalCursorInsideBoundary(context, context.state.canonicalCursor);
159
178
  return buildResolvedRootPath({
160
179
  absolutePath: context.absolutePath,
161
180
  canonicalPath: context.state.canonicalCursor,
@@ -181,15 +200,19 @@ function lexicalStatDisposition(params) {
181
200
  return params.allowFinalSymlink && params.isLast ? "break" : "resolve-link";
182
201
  }
183
202
  function applyResolvedSymlinkHop(context, linkCanonical) {
184
- if (!isPathInside(context.rootCanonicalPath, linkCanonical)) {
203
+ let admitted;
204
+ try {
205
+ admitted = assertLexicalCursorInsideBoundary(context, linkCanonical);
206
+ }
207
+ catch {
185
208
  throw symlinkEscapeError({
186
209
  boundaryLabel: context.resolveParams.boundaryLabel,
187
210
  rootCanonicalPath: context.rootCanonicalPath,
188
211
  symlinkPath: context.state.lexicalCursor,
189
212
  });
190
213
  }
191
- context.state.canonicalCursor = linkCanonical;
192
- context.state.lexicalCursor = linkCanonical;
214
+ context.state.canonicalCursor = admitted;
215
+ context.state.lexicalCursor = admitted;
193
216
  }
194
217
  function applyParentTraversalStep(context) {
195
218
  context.state.lexicalCursor = path.resolve(context.state.lexicalCursor, "..");
@@ -380,8 +403,13 @@ function relativeInsideRoot(rootPath, targetPath) {
380
403
  return relative;
381
404
  }
382
405
  function assertInsideBoundary(params) {
383
- if (isPathInside(params.rootCanonicalPath, params.candidatePath)) {
384
- return;
406
+ const admitted = admitPathInsideRoot({
407
+ rootPath: params.rootCanonicalPath,
408
+ candidatePath: params.candidatePath,
409
+ rootIdentity: params.rootIdentity,
410
+ });
411
+ if (admitted) {
412
+ return admitted.path;
385
413
  }
386
414
  throw new Error(`Path resolves outside ${params.boundaryLabel} (${shortPath(params.rootCanonicalPath)}): ${shortPath(params.absolutePath)}`);
387
415
  }
@@ -1,4 +1,5 @@
1
1
  import { FsSafeError } from "./errors.js";
2
+ import { type ResolvePathWithinRootParams } from "./path-scope-lexical.js";
2
3
  type InvalidPathResult = {
3
4
  ok: false;
4
5
  error: string;
@@ -50,12 +51,7 @@ export type PathScope = {
50
51
  mode?: number;
51
52
  }): Promise<DirectoryResult>;
52
53
  };
53
- export declare function resolvePathWithinRoot(params: {
54
- rootDir: string;
55
- requestedPath: string;
56
- scopeLabel: string;
57
- defaultFileName?: string;
58
- }): {
54
+ export declare function resolvePathWithinRoot(params: ResolvePathWithinRootParams): {
59
55
  ok: true;
60
56
  path: string;
61
57
  } | {
@@ -1 +1 @@
1
- {"version":3,"file":"root-paths.d.ts","sourceRoot":"","sources":["../src/root-paths.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"root-paths.d.ts","sourceRoot":"","sources":["../src/root-paths.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAW1C,OAAO,EAAmC,KAAK,2BAA2B,EAAE,MAAM,yBAAyB,CAAC;AAE5G,KAAK,iBAAiB,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AACtD,KAAK,eAAe,GAChB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC1B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,WAAW,CAAA;CAAE,CAAC;AAC3D,KAAK,4BAA4B,GAAG;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AACF,KAAK,4BAA4B,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,iBAAiB,CAAC;AACtF,MAAM,MAAM,uBAAuB,GAAG;IACpC,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AACF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AACF,MAAM,MAAM,SAAS,GAAG;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CACL,aAAa,EAAE,MAAM,EACrB,OAAO,CAAC,EAAE,uBAAuB,GAChC;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7D,UAAU,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,4BAA4B,CAAC;IACnE,QAAQ,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,4BAA4B,CAAC,CAAC;IAC1E,KAAK,CAAC,cAAc,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,4BAA4B,CAAC,CAAC;IACvE,QAAQ,CACN,aAAa,EAAE,MAAM,EACrB,OAAO,CAAC,EAAE,uBAAuB,GAChC,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACtE,SAAS,CACP,aAAa,EAAE,MAAM,EACrB,OAAO,CAAC,EAAE,uBAAuB,GAAG;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GACpD,OAAO,CAAC,eAAe,CAAC,CAAC;CAC7B,CAAC;AAuDF,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,2BAA2B,GAClC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAE3D;AAED,wBAAsB,6BAA6B,CAAC,MAAM,EAAE;IAC1D,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAiCrE;AAuDD,wBAAsB,yBAAyB,CAAC,MAAM,EAAE;IACtD,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GAAG,OAAO,CAAC,eAAe,CAAC,CA2E3B;AAED,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,4BAA4B,GACnC,4BAA4B,CAwB9B;AAED,wBAAsB,8BAA8B,CAClD,MAAM,EAAE,4BAA4B,GACnC,OAAO,CAAC,4BAA4B,CAAC,CAEvC;AAED,wBAAsB,oCAAoC,CACxD,MAAM,EAAE,4BAA4B,GACnC,OAAO,CAAC,4BAA4B,CAAC,CAEvC;AAED,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,GAAG,SAAS,CAwC/E"}
@@ -6,19 +6,13 @@ import { FsSafeError } from "./errors.js";
6
6
  import { assertNoNulPathInput, hasNodeErrorCode, isNodeError, isNotFoundPathError, isPathInside, isPathRelativeEscape, } from "./path.js";
7
7
  import { root as openRoot } from "./root.js";
8
8
  import { realpathSync } from "./realpath.js";
9
+ import { resolvePathWithinNormalizedRoot } from "./path-scope-lexical.js";
9
10
  function invalidPath(scopeLabel) {
10
11
  return {
11
12
  ok: false,
12
13
  error: `Invalid path: must stay within ${scopeLabel}`,
13
14
  };
14
15
  }
15
- function pathStaysWithinRoot(rootDir, candidatePath) {
16
- if (process.platform !== "win32") {
17
- return candidatePath !== rootDir && isPathInside(rootDir, candidatePath);
18
- }
19
- const relative = path.relative(rootDir, candidatePath);
20
- return Boolean(relative) && !isPathRelativeEscape(relative);
21
- }
22
16
  async function resolveRealPathIfExists(targetPath) {
23
17
  try {
24
18
  return realpathSync.native(targetPath);
@@ -62,23 +56,7 @@ async function validateCanonicalPathWithinRoot(params) {
62
56
  }
63
57
  }
64
58
  export function resolvePathWithinRoot(params) {
65
- const root = path.resolve(params.rootDir);
66
- const raw = params.requestedPath.trim();
67
- if (!raw) {
68
- if (!params.defaultFileName) {
69
- return { ok: false, error: "path is required" };
70
- }
71
- const defaultPath = path.resolve(root, params.defaultFileName);
72
- if (!pathStaysWithinRoot(root, defaultPath)) {
73
- return { ok: false, error: `Invalid path: must stay within ${params.scopeLabel}` };
74
- }
75
- return { ok: true, path: defaultPath };
76
- }
77
- const resolved = path.resolve(root, raw);
78
- if (!pathStaysWithinRoot(root, resolved)) {
79
- return { ok: false, error: `Invalid path: must stay within ${params.scopeLabel}` };
80
- }
81
- return { ok: true, path: resolved };
59
+ return resolvePathWithinNormalizedRoot(params, path.resolve(params.rootDir));
82
60
  }
83
61
  export async function resolveWritablePathWithinRoot(params) {
84
62
  const lexical = resolvePathWithinRoot(params);
@@ -233,12 +211,22 @@ export async function ensureDirectoryWithinRoot(params) {
233
211
  }
234
212
  export function resolvePathsWithinRoot(params) {
235
213
  const resolvedPaths = [];
214
+ let previousRootDir;
215
+ let previousResolvedRoot = "";
236
216
  for (const raw of params.requestedPaths) {
237
- const pathResult = resolvePathWithinRoot({
217
+ const request = {
238
218
  rootDir: params.rootDir,
239
219
  requestedPath: raw,
240
220
  scopeLabel: params.scopeLabel,
241
- });
221
+ };
222
+ // Relative roots may follow cwd changes made by an input iterator or getter.
223
+ const resolvedRoot = process.platform !== "win32" && typeof request.rootDir === "string" &&
224
+ request.rootDir.startsWith("/") && request.rootDir === previousRootDir
225
+ ? previousResolvedRoot
226
+ : path.resolve(request.rootDir);
227
+ previousRootDir = request.rootDir;
228
+ previousResolvedRoot = resolvedRoot;
229
+ const pathResult = resolvePathWithinNormalizedRoot(request, resolvedRoot);
242
230
  if (!pathResult.ok) {
243
231
  return { ok: false, error: pathResult.error };
244
232
  }
@@ -1 +1 @@
1
- {"version":3,"file":"root-walk.d.ts","sourceRoot":"","sources":["../src/root-walk.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAC;AAClG,OAAO,KAAK,EAAY,QAAQ,EAAE,MAAM,YAAY,CAAC;AAGrD,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,oBAAoB,CAAC;AAClE,MAAM,MAAM,qBAAqB,GAAG,UAAU,GAAG,OAAO,CAAC;AACzD,MAAM,MAAM,8BAA8B,GAAG,OAAO,GAAG,iBAAiB,CAAC;AACzE,MAAM,MAAM,yBAAyB,GAAG,SAAS,GAAG,MAAM,GAAG,cAAc,CAAC;AAC5E,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,WAAW,GAAG,OAAO,CAAC;AACnE,MAAM,MAAM,iBAAiB,GAAG,qBAAqB,GAAG,iBAAiB,GAAG,WAAW,CAAC;AAExF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,qBAAqB,CAAC;IAC5B,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,aAAa,GACrB,iBAAiB,GACjB;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,CAAC,CAAA;CAAE,GACpD;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,iBAAiB,CAAC;IAAC,IAAI,EAAE,CAAC,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC;AAE/E,MAAM,MAAM,mBAAmB,GAAG,CAAC,KAAK,EAAE,iBAAiB,KAAK,yBAAyB,CAAC;AAE1F,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,QAAQ,GAAG,YAAY,CAAC;IAChC,aAAa,EAAE,qBAAqB,CAAC;IACrC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,aAAa,CAAC,EAAE,qBAAqB,CAAC;IACtC,WAAW,CAAC,EAAE,mBAAmB,CAAC;IAClC,gBAAgB,CAAC,EAAE,8BAA8B,CAAC;CACnD,CAAC;AAEF,KAAK,kBAAkB,GAAG;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC9C,IAAI,CACF,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,2BAA2B,GACnC,OAAO,CAAC,oBAAoB,CAAC,CAAC;CAClC,CAAC;AAqBF,wBAAuB,QAAQ,CAC7B,IAAI,EAAE,kBAAkB,EACxB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,eAAe,GACvB,cAAc,CAAC,aAAa,CAAC,CAqK/B"}
1
+ {"version":3,"file":"root-walk.d.ts","sourceRoot":"","sources":["../src/root-walk.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAC;AAClG,OAAO,KAAK,EAAY,QAAQ,EAAE,MAAM,YAAY,CAAC;AAGrD,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,oBAAoB,CAAC;AAClE,MAAM,MAAM,qBAAqB,GAAG,UAAU,GAAG,OAAO,CAAC;AACzD,MAAM,MAAM,8BAA8B,GAAG,OAAO,GAAG,iBAAiB,CAAC;AACzE,MAAM,MAAM,yBAAyB,GAAG,SAAS,GAAG,MAAM,GAAG,cAAc,CAAC;AAC5E,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,WAAW,GAAG,OAAO,CAAC;AACnE,MAAM,MAAM,iBAAiB,GAAG,qBAAqB,GAAG,iBAAiB,GAAG,WAAW,CAAC;AAExF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,qBAAqB,CAAC;IAC5B,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,aAAa,GACrB,iBAAiB,GACjB;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,CAAC,CAAA;CAAE,GACpD;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,iBAAiB,CAAC;IAAC,IAAI,EAAE,CAAC,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC;AAE/E,MAAM,MAAM,mBAAmB,GAAG,CAAC,KAAK,EAAE,iBAAiB,KAAK,yBAAyB,CAAC;AAE1F,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,QAAQ,GAAG,YAAY,CAAC;IAChC,aAAa,EAAE,qBAAqB,CAAC;IACrC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,aAAa,CAAC,EAAE,qBAAqB,CAAC;IACtC,WAAW,CAAC,EAAE,mBAAmB,CAAC;IAClC,gBAAgB,CAAC,EAAE,8BAA8B,CAAC;CACnD,CAAC;AAEF,KAAK,kBAAkB,GAAG;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC9C,IAAI,CACF,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,2BAA2B,GACnC,OAAO,CAAC,oBAAoB,CAAC,CAAC;CAClC,CAAC;AAqBF,wBAAuB,QAAQ,CAC7B,IAAI,EAAE,kBAAkB,EACxB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,eAAe,GACvB,cAAc,CAAC,aAAa,CAAC,CAsK/B"}
package/dist/root-walk.js CHANGED
@@ -98,6 +98,7 @@ export async function* walkRoot(root, relativePath, options) {
98
98
  let failed = false;
99
99
  let operationError;
100
100
  try {
101
+ const relativeDirectory = directory.split(path.sep).join(path.posix.sep);
101
102
  while (true) {
102
103
  let next;
103
104
  try {
@@ -112,7 +113,7 @@ export async function* walkRoot(root, relativePath, options) {
112
113
  return;
113
114
  const name = next.kind === "entry" ? next.entry.name : next.name;
114
115
  const child = directory
115
- ? path.posix.join(directory.split(path.sep).join(path.posix.sep), name)
116
+ ? path.posix.join(relativeDirectory, name)
116
117
  : name;
117
118
  if (next.kind === "limit") {
118
119
  yield onLimit(child);
@@ -1,6 +1,8 @@
1
+ import { type RootBoundaryIdentity } from "./root-boundary.js";
1
2
  export declare function inheritWriteTargetMode(params: {
2
3
  targetPath: string;
3
4
  rootWithSep: string;
5
+ rootIdentity?: RootBoundaryIdentity;
4
6
  requestedMode?: number;
5
7
  }): Promise<number>;
6
8
  //# sourceMappingURL=root-write-mode.d.ts.map