@openclaw/fs-safe 0.9.0 → 0.10.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 (178) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +17 -0
  3. package/dist/advanced.d.ts +1 -0
  4. package/dist/advanced.d.ts.map +1 -1
  5. package/dist/advanced.js +1 -0
  6. package/dist/archive-crc32.d.ts.map +1 -1
  7. package/dist/archive-crc32.js +6 -1
  8. package/dist/archive-deadline.d.ts.map +1 -1
  9. package/dist/archive-deadline.js +3 -4
  10. package/dist/archive-gzip-tail.d.ts +2 -0
  11. package/dist/archive-gzip-tail.d.ts.map +1 -1
  12. package/dist/archive-gzip-tail.js +20 -3
  13. package/dist/archive-merge.js +1 -1
  14. package/dist/archive-native.d.ts.map +1 -1
  15. package/dist/archive-native.js +3 -2
  16. package/dist/archive-read.d.ts.map +1 -1
  17. package/dist/archive-read.js +67 -66
  18. package/dist/archive-tar-stream.d.ts +11 -4
  19. package/dist/archive-tar-stream.d.ts.map +1 -1
  20. package/dist/archive-tar-stream.js +20 -8
  21. package/dist/archive-tar-wasm.d.ts.map +1 -1
  22. package/dist/archive-tar-wasm.js +2 -1
  23. package/dist/bounded-read.d.ts +7 -0
  24. package/dist/bounded-read.d.ts.map +1 -1
  25. package/dist/bounded-read.js +73 -43
  26. package/dist/clone-metadata.d.ts +19 -0
  27. package/dist/clone-metadata.d.ts.map +1 -0
  28. package/dist/clone-metadata.js +32 -0
  29. package/dist/copy-file-input.d.ts +23 -0
  30. package/dist/copy-file-input.d.ts.map +1 -0
  31. package/dist/copy-file-input.js +91 -0
  32. package/dist/copy-policy.d.ts +3 -0
  33. package/dist/copy-policy.d.ts.map +1 -0
  34. package/dist/copy-policy.js +8 -0
  35. package/dist/copy-publication.d.ts +10 -1
  36. package/dist/copy-publication.d.ts.map +1 -1
  37. package/dist/copy-publication.js +27 -0
  38. package/dist/copy-tree-portable.d.ts +9 -0
  39. package/dist/copy-tree-portable.d.ts.map +1 -0
  40. package/dist/copy-tree-portable.js +191 -0
  41. package/dist/copy.d.ts +16 -0
  42. package/dist/copy.d.ts.map +1 -0
  43. package/dist/copy.js +123 -0
  44. package/dist/durability.d.ts +1 -1
  45. package/dist/durability.d.ts.map +1 -1
  46. package/dist/error-detail.d.ts.map +1 -1
  47. package/dist/error-detail.js +4 -1
  48. package/dist/file-hash.d.ts +6 -2
  49. package/dist/file-hash.d.ts.map +1 -1
  50. package/dist/file-hash.js +44 -8
  51. package/dist/filename.d.ts.map +1 -1
  52. package/dist/filename.js +2 -12
  53. package/dist/guarded-mkdir.d.ts +1 -0
  54. package/dist/guarded-mkdir.d.ts.map +1 -1
  55. package/dist/guarded-mkdir.js +1 -0
  56. package/dist/guarded-mutation.d.ts +2 -0
  57. package/dist/guarded-mutation.d.ts.map +1 -1
  58. package/dist/guarded-mutation.js +8 -4
  59. package/dist/home-dir.d.ts.map +1 -1
  60. package/dist/home-dir.js +9 -7
  61. package/dist/index.d.ts +1 -1
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/install-path.d.ts.map +1 -1
  64. package/dist/install-path.js +2 -6
  65. package/dist/json-durable-queue.d.ts.map +1 -1
  66. package/dist/json-durable-queue.js +13 -14
  67. package/dist/local-roots.d.ts.map +1 -1
  68. package/dist/local-roots.js +22 -24
  69. package/dist/move-path-stage.d.ts +8 -0
  70. package/dist/move-path-stage.d.ts.map +1 -0
  71. package/dist/move-path-stage.js +55 -0
  72. package/dist/move-path.d.ts.map +1 -1
  73. package/dist/move-path.js +26 -31
  74. package/dist/mutation-authority.d.ts +8 -0
  75. package/dist/mutation-authority.d.ts.map +1 -0
  76. package/dist/mutation-authority.js +36 -0
  77. package/dist/native-binding.d.ts +24 -1
  78. package/dist/native-binding.d.ts.map +1 -1
  79. package/dist/native-operations.d.ts +1 -1
  80. package/dist/native-operations.d.ts.map +1 -1
  81. package/dist/native-operations.js +11 -5
  82. package/dist/native-pinned-write-windows.d.ts.map +1 -1
  83. package/dist/native-pinned-write-windows.js +18 -7
  84. package/dist/native-pinned-write.d.ts.map +1 -1
  85. package/dist/native-pinned-write.js +1 -0
  86. package/dist/native-staged-file.d.ts +3 -3
  87. package/dist/native-staged-file.d.ts.map +1 -1
  88. package/dist/native-staged-file.js +35 -8
  89. package/dist/path.d.ts.map +1 -1
  90. package/dist/path.js +3 -1
  91. package/dist/permissions-windows.d.ts.map +1 -1
  92. package/dist/permissions-windows.js +38 -48
  93. package/dist/pinned-operation.js +1 -1
  94. package/dist/pinned-write.d.ts +5 -1
  95. package/dist/pinned-write.d.ts.map +1 -1
  96. package/dist/pinned-write.js +50 -30
  97. package/dist/positional-read.d.ts +9 -0
  98. package/dist/positional-read.d.ts.map +1 -0
  99. package/dist/positional-read.js +36 -0
  100. package/dist/publish-copy-stage.d.ts +13 -0
  101. package/dist/publish-copy-stage.d.ts.map +1 -0
  102. package/dist/publish-copy-stage.js +47 -0
  103. package/dist/publish-file.d.ts.map +1 -1
  104. package/dist/publish-file.js +6 -5
  105. package/dist/replace-file-temp-owner.d.ts.map +1 -1
  106. package/dist/replace-file-temp-owner.js +6 -2
  107. package/dist/root-context.d.ts +3 -0
  108. package/dist/root-context.d.ts.map +1 -1
  109. package/dist/root-context.js +51 -26
  110. package/dist/root-directory-list.d.ts +24 -0
  111. package/dist/root-directory-list.d.ts.map +1 -0
  112. package/dist/root-directory-list.js +201 -0
  113. package/dist/root-errors.d.ts +1 -0
  114. package/dist/root-errors.d.ts.map +1 -1
  115. package/dist/root-errors.js +3 -0
  116. package/dist/root-file.d.ts +2 -0
  117. package/dist/root-file.d.ts.map +1 -1
  118. package/dist/root-file.js +12 -4
  119. package/dist/root-impl.d.ts +6 -50
  120. package/dist/root-impl.d.ts.map +1 -1
  121. package/dist/root-impl.js +330 -210
  122. package/dist/root-options.d.ts +66 -0
  123. package/dist/root-options.d.ts.map +1 -0
  124. package/dist/root-options.js +18 -0
  125. package/dist/root-path-existing.d.ts +5 -0
  126. package/dist/root-path-existing.d.ts.map +1 -1
  127. package/dist/root-path-existing.js +56 -1
  128. package/dist/root-path.d.ts +1 -0
  129. package/dist/root-path.d.ts.map +1 -1
  130. package/dist/root-path.js +74 -162
  131. package/dist/root-symlink-policy.d.ts +13 -0
  132. package/dist/root-symlink-policy.d.ts.map +1 -0
  133. package/dist/root-symlink-policy.js +34 -0
  134. package/dist/root-walk.d.ts +5 -4
  135. package/dist/root-walk.d.ts.map +1 -1
  136. package/dist/root-walk.js +99 -50
  137. package/dist/root-write-mode.d.ts.map +1 -1
  138. package/dist/root-write-mode.js +1 -4
  139. package/dist/root.d.ts +3 -1
  140. package/dist/root.d.ts.map +1 -1
  141. package/dist/secure-file.d.ts.map +1 -1
  142. package/dist/secure-file.js +4 -4
  143. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  144. package/dist/sidecar-lock-acquire.js +2 -1
  145. package/dist/sidecar-lock-policy.d.ts.map +1 -1
  146. package/dist/sidecar-lock-policy.js +3 -1
  147. package/dist/temp-cleanup.d.ts.map +1 -1
  148. package/dist/temp-cleanup.js +3 -2
  149. package/dist/timing.d.ts +1 -0
  150. package/dist/timing.d.ts.map +1 -1
  151. package/dist/timing.js +25 -6
  152. package/dist/trash.js +2 -2
  153. package/dist/walk.d.ts.map +1 -1
  154. package/dist/walk.js +6 -26
  155. package/dist/windows-owner.d.ts +9 -12
  156. package/dist/windows-owner.d.ts.map +1 -1
  157. package/dist/windows-owner.js +24 -58
  158. package/dist/write-file-handle.d.ts +6 -0
  159. package/dist/write-file-handle.d.ts.map +1 -0
  160. package/dist/write-file-handle.js +25 -0
  161. package/docs/advanced.md +18 -4
  162. package/docs/archive.md +22 -7
  163. package/docs/atomic.md +7 -0
  164. package/docs/contributing.md +7 -0
  165. package/docs/copy.md +86 -0
  166. package/docs/durability.md +17 -2
  167. package/docs/errors.md +1 -1
  168. package/docs/local-roots.md +8 -1
  169. package/docs/native.md +14 -1
  170. package/docs/permissions.md +20 -10
  171. package/docs/positional-read.md +63 -0
  172. package/docs/root.md +166 -4
  173. package/docs/security-model.md +14 -0
  174. package/docs/timing.md +2 -0
  175. package/docs/types.md +18 -11
  176. package/docs/walk.md +54 -5
  177. package/docs/writing.md +20 -2
  178. package/package.json +13 -8
@@ -54,9 +54,13 @@ import { requireNativeBinding } from "./native.js";
54
54
  import { syncFileBestEffortSync } from "./file-sync.js";
55
55
  import { randomUUID } from "node:crypto";
56
56
  import fs from "node:fs";
57
+ import path from "node:path";
57
58
  import { FsSafeError } from "./errors.js";
59
+ import { MutationAuthorityError } from "./mutation-authority.js";
58
60
  import { writeNativeInput } from "./native-operations.js";
61
+ import { assertNativeCopyCompleted, createNativeCopyFile } from "./copy-file-input.js";
59
62
  import { assertStagedDirectoryCurrent, openStagedDirectory } from "./staged-directory.js";
63
+ import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
60
64
  export function assertNativeStaging(binding) {
61
65
  if ([
62
66
  binding.createStagedFile,
@@ -93,24 +97,27 @@ class NativeStagedFile {
93
97
  #portableNames;
94
98
  #publishedMode;
95
99
  #sync;
100
+ #assertBeforeMutation;
96
101
  #name = `.fs-safe-${randomUUID()}.tmp`;
97
102
  #state = { status: "open", publication: NOT_PUBLISHED };
98
103
  #receipt;
99
- constructor(binding, parentFd, directory, portableNames, publishedMode, sync) {
104
+ #rejectFinalSymlink = false;
105
+ constructor(binding, parentFd, directory, portableNames, publishedMode, sync, assertBeforeMutation) {
100
106
  this.#binding = binding;
101
107
  this.#parentFd = parentFd;
102
108
  this.#directory = directory;
103
109
  this.#portableNames = portableNames;
104
110
  this.#publishedMode = publishedMode;
105
111
  this.#sync = sync;
112
+ this.#assertBeforeMutation = assertBeforeMutation;
106
113
  }
107
114
  // Takes ownership of parentFd, including all construction and preparation failures.
108
115
  static async create(binding, parentFd, directory, input, mode, maxBytes,
109
116
  // Public staging uses portable names; existing POSIX writes accept literal names.
110
- portableNames = true, sync = true) {
117
+ portableNames = true, sync = true, assertBeforeMutation) {
111
118
  let staged;
112
119
  try {
113
- staged = new NativeStagedFile(binding, parentFd, directory, portableNames, mode, sync);
120
+ staged = new NativeStagedFile(binding, parentFd, directory, portableNames, mode, sync, assertBeforeMutation);
114
121
  }
115
122
  catch (error) {
116
123
  try {
@@ -129,8 +136,11 @@ class NativeStagedFile {
129
136
  try {
130
137
  // This owner never escapes. Only the internal verifier borrows its fd;
131
138
  // public descriptor methods remain await-free and cannot race disposal.
132
- const staged = __addDisposableResource(env_1, await NativeStagedFile.create(binding, parentFd, directory, params.input, params.mode, params.maxBytes, false, params.sync), true);
133
- const published = await staged.publish(params.basename, { overwrite: params.overwrite !== false });
139
+ const staged = __addDisposableResource(env_1, await NativeStagedFile.create(binding, parentFd, directory, params.input, params.mode, params.maxBytes, false, params.sync, params.assertBeforeMutation), true);
140
+ staged.#rejectFinalSymlink = params.rejectFinalSymlink === true;
141
+ if (params.input.kind === "file")
142
+ await params.input.verifySource();
143
+ const published = await staged.publish(params.basename, { overwrite: params.overwrite !== false }, params.onPublished);
134
144
  const identity = published.staged.identity;
135
145
  await params.verifyPublished?.(staged.#file(), identity, parentGuard);
136
146
  return { dev: identity.dev, ino: identity.ino };
@@ -190,10 +200,22 @@ class NativeStagedFile {
190
200
  // The exclusive open performs no fallible post-open checks. Store its fd
191
201
  // before every subsequent operation, including the first metadata read.
192
202
  const state = this.#open();
193
- state.fileFd = this.#binding.createStagedFile(this.#parentFd, this.#name);
203
+ this.#assertBeforeMutation?.();
204
+ const copied = input.kind === "file"
205
+ ? await createNativeCopyFile(this.#binding, input, this.#parentFd, this.#name, maxBytes, false)
206
+ : undefined;
207
+ state.fileFd = copied?.fd;
208
+ if (state.fileFd === undefined) {
209
+ this.#assertBeforeMutation?.();
210
+ state.fileFd = this.#binding.createStagedFile(this.#parentFd, this.#name);
211
+ }
194
212
  const fd = state.fileFd;
213
+ if (input.kind === "file")
214
+ assertNativeCopyCompleted(input, copied);
215
+ this.#assertBeforeMutation?.();
195
216
  fs.fchmodSync(fd, 0o600);
196
- await writeNativeInput(fd, input, maxBytes);
217
+ if (!copied)
218
+ await writeNativeInput(fd, input, maxBytes, this.#assertBeforeMutation);
197
219
  if (this.#sync)
198
220
  syncFileBestEffortSync(fd);
199
221
  const stat = fs.fstatSync(fd, { bigint: true });
@@ -230,7 +252,7 @@ class NativeStagedFile {
230
252
  async assertCurrent() {
231
253
  this.#assertCurrent();
232
254
  }
233
- async publish(basename, options) {
255
+ async publish(basename, options, onPublished) {
234
256
  const overwrite = options?.overwrite;
235
257
  try {
236
258
  const state = this.#open();
@@ -239,6 +261,8 @@ class NativeStagedFile {
239
261
  throw new FsSafeError("invalid-path", "publication needs a distinct basename and explicit overwrite policy");
240
262
  }
241
263
  this.#assertCurrent();
264
+ assertFinalSymlinkRejected(path.join(this.#directory.realPath, basename), this.#rejectFinalSymlink);
265
+ this.#assertBeforeMutation?.();
242
266
  try {
243
267
  if (overwrite) {
244
268
  this.#binding.renameReplace(this.#parentFd, this.#name, this.#parentFd, basename);
@@ -262,6 +286,7 @@ class NativeStagedFile {
262
286
  overwrite,
263
287
  });
264
288
  state.publication = receipt;
289
+ onPublished?.(receipt.staged.identity);
265
290
  const stagedMode = this.#assertNamed(basename);
266
291
  assertStagedDirectoryCurrent(this.#directory);
267
292
  // Keep contents private until the published name passes its identity
@@ -282,6 +307,8 @@ class NativeStagedFile {
282
307
  return receipt;
283
308
  }
284
309
  catch (error) {
310
+ if (error instanceof MutationAuthorityError)
311
+ throw error;
285
312
  // Closure rejects further use, not the recorded outcome of an earlier publication.
286
313
  const publication = this.#state.status === "closed"
287
314
  ? this.#state.receipt.publication
@@ -1 +1 @@
1
- {"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../src/path.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAMzB,OAAO,EACL,4BAA4B,EAC5B,sBAAsB,EACtB,yBAAyB,EACzB,KAAK,yBAAyB,EAC9B,KAAK,2BAA2B,EAChC,KAAK,0BAA0B,GAChC,MAAM,kBAAkB,CAAC;AAM1B,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAWvE;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,cAAc,CAI1E;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtE;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,SAA6B,GAAG,IAAI,CAIjG;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE3D;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE1D;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CA4BlE;AAED,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAElE;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAG1D;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAExE;AAED,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,GAAG,IAAI,CAa/F;AAED,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,IAAI,CAAC,EAAE;IAAE,eAAe,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAChE,OAAO,CAUT;AAED,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,EAAE,CAAC,KAAK,GAAG,IAAI,CAMhE;AAED,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE,CA0BpE;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAOrF"}
1
+ {"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../src/path.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AAMzB,OAAO,EACL,4BAA4B,EAC5B,sBAAsB,EACtB,yBAAyB,EACzB,KAAK,yBAAyB,EAC9B,KAAK,2BAA2B,EAChC,KAAK,0BAA0B,GAChC,MAAM,kBAAkB,CAAC;AAM1B,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAWvE;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,cAAc,CAI1E;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAEtE;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,SAA6B,GAAG,IAAI,CAIjG;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE3D;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE1D;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CA8BlE;AAED,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAElE;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAG1D;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAExE;AAED,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,GAAG,IAAI,CAa/F;AAED,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,IAAI,CAAC,EAAE;IAAE,eAAe,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAChE,OAAO,CAUT;AAED,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,EAAE,CAAC,KAAK,GAAG,IAAI,CAMhE;AAED,wBAAgB,qBAAqB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,EAAE,CA0BpE;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAOrF"}
package/dist/path.js CHANGED
@@ -49,7 +49,9 @@ export function isPathInside(root, target) {
49
49
  target.charCodeAt(0) === POSIX_SEPARATOR_CHAR_CODE &&
50
50
  !target.includes("/..") &&
51
51
  (target === root ||
52
- (target.startsWith(root) && target.charCodeAt(root.length) === POSIX_SEPARATOR_CHAR_CODE))) {
52
+ (target.startsWith(root) &&
53
+ (root.charCodeAt(root.length - 1) === POSIX_SEPARATOR_CHAR_CODE ||
54
+ target.charCodeAt(root.length) === POSIX_SEPARATOR_CHAR_CODE)))) {
53
55
  return true;
54
56
  }
55
57
  const resolvedRoot = path.resolve(root);
@@ -1 +1 @@
1
- {"version":3,"file":"permissions-windows.d.ts","sourceRoot":"","sources":["../src/permissions-windows.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"permissions-windows.d.ts","sourceRoot":"","sources":["../src/permissions-windows.ts"],"names":[],"mappings":"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"}
@@ -1,9 +1,9 @@
1
1
  import os from "node:os";
2
2
  import { getNativeBinding } from "./native.js";
3
- import { executePermissionCommand, formatPermissionErrorDetail, getPermissionCommandFailure, } from "./permission-exec.js";
3
+ import { executePermissionCommand } from "./permission-exec.js";
4
4
  import { normalizeLowercaseStringOrEmpty } from "./string-coerce.js";
5
5
  import { resolveWindowsSystemCommand } from "./windows-command.js";
6
- import { inspectWindowsOwner, resolveWindowsCurrentUserSid, resolveWindowsPrincipalSids, } from "./windows-owner.js";
6
+ import { inspectWindowsOwner } from "./windows-owner.js";
7
7
  const INHERIT_FLAGS = new Set(["I", "OI", "CI", "IO", "NP"]);
8
8
  const WORLD_PRINCIPALS = new Set(["everyone", "users", "builtin\\users", "authenticated users", "nt authority\\authenticated users", "anonymous logon", "nt authority\\anonymous logon", "guests", "builtin\\guests", "interactive", "nt authority\\interactive", "network", "nt authority\\network", "local"]);
9
9
  const TRUSTED_BASE = new Set([
@@ -92,13 +92,7 @@ export async function inspectWindowsPermissions(params) {
92
92
  const error = `Windows owner inspection failed: ${owner.error}`;
93
93
  return { ...unverified, ownerError: owner.error, error, errorDetail: owner.errorDetail, errorCause: owner.errorCause };
94
94
  }
95
- const acl = await inspectWindowsAcl(params.targetPath, {
96
- env: params.opts?.env,
97
- exec: params.opts?.exec,
98
- currentUserSid: owner.currentUserSid,
99
- principalSids: owner.principalSids,
100
- principalTranslationFailed: owner.principalTranslationFailed,
101
- });
95
+ const acl = summarizeWindowsOwnerAcl(owner);
102
96
  const ownerFields = {
103
97
  ...(owner.sid ? { ownerSid: owner.sid } : {}),
104
98
  ...(owner.trusted !== undefined ? { ownerTrusted: owner.trusted } : {}),
@@ -230,49 +224,45 @@ export function summarizeWindowsAcl(entries, env) {
230
224
  }
231
225
  return { trusted, untrustedWorld, untrustedGroup };
232
226
  }
233
- export async function inspectWindowsAcl(targetPath, opts) {
234
- let command = "";
235
- let startedAt = performance.now();
236
- const exec = async (file, args) => {
237
- command = file;
238
- startedAt = performance.now();
239
- return await (opts?.exec ?? defaultPermissionExec)(file, args);
240
- };
241
- try {
242
- if (opts?.principalTranslationFailed)
243
- throw new Error("Windows ACL principal SID translation failed");
244
- const { stdout, stderr } = await exec(resolveWindowsSystemCommand("icacls.exe", opts?.env), [targetPath]);
245
- let entries = parseIcaclsOutput(`${stdout}\n${stderr}`.trim(), targetPath);
246
- if (!entries.length)
247
- throw new Error("Windows ACL output could not be verified");
248
- const unresolvedPrincipals = entries.filter((entry) => !entry.sid).map((entry) => entry.principal);
249
- const principalSids = await resolveWindowsPrincipalSids({ principals: unresolvedPrincipals, known: opts?.principalSids, env: opts?.env, exec });
250
- entries = entries.map((entry) => {
251
- const sid = entry.sid ?? principalSids[entry.principal.toLowerCase()];
252
- if (!sid)
253
- throw new Error(`Windows ACL principal SID could not be verified: ${entry.principal}`);
254
- return { ...entry, sid };
255
- });
256
- let currentUserSid = normalizeSid(opts?.currentUserSid ?? "");
257
- let effectiveEnv = currentUserSid ? { USERSID: currentUserSid } : undefined;
258
- let { trusted, untrustedWorld, untrustedGroup } = summarizeWindowsAcl(entries, effectiveEnv);
259
- if (!currentUserSid && untrustedGroup.some((entry) => entry.sid && !TRUSTED_SIDS.has(entry.sid))) {
260
- currentUserSid = (await resolveWindowsCurrentUserSid({ exec, env: opts?.env })) ?? "";
261
- if (currentUserSid) {
262
- effectiveEnv = { USERSID: currentUserSid };
263
- ({ trusted, untrustedWorld, untrustedGroup } = summarizeWindowsAcl(entries, effectiveEnv));
264
- }
265
- }
266
- return { ok: true, entries, trusted, untrustedWorld, untrustedGroup };
267
- }
268
- catch (err) {
227
+ const MASK_RIGHTS = [
228
+ [0x1000_0000, "GA"], [0x8000_0000, "GR"], [0x4000_0000, "GW"], [0x2000_0000, "GE"],
229
+ [0x0001_0000, "D"], [0x0002_0000, "RC"], [0x0004_0000, "WDAC"], [0x0008_0000, "WO"],
230
+ [0x0010_0000, "S"], [0x0001, "RD"], [0x0002, "WD"], [0x0004, "AD"],
231
+ [0x0008, "REA"], [0x0010, "WEA"], [0x0020, "X"], [0x0040, "DC"], [0x0080, "RA"], [0x0100, "WA"],
232
+ ];
233
+ function summarizeWindowsOwnerAcl(owner) {
234
+ const error = owner.error ?? owner.aclError;
235
+ if (error || owner.daclPresent === undefined || !owner.aces || !owner.currentUserSid) {
269
236
  return {
270
237
  ok: false, entries: [], trusted: [], untrustedWorld: [], untrustedGroup: [],
271
- error: formatPermissionErrorDetail(String(err)),
272
- errorDetail: getPermissionCommandFailure(err, command, performance.now() - startedAt),
273
- errorCause: err,
238
+ error: error ?? "Windows ACL query returned incomplete descriptor data",
239
+ errorDetail: owner.errorDetail, errorCause: owner.errorCause,
274
240
  };
275
241
  }
242
+ // A null DACL grants everyone full access; an empty DACL grants nothing.
243
+ const grants = owner.daclPresent ? owner.aces : [{ sid: "s-1-1-0", mask: 0x001f_01ff, deny: false, inheritOnly: false }];
244
+ const entries = grants.filter(ace => !ace.deny && !ace.inheritOnly).map(ace => {
245
+ const rights = ace.mask === 0x001f_01ff ? ["F"] : MASK_RIGHTS.filter(([mask]) => (ace.mask & mask) !== 0).map(([, name]) => name);
246
+ return {
247
+ principal: ace.sid, sid: ace.sid, rights, rawRights: "(" + rights.join(",") + ")",
248
+ // Retain the coarse policy's conservative treatment of read-control and
249
+ // mutation rights. Denies do not subtract grants or claim effective access.
250
+ canRead: (ace.mask & 0x9002_0089) !== 0,
251
+ canWrite: (ace.mask & 0x500d_0156) !== 0,
252
+ };
253
+ });
254
+ return { ok: true, entries, ...summarizeWindowsAcl(entries, { USERSID: owner.currentUserSid }) };
255
+ }
256
+ export async function inspectWindowsAcl(targetPath, opts) {
257
+ if (opts?.principalTranslationFailed) {
258
+ const error = new Error("Windows ACL principal SID translation failed");
259
+ return summarizeWindowsOwnerAcl({ error: String(error), errorCause: error });
260
+ }
261
+ const owner = await inspectWindowsOwner({ targetPath, env: opts?.env, exec: opts?.exec ?? defaultPermissionExec });
262
+ return summarizeWindowsOwnerAcl({
263
+ ...owner,
264
+ currentUserSid: normalizeSid(opts?.currentUserSid ?? "") || owner.currentUserSid,
265
+ });
276
266
  }
277
267
  export function formatWindowsAclSummary(summary) {
278
268
  if (!summary.ok)
@@ -27,7 +27,7 @@ function validatePinnedRelativePath(relativePath) {
27
27
  relativePath.startsWith("..\\")) {
28
28
  throw new FsSafeError("invalid-path", "relative path must not escape root");
29
29
  }
30
- for (const segment of relativePath.split("/")) {
30
+ for (const segment of relativePath.split(process.platform === "win32" ? /[\\/]/ : "/")) {
31
31
  if (segment === "..") {
32
32
  throw new FsSafeError("invalid-path", "relative path must not contain '..'");
33
33
  }
@@ -1,6 +1,7 @@
1
1
  import type { Readable } from "node:stream";
2
2
  import { type AnyAsyncDirectoryGuard } from "./directory-guard.js";
3
3
  import type { FileIdentityStat } from "./file-identity.js";
4
+ import { type CopyFileInput } from "./copy-file-input.js";
4
5
  export type PinnedWriteInput = {
5
6
  kind: "buffer";
6
7
  data: string | Buffer;
@@ -8,7 +9,7 @@ export type PinnedWriteInput = {
8
9
  } | {
9
10
  kind: "stream";
10
11
  stream: Readable;
11
- };
12
+ } | CopyFileInput;
12
13
  export type RenameIdentityPolicy = "strict" | "verify-content-with-lock";
13
14
  export type PublishedWriteIdentity = Readonly<{
14
15
  dev: bigint;
@@ -22,10 +23,13 @@ export type PinnedWriteParams = {
22
23
  mode: number;
23
24
  sync?: boolean;
24
25
  overwrite?: boolean;
26
+ assertBeforeMutation?: () => void;
27
+ rejectFinalSymlink?: boolean;
25
28
  maxBytes?: number;
26
29
  input: PinnedWriteInput;
27
30
  rootIdentity?: FileIdentityStat;
28
31
  onRenameIdentityMismatch?: "verify-content";
32
+ onPublished?: (identity: PublishedWriteIdentity) => void;
29
33
  verifyPublished?: (fd: number, identity: PublishedWriteIdentity, parentGuard: AnyAsyncDirectoryGuard) => Promise<void>;
30
34
  };
31
35
  export declare function runPinnedWriteHelper(params: PinnedWriteParams): Promise<FileIdentityStat>;
@@ -1 +1 @@
1
- {"version":3,"file":"pinned-write.d.ts","sourceRoot":"","sources":["../src/pinned-write.ts"],"names":[],"mappings":"AAMA,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;AAsDzC,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,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,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,gBAAgB,CAAC;IAE5C,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
+ {"version":3,"file":"pinned-write.d.ts","sourceRoot":"","sources":["../src/pinned-write.ts"],"names":[],"mappings":"AAMA,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;AAa3D,OAAO,EAAE,KAAK,aAAa,EAAqB,MAAM,sBAAsB,CAAC;AAG7E,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,GACpC,aAAa,CAAC;AA4ClB,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,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,oBAAoB,CAAC,EAAE,MAAM,IAAI,CAAC;IAClC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,gBAAgB,CAAC;IACxB,YAAY,CAAC,EAAE,gBAAgB,CAAC;IAChC,wBAAwB,CAAC,EAAE,gBAAgB,CAAC;IAC5C,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,sBAAsB,KAAK,IAAI,CAAC;IAEzD,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"}
@@ -17,6 +17,10 @@ import { resolveReadOpenFlags } from "./read-open-flags.js";
17
17
  import { cleanupPinnedFilePath } from "./replace-file-temp-owner.js";
18
18
  import { withSidecarLock } from "./sidecar-lock.js";
19
19
  import { getFsSafeTestHooks } from "./test-hooks.js";
20
+ import { writeAllToFile } from "./write-file-handle.js";
21
+ import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
22
+ import { writeCopyFileToFd } from "./copy-file-input.js";
23
+ import { publishCopyStage } from "./publish-copy-stage.js";
20
24
  function byteLength(input, encoding) {
21
25
  return typeof input === "string"
22
26
  ? Buffer.byteLength(input, encoding ?? "utf8")
@@ -36,20 +40,13 @@ function assertWithinMaxBytes(bytes, maxBytes) {
36
40
  throw new FsSafeError("too-large", `file exceeds limit of ${maxBytes} bytes (got at least ${bytes})`);
37
41
  }
38
42
  }
39
- async function writeStreamToHandle(stream, handle, maxBytes) {
43
+ async function writeStreamToHandle(stream, handle, maxBytes, assertBeforeMutation) {
40
44
  let bytes = 0;
41
45
  for await (const chunk of stream) {
42
46
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
43
47
  bytes += buffer.byteLength;
44
48
  assertWithinMaxBytes(bytes, maxBytes);
45
- let offset = 0;
46
- while (offset < buffer.byteLength) {
47
- const { bytesWritten } = await handle.write(buffer, offset, buffer.byteLength - offset);
48
- if (bytesWritten <= 0) {
49
- throw new FsSafeError("helper-failed", "fallback stream write made no progress");
50
- }
51
- offset += bytesWritten;
52
- }
49
+ await writeAllToFile(handle, buffer, { assertBeforeMutation });
53
50
  }
54
51
  }
55
52
  export async function runPinnedWriteHelper(params) {
@@ -95,7 +92,6 @@ async function runPinnedWriteFallback(params) {
95
92
  ? { dev: params.rootIdentity.dev, ino: params.rootIdentity.ino } : undefined;
96
93
  if (exactRoot)
97
94
  await inspectDirectoryIdentity(params.rootPath, exactRoot);
98
- const guardOptions = { bigint: exactRoot !== undefined };
99
95
  let parentPath = params.relativeParentPath
100
96
  ? path.join(params.rootPath, ...params.relativeParentPath.split("/"))
101
97
  : params.rootPath;
@@ -107,15 +103,23 @@ async function runPinnedWriteFallback(params) {
107
103
  parentPath = await mkdirPathComponentsWithGuards({
108
104
  rootReal: params.rootPath,
109
105
  targetPath: parentPath,
106
+ assertBeforeMutation: params.assertBeforeMutation,
110
107
  beforeComponent: async (componentPath) => await getFsSafeTestHooks()?.beforeRootFallbackMutation?.("mkdir", componentPath),
111
108
  });
112
109
  }
113
110
  const parentGuard = params.mkdir
114
- ? await createAsyncDirectoryGuard(parentPath, guardOptions)
115
- : await createNearestExistingDirectoryGuard(params.rootPath, parentPath, guardOptions);
111
+ ? await createAsyncDirectoryGuard(parentPath, { bigint: true })
112
+ : await createNearestExistingDirectoryGuard(params.rootPath, parentPath, { bigint: true });
116
113
  const targetPath = path.join(parentPath, params.basename);
117
- if (params.overwrite === false) {
118
- const handle = await withAsyncDirectoryGuards([parentGuard], async () => await fs.open(targetPath, fsSync.constants.O_WRONLY | fsSync.constants.O_CREAT | fsSync.constants.O_EXCL, params.mode), {
114
+ if (params.overwrite === false && params.input.kind !== "file") {
115
+ const assertBeforeMutation = () => {
116
+ assertFinalSymlinkRejected(targetPath, params.rejectFinalSymlink);
117
+ params.assertBeforeMutation?.();
118
+ };
119
+ const handle = await withAsyncDirectoryGuards([parentGuard], async () => {
120
+ assertBeforeMutation();
121
+ return await fs.open(targetPath, fsSync.constants.O_WRONLY | fsSync.constants.O_CREAT | fsSync.constants.O_EXCL, params.mode);
122
+ }, {
119
123
  onPostGuardFailure: async (openedHandle) => {
120
124
  // The parent failed verification, so targetPath may now resolve
121
125
  // somewhere else. Close the fd, but do not clean up by path.
@@ -129,15 +133,12 @@ async function runPinnedWriteFallback(params) {
129
133
  createdIdentity = verificationIdentity;
130
134
  if (params.input.kind === "buffer") {
131
135
  assertWithinMaxBytes(byteLength(params.input.data, params.input.encoding), params.maxBytes);
132
- if (typeof params.input.data === "string") {
133
- await handle.writeFile(params.input.data, params.input.encoding ?? "utf8");
134
- }
135
- else {
136
- await handle.writeFile(params.input.data);
137
- }
136
+ await writeAllToFile(handle, params.input.data, {
137
+ encoding: params.input.encoding, assertBeforeMutation,
138
+ });
138
139
  }
139
140
  else {
140
- await writeStreamToHandle(params.input.stream, handle, params.maxBytes);
141
+ await writeStreamToHandle(params.input.stream, handle, params.maxBytes, assertBeforeMutation);
141
142
  }
142
143
  // Content writes may clear set-ID bits; finalize them through the owned fd.
143
144
  await handle.chmod(params.mode);
@@ -176,20 +177,21 @@ async function runPinnedWriteFallback(params) {
176
177
  let readHandle;
177
178
  let renamed = false;
178
179
  try {
180
+ params.assertBeforeMutation?.();
179
181
  handle = await fs.open(tempPath, tempFlags, params.mode);
180
182
  let verificationIdentity = fsSync.fstatSync(handle.fd, { bigint: true });
181
183
  tempIdentity = verificationIdentity;
182
184
  if (params.input.kind === "buffer") {
183
185
  assertWithinMaxBytes(byteLength(params.input.data, params.input.encoding), params.maxBytes);
184
- if (typeof params.input.data === "string") {
185
- await handle.writeFile(params.input.data, params.input.encoding ?? "utf8");
186
- }
187
- else {
188
- await handle.writeFile(params.input.data);
189
- }
186
+ await writeAllToFile(handle, params.input.data, {
187
+ encoding: params.input.encoding, assertBeforeMutation: params.assertBeforeMutation,
188
+ });
189
+ }
190
+ else if (params.input.kind === "file") {
191
+ await writeCopyFileToFd(handle.fd, params.input, params.maxBytes, params.assertBeforeMutation);
190
192
  }
191
193
  else {
192
- await writeStreamToHandle(params.input.stream, handle, params.maxBytes);
194
+ await writeStreamToHandle(params.input.stream, handle, params.maxBytes, params.assertBeforeMutation);
193
195
  }
194
196
  tempStat = fsSync.fstatSync(handle.fd);
195
197
  const tempPathStat = fsSync.lstatSync(tempPath);
@@ -200,10 +202,28 @@ async function runPinnedWriteFallback(params) {
200
202
  await handle.chmod(params.mode);
201
203
  if (params.sync !== false)
202
204
  await syncFileBestEffort(handle);
205
+ if (params.input.kind === "file")
206
+ await params.input.verifySource();
203
207
  let verifiedIdentity = expectedTempStat;
204
208
  await withAsyncDirectoryGuards([parentGuard], async () => {
205
- await fs.rename(tempPath, targetPath);
206
- renamed = true;
209
+ assertFinalSymlinkRejected(targetPath, params.rejectFinalSymlink);
210
+ if (params.overwrite === false) {
211
+ publishCopyStage({
212
+ temporaryPath: tempPath, targetPath, fd: handle.fd,
213
+ identity: tempIdentity, parentGuard,
214
+ assertBeforeMutation: params.assertBeforeMutation,
215
+ onPublished: (identity) => {
216
+ renamed = true;
217
+ params.onPublished?.(identity);
218
+ },
219
+ });
220
+ }
221
+ else {
222
+ params.assertBeforeMutation?.();
223
+ await fs.rename(tempPath, targetPath);
224
+ renamed = true;
225
+ params.onPublished?.(verificationIdentity);
226
+ }
207
227
  await getFsSafeTestHooks()?.afterPinnedWriteFallbackRename?.(targetPath);
208
228
  if (params.sync !== false)
209
229
  await syncDirectoryBestEffort(parentPath);
@@ -0,0 +1,9 @@
1
+ import type { FileHandle } from "node:fs/promises";
2
+ export type ReadFileWindowOptions = {
3
+ signal?: AbortSignal;
4
+ };
5
+ /** Fills a caller-owned buffer from an explicit position, stopping at EOF. */
6
+ export declare function readFileWindowFully(handle: FileHandle, buffer: Buffer, position: number, { signal }?: ReadFileWindowOptions): Promise<number>;
7
+ /** Synchronous positional read; neither variant advances or closes its descriptor. */
8
+ export declare function readFileWindowFullySync(fd: number, buffer: Buffer, position: number): number;
9
+ //# sourceMappingURL=positional-read.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"positional-read.d.ts","sourceRoot":"","sources":["../src/positional-read.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAKnD,MAAM,MAAM,qBAAqB,GAAG;IAClC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,CAAC;AASF,8EAA8E;AAC9E,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,UAAU,EAClB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,EAAE,MAAM,EAAE,GAAE,qBAA0B,GACrC,OAAO,CAAC,MAAM,CAAC,CAcjB;AAED,sFAAsF;AACtF,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAW5F"}
@@ -0,0 +1,36 @@
1
+ import fs from "node:fs";
2
+ // Node's synchronous read length is a signed 32-bit value on supported runtimes.
3
+ const MAX_READ_BYTES = 0x7fffffff;
4
+ function validateWindow(buffer, position) {
5
+ if (!Number.isSafeInteger(position) || position < 0 ||
6
+ !Number.isSafeInteger(position + buffer.length)) {
7
+ throw new RangeError("read position and window end must be non-negative safe integers");
8
+ }
9
+ }
10
+ /** Fills a caller-owned buffer from an explicit position, stopping at EOF. */
11
+ export async function readFileWindowFully(handle, buffer, position, { signal } = {}) {
12
+ validateWindow(buffer, position);
13
+ signal?.throwIfAborted();
14
+ let total = 0;
15
+ while (total < buffer.length) {
16
+ const { bytesRead } = await handle.read(buffer, total, Math.min(buffer.length - total, MAX_READ_BYTES), position + total);
17
+ // Settle the pending read before cancellation lets the caller reuse its buffer or handle.
18
+ signal?.throwIfAborted();
19
+ if (bytesRead === 0)
20
+ break;
21
+ total += bytesRead;
22
+ }
23
+ return total;
24
+ }
25
+ /** Synchronous positional read; neither variant advances or closes its descriptor. */
26
+ export function readFileWindowFullySync(fd, buffer, position) {
27
+ validateWindow(buffer, position);
28
+ let total = 0;
29
+ while (total < buffer.length) {
30
+ const bytesRead = fs.readSync(fd, buffer, total, Math.min(buffer.length - total, MAX_READ_BYTES), position + total);
31
+ if (bytesRead === 0)
32
+ break;
33
+ total += bytesRead;
34
+ }
35
+ return total;
36
+ }
@@ -0,0 +1,13 @@
1
+ import { type BigIntStats } from "node:fs";
2
+ import type { AnyAsyncDirectoryGuard } from "./directory-guard.js";
3
+ import type { PublishedWriteIdentity } from "./pinned-write.js";
4
+ export declare function publishCopyStage(params: {
5
+ temporaryPath: string;
6
+ targetPath: string;
7
+ fd: number;
8
+ identity: BigIntStats;
9
+ parentGuard: AnyAsyncDirectoryGuard;
10
+ assertBeforeMutation?: () => void;
11
+ onPublished?: (identity: PublishedWriteIdentity) => void;
12
+ }): void;
13
+ //# sourceMappingURL=publish-copy-stage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publish-copy-stage.d.ts","sourceRoot":"","sources":["../src/publish-copy-stage.ts"],"names":[],"mappings":"AAAA,OAAW,EAAE,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAE/C,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAGnE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAIhE,wBAAgB,gBAAgB,CAAC,MAAM,EAAE;IACvC,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,WAAW,CAAC;IACtB,WAAW,EAAE,sBAAsB,CAAC;IACpC,oBAAoB,CAAC,EAAE,MAAM,IAAI,CAAC;IAClC,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,sBAAsB,KAAK,IAAI,CAAC;CAC1D,GAAG,IAAI,CAqCP"}
@@ -0,0 +1,47 @@
1
+ import fs, {} from "node:fs";
2
+ import path from "node:path";
3
+ import { FsSafeError } from "./errors.js";
4
+ import { sameFileIdentityForCleanup } from "./file-identity.js";
5
+ // Node has no portable no-replace rename. Link only our completed private stage,
6
+ // then unlink it in the same JS turn so local readers never observe two names.
7
+ export function publishCopyStage(params) {
8
+ const { identity, temporaryPath, targetPath } = params;
9
+ const staged = fs.lstatSync(temporaryPath, { bigint: true });
10
+ const opened = fs.fstatSync(params.fd, { bigint: true });
11
+ if (!staged.isFile() || staged.isSymbolicLink() || staged.nlink !== 1n ||
12
+ !sameFileIdentityForCleanup(staged, identity) ||
13
+ !sameFileIdentityForCleanup(opened, identity)) {
14
+ throw new FsSafeError("path-mismatch", "exclusive copy stage changed before publication");
15
+ }
16
+ params.assertBeforeMutation?.();
17
+ fs.linkSync(temporaryPath, targetPath);
18
+ let observerRejected = false;
19
+ let observerError;
20
+ try {
21
+ params.onPublished?.(identity);
22
+ }
23
+ catch (error) {
24
+ observerRejected = true;
25
+ observerError = error;
26
+ }
27
+ try {
28
+ const parent = fs.lstatSync(path.dirname(temporaryPath), { bigint: true });
29
+ const current = fs.lstatSync(temporaryPath, { bigint: true });
30
+ if (parent.isSymbolicLink() || !parent.isDirectory() ||
31
+ !sameFileIdentityForCleanup(parent, params.parentGuard.stat) ||
32
+ current.isSymbolicLink() || !current.isFile() ||
33
+ !sameFileIdentityForCleanup(current, identity)) {
34
+ throw new FsSafeError("path-mismatch", "published copy temporary name changed before cleanup");
35
+ }
36
+ // This removes the stage's name, never the published destination or source.
37
+ fs.unlinkSync(temporaryPath);
38
+ }
39
+ catch (error) {
40
+ throw new FsSafeError("helper-failed", "published copy temporary cleanup failed", {
41
+ cause: observerRejected ? new AggregateError([observerError, error], "observer and cleanup failed") : error,
42
+ details: { publication: "published", path: targetPath, dev: identity.dev, ino: identity.ino, cleanup: "failed" },
43
+ });
44
+ }
45
+ if (observerRejected)
46
+ throw observerError;
47
+ }
@@ -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;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"}
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;AA2ND,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,sBAAsB,CAAC,EAAE,gBAAgB,CAAC;IAC1C,QAAQ,EAAE,4BAA4B,CAAC;IACvC,aAAa,CAAC,EAAE,qCAAqC,CAAC;IACtD,aAAa,CAAC,EAAE,gBAAgB,CAAC;CAClC,GAAG,OAAO,CAAC,0BAA0B,CAAC,CA4MtC"}
@@ -90,9 +90,9 @@ async function copyPinnedSource(params) {
90
90
  throw error;
91
91
  }
92
92
  let target;
93
- let createdIdentity = fsSync.fstatSync(nativeFd, { bigint: true });
94
- rememberCreatedTarget(params.failure, createdIdentity, "copy-verify");
95
93
  try {
94
+ let createdIdentity = fsSync.fstatSync(nativeFd, { bigint: true });
95
+ rememberCreatedTarget(params.failure, createdIdentity, "copy-verify");
96
96
  fsSync.fchmodSync(nativeFd, 0o600);
97
97
  createdIdentity = fsSync.fstatSync(nativeFd, { bigint: true });
98
98
  await getFsSafeTestHooks()?.afterPublishTargetCreated?.("exclusive-copy", params.targetPath, createdIdentity);
@@ -106,8 +106,9 @@ async function copyPinnedSource(params) {
106
106
  throw new FsSafeError("path-mismatch", "native publication target changed after copy");
107
107
  }
108
108
  const hashed = await hashFileHandle(target, params.native);
109
- fsSync.closeSync(nativeFd);
109
+ const completedFd = nativeFd;
110
110
  nativeFd = undefined;
111
+ fsSync.closeSync(completedFd);
111
112
  return {
112
113
  handle: target,
113
114
  exactIdentity: opened,
@@ -126,9 +127,9 @@ async function copyPinnedSource(params) {
126
127
  }
127
128
  }
128
129
  const target = await fs.open(params.targetPath, "wx+", 0o600);
129
- const createdIdentity = fsSync.fstatSync(target.fd, { bigint: true });
130
- rememberCreatedTarget(params.failure, createdIdentity, "copy-verify");
131
130
  try {
131
+ const createdIdentity = fsSync.fstatSync(target.fd, { bigint: true });
132
+ rememberCreatedTarget(params.failure, createdIdentity, "copy-verify");
132
133
  await target.chmod(0o600);
133
134
  const exactIdentity = fsSync.fstatSync(target.fd, { bigint: true });
134
135
  await getFsSafeTestHooks()?.afterPublishTargetCreated?.("exclusive-copy", params.targetPath, exactIdentity);