@openclaw/fs-safe 0.14.0 → 0.15.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 +50 -0
- package/README.md +10 -4
- package/dist/advanced.d.ts +1 -1
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +1 -1
- package/dist/archive-merge.d.ts.map +1 -1
- package/dist/archive-merge.js +113 -46
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +4 -4
- package/dist/archive-zip-directory.d.ts +4 -0
- package/dist/archive-zip-directory.d.ts.map +1 -1
- package/dist/archive-zip-directory.js +2 -0
- package/dist/archive-zip-entry.d.ts +6 -2
- package/dist/archive-zip-entry.d.ts.map +1 -1
- package/dist/archive-zip-entry.js +23 -8
- package/dist/archive-zip-integrity.d.ts.map +1 -1
- package/dist/archive-zip-integrity.js +3 -4
- package/dist/archive-zip-loader.d.ts.map +1 -1
- package/dist/archive-zip-loader.js +107 -31
- package/dist/archive-zip-names.d.ts +1 -0
- package/dist/archive-zip-names.d.ts.map +1 -1
- package/dist/archive-zip-names.js +6 -0
- package/dist/archive.js +6 -5
- package/dist/bounded-read-stream.d.ts +0 -1
- package/dist/bounded-read-stream.d.ts.map +1 -1
- package/dist/bounded-read-stream.js +0 -6
- package/dist/copy-publication.d.ts +6 -0
- package/dist/copy-publication.d.ts.map +1 -1
- package/dist/copy-publication.js +3 -0
- package/dist/copy-tree-portable.d.ts.map +1 -1
- package/dist/copy-tree-portable.js +44 -24
- package/dist/copy.d.ts.map +1 -1
- package/dist/copy.js +29 -11
- package/dist/directory-mode-owner.js +5 -5
- package/dist/file-handle-transfer.d.ts +2 -0
- package/dist/file-handle-transfer.d.ts.map +1 -1
- package/dist/file-handle-transfer.js +57 -2
- package/dist/file-identity.d.ts.map +1 -1
- package/dist/file-identity.js +18 -4
- package/dist/file-lock-sync-admission.d.ts +19 -0
- package/dist/file-lock-sync-admission.d.ts.map +1 -0
- package/dist/file-lock-sync-admission.js +93 -0
- package/dist/file-lock-sync-root-acquire.d.ts +4 -0
- package/dist/file-lock-sync-root-acquire.d.ts.map +1 -0
- package/dist/file-lock-sync-root-acquire.js +370 -0
- package/dist/file-lock-sync-root-arbitration.d.ts +18 -0
- package/dist/file-lock-sync-root-arbitration.d.ts.map +1 -0
- package/dist/file-lock-sync-root-arbitration.js +66 -0
- package/dist/file-lock-sync-root-held.d.ts +34 -0
- package/dist/file-lock-sync-root-held.d.ts.map +1 -0
- package/dist/file-lock-sync-root-held.js +393 -0
- package/dist/file-lock-sync-root-io.d.ts +44 -0
- package/dist/file-lock-sync-root-io.d.ts.map +1 -0
- package/dist/file-lock-sync-root-io.js +209 -0
- package/dist/file-lock-sync-root-mutation.d.ts +17 -0
- package/dist/file-lock-sync-root-mutation.d.ts.map +1 -0
- package/dist/file-lock-sync-root-mutation.js +277 -0
- package/dist/file-lock-sync-root-options.d.ts +20 -0
- package/dist/file-lock-sync-root-options.d.ts.map +1 -0
- package/dist/file-lock-sync-root-options.js +58 -0
- package/dist/file-lock-sync-root-registration.d.ts +2 -0
- package/dist/file-lock-sync-root-registration.d.ts.map +1 -0
- package/dist/file-lock-sync-root-registration.js +90 -0
- package/dist/file-lock-sync-root.d.ts +36 -0
- package/dist/file-lock-sync-root.d.ts.map +1 -0
- package/dist/file-lock-sync-root.js +361 -0
- package/dist/file-lock-sync-stale-admission.d.ts +24 -0
- package/dist/file-lock-sync-stale-admission.d.ts.map +1 -0
- package/dist/file-lock-sync-stale-admission.js +205 -0
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +245 -205
- package/dist/file-store-prune.d.ts.map +1 -1
- package/dist/file-store-prune.js +5 -1
- package/dist/file-store-sync-write.js +3 -3
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +47 -12
- package/dist/json-document-store.d.ts.map +1 -1
- package/dist/json-document-store.js +22 -15
- package/dist/json-durable-queue-ownership.d.ts +0 -1
- package/dist/json-durable-queue-ownership.d.ts.map +1 -1
- package/dist/json-durable-queue-ownership.js +0 -6
- package/dist/native-binding.d.ts +2 -0
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-parent-admission.d.ts +3 -2
- package/dist/native-parent-admission.d.ts.map +1 -1
- package/dist/native-parent-admission.js +24 -5
- package/dist/native-pinned-write-windows.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +7 -7
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +7 -11
- package/dist/native-policy-parent-windows.d.ts +14 -0
- package/dist/native-policy-parent-windows.d.ts.map +1 -0
- package/dist/native-policy-parent-windows.js +200 -0
- package/dist/native-rename-outcome.d.ts +4 -0
- package/dist/native-rename-outcome.d.ts.map +1 -0
- package/dist/native-rename-outcome.js +8 -0
- package/dist/native-staged-file.d.ts +2 -2
- package/dist/native-staged-file.d.ts.map +1 -1
- package/dist/native-staged-file.js +42 -40
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +12 -8
- package/dist/path-prefix.d.ts.map +1 -1
- package/dist/path-prefix.js +30 -8
- package/dist/path-suffix-aliases.d.ts +2 -0
- package/dist/path-suffix-aliases.d.ts.map +1 -1
- package/dist/path-suffix-aliases.js +25 -17
- package/dist/permission-exec.d.ts +2 -0
- package/dist/permission-exec.d.ts.map +1 -1
- package/dist/permission-exec.js +150 -21
- package/dist/permissions-windows.js +1 -1
- package/dist/pinned-mutation-admission.d.ts.map +1 -1
- package/dist/pinned-mutation-admission.js +10 -5
- package/dist/pinned-mutation-observation.d.ts +0 -1
- package/dist/pinned-mutation-observation.d.ts.map +1 -1
- package/dist/pinned-mutation-observation.js +0 -19
- package/dist/pinned-mutation-shared-route.d.ts +1 -0
- package/dist/pinned-mutation-shared-route.d.ts.map +1 -1
- package/dist/pinned-mutation-shared-route.js +1 -1
- package/dist/pinned-write-types.d.ts +2 -0
- package/dist/pinned-write-types.d.ts.map +1 -1
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +75 -121
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +1 -15
- package/dist/replace-directory.d.ts.map +1 -1
- package/dist/replace-directory.js +256 -18
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +62 -70
- package/dist/replace-file-copy-source.d.ts.map +1 -1
- package/dist/replace-file-copy-source.js +10 -12
- package/dist/replace-file-temp-owner.d.ts +5 -2
- package/dist/replace-file-temp-owner.d.ts.map +1 -1
- package/dist/replace-file-temp-owner.js +65 -30
- package/dist/replace-file.js +6 -6
- package/dist/retained-directory-replacement.d.ts +26 -0
- package/dist/retained-directory-replacement.d.ts.map +1 -0
- package/dist/retained-directory-replacement.js +193 -0
- package/dist/root-boundary.d.ts +1 -0
- package/dist/root-boundary.d.ts.map +1 -1
- package/dist/root-boundary.js +4 -0
- package/dist/root-context.d.ts +0 -8
- package/dist/root-context.d.ts.map +1 -1
- package/dist/root-context.js +0 -3
- package/dist/root-create-input.d.ts +6 -0
- package/dist/root-create-input.d.ts.map +1 -1
- package/dist/root-create-input.js +5 -1
- package/dist/root-directory-list.d.ts +1 -0
- package/dist/root-directory-list.d.ts.map +1 -1
- package/dist/root-directory-list.js +1 -0
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +18 -10
- package/dist/root-move-noreplace.d.ts.map +1 -1
- package/dist/root-move-noreplace.js +2 -2
- package/dist/root-path-errors.d.ts +1 -0
- package/dist/root-path-errors.d.ts.map +1 -1
- package/dist/root-path-errors.js +11 -2
- package/dist/root-path-existing.d.ts.map +1 -1
- package/dist/root-path-existing.js +11 -35
- package/dist/root-path.js +1 -13
- package/dist/root-remove.d.ts +1 -0
- package/dist/root-remove.d.ts.map +1 -1
- package/dist/root-remove.js +4 -0
- package/dist/root-walk.d.ts +1 -1
- package/dist/root-walk.d.ts.map +1 -1
- package/dist/root-walk.js +17 -2
- package/dist/root-write-admission.d.ts +0 -2
- package/dist/root-write-admission.d.ts.map +1 -1
- package/dist/root-write-admission.js +1 -15
- package/dist/root-write-complete-parent.d.ts.map +1 -1
- package/dist/root-write-complete-parent.js +7 -23
- package/dist/root-write-verification.d.ts.map +1 -1
- package/dist/root-write-verification.js +29 -42
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +2 -24
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +3 -24
- package/dist/secret-read-policy.d.ts +6 -2
- package/dist/secret-read-policy.d.ts.map +1 -1
- package/dist/secret-read-policy.js +26 -2
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +23 -12
- package/dist/sidecar-lock-acquire.d.ts +2 -28
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +288 -199
- package/dist/sidecar-lock-admission-context.d.ts +19 -0
- package/dist/sidecar-lock-admission-context.d.ts.map +1 -0
- package/dist/sidecar-lock-admission-context.js +60 -0
- package/dist/sidecar-lock-admission-parser.d.ts +43 -0
- package/dist/sidecar-lock-admission-parser.d.ts.map +1 -0
- package/dist/sidecar-lock-admission-parser.js +113 -0
- package/dist/sidecar-lock-admission.d.ts +35 -0
- package/dist/sidecar-lock-admission.d.ts.map +1 -0
- package/dist/sidecar-lock-admission.js +7 -0
- package/dist/sidecar-lock-reclaim.d.ts +9 -4
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +80 -25
- package/dist/sidecar-lock-stale-admission.d.ts +39 -0
- package/dist/sidecar-lock-stale-admission.d.ts.map +1 -0
- package/dist/sidecar-lock-stale-admission.js +232 -0
- package/dist/sidecar-lock-target.d.ts +8 -0
- package/dist/sidecar-lock-target.d.ts.map +1 -0
- package/dist/sidecar-lock-target.js +55 -0
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +100 -16
- package/dist/temp-workspace-descriptor.d.ts.map +1 -1
- package/dist/temp-workspace-descriptor.js +9 -27
- package/dist/temp-workspace-owner.d.ts.map +1 -1
- package/dist/temp-workspace-owner.js +8 -8
- package/dist/walk.d.ts +5 -1
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +19 -6
- package/dist/windows-owner.d.ts.map +1 -1
- package/dist/windows-owner.js +2 -2
- package/docs/advanced.md +1 -1
- package/docs/archive.md +36 -9
- package/docs/atomic.md +85 -8
- package/docs/copy.md +35 -0
- package/docs/file-store.md +19 -0
- package/docs/json-store.md +5 -0
- package/docs/native-helper.md +10 -3
- package/docs/output.md +6 -0
- package/docs/path-prefix.md +10 -0
- package/docs/path-suffix-aliases.md +51 -6
- package/docs/permissions.md +13 -0
- package/docs/public-api.md +3 -2
- package/docs/root.md +22 -3
- package/docs/sidecar-lock.md +109 -4
- package/docs/staged-file.md +7 -3
- package/docs/temp.md +20 -3
- package/docs/walk.md +67 -1
- package/docs/writing.md +5 -1
- package/package.json +10 -10
- package/dist/darwin-acl.d.ts +0 -4
- package/dist/darwin-acl.d.ts.map +0 -1
- package/dist/darwin-acl.js +0 -24
package/dist/walk.js
CHANGED
|
@@ -16,6 +16,10 @@ function validateWalkOptions(options) {
|
|
|
16
16
|
throw new TypeError(`invalid walk symlink policy: ${String(options.symlinks)}`);
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
+
function isObjectResult(result) {
|
|
20
|
+
return result !== null &&
|
|
21
|
+
(typeof result === "object" || typeof result === "function");
|
|
22
|
+
}
|
|
19
23
|
function kindForDirent(dirent) {
|
|
20
24
|
if (dirent.isDirectory())
|
|
21
25
|
return "directory";
|
|
@@ -85,6 +89,8 @@ export function walkDirectorySync(rootDir, options = {}) {
|
|
|
85
89
|
let realDir;
|
|
86
90
|
const operationPath = pathForWindowsFilesystem(dir);
|
|
87
91
|
try {
|
|
92
|
+
if (depth > 1 && symlinks !== "follow" && fsSync.lstatSync(operationPath).isSymbolicLink())
|
|
93
|
+
return;
|
|
88
94
|
realDir = realpathSync(operationPath);
|
|
89
95
|
}
|
|
90
96
|
catch (error) {
|
|
@@ -146,6 +152,8 @@ export async function walkDirectory(rootDir, options = {}) {
|
|
|
146
152
|
let realDir;
|
|
147
153
|
const operationPath = pathForWindowsFilesystem(dir);
|
|
148
154
|
try {
|
|
155
|
+
if (depth > 1 && symlinks !== "follow" && fsSync.lstatSync(operationPath).isSymbolicLink())
|
|
156
|
+
return;
|
|
149
157
|
realDir = realpathSync.native(operationPath);
|
|
150
158
|
}
|
|
151
159
|
catch (error) {
|
|
@@ -175,15 +183,20 @@ export async function walkDirectory(rootDir, options = {}) {
|
|
|
175
183
|
continue;
|
|
176
184
|
const relativePath = relativeDir ? `${relativeDir}${path.sep}${dirent.name}` : dirent.name;
|
|
177
185
|
const entry = buildEntry({ relativePath, fullPath, dirent, depth, kind });
|
|
178
|
-
|
|
186
|
+
const include = options.include;
|
|
187
|
+
const included = include == null ? true : Reflect.apply(include, options, [entry]);
|
|
188
|
+
if ((isObjectResult(included) ? await included : included) ?? true) {
|
|
179
189
|
result.entries.push(entry);
|
|
180
190
|
}
|
|
181
191
|
if (kind === "directory" &&
|
|
182
|
-
(options.maxDepth === undefined || depth < options.maxDepth)
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (
|
|
186
|
-
|
|
192
|
+
(options.maxDepth === undefined || depth < options.maxDepth)) {
|
|
193
|
+
const descend = options.descend;
|
|
194
|
+
const descended = descend == null ? true : Reflect.apply(descend, options, [entry]);
|
|
195
|
+
if ((isObjectResult(descended) ? await descended : descended) ?? true) {
|
|
196
|
+
await visit(fullPath, relativePath, depth + 1);
|
|
197
|
+
if (result.truncated)
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
187
200
|
}
|
|
188
201
|
}
|
|
189
202
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"windows-owner.d.ts","sourceRoot":"","sources":["../src/windows-owner.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"windows-owner.d.ts","sourceRoot":"","sources":["../src/windows-owner.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,wBAAwB,EAC9B,MAAM,sBAAsB,CAAC;AAI9B,MAAM,MAAM,gBAAgB,GAAG,CAC7B,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,KACX,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAEjD,MAAM,MAAM,mBAAmB,GAAG;IAChC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,IAAI,CAAC,EAAE,eAAe,EAAE,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,wBAAwB,CAAC;IACvC,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,WAAW,EAAE,OAAO,CAAC;CACtB,CAAC;AAqDF,wBAAsB,mBAAmB,CAAC,MAAM,EAAE;IAChD,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,IAAI,EAAE,gBAAgB,CAAC;CACxB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAmD/B"}
|
package/dist/windows-owner.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { formatPermissionErrorDetail, getPermissionCommandFailure, } from "./permission-exec.js";
|
|
1
|
+
import { formatCaughtPermissionFailure, formatPermissionErrorDetail, getPermissionCommandFailure, } from "./permission-exec.js";
|
|
2
2
|
import { resolveWindowsSystemCommand } from "./windows-command.js";
|
|
3
3
|
import { hasWindowsPathAlias } from "./windows-path-alias.js";
|
|
4
4
|
const SID_RE = /^\*?s-\d+-\d+(-\d+)+$/i;
|
|
@@ -89,7 +89,7 @@ export async function inspectWindowsOwner(params) {
|
|
|
89
89
|
}
|
|
90
90
|
catch (err) {
|
|
91
91
|
return {
|
|
92
|
-
error:
|
|
92
|
+
error: formatCaughtPermissionFailure(err),
|
|
93
93
|
errorDetail: getPermissionCommandFailure(err, command, performance.now() - startedAt),
|
|
94
94
|
errorCause: err,
|
|
95
95
|
};
|
package/docs/advanced.md
CHANGED
|
@@ -69,7 +69,7 @@ Operational filesystem failures such as permissions or I/O errors are rethrown.
|
|
|
69
69
|
|---|---|---|
|
|
70
70
|
| `readFileDescriptorBounded`, `readFileDescriptorBoundedSync`, `readFileHandleBounded` | – | Incremental whole-file reads for already-open descriptors/handles. They consume at most `maxBytes + 1`, do not close the input, and throw `FsSafeError("too-large")` on overflow. |
|
|
71
71
|
| `readFileWindowFully`, `readFileWindowFullySync`, `ReadFileWindowOptions` | [positional-read.md](positional-read.md) | Fill a caller-owned buffer at an explicit file position, completing short reads and returning the EOF count without moving or closing the descriptor. |
|
|
72
|
-
| `copyFileHandle`, `CopyFileHandleOptions` | [copy.md](copy.md#borrowed-filehandle-transfers) | Copy caller-owned regular
|
|
72
|
+
| `copyFileHandle`, `copyFileDescriptorSync`, `CopyFileHandleOptions` | [copy.md](copy.md#borrowed-filehandle-transfers) | Copy caller-owned regular files through async handles or sync descriptors from position zero with byte limits and synchronous callbacks; preserves cursors and leaves publication and cleanup to the caller. |
|
|
73
73
|
| `overwriteFileHandle`, `OverwriteFileHandleOptions` | [in-place-write.md](in-place-write.md) | Overwrite a borrowed read/write handle with prefix-only preparation and best-effort rollback; preserves its inode, cursor, and caller-owned lifetime. |
|
|
74
74
|
| `openRootFile`, `openRootFileSync`, `canUseRootFileOpen`, `matchRootFileOpenFailure`, related types | – | Low-level root-bounded open; rejects every symlink component by default, with `symlinks: "follow-parents-within-root"` for contained parent aliases or `"follow-within-root"` for final links too. |
|
|
75
75
|
| `appendRegularFile`, `appendRegularFileSync`, `readRegularFile`, `readRegularFileSync`, `statRegularFile`, `statRegularFileSync`, `resolveRegularFileAppendFlags`, `AppendRegularFileOptions`, `RegularFileStatResult` | [regular-file.md](regular-file.md) | Type-checked regular-file I/O. |
|
package/docs/archive.md
CHANGED
|
@@ -124,7 +124,12 @@ Readable directories do not depend on procfs. A Linux search-only directory
|
|
|
124
124
|
needing a mode change requires accessible, genuine procfs; unavailable or
|
|
125
125
|
untrusted authority rejects explicitly instead of silently accepting a wrong
|
|
126
126
|
mode. Other unsupported search-only routes also fail closed. Windows retains
|
|
127
|
-
its existing bounded lack of POSIX mode enforcement.
|
|
127
|
+
its existing bounded lack of POSIX mode enforcement. Best-effort mode handling
|
|
128
|
+
applies only to the mode change itself: an authority or deadline check that
|
|
129
|
+
fails immediately before dispatch still propagates, including a custom
|
|
130
|
+
one-shot structural check. A check failure observed immediately after dispatch
|
|
131
|
+
is retained while post-dispatch authority verification and final mode inspection
|
|
132
|
+
settle, then propagated with its exact JavaScript value, including falsy values.
|
|
128
133
|
|
|
129
134
|
Extraction and TAR inspection first copy the admitted source into a private
|
|
130
135
|
staging file. This copy reuses at most 512 KiB of scratch space, reduced for
|
|
@@ -148,14 +153,24 @@ must agree with this kind, physical index, size, known path, and UNIX-creator mo
|
|
|
148
153
|
before extraction or any member read. Bounded ZIP reads retain this metadata from
|
|
149
154
|
their single admission pass without another input copy or scan.
|
|
150
155
|
|
|
151
|
-
Portable ZIP preflight, extraction, and reads
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
156
|
+
Portable ZIP preflight, extraction, and reads bind every decoder insertion to
|
|
157
|
+
its admitted physical record before JSZip can coerce its type or discard its
|
|
158
|
+
payload. Names, physical order, original directory/permission metadata,
|
|
159
|
+
compression method, compressed and decoded sizes, and CRC must agree. The
|
|
160
|
+
private loader then applies the admitted kind, preserving UNIX-only directory
|
|
161
|
+
attributes, backslash-only directories, and high-word symlinks from any creator.
|
|
162
|
+
Symlinks remain subject to the existing filter and blocked-link policy.
|
|
163
|
+
UNIX socket and block-device type bits do not turn regular-file payloads into
|
|
164
|
+
empty directories. Directory and symlink callbacks receive their physical
|
|
165
|
+
declared sizes; directory bodies are not published as files. Unsupported
|
|
166
|
+
link-like types remain visible as `other` and are safely omitted when accepted.
|
|
167
|
+
UNIX creator metadata and permission defaults remain as described above.
|
|
168
|
+
|
|
169
|
+
The loader adapter belongs to one private JSZip instance and is removed after
|
|
170
|
+
loading, including failure. Public preflight still returns ordinary JSZip entry
|
|
171
|
+
objects, with directory keys ending in `/` and recognizable symlink type bits.
|
|
172
|
+
Compressed data is retained even for declared-zero entries, so an empty-size
|
|
173
|
+
claim cannot bypass payload-size or CRC verification during extraction or reads.
|
|
159
174
|
|
|
160
175
|
Within one ZIP entry, identical local and central name bytes reuse the same
|
|
161
176
|
decoded validation. Unicode Path admission is shared only when both the raw names
|
|
@@ -269,11 +284,23 @@ A failure before publication preserves a pre-existing file, and rejection does
|
|
|
269
284
|
not grant authority to delete a substituted file or alias. Failed extraction does not
|
|
270
285
|
restore overwritten contents. Active destination mutations and their guarded
|
|
271
286
|
cleanup still finish before rejection; no later destination mutation begins.
|
|
287
|
+
Portable ZIP output is not eligible for publication until its stream closes or
|
|
288
|
+
the defensive `FileHandle` close succeeds. If that fallback close rejects,
|
|
289
|
+
`extractArchive()` propagates the error and publishes no entry from the staged
|
|
290
|
+
tree. Cleanup retains its best-effort `FileHandle` close; it does not transfer
|
|
291
|
+
the descriptor to a raw or native closer.
|
|
272
292
|
New directories whose finalization was never reached can retain their
|
|
273
293
|
private working mode after failure. Failure cleanup closes retained descriptors;
|
|
274
294
|
it does not run a cleanup chmod sweep or roll back the archive. The public merge
|
|
275
295
|
helper still derives modes from its external source tree and must be able to
|
|
276
296
|
read that source; it never chmods an unreadable external source to admit it.
|
|
297
|
+
It retains the source root and each active child directory's exact identity
|
|
298
|
+
through traversal and copy verification. Each source file is opened once,
|
|
299
|
+
admitted against that root and its earlier exact identity observation, and
|
|
300
|
+
copied from the admitted descriptor; public file modes use that descriptor's
|
|
301
|
+
ordinary permission bits. Replacing a source ancestor or leaf rejects the
|
|
302
|
+
merge before replacement bytes can be published. These checks do not provide
|
|
303
|
+
a snapshot against writes to the same source inode.
|
|
277
304
|
That helper retains per-copy durability and immediate postorder directory-mode
|
|
278
305
|
finalization; the deferred pass described above belongs to `extractArchive()`.
|
|
279
306
|
|
package/docs/atomic.md
CHANGED
|
@@ -92,6 +92,17 @@ await replaceFileAtomic({
|
|
|
92
92
|
|
|
93
93
|
If `beforeRename` throws, the rename is skipped and the owned temp file is removed — the destination is unchanged. Cleanup unlinks only the exact admitted single-link file; a substitute observed at the temp name is preserved and removed from cleanup authority. The same identity is rechecked before every rename retry, when entering copy fallback, and at the final name after rename. A post-rename verification failure reports the race without rolling back or deleting the published name.
|
|
94
94
|
|
|
95
|
+
JavaScript permits `beforeRename` callbacks to throw any value, including
|
|
96
|
+
`undefined`, `null`, `false`, signed zero, `0n`, an empty string, and `NaN`.
|
|
97
|
+
Once such an operation failure reaches temp-owner settlement, atomic replacement
|
|
98
|
+
preserves that value when cleanup and close succeed. With
|
|
99
|
+
`throwOnCleanupError: true`, an additional owned-temp cleanup failure keeps the
|
|
100
|
+
existing cleanup wrapper whose `cause` is the original thrown value. A later
|
|
101
|
+
descriptor-close failure is reported in an `AggregateError`, in operation/cleanup
|
|
102
|
+
then close order. The default `throwOnCleanupError: false` omits only the cleanup
|
|
103
|
+
failure: the temp stays registered for identity-checked process-exit cleanup, the
|
|
104
|
+
descriptor is still closed, and a close failure remains reportable.
|
|
105
|
+
|
|
95
106
|
Identity checks and pathname rename/unlink remain separate syscalls, not atomic conditional mutations. Use an approved writable parent plus cooperative locking or OS isolation when arbitrary concurrent namespace mutation is in scope.
|
|
96
107
|
|
|
97
108
|
### FUSE, Windows exFAT/FAT32, and unstable rename identity
|
|
@@ -148,6 +159,19 @@ that same descriptor, and synchronizes the result. Any write, mode, or sync
|
|
|
148
159
|
failure triggers a byte-and-mode restore and another sync through the same
|
|
149
160
|
descriptor.
|
|
150
161
|
|
|
162
|
+
With `syncTempFile: false`, an exclusive-create copy fallback does not report
|
|
163
|
+
success until its new destination writer closes successfully. This includes
|
|
164
|
+
`"restore-original"` when the destination did not exist. A close rejection or throw is
|
|
165
|
+
propagated exactly, including falsy values. The destination may already contain
|
|
166
|
+
all or part of the replacement, so a close failure does not prove that the old
|
|
167
|
+
destination survived or that the replacement was published. The outer atomic
|
|
168
|
+
operation still attempts identity-bound cleanup of its owned source temp; an
|
|
169
|
+
unverifiable or substituted temp remains preserved. If writing, mode adjustment,
|
|
170
|
+
or another earlier operation also fails, that earlier value remains the reported
|
|
171
|
+
failure and the destination close is attempted once. Successful synchronized
|
|
172
|
+
fallbacks and in-place `"restore-original"` replacements retain their existing best-effort final-close
|
|
173
|
+
handling.
|
|
174
|
+
|
|
151
175
|
Restore failures are `FsSafeError("helper-failed")` values with typed
|
|
152
176
|
`details.cleanup` set to `"restored"` or `"restore-failed"`. An original larger
|
|
153
177
|
than `maxRestoreBytes` fails with `too-large` before mutation. A missing
|
|
@@ -168,7 +192,9 @@ synchronous boot paths or test setup code. It returns the same
|
|
|
168
192
|
|
|
169
193
|
## `replaceDirectoryAtomic`
|
|
170
194
|
|
|
171
|
-
|
|
195
|
+
Publish one staged directory at a target without overwriting a concurrently
|
|
196
|
+
created entry. Despite the historical name, replacing an existing target is a
|
|
197
|
+
guarded two-rename protocol, not an atomic directory exchange.
|
|
172
198
|
|
|
173
199
|
```ts
|
|
174
200
|
import { replaceDirectoryAtomic } from "@openclaw/fs-safe/atomic";
|
|
@@ -179,15 +205,66 @@ await replaceDirectoryAtomic({
|
|
|
179
205
|
});
|
|
180
206
|
```
|
|
181
207
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
208
|
+
Every publication requires the dedicated native identity-fenced,
|
|
209
|
+
descriptor-relative no-replace rename capability and readable retained
|
|
210
|
+
descriptors for the staged and target parents. Older bindings that expose only
|
|
211
|
+
the legacy four-argument no-replace rename fail with `helper-unavailable`
|
|
212
|
+
before target-parent creation or any other replacement effect. This applies
|
|
213
|
+
when the parents are the same or different. There is no JavaScript rename
|
|
214
|
+
fallback, and a cross-device rename still fails. Replacing an existing target
|
|
215
|
+
additionally requires usable native bounded owned-tree cleanup and a readable
|
|
216
|
+
retained descriptor for the original target.
|
|
217
|
+
|
|
218
|
+
If the target is absent, the helper publishes `stagedDir → targetDir` with a
|
|
219
|
+
single no-replace rename. If the target exists, it renames `targetDir` to a
|
|
220
|
+
randomized sibling backup and then renames `stagedDir → targetDir`. The target
|
|
221
|
+
name is temporarily absent between those two operations. A competing entry is
|
|
222
|
+
never overwritten. Publication rollback is attempted only when the staged
|
|
223
|
+
rename is known not to have committed and the target name is still absent; the
|
|
224
|
+
rollback itself is no-replace, so a competitor is preserved and the backup is
|
|
225
|
+
left for recovery.
|
|
226
|
+
|
|
227
|
+
Exact staged-directory identity and parent checks run before and after each
|
|
228
|
+
rename. On Windows, the native rename opens the source relative to the retained
|
|
229
|
+
parent, compares that handle's exact volume and file-index identity with the
|
|
230
|
+
pre-rename bigint receipt, and only then mutates it. POSIX does not provide a
|
|
231
|
+
rename operation that also compares an expected source inode, so on POSIX a
|
|
232
|
+
source-name substitution in the final check-to-rename gap can be moved briefly
|
|
233
|
+
and then detected by the post-rename verification. Similarly, a successful
|
|
234
|
+
rename can be followed by a verification error. Inspect the error's
|
|
235
|
+
`details.publication` value rather than treating rejection as proof that
|
|
236
|
+
publication did not happen.
|
|
237
|
+
|
|
238
|
+
A Windows source-identity mismatch is rejected before mutation, reported
|
|
239
|
+
publicly as `path-mismatch`, and treated as definitely uncommitted so an earlier
|
|
240
|
+
backup can be rolled back. Other `path-mismatch`-shaped native errors are not
|
|
241
|
+
assumed to be pre-commit failures; their publication outcome remains
|
|
242
|
+
indeterminate and observed competitors are preserved.
|
|
243
|
+
|
|
244
|
+
Any native rename error without explicit pre-dispatch provenance has an
|
|
245
|
+
indeterminate outcome, including ordinary errno such as `ENOENT`, `EEXIST`,
|
|
246
|
+
or `EACCES`: a remote filesystem may commit before losing its reply. The helper
|
|
247
|
+
does not guess whether that rename committed or perform another rename or
|
|
248
|
+
cleanup based on that guess; observed names are preserved for caller-directed
|
|
249
|
+
recovery. An error `details.backupPath`, when present, is only the attempted or
|
|
250
|
+
last-observed backup pathname. It does not prove that the path still exists or
|
|
251
|
+
still names the original directory.
|
|
252
|
+
|
|
253
|
+
After a verified commit, the original backup is removed through its retained
|
|
254
|
+
directory identity and bounded native traversal. Cleanup failures reject after
|
|
255
|
+
publication and can leave the backup. On POSIX, cleanup retains the
|
|
256
|
+
[bounded final-entry unlink limitation](temp.md#private-temp-workspaces).
|
|
257
|
+
|
|
258
|
+
Concurrent calls for the same resolved target are serialized inside the current
|
|
259
|
+
process so their backup, publication, and cleanup phases cannot interleave.
|
|
260
|
+
Other processes are not serialized; no-replace renames provide the competitor
|
|
261
|
+
boundary. On Windows, ordinary drive-relative staged and target paths are
|
|
262
|
+
anchored at entry before namespace-alias admission and resolution.
|
|
187
263
|
`backupPrefix`, when supplied, is sanitized as one path prefix and cannot contain
|
|
188
264
|
path separators or NUL bytes; the generated backup tail is randomized.
|
|
189
265
|
|
|
190
|
-
Use it when callers must
|
|
266
|
+
Use it when callers must publish a whole staged tree with these recovery
|
|
267
|
+
semantics. For single-file replacement, `replaceFileAtomic` is the right tool.
|
|
191
268
|
|
|
192
269
|
## `writeTextAtomic`
|
|
193
270
|
|
|
@@ -396,7 +473,7 @@ type ReplaceFileAtomicSyncFileSystem = {
|
|
|
396
473
|
};
|
|
397
474
|
```
|
|
398
475
|
|
|
399
|
-
The async interface already requires `open()`, whose `FileHandle` supplies `chmod()`, so injecting `node:fs` or another conforming adapter needs no new async member.
|
|
476
|
+
The async interface already requires `open()`, whose `FileHandle` supplies `chmod()`, so injecting `node:fs` or another conforming adapter needs no new async member. The async temp owner consumes its retained handle before awaiting `close()` during publication handoff and terminal settlement: if a custom adapter releases the resource and then rejects, that rejection is reported without calling `close()` on the same retained handle again. If publication verification opened a replacement handle before the previous retained handle failed to close, the replacement receives one best-effort close attempt. On POSIX, `open()` must support no-follow directory descriptors as Node does. A custom synchronous filesystem that passes `mode`, `dirMode`, or `preserveExistingMode` must supply `fchmodSync`; omission fails before any file or directory is created and never falls back to a pathname `chmod`. Existing synchronous adapters that request none of those options may omit it; their parent is still opened and identity-checked through a no-follow directory descriptor. Injecting plain `node:fs` supports explicit file and directory modes. Older adapter literals may continue to include `chmod` or `chmodSync` for source compatibility, but those operations are ignored. Copy fallback applies the file mode through its pinned destination descriptor as well, preserving exact modes despite the process umask.
|
|
400
477
|
|
|
401
478
|
## See also
|
|
402
479
|
|
package/docs/copy.md
CHANGED
|
@@ -66,6 +66,8 @@ On Windows, automatic byte copying uses the native binding when available to tra
|
|
|
66
66
|
|
|
67
67
|
On Linux, automatic byte copying also uses the native binding when available. It reads in chunks up to 1 MiB, using smaller buffers for smaller source-size hints, and leaves leading and trailing zero-filled portions of each chunk unwritten in the new file, avoiding their allocation on filesystems that support sparse files. A final size update preserves trailing holes and all-zero files. This path uses reads and writes, without cloning or copy offload; it still reads the full logical contents and does not promise identical sparse extent layout. `clone: "never"` retains the JavaScript byte-copy path.
|
|
68
68
|
|
|
69
|
+
Tree-copy cleanup attempts every acquired close once, even when another close fails. Portable file handles close output before input; completed directories close their pinned source before target; the public wrapper then closes its source before the destination parent. A copy, identity, metadata, native-clone, or cancellation failure already observed at one of those scopes remains the reported value instead of being replaced by cleanup. If that scope otherwise succeeded, its first close failure is reported unchanged. Concurrent siblings have no global structural close order: the first observed sibling failure stops new work while admitted work settles. Portable copying records caller cancellation when it occurs, so a later abort during cleanup cannot replace an earlier copy failure.
|
|
70
|
+
|
|
69
71
|
Clones preserve file contents, empty directories, timestamps, executable modes where supported, and literal symbolic links. Editing a clone does not modify its source. Unsupported filesystem operations fail; callers may choose their own copy or checkout fallback after the failed operation has settled.
|
|
70
72
|
|
|
71
73
|
Native Windows byte copies can store large zero-filled chunks as sparse ranges when the destination is initially empty and its filesystem supports sparse files. This still reads every source byte and creates an independent copy.
|
|
@@ -137,6 +139,37 @@ descriptor inspection does not prove that the source remained unchanged while
|
|
|
137
139
|
copying. Keep existing source-fingerprint and publication checks around the
|
|
138
140
|
transfer when building snapshot operations.
|
|
139
141
|
|
|
142
|
+
### Synchronous descriptor transfers
|
|
143
|
+
|
|
144
|
+
`copyFileDescriptorSync(sourceFd, targetFd, options?)` provides the same
|
|
145
|
+
zero-origin byte transfer for borrowed numeric descriptors. It shares
|
|
146
|
+
`CopyFileHandleOptions` and returns the copied byte count synchronously:
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
import { copyFileDescriptorSync } from "@openclaw/fs-safe/advanced";
|
|
150
|
+
|
|
151
|
+
const bytes = copyFileDescriptorSync(sourceFd, targetFd, {
|
|
152
|
+
maxBytes: expectedSize,
|
|
153
|
+
assertBeforeMutation: assertSnapshotOwnerCurrent,
|
|
154
|
+
});
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The same regular-file and exact-identity admission, byte limits, short-I/O
|
|
158
|
+
handling, cursor preservation, and caller-owned cleanup apply. The target must
|
|
159
|
+
be opened without append mode, and both descriptors must remain open and free
|
|
160
|
+
of concurrent I/O, including inside callbacks. Neither helper makes a mutable
|
|
161
|
+
source into a consistent snapshot or truncates an existing destination suffix.
|
|
162
|
+
|
|
163
|
+
The synchronous helper snapshots the four options once and invokes both
|
|
164
|
+
callbacks with no receiver (`this` is `undefined` in strict callbacks).
|
|
165
|
+
`onChunk` receives a borrowed view that must be consumed immediately without
|
|
166
|
+
retaining or mutating it. Both callbacks must finish synchronously; thenables
|
|
167
|
+
throw `TypeError` before the current write. Cancellation is cooperative: a
|
|
168
|
+
pre-aborted signal or an abort triggered by a callback stops the transfer before
|
|
169
|
+
the next write. Timers cannot interrupt synchronous filesystem calls while the
|
|
170
|
+
event loop is blocked. Authority runs before every partial write; an abort
|
|
171
|
+
triggered by that assertion prevents the same write.
|
|
172
|
+
|
|
140
173
|
## Ownership and cancellation
|
|
141
174
|
|
|
142
175
|
These are low-level operations on caller-owned absolute paths, not Root-relative methods. The source and destination parent must be real directories. The library pins their descriptors and verifies their identities; it does not establish the caller's authorization to use them. Keep the source immutable for the operation, including writes through other aliases, and keep the destination namespace under the caller's control. Literal symlinks in the cloned contents are preserved rather than followed or sanitized.
|
|
@@ -145,6 +178,8 @@ An already aborted signal prevents dispatch. In-flight cancellation stops cancel
|
|
|
145
178
|
|
|
146
179
|
Completion is not a crash-durability guarantee. The API is suitable for reconstructible templates and checkouts; it does not sync every file or replace application-level publication and recovery rules.
|
|
147
180
|
|
|
181
|
+
A close-only rejection reports descriptor settlement, not whether bytes reached stable storage. The destination can already be complete when a close fails, just as other failed or cancelled calls can leave caller-owned output. Inspect or remove that output only after the copying promise settles and under the same source and destination authority assumptions.
|
|
182
|
+
|
|
148
183
|
Byte copying retains fractional file and directory access/modification timestamps to the precision supported by Node's timestamp APIs and the destination filesystem. This includes dates before 1970 on Unix. On Windows, [Node's unsigned stat seconds](https://github.com/nodejs/node/blob/v26.8.2/src/node_file-inl.h#L93-L104) can report pre-1970 timestamps as dates about 136 years later; byte copying inherits that upstream limitation.
|
|
149
184
|
|
|
150
185
|
## Platform tests and benchmarks
|
package/docs/file-store.md
CHANGED
|
@@ -133,6 +133,13 @@ the published entry intact for caller-owned recovery. Ordinary write-only and
|
|
|
133
133
|
mode-000 outputs do not require a readable descriptor when pathname metadata is
|
|
134
134
|
available.
|
|
135
135
|
|
|
136
|
+
If a synchronous write operation and its final temp-descriptor close both fail,
|
|
137
|
+
the store reports them in operation-then-close order in an `AggregateError`.
|
|
138
|
+
This ordering and the original JavaScript thrown value are preserved even when
|
|
139
|
+
that value is `undefined` or otherwise falsy. An unsuccessful best-effort temp
|
|
140
|
+
unlink remains registered for identity-checked process-exit cleanup; it does not
|
|
141
|
+
prevent the close attempt or replace either reportable failure.
|
|
142
|
+
|
|
136
143
|
If an opaque pathname cannot be reopened because of an ACL denial or sharing
|
|
137
144
|
restriction, the synchronous writer intentionally rejects with `path-mismatch`:
|
|
138
145
|
its exact publication identity cannot be verified. There is no equal-content
|
|
@@ -219,6 +226,14 @@ with the same precedence as `write`.
|
|
|
219
226
|
|
|
220
227
|
Per-call overrides for the store-level defaults:
|
|
221
228
|
|
|
229
|
+
Writes capture byte limits, modes, and durability before asynchronous work or
|
|
230
|
+
stream consumption. JSON writes capture those fields and the trailing-newline
|
|
231
|
+
setting before serialization. Later mutation cannot change those captured values.
|
|
232
|
+
Accessors run on the original options object. Ordinary writes retain content
|
|
233
|
+
conversion and byte-limit validation before reading modes and durability.
|
|
234
|
+
The legacy non-private stream `tempPrefix` accessor still runs after staging;
|
|
235
|
+
it does not control the publication policy.
|
|
236
|
+
|
|
222
237
|
```ts
|
|
223
238
|
type FileStoreWriteOptions = {
|
|
224
239
|
durable?: boolean; // store default, otherwise true
|
|
@@ -283,6 +298,10 @@ refreshes are preserved; replacements that are themselves expired remain
|
|
|
283
298
|
eligible. This does not require read permission. The existing best-effort
|
|
284
299
|
external-process race window after dispatch still applies.
|
|
285
300
|
|
|
301
|
+
Empty-directory pruning likewise rechecks that the selected entry is still a
|
|
302
|
+
directory immediately before guarded removal. File and symlink replacements
|
|
303
|
+
are preserved, and a directory that becomes nonempty is left in place.
|
|
304
|
+
|
|
286
305
|
## Difference from `Root`
|
|
287
306
|
|
|
288
307
|
| `FileStore` | `Root` |
|
package/docs/json-store.md
CHANGED
|
@@ -85,6 +85,11 @@ For `fileStore(...).json(rel, options)`, `options.durable` overrides the parent
|
|
|
85
85
|
file store's durability, while omission or `undefined` inherits it. Modes,
|
|
86
86
|
identity checks, mutation serialization, and sidecar locking are unchanged.
|
|
87
87
|
|
|
88
|
+
Each `write`, `update`, or `updateOr` invocation captures the retained options'
|
|
89
|
+
`durable` and `trailingNewline` values before queueing, locking, reading, or
|
|
90
|
+
calling the updater. Changes to those options affect later invocations only,
|
|
91
|
+
including when a mutation is waiting behind another operation.
|
|
92
|
+
|
|
88
93
|
The store does **not** validate the parsed value against `T` at runtime — the cast is unchecked. Wrap with a schema (zod/valibot) if the file might be hand-edited or written by another process you don't control.
|
|
89
94
|
|
|
90
95
|
## `read()`
|
package/docs/native-helper.md
CHANGED
|
@@ -95,9 +95,16 @@ clone/copy/hash workers, POSIX canonicalization, and Windows security descriptor
|
|
|
95
95
|
layer owns policy, retries, filters, budgets, modes, cleanup, error
|
|
96
96
|
normalization, and the decision to fall back.
|
|
97
97
|
|
|
98
|
-
- Linux uses `openat2` with `RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS`, `renameat`, and `renameat2(RENAME_NOREPLACE)`. Owned-tree cleanup enumerates and unlinks through retained directory descriptors and rejects device crossings.
|
|
99
|
-
- macOS 15.4 and newer prefer `O_RESOLVE_BENEATH`; older kernels resolve components with `O_NOFOLLOW` and restart in-root symlinks from the pinned root descriptor. Both routes use an `F_GETPATH` post-open escape detector and report `best-effort` because directory rename races are not atomic with that check. Publication uses `renameat` for replacement and `renameatx_np(RENAME_EXCL)` for no-replace; owned-tree cleanup uses descriptor-relative `openat`/`unlinkat`.
|
|
100
|
-
- Windows uses handle-relative `NtCreateFile`, rejects reparse points during root-bounded traversal, uses `FileRenameInfoEx` with replacement selected explicitly by the TypeScript policy layer
|
|
98
|
+
- Linux uses `openat2` with `RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS`, `renameat`, and `renameat2(RENAME_NOREPLACE)`. Direct-child no-replace renames borrow already-retained parent descriptors; deeper relative paths retain the guarded reopen. Owned-tree cleanup enumerates and unlinks through retained directory descriptors and rejects device crossings.
|
|
99
|
+
- macOS 15.4 and newer prefer `O_RESOLVE_BENEATH`; older kernels resolve components with `O_NOFOLLOW` and restart in-root symlinks from the pinned root descriptor. Both routes use an `F_GETPATH` post-open escape detector and report `best-effort` because directory rename races are not atomic with that check. Direct-child no-replace renames borrow already-retained parents without another `F_GETPATH`; deeper paths retain the guarded reopen. Publication uses `renameat` for replacement and `renameatx_np(RENAME_EXCL)` for no-replace; owned-tree cleanup uses descriptor-relative `openat`/`unlinkat`.
|
|
100
|
+
- Windows uses handle-relative `NtCreateFile`, rejects reparse points during root-bounded traversal, and uses `FileRenameInfoEx` with replacement selected explicitly by the TypeScript policy layer. The dedicated retained-directory `renameNoReplaceWithIdentity` primitive compares its required exact source receipt with the handle opened for rename before mutation. Its internal mismatch status is normalized to public `path-mismatch`; the legacy four-argument `renameNoReplace` export and its other callers are unchanged. Owned trees are deleted through exact opened handles with `FileDispositionInfoEx`; symlink/reparse entries in owned trees are removed as leaves and never traversed. Descriptors crossing N-API are converted only by the host executable's paired `uv_get_osfhandle` and `uv_open_osfhandle` exports. A runtime without both exports is unsupported for these native operations; the binding never guesses a raw HANDLE or uses a foreign CRT descriptor table. Descriptor-producing operations also require that same host's synchronous libuv close and request-management APIs before exporting an owned descriptor.
|
|
101
|
+
|
|
102
|
+
`replaceDirectoryAtomic()` requires `renameNoReplaceWithIdentity` before it
|
|
103
|
+
creates a missing target parent. On POSIX the dedicated entry point keeps the
|
|
104
|
+
existing pre-dispatch exact receipt fence but dispatches direct-child names
|
|
105
|
+
through the retained parents without another receipt, duplicate, reopen, or
|
|
106
|
+
macOS `F_GETPATH`; the documented final source-name substitution window remains
|
|
107
|
+
there. Deeper names retain guarded parent traversal.
|
|
101
108
|
|
|
102
109
|
Native primitives back create-only and replacing pinned writes, no-clobber
|
|
103
110
|
`Root.move()`, async sidecar creation, guarded publication, archive acceleration,
|
package/docs/output.md
CHANGED
|
@@ -49,6 +49,12 @@ The requested `path` must name a file. Missing destination parents are created
|
|
|
49
49
|
by the helper because the operation is "produce this output file under the
|
|
50
50
|
root"; callers should choose the filename before calling this API.
|
|
51
51
|
|
|
52
|
+
The helper reads each option once before its first asynchronous operation.
|
|
53
|
+
Changing the options object after invocation does not change the selected
|
|
54
|
+
writer, staging mode, isolation, filename fallback, byte limit, or final mode
|
|
55
|
+
for that write. Workspace writers retain the original options object as their
|
|
56
|
+
callback receiver; sibling writers retain the internal staging receiver.
|
|
57
|
+
|
|
52
58
|
`maxBytes` must be a non-negative safe integer or positive `Infinity`; zero is an active cap and `Infinity` disables it. Invalid values reject before the producer or filesystem staging runs.
|
|
53
59
|
|
|
54
60
|
Use `maxBytes` when the external producer can create arbitrarily large files,
|
package/docs/path-prefix.md
CHANGED
|
@@ -56,6 +56,16 @@ they are collapsed. Native realpath alone does not establish this permission
|
|
|
56
56
|
on every platform. Empty components from repeated or trailing separators do
|
|
57
57
|
not introduce a `.` lookup.
|
|
58
58
|
|
|
59
|
+
Raw component queues of at most 32 entries retain the legacy small-array
|
|
60
|
+
consumption path. After both the initial parse and every symlink expansion, a
|
|
61
|
+
longer queue uses forward cursor bookkeeping instead of moving the unprocessed
|
|
62
|
+
suffix for every component. The fixed small-queue bound limits repeated front
|
|
63
|
+
removal while retaining legacy shift-based consumption for shallow paths. This
|
|
64
|
+
asymptotic bound is not a platform performance result; performance acceptance
|
|
65
|
+
requires separate benchmark evidence. Callers should still apply their own
|
|
66
|
+
input-size limits: the helper is synchronous, retains the raw suffix, and
|
|
67
|
+
performs filesystem work for each non-empty existing component.
|
|
68
|
+
|
|
59
69
|
This is a read-only path observation. It neither pins files nor creates a root
|
|
60
70
|
boundary, authorizes access, or guarantees a consistent snapshot during
|
|
61
71
|
concurrent changes. Results can become stale immediately. Use a guarded Root
|
|
@@ -36,6 +36,7 @@ type ProbePathSuffixAliasesOptions = {
|
|
|
36
36
|
directory: string;
|
|
37
37
|
left: string;
|
|
38
38
|
right: string;
|
|
39
|
+
maxDepth?: number;
|
|
39
40
|
shouldProbeCaseVariants?: (leftNfc: string, rightNfc: string) => boolean;
|
|
40
41
|
};
|
|
41
42
|
|
|
@@ -44,9 +45,11 @@ function probePathSuffixAliasesSync(
|
|
|
44
45
|
): boolean | undefined;
|
|
45
46
|
```
|
|
46
47
|
|
|
47
|
-
The helper reads
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
The helper reads `directory`, rejects non-string values and supplied paths longer
|
|
49
|
+
than 32,768 code units, then rejects NUL-containing inputs. It resolves the path
|
|
50
|
+
to an absolute path and applies the same limit to that result before reading
|
|
51
|
+
`maxDepth`. Each option is read once. Later option getters or the predicate cannot retarget a
|
|
52
|
+
relative directory by changing the working directory. When
|
|
50
53
|
filesystem observations are needed, the directory is canonicalized and its
|
|
51
54
|
identity is checked; an initial directory alias can be followed.
|
|
52
55
|
|
|
@@ -56,26 +59,68 @@ inputs are rejected with `TypeError`. Windows additionally rejects drive-relativ
|
|
|
56
59
|
components, colons, and reserved device names, including device aliases with
|
|
57
60
|
extensions or trailing ignored characters. On POSIX, backslashes and colons are
|
|
58
61
|
ordinary filename characters. The optional predicate must be a function.
|
|
62
|
+
`maxDepth` defaults to `32` and must be a non-negative safe integer; invalid
|
|
63
|
+
values, including `Infinity`, throw `RangeError` before mutation or an
|
|
64
|
+
identical-suffix return. A value of `0` admits no ordinary nonempty suffix.
|
|
59
65
|
|
|
60
66
|
Both suffixes and the predicate are validated before the identical-suffix fast
|
|
61
67
|
path. Identical, valid, within-budget suffixes return `true` without filesystem
|
|
62
68
|
access or a predicate call. This does not prove that the directory exists or that
|
|
63
69
|
the suffix can be created.
|
|
64
70
|
|
|
65
|
-
|
|
71
|
+
The forward-observation allowance never exceeds 32,768, even with a large
|
|
72
|
+
`maxDepth`. A deeper or repeatedly colliding probe can return `undefined` when
|
|
73
|
+
that ceiling is reached. Reverse cleanup still runs outside this allowance.
|
|
66
74
|
|
|
67
|
-
|
|
75
|
+
## Resource budgets
|
|
76
|
+
|
|
77
|
+
The default limits for one call are:
|
|
68
78
|
|
|
69
79
|
| Resource | Limit | On exceeding the limit |
|
|
70
80
|
|---|---|---|
|
|
71
81
|
| Each supplied suffix | 8,192 UTF-16 code units | `RangeError` before mutation |
|
|
72
82
|
| Supplied and resolved directory paths | 32,768 UTF-16 code units each | `RangeError` before mutation |
|
|
73
|
-
| Each suffix's component count | 32 | `RangeError` before mutation |
|
|
83
|
+
| Each suffix's component count | `maxDepth`, default 32 | `RangeError` before mutation |
|
|
74
84
|
| Directory-creation attempts | 128 | `undefined` after cleanup |
|
|
75
85
|
| Successfully created probe directories | 64 | `undefined` after cleanup |
|
|
76
86
|
| Forward filesystem observations | 4,096 | `undefined` after cleanup |
|
|
77
87
|
| Each generated actual path | 32,768 UTF-16 code units | `undefined` after cleanup |
|
|
78
88
|
|
|
89
|
+
### Deeper observations
|
|
90
|
+
|
|
91
|
+
Applications comparing deeper prospective paths can explicitly raise `maxDepth`:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
const prefix = "future/".repeat(32);
|
|
95
|
+
const aliases = probePathSuffixAliasesSync({
|
|
96
|
+
directory: "/trusted/existing-directory",
|
|
97
|
+
left: `${prefix}Report.sqlite`,
|
|
98
|
+
right: `${prefix}report.sqlite`,
|
|
99
|
+
maxDepth: 33,
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The 8,192-code-unit suffix limit and 32,768-code-unit path limits remain fixed.
|
|
104
|
+
Ordinary-component validation, Windows path controls, identity checks, and
|
|
105
|
+
cleanup rules also remain unchanged.
|
|
106
|
+
|
|
107
|
+
Operation budgets grow proportionally from the actual admitted suffix depth,
|
|
108
|
+
not the requested `maxDepth`. For actual depth `D`, let `B = max(32, D)`.
|
|
109
|
+
Directory-creation attempts are limited to `4 × B`, successfully created
|
|
110
|
+
directories to `2 × B`, and forward filesystem observations to `min(32,768, 4 × B²)`.
|
|
111
|
+
The quadratic observation allowance accommodates rechecks of owned ancestors.
|
|
112
|
+
|
|
113
|
+
For example, 33 components allow 132 creation attempts, 66 created directories,
|
|
114
|
+
and 4,356 forward observations; 65 components allow 260, 130, and 16,900.
|
|
115
|
+
Specifying a large `maxDepth` for a short suffix keeps the original budgets.
|
|
116
|
+
The suffix-length limit bounds actual depth to at most 4,096, so every budget
|
|
117
|
+
remains a finite safe integer.
|
|
118
|
+
|
|
119
|
+
Admission does not guarantee a boolean result. Repeated collisions, many
|
|
120
|
+
normalization probes, filesystem limits, or identity failures can exhaust the
|
|
121
|
+
budget or prevent an observation. The helper then returns `undefined` after
|
|
122
|
+
cleanup attempts; callers must preserve their explicit ambiguity policy.
|
|
123
|
+
|
|
79
124
|
Input limits are checked even for identical suffixes. Dynamic budgets count work
|
|
80
125
|
across the whole call, including collision retries; removing a probe does not
|
|
81
126
|
restore its creation budget. Cleanup is still attempted when a forward budget is
|
package/docs/permissions.md
CHANGED
|
@@ -102,6 +102,19 @@ characters, including a trailing `…` when truncated. Diagnostics do not copy
|
|
|
102
102
|
stdout or read target file contents. The separate `errorCause` retains the
|
|
103
103
|
original exception for restricted local diagnosis; do not serialize or expose
|
|
104
104
|
it as display text.
|
|
105
|
+
Custom executors may reject with any JavaScript value. The fallback display
|
|
106
|
+
formatter handles primitives directly and reads only string-valued `name` and
|
|
107
|
+
`message` data descriptors through a small, fixed prototype budget. It does not
|
|
108
|
+
coerce objects, invoke accessors, or inspect proxy targets; unavailable display
|
|
109
|
+
facts use a bounded generic reason. Command fields follow the same best-effort
|
|
110
|
+
data-descriptor rule. Raw string, `Buffer`, or genuine `Uint8Array` stderr
|
|
111
|
+
retains the sanitization above. Byte stderr is copied through captured
|
|
112
|
+
typed-array intrinsics into a private bounded snapshot before replacement-based
|
|
113
|
+
UTF-8 decoding; receiver properties, iterators, constructors, and altered
|
|
114
|
+
prototypes are not consulted. Detached or out-of-bounds byte views contribute
|
|
115
|
+
no stderr detail. These diagnostic limits do not relax permission policy:
|
|
116
|
+
incomplete owner or ACL inspection remains unverified, and `errorCause` remains
|
|
117
|
+
the exact rejected value even when no display metadata is safe to obtain.
|
|
105
118
|
The parser and remediation command builders remain on the advanced surface for
|
|
106
119
|
CLIs processing captured `icacls` output or presenting an explicit repair.
|
|
107
120
|
Runtime inspection does not parse that display text. A null DACL reports
|
package/docs/public-api.md
CHANGED
|
@@ -43,8 +43,9 @@ The advanced root-file primitive exports `OpenRootFileParams`,
|
|
|
43
43
|
`RootFileOpenFailureReason`. These are composition types for callers building
|
|
44
44
|
their own pinned-open flow, not substitutes for the higher-level `Root` verbs.
|
|
45
45
|
|
|
46
|
-
`copyFileHandle` and `
|
|
47
|
-
regular files without taking over their
|
|
46
|
+
`copyFileHandle` and `copyFileDescriptorSync` share `CopyFileHandleOptions` to
|
|
47
|
+
transfer bytes between already-open regular files without taking over their
|
|
48
|
+
cursors, lifetime, or publication.
|
|
48
49
|
See [borrowed-handle transfers](copy.md#borrowed-filehandle-transfers).
|
|
49
50
|
|
|
50
51
|
`readDirectoryIdentity`, `assertDirectoryIdentitySync`, and `DirectoryIdentity`
|