@openclaw/fs-safe 0.10.0 → 0.12.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 (302) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/LICENSE +1 -0
  3. package/README.md +39 -6
  4. package/dist/absolute-path.d.ts.map +1 -1
  5. package/dist/absolute-path.js +3 -2
  6. package/dist/advanced.d.ts +5 -1
  7. package/dist/advanced.d.ts.map +1 -1
  8. package/dist/advanced.js +5 -1
  9. package/dist/archive-durability.d.ts +6 -6
  10. package/dist/archive-durability.d.ts.map +1 -1
  11. package/dist/archive-durability.js +1 -1
  12. package/dist/archive-entry.d.ts.map +1 -1
  13. package/dist/archive-entry.js +8 -8
  14. package/dist/archive-gzip-tail.d.ts +1 -0
  15. package/dist/archive-gzip-tail.d.ts.map +1 -1
  16. package/dist/archive-gzip-tail.js +16 -9
  17. package/dist/archive-input.d.ts.map +1 -1
  18. package/dist/archive-input.js +4 -2
  19. package/dist/archive-merge.d.ts +5 -1
  20. package/dist/archive-merge.d.ts.map +1 -1
  21. package/dist/archive-merge.js +15 -12
  22. package/dist/archive-native.js +4 -4
  23. package/dist/archive-parser.wasm +0 -0
  24. package/dist/archive-read.d.ts.map +1 -1
  25. package/dist/archive-read.js +54 -42
  26. package/dist/archive-staging.d.ts +6 -3
  27. package/dist/archive-staging.d.ts.map +1 -1
  28. package/dist/archive-staging.js +42 -22
  29. package/dist/archive-tar-stream.d.ts.map +1 -1
  30. package/dist/archive-tar-stream.js +7 -6
  31. package/dist/archive-tar-wasm.d.ts.map +1 -1
  32. package/dist/archive-tar-wasm.js +16 -13
  33. package/dist/archive-zip-admission.d.ts +1 -1
  34. package/dist/archive-zip-admission.d.ts.map +1 -1
  35. package/dist/archive-zip-admission.js +48 -12
  36. package/dist/archive-zip-loader.d.ts +6 -0
  37. package/dist/archive-zip-loader.d.ts.map +1 -0
  38. package/dist/archive-zip-loader.js +38 -0
  39. package/dist/archive-zip-names.d.ts.map +1 -1
  40. package/dist/archive-zip-names.js +26 -9
  41. package/dist/archive-zip-preflight.d.ts +2 -3
  42. package/dist/archive-zip-preflight.d.ts.map +1 -1
  43. package/dist/archive-zip-preflight.js +2 -34
  44. package/dist/archive.d.ts.map +1 -1
  45. package/dist/archive.js +12 -10
  46. package/dist/bounded-read.d.ts +5 -0
  47. package/dist/bounded-read.d.ts.map +1 -1
  48. package/dist/bounded-read.js +18 -11
  49. package/dist/copy-file-input.d.ts +0 -1
  50. package/dist/copy-file-input.d.ts.map +1 -1
  51. package/dist/copy-file-input.js +4 -26
  52. package/dist/copy-tree-portable.d.ts.map +1 -1
  53. package/dist/copy-tree-portable.js +57 -26
  54. package/dist/copy.d.ts +1 -1
  55. package/dist/copy.d.ts.map +1 -1
  56. package/dist/copy.js +3 -1
  57. package/dist/device-path.d.ts.map +1 -1
  58. package/dist/device-path.js +5 -3
  59. package/dist/directory-durability.d.ts.map +1 -1
  60. package/dist/directory-durability.js +5 -4
  61. package/dist/directory-guard.d.ts +11 -1
  62. package/dist/directory-guard.d.ts.map +1 -1
  63. package/dist/directory-guard.js +53 -11
  64. package/dist/durability.d.ts +1 -1
  65. package/dist/durability.d.ts.map +1 -1
  66. package/dist/durability.js +1 -1
  67. package/dist/file-handle-transfer.d.ts +14 -0
  68. package/dist/file-handle-transfer.d.ts.map +1 -0
  69. package/dist/file-handle-transfer.js +64 -0
  70. package/dist/file-hash.d.ts +3 -0
  71. package/dist/file-hash.d.ts.map +1 -1
  72. package/dist/file-hash.js +99 -32
  73. package/dist/file-lock-sync.d.ts.map +1 -1
  74. package/dist/file-lock-sync.js +8 -4
  75. package/dist/file-store-boundary.d.ts.map +1 -1
  76. package/dist/file-store-boundary.js +7 -5
  77. package/dist/file-store-path.d.ts +3 -0
  78. package/dist/file-store-path.d.ts.map +1 -0
  79. package/dist/file-store-path.js +27 -0
  80. package/dist/file-store-prune.d.ts.map +1 -1
  81. package/dist/file-store-prune.js +15 -5
  82. package/dist/file-store-sync-write.d.ts.map +1 -1
  83. package/dist/file-store-sync-write.js +56 -44
  84. package/dist/file-store.d.ts.map +1 -1
  85. package/dist/file-store.js +2 -18
  86. package/dist/filename.d.ts +1 -0
  87. package/dist/filename.d.ts.map +1 -1
  88. package/dist/filename.js +32 -14
  89. package/dist/guarded-mkdir.d.ts +2 -0
  90. package/dist/guarded-mkdir.d.ts.map +1 -1
  91. package/dist/guarded-mkdir.js +52 -16
  92. package/dist/guest-dispatch-python.d.ts +2 -0
  93. package/dist/guest-dispatch-python.d.ts.map +1 -0
  94. package/dist/guest-dispatch-python.js +117 -0
  95. package/dist/guest-native-python.d.ts +4 -0
  96. package/dist/guest-native-python.d.ts.map +1 -0
  97. package/dist/guest-native-python.js +135 -0
  98. package/dist/guest.d.ts +9 -0
  99. package/dist/guest.d.ts.map +1 -0
  100. package/dist/guest.js +421 -0
  101. package/dist/index.d.ts +1 -1
  102. package/dist/index.d.ts.map +1 -1
  103. package/dist/install-path.d.ts +6 -0
  104. package/dist/install-path.d.ts.map +1 -1
  105. package/dist/install-path.js +16 -2
  106. package/dist/json-durable-queue-directory.js +3 -3
  107. package/dist/json-durable-queue-ownership.d.ts +2 -0
  108. package/dist/json-durable-queue-ownership.d.ts.map +1 -1
  109. package/dist/json-durable-queue-ownership.js +47 -3
  110. package/dist/json-durable-queue-read.d.ts +6 -0
  111. package/dist/json-durable-queue-read.d.ts.map +1 -0
  112. package/dist/json-durable-queue-read.js +59 -0
  113. package/dist/json-durable-queue.d.ts +1 -1
  114. package/dist/json-durable-queue.d.ts.map +1 -1
  115. package/dist/json-durable-queue.js +43 -84
  116. package/dist/json.d.ts.map +1 -1
  117. package/dist/json.js +2 -1
  118. package/dist/local-roots.d.ts.map +1 -1
  119. package/dist/local-roots.js +2 -1
  120. package/dist/move-path-stage.d.ts.map +1 -1
  121. package/dist/move-path-stage.js +2 -1
  122. package/dist/move-path.d.ts.map +1 -1
  123. package/dist/move-path.js +4 -3
  124. package/dist/mutation-authority.d.ts +1 -0
  125. package/dist/mutation-authority.d.ts.map +1 -1
  126. package/dist/mutation-authority.js +4 -4
  127. package/dist/native-binding.d.ts +14 -2
  128. package/dist/native-binding.d.ts.map +1 -1
  129. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  130. package/dist/native-pinned-write-windows.js +3 -2
  131. package/dist/native-pinned-write.d.ts.map +1 -1
  132. package/dist/native-pinned-write.js +2 -1
  133. package/dist/opened-realpath.d.ts.map +1 -1
  134. package/dist/opened-realpath.js +5 -4
  135. package/dist/output.d.ts +2 -0
  136. package/dist/output.d.ts.map +1 -1
  137. package/dist/output.js +2 -0
  138. package/dist/overwrite-file-handle.d.ts +8 -0
  139. package/dist/overwrite-file-handle.d.ts.map +1 -0
  140. package/dist/overwrite-file-handle.js +42 -0
  141. package/dist/path-case.d.ts +7 -0
  142. package/dist/path-case.d.ts.map +1 -0
  143. package/dist/path-case.js +136 -0
  144. package/dist/path-scope-lexical.d.ts +14 -0
  145. package/dist/path-scope-lexical.d.ts.map +1 -0
  146. package/dist/path-scope-lexical.js +27 -0
  147. package/dist/path.d.ts.map +1 -1
  148. package/dist/path.js +22 -3
  149. package/dist/permissions-windows.d.ts +1 -1
  150. package/dist/permissions-windows.d.ts.map +1 -1
  151. package/dist/permissions-windows.js +48 -6
  152. package/dist/pinned-open.d.ts.map +1 -1
  153. package/dist/pinned-open.js +3 -1
  154. package/dist/pinned-write.d.ts +2 -2
  155. package/dist/pinned-write.d.ts.map +1 -1
  156. package/dist/pinned-write.js +3 -1
  157. package/dist/private-temp-workspace.d.ts.map +1 -1
  158. package/dist/private-temp-workspace.js +6 -4
  159. package/dist/realpath.d.ts +4 -0
  160. package/dist/realpath.d.ts.map +1 -0
  161. package/dist/realpath.js +43 -0
  162. package/dist/recursive-mkdir-path.d.ts +3 -0
  163. package/dist/recursive-mkdir-path.d.ts.map +1 -0
  164. package/dist/recursive-mkdir-path.js +8 -0
  165. package/dist/replace-directory.d.ts.map +1 -1
  166. package/dist/replace-directory.js +2 -1
  167. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  168. package/dist/replace-file-copy-fallback.js +23 -31
  169. package/dist/replace-file-copy-source.d.ts.map +1 -1
  170. package/dist/replace-file-copy-source.js +7 -12
  171. package/dist/replace-file-mode.d.ts +3 -0
  172. package/dist/replace-file-mode.d.ts.map +1 -0
  173. package/dist/replace-file-mode.js +10 -0
  174. package/dist/replace-file.d.ts +1 -0
  175. package/dist/replace-file.d.ts.map +1 -1
  176. package/dist/replace-file.js +14 -8
  177. package/dist/root-boundary.d.ts +25 -0
  178. package/dist/root-boundary.d.ts.map +1 -0
  179. package/dist/root-boundary.js +177 -0
  180. package/dist/root-context.d.ts.map +1 -1
  181. package/dist/root-context.js +40 -13
  182. package/dist/root-create-input.d.ts +10 -0
  183. package/dist/root-create-input.d.ts.map +1 -0
  184. package/dist/root-create-input.js +80 -0
  185. package/dist/root-directory-list.d.ts +3 -1
  186. package/dist/root-directory-list.d.ts.map +1 -1
  187. package/dist/root-directory-list.js +31 -5
  188. package/dist/root-entries.d.ts +11 -0
  189. package/dist/root-entries.d.ts.map +1 -0
  190. package/dist/root-entries.js +61 -0
  191. package/dist/root-errors.d.ts +5 -5
  192. package/dist/root-errors.d.ts.map +1 -1
  193. package/dist/root-errors.js +13 -12
  194. package/dist/root-impl.d.ts +13 -3
  195. package/dist/root-impl.d.ts.map +1 -1
  196. package/dist/root-impl.js +93 -70
  197. package/dist/root-move-preflight.d.ts +8 -0
  198. package/dist/root-move-preflight.d.ts.map +1 -0
  199. package/dist/root-move-preflight.js +16 -0
  200. package/dist/root-options.d.ts +12 -1
  201. package/dist/root-options.d.ts.map +1 -1
  202. package/dist/root-path-existing.d.ts +2 -0
  203. package/dist/root-path-existing.d.ts.map +1 -1
  204. package/dist/root-path-existing.js +14 -5
  205. package/dist/root-path-symlink.d.ts.map +1 -1
  206. package/dist/root-path-symlink.js +3 -2
  207. package/dist/root-path.d.ts +2 -0
  208. package/dist/root-path.d.ts.map +1 -1
  209. package/dist/root-path.js +54 -26
  210. package/dist/root-paths.d.ts +2 -6
  211. package/dist/root-paths.d.ts.map +1 -1
  212. package/dist/root-paths.js +24 -32
  213. package/dist/root-remove.d.ts +5 -0
  214. package/dist/root-remove.d.ts.map +1 -0
  215. package/dist/root-remove.js +286 -0
  216. package/dist/root-symlink-policy.d.ts +2 -1
  217. package/dist/root-symlink-policy.d.ts.map +1 -1
  218. package/dist/root-symlink-policy.js +2 -2
  219. package/dist/root-walk.d.ts.map +1 -1
  220. package/dist/root-walk.js +2 -1
  221. package/dist/root-write-mode.d.ts +2 -0
  222. package/dist/root-write-mode.d.ts.map +1 -1
  223. package/dist/root-write-mode.js +21 -7
  224. package/dist/root-write-verification.d.ts.map +1 -1
  225. package/dist/root-write-verification.js +12 -3
  226. package/dist/root.d.ts +2 -1
  227. package/dist/root.d.ts.map +1 -1
  228. package/dist/safe-path-segment.d.ts.map +1 -1
  229. package/dist/safe-path-segment.js +3 -1
  230. package/dist/secret-file.d.ts.map +1 -1
  231. package/dist/secret-file.js +2 -1
  232. package/dist/secret-read-async.d.ts.map +1 -1
  233. package/dist/secret-read-async.js +2 -1
  234. package/dist/secure-file-windows.d.ts +10 -0
  235. package/dist/secure-file-windows.d.ts.map +1 -0
  236. package/dist/secure-file-windows.js +186 -0
  237. package/dist/secure-file.d.ts.map +1 -1
  238. package/dist/secure-file.js +27 -7
  239. package/dist/secure-temp-dir.d.ts.map +1 -1
  240. package/dist/secure-temp-dir.js +2 -1
  241. package/dist/sibling-staged-file.d.ts +2 -0
  242. package/dist/sibling-staged-file.d.ts.map +1 -1
  243. package/dist/sibling-staged-file.js +49 -8
  244. package/dist/sibling-temp.d.ts +2 -0
  245. package/dist/sibling-temp.d.ts.map +1 -1
  246. package/dist/sibling-temp.js +8 -5
  247. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  248. package/dist/sidecar-lock-acquire.js +16 -5
  249. package/dist/sidecar-lock-policy.d.ts +2 -0
  250. package/dist/sidecar-lock-policy.d.ts.map +1 -1
  251. package/dist/sidecar-lock-policy.js +17 -0
  252. package/dist/sidecar-lock.d.ts.map +1 -1
  253. package/dist/sidecar-lock.js +2 -3
  254. package/dist/staged-directory.d.ts.map +1 -1
  255. package/dist/staged-directory.js +4 -3
  256. package/dist/temp-target.d.ts +14 -12
  257. package/dist/temp-target.d.ts.map +1 -1
  258. package/dist/temp-target.js +15 -7
  259. package/dist/trash.d.ts.map +1 -1
  260. package/dist/trash.js +7 -5
  261. package/dist/unicode-path.d.ts +3 -0
  262. package/dist/unicode-path.d.ts.map +1 -0
  263. package/dist/unicode-path.js +13 -0
  264. package/dist/walk.d.ts.map +1 -1
  265. package/dist/walk.js +14 -12
  266. package/dist/write-file-handle.d.ts +1 -0
  267. package/dist/write-file-handle.d.ts.map +1 -1
  268. package/dist/write-file-handle.js +3 -2
  269. package/docs/advanced.md +7 -1
  270. package/docs/archive.md +31 -5
  271. package/docs/atomic.md +17 -1
  272. package/docs/config.md +1 -0
  273. package/docs/contributing.md +33 -2
  274. package/docs/copy.md +75 -6
  275. package/docs/directory-identity.md +85 -0
  276. package/docs/durability.md +40 -3
  277. package/docs/entries.md +109 -0
  278. package/docs/errors.md +3 -3
  279. package/docs/file-store.md +21 -0
  280. package/docs/filename.md +9 -2
  281. package/docs/guest.md +146 -0
  282. package/docs/in-place-write.md +81 -0
  283. package/docs/index.md +2 -0
  284. package/docs/install-path.md +59 -13
  285. package/docs/install.md +34 -0
  286. package/docs/native-helper.md +14 -5
  287. package/docs/native.md +18 -2
  288. package/docs/output.md +32 -6
  289. package/docs/path-case.md +64 -0
  290. package/docs/path-scope.md +1 -1
  291. package/docs/path.md +1 -1
  292. package/docs/permissions.md +37 -3
  293. package/docs/public-api.md +36 -2
  294. package/docs/root.md +33 -3
  295. package/docs/secure-file.md +17 -14
  296. package/docs/security-model.md +1 -1
  297. package/docs/sidecar-lock.md +12 -3
  298. package/docs/store.md +22 -1
  299. package/docs/temp.md +39 -6
  300. package/docs/types.md +1 -1
  301. package/docs/writing.md +153 -3
  302. package/package.json +14 -8
@@ -1 +1 @@
1
- {"version":3,"file":"file-store-prune.d.ts","sourceRoot":"","sources":["../src/file-store-prune.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AAEF,wBAAsB,wBAAwB,CAAC,MAAM,EAAE;IACrD,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,qBAAqB,CAAC;CAChC,GAAG,OAAO,CAAC,IAAI,CAAC,CAsFhB"}
1
+ {"version":3,"file":"file-store-prune.d.ts","sourceRoot":"","sources":["../src/file-store-prune.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,qBAAqB,GAAG;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AAEF,wBAAsB,wBAAwB,CAAC,MAAM,EAAE;IACrD,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,qBAAqB,CAAC;CAChC,GAAG,OAAO,CAAC,IAAI,CAAC,CA8FhB"}
@@ -3,6 +3,8 @@ import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { FsSafeError } from "./errors.js";
5
5
  import { isPathInside } from "./path.js";
6
+ import { realpathSync } from "./realpath.js";
7
+ import { recursiveMkdirPath } from "./recursive-mkdir-path.js";
6
8
  import { root } from "./root.js";
7
9
  import { getFsSafeTestHooks } from "./test-hooks.js";
8
10
  export async function pruneExpiredStoreEntries(params) {
@@ -10,8 +12,8 @@ export async function pruneExpiredStoreEntries(params) {
10
12
  const recursive = params.options.recursive ?? false;
11
13
  const maxDepth = params.options.maxDepth;
12
14
  const pruneEmptyDirs = (recursive || maxDepth !== undefined) && (params.options.pruneEmptyDirs ?? false);
13
- await fs.mkdir(params.rootDir, { recursive: true, mode: params.dirMode });
14
- const rootReal = fsSync.realpathSync.native(params.rootDir);
15
+ await fs.mkdir(recursiveMkdirPath(params.rootDir), { recursive: true, mode: params.dirMode });
16
+ const rootReal = realpathSync.native(params.rootDir);
15
17
  const scopedRoot = await root(rootReal);
16
18
  const rootGuard = {
17
19
  dir: rootReal,
@@ -24,7 +26,7 @@ export async function pruneExpiredStoreEntries(params) {
24
26
  !stat.isDirectory() ||
25
27
  stat.dev !== rootGuard.stat.dev ||
26
28
  stat.ino !== rootGuard.stat.ino ||
27
- fsSync.realpathSync.native(rootGuard.dir) !== rootGuard.realPath) {
29
+ realpathSync.native(rootGuard.dir) !== rootGuard.realPath) {
28
30
  throw new FsSafeError("path-mismatch", "store root changed during prune");
29
31
  }
30
32
  }
@@ -33,7 +35,7 @@ export async function pruneExpiredStoreEntries(params) {
33
35
  if (!before || before.isSymbolicLink() || !before.isDirectory()) {
34
36
  return null;
35
37
  }
36
- const real = observeOrNull(() => fsSync.realpathSync.native(dir));
38
+ const real = observeOrNull(() => realpathSync.native(dir));
37
39
  if (!real || !isPathInside(rootReal, real)) {
38
40
  return null;
39
41
  }
@@ -74,7 +76,15 @@ export async function pruneExpiredStoreEntries(params) {
74
76
  }
75
77
  if (stat.isFile() && now - stat.mtimeMs > params.options.ttlMs) {
76
78
  await assertRootGuard();
77
- await scopedRoot.remove(relativePath).catch(() => undefined);
79
+ await scopedRoot.remove(relativePath, {
80
+ assertBeforeMutation: () => {
81
+ // Removal preparation can outlive the expiry observation above.
82
+ const current = fsSync.lstatSync(fullPath);
83
+ if (!current.isFile() || !(now - current.mtimeMs > params.options.ttlMs)) {
84
+ throw new FsSafeError("path-mismatch", "store entry is no longer an expired file");
85
+ }
86
+ },
87
+ }).catch(() => undefined);
78
88
  }
79
89
  }
80
90
  if (!pruneEmptyDirs) {
@@ -1 +1 @@
1
- {"version":3,"file":"file-store-sync-write.d.ts","sourceRoot":"","sources":["../src/file-store-sync-write.ts"],"names":[],"mappings":"AAwBA,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,WAAW,EAAE,OAAO,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,MAAM,CA+GT"}
1
+ {"version":3,"file":"file-store-sync-write.d.ts","sourceRoot":"","sources":["../src/file-store-sync-write.ts"],"names":[],"mappings":"AAwDA,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,WAAW,EAAE,OAAO,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,MAAM,CAwFT"}
@@ -1,11 +1,14 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import fs from "node:fs";
2
+ import fs, {} from "node:fs";
3
3
  import path from "node:path";
4
4
  import { syncDirectorySync } from "./directory-durability.js";
5
5
  import { FsSafeError } from "./errors.js";
6
- import { sameFileIdentity } from "./file-identity.js";
7
6
  import { assertSyncDirectoryGuard, ensureParentSync, ensureStoreDirectorySync, } from "./file-store-boundary.js";
8
7
  import { isPathInside } from "./path.js";
8
+ import { resolveReadOpenFlags } from "./read-open-flags.js";
9
+ import { writeTempFileSync } from "./replace-file-descriptor.js";
10
+ import { SyncAtomicTempOwner } from "./replace-file-temp-owner.js";
11
+ import { inspectFileIdentitySync } from "./strict-file-identity.js";
9
12
  import { getFsSafeTestHooks } from "./test-hooks.js";
10
13
  function ensurePrivateDirectorySync(rootDir, targetDir, mode) {
11
14
  return ensureStoreDirectorySync({
@@ -15,6 +18,35 @@ function ensurePrivateDirectorySync(rootDir, targetDir, mode) {
15
18
  messagePrefix: "private store",
16
19
  });
17
20
  }
21
+ function verifyStoreFile(fd, expected, filePath) {
22
+ const assertFile = (stat, allowUnknown = false) => {
23
+ const unknown = allowUnknown && process.platform === "win32" && (stat.dev === 0n || stat.ino === 0n);
24
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1n ||
25
+ (stat.dev !== expected.dev && !(unknown && stat.dev === 0n)) ||
26
+ (stat.ino !== expected.ino && !(unknown && stat.ino === 0n))) {
27
+ throw new FsSafeError("path-mismatch", "store file changed during write");
28
+ }
29
+ return unknown;
30
+ };
31
+ const assertDescriptor = (descriptor) => {
32
+ assertFile(inspectFileIdentitySync(() => fs.fstatSync(descriptor, { bigint: true }), expected));
33
+ };
34
+ assertDescriptor(fd);
35
+ if (assertFile(fs.lstatSync(filePath, { bigint: true }), true)) {
36
+ // A Windows pathname can have opaque metadata even while the retained writer is known.
37
+ // Reopen for identity only; equal content cannot establish that publication kept its file.
38
+ const reopened = fs.openSync(filePath, resolveReadOpenFlags());
39
+ try {
40
+ assertDescriptor(reopened);
41
+ assertFile(fs.lstatSync(filePath, { bigint: true }), true);
42
+ assertDescriptor(reopened);
43
+ }
44
+ finally {
45
+ fs.closeSync(reopened);
46
+ }
47
+ }
48
+ assertDescriptor(fd);
49
+ }
18
50
  export function writeFileSyncAtomic(params) {
19
51
  const filePath = path.resolve(params.filePath);
20
52
  if (!isPathInside(params.rootDir, filePath)) {
@@ -43,58 +75,41 @@ export function writeFileSyncAtomic(params) {
43
75
  });
44
76
  }
45
77
  const tempPath = path.join(parentGuard?.dir ?? path.dirname(filePath), `.fs-safe-${process.pid}-${randomUUID()}.tmp`);
46
- let tempExists = false;
78
+ const owner = new SyncAtomicTempOwner(tempPath);
79
+ let originalError;
47
80
  try {
48
81
  getFsSafeTestHooks()?.beforeFileStoreSyncPrivateWrite?.(filePath);
49
82
  if (parentGuard) {
50
83
  assertSyncDirectoryGuard(parentGuard);
51
84
  }
52
- const tempStat = (() => {
53
- const descriptor = fs.openSync(tempPath, "wx", params.mode);
54
- tempExists = true;
55
- try {
56
- fs.writeFileSync(descriptor, params.content);
85
+ owner.start();
86
+ const temp = writeTempFileSync({
87
+ fsModule: fs, tempPath, content: params.content, mode: params.mode, sync: false,
88
+ onIdentity: owner.onIdentity,
89
+ fchmodSync: (descriptor, mode) => {
57
90
  try {
58
- fs.fchmodSync(descriptor, params.mode);
91
+ fs.fchmodSync(descriptor, mode);
59
92
  }
60
93
  catch {
61
94
  // Best-effort on platforms that do not enforce POSIX modes.
62
95
  }
63
- const opened = fs.fstatSync(descriptor);
64
- if (!opened.isFile() || opened.nlink > 1) {
65
- throw new FsSafeError("path-mismatch", "store temp is not an owned regular file");
66
- }
67
- if (params.durable)
68
- fs.fsyncSync(descriptor);
69
- return opened;
70
- }
71
- finally {
72
- fs.closeSync(descriptor);
73
- }
74
- })();
75
- const tempPathStat = fs.lstatSync(tempPath);
76
- if (tempPathStat.isSymbolicLink() ||
77
- !tempPathStat.isFile() ||
78
- tempPathStat.nlink > 1 ||
79
- !sameFileIdentity(tempPathStat, tempStat)) {
80
- throw new FsSafeError("path-mismatch", "store temp changed before publication");
81
- }
96
+ },
97
+ });
98
+ owner.adopt(temp);
99
+ // Preserve the store's strict fsync errors; the generic temp helper tolerates EPERM.
100
+ if (params.durable)
101
+ fs.fsyncSync(temp.fd);
102
+ verifyStoreFile(temp.fd, owner.identity, tempPath);
82
103
  if (parentGuard) {
83
104
  assertSyncDirectoryGuard(parentGuard);
84
105
  }
85
106
  fs.renameSync(tempPath, filePath);
86
- tempExists = false;
107
+ owner.markRenamed();
87
108
  if (parentGuard) {
88
109
  assertSyncDirectoryGuard(parentGuard);
89
110
  }
90
111
  try {
91
- const publishedStat = fs.lstatSync(filePath);
92
- if (publishedStat.isSymbolicLink() ||
93
- !publishedStat.isFile() ||
94
- publishedStat.nlink > 1 ||
95
- !sameFileIdentity(tempStat, publishedStat)) {
96
- throw new FsSafeError("path-mismatch", "store target changed after write");
97
- }
112
+ verifyStoreFile(temp.fd, owner.identity, filePath);
98
113
  }
99
114
  catch (error) {
100
115
  if (error instanceof FsSafeError) {
@@ -117,14 +132,11 @@ export function writeFileSyncAtomic(params) {
117
132
  }
118
133
  return filePath;
119
134
  }
135
+ catch (error) {
136
+ originalError = error;
137
+ throw error;
138
+ }
120
139
  finally {
121
- if (tempExists) {
122
- try {
123
- fs.unlinkSync(tempPath);
124
- }
125
- catch {
126
- // Best-effort cleanup after write failure.
127
- }
128
- }
140
+ owner.finish({ fsModule: fs, originalError, throwOnCleanupError: false });
129
141
  }
130
142
  }
@@ -1 +1 @@
1
- {"version":3,"file":"file-store.d.ts","sourceRoot":"","sources":["../src/file-store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"file-store.d.ts","sourceRoot":"","sources":["../src/file-store.ts"],"names":[],"mappings":"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"}
@@ -5,35 +5,19 @@ import { normalizeMaxBytes } from "./byte-budget.js";
5
5
  import { readFileDescriptorBoundedSync } from "./bounded-read.js";
6
6
  import { FsSafeError } from "./errors.js";
7
7
  import { assertFileStoreMaxBytes } from "./file-store-limit.js";
8
+ import { assertRelativePath, resolveStorePath } from "./file-store-path.js";
8
9
  import { pruneExpiredStoreEntries } from "./file-store-prune.js";
9
10
  import { ensureParentInRoot, openPrivateStoreLockRoot, openWritableStoreRoot, writeStreamToTempSource, } from "./file-store-boundary.js";
10
11
  import { writeFileSyncAtomic } from "./file-store-sync-write.js";
11
12
  import { createJsonStore } from "./json-document-store.js";
12
13
  import { stringifyJsonDocument } from "./json-stringify.js";
13
- import { isNotFoundPathError, resolveSafeRelativePath, splitSafeRelativePath } from "./path.js";
14
+ import { isNotFoundPathError } from "./path.js";
14
15
  import { throwFsSafeReadError } from "./root-errors.js";
15
16
  import { root } from "./root.js";
16
17
  import { DEFAULT_ROOT_MAX_BYTES } from "./root-impl.js";
17
18
  import { readRegularFile } from "./regular-file.js";
18
19
  import { matchRootFileOpenFailure, openRootFileSync } from "./root-file.js";
19
20
  import { writeSecretFileAtomic } from "./secret-file.js";
20
- function assertRelativePath(relativePath) {
21
- const raw = relativePath.trim();
22
- if (!raw || raw !== relativePath) {
23
- throw new FsSafeError("invalid-path", "store key must be non-empty and unpadded");
24
- }
25
- const segments = splitSafeRelativePath(raw);
26
- if (segments.length === 0 ||
27
- segments.join("/") !== raw ||
28
- raw.normalize("NFC") !== raw ||
29
- segments.some((segment) => /[ .]$/u.test(segment))) {
30
- throw new FsSafeError("invalid-path", "store key must use one canonical relative spelling");
31
- }
32
- return raw;
33
- }
34
- function resolveStorePath(rootDir, relativePath) {
35
- return resolveSafeRelativePath(rootDir, assertRelativePath(relativePath));
36
- }
37
21
  function isNotFound(error) {
38
22
  return error instanceof FsSafeError ? error.code === "not-found" : isNotFoundPathError(error);
39
23
  }
@@ -1,3 +1,4 @@
1
+ export declare function suffixWindowsReservedDeviceName(fileName: string): string;
1
2
  /** Keeps short names exact and trims only the filename tail of a composite temp name. */
2
3
  export declare function fitFileNameToPortableComponent(params: {
3
4
  prefix: string;
@@ -1 +1 @@
1
- {"version":3,"file":"filename.d.ts","sourceRoot":"","sources":["../src/filename.ts"],"names":[],"mappings":"AAqCA,yFAAyF;AACzF,wBAAgB,8BAA8B,CAAC,MAAM,EAAE;IACrD,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,MAAM,CA8BT;AAED,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAoBxF"}
1
+ {"version":3,"file":"filename.d.ts","sourceRoot":"","sources":["../src/filename.ts"],"names":[],"mappings":"AAwBA,wBAAgB,+BAA+B,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAexE;AAkBD,yFAAyF;AACzF,wBAAgB,8BAA8B,CAAC,MAAM,EAAE;IACrD,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,MAAM,CA8BT;AAED,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAqBxF"}
package/dist/filename.js CHANGED
@@ -1,8 +1,14 @@
1
1
  import path from "node:path";
2
2
  import { WINDOWS_RESERVED_DEVICE_NAMES } from "./device-path.js";
3
+ import { maxNormalizedUtf8Bytes } from "./unicode-path.js";
3
4
  const INVALID_FILE_NAME_CHARACTERS = /[\u0000-\u001f\u007f-\u009f<>:"/\\|?*]/g;
4
- function trimWindowsIgnoredSuffix(value) {
5
- let end = value.length;
5
+ function canStartWindowsDeviceName(character) {
6
+ const folded = character | 0x20;
7
+ return folded === 0x61 || folded === 0x63 || folded === 0x6c ||
8
+ folded === 0x6e || folded === 0x70;
9
+ }
10
+ function windowsDeviceStemEnd(value, stemEnd) {
11
+ let end = stemEnd;
6
12
  while (end > 0) {
7
13
  const character = value.charCodeAt(end - 1);
8
14
  if (character !== 0x20 && character !== 0x2e) {
@@ -10,21 +16,34 @@ function trimWindowsIgnoredSuffix(value) {
10
16
  }
11
17
  end -= 1;
12
18
  }
13
- return end === value.length ? value : value.slice(0, end);
19
+ return end;
14
20
  }
15
- function suffixWindowsReservedDeviceName(fileName) {
21
+ export function suffixWindowsReservedDeviceName(fileName) {
22
+ if (!canStartWindowsDeviceName(fileName.charCodeAt(0))) {
23
+ return fileName;
24
+ }
16
25
  const extensionIndex = fileName.indexOf(".");
17
26
  const baseNameEnd = extensionIndex < 0 ? fileName.length : extensionIndex;
18
- const baseName = fileName.slice(0, baseNameEnd);
19
- const deviceBaseName = trimWindowsIgnoredSuffix(baseName);
20
- if (!WINDOWS_RESERVED_DEVICE_NAMES.has(deviceBaseName.toUpperCase())) {
27
+ const deviceBaseNameEnd = windowsDeviceStemEnd(fileName, baseNameEnd);
28
+ if (deviceBaseNameEnd === 0 ||
29
+ deviceBaseNameEnd > 7 ||
30
+ !WINDOWS_RESERVED_DEVICE_NAMES.has(fileName.slice(0, deviceBaseNameEnd).toUpperCase())) {
21
31
  return fileName;
22
32
  }
23
- return `${baseName}_${fileName.slice(baseNameEnd)}`;
33
+ return `${fileName.slice(0, baseNameEnd)}_${fileName.slice(baseNameEnd)}`;
24
34
  }
25
35
  const PORTABLE_FILE_NAME_BYTES = 255;
36
+ const UNTRUSTED_FILE_NAME_CODE_UNITS = 200;
37
+ function truncateWithoutSplittingSurrogatePair(value, maxLength) {
38
+ let truncated = value.slice(0, maxLength);
39
+ const trailingCodeUnit = truncated.charCodeAt(truncated.length - 1);
40
+ if (trailingCodeUnit >= 0xd800 && trailingCodeUnit <= 0xdbff) {
41
+ truncated = truncated.slice(0, -1);
42
+ }
43
+ return truncated;
44
+ }
26
45
  function normalizedFileNameBytes(value) {
27
- return Math.max(Buffer.byteLength(value.normalize("NFC"), "utf8"), Buffer.byteLength(value.normalize("NFD"), "utf8"));
46
+ return maxNormalizedUtf8Bytes(value, true);
28
47
  }
29
48
  /** Keeps short names exact and trims only the filename tail of a composite temp name. */
30
49
  export function fitFileNameToPortableComponent(params) {
@@ -70,11 +89,10 @@ export function sanitizeUntrustedFileName(fileName, fallbackName) {
70
89
  return fallbackName;
71
90
  }
72
91
  base = suffixWindowsReservedDeviceName(base);
73
- if (base.length > 200) {
74
- base = base.slice(0, 200);
75
- const trailingCodeUnit = base.charCodeAt(base.length - 1);
76
- if (trailingCodeUnit >= 0xd800 && trailingCodeUnit <= 0xdbff) {
77
- base = base.slice(0, -1);
92
+ if (base.length > UNTRUSTED_FILE_NAME_CODE_UNITS) {
93
+ base = truncateWithoutSplittingSurrogatePair(base, UNTRUSTED_FILE_NAME_CODE_UNITS);
94
+ if (suffixWindowsReservedDeviceName(base) !== base) {
95
+ base = suffixWindowsReservedDeviceName(truncateWithoutSplittingSurrogatePair(base, UNTRUSTED_FILE_NAME_CODE_UNITS - 1));
78
96
  }
79
97
  }
80
98
  return base;
@@ -1,3 +1,4 @@
1
+ import { type RootBoundaryIdentity } from "./root-boundary.js";
1
2
  /**
2
3
  * Creates each missing path component from `rootReal` down to `targetPath`,
3
4
  * guarding every step. Returns the real (symlink-resolved) path of the final
@@ -11,5 +12,6 @@ export declare function mkdirPathComponentsWithGuards(params: {
11
12
  assertBeforeMutation?: () => void;
12
13
  mode?: number;
13
14
  rejectSymlinks?: boolean;
15
+ rootIdentity?: RootBoundaryIdentity;
14
16
  }): Promise<string>;
15
17
  //# sourceMappingURL=guarded-mkdir.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"guarded-mkdir.d.ts","sourceRoot":"","sources":["../src/guarded-mkdir.ts"],"names":[],"mappings":"AA0BA;;;;;GAKG;AACH,wBAAsB,6BAA6B,CAAC,MAAM,EAAE;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAClE,oBAAoB,CAAC,EAAE,MAAM,IAAI,CAAC;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,GAAG,OAAO,CAAC,MAAM,CAAC,CAwDlB"}
1
+ {"version":3,"file":"guarded-mkdir.d.ts","sourceRoot":"","sources":["../src/guarded-mkdir.ts"],"names":[],"mappings":"AAYA,OAAO,EAAuB,KAAK,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAgCpF;;;;;GAKG;AACH,wBAAsB,6BAA6B,CAAC,MAAM,EAAE;IAC1D,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAClE,oBAAoB,CAAC,EAAE,MAAM,IAAI,CAAC;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,YAAY,CAAC,EAAE,oBAAoB,CAAC;CACrC,GAAG,OAAO,CAAC,MAAM,CAAC,CAqFlB"}
@@ -1,17 +1,25 @@
1
1
  import fsSync from "node:fs";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard } from "./directory-guard.js";
4
+ import { assertAsyncDirectoryGuard, createAsyncDirectoryGuard, } from "./directory-guard.js";
5
5
  import { FsSafeError } from "./errors.js";
6
6
  import { isNotFoundPathError, isPathRelativeEscape } from "./path.js";
7
- import { directoryComponentNotDirectoryError } from "./root-errors.js";
8
- function isSameOrChildPath(candidate, parent) {
9
- const parentPrefix = parent.endsWith(path.sep) ? parent : `${parent}${path.sep}`;
10
- return candidate === parent || candidate.startsWith(parentPrefix);
7
+ import { directoryComponentNotDirectoryError, rootPathChangedError } from "./root-errors.js";
8
+ import { realpathSync } from "./realpath.js";
9
+ import { admitPathInsideRoot } from "./root-boundary.js";
10
+ function suppliedExactRootIdentity(identity) {
11
+ return typeof identity?.dev === "bigint" && typeof identity.ino === "bigint"
12
+ ? { dev: identity.dev, ino: identity.ino }
13
+ : undefined;
14
+ }
15
+ function assertGuardMatchesRootIdentity(guard, expected) {
16
+ if (guard.stat.dev !== expected.dev || guard.stat.ino !== expected.ino) {
17
+ throw rootPathChangedError();
18
+ }
11
19
  }
12
20
  async function realpathOrThrowNotFile(target) {
13
21
  try {
14
- return path.resolve(fsSync.realpathSync.native(target));
22
+ return path.resolve(realpathSync.native(target));
15
23
  }
16
24
  catch (error) {
17
25
  if (isNotFoundPathError(error)) {
@@ -30,16 +38,38 @@ async function realpathOrThrowNotFile(target) {
30
38
  */
31
39
  export async function mkdirPathComponentsWithGuards(params) {
32
40
  const root = path.resolve(params.rootReal);
33
- const rootCanonical = path.resolve(fsSync.realpathSync.native(root));
41
+ const configuredRootGuard = await createAsyncDirectoryGuard(root, { bigint: true });
42
+ const suppliedIdentity = suppliedExactRootIdentity(params.rootIdentity);
43
+ const checkedRootIdentity = suppliedIdentity ?? {
44
+ dev: configuredRootGuard.stat.dev,
45
+ ino: configuredRootGuard.stat.ino,
46
+ };
47
+ assertGuardMatchesRootIdentity(configuredRootGuard, checkedRootIdentity);
48
+ const rootCanonical = path.resolve(configuredRootGuard.realPath);
49
+ const rootGuard = rootCanonical === root
50
+ ? configuredRootGuard
51
+ : await createAsyncDirectoryGuard(rootCanonical, { bigint: true });
52
+ assertGuardMatchesRootIdentity(rootGuard, checkedRootIdentity);
34
53
  const target = path.resolve(params.targetPath);
35
- const relative = path.relative(root, target);
36
- if (isPathRelativeEscape(relative)) {
54
+ const admissionParams = {
55
+ candidatePath: target,
56
+ rootIdentity: checkedRootIdentity,
57
+ };
58
+ // Derive the suffix from the caller's trusted root spelling first. The
59
+ // canonical spelling is also accepted after both names have been bound to
60
+ // the same exact root object above.
61
+ const admittedTarget = admitPathInsideRoot({ rootPath: root, ...admissionParams }) ??
62
+ (rootCanonical === root
63
+ ? undefined
64
+ : admitPathInsideRoot({ rootPath: rootCanonical, ...admissionParams }));
65
+ if (!admittedTarget || isPathRelativeEscape(admittedTarget.relativePath)) {
37
66
  throw new FsSafeError("outside-workspace", "directory is outside workspace root");
38
67
  }
39
- let current = root;
40
- for (const part of relative.split(path.sep).filter(Boolean)) {
68
+ let current = rootCanonical;
69
+ let currentGuard = rootGuard;
70
+ for (const part of admittedTarget.relativePath.split(path.sep).filter(Boolean)) {
41
71
  const next = path.join(current, part);
42
- const parentGuard = await createAsyncDirectoryGuard(current);
72
+ const parentGuard = currentGuard;
43
73
  await assertAsyncDirectoryGuard(parentGuard);
44
74
  await params.beforeComponent?.(next);
45
75
  params.assertBeforeMutation?.();
@@ -57,10 +87,16 @@ export async function mkdirPathComponentsWithGuards(params) {
57
87
  }
58
88
  // Node's recursive mkdir follows symlinks in missing components. Build one
59
89
  // segment at a time and realpath-check each segment before descending.
60
- const nextReal = await realpathOrThrowNotFile(next);
61
- if (!isSameOrChildPath(nextReal, rootCanonical)) {
90
+ const observedNextReal = await realpathOrThrowNotFile(next);
91
+ const admittedNextReal = admitPathInsideRoot({
92
+ rootPath: rootCanonical,
93
+ candidatePath: observedNextReal,
94
+ rootIdentity: checkedRootIdentity,
95
+ });
96
+ if (!admittedNextReal) {
62
97
  throw new FsSafeError("outside-workspace", "directory escaped workspace root");
63
98
  }
99
+ const nextReal = admittedNextReal.path;
64
100
  if (stat.isSymbolicLink()) {
65
101
  // An existing path component may legitimately be a symlink to a real
66
102
  // directory inside the root (e.g. a skill-bank layout). We already
@@ -75,12 +111,12 @@ export async function mkdirPathComponentsWithGuards(params) {
75
111
  if (!targetStat.isDirectory()) {
76
112
  throw directoryComponentNotDirectoryError();
77
113
  }
78
- await createAsyncDirectoryGuard(nextReal);
114
+ currentGuard = await createAsyncDirectoryGuard(nextReal);
79
115
  await assertAsyncDirectoryGuard(parentGuard);
80
116
  current = nextReal;
81
117
  continue;
82
118
  }
83
- await createAsyncDirectoryGuard(next);
119
+ currentGuard = await createAsyncDirectoryGuard(next);
84
120
  await assertAsyncDirectoryGuard(parentGuard);
85
121
  current = next;
86
122
  }
@@ -0,0 +1,2 @@
1
+ export declare const GUEST_FILESYSTEM_DISPATCH_PYTHON: string;
2
+ //# sourceMappingURL=guest-dispatch-python.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guest-dispatch-python.d.ts","sourceRoot":"","sources":["../src/guest-dispatch-python.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,gCAAgC,QAoHjC,CAAC"}
@@ -0,0 +1,117 @@
1
+ export const GUEST_FILESYSTEM_DISPATCH_PYTHON = [
2
+ "if operation == 'copy':",
3
+ " src_root_fd = open_dir(sys.argv[2])",
4
+ " dst_root_fd = open_dir(sys.argv[5])",
5
+ " src_parent_fd = None",
6
+ " dst_parent_fd = None",
7
+ " try:",
8
+ " src_parent_fd = walk_dir(src_root_fd, sys.argv[3], False)",
9
+ " dst_parent_fd = walk_dir(dst_root_fd, sys.argv[6], sys.argv[8] == '1')",
10
+ " copy_file_atomic(src_parent_fd, sys.argv[4], dst_parent_fd, sys.argv[7])",
11
+ " finally:",
12
+ " if src_parent_fd is not None:",
13
+ " os.close(src_parent_fd)",
14
+ " if dst_parent_fd is not None:",
15
+ " os.close(dst_parent_fd)",
16
+ " os.close(src_root_fd)",
17
+ " os.close(dst_root_fd)",
18
+ "elif operation == 'write':",
19
+ " root_fd = open_dir(sys.argv[2])",
20
+ " parent_fd = None",
21
+ " try:",
22
+ " parent_fd = walk_dir(root_fd, sys.argv[3], sys.argv[5] == '1')",
23
+ " write_atomic(parent_fd, sys.argv[4], sys.stdin.buffer)",
24
+ " finally:",
25
+ " if parent_fd is not None:",
26
+ " os.close(parent_fd)",
27
+ " os.close(root_fd)",
28
+ "elif operation == 'create':",
29
+ " root_fd = open_dir(sys.argv[2])",
30
+ " parent_fd = None",
31
+ " try:",
32
+ " parent_fd = walk_dir(root_fd, sys.argv[3], sys.argv[5] == '1')",
33
+ " try:",
34
+ " create_exclusive(parent_fd, sys.argv[4], sys.stdin.buffer)",
35
+ " except FileExistsError:",
36
+ " sys.exit(SANDBOX_CREATE_EXISTS_EXIT_CODE)",
37
+ " finally:",
38
+ " if parent_fd is not None:",
39
+ " os.close(parent_fd)",
40
+ " os.close(root_fd)",
41
+ "elif operation == 'read':",
42
+ " root_fd = open_dir(sys.argv[2])",
43
+ " parent_fd = None",
44
+ " try:",
45
+ " parent_fd = walk_dir(root_fd, sys.argv[3], False)",
46
+ " if len(sys.argv) > 5:",
47
+ " read_file_bounded(parent_fd, sys.argv[4], int(sys.argv[5]))",
48
+ " else:",
49
+ " read_file(parent_fd, sys.argv[4])",
50
+ " except FileNotFoundError:",
51
+ " sys.exit(SANDBOX_READ_NOT_FOUND_EXIT_CODE)",
52
+ " finally:",
53
+ " if parent_fd is not None:",
54
+ " os.close(parent_fd)",
55
+ " os.close(root_fd)",
56
+ "elif operation == 'readdir':",
57
+ " root_fd = open_dir(sys.argv[2])",
58
+ " target_fd = None",
59
+ " try:",
60
+ " target_fd = walk_dir(root_fd, sys.argv[3], False)",
61
+ " with os.scandir(target_fd) as entries:",
62
+ " print(json.dumps([{'name': entry.name, 'isDirectory': entry.is_dir(follow_symlinks=False)} for entry in entries]))",
63
+ " finally:",
64
+ " if target_fd is not None:",
65
+ " os.close(target_fd)",
66
+ " os.close(root_fd)",
67
+ "elif operation == 'mkdirp':",
68
+ " root_fd = open_dir(sys.argv[2])",
69
+ " target_fd = None",
70
+ " try:",
71
+ " target_fd = walk_dir(root_fd, sys.argv[3], True)",
72
+ " os.fsync(target_fd)",
73
+ " finally:",
74
+ " if target_fd is not None:",
75
+ " os.close(target_fd)",
76
+ " os.close(root_fd)",
77
+ "elif operation == 'remove':",
78
+ " root_fd = open_dir(sys.argv[2])",
79
+ " parent_fd = None",
80
+ " try:",
81
+ " parent_fd = walk_dir(root_fd, sys.argv[3], False)",
82
+ " try:",
83
+ " if sys.argv[5] == '1':",
84
+ " remove_tree(parent_fd, sys.argv[4])",
85
+ " else:",
86
+ " entry_stat = os.lstat(sys.argv[4], dir_fd=parent_fd)",
87
+ " if stat.S_ISDIR(entry_stat.st_mode) and not stat.S_ISLNK(entry_stat.st_mode):",
88
+ " os.rmdir(sys.argv[4], dir_fd=parent_fd)",
89
+ " else:",
90
+ " os.unlink(sys.argv[4], dir_fd=parent_fd)",
91
+ " os.fsync(parent_fd)",
92
+ " except FileNotFoundError:",
93
+ " if sys.argv[6] != '1':",
94
+ " raise",
95
+ " finally:",
96
+ " if parent_fd is not None:",
97
+ " os.close(parent_fd)",
98
+ " os.close(root_fd)",
99
+ "elif operation == 'rename':",
100
+ " src_root_fd = open_dir(sys.argv[2])",
101
+ " dst_root_fd = open_dir(sys.argv[5])",
102
+ " src_parent_fd = None",
103
+ " dst_parent_fd = None",
104
+ " try:",
105
+ " src_parent_fd = walk_dir(src_root_fd, sys.argv[3], False)",
106
+ " dst_parent_fd = walk_dir(dst_root_fd, sys.argv[6], sys.argv[8] == '1')",
107
+ " move_entry(src_parent_fd, sys.argv[4], dst_parent_fd, sys.argv[7])",
108
+ " finally:",
109
+ " if src_parent_fd is not None:",
110
+ " os.close(src_parent_fd)",
111
+ " if dst_parent_fd is not None:",
112
+ " os.close(dst_parent_fd)",
113
+ " os.close(src_root_fd)",
114
+ " os.close(dst_root_fd)",
115
+ "else:",
116
+ " raise RuntimeError('unknown sandbox mutation operation: ' + operation)",
117
+ ].join("\n");
@@ -0,0 +1,4 @@
1
+ export declare const GUEST_FILESYSTEM_CREATE_STAGING_PYTHON: string;
2
+ export declare const GUEST_FILESYSTEM_RENAME_NO_REPLACE_PYTHON: string;
3
+ export declare const GUEST_FILESYSTEM_CREATE_EXCLUSIVE_PYTHON: string;
4
+ //# sourceMappingURL=guest-native-python.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guest-native-python.d.ts","sourceRoot":"","sources":["../src/guest-native-python.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,sCAAsC,QA+BvC,CAAC;AAEb,eAAO,MAAM,yCAAyC,QAgD1C,CAAC;AAEb,eAAO,MAAM,wCAAwC,QAqDzC,CAAC"}