@openclaw/fs-safe 0.9.0 → 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 (178) hide show
  1. package/CHANGELOG.md +78 -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-gzip-tail.d.ts +2 -0
  11. package/dist/archive-gzip-tail.d.ts.map +1 -1
  12. package/dist/archive-gzip-tail.js +20 -3
  13. package/dist/archive-merge.js +1 -1
  14. package/dist/archive-native.d.ts.map +1 -1
  15. package/dist/archive-native.js +3 -2
  16. package/dist/archive-read.d.ts.map +1 -1
  17. package/dist/archive-read.js +67 -66
  18. package/dist/archive-tar-stream.d.ts +11 -4
  19. package/dist/archive-tar-stream.d.ts.map +1 -1
  20. package/dist/archive-tar-stream.js +20 -8
  21. package/dist/archive-tar-wasm.d.ts.map +1 -1
  22. package/dist/archive-tar-wasm.js +2 -1
  23. package/dist/bounded-read.d.ts +7 -0
  24. package/dist/bounded-read.d.ts.map +1 -1
  25. package/dist/bounded-read.js +73 -43
  26. package/dist/clone-metadata.d.ts +19 -0
  27. package/dist/clone-metadata.d.ts.map +1 -0
  28. package/dist/clone-metadata.js +32 -0
  29. package/dist/copy-file-input.d.ts +23 -0
  30. package/dist/copy-file-input.d.ts.map +1 -0
  31. package/dist/copy-file-input.js +91 -0
  32. package/dist/copy-policy.d.ts +3 -0
  33. package/dist/copy-policy.d.ts.map +1 -0
  34. package/dist/copy-policy.js +8 -0
  35. package/dist/copy-publication.d.ts +10 -1
  36. package/dist/copy-publication.d.ts.map +1 -1
  37. package/dist/copy-publication.js +27 -0
  38. package/dist/copy-tree-portable.d.ts +9 -0
  39. package/dist/copy-tree-portable.d.ts.map +1 -0
  40. package/dist/copy-tree-portable.js +191 -0
  41. package/dist/copy.d.ts +16 -0
  42. package/dist/copy.d.ts.map +1 -0
  43. package/dist/copy.js +123 -0
  44. package/dist/durability.d.ts +1 -1
  45. package/dist/durability.d.ts.map +1 -1
  46. package/dist/error-detail.d.ts.map +1 -1
  47. package/dist/error-detail.js +4 -1
  48. package/dist/file-hash.d.ts +6 -2
  49. package/dist/file-hash.d.ts.map +1 -1
  50. package/dist/file-hash.js +44 -8
  51. package/dist/filename.d.ts.map +1 -1
  52. package/dist/filename.js +2 -12
  53. package/dist/guarded-mkdir.d.ts +1 -0
  54. package/dist/guarded-mkdir.d.ts.map +1 -1
  55. package/dist/guarded-mkdir.js +1 -0
  56. package/dist/guarded-mutation.d.ts +2 -0
  57. package/dist/guarded-mutation.d.ts.map +1 -1
  58. package/dist/guarded-mutation.js +8 -4
  59. package/dist/home-dir.d.ts.map +1 -1
  60. package/dist/home-dir.js +9 -7
  61. package/dist/index.d.ts +1 -1
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/install-path.d.ts.map +1 -1
  64. package/dist/install-path.js +2 -6
  65. package/dist/json-durable-queue.d.ts.map +1 -1
  66. package/dist/json-durable-queue.js +13 -14
  67. package/dist/local-roots.d.ts.map +1 -1
  68. package/dist/local-roots.js +22 -24
  69. package/dist/move-path-stage.d.ts +8 -0
  70. package/dist/move-path-stage.d.ts.map +1 -0
  71. package/dist/move-path-stage.js +55 -0
  72. package/dist/move-path.d.ts.map +1 -1
  73. package/dist/move-path.js +26 -31
  74. package/dist/mutation-authority.d.ts +8 -0
  75. package/dist/mutation-authority.d.ts.map +1 -0
  76. package/dist/mutation-authority.js +36 -0
  77. package/dist/native-binding.d.ts +24 -1
  78. package/dist/native-binding.d.ts.map +1 -1
  79. package/dist/native-operations.d.ts +1 -1
  80. package/dist/native-operations.d.ts.map +1 -1
  81. package/dist/native-operations.js +11 -5
  82. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  83. package/dist/native-pinned-write-windows.js +18 -7
  84. package/dist/native-pinned-write.d.ts.map +1 -1
  85. package/dist/native-pinned-write.js +1 -0
  86. package/dist/native-staged-file.d.ts +3 -3
  87. package/dist/native-staged-file.d.ts.map +1 -1
  88. package/dist/native-staged-file.js +35 -8
  89. package/dist/path.d.ts.map +1 -1
  90. package/dist/path.js +3 -1
  91. package/dist/permissions-windows.d.ts.map +1 -1
  92. package/dist/permissions-windows.js +38 -48
  93. package/dist/pinned-operation.js +1 -1
  94. package/dist/pinned-write.d.ts +5 -1
  95. package/dist/pinned-write.d.ts.map +1 -1
  96. package/dist/pinned-write.js +50 -30
  97. package/dist/positional-read.d.ts +9 -0
  98. package/dist/positional-read.d.ts.map +1 -0
  99. package/dist/positional-read.js +36 -0
  100. package/dist/publish-copy-stage.d.ts +13 -0
  101. package/dist/publish-copy-stage.d.ts.map +1 -0
  102. package/dist/publish-copy-stage.js +47 -0
  103. package/dist/publish-file.d.ts.map +1 -1
  104. package/dist/publish-file.js +6 -5
  105. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  106. package/dist/replace-file-temp-owner.js +6 -2
  107. package/dist/root-context.d.ts +3 -0
  108. package/dist/root-context.d.ts.map +1 -1
  109. package/dist/root-context.js +51 -26
  110. package/dist/root-directory-list.d.ts +24 -0
  111. package/dist/root-directory-list.d.ts.map +1 -0
  112. package/dist/root-directory-list.js +201 -0
  113. package/dist/root-errors.d.ts +1 -0
  114. package/dist/root-errors.d.ts.map +1 -1
  115. package/dist/root-errors.js +3 -0
  116. package/dist/root-file.d.ts +2 -0
  117. package/dist/root-file.d.ts.map +1 -1
  118. package/dist/root-file.js +12 -4
  119. package/dist/root-impl.d.ts +6 -50
  120. package/dist/root-impl.d.ts.map +1 -1
  121. package/dist/root-impl.js +330 -210
  122. package/dist/root-options.d.ts +66 -0
  123. package/dist/root-options.d.ts.map +1 -0
  124. package/dist/root-options.js +18 -0
  125. package/dist/root-path-existing.d.ts +5 -0
  126. package/dist/root-path-existing.d.ts.map +1 -1
  127. package/dist/root-path-existing.js +56 -1
  128. package/dist/root-path.d.ts +1 -0
  129. package/dist/root-path.d.ts.map +1 -1
  130. package/dist/root-path.js +74 -162
  131. package/dist/root-symlink-policy.d.ts +13 -0
  132. package/dist/root-symlink-policy.d.ts.map +1 -0
  133. package/dist/root-symlink-policy.js +34 -0
  134. package/dist/root-walk.d.ts +5 -4
  135. package/dist/root-walk.d.ts.map +1 -1
  136. package/dist/root-walk.js +99 -50
  137. package/dist/root-write-mode.d.ts.map +1 -1
  138. package/dist/root-write-mode.js +1 -4
  139. package/dist/root.d.ts +3 -1
  140. package/dist/root.d.ts.map +1 -1
  141. package/dist/secure-file.d.ts.map +1 -1
  142. package/dist/secure-file.js +4 -4
  143. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  144. package/dist/sidecar-lock-acquire.js +2 -1
  145. package/dist/sidecar-lock-policy.d.ts.map +1 -1
  146. package/dist/sidecar-lock-policy.js +3 -1
  147. package/dist/temp-cleanup.d.ts.map +1 -1
  148. package/dist/temp-cleanup.js +3 -2
  149. package/dist/timing.d.ts +1 -0
  150. package/dist/timing.d.ts.map +1 -1
  151. package/dist/timing.js +25 -6
  152. package/dist/trash.js +2 -2
  153. package/dist/walk.d.ts.map +1 -1
  154. package/dist/walk.js +6 -26
  155. package/dist/windows-owner.d.ts +9 -12
  156. package/dist/windows-owner.d.ts.map +1 -1
  157. package/dist/windows-owner.js +24 -58
  158. package/dist/write-file-handle.d.ts +6 -0
  159. package/dist/write-file-handle.d.ts.map +1 -0
  160. package/dist/write-file-handle.js +25 -0
  161. package/docs/advanced.md +18 -4
  162. package/docs/archive.md +22 -7
  163. package/docs/atomic.md +7 -0
  164. package/docs/contributing.md +7 -0
  165. package/docs/copy.md +86 -0
  166. package/docs/durability.md +17 -2
  167. package/docs/errors.md +1 -1
  168. package/docs/local-roots.md +8 -1
  169. package/docs/native.md +14 -1
  170. package/docs/permissions.md +20 -10
  171. package/docs/positional-read.md +63 -0
  172. package/docs/root.md +166 -4
  173. package/docs/security-model.md +14 -0
  174. package/docs/timing.md +2 -0
  175. package/docs/types.md +18 -11
  176. package/docs/walk.md +54 -5
  177. package/docs/writing.md +20 -2
  178. package/package.json +13 -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,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,7 +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";
34
- import { onCopyPublication } from "./copy-publication.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";
35
95
  export { resolveOpenedFileRealPathForHandle } from "./opened-realpath.js";
36
96
  function logWarn(message) {
37
97
  if (process.env.FS_SAFE_DEBUG_WARNINGS === "1") {
@@ -54,22 +114,6 @@ const OPEN_APPEND_CREATE_FLAGS = fsConstants.O_RDWR |
54
114
  fsConstants.O_CREAT |
55
115
  fsConstants.O_EXCL |
56
116
  (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
117
  function openResult(params) {
74
118
  return {
75
119
  handle: params.handle,
@@ -215,12 +259,14 @@ export class RootHandle {
215
259
  mutationOptions(options) {
216
260
  return {
217
261
  ...options,
262
+ assertBeforeMutation: composeMutationAssertions(this.defaults.assertBeforeMutation, options.assertBeforeMutation),
263
+ mutationSymlinks: options.mutationSymlinks ?? this.defaults.mutationSymlinks,
218
264
  denyMutations: mergeDenyMutationPolicies(this.defaults.denyMutations, options.denyMutations),
219
265
  };
220
266
  }
221
267
  async resolve(relativePath) {
222
268
  assertValidRootDestinationPath(relativePath);
223
- return (await resolvePathInRoot(this.context, relativePath, { allowFinalSymlink: true })).resolved;
269
+ return (await resolvePathInRoot(this.context, relativePath, { allowFinalSymlink: true, resolveCanonical: true })).resolved;
224
270
  }
225
271
  async open(relativePath, options = {}) {
226
272
  return await openFileInRoot(this.context, {
@@ -266,7 +312,7 @@ export class RootHandle {
266
312
  ...this.mutationOptions(options),
267
313
  append: writeMode === "append",
268
314
  truncateExisting: writeMode === "replace",
269
- });
315
+ }).catch(rethrowMutationAuthorityError);
270
316
  }
271
317
  async append(relativePath, data, options = {}) {
272
318
  assertValidRootDestinationPath(relativePath);
@@ -277,28 +323,28 @@ export class RootHandle {
277
323
  mode: this.defaults.mode,
278
324
  ...this.mutationOptions(options),
279
325
  durable: options.durable ?? this.defaults.durable ?? true,
280
- });
326
+ }).catch(rethrowMutationAuthorityError);
281
327
  }
282
328
  async remove(relativePath, options = {}) {
283
329
  assertValidRootRelativePath(relativePath);
284
330
  await removePathInRoot(this.context, {
285
331
  relativePath,
286
332
  ...this.mutationOptions(options),
287
- });
333
+ }).catch(rethrowMutationAuthorityError);
288
334
  }
289
335
  async mkdir(relativePath, options = {}) {
290
336
  assertValidRootDestinationPath(relativePath);
291
337
  await mkdirPathInRoot(this.context, {
292
338
  relativePath,
293
339
  ...this.mutationOptions(options),
294
- });
340
+ }).catch(rethrowMutationAuthorityError);
295
341
  }
296
342
  async ensureRoot(options = {}) {
297
343
  await mkdirPathInRoot(this.context, {
298
344
  relativePath: "",
299
345
  allowRoot: true,
300
346
  ...this.mutationOptions(options),
301
- });
347
+ }).catch(rethrowMutationAuthorityError);
302
348
  }
303
349
  async write(relativePath, data, options = {}) {
304
350
  assertValidRootDestinationPath(relativePath);
@@ -310,7 +356,7 @@ export class RootHandle {
310
356
  renameIdentity: this.defaults.renameIdentity,
311
357
  ...this.mutationOptions(options),
312
358
  durable: options.durable ?? this.defaults.durable ?? true,
313
- });
359
+ }).catch(rethrowMutationAuthorityError);
314
360
  }
315
361
  async create(relativePath, data, options = {}) {
316
362
  assertValidRootDestinationPath(relativePath);
@@ -322,7 +368,7 @@ export class RootHandle {
322
368
  ...this.mutationOptions(options),
323
369
  durable: options.durable ?? this.defaults.durable ?? true,
324
370
  overwrite: false,
325
- });
371
+ }).catch(rethrowMutationAuthorityError);
326
372
  }
327
373
  async writeJson(relativePath, data, options = {}) {
328
374
  const { replacer, space, trailingNewline = true, ...writeOptions } = options;
@@ -334,19 +380,20 @@ export class RootHandle {
334
380
  const json = stringifyJsonDocument(data, replacer, space);
335
381
  await this.create(relativePath, trailingNewline ? `${json}\n` : json, writeOptions);
336
382
  }
337
- async copyIn(relativePath, sourcePath, options = {}) {
383
+ async copyIn(relativePath, source, options = {}) {
384
+ options.signal?.throwIfAborted();
338
385
  assertValidRootDestinationPath(relativePath);
339
386
  const { maxBytes, ...copyOptions } = this.mutationOptions(options);
340
387
  await copyFileInRoot(this.context, {
341
- sourcePath,
388
+ source: typeof source === "string" ? source : { root: source.root, relativePath: source.relativePath },
342
389
  relativePath,
343
390
  maxBytes: normalizeMaxBytes(maxBytes, { defaultValue: this.defaults.maxBytes }),
344
391
  mkdir: this.defaults.mkdir,
345
- mode: this.defaults.mode,
346
392
  ...copyOptions,
393
+ mode: options.mode ?? this.defaults.mode,
347
394
  durable: options.durable ?? this.defaults.durable ?? true,
348
395
  verifyPublished: options[onCopyPublication],
349
- });
396
+ }).catch(rethrowMutationAuthorityError);
350
397
  }
351
398
  async exists(relativePath) {
352
399
  try {
@@ -374,7 +421,7 @@ export class RootHandle {
374
421
  assertValidRootRelativePath(fromRelative);
375
422
  assertValidRootDestinationPath(toRelative);
376
423
  validatePinnedOperationPayload({ from: fromRelative, to: toRelative });
377
- const { denyMutations } = this.mutationOptions(options);
424
+ const { denyMutations, assertBeforeMutation, mutationSymlinks } = this.mutationOptions(options);
378
425
  await assertMoveMutationAllowed(this.context, {
379
426
  fromRelative,
380
427
  toRelative,
@@ -383,31 +430,25 @@ export class RootHandle {
383
430
  await movePathFallback(this.context, {
384
431
  fromRelative,
385
432
  denyMutations,
433
+ assertBeforeMutation,
434
+ mutationSymlinks,
386
435
  overwrite: options.overwrite ?? false,
387
436
  toRelative,
388
- });
437
+ }).catch(rethrowMutationAuthorityError);
389
438
  }
390
439
  walk(relativePath, options) {
391
440
  assertValidRootRelativePath(relativePath);
392
- 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);
393
450
  }
394
451
  }
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
452
  export async function root(rootDir, defaults = {}) {
412
453
  normalizeMaxBytes(defaults.maxBytes);
413
454
  return new RootHandle(await resolveRootContext(rootDir), defaults);
@@ -416,7 +457,8 @@ async function openFileInRoot(root, params) {
416
457
  const { rootWithSep, resolved } = await resolvePathInRoot(root, params.relativePath, {
417
458
  allowFinalSymlink: true,
418
459
  rejectUnsafeDeviceReads: true,
419
- rejectSymlinks: params.symlinks !== "follow-within-root",
460
+ ...readSymlinkResolution(params.symlinks),
461
+ resolveCanonical: true,
420
462
  });
421
463
  const { opened } = await openVerifiedLocalFile(resolved, {
422
464
  symlinks: params.symlinks,
@@ -472,38 +514,22 @@ function buildAtomicWriteTempPath(targetPath) {
472
514
  function rootWriteQueueKey(root, relativePath) {
473
515
  return `${root.rootReal}\0${relativePath}`;
474
516
  }
475
- async function prepareRootWriteTarget(rootReal, targetPath) {
517
+ async function prepareRootWriteTarget(rootReal, targetPath, assertBeforeMutation) {
476
518
  const parentPath = await mkdirPathComponentsWithGuards({
477
519
  rootReal,
478
520
  targetPath: path.dirname(targetPath),
521
+ assertBeforeMutation,
479
522
  });
480
523
  // Continue through the guarded walk's real parent instead of re-entering
481
524
  // the original path through a symlinked component.
482
525
  return path.join(parentPath, path.basename(targetPath));
483
526
  }
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
527
  async function resolveGuardedWritePathInRoot(root, params) {
504
528
  const resolvedPath = await resolvePathInRoot(root, params.relativePath, {
505
529
  aliasErrorCode: "path-alias",
530
+ rejectAmbiguousParents: true,
506
531
  allowFinalSymlink: params.allowFinalSymlink,
532
+ ...mutationSymlinkResolution(params.mutationSymlinks),
507
533
  });
508
534
  await assertMutationNotDenied(resolvedPath.resolved, params.denyMutations, params.protectDeniedAncestors ? { protectAncestors: true } : undefined);
509
535
  if (await (params.shouldAssertNoPathAlias?.(resolvedPath) ?? true)) {
@@ -526,12 +552,14 @@ async function openWritableFileInRoot(root, params) {
526
552
  const { rootReal, rootWithSep, resolved } = await resolveGuardedWritePathInRoot(root, {
527
553
  relativePath: params.relativePath,
528
554
  denyMutations: params.denyMutations,
555
+ mutationSymlinks: params.mutationSymlinks,
529
556
  });
530
557
  let ioPath = params.mkdir === false
531
558
  ? resolved
532
- : await prepareRootWriteTarget(rootReal, resolved);
559
+ : await prepareRootWriteTarget(rootReal, resolved, params.assertBeforeMutation);
533
560
  try {
534
- const resolvedRealPath = fsSync.realpathSync.native(ioPath);
561
+ assertFinalSymlinkRejected(ioPath, params.mutationSymlinks !== undefined);
562
+ const resolvedRealPath = params.mutationSymlinks === undefined ? fsSync.realpathSync.native(ioPath) : ioPath;
535
563
  if (!isPathInside(rootWithSep, resolvedRealPath)) {
536
564
  throw outsideWorkspaceError();
537
565
  }
@@ -561,6 +589,7 @@ async function openWritableFileInRoot(root, params) {
561
589
  if (!isNotFoundPathError(err)) {
562
590
  throw err;
563
591
  }
592
+ params.assertBeforeMutation?.();
564
593
  handle = await fs.open(ioPath, createFlags, mode);
565
594
  createdForWrite = true;
566
595
  }
@@ -619,6 +648,8 @@ async function openWritableFileInRoot(root, params) {
619
648
  // Truncate only after boundary and identity checks complete. This avoids
620
649
  // irreversible side effects if a symlink target changes before validation.
621
650
  if (params.append !== true && params.truncateExisting !== false && !createdForWrite) {
651
+ assertFinalSymlinkRejected(ioPath, params.mutationSymlinks !== undefined);
652
+ params.assertBeforeMutation?.();
622
653
  await handle.truncate(0);
623
654
  }
624
655
  return {
@@ -641,42 +672,72 @@ async function openWritableFileInRoot(root, params) {
641
672
  }
642
673
  }
643
674
  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
- });
675
+ const env_1 = { stack: [], error: void 0, hasError: false };
652
676
  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";
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));
663
727
  }
664
728
  }
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));
729
+ catch (error) {
730
+ rethrowMutationAuthorityError(error);
676
731
  }
677
732
  }
733
+ catch (e_1) {
734
+ env_1.error = e_1;
735
+ env_1.hasError = true;
736
+ }
678
737
  finally {
679
- await target.handle.close().catch(() => { });
738
+ const result_1 = __disposeResources(env_1);
739
+ if (result_1)
740
+ await result_1;
680
741
  }
681
742
  }
682
743
  async function removePathInRoot(root, params) {
@@ -684,10 +745,11 @@ async function removePathInRoot(root, params) {
684
745
  const resolved = await resolvePinnedPathInRoot(root, {
685
746
  relativePath: params.relativePath,
686
747
  denyMutations: params.denyMutations,
748
+ mutationSymlinks: params.mutationSymlinks,
687
749
  remove: true,
688
750
  });
689
751
  try {
690
- await removePathFallback(resolved);
752
+ await removePathFallback(resolved, params.assertBeforeMutation, params.mutationSymlinks !== undefined);
691
753
  }
692
754
  catch (error) {
693
755
  throw normalizePinnedPathError(error);
@@ -697,7 +759,7 @@ async function mkdirPathInRoot(root, params) {
697
759
  validatePinnedOperationPayload({ relativePath: params.relativePath });
698
760
  const resolved = await resolvePinnedPathInRoot(root, params);
699
761
  try {
700
- await mkdirPathFallback(resolved);
762
+ await mkdirPathFallback(resolved, params.assertBeforeMutation, params.mutationSymlinks !== undefined);
701
763
  }
702
764
  catch (error) {
703
765
  throw normalizePinnedPathError(error);
@@ -710,7 +772,7 @@ async function writeFileInRoot(root, params) {
710
772
  await writeFileFallback(root, params);
711
773
  return;
712
774
  }
713
- 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);
714
776
  await serializePathWrite(pinned.targetPath, async () => {
715
777
  await commitPinnedWriteInRoot(root, pinned, params);
716
778
  });
@@ -729,8 +791,10 @@ async function commitPinnedWriteInRoot(root, pinned, params) {
729
791
  mode: params.mode ?? pinned.mode,
730
792
  sync: params.durable !== false,
731
793
  overwrite: params.overwrite,
794
+ rejectFinalSymlink: params.mutationSymlinks !== undefined,
732
795
  input: { kind: "buffer", data: params.data, encoding: params.encoding },
733
796
  rootIdentity: root.rootIdentity,
797
+ assertBeforeMutation: params.assertBeforeMutation,
734
798
  verifyPublished: async (fd, expectedIdentity, parentGuard) => {
735
799
  verifyingPublication = true;
736
800
  try {
@@ -765,46 +829,75 @@ async function commitPinnedWriteInRoot(root, pinned, params) {
765
829
  }
766
830
  }
767
831
  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
- });
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
+ }
773
852
  if (params.maxBytes !== undefined && source.stat.size > params.maxBytes) {
774
853
  await source.handle.close().catch(() => { });
775
854
  throw new FsSafeError("too-large", `file exceeds limit of ${params.maxBytes} bytes (got ${source.stat.size})`);
776
855
  }
777
856
  try {
778
857
  await serializePathWrite(rootWriteQueueKey(root, params.relativePath), async () => {
779
- 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);
780
859
  await serializePathWrite(pinned.targetPath, async () => {
781
860
  await assertCopySourceCurrent(source, sourceIdentity);
782
- 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);
783
869
  try {
784
- identity = await runPinnedWriteHelper({
870
+ await runPinnedWriteHelper({
785
871
  rootPath: pinned.rootReal,
786
872
  relativeParentPath: pinned.relativeParentPath,
787
873
  basename: pinned.basename,
788
874
  mkdir: params.mkdir !== false,
789
875
  mode: pinned.mode,
790
- overwrite: true,
876
+ overwrite: params.overwrite !== false,
877
+ rejectFinalSymlink: params.mutationSymlinks !== undefined,
791
878
  maxBytes: params.maxBytes,
792
879
  sync: params.durable !== false,
880
+ assertBeforeMutation: () => {
881
+ if (params.signal?.aborted)
882
+ throw new MutationAuthorityError(params.signal.reason);
883
+ params.assertBeforeMutation?.();
884
+ },
793
885
  verifyPublished: params.verifyPublished,
794
- input: { kind: "stream", stream: source.handle.createReadStream() },
886
+ onPublished: observer.onPublished,
887
+ input: { kind: "file", handle: source.handle, size: source.stat.size, clone, signal: params.signal, verifySource },
795
888
  rootIdentity: root.rootIdentity,
796
889
  });
797
890
  }
798
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
+ }
799
898
  throw normalizePinnedWriteError(error);
800
899
  }
801
- try {
802
- await assertCopySourcePathCurrent(source, sourceIdentity);
803
- }
804
- catch (error) {
805
- await removePathIfIdentityUnchanged(pinned.targetPath, identity).catch(() => undefined);
806
- throw error;
807
- }
900
+ await verifySource();
808
901
  });
809
902
  });
810
903
  }
@@ -812,33 +905,20 @@ async function copyFileInRoot(root, params) {
812
905
  await source.handle.close().catch(() => { });
813
906
  }
814
907
  }
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
908
  async function removePathIfIdentityUnchanged(targetPath, identity) {
829
909
  const parentGuard = await createAsyncDirectoryGuard(path.dirname(targetPath));
830
- const current = fsSync.lstatSync(targetPath);
831
- if (current.isSymbolicLink() || !sameFileIdentityForCleanup(current, identity)) {
832
- return;
833
- }
834
910
  await withAsyncDirectoryGuards([parentGuard], async () => {
835
- await fs.rm(targetPath);
911
+ const current = fsSync.lstatSync(targetPath);
912
+ if (current.isSymbolicLink() || !sameFileIdentityForCleanup(current, identity))
913
+ return;
914
+ await fs.unlink(targetPath);
836
915
  });
837
916
  }
838
- async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode, denyMutations, overwrite = true) {
917
+ async function resolvePinnedWriteTargetInRoot(root, relativePath, requestedMode, denyMutations, overwrite = true, mutationSymlinks) {
839
918
  const { rootReal, rootWithSep, resolved } = await resolveGuardedWritePathInRoot(root, {
840
919
  relativePath,
841
920
  denyMutations,
921
+ mutationSymlinks,
842
922
  });
843
923
  // resolvePathInRoot already enforces isPathInside, so any actual escape
844
924
  // is rejected upstream.
@@ -884,6 +964,7 @@ async function resolvePinnedPathInRoot(root, params) {
884
964
  return await resolvePinnedOperationPathInRoot(root, {
885
965
  allowRoot: params.allowRoot,
886
966
  denyMutations: params.denyMutations,
967
+ mutationSymlinks: params.mutationSymlinks,
887
968
  protectDenyMutationAncestors: params.remove === true,
888
969
  relativePath: params.relativePath,
889
970
  policy: params.remove ? PATH_ALIAS_POLICIES.unlinkTarget : PATH_ALIAS_POLICIES.strict,
@@ -893,6 +974,7 @@ async function resolvePinnedOperationPathInRoot(root, params) {
893
974
  const resolved = await resolvePinnedRootPathInRoot(root, {
894
975
  relativePath: params.relativePath,
895
976
  policy: params.policy,
977
+ mutationSymlinks: params.mutationSymlinks,
896
978
  });
897
979
  const relativeResolved = path.relative(resolved.rootReal, resolved.canonicalPath);
898
980
  if ((relativeResolved === "" || relativeResolved === ".") && params.allowRoot === true) {
@@ -929,9 +1011,12 @@ async function resolvePinnedRootPathInRoot(root, params) {
929
1011
  rootCanonicalPath: rootReal,
930
1012
  boundaryLabel: "root",
931
1013
  policy: params.policy,
1014
+ ...mutationSymlinkResolution(params.mutationSymlinks),
932
1015
  });
933
1016
  }
934
1017
  catch (err) {
1018
+ if (err instanceof FsSafeError && err.code === "symlink")
1019
+ throw err;
935
1020
  throw new FsSafeError("path-alias", "path alias escape blocked", { cause: err });
936
1021
  }
937
1022
  const rootWithSep = ensureTrailingSep(resolved.rootCanonicalPath);
@@ -952,19 +1037,23 @@ async function prepareRemoveGuard(targetPath) {
952
1037
  throw normalizeRemoveGuardError(error);
953
1038
  }
954
1039
  }
955
- async function removePathFallback(resolved) {
1040
+ async function removePathFallback(resolved, assertBeforeMutation, rejectFinalSymlink = false) {
956
1041
  const guard = await prepareRemoveGuard(resolved.resolved);
957
1042
  try {
958
- 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));
959
1047
  }
960
1048
  catch (error) {
961
1049
  throw normalizeRemovePathError(error);
962
1050
  }
963
- await assertAsyncDirectoryGuard(guard).catch(() => undefined);
1051
+ await assertAsyncDirectoryGuard(guard).catch(error => { throw normalizeRemoveGuardError(error); });
964
1052
  }
965
- async function mkdirPathFallback(resolved) {
1053
+ async function mkdirPathFallback(resolved, assertBeforeMutation, rejectSymlinks = false) {
966
1054
  await mkdirPathComponentsWithGuards({
967
- rootReal: resolved.rootReal, targetPath: resolved.resolved,
1055
+ rootReal: resolved.rootReal, targetPath: resolved.resolved, assertBeforeMutation,
1056
+ rejectSymlinks,
968
1057
  beforeComponent: async (componentPath) => await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("mkdir", componentPath),
969
1058
  });
970
1059
  }
@@ -984,31 +1073,7 @@ async function statPathFallback(root, relativePath) {
984
1073
  }
985
1074
  async function listPathFallback(root, relativePath, withFileTypes) {
986
1075
  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
- }
1076
+ return await listDirectoryPath(root, resolved.resolved, withFileTypes);
1012
1077
  }
1013
1078
  async function assertMoveMutationAllowed(root, params) {
1014
1079
  // Keep this preflight separate from the pinned resolutions in movePathFallback:
@@ -1028,15 +1093,18 @@ async function movePathFallback(root, params) {
1028
1093
  const source = await resolvePathInRoot(root, params.fromRelative, {
1029
1094
  aliasErrorCode: "path-alias",
1030
1095
  allowFinalSymlink: true,
1096
+ ...mutationSymlinkResolution(params.mutationSymlinks),
1031
1097
  });
1032
1098
  await assertMutationNotDenied(source.resolved, params.denyMutations, { protectAncestors: true });
1033
1099
  await resolvePinnedRootPathInRoot(root, {
1034
1100
  relativePath: params.fromRelative,
1035
1101
  policy: PATH_ALIAS_POLICIES.strict,
1102
+ mutationSymlinks: params.mutationSymlinks,
1036
1103
  });
1037
1104
  const target = await resolveGuardedWritePathInRoot(root, {
1038
1105
  relativePath: params.toRelative,
1039
1106
  denyMutations: params.denyMutations,
1107
+ mutationSymlinks: params.mutationSymlinks,
1040
1108
  allowFinalSymlink: true,
1041
1109
  protectDeniedAncestors: true,
1042
1110
  shouldAssertNoPathAlias: async (resolvedTarget) => {
@@ -1093,6 +1161,9 @@ async function movePathFallback(root, params) {
1093
1161
  await assertAsyncDirectoryGuard(sourceParentGuard);
1094
1162
  await assertAsyncDirectoryGuard(targetParentGuard);
1095
1163
  try {
1164
+ assertFinalSymlinkRejected(source.resolved, params.mutationSymlinks !== undefined);
1165
+ assertFinalSymlinkRejected(target.resolved, params.mutationSymlinks !== undefined);
1166
+ params.assertBeforeMutation?.();
1096
1167
  await fs.rename(source.resolved, target.resolved);
1097
1168
  }
1098
1169
  catch (error) {
@@ -1106,7 +1177,13 @@ async function movePathFallback(root, params) {
1106
1177
  }
1107
1178
  throw error;
1108
1179
  }
1109
- await assertAsyncDirectoryGuard(targetParentGuard).catch(() => undefined);
1180
+ try {
1181
+ await assertAsyncDirectoryGuard(sourceParentGuard);
1182
+ await assertAsyncDirectoryGuard(targetParentGuard);
1183
+ }
1184
+ catch (error) {
1185
+ throw normalizePinnedPathError(error);
1186
+ }
1110
1187
  }
1111
1188
  async function writeFileFallback(root, params) {
1112
1189
  if (params.overwrite === false) {
@@ -1119,47 +1196,76 @@ async function writeFileFallback(root, params) {
1119
1196
  // Private, writable placeholder: Windows cannot rename over a read-only file.
1120
1197
  mode: 0o600,
1121
1198
  denyMutations: params.denyMutations,
1199
+ assertBeforeMutation: params.assertBeforeMutation,
1200
+ mutationSymlinks: params.mutationSymlinks,
1122
1201
  truncateExisting: false,
1123
1202
  });
1124
1203
  const destinationPath = target.realPath;
1125
1204
  const mode = params.mode ?? (target.stat.mode & 0o777);
1126
- await target.handle.close().catch(() => { });
1127
- 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
+ });
1128
1209
  let tempPath = null;
1129
1210
  let unregisterTempPath = null;
1130
1211
  let writtenHandle;
1212
+ let writtenIdentity;
1213
+ let placeholderIdentity;
1214
+ let published = false;
1131
1215
  try {
1216
+ if (target.createdForWrite)
1217
+ placeholderIdentity = fsSync.fstatSync(target.handle.fd, { bigint: true });
1132
1218
  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,
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,
1139
1225
  });
1140
- writtenHandle = written.handle;
1141
- unregisterTempPath.setIdentity(written.identity);
1226
+ if (params.durable !== false)
1227
+ await writtenHandle.sync();
1142
1228
  const commitTempPath = tempPath;
1229
+ const commitHandle = writtenHandle;
1230
+ const commitIdentity = writtenIdentity;
1143
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?.();
1144
1245
  await fs.rename(commitTempPath, destinationPath);
1145
1246
  tempPath = null;
1247
+ published = true;
1146
1248
  });
1147
1249
  unregisterTempPath();
1148
1250
  unregisterTempPath = null;
1149
1251
  // Final mode via the retained handle after publication, as the native writer does.
1150
1252
  try {
1151
- await written.handle.chmod(mode);
1253
+ await writtenHandle.chmod(mode);
1152
1254
  }
1153
1255
  catch (error) {
1154
- await removePathIfIdentityUnchanged(destinationPath, written.identity).catch(() => { });
1256
+ await cleanupPinnedFilePath({
1257
+ pathname: destinationPath, handle: writtenHandle, identity: writtenIdentity, parentGuard: destinationGuard,
1258
+ });
1155
1259
  throw error;
1156
1260
  }
1261
+ if (params.durable !== false)
1262
+ await writtenHandle.sync();
1157
1263
  try {
1158
1264
  await verifyAtomicWriteResult({
1159
1265
  root,
1160
1266
  targetPath: destinationPath,
1161
- expectedIdentity: written.identity,
1162
- fd: written.handle.fd,
1267
+ expectedIdentity: writtenIdentity,
1268
+ fd: writtenHandle.fd,
1163
1269
  parentGuard: destinationGuard,
1164
1270
  });
1165
1271
  }
@@ -1167,12 +1273,22 @@ async function writeFileFallback(root, params) {
1167
1273
  emitWriteBoundaryWarning(`post-write verification failed: ${String(err)}`);
1168
1274
  throw err;
1169
1275
  }
1276
+ if (params.durable !== false)
1277
+ await syncDirectoryBestEffort(path.dirname(destinationPath));
1170
1278
  }
1171
1279
  finally {
1172
- await writtenHandle?.close().catch(() => undefined);
1173
- if (tempPath) {
1174
- 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
+ });
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
+ });
1175
1290
  }
1291
+ await writtenHandle?.close().catch(() => undefined);
1176
1292
  unregisterTempPath?.();
1177
1293
  }
1178
1294
  }
@@ -1180,38 +1296,38 @@ async function writeMissingFileFallback(root, params) {
1180
1296
  const { rootReal, resolved } = await resolveGuardedWritePathInRoot(root, {
1181
1297
  relativePath: params.relativePath,
1182
1298
  denyMutations: params.denyMutations,
1299
+ mutationSymlinks: params.mutationSymlinks,
1183
1300
  });
1184
1301
  const targetPath = params.mkdir === false
1185
1302
  ? resolved
1186
- : await prepareRootWriteTarget(rootReal, resolved);
1187
- const parentGuard = await createAsyncDirectoryGuard(path.dirname(targetPath));
1303
+ : await prepareRootWriteTarget(rootReal, resolved, params.assertBeforeMutation);
1304
+ const parentGuard = await createAsyncDirectoryGuard(path.dirname(targetPath), { bigint: true });
1188
1305
  let created = false;
1189
1306
  let createdIdentity;
1190
1307
  let writtenHandle;
1191
1308
  let verifyingPublication = false;
1192
1309
  try {
1193
1310
  const { handle, writtenStat } = await withAsyncDirectoryGuards([parentGuard], async () => {
1311
+ assertFinalSymlinkRejected(targetPath, params.mutationSymlinks !== undefined);
1312
+ params.assertBeforeMutation?.();
1194
1313
  const handle = await fs.open(targetPath, OPEN_WRITE_CREATE_FLAGS, params.mode ?? 0o600).catch((error) => recordExclusiveCreateFailure(error, targetPath));
1314
+ writtenHandle = handle;
1195
1315
  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
- }
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 };
1211
1328
  }, {
1212
- onPostGuardFailure: async ({ handle }) => {
1329
+ onPostGuardFailure: () => {
1213
1330
  created = false; // Parent is untrusted now; skip outer path cleanup by name.
1214
- await handle.close().catch(() => undefined);
1215
1331
  },
1216
1332
  });
1217
1333
  writtenHandle = handle;
@@ -1224,6 +1340,8 @@ async function writeMissingFileFallback(root, params) {
1224
1340
  fd: handle.fd,
1225
1341
  parentGuard,
1226
1342
  });
1343
+ if (params.durable !== false)
1344
+ await syncDirectoryBestEffort(path.dirname(targetPath));
1227
1345
  }
1228
1346
  catch (err) {
1229
1347
  if (verifyingPublication)
@@ -1236,9 +1354,11 @@ async function writeMissingFileFallback(root, params) {
1236
1354
  throw err;
1237
1355
  }
1238
1356
  finally {
1239
- await writtenHandle?.close().catch(() => undefined);
1240
- if (created && createdIdentity) {
1241
- await removePathIfIdentityUnchanged(targetPath, createdIdentity).catch(() => undefined);
1357
+ if (created && writtenHandle) {
1358
+ await cleanupPinnedFilePath({
1359
+ pathname: targetPath, handle: writtenHandle, identity: createdIdentity, parentGuard,
1360
+ });
1242
1361
  }
1362
+ await writtenHandle?.close().catch(() => undefined);
1243
1363
  }
1244
1364
  }