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