@openclaw/fs-safe 0.6.0 → 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 (183) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +7 -0
  3. package/dist/archive-deadline.d.ts +3 -0
  4. package/dist/archive-deadline.d.ts.map +1 -1
  5. package/dist/archive-deadline.js +44 -8
  6. package/dist/archive-entry.d.ts.map +1 -1
  7. package/dist/archive-entry.js +1 -0
  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 +3 -0
  11. package/dist/archive-input.d.ts.map +1 -1
  12. package/dist/archive-input.js +26 -16
  13. package/dist/archive-limits.d.ts +11 -3
  14. package/dist/archive-limits.d.ts.map +1 -1
  15. package/dist/archive-limits.js +24 -0
  16. package/dist/archive-native.d.ts +3 -2
  17. package/dist/archive-native.d.ts.map +1 -1
  18. package/dist/archive-native.js +24 -9
  19. package/dist/archive-policy.d.ts +2 -0
  20. package/dist/archive-policy.d.ts.map +1 -1
  21. package/dist/archive-policy.js +9 -1
  22. package/dist/archive-read.d.ts.map +1 -1
  23. package/dist/archive-read.js +69 -41
  24. package/dist/archive-staging.d.ts +3 -0
  25. package/dist/archive-staging.d.ts.map +1 -1
  26. package/dist/archive-staging.js +91 -43
  27. package/dist/archive-tar-admission.d.ts +7 -0
  28. package/dist/archive-tar-admission.d.ts.map +1 -0
  29. package/dist/archive-tar-admission.js +43 -0
  30. package/dist/archive-tar-gnu.d.ts +2 -0
  31. package/dist/archive-tar-gnu.d.ts.map +1 -0
  32. package/dist/archive-tar-gnu.js +20 -0
  33. package/dist/archive-tar-header.d.ts +8 -0
  34. package/dist/archive-tar-header.d.ts.map +1 -0
  35. package/dist/archive-tar-header.js +47 -0
  36. package/dist/archive-tar-meta.d.ts +29 -1
  37. package/dist/archive-tar-meta.d.ts.map +1 -1
  38. package/dist/archive-tar-meta.js +104 -35
  39. package/dist/archive-tar-runtime.d.ts +3 -0
  40. package/dist/archive-tar-runtime.d.ts.map +1 -1
  41. package/dist/archive-tar-runtime.js +3 -0
  42. package/dist/archive-tar.d.ts.map +1 -1
  43. package/dist/archive-tar.js +8 -2
  44. package/dist/archive-zip-names.js +2 -2
  45. package/dist/archive.d.ts.map +1 -1
  46. package/dist/archive.js +38 -26
  47. package/dist/atomic.d.ts +1 -1
  48. package/dist/atomic.d.ts.map +1 -1
  49. package/dist/bounded-read-stream.d.ts.map +1 -1
  50. package/dist/bounded-read-stream.js +2 -4
  51. package/dist/bounded-read.d.ts.map +1 -1
  52. package/dist/bounded-read.js +3 -10
  53. package/dist/byte-budget.d.ts +5 -0
  54. package/dist/byte-budget.d.ts.map +1 -0
  55. package/dist/byte-budget.js +9 -0
  56. package/dist/file-lock-sync.d.ts.map +1 -1
  57. package/dist/file-lock-sync.js +10 -5
  58. package/dist/file-store-boundary.d.ts.map +1 -1
  59. package/dist/file-store-boundary.js +9 -5
  60. package/dist/file-store-limit.d.ts +2 -0
  61. package/dist/file-store-limit.d.ts.map +1 -0
  62. package/dist/file-store-limit.js +8 -0
  63. package/dist/file-store-sync-write.d.ts.map +1 -1
  64. package/dist/file-store-sync-write.js +38 -8
  65. package/dist/file-store.d.ts.map +1 -1
  66. package/dist/file-store.js +26 -26
  67. package/dist/json-durable-queue-directory.d.ts +2 -0
  68. package/dist/json-durable-queue-directory.d.ts.map +1 -0
  69. package/dist/json-durable-queue-directory.js +20 -0
  70. package/dist/json-durable-queue-ownership.d.ts +14 -0
  71. package/dist/json-durable-queue-ownership.d.ts.map +1 -0
  72. package/dist/json-durable-queue-ownership.js +168 -0
  73. package/dist/json-durable-queue-retirement.d.ts +9 -0
  74. package/dist/json-durable-queue-retirement.d.ts.map +1 -0
  75. package/dist/json-durable-queue-retirement.js +126 -0
  76. package/dist/json-durable-queue-transfer-lock.d.ts +2 -0
  77. package/dist/json-durable-queue-transfer-lock.d.ts.map +1 -0
  78. package/dist/json-durable-queue-transfer-lock.js +19 -0
  79. package/dist/json-durable-queue.d.ts +1 -0
  80. package/dist/json-durable-queue.d.ts.map +1 -1
  81. package/dist/json-durable-queue.js +90 -57
  82. package/dist/json.d.ts.map +1 -1
  83. package/dist/json.js +27 -8
  84. package/dist/local-roots.d.ts.map +1 -1
  85. package/dist/local-roots.js +4 -2
  86. package/dist/native-binding.d.ts +12 -3
  87. package/dist/native-binding.d.ts.map +1 -1
  88. package/dist/output-sibling.d.ts.map +1 -1
  89. package/dist/output-sibling.js +11 -110
  90. package/dist/output.d.ts.map +1 -1
  91. package/dist/output.js +4 -2
  92. package/dist/pinned-open.d.ts.map +1 -1
  93. package/dist/pinned-open.js +27 -48
  94. package/dist/pinned-write.d.ts.map +1 -1
  95. package/dist/pinned-write.js +8 -9
  96. package/dist/private-temp-workspace.d.ts +3 -1
  97. package/dist/private-temp-workspace.d.ts.map +1 -1
  98. package/dist/private-temp-workspace.js +81 -56
  99. package/dist/publish-file.d.ts.map +1 -1
  100. package/dist/publish-file.js +2 -4
  101. package/dist/read-opened-file.d.ts.map +1 -1
  102. package/dist/read-opened-file.js +6 -4
  103. package/dist/regular-file.d.ts.map +1 -1
  104. package/dist/regular-file.js +136 -82
  105. package/dist/replace-file-copy-fallback.d.ts +3 -1
  106. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  107. package/dist/replace-file-copy-fallback.js +21 -36
  108. package/dist/replace-file-copy-source.d.ts +21 -0
  109. package/dist/replace-file-copy-source.d.ts.map +1 -0
  110. package/dist/replace-file-copy-source.js +112 -0
  111. package/dist/replace-file-descriptor.d.ts +13 -3
  112. package/dist/replace-file-descriptor.d.ts.map +1 -1
  113. package/dist/replace-file-descriptor.js +32 -7
  114. package/dist/replace-file-rename-policy.d.ts +7 -0
  115. package/dist/replace-file-rename-policy.d.ts.map +1 -0
  116. package/dist/replace-file-rename-policy.js +30 -0
  117. package/dist/replace-file-temp-owner.d.ts +46 -0
  118. package/dist/replace-file-temp-owner.d.ts.map +1 -0
  119. package/dist/replace-file-temp-owner.js +346 -0
  120. package/dist/replace-file.d.ts +6 -1
  121. package/dist/replace-file.d.ts.map +1 -1
  122. package/dist/replace-file.js +72 -58
  123. package/dist/root-impl.d.ts.map +1 -1
  124. package/dist/root-impl.js +40 -26
  125. package/dist/secret-file.d.ts.map +1 -1
  126. package/dist/secret-file.js +6 -3
  127. package/dist/secret-read-async.d.ts.map +1 -1
  128. package/dist/secret-read-async.js +6 -5
  129. package/dist/secure-file.d.ts.map +1 -1
  130. package/dist/secure-file.js +17 -13
  131. package/dist/sibling-staged-file.d.ts +15 -0
  132. package/dist/sibling-staged-file.d.ts.map +1 -0
  133. package/dist/sibling-staged-file.js +148 -0
  134. package/dist/sibling-temp.d.ts +3 -0
  135. package/dist/sibling-temp.d.ts.map +1 -1
  136. package/dist/sibling-temp.js +30 -74
  137. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  138. package/dist/sidecar-lock-acquire.js +48 -27
  139. package/dist/sidecar-lock-handle.d.ts +6 -2
  140. package/dist/sidecar-lock-handle.d.ts.map +1 -1
  141. package/dist/sidecar-lock-handle.js +17 -3
  142. package/dist/sidecar-lock-policy.d.ts +2 -0
  143. package/dist/sidecar-lock-policy.d.ts.map +1 -1
  144. package/dist/sidecar-lock-policy.js +29 -0
  145. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  146. package/dist/sidecar-lock-reclaim.js +29 -7
  147. package/dist/sidecar-lock.d.ts.map +1 -1
  148. package/dist/sidecar-lock.js +28 -16
  149. package/dist/suppressed-error.d.ts +6 -0
  150. package/dist/suppressed-error.d.ts.map +1 -0
  151. package/dist/suppressed-error.js +15 -0
  152. package/dist/temp-cleanup.d.ts +2 -0
  153. package/dist/temp-cleanup.d.ts.map +1 -1
  154. package/dist/temp-cleanup.js +25 -10
  155. package/dist/temp-workspace-owner.d.ts +23 -0
  156. package/dist/temp-workspace-owner.d.ts.map +1 -0
  157. package/dist/temp-workspace-owner.js +320 -0
  158. package/dist/temp.d.ts +1 -1
  159. package/dist/temp.d.ts.map +1 -1
  160. package/dist/test-hooks.d.ts +5 -0
  161. package/dist/test-hooks.d.ts.map +1 -1
  162. package/docs/advanced.md +8 -1
  163. package/docs/archive.md +194 -26
  164. package/docs/atomic.md +11 -2
  165. package/docs/durability.md +1 -1
  166. package/docs/file-store.md +2 -0
  167. package/docs/install.md +8 -4
  168. package/docs/json.md +8 -4
  169. package/docs/native-helper.md +15 -3
  170. package/docs/native.md +33 -6
  171. package/docs/output.md +10 -0
  172. package/docs/public-api.md +6 -3
  173. package/docs/quickstart.md +1 -1
  174. package/docs/reading.md +1 -1
  175. package/docs/regular-file.md +9 -2
  176. package/docs/root.md +2 -0
  177. package/docs/secret-file.md +5 -1
  178. package/docs/secure-file.md +4 -2
  179. package/docs/security-model.md +18 -0
  180. package/docs/sidecar-lock.md +4 -0
  181. package/docs/store.md +13 -0
  182. package/docs/temp.md +128 -14
  183. package/package.json +10 -10
@@ -4,11 +4,18 @@ import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { fileStore, fileStoreSync, } from "./file-store.js";
6
6
  import { FsSafeError } from "./errors.js";
7
- import { sameFileIdentityForCleanup } from "./file-identity.js";
8
7
  import { isNotFoundPathError } from "./path.js";
9
8
  import { throwFsSafeReadError } from "./read-error.js";
10
9
  import { matchRootFileOpenFailure, openRootFileSync, } from "./root-file.js";
11
10
  import { registerTempPathForExit, } from "./temp-cleanup.js";
11
+ import { TempWorkspaceCleanupCapability, TempWorkspaceCleanupOwner, } from "./temp-workspace-owner.js";
12
+ function resolveTempWorkspaceCleanupSafety(value) {
13
+ if (value === undefined || value === "compatible")
14
+ return "compatible";
15
+ if (value === "require-bounded")
16
+ return value;
17
+ throw new TypeError("cleanupSafety must be compatible or require-bounded");
18
+ }
12
19
  function sanitizeTempPrefix(prefix) {
13
20
  const sanitized = prefix.trim().replace(/[^a-zA-Z0-9._-]/g, "-");
14
21
  if (!sanitized || sanitized === "." || sanitized === "..") {
@@ -79,52 +86,47 @@ function ensurePrivateDirectorySync(dir, mode) {
79
86
  // Best-effort on platforms that do not enforce POSIX modes.
80
87
  }
81
88
  }
82
- async function cleanupWorkspace(dir, identity) {
83
- let current;
84
- try {
85
- current = await fs.lstat(dir, { bigint: true });
86
- }
87
- catch (error) {
88
- return error.code === "ENOENT" ? "missing" : "identity-mismatch";
89
- }
90
- if (!sameFileIdentityForCleanup(current, identity)) {
91
- return "identity-mismatch";
92
- }
93
- await fs.rm(dir, { recursive: true, force: true });
94
- return "removed";
95
- }
96
- function cleanupWorkspaceSync(dir, identity) {
97
- let current;
98
- try {
99
- current = fsSync.lstatSync(dir, { bigint: true });
100
- }
101
- catch (error) {
102
- return error.code === "ENOENT" ? "missing" : "identity-mismatch";
103
- }
104
- if (!sameFileIdentityForCleanup(current, identity)) {
105
- return "identity-mismatch";
106
- }
107
- fsSync.rmSync(dir, { recursive: true, force: true });
108
- return "removed";
109
- }
110
89
  async function createTempWorkspace(options) {
111
90
  const dirMode = options.dirMode ?? 0o700;
112
91
  const mode = options.mode ?? 0o600;
92
+ const cleanupSafety = resolveTempWorkspaceCleanupSafety(options.cleanupSafety);
113
93
  const requestedRoot = path.resolve(options.rootDir);
114
94
  const root = await fs.realpath(requestedRoot).catch(() => requestedRoot);
115
95
  await ensurePrivateDirectory(root, dirMode);
116
- const dir = await fs.mkdtemp(path.join(root, sanitizeTempPrefix(options.prefix)));
117
- await fs.chmod(dir, dirMode).catch(() => undefined);
118
- const stat = await fs.lstat(dir, { bigint: true });
119
- if (stat.isSymbolicLink() || !stat.isDirectory()) {
120
- throw new Error(`Temp workspace must be a directory: ${dir}`);
96
+ const capability = new TempWorkspaceCleanupCapability(root, cleanupSafety);
97
+ let dir;
98
+ let stat;
99
+ let cleanupOwner;
100
+ let unregisterTempDir;
101
+ try {
102
+ dir = await fs.mkdtemp(path.join(root, sanitizeTempPrefix(options.prefix)));
103
+ await fs.chmod(dir, dirMode).catch(() => undefined);
104
+ stat = await fs.lstat(dir, { bigint: true });
105
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
106
+ throw new Error(`Temp workspace must be a directory: ${dir}`);
107
+ }
108
+ if (capability.parent)
109
+ capability.assertCurrent();
110
+ cleanupOwner = new TempWorkspaceCleanupOwner(dir, stat, capability);
111
+ unregisterTempDir = registerTempPathForExit(dir, {
112
+ cleanupSync: () => cleanupOwner.cleanupSync(),
113
+ });
121
114
  }
115
+ catch (error) {
116
+ try {
117
+ if (cleanupOwner)
118
+ cleanupOwner.cleanupSync();
119
+ else
120
+ capability.close();
121
+ }
122
+ catch (cleanupError) {
123
+ throw new AggregateError([error, cleanupError], "temp workspace creation and cleanup both failed");
124
+ }
125
+ throw error;
126
+ }
127
+ const owner = cleanupOwner;
122
128
  const identity = { dev: Number(stat.dev), ino: Number(stat.ino) };
123
- const cleanupIdentity = { dev: stat.dev, ino: stat.ino };
124
- const unregisterTempDir = registerTempPathForExit(dir, {
125
- recursive: true,
126
- identity: cleanupIdentity,
127
- });
129
+ // Once registered, even a store-construction failure remains exit-cleanable.
128
130
  const store = fileStore({ rootDir: dir, private: true, dirMode, mode });
129
131
  return {
130
132
  dir,
@@ -148,7 +150,7 @@ async function createTempWorkspace(options) {
148
150
  },
149
151
  cleanup: async () => {
150
152
  try {
151
- return await cleanupWorkspace(dir, cleanupIdentity);
153
+ return await owner.cleanup();
152
154
  }
153
155
  finally {
154
156
  unregisterTempDir();
@@ -156,7 +158,7 @@ async function createTempWorkspace(options) {
156
158
  },
157
159
  [Symbol.asyncDispose]: async () => {
158
160
  try {
159
- await cleanupWorkspace(dir, cleanupIdentity);
161
+ await owner.cleanup();
160
162
  }
161
163
  finally {
162
164
  unregisterTempDir();
@@ -182,6 +184,7 @@ export async function withTempWorkspace(options, run) {
182
184
  export function tempWorkspaceSync(options) {
183
185
  const dirMode = options.dirMode ?? 0o700;
184
186
  const mode = options.mode ?? 0o600;
187
+ const cleanupSafety = resolveTempWorkspaceCleanupSafety(options.cleanupSafety);
185
188
  const requestedRoot = path.resolve(options.rootDir);
186
189
  let root = requestedRoot;
187
190
  try {
@@ -191,23 +194,45 @@ export function tempWorkspaceSync(options) {
191
194
  root = requestedRoot;
192
195
  }
193
196
  ensurePrivateDirectorySync(root, dirMode);
194
- const dir = fsSync.mkdtempSync(path.join(root, sanitizeTempPrefix(options.prefix)));
197
+ const capability = new TempWorkspaceCleanupCapability(root, cleanupSafety);
198
+ let dir;
199
+ let stat;
200
+ let cleanupOwner;
201
+ let unregisterTempDir;
195
202
  try {
196
- fsSync.chmodSync(dir, dirMode);
203
+ dir = fsSync.mkdtempSync(path.join(root, sanitizeTempPrefix(options.prefix)));
204
+ try {
205
+ fsSync.chmodSync(dir, dirMode);
206
+ }
207
+ catch {
208
+ // Best-effort on platforms that do not enforce POSIX modes.
209
+ }
210
+ stat = fsSync.lstatSync(dir, { bigint: true });
211
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
212
+ throw new Error(`Temp workspace must be a directory: ${dir}`);
213
+ }
214
+ if (capability.parent)
215
+ capability.assertCurrent();
216
+ cleanupOwner = new TempWorkspaceCleanupOwner(dir, stat, capability);
217
+ unregisterTempDir = registerTempPathForExit(dir, {
218
+ cleanupSync: () => cleanupOwner.cleanupSync(),
219
+ });
197
220
  }
198
- catch {
199
- // Best-effort on platforms that do not enforce POSIX modes.
200
- }
201
- const stat = fsSync.lstatSync(dir, { bigint: true });
202
- if (stat.isSymbolicLink() || !stat.isDirectory()) {
203
- throw new Error(`Temp workspace must be a directory: ${dir}`);
221
+ catch (error) {
222
+ try {
223
+ if (cleanupOwner)
224
+ cleanupOwner.cleanupSync();
225
+ else
226
+ capability.close();
227
+ }
228
+ catch (cleanupError) {
229
+ throw new AggregateError([error, cleanupError], "temp workspace creation and cleanup both failed");
230
+ }
231
+ throw error;
204
232
  }
233
+ const owner = cleanupOwner;
205
234
  const identity = { dev: Number(stat.dev), ino: Number(stat.ino) };
206
- const cleanupIdentity = { dev: stat.dev, ino: stat.ino };
207
- const unregisterTempDir = registerTempPathForExit(dir, {
208
- recursive: true,
209
- identity: cleanupIdentity,
210
- });
235
+ // Once registered, even a store-construction failure remains exit-cleanable.
211
236
  const store = fileStoreSync({ rootDir: dir, private: true, dirMode, mode });
212
237
  return {
213
238
  dir,
@@ -244,7 +269,7 @@ export function tempWorkspaceSync(options) {
244
269
  },
245
270
  cleanup: () => {
246
271
  try {
247
- return cleanupWorkspaceSync(dir, cleanupIdentity);
272
+ return owner.cleanupSync();
248
273
  }
249
274
  finally {
250
275
  unregisterTempDir();
@@ -252,7 +277,7 @@ export function tempWorkspaceSync(options) {
252
277
  },
253
278
  [Symbol.dispose]: () => {
254
279
  try {
255
- cleanupWorkspaceSync(dir, cleanupIdentity);
280
+ owner.cleanupSync();
256
281
  }
257
282
  finally {
258
283
  unregisterTempDir();
@@ -1 +1 @@
1
- {"version":3,"file":"publish-file.d.ts","sourceRoot":"","sources":["../src/publish-file.ts"],"names":[],"mappings":"AACA,OAAe,EAAoB,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAI/D,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EAC1B,MAAM,2BAA2B,CAAC;AAGnC,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EAML,KAAK,qCAAqC,EAC3C,MAAM,2BAA2B,CAAC;AAGnC,YAAY,EACV,2BAA2B,EAC3B,wCAAwC,EACxC,kCAAkC,EAClC,gCAAgC,EAChC,qCAAqC,GACtC,MAAM,2BAA2B,CAAC;AAEnC,MAAM,MAAM,4BAA4B,GACpC,cAAc,GACd,eAAe,GACf,kBAAkB,CAAC;AAEvB,MAAM,MAAM,0BAA0B,GAAG;IACvC,MAAM,EAAE,UAAU,GAAG,gBAAgB,GAAG,kBAAkB,CAAC;IAC3D,QAAQ,EAAE,KAAK,CAAC;IAChB,aAAa,EAAE,oBAAoB,CAAC;CACrC,CAAC;AAkBF,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE/D;AA2ND,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,sBAAsB,CAAC,EAAE,gBAAgB,CAAC;IAC1C,QAAQ,EAAE,4BAA4B,CAAC;IACvC,aAAa,CAAC,EAAE,qCAAqC,CAAC;IACtD,aAAa,CAAC,EAAE,gBAAgB,CAAC;CAClC,GAAG,OAAO,CAAC,0BAA0B,CAAC,CA4MtC"}
1
+ {"version":3,"file":"publish-file.d.ts","sourceRoot":"","sources":["../src/publish-file.ts"],"names":[],"mappings":"AACA,OAAe,EAAoB,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAI/D,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EAC1B,MAAM,2BAA2B,CAAC;AAGnC,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,oBAAoB,CAAC;AAI5B,OAAO,EAML,KAAK,qCAAqC,EAC3C,MAAM,2BAA2B,CAAC;AAGnC,YAAY,EACV,2BAA2B,EAC3B,wCAAwC,EACxC,kCAAkC,EAClC,gCAAgC,EAChC,qCAAqC,GACtC,MAAM,2BAA2B,CAAC;AAEnC,MAAM,MAAM,4BAA4B,GACpC,cAAc,GACd,eAAe,GACf,kBAAkB,CAAC;AAEvB,MAAM,MAAM,0BAA0B,GAAG;IACvC,MAAM,EAAE,UAAU,GAAG,gBAAgB,GAAG,kBAAkB,CAAC;IAC3D,QAAQ,EAAE,KAAK,CAAC;IAChB,aAAa,EAAE,oBAAoB,CAAC;CACrC,CAAC;AAkBF,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE/D;AAsND,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,sBAAsB,CAAC,EAAE,gBAAgB,CAAC;IAC1C,QAAQ,EAAE,4BAA4B,CAAC;IACvC,aAAa,CAAC,EAAE,qCAAqC,CAAC;IACtD,aAAa,CAAC,EAAE,gBAAgB,CAAC;CAClC,GAAG,OAAO,CAAC,0BAA0B,CAAC,CA4MtC"}
@@ -8,6 +8,7 @@ import { hashFileHandle } from "./file-hash.js";
8
8
  import { sameFileIdentity, sameFileIdentityForCleanup, } from "./file-identity.js";
9
9
  import { syncNativeFileBestEffort } from "./native-operations.js";
10
10
  import { getNativeBinding, requireNativeBinding } from "./native.js";
11
+ import { resolveReadOpenFlags } from "./read-open-flags.js";
11
12
  import { directorySyncFailure, publicationFailure, rememberCreatedTarget, } from "./publish-file-failure.js";
12
13
  import { getFsSafeTestHooks } from "./test-hooks.js";
13
14
  const HARDLINK_FALLBACK_CODES = new Set([
@@ -29,10 +30,7 @@ export function isHardlinkFallbackError(error) {
29
30
  return HARDLINK_FALLBACK_CODES.has(error?.code ?? "");
30
31
  }
31
32
  function sourceOpenFlags() {
32
- return (fsSync.constants.O_RDONLY |
33
- (process.platform !== "win32" && typeof fsSync.constants.O_NOFOLLOW === "number"
34
- ? fsSync.constants.O_NOFOLLOW
35
- : 0));
33
+ return resolveReadOpenFlags();
36
34
  }
37
35
  function directoryOpenFlags() {
38
36
  return (fsSync.constants.O_RDONLY |
@@ -1 +1 @@
1
- {"version":3,"file":"read-opened-file.d.ts","sourceRoot":"","sources":["../src/read-opened-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAI7D,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,oBAAoB,CAAC;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,CAAC;CACb,CAAC;AAEF,KAAK,UAAU,GAAG;IAChB,MAAM,EAAE,UAAU,CAAC;IACnB,WAAW,EAAE,oBAAoB,CAAC;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,CAAC;CACb,CAAC;AAEF,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,UAAU,CAAC,CAiBtB"}
1
+ {"version":3,"file":"read-opened-file.d.ts","sourceRoot":"","sources":["../src/read-opened-file.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAI7D,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,oBAAoB,CAAC;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,CAAC;CACb,CAAC;AAEF,KAAK,UAAU,GAAG;IAChB,MAAM,EAAE,UAAU,CAAC;IACnB,WAAW,EAAE,oBAAoB,CAAC;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,CAAC;CACb,CAAC;AAEF,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,UAAU,CAAC,CAkBtB"}
@@ -1,12 +1,14 @@
1
+ import { normalizeMaxBytes } from "./byte-budget.js";
1
2
  import { readFileHandleBounded } from "./bounded-read.js";
2
3
  import { FsSafeError } from "./errors.js";
3
4
  export async function readOpenedFileSafely(params) {
4
- if (params.maxBytes !== undefined && params.opened.stat.size > params.maxBytes) {
5
- throw new FsSafeError("too-large", `file exceeds limit of ${params.maxBytes} bytes (got ${params.opened.stat.size})`);
5
+ const maxBytes = normalizeMaxBytes(params.maxBytes);
6
+ if (maxBytes !== undefined && params.opened.stat.size > maxBytes) {
7
+ throw new FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got ${params.opened.stat.size})`);
6
8
  }
7
- const buffer = params.maxBytes === undefined
9
+ const buffer = maxBytes === undefined
8
10
  ? await params.opened.handle.readFile()
9
- : await readFileHandleBounded(params.opened.handle, params.maxBytes);
11
+ : await readFileHandleBounded(params.opened.handle, maxBytes);
10
12
  return {
11
13
  buffer,
12
14
  containment: params.opened.containment,
@@ -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;AAY7B,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;AAaD,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"}
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;AAClD,OAAO,MAAM,MAAM,SAAS,CAAC;AAc7B,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;AAaD,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,CA6D3C;AA2DD,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,CAoCA;AAmBD,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAwExF;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,wBAAwB,GAAG,IAAI,CAyE7E"}
@@ -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"}