@openclaw/fs-safe 0.9.0 → 0.11.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 (326) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/LICENSE +1 -0
  3. package/README.md +52 -4
  4. package/dist/absolute-path.d.ts.map +1 -1
  5. package/dist/absolute-path.js +3 -2
  6. package/dist/advanced.d.ts +5 -0
  7. package/dist/advanced.d.ts.map +1 -1
  8. package/dist/advanced.js +5 -0
  9. package/dist/archive-crc32.d.ts.map +1 -1
  10. package/dist/archive-crc32.js +6 -1
  11. package/dist/archive-deadline.d.ts.map +1 -1
  12. package/dist/archive-deadline.js +3 -4
  13. package/dist/archive-durability.d.ts +6 -6
  14. package/dist/archive-durability.d.ts.map +1 -1
  15. package/dist/archive-durability.js +1 -1
  16. package/dist/archive-entry.d.ts.map +1 -1
  17. package/dist/archive-entry.js +4 -5
  18. package/dist/archive-gzip-tail.d.ts +3 -0
  19. package/dist/archive-gzip-tail.d.ts.map +1 -1
  20. package/dist/archive-gzip-tail.js +23 -3
  21. package/dist/archive-input.d.ts.map +1 -1
  22. package/dist/archive-input.js +4 -2
  23. package/dist/archive-merge.d.ts +5 -1
  24. package/dist/archive-merge.d.ts.map +1 -1
  25. package/dist/archive-merge.js +16 -13
  26. package/dist/archive-native.d.ts.map +1 -1
  27. package/dist/archive-native.js +7 -6
  28. package/dist/archive-parser.wasm +0 -0
  29. package/dist/archive-read.d.ts.map +1 -1
  30. package/dist/archive-read.js +83 -74
  31. package/dist/archive-staging.d.ts +6 -3
  32. package/dist/archive-staging.d.ts.map +1 -1
  33. package/dist/archive-staging.js +42 -22
  34. package/dist/archive-tar-stream.d.ts +11 -4
  35. package/dist/archive-tar-stream.d.ts.map +1 -1
  36. package/dist/archive-tar-stream.js +23 -10
  37. package/dist/archive-tar-wasm.d.ts.map +1 -1
  38. package/dist/archive-tar-wasm.js +18 -14
  39. package/dist/archive-zip-admission.d.ts +1 -1
  40. package/dist/archive-zip-admission.d.ts.map +1 -1
  41. package/dist/archive-zip-admission.js +48 -12
  42. package/dist/archive-zip-loader.d.ts +6 -0
  43. package/dist/archive-zip-loader.d.ts.map +1 -0
  44. package/dist/archive-zip-loader.js +38 -0
  45. package/dist/archive-zip-names.d.ts.map +1 -1
  46. package/dist/archive-zip-names.js +13 -8
  47. package/dist/archive-zip-preflight.d.ts +2 -3
  48. package/dist/archive-zip-preflight.d.ts.map +1 -1
  49. package/dist/archive-zip-preflight.js +2 -34
  50. package/dist/archive.d.ts.map +1 -1
  51. package/dist/archive.js +12 -10
  52. package/dist/bounded-read.d.ts +12 -0
  53. package/dist/bounded-read.d.ts.map +1 -1
  54. package/dist/bounded-read.js +82 -45
  55. package/dist/clone-metadata.d.ts +19 -0
  56. package/dist/clone-metadata.d.ts.map +1 -0
  57. package/dist/clone-metadata.js +32 -0
  58. package/dist/copy-file-input.d.ts +22 -0
  59. package/dist/copy-file-input.d.ts.map +1 -0
  60. package/dist/copy-file-input.js +69 -0
  61. package/dist/copy-policy.d.ts +3 -0
  62. package/dist/copy-policy.d.ts.map +1 -0
  63. package/dist/copy-policy.js +8 -0
  64. package/dist/copy-publication.d.ts +10 -1
  65. package/dist/copy-publication.d.ts.map +1 -1
  66. package/dist/copy-publication.js +27 -0
  67. package/dist/copy-tree-portable.d.ts +9 -0
  68. package/dist/copy-tree-portable.d.ts.map +1 -0
  69. package/dist/copy-tree-portable.js +222 -0
  70. package/dist/copy.d.ts +16 -0
  71. package/dist/copy.d.ts.map +1 -0
  72. package/dist/copy.js +125 -0
  73. package/dist/directory-durability.d.ts.map +1 -1
  74. package/dist/directory-durability.js +5 -4
  75. package/dist/directory-guard.d.ts +11 -1
  76. package/dist/directory-guard.d.ts.map +1 -1
  77. package/dist/directory-guard.js +53 -11
  78. package/dist/durability.d.ts +1 -1
  79. package/dist/durability.d.ts.map +1 -1
  80. package/dist/durability.js +1 -1
  81. package/dist/error-detail.d.ts.map +1 -1
  82. package/dist/error-detail.js +4 -1
  83. package/dist/file-handle-transfer.d.ts +14 -0
  84. package/dist/file-handle-transfer.d.ts.map +1 -0
  85. package/dist/file-handle-transfer.js +64 -0
  86. package/dist/file-hash.d.ts +9 -2
  87. package/dist/file-hash.d.ts.map +1 -1
  88. package/dist/file-hash.js +135 -39
  89. package/dist/file-lock-sync.d.ts.map +1 -1
  90. package/dist/file-lock-sync.js +8 -4
  91. package/dist/file-store-boundary.d.ts.map +1 -1
  92. package/dist/file-store-boundary.js +7 -5
  93. package/dist/file-store-path.d.ts +3 -0
  94. package/dist/file-store-path.d.ts.map +1 -0
  95. package/dist/file-store-path.js +27 -0
  96. package/dist/file-store-prune.d.ts.map +1 -1
  97. package/dist/file-store-prune.js +6 -4
  98. package/dist/file-store-sync-write.d.ts.map +1 -1
  99. package/dist/file-store-sync-write.js +56 -44
  100. package/dist/file-store.d.ts.map +1 -1
  101. package/dist/file-store.js +2 -18
  102. package/dist/filename.d.ts.map +1 -1
  103. package/dist/filename.js +4 -13
  104. package/dist/guarded-mkdir.d.ts +1 -0
  105. package/dist/guarded-mkdir.d.ts.map +1 -1
  106. package/dist/guarded-mkdir.js +4 -2
  107. package/dist/guarded-mutation.d.ts +2 -0
  108. package/dist/guarded-mutation.d.ts.map +1 -1
  109. package/dist/guarded-mutation.js +8 -4
  110. package/dist/guest-dispatch-python.d.ts +2 -0
  111. package/dist/guest-dispatch-python.d.ts.map +1 -0
  112. package/dist/guest-dispatch-python.js +117 -0
  113. package/dist/guest-native-python.d.ts +4 -0
  114. package/dist/guest-native-python.d.ts.map +1 -0
  115. package/dist/guest-native-python.js +135 -0
  116. package/dist/guest.d.ts +9 -0
  117. package/dist/guest.d.ts.map +1 -0
  118. package/dist/guest.js +413 -0
  119. package/dist/home-dir.d.ts.map +1 -1
  120. package/dist/home-dir.js +9 -7
  121. package/dist/index.d.ts +1 -1
  122. package/dist/index.d.ts.map +1 -1
  123. package/dist/install-path.d.ts.map +1 -1
  124. package/dist/install-path.js +5 -8
  125. package/dist/json-durable-queue-directory.js +3 -3
  126. package/dist/json-durable-queue.d.ts.map +1 -1
  127. package/dist/json-durable-queue.js +19 -18
  128. package/dist/json.d.ts.map +1 -1
  129. package/dist/json.js +2 -1
  130. package/dist/local-roots.d.ts.map +1 -1
  131. package/dist/local-roots.js +24 -25
  132. package/dist/move-path-stage.d.ts +8 -0
  133. package/dist/move-path-stage.d.ts.map +1 -0
  134. package/dist/move-path-stage.js +56 -0
  135. package/dist/move-path.d.ts.map +1 -1
  136. package/dist/move-path.js +30 -34
  137. package/dist/mutation-authority.d.ts +9 -0
  138. package/dist/mutation-authority.d.ts.map +1 -0
  139. package/dist/mutation-authority.js +36 -0
  140. package/dist/native-binding.d.ts +29 -1
  141. package/dist/native-binding.d.ts.map +1 -1
  142. package/dist/native-operations.d.ts +1 -1
  143. package/dist/native-operations.d.ts.map +1 -1
  144. package/dist/native-operations.js +11 -5
  145. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  146. package/dist/native-pinned-write-windows.js +19 -7
  147. package/dist/native-pinned-write.d.ts.map +1 -1
  148. package/dist/native-pinned-write.js +3 -1
  149. package/dist/native-staged-file.d.ts +3 -3
  150. package/dist/native-staged-file.d.ts.map +1 -1
  151. package/dist/native-staged-file.js +35 -8
  152. package/dist/opened-realpath.d.ts.map +1 -1
  153. package/dist/opened-realpath.js +5 -4
  154. package/dist/output.d.ts +2 -0
  155. package/dist/output.d.ts.map +1 -1
  156. package/dist/output.js +2 -0
  157. package/dist/overwrite-file-handle.d.ts +8 -0
  158. package/dist/overwrite-file-handle.d.ts.map +1 -0
  159. package/dist/overwrite-file-handle.js +42 -0
  160. package/dist/path-case.d.ts +7 -0
  161. package/dist/path-case.d.ts.map +1 -0
  162. package/dist/path-case.js +136 -0
  163. package/dist/path.d.ts.map +1 -1
  164. package/dist/path.js +5 -2
  165. package/dist/permissions-windows.d.ts +1 -1
  166. package/dist/permissions-windows.d.ts.map +1 -1
  167. package/dist/permissions-windows.js +85 -53
  168. package/dist/pinned-open.d.ts.map +1 -1
  169. package/dist/pinned-open.js +3 -1
  170. package/dist/pinned-operation.js +1 -1
  171. package/dist/pinned-write.d.ts +7 -3
  172. package/dist/pinned-write.d.ts.map +1 -1
  173. package/dist/pinned-write.js +51 -30
  174. package/dist/positional-read.d.ts +9 -0
  175. package/dist/positional-read.d.ts.map +1 -0
  176. package/dist/positional-read.js +36 -0
  177. package/dist/private-temp-workspace.d.ts.map +1 -1
  178. package/dist/private-temp-workspace.js +6 -4
  179. package/dist/publish-copy-stage.d.ts +13 -0
  180. package/dist/publish-copy-stage.d.ts.map +1 -0
  181. package/dist/publish-copy-stage.js +47 -0
  182. package/dist/publish-file.d.ts.map +1 -1
  183. package/dist/publish-file.js +6 -5
  184. package/dist/realpath.d.ts +4 -0
  185. package/dist/realpath.d.ts.map +1 -0
  186. package/dist/realpath.js +43 -0
  187. package/dist/recursive-mkdir-path.d.ts +3 -0
  188. package/dist/recursive-mkdir-path.d.ts.map +1 -0
  189. package/dist/recursive-mkdir-path.js +8 -0
  190. package/dist/replace-directory.d.ts.map +1 -1
  191. package/dist/replace-directory.js +2 -1
  192. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  193. package/dist/replace-file-copy-fallback.js +23 -31
  194. package/dist/replace-file-copy-source.d.ts.map +1 -1
  195. package/dist/replace-file-copy-source.js +7 -12
  196. package/dist/replace-file-mode.d.ts +3 -0
  197. package/dist/replace-file-mode.d.ts.map +1 -0
  198. package/dist/replace-file-mode.js +10 -0
  199. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  200. package/dist/replace-file-temp-owner.js +6 -2
  201. package/dist/replace-file.d.ts +1 -0
  202. package/dist/replace-file.d.ts.map +1 -1
  203. package/dist/replace-file.js +14 -8
  204. package/dist/root-context.d.ts +3 -0
  205. package/dist/root-context.d.ts.map +1 -1
  206. package/dist/root-context.js +53 -28
  207. package/dist/root-create-input.d.ts +10 -0
  208. package/dist/root-create-input.d.ts.map +1 -0
  209. package/dist/root-create-input.js +80 -0
  210. package/dist/root-directory-list.d.ts +26 -0
  211. package/dist/root-directory-list.d.ts.map +1 -0
  212. package/dist/root-directory-list.js +219 -0
  213. package/dist/root-entries.d.ts +11 -0
  214. package/dist/root-entries.d.ts.map +1 -0
  215. package/dist/root-entries.js +61 -0
  216. package/dist/root-errors.d.ts +6 -5
  217. package/dist/root-errors.d.ts.map +1 -1
  218. package/dist/root-errors.js +16 -12
  219. package/dist/root-file.d.ts +2 -0
  220. package/dist/root-file.d.ts.map +1 -1
  221. package/dist/root-file.js +12 -4
  222. package/dist/root-impl.d.ts +17 -51
  223. package/dist/root-impl.d.ts.map +1 -1
  224. package/dist/root-impl.js +358 -234
  225. package/dist/root-options.d.ts +77 -0
  226. package/dist/root-options.d.ts.map +1 -0
  227. package/dist/root-options.js +18 -0
  228. package/dist/root-path-existing.d.ts +5 -0
  229. package/dist/root-path-existing.d.ts.map +1 -1
  230. package/dist/root-path-existing.js +59 -3
  231. package/dist/root-path-symlink.d.ts.map +1 -1
  232. package/dist/root-path-symlink.js +3 -2
  233. package/dist/root-path.d.ts +1 -0
  234. package/dist/root-path.d.ts.map +1 -1
  235. package/dist/root-path.js +74 -162
  236. package/dist/root-paths.d.ts.map +1 -1
  237. package/dist/root-paths.js +13 -9
  238. package/dist/root-remove.d.ts +5 -0
  239. package/dist/root-remove.d.ts.map +1 -0
  240. package/dist/root-remove.js +286 -0
  241. package/dist/root-symlink-policy.d.ts +14 -0
  242. package/dist/root-symlink-policy.d.ts.map +1 -0
  243. package/dist/root-symlink-policy.js +34 -0
  244. package/dist/root-walk.d.ts +5 -4
  245. package/dist/root-walk.d.ts.map +1 -1
  246. package/dist/root-walk.js +99 -50
  247. package/dist/root-write-mode.d.ts.map +1 -1
  248. package/dist/root-write-mode.js +3 -5
  249. package/dist/root.d.ts +4 -1
  250. package/dist/root.d.ts.map +1 -1
  251. package/dist/secret-file.d.ts.map +1 -1
  252. package/dist/secret-file.js +2 -1
  253. package/dist/secret-read-async.d.ts.map +1 -1
  254. package/dist/secret-read-async.js +2 -1
  255. package/dist/secure-file.d.ts.map +1 -1
  256. package/dist/secure-file.js +25 -8
  257. package/dist/secure-temp-dir.d.ts.map +1 -1
  258. package/dist/secure-temp-dir.js +2 -1
  259. package/dist/sibling-staged-file.d.ts +1 -0
  260. package/dist/sibling-staged-file.d.ts.map +1 -1
  261. package/dist/sibling-staged-file.js +42 -8
  262. package/dist/sibling-temp.d.ts +2 -0
  263. package/dist/sibling-temp.d.ts.map +1 -1
  264. package/dist/sibling-temp.js +6 -4
  265. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  266. package/dist/sidecar-lock-acquire.js +17 -5
  267. package/dist/sidecar-lock-policy.d.ts +2 -0
  268. package/dist/sidecar-lock-policy.d.ts.map +1 -1
  269. package/dist/sidecar-lock-policy.js +20 -1
  270. package/dist/staged-directory.d.ts.map +1 -1
  271. package/dist/staged-directory.js +4 -3
  272. package/dist/temp-cleanup.d.ts.map +1 -1
  273. package/dist/temp-cleanup.js +3 -2
  274. package/dist/temp-target.d.ts +14 -12
  275. package/dist/temp-target.d.ts.map +1 -1
  276. package/dist/temp-target.js +12 -6
  277. package/dist/timing.d.ts +1 -0
  278. package/dist/timing.d.ts.map +1 -1
  279. package/dist/timing.js +25 -6
  280. package/dist/trash.d.ts.map +1 -1
  281. package/dist/trash.js +9 -7
  282. package/dist/unicode-path.d.ts +3 -0
  283. package/dist/unicode-path.d.ts.map +1 -0
  284. package/dist/unicode-path.js +13 -0
  285. package/dist/walk.d.ts.map +1 -1
  286. package/dist/walk.js +9 -28
  287. package/dist/windows-owner.d.ts +9 -12
  288. package/dist/windows-owner.d.ts.map +1 -1
  289. package/dist/windows-owner.js +24 -58
  290. package/dist/write-file-handle.d.ts +7 -0
  291. package/dist/write-file-handle.d.ts.map +1 -0
  292. package/dist/write-file-handle.js +26 -0
  293. package/docs/advanced.md +22 -4
  294. package/docs/archive.md +44 -9
  295. package/docs/atomic.md +24 -1
  296. package/docs/config.md +1 -0
  297. package/docs/contributing.md +36 -1
  298. package/docs/copy.md +155 -0
  299. package/docs/directory-identity.md +85 -0
  300. package/docs/durability.md +53 -3
  301. package/docs/entries.md +109 -0
  302. package/docs/errors.md +4 -4
  303. package/docs/file-store.md +15 -0
  304. package/docs/guest.md +141 -0
  305. package/docs/in-place-write.md +81 -0
  306. package/docs/index.md +2 -0
  307. package/docs/install.md +31 -0
  308. package/docs/local-roots.md +8 -1
  309. package/docs/native-helper.md +10 -3
  310. package/docs/native.md +18 -1
  311. package/docs/output.md +32 -6
  312. package/docs/path-case.md +64 -0
  313. package/docs/path-scope.md +1 -1
  314. package/docs/permissions.md +29 -10
  315. package/docs/positional-read.md +63 -0
  316. package/docs/public-api.md +31 -2
  317. package/docs/root.md +196 -6
  318. package/docs/secure-file.md +2 -0
  319. package/docs/security-model.md +14 -0
  320. package/docs/sidecar-lock.md +12 -3
  321. package/docs/temp.md +35 -6
  322. package/docs/timing.md +2 -0
  323. package/docs/types.md +19 -12
  324. package/docs/walk.md +54 -5
  325. package/docs/writing.md +173 -5
  326. package/package.json +19 -8
package/dist/root-impl.js CHANGED
@@ -1,8 +1,61 @@
1
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
2
+ if (value !== null && value !== void 0) {
3
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
4
+ var dispose, inner;
5
+ if (async) {
6
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
7
+ dispose = value[Symbol.asyncDispose];
8
+ }
9
+ if (dispose === void 0) {
10
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
11
+ dispose = value[Symbol.dispose];
12
+ if (async) inner = dispose;
13
+ }
14
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
15
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
16
+ env.stack.push({ value: value, dispose: dispose, async: async });
17
+ }
18
+ else if (async) {
19
+ env.stack.push({ async: true });
20
+ }
21
+ return value;
22
+ };
23
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
24
+ return function (env) {
25
+ function fail(e) {
26
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
27
+ env.hasError = true;
28
+ }
29
+ var r, s = 0;
30
+ function next() {
31
+ while (r = env.stack.pop()) {
32
+ try {
33
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
34
+ if (r.dispose) {
35
+ var result = r.dispose.call(r.value);
36
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
37
+ }
38
+ else s |= 1;
39
+ }
40
+ catch (e) {
41
+ fail(e);
42
+ }
43
+ }
44
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
45
+ if (env.hasError) throw env.error;
46
+ }
47
+ return next();
48
+ };
49
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
50
+ var e = new Error(message);
51
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
+ });
1
53
  import { randomUUID } from "node:crypto";
2
54
  import fsSync, { constants as fsConstants } from "node:fs";
3
55
  import fs from "node:fs/promises";
4
56
  import path from "node:path";
5
57
  import { normalizeMaxBytes } from "./byte-budget.js";
58
+ import { assertCopySourceCurrent, resolveFileCopyCloneMode } from "./copy-file-input.js";
6
59
  import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, createNearestExistingDirectoryGuard } from "./directory-guard.js";
7
60
  import { FsSafeError } from "./errors.js";
8
61
  import { syncDirectoryBestEffort } from "./directory-durability.js";
@@ -18,20 +71,31 @@ import { validatePinnedOperationPayload } from "./pinned-operation.js";
18
71
  import { assertNoPathAliasEscape, PATH_ALIAS_POLICIES } from "./path-policy.js";
19
72
  import { assertNoNulPathInput, assertNoUnsafeDeviceReadPath, hasNodeErrorCode, isNotFoundPathError, isPathInside, isSymlinkOpenError, } from "./path.js";
20
73
  import { readOpenedFileSafely } from "./read-opened-file.js";
74
+ import { cleanupPinnedFilePath } from "./replace-file-temp-owner.js";
21
75
  import { resolveReadOpenFlags } from "./read-open-flags.js";
76
+ import { realpathSync } from "./realpath.js";
22
77
  import { isNonRegularWriteOpenError, resolveNonblockingWriteFlag } from "./write-open-flags.js";
23
78
  import { resolveRootPath } from "./root-path.js";
79
+ import { openRootDirectoryListing, listDirectoryPath, pathStatFromStats } from "./root-directory-list.js";
80
+ import { entriesInRoot } from "./root-entries.js";
24
81
  import { assertRootIdentityCurrent, assertValidRootDestinationPath, assertValidRootRelativePath, ensureTrailingSep, expandRelativePathWithHome, resolvePathInRoot, resolveRootContext, rootRelativeReadPath, } from "./root-context.js";
25
- import { fileNotFoundError, hardlinkedPathNotAllowedError, isAlreadyExistsError, normalizePinnedPathError, normalizePinnedWriteError, normalizeRemoveGuardError, normalizeRemovePathError, outsideWorkspaceError, } from "./root-errors.js";
82
+ import { fileNotFoundError, hardlinkedPathNotAllowedError, isAlreadyExistsError, normalizePinnedPathError, normalizePinnedWriteError, outsideWorkspaceError, } from "./root-errors.js";
26
83
  import { getFsSafeTestHooks } from "./test-hooks.js";
27
84
  import { stringifyJsonDocument } from "./json-stringify.js";
28
85
  import { walkRoot } from "./root-walk.js";
29
86
  import { registerTempPathForExit } from "./temp-cleanup.js";
87
+ import { removePathInRootFallback, validateRemoveOptions } from "./root-remove.js";
30
88
  import { serializePathWrite } from "./write-queue.js";
31
89
  import { verifyAtomicWriteResult } from "./root-write-verification.js";
32
90
  import { inheritWriteTargetMode } from "./root-write-mode.js";
33
91
  import { inspectFileIdentity } from "./strict-file-identity.js";
34
- import { onCopyPublication } from "./copy-publication.js";
92
+ import { createCopyPublicationObserver, onCopyPublication } from "./copy-publication.js";
93
+ import { writeAllToFile } from "./write-file-handle.js";
94
+ import { createInputOptions, rethrowCreateInputError, rootWriteInput } from "./root-create-input.js";
95
+ import { assertFinalSymlinkRejected, mutationSymlinkResolution, readSymlinkResolution } from "./root-symlink-policy.js";
96
+ import { mergeReadOptions, readDefaults, } from "./root-options.js";
97
+ import { composeMutationAssertions, MutationAuthorityError, rethrowMutationAuthorityError } from "./mutation-authority.js";
98
+ export { DEFAULT_ROOT_MAX_BYTES } from "./root-options.js";
35
99
  export { resolveOpenedFileRealPathForHandle } from "./opened-realpath.js";
36
100
  function logWarn(message) {
37
101
  if (process.env.FS_SAFE_DEBUG_WARNINGS === "1") {
@@ -54,22 +118,6 @@ const OPEN_APPEND_CREATE_FLAGS = fsConstants.O_RDWR |
54
118
  fsConstants.O_CREAT |
55
119
  fsConstants.O_EXCL |
56
120
  (SUPPORTS_NOFOLLOW ? fsConstants.O_NOFOLLOW : 0);
57
- export const DEFAULT_ROOT_MAX_BYTES = 16 * 1024 * 1024;
58
- function pathStatFromStats(stat) {
59
- return {
60
- dev: Number(stat.dev),
61
- gid: Number(stat.gid),
62
- ino: Number(stat.ino),
63
- isDirectory: stat.isDirectory(),
64
- isFile: stat.isFile(),
65
- isSymbolicLink: stat.isSymbolicLink(),
66
- mode: stat.mode,
67
- mtimeMs: stat.mtimeMs,
68
- nlink: stat.nlink,
69
- size: stat.size,
70
- uid: stat.uid,
71
- };
72
- }
73
121
  function openResult(params) {
74
122
  return {
75
123
  handle: params.handle,
@@ -215,12 +263,14 @@ export class RootHandle {
215
263
  mutationOptions(options) {
216
264
  return {
217
265
  ...options,
266
+ assertBeforeMutation: composeMutationAssertions(this.defaults.assertBeforeMutation, options.assertBeforeMutation),
267
+ mutationSymlinks: options.mutationSymlinks ?? this.defaults.mutationSymlinks,
218
268
  denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
219
269
  };
220
270
  }
221
271
  async resolve(relativePath) {
222
272
  assertValidRootDestinationPath(relativePath);
223
- return (await resolvePathInRoot(this.context, relativePath, { allowFinalSymlink: true })).resolved;
273
+ return (await resolvePathInRoot(this.context, relativePath, { allowFinalSymlink: true, resolveCanonical: true })).resolved;
224
274
  }
225
275
  async open(relativePath, options = {}) {
226
276
  return await openFileInRoot(this.context, {
@@ -266,7 +316,7 @@ export class RootHandle {
266
316
  ...this.mutationOptions(options),
267
317
  append: writeMode === "append",
268
318
  truncateExisting: writeMode === "replace",
269
- });
319
+ }).catch(rethrowMutationAuthorityError);
270
320
  }
271
321
  async append(relativePath, data, options = {}) {
272
322
  assertValidRootDestinationPath(relativePath);
@@ -277,28 +327,30 @@ export class RootHandle {
277
327
  mode: this.defaults.mode,
278
328
  ...this.mutationOptions(options),
279
329
  durable: options.durable ?? this.defaults.durable ?? true,
280
- });
330
+ }).catch(rethrowMutationAuthorityError);
281
331
  }
282
332
  async remove(relativePath, options = {}) {
283
333
  assertValidRootRelativePath(relativePath);
334
+ validateRemoveOptions(options);
335
+ options.signal?.throwIfAborted();
284
336
  await removePathInRoot(this.context, {
285
337
  relativePath,
286
338
  ...this.mutationOptions(options),
287
- });
339
+ }).catch(rethrowMutationAuthorityError);
288
340
  }
289
341
  async mkdir(relativePath, options = {}) {
290
342
  assertValidRootDestinationPath(relativePath);
291
343
  await mkdirPathInRoot(this.context, {
292
344
  relativePath,
293
345
  ...this.mutationOptions(options),
294
- });
346
+ }).catch(rethrowMutationAuthorityError);
295
347
  }
296
348
  async ensureRoot(options = {}) {
297
349
  await mkdirPathInRoot(this.context, {
298
350
  relativePath: "",
299
351
  allowRoot: true,
300
352
  ...this.mutationOptions(options),
301
- });
353
+ }).catch(rethrowMutationAuthorityError);
302
354
  }
303
355
  async write(relativePath, data, options = {}) {
304
356
  assertValidRootDestinationPath(relativePath);
@@ -310,7 +362,7 @@ export class RootHandle {
310
362
  renameIdentity: this.defaults.renameIdentity,
311
363
  ...this.mutationOptions(options),
312
364
  durable: options.durable ?? this.defaults.durable ?? true,
313
- });
365
+ }).catch(rethrowMutationAuthorityError);
314
366
  }
315
367
  async create(relativePath, data, options = {}) {
316
368
  assertValidRootDestinationPath(relativePath);
@@ -319,10 +371,10 @@ export class RootHandle {
319
371
  data,
320
372
  mkdir: this.defaults.mkdir,
321
373
  mode: this.defaults.mode,
322
- ...this.mutationOptions(options),
374
+ ...this.mutationOptions(createInputOptions(data, options, this.defaults.maxBytes)),
323
375
  durable: options.durable ?? this.defaults.durable ?? true,
324
376
  overwrite: false,
325
- });
377
+ }).catch(rethrowMutationAuthorityError).catch(rethrowCreateInputError);
326
378
  }
327
379
  async writeJson(relativePath, data, options = {}) {
328
380
  const { replacer, space, trailingNewline = true, ...writeOptions } = options;
@@ -334,19 +386,20 @@ export class RootHandle {
334
386
  const json = stringifyJsonDocument(data, replacer, space);
335
387
  await this.create(relativePath, trailingNewline ? `${json}\n` : json, writeOptions);
336
388
  }
337
- async copyIn(relativePath, sourcePath, options = {}) {
389
+ async copyIn(relativePath, source, options = {}) {
390
+ options.signal?.throwIfAborted();
338
391
  assertValidRootDestinationPath(relativePath);
339
392
  const { maxBytes, ...copyOptions } = this.mutationOptions(options);
340
393
  await copyFileInRoot(this.context, {
341
- sourcePath,
394
+ source: typeof source === "string" ? source : { root: source.root, relativePath: source.relativePath },
342
395
  relativePath,
343
396
  maxBytes: normalizeMaxBytes(maxBytes, { defaultValue: this.defaults.maxBytes }),
344
397
  mkdir: this.defaults.mkdir,
345
- mode: this.defaults.mode,
346
398
  ...copyOptions,
399
+ mode: options.mode ?? this.defaults.mode,
347
400
  durable: options.durable ?? this.defaults.durable ?? true,
348
401
  verifyPublished: options[onCopyPublication],
349
- });
402
+ }).catch(rethrowMutationAuthorityError);
350
403
  }
351
404
  async exists(relativePath) {
352
405
  try {
@@ -374,7 +427,7 @@ export class RootHandle {
374
427
  assertValidRootRelativePath(fromRelative);
375
428
  assertValidRootDestinationPath(toRelative);
376
429
  validatePinnedOperationPayload({ from: fromRelative, to: toRelative });
377
- const { denyMutations } = this.mutationOptions(options);
430
+ const { denyMutations, assertBeforeMutation, mutationSymlinks } = this.mutationOptions(options);
378
431
  await assertMoveMutationAllowed(this.context, {
379
432
  fromRelative,
380
433
  toRelative,
@@ -383,40 +436,53 @@ export class RootHandle {
383
436
  await movePathFallback(this.context, {
384
437
  fromRelative,
385
438
  denyMutations,
439
+ assertBeforeMutation,
440
+ mutationSymlinks,
386
441
  overwrite: options.overwrite ?? false,
387
442
  toRelative,
443
+ }).catch(rethrowMutationAuthorityError);
444
+ }
445
+ entries(relativePath, options = {}) {
446
+ assertValidRootRelativePath(relativePath);
447
+ return entriesInRoot(this.context, relativePath, {
448
+ ...options,
449
+ symlinks: options.symlinks ?? this.defaults.symlinks,
388
450
  });
389
451
  }
390
452
  walk(relativePath, options) {
391
453
  assertValidRootRelativePath(relativePath);
392
- return walkRoot(this, relativePath, options);
454
+ return walkRoot({
455
+ rootReal: this.rootReal,
456
+ stat: relative => this.stat(relative),
457
+ list: async (relative, listingOptions) => {
458
+ validatePinnedOperationPayload({ relativePath: relative });
459
+ const resolved = await resolvePinnedPathInRoot(this.context, { relativePath: relative, allowRoot: true });
460
+ return await openRootDirectoryListing(this.context, resolved.resolved, listingOptions);
461
+ },
462
+ }, relativePath, options);
393
463
  }
394
464
  }
395
- function readDefaults(defaults) {
396
- return {
397
- hardlinks: defaults.hardlinks,
398
- maxBytes: normalizeMaxBytes(defaults.maxBytes, { defaultValue: DEFAULT_ROOT_MAX_BYTES }),
399
- symlinks: defaults.symlinks,
400
- };
401
- }
402
- function mergeReadOptions(defaults, options) {
403
- const merged = readDefaults(defaults);
404
- if (options.hardlinks !== undefined)
405
- merged.hardlinks = options.hardlinks;
406
- merged.maxBytes = normalizeMaxBytes(options.maxBytes, { defaultValue: merged.maxBytes });
407
- if (options.symlinks !== undefined)
408
- merged.symlinks = options.symlinks;
409
- return merged;
410
- }
411
465
  export async function root(rootDir, defaults = {}) {
412
466
  normalizeMaxBytes(defaults.maxBytes);
413
467
  return new RootHandle(await resolveRootContext(rootDir), defaults);
414
468
  }
469
+ // Internal callers that already hold an exact admitted directory capability must
470
+ // not recapture a different filesystem object while constructing the Root.
471
+ export function rootFromDirectoryGuard(guard, defaults = {}) {
472
+ normalizeMaxBytes(defaults.maxBytes);
473
+ return new RootHandle({
474
+ rootDir: path.resolve(guard.dir),
475
+ rootIdentity: { dev: guard.stat.dev, ino: guard.stat.ino },
476
+ rootReal: guard.realPath,
477
+ rootWithSep: ensureTrailingSep(guard.realPath),
478
+ }, defaults);
479
+ }
415
480
  async function openFileInRoot(root, params) {
416
481
  const { rootWithSep, resolved } = await resolvePathInRoot(root, params.relativePath, {
417
482
  allowFinalSymlink: true,
418
483
  rejectUnsafeDeviceReads: true,
419
- rejectSymlinks: params.symlinks !== "follow-within-root",
484
+ ...readSymlinkResolution(params.symlinks),
485
+ resolveCanonical: true,
420
486
  });
421
487
  const { opened } = await openVerifiedLocalFile(resolved, {
422
488
  symlinks: params.symlinks,
@@ -472,38 +538,22 @@ function buildAtomicWriteTempPath(targetPath) {
472
538
  function rootWriteQueueKey(root, relativePath) {
473
539
  return `${root.rootReal}\0${relativePath}`;
474
540
  }
475
- async function prepareRootWriteTarget(rootReal, targetPath) {
541
+ async function prepareRootWriteTarget(rootReal, targetPath, assertBeforeMutation) {
476
542
  const parentPath = await mkdirPathComponentsWithGuards({
477
543
  rootReal,
478
544
  targetPath: path.dirname(targetPath),
545
+ assertBeforeMutation,
479
546
  });
480
547
  // Continue through the guarded walk's real parent instead of re-entering
481
548
  // the original path through a symlinked component.
482
549
  return path.join(parentPath, path.basename(targetPath));
483
550
  }
484
- async function writeTempFileForAtomicReplace(params) {
485
- const tempHandle = await fs.open(params.tempPath, OPEN_WRITE_CREATE_FLAGS, params.mode);
486
- try {
487
- if (typeof params.data === "string") {
488
- await tempHandle.writeFile(params.data, params.encoding ?? "utf8");
489
- }
490
- else {
491
- await tempHandle.writeFile(params.data);
492
- }
493
- return {
494
- handle: tempHandle,
495
- identity: fsSync.fstatSync(tempHandle.fd, { bigint: true }),
496
- };
497
- }
498
- catch (error) {
499
- await tempHandle.close().catch(() => { });
500
- throw error;
501
- }
502
- }
503
551
  async function resolveGuardedWritePathInRoot(root, params) {
504
552
  const resolvedPath = await resolvePathInRoot(root, params.relativePath, {
505
553
  aliasErrorCode: "path-alias",
554
+ rejectAmbiguousParents: true,
506
555
  allowFinalSymlink: params.allowFinalSymlink,
556
+ ...mutationSymlinkResolution(params.mutationSymlinks),
507
557
  });
508
558
  await assertMutationNotDenied(resolvedPath.resolved, params.denyMutations, params.protectDeniedAncestors ? { protectAncestors: true } : undefined);
509
559
  if (await (params.shouldAssertNoPathAlias?.(resolvedPath) ?? true)) {
@@ -526,12 +576,14 @@ async function openWritableFileInRoot(root, params) {
526
576
  const { rootReal, rootWithSep, resolved } = await resolveGuardedWritePathInRoot(root, {
527
577
  relativePath: params.relativePath,
528
578
  denyMutations: params.denyMutations,
579
+ mutationSymlinks: params.mutationSymlinks,
529
580
  });
530
581
  let ioPath = params.mkdir === false
531
582
  ? resolved
532
- : await prepareRootWriteTarget(rootReal, resolved);
583
+ : await prepareRootWriteTarget(rootReal, resolved, params.assertBeforeMutation);
533
584
  try {
534
- const resolvedRealPath = fsSync.realpathSync.native(ioPath);
585
+ assertFinalSymlinkRejected(ioPath, params.mutationSymlinks !== undefined);
586
+ const resolvedRealPath = params.mutationSymlinks === undefined ? realpathSync.native(ioPath) : ioPath;
535
587
  if (!isPathInside(rootWithSep, resolvedRealPath)) {
536
588
  throw outsideWorkspaceError();
537
589
  }
@@ -561,6 +613,7 @@ async function openWritableFileInRoot(root, params) {
561
613
  if (!isNotFoundPathError(err)) {
562
614
  throw err;
563
615
  }
616
+ params.assertBeforeMutation?.();
564
617
  handle = await fs.open(ioPath, createFlags, mode);
565
618
  createdForWrite = true;
566
619
  }
@@ -619,6 +672,8 @@ async function openWritableFileInRoot(root, params) {
619
672
  // Truncate only after boundary and identity checks complete. This avoids
620
673
  // irreversible side effects if a symlink target changes before validation.
621
674
  if (params.append !== true && params.truncateExisting !== false && !createdForWrite) {
675
+ assertFinalSymlinkRejected(ioPath, params.mutationSymlinks !== undefined);
676
+ params.assertBeforeMutation?.();
622
677
  await handle.truncate(0);
623
678
  }
624
679
  return {
@@ -641,42 +696,72 @@ async function openWritableFileInRoot(root, params) {
641
696
  }
642
697
  }
643
698
  async function appendFileInRoot(root, params) {
644
- const target = await openWritableFileInRoot(root, {
645
- relativePath: params.relativePath,
646
- mkdir: params.mkdir,
647
- mode: params.mode,
648
- denyMutations: params.denyMutations,
649
- truncateExisting: false,
650
- append: true,
651
- });
699
+ const env_1 = { stack: [], error: void 0, hasError: false };
652
700
  try {
653
- let prefix = "";
654
- if (params.prependNewlineIfNeeded === true &&
655
- !target.createdForWrite &&
656
- target.stat.size > 0 &&
657
- ((typeof params.data === "string" && !params.data.startsWith("\n")) ||
658
- (Buffer.isBuffer(params.data) && params.data.length > 0 && params.data[0] !== 0x0a))) {
659
- const lastByte = Buffer.alloc(1);
660
- const { bytesRead } = await target.handle.read(lastByte, 0, 1, target.stat.size - 1);
661
- if (bytesRead === 1 && lastByte[0] !== 0x0a) {
662
- prefix = "\n";
701
+ const target = await openWritableFileInRoot(root, {
702
+ relativePath: params.relativePath,
703
+ mkdir: params.mkdir,
704
+ mode: params.mode,
705
+ denyMutations: params.denyMutations,
706
+ assertBeforeMutation: params.assertBeforeMutation,
707
+ mutationSymlinks: params.mutationSymlinks,
708
+ truncateExisting: false,
709
+ append: true,
710
+ });
711
+ let dispatched = false;
712
+ // Reverse disposal order closes the handle before path cleanup and retains both failures.
713
+ const cleanup = __addDisposableResource(env_1, {
714
+ async [Symbol.asyncDispose]() {
715
+ if (!dispatched && target.createdForWrite) {
716
+ await removePathIfIdentityUnchanged(target.realPath, target.stat);
717
+ }
718
+ },
719
+ }, true);
720
+ const handle = __addDisposableResource(env_1, target.handle, true);
721
+ try {
722
+ let prefix = "";
723
+ if (params.prependNewlineIfNeeded === true &&
724
+ !target.createdForWrite &&
725
+ target.stat.size > 0 &&
726
+ ((typeof params.data === "string" && !params.data.startsWith("\n")) ||
727
+ (Buffer.isBuffer(params.data) && params.data.length > 0 && params.data[0] !== 0x0a))) {
728
+ const newline = Buffer.from("\n", typeof params.data === "string" ? params.encoding : "utf8");
729
+ const tail = Buffer.alloc(newline.length);
730
+ const { bytesRead } = await target.handle.read(tail, 0, tail.length, Math.max(0, target.stat.size - tail.length));
731
+ if (bytesRead > 0 && (bytesRead !== newline.length || !tail.equals(newline))) {
732
+ prefix = "\n";
733
+ }
734
+ }
735
+ const payload = typeof params.data === "string" ? `${prefix}${params.data}`
736
+ : prefix.length > 0 ? Buffer.concat([Buffer.from(prefix, "utf8"), params.data]) : params.data;
737
+ await writeAllToFile(target.handle, payload, {
738
+ encoding: params.encoding,
739
+ assertBeforeMutation: () => {
740
+ assertFinalSymlinkRejected(target.realPath, params.mutationSymlinks !== undefined);
741
+ params.assertBeforeMutation?.();
742
+ dispatched = true;
743
+ },
744
+ });
745
+ // A successful empty append still creates the file, as Node's appendFile does.
746
+ dispatched = true;
747
+ if (params.durable !== false)
748
+ await target.handle.sync();
749
+ if (params.durable !== false && target.createdForWrite) {
750
+ await syncDirectoryBestEffort(path.dirname(target.realPath));
663
751
  }
664
752
  }
665
- if (typeof params.data === "string") {
666
- await target.handle.appendFile(`${prefix}${params.data}`, params.encoding ?? "utf8");
667
- }
668
- else {
669
- const payload = prefix.length > 0 ? Buffer.concat([Buffer.from(prefix, "utf8"), params.data]) : params.data;
670
- await target.handle.appendFile(payload);
671
- }
672
- if (params.durable !== false)
673
- await target.handle.sync();
674
- if (params.durable !== false && target.createdForWrite) {
675
- await syncDirectoryBestEffort(path.dirname(target.realPath));
753
+ catch (error) {
754
+ rethrowMutationAuthorityError(error);
676
755
  }
677
756
  }
757
+ catch (e_1) {
758
+ env_1.error = e_1;
759
+ env_1.hasError = true;
760
+ }
678
761
  finally {
679
- await target.handle.close().catch(() => { });
762
+ const result_1 = __disposeResources(env_1);
763
+ if (result_1)
764
+ await result_1;
680
765
  }
681
766
  }
682
767
  async function removePathInRoot(root, params) {
@@ -684,12 +769,15 @@ async function removePathInRoot(root, params) {
684
769
  const resolved = await resolvePinnedPathInRoot(root, {
685
770
  relativePath: params.relativePath,
686
771
  denyMutations: params.denyMutations,
772
+ mutationSymlinks: params.mutationSymlinks,
687
773
  remove: true,
688
774
  });
689
775
  try {
690
- await removePathFallback(resolved);
776
+ await removePathInRootFallback(root, resolved.resolved, params);
691
777
  }
692
778
  catch (error) {
779
+ if (params.recursive)
780
+ throw error;
693
781
  throw normalizePinnedPathError(error);
694
782
  }
695
783
  }
@@ -697,26 +785,27 @@ async function mkdirPathInRoot(root, params) {
697
785
  validatePinnedOperationPayload({ relativePath: params.relativePath });
698
786
  const resolved = await resolvePinnedPathInRoot(root, params);
699
787
  try {
700
- await mkdirPathFallback(resolved);
788
+ await mkdirPathFallback(resolved, params.assertBeforeMutation, params.mutationSymlinks !== undefined);
701
789
  }
702
790
  catch (error) {
703
791
  throw normalizePinnedPathError(error);
704
792
  }
705
793
  }
706
794
  async function writeFileInRoot(root, params) {
795
+ const input = rootWriteInput(params);
707
796
  await serializePathWrite(rootWriteQueueKey(root, params.relativePath), async () => {
708
- if (process.platform === "win32" &&
797
+ if (input.kind === "buffer" && process.platform === "win32" &&
709
798
  (params.renameIdentity === "verify-content-with-lock" || !getNativeBinding())) {
710
- await writeFileFallback(root, params);
799
+ await writeFileFallback(root, { ...params, data: input.data });
711
800
  return;
712
801
  }
713
- const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode, params.denyMutations, params.overwrite);
802
+ const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode, params.denyMutations, params.overwrite, params.mutationSymlinks);
714
803
  await serializePathWrite(pinned.targetPath, async () => {
715
- await commitPinnedWriteInRoot(root, pinned, params);
804
+ await commitPinnedWriteInRoot(root, pinned, params, input);
716
805
  });
717
806
  });
718
807
  }
719
- async function commitPinnedWriteInRoot(root, pinned, params) {
808
+ async function commitPinnedWriteInRoot(root, pinned, params, input) {
720
809
  let verifyingPublication = false;
721
810
  try {
722
811
  await runPinnedWriteWithRenamePolicy({
@@ -729,8 +818,11 @@ async function commitPinnedWriteInRoot(root, pinned, params) {
729
818
  mode: params.mode ?? pinned.mode,
730
819
  sync: params.durable !== false,
731
820
  overwrite: params.overwrite,
732
- input: { kind: "buffer", data: params.data, encoding: params.encoding },
821
+ rejectFinalSymlink: params.mutationSymlinks !== undefined,
822
+ input,
823
+ maxBytes: params.maxBytes,
733
824
  rootIdentity: root.rootIdentity,
825
+ assertBeforeMutation: params.assertBeforeMutation,
734
826
  verifyPublished: async (fd, expectedIdentity, parentGuard) => {
735
827
  verifyingPublication = true;
736
828
  try {
@@ -765,46 +857,75 @@ async function commitPinnedWriteInRoot(root, pinned, params) {
765
857
  }
766
858
  }
767
859
  async function copyFileInRoot(root, params) {
768
- assertValidRootRelativePath(params.relativePath);
769
- assertNoNulPathInput(params.sourcePath, "source path contains a NUL byte");
770
- const { opened: source, identity: sourceIdentity } = await openVerifiedLocalFile(params.sourcePath, {
771
- hardlinks: params.sourceHardlinks,
772
- });
860
+ params.signal?.throwIfAborted();
861
+ const clone = resolveFileCopyCloneMode(params.clone);
862
+ let source;
863
+ let sourceIdentity;
864
+ if (typeof params.source === "string") {
865
+ assertNoNulPathInput(params.source, "source path contains a NUL byte");
866
+ ({ opened: source, identity: sourceIdentity } = await openVerifiedLocalFile(params.source, {
867
+ hardlinks: params.sourceHardlinks,
868
+ }));
869
+ }
870
+ else {
871
+ source = await params.source.root.open(params.source.relativePath, params.sourceHardlinks === undefined ? undefined : { hardlinks: params.sourceHardlinks });
872
+ try {
873
+ sourceIdentity = await inspectFileIdentity(() => fsSync.fstatSync(source.handle.fd, { bigint: true }));
874
+ }
875
+ catch (error) {
876
+ await source.handle.close().catch(() => undefined);
877
+ throw error;
878
+ }
879
+ }
773
880
  if (params.maxBytes !== undefined && source.stat.size > params.maxBytes) {
774
881
  await source.handle.close().catch(() => { });
775
882
  throw new FsSafeError("too-large", `file exceeds limit of ${params.maxBytes} bytes (got ${source.stat.size})`);
776
883
  }
777
884
  try {
778
885
  await serializePathWrite(rootWriteQueueKey(root, params.relativePath), async () => {
779
- const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode, params.denyMutations);
886
+ const pinned = await resolvePinnedWriteTargetInRoot(root, params.relativePath, params.mode ?? (params.preserveSourceMode ? Number(sourceIdentity.mode & 4095n) : undefined), params.denyMutations, params.overwrite !== false, params.mutationSymlinks);
780
887
  await serializePathWrite(pinned.targetPath, async () => {
781
888
  await assertCopySourceCurrent(source, sourceIdentity);
782
- let identity;
889
+ const verifySource = async () => {
890
+ params.signal?.throwIfAborted();
891
+ if (typeof params.source !== "string") {
892
+ await params.source.root.stat(".");
893
+ }
894
+ await assertCopySourceCurrent(source, sourceIdentity);
895
+ };
896
+ const observer = createCopyPublicationObserver(pinned.targetPath, params.onDestinationPublished);
783
897
  try {
784
- identity = await runPinnedWriteHelper({
898
+ await runPinnedWriteHelper({
785
899
  rootPath: pinned.rootReal,
786
900
  relativeParentPath: pinned.relativeParentPath,
787
901
  basename: pinned.basename,
788
902
  mkdir: params.mkdir !== false,
789
903
  mode: pinned.mode,
790
- overwrite: true,
904
+ overwrite: params.overwrite !== false,
905
+ rejectFinalSymlink: params.mutationSymlinks !== undefined,
791
906
  maxBytes: params.maxBytes,
792
907
  sync: params.durable !== false,
908
+ assertBeforeMutation: () => {
909
+ if (params.signal?.aborted)
910
+ throw new MutationAuthorityError(params.signal.reason);
911
+ params.assertBeforeMutation?.();
912
+ },
793
913
  verifyPublished: params.verifyPublished,
794
- input: { kind: "stream", stream: source.handle.createReadStream() },
914
+ onPublished: observer.onPublished,
915
+ input: { kind: "file", handle: source.handle, size: source.stat.size, clone, signal: params.signal, verifySource },
795
916
  rootIdentity: root.rootIdentity,
796
917
  });
797
918
  }
798
919
  catch (error) {
920
+ observer.rethrowObserverFailure(error);
921
+ if (params.signal?.aborted && error === params.signal.reason)
922
+ throw error;
923
+ if (isAlreadyExistsError(error)) {
924
+ throw new FsSafeError("already-exists", "copy destination already exists", { cause: error });
925
+ }
799
926
  throw normalizePinnedWriteError(error);
800
927
  }
801
- try {
802
- await assertCopySourcePathCurrent(source, sourceIdentity);
803
- }
804
- catch (error) {
805
- await removePathIfIdentityUnchanged(pinned.targetPath, identity).catch(() => undefined);
806
- throw error;
807
- }
928
+ await verifySource();
808
929
  });
809
930
  });
810
931
  }
@@ -812,33 +933,20 @@ async function copyFileInRoot(root, params) {
812
933
  await source.handle.close().catch(() => { });
813
934
  }
814
935
  }
815
- async function assertCopySourceCurrent(source, identity) {
816
- await inspectFileIdentity(() => fsSync.fstatSync(source.handle.fd, { bigint: true }), identity);
817
- await assertCopySourcePathCurrent(source, identity);
818
- }
819
- async function assertCopySourcePathCurrent(source, identity) {
820
- await inspectFileIdentity(async () => {
821
- const current = fsSync.lstatSync(source.realPath, { bigint: true });
822
- if (current.isSymbolicLink() || !current.isFile()) {
823
- throw new FsSafeError("path-mismatch", "copy source path changed");
824
- }
825
- return current;
826
- }, identity);
827
- }
828
936
  async function removePathIfIdentityUnchanged(targetPath, identity) {
829
937
  const parentGuard = await createAsyncDirectoryGuard(path.dirname(targetPath));
830
- const current = fsSync.lstatSync(targetPath);
831
- if (current.isSymbolicLink() || !sameFileIdentityForCleanup(current, identity)) {
832
- return;
833
- }
834
938
  await withAsyncDirectoryGuards([parentGuard], async () => {
835
- await fs.rm(targetPath);
939
+ const current = fsSync.lstatSync(targetPath);
940
+ if (current.isSymbolicLink() || !sameFileIdentityForCleanup(current, identity))
941
+ return;
942
+ await fs.unlink(targetPath);
836
943
  });
837
944
  }
838
- async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode, denyMutations, overwrite = true) {
945
+ async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode, denyMutations, overwrite = true, mutationSymlinks) {
839
946
  const { rootReal, rootWithSep, resolved } = await resolveGuardedWritePathInRoot(root, {
840
947
  relativePath,
841
948
  denyMutations,
949
+ mutationSymlinks,
842
950
  });
843
951
  // resolvePathInRoot already enforces isPathInside, so any actual escape
844
952
  // is rejected upstream.
@@ -884,6 +992,7 @@ async function resolvePinnedPathInRoot(root, params) {
884
992
  return await resolvePinnedOperationPathInRoot(root, {
885
993
  allowRoot: params.allowRoot,
886
994
  denyMutations: params.denyMutations,
995
+ mutationSymlinks: params.mutationSymlinks,
887
996
  protectDenyMutationAncestors: params.remove === true,
888
997
  relativePath: params.relativePath,
889
998
  policy: params.remove ? PATH_ALIAS_POLICIES.unlinkTarget : PATH_ALIAS_POLICIES.strict,
@@ -893,6 +1002,7 @@ async function resolvePinnedOperationPathInRoot(root, params) {
893
1002
  const resolved = await resolvePinnedRootPathInRoot(root, {
894
1003
  relativePath: params.relativePath,
895
1004
  policy: params.policy,
1005
+ mutationSymlinks: params.mutationSymlinks,
896
1006
  });
897
1007
  const relativeResolved = path.relative(resolved.rootReal, resolved.canonicalPath);
898
1008
  if ((relativeResolved === "" || relativeResolved === ".") && params.allowRoot === true) {
@@ -929,9 +1039,12 @@ async function resolvePinnedRootPathInRoot(root, params) {
929
1039
  rootCanonicalPath: rootReal,
930
1040
  boundaryLabel: "root",
931
1041
  policy: params.policy,
1042
+ ...mutationSymlinkResolution(params.mutationSymlinks),
932
1043
  });
933
1044
  }
934
1045
  catch (err) {
1046
+ if (err instanceof FsSafeError && err.code === "symlink")
1047
+ throw err;
935
1048
  throw new FsSafeError("path-alias", "path alias escape blocked", { cause: err });
936
1049
  }
937
1050
  const rootWithSep = ensureTrailingSep(resolved.rootCanonicalPath);
@@ -941,30 +1054,10 @@ async function resolvePinnedRootPathInRoot(root, params) {
941
1054
  canonicalPath: resolved.canonicalPath,
942
1055
  };
943
1056
  }
944
- async function prepareRemoveGuard(targetPath) {
945
- try {
946
- const guard = await createAsyncDirectoryGuard(path.dirname(targetPath));
947
- await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("remove", targetPath);
948
- await assertAsyncDirectoryGuard(guard);
949
- return guard;
950
- }
951
- catch (error) {
952
- throw normalizeRemoveGuardError(error);
953
- }
954
- }
955
- async function removePathFallback(resolved) {
956
- const guard = await prepareRemoveGuard(resolved.resolved);
957
- try {
958
- await (fsSync.lstatSync(resolved.resolved).isDirectory() ? fs.rmdir(resolved.resolved) : fs.rm(resolved.resolved));
959
- }
960
- catch (error) {
961
- throw normalizeRemovePathError(error);
962
- }
963
- await assertAsyncDirectoryGuard(guard).catch(() => undefined);
964
- }
965
- async function mkdirPathFallback(resolved) {
1057
+ async function mkdirPathFallback(resolved, assertBeforeMutation, rejectSymlinks = false) {
966
1058
  await mkdirPathComponentsWithGuards({
967
- rootReal: resolved.rootReal, targetPath: resolved.resolved,
1059
+ rootReal: resolved.rootReal, targetPath: resolved.resolved, assertBeforeMutation,
1060
+ rejectSymlinks,
968
1061
  beforeComponent: async (componentPath) => await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("mkdir", componentPath),
969
1062
  });
970
1063
  }
@@ -984,31 +1077,7 @@ async function statPathFallback(root, relativePath) {
984
1077
  }
985
1078
  async function listPathFallback(root, relativePath, withFileTypes) {
986
1079
  const resolved = await resolvePinnedPathInRoot(root, { relativePath, allowRoot: true });
987
- try {
988
- const names = await fs.readdir(resolved.resolved);
989
- const sortedNames = names.toSorted();
990
- if (!withFileTypes) {
991
- await assertRootIdentityCurrent(root);
992
- return sortedNames;
993
- }
994
- const entries = [];
995
- for (const name of sortedNames) {
996
- entries.push({
997
- name,
998
- ...pathStatFromStats(fsSync.lstatSync(path.join(resolved.resolved, name))),
999
- });
1000
- }
1001
- await assertRootIdentityCurrent(root);
1002
- return entries;
1003
- }
1004
- catch (error) {
1005
- if (isNotFoundPathError(error)) {
1006
- throw new FsSafeError("not-found", "directory not found", {
1007
- cause: error instanceof Error ? error : undefined,
1008
- });
1009
- }
1010
- throw error;
1011
- }
1080
+ return await listDirectoryPath(root, resolved.resolved, withFileTypes);
1012
1081
  }
1013
1082
  async function assertMoveMutationAllowed(root, params) {
1014
1083
  // Keep this preflight separate from the pinned resolutions in movePathFallback:
@@ -1028,15 +1097,18 @@ async function movePathFallback(root, params) {
1028
1097
  const source = await resolvePathInRoot(root, params.fromRelative, {
1029
1098
  aliasErrorCode: "path-alias",
1030
1099
  allowFinalSymlink: true,
1100
+ ...mutationSymlinkResolution(params.mutationSymlinks),
1031
1101
  });
1032
1102
  await assertMutationNotDenied(source.resolved, params.denyMutations, { protectAncestors: true });
1033
1103
  await resolvePinnedRootPathInRoot(root, {
1034
1104
  relativePath: params.fromRelative,
1035
1105
  policy: PATH_ALIAS_POLICIES.strict,
1106
+ mutationSymlinks: params.mutationSymlinks,
1036
1107
  });
1037
1108
  const target = await resolveGuardedWritePathInRoot(root, {
1038
1109
  relativePath: params.toRelative,
1039
1110
  denyMutations: params.denyMutations,
1111
+ mutationSymlinks: params.mutationSymlinks,
1040
1112
  allowFinalSymlink: true,
1041
1113
  protectDeniedAncestors: true,
1042
1114
  shouldAssertNoPathAlias: async (resolvedTarget) => {
@@ -1093,6 +1165,9 @@ async function movePathFallback(root, params) {
1093
1165
  await assertAsyncDirectoryGuard(sourceParentGuard);
1094
1166
  await assertAsyncDirectoryGuard(targetParentGuard);
1095
1167
  try {
1168
+ assertFinalSymlinkRejected(source.resolved, params.mutationSymlinks !== undefined);
1169
+ assertFinalSymlinkRejected(target.resolved, params.mutationSymlinks !== undefined);
1170
+ params.assertBeforeMutation?.();
1096
1171
  await fs.rename(source.resolved, target.resolved);
1097
1172
  }
1098
1173
  catch (error) {
@@ -1106,7 +1181,13 @@ async function movePathFallback(root, params) {
1106
1181
  }
1107
1182
  throw error;
1108
1183
  }
1109
- await assertAsyncDirectoryGuard(targetParentGuard).catch(() => undefined);
1184
+ try {
1185
+ await assertAsyncDirectoryGuard(sourceParentGuard);
1186
+ await assertAsyncDirectoryGuard(targetParentGuard);
1187
+ }
1188
+ catch (error) {
1189
+ throw normalizePinnedPathError(error);
1190
+ }
1110
1191
  }
1111
1192
  async function writeFileFallback(root, params) {
1112
1193
  if (params.overwrite === false) {
@@ -1119,47 +1200,76 @@ async function writeFileFallback(root, params) {
1119
1200
  // Private, writable placeholder: Windows cannot rename over a read-only file.
1120
1201
  mode: 0o600,
1121
1202
  denyMutations: params.denyMutations,
1203
+ assertBeforeMutation: params.assertBeforeMutation,
1204
+ mutationSymlinks: params.mutationSymlinks,
1122
1205
  truncateExisting: false,
1123
1206
  });
1124
1207
  const destinationPath = target.realPath;
1125
1208
  const mode = params.mode ?? (target.stat.mode & 0o777);
1126
- await target.handle.close().catch(() => { });
1127
- const destinationGuard = await createAsyncDirectoryGuard(path.dirname(destinationPath));
1209
+ const destinationGuard = await createAsyncDirectoryGuard(path.dirname(destinationPath), { bigint: true }).catch(async (error) => {
1210
+ await target.handle.close().catch(() => undefined);
1211
+ throw error;
1212
+ });
1128
1213
  let tempPath = null;
1129
1214
  let unregisterTempPath = null;
1130
1215
  let writtenHandle;
1216
+ let writtenIdentity;
1217
+ let placeholderIdentity;
1218
+ let published = false;
1131
1219
  try {
1220
+ if (target.createdForWrite)
1221
+ placeholderIdentity = fsSync.fstatSync(target.handle.fd, { bigint: true });
1132
1222
  tempPath = buildAtomicWriteTempPath(destinationPath);
1133
- unregisterTempPath = registerTempPathForExit(tempPath);
1134
- const written = await writeTempFileForAtomicReplace({
1135
- tempPath,
1136
- data: params.data,
1137
- encoding: params.encoding,
1138
- mode: 0o600,
1223
+ params.assertBeforeMutation?.();
1224
+ writtenHandle = await fs.open(tempPath, OPEN_WRITE_CREATE_FLAGS, 0o600);
1225
+ writtenIdentity = fsSync.fstatSync(writtenHandle.fd, { bigint: true });
1226
+ unregisterTempPath = registerTempPathForExit(tempPath, { identity: writtenIdentity, singleLinkFile: true });
1227
+ await writeAllToFile(writtenHandle, params.data, {
1228
+ encoding: params.encoding, assertBeforeMutation: params.assertBeforeMutation,
1139
1229
  });
1140
- writtenHandle = written.handle;
1141
- unregisterTempPath.setIdentity(written.identity);
1230
+ if (params.durable !== false)
1231
+ await writtenHandle.sync();
1142
1232
  const commitTempPath = tempPath;
1233
+ const commitHandle = writtenHandle;
1234
+ const commitIdentity = writtenIdentity;
1143
1235
  await withAsyncDirectoryGuards([destinationGuard], async () => {
1236
+ await verifyAtomicWriteResult({
1237
+ root, targetPath: commitTempPath, fd: commitHandle.fd,
1238
+ expectedIdentity: commitIdentity, parentGuard: destinationGuard,
1239
+ });
1240
+ if (target.createdForWrite) {
1241
+ await cleanupPinnedFilePath({
1242
+ pathname: destinationPath, handle: target.handle, identity: placeholderIdentity, parentGuard: destinationGuard,
1243
+ });
1244
+ }
1245
+ // Windows cannot replace a destination while its old handle remains open.
1246
+ await target.handle.close();
1247
+ assertFinalSymlinkRejected(destinationPath, params.mutationSymlinks !== undefined);
1248
+ params.assertBeforeMutation?.();
1144
1249
  await fs.rename(commitTempPath, destinationPath);
1145
1250
  tempPath = null;
1251
+ published = true;
1146
1252
  });
1147
1253
  unregisterTempPath();
1148
1254
  unregisterTempPath = null;
1149
1255
  // Final mode via the retained handle after publication, as the native writer does.
1150
1256
  try {
1151
- await written.handle.chmod(mode);
1257
+ await writtenHandle.chmod(mode);
1152
1258
  }
1153
1259
  catch (error) {
1154
- await removePathIfIdentityUnchanged(destinationPath, written.identity).catch(() => { });
1260
+ await cleanupPinnedFilePath({
1261
+ pathname: destinationPath, handle: writtenHandle, identity: writtenIdentity, parentGuard: destinationGuard,
1262
+ });
1155
1263
  throw error;
1156
1264
  }
1265
+ if (params.durable !== false)
1266
+ await writtenHandle.sync();
1157
1267
  try {
1158
1268
  await verifyAtomicWriteResult({
1159
1269
  root,
1160
1270
  targetPath: destinationPath,
1161
- expectedIdentity: written.identity,
1162
- fd: written.handle.fd,
1271
+ expectedIdentity: writtenIdentity,
1272
+ fd: writtenHandle.fd,
1163
1273
  parentGuard: destinationGuard,
1164
1274
  });
1165
1275
  }
@@ -1167,12 +1277,22 @@ async function writeFileFallback(root, params) {
1167
1277
  emitWriteBoundaryWarning(`post-write verification failed: ${String(err)}`);
1168
1278
  throw err;
1169
1279
  }
1280
+ if (params.durable !== false)
1281
+ await syncDirectoryBestEffort(path.dirname(destinationPath));
1170
1282
  }
1171
1283
  finally {
1172
- await writtenHandle?.close().catch(() => undefined);
1173
- if (tempPath) {
1174
- await fs.rm(tempPath, { force: true }).catch(() => { });
1284
+ if (!published && target.createdForWrite) {
1285
+ await cleanupPinnedFilePath({
1286
+ pathname: destinationPath, handle: target.handle, identity: placeholderIdentity, parentGuard: destinationGuard,
1287
+ });
1288
+ }
1289
+ await target.handle.close().catch(() => undefined);
1290
+ if (tempPath && writtenHandle) {
1291
+ await cleanupPinnedFilePath({
1292
+ pathname: tempPath, handle: writtenHandle, identity: writtenIdentity, parentGuard: destinationGuard,
1293
+ });
1175
1294
  }
1295
+ await writtenHandle?.close().catch(() => undefined);
1176
1296
  unregisterTempPath?.();
1177
1297
  }
1178
1298
  }
@@ -1180,38 +1300,38 @@ async function writeMissingFileFallback(root, params) {
1180
1300
  const { rootReal, resolved } = await resolveGuardedWritePathInRoot(root, {
1181
1301
  relativePath: params.relativePath,
1182
1302
  denyMutations: params.denyMutations,
1303
+ mutationSymlinks: params.mutationSymlinks,
1183
1304
  });
1184
1305
  const targetPath = params.mkdir === false
1185
1306
  ? resolved
1186
- : await prepareRootWriteTarget(rootReal, resolved);
1187
- const parentGuard = await createAsyncDirectoryGuard(path.dirname(targetPath));
1307
+ : await prepareRootWriteTarget(rootReal, resolved, params.assertBeforeMutation);
1308
+ const parentGuard = await createAsyncDirectoryGuard(path.dirname(targetPath), { bigint: true });
1188
1309
  let created = false;
1189
1310
  let createdIdentity;
1190
1311
  let writtenHandle;
1191
1312
  let verifyingPublication = false;
1192
1313
  try {
1193
1314
  const { handle, writtenStat } = await withAsyncDirectoryGuards([parentGuard], async () => {
1315
+ assertFinalSymlinkRejected(targetPath, params.mutationSymlinks !== undefined);
1316
+ params.assertBeforeMutation?.();
1194
1317
  const handle = await fs.open(targetPath, OPEN_WRITE_CREATE_FLAGS, params.mode ?? 0o600).catch((error) => recordExclusiveCreateFailure(error, targetPath));
1318
+ writtenHandle = handle;
1195
1319
  created = true;
1196
- try {
1197
- createdIdentity = fsSync.fstatSync(handle.fd);
1198
- const writtenStat = fsSync.fstatSync(handle.fd, { bigint: true });
1199
- if (typeof params.data === "string") {
1200
- await handle.writeFile(params.data, params.encoding ?? "utf8");
1201
- }
1202
- else {
1203
- await handle.writeFile(params.data);
1204
- }
1205
- return { handle, writtenStat };
1206
- }
1207
- catch (error) {
1208
- await handle.close().catch(() => undefined);
1209
- throw error;
1210
- }
1320
+ const writtenStat = fsSync.fstatSync(handle.fd, { bigint: true });
1321
+ createdIdentity = writtenStat;
1322
+ await writeAllToFile(handle, params.data, {
1323
+ encoding: params.encoding,
1324
+ assertBeforeMutation: () => {
1325
+ assertFinalSymlinkRejected(targetPath, params.mutationSymlinks !== undefined);
1326
+ params.assertBeforeMutation?.();
1327
+ },
1328
+ });
1329
+ if (params.durable !== false)
1330
+ await handle.sync();
1331
+ return { handle, writtenStat };
1211
1332
  }, {
1212
- onPostGuardFailure: async ({ handle }) => {
1333
+ onPostGuardFailure: () => {
1213
1334
  created = false; // Parent is untrusted now; skip outer path cleanup by name.
1214
- await handle.close().catch(() => undefined);
1215
1335
  },
1216
1336
  });
1217
1337
  writtenHandle = handle;
@@ -1224,6 +1344,8 @@ async function writeMissingFileFallback(root, params) {
1224
1344
  fd: handle.fd,
1225
1345
  parentGuard,
1226
1346
  });
1347
+ if (params.durable !== false)
1348
+ await syncDirectoryBestEffort(path.dirname(targetPath));
1227
1349
  }
1228
1350
  catch (err) {
1229
1351
  if (verifyingPublication)
@@ -1236,9 +1358,11 @@ async function writeMissingFileFallback(root, params) {
1236
1358
  throw err;
1237
1359
  }
1238
1360
  finally {
1239
- await writtenHandle?.close().catch(() => undefined);
1240
- if (created && createdIdentity) {
1241
- await removePathIfIdentityUnchanged(targetPath, createdIdentity).catch(() => undefined);
1361
+ if (created && writtenHandle) {
1362
+ await cleanupPinnedFilePath({
1363
+ pathname: targetPath, handle: writtenHandle, identity: createdIdentity, parentGuard,
1364
+ });
1242
1365
  }
1366
+ await writtenHandle?.close().catch(() => undefined);
1243
1367
  }
1244
1368
  }