@openclaw/fs-safe 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +79 -0
- package/README.md +7 -0
- package/dist/archive-deadline.d.ts +3 -0
- package/dist/archive-deadline.d.ts.map +1 -1
- package/dist/archive-deadline.js +44 -8
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +1 -0
- package/dist/archive-errors.d.ts +1 -0
- package/dist/archive-errors.d.ts.map +1 -1
- package/dist/archive-errors.js +3 -0
- package/dist/archive-input.d.ts.map +1 -1
- package/dist/archive-input.js +26 -16
- package/dist/archive-limits.d.ts +11 -3
- package/dist/archive-limits.d.ts.map +1 -1
- package/dist/archive-limits.js +24 -0
- package/dist/archive-native.d.ts +3 -2
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +24 -9
- package/dist/archive-policy.d.ts +2 -0
- package/dist/archive-policy.d.ts.map +1 -1
- package/dist/archive-policy.js +9 -1
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +69 -41
- package/dist/archive-staging.d.ts +3 -0
- package/dist/archive-staging.d.ts.map +1 -1
- package/dist/archive-staging.js +91 -43
- package/dist/archive-tar-admission.d.ts +7 -0
- package/dist/archive-tar-admission.d.ts.map +1 -0
- package/dist/archive-tar-admission.js +43 -0
- package/dist/archive-tar-gnu.d.ts +2 -0
- package/dist/archive-tar-gnu.d.ts.map +1 -0
- package/dist/archive-tar-gnu.js +20 -0
- package/dist/archive-tar-header.d.ts +8 -0
- package/dist/archive-tar-header.d.ts.map +1 -0
- package/dist/archive-tar-header.js +47 -0
- package/dist/archive-tar-meta.d.ts +29 -1
- package/dist/archive-tar-meta.d.ts.map +1 -1
- package/dist/archive-tar-meta.js +104 -35
- package/dist/archive-tar-runtime.d.ts +3 -0
- package/dist/archive-tar-runtime.d.ts.map +1 -1
- package/dist/archive-tar-runtime.js +3 -0
- package/dist/archive-tar.d.ts.map +1 -1
- package/dist/archive-tar.js +8 -2
- package/dist/archive-zip-names.js +2 -2
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +38 -27
- package/dist/atomic.d.ts +1 -1
- package/dist/atomic.d.ts.map +1 -1
- package/dist/bounded-read-stream.d.ts.map +1 -1
- package/dist/bounded-read-stream.js +2 -4
- package/dist/bounded-read.d.ts.map +1 -1
- package/dist/bounded-read.js +3 -10
- package/dist/byte-budget.d.ts +5 -0
- package/dist/byte-budget.d.ts.map +1 -0
- package/dist/byte-budget.js +9 -0
- package/dist/directory-guard.d.ts +2 -1
- package/dist/directory-guard.d.ts.map +1 -1
- package/dist/directory-guard.js +10 -0
- package/dist/file-lock-sync.d.ts.map +1 -1
- package/dist/file-lock-sync.js +81 -30
- package/dist/file-observation.d.ts +9 -0
- package/dist/file-observation.d.ts.map +1 -0
- package/dist/file-observation.js +22 -0
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +9 -5
- package/dist/file-store-limit.d.ts +2 -0
- package/dist/file-store-limit.d.ts.map +1 -0
- package/dist/file-store-limit.js +8 -0
- package/dist/file-store-sync-write.d.ts.map +1 -1
- package/dist/file-store-sync-write.js +38 -8
- package/dist/file-store.d.ts.map +1 -1
- package/dist/file-store.js +26 -26
- package/dist/json-durable-queue-directory.d.ts +2 -0
- package/dist/json-durable-queue-directory.d.ts.map +1 -0
- package/dist/json-durable-queue-directory.js +20 -0
- package/dist/json-durable-queue-ownership.d.ts +14 -0
- package/dist/json-durable-queue-ownership.d.ts.map +1 -0
- package/dist/json-durable-queue-ownership.js +168 -0
- package/dist/json-durable-queue-retirement.d.ts +9 -0
- package/dist/json-durable-queue-retirement.d.ts.map +1 -0
- package/dist/json-durable-queue-retirement.js +126 -0
- package/dist/json-durable-queue-transfer-lock.d.ts +2 -0
- package/dist/json-durable-queue-transfer-lock.d.ts.map +1 -0
- package/dist/json-durable-queue-transfer-lock.js +19 -0
- package/dist/json-durable-queue.d.ts +1 -0
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +90 -57
- package/dist/json.d.ts.map +1 -1
- package/dist/json.js +32 -13
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +4 -2
- package/dist/native-binding.d.ts +12 -3
- package/dist/native-binding.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +1 -1
- package/dist/opened-file-failure.d.ts +7 -0
- package/dist/opened-file-failure.d.ts.map +1 -0
- package/dist/opened-file-failure.js +41 -0
- package/dist/opened-realpath.d.ts.map +1 -1
- package/dist/opened-realpath.js +8 -2
- package/dist/output-sibling.d.ts.map +1 -1
- package/dist/output-sibling.js +11 -110
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +4 -2
- package/dist/pinned-open.d.ts.map +1 -1
- package/dist/pinned-open.js +27 -48
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +10 -10
- package/dist/private-temp-workspace.d.ts +3 -1
- package/dist/private-temp-workspace.d.ts.map +1 -1
- package/dist/private-temp-workspace.js +81 -56
- package/dist/publish-file.d.ts.map +1 -1
- package/dist/publish-file.js +2 -4
- package/dist/read-opened-file.d.ts.map +1 -1
- package/dist/read-opened-file.js +6 -4
- package/dist/regular-file.d.ts.map +1 -1
- package/dist/regular-file.js +136 -82
- package/dist/replace-file-copy-fallback.d.ts +3 -1
- package/dist/replace-file-copy-fallback.d.ts.map +1 -1
- package/dist/replace-file-copy-fallback.js +21 -36
- package/dist/replace-file-copy-source.d.ts +21 -0
- package/dist/replace-file-copy-source.d.ts.map +1 -0
- package/dist/replace-file-copy-source.js +112 -0
- package/dist/replace-file-descriptor.d.ts +13 -3
- package/dist/replace-file-descriptor.d.ts.map +1 -1
- package/dist/replace-file-descriptor.js +32 -7
- package/dist/replace-file-rename-policy.d.ts +7 -0
- package/dist/replace-file-rename-policy.d.ts.map +1 -0
- package/dist/replace-file-rename-policy.js +30 -0
- package/dist/replace-file-temp-owner.d.ts +46 -0
- package/dist/replace-file-temp-owner.d.ts.map +1 -0
- package/dist/replace-file-temp-owner.js +346 -0
- package/dist/replace-file.d.ts +6 -1
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +72 -58
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +113 -82
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +6 -3
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +6 -5
- package/dist/secure-file.d.ts.map +1 -1
- package/dist/secure-file.js +17 -13
- package/dist/sibling-staged-file.d.ts +15 -0
- package/dist/sibling-staged-file.d.ts.map +1 -0
- package/dist/sibling-staged-file.js +148 -0
- package/dist/sibling-temp.d.ts +3 -0
- package/dist/sibling-temp.d.ts.map +1 -1
- package/dist/sibling-temp.js +30 -74
- package/dist/sidecar-lock-acquire.d.ts.map +1 -1
- package/dist/sidecar-lock-acquire.js +91 -37
- package/dist/sidecar-lock-handle.d.ts +6 -2
- package/dist/sidecar-lock-handle.d.ts.map +1 -1
- package/dist/sidecar-lock-handle.js +17 -3
- package/dist/sidecar-lock-policy.d.ts +2 -0
- package/dist/sidecar-lock-policy.d.ts.map +1 -1
- package/dist/sidecar-lock-policy.js +29 -0
- package/dist/sidecar-lock-reclaim.d.ts +16 -5
- package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
- package/dist/sidecar-lock-reclaim.js +60 -28
- package/dist/sidecar-lock-root.d.ts +3 -0
- package/dist/sidecar-lock-root.d.ts.map +1 -0
- package/dist/sidecar-lock-root.js +76 -0
- package/dist/sidecar-lock.d.ts.map +1 -1
- package/dist/sidecar-lock.js +28 -16
- package/dist/strict-file-identity.d.ts.map +1 -1
- package/dist/strict-file-identity.js +4 -1
- package/dist/suppressed-error.d.ts +6 -0
- package/dist/suppressed-error.d.ts.map +1 -0
- package/dist/suppressed-error.js +15 -0
- package/dist/temp-cleanup.d.ts +2 -0
- package/dist/temp-cleanup.d.ts.map +1 -1
- package/dist/temp-cleanup.js +25 -10
- package/dist/temp-workspace-owner.d.ts +23 -0
- package/dist/temp-workspace-owner.d.ts.map +1 -0
- package/dist/temp-workspace-owner.js +320 -0
- package/dist/temp.d.ts +1 -1
- package/dist/temp.d.ts.map +1 -1
- package/dist/test-hooks.d.ts +5 -0
- package/dist/test-hooks.d.ts.map +1 -1
- package/docs/advanced.md +8 -1
- package/docs/archive.md +202 -26
- package/docs/atomic.md +11 -2
- package/docs/durability.md +1 -1
- package/docs/file-store.md +2 -0
- package/docs/install.md +8 -4
- package/docs/json.md +11 -4
- package/docs/native-helper.md +15 -3
- package/docs/native.md +33 -6
- package/docs/output.md +16 -3
- package/docs/public-api.md +6 -3
- package/docs/quickstart.md +1 -1
- package/docs/reading.md +1 -1
- package/docs/regular-file.md +9 -2
- package/docs/root.md +2 -0
- package/docs/secret-file.md +5 -1
- package/docs/secure-file.md +4 -2
- package/docs/security-model.md +18 -0
- package/docs/sidecar-lock.md +60 -4
- package/docs/store.md +13 -0
- package/docs/temp.md +128 -14
- package/docs/writing.md +6 -0
- package/package.json +10 -10
package/docs/archive.md
CHANGED
|
@@ -23,7 +23,7 @@ await extractArchive({
|
|
|
23
23
|
archivePath: "/srv/uploads/plugin.zip",
|
|
24
24
|
destDir: "/srv/workspace/plugins/plugin",
|
|
25
25
|
kind: "zip", // optional; resolveArchiveKind() can infer
|
|
26
|
-
timeoutMs: 15_000, // hard
|
|
26
|
+
timeoutMs: 15_000, // hard budget; active destination mutation is joined
|
|
27
27
|
stripComponents: 0, // tar-style strip-leading-dirs
|
|
28
28
|
entryModes: "clamp", // default; use "preserve" for archive rwx bits
|
|
29
29
|
entryFilter: ({ path, kind, size }) => "extract",
|
|
@@ -45,7 +45,7 @@ await extractArchive({
|
|
|
45
45
|
type ExtractArchiveOptions = {
|
|
46
46
|
archivePath: string; // absolute path to the archive
|
|
47
47
|
destDir: string; // absolute destination directory; must already exist
|
|
48
|
-
timeoutMs: number; // positive wall-clock
|
|
48
|
+
timeoutMs: number; // positive wall-clock budget; <= 0/non-finite disables it
|
|
49
49
|
kind?: ArchiveKind; // "zip" | "tar" | "tar-zstd" | "tar-bzip2"
|
|
50
50
|
stripComponents?: number; // strip N leading dirs from entry paths
|
|
51
51
|
tarGzip?: boolean; // when archive is .tar.gz/.tgz
|
|
@@ -68,8 +68,8 @@ private staging tree; ZIP applies the same policy to `unixPermissions`.
|
|
|
68
68
|
Native extraction is deliberately split into two phases. Rust first reports an
|
|
69
69
|
entry manifest without creating paths. TypeScript validates paths, applies
|
|
70
70
|
`stripComponents`, filters, limits, and mode policy, then passes an explicit
|
|
71
|
-
accepted-entry plan back to Rust. Rust
|
|
72
|
-
fd-relative `mkdirBeneath`/exclusive-open writes. This keeps policy identical
|
|
71
|
+
accepted-entry plan back to Rust. Rust owns raw-stream admission, decompression,
|
|
72
|
+
and fd-relative `mkdirBeneath`/exclusive-open writes. This keeps filter policy identical
|
|
73
73
|
between native and JavaScript paths rather than reimplementing it in Rust.
|
|
74
74
|
|
|
75
75
|
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 separator and dot-component equivalence is allowed only after validation. Ordinary legacy filename decoding remains backend-selected.
|
|
@@ -82,11 +82,29 @@ toward `maxEntries` and undergo traversal validation. JavaScript TAR extraction
|
|
|
82
82
|
passes node-tar this accepted output path with its own stripping disabled, so
|
|
83
83
|
depth checks, collision checks, writes, and mode application agree.
|
|
84
84
|
|
|
85
|
-
An `entryFilter` sees the validated effective archive path
|
|
86
|
-
|
|
85
|
+
An `entryFilter` sees the validated **canonical effective archive path before
|
|
86
|
+
stripping**, entry kind, and declared size. On every JavaScript and native
|
|
87
|
+
ZIP/TAR backend (including gzip and native zstd/bzip2), backslashes become `/`,
|
|
88
|
+
empty and `.` components are removed, and trailing separators are removed from
|
|
89
|
+
directory paths. For example, `./pkg//state\cache/value` is presented as
|
|
90
|
+
`pkg/state/cache/value`, even with `stripComponents: 1`. Case and Unicode
|
|
91
|
+
spelling are preserved. Local PAX `path`, GNU long-name, and supported ZIP
|
|
92
|
+
Unicode Path names use the same canonicalization.
|
|
93
|
+
|
|
94
|
+
Raw paths undergo traversal, absolute/drive-path, and NUL validation **before**
|
|
95
|
+
canonicalization; normalization cannot turn an unsafe path into an accepted
|
|
96
|
+
one. Stripping and output collision checks use this same canonical identity.
|
|
97
|
+
Filters that compare exact strings should use canonical pre-strip paths,
|
|
98
|
+
including directory names without a trailing `/`.
|
|
87
99
|
Returning `"skip"` rejects the whole archive unless `onFiltered` is
|
|
88
|
-
explicitly `"skip-entry"`.
|
|
89
|
-
|
|
100
|
+
explicitly `"skip-entry"`. Runtime values other than `"reject-archive"` and
|
|
101
|
+
`"skip-entry"` reject before extraction starts instead of falling through to
|
|
102
|
+
skip behavior. Path traversal and archive-wide entry-count checks still apply
|
|
103
|
+
to skipped entries.
|
|
104
|
+
`maxEntryBytes` and `maxExtractedBytes` charge only entries accepted after
|
|
105
|
+
stripping and filtering. Skipping a large member does not consume these payload
|
|
106
|
+
budgets. The separate complete-stream decoded limit still applies to all TAR
|
|
107
|
+
content, including skipped or fully stripped members.
|
|
90
108
|
|
|
91
109
|
For example, a fleet restore can omit regenerated cache entries while rejecting
|
|
92
110
|
any other policy mismatch by default:
|
|
@@ -111,14 +129,33 @@ If skipping was not explicitly part of the restore contract, omit
|
|
|
111
129
|
`onFiltered`; the first `"skip"` then rejects the complete archive with
|
|
112
130
|
`ArchiveSecurityError("entry-filtered")`.
|
|
113
131
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
132
|
+
Both TAR implementations finish bounded admission before TypeScript policy
|
|
133
|
+
evaluation, so a rejected plan never starts extraction. The JavaScript path
|
|
134
|
+
owns the extraction file stream and aborts node-tar through a pipeline on
|
|
135
|
+
parser disagreement, validation, or timeout failure, destroying both ends
|
|
136
|
+
instead of leaving a paused parser to drain indefinitely.
|
|
137
|
+
|
|
138
|
+
TAR character devices, block devices, and FIFOs are presented to the filter as
|
|
139
|
+
`kind: "other"`. Accepted entries of these types reject with
|
|
140
|
+
`ArchiveSecurityError("entry-link")`; an explicit `"skip-entry"` filter can omit
|
|
141
|
+
them. GNU typeflag `D` (`GNUDumpDir`) is a directory on both backends, including
|
|
142
|
+
its filter kind, canonical path, and directory creation policy. Its declared
|
|
143
|
+
body size follows the existing TAR strip/filter payload budgets; dump contents
|
|
144
|
+
are not restored as files.
|
|
145
|
+
|
|
146
|
+
Unsupported logical TAR records, including volume headers (`V`), Solaris ACL
|
|
147
|
+
records (`A`), inodes (`I`), continuations (`M`), and unrecognized typeflags,
|
|
148
|
+
still undergo entry counting, raw/effective path validation, stripping, depth
|
|
149
|
+
and output collision checks in physical order. Each remaining record reaches
|
|
150
|
+
`entryFilter` once with its canonical pre-strip path, `kind: "other"`, and
|
|
151
|
+
declared effective size. A filter skip rejects with `"entry-filtered"` unless
|
|
152
|
+
`onFiltered: "skip-entry"` is explicit. Accepted unsupported records are safely
|
|
153
|
+
omitted and do not consume output payload budgets. This applies even when the
|
|
154
|
+
underlying TAR parser suppresses the record. GNU long names describe one such
|
|
155
|
+
record and are then cleared; local PAX on unsupported types and GNU sparse
|
|
156
|
+
`S` records retain their existing fail-closed format policy.
|
|
157
|
+
|
|
158
|
+
If `kind` is omitted, the helper calls `resolveArchiveKind(archivePath)` and throws if the extension is not recognized. Pass `kind` explicitly when the archive name doesn't carry the type (e.g. content-addressed names). Archive inputs must remain regular files from preview through descriptor admission; POSIX opens are no-follow and nonblocking, so a FIFO swap cannot stall before deadline checks resume. A positive finite `timeoutMs` is a wall-clock budget; zero, negative, `NaN`, and infinity disable the deadline. Non-mutating work rejects promptly when the budget expires. If a live destination mutation is already in flight, rejection waits only for that mutation and any rollback to finish; no later destination mutation can begin.
|
|
122
159
|
|
|
123
160
|
### Limits
|
|
124
161
|
|
|
@@ -126,8 +163,8 @@ If `kind` is omitted, the helper calls `resolveArchiveKind(archivePath)` and thr
|
|
|
126
163
|
type ArchiveExtractLimits = {
|
|
127
164
|
maxArchiveBytes?: number; // refuse if archivePath stat'd size exceeds this
|
|
128
165
|
maxEntries?: number; // refuse before extracting if entry count > this
|
|
129
|
-
maxExtractedBytes?: number; //
|
|
130
|
-
maxEntryBytes?: number; //
|
|
166
|
+
maxExtractedBytes?: number; // cap total payload bytes accepted after strip/filter
|
|
167
|
+
maxEntryBytes?: number; // cap one accepted entry after strip/filter
|
|
131
168
|
maxMetaEntryBytes?: number; // refuse one PAX/GNU metadata body above this
|
|
132
169
|
maxEntryPathComponents?: number; // bound output path depth after stripComponents
|
|
133
170
|
};
|
|
@@ -142,13 +179,24 @@ A limit violation throws `ArchiveLimitError`. Its constant and string code are:
|
|
|
142
179
|
| `ARCHIVE_SIZE_EXCEEDS_LIMIT` | `archive-size-exceeds-limit` |
|
|
143
180
|
| `ENTRY_COUNT_EXCEEDS_LIMIT` | `archive-entry-count-exceeds-limit` |
|
|
144
181
|
| `EXTRACTED_SIZE_EXCEEDS_LIMIT` | `archive-extracted-size-exceeds-limit` |
|
|
182
|
+
| `DECODED_SIZE_EXCEEDS_LIMIT` | `archive-decoded-size-exceeds-limit` |
|
|
145
183
|
| `ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT` | `archive-entry-extracted-size-exceeds-limit` |
|
|
146
184
|
| `META_ENTRY_SIZE_EXCEEDS_LIMIT` | `archive-meta-entry-size-exceeds-limit` |
|
|
147
185
|
| `ENTRY_PATH_COMPONENTS_EXCEEDS_LIMIT` | `archive-entry-path-components-exceeds-limit` |
|
|
148
186
|
| `MANIFEST_SIZE_EXCEEDS_LIMIT` | `archive-manifest-size-exceeds-limit` |
|
|
149
187
|
|
|
150
|
-
`MANIFEST_SIZE_EXCEEDS_LIMIT` is
|
|
151
|
-
|
|
188
|
+
`MANIFEST_SIZE_EXCEEDS_LIMIT` is an active internal TAR admission limit, shared
|
|
189
|
+
by JavaScript and native extraction and bounded reads. Each logical member,
|
|
190
|
+
including ignored, filtered, and fully stripped members, charges
|
|
191
|
+
`64 + 2 * UTF-8 byte length of its effective pre-strip path` before emission or
|
|
192
|
+
retention. PAX/GNU metadata headers do not themselves charge a member cost.
|
|
193
|
+
The allowance is independent of `maxArchiveBytes`: derive a per-member path
|
|
194
|
+
allowance of `max(256, min(maxMetaEntryBytes, max(1, maxEntryPathComponents) * 256))`,
|
|
195
|
+
apply the same 64-byte overhead and doubled path cost, multiply by `maxEntries`,
|
|
196
|
+
and cap the total at 64 MiB using saturating arithmetic. Zero and very large
|
|
197
|
+
public limits remain deterministic. There is no public `maxManifestBytes`
|
|
198
|
+
option; this charged manifest budget supplements the decoded and metadata
|
|
199
|
+
limits rather than bounding the complete process heap.
|
|
152
200
|
|
|
153
201
|
Catch and branch on the code to surface a meaningful response to the caller.
|
|
154
202
|
|
|
@@ -165,9 +213,91 @@ codes remain `"destination-not-directory"`, `"destination-symlink"`, and
|
|
|
165
213
|
- **TOCTOU during merge:** extraction first writes to a private temp dir, then merges into `destDir` using the same boundary checks as `root().write()`. Destination symlink swaps are checked with the selected platform mechanism; non-Linux routes retain the best-effort race window documented in the [security model](security-model.md#containment-guarantees-by-platform).
|
|
166
214
|
- **Zip bombs:** `maxExtractedBytes` and `maxEntryBytes` apply to *post-decompression* bytes, so highly-compressed payloads hit the cap before they exhaust disk.
|
|
167
215
|
- **Corrupt ZIP payloads:** streamed output must match both the central-directory CRC and declared uncompressed size before it can leave private staging.
|
|
168
|
-
- **
|
|
216
|
+
- **Corrupt gzip streams:** truncated compressed bodies, missing trailers, and checksum failures reject before extraction publishes files or an entry read returns bytes, on both JavaScript and native backends.
|
|
217
|
+
- **Slow-loris archives:** `timeoutMs` is a hard wall-clock budget for non-mutating work. Extraction is aborted on overrun; if a destination mutation is already in flight, that mutation and rollback are joined before rejection so archive-controlled publication cannot continue afterward.
|
|
169
218
|
- **Metadata bombs:** a streaming pass-through reader rejects oversized PAX, GNU long-name, and GNU long-link bodies before either TAR implementation buffers them. It understands octal and base-256 fixed sizes and validates bounded local PAX bodies before using their size overrides for member framing. Original archive bytes remain unchanged.
|
|
170
219
|
|
|
220
|
+
### Raw TAR framing
|
|
221
|
+
|
|
222
|
+
Extraction and bounded reads admit the complete decoded TAR stream through the
|
|
223
|
+
raw meter before either backend's TAR parser runs. This applies to plain TAR,
|
|
224
|
+
gzip, and native-supported zstd/bzip2, without changing native-mode availability
|
|
225
|
+
or fallback policy. The existing TypeScript and Rust meters enforce the same
|
|
226
|
+
framing rules before parser normalization:
|
|
227
|
+
|
|
228
|
+
- Every nonzero header must have a valid unsigned octal checksum, delimited
|
|
229
|
+
within its field. Checksum validation precedes metadata allocation and member
|
|
230
|
+
policy. Fixed name, prefix, and linkname fields require strict UTF-8 and NUL
|
|
231
|
+
padding. Raw hardlink (`1`) and symlink (`2`) headers require a nonempty
|
|
232
|
+
linkname; every other type, including PAX/GNU metadata, requires an empty
|
|
233
|
+
linkname. This check precedes metadata handling and member/filter policy.
|
|
234
|
+
- Directory (`5`), hardlink (`1`), and symlink (`2`) raw headers must declare
|
|
235
|
+
zero body bytes, whether or not local PAX metadata is present. Valid zero-size
|
|
236
|
+
links remain subject to the existing link/filter policy.
|
|
237
|
+
- EOF requires two consecutive, complete 512-byte zero blocks at a header
|
|
238
|
+
boundary. A header after just one zero block, a missing/partial EOF marker,
|
|
239
|
+
and any nonzero bytes after EOF reject. Additional zero padding after EOF may
|
|
240
|
+
have any byte length within the decoded ceiling; zero blocks inside a declared
|
|
241
|
+
member body are payload.
|
|
242
|
+
- Headers and padded bodies must be complete. Size fields accept unsigned
|
|
243
|
+
octal with ASCII-space/NUL padding or supported positive base-256 encoding;
|
|
244
|
+
malformed numbers and non-padding bytes after a NUL reject. Raw sizes and
|
|
245
|
+
padded sizes must fit `Number.MAX_SAFE_INTEGER`, even with PAX overrides,
|
|
246
|
+
before member budgets are considered.
|
|
247
|
+
|
|
248
|
+
Framing failures use `ArchiveFormatError("archive-header-invalid")`, except
|
|
249
|
+
invalid UTF-8 or nonzero bytes after the first NUL in fixed name, linkname,
|
|
250
|
+
and USTAR prefix fields, which use `ArchiveSecurityError("entry-path")`.
|
|
251
|
+
Missing linknames on links and nonempty linknames on non-links still use the
|
|
252
|
+
format error. PAX `x` and GNU long-name/long-link `L`/`K` payloads retain their
|
|
253
|
+
existing support and metadata limits; the zero-body rule is not applied to all
|
|
254
|
+
non-regular types.
|
|
255
|
+
PAX effective sizes still determine regular-member framing. Admission preserves
|
|
256
|
+
the input bytes, and all entry/path/byte limits and extraction deadlines remain
|
|
257
|
+
in force. Native inspection now completes this admission pass before parsing,
|
|
258
|
+
requiring one additional streaming read/decompression pass.
|
|
259
|
+
JavaScript admission reports an ordered logical-member manifest from the raw
|
|
260
|
+
meter, bounded by entry-count, manifest, and decoded limits. Policy runs once
|
|
261
|
+
over that manifest; extraction checks parser-visible members against the
|
|
262
|
+
accepted decisions before writing. Original member names and USTAR prefixes
|
|
263
|
+
are validated even when overridden, and non-padding bytes after a fixed path
|
|
264
|
+
field's NUL terminator reject rather than hiding an unsafe suffix.
|
|
265
|
+
Both meters enforce the 255-byte component ceiling under NFC and NFD before
|
|
266
|
+
metadata replaces a raw path, including Hangul decomposition expansion.
|
|
267
|
+
Native extraction and entry reads also drain their metered readers through
|
|
268
|
+
physical EOF after parser traversal, before completing directory modes,
|
|
269
|
+
publishing staged files, or returning the requested bytes. Finding the requested
|
|
270
|
+
member or reaching the parser's logical EOF cannot bypass trailing validation.
|
|
271
|
+
|
|
272
|
+
The raw meter enforces `maxEntries` before consuming each logical member's body,
|
|
273
|
+
including members later skipped by filtering or stripping. PAX/GNU metadata
|
|
274
|
+
headers do not count as members; their payloads use `maxMetaEntryBytes`.
|
|
275
|
+
The meter does not receive `maxEntryBytes` or `maxExtractedBytes`: those payload
|
|
276
|
+
budgets apply only after strip/filter acceptance, using declared effective
|
|
277
|
+
sizes and excluding block padding. JavaScript's entry checker and the native
|
|
278
|
+
accepted-plan builder retain this shared policy. Every TAR admission/parser
|
|
279
|
+
pass has a separate absolute decoded ceiling:
|
|
280
|
+
`maxExtractedBytes + maxArchiveBytes`, safely clamped to
|
|
281
|
+
`Number.MAX_SAFE_INTEGER` (768 MiB with defaults). It counts every admitted
|
|
282
|
+
decoded byte: headers, bodies, metadata, all block padding, both EOF blocks,
|
|
283
|
+
and zero padding after EOF. It bounds complete decoding before parser policy,
|
|
284
|
+
including all filtered/stripped content; cumulative metadata and zero tails
|
|
285
|
+
cannot bypass this bound. Exceeding this ceiling throws
|
|
286
|
+
`ArchiveLimitError("archive-decoded-size-exceeds-limit")`.
|
|
287
|
+
|
|
288
|
+
The same TypeScript helper derives the ceiling for JavaScript and every native
|
|
289
|
+
TAR pass. Before selecting a backend, it caps internal metadata/decoded limits at
|
|
290
|
+
`Number.MAX_SAFE_INTEGER` and logical entry counts at `2^32 - 1`. Larger finite
|
|
291
|
+
options such as `Number.MAX_VALUE` remain valid; high-level payload budgets keep
|
|
292
|
+
their large values. The decoded ceiling uses clamped `maxExtractedBytes` and
|
|
293
|
+
archive overhead with safe addition. Ordinary limits, including
|
|
294
|
+
zero and the existing defaulting/rounding rules, retain their behavior.
|
|
295
|
+
There is no new public option. This is an absolute decoded admission
|
|
296
|
+
cap, not a decompression-ratio policy; bounded stream/codec read-ahead remains.
|
|
297
|
+
After this complete preflight, the JavaScript backend disables node-tar's
|
|
298
|
+
independent ratio threshold so it cannot reject data that the native backend
|
|
299
|
+
accepts within the same absolute limits.
|
|
300
|
+
|
|
171
301
|
### Bounded local PAX support
|
|
172
302
|
|
|
173
303
|
Extraction and single-entry reads accept one nonempty local POSIX `x` header
|
|
@@ -175,8 +305,8 @@ Extraction and single-entry reads accept one nonempty local POSIX `x` header
|
|
|
175
305
|
directory, symlink, or hardlink. `path`, `linkpath`, and `size` override that
|
|
176
306
|
member only. Effective paths still pass traversal validation before stripping,
|
|
177
307
|
then the output paths pass depth and collision checks. The filter receives the
|
|
178
|
-
effective pre-strip path, followed by link policy checks. PAX never
|
|
179
|
-
creation. Effective sizes drive framing, filters, and the existing output-byte
|
|
308
|
+
canonical effective pre-strip path, followed by link policy checks. PAX never
|
|
309
|
+
permits link creation. Effective sizes drive framing, filters, and the existing output-byte
|
|
180
310
|
budgets; `maxEntries` still counts members, not their metadata headers.
|
|
181
311
|
|
|
182
312
|
Records must have exact byte lengths, ASCII keys, a final newline, and no
|
|
@@ -206,13 +336,38 @@ Global `g`, old `X`, old GNU `N`, empty/dangling/repeated local headers, mixed
|
|
|
206
336
|
PAX/GNU extension chains, unknown keys, charset declarations, ACL extensions,
|
|
207
337
|
and all sparse extensions (including `GNU.sparse.*`, `SCHILY.filetype`,
|
|
208
338
|
`SCHILY.realsize`, and `SCHILY.size`) fail closed with
|
|
209
|
-
`ArchiveFormatError("archive-header-invalid")`.
|
|
210
|
-
and long-link `K` support is unchanged. GNU sparse extension blocks are still
|
|
339
|
+
`ArchiveFormatError("archive-header-invalid")`. GNU sparse extension blocks are still
|
|
211
340
|
metered in 512-byte units before rejection, preserving metadata-limit errors
|
|
212
341
|
for excessive chains. The per-body `maxMetaEntryBytes` limit bounds PAX storage
|
|
213
342
|
and duplicate-key state; one local header per member prevents local metadata
|
|
214
343
|
chains without introducing a new limit or changing defaults.
|
|
215
344
|
|
|
345
|
+
### Bounded GNU long names and links
|
|
346
|
+
|
|
347
|
+
Both raw meters buffer GNU long-name `L` and long-link `K` bodies within
|
|
348
|
+
`maxMetaEntryBytes` before either TAR parser runs. A body must contain a nonempty
|
|
349
|
+
UTF-8 name, with either no NUL or exactly one terminal NUL. Embedded NULs,
|
|
350
|
+
additional terminal NULs, bytes after a NUL, and invalid UTF-8 reject with
|
|
351
|
+
`ArchiveFormatError("archive-header-invalid")`. The meters preserve original
|
|
352
|
+
archive bytes, including the optional terminator and block padding.
|
|
353
|
+
|
|
354
|
+
One logical member may have at most one `L` and one `K`, in either order.
|
|
355
|
+
Repeated metadata of either kind, mixed PAX/GNU chains in either direction,
|
|
356
|
+
and GNU metadata without a following member reject with the same format error.
|
|
357
|
+
Pending metadata is cleared only when its described member is admitted;
|
|
358
|
+
metadata records do not count toward `maxEntries`.
|
|
359
|
+
|
|
360
|
+
An `L` name undergoes raw-path validation before parser normalization, stripping,
|
|
361
|
+
or filtering; unsafe paths reject with `ArchiveSecurityError("entry-path")`.
|
|
362
|
+
The validated name remains pending until its described header arrives. An
|
|
363
|
+
effective name ending in `/` or `\` requires raw directory type `5` or `D`;
|
|
364
|
+
other types reject with `ArchiveFormatError` before filtering, preventing the
|
|
365
|
+
parsers from disagreeing about a member's type.
|
|
366
|
+
`K` validates encoding and NUL structure without authorizing link creation.
|
|
367
|
+
Normal link/filter policy still governs the described member. Canonical
|
|
368
|
+
pre-strip filter paths, decoded-stream ceilings, and physical EOF checks apply
|
|
369
|
+
to plain/gzip TAR and native zstd/bzip2 alike.
|
|
370
|
+
|
|
216
371
|
## `resolveArchiveKind`
|
|
217
372
|
|
|
218
373
|
```ts
|
|
@@ -259,11 +414,28 @@ await extractArchive({
|
|
|
259
414
|
regular-file entry into a bounded `Buffer` without extracting a tree. It pins
|
|
260
415
|
and privately stages the archive input, rejects link, directory, and duplicate
|
|
261
416
|
entries, verifies ZIP CRC and declared size,
|
|
262
|
-
and throws `ArchiveLimitError` if
|
|
417
|
+
and throws `ArchiveLimitError` if the requested entry's output exceeds
|
|
418
|
+
`maxBytes`. ZIP output within that cap must match the declared uncompressed
|
|
419
|
+
size exactly; either a shorter or longer payload throws
|
|
420
|
+
`ArchiveFormatError("archive-header-invalid")` before bytes are returned on
|
|
421
|
+
both JavaScript and native backends.
|
|
422
|
+
For TAR, `maxBytes` applies only to that requested entry: a larger
|
|
423
|
+
unrequested member remains valid within the default archive admission limits.
|
|
424
|
+
TAR traversal uses default entry-count, compressed-input, and metadata limits,
|
|
425
|
+
plus the 768 MiB decoded ceiling derived from default extracted/archive byte
|
|
426
|
+
limits. It does not apply payload budgets to unrequested members. ZIP
|
|
263
427
|
inputs retain the archive subpath's 256 MiB compressed-input ceiling.
|
|
264
428
|
With a native binding it uses the same Rust decoders as extraction, including
|
|
265
429
|
zstd and bzip2 TAR. Without native it retains the JS ZIP/TAR/gzip implementation.
|
|
266
430
|
|
|
431
|
+
Requested paths and effective member names use extraction's canonical pre-strip
|
|
432
|
+
identity: backslashes become `/`, and repeated separators and `.` components
|
|
433
|
+
are removed after raw-path validation. For example, `./pkg//value` and
|
|
434
|
+
`pkg\value` both address `pkg/value`, including supported GNU/PAX and ZIP
|
|
435
|
+
Unicode Path names. Case and Unicode spelling are preserved. Requests ending
|
|
436
|
+
in `/` or `\` still reject as non-files. Canonical duplicate members reject
|
|
437
|
+
before an unrelated requested entry can be returned.
|
|
438
|
+
|
|
267
439
|
```ts
|
|
268
440
|
const rawManifest = await readArchiveEntry(uploadPath, "package/manifest.json", {
|
|
269
441
|
maxBytes: 64 * 1024,
|
|
@@ -308,6 +480,10 @@ import {
|
|
|
308
480
|
- `resolveArchiveOutputPath({ destDir, entryPath })` — combines the entry path with the destination, after validation.
|
|
309
481
|
- `isWindowsDrivePath(value)` — detects drive-relative segments such as `C:secret` or `nested/C:secret` that should be rejected.
|
|
310
482
|
|
|
483
|
+
Validate attacker-controlled paths before calling normalization or stripping
|
|
484
|
+
helpers. After validation, `stripArchivePath(entryPath, 0)` returns the canonical
|
|
485
|
+
pre-strip identity used by extraction filters (or `null` for an empty path).
|
|
486
|
+
|
|
311
487
|
## Common patterns
|
|
312
488
|
|
|
313
489
|
### Extract an upload, surface budget violations
|
package/docs/atomic.md
CHANGED
|
@@ -48,6 +48,7 @@ type ReplaceFileAtomicOptions = {
|
|
|
48
48
|
copyFallbackRestore?: "restore-original" | "none"; // default: "none"
|
|
49
49
|
maxRestoreBytes?: number; // required with "restore-original"
|
|
50
50
|
destinationHardlinks?: "reject"; // default unset (no destination nlink policy)
|
|
51
|
+
renameIdentity?: "strict" | "verify-content-with-lock"; // default "strict"
|
|
51
52
|
syncTempFile?: boolean; // fsync(temp) before rename, or the final file after copy fallback; default false
|
|
52
53
|
syncParentDir?: boolean; // fsync(parent) after rename, POSIX only; default false
|
|
53
54
|
throwOnCleanupError?: boolean; // report temp cleanup failure; default false
|
|
@@ -58,7 +59,7 @@ type ReplaceFileAtomicOptions = {
|
|
|
58
59
|
|
|
59
60
|
### `beforeRename`
|
|
60
61
|
|
|
61
|
-
Runs after the temp file is fully written and before the rename. Use it to take a backup snapshot, capture the about-to-be-replaced contents, or notify an observer:
|
|
62
|
+
Runs after the temp file is fully written and before the rename. Use it to take a backup snapshot, capture the about-to-be-replaced contents, or notify an observer. The helper retains the staged descriptor and exact bigint identity across the hook; replacing, deleting, hardlinking, or changing the temp entry to a non-regular file is rejected before publication:
|
|
62
63
|
|
|
63
64
|
```ts
|
|
64
65
|
await replaceFileAtomic({
|
|
@@ -70,7 +71,15 @@ await replaceFileAtomic({
|
|
|
70
71
|
});
|
|
71
72
|
```
|
|
72
73
|
|
|
73
|
-
If `beforeRename` throws, the rename is skipped and the temp file is removed — the destination is unchanged.
|
|
74
|
+
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.
|
|
75
|
+
|
|
76
|
+
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.
|
|
77
|
+
|
|
78
|
+
### FUSE mounts and unstable rename identity
|
|
79
|
+
|
|
80
|
+
Strict source-to-destination identity is the default. Some FUSE mounts assign a different inode to the destination during rename even without concurrency. Set `renameIdentity: "verify-content-with-lock"` to accept that boundary only when the re-opened no-follow destination has the exact requested SHA-256 content under an exclusive hashed sidecar lock in the destination parent. The newly accepted descriptor and identity remain pinned through parent sync and final verification. The synchronous helper provides the same policy with the synchronous lock implementation.
|
|
81
|
+
|
|
82
|
+
This is the same explicit weaker contract available on `Root` writes: cooperating writers are serialized, stale locks fail closed, and mismatched content is rejected after publication without rollback. A same-authority actor that ignores the advisory lock can still substitute another file with identical bytes, so do not use this compatibility policy in directories writable by untrusted same-UID processes.
|
|
74
83
|
|
|
75
84
|
### `EPERM` and copy fallback
|
|
76
85
|
|
package/docs/durability.md
CHANGED
|
@@ -90,7 +90,7 @@ target fails with `FsSafeError("path-mismatch")`.
|
|
|
90
90
|
## Exclusive file publication
|
|
91
91
|
|
|
92
92
|
`publishFileExclusive()` materializes one file without clobbering an existing
|
|
93
|
-
target. It pins the source with `O_NOFOLLOW`, optionally verifies
|
|
93
|
+
target. It pins the source with nonblocking `O_NOFOLLOW`, optionally verifies
|
|
94
94
|
`expectedSourceIdentity`, tries a hardlink first, then synchronizes the target
|
|
95
95
|
parent directory.
|
|
96
96
|
|
package/docs/file-store.md
CHANGED
|
@@ -31,6 +31,8 @@ const cache = fileStore({
|
|
|
31
31
|
});
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
Store and per-call `maxBytes` values must be non-negative safe integers or positive `Infinity`. Zero is an active zero-byte cap; `Infinity` disables the cap. An omitted or explicitly `undefined` per-call value preserves the store-level limit. The same rule applies to buffer writes, streams, copies, async reads, and synchronous reads/writes.
|
|
35
|
+
|
|
34
36
|
Use `private: true` for credentials, auth profiles, tokens, and other private
|
|
35
37
|
state. Private mode keeps the same `FileStore` shape but routes writes through
|
|
36
38
|
the secret-file atomic path, refusing symlink parent components and re-asserting
|
package/docs/install.md
CHANGED
|
@@ -92,9 +92,9 @@ native targets and npm-compatible OS, CPU, and Linux libc filters install only
|
|
|
92
92
|
the matching binary. Consumers do not run a native build, download code at
|
|
93
93
|
runtime, or execute a postinstall step. Omitting optional dependencies keeps
|
|
94
94
|
non-archive fallback-capable operations working in `auto` or `off`. Native-only
|
|
95
|
-
features, including retained-directory staging,
|
|
96
|
-
zstd/bzip2 TAR handling, and Windows private-directory
|
|
97
|
-
unavailable. Operations needing the binding in `require` mode fail with
|
|
95
|
+
features, including strict owned-tree temp cleanup, retained-directory staging,
|
|
96
|
+
atomic `rename-noreplace`, zstd/bzip2 TAR handling, and Windows private-directory
|
|
97
|
+
creation, remain unavailable. Operations needing the binding in `require` mode fail with
|
|
98
98
|
`helper-unavailable` when the matching package is absent or incompatible.
|
|
99
99
|
|
|
100
100
|
Upgrading an existing 0.5 consumer? Follow [Migrating to 0.6](migrating-to-0.6.md)
|
|
@@ -127,7 +127,11 @@ FS_SAFE_NATIVE_MODE=off # auto | off | require
|
|
|
127
127
|
Disabling native loading keeps fallback-capable operations working through Node path
|
|
128
128
|
operations guarded by lexical and canonical checks plus identity verification.
|
|
129
129
|
Use `require` when native-backed operations must fail instead of falling back.
|
|
130
|
-
|
|
130
|
+
Temp workspaces retain compatible JavaScript quarantine cleanup in `auto` and
|
|
131
|
+
`off`. Set `cleanupSafety: "require-bounded"` to reject before child creation
|
|
132
|
+
unless native no-replace quarantine and descriptor-bounded tree removal are
|
|
133
|
+
available. See the [temp workspace contract](temp.md#private-temp-workspaces). The exact boundary
|
|
134
|
+
for other operations is documented in [native helper policy](native-helper.md).
|
|
131
135
|
|
|
132
136
|
## Verify the install
|
|
133
137
|
|
package/docs/json.md
CHANGED
|
@@ -135,10 +135,17 @@ where lower latency matters more than crash-durability.
|
|
|
135
135
|
|
|
136
136
|
Synchronous variant. It pretty-prints with two spaces, appends a newline,
|
|
137
137
|
creates parents at `0o700`, writes at `0o600`, and synchronizes the parent
|
|
138
|
-
directory best-effort.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
138
|
+
directory best-effort. File-mode tightening carries the staged bigint identity
|
|
139
|
+
through rename and applies `fchmod` only when the reopened descriptor and current
|
|
140
|
+
pathname still name that same single-link regular file; a swap is preserved and
|
|
141
|
+
skips this best-effort step. It has no options bag. On `EPERM`/`EEXIST`, its legacy
|
|
142
|
+
compatibility path removes the existing destination and retries the staged-file
|
|
143
|
+
rename, so that fallback is temporarily non-atomic while retaining the staged
|
|
144
|
+
file's `0600` mode; use the async `writeJson()`/`replaceFileAtomic()` surfaces
|
|
145
|
+
when fallback policy must be explicit.
|
|
146
|
+
|
|
147
|
+
Its private sibling temporary name is independent of the destination basename,
|
|
148
|
+
so staging does not lengthen a valid destination filename.
|
|
142
149
|
|
|
143
150
|
```ts
|
|
144
151
|
writeJsonSync("./prefs.json", { theme: "dark" });
|
package/docs/native-helper.md
CHANGED
|
@@ -31,6 +31,18 @@ The equivalent environment variables are `FS_SAFE_NATIVE_MODE` and `OPENCLAW_FS_
|
|
|
31
31
|
|
|
32
32
|
Configure the mode once during startup. Loading is lazy and cached; changing from `auto` to `require` after a failed load changes failure policy but does not repeatedly probe the binary.
|
|
33
33
|
|
|
34
|
+
[`tempWorkspace()` and its scoped/sync variants](temp.md#private-temp-workspaces)
|
|
35
|
+
remain available in every mode. Their default compatible cleanup uses guarded
|
|
36
|
+
JavaScript quarantine when owned native tree removal is unavailable.
|
|
37
|
+
`cleanupSafety: "require-bounded"` instead rejects before child creation unless
|
|
38
|
+
no-replace quarantine plus descriptor-relative owned-tree removal are available.
|
|
39
|
+
On Linux, admission probes the exact `openat2` child-directory flags, including
|
|
40
|
+
`RESOLVE_NO_XDEV`, at runtime. An unavailable or denied probe selects compatible
|
|
41
|
+
JavaScript cleanup even in global `require` mode; `require-bounded` rejects before
|
|
42
|
+
child creation.
|
|
43
|
+
Already-created strict workspaces retain their binding
|
|
44
|
+
and descriptors across later mode changes.
|
|
45
|
+
|
|
34
46
|
[`stageFileInDirectory()`](staged-file.md) always requires native support on
|
|
35
47
|
Linux/macOS and rejects before creation when off, unavailable, or missing the
|
|
36
48
|
required capability. Windows is unsupported for this lifecycle. This does not
|
|
@@ -44,9 +56,9 @@ clone/copy/hash workers, and Windows security descriptor calls. The TypeScript
|
|
|
44
56
|
layer owns policy, retries, filters, budgets, modes, cleanup, error
|
|
45
57
|
normalization, and the decision to fall back.
|
|
46
58
|
|
|
47
|
-
- Linux uses `openat2` with `RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS`, `renameat`, and `renameat2(RENAME_NOREPLACE)`.
|
|
48
|
-
- 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
|
|
49
|
-
- Windows uses handle-relative `NtCreateFile`, rejects reparse points,
|
|
59
|
+
- 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.
|
|
60
|
+
- 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`.
|
|
61
|
+
- Windows uses handle-relative `NtCreateFile`, rejects reparse points during root-bounded traversal, uses `FileRenameInfoEx` with replacement selected explicitly by the TypeScript policy layer, and deletes owned trees through exact opened handles with `FileDispositionInfoEx`; symlink/reparse entries in owned trees are removed as leaves and never traversed.
|
|
50
62
|
|
|
51
63
|
Native primitives back create-only and replacing pinned writes, async sidecar creation,
|
|
52
64
|
guarded publication, archive acceleration, and direct Windows ACL operations.
|
package/docs/native.md
CHANGED
|
@@ -64,12 +64,39 @@ returns a bounded manifest. TypeScript applies the shared path, filter, strip,
|
|
|
64
64
|
mode, and byte policies and returns an index-bound extraction plan. Rust then
|
|
65
65
|
creates only those planned entries beneath a private staging descriptor.
|
|
66
66
|
|
|
67
|
-
A
|
|
68
|
-
|
|
69
|
-
metadata
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
67
|
+
A raw meter sits between decompression and the TAR crate, with matching
|
|
68
|
+
TypeScript admission before node-tar. It parses 512-byte headers and bounded
|
|
69
|
+
local PAX `x` metadata, using supported effective sizes to locate the following
|
|
70
|
+
member body. GNU long-name/link `L`/`K` payloads remain
|
|
71
|
+
supported. `maxMetaEntryBytes` bounds each metadata body before allocation;
|
|
72
|
+
unsupported global/old metadata and sparse forms fail closed rather than being
|
|
73
|
+
interpreted as ordinary members. See [bounded local PAX support](archive.md#bounded-local-pax-support).
|
|
74
|
+
|
|
75
|
+
Every raw pass receives only TypeScript's resolved `maxEntries`,
|
|
76
|
+
`maxMetaEntryBytes`, and `maxDecodedBytes`. Shared resolution caps metadata and
|
|
77
|
+
decoded byte fields at JavaScript's safe-integer maximum and entry counts at
|
|
78
|
+
`2^32 - 1` before backend selection. Large finite limits remain accepted;
|
|
79
|
+
native conversion mirrors those caps and rejects malformed non-finite or
|
|
80
|
+
negative direct-call values before casting. Logical member headers count
|
|
81
|
+
before filtering/stripping; metadata records do not. `maxEntryBytes` and
|
|
82
|
+
`maxExtractedBytes` remain exclusively in TypeScript's accepted-plan builder,
|
|
83
|
+
after strip/filter policy, and are absent from the raw meter's interface.
|
|
84
|
+
Bounded reads use the default count/metadata/decoded bounds; public `maxBytes`
|
|
85
|
+
bounds only the requested output. TypeScript derives the internal decoded cap
|
|
86
|
+
by safely adding `maxExtractedBytes` and `maxArchiveBytes`, clamped to the safe
|
|
87
|
+
integer maximum. Every native pass receives that same cap and charges headers,
|
|
88
|
+
metadata, bodies, padding, EOF blocks, and trailing zeros. It rejects overflow
|
|
89
|
+
with `archive-decoded-size-exceeds-limit`; no ratio policy is implied.
|
|
90
|
+
Extraction and entry reads drain the metered reader through physical EOF after
|
|
91
|
+
TAR iteration. Trailing framing or decoded-limit failures propagate before
|
|
92
|
+
directory modes are finalized, staging is published, or selected bytes return.
|
|
93
|
+
Native reads stop at framing boundaries so a rejected header does not request
|
|
94
|
+
its body from the decoder; codec buffering can still read ahead internally.
|
|
95
|
+
Inspection finishes the complete bounded framing pass before parsing. Directory
|
|
96
|
+
and link bodies, missing two-block EOF, and nonzero trailers reject on both
|
|
97
|
+
backends, as detailed in [raw TAR framing](archive.md#raw-tar-framing). Raw and
|
|
98
|
+
padded sizes above JavaScript's safe-integer maximum reject as invalid framing
|
|
99
|
+
before applying member budgets, including when local PAX overrides the size.
|
|
73
100
|
|
|
74
101
|
## Publication and hashing
|
|
75
102
|
|
package/docs/output.md
CHANGED
|
@@ -48,6 +48,8 @@ The requested `path` must name a file. Missing destination parents are created
|
|
|
48
48
|
by the helper because the operation is "produce this output file under the
|
|
49
49
|
root"; callers should choose the filename before calling this API.
|
|
50
50
|
|
|
51
|
+
`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.
|
|
52
|
+
|
|
51
53
|
Use `maxBytes` when the external producer can create arbitrarily large files,
|
|
52
54
|
and `mode` when the finalized file needs a specific POSIX mode. Both staging
|
|
53
55
|
modes enforce them after the producer returns and before committing the target.
|
|
@@ -56,9 +58,12 @@ through the package's filename sanitizer; `fallbackFileName` supplies the name
|
|
|
56
58
|
when nothing remains. This removes traversal, device-name, and invalid-character
|
|
57
59
|
hazards but does not trim Windows-normalized trailing dots or spaces; reject or
|
|
58
60
|
rewrite those when cross-platform filename uniqueness matters.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
`staging: "workspace"` passes the sanitized basename to the producer.
|
|
62
|
+
`staging: "sibling"` embeds that basename in its randomized temporary name.
|
|
63
|
+
The final target and returned `path` use the destination basename, sanitized
|
|
64
|
+
when needed as described above. Guarded temporary files used only inside
|
|
65
|
+
fs-safe have independent names so their length does not grow with the
|
|
66
|
+
destination basename.
|
|
62
67
|
|
|
63
68
|
## Choosing a staging mode
|
|
64
69
|
|
|
@@ -80,6 +85,14 @@ rename. If an error leaves the sibling temp in place and immediate cleanup
|
|
|
80
85
|
fails, its verified identity remains registered for a best-effort process-exit
|
|
81
86
|
cleanup retry.
|
|
82
87
|
|
|
88
|
+
Sibling staging shares the [callback sibling owner](temp.md#sibling-temp-writes):
|
|
89
|
+
it checks exact pre-open, descriptor, and current-path identities, retains the
|
|
90
|
+
descriptor through publication, and never chmods or reads a replacement by path.
|
|
91
|
+
Cleanup preserves unverified paths, including partial output when the callback
|
|
92
|
+
throws before admission. Native-off and Windows operation remain supported with
|
|
93
|
+
the platform limits and non-atomic rename/unlink identity checks described there.
|
|
94
|
+
When `mode` is omitted, output-sibling staging preserves the producer's mode.
|
|
95
|
+
|
|
83
96
|
## Why not pass the final path to the library?
|
|
84
97
|
|
|
85
98
|
If a target parent can be swapped after validation, handing an external library
|
package/docs/public-api.md
CHANGED
|
@@ -69,9 +69,12 @@ The file-lock diagnostics surface includes `FileLockHeldEntry`,
|
|
|
69
69
|
manager controls; production code should not use them as lock recovery.
|
|
70
70
|
|
|
71
71
|
Standalone walkers use the `WalkEntryKind` and `WalkSymlinkPolicy` unions.
|
|
72
|
-
Private workspaces expose `TempPathIdentityReceipt
|
|
73
|
-
`TempWorkspaceCleanupResult`
|
|
74
|
-
|
|
72
|
+
Private workspaces expose `TempPathIdentityReceipt`,
|
|
73
|
+
`TempWorkspaceCleanupResult`, and `TempWorkspaceCleanupSafety` so callers can
|
|
74
|
+
distinguish outcomes and select compatible or required bounded-tree cleanup.
|
|
75
|
+
Strict workspace creation requires native no-replace quarantine plus retained
|
|
76
|
+
parent/workspace descriptors; see the
|
|
77
|
+
[creation and cleanup contract](temp.md#private-temp-workspaces).
|
|
75
78
|
|
|
76
79
|
## Atomic replacement and durability
|
|
77
80
|
|
package/docs/quickstart.md
CHANGED
|
@@ -145,7 +145,7 @@ await withTempWorkspace({ rootDir: "/srv/jobs/tmp", prefix: "build-" }, async (w
|
|
|
145
145
|
});
|
|
146
146
|
```
|
|
147
147
|
|
|
148
|
-
The directory is mode `0700
|
|
148
|
+
The directory is mode `0700` under the caller-provided root, and cleanup runs when the callback returns or throws. Compatible cleanup remains available without native support; pass `cleanupSafety: "require-bounded"` to require native no-replace quarantine and descriptor-bounded tree removal before creating a child. Cleanup preserves raced or ambiguous entries under a private quarantine name. See the [temp workspace compatibility and security contract](temp.md#private-temp-workspaces).
|
|
149
149
|
|
|
150
150
|
## Where to next
|
|
151
151
|
|
package/docs/reading.md
CHANGED
|
@@ -89,7 +89,7 @@ type RootReadOptions = {
|
|
|
89
89
|
};
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
`maxBytes` is enforced eagerly: the library reads up to `maxBytes + 1` and throws `too-large` if there is more, so a hostile target cannot silently exhaust memory.
|
|
92
|
+
`maxBytes` is enforced eagerly: the library reads up to `maxBytes + 1` and throws `too-large` if there is more, so a hostile target cannot silently exhaust memory. Values must be non-negative safe integers or positive `Infinity`; zero is an active cap, while `Infinity` disables it. Explicitly forwarding `undefined` preserves the Root default.
|
|
93
93
|
|
|
94
94
|
`nonBlockingRead` remains as a compatibility hint. Safe reads always add the platform's nonblocking open flag where available so a raced FIFO cannot pin a worker indefinitely; regular-file descriptor reads retain normal Node behavior.
|
|
95
95
|
|
package/docs/regular-file.md
CHANGED
|
@@ -64,7 +64,11 @@ processLog(result.buffer);
|
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
The result is `{ buffer, stat }`. Missing files preserve the normal `ENOENT`
|
|
67
|
-
shape; non-regular targets throw.
|
|
67
|
+
shape; non-regular targets throw. Reads compare exact bigint identities from the
|
|
68
|
+
preview, opened descriptor, and current path before consuming bytes; the returned
|
|
69
|
+
`stat` remains numeric Node `Stats`. Unknown Windows identities receive one
|
|
70
|
+
re-inspection without reopening; persistent unknowns or replacements throw
|
|
71
|
+
`FsSafeError("path-mismatch")`.
|
|
68
72
|
|
|
69
73
|
Throws `FsSafeError` with code `too-large` if the file exceeds `maxBytes`. Other I/O errors propagate as `NodeJS.ErrnoException`.
|
|
70
74
|
|
|
@@ -101,7 +105,10 @@ type AppendRegularFileOptions = {
|
|
|
101
105
|
};
|
|
102
106
|
```
|
|
103
107
|
|
|
104
|
-
The helper refuses symlink and hardlinked final targets.
|
|
108
|
+
The helper refuses symlink and hardlinked final targets. Preview, opened
|
|
109
|
+
descriptor, and current pathname identities remain exact bigints through the
|
|
110
|
+
append boundary; rounded-equal replacements and persistent unknown Windows
|
|
111
|
+
identities reject before chmod or writing bytes. With
|
|
105
112
|
`rejectSymlinkParents: true`, it also rejects symlinked ancestor directories.
|
|
106
113
|
|
|
107
114
|
### `appendRegularFileSync(options)`
|