@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
@@ -2,9 +2,10 @@ import { randomBytes } from "node:crypto";
2
2
  import fsSync, {} from "node:fs";
3
3
  import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
- import { readFileHandleBounded } from "./bounded-read.js";
5
+ import { readFileDescriptorBoundedSync, readFileHandleBounded } from "./bounded-read.js";
6
6
  import { FsSafeError } from "./errors.js";
7
7
  import { sameFileIdentity } from "./file-identity.js";
8
+ import { getFsSafeTestHooks } from "./test-hooks.js";
8
9
  const MAX_LOCK_PAYLOAD_BYTES = 1024 * 1024;
9
10
  const SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES = 16;
10
11
  const SIDECAR_LOCK_OWNERSHIP_TOKEN_BITS = SIDECAR_LOCK_OWNERSHIP_TOKEN_BYTES * 8;
@@ -55,6 +56,7 @@ export function parseSidecarLockPayload(raw, parser) {
55
56
  }
56
57
  }
57
58
  export async function readSidecarLockSnapshot(lockPath, options = {}) {
59
+ let handle;
58
60
  try {
59
61
  if (options.lockRoot) {
60
62
  const opened = await options.lockRoot.open(relativeSidecarLockPath(options.lockRoot, lockPath));
@@ -70,9 +72,44 @@ export async function readSidecarLockSnapshot(lockPath, options = {}) {
70
72
  await opened.handle.close().catch(() => undefined);
71
73
  }
72
74
  }
73
- const stat = await fs.lstat(lockPath);
74
- const raw = await fs.readFile(lockPath, "utf8");
75
- return { raw, payload: parseSidecarLockPayload(raw, options.parsePayload), stat };
75
+ const before = await fs.lstat(lockPath);
76
+ if (!before.isFile() || before.isSymbolicLink()) {
77
+ if (options.rejectNonFile) {
78
+ throw new FsSafeError("not-file", `sidecar lock is not a regular file: ${lockPath}`);
79
+ }
80
+ return null;
81
+ }
82
+ await getFsSafeTestHooks()?.beforeSidecarLockSnapshotOpen?.(lockPath);
83
+ const noFollow = process.platform !== "win32" && typeof fsSync.constants.O_NOFOLLOW === "number"
84
+ ? fsSync.constants.O_NOFOLLOW
85
+ : 0;
86
+ try {
87
+ handle = await fs.open(lockPath, fsSync.constants.O_RDONLY |
88
+ noFollow |
89
+ (typeof fsSync.constants.O_NONBLOCK === "number" ? fsSync.constants.O_NONBLOCK : 0));
90
+ }
91
+ catch (error) {
92
+ if (options.rejectNonFile && error.code === "ELOOP") {
93
+ throw new FsSafeError("not-file", `sidecar lock is not a regular file: ${lockPath}`, {
94
+ cause: error,
95
+ });
96
+ }
97
+ throw error;
98
+ }
99
+ const opened = await handle.stat();
100
+ if (!opened.isFile()) {
101
+ if (options.rejectNonFile) {
102
+ throw new FsSafeError("not-file", `sidecar lock is not a regular file: ${lockPath}`);
103
+ }
104
+ return null;
105
+ }
106
+ if (!options.allowDescriptorIdentityDrift && !sameFileIdentity(before, opened))
107
+ return null;
108
+ const raw = (await readFileHandleBounded(handle, MAX_LOCK_PAYLOAD_BYTES)).toString("utf8");
109
+ const after = await fs.lstat(lockPath);
110
+ if (!after.isFile() || !sameFileIdentity(before, after))
111
+ return null;
112
+ return { raw, payload: parseSidecarLockPayload(raw, options.parsePayload), stat: after };
76
113
  }
77
114
  catch (err) {
78
115
  if (err.code === "ENOENT" ||
@@ -81,19 +118,26 @@ export async function readSidecarLockSnapshot(lockPath, options = {}) {
81
118
  }
82
119
  throw err;
83
120
  }
121
+ finally {
122
+ await handle?.close().catch(() => undefined);
123
+ }
84
124
  }
85
- export function readSidecarLockSnapshotSync(lockPath, parsePayload) {
125
+ export function readSidecarLockSnapshotSync(lockPath, parsePayload, options = {}) {
86
126
  let fd;
87
127
  try {
88
128
  const before = fsSync.lstatSync(lockPath);
89
- if (!before.isFile() || before.isSymbolicLink())
129
+ if (!before.isFile() || before.isSymbolicLink()) {
130
+ if (options.rejectNonFile) {
131
+ throw new FsSafeError("not-file", `sidecar lock is not a regular file: ${lockPath}`);
132
+ }
90
133
  return null;
134
+ }
91
135
  const noFollow = process.platform !== "win32" && typeof fsSync.constants.O_NOFOLLOW === "number"
92
136
  ? fsSync.constants.O_NOFOLLOW
93
137
  : 0;
94
138
  fd = fsSync.openSync(lockPath, fsSync.constants.O_RDONLY | noFollow);
95
139
  const opened = fsSync.fstatSync(fd);
96
- const raw = fsSync.readFileSync(fd, "utf8");
140
+ const raw = readFileDescriptorBoundedSync(fd, MAX_LOCK_PAYLOAD_BYTES).toString("utf8");
97
141
  const after = fsSync.lstatSync(lockPath);
98
142
  if (!sameFileIdentity(before, opened) || !sameFileIdentity(opened, after))
99
143
  return null;
@@ -139,7 +183,10 @@ export function sidecarLockSnapshotMatches(current, observed) {
139
183
  return observed.stat !== undefined && current.stat !== undefined;
140
184
  }
141
185
  export async function removeSidecarLockIfUnchanged(lockPath, observed, options = {}) {
142
- const current = await readSidecarLockSnapshot(lockPath, options);
186
+ const current = await readSidecarLockSnapshot(lockPath, {
187
+ ...options,
188
+ allowDescriptorIdentityDrift: observed?.ownershipToken !== undefined,
189
+ });
143
190
  if (!current || !observed || !sidecarLockSnapshotMatches(current, observed)) {
144
191
  return false;
145
192
  }
@@ -152,7 +199,10 @@ export async function removeSidecarLockIfUnchanged(lockPath, observed, options =
152
199
  return true;
153
200
  }
154
201
  export async function sidecarLockSnapshotStillPresent(lockPath, observed, options = {}) {
155
- const current = await readSidecarLockSnapshot(lockPath, options);
202
+ const current = await readSidecarLockSnapshot(lockPath, {
203
+ ...options,
204
+ allowDescriptorIdentityDrift: observed?.ownershipToken !== undefined,
205
+ });
156
206
  return !!current && !!observed && sidecarLockSnapshotMatches(current, observed);
157
207
  }
158
208
  export async function sidecarReclaimGuardExists(pathname) {
@@ -1 +1 @@
1
- {"version":3,"file":"sidecar-lock.d.ts","sourceRoot":"","sources":["../src/sidecar-lock.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EACV,yBAAyB,EACzB,iBAAiB,EACjB,oBAAoB,EAEpB,sBAAsB,EACvB,MAAM,yBAAyB,CAAC;AAKjC,YAAY,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAC1E,YAAY,EACV,yBAAyB,EACzB,0BAA0B,EAC1B,iBAAiB,EACjB,oBAAoB,EACpB,uBAAuB,EACvB,wBAAwB,EACxB,sBAAsB,GACvB,MAAM,yBAAyB,CAAC;AAsLjC,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM;cAgB3B,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,WACpD,yBAAyB,CAAC,QAAQ,CAAC,KAC3C,OAAO,CAAC,iBAAiB,CAAC;eAgNL,CAAC,EAAE,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,WACxD,yBAAyB,CAAC,QAAQ,CAAC,MACxC,MAAM,OAAO,CAAC,CAAC,CAAC,KACnB,OAAO,CAAC,CAAC,CAAC;iBASW,OAAO,CAAC,IAAI,CAAC;iBAQnB,IAAI;uBAIE,oBAAoB,EAAE;EAW/C;AAED,wBAAsB,eAAe,CAAC,CAAC,EAAE,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/E,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,sBAAsB,CAAC,QAAQ,CAAC,EACzC,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC,CAAC,CAAC,CAMZ"}
1
+ {"version":3,"file":"sidecar-lock.d.ts","sourceRoot":"","sources":["../src/sidecar-lock.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EACV,yBAAyB,EACzB,iBAAiB,EACjB,oBAAoB,EACpB,sBAAsB,EACvB,MAAM,yBAAyB,CAAC;AACjC,YAAY,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAC1E,YAAY,EACV,yBAAyB,EACzB,0BAA0B,EAC1B,iBAAiB,EACjB,oBAAoB,EACpB,uBAAuB,EACvB,wBAAwB,EACxB,sBAAsB,GACvB,MAAM,yBAAyB,CAAC;AAmLjC,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM;cAS3B,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,WACpD,yBAAyB,CAAC,QAAQ,CAAC,KAC3C,OAAO,CAAC,iBAAiB,CAAC;eAYL,CAAC,EAAE,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,WACxD,yBAAyB,CAAC,QAAQ,CAAC,MACxC,MAAM,OAAO,CAAC,CAAC,CAAC,KACnB,OAAO,CAAC,CAAC,CAAC;iBASW,OAAO,CAAC,IAAI,CAAC;iBAQnB,IAAI;uBAIE,oBAAoB,EAAE;EAW/C;AAED,wBAAsB,eAAe,CAAC,CAAC,EAAE,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/E,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,sBAAsB,CAAC,QAAQ,CAAC,EACzC,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC,CAAC,CAAC,CAMZ"}
@@ -1,13 +1,11 @@
1
1
  import fsSync from "node:fs";
2
- import fs from "node:fs/promises";
3
- import path from "node:path";
4
2
  import { sameFileIdentity } from "./file-identity.js";
5
- import { readSidecarLockSnapshot, relativeSidecarLockPath, releaseSidecarReclaimGuard, removeSidecarLockIfUnchanged, removeStaleSidecarLockIfAllowed, serializeSidecarLockPayload, sidecarLockSnapshotMatches, sidecarLockSnapshotStillPresent, sidecarReclaimGuardExists, tryAcquireSidecarReclaimGuard, } from "./sidecar-lock-reclaim.js";
6
- import { FsSafeError } from "./errors.js";
7
- import { createNativeExclusiveFile } from "./native-operations.js";
3
+ import { removeSidecarLockIfUnchanged, sidecarLockSnapshotMatches, } from "./sidecar-lock-reclaim.js";
4
+ import { acquireSidecarLock } from "./sidecar-lock-acquire.js";
8
5
  import { createHeldSidecarLockHandle } from "./sidecar-lock-handle.js";
9
- import { computeSidecarLockDelayMs, defaultSidecarLockShouldReclaim, } from "./sidecar-lock-policy.js";
10
6
  const GLOBAL_STATE_KEY = Symbol.for("fsSafe.sidecarLockManagers");
7
+ const GLOBAL_CLEANUP_KEY = Symbol.for("fsSafe.sidecarLockCleanupRegistered");
8
+ const GLOBAL_CLEANUP_HANDLER_KEY = Symbol.for("fsSafe.sidecarLockCleanupHandler");
11
9
  function getGlobalManagers() {
12
10
  const globalWithState = globalThis;
13
11
  if (!globalWithState[GLOBAL_STATE_KEY]) {
@@ -79,17 +77,6 @@ function snapshotMatchesSync(lockPath, observed) {
79
77
  }
80
78
  }
81
79
  }
82
- async function resolveNormalizedTargetPath(targetPath) {
83
- const resolved = path.resolve(targetPath);
84
- const dir = path.dirname(resolved);
85
- await fs.mkdir(dir, { recursive: true });
86
- try {
87
- return path.join(await fs.realpath(dir), path.basename(resolved));
88
- }
89
- catch {
90
- return resolved;
91
- }
92
- }
93
80
  function releaseAllReclaimGuardsSync(state) {
94
81
  for (const reclaimGuardPath of state.reclaimGuards) {
95
82
  try {
@@ -116,6 +103,19 @@ function releaseAllLocksSync(state) {
116
103
  }
117
104
  releaseAllReclaimGuardsSync(state);
118
105
  }
106
+ function ensureGlobalExitCleanupRegistered() {
107
+ const globalWithCleanup = globalThis;
108
+ if (globalWithCleanup[GLOBAL_CLEANUP_KEY])
109
+ return;
110
+ globalWithCleanup[GLOBAL_CLEANUP_KEY] = true;
111
+ const cleanup = () => {
112
+ for (const state of getGlobalManagers().values()) {
113
+ releaseAllLocksSync(state);
114
+ }
115
+ };
116
+ globalWithCleanup[GLOBAL_CLEANUP_HANDLER_KEY] = cleanup;
117
+ process.on("exit", cleanup);
118
+ }
119
119
  async function releaseHeldLock(state, normalizedTargetPath, held, options = {}) {
120
120
  const current = state.held.get(normalizedTargetPath);
121
121
  if (current !== held) {
@@ -164,217 +164,18 @@ function handleForHeldLock(state, normalizedTargetPath, held) {
164
164
  export function createSidecarLockManager(key) {
165
165
  const state = resolveManagerState(key);
166
166
  function ensureExitCleanupRegistered() {
167
- if (!state.cleanupRegistered) {
168
- state.cleanupRegistered = true;
169
- state.reclaimCleanupRegistered = true;
170
- process.on("exit", () => releaseAllLocksSync(state));
171
- return;
172
- }
173
- if (!state.reclaimCleanupRegistered) {
174
- state.reclaimCleanupRegistered = true;
175
- process.on("exit", () => releaseAllReclaimGuardsSync(state));
176
- }
167
+ state.cleanupRegistered = true;
168
+ state.reclaimCleanupRegistered = true;
169
+ ensureGlobalExitCleanupRegistered();
177
170
  }
178
171
  async function acquire(options) {
179
- ensureExitCleanupRegistered();
180
- const normalizedTargetPath = await resolveNormalizedTargetPath(options.targetPath);
181
- const lockPath = options.lockPath ?? `${normalizedTargetPath}.lock`;
182
- const held = state.held.get(normalizedTargetPath);
183
- if (held &&
184
- options.reentrantOwner !== undefined &&
185
- held.reentrantOwner !== undefined &&
186
- options.reentrantOwner === held.reentrantOwner) {
187
- held.refCount += 1;
188
- return handleForHeldLock(state, normalizedTargetPath, held);
189
- }
190
- const startedAt = Date.now();
191
- const retry = options.retry ?? {};
192
- const maxRetries = options.timeoutMs === Number.POSITIVE_INFINITY ? undefined : retry.retries;
193
- const reclaimGuardPath = `${lockPath}.reclaim`;
194
- let ownsReclaimGuard = false;
195
- let attempt = 0;
196
- const waitForRetry = async () => {
197
- const elapsed = Date.now() - startedAt;
198
- if ((options.timeoutMs !== undefined &&
199
- options.timeoutMs !== Number.POSITIVE_INFINITY &&
200
- elapsed >= options.timeoutMs) ||
201
- (maxRetries !== undefined && attempt >= maxRetries)) {
202
- throw Object.assign(new Error(`file lock timeout for ${normalizedTargetPath}`), {
203
- code: "file_lock_timeout",
204
- lockPath,
205
- normalizedTargetPath,
206
- });
207
- }
208
- const remaining = options.timeoutMs === undefined || options.timeoutMs === Number.POSITIVE_INFINITY
209
- ? Number.POSITIVE_INFINITY
210
- : Math.max(0, options.timeoutMs - elapsed);
211
- const delay = Math.min(computeSidecarLockDelayMs(retry, attempt), remaining);
212
- attempt += 1;
213
- await new Promise((resolve) => setTimeout(resolve, delay));
214
- };
215
- try {
216
- while (true) {
217
- if (!ownsReclaimGuard && (await sidecarReclaimGuardExists(reclaimGuardPath))) {
218
- await waitForRetry();
219
- continue;
220
- }
221
- let handle = null;
222
- try {
223
- const payload = await options.payload();
224
- const { raw, ownershipToken } = serializeSidecarLockPayload(payload);
225
- if (options.lockRoot) {
226
- const relativeLockPath = relativeSidecarLockPath(options.lockRoot, lockPath);
227
- try {
228
- await options.lockRoot.create(relativeLockPath, raw, { mkdir: true, mode: 0o600 });
229
- }
230
- catch (error) {
231
- if (error instanceof FsSafeError && error.code === "already-exists") {
232
- throw Object.assign(new Error("sidecar lock exists"), { code: "EEXIST" });
233
- }
234
- throw error;
235
- }
236
- handle = (await options.lockRoot.open(relativeLockPath)).handle;
237
- }
238
- else {
239
- handle = (await createNativeExclusiveFile(lockPath, 0o600)) ?? await fs.open(lockPath, "wx");
240
- await handle.writeFile(raw, "utf8");
241
- }
242
- const snapshot = { raw, payload, stat: await handle.stat(), ownershipToken };
243
- const createdHeld = {
244
- refCount: 1,
245
- reentrantOwner: options.reentrantOwner,
246
- handle,
247
- lockPath,
248
- snapshot,
249
- acquiredAt: Date.now(),
250
- metadata: options.metadata ?? {},
251
- lockRoot: options.lockRoot,
252
- parsePayload: options.parsePayload,
253
- };
254
- state.held.set(normalizedTargetPath, createdHeld);
255
- if (ownsReclaimGuard) {
256
- try {
257
- await releaseSidecarReclaimGuard(state.reclaimGuards, reclaimGuardPath);
258
- ownsReclaimGuard = false;
259
- }
260
- catch (err) {
261
- await releaseHeldLock(state, normalizedTargetPath, createdHeld, { force: true });
262
- throw err;
263
- }
264
- }
265
- const returnedHandle = handleForHeldLock(state, normalizedTargetPath, createdHeld);
266
- const interval = options.compromiseCheckIntervalMs;
267
- if (options.onCompromised && interval !== undefined && interval > 0) {
268
- createdHeld.compromiseTimer = setInterval(() => {
269
- void returnedHandle.verifyStillHeld().then((stillHeld) => {
270
- if (!stillHeld && createdHeld.compromiseTimer) {
271
- clearInterval(createdHeld.compromiseTimer);
272
- createdHeld.compromiseTimer = undefined;
273
- options.onCompromised?.({ lockPath, normalizedTargetPath });
274
- }
275
- });
276
- }, interval);
277
- createdHeld.compromiseTimer.unref();
278
- }
279
- return returnedHandle;
280
- }
281
- catch (err) {
282
- if (handle) {
283
- const failedSnapshot = { payload: null };
284
- try {
285
- failedSnapshot.stat = await handle.stat();
286
- }
287
- catch {
288
- // Best-effort cleanup of a failed exclusive create.
289
- }
290
- const current = state.held.get(normalizedTargetPath);
291
- if (current?.handle === handle) {
292
- state.held.delete(normalizedTargetPath);
293
- }
294
- // If payload serialization/write fails, the file may be empty or
295
- // partial JSON, so remove while our exclusive handle is still open.
296
- if (!options.lockRoot) {
297
- await fs.rm(lockPath, { force: true }).catch(() => undefined);
298
- }
299
- await handle.close().catch(() => undefined);
300
- // Windows can refuse removing an open file; retry after close but
301
- // only if the path still points at the file identity we created.
302
- await removeSidecarLockIfUnchanged(lockPath, failedSnapshot, {
303
- lockRoot: options.lockRoot,
304
- parsePayload: options.parsePayload,
305
- });
306
- }
307
- if (err.code !== "EEXIST") {
308
- throw err;
309
- }
310
- if (ownsReclaimGuard) {
311
- await releaseSidecarReclaimGuard(state.reclaimGuards, reclaimGuardPath);
312
- ownsReclaimGuard = false;
313
- continue;
314
- }
315
- const nowMs = Date.now();
316
- const snapshot = await readSidecarLockSnapshot(lockPath, {
317
- lockRoot: options.lockRoot,
318
- parsePayload: options.parsePayload,
319
- });
320
- if (!snapshot) {
321
- continue;
322
- }
323
- if (state.held.has(normalizedTargetPath)) {
324
- await waitForRetry();
325
- continue;
326
- }
327
- const shouldReclaim = options.shouldReclaim ?? defaultSidecarLockShouldReclaim;
328
- if (await shouldReclaim({
329
- lockPath,
330
- normalizedTargetPath,
331
- payload: snapshot?.payload ?? null,
332
- staleMs: options.staleMs,
333
- nowMs,
334
- heldByThisProcess: state.held.has(normalizedTargetPath),
335
- })) {
336
- if (!(await sidecarLockSnapshotStillPresent(lockPath, snapshot, {
337
- lockRoot: options.lockRoot,
338
- parsePayload: options.parsePayload,
339
- }))) {
340
- continue;
341
- }
342
- const staleRecovery = options.staleRecovery ?? "fail-closed";
343
- if (staleRecovery === "remove-if-unchanged") {
344
- if (!(await tryAcquireSidecarReclaimGuard(state.reclaimGuards, reclaimGuardPath))) {
345
- await waitForRetry();
346
- continue;
347
- }
348
- ownsReclaimGuard = true;
349
- const removal = await removeStaleSidecarLockIfAllowed({
350
- lockPath,
351
- normalizedTargetPath,
352
- snapshot,
353
- shouldRemoveStaleLock: options.shouldRemoveStaleLock,
354
- lockRoot: options.lockRoot,
355
- parsePayload: options.parsePayload,
356
- });
357
- if (removal === "removed" || removal === "changed") {
358
- continue;
359
- }
360
- await releaseSidecarReclaimGuard(state.reclaimGuards, reclaimGuardPath);
361
- ownsReclaimGuard = false;
362
- }
363
- throw Object.assign(new Error(`file lock stale for ${normalizedTargetPath}`), {
364
- code: "file_lock_stale",
365
- lockPath,
366
- normalizedTargetPath,
367
- });
368
- }
369
- await waitForRetry();
370
- }
371
- }
372
- }
373
- finally {
374
- if (ownsReclaimGuard) {
375
- await releaseSidecarReclaimGuard(state.reclaimGuards, reclaimGuardPath).catch(() => undefined);
376
- }
377
- }
172
+ return await acquireSidecarLock(options, {
173
+ held: state.held,
174
+ reclaimGuards: state.reclaimGuards,
175
+ ensureExitCleanupRegistered,
176
+ handleForHeldLock: (normalizedTargetPath, held) => handleForHeldLock(state, normalizedTargetPath, held),
177
+ releaseHeldLock: async (normalizedTargetPath, held, releaseOptions) => await releaseHeldLock(state, normalizedTargetPath, held, releaseOptions),
178
+ });
378
179
  }
379
180
  async function withLock(options, fn) {
380
181
  const lock = await acquire(options);
@@ -1 +1 @@
1
- {"version":3,"file":"symlink-parents.d.ts","sourceRoot":"","sources":["../src/symlink-parents.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,6BAA6B,GAAG;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAyBF,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,6BAA6B,GACpC,OAAO,CAAC,IAAI,CAAC,CA0Bf;AAED,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,6BAA6B,GACpC,IAAI,CA0BN"}
1
+ {"version":3,"file":"symlink-parents.d.ts","sourceRoot":"","sources":["../src/symlink-parents.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,6BAA6B,GAAG;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAyBF,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,6BAA6B,GACpC,OAAO,CAAC,IAAI,CAAC,CA6Bf;AAED,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,6BAA6B,GACpC,IAAI,CA6BN"}
@@ -1,7 +1,8 @@
1
1
  import fsSync from "node:fs";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { isNotFoundPathError, isPathRelativeEscape } from "./path.js";
4
+ import { FsSafeError } from "./errors.js";
5
+ import { hasNodeErrorCode, isPathRelativeEscape } from "./path.js";
5
6
  function resolvePathWalk(params) {
6
7
  const root = path.resolve(params.rootDir);
7
8
  const target = path.resolve(params.targetPath);
@@ -26,7 +27,7 @@ export async function assertNoSymlinkParents(params) {
26
27
  return;
27
28
  }
28
29
  let current = walk.root;
29
- for (const segment of walk.segments) {
30
+ for (const [index, segment] of walk.segments.entries()) {
30
31
  current = path.join(current, segment);
31
32
  try {
32
33
  const stat = await fs.lstat(current);
@@ -36,12 +37,12 @@ export async function assertNoSymlinkParents(params) {
36
37
  }
37
38
  throw new Error(formatUnsafePath(params, current));
38
39
  }
39
- if (params.requireDirectories && !stat.isDirectory()) {
40
- throw new Error(`${params.messagePrefix ?? "Path"} must traverse directories: ${current}`);
40
+ if ((params.requireDirectories || index < walk.segments.length - 1) && !stat.isDirectory()) {
41
+ throw new FsSafeError("not-file", `${params.messagePrefix ?? "Path"} must traverse directories: ${current}`);
41
42
  }
42
43
  }
43
44
  catch (err) {
44
- if (isNotFoundPathError(err) && params.allowMissing !== false) {
45
+ if (hasNodeErrorCode(err, "ENOENT") && params.allowMissing !== false) {
45
46
  return;
46
47
  }
47
48
  throw err;
@@ -54,7 +55,7 @@ export function assertNoSymlinkParentsSync(params) {
54
55
  return;
55
56
  }
56
57
  let current = walk.root;
57
- for (const segment of walk.segments) {
58
+ for (const [index, segment] of walk.segments.entries()) {
58
59
  current = path.join(current, segment);
59
60
  try {
60
61
  const stat = fsSync.lstatSync(current);
@@ -64,8 +65,8 @@ export function assertNoSymlinkParentsSync(params) {
64
65
  }
65
66
  throw new Error(formatUnsafePath(params, current));
66
67
  }
67
- if (params.requireDirectories && !stat.isDirectory()) {
68
- throw new Error(`${params.messagePrefix ?? "Path"} must traverse directories: ${current}`);
68
+ if ((params.requireDirectories || index < walk.segments.length - 1) && !stat.isDirectory()) {
69
+ throw new FsSafeError("not-file", `${params.messagePrefix ?? "Path"} must traverse directories: ${current}`);
69
70
  }
70
71
  }
71
72
  catch (err) {
@@ -1 +1 @@
1
- {"version":3,"file":"temp-target.d.ts","sourceRoot":"","sources":["../src/temp-target.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,QAAQ,GAAG;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AA0EF,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAI7D;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GAAG,MAAM,CAaT;AAyBD,wBAAsB,QAAQ,CAAC,MAAM,EAAE;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAC3C,GAAG,OAAO,CAAC,QAAQ,CAAC,CAqBpB;AAED,wBAAsB,YAAY,CAAC,CAAC,EAClC,MAAM,EAAE;IACN,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAC3C,EACD,EAAE,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GAClC,OAAO,CAAC,CAAC,CAAC,CAOZ"}
1
+ {"version":3,"file":"temp-target.d.ts","sourceRoot":"","sources":["../src/temp-target.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,QAAQ,GAAG;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AA0EF,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAI7D;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GAAG,MAAM,CAaT;AAsCD,wBAAsB,QAAQ,CAAC,MAAM,EAAE;IACrC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAC3C,GAAG,OAAO,CAAC,QAAQ,CAAC,CAsBpB;AAED,wBAAsB,YAAY,CAAC,CAAC,EAClC,MAAM,EAAE;IACN,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAC3C,EACD,EAAE,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GAClC,OAAO,CAAC,CAAC,CAAC,CAOZ"}
@@ -1,6 +1,7 @@
1
1
  import crypto from "node:crypto";
2
- import { mkdtemp, rm } from "node:fs/promises";
2
+ import { lstat, mkdtemp, rm } from "node:fs/promises";
3
3
  import path from "node:path";
4
+ import { sameFileIdentity } from "./file-identity.js";
4
5
  import { assertSafePathSegment, sanitizeSafePathSegment } from "./safe-path-segment.js";
5
6
  import { resolveSecureTempRoot } from "./secure-temp-dir.js";
6
7
  import { registerTempPathForExit } from "./temp-cleanup.js";
@@ -89,8 +90,17 @@ function isNodeErrorWithCode(err, code) {
89
90
  "code" in err &&
90
91
  err.code === code);
91
92
  }
92
- async function cleanupTempDir(dir, onCleanupError) {
93
+ async function cleanupTempDir(dir, identity, onCleanupError) {
93
94
  try {
95
+ const current = await lstat(dir).catch((error) => {
96
+ if (isNodeErrorWithCode(error, "ENOENT")) {
97
+ return undefined;
98
+ }
99
+ throw error;
100
+ });
101
+ if (!current || !sameFileIdentity(current, identity)) {
102
+ return;
103
+ }
94
104
  await rm(dir, { recursive: true, force: true });
95
105
  }
96
106
  catch (err) {
@@ -106,11 +116,12 @@ export async function tempFile(params) {
106
116
  const rootDir = resolveTempRoot(params.rootDir);
107
117
  const prefix = `${sanitizePrefix(params.prefix)}-`;
108
118
  const dir = await mkdtemp(path.join(rootDir, prefix));
109
- const unregisterTempDir = registerTempPathForExit(dir, { recursive: true });
119
+ const identity = await lstat(dir);
120
+ const unregisterTempDir = registerTempPathForExit(dir, { recursive: true, identity });
110
121
  const file = (fileName) => path.join(dir, sanitizeTempFileName(fileName ?? params.fileName ?? "download.bin"));
111
122
  const cleanup = async () => {
112
123
  try {
113
- await cleanupTempDir(dir, params.onCleanupError);
124
+ await cleanupTempDir(dir, identity, params.onCleanupError);
114
125
  }
115
126
  finally {
116
127
  unregisterTempDir();
@@ -10,6 +10,7 @@ export type FsSafeTestHooks = {
10
10
  beforeRootFallbackMutation?: (operation: "mkdir" | "move" | "remove", targetPath: string) => Promise<void> | void;
11
11
  afterPinnedWriteFallbackRename?: (targetPath: string) => Promise<void> | void;
12
12
  beforeSiblingTempWrite?: (tempPath: string) => Promise<void> | void;
13
+ beforeSidecarLockSnapshotOpen?: (lockPath: string) => Promise<void> | void;
13
14
  beforeTrashMove?: (targetPath: string, destPath: string) => void;
14
15
  afterPublishTargetCreated?: (method: "hardlink" | "exclusive-copy" | "rename-noreplace", targetPath: string, identity: FileIdentityStat) => Promise<void> | void;
15
16
  beforePublishDirectorySync?: (method: "hardlink" | "exclusive-copy" | "rename-noreplace", targetPath: string, identity: FileIdentityStat) => Promise<void> | void;
@@ -1 +1 @@
1
- {"version":3,"file":"test-hooks.d.ts","sourceRoot":"","sources":["../src/test-hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE3D,MAAM,MAAM,eAAe,GAAG;IAC5B,iBAAiB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC/D,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACvE,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC3E,2BAA2B,CAAC,EAAE,CAC5B,SAAS,EAAE,OAAO,GAAG,OAAO,EAC5B,UAAU,EAAE,MAAM,KACf,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1B,2BAA2B,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACxE,+BAA+B,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7D,0BAA0B,CAAC,EAAE,CAC3B,SAAS,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,EACtC,UAAU,EAAE,MAAM,KACf,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1B,8BAA8B,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC9E,sBAAsB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACpE,eAAe,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACjE,yBAAyB,CAAC,EAAE,CAC1B,MAAM,EAAE,UAAU,GAAG,gBAAgB,GAAG,kBAAkB,EAC1D,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,gBAAgB,KACvB,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1B,0BAA0B,CAAC,EAAE,CAC3B,MAAM,EAAE,UAAU,GAAG,gBAAgB,GAAG,kBAAkB,EAC1D,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,gBAAgB,KACvB,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CAC3B,CAAC;AAQF,wBAAgB,kBAAkB,IAAI,eAAe,GAAG,SAAS,CAEhE;AAED,wBAAgB,2BAA2B,CAAC,KAAK,CAAC,EAAE,eAAe,GAAG,IAAI,CAKzE"}
1
+ {"version":3,"file":"test-hooks.d.ts","sourceRoot":"","sources":["../src/test-hooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE3D,MAAM,MAAM,eAAe,GAAG;IAC5B,iBAAiB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC/D,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACvE,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC3E,2BAA2B,CAAC,EAAE,CAC5B,SAAS,EAAE,OAAO,GAAG,OAAO,EAC5B,UAAU,EAAE,MAAM,KACf,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1B,2BAA2B,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACxE,+BAA+B,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7D,0BAA0B,CAAC,EAAE,CAC3B,SAAS,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,EACtC,UAAU,EAAE,MAAM,KACf,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1B,8BAA8B,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC9E,sBAAsB,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACpE,6BAA6B,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC3E,eAAe,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACjE,yBAAyB,CAAC,EAAE,CAC1B,MAAM,EAAE,UAAU,GAAG,gBAAgB,GAAG,kBAAkB,EAC1D,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,gBAAgB,KACvB,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1B,0BAA0B,CAAC,EAAE,CAC3B,MAAM,EAAE,UAAU,GAAG,gBAAgB,GAAG,kBAAkB,EAC1D,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,gBAAgB,KACvB,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CAC3B,CAAC;AAQF,wBAAgB,kBAAkB,IAAI,eAAe,GAAG,SAAS,CAEhE;AAED,wBAAgB,2BAA2B,CAAC,KAAK,CAAC,EAAE,eAAe,GAAG,IAAI,CAKzE"}
package/dist/timing.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ export declare function sleep(ms: number): Promise<void>;
2
+ export declare function sleepSync(ms: number): void;
1
3
  export declare function withTimeout<T>(promise: Promise<T>, timeoutMs: number, labelOrOptions?: string | {
2
4
  label?: string;
3
5
  message?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"timing.d.ts","sourceRoot":"","sources":["../src/timing.ts"],"names":[],"mappings":"AAAA,wBAAsB,WAAW,CAAC,CAAC,EACjC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,SAAS,EAAE,MAAM,EACjB,cAAc,GAAE,MAAM,GAAG;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,KAAK,CAAA;CAErF,GACA,OAAO,CAAC,CAAC,CAAC,CA6BZ"}
1
+ {"version":3,"file":"timing.d.ts","sourceRoot":"","sources":["../src/timing.ts"],"names":[],"mappings":"AAAA,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/C;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAK1C;AAED,wBAAsB,WAAW,CAAC,CAAC,EACjC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,SAAS,EAAE,MAAM,EACjB,cAAc,GAAE,MAAM,GAAG;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,KAAK,CAAA;CAErF,GACA,OAAO,CAAC,CAAC,CAAC,CA6BZ"}
package/dist/timing.js CHANGED
@@ -1,3 +1,12 @@
1
+ export function sleep(ms) {
2
+ return new Promise((resolve) => setTimeout(resolve, ms));
3
+ }
4
+ export function sleepSync(ms) {
5
+ if (ms <= 0) {
6
+ return;
7
+ }
8
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
9
+ }
1
10
  export async function withTimeout(promise, timeoutMs, labelOrOptions = {
2
11
  message: "timeout",
3
12
  }) {
@@ -1 +1 @@
1
- {"version":3,"file":"trash.d.ts","sourceRoot":"","sources":["../src/trash.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,sBAAsB,GAAG;IACnC,YAAY,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;CACjC,CAAC;AAwKF,wBAAsB,eAAe,CACnC,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,MAAM,CAAC,CAcjB"}
1
+ {"version":3,"file":"trash.d.ts","sourceRoot":"","sources":["../src/trash.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,sBAAsB,GAAG;IACnC,YAAY,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;CACjC,CAAC;AAkLF,wBAAsB,eAAe,CACnC,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,MAAM,CAAC,CAcjB"}
package/dist/trash.js CHANGED
@@ -117,6 +117,15 @@ function reserveTrashDestination(trashDir, base, timestamp) {
117
117
  }
118
118
  return resolveContainedPath(container, base);
119
119
  }
120
+ function copyTrashTargetSync(target, dest) {
121
+ if (target.stat.isSymbolicLink()) {
122
+ const linkTarget = fs.readlinkSync(target.path);
123
+ assertTrashTargetGuard(target);
124
+ fs.symlinkSync(linkTarget, dest);
125
+ return;
126
+ }
127
+ fs.cpSync(target.path, dest, { recursive: true, force: false, errorOnExist: true });
128
+ }
120
129
  function movePathToDestination(target, dest) {
121
130
  getFsSafeTestHooks()?.beforeTrashMove?.(target.path, dest);
122
131
  assertTrashTargetGuard(target);
@@ -134,7 +143,7 @@ function movePathToDestination(target, dest) {
134
143
  }
135
144
  try {
136
145
  assertTrashTargetGuard(target);
137
- fs.cpSync(target.path, dest, { recursive: true, force: false, errorOnExist: true });
146
+ copyTrashTargetSync(target, dest);
138
147
  assertTrashTargetGuard(target);
139
148
  guardedRmSync({ target: target.path, recursive: true, force: false, verifyAfter: false });
140
149
  return true;
@@ -1 +1 @@
1
- {"version":3,"file":"walk.d.ts","sourceRoot":"","sources":["../src/walk.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,SAAS,CAAC;AAI7B,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC;AACvE,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE9D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,aAAa,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC;IACjD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC;CAClD,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,EAAE,OAAO,CAAC;IAGnB,UAAU,CAAC,EAAE,oBAAoB,EAAE,CAAC;CACrC,CAAC;AAEF,KAAK,+BAA+B,GAAG,mBAAmB,GAAG;IAC3D,UAAU,EAAE,oBAAoB,EAAE,CAAC;CACpC,CAAC;AA8EF,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC,+BAA+B,CAwDjC;AAED,wBAAsB,aAAa,CACjC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,+BAA+B,CAAC,CAwD1C"}
1
+ {"version":3,"file":"walk.d.ts","sourceRoot":"","sources":["../src/walk.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,SAAS,CAAC;AAI7B,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC;AACvE,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE9D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,aAAa,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC;IACjD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC;CAClD,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,EAAE,OAAO,CAAC;IAGnB,UAAU,CAAC,EAAE,oBAAoB,EAAE,CAAC;CACrC,CAAC;AAEF,KAAK,+BAA+B,GAAG,mBAAmB,GAAG;IAC3D,UAAU,EAAE,oBAAoB,EAAE,CAAC;CACpC,CAAC;AA+FF,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC,+BAA+B,CAyDjC;AAED,wBAAsB,aAAa,CACjC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,+BAA+B,CAAC,CAyD1C"}
package/dist/walk.js CHANGED
@@ -1,6 +1,19 @@
1
1
  import fsSync from "node:fs";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
+ function validateWalkBudget(name, value) {
5
+ if (value !== undefined && (!Number.isSafeInteger(value) || value < 0)) {
6
+ throw new RangeError(`${name} must be a non-negative safe integer`);
7
+ }
8
+ }
9
+ function validateWalkOptions(options) {
10
+ validateWalkBudget("maxDepth", options.maxDepth);
11
+ validateWalkBudget("maxEntries", options.maxEntries);
12
+ if (options.symlinks !== undefined &&
13
+ !["skip", "follow", "include"].includes(options.symlinks)) {
14
+ throw new TypeError(`invalid walk symlink policy: ${String(options.symlinks)}`);
15
+ }
16
+ }
4
17
  function kindForDirent(dirent) {
5
18
  if (dirent.isDirectory())
6
19
  return "directory";
@@ -75,6 +88,7 @@ async function resolveAsyncKind(fullPath, dirent, symlinks) {
75
88
  return "other";
76
89
  }
77
90
  export function walkDirectorySync(rootDir, options = {}) {
91
+ validateWalkOptions(options);
78
92
  const root = path.resolve(rootDir);
79
93
  const symlinks = options.symlinks ?? "skip";
80
94
  const result = {
@@ -133,6 +147,7 @@ export function walkDirectorySync(rootDir, options = {}) {
133
147
  return result;
134
148
  }
135
149
  export async function walkDirectory(rootDir, options = {}) {
150
+ validateWalkOptions(options);
136
151
  const root = path.resolve(rootDir);
137
152
  const symlinks = options.symlinks ?? "skip";
138
153
  const result = {