@openclaw/fs-safe 0.15.0 → 0.17.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 +86 -0
- package/README.md +36 -7
- package/dist/absolute-path.d.ts.map +1 -1
- package/dist/absolute-path.js +2 -8
- package/dist/advanced.d.ts +3 -0
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +3 -0
- package/dist/archive-deadline.d.ts.map +1 -1
- package/dist/archive-deadline.js +22 -23
- package/dist/archive-kind.d.ts +0 -1
- package/dist/archive-kind.d.ts.map +1 -1
- package/dist/archive-kind.js +5 -17
- package/dist/archive-merge.d.ts +1 -0
- package/dist/archive-merge.d.ts.map +1 -1
- package/dist/archive-merge.js +4 -4
- package/dist/archive-native.d.ts +1 -0
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +1 -0
- package/dist/archive-options.d.ts +2 -0
- package/dist/archive-options.d.ts.map +1 -1
- package/dist/archive-parser.wasm +0 -0
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +6 -7
- package/dist/archive-tar-stream.d.ts +3 -0
- package/dist/archive-tar-stream.d.ts.map +1 -1
- package/dist/archive-tar-stream.js +56 -37
- package/dist/archive-tar-wasm.d.ts +16 -4
- package/dist/archive-tar-wasm.d.ts.map +1 -1
- package/dist/archive-tar-wasm.js +134 -34
- package/dist/archive-zip-count.d.ts.map +1 -1
- package/dist/archive-zip-count.js +21 -1
- package/dist/archive-zip-directory.d.ts.map +1 -1
- package/dist/archive-zip-directory.js +23 -1
- package/dist/archive-zip-loader.d.ts +2 -0
- package/dist/archive-zip-loader.d.ts.map +1 -1
- package/dist/archive-zip-loader.js +7 -0
- package/dist/archive-zip-names.d.ts.map +1 -1
- package/dist/archive-zip-names.js +7 -2
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +14 -9
- package/dist/byte-view.d.ts +3 -0
- package/dist/byte-view.d.ts.map +1 -0
- package/dist/byte-view.js +13 -0
- package/dist/clone-metadata.d.ts +1 -0
- package/dist/clone-metadata.d.ts.map +1 -1
- package/dist/clone-metadata.js +6 -2
- package/dist/create-directory.d.ts +20 -0
- package/dist/create-directory.d.ts.map +1 -0
- package/dist/create-directory.js +130 -0
- package/dist/create-file-async.d.ts +7 -0
- package/dist/create-file-async.d.ts.map +1 -0
- package/dist/create-file-async.js +121 -0
- package/dist/create-file.d.ts +8 -0
- package/dist/create-file.d.ts.map +1 -0
- package/dist/create-file.js +190 -0
- package/dist/create-owned-file.d.ts +8 -0
- package/dist/create-owned-file.d.ts.map +1 -0
- package/dist/create-owned-file.js +16 -0
- package/dist/create.d.ts +4 -0
- package/dist/create.d.ts.map +1 -0
- package/dist/create.js +2 -0
- package/dist/creation-darwin.d.ts +6 -0
- package/dist/creation-darwin.d.ts.map +1 -0
- package/dist/creation-darwin.js +70 -0
- package/dist/creation-file-state.d.ts +19 -0
- package/dist/creation-file-state.d.ts.map +1 -0
- package/dist/creation-file-state.js +118 -0
- package/dist/creation-path.d.ts +21 -0
- package/dist/creation-path.d.ts.map +1 -0
- package/dist/creation-path.js +71 -0
- package/dist/creation-permissions.d.ts +19 -0
- package/dist/creation-permissions.d.ts.map +1 -0
- package/dist/creation-permissions.js +125 -0
- package/dist/directory-durability.d.ts +7 -7
- package/dist/directory-durability.d.ts.map +1 -1
- package/dist/directory-durability.js +22 -80
- package/dist/directory-guard.d.ts +3 -0
- package/dist/directory-guard.d.ts.map +1 -1
- package/dist/directory-mode-node.d.ts +2 -0
- package/dist/directory-mode-node.d.ts.map +1 -1
- package/dist/directory-mode-node.js +8 -0
- package/dist/directory-receipt.d.ts +24 -0
- package/dist/directory-receipt.d.ts.map +1 -0
- package/dist/directory-receipt.js +123 -0
- package/dist/file-cleanup.d.ts +20 -0
- package/dist/file-cleanup.d.ts.map +1 -0
- package/dist/file-cleanup.js +81 -0
- package/dist/file-contents.d.ts +6 -0
- package/dist/file-contents.d.ts.map +1 -0
- package/dist/file-contents.js +40 -0
- package/dist/file-hash.d.ts.map +1 -1
- package/dist/file-hash.js +16 -4
- package/dist/file-lock-sync-root-acquire.d.ts.map +1 -1
- package/dist/file-lock-sync-root-acquire.js +3 -0
- package/dist/file-lock-sync-root-held.d.ts +1 -2
- package/dist/file-lock-sync-root-held.d.ts.map +1 -1
- package/dist/file-lock-sync-root-held.js +7 -5
- package/dist/file-lock-sync-stale-admission.d.ts.map +1 -1
- package/dist/file-lock-sync-stale-admission.js +3 -0
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +8 -11
- package/dist/file-observation.d.ts +1 -1
- package/dist/file-observation.d.ts.map +1 -1
- package/dist/file-store-boundary.d.ts +2 -6
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +3 -9
- package/dist/file-store-sync-write.d.ts.map +1 -1
- package/dist/file-store-sync-write.js +2 -5
- package/dist/file-store.js +3 -3
- package/dist/guarded-mkdir.d.ts +1 -0
- package/dist/guarded-mkdir.d.ts.map +1 -1
- package/dist/guarded-mkdir.js +27 -19
- package/dist/install-path.d.ts.map +1 -1
- package/dist/install-path.js +2 -5
- package/dist/json-durable-queue-ownership.d.ts.map +1 -1
- package/dist/json-durable-queue-ownership.js +2 -6
- package/dist/json-durable-queue-paths.d.ts.map +1 -1
- package/dist/json-durable-queue-paths.js +2 -24
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +10 -9
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +32 -75
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +19 -21
- package/dist/move-path-cleanup.d.ts +5 -19
- package/dist/move-path-cleanup.d.ts.map +1 -1
- package/dist/move-path-cleanup.js +57 -21
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +63 -40
- package/dist/native-binding.d.ts +11 -1
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-fallback-warning.d.ts +4 -0
- package/dist/native-fallback-warning.d.ts.map +1 -0
- package/dist/native-fallback-warning.js +11 -0
- package/dist/native-operations.d.ts +0 -2
- package/dist/native-operations.d.ts.map +1 -1
- package/dist/native-operations.js +0 -24
- package/dist/native-parent-admission.d.ts +2 -0
- package/dist/native-parent-admission.d.ts.map +1 -1
- package/dist/native-parent-admission.js +3 -2
- package/dist/native-pinned-write-windows.d.ts +1 -1
- package/dist/native-pinned-write-windows.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +173 -28
- package/dist/native-pinned-write.d.ts.map +1 -1
- package/dist/native-pinned-write.js +19 -3
- package/dist/native-policy-parent-windows.d.ts.map +1 -1
- package/dist/native-policy-parent-windows.js +15 -6
- package/dist/native-staged-file.d.ts +5 -3
- package/dist/native-staged-file.d.ts.map +1 -1
- package/dist/native-staged-file.js +90 -40
- package/dist/native.js +2 -2
- package/dist/opened-realpath.d.ts.map +1 -1
- package/dist/opened-realpath.js +11 -2
- package/dist/owner-dacl.d.ts.map +1 -1
- package/dist/owner-dacl.js +10 -4
- package/dist/path.d.ts.map +1 -1
- package/dist/path.js +2 -1
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +3 -17
- package/dist/pinned-write-input.d.ts +4 -0
- package/dist/pinned-write-input.d.ts.map +1 -0
- package/dist/pinned-write-input.js +35 -0
- package/dist/pinned-write-mode.d.ts +5 -0
- package/dist/pinned-write-mode.d.ts.map +1 -0
- package/dist/pinned-write-mode.js +31 -0
- package/dist/pinned-write-staged.d.ts +6 -0
- package/dist/pinned-write-staged.d.ts.map +1 -0
- package/dist/pinned-write-staged.js +186 -0
- package/dist/pinned-write-types.d.ts +3 -0
- package/dist/pinned-write-types.d.ts.map +1 -1
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +41 -147
- package/dist/private-directory.d.ts.map +1 -1
- package/dist/private-directory.js +18 -4
- package/dist/private-producer-handoff-sync.d.ts +14 -0
- package/dist/private-producer-handoff-sync.d.ts.map +1 -0
- package/dist/private-producer-handoff-sync.js +114 -0
- package/dist/private-producer-handoff.d.ts +22 -4
- package/dist/private-producer-handoff.d.ts.map +1 -1
- package/dist/private-producer-handoff.js +140 -77
- package/dist/publish-copy-stage.d.ts +2 -1
- package/dist/publish-copy-stage.d.ts.map +1 -1
- package/dist/publish-copy-stage.js +16 -7
- package/dist/publish-file.d.ts +2 -2
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +58 -98
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +35 -44
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +28 -26
- package/dist/replace-file-copy-source.d.ts.map +1 -1
- package/dist/replace-file-copy-source.js +13 -22
- package/dist/replace-file-descriptor.d.ts.map +1 -1
- package/dist/replace-file-descriptor.js +10 -16
- package/dist/replace-file-temp-owner.d.ts +0 -7
- package/dist/replace-file-temp-owner.d.ts.map +1 -1
- package/dist/replace-file-temp-owner.js +9 -60
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +9 -13
- package/dist/root-create-input.d.ts +2 -1
- package/dist/root-create-input.d.ts.map +1 -1
- package/dist/root-create-input.js +13 -4
- package/dist/root-directory-creation.d.ts +3 -3
- package/dist/root-directory-creation.d.ts.map +1 -1
- package/dist/root-directory-creation.js +15 -3
- package/dist/root-directory-list.d.ts.map +1 -1
- package/dist/root-directory-list.js +20 -3
- package/dist/root-file-final-admission.d.ts +1 -1
- package/dist/root-file-final-admission.d.ts.map +1 -1
- package/dist/root-file-final-admission.js +5 -2
- package/dist/root-file.d.ts.map +1 -1
- package/dist/root-file.js +3 -2
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +78 -27
- package/dist/root-move-noreplace.d.ts +2 -0
- package/dist/root-move-noreplace.d.ts.map +1 -1
- package/dist/root-move-noreplace.js +22 -13
- package/dist/root-options.d.ts +12 -4
- package/dist/root-options.d.ts.map +1 -1
- package/dist/root-path-stat.d.ts.map +1 -1
- package/dist/root-path-stat.js +59 -7
- package/dist/root-read-admission.d.ts.map +1 -1
- package/dist/root-read-admission.js +7 -2
- package/dist/root-remove.d.ts.map +1 -1
- package/dist/root-remove.js +15 -1
- package/dist/root-write-publication.js +1 -1
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +1 -0
- package/dist/secure-file-windows.d.ts +6 -0
- package/dist/secure-file-windows.d.ts.map +1 -1
- package/dist/secure-file-windows.js +34 -117
- package/dist/secure-file.js +2 -2
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +4 -6
- package/dist/sidecar-lock-handle.d.ts +3 -0
- package/dist/sidecar-lock-handle.d.ts.map +1 -1
- package/dist/sidecar-lock-handle.js +6 -0
- package/dist/sidecar-lock-reclaim.d.ts +1 -1
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +11 -8
- package/dist/sidecar-lock-root.d.ts.map +1 -1
- package/dist/sidecar-lock-root.js +2 -1
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +3 -5
- package/dist/staged-directory.d.ts +2 -2
- package/dist/staged-directory.d.ts.map +1 -1
- package/dist/staged-directory.js +6 -6
- package/dist/staged-file-settlement.d.ts +17 -0
- package/dist/staged-file-settlement.d.ts.map +1 -0
- package/dist/staged-file-settlement.js +57 -0
- package/dist/strict-file-identity.d.ts +1 -1
- package/dist/strict-file-identity.d.ts.map +1 -1
- package/dist/strict-file-identity.js +9 -9
- package/dist/symlink-parents.d.ts.map +1 -1
- package/dist/symlink-parents.js +2 -27
- package/dist/temp-workspace-owner.js +4 -4
- package/dist/unicode-path.d.ts.map +1 -1
- package/dist/unicode-path.js +3 -0
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +4 -2
- package/dist/windows-owner.d.ts.map +1 -1
- package/dist/windows-owner.js +2 -1
- package/dist/windows-security-bridge.cs +336 -0
- package/dist/windows-security-bridge.ps1 +15 -0
- package/dist/windows-security-command.d.ts +26 -0
- package/dist/windows-security-command.d.ts.map +1 -0
- package/dist/windows-security-command.js +363 -0
- package/dist/windows-security-facts.d.ts +6 -0
- package/dist/windows-security-facts.d.ts.map +1 -0
- package/dist/windows-security-facts.js +108 -0
- package/dist/write-file-handle.d.ts +7 -0
- package/dist/write-file-handle.d.ts.map +1 -1
- package/dist/write-file-handle.js +23 -0
- package/dist/write-open-flags.d.ts.map +1 -1
- package/dist/write-open-flags.js +1 -8
- package/dist/write-queue.d.ts.map +1 -1
- package/dist/write-queue.js +1 -4
- package/docs/advanced.md +71 -2
- package/docs/archive.md +102 -39
- package/docs/atomic.md +29 -5
- package/docs/config.md +6 -2
- package/docs/contributing.md +48 -4
- package/docs/copy.md +2 -0
- package/docs/creation.md +132 -0
- package/docs/durability.md +59 -0
- package/docs/file-contents.md +68 -0
- package/docs/install.md +31 -7
- package/docs/json.md +5 -4
- package/docs/local-roots.md +2 -0
- package/docs/migrating-to-0.5.md +15 -6
- package/docs/migrating-to-0.6.md +9 -4
- package/docs/mutation-policy-proof.md +5 -3
- package/docs/native-helper.md +22 -9
- package/docs/native.md +47 -15
- package/docs/path.md +4 -4
- package/docs/permissions.md +37 -14
- package/docs/public-api.md +5 -0
- package/docs/quickstart.md +1 -1
- package/docs/reading.md +2 -2
- package/docs/regular-file.md +3 -0
- package/docs/root.md +43 -0
- package/docs/secret-file.md +11 -2
- package/docs/secure-file.md +9 -4
- package/docs/sidecar-lock.md +14 -5
- package/docs/staged-file.md +9 -3
- package/docs/store.md +3 -1
- package/docs/temp.md +4 -1
- package/docs/types.md +18 -2
- package/docs/walk.md +7 -0
- package/docs/writing.md +80 -7
- package/package.json +18 -15
package/docs/advanced.md
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
|
|
20
20
|
## What lives here
|
|
21
21
|
|
|
22
|
-
The exports group into a handful of themes.
|
|
22
|
+
The exports group into a handful of themes. Documented helpers link to their contract below or a dedicated page; everything else is reference-only and tracked here.
|
|
23
23
|
|
|
24
24
|
### Path scopes and root paths
|
|
25
25
|
|
|
@@ -28,7 +28,8 @@ The exports group into a handful of themes. Each documented helper has its own p
|
|
|
28
28
|
| `pathScope`, `PathScope`, `PathScopeOptions`, `PathScopeResolveOptions` | [path-scope.md](path-scope.md) | Absolute-path boundary helper with `Result`-shaped returns. |
|
|
29
29
|
| `ensureDirectoryWithinRoot` | [path-scope.md](path-scope.md#ensuredir-rel-options) | Create a directory while enforcing the root boundary; same result contract as `pathScope().ensureDir()`. |
|
|
30
30
|
| `resolvePathWithinRoot`, `resolvePathsWithinRoot` | – | Resolve one or many relative paths against a trusted root. |
|
|
31
|
-
| `resolveExistingPathsWithinRoot
|
|
31
|
+
| `resolveExistingPathsWithinRoot` | – | Validate existing regular files inside the root, while allowing missing paths. |
|
|
32
|
+
| `resolveStrictExistingPathsWithinRoot` | – | Require every target to exist as a regular non-symlink file inside the root. |
|
|
32
33
|
| `resolveWritablePathWithinRoot` | – | Resolve a write target inside a root. |
|
|
33
34
|
| `resolveRootPath`, `resolveRootPathSync`, `ResolvedRootPath`, `ROOT_PATH_ALIAS_POLICIES`, `RootPathAliasPolicy` | – | Resolve a root directory honoring alias policy. |
|
|
34
35
|
| `resolvePathViaExistingAncestorSync` | – | Walk to an existing ancestor for paths whose tail does not yet exist. |
|
|
@@ -68,8 +69,11 @@ Operational filesystem failures such as permissions or I/O errors are rethrown.
|
|
|
68
69
|
| Export | Page | Notes |
|
|
69
70
|
|---|---|---|
|
|
70
71
|
| `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. |
|
|
72
|
+
| `createDirectory`, `createDirectorySync`, `createFileSync` | [Exclusive leaf creation](creation.md) | Create one exclusive entry under an existing trusted parent, optionally with private permissions; file creation returns an owned disposable descriptor. |
|
|
71
73
|
| `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. |
|
|
74
|
+
| `writeFileWindowFully`, `WriteFileWindowOptions` | [Borrowed-handle writes](#borrowed-handle-writes) | Write all supplied bytes at an explicit position or the current cursor, completing short writes with cancellation and per-write authority checks. |
|
|
72
75
|
| `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. |
|
|
76
|
+
| `sameFileContentsSync`, `SameFileContentsOptions` | [Exact file comparison](file-contents.md) | Compare borrowed regular-file descriptors byte for byte through EOF with bounded memory and an optional per-file byte limit, preserving both cursors and lifetimes. |
|
|
73
77
|
| `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
78
|
| `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
79
|
| `appendRegularFile`, `appendRegularFileSync`, `readRegularFile`, `readRegularFileSync`, `statRegularFile`, `statRegularFileSync`, `resolveRegularFileAppendFlags`, `AppendRegularFileOptions`, `RegularFileStatResult` | [regular-file.md](regular-file.md) | Type-checked regular-file I/O. |
|
|
@@ -149,6 +153,71 @@ component is followed by another segment, both helpers throw
|
|
|
149
153
|
`FsSafeError("not-file")` before the platform can expose that state as POSIX
|
|
150
154
|
`ENOTDIR` or Windows `ENOENT`.
|
|
151
155
|
|
|
156
|
+
#### Borrowed-handle writes
|
|
157
|
+
|
|
158
|
+
Use `writeFileWindowFully()` when you already own a writable file handle and
|
|
159
|
+
need to complete a byte-window write, including positive short writes.
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
import { root } from "@openclaw/fs-safe";
|
|
163
|
+
import { writeFileWindowFully } from "@openclaw/fs-safe/advanced";
|
|
164
|
+
|
|
165
|
+
const workspace = await root("/srv/workspace");
|
|
166
|
+
await using opened = await workspace.openWritable("record.bin", { writeMode: "update" });
|
|
167
|
+
await writeFileWindowFully(opened.handle, Buffer.from([1, 2, 3]), 16);
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
type WriteFileWindowOptions = {
|
|
172
|
+
signal?: AbortSignal;
|
|
173
|
+
assertBeforeMutation?: () => void;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
function writeFileWindowFully(
|
|
177
|
+
handle: import("node:fs/promises").FileHandle,
|
|
178
|
+
bytes: Uint8Array,
|
|
179
|
+
position: number | null,
|
|
180
|
+
options?: WriteFileWindowOptions,
|
|
181
|
+
): Promise<void>;
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
A numeric `position` writes at that offset without moving the handle's cursor.
|
|
185
|
+
It and the exclusive window end (`position + bytes.byteLength`) must be
|
|
186
|
+
non-negative safe integers; invalid ranges throw `RangeError` before mutation.
|
|
187
|
+
Bounds come from the intrinsic byte view, ignoring shadowed metadata properties.
|
|
188
|
+
Pass `null` to write at and advance the current cursor. Each syscall writes at
|
|
189
|
+
most 512 KiB. A write that makes no progress throws
|
|
190
|
+
`FsSafeError("helper-failed")`; filesystem errors propagate unchanged.
|
|
191
|
+
Empty input still validates the range and checks cancellation, but performs no
|
|
192
|
+
I/O and does not call `assertBeforeMutation`.
|
|
193
|
+
|
|
194
|
+
The caller must supply a writable regular-file handle, opened **without append
|
|
195
|
+
mode** for numeric positions. Some operating systems ignore positioned-write
|
|
196
|
+
offsets on append handles, and this helper does not inspect file type or open
|
|
197
|
+
flags. Opening, path admission, identity checks, and closing remain the caller's
|
|
198
|
+
responsibility. Keep the handle open and the borrowed bytes unchanged, attached,
|
|
199
|
+
and accessible until the promise settles; avoid concurrent I/O when it can change
|
|
200
|
+
the intended contents or shared cursor. The helper does not acquire a lock.
|
|
201
|
+
|
|
202
|
+
`assertBeforeMutation` runs synchronously immediately before every write,
|
|
203
|
+
including short-write retries. A thrown value propagates unchanged; a Promise or
|
|
204
|
+
thenable return rejects with `TypeError` before that write. The callback must not
|
|
205
|
+
modify the payload or handle. It does not run as a final completion check; the
|
|
206
|
+
caller owns any authority check before later publication or other mutations.
|
|
207
|
+
|
|
208
|
+
`signal` is checked at admission, before and after each authority callback, and
|
|
209
|
+
after each pending write settles. Cancellation waits for an in-flight write and
|
|
210
|
+
then rejects with the signal's reason without starting another syscall. If that
|
|
211
|
+
write fails, its filesystem error or zero-progress failure takes precedence over cancellation. Already
|
|
212
|
+
written bytes remain changed; there is no rollback or hidden write after the
|
|
213
|
+
promise settles.
|
|
214
|
+
|
|
215
|
+
The helper neither truncates an existing suffix nor changes permissions,
|
|
216
|
+
synchronizes, or closes the handle. Callers retain those responsibilities and
|
|
217
|
+
any wider transaction policy. For complete replacement with best-effort
|
|
218
|
+
rollback, use [`overwriteFileHandle()`](in-place-write.md); for root-bounded
|
|
219
|
+
atomic replacement, use [`Root.write()`](writing.md).
|
|
220
|
+
|
|
152
221
|
### Local roots and file URLs
|
|
153
222
|
|
|
154
223
|
| Export | Page | Notes |
|
package/docs/archive.md
CHANGED
|
@@ -3,11 +3,27 @@
|
|
|
3
3
|
`@openclaw/fs-safe/archive` extracts ZIP and TAR archives behind one API, with traversal checks, blocked-link-type rejection, and entry-count and byte budgets. When the native binding is available for the current platform, Rust streams ZIP, TAR, gzip, zstd, and bzip2 while TypeScript remains the sole policy owner; every accepted output is created fd-relative in a private staging root. Extraction then merges through the same safe-open boundary used by direct writes — a symlinked entry can't trick the merge into following an out-of-tree path.
|
|
4
4
|
|
|
5
5
|
TAR admission uses one Rust core compiled into both the native binding and a
|
|
6
|
-
bundled, import-free WebAssembly module.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
TAR
|
|
6
|
+
bundled, import-free WebAssembly module. In `off`, or `auto` when the native
|
|
7
|
+
binding is unavailable, extraction and bounded entry reads use that module for
|
|
8
|
+
plain TAR, gzip, zstd, and bzip2. Zstd and bzip2 use bundled WASM builds of the
|
|
9
|
+
same codec implementations used by native; gzip uses Node's built-in decoder.
|
|
10
|
+
These TAR routes work with all optional dependencies omitted and need no
|
|
11
|
+
runtime interpreter, download, install script, or consumer compiler toolchain.
|
|
12
|
+
ZIP fallback still requires optional `jszip`.
|
|
13
|
+
|
|
14
|
+
The shared TAR parser reuses the already-validated owned path for ordinary
|
|
15
|
+
members. Original header names and USTAR prefixes still undergo validation
|
|
16
|
+
even when PAX or GNU metadata supplies an override; effective override paths
|
|
17
|
+
retain their separate checks. Empty USTAR prefixes retain field decoding and
|
|
18
|
+
padding checks; path validation applies to nonempty prefixes. Joining an
|
|
19
|
+
admitted prefix and name with a separator preserves their checked components,
|
|
20
|
+
so the parser does not repeat the same component validation on the joined path.
|
|
21
|
+
|
|
22
|
+
`auto` prefers an available native binding; a native operation failure is
|
|
23
|
+
terminal and never retries through WASM. `require` rejects a missing binding
|
|
24
|
+
with `FsSafeError("helper-unavailable")`, including for zstd/bzip2 suffix
|
|
25
|
+
resolution. The separate `inspectTarArchive()` API still accepts only plain TAR
|
|
26
|
+
and gzip.
|
|
11
27
|
|
|
12
28
|
```ts
|
|
13
29
|
import { extractArchive, resolveArchiveKind } from "@openclaw/fs-safe/archive";
|
|
@@ -23,6 +39,7 @@ await extractArchive({
|
|
|
23
39
|
timeoutMs: 15_000, // hard budget; active destination mutation is joined
|
|
24
40
|
stripComponents: 0, // tar-style strip-leading-dirs
|
|
25
41
|
entryModes: "clamp", // default; use "preserve" for archive rwx bits
|
|
42
|
+
entryUmask: 0, // default; remove these bits from final modes
|
|
26
43
|
entryFilter: ({ path, kind, size }) => "extract",
|
|
27
44
|
onFiltered: "reject-archive", // default; opt into "skip-entry" explicitly
|
|
28
45
|
limits: {
|
|
@@ -42,7 +59,7 @@ await extractArchive({
|
|
|
42
59
|
type ExtractArchiveOptions = {
|
|
43
60
|
archivePath: string; // absolute path to the archive
|
|
44
61
|
destDir: string; // absolute destination directory; must already exist
|
|
45
|
-
timeoutMs: number; // positive
|
|
62
|
+
timeoutMs: number; // positive elapsed-time budget; <= 0/non-finite disables it
|
|
46
63
|
durable?: boolean; // false; opt into syncing published files and directories before completion
|
|
47
64
|
kind?: ArchiveKind; // "zip" | "tar" | "tar-zstd" | "tar-bzip2"
|
|
48
65
|
stripComponents?: number; // strip N leading dirs from entry paths
|
|
@@ -50,6 +67,7 @@ type ExtractArchiveOptions = {
|
|
|
50
67
|
limits?: ArchiveExtractLimits;
|
|
51
68
|
logger?: ArchiveLogger; // { info?, warn? }
|
|
52
69
|
entryModes?: "clamp" | "preserve";
|
|
70
|
+
entryUmask?: number; // integer 0..0o777; defaults to 0
|
|
53
71
|
entryFilter?: (entry: { path: string; kind: ArchiveEntryKind; size: number }) =>
|
|
54
72
|
"extract" | "skip";
|
|
55
73
|
onFiltered?: "reject-archive" | "skip-entry";
|
|
@@ -63,6 +81,12 @@ once, deepest first, and finally the destination directory. All work stays insid
|
|
|
63
81
|
the extraction deadline; active syncs are joined before rejection. File sync
|
|
64
82
|
failures use the same error surface as `Root.copyIn()`; directory I/O failures
|
|
65
83
|
also reject, with the existing platform limitations on directory flushing.
|
|
84
|
+
|
|
85
|
+
Deadline checks use a monotonic clock, including before queued mutations start
|
|
86
|
+
and before reporting success. Synchronous caller code can delay the timer, but
|
|
87
|
+
cannot permit the next operation after the budget expires. This does not
|
|
88
|
+
interrupt a callback halfway through execution or replace its own thrown error;
|
|
89
|
+
active destination mutations are still joined before timeout rejection.
|
|
66
90
|
Files whose final mode prevents reading, including `0o000` and write-only files,
|
|
67
91
|
sync once through the copy's retained descriptor during publication. Permissions
|
|
68
92
|
are never widened to reopen them. Directory modes are finalized after the file
|
|
@@ -95,6 +119,15 @@ including a mode containing only stripped special bits, stays zero under
|
|
|
95
119
|
directories; ZIP UNIX creator records with zero attributes are explicit zero,
|
|
96
120
|
while non-UNIX ZIP records use the absent-metadata defaults.
|
|
97
121
|
|
|
122
|
+
`entryUmask` removes permission bits after the selected mode policy: final modes
|
|
123
|
+
are the policy result `& ~entryUmask`. It applies to files, explicit directories,
|
|
124
|
+
and implicit parent directories, including existing destination directories.
|
|
125
|
+
The destination root and private staging modes are unchanged. The default `0`
|
|
126
|
+
preserves existing behavior; invalid masks reject before extraction begins.
|
|
127
|
+
fs-safe neither reads nor changes the process umask. Pass
|
|
128
|
+
`entryUmask: process.umask()` explicitly when that is the caller's policy.
|
|
129
|
+
Windows retains the POSIX-mode limitations described below.
|
|
130
|
+
|
|
98
131
|
TAR mode fields containing only NUL/ASCII-space padding use absent defaults.
|
|
99
132
|
Both backends recognize GNU binary modes, including signed values,
|
|
100
133
|
within JavaScript's safe-integer range before masking permission bits.
|
|
@@ -108,7 +141,7 @@ stay `0o600` and directories `0o700` until publication. Files receive their fina
|
|
|
108
141
|
mode through the guarded copy's owned writer descriptor. Directories are pinned
|
|
109
142
|
before descending and finalized after their children, including empty and
|
|
110
143
|
restrictive directories. Explicit accepted directory modes win regardless of
|
|
111
|
-
archive order; implicit parents receive `0o755`. Existing destination directories
|
|
144
|
+
archive order; implicit parents receive `0o755 & ~entryUmask`. Existing destination directories
|
|
112
145
|
also receive the requested final mode. They are never temporarily widened to
|
|
113
146
|
allow child writes; insufficient write/search access still rejects.
|
|
114
147
|
|
|
@@ -146,6 +179,13 @@ between native and JavaScript paths rather than reimplementing it in Rust.
|
|
|
146
179
|
|
|
147
180
|
ZIP extraction and bounded reads admit every physical central-directory record and its referenced local header before either decoder can normalize or collapse names. Raw names and valid Unicode Path names must pass traversal checks before stripping, filtering, or selecting a requested member; duplicate or colliding names reject with `entry-path`, even in unrelated or skipped members. Materially conflicting local/central or Unicode interpretations, malformed critical metadata, and ambiguous framing reject with `ArchiveFormatError`. Harmless internal separator and dot-component equivalence is allowed only after validation; every raw and Unicode interpretation must also agree on whether its name ends in `/` or `\`. Ordinary legacy filename decoding remains backend-selected. Native ZIP extraction groups nearby metadata reads into at most two 4 KiB read-ahead buffers per admission pass; larger records retain separately bounded reads. Buffered record views remain stable across eviction, and cached work periodically yields for deadline checks.
|
|
148
181
|
|
|
182
|
+
ZIP end-record admission searches the bounded comment window for signatures
|
|
183
|
+
while retaining complete comment-length and ambiguity checks. Dense signature
|
|
184
|
+
sequences fall back to the bounded byte scan.
|
|
185
|
+
The separate `readZipCentralDirectoryEntryCount(buffer)` hint uses bounded
|
|
186
|
+
reverse searches for comments and retains its latest-valid-record selection;
|
|
187
|
+
it does not replace strict archive admission.
|
|
188
|
+
|
|
149
189
|
ZIP admission establishes each entry's kind before callbacks: a high-word UNIX
|
|
150
190
|
symlink type takes precedence regardless of creator, followed by the DOS directory
|
|
151
191
|
bit, the exact UNIX directory type, or a terminal slash/backslash. Native manifests
|
|
@@ -177,6 +217,10 @@ decoded validation. Unicode Path admission is shared only when both the raw name
|
|
|
177
217
|
and the complete Unicode fields match; different fields still verify their own
|
|
178
218
|
CRC and interpretation. Shared backing memory is checked independently. Decoded
|
|
179
219
|
name validation is not reused across entries or archives.
|
|
220
|
+
UTF-8-flagged ASCII names in nonshared backing memory reuse their raw-path
|
|
221
|
+
validation, and an identical decoded spelling reuses its canonical key. Shared
|
|
222
|
+
name bytes still undergo independent decoding and validation; Unicode Path
|
|
223
|
+
fields retain their own CRC and interpretation checks.
|
|
180
224
|
|
|
181
225
|
`stripComponents` removes leading nonempty, non-`.` path components after
|
|
182
226
|
normalizing separators. For example, `./pkg/hello.txt` with
|
|
@@ -188,12 +232,15 @@ collision checks, writes, and mode application agree.
|
|
|
188
232
|
|
|
189
233
|
An `entryFilter` sees the validated **canonical effective archive path before
|
|
190
234
|
stripping**, entry kind, and declared size. On every JavaScript and native
|
|
191
|
-
ZIP/TAR backend (including gzip and
|
|
235
|
+
ZIP/TAR backend (including gzip, zstd, and bzip2), backslashes become `/`,
|
|
192
236
|
empty and `.` components are removed, and trailing separators are removed from
|
|
193
237
|
directory paths. For example, `./pkg//state\cache/value` is presented as
|
|
194
238
|
`pkg/state/cache/value`, even with `stripComponents: 1`. Case and Unicode
|
|
195
239
|
spelling are preserved. Local PAX `path`, GNU long-name, and supported ZIP
|
|
196
240
|
Unicode Path names use the same canonicalization.
|
|
241
|
+
Callbacks follow physical archive order, including ZIP names that look like
|
|
242
|
+
integer object keys. The public ZIP loader's `files` object retains ordinary
|
|
243
|
+
JavaScript object enumeration and mutation behavior.
|
|
197
244
|
|
|
198
245
|
Raw paths undergo traversal, absolute/drive-path, and NUL validation **before**
|
|
199
246
|
canonicalization; normalization cannot turn an unsafe path into an accepted
|
|
@@ -368,14 +415,16 @@ Native gzip, zstd, and bzip2 readers check cancellation before refilling
|
|
|
368
415
|
compressed input and before each decoded read, including buffered output. These checks
|
|
369
416
|
apply to file extraction and in-memory member reads; they cannot interrupt an
|
|
370
417
|
already-running filesystem read or a decoder step using already-buffered input.
|
|
418
|
+
Portable zstd/bzip2 decoding checks cancellation between bounded codec steps and
|
|
419
|
+
periodically yields to the event loop, including while consuming output-free
|
|
420
|
+
members. An individual WASM call cannot be interrupted. Teardown joins the input,
|
|
421
|
+
parser, and any Node decoder streams before disposing their shared WASM state.
|
|
371
422
|
|
|
372
423
|
### Raw TAR framing
|
|
373
424
|
|
|
374
425
|
Extraction and bounded reads admit the complete decoded TAR stream through the
|
|
375
|
-
shared Rust core. This applies to plain TAR,
|
|
376
|
-
|
|
377
|
-
or fallback policy. The native and WASM builds enforce the same
|
|
378
|
-
framing rules:
|
|
426
|
+
shared Rust core. This applies to plain TAR, gzip, zstd, and bzip2 on native and
|
|
427
|
+
fallback paths. The native and WASM builds enforce the same framing rules:
|
|
379
428
|
|
|
380
429
|
- Every nonzero header must have a valid unsigned octal checksum, delimited
|
|
381
430
|
within its field. Checksum validation precedes metadata allocation and member
|
|
@@ -420,14 +469,24 @@ returning selected bytes. Unrequested, filtered, and stripped members cannot
|
|
|
420
469
|
bypass validation. Decompression remains streaming; no complete decoded archive
|
|
421
470
|
is retained in memory or written to a decoded spool.
|
|
422
471
|
|
|
423
|
-
The WASM transport has
|
|
424
|
-
and a 256 MiB maximum linear memory per isolated parser
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
bounded before allocation; allocation
|
|
428
|
-
bounds queued chunks, and completion
|
|
429
|
-
|
|
430
|
-
|
|
472
|
+
The WASM transport has fixed 64 KiB input/output windows, one pending member
|
|
473
|
+
event, and a 256 MiB maximum linear memory per isolated session. The parser and
|
|
474
|
+
portable zstd/bzip2 decoder share that session and memory ceiling. JavaScript
|
|
475
|
+
gzip decoding also emits chunks of at most 64 KiB for both staged files and
|
|
476
|
+
buffered inputs. Metadata is bounded before allocation; codec allocation
|
|
477
|
+
failure rejects. Stream backpressure bounds queued chunks, and completion or
|
|
478
|
+
error releases the session's parser and decoder state after stream teardown.
|
|
479
|
+
The manifest retains the existing charged budget below; linear memory is an
|
|
480
|
+
additional execution resource bound, not a new public limit option.
|
|
481
|
+
|
|
482
|
+
Portable zstd/bzip2 decoding consumes every concatenated member through physical
|
|
483
|
+
EOF and verifies container integrity, including available checksums. Zstd
|
|
484
|
+
skippable frames are consumed without becoming TAR data. Truncated members and
|
|
485
|
+
trailing non-container bytes reject with `ArchiveFormatError` before filters,
|
|
486
|
+
publication, or selected bytes are returned. Decoded TAR EOF and byte-budget
|
|
487
|
+
checks still apply across member boundaries; a second TAR after EOF is not
|
|
488
|
+
silently ignored. The gzip-only compressed-padding policy above does not extend
|
|
489
|
+
to zstd/bzip2 containers.
|
|
431
490
|
|
|
432
491
|
The raw meter enforces `maxEntries` before consuming each logical member's body,
|
|
433
492
|
including members later skipped by filtering or stripping. PAX/GNU metadata
|
|
@@ -524,7 +583,7 @@ parsers from disagreeing about a member's type.
|
|
|
524
583
|
`K` validates encoding and NUL structure without authorizing link creation.
|
|
525
584
|
Normal link/filter policy still governs the described member. Canonical
|
|
526
585
|
pre-strip filter paths, decoded-stream ceilings, and physical EOF checks apply
|
|
527
|
-
to plain/gzip TAR and
|
|
586
|
+
to plain/gzip TAR and zstd/bzip2 alike.
|
|
528
587
|
|
|
529
588
|
## `inspectTarArchive`
|
|
530
589
|
|
|
@@ -590,7 +649,7 @@ import { resolveArchiveKind, type ArchiveKind } from "@openclaw/fs-safe/archive"
|
|
|
590
649
|
|
|
591
650
|
const kind = resolveArchiveKind("upload.zip"); // "zip"
|
|
592
651
|
const tar = resolveArchiveKind("upload.tar.gz"); // "tar"
|
|
593
|
-
const zstd = resolveArchiveKind("upload.tar.zst"); // "tar-zstd"
|
|
652
|
+
const zstd = resolveArchiveKind("upload.tar.zst"); // "tar-zstd" in auto/off; require checks native
|
|
594
653
|
const unknown = resolveArchiveKind("upload.bin"); // null
|
|
595
654
|
```
|
|
596
655
|
|
|
@@ -598,17 +657,18 @@ Recognizes:
|
|
|
598
657
|
|
|
599
658
|
- `*.zip` → `"zip"`
|
|
600
659
|
- `*.tar`, `*.tar.gz`, `*.tgz` → `"tar"`
|
|
601
|
-
- `*.tar.zst`, `*.tar.zstd`, `*.tzst` → `"tar-zstd"`
|
|
602
|
-
- `*.tar.bz2`, `*.tbz2`, `*.tbz` → `"tar-bzip2"`
|
|
660
|
+
- `*.tar.zst`, `*.tar.zstd`, `*.tzst` → `"tar-zstd"`
|
|
661
|
+
- `*.tar.bz2`, `*.tbz2`, `*.tbz` → `"tar-bzip2"`
|
|
603
662
|
|
|
604
663
|
Returns `null` for unknown extensions; check the result before calling
|
|
605
|
-
`extractArchive` if the filename is caller-controlled.
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
664
|
+
`extractArchive` if the filename is caller-controlled. Recognized zstd and bzip2
|
|
665
|
+
TAR extensions resolve in `auto` and `off` even without a native binding, using
|
|
666
|
+
the bundled codecs for subsequent extraction or reads. Explicit `require`
|
|
667
|
+
still checks native availability during suffix resolution and throws
|
|
668
|
+
`FsSafeError("helper-unavailable")` when the binding cannot load.
|
|
609
669
|
|
|
610
|
-
For a
|
|
611
|
-
the first archive call so a
|
|
670
|
+
For a deployment that requires native archive processing, configure native mode
|
|
671
|
+
before the first archive call so a missing binding fails at the boundary:
|
|
612
672
|
|
|
613
673
|
```ts
|
|
614
674
|
import { configureFsSafeNative } from "@openclaw/fs-safe/config";
|
|
@@ -627,8 +687,10 @@ await extractArchive({
|
|
|
627
687
|
|
|
628
688
|
`readArchiveEntry(archivePath, entryPath, { maxBytes, kind? })` reads one
|
|
629
689
|
regular-file entry into a bounded `Buffer` without extracting a tree. It reads
|
|
630
|
-
the input through an identity-checked descriptor, rejects
|
|
631
|
-
|
|
690
|
+
the input through an identity-checked descriptor, rejects a requested link or
|
|
691
|
+
directory, and rejects duplicate entry names anywhere in the archive. Unrequested
|
|
692
|
+
links and directories do not prevent reading a regular file; no links are followed
|
|
693
|
+
or created. It verifies ZIP CRC and declared size,
|
|
632
694
|
and throws `ArchiveLimitError` if the requested entry's output exceeds
|
|
633
695
|
`maxBytes`. ZIP output within that cap must match the declared uncompressed
|
|
634
696
|
size exactly; either a shorter or longer payload throws
|
|
@@ -641,7 +703,9 @@ plus the 768 MiB decoded ceiling derived from default extracted/archive byte
|
|
|
641
703
|
limits. It does not apply payload budgets to unrequested members. ZIP
|
|
642
704
|
inputs retain the archive subpath's 256 MiB compressed-input ceiling.
|
|
643
705
|
With a native binding it uses the same Rust decoders as extraction, including
|
|
644
|
-
zstd and bzip2 TAR. Without native
|
|
706
|
+
zstd and bzip2 TAR. Without native, the guarded fallback uses bundled WASM for
|
|
707
|
+
TAR admission and zstd/bzip2 decoding, Node gunzip for gzip, and optional JSZip
|
|
708
|
+
for ZIP. Native `require` still rejects an unavailable binding.
|
|
645
709
|
Archive member reads retain their private in-memory input without a disk
|
|
646
710
|
snapshot. JavaScript ZIP member reads reuse their completed physical admission
|
|
647
711
|
when loading the decoder, which still checks its decoded names and entry count.
|
|
@@ -652,12 +716,11 @@ allocation without another copy where external buffers are supported.
|
|
|
652
716
|
Native TAR retains the fully admitted member offsets alongside the same input
|
|
653
717
|
allocation. Plain TAR copies only the selected payload range after full archive
|
|
654
718
|
validation. Gzip, zstd, and bzip2 replay bounded decompression and still validate
|
|
655
|
-
all framing, trailers, and physical padding before returning. The
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
require copies.
|
|
719
|
+
all framing, trailers, and physical padding before returning. The fallback also
|
|
720
|
+
retains admitted member offsets. After full admission, plain TAR copies the
|
|
721
|
+
selected range directly from its private snapshot; gzip, zstd, and bzip2 replay
|
|
722
|
+
bounded decompression through the same parser. WASM transport and selected
|
|
723
|
+
output use owned copies, so reusable codec windows cannot escape to callers.
|
|
661
724
|
Returned buffers own their bytes, so changing a result cannot modify an archive
|
|
662
725
|
reader or retain an unrelated part of the input through its backing ArrayBuffer.
|
|
663
726
|
|
package/docs/atomic.md
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
|
|
17
17
|
Write `content` to a sibling temp file in the destination directory, apply the parent-directory and final file modes through verified descriptors, optionally `fsync` the file descriptor, optionally `fsync` the parent directory after rename, then atomically rename over the destination. No permission change follows a caller-supplied pathname.
|
|
18
18
|
|
|
19
|
-
On POSIX, the parent is opened with no-follow and directory-only flags, checked against its pre-open identity, and mode-adjusted through that descriptor. A replacement symlink is rejected rather than followed. If the directory cannot be opened for descriptor access, the operation fails closed instead of retrying by pathname. Windows does not enforce POSIX directory modes and Node cannot consistently open directory descriptors there, so `dirMode` is passed only to `mkdir`; no pathname `chmod` fallback is attempted.
|
|
19
|
+
On POSIX, the parent is opened with no-follow and directory-only flags, checked against its exact pre-open device/inode identity, and mode-adjusted through that descriptor. A replacement symlink is rejected rather than followed. If the directory cannot be opened for descriptor access, the operation fails closed instead of retrying by pathname. Windows does not enforce POSIX directory modes and Node cannot consistently open directory descriptors there, so `dirMode` is passed only to `mkdir`; no pathname `chmod` fallback is attempted.
|
|
20
20
|
|
|
21
21
|
Async replacements to the same destination are serialized inside the current process, so two overlapping `replaceFileAtomic()` calls do not interleave their temp-write/rename phases. Use a sidecar lock when multiple processes may write the same target.
|
|
22
22
|
|
|
@@ -92,10 +92,13 @@ 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
|
|
95
|
+
JavaScript permits `beforeRename` callbacks and filesystem adapters to throw any value, including
|
|
96
96
|
`undefined`, `null`, `false`, signed zero, `0n`, an empty string, and `NaN`.
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
Atomic replacement preserves such operational failures when cleanup and close
|
|
98
|
+
succeed, including rename and post-rename verification failures. Rename retry
|
|
99
|
+
and copy-fallback classification reads the error code once without coercion;
|
|
100
|
+
missing or unreadable codes preserve the original failure. A rejected call has
|
|
101
|
+
no success receipt even if an adapter committed its rename before throwing. With
|
|
99
102
|
`throwOnCleanupError: true`, an additional owned-temp cleanup failure keeps the
|
|
100
103
|
existing cleanup wrapper whose `cause` is the original thrown value. A later
|
|
101
104
|
descriptor-close failure is reported in an `AggregateError`, in operation/cleanup
|
|
@@ -150,6 +153,14 @@ must not have aliases. The policy reads `nlink` from a pinned destination
|
|
|
150
153
|
descriptor, not pathname metadata, before rename and rechecks it in the copy
|
|
151
154
|
fallback.
|
|
152
155
|
|
|
156
|
+
Source and pinned destination admission compare exact bigint device/inode
|
|
157
|
+
observations, so distinct identities that round to the same JavaScript number
|
|
158
|
+
cannot authorize a copy. Unknown Windows identities get one bounded reinspection
|
|
159
|
+
of the same descriptor or path; incomplete or inconsistent observations fail
|
|
160
|
+
closed without reopening. Injected filesystem adapters must honor the
|
|
161
|
+
`{ bigint: true }` stat option. Source admission reuses that exact pair instead
|
|
162
|
+
of immediately repeating it with numeric metadata.
|
|
163
|
+
|
|
153
164
|
The default `copyFallbackRestore: "none"` preserves the existing fallback
|
|
154
165
|
contract: a failed copy can leave a partial destination. For state files where
|
|
155
166
|
preserving the old bytes is more important, choose `"restore-original"` and set
|
|
@@ -349,7 +360,10 @@ the preflight cap fails with `FsSafeError("too-large")`.
|
|
|
349
360
|
If another writer changes source entries during the fallback, the staged copy
|
|
350
361
|
throws `ESTALE` before commit when possible. If the destination has already
|
|
351
362
|
been committed, cleanup still preserves the changed source entries and throws
|
|
352
|
-
`ESTALE`.
|
|
363
|
+
`ESTALE`. Copied file and symlink manifests retain exact bigint identities and
|
|
364
|
+
nanosecond timestamps, so rounded file IDs cannot authorize copying or removal
|
|
365
|
+
of a different entry. Hardlink groups also use exact identities. Directory
|
|
366
|
+
manifests retain an exact bigint device/inode receipt from
|
|
353
367
|
copy admission. Each directory is rechecked after traversal, and the source root
|
|
354
368
|
is checked again before publication. Cleanup checks the same receipt before
|
|
355
369
|
removing children, then invokes mutation authority and rechecks the receipt and
|
|
@@ -366,6 +380,11 @@ unlink is verified through a remaining manifested alias and its exact resulting
|
|
|
366
380
|
identity becomes the next cleanup receipt. This accounts for the operation's
|
|
367
381
|
own link-count and ctime changes without suppressing unexpected external
|
|
368
382
|
mutations.
|
|
383
|
+
On Windows, opening a regular source may advance its ctime while all other
|
|
384
|
+
fingerprint fields match. That exception applies only to opening; post-copy
|
|
385
|
+
verification and cleanup retain their full fingerprint checks.
|
|
386
|
+
Copied aliases share each verified open-time update. Changes observed between
|
|
387
|
+
copies still reject instead of being mistaken for an owned open transition.
|
|
369
388
|
|
|
370
389
|
### Mutation authority and publication receipts
|
|
371
390
|
|
|
@@ -404,6 +423,11 @@ thenable, or any other value fails with a `TypeError`; rejected asynchronous
|
|
|
404
423
|
results are consumed. Perform asynchronous policy checks before calling the
|
|
405
424
|
helper and use the authority callback to recheck the current owner at each
|
|
406
425
|
mutation boundary. All callbacks are captured before the first await.
|
|
426
|
+
Copied source leaves are checked again immediately after authority returns and
|
|
427
|
+
before unlink is submitted. Supplying any of the three callbacks also
|
|
428
|
+
retains the original source-parent route and renews copied-directory ancestry
|
|
429
|
+
before cleanup. Substituted entries are preserved; pathname checks and unlink
|
|
430
|
+
remain a best-effort sequence, not atomic.
|
|
407
431
|
|
|
408
432
|
`onDestinationPublished` runs exactly once after a successful rename resolves,
|
|
409
433
|
before awaited post-rename directory checks or source cleanup. It receives a
|
package/docs/config.md
CHANGED
|
@@ -37,10 +37,14 @@ Set the process-global loading policy. Configure once at startup, before the fir
|
|
|
37
37
|
|
|
38
38
|
| Mode | Behavior |
|
|
39
39
|
|---|---|
|
|
40
|
-
| `auto` | Default. Prefer the platform binding and use
|
|
41
|
-
| `off` | Do not load the binding; use
|
|
40
|
+
| `auto` | Default. Prefer the platform binding and use supported fallbacks when it is unavailable. |
|
|
41
|
+
| `off` | Do not load the binding; use supported fallbacks and reject native-only operations. |
|
|
42
42
|
| `require` | Operations that need the binding raise `FsSafeError("helper-unavailable")` when it cannot load. |
|
|
43
43
|
|
|
44
|
+
Fallbacks include guarded JavaScript, the bundled TAR/gzip WASM parser, and the
|
|
45
|
+
[packaged Windows security scripts](install.md#windows-security-fallback).
|
|
46
|
+
Windows command fallbacks remain subject to normal system execution policy.
|
|
47
|
+
|
|
44
48
|
## `getFsSafeNativeConfig()`
|
|
45
49
|
|
|
46
50
|
```ts
|
package/docs/contributing.md
CHANGED
|
@@ -19,10 +19,50 @@ manager version declared in `package.json`.
|
|
|
19
19
|
pnpm build
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
Runs TypeScript compilation and builds the portable Rust TAR parser
|
|
23
|
-
`wasm32-unknown-unknown`. Contributors need Rust (the
|
|
24
|
-
minimum or newer)
|
|
25
|
-
|
|
22
|
+
Runs TypeScript compilation and builds the portable Rust TAR parser and its
|
|
23
|
+
bzip2/zstd codecs for `wasm32-unknown-unknown`. Contributors need Rust (the
|
|
24
|
+
native crate's declared minimum or newer), `rustup target add
|
|
25
|
+
wasm32-unknown-unknown`, and LLVM's WebAssembly-capable `clang` and `llvm-ar`.
|
|
26
|
+
The system's native `ar` is not sufficient. `pnpm archive:wasm` rebuilds just
|
|
27
|
+
the portable module.
|
|
28
|
+
|
|
29
|
+
Linux, macOS, and Windows CI use the same pinned WASI SDK 34 LLVM toolchain;
|
|
30
|
+
Alpine uses its versioned LLVM 22 packages alongside `rust-wasm`. For local
|
|
31
|
+
builds, install LLVM through your package manager or use the official
|
|
32
|
+
[WASI SDK](https://github.com/WebAssembly/wasi-sdk/releases/tag/wasi-sdk-34).
|
|
33
|
+
On macOS, `brew install llvm` supplies the archiver missing from Apple's
|
|
34
|
+
Command Line Tools. On Windows, install the LLVM distribution with both
|
|
35
|
+
`clang.exe` and `llvm-ar.exe`. On Linux, install the matching `clang` and
|
|
36
|
+
`llvm` packages; a GCC-only build toolchain cannot compile these WASM codecs.
|
|
37
|
+
|
|
38
|
+
The build discovers tools on `PATH`, in `LLVM_PATH/bin`, in Homebrew's LLVM
|
|
39
|
+
prefixes, and in Windows' standard LLVM installation. It also checks the
|
|
40
|
+
versioned `clang-18` through `clang-21` and `llvm-ar-18` through `llvm-ar-21`
|
|
41
|
+
executables. To select another installation explicitly, set
|
|
42
|
+
`CC_wasm32_unknown_unknown` and `AR_wasm32_unknown_unknown` to its compiler
|
|
43
|
+
and archiver. The corresponding hyphenated target variables and cc-rs's
|
|
44
|
+
`TARGET_CC`/`TARGET_AR` or `CC`/`AR` overrides are also respected; an unusable
|
|
45
|
+
explicit override fails with a builder diagnostic instead of being ignored.
|
|
46
|
+
Windows build environment names are case-insensitive, including when worker
|
|
47
|
+
processes uppercase them. The build normalizes only its copied child environment.
|
|
48
|
+
Clang's implicit configuration is disabled for this target so the WASI SDK's
|
|
49
|
+
default libc/sysroot cannot leak into the import-free module. These settings
|
|
50
|
+
affect compilation only and do not become runtime dependencies.
|
|
51
|
+
|
|
52
|
+
The build disables release LTO only in the WASM Cargo subprocess. An observed
|
|
53
|
+
Rust 1.98.1 optimized-WASM-LTO allocation/free failure makes that necessary;
|
|
54
|
+
the native release profile stays unchanged. The WASM linker strips debug
|
|
55
|
+
sections to keep the bundled module small without stripping native binaries.
|
|
56
|
+
The build verifies zero host
|
|
57
|
+
imports and one unshared 32-bit memory with the existing 256 MiB maximum
|
|
58
|
+
before copying the artifact. Allocator regression tests build a separate
|
|
59
|
+
instrumented module with `pnpm archive:wasm:allocator-tests` under the Cargo
|
|
60
|
+
target directory. That module is never copied to `dist/` or packaged. `pnpm
|
|
61
|
+
check` and coverage collection build it explicitly before testing. After a
|
|
62
|
+
fresh checkout, run that command before `pnpm test`, `pnpm test:coverage`, or
|
|
63
|
+
focused `test/archive-codec-wasm-allocator.test.ts` runs; the tests fail if
|
|
64
|
+
their prerequisite artifact is missing.
|
|
65
|
+
|
|
26
66
|
The import-free asset lands at `dist/archive-parser.wasm`; source tests and
|
|
27
67
|
compiled consumers both resolve that generated artifact. Run `pnpm build`
|
|
28
68
|
before source tests in a fresh checkout. Do not commit `dist/` or built WASM.
|
|
@@ -117,6 +157,10 @@ pnpm archive:producer-smoke ./consumer require
|
|
|
117
157
|
This uses a child bound to canonical cwd/device/inode running `/usr/bin/tar -czf - .`
|
|
118
158
|
with unchanged stdout, and npm tar, on synthetic Unicode/newline/long-name files,
|
|
119
159
|
then the installed package API for exact payload hashes and bounded reads.
|
|
160
|
+
The consumer must be separate from the source checkout; package resolution must
|
|
161
|
+
stay within its own `node_modules`, including pnpm's local `.pnpm` layout.
|
|
162
|
+
Workspace self-resolution, upward resolution, and external package links reject
|
|
163
|
+
before package imports or archive fixture creation.
|
|
120
164
|
It also rejects a valid PAX override attached to an invalid raw UTF-8 field.
|
|
121
165
|
The `require` command must resolve the freshly packed native binding; the
|
|
122
166
|
`off` command uses the installed WASM asset. No live user files are read.
|
package/docs/copy.md
CHANGED
|
@@ -78,6 +78,8 @@ XFS and ZFS preserve regular-file and directory modes, timestamps, extended attr
|
|
|
78
78
|
|
|
79
79
|
`readCloneFileMetadata(files)` asynchronously reads APFS data-stream identities and file metadata in one native batch. Results correspond to input order; missing or unsupported entries return `undefined`. The returned `CloneFileMetadata` includes clone ID, device/inode, size, mode, ownership, and timestamps. These are point-in-time observations, not authorization or proof that later reads remain unchanged. Consumers such as Git index adapters must validate their own content and timestamp invariants. The reader does not follow leaf symbolic links.
|
|
80
80
|
|
|
81
|
+
All input paths must be absolute and valid before native availability is checked. On platforms other than macOS, `auto` and `off` can return one `undefined` per path without the addon, matching native's unsupported result. On macOS, `off` or an unavailable addon still rejects with `helper-unavailable`; JavaScript cannot supply APFS clone IDs. Explicit `require` mode rejects an unavailable addon on every platform, including for an empty batch. Errors from a loaded native helper remain terminal.
|
|
82
|
+
|
|
81
83
|
## Borrowed FileHandle transfers
|
|
82
84
|
|
|
83
85
|
`copyFileHandle` from `@openclaw/fs-safe/advanced` copies bytes between two
|