@openclaw/fs-safe 0.4.7 → 0.5.1

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 (247) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +82 -43
  3. package/dist/archive-entry.d.ts.map +1 -1
  4. package/dist/archive-entry.js +5 -4
  5. package/dist/archive-errors.d.ts +11 -0
  6. package/dist/archive-errors.d.ts.map +1 -0
  7. package/dist/archive-errors.js +16 -0
  8. package/dist/archive-input.d.ts +12 -0
  9. package/dist/archive-input.d.ts.map +1 -0
  10. package/dist/archive-input.js +72 -0
  11. package/dist/archive-kind.d.ts +1 -1
  12. package/dist/archive-kind.d.ts.map +1 -1
  13. package/dist/archive-kind.js +18 -0
  14. package/dist/archive-limits.d.ts +10 -0
  15. package/dist/archive-limits.d.ts.map +1 -1
  16. package/dist/archive-limits.js +18 -0
  17. package/dist/archive-native.d.ts +18 -0
  18. package/dist/archive-native.d.ts.map +1 -0
  19. package/dist/archive-native.js +115 -0
  20. package/dist/archive-options.d.ts +21 -0
  21. package/dist/archive-options.d.ts.map +1 -0
  22. package/dist/archive-options.js +1 -0
  23. package/dist/archive-policy.d.ts +20 -0
  24. package/dist/archive-policy.d.ts.map +1 -0
  25. package/dist/archive-policy.js +29 -0
  26. package/dist/archive-read.d.ts +6 -0
  27. package/dist/archive-read.d.ts.map +1 -0
  28. package/dist/archive-read.js +200 -0
  29. package/dist/archive-staging.d.ts +2 -5
  30. package/dist/archive-staging.d.ts.map +1 -1
  31. package/dist/archive-staging.js +19 -10
  32. package/dist/archive-tar-meta.d.ts +6 -0
  33. package/dist/archive-tar-meta.d.ts.map +1 -0
  34. package/dist/archive-tar-meta.js +143 -0
  35. package/dist/archive-tar-runtime.d.ts +43 -0
  36. package/dist/archive-tar-runtime.d.ts.map +1 -0
  37. package/dist/archive-tar-runtime.js +8 -0
  38. package/dist/archive-tar.d.ts +5 -1
  39. package/dist/archive-tar.d.ts.map +1 -1
  40. package/dist/archive-tar.js +24 -6
  41. package/dist/archive-zip-entry.d.ts +15 -0
  42. package/dist/archive-zip-entry.d.ts.map +1 -0
  43. package/dist/archive-zip-entry.js +17 -0
  44. package/dist/archive.d.ts +7 -17
  45. package/dist/archive.d.ts.map +1 -1
  46. package/dist/archive.js +111 -108
  47. package/dist/atomic.d.ts +1 -1
  48. package/dist/atomic.d.ts.map +1 -1
  49. package/dist/config.d.ts +1 -1
  50. package/dist/config.d.ts.map +1 -1
  51. package/dist/config.js +1 -1
  52. package/dist/containment.d.ts +2 -0
  53. package/dist/containment.d.ts.map +1 -0
  54. package/dist/containment.js +1 -0
  55. package/dist/device-path.d.ts.map +1 -1
  56. package/dist/device-path.js +24 -2
  57. package/dist/durability.d.ts +2 -0
  58. package/dist/durability.d.ts.map +1 -1
  59. package/dist/durability.js +2 -0
  60. package/dist/errors.d.ts +4 -1
  61. package/dist/errors.d.ts.map +1 -1
  62. package/dist/errors.js +2 -0
  63. package/dist/file-hash.d.ts +10 -0
  64. package/dist/file-hash.d.ts.map +1 -0
  65. package/dist/file-hash.js +73 -0
  66. package/dist/file-lock-sync.d.ts +35 -0
  67. package/dist/file-lock-sync.d.ts.map +1 -0
  68. package/dist/file-lock-sync.js +233 -0
  69. package/dist/file-lock.d.ts +2 -0
  70. package/dist/file-lock.d.ts.map +1 -1
  71. package/dist/file-lock.js +1 -0
  72. package/dist/filename.d.ts.map +1 -1
  73. package/dist/filename.js +4 -1
  74. package/dist/guarded-mkdir.d.ts +7 -1
  75. package/dist/guarded-mkdir.d.ts.map +1 -1
  76. package/dist/guarded-mkdir.js +45 -3
  77. package/dist/index.d.ts +4 -3
  78. package/dist/index.d.ts.map +1 -1
  79. package/dist/index.js +1 -1
  80. package/dist/json-document-store.d.ts.map +1 -1
  81. package/dist/json-document-store.js +30 -15
  82. package/dist/move-path.d.ts.map +1 -1
  83. package/dist/move-path.js +101 -17
  84. package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
  85. package/dist/native/darwin-x64/fs-safe-native.node +0 -0
  86. package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
  87. package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
  88. package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
  89. package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
  90. package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
  91. package/dist/native-binding.d.ts +79 -0
  92. package/dist/native-binding.d.ts.map +1 -0
  93. package/dist/native-binding.js +1 -0
  94. package/dist/native-config.d.ts +17 -0
  95. package/dist/native-config.d.ts.map +1 -0
  96. package/dist/native-config.js +71 -0
  97. package/dist/native-operations.d.ts +21 -0
  98. package/dist/native-operations.d.ts.map +1 -0
  99. package/dist/native-operations.js +115 -0
  100. package/dist/native-pinned-write.d.ts +5 -0
  101. package/dist/native-pinned-write.d.ts.map +1 -0
  102. package/dist/native-pinned-write.js +115 -0
  103. package/dist/native.d.ts +14 -0
  104. package/dist/native.d.ts.map +1 -0
  105. package/dist/native.js +204 -0
  106. package/dist/output-sibling.d.ts +8 -0
  107. package/dist/output-sibling.d.ts.map +1 -0
  108. package/dist/output-sibling.js +122 -0
  109. package/dist/output.d.ts +2 -0
  110. package/dist/output.d.ts.map +1 -1
  111. package/dist/output.js +32 -5
  112. package/dist/owner-dacl.d.ts +31 -0
  113. package/dist/owner-dacl.d.ts.map +1 -0
  114. package/dist/owner-dacl.js +27 -0
  115. package/dist/path-policy.js +1 -1
  116. package/dist/path.d.ts.map +1 -1
  117. package/dist/path.js +3 -2
  118. package/dist/permissions-public.d.ts +2 -0
  119. package/dist/permissions-public.d.ts.map +1 -1
  120. package/dist/permissions-public.js +2 -0
  121. package/dist/permissions.d.ts.map +1 -1
  122. package/dist/permissions.js +6 -0
  123. package/dist/pinned-operation.d.ts +2 -0
  124. package/dist/pinned-operation.d.ts.map +1 -0
  125. package/dist/pinned-operation.js +35 -0
  126. package/dist/pinned-write.d.ts +3 -16
  127. package/dist/pinned-write.d.ts.map +1 -1
  128. package/dist/pinned-write.js +20 -85
  129. package/dist/private-directory.d.ts +5 -0
  130. package/dist/private-directory.d.ts.map +1 -0
  131. package/dist/private-directory.js +13 -0
  132. package/dist/private-temp-workspace.d.ts +6 -2
  133. package/dist/private-temp-workspace.d.ts.map +1 -1
  134. package/dist/private-temp-workspace.js +40 -13
  135. package/dist/publish-file-failure.d.ts +27 -0
  136. package/dist/publish-file-failure.d.ts.map +1 -0
  137. package/dist/publish-file-failure.js +21 -0
  138. package/dist/publish-file.d.ts +21 -0
  139. package/dist/publish-file.d.ts.map +1 -0
  140. package/dist/publish-file.js +351 -0
  141. package/dist/read-opened-file.d.ts +3 -0
  142. package/dist/read-opened-file.d.ts.map +1 -1
  143. package/dist/read-opened-file.js +1 -0
  144. package/dist/replace-file-copy-fallback.d.ts +34 -0
  145. package/dist/replace-file-copy-fallback.d.ts.map +1 -0
  146. package/dist/replace-file-copy-fallback.js +339 -0
  147. package/dist/replace-file.d.ts +6 -2
  148. package/dist/replace-file.d.ts.map +1 -1
  149. package/dist/replace-file.js +39 -90
  150. package/dist/root-context.d.ts +4 -1
  151. package/dist/root-context.d.ts.map +1 -1
  152. package/dist/root-context.js +20 -1
  153. package/dist/root-impl.d.ts +5 -0
  154. package/dist/root-impl.d.ts.map +1 -1
  155. package/dist/root-impl.js +128 -225
  156. package/dist/root-path-existing.d.ts +3 -0
  157. package/dist/root-path-existing.d.ts.map +1 -0
  158. package/dist/root-path-existing.js +69 -0
  159. package/dist/root-path.d.ts +1 -2
  160. package/dist/root-path.d.ts.map +1 -1
  161. package/dist/root-path.js +67 -81
  162. package/dist/root-walk.d.ts +41 -0
  163. package/dist/root-walk.d.ts.map +1 -0
  164. package/dist/root-walk.js +114 -0
  165. package/dist/root.d.ts +2 -0
  166. package/dist/root.d.ts.map +1 -1
  167. package/dist/safe-path-segment.d.ts.map +1 -1
  168. package/dist/safe-path-segment.js +16 -4
  169. package/dist/secret-file.d.ts +5 -2
  170. package/dist/secret-file.d.ts.map +1 -1
  171. package/dist/secret-file.js +20 -2
  172. package/dist/secret-read-async.d.ts +4 -0
  173. package/dist/secret-read-async.d.ts.map +1 -0
  174. package/dist/secret-read-async.js +95 -0
  175. package/dist/secret.d.ts +2 -1
  176. package/dist/secret.d.ts.map +1 -1
  177. package/dist/secret.js +2 -1
  178. package/dist/sibling-temp.d.ts.map +1 -1
  179. package/dist/sibling-temp.js +1 -0
  180. package/dist/sidecar-lock-handle.d.ts +20 -0
  181. package/dist/sidecar-lock-handle.d.ts.map +1 -0
  182. package/dist/sidecar-lock-handle.js +28 -0
  183. package/dist/sidecar-lock-policy.d.ts +10 -0
  184. package/dist/sidecar-lock-policy.d.ts.map +1 -0
  185. package/dist/sidecar-lock-policy.js +29 -0
  186. package/dist/sidecar-lock-reclaim.d.ts +22 -6
  187. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  188. package/dist/sidecar-lock-reclaim.js +106 -20
  189. package/dist/sidecar-lock-types.d.ts +56 -0
  190. package/dist/sidecar-lock-types.d.ts.map +1 -0
  191. package/dist/sidecar-lock-types.js +1 -0
  192. package/dist/sidecar-lock.d.ts +2 -45
  193. package/dist/sidecar-lock.d.ts.map +1 -1
  194. package/dist/sidecar-lock.js +94 -55
  195. package/dist/temp-cleanup.d.ts +7 -1
  196. package/dist/temp-cleanup.d.ts.map +1 -1
  197. package/dist/temp-cleanup.js +35 -5
  198. package/dist/temp-target.d.ts.map +1 -1
  199. package/dist/temp-target.js +51 -3
  200. package/dist/temp.d.ts +2 -1
  201. package/dist/temp.d.ts.map +1 -1
  202. package/dist/test-hooks.d.ts +3 -0
  203. package/dist/test-hooks.d.ts.map +1 -1
  204. package/dist/windows-permissions-native.d.ts +9 -0
  205. package/dist/windows-permissions-native.d.ts.map +1 -0
  206. package/dist/windows-permissions-native.js +31 -0
  207. package/docs/archive.md +131 -13
  208. package/docs/atomic.md +30 -1
  209. package/docs/config.md +51 -21
  210. package/docs/durability.md +211 -1
  211. package/docs/errors.md +36 -6
  212. package/docs/filename.md +0 -0
  213. package/docs/index.md +11 -8
  214. package/docs/install.md +27 -26
  215. package/docs/json-store.md +18 -3
  216. package/docs/migrating-to-0.5.md +196 -0
  217. package/docs/native-helper.md +85 -0
  218. package/docs/native.md +134 -0
  219. package/docs/output.md +37 -11
  220. package/docs/path.md +1 -1
  221. package/docs/permissions.md +84 -1
  222. package/docs/quickstart.md +5 -2
  223. package/docs/reading.md +4 -4
  224. package/docs/root.md +31 -15
  225. package/docs/secret-file.md +50 -1
  226. package/docs/security-model.md +29 -8
  227. package/docs/sidecar-lock.md +92 -7
  228. package/docs/temp.md +24 -1
  229. package/docs/test-hooks.md +22 -2
  230. package/docs/testing.md +18 -7
  231. package/docs/types.md +3 -1
  232. package/docs/walk.md +55 -0
  233. package/docs/writing.md +3 -3
  234. package/package.json +10 -4
  235. package/dist/pinned-helper.d.ts +0 -9
  236. package/dist/pinned-helper.d.ts.map +0 -1
  237. package/dist/pinned-helper.js +0 -19
  238. package/dist/pinned-path.d.ts +0 -7
  239. package/dist/pinned-path.d.ts.map +0 -1
  240. package/dist/pinned-path.js +0 -21
  241. package/dist/pinned-python-config.d.ts +0 -9
  242. package/dist/pinned-python-config.d.ts.map +0 -1
  243. package/dist/pinned-python-config.js +0 -38
  244. package/dist/pinned-python.d.ts +0 -12
  245. package/dist/pinned-python.d.ts.map +0 -1
  246. package/dist/pinned-python.js +0 -688
  247. package/docs/python-helper.md +0 -103
@@ -0,0 +1,351 @@
1
+ import { createHash } from "node:crypto";
2
+ import fsSync, {} from "node:fs";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { pinDirectory, } from "./directory-durability.js";
6
+ import { FsSafeError } from "./errors.js";
7
+ import { hashFileHandle } from "./file-hash.js";
8
+ import { sameFileIdentity } from "./file-identity.js";
9
+ import { syncNativeFileBestEffort } from "./native-operations.js";
10
+ import { getNativeBinding, requireNativeBinding } from "./native.js";
11
+ import { directorySyncFailure, publicationFailure, rememberCreatedTarget, } from "./publish-file-failure.js";
12
+ import { getFsSafeTestHooks } from "./test-hooks.js";
13
+ const HARDLINK_FALLBACK_CODES = new Set([
14
+ "EPERM",
15
+ "EXDEV",
16
+ "ENOTSUP",
17
+ "EOPNOTSUPP",
18
+ "ENOSYS",
19
+ ]);
20
+ const NATIVE_COPY_FALLBACK_CODES = new Set([
21
+ "EINVAL",
22
+ "ENOSYS",
23
+ "ENOTSUP",
24
+ "EOPNOTSUPP",
25
+ "EPERM",
26
+ "EXDEV",
27
+ ]);
28
+ export function isHardlinkFallbackError(error) {
29
+ return HARDLINK_FALLBACK_CODES.has(error?.code ?? "");
30
+ }
31
+ function sourceOpenFlags() {
32
+ return (fsSync.constants.O_RDONLY |
33
+ (process.platform !== "win32" && typeof fsSync.constants.O_NOFOLLOW === "number"
34
+ ? fsSync.constants.O_NOFOLLOW
35
+ : 0));
36
+ }
37
+ function directoryOpenFlags() {
38
+ return (fsSync.constants.O_RDONLY |
39
+ (typeof fsSync.constants.O_DIRECTORY === "number" ? fsSync.constants.O_DIRECTORY : 0));
40
+ }
41
+ async function openNativeParent(binding, filePath) {
42
+ const parentPath = path.dirname(filePath);
43
+ const handle = await fs.open(parentPath, directoryOpenFlags());
44
+ try {
45
+ const pathname = await fs.lstat(parentPath);
46
+ const opened = binding.fstatIdentity(handle.fd);
47
+ if (pathname.isSymbolicLink() || !sameFileIdentity(pathname, opened)) {
48
+ throw new FsSafeError("path-mismatch", "publication parent changed while opening");
49
+ }
50
+ return { basename: path.basename(filePath), handle };
51
+ }
52
+ catch (error) {
53
+ await handle.close().catch(() => undefined);
54
+ throw error;
55
+ }
56
+ }
57
+ async function assertPinnedSourceCurrent(params) {
58
+ const opened = await params.handle.stat();
59
+ const current = await fs.lstat(params.sourcePath);
60
+ if (!opened.isFile() ||
61
+ current.isSymbolicLink() ||
62
+ !current.isFile() ||
63
+ !sameFileIdentity(opened, params.identity) ||
64
+ !sameFileIdentity(current, opened)) {
65
+ throw new FsSafeError("path-mismatch", "publication source changed during operation");
66
+ }
67
+ }
68
+ async function copyPinnedSource(params) {
69
+ if (params.native && params.targetNativeParent) {
70
+ for (const method of ["clone", "copy-file-range"]) {
71
+ let nativeFd;
72
+ try {
73
+ if (method === "clone") {
74
+ nativeFd = params.native.cloneFileExclusive(params.source.fd, params.targetNativeParent.handle.fd, params.targetNativeParent.basename);
75
+ }
76
+ else {
77
+ const copied = await params.native.copyFileRangeExclusive(params.source.fd, params.targetNativeParent.handle.fd, params.targetNativeParent.basename);
78
+ if (copied.errorCode) {
79
+ throw Object.assign(new Error(copied.errorMessage ?? "native copy failed"), {
80
+ code: copied.errorCode,
81
+ });
82
+ }
83
+ nativeFd = copied.fd;
84
+ }
85
+ }
86
+ catch (error) {
87
+ if (NATIVE_COPY_FALLBACK_CODES.has(error.code ?? "")) {
88
+ continue;
89
+ }
90
+ throw error;
91
+ }
92
+ let target;
93
+ const createdIdentity = params.native.fstatIdentity(nativeFd);
94
+ rememberCreatedTarget(params.failure, createdIdentity, "copy-verify");
95
+ try {
96
+ await getFsSafeTestHooks()?.afterPublishTargetCreated?.("exclusive-copy", params.targetPath, createdIdentity);
97
+ const identity = await fs.lstat(params.targetPath);
98
+ target = await fs.open(params.targetPath, sourceOpenFlags());
99
+ const opened = await target.stat();
100
+ if (identity.isSymbolicLink() ||
101
+ !identity.isFile() ||
102
+ !sameFileIdentity(createdIdentity, identity) ||
103
+ !sameFileIdentity(createdIdentity, opened)) {
104
+ throw new FsSafeError("path-mismatch", "native publication target changed after copy");
105
+ }
106
+ const hashed = await hashFileHandle(target, params.native);
107
+ fsSync.closeSync(nativeFd);
108
+ nativeFd = undefined;
109
+ return {
110
+ handle: target,
111
+ stat: await target.stat(),
112
+ digest: hashed.digest,
113
+ bytes: hashed.bytes,
114
+ };
115
+ }
116
+ catch (error) {
117
+ await target?.close().catch(() => undefined);
118
+ throw error;
119
+ }
120
+ finally {
121
+ if (nativeFd !== undefined)
122
+ fsSync.closeSync(nativeFd);
123
+ }
124
+ }
125
+ }
126
+ const target = await fs.open(params.targetPath, "wx+", 0o600);
127
+ const identity = await target.stat();
128
+ rememberCreatedTarget(params.failure, identity, "copy-verify");
129
+ try {
130
+ await getFsSafeTestHooks()?.afterPublishTargetCreated?.("exclusive-copy", params.targetPath, identity);
131
+ const hash = createHash("sha256");
132
+ const buffer = Buffer.allocUnsafe(64 * 1024);
133
+ let position = 0;
134
+ while (true) {
135
+ const { bytesRead } = await params.source.read(buffer, 0, buffer.length, position);
136
+ if (bytesRead === 0) {
137
+ break;
138
+ }
139
+ hash.update(buffer.subarray(0, bytesRead));
140
+ let written = 0;
141
+ while (written < bytesRead) {
142
+ const result = await target.write(buffer, written, bytesRead - written, position + written);
143
+ if (result.bytesWritten <= 0) {
144
+ throw new FsSafeError("helper-failed", "exclusive publication copy made no progress");
145
+ }
146
+ written += result.bytesWritten;
147
+ }
148
+ position += bytesRead;
149
+ }
150
+ await target.sync();
151
+ return { handle: target, stat: identity, digest: hash.digest("hex"), bytes: position };
152
+ }
153
+ catch (error) {
154
+ await target.close().catch(() => undefined);
155
+ throw error;
156
+ }
157
+ }
158
+ async function removeCreatedTargetIfUnchanged(targetPath, identity) {
159
+ if (!identity) {
160
+ return "unknown";
161
+ }
162
+ try {
163
+ const current = await fs.lstat(targetPath);
164
+ if (!current.isSymbolicLink() && sameFileIdentity(current, identity)) {
165
+ await fs.rm(targetPath);
166
+ return "removed";
167
+ }
168
+ return "preserved";
169
+ }
170
+ catch (error) {
171
+ return error.code === "ENOENT" ? "removed" : "unknown";
172
+ }
173
+ }
174
+ async function syncPublishedParent(params) {
175
+ params.failure.phase = "directory-sync";
176
+ try {
177
+ await getFsSafeTestHooks()?.beforePublishDirectorySync?.(params.method, params.targetPath, params.failure.targetIdentity);
178
+ // PinnedDirectory.sync() revalidates descriptor/path identity immediately
179
+ // before and after fsync; keep that check inside the shared sync boundary.
180
+ return await params.parent.sync();
181
+ }
182
+ catch (error) {
183
+ params.failure.directorySync = directorySyncFailure(error);
184
+ throw error;
185
+ }
186
+ }
187
+ export async function publishFileExclusive(params) {
188
+ const sourcePath = path.resolve(params.sourcePath);
189
+ const targetPath = path.resolve(params.targetPath);
190
+ const parentPath = path.dirname(targetPath);
191
+ if (params.parentReceipt && path.resolve(params.parentReceipt.path) !== parentPath) {
192
+ throw new FsSafeError("path-mismatch", "publication parent receipt does not match target parent");
193
+ }
194
+ const sourcePathStat = await fs.lstat(sourcePath);
195
+ if (sourcePathStat.isSymbolicLink() || !sourcePathStat.isFile()) {
196
+ throw new FsSafeError("not-file", "publication source must be a regular file");
197
+ }
198
+ const source = await fs.open(sourcePath, sourceOpenFlags());
199
+ let parent;
200
+ let sourceNativeParent;
201
+ let targetNativeParent;
202
+ const failure = {
203
+ phase: params.strategy === "rename-noreplace" ? "rename-create" : "hardlink-create",
204
+ targetCreated: false,
205
+ preserveTarget: false,
206
+ };
207
+ try {
208
+ parent = await pinDirectory(params.parentReceipt ?? parentPath, {
209
+ label: "publication parent",
210
+ });
211
+ const sourceIdentity = await source.stat();
212
+ if (!sameFileIdentity(sourcePathStat, sourceIdentity) ||
213
+ (params.expectedSourceIdentity &&
214
+ !sameFileIdentity(params.expectedSourceIdentity, sourceIdentity))) {
215
+ throw new FsSafeError("path-mismatch", "publication source identity did not match");
216
+ }
217
+ await parent.assertCurrent();
218
+ await assertPinnedSourceCurrent({ sourcePath, handle: source, identity: sourceIdentity });
219
+ const native = params.strategy === "rename-noreplace"
220
+ ? requireNativeBinding()
221
+ : getNativeBinding();
222
+ if (native) {
223
+ sourceNativeParent = await openNativeParent(native, sourcePath);
224
+ targetNativeParent = await openNativeParent(native, targetPath);
225
+ }
226
+ if (params.strategy === "rename-noreplace") {
227
+ const binding = requireNativeBinding();
228
+ binding.renameNoReplace(sourceNativeParent.handle.fd, sourceNativeParent.basename, targetNativeParent.handle.fd, targetNativeParent.basename);
229
+ rememberCreatedTarget(failure, sourceIdentity, "rename-verify");
230
+ // A failed post-rename fence must not delete the only remaining name.
231
+ failure.preserveTarget = true;
232
+ await getFsSafeTestHooks()?.afterPublishTargetCreated?.("rename-noreplace", targetPath, sourceIdentity);
233
+ const targetIdentity = await fs.lstat(targetPath);
234
+ if (targetIdentity.isSymbolicLink() ||
235
+ !targetIdentity.isFile() ||
236
+ !sameFileIdentity(targetIdentity, sourceIdentity)) {
237
+ throw new FsSafeError("path-mismatch", "no-replace publication target changed");
238
+ }
239
+ try {
240
+ await fs.lstat(sourcePath);
241
+ throw new FsSafeError("path-mismatch", "no-replace publication source still exists");
242
+ }
243
+ catch (error) {
244
+ if (error.code !== "ENOENT") {
245
+ throw error;
246
+ }
247
+ }
248
+ syncNativeFileBestEffort(sourceNativeParent.handle.fd);
249
+ return {
250
+ method: "rename-noreplace",
251
+ identity: targetIdentity,
252
+ directorySync: await syncPublishedParent({
253
+ parent,
254
+ failure,
255
+ method: "rename-noreplace",
256
+ targetPath,
257
+ }),
258
+ };
259
+ }
260
+ try {
261
+ if (native) {
262
+ native.linkBeneath(sourceNativeParent.handle.fd, sourceNativeParent.basename, targetNativeParent.handle.fd, targetNativeParent.basename);
263
+ }
264
+ else {
265
+ await fs.link(sourcePath, targetPath);
266
+ }
267
+ rememberCreatedTarget(failure, sourceIdentity, "hardlink-verify");
268
+ await getFsSafeTestHooks()?.afterPublishTargetCreated?.("hardlink", targetPath, sourceIdentity);
269
+ const targetIdentity = await fs.lstat(targetPath);
270
+ if (targetIdentity.isSymbolicLink() ||
271
+ !targetIdentity.isFile() ||
272
+ !sameFileIdentity(targetIdentity, sourceIdentity)) {
273
+ throw new FsSafeError("path-mismatch", "hardlink publication target changed");
274
+ }
275
+ await assertPinnedSourceCurrent({ sourcePath, handle: source, identity: sourceIdentity });
276
+ return {
277
+ method: "hardlink",
278
+ identity: targetIdentity,
279
+ directorySync: await syncPublishedParent({
280
+ parent,
281
+ failure,
282
+ method: "hardlink",
283
+ targetPath,
284
+ }),
285
+ };
286
+ }
287
+ catch (error) {
288
+ if (failure.targetCreated ||
289
+ !isHardlinkFallbackError(error) ||
290
+ params.strategy === "link-required") {
291
+ throw error;
292
+ }
293
+ }
294
+ failure.phase = "copy-create";
295
+ let target;
296
+ let targetIdentity;
297
+ try {
298
+ const copied = await copyPinnedSource({
299
+ source,
300
+ targetPath,
301
+ native,
302
+ targetNativeParent,
303
+ failure,
304
+ });
305
+ target = copied.handle;
306
+ targetIdentity = copied.stat;
307
+ const targetPathStat = await fs.lstat(targetPath);
308
+ const copiedBack = await hashFileHandle(target, native);
309
+ const sourceAfter = await hashFileHandle(source, native);
310
+ if (targetPathStat.isSymbolicLink() ||
311
+ !sameFileIdentity(targetPathStat, targetIdentity) ||
312
+ copiedBack.bytes !== copied.bytes ||
313
+ copiedBack.digest !== copied.digest ||
314
+ sourceAfter.bytes !== copied.bytes ||
315
+ sourceAfter.digest !== copied.digest) {
316
+ throw new FsSafeError("path-mismatch", "exclusive publication copy failed content fencing");
317
+ }
318
+ await assertPinnedSourceCurrent({ sourcePath, handle: source, identity: sourceIdentity });
319
+ const directorySync = await syncPublishedParent({
320
+ parent,
321
+ failure,
322
+ method: "exclusive-copy",
323
+ targetPath,
324
+ });
325
+ return { method: "exclusive-copy", identity: targetPathStat, directorySync };
326
+ }
327
+ catch (error) {
328
+ await target?.close().catch(() => undefined);
329
+ target = undefined;
330
+ throw error;
331
+ }
332
+ finally {
333
+ await target?.close().catch(() => undefined);
334
+ }
335
+ }
336
+ catch (error) {
337
+ if (!failure.targetCreated)
338
+ throw error;
339
+ const preserveSyncFailure = failure.phase === "directory-sync" && params.onSyncFailure === "preserve";
340
+ const cleanup = failure.preserveTarget || preserveSyncFailure
341
+ ? "preserved"
342
+ : await removeCreatedTargetIfUnchanged(targetPath, failure.targetIdentity);
343
+ throw publicationFailure(error, failure, cleanup);
344
+ }
345
+ finally {
346
+ await sourceNativeParent?.handle.close().catch(() => undefined);
347
+ await targetNativeParent?.handle.close().catch(() => undefined);
348
+ await source.close().catch(() => undefined);
349
+ await parent?.close().catch(() => undefined);
350
+ }
351
+ }
@@ -1,12 +1,15 @@
1
1
  import type { Stats } from "node:fs";
2
2
  import type { FileHandle } from "node:fs/promises";
3
+ import type { ContainmentGuarantee } from "./containment.js";
3
4
  export type ReadResult = {
4
5
  buffer: Buffer;
6
+ containment: ContainmentGuarantee;
5
7
  realPath: string;
6
8
  stat: Stats;
7
9
  };
8
10
  type OpenedFile = {
9
11
  handle: FileHandle;
12
+ containment: ContainmentGuarantee;
10
13
  realPath: string;
11
14
  stat: Stats;
12
15
  };
@@ -1 +1 @@
1
- {"version":3,"file":"read-opened-file.d.ts","sourceRoot":"","sources":["../src/read-opened-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAInD,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,CAAC;CACb,CAAC;AAEF,KAAK,UAAU,GAAG;IAChB,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,CAAC;CACb,CAAC;AAEF,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,UAAU,CAAC,CAgBtB"}
1
+ {"version":3,"file":"read-opened-file.d.ts","sourceRoot":"","sources":["../src/read-opened-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAI7D,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,oBAAoB,CAAC;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,CAAC;CACb,CAAC;AAEF,KAAK,UAAU,GAAG;IAChB,MAAM,EAAE,UAAU,CAAC;IACnB,WAAW,EAAE,oBAAoB,CAAC;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,CAAC;CACb,CAAC;AAEF,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,UAAU,CAAC,CAiBtB"}
@@ -9,6 +9,7 @@ export async function readOpenedFileSafely(params) {
9
9
  : await readFileHandleBounded(params.opened.handle, params.maxBytes);
10
10
  return {
11
11
  buffer,
12
+ containment: params.opened.containment,
12
13
  realPath: params.opened.realPath,
13
14
  stat: params.opened.stat,
14
15
  };
@@ -0,0 +1,34 @@
1
+ import syncFs from "node:fs";
2
+ export type ReplaceFileDestinationHardlinkPolicy = "reject";
3
+ export type ReplaceFileCopyFallbackRestorePolicy = "restore-original" | "none";
4
+ export type ReplaceFileAtomicRestoreCleanup = "restored" | "restore-failed";
5
+ export type ReplaceFileAtomicRestoreFailureDetails = {
6
+ cleanup: ReplaceFileAtomicRestoreCleanup;
7
+ };
8
+ type AsyncFallbackFs = {
9
+ lstat: typeof import("node:fs/promises").lstat;
10
+ open: typeof import("node:fs/promises").open;
11
+ rm: typeof import("node:fs/promises").rm;
12
+ unlink: typeof import("node:fs/promises").unlink;
13
+ };
14
+ type SyncFallbackFs = Pick<typeof syncFs, "closeSync" | "fstatSync" | "fsyncSync" | "ftruncateSync" | "lstatSync" | "openSync" | "readSync" | "rmSync" | "unlinkSync" | "writeSync">;
15
+ export declare function assertDestinationHardlinkPolicy(fsModule: AsyncFallbackFs, dest: string, policy?: ReplaceFileDestinationHardlinkPolicy): Promise<void>;
16
+ export declare function assertDestinationHardlinkPolicySync(fsModule: SyncFallbackFs, dest: string, policy?: ReplaceFileDestinationHardlinkPolicy): void;
17
+ export declare function copyFallbackReplace(params: {
18
+ fsModule: AsyncFallbackFs;
19
+ src: string;
20
+ dest: string;
21
+ destinationHardlinks?: ReplaceFileDestinationHardlinkPolicy;
22
+ restore: ReplaceFileCopyFallbackRestorePolicy;
23
+ maxRestoreBytes?: number;
24
+ }): Promise<void>;
25
+ export declare function copyFallbackReplaceSync(params: {
26
+ fsModule: SyncFallbackFs;
27
+ src: string;
28
+ dest: string;
29
+ destinationHardlinks?: ReplaceFileDestinationHardlinkPolicy;
30
+ restore: ReplaceFileCopyFallbackRestorePolicy;
31
+ maxRestoreBytes?: number;
32
+ }): void;
33
+ export {};
34
+ //# sourceMappingURL=replace-file-copy-fallback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replace-file-copy-fallback.d.ts","sourceRoot":"","sources":["../src/replace-file-copy-fallback.ts"],"names":[],"mappings":"AAAA,OAAO,MAAsB,MAAM,SAAS,CAAC;AAK7C,MAAM,MAAM,oCAAoC,GAAG,QAAQ,CAAC;AAC5D,MAAM,MAAM,oCAAoC,GAAG,kBAAkB,GAAG,MAAM,CAAC;AAC/E,MAAM,MAAM,+BAA+B,GAAG,UAAU,GAAG,gBAAgB,CAAC;AAC5E,MAAM,MAAM,sCAAsC,GAAG;IACnD,OAAO,EAAE,+BAA+B,CAAC;CAC1C,CAAC;AAEF,KAAK,eAAe,GAAG;IACrB,KAAK,EAAE,cAAc,kBAAkB,EAAE,KAAK,CAAC;IAC/C,IAAI,EAAE,cAAc,kBAAkB,EAAE,IAAI,CAAC;IAC7C,EAAE,EAAE,cAAc,kBAAkB,EAAE,EAAE,CAAC;IACzC,MAAM,EAAE,cAAc,kBAAkB,EAAE,MAAM,CAAC;CAClD,CAAC;AAEF,KAAK,cAAc,GAAG,IAAI,CACxB,OAAO,MAAM,EACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,eAAe,GACf,WAAW,GACX,UAAU,GACV,UAAU,GACV,QAAQ,GACR,YAAY,GACZ,WAAW,CACd,CAAC;AAuFF,wBAAsB,+BAA+B,CACnD,QAAQ,EAAE,eAAe,EACzB,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,oCAAoC,GAC5C,OAAO,CAAC,IAAI,CAAC,CAqBf;AAED,wBAAgB,mCAAmC,CACjD,QAAQ,EAAE,cAAc,EACxB,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,oCAAoC,GAC5C,IAAI,CAwBN;AA0HD,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,QAAQ,EAAE,eAAe,CAAC;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,oBAAoB,CAAC,EAAE,oCAAoC,CAAC;IAC5D,OAAO,EAAE,oCAAoC,CAAC;IAC9C,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,GAAG,OAAO,CAAC,IAAI,CAAC,CAuDhB;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,QAAQ,EAAE,cAAc,CAAC;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,oBAAoB,CAAC,EAAE,oCAAoC,CAAC;IAC5D,OAAO,EAAE,oCAAoC,CAAC;IAC9C,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,GAAG,IAAI,CA4EP"}