@openclaw/fs-safe 0.13.1 → 0.15.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 (256) hide show
  1. package/CHANGELOG.md +64 -1
  2. package/README.md +10 -4
  3. package/dist/advanced.d.ts +1 -1
  4. package/dist/advanced.d.ts.map +1 -1
  5. package/dist/advanced.js +1 -1
  6. package/dist/archive-merge.d.ts.map +1 -1
  7. package/dist/archive-merge.js +113 -46
  8. package/dist/archive-read.d.ts.map +1 -1
  9. package/dist/archive-read.js +4 -4
  10. package/dist/archive-zip-directory.d.ts +4 -0
  11. package/dist/archive-zip-directory.d.ts.map +1 -1
  12. package/dist/archive-zip-directory.js +2 -0
  13. package/dist/archive-zip-entry.d.ts +6 -2
  14. package/dist/archive-zip-entry.d.ts.map +1 -1
  15. package/dist/archive-zip-entry.js +23 -8
  16. package/dist/archive-zip-integrity.d.ts.map +1 -1
  17. package/dist/archive-zip-integrity.js +3 -4
  18. package/dist/archive-zip-loader.d.ts.map +1 -1
  19. package/dist/archive-zip-loader.js +107 -31
  20. package/dist/archive-zip-names.d.ts +1 -0
  21. package/dist/archive-zip-names.d.ts.map +1 -1
  22. package/dist/archive-zip-names.js +6 -0
  23. package/dist/archive.js +6 -5
  24. package/dist/bounded-read-stream.d.ts +0 -1
  25. package/dist/bounded-read-stream.d.ts.map +1 -1
  26. package/dist/bounded-read-stream.js +0 -6
  27. package/dist/copy-publication.d.ts +6 -0
  28. package/dist/copy-publication.d.ts.map +1 -1
  29. package/dist/copy-publication.js +3 -0
  30. package/dist/copy-tree-portable.d.ts.map +1 -1
  31. package/dist/copy-tree-portable.js +44 -24
  32. package/dist/copy.d.ts.map +1 -1
  33. package/dist/copy.js +29 -11
  34. package/dist/directory-mode-owner.js +5 -5
  35. package/dist/file-handle-transfer.d.ts +2 -0
  36. package/dist/file-handle-transfer.d.ts.map +1 -1
  37. package/dist/file-handle-transfer.js +57 -2
  38. package/dist/file-identity.d.ts.map +1 -1
  39. package/dist/file-identity.js +18 -4
  40. package/dist/file-lock-sync-admission.d.ts +19 -0
  41. package/dist/file-lock-sync-admission.d.ts.map +1 -0
  42. package/dist/file-lock-sync-admission.js +93 -0
  43. package/dist/file-lock-sync-root-acquire.d.ts +4 -0
  44. package/dist/file-lock-sync-root-acquire.d.ts.map +1 -0
  45. package/dist/file-lock-sync-root-acquire.js +370 -0
  46. package/dist/file-lock-sync-root-arbitration.d.ts +18 -0
  47. package/dist/file-lock-sync-root-arbitration.d.ts.map +1 -0
  48. package/dist/file-lock-sync-root-arbitration.js +66 -0
  49. package/dist/file-lock-sync-root-held.d.ts +34 -0
  50. package/dist/file-lock-sync-root-held.d.ts.map +1 -0
  51. package/dist/file-lock-sync-root-held.js +393 -0
  52. package/dist/file-lock-sync-root-io.d.ts +44 -0
  53. package/dist/file-lock-sync-root-io.d.ts.map +1 -0
  54. package/dist/file-lock-sync-root-io.js +209 -0
  55. package/dist/file-lock-sync-root-mutation.d.ts +17 -0
  56. package/dist/file-lock-sync-root-mutation.d.ts.map +1 -0
  57. package/dist/file-lock-sync-root-mutation.js +277 -0
  58. package/dist/file-lock-sync-root-options.d.ts +20 -0
  59. package/dist/file-lock-sync-root-options.d.ts.map +1 -0
  60. package/dist/file-lock-sync-root-options.js +58 -0
  61. package/dist/file-lock-sync-root-registration.d.ts +2 -0
  62. package/dist/file-lock-sync-root-registration.d.ts.map +1 -0
  63. package/dist/file-lock-sync-root-registration.js +90 -0
  64. package/dist/file-lock-sync-root.d.ts +36 -0
  65. package/dist/file-lock-sync-root.d.ts.map +1 -0
  66. package/dist/file-lock-sync-root.js +361 -0
  67. package/dist/file-lock-sync-stale-admission.d.ts +24 -0
  68. package/dist/file-lock-sync-stale-admission.d.ts.map +1 -0
  69. package/dist/file-lock-sync-stale-admission.js +205 -0
  70. package/dist/file-lock-sync.d.ts.map +1 -1
  71. package/dist/file-lock-sync.js +245 -205
  72. package/dist/file-store-prune.d.ts.map +1 -1
  73. package/dist/file-store-prune.js +5 -1
  74. package/dist/file-store-sync-directory.d.ts.map +1 -1
  75. package/dist/file-store-sync-directory.js +14 -7
  76. package/dist/file-store-sync-write.js +3 -3
  77. package/dist/file-store.d.ts.map +1 -1
  78. package/dist/file-store.js +47 -12
  79. package/dist/guest-dispatch-python.js +1 -1
  80. package/dist/json-document-store.d.ts.map +1 -1
  81. package/dist/json-document-store.js +22 -15
  82. package/dist/json-durable-queue-ownership.d.ts +0 -1
  83. package/dist/json-durable-queue-ownership.d.ts.map +1 -1
  84. package/dist/json-durable-queue-ownership.js +0 -6
  85. package/dist/native-binding.d.ts +8 -0
  86. package/dist/native-binding.d.ts.map +1 -1
  87. package/dist/native-parent-admission.d.ts +3 -2
  88. package/dist/native-parent-admission.d.ts.map +1 -1
  89. package/dist/native-parent-admission.js +24 -5
  90. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  91. package/dist/native-pinned-write-windows.js +7 -7
  92. package/dist/native-pinned-write.d.ts.map +1 -1
  93. package/dist/native-pinned-write.js +202 -172
  94. package/dist/native-policy-directory-observation.d.ts +12 -0
  95. package/dist/native-policy-directory-observation.d.ts.map +1 -0
  96. package/dist/native-policy-directory-observation.js +61 -0
  97. package/dist/native-policy-parent-windows.d.ts +14 -0
  98. package/dist/native-policy-parent-windows.d.ts.map +1 -0
  99. package/dist/native-policy-parent-windows.js +200 -0
  100. package/dist/native-rename-outcome.d.ts +4 -0
  101. package/dist/native-rename-outcome.d.ts.map +1 -0
  102. package/dist/native-rename-outcome.js +8 -0
  103. package/dist/native-staged-file.d.ts +2 -2
  104. package/dist/native-staged-file.d.ts.map +1 -1
  105. package/dist/native-staged-file.js +42 -40
  106. package/dist/output.d.ts.map +1 -1
  107. package/dist/output.js +12 -8
  108. package/dist/path-prefix.d.ts.map +1 -1
  109. package/dist/path-prefix.js +30 -8
  110. package/dist/path-suffix-aliases.d.ts +2 -0
  111. package/dist/path-suffix-aliases.d.ts.map +1 -1
  112. package/dist/path-suffix-aliases.js +25 -17
  113. package/dist/permission-exec.d.ts +2 -0
  114. package/dist/permission-exec.d.ts.map +1 -1
  115. package/dist/permission-exec.js +150 -21
  116. package/dist/permissions-windows.js +1 -1
  117. package/dist/pinned-mutation-admission.d.ts.map +1 -1
  118. package/dist/pinned-mutation-admission.js +23 -29
  119. package/dist/pinned-mutation-observation.d.ts +7 -2
  120. package/dist/pinned-mutation-observation.d.ts.map +1 -1
  121. package/dist/pinned-mutation-observation.js +81 -18
  122. package/dist/pinned-mutation-shared-route.d.ts +1 -0
  123. package/dist/pinned-mutation-shared-route.d.ts.map +1 -1
  124. package/dist/pinned-mutation-shared-route.js +1 -1
  125. package/dist/pinned-write-types.d.ts +2 -0
  126. package/dist/pinned-write-types.d.ts.map +1 -1
  127. package/dist/private-temp-workspace.d.ts.map +1 -1
  128. package/dist/private-temp-workspace.js +75 -121
  129. package/dist/regular-file.d.ts.map +1 -1
  130. package/dist/regular-file.js +1 -15
  131. package/dist/replace-directory.d.ts.map +1 -1
  132. package/dist/replace-directory.js +256 -18
  133. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  134. package/dist/replace-file-copy-fallback.js +62 -70
  135. package/dist/replace-file-copy-source.d.ts.map +1 -1
  136. package/dist/replace-file-copy-source.js +10 -12
  137. package/dist/replace-file-temp-owner.d.ts +5 -2
  138. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  139. package/dist/replace-file-temp-owner.js +65 -30
  140. package/dist/replace-file.js +6 -6
  141. package/dist/retained-directory-replacement.d.ts +26 -0
  142. package/dist/retained-directory-replacement.d.ts.map +1 -0
  143. package/dist/retained-directory-replacement.js +193 -0
  144. package/dist/root-boundary.d.ts +1 -0
  145. package/dist/root-boundary.d.ts.map +1 -1
  146. package/dist/root-boundary.js +4 -0
  147. package/dist/root-context.d.ts +0 -8
  148. package/dist/root-context.d.ts.map +1 -1
  149. package/dist/root-context.js +0 -3
  150. package/dist/root-create-input.d.ts +6 -0
  151. package/dist/root-create-input.d.ts.map +1 -1
  152. package/dist/root-create-input.js +5 -1
  153. package/dist/root-directory-creation.d.ts.map +1 -1
  154. package/dist/root-directory-creation.js +5 -4
  155. package/dist/root-directory-list.d.ts +1 -0
  156. package/dist/root-directory-list.d.ts.map +1 -1
  157. package/dist/root-directory-list.js +1 -0
  158. package/dist/root-impl.d.ts.map +1 -1
  159. package/dist/root-impl.js +20 -11
  160. package/dist/root-move-noreplace.d.ts.map +1 -1
  161. package/dist/root-move-noreplace.js +2 -2
  162. package/dist/root-path-errors.d.ts +1 -0
  163. package/dist/root-path-errors.d.ts.map +1 -1
  164. package/dist/root-path-errors.js +11 -2
  165. package/dist/root-path-existing.d.ts.map +1 -1
  166. package/dist/root-path-existing.js +12 -37
  167. package/dist/root-path.js +1 -13
  168. package/dist/root-remove.d.ts +1 -0
  169. package/dist/root-remove.d.ts.map +1 -1
  170. package/dist/root-remove.js +4 -0
  171. package/dist/root-walk.d.ts +1 -1
  172. package/dist/root-walk.d.ts.map +1 -1
  173. package/dist/root-walk.js +17 -2
  174. package/dist/root-write-admission.d.ts +0 -2
  175. package/dist/root-write-admission.d.ts.map +1 -1
  176. package/dist/root-write-admission.js +1 -15
  177. package/dist/root-write-compatibility.d.ts +1 -2
  178. package/dist/root-write-compatibility.d.ts.map +1 -1
  179. package/dist/root-write-compatibility.js +17 -68
  180. package/dist/root-write-complete-parent.d.ts.map +1 -1
  181. package/dist/root-write-complete-parent.js +10 -24
  182. package/dist/root-write-lock-binding.d.ts +15 -0
  183. package/dist/root-write-lock-binding.d.ts.map +1 -0
  184. package/dist/root-write-lock-binding.js +162 -0
  185. package/dist/root-write-verification.d.ts.map +1 -1
  186. package/dist/root-write-verification.js +29 -42
  187. package/dist/secret-file.d.ts.map +1 -1
  188. package/dist/secret-file.js +2 -24
  189. package/dist/secret-read-async.d.ts.map +1 -1
  190. package/dist/secret-read-async.js +3 -24
  191. package/dist/secret-read-policy.d.ts +6 -2
  192. package/dist/secret-read-policy.d.ts.map +1 -1
  193. package/dist/secret-read-policy.js +26 -2
  194. package/dist/sibling-temp.d.ts.map +1 -1
  195. package/dist/sibling-temp.js +23 -12
  196. package/dist/sidecar-lock-acquire.d.ts +2 -28
  197. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  198. package/dist/sidecar-lock-acquire.js +288 -199
  199. package/dist/sidecar-lock-admission-context.d.ts +19 -0
  200. package/dist/sidecar-lock-admission-context.d.ts.map +1 -0
  201. package/dist/sidecar-lock-admission-context.js +60 -0
  202. package/dist/sidecar-lock-admission-parser.d.ts +43 -0
  203. package/dist/sidecar-lock-admission-parser.d.ts.map +1 -0
  204. package/dist/sidecar-lock-admission-parser.js +113 -0
  205. package/dist/sidecar-lock-admission.d.ts +35 -0
  206. package/dist/sidecar-lock-admission.d.ts.map +1 -0
  207. package/dist/sidecar-lock-admission.js +7 -0
  208. package/dist/sidecar-lock-reclaim.d.ts +9 -4
  209. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  210. package/dist/sidecar-lock-reclaim.js +80 -25
  211. package/dist/sidecar-lock-stale-admission.d.ts +39 -0
  212. package/dist/sidecar-lock-stale-admission.d.ts.map +1 -0
  213. package/dist/sidecar-lock-stale-admission.js +232 -0
  214. package/dist/sidecar-lock-target.d.ts +8 -0
  215. package/dist/sidecar-lock-target.d.ts.map +1 -0
  216. package/dist/sidecar-lock-target.js +55 -0
  217. package/dist/sidecar-lock.d.ts.map +1 -1
  218. package/dist/sidecar-lock.js +100 -16
  219. package/dist/staged-directory.d.ts +5 -2
  220. package/dist/staged-directory.d.ts.map +1 -1
  221. package/dist/staged-directory.js +37 -1
  222. package/dist/temp-workspace-admission.d.ts.map +1 -1
  223. package/dist/temp-workspace-admission.js +28 -4
  224. package/dist/temp-workspace-descriptor.d.ts.map +1 -1
  225. package/dist/temp-workspace-descriptor.js +9 -27
  226. package/dist/temp-workspace-owner.d.ts.map +1 -1
  227. package/dist/temp-workspace-owner.js +8 -8
  228. package/dist/walk.d.ts +5 -1
  229. package/dist/walk.d.ts.map +1 -1
  230. package/dist/walk.js +19 -6
  231. package/dist/windows-owner.d.ts.map +1 -1
  232. package/dist/windows-owner.js +2 -2
  233. package/docs/advanced.md +1 -1
  234. package/docs/archive.md +36 -9
  235. package/docs/atomic.md +85 -8
  236. package/docs/copy.md +35 -0
  237. package/docs/file-store.md +19 -0
  238. package/docs/guest.md +1 -1
  239. package/docs/json-store.md +5 -0
  240. package/docs/native-helper.md +10 -3
  241. package/docs/native.md +8 -0
  242. package/docs/output.md +6 -0
  243. package/docs/path-prefix.md +10 -0
  244. package/docs/path-suffix-aliases.md +51 -6
  245. package/docs/permissions.md +13 -0
  246. package/docs/public-api.md +3 -2
  247. package/docs/root.md +22 -3
  248. package/docs/sidecar-lock.md +109 -4
  249. package/docs/staged-file.md +7 -3
  250. package/docs/temp.md +20 -3
  251. package/docs/walk.md +67 -1
  252. package/docs/writing.md +11 -1
  253. package/package.json +10 -10
  254. package/dist/darwin-acl.d.ts +0 -4
  255. package/dist/darwin-acl.d.ts.map +0 -1
  256. package/dist/darwin-acl.js +0 -24
@@ -0,0 +1,200 @@
1
+ import fsSync, {} from "node:fs";
2
+ import path from "node:path";
3
+ import { assertDirectoryIdentitySync, inspectDirectoryIdentitySync, } from "./directory-guard.js";
4
+ import { FsSafeError } from "./errors.js";
5
+ import { openNativeParentAdmission } from "./native-parent-admission.js";
6
+ import { captureNativeFdClose } from "./native-binding.js";
7
+ import { inspectNativeDirectoryObservation } from "./native-directory-observation.js";
8
+ import { createPathSegmentRoute, joinPathSegmentRoute } from "./path-segment-route.js";
9
+ import { isSymlinkOpenError } from "./path.js";
10
+ import { checkedMutationDirectory } from "./pinned-mutation-observation.js";
11
+ import { canReuseParentWithMutationAssertion } from "./root-write-lock-binding.js";
12
+ import { inspectFileIdentitySync } from "./strict-file-identity.js";
13
+ function assertParentCurrent(parent) {
14
+ const current = inspectFileIdentitySync(() => fsSync.fstatSync(parent.fd, { bigint: true }), parent.guard.stat);
15
+ assertDirectoryIdentitySync(parent.guard.dir, {
16
+ dev: parent.guard.stat.dev,
17
+ ino: parent.guard.stat.ino,
18
+ realPath: parent.guard.realPath,
19
+ });
20
+ return current;
21
+ }
22
+ function closeAfterFailure(closeFd, fd) {
23
+ if (fd === undefined)
24
+ return;
25
+ try {
26
+ closeFd(fd);
27
+ }
28
+ catch {
29
+ // Windows retains the operation failure while attempting every owned close.
30
+ }
31
+ }
32
+ export async function capturePolicyAwareWindowsParent(binding, params, rootAdmission) {
33
+ const closeFd = captureNativeFdClose(binding);
34
+ let session;
35
+ function parentObservation(fd, guard) {
36
+ return checkedMutationDirectory(guard.dir, guard.realPath, guard.stat, typeof binding.observeDirectory === "function" ? () => {
37
+ const stat = inspectFileIdentitySync(() => fsSync.fstatSync(fd, { bigint: true }), guard.stat);
38
+ let observed;
39
+ try {
40
+ observed = inspectNativeDirectoryObservation(binding, guard.dir, stat);
41
+ }
42
+ catch (error) {
43
+ if (error?.code === "OBSERVATION_UNAVAILABLE")
44
+ return undefined;
45
+ throw error;
46
+ }
47
+ return { canonicalPath: observed.realPath,
48
+ identity: { dev: stat.dev, ino: stat.ino, mode: stat.mode, nlink: stat.nlink } };
49
+ } : undefined);
50
+ }
51
+ async function openParent(fd, parentPath, relativePath) {
52
+ try {
53
+ const admitted = await openNativeParentAdmission(binding, {
54
+ ...rootAdmission,
55
+ root: { fd },
56
+ rootPath: parentPath,
57
+ // Policy fences retain every identity bit even with a legacy numeric root.
58
+ exactRoot: true,
59
+ }, relativePath, "native-directory");
60
+ const guard = { ...admitted.guard, stat: admitted.guard.stat };
61
+ return { fd: admitted.fd, guard, observation: parentObservation(admitted.fd, guard) };
62
+ }
63
+ catch (error) {
64
+ if (!isSymlinkOpenError(error))
65
+ throw error;
66
+ throw new FsSafeError(params.mutationAdmission.rejectParentSymlinks ? "symlink" : "path-mismatch", "native write parent changed during policy admission", { cause: error instanceof Error ? error : undefined });
67
+ }
68
+ }
69
+ async function authorize(targetPath, mutationPath = targetPath, phase = "parent") {
70
+ const request = Object.freeze({ targetPath, mutationPath, phase });
71
+ return await (session ? session.authorize(request) : params.mutationAdmission.authorize(request));
72
+ }
73
+ // Existing complete parents need one open and one target admission. Exact
74
+ // parent denies apply only to creation, never to merely using a directory.
75
+ let complete;
76
+ try {
77
+ complete = await openParent(rootAdmission.root.fd, params.rootPath, params.relativeParentPath);
78
+ }
79
+ catch (error) {
80
+ if (error?.code !== "ENOENT" || !params.mkdir)
81
+ throw error;
82
+ }
83
+ if (complete) {
84
+ try {
85
+ await authorize(path.join(complete.guard.realPath, params.basename));
86
+ assertParentCurrent(complete);
87
+ return complete;
88
+ }
89
+ catch (error) {
90
+ closeAfterFailure(closeFd, complete.fd);
91
+ throw error;
92
+ }
93
+ }
94
+ const segments = params.relativeParentPath.split("/").filter(Boolean);
95
+ const route = createPathSegmentRoute(segments);
96
+ const rootStat = inspectDirectoryIdentitySync(params.rootPath, inspectFileIdentitySync(() => fsSync.fstatSync(rootAdmission.root.fd, { bigint: true })));
97
+ let current = {
98
+ fd: rootAdmission.root.fd,
99
+ guard: { dir: params.rootPath, realPath: params.rootPath, stat: rootStat },
100
+ observation: parentObservation(rootAdmission.root.fd, {
101
+ dir: params.rootPath, realPath: params.rootPath, stat: rootStat,
102
+ }),
103
+ };
104
+ let ownedFd;
105
+ const initialTarget = joinPathSegmentRoute(params.rootPath, route, 0, params.basename);
106
+ if (canReuseParentWithMutationAssertion(params.assertBeforeMutation, params.rootPath, initialTarget)) {
107
+ session = params.mutationAdmission?.beginNativeParentWalk?.() ??
108
+ params.mutationAdmission?.beginSharedParentWalk?.();
109
+ if (session && session.retainedTargetPath !== initialTarget) {
110
+ session.dispose();
111
+ session = undefined;
112
+ }
113
+ }
114
+ try {
115
+ for (let index = 0; index < segments.length; index += 1) {
116
+ const segment = segments[index];
117
+ const parentPath = current.guard.realPath;
118
+ let child;
119
+ let createdByMkdir = false;
120
+ let createReceipt;
121
+ try {
122
+ child = await openParent(current.fd, parentPath, segment);
123
+ }
124
+ catch (error) {
125
+ if (error?.code !== "ENOENT")
126
+ throw error;
127
+ // The request admits both the prospective leaf and this missing direct
128
+ // child. A deeper denial can leave already admitted ancestors intact.
129
+ const targetPath = session?.retainedTargetPath ??
130
+ joinPathSegmentRoute(parentPath, route, index, params.basename);
131
+ const mutationPath = path.join(parentPath, segment);
132
+ const request = Object.freeze({ targetPath, mutationPath, phase: "parent-create" });
133
+ createReceipt = session?.tryAuthorizeAtParent(request, current.observation);
134
+ if (!createReceipt) {
135
+ createReceipt = await authorize(targetPath, mutationPath, "parent-create");
136
+ assertParentCurrent(current);
137
+ }
138
+ if (params.assertBeforeMutation) {
139
+ // Opaque callbacks retain their post-callback descriptor/path fence.
140
+ params.assertBeforeMutation();
141
+ assertParentCurrent(current);
142
+ }
143
+ // Synchronous session admission already ends at the exact parent fence.
144
+ const mkdirChild = binding.mkdirChildBeneath;
145
+ if (typeof mkdirChild !== "function") {
146
+ throw new FsSafeError("helper-unavailable", "native direct-child parent creation is unavailable");
147
+ }
148
+ createdByMkdir = mkdirChild.call(binding, current.fd, segment, 0o777) === true;
149
+ child = await openParent(current.fd, parentPath, segment);
150
+ }
151
+ try {
152
+ const targetPath = joinPathSegmentRoute(child.guard.realPath, route, index + 1, params.basename);
153
+ if (session && targetPath !== session.retainedTargetPath) {
154
+ session.dispose();
155
+ session = undefined;
156
+ }
157
+ let evidence;
158
+ if (session && createdByMkdir && createReceipt) {
159
+ try {
160
+ evidence = Object.freeze({
161
+ admission: createReceipt,
162
+ parent: parentObservation(current.fd, { ...current.guard,
163
+ stat: inspectFileIdentitySync(() => fsSync.fstatSync(current.fd, { bigint: true }), current.guard.stat),
164
+ }),
165
+ child: child.observation,
166
+ });
167
+ }
168
+ catch {
169
+ // Incomplete optional evidence returns to ordered admission below.
170
+ }
171
+ }
172
+ // Advancement refreshes both named directories after all Root/policy
173
+ // observations; these receipts supply expectations, not cached authority.
174
+ const advanced = evidence && session.advanceCreatedDirectory(evidence);
175
+ if (!advanced) {
176
+ await authorize(targetPath);
177
+ assertParentCurrent(child);
178
+ }
179
+ }
180
+ catch (error) {
181
+ closeAfterFailure(closeFd, child.fd);
182
+ throw error;
183
+ }
184
+ const previousFd = ownedFd;
185
+ current = child;
186
+ ownedFd = child.fd;
187
+ if (previousFd !== undefined)
188
+ closeFd(previousFd);
189
+ }
190
+ if (ownedFd === undefined) {
191
+ throw new FsSafeError("path-mismatch", "native write parent admission did not complete");
192
+ }
193
+ ownedFd = undefined;
194
+ return current;
195
+ }
196
+ finally {
197
+ session?.dispose();
198
+ closeAfterFailure(closeFd, ownedFd);
199
+ }
200
+ }
@@ -0,0 +1,4 @@
1
+ export declare const NATIVE_RENAME_SOURCE_IDENTITY_MISMATCH = "FS_SAFE_INTERNAL_RENAME_SOURCE_IDENTITY_MISMATCH";
2
+ export type NativeRenameFailureOutcome = "uncommitted" | "indeterminate";
3
+ export declare function classifyNativeRenameFailure(error: unknown): NativeRenameFailureOutcome;
4
+ //# sourceMappingURL=native-rename-outcome.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native-rename-outcome.d.ts","sourceRoot":"","sources":["../src/native-rename-outcome.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,sCAAsC,qDACC,CAAC;AAErD,MAAM,MAAM,0BAA0B,GAAG,aAAa,GAAG,eAAe,CAAC;AAEzE,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,OAAO,GAAG,0BAA0B,CAMtF"}
@@ -0,0 +1,8 @@
1
+ export const NATIVE_RENAME_SOURCE_IDENTITY_MISMATCH = "FS_SAFE_INTERNAL_RENAME_SOURCE_IDENTITY_MISMATCH";
2
+ export function classifyNativeRenameFailure(error) {
3
+ const code = error?.code ?? "";
4
+ // Ordinary errno can follow a committed remote rename whose reply was lost.
5
+ return code === NATIVE_RENAME_SOURCE_IDENTITY_MISMATCH
6
+ ? "uncommitted"
7
+ : "indeterminate";
8
+ }
@@ -8,8 +8,8 @@ export type NativeStagingBinding = NativeBinding & Required<Pick<NativeBinding,
8
8
  export declare function assertNativeStaging(binding: NativeBinding): asserts binding is NativeStagingBinding;
9
9
  declare class NativeStagedFile implements StagedFile {
10
10
  #private;
11
- constructor(binding: NativeStagingBinding, parentFd: number, closeParentFd: (fd: number) => void, directory: StagedFileReceipt["directory"], portableNames: boolean, publishedMode: number, sync: boolean, assertBeforeMutation?: () => void);
12
- static create(binding: NativeStagingBinding, parentFd: number, closeParentFd: (fd: number) => void, directory: StagedFileReceipt["directory"], input: PinnedWriteInput, mode: number, maxBytes?: number, portableNames?: boolean, sync?: boolean, assertBeforeMutation?: () => void): Promise<NativeStagedFile>;
11
+ constructor(binding: NativeStagingBinding, parentFd: number, closeParentFd: (fd: number) => void, directory: StagedFileReceipt["directory"], portableNames: boolean, publishedMode: number, sync: boolean, assertBeforeMutation?: () => void, name?: string);
12
+ static create(binding: NativeStagingBinding, parentFd: number, closeParentFd: (fd: number) => void, directory: StagedFileReceipt["directory"], input: PinnedWriteInput, mode: number, maxBytes?: number, portableNames?: boolean, sync?: boolean, assertBeforeMutation?: () => void, exclusiveBasename?: string): Promise<NativeStagedFile>;
13
13
  static write(binding: NativeStagingBinding, parentFd: number, closeParentFd: (fd: number) => void, directory: StagedFileReceipt["directory"], params: PinnedWriteParams, parentGuard: AnyAsyncDirectoryGuard): Promise<FileIdentityStat>;
14
14
  get receipt(): StagedFileReceipt;
15
15
  assertCurrent(): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"native-staged-file.d.ts","sourceRoot":"","sources":["../src/native-staged-file.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"native-staged-file.d.ts","sourceRoot":"","sources":["../src/native-staged-file.ts"],"names":[],"mappings":"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"}
@@ -62,6 +62,7 @@ import { writeNativeInput } from "./native-operations.js";
62
62
  import { assertNativeCopyCompleted, createNativeCopyFile } from "./copy-file-input.js";
63
63
  import { assertStagedDirectoryCurrent, openStagedDirectory } from "./staged-directory.js";
64
64
  import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
65
+ import { classifyNativeRenameFailure } from "./native-rename-outcome.js";
65
66
  export function assertNativeStaging(binding) {
66
67
  if ([
67
68
  binding.closeOwnedFd,
@@ -81,11 +82,6 @@ function assertBasename(name, portable) {
81
82
  }
82
83
  }
83
84
  const NOT_PUBLISHED = Object.freeze({ status: "not-published" });
84
- const UNCOMMITTED_RENAME_ERRORS = new Set([
85
- "EACCES", "EBADF", "EBUSY", "EEXIST", "EINVAL", "EISDIR", "ELOOP", "EMLINK",
86
- "ENAMETOOLONG", "ENOENT", "ENOSPC", "ENOSYS", "ENOTDIR", "ENOTEMPTY", "ENOTSUP",
87
- "EPERM", "EROFS", "ETXTBSY", "EXDEV",
88
- ]);
89
85
  function failure(error, details) {
90
86
  const code = error instanceof FsSafeError
91
87
  ? error.code
@@ -102,11 +98,13 @@ class NativeStagedFile {
102
98
  #publishedMode;
103
99
  #sync;
104
100
  #assertBeforeMutation;
105
- #name = `.fs-safe-${randomUUID()}.tmp`;
101
+ #name;
106
102
  #state = { status: "open", publication: NOT_PUBLISHED };
107
103
  #receipt;
108
104
  #rejectFinalSymlink = false;
109
- constructor(binding, parentFd, closeParentFd, directory, portableNames, publishedMode, sync, assertBeforeMutation) {
105
+ constructor(binding, parentFd, closeParentFd, directory, portableNames, publishedMode, sync, assertBeforeMutation, name = `.fs-safe-${randomUUID()}.tmp`) {
106
+ assertBasename(name, portableNames);
107
+ this.#name = name;
110
108
  this.#closeFd = captureNativeFdClose(binding);
111
109
  this.#binding = binding;
112
110
  this.#parentFd = parentFd;
@@ -121,10 +119,10 @@ class NativeStagedFile {
121
119
  // The supplied closer owns parentFd even on construction or preparation failure.
122
120
  static async create(binding, parentFd, closeParentFd, directory, input, mode, maxBytes,
123
121
  // Public staging uses portable names; existing POSIX writes accept literal names.
124
- portableNames = true, sync = true, assertBeforeMutation) {
122
+ portableNames = true, sync = true, assertBeforeMutation, exclusiveBasename) {
125
123
  let staged;
126
124
  try {
127
- staged = new NativeStagedFile(binding, parentFd, closeParentFd, directory, portableNames, mode, sync, assertBeforeMutation);
125
+ staged = new NativeStagedFile(binding, parentFd, closeParentFd, directory, portableNames, mode, sync, assertBeforeMutation, exclusiveBasename);
128
126
  }
129
127
  catch (error) {
130
128
  try {
@@ -141,13 +139,20 @@ class NativeStagedFile {
141
139
  static async write(binding, parentFd, closeParentFd, directory, params, parentGuard) {
142
140
  const env_1 = { stack: [], error: void 0, hasError: false };
143
141
  try {
142
+ const exclusive = params.overwrite === false && params.input.kind === "buffer" && params.input.stageBeforePublish === false;
144
143
  // This owner never escapes. Only the internal verifier borrows its fd;
145
144
  // public descriptor methods remain await-free and cannot race disposal.
146
- const staged = __addDisposableResource(env_1, await NativeStagedFile.create(binding, parentFd, closeParentFd, directory, params.input, params.mode, params.maxBytes, false, params.sync, params.assertBeforeMutation), true);
145
+ const staged = __addDisposableResource(env_1, await NativeStagedFile.create(binding, parentFd, closeParentFd, directory, params.input, params.mode, params.maxBytes, false, params.sync, params.assertBeforeMutation, exclusive ? params.basename : undefined), true);
147
146
  staged.#rejectFinalSymlink = params.rejectFinalSymlink === true;
148
147
  if (params.input.kind === "file")
149
148
  await params.input.verifySource();
150
- const published = await staged.publish(params.basename, { overwrite: params.overwrite !== false }, params.onPublished);
149
+ if (exclusive) {
150
+ staged.#assertCurrent();
151
+ params.assertBeforeMutation?.();
152
+ }
153
+ const published = exclusive
154
+ ? staged.#completePublication(params.basename, false, params.onPublished)
155
+ : await staged.publish(params.basename, { overwrite: params.overwrite !== false }, params.onPublished);
151
156
  const identity = published.staged.identity;
152
157
  await params.verifyPublished?.(staged.#file(), identity, parentGuard);
153
158
  return { dev: identity.dev, ino: identity.ino };
@@ -279,39 +284,13 @@ class NativeStagedFile {
279
284
  }
280
285
  }
281
286
  catch (error) {
282
- // Unknown rename errors can be indeterminate on remote filesystems.
283
- if (!UNCOMMITTED_RENAME_ERRORS.has(error?.code ?? "")) {
287
+ // Only explicit pre-dispatch provenance can rule out a committed rename.
288
+ if (classifyNativeRenameFailure(error) === "indeterminate") {
284
289
  state.publication = Object.freeze({ status: "indeterminate", basename, overwrite });
285
290
  }
286
291
  throw error;
287
292
  }
288
- // Commit is recorded synchronously before any post-rename operation.
289
- const receipt = Object.freeze({
290
- status: "published",
291
- staged: this.receipt,
292
- basename,
293
- overwrite,
294
- });
295
- state.publication = receipt;
296
- onPublished?.(receipt.staged.identity);
297
- const stagedMode = this.#assertNamed(basename);
298
- assertStagedDirectoryCurrent(this.#directory);
299
- // Keep contents private until the published name passes its identity
300
- // fence. Mode changes use the owned fd, including for final mode 000.
301
- const fd = this.#file();
302
- if (this.#publishedMode !== 0o600 || stagedMode !== 0o600)
303
- fs.fchmodSync(fd, this.#publishedMode);
304
- // With sync enabled, content was synced before rename. A lost chmod leaves staged 0600,
305
- // no wider than modes retaining owner rw. Restrictive modes and observed
306
- // widening of the stage still need the permission correction made durable.
307
- if (this.#sync && ((this.#publishedMode & 0o600) !== 0o600 || (stagedMode & ~this.#publishedMode) !== 0)) {
308
- syncFileBestEffortSync(fd);
309
- }
310
- if (this.#sync)
311
- syncFileBestEffortSync(this.#parentFd);
312
- this.#assertNamed(basename);
313
- assertStagedDirectoryCurrent(this.#directory);
314
- return receipt;
293
+ return this.#completePublication(basename, overwrite, onPublished);
315
294
  }
316
295
  catch (error) {
317
296
  if (error instanceof MutationAuthorityError)
@@ -323,6 +302,29 @@ class NativeStagedFile {
323
302
  throw failure(error, { phase: "publish", publication });
324
303
  }
325
304
  }
305
+ #completePublication(basename, overwrite, onPublished) {
306
+ // Record complete content before fallible post-publication verification.
307
+ const receipt = Object.freeze({ status: "published", staged: this.receipt, basename, overwrite });
308
+ this.#open().publication = receipt;
309
+ onPublished?.(receipt.staged.identity);
310
+ const stagedMode = this.#assertNamed(basename);
311
+ assertStagedDirectoryCurrent(this.#directory);
312
+ // Keep contents private until the name passes its identity fence. Mode
313
+ // changes use the owned fd, including for final mode 000.
314
+ const fd = this.#file();
315
+ if (this.#publishedMode !== 0o600 || stagedMode !== 0o600)
316
+ fs.fchmodSync(fd, this.#publishedMode);
317
+ // Content was synced during preparation. A lost chmod leaves 0600, no wider
318
+ // than modes retaining owner rw; restrictive modes still need a durable correction.
319
+ if (this.#sync && ((this.#publishedMode & 0o600) !== 0o600 || (stagedMode & ~this.#publishedMode) !== 0)) {
320
+ syncFileBestEffortSync(fd);
321
+ }
322
+ if (this.#sync)
323
+ syncFileBestEffortSync(this.#parentFd);
324
+ this.#assertNamed(basename);
325
+ assertStagedDirectoryCurrent(this.#directory);
326
+ return receipt;
327
+ }
326
328
  #finalize() {
327
329
  if (this.#state.status === "closed") {
328
330
  return this.#state;
@@ -1 +1 @@
1
- {"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../src/output.ts"],"names":[],"mappings":"AAWA,MAAM,MAAM,wBAAwB,CAAC,CAAC,GAAG,IAAI,IAAI;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IAClC,uEAAuE;IACvE,iBAAiB,CAAC,EAAE,mBAAmB,CAAC;IACxC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,uBAAuB,CAAC,CAAC,GAAG,IAAI,IAAI;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,CAAC,CAAC;CACX,CAAC;AAyDF,wBAAsB,2BAA2B,CAAC,CAAC,GAAG,IAAI,EACxD,OAAO,EAAE,wBAAwB,CAAC,CAAC,CAAC,GACnC,OAAO,CAAC,uBAAuB,CAAC,CAAC,CAAC,CAAC,CA2DrC"}
1
+ {"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../src/output.ts"],"names":[],"mappings":"AAWA,MAAM,MAAM,wBAAwB,CAAC,CAAC,GAAG,IAAI,IAAI;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IAClC,uEAAuE;IACvE,iBAAiB,CAAC,EAAE,mBAAmB,CAAC;IACxC,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,uBAAuB,CAAC,CAAC,GAAG,IAAI,IAAI;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,CAAC,CAAC;CACX,CAAC;AAyDF,wBAAsB,2BAA2B,CAAC,CAAC,GAAG,IAAI,EACxD,OAAO,EAAE,wBAAwB,CAAC,CAAC,CAAC,GACnC,OAAO,CAAC,uBAAuB,CAAC,CAAC,CAAC,CAAC,CA+DrC"}
package/dist/output.js CHANGED
@@ -57,6 +57,10 @@ export async function writeExternalFileWithinRoot(options) {
57
57
  const requestedTargetPath = options.path;
58
58
  assertNoWindowsPathAlias(path.dirname(requestedTargetPath), "filesystem", "output target parent uses a Windows filesystem namespace alias");
59
59
  const producerIsolation = options.producerIsolation;
60
+ const write = options.write;
61
+ const mode = options.mode;
62
+ const staging = options.staging;
63
+ const fallbackFileName = options.fallbackFileName;
60
64
  const targetRoot = await root(rootDir);
61
65
  if (requestedTargetPath.length === 0) {
62
66
  throw new FsSafeError("invalid-path", "target path is required");
@@ -68,11 +72,11 @@ export async function writeExternalFileWithinRoot(options) {
68
72
  targetPath: requestedTargetPath,
69
73
  });
70
74
  assertFileTargetPath(rawTargetPath);
71
- const targetPath = sanitizedTargetPath(rawTargetPath, options.fallbackFileName);
75
+ const targetPath = sanitizedTargetPath(rawTargetPath, fallbackFileName);
72
76
  assertNoWindowsPathAlias(targetPath, "filesystem", "output target uses a Windows filesystem namespace alias");
73
77
  const finalPath = await targetRoot.resolve(targetPath);
74
78
  assertNoWindowsPathAlias(finalPath, "filesystem", "output target uses a Windows filesystem namespace alias");
75
- if (options.staging === "sibling") {
79
+ if (staging === "sibling") {
76
80
  const parentPath = path.dirname(targetPath);
77
81
  if (parentPath !== ".") {
78
82
  await targetRoot.mkdir(parentPath);
@@ -80,23 +84,23 @@ export async function writeExternalFileWithinRoot(options) {
80
84
  const siblingFinalPath = await targetRoot.resolve(targetPath);
81
85
  const result = await writeExternalFileViaSibling({
82
86
  finalPath: siblingFinalPath,
83
- write: options.write,
87
+ write,
84
88
  producerIsolation,
85
- fallbackFileName: options.fallbackFileName,
89
+ fallbackFileName,
86
90
  maxBytes,
87
- mode: options.mode,
91
+ mode,
88
92
  });
89
93
  return { path: siblingFinalPath, result };
90
94
  }
91
95
  const staged = await tempFile({
92
96
  prefix: "fs-safe-output",
93
- fileName: tempFileNameForTarget(targetPath, options.fallbackFileName),
97
+ fileName: tempFileNameForTarget(targetPath, fallbackFileName),
94
98
  });
95
99
  try {
96
- const result = await options.write(staged.path);
100
+ const result = await Function.prototype.call.call(write, options, staged.path);
97
101
  await targetRoot.copyIn(targetPath, staged.path, {
98
102
  maxBytes,
99
- mode: options.mode,
103
+ mode,
100
104
  mkdir: true,
101
105
  sourceHardlinks: "reject",
102
106
  });
@@ -1 +1 @@
1
- {"version":3,"file":"path-prefix.d.ts","sourceRoot":"","sources":["../src/path-prefix.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,kBAAkB,EAAE,SAAS,MAAM,EAAE,CAAC;CAChD,CAAC;AAkBF,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,kBAAkB,CAsDvE"}
1
+ {"version":3,"file":"path-prefix.d.ts","sourceRoot":"","sources":["../src/path-prefix.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,kBAAkB,EAAE,SAAS,MAAM,EAAE,CAAC;CAChD,CAAC;AAuBF,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,kBAAkB,CAuEvE"}
@@ -19,15 +19,23 @@ function resolutionError(code, absolutePath) {
19
19
  code, syscall: "resolvePathPrefix", path: absolutePath,
20
20
  });
21
21
  }
22
+ // Leave ample room for ordinary shallow paths to retain the legacy small-array
23
+ // path. At 32 components, repeated front removal can reindex at most 496 array
24
+ // positions per constructed queue; longer queues use a forward cursor instead.
25
+ const SHIFT_QUEUE_COMPONENT_LIMIT = 32;
22
26
  export function resolvePathPrefixSync(input) {
23
27
  assertNoNulPathInput(input);
24
28
  const absolutePath = absolutePathWithRawSegments(input);
25
29
  let resolved = rawRoot(absolutePath);
26
- const remaining = absolutePath.slice(resolved.length).split(path.sep);
30
+ let remaining = absolutePath.slice(resolved.length).split(path.sep);
31
+ let shiftQueue = remaining.length <= SHIFT_QUEUE_COMPONENT_LIMIT;
32
+ let nextSegment = 0;
33
+ let remainingCount = remaining.length;
27
34
  const visitedStates = new Set();
28
35
  let symlinkHops = 0;
29
- while (remaining.length > 0) {
30
- const segment = remaining.shift();
36
+ while (remainingCount > 0) {
37
+ const segment = shiftQueue ? remaining.shift() : remaining[nextSegment++];
38
+ remainingCount--;
31
39
  if (segment === "")
32
40
  continue;
33
41
  if (segment === "." || segment === "..") {
@@ -51,32 +59,46 @@ export function resolvePathPrefixSync(input) {
51
59
  return {
52
60
  absolutePath,
53
61
  existingPath: realpathSync.native(pathForWindowsFilesystem(resolved)),
54
- unresolvedSegments: [segment, ...remaining],
62
+ unresolvedSegments: shiftQueue ? [segment, ...remaining] : remaining.slice(nextSegment - 1),
55
63
  };
56
64
  }
57
65
  if (!stat.isSymbolicLink()) {
58
- if (!stat.isDirectory() && remaining.length > 0)
66
+ if (!stat.isDirectory() && remainingCount > 0)
59
67
  throw resolutionError("ENOTDIR", absolutePath);
60
68
  resolved = candidate;
61
69
  continue;
62
70
  }
63
- const state = JSON.stringify([String(stat.dev), String(stat.ino), candidate, remaining]);
71
+ const pendingSuffix = shiftQueue ? remaining : remaining.slice(nextSegment);
72
+ const state = JSON.stringify([String(stat.dev), String(stat.ino), candidate, pendingSuffix]);
64
73
  if (symlinkHops >= 64 || visitedStates.has(state))
65
74
  throw resolutionError("ELOOP", absolutePath);
66
75
  visitedStates.add(state);
67
76
  symlinkHops++;
68
77
  const target = fs.readlinkSync(candidate);
69
78
  const rawTarget = path.sep === "\\" ? target.replaceAll("/", "\\") : target;
79
+ let targetSegments;
70
80
  if (path.isAbsolute(rawTarget)) {
71
81
  const targetRoot = rawRoot(rawTarget);
72
82
  // A rooted Windows link target uses the link's drive/share, not cwd's.
73
83
  resolved = path.sep === "\\" && targetRoot === "\\" ? rawRoot(resolved) : targetRoot;
74
- remaining.unshift(...rawTarget.slice(targetRoot.length).split(path.sep));
84
+ targetSegments = rawTarget.slice(targetRoot.length).split(path.sep);
75
85
  }
76
86
  else {
77
87
  // A target's parent traversal applies after resolving its preceding links.
78
- remaining.unshift(...rawTarget.split(path.sep));
88
+ targetSegments = rawTarget.split(path.sep);
79
89
  }
90
+ const expandedCount = targetSegments.length + pendingSuffix.length;
91
+ if (expandedCount <= SHIFT_QUEUE_COMPONENT_LIMIT) {
92
+ // The spread is bounded by the same fixed limit as subsequent shifts.
93
+ remaining = pendingSuffix;
94
+ remaining.unshift(...targetSegments);
95
+ }
96
+ else {
97
+ remaining = targetSegments.concat(pendingSuffix);
98
+ }
99
+ remainingCount = remaining.length;
100
+ shiftQueue = remainingCount <= SHIFT_QUEUE_COMPONENT_LIMIT;
101
+ nextSegment = 0;
80
102
  }
81
103
  return { absolutePath, existingPath: realpathSync.native(pathForWindowsFilesystem(resolved)), unresolvedSegments: [] };
82
104
  }
@@ -2,6 +2,8 @@ export type ProbePathSuffixAliasesOptions = {
2
2
  directory: string;
3
3
  left: string;
4
4
  right: string;
5
+ /** Maximum relative component count; defaults to 32. */
6
+ maxDepth?: number;
5
7
  /** Synchronous caller policy for differing NFC pairs not equivalent under ASCII case folding. */
6
8
  shouldProbeCaseVariants?: (leftNfc: string, rightNfc: string) => boolean;
7
9
  };
@@ -1 +1 @@
1
- {"version":3,"file":"path-suffix-aliases.d.ts","sourceRoot":"","sources":["../src/path-suffix-aliases.ts"],"names":[],"mappings":"AAeA,MAAM,MAAM,6BAA6B,GAAG;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,iGAAiG;IACjG,uBAAuB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;CAC1E,CAAC;AA6UF,4FAA4F;AAC5F,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,6BAA6B,GAAG,OAAO,GAAG,SAAS,CA8CtG"}
1
+ {"version":3,"file":"path-suffix-aliases.d.ts","sourceRoot":"","sources":["../src/path-suffix-aliases.ts"],"names":[],"mappings":"AAeA,MAAM,MAAM,6BAA6B,GAAG;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iGAAiG;IACjG,uBAAuB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;CAC1E,CAAC;AAkVF,4FAA4F;AAC5F,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,6BAA6B,GAAG,OAAO,GAAG,SAAS,CAmDtG"}