@openclaw/openshell-sandbox 2026.10.5-beta.4 → 2026.10.10-beta.2

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 (240) hide show
  1. package/README.md +1 -1
  2. package/node_modules/@openclaw/fs-safe/CHANGELOG.md +148 -0
  3. package/node_modules/@openclaw/fs-safe/README.md +20 -4
  4. package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +2 -1
  5. package/node_modules/@openclaw/fs-safe/dist/advanced.js +1 -0
  6. package/node_modules/@openclaw/fs-safe/dist/archive-merge.js +28 -3
  7. package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
  8. package/node_modules/@openclaw/fs-safe/dist/archive-read.js +11 -5
  9. package/node_modules/@openclaw/fs-safe/dist/archive-zip-loader.js +4 -2
  10. package/node_modules/@openclaw/fs-safe/dist/archive.js +2 -2
  11. package/node_modules/@openclaw/fs-safe/dist/atomic-io.js +2 -2
  12. package/node_modules/@openclaw/fs-safe/dist/bounded-read.d.ts +1 -0
  13. package/node_modules/@openclaw/fs-safe/dist/bounded-read.js +9 -2
  14. package/node_modules/@openclaw/fs-safe/dist/clone-metadata.d.ts +2 -0
  15. package/node_modules/@openclaw/fs-safe/dist/clone-metadata.js +2 -1
  16. package/node_modules/@openclaw/fs-safe/dist/config.d.ts +6 -0
  17. package/node_modules/@openclaw/fs-safe/dist/config.js +5 -0
  18. package/node_modules/@openclaw/fs-safe/dist/copy-file-input.d.ts +3 -1
  19. package/node_modules/@openclaw/fs-safe/dist/copy-file-input.js +8 -7
  20. package/node_modules/@openclaw/fs-safe/dist/copy-link.d.ts +26 -0
  21. package/node_modules/@openclaw/fs-safe/dist/copy-link.js +170 -0
  22. package/node_modules/@openclaw/fs-safe/dist/copy-metadata.d.ts +9 -0
  23. package/node_modules/@openclaw/fs-safe/dist/copy-metadata.js +39 -0
  24. package/node_modules/@openclaw/fs-safe/dist/copy-path-admission.js +11 -2
  25. package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.d.ts +2 -4
  26. package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.js +4 -2
  27. package/node_modules/@openclaw/fs-safe/dist/copy-tree-portable.js +3 -6
  28. package/node_modules/@openclaw/fs-safe/dist/copy.js +3 -3
  29. package/node_modules/@openclaw/fs-safe/dist/create.js +1 -1
  30. package/node_modules/@openclaw/fs-safe/dist/creation-boundary.js +7 -4
  31. package/node_modules/@openclaw/fs-safe/dist/darwin-acl.d.ts +12 -0
  32. package/node_modules/@openclaw/fs-safe/dist/darwin-acl.js +29 -0
  33. package/node_modules/@openclaw/fs-safe/dist/directory-durability.d.ts +7 -4
  34. package/node_modules/@openclaw/fs-safe/dist/directory-guard.js +20 -5
  35. package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.d.ts +3 -0
  36. package/node_modules/@openclaw/fs-safe/dist/directory-mode-node.js +27 -2
  37. package/node_modules/@openclaw/fs-safe/dist/directory-receipt.d.ts +7 -7
  38. package/node_modules/@openclaw/fs-safe/dist/directory-receipt.js +9 -4
  39. package/node_modules/@openclaw/fs-safe/dist/durability.d.ts +2 -1
  40. package/node_modules/@openclaw/fs-safe/dist/entry-publication.js +2 -2
  41. package/node_modules/@openclaw/fs-safe/dist/exact-identity.d.ts +17 -0
  42. package/node_modules/@openclaw/fs-safe/dist/exact-identity.js +29 -0
  43. package/node_modules/@openclaw/fs-safe/dist/file-handle-transfer.js +4 -2
  44. package/node_modules/@openclaw/fs-safe/dist/file-hash.d.ts +1 -1
  45. package/node_modules/@openclaw/fs-safe/dist/file-hash.js +11 -9
  46. package/node_modules/@openclaw/fs-safe/dist/file-identity.d.ts +7 -1
  47. package/node_modules/@openclaw/fs-safe/dist/file-identity.js +27 -5
  48. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-admission.js +1 -2
  49. package/node_modules/@openclaw/fs-safe/dist/file-lock-sync.js +8 -1
  50. package/node_modules/@openclaw/fs-safe/dist/file-lock.d.ts +1 -0
  51. package/node_modules/@openclaw/fs-safe/dist/file-lock.js +1 -0
  52. package/node_modules/@openclaw/fs-safe/dist/file-write-lease.d.ts +9 -0
  53. package/node_modules/@openclaw/fs-safe/dist/file-write-lease.js +42 -0
  54. package/node_modules/@openclaw/fs-safe/dist/guarded-mkdir.d.ts +1 -0
  55. package/node_modules/@openclaw/fs-safe/dist/guarded-mkdir.js +22 -15
  56. package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +3 -0
  57. package/node_modules/@openclaw/fs-safe/dist/hardlink-fallback.d.ts +1 -0
  58. package/node_modules/@openclaw/fs-safe/dist/hardlink-fallback.js +5 -0
  59. package/node_modules/@openclaw/fs-safe/dist/index.d.ts +1 -0
  60. package/node_modules/@openclaw/fs-safe/dist/index.js +1 -0
  61. package/node_modules/@openclaw/fs-safe/dist/json-durable-queue-ownership.js +16 -3
  62. package/node_modules/@openclaw/fs-safe/dist/local-file-descriptor.js +24 -13
  63. package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +85 -18
  64. package/node_modules/@openclaw/fs-safe/dist/native-config.d.ts +5 -0
  65. package/node_modules/@openclaw/fs-safe/dist/native-config.js +11 -0
  66. package/node_modules/@openclaw/fs-safe/dist/native-directory-observation.js +6 -4
  67. package/node_modules/@openclaw/fs-safe/dist/native-fallback-warning.d.ts +2 -1
  68. package/node_modules/@openclaw/fs-safe/dist/native-fallback-warning.js +26 -2
  69. package/node_modules/@openclaw/fs-safe/dist/native-guidance.d.ts +3 -0
  70. package/node_modules/@openclaw/fs-safe/dist/native-guidance.js +21 -0
  71. package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.d.ts +1 -1
  72. package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.js +2 -0
  73. package/node_modules/@openclaw/fs-safe/dist/native-operations.js +8 -6
  74. package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.d.ts +44 -4
  75. package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.js +85 -15
  76. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.d.ts +3 -3
  77. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.js +33 -6
  78. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.d.ts +4 -1
  79. package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +22 -3
  80. package/node_modules/@openclaw/fs-safe/dist/native-policy-directory-observation.js +4 -3
  81. package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.d.ts +1 -1
  82. package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.js +3 -0
  83. package/node_modules/@openclaw/fs-safe/dist/native-staged-file-metadata.d.ts +3 -0
  84. package/node_modules/@openclaw/fs-safe/dist/native-staged-file-metadata.js +16 -0
  85. package/node_modules/@openclaw/fs-safe/dist/native-staged-file.d.ts +1 -1
  86. package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +15 -5
  87. package/node_modules/@openclaw/fs-safe/dist/native-stat-identity.d.ts +3 -0
  88. package/node_modules/@openclaw/fs-safe/dist/native-stat-identity.js +7 -0
  89. package/node_modules/@openclaw/fs-safe/dist/native-status.d.ts +17 -0
  90. package/node_modules/@openclaw/fs-safe/dist/native-status.js +30 -0
  91. package/node_modules/@openclaw/fs-safe/dist/native.d.ts +31 -4
  92. package/node_modules/@openclaw/fs-safe/dist/native.js +107 -9
  93. package/node_modules/@openclaw/fs-safe/dist/opened-realpath.js +9 -1
  94. package/node_modules/@openclaw/fs-safe/dist/overwrite-file-handle.js +4 -1
  95. package/node_modules/@openclaw/fs-safe/dist/owner-dacl-batch.js +1 -1
  96. package/node_modules/@openclaw/fs-safe/dist/owner-dacl.js +1 -1
  97. package/node_modules/@openclaw/fs-safe/dist/permission-exec.js +9 -1
  98. package/node_modules/@openclaw/fs-safe/dist/permissions-public.d.ts +1 -0
  99. package/node_modules/@openclaw/fs-safe/dist/permissions-public.js +1 -0
  100. package/node_modules/@openclaw/fs-safe/dist/permissions-windows.js +83 -47
  101. package/node_modules/@openclaw/fs-safe/dist/pinned-open.d.ts +2 -0
  102. package/node_modules/@openclaw/fs-safe/dist/pinned-open.js +8 -3
  103. package/node_modules/@openclaw/fs-safe/dist/pinned-write-input.js +13 -0
  104. package/node_modules/@openclaw/fs-safe/dist/pinned-write-staged.js +10 -8
  105. package/node_modules/@openclaw/fs-safe/dist/pinned-write-types.d.ts +2 -1
  106. package/node_modules/@openclaw/fs-safe/dist/pinned-write.js +7 -5
  107. package/node_modules/@openclaw/fs-safe/dist/pipe.d.ts +9 -0
  108. package/node_modules/@openclaw/fs-safe/dist/pipe.js +26 -0
  109. package/node_modules/@openclaw/fs-safe/dist/private-directory.js +1 -1
  110. package/node_modules/@openclaw/fs-safe/dist/private-producer-handoff.js +2 -5
  111. package/node_modules/@openclaw/fs-safe/dist/private-temp-workspace.d.ts +3 -2
  112. package/node_modules/@openclaw/fs-safe/dist/private-temp-workspace.js +38 -10
  113. package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.js +92 -10
  114. package/node_modules/@openclaw/fs-safe/dist/publish-file-failure.d.ts +3 -0
  115. package/node_modules/@openclaw/fs-safe/dist/publish-file-failure.js +3 -0
  116. package/node_modules/@openclaw/fs-safe/dist/publish-file.d.ts +5 -3
  117. package/node_modules/@openclaw/fs-safe/dist/publish-file.js +26 -20
  118. package/node_modules/@openclaw/fs-safe/dist/read-opened-file.d.ts +2 -0
  119. package/node_modules/@openclaw/fs-safe/dist/read-opened-file.js +3 -4
  120. package/node_modules/@openclaw/fs-safe/dist/realpath.js +32 -0
  121. package/node_modules/@openclaw/fs-safe/dist/regular-file.d.ts +4 -0
  122. package/node_modules/@openclaw/fs-safe/dist/regular-file.js +11 -15
  123. package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.js +3 -2
  124. package/node_modules/@openclaw/fs-safe/dist/root-copy.d.ts +18 -0
  125. package/node_modules/@openclaw/fs-safe/dist/root-copy.js +128 -0
  126. package/node_modules/@openclaw/fs-safe/dist/root-create-native.d.ts +8 -13
  127. package/node_modules/@openclaw/fs-safe/dist/root-create-native.js +252 -27
  128. package/node_modules/@openclaw/fs-safe/dist/root-directory-creation.js +5 -1
  129. package/node_modules/@openclaw/fs-safe/dist/root-directory-list-types.d.ts +11 -8
  130. package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +35 -13
  131. package/node_modules/@openclaw/fs-safe/dist/root-file-open-batch-sync.d.ts +8 -0
  132. package/node_modules/@openclaw/fs-safe/dist/root-file-open-batch-sync.js +108 -0
  133. package/node_modules/@openclaw/fs-safe/dist/root-file.d.ts +7 -0
  134. package/node_modules/@openclaw/fs-safe/dist/root-file.js +15 -0
  135. package/node_modules/@openclaw/fs-safe/dist/root-impl.d.ts +2 -0
  136. package/node_modules/@openclaw/fs-safe/dist/root-impl.js +154 -175
  137. package/node_modules/@openclaw/fs-safe/dist/root-mkdir-native.d.ts +2 -0
  138. package/node_modules/@openclaw/fs-safe/dist/root-mkdir-native.js +239 -0
  139. package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +37 -15
  140. package/node_modules/@openclaw/fs-safe/dist/root-native-creation-path.d.ts +32 -0
  141. package/node_modules/@openclaw/fs-safe/dist/root-native-creation-path.js +56 -0
  142. package/node_modules/@openclaw/fs-safe/dist/root-options.d.ts +2 -0
  143. package/node_modules/@openclaw/fs-safe/dist/root-public-types.d.ts +1 -0
  144. package/node_modules/@openclaw/fs-safe/dist/root-remove-native.js +95 -16
  145. package/node_modules/@openclaw/fs-safe/dist/root-remove.js +6 -4
  146. package/node_modules/@openclaw/fs-safe/dist/root-walk.js +23 -19
  147. package/node_modules/@openclaw/fs-safe/dist/root-write-admission.js +8 -6
  148. package/node_modules/@openclaw/fs-safe/dist/root-write-publication.js +5 -1
  149. package/node_modules/@openclaw/fs-safe/dist/secure-file-windows.js +3 -1
  150. package/node_modules/@openclaw/fs-safe/dist/secure-file.d.ts +2 -0
  151. package/node_modules/@openclaw/fs-safe/dist/secure-file.js +10 -4
  152. package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +1 -1
  153. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js +2 -2
  154. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-retained.js +1 -1
  155. package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-target.js +8 -1
  156. package/node_modules/@openclaw/fs-safe/dist/staged-directory.d.ts +13 -0
  157. package/node_modules/@openclaw/fs-safe/dist/staged-directory.js +29 -0
  158. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.js +5 -3
  159. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-descriptor.d.ts +8 -0
  160. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-descriptor.js +13 -2
  161. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-owner.js +71 -3
  162. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-ownership.d.ts +2 -1
  163. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-ownership.js +22 -0
  164. package/node_modules/@openclaw/fs-safe/dist/temp-workspace-types.d.ts +3 -0
  165. package/node_modules/@openclaw/fs-safe/dist/test-hooks.d.ts +1 -0
  166. package/node_modules/@openclaw/fs-safe/dist/test-hooks.js +1 -0
  167. package/node_modules/@openclaw/fs-safe/dist/trash.js +2 -2
  168. package/node_modules/@openclaw/fs-safe/dist/types.d.ts +4 -0
  169. package/node_modules/@openclaw/fs-safe/dist/watch-native.js +2 -2
  170. package/node_modules/@openclaw/fs-safe/dist/windows-appcontainer.d.ts +5 -0
  171. package/node_modules/@openclaw/fs-safe/dist/windows-appcontainer.js +39 -0
  172. package/node_modules/@openclaw/fs-safe/dist/windows-owner.d.ts +1 -0
  173. package/node_modules/@openclaw/fs-safe/dist/windows-owner.js +25 -36
  174. package/node_modules/@openclaw/fs-safe/dist/windows-security-command.js +24 -5
  175. package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.d.ts +2 -1
  176. package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.js +4 -0
  177. package/node_modules/@openclaw/fs-safe/dist/windows-security-helper.d.ts +2 -0
  178. package/node_modules/@openclaw/fs-safe/dist/windows-security-helper.exe +0 -0
  179. package/node_modules/@openclaw/fs-safe/dist/windows-security-helper.js +7 -0
  180. package/node_modules/@openclaw/fs-safe/dist/windows-test-hooks.d.ts +16 -0
  181. package/node_modules/@openclaw/fs-safe/dist/windows-test-hooks.js +51 -0
  182. package/node_modules/@openclaw/fs-safe/dist/write-file-handle.d.ts +1 -0
  183. package/node_modules/@openclaw/fs-safe/dist/write-file-handle.js +12 -10
  184. package/node_modules/@openclaw/fs-safe/docs/advanced.md +2 -1
  185. package/node_modules/@openclaw/fs-safe/docs/archive.md +16 -0
  186. package/node_modules/@openclaw/fs-safe/docs/config.md +71 -1
  187. package/node_modules/@openclaw/fs-safe/docs/contributing.md +45 -5
  188. package/node_modules/@openclaw/fs-safe/docs/copy.md +45 -5
  189. package/node_modules/@openclaw/fs-safe/docs/durability.md +34 -7
  190. package/node_modules/@openclaw/fs-safe/docs/errors.md +8 -0
  191. package/node_modules/@openclaw/fs-safe/docs/guest.md +2 -0
  192. package/node_modules/@openclaw/fs-safe/docs/install.md +47 -27
  193. package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.5.md +6 -6
  194. package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.6.md +1 -1
  195. package/node_modules/@openclaw/fs-safe/docs/native-helper.md +79 -6
  196. package/node_modules/@openclaw/fs-safe/docs/native.md +279 -8
  197. package/node_modules/@openclaw/fs-safe/docs/permissions.md +64 -15
  198. package/node_modules/@openclaw/fs-safe/docs/pipe.md +81 -0
  199. package/node_modules/@openclaw/fs-safe/docs/platform-capabilities.md +63 -0
  200. package/node_modules/@openclaw/fs-safe/docs/public-api.md +10 -0
  201. package/node_modules/@openclaw/fs-safe/docs/queue-link-free-design.md +237 -0
  202. package/node_modules/@openclaw/fs-safe/docs/quickstart.md +23 -3
  203. package/node_modules/@openclaw/fs-safe/docs/reading.md +56 -2
  204. package/node_modules/@openclaw/fs-safe/docs/regular-file.md +2 -0
  205. package/node_modules/@openclaw/fs-safe/docs/secret-file.md +5 -5
  206. package/node_modules/@openclaw/fs-safe/docs/secure-file.md +1 -1
  207. package/node_modules/@openclaw/fs-safe/docs/security-model.md +35 -0
  208. package/node_modules/@openclaw/fs-safe/docs/sidecar-lock.md +31 -13
  209. package/node_modules/@openclaw/fs-safe/docs/staged-file.md +18 -0
  210. package/node_modules/@openclaw/fs-safe/docs/store.md +15 -0
  211. package/node_modules/@openclaw/fs-safe/docs/temp.md +39 -0
  212. package/node_modules/@openclaw/fs-safe/docs/testing.md +13 -0
  213. package/node_modules/@openclaw/fs-safe/docs/types.md +51 -81
  214. package/node_modules/@openclaw/fs-safe/docs/walk.md +4 -1
  215. package/node_modules/@openclaw/fs-safe/docs/watch.md +3 -2
  216. package/node_modules/@openclaw/fs-safe/docs/writing.md +121 -14
  217. package/node_modules/@openclaw/fs-safe/package.json +21 -12
  218. package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
  219. package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
  220. package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
  221. package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
  222. package/node_modules/@openclaw/fs-safe-freebsd-arm64/fs-safe-native.node +0 -0
  223. package/node_modules/@openclaw/fs-safe-freebsd-arm64/package.json +29 -0
  224. package/node_modules/@openclaw/fs-safe-freebsd-x64/fs-safe-native.node +0 -0
  225. package/node_modules/@openclaw/fs-safe-freebsd-x64/package.json +29 -0
  226. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
  227. package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
  228. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
  229. package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
  230. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
  231. package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
  232. package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
  233. package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
  234. package/node_modules/@openclaw/fs-safe-win32-arm64-msvc/fs-safe-native.node +0 -0
  235. package/node_modules/@openclaw/fs-safe-win32-arm64-msvc/package.json +29 -0
  236. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/fs-safe-native.node +0 -0
  237. package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
  238. package/package.json +5 -5
  239. package/node_modules/@openclaw/fs-safe/dist/windows-security-bridge.cs +0 -336
  240. package/node_modules/@openclaw/fs-safe/dist/windows-security-bridge.ps1 +0 -90
@@ -54,36 +54,102 @@ import fs from "node:fs";
54
54
  import { randomUUID } from "node:crypto";
55
55
  import fsAsync from "node:fs/promises";
56
56
  import path from "node:path";
57
- import { assertSyncDirectoryGuard } from "./directory-guard.js";
57
+ import { assertSyncDirectoryGuard, inspectDirectoryIdentitySync } from "./directory-guard.js";
58
58
  import { nodeDirectorySearchOnlyFlags } from "./directory-mode-node.js";
59
59
  import { assertDarwinCreationAcl, assertPrivateDirectory } from "./creation-boundary.js";
60
60
  import { assertMutationNotDenied } from "./deny-mutations.js";
61
61
  import { FsSafeError } from "./errors.js";
62
62
  import { getNativeBinding } from "./native.js";
63
63
  import { captureNativeFdClose } from "./native-binding.js";
64
- import { openNativeRootAdmission } from "./native-parent-admission.js";
64
+ import { openExactNativeRootSync, openNativeRootAdmission } from "./native-parent-admission.js";
65
65
  import { capturePolicyAwareNativeParent } from "./native-policy-parent.js";
66
- import { isNotFoundPathError, isPathInside } from "./path.js";
66
+ import { hasNodeErrorCode, isNotFoundPathError, isPathInside } from "./path.js";
67
67
  import { preparePinnedWriteMutationAdmission } from "./pinned-mutation-admission.js";
68
68
  import { resolvePathViaExistingAncestor } from "./root-path-existing.js";
69
69
  import { admitPathInsideRoot } from "./root-boundary.js";
70
+ import { directoryComponentNotDirectoryError } from "./root-errors.js";
70
71
  import { assertRootIdentityCurrentSync } from "./root-context.js";
71
72
  import { inspectFileIdentitySync } from "./strict-file-identity.js";
72
73
  import { getFsSafeTestHooks } from "./test-hooks.js";
73
74
  import { createSuppressedError } from "./suppressed-error.js";
75
+ import { describePolicyStagedDirectory } from "./staged-directory.js";
76
+ import { pathForWindowsFilesystem } from "./windows-path-alias.js";
77
+ import { realpathSync } from "./realpath.js";
78
+ import { isSafePathSegment } from "./safe-path-segment.js";
79
+ import { nativePolicyDirectoryObserver } from "./native-policy-directory-observation.js";
80
+ import { existingCreationParent } from "./root-native-creation-path.js";
81
+ export { probeNativeCreationPath } from "./root-native-creation-path.js";
74
82
  export function rootWriteQueueKey(root, relativePath) {
75
83
  return `${root.rootReal}\0${relativePath}`;
76
84
  }
77
85
  export function buildAtomicWriteTempPath(targetPath) {
78
86
  return path.join(path.dirname(targetPath), `.fs-safe-${randomUUID()}.tmp`);
79
87
  }
88
+ const nativeCreateCollisions = new WeakSet();
89
+ export function isNativeCreateCollision(error) {
90
+ return typeof error === "object" && error !== null && nativeCreateCollisions.has(error);
91
+ }
80
92
  function unavailable() {
81
93
  throw new FsSafeError("helper-unavailable", "native confined creation is unavailable");
82
94
  }
83
95
  async function withNativeDirectory(params, operation, discard) {
84
- const binding = getNativeBinding();
96
+ const binding = getNativeBinding("openBeneath", "mkdirChildBeneath");
85
97
  if (!binding?.openBeneath || !binding.mkdirChildBeneath || (params.private && process.platform === "win32"))
86
98
  return unavailable();
99
+ const existing = params.selectedParent ?? existingCreationParent(params);
100
+ if (existing) {
101
+ let pending;
102
+ try {
103
+ const env_1 = { stack: [], error: void 0, hasError: false };
104
+ try {
105
+ const rootOwner = __addDisposableResource(env_1, openExactNativeRootSync(binding, {
106
+ rootPath: params.root.rootReal, rootIdentity: params.root.rootIdentity,
107
+ }), false);
108
+ const close = captureNativeFdClose(binding);
109
+ const borrowed = existing.path === params.root.rootReal;
110
+ const fd = borrowed ? rootOwner.fd : binding.openBeneath(rootOwner.fd, existing.relative, existing.flags).fd;
111
+ const parentOwner = __addDisposableResource(env_1, { [Symbol.dispose]() { if (!borrowed)
112
+ close(fd); } }, false);
113
+ inspectFileIdentitySync(() => borrowed ? rootOwner.stat : fs.fstatSync(fd, { bigint: true }), existing.identity);
114
+ verifyMkdirDirectory(binding, fd, existing.path);
115
+ const guard = { dir: existing.path, realPath: existing.path, stat: existing.identity };
116
+ const assertCurrent = () => {
117
+ let currentRoot;
118
+ assertRootIdentityCurrentSync(params.root, stat => { currentRoot = stat; });
119
+ if (borrowed) {
120
+ // Root and parent are the same name in this synchronous fence.
121
+ inspectDirectoryIdentitySync(existing.path, existing.identity, currentRoot);
122
+ if (realpathSync.native(pathForWindowsFilesystem(existing.path)) !== existing.path) {
123
+ throw new FsSafeError("path-mismatch", "directory changed during operation");
124
+ }
125
+ }
126
+ else {
127
+ assertSyncDirectoryGuard(guard);
128
+ }
129
+ };
130
+ pending = { value: await operation(binding, fd, assertCurrent) };
131
+ return pending.value;
132
+ }
133
+ catch (e_1) {
134
+ env_1.error = e_1;
135
+ env_1.hasError = true;
136
+ }
137
+ finally {
138
+ __disposeResources(env_1);
139
+ }
140
+ }
141
+ catch (error) {
142
+ if (pending && discard) {
143
+ try {
144
+ await discard(pending.value);
145
+ }
146
+ catch (cleanupError) {
147
+ throw createSuppressedError(cleanupError, error, "creation handoff and cleanup failed");
148
+ }
149
+ }
150
+ throw error;
151
+ }
152
+ }
87
153
  const directoryTarget = params.directory === params.target;
88
154
  const canonicalTarget = async (target) => directoryTarget
89
155
  ? await resolvePathViaExistingAncestor(target)
@@ -100,14 +166,14 @@ async function withNativeDirectory(params, operation, discard) {
100
166
  const close = captureNativeFdClose(binding);
101
167
  let pending;
102
168
  try {
103
- const env_1 = { stack: [], error: void 0, hasError: false };
169
+ const env_2 = { stack: [], error: void 0, hasError: false };
104
170
  try {
105
171
  const admitted = await openNativeRootAdmission(binding, {
106
172
  rootPath: params.root.rootReal, rootIdentity: params.root.rootIdentity, operation: "create", reportCloseErrors: true, searchOnly: true,
107
173
  });
108
- const rootOwner = __addDisposableResource(env_1, admitted.root, true);
174
+ const rootOwner = __addDisposableResource(env_2, admitted.root, true);
109
175
  const descriptors = [];
110
- const directoryOwner = __addDisposableResource(env_1, { [Symbol.dispose]() {
176
+ const directoryOwner = __addDisposableResource(env_2, { [Symbol.dispose]() {
111
177
  const errors = [];
112
178
  for (const fd of descriptors.reverse())
113
179
  try {
@@ -205,12 +271,12 @@ async function withNativeDirectory(params, operation, discard) {
205
271
  pending = { value: await operation(binding, current, assertCurrent) };
206
272
  return pending.value;
207
273
  }
208
- catch (e_1) {
209
- env_1.error = e_1;
210
- env_1.hasError = true;
274
+ catch (e_2) {
275
+ env_2.error = e_2;
276
+ env_2.hasError = true;
211
277
  }
212
278
  finally {
213
- const result_1 = __disposeResources(env_1);
279
+ const result_1 = __disposeResources(env_2);
214
280
  if (result_1)
215
281
  await result_1;
216
282
  }
@@ -227,15 +293,166 @@ async function withNativeDirectory(params, operation, discard) {
227
293
  throw error;
228
294
  }
229
295
  }
296
+ /** Admit only an ordinary direct child; aliases keep the full resolver. */
297
+ export function tryMkdirNativeDirectChild(root, relativePath) {
298
+ if (!isSafePathSegment(relativePath, { allowDotPrefix: true }) || relativePath.endsWith(".") ||
299
+ getFsSafeTestHooks()?.beforeRootFallbackMutation)
300
+ return false;
301
+ assertRootIdentityCurrentSync(root);
302
+ const target = path.join(root.rootReal, relativePath);
303
+ try {
304
+ const entry = fs.lstatSync(pathForWindowsFilesystem(target), { throwIfNoEntry: false });
305
+ if (entry && (!entry.isDirectory() ||
306
+ realpathSync.native(pathForWindowsFilesystem(target)) !== target))
307
+ return false;
308
+ }
309
+ catch {
310
+ return false;
311
+ }
312
+ try {
313
+ mkdirNativeRootChild({ root, target });
314
+ return true;
315
+ }
316
+ catch (error) {
317
+ if (hasNodeErrorCode(error, "ENOTDIR"))
318
+ throw directoryComponentNotDirectoryError(error);
319
+ throw error;
320
+ }
321
+ }
230
322
  export async function tryMkdirRootNative(params) {
231
323
  // The protected Windows creator already verifies the admitted parent identity
232
324
  // and uses handle-relative NtCreateFile with its protected security descriptor.
233
325
  if (params.private && process.platform === "win32")
234
326
  return false;
235
- return await withNativeDirectory(params, async (_binding, _fd, assertCurrent) => { assertCurrent(); return true; });
327
+ try {
328
+ if (!params.private && !params.policy && !params.assertBeforeMutation &&
329
+ !getFsSafeTestHooks()?.beforeRootFallbackMutation &&
330
+ params.originalPath === path.basename(params.target) &&
331
+ path.dirname(params.target) === params.root.rootReal &&
332
+ params.directory === params.target) {
333
+ mkdirNativeRootChild(params);
334
+ return true;
335
+ }
336
+ return await withNativeDirectory(params, async (_binding, _fd, assertCurrent) => { assertCurrent(); return true; });
337
+ }
338
+ catch (error) {
339
+ if (hasNodeErrorCode(error, "ENOTDIR"))
340
+ throw directoryComponentNotDirectoryError(error);
341
+ throw error;
342
+ }
343
+ }
344
+ export function verifyMkdirDirectory(binding, fd, pathname) {
345
+ const observe = nativePolicyDirectoryObserver(binding, fd, pathname);
346
+ try {
347
+ // The native observation compares this retained descriptor before and
348
+ // after the no-follow name check, including mode, links, and canonical path.
349
+ if (observe?.())
350
+ return;
351
+ }
352
+ finally {
353
+ observe?.dispose();
354
+ }
355
+ describePolicyStagedDirectory(fd, pathname);
356
+ }
357
+ export function ensureNativeRootCurrent(root) {
358
+ assertRootIdentityCurrentSync(root);
359
+ const binding = getNativeBinding("openBeneath", "mkdirChildBeneath");
360
+ if (!binding?.openBeneath || !binding.mkdirChildBeneath)
361
+ return unavailable();
362
+ if (binding.verifyRootDirectory) {
363
+ const closeNative = captureNativeFdClose(binding);
364
+ const failure = binding.verifyRootDirectory(root.rootReal, root.rootIdentity.dev, root.rootIdentity.ino);
365
+ if (failure === null)
366
+ return;
367
+ if (failure?.fallbackFd !== undefined && failure.fallbackFd !== null) {
368
+ const env_3 = { stack: [], error: void 0, hasError: false };
369
+ try {
370
+ const fd = failure.fallbackFd;
371
+ if (!Number.isInteger(fd) || fd < 0)
372
+ throw new FsSafeError("helper-failed", "invalid root fallback descriptor");
373
+ const owner = __addDisposableResource(env_3, { [Symbol.dispose]() { closeNative(fd); } }, false);
374
+ if (failure.errorCode || failure.closeErrorCode)
375
+ throw new FsSafeError("helper-failed", "invalid root verification result");
376
+ ensureNativeRootCurrentLegacy(root, binding);
377
+ return;
378
+ }
379
+ catch (e_3) {
380
+ env_3.error = e_3;
381
+ env_3.hasError = true;
382
+ }
383
+ finally {
384
+ __disposeResources(env_3);
385
+ }
386
+ }
387
+ const error = failure?.errorCode ? rootVerificationError(failure.errorCode, failure.errorMessage, failure.errorErrno) : undefined;
388
+ const closeError = failure?.closeErrorCode ? Object.assign(new Error(failure.closeErrorMessage), {
389
+ code: failure.closeErrorCode,
390
+ ...(typeof failure.closeErrorErrno === "number" ? { errno: failure.closeErrorErrno } : {}),
391
+ }) : undefined;
392
+ if (error && closeError)
393
+ throw createSuppressedError(closeError, error, "root verification and close failed");
394
+ if (error || closeError)
395
+ throw error ?? closeError;
396
+ throw new FsSafeError("helper-failed", "invalid root verification result");
397
+ }
398
+ ensureNativeRootCurrentLegacy(root, binding);
399
+ }
400
+ function rootVerificationError(code, message, errno) {
401
+ const error = Object.assign(new Error(message), { code, ...(typeof errno === "number" ? { errno } : {}) });
402
+ return code === "path-mismatch" || code === "OBSERVATION_REDIRECTED"
403
+ ? new FsSafeError("path-mismatch", "root identity changed during verification", { cause: error }) : error;
404
+ }
405
+ function ensureNativeRootCurrentLegacy(root, binding) {
406
+ const env_4 = { stack: [], error: void 0, hasError: false };
407
+ try {
408
+ const rootOwner = __addDisposableResource(env_4, openExactNativeRootSync(binding, {
409
+ rootPath: root.rootReal, rootIdentity: root.rootIdentity,
410
+ }), false);
411
+ verifyMkdirDirectory(binding, rootOwner.fd, root.rootReal);
412
+ assertRootIdentityCurrentSync(root);
413
+ }
414
+ catch (e_4) {
415
+ env_4.error = e_4;
416
+ env_4.hasError = true;
417
+ }
418
+ finally {
419
+ __disposeResources(env_4);
420
+ }
421
+ }
422
+ function mkdirNativeRootChild(params) {
423
+ const env_5 = { stack: [], error: void 0, hasError: false };
424
+ try {
425
+ const binding = getNativeBinding("openBeneath", "mkdirChildBeneath");
426
+ if (!binding?.openBeneath || !binding.mkdirChildBeneath)
427
+ return unavailable();
428
+ const rootOwner = __addDisposableResource(env_5, openExactNativeRootSync(binding, {
429
+ rootPath: params.root.rootReal, rootIdentity: params.root.rootIdentity,
430
+ }), false);
431
+ verifyMkdirDirectory(binding, rootOwner.fd, params.root.rootReal);
432
+ const flags = (nodeDirectorySearchOnlyFlags()?.flags ?? fs.constants.O_RDONLY) |
433
+ (fs.constants.O_DIRECTORY ?? 0) | (fs.constants.O_NOFOLLOW ?? 0);
434
+ const name = path.basename(params.target);
435
+ // The admitted Root is already this operation's immediate parent. Retain
436
+ // both handles through the same fresh pathname and canonical identity fences.
437
+ const child = binding.mkdirOpenChildBeneath
438
+ ? binding.mkdirOpenChildBeneath(rootOwner.fd, name, 0o777, flags).fd
439
+ : (binding.mkdirChildBeneath(rootOwner.fd, name, 0o777), binding.openBeneath(rootOwner.fd, name, flags).fd);
440
+ const close = captureNativeFdClose(binding);
441
+ const childOwner = __addDisposableResource(env_5, { [Symbol.dispose]() { close(child); } }, false);
442
+ assertRootIdentityCurrentSync(params.root);
443
+ verifyMkdirDirectory(binding, child, params.target);
444
+ verifyMkdirDirectory(binding, rootOwner.fd, params.root.rootReal);
445
+ }
446
+ catch (e_5) {
447
+ env_5.error = e_5;
448
+ env_5.hasError = true;
449
+ }
450
+ finally {
451
+ __disposeResources(env_5);
452
+ }
236
453
  }
237
454
  export async function tryOpenCreateRootNative(params) {
238
- const binding = getNativeBinding();
455
+ const binding = getNativeBinding("openBeneath", "mkdirChildBeneath", "removeStagedFile", "openCreateBeneath");
239
456
  if (!binding?.removeStagedFile || !binding.openCreateBeneath)
240
457
  return unavailable();
241
458
  const access = (params.existingFlags & fs.constants.O_RDWR) ? 0o600 : 0o200;
@@ -253,7 +470,7 @@ export async function tryOpenCreateRootNative(params) {
253
470
  if (creationMode() === undefined)
254
471
  return unavailable();
255
472
  return await withNativeDirectory(params, async (binding, parent, assertCurrent) => {
256
- const env_2 = { stack: [], error: void 0, hasError: false };
473
+ const env_6 = { stack: [], error: void 0, hasError: false };
257
474
  try {
258
475
  params.assertBeforeMutation?.();
259
476
  assertCurrent();
@@ -263,13 +480,21 @@ export async function tryOpenCreateRootNative(params) {
263
480
  // Native creation owns the only O_CREAT dispatch. The subsequent Node open
264
481
  // carries neither O_CREAT nor O_TRUNC and must match this retained inode.
265
482
  const close = captureNativeFdClose(binding);
266
- const fd = binding.openCreateBeneath(parent, path.basename(params.target), params.flags, params.mode & 0o7777);
483
+ let fd;
484
+ try {
485
+ fd = binding.openCreateBeneath(parent, path.basename(params.target), params.flags, params.mode & 0o7777);
486
+ }
487
+ catch (error) {
488
+ if (hasNodeErrorCode(error, "EEXIST") && typeof error === "object" && error !== null)
489
+ nativeCreateCollisions.add(error);
490
+ throw error;
491
+ }
267
492
  let rawOpen = true;
268
493
  const closeRaw = () => { if (rawOpen) {
269
494
  rawOpen = false;
270
495
  close(fd);
271
496
  } };
272
- const owner = __addDisposableResource(env_2, { [Symbol.dispose]: closeRaw }, false);
497
+ const owner = __addDisposableResource(env_6, { [Symbol.dispose]: closeRaw }, false);
273
498
  try {
274
499
  const identity = inspectFileIdentitySync(() => fs.fstatSync(fd, { bigint: true }));
275
500
  if (!identity.isFile() || identity.nlink !== 1n)
@@ -323,26 +548,26 @@ export async function tryOpenCreateRootNative(params) {
323
548
  throw error;
324
549
  }
325
550
  }
326
- catch (e_2) {
327
- env_2.error = e_2;
328
- env_2.hasError = true;
551
+ catch (e_6) {
552
+ env_6.error = e_6;
553
+ env_6.hasError = true;
329
554
  }
330
555
  finally {
331
- __disposeResources(env_2);
556
+ __disposeResources(env_6);
332
557
  }
333
558
  }, async (created) => {
334
- const env_3 = { stack: [], error: void 0, hasError: false };
559
+ const env_7 = { stack: [], error: void 0, hasError: false };
335
560
  try {
336
- const handle = __addDisposableResource(env_3, created.handle, true);
337
- const parent = __addDisposableResource(env_3, { [Symbol.dispose]: created.releaseCreationParent }, false);
561
+ const handle = __addDisposableResource(env_7, created.handle, true);
562
+ const parent = __addDisposableResource(env_7, { [Symbol.dispose]: created.releaseCreationParent }, false);
338
563
  await created.cleanupCreated();
339
564
  }
340
- catch (e_3) {
341
- env_3.error = e_3;
342
- env_3.hasError = true;
565
+ catch (e_7) {
566
+ env_7.error = e_7;
567
+ env_7.hasError = true;
343
568
  }
344
569
  finally {
345
- const result_2 = __disposeResources(env_3);
570
+ const result_2 = __disposeResources(env_7);
346
571
  if (result_2)
347
572
  await result_2;
348
573
  }
@@ -198,10 +198,14 @@ export async function mkdirPathFallback(root, resolved, assertBeforeMutation, re
198
198
  const baseParams = {
199
199
  rootReal: resolved.rootReal, targetPath: resolved.resolved, assertBeforeMutation,
200
200
  rootIdentity: root.rootIdentity,
201
+ synchronousMkdir: !privateMode && !mutationAdmission && !assertBeforeMutation &&
202
+ !getFsSafeTestHooks()?.beforeRootFallbackMutation,
201
203
  rejectSymlinks,
202
204
  private: privateMode,
203
205
  mode: privateMode ? 0o700 : undefined,
204
- beforeComponent: async (componentPath) => await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("mkdir", componentPath),
206
+ beforeComponent: getFsSafeTestHooks()?.beforeRootFallbackMutation
207
+ ? (componentPath) => getFsSafeTestHooks()?.beforeRootFallbackMutation?.("mkdir", componentPath)
208
+ : undefined,
205
209
  };
206
210
  const mutationWalk = mutationAdmission
207
211
  ? mutationWalkOptions(mutationAdmission, (prospectiveDirectory) => prospectiveDirectory, assertBeforeMutation, root.rootReal, resolved.resolved)
@@ -5,17 +5,20 @@ export type RootDirectoryListingPaths = {
5
5
  directory: string;
6
6
  names: Map<string, string>;
7
7
  };
8
+ export type RootDirectoryListingResult = {
9
+ kind: "entry";
10
+ entry: DirEntry;
11
+ identity?: ExactStatIdentity;
12
+ } | {
13
+ kind: "limit";
14
+ name: string;
15
+ } | undefined;
8
16
  export type RootDirectoryListing = {
9
17
  paths: RootDirectoryListingPaths;
10
18
  assertCurrent(): Promise<void>;
11
- next(): Promise<{
12
- kind: "entry";
13
- entry: DirEntry;
14
- identity?: ExactStatIdentity;
15
- } | {
16
- kind: "limit";
17
- name: string;
18
- } | undefined>;
19
+ next(): Promise<RootDirectoryListingResult>;
20
+ /** Only a complete, already-fenced snapshot can be consumed without awaiting. */
21
+ nextSnapshot?: () => RootDirectoryListingResult;
19
22
  [Symbol.asyncDispose](): Promise<void>;
20
23
  };
21
24
  export type RootDirectoryListingOptions = {
@@ -1,3 +1,4 @@
1
+ import { exactFileIdentity, numberStatScalar } from "./exact-identity.js";
1
2
  import { isUtf8 } from "node:buffer";
2
3
  import fsSync from "node:fs";
3
4
  import fs from "node:fs/promises";
@@ -21,6 +22,15 @@ function directoryEntryName(bytes) {
21
22
  }
22
23
  return bytes.toString("utf8");
23
24
  }
25
+ async function readDirectoryNames(directory) {
26
+ const names = await fs.readdir(directory);
27
+ // Invalid UTF-8 always decodes to U+FFFD. Re-read suspect batches as bytes
28
+ // so literal U+FFFD names remain valid without returning a lossy spelling.
29
+ if (names.some(name => name.includes("\ufffd"))) {
30
+ return (await fs.readdir(directory, { encoding: "buffer" })).map(directoryEntryName);
31
+ }
32
+ return names;
33
+ }
24
34
  async function readDirectoryEntryName(handle, synchronous = false) {
25
35
  const entry = synchronous ? handle.readSync() : await handle.read();
26
36
  // Bun returns the raw Buffer directly; Node returns a Dirent whose name is a Buffer.
@@ -37,13 +47,16 @@ export function pathStatFromStats(stat, name) {
37
47
  ? Number(stat.mtimeNs) / 1_000_000
38
48
  : stat.mtime.getTime()
39
49
  : stat.mtimeMs;
40
- const dev = Number(stat.dev), gid = Number(stat.gid), ino = Number(stat.ino);
50
+ const exactIdentity = exactFileIdentity(stat);
51
+ const dev = numberStatScalar(stat.dev), gid = numberStatScalar(stat.gid), ino = numberStatScalar(stat.ino);
41
52
  const isDirectory = stat.isDirectory(), isFile = stat.isFile(), isSymbolicLink = stat.isSymbolicLink();
42
- const mode = Number(stat.mode), nlink = Number(stat.nlink), size = Number(stat.size), uid = Number(stat.uid);
53
+ const mode = numberStatScalar(stat.mode), nlink = numberStatScalar(stat.nlink), size = numberStatScalar(stat.size), uid = numberStatScalar(stat.uid);
43
54
  // Construct named metadata directly, preserving property order without a spread copy.
44
- return name === undefined
55
+ const result = name === undefined
45
56
  ? { dev, gid, ino, isDirectory, isFile, isSymbolicLink, mode, mtimeMs, nlink, size, uid }
46
57
  : { name, dev, gid, ino, isDirectory, isFile, isSymbolicLink, mode, mtimeMs, nlink, size, uid };
58
+ // PathStat's established JSON shape stays numeric. Retain this object or copy the companion explicitly.
59
+ return Object.defineProperty(result, "exactIdentity", { value: exactIdentity });
47
60
  }
48
61
  export async function createRootDirectoryObservationGuard(root, directory) {
49
62
  const guard = await createAsyncDirectoryGuard(directory, { bigint: true });
@@ -207,11 +220,11 @@ async function listGuardedDirectoryPath(root, guard, withFileTypes, receipt) {
207
220
  const beforeObservation = getFsSafeTestHooks()?.beforeRootListObservation;
208
221
  if (beforeObservation)
209
222
  await beforeObservation(guard.realPath, withFileTypes);
210
- const names = (await fs.readdir(guard.realPath, { encoding: "buffer" })).map(directoryEntryName).sort();
223
+ const names = (await readDirectoryNames(guard.realPath)).sort();
211
224
  if (withFileTypes) {
212
225
  // readdir supplies literal child names beneath the already-canonical directory.
213
226
  const prefix = guard.realPath.endsWith(path.sep) ? guard.realPath : `${guard.realPath}${path.sep}`;
214
- entries = names.map(name => pathStatFromStats(fsSync.lstatSync(`${prefix}${name}`), name));
227
+ entries = names.map(name => pathStatFromStats(fsSync.lstatSync(`${prefix}${name}`, { bigint: true }), name));
215
228
  }
216
229
  else {
217
230
  entries = names;
@@ -339,7 +352,7 @@ export async function openRootDirectoryListing(root, directory, options, receipt
339
352
  preparedIndex = 0;
340
353
  while (true) {
341
354
  try {
342
- const entry = pathStatFromStats(fsSync.lstatSync(path.join(guard.realPath, name)), name);
355
+ const entry = pathStatFromStats(fsSync.lstatSync(path.join(guard.realPath, name), { bigint: true }), name);
343
356
  prepared.push(entry);
344
357
  // No later sibling can be observed before a possible recursive descent.
345
358
  if (entry.isDirectory || entry.isSymbolicLink || prepared.length >= (options.metadataBatchSize ?? METADATA_BATCH_SIZE))
@@ -367,18 +380,27 @@ export async function openRootDirectoryListing(root, directory, options, receipt
367
380
  throw error;
368
381
  }
369
382
  };
383
+ const nextSnapshot = snapshot ? () => {
384
+ try {
385
+ options.signal?.throwIfAborted();
386
+ const entry = snapshot[index++];
387
+ if (!entry)
388
+ return;
389
+ return options.admitEntry() ? { kind: "entry", entry } : { kind: "limit", name: entry.name };
390
+ }
391
+ catch (error) {
392
+ throw normalizeDirectoryError(error);
393
+ }
394
+ } : undefined;
370
395
  return {
371
396
  paths,
372
397
  assertCurrent,
398
+ nextSnapshot,
373
399
  async next() {
374
400
  try {
401
+ if (nextSnapshot)
402
+ return nextSnapshot();
375
403
  options.signal?.throwIfAborted();
376
- if (snapshot) {
377
- const entry = snapshot[index++];
378
- if (!entry)
379
- return;
380
- return options.admitEntry() ? { kind: "entry", entry } : { kind: "limit", name: entry.name };
381
- }
382
404
  if (names) {
383
405
  if (preparedIndex >= prepared.length && !pendingFailure && pendingLimit === undefined) {
384
406
  await prepareBatch(names);
@@ -409,7 +431,7 @@ export async function openRootDirectoryListing(root, directory, options, receipt
409
431
  let observed;
410
432
  try {
411
433
  observed = options.exactIdentity ? inspectStatObservationSync(bigint => bigint
412
- ? fsSync.lstatSync(pathname, { bigint: true }) : fsSync.lstatSync(pathname)) : { stat: fsSync.lstatSync(pathname) };
434
+ ? fsSync.lstatSync(pathname, { bigint: true }) : fsSync.lstatSync(pathname)) : { stat: fsSync.lstatSync(pathname, { bigint: true }) };
413
435
  }
414
436
  catch (error) {
415
437
  await assertCurrent();
@@ -0,0 +1,8 @@
1
+ import { type OpenRootFileSyncParams, type RootFileOpenResult } from "./root-file.js";
2
+ export type RootFileOpenBatchSync = {
3
+ openFile(params: OpenRootFileSyncParams): RootFileOpenResult;
4
+ close(): void;
5
+ [Symbol.dispose](): void;
6
+ };
7
+ /** Reuses native Windows root handles; other reads keep their one-shot admission. */
8
+ export declare function createRootFileOpenBatchSync(): RootFileOpenBatchSync;
@@ -0,0 +1,108 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { inspectDirectoryIdentitySync } from "./directory-guard.js";
4
+ import { FsSafeError } from "./errors.js";
5
+ import { getNativeBinding } from "./native.js";
6
+ import { openExactNativeRootSync } from "./native-parent-admission.js";
7
+ import { isExpectedPathError } from "./pinned-open.js";
8
+ import { realpathSync } from "./realpath.js";
9
+ import { resolveReadOpenFlags } from "./read-open-flags.js";
10
+ import { observeCanonicalRoot } from "./root-file-final-admission.js";
11
+ import { openRootFileSync, openRootFileWithRetainedRootSync } from "./root-file.js";
12
+ import { hasWindowsPathAlias } from "./windows-path-alias.js";
13
+ const WINDOWS_ERROR_REPARSE_POINT_ENCOUNTERED = 4395;
14
+ function canRetainRoot(params) {
15
+ return process.platform === "win32" && typeof params.rootPath === "string" &&
16
+ typeof params.absolutePath === "string" && params.ioFs === undefined &&
17
+ params.rootRealPath === undefined && !params.skipLexicalRootCheck &&
18
+ (params.symlinks === "reject" || (params.symlinks === undefined && params.rejectSymlinks !== false)) &&
19
+ /^[a-z]:\\/iu.test(params.rootPath) &&
20
+ !hasWindowsPathAlias(params.rootPath, "filesystem") &&
21
+ !hasWindowsPathAlias(params.absolutePath, "filesystem") &&
22
+ !params.absolutePath.slice(3).split("\\").some(segment => /[. ]$/u.test(segment) || segment.includes("~")) &&
23
+ !params.rootPath.includes("\0") && !params.absolutePath.includes("\0") &&
24
+ path.resolve(params.rootPath) === params.rootPath && path.resolve(params.absolutePath) === params.absolutePath &&
25
+ params.absolutePath.startsWith(params.rootPath.endsWith(path.sep) ? params.rootPath : `${params.rootPath}${path.sep}`);
26
+ }
27
+ function failure(error) {
28
+ return { ok: false, reason: error instanceof FsSafeError ? "validation" : isExpectedPathError(error) ? "path" : "io", error };
29
+ }
30
+ /** Reuses native Windows root handles; other reads keep their one-shot admission. */
31
+ export function createRootFileOpenBatchSync() {
32
+ const roots = new Map();
33
+ let closed = false;
34
+ const close = () => {
35
+ if (closed)
36
+ return;
37
+ closed = true;
38
+ let error;
39
+ let failed = false;
40
+ for (const { owner } of roots.values()) {
41
+ try {
42
+ owner.close();
43
+ }
44
+ catch (cause) {
45
+ if (!failed) {
46
+ failed = true;
47
+ error = cause;
48
+ }
49
+ }
50
+ }
51
+ roots.clear();
52
+ if (failed)
53
+ throw error;
54
+ };
55
+ return Object.freeze({
56
+ openFile(input) {
57
+ if (closed)
58
+ return failure(new FsSafeError("invalid-path", "open batch is closed"));
59
+ const params = {
60
+ absolutePath: input.absolutePath, rootPath: input.rootPath,
61
+ boundaryLabel: input.boundaryLabel, rootRealPath: input.rootRealPath,
62
+ maxBytes: input.maxBytes, rejectHardlinks: input.rejectHardlinks,
63
+ rejectSymlinks: input.rejectSymlinks, symlinks: input.symlinks,
64
+ allowedType: input.allowedType, skipLexicalRootCheck: input.skipLexicalRootCheck,
65
+ ioFs: input.ioFs,
66
+ };
67
+ try {
68
+ if (!canRetainRoot(params))
69
+ return openRootFileSync(params);
70
+ const binding = getNativeBinding("openBeneath");
71
+ const openBeneath = binding?.openBeneath;
72
+ if (!binding || !openBeneath)
73
+ return openRootFileSync(params);
74
+ let retained = roots.get(params.rootPath);
75
+ if (!retained) {
76
+ if (realpathSync(params.rootPath) !== params.rootPath)
77
+ return openRootFileSync(params);
78
+ const observation = observeCanonicalRoot(fs, params.rootPath);
79
+ if (!observation.ok)
80
+ return failure(observation.error);
81
+ const owner = openExactNativeRootSync(binding, { rootPath: observation.path, rootIdentity: observation.identity });
82
+ retained = { owner, observation };
83
+ roots.set(params.rootPath, retained);
84
+ }
85
+ const { owner, observation } = retained;
86
+ inspectDirectoryIdentitySync(observation.path, observation.identity);
87
+ let admittedFd;
88
+ try {
89
+ // Reject reparses before any full-leaf pathname observation can follow them.
90
+ admittedFd = openBeneath.call(binding, owner.fd, path.relative(params.rootPath, params.absolutePath), resolveReadOpenFlags()).fd;
91
+ }
92
+ catch (error) {
93
+ const failure = error;
94
+ if (failure?.code === "ELOOP" || failure?.errno === WINDOWS_ERROR_REPARSE_POINT_ENCOUNTERED) {
95
+ throw new FsSafeError("symlink", "symlink not allowed", { cause: error });
96
+ }
97
+ throw error;
98
+ }
99
+ return openRootFileWithRetainedRootSync(params, observation, admittedFd);
100
+ }
101
+ catch (error) {
102
+ return failure(error);
103
+ }
104
+ },
105
+ close,
106
+ [Symbol.dispose]: close,
107
+ });
108
+ }