@openclaw/fs-safe 0.12.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (395) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +12 -7
  3. package/dist/absolute-path.d.ts.map +1 -1
  4. package/dist/absolute-path.js +32 -11
  5. package/dist/advanced.d.ts +2 -0
  6. package/dist/advanced.d.ts.map +1 -1
  7. package/dist/advanced.js +2 -0
  8. package/dist/archive-input.d.ts.map +1 -1
  9. package/dist/archive-input.js +6 -2
  10. package/dist/archive-merge.d.ts.map +1 -1
  11. package/dist/archive-merge.js +15 -2
  12. package/dist/archive-native.d.ts.map +1 -1
  13. package/dist/archive-native.js +7 -19
  14. package/dist/archive-plan.js +1 -1
  15. package/dist/archive-read.d.ts.map +1 -1
  16. package/dist/archive-read.js +18 -13
  17. package/dist/archive-staging.d.ts.map +1 -1
  18. package/dist/archive-staging.js +43 -10
  19. package/dist/archive-tar-inspect.d.ts.map +1 -1
  20. package/dist/archive-tar-inspect.js +4 -1
  21. package/dist/archive-zip-admission.d.ts +1 -1
  22. package/dist/archive-zip-admission.d.ts.map +1 -1
  23. package/dist/archive-zip-admission.js +2 -2
  24. package/dist/archive-zip-directory.d.ts +3 -0
  25. package/dist/archive-zip-directory.d.ts.map +1 -1
  26. package/dist/archive-zip-directory.js +13 -2
  27. package/dist/archive-zip-loader.d.ts +3 -2
  28. package/dist/archive-zip-loader.d.ts.map +1 -1
  29. package/dist/archive-zip-loader.js +30 -4
  30. package/dist/archive-zip-manifest.d.ts +5 -0
  31. package/dist/archive-zip-manifest.d.ts.map +1 -0
  32. package/dist/archive-zip-manifest.js +22 -0
  33. package/dist/archive-zip-names.d.ts +6 -1
  34. package/dist/archive-zip-names.d.ts.map +1 -1
  35. package/dist/archive-zip-names.js +35 -14
  36. package/dist/archive-zip-preflight.d.ts.map +1 -1
  37. package/dist/archive-zip-preflight.js +3 -2
  38. package/dist/archive.d.ts.map +1 -1
  39. package/dist/archive.js +9 -4
  40. package/dist/copy-file-input.d.ts +1 -1
  41. package/dist/copy-file-input.d.ts.map +1 -1
  42. package/dist/copy-file-input.js +2 -0
  43. package/dist/darwin-acl.d.ts +4 -0
  44. package/dist/darwin-acl.d.ts.map +1 -0
  45. package/dist/darwin-acl.js +24 -0
  46. package/dist/deny-mutations.d.ts.map +1 -1
  47. package/dist/deny-mutations.js +8 -2
  48. package/dist/directory-durability.d.ts.map +1 -1
  49. package/dist/directory-durability.js +67 -23
  50. package/dist/directory-entry-path.d.ts +3 -0
  51. package/dist/directory-entry-path.d.ts.map +1 -0
  52. package/dist/directory-entry-path.js +21 -0
  53. package/dist/directory-guard.d.ts +17 -1
  54. package/dist/directory-guard.d.ts.map +1 -1
  55. package/dist/directory-guard.js +134 -48
  56. package/dist/directory-mode-node.d.ts +12 -0
  57. package/dist/directory-mode-node.d.ts.map +1 -1
  58. package/dist/directory-mode-node.js +102 -4
  59. package/dist/effective-uid.d.ts +2 -0
  60. package/dist/effective-uid.d.ts.map +1 -0
  61. package/dist/effective-uid.js +25 -0
  62. package/dist/file-handle-transfer.d.ts.map +1 -1
  63. package/dist/file-handle-transfer.js +98 -27
  64. package/dist/file-hash.d.ts.map +1 -1
  65. package/dist/file-hash.js +3 -0
  66. package/dist/file-lock-sync.d.ts.map +1 -1
  67. package/dist/file-lock-sync.js +36 -6
  68. package/dist/file-lock.d.ts.map +1 -1
  69. package/dist/file-lock.js +29 -9
  70. package/dist/file-observation.d.ts +1 -1
  71. package/dist/file-observation.d.ts.map +1 -1
  72. package/dist/file-store-boundary.d.ts +6 -2
  73. package/dist/file-store-boundary.d.ts.map +1 -1
  74. package/dist/file-store-boundary.js +20 -65
  75. package/dist/file-store-copy-source.d.ts +5 -0
  76. package/dist/file-store-copy-source.d.ts.map +1 -0
  77. package/dist/file-store-copy-source.js +31 -0
  78. package/dist/file-store-path.d.ts.map +1 -1
  79. package/dist/file-store-path.js +4 -1
  80. package/dist/file-store-sync-directory.d.ts +16 -0
  81. package/dist/file-store-sync-directory.d.ts.map +1 -0
  82. package/dist/file-store-sync-directory.js +349 -0
  83. package/dist/file-store.d.ts.map +1 -1
  84. package/dist/file-store.js +11 -28
  85. package/dist/filename.d.ts.map +1 -1
  86. package/dist/filename.js +65 -20
  87. package/dist/fs.d.ts.map +1 -1
  88. package/dist/fs.js +3 -2
  89. package/dist/guarded-mkdir.d.ts +8 -0
  90. package/dist/guarded-mkdir.d.ts.map +1 -1
  91. package/dist/guarded-mkdir.js +176 -27
  92. package/dist/guest.d.ts.map +1 -1
  93. package/dist/guest.js +4 -1
  94. package/dist/home-dir.d.ts.map +1 -1
  95. package/dist/home-dir.js +73 -10
  96. package/dist/install-path.d.ts.map +1 -1
  97. package/dist/install-path.js +54 -19
  98. package/dist/json-durable-queue-ownership.d.ts +6 -0
  99. package/dist/json-durable-queue-ownership.d.ts.map +1 -1
  100. package/dist/json-durable-queue-ownership.js +89 -42
  101. package/dist/json-durable-queue-paths.d.ts +11 -0
  102. package/dist/json-durable-queue-paths.d.ts.map +1 -0
  103. package/dist/json-durable-queue-paths.js +42 -0
  104. package/dist/json-durable-queue-read.d.ts.map +1 -1
  105. package/dist/json-durable-queue-read.js +2 -0
  106. package/dist/json-durable-queue.d.ts.map +1 -1
  107. package/dist/json-durable-queue.js +50 -58
  108. package/dist/json-store.d.ts.map +1 -1
  109. package/dist/json-store.js +5 -1
  110. package/dist/json.d.ts.map +1 -1
  111. package/dist/json.js +54 -22
  112. package/dist/local-file-access.d.ts.map +1 -1
  113. package/dist/local-file-access.js +4 -0
  114. package/dist/local-file-descriptor.d.ts +17 -0
  115. package/dist/local-file-descriptor.d.ts.map +1 -0
  116. package/dist/local-file-descriptor.js +84 -0
  117. package/dist/local-roots.d.ts.map +1 -1
  118. package/dist/local-roots.js +35 -7
  119. package/dist/move-path-cleanup.d.ts +2 -0
  120. package/dist/move-path-cleanup.d.ts.map +1 -1
  121. package/dist/move-path-cleanup.js +44 -18
  122. package/dist/move-path.d.ts.map +1 -1
  123. package/dist/move-path.js +31 -6
  124. package/dist/native-binding.d.ts +17 -0
  125. package/dist/native-binding.d.ts.map +1 -1
  126. package/dist/native-binding.js +8 -1
  127. package/dist/native-directory-observation.d.ts +17 -0
  128. package/dist/native-directory-observation.d.ts.map +1 -0
  129. package/dist/native-directory-observation.js +37 -0
  130. package/dist/native-operations.d.ts.map +1 -1
  131. package/dist/native-operations.js +6 -4
  132. package/dist/native-parent-admission.d.ts +32 -0
  133. package/dist/native-parent-admission.d.ts.map +1 -0
  134. package/dist/native-parent-admission.js +127 -0
  135. package/dist/native-pinned-write-windows.d.ts +0 -1
  136. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  137. package/dist/native-pinned-write-windows.js +8 -9
  138. package/dist/native-pinned-write.d.ts.map +1 -1
  139. package/dist/native-pinned-write.js +314 -42
  140. package/dist/native-staged-file.d.ts +4 -4
  141. package/dist/native-staged-file.d.ts.map +1 -1
  142. package/dist/native-staged-file.js +20 -10
  143. package/dist/native.d.ts +1 -1
  144. package/dist/native.d.ts.map +1 -1
  145. package/dist/native.js +7 -2
  146. package/dist/output.d.ts.map +1 -1
  147. package/dist/output.js +15 -5
  148. package/dist/overwrite-file-handle.d.ts.map +1 -1
  149. package/dist/overwrite-file-handle.js +5 -1
  150. package/dist/owner-dacl.d.ts.map +1 -1
  151. package/dist/owner-dacl.js +2 -0
  152. package/dist/path-policy.d.ts.map +1 -1
  153. package/dist/path-policy.js +7 -2
  154. package/dist/path-prefix.d.ts +7 -0
  155. package/dist/path-prefix.d.ts.map +1 -0
  156. package/dist/path-prefix.js +82 -0
  157. package/dist/path-scope-lexical.d.ts.map +1 -1
  158. package/dist/path-scope-lexical.js +18 -8
  159. package/dist/path-segment-route.d.ts +7 -0
  160. package/dist/path-segment-route.d.ts.map +1 -0
  161. package/dist/path-segment-route.js +24 -0
  162. package/dist/path-suffix-aliases.d.ts +10 -0
  163. package/dist/path-suffix-aliases.d.ts.map +1 -0
  164. package/dist/path-suffix-aliases.js +386 -0
  165. package/dist/path.d.ts.map +1 -1
  166. package/dist/path.js +16 -5
  167. package/dist/permissions-windows.d.ts.map +1 -1
  168. package/dist/permissions-windows.js +14 -3
  169. package/dist/permissions.d.ts.map +1 -1
  170. package/dist/permissions.js +37 -8
  171. package/dist/pinned-mutation-admission.d.ts +25 -0
  172. package/dist/pinned-mutation-admission.d.ts.map +1 -0
  173. package/dist/pinned-mutation-admission.js +425 -0
  174. package/dist/pinned-mutation-observation.d.ts +34 -0
  175. package/dist/pinned-mutation-observation.d.ts.map +1 -0
  176. package/dist/pinned-mutation-observation.js +142 -0
  177. package/dist/pinned-mutation-shared-route.d.ts +24 -0
  178. package/dist/pinned-mutation-shared-route.d.ts.map +1 -0
  179. package/dist/pinned-mutation-shared-route.js +70 -0
  180. package/dist/pinned-open.d.ts +6 -0
  181. package/dist/pinned-open.d.ts.map +1 -1
  182. package/dist/pinned-open.js +28 -10
  183. package/dist/pinned-write-types.d.ts +75 -0
  184. package/dist/pinned-write-types.d.ts.map +1 -0
  185. package/dist/pinned-write-types.js +1 -0
  186. package/dist/pinned-write.d.ts +7 -33
  187. package/dist/pinned-write.d.ts.map +1 -1
  188. package/dist/pinned-write.js +151 -17
  189. package/dist/private-directory.d.ts.map +1 -1
  190. package/dist/private-directory.js +2 -0
  191. package/dist/private-producer-handoff.d.ts +16 -0
  192. package/dist/private-producer-handoff.d.ts.map +1 -0
  193. package/dist/private-producer-handoff.js +272 -0
  194. package/dist/private-temp-workspace.d.ts +2 -39
  195. package/dist/private-temp-workspace.d.ts.map +1 -1
  196. package/dist/private-temp-workspace.js +183 -77
  197. package/dist/publish-file.d.ts.map +1 -1
  198. package/dist/publish-file.js +38 -32
  199. package/dist/regular-file.d.ts.map +1 -1
  200. package/dist/regular-file.js +56 -45
  201. package/dist/replace-directory.d.ts.map +1 -1
  202. package/dist/replace-directory.js +12 -5
  203. package/dist/replace-file-temp-owner.d.ts +1 -0
  204. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  205. package/dist/replace-file-temp-owner.js +12 -1
  206. package/dist/replace-file.d.ts.map +1 -1
  207. package/dist/replace-file.js +24 -24
  208. package/dist/root-boundary.d.ts +4 -0
  209. package/dist/root-boundary.d.ts.map +1 -1
  210. package/dist/root-boundary.js +6 -1
  211. package/dist/root-context.d.ts +12 -1
  212. package/dist/root-context.d.ts.map +1 -1
  213. package/dist/root-context.js +57 -11
  214. package/dist/root-directory-creation.d.ts +13 -0
  215. package/dist/root-directory-creation.d.ts.map +1 -0
  216. package/dist/root-directory-creation.js +212 -0
  217. package/dist/root-directory-list.d.ts +16 -4
  218. package/dist/root-directory-list.d.ts.map +1 -1
  219. package/dist/root-directory-list.js +180 -39
  220. package/dist/root-directory.d.ts +23 -0
  221. package/dist/root-directory.d.ts.map +1 -0
  222. package/dist/root-directory.js +141 -0
  223. package/dist/root-errors.d.ts.map +1 -1
  224. package/dist/root-errors.js +12 -1
  225. package/dist/root-file-final-admission.d.ts +21 -0
  226. package/dist/root-file-final-admission.d.ts.map +1 -0
  227. package/dist/root-file-final-admission.js +83 -0
  228. package/dist/root-file.d.ts.map +1 -1
  229. package/dist/root-file.js +103 -24
  230. package/dist/root-impl.d.ts +1 -0
  231. package/dist/root-impl.d.ts.map +1 -1
  232. package/dist/root-impl.js +289 -317
  233. package/dist/root-move-noreplace.d.ts +14 -0
  234. package/dist/root-move-noreplace.d.ts.map +1 -0
  235. package/dist/root-move-noreplace.js +202 -0
  236. package/dist/root-observed-path.d.ts +9 -0
  237. package/dist/root-observed-path.d.ts.map +1 -0
  238. package/dist/root-observed-path.js +95 -0
  239. package/dist/root-path-errors.d.ts +12 -0
  240. package/dist/root-path-errors.d.ts.map +1 -0
  241. package/dist/root-path-errors.js +13 -0
  242. package/dist/root-path-existing.d.ts.map +1 -1
  243. package/dist/root-path-existing.js +53 -20
  244. package/dist/root-path-observation.d.ts +63 -0
  245. package/dist/root-path-observation.d.ts.map +1 -0
  246. package/dist/root-path-observation.js +180 -0
  247. package/dist/root-path-stat.d.ts +5 -0
  248. package/dist/root-path-stat.d.ts.map +1 -0
  249. package/dist/root-path-stat.js +101 -0
  250. package/dist/root-path-symlink.d.ts.map +1 -1
  251. package/dist/root-path-symlink.js +23 -4
  252. package/dist/root-path.d.ts +11 -0
  253. package/dist/root-path.d.ts.map +1 -1
  254. package/dist/root-path.js +215 -53
  255. package/dist/root-paths-lexical.d.ts +9 -0
  256. package/dist/root-paths-lexical.d.ts.map +1 -0
  257. package/dist/root-paths-lexical.js +22 -0
  258. package/dist/root-paths.d.ts +3 -25
  259. package/dist/root-paths.d.ts.map +1 -1
  260. package/dist/root-paths.js +77 -154
  261. package/dist/root-read-admission.d.ts +26 -0
  262. package/dist/root-read-admission.d.ts.map +1 -0
  263. package/dist/root-read-admission.js +94 -0
  264. package/dist/root-remove-identity.d.ts +15 -0
  265. package/dist/root-remove-identity.d.ts.map +1 -0
  266. package/dist/root-remove-identity.js +89 -0
  267. package/dist/root-remove-receipt.d.ts +17 -0
  268. package/dist/root-remove-receipt.d.ts.map +1 -0
  269. package/dist/root-remove-receipt.js +37 -0
  270. package/dist/root-remove.d.ts +2 -1
  271. package/dist/root-remove.d.ts.map +1 -1
  272. package/dist/root-remove.js +172 -10
  273. package/dist/root-write-admission.d.ts +68 -0
  274. package/dist/root-write-admission.d.ts.map +1 -0
  275. package/dist/root-write-admission.js +322 -0
  276. package/dist/root-write-compatibility.d.ts +13 -0
  277. package/dist/root-write-compatibility.d.ts.map +1 -0
  278. package/dist/root-write-compatibility.js +74 -0
  279. package/dist/root-write-complete-parent.d.ts +42 -0
  280. package/dist/root-write-complete-parent.d.ts.map +1 -0
  281. package/dist/root-write-complete-parent.js +195 -0
  282. package/dist/root-write-publication.d.ts +24 -0
  283. package/dist/root-write-publication.d.ts.map +1 -0
  284. package/dist/root-write-publication.js +85 -0
  285. package/dist/root-write-verification.d.ts.map +1 -1
  286. package/dist/root-write-verification.js +6 -4
  287. package/dist/secret-file.d.ts.map +1 -1
  288. package/dist/secret-file.js +62 -24
  289. package/dist/secret-read-async.d.ts.map +1 -1
  290. package/dist/secret-read-async.js +18 -13
  291. package/dist/secret-read-policy.d.ts.map +1 -1
  292. package/dist/secret-read-policy.js +5 -1
  293. package/dist/secure-file.d.ts.map +1 -1
  294. package/dist/secure-file.js +92 -7
  295. package/dist/secure-temp-dir.d.ts +6 -3
  296. package/dist/secure-temp-dir.d.ts.map +1 -1
  297. package/dist/secure-temp-dir.js +121 -101
  298. package/dist/secure-temp-repair.d.ts +35 -0
  299. package/dist/secure-temp-repair.d.ts.map +1 -0
  300. package/dist/secure-temp-repair.js +106 -0
  301. package/dist/sibling-staged-file.d.ts +3 -2
  302. package/dist/sibling-staged-file.d.ts.map +1 -1
  303. package/dist/sibling-staged-file.js +179 -55
  304. package/dist/sibling-temp.d.ts.map +1 -1
  305. package/dist/sibling-temp.js +27 -13
  306. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  307. package/dist/sidecar-lock-acquire.js +44 -14
  308. package/dist/sidecar-lock-root.d.ts.map +1 -1
  309. package/dist/sidecar-lock-root.js +4 -2
  310. package/dist/staged-directory.d.ts +14 -5
  311. package/dist/staged-directory.d.ts.map +1 -1
  312. package/dist/staged-directory.js +54 -3
  313. package/dist/standalone-publication-path.d.ts +2 -0
  314. package/dist/standalone-publication-path.d.ts.map +1 -0
  315. package/dist/standalone-publication-path.js +6 -0
  316. package/dist/stat-observation.d.ts +11 -0
  317. package/dist/stat-observation.d.ts.map +1 -0
  318. package/dist/stat-observation.js +64 -0
  319. package/dist/strict-file-identity.d.ts.map +1 -1
  320. package/dist/strict-file-identity.js +39 -2
  321. package/dist/symlink-parents.d.ts.map +1 -1
  322. package/dist/symlink-parents.js +7 -2
  323. package/dist/temp-target.d.ts +2 -0
  324. package/dist/temp-target.d.ts.map +1 -1
  325. package/dist/temp-target.js +130 -3
  326. package/dist/temp-workspace-admission.d.ts +22 -0
  327. package/dist/temp-workspace-admission.d.ts.map +1 -0
  328. package/dist/temp-workspace-admission.js +374 -0
  329. package/dist/temp-workspace-child-admission.d.ts +13 -0
  330. package/dist/temp-workspace-child-admission.d.ts.map +1 -0
  331. package/dist/temp-workspace-child-admission.js +95 -0
  332. package/dist/temp-workspace-descriptor.d.ts +48 -0
  333. package/dist/temp-workspace-descriptor.d.ts.map +1 -0
  334. package/dist/temp-workspace-descriptor.js +363 -0
  335. package/dist/temp-workspace-identity.d.ts +15 -0
  336. package/dist/temp-workspace-identity.d.ts.map +1 -0
  337. package/dist/temp-workspace-identity.js +41 -0
  338. package/dist/temp-workspace-owner.d.ts +7 -6
  339. package/dist/temp-workspace-owner.d.ts.map +1 -1
  340. package/dist/temp-workspace-owner.js +121 -61
  341. package/dist/temp-workspace-permissions.d.ts +4 -0
  342. package/dist/temp-workspace-permissions.d.ts.map +1 -0
  343. package/dist/temp-workspace-permissions.js +32 -0
  344. package/dist/temp-workspace-types.d.ts +40 -0
  345. package/dist/temp-workspace-types.d.ts.map +1 -0
  346. package/dist/temp-workspace-types.js +1 -0
  347. package/dist/temp.d.ts +1 -1
  348. package/dist/temp.d.ts.map +1 -1
  349. package/dist/temp.js +1 -1
  350. package/dist/test-hooks.d.ts +7 -0
  351. package/dist/test-hooks.d.ts.map +1 -1
  352. package/dist/text-atomic.d.ts.map +1 -1
  353. package/dist/text-atomic.js +3 -1
  354. package/dist/trash.d.ts.map +1 -1
  355. package/dist/trash.js +54 -8
  356. package/dist/walk.d.ts.map +1 -1
  357. package/dist/walk.js +9 -6
  358. package/dist/windows-owner.d.ts.map +1 -1
  359. package/dist/windows-owner.js +5 -0
  360. package/dist/windows-path-alias.d.ts +39 -0
  361. package/dist/windows-path-alias.d.ts.map +1 -0
  362. package/dist/windows-path-alias.js +153 -0
  363. package/docs/advanced.md +25 -0
  364. package/docs/archive.md +23 -2
  365. package/docs/atomic.md +27 -3
  366. package/docs/copy.md +3 -0
  367. package/docs/durability.md +14 -0
  368. package/docs/errors.md +14 -6
  369. package/docs/file-store.md +24 -3
  370. package/docs/filename.md +14 -7
  371. package/docs/install-path.md +2 -2
  372. package/docs/install.md +8 -7
  373. package/docs/json-store.md +5 -1
  374. package/docs/json.md +11 -0
  375. package/docs/mutation-policy-proof.md +65 -0
  376. package/docs/native-helper.md +41 -11
  377. package/docs/native.md +32 -8
  378. package/docs/output.md +33 -11
  379. package/docs/path-prefix.md +64 -0
  380. package/docs/path-suffix-aliases.md +159 -0
  381. package/docs/path.md +11 -0
  382. package/docs/private-file-store.md +14 -0
  383. package/docs/public-api.md +9 -0
  384. package/docs/quickstart.md +1 -1
  385. package/docs/reading.md +8 -1
  386. package/docs/root.md +18 -3
  387. package/docs/secret-file.md +3 -0
  388. package/docs/secure-file.md +6 -2
  389. package/docs/security-model.md +75 -8
  390. package/docs/sidecar-lock.md +28 -1
  391. package/docs/store.md +5 -1
  392. package/docs/temp.md +260 -36
  393. package/docs/test-hooks.md +14 -0
  394. package/docs/writing.md +38 -8
  395. package/package.json +10 -10
package/dist/filename.js CHANGED
@@ -2,6 +2,8 @@ import path from "node:path";
2
2
  import { WINDOWS_RESERVED_DEVICE_NAMES } from "./device-path.js";
3
3
  import { maxNormalizedUtf8Bytes } from "./unicode-path.js";
4
4
  const INVALID_FILE_NAME_CHARACTERS = /[\u0000-\u001f\u007f-\u009f<>:"/\\|?*]/g;
5
+ const HAS_INVALID_FILE_NAME_CHARACTER = /[\u0000-\u001f\u007f-\u009f<>:"/\\|?*]/;
6
+ const HAS_RETAINED_FILE_NAME_CHARACTER = /[^\u0000-\u001f\u007f-\u009f<>:"/\\|?*\s]/;
5
7
  function canStartWindowsDeviceName(character) {
6
8
  const folded = character | 0x20;
7
9
  return folded === 0x61 || folded === 0x63 || folded === 0x6c ||
@@ -33,18 +35,44 @@ export function suffixWindowsReservedDeviceName(fileName) {
33
35
  return `${fileName.slice(0, baseNameEnd)}_${fileName.slice(baseNameEnd)}`;
34
36
  }
35
37
  const PORTABLE_FILE_NAME_BYTES = 255;
36
- const UNTRUSTED_FILE_NAME_CODE_UNITS = 200;
37
- function truncateWithoutSplittingSurrogatePair(value, maxLength) {
38
- let truncated = value.slice(0, maxLength);
38
+ const SANITIZED_FILE_NAME_CODE_UNITS = 200;
39
+ const SAFE_FALLBACK_FILE_NAME = "file";
40
+ /**
41
+ * Uses native string and regexp operations to recognize the full sanitizer's
42
+ * bounded fixed points. The separate non-global regexp keeps this predicate
43
+ * stateless, and the authoritative device-name transform remains the final
44
+ * admission check.
45
+ */
46
+ function isBoundedSanitizedFileName(fileName, trimmedFileName) {
47
+ return typeof fileName === "string" &&
48
+ fileName.length > 0 &&
49
+ fileName.length <= SANITIZED_FILE_NAME_CODE_UNITS &&
50
+ fileName !== "." &&
51
+ fileName !== ".." &&
52
+ trimmedFileName === fileName &&
53
+ !HAS_INVALID_FILE_NAME_CHARACTER.test(fileName) &&
54
+ suffixWindowsReservedDeviceName(fileName) === fileName;
55
+ }
56
+ function hasWindowsDrivePrefix(value) {
57
+ if (value.length < 2 || value.charCodeAt(1) !== 0x3a)
58
+ return false;
59
+ const firstCodeUnit = value.charCodeAt(0);
60
+ return (firstCodeUnit >= 0x41 && firstCodeUnit <= 0x5a) ||
61
+ (firstCodeUnit >= 0x61 && firstCodeUnit <= 0x7a);
62
+ }
63
+ function normalizedFileNameBytes(value) {
64
+ return maxNormalizedUtf8Bytes(value, true);
65
+ }
66
+ function truncateCodeUnitsWithoutSplittingSurrogate(value, limit) {
67
+ if (value.length <= limit)
68
+ return value;
69
+ let truncated = value.slice(0, limit);
39
70
  const trailingCodeUnit = truncated.charCodeAt(truncated.length - 1);
40
71
  if (trailingCodeUnit >= 0xd800 && trailingCodeUnit <= 0xdbff) {
41
72
  truncated = truncated.slice(0, -1);
42
73
  }
43
74
  return truncated;
44
75
  }
45
- function normalizedFileNameBytes(value) {
46
- return maxNormalizedUtf8Bytes(value, true);
47
- }
48
76
  /** Keeps short names exact and trims only the filename tail of a composite temp name. */
49
77
  export function fitFileNameToPortableComponent(params) {
50
78
  const complete = `${params.prefix}${params.fileName}${params.suffix}`;
@@ -77,23 +105,40 @@ export function fitFileNameToPortableComponent(params) {
77
105
  }
78
106
  return `${codePoints.slice(0, low).join("")}${tailSuffix}`;
79
107
  }
80
- export function sanitizeUntrustedFileName(fileName, fallbackName) {
81
- const trimmed = typeof fileName === "string" ? fileName.trim() : "";
82
- if (!trimmed) {
83
- return fallbackName;
108
+ function sanitizeFileNameCandidate(fileName) {
109
+ if (typeof fileName !== "string")
110
+ return undefined;
111
+ const trimmed = fileName.trim();
112
+ if (!trimmed)
113
+ return undefined;
114
+ if (isBoundedSanitizedFileName(fileName, trimmed))
115
+ return fileName;
116
+ // Basename selection cannot manufacture a character, and the complete
117
+ // sanitizer removes this same invalid set before its final trim.
118
+ if (!HAS_RETAINED_FILE_NAME_CHARACTER.test(trimmed))
119
+ return undefined;
120
+ let base = trimmed;
121
+ if (base.includes("/"))
122
+ base = path.posix.basename(base);
123
+ if (base.includes("\\") || hasWindowsDrivePrefix(base)) {
124
+ base = path.win32.basename(base);
84
125
  }
85
- let base = path.posix.basename(trimmed);
86
- base = path.win32.basename(base);
87
126
  base = base.replace(INVALID_FILE_NAME_CHARACTERS, "").trim();
88
127
  if (!base || base === "." || base === "..") {
89
- return fallbackName;
128
+ return undefined;
90
129
  }
91
- base = suffixWindowsReservedDeviceName(base);
92
- if (base.length > UNTRUSTED_FILE_NAME_CODE_UNITS) {
93
- base = truncateWithoutSplittingSurrogatePair(base, UNTRUSTED_FILE_NAME_CODE_UNITS);
94
- if (suffixWindowsReservedDeviceName(base) !== base) {
95
- base = suffixWindowsReservedDeviceName(truncateWithoutSplittingSurrogatePair(base, UNTRUSTED_FILE_NAME_CODE_UNITS - 1));
96
- }
130
+ base = truncateCodeUnitsWithoutSplittingSurrogate(base, SANITIZED_FILE_NAME_CODE_UNITS);
131
+ let safeBase = suffixWindowsReservedDeviceName(base);
132
+ if (safeBase.length > SANITIZED_FILE_NAME_CODE_UNITS) {
133
+ // The safety suffix is the final invariant. Shorten the unsuffixed tail so
134
+ // truncation cannot turn a padded reserved stem back into a device name.
135
+ base = truncateCodeUnitsWithoutSplittingSurrogate(base, SANITIZED_FILE_NAME_CODE_UNITS - 1);
136
+ safeBase = suffixWindowsReservedDeviceName(base);
97
137
  }
98
- return base;
138
+ return safeBase;
139
+ }
140
+ export function sanitizeUntrustedFileName(fileName, fallbackName) {
141
+ return (sanitizeFileNameCandidate(fileName) ??
142
+ sanitizeFileNameCandidate(fallbackName) ??
143
+ SAFE_FALLBACK_FILE_NAME);
99
144
  }
package/dist/fs.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../src/fs.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOnE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAOxD"}
1
+ {"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../src/fs.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOnE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAOxD"}
package/dist/fs.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import fs from "node:fs";
2
+ import { pathForWindowsFilesystem } from "./windows-path-alias.js";
2
3
  /**
3
4
  * Returns true when `fs.stat()` can stat the path.
4
5
  *
@@ -7,7 +8,7 @@ import fs from "node:fs";
7
8
  */
8
9
  export async function pathExists(filePath) {
9
10
  try {
10
- fs.statSync(filePath);
11
+ fs.statSync(pathForWindowsFilesystem(filePath));
11
12
  return true;
12
13
  }
13
14
  catch {
@@ -20,7 +21,7 @@ export async function pathExists(filePath) {
20
21
  */
21
22
  export function pathExistsSync(filePath) {
22
23
  try {
23
- fs.statSync(filePath);
24
+ fs.statSync(pathForWindowsFilesystem(filePath));
24
25
  return true;
25
26
  }
26
27
  catch {
@@ -1,4 +1,6 @@
1
1
  import { type RootBoundaryIdentity } from "./root-boundary.js";
2
+ import { type MutationDirectoryObservation } from "./pinned-mutation-observation.js";
3
+ import type { PinnedCreatedDirectoryReceipt, PinnedMutationAdmissionReceipt, PinnedMutationAuthorizationToken } from "./pinned-write.js";
2
4
  /**
3
5
  * Creates each missing path component from `rootReal` down to `targetPath`,
4
6
  * guarding every step. Returns the real (symlink-resolved) path of the final
@@ -9,9 +11,15 @@ export declare function mkdirPathComponentsWithGuards(params: {
9
11
  rootReal: string;
10
12
  targetPath: string;
11
13
  beforeComponent?: (componentPath: string) => Promise<void> | void;
14
+ beforeCreateComponent?: (componentPath: string, prospectiveTargetPath: string, retainedTargetPath: string | undefined, parent: MutationDirectoryObservation) => Promise<PinnedMutationAdmissionReceipt | undefined> | PinnedMutationAdmissionReceipt | undefined;
15
+ beforeUseComponent?: (componentPath: string, prospectiveTargetPath: string, retainedTargetPath: string | undefined) => Promise<void> | void;
16
+ afterCreateComponent?: (receipt: PinnedCreatedDirectoryReceipt) => PinnedMutationAuthorizationToken | undefined;
12
17
  assertBeforeMutation?: () => void;
13
18
  mode?: number;
14
19
  rejectSymlinks?: boolean;
20
+ revalidateParentAfterBeforeComponent?: boolean;
21
+ synchronousAuthorizationIncludesFence?: boolean;
22
+ retainedTargetPath?: string;
15
23
  rootIdentity?: RootBoundaryIdentity;
16
24
  }): Promise<string>;
17
25
  //# sourceMappingURL=guarded-mkdir.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"guarded-mkdir.d.ts","sourceRoot":"","sources":["../src/guarded-mkdir.ts"],"names":[],"mappings":"AAYA,OAAO,EAAuB,KAAK,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAgCpF;;;;;GAKG;AACH,wBAAsB,6BAA6B,CAAC,MAAM,EAAE;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAClE,oBAAoB,CAAC,EAAE,MAAM,IAAI,CAAC;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,YAAY,CAAC,EAAE,oBAAoB,CAAC;CACrC,GAAG,OAAO,CAAC,MAAM,CAAC,CAqFlB"}
1
+ {"version":3,"file":"guarded-mkdir.d.ts","sourceRoot":"","sources":["../src/guarded-mkdir.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAuB,KAAK,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AACpF,OAAO,EAA4B,KAAK,4BAA4B,EAAE,MAAM,kCAAkC,CAAC;AAC/G,OAAO,KAAK,EACV,6BAA6B,EAC7B,8BAA8B,EAC9B,gCAAgC,EACjC,MAAM,mBAAmB,CAAC;AAuG3B;;;;;GAKG;AACH,wBAAsB,6BAA6B,CAAC,MAAM,EAAE;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAClE,qBAAqB,CAAC,EAAE,CACtB,aAAa,EAAE,MAAM,EACrB,qBAAqB,EAAE,MAAM,EAC7B,kBAAkB,EAAE,MAAM,GAAG,SAAS,EACtC,MAAM,EAAE,4BAA4B,KACjC,OAAO,CAAC,8BAA8B,GAAG,SAAS,CAAC,GAAG,8BAA8B,GAAG,SAAS,CAAC;IACtG,kBAAkB,CAAC,EAAE,CACnB,aAAa,EAAE,MAAM,EACrB,qBAAqB,EAAE,MAAM,EAC7B,kBAAkB,EAAE,MAAM,GAAG,SAAS,KACnC,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1B,oBAAoB,CAAC,EAAE,CACrB,OAAO,EAAE,6BAA6B,KACnC,gCAAgC,GAAG,SAAS,CAAC;IAClD,oBAAoB,CAAC,EAAE,MAAM,IAAI,CAAC;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,oCAAoC,CAAC,EAAE,OAAO,CAAC;IAC/C,qCAAqC,CAAC,EAAE,OAAO,CAAC;IAChD,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,YAAY,CAAC,EAAE,oBAAoB,CAAC;CACrC,GAAG,OAAO,CAAC,MAAM,CAAC,CAyMlB"}
@@ -1,12 +1,58 @@
1
- import fsSync from "node:fs";
1
+ import fsSync, {} from "node:fs";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, } from "./directory-guard.js";
4
+ import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, inspectDirectoryIdentitySync, } from "./directory-guard.js";
5
5
  import { FsSafeError } from "./errors.js";
6
6
  import { isNotFoundPathError, isPathRelativeEscape } from "./path.js";
7
7
  import { directoryComponentNotDirectoryError, rootPathChangedError } from "./root-errors.js";
8
+ import { assertNoWindowsPathAlias, pathForWindowsFilesystem, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
8
9
  import { realpathSync } from "./realpath.js";
9
10
  import { admitPathInsideRoot } from "./root-boundary.js";
11
+ import { checkedMutationDirectory } from "./pinned-mutation-observation.js";
12
+ import { createPathSegmentRoute, joinPathSegmentRoute, } from "./path-segment-route.js";
13
+ function sameDirectoryFacts(left, right) {
14
+ return left.dev === right.dev && left.ino === right.ino && left.mode === right.mode &&
15
+ left.nlink === right.nlink;
16
+ }
17
+ function inspectGuardCurrent(parent) {
18
+ const stat = inspectDirectoryIdentitySync(parent.dir, parent.stat);
19
+ if (realpathSync.native(parent.dir) !== parent.realPath) {
20
+ throw new FsSafeError("path-mismatch", "directory changed during operation");
21
+ }
22
+ return stat;
23
+ }
24
+ function observedGuard(parent) {
25
+ return checkedMutationDirectory(parent.dir, parent.realPath, parent.stat);
26
+ }
27
+ function createdDirectoryEvidence(admission, parent, childPath) {
28
+ try {
29
+ const parentAfter = inspectGuardCurrent(parent);
30
+ const childBefore = inspectDirectoryIdentitySync(childPath);
31
+ const realPath = realpathSync.native(childPath);
32
+ if (realPath !== childPath)
33
+ return undefined;
34
+ const childAfter = inspectDirectoryIdentitySync(childPath, childBefore);
35
+ if (!sameDirectoryFacts(childBefore, childAfter))
36
+ return undefined;
37
+ const child = checkedMutationDirectory(childPath, realPath, childAfter);
38
+ const receipt = Object.freeze({
39
+ admission,
40
+ parent: checkedMutationDirectory(parent.dir, parent.realPath, parentAfter),
41
+ child,
42
+ });
43
+ return Object.freeze({
44
+ receipt,
45
+ guard: Object.freeze({ dir: childPath, realPath, stat: childAfter }),
46
+ });
47
+ }
48
+ catch {
49
+ // Failed optional evidence leaves the existing ordered admission in charge.
50
+ return undefined;
51
+ }
52
+ }
53
+ function sameAbsolutePath(left, right) {
54
+ return path.relative(path.resolve(left), path.resolve(right)) === "";
55
+ }
10
56
  function suppliedExactRootIdentity(identity) {
11
57
  return typeof identity?.dev === "bigint" && typeof identity.ino === "bigint"
12
58
  ? { dev: identity.dev, ino: identity.ino }
@@ -17,9 +63,14 @@ function assertGuardMatchesRootIdentity(guard, expected) {
17
63
  throw rootPathChangedError();
18
64
  }
19
65
  }
66
+ function isPromiseLike(value) {
67
+ return typeof value?.then === "function";
68
+ }
20
69
  async function realpathOrThrowNotFile(target) {
21
70
  try {
22
- return path.resolve(realpathSync.native(target));
71
+ const canonical = realpathSync.native(pathForWindowsFilesystem(target));
72
+ assertNoWindowsPathAlias(canonical, "filesystem", "canonical directory uses a Windows filesystem namespace alias");
73
+ return resolvePathPreservingWindowsRoot(canonical);
23
74
  }
24
75
  catch (error) {
25
76
  if (isNotFoundPathError(error)) {
@@ -37,20 +88,25 @@ async function realpathOrThrowNotFile(target) {
37
88
  * re-deriving it from the original, possibly-symlinked, lexical path.
38
89
  */
39
90
  export async function mkdirPathComponentsWithGuards(params) {
40
- const root = path.resolve(params.rootReal);
41
- const configuredRootGuard = await createAsyncDirectoryGuard(root, { bigint: true });
91
+ const rawRootReal = params.rootReal;
92
+ assertNoWindowsPathAlias(rawRootReal, "filesystem", "root directory uses a Windows filesystem namespace alias");
93
+ const rawTargetPath = params.targetPath;
94
+ assertNoWindowsPathAlias(rawTargetPath, "filesystem", "target directory uses a Windows filesystem namespace alias");
95
+ const root = resolvePathPreservingWindowsRoot(rawRootReal);
96
+ const target = resolvePathPreservingWindowsRoot(rawTargetPath);
42
97
  const suppliedIdentity = suppliedExactRootIdentity(params.rootIdentity);
98
+ const configuredRootGuard = await createAsyncDirectoryGuard(root, { bigint: true });
43
99
  const checkedRootIdentity = suppliedIdentity ?? {
44
100
  dev: configuredRootGuard.stat.dev,
45
101
  ino: configuredRootGuard.stat.ino,
46
102
  };
47
103
  assertGuardMatchesRootIdentity(configuredRootGuard, checkedRootIdentity);
48
- const rootCanonical = path.resolve(configuredRootGuard.realPath);
104
+ assertNoWindowsPathAlias(configuredRootGuard.realPath);
105
+ const rootCanonical = resolvePathPreservingWindowsRoot(configuredRootGuard.realPath);
49
106
  const rootGuard = rootCanonical === root
50
107
  ? configuredRootGuard
51
108
  : await createAsyncDirectoryGuard(rootCanonical, { bigint: true });
52
109
  assertGuardMatchesRootIdentity(rootGuard, checkedRootIdentity);
53
- const target = path.resolve(params.targetPath);
54
110
  const admissionParams = {
55
111
  candidatePath: target,
56
112
  rootIdentity: checkedRootIdentity,
@@ -67,37 +123,119 @@ export async function mkdirPathComponentsWithGuards(params) {
67
123
  }
68
124
  let current = rootCanonical;
69
125
  let currentGuard = rootGuard;
70
- for (const part of admittedTarget.relativePath.split(path.sep).filter(Boolean)) {
126
+ const parts = admittedTarget.relativePath.split(path.sep).filter(Boolean);
127
+ let partRoute;
128
+ let retainedTargetPath = params.retainedTargetPath &&
129
+ sameAbsolutePath(path.dirname(params.retainedTargetPath), target)
130
+ ? params.retainedTargetPath
131
+ : undefined;
132
+ let retainedParentPath = retainedTargetPath ? path.dirname(retainedTargetPath) : undefined;
133
+ for (let index = 0; index < parts.length; index += 1) {
134
+ const part = parts[index];
71
135
  const next = path.join(current, part);
72
136
  const parentGuard = currentGuard;
73
- await assertAsyncDirectoryGuard(parentGuard);
137
+ assertNoWindowsPathAlias(parentGuard.realPath, "filesystem", "canonical parent directory uses a Windows filesystem namespace alias");
138
+ let created = false;
139
+ let createReceipt;
140
+ if (!params.revalidateParentAfterBeforeComponent) {
141
+ await assertAsyncDirectoryGuard(parentGuard);
142
+ }
74
143
  await params.beforeComponent?.(next);
75
- params.assertBeforeMutation?.();
76
- try {
77
- await fs.mkdir(next, { mode: params.mode });
144
+ if (params.revalidateParentAfterBeforeComponent) {
145
+ await assertAsyncDirectoryGuard(parentGuard);
146
+ }
147
+ if (params.beforeCreateComponent) {
148
+ let missing = false;
149
+ try {
150
+ fsSync.lstatSync(next);
151
+ }
152
+ catch (error) {
153
+ if (!isNotFoundPathError(error))
154
+ throw error;
155
+ missing = true;
156
+ }
157
+ if (missing) {
158
+ let prospectiveParent = retainedParentPath;
159
+ if (!prospectiveParent) {
160
+ partRoute ??= createPathSegmentRoute(parts);
161
+ prospectiveParent = joinPathSegmentRoute(next, partRoute, index + 1);
162
+ }
163
+ const authorization = params.beforeCreateComponent(next, prospectiveParent, retainedTargetPath, observedGuard(parentGuard));
164
+ if (isPromiseLike(authorization)) {
165
+ createReceipt = await authorization;
166
+ await assertAsyncDirectoryGuard(parentGuard);
167
+ }
168
+ else {
169
+ createReceipt = authorization;
170
+ // A synchronous authorization must still end at a live pathname
171
+ // fence immediately before the authority callback and mkdir.
172
+ if (!params.synchronousAuthorizationIncludesFence)
173
+ inspectGuardCurrent(parentGuard);
174
+ }
175
+ params.assertBeforeMutation?.();
176
+ // Authority callbacks are an explicit freshness boundary. Recheck the
177
+ // exact parent synchronously so no callback-observed authority is
178
+ // carried into a pathname mkdir after the parent was swapped.
179
+ inspectGuardCurrent(parentGuard);
180
+ try {
181
+ await fs.mkdir(next, { mode: params.mode });
182
+ created = true;
183
+ }
184
+ catch (error) {
185
+ if (!error || typeof error !== "object" || !("code" in error) || error.code !== "EEXIST") {
186
+ throw error;
187
+ }
188
+ }
189
+ }
190
+ }
191
+ else {
192
+ params.assertBeforeMutation?.();
193
+ try {
194
+ await fs.mkdir(next, { mode: params.mode });
195
+ }
196
+ catch (error) {
197
+ if (!error || typeof error !== "object" || !("code" in error) || error.code !== "EEXIST") {
198
+ throw error;
199
+ }
200
+ }
201
+ }
202
+ let createdAuthorization;
203
+ let createdEvidence;
204
+ if (created && createReceipt && params.afterCreateComponent) {
205
+ createdEvidence = createdDirectoryEvidence(createReceipt, parentGuard, next);
206
+ if (createdEvidence) {
207
+ createdAuthorization = params.afterCreateComponent(createdEvidence.receipt);
208
+ }
78
209
  }
79
- catch (error) {
80
- if (!error || typeof error !== "object" || !("code" in error) || error.code !== "EEXIST") {
81
- throw error;
210
+ if (params.beforeUseComponent && !createdAuthorization) {
211
+ let prospectiveParent = retainedParentPath;
212
+ if (!prospectiveParent) {
213
+ partRoute ??= createPathSegmentRoute(parts);
214
+ prospectiveParent = joinPathSegmentRoute(next, partRoute, index + 1);
82
215
  }
216
+ await params.beforeUseComponent(next, prospectiveParent, retainedTargetPath);
217
+ await assertAsyncDirectoryGuard(parentGuard);
83
218
  }
84
- const stat = fsSync.lstatSync(next);
85
- if ((params.rejectSymlinks && stat.isSymbolicLink()) || (!stat.isSymbolicLink() && !stat.isDirectory())) {
219
+ const stat = createdAuthorization ? undefined : fsSync.lstatSync(next);
220
+ if (stat && ((params.rejectSymlinks && stat.isSymbolicLink()) ||
221
+ (!stat.isSymbolicLink() && !stat.isDirectory()))) {
86
222
  throw directoryComponentNotDirectoryError();
87
223
  }
88
224
  // Node's recursive mkdir follows symlinks in missing components. Build one
89
225
  // segment at a time and realpath-check each segment before descending.
90
- const observedNextReal = await realpathOrThrowNotFile(next);
226
+ const nextReal = createdAuthorization && createdEvidence
227
+ ? createdEvidence.receipt.child.canonicalPath
228
+ : await realpathOrThrowNotFile(next);
91
229
  const admittedNextReal = admitPathInsideRoot({
92
230
  rootPath: rootCanonical,
93
- candidatePath: observedNextReal,
231
+ candidatePath: nextReal,
94
232
  rootIdentity: checkedRootIdentity,
95
233
  });
96
234
  if (!admittedNextReal) {
97
235
  throw new FsSafeError("outside-workspace", "directory escaped workspace root");
98
236
  }
99
- const nextReal = admittedNextReal.path;
100
- if (stat.isSymbolicLink()) {
237
+ const admittedNextPath = admittedNextReal.path;
238
+ if (stat?.isSymbolicLink()) {
101
239
  // An existing path component may legitimately be a symlink to a real
102
240
  // directory inside the root (e.g. a skill-bank layout). We already
103
241
  // verified above that it resolves inside the root, so treat the
@@ -107,18 +245,29 @@ export async function mkdirPathComponentsWithGuards(params) {
107
245
  // every subsequent segment. Callers that need the final directory
108
246
  // (e.g. to guard it themselves after this function returns) must use
109
247
  // the returned resolved path, not their own lexical parent path.
110
- const targetStat = fsSync.statSync(nextReal);
248
+ const targetStat = fsSync.statSync(admittedNextPath);
111
249
  if (!targetStat.isDirectory()) {
112
250
  throw directoryComponentNotDirectoryError();
113
251
  }
114
- currentGuard = await createAsyncDirectoryGuard(nextReal);
252
+ currentGuard = await createAsyncDirectoryGuard(admittedNextPath, { bigint: true });
253
+ assertNoWindowsPathAlias(currentGuard.realPath, "filesystem", "canonical directory uses a Windows filesystem namespace alias");
115
254
  await assertAsyncDirectoryGuard(parentGuard);
116
- current = nextReal;
255
+ retainedTargetPath = undefined;
256
+ retainedParentPath = undefined;
257
+ current = admittedNextPath;
117
258
  continue;
118
259
  }
119
- currentGuard = await createAsyncDirectoryGuard(next);
120
- await assertAsyncDirectoryGuard(parentGuard);
121
- current = next;
260
+ if (createdAuthorization && createdEvidence) {
261
+ currentGuard = createdEvidence.guard;
262
+ }
263
+ else {
264
+ currentGuard = await createAsyncDirectoryGuard(admittedNextPath, { bigint: true });
265
+ }
266
+ assertNoWindowsPathAlias(currentGuard.realPath, "filesystem", "canonical directory uses a Windows filesystem namespace alias");
267
+ if (!createdAuthorization || !createdEvidence) {
268
+ await assertAsyncDirectoryGuard(parentGuard);
269
+ }
270
+ current = admittedNextPath;
122
271
  }
123
272
  return current;
124
273
  }
@@ -1 +1 @@
1
- {"version":3,"file":"guest.d.ts","sourceRoot":"","sources":["../src/guest.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,yCAAyC,EAC1C,MAAM,0BAA0B,CAAC;AAElC,yFAAyF;AACzF,eAAO,MAAM,wCAAwC,KAAK,CAAC;AAC3D,8EAA8E;AAC9E,eAAO,MAAM,yCAAyC,IAAI,CAAC;AAE3D,gGAAgG;AAChG,eAAO,MAAM,uBAAuB,QA2ZxB,CAAC;AAEb,OAAO,EAAE,yCAAyC,EAAE,CAAC"}
1
+ {"version":3,"file":"guest.d.ts","sourceRoot":"","sources":["../src/guest.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,yCAAyC,EAC1C,MAAM,0BAA0B,CAAC;AAElC,yFAAyF;AACzF,eAAO,MAAM,wCAAwC,KAAK,CAAC;AAC3D,8EAA8E;AAC9E,eAAO,MAAM,yCAAyC,IAAI,CAAC;AAE3D,gGAAgG;AAChG,eAAO,MAAM,uBAAuB,QA8ZxB,CAAC;AAEb,OAAO,EAAE,yCAAyC,EAAE,CAAC"}
package/dist/guest.js CHANGED
@@ -57,7 +57,10 @@ export const GUEST_FILESYSTEM_PYTHON = [
57
57
  " except FileNotFoundError:",
58
58
  " if not mkdir_enabled:",
59
59
  " raise",
60
- " os.mkdir(segment, 0o777, dir_fd=current_fd)",
60
+ " try:",
61
+ " os.mkdir(segment, 0o777, dir_fd=current_fd)",
62
+ " except FileExistsError:",
63
+ " pass # The competing entry must still pass the no-follow directory open.",
61
64
  " next_fd = open_dir(segment, dir_fd=current_fd)",
62
65
  " os.close(current_fd)",
63
66
  " current_fd = next_fd",
@@ -1 +1 @@
1
- {"version":3,"file":"home-dir.d.ts","sourceRoot":"","sources":["../src/home-dir.ts"],"names":[],"mappings":"AAoBA,wBAAgB,uBAAuB,CACrC,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,OAAO,GAAE,MAAM,MAAmB,GACjC,MAAM,GAAG,SAAS,CAGpB;AAwCD,wBAAgB,sBAAsB,CACpC,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,OAAO,GAAE,MAAM,MAAmB,GACjC,MAAM,CAER;AAED,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE;IACL,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC;CACxB,GACA,MAAM,CAYR;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE;IACL,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC;CACxB,GACA,MAAM,CAaR"}
1
+ {"version":3,"file":"home-dir.d.ts","sourceRoot":"","sources":["../src/home-dir.ts"],"names":[],"mappings":"AAsCA,wBAAgB,uBAAuB,CACrC,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,OAAO,GAAE,MAAM,MAAmB,GACjC,MAAM,GAAG,SAAS,CAOpB;AAwCD,wBAAgB,sBAAsB,CACpC,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,OAAO,GAAE,MAAM,MAAmB,GACjC,MAAM,CAIR;AAED,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE;IACL,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC;CACxB,GACA,MAAM,CAYR;AA4DD,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE;IACL,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,MAAM,CAAC;CACxB,GACA,MAAM,CAoBR"}
package/dist/home-dir.js CHANGED
@@ -1,6 +1,17 @@
1
1
  import os from "node:os";
2
2
  import path from "node:path";
3
+ import process from "node:process";
3
4
  import { normalizeOptionalString } from "./string-coerce.js";
5
+ import { assertNoWindowsPathAlias, assertNoWindowsPathAliasForPlatform, repairResolvedWindowsRoot, resolvePathPreservingWindowsRoot, } from "./windows-path-alias.js";
6
+ const PATH_ALIAS_MESSAGE = "path uses a Windows filesystem namespace alias";
7
+ function isOrdinaryRootedWindowsDrivePath(input) {
8
+ const drive = input.charCodeAt(0);
9
+ return input.length >= 3 &&
10
+ ((drive >= 0x41 && drive <= 0x5a) || (drive >= 0x61 && drive <= 0x7a)) &&
11
+ input.charCodeAt(1) === 0x3a &&
12
+ (input.charCodeAt(2) === 0x5c || input.charCodeAt(2) === 0x2f) &&
13
+ input.indexOf(":", 2) === -1;
14
+ }
4
15
  function hasHomePrefix(input) {
5
16
  return input === "~" || input.startsWith("~/") ||
6
17
  (path.sep === "\\" && input.startsWith("~\\"));
@@ -17,7 +28,12 @@ function normalize(value) {
17
28
  }
18
29
  export function resolveEffectiveHomeDir(env = process.env, homedir = os.homedir) {
19
30
  const raw = resolveRawHomeDir(env, homedir);
20
- return raw ? path.resolve(raw) : undefined;
31
+ if (!raw)
32
+ return undefined;
33
+ assertNoWindowsPathAlias(raw, "filesystem", "home path uses a Windows filesystem namespace alias");
34
+ const resolved = resolvePathPreservingWindowsRoot(raw);
35
+ assertNoWindowsPathAlias(resolved, "filesystem", "home path uses a Windows filesystem namespace alias");
36
+ return resolved;
21
37
  }
22
38
  function resolveRawHomeDir(env, homedir) {
23
39
  const explicitHome = normalize(env.OPENCLAW_HOME);
@@ -56,7 +72,9 @@ function normalizeSafe(homedir) {
56
72
  }
57
73
  }
58
74
  export function resolveRequiredHomeDir(env = process.env, homedir = os.homedir) {
59
- return resolveEffectiveHomeDir(env, homedir) ?? path.resolve(process.cwd());
75
+ const resolved = resolveEffectiveHomeDir(env, homedir) ?? path.resolve(process.cwd());
76
+ assertNoWindowsPathAlias(resolved, "filesystem", "home path uses a Windows filesystem namespace alias");
77
+ return resolved;
60
78
  }
61
79
  export function expandHomePrefix(input, opts) {
62
80
  if (!hasHomePrefix(input)) {
@@ -70,17 +88,62 @@ export function expandHomePrefix(input, opts) {
70
88
  // Expand before normalizing so a following .. traverses the actual home.
71
89
  return path.join(home, input.slice(2));
72
90
  }
73
- export function resolveHomeRelativePath(input, opts) {
74
- if (!input) {
75
- return input;
76
- }
77
- if (!hasHomePrefix(input)) {
78
- return path.resolve(input);
79
- }
91
+ function resolveExpandedHomePath(input, opts) {
80
92
  const expanded = expandHomePrefix(input, {
81
93
  home: resolveRequiredHomeDir(opts?.env ?? process.env, opts?.homedir ?? os.homedir),
82
94
  env: opts?.env,
83
95
  homedir: opts?.homedir,
84
96
  });
85
- return path.resolve(expanded);
97
+ const resolved = resolvePathPreservingWindowsRoot(expanded);
98
+ assertNoWindowsPathAlias(resolved, "filesystem", PATH_ALIAS_MESSAGE);
99
+ return resolved;
100
+ }
101
+ function admitFinalHomePath(resolved, platform) {
102
+ assertNoWindowsPathAliasForPlatform(resolved, "filesystem", PATH_ALIAS_MESSAGE, platform);
103
+ return resolved;
104
+ }
105
+ function finishOrdinaryHomePath(input, resolved) {
106
+ const resolvedPlatform = process.platform;
107
+ if (resolved === input && resolvedPlatform !== undefined)
108
+ return resolved;
109
+ return admitFinalHomePath(resolved, resolvedPlatform);
110
+ }
111
+ function repairAndFinishOrdinaryHomePath(input, resolved) {
112
+ return finishOrdinaryHomePath(input, repairResolvedWindowsRoot(input, resolved));
113
+ }
114
+ function resolveHomePathCold(input, opts, rawPlatform, ordinaryRawAdmitted) {
115
+ if (ordinaryRawAdmitted) {
116
+ return finishOrdinaryHomePath(input, resolvePathPreservingWindowsRoot(input));
117
+ }
118
+ assertNoWindowsPathAliasForPlatform(input, "filesystem", PATH_ALIAS_MESSAGE, rawPlatform);
119
+ if (!hasHomePrefix(input)) {
120
+ const resolved = resolvePathPreservingWindowsRoot(input);
121
+ // This cold branch was not admitted as an ordinary rooted drive. Recheck
122
+ // even an unchanged result because the live platform can change between
123
+ // raw admission and resolved-path admission. Passing undefined preserves
124
+ // the classifier's default process.platform read.
125
+ return admitFinalHomePath(resolved, process.platform);
126
+ }
127
+ return resolveExpandedHomePath(input, opts);
128
+ }
129
+ export function resolveHomeRelativePath(input, opts) {
130
+ if (!input) {
131
+ return input;
132
+ }
133
+ const rawPlatform = process.platform;
134
+ // Primitive ordinary drive paths have exactly one structural colon. Record
135
+ // that admission so an unchanged resolved string does not need rescanning.
136
+ const ordinaryRawAdmitted = rawPlatform === "win32" &&
137
+ typeof input === "string" &&
138
+ isOrdinaryRootedWindowsDrivePath(input);
139
+ // Keep the overwhelmingly common rooted-drive path close to path.resolve
140
+ // itself. Seven-byte inputs retain the general helper's observable
141
+ // namespace-root preflight; a rare six-byte result retains its repair.
142
+ if (!ordinaryRawAdmitted || input.length === 7) {
143
+ return resolveHomePathCold(input, opts, rawPlatform, ordinaryRawAdmitted);
144
+ }
145
+ const resolved = path.resolve(input);
146
+ return resolved.length === 6
147
+ ? repairAndFinishOrdinaryHomePath(input, resolved)
148
+ : finishOrdinaryHomePath(input, resolved);
86
149
  }
@@ -1 +1 @@
1
- {"version":3,"file":"install-path.d.ts","sourceRoot":"","sources":["../src/install-path.ts"],"names":[],"mappings":"AAMA,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMjD;AAED,2FAA2F;AAC3F,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAiB3D;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAO7D;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,kBAAkB,EAAE,MAAM,CAAC;IAC3B,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAAC;CACtC,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAe5D;AAED,wBAAsB,6BAA6B,CAAC,MAAM,EAAE;IAC1D,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;CACvB,GAAG,OAAO,CAAC,IAAI,CAAC,CAkDhB"}
1
+ {"version":3,"file":"install-path.d.ts","sourceRoot":"","sources":["../src/install-path.ts"],"names":[],"mappings":"AAWA,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMjD;AAED,2FAA2F;AAC3F,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAiB3D;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAO7D;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,kBAAkB,EAAE,MAAM,CAAC;IAC3B,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAAC;CACtC,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CA+B5D;AAED,wBAAsB,6BAA6B,CAAC,MAAM,EAAE;IAC1D,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;CACvB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuEhB"}