@openclaw/fs-safe 0.7.2 → 0.8.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 (155) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +5 -1
  3. package/dist/absolute-path.d.ts.map +1 -1
  4. package/dist/absolute-path.js +9 -10
  5. package/dist/archive-merge.d.ts +18 -0
  6. package/dist/archive-merge.d.ts.map +1 -0
  7. package/dist/archive-merge.js +157 -0
  8. package/dist/archive-native.d.ts.map +1 -1
  9. package/dist/archive-native.js +25 -5
  10. package/dist/archive-policy.d.ts +1 -1
  11. package/dist/archive-policy.d.ts.map +1 -1
  12. package/dist/archive-policy.js +1 -1
  13. package/dist/archive-read.d.ts.map +1 -1
  14. package/dist/archive-read.js +3 -2
  15. package/dist/archive-staging.d.ts +13 -8
  16. package/dist/archive-staging.d.ts.map +1 -1
  17. package/dist/archive-staging.js +44 -192
  18. package/dist/archive-tar-meta.d.ts.map +1 -1
  19. package/dist/archive-tar-meta.js +9 -5
  20. package/dist/archive-tar-runtime.d.ts +1 -0
  21. package/dist/archive-tar-runtime.d.ts.map +1 -1
  22. package/dist/archive-zip-admission.d.ts +2 -1
  23. package/dist/archive-zip-admission.d.ts.map +1 -1
  24. package/dist/archive-zip-admission.js +2 -2
  25. package/dist/archive-zip-directory.d.ts +8 -1
  26. package/dist/archive-zip-directory.d.ts.map +1 -1
  27. package/dist/archive-zip-directory.js +6 -2
  28. package/dist/archive-zip-entry.d.ts +1 -1
  29. package/dist/archive-zip-entry.d.ts.map +1 -1
  30. package/dist/archive-zip-names.d.ts +1 -1
  31. package/dist/archive-zip-names.d.ts.map +1 -1
  32. package/dist/archive-zip-names.js +1 -0
  33. package/dist/archive.d.ts.map +1 -1
  34. package/dist/archive.js +17 -25
  35. package/dist/directory-guard.d.ts +17 -5
  36. package/dist/directory-guard.d.ts.map +1 -1
  37. package/dist/directory-guard.js +8 -6
  38. package/dist/directory-mode-node.d.ts +8 -0
  39. package/dist/directory-mode-node.d.ts.map +1 -0
  40. package/dist/directory-mode-node.js +93 -0
  41. package/dist/directory-mode-owner.d.ts +21 -0
  42. package/dist/directory-mode-owner.d.ts.map +1 -0
  43. package/dist/directory-mode-owner.js +73 -0
  44. package/dist/file-observation.d.ts +1 -1
  45. package/dist/file-observation.d.ts.map +1 -1
  46. package/dist/file-store-boundary.d.ts +2 -0
  47. package/dist/file-store-boundary.d.ts.map +1 -1
  48. package/dist/file-store-boundary.js +13 -0
  49. package/dist/file-store.d.ts.map +1 -1
  50. package/dist/file-store.js +8 -11
  51. package/dist/filename.d.ts +6 -0
  52. package/dist/filename.d.ts.map +1 -1
  53. package/dist/filename.js +36 -0
  54. package/dist/guarded-mkdir.d.ts +2 -0
  55. package/dist/guarded-mkdir.d.ts.map +1 -1
  56. package/dist/guarded-mkdir.js +2 -2
  57. package/dist/guarded-mutation.d.ts +2 -2
  58. package/dist/guarded-mutation.d.ts.map +1 -1
  59. package/dist/json-document-store.d.ts +3 -0
  60. package/dist/json-document-store.d.ts.map +1 -1
  61. package/dist/json-document-store.js +2 -0
  62. package/dist/json-durable-queue-ownership.d.ts +3 -1
  63. package/dist/json-durable-queue-ownership.d.ts.map +1 -1
  64. package/dist/json-durable-queue-ownership.js +7 -4
  65. package/dist/json-durable-queue.d.ts.map +1 -1
  66. package/dist/json-durable-queue.js +25 -28
  67. package/dist/local-roots.d.ts.map +1 -1
  68. package/dist/local-roots.js +18 -56
  69. package/dist/move-path-cleanup.d.ts +43 -0
  70. package/dist/move-path-cleanup.d.ts.map +1 -0
  71. package/dist/move-path-cleanup.js +159 -0
  72. package/dist/move-path.d.ts.map +1 -1
  73. package/dist/move-path.js +30 -84
  74. package/dist/native-operations.d.ts +2 -4
  75. package/dist/native-operations.d.ts.map +1 -1
  76. package/dist/native-operations.js +7 -9
  77. package/dist/native-pinned-write-windows.d.ts +2 -2
  78. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  79. package/dist/native-pinned-write-windows.js +5 -5
  80. package/dist/native-pinned-write.d.ts.map +1 -1
  81. package/dist/native-pinned-write.js +14 -4
  82. package/dist/native-staged-file.d.ts +2 -2
  83. package/dist/native-staged-file.d.ts.map +1 -1
  84. package/dist/opened-file-failure.d.ts +1 -0
  85. package/dist/opened-file-failure.d.ts.map +1 -1
  86. package/dist/opened-file-failure.js +4 -0
  87. package/dist/output-sibling.d.ts.map +1 -1
  88. package/dist/output-sibling.js +9 -3
  89. package/dist/pinned-write.d.ts +2 -2
  90. package/dist/pinned-write.d.ts.map +1 -1
  91. package/dist/pinned-write.js +32 -13
  92. package/dist/publish-file.d.ts.map +1 -1
  93. package/dist/publish-file.js +7 -3
  94. package/dist/regular-file.d.ts +1 -1
  95. package/dist/regular-file.d.ts.map +1 -1
  96. package/dist/regular-file.js +35 -6
  97. package/dist/replace-file-descriptor.d.ts +9 -1
  98. package/dist/replace-file-descriptor.d.ts.map +1 -1
  99. package/dist/replace-file-descriptor.js +58 -10
  100. package/dist/replace-file-temp-owner.d.ts +7 -0
  101. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  102. package/dist/replace-file-temp-owner.js +29 -0
  103. package/dist/replace-file.d.ts +1 -0
  104. package/dist/replace-file.d.ts.map +1 -1
  105. package/dist/replace-file.js +12 -38
  106. package/dist/root-context.d.ts +4 -0
  107. package/dist/root-context.d.ts.map +1 -1
  108. package/dist/root-context.js +19 -0
  109. package/dist/root-impl.d.ts +43 -0
  110. package/dist/root-impl.d.ts.map +1 -1
  111. package/dist/root-impl.js +13 -11
  112. package/dist/root-path-symlink.d.ts +6 -2
  113. package/dist/root-path-symlink.d.ts.map +1 -1
  114. package/dist/root-path-symlink.js +7 -6
  115. package/dist/root-path.d.ts +1 -0
  116. package/dist/root-path.d.ts.map +1 -1
  117. package/dist/root-path.js +6 -2
  118. package/dist/root-walk.d.ts.map +1 -1
  119. package/dist/root-walk.js +4 -0
  120. package/dist/root-write-verification.d.ts +3 -2
  121. package/dist/root-write-verification.d.ts.map +1 -1
  122. package/dist/root-write-verification.js +6 -0
  123. package/dist/secret-file.d.ts +9 -0
  124. package/dist/secret-file.d.ts.map +1 -1
  125. package/dist/secret-file.js +136 -74
  126. package/dist/sibling-temp.d.ts.map +1 -1
  127. package/dist/sibling-temp.js +9 -3
  128. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  129. package/dist/sidecar-lock-acquire.js +18 -5
  130. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  131. package/dist/sidecar-lock-reclaim.js +6 -4
  132. package/dist/sidecar-lock.d.ts.map +1 -1
  133. package/dist/sidecar-lock.js +26 -0
  134. package/dist/write-open-flags.d.ts +5 -0
  135. package/dist/write-open-flags.d.ts.map +1 -0
  136. package/dist/write-open-flags.js +35 -0
  137. package/docs/advanced.md +1 -1
  138. package/docs/archive.md +56 -7
  139. package/docs/atomic.md +8 -2
  140. package/docs/contributing.md +13 -0
  141. package/docs/durability.md +3 -2
  142. package/docs/errors.md +3 -1
  143. package/docs/file-store.md +38 -8
  144. package/docs/local-roots.md +4 -3
  145. package/docs/output.md +4 -1
  146. package/docs/private-file-store.md +11 -2
  147. package/docs/reading.md +3 -1
  148. package/docs/regular-file.md +16 -2
  149. package/docs/root.md +4 -2
  150. package/docs/secret-file.md +37 -2
  151. package/docs/sidecar-lock.md +32 -5
  152. package/docs/store.md +5 -1
  153. package/docs/temp.md +19 -1
  154. package/docs/writing.md +24 -3
  155. package/package.json +14 -10
@@ -1,11 +1,12 @@
1
1
  import fs, {} from "node:fs";
2
2
  import path from "node:path";
3
3
  import { normalizeMaxBytes } from "./byte-budget.js";
4
+ import { syncDirectory } from "./directory-durability.js";
4
5
  import { syncQueueDirectoryCreation } from "./json-durable-queue-directory.js";
5
6
  import { acknowledgeDurableQueueEntry, claimDurableQueueEntry, completeDeliveredQueueEntry, moveDurableQueueEntryToFailed, } from "./json-durable-queue-ownership.js";
6
7
  import { stringifyJsonDocument } from "./json-stringify.js";
7
8
  import { resolveReadOpenFlags } from "./read-open-flags.js";
8
- import { replaceFileAtomic } from "./replace-file.js";
9
+ import { replaceFileAtomicWithDirectorySync } from "./replace-file.js";
9
10
  import { assertSafePathSegment } from "./safe-path-segment.js";
10
11
  import { inspectFileIdentity } from "./strict-file-identity.js";
11
12
  export const DEFAULT_JSON_DURABLE_QUEUE_ENTRY_MAX_BYTES = 16 * 1024 * 1024;
@@ -209,24 +210,13 @@ async function chmodQueueDirectory(dir) {
209
210
  }
210
211
  }
211
212
  export async function writeJsonDurableQueueEntry(params) {
212
- await replaceFileAtomic({
213
+ await replaceFileAtomicWithDirectorySync({
213
214
  filePath: params.filePath,
214
215
  content: stringifyJsonDocument(params.entry, null, 2),
215
216
  mode: 0o600,
216
217
  tempPrefix: params.tempPrefix,
217
218
  syncTempFile: true,
218
- syncParentDir: true,
219
- });
220
- }
221
- async function replaceJsonDurableQueueEntry(params) {
222
- await replaceFileAtomic({
223
- filePath: params.filePath,
224
- content: stringifyJsonDocument(params.entry, null, 2),
225
- mode: 0o600,
226
- tempPrefix: params.tempPrefix,
227
- syncTempFile: true,
228
- syncParentDir: true,
229
- });
219
+ }, syncDirectory);
230
220
  }
231
221
  async function inspectQueueEntry(inspect, maxBytes, expected) {
232
222
  let inspectionFailed = false;
@@ -303,7 +293,7 @@ export async function loadJsonDurableQueueEntry(params) {
303
293
  });
304
294
  const result = params.read ? await params.read(raw, params.paths.jsonPath) : { entry: raw };
305
295
  if (result.migrated) {
306
- await replaceJsonDurableQueueEntry({
296
+ await writeJsonDurableQueueEntry({
307
297
  filePath: claimedPath,
308
298
  entry: result.entry,
309
299
  tempPrefix: params.tempPrefix,
@@ -350,6 +340,12 @@ export async function loadPendingJsonDurableQueueEntries(options) {
350
340
  if (!suffix)
351
341
  continue;
352
342
  const id = file.slice(0, -suffix.length);
343
+ try {
344
+ assertSafeQueueEntryId(id);
345
+ }
346
+ catch {
347
+ continue;
348
+ }
353
349
  if (seenIds.has(id))
354
350
  continue;
355
351
  seenIds.add(id);
@@ -357,25 +353,26 @@ export async function loadPendingJsonDurableQueueEntries(options) {
357
353
  }
358
354
  const entries = [];
359
355
  for (const id of ids) {
356
+ const paths = resolveJsonDurableQueueEntryPaths(options.queueDir, id);
357
+ const claimedPath = await claimDurableQueueEntry(paths, { skipUnowned: true });
358
+ if (!claimedPath)
359
+ continue;
360
+ let result;
360
361
  try {
361
- const paths = resolveJsonDurableQueueEntryPaths(options.queueDir, id);
362
- const claimedPath = await claimDurableQueueEntry(paths);
363
- if (!claimedPath)
364
- continue;
365
362
  const raw = await readJsonDurableQueueEntry(claimedPath, { maxBytes: options.maxBytes });
366
- const result = options.read ? await options.read(raw, paths.jsonPath) : { entry: raw };
367
- if (result.migrated) {
368
- await replaceJsonDurableQueueEntry({
369
- filePath: claimedPath,
370
- entry: result.entry,
371
- tempPrefix: options.tempPrefix,
372
- });
373
- }
374
- entries.push(result.entry);
363
+ result = options.read ? await options.read(raw, paths.jsonPath) : { entry: raw };
375
364
  }
376
365
  catch {
377
366
  continue;
378
367
  }
368
+ if (result.migrated) {
369
+ await writeJsonDurableQueueEntry({
370
+ filePath: claimedPath,
371
+ entry: result.entry,
372
+ tempPrefix: options.tempPrefix,
373
+ });
374
+ }
375
+ entries.push(result.entry);
379
376
  }
380
377
  return entries;
381
378
  }
@@ -1 +1 @@
1
- {"version":3,"file":"local-roots.d.ts","sourceRoot":"","sources":["../src/local-roots.ts"],"names":[],"mappings":"AAOA,OAAO,EAAQ,KAAK,cAAc,EAAE,KAAK,UAAU,EAAE,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAE3F,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG,UAAU,GAAG;IAC9C,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,oCAAoC,GAAG,sBAAsB,GAAG;IAC1E,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,6BAA6B,GAAG,sBAAsB,GAAG;IACnE,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,aAAa,CAAC;CAC1B,CAAC;AAqGF,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,oCAAoC,GAC5C,oBAAoB,GAAG,IAAI,CA6B7B;AAED,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,6BAA6B,GACrC,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CA4CtC"}
1
+ {"version":3,"file":"local-roots.d.ts","sourceRoot":"","sources":["../src/local-roots.ts"],"names":[],"mappings":"AAQA,OAAO,EAAQ,KAAK,cAAc,EAAE,KAAK,UAAU,EAAE,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAE3F,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG,UAAU,GAAG;IAC9C,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,oCAAoC,GAAG,sBAAsB,GAAG;IAC1E,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,6BAA6B,GAAG,sBAAsB,GAAG;IACnE,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,aAAa,CAAC;CAC1B,CAAC;AAgDF,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,oCAAoC,GAC5C,oBAAoB,GAAG,IAAI,CAuC7B;AAED,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,6BAA6B,GACrC,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CA4CtC"}
@@ -5,6 +5,7 @@ import { FsSafeError } from "./errors.js";
5
5
  import { expandHomePrefix, resolveUserPath } from "./home-dir.js";
6
6
  import { isFileUrl, safeFileURLToPath } from "./local-file-access.js";
7
7
  import { isPathInside } from "./path.js";
8
+ import { resolveRootPathSync } from "./root-path.js";
8
9
  import { root } from "./root.js";
9
10
  function resolveLocalPathInput(input, label) {
10
11
  if (isFileUrl(input)) {
@@ -37,9 +38,6 @@ function resolveLocalRootInput(input, label) {
37
38
  }
38
39
  return path.resolve(resolved);
39
40
  }
40
- function isPathInsideRoot(candidate, rootDir) {
41
- return isPathInside(rootDir, candidate);
42
- }
43
41
  function resolveRootRealSync(rootDir) {
44
42
  try {
45
43
  // Configured roots may themselves be symlinks. Follow only this trusted
@@ -54,53 +52,6 @@ function resolveRootRealSync(rootDir) {
54
52
  return null;
55
53
  }
56
54
  }
57
- function resolveCandidateCanonicalSync(filePath) {
58
- let sawExistingLeaf = false;
59
- try {
60
- const stat = fsSync.lstatSync(filePath);
61
- sawExistingLeaf = true;
62
- return {
63
- exists: true,
64
- canonicalPath: fsSync.realpathSync(filePath),
65
- isFile: stat.isFile(),
66
- };
67
- }
68
- catch (err) {
69
- if (err.code !== "ENOENT") {
70
- throw err;
71
- }
72
- }
73
- if (sawExistingLeaf) {
74
- // lstat succeeded but realpath failed: this is an existing dangling
75
- // symlink, not a missing path callers may safely create through.
76
- throw new FsSafeError("symlink", "local roots candidate is a dangling symlink");
77
- }
78
- let cursor = filePath;
79
- const missingSegments = [];
80
- while (true) {
81
- const parent = path.dirname(cursor);
82
- if (parent === cursor) {
83
- return { exists: false, canonicalPath: filePath };
84
- }
85
- missingSegments.unshift(path.basename(cursor));
86
- cursor = parent;
87
- try {
88
- fsSync.lstatSync(cursor);
89
- const ancestorReal = fsSync.realpathSync(cursor);
90
- return {
91
- exists: false,
92
- canonicalPath: path.join(ancestorReal, ...missingSegments),
93
- };
94
- }
95
- catch (err) {
96
- if (err.code !== "ENOENT") {
97
- // Existing ancestors that cannot be canonicalized are symlink/error
98
- // terrain; do not reconstruct a trusted missing path through them.
99
- throw err;
100
- }
101
- }
102
- }
103
- }
104
55
  export function resolveLocalPathFromRootsSync(options) {
105
56
  const label = options.label ?? "local roots";
106
57
  const requestedPath = path.resolve(resolveLocalPathInput(options.filePath, "file path"));
@@ -112,7 +63,13 @@ export function resolveLocalPathFromRootsSync(options) {
112
63
  }
113
64
  let candidate;
114
65
  try {
115
- candidate = resolveCandidateCanonicalSync(requestedPath);
66
+ candidate = resolveRootPathSync({
67
+ absolutePath: requestedPath,
68
+ rootPath: rootDir,
69
+ rootCanonicalPath: rootReal,
70
+ boundaryLabel: label,
71
+ rejectUnresolvedSymlinks: true,
72
+ });
116
73
  }
117
74
  catch {
118
75
  continue;
@@ -120,12 +77,17 @@ export function resolveLocalPathFromRootsSync(options) {
120
77
  if (!candidate.exists && options.allowMissing !== true) {
121
78
  continue;
122
79
  }
123
- if (candidate.exists && options.requireFile === true && !candidate.isFile) {
124
- continue;
125
- }
126
- if (isPathInsideRoot(candidate.canonicalPath, rootReal)) {
127
- return { path: candidate.canonicalPath, root: rootReal };
80
+ if (candidate.exists && options.requireFile === true) {
81
+ try {
82
+ if (!fsSync.lstatSync(requestedPath).isFile()) {
83
+ continue;
84
+ }
85
+ }
86
+ catch {
87
+ continue;
88
+ }
128
89
  }
90
+ return { path: candidate.canonicalPath, root: rootReal };
129
91
  }
130
92
  return null;
131
93
  }
@@ -0,0 +1,43 @@
1
+ export type EntryIdentity = {
2
+ ctimeMs: number;
3
+ dev: number;
4
+ ino: number;
5
+ mode: number;
6
+ mtimeMs: number;
7
+ nlink: number;
8
+ size: number;
9
+ };
10
+ export type CopiedEntryManifest = (EntryIdentity & {
11
+ children: Array<{
12
+ name: string;
13
+ manifest: CopiedEntryManifest;
14
+ }>;
15
+ kind: "directory";
16
+ }) | (EntryIdentity & {
17
+ kind: "leaf";
18
+ });
19
+ type CleanupCopiedEntryResult = "removed" | "stale";
20
+ type CleanupAliasGroup = {
21
+ expected: EntryIdentity;
22
+ remainingPaths: Set<string>;
23
+ stale: boolean;
24
+ };
25
+ export type CleanupCopiedEntryState = {
26
+ aliasGroups: Map<string, CleanupAliasGroup>;
27
+ };
28
+ export declare function entryIdentity(stat: {
29
+ ctimeMs: number;
30
+ dev: number;
31
+ ino: number;
32
+ mode: number;
33
+ mtimeMs: number;
34
+ nlink: number;
35
+ size: number;
36
+ }): EntryIdentity;
37
+ export declare function sameIdentity(a: EntryIdentity, b: EntryIdentity): boolean;
38
+ export declare function sourceChangedError(sourcePath: string): Error;
39
+ export declare function assertSourceStillMatches(sourcePath: string, identity: EntryIdentity): Promise<void>;
40
+ export declare function createCleanupCopiedEntryState(sourcePath: string, manifest: CopiedEntryManifest): CleanupCopiedEntryState;
41
+ export declare function cleanupCopiedEntry(sourcePath: string, manifest: CopiedEntryManifest, state: CleanupCopiedEntryState): Promise<CleanupCopiedEntryResult>;
42
+ export {};
43
+ //# sourceMappingURL=move-path-cleanup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"move-path-cleanup.d.ts","sourceRoot":"","sources":["../src/move-path-cleanup.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAC3B,CAAC,aAAa,GAAG;IACf,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,mBAAmB,CAAA;KAAE,CAAC,CAAC;IACjE,IAAI,EAAE,WAAW,CAAC;CACnB,CAAC,GACF,CAAC,aAAa,GAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAEvC,KAAK,wBAAwB,GAAG,SAAS,GAAG,OAAO,CAAC;AAEpD,KAAK,iBAAiB,GAAG;IACvB,QAAQ,EAAE,aAAa,CAAC;IACxB,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC5B,KAAK,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;CAC7C,CAAC;AAEF,wBAAgB,aAAa,CAAC,IAAI,EAAE;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,aAAa,CAUhB;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,aAAa,EAAE,CAAC,EAAE,aAAa,GAAG,OAAO,CAUxE;AAMD,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,KAAK,CAI5D;AAED,wBAAsB,wBAAwB,CAC5C,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,aAAa,GACtB,OAAO,CAAC,IAAI,CAAC,CAIf;AA0BD,wBAAgB,6BAA6B,CAC3C,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,mBAAmB,GAC5B,uBAAuB,CAuBzB;AAsDD,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,mBAAmB,EAC7B,KAAK,EAAE,uBAAuB,GAC7B,OAAO,CAAC,wBAAwB,CAAC,CAkDnC"}
@@ -0,0 +1,159 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ export function entryIdentity(stat) {
4
+ return {
5
+ ctimeMs: stat.ctimeMs,
6
+ dev: stat.dev,
7
+ ino: stat.ino,
8
+ mode: stat.mode,
9
+ mtimeMs: stat.mtimeMs,
10
+ nlink: stat.nlink,
11
+ size: stat.size,
12
+ };
13
+ }
14
+ export function sameIdentity(a, b) {
15
+ return (a.dev === b.dev &&
16
+ a.ino === b.ino &&
17
+ a.mode === b.mode &&
18
+ a.nlink === b.nlink &&
19
+ a.size === b.size &&
20
+ a.mtimeMs === b.mtimeMs &&
21
+ a.ctimeMs === b.ctimeMs);
22
+ }
23
+ function sameDirectoryNode(a, b) {
24
+ return a.dev === b.dev && a.ino === b.ino;
25
+ }
26
+ export function sourceChangedError(sourcePath) {
27
+ return Object.assign(new Error(`Source changed during move fallback: ${sourcePath}`), {
28
+ code: "ESTALE",
29
+ });
30
+ }
31
+ export async function assertSourceStillMatches(sourcePath, identity) {
32
+ if (!sameIdentity(identity, entryIdentity(await fs.lstat(sourcePath)))) {
33
+ throw sourceChangedError(sourcePath);
34
+ }
35
+ }
36
+ function identityKey(identity) {
37
+ return `${identity.dev}:${identity.ino}`;
38
+ }
39
+ function collectAliasCandidates(sourcePath, manifest, candidates) {
40
+ if (manifest.kind === "directory") {
41
+ for (const child of manifest.children) {
42
+ collectAliasCandidates(path.join(sourcePath, child.name), child.manifest, candidates);
43
+ }
44
+ return;
45
+ }
46
+ if (manifest.nlink <= 1) {
47
+ return;
48
+ }
49
+ const key = identityKey(manifest);
50
+ const entries = candidates.get(key) ?? [];
51
+ entries.push({ manifest, path: sourcePath });
52
+ candidates.set(key, entries);
53
+ }
54
+ export function createCleanupCopiedEntryState(sourcePath, manifest) {
55
+ const candidates = new Map();
56
+ collectAliasCandidates(sourcePath, manifest, candidates);
57
+ const aliasGroups = new Map();
58
+ for (const [key, entries] of candidates) {
59
+ if (entries.length < 2) {
60
+ continue;
61
+ }
62
+ const first = entries[0];
63
+ if (!first || entries.some((entry) => !sameIdentity(first.manifest, entry.manifest))) {
64
+ throw sourceChangedError(sourcePath);
65
+ }
66
+ aliasGroups.set(key, {
67
+ expected: first.manifest,
68
+ remainingPaths: new Set(entries.map((entry) => entry.path)),
69
+ stale: false,
70
+ });
71
+ }
72
+ return { aliasGroups };
73
+ }
74
+ function sameOwnedUnlinkTransition(before, after) {
75
+ return (before.dev === after.dev &&
76
+ before.ino === after.ino &&
77
+ before.mode === after.mode &&
78
+ before.size === after.size &&
79
+ before.mtimeMs === after.mtimeMs &&
80
+ before.nlink > 0 &&
81
+ after.nlink === before.nlink - 1 &&
82
+ after.ctimeMs >= before.ctimeMs);
83
+ }
84
+ function poisonAliasGroup(group) {
85
+ group.stale = true;
86
+ return "stale";
87
+ }
88
+ async function observeOwnedAliasUnlink(sourcePath, group) {
89
+ group.remainingPaths.delete(sourcePath);
90
+ const remainingPath = group.remainingPaths.values().next().value;
91
+ if (!remainingPath) {
92
+ return "removed";
93
+ }
94
+ let observed;
95
+ try {
96
+ observed = await fs.lstat(remainingPath);
97
+ }
98
+ catch (error) {
99
+ if (error?.code === "ENOENT") {
100
+ return poisonAliasGroup(group);
101
+ }
102
+ throw error;
103
+ }
104
+ const observedIdentity = entryIdentity(observed);
105
+ if (!sameOwnedUnlinkTransition(group.expected, observedIdentity)) {
106
+ return poisonAliasGroup(group);
107
+ }
108
+ group.expected = observedIdentity;
109
+ return "removed";
110
+ }
111
+ function mergeCleanupResults(a, b) {
112
+ return a === "stale" || b === "stale" ? "stale" : "removed";
113
+ }
114
+ export async function cleanupCopiedEntry(sourcePath, manifest, state) {
115
+ const aliasGroup = manifest.kind === "leaf" ? state.aliasGroups.get(identityKey(manifest)) : undefined;
116
+ if (aliasGroup?.stale) {
117
+ return "stale";
118
+ }
119
+ let currentStat;
120
+ try {
121
+ currentStat = await fs.lstat(sourcePath);
122
+ }
123
+ catch (error) {
124
+ if (error?.code === "ENOENT") {
125
+ return aliasGroup ? poisonAliasGroup(aliasGroup) : "removed";
126
+ }
127
+ throw error;
128
+ }
129
+ if (manifest.kind === "directory") {
130
+ if (!currentStat.isDirectory() || !sameDirectoryNode(manifest, entryIdentity(currentStat))) {
131
+ return "stale";
132
+ }
133
+ // A same-inode directory can gain unrelated children after commit. Still
134
+ // clean manifest children so the fallback does not duplicate copied files.
135
+ let result = "removed";
136
+ for (const child of manifest.children) {
137
+ result = mergeCleanupResults(result, await cleanupCopiedEntry(path.join(sourcePath, child.name), child.manifest, state));
138
+ }
139
+ try {
140
+ await fs.rmdir(sourcePath);
141
+ }
142
+ catch (error) {
143
+ const code = error?.code;
144
+ if (code === "ENOTEMPTY" || code === "EEXIST") {
145
+ return "stale";
146
+ }
147
+ throw error;
148
+ }
149
+ return result;
150
+ }
151
+ const expected = aliasGroup?.expected ?? manifest;
152
+ if (!sameIdentity(expected, entryIdentity(currentStat))) {
153
+ return aliasGroup ? poisonAliasGroup(aliasGroup) : "stale";
154
+ }
155
+ await fs.unlink(sourcePath);
156
+ return aliasGroup
157
+ ? await observeOwnedAliasUnlink(sourcePath, aliasGroup)
158
+ : "removed";
159
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"move-path.d.ts","sourceRoot":"","sources":["../src/move-path.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,+BAA+B,GAAG;IAC5C,uFAAuF;IACvF,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;IACrC,EAAE,EAAE,MAAM,CAAC;CACZ,CAAC;AAEF,KAAK,sBAAsB,GAAG,cAAc,GAAG,uBAAuB,CAAC;AAEvE,wBAAgB,oCAAoC,CAClD,KAAK,EAAE,OAAO,EACd,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C,sBAAsB,GAAG,SAAS,CASpC;AAiVD,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,+BAA+B,GACvC,OAAO,CAAC,IAAI,CAAC,CA4Ef"}
1
+ {"version":3,"file":"move-path.d.ts","sourceRoot":"","sources":["../src/move-path.ts"],"names":[],"mappings":"AAoBA,MAAM,MAAM,+BAA+B,GAAG;IAC5C,uFAAuF;IACvF,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;IACrC,EAAE,EAAE,MAAM,CAAC;CACZ,CAAC;AAEF,KAAK,sBAAsB,GAAG,cAAc,GAAG,uBAAuB,CAAC;AAEvE,wBAAgB,oCAAoC,CAClD,KAAK,EAAE,OAAO,EACd,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C,sBAAsB,GAAG,SAAS,CASpC;AA8OD,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,+BAA+B,GACvC,OAAO,CAAC,IAAI,CAAC,CAkFf"}
package/dist/move-path.js CHANGED
@@ -4,6 +4,7 @@ import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { FsSafeError } from "./errors.js";
6
6
  import { guardedRename } from "./guarded-mutation.js";
7
+ import { assertSourceStillMatches, cleanupCopiedEntry, createCleanupCopiedEntryState, entryIdentity, sameIdentity, sourceChangedError, } from "./move-path-cleanup.js";
7
8
  import { resolveReadOpenFlags } from "./read-open-flags.js";
8
9
  import { registerTempPathForExit } from "./temp-cleanup.js";
9
10
  export function moveCopyFallbackReasonForRenameError(error, platform = process.platform) {
@@ -50,51 +51,34 @@ function isSameOrDescendant(parentPath, candidatePath) {
50
51
  return (relative === "" ||
51
52
  (!path.isAbsolute(relative) && !relative.startsWith(`..${path.sep}`) && relative !== ".."));
52
53
  }
53
- async function assertCopyDestinationOutsideSource(sourcePath, targetPath) {
54
- const sourceReal = await fs.realpath(sourcePath);
54
+ async function assertCopyDestinationOutsideSource(sourcePath, targetPath, expectedIdentity) {
55
+ const sourceStat = await fs.lstat(sourcePath);
56
+ const sourceIdentity = entryIdentity(sourceStat);
57
+ if (expectedIdentity && !sameIdentity(expectedIdentity, sourceIdentity)) {
58
+ throw sourceChangedError(sourcePath);
59
+ }
60
+ const normalizedSource = path.resolve(sourcePath);
55
61
  const normalizedTarget = path.resolve(targetPath);
56
- const targetParentReal = await fs.realpath(path.dirname(normalizedTarget));
62
+ const [sourceParentReal, targetParentReal] = await Promise.all([
63
+ fs.realpath(path.dirname(normalizedSource)),
64
+ fs.realpath(path.dirname(normalizedTarget)),
65
+ ]);
66
+ const sourceCandidate = path.join(sourceParentReal, path.basename(normalizedSource));
57
67
  const targetCandidate = path.join(targetParentReal, path.basename(normalizedTarget));
58
- if (isSameOrDescendant(sourceReal, targetCandidate)) {
68
+ const sourceBoundary = sourceStat.isDirectory()
69
+ ? await fs.realpath(sourcePath)
70
+ : sourceCandidate;
71
+ const unsafeTarget = sourceStat.isDirectory()
72
+ ? isSameOrDescendant(sourceBoundary, targetCandidate)
73
+ : path.relative(sourceBoundary, targetCandidate) === "";
74
+ if (unsafeTarget) {
59
75
  throw new FsSafeError("invalid-path", "Move destination must not be inside the source");
60
76
  }
61
- }
62
- function entryIdentity(stat) {
63
- return {
64
- ctimeMs: stat.ctimeMs,
65
- dev: stat.dev,
66
- ino: stat.ino,
67
- mode: stat.mode,
68
- mtimeMs: stat.mtimeMs,
69
- nlink: stat.nlink,
70
- size: stat.size,
71
- };
72
- }
73
- function sameIdentity(a, b) {
74
- return (a.dev === b.dev &&
75
- a.ino === b.ino &&
76
- a.mode === b.mode &&
77
- a.nlink === b.nlink &&
78
- a.size === b.size &&
79
- a.mtimeMs === b.mtimeMs &&
80
- a.ctimeMs === b.ctimeMs);
81
- }
82
- function sameDirectoryNode(a, b) {
83
- return a.dev === b.dev && a.ino === b.ino;
77
+ return sourceIdentity;
84
78
  }
85
79
  function modeBits(mode) {
86
80
  return mode & 0o777;
87
81
  }
88
- function sourceChangedError(sourcePath) {
89
- return Object.assign(new Error(`Source changed during move fallback: ${sourcePath}`), {
90
- code: "ESTALE",
91
- });
92
- }
93
- async function assertSourceStillMatches(sourcePath, identity) {
94
- if (!sameIdentity(identity, entryIdentity(await fs.lstat(sourcePath)))) {
95
- throw sourceChangedError(sourcePath);
96
- }
97
- }
98
82
  async function chmodDirectoryPinned(directoryPath, mode) {
99
83
  if (process.platform === "win32") {
100
84
  // Node cannot portably open a Windows directory for descriptor-bound
@@ -174,9 +158,12 @@ async function copyRegularFilePinned(params) {
174
158
  await sourceHandle.close();
175
159
  }
176
160
  }
177
- async function copyEntryWithManifest(from, to, options) {
161
+ async function copyEntryWithManifest(from, to, options, expectedIdentity) {
178
162
  const sourceStat = await fs.lstat(from);
179
163
  const identity = entryIdentity(sourceStat);
164
+ if (expectedIdentity && !sameIdentity(expectedIdentity, identity)) {
165
+ throw sourceChangedError(from);
166
+ }
180
167
  if (sourceStat.isSymbolicLink()) {
181
168
  await fs.symlink(await fs.readlink(from), to);
182
169
  // readlink() is path-based; verify the symlink we copied is still the one
@@ -224,48 +211,6 @@ async function copyEntryWithManifest(from, to, options) {
224
211
  });
225
212
  return { ...identity, kind: "leaf" };
226
213
  }
227
- function mergeCleanupResults(a, b) {
228
- return a === "stale" || b === "stale" ? "stale" : "removed";
229
- }
230
- async function cleanupCopiedEntry(sourcePath, manifest) {
231
- let currentStat;
232
- try {
233
- currentStat = await fs.lstat(sourcePath);
234
- }
235
- catch (error) {
236
- if (error?.code === "ENOENT") {
237
- return "removed";
238
- }
239
- throw error;
240
- }
241
- if (manifest.kind === "directory") {
242
- if (!currentStat.isDirectory() || !sameDirectoryNode(manifest, entryIdentity(currentStat))) {
243
- return "stale";
244
- }
245
- // A same-inode directory can gain unrelated children after commit. Still
246
- // clean manifest children so the fallback does not duplicate copied files.
247
- let result = "removed";
248
- for (const child of manifest.children) {
249
- result = mergeCleanupResults(result, await cleanupCopiedEntry(path.join(sourcePath, child.name), child.manifest));
250
- }
251
- try {
252
- await fs.rmdir(sourcePath);
253
- }
254
- catch (error) {
255
- const code = error?.code;
256
- if (code === "ENOTEMPTY" || code === "EEXIST") {
257
- return "stale";
258
- }
259
- throw error;
260
- }
261
- return result;
262
- }
263
- if (!sameIdentity(manifest, entryIdentity(currentStat))) {
264
- return "stale";
265
- }
266
- await fs.unlink(sourcePath);
267
- return "removed";
268
- }
269
214
  export async function movePathWithCopyFallback(options) {
270
215
  // Keep the initiating owner's callback across preparation and copy fallback.
271
216
  const callerAssert = options.assertBeforeRename;
@@ -307,7 +252,7 @@ export async function movePathWithCopyFallback(options) {
307
252
  // Commit a fresh inode through the copy path so a post-scan hardlink can
308
253
  // never become the published target; the copy loop fences nlink again.
309
254
  }
310
- await assertCopyDestinationOutsideSource(sourcePath, targetPath);
255
+ const sourceIdentity = await assertCopyDestinationOutsideSource(sourcePath, targetPath);
311
256
  const targetDir = path.dirname(targetPath);
312
257
  const staged = path.join(targetDir, `.fs-safe-move-${process.pid}-${randomUUID()}.tmp`);
313
258
  const unregisterStaged = registerTempPathForExit(staged, { recursive: true });
@@ -316,13 +261,14 @@ export async function movePathWithCopyFallback(options) {
316
261
  const manifest = await copyEntryWithManifest(sourcePath, staged, {
317
262
  sourceHardlinks: options.sourceHardlinks ?? "allow",
318
263
  ...(rejectHardlinks ? { budget: { discovered: 1 } } : {}),
319
- });
264
+ }, sourceIdentity);
265
+ const cleanupState = createCleanupCopiedEntryState(sourcePath, manifest);
320
266
  unregisterStaged.setIdentity(await fs.lstat(staged, { bigint: true }));
321
- await assertCopyDestinationOutsideSource(sourcePath, targetPath);
267
+ await assertCopyDestinationOutsideSource(sourcePath, targetPath, manifest);
322
268
  await guardedRename({ from: staged, to: targetPath, assertBeforeRename });
323
269
  stagedCommitted = true;
324
270
  unregisterStaged();
325
- const cleanupResult = await cleanupCopiedEntry(sourcePath, manifest);
271
+ const cleanupResult = await cleanupCopiedEntry(sourcePath, manifest, cleanupState);
326
272
  if (cleanupResult === "stale") {
327
273
  throw sourceChangedError(sourcePath);
328
274
  }
@@ -1,6 +1,5 @@
1
- import { type Stats } from "node:fs";
1
+ import { type BigIntStats, type Stats } from "node:fs";
2
2
  import type { ContainmentGuarantee } from "./containment.js";
3
- import { type NativeBinding } from "./native.js";
4
3
  import type { PinnedWriteInput } from "./pinned-write.js";
5
4
  export type NativeFileHandle = {
6
5
  readonly fd: number;
@@ -13,11 +12,10 @@ export declare function nativeOpenFlags(flags: number): number;
13
12
  export declare function writeNativeFd(fd: number, data: Buffer): void;
14
13
  export declare function writeNativeInput(fd: number, input: PinnedWriteInput, maxBytes?: number): Promise<void>;
15
14
  export declare function removeNativeCreatedFileIfStillPinned(params: {
16
- binding: NativeBinding;
17
15
  parentPath: string;
18
16
  parentFd: number;
19
17
  basename: string;
20
- created?: Stats;
18
+ created?: BigIntStats;
21
19
  }): void;
22
20
  export declare function createNativeExclusiveFile(targetPath: string, mode: number): Promise<NativeFileHandle | undefined>;
23
21
  export declare function syncNativeFileBestEffort(fd: number): void;
@@ -1 +1 @@
1
- {"version":3,"file":"native-operations.d.ts","sourceRoot":"","sources":["../src/native-operations.ts"],"names":[],"mappings":"AAAA,OAAe,EAAE,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAG7C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAG7D,OAAO,EAAoB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,WAAW,EAAE,oBAAoB,CAAC;IAC3C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC;IACvB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,QAAQ,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5E,CAAC;AAEF,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOrD;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAS5D;AAED,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,gBAAgB,EACvB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAgBf;AAsBD,wBAAgB,oCAAoC,CAAC,MAAM,EAAE;IAC3D,OAAO,EAAE,aAAa,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,KAAK,CAAC;CACjB,GAAG,IAAI,CAsBP;AAED,wBAAsB,yBAAyB,CAC7C,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAyDvC;AAED,wBAAgB,wBAAwB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAQzD"}
1
+ {"version":3,"file":"native-operations.d.ts","sourceRoot":"","sources":["../src/native-operations.ts"],"names":[],"mappings":"AAAA,OAAe,EAAE,KAAK,WAAW,EAAE,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAG/D,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAI7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,WAAW,EAAE,oBAAoB,CAAC;IAC3C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC;IACvB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,QAAQ,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5E,CAAC;AAEF,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOrD;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAS5D;AAED,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,gBAAgB,EACvB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAgBf;AAsBD,wBAAgB,oCAAoC,CAAC,MAAM,EAAE;IAC3D,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,WAAW,CAAC;CACvB,GAAG,IAAI,CAqBP;AAED,wBAAsB,yBAAyB,CAC7C,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAwDvC;AAED,wBAAgB,wBAAwB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAQzD"}
@@ -62,14 +62,13 @@ export function removeNativeCreatedFileIfStillPinned(params) {
62
62
  return;
63
63
  }
64
64
  try {
65
- const parentPathStat = fsSync.lstatSync(params.parentPath);
66
- const parentFdStat = params.binding.fstatIdentity(params.parentFd);
65
+ const parentPathStat = fsSync.lstatSync(params.parentPath, { bigint: true });
66
+ const parentFdStat = fsSync.fstatSync(params.parentFd, { bigint: true });
67
67
  const targetPath = path.join(params.parentPath, params.basename);
68
- const target = fsSync.lstatSync(targetPath);
69
- if (!parentPathStat.isSymbolicLink() &&
70
- parentPathStat.dev === parentFdStat.dev &&
71
- parentPathStat.ino === parentFdStat.ino &&
72
- !target.isSymbolicLink() &&
68
+ const target = fsSync.lstatSync(targetPath, { bigint: true });
69
+ if (!parentPathStat.isSymbolicLink() && parentPathStat.isDirectory() &&
70
+ sameFileIdentityForCleanup(parentPathStat, parentFdStat) &&
71
+ !target.isSymbolicLink() && target.isFile() &&
73
72
  sameFileIdentityForCleanup(target, params.created)) {
74
73
  fsSync.rmSync(targetPath);
75
74
  }
@@ -107,7 +106,7 @@ export async function createNativeExclusiveFile(targetPath, mode) {
107
106
  }
108
107
  fd = opened.fd;
109
108
  fsSync.fchmodSync(fd, mode);
110
- created = fsSync.fstatSync(fd);
109
+ created = fsSync.fstatSync(fd, { bigint: true });
111
110
  return wrapNativeFd(fd, opened.containment);
112
111
  }
113
112
  catch (error) {
@@ -119,7 +118,6 @@ export async function createNativeExclusiveFile(targetPath, mode) {
119
118
  // Preserve the original error.
120
119
  }
121
120
  removeNativeCreatedFileIfStillPinned({
122
- binding,
123
121
  parentPath,
124
122
  parentFd: parent.fd,
125
123
  basename,