@openclaw/fs-safe 0.5.1 → 0.5.3

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 (231) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +19 -6
  3. package/dist/absolute-path.d.ts.map +1 -1
  4. package/dist/absolute-path.js +20 -12
  5. package/dist/archive-entry.d.ts +1 -0
  6. package/dist/archive-entry.d.ts.map +1 -1
  7. package/dist/archive-entry.js +37 -6
  8. package/dist/archive-errors.d.ts +1 -0
  9. package/dist/archive-errors.d.ts.map +1 -1
  10. package/dist/archive-errors.js +4 -0
  11. package/dist/archive-input.d.ts +7 -0
  12. package/dist/archive-input.d.ts.map +1 -1
  13. package/dist/archive-input.js +11 -1
  14. package/dist/archive-limits.js +1 -1
  15. package/dist/archive-native.d.ts.map +1 -1
  16. package/dist/archive-native.js +12 -6
  17. package/dist/archive-policy.d.ts.map +1 -1
  18. package/dist/archive-policy.js +2 -1
  19. package/dist/archive-read.d.ts.map +1 -1
  20. package/dist/archive-read.js +62 -37
  21. package/dist/archive-staging.d.ts.map +1 -1
  22. package/dist/archive-staging.js +3 -2
  23. package/dist/archive-tar-meta.d.ts.map +1 -1
  24. package/dist/archive-tar-meta.js +15 -1
  25. package/dist/archive-tar-runtime.d.ts +1 -0
  26. package/dist/archive-tar-runtime.d.ts.map +1 -1
  27. package/dist/archive-tar-runtime.js +11 -0
  28. package/dist/archive-tar.d.ts.map +1 -1
  29. package/dist/archive-tar.js +8 -4
  30. package/dist/archive-zip-entry.d.ts +7 -1
  31. package/dist/archive-zip-entry.d.ts.map +1 -1
  32. package/dist/archive-zip-entry.js +11 -1
  33. package/dist/archive-zip-integrity.d.ts +5 -0
  34. package/dist/archive-zip-integrity.d.ts.map +1 -0
  35. package/dist/archive-zip-integrity.js +54 -0
  36. package/dist/archive-zip-preflight.d.ts.map +1 -1
  37. package/dist/archive-zip-preflight.js +12 -1
  38. package/dist/archive.d.ts.map +1 -1
  39. package/dist/archive.js +9 -5
  40. package/dist/bounded-read-stream.d.ts.map +1 -1
  41. package/dist/bounded-read-stream.js +13 -2
  42. package/dist/deny-mutations.d.ts +1 -0
  43. package/dist/deny-mutations.d.ts.map +1 -1
  44. package/dist/deny-mutations.js +6 -39
  45. package/dist/device-path.d.ts +1 -0
  46. package/dist/device-path.d.ts.map +1 -1
  47. package/dist/device-path.js +6 -6
  48. package/dist/directory-guard.d.ts.map +1 -1
  49. package/dist/directory-guard.js +5 -4
  50. package/dist/error-detail.d.ts +2 -0
  51. package/dist/error-detail.d.ts.map +1 -0
  52. package/dist/error-detail.js +4 -0
  53. package/dist/errors.d.ts +1 -1
  54. package/dist/errors.d.ts.map +1 -1
  55. package/dist/errors.js +4 -0
  56. package/dist/file-hash.d.ts.map +1 -1
  57. package/dist/file-hash.js +4 -1
  58. package/dist/file-lock-sync.d.ts.map +1 -1
  59. package/dist/file-lock-sync.js +88 -35
  60. package/dist/file-store-boundary.d.ts +6 -0
  61. package/dist/file-store-boundary.d.ts.map +1 -1
  62. package/dist/file-store-boundary.js +20 -5
  63. package/dist/file-store-sync-write.d.ts +9 -0
  64. package/dist/file-store-sync-write.d.ts.map +1 -0
  65. package/dist/file-store-sync-write.js +97 -0
  66. package/dist/file-store.d.ts.map +1 -1
  67. package/dist/file-store.js +62 -139
  68. package/dist/filename.d.ts.map +1 -1
  69. package/dist/filename.js +27 -0
  70. package/dist/guarded-mkdir.d.ts.map +1 -1
  71. package/dist/guarded-mkdir.js +5 -6
  72. package/dist/install-path.js +1 -1
  73. package/dist/json-document-store.d.ts.map +1 -1
  74. package/dist/json-document-store.js +19 -13
  75. package/dist/json.d.ts.map +1 -1
  76. package/dist/json.js +1 -3
  77. package/dist/local-file-access.d.ts +3 -2
  78. package/dist/local-file-access.d.ts.map +1 -1
  79. package/dist/local-file-access.js +12 -6
  80. package/dist/local-roots.d.ts.map +1 -1
  81. package/dist/local-roots.js +35 -30
  82. package/dist/move-path.d.ts.map +1 -1
  83. package/dist/move-path.js +51 -12
  84. package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
  85. package/dist/native/darwin-x64/fs-safe-native.node +0 -0
  86. package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
  87. package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
  88. package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
  89. package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
  90. package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
  91. package/dist/native-binding.d.ts +1 -0
  92. package/dist/native-binding.d.ts.map +1 -1
  93. package/dist/native-operations.d.ts.map +1 -1
  94. package/dist/native-operations.js +14 -1
  95. package/dist/native-pinned-write.d.ts.map +1 -1
  96. package/dist/native-pinned-write.js +44 -17
  97. package/dist/output-sibling.d.ts.map +1 -1
  98. package/dist/output-sibling.js +6 -5
  99. package/dist/path-policy.d.ts.map +1 -1
  100. package/dist/path-policy.js +1 -7
  101. package/dist/path.d.ts.map +1 -1
  102. package/dist/path.js +4 -0
  103. package/dist/permission-exec.d.ts +6 -0
  104. package/dist/permission-exec.d.ts.map +1 -0
  105. package/dist/permission-exec.js +28 -0
  106. package/dist/permissions-windows.d.ts +56 -0
  107. package/dist/permissions-windows.d.ts.map +1 -0
  108. package/dist/permissions-windows.js +285 -0
  109. package/dist/permissions.d.ts +3 -46
  110. package/dist/permissions.d.ts.map +1 -1
  111. package/dist/permissions.js +17 -328
  112. package/dist/pinned-open.d.ts.map +1 -1
  113. package/dist/pinned-open.js +21 -4
  114. package/dist/pinned-write.js +2 -1
  115. package/dist/private-temp-workspace.d.ts.map +1 -1
  116. package/dist/private-temp-workspace.js +45 -4
  117. package/dist/read-error.d.ts +2 -0
  118. package/dist/read-error.d.ts.map +1 -0
  119. package/dist/read-error.js +11 -0
  120. package/dist/regular-file.d.ts.map +1 -1
  121. package/dist/regular-file.js +21 -2
  122. package/dist/replace-directory.d.ts.map +1 -1
  123. package/dist/replace-directory.js +10 -1
  124. package/dist/replace-file-copy-fallback.d.ts +3 -0
  125. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  126. package/dist/replace-file-copy-fallback.js +18 -11
  127. package/dist/replace-file-descriptor.d.ts +33 -0
  128. package/dist/replace-file-descriptor.d.ts.map +1 -0
  129. package/dist/replace-file-descriptor.js +96 -0
  130. package/dist/replace-file.d.ts +9 -2
  131. package/dist/replace-file.d.ts.map +1 -1
  132. package/dist/replace-file.js +64 -72
  133. package/dist/root-context.d.ts +8 -0
  134. package/dist/root-context.d.ts.map +1 -1
  135. package/dist/root-context.js +43 -2
  136. package/dist/root-errors.d.ts +7 -0
  137. package/dist/root-errors.d.ts.map +1 -1
  138. package/dist/root-errors.js +46 -1
  139. package/dist/root-file.d.ts +1 -0
  140. package/dist/root-file.d.ts.map +1 -1
  141. package/dist/root-file.js +2 -0
  142. package/dist/root-impl.d.ts.map +1 -1
  143. package/dist/root-impl.js +216 -169
  144. package/dist/root-path-symlink.d.ts +3 -0
  145. package/dist/root-path-symlink.d.ts.map +1 -0
  146. package/dist/root-path-symlink.js +35 -0
  147. package/dist/root-path.d.ts +1 -0
  148. package/dist/root-path.d.ts.map +1 -1
  149. package/dist/root-path.js +129 -190
  150. package/dist/root-walk.d.ts.map +1 -1
  151. package/dist/root-walk.js +15 -0
  152. package/dist/safe-path-segment.d.ts +2 -0
  153. package/dist/safe-path-segment.d.ts.map +1 -1
  154. package/dist/safe-path-segment.js +12 -0
  155. package/dist/secret-file.d.ts.map +1 -1
  156. package/dist/secret-file.js +29 -6
  157. package/dist/secret-read-async.d.ts.map +1 -1
  158. package/dist/secret-read-async.js +5 -1
  159. package/dist/secure-temp-dir.d.ts.map +1 -1
  160. package/dist/secure-temp-dir.js +6 -1
  161. package/dist/short-path.d.ts +2 -0
  162. package/dist/short-path.d.ts.map +1 -0
  163. package/dist/short-path.js +7 -0
  164. package/dist/sidecar-lock-acquire.d.ts +30 -0
  165. package/dist/sidecar-lock-acquire.d.ts.map +1 -0
  166. package/dist/sidecar-lock-acquire.js +260 -0
  167. package/dist/sidecar-lock-policy.d.ts +3 -0
  168. package/dist/sidecar-lock-policy.d.ts.map +1 -1
  169. package/dist/sidecar-lock-policy.js +20 -3
  170. package/dist/sidecar-lock-reclaim.d.ts +5 -1
  171. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  172. package/dist/sidecar-lock-reclaim.js +59 -9
  173. package/dist/sidecar-lock.d.ts.map +1 -1
  174. package/dist/sidecar-lock.js +27 -226
  175. package/dist/symlink-parents.d.ts.map +1 -1
  176. package/dist/symlink-parents.js +9 -8
  177. package/dist/temp-target.d.ts.map +1 -1
  178. package/dist/temp-target.js +15 -4
  179. package/dist/test-hooks.d.ts +1 -0
  180. package/dist/test-hooks.d.ts.map +1 -1
  181. package/dist/timing.d.ts +2 -0
  182. package/dist/timing.d.ts.map +1 -1
  183. package/dist/timing.js +9 -0
  184. package/dist/trash.d.ts.map +1 -1
  185. package/dist/trash.js +10 -1
  186. package/dist/walk.d.ts.map +1 -1
  187. package/dist/walk.js +15 -0
  188. package/docs/advanced.md +7 -1
  189. package/docs/archive.md +27 -20
  190. package/docs/assets/readme-banner.jpg +0 -0
  191. package/docs/atomic.md +48 -19
  192. package/docs/errors.md +34 -8
  193. package/docs/file-store.md +14 -1
  194. package/docs/filename.md +17 -16
  195. package/docs/index.md +1 -0
  196. package/docs/install-path.md +9 -6
  197. package/docs/json.md +8 -2
  198. package/docs/local-roots.md +79 -103
  199. package/docs/native-helper.md +11 -5
  200. package/docs/native.md +2 -2
  201. package/docs/output.md +8 -3
  202. package/docs/path-scope.md +4 -1
  203. package/docs/path.md +7 -3
  204. package/docs/permissions.md +8 -0
  205. package/docs/public-api.md +109 -0
  206. package/docs/quickstart.md +6 -2
  207. package/docs/reading.md +13 -10
  208. package/docs/root.md +26 -4
  209. package/docs/secret-file.md +21 -4
  210. package/docs/security-model.md +21 -4
  211. package/docs/sidecar-lock.md +15 -5
  212. package/docs/temp.md +42 -5
  213. package/docs/test-hooks.md +2 -0
  214. package/docs/types.md +32 -15
  215. package/docs/writing.md +25 -12
  216. package/package.json +15 -4
  217. package/dist/archive-file-io.d.ts +0 -9
  218. package/dist/archive-file-io.d.ts.map +0 -1
  219. package/dist/archive-file-io.js +0 -11
  220. package/dist/file-store-source.d.ts +0 -5
  221. package/dist/file-store-source.d.ts.map +0 -1
  222. package/dist/file-store-source.js +0 -30
  223. package/dist/file-url.d.ts +0 -2
  224. package/dist/file-url.d.ts.map +0 -1
  225. package/dist/file-url.js +0 -1
  226. package/dist/path-stat.d.ts +0 -4
  227. package/dist/path-stat.d.ts.map +0 -1
  228. package/dist/path-stat.js +0 -15
  229. package/dist/windows-permissions-native.d.ts +0 -9
  230. package/dist/windows-permissions-native.d.ts.map +0 -1
  231. package/dist/windows-permissions-native.js +0 -31
@@ -189,15 +189,18 @@ function restoreFailure(writeError, cleanup, restoreError) {
189
189
  ? `Atomic copy fallback failed; original destination restored: ${primary.message}`
190
190
  : `Atomic copy fallback failed and original restoration failed: ${primary.message}`, { cause, details });
191
191
  }
192
- async function replacePinnedWithRestore(handle, replacement, maxRestoreBytes) {
192
+ async function replacePinnedWithRestore(handle, replacement, maxRestoreBytes, replacementMode) {
193
+ const originalMode = (await handle.stat()).mode;
193
194
  const original = await readBounded(handle, maxRestoreBytes);
194
195
  try {
195
196
  await writeAll(handle, replacement);
197
+ await handle.chmod(replacementMode);
196
198
  await handle.sync();
197
199
  }
198
200
  catch (writeError) {
199
201
  try {
200
202
  await writeAll(handle, original);
203
+ await handle.chmod(originalMode);
201
204
  await handle.sync();
202
205
  throw restoreFailure(writeError, "restored");
203
206
  }
@@ -209,15 +212,18 @@ async function replacePinnedWithRestore(handle, replacement, maxRestoreBytes) {
209
212
  }
210
213
  }
211
214
  }
212
- function replacePinnedWithRestoreSync(fsModule, fd, replacement, maxRestoreBytes) {
215
+ function replacePinnedWithRestoreSync(fsModule, fd, replacement, maxRestoreBytes, replacementMode, fchmodSync) {
216
+ const originalMode = fsModule.fstatSync(fd).mode;
213
217
  const original = readBoundedSync(fsModule, fd, maxRestoreBytes);
214
218
  try {
215
219
  writeAllSync(fsModule, fd, replacement);
220
+ fchmodSync?.(fd, replacementMode);
216
221
  fsModule.fsyncSync(fd);
217
222
  }
218
223
  catch (writeError) {
219
224
  try {
220
225
  writeAllSync(fsModule, fd, original);
226
+ fchmodSync?.(fd, originalMode);
221
227
  fsModule.fsyncSync(fd);
222
228
  throw restoreFailure(writeError, "restored");
223
229
  }
@@ -247,7 +253,7 @@ export async function copyFallbackReplace(params) {
247
253
  const pinned = await openPinnedDestination(params.fsModule, params.dest, params.destinationHardlinks);
248
254
  if (pinned) {
249
255
  destHandle = pinned.handle;
250
- await replacePinnedWithRestore(destHandle, replacement, params.maxRestoreBytes);
256
+ await replacePinnedWithRestore(destHandle, replacement, params.maxRestoreBytes, sourceStat.mode);
251
257
  }
252
258
  }
253
259
  if (!destHandle) {
@@ -265,13 +271,16 @@ export async function copyFallbackReplace(params) {
265
271
  }
266
272
  destHandle = await params.fsModule.open(params.dest, OPEN_WRITE_EXCLUSIVE_FLAGS, sourceStat.mode & 0o777);
267
273
  await destHandle.writeFile(replacement);
274
+ await destHandle.chmod(sourceStat.mode);
275
+ if (params.sync) {
276
+ await destHandle.sync();
277
+ }
268
278
  }
269
279
  }
270
280
  finally {
271
281
  await destHandle?.close().catch(() => undefined);
272
282
  await sourceHandle.close().catch(() => undefined);
273
283
  }
274
- await params.fsModule.unlink(params.src).catch(() => undefined);
275
284
  }
276
285
  export function copyFallbackReplaceSync(params) {
277
286
  const sourcePreview = params.fsModule.lstatSync(params.src);
@@ -291,7 +300,7 @@ export function copyFallbackReplaceSync(params) {
291
300
  const pinned = openPinnedDestinationSync(params.fsModule, params.dest, params.destinationHardlinks);
292
301
  if (pinned) {
293
302
  destFd = pinned.fd;
294
- replacePinnedWithRestoreSync(params.fsModule, destFd, replacement, params.maxRestoreBytes);
303
+ replacePinnedWithRestoreSync(params.fsModule, destFd, replacement, params.maxRestoreBytes, sourceStat.mode, params.fchmodSync);
295
304
  }
296
305
  }
297
306
  if (destFd === undefined) {
@@ -312,6 +321,10 @@ export function copyFallbackReplaceSync(params) {
312
321
  }
313
322
  destFd = params.fsModule.openSync(params.dest, OPEN_WRITE_EXCLUSIVE_FLAGS, sourceStat.mode & 0o777);
314
323
  writeAllSync(params.fsModule, destFd, replacement);
324
+ params.fchmodSync?.(destFd, sourceStat.mode);
325
+ if (params.sync) {
326
+ params.fsModule.fsyncSync(destFd);
327
+ }
315
328
  }
316
329
  }
317
330
  finally {
@@ -330,10 +343,4 @@ export function copyFallbackReplaceSync(params) {
330
343
  // Best-effort close after fallback replacement.
331
344
  }
332
345
  }
333
- try {
334
- params.fsModule.unlinkSync(params.src);
335
- }
336
- catch {
337
- // Best-effort cleanup after fallback replacement.
338
- }
339
346
  }
@@ -0,0 +1,33 @@
1
+ import syncFs, { type Stats } from "node:fs";
2
+ import fs from "node:fs/promises";
3
+ type AsyncTempFileSystem = Pick<typeof fs, "lstat" | "open" | "writeFile">;
4
+ type SyncTempFileSystem = Pick<typeof syncFs, "closeSync" | "fstatSync" | "fsyncSync" | "lstatSync" | "openSync" | "writeFileSync">;
5
+ export type SyncFchmod = (fd: number, mode: number) => void;
6
+ export declare function applyDirectoryMode(params: {
7
+ fsModule: AsyncTempFileSystem;
8
+ dirPath: string;
9
+ mode: number;
10
+ }): Promise<void>;
11
+ export declare function applyDirectoryModeSync(params: {
12
+ fsModule: SyncTempFileSystem;
13
+ dirPath: string;
14
+ mode: number;
15
+ fchmodSync?: SyncFchmod;
16
+ }): void;
17
+ export declare function writeTempFile(params: {
18
+ fsModule: AsyncTempFileSystem;
19
+ tempPath: string;
20
+ content: string | Uint8Array;
21
+ mode: number;
22
+ sync: boolean;
23
+ }): Promise<Stats>;
24
+ export declare function writeTempFileSync(params: {
25
+ fsModule: SyncTempFileSystem;
26
+ tempPath: string;
27
+ content: string | Uint8Array;
28
+ mode: number;
29
+ fchmodSync?: SyncFchmod;
30
+ sync: boolean;
31
+ }): Stats;
32
+ export {};
33
+ //# sourceMappingURL=replace-file-descriptor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replace-file-descriptor.d.ts","sourceRoot":"","sources":["../src/replace-file-descriptor.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,EAAE,EAAE,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,EAAuB,MAAM,kBAAkB,CAAC;AAIvD,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;AA2B5D,wBAAsB,kBAAkB,CAAC,MAAM,EAAE;IAC/C,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBhB;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;CACf,GAAG,OAAO,CAAC,KAAK,CAAC,CAkBjB;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;CACf,GAAG,KAAK,CAkBR"}
@@ -0,0 +1,96 @@
1
+ import syncFs, {} from "node:fs";
2
+ import fs, {} from "node:fs/promises";
3
+ import { FsSafeError } from "./errors.js";
4
+ import { sameFileIdentity } from "./file-identity.js";
5
+ function directoryOpenFlags() {
6
+ return (syncFs.constants.O_RDONLY |
7
+ syncFs.constants.O_DIRECTORY |
8
+ syncFs.constants.O_NOFOLLOW |
9
+ syncFs.constants.O_NONBLOCK);
10
+ }
11
+ function assertDirectory(identity, dirPath) {
12
+ if (identity.isSymbolicLink() || !identity.isDirectory()) {
13
+ throw new FsSafeError("not-file", `Atomic replace parent must be a real directory: ${dirPath}`);
14
+ }
15
+ }
16
+ function assertSameDirectory(expected, opened, dirPath) {
17
+ assertDirectory(opened, dirPath);
18
+ if (!sameFileIdentity(expected, opened)) {
19
+ throw new FsSafeError("path-mismatch", `Atomic replace parent changed before its mode could be applied: ${dirPath}`);
20
+ }
21
+ }
22
+ export async function applyDirectoryMode(params) {
23
+ // Node does not enforce POSIX directory modes on Windows, and its directory
24
+ // descriptors are not consistently openable. mkdir(mode) remains the only
25
+ // bounded behavior there; never fall back to a pathname chmod.
26
+ if (process.platform === "win32") {
27
+ return;
28
+ }
29
+ const expected = await params.fsModule.lstat(params.dirPath);
30
+ assertDirectory(expected, params.dirPath);
31
+ const handle = await params.fsModule.open(params.dirPath, directoryOpenFlags());
32
+ try {
33
+ assertSameDirectory(expected, await handle.stat(), params.dirPath);
34
+ await handle.chmod(params.mode);
35
+ }
36
+ finally {
37
+ await handle.close();
38
+ }
39
+ }
40
+ export function applyDirectoryModeSync(params) {
41
+ if (process.platform === "win32") {
42
+ return;
43
+ }
44
+ const expected = params.fsModule.lstatSync(params.dirPath);
45
+ assertDirectory(expected, params.dirPath);
46
+ const fd = params.fsModule.openSync(params.dirPath, directoryOpenFlags());
47
+ try {
48
+ assertSameDirectory(expected, params.fsModule.fstatSync(fd), params.dirPath);
49
+ params.fchmodSync?.(fd, params.mode);
50
+ }
51
+ finally {
52
+ params.fsModule.closeSync(fd);
53
+ }
54
+ }
55
+ export async function writeTempFile(params) {
56
+ const handle = await params.fsModule.open(params.tempPath, "wx", params.mode);
57
+ try {
58
+ await params.fsModule.writeFile(handle, params.content);
59
+ await handle.chmod(params.mode);
60
+ if (params.sync) {
61
+ try {
62
+ await handle.sync();
63
+ }
64
+ catch (error) {
65
+ if (error.code !== "EPERM") {
66
+ throw error;
67
+ }
68
+ }
69
+ }
70
+ return await handle.stat();
71
+ }
72
+ finally {
73
+ await handle.close();
74
+ }
75
+ }
76
+ export function writeTempFileSync(params) {
77
+ const fd = params.fsModule.openSync(params.tempPath, "wx", params.mode);
78
+ try {
79
+ params.fsModule.writeFileSync(fd, params.content);
80
+ params.fchmodSync?.(fd, params.mode);
81
+ if (params.sync) {
82
+ try {
83
+ params.fsModule.fsyncSync(fd);
84
+ }
85
+ catch (error) {
86
+ if (error.code !== "EPERM") {
87
+ throw error;
88
+ }
89
+ }
90
+ }
91
+ return params.fsModule.fstatSync(fd);
92
+ }
93
+ finally {
94
+ params.fsModule.closeSync(fd);
95
+ }
96
+ }
@@ -2,9 +2,16 @@ import syncFs from "node:fs";
2
2
  import fs from "node:fs/promises";
3
3
  import { type ReplaceFileAtomicRestoreCleanup, type ReplaceFileAtomicRestoreFailureDetails, type ReplaceFileCopyFallbackRestorePolicy, type ReplaceFileDestinationHardlinkPolicy } from "./replace-file-copy-fallback.js";
4
4
  export type ReplaceFileAtomicFileSystem = {
5
- promises: Pick<typeof fs, "mkdir" | "chmod" | "writeFile" | "rename" | "copyFile" | "unlink" | "rm" | "open" | "stat" | "lstat">;
5
+ promises: Pick<typeof fs, "mkdir" | "writeFile" | "rename" | "copyFile" | "unlink" | "rm" | "open" | "stat" | "lstat"> & {
6
+ /** @deprecated Accepted for adapter compatibility but never called. */
7
+ chmod?: typeof fs.chmod;
8
+ };
9
+ };
10
+ export type ReplaceFileAtomicSyncFileSystem = Pick<typeof syncFs, "mkdirSync" | "readFileSync" | "writeFileSync" | "renameSync" | "copyFileSync" | "unlinkSync" | "rmSync" | "openSync" | "fsyncSync" | "closeSync" | "fstatSync" | "statSync" | "lstatSync" | "ftruncateSync" | "readSync" | "writeSync"> & {
11
+ /** @deprecated Accepted for adapter compatibility but never called. */
12
+ chmodSync?: typeof syncFs.chmodSync;
13
+ fchmodSync?: typeof syncFs.fchmodSync;
6
14
  };
7
- export type ReplaceFileAtomicSyncFileSystem = Pick<typeof syncFs, "mkdirSync" | "chmodSync" | "readFileSync" | "writeFileSync" | "renameSync" | "copyFileSync" | "unlinkSync" | "rmSync" | "openSync" | "fsyncSync" | "closeSync" | "fstatSync" | "statSync" | "lstatSync" | "ftruncateSync" | "readSync" | "writeSync">;
8
15
  export type { ReplaceFileAtomicRestoreCleanup, ReplaceFileAtomicRestoreFailureDetails, ReplaceFileCopyFallbackRestorePolicy, ReplaceFileDestinationHardlinkPolicy, };
9
16
  type ReplaceFileAtomicBaseOptions = {
10
17
  filePath: string;
@@ -1 +1 @@
1
- {"version":3,"file":"replace-file.d.ts","sourceRoot":"","sources":["../src/replace-file.ts"],"names":[],"mappings":"AACA,OAAO,MAAM,MAAM,SAAS,CAAC;AAE7B,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAElC,OAAO,EAKL,KAAK,+BAA+B,EACpC,KAAK,sCAAsC,EAC3C,KAAK,oCAAoC,EACzC,KAAK,oCAAoC,EAC1C,MAAM,iCAAiC,CAAC;AAKzC,MAAM,MAAM,2BAA2B,GAAG;IACxC,QAAQ,EAAE,IAAI,CACZ,OAAO,EAAE,EACP,OAAO,GACP,OAAO,GACP,WAAW,GACX,QAAQ,GACR,UAAU,GACV,QAAQ,GACR,IAAI,GACJ,MAAM,GACN,MAAM,GACN,OAAO,CACV,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,+BAA+B,GAAG,IAAI,CAChD,OAAO,MAAM,EACX,WAAW,GACX,WAAW,GACX,cAAc,GACd,eAAe,GACf,YAAY,GACZ,cAAc,GACd,YAAY,GACZ,QAAQ,GACR,UAAU,GACV,WAAW,GACX,WAAW,GACX,WAAW,GACX,UAAU,GACV,WAAW,GACX,eAAe,GACf,UAAU,GACV,WAAW,CACd,CAAC;AAEF,YAAY,EACV,+BAA+B,EAC/B,sCAAsC,EACtC,oCAAoC,EACpC,oCAAoC,GACrC,CAAC;AAEF,KAAK,4BAA4B,GAAG;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,6BAA6B,CAAC,EAAE,OAAO,CAAC;IACxC,mBAAmB,CAAC,EAAE,oCAAoC,CAAC;IAC3D,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oBAAoB,CAAC,EAAE,oCAAoC,CAAC;IAC5D,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG,4BAA4B,GAAG;IACpE,UAAU,CAAC,EAAE,2BAA2B,CAAC;IACzC,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAClF,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG,4BAA4B,GAAG;IACxE,UAAU,CAAC,EAAE,+BAA+B,CAAC;IAC7C,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CACzE,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,MAAM,EAAE,QAAQ,GAAG,eAAe,CAAC;CACpC,CAAC;AAoOF,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,uBAAuB,CAAC,CAOlC;AA6DD,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,4BAA4B,GACpC,uBAAuB,CAsEzB"}
1
+ {"version":3,"file":"replace-file.d.ts","sourceRoot":"","sources":["../src/replace-file.ts"],"names":[],"mappings":"AACA,OAAO,MAAM,MAAM,SAAS,CAAC;AAE7B,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAElC,OAAO,EAKL,KAAK,+BAA+B,EACpC,KAAK,sCAAsC,EAC3C,KAAK,oCAAoC,EACzC,KAAK,oCAAoC,EAC1C,MAAM,iCAAiC,CAAC;AAazC,MAAM,MAAM,2BAA2B,GAAG;IACxC,QAAQ,EAAE,IAAI,CACZ,OAAO,EAAE,EACP,OAAO,GACP,WAAW,GACX,QAAQ,GACR,UAAU,GACV,QAAQ,GACR,IAAI,GACJ,MAAM,GACN,MAAM,GACN,OAAO,CACV,GAAG;QACF,uEAAuE;QACvE,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC,KAAK,CAAC;KACzB,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,+BAA+B,GAAG,IAAI,CAChD,OAAO,MAAM,EACX,WAAW,GACX,cAAc,GACd,eAAe,GACf,YAAY,GACZ,cAAc,GACd,YAAY,GACZ,QAAQ,GACR,UAAU,GACV,WAAW,GACX,WAAW,GACX,WAAW,GACX,UAAU,GACV,WAAW,GACX,eAAe,GACf,UAAU,GACV,WAAW,CACd,GAAG;IACF,uEAAuE;IACvE,SAAS,CAAC,EAAE,OAAO,MAAM,CAAC,SAAS,CAAC;IACpC,UAAU,CAAC,EAAE,OAAO,MAAM,CAAC,UAAU,CAAC;CACvC,CAAC;AAEF,YAAY,EACV,+BAA+B,EAC/B,sCAAsC,EACtC,oCAAoC,EACpC,oCAAoC,GACrC,CAAC;AAEF,KAAK,4BAA4B,GAAG;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,6BAA6B,CAAC,EAAE,OAAO,CAAC;IACxC,mBAAmB,CAAC,EAAE,oCAAoC,CAAC;IAC3D,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oBAAoB,CAAC,EAAE,oCAAoC,CAAC;IAC5D,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG,4BAA4B,GAAG;IACpE,UAAU,CAAC,EAAE,2BAA2B,CAAC;IACzC,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAClF,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG,4BAA4B,GAAG;IACxE,UAAU,CAAC,EAAE,+BAA+B,CAAC;IAC7C,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CACzE,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,MAAM,EAAE,QAAQ,GAAG,eAAe,CAAC;CACpC,CAAC;AAgNF,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,uBAAuB,CAAC,CAOlC;AAkED,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,4BAA4B,GACpC,uBAAuB,CAoFzB"}
@@ -3,8 +3,10 @@ import syncFs from "node:fs";
3
3
  import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { assertDestinationHardlinkPolicy, assertDestinationHardlinkPolicySync, copyFallbackReplace, copyFallbackReplaceSync, } from "./replace-file-copy-fallback.js";
6
+ import { applyDirectoryMode, applyDirectoryModeSync, writeTempFile, writeTempFileSync, } from "./replace-file-descriptor.js";
6
7
  import { assertSafePathPrefix } from "./safe-path-segment.js";
7
8
  import { registerTempPathForExit } from "./temp-cleanup.js";
9
+ import { sleep, sleepSync } from "./timing.js";
8
10
  import { serializePathWrite } from "./write-queue.js";
9
11
  function isRetryableRenameError(error) {
10
12
  return error.code === "EBUSY";
@@ -13,9 +15,6 @@ function isPermissionRenameError(error) {
13
15
  const code = error.code;
14
16
  return code === "EPERM" || code === "EEXIST";
15
17
  }
16
- async function sleep(ms) {
17
- await new Promise((resolve) => setTimeout(resolve, ms));
18
- }
19
18
  async function renameWithRetry(params) {
20
19
  for (let attempt = 0; attempt <= params.maxRetries; attempt++) {
21
20
  try {
@@ -35,6 +34,7 @@ async function renameWithRetry(params) {
35
34
  destinationHardlinks: params.destinationHardlinks,
36
35
  restore: params.copyFallbackRestore,
37
36
  maxRestoreBytes: params.maxRestoreBytes,
37
+ sync: params.syncFallback,
38
38
  });
39
39
  return { method: "copy-fallback" };
40
40
  }
@@ -43,12 +43,6 @@ async function renameWithRetry(params) {
43
43
  }
44
44
  throw new Error("Atomic rename retry loop exhausted.");
45
45
  }
46
- function sleepSync(ms) {
47
- if (ms <= 0) {
48
- return;
49
- }
50
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
51
- }
52
46
  function renameWithRetrySync(params) {
53
47
  for (let attempt = 0; attempt <= params.maxRetries; attempt++) {
54
48
  try {
@@ -68,6 +62,8 @@ function renameWithRetrySync(params) {
68
62
  destinationHardlinks: params.destinationHardlinks,
69
63
  restore: params.copyFallbackRestore,
70
64
  maxRestoreBytes: params.maxRestoreBytes,
65
+ fchmodSync: params.fchmodSync,
66
+ sync: params.syncFallback,
71
67
  });
72
68
  return { method: "copy-fallback" };
73
69
  }
@@ -126,33 +122,8 @@ function resolveModeSync(options) {
126
122
  }
127
123
  return stat ? stat.mode : defaultMode;
128
124
  }
129
- async function syncTempFile(fsModule, tempPath) {
130
- const handle = await fsModule.open(tempPath, "r+");
131
- try {
132
- await handle.sync();
133
- }
134
- catch (error) {
135
- if (error.code !== "EPERM") {
136
- throw error;
137
- }
138
- }
139
- finally {
140
- await handle.close();
141
- }
142
- }
143
- function syncTempFileSync(fsModule, tempPath) {
144
- const fd = fsModule.openSync(tempPath, "r+");
145
- try {
146
- fsModule.fsyncSync(fd);
147
- }
148
- catch (error) {
149
- if (error.code !== "EPERM") {
150
- throw error;
151
- }
152
- }
153
- finally {
154
- fsModule.closeSync(fd);
155
- }
125
+ function missingFchmodSyncError() {
126
+ return new TypeError("fileSystem.fchmodSync is required when mode, dirMode, or preserveExistingMode is specified");
156
127
  }
157
128
  async function syncDirectoryBestEffort(fsModule, dirPath) {
158
129
  let handle;
@@ -191,9 +162,15 @@ async function cleanupTempFile(params) {
191
162
  const cleanupError = await params.fsModule
192
163
  .rm(params.tempPath, { force: true })
193
164
  .catch((error) => error);
194
- if (cleanupError && params.throwOnCleanupError && params.originalError !== undefined) {
195
- throw new Error(`Atomic file replace failed (${String(params.originalError)}); cleanup also failed (${String(cleanupError)})`, { cause: params.originalError });
165
+ if (!cleanupError)
166
+ return true;
167
+ if (params.throwOnCleanupError) {
168
+ if (params.originalError !== undefined) {
169
+ throw new Error(`Atomic file replace failed (${String(params.originalError)}); cleanup also failed (${String(cleanupError)})`, { cause: params.originalError });
170
+ }
171
+ throw cleanupError;
196
172
  }
173
+ return false;
197
174
  }
198
175
  export async function replaceFileAtomic(options) {
199
176
  const filePath = options.filePath;
@@ -213,15 +190,17 @@ async function replaceFileAtomicUnserialized(options) {
213
190
  const unregisterTempPath = registerTempPathForExit(tempPath);
214
191
  let tempExists = false;
215
192
  let originalError;
216
- await fsModule.mkdir(dir, { recursive: true, mode: dirMode });
217
- await fsModule.chmod(dir, dirMode).catch(() => undefined);
218
193
  try {
194
+ await fsModule.mkdir(dir, { recursive: true, mode: dirMode });
195
+ await applyDirectoryMode({ fsModule, dirPath: dir, mode: dirMode });
219
196
  tempExists = true;
220
- await fsModule.writeFile(tempPath, options.content, { mode, flag: "wx" });
221
- unregisterTempPath.setIdentity(await fsModule.lstat(tempPath));
222
- if (options.syncTempFile) {
223
- await syncTempFile(fsModule, tempPath);
224
- }
197
+ unregisterTempPath.setIdentity(await writeTempFile({
198
+ fsModule,
199
+ tempPath,
200
+ content: options.content,
201
+ mode,
202
+ sync: options.syncTempFile === true,
203
+ }));
225
204
  if (options.beforeRename) {
226
205
  await options.beforeRename({ filePath, tempPath });
227
206
  }
@@ -236,10 +215,12 @@ async function replaceFileAtomicUnserialized(options) {
236
215
  copyFallbackRestore: options.copyFallbackRestore ?? "none",
237
216
  maxRestoreBytes: options.maxRestoreBytes,
238
217
  destinationHardlinks: options.destinationHardlinks,
218
+ syncFallback: options.syncTempFile === true,
239
219
  });
240
- tempExists = false;
241
- unregisterTempPath();
242
- await fsModule.chmod(filePath, mode).catch(() => undefined);
220
+ if (result.method === "rename") {
221
+ tempExists = false;
222
+ unregisterTempPath();
223
+ }
243
224
  if (options.syncParentDir) {
244
225
  await syncDirectoryBestEffort(fsModule, dir);
245
226
  }
@@ -250,15 +231,17 @@ async function replaceFileAtomicUnserialized(options) {
250
231
  throw error;
251
232
  }
252
233
  finally {
234
+ let tempRemoved = !tempExists;
253
235
  if (tempExists) {
254
- await cleanupTempFile({
236
+ tempRemoved = await cleanupTempFile({
255
237
  fsModule,
256
238
  tempPath,
257
239
  originalError,
258
240
  throwOnCleanupError: options.throwOnCleanupError === true,
259
241
  });
260
242
  }
261
- unregisterTempPath();
243
+ if (tempRemoved)
244
+ unregisterTempPath();
262
245
  }
263
246
  }
264
247
  export function replaceFileAtomicSync(options) {
@@ -269,24 +252,29 @@ export function replaceFileAtomicSync(options) {
269
252
  const dir = path.dirname(filePath);
270
253
  const dirMode = options.dirMode ?? 0o700;
271
254
  const mode = resolveModeSync(options);
255
+ const fchmodSync = options.fileSystem?.fchmodSync ?? (options.fileSystem === undefined ? syncFs.fchmodSync : undefined);
256
+ if (!fchmodSync &&
257
+ (options.mode !== undefined ||
258
+ options.preserveExistingMode === true ||
259
+ (process.platform !== "win32" && options.dirMode !== undefined))) {
260
+ throw missingFchmodSyncError();
261
+ }
272
262
  const tempPath = buildReplaceTempPath(filePath, options.tempPrefix);
273
263
  const unregisterTempPath = registerTempPathForExit(tempPath);
274
264
  let tempExists = false;
275
265
  let originalError;
276
- fsModule.mkdirSync(dir, { recursive: true, mode: dirMode });
277
- try {
278
- fsModule.chmodSync(dir, dirMode);
279
- }
280
- catch {
281
- // Best-effort on platforms that do not enforce POSIX modes.
282
- }
283
266
  try {
267
+ fsModule.mkdirSync(dir, { recursive: true, mode: dirMode });
268
+ applyDirectoryModeSync({ fsModule, dirPath: dir, mode: dirMode, fchmodSync });
284
269
  tempExists = true;
285
- fsModule.writeFileSync(tempPath, options.content, { mode, flag: "wx" });
286
- unregisterTempPath.setIdentity(fsModule.lstatSync(tempPath));
287
- if (options.syncTempFile) {
288
- syncTempFileSync(fsModule, tempPath);
289
- }
270
+ unregisterTempPath.setIdentity(writeTempFileSync({
271
+ fsModule,
272
+ tempPath,
273
+ content: options.content,
274
+ mode,
275
+ fchmodSync,
276
+ sync: options.syncTempFile === true,
277
+ }));
290
278
  if (options.beforeRename) {
291
279
  options.beforeRename({ filePath, tempPath });
292
280
  }
@@ -301,14 +289,12 @@ export function replaceFileAtomicSync(options) {
301
289
  copyFallbackRestore: options.copyFallbackRestore ?? "none",
302
290
  maxRestoreBytes: options.maxRestoreBytes,
303
291
  destinationHardlinks: options.destinationHardlinks,
292
+ fchmodSync,
293
+ syncFallback: options.syncTempFile === true,
304
294
  });
305
- tempExists = false;
306
- unregisterTempPath();
307
- try {
308
- fsModule.chmodSync(filePath, mode);
309
- }
310
- catch {
311
- // Best-effort on platforms that do not enforce POSIX modes.
295
+ if (result.method === "rename") {
296
+ tempExists = false;
297
+ unregisterTempPath();
312
298
  }
313
299
  if (options.syncParentDir) {
314
300
  syncDirectoryBestEffortSync(fsModule, dir);
@@ -320,17 +306,23 @@ export function replaceFileAtomicSync(options) {
320
306
  throw error;
321
307
  }
322
308
  finally {
309
+ let tempRemoved = !tempExists;
323
310
  if (tempExists) {
324
311
  try {
325
312
  fsModule.rmSync(tempPath, { force: true });
313
+ tempRemoved = true;
326
314
  }
327
315
  catch (cleanupError) {
328
- if (options.throwOnCleanupError && originalError !== undefined) {
329
- throw new Error(`Atomic file replace failed (${String(originalError)}); cleanup also failed (${String(cleanupError)})`, { cause: originalError });
316
+ if (options.throwOnCleanupError) {
317
+ if (originalError !== undefined) {
318
+ throw new Error(`Atomic file replace failed (${String(originalError)}); cleanup also failed (${String(cleanupError)})`, { cause: originalError });
319
+ }
320
+ throw cleanupError;
330
321
  }
331
322
  // The temp file is best-effort cleanup after write failure.
332
323
  }
333
324
  }
334
- unregisterTempPath();
325
+ if (tempRemoved)
326
+ unregisterTempPath();
335
327
  }
336
328
  }
@@ -1,15 +1,23 @@
1
1
  export type RootContext = {
2
2
  rootDir: string;
3
+ rootIdentity: {
4
+ dev: number;
5
+ ino: number;
6
+ };
3
7
  rootReal: string;
4
8
  rootWithSep: string;
5
9
  };
6
10
  export declare const ensureTrailingSep: (value: string) => string;
7
11
  export declare function assertValidRootRelativePath(relativePath: string): void;
12
+ export declare function assertValidRootDestinationPath(relativePath: string): void;
8
13
  export declare function expandRelativePathWithHome(relativePath: string): Promise<string>;
9
14
  export declare function resolveRootContext(rootDir: string): Promise<RootContext>;
15
+ export declare function assertRootIdentityCurrent(root: RootContext): Promise<void>;
10
16
  export declare function resolvePathInRoot(root: RootContext, relativePath: string, options?: {
11
17
  aliasErrorCode?: "outside-workspace" | "path-alias";
12
18
  allowFinalSymlink?: boolean;
19
+ rejectUnsafeDeviceReads?: boolean;
20
+ rejectSymlinks?: boolean;
13
21
  }): Promise<{
14
22
  rootReal: string;
15
23
  rootWithSep: string;
@@ -1 +1 @@
1
- {"version":3,"file":"root-context.d.ts","sourceRoot":"","sources":["../src/root-context.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,eAAO,MAAM,iBAAiB,UAAW,MAAM,WACM,CAAC;AAEtD,wBAAgB,2BAA2B,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAEtE;AAID,wBAAsB,0BAA0B,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAYtF;AAED,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAuB9E;AAED,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,WAAW,EACjB,YAAY,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE;IACR,cAAc,CAAC,EAAE,mBAAmB,GAAG,YAAY,CAAC;IACpD,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B,GACA,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CA6BtE;AAED,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAEvE"}
1
+ {"version":3,"file":"root-context.d.ts","sourceRoot":"","sources":["../src/root-context.ts"],"names":[],"mappings":"AAiBA,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,eAAO,MAAM,iBAAiB,UAAW,MAAM,WACM,CAAC;AAEtD,wBAAgB,2BAA2B,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAEtE;AAED,wBAAgB,8BAA8B,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAKzE;AAID,wBAAsB,0BAA0B,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAYtF;AAED,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CA0B9E;AAED,wBAAsB,yBAAyB,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBhF;AAED,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,WAAW,EACjB,YAAY,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE;IACR,cAAc,CAAC,EAAE,mBAAmB,GAAG,YAAY,CAAC;IACpD,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,GACA,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CA0CtE;AAED,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAEvE"}
@@ -2,13 +2,22 @@ import fs from "node:fs/promises";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { FsSafeError } from "./errors.js";
5
+ import { sameFileIdentity } from "./file-identity.js";
5
6
  import { expandHomePrefix } from "./home-dir.js";
6
- import { assertNoNulPathInput, isNotFoundPathError, isPathInside } from "./path.js";
7
+ import { assertNoNulPathInput, assertNoUnsafeDeviceReadPath, hasNodeErrorCode, isNotFoundPathError, isPathInside, } from "./path.js";
7
8
  import { ROOT_PATH_ALIAS_POLICIES, resolveRootPath } from "./root-path.js";
9
+ import { outsideWorkspaceError } from "./root-errors.js";
10
+ import { isDriveRelativePath } from "./safe-path-segment.js";
8
11
  export const ensureTrailingSep = (value) => value.endsWith(path.sep) ? value : value + path.sep;
9
12
  export function assertValidRootRelativePath(relativePath) {
10
13
  assertNoNulPathInput(relativePath, "relative path contains a NUL byte");
11
14
  }
15
+ export function assertValidRootDestinationPath(relativePath) {
16
+ assertValidRootRelativePath(relativePath);
17
+ if (isDriveRelativePath(relativePath)) {
18
+ throw new FsSafeError("invalid-path", "relative path must not start with a drive letter");
19
+ }
20
+ }
12
21
  let cachedHomePath;
13
22
  export async function expandRelativePathWithHome(relativePath) {
14
23
  const rawHome = process.env.HOME || process.env.USERPROFILE || os.homedir();
@@ -27,12 +36,14 @@ export async function expandRelativePathWithHome(relativePath) {
27
36
  export async function resolveRootContext(rootDir) {
28
37
  assertNoNulPathInput(rootDir, "root dir contains a NUL byte");
29
38
  let rootReal;
39
+ let rootIdentity;
30
40
  try {
31
41
  rootReal = await fs.realpath(rootDir);
32
42
  const rootStat = await fs.stat(rootReal);
33
43
  if (!rootStat.isDirectory()) {
34
44
  throw new FsSafeError("invalid-path", "root dir is not a directory");
35
45
  }
46
+ rootIdentity = { dev: rootStat.dev, ino: rootStat.ino };
36
47
  }
37
48
  catch (err) {
38
49
  if (err instanceof FsSafeError) {
@@ -45,16 +56,37 @@ export async function resolveRootContext(rootDir) {
45
56
  }
46
57
  return {
47
58
  rootDir: path.resolve(rootDir),
59
+ rootIdentity,
48
60
  rootReal,
49
61
  rootWithSep: ensureTrailingSep(rootReal),
50
62
  };
51
63
  }
64
+ export async function assertRootIdentityCurrent(root) {
65
+ let current;
66
+ try {
67
+ current = await fs.lstat(root.rootReal);
68
+ }
69
+ catch (error) {
70
+ throw new FsSafeError("path-mismatch", "root path changed during operation", {
71
+ cause: error instanceof Error ? error : undefined,
72
+ });
73
+ }
74
+ if (current.isSymbolicLink() ||
75
+ !current.isDirectory() ||
76
+ !sameFileIdentity(current, root.rootIdentity)) {
77
+ throw new FsSafeError("path-mismatch", "root path changed during operation");
78
+ }
79
+ }
52
80
  export async function resolvePathInRoot(root, relativePath, options) {
53
81
  assertValidRootRelativePath(relativePath);
82
+ await assertRootIdentityCurrent(root);
54
83
  const expanded = await expandRelativePathWithHome(relativePath);
55
84
  const resolved = path.resolve(root.rootWithSep, expanded);
56
85
  if (!isPathInside(root.rootWithSep, resolved)) {
57
- throw new FsSafeError("outside-workspace", "file is outside workspace root");
86
+ throw outsideWorkspaceError();
87
+ }
88
+ if (options?.rejectUnsafeDeviceReads === true) {
89
+ assertNoUnsafeDeviceReadPath(resolved);
58
90
  }
59
91
  const rawAbsolutePath = path.isAbsolute(expanded)
60
92
  ? expanded
@@ -66,9 +98,18 @@ export async function resolvePathInRoot(root, relativePath, options) {
66
98
  rootCanonicalPath: root.rootReal,
67
99
  boundaryLabel: "root",
68
100
  policy: options?.allowFinalSymlink ? ROOT_PATH_ALIAS_POLICIES.unlinkTarget : undefined,
101
+ rejectSymlinks: options?.rejectSymlinks,
69
102
  });
70
103
  }
71
104
  catch (error) {
105
+ if (error instanceof FsSafeError && error.code === "symlink") {
106
+ throw error;
107
+ }
108
+ if (hasNodeErrorCode(error, "ENAMETOOLONG")) {
109
+ throw new FsSafeError("invalid-path", "relative path is too long", {
110
+ cause: error instanceof Error ? error : undefined,
111
+ });
112
+ }
72
113
  const code = options?.aliasErrorCode ?? "outside-workspace";
73
114
  throw new FsSafeError(code, code === "path-alias" ? "path alias escape blocked" : "file is outside workspace root", {
74
115
  cause: error instanceof Error ? error : undefined,
@@ -1,4 +1,11 @@
1
+ import { FsSafeError } from "./errors.js";
2
+ export declare function fileNotFoundError(cause?: unknown): FsSafeError;
3
+ export declare function outsideWorkspaceError(): FsSafeError;
4
+ export declare function directoryComponentNotDirectoryError(cause?: unknown): FsSafeError;
5
+ export declare function hardlinkedPathNotAllowedError(): FsSafeError;
1
6
  export declare function isAlreadyExistsError(error: unknown): boolean;
2
7
  export declare function normalizePinnedWriteError(error: unknown): Error;
3
8
  export declare function normalizePinnedPathError(error: unknown): Error;
9
+ export declare function normalizeRemoveGuardError(error: unknown): Error;
10
+ export declare function normalizeRemovePathError(error: unknown): Error;
4
11
  //# sourceMappingURL=root-errors.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"root-errors.d.ts","sourceRoot":"","sources":["../src/root-errors.ts"],"names":[],"mappings":"AAGA,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE5D;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAO/D;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAO9D"}
1
+ {"version":3,"file":"root-errors.d.ts","sourceRoot":"","sources":["../src/root-errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAK1C,wBAAgB,iBAAiB,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,WAAW,CAI9D;AAED,wBAAgB,qBAAqB,IAAI,WAAW,CAEnD;AAED,wBAAgB,mCAAmC,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,WAAW,CAIhF;AAED,wBAAgB,6BAA6B,IAAI,WAAW,CAE3D;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE5D;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAU/D;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAO9D;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAQ/D;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,CAe9D"}