@openclaw/fs-safe 0.10.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 (259) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/LICENSE +1 -0
  3. package/README.md +36 -5
  4. package/dist/absolute-path.d.ts.map +1 -1
  5. package/dist/absolute-path.js +3 -2
  6. package/dist/advanced.d.ts +4 -0
  7. package/dist/advanced.d.ts.map +1 -1
  8. package/dist/advanced.js +4 -0
  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 +4 -5
  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 +3 -0
  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 +17 -9
  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 +13 -8
  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 +16 -9
  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/directory-durability.d.ts.map +1 -1
  58. package/dist/directory-durability.js +5 -4
  59. package/dist/directory-guard.d.ts +11 -1
  60. package/dist/directory-guard.d.ts.map +1 -1
  61. package/dist/directory-guard.js +53 -11
  62. package/dist/durability.d.ts +1 -1
  63. package/dist/durability.d.ts.map +1 -1
  64. package/dist/durability.js +1 -1
  65. package/dist/file-handle-transfer.d.ts +14 -0
  66. package/dist/file-handle-transfer.d.ts.map +1 -0
  67. package/dist/file-handle-transfer.js +64 -0
  68. package/dist/file-hash.d.ts +3 -0
  69. package/dist/file-hash.d.ts.map +1 -1
  70. package/dist/file-hash.js +91 -31
  71. package/dist/file-lock-sync.d.ts.map +1 -1
  72. package/dist/file-lock-sync.js +8 -4
  73. package/dist/file-store-boundary.d.ts.map +1 -1
  74. package/dist/file-store-boundary.js +7 -5
  75. package/dist/file-store-path.d.ts +3 -0
  76. package/dist/file-store-path.d.ts.map +1 -0
  77. package/dist/file-store-path.js +27 -0
  78. package/dist/file-store-prune.d.ts.map +1 -1
  79. package/dist/file-store-prune.js +6 -4
  80. package/dist/file-store-sync-write.d.ts.map +1 -1
  81. package/dist/file-store-sync-write.js +56 -44
  82. package/dist/file-store.d.ts.map +1 -1
  83. package/dist/file-store.js +2 -18
  84. package/dist/filename.d.ts.map +1 -1
  85. package/dist/filename.js +2 -1
  86. package/dist/guarded-mkdir.d.ts.map +1 -1
  87. package/dist/guarded-mkdir.js +3 -2
  88. package/dist/guest-dispatch-python.d.ts +2 -0
  89. package/dist/guest-dispatch-python.d.ts.map +1 -0
  90. package/dist/guest-dispatch-python.js +117 -0
  91. package/dist/guest-native-python.d.ts +4 -0
  92. package/dist/guest-native-python.d.ts.map +1 -0
  93. package/dist/guest-native-python.js +135 -0
  94. package/dist/guest.d.ts +9 -0
  95. package/dist/guest.d.ts.map +1 -0
  96. package/dist/guest.js +413 -0
  97. package/dist/index.d.ts +1 -1
  98. package/dist/index.d.ts.map +1 -1
  99. package/dist/install-path.d.ts.map +1 -1
  100. package/dist/install-path.js +3 -2
  101. package/dist/json-durable-queue-directory.js +3 -3
  102. package/dist/json-durable-queue.d.ts.map +1 -1
  103. package/dist/json-durable-queue.js +6 -4
  104. package/dist/json.d.ts.map +1 -1
  105. package/dist/json.js +2 -1
  106. package/dist/local-roots.d.ts.map +1 -1
  107. package/dist/local-roots.js +2 -1
  108. package/dist/move-path-stage.d.ts.map +1 -1
  109. package/dist/move-path-stage.js +2 -1
  110. package/dist/move-path.d.ts.map +1 -1
  111. package/dist/move-path.js +4 -3
  112. package/dist/mutation-authority.d.ts +1 -0
  113. package/dist/mutation-authority.d.ts.map +1 -1
  114. package/dist/mutation-authority.js +4 -4
  115. package/dist/native-binding.d.ts +7 -2
  116. package/dist/native-binding.d.ts.map +1 -1
  117. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  118. package/dist/native-pinned-write-windows.js +3 -2
  119. package/dist/native-pinned-write.d.ts.map +1 -1
  120. package/dist/native-pinned-write.js +2 -1
  121. package/dist/opened-realpath.d.ts.map +1 -1
  122. package/dist/opened-realpath.js +5 -4
  123. package/dist/output.d.ts +2 -0
  124. package/dist/output.d.ts.map +1 -1
  125. package/dist/output.js +2 -0
  126. package/dist/overwrite-file-handle.d.ts +8 -0
  127. package/dist/overwrite-file-handle.d.ts.map +1 -0
  128. package/dist/overwrite-file-handle.js +42 -0
  129. package/dist/path-case.d.ts +7 -0
  130. package/dist/path-case.d.ts.map +1 -0
  131. package/dist/path-case.js +136 -0
  132. package/dist/path.d.ts.map +1 -1
  133. package/dist/path.js +2 -1
  134. package/dist/permissions-windows.d.ts +1 -1
  135. package/dist/permissions-windows.d.ts.map +1 -1
  136. package/dist/permissions-windows.js +48 -6
  137. package/dist/pinned-open.d.ts.map +1 -1
  138. package/dist/pinned-open.js +3 -1
  139. package/dist/pinned-write.d.ts +2 -2
  140. package/dist/pinned-write.d.ts.map +1 -1
  141. package/dist/pinned-write.js +2 -1
  142. package/dist/private-temp-workspace.d.ts.map +1 -1
  143. package/dist/private-temp-workspace.js +6 -4
  144. package/dist/realpath.d.ts +4 -0
  145. package/dist/realpath.d.ts.map +1 -0
  146. package/dist/realpath.js +43 -0
  147. package/dist/recursive-mkdir-path.d.ts +3 -0
  148. package/dist/recursive-mkdir-path.d.ts.map +1 -0
  149. package/dist/recursive-mkdir-path.js +8 -0
  150. package/dist/replace-directory.d.ts.map +1 -1
  151. package/dist/replace-directory.js +2 -1
  152. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  153. package/dist/replace-file-copy-fallback.js +23 -31
  154. package/dist/replace-file-copy-source.d.ts.map +1 -1
  155. package/dist/replace-file-copy-source.js +7 -12
  156. package/dist/replace-file-mode.d.ts +3 -0
  157. package/dist/replace-file-mode.d.ts.map +1 -0
  158. package/dist/replace-file-mode.js +10 -0
  159. package/dist/replace-file.d.ts +1 -0
  160. package/dist/replace-file.d.ts.map +1 -1
  161. package/dist/replace-file.js +14 -8
  162. package/dist/root-context.d.ts.map +1 -1
  163. package/dist/root-context.js +8 -8
  164. package/dist/root-create-input.d.ts +10 -0
  165. package/dist/root-create-input.d.ts.map +1 -0
  166. package/dist/root-create-input.js +80 -0
  167. package/dist/root-directory-list.d.ts +3 -1
  168. package/dist/root-directory-list.d.ts.map +1 -1
  169. package/dist/root-directory-list.js +21 -3
  170. package/dist/root-entries.d.ts +11 -0
  171. package/dist/root-entries.d.ts.map +1 -0
  172. package/dist/root-entries.js +61 -0
  173. package/dist/root-errors.d.ts +5 -5
  174. package/dist/root-errors.d.ts.map +1 -1
  175. package/dist/root-errors.js +13 -12
  176. package/dist/root-impl.d.ts +13 -3
  177. package/dist/root-impl.d.ts.map +1 -1
  178. package/dist/root-impl.js +40 -36
  179. package/dist/root-options.d.ts +12 -1
  180. package/dist/root-options.d.ts.map +1 -1
  181. package/dist/root-path-existing.d.ts.map +1 -1
  182. package/dist/root-path-existing.js +4 -3
  183. package/dist/root-path-symlink.d.ts.map +1 -1
  184. package/dist/root-path-symlink.js +3 -2
  185. package/dist/root-paths.d.ts.map +1 -1
  186. package/dist/root-paths.js +13 -9
  187. package/dist/root-remove.d.ts +5 -0
  188. package/dist/root-remove.d.ts.map +1 -0
  189. package/dist/root-remove.js +286 -0
  190. package/dist/root-symlink-policy.d.ts +2 -1
  191. package/dist/root-symlink-policy.d.ts.map +1 -1
  192. package/dist/root-symlink-policy.js +2 -2
  193. package/dist/root-write-mode.d.ts.map +1 -1
  194. package/dist/root-write-mode.js +2 -1
  195. package/dist/root.d.ts +2 -1
  196. package/dist/root.d.ts.map +1 -1
  197. package/dist/secret-file.d.ts.map +1 -1
  198. package/dist/secret-file.js +2 -1
  199. package/dist/secret-read-async.d.ts.map +1 -1
  200. package/dist/secret-read-async.js +2 -1
  201. package/dist/secure-file.d.ts.map +1 -1
  202. package/dist/secure-file.js +21 -4
  203. package/dist/secure-temp-dir.d.ts.map +1 -1
  204. package/dist/secure-temp-dir.js +2 -1
  205. package/dist/sibling-staged-file.d.ts +1 -0
  206. package/dist/sibling-staged-file.d.ts.map +1 -1
  207. package/dist/sibling-staged-file.js +42 -8
  208. package/dist/sibling-temp.d.ts +2 -0
  209. package/dist/sibling-temp.d.ts.map +1 -1
  210. package/dist/sibling-temp.js +6 -4
  211. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  212. package/dist/sidecar-lock-acquire.js +15 -4
  213. package/dist/sidecar-lock-policy.d.ts +2 -0
  214. package/dist/sidecar-lock-policy.d.ts.map +1 -1
  215. package/dist/sidecar-lock-policy.js +17 -0
  216. package/dist/staged-directory.d.ts.map +1 -1
  217. package/dist/staged-directory.js +4 -3
  218. package/dist/temp-target.d.ts +14 -12
  219. package/dist/temp-target.d.ts.map +1 -1
  220. package/dist/temp-target.js +12 -6
  221. package/dist/trash.d.ts.map +1 -1
  222. package/dist/trash.js +7 -5
  223. package/dist/unicode-path.d.ts +3 -0
  224. package/dist/unicode-path.d.ts.map +1 -0
  225. package/dist/unicode-path.js +13 -0
  226. package/dist/walk.d.ts.map +1 -1
  227. package/dist/walk.js +3 -2
  228. package/dist/write-file-handle.d.ts +1 -0
  229. package/dist/write-file-handle.d.ts.map +1 -1
  230. package/dist/write-file-handle.js +3 -2
  231. package/docs/advanced.md +4 -0
  232. package/docs/archive.md +25 -5
  233. package/docs/atomic.md +17 -1
  234. package/docs/config.md +1 -0
  235. package/docs/contributing.md +29 -1
  236. package/docs/copy.md +75 -6
  237. package/docs/directory-identity.md +85 -0
  238. package/docs/durability.md +36 -1
  239. package/docs/entries.md +109 -0
  240. package/docs/errors.md +3 -3
  241. package/docs/file-store.md +15 -0
  242. package/docs/guest.md +141 -0
  243. package/docs/in-place-write.md +81 -0
  244. package/docs/index.md +2 -0
  245. package/docs/install.md +31 -0
  246. package/docs/native-helper.md +10 -3
  247. package/docs/native.md +4 -0
  248. package/docs/output.md +32 -6
  249. package/docs/path-case.md +64 -0
  250. package/docs/path-scope.md +1 -1
  251. package/docs/permissions.md +11 -2
  252. package/docs/public-api.md +31 -2
  253. package/docs/root.md +30 -2
  254. package/docs/secure-file.md +2 -0
  255. package/docs/sidecar-lock.md +12 -3
  256. package/docs/temp.md +35 -6
  257. package/docs/types.md +1 -1
  258. package/docs/writing.md +153 -3
  259. package/package.json +14 -8
package/CHANGELOG.md CHANGED
@@ -2,6 +2,62 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.11.0 - 2026-09-14
6
+
7
+ ### Highlights
8
+
9
+ - **Faster archive-heavy workloads:** reduce repeated ZIP admission, batch native ZIP metadata reads, and reuse TAR parser input and admitted payload ranges. Archive extraction and inspection also benefit from larger reusable staging and transfer buffers.
10
+ - **More capable filesystem roots:** iterate one directory with `Root.entries()`, remove trees with bounded or explicitly unlimited `Root.remove()`, and create complete files from async byte streams with `Root.create()`.
11
+ - **Reuse handles for copying, overwriting, and hashing:** add `copyFileHandle()`, `overwriteFileHandle()`, and `sha256FileSync()` while preserving caller-owned descriptors and file positions.
12
+ - **Faster Windows permissions and copying:** inspect inherited ACLs without PowerShell startup when the native binding is available, reduce copy allocations, and preserve large zero-filled ranges in empty destination files.
13
+ - **Safer publication and private files:** tighten hardlink, inode, permission, archive-destination, and lock validation; add private producer workspaces for callback writes and fix staging of long Unicode filenames.
14
+
15
+ ### Compatibility and upgrade notes
16
+
17
+ - `readSecureFile()` now rejects hardlinked inputs and rechecks the opened file after reading. Synchronous store publication rejects substituted or unverifiable file identities, including opaque Windows identities.
18
+ - Invalid lock stale thresholds and compromise-check intervals now reject before acquisition. Asynchronous compromise checks no longer overlap or turn overflowing timer values into rapid polling loops.
19
+ - Inherited atomic-replacement modes include only ordinary rwx bits from an existing non-symlink regular file. JavaScript raw sidecars are created with mode `0o600`, so a permissive umask cannot expose their payloads.
20
+ - Bun gains additional POSIX path support through the existing Rust binding, including with JIT disabled. Native-off policy remains explicit; addon-free limitations are documented. Node.js remains supported from version 22.
21
+
22
+ ### Directory and file workflows
23
+
24
+ - Add guarded, nonrecursive `Root.entries()` with child-symlink reporting, cancellation, entry limits, and bounded sorted-name collection. Traversal and link policy remain with the caller.
25
+ - Extend `Root.remove()` with recursive traversal, entry/depth budgets, sorted or filesystem order, cancellation, and missing-target handling. Callers may explicitly choose unlimited recursive-removal budgets; exact directory/leaf identity checks remain in place, and `force` continues sibling cleanup when a child directory disappears.
26
+ - Add a `Root.create()` overload for async byte iterables. Input consumption is bounded, cancellation settles pending work, and completed contents are published exclusively through the existing guarded writer.
27
+ - Add `copyFileHandle()` for bounded regular-file transfers and `overwriteFileHandle()` for in-place replacement with prefix-only rollback preparation, growth-before-overwrite ordering, and a once-only synchronous pre-write admission callback. Both preserve caller ownership and cursors.
28
+ - Add `sha256FileSync()` for bounded hashing of pathnames and borrowed descriptors, with exact pathname admission and no native-binding requirement.
29
+ - Add exact directory-identity observations and synchronous assertions for caller-owned staging and recovery, retaining bigint precision, optional canonical-path checks, and final-symlink rejection even with trailing separators.
30
+ - Add `probePathCaseInsensitiveSync()` for local ASCII-case observations, with a read-only option, owned temporary probes, and an explicit unknown result instead of operating-system guesses.
31
+ - Add opt-in `producerIsolation: "private-directory"` to sibling callback writes and external sibling outputs. Cleanup ownership is established before the producer runs, allowing partial failures to be cleaned without changing existing defaults or publication checks.
32
+
33
+ ### Archive and path performance
34
+
35
+ - Avoid repeating physical ZIP admission during JavaScript member reads. Reduce filename-validation allocations across both backends, and group native ZIP metadata reads into bounded buffers while retaining complete record, CRC, limit, and cancellation checks.
36
+ - Copy each JavaScript TAR WASM input window once across member events. Read selected plain-TAR payloads directly from the fully admitted private snapshot instead of parsing it a second time.
37
+ - Batch private archive-input staging through reusable buffers capped at 512 KiB. Match file-backed JavaScript gzip output to the 64 KiB WASM input window for faster extraction and TAR inspection; retain short-I/O handling, framing validation, bounded growth probes, and settled cancellation.
38
+ - Reduce repeated POSIX path-scoping and Root work, reuse checked store keys, and avoid redundant name normalization. Root exclusion, canonical spelling, Unicode byte limits, collision rules, home expansion, and live filesystem identity checks are preserved.
39
+ - Complete short gzip-header reads before classifying staged archives, preventing valid gzip files from being mistaken for TAR.
40
+ - Fit callback staging names using their original UTF-8 length as well as NFC/NFD lengths. Valid destination names whose normalization is shorter no longer fail with `ENAMETOOLONG` when a staging prefix is added; the final target spelling is preserved.
41
+
42
+ ### Copy performance and filesystem fidelity
43
+
44
+ - Batch large borrowed-handle and JavaScript Root transfers through reusable 512 KiB buffers, retaining byte limits, cancellation, and per-write authority checks. Small byte budgets also bound scratch allocation and the overflow probe.
45
+ - Batch atomic copy-fallback restore snapshots and synchronous source reads into reusable buffers, reducing reads and full-buffer copies while preserving restore budgets, short reads, identity checks, original modes, and rollback behavior.
46
+ - Share portable directory-copy workers across sibling directories, with bounded deferred completion, joined cancellation, and bottom-up timestamp restoration. Preserve fractional timestamps, including pre-1970 dates on Unix, to the precision supported by Node and the destination filesystem.
47
+ - Add Linux ZFS directory cloning through the bounded reflink traversal, with independent destinations, metadata preservation, and no byte-copy fallback when cloning is required. XFS clones now preserve user extended attributes on read-only files and directories, along with their modes and ACLs.
48
+ - Preserve large zero-filled ranges without allocating them during native Linux automatic directory byte copies and native Windows byte copies into empty files. Exact lengths, contents, cancellation settlement, and existing-target overwrite behavior remain intact.
49
+ - Confirm EOF when Linux copy offload initially reports zero bytes, so automatic copying reads available data instead of publishing an empty file.
50
+ - Size Windows native copy buffers to small inputs, reuse directory-enumeration buffers, and start ReFS workers as file jobs arrive. Report disk-full and sharing failures as `ENOSPC` and `EBUSY`; worker-start failures join admitted workers before returning instead of panicking across the native boundary.
51
+
52
+ ### Permissions, compatibility, and tooling
53
+
54
+ - Inspect complete inherited local Windows ACLs through the native descriptor reader when available, preserving SID classification, explicit test injections, .NET normalization, and structured fallback diagnostics. Classify canonical SID facts directly without a redundant account lookup, retaining fail-closed behavior when discovery is unavailable.
55
+ - Retain the admitted archive destination's exact bigint identity through publication, rejecting replacements introduced by entry filters or concurrent actors before files can be published into them.
56
+ - Support Bun POSIX resolution for restrictive permissions, literal backslashes, sockets, and symlink/parent traversal. Preserve raw path components in absolute recursive-mkdir inputs to fix relative publication and queue writes on Bun for Windows.
57
+ - Export the caller-launched Python guest filesystem program and shared no-replace rename fragment through `@openclaw/fs-safe/guest`. The Linux/macOS protocol validates basenames before filesystem operations and uses short independent staging names for long-basename writes and cross-device moves.
58
+ - Support standalone `@pnpm/exe` in consumer smoke and lifecycle tests while retaining the declared pnpm version and isolated consumer configuration. Strengthen exact-inode durability checks and batch independent native ACL test observations.
59
+ - Expand method benchmarks to 557 representative workloads across Linux, macOS, and Windows, with native/JavaScript modes, equal-concurrency copy comparisons, large collections, contention, and explicit platform exclusions. Verify returned data and digests, initialize private Windows fixture ACLs, apply synchronous iteration reductions consistently, and handle expected synchronous rejections during timed calls as well as warmup.
60
+
5
61
  ## 0.10.0 - 2026-09-13
6
62
 
7
63
  ### Highlights
package/LICENSE CHANGED
@@ -1,6 +1,7 @@
1
1
  MIT License
2
2
 
3
3
  Copyright (c) 2026 openclaw
4
+ Copyright (c) 2026 OpenClaw Foundation (guest filesystem Python source)
4
5
 
5
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
7
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -59,6 +59,8 @@ pnpm add @openclaw/fs-safe
59
59
 
60
60
  Node 22 or newer. Core root/path/json/temp helpers avoid framework dependencies. With all optional dependencies omitted, public subpaths remain safe to import and non-archive fallback-capable operations work in `auto` or `off`. Native-only features remain unavailable, and operations needing the binding in `require` mode fail with `helper-unavailable`. TAR/gzip fallback uses the bundled WASM build of the same Rust parser as native and works with optional dependencies omitted. ZIP fallback still needs optional `jszip`. See the [0.6 migration guide](docs/migrating-to-0.6.md).
61
61
 
62
+ Bun 1.4.2 is also supported with the [Bun runtime requirements](docs/install.md#bun-runtime), including the matching Rust addon on macOS and Linux. JIT-disabled Bun works too.
63
+
62
64
  The package installs one prebuilt native binding for the current supported target. It
63
65
  supplies fd-relative and atomic no-replace primitives that Node does not expose
64
66
  directly. Configure the lazy loader before first use when you need a strict
@@ -131,6 +133,11 @@ await fs.move("notes/today.txt", "notes/archive/today.txt", { overwrite: true })
131
133
  await fs.remove("notes/archive/today.txt");
132
134
  ```
133
135
 
136
+ Use `remove(path, { recursive: true, maxEntries: 20_000, maxDepth: 32 })` for
137
+ bounded tree cleanup. It streams directory entries, checks mutation authority
138
+ before every removal, and supports cancellation. See [removal options and partial
139
+ failure semantics](docs/writing.md).
140
+
134
141
  `root()` takes the trusted directory; relative paths in subsequent calls are resolved against it. Defaults you pass to `root()` apply to every call below; per-call options override them.
135
142
 
136
143
  `copyIn()` also accepts `{ root: sourceRoot, relativePath }`, `overwrite: false`,
@@ -151,6 +158,12 @@ const opened = await fs.open("notes/today.txt");
151
158
  await fs.create("notes/README.md", "seed\n"); // throws if it already exists
152
159
  ```
153
160
 
161
+ `create()` also accepts an `AsyncIterable<Uint8Array>` for large or incrementally
162
+ produced files. Streamed creates keep the destination absent until all chunks
163
+ are written, support `maxBytes` and `signal`, and recheck mutation authority
164
+ before writes and publication. See [streamed creation](docs/writing.md#streamed-creation)
165
+ for producer ownership and cancellation semantics.
166
+
154
167
  `write()` replaces file contents by default; pass `{ overwrite: false }` or use `create()` when an existing file should be an error. `move()` defaults to no clobber because it can otherwise delete an unrelated target while also consuming the source. Pass `{ overwrite: true }` when replacing the target is intended.
155
168
 
156
169
  Mutating methods accept `assertBeforeMutation: () => void` for live lease or
@@ -220,7 +233,7 @@ const locked = await root("/srv/workspace", {
220
233
  await locked.write(".env", "token"); // FsSafeError code "denied-path"
221
234
  ```
222
235
 
223
- `stat()`, `exists()`, and `list()` are boundary-checked, but they cannot pin a later operation to the same filesystem object. Use `read()`, `open()`, `write()`, `create()`, `copyIn()`, `move()`, or `remove()` for operation-local identity checks, and inspect `containment` when the platform distinction matters.
236
+ `stat()`, `exists()`, `list()`, and `entries()` are boundary-checked, but they cannot pin a later operation to the same filesystem object. Use `read()`, `open()`, `write()`, `create()`, `copyIn()`, `move()`, or `remove()` for operation-local identity checks, and inspect `containment` when the platform distinction matters.
224
237
 
225
238
  ## Subpaths
226
239
 
@@ -239,18 +252,19 @@ contract. Low-level helpers that OpenClaw needs to compose higher-level APIs are
239
252
  | `@openclaw/fs-safe/store` | `fileStore`, `fileStoreSync`, and `jsonStore` |
240
253
  | `@openclaw/fs-safe/secret` | sync/async strict and try-style secret reads, atomic replace, and create-only secret writes |
241
254
  | `@openclaw/fs-safe/atomic` | `replaceFileAtomic`, `replaceFileAtomicSync`, `replaceDirectoryAtomic`, `movePathWithCopyFallback` |
242
- | `@openclaw/fs-safe/durability` | pinned directory identities, strict directory sync, durable nested-directory creation, exclusive publication, streaming SHA-256, provenance receipts, and sync-failure policy |
255
+ | `@openclaw/fs-safe/durability` | pinned directory identities, strict directory sync, durable nested-directory creation, exclusive publication, streaming and synchronous SHA-256, provenance receipts, and sync-failure policy |
243
256
  | `@openclaw/fs-safe/temp` | `tempWorkspace`, `tempWorkspaceSync`, `withTempWorkspace`, `resolveSecureTempRoot` |
244
257
  | `@openclaw/fs-safe/secure-file` | fd-pinned absolute file reads with owner, mode, ACL, trusted-dir, size, and timeout checks |
245
258
  | `@openclaw/fs-safe/file-lock` | async/sync sidecar locks, root-bounded sidecars, ownership verification, and stale policy |
246
259
  | `@openclaw/fs-safe/permissions` | POSIX mode and Windows ACL inspection, raw owner/ACE facts, private-directory creation, and remediation helpers |
247
260
  | `@openclaw/fs-safe/walk` | budget-bounded directory walking with symlink policy, filters, and truncation accounting; not root-bounded |
248
- | `@openclaw/fs-safe/copy` | directory copying with `clone: "auto"`, `"always"`, or `"never"`; native APFS, Btrfs, ReFS, and XFS cloning, portable byte copying, and clone metadata; see [directory copying](docs/copy.md) |
261
+ | `@openclaw/fs-safe/copy` | directory copying with `clone: "auto"`, `"always"`, or `"never"`; native APFS, Btrfs, ReFS, XFS, and ZFS cloning, portable byte copying, and clone metadata; see [directory copying](docs/copy.md) |
249
262
  | `@openclaw/fs-safe/archive` | policy-driven ZIP/TAR extraction, clamp/filter policy, metadata/path-depth limits, native gzip/zstd/bzip2, and bounded entry reads |
250
- | `@openclaw/fs-safe/advanced` | lower-level composition helpers such as path scopes, root-file open, bounded descriptor reads, install paths, filename sanitizing, temp-file targets, sibling-temp writes, local-root readers, regular-file helpers, `pathExists`, and `withTimeout`; less stable than focused public subpaths |
263
+ | `@openclaw/fs-safe/advanced` | lower-level composition helpers such as path scopes, root-file open, bounded descriptor reads, [borrowed-handle copying](docs/copy.md#borrowed-filehandle-transfers), [exact directory identity](docs/directory-identity.md), [case probing](docs/path-case.md), [in-place writes](docs/in-place-write.md), install paths, filename sanitizing, temp-file targets, sibling-temp writes, local-root readers, regular-file helpers, `pathExists`, and `withTimeout`; less stable than focused public subpaths |
251
264
  | `@openclaw/fs-safe/errors` | `FsSafeError`, closed codes/categories, causes, and operation-specific details receipts |
252
265
  | `@openclaw/fs-safe/types` | shared types: `DirEntry`, `PathStat`, … |
253
266
  | `@openclaw/fs-safe/test-hooks` | hooks the test suite uses to inject races; registration requires `NODE_ENV=test` or `VITEST=true` |
267
+ | `@openclaw/fs-safe/guest` | Python source and exit constants for caller-launched filesystem operations in Linux/macOS guests without Node; see the [guest protocol and trust boundary](docs/guest.md) |
254
268
 
255
269
  ## Failure semantics in the name
256
270
 
@@ -311,7 +325,7 @@ original directory on Linux/macOS and requires native support for this operation
311
325
  It offers atomic replace/no-replace publication, not expected-inode replacement
312
326
  or a crash-durability promise; application checks and coordination remain yours.
313
327
 
314
- `replaceFileAtomic()` writes a sibling temp file, applies its exact mode through the still-open descriptor, optionally fsyncs it, and renames it over the destination. It never follows the published destination path to set file permissions. Mode preservation, pinned-destination hardlink rejection, rename retry / copy fallback on `EPERM`, bounded original-content restoration after a torn fallback, parent-directory fsync, and a `beforeRename` hook for backup or observer flows are all opt-in. `movePathWithCopyFallback()` stages cross-device moves before commit and removes only the copied source entries, so concurrent source additions or replacements are preserved. Its optional synchronous `assertBeforeMutation` hook rechecks caller authority before renames and each source removal; `onDestinationPublished` reports an exact bigint destination identity before later checks or cleanup can fail. See [mutation authority and publication receipts](docs/atomic.md#mutation-authority-and-publication-receipts).
328
+ `replaceFileAtomic()` writes a sibling temp file, applies its exact mode through the still-open descriptor, optionally fsyncs it, and renames it over the destination. It never follows the published destination path to set file permissions. Mode preservation inherits only rwx bits from an existing non-symlink regular file; special bits, ownership, ACLs, and extended attributes are not copied. Pinned-destination hardlink rejection, rename retry / copy fallback on `EPERM`, bounded original-content restoration after a torn fallback, parent-directory fsync, and a `beforeRename` hook for backup or observer flows are all opt-in. `movePathWithCopyFallback()` stages cross-device moves before commit and removes only the copied source entries, so concurrent source additions or replacements are preserved. Its optional synchronous `assertBeforeMutation` hook rechecks caller authority before renames and each source removal; `onDestinationPublished` reports an exact bigint destination identity before later checks or cleanup can fail. See [mutation authority and publication receipts](docs/atomic.md#mutation-authority-and-publication-receipts).
315
329
 
316
330
  ```ts
317
331
  import { replaceFileAtomic } from "@openclaw/fs-safe/atomic";
@@ -352,6 +366,12 @@ fsyncs the completed file, and atomically renames it over the target. Choose it
352
366
  when the destination directory is itself the writable boundary and atomic
353
367
  replacement matters.
354
368
 
369
+ For sibling producers that can leave partial output before throwing, opt in to
370
+ `producerIsolation: "private-directory"`. The callback writes inside an owned
371
+ private workspace on the target filesystem, allowing cleanup after producer
372
+ failure while preserving sibling publication behavior. See [external outputs](docs/output.md)
373
+ for the identity checks and cleanup limits.
374
+
355
375
  Use it when the final filename is known before the external writer runs. If the
356
376
  filename depends on sniffing the produced bytes, write to a private temp
357
377
  workspace first, then finalize through the normal root APIs after validation.
@@ -458,6 +478,17 @@ flows where a warning is preferable to refusing the file.
458
478
 
459
479
  ## Directory walking
460
480
 
481
+ [`Root.entries()`](docs/entries.md) observes one directory without descending or
482
+ following child symlinks. It streams in filesystem order by default and supports
483
+ cancellation, entry limits that throw on overflow, and bounded sorted-name
484
+ collection. Use it when the caller owns traversal or symlink validation:
485
+
486
+ ```ts
487
+ for await (const entry of fs.entries("plugins", { maxEntries: 1_000 })) {
488
+ console.log(entry.name, entry.isSymbolicLink);
489
+ }
490
+ ```
491
+
461
492
  `walkDirectory()` and `walkDirectorySync()` replace ad-hoc recursive
462
493
  `readdir()` loops with entry and depth budgets, a symlink policy, and stable
463
494
  relative paths.
@@ -1 +1 @@
1
- {"version":3,"file":"absolute-path.d.ts","sourceRoot":"","sources":["../src/absolute-path.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,WAAW,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAIhE,MAAM,MAAM,yBAAyB,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAU5D,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG,oBAAoB,GAAG;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,8BAA8B,GAAG;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,6BAA6B,GACrC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC1B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,eAAe,CAAC;IAAC,KAAK,EAAE,WAAW,CAAA;CAAE,CAAC;AAmL7D,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAWhE;AAED,wBAAsB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAEnF;AAuBD,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,8BAAmC,GAC3C,OAAO,CAAC,6BAA6B,CAAC,CA+ExC;AAED,wBAAsB,iCAAiC,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAazF;AAED,wBAAsB,0BAA0B,CAC9C,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,yBAAyB,CAAA;CAAO,GACrD,OAAO,CAAC,oBAAoB,CAAC,CAgB/B;AAED,wBAAsB,2BAA2B,CAC/C,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,yBAAyB,CAAA;CAAO,GACrD,OAAO,CAAC,4BAA4B,CAAC,CA2BvC"}
1
+ {"version":3,"file":"absolute-path.d.ts","sourceRoot":"","sources":["../src/absolute-path.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,WAAW,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAKhE,MAAM,MAAM,yBAAyB,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAU5D,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG,oBAAoB,GAAG;IAChE,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,8BAA8B,GAAG;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,6BAA6B,GACrC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC1B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,eAAe,CAAC;IAAC,KAAK,EAAE,WAAW,CAAA;CAAE,CAAC;AAmL7D,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAWhE;AAED,wBAAsB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAEnF;AAuBD,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,8BAAmC,GAC3C,OAAO,CAAC,6BAA6B,CAAC,CA+ExC;AAED,wBAAsB,iCAAiC,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAazF;AAED,wBAAsB,0BAA0B,CAC9C,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,yBAAyB,CAAA;CAAO,GACrD,OAAO,CAAC,oBAAoB,CAAC,CAgB/B;AAED,wBAAsB,2BAA2B,CAC/C,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,yBAAyB,CAAA;CAAO,GACrD,OAAO,CAAC,4BAA4B,CAAC,CA2BvC"}
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, } from "./directory-guard.js";
5
5
  import { FsSafeError } from "./errors.js";
6
6
  import { pathExists } from "./fs.js";
7
+ import { realpathSync } from "./realpath.js";
7
8
  import { resolveRootPath } from "./root-path.js";
8
9
  function resolveSymlinkPolicy(policy) {
9
10
  if (policy === undefined)
@@ -251,7 +252,7 @@ export async function canonicalPathFromExistingAncestor(filePath) {
251
252
  }
252
253
  let canonicalAncestor = ancestor;
253
254
  try {
254
- canonicalAncestor = fsSync.realpathSync.native(ancestor);
255
+ canonicalAncestor = realpathSync.native(ancestor);
255
256
  }
256
257
  catch {
257
258
  // Keep lexical path when the existing ancestor cannot be canonicalized.
@@ -264,7 +265,7 @@ export async function resolveAbsolutePathForRead(filePath, options = {}) {
264
265
  const normalized = assertAbsolutePathInput(filePath);
265
266
  let canonicalPath;
266
267
  try {
267
- canonicalPath = fsSync.realpathSync.native(normalized);
268
+ canonicalPath = realpathSync.native(normalized);
268
269
  }
269
270
  catch (err) {
270
271
  if (err.code === "ENOENT") {
@@ -1,4 +1,8 @@
1
1
  export { createAsyncLock } from "./async-lock.js";
2
+ export { copyFileHandle, type CopyFileHandleOptions } from "./file-handle-transfer.js";
3
+ export { overwriteFileHandle, type OverwriteFileHandleOptions } from "./overwrite-file-handle.js";
4
+ export { probePathCaseInsensitiveSync, type ProbePathCaseOptions } from "./path-case.js";
5
+ export { readDirectoryIdentity, assertDirectoryIdentitySync, type DirectoryIdentity, } from "./directory-guard.js";
2
6
  export { stageFileInDirectory, type StagedFile, type StagedFileReceipt, type PublishedFileReceipt, type StagedFilePublication, type StagedFileCleanupReceipt, type StagedFileFailureDetails, } from "./native-staged-file.js";
3
7
  export { readFileDescriptorBounded, readFileDescriptorBoundedSync, readFileHandleBounded, } from "./bounded-read.js";
4
8
  export { readFileWindowFully, readFileWindowFullySync, type ReadFileWindowOptions, } from "./positional-read.js";
@@ -1 +1 @@
1
- {"version":3,"file":"advanced.d.ts","sourceRoot":"","sources":["../src/advanced.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"advanced.d.ts","sourceRoot":"","sources":["../src/advanced.ts"],"names":[],"mappings":"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"}
package/dist/advanced.js CHANGED
@@ -2,6 +2,10 @@
2
2
  // public subpaths; prefer root/json/store/temp/archive unless you are building a
3
3
  // higher-level primitive.
4
4
  export { createAsyncLock } from "./async-lock.js";
5
+ export { copyFileHandle } from "./file-handle-transfer.js";
6
+ export { overwriteFileHandle } from "./overwrite-file-handle.js";
7
+ export { probePathCaseInsensitiveSync } from "./path-case.js";
8
+ export { readDirectoryIdentity, assertDirectoryIdentitySync, } from "./directory-guard.js";
5
9
  export { stageFileInDirectory, } from "./native-staged-file.js";
6
10
  export { readFileDescriptorBounded, readFileDescriptorBoundedSync, readFileHandleBounded, } from "./bounded-read.js";
7
11
  export { readFileWindowFully, readFileWindowFullySync, } from "./positional-read.js";
@@ -1,21 +1,21 @@
1
1
  import { type ExtractionDeadline } from "./archive-deadline.js";
2
- import type { AsyncDirectoryGuard } from "./directory-guard.js";
2
+ import { type ArchiveDirectoryGuard } from "./archive-staging.js";
3
3
  import type { PublishedWriteIdentity } from "./pinned-write.js";
4
4
  import type { Root } from "./root.js";
5
5
  export type ArchivePublishedFile = {
6
6
  relativePath: string;
7
7
  identity: PublishedWriteIdentity;
8
- guards: readonly AsyncDirectoryGuard[];
8
+ guards: readonly ArchiveDirectoryGuard[];
9
9
  };
10
10
  export type ArchivePublishedDirectory = {
11
- guard: AsyncDirectoryGuard;
12
- parents: readonly AsyncDirectoryGuard[];
11
+ guard: ArchiveDirectoryGuard;
12
+ parents: readonly ArchiveDirectoryGuard[];
13
13
  mode: number;
14
14
  };
15
15
  export declare function finalizeArchivePublication(params: {
16
16
  targetRoot: Root;
17
- destinationGuard: AsyncDirectoryGuard;
18
- sourceGuard: AsyncDirectoryGuard;
17
+ destinationGuard: ArchiveDirectoryGuard;
18
+ sourceGuard: ArchiveDirectoryGuard;
19
19
  files: readonly ArchivePublishedFile[];
20
20
  directories: readonly ArchivePublishedDirectory[];
21
21
  durable: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"archive-durability.d.ts","sourceRoot":"","sources":["../src/archive-durability.ts"],"names":[],"mappings":"AAEA,OAAO,EAAoC,KAAK,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAElG,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAIhE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAChE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAMtC,MAAM,MAAM,oBAAoB,GAAG;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,sBAAsB,CAAC;IACjC,MAAM,EAAE,SAAS,mBAAmB,EAAE,CAAC;CACxC,CAAC;AACF,MAAM,MAAM,yBAAyB,GAAG;IACtC,KAAK,EAAE,mBAAmB,CAAC;IAC3B,OAAO,EAAE,SAAS,mBAAmB,EAAE,CAAC;IACxC,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,wBAAsB,0BAA0B,CAAC,MAAM,EAAE;IACvD,UAAU,EAAE,IAAI,CAAC;IACjB,gBAAgB,EAAE,mBAAmB,CAAC;IACtC,WAAW,EAAE,mBAAmB,CAAC;IACjC,KAAK,EAAE,SAAS,oBAAoB,EAAE,CAAC;IACvC,WAAW,EAAE,SAAS,yBAAyB,EAAE,CAAC;IAClD,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,kBAAkB,CAAC;CAC/B,GAAG,OAAO,CAAC,IAAI,CAAC,CA4FhB"}
1
+ {"version":3,"file":"archive-durability.d.ts","sourceRoot":"","sources":["../src/archive-durability.ts"],"names":[],"mappings":"AAEA,OAAO,EAAoC,KAAK,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAClG,OAAO,EAIL,KAAK,qBAAqB,EAC3B,MAAM,sBAAsB,CAAC;AAI9B,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAChE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAMtC,MAAM,MAAM,oBAAoB,GAAG;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,sBAAsB,CAAC;IACjC,MAAM,EAAE,SAAS,qBAAqB,EAAE,CAAC;CAC1C,CAAC;AACF,MAAM,MAAM,yBAAyB,GAAG;IACtC,KAAK,EAAE,qBAAqB,CAAC;IAC7B,OAAO,EAAE,SAAS,qBAAqB,EAAE,CAAC;IAC1C,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,wBAAsB,0BAA0B,CAAC,MAAM,EAAE;IACvD,UAAU,EAAE,IAAI,CAAC;IACjB,gBAAgB,EAAE,qBAAqB,CAAC;IACxC,WAAW,EAAE,qBAAqB,CAAC;IACnC,KAAK,EAAE,SAAS,oBAAoB,EAAE,CAAC;IACvC,WAAW,EAAE,SAAS,yBAAyB,EAAE,CAAC;IAClD,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,kBAAkB,CAAC;CAC/B,GAAG,OAAO,CAAC,IAAI,CAAC,CA4FhB"}
@@ -53,7 +53,7 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
53
53
  import fsSync from "node:fs";
54
54
  import path from "node:path";
55
55
  import { ownExtractionDestinationMutation } from "./archive-deadline.js";
56
- import { assertDirectoryIdentityGuard, assertResolvedInsideDestination, createArchiveSymlinkTraversalError } from "./archive-staging.js";
56
+ import { assertDirectoryIdentityGuard, assertResolvedInsideDestination, createArchiveSymlinkTraversalError, } from "./archive-staging.js";
57
57
  import { pinNodeDirectoryForMode } from "./directory-mode-node.js";
58
58
  import { pinDirectory, syncDirectory } from "./directory-durability.js";
59
59
  import { syncFileBestEffort } from "./file-sync.js";
@@ -1 +1 @@
1
- {"version":3,"file":"archive-entry.d.ts","sourceRoot":"","sources":["../src/archive-entry.ts"],"names":[],"mappings":"AAMA,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAIzD;AAED,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAChC,IAAI,CAoEN;AAGD,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAc1F;AAED,wBAAgB,8BAA8B,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,IAAI,CAgBlG;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,MAAM,CAWT"}
1
+ {"version":3,"file":"archive-entry.d.ts","sourceRoot":"","sources":["../src/archive-entry.ts"],"names":[],"mappings":"AAOA,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAChC,IAAI,CA+DN;AAGD,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAc1F;AAED,wBAAgB,8BAA8B,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,IAAI,CAgBlG;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,MAAM,CAWT"}
@@ -3,10 +3,9 @@ import { ArchiveSecurityError } from "./archive-errors.js";
3
3
  import { isWindowsReservedDeviceName } from "./device-path.js";
4
4
  import { formatErrorDetail } from "./error-detail.js";
5
5
  import { resolveSafeBaseDir } from "./path.js";
6
+ import { lowerCaseNfc, maxNormalizedUtf8Bytes } from "./unicode-path.js";
6
7
  export function isWindowsDrivePath(value) {
7
- return normalizeArchiveEntryPath(value)
8
- .split("/")
9
- .some((segment) => /^[a-zA-Z]:/.test(segment));
8
+ return /(?:^|\/)[a-zA-Z]:/.test(normalizeArchiveEntryPath(value));
10
9
  }
11
10
  export function normalizeArchiveEntryPath(raw) {
12
11
  return raw.replaceAll("\\", "/");
@@ -31,7 +30,7 @@ export function validateArchiveEntryPath(entryPath, params) {
31
30
  throw new ArchiveSecurityError("entry-path", `archive entry uses a reserved device path: ${formatErrorDetail(entryPath)}`);
32
31
  }
33
32
  const normalized = path.posix.normalize(slashNormalized);
34
- if (normalized.split("/").some((segment) => Math.max(Buffer.byteLength(segment.normalize("NFC")), Buffer.byteLength(segment.normalize("NFD"))) > 255)) {
33
+ if (normalized.split("/").some((segment) => maxNormalizedUtf8Bytes(segment) > 255)) {
35
34
  throw new ArchiveSecurityError("entry-path", `archive entry has an overlong path component: ${formatErrorDetail(entryPath)}`);
36
35
  }
37
36
  const escapeLabel = params?.escapeLabel ?? "destination";
@@ -67,7 +66,7 @@ export function createArchiveOutputPathTracker() {
67
66
  // Archive policy must not depend on the destination volume's case or
68
67
  // Unicode-normalization behavior. Otherwise the JavaScript and native
69
68
  // writers can disagree about which of two colliding entries wins.
70
- const collisionKey = normalized.normalize("NFC").toLowerCase().normalize("NFC");
69
+ const collisionKey = lowerCaseNfc(normalized);
71
70
  if (seen.has(collisionKey)) {
72
71
  throw new ArchiveSecurityError("entry-path", `archive entries collide at output path ${formatErrorDetail(normalized)}: ${formatErrorDetail(originalPath)}`);
73
72
  }
@@ -1,5 +1,6 @@
1
1
  import { Writable } from "node:stream";
2
2
  import type { Gunzip } from "node:zlib";
3
+ export declare function isGzipBuffer(input: Uint8Array): boolean;
3
4
  /** Validate the same physical suffix when admission owns an in-memory input. */
4
5
  export declare function validateGzipBufferTail(input: Buffer, consumed: number, signal?: AbortSignal): Promise<void>;
5
6
  /** Track physical input, not the sum of bytes consumed across separate writes:
@@ -1 +1 @@
1
- {"version":3,"file":"archive-gzip-tail.d.ts","sourceRoot":"","sources":["../src/archive-gzip-tail.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AASxC,gFAAgF;AAChF,wBAAsB,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAWjH;AAED;8EAC8E;AAC9E,qBAAa,SAAU,SAAQ,QAAQ;IAIzB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAHpC,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,WAAW,CAAqB;IAExC,YAA6B,OAAO,EAAE,MAAM,EAE3C;IAED,IAAI,UAAU,IAAI,MAAM,CAEvB;IAEQ,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,QAAQ,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI,GAAG,IAAI,CAcxG;IAEQ,MAAM,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI,GAAG,IAAI,CAE9D;IAEQ,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI,GAAG,IAAI,CAGpF;CACF;AAED;6EAC6E;AAC7E,wBAAsB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBvH"}
1
+ {"version":3,"file":"archive-gzip-tail.d.ts","sourceRoot":"","sources":["../src/archive-gzip-tail.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAGxC,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAEvD;AAQD,gFAAgF;AAChF,wBAAsB,sBAAsB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAWjH;AAED;8EAC8E;AAC9E,qBAAa,SAAU,SAAQ,QAAQ;IAIzB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAHpC,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,WAAW,CAAqB;IAExC,YAA6B,OAAO,EAAE,MAAM,EAE3C;IAED,IAAI,UAAU,IAAI,MAAM,CAEvB;IAEQ,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,QAAQ,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI,GAAG,IAAI,CAcxG;IAEQ,MAAM,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI,GAAG,IAAI,CAE9D;IAEQ,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI,GAAG,IAAI,CAGpF;CACF;AAED;6EAC6E;AAC7E,wBAAsB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBvH"}
@@ -2,6 +2,9 @@ import fsSync from "node:fs";
2
2
  import fs from "node:fs/promises";
3
3
  import { Writable } from "node:stream";
4
4
  import { ArchiveFormatError } from "./archive-errors.js";
5
+ export function isGzipBuffer(input) {
6
+ return input[0] === 31 && input[1] === 139;
7
+ }
5
8
  function assertConsumedBoundary(consumed, size) {
6
9
  if (!Number.isSafeInteger(consumed) || consumed <= 0 || consumed > size) {
7
10
  throw new ArchiveFormatError("invalid gzip consumed-input boundary");
@@ -1 +1 @@
1
- {"version":3,"file":"archive-input.d.ts","sourceRoot":"","sources":["../src/archive-input.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAGnD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAGL,KAAK,4BAA4B,EAClC,MAAM,qBAAqB,CAAC;AAM7B,MAAM,MAAM,iBAAiB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC;AAM/E,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,MAAM,EAAE,UAAU,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,kBAAkB,CAAC;CAC9B,GAAG,OAAO,CAAC,IAAI,CAAC,CAchB;AAED,wBAAsB,6BAA6B,CAAC,MAAM,EAAE;IAC1D,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,4BAA4B,CAAC;IACrC,QAAQ,EAAE,kBAAkB,CAAC;CAC9B,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAiE7B"}
1
+ {"version":3,"file":"archive-input.d.ts","sourceRoot":"","sources":["../src/archive-input.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAGnD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAGL,KAAK,4BAA4B,EAClC,MAAM,qBAAqB,CAAC;AAM7B,MAAM,MAAM,iBAAiB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAAC;AAM/E,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,MAAM,EAAE,UAAU,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,kBAAkB,CAAC;CAC9B,GAAG,OAAO,CAAC,IAAI,CAAC,CAchB;AAED,wBAAsB,6BAA6B,CAAC,MAAM,EAAE;IAC1D,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,4BAA4B,CAAC;IACrC,QAAQ,EAAE,kBAAkB,CAAC;CAC9B,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAqE7B"}
@@ -61,11 +61,13 @@ export async function stageArchiveFileForExtraction(params) {
61
61
  ? fsConstants.O_NOFOLLOW
62
62
  : 0);
63
63
  output = await fs.open(staged.path, flags, 0o600);
64
- const buffer = Buffer.allocUnsafe(64 * 1024);
64
+ const buffer = Buffer.allocUnsafe(Math.min(512 * 1024, Math.max(64 * 1024, Number(opened.size)), params.limits.maxArchiveBytes + 1));
65
65
  let written = 0;
66
66
  while (true) {
67
67
  params.deadline.check();
68
- const { bytesRead } = await handle.read(buffer, 0, buffer.length, null);
68
+ const length = Math.min(buffer.length, params.limits.maxArchiveBytes - written + 1);
69
+ const { bytesRead } = await handle.read(buffer, 0, length, null);
70
+ params.deadline.check();
69
71
  if (bytesRead === 0)
70
72
  break;
71
73
  written += bytesRead;
@@ -1,4 +1,5 @@
1
1
  import { type ExtractionDeadline } from "./archive-deadline.js";
2
+ import { type ArchiveDirectoryGuard } from "./archive-staging.js";
2
3
  export type ArchivePublicationEntry = {
3
4
  path: string;
4
5
  kind: "file" | "directory";
@@ -10,7 +11,10 @@ type MergeParams = {
10
11
  destinationRealDir: string;
11
12
  deadline?: ExtractionDeadline;
12
13
  };
13
- export declare function mergePlannedArchiveIntoDestination(params: MergeParams & {
14
+ type GuardedMergeParams = Pick<MergeParams, "sourceDir" | "deadline"> & {
15
+ destinationGuard: ArchiveDirectoryGuard;
16
+ };
17
+ export declare function mergePlannedArchiveIntoDestination(params: GuardedMergeParams & {
14
18
  entries: readonly ArchivePublicationEntry[];
15
19
  durable?: boolean;
16
20
  }): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"archive-merge.d.ts","sourceRoot":"","sources":["../src/archive-merge.ts"],"names":[],"mappings":"AAGA,OAAO,EAAoC,KAAK,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAkBlG,MAAM,MAAM,uBAAuB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AACjG,KAAK,WAAW,GAAG;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,EAAE,kBAAkB,CAAC;CAC/B,CAAC;AAEF,wBAAsB,kCAAkC,CACtD,MAAM,EAAE,WAAW,GAAG;IAAE,OAAO,EAAE,SAAS,uBAAuB,EAAE,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GACvF,OAAO,CAAC,IAAI,CAAC,CAEf;AAED,wBAAsB,iCAAiC,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAE1F"}
1
+ {"version":3,"file":"archive-merge.d.ts","sourceRoot":"","sources":["../src/archive-merge.ts"],"names":[],"mappings":"AAGA,OAAO,EAAoC,KAAK,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAClG,OAAO,EAIL,KAAK,qBAAqB,EAC3B,MAAM,sBAAsB,CAAC;AAa9B,MAAM,MAAM,uBAAuB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AACjG,KAAK,WAAW,GAAG;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,EAAE,kBAAkB,CAAC;CAC/B,CAAC;AACF,KAAK,kBAAkB,GAAG,IAAI,CAAC,WAAW,EAAE,WAAW,GAAG,UAAU,CAAC,GAAG;IACtE,gBAAgB,EAAE,qBAAqB,CAAC;CACzC,CAAC;AAEF,wBAAsB,kCAAkC,CACtD,MAAM,EAAE,kBAAkB,GAAG;IAAE,OAAO,EAAE,SAAS,uBAAuB,EAAE,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAC9F,OAAO,CAAC,IAAI,CAAC,CAEf;AAED,wBAAsB,iCAAiC,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAG1F"}
@@ -3,13 +3,13 @@ import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { ownExtractionDestinationMutation } from "./archive-deadline.js";
5
5
  import { assertDirectoryIdentityGuard, assertResolvedInsideDestination, createDirectoryIdentityGuard, createArchiveSymlinkTraversalError, preparePrivateArchiveOutputPath, } from "./archive-staging.js";
6
- import {} from "./directory-guard.js";
7
6
  import {} from "./directory-mode-owner.js";
8
7
  import { pinNodeDirectoryForMode } from "./directory-mode-node.js";
9
8
  import { FsSafeError } from "./errors.js";
10
9
  import { formatErrorDetail } from "./error-detail.js";
11
10
  import { isPathInside } from "./path.js";
12
- import { root } from "./root.js";
11
+ import { rootFromDirectoryGuard } from "./root-impl.js";
12
+ import { realpathSync } from "./realpath.js";
13
13
  import { getFsSafeTestHooks } from "./test-hooks.js";
14
14
  import { onCopyPublication } from "./copy-publication.js";
15
15
  import { syncFileBestEffortSync } from "./file-sync.js";
@@ -18,16 +18,18 @@ export async function mergePlannedArchiveIntoDestination(params) {
18
18
  await mergeTree(params, params.entries, params.durable === true);
19
19
  }
20
20
  export async function mergeExtractedTreeIntoDestination(params) {
21
- await mergeTree(params);
21
+ const destinationGuard = await createDirectoryIdentityGuard(params.destinationRealDir);
22
+ await mergeTree({ sourceDir: params.sourceDir, deadline: params.deadline, destinationGuard });
22
23
  }
23
24
  async function mergeTree(params, publication, durable = true) {
24
25
  const publishedFiles = [];
25
26
  const publishedDirectories = [];
26
27
  const check = () => params.deadline?.check();
27
28
  check();
28
- const destinationGuard = await createDirectoryIdentityGuard(params.destinationRealDir);
29
- check();
30
- const targetRoot = await root(params.destinationRealDir);
29
+ const { destinationGuard } = params;
30
+ const destinationDir = destinationGuard.dir;
31
+ const destinationRealDir = destinationGuard.realPath;
32
+ const targetRoot = rootFromDirectoryGuard(destinationGuard);
31
33
  check();
32
34
  const sourceGuard = await createDirectoryIdentityGuard(params.sourceDir);
33
35
  check();
@@ -35,7 +37,7 @@ async function mergeTree(params, publication, durable = true) {
35
37
  for (const entry of publication ?? []) {
36
38
  // Resolve admitted spelling in private staging, preserving the volume's case
37
39
  // and Unicode behavior without assigning explicit modes to distinct parents.
38
- const stagedPath = fsSync.realpathSync.native(path.join(params.sourceDir, entry.path));
40
+ const stagedPath = realpathSync.native(path.join(params.sourceDir, entry.path));
39
41
  check();
40
42
  if (!isPathInside(sourceGuard.realPath, stagedPath) || plan.has(stagedPath)) {
41
43
  throw new FsSafeError("path-mismatch", "archive publication paths changed in staging");
@@ -64,12 +66,12 @@ async function mergeTree(params, publication, durable = true) {
64
66
  const sourcePath = path.join(sourceDir, entry.name);
65
67
  const relPath = path.relative(params.sourceDir, sourcePath);
66
68
  const originalPath = relPath.split(path.sep).join("/");
67
- const destinationPath = path.join(params.destinationDir, relPath);
69
+ const destinationPath = path.join(destinationDir, relPath);
68
70
  const sourceStat = fsSync.lstatSync(sourcePath);
69
71
  check();
70
72
  if (sourceStat.isSymbolicLink())
71
73
  throw createArchiveSymlinkTraversalError(originalPath);
72
- const sourceReal = fsSync.realpathSync.native(sourcePath);
74
+ const sourceReal = realpathSync.native(sourcePath);
73
75
  check();
74
76
  if (!isPathInside(sourceGuard.realPath, sourceReal))
75
77
  throw createArchiveSymlinkTraversalError(originalPath);
@@ -83,7 +85,8 @@ async function mergeTree(params, publication, durable = true) {
83
85
  }
84
86
  const mode = plan ? planned?.mode ?? 0o755 : sourceStat.mode & 0o777;
85
87
  await preparePrivateArchiveOutputPath({
86
- ...params, relPath, outPath: destinationPath, originalPath, isDirectory: kind === "directory",
88
+ destinationDir, destinationRealDir, deadline: params.deadline,
89
+ relPath, outPath: destinationPath, originalPath, isDirectory: kind === "directory",
87
90
  }, assertGuards, destinationGuard);
88
91
  check();
89
92
  if (kind === "directory") {
@@ -119,7 +122,7 @@ async function mergeTree(params, publication, durable = true) {
119
122
  await assertDirectoryIdentityGuard(guard);
120
123
  check();
121
124
  await assertResolvedInsideDestination({
122
- destinationRealDir: params.destinationRealDir, targetPath: destinationPath, originalPath,
125
+ destinationRealDir, targetPath: destinationPath, originalPath,
123
126
  });
124
127
  check();
125
128
  } });
@@ -162,7 +165,7 @@ async function mergeTree(params, publication, durable = true) {
162
165
  check();
163
166
  await assertGuards();
164
167
  await assertResolvedInsideDestination({
165
- destinationRealDir: params.destinationRealDir, targetPath: destinationPath, originalPath,
168
+ destinationRealDir, targetPath: destinationPath, originalPath,
166
169
  });
167
170
  check();
168
171
  const stat = fsSync.lstatSync(destinationPath);
@@ -6,7 +6,7 @@ import { stripArchivePath, validateArchiveEntryPath } from "./archive-entry.js";
6
6
  import { createArchiveEntryPlanner } from "./archive-plan.js";
7
7
  import { stageArchiveFileForExtraction } from "./archive-input.js";
8
8
  import { assertArchiveEntryCountWithinLimit, } from "./archive-limits.js";
9
- import { prepareArchiveDestinationDir, withStagedArchiveDestination, } from "./archive-staging.js";
9
+ import { prepareArchiveDestinationGuard, withStagedArchiveDestination, } from "./archive-staging.js";
10
10
  import { mergePlannedArchiveIntoDestination } from "./archive-merge.js";
11
11
  import { admitZipFile } from "./archive-zip-admission.js";
12
12
  export function throwMappedNativeArchiveError(error) {
@@ -35,7 +35,8 @@ export async function extractNativeArchive(params) {
35
35
  const physicalCount = params.kind === "zip"
36
36
  ? await admitZipFile(stagedArchive.path, limits, params.deadline, (entry) => { zipEntries.push(entry); })
37
37
  : undefined;
38
- const destinationRealDir = await prepareArchiveDestinationDir(params.destDir);
38
+ const destinationGuard = await prepareArchiveDestinationGuard(params.destDir);
39
+ const destinationRealDir = destinationGuard.realPath;
39
40
  await withStagedArchiveDestination({
40
41
  destinationRealDir,
41
42
  run: async (stagingDir) => {
@@ -95,8 +96,7 @@ export async function extractNativeArchive(params) {
95
96
  entries: plan,
96
97
  durable: params.durable,
97
98
  sourceDir: stagingDir,
98
- destinationDir: params.destDir,
99
- destinationRealDir,
99
+ destinationGuard,
100
100
  deadline: params.deadline,
101
101
  });
102
102
  params.deadline.check();
Binary file
@@ -1 +1 @@
1
- {"version":3,"file":"archive-read.d.ts","sourceRoot":"","sources":["../src/archive-read.ts"],"names":[],"mappings":"AAeA,OAAO,EAAiD,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAiOpG,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,WAAW,CAAA;CAAE,GAChD,OAAO,CAAC,MAAM,CAAC,CAkBjB"}
1
+ {"version":3,"file":"archive-read.d.ts","sourceRoot":"","sources":["../src/archive-read.ts"],"names":[],"mappings":"AAeA,OAAO,EAAiD,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAyOpG,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,WAAW,CAAA;CAAE,GAChD,OAAO,CAAC,MAAM,CAAC,CAkBjB"}