@openclaw/fs-safe 0.10.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 (302) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/LICENSE +1 -0
  3. package/README.md +39 -6
  4. package/dist/absolute-path.d.ts.map +1 -1
  5. package/dist/absolute-path.js +3 -2
  6. package/dist/advanced.d.ts +5 -1
  7. package/dist/advanced.d.ts.map +1 -1
  8. package/dist/advanced.js +5 -1
  9. package/dist/archive-durability.d.ts +6 -6
  10. package/dist/archive-durability.d.ts.map +1 -1
  11. package/dist/archive-durability.js +1 -1
  12. package/dist/archive-entry.d.ts.map +1 -1
  13. package/dist/archive-entry.js +8 -8
  14. package/dist/archive-gzip-tail.d.ts +1 -0
  15. package/dist/archive-gzip-tail.d.ts.map +1 -1
  16. package/dist/archive-gzip-tail.js +16 -9
  17. package/dist/archive-input.d.ts.map +1 -1
  18. package/dist/archive-input.js +4 -2
  19. package/dist/archive-merge.d.ts +5 -1
  20. package/dist/archive-merge.d.ts.map +1 -1
  21. package/dist/archive-merge.js +15 -12
  22. package/dist/archive-native.js +4 -4
  23. package/dist/archive-parser.wasm +0 -0
  24. package/dist/archive-read.d.ts.map +1 -1
  25. package/dist/archive-read.js +54 -42
  26. package/dist/archive-staging.d.ts +6 -3
  27. package/dist/archive-staging.d.ts.map +1 -1
  28. package/dist/archive-staging.js +42 -22
  29. package/dist/archive-tar-stream.d.ts.map +1 -1
  30. package/dist/archive-tar-stream.js +7 -6
  31. package/dist/archive-tar-wasm.d.ts.map +1 -1
  32. package/dist/archive-tar-wasm.js +16 -13
  33. package/dist/archive-zip-admission.d.ts +1 -1
  34. package/dist/archive-zip-admission.d.ts.map +1 -1
  35. package/dist/archive-zip-admission.js +48 -12
  36. package/dist/archive-zip-loader.d.ts +6 -0
  37. package/dist/archive-zip-loader.d.ts.map +1 -0
  38. package/dist/archive-zip-loader.js +38 -0
  39. package/dist/archive-zip-names.d.ts.map +1 -1
  40. package/dist/archive-zip-names.js +26 -9
  41. package/dist/archive-zip-preflight.d.ts +2 -3
  42. package/dist/archive-zip-preflight.d.ts.map +1 -1
  43. package/dist/archive-zip-preflight.js +2 -34
  44. package/dist/archive.d.ts.map +1 -1
  45. package/dist/archive.js +12 -10
  46. package/dist/bounded-read.d.ts +5 -0
  47. package/dist/bounded-read.d.ts.map +1 -1
  48. package/dist/bounded-read.js +18 -11
  49. package/dist/copy-file-input.d.ts +0 -1
  50. package/dist/copy-file-input.d.ts.map +1 -1
  51. package/dist/copy-file-input.js +4 -26
  52. package/dist/copy-tree-portable.d.ts.map +1 -1
  53. package/dist/copy-tree-portable.js +57 -26
  54. package/dist/copy.d.ts +1 -1
  55. package/dist/copy.d.ts.map +1 -1
  56. package/dist/copy.js +3 -1
  57. package/dist/device-path.d.ts.map +1 -1
  58. package/dist/device-path.js +5 -3
  59. package/dist/directory-durability.d.ts.map +1 -1
  60. package/dist/directory-durability.js +5 -4
  61. package/dist/directory-guard.d.ts +11 -1
  62. package/dist/directory-guard.d.ts.map +1 -1
  63. package/dist/directory-guard.js +53 -11
  64. package/dist/durability.d.ts +1 -1
  65. package/dist/durability.d.ts.map +1 -1
  66. package/dist/durability.js +1 -1
  67. package/dist/file-handle-transfer.d.ts +14 -0
  68. package/dist/file-handle-transfer.d.ts.map +1 -0
  69. package/dist/file-handle-transfer.js +64 -0
  70. package/dist/file-hash.d.ts +3 -0
  71. package/dist/file-hash.d.ts.map +1 -1
  72. package/dist/file-hash.js +99 -32
  73. package/dist/file-lock-sync.d.ts.map +1 -1
  74. package/dist/file-lock-sync.js +8 -4
  75. package/dist/file-store-boundary.d.ts.map +1 -1
  76. package/dist/file-store-boundary.js +7 -5
  77. package/dist/file-store-path.d.ts +3 -0
  78. package/dist/file-store-path.d.ts.map +1 -0
  79. package/dist/file-store-path.js +27 -0
  80. package/dist/file-store-prune.d.ts.map +1 -1
  81. package/dist/file-store-prune.js +15 -5
  82. package/dist/file-store-sync-write.d.ts.map +1 -1
  83. package/dist/file-store-sync-write.js +56 -44
  84. package/dist/file-store.d.ts.map +1 -1
  85. package/dist/file-store.js +2 -18
  86. package/dist/filename.d.ts +1 -0
  87. package/dist/filename.d.ts.map +1 -1
  88. package/dist/filename.js +32 -14
  89. package/dist/guarded-mkdir.d.ts +2 -0
  90. package/dist/guarded-mkdir.d.ts.map +1 -1
  91. package/dist/guarded-mkdir.js +52 -16
  92. package/dist/guest-dispatch-python.d.ts +2 -0
  93. package/dist/guest-dispatch-python.d.ts.map +1 -0
  94. package/dist/guest-dispatch-python.js +117 -0
  95. package/dist/guest-native-python.d.ts +4 -0
  96. package/dist/guest-native-python.d.ts.map +1 -0
  97. package/dist/guest-native-python.js +135 -0
  98. package/dist/guest.d.ts +9 -0
  99. package/dist/guest.d.ts.map +1 -0
  100. package/dist/guest.js +421 -0
  101. package/dist/index.d.ts +1 -1
  102. package/dist/index.d.ts.map +1 -1
  103. package/dist/install-path.d.ts +6 -0
  104. package/dist/install-path.d.ts.map +1 -1
  105. package/dist/install-path.js +16 -2
  106. package/dist/json-durable-queue-directory.js +3 -3
  107. package/dist/json-durable-queue-ownership.d.ts +2 -0
  108. package/dist/json-durable-queue-ownership.d.ts.map +1 -1
  109. package/dist/json-durable-queue-ownership.js +47 -3
  110. package/dist/json-durable-queue-read.d.ts +6 -0
  111. package/dist/json-durable-queue-read.d.ts.map +1 -0
  112. package/dist/json-durable-queue-read.js +59 -0
  113. package/dist/json-durable-queue.d.ts +1 -1
  114. package/dist/json-durable-queue.d.ts.map +1 -1
  115. package/dist/json-durable-queue.js +43 -84
  116. package/dist/json.d.ts.map +1 -1
  117. package/dist/json.js +2 -1
  118. package/dist/local-roots.d.ts.map +1 -1
  119. package/dist/local-roots.js +2 -1
  120. package/dist/move-path-stage.d.ts.map +1 -1
  121. package/dist/move-path-stage.js +2 -1
  122. package/dist/move-path.d.ts.map +1 -1
  123. package/dist/move-path.js +4 -3
  124. package/dist/mutation-authority.d.ts +1 -0
  125. package/dist/mutation-authority.d.ts.map +1 -1
  126. package/dist/mutation-authority.js +4 -4
  127. package/dist/native-binding.d.ts +14 -2
  128. package/dist/native-binding.d.ts.map +1 -1
  129. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  130. package/dist/native-pinned-write-windows.js +3 -2
  131. package/dist/native-pinned-write.d.ts.map +1 -1
  132. package/dist/native-pinned-write.js +2 -1
  133. package/dist/opened-realpath.d.ts.map +1 -1
  134. package/dist/opened-realpath.js +5 -4
  135. package/dist/output.d.ts +2 -0
  136. package/dist/output.d.ts.map +1 -1
  137. package/dist/output.js +2 -0
  138. package/dist/overwrite-file-handle.d.ts +8 -0
  139. package/dist/overwrite-file-handle.d.ts.map +1 -0
  140. package/dist/overwrite-file-handle.js +42 -0
  141. package/dist/path-case.d.ts +7 -0
  142. package/dist/path-case.d.ts.map +1 -0
  143. package/dist/path-case.js +136 -0
  144. package/dist/path-scope-lexical.d.ts +14 -0
  145. package/dist/path-scope-lexical.d.ts.map +1 -0
  146. package/dist/path-scope-lexical.js +27 -0
  147. package/dist/path.d.ts.map +1 -1
  148. package/dist/path.js +22 -3
  149. package/dist/permissions-windows.d.ts +1 -1
  150. package/dist/permissions-windows.d.ts.map +1 -1
  151. package/dist/permissions-windows.js +48 -6
  152. package/dist/pinned-open.d.ts.map +1 -1
  153. package/dist/pinned-open.js +3 -1
  154. package/dist/pinned-write.d.ts +2 -2
  155. package/dist/pinned-write.d.ts.map +1 -1
  156. package/dist/pinned-write.js +3 -1
  157. package/dist/private-temp-workspace.d.ts.map +1 -1
  158. package/dist/private-temp-workspace.js +6 -4
  159. package/dist/realpath.d.ts +4 -0
  160. package/dist/realpath.d.ts.map +1 -0
  161. package/dist/realpath.js +43 -0
  162. package/dist/recursive-mkdir-path.d.ts +3 -0
  163. package/dist/recursive-mkdir-path.d.ts.map +1 -0
  164. package/dist/recursive-mkdir-path.js +8 -0
  165. package/dist/replace-directory.d.ts.map +1 -1
  166. package/dist/replace-directory.js +2 -1
  167. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  168. package/dist/replace-file-copy-fallback.js +23 -31
  169. package/dist/replace-file-copy-source.d.ts.map +1 -1
  170. package/dist/replace-file-copy-source.js +7 -12
  171. package/dist/replace-file-mode.d.ts +3 -0
  172. package/dist/replace-file-mode.d.ts.map +1 -0
  173. package/dist/replace-file-mode.js +10 -0
  174. package/dist/replace-file.d.ts +1 -0
  175. package/dist/replace-file.d.ts.map +1 -1
  176. package/dist/replace-file.js +14 -8
  177. package/dist/root-boundary.d.ts +25 -0
  178. package/dist/root-boundary.d.ts.map +1 -0
  179. package/dist/root-boundary.js +177 -0
  180. package/dist/root-context.d.ts.map +1 -1
  181. package/dist/root-context.js +40 -13
  182. package/dist/root-create-input.d.ts +10 -0
  183. package/dist/root-create-input.d.ts.map +1 -0
  184. package/dist/root-create-input.js +80 -0
  185. package/dist/root-directory-list.d.ts +3 -1
  186. package/dist/root-directory-list.d.ts.map +1 -1
  187. package/dist/root-directory-list.js +31 -5
  188. package/dist/root-entries.d.ts +11 -0
  189. package/dist/root-entries.d.ts.map +1 -0
  190. package/dist/root-entries.js +61 -0
  191. package/dist/root-errors.d.ts +5 -5
  192. package/dist/root-errors.d.ts.map +1 -1
  193. package/dist/root-errors.js +13 -12
  194. package/dist/root-impl.d.ts +13 -3
  195. package/dist/root-impl.d.ts.map +1 -1
  196. package/dist/root-impl.js +93 -70
  197. package/dist/root-move-preflight.d.ts +8 -0
  198. package/dist/root-move-preflight.d.ts.map +1 -0
  199. package/dist/root-move-preflight.js +16 -0
  200. package/dist/root-options.d.ts +12 -1
  201. package/dist/root-options.d.ts.map +1 -1
  202. package/dist/root-path-existing.d.ts +2 -0
  203. package/dist/root-path-existing.d.ts.map +1 -1
  204. package/dist/root-path-existing.js +14 -5
  205. package/dist/root-path-symlink.d.ts.map +1 -1
  206. package/dist/root-path-symlink.js +3 -2
  207. package/dist/root-path.d.ts +2 -0
  208. package/dist/root-path.d.ts.map +1 -1
  209. package/dist/root-path.js +54 -26
  210. package/dist/root-paths.d.ts +2 -6
  211. package/dist/root-paths.d.ts.map +1 -1
  212. package/dist/root-paths.js +24 -32
  213. package/dist/root-remove.d.ts +5 -0
  214. package/dist/root-remove.d.ts.map +1 -0
  215. package/dist/root-remove.js +286 -0
  216. package/dist/root-symlink-policy.d.ts +2 -1
  217. package/dist/root-symlink-policy.d.ts.map +1 -1
  218. package/dist/root-symlink-policy.js +2 -2
  219. package/dist/root-walk.d.ts.map +1 -1
  220. package/dist/root-walk.js +2 -1
  221. package/dist/root-write-mode.d.ts +2 -0
  222. package/dist/root-write-mode.d.ts.map +1 -1
  223. package/dist/root-write-mode.js +21 -7
  224. package/dist/root-write-verification.d.ts.map +1 -1
  225. package/dist/root-write-verification.js +12 -3
  226. package/dist/root.d.ts +2 -1
  227. package/dist/root.d.ts.map +1 -1
  228. package/dist/safe-path-segment.d.ts.map +1 -1
  229. package/dist/safe-path-segment.js +3 -1
  230. package/dist/secret-file.d.ts.map +1 -1
  231. package/dist/secret-file.js +2 -1
  232. package/dist/secret-read-async.d.ts.map +1 -1
  233. package/dist/secret-read-async.js +2 -1
  234. package/dist/secure-file-windows.d.ts +10 -0
  235. package/dist/secure-file-windows.d.ts.map +1 -0
  236. package/dist/secure-file-windows.js +186 -0
  237. package/dist/secure-file.d.ts.map +1 -1
  238. package/dist/secure-file.js +27 -7
  239. package/dist/secure-temp-dir.d.ts.map +1 -1
  240. package/dist/secure-temp-dir.js +2 -1
  241. package/dist/sibling-staged-file.d.ts +2 -0
  242. package/dist/sibling-staged-file.d.ts.map +1 -1
  243. package/dist/sibling-staged-file.js +49 -8
  244. package/dist/sibling-temp.d.ts +2 -0
  245. package/dist/sibling-temp.d.ts.map +1 -1
  246. package/dist/sibling-temp.js +8 -5
  247. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  248. package/dist/sidecar-lock-acquire.js +16 -5
  249. package/dist/sidecar-lock-policy.d.ts +2 -0
  250. package/dist/sidecar-lock-policy.d.ts.map +1 -1
  251. package/dist/sidecar-lock-policy.js +17 -0
  252. package/dist/sidecar-lock.d.ts.map +1 -1
  253. package/dist/sidecar-lock.js +2 -3
  254. package/dist/staged-directory.d.ts.map +1 -1
  255. package/dist/staged-directory.js +4 -3
  256. package/dist/temp-target.d.ts +14 -12
  257. package/dist/temp-target.d.ts.map +1 -1
  258. package/dist/temp-target.js +15 -7
  259. package/dist/trash.d.ts.map +1 -1
  260. package/dist/trash.js +7 -5
  261. package/dist/unicode-path.d.ts +3 -0
  262. package/dist/unicode-path.d.ts.map +1 -0
  263. package/dist/unicode-path.js +13 -0
  264. package/dist/walk.d.ts.map +1 -1
  265. package/dist/walk.js +14 -12
  266. package/dist/write-file-handle.d.ts +1 -0
  267. package/dist/write-file-handle.d.ts.map +1 -1
  268. package/dist/write-file-handle.js +3 -2
  269. package/docs/advanced.md +7 -1
  270. package/docs/archive.md +31 -5
  271. package/docs/atomic.md +17 -1
  272. package/docs/config.md +1 -0
  273. package/docs/contributing.md +33 -2
  274. package/docs/copy.md +75 -6
  275. package/docs/directory-identity.md +85 -0
  276. package/docs/durability.md +40 -3
  277. package/docs/entries.md +109 -0
  278. package/docs/errors.md +3 -3
  279. package/docs/file-store.md +21 -0
  280. package/docs/filename.md +9 -2
  281. package/docs/guest.md +146 -0
  282. package/docs/in-place-write.md +81 -0
  283. package/docs/index.md +2 -0
  284. package/docs/install-path.md +59 -13
  285. package/docs/install.md +34 -0
  286. package/docs/native-helper.md +14 -5
  287. package/docs/native.md +18 -2
  288. package/docs/output.md +32 -6
  289. package/docs/path-case.md +64 -0
  290. package/docs/path-scope.md +1 -1
  291. package/docs/path.md +1 -1
  292. package/docs/permissions.md +37 -3
  293. package/docs/public-api.md +36 -2
  294. package/docs/root.md +33 -3
  295. package/docs/secure-file.md +17 -14
  296. package/docs/security-model.md +1 -1
  297. package/docs/sidecar-lock.md +12 -3
  298. package/docs/store.md +22 -1
  299. package/docs/temp.md +39 -6
  300. package/docs/types.md +1 -1
  301. package/docs/writing.md +153 -3
  302. package/package.json +14 -8
@@ -0,0 +1,177 @@
1
+ import path from "node:path";
2
+ import { inspectDirectoryIdentitySync } from "./directory-guard.js";
3
+ import { isPathRelativeEscape } from "./path.js";
4
+ import { realpathSync } from "./realpath.js";
5
+ function windowsComparablePath(value, normalize) {
6
+ const original = normalize
7
+ ? path.win32.normalize(value.replaceAll("/", "\\"))
8
+ : value.replaceAll("/", "\\");
9
+ if (/^\\\\\?\\UNC\\/i.test(original)) {
10
+ return {
11
+ original,
12
+ comparison: `\\\\${original.slice(8)}`,
13
+ originalIndex: (comparisonIndex) => comparisonIndex + 6,
14
+ };
15
+ }
16
+ if (original.startsWith("\\\\?\\")) {
17
+ return {
18
+ original,
19
+ comparison: original.slice(4),
20
+ originalIndex: (comparisonIndex) => comparisonIndex + 4,
21
+ };
22
+ }
23
+ return { original, comparison: original, originalIndex: (comparisonIndex) => comparisonIndex };
24
+ }
25
+ function trimWindowsTrailingSeparators(value) {
26
+ const rootLength = path.win32.parse(value).root.length;
27
+ let end = value.length;
28
+ while (end > rootLength && value[end - 1] === "\\")
29
+ end -= 1;
30
+ return value.slice(0, end);
31
+ }
32
+ function trimWindowsLeadingSeparators(value) {
33
+ let start = 0;
34
+ while (value[start] === "\\")
35
+ start += 1;
36
+ return value.slice(start);
37
+ }
38
+ function exactWindowsRootPrefix(rootPath, candidatePath) {
39
+ const root = trimWindowsTrailingSeparators(rootPath.replaceAll("/", "\\"));
40
+ const candidate = candidatePath.replaceAll("/", "\\");
41
+ let relativePath;
42
+ if (candidate === root) {
43
+ relativePath = "";
44
+ }
45
+ else {
46
+ const rootWithSep = root.endsWith("\\") ? root : `${root}\\`;
47
+ if (!candidate.startsWith(rootWithSep))
48
+ return undefined;
49
+ relativePath = trimWindowsLeadingSeparators(candidate.slice(rootWithSep.length));
50
+ }
51
+ return {
52
+ admission: "exact",
53
+ candidateRootPath: root,
54
+ path: relativePath === "" ? root : `${root}${root.endsWith("\\") ? "" : "\\"}${relativePath}`,
55
+ relativePath,
56
+ };
57
+ }
58
+ function windowsRootPrefix(rootPath, candidatePath) {
59
+ const exact = exactWindowsRootPrefix(rootPath, candidatePath);
60
+ if (exact)
61
+ return exact;
62
+ const trusted = windowsComparablePath(rootPath, true);
63
+ const supplied = windowsComparablePath(candidatePath, false);
64
+ const root = trimWindowsTrailingSeparators(trusted.original);
65
+ const rootComparison = trimWindowsTrailingSeparators(trusted.comparison);
66
+ const candidate = supplied.comparison;
67
+ if (!path.win32.isAbsolute(supplied.original))
68
+ return undefined;
69
+ let relativePath;
70
+ let candidateRootPath;
71
+ let admission;
72
+ if (candidate === rootComparison) {
73
+ relativePath = "";
74
+ candidateRootPath = supplied.original;
75
+ admission = "exact";
76
+ }
77
+ else if (candidate.toLowerCase() === rootComparison.toLowerCase()) {
78
+ relativePath = "";
79
+ candidateRootPath = supplied.original;
80
+ admission = "identity";
81
+ }
82
+ else {
83
+ const rootWithSep = rootComparison.endsWith("\\") ? rootComparison : `${rootComparison}\\`;
84
+ const candidatePrefix = candidate.slice(0, rootWithSep.length);
85
+ if (candidatePrefix === rootWithSep) {
86
+ admission = "exact";
87
+ }
88
+ else if (candidatePrefix.toLowerCase() === rootWithSep.toLowerCase()) {
89
+ admission = "identity";
90
+ }
91
+ else {
92
+ return undefined;
93
+ }
94
+ relativePath = trimWindowsLeadingSeparators(candidate.slice(rootWithSep.length));
95
+ candidateRootPath = supplied.original.slice(0, supplied.originalIndex(rootComparison.length));
96
+ }
97
+ const rebasedPath = relativePath === ""
98
+ ? root
99
+ : `${root}${root.endsWith("\\") ? "" : "\\"}${relativePath}`;
100
+ return { candidateRootPath, path: rebasedPath, relativePath, admission };
101
+ }
102
+ function exactRootIdentity(rootPath, expected) {
103
+ if (typeof expected?.dev === "bigint" && typeof expected.ino === "bigint") {
104
+ return { dev: expected.dev, ino: expected.ino };
105
+ }
106
+ const observed = inspectDirectoryIdentitySync(rootPath);
107
+ return { dev: observed.dev, ino: observed.ino };
108
+ }
109
+ /**
110
+ * Admit a path at a trusted directory boundary.
111
+ *
112
+ * On Windows, an exact structural prefix is allocation-only and performs no
113
+ * filesystem observation. A match which depends on case folding is accepted
114
+ * only when the candidate prefix names the exact trusted root object. Accepted
115
+ * paths are always returned under the trusted root spelling.
116
+ */
117
+ export function admitPathInsideRoot(params) {
118
+ // Some platform-fallback tests intentionally spoof process.platform while
119
+ // retaining Node's POSIX path module. A single-slash absolute root remains
120
+ // unambiguously POSIX; double-slash and slash-backslash roots are reserved
121
+ // for the synthetic Windows namespace cases below.
122
+ const usesPosixBoundary = process.platform !== "win32" || (path.sep === "/" &&
123
+ params.rootPath.startsWith("/") &&
124
+ params.rootPath[1] !== "/" &&
125
+ params.rootPath[1] !== "\\");
126
+ if (usesPosixBoundary) {
127
+ if (params.rootPath.startsWith("/") &&
128
+ params.candidatePath.startsWith("/") &&
129
+ !params.candidatePath.includes("/..") &&
130
+ (params.candidatePath === params.rootPath ||
131
+ (params.candidatePath.startsWith(params.rootPath) &&
132
+ (params.rootPath.endsWith("/") || params.candidatePath[params.rootPath.length] === "/")))) {
133
+ const relativeStart = params.rootPath.endsWith("/")
134
+ ? params.rootPath.length
135
+ : params.rootPath.length + 1;
136
+ return {
137
+ admission: "exact",
138
+ path: params.candidatePath,
139
+ relativePath: params.candidatePath === params.rootPath
140
+ ? ""
141
+ : params.candidatePath.slice(relativeStart),
142
+ };
143
+ }
144
+ const root = path.resolve(params.rootPath);
145
+ const candidate = path.resolve(params.candidatePath);
146
+ const relativePath = path.relative(root, candidate);
147
+ if (isPathRelativeEscape(relativePath))
148
+ return undefined;
149
+ return { path: candidate, relativePath: relativePath === "." ? "" : relativePath, admission: "exact" };
150
+ }
151
+ const match = windowsRootPrefix(params.rootPath, params.candidatePath);
152
+ if (!match)
153
+ return undefined;
154
+ const admitted = () => ({
155
+ admission: match.admission,
156
+ path: match.path,
157
+ relativePath: match.relativePath,
158
+ });
159
+ if (match.admission === "exact")
160
+ return admitted();
161
+ const cached = params.identityCache?.get(match.candidateRootPath);
162
+ if (cached !== undefined)
163
+ return cached ? admitted() : undefined;
164
+ try {
165
+ const expected = exactRootIdentity(params.rootPath, params.rootIdentity);
166
+ const candidateRootPath = params.resolveCandidateRoot
167
+ ? realpathSync.native(match.candidateRootPath)
168
+ : match.candidateRootPath;
169
+ inspectDirectoryIdentitySync(candidateRootPath, expected);
170
+ params.identityCache?.set(match.candidateRootPath, true);
171
+ return admitted();
172
+ }
173
+ catch {
174
+ params.identityCache?.set(match.candidateRootPath, false);
175
+ return undefined;
176
+ }
177
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"root-context.d.ts","sourceRoot":"","sources":["../src/root-context.ts"],"names":[],"mappings":"AAkBA,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1E,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,eAAO,MAAM,iBAAiB,UAAW,MAAM,WACM,CAAC;AAEtD,wBAAgB,2BAA2B,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAEtE;AAED,wBAAgB,8BAA8B,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAKzE;AAMD,wBAAsB,0BAA0B,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAgBtF;AAED,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CA4B9E;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAgBhF;AAED,wBAAsB,yBAAyB,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBhF;AAED,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,WAAW,EACjB,YAAY,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE;IACR,cAAc,CAAC,EAAE,mBAAmB,GAAG,YAAY,CAAC;IACpD,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC,GACA,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAqDtE;AAED,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAEvE"}
1
+ {"version":3,"file":"root-context.d.ts","sourceRoot":"","sources":["../src/root-context.ts"],"names":[],"mappings":"AAoBA,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1E,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,eAAO,MAAM,iBAAiB,UAAW,MAAM,WACM,CAAC;AAEtD,wBAAgB,2BAA2B,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAEtE;AAED,wBAAgB,8BAA8B,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAKzE;AAMD,wBAAsB,0BAA0B,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAetF;AAED,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CA4B9E;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAyBhF;AAED,wBAAsB,yBAAyB,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBhF;AAED,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,WAAW,EACjB,YAAY,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE;IACR,cAAc,CAAC,EAAE,mBAAmB,GAAG,YAAY,CAAC;IACpD,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC,GACA,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAoEtE;AAED,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAEvE"}
@@ -6,8 +6,10 @@ import { FsSafeError } from "./errors.js";
6
6
  import { sameFileIdentity } from "./file-identity.js";
7
7
  import { assertNoNulPathInput, assertNoUnsafeDeviceReadPath, hasNodeErrorCode, isNotFoundPathError, isPathInside, } from "./path.js";
8
8
  import { ROOT_PATH_ALIAS_POLICIES, resolveRootPath } from "./root-path.js";
9
+ import { admitPathInsideRoot } from "./root-boundary.js";
9
10
  import { outsideWorkspaceError, rootPathChangedError } from "./root-errors.js";
10
11
  import { isDriveRelativePath } from "./safe-path-segment.js";
12
+ import { realpathSync } from "./realpath.js";
11
13
  import { inspectFileIdentity } from "./strict-file-identity.js";
12
14
  export const ensureTrailingSep = (value) => value.endsWith(path.sep) ? value : value + path.sep;
13
15
  export function assertValidRootRelativePath(relativePath) {
@@ -23,23 +25,22 @@ let cachedHomePath;
23
25
  const POSIX_PARENT_COMPONENT = /(?:^|\/)\.\.(?:\/|$)/;
24
26
  const WINDOWS_PARENT_COMPONENT = /(?:^|[\\/])\.\.(?:[\\/]|$)/;
25
27
  export async function expandRelativePathWithHome(relativePath) {
28
+ const homeOnly = relativePath === "~";
29
+ const homePrefix = relativePath.startsWith("~/") || (path.sep === "\\" && relativePath.startsWith("~\\"));
30
+ if (!homeOnly && !homePrefix)
31
+ return relativePath;
26
32
  const rawHome = process.env.HOME || process.env.USERPROFILE || os.homedir();
27
33
  if (cachedHomePath?.raw !== rawHome) {
28
34
  let realHome = rawHome;
29
35
  try {
30
- realHome = fs.realpathSync.native(rawHome);
36
+ realHome = realpathSync.native(rawHome);
31
37
  }
32
38
  catch {
33
39
  // If the home dir cannot be canonicalized, keep lexical expansion behavior.
34
40
  }
35
41
  cachedHomePath = { raw: rawHome, real: realHome };
36
42
  }
37
- if (relativePath === "~")
38
- return cachedHomePath.real;
39
- if (relativePath.startsWith("~/") || (path.sep === "\\" && relativePath.startsWith("~\\"))) {
40
- return `${ensureTrailingSep(cachedHomePath.real)}${relativePath.slice(2)}`;
41
- }
42
- return relativePath;
43
+ return homeOnly ? cachedHomePath.real : `${ensureTrailingSep(cachedHomePath.real)}${relativePath.slice(2)}`;
43
44
  }
44
45
  export async function resolveRootContext(rootDir) {
45
46
  assertNoNulPathInput(rootDir, "root dir contains a NUL byte");
@@ -47,7 +48,7 @@ export async function resolveRootContext(rootDir) {
47
48
  let rootReal;
48
49
  let rootIdentity;
49
50
  try {
50
- rootReal = fs.realpathSync.native(rootDir);
51
+ rootReal = realpathSync.native(rootDir);
51
52
  const rootStat = await inspectFileIdentity(() => {
52
53
  const stat = fs.statSync(rootReal, { bigint: true });
53
54
  if (!stat.isDirectory())
@@ -77,11 +78,21 @@ export function rootRelativeReadPath(root, filePath) {
77
78
  if (!absoluteInput)
78
79
  return filePath;
79
80
  const raw = process.platform === "win32" ? filePath.replaceAll("/", path.sep) : filePath;
80
- for (const base of [root.rootDir, root.rootReal]) {
81
+ const bases = root.rootDir === root.rootReal ? [root.rootReal] : [root.rootDir, root.rootReal];
82
+ for (const base of bases) {
83
+ if (process.platform === "win32") {
84
+ const admitted = admitPathInsideRoot({
85
+ rootPath: base,
86
+ candidatePath: raw,
87
+ rootIdentity: root.rootIdentity,
88
+ resolveCandidateRoot: base === root.rootDir && root.rootDir !== root.rootReal,
89
+ });
90
+ if (admitted)
91
+ return admitted.relativePath;
92
+ continue;
93
+ }
81
94
  const prefix = ensureTrailingSep(base);
82
- const matches = process.platform === "win32"
83
- ? raw.slice(0, prefix.length).toLowerCase() === prefix.toLowerCase()
84
- : raw.startsWith(prefix);
95
+ const matches = raw.startsWith(prefix);
85
96
  if (matches) {
86
97
  let start = prefix.length;
87
98
  while (raw[start] === path.sep)
@@ -128,6 +139,7 @@ export async function resolvePathInRoot(root, relativePath, options) {
128
139
  absolutePath: rawAbsolutePath,
129
140
  rootPath: root.rootReal,
130
141
  rootCanonicalPath: root.rootReal,
142
+ rootIdentity: root.rootIdentity,
131
143
  boundaryLabel: "root",
132
144
  policy: options?.allowFinalSymlink ? ROOT_PATH_ALIAS_POLICIES.unlinkTarget : undefined,
133
145
  rejectSymlinks: options?.rejectSymlinks,
@@ -142,8 +154,23 @@ export async function resolvePathInRoot(root, relativePath, options) {
142
154
  throw new FsSafeError("path-alias", "parent traversal resolves differently through a symlink");
143
155
  }
144
156
  }
145
- if (options?.resolveCanonical)
157
+ if (options?.resolveCanonical) {
146
158
  resolved = checked.canonicalPath;
159
+ }
160
+ else {
161
+ // resolveRootPath preserves ordinary caller spelling as part of its
162
+ // public receipt. Re-admit the exact normalized spelling before it is
163
+ // reused for Root I/O: on a case-sensitive Windows directory it may
164
+ // otherwise name a distinct case-folded sibling after raw traversal.
165
+ const admitted = admitPathInsideRoot({
166
+ rootPath: root.rootReal,
167
+ candidatePath: checked.absolutePath,
168
+ rootIdentity: root.rootIdentity,
169
+ });
170
+ if (!admitted)
171
+ throw outsideWorkspaceError();
172
+ resolved = admitted.path;
173
+ }
147
174
  }
148
175
  catch (error) {
149
176
  if (error instanceof FsSafeError && error.code === "symlink") {
@@ -0,0 +1,10 @@
1
+ import type { PinnedWriteInput } from "./pinned-write.js";
2
+ import type { RootCreateOptions, RootCreateStreamOptions, RootWriteOptions } from "./root-options.js";
3
+ export type RootWriteParams = RootWriteOptions & RootCreateStreamOptions & {
4
+ relativePath: string;
5
+ data: string | Buffer | AsyncIterable<Uint8Array>;
6
+ };
7
+ export declare function rethrowCreateInputError(error: unknown): never;
8
+ export declare function createInputOptions(data: RootWriteParams["data"], options: RootCreateOptions & RootCreateStreamOptions, defaultMaxBytes?: number): RootCreateOptions & RootCreateStreamOptions;
9
+ export declare function rootWriteInput(params: RootWriteParams): PinnedWriteInput;
10
+ //# sourceMappingURL=root-create-input.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"root-create-input.d.ts","sourceRoot":"","sources":["../src/root-create-input.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,KAAK,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAEtG,MAAM,MAAM,eAAe,GAAG,gBAAgB,GAAG,uBAAuB,GAAG;IACzE,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,aAAa,CAAC,UAAU,CAAC,CAAC;CACnD,CAAC;AAQF,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAG7D;AAED,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,eAAe,CAAC,MAAM,CAAC,EAC7B,OAAO,EAAE,iBAAiB,GAAG,uBAAuB,EACpD,eAAe,CAAC,EAAE,MAAM,GACvB,iBAAiB,GAAG,uBAAuB,CAe7C;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,eAAe,GAAG,gBAAgB,CAwCxE"}
@@ -0,0 +1,80 @@
1
+ import { normalizeMaxBytes } from "./byte-budget.js";
2
+ import { FsSafeError } from "./errors.js";
3
+ import { MutationAuthorityError } from "./mutation-authority.js";
4
+ class CreateInputError extends FsSafeError {
5
+ rejection;
6
+ constructor(rejection) {
7
+ super("helper-failed", "create input failed", { cause: rejection });
8
+ this.rejection = rejection;
9
+ }
10
+ }
11
+ export function rethrowCreateInputError(error) {
12
+ if (error instanceof CreateInputError)
13
+ throw error.rejection;
14
+ throw error;
15
+ }
16
+ export function createInputOptions(data, options, defaultMaxBytes) {
17
+ if (typeof data === "string" || Buffer.isBuffer(data))
18
+ return options;
19
+ options.signal?.throwIfAborted();
20
+ if (options.encoding !== undefined || options.renameIdentity !== undefined) {
21
+ throw new TypeError("streamed create does not accept encoding or renameIdentity");
22
+ }
23
+ const maxBytes = normalizeMaxBytes(options.maxBytes, { defaultValue: defaultMaxBytes });
24
+ return {
25
+ ...options,
26
+ maxBytes,
27
+ assertBeforeMutation: () => {
28
+ options.signal?.throwIfAborted();
29
+ return options.assertBeforeMutation?.();
30
+ },
31
+ };
32
+ }
33
+ export function rootWriteInput(params) {
34
+ const data = params.data;
35
+ if (typeof data === "string" || Buffer.isBuffer(data)) {
36
+ return { kind: "buffer", data, encoding: params.encoding };
37
+ }
38
+ return {
39
+ kind: "stream",
40
+ stageBeforePublish: true,
41
+ stream: (async function* () {
42
+ let iterator;
43
+ let completed = false;
44
+ try {
45
+ params.signal?.throwIfAborted();
46
+ iterator = data[Symbol.asyncIterator]();
47
+ while (true) {
48
+ if (params.signal?.aborted)
49
+ throw new MutationAuthorityError(params.signal.reason);
50
+ const next = await iterator.next();
51
+ completed = next.done === true;
52
+ if (params.signal?.aborted)
53
+ throw new MutationAuthorityError(params.signal.reason);
54
+ if (completed)
55
+ return;
56
+ const chunk = next.value;
57
+ if (!(chunk instanceof Uint8Array)) {
58
+ throw new TypeError("streamed create chunks must be Uint8Array values");
59
+ }
60
+ yield chunk;
61
+ }
62
+ }
63
+ catch (error) {
64
+ if (error instanceof MutationAuthorityError)
65
+ throw error;
66
+ throw new CreateInputError(error);
67
+ }
68
+ finally {
69
+ if (iterator && !completed) {
70
+ try {
71
+ await iterator.return?.();
72
+ }
73
+ catch (error) {
74
+ throw new CreateInputError(error);
75
+ }
76
+ }
77
+ }
78
+ })(),
79
+ };
80
+ }
@@ -18,7 +18,9 @@ export type RootDirectoryListingOptions = {
18
18
  order: "sorted" | "filesystem";
19
19
  signal?: AbortSignal;
20
20
  snapshot: boolean;
21
+ maxNames?: number;
22
+ metadataBatchSize?: number;
21
23
  admitEntry(): boolean;
22
24
  };
23
- export declare function listDirectoryForWalk(root: RootContext, directory: string, options: RootDirectoryListingOptions): Promise<RootDirectoryListing>;
25
+ export declare function openRootDirectoryListing(root: RootContext, directory: string, options: RootDirectoryListingOptions): Promise<RootDirectoryListing>;
24
26
  //# sourceMappingURL=root-directory-list.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"root-directory-list.d.ts","sourceRoot":"","sources":["../src/root-directory-list.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAQ1C,OAAO,EAA6B,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGhF,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAIrD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,KAAK,GAAG,QAAQ,CAcvD;AAWD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,IAAI,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;AAClH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,EAAE,GAAG,QAAQ,EAAE,CAAC,CAAC;AAqBhI,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,IAAI,OAAO,CAAC;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,QAAQ,CAAA;KAAE,GAAG;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC,CAAC;IAClG,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,KAAK,EAAE,QAAQ,GAAG,YAAY,CAAC;IAC/B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,IAAI,OAAO,CAAC;CACvB,CAAC;AAEF,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,WAAW,EACjB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,2BAA2B,GACnC,OAAO,CAAC,oBAAoB,CAAC,CAqI/B"}
1
+ {"version":3,"file":"root-directory-list.d.ts","sourceRoot":"","sources":["../src/root-directory-list.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAQ1C,OAAO,EAA6B,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAIhF,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAIrD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,KAAK,GAAG,QAAQ,CAcvD;AAWD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,IAAI,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;AAClH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,aAAa,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,EAAE,GAAG,QAAQ,EAAE,CAAC,CAAC;AAqBhI,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,IAAI,OAAO,CAAC;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,QAAQ,CAAA;KAAE,GAAG;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC,CAAC;IAClG,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,KAAK,EAAE,QAAQ,GAAG,YAAY,CAAC;IAC/B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,UAAU,IAAI,OAAO,CAAC;CACvB,CAAC;AAEF,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,WAAW,EACjB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,2BAA2B,GACnC,OAAO,CAAC,oBAAoB,CAAC,CA6J/B"}
@@ -4,9 +4,10 @@ import path from "node:path";
4
4
  import { setImmediate as yieldToEventLoop } from "node:timers/promises";
5
5
  import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard } from "./directory-guard.js";
6
6
  import { FsSafeError } from "./errors.js";
7
- import { isNotFoundPathError, isPathInside } from "./path.js";
7
+ import { isNotFoundPathError } from "./path.js";
8
8
  import { assertRootIdentityCurrent } from "./root-context.js";
9
9
  import { rootPathChangedError } from "./root-errors.js";
10
+ import { admitPathInsideRoot } from "./root-boundary.js";
10
11
  import { createSuppressedError } from "./suppressed-error.js";
11
12
  const METADATA_BATCH_SIZE = 32;
12
13
  export function pathStatFromStats(stat) {
@@ -48,13 +49,20 @@ export async function listDirectoryPath(root, directory, withFileTypes) {
48
49
  throw normalizeDirectoryError(error);
49
50
  }
50
51
  }
51
- export async function listDirectoryForWalk(root, directory, options) {
52
+ export async function openRootDirectoryListing(root, directory, options) {
52
53
  const guard = await createAsyncDirectoryGuard(directory, { bigint: true }).catch((error) => {
53
54
  throw normalizeDirectoryError(error);
54
55
  });
55
- if (!isPathInside(root.rootReal, guard.realPath)) {
56
+ const admittedRealPath = admitPathInsideRoot({
57
+ rootPath: root.rootReal,
58
+ candidatePath: guard.realPath,
59
+ rootIdentity: root.rootIdentity,
60
+ });
61
+ if (!admittedRealPath) {
56
62
  throw new FsSafeError("outside-workspace", "directory is outside workspace root");
57
63
  }
64
+ guard.dir = admittedRealPath.path;
65
+ guard.realPath = admittedRealPath.path;
58
66
  const guardPinsRoot = guard.dir === root.rootReal &&
59
67
  guard.stat.dev === root.rootIdentity.dev && guard.stat.ino === root.rootIdentity.ino;
60
68
  const assertCurrent = async () => {
@@ -97,6 +105,22 @@ export async function listDirectoryForWalk(root, directory, options) {
97
105
  else if (options.snapshot) {
98
106
  snapshot = await listDirectoryPath(root, guard.realPath, true);
99
107
  }
108
+ else if (options.maxNames !== undefined) {
109
+ names = [];
110
+ handle = await fs.opendir(guard.realPath, { bufferSize: 1 });
111
+ while (true) {
112
+ await assertCurrent();
113
+ const name = (await handle.read())?.name;
114
+ await assertCurrent();
115
+ if (name === undefined)
116
+ break;
117
+ if (names.length >= options.maxNames) {
118
+ throw new FsSafeError("too-large", "directory entry budget exceeded");
119
+ }
120
+ names.push(name);
121
+ }
122
+ await close();
123
+ }
100
124
  else {
101
125
  names = await fs.readdir(guard.realPath);
102
126
  }
@@ -115,8 +139,10 @@ export async function listDirectoryForWalk(root, directory, options) {
115
139
  }
116
140
  const prepareBatch = async (sortedNames) => {
117
141
  let name = sortedNames[index++];
118
- if (name === undefined)
142
+ if (name === undefined) {
143
+ await assertCurrent();
119
144
  return;
145
+ }
120
146
  if (!options.admitEntry()) {
121
147
  pendingLimit = name;
122
148
  return;
@@ -132,7 +158,7 @@ export async function listDirectoryForWalk(root, directory, options) {
132
158
  const entry = { name, ...pathStatFromStats(fsSync.lstatSync(path.join(guard.realPath, name))) };
133
159
  prepared.push(entry);
134
160
  // No later sibling can be observed before a possible recursive descent.
135
- if (entry.isDirectory || entry.isSymbolicLink || prepared.length >= METADATA_BATCH_SIZE)
161
+ if (entry.isDirectory || entry.isSymbolicLink || prepared.length >= (options.metadataBatchSize ?? METADATA_BATCH_SIZE))
136
162
  break;
137
163
  }
138
164
  catch (error) {
@@ -0,0 +1,11 @@
1
+ import { type RootContext } from "./root-context.js";
2
+ import { type SymlinkPolicy } from "./root-symlink-policy.js";
3
+ import type { DirEntry } from "./types.js";
4
+ export type RootEntriesOptions = {
5
+ maxEntries?: number;
6
+ order?: "filesystem" | "sorted";
7
+ signal?: AbortSignal;
8
+ symlinks?: SymlinkPolicy;
9
+ };
10
+ export declare function entriesInRoot(root: RootContext, relativePath: string, options: RootEntriesOptions): AsyncGenerator<DirEntry>;
11
+ //# sourceMappingURL=root-entries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"root-entries.d.ts","sourceRoot":"","sources":["../src/root-entries.ts"],"names":[],"mappings":"AAEA,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAExE,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAErF,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,MAAM,MAAM,kBAAkB,GAAG;IAC/B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,YAAY,GAAG,QAAQ,CAAC;IAChC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,QAAQ,CAAC,EAAE,aAAa,CAAC;CAC1B,CAAC;AAEF,wBAAuB,aAAa,CAClC,IAAI,EAAE,WAAW,EACjB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,kBAAkB,GAC1B,cAAc,CAAC,QAAQ,CAAC,CAkD1B"}
@@ -0,0 +1,61 @@
1
+ import { FsSafeError } from "./errors.js";
2
+ import { validatePinnedOperationPayload } from "./pinned-operation.js";
3
+ import { resolvePathInRoot } from "./root-context.js";
4
+ import { openRootDirectoryListing } from "./root-directory-list.js";
5
+ import { readSymlinkResolution } from "./root-symlink-policy.js";
6
+ import { createSuppressedError } from "./suppressed-error.js";
7
+ export async function* entriesInRoot(root, relativePath, options) {
8
+ const maxEntries = options.maxEntries;
9
+ if (maxEntries !== undefined && (!Number.isSafeInteger(maxEntries) || maxEntries < 0)) {
10
+ throw new RangeError("maxEntries must be a non-negative safe integer");
11
+ }
12
+ if (options.order !== undefined && options.order !== "filesystem" && options.order !== "sorted") {
13
+ throw new TypeError(`invalid root entries order: ${String(options.order)}`);
14
+ }
15
+ options.signal?.throwIfAborted();
16
+ validatePinnedOperationPayload({ relativePath });
17
+ const resolved = await resolvePathInRoot(root, relativePath, {
18
+ ...readSymlinkResolution(options.symlinks),
19
+ resolveCanonical: true,
20
+ });
21
+ options.signal?.throwIfAborted();
22
+ let examined = 0;
23
+ const listing = await openRootDirectoryListing(root, resolved.resolved, {
24
+ order: options.order ?? "filesystem",
25
+ signal: options.signal,
26
+ snapshot: false,
27
+ maxNames: maxEntries,
28
+ metadataBatchSize: 1,
29
+ admitEntry: () => maxEntries === undefined || examined++ < maxEntries,
30
+ });
31
+ let failed = false;
32
+ let operationError;
33
+ try {
34
+ while (true) {
35
+ const next = await listing.next();
36
+ options.signal?.throwIfAborted();
37
+ if (next === undefined)
38
+ return;
39
+ if (next.kind === "limit") {
40
+ throw new FsSafeError("too-large", "directory entry budget exceeded");
41
+ }
42
+ yield next.entry;
43
+ }
44
+ }
45
+ catch (error) {
46
+ failed = true;
47
+ operationError = error;
48
+ throw error;
49
+ }
50
+ finally {
51
+ try {
52
+ await listing[Symbol.asyncDispose]();
53
+ }
54
+ catch (closeError) {
55
+ if (failed) {
56
+ throw createSuppressedError(closeError, operationError, "directory iteration and close both failed");
57
+ }
58
+ throw closeError;
59
+ }
60
+ }
61
+ }
@@ -1,13 +1,13 @@
1
- import { FsSafeError } from "./errors.js";
2
- export declare function fileNotFoundError(cause?: unknown): FsSafeError;
1
+ import { FsSafeError, type FsSafeErrorDetails } from "./errors.js";
2
+ export declare function fileNotFoundError(cause?: unknown, details?: FsSafeErrorDetails): FsSafeError;
3
3
  export declare function outsideWorkspaceError(): FsSafeError;
4
4
  export declare function rootPathChangedError(cause?: Error): FsSafeError;
5
5
  export declare function directoryComponentNotDirectoryError(cause?: unknown): FsSafeError;
6
6
  export declare function hardlinkedPathNotAllowedError(): FsSafeError;
7
7
  export declare function isAlreadyExistsError(error: unknown): boolean;
8
8
  export declare function normalizePinnedWriteError(error: unknown): Error;
9
- export declare function normalizePinnedPathError(error: unknown): Error;
10
- export declare function normalizeRemoveGuardError(error: unknown): Error;
11
- export declare function normalizeRemovePathError(error: unknown): Error;
9
+ export declare function normalizePinnedPathError(error: unknown, details?: FsSafeErrorDetails): Error;
10
+ export declare function normalizeRemoveGuardError(error: unknown, details?: FsSafeErrorDetails): Error;
11
+ export declare function normalizeRemovePathError(error: unknown, details?: FsSafeErrorDetails): Error;
12
12
  export declare function throwFsSafeReadError(error: unknown, label: string): never;
13
13
  //# sourceMappingURL=root-errors.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"root-errors.d.ts","sourceRoot":"","sources":["../src/root-errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAK1C,wBAAgB,iBAAiB,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,WAAW,CAI9D;AAED,wBAAgB,qBAAqB,IAAI,WAAW,CAEnD;AAED,wBAAgB,oBAAoB,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,WAAW,CAE/D;AAED,wBAAgB,mCAAmC,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,WAAW,CAIhF;AAED,wBAAgB,6BAA6B,IAAI,WAAW,CAE3D;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE5D;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAU/D;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAO9D;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAQ/D;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAe9D;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,KAAK,CAQzE"}
1
+ {"version":3,"file":"root-errors.d.ts","sourceRoot":"","sources":["../src/root-errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,KAAK,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAKnE,wBAAgB,iBAAiB,CAAC,KAAK,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,WAAW,CAI5F;AAED,wBAAgB,qBAAqB,IAAI,WAAW,CAEnD;AAED,wBAAgB,oBAAoB,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,WAAW,CAE/D;AAED,wBAAgB,mCAAmC,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,WAAW,CAIhF;AAED,wBAAgB,6BAA6B,IAAI,WAAW,CAE3D;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE5D;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAU/D;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,KAAK,CAQ5F;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,KAAK,CAQ7F;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,KAAK,CAe5F;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,KAAK,CAQzE"}
@@ -1,10 +1,10 @@
1
1
  import { FsSafeError } from "./errors.js";
2
2
  import { hasNodeErrorCode, isNodeError, isNotFoundPathError } from "./path.js";
3
3
  const REMOVE_NOT_EMPTY_CODES = new Set(["ENOTEMPTY", "EEXIST"]);
4
- export function fileNotFoundError(cause) {
4
+ export function fileNotFoundError(cause, details) {
5
5
  return cause === undefined
6
- ? new FsSafeError("not-found", "file not found")
7
- : new FsSafeError("not-found", "file not found", { cause });
6
+ ? new FsSafeError("not-found", "file not found", { details })
7
+ : new FsSafeError("not-found", "file not found", { cause, details });
8
8
  }
9
9
  export function outsideWorkspaceError() {
10
10
  return new FsSafeError("outside-workspace", "file is outside workspace root");
@@ -34,38 +34,39 @@ export function normalizePinnedWriteError(error) {
34
34
  cause: error instanceof Error ? error : undefined,
35
35
  });
36
36
  }
37
- export function normalizePinnedPathError(error) {
37
+ export function normalizePinnedPathError(error, details) {
38
38
  if (error instanceof FsSafeError) {
39
39
  return error;
40
40
  }
41
41
  return new FsSafeError("path-alias", "path is not under root", {
42
42
  cause: error instanceof Error ? error : undefined,
43
+ details,
43
44
  });
44
45
  }
45
- export function normalizeRemoveGuardError(error) {
46
+ export function normalizeRemoveGuardError(error, details) {
46
47
  if (error instanceof FsSafeError) {
47
48
  return error;
48
49
  }
49
50
  if (isNotFoundPathError(error)) {
50
- return fileNotFoundError(error instanceof Error ? error : undefined);
51
+ return fileNotFoundError(error instanceof Error ? error : undefined, details);
51
52
  }
52
- return normalizePinnedPathError(error);
53
+ return normalizePinnedPathError(error, details);
53
54
  }
54
- export function normalizeRemovePathError(error) {
55
+ export function normalizeRemovePathError(error, details) {
55
56
  if (error instanceof FsSafeError) {
56
57
  return error;
57
58
  }
58
59
  if (!isNodeError(error) || typeof error.code !== "string") {
59
- return normalizePinnedPathError(error);
60
+ return normalizePinnedPathError(error, details);
60
61
  }
61
62
  const cause = error instanceof Error ? error : undefined;
62
63
  if (isNotFoundPathError(error)) {
63
- return fileNotFoundError(cause);
64
+ return fileNotFoundError(cause, details);
64
65
  }
65
66
  if (REMOVE_NOT_EMPTY_CODES.has(error.code)) {
66
- return new FsSafeError("not-empty", "directory is not empty", { cause });
67
+ return new FsSafeError("not-empty", "directory is not empty", { cause, details });
67
68
  }
68
- return new FsSafeError("not-removable", "path could not be removed", { cause });
69
+ return new FsSafeError("not-removable", "path could not be removed", { cause, details });
69
70
  }
70
71
  export function throwFsSafeReadError(error, label) {
71
72
  if (error instanceof FsSafeError) {