@openclaw/openshell-sandbox 2026.10.1-beta.2 → 2026.10.5-beta.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/dist/index.js +69 -111
- package/node_modules/@openclaw/fs-safe/CHANGELOG.md +91 -0
- package/node_modules/@openclaw/fs-safe/README.md +1 -1
- 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-durability.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/archive-entry.js +7 -3
- package/node_modules/@openclaw/fs-safe/dist/archive-merge.js +4 -4
- package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-read.js +5 -4
- package/node_modules/@openclaw/fs-safe/dist/archive-staging.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/archive-staging.js +22 -38
- package/node_modules/@openclaw/fs-safe/dist/copy-path-admission.d.ts +17 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-path-admission.js +117 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.d.ts +38 -0
- package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.js +195 -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/file-lock-sync-root-acquire.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-acquire.js +57 -11
- package/node_modules/@openclaw/fs-safe/dist/guarded-mutation.js +5 -5
- package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +52 -5
- package/node_modules/@openclaw/fs-safe/dist/guest.js +1 -1
- 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 +4 -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-operations.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/native-operations.js +22 -26
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.js +5 -1
- 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 +52 -6
- package/node_modules/@openclaw/fs-safe/dist/permission-exec.js +2 -1
- 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-types.d.ts +6 -1
- package/node_modules/@openclaw/fs-safe/dist/pinned-write.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/private-temp-workspace.js +2 -2
- 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.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/publish-file.js +35 -2
- package/node_modules/@openclaw/fs-safe/dist/replace-directory.js +6 -5
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.js +1 -4
- package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.js +6 -0
- package/node_modules/@openclaw/fs-safe/dist/root-create-input.d.ts +15 -1
- package/node_modules/@openclaw/fs-safe/dist/root-create-input.js +4 -0
- package/node_modules/@openclaw/fs-safe/dist/root-directory-entry.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +6 -12
- package/node_modules/@openclaw/fs-safe/dist/root-errors.js +25 -4
- package/node_modules/@openclaw/fs-safe/dist/root-impl.js +18 -18
- 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/root-paths.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/root-walk.js +4 -4
- package/node_modules/@openclaw/fs-safe/dist/root-write-verification.d.ts +10 -1
- package/node_modules/@openclaw/fs-safe/dist/root-write-verification.js +20 -0
- package/node_modules/@openclaw/fs-safe/dist/secure-temp-dir.js +19 -7
- package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +18 -3
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-acquire.js +25 -3
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-admission.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-retained.d.ts +14 -0
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-retained.js +187 -0
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock.js +15 -1
- package/node_modules/@openclaw/fs-safe/dist/staged-file-settlement.js +4 -1
- package/node_modules/@openclaw/fs-safe/dist/temp-directory-diagnostics.d.ts +10 -0
- package/node_modules/@openclaw/fs-safe/dist/temp-directory-diagnostics.js +29 -0
- package/node_modules/@openclaw/fs-safe/dist/temp-target.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-admission.js +6 -6
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.d.ts +4 -4
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.js +17 -16
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-descriptor.js +20 -28
- 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/dist/walk.js +2 -13
- package/node_modules/@openclaw/fs-safe/dist/watch-native.d.ts +2 -2
- package/node_modules/@openclaw/fs-safe/dist/watch-native.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/watch-rescan.js +3 -9
- package/node_modules/@openclaw/fs-safe/dist/watch.js +2 -3
- package/node_modules/@openclaw/fs-safe/docs/advanced.md +1 -0
- package/node_modules/@openclaw/fs-safe/docs/archive.md +5 -1
- package/node_modules/@openclaw/fs-safe/docs/atomic.md +11 -3
- package/node_modules/@openclaw/fs-safe/docs/contributing.md +1 -1
- package/node_modules/@openclaw/fs-safe/docs/copy.md +109 -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/guest.md +21 -4
- 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/path-scope.md +4 -0
- package/node_modules/@openclaw/fs-safe/docs/public-api.md +3 -1
- package/node_modules/@openclaw/fs-safe/docs/sidecar-lock.md +46 -8
- package/node_modules/@openclaw/fs-safe/docs/staged-file.md +10 -3
- package/node_modules/@openclaw/fs-safe/docs/temp.md +12 -0
- package/node_modules/@openclaw/fs-safe/docs/watch.md +6 -3
- package/node_modules/@openclaw/fs-safe/docs/writing.md +40 -11
- package/node_modules/@openclaw/fs-safe/package.json +16 -16
- 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
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-arbitration.d.ts +0 -17
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-arbitration.js +0 -66
- package/node_modules/@openclaw/fs-safe/dist/root-move-preflight.d.ts +0 -7
- package/node_modules/@openclaw/fs-safe/dist/root-move-preflight.js +0 -16
|
@@ -32,6 +32,16 @@ export const GUEST_FILESYSTEM_CREATE_STAGING_PYTHON = [
|
|
|
32
32
|
].join("\n");
|
|
33
33
|
export const GUEST_FILESYSTEM_RENAME_NO_REPLACE_PYTHON = [
|
|
34
34
|
"def rename_no_replace(src_parent_fd, src_basename, dst_parent_fd, dst_basename):",
|
|
35
|
+
" import stat",
|
|
36
|
+
" source = os.lstat(src_basename, dir_fd=src_parent_fd)",
|
|
37
|
+
" kind = stat.S_IFMT(source.st_mode)",
|
|
38
|
+
" expected = (source.st_dev, source.st_ino, kind)",
|
|
39
|
+
" def verify(parent_fd, basename, links):",
|
|
40
|
+
" current = os.lstat(basename, dir_fd=parent_fd)",
|
|
41
|
+
" if (current.st_dev, current.st_ino, stat.S_IFMT(current.st_mode)) != expected:",
|
|
42
|
+
" raise OSError(errno.ESTALE, 'no-replace publication identity changed', basename)",
|
|
43
|
+
" if kind == stat.S_IFREG and current.st_nlink != links:",
|
|
44
|
+
" raise OSError(errno.ESTALE, 'no-replace publication link count changed', basename)",
|
|
35
45
|
" libc = ctypes.CDLL(None, use_errno=True)",
|
|
36
46
|
" is_linux = sys.platform.startswith('linux')",
|
|
37
47
|
" if is_linux:",
|
|
@@ -62,6 +72,31 @@ export const GUEST_FILESYSTEM_RENAME_NO_REPLACE_PYTHON = [
|
|
|
62
72
|
" raise OSError(error_code, os.strerror(error_code), dst_basename)",
|
|
63
73
|
" elif not is_linux:",
|
|
64
74
|
" raise OSError(errno.ENOSYS, 'atomic no-replace rename is unavailable')",
|
|
75
|
+
" # Only a definitive rejection of the fixed flags reaches this fallback.",
|
|
76
|
+
" if kind not in (stat.S_IFDIR, stat.S_IFREG):",
|
|
77
|
+
" raise OSError(errno.EINVAL, 'no-replace fallback requires a file or directory', src_basename)",
|
|
78
|
+
" verify(src_parent_fd, src_basename, 1)",
|
|
79
|
+
" try:",
|
|
80
|
+
" os.lstat(dst_basename, dir_fd=dst_parent_fd)",
|
|
81
|
+
" except FileNotFoundError:",
|
|
82
|
+
" pass",
|
|
83
|
+
" else:",
|
|
84
|
+
" raise FileExistsError(errno.EEXIST, os.strerror(errno.EEXIST), dst_basename)",
|
|
85
|
+
" if kind == stat.S_IFDIR:",
|
|
86
|
+
" verify(src_parent_fd, src_basename, 1)",
|
|
87
|
+
" # A directory rename can replace only an EMPTY directory raced into",
|
|
88
|
+
" # the absent target. Non-empty directories and files are refused.",
|
|
89
|
+
" # The slash enforces source directory type at the syscall, even if",
|
|
90
|
+
" # a peer substitutes a file/symlink after verification. Identity",
|
|
91
|
+
" # checks are not compare-and-swap; verify publication afterward.",
|
|
92
|
+
" try:",
|
|
93
|
+
" os.rename(src_basename + '/', dst_basename, src_dir_fd=src_parent_fd, dst_dir_fd=dst_parent_fd)",
|
|
94
|
+
" except OSError as error:",
|
|
95
|
+
" if error.errno in (errno.ENOTEMPTY, errno.ENOTDIR, errno.EEXIST):",
|
|
96
|
+
" raise FileExistsError(errno.EEXIST, os.strerror(errno.EEXIST), dst_basename) from error",
|
|
97
|
+
" raise",
|
|
98
|
+
" verify(dst_parent_fd, dst_basename, 1)",
|
|
99
|
+
" return",
|
|
65
100
|
" os.link(",
|
|
66
101
|
" src_basename,",
|
|
67
102
|
" dst_basename,",
|
|
@@ -69,12 +104,19 @@ export const GUEST_FILESYSTEM_RENAME_NO_REPLACE_PYTHON = [
|
|
|
69
104
|
" dst_dir_fd=dst_parent_fd,",
|
|
70
105
|
" follow_symlinks=False,",
|
|
71
106
|
" )",
|
|
107
|
+
" # Only this exact two-name interval permits nlink=2. Never roll back the",
|
|
108
|
+
" # target on failure: it may be the only remaining link to the source.",
|
|
109
|
+
" verify(dst_parent_fd, dst_basename, 2)",
|
|
110
|
+
" verify(src_parent_fd, src_basename, 2)",
|
|
111
|
+
" os.unlink(src_basename, dir_fd=src_parent_fd)",
|
|
112
|
+
" verify(dst_parent_fd, dst_basename, 1)",
|
|
72
113
|
].join("\n");
|
|
73
114
|
export const GUEST_FILESYSTEM_CREATE_EXCLUSIVE_PYTHON = [
|
|
74
115
|
"def create_exclusive(parent_fd, basename, stdin_buffer):",
|
|
75
116
|
" staging_fd = None",
|
|
76
117
|
" staging_name = None",
|
|
77
118
|
" temp_fd = None",
|
|
119
|
+
" temp_inode = None",
|
|
78
120
|
" temp_name = 'payload'",
|
|
79
121
|
" try:",
|
|
80
122
|
" try:",
|
|
@@ -85,6 +127,7 @@ export const GUEST_FILESYSTEM_CREATE_EXCLUSIVE_PYTHON = [
|
|
|
85
127
|
" raise FileExistsError(errno.EEXIST, os.strerror(errno.EEXIST), basename)",
|
|
86
128
|
" staging_name, staging_fd = create_staging_dir(parent_fd)",
|
|
87
129
|
" temp_fd = os.open(temp_name, WRITE_FLAGS, 0o600, dir_fd=staging_fd)",
|
|
130
|
+
" temp_inode = inode_identity(os.fstat(temp_fd))",
|
|
88
131
|
" while True:",
|
|
89
132
|
" chunk = stdin_buffer.read(65536)",
|
|
90
133
|
" if not chunk:",
|
|
@@ -92,8 +135,7 @@ export const GUEST_FILESYSTEM_CREATE_EXCLUSIVE_PYTHON = [
|
|
|
92
135
|
" write_all(temp_fd, chunk)",
|
|
93
136
|
" # exclusive create payload is durable before publication",
|
|
94
137
|
" os.fsync(temp_fd)",
|
|
95
|
-
"
|
|
96
|
-
" # Publish with a native atomic no-replace rename.",
|
|
138
|
+
" # Publish with no-replace rename or the checked file fallback.",
|
|
97
139
|
" rename_no_replace(staging_fd, temp_name, parent_fd, basename)",
|
|
98
140
|
" target_stat = os.lstat(basename, dir_fd=parent_fd)",
|
|
99
141
|
" if temp_inode != inode_identity(target_stat):",
|
|
@@ -105,14 +147,19 @@ export const GUEST_FILESYSTEM_CREATE_EXCLUSIVE_PYTHON = [
|
|
|
105
147
|
" if staging_fd is not None:",
|
|
106
148
|
" # Cleanup stays relative to the private pinned directory, so a",
|
|
107
149
|
" # parent-path substitution cannot redirect payload deletion.",
|
|
150
|
+
" payload_removed = False",
|
|
108
151
|
" try:",
|
|
109
|
-
" os.
|
|
152
|
+
" current_payload = os.lstat(temp_name, dir_fd=staging_fd)",
|
|
153
|
+
" # Preserve ambiguous or partially published link pairs.",
|
|
154
|
+
" if temp_inode == inode_identity(current_payload) and current_payload.st_nlink == 1:",
|
|
155
|
+
" os.unlink(temp_name, dir_fd=staging_fd)",
|
|
156
|
+
" payload_removed = True",
|
|
110
157
|
" except FileNotFoundError:",
|
|
111
|
-
"
|
|
158
|
+
" payload_removed = True",
|
|
112
159
|
" staging_identity = entry_identity(os.fstat(staging_fd))",
|
|
113
160
|
" os.close(staging_fd)",
|
|
114
161
|
" staging_removed = False",
|
|
115
|
-
" if staging_name is not None:",
|
|
162
|
+
" if staging_name is not None and payload_removed:",
|
|
116
163
|
" try:",
|
|
117
164
|
" current_staging = os.lstat(staging_name, dir_fd=parent_fd)",
|
|
118
165
|
" if same_identity(staging_identity, current_staging):",
|
|
@@ -395,7 +395,7 @@ export const GUEST_FILESYSTEM_PYTHON = [
|
|
|
395
395
|
" break",
|
|
396
396
|
" write_all(temp_fd, chunk)",
|
|
397
397
|
" try:",
|
|
398
|
-
" os.fchmod(temp_fd, stat.S_IMODE(
|
|
398
|
+
" os.fchmod(temp_fd, stat.S_IMODE(src_file_stat.st_mode))",
|
|
399
399
|
" except AttributeError:",
|
|
400
400
|
" pass",
|
|
401
401
|
" os.fsync(temp_fd)",
|
|
@@ -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";
|
|
@@ -139,6 +139,7 @@ export interface NativeBinding {
|
|
|
139
139
|
renameReplaceWithIdentity?(sourceParent: number, source: string, targetParent: number, target: string, dev: bigint, ino: bigint): void;
|
|
140
140
|
/** Windows-only, private handle custody; no borrowed/runtime descriptors. */
|
|
141
141
|
retainWindowsFile?(directory: string, basename: string, parentDev: bigint, parentIno: bigint, dev: bigint, ino: bigint, size: bigint, mtimeNs: bigint, ctimeNs: bigint, sha256: string, maxBytes: number): NativeRetainedFile;
|
|
142
|
+
retainWindowsSidecar?(parentFd: number, directory: string, basename: string, parentDev: bigint, parentIno: bigint, dev: bigint, ino: bigint, size: bigint, sha256: string, maxBytes: number): NativeRetainedFile;
|
|
142
143
|
watchRegister?(root: string, limit: number, callback: (batch: import("./watch-native.js").NativeWatchWireBatch) => void, persistent: boolean): number;
|
|
143
144
|
watchConfigure?(id: number, anchors: string[], exclusions: string[]): void;
|
|
144
145
|
watchEntries?(id: number, entries: import("./watch-native.js").NativeWatchEntry[]): {
|
|
@@ -203,6 +204,7 @@ export interface NativeBinding {
|
|
|
203
204
|
stagedFileMatches?(parentFd: number, basename: string, fileFd: number): boolean;
|
|
204
205
|
removeStagedFile?(parentFd: number, basename: string, fileFd: number): "removed" | "name-absent" | "preserved";
|
|
205
206
|
copyFileExclusive?(sourceFd: number, parentFd: number, basename: string, clone: CopyCloneMode, maxBytes: number | undefined, signal: AbortSignal | undefined): Promise<NativeFileCopyResult>;
|
|
207
|
+
copyFileExclusiveSync?(sourceFd: number, parentFd: number, basename: string, clone: CopyCloneMode, maxBytes: number | undefined): NativeFileCopyResult;
|
|
206
208
|
cloneFileExclusive(sourceFd: number, targetRootFd: number, targetRelPath: string): number;
|
|
207
209
|
copyFileRangeExclusive(sourceFd: number, targetRootFd: number, targetRelPath: string): Promise<NativeCopyResult>;
|
|
208
210
|
createPrivateDirectory(path: string): void;
|
|
@@ -235,6 +237,8 @@ export interface NativeBinding {
|
|
|
235
237
|
entryPublicationFilesystem?(parentFd: number): string;
|
|
236
238
|
publishRetainedEntryNoReplace?(sourceParentFd: number, sourceBasename: string, sourceFd: number, targetParentFd: number, targetBasename: string): NativePublicationTransition;
|
|
237
239
|
renameNoReplace(...args: NativeTwoPathArgs): void;
|
|
240
|
+
/** Linux auto-mode fallback through already-admitted direct-child parents. */
|
|
241
|
+
moveNoReplaceFallback?(...args: [...paths: NativeTwoPathArgs, expectedSourceDev: bigint, expectedSourceIno: bigint]): void;
|
|
238
242
|
/** Identity-fenced retained-directory rename capability. */
|
|
239
243
|
renameNoReplaceWithIdentity?(...args: [...paths: NativeTwoPathArgs, expectedSourceDev: bigint, expectedSourceIno: bigint]): void;
|
|
240
244
|
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
|
+
}
|
|
@@ -5,7 +5,6 @@ export type NativeFileHandle = {
|
|
|
5
5
|
writeFile(data: string | Buffer, encoding?: BufferEncoding): Promise<void>;
|
|
6
6
|
};
|
|
7
7
|
export declare function nativeOpenFlags(flags: number): number;
|
|
8
|
-
export declare function writeNativeFd(fd: number, data: Buffer): void;
|
|
9
8
|
export declare function removeNativeCreatedFileIfStillPinned(params: {
|
|
10
9
|
parentPath: string;
|
|
11
10
|
parentFd: number;
|
|
@@ -10,31 +10,6 @@ export function nativeOpenFlags(flags) {
|
|
|
10
10
|
(closeOnExec ?? 0) |
|
|
11
11
|
(typeof fsSync.constants.O_NOFOLLOW === "number" ? fsSync.constants.O_NOFOLLOW : 0));
|
|
12
12
|
}
|
|
13
|
-
export function writeNativeFd(fd, data) {
|
|
14
|
-
let offset = 0;
|
|
15
|
-
while (offset < data.byteLength) {
|
|
16
|
-
const written = fsSync.writeSync(fd, data, offset, data.byteLength - offset);
|
|
17
|
-
if (written <= 0) {
|
|
18
|
-
throw Object.assign(new Error("native file write made no progress"), { code: "EIO" });
|
|
19
|
-
}
|
|
20
|
-
offset += written;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
function wrapNativeFd(fd, closeFd) {
|
|
24
|
-
let open = true;
|
|
25
|
-
return {
|
|
26
|
-
fd,
|
|
27
|
-
async close() {
|
|
28
|
-
if (open) {
|
|
29
|
-
open = false;
|
|
30
|
-
closeFd(fd);
|
|
31
|
-
}
|
|
32
|
-
},
|
|
33
|
-
async writeFile(data, encoding) {
|
|
34
|
-
writeNativeFd(fd, Buffer.isBuffer(data) ? data : Buffer.from(data, encoding ?? "utf8"));
|
|
35
|
-
},
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
13
|
export function removeNativeCreatedFileIfStillPinned(params) {
|
|
39
14
|
if (!params.created) {
|
|
40
15
|
return;
|
|
@@ -86,7 +61,28 @@ export async function createNativeExclusiveFile(targetPath, mode) {
|
|
|
86
61
|
fd = opened.fd;
|
|
87
62
|
fsSync.fchmodSync(fd, mode);
|
|
88
63
|
created = fsSync.fstatSync(fd, { bigint: true });
|
|
89
|
-
|
|
64
|
+
const ownedFd = fd;
|
|
65
|
+
let open = true;
|
|
66
|
+
return {
|
|
67
|
+
fd: ownedFd,
|
|
68
|
+
async close() {
|
|
69
|
+
if (open) {
|
|
70
|
+
open = false;
|
|
71
|
+
closeFd(ownedFd);
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
async writeFile(data, encoding) {
|
|
75
|
+
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data, encoding ?? "utf8");
|
|
76
|
+
let offset = 0;
|
|
77
|
+
while (offset < buffer.byteLength) {
|
|
78
|
+
const written = fsSync.writeSync(ownedFd, buffer, offset, buffer.byteLength - offset);
|
|
79
|
+
if (written <= 0) {
|
|
80
|
+
throw Object.assign(new Error("native file write made no progress"), { code: "EIO" });
|
|
81
|
+
}
|
|
82
|
+
offset += written;
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
};
|
|
90
86
|
}
|
|
91
87
|
catch (error) {
|
|
92
88
|
if (fd !== undefined) {
|
|
@@ -188,7 +188,11 @@ export async function runPinnedWriteWindows(binding, params, root, parentFd, par
|
|
|
188
188
|
if (params.sync !== false)
|
|
189
189
|
syncFileBestEffortSync(parentFd);
|
|
190
190
|
// Verification follows publication and final chmod, outside rollback handling.
|
|
191
|
-
|
|
191
|
+
if (params.overwrite === false && params.input.kind === "buffer" && params.input.stageBeforePublish === false) {
|
|
192
|
+
await params.verifyPublished?.(targetFd, verificationIdentity, parentGuard, { fd: parentFd, binding });
|
|
193
|
+
}
|
|
194
|
+
else
|
|
195
|
+
await params.verifyPublished?.(targetFd, verificationIdentity, parentGuard);
|
|
192
196
|
completed = true;
|
|
193
197
|
return { dev: targetIdentity.dev, ino: targetIdentity.ino };
|
|
194
198
|
}
|
|
@@ -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: {
|
|
@@ -59,12 +59,14 @@ import path from "node:path";
|
|
|
59
59
|
import { FsSafeError } from "./errors.js";
|
|
60
60
|
import { assertSynchronousCallbackResult, isMutationAuthorityError } from "./mutation-authority.js";
|
|
61
61
|
import { captureNativeFdClose } from "./native-binding.js";
|
|
62
|
-
import { writePinnedInput } from "./pinned-write-input.js";
|
|
62
|
+
import { replayPinnedInput, writePinnedInput } from "./pinned-write-input.js";
|
|
63
63
|
import { assertNativeCopyCompleted, createNativeCopyFile } from "./copy-file-input.js";
|
|
64
64
|
import { assertStagedDirectoryCurrent, openStagedDirectory } from "./staged-directory.js";
|
|
65
65
|
import { assertFinalSymlinkRejected } from "./root-symlink-policy.js";
|
|
66
66
|
import { classifyNativeRenameFailure } from "./native-rename-outcome.js";
|
|
67
67
|
import { createStagedFileReceipt, stagedFailure } from "./staged-file-settlement.js";
|
|
68
|
+
import { noReplaceUnavailable, rememberNoReplaceUnavailable } from "./native-noreplace.js";
|
|
69
|
+
import { isFsSafeNativeRequired } from "./native-config.js";
|
|
68
70
|
export function assertNativeStaging(binding) {
|
|
69
71
|
if ([
|
|
70
72
|
binding.closeOwnedFd,
|
|
@@ -93,6 +95,9 @@ class NativeStagedFile {
|
|
|
93
95
|
#state = { status: "open", publication: NOT_PUBLISHED };
|
|
94
96
|
#receipt;
|
|
95
97
|
#rejectFinalSymlink = false;
|
|
98
|
+
#unsupportedNoReplace;
|
|
99
|
+
#fallbackCleanup;
|
|
100
|
+
#fallbackCleanupError;
|
|
96
101
|
constructor(owner) {
|
|
97
102
|
const name = owner.name ?? `.fs-safe-${randomUUID()}.tmp`;
|
|
98
103
|
assertBasename(name, owner.portableNames !== false);
|
|
@@ -121,7 +126,7 @@ class NativeStagedFile {
|
|
|
121
126
|
await staged.#prepare(input, maxBytes);
|
|
122
127
|
return staged;
|
|
123
128
|
}
|
|
124
|
-
static async write(binding, parentFd, closeParentFd, directory, params, parentGuard) {
|
|
129
|
+
static async write(binding, parentFd, closeParentFd, directory, params, parentGuard, fallback) {
|
|
125
130
|
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
126
131
|
try {
|
|
127
132
|
const exclusive = params.overwrite === false && params.input.kind === "buffer" && params.input.stageBeforePublish === false;
|
|
@@ -143,12 +148,39 @@ class NativeStagedFile {
|
|
|
143
148
|
if (staged.#verifyMode)
|
|
144
149
|
staged.#assertCurrent();
|
|
145
150
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
151
|
+
let published;
|
|
152
|
+
try {
|
|
153
|
+
published = exclusive
|
|
154
|
+
? staged.#completePublication(params.basename, false, params.onPublished)
|
|
155
|
+
: await staged.publish(params.basename, { overwrite: params.overwrite !== false }, params.onPublished);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
if (!staged.#unsupportedNoReplace || isFsSafeNativeRequired() ||
|
|
159
|
+
(params.input.kind === "file" && params.input.clone === "always"))
|
|
160
|
+
throw error;
|
|
161
|
+
// Remove only our identity-known stage before re-admitting the JS write.
|
|
162
|
+
// Keep its descriptor alive to replay a one-shot stream in bounded chunks.
|
|
163
|
+
staged.#fallbackCleanup = "failed";
|
|
164
|
+
let removed;
|
|
165
|
+
try {
|
|
166
|
+
removed = binding.removeStagedFile(parentFd, staged.#owner.name, staged.#file());
|
|
167
|
+
staged.#fallbackCleanup = removed;
|
|
168
|
+
}
|
|
169
|
+
catch (cleanupError) {
|
|
170
|
+
staged.#fallbackCleanupError = { error: cleanupError };
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
if (removed !== "removed" && removed !== "name-absent")
|
|
174
|
+
throw error;
|
|
175
|
+
rememberNoReplaceUnavailable(binding, parentFd, staged.#unsupportedNoReplace);
|
|
176
|
+
return await fallback(replayPinnedInput(staged.#file(), params.input));
|
|
177
|
+
}
|
|
149
178
|
const identity = published.staged.identity;
|
|
150
179
|
try {
|
|
151
|
-
|
|
180
|
+
if (exclusive)
|
|
181
|
+
await params.verifyPublished?.(staged.#file(), identity, parentGuard, { fd: parentFd, binding });
|
|
182
|
+
else
|
|
183
|
+
await params.verifyPublished?.(staged.#file(), identity, parentGuard);
|
|
152
184
|
}
|
|
153
185
|
catch (error) {
|
|
154
186
|
if (params.overwrite === false && params.input.kind !== "file" && params.input.stageBeforePublish === true) {
|
|
@@ -321,6 +353,15 @@ class NativeStagedFile {
|
|
|
321
353
|
catch (error) {
|
|
322
354
|
// Record before inspecting metadata, whose getters can reenter cleanup.
|
|
323
355
|
state.publication = Object.freeze({ status: "indeterminate", basename, overwrite });
|
|
356
|
+
// Linux VFS and rename(2): unsupported flags return EINVAL; ENOSYS
|
|
357
|
+
// means no syscall. A regular file renamed to a distinct sibling cannot
|
|
358
|
+
// hit the directory-into-itself EINVAL case. Nothing was published.
|
|
359
|
+
const unavailable = !overwrite ? noReplaceUnavailable(error, "publication", true) : undefined;
|
|
360
|
+
if (unavailable && this.#state === state && state.status === "open") {
|
|
361
|
+
state.publication = NOT_PUBLISHED;
|
|
362
|
+
this.#unsupportedNoReplace = unavailable;
|
|
363
|
+
throw unavailable;
|
|
364
|
+
}
|
|
324
365
|
// Only explicit pre-dispatch provenance can rule out a committed rename.
|
|
325
366
|
if (classifyNativeRenameFailure(error) === "uncommitted") {
|
|
326
367
|
state.publication = NOT_PUBLISHED;
|
|
@@ -377,6 +418,11 @@ class NativeStagedFile {
|
|
|
377
418
|
if (state.publication.status === "indeterminate") {
|
|
378
419
|
outcome = "preserved";
|
|
379
420
|
}
|
|
421
|
+
else if (this.#fallbackCleanup) {
|
|
422
|
+
outcome = this.#fallbackCleanup;
|
|
423
|
+
if (this.#fallbackCleanupError)
|
|
424
|
+
errors.push(this.#fallbackCleanupError.error);
|
|
425
|
+
}
|
|
380
426
|
else if (state.publication.status === "not-published" && state.fileFd !== undefined) {
|
|
381
427
|
try {
|
|
382
428
|
outcome = this.#owner.binding.removeStagedFile(this.#owner.parentFd, this.#owner.name, state.fileFd);
|
|
@@ -5,7 +5,8 @@ import { formatErrorDetail } from "./error-detail.js";
|
|
|
5
5
|
const execFileAsync = promisify(execFile);
|
|
6
6
|
export const DEFAULT_PERMISSION_EXEC_TIMEOUT_MS = 30_000;
|
|
7
7
|
export function formatPermissionErrorDetail(value) {
|
|
8
|
-
|
|
8
|
+
// Escaping cannot shrink input; 401 code units decide the 400-character excerpt.
|
|
9
|
+
const formatted = formatErrorDetail(value.slice(0, 401));
|
|
9
10
|
return formatted.length > 400 ? `${formatted.slice(0, 399)}…` : formatted;
|
|
10
11
|
}
|
|
11
12
|
const MAX_CAUGHT_FAILURE_PROTOTYPES = 4;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import type { FileHandle } from "node:fs/promises";
|
|
2
2
|
import type { PinnedWriteInput } from "./pinned-write-types.js";
|
|
3
|
+
/** Replay a consumed stream from an owned, readable stage descriptor. */
|
|
4
|
+
export declare function replayPinnedInput(fd: number, input: PinnedWriteInput): PinnedWriteInput;
|
|
3
5
|
export declare function writePinnedInput(target: FileHandle | number, input: PinnedWriteInput, maxBytes?: number, assertBeforeMutation?: () => void): Promise<void>;
|
|
@@ -1,8 +1,25 @@
|
|
|
1
|
+
import { readSync } from "node:fs";
|
|
1
2
|
import { types } from "node:util";
|
|
2
3
|
import { snapshotByteView } from "./byte-view.js";
|
|
3
4
|
import { writeCopyFileToFd } from "./copy-file-input.js";
|
|
4
5
|
import { FsSafeError } from "./errors.js";
|
|
5
6
|
import { writeAllToFile } from "./write-file-handle.js";
|
|
7
|
+
/** Replay a consumed stream from an owned, readable stage descriptor. */
|
|
8
|
+
export function replayPinnedInput(fd, input) {
|
|
9
|
+
if (input.kind !== "stream")
|
|
10
|
+
return input;
|
|
11
|
+
return { kind: "stream", stageBeforePublish: true, stream: (async function* () {
|
|
12
|
+
let position = 0;
|
|
13
|
+
const buffer = Buffer.alloc(64 * 1024);
|
|
14
|
+
for (;;) {
|
|
15
|
+
const bytes = readSync(fd, buffer, 0, buffer.length, position);
|
|
16
|
+
if (!bytes)
|
|
17
|
+
return;
|
|
18
|
+
position += bytes;
|
|
19
|
+
yield buffer.subarray(0, bytes);
|
|
20
|
+
}
|
|
21
|
+
})() };
|
|
22
|
+
}
|
|
6
23
|
const isUint8Array = types.isUint8Array;
|
|
7
24
|
function streamChunkBytes(chunk) {
|
|
8
25
|
if (Buffer.isBuffer(chunk))
|
|
@@ -2,6 +2,11 @@ import type { CopyFileInput } from "./copy-file-input.js";
|
|
|
2
2
|
import type { AnyAsyncDirectoryGuard } from "./directory-guard.js";
|
|
3
3
|
import type { FileIdentityStat } from "./file-identity.js";
|
|
4
4
|
import type { MutationDirectoryObservation } from "./pinned-mutation-observation.js";
|
|
5
|
+
import type { NativeBinding } from "./native-binding.js";
|
|
6
|
+
export type NativeWriteParent = Readonly<{
|
|
7
|
+
fd: number;
|
|
8
|
+
binding: NativeBinding;
|
|
9
|
+
}>;
|
|
5
10
|
export type PinnedWriteInput = {
|
|
6
11
|
kind: "buffer";
|
|
7
12
|
data: string | Buffer;
|
|
@@ -64,5 +69,5 @@ export type PinnedWriteParams = {
|
|
|
64
69
|
mutationAdmission?: PinnedWriteMutationAdmission;
|
|
65
70
|
onRenameIdentityMismatch?: "verify-content";
|
|
66
71
|
onPublished?: (identity: PublishedWriteIdentity) => void;
|
|
67
|
-
verifyPublished?: (fd: number, identity: PublishedWriteIdentity, parentGuard: AnyAsyncDirectoryGuard) => Promise<void>;
|
|
72
|
+
verifyPublished?: (fd: number, identity: PublishedWriteIdentity, parentGuard: AnyAsyncDirectoryGuard, nativeParent?: NativeWriteParent) => Promise<void>;
|
|
68
73
|
};
|
|
@@ -90,7 +90,7 @@ export async function runPinnedWriteHelper(params) {
|
|
|
90
90
|
}
|
|
91
91
|
const native = getNativeBinding();
|
|
92
92
|
if (native) {
|
|
93
|
-
return await runPinnedWriteNative(native, normalizedParams);
|
|
93
|
+
return await runPinnedWriteNative(native, normalizedParams, input => runPinnedWriteFallback({ ...normalizedParams, input }));
|
|
94
94
|
}
|
|
95
95
|
return await runPinnedWriteFallback(normalizedParams);
|
|
96
96
|
}
|
|
@@ -166,7 +166,7 @@ async function createTempWorkspace(options, scopedPrefix = false) {
|
|
|
166
166
|
else
|
|
167
167
|
admission.assertCurrent();
|
|
168
168
|
const stat = inspectDirectoryIdentitySync(dir);
|
|
169
|
-
const needsModeInitialization = validateInitialTempWorkspaceChild(stat, admission.ownerUid, dirMode);
|
|
169
|
+
const needsModeInitialization = validateInitialTempWorkspaceChild(stat, admission.ownerUid, dirMode, dir);
|
|
170
170
|
// Retain while the child still has its private creation mode so an
|
|
171
171
|
// explicit dirMode such as 0 cannot make identity descriptor acquisition fail.
|
|
172
172
|
retainedChild = TempWorkspaceRetainedChild.retain(dir, stat);
|
|
@@ -261,7 +261,7 @@ function createTempWorkspaceSync(options, scopedPrefix = false) {
|
|
|
261
261
|
else {
|
|
262
262
|
stat = inspectDirectoryIdentitySync(dir);
|
|
263
263
|
}
|
|
264
|
-
const needsModeInitialization = validateInitialTempWorkspaceChild(stat, admission.ownerUid, dirMode);
|
|
264
|
+
const needsModeInitialization = validateInitialTempWorkspaceChild(stat, admission.ownerUid, dirMode, dir);
|
|
265
265
|
retainedChild ??= TempWorkspaceRetainedChild.retain(dir, stat);
|
|
266
266
|
if (needsModeInitialization) {
|
|
267
267
|
retainedChild.initializeModeSync(dirMode, admission.ownerUid, admission.assertCurrent);
|