@openclaw/matrix 2026.10.1-beta.2 → 2026.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/node_modules/@openclaw/fs-safe/CHANGELOG.md +56 -0
- package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/advanced.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-entry.js +7 -3
- package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.d.ts +35 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.js +218 -0
- package/node_modules/@openclaw/fs-safe/dist/create.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/create.js +4 -1
- package/node_modules/@openclaw/fs-safe/dist/entry-publication.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/errors.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/errors.js +5 -0
- package/node_modules/@openclaw/fs-safe/dist/index.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/index.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/native-move-fallback.js +54 -0
- package/node_modules/@openclaw/fs-safe/dist/native-noreplace.d.ts +8 -0
- package/node_modules/@openclaw/fs-safe/dist/native-noreplace.js +55 -0
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +12 -2
- package/node_modules/@openclaw/fs-safe/dist/native-rename-outcome.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/native-staged-file.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +48 -5
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-input.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-input.js +17 -0
- package/node_modules/@openclaw/fs-safe/dist/pinned-write.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/publish-file-failure.d.ts +5 -0
- package/node_modules/@openclaw/fs-safe/dist/publish-file-failure.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/publish-file-native.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/publish-file-native.js +35 -0
- package/node_modules/@openclaw/fs-safe/dist/publish-file.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/publish-file.js +8 -2
- package/node_modules/@openclaw/fs-safe/dist/replace-directory.js +6 -5
- package/node_modules/@openclaw/fs-safe/dist/root-errors.js +25 -4
- package/node_modules/@openclaw/fs-safe/dist/root-impl.js +3 -3
- package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.d.ts +4 -2
- package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +13 -17
- package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +18 -3
- package/node_modules/@openclaw/fs-safe/dist/staged-file-settlement.js +4 -1
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-owner.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-owner.js +27 -4
- package/node_modules/@openclaw/fs-safe/docs/advanced.md +1 -0
- package/node_modules/@openclaw/fs-safe/docs/archive.md +2 -1
- package/node_modules/@openclaw/fs-safe/docs/copy.md +61 -1
- package/node_modules/@openclaw/fs-safe/docs/durability.md +50 -5
- package/node_modules/@openclaw/fs-safe/docs/errors.md +37 -0
- package/node_modules/@openclaw/fs-safe/docs/native-helper.md +5 -3
- package/node_modules/@openclaw/fs-safe/docs/native.md +39 -6
- package/node_modules/@openclaw/fs-safe/docs/public-api.md +3 -1
- package/node_modules/@openclaw/fs-safe/docs/staged-file.md +10 -3
- package/node_modules/@openclaw/fs-safe/docs/watch.md +3 -1
- package/node_modules/@openclaw/fs-safe/docs/writing.md +40 -11
- package/node_modules/@openclaw/fs-safe/package.json +11 -11
- package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-win32-x64-msvc/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
- package/package.json +5 -5
|
@@ -2,6 +2,62 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.24.2 - 2026-10-07
|
|
6
|
+
|
|
7
|
+
### Fixes
|
|
8
|
+
|
|
9
|
+
- **Linux exclusive publication:** `publishFileExclusive` with `rename-noreplace` uses the identity-checked link/unlink move fallback in native `auto` when `RENAME_NOREPLACE` is unsupported, consumes the source on success, and reports `fallback: "link-unlink"` plus precise partial-failure receipts; native `require` stays fail-closed. Link/unlink is not crash-atomic. ([#843](https://github.com/openclaw/fs-safe/pull/843), [openclaw#166246](https://github.com/openclaw/openclaw/issues/166246)) Thanks @MertBasar0.
|
|
10
|
+
- **Native clone descriptors:** native file copies return read-write descriptors, so Linux clones can be read immediately and macOS clones can be written while preserving read-only source modes. ([#844](https://github.com/openclaw/fs-safe/pull/844))
|
|
11
|
+
|
|
12
|
+
## 0.24.1 - 2026-10-06
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
- **Pinned source copies:** `copyRootFileSync` accepts `expectedSourceIdentity` to reject a replaced source before destination creation and returns the verified `sourceIdentity` for every copy method. This refines the `copyRootFileSync` API introduced in 0.24.0. ([#841](https://github.com/openclaw/fs-safe/pull/841))
|
|
17
|
+
|
|
18
|
+
## 0.24.0 - 2026-10-06
|
|
19
|
+
|
|
20
|
+
### Highlights
|
|
21
|
+
|
|
22
|
+
- **No-replace moves on more Linux filesystems:** when filesystems reject `renameat2` with `RENAME_NOREPLACE`, including reported QNAP ZFS shares and some overlay mounts, `auto` falls back safely: directories use plain rename that can at most replace an empty directory, and files use identity-checked link/unlink that cannot overwrite a target. Native `require` fails closed. ([#838](https://github.com/openclaw/fs-safe/pull/838), fixes [openclaw#165617](https://github.com/openclaw/openclaw/issues/165617)) Thanks @StarGazer1995, original reporter @bbs2i58, and @MertBasar0.
|
|
23
|
+
- **Synchronous guarded copies:** new `copyRootFileSync` on `advanced` supports APFS cloning, reflinks, `copy_file_range`, and guarded byte fallback, returning an owned destination descriptor for synchronous capture hashes. ([#839](https://github.com/openclaw/fs-safe/pull/839))
|
|
24
|
+
- **Public capability classification:** new `isNoReplaceUnsupported(error)` identifies unavailable no-replace rename support without matching internal native error codes. ([#838](https://github.com/openclaw/fs-safe/pull/838))
|
|
25
|
+
|
|
26
|
+
### Features
|
|
27
|
+
|
|
28
|
+
- **Synchronous root-to-root copy:** `copyRootFileSync` exclusively creates an absent destination under its admitted root, bounds the copy with `maxBytes`, verifies source and destination identities, and returns a disposable descriptor with copy-method and byte-count receipts. ([#839](https://github.com/openclaw/fs-safe/pull/839))
|
|
29
|
+
- **No-replace error classifier:** export `isNoReplaceUnsupported(error)` from the root package and `errors` subpath for stable `helper-unavailable` capability diagnostics. ([#838](https://github.com/openclaw/fs-safe/pull/838))
|
|
30
|
+
|
|
31
|
+
### Fixes
|
|
32
|
+
|
|
33
|
+
- **Linux no-replace moves:** `Root.move()` uses retained-parent file link/unlink or directory rename when `RENAME_NOREPLACE` is unsupported in `auto`, rejects file collisions, and preserves publication/source-removal details after partial failures. ([#838](https://github.com/openclaw/fs-safe/pull/838)) Thanks @StarGazer1995, @bbs2i58, and @MertBasar0.
|
|
34
|
+
|
|
35
|
+
### Performance
|
|
36
|
+
|
|
37
|
+
- **Synchronous copy acceleration:** reuse the native bounded copy engine for APFS clones, Linux reflinks, and `copy_file_range` before byte copying when `copyRootFileSync` is called with `clone: "auto"`. ([#839](https://github.com/openclaw/fs-safe/pull/839))
|
|
38
|
+
|
|
39
|
+
### Compatibility
|
|
40
|
+
|
|
41
|
+
- **Move fallback limits:** native `require` keeps refusing unsupported no-replace moves. In `auto`, the directory fallback can replace a concurrently created empty directory; nonempty directories and non-directory competitors reject. File fallback requires hard-link support and fails closed if links are denied or unsupported. ([#838](https://github.com/openclaw/fs-safe/pull/838))
|
|
42
|
+
- **Synchronous copy contract:** `copyRootFileSync` defaults to `clone: "never"`, requires existing parents and an absent destination, and performs direct exclusive creation. Use a private capture directory; it does not promise atomic publication, crash durability, or a coherent snapshot of concurrent in-place edits. ([#839](https://github.com/openclaw/fs-safe/pull/839))
|
|
43
|
+
|
|
44
|
+
## 0.23.1 - 2026-10-04
|
|
45
|
+
|
|
46
|
+
### Highlights
|
|
47
|
+
|
|
48
|
+
- **gVisor and 9p file creation:** `create()` and other no-replace publication fall back in `auto` mode on filesystems that reject `renameat2` with `RENAME_NOREPLACE`; `require` mode fails closed. ([#834](https://github.com/openclaw/fs-safe/pull/834), fixes [#828](https://github.com/openclaw/fs-safe/issues/828)) Thanks @vishnukool.
|
|
49
|
+
- **Container copy compatibility:** seccomp or LSM policies denying `FICLONE` no longer break `clone: "auto"` copies, for both single files and `copyTree`. ([#829](https://github.com/openclaw/fs-safe/pull/829), [#835](https://github.com/openclaw/fs-safe/pull/835)) Thanks @Mohl, with co-author credit to @foxsky.
|
|
50
|
+
- **macOS watching:** a FIFO at a watched name no longer freezes watching across the process. ([#830](https://github.com/openclaw/fs-safe/pull/830)) Thanks @SebTardif.
|
|
51
|
+
|
|
52
|
+
### Fixes
|
|
53
|
+
|
|
54
|
+
- **gVisor file creation:** fall back safely when the filesystem rejects native no-replace publication, clean unpublished stages, and report the missing capability in native-required mode. ([#834](https://github.com/openclaw/fs-safe/pull/834)) Thanks @vishnukool.
|
|
55
|
+
- **ZIP collision checks:** reject directory and file names that differ only by case, Unicode normalization, or a trailing separator before extraction or bounded reads. ([#832](https://github.com/openclaw/fs-safe/pull/832)) Thanks @SebTardif.
|
|
56
|
+
- **macOS entry watch:** prevent a FIFO at a watched name from blocking every watcher in the process. ([#830](https://github.com/openclaw/fs-safe/pull/830)) Thanks @SebTardif.
|
|
57
|
+
- **Disk-full diagnostics:** preserve disk-full (`ENOSPC`) diagnostics and cleanup receipts through combined write and disposal failures instead of reporting a misleading file-type error. ([#833](https://github.com/openclaw/fs-safe/pull/833)) Thanks @steipete.
|
|
58
|
+
- **Linux clone fallback:** copies with `clone: "auto"` fall back to guarded byte copying when container seccomp or LSM policy denies `FICLONE`; `clone: "always"` reports `unsupported-platform` and removes its stage. ([#829](https://github.com/openclaw/fs-safe/pull/829)) Thanks @Mohl, with co-author credit to @foxsky.
|
|
59
|
+
- **Linux tree clone fallback:** `copyTree` with `clone: "auto"` copies bytes when XFS/ZFS file reflinks are unavailable or denied by container policy; `"always"` reports `unsupported-platform` after partial-tree cleanup. ([#835](https://github.com/openclaw/fs-safe/pull/835)) Thanks @steipete.
|
|
60
|
+
|
|
5
61
|
## 0.23.0 - 2026-10-02
|
|
6
62
|
|
|
7
63
|
### Highlights
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { copyRootFileSync, type CopyRootFileSyncOptions, type CopiedRootFileSync } from "./copy-root-file-sync.js";
|
|
1
2
|
export { createAsyncLock } from "./async-lock.js";
|
|
2
3
|
export { createDirectory, createDirectorySync, createFileSync, type CreateDirectoryOptions, type CreateFileOptions, type OwnedFileDescriptorSync, } from "./create.js";
|
|
3
4
|
export { copyFileHandle, copyFileDescriptorSync, type CopyFileHandleOptions } from "./file-handle-transfer.js";
|
|
@@ -63,13 +63,17 @@ export function stripArchivePath(entryPath, stripComponents) {
|
|
|
63
63
|
export function createArchiveOutputPathTracker() {
|
|
64
64
|
const seen = new Set();
|
|
65
65
|
return (entryPath, originalPath) => {
|
|
66
|
-
|
|
66
|
+
// Extraction passes stripArchivePath output. Native manifests and physical
|
|
67
|
+
// admission pass raw names, and path.posix.normalize keeps a directory's
|
|
68
|
+
// trailing slash, so "A/" and "a" would not share a key.
|
|
69
|
+
const canonical = stripArchivePath(entryPath, 0)
|
|
70
|
+
?? path.posix.normalize(normalizeArchiveEntryPath(entryPath));
|
|
67
71
|
// Archive policy must not depend on the destination volume's case or
|
|
68
72
|
// Unicode-normalization behavior. Otherwise the JavaScript and native
|
|
69
73
|
// writers can disagree about which of two colliding entries wins.
|
|
70
|
-
const collisionKey = lowerCaseNfc(
|
|
74
|
+
const collisionKey = lowerCaseNfc(canonical);
|
|
71
75
|
if (seen.has(collisionKey)) {
|
|
72
|
-
throw new ArchiveSecurityError("entry-path", `archive entries collide at output path ${formatErrorDetail(
|
|
76
|
+
throw new ArchiveSecurityError("entry-path", `archive entries collide at output path ${formatErrorDetail(canonical)}: ${formatErrorDetail(originalPath)}`);
|
|
73
77
|
}
|
|
74
78
|
seen.add(collisionKey);
|
|
75
79
|
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type CopyCloneMode } from "./copy-policy.js";
|
|
2
|
+
import { type OwnedFileDescriptorSync } from "./create-owned-file.js";
|
|
3
|
+
type CopyPath = {
|
|
4
|
+
rootPath: string;
|
|
5
|
+
absolutePath: string;
|
|
6
|
+
};
|
|
7
|
+
export type CopyRootFileSyncOptions = {
|
|
8
|
+
source: CopyPath;
|
|
9
|
+
destination: CopyPath;
|
|
10
|
+
expectedSourceIdentity?: {
|
|
11
|
+
dev: bigint;
|
|
12
|
+
ino: bigint;
|
|
13
|
+
};
|
|
14
|
+
clone?: CopyCloneMode;
|
|
15
|
+
maxBytes?: number;
|
|
16
|
+
mode?: number;
|
|
17
|
+
preserveSourceMode?: boolean;
|
|
18
|
+
sourceHardlinks?: "reject" | "allow";
|
|
19
|
+
};
|
|
20
|
+
export type CopiedRootFileSync = OwnedFileDescriptorSync & Readonly<{
|
|
21
|
+
path: string;
|
|
22
|
+
bytes: number;
|
|
23
|
+
method: "clone" | "copy-file-range" | "copy";
|
|
24
|
+
identity: Readonly<{
|
|
25
|
+
dev: bigint;
|
|
26
|
+
ino: bigint;
|
|
27
|
+
}>;
|
|
28
|
+
sourceIdentity: Readonly<{
|
|
29
|
+
dev: bigint;
|
|
30
|
+
ino: bigint;
|
|
31
|
+
}>;
|
|
32
|
+
}>;
|
|
33
|
+
/** Exclusively copies an admitted source and transfers the owned destination fd. */
|
|
34
|
+
export declare function copyRootFileSync(options: CopyRootFileSyncOptions): CopiedRootFileSync;
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import fs, {} from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { normalizeMaxBytes } from "./byte-budget.js";
|
|
4
|
+
import { resolveCopyCloneMode } from "./copy-policy.js";
|
|
5
|
+
import { createFileWithAdmissionSync } from "./create.js";
|
|
6
|
+
import { ownFileDescriptorSync } from "./create-owned-file.js";
|
|
7
|
+
import { creationAdmissionFromParent, removeRecordedCreationFileSync } from "./creation-boundary.js";
|
|
8
|
+
import { assertSyncDirectoryGuard, captureDirectoryGuard } from "./directory-guard.js";
|
|
9
|
+
import { FsSafeError } from "./errors.js";
|
|
10
|
+
import { assertExclusiveCreateLeaf } from "./exclusive-create.js";
|
|
11
|
+
import { copyFileDescriptorSync } from "./file-handle-transfer.js";
|
|
12
|
+
import { captureNativeFdClose } from "./native-binding.js";
|
|
13
|
+
import { getNativeBinding } from "./native.js";
|
|
14
|
+
import { realpathSync } from "./realpath.js";
|
|
15
|
+
import { openRootFileSync } from "./root-file.js";
|
|
16
|
+
import { resolveRootPathSync, ROOT_PATH_ALIAS_POLICIES } from "./root-path.js";
|
|
17
|
+
import { inspectOpenedPathIdentitySync } from "./root-read-admission.js";
|
|
18
|
+
import { openStagedDirectory } from "./staged-directory.js";
|
|
19
|
+
import { inspectFileIdentitySync } from "./strict-file-identity.js";
|
|
20
|
+
function copyError(error) {
|
|
21
|
+
if (error instanceof FsSafeError)
|
|
22
|
+
return error;
|
|
23
|
+
const code = error?.code;
|
|
24
|
+
return new FsSafeError(code === "EEXIST" ? "already-exists" : code === "ENOENT" ? "not-found" :
|
|
25
|
+
code === "too-large" ? "too-large" :
|
|
26
|
+
["ENOTSUP", "EOPNOTSUPP", "ENOSYS", "EXDEV", "EINVAL"].includes(code ?? "")
|
|
27
|
+
? "unsupported-platform" : "helper-failed", "guarded synchronous file copy failed", { cause: error });
|
|
28
|
+
}
|
|
29
|
+
function admitCopyPath(input, create = false) {
|
|
30
|
+
if (!path.isAbsolute(input.rootPath) || !path.isAbsolute(input.absolutePath)) {
|
|
31
|
+
throw new FsSafeError("invalid-path", "copy roots and paths must be absolute");
|
|
32
|
+
}
|
|
33
|
+
const root = captureDirectoryGuard(realpathSync(input.rootPath), "native", { bigint: true });
|
|
34
|
+
const resolve = () => {
|
|
35
|
+
const parent = create ? resolveRootPathSync({
|
|
36
|
+
...input, absolutePath: path.dirname(input.absolutePath), boundaryLabel: "copy parent",
|
|
37
|
+
rejectSymlinks: true, rejectFinalSymlink: true,
|
|
38
|
+
}) : undefined;
|
|
39
|
+
const selected = resolveRootPathSync({
|
|
40
|
+
...input, boundaryLabel: "copy", rejectSymlinks: !create, rejectFinalSymlink: !create,
|
|
41
|
+
policy: create ? ROOT_PATH_ALIAS_POLICIES.unlinkTarget : undefined,
|
|
42
|
+
});
|
|
43
|
+
if (parent && path.dirname(selected.canonicalPath) !== parent.canonicalPath) {
|
|
44
|
+
throw new FsSafeError("path-mismatch", "copy destination parent changed");
|
|
45
|
+
}
|
|
46
|
+
return selected;
|
|
47
|
+
};
|
|
48
|
+
const selected = resolve();
|
|
49
|
+
const parent = captureDirectoryGuard(path.dirname(selected.canonicalPath), "native", { bigint: true });
|
|
50
|
+
const assertCurrent = () => {
|
|
51
|
+
assertSyncDirectoryGuard(root);
|
|
52
|
+
assertSyncDirectoryGuard(parent);
|
|
53
|
+
const current = resolve();
|
|
54
|
+
if (current.rootCanonicalPath !== root.realPath || current.canonicalPath !== selected.canonicalPath) {
|
|
55
|
+
throw new FsSafeError("path-mismatch", "copy path changed during operation");
|
|
56
|
+
}
|
|
57
|
+
assertSyncDirectoryGuard(root);
|
|
58
|
+
assertSyncDirectoryGuard(parent);
|
|
59
|
+
};
|
|
60
|
+
assertCurrent();
|
|
61
|
+
return { path: selected.canonicalPath, parent, assertCurrent };
|
|
62
|
+
}
|
|
63
|
+
/** Exclusively copies an admitted source and transfers the owned destination fd. */
|
|
64
|
+
export function copyRootFileSync(options) {
|
|
65
|
+
// Snapshot caller options before the first filesystem observation.
|
|
66
|
+
const sourceInput = options.source;
|
|
67
|
+
const sourcePath = { rootPath: sourceInput.rootPath, absolutePath: sourceInput.absolutePath };
|
|
68
|
+
const targetInput = options.destination;
|
|
69
|
+
const targetPath = { rootPath: targetInput.rootPath, absolutePath: targetInput.absolutePath };
|
|
70
|
+
const expectedInput = options.expectedSourceIdentity;
|
|
71
|
+
const expectedSourceIdentity = expectedInput === undefined ? undefined : { dev: expectedInput.dev, ino: expectedInput.ino };
|
|
72
|
+
const clone = resolveCopyCloneMode(options.clone, "never");
|
|
73
|
+
const maxBytes = normalizeMaxBytes(options.maxBytes);
|
|
74
|
+
const mode = options.mode;
|
|
75
|
+
const preserveSourceMode = options.preserveSourceMode;
|
|
76
|
+
const sourceHardlinks = options.sourceHardlinks ?? "reject";
|
|
77
|
+
if (mode !== undefined && (!Number.isInteger(mode) || mode < 0 || mode > 0o777)) {
|
|
78
|
+
throw new FsSafeError("invalid-path", "copy mode must be an integer from 0 to 0o777");
|
|
79
|
+
}
|
|
80
|
+
if (sourceHardlinks !== "allow" && sourceHardlinks !== "reject") {
|
|
81
|
+
throw new FsSafeError("invalid-path", "invalid copy source hardlink policy");
|
|
82
|
+
}
|
|
83
|
+
let sourceOwner;
|
|
84
|
+
let targetOwner;
|
|
85
|
+
let parentOwner;
|
|
86
|
+
let parentOpen = false;
|
|
87
|
+
let target;
|
|
88
|
+
let identity;
|
|
89
|
+
const native = getNativeBinding();
|
|
90
|
+
const nativeCopy = process.platform !== "win32" && native?.copyFileExclusiveSync && native.removeStagedFile
|
|
91
|
+
? native.copyFileExclusiveSync.bind(native) : undefined;
|
|
92
|
+
let nativeTarget = false;
|
|
93
|
+
try {
|
|
94
|
+
const source = admitCopyPath(sourcePath);
|
|
95
|
+
target = admitCopyPath(targetPath, true);
|
|
96
|
+
// Preserve exclusive-create collision semantics for dangling Windows leaves.
|
|
97
|
+
assertExclusiveCreateLeaf(targetPath.absolutePath);
|
|
98
|
+
if (fs.lstatSync(target.path, { throwIfNoEntry: false })) {
|
|
99
|
+
throw new FsSafeError("already-exists", "copy destination already exists");
|
|
100
|
+
}
|
|
101
|
+
const opened = openRootFileSync({
|
|
102
|
+
...sourcePath, boundaryLabel: "copy source", maxBytes,
|
|
103
|
+
rejectHardlinks: sourceHardlinks !== "allow",
|
|
104
|
+
});
|
|
105
|
+
if (!opened.ok)
|
|
106
|
+
throw opened.error ?? new FsSafeError("helper-failed", "copy source admission failed");
|
|
107
|
+
sourceOwner = ownFileDescriptorSync(opened.fd);
|
|
108
|
+
const sourceIdentity = inspectFileIdentitySync(() => fs.fstatSync(opened.fd, { bigint: true }), expectedSourceIdentity);
|
|
109
|
+
const verifySource = () => {
|
|
110
|
+
source.assertCurrent();
|
|
111
|
+
for (const inspect of [
|
|
112
|
+
() => fs.fstatSync(opened.fd, { bigint: true }),
|
|
113
|
+
() => inspectOpenedPathIdentitySync(source.path, undefined),
|
|
114
|
+
]) {
|
|
115
|
+
const current = inspectFileIdentitySync(inspect, sourceIdentity);
|
|
116
|
+
if (!current.isFile())
|
|
117
|
+
throw new FsSafeError("not-file", "copy source must be a regular file");
|
|
118
|
+
if (sourceHardlinks !== "allow" && current.nlink > 1n) {
|
|
119
|
+
throw new FsSafeError("hardlink", "copy source must not be hardlinked");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
const selectedMode = mode ?? (preserveSourceMode ? Number(sourceIdentity.mode & 511n) : 0o600 & ~process.umask());
|
|
124
|
+
let method = "copy";
|
|
125
|
+
let copied;
|
|
126
|
+
verifySource();
|
|
127
|
+
target.assertCurrent();
|
|
128
|
+
if (nativeCopy) {
|
|
129
|
+
const closeNative = captureNativeFdClose(native);
|
|
130
|
+
const parent = openStagedDirectory(target.parent.realPath);
|
|
131
|
+
parentOwner = ownFileDescriptorSync(parent.fd);
|
|
132
|
+
parentOpen = true;
|
|
133
|
+
inspectFileIdentitySync(() => fs.fstatSync(parent.fd, { bigint: true }), { dev: BigInt(target.parent.stat.dev), ino: BigInt(target.parent.stat.ino) });
|
|
134
|
+
target.assertCurrent();
|
|
135
|
+
copied = nativeCopy(opened.fd, parent.fd, path.basename(target.path), clone, maxBytes !== undefined && Number.isFinite(maxBytes) ? maxBytes : undefined);
|
|
136
|
+
targetOwner = ownFileDescriptorSync(copied.fd, closeNative);
|
|
137
|
+
nativeTarget = true;
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
if (clone === "always")
|
|
141
|
+
throw new FsSafeError("unsupported-platform", "synchronous native file cloning is unavailable");
|
|
142
|
+
targetOwner = createFileWithAdmissionSync(target.path, { mode: 0o600, assertBeforeMutation: target.assertCurrent }, creationAdmissionFromParent(target.parent));
|
|
143
|
+
}
|
|
144
|
+
identity = inspectFileIdentitySync(() => fs.fstatSync(targetOwner.fd, { bigint: true }));
|
|
145
|
+
const verifyTarget = () => {
|
|
146
|
+
target.assertCurrent();
|
|
147
|
+
const current = inspectFileIdentitySync(() => fs.fstatSync(targetOwner.fd, { bigint: true }), identity);
|
|
148
|
+
const named = inspectFileIdentitySync(() => inspectOpenedPathIdentitySync(target.path, undefined), identity);
|
|
149
|
+
if (!current.isFile() || current.nlink !== 1n || !named.isFile() || named.nlink !== 1n) {
|
|
150
|
+
throw new FsSafeError("path-mismatch", "copy destination changed");
|
|
151
|
+
}
|
|
152
|
+
return current;
|
|
153
|
+
};
|
|
154
|
+
verifyTarget();
|
|
155
|
+
if (copied?.errorCode) {
|
|
156
|
+
throw copyError(Object.assign(new Error(copied.errorMessage), { code: copied.errorCode }));
|
|
157
|
+
}
|
|
158
|
+
if (copied) {
|
|
159
|
+
if (!["clone", "copy-file-range", "copy"].includes(copied.method)) {
|
|
160
|
+
throw new FsSafeError("helper-failed", "native copy returned an unknown method");
|
|
161
|
+
}
|
|
162
|
+
method = copied.method;
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
copyFileDescriptorSync(opened.fd, targetOwner.fd, { maxBytes });
|
|
166
|
+
}
|
|
167
|
+
verifySource();
|
|
168
|
+
const completed = verifyTarget();
|
|
169
|
+
if (maxBytes !== undefined && Number.isFinite(maxBytes) && completed.size > BigInt(maxBytes)) {
|
|
170
|
+
throw new FsSafeError("too-large", "copy exceeds maxBytes");
|
|
171
|
+
}
|
|
172
|
+
fs.fchmodSync(targetOwner.fd, selectedMode);
|
|
173
|
+
verifySource();
|
|
174
|
+
verifyTarget();
|
|
175
|
+
sourceOwner.close();
|
|
176
|
+
parentOpen = false;
|
|
177
|
+
parentOwner?.close();
|
|
178
|
+
return Object.freeze({
|
|
179
|
+
...targetOwner, path: target.path, bytes: Number(completed.size), method,
|
|
180
|
+
identity: Object.freeze({ dev: identity.dev, ino: identity.ino }),
|
|
181
|
+
sourceIdentity: Object.freeze({ dev: sourceIdentity.dev, ino: sourceIdentity.ino }),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
const primary = copyError(error);
|
|
186
|
+
const cleanup = [];
|
|
187
|
+
if (targetOwner && target) {
|
|
188
|
+
try {
|
|
189
|
+
if (nativeTarget && parentOpen) {
|
|
190
|
+
const removed = native.removeStagedFile(parentOwner.fd, path.basename(target.path), targetOwner.fd);
|
|
191
|
+
if (removed === "preserved")
|
|
192
|
+
throw new FsSafeError("path-mismatch", "copy cleanup preserved a replacement");
|
|
193
|
+
}
|
|
194
|
+
else if (identity) {
|
|
195
|
+
removeRecordedCreationFileSync(target.path, identity, target.assertCurrent);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
catch (failure) {
|
|
199
|
+
cleanup.push(failure);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
for (const owner of [targetOwner, sourceOwner, parentOwner]) {
|
|
203
|
+
try {
|
|
204
|
+
owner?.close();
|
|
205
|
+
}
|
|
206
|
+
catch (failure) {
|
|
207
|
+
cleanup.push(failure);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (cleanup.length) {
|
|
211
|
+
throw new FsSafeError(primary.code, primary.message, {
|
|
212
|
+
cause: new AggregateError([primary, ...cleanup], "copy and cleanup failed"),
|
|
213
|
+
details: { path: target?.path, cleanup: "failed" },
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
throw primary;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -4,6 +4,7 @@ import { type CreationPermissions, type CreationParentIdentity } from "./creatio
|
|
|
4
4
|
export type { OwnedFileDescriptorSync } from "./create-owned-file.js";
|
|
5
5
|
export type CreateFileOptions = CreateDirectoryOptions;
|
|
6
6
|
export declare function createFileSync(targetPath: string, options?: CreateFileOptions): OwnedFileDescriptorSync;
|
|
7
|
+
export declare function createFileWithAdmissionSync(targetPath: string, options?: CreateFileOptions, admission?: CreationAdmission): OwnedFileDescriptorSync;
|
|
7
8
|
/** Internal async writer; Windows preparation uses the nonblocking stage owner. */
|
|
8
9
|
export declare function createFileHandle(targetPath: string, options?: CreateFileOptions, admission?: CreationAdmission): Promise<FileHandle>;
|
|
9
10
|
export type CreateDirectoryOptions = CreationPermissions & {
|
|
@@ -108,9 +108,12 @@ function createPrivateWindowsFile(selected, options, mode) {
|
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
export function createFileSync(targetPath, options = {}) {
|
|
111
|
+
return createFileWithAdmissionSync(targetPath, options);
|
|
112
|
+
}
|
|
113
|
+
export function createFileWithAdmissionSync(targetPath, options = {}, admission = {}) {
|
|
111
114
|
const permissions = resolveCreationPermissions(options, false);
|
|
112
115
|
const assertion = options.assertBeforeMutation;
|
|
113
|
-
const selected = prepareCreationPath(targetPath);
|
|
116
|
+
const selected = prepareCreationPath(targetPath, admission.expectedParentIdentity);
|
|
114
117
|
if (permissions.private && process.platform === "win32") {
|
|
115
118
|
assertPrivateFileCreationAvailable();
|
|
116
119
|
return createPrivateWindowsFile(selected, { ...permissions, assertBeforeMutation: assertion }, permissions.mode);
|
|
@@ -2,6 +2,7 @@ import fs, {} from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { assertDirectoryIdentitySync } from "./directory-guard.js";
|
|
4
4
|
import { FsSafeError } from "./errors.js";
|
|
5
|
+
import { noReplaceUnavailable } from "./native-noreplace.js";
|
|
5
6
|
import { assertSynchronousCallbackResult } from "./mutation-authority.js";
|
|
6
7
|
import { captureNativeFdClose } from "./native-binding.js";
|
|
7
8
|
import { requireNativeBinding } from "./native.js";
|
|
@@ -120,7 +121,8 @@ class Publication {
|
|
|
120
121
|
}
|
|
121
122
|
transition = native.outcome; // Capture BEFORE diagnostics, observations or close.
|
|
122
123
|
if (native.errorCode || transition !== "committed") {
|
|
123
|
-
|
|
124
|
+
const error = Object.assign(new Error(native.errorMessage ?? "entry publication did not commit"), { code: native.errorCode ?? "helper-failed" });
|
|
125
|
+
throw noReplaceUnavailable(error, "retained entry publication") ?? error;
|
|
124
126
|
}
|
|
125
127
|
phase = "postcheck";
|
|
126
128
|
verification = "failed";
|
|
@@ -12,4 +12,6 @@ export declare class FsSafeError extends Error {
|
|
|
12
12
|
details?: FsSafeErrorDetails;
|
|
13
13
|
});
|
|
14
14
|
}
|
|
15
|
+
/** A rejected no-replace rename capability, not a generic missing native helper. */
|
|
16
|
+
export declare function isNoReplaceUnsupported(error: unknown): error is FsSafeError;
|
|
15
17
|
export {};
|
|
@@ -25,3 +25,8 @@ export class FsSafeError extends Error {
|
|
|
25
25
|
this.details = options.details;
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
+
/** A rejected no-replace rename capability, not a generic missing native helper. */
|
|
29
|
+
export function isNoReplaceUnsupported(error) {
|
|
30
|
+
return error instanceof FsSafeError && error.code === "helper-unavailable" &&
|
|
31
|
+
error.details?.capability === "rename-noreplace";
|
|
32
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { FsSafeError, categorizeFsSafeError, } from "./errors.js";
|
|
1
|
+
export { FsSafeError, categorizeFsSafeError, isNoReplaceUnsupported, } from "./errors.js";
|
|
2
2
|
export type * from "./errors.js";
|
|
3
3
|
export { DEFAULT_ROOT_MAX_BYTES, root, type Root, } from "./root.js";
|
|
4
4
|
export type * from "./root-public-types.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { FsSafeError, categorizeFsSafeError, } from "./errors.js";
|
|
1
|
+
export { FsSafeError, categorizeFsSafeError, isNoReplaceUnsupported, } from "./errors.js";
|
|
2
2
|
export { DEFAULT_ROOT_MAX_BYTES, root, } from "./root.js";
|
|
3
3
|
export { configureFsSafeNative, getFsSafeNativeConfig, } from "./native-config.js";
|
|
4
4
|
export { writeExternalFileWithinRoot, } from "./output.js";
|
|
@@ -203,6 +203,7 @@ export interface NativeBinding {
|
|
|
203
203
|
stagedFileMatches?(parentFd: number, basename: string, fileFd: number): boolean;
|
|
204
204
|
removeStagedFile?(parentFd: number, basename: string, fileFd: number): "removed" | "name-absent" | "preserved";
|
|
205
205
|
copyFileExclusive?(sourceFd: number, parentFd: number, basename: string, clone: CopyCloneMode, maxBytes: number | undefined, signal: AbortSignal | undefined): Promise<NativeFileCopyResult>;
|
|
206
|
+
copyFileExclusiveSync?(sourceFd: number, parentFd: number, basename: string, clone: CopyCloneMode, maxBytes: number | undefined): NativeFileCopyResult;
|
|
206
207
|
cloneFileExclusive(sourceFd: number, targetRootFd: number, targetRelPath: string): number;
|
|
207
208
|
copyFileRangeExclusive(sourceFd: number, targetRootFd: number, targetRelPath: string): Promise<NativeCopyResult>;
|
|
208
209
|
createPrivateDirectory(path: string): void;
|
|
@@ -235,6 +236,8 @@ export interface NativeBinding {
|
|
|
235
236
|
entryPublicationFilesystem?(parentFd: number): string;
|
|
236
237
|
publishRetainedEntryNoReplace?(sourceParentFd: number, sourceBasename: string, sourceFd: number, targetParentFd: number, targetBasename: string): NativePublicationTransition;
|
|
237
238
|
renameNoReplace(...args: NativeTwoPathArgs): void;
|
|
239
|
+
/** Linux auto-mode fallback through already-admitted direct-child parents. */
|
|
240
|
+
moveNoReplaceFallback?(...args: [...paths: NativeTwoPathArgs, expectedSourceDev: bigint, expectedSourceIno: bigint]): void;
|
|
238
241
|
/** Identity-fenced retained-directory rename capability. */
|
|
239
242
|
renameNoReplaceWithIdentity?(...args: [...paths: NativeTwoPathArgs, expectedSourceDev: bigint, expectedSourceIno: bigint]): void;
|
|
240
243
|
renameReplace(...args: NativeTwoPathArgs): void;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { BigIntStats } from "node:fs";
|
|
2
|
+
import type { NativeBinding } from "./native-binding.js";
|
|
3
|
+
export declare function nativeMoveNoReplace(binding: NativeBinding, paths: Parameters<NativeBinding["renameNoReplace"]>, expected: BigIntStats | undefined, allowFallback: boolean, operation?: string): "link-unlink" | undefined;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { FsSafeError } from "./errors.js";
|
|
2
|
+
import { cachedNoReplaceUnavailable, noReplaceUnavailable, rememberNoReplaceUnavailable, } from "./native-noreplace.js";
|
|
3
|
+
function fallbackError(error, operation) {
|
|
4
|
+
const code = error?.code;
|
|
5
|
+
const message = error instanceof Error ? error.message : "native no-replace move fallback failed";
|
|
6
|
+
if (code === "EEXIST") {
|
|
7
|
+
return new FsSafeError("already-exists", "no-replace fallback destination exists", { cause: error });
|
|
8
|
+
}
|
|
9
|
+
if (code === "FS_SAFE_INTERNAL_MOVE_LINK_UNSUPPORTED") {
|
|
10
|
+
return new FsSafeError("helper-unavailable", message, { cause: error,
|
|
11
|
+
details: { capability: "rename-noreplace", fallback: "link-unlink", fallbackCapability: "linkat" } });
|
|
12
|
+
}
|
|
13
|
+
const sourceRemoval = new Map([
|
|
14
|
+
["FS_SAFE_INTERNAL_MOVE_LINK_CHANGED", "not-attempted"],
|
|
15
|
+
["FS_SAFE_INTERNAL_MOVE_SOURCE_LINKED", "still-linked"],
|
|
16
|
+
["FS_SAFE_INTERNAL_MOVE_UNLINK_UNVERIFIED", "unverified"],
|
|
17
|
+
["FS_SAFE_INTERNAL_MOVE_PUBLISHED", "removed"],
|
|
18
|
+
]).get(code ?? "");
|
|
19
|
+
if (sourceRemoval) {
|
|
20
|
+
return new FsSafeError(code === "FS_SAFE_INTERNAL_MOVE_LINK_CHANGED" ? "path-mismatch" : "helper-failed", message, { cause: error, details: { operation, fallback: "link-unlink",
|
|
21
|
+
publication: "published", sourceRemoval } });
|
|
22
|
+
}
|
|
23
|
+
// Keep identity errors available to the caller's normalizer.
|
|
24
|
+
return error;
|
|
25
|
+
}
|
|
26
|
+
export function nativeMoveNoReplace(binding, paths, expected, allowFallback, operation = "move") {
|
|
27
|
+
const [sourceFd, source, targetFd, target] = paths;
|
|
28
|
+
let unavailable = cachedNoReplaceUnavailable(binding, sourceFd, operation);
|
|
29
|
+
if (!unavailable) {
|
|
30
|
+
try {
|
|
31
|
+
binding.renameNoReplace(...paths);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
unavailable = noReplaceUnavailable(error, operation, true);
|
|
36
|
+
if (!unavailable)
|
|
37
|
+
throw error;
|
|
38
|
+
// As in sibling publication/quarantine, distinct sibling names cannot
|
|
39
|
+
// have a directory-ancestry EINVAL. Cross-parent moves must not cache it.
|
|
40
|
+
if (sourceFd === targetFd && source !== target) {
|
|
41
|
+
rememberNoReplaceUnavailable(binding, sourceFd, unavailable);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (!allowFallback || process.platform !== "linux" || !binding.moveNoReplaceFallback || !expected)
|
|
46
|
+
throw unavailable;
|
|
47
|
+
try {
|
|
48
|
+
binding.moveNoReplaceFallback(...paths, expected.dev, expected.ino);
|
|
49
|
+
return expected.isFile() ? "link-unlink" : undefined;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
throw fallbackError(error, operation);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { FsSafeError } from "./errors.js";
|
|
2
|
+
import type { NativeBinding } from "./native-binding.js";
|
|
3
|
+
export declare const NATIVE_NOREPLACE_UNSUPPORTED = "FS_SAFE_INTERNAL_RENAME_NOREPLACE_UNSUPPORTED";
|
|
4
|
+
export declare function isNoReplaceUnavailable(error: unknown): error is FsSafeError;
|
|
5
|
+
export declare function noReplaceUnavailable(error: unknown, operation: string, syscallOnly?: boolean): FsSafeError | undefined;
|
|
6
|
+
export declare function rememberNoReplaceUnavailable(binding: NativeBinding, parentFd: number, error: FsSafeError): void;
|
|
7
|
+
export declare function cachedNoReplaceUnavailable(binding: NativeBinding, parentFd: number, operation?: string): FsSafeError | undefined;
|
|
8
|
+
export declare function cachedNoReplaceDevice(binding: NativeBinding, device: bigint, operation: string): FsSafeError | undefined;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { FsSafeError } from "./errors.js";
|
|
3
|
+
export const NATIVE_NOREPLACE_UNSUPPORTED = "FS_SAFE_INTERNAL_RENAME_NOREPLACE_UNSUPPORTED";
|
|
4
|
+
const unsupported = new WeakMap();
|
|
5
|
+
const capabilityErrors = new WeakMap();
|
|
6
|
+
const MAX_DEVICES = 128;
|
|
7
|
+
function unavailable(operation, detail, cause) {
|
|
8
|
+
const error = new FsSafeError("helper-unavailable", `native no-replace ${operation} is unsupported on this filesystem (${detail})`, { cause, details: { capability: "rename-noreplace" } });
|
|
9
|
+
capabilityErrors.set(error, detail);
|
|
10
|
+
return error;
|
|
11
|
+
}
|
|
12
|
+
export function isNoReplaceUnavailable(error) {
|
|
13
|
+
return error instanceof FsSafeError && capabilityErrors.has(error);
|
|
14
|
+
}
|
|
15
|
+
export function noReplaceUnavailable(error, operation, syscallOnly = false) {
|
|
16
|
+
try {
|
|
17
|
+
const code = error?.code;
|
|
18
|
+
if (code !== NATIVE_NOREPLACE_UNSUPPORTED &&
|
|
19
|
+
!(syscallOnly && ["EINVAL", "ENOSYS", "ENOTSUP", "EOPNOTSUPP"].includes(code ?? "")))
|
|
20
|
+
return;
|
|
21
|
+
const capability = process.platform === "darwin" ? "renameatx_np RENAME_EXCL"
|
|
22
|
+
: process.platform === "win32" ? "native no-replace rename" : "renameat2 RENAME_NOREPLACE";
|
|
23
|
+
const detail = code === NATIVE_NOREPLACE_UNSUPPORTED
|
|
24
|
+
? error.message : `${capability}: ${code}`;
|
|
25
|
+
return unavailable(operation, detail, error);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Diagnostic getters do not establish non-publication.
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function rememberNoReplaceUnavailable(binding, parentFd, error) {
|
|
33
|
+
const detail = capabilityErrors.get(error);
|
|
34
|
+
if (!detail)
|
|
35
|
+
return;
|
|
36
|
+
const device = fs.fstatSync(parentFd, { bigint: true }).dev;
|
|
37
|
+
let devices = unsupported.get(binding);
|
|
38
|
+
if (!devices)
|
|
39
|
+
unsupported.set(binding, devices = new Map());
|
|
40
|
+
if (!devices.has(device) && devices.size >= MAX_DEVICES)
|
|
41
|
+
devices.delete(devices.keys().next().value);
|
|
42
|
+
devices.set(device, detail);
|
|
43
|
+
}
|
|
44
|
+
export function cachedNoReplaceUnavailable(binding, parentFd, operation = "publication") {
|
|
45
|
+
if (!unsupported.has(binding))
|
|
46
|
+
return;
|
|
47
|
+
return cachedNoReplaceDevice(binding, fs.fstatSync(parentFd, { bigint: true }).dev, operation);
|
|
48
|
+
}
|
|
49
|
+
export function cachedNoReplaceDevice(binding, device, operation) {
|
|
50
|
+
const devices = unsupported.get(binding);
|
|
51
|
+
if (!devices)
|
|
52
|
+
return;
|
|
53
|
+
const detail = devices.get(device);
|
|
54
|
+
return detail ? unavailable(operation, detail) : undefined;
|
|
55
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { FileIdentityStat } from "./file-identity.js";
|
|
2
2
|
import type { NativeBinding } from "./native.js";
|
|
3
3
|
import type { PinnedWriteParams } from "./pinned-write-types.js";
|
|
4
|
-
export declare function runPinnedWriteNative(binding: NativeBinding, params: PinnedWriteParams): Promise<FileIdentityStat>;
|
|
4
|
+
export declare function runPinnedWriteNative(binding: NativeBinding, params: PinnedWriteParams, fallback: (input: PinnedWriteParams["input"]) => Promise<FileIdentityStat>): Promise<FileIdentityStat>;
|
|
@@ -57,7 +57,9 @@ import { capturePolicyAwareNativeParent } from "./native-policy-parent.js";
|
|
|
57
57
|
import { openNativeParentAdmission, openNativeRootAdmission } from "./native-parent-admission.js";
|
|
58
58
|
import { assertNativeStaging, writeNativeStage } from "./native-staged-file.js";
|
|
59
59
|
import { captureNativeFdClose } from "./native-binding.js";
|
|
60
|
-
|
|
60
|
+
import { cachedNoReplaceUnavailable } from "./native-noreplace.js";
|
|
61
|
+
import { isFsSafeNativeRequired } from "./native-config.js";
|
|
62
|
+
export async function runPinnedWriteNative(binding, params, fallback) {
|
|
61
63
|
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
62
64
|
try {
|
|
63
65
|
const closeFd = captureNativeFdClose(binding);
|
|
@@ -126,9 +128,17 @@ export async function runPinnedWriteNative(binding, params) {
|
|
|
126
128
|
windowsOwnsDirectories = true;
|
|
127
129
|
return await runPinnedWriteWindows(binding, params, root, parentFd, verificationGuard);
|
|
128
130
|
}
|
|
131
|
+
if (params.overwrite === false && !(params.input.kind === "buffer" && params.input.stageBeforePublish === false)) {
|
|
132
|
+
const unavailable = cachedNoReplaceUnavailable(binding, parentFd);
|
|
133
|
+
if (unavailable) {
|
|
134
|
+
if (isFsSafeNativeRequired() || (params.input.kind === "file" && params.input.clone === "always"))
|
|
135
|
+
throw unavailable;
|
|
136
|
+
return await fallback(params.input);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
129
139
|
const ownedParent = parentFd;
|
|
130
140
|
parentFd = undefined;
|
|
131
|
-
return await writeNativeStage(binding, ownedParent, closeFd, directory, params, verificationGuard);
|
|
141
|
+
return await writeNativeStage(binding, ownedParent, closeFd, directory, params, verificationGuard, fallback);
|
|
132
142
|
}
|
|
133
143
|
catch (error) {
|
|
134
144
|
admissionFailure = { error };
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { NATIVE_NOREPLACE_UNSUPPORTED } from "./native-noreplace.js";
|
|
1
2
|
export const NATIVE_RENAME_SOURCE_IDENTITY_MISMATCH = "FS_SAFE_INTERNAL_RENAME_SOURCE_IDENTITY_MISMATCH";
|
|
2
3
|
export function classifyNativeRenameFailure(error) {
|
|
3
4
|
try {
|
|
4
|
-
|
|
5
|
+
const code = error?.code;
|
|
6
|
+
if (code === NATIVE_RENAME_SOURCE_IDENTITY_MISMATCH || code === NATIVE_NOREPLACE_UNSUPPORTED) {
|
|
5
7
|
return "uncommitted";
|
|
6
8
|
}
|
|
7
9
|
}
|
|
@@ -25,7 +25,7 @@ declare class NativeStagedFile implements StagedFile {
|
|
|
25
25
|
#private;
|
|
26
26
|
constructor(owner: NativeStageOwner);
|
|
27
27
|
static create(owner: NativeStageOwner, input: PinnedWriteInput, maxBytes?: number): Promise<NativeStagedFile>;
|
|
28
|
-
static write(binding: NativeStagingBinding, parentFd: number, closeParentFd: (fd: number) => void, directory: StagedFileReceipt["directory"], params: PinnedWriteParams, parentGuard: AnyAsyncDirectoryGuard): Promise<FileIdentityStat>;
|
|
28
|
+
static write(binding: NativeStagingBinding, parentFd: number, closeParentFd: (fd: number) => void, directory: StagedFileReceipt["directory"], params: PinnedWriteParams, parentGuard: AnyAsyncDirectoryGuard, fallback: (input: PinnedWriteInput) => Promise<FileIdentityStat>): Promise<FileIdentityStat>;
|
|
29
29
|
get receipt(): StagedFileReceipt;
|
|
30
30
|
assertCurrent(): Promise<void>;
|
|
31
31
|
publish(basename: string, options: {
|