@openclaw/fs-safe 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (302) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/LICENSE +1 -0
  3. package/README.md +39 -6
  4. package/dist/absolute-path.d.ts.map +1 -1
  5. package/dist/absolute-path.js +3 -2
  6. package/dist/advanced.d.ts +5 -1
  7. package/dist/advanced.d.ts.map +1 -1
  8. package/dist/advanced.js +5 -1
  9. package/dist/archive-durability.d.ts +6 -6
  10. package/dist/archive-durability.d.ts.map +1 -1
  11. package/dist/archive-durability.js +1 -1
  12. package/dist/archive-entry.d.ts.map +1 -1
  13. package/dist/archive-entry.js +8 -8
  14. package/dist/archive-gzip-tail.d.ts +1 -0
  15. package/dist/archive-gzip-tail.d.ts.map +1 -1
  16. package/dist/archive-gzip-tail.js +16 -9
  17. package/dist/archive-input.d.ts.map +1 -1
  18. package/dist/archive-input.js +4 -2
  19. package/dist/archive-merge.d.ts +5 -1
  20. package/dist/archive-merge.d.ts.map +1 -1
  21. package/dist/archive-merge.js +15 -12
  22. package/dist/archive-native.js +4 -4
  23. package/dist/archive-parser.wasm +0 -0
  24. package/dist/archive-read.d.ts.map +1 -1
  25. package/dist/archive-read.js +54 -42
  26. package/dist/archive-staging.d.ts +6 -3
  27. package/dist/archive-staging.d.ts.map +1 -1
  28. package/dist/archive-staging.js +42 -22
  29. package/dist/archive-tar-stream.d.ts.map +1 -1
  30. package/dist/archive-tar-stream.js +7 -6
  31. package/dist/archive-tar-wasm.d.ts.map +1 -1
  32. package/dist/archive-tar-wasm.js +16 -13
  33. package/dist/archive-zip-admission.d.ts +1 -1
  34. package/dist/archive-zip-admission.d.ts.map +1 -1
  35. package/dist/archive-zip-admission.js +48 -12
  36. package/dist/archive-zip-loader.d.ts +6 -0
  37. package/dist/archive-zip-loader.d.ts.map +1 -0
  38. package/dist/archive-zip-loader.js +38 -0
  39. package/dist/archive-zip-names.d.ts.map +1 -1
  40. package/dist/archive-zip-names.js +26 -9
  41. package/dist/archive-zip-preflight.d.ts +2 -3
  42. package/dist/archive-zip-preflight.d.ts.map +1 -1
  43. package/dist/archive-zip-preflight.js +2 -34
  44. package/dist/archive.d.ts.map +1 -1
  45. package/dist/archive.js +12 -10
  46. package/dist/bounded-read.d.ts +5 -0
  47. package/dist/bounded-read.d.ts.map +1 -1
  48. package/dist/bounded-read.js +18 -11
  49. package/dist/copy-file-input.d.ts +0 -1
  50. package/dist/copy-file-input.d.ts.map +1 -1
  51. package/dist/copy-file-input.js +4 -26
  52. package/dist/copy-tree-portable.d.ts.map +1 -1
  53. package/dist/copy-tree-portable.js +57 -26
  54. package/dist/copy.d.ts +1 -1
  55. package/dist/copy.d.ts.map +1 -1
  56. package/dist/copy.js +3 -1
  57. package/dist/device-path.d.ts.map +1 -1
  58. package/dist/device-path.js +5 -3
  59. package/dist/directory-durability.d.ts.map +1 -1
  60. package/dist/directory-durability.js +5 -4
  61. package/dist/directory-guard.d.ts +11 -1
  62. package/dist/directory-guard.d.ts.map +1 -1
  63. package/dist/directory-guard.js +53 -11
  64. package/dist/durability.d.ts +1 -1
  65. package/dist/durability.d.ts.map +1 -1
  66. package/dist/durability.js +1 -1
  67. package/dist/file-handle-transfer.d.ts +14 -0
  68. package/dist/file-handle-transfer.d.ts.map +1 -0
  69. package/dist/file-handle-transfer.js +64 -0
  70. package/dist/file-hash.d.ts +3 -0
  71. package/dist/file-hash.d.ts.map +1 -1
  72. package/dist/file-hash.js +99 -32
  73. package/dist/file-lock-sync.d.ts.map +1 -1
  74. package/dist/file-lock-sync.js +8 -4
  75. package/dist/file-store-boundary.d.ts.map +1 -1
  76. package/dist/file-store-boundary.js +7 -5
  77. package/dist/file-store-path.d.ts +3 -0
  78. package/dist/file-store-path.d.ts.map +1 -0
  79. package/dist/file-store-path.js +27 -0
  80. package/dist/file-store-prune.d.ts.map +1 -1
  81. package/dist/file-store-prune.js +15 -5
  82. package/dist/file-store-sync-write.d.ts.map +1 -1
  83. package/dist/file-store-sync-write.js +56 -44
  84. package/dist/file-store.d.ts.map +1 -1
  85. package/dist/file-store.js +2 -18
  86. package/dist/filename.d.ts +1 -0
  87. package/dist/filename.d.ts.map +1 -1
  88. package/dist/filename.js +32 -14
  89. package/dist/guarded-mkdir.d.ts +2 -0
  90. package/dist/guarded-mkdir.d.ts.map +1 -1
  91. package/dist/guarded-mkdir.js +52 -16
  92. package/dist/guest-dispatch-python.d.ts +2 -0
  93. package/dist/guest-dispatch-python.d.ts.map +1 -0
  94. package/dist/guest-dispatch-python.js +117 -0
  95. package/dist/guest-native-python.d.ts +4 -0
  96. package/dist/guest-native-python.d.ts.map +1 -0
  97. package/dist/guest-native-python.js +135 -0
  98. package/dist/guest.d.ts +9 -0
  99. package/dist/guest.d.ts.map +1 -0
  100. package/dist/guest.js +421 -0
  101. package/dist/index.d.ts +1 -1
  102. package/dist/index.d.ts.map +1 -1
  103. package/dist/install-path.d.ts +6 -0
  104. package/dist/install-path.d.ts.map +1 -1
  105. package/dist/install-path.js +16 -2
  106. package/dist/json-durable-queue-directory.js +3 -3
  107. package/dist/json-durable-queue-ownership.d.ts +2 -0
  108. package/dist/json-durable-queue-ownership.d.ts.map +1 -1
  109. package/dist/json-durable-queue-ownership.js +47 -3
  110. package/dist/json-durable-queue-read.d.ts +6 -0
  111. package/dist/json-durable-queue-read.d.ts.map +1 -0
  112. package/dist/json-durable-queue-read.js +59 -0
  113. package/dist/json-durable-queue.d.ts +1 -1
  114. package/dist/json-durable-queue.d.ts.map +1 -1
  115. package/dist/json-durable-queue.js +43 -84
  116. package/dist/json.d.ts.map +1 -1
  117. package/dist/json.js +2 -1
  118. package/dist/local-roots.d.ts.map +1 -1
  119. package/dist/local-roots.js +2 -1
  120. package/dist/move-path-stage.d.ts.map +1 -1
  121. package/dist/move-path-stage.js +2 -1
  122. package/dist/move-path.d.ts.map +1 -1
  123. package/dist/move-path.js +4 -3
  124. package/dist/mutation-authority.d.ts +1 -0
  125. package/dist/mutation-authority.d.ts.map +1 -1
  126. package/dist/mutation-authority.js +4 -4
  127. package/dist/native-binding.d.ts +14 -2
  128. package/dist/native-binding.d.ts.map +1 -1
  129. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  130. package/dist/native-pinned-write-windows.js +3 -2
  131. package/dist/native-pinned-write.d.ts.map +1 -1
  132. package/dist/native-pinned-write.js +2 -1
  133. package/dist/opened-realpath.d.ts.map +1 -1
  134. package/dist/opened-realpath.js +5 -4
  135. package/dist/output.d.ts +2 -0
  136. package/dist/output.d.ts.map +1 -1
  137. package/dist/output.js +2 -0
  138. package/dist/overwrite-file-handle.d.ts +8 -0
  139. package/dist/overwrite-file-handle.d.ts.map +1 -0
  140. package/dist/overwrite-file-handle.js +42 -0
  141. package/dist/path-case.d.ts +7 -0
  142. package/dist/path-case.d.ts.map +1 -0
  143. package/dist/path-case.js +136 -0
  144. package/dist/path-scope-lexical.d.ts +14 -0
  145. package/dist/path-scope-lexical.d.ts.map +1 -0
  146. package/dist/path-scope-lexical.js +27 -0
  147. package/dist/path.d.ts.map +1 -1
  148. package/dist/path.js +22 -3
  149. package/dist/permissions-windows.d.ts +1 -1
  150. package/dist/permissions-windows.d.ts.map +1 -1
  151. package/dist/permissions-windows.js +48 -6
  152. package/dist/pinned-open.d.ts.map +1 -1
  153. package/dist/pinned-open.js +3 -1
  154. package/dist/pinned-write.d.ts +2 -2
  155. package/dist/pinned-write.d.ts.map +1 -1
  156. package/dist/pinned-write.js +3 -1
  157. package/dist/private-temp-workspace.d.ts.map +1 -1
  158. package/dist/private-temp-workspace.js +6 -4
  159. package/dist/realpath.d.ts +4 -0
  160. package/dist/realpath.d.ts.map +1 -0
  161. package/dist/realpath.js +43 -0
  162. package/dist/recursive-mkdir-path.d.ts +3 -0
  163. package/dist/recursive-mkdir-path.d.ts.map +1 -0
  164. package/dist/recursive-mkdir-path.js +8 -0
  165. package/dist/replace-directory.d.ts.map +1 -1
  166. package/dist/replace-directory.js +2 -1
  167. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  168. package/dist/replace-file-copy-fallback.js +23 -31
  169. package/dist/replace-file-copy-source.d.ts.map +1 -1
  170. package/dist/replace-file-copy-source.js +7 -12
  171. package/dist/replace-file-mode.d.ts +3 -0
  172. package/dist/replace-file-mode.d.ts.map +1 -0
  173. package/dist/replace-file-mode.js +10 -0
  174. package/dist/replace-file.d.ts +1 -0
  175. package/dist/replace-file.d.ts.map +1 -1
  176. package/dist/replace-file.js +14 -8
  177. package/dist/root-boundary.d.ts +25 -0
  178. package/dist/root-boundary.d.ts.map +1 -0
  179. package/dist/root-boundary.js +177 -0
  180. package/dist/root-context.d.ts.map +1 -1
  181. package/dist/root-context.js +40 -13
  182. package/dist/root-create-input.d.ts +10 -0
  183. package/dist/root-create-input.d.ts.map +1 -0
  184. package/dist/root-create-input.js +80 -0
  185. package/dist/root-directory-list.d.ts +3 -1
  186. package/dist/root-directory-list.d.ts.map +1 -1
  187. package/dist/root-directory-list.js +31 -5
  188. package/dist/root-entries.d.ts +11 -0
  189. package/dist/root-entries.d.ts.map +1 -0
  190. package/dist/root-entries.js +61 -0
  191. package/dist/root-errors.d.ts +5 -5
  192. package/dist/root-errors.d.ts.map +1 -1
  193. package/dist/root-errors.js +13 -12
  194. package/dist/root-impl.d.ts +13 -3
  195. package/dist/root-impl.d.ts.map +1 -1
  196. package/dist/root-impl.js +93 -70
  197. package/dist/root-move-preflight.d.ts +8 -0
  198. package/dist/root-move-preflight.d.ts.map +1 -0
  199. package/dist/root-move-preflight.js +16 -0
  200. package/dist/root-options.d.ts +12 -1
  201. package/dist/root-options.d.ts.map +1 -1
  202. package/dist/root-path-existing.d.ts +2 -0
  203. package/dist/root-path-existing.d.ts.map +1 -1
  204. package/dist/root-path-existing.js +14 -5
  205. package/dist/root-path-symlink.d.ts.map +1 -1
  206. package/dist/root-path-symlink.js +3 -2
  207. package/dist/root-path.d.ts +2 -0
  208. package/dist/root-path.d.ts.map +1 -1
  209. package/dist/root-path.js +54 -26
  210. package/dist/root-paths.d.ts +2 -6
  211. package/dist/root-paths.d.ts.map +1 -1
  212. package/dist/root-paths.js +24 -32
  213. package/dist/root-remove.d.ts +5 -0
  214. package/dist/root-remove.d.ts.map +1 -0
  215. package/dist/root-remove.js +286 -0
  216. package/dist/root-symlink-policy.d.ts +2 -1
  217. package/dist/root-symlink-policy.d.ts.map +1 -1
  218. package/dist/root-symlink-policy.js +2 -2
  219. package/dist/root-walk.d.ts.map +1 -1
  220. package/dist/root-walk.js +2 -1
  221. package/dist/root-write-mode.d.ts +2 -0
  222. package/dist/root-write-mode.d.ts.map +1 -1
  223. package/dist/root-write-mode.js +21 -7
  224. package/dist/root-write-verification.d.ts.map +1 -1
  225. package/dist/root-write-verification.js +12 -3
  226. package/dist/root.d.ts +2 -1
  227. package/dist/root.d.ts.map +1 -1
  228. package/dist/safe-path-segment.d.ts.map +1 -1
  229. package/dist/safe-path-segment.js +3 -1
  230. package/dist/secret-file.d.ts.map +1 -1
  231. package/dist/secret-file.js +2 -1
  232. package/dist/secret-read-async.d.ts.map +1 -1
  233. package/dist/secret-read-async.js +2 -1
  234. package/dist/secure-file-windows.d.ts +10 -0
  235. package/dist/secure-file-windows.d.ts.map +1 -0
  236. package/dist/secure-file-windows.js +186 -0
  237. package/dist/secure-file.d.ts.map +1 -1
  238. package/dist/secure-file.js +27 -7
  239. package/dist/secure-temp-dir.d.ts.map +1 -1
  240. package/dist/secure-temp-dir.js +2 -1
  241. package/dist/sibling-staged-file.d.ts +2 -0
  242. package/dist/sibling-staged-file.d.ts.map +1 -1
  243. package/dist/sibling-staged-file.js +49 -8
  244. package/dist/sibling-temp.d.ts +2 -0
  245. package/dist/sibling-temp.d.ts.map +1 -1
  246. package/dist/sibling-temp.js +8 -5
  247. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  248. package/dist/sidecar-lock-acquire.js +16 -5
  249. package/dist/sidecar-lock-policy.d.ts +2 -0
  250. package/dist/sidecar-lock-policy.d.ts.map +1 -1
  251. package/dist/sidecar-lock-policy.js +17 -0
  252. package/dist/sidecar-lock.d.ts.map +1 -1
  253. package/dist/sidecar-lock.js +2 -3
  254. package/dist/staged-directory.d.ts.map +1 -1
  255. package/dist/staged-directory.js +4 -3
  256. package/dist/temp-target.d.ts +14 -12
  257. package/dist/temp-target.d.ts.map +1 -1
  258. package/dist/temp-target.js +15 -7
  259. package/dist/trash.d.ts.map +1 -1
  260. package/dist/trash.js +7 -5
  261. package/dist/unicode-path.d.ts +3 -0
  262. package/dist/unicode-path.d.ts.map +1 -0
  263. package/dist/unicode-path.js +13 -0
  264. package/dist/walk.d.ts.map +1 -1
  265. package/dist/walk.js +14 -12
  266. package/dist/write-file-handle.d.ts +1 -0
  267. package/dist/write-file-handle.d.ts.map +1 -1
  268. package/dist/write-file-handle.js +3 -2
  269. package/docs/advanced.md +7 -1
  270. package/docs/archive.md +31 -5
  271. package/docs/atomic.md +17 -1
  272. package/docs/config.md +1 -0
  273. package/docs/contributing.md +33 -2
  274. package/docs/copy.md +75 -6
  275. package/docs/directory-identity.md +85 -0
  276. package/docs/durability.md +40 -3
  277. package/docs/entries.md +109 -0
  278. package/docs/errors.md +3 -3
  279. package/docs/file-store.md +21 -0
  280. package/docs/filename.md +9 -2
  281. package/docs/guest.md +146 -0
  282. package/docs/in-place-write.md +81 -0
  283. package/docs/index.md +2 -0
  284. package/docs/install-path.md +59 -13
  285. package/docs/install.md +34 -0
  286. package/docs/native-helper.md +14 -5
  287. package/docs/native.md +18 -2
  288. package/docs/output.md +32 -6
  289. package/docs/path-case.md +64 -0
  290. package/docs/path-scope.md +1 -1
  291. package/docs/path.md +1 -1
  292. package/docs/permissions.md +37 -3
  293. package/docs/public-api.md +36 -2
  294. package/docs/root.md +33 -3
  295. package/docs/secure-file.md +17 -14
  296. package/docs/security-model.md +1 -1
  297. package/docs/sidecar-lock.md +12 -3
  298. package/docs/store.md +22 -1
  299. package/docs/temp.md +39 -6
  300. package/docs/types.md +1 -1
  301. package/docs/writing.md +153 -3
  302. package/package.json +14 -8
@@ -37,9 +37,6 @@ function resolveManagerState(key) {
37
37
  // Backfill state created by fs-safe versions that predate reclaim guards.
38
38
  state.reclaimCleanupRegistered ??= false;
39
39
  state.reclaimGuards ??= new Set();
40
- for (const held of state.held.values()) {
41
- held.refCount ??= 1;
42
- }
43
40
  }
44
41
  return state;
45
42
  }
@@ -163,6 +160,8 @@ async function releaseHeldLock(state, normalizedTargetPath, held, options = {})
163
160
  await held.releasePromise;
164
161
  return true;
165
162
  }
163
+ // Older package copies can add holders after this manager was constructed.
164
+ held.refCount ??= 1;
166
165
  if (options.force) {
167
166
  held.refCount = 0;
168
167
  }
@@ -1 +1 @@
1
- {"version":3,"file":"staged-directory.d.ts","sourceRoot":"","sources":["../src/staged-directory.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAElE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhE,KAAK,iBAAiB,GAAG,iBAAiB,CAAC,WAAW,CAAC,CAAC;AAExD,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,gBAAgB,EAC1B,MAAM,EAAE,QAAQ,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,GAC7C,OAAO,CAKT;AAED,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,iBAAiB,CAYvF;AAED,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAQ7E;AAED,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,gBAAgB,GAAG;IACzE,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,iBAAiB,CAAC;CAC5B,CA+BA"}
1
+ {"version":3,"file":"staged-directory.d.ts","sourceRoot":"","sources":["../src/staged-directory.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAElE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAE3D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhE,KAAK,iBAAiB,GAAG,iBAAiB,CAAC,WAAW,CAAC,CAAC;AAExD,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,gBAAgB,EAC1B,MAAM,EAAE,QAAQ,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,GAC7C,OAAO,CAKT;AAED,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,iBAAiB,CAYvF;AAED,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI,CAQ7E;AAED,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,GAAG,gBAAgB,GAAG;IACzE,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,iBAAiB,CAAC;CAC5B,CA+BA"}
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { FsSafeError } from "./errors.js";
4
+ import { realpathSync } from "./realpath.js";
4
5
  export function exactIdentityMatches(expected, actual) {
5
6
  return ["dev", "ino"].every((key) => {
6
7
  const value = expected[key];
@@ -14,7 +15,7 @@ export function describeStagedDirectory(fd, pathname) {
14
15
  }
15
16
  const receipt = Object.freeze({
16
17
  path: path.resolve(pathname),
17
- realPath: fs.realpathSync(pathname),
18
+ realPath: realpathSync(pathname),
18
19
  identity: Object.freeze({ dev: identity.dev, ino: identity.ino }),
19
20
  });
20
21
  assertStagedDirectoryCurrent(receipt);
@@ -23,7 +24,7 @@ export function describeStagedDirectory(fd, pathname) {
23
24
  export function assertStagedDirectoryCurrent(receipt) {
24
25
  const current = fs.lstatSync(receipt.path, { bigint: true });
25
26
  if (!current.isDirectory() || !exactIdentityMatches(receipt.identity, current) ||
26
- fs.realpathSync(receipt.path) !== receipt.realPath) {
27
+ realpathSync(receipt.path) !== receipt.realPath) {
27
28
  throw new FsSafeError("path-mismatch", "staging directory pathname changed");
28
29
  }
29
30
  }
@@ -37,7 +38,7 @@ export function openStagedDirectory(directory) {
37
38
  if (!before.isDirectory()) {
38
39
  throw new FsSafeError("not-file", "staging parent must be a real directory");
39
40
  }
40
- if (expected && (!exactIdentityMatches(expected, before) || fs.realpathSync(pathname) !== expected.realPath)) {
41
+ if (expected && (!exactIdentityMatches(expected, before) || realpathSync(pathname) !== expected.realPath)) {
41
42
  throw new FsSafeError("path-mismatch", "stale staging directory receipt");
42
43
  }
43
44
  const fd = fs.openSync(pathname, fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
@@ -1,3 +1,4 @@
1
+ import fsSync from "node:fs";
1
2
  export type TempFile = {
2
3
  dir: string;
3
4
  path: string;
@@ -5,6 +6,12 @@ export type TempFile = {
5
6
  cleanup: () => Promise<void>;
6
7
  [Symbol.asyncDispose](): Promise<void>;
7
8
  };
9
+ type TempFileOptions = {
10
+ rootDir?: string;
11
+ prefix: string;
12
+ fileName?: string;
13
+ onCleanupError?: (error: unknown) => void;
14
+ };
8
15
  export declare function sanitizeTempFileName(fileName: string): string;
9
16
  export declare function buildRandomTempFilePath(params: {
10
17
  rootDir?: string;
@@ -13,16 +20,11 @@ export declare function buildRandomTempFilePath(params: {
13
20
  now?: number;
14
21
  uuid?: string;
15
22
  }): string;
16
- export declare function tempFile(params: {
17
- rootDir?: string;
18
- prefix: string;
19
- fileName?: string;
20
- onCleanupError?: (error: unknown) => void;
21
- }): Promise<TempFile>;
22
- export declare function withTempFile<T>(params: {
23
- rootDir?: string;
24
- prefix: string;
25
- fileName?: string;
26
- onCleanupError?: (error: unknown) => void;
27
- }, fn: (tmpPath: string) => Promise<T>): Promise<T>;
23
+ export declare function createOwnedTempFile(params: TempFileOptions): Promise<{
24
+ target: TempFile;
25
+ identity: Readonly<Pick<fsSync.BigIntStats, "dev" | "ino">>;
26
+ }>;
27
+ export declare function tempFile(params: TempFileOptions): Promise<TempFile>;
28
+ export declare function withTempFile<T>(params: TempFileOptions, fn: (tmpPath: string) => Promise<T>): Promise<T>;
29
+ export {};
28
30
  //# sourceMappingURL=temp-target.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"temp-target.d.ts","sourceRoot":"","sources":["../src/temp-target.ts"],"names":[],"mappings":"AASA,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;AA8DF,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;AAyCD,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,CAwBpB;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":"AACA,OAAO,MAAM,MAAM,SAAS,CAAC;AAS7B,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;AAEF,KAAK,eAAe,GAAG;IACrB,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,CAAC;AA8DF,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAK7D;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;AAyCD,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC;IAC1E,MAAM,EAAE,QAAQ,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC;CAC7D,CAAC,CA2BD;AAED,wBAAsB,QAAQ,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,QAAQ,CAAC,CAEzE;AAED,wBAAsB,YAAY,CAAC,CAAC,EAClC,MAAM,EAAE,eAAe,EACvB,EAAE,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GAClC,OAAO,CAAC,CAAC,CAAC,CAOZ"}
@@ -2,6 +2,7 @@ import crypto 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 { suffixWindowsReservedDeviceName } from "./filename.js";
5
6
  import { sameFileIdentityForCleanup } from "./file-identity.js";
6
7
  import { assertSafePathSegment, sanitizeSafePathSegment, trimHyphenEdges } from "./safe-path-segment.js";
7
8
  import { resolveSecureTempRoot } from "./secure-temp-dir.js";
@@ -57,9 +58,10 @@ function sanitizeExtension(extension) {
57
58
  return token ? `.${token}` : "";
58
59
  }
59
60
  export function sanitizeTempFileName(fileName) {
60
- return sanitizeSafePathSegment(path.basename(fileName), "download.bin", {
61
+ const sanitized = sanitizeSafePathSegment(path.basename(fileName), "download.bin", {
61
62
  allowDotPrefix: true,
62
63
  });
64
+ return suffixWindowsReservedDeviceName(sanitized);
63
65
  }
64
66
  export function buildRandomTempFilePath(params) {
65
67
  const rootDir = resolveTempRoot(params.rootDir);
@@ -106,7 +108,7 @@ async function cleanupTempDir(dir, identity, onCleanupError) {
106
108
  function resolveTempRoot(rootDir) {
107
109
  return path.resolve(rootDir ?? resolveSecureTempRoot({ fallbackPrefix: "fs-safe" }));
108
110
  }
109
- export async function tempFile(params) {
111
+ export async function createOwnedTempFile(params) {
110
112
  const rootDir = resolveTempRoot(params.rootDir);
111
113
  const prefix = `${sanitizePrefix(params.prefix)}-`;
112
114
  const dir = await fs.mkdtemp(path.join(rootDir, prefix));
@@ -124,13 +126,19 @@ export async function tempFile(params) {
124
126
  }
125
127
  };
126
128
  return {
127
- dir,
128
- path: file(),
129
- file,
130
- cleanup,
131
- [Symbol.asyncDispose]: cleanup,
129
+ target: {
130
+ dir,
131
+ path: file(),
132
+ file,
133
+ cleanup,
134
+ [Symbol.asyncDispose]: cleanup,
135
+ },
136
+ identity: Object.freeze({ dev: identity.dev, ino: identity.ino }),
132
137
  };
133
138
  }
139
+ export async function tempFile(params) {
140
+ return (await createOwnedTempFile(params)).target;
141
+ }
134
142
  export async function withTempFile(params, fn) {
135
143
  const target = await tempFile(params);
136
144
  try {
@@ -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;AAkLF,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":"AASA,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
@@ -3,6 +3,8 @@ import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { sameFileIdentity } from "./file-identity.js";
5
5
  import { guardedRenameSync, guardedRmSync } from "./guarded-mutation.js";
6
+ import { realpathSync } from "./realpath.js";
7
+ import { recursiveMkdirPath } from "./recursive-mkdir-path.js";
6
8
  import { getFsSafeTestHooks } from "./test-hooks.js";
7
9
  const TRASH_DESTINATION_COLLISION_CODES = new Set(["EEXIST", "ENOTEMPTY", "ERR_FS_CP_EEXIST"]);
8
10
  const TRASH_DESTINATION_RETRY_LIMIT = 4;
@@ -26,7 +28,7 @@ function resolveAllowedTrashRoots(allowedRoots) {
26
28
  try {
27
29
  // Keep both spellings: broken symlink targets cannot be realpathed and
28
30
  // may only compare equal to the caller's lexical allowed root.
29
- return [path.resolve(fs.realpathSync.native(root)), lexicalRoot];
31
+ return [path.resolve(realpathSync.native(root)), lexicalRoot];
30
32
  }
31
33
  catch {
32
34
  return [lexicalRoot];
@@ -36,7 +38,7 @@ function resolveAllowedTrashRoots(allowedRoots) {
36
38
  }
37
39
  function resolveTrashTargetPath(targetPath) {
38
40
  try {
39
- return { path: path.resolve(fs.realpathSync.native(targetPath)), resolved: true };
41
+ return { path: path.resolve(realpathSync.native(targetPath)), resolved: true };
40
42
  }
41
43
  catch {
42
44
  // Broken symlinks are valid trash targets. Fall back to the lexical path,
@@ -75,13 +77,13 @@ function assertTrashTargetGuard(guard) {
75
77
  function resolveTrashDir() {
76
78
  const homeDir = os.homedir();
77
79
  const trashDir = path.join(homeDir, ".Trash");
78
- fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
80
+ fs.mkdirSync(recursiveMkdirPath(trashDir), { recursive: true, mode: 0o700 });
79
81
  const trashDirStat = fs.lstatSync(trashDir);
80
82
  if (!trashDirStat.isDirectory() || trashDirStat.isSymbolicLink()) {
81
83
  throw new Error(`Refusing to use non-directory/symlink trash directory: ${trashDir}`);
82
84
  }
83
- const realHome = path.resolve(fs.realpathSync.native(homeDir));
84
- const resolvedTrashDir = path.resolve(fs.realpathSync.native(trashDir));
85
+ const realHome = path.resolve(realpathSync.native(homeDir));
86
+ const resolvedTrashDir = path.resolve(realpathSync.native(trashDir));
85
87
  if (resolvedTrashDir === realHome || !isSameOrChildPath(resolvedTrashDir, realHome)) {
86
88
  throw new Error(`Trash directory escaped home directory: ${trashDir}`);
87
89
  }
@@ -0,0 +1,3 @@
1
+ export declare function lowerCaseNfc(value: string): string;
2
+ export declare function maxNormalizedUtf8Bytes(value: string, includeRaw?: boolean): number;
3
+ //# sourceMappingURL=unicode-path.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unicode-path.d.ts","sourceRoot":"","sources":["../src/unicode-path.ts"],"names":[],"mappings":"AAEA,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,UAAQ,GAAG,MAAM,CAUhF"}
@@ -0,0 +1,13 @@
1
+ const NON_ASCII = /[^\x00-\x7f]/;
2
+ export function lowerCaseNfc(value) {
3
+ return NON_ASCII.test(value) ? value.normalize("NFC").toLowerCase().normalize("NFC") : value.toLowerCase();
4
+ }
5
+ export function maxNormalizedUtf8Bytes(value, includeRaw = false) {
6
+ const nfc = value.normalize("NFC");
7
+ const bytes = Buffer.byteLength(nfc, "utf8");
8
+ const maximum = includeRaw && nfc !== value ? Math.max(bytes, Buffer.byteLength(value, "utf8")) : bytes;
9
+ // ASCII NFC output also has identical NFD and one UTF-8 byte per code unit.
10
+ if (bytes === nfc.length)
11
+ return maximum;
12
+ return Math.max(maximum, Buffer.byteLength(value.normalize("NFD"), "utf8"));
13
+ }
@@ -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;AAgFF,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"}
1
+ {"version":3,"file":"walk.d.ts","sourceRoot":"","sources":["../src/walk.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,SAAS,CAAC;AAK7B,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+EF,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC,+BAA+B,CA0DjC;AAED,wBAAsB,aAAa,CACjC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,+BAA+B,CAAC,CA0D1C"}
package/dist/walk.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import fsSync from "node:fs";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
+ import { realpathSync } from "./realpath.js";
4
5
  function validateWalkBudget(name, value) {
5
6
  if (value !== undefined && (!Number.isSafeInteger(value) || value < 0)) {
6
7
  throw new RangeError(`${name} must be a non-negative safe integer`);
@@ -28,11 +29,10 @@ function shouldStop(result, options) {
28
29
  }
29
30
  function buildEntry(params) {
30
31
  const fullPath = params.fullPath;
31
- const relativePath = path.relative(params.rootDir, fullPath) || params.dirent.name;
32
32
  return {
33
33
  name: params.dirent.name,
34
34
  path: fullPath,
35
- relativePath,
35
+ relativePath: params.relativePath,
36
36
  depth: params.depth,
37
37
  kind: params.kind ?? kindForDirent(params.dirent),
38
38
  dirent: params.dirent,
@@ -78,12 +78,12 @@ export function walkDirectorySync(rootDir, options = {}) {
78
78
  failedDirs: [],
79
79
  };
80
80
  const visitedDirs = new Set();
81
- function visit(dir, depth) {
81
+ function visit(dir, relativeDir, depth) {
82
82
  if (options.maxDepth !== undefined && depth > options.maxDepth)
83
83
  return;
84
84
  let realDir;
85
85
  try {
86
- realDir = fsSync.realpathSync(dir);
86
+ realDir = realpathSync(dir);
87
87
  }
88
88
  catch (error) {
89
89
  recordFailedDir(result, root, dir, depth, error);
@@ -110,20 +110,21 @@ export function walkDirectorySync(rootDir, options = {}) {
110
110
  const kind = resolveKind(fullPath, dirent, symlinks);
111
111
  if (!kind)
112
112
  continue;
113
- const entry = buildEntry({ rootDir: root, fullPath, dirent, depth, kind });
113
+ const relativePath = relativeDir ? `${relativeDir}${path.sep}${dirent.name}` : dirent.name;
114
+ const entry = buildEntry({ relativePath, fullPath, dirent, depth, kind });
114
115
  if (options.include?.(entry) ?? true) {
115
116
  result.entries.push(entry);
116
117
  }
117
118
  if (kind === "directory" &&
118
119
  (options.maxDepth === undefined || depth < options.maxDepth) &&
119
120
  (options.descend?.(entry) ?? true)) {
120
- visit(fullPath, depth + 1);
121
+ visit(fullPath, relativePath, depth + 1);
121
122
  if (result.truncated)
122
123
  return;
123
124
  }
124
125
  }
125
126
  }
126
- visit(root, 1);
127
+ visit(root, "", 1);
127
128
  return result;
128
129
  }
129
130
  export async function walkDirectory(rootDir, options = {}) {
@@ -137,12 +138,12 @@ export async function walkDirectory(rootDir, options = {}) {
137
138
  failedDirs: [],
138
139
  };
139
140
  const visitedDirs = new Set();
140
- async function visit(dir, depth) {
141
+ async function visit(dir, relativeDir, depth) {
141
142
  if (options.maxDepth !== undefined && depth > options.maxDepth)
142
143
  return;
143
144
  let realDir;
144
145
  try {
145
- realDir = fsSync.realpathSync.native(dir);
146
+ realDir = realpathSync.native(dir);
146
147
  }
147
148
  catch (error) {
148
149
  recordFailedDir(result, root, dir, depth, error);
@@ -169,19 +170,20 @@ export async function walkDirectory(rootDir, options = {}) {
169
170
  const kind = resolveKind(fullPath, dirent, symlinks);
170
171
  if (!kind)
171
172
  continue;
172
- const entry = buildEntry({ rootDir: root, fullPath, dirent, depth, kind });
173
+ const relativePath = relativeDir ? `${relativeDir}${path.sep}${dirent.name}` : dirent.name;
174
+ const entry = buildEntry({ relativePath, fullPath, dirent, depth, kind });
173
175
  if (options.include?.(entry) ?? true) {
174
176
  result.entries.push(entry);
175
177
  }
176
178
  if (kind === "directory" &&
177
179
  (options.maxDepth === undefined || depth < options.maxDepth) &&
178
180
  (options.descend?.(entry) ?? true)) {
179
- await visit(fullPath, depth + 1);
181
+ await visit(fullPath, relativePath, depth + 1);
180
182
  if (result.truncated)
181
183
  return;
182
184
  }
183
185
  }
184
186
  }
185
- await visit(root, 1);
187
+ await visit(root, "", 1);
186
188
  return result;
187
189
  }
@@ -1,6 +1,7 @@
1
1
  import type { FileHandle } from "node:fs/promises";
2
2
  export declare function writeAllToFile(target: FileHandle | number, data: string | Uint8Array, options?: {
3
3
  encoding?: BufferEncoding;
4
+ position?: number;
4
5
  assertBeforeMutation?: () => void;
5
6
  }): Promise<void>;
6
7
  //# sourceMappingURL=write-file-handle.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"write-file-handle.d.ts","sourceRoot":"","sources":["../src/write-file-handle.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAKnD,wBAAsB,cAAc,CAClC,MAAM,EAAE,UAAU,GAAG,MAAM,EAC3B,IAAI,EAAE,MAAM,GAAG,UAAU,EACzB,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,cAAc,CAAC;IAAC,oBAAoB,CAAC,EAAE,MAAM,IAAI,CAAA;CAAO,GAC7E,OAAO,CAAC,IAAI,CAAC,CAmBf"}
1
+ {"version":3,"file":"write-file-handle.d.ts","sourceRoot":"","sources":["../src/write-file-handle.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAKnD,wBAAsB,cAAc,CAClC,MAAM,EAAE,UAAU,GAAG,MAAM,EAC3B,IAAI,EAAE,MAAM,GAAG,UAAU,EACzB,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,cAAc,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,oBAAoB,CAAC,EAAE,MAAM,IAAI,CAAA;CAAO,GAChG,OAAO,CAAC,IAAI,CAAC,CAoBf"}
@@ -6,17 +6,18 @@ export async function writeAllToFile(target, data, options = {}) {
6
6
  let offset = 0;
7
7
  while (offset < buffer.byteLength) {
8
8
  const length = Math.min(WRITE_CHUNK_BYTES, buffer.byteLength - offset);
9
+ const position = options.position === undefined ? null : options.position + offset;
9
10
  options.assertBeforeMutation?.();
10
11
  const written = typeof target === "number"
11
12
  ? await new Promise((resolve, reject) => {
12
- fs.write(target, buffer, offset, length, null, (error, bytesWritten) => {
13
+ fs.write(target, buffer, offset, length, position, (error, bytesWritten) => {
13
14
  if (error)
14
15
  reject(error);
15
16
  else
16
17
  resolve(bytesWritten);
17
18
  });
18
19
  })
19
- : (await target.write(buffer, offset, length, null)).bytesWritten;
20
+ : (await target.write(buffer, offset, length, position)).bytesWritten;
20
21
  if (written <= 0) {
21
22
  throw new FsSafeError("helper-failed", "file write made no progress");
22
23
  }
package/docs/advanced.md CHANGED
@@ -32,6 +32,7 @@ The exports group into a handful of themes. Each documented helper has its own p
32
32
  | `resolveWritablePathWithinRoot` | – | Resolve a write target inside a root. |
33
33
  | `resolveRootPath`, `resolveRootPathSync`, `ResolvedRootPath`, `ROOT_PATH_ALIAS_POLICIES`, `RootPathAliasPolicy` | – | Resolve a root directory honoring alias policy. |
34
34
  | `resolvePathViaExistingAncestorSync` | – | Walk to an existing ancestor for paths whose tail does not yet exist. |
35
+ | `probePathCaseInsensitiveSync`, `ProbePathCaseOptions` | [path-case.md](path-case.md) | Observe local ASCII-case behavior with explicit read-only mode and owned temporary-probe cleanup. |
35
36
 
36
37
  `ensureDirectoryWithinRoot({ rootDir, requestedPath, scopeLabel, defaultDirName?, mode? })`
37
38
  returns `{ ok: true, path }` or `{ ok: false, error: string, diagnostic?: FsSafeError }`.
@@ -66,9 +67,12 @@ Operational filesystem failures such as permissions or I/O errors are rethrown.
66
67
  |---|---|---|
67
68
  | `readFileDescriptorBounded`, `readFileDescriptorBoundedSync`, `readFileHandleBounded` | – | Incremental whole-file reads for already-open descriptors/handles. They consume at most `maxBytes + 1`, do not close the input, and throw `FsSafeError("too-large")` on overflow. |
68
69
  | `readFileWindowFully`, `readFileWindowFullySync`, `ReadFileWindowOptions` | [positional-read.md](positional-read.md) | Fill a caller-owned buffer at an explicit file position, completing short reads and returning the EOF count without moving or closing the descriptor. |
70
+ | `copyFileHandle`, `CopyFileHandleOptions` | [copy.md](copy.md#borrowed-filehandle-transfers) | Copy caller-owned regular-file handles from position zero with byte limits, settled cancellation, and a synchronous source observer; preserves cursors and leaves publication and cleanup to the caller. |
71
+ | `overwriteFileHandle`, `OverwriteFileHandleOptions` | [in-place-write.md](in-place-write.md) | Overwrite a borrowed read/write handle with prefix-only preparation and best-effort rollback; preserves its inode, cursor, and caller-owned lifetime. |
69
72
  | `openRootFile`, `openRootFileSync`, `canUseRootFileOpen`, `matchRootFileOpenFailure`, related types | – | Low-level root-bounded open; rejects every symlink component by default, with `symlinks: "follow-parents-within-root"` for contained parent aliases or `"follow-within-root"` for final links too. |
70
73
  | `appendRegularFile`, `appendRegularFileSync`, `readRegularFile`, `readRegularFileSync`, `statRegularFile`, `statRegularFileSync`, `resolveRegularFileAppendFlags`, `AppendRegularFileOptions`, `RegularFileStatResult` | [regular-file.md](regular-file.md) | Type-checked regular-file I/O. |
71
74
  | `sameFileIdentity`, `FileIdentityStat` | – | Compare two stats for same-inode equality. |
75
+ | `readDirectoryIdentity`, `assertDirectoryIdentitySync`, `DirectoryIdentity` | [directory-identity.md](directory-identity.md) | Observe exact bigint directory identity and synchronously check a caller-selected path, optionally retaining its canonical path. |
72
76
  | `pathExists`, `pathExistsSync` | – | Boolean existence check that does not throw on `ENOENT`. |
73
77
  | `assertNoSymlinkParents`, `assertNoSymlinkParentsSync`, `AssertNoSymlinkParentsOptions` | – | Reject paths whose ancestor chain contains symlinks. |
74
78
  | `assertNoHardlinkedFinalPath`, `assertNoPathAliasEscape`, `PATH_ALIAS_POLICIES`, `PathAliasPolicy` | – | Hardlink/alias defense building blocks. |
@@ -92,6 +96,8 @@ byte limit. Continuation buffers grow only after filling with actual bytes,
92
96
  up to the byte budget plus its probe; file-size hints cannot force that growth.
93
97
  Unknown-size inputs start with at most 64 KiB. This avoids per-chunk copies and
94
98
  a final concatenation when a file exceeds the initial allocation.
99
+ Regular files that report a size of zero, such as virtual files, continue through
100
+ positive short reads until actual EOF or byte-limit overflow.
95
101
 
96
102
  The bounded descriptor helpers start at the descriptor's current offset and
97
103
  leave ownership with the caller. They are intended for the second half of a
@@ -129,7 +135,7 @@ component is followed by another segment, both helpers throw
129
135
 
130
136
  | Export | Page | Notes |
131
137
  |---|---|---|
132
- | `safeDirName`, `safePathSegmentHashed`, `resolveSafeInstallDir`, `assertCanonicalPathWithinBase` | [install-path.md](install-path.md) | Build install-target directories from caller-supplied identifiers. |
138
+ | `safeDirName`, `safePathSegmentHashed`, `safePathSegmentHashedV2`, `resolveSafeInstallDir`, `assertCanonicalPathWithinBase` | [install-path.md](install-path.md) | Build install-target directories; use V2 for untrusted identifier mappings. |
133
139
  | `sanitizeUntrustedFileName` | [filename.md](filename.md) | Coerce an untrusted string into a safe filename. |
134
140
  | `resolveHomeRelativePath` | – | Expand a leading `~` before resolving `.` and `..`; tildes inside relative paths stay literal. |
135
141
 
package/docs/archive.md CHANGED
@@ -126,6 +126,12 @@ untrusted authority rejects explicitly instead of silently accepting a wrong
126
126
  mode. Other unsupported search-only routes also fail closed. Windows retains
127
127
  its existing bounded lack of POSIX mode enforcement.
128
128
 
129
+ Extraction and TAR inspection first copy the admitted source into a private
130
+ staging file. This copy reuses at most 512 KiB of scratch space, reduced for
131
+ small inputs and capped by the archive byte limit plus one overflow-probe byte.
132
+ Each read stays within the remaining budget plus that probe; deadline checks
133
+ surround reads, and short writes finish before the buffer is reused.
134
+
129
135
  Native extraction is deliberately split into two phases. Rust first reports an
130
136
  entry manifest without creating paths. TypeScript validates paths, applies
131
137
  `stripComponents`, filters, limits, and mode policy, then passes an explicit
@@ -133,7 +139,13 @@ accepted-entry plan back to Rust. Rust owns raw-stream admission, decompression,
133
139
  and fd-relative `mkdirBeneath`/exclusive-open writes. This keeps filter policy identical
134
140
  between native and JavaScript paths rather than reimplementing it in Rust.
135
141
 
136
- ZIP extraction and bounded reads admit every physical central-directory record and its referenced local header before either decoder can normalize or collapse names. Raw names and valid Unicode Path names must pass traversal checks before stripping, filtering, or selecting a requested member; duplicate or colliding names reject with `entry-path`, even in unrelated or skipped members. Materially conflicting local/central or Unicode interpretations, malformed critical metadata, and ambiguous framing reject with `ArchiveFormatError`. Harmless separator and dot-component equivalence is allowed only after validation. Ordinary legacy filename decoding remains backend-selected.
142
+ ZIP extraction and bounded reads admit every physical central-directory record and its referenced local header before either decoder can normalize or collapse names. Raw names and valid Unicode Path names must pass traversal checks before stripping, filtering, or selecting a requested member; duplicate or colliding names reject with `entry-path`, even in unrelated or skipped members. Materially conflicting local/central or Unicode interpretations, malformed critical metadata, and ambiguous framing reject with `ArchiveFormatError`. Harmless separator and dot-component equivalence is allowed only after validation. Ordinary legacy filename decoding remains backend-selected. Native ZIP extraction groups nearby metadata reads into at most two 4 KiB read-ahead buffers per admission pass; larger records retain separately bounded reads. Buffered record views remain stable across eviction, and cached work periodically yields for deadline checks.
143
+
144
+ Within one ZIP entry, identical local and central name bytes reuse the same
145
+ decoded validation. Unicode Path admission is shared only when both the raw names
146
+ and the complete Unicode fields match; different fields still verify their own
147
+ CRC and interpretation. Shared backing memory is checked independently. Decoded
148
+ name validation is not reused across entries or archives.
137
149
 
138
150
  `stripComponents` removes leading nonempty, non-`.` path components after
139
151
  normalizing separators. For example, `./pkg/hello.txt` with
@@ -222,6 +234,13 @@ record and are then cleared; local PAX on unsupported types and GNU sparse
222
234
 
223
235
  If `kind` is omitted, the helper calls `resolveArchiveKind(archivePath)` and throws if the extension is not recognized. Pass `kind` explicitly when the archive name doesn't carry the type (e.g. content-addressed names). Archive inputs must remain regular files from preview through descriptor admission; POSIX opens are no-follow and nonblocking, so a FIFO swap cannot stall before deadline checks resume. A positive finite `timeoutMs` is a wall-clock budget; zero, negative, `NaN`, and infinity disable the deadline. Non-mutating work rejects promptly when the budget expires. If a live destination mutation is already in flight, rejection waits only for that mutation and any rollback to finish; no later destination mutation can begin.
224
236
 
237
+ Extraction captures the destination's lossless filesystem identity before any
238
+ entry filter runs and retains that capability through final publication. If a
239
+ filter or concurrent actor renames or replaces the destination, extraction
240
+ rejects with `destination-symlink-traversal` before publishing into the
241
+ replacement. This check uses bigint device and inode identities so large native
242
+ identifiers cannot compare equal after JavaScript number rounding.
243
+
225
244
  The destination merge is nontransactional: each file is published atomically,
226
245
  but completed files and directories can remain when a later copy, post-copy
227
246
  check, mode application, or deadline fails. This also applies to
@@ -354,7 +373,9 @@ bypass validation. Decompression remains streaming; no complete decoded archive
354
373
  is retained in memory or written to a decoded spool.
355
374
 
356
375
  The WASM transport has a fixed 64 KiB input buffer, one pending member event,
357
- and a 256 MiB maximum linear memory per isolated parser instance. Metadata is
376
+ and a 256 MiB maximum linear memory per isolated parser instance. JavaScript
377
+ gzip decoding emits chunks of at most 64 KiB for both staged files and buffered
378
+ inputs, matching that input window. Metadata is
358
379
  bounded before allocation; allocation failure rejects. Stream backpressure
359
380
  bounds queued chunks, and completion/error destroys the instance's parser
360
381
  state. The manifest retains the existing charged budget below; linear memory
@@ -574,7 +595,9 @@ inputs retain the archive subpath's 256 MiB compressed-input ceiling.
574
595
  With a native binding it uses the same Rust decoders as extraction, including
575
596
  zstd and bzip2 TAR. Without native it retains the JS ZIP/TAR/gzip implementation.
576
597
  Archive member reads retain their private in-memory input without a disk
577
- snapshot. The native ZIP reader retains the private allocation and parsed directory across worker-thread
598
+ snapshot. JavaScript ZIP member reads reuse their completed physical admission
599
+ when loading the decoder, which still checks its decoded names and entry count.
600
+ The native ZIP reader retains the private allocation and parsed directory across worker-thread
578
601
  inspection and reading without an extra archive-byte copy.
579
602
  Decompression still allocates its bounded output; Node receives that native
580
603
  allocation without another copy where external buffers are supported.
@@ -582,8 +605,11 @@ Native TAR retains the fully admitted member offsets alongside the same input
582
605
  allocation. Plain TAR copies only the selected payload range after full archive
583
606
  validation. Gzip, zstd, and bzip2 replay bounded decompression and still validate
584
607
  all framing, trailers, and physical padding before returning. The JavaScript
585
- TAR/gzip fallback streams views of the private input into the shared WASM parser
586
- for admission and replay; WASM transport and selected output still require copies.
608
+ TAR/gzip fallback copies each input window into WASM once, consuming member
609
+ events at offsets within that window. After full admission, plain TAR copies the
610
+ selected range directly from its private snapshot; gzip still replays bounded
611
+ decompression through the parser. WASM transport and selected output still
612
+ require copies.
587
613
  Returned buffers own their bytes, so changing a result cannot modify an archive
588
614
  reader or retain an unrelated part of the input through its backing ArrayBuffer.
589
615
 
package/docs/atomic.md CHANGED
@@ -40,7 +40,7 @@ type ReplaceFileAtomicOptions = {
40
40
  content: string | Uint8Array;
41
41
  dirMode?: number; // parent-directory mode (POSIX; default 0o700)
42
42
  mode?: number; // new-file mode (default 0o600)
43
- preserveExistingMode?: boolean; // copy existing mode; default false
43
+ preserveExistingMode?: boolean; // inherit existing regular-file rwx bits; default false
44
44
  tempPrefix?: string; // default ".fs-safe-replace"
45
45
  renameMaxRetries?: number; // EBUSY retries; default 0
46
46
  renameRetryBaseDelayMs?: number; // exponential base; default 50
@@ -57,6 +57,17 @@ type ReplaceFileAtomicOptions = {
57
57
  };
58
58
  ```
59
59
 
60
+ `preserveExistingMode` snapshots only the ordinary rwx bits (`0o777`) from an
61
+ existing non-symlink regular destination. A final symlink fails with
62
+ `FsSafeError("symlink")`; a directory or other non-regular destination fails
63
+ with `FsSafeError("not-file")`. Set-user-ID, set-group-ID, and sticky bits are
64
+ never inherited. Mode inheritance does not copy ownership, ACLs, extended
65
+ attributes, or exact destination identity. Rename publication creates a new
66
+ inode; an in-place copy fallback can retain metadata already attached to its
67
+ pinned destination. The snapshot does not make replacement a compare-and-swap
68
+ operation, so the destination parent must still be protected from untrusted
69
+ concurrent namespace mutation.
70
+
60
71
  ### `beforeRename`
61
72
 
62
73
  Runs after the temp file is fully written and before the rename. Use it to take a backup snapshot, capture the about-to-be-replaced contents, or notify an observer. The helper retains the staged descriptor and exact bigint identity across the hook; replacing, deleting, hardlinking, or changing the temp entry to a non-regular file is rejected before publication:
@@ -135,6 +146,11 @@ than `maxRestoreBytes` fails with `too-large` before mutation. A missing
135
146
  destination has no original to restore and follows the exclusive-create copy
136
147
  fallback.
137
148
 
149
+ Restore snapshots use the pinned file's size as an allocation hint, with an
150
+ initial allocation capped at 16 MiB plus the overflow byte. Reads continue
151
+ through short reads and EOF, grow only as data arrives, and enforce the same
152
+ `maxRestoreBytes` budget even if the destination grows after its size was read.
153
+
138
154
  ### Sync variant
139
155
 
140
156
  `replaceFileAtomicSync` accepts the same base options, a synchronous
package/docs/config.md CHANGED
@@ -65,6 +65,7 @@ type FsSafeLockConfig = {
65
65
  Set process-wide defaults for sidecar lock options. This does **not** turn locking on globally; callers still need to pass `lock: true` or a lock options object for the specific JSON store/resource that needs cross-process coordination.
66
66
 
67
67
  `staleRecovery` defaults to `"fail-closed"`. The opt-in `"remove-if-unchanged"` mode requires caller approval and serializes the final snapshot check and unlink with an exclusive `.reclaim` guard. A reclaim guard left by a killed reclaimer fails closed and requires externally coordinated cleanup.
68
+ `staleMs` must be non-negative and not `NaN`; `Infinity` disables age-based staleness.
68
69
 
69
70
  For a daemon that should wait briefly for normal contention but never delete a
70
71
  stale owner without per-lock approval:
@@ -42,6 +42,32 @@ Vitest. Tests live in `test/` and follow `*.test.ts`. Run a single file with:
42
42
  pnpm test test/archive.test.ts
43
43
  ```
44
44
 
45
+ Guest filesystem tests and package smoke also need `python3` on Linux and
46
+ macOS. They execute the exported source on synthetic files; Windows checks
47
+ the import surface and leaves POSIX execution to the Linux/macOS lanes.
48
+ After building on Linux, `node scripts/check-pack.mjs --guest-cross-device`
49
+ also proves an installed-package directory move from temporary storage to
50
+ `/dev/shm`; the command requires those locations to be different filesystems.
51
+
52
+ With Bun 1.4.2 installed, build the host addon and run the native compatibility
53
+ lane in real Bun workers, then exercise the built package with JIT disabled:
54
+
55
+ ```bash
56
+ pnpm native:build
57
+ pnpm test:bun:native
58
+ bun --jitless scripts/bun-native-proof.mjs
59
+ ```
60
+
61
+ Keep the Node/pnpm build toolchain above. CI runs the native compatibility lane
62
+ on Linux, macOS, and Windows. The built-package proof checks `auto`/`require`,
63
+ native-off loading policy, and a separate installation without the addon.
64
+
65
+ `pnpm test:bun` runs the entire Node-oriented suite as a diagnostic. On released
66
+ Bun, its explicit native-off and missing-helper cases include unsupported
67
+ permission/path behavior described in [install](install.md#bun-runtime); this
68
+ command is not a passing compatibility gate. Node CI retains every fallback
69
+ assertion. Neither lane rewrites `off` to `auto` or marks defects as expected passes.
70
+
45
71
  Use `vi.mock` sparingly. Most tests should drive real disk operations in a `mkdtemp`-created scratch directory, asserting on observable behavior. The library has [test hooks](testing.md) for the rare cases where you need to inject a TOCTOU race deterministically.
46
72
 
47
73
  Vitest timeouts do not cancel filesystem promises. Shared fixtures with expensive
@@ -50,7 +76,10 @@ setup use `useSuiteFixture` from `test/helpers/suite-fixture.ts`: setup has a se
50
76
  removing directories. Run shared-state corpora sequentially with a deadline per
51
77
  payload. Keep child-process liveness limits separate from fixture preparation.
52
78
  The Windows CI slow-copy proof runs the real package-copy process-exit test with a
53
- six-second copy delay, retaining its four-second child deadline:
79
+ 16-second copy delay, a 10-second child deadline, a 15-second test deadline, and
80
+ 60-second setup and teardown hook budgets. Other hosts retain the ordinary
81
+ six-second delay, four-second child deadline, five-second test deadline, and
82
+ 30-second hook budgets:
54
83
 
55
84
  ```bash
56
85
  pnpm build
@@ -123,7 +152,9 @@ collection uses the actual seven collected native tarballs instead. Run it with
123
152
  `pnpm package:collect` after assembling all seven real bindings; missing targets
124
153
  fail collection. `pnpm package:collect --allow-host-only` exercises the same
125
154
  lifecycle boundary locally but proves only the host. Both collection commands
126
- require the pnpm lifecycle CLI path; direct `node` invocation is unsupported. Archive
155
+ require the pnpm lifecycle CLI path; JavaScript CLIs run through Node and standalone
156
+ `@pnpm/exe` binaries run directly. Shell/cmd shims and PATH fallback are not used;
157
+ direct `node` invocation without lifecycle metadata is unsupported. Archive
127
158
  codecs and their dependencies are packed from the installed dependency graph.
128
159
 
129
160
  PR CI builds and executes four host targets: Linux x64 glibc, Linux x64 musl