@openclaw/fs-safe 0.9.0 → 0.11.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 (326) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/LICENSE +1 -0
  3. package/README.md +52 -4
  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 -0
  7. package/dist/advanced.d.ts.map +1 -1
  8. package/dist/advanced.js +5 -0
  9. package/dist/archive-crc32.d.ts.map +1 -1
  10. package/dist/archive-crc32.js +6 -1
  11. package/dist/archive-deadline.d.ts.map +1 -1
  12. package/dist/archive-deadline.js +3 -4
  13. package/dist/archive-durability.d.ts +6 -6
  14. package/dist/archive-durability.d.ts.map +1 -1
  15. package/dist/archive-durability.js +1 -1
  16. package/dist/archive-entry.d.ts.map +1 -1
  17. package/dist/archive-entry.js +4 -5
  18. package/dist/archive-gzip-tail.d.ts +3 -0
  19. package/dist/archive-gzip-tail.d.ts.map +1 -1
  20. package/dist/archive-gzip-tail.js +23 -3
  21. package/dist/archive-input.d.ts.map +1 -1
  22. package/dist/archive-input.js +4 -2
  23. package/dist/archive-merge.d.ts +5 -1
  24. package/dist/archive-merge.d.ts.map +1 -1
  25. package/dist/archive-merge.js +16 -13
  26. package/dist/archive-native.d.ts.map +1 -1
  27. package/dist/archive-native.js +7 -6
  28. package/dist/archive-parser.wasm +0 -0
  29. package/dist/archive-read.d.ts.map +1 -1
  30. package/dist/archive-read.js +83 -74
  31. package/dist/archive-staging.d.ts +6 -3
  32. package/dist/archive-staging.d.ts.map +1 -1
  33. package/dist/archive-staging.js +42 -22
  34. package/dist/archive-tar-stream.d.ts +11 -4
  35. package/dist/archive-tar-stream.d.ts.map +1 -1
  36. package/dist/archive-tar-stream.js +23 -10
  37. package/dist/archive-tar-wasm.d.ts.map +1 -1
  38. package/dist/archive-tar-wasm.js +18 -14
  39. package/dist/archive-zip-admission.d.ts +1 -1
  40. package/dist/archive-zip-admission.d.ts.map +1 -1
  41. package/dist/archive-zip-admission.js +48 -12
  42. package/dist/archive-zip-loader.d.ts +6 -0
  43. package/dist/archive-zip-loader.d.ts.map +1 -0
  44. package/dist/archive-zip-loader.js +38 -0
  45. package/dist/archive-zip-names.d.ts.map +1 -1
  46. package/dist/archive-zip-names.js +13 -8
  47. package/dist/archive-zip-preflight.d.ts +2 -3
  48. package/dist/archive-zip-preflight.d.ts.map +1 -1
  49. package/dist/archive-zip-preflight.js +2 -34
  50. package/dist/archive.d.ts.map +1 -1
  51. package/dist/archive.js +12 -10
  52. package/dist/bounded-read.d.ts +12 -0
  53. package/dist/bounded-read.d.ts.map +1 -1
  54. package/dist/bounded-read.js +82 -45
  55. package/dist/clone-metadata.d.ts +19 -0
  56. package/dist/clone-metadata.d.ts.map +1 -0
  57. package/dist/clone-metadata.js +32 -0
  58. package/dist/copy-file-input.d.ts +22 -0
  59. package/dist/copy-file-input.d.ts.map +1 -0
  60. package/dist/copy-file-input.js +69 -0
  61. package/dist/copy-policy.d.ts +3 -0
  62. package/dist/copy-policy.d.ts.map +1 -0
  63. package/dist/copy-policy.js +8 -0
  64. package/dist/copy-publication.d.ts +10 -1
  65. package/dist/copy-publication.d.ts.map +1 -1
  66. package/dist/copy-publication.js +27 -0
  67. package/dist/copy-tree-portable.d.ts +9 -0
  68. package/dist/copy-tree-portable.d.ts.map +1 -0
  69. package/dist/copy-tree-portable.js +222 -0
  70. package/dist/copy.d.ts +16 -0
  71. package/dist/copy.d.ts.map +1 -0
  72. package/dist/copy.js +125 -0
  73. package/dist/directory-durability.d.ts.map +1 -1
  74. package/dist/directory-durability.js +5 -4
  75. package/dist/directory-guard.d.ts +11 -1
  76. package/dist/directory-guard.d.ts.map +1 -1
  77. package/dist/directory-guard.js +53 -11
  78. package/dist/durability.d.ts +1 -1
  79. package/dist/durability.d.ts.map +1 -1
  80. package/dist/durability.js +1 -1
  81. package/dist/error-detail.d.ts.map +1 -1
  82. package/dist/error-detail.js +4 -1
  83. package/dist/file-handle-transfer.d.ts +14 -0
  84. package/dist/file-handle-transfer.d.ts.map +1 -0
  85. package/dist/file-handle-transfer.js +64 -0
  86. package/dist/file-hash.d.ts +9 -2
  87. package/dist/file-hash.d.ts.map +1 -1
  88. package/dist/file-hash.js +135 -39
  89. package/dist/file-lock-sync.d.ts.map +1 -1
  90. package/dist/file-lock-sync.js +8 -4
  91. package/dist/file-store-boundary.d.ts.map +1 -1
  92. package/dist/file-store-boundary.js +7 -5
  93. package/dist/file-store-path.d.ts +3 -0
  94. package/dist/file-store-path.d.ts.map +1 -0
  95. package/dist/file-store-path.js +27 -0
  96. package/dist/file-store-prune.d.ts.map +1 -1
  97. package/dist/file-store-prune.js +6 -4
  98. package/dist/file-store-sync-write.d.ts.map +1 -1
  99. package/dist/file-store-sync-write.js +56 -44
  100. package/dist/file-store.d.ts.map +1 -1
  101. package/dist/file-store.js +2 -18
  102. package/dist/filename.d.ts.map +1 -1
  103. package/dist/filename.js +4 -13
  104. package/dist/guarded-mkdir.d.ts +1 -0
  105. package/dist/guarded-mkdir.d.ts.map +1 -1
  106. package/dist/guarded-mkdir.js +4 -2
  107. package/dist/guarded-mutation.d.ts +2 -0
  108. package/dist/guarded-mutation.d.ts.map +1 -1
  109. package/dist/guarded-mutation.js +8 -4
  110. package/dist/guest-dispatch-python.d.ts +2 -0
  111. package/dist/guest-dispatch-python.d.ts.map +1 -0
  112. package/dist/guest-dispatch-python.js +117 -0
  113. package/dist/guest-native-python.d.ts +4 -0
  114. package/dist/guest-native-python.d.ts.map +1 -0
  115. package/dist/guest-native-python.js +135 -0
  116. package/dist/guest.d.ts +9 -0
  117. package/dist/guest.d.ts.map +1 -0
  118. package/dist/guest.js +413 -0
  119. package/dist/home-dir.d.ts.map +1 -1
  120. package/dist/home-dir.js +9 -7
  121. package/dist/index.d.ts +1 -1
  122. package/dist/index.d.ts.map +1 -1
  123. package/dist/install-path.d.ts.map +1 -1
  124. package/dist/install-path.js +5 -8
  125. package/dist/json-durable-queue-directory.js +3 -3
  126. package/dist/json-durable-queue.d.ts.map +1 -1
  127. package/dist/json-durable-queue.js +19 -18
  128. package/dist/json.d.ts.map +1 -1
  129. package/dist/json.js +2 -1
  130. package/dist/local-roots.d.ts.map +1 -1
  131. package/dist/local-roots.js +24 -25
  132. package/dist/move-path-stage.d.ts +8 -0
  133. package/dist/move-path-stage.d.ts.map +1 -0
  134. package/dist/move-path-stage.js +56 -0
  135. package/dist/move-path.d.ts.map +1 -1
  136. package/dist/move-path.js +30 -34
  137. package/dist/mutation-authority.d.ts +9 -0
  138. package/dist/mutation-authority.d.ts.map +1 -0
  139. package/dist/mutation-authority.js +36 -0
  140. package/dist/native-binding.d.ts +29 -1
  141. package/dist/native-binding.d.ts.map +1 -1
  142. package/dist/native-operations.d.ts +1 -1
  143. package/dist/native-operations.d.ts.map +1 -1
  144. package/dist/native-operations.js +11 -5
  145. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  146. package/dist/native-pinned-write-windows.js +19 -7
  147. package/dist/native-pinned-write.d.ts.map +1 -1
  148. package/dist/native-pinned-write.js +3 -1
  149. package/dist/native-staged-file.d.ts +3 -3
  150. package/dist/native-staged-file.d.ts.map +1 -1
  151. package/dist/native-staged-file.js +35 -8
  152. package/dist/opened-realpath.d.ts.map +1 -1
  153. package/dist/opened-realpath.js +5 -4
  154. package/dist/output.d.ts +2 -0
  155. package/dist/output.d.ts.map +1 -1
  156. package/dist/output.js +2 -0
  157. package/dist/overwrite-file-handle.d.ts +8 -0
  158. package/dist/overwrite-file-handle.d.ts.map +1 -0
  159. package/dist/overwrite-file-handle.js +42 -0
  160. package/dist/path-case.d.ts +7 -0
  161. package/dist/path-case.d.ts.map +1 -0
  162. package/dist/path-case.js +136 -0
  163. package/dist/path.d.ts.map +1 -1
  164. package/dist/path.js +5 -2
  165. package/dist/permissions-windows.d.ts +1 -1
  166. package/dist/permissions-windows.d.ts.map +1 -1
  167. package/dist/permissions-windows.js +85 -53
  168. package/dist/pinned-open.d.ts.map +1 -1
  169. package/dist/pinned-open.js +3 -1
  170. package/dist/pinned-operation.js +1 -1
  171. package/dist/pinned-write.d.ts +7 -3
  172. package/dist/pinned-write.d.ts.map +1 -1
  173. package/dist/pinned-write.js +51 -30
  174. package/dist/positional-read.d.ts +9 -0
  175. package/dist/positional-read.d.ts.map +1 -0
  176. package/dist/positional-read.js +36 -0
  177. package/dist/private-temp-workspace.d.ts.map +1 -1
  178. package/dist/private-temp-workspace.js +6 -4
  179. package/dist/publish-copy-stage.d.ts +13 -0
  180. package/dist/publish-copy-stage.d.ts.map +1 -0
  181. package/dist/publish-copy-stage.js +47 -0
  182. package/dist/publish-file.d.ts.map +1 -1
  183. package/dist/publish-file.js +6 -5
  184. package/dist/realpath.d.ts +4 -0
  185. package/dist/realpath.d.ts.map +1 -0
  186. package/dist/realpath.js +43 -0
  187. package/dist/recursive-mkdir-path.d.ts +3 -0
  188. package/dist/recursive-mkdir-path.d.ts.map +1 -0
  189. package/dist/recursive-mkdir-path.js +8 -0
  190. package/dist/replace-directory.d.ts.map +1 -1
  191. package/dist/replace-directory.js +2 -1
  192. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  193. package/dist/replace-file-copy-fallback.js +23 -31
  194. package/dist/replace-file-copy-source.d.ts.map +1 -1
  195. package/dist/replace-file-copy-source.js +7 -12
  196. package/dist/replace-file-mode.d.ts +3 -0
  197. package/dist/replace-file-mode.d.ts.map +1 -0
  198. package/dist/replace-file-mode.js +10 -0
  199. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  200. package/dist/replace-file-temp-owner.js +6 -2
  201. package/dist/replace-file.d.ts +1 -0
  202. package/dist/replace-file.d.ts.map +1 -1
  203. package/dist/replace-file.js +14 -8
  204. package/dist/root-context.d.ts +3 -0
  205. package/dist/root-context.d.ts.map +1 -1
  206. package/dist/root-context.js +53 -28
  207. package/dist/root-create-input.d.ts +10 -0
  208. package/dist/root-create-input.d.ts.map +1 -0
  209. package/dist/root-create-input.js +80 -0
  210. package/dist/root-directory-list.d.ts +26 -0
  211. package/dist/root-directory-list.d.ts.map +1 -0
  212. package/dist/root-directory-list.js +219 -0
  213. package/dist/root-entries.d.ts +11 -0
  214. package/dist/root-entries.d.ts.map +1 -0
  215. package/dist/root-entries.js +61 -0
  216. package/dist/root-errors.d.ts +6 -5
  217. package/dist/root-errors.d.ts.map +1 -1
  218. package/dist/root-errors.js +16 -12
  219. package/dist/root-file.d.ts +2 -0
  220. package/dist/root-file.d.ts.map +1 -1
  221. package/dist/root-file.js +12 -4
  222. package/dist/root-impl.d.ts +17 -51
  223. package/dist/root-impl.d.ts.map +1 -1
  224. package/dist/root-impl.js +358 -234
  225. package/dist/root-options.d.ts +77 -0
  226. package/dist/root-options.d.ts.map +1 -0
  227. package/dist/root-options.js +18 -0
  228. package/dist/root-path-existing.d.ts +5 -0
  229. package/dist/root-path-existing.d.ts.map +1 -1
  230. package/dist/root-path-existing.js +59 -3
  231. package/dist/root-path-symlink.d.ts.map +1 -1
  232. package/dist/root-path-symlink.js +3 -2
  233. package/dist/root-path.d.ts +1 -0
  234. package/dist/root-path.d.ts.map +1 -1
  235. package/dist/root-path.js +74 -162
  236. package/dist/root-paths.d.ts.map +1 -1
  237. package/dist/root-paths.js +13 -9
  238. package/dist/root-remove.d.ts +5 -0
  239. package/dist/root-remove.d.ts.map +1 -0
  240. package/dist/root-remove.js +286 -0
  241. package/dist/root-symlink-policy.d.ts +14 -0
  242. package/dist/root-symlink-policy.d.ts.map +1 -0
  243. package/dist/root-symlink-policy.js +34 -0
  244. package/dist/root-walk.d.ts +5 -4
  245. package/dist/root-walk.d.ts.map +1 -1
  246. package/dist/root-walk.js +99 -50
  247. package/dist/root-write-mode.d.ts.map +1 -1
  248. package/dist/root-write-mode.js +3 -5
  249. package/dist/root.d.ts +4 -1
  250. package/dist/root.d.ts.map +1 -1
  251. package/dist/secret-file.d.ts.map +1 -1
  252. package/dist/secret-file.js +2 -1
  253. package/dist/secret-read-async.d.ts.map +1 -1
  254. package/dist/secret-read-async.js +2 -1
  255. package/dist/secure-file.d.ts.map +1 -1
  256. package/dist/secure-file.js +25 -8
  257. package/dist/secure-temp-dir.d.ts.map +1 -1
  258. package/dist/secure-temp-dir.js +2 -1
  259. package/dist/sibling-staged-file.d.ts +1 -0
  260. package/dist/sibling-staged-file.d.ts.map +1 -1
  261. package/dist/sibling-staged-file.js +42 -8
  262. package/dist/sibling-temp.d.ts +2 -0
  263. package/dist/sibling-temp.d.ts.map +1 -1
  264. package/dist/sibling-temp.js +6 -4
  265. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  266. package/dist/sidecar-lock-acquire.js +17 -5
  267. package/dist/sidecar-lock-policy.d.ts +2 -0
  268. package/dist/sidecar-lock-policy.d.ts.map +1 -1
  269. package/dist/sidecar-lock-policy.js +20 -1
  270. package/dist/staged-directory.d.ts.map +1 -1
  271. package/dist/staged-directory.js +4 -3
  272. package/dist/temp-cleanup.d.ts.map +1 -1
  273. package/dist/temp-cleanup.js +3 -2
  274. package/dist/temp-target.d.ts +14 -12
  275. package/dist/temp-target.d.ts.map +1 -1
  276. package/dist/temp-target.js +12 -6
  277. package/dist/timing.d.ts +1 -0
  278. package/dist/timing.d.ts.map +1 -1
  279. package/dist/timing.js +25 -6
  280. package/dist/trash.d.ts.map +1 -1
  281. package/dist/trash.js +9 -7
  282. package/dist/unicode-path.d.ts +3 -0
  283. package/dist/unicode-path.d.ts.map +1 -0
  284. package/dist/unicode-path.js +13 -0
  285. package/dist/walk.d.ts.map +1 -1
  286. package/dist/walk.js +9 -28
  287. package/dist/windows-owner.d.ts +9 -12
  288. package/dist/windows-owner.d.ts.map +1 -1
  289. package/dist/windows-owner.js +24 -58
  290. package/dist/write-file-handle.d.ts +7 -0
  291. package/dist/write-file-handle.d.ts.map +1 -0
  292. package/dist/write-file-handle.js +26 -0
  293. package/docs/advanced.md +22 -4
  294. package/docs/archive.md +44 -9
  295. package/docs/atomic.md +24 -1
  296. package/docs/config.md +1 -0
  297. package/docs/contributing.md +36 -1
  298. package/docs/copy.md +155 -0
  299. package/docs/directory-identity.md +85 -0
  300. package/docs/durability.md +53 -3
  301. package/docs/entries.md +109 -0
  302. package/docs/errors.md +4 -4
  303. package/docs/file-store.md +15 -0
  304. package/docs/guest.md +141 -0
  305. package/docs/in-place-write.md +81 -0
  306. package/docs/index.md +2 -0
  307. package/docs/install.md +31 -0
  308. package/docs/local-roots.md +8 -1
  309. package/docs/native-helper.md +10 -3
  310. package/docs/native.md +18 -1
  311. package/docs/output.md +32 -6
  312. package/docs/path-case.md +64 -0
  313. package/docs/path-scope.md +1 -1
  314. package/docs/permissions.md +29 -10
  315. package/docs/positional-read.md +63 -0
  316. package/docs/public-api.md +31 -2
  317. package/docs/root.md +196 -6
  318. package/docs/secure-file.md +2 -0
  319. package/docs/security-model.md +14 -0
  320. package/docs/sidecar-lock.md +12 -3
  321. package/docs/temp.md +35 -6
  322. package/docs/timing.md +2 -0
  323. package/docs/types.md +19 -12
  324. package/docs/walk.md +54 -5
  325. package/docs/writing.md +173 -5
  326. package/package.json +19 -8
@@ -2,50 +2,69 @@ import fs from "node:fs";
2
2
  import { normalizeMaxBytes } from "./byte-budget.js";
3
3
  import { FsSafeError } from "./errors.js";
4
4
  const READ_CHUNK_BYTES = 64 * 1024;
5
- function createScratchBuffer(maxBytes) {
6
- const initialReadBytes = Number.isFinite(maxBytes)
7
- ? Math.min(READ_CHUNK_BYTES, maxBytes + 1)
8
- : READ_CHUNK_BYTES;
9
- return Buffer.allocUnsafe(Math.max(1, initialReadBytes));
5
+ // Preserve one-read performance through the default Root byte budget.
6
+ const MAX_INITIAL_READ_BYTES = 16 * 1024 * 1024;
7
+ function createInitialBuffer(maxBytes, size, allocate = Buffer.allocUnsafe) {
8
+ // A size hint can describe a huge sparse file or an already exhausted fd.
9
+ return allocate(Math.min(maxBytes, size, MAX_INITIAL_READ_BYTES) + 1);
10
10
  }
11
- function nextReadLength(total, maxBytes, capacity) {
12
- return Number.isFinite(maxBytes)
13
- ? Math.min(capacity, maxBytes - total + 1)
14
- : capacity;
11
+ function createScratchBuffer(maxBytes, allocate = Buffer.allocUnsafe) {
12
+ return allocate(Math.min(READ_CHUNK_BYTES, maxBytes + 1));
15
13
  }
16
- function appendChunk(params) {
17
- const total = params.total + params.bytesRead;
18
- if (total > params.maxBytes) {
19
- throw new FsSafeError("too-large", `file exceeds limit of ${params.maxBytes} bytes (got at least ${total})`);
14
+ function growReadBuffer(buffer, maxBytes, allocate = Buffer.allocUnsafe) {
15
+ // Grow only after consuming the whole buffer, never from an unchecked size hint.
16
+ const capacity = Math.min(maxBytes + 1, Math.max(READ_CHUNK_BYTES, buffer.length * 2));
17
+ const grown = allocate(capacity);
18
+ buffer.copy(grown);
19
+ return grown;
20
+ }
21
+ function finishReadBuffer(buffer, total, allocate) {
22
+ if (total === 0)
23
+ return Buffer.alloc(0);
24
+ const result = buffer.subarray(0, total);
25
+ if (total >= buffer.length / 2)
26
+ return result;
27
+ if (!allocate)
28
+ return Buffer.from(result);
29
+ const compact = allocate(total);
30
+ result.copy(compact);
31
+ return compact;
32
+ }
33
+ function addReadBytes(total, bytesRead, maxBytes, createLimitError) {
34
+ const next = total + bytesRead;
35
+ if (next > maxBytes) {
36
+ if (createLimitError)
37
+ throw createLimitError();
38
+ throw new FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${next})`);
20
39
  }
21
- params.chunks.push(Buffer.from(params.scratch.subarray(0, params.bytesRead)));
22
- return total;
40
+ return next;
23
41
  }
24
- async function readBoundedAsync(maxBytes, readChunk, observeRegularFileSize) {
42
+ export async function readBoundedAsync(maxBytes, readChunk, options = {}) {
25
43
  normalizeMaxBytes(maxBytes);
26
- const chunks = [];
27
44
  let total = 0;
28
- const size = observeRegularFileSize?.();
45
+ const { observeRegularFileSize, createLimitError } = options;
46
+ const allocate = options.unpooled ? Buffer.allocUnsafeSlow : undefined;
47
+ const size = options.initialSize ?? observeRegularFileSize?.();
48
+ let buffer = size === undefined ? createScratchBuffer(maxBytes, allocate) : createInitialBuffer(maxBytes, size, allocate);
29
49
  if (size !== undefined) {
30
- const first = Buffer.allocUnsafe(Math.min(maxBytes, size) + 1);
31
- const bytesRead = await readChunk(first, first.length);
50
+ const bytesRead = await readChunk(buffer, buffer.length);
32
51
  if (bytesRead === 0)
33
- return first.subarray(0, 0);
34
- // Short reads can occur before EOF. Only accept one when a fresh fd size
35
- // observation proves we consumed at least the entire current file size.
36
- const currentSize = bytesRead < first.length ? observeRegularFileSize?.() : undefined;
52
+ return finishReadBuffer(buffer, 0, allocate);
53
+ // Short reads can occur before EOF. Only use the size shortcut on this
54
+ // initial read; virtual files can report zero or stale sizes thereafter.
55
+ const currentSize = bytesRead < buffer.length ? observeRegularFileSize?.() : undefined;
56
+ total = addReadBytes(total, bytesRead, maxBytes, createLimitError);
37
57
  if (currentSize !== undefined && bytesRead >= currentSize)
38
- return first.subarray(0, bytesRead);
39
- total = appendChunk({ chunks, scratch: first, bytesRead, total, maxBytes });
58
+ return finishReadBuffer(buffer, total, allocate);
40
59
  }
41
- const scratch = createScratchBuffer(maxBytes);
42
60
  while (true) {
43
- const length = nextReadLength(total, maxBytes, scratch.length);
44
- const bytesRead = await readChunk(scratch, length);
45
- if (bytesRead === 0) {
46
- return Buffer.concat(chunks, total);
47
- }
48
- total = appendChunk({ chunks, scratch, bytesRead, total, maxBytes });
61
+ if (total === buffer.length)
62
+ buffer = growReadBuffer(buffer, maxBytes, allocate);
63
+ const remaining = buffer.subarray(total);
64
+ const bytesRead = await readChunk(remaining, remaining.length);
65
+ if (bytesRead === 0)
66
+ return finishReadBuffer(buffer, total, allocate);
67
+ total = addReadBytes(total, bytesRead, maxBytes, createLimitError);
49
68
  }
50
69
  }
51
70
  /**
@@ -58,7 +77,7 @@ export async function readFileHandleBounded(handle, maxBytes) {
58
77
  const fd = "fd" in handle && typeof handle.fd === "number" ? handle.fd : undefined;
59
78
  return await readBoundedAsync(maxBytes, async (scratch, length) => {
60
79
  return (await handle.read(scratch, 0, length, null)).bytesRead;
61
- }, fd === undefined ? undefined : () => regularFileSize(fd));
80
+ }, { observeRegularFileSize: fd === undefined ? undefined : () => regularFileSize(fd) });
62
81
  }
63
82
  function regularFileSize(fd) {
64
83
  try {
@@ -87,20 +106,38 @@ export async function readFileDescriptorBounded(fd, maxBytes) {
87
106
  normalizeMaxBytes(maxBytes);
88
107
  return await readBoundedAsync(maxBytes, async (scratch, length) => {
89
108
  return await readDescriptorChunk(fd, scratch, length);
90
- }, () => regularFileSize(fd));
109
+ }, { observeRegularFileSize: () => regularFileSize(fd) });
91
110
  }
92
- /** Sync bounded read from a numeric descriptor. The caller owns the descriptor. */
93
- export function readFileDescriptorBoundedSync(fd, maxBytes) {
111
+ export function readBoundedSync(maxBytes, readChunk, options = {}) {
94
112
  normalizeMaxBytes(maxBytes);
95
- const chunks = [];
96
- const scratch = createScratchBuffer(maxBytes);
97
113
  let total = 0;
114
+ const { observeRegularFileSize, createLimitError } = options;
115
+ const size = options.initialSize ?? observeRegularFileSize?.();
116
+ let buffer = size === undefined ? createScratchBuffer(maxBytes) : createInitialBuffer(maxBytes, size);
117
+ if (size !== undefined) {
118
+ const bytesRead = readChunk(buffer, buffer.length);
119
+ if (bytesRead === 0)
120
+ return finishReadBuffer(buffer, 0);
121
+ const currentSize = bytesRead < buffer.length ? observeRegularFileSize?.() : undefined;
122
+ total = addReadBytes(total, bytesRead, maxBytes, createLimitError);
123
+ if (currentSize !== undefined && bytesRead >= currentSize)
124
+ return finishReadBuffer(buffer, total);
125
+ }
98
126
  while (true) {
99
- const length = nextReadLength(total, maxBytes, scratch.length);
100
- const bytesRead = fs.readSync(fd, scratch, 0, length, null);
101
- if (bytesRead === 0) {
102
- return Buffer.concat(chunks, total);
103
- }
104
- total = appendChunk({ chunks, scratch, bytesRead, total, maxBytes });
127
+ if (total === buffer.length)
128
+ buffer = growReadBuffer(buffer, maxBytes);
129
+ const remaining = buffer.subarray(total);
130
+ const bytesRead = readChunk(remaining, remaining.length);
131
+ if (bytesRead === 0)
132
+ return finishReadBuffer(buffer, total);
133
+ total = addReadBytes(total, bytesRead, maxBytes, createLimitError);
105
134
  }
106
135
  }
136
+ /** Sync bounded read from a numeric descriptor. The caller owns the descriptor. */
137
+ export function readFileDescriptorBoundedSync(fd, maxBytes) {
138
+ return readBoundedSync(maxBytes, (buffer, length) => fs.readSync(fd, buffer, 0, length, null), {
139
+ // Small budgets already bound the first allocation without a size lookup.
140
+ initialSize: maxBytes <= READ_CHUNK_BYTES ? maxBytes : undefined,
141
+ observeRegularFileSize: () => regularFileSize(fd),
142
+ });
143
+ }
@@ -0,0 +1,19 @@
1
+ export type CloneFileMetadata = {
2
+ dev: number;
3
+ type: number;
4
+ mtimeSec: number;
5
+ mtimeNs: number;
6
+ ctimeSec: number;
7
+ ctimeNs: number;
8
+ uid: number;
9
+ gid: number;
10
+ mode: number;
11
+ ino: bigint;
12
+ size: bigint;
13
+ cloneId: bigint;
14
+ };
15
+ /** Batched APFS metadata snapshots; absent on unsupported files or filesystems.
16
+ * These are point-in-time facts, not authorization or a guarantee against later edits.
17
+ */
18
+ export declare function readCloneFileMetadata(files: readonly string[]): Promise<(CloneFileMetadata | undefined)[]>;
19
+ //# sourceMappingURL=clone-metadata.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clone-metadata.d.ts","sourceRoot":"","sources":["../src/clone-metadata.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,iBAAiB,GAAG;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF;;GAEG;AACH,wBAAsB,qBAAqB,CACzC,KAAK,EAAE,SAAS,MAAM,EAAE,GACvB,OAAO,CAAC,CAAC,iBAAiB,GAAG,SAAS,CAAC,EAAE,CAAC,CA4B5C"}
@@ -0,0 +1,32 @@
1
+ import { assertAbsolutePathInput } from "./absolute-path.js";
2
+ import { requireNativeBinding } from "./native.js";
3
+ /** Batched APFS metadata snapshots; absent on unsupported files or filesystems.
4
+ * These are point-in-time facts, not authorization or a guarantee against later edits.
5
+ */
6
+ export async function readCloneFileMetadata(files) {
7
+ const paths = files.map(assertAbsolutePathInput);
8
+ const results = await requireNativeBinding().readCloneFileMetadata(paths);
9
+ return results.map((result) => {
10
+ if (!result ||
11
+ result.length !== 100 ||
12
+ result.readUInt32LE(0) !== 100 ||
13
+ result.readUInt32LE(4) !== 0x82038c0a ||
14
+ result.readUInt32LE(16) !== 0x200 ||
15
+ result.readUInt32LE(20) !== 0x100)
16
+ return undefined;
17
+ return {
18
+ dev: result.readUInt32LE(24),
19
+ type: result.readUInt32LE(28),
20
+ mtimeSec: Number(result.readBigInt64LE(32)),
21
+ mtimeNs: Number(result.readBigInt64LE(40)),
22
+ ctimeSec: Number(result.readBigInt64LE(48)),
23
+ ctimeNs: Number(result.readBigInt64LE(56)),
24
+ uid: result.readUInt32LE(64),
25
+ gid: result.readUInt32LE(68),
26
+ mode: result.readUInt32LE(72),
27
+ ino: result.readBigUInt64LE(76),
28
+ size: result.readBigUInt64LE(84),
29
+ cloneId: result.readBigUInt64LE(92),
30
+ };
31
+ });
32
+ }
@@ -0,0 +1,22 @@
1
+ import { type BigIntStats } from "node:fs";
2
+ import type { FileHandle } from "node:fs/promises";
3
+ import { type CopyCloneMode } from "./copy-policy.js";
4
+ import { type NativeBinding } from "./native.js";
5
+ import type { NativeFileCopyResult } from "./native-binding.js";
6
+ export type CopyFileInput = {
7
+ kind: "file";
8
+ handle: FileHandle;
9
+ size: number;
10
+ clone: CopyCloneMode;
11
+ signal?: AbortSignal;
12
+ verifySource(): Promise<void>;
13
+ };
14
+ export declare function resolveFileCopyCloneMode(mode?: CopyCloneMode): CopyCloneMode;
15
+ export declare function assertCopySourceCurrent(source: {
16
+ handle: FileHandle;
17
+ realPath: string;
18
+ }, identity: BigIntStats): Promise<void>;
19
+ export declare function writeCopyFileToFd(fd: number, input: CopyFileInput, maxBytes?: number, assertBeforeMutation?: () => void): Promise<void>;
20
+ export declare function createNativeCopyFile(native: NativeBinding, input: CopyFileInput, parentFd: number, basename: string, maxBytes: number | undefined, sync: boolean): Promise<NativeFileCopyResult | undefined>;
21
+ export declare function assertNativeCopyCompleted(input: CopyFileInput, copied?: NativeFileCopyResult): void;
22
+ //# sourceMappingURL=copy-file-input.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"copy-file-input.d.ts","sourceRoot":"","sources":["../src/copy-file-input.ts"],"names":[],"mappings":"AAAA,OAAW,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAwB,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAE5E,OAAO,EAAoB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAIhE,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,UAAU,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,aAAa,CAAC;IACrB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/B,CAAC;AAEF,wBAAgB,wBAAwB,CAAC,IAAI,CAAC,EAAE,aAAa,GAAG,aAAa,CAM5E;AAED,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,EAChD,QAAQ,EAAE,WAAW,GACpB,OAAO,CAAC,IAAI,CAAC,CASf;AAED,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,aAAa,EACpB,QAAQ,CAAC,EAAE,MAAM,EACjB,oBAAoB,CAAC,EAAE,MAAM,IAAI,GAChC,OAAO,CAAC,IAAI,CAAC,CAIf;AAED,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,aAAa,EACpB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,IAAI,EAAE,OAAO,GACZ,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC,CA8B3C;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,aAAa,EAAE,MAAM,CAAC,EAAE,oBAAoB,GAAG,IAAI,CAWnG"}
@@ -0,0 +1,69 @@
1
+ import fs, {} from "node:fs";
2
+ import { resolveCopyCloneMode } from "./copy-policy.js";
3
+ import { FsSafeError } from "./errors.js";
4
+ import { getNativeBinding } from "./native.js";
5
+ import { inspectFileIdentity } from "./strict-file-identity.js";
6
+ import { transferFileHandle } from "./file-handle-transfer.js";
7
+ export function resolveFileCopyCloneMode(mode) {
8
+ const clone = resolveCopyCloneMode(mode, "never");
9
+ if (clone === "always" && !getNativeBinding()?.copyFileExclusive) {
10
+ throw new FsSafeError("helper-unavailable", "native file cloning is unavailable");
11
+ }
12
+ return clone;
13
+ }
14
+ export async function assertCopySourceCurrent(source, identity) {
15
+ await inspectFileIdentity(() => fs.fstatSync(source.handle.fd, { bigint: true }), identity);
16
+ await inspectFileIdentity(() => {
17
+ const current = fs.lstatSync(source.realPath, { bigint: true });
18
+ if (current.isSymbolicLink() || !current.isFile()) {
19
+ throw new FsSafeError("path-mismatch", "copy source path changed");
20
+ }
21
+ return current;
22
+ }, identity);
23
+ }
24
+ export async function writeCopyFileToFd(fd, input, maxBytes, assertBeforeMutation) {
25
+ await transferFileHandle(input.handle, fd, {
26
+ maxBytes, sizeHint: input.size, signal: input.signal, assertBeforeMutation,
27
+ });
28
+ }
29
+ export async function createNativeCopyFile(native, input, parentFd, basename, maxBytes, sync) {
30
+ input.signal?.throwIfAborted();
31
+ if (!native.copyFileExclusive) {
32
+ if (input.clone === "always") {
33
+ throw new FsSafeError("helper-unavailable", "native file cloning is unavailable");
34
+ }
35
+ return undefined;
36
+ }
37
+ const nativeSignal = input.signal ? AbortSignal.any([input.signal]) : undefined;
38
+ try {
39
+ // The caller adopts this descriptor before observing a later cancellation.
40
+ return await native.copyFileExclusive(input.handle.fd, parentFd, basename, input.clone, maxBytes !== undefined && Number.isFinite(maxBytes) ? maxBytes : undefined, nativeSignal, sync);
41
+ }
42
+ catch (error) {
43
+ const code = error?.code;
44
+ if (code === "Cancelled" || code === "ABORT_ERR")
45
+ input.signal?.throwIfAborted();
46
+ if (code === "too-large") {
47
+ throw new FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes`, { cause: error });
48
+ }
49
+ if (code === "ENOTSUP" && input.clone !== "always")
50
+ return undefined;
51
+ if (code === "ENOTSUP") {
52
+ throw new FsSafeError("unsupported-platform", "native file cloning is unsupported", { cause: error });
53
+ }
54
+ throw new FsSafeError("helper-failed", "native file copy failed", { cause: error });
55
+ }
56
+ finally {
57
+ if (nativeSignal)
58
+ nativeSignal.onabort = null;
59
+ }
60
+ }
61
+ export function assertNativeCopyCompleted(input, copied) {
62
+ if (copied?.errorCode) {
63
+ if (copied.errorCode === "Cancelled" || copied.errorCode === "ABORT_ERR")
64
+ input.signal?.throwIfAborted();
65
+ const unsupported = ["ENOTSUP", "EOPNOTSUPP", "ENOSYS", "EXDEV", "EINVAL"].includes(copied.errorCode);
66
+ throw new FsSafeError(copied.errorCode === "too-large" ? "too-large" : unsupported ? "unsupported-platform" : "helper-failed", copied.errorMessage ?? "native file copy failed", { cause: Object.assign(new Error(copied.errorMessage ?? "native file copy failed"), { code: copied.errorCode }) });
67
+ }
68
+ input.signal?.throwIfAborted();
69
+ }
@@ -0,0 +1,3 @@
1
+ export type CopyCloneMode = "auto" | "always" | "never";
2
+ export declare function resolveCopyCloneMode(mode: CopyCloneMode | undefined, defaultMode: CopyCloneMode): CopyCloneMode;
3
+ //# sourceMappingURL=copy-policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"copy-policy.d.ts","sourceRoot":"","sources":["../src/copy-policy.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;AAExD,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,aAAa,GAAG,SAAS,EAC/B,WAAW,EAAE,aAAa,GACzB,aAAa,CAMf"}
@@ -0,0 +1,8 @@
1
+ import { FsSafeError } from "./errors.js";
2
+ export function resolveCopyCloneMode(mode, defaultMode) {
3
+ const policy = mode === undefined ? defaultMode : mode;
4
+ if (policy !== "auto" && policy !== "always" && policy !== "never") {
5
+ throw new FsSafeError("invalid-path", "copy clone policy must be auto, always, or never");
6
+ }
7
+ return policy;
8
+ }
@@ -1,4 +1,13 @@
1
- import type { PinnedWriteParams } from "./pinned-write.js";
1
+ import type { PinnedWriteParams, PublishedWriteIdentity } from "./pinned-write.js";
2
+ export type RootCopyPublicationReceipt = Readonly<{
3
+ path: string;
4
+ dev: bigint;
5
+ ino: bigint;
6
+ }>;
7
+ export declare function createCopyPublicationObserver(path: string, notify?: (receipt: RootCopyPublicationReceipt) => void): {
8
+ onPublished(identity: PublishedWriteIdentity): void;
9
+ rethrowObserverFailure(error: unknown): void;
10
+ };
2
11
  export declare const onCopyPublication: unique symbol;
3
12
  export type CopyPublicationOptions = {
4
13
  [onCopyPublication]?: PinnedWriteParams["verifyPublished"];
@@ -1 +1 @@
1
- {"version":3,"file":"copy-publication.d.ts","sourceRoot":"","sources":["../src/copy-publication.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAI3D,eAAO,MAAM,iBAAiB,eAA8B,CAAC;AAC7D,MAAM,MAAM,sBAAsB,GAAG;IACnC,CAAC,iBAAiB,CAAC,CAAC,EAAE,iBAAiB,CAAC,iBAAiB,CAAC,CAAC;CAC5D,CAAC"}
1
+ {"version":3,"file":"copy-publication.d.ts","sourceRoot":"","sources":["../src/copy-publication.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAGnF,MAAM,MAAM,0BAA0B,GAAG,QAAQ,CAAC;IAChD,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb,CAAC,CAAC;AAEH,wBAAgB,6BAA6B,CAC3C,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,0BAA0B,KAAK,IAAI;IAIpD,WAAW,WAAW,sBAAsB,GAAG,IAAI;IAcnD,sBAAsB,QAAQ,OAAO,GAAG,IAAI;EAO/C;AAID,eAAO,MAAM,iBAAiB,eAA8B,CAAC;AAC7D,MAAM,MAAM,sBAAsB,GAAG;IACnC,CAAC,iBAAiB,CAAC,CAAC,EAAE,iBAAiB,CAAC,iBAAiB,CAAC,CAAC;CAC5D,CAAC"}
@@ -1,3 +1,30 @@
1
+ import { FsSafeError } from "./errors.js";
2
+ export function createCopyPublicationObserver(path, notify) {
3
+ let rejected;
4
+ return {
5
+ onPublished(identity) {
6
+ const receipt = Object.freeze({ path, dev: identity.dev, ino: identity.ino });
7
+ try {
8
+ const result = notify?.(receipt);
9
+ if (result !== null && (typeof result === "object" || typeof result === "function") &&
10
+ "then" in result && typeof result.then === "function") {
11
+ void Promise.resolve(result).catch(() => undefined);
12
+ throw new TypeError("onDestinationPublished must be synchronous");
13
+ }
14
+ }
15
+ catch (error) {
16
+ rejected = { error };
17
+ throw error;
18
+ }
19
+ },
20
+ rethrowObserverFailure(error) {
21
+ if (rejected && (error === rejected.error ||
22
+ (error instanceof FsSafeError && error.cause === rejected.error && error.details?.phase === "publish"))) {
23
+ throw rejected.error;
24
+ }
25
+ },
26
+ };
27
+ }
1
28
  // Internal composition hook; the descriptor is borrowed until the callback returns.
2
29
  // Kept off RootCopyOptions and all public package exports.
3
30
  export const onCopyPublication = Symbol("onCopyPublication");
@@ -0,0 +1,9 @@
1
+ import type { NativeBinding } from "./native-binding.js";
2
+ import { openStagedDirectory } from "./staged-directory.js";
3
+ /** Byte-copy adapter for the shared immutable-source, caller-owned namespace contract. */
4
+ export declare function copyOwnedTree(source: ReturnType<typeof openStagedDirectory>, destination: string, options: {
5
+ signal?: AbortSignal;
6
+ concurrency: number;
7
+ copyFileContents?: NativeBinding["copyFileContents"];
8
+ }): Promise<void>;
9
+ //# sourceMappingURL=copy-tree-portable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"copy-tree-portable.d.ts","sourceRoot":"","sources":["../src/copy-tree-portable.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAGL,mBAAmB,EACpB,MAAM,uBAAuB,CAAC;AAQ/B,0FAA0F;AAC1F,wBAAsB,aAAa,CACjC,MAAM,EAAE,UAAU,CAAC,OAAO,mBAAmB,CAAC,EAC9C,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE;IACP,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,aAAa,CAAC,kBAAkB,CAAC,CAAC;CACtD,GACA,OAAO,CAAC,IAAI,CAAC,CAiNf"}
@@ -0,0 +1,222 @@
1
+ import { setMaxListeners } from "node:events";
2
+ import fs from "node:fs";
3
+ import fsp, {} from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { FsSafeError } from "./errors.js";
6
+ import { assertStagedDirectoryCurrent, exactIdentityMatches, openStagedDirectory, } from "./staged-directory.js";
7
+ function timestampSeconds(nanoseconds) {
8
+ // Dates discard sub-millisecond precision; negative numbers mean "now" in
9
+ // Node's utimes API. Numeric strings retain fractional, pre-epoch timestamps.
10
+ return String(Number(nanoseconds / 1000000000n) + Number(nanoseconds % 1000000000n) / 1e9);
11
+ }
12
+ /** Byte-copy adapter for the shared immutable-source, caller-owned namespace contract. */
13
+ export async function copyOwnedTree(source, destination, options) {
14
+ options.signal?.throwIfAborted();
15
+ const cancellation = new AbortController();
16
+ const signal = cancellation.signal;
17
+ setMaxListeners(options.concurrency, signal);
18
+ const abort = () => cancellation.abort(options.signal?.reason);
19
+ options.signal?.addEventListener("abort", abort, { once: true });
20
+ const pending = new Set();
21
+ const finishing = new Set();
22
+ const buffers = [];
23
+ async function schedule(operation, children) {
24
+ signal.throwIfAborted();
25
+ const task = operation()
26
+ .catch((error) => {
27
+ cancellation.abort(error);
28
+ })
29
+ .finally(() => {
30
+ pending.delete(task);
31
+ children.delete(task);
32
+ });
33
+ pending.add(task);
34
+ children.add(task);
35
+ if (pending.size >= options.concurrency)
36
+ await Promise.race(pending);
37
+ signal.throwIfAborted();
38
+ }
39
+ async function copyFile(from, to, stat) {
40
+ const input = await fsp.open(from, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
41
+ let output;
42
+ try {
43
+ const opened = await input.stat({ bigint: true });
44
+ if (!opened.isFile() || !exactIdentityMatches(stat, opened)) {
45
+ throw new FsSafeError("path-mismatch", "copy source changed while opening");
46
+ }
47
+ output = await fsp.open(to, "wx", 0o600);
48
+ if (options.copyFileContents) {
49
+ // napi-rs owns onabort. Each admitted file gets a separate signal so
50
+ // concurrent native copies cannot replace one another's cancellation.
51
+ const fileCancellation = new AbortController();
52
+ const abortFile = () => fileCancellation.abort();
53
+ signal.addEventListener("abort", abortFile, { once: true });
54
+ try {
55
+ signal.throwIfAborted();
56
+ await options.copyFileContents(input.fd, output.fd, fileCancellation.signal);
57
+ }
58
+ finally {
59
+ signal.removeEventListener("abort", abortFile);
60
+ }
61
+ }
62
+ else {
63
+ const buffer = buffers.pop() ??
64
+ Buffer.allocUnsafe(process.platform === "win32" ? 1024 * 1024 : 128 * 1024);
65
+ try {
66
+ let position = 0;
67
+ while (true) {
68
+ signal?.throwIfAborted();
69
+ const { bytesRead } = await input.read(buffer, 0, buffer.length, position);
70
+ if (bytesRead === 0)
71
+ break;
72
+ let written = 0;
73
+ while (written < bytesRead) {
74
+ signal?.throwIfAborted();
75
+ const { bytesWritten } = await output.write(buffer, written, bytesRead - written, position + written);
76
+ if (bytesWritten === 0)
77
+ throw new Error("copy write made no progress");
78
+ written += bytesWritten;
79
+ }
80
+ position += bytesRead;
81
+ }
82
+ }
83
+ finally {
84
+ buffers.push(buffer);
85
+ }
86
+ }
87
+ signal.throwIfAborted();
88
+ if (process.platform !== "win32")
89
+ await output.chmod(Number(stat.mode & 4095n));
90
+ await output.utimes(timestampSeconds(stat.atimeNs), timestampSeconds(stat.mtimeNs));
91
+ }
92
+ catch (error) {
93
+ cancellation.abort(error);
94
+ throw error;
95
+ }
96
+ finally {
97
+ try {
98
+ await output?.close();
99
+ }
100
+ finally {
101
+ await input.close();
102
+ }
103
+ }
104
+ }
105
+ async function copyDirectory(from, to, parentChildren) {
106
+ signal?.throwIfAborted();
107
+ const stat = await fsp.lstat(from, { bigint: true });
108
+ // Exclusive admission prevents merging a pre-existing directory, including
109
+ // any destination left by an unsuccessful native clone.
110
+ await fsp.mkdir(to, { mode: 0o700 });
111
+ const target = openStagedDirectory(to);
112
+ const children = new Set();
113
+ let original;
114
+ try {
115
+ original = openStagedDirectory(from);
116
+ if (!exactIdentityMatches(stat, original.receipt.identity)) {
117
+ throw new FsSafeError("path-mismatch", "copy source directory changed while opening");
118
+ }
119
+ for (const entry of await fsp.readdir(from, { withFileTypes: true })) {
120
+ signal?.throwIfAborted();
121
+ assertStagedDirectoryCurrent(target.receipt);
122
+ assertStagedDirectoryCurrent(original.receipt);
123
+ const childSource = path.join(from, entry.name);
124
+ const childTarget = path.join(to, entry.name);
125
+ const child = await fsp.lstat(childSource, { bigint: true });
126
+ if (child.isDirectory()) {
127
+ await copyDirectory(childSource, childTarget, children);
128
+ }
129
+ else if (child.isFile()) {
130
+ await schedule(() => copyFile(childSource, childTarget, child), children);
131
+ }
132
+ else if (child.isSymbolicLink()) {
133
+ const link = process.platform === "win32"
134
+ ? await fsp.readlink(childSource)
135
+ : await fsp.readlink(childSource, { encoding: "buffer" });
136
+ let type;
137
+ if (process.platform === "win32") {
138
+ // Older Node releases infer link type from the destination, where a
139
+ // relative directory target may not exist yet. Inspect the source.
140
+ // lstat does not expose the Windows link's directory attribute, so
141
+ // an unresolved source cannot be recreated with a proven type.
142
+ try {
143
+ type = (await fsp.stat(childSource)).isDirectory() ? "dir" : "file";
144
+ }
145
+ catch (error) {
146
+ if (error instanceof Error &&
147
+ "code" in error &&
148
+ (error.code === "ENOENT" || error.code === "ENOTDIR" || error.code === "ELOOP")) {
149
+ throw new FsSafeError("unsupported-platform", "byte copying cannot determine the type of an unresolved Windows symbolic link", { cause: error });
150
+ }
151
+ throw error;
152
+ }
153
+ }
154
+ signal?.throwIfAborted();
155
+ await fsp.symlink(link, childTarget, type);
156
+ }
157
+ else {
158
+ throw new FsSafeError("not-file", "tree copying does not support special files");
159
+ }
160
+ }
161
+ }
162
+ catch (error) {
163
+ cancellation.abort(error);
164
+ throw error;
165
+ }
166
+ finally {
167
+ // Traversal can advance into siblings while this directory's own files
168
+ // settle. Children retain their parents and finish metadata bottom-up.
169
+ const completion = (async () => {
170
+ try {
171
+ await Promise.all(children);
172
+ signal.throwIfAborted();
173
+ if (original) {
174
+ assertStagedDirectoryCurrent(original.receipt);
175
+ assertStagedDirectoryCurrent(target.receipt);
176
+ if (process.platform !== "win32")
177
+ fs.fchmodSync(target.fd, Number(stat.mode & 4095n));
178
+ await fsp.utimes(to, timestampSeconds(stat.atimeNs), timestampSeconds(stat.mtimeNs));
179
+ assertStagedDirectoryCurrent(target.receipt);
180
+ }
181
+ }
182
+ finally {
183
+ try {
184
+ if (original)
185
+ fs.closeSync(original.fd);
186
+ }
187
+ finally {
188
+ fs.closeSync(target.fd);
189
+ }
190
+ }
191
+ })()
192
+ .catch((error) => {
193
+ cancellation.abort(error);
194
+ })
195
+ .finally(() => {
196
+ finishing.delete(completion);
197
+ parentChildren?.delete(completion);
198
+ });
199
+ finishing.add(completion);
200
+ parentChildren?.add(completion);
201
+ // Bound traversed-but-unsettled directories as well as file workers;
202
+ // ancestors stay open along the current traversal path.
203
+ if (finishing.size >= options.concurrency)
204
+ await Promise.race(finishing);
205
+ }
206
+ signal.throwIfAborted();
207
+ }
208
+ try {
209
+ await copyDirectory(source.receipt.realPath, destination);
210
+ await Promise.all(finishing);
211
+ signal.throwIfAborted();
212
+ }
213
+ catch (error) {
214
+ await Promise.all(finishing);
215
+ options.signal?.throwIfAborted();
216
+ throw error;
217
+ }
218
+ finally {
219
+ await Promise.all(finishing);
220
+ options.signal?.removeEventListener("abort", abort);
221
+ }
222
+ }
package/dist/copy.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { type CopyCloneMode } from "./copy-policy.js";
2
+ export { readCloneFileMetadata, type CloneFileMetadata } from "./clone-metadata.js";
3
+ export type { CopyCloneMode } from "./copy-policy.js";
4
+ export type TreeCloneBackend = "apfs" | "btrfs" | "refs" | "xfs" | "zfs";
5
+ export type CopyTreeOptions = {
6
+ clone?: CopyCloneMode;
7
+ signal?: AbortSignal;
8
+ concurrency?: number;
9
+ };
10
+ /** Inspect an existing real directory without creating probe files. */
11
+ export declare function probeTreeClone(parentPath: string): TreeCloneBackend | undefined;
12
+ /** Create an empty clone source: a Btrfs subvolume or an ordinary supported directory. */
13
+ export declare function createCloneSource(destination: string, options?: Pick<CopyTreeOptions, "signal">): Promise<void>;
14
+ /** Copy an immutable, caller-owned tree, preferring native cloning unless configured otherwise. */
15
+ export declare function copyTree(source: string, destination: string, options?: CopyTreeOptions): Promise<void>;
16
+ //# sourceMappingURL=copy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"copy.d.ts","sourceRoot":"","sources":["../src/copy.ts"],"names":[],"mappings":"AAGA,OAAO,EAAwB,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAK5E,OAAO,EAAE,qBAAqB,EAAE,KAAK,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACpF,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,KAAK,GAAG,KAAK,CAAC;AACzE,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,CAAC,EAAE,aAAa,CAAC;IACtB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,uEAAuE;AACvE,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAS/E;AAED,0FAA0F;AAC1F,wBAAsB,iBAAiB,CACrC,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,IAAI,CAAC,eAAe,EAAE,QAAQ,CAAM,GAC5C,OAAO,CAAC,IAAI,CAAC,CAEf;AAED,mGAAmG;AACnG,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,IAAI,CAAC,CAIf"}