@openclaw/fs-safe 0.7.2 → 0.8.1

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 (159) hide show
  1. package/CHANGELOG.md +71 -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 +1 -0
  129. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  130. package/dist/sidecar-lock-acquire.js +24 -5
  131. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  132. package/dist/sidecar-lock-reclaim.js +6 -4
  133. package/dist/sidecar-lock-types.d.ts +7 -0
  134. package/dist/sidecar-lock-types.d.ts.map +1 -1
  135. package/dist/sidecar-lock.d.ts +2 -0
  136. package/dist/sidecar-lock.d.ts.map +1 -1
  137. package/dist/sidecar-lock.js +44 -3
  138. package/dist/write-open-flags.d.ts +5 -0
  139. package/dist/write-open-flags.d.ts.map +1 -0
  140. package/dist/write-open-flags.js +35 -0
  141. package/docs/advanced.md +1 -1
  142. package/docs/archive.md +56 -7
  143. package/docs/atomic.md +8 -2
  144. package/docs/contributing.md +13 -0
  145. package/docs/durability.md +3 -2
  146. package/docs/errors.md +3 -1
  147. package/docs/file-store.md +38 -8
  148. package/docs/local-roots.md +4 -3
  149. package/docs/output.md +4 -1
  150. package/docs/private-file-store.md +11 -2
  151. package/docs/reading.md +3 -1
  152. package/docs/regular-file.md +16 -2
  153. package/docs/root.md +4 -2
  154. package/docs/secret-file.md +37 -2
  155. package/docs/sidecar-lock.md +33 -5
  156. package/docs/store.md +5 -1
  157. package/docs/temp.md +19 -1
  158. package/docs/writing.md +24 -3
  159. package/package.json +14 -10
@@ -1,8 +1,8 @@
1
1
  import type { FileHandle } from "node:fs/promises";
2
- import type { AsyncDirectoryGuard } from "./directory-guard.js";
2
+ import type { AnyAsyncDirectoryGuard } from "./directory-guard.js";
3
3
  import type { FileIdentityStat } from "./file-identity.js";
4
4
  import type { NativeBinding } from "./native.js";
5
5
  import type { PinnedWriteParams } from "./pinned-write.js";
6
6
  export declare function sameNativeIdentity(left: Pick<FileIdentityStat, "dev" | "ino">, right: Pick<FileIdentityStat, "dev" | "ino">): boolean;
7
- export declare function runPinnedWriteWindows(binding: NativeBinding, params: PinnedWriteParams, root: FileHandle, parentFd: number, parentGuard: AsyncDirectoryGuard): Promise<FileIdentityStat>;
7
+ export declare function runPinnedWriteWindows(binding: NativeBinding, params: PinnedWriteParams, root: FileHandle, parentFd: number, parentGuard: AnyAsyncDirectoryGuard): Promise<FileIdentityStat>;
8
8
  //# sourceMappingURL=native-pinned-write-windows.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"native-pinned-write-windows.d.ts","sourceRoot":"","sources":["../src/native-pinned-write-windows.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEhE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAO3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAE3D,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,IAAI,CAAC,gBAAgB,EAAE,KAAK,GAAG,KAAK,CAAC,EAC3C,KAAK,EAAE,IAAI,CAAC,gBAAgB,EAAE,KAAK,GAAG,KAAK,CAAC,GAC3C,OAAO,CAET;AAWD,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,aAAa,EACtB,MAAM,EAAE,iBAAiB,EACzB,IAAI,EAAE,UAAU,EAChB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,mBAAmB,GAC/B,OAAO,CAAC,gBAAgB,CAAC,CAgF3B"}
1
+ {"version":3,"file":"native-pinned-write-windows.d.ts","sourceRoot":"","sources":["../src/native-pinned-write-windows.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAEnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAO3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAG3D,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,IAAI,CAAC,gBAAgB,EAAE,KAAK,GAAG,KAAK,CAAC,EAC3C,KAAK,EAAE,IAAI,CAAC,gBAAgB,EAAE,KAAK,GAAG,KAAK,CAAC,GAC3C,OAAO,CAET;AAWD,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,aAAa,EACtB,MAAM,EAAE,iBAAiB,EACzB,IAAI,EAAE,UAAU,EAChB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,sBAAsB,GAClC,OAAO,CAAC,gBAAgB,CAAC,CAiF3B"}
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import fsSync, {} from "node:fs";
3
3
  import { FsSafeError } from "./errors.js";
4
4
  import { nativeOpenFlags, removeNativeCreatedFileIfStillPinned, syncNativeFileBestEffort, writeNativeInput, } from "./native-operations.js";
5
+ import { inspectFileIdentitySync } from "./strict-file-identity.js";
5
6
  export function sameNativeIdentity(left, right) {
6
7
  return left.dev === right.dev && left.ino === right.ino;
7
8
  }
@@ -26,8 +27,8 @@ export async function runPinnedWriteWindows(binding, params, root, parentFd, par
26
27
  try {
27
28
  tempName = `.fs-safe-${randomUUID()}.tmp`;
28
29
  tempFd = binding.openBeneath(parentFd, tempName, nativeOpenFlags(fsSync.constants.O_WRONLY | fsSync.constants.O_CREAT | fsSync.constants.O_EXCL)).fd;
29
- tempIdentity = fsSync.fstatSync(tempFd);
30
- const verificationIdentity = fsSync.fstatSync(tempFd, { bigint: true });
30
+ const verificationIdentity = inspectFileIdentitySync(() => fsSync.fstatSync(tempFd, { bigint: true }));
31
+ tempIdentity = verificationIdentity;
31
32
  // Creation is requested at 0600 in the binding, but a restrictive umask
32
33
  // can remove owner access. Keep the unpublished inode private and
33
34
  // reopenable until the published name has been identity-fenced.
@@ -42,8 +43,9 @@ export async function runPinnedWriteWindows(binding, params, root, parentFd, par
42
43
  }
43
44
  renamed = true;
44
45
  targetFd = binding.openBeneath(parentFd, params.basename, nativeOpenFlags(fsSync.constants.O_RDONLY)).fd;
46
+ const targetStat = inspectFileIdentitySync(() => fsSync.fstatSync(targetFd, { bigint: true }), verificationIdentity);
45
47
  const targetIdentity = binding.fstatIdentity(targetFd);
46
- if (!targetIdentity.isFile || !sameNativeIdentity(tempIdentity, targetIdentity)) {
48
+ if (!targetStat.isFile()) {
47
49
  throw new FsSafeError("path-mismatch", "native write target changed after rename");
48
50
  }
49
51
  // Native exclusive creation starts at 0600. Apply the requested mode only
@@ -57,7 +59,6 @@ export async function runPinnedWriteWindows(binding, params, root, parentFd, par
57
59
  closeWriteFd(targetFd);
58
60
  targetFd = undefined;
59
61
  removeNativeCreatedFileIfStillPinned({
60
- binding,
61
62
  parentPath,
62
63
  parentFd,
63
64
  basename: params.basename,
@@ -76,7 +77,6 @@ export async function runPinnedWriteWindows(binding, params, root, parentFd, par
76
77
  const tempCloseError = closeWriteFd(tempFd);
77
78
  if (!renamed) {
78
79
  removeNativeCreatedFileIfStillPinned({
79
- binding,
80
80
  parentPath,
81
81
  parentFd,
82
82
  basename: tempName,
@@ -1 +1 @@
1
- {"version":3,"file":"native-pinned-write.d.ts","sourceRoot":"","sources":["../src/native-pinned-write.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAG3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAG3D,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAmFvH"}
1
+ {"version":3,"file":"native-pinned-write.d.ts","sourceRoot":"","sources":["../src/native-pinned-write.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAG3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAI3D,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CA0FvH"}
@@ -53,10 +53,12 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
53
53
  import fsSync from "node:fs";
54
54
  import fs from "node:fs/promises";
55
55
  import path from "node:path";
56
+ import { inspectDirectoryIdentity } from "./directory-guard.js";
56
57
  import { FsSafeError } from "./errors.js";
57
58
  import { runPinnedWriteWindows, sameNativeIdentity } from "./native-pinned-write-windows.js";
58
59
  import { assertNativeStaging, writeNativeStage } from "./native-staged-file.js";
59
60
  import { describeStagedDirectory, exactIdentityMatches } from "./staged-directory.js";
61
+ import { inspectFileIdentitySync } from "./strict-file-identity.js";
60
62
  export async function runPinnedWriteNative(binding, params) {
61
63
  const env_1 = { stack: [], error: void 0, hasError: false };
62
64
  try {
@@ -79,8 +81,14 @@ export async function runPinnedWriteNative(binding, params) {
79
81
  },
80
82
  }, false);
81
83
  try {
84
+ const exactRoot = typeof params.rootIdentity?.dev === "bigint" && typeof params.rootIdentity.ino === "bigint"
85
+ ? { dev: params.rootIdentity.dev, ino: params.rootIdentity.ino } : undefined;
82
86
  let rootMatches;
83
- if (windows) {
87
+ if (exactRoot) {
88
+ inspectFileIdentitySync(() => fsSync.fstatSync(root.fd, { bigint: true }), exactRoot);
89
+ rootMatches = true;
90
+ }
91
+ else if (windows) {
84
92
  const identity = binding.fstatIdentity(root.fd);
85
93
  rootMatches = !params.rootIdentity || sameNativeIdentity(params.rootIdentity, identity);
86
94
  }
@@ -99,14 +107,16 @@ export async function runPinnedWriteNative(binding, params) {
99
107
  ? path.join(params.rootPath, ...params.relativeParentPath.split("/"))
100
108
  : params.rootPath);
101
109
  const directory = windows ? undefined : describeStagedDirectory(parentFd, parentPath);
102
- const parentPathStat = await fs.lstat(parentPath);
103
- if (windows) {
110
+ const parentPathStat = exactRoot
111
+ ? await inspectDirectoryIdentity(parentPath, inspectFileIdentitySync(() => fsSync.fstatSync(parentFd, { bigint: true })))
112
+ : await fs.lstat(parentPath);
113
+ if (windows && !exactRoot) {
104
114
  const parentIdentity = binding.fstatIdentity(parentFd);
105
115
  if (parentPathStat.isSymbolicLink() || !sameNativeIdentity(parentPathStat, parentIdentity)) {
106
116
  throw new FsSafeError("path-mismatch", "native write parent changed during resolution");
107
117
  }
108
118
  }
109
- else if (parentPathStat.isSymbolicLink() || !exactIdentityMatches(parentPathStat, directory.identity)) {
119
+ else if (!windows && (parentPathStat.isSymbolicLink() || !exactIdentityMatches(parentPathStat, directory.identity))) {
110
120
  throw new FsSafeError("path-mismatch", "native write parent changed during resolution");
111
121
  }
112
122
  const verificationGuard = { dir: parentPath, realPath: parentPath, stat: parentPathStat };
@@ -1,4 +1,4 @@
1
- import type { AsyncDirectoryGuard } from "./directory-guard.js";
1
+ import type { AnyAsyncDirectoryGuard } from "./directory-guard.js";
2
2
  import type { FileIdentityStat } from "./file-identity.js";
3
3
  import type { NativeBinding } from "./native-binding.js";
4
4
  import type { PinnedWriteInput, PinnedWriteParams } from "./pinned-write.js";
@@ -9,7 +9,7 @@ declare class NativeStagedFile implements StagedFile {
9
9
  #private;
10
10
  constructor(binding: NativeStagingBinding, parentFd: number, directory: StagedFileReceipt["directory"], portableNames: boolean, publishedMode: number);
11
11
  static create(binding: NativeStagingBinding, parentFd: number, directory: StagedFileReceipt["directory"], input: PinnedWriteInput, mode: number, maxBytes?: number, portableNames?: boolean): Promise<NativeStagedFile>;
12
- static write(binding: NativeStagingBinding, parentFd: number, directory: StagedFileReceipt["directory"], params: PinnedWriteParams, parentGuard: AsyncDirectoryGuard): Promise<FileIdentityStat>;
12
+ static write(binding: NativeStagingBinding, parentFd: number, directory: StagedFileReceipt["directory"], params: PinnedWriteParams, parentGuard: AnyAsyncDirectoryGuard): Promise<FileIdentityStat>;
13
13
  get receipt(): StagedFileReceipt;
14
14
  assertCurrent(): Promise<void>;
15
15
  publish(basename: string, options: {
@@ -1 +1 @@
1
- {"version":3,"file":"native-staged-file.d.ts","sourceRoot":"","sources":["../src/native-staged-file.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAEhE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAE7E,OAAO,KAAK,EACV,oBAAoB,EACpB,UAAU,EACV,wBAAwB,EAGxB,iBAAiB,EAClB,MAAM,wBAAwB,CAAC;AAEhC,MAAM,MAAM,oBAAoB,GAAG,aAAa,GAAG,QAAQ,CAAC,IAAI,CAC9D,aAAa,EACb,kBAAkB,GAAG,mBAAmB,GAAG,kBAAkB,CAC9D,CAAC,CAAC;AAEH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,IAAI,oBAAoB,CAUnG;AA6BD,cAAM,gBAAiB,YAAW,UAAU;;IAU1C,YACE,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,iBAAiB,CAAC,WAAW,CAAC,EACzC,aAAa,EAAE,OAAO,EACtB,aAAa,EAAE,MAAM,EAOtB;IAGD,OAAa,MAAM,CACjB,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,iBAAiB,CAAC,WAAW,CAAC,EACzC,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,MAAM,EACZ,QAAQ,CAAC,EAAE,MAAM,EAEjB,aAAa,UAAO,GACnB,OAAO,CAAC,gBAAgB,CAAC,CAc3B;IAED,OAAa,KAAK,CAChB,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,iBAAiB,CAAC,WAAW,CAAC,EACzC,MAAM,EAAE,iBAAiB,EACzB,WAAW,EAAE,mBAAmB,GAC/B,OAAO,CAAC,gBAAgB,CAAC,CAU3B;IAED,IAAI,OAAO,IAAI,iBAAiB,CAK/B;IAoFK,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAEnC;IAEK,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,SAAS,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAgD9F;IA6CK,OAAO,IAAI,OAAO,CAAC,wBAAwB,CAAC,CAMjD;IAEK,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAO3C;CACF;AAED,eAAO,MAAM,iBAAiB,EAAE,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,OAAO,gBAAgB,CAAC,MAAM,CAAC,KAAK,OAAO,CAAC,UAAU,CAClF,CAAC;AAC1B,eAAO,MAAM,gBAAgB,+BAAyB,CAAC"}
1
+ {"version":3,"file":"native-staged-file.d.ts","sourceRoot":"","sources":["../src/native-staged-file.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAEnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAE7E,OAAO,KAAK,EACV,oBAAoB,EACpB,UAAU,EACV,wBAAwB,EAGxB,iBAAiB,EAClB,MAAM,wBAAwB,CAAC;AAEhC,MAAM,MAAM,oBAAoB,GAAG,aAAa,GAAG,QAAQ,CAAC,IAAI,CAC9D,aAAa,EACb,kBAAkB,GAAG,mBAAmB,GAAG,kBAAkB,CAC9D,CAAC,CAAC;AAEH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,IAAI,oBAAoB,CAUnG;AA6BD,cAAM,gBAAiB,YAAW,UAAU;;IAU1C,YACE,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,iBAAiB,CAAC,WAAW,CAAC,EACzC,aAAa,EAAE,OAAO,EACtB,aAAa,EAAE,MAAM,EAOtB;IAGD,OAAa,MAAM,CACjB,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,iBAAiB,CAAC,WAAW,CAAC,EACzC,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,MAAM,EACZ,QAAQ,CAAC,EAAE,MAAM,EAEjB,aAAa,UAAO,GACnB,OAAO,CAAC,gBAAgB,CAAC,CAc3B;IAED,OAAa,KAAK,CAChB,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,iBAAiB,CAAC,WAAW,CAAC,EACzC,MAAM,EAAE,iBAAiB,EACzB,WAAW,EAAE,sBAAsB,GAClC,OAAO,CAAC,gBAAgB,CAAC,CAU3B;IAED,IAAI,OAAO,IAAI,iBAAiB,CAK/B;IAoFK,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAEnC;IAEK,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,SAAS,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAgD9F;IA6CK,OAAO,IAAI,OAAO,CAAC,wBAAwB,CAAC,CAMjD;IAEK,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAO3C;CACF;AAED,eAAO,MAAM,iBAAiB,EAAE,CAAC,GAAG,IAAI,EAAE,UAAU,CAAC,OAAO,gBAAgB,CAAC,MAAM,CAAC,KAAK,OAAO,CAAC,UAAU,CAClF,CAAC;AAC1B,eAAO,MAAM,gBAAgB,+BAAyB,CAAC"}
@@ -1,6 +1,7 @@
1
1
  import type { BigIntStats } from "node:fs";
2
2
  import type { FileHandle } from "node:fs/promises";
3
3
  export declare function recordFileOpenFailure(error: unknown, filePath: string): never;
4
+ export declare function recordExclusiveCreateFailure(error: unknown, filePath: string): never;
4
5
  export declare function openedPathResolutionError(error?: Error): Error;
5
6
  export declare function recordPreOpenFileChange(error: unknown, handle: FileHandle, filePath: string, before: BigIntStats | undefined, opened: BigIntStats | undefined): Promise<void>;
6
7
  export declare function recordOpenedFileFailure(error: unknown, handle: FileHandle, filePath: string, identity: BigIntStats): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"opened-file-failure.d.ts","sourceRoot":"","sources":["../src/opened-file-failure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAKnD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,KAAK,CAG7E;AAED,wBAAgB,yBAAyB,CACvC,KAAK,GAAE,KAA8E,GACpF,KAAK,CAGP;AAED,wBAAsB,uBAAuB,CAC3C,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,UAAU,EAClB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,MAAM,EAAE,WAAW,GAAG,SAAS,GAC9B,OAAO,CAAC,IAAI,CAAC,CAcf;AAED,wBAAsB,uBAAuB,CAC3C,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,UAAU,EAClB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,WAAW,GACpB,OAAO,CAAC,IAAI,CAAC,CASf"}
1
+ {"version":3,"file":"opened-file-failure.d.ts","sourceRoot":"","sources":["../src/opened-file-failure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAKnD,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,KAAK,CAG7E;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,KAAK,CAGpF;AAED,wBAAgB,yBAAyB,CACvC,KAAK,GAAE,KAA8E,GACpF,KAAK,CAGP;AAED,wBAAsB,uBAAuB,CAC3C,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,UAAU,EAClB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,WAAW,GAAG,SAAS,EAC/B,MAAM,EAAE,WAAW,GAAG,SAAS,GAC9B,OAAO,CAAC,IAAI,CAAC,CAcf;AAED,wBAAsB,uBAAuB,CAC3C,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,UAAU,EAClB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,WAAW,GACpB,OAAO,CAAC,IAAI,CAAC,CASf"}
@@ -5,6 +5,10 @@ export function recordFileOpenFailure(error, filePath) {
5
5
  recordFileObservationFailure(error, `open:${filePath}`);
6
6
  throw error;
7
7
  }
8
+ export function recordExclusiveCreateFailure(error, filePath) {
9
+ recordFileObservationFailure(error, `exclusive-create:${filePath}`);
10
+ throw error;
11
+ }
8
12
  export function openedPathResolutionError(error = new FsSafeError("path-mismatch", "unable to resolve opened file path")) {
9
13
  recordFileObservationFailure(error, "resolution");
10
14
  return error;
@@ -1 +1 @@
1
- {"version":3,"file":"output-sibling.d.ts","sourceRoot":"","sources":["../src/output-sibling.ts"],"names":[],"mappings":"AAoBA,wBAAsB,2BAA2B,CAAC,CAAC,EAAE,MAAM,EAAE;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACxC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GAAG,OAAO,CAAC,CAAC,CAAC,CAYb"}
1
+ {"version":3,"file":"output-sibling.d.ts","sourceRoot":"","sources":["../src/output-sibling.ts"],"names":[],"mappings":"AAuBA,wBAAsB,2BAA2B,CAAC,CAAC,EAAE,MAAM,EAAE;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACxC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GAAG,OAAO,CAAC,CAAC,CAAC,CAYb"}
@@ -1,13 +1,19 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import path from "node:path";
3
- import { sanitizeUntrustedFileName } from "./filename.js";
3
+ import { fitFileNameToPortableComponent, sanitizeUntrustedFileName } from "./filename.js";
4
4
  import { writeCallbackSibling } from "./sibling-staged-file.js";
5
5
  function safeFallbackFileName(fallbackFileName) {
6
6
  return sanitizeUntrustedFileName(fallbackFileName ?? "output.bin", "output.bin");
7
7
  }
8
8
  function buildSiblingTempPath(targetPath, fallbackFileName) {
9
- const safeTail = sanitizeUntrustedFileName(path.basename(targetPath), safeFallbackFileName(fallbackFileName));
10
- return path.join(path.dirname(targetPath), `.fs-safe-output-${process.pid}-${randomUUID()}-${safeTail}.part`);
9
+ const prefix = `.fs-safe-output-${process.pid}-${randomUUID()}-`;
10
+ const suffix = ".part";
11
+ const safeTail = fitFileNameToPortableComponent({
12
+ prefix,
13
+ fileName: sanitizeUntrustedFileName(path.basename(targetPath), safeFallbackFileName(fallbackFileName)),
14
+ suffix,
15
+ });
16
+ return path.join(path.dirname(targetPath), `${prefix}${safeTail}${suffix}`);
11
17
  }
12
18
  export async function writeExternalFileViaSibling(params) {
13
19
  const finalPath = path.resolve(params.finalPath);
@@ -1,5 +1,5 @@
1
1
  import type { Readable } from "node:stream";
2
- import { type AsyncDirectoryGuard } from "./directory-guard.js";
2
+ import { type AnyAsyncDirectoryGuard } from "./directory-guard.js";
3
3
  import type { FileIdentityStat } from "./file-identity.js";
4
4
  export type PinnedWriteInput = {
5
5
  kind: "buffer";
@@ -26,7 +26,7 @@ export type PinnedWriteParams = {
26
26
  input: PinnedWriteInput;
27
27
  rootIdentity?: FileIdentityStat;
28
28
  onRenameIdentityMismatch?: RenameIdentityMismatchPolicy;
29
- verifyPublished?: (fd: number, identity: PublishedWriteIdentity, parentGuard: AsyncDirectoryGuard) => Promise<void>;
29
+ verifyPublished?: (fd: number, identity: PublishedWriteIdentity, parentGuard: AnyAsyncDirectoryGuard) => Promise<void>;
30
30
  };
31
31
  export declare function runPinnedWriteHelper(params: PinnedWriteParams): Promise<FileIdentityStat>;
32
32
  export declare function runPinnedWriteWithRenamePolicy(params: PinnedWriteParams & {
@@ -1 +1 @@
1
- {"version":3,"file":"pinned-write.d.ts","sourceRoot":"","sources":["../src/pinned-write.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,OAAO,EAAkE,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAGhI,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAW3D,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,cAAc,CAAA;CAAE,GACpE;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,QAAQ,CAAA;CAAE,CAAC;AAgEzC,MAAM,MAAM,4BAA4B,GAAG,OAAO,GAAG,gBAAgB,CAAC;AAEtE,MAAM,MAAM,oBAAoB,GAAG,QAAQ,GAAG,0BAA0B,CAAC;AAEzE,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAE5E,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,gBAAgB,CAAC;IACxB,YAAY,CAAC,EAAE,gBAAgB,CAAC;IAChC,wBAAwB,CAAC,EAAE,4BAA4B,CAAC;IAExD,eAAe,CAAC,EAAE,CAChB,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,sBAAsB,EAChC,WAAW,EAAE,mBAAmB,KAC7B,OAAO,CAAC,IAAI,CAAC,CAAC;CACpB,CAAC;AAEF,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAgB/F;AAED,wBAAsB,8BAA8B,CAClD,MAAM,EAAE,iBAAiB,GAAG;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,oBAAoB,CAAC;CACvC,GACA,OAAO,CAAC,gBAAgB,CAAC,CA2B3B"}
1
+ {"version":3,"file":"pinned-write.d.ts","sourceRoot":"","sources":["../src/pinned-write.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,OAAO,EAA4F,KAAK,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAG7J,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAY3D,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,cAAc,CAAA;CAAE,GACpE;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,QAAQ,CAAA;CAAE,CAAC;AAgEzC,MAAM,MAAM,4BAA4B,GAAG,OAAO,GAAG,gBAAgB,CAAC;AAEtE,MAAM,MAAM,oBAAoB,GAAG,QAAQ,GAAG,0BAA0B,CAAC;AAEzE,MAAM,MAAM,sBAAsB,GAAG,QAAQ,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAE5E,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,gBAAgB,CAAC;IACxB,YAAY,CAAC,EAAE,gBAAgB,CAAC;IAChC,wBAAwB,CAAC,EAAE,4BAA4B,CAAC;IAExD,eAAe,CAAC,EAAE,CAChB,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,sBAAsB,EAChC,WAAW,EAAE,sBAAsB,KAChC,OAAO,CAAC,IAAI,CAAC,CAAC;CACpB,CAAC;AAEF,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAgB/F;AAED,wBAAsB,8BAA8B,CAClD,MAAM,EAAE,iBAAiB,GAAG;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,oBAAoB,CAAC;CACvC,GACA,OAAO,CAAC,gBAAgB,CAAC,CA2B3B"}
@@ -1,9 +1,9 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import fsSync from "node:fs";
2
+ import fsSync, {} from "node:fs";
3
3
  import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { normalizeMaxBytes } from "./byte-budget.js";
6
- import { createAsyncDirectoryGuard, createNearestExistingDirectoryGuard } from "./directory-guard.js";
6
+ import { createAsyncDirectoryGuard, createNearestExistingDirectoryGuard, inspectDirectoryIdentity } from "./directory-guard.js";
7
7
  import { FsSafeError } from "./errors.js";
8
8
  import { syncDirectoryBestEffort } from "./fsync.js";
9
9
  import { sameFileIdentity, sha256Hex } from "./file-identity.js";
@@ -13,6 +13,7 @@ import { runPinnedWriteNative } from "./native-pinned-write.js";
13
13
  import { getNativeBinding } from "./native.js";
14
14
  import { validatePinnedOperationPayload } from "./pinned-operation.js";
15
15
  import { resolveReadOpenFlags } from "./read-open-flags.js";
16
+ import { cleanupPinnedFilePath } from "./replace-file-temp-owner.js";
16
17
  import { withSidecarLock } from "./sidecar-lock.js";
17
18
  import { getFsSafeTestHooks } from "./test-hooks.js";
18
19
  function byteLength(input, encoding) {
@@ -99,6 +100,11 @@ export async function runPinnedWriteWithRenamePolicy(params) {
99
100
  }));
100
101
  }
101
102
  async function runPinnedWriteFallback(params) {
103
+ const exactRoot = typeof params.rootIdentity?.dev === "bigint" && typeof params.rootIdentity.ino === "bigint"
104
+ ? { dev: params.rootIdentity.dev, ino: params.rootIdentity.ino } : undefined;
105
+ if (exactRoot)
106
+ await inspectDirectoryIdentity(params.rootPath, exactRoot);
107
+ const guardOptions = { bigint: exactRoot !== undefined };
102
108
  let parentPath = params.relativeParentPath
103
109
  ? path.join(params.rootPath, ...params.relativeParentPath.split("/"))
104
110
  : params.rootPath;
@@ -114,8 +120,8 @@ async function runPinnedWriteFallback(params) {
114
120
  });
115
121
  }
116
122
  const parentGuard = params.mkdir
117
- ? await createAsyncDirectoryGuard(parentPath)
118
- : await createNearestExistingDirectoryGuard(params.rootPath, parentPath);
123
+ ? await createAsyncDirectoryGuard(parentPath, guardOptions)
124
+ : await createNearestExistingDirectoryGuard(params.rootPath, parentPath, guardOptions);
119
125
  const targetPath = path.join(parentPath, params.basename);
120
126
  if (params.overwrite === false) {
121
127
  const handle = await withAsyncDirectoryGuards([parentGuard], async () => await fs.open(targetPath, fsSync.constants.O_WRONLY | fsSync.constants.O_CREAT | fsSync.constants.O_EXCL, params.mode), {
@@ -126,9 +132,10 @@ async function runPinnedWriteFallback(params) {
126
132
  },
127
133
  });
128
134
  let created = true;
135
+ let createdIdentity;
129
136
  try {
130
137
  const verificationIdentity = await handle.stat({ bigint: true });
131
- await handle.chmod(params.mode);
138
+ createdIdentity = verificationIdentity;
132
139
  if (params.input.kind === "buffer") {
133
140
  assertWithinMaxBytes(byteLength(params.input.data, params.input.encoding), params.maxBytes);
134
141
  if (typeof params.input.data === "string") {
@@ -141,6 +148,8 @@ async function runPinnedWriteFallback(params) {
141
148
  else {
142
149
  await writeStreamToHandle(params.input.stream, handle, params.maxBytes);
143
150
  }
151
+ // Content writes may clear set-ID bits; finalize them through the owned fd.
152
+ await handle.chmod(params.mode);
144
153
  await syncFileBestEffort(handle);
145
154
  const stat = await handle.stat();
146
155
  await syncDirectoryBestEffort(parentPath);
@@ -150,9 +159,13 @@ async function runPinnedWriteFallback(params) {
150
159
  return { dev: stat.dev, ino: stat.ino };
151
160
  }
152
161
  finally {
153
- await handle.close().catch(() => undefined);
154
- if (created) {
155
- await fs.rm(targetPath, { force: true }).catch(() => undefined);
162
+ try {
163
+ if (created) {
164
+ await cleanupPinnedFilePath({ pathname: targetPath, handle, identity: createdIdentity, parentGuard });
165
+ }
166
+ }
167
+ finally {
168
+ await handle.close().catch(() => undefined);
156
169
  }
157
170
  }
158
171
  }
@@ -166,12 +179,13 @@ async function runPinnedWriteFallback(params) {
166
179
  : 0);
167
180
  let handle;
168
181
  let tempStat;
182
+ let tempIdentity;
169
183
  let readHandle;
170
184
  let renamed = false;
171
185
  try {
172
186
  handle = await fs.open(tempPath, tempFlags, params.mode);
173
187
  let verificationIdentity = await handle.stat({ bigint: true });
174
- await handle.chmod(params.mode);
188
+ tempIdentity = verificationIdentity;
175
189
  if (params.input.kind === "buffer") {
176
190
  assertWithinMaxBytes(byteLength(params.input.data, params.input.encoding), params.maxBytes);
177
191
  if (typeof params.input.data === "string") {
@@ -190,6 +204,7 @@ async function runPinnedWriteFallback(params) {
190
204
  throw new FsSafeError("path-mismatch", "fallback temp path changed during write");
191
205
  }
192
206
  const expectedTempStat = tempStat;
207
+ await handle.chmod(params.mode);
193
208
  await syncFileBestEffort(handle);
194
209
  let verifiedIdentity = expectedTempStat;
195
210
  await withAsyncDirectoryGuards([parentGuard], async () => {
@@ -229,10 +244,14 @@ async function runPinnedWriteFallback(params) {
229
244
  return { dev: verifiedIdentity.dev, ino: verifiedIdentity.ino };
230
245
  }
231
246
  finally {
232
- await readHandle?.close().catch(() => undefined);
233
- await handle?.close().catch(() => undefined);
234
- if (!renamed) {
235
- await fs.rm(tempPath, { force: true }).catch(() => undefined);
247
+ try {
248
+ if (!renamed && handle) {
249
+ await cleanupPinnedFilePath({ pathname: tempPath, handle, identity: tempIdentity, parentGuard });
250
+ }
251
+ }
252
+ finally {
253
+ await readHandle?.close().catch(() => undefined);
254
+ await handle?.close().catch(() => undefined);
236
255
  }
237
256
  }
238
257
  }
@@ -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;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"}
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;AA0ND,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"}
@@ -90,9 +90,11 @@ async function copyPinnedSource(params) {
90
90
  throw error;
91
91
  }
92
92
  let target;
93
- const createdIdentity = fsSync.fstatSync(nativeFd, { bigint: true });
93
+ let createdIdentity = fsSync.fstatSync(nativeFd, { bigint: true });
94
94
  rememberCreatedTarget(params.failure, createdIdentity, "copy-verify");
95
95
  try {
96
+ fsSync.fchmodSync(nativeFd, 0o600);
97
+ createdIdentity = fsSync.fstatSync(nativeFd, { bigint: true });
96
98
  await getFsSafeTestHooks()?.afterPublishTargetCreated?.("exclusive-copy", params.targetPath, createdIdentity);
97
99
  const identity = await fs.lstat(params.targetPath, { bigint: true });
98
100
  target = await fs.open(params.targetPath, sourceOpenFlags());
@@ -124,9 +126,11 @@ async function copyPinnedSource(params) {
124
126
  }
125
127
  }
126
128
  const target = await fs.open(params.targetPath, "wx+", 0o600);
127
- const exactIdentity = await target.stat({ bigint: true });
128
- rememberCreatedTarget(params.failure, exactIdentity, "copy-verify");
129
+ const createdIdentity = await target.stat({ bigint: true });
130
+ rememberCreatedTarget(params.failure, createdIdentity, "copy-verify");
129
131
  try {
132
+ await target.chmod(0o600);
133
+ const exactIdentity = await target.stat({ bigint: true });
130
134
  await getFsSafeTestHooks()?.afterPublishTargetCreated?.("exclusive-copy", params.targetPath, exactIdentity);
131
135
  const hash = createHash("sha256");
132
136
  const buffer = Buffer.allocUnsafe(64 * 1024);
@@ -6,7 +6,7 @@ export type RegularFileStatResult = {
6
6
  missing: false;
7
7
  stat: Stats;
8
8
  };
9
- type RegularFileAppendFlagConstants = Pick<typeof fsSync.constants, "O_APPEND" | "O_CREAT" | "O_WRONLY"> & Partial<Pick<typeof fsSync.constants, "O_NOFOLLOW">>;
9
+ type RegularFileAppendFlagConstants = Pick<typeof fsSync.constants, "O_APPEND" | "O_CREAT" | "O_WRONLY"> & Partial<Pick<typeof fsSync.constants, "O_NOFOLLOW" | "O_NONBLOCK">>;
10
10
  export type AppendRegularFileOptions = {
11
11
  filePath: string;
12
12
  content: string | Uint8Array;
@@ -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;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"}
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;AAmB7B,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,GAAG,YAAY,CAAC,CAAC,CAAC;AAEtE,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,CASR;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,CAgFxF;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,wBAAwB,GAAG,IAAI,CAgF7E"}
@@ -10,12 +10,14 @@ import { isNotFoundPathError } from "./path.js";
10
10
  import { resolveReadOpenFlags } from "./read-open-flags.js";
11
11
  import { assertNoSymlinkParents, assertNoSymlinkParentsSync } from "./symlink-parents.js";
12
12
  import { getFsSafeTestHooks } from "./test-hooks.js";
13
+ import { isNonRegularWriteOpenError, isNonRegularWriteOpenErrorSync, resolveNonblockingWriteFlag, } from "./write-open-flags.js";
13
14
  export function resolveRegularFileAppendFlags(constants = fsSync.constants) {
14
15
  const noFollow = constants.O_NOFOLLOW;
15
16
  return (constants.O_CREAT |
16
17
  constants.O_APPEND |
17
18
  constants.O_WRONLY |
18
- (typeof noFollow === "number" ? noFollow : 0));
19
+ (typeof noFollow === "number" ? noFollow : 0) |
20
+ resolveNonblockingWriteFlag(constants));
19
21
  }
20
22
  function regularFileTooLargeError(filePath, maxBytes, cause) {
21
23
  return new FsSafeError("too-large", `File exceeds ${maxBytes} bytes: ${filePath}`, { cause });
@@ -265,7 +267,17 @@ export async function appendRegularFile(options) {
265
267
  return;
266
268
  }
267
269
  await getFsSafeTestHooks()?.beforeRegularFileAppendOpen?.(options.filePath);
268
- const handle = await fs.open(options.filePath, resolveRegularFileAppendFlags(), options.mode ?? 0o600);
270
+ const flags = resolveRegularFileAppendFlags();
271
+ let handle;
272
+ try {
273
+ handle = await fs.open(options.filePath, flags, options.mode ?? 0o600);
274
+ }
275
+ catch (error) {
276
+ if (await isNonRegularWriteOpenError(error, options.filePath, flags)) {
277
+ throw new Error(`Refusing to append to non-file: ${options.filePath}`);
278
+ }
279
+ throw error;
280
+ }
269
281
  try {
270
282
  let identity;
271
283
  try {
@@ -287,8 +299,12 @@ export async function appendRegularFile(options) {
287
299
  Number(identity.size) + contentBytes > options.maxFileBytes) {
288
300
  return;
289
301
  }
290
- await handle.chmod(options.mode ?? 0o600);
302
+ const mode = options.mode ?? 0o600;
303
+ // Tighten before writing; restore explicit special bits only after content is complete.
304
+ await handle.chmod(mode);
291
305
  await handle.appendFile(options.content, options.encoding ?? "utf8");
306
+ if (mode & 0o7000)
307
+ await handle.chmod(mode);
292
308
  }
293
309
  finally {
294
310
  await handle.close();
@@ -331,7 +347,17 @@ export function appendRegularFileSync(options) {
331
347
  return;
332
348
  }
333
349
  getFsSafeTestHooks()?.beforeRegularFileAppendOpenSync?.(options.filePath);
334
- const fd = fsSync.openSync(options.filePath, resolveRegularFileAppendFlags(), options.mode ?? 0o600);
350
+ const flags = resolveRegularFileAppendFlags();
351
+ let fd;
352
+ try {
353
+ fd = fsSync.openSync(options.filePath, flags, options.mode ?? 0o600);
354
+ }
355
+ catch (error) {
356
+ if (isNonRegularWriteOpenErrorSync(error, options.filePath, flags)) {
357
+ throw new Error(`Refusing to append to non-file: ${options.filePath}`);
358
+ }
359
+ throw error;
360
+ }
335
361
  try {
336
362
  let identity;
337
363
  try {
@@ -353,8 +379,11 @@ export function appendRegularFileSync(options) {
353
379
  Number(identity.size) + contentBuffer.byteLength > options.maxFileBytes) {
354
380
  return;
355
381
  }
356
- fsSync.fchmodSync(fd, options.mode ?? 0o600);
357
- fsSync.writeSync(fd, contentBuffer, 0, contentBuffer.byteLength);
382
+ const mode = options.mode ?? 0o600;
383
+ fsSync.fchmodSync(fd, mode);
384
+ fsSync.appendFileSync(fd, contentBuffer);
385
+ if (mode & 0o7000)
386
+ fsSync.fchmodSync(fd, mode);
358
387
  }
359
388
  finally {
360
389
  fsSync.closeSync(fd);
@@ -1,13 +1,21 @@
1
1
  import syncFs, { type BigIntStats } from "node:fs";
2
2
  import fs, { type FileHandle } from "node:fs/promises";
3
+ import { type DirectoryModeOwner } from "./directory-mode-owner.js";
3
4
  type AsyncTempFileSystem = Pick<typeof fs, "lstat" | "open" | "writeFile">;
4
5
  type SyncTempFileSystem = Pick<typeof syncFs, "closeSync" | "fstatSync" | "fsyncSync" | "lstatSync" | "openSync" | "writeFileSync">;
5
6
  export type SyncFchmod = (fd: number, mode: number) => void;
7
+ export declare function syncDirectoryBestEffort(fsModule: Pick<typeof fs, "open">, dirPath: string): Promise<void>;
8
+ export declare function syncDirectoryBestEffortSync(fsModule: Pick<typeof syncFs, "openSync" | "fsyncSync" | "closeSync">, dirPath: string): void;
9
+ export declare function pinDirectoryForMode(params: {
10
+ fsModule: AsyncTempFileSystem;
11
+ dirPath: string;
12
+ /** Compatibility for best-effort directory modes; admission and close still fail closed. */
13
+ ignoreChmodError?: boolean;
14
+ }): Promise<DirectoryModeOwner | undefined>;
6
15
  export declare function applyDirectoryMode(params: {
7
16
  fsModule: AsyncTempFileSystem;
8
17
  dirPath: string;
9
18
  mode: number;
10
- /** Compatibility for best-effort directory modes; admission and close still fail closed. */
11
19
  ignoreChmodError?: boolean;
12
20
  }): Promise<void>;
13
21
  export declare function applyDirectoryModeSync(params: {
@@ -1 +1 @@
1
- {"version":3,"file":"replace-file-descriptor.d.ts","sourceRoot":"","sources":["../src/replace-file-descriptor.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,EAAE,EAAE,KAAK,WAAW,EAAc,MAAM,SAAS,CAAC;AAC/D,OAAO,EAAE,EAAE,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAKvD,KAAK,mBAAmB,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,GAAG,MAAM,GAAG,WAAW,CAAC,CAAC;AAC3E,KAAK,kBAAkB,GAAG,IAAI,CAC5B,OAAO,MAAM,EACb,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,UAAU,GAAG,eAAe,CACrF,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;AA2B5D,wBAAsB,kBAAkB,CAAC,MAAM,EAAE;IAC/C,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,4FAA4F;IAC5F,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBhB;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE;IAC7C,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB,GAAG,IAAI,CAcP;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE;IAC1C,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,WAAW,KAAK,IAAI,CAAC;CAC9C,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,WAAW,CAAA;CAAE,CAAC,CA0BzD;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE;IACxC,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,IAAI,EAAE,OAAO,CAAC;IACd,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,WAAW,KAAK,IAAI,CAAC;CAC9C,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,WAAW,CAAA;CAAE,CA0BxC"}
1
+ {"version":3,"file":"replace-file-descriptor.d.ts","sourceRoot":"","sources":["../src/replace-file-descriptor.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,EAAE,EAAE,KAAK,WAAW,EAAc,MAAM,SAAS,CAAC;AAC/D,OAAO,EAAE,EAAE,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAIvD,OAAO,EAA0C,KAAK,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAE5G,KAAK,mBAAmB,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,GAAG,MAAM,GAAG,WAAW,CAAC,CAAC;AAC3E,KAAK,kBAAkB,GAAG,IAAI,CAC5B,OAAO,MAAM,EACb,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,UAAU,GAAG,eAAe,CACrF,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;AAE5D,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,IAAI,CAAC,OAAO,EAAE,EAAE,MAAM,CAAC,EACjC,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC,CAUf;AAED,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,IAAI,CAAC,OAAO,MAAM,EAAE,UAAU,GAAG,WAAW,GAAG,WAAW,CAAC,EACrE,OAAO,EAAE,MAAM,GACd,IAAI,CAgBN;AA2BD,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,4FAA4F;IAC5F,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,GAAG,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC,CA4B1C;AAED,wBAAsB,kBAAkB,CAAC,MAAM,EAAE;IAC/C,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,GAAG,OAAO,CAAC,IAAI,CAAC,CAOhB;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE;IAC7C,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,UAAU,CAAC;CACzB,GAAG,IAAI,CAcP;AAED,wBAAsB,aAAa,CAAC,MAAM,EAAE;IAC1C,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,WAAW,KAAK,IAAI,CAAC;CAC9C,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE,WAAW,CAAA;CAAE,CAAC,CA0BzD;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE;IACxC,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,UAAU,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,IAAI,EAAE,OAAO,CAAC;IACd,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,WAAW,KAAK,IAAI,CAAC;CAC9C,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,WAAW,CAAA;CAAE,CA0BxC"}
@@ -3,6 +3,40 @@ import fs, {} from "node:fs/promises";
3
3
  import { FsSafeError } from "./errors.js";
4
4
  import { sameFileIdentity } from "./file-identity.js";
5
5
  import { inspectFileIdentity, inspectFileIdentitySync } from "./strict-file-identity.js";
6
+ import { assertOwnedDirectory, ownDirectoryMode } from "./directory-mode-owner.js";
7
+ export async function syncDirectoryBestEffort(fsModule, dirPath) {
8
+ let handle;
9
+ try {
10
+ handle = await fsModule.open(dirPath, "r");
11
+ await handle.sync();
12
+ }
13
+ catch {
14
+ // Best-effort on platforms/filesystems that do not support directory fsync.
15
+ }
16
+ finally {
17
+ await handle?.close().catch(() => undefined);
18
+ }
19
+ }
20
+ export function syncDirectoryBestEffortSync(fsModule, dirPath) {
21
+ let fd;
22
+ try {
23
+ fd = fsModule.openSync(dirPath, "r");
24
+ fsModule.fsyncSync(fd);
25
+ }
26
+ catch {
27
+ // Best-effort on platforms/filesystems that do not support directory fsync.
28
+ }
29
+ finally {
30
+ if (fd !== undefined) {
31
+ try {
32
+ fsModule.closeSync(fd);
33
+ }
34
+ catch {
35
+ // Best-effort close after directory fsync.
36
+ }
37
+ }
38
+ }
39
+ }
6
40
  function directoryOpenFlags() {
7
41
  return (syncFs.constants.O_RDONLY |
8
42
  syncFs.constants.O_DIRECTORY |
@@ -20,7 +54,7 @@ function assertSameDirectory(expected, opened, dirPath) {
20
54
  throw new FsSafeError("path-mismatch", `Atomic replace parent changed before its mode could be applied: ${dirPath}`);
21
55
  }
22
56
  }
23
- export async function applyDirectoryMode(params) {
57
+ export async function pinDirectoryForMode(params) {
24
58
  // Node does not enforce POSIX directory modes on Windows, and its directory
25
59
  // descriptors are not consistently openable. mkdir(mode) remains the only
26
60
  // bounded behavior there; never fall back to a pathname chmod.
@@ -31,17 +65,31 @@ export async function applyDirectoryMode(params) {
31
65
  assertDirectory(expected, params.dirPath);
32
66
  const handle = await params.fsModule.open(params.dirPath, directoryOpenFlags());
33
67
  try {
34
- assertSameDirectory(expected, await handle.stat(), params.dirPath);
35
- try {
36
- await handle.chmod(params.mode);
37
- }
38
- catch (error) {
39
- if (params.ignoreChmodError !== true)
40
- throw error;
41
- }
68
+ const owner = ownDirectoryMode({
69
+ async inspect() {
70
+ const opened = await handle.stat();
71
+ assertOwnedDirectory(expected, opened);
72
+ return opened.mode & 0o7777;
73
+ },
74
+ chmod: (mode) => handle.chmod(mode),
75
+ close: () => handle.close(),
76
+ ignoreChmodError: params.ignoreChmodError,
77
+ });
78
+ await owner.verify();
79
+ return owner;
42
80
  }
43
- finally {
81
+ catch (error) {
44
82
  await handle.close();
83
+ throw error;
84
+ }
85
+ }
86
+ export async function applyDirectoryMode(params) {
87
+ const owner = await pinDirectoryForMode(params);
88
+ try {
89
+ await owner?.apply(params.mode);
90
+ }
91
+ finally {
92
+ await owner?.close();
45
93
  }
46
94
  }
47
95
  export function applyDirectoryModeSync(params) {
@@ -1,7 +1,14 @@
1
1
  import syncFs, { type BigIntStats } from "node:fs";
2
2
  import fs, { type FileHandle } from "node:fs/promises";
3
+ import { type AnyAsyncDirectoryGuard } from "./directory-guard.js";
3
4
  type AsyncOwnerFileSystem = Pick<typeof fs, "lstat" | "open" | "unlink">;
4
5
  type SyncOwnerFileSystem = Pick<typeof syncFs, "closeSync" | "fstatSync" | "lstatSync" | "openSync" | "readFileSync" | "unlinkSync">;
6
+ export declare function cleanupPinnedFilePath(params: {
7
+ pathname: string;
8
+ handle: FileHandle;
9
+ identity?: BigIntStats;
10
+ parentGuard: AnyAsyncDirectoryGuard;
11
+ }): Promise<void>;
5
12
  export declare class AsyncAtomicTempOwner {
6
13
  #private;
7
14
  readonly pathname: string;