@openclaw/fs-safe 0.9.0 → 0.10.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 (178) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +17 -0
  3. package/dist/advanced.d.ts +1 -0
  4. package/dist/advanced.d.ts.map +1 -1
  5. package/dist/advanced.js +1 -0
  6. package/dist/archive-crc32.d.ts.map +1 -1
  7. package/dist/archive-crc32.js +6 -1
  8. package/dist/archive-deadline.d.ts.map +1 -1
  9. package/dist/archive-deadline.js +3 -4
  10. package/dist/archive-gzip-tail.d.ts +2 -0
  11. package/dist/archive-gzip-tail.d.ts.map +1 -1
  12. package/dist/archive-gzip-tail.js +20 -3
  13. package/dist/archive-merge.js +1 -1
  14. package/dist/archive-native.d.ts.map +1 -1
  15. package/dist/archive-native.js +3 -2
  16. package/dist/archive-read.d.ts.map +1 -1
  17. package/dist/archive-read.js +67 -66
  18. package/dist/archive-tar-stream.d.ts +11 -4
  19. package/dist/archive-tar-stream.d.ts.map +1 -1
  20. package/dist/archive-tar-stream.js +20 -8
  21. package/dist/archive-tar-wasm.d.ts.map +1 -1
  22. package/dist/archive-tar-wasm.js +2 -1
  23. package/dist/bounded-read.d.ts +7 -0
  24. package/dist/bounded-read.d.ts.map +1 -1
  25. package/dist/bounded-read.js +73 -43
  26. package/dist/clone-metadata.d.ts +19 -0
  27. package/dist/clone-metadata.d.ts.map +1 -0
  28. package/dist/clone-metadata.js +32 -0
  29. package/dist/copy-file-input.d.ts +23 -0
  30. package/dist/copy-file-input.d.ts.map +1 -0
  31. package/dist/copy-file-input.js +91 -0
  32. package/dist/copy-policy.d.ts +3 -0
  33. package/dist/copy-policy.d.ts.map +1 -0
  34. package/dist/copy-policy.js +8 -0
  35. package/dist/copy-publication.d.ts +10 -1
  36. package/dist/copy-publication.d.ts.map +1 -1
  37. package/dist/copy-publication.js +27 -0
  38. package/dist/copy-tree-portable.d.ts +9 -0
  39. package/dist/copy-tree-portable.d.ts.map +1 -0
  40. package/dist/copy-tree-portable.js +191 -0
  41. package/dist/copy.d.ts +16 -0
  42. package/dist/copy.d.ts.map +1 -0
  43. package/dist/copy.js +123 -0
  44. package/dist/durability.d.ts +1 -1
  45. package/dist/durability.d.ts.map +1 -1
  46. package/dist/error-detail.d.ts.map +1 -1
  47. package/dist/error-detail.js +4 -1
  48. package/dist/file-hash.d.ts +6 -2
  49. package/dist/file-hash.d.ts.map +1 -1
  50. package/dist/file-hash.js +44 -8
  51. package/dist/filename.d.ts.map +1 -1
  52. package/dist/filename.js +2 -12
  53. package/dist/guarded-mkdir.d.ts +1 -0
  54. package/dist/guarded-mkdir.d.ts.map +1 -1
  55. package/dist/guarded-mkdir.js +1 -0
  56. package/dist/guarded-mutation.d.ts +2 -0
  57. package/dist/guarded-mutation.d.ts.map +1 -1
  58. package/dist/guarded-mutation.js +8 -4
  59. package/dist/home-dir.d.ts.map +1 -1
  60. package/dist/home-dir.js +9 -7
  61. package/dist/index.d.ts +1 -1
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/install-path.d.ts.map +1 -1
  64. package/dist/install-path.js +2 -6
  65. package/dist/json-durable-queue.d.ts.map +1 -1
  66. package/dist/json-durable-queue.js +13 -14
  67. package/dist/local-roots.d.ts.map +1 -1
  68. package/dist/local-roots.js +22 -24
  69. package/dist/move-path-stage.d.ts +8 -0
  70. package/dist/move-path-stage.d.ts.map +1 -0
  71. package/dist/move-path-stage.js +55 -0
  72. package/dist/move-path.d.ts.map +1 -1
  73. package/dist/move-path.js +26 -31
  74. package/dist/mutation-authority.d.ts +8 -0
  75. package/dist/mutation-authority.d.ts.map +1 -0
  76. package/dist/mutation-authority.js +36 -0
  77. package/dist/native-binding.d.ts +24 -1
  78. package/dist/native-binding.d.ts.map +1 -1
  79. package/dist/native-operations.d.ts +1 -1
  80. package/dist/native-operations.d.ts.map +1 -1
  81. package/dist/native-operations.js +11 -5
  82. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  83. package/dist/native-pinned-write-windows.js +18 -7
  84. package/dist/native-pinned-write.d.ts.map +1 -1
  85. package/dist/native-pinned-write.js +1 -0
  86. package/dist/native-staged-file.d.ts +3 -3
  87. package/dist/native-staged-file.d.ts.map +1 -1
  88. package/dist/native-staged-file.js +35 -8
  89. package/dist/path.d.ts.map +1 -1
  90. package/dist/path.js +3 -1
  91. package/dist/permissions-windows.d.ts.map +1 -1
  92. package/dist/permissions-windows.js +38 -48
  93. package/dist/pinned-operation.js +1 -1
  94. package/dist/pinned-write.d.ts +5 -1
  95. package/dist/pinned-write.d.ts.map +1 -1
  96. package/dist/pinned-write.js +50 -30
  97. package/dist/positional-read.d.ts +9 -0
  98. package/dist/positional-read.d.ts.map +1 -0
  99. package/dist/positional-read.js +36 -0
  100. package/dist/publish-copy-stage.d.ts +13 -0
  101. package/dist/publish-copy-stage.d.ts.map +1 -0
  102. package/dist/publish-copy-stage.js +47 -0
  103. package/dist/publish-file.d.ts.map +1 -1
  104. package/dist/publish-file.js +6 -5
  105. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  106. package/dist/replace-file-temp-owner.js +6 -2
  107. package/dist/root-context.d.ts +3 -0
  108. package/dist/root-context.d.ts.map +1 -1
  109. package/dist/root-context.js +51 -26
  110. package/dist/root-directory-list.d.ts +24 -0
  111. package/dist/root-directory-list.d.ts.map +1 -0
  112. package/dist/root-directory-list.js +201 -0
  113. package/dist/root-errors.d.ts +1 -0
  114. package/dist/root-errors.d.ts.map +1 -1
  115. package/dist/root-errors.js +3 -0
  116. package/dist/root-file.d.ts +2 -0
  117. package/dist/root-file.d.ts.map +1 -1
  118. package/dist/root-file.js +12 -4
  119. package/dist/root-impl.d.ts +6 -50
  120. package/dist/root-impl.d.ts.map +1 -1
  121. package/dist/root-impl.js +330 -210
  122. package/dist/root-options.d.ts +66 -0
  123. package/dist/root-options.d.ts.map +1 -0
  124. package/dist/root-options.js +18 -0
  125. package/dist/root-path-existing.d.ts +5 -0
  126. package/dist/root-path-existing.d.ts.map +1 -1
  127. package/dist/root-path-existing.js +56 -1
  128. package/dist/root-path.d.ts +1 -0
  129. package/dist/root-path.d.ts.map +1 -1
  130. package/dist/root-path.js +74 -162
  131. package/dist/root-symlink-policy.d.ts +13 -0
  132. package/dist/root-symlink-policy.d.ts.map +1 -0
  133. package/dist/root-symlink-policy.js +34 -0
  134. package/dist/root-walk.d.ts +5 -4
  135. package/dist/root-walk.d.ts.map +1 -1
  136. package/dist/root-walk.js +99 -50
  137. package/dist/root-write-mode.d.ts.map +1 -1
  138. package/dist/root-write-mode.js +1 -4
  139. package/dist/root.d.ts +3 -1
  140. package/dist/root.d.ts.map +1 -1
  141. package/dist/secure-file.d.ts.map +1 -1
  142. package/dist/secure-file.js +4 -4
  143. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  144. package/dist/sidecar-lock-acquire.js +2 -1
  145. package/dist/sidecar-lock-policy.d.ts.map +1 -1
  146. package/dist/sidecar-lock-policy.js +3 -1
  147. package/dist/temp-cleanup.d.ts.map +1 -1
  148. package/dist/temp-cleanup.js +3 -2
  149. package/dist/timing.d.ts +1 -0
  150. package/dist/timing.d.ts.map +1 -1
  151. package/dist/timing.js +25 -6
  152. package/dist/trash.js +2 -2
  153. package/dist/walk.d.ts.map +1 -1
  154. package/dist/walk.js +6 -26
  155. package/dist/windows-owner.d.ts +9 -12
  156. package/dist/windows-owner.d.ts.map +1 -1
  157. package/dist/windows-owner.js +24 -58
  158. package/dist/write-file-handle.d.ts +6 -0
  159. package/dist/write-file-handle.d.ts.map +1 -0
  160. package/dist/write-file-handle.js +25 -0
  161. package/docs/advanced.md +18 -4
  162. package/docs/archive.md +22 -7
  163. package/docs/atomic.md +7 -0
  164. package/docs/contributing.md +7 -0
  165. package/docs/copy.md +86 -0
  166. package/docs/durability.md +17 -2
  167. package/docs/errors.md +1 -1
  168. package/docs/local-roots.md +8 -1
  169. package/docs/native.md +14 -1
  170. package/docs/permissions.md +20 -10
  171. package/docs/positional-read.md +63 -0
  172. package/docs/root.md +166 -4
  173. package/docs/security-model.md +14 -0
  174. package/docs/timing.md +2 -0
  175. package/docs/types.md +18 -11
  176. package/docs/walk.md +54 -5
  177. package/docs/writing.md +20 -2
  178. package/package.json +13 -8
@@ -17,6 +17,7 @@ const types = new Map([
17
17
  [51, "CharacterDevice"], [52, "BlockDevice"], [53, "Directory"],
18
18
  [54, "FIFO"], [55, "ContiguousFile"], [68, "GNUDumpDir"],
19
19
  ]);
20
+ const utf8Decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
20
21
  function parserError(message) {
21
22
  const mapped = classifyArchiveParserError(message);
22
23
  if (mapped)
@@ -47,7 +48,7 @@ export class TarParserStream extends Transform {
47
48
  }
48
49
  text() {
49
50
  const abi = this.abi;
50
- return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(this.bytes(abi.text_ptr(), abi.text_len()));
51
+ return utf8Decoder.decode(this.bytes(abi.text_ptr(), abi.text_len()));
51
52
  }
52
53
  _transform(chunk, _encoding, callback) {
53
54
  try {
@@ -1,5 +1,12 @@
1
1
  import type { FileHandle } from "node:fs/promises";
2
2
  type ReadableFileHandle = Pick<FileHandle, "read">;
3
+ export declare function readBoundedAsync(maxBytes: number, readChunk: (scratch: Buffer, length: number) => Promise<number>, options?: {
4
+ observeRegularFileSize?: () => number | undefined;
5
+ initialSize?: number;
6
+ createLimitError?: () => Error;
7
+ /** Keep the result outside Node's shared small-buffer allocation pool. */
8
+ unpooled?: boolean;
9
+ }): Promise<Buffer>;
3
10
  /**
4
11
  * Reads from the handle's current offset without closing it. A bounded read
5
12
  * consumes at most maxBytes + 1 bytes so growth after an earlier stat cannot
@@ -1 +1 @@
1
- {"version":3,"file":"bounded-read.d.ts","sourceRoot":"","sources":["../src/bounded-read.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAMnD,KAAK,kBAAkB,GAAG,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;AA+DnD;;;;GAIG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,kBAAkB,EAC1B,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,CAAC,CAMjB;AAyBD,oFAAoF;AACpF,wBAAsB,yBAAyB,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAK7F;AAED,mFAAmF;AACnF,wBAAgB,6BAA6B,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAalF"}
1
+ {"version":3,"file":"bounded-read.d.ts","sourceRoot":"","sources":["../src/bounded-read.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAQnD,KAAK,kBAAkB,GAAG,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;AA8CnD,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,EAC/D,OAAO,GAAE;IACP,sBAAsB,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IAClD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gBAAgB,CAAC,EAAE,MAAM,KAAK,CAAC;IAC/B,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,OAAO,CAAC;CACf,GACL,OAAO,CAAC,MAAM,CAAC,CAuBjB;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,kBAAkB,EAC1B,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,CAAC,CAMjB;AAyBD,oFAAoF;AACpF,wBAAsB,yBAAyB,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAK7F;AAED,mFAAmF;AACnF,wBAAgB,6BAA6B,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAoBlF"}
@@ -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,31 @@ 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
111
  /** Sync bounded read from a numeric descriptor. The caller owns the descriptor. */
93
112
  export function readFileDescriptorBoundedSync(fd, maxBytes) {
94
113
  normalizeMaxBytes(maxBytes);
95
- const chunks = [];
96
- const scratch = createScratchBuffer(maxBytes);
97
114
  let total = 0;
115
+ // Small budgets already bound the first allocation without a size lookup.
116
+ const size = maxBytes <= READ_CHUNK_BYTES ? maxBytes : regularFileSize(fd);
117
+ let buffer = size === undefined ? createScratchBuffer(maxBytes) : createInitialBuffer(maxBytes, size);
118
+ if (size !== undefined) {
119
+ const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, null);
120
+ if (bytesRead === 0)
121
+ return finishReadBuffer(buffer, 0);
122
+ const currentSize = bytesRead < buffer.length ? regularFileSize(fd) : undefined;
123
+ total = addReadBytes(total, bytesRead, maxBytes);
124
+ if (currentSize !== undefined && bytesRead >= currentSize)
125
+ return finishReadBuffer(buffer, total);
126
+ }
98
127
  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 });
128
+ if (total === buffer.length)
129
+ buffer = growReadBuffer(buffer, maxBytes);
130
+ const remaining = buffer.subarray(total);
131
+ const bytesRead = fs.readSync(fd, remaining, 0, remaining.length, null);
132
+ if (bytesRead === 0)
133
+ return finishReadBuffer(buffer, total);
134
+ total = addReadBytes(total, bytesRead, maxBytes);
105
135
  }
106
136
  }
@@ -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,23 @@
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 copyFileChunks(input: CopyFileInput, maxBytes?: number): AsyncGenerator<Buffer>;
20
+ export declare function writeCopyFileToFd(fd: number, input: CopyFileInput, maxBytes?: number, assertBeforeMutation?: () => void): Promise<void>;
21
+ export declare function createNativeCopyFile(native: NativeBinding, input: CopyFileInput, parentFd: number, basename: string, maxBytes: number | undefined, sync: boolean): Promise<NativeFileCopyResult | undefined>;
22
+ export declare function assertNativeCopyCompleted(input: CopyFileInput, copied?: NativeFileCopyResult): void;
23
+ //# 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,wBAAuB,cAAc,CACnC,KAAK,EAAE,aAAa,EACpB,QAAQ,SAAW,GAClB,cAAc,CAAC,MAAM,CAAC,CAiBxB;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,CASf;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,91 @@
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 { writeAllToFile } from "./write-file-handle.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* copyFileChunks(input, maxBytes = Infinity) {
25
+ const buffer = Buffer.allocUnsafe(Math.min(256 * 1024, Math.max(64 * 1024, input.size)));
26
+ let position = 0;
27
+ while (true) {
28
+ input.signal?.throwIfAborted();
29
+ const { bytesRead } = await input.handle.read(buffer, 0, Math.min(buffer.length, maxBytes - position + 1), position);
30
+ input.signal?.throwIfAborted();
31
+ if (bytesRead > maxBytes - position) {
32
+ throw new FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes`);
33
+ }
34
+ if (bytesRead === 0)
35
+ return;
36
+ position += bytesRead;
37
+ // The consumer finishes writing before the next iteration reuses this buffer.
38
+ yield buffer.subarray(0, bytesRead);
39
+ }
40
+ }
41
+ export async function writeCopyFileToFd(fd, input, maxBytes, assertBeforeMutation) {
42
+ for await (const chunk of copyFileChunks(input, maxBytes)) {
43
+ await writeAllToFile(fd, chunk, {
44
+ assertBeforeMutation: () => {
45
+ input.signal?.throwIfAborted();
46
+ assertBeforeMutation?.();
47
+ },
48
+ });
49
+ }
50
+ }
51
+ export async function createNativeCopyFile(native, input, parentFd, basename, maxBytes, sync) {
52
+ input.signal?.throwIfAborted();
53
+ if (!native.copyFileExclusive) {
54
+ if (input.clone === "always") {
55
+ throw new FsSafeError("helper-unavailable", "native file cloning is unavailable");
56
+ }
57
+ return undefined;
58
+ }
59
+ const nativeSignal = input.signal ? AbortSignal.any([input.signal]) : undefined;
60
+ try {
61
+ // The caller adopts this descriptor before observing a later cancellation.
62
+ return await native.copyFileExclusive(input.handle.fd, parentFd, basename, input.clone, maxBytes !== undefined && Number.isFinite(maxBytes) ? maxBytes : undefined, nativeSignal, sync);
63
+ }
64
+ catch (error) {
65
+ const code = error?.code;
66
+ if (code === "Cancelled" || code === "ABORT_ERR")
67
+ input.signal?.throwIfAborted();
68
+ if (code === "too-large") {
69
+ throw new FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes`, { cause: error });
70
+ }
71
+ if (code === "ENOTSUP" && input.clone !== "always")
72
+ return undefined;
73
+ if (code === "ENOTSUP") {
74
+ throw new FsSafeError("unsupported-platform", "native file cloning is unsupported", { cause: error });
75
+ }
76
+ throw new FsSafeError("helper-failed", "native file copy failed", { cause: error });
77
+ }
78
+ finally {
79
+ if (nativeSignal)
80
+ nativeSignal.onabort = null;
81
+ }
82
+ }
83
+ export function assertNativeCopyCompleted(input, copied) {
84
+ if (copied?.errorCode) {
85
+ if (copied.errorCode === "Cancelled" || copied.errorCode === "ABORT_ERR")
86
+ input.signal?.throwIfAborted();
87
+ const unsupported = ["ENOTSUP", "EOPNOTSUPP", "ENOSYS", "EXDEV", "EINVAL"].includes(copied.errorCode);
88
+ 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 }) });
89
+ }
90
+ input.signal?.throwIfAborted();
91
+ }
@@ -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;AAE/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,CAkLf"}
@@ -0,0 +1,191 @@
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
+ /** Byte-copy adapter for the shared immutable-source, caller-owned namespace contract. */
8
+ export async function copyOwnedTree(source, destination, options) {
9
+ options.signal?.throwIfAborted();
10
+ const cancellation = new AbortController();
11
+ const signal = cancellation.signal;
12
+ setMaxListeners(options.concurrency, signal);
13
+ const abort = () => cancellation.abort(options.signal?.reason);
14
+ options.signal?.addEventListener("abort", abort, { once: true });
15
+ const pending = new Set();
16
+ const buffers = [];
17
+ const settle = async () => {
18
+ await Promise.all(pending);
19
+ };
20
+ async function schedule(operation) {
21
+ signal.throwIfAborted();
22
+ const task = operation()
23
+ .catch((error) => {
24
+ cancellation.abort(error);
25
+ })
26
+ .finally(() => {
27
+ pending.delete(task);
28
+ });
29
+ pending.add(task);
30
+ if (pending.size >= options.concurrency)
31
+ await Promise.race(pending);
32
+ signal.throwIfAborted();
33
+ }
34
+ async function copyFile(from, to, stat) {
35
+ const input = await fsp.open(from, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
36
+ let output;
37
+ try {
38
+ const opened = await input.stat({ bigint: true });
39
+ if (!opened.isFile() || !exactIdentityMatches(stat, opened)) {
40
+ throw new FsSafeError("path-mismatch", "copy source changed while opening");
41
+ }
42
+ output = await fsp.open(to, "wx", 0o600);
43
+ if (options.copyFileContents) {
44
+ // napi-rs owns onabort. Each admitted file gets a separate signal so
45
+ // concurrent native copies cannot replace one another's cancellation.
46
+ const fileCancellation = new AbortController();
47
+ const abortFile = () => fileCancellation.abort();
48
+ signal.addEventListener("abort", abortFile, { once: true });
49
+ try {
50
+ signal.throwIfAborted();
51
+ await options.copyFileContents(input.fd, output.fd, fileCancellation.signal);
52
+ }
53
+ finally {
54
+ signal.removeEventListener("abort", abortFile);
55
+ }
56
+ }
57
+ else {
58
+ const buffer = buffers.pop() ??
59
+ Buffer.allocUnsafe(process.platform === "win32" ? 1024 * 1024 : 128 * 1024);
60
+ try {
61
+ let position = 0;
62
+ while (true) {
63
+ signal?.throwIfAborted();
64
+ const { bytesRead } = await input.read(buffer, 0, buffer.length, position);
65
+ if (bytesRead === 0)
66
+ break;
67
+ let written = 0;
68
+ while (written < bytesRead) {
69
+ signal?.throwIfAborted();
70
+ const { bytesWritten } = await output.write(buffer, written, bytesRead - written, position + written);
71
+ if (bytesWritten === 0)
72
+ throw new Error("copy write made no progress");
73
+ written += bytesWritten;
74
+ }
75
+ position += bytesRead;
76
+ }
77
+ }
78
+ finally {
79
+ buffers.push(buffer);
80
+ }
81
+ }
82
+ signal.throwIfAborted();
83
+ if (process.platform !== "win32")
84
+ await output.chmod(Number(stat.mode & 4095n));
85
+ await output.utimes(stat.atime, stat.mtime);
86
+ }
87
+ catch (error) {
88
+ cancellation.abort(error);
89
+ throw error;
90
+ }
91
+ finally {
92
+ try {
93
+ await output?.close();
94
+ }
95
+ finally {
96
+ await input.close();
97
+ }
98
+ }
99
+ }
100
+ async function copyDirectory(from, to) {
101
+ signal?.throwIfAborted();
102
+ const stat = await fsp.lstat(from, { bigint: true });
103
+ // Exclusive admission prevents merging a pre-existing directory, including
104
+ // any destination left by an unsuccessful native clone.
105
+ await fsp.mkdir(to, { mode: 0o700 });
106
+ const target = openStagedDirectory(to);
107
+ let original;
108
+ try {
109
+ original = openStagedDirectory(from);
110
+ if (!exactIdentityMatches(stat, original.receipt.identity)) {
111
+ throw new FsSafeError("path-mismatch", "copy source directory changed while opening");
112
+ }
113
+ for (const entry of await fsp.readdir(from, { withFileTypes: true })) {
114
+ signal?.throwIfAborted();
115
+ assertStagedDirectoryCurrent(target.receipt);
116
+ assertStagedDirectoryCurrent(original.receipt);
117
+ const childSource = path.join(from, entry.name);
118
+ const childTarget = path.join(to, entry.name);
119
+ const child = await fsp.lstat(childSource, { bigint: true });
120
+ if (child.isDirectory()) {
121
+ await copyDirectory(childSource, childTarget);
122
+ }
123
+ else if (child.isFile()) {
124
+ await schedule(() => copyFile(childSource, childTarget, child));
125
+ }
126
+ else if (child.isSymbolicLink()) {
127
+ const link = process.platform === "win32"
128
+ ? await fsp.readlink(childSource)
129
+ : await fsp.readlink(childSource, { encoding: "buffer" });
130
+ let type;
131
+ if (process.platform === "win32") {
132
+ // Older Node releases infer link type from the destination, where a
133
+ // relative directory target may not exist yet. Inspect the source.
134
+ // lstat does not expose the Windows link's directory attribute, so
135
+ // an unresolved source cannot be recreated with a proven type.
136
+ try {
137
+ type = (await fsp.stat(childSource)).isDirectory() ? "dir" : "file";
138
+ }
139
+ catch (error) {
140
+ if (error instanceof Error &&
141
+ "code" in error &&
142
+ (error.code === "ENOENT" || error.code === "ENOTDIR" || error.code === "ELOOP")) {
143
+ throw new FsSafeError("unsupported-platform", "byte copying cannot determine the type of an unresolved Windows symbolic link", { cause: error });
144
+ }
145
+ throw error;
146
+ }
147
+ }
148
+ signal?.throwIfAborted();
149
+ await fsp.symlink(link, childTarget, type);
150
+ }
151
+ else {
152
+ throw new FsSafeError("not-file", "tree copying does not support special files");
153
+ }
154
+ }
155
+ await settle();
156
+ signal.throwIfAborted();
157
+ assertStagedDirectoryCurrent(original.receipt);
158
+ assertStagedDirectoryCurrent(target.receipt);
159
+ if (process.platform !== "win32")
160
+ fs.fchmodSync(target.fd, Number(stat.mode & 4095n));
161
+ await fsp.utimes(to, stat.atime, stat.mtime);
162
+ assertStagedDirectoryCurrent(target.receipt);
163
+ }
164
+ catch (error) {
165
+ cancellation.abort(error);
166
+ throw error;
167
+ }
168
+ finally {
169
+ // Directory descriptors and destination ownership outlive every admitted
170
+ // write, including when traversal, a sibling copy, or cancellation fails.
171
+ await settle();
172
+ try {
173
+ if (original)
174
+ fs.closeSync(original.fd);
175
+ }
176
+ finally {
177
+ fs.closeSync(target.fd);
178
+ }
179
+ }
180
+ }
181
+ try {
182
+ await copyDirectory(source.receipt.realPath, destination);
183
+ }
184
+ catch (error) {
185
+ options.signal?.throwIfAborted();
186
+ throw error;
187
+ }
188
+ finally {
189
+ options.signal?.removeEventListener("abort", abort);
190
+ }
191
+ }