@openclaw/fs-safe 0.14.0 → 0.16.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 (370) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +38 -10
  3. package/dist/advanced.d.ts +2 -1
  4. package/dist/advanced.d.ts.map +1 -1
  5. package/dist/advanced.js +2 -1
  6. package/dist/archive-kind.d.ts +0 -1
  7. package/dist/archive-kind.d.ts.map +1 -1
  8. package/dist/archive-kind.js +5 -17
  9. package/dist/archive-merge.d.ts.map +1 -1
  10. package/dist/archive-merge.js +113 -46
  11. package/dist/archive-parser.wasm +0 -0
  12. package/dist/archive-read.d.ts.map +1 -1
  13. package/dist/archive-read.js +10 -11
  14. package/dist/archive-tar-stream.d.ts +3 -0
  15. package/dist/archive-tar-stream.d.ts.map +1 -1
  16. package/dist/archive-tar-stream.js +56 -37
  17. package/dist/archive-tar-wasm.d.ts +16 -4
  18. package/dist/archive-tar-wasm.d.ts.map +1 -1
  19. package/dist/archive-tar-wasm.js +134 -34
  20. package/dist/archive-zip-directory.d.ts +4 -0
  21. package/dist/archive-zip-directory.d.ts.map +1 -1
  22. package/dist/archive-zip-directory.js +2 -0
  23. package/dist/archive-zip-entry.d.ts +6 -2
  24. package/dist/archive-zip-entry.d.ts.map +1 -1
  25. package/dist/archive-zip-entry.js +23 -8
  26. package/dist/archive-zip-integrity.d.ts.map +1 -1
  27. package/dist/archive-zip-integrity.js +3 -4
  28. package/dist/archive-zip-loader.d.ts.map +1 -1
  29. package/dist/archive-zip-loader.js +107 -31
  30. package/dist/archive-zip-names.d.ts +1 -0
  31. package/dist/archive-zip-names.d.ts.map +1 -1
  32. package/dist/archive-zip-names.js +6 -0
  33. package/dist/archive.d.ts.map +1 -1
  34. package/dist/archive.js +11 -11
  35. package/dist/bounded-read-stream.d.ts +0 -1
  36. package/dist/bounded-read-stream.d.ts.map +1 -1
  37. package/dist/bounded-read-stream.js +0 -6
  38. package/dist/clone-metadata.d.ts +1 -0
  39. package/dist/clone-metadata.d.ts.map +1 -1
  40. package/dist/clone-metadata.js +6 -2
  41. package/dist/copy-publication.d.ts +6 -0
  42. package/dist/copy-publication.d.ts.map +1 -1
  43. package/dist/copy-publication.js +3 -0
  44. package/dist/copy-tree-portable.d.ts.map +1 -1
  45. package/dist/copy-tree-portable.js +44 -24
  46. package/dist/copy.d.ts.map +1 -1
  47. package/dist/copy.js +29 -11
  48. package/dist/create-directory.d.ts +20 -0
  49. package/dist/create-directory.d.ts.map +1 -0
  50. package/dist/create-directory.js +130 -0
  51. package/dist/create-file-async.d.ts +7 -0
  52. package/dist/create-file-async.d.ts.map +1 -0
  53. package/dist/create-file-async.js +121 -0
  54. package/dist/create-file.d.ts +8 -0
  55. package/dist/create-file.d.ts.map +1 -0
  56. package/dist/create-file.js +190 -0
  57. package/dist/create-owned-file.d.ts +8 -0
  58. package/dist/create-owned-file.d.ts.map +1 -0
  59. package/dist/create-owned-file.js +16 -0
  60. package/dist/create.d.ts +4 -0
  61. package/dist/create.d.ts.map +1 -0
  62. package/dist/create.js +2 -0
  63. package/dist/creation-darwin.d.ts +7 -0
  64. package/dist/creation-darwin.d.ts.map +1 -0
  65. package/dist/creation-darwin.js +79 -0
  66. package/dist/creation-file-state.d.ts +19 -0
  67. package/dist/creation-file-state.d.ts.map +1 -0
  68. package/dist/creation-file-state.js +118 -0
  69. package/dist/creation-path.d.ts +21 -0
  70. package/dist/creation-path.d.ts.map +1 -0
  71. package/dist/creation-path.js +71 -0
  72. package/dist/creation-permissions.d.ts +19 -0
  73. package/dist/creation-permissions.d.ts.map +1 -0
  74. package/dist/creation-permissions.js +125 -0
  75. package/dist/directory-durability.d.ts +1 -1
  76. package/dist/directory-durability.d.ts.map +1 -1
  77. package/dist/directory-durability.js +22 -80
  78. package/dist/directory-guard.d.ts +3 -0
  79. package/dist/directory-guard.d.ts.map +1 -1
  80. package/dist/directory-mode-node.d.ts +2 -0
  81. package/dist/directory-mode-node.d.ts.map +1 -1
  82. package/dist/directory-mode-node.js +8 -0
  83. package/dist/directory-mode-owner.js +5 -5
  84. package/dist/directory-receipt.d.ts +24 -0
  85. package/dist/directory-receipt.d.ts.map +1 -0
  86. package/dist/directory-receipt.js +127 -0
  87. package/dist/file-cleanup.d.ts +19 -0
  88. package/dist/file-cleanup.d.ts.map +1 -0
  89. package/dist/file-cleanup.js +78 -0
  90. package/dist/file-handle-transfer.d.ts +2 -0
  91. package/dist/file-handle-transfer.d.ts.map +1 -1
  92. package/dist/file-handle-transfer.js +57 -2
  93. package/dist/file-identity.d.ts.map +1 -1
  94. package/dist/file-identity.js +18 -4
  95. package/dist/file-lock-sync-admission.d.ts +19 -0
  96. package/dist/file-lock-sync-admission.d.ts.map +1 -0
  97. package/dist/file-lock-sync-admission.js +93 -0
  98. package/dist/file-lock-sync-root-acquire.d.ts +4 -0
  99. package/dist/file-lock-sync-root-acquire.d.ts.map +1 -0
  100. package/dist/file-lock-sync-root-acquire.js +370 -0
  101. package/dist/file-lock-sync-root-arbitration.d.ts +18 -0
  102. package/dist/file-lock-sync-root-arbitration.d.ts.map +1 -0
  103. package/dist/file-lock-sync-root-arbitration.js +66 -0
  104. package/dist/file-lock-sync-root-held.d.ts +34 -0
  105. package/dist/file-lock-sync-root-held.d.ts.map +1 -0
  106. package/dist/file-lock-sync-root-held.js +393 -0
  107. package/dist/file-lock-sync-root-io.d.ts +44 -0
  108. package/dist/file-lock-sync-root-io.d.ts.map +1 -0
  109. package/dist/file-lock-sync-root-io.js +209 -0
  110. package/dist/file-lock-sync-root-mutation.d.ts +17 -0
  111. package/dist/file-lock-sync-root-mutation.d.ts.map +1 -0
  112. package/dist/file-lock-sync-root-mutation.js +277 -0
  113. package/dist/file-lock-sync-root-options.d.ts +20 -0
  114. package/dist/file-lock-sync-root-options.d.ts.map +1 -0
  115. package/dist/file-lock-sync-root-options.js +58 -0
  116. package/dist/file-lock-sync-root-registration.d.ts +2 -0
  117. package/dist/file-lock-sync-root-registration.d.ts.map +1 -0
  118. package/dist/file-lock-sync-root-registration.js +90 -0
  119. package/dist/file-lock-sync-root.d.ts +36 -0
  120. package/dist/file-lock-sync-root.d.ts.map +1 -0
  121. package/dist/file-lock-sync-root.js +361 -0
  122. package/dist/file-lock-sync-stale-admission.d.ts +24 -0
  123. package/dist/file-lock-sync-stale-admission.d.ts.map +1 -0
  124. package/dist/file-lock-sync-stale-admission.js +205 -0
  125. package/dist/file-lock-sync.d.ts.map +1 -1
  126. package/dist/file-lock-sync.js +245 -205
  127. package/dist/file-observation.d.ts +1 -1
  128. package/dist/file-observation.d.ts.map +1 -1
  129. package/dist/file-store-boundary.d.ts +2 -6
  130. package/dist/file-store-boundary.d.ts.map +1 -1
  131. package/dist/file-store-boundary.js +3 -9
  132. package/dist/file-store-prune.d.ts.map +1 -1
  133. package/dist/file-store-prune.js +5 -1
  134. package/dist/file-store-sync-write.d.ts.map +1 -1
  135. package/dist/file-store-sync-write.js +5 -8
  136. package/dist/file-store.d.ts.map +1 -1
  137. package/dist/file-store.js +47 -12
  138. package/dist/guarded-mkdir.d.ts +1 -0
  139. package/dist/guarded-mkdir.d.ts.map +1 -1
  140. package/dist/guarded-mkdir.js +36 -7
  141. package/dist/json-document-store.d.ts.map +1 -1
  142. package/dist/json-document-store.js +22 -15
  143. package/dist/json-durable-queue-ownership.d.ts +0 -1
  144. package/dist/json-durable-queue-ownership.d.ts.map +1 -1
  145. package/dist/json-durable-queue-ownership.js +0 -6
  146. package/dist/move-path.js +1 -1
  147. package/dist/native-binding.d.ts +13 -1
  148. package/dist/native-binding.d.ts.map +1 -1
  149. package/dist/native-fallback-warning.d.ts +4 -0
  150. package/dist/native-fallback-warning.d.ts.map +1 -0
  151. package/dist/native-fallback-warning.js +11 -0
  152. package/dist/native-operations.d.ts +0 -2
  153. package/dist/native-operations.d.ts.map +1 -1
  154. package/dist/native-operations.js +0 -24
  155. package/dist/native-parent-admission.d.ts +5 -2
  156. package/dist/native-parent-admission.d.ts.map +1 -1
  157. package/dist/native-parent-admission.js +27 -7
  158. package/dist/native-pinned-write-windows.d.ts +1 -1
  159. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  160. package/dist/native-pinned-write-windows.js +174 -29
  161. package/dist/native-pinned-write.d.ts.map +1 -1
  162. package/dist/native-pinned-write.js +26 -14
  163. package/dist/native-policy-parent-windows.d.ts +14 -0
  164. package/dist/native-policy-parent-windows.d.ts.map +1 -0
  165. package/dist/native-policy-parent-windows.js +209 -0
  166. package/dist/native-rename-outcome.d.ts +4 -0
  167. package/dist/native-rename-outcome.d.ts.map +1 -0
  168. package/dist/native-rename-outcome.js +8 -0
  169. package/dist/native-staged-file.d.ts +3 -2
  170. package/dist/native-staged-file.d.ts.map +1 -1
  171. package/dist/native-staged-file.js +121 -72
  172. package/dist/output.d.ts.map +1 -1
  173. package/dist/output.js +12 -8
  174. package/dist/owner-dacl.d.ts.map +1 -1
  175. package/dist/owner-dacl.js +10 -4
  176. package/dist/path-prefix.d.ts.map +1 -1
  177. package/dist/path-prefix.js +30 -8
  178. package/dist/path-suffix-aliases.d.ts +2 -0
  179. package/dist/path-suffix-aliases.d.ts.map +1 -1
  180. package/dist/path-suffix-aliases.js +25 -17
  181. package/dist/permission-exec.d.ts +2 -0
  182. package/dist/permission-exec.d.ts.map +1 -1
  183. package/dist/permission-exec.js +150 -21
  184. package/dist/permissions-windows.js +1 -1
  185. package/dist/pinned-mutation-admission.d.ts.map +1 -1
  186. package/dist/pinned-mutation-admission.js +10 -5
  187. package/dist/pinned-mutation-observation.d.ts +0 -1
  188. package/dist/pinned-mutation-observation.d.ts.map +1 -1
  189. package/dist/pinned-mutation-observation.js +0 -19
  190. package/dist/pinned-mutation-shared-route.d.ts +1 -0
  191. package/dist/pinned-mutation-shared-route.d.ts.map +1 -1
  192. package/dist/pinned-mutation-shared-route.js +1 -1
  193. package/dist/pinned-write-input.d.ts +4 -0
  194. package/dist/pinned-write-input.d.ts.map +1 -0
  195. package/dist/pinned-write-input.js +25 -0
  196. package/dist/pinned-write-mode.d.ts +5 -0
  197. package/dist/pinned-write-mode.d.ts.map +1 -0
  198. package/dist/pinned-write-mode.js +24 -0
  199. package/dist/pinned-write-staged.d.ts +6 -0
  200. package/dist/pinned-write-staged.d.ts.map +1 -0
  201. package/dist/pinned-write-staged.js +187 -0
  202. package/dist/pinned-write-types.d.ts +5 -0
  203. package/dist/pinned-write-types.d.ts.map +1 -1
  204. package/dist/pinned-write.d.ts.map +1 -1
  205. package/dist/pinned-write.js +35 -145
  206. package/dist/private-directory.d.ts.map +1 -1
  207. package/dist/private-directory.js +18 -4
  208. package/dist/private-producer-handoff-sync.d.ts +14 -0
  209. package/dist/private-producer-handoff-sync.d.ts.map +1 -0
  210. package/dist/private-producer-handoff-sync.js +114 -0
  211. package/dist/private-producer-handoff.d.ts +22 -4
  212. package/dist/private-producer-handoff.d.ts.map +1 -1
  213. package/dist/private-producer-handoff.js +140 -77
  214. package/dist/private-temp-workspace.d.ts.map +1 -1
  215. package/dist/private-temp-workspace.js +75 -121
  216. package/dist/publish-copy-stage.d.ts +2 -1
  217. package/dist/publish-copy-stage.d.ts.map +1 -1
  218. package/dist/publish-copy-stage.js +16 -7
  219. package/dist/publish-file.d.ts.map +1 -1
  220. package/dist/publish-file.js +2 -2
  221. package/dist/regular-file.d.ts.map +1 -1
  222. package/dist/regular-file.js +1 -15
  223. package/dist/replace-directory.d.ts.map +1 -1
  224. package/dist/replace-directory.js +256 -18
  225. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  226. package/dist/replace-file-copy-fallback.js +62 -70
  227. package/dist/replace-file-copy-source.d.ts.map +1 -1
  228. package/dist/replace-file-copy-source.js +10 -12
  229. package/dist/replace-file-temp-owner.d.ts +5 -9
  230. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  231. package/dist/replace-file-temp-owner.js +56 -72
  232. package/dist/replace-file.js +6 -6
  233. package/dist/retained-directory-replacement.d.ts +26 -0
  234. package/dist/retained-directory-replacement.d.ts.map +1 -0
  235. package/dist/retained-directory-replacement.js +193 -0
  236. package/dist/root-boundary.d.ts +1 -0
  237. package/dist/root-boundary.d.ts.map +1 -1
  238. package/dist/root-boundary.js +4 -0
  239. package/dist/root-context.d.ts +0 -8
  240. package/dist/root-context.d.ts.map +1 -1
  241. package/dist/root-context.js +0 -3
  242. package/dist/root-create-input.d.ts +8 -1
  243. package/dist/root-create-input.d.ts.map +1 -1
  244. package/dist/root-create-input.js +17 -4
  245. package/dist/root-directory-creation.d.ts +3 -3
  246. package/dist/root-directory-creation.d.ts.map +1 -1
  247. package/dist/root-directory-creation.js +15 -3
  248. package/dist/root-directory-list.d.ts +1 -0
  249. package/dist/root-directory-list.d.ts.map +1 -1
  250. package/dist/root-directory-list.js +1 -0
  251. package/dist/root-impl.d.ts.map +1 -1
  252. package/dist/root-impl.js +46 -17
  253. package/dist/root-move-noreplace.d.ts.map +1 -1
  254. package/dist/root-move-noreplace.js +24 -15
  255. package/dist/root-options.d.ts +12 -4
  256. package/dist/root-options.d.ts.map +1 -1
  257. package/dist/root-path-errors.d.ts +1 -0
  258. package/dist/root-path-errors.d.ts.map +1 -1
  259. package/dist/root-path-errors.js +11 -2
  260. package/dist/root-path-existing.d.ts.map +1 -1
  261. package/dist/root-path-existing.js +11 -35
  262. package/dist/root-path-stat.d.ts.map +1 -1
  263. package/dist/root-path-stat.js +59 -7
  264. package/dist/root-path.js +1 -13
  265. package/dist/root-remove.d.ts +1 -0
  266. package/dist/root-remove.d.ts.map +1 -1
  267. package/dist/root-remove.js +4 -0
  268. package/dist/root-walk.d.ts +1 -1
  269. package/dist/root-walk.d.ts.map +1 -1
  270. package/dist/root-walk.js +17 -2
  271. package/dist/root-write-admission.d.ts +0 -2
  272. package/dist/root-write-admission.d.ts.map +1 -1
  273. package/dist/root-write-admission.js +1 -15
  274. package/dist/root-write-complete-parent.d.ts.map +1 -1
  275. package/dist/root-write-complete-parent.js +7 -23
  276. package/dist/root-write-publication.js +1 -1
  277. package/dist/root-write-verification.d.ts.map +1 -1
  278. package/dist/root-write-verification.js +29 -42
  279. package/dist/secret-file.d.ts.map +1 -1
  280. package/dist/secret-file.js +3 -24
  281. package/dist/secret-read-async.d.ts.map +1 -1
  282. package/dist/secret-read-async.js +3 -24
  283. package/dist/secret-read-policy.d.ts +6 -2
  284. package/dist/secret-read-policy.d.ts.map +1 -1
  285. package/dist/secret-read-policy.js +26 -2
  286. package/dist/secure-file-windows.d.ts +6 -0
  287. package/dist/secure-file-windows.d.ts.map +1 -1
  288. package/dist/secure-file-windows.js +34 -117
  289. package/dist/secure-file.js +2 -2
  290. package/dist/sibling-temp.d.ts.map +1 -1
  291. package/dist/sibling-temp.js +23 -12
  292. package/dist/sidecar-lock-acquire.d.ts +2 -28
  293. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  294. package/dist/sidecar-lock-acquire.js +288 -199
  295. package/dist/sidecar-lock-admission-context.d.ts +19 -0
  296. package/dist/sidecar-lock-admission-context.d.ts.map +1 -0
  297. package/dist/sidecar-lock-admission-context.js +60 -0
  298. package/dist/sidecar-lock-admission-parser.d.ts +43 -0
  299. package/dist/sidecar-lock-admission-parser.d.ts.map +1 -0
  300. package/dist/sidecar-lock-admission-parser.js +113 -0
  301. package/dist/sidecar-lock-admission.d.ts +35 -0
  302. package/dist/sidecar-lock-admission.d.ts.map +1 -0
  303. package/dist/sidecar-lock-admission.js +7 -0
  304. package/dist/sidecar-lock-reclaim.d.ts +9 -4
  305. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  306. package/dist/sidecar-lock-reclaim.js +80 -25
  307. package/dist/sidecar-lock-root.d.ts.map +1 -1
  308. package/dist/sidecar-lock-root.js +2 -1
  309. package/dist/sidecar-lock-stale-admission.d.ts +39 -0
  310. package/dist/sidecar-lock-stale-admission.d.ts.map +1 -0
  311. package/dist/sidecar-lock-stale-admission.js +232 -0
  312. package/dist/sidecar-lock-target.d.ts +8 -0
  313. package/dist/sidecar-lock-target.d.ts.map +1 -0
  314. package/dist/sidecar-lock-target.js +55 -0
  315. package/dist/sidecar-lock.d.ts.map +1 -1
  316. package/dist/sidecar-lock.js +100 -16
  317. package/dist/staged-directory.d.ts.map +1 -1
  318. package/dist/staged-directory.js +6 -6
  319. package/dist/staged-file-settlement.d.ts +17 -0
  320. package/dist/staged-file-settlement.d.ts.map +1 -0
  321. package/dist/staged-file-settlement.js +57 -0
  322. package/dist/temp-workspace-descriptor.d.ts.map +1 -1
  323. package/dist/temp-workspace-descriptor.js +9 -27
  324. package/dist/temp-workspace-owner.d.ts.map +1 -1
  325. package/dist/temp-workspace-owner.js +8 -8
  326. package/dist/walk.d.ts +5 -1
  327. package/dist/walk.d.ts.map +1 -1
  328. package/dist/walk.js +19 -6
  329. package/dist/windows-owner.d.ts.map +1 -1
  330. package/dist/windows-owner.js +4 -3
  331. package/dist/windows-security-bridge.cs +336 -0
  332. package/dist/windows-security-bridge.ps1 +15 -0
  333. package/dist/windows-security-command.d.ts +26 -0
  334. package/dist/windows-security-command.d.ts.map +1 -0
  335. package/dist/windows-security-command.js +363 -0
  336. package/dist/windows-security-facts.d.ts +6 -0
  337. package/dist/windows-security-facts.d.ts.map +1 -0
  338. package/dist/windows-security-facts.js +108 -0
  339. package/docs/advanced.md +4 -2
  340. package/docs/archive.md +97 -46
  341. package/docs/atomic.md +85 -8
  342. package/docs/config.md +6 -2
  343. package/docs/contributing.md +44 -4
  344. package/docs/copy.md +37 -0
  345. package/docs/creation.md +128 -0
  346. package/docs/durability.md +24 -0
  347. package/docs/file-store.md +19 -0
  348. package/docs/install.md +31 -7
  349. package/docs/json-store.md +5 -0
  350. package/docs/migrating-to-0.5.md +15 -6
  351. package/docs/migrating-to-0.6.md +9 -4
  352. package/docs/native-helper.md +32 -12
  353. package/docs/native.md +38 -7
  354. package/docs/output.md +6 -0
  355. package/docs/path-prefix.md +10 -0
  356. package/docs/path-suffix-aliases.md +51 -6
  357. package/docs/permissions.md +50 -14
  358. package/docs/public-api.md +3 -2
  359. package/docs/root.md +56 -3
  360. package/docs/secret-file.md +11 -2
  361. package/docs/secure-file.md +9 -4
  362. package/docs/sidecar-lock.md +114 -8
  363. package/docs/staged-file.md +12 -3
  364. package/docs/temp.md +20 -3
  365. package/docs/walk.md +67 -1
  366. package/docs/writing.md +76 -6
  367. package/package.json +19 -16
  368. package/dist/darwin-acl.d.ts +0 -4
  369. package/dist/darwin-acl.d.ts.map +0 -1
  370. package/dist/darwin-acl.js +0 -24
@@ -1,12 +1,94 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import fsSync from "node:fs";
2
3
  import fs from "node:fs/promises";
3
4
  import path from "node:path";
4
- import { guardedRename, guardedRm } from "./guarded-mutation.js";
5
+ import { assertSyncDirectoryGuard, createAsyncDirectoryGuard, inspectDirectoryIdentitySync, } from "./directory-guard.js";
6
+ import { FsSafeError } from "./errors.js";
7
+ import { classifyNativeRenameFailure, NATIVE_RENAME_SOURCE_IDENTITY_MISMATCH, } from "./native-rename-outcome.js";
5
8
  import { recursiveMkdirPath } from "./recursive-mkdir-path.js";
9
+ import { RetainedDirectoryReplacement } from "./retained-directory-replacement.js";
6
10
  import { assertSafePathPrefix } from "./safe-path-segment.js";
7
11
  import { admitStandalonePublicationPath } from "./standalone-publication-path.js";
8
12
  import { serializePathWrite } from "./write-queue.js";
9
13
  import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
14
+ function distinctGuards(source, target) {
15
+ return source === target ? [source] : [source, target];
16
+ }
17
+ function assertGuardsCurrent(guards) {
18
+ for (const guard of guards)
19
+ assertSyncDirectoryGuard(guard);
20
+ }
21
+ function copyOperationalCode(target, source) {
22
+ const code = source?.code;
23
+ if (typeof code === "string") {
24
+ Object.defineProperty(target, "code", { configurable: true, value: code });
25
+ }
26
+ }
27
+ function replacementFailure(error, message, details) {
28
+ if (error instanceof FsSafeError) {
29
+ return new FsSafeError(error.code, message, { cause: error, details });
30
+ }
31
+ const failure = new Error(message, { cause: error });
32
+ copyOperationalCode(failure, error);
33
+ Object.defineProperty(failure, "details", { configurable: true, value: details });
34
+ return failure;
35
+ }
36
+ function replacementAggregate(error, recoveryError, message, details) {
37
+ const combined = new AggregateError([error, recoveryError], "directory replacement operation and recovery both failed");
38
+ if (error instanceof FsSafeError) {
39
+ return new FsSafeError(error.code, message, { cause: combined, details });
40
+ }
41
+ const failure = new Error(message, { cause: combined });
42
+ copyOperationalCode(failure, error);
43
+ Object.defineProperty(failure, "details", { configurable: true, value: details });
44
+ return failure;
45
+ }
46
+ function isMissingDirectoryEntry(error) {
47
+ return error?.code === "ENOENT";
48
+ }
49
+ function observeOptionalDirectory(pathname) {
50
+ let initial;
51
+ try {
52
+ initial = fsSync.lstatSync(pathname, { bigint: true });
53
+ }
54
+ catch (error) {
55
+ if (isMissingDirectoryEntry(error))
56
+ return undefined;
57
+ throw error;
58
+ }
59
+ return inspectDirectoryIdentitySync(pathname, undefined, initial);
60
+ }
61
+ function normalizeNativeRenameFailure(error) {
62
+ const code = error?.code;
63
+ if (code === NATIVE_RENAME_SOURCE_IDENTITY_MISMATCH) {
64
+ return new FsSafeError("path-mismatch", "directory replacement source identity changed before native rename", { cause: error });
65
+ }
66
+ if (code === "EINVAL" || code === "ENOSYS" || code === "ENOTSUP" ||
67
+ code === "EOPNOTSUPP") {
68
+ return new FsSafeError("helper-unavailable", "native no-replace directory rename is unavailable on this filesystem", { cause: error });
69
+ }
70
+ return error;
71
+ }
72
+ function renameRetainedDirectory(params) {
73
+ const guards = distinctGuards(params.sourceParent, params.targetParent);
74
+ assertGuardsCurrent(guards);
75
+ const initial = fsSync.lstatSync(params.from, { bigint: true });
76
+ const identity = inspectDirectoryIdentitySync(params.from, params.expectedSource, initial);
77
+ try {
78
+ params.renameNoReplace(identity);
79
+ }
80
+ catch (error) {
81
+ if (classifyNativeRenameFailure(error) === "indeterminate") {
82
+ params.onIndeterminate();
83
+ }
84
+ throw normalizeNativeRenameFailure(error);
85
+ }
86
+ // Record the transition synchronously before any post-rename observation.
87
+ params.onRenamed?.(identity);
88
+ inspectDirectoryIdentitySync(params.to, identity);
89
+ assertGuardsCurrent(guards);
90
+ return identity;
91
+ }
10
92
  export async function replaceDirectoryAtomic(options) {
11
93
  const stagedDirInput = admitStandalonePublicationPath(options.stagedDir, "staged directory uses a Windows filesystem namespace alias");
12
94
  const targetDirInput = admitStandalonePublicationPath(options.targetDir, "target directory uses a Windows filesystem namespace alias");
@@ -23,28 +105,184 @@ async function replaceDirectoryAtomicUnserialized(stagedDir, targetDir, backupPr
23
105
  const parentDir = path.dirname(targetDir);
24
106
  const backupPrefix = assertSafePathPrefix(backupPrefixInput ?? ".fs-safe-dir-backup-", { label: "atomic directory backup prefix" });
25
107
  const backupDir = path.join(parentDir, `${backupPrefix}${process.pid}-${randomUUID()}`);
26
- let backupCreated = false;
108
+ const backup = {
109
+ identity: undefined,
110
+ transition: "not-renamed",
111
+ };
112
+ const publication = {
113
+ status: "not-published",
114
+ };
115
+ let operationFailed = false;
116
+ let operationError;
117
+ let operationDetails = {
118
+ phase: "backup",
119
+ publication: publication.status,
120
+ recovery: "none",
121
+ };
122
+ const retainedBinding = RetainedDirectoryReplacement.requireBinding();
27
123
  await fs.mkdir(recursiveMkdirPath(parentDir), { recursive: true });
124
+ const targetParent = await createAsyncDirectoryGuard(parentDir, { bigint: true });
125
+ const stagedParentDir = path.dirname(stagedDir);
126
+ const stagedParent = stagedParentDir === parentDir
127
+ ? targetParent
128
+ : await createAsyncDirectoryGuard(stagedParentDir, { bigint: true });
129
+ const stagedIdentity = inspectDirectoryIdentitySync(stagedDir);
130
+ const targetIdentity = observeOptionalDirectory(targetDir);
131
+ const retained = RetainedDirectoryReplacement.retain({
132
+ targetParent: {
133
+ path: parentDir,
134
+ realPath: targetParent.realPath,
135
+ identity: targetParent.stat,
136
+ },
137
+ stagedParent: {
138
+ path: stagedParentDir,
139
+ realPath: stagedParent.realPath,
140
+ identity: stagedParent.stat,
141
+ },
142
+ ...(targetIdentity ? {
143
+ originalTarget: { path: targetDir, identity: targetIdentity },
144
+ } : {}),
145
+ }, retainedBinding);
146
+ const targetBasename = path.basename(targetDir);
147
+ const stagedBasename = path.basename(stagedDir);
148
+ const backupBasename = path.basename(backupDir);
149
+ const details = (phase, recovery) => {
150
+ operationDetails = {
151
+ phase,
152
+ publication: publication.status,
153
+ recovery,
154
+ ...(backup.transition === "not-renamed" ? {} : { backupPath: backupDir }),
155
+ };
156
+ return operationDetails;
157
+ };
158
+ const rollback = (error, failedPhase, identity) => {
159
+ const rollbackState = {
160
+ transition: "not-renamed",
161
+ };
162
+ try {
163
+ renameRetainedDirectory({
164
+ from: backupDir,
165
+ to: targetDir,
166
+ sourceParent: targetParent,
167
+ targetParent,
168
+ expectedSource: identity,
169
+ renameNoReplace: expectedSource => {
170
+ retained.renameNoReplace("target", backupBasename, targetBasename, expectedSource);
171
+ },
172
+ onIndeterminate() {
173
+ rollbackState.transition = "indeterminate";
174
+ backup.transition = "indeterminate";
175
+ },
176
+ onRenamed() {
177
+ rollbackState.transition = "renamed";
178
+ backup.transition = "not-renamed";
179
+ },
180
+ });
181
+ }
182
+ catch (rollbackError) {
183
+ const recovery = rollbackState.transition === "renamed"
184
+ ? "restored-unverified"
185
+ : rollbackState.transition === "indeterminate"
186
+ ? "indeterminate"
187
+ : "backup-path-preserved";
188
+ throw replacementAggregate(error, rollbackError, `directory ${failedPhase} and rollback failed`, details("rollback", recovery));
189
+ }
190
+ throw replacementFailure(error, `directory ${failedPhase} failed after restoring the original target`, details(failedPhase, "restored"));
191
+ };
28
192
  try {
29
- await guardedRename({ from: targetDir, to: backupDir });
30
- backupCreated = true;
31
- }
32
- catch (err) {
33
- if (err.code !== "ENOENT") {
34
- throw err;
193
+ if (targetIdentity) {
194
+ try {
195
+ backup.identity = renameRetainedDirectory({
196
+ from: targetDir,
197
+ to: backupDir,
198
+ sourceParent: targetParent,
199
+ targetParent,
200
+ expectedSource: targetIdentity,
201
+ renameNoReplace: expectedSource => {
202
+ retained.renameNoReplace("target", targetBasename, backupBasename, expectedSource);
203
+ },
204
+ onIndeterminate() {
205
+ backup.transition = "indeterminate";
206
+ },
207
+ onRenamed(identity) {
208
+ backup.identity = identity;
209
+ backup.transition = "renamed";
210
+ },
211
+ });
212
+ }
213
+ catch (err) {
214
+ if (backup.transition === "renamed" && backup.identity) {
215
+ rollback(err, "backup", backup.identity);
216
+ }
217
+ const recovery = backup.transition === "indeterminate" ? "indeterminate" : "none";
218
+ throw replacementFailure(err, "directory backup failed before publication", details("backup", recovery));
219
+ }
220
+ }
221
+ try {
222
+ renameRetainedDirectory({
223
+ from: stagedDir,
224
+ to: targetDir,
225
+ sourceParent: stagedParent,
226
+ targetParent,
227
+ expectedSource: stagedIdentity,
228
+ renameNoReplace: expectedSource => {
229
+ retained.renameNoReplace("staged", stagedBasename, targetBasename, expectedSource);
230
+ },
231
+ onIndeterminate() {
232
+ publication.status = "indeterminate";
233
+ },
234
+ onRenamed() {
235
+ publication.status = "published";
236
+ },
237
+ });
238
+ }
239
+ catch (err) {
240
+ if (publication.status === "published") {
241
+ throw replacementFailure(err, "published directory failed retained identity verification", details("publish", backup.transition === "renamed" ? "backup-path-preserved" : "none"));
242
+ }
243
+ if (publication.status === "indeterminate") {
244
+ throw replacementFailure(err, "directory publication outcome is indeterminate", details("publish", backup.transition === "renamed" ? "backup-path-preserved" : "none"));
245
+ }
246
+ if (backup.transition === "renamed" && backup.identity) {
247
+ rollback(err, "publish", backup.identity);
248
+ }
249
+ throw replacementFailure(err, "directory publication failed before commit", details("publish", "none"));
250
+ }
251
+ details("publish", backup.transition === "renamed" ? "backup-path-preserved" : "none");
252
+ if (backup.transition === "renamed" && backup.identity) {
253
+ let cleanupAttempted = false;
254
+ try {
255
+ assertSyncDirectoryGuard(targetParent);
256
+ inspectDirectoryIdentitySync(backupDir, backup.identity);
257
+ cleanupAttempted = true;
258
+ const outcome = await retained.removeOriginal(backupBasename);
259
+ if (outcome !== "removed") {
260
+ throw new FsSafeError("path-mismatch", "retained backup changed before bounded cleanup");
261
+ }
262
+ backup.transition = "not-renamed";
263
+ details("cleanup", "none");
264
+ }
265
+ catch (error) {
266
+ throw replacementFailure(error, "published directory but retained backup cleanup failed", {
267
+ ...details("cleanup", cleanupAttempted ? "cleanup-incomplete" : "backup-path-preserved"),
268
+ });
269
+ }
35
270
  }
36
271
  }
37
- try {
38
- await guardedRename({ from: stagedDir, to: targetDir });
272
+ catch (error) {
273
+ operationFailed = true;
274
+ operationError = error;
275
+ throw error;
39
276
  }
40
- catch (err) {
41
- if (backupCreated) {
42
- await guardedRename({ from: backupDir, to: targetDir }).catch(() => undefined);
43
- backupCreated = false;
277
+ finally {
278
+ try {
279
+ retained.close();
280
+ }
281
+ catch (closeError) {
282
+ if (operationFailed) {
283
+ throw replacementAggregate(operationError, closeError, "directory replacement and retained descriptor close failed", operationDetails);
284
+ }
285
+ throw replacementFailure(closeError, "directory replacement retained descriptor close failed", operationDetails);
44
286
  }
45
- throw err;
46
- }
47
- if (backupCreated) {
48
- await guardedRm({ target: backupDir, recursive: true, force: true, verifyAfter: false });
49
287
  }
50
288
  }
@@ -1 +1 @@
1
- {"version":3,"file":"replace-file-copy-fallback.d.ts","sourceRoot":"","sources":["../src/replace-file-copy-fallback.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,EAAE,EAAE,KAAK,WAAW,EAAc,MAAM,SAAS,CAAC;AAQ/D,MAAM,MAAM,oCAAoC,GAAG,QAAQ,CAAC;AAC5D,MAAM,MAAM,oCAAoC,GAAG,kBAAkB,GAAG,MAAM,CAAC;AAC/E,MAAM,MAAM,+BAA+B,GAAG,UAAU,GAAG,gBAAgB,CAAC;AAC5E,MAAM,MAAM,sCAAsC,GAAG;IACnD,OAAO,EAAE,+BAA+B,CAAC;CAC1C,CAAC;AAEF,KAAK,eAAe,GAAG;IACrB,KAAK,EAAE,cAAc,kBAAkB,EAAE,KAAK,CAAC;IAC/C,IAAI,EAAE,cAAc,kBAAkB,EAAE,IAAI,CAAC;IAC7C,EAAE,EAAE,cAAc,kBAAkB,EAAE,EAAE,CAAC;IACzC,MAAM,EAAE,cAAc,kBAAkB,EAAE,MAAM,CAAC;CAClD,CAAC;AAEF,KAAK,cAAc,GAAG,IAAI,CACxB,OAAO,MAAM,EACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,eAAe,GACf,WAAW,GACX,UAAU,GACV,UAAU,GACV,QAAQ,GACR,YAAY,GACZ,WAAW,CACd,CAAC;AAyFF,wBAAsB,+BAA+B,CACnD,QAAQ,EAAE,eAAe,EACzB,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,oCAAoC,GAC5C,OAAO,CAAC,IAAI,CAAC,CAwBf;AAED,wBAAgB,mCAAmC,CACjD,QAAQ,EAAE,cAAc,EACxB,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,oCAAoC,GAC5C,IAAI,CAwBN;AA+HD,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,QAAQ,EAAE,eAAe,CAAC;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,oBAAoB,CAAC,EAAE,oCAAoC,CAAC;IAC5D,OAAO,EAAE,oCAAoC,CAAC;IAC9C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,sBAAsB,CAAC,EAAE,WAAW,CAAC;IACrC,IAAI,EAAE,OAAO,CAAC;CACf,GAAG,OAAO,CAAC,IAAI,CAAC,CA4DhB;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,QAAQ,EAAE,cAAc,CAAC;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,oBAAoB,CAAC,EAAE,oCAAoC,CAAC;IAC5D,OAAO,EAAE,oCAAoC,CAAC;IAC9C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,sBAAsB,CAAC,EAAE,WAAW,CAAC;IACrC,UAAU,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,IAAI,EAAE,OAAO,CAAC;CACf,GAAG,IAAI,CAkEP"}
1
+ {"version":3,"file":"replace-file-copy-fallback.d.ts","sourceRoot":"","sources":["../src/replace-file-copy-fallback.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,EAAE,EAAE,KAAK,WAAW,EAAc,MAAM,SAAS,CAAC;AAQ/D,MAAM,MAAM,oCAAoC,GAAG,QAAQ,CAAC;AAC5D,MAAM,MAAM,oCAAoC,GAAG,kBAAkB,GAAG,MAAM,CAAC;AAC/E,MAAM,MAAM,+BAA+B,GAAG,UAAU,GAAG,gBAAgB,CAAC;AAC5E,MAAM,MAAM,sCAAsC,GAAG;IACnD,OAAO,EAAE,+BAA+B,CAAC;CAC1C,CAAC;AAEF,KAAK,eAAe,GAAG;IACrB,KAAK,EAAE,cAAc,kBAAkB,EAAE,KAAK,CAAC;IAC/C,IAAI,EAAE,cAAc,kBAAkB,EAAE,IAAI,CAAC;IAC7C,EAAE,EAAE,cAAc,kBAAkB,EAAE,EAAE,CAAC;IACzC,MAAM,EAAE,cAAc,kBAAkB,EAAE,MAAM,CAAC;CAClD,CAAC;AAEF,KAAK,cAAc,GAAG,IAAI,CACxB,OAAO,MAAM,EACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,eAAe,GACf,WAAW,GACX,UAAU,GACV,UAAU,GACV,QAAQ,GACR,YAAY,GACZ,WAAW,CACd,CAAC;AAqHF,wBAAsB,+BAA+B,CACnD,QAAQ,EAAE,eAAe,EACzB,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,oCAAoC,GAC5C,OAAO,CAAC,IAAI,CAAC,CAIf;AAED,wBAAgB,mCAAmC,CACjD,QAAQ,EAAE,cAAc,EACxB,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,oCAAoC,GAC5C,IAAI,CAIN;AA+HD,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,QAAQ,EAAE,eAAe,CAAC;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,oBAAoB,CAAC,EAAE,oCAAoC,CAAC;IAC5D,OAAO,EAAE,oCAAoC,CAAC;IAC9C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,sBAAsB,CAAC,EAAE,WAAW,CAAC;IACrC,IAAI,EAAE,OAAO,CAAC;CACf,GAAG,OAAO,CAAC,IAAI,CAAC,CAuEhB;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,QAAQ,EAAE,cAAc,CAAC;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,oBAAoB,CAAC,EAAE,oCAAoC,CAAC;IAC5D,OAAO,EAAE,oCAAoC,CAAC;IAC9C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,sBAAsB,CAAC,EAAE,WAAW,CAAC;IACrC,UAAU,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,IAAI,EAAE,OAAO,CAAC;CACf,GAAG,IAAI,CAuEP"}
@@ -10,10 +10,28 @@ const NOFOLLOW = SUPPORTS_NOFOLLOW ? syncFs.constants.O_NOFOLLOW : 0;
10
10
  const OPEN_READ_FLAGS = resolveReadOpenFlags();
11
11
  const OPEN_READ_WRITE_FLAGS = syncFs.constants.O_RDWR | NOFOLLOW;
12
12
  const OPEN_WRITE_EXCLUSIVE_FLAGS = syncFs.constants.O_WRONLY | syncFs.constants.O_CREAT | syncFs.constants.O_EXCL | NOFOLLOW;
13
+ function closeSyncAfterAdmissionFailure(fsModule, fd, error) {
14
+ try {
15
+ fsModule.closeSync(fd);
16
+ }
17
+ catch {
18
+ // Preserve the already-selected admission failure.
19
+ }
20
+ throw error;
21
+ }
13
22
  function notFound(error) {
14
23
  return error.code === "ENOENT";
15
24
  }
16
- function assertPinnedDestination(pathname, opened, dest, hardlinks) {
25
+ function assertPinnedDestination(pathname, opened, dest, hardlinks, admission) {
26
+ if (admission === "hardlinks") {
27
+ if (pathname.isSymbolicLink() || !pathname.isFile() || !sameFileIdentity(pathname, opened)) {
28
+ throw new FsSafeError("path-mismatch", `Atomic replace destination changed while opening: ${dest}`);
29
+ }
30
+ if (opened.nlink > 1) {
31
+ throw new FsSafeError("hardlink", `Hardlinked atomic replace destination not allowed: ${dest}`);
32
+ }
33
+ return;
34
+ }
17
35
  if (pathname.isSymbolicLink()) {
18
36
  throw new FsSafeError("symlink", `Refusing copy fallback through symlink destination: ${dest}`);
19
37
  }
@@ -27,7 +45,7 @@ function assertPinnedDestination(pathname, opened, dest, hardlinks) {
27
45
  throw new FsSafeError("hardlink", `Hardlinked copy fallback destination not allowed: ${dest}`);
28
46
  }
29
47
  }
30
- async function openPinnedDestination(fsModule, dest, hardlinks) {
48
+ async function openPinnedDestination(fsModule, dest, admission, hardlinks) {
31
49
  let preview;
32
50
  try {
33
51
  preview = fsModule === fs ? syncFs.lstatSync(dest) : await fsModule.lstat(dest);
@@ -39,22 +57,24 @@ async function openPinnedDestination(fsModule, dest, hardlinks) {
39
57
  }
40
58
  if (!preview)
41
59
  return null;
42
- if (preview.isSymbolicLink()) {
60
+ if (admission === "hardlinks" && (preview.isSymbolicLink() || !preview.isFile()))
61
+ return null;
62
+ if (admission === "restore" && preview.isSymbolicLink()) {
43
63
  throw new FsSafeError("symlink", `Refusing copy fallback through symlink destination: ${dest}`);
44
64
  }
45
- const handle = await fsModule.open(dest, OPEN_READ_WRITE_FLAGS);
65
+ const handle = await fsModule.open(dest, admission === "restore" ? OPEN_READ_WRITE_FLAGS : OPEN_READ_FLAGS);
46
66
  try {
47
67
  const opened = fsModule === fs ? syncFs.fstatSync(handle.fd) : await handle.stat();
48
68
  const current = fsModule === fs ? syncFs.lstatSync(dest) : await fsModule.lstat(dest);
49
- assertPinnedDestination(current, opened, dest, hardlinks);
50
- return { handle, stat: opened };
69
+ assertPinnedDestination(current, opened, dest, hardlinks, admission);
70
+ return handle;
51
71
  }
52
72
  catch (error) {
53
73
  await handle.close().catch(() => undefined);
54
74
  throw error;
55
75
  }
56
76
  }
57
- function openPinnedDestinationSync(fsModule, dest, hardlinks) {
77
+ function openPinnedDestinationSync(fsModule, dest, admission, hardlinks) {
58
78
  let preview;
59
79
  try {
60
80
  preview = fsModule.lstatSync(dest);
@@ -64,77 +84,34 @@ function openPinnedDestinationSync(fsModule, dest, hardlinks) {
64
84
  return null;
65
85
  throw error;
66
86
  }
67
- if (preview.isSymbolicLink()) {
87
+ if (admission === "hardlinks" && (preview.isSymbolicLink() || !preview.isFile()))
88
+ return null;
89
+ if (admission === "restore" && preview.isSymbolicLink()) {
68
90
  throw new FsSafeError("symlink", `Refusing copy fallback through symlink destination: ${dest}`);
69
91
  }
70
- const fd = fsModule.openSync(dest, OPEN_READ_WRITE_FLAGS);
92
+ const fd = fsModule.openSync(dest, admission === "restore" ? OPEN_READ_WRITE_FLAGS : OPEN_READ_FLAGS);
71
93
  try {
72
94
  const opened = fsModule.fstatSync(fd);
73
- assertPinnedDestination(fsModule.lstatSync(dest), opened, dest, hardlinks);
74
- return { fd, stat: opened };
95
+ assertPinnedDestination(fsModule.lstatSync(dest), opened, dest, hardlinks, admission);
96
+ return fd;
75
97
  }
76
98
  catch (error) {
77
- fsModule.closeSync(fd);
78
- throw error;
99
+ closeSyncAfterAdmissionFailure(fsModule, fd, error);
79
100
  }
80
101
  }
81
102
  export async function assertDestinationHardlinkPolicy(fsModule, dest, policy) {
82
103
  if (policy !== "reject")
83
104
  return;
84
- let preview;
85
- try {
86
- preview = fsModule === fs ? syncFs.lstatSync(dest) : await fsModule.lstat(dest);
87
- }
88
- catch (error) {
89
- if (notFound(error))
90
- return;
91
- throw error;
92
- }
93
- if (!preview || preview.isSymbolicLink() || !preview.isFile())
94
- return;
95
- const handle = await fsModule.open(dest, OPEN_READ_FLAGS);
96
- try {
97
- const opened = fsModule === fs ? syncFs.fstatSync(handle.fd) : await handle.stat();
98
- const current = fsModule === fs ? syncFs.lstatSync(dest) : await fsModule.lstat(dest);
99
- if (current.isSymbolicLink() || !current.isFile() || !sameFileIdentity(current, opened)) {
100
- throw new FsSafeError("path-mismatch", `Atomic replace destination changed while opening: ${dest}`);
101
- }
102
- if (opened.nlink > 1) {
103
- throw new FsSafeError("hardlink", `Hardlinked atomic replace destination not allowed: ${dest}`);
104
- }
105
- }
106
- finally {
105
+ const handle = await openPinnedDestination(fsModule, dest, "hardlinks");
106
+ if (handle)
107
107
  await handle.close().catch(() => undefined);
108
- }
109
108
  }
110
109
  export function assertDestinationHardlinkPolicySync(fsModule, dest, policy) {
111
110
  if (policy !== "reject")
112
111
  return;
113
- let preview;
114
- try {
115
- preview = fsModule.lstatSync(dest);
116
- }
117
- catch (error) {
118
- if (notFound(error))
119
- return;
120
- throw error;
121
- }
122
- if (preview.isSymbolicLink() || !preview.isFile())
123
- return;
124
- const fd = fsModule.openSync(dest, OPEN_READ_FLAGS);
125
- try {
126
- const opened = fsModule.fstatSync(fd);
127
- const current = fsModule.lstatSync(dest);
128
- if (current.isSymbolicLink() || !current.isFile() || !sameFileIdentity(current, opened)) {
129
- throw new FsSafeError("path-mismatch", `Atomic replace destination changed while opening: ${dest}`);
130
- }
131
- if (opened.nlink > 1) {
132
- throw new FsSafeError("hardlink", `Hardlinked atomic replace destination not allowed: ${dest}`);
133
- }
134
- }
135
- finally {
112
+ const fd = openPinnedDestinationSync(fsModule, dest, "hardlinks");
113
+ if (fd !== null)
136
114
  fsModule.closeSync(fd);
137
- }
138
115
  }
139
116
  function restoreReadOptions(stat, maxBytes) {
140
117
  return {
@@ -183,7 +160,7 @@ function writeAllSync(fsModule, fd, data) {
183
160
  function restoreFailure(writeError, cleanup, restoreError) {
184
161
  const primary = writeError instanceof Error ? writeError : new Error(String(writeError));
185
162
  const details = { cleanup };
186
- const cause = restoreError
163
+ const cause = cleanup === "restore-failed"
187
164
  ? new AggregateError([primary, restoreError], "copy fallback and original restoration both failed")
188
165
  : primary;
189
166
  return new FsSafeError("helper-failed", cleanup === "restored"
@@ -246,11 +223,12 @@ export async function copyFallbackReplace(params) {
246
223
  });
247
224
  const { replacement } = source;
248
225
  let destHandle = null;
226
+ let closeRequiredForSuccess = false;
249
227
  try {
250
228
  if (params.restore === "restore-original") {
251
- const pinned = await openPinnedDestination(params.fsModule, params.dest, params.destinationHardlinks);
229
+ const pinned = await openPinnedDestination(params.fsModule, params.dest, "restore", params.destinationHardlinks);
252
230
  if (pinned) {
253
- destHandle = pinned.handle;
231
+ destHandle = pinned;
254
232
  await replacePinnedWithRestore(params.fsModule, destHandle, replacement, params.maxRestoreBytes, source.mode);
255
233
  }
256
234
  }
@@ -277,10 +255,20 @@ export async function copyFallbackReplace(params) {
277
255
  if (params.sync) {
278
256
  await destHandle.sync();
279
257
  }
258
+ closeRequiredForSuccess = !params.sync;
280
259
  }
281
260
  }
282
261
  finally {
283
- await destHandle?.close().catch(() => undefined);
262
+ if (destHandle) {
263
+ try {
264
+ await destHandle.close();
265
+ }
266
+ catch (closeError) {
267
+ if (closeRequiredForSuccess) {
268
+ throw closeError;
269
+ }
270
+ }
271
+ }
284
272
  }
285
273
  }
286
274
  export function copyFallbackReplaceSync(params) {
@@ -291,11 +279,12 @@ export function copyFallbackReplaceSync(params) {
291
279
  });
292
280
  const { replacement } = source;
293
281
  let destFd;
282
+ let closeRequiredForSuccess = false;
294
283
  try {
295
284
  if (params.restore === "restore-original") {
296
- const pinned = openPinnedDestinationSync(params.fsModule, params.dest, params.destinationHardlinks);
297
- if (pinned) {
298
- destFd = pinned.fd;
285
+ const pinned = openPinnedDestinationSync(params.fsModule, params.dest, "restore", params.destinationHardlinks);
286
+ if (pinned !== null) {
287
+ destFd = pinned;
299
288
  replacePinnedWithRestoreSync(params.fsModule, destFd, replacement, params.maxRestoreBytes, source.mode, params.fchmodSync);
300
289
  }
301
290
  }
@@ -321,6 +310,7 @@ export function copyFallbackReplaceSync(params) {
321
310
  if (params.sync) {
322
311
  params.fsModule.fsyncSync(destFd);
323
312
  }
313
+ closeRequiredForSuccess = !params.sync;
324
314
  }
325
315
  }
326
316
  finally {
@@ -328,8 +318,10 @@ export function copyFallbackReplaceSync(params) {
328
318
  try {
329
319
  params.fsModule.closeSync(destFd);
330
320
  }
331
- catch {
332
- // Best-effort close after fallback replacement.
321
+ catch (closeError) {
322
+ if (closeRequiredForSuccess) {
323
+ throw closeError;
324
+ }
333
325
  }
334
326
  }
335
327
  }
@@ -1 +1 @@
1
- {"version":3,"file":"replace-file-copy-source.d.ts","sourceRoot":"","sources":["../src/replace-file-copy-source.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,EAAE,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAQnD,KAAK,qBAAqB,GAAG,IAAI,CAAC,cAAc,kBAAkB,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC,CAAC;AACvF,KAAK,oBAAoB,GAAG,IAAI,CAC9B,OAAO,MAAM,EACb,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,UAAU,GAAG,UAAU,CAClE,CAAC;AAqDF,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,QAAQ,EAAE,qBAAqB,CAAC;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,gBAAgB,CAAC,EAAE,WAAW,CAAC;CAChC,GAAG,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CA8BjD;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,gBAAgB,CAAC,EAAE,WAAW,CAAC;CAChC,GAAG;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CA8BxC"}
1
+ {"version":3,"file":"replace-file-copy-source.d.ts","sourceRoot":"","sources":["../src/replace-file-copy-source.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,EAAE,EAAE,KAAK,WAAW,EAAc,MAAM,SAAS,CAAC;AAQ/D,KAAK,qBAAqB,GAAG,IAAI,CAAC,cAAc,kBAAkB,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC,CAAC;AACvF,KAAK,oBAAoB,GAAG,IAAI,CAC9B,OAAO,MAAM,EACb,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,UAAU,GAAG,UAAU,CAClE,CAAC;AA8DF,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,QAAQ,EAAE,qBAAqB,CAAC;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,gBAAgB,CAAC,EAAE,WAAW,CAAC;CAChC,GAAG,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAyBjD;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,gBAAgB,CAAC,EAAE,WAAW,CAAC;CAChC,GAAG;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAyBxC"}
@@ -17,6 +17,14 @@ function assertSourcePreview(source, src) {
17
17
  throw new FsSafeError("hardlink", `Hardlinked copy fallback source not allowed: ${src}`);
18
18
  }
19
19
  }
20
+ function assertOpenedSource(opened, current, src) {
21
+ if (!opened.isFile() || current.isSymbolicLink() || !sameFileIdentity(opened, current)) {
22
+ throw new FsSafeError("path-mismatch", `Copy fallback source changed while opening: ${src}`);
23
+ }
24
+ if (opened.nlink !== 1) {
25
+ throw new FsSafeError("hardlink", `Hardlinked copy fallback source not allowed: ${src}`);
26
+ }
27
+ }
20
28
  function sourceSymlinkError(src, cause) {
21
29
  return new FsSafeError("symlink", `Refusing copy fallback from non-file source: ${src}`, {
22
30
  cause,
@@ -66,12 +74,7 @@ export async function readOwnedCopySource(params) {
66
74
  const opened = params.fsModule === fs ? syncFs.fstatSync(handle.fd) : await handle.stat();
67
75
  const current = params.fsModule === fs
68
76
  ? syncFs.lstatSync(params.src) : await params.fsModule.lstat(params.src);
69
- if (!opened.isFile() || current.isSymbolicLink() || !sameFileIdentity(opened, current)) {
70
- throw new FsSafeError("path-mismatch", `Copy fallback source changed while opening: ${params.src}`);
71
- }
72
- if (opened.nlink !== 1) {
73
- throw new FsSafeError("hardlink", `Hardlinked copy fallback source not allowed: ${params.src}`);
74
- }
77
+ assertOpenedSource(opened, current, params.src);
75
78
  return { replacement: await handle.readFile(), mode: opened.mode };
76
79
  }
77
80
  finally {
@@ -88,12 +91,7 @@ export function readOwnedCopySourceSync(params) {
88
91
  }
89
92
  const opened = params.fsModule.fstatSync(fd);
90
93
  const current = params.fsModule.lstatSync(params.src);
91
- if (!opened.isFile() || current.isSymbolicLink() || !sameFileIdentity(opened, current)) {
92
- throw new FsSafeError("path-mismatch", `Copy fallback source changed while opening: ${params.src}`);
93
- }
94
- if (opened.nlink !== 1) {
95
- throw new FsSafeError("hardlink", `Hardlinked copy fallback source not allowed: ${params.src}`);
96
- }
94
+ assertOpenedSource(opened, current, params.src);
97
95
  return { replacement: readAllSync(params.fsModule, fd, opened.size), mode: opened.mode };
98
96
  }
99
97
  finally {
@@ -1,15 +1,11 @@
1
1
  import syncFs, { type BigIntStats } from "node:fs";
2
2
  import fs, { type FileHandle } from "node:fs/promises";
3
- import { type AnyAsyncDirectoryGuard } from "./directory-guard.js";
4
3
  type AsyncOwnerFileSystem = Pick<typeof fs, "lstat" | "open" | "unlink">;
5
4
  type SyncOwnerFileSystem = Pick<typeof syncFs, "closeSync" | "fstatSync" | "lstatSync" | "openSync" | "readFileSync" | "unlinkSync">;
5
+ export type AtomicTempFailure = Readonly<{
6
+ error: unknown;
7
+ }>;
6
8
  export declare function removePathIfIdentityUnchanged(targetPath: string, identity: Pick<BigIntStats, "dev" | "ino">): Promise<void>;
7
- export declare function cleanupPinnedFilePath(params: {
8
- pathname: string;
9
- handle: FileHandle;
10
- identity?: BigIntStats;
11
- parentGuard: AnyAsyncDirectoryGuard;
12
- }): Promise<void>;
13
9
  export declare class AsyncAtomicTempOwner {
14
10
  #private;
15
11
  readonly pathname: string;
@@ -26,7 +22,7 @@ export declare class AsyncAtomicTempOwner {
26
22
  markRenamed(): void;
27
23
  finish(params: {
28
24
  fsModule: AsyncOwnerFileSystem;
29
- originalError?: unknown;
25
+ originalFailure?: AtomicTempFailure;
30
26
  throwOnCleanupError: boolean;
31
27
  }): Promise<void>;
32
28
  }
@@ -46,7 +42,7 @@ export declare class SyncAtomicTempOwner {
46
42
  markRenamed(): void;
47
43
  finish(params: {
48
44
  fsModule: SyncOwnerFileSystem;
49
- originalError?: unknown;
45
+ originalFailure?: AtomicTempFailure;
50
46
  throwOnCleanupError: boolean;
51
47
  }): void;
52
48
  }
@@ -1 +1 @@
1
- {"version":3,"file":"replace-file-temp-owner.d.ts","sourceRoot":"","sources":["../src/replace-file-temp-owner.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,EAAE,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,EAAE,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,EAAwD,KAAK,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAQzH,KAAK,oBAAoB,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC,CAAC;AACzE,KAAK,mBAAmB,GAAG,IAAI,CAC7B,OAAO,MAAM,EACb,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,UAAU,GAAG,cAAc,GAAG,YAAY,CACrF,CAAC;AAIF,wBAAsB,6BAA6B,CACjD,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,IAAI,CAAC,WAAW,EAAE,KAAK,GAAG,KAAK,CAAC,GACzC,OAAO,CAAC,IAAI,CAAC,CAOf;AA8DD,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,CAAC,EAAE,WAAW,CAAC;IACvB,WAAW,EAAE,sBAAsB,CAAC;CACrC,GAAG,OAAO,CAAC,IAAI,CAAC,CAuBhB;AA+BD,qBAAa,oBAAoB;;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAM1B,YAAY,QAAQ,EAAE,MAAM,EAG3B;IAED,KAAK,IAAI,IAAI,CAEZ;IAED,QAAQ,CAAC,UAAU,aAAc,WAAW,KAAG,IAAI,CAGjD;IAEF,KAAK,CAAC,IAAI,EAAE;QAAE,MAAM,EAAE,UAAU,CAAC;QAAC,QAAQ,EAAE,WAAW,CAAA;KAAE,GAAG,IAAI,CAG/D;IAED,IAAI,QAAQ,IAAI,WAAW,CAG1B;IAEK,aAAa,CAAC,QAAQ,EAAE,oBAAoB,EAAE,QAAQ,SAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoB3F;IAEK,eAAe,CACnB,QAAQ,EAAE,oBAAoB,EAC9B,QAAQ,EAAE,MAAM,EAChB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,IAAI,CAAC,CA4Cf;IAED,WAAW,IAAI,IAAI,CAGlB;IAEK,MAAM,CAAC,MAAM,EAAE;QACnB,QAAQ,EAAE,oBAAoB,CAAC;QAC/B,aAAa,CAAC,EAAE,OAAO,CAAC;QACxB,mBAAmB,EAAE,OAAO,CAAC;KAC9B,GAAG,OAAO,CAAC,IAAI,CAAC,CA8BhB;CACF;AAED,qBAAa,mBAAmB;;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAM1B,YAAY,QAAQ,EAAE,MAAM,EAG3B;IAED,KAAK,IAAI,IAAI,CAEZ;IAED,QAAQ,CAAC,UAAU,aAAc,WAAW,KAAG,IAAI,CAGjD;IAEF,KAAK,CAAC,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,WAAW,CAAA;KAAE,GAAG,IAAI,CAGvD;IAED,IAAI,QAAQ,IAAI,WAAW,CAG1B;IAED,aAAa,CAAC,QAAQ,EAAE,mBAAmB,EAAE,QAAQ,SAAgB,GAAG,IAAI,CAkB3E;IAED,eAAe,CACb,QAAQ,EAAE,mBAAmB,EAC7B,QAAQ,EAAE,MAAM,EAChB,YAAY,CAAC,EAAE,MAAM,GACpB,IAAI,CAkDN;IAED,WAAW,IAAI,IAAI,CAGlB;IAED,MAAM,CAAC,MAAM,EAAE;QACb,QAAQ,EAAE,mBAAmB,CAAC;QAC9B,aAAa,CAAC,EAAE,OAAO,CAAC;QACxB,mBAAmB,EAAE,OAAO,CAAC;KAC9B,GAAG,IAAI,CAkCP;CACF"}
1
+ {"version":3,"file":"replace-file-temp-owner.d.ts","sourceRoot":"","sources":["../src/replace-file-temp-owner.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,EAAE,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,EAAE,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAWvD,KAAK,oBAAoB,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC,CAAC;AACzE,KAAK,mBAAmB,GAAG,IAAI,CAC7B,OAAO,MAAM,EACb,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,UAAU,GAAG,cAAc,GAAG,YAAY,CACrF,CAAC;AAIF,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC,CAAC;AAmB7D,wBAAsB,6BAA6B,CACjD,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,IAAI,CAAC,WAAW,EAAE,KAAK,GAAG,KAAK,CAAC,GACzC,OAAO,CAAC,IAAI,CAAC,CAOf;AAkFD,qBAAa,oBAAoB;;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAM1B,YAAY,QAAQ,EAAE,MAAM,EAG3B;IAED,KAAK,IAAI,IAAI,CAEZ;IAED,QAAQ,CAAC,UAAU,aAAc,WAAW,KAAG,IAAI,CAGjD;IAEF,KAAK,CAAC,IAAI,EAAE;QAAE,MAAM,EAAE,UAAU,CAAC;QAAC,QAAQ,EAAE,WAAW,CAAA;KAAE,GAAG,IAAI,CAG/D;IAED,IAAI,QAAQ,IAAI,WAAW,CAG1B;IAEK,aAAa,CAAC,QAAQ,EAAE,oBAAoB,EAAE,QAAQ,SAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoB3F;IAEK,eAAe,CACnB,QAAQ,EAAE,oBAAoB,EAC9B,QAAQ,EAAE,MAAM,EAChB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,IAAI,CAAC,CA8Cf;IAED,WAAW,IAAI,IAAI,CAGlB;IAEK,MAAM,CAAC,MAAM,EAAE;QACnB,QAAQ,EAAE,oBAAoB,CAAC;QAC/B,eAAe,CAAC,EAAE,iBAAiB,CAAC;QACpC,mBAAmB,EAAE,OAAO,CAAC;KAC9B,GAAG,OAAO,CAAC,IAAI,CAAC,CAqChB;CACF;AAED,qBAAa,mBAAmB;;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAM1B,YAAY,QAAQ,EAAE,MAAM,EAG3B;IAED,KAAK,IAAI,IAAI,CAEZ;IAED,QAAQ,CAAC,UAAU,aAAc,WAAW,KAAG,IAAI,CAGjD;IAEF,KAAK,CAAC,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,WAAW,CAAA;KAAE,GAAG,IAAI,CAGvD;IAED,IAAI,QAAQ,IAAI,WAAW,CAG1B;IAED,aAAa,CAAC,QAAQ,EAAE,mBAAmB,EAAE,QAAQ,SAAgB,GAAG,IAAI,CAkB3E;IAED,eAAe,CACb,QAAQ,EAAE,mBAAmB,EAC7B,QAAQ,EAAE,MAAM,EAChB,YAAY,CAAC,EAAE,MAAM,GACpB,IAAI,CAkDN;IAED,WAAW,IAAI,IAAI,CAGlB;IAED,MAAM,CAAC,MAAM,EAAE;QACb,QAAQ,EAAE,mBAAmB,CAAC;QAC9B,eAAe,CAAC,EAAE,iBAAiB,CAAC;QACpC,mBAAmB,EAAE,OAAO,CAAC;KAC9B,GAAG,IAAI,CAuCP;CACF"}