@openclaw/fs-safe 0.5.6 → 0.7.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 (289) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/README.md +23 -10
  3. package/dist/advanced.d.ts +2 -1
  4. package/dist/advanced.d.ts.map +1 -1
  5. package/dist/advanced.js +1 -0
  6. package/dist/archive-crc32.d.ts +2 -0
  7. package/dist/archive-crc32.d.ts.map +1 -0
  8. package/dist/archive-crc32.js +14 -0
  9. package/dist/archive-deadline.d.ts +3 -0
  10. package/dist/archive-deadline.d.ts.map +1 -1
  11. package/dist/archive-deadline.js +44 -8
  12. package/dist/archive-entry.d.ts.map +1 -1
  13. package/dist/archive-entry.js +1 -0
  14. package/dist/archive-errors.d.ts +1 -0
  15. package/dist/archive-errors.d.ts.map +1 -1
  16. package/dist/archive-errors.js +3 -0
  17. package/dist/archive-input.d.ts.map +1 -1
  18. package/dist/archive-input.js +26 -16
  19. package/dist/archive-kind.js +2 -2
  20. package/dist/archive-limits.d.ts +11 -3
  21. package/dist/archive-limits.d.ts.map +1 -1
  22. package/dist/archive-limits.js +24 -0
  23. package/dist/archive-native.d.ts +3 -2
  24. package/dist/archive-native.d.ts.map +1 -1
  25. package/dist/archive-native.js +31 -9
  26. package/dist/archive-policy.d.ts +2 -0
  27. package/dist/archive-policy.d.ts.map +1 -1
  28. package/dist/archive-policy.js +9 -1
  29. package/dist/archive-read.d.ts.map +1 -1
  30. package/dist/archive-read.js +88 -41
  31. package/dist/archive-staging.d.ts +3 -0
  32. package/dist/archive-staging.d.ts.map +1 -1
  33. package/dist/archive-staging.js +91 -43
  34. package/dist/archive-tar-admission.d.ts +7 -0
  35. package/dist/archive-tar-admission.d.ts.map +1 -0
  36. package/dist/archive-tar-admission.js +43 -0
  37. package/dist/archive-tar-gnu.d.ts +2 -0
  38. package/dist/archive-tar-gnu.d.ts.map +1 -0
  39. package/dist/archive-tar-gnu.js +20 -0
  40. package/dist/archive-tar-header.d.ts +8 -0
  41. package/dist/archive-tar-header.d.ts.map +1 -0
  42. package/dist/archive-tar-header.js +47 -0
  43. package/dist/archive-tar-meta.d.ts +29 -1
  44. package/dist/archive-tar-meta.d.ts.map +1 -1
  45. package/dist/archive-tar-meta.js +135 -19
  46. package/dist/archive-tar-pax.d.ts +8 -0
  47. package/dist/archive-tar-pax.d.ts.map +1 -0
  48. package/dist/archive-tar-pax.js +100 -0
  49. package/dist/archive-tar-runtime.d.ts +4 -0
  50. package/dist/archive-tar-runtime.d.ts.map +1 -1
  51. package/dist/archive-tar-runtime.js +3 -0
  52. package/dist/archive-tar.d.ts.map +1 -1
  53. package/dist/archive-tar.js +8 -2
  54. package/dist/archive-zip-admission.d.ts +7 -0
  55. package/dist/archive-zip-admission.d.ts.map +1 -0
  56. package/dist/archive-zip-admission.js +60 -0
  57. package/dist/archive-zip-count.d.ts +2 -0
  58. package/dist/archive-zip-count.d.ts.map +1 -0
  59. package/dist/archive-zip-count.js +140 -0
  60. package/dist/archive-zip-directory.d.ts +8 -0
  61. package/dist/archive-zip-directory.d.ts.map +1 -0
  62. package/dist/archive-zip-directory.js +224 -0
  63. package/dist/archive-zip-integrity.d.ts.map +1 -1
  64. package/dist/archive-zip-integrity.js +1 -14
  65. package/dist/archive-zip-names.d.ts +12 -0
  66. package/dist/archive-zip-names.d.ts.map +1 -0
  67. package/dist/archive-zip-names.js +99 -0
  68. package/dist/archive-zip-preflight.d.ts +1 -1
  69. package/dist/archive-zip-preflight.d.ts.map +1 -1
  70. package/dist/archive-zip-preflight.js +8 -145
  71. package/dist/archive.d.ts.map +1 -1
  72. package/dist/archive.js +63 -42
  73. package/dist/atomic.d.ts +1 -1
  74. package/dist/atomic.d.ts.map +1 -1
  75. package/dist/bounded-read-stream.d.ts.map +1 -1
  76. package/dist/bounded-read-stream.js +2 -4
  77. package/dist/bounded-read.d.ts.map +1 -1
  78. package/dist/bounded-read.js +3 -10
  79. package/dist/byte-budget.d.ts +5 -0
  80. package/dist/byte-budget.d.ts.map +1 -0
  81. package/dist/byte-budget.js +9 -0
  82. package/dist/file-hash.d.ts.map +1 -1
  83. package/dist/file-hash.js +25 -19
  84. package/dist/file-lock-sync.d.ts.map +1 -1
  85. package/dist/file-lock-sync.js +26 -9
  86. package/dist/file-store-boundary.d.ts.map +1 -1
  87. package/dist/file-store-boundary.js +9 -5
  88. package/dist/file-store-limit.d.ts +2 -0
  89. package/dist/file-store-limit.d.ts.map +1 -0
  90. package/dist/file-store-limit.js +8 -0
  91. package/dist/file-store-sync-write.d.ts.map +1 -1
  92. package/dist/file-store-sync-write.js +38 -8
  93. package/dist/file-store.d.ts.map +1 -1
  94. package/dist/file-store.js +26 -26
  95. package/dist/json-durable-queue-directory.d.ts +2 -0
  96. package/dist/json-durable-queue-directory.d.ts.map +1 -0
  97. package/dist/json-durable-queue-directory.js +20 -0
  98. package/dist/json-durable-queue-ownership.d.ts +14 -0
  99. package/dist/json-durable-queue-ownership.d.ts.map +1 -0
  100. package/dist/json-durable-queue-ownership.js +168 -0
  101. package/dist/json-durable-queue-retirement.d.ts +9 -0
  102. package/dist/json-durable-queue-retirement.d.ts.map +1 -0
  103. package/dist/json-durable-queue-retirement.js +126 -0
  104. package/dist/json-durable-queue-transfer-lock.d.ts +2 -0
  105. package/dist/json-durable-queue-transfer-lock.d.ts.map +1 -0
  106. package/dist/json-durable-queue-transfer-lock.js +19 -0
  107. package/dist/json-durable-queue.d.ts +1 -0
  108. package/dist/json-durable-queue.d.ts.map +1 -1
  109. package/dist/json-durable-queue.js +90 -57
  110. package/dist/json.d.ts.map +1 -1
  111. package/dist/json.js +27 -8
  112. package/dist/local-roots.d.ts.map +1 -1
  113. package/dist/local-roots.js +4 -2
  114. package/dist/native-binding.d.ts +15 -3
  115. package/dist/native-binding.d.ts.map +1 -1
  116. package/dist/native-operations.d.ts +4 -1
  117. package/dist/native-operations.d.ts.map +1 -1
  118. package/dist/native-operations.js +22 -6
  119. package/dist/native-pinned-write-windows.d.ts +8 -0
  120. package/dist/native-pinned-write-windows.d.ts.map +1 -0
  121. package/dist/native-pinned-write-windows.js +92 -0
  122. package/dist/native-pinned-write.d.ts.map +1 -1
  123. package/dist/native-pinned-write.js +136 -127
  124. package/dist/native-staged-file.d.ts +24 -0
  125. package/dist/native-staged-file.d.ts.map +1 -0
  126. package/dist/native-staged-file.js +337 -0
  127. package/dist/native.d.ts.map +1 -1
  128. package/dist/native.js +4 -4
  129. package/dist/opened-realpath.d.ts +2 -0
  130. package/dist/opened-realpath.d.ts.map +1 -1
  131. package/dist/opened-realpath.js +12 -7
  132. package/dist/output-sibling.d.ts.map +1 -1
  133. package/dist/output-sibling.js +11 -110
  134. package/dist/output.d.ts.map +1 -1
  135. package/dist/output.js +4 -2
  136. package/dist/owner-dacl.d.ts.map +1 -1
  137. package/dist/owner-dacl.js +2 -1
  138. package/dist/permission-exec.d.ts +19 -0
  139. package/dist/permission-exec.d.ts.map +1 -1
  140. package/dist/permission-exec.js +57 -11
  141. package/dist/permissions-public.d.ts +1 -1
  142. package/dist/permissions-public.d.ts.map +1 -1
  143. package/dist/permissions-windows.d.ts +3 -0
  144. package/dist/permissions-windows.d.ts.map +1 -1
  145. package/dist/permissions-windows.js +16 -5
  146. package/dist/permissions.d.ts +5 -0
  147. package/dist/permissions.d.ts.map +1 -1
  148. package/dist/pinned-open.d.ts.map +1 -1
  149. package/dist/pinned-open.js +27 -48
  150. package/dist/pinned-write.d.ts +6 -0
  151. package/dist/pinned-write.d.ts.map +1 -1
  152. package/dist/pinned-write.js +26 -37
  153. package/dist/private-directory.d.ts.map +1 -1
  154. package/dist/private-directory.js +3 -2
  155. package/dist/private-temp-workspace.d.ts +3 -1
  156. package/dist/private-temp-workspace.d.ts.map +1 -1
  157. package/dist/private-temp-workspace.js +81 -56
  158. package/dist/publish-file.d.ts.map +1 -1
  159. package/dist/publish-file.js +2 -4
  160. package/dist/read-opened-file.d.ts.map +1 -1
  161. package/dist/read-opened-file.js +6 -4
  162. package/dist/regular-file.d.ts.map +1 -1
  163. package/dist/regular-file.js +136 -82
  164. package/dist/replace-file-copy-fallback.d.ts +3 -1
  165. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  166. package/dist/replace-file-copy-fallback.js +21 -36
  167. package/dist/replace-file-copy-source.d.ts +21 -0
  168. package/dist/replace-file-copy-source.d.ts.map +1 -0
  169. package/dist/replace-file-copy-source.js +112 -0
  170. package/dist/replace-file-descriptor.d.ts +13 -3
  171. package/dist/replace-file-descriptor.d.ts.map +1 -1
  172. package/dist/replace-file-descriptor.js +32 -7
  173. package/dist/replace-file-rename-policy.d.ts +7 -0
  174. package/dist/replace-file-rename-policy.d.ts.map +1 -0
  175. package/dist/replace-file-rename-policy.js +30 -0
  176. package/dist/replace-file-temp-owner.d.ts +46 -0
  177. package/dist/replace-file-temp-owner.d.ts.map +1 -0
  178. package/dist/replace-file-temp-owner.js +346 -0
  179. package/dist/replace-file.d.ts +6 -1
  180. package/dist/replace-file.d.ts.map +1 -1
  181. package/dist/replace-file.js +72 -58
  182. package/dist/root-impl.d.ts.map +1 -1
  183. package/dist/root-impl.js +118 -98
  184. package/dist/root-paths.d.ts +11 -14
  185. package/dist/root-paths.d.ts.map +1 -1
  186. package/dist/root-paths.js +36 -27
  187. package/dist/root-write-verification.d.ts +11 -0
  188. package/dist/root-write-verification.d.ts.map +1 -0
  189. package/dist/root-write-verification.js +91 -0
  190. package/dist/secret-file.d.ts +1 -6
  191. package/dist/secret-file.d.ts.map +1 -1
  192. package/dist/secret-file.js +49 -120
  193. package/dist/secret-read-async.d.ts +1 -1
  194. package/dist/secret-read-async.d.ts.map +1 -1
  195. package/dist/secret-read-async.js +51 -72
  196. package/dist/secret-read-policy.d.ts +13 -0
  197. package/dist/secret-read-policy.d.ts.map +1 -0
  198. package/dist/secret-read-policy.js +28 -0
  199. package/dist/secret.d.ts +2 -1
  200. package/dist/secret.d.ts.map +1 -1
  201. package/dist/secret.js +2 -1
  202. package/dist/secure-file.d.ts.map +1 -1
  203. package/dist/secure-file.js +42 -29
  204. package/dist/sibling-staged-file.d.ts +15 -0
  205. package/dist/sibling-staged-file.d.ts.map +1 -0
  206. package/dist/sibling-staged-file.js +148 -0
  207. package/dist/sibling-temp.d.ts +3 -0
  208. package/dist/sibling-temp.d.ts.map +1 -1
  209. package/dist/sibling-temp.js +30 -74
  210. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  211. package/dist/sidecar-lock-acquire.js +48 -27
  212. package/dist/sidecar-lock-handle.d.ts +6 -2
  213. package/dist/sidecar-lock-handle.d.ts.map +1 -1
  214. package/dist/sidecar-lock-handle.js +17 -3
  215. package/dist/sidecar-lock-policy.d.ts +2 -0
  216. package/dist/sidecar-lock-policy.d.ts.map +1 -1
  217. package/dist/sidecar-lock-policy.js +29 -0
  218. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  219. package/dist/sidecar-lock-reclaim.js +29 -7
  220. package/dist/sidecar-lock.d.ts.map +1 -1
  221. package/dist/sidecar-lock.js +28 -16
  222. package/dist/staged-directory.d.ts +16 -0
  223. package/dist/staged-directory.d.ts.map +1 -0
  224. package/dist/staged-directory.js +60 -0
  225. package/dist/staged-file-types.d.ts +56 -0
  226. package/dist/staged-file-types.d.ts.map +1 -0
  227. package/dist/staged-file-types.js +1 -0
  228. package/dist/staged-file.d.ts +10 -0
  229. package/dist/staged-file.d.ts.map +1 -0
  230. package/dist/staged-file.js +15 -0
  231. package/dist/strict-file-identity.d.ts +6 -0
  232. package/dist/strict-file-identity.d.ts.map +1 -0
  233. package/dist/strict-file-identity.js +48 -0
  234. package/dist/suppressed-error.d.ts +6 -0
  235. package/dist/suppressed-error.d.ts.map +1 -0
  236. package/dist/suppressed-error.js +15 -0
  237. package/dist/temp-cleanup.d.ts +2 -0
  238. package/dist/temp-cleanup.d.ts.map +1 -1
  239. package/dist/temp-cleanup.js +25 -10
  240. package/dist/temp-workspace-owner.d.ts +23 -0
  241. package/dist/temp-workspace-owner.d.ts.map +1 -0
  242. package/dist/temp-workspace-owner.js +320 -0
  243. package/dist/temp.d.ts +1 -1
  244. package/dist/temp.d.ts.map +1 -1
  245. package/dist/test-hooks.d.ts +5 -0
  246. package/dist/test-hooks.d.ts.map +1 -1
  247. package/dist/windows-owner.d.ts +3 -0
  248. package/dist/windows-owner.d.ts.map +1 -1
  249. package/dist/windows-owner.js +10 -2
  250. package/docs/advanced.md +19 -2
  251. package/docs/archive.md +250 -35
  252. package/docs/atomic.md +11 -2
  253. package/docs/config.md +7 -0
  254. package/docs/contributing.md +45 -7
  255. package/docs/durability.md +18 -5
  256. package/docs/errors.md +16 -1
  257. package/docs/file-store.md +2 -0
  258. package/docs/index.md +3 -1
  259. package/docs/install.md +20 -8
  260. package/docs/json.md +8 -4
  261. package/docs/migrating-to-0.5.md +7 -7
  262. package/docs/migrating-to-0.6.md +43 -0
  263. package/docs/native-helper.md +30 -8
  264. package/docs/native.md +73 -17
  265. package/docs/output.md +10 -0
  266. package/docs/path-scope.md +28 -2
  267. package/docs/permissions.md +13 -2
  268. package/docs/public-api.md +6 -3
  269. package/docs/quickstart.md +1 -1
  270. package/docs/reading.md +1 -1
  271. package/docs/regular-file.md +9 -2
  272. package/docs/root.md +3 -1
  273. package/docs/secret-file.md +12 -0
  274. package/docs/secure-file.md +21 -3
  275. package/docs/security-model.md +33 -1
  276. package/docs/sidecar-lock.md +15 -1
  277. package/docs/staged-file.md +178 -0
  278. package/docs/store.md +13 -0
  279. package/docs/temp.md +128 -14
  280. package/docs/testing.md +1 -1
  281. package/docs/writing.md +19 -0
  282. package/package.json +16 -9
  283. package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
  284. package/dist/native/darwin-x64/fs-safe-native.node +0 -0
  285. package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
  286. package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
  287. package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
  288. package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
  289. package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
@@ -2,12 +2,14 @@ import fsSync from "node:fs";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { readFileDescriptorBoundedSync, readFileHandleBounded } from "./bounded-read.js";
5
+ import { normalizeMaxBytes } from "./byte-budget.js";
5
6
  import { assertNoUnsafeDeviceReadPath } from "./device-path.js";
6
7
  import { FsSafeError } from "./errors.js";
7
- import { sameFileIdentity } from "./file-identity.js";
8
+ import { inspectFileIdentity, inspectFileIdentitySync } from "./strict-file-identity.js";
8
9
  import { isNotFoundPathError } from "./path.js";
9
10
  import { resolveReadOpenFlags } from "./read-open-flags.js";
10
11
  import { assertNoSymlinkParents, assertNoSymlinkParentsSync } from "./symlink-parents.js";
12
+ import { getFsSafeTestHooks } from "./test-hooks.js";
11
13
  export function resolveRegularFileAppendFlags(constants = fsSync.constants) {
12
14
  const noFollow = constants.O_NOFOLLOW;
13
15
  return (constants.O_CREAT |
@@ -57,13 +59,15 @@ export function statRegularFileSync(filePath) {
57
59
  return { missing: false, stat };
58
60
  }
59
61
  export async function readRegularFile(params) {
62
+ const maxBytes = normalizeMaxBytes(params.maxBytes);
60
63
  assertNoUnsafeDeviceReadPath(params.filePath);
61
- const result = await statRegularFile(params.filePath);
62
- if (result.missing) {
63
- throw Object.assign(new Error(`File not found: ${params.filePath}`), { code: "ENOENT" });
64
- }
65
- if (params.maxBytes !== undefined && result.stat.size > params.maxBytes) {
66
- throw regularFileTooLargeError(params.filePath, params.maxBytes);
64
+ const before = await inspectFileIdentity(async () => {
65
+ const stat = await fs.lstat(params.filePath, { bigint: true });
66
+ assertRegularReadStat(stat, params.filePath, true);
67
+ return stat;
68
+ }).catch((error) => throwReadPreviewError(error, params.filePath));
69
+ if (maxBytes !== undefined && before.size > maxBytes) {
70
+ throw regularFileTooLargeError(params.filePath, maxBytes);
67
71
  }
68
72
  let handle;
69
73
  try {
@@ -77,9 +81,17 @@ export async function readRegularFile(params) {
77
81
  }
78
82
  try {
79
83
  const stat = await handle.stat();
80
- let pathStat;
84
+ const identity = await inspectFileIdentity(async () => {
85
+ const exact = await handle.stat({ bigint: true });
86
+ assertRegularReadStat(exact, params.filePath);
87
+ return exact;
88
+ }, before);
81
89
  try {
82
- pathStat = await fs.lstat(params.filePath);
90
+ await inspectFileIdentity(async () => {
91
+ const current = await fs.lstat(params.filePath, { bigint: true });
92
+ assertRegularReadStat(current, params.filePath);
93
+ return current;
94
+ }, identity);
83
95
  }
84
96
  catch (err) {
85
97
  if (isNotFoundPathError(err)) {
@@ -87,27 +99,21 @@ export async function readRegularFile(params) {
87
99
  }
88
100
  throw err;
89
101
  }
90
- verifyStableReadTarget({
91
- filePath: params.filePath,
92
- pathStat,
93
- postOpenStat: stat,
94
- preOpenStat: result.stat,
95
- });
96
- if (params.maxBytes !== undefined && stat.size > params.maxBytes) {
97
- throw regularFileTooLargeError(params.filePath, params.maxBytes);
102
+ if (maxBytes !== undefined && stat.size > maxBytes) {
103
+ throw regularFileTooLargeError(params.filePath, maxBytes);
98
104
  }
99
105
  // With a byte cap, avoid readFile(): a raced file growth would allocate
100
106
  // the oversized content before the post-read check could reject it.
101
107
  let buffer;
102
108
  try {
103
109
  buffer =
104
- params.maxBytes === undefined
110
+ maxBytes === undefined
105
111
  ? await handle.readFile()
106
- : await readFileHandleBounded(handle, params.maxBytes);
112
+ : await readFileHandleBounded(handle, maxBytes);
107
113
  }
108
114
  catch (error) {
109
- if (params.maxBytes !== undefined) {
110
- translateBoundedReadOverflow(error, params.filePath, params.maxBytes);
115
+ if (maxBytes !== undefined) {
116
+ translateBoundedReadOverflow(error, params.filePath, maxBytes);
111
117
  }
112
118
  throw error;
113
119
  }
@@ -117,20 +123,30 @@ export async function readRegularFile(params) {
117
123
  await handle.close();
118
124
  }
119
125
  }
120
- function verifyStableReadTarget(params) {
121
- if (!params.postOpenStat.isFile() || params.pathStat.isSymbolicLink() || !params.pathStat.isFile()) {
122
- throw new Error(`File is not a regular file: ${params.filePath}`);
126
+ function throwReadPreviewError(error, filePath) {
127
+ if (isNotFoundPathError(error)) {
128
+ throw Object.assign(new Error(`File not found: ${filePath}`), { code: "ENOENT" });
123
129
  }
124
- if (!sameFileIdentity(params.preOpenStat, params.postOpenStat) ||
125
- !sameFileIdentity(params.pathStat, params.postOpenStat)) {
126
- throw new FsSafeError("path-mismatch", `File changed during read: ${params.filePath}`);
130
+ throw error;
131
+ }
132
+ function assertRegularReadStat(stat, filePath, preview = false) {
133
+ if (stat.isSymbolicLink() || !stat.isFile()) {
134
+ throw new Error(preview ? "path must be a regular file" : `File is not a regular file: ${filePath}`);
127
135
  }
128
136
  }
129
137
  function readOpenedRegularFileSync(params) {
130
138
  const stat = fsSync.fstatSync(params.fd);
131
- let pathStat;
139
+ const identity = inspectFileIdentitySync(() => {
140
+ const exact = fsSync.fstatSync(params.fd, { bigint: true });
141
+ assertRegularReadStat(exact, params.filePath);
142
+ return exact;
143
+ }, params.preOpenStat);
132
144
  try {
133
- pathStat = fsSync.lstatSync(params.filePath);
145
+ inspectFileIdentitySync(() => {
146
+ const current = fsSync.lstatSync(params.filePath, { bigint: true });
147
+ assertRegularReadStat(current, params.filePath);
148
+ return current;
149
+ }, identity);
134
150
  }
135
151
  catch (error) {
136
152
  if (isNotFoundPathError(error)) {
@@ -138,12 +154,6 @@ function readOpenedRegularFileSync(params) {
138
154
  }
139
155
  throw error;
140
156
  }
141
- verifyStableReadTarget({
142
- filePath: params.filePath,
143
- pathStat,
144
- postOpenStat: stat,
145
- preOpenStat: params.preOpenStat,
146
- });
147
157
  if (params.maxBytes !== undefined && stat.size > params.maxBytes) {
148
158
  throw regularFileTooLargeError(params.filePath, params.maxBytes);
149
159
  }
@@ -165,13 +175,21 @@ function readOpenedRegularFileSync(params) {
165
175
  return { buffer, stat };
166
176
  }
167
177
  export function readRegularFileSync(params) {
178
+ const maxBytes = normalizeMaxBytes(params.maxBytes);
168
179
  assertNoUnsafeDeviceReadPath(params.filePath);
169
- const result = statRegularFileSync(params.filePath);
170
- if (result.missing) {
171
- throw Object.assign(new Error(`File not found: ${params.filePath}`), { code: "ENOENT" });
180
+ let before;
181
+ try {
182
+ before = inspectFileIdentitySync(() => {
183
+ const stat = fsSync.lstatSync(params.filePath, { bigint: true });
184
+ assertRegularReadStat(stat, params.filePath, true);
185
+ return stat;
186
+ });
172
187
  }
173
- if (params.maxBytes !== undefined && result.stat.size > params.maxBytes) {
174
- throw regularFileTooLargeError(params.filePath, params.maxBytes);
188
+ catch (error) {
189
+ throwReadPreviewError(error, params.filePath);
190
+ }
191
+ if (maxBytes !== undefined && before.size > maxBytes) {
192
+ throw regularFileTooLargeError(params.filePath, maxBytes);
175
193
  }
176
194
  let fd;
177
195
  try {
@@ -187,25 +205,28 @@ export function readRegularFileSync(params) {
187
205
  return readOpenedRegularFileSync({
188
206
  fd,
189
207
  filePath: params.filePath,
190
- preOpenStat: result.stat,
191
- maxBytes: params.maxBytes,
208
+ preOpenStat: before,
209
+ maxBytes,
192
210
  });
193
211
  }
194
212
  finally {
195
213
  fsSync.closeSync(fd);
196
214
  }
197
215
  }
198
- function verifyStableAppendTarget(params) {
199
- if (!params.postOpenStat.isFile()) {
200
- throw new Error(`Refusing to append to non-file: ${params.filePath}`);
216
+ function assertRegularAppendStat(stat, filePath) {
217
+ if (!stat.isFile()) {
218
+ throw new Error(`Refusing to append to non-file: ${filePath}`);
201
219
  }
202
- if (params.postOpenStat.nlink > 1) {
203
- throw new Error(`Refusing to append to hardlinked file: ${params.filePath}`);
220
+ if (stat.nlink > 1n) {
221
+ throw new Error(`Refusing to append to hardlinked file: ${filePath}`);
204
222
  }
205
- const pre = params.preOpenStat;
206
- if (pre && (pre.dev !== params.postOpenStat.dev || pre.ino !== params.postOpenStat.ino)) {
207
- throw new Error(`Refusing to append after file changed: ${params.filePath}`);
223
+ }
224
+ function throwAppendIdentityError(error, filePath) {
225
+ if (isNotFoundPathError(error) ||
226
+ (error instanceof FsSafeError && error.code === "path-mismatch")) {
227
+ throw new Error(`Refusing to append after file changed: ${filePath}`, { cause: error });
208
228
  }
229
+ throw error;
209
230
  }
210
231
  export async function appendRegularFile(options) {
211
232
  if (options.rejectSymlinkParents === true) {
@@ -221,32 +242,49 @@ export async function appendRegularFile(options) {
221
242
  }
222
243
  let preOpenStat;
223
244
  try {
224
- const stat = await fs.lstat(options.filePath);
225
- if (stat.isSymbolicLink()) {
226
- throw new Error(`Refusing to append through symlink: ${options.filePath}`);
227
- }
228
- if (!stat.isFile()) {
229
- throw new Error(`Refusing to append to non-file: ${options.filePath}`);
230
- }
231
- preOpenStat = stat;
245
+ preOpenStat = await inspectFileIdentity(async () => {
246
+ const stat = await fs.lstat(options.filePath, { bigint: true });
247
+ if (stat.isSymbolicLink()) {
248
+ throw new Error(`Refusing to append through symlink: ${options.filePath}`);
249
+ }
250
+ if (!stat.isFile()) {
251
+ throw new Error(`Refusing to append to non-file: ${options.filePath}`);
252
+ }
253
+ return stat;
254
+ });
232
255
  }
233
- catch (err) {
234
- if (!isNotFoundPathError(err)) {
235
- throw err;
236
- }
256
+ catch (error) {
257
+ if (!isNotFoundPathError(error))
258
+ throw error;
237
259
  }
238
260
  const contentBytes = Buffer.isBuffer(options.content)
239
261
  ? options.content.byteLength
240
262
  : Buffer.byteLength(options.content, options.encoding ?? "utf8");
241
263
  if (options.maxFileBytes !== undefined &&
242
- (preOpenStat?.size ?? 0) + contentBytes > options.maxFileBytes) {
264
+ Number(preOpenStat?.size ?? 0n) + contentBytes > options.maxFileBytes) {
243
265
  return;
244
266
  }
267
+ await getFsSafeTestHooks()?.beforeRegularFileAppendOpen?.(options.filePath);
245
268
  const handle = await fs.open(options.filePath, resolveRegularFileAppendFlags(), options.mode ?? 0o600);
246
269
  try {
247
- const stat = await handle.stat();
248
- verifyStableAppendTarget({ preOpenStat, postOpenStat: stat, filePath: options.filePath });
249
- if (options.maxFileBytes !== undefined && stat.size + contentBytes > options.maxFileBytes) {
270
+ let identity;
271
+ try {
272
+ identity = await inspectFileIdentity(async () => {
273
+ const stat = await handle.stat({ bigint: true });
274
+ assertRegularAppendStat(stat, options.filePath);
275
+ return stat;
276
+ }, preOpenStat);
277
+ await inspectFileIdentity(async () => {
278
+ const current = await fs.lstat(options.filePath, { bigint: true });
279
+ assertRegularAppendStat(current, options.filePath);
280
+ return current;
281
+ }, identity);
282
+ }
283
+ catch (error) {
284
+ throwAppendIdentityError(error, options.filePath);
285
+ }
286
+ if (options.maxFileBytes !== undefined &&
287
+ Number(identity.size) + contentBytes > options.maxFileBytes) {
250
288
  return;
251
289
  }
252
290
  await handle.chmod(options.mode ?? 0o600);
@@ -270,33 +308,49 @@ export function appendRegularFileSync(options) {
270
308
  }
271
309
  let preOpenStat;
272
310
  try {
273
- const stat = fsSync.lstatSync(options.filePath);
274
- if (stat.isSymbolicLink()) {
275
- throw new Error(`Refusing to append through symlink: ${options.filePath}`);
276
- }
277
- if (!stat.isFile()) {
278
- throw new Error(`Refusing to append to non-file: ${options.filePath}`);
279
- }
280
- preOpenStat = stat;
311
+ preOpenStat = inspectFileIdentitySync(() => {
312
+ const stat = fsSync.lstatSync(options.filePath, { bigint: true });
313
+ if (stat.isSymbolicLink()) {
314
+ throw new Error(`Refusing to append through symlink: ${options.filePath}`);
315
+ }
316
+ if (!stat.isFile()) {
317
+ throw new Error(`Refusing to append to non-file: ${options.filePath}`);
318
+ }
319
+ return stat;
320
+ });
281
321
  }
282
- catch (err) {
283
- if (!isNotFoundPathError(err)) {
284
- throw err;
285
- }
322
+ catch (error) {
323
+ if (!isNotFoundPathError(error))
324
+ throw error;
286
325
  }
287
326
  const contentBuffer = typeof options.content === "string"
288
327
  ? Buffer.from(options.content, options.encoding ?? "utf8")
289
328
  : Buffer.from(options.content);
290
329
  if (options.maxFileBytes !== undefined &&
291
- (preOpenStat?.size ?? 0) + contentBuffer.byteLength > options.maxFileBytes) {
330
+ Number(preOpenStat?.size ?? 0n) + contentBuffer.byteLength > options.maxFileBytes) {
292
331
  return;
293
332
  }
333
+ getFsSafeTestHooks()?.beforeRegularFileAppendOpenSync?.(options.filePath);
294
334
  const fd = fsSync.openSync(options.filePath, resolveRegularFileAppendFlags(), options.mode ?? 0o600);
295
335
  try {
296
- const stat = fsSync.fstatSync(fd);
297
- verifyStableAppendTarget({ preOpenStat, postOpenStat: stat, filePath: options.filePath });
336
+ let identity;
337
+ try {
338
+ identity = inspectFileIdentitySync(() => {
339
+ const stat = fsSync.fstatSync(fd, { bigint: true });
340
+ assertRegularAppendStat(stat, options.filePath);
341
+ return stat;
342
+ }, preOpenStat);
343
+ inspectFileIdentitySync(() => {
344
+ const current = fsSync.lstatSync(options.filePath, { bigint: true });
345
+ assertRegularAppendStat(current, options.filePath);
346
+ return current;
347
+ }, identity);
348
+ }
349
+ catch (error) {
350
+ throwAppendIdentityError(error, options.filePath);
351
+ }
298
352
  if (options.maxFileBytes !== undefined &&
299
- stat.size + contentBuffer.byteLength > options.maxFileBytes) {
353
+ Number(identity.size) + contentBuffer.byteLength > options.maxFileBytes) {
300
354
  return;
301
355
  }
302
356
  fsSync.fchmodSync(fd, options.mode ?? 0o600);
@@ -1,4 +1,4 @@
1
- import syncFs from "node:fs";
1
+ import syncFs, { type BigIntStats } from "node:fs";
2
2
  export type ReplaceFileDestinationHardlinkPolicy = "reject";
3
3
  export type ReplaceFileCopyFallbackRestorePolicy = "restore-original" | "none";
4
4
  export type ReplaceFileAtomicRestoreCleanup = "restored" | "restore-failed";
@@ -21,6 +21,7 @@ export declare function copyFallbackReplace(params: {
21
21
  destinationHardlinks?: ReplaceFileDestinationHardlinkPolicy;
22
22
  restore: ReplaceFileCopyFallbackRestorePolicy;
23
23
  maxRestoreBytes?: number;
24
+ expectedSourceIdentity?: BigIntStats;
24
25
  sync: boolean;
25
26
  }): Promise<void>;
26
27
  export declare function copyFallbackReplaceSync(params: {
@@ -30,6 +31,7 @@ export declare function copyFallbackReplaceSync(params: {
30
31
  destinationHardlinks?: ReplaceFileDestinationHardlinkPolicy;
31
32
  restore: ReplaceFileCopyFallbackRestorePolicy;
32
33
  maxRestoreBytes?: number;
34
+ expectedSourceIdentity?: BigIntStats;
33
35
  fchmodSync?: (fd: number, mode: number) => void;
34
36
  sync: boolean;
35
37
  }): void;
@@ -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;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"}
1
+ {"version":3,"file":"replace-file-copy-fallback.d.ts","sourceRoot":"","sources":["../src/replace-file-copy-fallback.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,EAAE,EAAE,KAAK,WAAW,EAAc,MAAM,SAAS,CAAC;AAO/D,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,sBAAsB,CAAC,EAAE,WAAW,CAAC;IACrC,IAAI,EAAE,OAAO,CAAC;CACf,GAAG,OAAO,CAAC,IAAI,CAAC,CAwDhB;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,sBAAsB,CAAC,EAAE,WAAW,CAAC;IACrC,UAAU,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,IAAI,EAAE,OAAO,CAAC;CACf,GAAG,IAAI,CAkEP"}
@@ -1,9 +1,11 @@
1
1
  import syncFs, {} from "node:fs";
2
2
  import { FsSafeError } from "./errors.js";
3
3
  import { sameFileIdentity } from "./file-identity.js";
4
+ import { readOwnedCopySource, readOwnedCopySourceSync } from "./replace-file-copy-source.js";
5
+ import { resolveReadOpenFlags } from "./read-open-flags.js";
4
6
  const SUPPORTS_NOFOLLOW = process.platform !== "win32" && "O_NOFOLLOW" in syncFs.constants;
5
7
  const NOFOLLOW = SUPPORTS_NOFOLLOW ? syncFs.constants.O_NOFOLLOW : 0;
6
- const OPEN_READ_FLAGS = syncFs.constants.O_RDONLY | NOFOLLOW;
8
+ const OPEN_READ_FLAGS = resolveReadOpenFlags();
7
9
  const OPEN_READ_WRITE_FLAGS = syncFs.constants.O_RDWR | NOFOLLOW;
8
10
  const OPEN_WRITE_EXCLUSIVE_FLAGS = syncFs.constants.O_WRONLY | syncFs.constants.O_CREAT | syncFs.constants.O_EXCL | NOFOLLOW;
9
11
  const READ_CHUNK_BYTES = 64 * 1024;
@@ -236,24 +238,19 @@ function replacePinnedWithRestoreSync(fsModule, fd, replacement, maxRestoreBytes
236
238
  }
237
239
  }
238
240
  export async function copyFallbackReplace(params) {
239
- const sourcePreview = await params.fsModule.lstat(params.src);
240
- if (sourcePreview.isSymbolicLink() || !sourcePreview.isFile()) {
241
- throw new Error(`Refusing copy fallback from non-file source: ${params.src}`);
242
- }
243
- const sourceHandle = await params.fsModule.open(params.src, OPEN_READ_FLAGS);
241
+ const source = await readOwnedCopySource({
242
+ fsModule: params.fsModule,
243
+ src: params.src,
244
+ expectedIdentity: params.expectedSourceIdentity,
245
+ });
246
+ const { replacement } = source;
244
247
  let destHandle = null;
245
248
  try {
246
- const sourceStat = await sourceHandle.stat();
247
- const sourceCurrent = await params.fsModule.lstat(params.src);
248
- if (!sourceStat.isFile() || sourceCurrent.isSymbolicLink() || !sameFileIdentity(sourceStat, sourceCurrent)) {
249
- throw new FsSafeError("path-mismatch", `Copy fallback source changed while opening: ${params.src}`);
250
- }
251
- const replacement = await sourceHandle.readFile();
252
249
  if (params.restore === "restore-original") {
253
250
  const pinned = await openPinnedDestination(params.fsModule, params.dest, params.destinationHardlinks);
254
251
  if (pinned) {
255
252
  destHandle = pinned.handle;
256
- await replacePinnedWithRestore(destHandle, replacement, params.maxRestoreBytes, sourceStat.mode);
253
+ await replacePinnedWithRestore(destHandle, replacement, params.maxRestoreBytes, source.mode);
257
254
  }
258
255
  }
259
256
  if (!destHandle) {
@@ -269,9 +266,9 @@ export async function copyFallbackReplace(params) {
269
266
  await assertDestinationHardlinkPolicy(params.fsModule, params.dest, params.destinationHardlinks);
270
267
  await params.fsModule.rm(params.dest, { force: true });
271
268
  }
272
- destHandle = await params.fsModule.open(params.dest, OPEN_WRITE_EXCLUSIVE_FLAGS, sourceStat.mode & 0o777);
269
+ destHandle = await params.fsModule.open(params.dest, OPEN_WRITE_EXCLUSIVE_FLAGS, source.mode & 0o777);
273
270
  await destHandle.writeFile(replacement);
274
- await destHandle.chmod(sourceStat.mode);
271
+ await destHandle.chmod(source.mode);
275
272
  if (params.sync) {
276
273
  await destHandle.sync();
277
274
  }
@@ -279,28 +276,22 @@ export async function copyFallbackReplace(params) {
279
276
  }
280
277
  finally {
281
278
  await destHandle?.close().catch(() => undefined);
282
- await sourceHandle.close().catch(() => undefined);
283
279
  }
284
280
  }
285
281
  export function copyFallbackReplaceSync(params) {
286
- const sourcePreview = params.fsModule.lstatSync(params.src);
287
- if (sourcePreview.isSymbolicLink() || !sourcePreview.isFile()) {
288
- throw new Error(`Refusing copy fallback from non-file source: ${params.src}`);
289
- }
290
- const sourceFd = params.fsModule.openSync(params.src, OPEN_READ_FLAGS);
282
+ const source = readOwnedCopySourceSync({
283
+ fsModule: params.fsModule,
284
+ src: params.src,
285
+ expectedIdentity: params.expectedSourceIdentity,
286
+ });
287
+ const { replacement } = source;
291
288
  let destFd;
292
289
  try {
293
- const sourceStat = params.fsModule.fstatSync(sourceFd);
294
- const sourceCurrent = params.fsModule.lstatSync(params.src);
295
- if (!sourceStat.isFile() || sourceCurrent.isSymbolicLink() || !sameFileIdentity(sourceStat, sourceCurrent)) {
296
- throw new FsSafeError("path-mismatch", `Copy fallback source changed while opening: ${params.src}`);
297
- }
298
- const replacement = readBoundedSync(params.fsModule, sourceFd, Number.MAX_SAFE_INTEGER);
299
290
  if (params.restore === "restore-original") {
300
291
  const pinned = openPinnedDestinationSync(params.fsModule, params.dest, params.destinationHardlinks);
301
292
  if (pinned) {
302
293
  destFd = pinned.fd;
303
- replacePinnedWithRestoreSync(params.fsModule, destFd, replacement, params.maxRestoreBytes, sourceStat.mode, params.fchmodSync);
294
+ replacePinnedWithRestoreSync(params.fsModule, destFd, replacement, params.maxRestoreBytes, source.mode, params.fchmodSync);
304
295
  }
305
296
  }
306
297
  if (destFd === undefined) {
@@ -319,9 +310,9 @@ export function copyFallbackReplaceSync(params) {
319
310
  assertDestinationHardlinkPolicySync(params.fsModule, params.dest, params.destinationHardlinks);
320
311
  params.fsModule.rmSync(params.dest, { force: true });
321
312
  }
322
- destFd = params.fsModule.openSync(params.dest, OPEN_WRITE_EXCLUSIVE_FLAGS, sourceStat.mode & 0o777);
313
+ destFd = params.fsModule.openSync(params.dest, OPEN_WRITE_EXCLUSIVE_FLAGS, source.mode & 0o777);
323
314
  writeAllSync(params.fsModule, destFd, replacement);
324
- params.fchmodSync?.(destFd, sourceStat.mode);
315
+ params.fchmodSync?.(destFd, source.mode);
325
316
  if (params.sync) {
326
317
  params.fsModule.fsyncSync(destFd);
327
318
  }
@@ -336,11 +327,5 @@ export function copyFallbackReplaceSync(params) {
336
327
  // Best-effort close after fallback replacement.
337
328
  }
338
329
  }
339
- try {
340
- params.fsModule.closeSync(sourceFd);
341
- }
342
- catch {
343
- // Best-effort close after fallback replacement.
344
- }
345
330
  }
346
331
  }
@@ -0,0 +1,21 @@
1
+ import syncFs, { type BigIntStats } from "node:fs";
2
+ type AsyncSourceFileSystem = Pick<typeof import("node:fs/promises"), "lstat" | "open">;
3
+ type SyncSourceFileSystem = Pick<typeof syncFs, "closeSync" | "fstatSync" | "lstatSync" | "openSync" | "readSync">;
4
+ export declare function readOwnedCopySource(params: {
5
+ fsModule: AsyncSourceFileSystem;
6
+ src: string;
7
+ expectedIdentity?: BigIntStats;
8
+ }): Promise<{
9
+ replacement: Buffer;
10
+ mode: number;
11
+ }>;
12
+ export declare function readOwnedCopySourceSync(params: {
13
+ fsModule: SyncSourceFileSystem;
14
+ src: string;
15
+ expectedIdentity?: BigIntStats;
16
+ }): {
17
+ replacement: Buffer;
18
+ mode: number;
19
+ };
20
+ export {};
21
+ //# sourceMappingURL=replace-file-copy-source.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replace-file-copy-source.d.ts","sourceRoot":"","sources":["../src/replace-file-copy-source.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,EAAE,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAMnD,KAAK,qBAAqB,GAAG,IAAI,CAAC,cAAc,kBAAkB,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC,CAAC;AACvF,KAAK,oBAAoB,GAAG,IAAI,CAC9B,OAAO,MAAM,EACb,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,UAAU,GAAG,UAAU,CAClE,CAAC;AAgEF,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,QAAQ,EAAE,qBAAqB,CAAC;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,gBAAgB,CAAC,EAAE,WAAW,CAAC;CAChC,GAAG,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CA0BjD;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,gBAAgB,CAAC,EAAE,WAAW,CAAC;CAChC,GAAG;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CA8BxC"}
@@ -0,0 +1,112 @@
1
+ import syncFs, {} from "node:fs";
2
+ import { FsSafeError } from "./errors.js";
3
+ import { sameFileIdentity } from "./file-identity.js";
4
+ import { inspectFileIdentity, inspectFileIdentitySync } from "./strict-file-identity.js";
5
+ const NOFOLLOW = process.platform !== "win32" && "O_NOFOLLOW" in syncFs.constants
6
+ ? syncFs.constants.O_NOFOLLOW
7
+ : 0;
8
+ const NONBLOCK = process.platform !== "win32" && "O_NONBLOCK" in syncFs.constants
9
+ ? syncFs.constants.O_NONBLOCK
10
+ : 0;
11
+ const OPEN_READ_FLAGS = syncFs.constants.O_RDONLY | NOFOLLOW | NONBLOCK;
12
+ const READ_CHUNK_BYTES = 64 * 1024;
13
+ function assertSourcePreview(source, src) {
14
+ if (source.isSymbolicLink()) {
15
+ throw new FsSafeError("symlink", `Refusing copy fallback from non-file source: ${src}`);
16
+ }
17
+ if (!source.isFile()) {
18
+ throw new FsSafeError("not-file", `Refusing copy fallback from non-file source: ${src}`);
19
+ }
20
+ if (source.nlink !== 1) {
21
+ throw new FsSafeError("hardlink", `Hardlinked copy fallback source not allowed: ${src}`);
22
+ }
23
+ }
24
+ function sourceSymlinkError(src, cause) {
25
+ return new FsSafeError("symlink", `Refusing copy fallback from non-file source: ${src}`, {
26
+ cause,
27
+ });
28
+ }
29
+ async function openSource(fsModule, src) {
30
+ try {
31
+ return await fsModule.open(src, OPEN_READ_FLAGS);
32
+ }
33
+ catch (error) {
34
+ if (error.code === "ELOOP") {
35
+ throw sourceSymlinkError(src, error);
36
+ }
37
+ throw error;
38
+ }
39
+ }
40
+ function openSourceSync(fsModule, src) {
41
+ try {
42
+ return fsModule.openSync(src, OPEN_READ_FLAGS);
43
+ }
44
+ catch (error) {
45
+ if (error.code === "ELOOP") {
46
+ throw sourceSymlinkError(src, error);
47
+ }
48
+ throw error;
49
+ }
50
+ }
51
+ function readAllSync(fsModule, fd) {
52
+ const chunks = [];
53
+ let position = 0;
54
+ while (true) {
55
+ const buffer = Buffer.allocUnsafe(READ_CHUNK_BYTES);
56
+ const bytesRead = fsModule.readSync(fd, buffer, 0, buffer.length, position);
57
+ if (bytesRead === 0)
58
+ break;
59
+ position += bytesRead;
60
+ chunks.push(buffer.subarray(0, bytesRead));
61
+ }
62
+ return Buffer.concat(chunks, position);
63
+ }
64
+ export async function readOwnedCopySource(params) {
65
+ assertSourcePreview(await params.fsModule.lstat(params.src), params.src);
66
+ const handle = await openSource(params.fsModule, params.src);
67
+ try {
68
+ if (params.expectedIdentity) {
69
+ const exact = await inspectFileIdentity(() => handle.stat({ bigint: true }), params.expectedIdentity);
70
+ await inspectFileIdentity(() => params.fsModule.lstat(params.src, { bigint: true }), exact);
71
+ }
72
+ const opened = await handle.stat();
73
+ const current = await params.fsModule.lstat(params.src);
74
+ if (!opened.isFile() || current.isSymbolicLink() || !sameFileIdentity(opened, current)) {
75
+ throw new FsSafeError("path-mismatch", `Copy fallback source changed while opening: ${params.src}`);
76
+ }
77
+ if (opened.nlink !== 1) {
78
+ throw new FsSafeError("hardlink", `Hardlinked copy fallback source not allowed: ${params.src}`);
79
+ }
80
+ return { replacement: await handle.readFile(), mode: opened.mode };
81
+ }
82
+ finally {
83
+ await handle.close().catch(() => undefined);
84
+ }
85
+ }
86
+ export function readOwnedCopySourceSync(params) {
87
+ assertSourcePreview(params.fsModule.lstatSync(params.src), params.src);
88
+ const fd = openSourceSync(params.fsModule, params.src);
89
+ try {
90
+ if (params.expectedIdentity) {
91
+ const exact = inspectFileIdentitySync(() => params.fsModule.fstatSync(fd, { bigint: true }), params.expectedIdentity);
92
+ inspectFileIdentitySync(() => params.fsModule.lstatSync(params.src, { bigint: true }), exact);
93
+ }
94
+ const opened = params.fsModule.fstatSync(fd);
95
+ const current = params.fsModule.lstatSync(params.src);
96
+ if (!opened.isFile() || current.isSymbolicLink() || !sameFileIdentity(opened, current)) {
97
+ throw new FsSafeError("path-mismatch", `Copy fallback source changed while opening: ${params.src}`);
98
+ }
99
+ if (opened.nlink !== 1) {
100
+ throw new FsSafeError("hardlink", `Hardlinked copy fallback source not allowed: ${params.src}`);
101
+ }
102
+ return { replacement: readAllSync(params.fsModule, fd), mode: opened.mode };
103
+ }
104
+ finally {
105
+ try {
106
+ params.fsModule.closeSync(fd);
107
+ }
108
+ catch {
109
+ // Best-effort close after reading a copy-fallback source.
110
+ }
111
+ }
112
+ }