@openclaw/fs-safe 0.16.0 → 0.17.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 (180) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +8 -1
  3. package/dist/absolute-path.d.ts.map +1 -1
  4. package/dist/absolute-path.js +2 -8
  5. package/dist/advanced.d.ts +2 -0
  6. package/dist/advanced.d.ts.map +1 -1
  7. package/dist/advanced.js +2 -0
  8. package/dist/archive-deadline.d.ts.map +1 -1
  9. package/dist/archive-deadline.js +22 -23
  10. package/dist/archive-merge.d.ts +1 -0
  11. package/dist/archive-merge.d.ts.map +1 -1
  12. package/dist/archive-merge.js +4 -4
  13. package/dist/archive-native.d.ts +1 -0
  14. package/dist/archive-native.d.ts.map +1 -1
  15. package/dist/archive-native.js +1 -0
  16. package/dist/archive-options.d.ts +2 -0
  17. package/dist/archive-options.d.ts.map +1 -1
  18. package/dist/archive-parser.wasm +0 -0
  19. package/dist/archive-zip-count.d.ts.map +1 -1
  20. package/dist/archive-zip-count.js +21 -1
  21. package/dist/archive-zip-directory.d.ts.map +1 -1
  22. package/dist/archive-zip-directory.js +23 -1
  23. package/dist/archive-zip-loader.d.ts +2 -0
  24. package/dist/archive-zip-loader.d.ts.map +1 -1
  25. package/dist/archive-zip-loader.js +7 -0
  26. package/dist/archive-zip-names.d.ts.map +1 -1
  27. package/dist/archive-zip-names.js +7 -2
  28. package/dist/archive.d.ts.map +1 -1
  29. package/dist/archive.js +9 -3
  30. package/dist/byte-view.d.ts +3 -0
  31. package/dist/byte-view.d.ts.map +1 -0
  32. package/dist/byte-view.js +13 -0
  33. package/dist/creation-darwin.d.ts +0 -1
  34. package/dist/creation-darwin.d.ts.map +1 -1
  35. package/dist/creation-darwin.js +0 -9
  36. package/dist/directory-durability.d.ts +6 -6
  37. package/dist/directory-durability.d.ts.map +1 -1
  38. package/dist/directory-receipt.d.ts +2 -2
  39. package/dist/directory-receipt.d.ts.map +1 -1
  40. package/dist/directory-receipt.js +15 -19
  41. package/dist/file-cleanup.d.ts +1 -0
  42. package/dist/file-cleanup.d.ts.map +1 -1
  43. package/dist/file-cleanup.js +7 -4
  44. package/dist/file-contents.d.ts +6 -0
  45. package/dist/file-contents.d.ts.map +1 -0
  46. package/dist/file-contents.js +40 -0
  47. package/dist/file-hash.d.ts.map +1 -1
  48. package/dist/file-hash.js +16 -4
  49. package/dist/file-lock-sync-root-acquire.d.ts.map +1 -1
  50. package/dist/file-lock-sync-root-acquire.js +3 -0
  51. package/dist/file-lock-sync-root-held.d.ts +1 -2
  52. package/dist/file-lock-sync-root-held.d.ts.map +1 -1
  53. package/dist/file-lock-sync-root-held.js +7 -5
  54. package/dist/file-lock-sync-stale-admission.d.ts.map +1 -1
  55. package/dist/file-lock-sync-stale-admission.js +3 -0
  56. package/dist/file-lock-sync.d.ts.map +1 -1
  57. package/dist/file-lock-sync.js +8 -11
  58. package/dist/file-store.js +3 -3
  59. package/dist/guarded-mkdir.d.ts.map +1 -1
  60. package/dist/guarded-mkdir.js +6 -27
  61. package/dist/install-path.d.ts.map +1 -1
  62. package/dist/install-path.js +2 -5
  63. package/dist/json-durable-queue-ownership.d.ts.map +1 -1
  64. package/dist/json-durable-queue-ownership.js +2 -6
  65. package/dist/json-durable-queue-paths.d.ts.map +1 -1
  66. package/dist/json-durable-queue-paths.js +2 -24
  67. package/dist/json-durable-queue.d.ts.map +1 -1
  68. package/dist/json-durable-queue.js +10 -9
  69. package/dist/json.d.ts.map +1 -1
  70. package/dist/json.js +32 -75
  71. package/dist/local-roots.d.ts.map +1 -1
  72. package/dist/local-roots.js +19 -21
  73. package/dist/move-path-cleanup.d.ts +5 -19
  74. package/dist/move-path-cleanup.d.ts.map +1 -1
  75. package/dist/move-path-cleanup.js +57 -21
  76. package/dist/move-path.d.ts.map +1 -1
  77. package/dist/move-path.js +62 -39
  78. package/dist/native-staged-file.d.ts +2 -1
  79. package/dist/native-staged-file.d.ts.map +1 -1
  80. package/dist/native-staged-file.js +8 -5
  81. package/dist/native.js +2 -2
  82. package/dist/opened-realpath.d.ts.map +1 -1
  83. package/dist/opened-realpath.js +11 -2
  84. package/dist/path.d.ts.map +1 -1
  85. package/dist/path.js +2 -1
  86. package/dist/permissions.d.ts.map +1 -1
  87. package/dist/permissions.js +3 -17
  88. package/dist/pinned-write-input.d.ts.map +1 -1
  89. package/dist/pinned-write-input.js +11 -1
  90. package/dist/pinned-write-mode.d.ts +3 -3
  91. package/dist/pinned-write-mode.d.ts.map +1 -1
  92. package/dist/pinned-write-mode.js +15 -8
  93. package/dist/pinned-write-staged.d.ts.map +1 -1
  94. package/dist/pinned-write-staged.js +10 -11
  95. package/dist/pinned-write.d.ts.map +1 -1
  96. package/dist/pinned-write.js +17 -13
  97. package/dist/publish-file.d.ts +2 -2
  98. package/dist/publish-file.d.ts.map +1 -1
  99. package/dist/publish-file.js +56 -96
  100. package/dist/regular-file.d.ts.map +1 -1
  101. package/dist/regular-file.js +35 -44
  102. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  103. package/dist/replace-file-copy-fallback.js +28 -26
  104. package/dist/replace-file-copy-source.d.ts.map +1 -1
  105. package/dist/replace-file-copy-source.js +13 -22
  106. package/dist/replace-file-descriptor.d.ts.map +1 -1
  107. package/dist/replace-file-descriptor.js +10 -16
  108. package/dist/replace-file-temp-owner.js +6 -6
  109. package/dist/replace-file.d.ts.map +1 -1
  110. package/dist/replace-file.js +9 -13
  111. package/dist/root-directory-list.d.ts.map +1 -1
  112. package/dist/root-directory-list.js +20 -3
  113. package/dist/root-file-final-admission.d.ts +1 -1
  114. package/dist/root-file-final-admission.d.ts.map +1 -1
  115. package/dist/root-file-final-admission.js +5 -2
  116. package/dist/root-file.d.ts.map +1 -1
  117. package/dist/root-file.js +3 -2
  118. package/dist/root-impl.d.ts.map +1 -1
  119. package/dist/root-impl.js +50 -20
  120. package/dist/root-move-noreplace.d.ts +2 -0
  121. package/dist/root-move-noreplace.d.ts.map +1 -1
  122. package/dist/root-move-noreplace.js +2 -2
  123. package/dist/root-read-admission.d.ts.map +1 -1
  124. package/dist/root-read-admission.js +7 -2
  125. package/dist/root-remove.d.ts.map +1 -1
  126. package/dist/root-remove.js +15 -1
  127. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  128. package/dist/sidecar-lock-acquire.js +4 -6
  129. package/dist/sidecar-lock-handle.d.ts +3 -0
  130. package/dist/sidecar-lock-handle.d.ts.map +1 -1
  131. package/dist/sidecar-lock-handle.js +6 -0
  132. package/dist/sidecar-lock-reclaim.d.ts +1 -1
  133. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  134. package/dist/sidecar-lock-reclaim.js +11 -8
  135. package/dist/sidecar-lock.d.ts.map +1 -1
  136. package/dist/sidecar-lock.js +3 -5
  137. package/dist/staged-directory.d.ts +2 -2
  138. package/dist/staged-directory.d.ts.map +1 -1
  139. package/dist/strict-file-identity.d.ts +1 -1
  140. package/dist/strict-file-identity.d.ts.map +1 -1
  141. package/dist/strict-file-identity.js +9 -9
  142. package/dist/symlink-parents.d.ts.map +1 -1
  143. package/dist/symlink-parents.js +2 -27
  144. package/dist/temp-workspace-owner.js +4 -4
  145. package/dist/unicode-path.d.ts.map +1 -1
  146. package/dist/unicode-path.js +3 -0
  147. package/dist/walk.d.ts.map +1 -1
  148. package/dist/walk.js +4 -2
  149. package/dist/write-file-handle.d.ts +7 -0
  150. package/dist/write-file-handle.d.ts.map +1 -1
  151. package/dist/write-file-handle.js +23 -0
  152. package/dist/write-open-flags.d.ts.map +1 -1
  153. package/dist/write-open-flags.js +1 -8
  154. package/dist/write-queue.d.ts.map +1 -1
  155. package/dist/write-queue.js +1 -4
  156. package/docs/advanced.md +68 -1
  157. package/docs/archive.md +41 -2
  158. package/docs/atomic.md +29 -5
  159. package/docs/contributing.md +4 -0
  160. package/docs/creation.md +8 -4
  161. package/docs/durability.md +35 -0
  162. package/docs/file-contents.md +68 -0
  163. package/docs/json.md +5 -4
  164. package/docs/local-roots.md +2 -0
  165. package/docs/mutation-policy-proof.md +5 -3
  166. package/docs/native.md +9 -8
  167. package/docs/path.md +4 -4
  168. package/docs/public-api.md +5 -0
  169. package/docs/quickstart.md +1 -1
  170. package/docs/reading.md +2 -2
  171. package/docs/regular-file.md +3 -0
  172. package/docs/root.md +9 -0
  173. package/docs/sidecar-lock.md +9 -1
  174. package/docs/staged-file.md +4 -3
  175. package/docs/store.md +3 -1
  176. package/docs/temp.md +4 -1
  177. package/docs/types.md +18 -2
  178. package/docs/walk.md +7 -0
  179. package/docs/writing.md +9 -2
  180. package/package.json +8 -8
@@ -1,4 +1,4 @@
1
- import { type Stats } from "node:fs";
1
+ import { type BigIntStats, type Stats } from "node:fs";
2
2
  import { type DirectoryReceipt, type DirectorySyncOutcome } from "./directory-durability.js";
3
3
  import { type FileIdentityStat } from "./file-identity.js";
4
4
  import { type PublishFileExclusiveSyncFailurePolicy } from "./publish-file-failure.js";
@@ -16,6 +16,6 @@ export declare function publishFileExclusive(params: {
16
16
  expectedSourceIdentity?: FileIdentityStat;
17
17
  strategy: PublishFileExclusiveStrategy;
18
18
  onSyncFailure?: PublishFileExclusiveSyncFailurePolicy;
19
- parentReceipt?: DirectoryReceipt;
19
+ parentReceipt?: DirectoryReceipt<Stats | BigIntStats>;
20
20
  }): Promise<PublishFileExclusiveResult>;
21
21
  //# sourceMappingURL=publish-file.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"publish-file.d.ts","sourceRoot":"","sources":["../src/publish-file.ts"],"names":[],"mappings":"AACA,OAAe,EAAoB,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAG/D,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EAC1B,MAAM,2BAA2B,CAAC;AAInC,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,oBAAoB,CAAC;AAK5B,OAAO,EAML,KAAK,qCAAqC,EAC3C,MAAM,2BAA2B,CAAC;AAKnC,YAAY,EACV,2BAA2B,EAC3B,wCAAwC,EACxC,kCAAkC,EAClC,gCAAgC,EAChC,qCAAqC,GACtC,MAAM,2BAA2B,CAAC;AAEnC,MAAM,MAAM,4BAA4B,GAAG,cAAc,GAAG,eAAe,GAAG,kBAAkB,CAAC;AAEjG,MAAM,MAAM,0BAA0B,GAAG;IACvC,MAAM,EAAE,UAAU,GAAG,gBAAgB,GAAG,kBAAkB,CAAC;IAC3D,QAAQ,EAAE,KAAK,CAAC;IAChB,aAAa,EAAE,oBAAoB,CAAC;CACrC,CAAC;AAIF,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE/D;AA0ND,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,sBAAsB,CAAC,EAAE,gBAAgB,CAAC;IAC1C,QAAQ,EAAE,4BAA4B,CAAC;IACvC,aAAa,CAAC,EAAE,qCAAqC,CAAC;IACtD,aAAa,CAAC,EAAE,gBAAgB,CAAC;CAClC,GAAG,OAAO,CAAC,0BAA0B,CAAC,CA4NtC"}
1
+ {"version":3,"file":"publish-file.d.ts","sourceRoot":"","sources":["../src/publish-file.ts"],"names":[],"mappings":"AACA,OAAe,EAAE,KAAK,WAAW,EAAE,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAG/D,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EAC1B,MAAM,2BAA2B,CAAC;AAInC,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,oBAAoB,CAAC;AAK5B,OAAO,EAML,KAAK,qCAAqC,EAC3C,MAAM,2BAA2B,CAAC;AAOnC,YAAY,EACV,2BAA2B,EAC3B,wCAAwC,EACxC,kCAAkC,EAClC,gCAAgC,EAChC,qCAAqC,GACtC,MAAM,2BAA2B,CAAC;AAEnC,MAAM,MAAM,4BAA4B,GAAG,cAAc,GAAG,eAAe,GAAG,kBAAkB,CAAC;AAEjG,MAAM,MAAM,0BAA0B,GAAG;IACvC,MAAM,EAAE,UAAU,GAAG,gBAAgB,GAAG,kBAAkB,CAAC;IAC3D,QAAQ,EAAE,KAAK,CAAC;IAChB,aAAa,EAAE,oBAAoB,CAAC;CACrC,CAAC;AAIF,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE/D;AAgOD,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,sBAAsB,CAAC,EAAE,gBAAgB,CAAC;IAC1C,QAAQ,EAAE,4BAA4B,CAAC;IACvC,aAAa,CAAC,EAAE,qCAAqC,CAAC;IACtD,aAAa,CAAC,EAAE,gBAAgB,CAAC,KAAK,GAAG,WAAW,CAAC,CAAC;CACvD,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAsKtC"}
@@ -15,6 +15,8 @@ import { directorySyncFailure, publicationFailure, rememberCreatedTarget, } from
15
15
  import { admitStandalonePublicationPath } from "./standalone-publication-path.js";
16
16
  import { getFsSafeTestHooks } from "./test-hooks.js";
17
17
  import { assertNoWindowsPathAlias } from "./windows-path-alias.js";
18
+ import { hasErrorCode } from "./file-cleanup.js";
19
+ import { writeAllToFile } from "./write-file-handle.js";
18
20
  const HARDLINK_FALLBACK_CODES = new Set(["EPERM", "EXDEV", "ENOTSUP", "EOPNOTSUPP", "ENOSYS"]);
19
21
  const NATIVE_COPY_FALLBACK_CODES = new Set(["EINVAL", "ENOSYS", "ENOTSUP", "EOPNOTSUPP", "EPERM", "EXDEV"]);
20
22
  export function isHardlinkFallbackError(error) {
@@ -41,7 +43,7 @@ async function openNativeParent(filePath) {
41
43
  throw error;
42
44
  }
43
45
  }
44
- async function assertPinnedSourceCurrent(params) {
46
+ function assertPinnedSourceCurrent(params) {
45
47
  // Windows file indexes can exceed Number.MAX_SAFE_INTEGER, so publication
46
48
  // fences must compare bigint stats instead of rounded numeric identities.
47
49
  const opened = fsSync.fstatSync(params.handle.fd, { bigint: true });
@@ -54,6 +56,23 @@ async function assertPinnedSourceCurrent(params) {
54
56
  throw new FsSafeError("path-mismatch", "publication source changed during operation");
55
57
  }
56
58
  }
59
+ function assertPublishedTargetCurrent(targetPath, identity, message) {
60
+ const current = fsSync.lstatSync(targetPath, { bigint: true });
61
+ if (current.isSymbolicLink() || !current.isFile() || !sameFileIdentity(current, identity)) {
62
+ throw new FsSafeError("path-mismatch", message);
63
+ }
64
+ }
65
+ function assertRenamedSourceAbsent(sourcePath) {
66
+ try {
67
+ fsSync.lstatSync(sourcePath);
68
+ }
69
+ catch (error) {
70
+ if (hasErrorCode(error, "ENOENT"))
71
+ return;
72
+ throw error;
73
+ }
74
+ throw new FsSafeError("path-mismatch", "no-replace publication source still exists");
75
+ }
57
76
  async function copyPinnedSource(params) {
58
77
  if (params.native && params.targetNativeParent) {
59
78
  const closeFd = captureNativeFdClose(params.native);
@@ -131,15 +150,9 @@ async function copyPinnedSource(params) {
131
150
  if (bytesRead === 0) {
132
151
  break;
133
152
  }
134
- hash.update(buffer.subarray(0, bytesRead));
135
- let written = 0;
136
- while (written < bytesRead) {
137
- const result = await target.write(buffer, written, bytesRead - written, position + written);
138
- if (result.bytesWritten <= 0) {
139
- throw new FsSafeError("helper-failed", "exclusive publication copy made no progress");
140
- }
141
- written += result.bytesWritten;
142
- }
153
+ const chunk = buffer.subarray(0, bytesRead);
154
+ hash.update(chunk);
155
+ await writeAllToFile(target, chunk, { position });
143
156
  position += bytesRead;
144
157
  }
145
158
  await target.sync();
@@ -208,6 +221,7 @@ export async function publishFileExclusive(params) {
208
221
  let parent;
209
222
  let sourceNativeParent;
210
223
  let targetNativeParent;
224
+ let copiedTarget;
211
225
  const strategy = params.strategy;
212
226
  const failure = {
213
227
  phase: strategy === "rename-noreplace" ? "rename-create" : "hardlink-create",
@@ -232,8 +246,20 @@ export async function publishFileExclusive(params) {
232
246
  : sourceIdentity))) {
233
247
  throw new FsSafeError("path-mismatch", "publication source identity did not match");
234
248
  }
249
+ const assertSourceCurrent = () => assertPinnedSourceCurrent({ sourcePath, handle: source, identity: sourceExactIdentity });
250
+ const finishPublication = async (method, handle, identity) => {
251
+ const verifyPhase = failure.phase;
252
+ const directorySync = await syncPublishedParent({ parent: parent, failure, method, targetPath });
253
+ failure.phase = verifyPhase;
254
+ if (method === "rename-noreplace")
255
+ assertRenamedSourceAbsent(sourcePath);
256
+ else
257
+ assertSourceCurrent();
258
+ assertPublishedTargetCurrent(targetPath, identity, "publication target changed during directory sync");
259
+ return { method, identity: fsSync.fstatSync(handle.fd), directorySync };
260
+ };
235
261
  await parent.assertCurrent();
236
- await assertPinnedSourceCurrent({ sourcePath, handle: source, identity: sourceExactIdentity });
262
+ assertSourceCurrent();
237
263
  const native = strategy === "rename-noreplace"
238
264
  ? requireNativeBinding()
239
265
  : getNativeBinding();
@@ -248,33 +274,10 @@ export async function publishFileExclusive(params) {
248
274
  // A failed post-rename fence must not delete the only remaining name.
249
275
  failure.preserveTarget = true;
250
276
  await getFsSafeTestHooks()?.afterPublishTargetCreated?.("rename-noreplace", targetPath, sourceExactIdentity);
251
- const targetExactIdentity = fsSync.lstatSync(targetPath, { bigint: true });
252
- if (targetExactIdentity.isSymbolicLink() ||
253
- !targetExactIdentity.isFile() ||
254
- !sameFileIdentity(targetExactIdentity, sourceExactIdentity)) {
255
- throw new FsSafeError("path-mismatch", "no-replace publication target changed");
256
- }
257
- try {
258
- fsSync.lstatSync(sourcePath);
259
- throw new FsSafeError("path-mismatch", "no-replace publication source still exists");
260
- }
261
- catch (error) {
262
- if (error.code !== "ENOENT") {
263
- throw error;
264
- }
265
- }
277
+ assertPublishedTargetCurrent(targetPath, sourceExactIdentity, "no-replace publication target changed");
278
+ assertRenamedSourceAbsent(sourcePath);
266
279
  syncFileBestEffortSync(sourceNativeParent.handle.fd);
267
- const targetIdentity = fsSync.lstatSync(targetPath);
268
- return {
269
- method: "rename-noreplace",
270
- identity: targetIdentity,
271
- directorySync: await syncPublishedParent({
272
- parent,
273
- failure,
274
- method: "rename-noreplace",
275
- targetPath,
276
- }),
277
- };
280
+ return await finishPublication("rename-noreplace", source, sourceExactIdentity);
278
281
  }
279
282
  try {
280
283
  if (native) {
@@ -285,24 +288,9 @@ export async function publishFileExclusive(params) {
285
288
  }
286
289
  rememberCreatedTarget(failure, sourceExactIdentity, "hardlink-verify");
287
290
  await getFsSafeTestHooks()?.afterPublishTargetCreated?.("hardlink", targetPath, sourceExactIdentity);
288
- const targetExactIdentity = fsSync.lstatSync(targetPath, { bigint: true });
289
- if (targetExactIdentity.isSymbolicLink() ||
290
- !targetExactIdentity.isFile() ||
291
- !sameFileIdentity(targetExactIdentity, sourceExactIdentity)) {
292
- throw new FsSafeError("path-mismatch", "hardlink publication target changed");
293
- }
294
- await assertPinnedSourceCurrent({ sourcePath, handle: source, identity: sourceExactIdentity });
295
- const targetIdentity = fsSync.lstatSync(targetPath);
296
- return {
297
- method: "hardlink",
298
- identity: targetIdentity,
299
- directorySync: await syncPublishedParent({
300
- parent,
301
- failure,
302
- method: "hardlink",
303
- targetPath,
304
- }),
305
- };
291
+ assertPublishedTargetCurrent(targetPath, sourceExactIdentity, "hardlink publication target changed");
292
+ assertSourceCurrent();
293
+ return await finishPublication("hardlink", source, sourceExactIdentity);
306
294
  }
307
295
  catch (error) {
308
296
  if (failure.targetCreated ||
@@ -312,48 +300,19 @@ export async function publishFileExclusive(params) {
312
300
  }
313
301
  }
314
302
  failure.phase = "copy-create";
315
- let target;
316
- let targetIdentity;
317
- try {
318
- const copied = await copyPinnedSource({
319
- source,
320
- targetPath,
321
- native,
322
- targetNativeParent,
323
- failure,
324
- });
325
- target = copied.handle;
326
- targetIdentity = fsSync.fstatSync(target.fd);
327
- const targetPathStat = fsSync.lstatSync(targetPath);
328
- const targetPathExactStat = fsSync.lstatSync(targetPath, { bigint: true });
329
- const copiedBack = await hashFileHandle(target, native);
330
- const sourceAfter = await hashFileHandle(source, native);
331
- if (targetPathStat.isSymbolicLink() ||
332
- targetPathExactStat.isSymbolicLink() ||
333
- !sameFileIdentity(targetPathExactStat, copied.exactIdentity) ||
334
- copiedBack.bytes !== copied.bytes ||
335
- copiedBack.digest !== copied.digest ||
336
- sourceAfter.bytes !== copied.bytes ||
337
- sourceAfter.digest !== copied.digest) {
338
- throw new FsSafeError("path-mismatch", "exclusive publication copy failed content fencing");
339
- }
340
- await assertPinnedSourceCurrent({ sourcePath, handle: source, identity: sourceExactIdentity });
341
- const directorySync = await syncPublishedParent({
342
- parent,
343
- failure,
344
- method: "exclusive-copy",
345
- targetPath,
346
- });
347
- return { method: "exclusive-copy", identity: targetPathStat, directorySync };
348
- }
349
- catch (error) {
350
- await target?.close().catch(() => undefined);
351
- target = undefined;
352
- throw error;
353
- }
354
- finally {
355
- await target?.close().catch(() => undefined);
303
+ const copied = await copyPinnedSource({ source, targetPath, native, targetNativeParent, failure });
304
+ copiedTarget = copied.handle;
305
+ const copiedBack = await hashFileHandle(copiedTarget, native);
306
+ const sourceAfter = await hashFileHandle(source, native);
307
+ assertPublishedTargetCurrent(targetPath, copied.exactIdentity, "exclusive publication copy failed content fencing");
308
+ if (copiedBack.bytes !== copied.bytes ||
309
+ copiedBack.digest !== copied.digest ||
310
+ sourceAfter.bytes !== copied.bytes ||
311
+ sourceAfter.digest !== copied.digest) {
312
+ throw new FsSafeError("path-mismatch", "exclusive publication copy failed content fencing");
356
313
  }
314
+ assertSourceCurrent();
315
+ return await finishPublication("exclusive-copy", copiedTarget, copied.exactIdentity);
357
316
  }
358
317
  catch (error) {
359
318
  if (!failure.targetCreated)
@@ -365,6 +324,7 @@ export async function publishFileExclusive(params) {
365
324
  throw publicationFailure(error, failure, cleanup);
366
325
  }
367
326
  finally {
327
+ await copiedTarget?.close().catch(() => undefined);
368
328
  await sourceNativeParent?.handle.close().catch(() => undefined);
369
329
  await targetNativeParent?.handle.close().catch(() => undefined);
370
330
  await source.close().catch(() => undefined);
@@ -1 +1 @@
1
- {"version":3,"file":"regular-file.d.ts","sourceRoot":"","sources":["../src/regular-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAe,KAAK,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,MAAM,MAAM,SAAS,CAAC;AAuB7B,MAAM,MAAM,qBAAqB,GAAG;IAAE,OAAO,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,KAAK,CAAA;CAAE,CAAC;AAExF,KAAK,8BAA8B,GAAG,IAAI,CACxC,OAAO,MAAM,CAAC,SAAS,EACvB,UAAU,GAAG,SAAS,GAAG,UAAU,CACpC,GACC,OAAO,CAAC,IAAI,CAAC,OAAO,MAAM,CAAC,SAAS,EAAE,YAAY,GAAG,YAAY,CAAC,CAAC,CAAC;AAEtE,MAAM,MAAM,wBAAwB,GAAG;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC,CAAC;AAEF,wBAAgB,6BAA6B,CAC3C,SAAS,GAAE,8BAAiD,GAC3D,MAAM,CASR;AAaD,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAEtF;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,qBAAqB,CAe3E;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,KAAK,CAAA;CAAE,CAAC,CA+D3C;AA2DD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG;IACpF,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,KAAK,CAAC;CACb,CAsCA;AAmBD,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAkFxF;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,wBAAwB,GAAG,IAAI,CAkF7E"}
1
+ {"version":3,"file":"regular-file.d.ts","sourceRoot":"","sources":["../src/regular-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAe,KAAK,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,MAAM,MAAM,SAAS,CAAC;AAuB7B,MAAM,MAAM,qBAAqB,GAAG;IAAE,OAAO,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,KAAK,CAAA;CAAE,CAAC;AAExF,KAAK,8BAA8B,GAAG,IAAI,CACxC,OAAO,MAAM,CAAC,SAAS,EACvB,UAAU,GAAG,SAAS,GAAG,UAAU,CACpC,GACC,OAAO,CAAC,IAAI,CAAC,OAAO,MAAM,CAAC,SAAS,EAAE,YAAY,GAAG,YAAY,CAAC,CAAC,CAAC;AAEtE,MAAM,MAAM,wBAAwB,GAAG;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC,CAAC;AAEF,wBAAgB,6BAA6B,CAC3C,SAAS,GAAE,8BAAiD,GAC3D,MAAM,CASR;AAaD,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAEtF;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,qBAAqB,CAe3E;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,KAAK,CAAA;CAAE,CAAC,CA+D3C;AA2DD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG;IACpF,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,KAAK,CAAC;CACb,CAsCA;AAoCD,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAqExF;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,wBAAwB,GAAG,IAAI,CAqE7E"}
@@ -223,10 +223,26 @@ function throwAppendIdentityError(error, filePath) {
223
223
  }
224
224
  throw error;
225
225
  }
226
- export async function appendRegularFile(options) {
226
+ function captureAppendOptions(options) {
227
227
  const filePath = options.filePath;
228
228
  assertNoWindowsPathAlias(filePath, "filesystem", "file path uses a Windows filesystem namespace alias");
229
- if (options.rejectSymlinkParents === true) {
229
+ return {
230
+ filePath, content: options.content, encoding: options.encoding ?? "utf8",
231
+ mode: options.mode ?? 0o600, maxFileBytes: options.maxFileBytes,
232
+ rejectSymlinkParents: options.rejectSymlinkParents,
233
+ };
234
+ }
235
+ function inspectAppendPath(filePath) {
236
+ const stat = fsSync.lstatSync(filePath, { bigint: true });
237
+ if (stat.isSymbolicLink())
238
+ throw new Error(`Refusing to append through symlink: ${filePath}`);
239
+ if (!stat.isFile())
240
+ throw new Error(`Refusing to append to non-file: ${filePath}`);
241
+ return stat;
242
+ }
243
+ export async function appendRegularFile(options) {
244
+ const { filePath, content, encoding, mode, maxFileBytes, rejectSymlinkParents } = captureAppendOptions(options);
245
+ if (rejectSymlinkParents === true) {
230
246
  const resolvedDir = resolvePathPreservingWindowsRoot(path.dirname(filePath));
231
247
  await assertNoSymlinkParents({
232
248
  rootDir: path.parse(resolvedDir).root,
@@ -239,33 +255,22 @@ export async function appendRegularFile(options) {
239
255
  }
240
256
  let preOpenStat;
241
257
  try {
242
- preOpenStat = await inspectFileIdentity(async () => {
243
- const stat = fsSync.lstatSync(filePath, { bigint: true });
244
- if (stat.isSymbolicLink()) {
245
- throw new Error(`Refusing to append through symlink: ${filePath}`);
246
- }
247
- if (!stat.isFile()) {
248
- throw new Error(`Refusing to append to non-file: ${filePath}`);
249
- }
250
- return stat;
251
- });
258
+ preOpenStat = await inspectFileIdentity(async () => inspectAppendPath(filePath));
252
259
  }
253
260
  catch (error) {
254
261
  if (!isNotFoundPathError(error))
255
262
  throw error;
256
263
  }
257
- const contentBytes = Buffer.isBuffer(options.content)
258
- ? options.content.byteLength
259
- : Buffer.byteLength(options.content, options.encoding ?? "utf8");
260
- if (options.maxFileBytes !== undefined &&
261
- Number(preOpenStat?.size ?? 0n) + contentBytes > options.maxFileBytes) {
264
+ const contentBytes = Buffer.isBuffer(content) ? content.byteLength : Buffer.byteLength(content, encoding);
265
+ if (maxFileBytes !== undefined &&
266
+ Number(preOpenStat?.size ?? 0n) + contentBytes > maxFileBytes) {
262
267
  return;
263
268
  }
264
269
  await getFsSafeTestHooks()?.beforeRegularFileAppendOpen?.(filePath);
265
270
  const flags = resolveRegularFileAppendFlags();
266
271
  let handle;
267
272
  try {
268
- handle = await fs.open(filePath, flags, options.mode ?? 0o600);
273
+ handle = await fs.open(filePath, flags, mode);
269
274
  }
270
275
  catch (error) {
271
276
  if (await isNonRegularWriteOpenError(error, filePath, flags)) {
@@ -290,14 +295,13 @@ export async function appendRegularFile(options) {
290
295
  catch (error) {
291
296
  throwAppendIdentityError(error, filePath);
292
297
  }
293
- if (options.maxFileBytes !== undefined &&
294
- Number(identity.size) + contentBytes > options.maxFileBytes) {
298
+ if (maxFileBytes !== undefined &&
299
+ Number(identity.size) + contentBytes > maxFileBytes) {
295
300
  return;
296
301
  }
297
- const mode = options.mode ?? 0o600;
298
302
  // Tighten before writing; restore explicit special bits only after content is complete.
299
303
  await handle.chmod(mode);
300
- await handle.appendFile(options.content, options.encoding ?? "utf8");
304
+ await handle.appendFile(content, encoding);
301
305
  if (mode & 0o7000)
302
306
  await handle.chmod(mode);
303
307
  }
@@ -306,9 +310,8 @@ export async function appendRegularFile(options) {
306
310
  }
307
311
  }
308
312
  export function appendRegularFileSync(options) {
309
- const filePath = options.filePath;
310
- assertNoWindowsPathAlias(filePath, "filesystem", "file path uses a Windows filesystem namespace alias");
311
- if (options.rejectSymlinkParents === true) {
313
+ const { filePath, content, encoding, mode, maxFileBytes, rejectSymlinkParents } = captureAppendOptions(options);
314
+ if (rejectSymlinkParents === true) {
312
315
  const resolvedDir = resolvePathPreservingWindowsRoot(path.dirname(filePath));
313
316
  assertNoSymlinkParentsSync({
314
317
  rootDir: path.parse(resolvedDir).root,
@@ -321,33 +324,22 @@ export function appendRegularFileSync(options) {
321
324
  }
322
325
  let preOpenStat;
323
326
  try {
324
- preOpenStat = inspectFileIdentitySync(() => {
325
- const stat = fsSync.lstatSync(filePath, { bigint: true });
326
- if (stat.isSymbolicLink()) {
327
- throw new Error(`Refusing to append through symlink: ${filePath}`);
328
- }
329
- if (!stat.isFile()) {
330
- throw new Error(`Refusing to append to non-file: ${filePath}`);
331
- }
332
- return stat;
333
- });
327
+ preOpenStat = inspectFileIdentitySync(() => inspectAppendPath(filePath));
334
328
  }
335
329
  catch (error) {
336
330
  if (!isNotFoundPathError(error))
337
331
  throw error;
338
332
  }
339
- const contentBuffer = typeof options.content === "string"
340
- ? Buffer.from(options.content, options.encoding ?? "utf8")
341
- : Buffer.from(options.content);
342
- if (options.maxFileBytes !== undefined &&
343
- Number(preOpenStat?.size ?? 0n) + contentBuffer.byteLength > options.maxFileBytes) {
333
+ const contentBuffer = typeof content === "string" ? Buffer.from(content, encoding) : Buffer.from(content);
334
+ if (maxFileBytes !== undefined &&
335
+ Number(preOpenStat?.size ?? 0n) + contentBuffer.byteLength > maxFileBytes) {
344
336
  return;
345
337
  }
346
338
  getFsSafeTestHooks()?.beforeRegularFileAppendOpenSync?.(filePath);
347
339
  const flags = resolveRegularFileAppendFlags();
348
340
  let fd;
349
341
  try {
350
- fd = fsSync.openSync(filePath, flags, options.mode ?? 0o600);
342
+ fd = fsSync.openSync(filePath, flags, mode);
351
343
  }
352
344
  catch (error) {
353
345
  if (isNonRegularWriteOpenErrorSync(error, filePath, flags)) {
@@ -372,11 +364,10 @@ export function appendRegularFileSync(options) {
372
364
  catch (error) {
373
365
  throwAppendIdentityError(error, filePath);
374
366
  }
375
- if (options.maxFileBytes !== undefined &&
376
- Number(identity.size) + contentBuffer.byteLength > options.maxFileBytes) {
367
+ if (maxFileBytes !== undefined &&
368
+ Number(identity.size) + contentBuffer.byteLength > maxFileBytes) {
377
369
  return;
378
370
  }
379
- const mode = options.mode ?? 0o600;
380
371
  fsSync.fchmodSync(fd, mode);
381
372
  fsSync.appendFileSync(fd, contentBuffer);
382
373
  if (mode & 0o7000)
@@ -1 +1 @@
1
- {"version":3,"file":"replace-file-copy-fallback.d.ts","sourceRoot":"","sources":["../src/replace-file-copy-fallback.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,EAAE,EAAE,KAAK,WAAW,EAAc,MAAM,SAAS,CAAC;AAQ/D,MAAM,MAAM,oCAAoC,GAAG,QAAQ,CAAC;AAC5D,MAAM,MAAM,oCAAoC,GAAG,kBAAkB,GAAG,MAAM,CAAC;AAC/E,MAAM,MAAM,+BAA+B,GAAG,UAAU,GAAG,gBAAgB,CAAC;AAC5E,MAAM,MAAM,sCAAsC,GAAG;IACnD,OAAO,EAAE,+BAA+B,CAAC;CAC1C,CAAC;AAEF,KAAK,eAAe,GAAG;IACrB,KAAK,EAAE,cAAc,kBAAkB,EAAE,KAAK,CAAC;IAC/C,IAAI,EAAE,cAAc,kBAAkB,EAAE,IAAI,CAAC;IAC7C,EAAE,EAAE,cAAc,kBAAkB,EAAE,EAAE,CAAC;IACzC,MAAM,EAAE,cAAc,kBAAkB,EAAE,MAAM,CAAC;CAClD,CAAC;AAEF,KAAK,cAAc,GAAG,IAAI,CACxB,OAAO,MAAM,EACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,eAAe,GACf,WAAW,GACX,UAAU,GACV,UAAU,GACV,QAAQ,GACR,YAAY,GACZ,WAAW,CACd,CAAC;AAqHF,wBAAsB,+BAA+B,CACnD,QAAQ,EAAE,eAAe,EACzB,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,oCAAoC,GAC5C,OAAO,CAAC,IAAI,CAAC,CAIf;AAED,wBAAgB,mCAAmC,CACjD,QAAQ,EAAE,cAAc,EACxB,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,oCAAoC,GAC5C,IAAI,CAIN;AA+HD,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,QAAQ,EAAE,eAAe,CAAC;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,oBAAoB,CAAC,EAAE,oCAAoC,CAAC;IAC5D,OAAO,EAAE,oCAAoC,CAAC;IAC9C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,sBAAsB,CAAC,EAAE,WAAW,CAAC;IACrC,IAAI,EAAE,OAAO,CAAC;CACf,GAAG,OAAO,CAAC,IAAI,CAAC,CAuEhB;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,QAAQ,EAAE,cAAc,CAAC;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,oBAAoB,CAAC,EAAE,oCAAoC,CAAC;IAC5D,OAAO,EAAE,oCAAoC,CAAC;IAC9C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,sBAAsB,CAAC,EAAE,WAAW,CAAC;IACrC,UAAU,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,IAAI,EAAE,OAAO,CAAC;CACf,GAAG,IAAI,CAuEP"}
1
+ {"version":3,"file":"replace-file-copy-fallback.d.ts","sourceRoot":"","sources":["../src/replace-file-copy-fallback.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,EAAE,EAAE,KAAK,WAAW,EAAc,MAAM,SAAS,CAAC;AAS/D,MAAM,MAAM,oCAAoC,GAAG,QAAQ,CAAC;AAC5D,MAAM,MAAM,oCAAoC,GAAG,kBAAkB,GAAG,MAAM,CAAC;AAC/E,MAAM,MAAM,+BAA+B,GAAG,UAAU,GAAG,gBAAgB,CAAC;AAC5E,MAAM,MAAM,sCAAsC,GAAG;IACnD,OAAO,EAAE,+BAA+B,CAAC;CAC1C,CAAC;AAEF,KAAK,eAAe,GAAG;IACrB,KAAK,EAAE,cAAc,kBAAkB,EAAE,KAAK,CAAC;IAC/C,IAAI,EAAE,cAAc,kBAAkB,EAAE,IAAI,CAAC;IAC7C,EAAE,EAAE,cAAc,kBAAkB,EAAE,EAAE,CAAC;IACzC,MAAM,EAAE,cAAc,kBAAkB,EAAE,MAAM,CAAC;CAClD,CAAC;AAEF,KAAK,cAAc,GAAG,IAAI,CACxB,OAAO,MAAM,EACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,eAAe,GACf,WAAW,GACX,UAAU,GACV,UAAU,GACV,QAAQ,GACR,YAAY,GACZ,WAAW,CACd,CAAC;AAqHF,wBAAsB,+BAA+B,CACnD,QAAQ,EAAE,eAAe,EACzB,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,oCAAoC,GAC5C,OAAO,CAAC,IAAI,CAAC,CAIf;AAED,wBAAgB,mCAAmC,CACjD,QAAQ,EAAE,cAAc,EACxB,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,oCAAoC,GAC5C,IAAI,CAIN;AA+HD,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,QAAQ,EAAE,eAAe,CAAC;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,oBAAoB,CAAC,EAAE,oCAAoC,CAAC;IAC5D,OAAO,EAAE,oCAAoC,CAAC;IAC9C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,sBAAsB,CAAC,EAAE,WAAW,CAAC;IACrC,IAAI,EAAE,OAAO,CAAC;CACf,GAAG,OAAO,CAAC,IAAI,CAAC,CAuEhB;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,QAAQ,EAAE,cAAc,CAAC;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,oBAAoB,CAAC,EAAE,oCAAoC,CAAC;IAC5D,OAAO,EAAE,oCAAoC,CAAC;IAC9C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,sBAAsB,CAAC,EAAE,WAAW,CAAC;IACrC,UAAU,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,IAAI,EAAE,OAAO,CAAC;CACf,GAAG,IAAI,CAuEP"}
@@ -2,9 +2,10 @@ import syncFs, {} from "node:fs";
2
2
  import fs, {} from "node:fs/promises";
3
3
  import { readBoundedAsync, readBoundedSync } from "./bounded-read.js";
4
4
  import { FsSafeError } from "./errors.js";
5
- import { sameFileIdentity } from "./file-identity.js";
5
+ import { inspectFileIdentity, inspectFileIdentitySync } from "./strict-file-identity.js";
6
6
  import { readOwnedCopySource, readOwnedCopySourceSync } from "./replace-file-copy-source.js";
7
7
  import { resolveReadOpenFlags } from "./read-open-flags.js";
8
+ import { hasErrorCode } from "./file-cleanup.js";
8
9
  const SUPPORTS_NOFOLLOW = process.platform !== "win32" && "O_NOFOLLOW" in syncFs.constants;
9
10
  const NOFOLLOW = SUPPORTS_NOFOLLOW ? syncFs.constants.O_NOFOLLOW : 0;
10
11
  const OPEN_READ_FLAGS = resolveReadOpenFlags();
@@ -19,39 +20,37 @@ function closeSyncAfterAdmissionFailure(fsModule, fd, error) {
19
20
  }
20
21
  throw error;
21
22
  }
22
- function notFound(error) {
23
- return error.code === "ENOENT";
24
- }
25
- function assertPinnedDestination(pathname, opened, dest, hardlinks, admission) {
23
+ function admitDestinationKind(pathname, opened, dest, admission) {
26
24
  if (admission === "hardlinks") {
27
- if (pathname.isSymbolicLink() || !pathname.isFile() || !sameFileIdentity(pathname, opened)) {
25
+ if (pathname.isSymbolicLink() || !pathname.isFile()) {
28
26
  throw new FsSafeError("path-mismatch", `Atomic replace destination changed while opening: ${dest}`);
29
27
  }
30
- if (opened.nlink > 1) {
31
- throw new FsSafeError("hardlink", `Hardlinked atomic replace destination not allowed: ${dest}`);
32
- }
33
- return;
34
28
  }
35
- if (pathname.isSymbolicLink()) {
29
+ else if (pathname.isSymbolicLink()) {
36
30
  throw new FsSafeError("symlink", `Refusing copy fallback through symlink destination: ${dest}`);
37
31
  }
38
- if (!pathname.isFile() || !opened.isFile()) {
32
+ else if (!pathname.isFile() || !opened.isFile()) {
39
33
  throw new FsSafeError("not-file", `Copy fallback destination must be a regular file: ${dest}`);
40
34
  }
41
- if (!sameFileIdentity(pathname, opened)) {
42
- throw new FsSafeError("path-mismatch", `Copy fallback destination changed while opening: ${dest}`);
43
- }
44
- if (hardlinks === "reject" && opened.nlink > 1) {
45
- throw new FsSafeError("hardlink", `Hardlinked copy fallback destination not allowed: ${dest}`);
35
+ return pathname;
36
+ }
37
+ function assertDestinationLinks(opened, dest, admission, hardlinks) {
38
+ if ((admission === "hardlinks" || hardlinks === "reject") && opened.nlink > 1n) {
39
+ throw new FsSafeError("hardlink", `Hardlinked ${admission === "hardlinks" ? "atomic replace" : "copy fallback"} destination not allowed: ${dest}`);
46
40
  }
47
41
  }
42
+ function inspectPinnedDestinationSync(fsModule, fd, dest, admission, hardlinks) {
43
+ const opened = inspectFileIdentitySync(() => fsModule.fstatSync(fd, { bigint: true }));
44
+ inspectFileIdentitySync(() => admitDestinationKind(fsModule.lstatSync(dest, { bigint: true }), opened, dest, admission), opened);
45
+ assertDestinationLinks(opened, dest, admission, hardlinks);
46
+ }
48
47
  async function openPinnedDestination(fsModule, dest, admission, hardlinks) {
49
48
  let preview;
50
49
  try {
51
50
  preview = fsModule === fs ? syncFs.lstatSync(dest) : await fsModule.lstat(dest);
52
51
  }
53
52
  catch (error) {
54
- if (notFound(error))
53
+ if (hasErrorCode(error, "ENOENT"))
55
54
  return null;
56
55
  throw error;
57
56
  }
@@ -64,9 +63,13 @@ async function openPinnedDestination(fsModule, dest, admission, hardlinks) {
64
63
  }
65
64
  const handle = await fsModule.open(dest, admission === "restore" ? OPEN_READ_WRITE_FLAGS : OPEN_READ_FLAGS);
66
65
  try {
67
- const opened = fsModule === fs ? syncFs.fstatSync(handle.fd) : await handle.stat();
68
- const current = fsModule === fs ? syncFs.lstatSync(dest) : await fsModule.lstat(dest);
69
- assertPinnedDestination(current, opened, dest, hardlinks, admission);
66
+ if (fsModule === fs)
67
+ inspectPinnedDestinationSync(syncFs, handle.fd, dest, admission, hardlinks);
68
+ else {
69
+ const opened = await inspectFileIdentity(() => handle.stat({ bigint: true }));
70
+ await inspectFileIdentity(async () => admitDestinationKind(await fsModule.lstat(dest, { bigint: true }), opened, dest, admission), opened);
71
+ assertDestinationLinks(opened, dest, admission, hardlinks);
72
+ }
70
73
  return handle;
71
74
  }
72
75
  catch (error) {
@@ -80,7 +83,7 @@ function openPinnedDestinationSync(fsModule, dest, admission, hardlinks) {
80
83
  preview = fsModule.lstatSync(dest);
81
84
  }
82
85
  catch (error) {
83
- if (notFound(error))
86
+ if (hasErrorCode(error, "ENOENT"))
84
87
  return null;
85
88
  throw error;
86
89
  }
@@ -91,8 +94,7 @@ function openPinnedDestinationSync(fsModule, dest, admission, hardlinks) {
91
94
  }
92
95
  const fd = fsModule.openSync(dest, admission === "restore" ? OPEN_READ_WRITE_FLAGS : OPEN_READ_FLAGS);
93
96
  try {
94
- const opened = fsModule.fstatSync(fd);
95
- assertPinnedDestination(fsModule.lstatSync(dest), opened, dest, hardlinks, admission);
97
+ inspectPinnedDestinationSync(fsModule, fd, dest, admission, hardlinks);
96
98
  return fd;
97
99
  }
98
100
  catch (error) {
@@ -239,7 +241,7 @@ export async function copyFallbackReplace(params) {
239
241
  ? syncFs.lstatSync(params.dest) : await params.fsModule.lstat(params.dest);
240
242
  }
241
243
  catch (error) {
242
- if (!notFound(error))
244
+ if (!hasErrorCode(error, "ENOENT"))
243
245
  throw error;
244
246
  }
245
247
  if (destStat?.isSymbolicLink()) {
@@ -294,7 +296,7 @@ export function copyFallbackReplaceSync(params) {
294
296
  destStat = params.fsModule.lstatSync(params.dest);
295
297
  }
296
298
  catch (error) {
297
- if (!notFound(error))
299
+ if (!hasErrorCode(error, "ENOENT"))
298
300
  throw error;
299
301
  }
300
302
  if (destStat?.isSymbolicLink()) {
@@ -1 +1 @@
1
- {"version":3,"file":"replace-file-copy-source.d.ts","sourceRoot":"","sources":["../src/replace-file-copy-source.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,EAAE,EAAE,KAAK,WAAW,EAAc,MAAM,SAAS,CAAC;AAQ/D,KAAK,qBAAqB,GAAG,IAAI,CAAC,cAAc,kBAAkB,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC,CAAC;AACvF,KAAK,oBAAoB,GAAG,IAAI,CAC9B,OAAO,MAAM,EACb,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,UAAU,GAAG,UAAU,CAClE,CAAC;AA8DF,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,QAAQ,EAAE,qBAAqB,CAAC;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,gBAAgB,CAAC,EAAE,WAAW,CAAC;CAChC,GAAG,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAyBjD;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,gBAAgB,CAAC,EAAE,WAAW,CAAC;CAChC,GAAG;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAyBxC"}
1
+ {"version":3,"file":"replace-file-copy-source.d.ts","sourceRoot":"","sources":["../src/replace-file-copy-source.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,EAAE,EAAE,KAAK,WAAW,EAAc,MAAM,SAAS,CAAC;AAQ/D,KAAK,qBAAqB,GAAG,IAAI,CAAC,cAAc,kBAAkB,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC,CAAC;AACvF,KAAK,oBAAoB,GAAG,IAAI,CAC9B,OAAO,MAAM,EACb,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,UAAU,GAAG,UAAU,CAClE,CAAC;AA8DF,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,QAAQ,EAAE,qBAAqB,CAAC;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,gBAAgB,CAAC,EAAE,WAAW,CAAC;CAChC,GAAG,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAoBjD;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,gBAAgB,CAAC,EAAE,WAAW,CAAC;CAChC,GAAG;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAqBxC"}
@@ -3,8 +3,8 @@ import fs, {} from "node:fs/promises";
3
3
  import { readBoundedSync } from "./bounded-read.js";
4
4
  import { resolveReadOpenFlags } from "./read-open-flags.js";
5
5
  import { FsSafeError } from "./errors.js";
6
- import { sameFileIdentity } from "./file-identity.js";
7
6
  import { inspectFileIdentity, inspectFileIdentitySync } from "./strict-file-identity.js";
7
+ import { hasErrorCode } from "./file-cleanup.js";
8
8
  const OPEN_READ_FLAGS = resolveReadOpenFlags();
9
9
  function assertSourcePreview(source, src) {
10
10
  if (source.isSymbolicLink()) {
@@ -18,10 +18,10 @@ function assertSourcePreview(source, src) {
18
18
  }
19
19
  }
20
20
  function assertOpenedSource(opened, current, src) {
21
- if (!opened.isFile() || current.isSymbolicLink() || !sameFileIdentity(opened, current)) {
21
+ if (!opened.isFile() || current.isSymbolicLink()) {
22
22
  throw new FsSafeError("path-mismatch", `Copy fallback source changed while opening: ${src}`);
23
23
  }
24
- if (opened.nlink !== 1) {
24
+ if (opened.nlink !== 1n) {
25
25
  throw new FsSafeError("hardlink", `Hardlinked copy fallback source not allowed: ${src}`);
26
26
  }
27
27
  }
@@ -35,7 +35,7 @@ async function openSource(fsModule, src) {
35
35
  return await fsModule.open(src, OPEN_READ_FLAGS);
36
36
  }
37
37
  catch (error) {
38
- if (error.code === "ELOOP") {
38
+ if (hasErrorCode(error, "ELOOP")) {
39
39
  throw sourceSymlinkError(src, error);
40
40
  }
41
41
  throw error;
@@ -46,7 +46,7 @@ function openSourceSync(fsModule, src) {
46
46
  return fsModule.openSync(src, OPEN_READ_FLAGS);
47
47
  }
48
48
  catch (error) {
49
- if (error.code === "ELOOP") {
49
+ if (hasErrorCode(error, "ELOOP")) {
50
50
  throw sourceSymlinkError(src, error);
51
51
  }
52
52
  throw error;
@@ -65,17 +65,12 @@ export async function readOwnedCopySource(params) {
65
65
  ? syncFs.lstatSync(params.src) : await params.fsModule.lstat(params.src), params.src);
66
66
  const handle = await openSource(params.fsModule, params.src);
67
67
  try {
68
- if (params.expectedIdentity) {
69
- const exact = await inspectFileIdentity(() => params.fsModule === fs
70
- ? syncFs.fstatSync(handle.fd, { bigint: true }) : handle.stat({ bigint: true }), params.expectedIdentity);
71
- await inspectFileIdentity(() => params.fsModule === fs
72
- ? syncFs.lstatSync(params.src, { bigint: true }) : params.fsModule.lstat(params.src, { bigint: true }), exact);
73
- }
74
- const opened = params.fsModule === fs ? syncFs.fstatSync(handle.fd) : await handle.stat();
75
- const current = params.fsModule === fs
76
- ? syncFs.lstatSync(params.src) : await params.fsModule.lstat(params.src);
68
+ const opened = await inspectFileIdentity(() => params.fsModule === fs
69
+ ? syncFs.fstatSync(handle.fd, { bigint: true }) : handle.stat({ bigint: true }), params.expectedIdentity);
70
+ const current = await inspectFileIdentity(() => params.fsModule === fs
71
+ ? syncFs.lstatSync(params.src, { bigint: true }) : params.fsModule.lstat(params.src, { bigint: true }), opened);
77
72
  assertOpenedSource(opened, current, params.src);
78
- return { replacement: await handle.readFile(), mode: opened.mode };
73
+ return { replacement: await handle.readFile(), mode: Number(opened.mode) };
79
74
  }
80
75
  finally {
81
76
  await handle.close().catch(() => undefined);
@@ -85,14 +80,10 @@ export function readOwnedCopySourceSync(params) {
85
80
  assertSourcePreview(params.fsModule.lstatSync(params.src), params.src);
86
81
  const fd = openSourceSync(params.fsModule, params.src);
87
82
  try {
88
- if (params.expectedIdentity) {
89
- const exact = inspectFileIdentitySync(() => params.fsModule.fstatSync(fd, { bigint: true }), params.expectedIdentity);
90
- inspectFileIdentitySync(() => params.fsModule.lstatSync(params.src, { bigint: true }), exact);
91
- }
92
- const opened = params.fsModule.fstatSync(fd);
93
- const current = params.fsModule.lstatSync(params.src);
83
+ const opened = inspectFileIdentitySync(() => params.fsModule.fstatSync(fd, { bigint: true }), params.expectedIdentity);
84
+ const current = inspectFileIdentitySync(() => params.fsModule.lstatSync(params.src, { bigint: true }), opened);
94
85
  assertOpenedSource(opened, current, params.src);
95
- return { replacement: readAllSync(params.fsModule, fd, opened.size), mode: opened.mode };
86
+ return { replacement: readAllSync(params.fsModule, fd, Number(opened.size)), mode: Number(opened.mode) };
96
87
  }
97
88
  finally {
98
89
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"replace-file-descriptor.d.ts","sourceRoot":"","sources":["../src/replace-file-descriptor.ts"],"names":[],"mappings":"AACA,OAAO,MAAM,EAAE,EAAE,KAAK,WAAW,EAAc,MAAM,SAAS,CAAC;AAC/D,OAAO,EAAE,EAAE,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAIvD,OAAO,EAA0C,KAAK,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAE5G,KAAK,mBAAmB,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,GAAG,MAAM,GAAG,WAAW,CAAC,CAAC;AAC3E,KAAK,kBAAkB,GAAG,IAAI,CAC5B,OAAO,MAAM,EACb,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,UAAU,GAAG,eAAe,CACrF,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;AAE5D,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,IAAI,CAAC,OAAO,EAAE,EAAE,MAAM,CAAC,EACjC,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC,CAUf;AAED,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,IAAI,CAAC,OAAO,MAAM,EAAE,UAAU,GAAG,WAAW,GAAG,WAAW,CAAC,EACrE,OAAO,EAAE,MAAM,GACd,IAAI,CAgBN;AA2BD,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,4FAA4F;IAC5F,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,GAAG,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC,CA6B1C;AAED,wBAAsB,kBAAkB,CAAC,MAAM,EAAE;IAC/C,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,GAAG,OAAO,CAAC,IAAI,CAAC,CAOhB;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE;IAC7C,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB,GAAG,IAAI,CAeP;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE;IAC1C,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,WAAW,KAAK,IAAI,CAAC;CAC9C,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,WAAW,CAAA;CAAE,CAAC,CAuBzD;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE;IACxC,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,IAAI,EAAE,OAAO,CAAC;IACd,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,WAAW,KAAK,IAAI,CAAC;CAC9C,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,WAAW,CAAA;CAAE,CAoBxC"}
1
+ {"version":3,"file":"replace-file-descriptor.d.ts","sourceRoot":"","sources":["../src/replace-file-descriptor.ts"],"names":[],"mappings":"AACA,OAAO,MAAM,EAAE,EAAE,KAAK,WAAW,EAAc,MAAM,SAAS,CAAC;AAC/D,OAAO,EAAE,EAAE,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAGvD,OAAO,EAAoB,KAAK,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAEtF,KAAK,mBAAmB,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,GAAG,MAAM,GAAG,WAAW,CAAC,CAAC;AAC3E,KAAK,kBAAkB,GAAG,IAAI,CAC5B,OAAO,MAAM,EACb,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,UAAU,GAAG,eAAe,CACrF,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;AAE5D,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,IAAI,CAAC,OAAO,EAAE,EAAE,MAAM,CAAC,EACjC,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC,CAUf;AAED,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,IAAI,CAAC,OAAO,MAAM,EAAE,UAAU,GAAG,WAAW,GAAG,WAAW,CAAC,EACrE,OAAO,EAAE,MAAM,GACd,IAAI,CAgBN;AAkBD,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,4FAA4F;IAC5F,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,GAAG,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC,CA8B1C;AAED,wBAAsB,kBAAkB,CAAC,MAAM,EAAE;IAC/C,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,GAAG,OAAO,CAAC,IAAI,CAAC,CAOhB;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE;IAC7C,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB,GAAG,IAAI,CAcP;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE;IAC1C,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,WAAW,KAAK,IAAI,CAAC;CAC9C,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,WAAW,CAAA;CAAE,CAAC,CAuBzD;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE;IACxC,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,IAAI,EAAE,OAAO,CAAC;IACd,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,WAAW,KAAK,IAAI,CAAC;CAC9C,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,WAAW,CAAA;CAAE,CAoBxC"}