@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.
- package/CHANGELOG.md +78 -0
- package/README.md +17 -0
- package/dist/advanced.d.ts +1 -0
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +1 -0
- package/dist/archive-crc32.d.ts.map +1 -1
- package/dist/archive-crc32.js +6 -1
- package/dist/archive-deadline.d.ts.map +1 -1
- package/dist/archive-deadline.js +3 -4
- package/dist/archive-gzip-tail.d.ts +2 -0
- package/dist/archive-gzip-tail.d.ts.map +1 -1
- package/dist/archive-gzip-tail.js +20 -3
- package/dist/archive-merge.js +1 -1
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +3 -2
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +67 -66
- package/dist/archive-tar-stream.d.ts +11 -4
- package/dist/archive-tar-stream.d.ts.map +1 -1
- package/dist/archive-tar-stream.js +20 -8
- package/dist/archive-tar-wasm.d.ts.map +1 -1
- package/dist/archive-tar-wasm.js +2 -1
- package/dist/bounded-read.d.ts +7 -0
- package/dist/bounded-read.d.ts.map +1 -1
- package/dist/bounded-read.js +73 -43
- package/dist/clone-metadata.d.ts +19 -0
- package/dist/clone-metadata.d.ts.map +1 -0
- package/dist/clone-metadata.js +32 -0
- package/dist/copy-file-input.d.ts +23 -0
- package/dist/copy-file-input.d.ts.map +1 -0
- package/dist/copy-file-input.js +91 -0
- package/dist/copy-policy.d.ts +3 -0
- package/dist/copy-policy.d.ts.map +1 -0
- package/dist/copy-policy.js +8 -0
- package/dist/copy-publication.d.ts +10 -1
- package/dist/copy-publication.d.ts.map +1 -1
- package/dist/copy-publication.js +27 -0
- package/dist/copy-tree-portable.d.ts +9 -0
- package/dist/copy-tree-portable.d.ts.map +1 -0
- package/dist/copy-tree-portable.js +191 -0
- package/dist/copy.d.ts +16 -0
- package/dist/copy.d.ts.map +1 -0
- package/dist/copy.js +123 -0
- package/dist/durability.d.ts +1 -1
- package/dist/durability.d.ts.map +1 -1
- package/dist/error-detail.d.ts.map +1 -1
- package/dist/error-detail.js +4 -1
- package/dist/file-hash.d.ts +6 -2
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +44 -8
- package/dist/filename.d.ts.map +1 -1
- package/dist/filename.js +2 -12
- package/dist/guarded-mkdir.d.ts +1 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +1 -0
- package/dist/guarded-mutation.d.ts +2 -0
- package/dist/guarded-mutation.d.ts.map +1 -1
- package/dist/guarded-mutation.js +8 -4
- package/dist/home-dir.d.ts.map +1 -1
- package/dist/home-dir.js +9 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/install-path.d.ts.map +1 -1
- package/dist/install-path.js +2 -6
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +13 -14
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +22 -24
- package/dist/move-path-stage.d.ts +8 -0
- package/dist/move-path-stage.d.ts.map +1 -0
- package/dist/move-path-stage.js +55 -0
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +26 -31
- package/dist/mutation-authority.d.ts +8 -0
- package/dist/mutation-authority.d.ts.map +1 -0
- package/dist/mutation-authority.js +36 -0
- package/dist/native-binding.d.ts +24 -1
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-operations.d.ts +1 -1
- package/dist/native-operations.d.ts.map +1 -1
- package/dist/native-operations.js +11 -5
- package/dist/native-pinned-write-windows.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +18 -7
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +1 -0
- package/dist/native-staged-file.d.ts +3 -3
- package/dist/native-staged-file.d.ts.map +1 -1
- package/dist/native-staged-file.js +35 -8
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +3 -1
- package/dist/permissions-windows.d.ts.map +1 -1
- package/dist/permissions-windows.js +38 -48
- package/dist/pinned-operation.js +1 -1
- package/dist/pinned-write.d.ts +5 -1
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +50 -30
- package/dist/positional-read.d.ts +9 -0
- package/dist/positional-read.d.ts.map +1 -0
- package/dist/positional-read.js +36 -0
- package/dist/publish-copy-stage.d.ts +13 -0
- package/dist/publish-copy-stage.d.ts.map +1 -0
- package/dist/publish-copy-stage.js +47 -0
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +6 -5
- package/dist/replace-file-temp-owner.d.ts.map +1 -1
- package/dist/replace-file-temp-owner.js +6 -2
- package/dist/root-context.d.ts +3 -0
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +51 -26
- package/dist/root-directory-list.d.ts +24 -0
- package/dist/root-directory-list.d.ts.map +1 -0
- package/dist/root-directory-list.js +201 -0
- package/dist/root-errors.d.ts +1 -0
- package/dist/root-errors.d.ts.map +1 -1
- package/dist/root-errors.js +3 -0
- package/dist/root-file.d.ts +2 -0
- package/dist/root-file.d.ts.map +1 -1
- package/dist/root-file.js +12 -4
- package/dist/root-impl.d.ts +6 -50
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +330 -210
- package/dist/root-options.d.ts +66 -0
- package/dist/root-options.d.ts.map +1 -0
- package/dist/root-options.js +18 -0
- package/dist/root-path-existing.d.ts +5 -0
- package/dist/root-path-existing.d.ts.map +1 -1
- package/dist/root-path-existing.js +56 -1
- package/dist/root-path.d.ts +1 -0
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +74 -162
- package/dist/root-symlink-policy.d.ts +13 -0
- package/dist/root-symlink-policy.d.ts.map +1 -0
- package/dist/root-symlink-policy.js +34 -0
- package/dist/root-walk.d.ts +5 -4
- package/dist/root-walk.d.ts.map +1 -1
- package/dist/root-walk.js +99 -50
- package/dist/root-write-mode.d.ts.map +1 -1
- package/dist/root-write-mode.js +1 -4
- package/dist/root.d.ts +3 -1
- package/dist/root.d.ts.map +1 -1
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +4 -4
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +2 -1
- package/dist/sidecar-lock-policy.d.ts.map +1 -1
- package/dist/sidecar-lock-policy.js +3 -1
- package/dist/temp-cleanup.d.ts.map +1 -1
- package/dist/temp-cleanup.js +3 -2
- package/dist/timing.d.ts +1 -0
- package/dist/timing.d.ts.map +1 -1
- package/dist/timing.js +25 -6
- package/dist/trash.js +2 -2
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +6 -26
- package/dist/windows-owner.d.ts +9 -12
- package/dist/windows-owner.d.ts.map +1 -1
- package/dist/windows-owner.js +24 -58
- package/dist/write-file-handle.d.ts +6 -0
- package/dist/write-file-handle.d.ts.map +1 -0
- package/dist/write-file-handle.js +25 -0
- package/docs/advanced.md +18 -4
- package/docs/archive.md +22 -7
- package/docs/atomic.md +7 -0
- package/docs/contributing.md +7 -0
- package/docs/copy.md +86 -0
- package/docs/durability.md +17 -2
- package/docs/errors.md +1 -1
- package/docs/local-roots.md +8 -1
- package/docs/native.md +14 -1
- package/docs/permissions.md +20 -10
- package/docs/positional-read.md +63 -0
- package/docs/root.md +166 -4
- package/docs/security-model.md +14 -0
- package/docs/timing.md +2 -0
- package/docs/types.md +18 -11
- package/docs/walk.md +54 -5
- package/docs/writing.md +20 -2
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/dist/path.d.ts.map
CHANGED
|
@@ -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,
|
|
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) &&
|
|
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":"AAEA,OAAO,
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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:
|
|
272
|
-
errorDetail:
|
|
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)
|
package/dist/pinned-operation.js
CHANGED
|
@@ -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
|
}
|
package/dist/pinned-write.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/pinned-write.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
115
|
-
: await createNearestExistingDirectoryGuard(params.rootPath, parentPath,
|
|
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
|
|
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
|
-
|
|
133
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
206
|
-
|
|
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;
|
|
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"}
|
package/dist/publish-file.js
CHANGED
|
@@ -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
|
-
|
|
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);
|