@openclaw/fs-safe 0.5.1 → 0.5.3

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 (231) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +19 -6
  3. package/dist/absolute-path.d.ts.map +1 -1
  4. package/dist/absolute-path.js +20 -12
  5. package/dist/archive-entry.d.ts +1 -0
  6. package/dist/archive-entry.d.ts.map +1 -1
  7. package/dist/archive-entry.js +37 -6
  8. package/dist/archive-errors.d.ts +1 -0
  9. package/dist/archive-errors.d.ts.map +1 -1
  10. package/dist/archive-errors.js +4 -0
  11. package/dist/archive-input.d.ts +7 -0
  12. package/dist/archive-input.d.ts.map +1 -1
  13. package/dist/archive-input.js +11 -1
  14. package/dist/archive-limits.js +1 -1
  15. package/dist/archive-native.d.ts.map +1 -1
  16. package/dist/archive-native.js +12 -6
  17. package/dist/archive-policy.d.ts.map +1 -1
  18. package/dist/archive-policy.js +2 -1
  19. package/dist/archive-read.d.ts.map +1 -1
  20. package/dist/archive-read.js +62 -37
  21. package/dist/archive-staging.d.ts.map +1 -1
  22. package/dist/archive-staging.js +3 -2
  23. package/dist/archive-tar-meta.d.ts.map +1 -1
  24. package/dist/archive-tar-meta.js +15 -1
  25. package/dist/archive-tar-runtime.d.ts +1 -0
  26. package/dist/archive-tar-runtime.d.ts.map +1 -1
  27. package/dist/archive-tar-runtime.js +11 -0
  28. package/dist/archive-tar.d.ts.map +1 -1
  29. package/dist/archive-tar.js +8 -4
  30. package/dist/archive-zip-entry.d.ts +7 -1
  31. package/dist/archive-zip-entry.d.ts.map +1 -1
  32. package/dist/archive-zip-entry.js +11 -1
  33. package/dist/archive-zip-integrity.d.ts +5 -0
  34. package/dist/archive-zip-integrity.d.ts.map +1 -0
  35. package/dist/archive-zip-integrity.js +54 -0
  36. package/dist/archive-zip-preflight.d.ts.map +1 -1
  37. package/dist/archive-zip-preflight.js +12 -1
  38. package/dist/archive.d.ts.map +1 -1
  39. package/dist/archive.js +9 -5
  40. package/dist/bounded-read-stream.d.ts.map +1 -1
  41. package/dist/bounded-read-stream.js +13 -2
  42. package/dist/deny-mutations.d.ts +1 -0
  43. package/dist/deny-mutations.d.ts.map +1 -1
  44. package/dist/deny-mutations.js +6 -39
  45. package/dist/device-path.d.ts +1 -0
  46. package/dist/device-path.d.ts.map +1 -1
  47. package/dist/device-path.js +6 -6
  48. package/dist/directory-guard.d.ts.map +1 -1
  49. package/dist/directory-guard.js +5 -4
  50. package/dist/error-detail.d.ts +2 -0
  51. package/dist/error-detail.d.ts.map +1 -0
  52. package/dist/error-detail.js +4 -0
  53. package/dist/errors.d.ts +1 -1
  54. package/dist/errors.d.ts.map +1 -1
  55. package/dist/errors.js +4 -0
  56. package/dist/file-hash.d.ts.map +1 -1
  57. package/dist/file-hash.js +4 -1
  58. package/dist/file-lock-sync.d.ts.map +1 -1
  59. package/dist/file-lock-sync.js +88 -35
  60. package/dist/file-store-boundary.d.ts +6 -0
  61. package/dist/file-store-boundary.d.ts.map +1 -1
  62. package/dist/file-store-boundary.js +20 -5
  63. package/dist/file-store-sync-write.d.ts +9 -0
  64. package/dist/file-store-sync-write.d.ts.map +1 -0
  65. package/dist/file-store-sync-write.js +97 -0
  66. package/dist/file-store.d.ts.map +1 -1
  67. package/dist/file-store.js +62 -139
  68. package/dist/filename.d.ts.map +1 -1
  69. package/dist/filename.js +27 -0
  70. package/dist/guarded-mkdir.d.ts.map +1 -1
  71. package/dist/guarded-mkdir.js +5 -6
  72. package/dist/install-path.js +1 -1
  73. package/dist/json-document-store.d.ts.map +1 -1
  74. package/dist/json-document-store.js +19 -13
  75. package/dist/json.d.ts.map +1 -1
  76. package/dist/json.js +1 -3
  77. package/dist/local-file-access.d.ts +3 -2
  78. package/dist/local-file-access.d.ts.map +1 -1
  79. package/dist/local-file-access.js +12 -6
  80. package/dist/local-roots.d.ts.map +1 -1
  81. package/dist/local-roots.js +35 -30
  82. package/dist/move-path.d.ts.map +1 -1
  83. package/dist/move-path.js +51 -12
  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 +1 -0
  92. package/dist/native-binding.d.ts.map +1 -1
  93. package/dist/native-operations.d.ts.map +1 -1
  94. package/dist/native-operations.js +14 -1
  95. package/dist/native-pinned-write.d.ts.map +1 -1
  96. package/dist/native-pinned-write.js +44 -17
  97. package/dist/output-sibling.d.ts.map +1 -1
  98. package/dist/output-sibling.js +6 -5
  99. package/dist/path-policy.d.ts.map +1 -1
  100. package/dist/path-policy.js +1 -7
  101. package/dist/path.d.ts.map +1 -1
  102. package/dist/path.js +4 -0
  103. package/dist/permission-exec.d.ts +6 -0
  104. package/dist/permission-exec.d.ts.map +1 -0
  105. package/dist/permission-exec.js +28 -0
  106. package/dist/permissions-windows.d.ts +56 -0
  107. package/dist/permissions-windows.d.ts.map +1 -0
  108. package/dist/permissions-windows.js +285 -0
  109. package/dist/permissions.d.ts +3 -46
  110. package/dist/permissions.d.ts.map +1 -1
  111. package/dist/permissions.js +17 -328
  112. package/dist/pinned-open.d.ts.map +1 -1
  113. package/dist/pinned-open.js +21 -4
  114. package/dist/pinned-write.js +2 -1
  115. package/dist/private-temp-workspace.d.ts.map +1 -1
  116. package/dist/private-temp-workspace.js +45 -4
  117. package/dist/read-error.d.ts +2 -0
  118. package/dist/read-error.d.ts.map +1 -0
  119. package/dist/read-error.js +11 -0
  120. package/dist/regular-file.d.ts.map +1 -1
  121. package/dist/regular-file.js +21 -2
  122. package/dist/replace-directory.d.ts.map +1 -1
  123. package/dist/replace-directory.js +10 -1
  124. package/dist/replace-file-copy-fallback.d.ts +3 -0
  125. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  126. package/dist/replace-file-copy-fallback.js +18 -11
  127. package/dist/replace-file-descriptor.d.ts +33 -0
  128. package/dist/replace-file-descriptor.d.ts.map +1 -0
  129. package/dist/replace-file-descriptor.js +96 -0
  130. package/dist/replace-file.d.ts +9 -2
  131. package/dist/replace-file.d.ts.map +1 -1
  132. package/dist/replace-file.js +64 -72
  133. package/dist/root-context.d.ts +8 -0
  134. package/dist/root-context.d.ts.map +1 -1
  135. package/dist/root-context.js +43 -2
  136. package/dist/root-errors.d.ts +7 -0
  137. package/dist/root-errors.d.ts.map +1 -1
  138. package/dist/root-errors.js +46 -1
  139. package/dist/root-file.d.ts +1 -0
  140. package/dist/root-file.d.ts.map +1 -1
  141. package/dist/root-file.js +2 -0
  142. package/dist/root-impl.d.ts.map +1 -1
  143. package/dist/root-impl.js +216 -169
  144. package/dist/root-path-symlink.d.ts +3 -0
  145. package/dist/root-path-symlink.d.ts.map +1 -0
  146. package/dist/root-path-symlink.js +35 -0
  147. package/dist/root-path.d.ts +1 -0
  148. package/dist/root-path.d.ts.map +1 -1
  149. package/dist/root-path.js +129 -190
  150. package/dist/root-walk.d.ts.map +1 -1
  151. package/dist/root-walk.js +15 -0
  152. package/dist/safe-path-segment.d.ts +2 -0
  153. package/dist/safe-path-segment.d.ts.map +1 -1
  154. package/dist/safe-path-segment.js +12 -0
  155. package/dist/secret-file.d.ts.map +1 -1
  156. package/dist/secret-file.js +29 -6
  157. package/dist/secret-read-async.d.ts.map +1 -1
  158. package/dist/secret-read-async.js +5 -1
  159. package/dist/secure-temp-dir.d.ts.map +1 -1
  160. package/dist/secure-temp-dir.js +6 -1
  161. package/dist/short-path.d.ts +2 -0
  162. package/dist/short-path.d.ts.map +1 -0
  163. package/dist/short-path.js +7 -0
  164. package/dist/sidecar-lock-acquire.d.ts +30 -0
  165. package/dist/sidecar-lock-acquire.d.ts.map +1 -0
  166. package/dist/sidecar-lock-acquire.js +260 -0
  167. package/dist/sidecar-lock-policy.d.ts +3 -0
  168. package/dist/sidecar-lock-policy.d.ts.map +1 -1
  169. package/dist/sidecar-lock-policy.js +20 -3
  170. package/dist/sidecar-lock-reclaim.d.ts +5 -1
  171. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  172. package/dist/sidecar-lock-reclaim.js +59 -9
  173. package/dist/sidecar-lock.d.ts.map +1 -1
  174. package/dist/sidecar-lock.js +27 -226
  175. package/dist/symlink-parents.d.ts.map +1 -1
  176. package/dist/symlink-parents.js +9 -8
  177. package/dist/temp-target.d.ts.map +1 -1
  178. package/dist/temp-target.js +15 -4
  179. package/dist/test-hooks.d.ts +1 -0
  180. package/dist/test-hooks.d.ts.map +1 -1
  181. package/dist/timing.d.ts +2 -0
  182. package/dist/timing.d.ts.map +1 -1
  183. package/dist/timing.js +9 -0
  184. package/dist/trash.d.ts.map +1 -1
  185. package/dist/trash.js +10 -1
  186. package/dist/walk.d.ts.map +1 -1
  187. package/dist/walk.js +15 -0
  188. package/docs/advanced.md +7 -1
  189. package/docs/archive.md +27 -20
  190. package/docs/assets/readme-banner.jpg +0 -0
  191. package/docs/atomic.md +48 -19
  192. package/docs/errors.md +34 -8
  193. package/docs/file-store.md +14 -1
  194. package/docs/filename.md +17 -16
  195. package/docs/index.md +1 -0
  196. package/docs/install-path.md +9 -6
  197. package/docs/json.md +8 -2
  198. package/docs/local-roots.md +79 -103
  199. package/docs/native-helper.md +11 -5
  200. package/docs/native.md +2 -2
  201. package/docs/output.md +8 -3
  202. package/docs/path-scope.md +4 -1
  203. package/docs/path.md +7 -3
  204. package/docs/permissions.md +8 -0
  205. package/docs/public-api.md +109 -0
  206. package/docs/quickstart.md +6 -2
  207. package/docs/reading.md +13 -10
  208. package/docs/root.md +26 -4
  209. package/docs/secret-file.md +21 -4
  210. package/docs/security-model.md +21 -4
  211. package/docs/sidecar-lock.md +15 -5
  212. package/docs/temp.md +42 -5
  213. package/docs/test-hooks.md +2 -0
  214. package/docs/types.md +32 -15
  215. package/docs/writing.md +25 -12
  216. package/package.json +15 -4
  217. package/dist/archive-file-io.d.ts +0 -9
  218. package/dist/archive-file-io.d.ts.map +0 -1
  219. package/dist/archive-file-io.js +0 -11
  220. package/dist/file-store-source.d.ts +0 -5
  221. package/dist/file-store-source.d.ts.map +0 -1
  222. package/dist/file-store-source.js +0 -30
  223. package/dist/file-url.d.ts +0 -2
  224. package/dist/file-url.d.ts.map +0 -1
  225. package/dist/file-url.js +0 -1
  226. package/dist/path-stat.d.ts +0 -4
  227. package/dist/path-stat.d.ts.map +0 -1
  228. package/dist/path-stat.js +0 -15
  229. package/dist/windows-permissions-native.d.ts +0 -9
  230. package/dist/windows-permissions-native.d.ts.map +0 -1
  231. package/dist/windows-permissions-native.js +0 -31
@@ -1,52 +1,6 @@
1
- import { execFile } from "node:child_process";
2
1
  import fs from "node:fs/promises";
3
- import os from "node:os";
4
- import path from "node:path";
5
- import { promisify } from "node:util";
6
- import { normalizeLowercaseStringOrEmpty } from "./string-coerce.js";
7
- import { inspectWindowsPermissionsNative } from "./windows-permissions-native.js";
8
- import { resolveWindowsSystemCommand } from "./windows-command.js";
9
- import { inspectWindowsOwner, resolveWindowsCurrentUserSid, resolveWindowsPrincipalSids, } from "./windows-owner.js";
10
- const execFileAsync = promisify(execFile);
11
- const INHERIT_FLAGS = new Set(["I", "OI", "CI", "IO", "NP"]);
12
- const WORLD_PRINCIPALS = new Set(["everyone", "users", "builtin\\users", "authenticated users", "nt authority\\authenticated users", "anonymous logon", "nt authority\\anonymous logon", "guests", "builtin\\guests", "interactive", "nt authority\\interactive", "network", "nt authority\\network", "local"]);
13
- const TRUSTED_BASE = new Set([
14
- "nt authority\\system",
15
- "system",
16
- "builtin\\administrators",
17
- "creator owner",
18
- "autorite nt\\système",
19
- "nt-autorität\\system",
20
- "autoridad nt\\system",
21
- "autoridade nt\\system",
22
- ]);
23
- const WORLD_SUFFIXES = ["\\users", "\\authenticated users"];
24
- const SID_RE = /^\*?s-\d+-\d+(-\d+)+$/i;
25
- const TRUSTED_SIDS = new Set([
26
- "s-1-5-18",
27
- "s-1-5-32-544",
28
- "s-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464",
29
- ]);
30
- const WORLD_SIDS = new Set(["s-1-1-0", "s-1-5-11", "s-1-5-32-545", "s-1-5-7", "s-1-5-32-546", "s-1-5-4", "s-1-2-0", "s-1-5-2"]);
31
- const STATUS_PREFIXES = [
32
- "successfully processed",
33
- "processed",
34
- "failed processing",
35
- "no mapping between account names",
36
- ];
37
- function stripDiacritics(value) {
38
- return value.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
39
- }
40
- const TRUSTED_BASE_ASCII = new Set([...TRUSTED_BASE].map(stripDiacritics));
41
- const normalize = (value) => normalizeLowercaseStringOrEmpty(value);
42
- const defaultWindowsUserInfo = () => os.userInfo();
43
- function defaultPermissionExec(command, args) {
44
- return execFileAsync(command, args, {
45
- encoding: "utf8",
46
- windowsHide: true,
47
- maxBuffer: 1024 * 1024,
48
- });
49
- }
2
+ import { formatIcaclsResetCommand, inspectWindowsPermissions, } from "./permissions-windows.js";
3
+ export { createIcaclsResetCommand, formatIcaclsResetCommand, formatWindowsAclSummary, inspectWindowsAcl, parseIcaclsOutput, resolveWindowsUserPrincipal, summarizeWindowsAcl, } from "./permissions-windows.js";
50
4
  export async function safeStat(targetPath) {
51
5
  try {
52
6
  const lst = await fs.lstat(targetPath);
@@ -103,58 +57,14 @@ export async function inspectPathPermissions(targetPath, opts) {
103
57
  const bits = modeBits(effectiveMode);
104
58
  const platform = opts?.platform ?? process.platform;
105
59
  if (platform === "win32") {
106
- const native = inspectWindowsPermissionsNative({
107
- targetPath, stat: st, effectiveIsDir, effectiveMode, bits,
108
- });
109
- if (native)
110
- return native;
111
- const owner = await inspectWindowsOwner({
60
+ return await inspectWindowsPermissions({
112
61
  targetPath,
113
- env: opts?.env,
114
- exec: opts?.exec ?? defaultPermissionExec,
115
- });
116
- const acl = await inspectWindowsAcl(targetPath, {
117
- env: opts?.env,
118
- exec: opts?.exec,
119
- currentUserSid: owner.currentUserSid,
120
- principalSids: owner.principalSids,
121
- principalTranslationFailed: owner.principalTranslationFailed,
122
- });
123
- const ownerFields = {
124
- ...(owner.sid ? { ownerSid: owner.sid } : {}),
125
- ...(owner.trusted !== undefined ? { ownerTrusted: owner.trusted } : {}),
126
- ...(owner.error ? { ownerError: owner.error } : {}),
127
- };
128
- if (!acl.ok) {
129
- return {
130
- ok: true,
131
- isSymlink: st.isSymlink,
132
- isDir: effectiveIsDir,
133
- mode: effectiveMode,
134
- bits,
135
- source: "unknown",
136
- worldWritable: false,
137
- groupWritable: false,
138
- worldReadable: false,
139
- groupReadable: false,
140
- ...ownerFields,
141
- error: acl.error,
142
- };
143
- }
144
- return {
145
- ok: true,
146
- isSymlink: st.isSymlink,
147
- isDir: effectiveIsDir,
148
- mode: effectiveMode,
62
+ stat: st,
63
+ effectiveIsDir,
64
+ effectiveMode,
149
65
  bits,
150
- source: "windows-acl",
151
- worldWritable: acl.untrustedWorld.some((entry) => entry.canWrite),
152
- groupWritable: acl.untrustedGroup.some((entry) => entry.canWrite),
153
- worldReadable: acl.untrustedWorld.some((entry) => entry.canRead),
154
- groupReadable: acl.untrustedGroup.some((entry) => entry.canRead),
155
- ...ownerFields,
156
- aclSummary: formatWindowsAclSummary(acl),
157
- };
66
+ opts,
67
+ });
158
68
  }
159
69
  return {
160
70
  ok: true,
@@ -182,7 +92,15 @@ export function formatPermissionRemediation(params) {
182
92
  env: params.env,
183
93
  });
184
94
  }
185
- return `chmod ${params.posixMode.toString(8).padStart(3, "0")} ${params.targetPath}`;
95
+ const optionSeparator = params.targetPath.startsWith("-") ? "-- " : "";
96
+ return (`chmod ${params.posixMode.toString(8).padStart(3, "0")} ${optionSeparator}` +
97
+ formatPosixShellArgument(params.targetPath));
98
+ }
99
+ function formatPosixShellArgument(value) {
100
+ if (value && /^[A-Za-z0-9_@%+=:,./-]+$/.test(value)) {
101
+ return value;
102
+ }
103
+ return `'${value.replaceAll("'", `'\\''`)}'`;
186
104
  }
187
105
  export function modeBits(mode) {
188
106
  return mode == null ? null : mode & 0o777;
@@ -202,232 +120,3 @@ export function isWorldReadable(bits) {
202
120
  export function isGroupReadable(bits) {
203
121
  return bits != null && (bits & 0o040) !== 0;
204
122
  }
205
- function normalizeSid(value) {
206
- const normalized = normalize(value);
207
- return normalized.startsWith("*") ? normalized.slice(1) : normalized;
208
- }
209
- export function resolveWindowsUserPrincipal(env, userInfo = defaultWindowsUserInfo) {
210
- const username = env?.USERNAME?.trim() || userInfo().username?.trim();
211
- if (!username) {
212
- return null;
213
- }
214
- const domain = env?.USERDOMAIN?.trim();
215
- return domain ? `${domain}\\${username}` : username;
216
- }
217
- function buildTrustedPrincipals(env) {
218
- const trusted = new Set(TRUSTED_BASE);
219
- const principal = resolveWindowsUserPrincipal(env);
220
- if (principal) {
221
- trusted.add(normalize(principal));
222
- const userOnly = principal.split("\\").at(-1);
223
- if (userOnly) {
224
- trusted.add(normalize(userOnly));
225
- }
226
- }
227
- const userSid = normalizeSid(env?.USERSID ?? "");
228
- if (userSid && SID_RE.test(userSid) && !WORLD_SIDS.has(userSid)) {
229
- trusted.add(userSid);
230
- }
231
- return trusted;
232
- }
233
- function classifyPrincipal(principal, trustedPrincipals) {
234
- const normalized = normalize(principal);
235
- if (SID_RE.test(normalized)) {
236
- const sid = normalizeSid(normalized);
237
- if (WORLD_SIDS.has(sid))
238
- return "world";
239
- if (TRUSTED_SIDS.has(sid) || trustedPrincipals.has(sid))
240
- return "trusted";
241
- return "group";
242
- }
243
- if (trustedPrincipals.has(normalized) || TRUSTED_BASE.has(normalized)) {
244
- return "trusted";
245
- }
246
- if (WORLD_PRINCIPALS.has(normalized) ||
247
- WORLD_SUFFIXES.some((suffix) => normalized.endsWith(suffix))) {
248
- return "world";
249
- }
250
- const stripped = stripDiacritics(normalized);
251
- if (stripped !== normalized && TRUSTED_BASE_ASCII.has(stripped)) {
252
- return "trusted";
253
- }
254
- return "group";
255
- }
256
- function rightsFromTokens(tokens) {
257
- const upper = tokens.join("").toUpperCase();
258
- return {
259
- canWrite: upper.includes("F") || upper.includes("M") || upper.includes("W") || upper.includes("D"),
260
- canRead: upper.includes("F") || upper.includes("M") || upper.includes("R"),
261
- };
262
- }
263
- function stripTargetPrefix(params) {
264
- if (params.lowerLine.startsWith(params.lowerTarget)) {
265
- return params.trimmedLine.slice(params.normalizedTarget.length).trim();
266
- }
267
- if (params.lowerLine.startsWith(params.quotedLower)) {
268
- return params.trimmedLine.slice(params.quotedTarget.length).trim();
269
- }
270
- return params.trimmedLine;
271
- }
272
- function parseAceEntry(entry) {
273
- if (!entry.includes("("))
274
- return null;
275
- const idx = entry.indexOf(":");
276
- if (idx === -1)
277
- return null;
278
- const principal = entry.slice(0, idx).trim();
279
- const rawRights = entry.slice(idx + 1).trim();
280
- const tokens = rawRights
281
- .match(/\(([^)]+)\)/g)
282
- ?.map((token) => token.slice(1, -1).trim())
283
- .filter(Boolean) ?? [];
284
- if (tokens.some((token) => token.toUpperCase() === "DENY"))
285
- return null;
286
- const rights = tokens.filter((token) => !INHERIT_FLAGS.has(token.toUpperCase()));
287
- if (rights.length === 0)
288
- return null;
289
- const normalizedPrincipal = normalizeSid(principal);
290
- return {
291
- principal,
292
- ...(SID_RE.test(normalizedPrincipal) ? { sid: normalizedPrincipal } : {}),
293
- rights,
294
- rawRights,
295
- ...rightsFromTokens(rights),
296
- };
297
- }
298
- export function parseIcaclsOutput(output, targetPath) {
299
- const entries = [];
300
- const normalizedTarget = targetPath.trim();
301
- const lowerTarget = normalizedTarget.toLowerCase();
302
- const quotedTarget = `"${normalizedTarget}"`;
303
- const quotedLower = quotedTarget.toLowerCase();
304
- for (const rawLine of output.split(/\r?\n/)) {
305
- const line = rawLine.trimEnd();
306
- if (!line.trim())
307
- continue;
308
- const trimmed = line.trim();
309
- const lower = trimmed.toLowerCase();
310
- if (STATUS_PREFIXES.some((prefix) => lower.startsWith(prefix)))
311
- continue;
312
- const parsed = parseAceEntry(stripTargetPrefix({
313
- trimmedLine: trimmed,
314
- lowerLine: lower,
315
- normalizedTarget,
316
- lowerTarget,
317
- quotedTarget,
318
- quotedLower,
319
- }));
320
- if (parsed)
321
- entries.push(parsed);
322
- }
323
- return entries;
324
- }
325
- export function summarizeWindowsAcl(entries, env) {
326
- const trustedPrincipals = buildTrustedPrincipals(env);
327
- const trusted = [];
328
- const untrustedWorld = [];
329
- const untrustedGroup = [];
330
- for (const entry of entries) {
331
- const classification = classifyPrincipal(entry.sid ?? entry.principal, trustedPrincipals);
332
- if (classification === "trusted")
333
- trusted.push(entry);
334
- else if (classification === "world")
335
- untrustedWorld.push(entry);
336
- else
337
- untrustedGroup.push(entry);
338
- }
339
- return { trusted, untrustedWorld, untrustedGroup };
340
- }
341
- export async function inspectWindowsAcl(targetPath, opts) {
342
- const exec = opts?.exec ?? defaultPermissionExec;
343
- try {
344
- if (opts?.principalTranslationFailed) {
345
- throw new Error("Windows ACL principal SID translation failed");
346
- }
347
- const { stdout, stderr } = await exec(resolveWindowsSystemCommand("icacls.exe", opts?.env), [
348
- targetPath,
349
- ]);
350
- let entries = parseIcaclsOutput(`${stdout}\n${stderr}`.trim(), targetPath);
351
- const unresolvedPrincipals = entries
352
- .filter((entry) => !entry.sid)
353
- .map((entry) => entry.principal);
354
- const principalSids = await resolveWindowsPrincipalSids({
355
- principals: unresolvedPrincipals,
356
- known: opts?.principalSids,
357
- env: opts?.env,
358
- exec,
359
- });
360
- entries = entries.map((entry) => {
361
- const sid = entry.sid ?? principalSids[entry.principal.toLowerCase()];
362
- if (!sid) {
363
- throw new Error(`Windows ACL principal SID could not be verified: ${entry.principal}`);
364
- }
365
- return { ...entry, sid };
366
- });
367
- let currentUserSid = normalizeSid(opts?.currentUserSid ?? "");
368
- let effectiveEnv = currentUserSid ? { USERSID: currentUserSid } : undefined;
369
- let { trusted, untrustedWorld, untrustedGroup } = summarizeWindowsAcl(entries, effectiveEnv);
370
- const needsUserSidResolution = !currentUserSid && untrustedGroup.some((entry) => entry.sid && !TRUSTED_SIDS.has(entry.sid));
371
- if (needsUserSidResolution) {
372
- currentUserSid =
373
- (await resolveWindowsCurrentUserSid({ exec, env: opts?.env })) ?? "";
374
- if (currentUserSid) {
375
- effectiveEnv = { USERSID: currentUserSid };
376
- ({ trusted, untrustedWorld, untrustedGroup } = summarizeWindowsAcl(entries, effectiveEnv));
377
- }
378
- }
379
- return { ok: true, entries, trusted, untrustedWorld, untrustedGroup };
380
- }
381
- catch (err) {
382
- return {
383
- ok: false,
384
- entries: [],
385
- trusted: [],
386
- untrustedWorld: [],
387
- untrustedGroup: [],
388
- error: String(err),
389
- };
390
- }
391
- }
392
- export function formatWindowsAclSummary(summary) {
393
- if (!summary.ok)
394
- return "unknown";
395
- const untrusted = [...summary.untrustedWorld, ...summary.untrustedGroup];
396
- return untrusted.length === 0
397
- ? "trusted-only"
398
- : untrusted.map((entry) => `${entry.principal}:${entry.rawRights}`).join(", ");
399
- }
400
- export function formatIcaclsResetCommand(targetPath, opts) {
401
- const command = resolveWindowsSystemCommand("icacls.exe", opts.env);
402
- const user = resolveWindowsUserPrincipal(opts.env, opts.userInfo) ?? "%USERNAME%";
403
- const grant = opts.isDir ? "(OI)(CI)F" : "F";
404
- return [
405
- command,
406
- `"${targetPath}"`,
407
- "/inheritance:r",
408
- "/grant:r",
409
- `"${user}:${grant}"`,
410
- "/grant:r",
411
- `"*S-1-5-18:${grant}"`,
412
- ].join(" ");
413
- }
414
- export function createIcaclsResetCommand(targetPath, opts) {
415
- const user = resolveWindowsUserPrincipal(opts.env, opts.userInfo);
416
- if (!user) {
417
- return null;
418
- }
419
- const grant = opts.isDir ? "(OI)(CI)F" : "F";
420
- const args = [
421
- targetPath,
422
- "/inheritance:r",
423
- "/grant:r",
424
- `${user}:${grant}`,
425
- "/grant:r",
426
- `*S-1-5-18:${grant}`,
427
- ];
428
- return {
429
- command: resolveWindowsSystemCommand("icacls.exe", opts.env),
430
- args,
431
- display: formatIcaclsResetCommand(targetPath, opts),
432
- };
433
- }
@@ -1 +1 @@
1
- {"version":3,"file":"pinned-open.d.ts","sourceRoot":"","sources":["../src/pinned-open.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAIzB,MAAM,MAAM,2BAA2B,GAAG,MAAM,GAAG,YAAY,GAAG,IAAI,CAAC;AAEvE,MAAM,MAAM,oBAAoB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAA;CAAE,GACtD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,2BAA2B,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAExE,MAAM,MAAM,yBAAyB,GAAG,MAAM,GAAG,WAAW,CAAC;AAE7D,MAAM,MAAM,gBAAgB,GAAG,IAAI,CACjC,OAAO,EAAE,EACT,WAAW,GAAG,WAAW,GAAG,cAAc,GAAG,UAAU,GAAG,WAAW,GAAG,WAAW,CACpF,CAAC;AAYF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,yBAAyB,CAAC;IACxC,IAAI,CAAC,EAAE,gBAAgB,CAAC;CACzB,GAAG,oBAAoB,CAiEvB"}
1
+ {"version":3,"file":"pinned-open.d.ts","sourceRoot":"","sources":["../src/pinned-open.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAKzB,MAAM,MAAM,2BAA2B,GAAG,MAAM,GAAG,YAAY,GAAG,IAAI,CAAC;AAEvE,MAAM,MAAM,oBAAoB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAA;CAAE,GACtD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,2BAA2B,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAExE,MAAM,MAAM,yBAAyB,GAAG,MAAM,GAAG,WAAW,CAAC;AAE7D,MAAM,MAAM,gBAAgB,GAAG,IAAI,CACjC,OAAO,EAAE,EACT,WAAW,GAAG,WAAW,GAAG,cAAc,GAAG,UAAU,GAAG,WAAW,GAAG,WAAW,CACpF,CAAC;AAYF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,yBAAyB,CAAC;IACxC,IAAI,CAAC,EAAE,gBAAgB,CAAC;CACzB,GAAG,oBAAoB,CAiFvB"}
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import { isUnsafeDeviceReadPath } from "./device-path.js";
3
+ import { FsSafeError } from "./errors.js";
3
4
  import { sameFileIdentity as hasSameFileIdentity } from "./file-identity.js";
4
5
  function isExpectedPathError(error) {
5
6
  const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "";
@@ -30,10 +31,18 @@ export function openPinnedFileSync(params) {
30
31
  }
31
32
  const preOpenStat = ioFs.lstatSync(realPath);
32
33
  if (!isAllowedType(preOpenStat, allowedType)) {
33
- return { ok: false, reason: "validation" };
34
+ return {
35
+ ok: false,
36
+ reason: "validation",
37
+ error: new FsSafeError("not-file", "path does not have the required file type"),
38
+ };
34
39
  }
35
40
  if (params.rejectHardlinks && preOpenStat.isFile() && preOpenStat.nlink > 1) {
36
- return { ok: false, reason: "validation" };
41
+ return {
42
+ ok: false,
43
+ reason: "validation",
44
+ error: new FsSafeError("hardlink", "path must not be hardlinked"),
45
+ };
37
46
  }
38
47
  if (params.maxBytes !== undefined &&
39
48
  preOpenStat.isFile() &&
@@ -43,10 +52,18 @@ export function openPinnedFileSync(params) {
43
52
  fd = ioFs.openSync(realPath, openReadFlags);
44
53
  const openedStat = ioFs.fstatSync(fd);
45
54
  if (!isAllowedType(openedStat, allowedType)) {
46
- return { ok: false, reason: "validation" };
55
+ return {
56
+ ok: false,
57
+ reason: "validation",
58
+ error: new FsSafeError("not-file", "path does not have the required file type"),
59
+ };
47
60
  }
48
61
  if (params.rejectHardlinks && openedStat.isFile() && openedStat.nlink > 1) {
49
- return { ok: false, reason: "validation" };
62
+ return {
63
+ ok: false,
64
+ reason: "validation",
65
+ error: new FsSafeError("hardlink", "path must not be hardlinked"),
66
+ };
50
67
  }
51
68
  if (params.maxBytes !== undefined && openedStat.isFile() && openedStat.size > params.maxBytes) {
52
69
  return { ok: false, reason: "validation" };
@@ -69,7 +69,7 @@ export async function runPinnedWriteHelper(params) {
69
69
  return await runPinnedWriteFallback(params);
70
70
  }
71
71
  const native = getNativeBinding();
72
- if (native && params.overwrite === false) {
72
+ if (native) {
73
73
  return await runPinnedWriteNative(native, params);
74
74
  }
75
75
  return await runPinnedWriteFallback(params);
@@ -107,6 +107,7 @@ async function runPinnedWriteFallback(params) {
107
107
  parentPath = await mkdirPathComponentsWithGuards({
108
108
  rootReal: params.rootPath,
109
109
  targetPath: parentPath,
110
+ beforeComponent: async (componentPath) => await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("mkdir", componentPath),
110
111
  });
111
112
  }
112
113
  const parentGuard = params.mkdir
@@ -1 +1 @@
1
- {"version":3,"file":"private-temp-workspace.d.ts","sourceRoot":"","sources":["../src/private-temp-workspace.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,KAAK,SAAS,EACd,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAEL,KAAK,uBAAuB,EAC7B,MAAM,mBAAmB,CAAC;AAE3B,MAAM,MAAM,0BAA0B,GAAG,SAAS,GAAG,SAAS,GAAG,mBAAmB,CAAC;AAErF,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,uBAAuB,CAAC;IAClC,KAAK,EAAE,SAAS,CAAC;IACjB,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACpE,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC3D,SAAS,CACP,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,OAAO,EACb,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GACtC,OAAO,CAAC,MAAM,CAAC,CAAC;IACnB,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9D,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACxC,OAAO,IAAI,OAAO,CAAC,0BAA0B,CAAC,CAAC;IAC/C,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,uBAAuB,CAAC;IAClC,KAAK,EAAE,aAAa,CAAC;IACrB,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,CAAC;IAC3D,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAClD,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,MAAM,CAAC;IAC5F,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,OAAO,IAAI,0BAA0B,CAAC;IACtC,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC;CAC1B,CAAC;AA0IF,wBAAsB,aAAa,CACjC,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,aAAa,CAAC,CAExB;AAED,wBAAsB,iBAAiB,CAAC,CAAC,EACvC,OAAO,EAAE,oBAAoB,EAC7B,GAAG,EAAE,CAAC,SAAS,EAAE,aAAa,KAAK,OAAO,CAAC,CAAC,CAAC,GAC5C,OAAO,CAAC,CAAC,CAAC,CAUZ;AAED,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,oBAAoB,GAC5B,iBAAiB,CAsEnB;AAED,wBAAgB,qBAAqB,CAAC,CAAC,EACrC,OAAO,EAAE,oBAAoB,EAC7B,GAAG,EAAE,CAAC,SAAS,EAAE,iBAAiB,KAAK,CAAC,GACvC,CAAC,CAUH"}
1
+ {"version":3,"file":"private-temp-workspace.d.ts","sourceRoot":"","sources":["../src/private-temp-workspace.ts"],"names":[],"mappings":"AAIA,OAAO,EAGL,KAAK,SAAS,EACd,KAAK,aAAa,EACnB,MAAM,iBAAiB,CAAC;AAUzB,OAAO,EAEL,KAAK,uBAAuB,EAC7B,MAAM,mBAAmB,CAAC;AAE3B,MAAM,MAAM,0BAA0B,GAAG,SAAS,GAAG,SAAS,GAAG,mBAAmB,CAAC;AAErF,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,uBAAuB,CAAC;IAClC,KAAK,EAAE,SAAS,CAAC;IACjB,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACpE,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC3D,SAAS,CACP,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,OAAO,EACb,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GACtC,OAAO,CAAC,MAAM,CAAC,CAAC;IACnB,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9D,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACxC,OAAO,IAAI,OAAO,CAAC,0BAA0B,CAAC,CAAC;IAC/C,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,uBAAuB,CAAC;IAClC,KAAK,EAAE,aAAa,CAAC;IACrB,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,CAAC;IAC3D,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAClD,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,MAAM,CAAC;IAC5F,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;IAC/B,OAAO,IAAI,0BAA0B,CAAC;IACtC,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC;CAC1B,CAAC;AA2KF,wBAAsB,aAAa,CACjC,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,aAAa,CAAC,CAExB;AAED,wBAAsB,iBAAiB,CAAC,CAAC,EACvC,OAAO,EAAE,oBAAoB,EAC7B,GAAG,EAAE,CAAC,SAAS,EAAE,aAAa,KAAK,OAAO,CAAC,CAAC,CAAC,GAC5C,OAAO,CAAC,CAAC,CAAC,CAUZ;AAED,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,oBAAoB,GAC5B,iBAAiB,CA0EnB;AAED,wBAAgB,qBAAqB,CAAC,CAAC,EACrC,OAAO,EAAE,oBAAoB,EAC7B,GAAG,EAAE,CAAC,SAAS,EAAE,iBAAiB,KAAK,CAAC,GACvC,CAAC,CAUH"}
@@ -3,8 +3,11 @@ import fsSync from "node:fs";
3
3
  import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { fileStore, fileStoreSync, } from "./file-store.js";
6
- import { openRootFileSync } from "./root-file.js";
6
+ import { FsSafeError } from "./errors.js";
7
7
  import { sameFileIdentity } from "./file-identity.js";
8
+ import { isNotFoundPathError } from "./path.js";
9
+ import { throwFsSafeReadError } from "./read-error.js";
10
+ import { matchRootFileOpenFailure, openRootFileSync, } from "./root-file.js";
8
11
  import { registerTempPathForExit, } from "./temp-cleanup.js";
9
12
  function sanitizeTempPrefix(prefix) {
10
13
  const sanitized = prefix.trim().replace(/[^a-zA-Z0-9._-]/g, "-");
@@ -29,6 +32,32 @@ function assertWorkspaceFileName(fileName) {
29
32
  }
30
33
  return value;
31
34
  }
35
+ function throwTempWorkspaceOpenFailure(failure) {
36
+ return matchRootFileOpenFailure(failure, {
37
+ path: ({ error }) => {
38
+ if (isNotFoundPathError(error)) {
39
+ throw new FsSafeError("not-found", "temp workspace file not found", { cause: error });
40
+ }
41
+ throw new FsSafeError("path-mismatch", "temp workspace target changed during read", {
42
+ cause: error,
43
+ });
44
+ },
45
+ validation: ({ error }) => {
46
+ if (error instanceof FsSafeError) {
47
+ throw error;
48
+ }
49
+ throw new FsSafeError("path-mismatch", "temp workspace target failed read validation", {
50
+ cause: error,
51
+ });
52
+ },
53
+ io: ({ error }) => throwFsSafeReadError(error, "temp workspace"),
54
+ fallback: ({ error }) => {
55
+ throw new FsSafeError("path-mismatch", "temp workspace target changed during read", {
56
+ cause: error,
57
+ });
58
+ },
59
+ });
60
+ }
32
61
  async function ensurePrivateDirectory(dir, mode) {
33
62
  await fs.mkdir(dir, { recursive: true, mode });
34
63
  const stat = await fs.stat(dir);
@@ -105,7 +134,14 @@ async function createTempWorkspace(options) {
105
134
  trailingNewline: writeOptions?.trailingNewline,
106
135
  }),
107
136
  copyIn: async (fileName, sourcePath) => await store.copyIn(assertWorkspaceFileName(fileName), sourcePath, { mode }),
108
- read: async (fileName) => await store.readBytes(assertWorkspaceFileName(fileName)),
137
+ read: async (fileName) => {
138
+ try {
139
+ return await store.readBytes(assertWorkspaceFileName(fileName));
140
+ }
141
+ catch (error) {
142
+ throwFsSafeReadError(error, "temp workspace");
143
+ }
144
+ },
109
145
  cleanup: async () => {
110
146
  try {
111
147
  return await cleanupWorkspace(dir, identity);
@@ -184,10 +220,15 @@ export function tempWorkspaceSync(options) {
184
220
  rejectHardlinks: true,
185
221
  });
186
222
  if (!opened.ok) {
187
- throw Object.assign(new Error(`File not found: ${fileName}`), { code: "ENOENT" });
223
+ throwTempWorkspaceOpenFailure(opened);
188
224
  }
189
225
  try {
190
- return fsSync.readFileSync(opened.fd);
226
+ try {
227
+ return fsSync.readFileSync(opened.fd);
228
+ }
229
+ catch (error) {
230
+ throwFsSafeReadError(error, "temp workspace");
231
+ }
191
232
  }
192
233
  finally {
193
234
  fsSync.closeSync(opened.fd);
@@ -0,0 +1,2 @@
1
+ export declare function throwFsSafeReadError(error: unknown, label: string): never;
2
+ //# sourceMappingURL=read-error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-error.d.ts","sourceRoot":"","sources":["../src/read-error.ts"],"names":[],"mappings":"AAGA,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,KAAK,CAQzE"}
@@ -0,0 +1,11 @@
1
+ import { FsSafeError } from "./errors.js";
2
+ import { isNodeError } from "./path.js";
3
+ export function throwFsSafeReadError(error, label) {
4
+ if (error instanceof FsSafeError) {
5
+ throw error;
6
+ }
7
+ if (isNodeError(error)) {
8
+ throw new FsSafeError("read-failed", `${label} target could not be read`, { cause: error });
9
+ }
10
+ throw error;
11
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"regular-file.d.ts","sourceRoot":"","sources":["../src/regular-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,MAAM,MAAM,SAAS,CAAC;AAW7B,MAAM,MAAM,qBAAqB,GAAG;IAAE,OAAO,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,KAAK,CAAA;CAAE,CAAC;AAExF,KAAK,8BAA8B,GAAG,IAAI,CACxC,OAAO,MAAM,CAAC,SAAS,EACvB,UAAU,GAAG,SAAS,GAAG,UAAU,CACpC,GACC,OAAO,CAAC,IAAI,CAAC,OAAO,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC,CAAC;AAEvD,MAAM,MAAM,wBAAwB,GAAG;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC,CAAC;AAEF,wBAAgB,6BAA6B,CAC3C,SAAS,GAAE,8BAAiD,GAC3D,MAAM,CAQR;AAsBD,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CActF;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,qBAAqB,CAc3E;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,KAAK,CAAA;CAAE,CAAC,CAyD3C;AAoDD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG;IACpF,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,KAAK,CAAC;CACb,CAqBA;AAmBD,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuDxF;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,wBAAwB,GAAG,IAAI,CA2D7E"}
1
+ {"version":3,"file":"regular-file.d.ts","sourceRoot":"","sources":["../src/regular-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,MAAM,MAAM,SAAS,CAAC;AAW7B,MAAM,MAAM,qBAAqB,GAAG;IAAE,OAAO,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,KAAK,CAAA;CAAE,CAAC;AAExF,KAAK,8BAA8B,GAAG,IAAI,CACxC,OAAO,MAAM,CAAC,SAAS,EACvB,UAAU,GAAG,SAAS,GAAG,UAAU,CACpC,GACC,OAAO,CAAC,IAAI,CAAC,OAAO,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC,CAAC;AAEvD,MAAM,MAAM,wBAAwB,GAAG;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC,CAAC;AAEF,wBAAgB,6BAA6B,CAC3C,SAAS,GAAE,8BAAiD,GAC3D,MAAM,CAQR;AAsBD,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CActF;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,qBAAqB,CAc3E;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,KAAK,CAAA;CAAE,CAAC,CAyD3C;AA6DD,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG;IACpF,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,KAAK,CAAC;CACb,CA6BA;AAmBD,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuDxF;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,wBAAwB,GAAG,IAAI,CA2D7E"}
@@ -133,9 +133,19 @@ function verifyStableReadTarget(params) {
133
133
  }
134
134
  function readOpenedRegularFileSync(params) {
135
135
  const stat = fsSync.fstatSync(params.fd);
136
+ let pathStat;
137
+ try {
138
+ pathStat = fsSync.lstatSync(params.filePath);
139
+ }
140
+ catch (error) {
141
+ if (isNotFoundPathError(error)) {
142
+ throw new FsSafeError("path-mismatch", `File changed during read: ${params.filePath}`);
143
+ }
144
+ throw error;
145
+ }
136
146
  verifyStableReadTarget({
137
147
  filePath: params.filePath,
138
- pathStat: fsSync.lstatSync(params.filePath),
148
+ pathStat,
139
149
  postOpenStat: stat,
140
150
  preOpenStat: params.preOpenStat,
141
151
  });
@@ -168,7 +178,16 @@ export function readRegularFileSync(params) {
168
178
  if (params.maxBytes !== undefined && result.stat.size > params.maxBytes) {
169
179
  throw regularFileTooLargeError(params.filePath, params.maxBytes);
170
180
  }
171
- const fd = fsSync.openSync(params.filePath, resolveRegularFileReadFlags());
181
+ let fd;
182
+ try {
183
+ fd = fsSync.openSync(params.filePath, resolveRegularFileReadFlags());
184
+ }
185
+ catch (error) {
186
+ if (isNotFoundPathError(error)) {
187
+ throw new FsSafeError("path-mismatch", `File changed during read: ${params.filePath}`);
188
+ }
189
+ throw error;
190
+ }
172
191
  try {
173
192
  return readOpenedRegularFileSync({
174
193
  fd,
@@ -1 +1 @@
1
- {"version":3,"file":"replace-directory.d.ts","sourceRoot":"","sources":["../src/replace-directory.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,6BAA6B,GAAG;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,6BAA6B,GACrC,OAAO,CAAC,IAAI,CAAC,CAiCf"}
1
+ {"version":3,"file":"replace-directory.d.ts","sourceRoot":"","sources":["../src/replace-directory.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,6BAA6B,GAAG;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,6BAA6B,GACrC,OAAO,CAAC,IAAI,CAAC,CAKf"}
@@ -1,11 +1,20 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import fs from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import { guardedRename, guardedRm } from "./guarded-mutation.js";
5
+ import { assertSafePathPrefix } from "./safe-path-segment.js";
6
+ import { serializePathWrite } from "./write-queue.js";
4
7
  export async function replaceDirectoryAtomic(options) {
5
8
  const targetDir = path.resolve(options.targetDir);
9
+ await serializePathWrite(targetDir, async () => {
10
+ await replaceDirectoryAtomicUnserialized(options, targetDir);
11
+ });
12
+ }
13
+ async function replaceDirectoryAtomicUnserialized(options, targetDir) {
6
14
  const stagedDir = path.resolve(options.stagedDir);
7
15
  const parentDir = path.dirname(targetDir);
8
- const backupDir = path.join(parentDir, `${options.backupPrefix ?? ".fs-safe-dir-backup-"}${process.pid}-${Date.now()}`);
16
+ const backupPrefix = assertSafePathPrefix(options.backupPrefix ?? ".fs-safe-dir-backup-", { label: "atomic directory backup prefix" });
17
+ const backupDir = path.join(parentDir, `${backupPrefix}${process.pid}-${randomUUID()}`);
9
18
  let backupCreated = false;
10
19
  await fs.mkdir(parentDir, { recursive: true });
11
20
  try {
@@ -21,6 +21,7 @@ export declare function copyFallbackReplace(params: {
21
21
  destinationHardlinks?: ReplaceFileDestinationHardlinkPolicy;
22
22
  restore: ReplaceFileCopyFallbackRestorePolicy;
23
23
  maxRestoreBytes?: number;
24
+ sync: boolean;
24
25
  }): Promise<void>;
25
26
  export declare function copyFallbackReplaceSync(params: {
26
27
  fsModule: SyncFallbackFs;
@@ -29,6 +30,8 @@ export declare function copyFallbackReplaceSync(params: {
29
30
  destinationHardlinks?: ReplaceFileDestinationHardlinkPolicy;
30
31
  restore: ReplaceFileCopyFallbackRestorePolicy;
31
32
  maxRestoreBytes?: number;
33
+ fchmodSync?: (fd: number, mode: number) => void;
34
+ sync: boolean;
32
35
  }): void;
33
36
  export {};
34
37
  //# sourceMappingURL=replace-file-copy-fallback.d.ts.map
@@ -1 +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"}
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;AAmID,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;IACzB,IAAI,EAAE,OAAO,CAAC;CACf,GAAG,OAAO,CAAC,IAAI,CAAC,CA+DhB;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;IACzB,UAAU,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,IAAI,EAAE,OAAO,CAAC;CACf,GAAG,IAAI,CA6EP"}