@openclaw/fs-safe 0.19.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/README.md +24 -6
- package/dist/advanced.d.ts +4 -0
- package/dist/advanced.js +2 -0
- package/dist/archive-plan.d.ts +2 -7
- package/dist/archive-read.js +9 -18
- package/dist/archive-zip-entry.d.ts +11 -11
- package/dist/archive-zip-entry.js +3 -35
- package/dist/archive-zip-integrity.d.ts +2 -2
- package/dist/archive-zip-integrity.js +2 -12
- package/dist/archive-zip-loader.d.ts +7 -3
- package/dist/archive-zip-loader.js +10 -9
- package/dist/archive-zip-preflight.d.ts +2 -1
- package/dist/archive-zip-preflight.js +16 -7
- package/dist/archive.js +17 -16
- package/dist/atomic.d.ts +1 -1
- package/dist/directory-receipt.js +5 -7
- package/dist/effective-uid.js +1 -4
- package/dist/errors.d.ts +3 -1
- package/dist/errors.js +3 -2
- package/dist/file-lock-sync-root-held.js +1 -4
- package/dist/file-store.d.ts +4 -7
- package/dist/json-document-store.d.ts +4 -9
- package/dist/local-file-access.js +2 -5
- package/dist/move-path-cleanup.js +4 -4
- package/dist/native-binding.d.ts +28 -1
- package/dist/native-staged-symlink.d.ts +13 -0
- package/dist/native-staged-symlink.js +303 -0
- package/dist/owner-dacl-batch-worker.d.ts +1 -0
- package/dist/owner-dacl-batch-worker.js +54 -0
- package/dist/owner-dacl-batch.d.ts +5 -0
- package/dist/owner-dacl-batch.js +64 -0
- package/dist/owner-dacl.d.ts +2 -0
- package/dist/owner-dacl.js +3 -0
- package/dist/path.js +17 -1
- package/dist/permission-exec.js +3 -6
- package/dist/permissions-public.d.ts +1 -0
- package/dist/permissions-public.js +1 -0
- package/dist/pinned-mutation-admission.d.ts +0 -1
- package/dist/pinned-open.d.ts +0 -1
- package/dist/pinned-open.js +1 -2
- package/dist/publish-copy-stage.js +4 -0
- package/dist/read-opened-file.d.ts +2 -5
- package/dist/regular-file.js +3 -3
- package/dist/replace-file-buffer.d.ts +4 -0
- package/dist/replace-file-buffer.js +36 -0
- package/dist/replace-file-copy-fallback.d.ts +3 -2
- package/dist/replace-file-copy-fallback.js +66 -38
- package/dist/replace-file-descriptor.d.ts +4 -0
- package/dist/replace-file-descriptor.js +9 -1
- package/dist/replace-file-destination.d.ts +17 -0
- package/dist/replace-file-destination.js +61 -0
- package/dist/replace-file-mutation.d.ts +26 -0
- package/dist/replace-file-mutation.js +47 -0
- package/dist/replace-file-temp-owner.d.ts +2 -2
- package/dist/replace-file-temp-owner.js +16 -4
- package/dist/replace-file-types.d.ts +55 -0
- package/dist/replace-file-types.js +1 -0
- package/dist/replace-file.d.ts +3 -55
- package/dist/replace-file.js +29 -10
- package/dist/retained-file-types.d.ts +61 -0
- package/dist/retained-file-types.js +1 -0
- package/dist/retained-file.d.ts +3 -0
- package/dist/retained-file.js +121 -0
- package/dist/root-directory-entry.d.ts +9 -0
- package/dist/root-directory-entry.js +28 -0
- package/dist/root-directory-list.d.ts +7 -1
- package/dist/root-directory-list.js +48 -23
- package/dist/root-handle-context.d.ts +4 -0
- package/dist/root-handle-context.js +12 -0
- package/dist/root-impl.d.ts +3 -3
- package/dist/root-impl.js +8 -5
- package/dist/root-observed-path.d.ts +0 -1
- package/dist/root-observed-path.js +0 -3
- package/dist/root-path-observation.d.ts +4 -11
- package/dist/root-path.js +7 -10
- package/dist/root-walk.d.ts +19 -12
- package/dist/root-walk.js +49 -18
- package/dist/root-write-admission.js +0 -2
- package/dist/safe-path-segment.d.ts +1 -0
- package/dist/safe-path-segment.js +8 -2
- package/dist/secure-file.js +3 -2
- package/dist/sidecar-lock.js +5 -3
- package/dist/staged-symlink-types.d.ts +49 -0
- package/dist/staged-symlink-types.js +1 -0
- package/dist/symlink-parents.js +58 -7
- package/dist/temp-target.js +5 -2
- package/dist/temp-workspace-admission.js +22 -21
- package/dist/temp-workspace-child-admission.d.ts +1 -1
- package/dist/temp-workspace-child-admission.js +14 -9
- package/dist/temp-workspace-owner.js +4 -9
- package/dist/temp-workspace-ownership.d.ts +8 -0
- package/dist/temp-workspace-ownership.js +52 -0
- package/dist/test-hooks.d.ts +3 -0
- package/dist/text-atomic.d.ts +2 -1
- package/dist/text-atomic.js +2 -0
- package/dist/trash.js +27 -1
- package/dist/walk.d.ts +2 -5
- package/dist/watch-alias.d.ts +6 -0
- package/dist/watch-alias.js +80 -0
- package/dist/watch-hints.d.ts +8 -0
- package/dist/watch-hints.js +77 -0
- package/dist/watch-native.d.ts +32 -0
- package/dist/watch-native.js +56 -0
- package/dist/watch-scan.d.ts +24 -0
- package/dist/watch-scan.js +269 -0
- package/dist/watch-types.d.ts +58 -0
- package/dist/watch-types.js +1 -0
- package/dist/watch.d.ts +5 -0
- package/dist/watch.js +502 -0
- package/dist/windows-owner.d.ts +0 -1
- package/dist/windows-owner.js +0 -1
- package/dist/windows-security-bridge.cs +6 -4
- package/dist/windows-security-bridge.ps1 +78 -3
- package/dist/windows-security-command.d.ts +8 -0
- package/dist/windows-security-command.js +66 -12
- package/dist/windows-security-facts.d.ts +3 -0
- package/dist/windows-security-facts.js +4 -0
- package/docs/advanced.md +4 -2
- package/docs/archive.md +8 -0
- package/docs/atomic.md +72 -3
- package/docs/contributing.md +35 -0
- package/docs/durability.md +7 -0
- package/docs/index.md +1 -0
- package/docs/install.md +28 -0
- package/docs/native-helper.md +14 -4
- package/docs/native.md +45 -1
- package/docs/permissions.md +66 -0
- package/docs/public-api.md +7 -1
- package/docs/retained-file.md +113 -0
- package/docs/root.md +6 -1
- package/docs/security-model.md +4 -1
- package/docs/sidecar-lock.md +2 -0
- package/docs/staged-symlink.md +123 -0
- package/docs/store.md +3 -1
- package/docs/temp.md +24 -4
- package/docs/testing.md +88 -0
- package/docs/types.md +6 -0
- package/docs/walk.md +22 -1
- package/docs/watch.md +184 -0
- package/docs/writing.md +10 -0
- package/package.json +13 -9
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,56 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.21.0 - 2026-09-26
|
|
6
|
+
|
|
7
|
+
### Highlights
|
|
8
|
+
|
|
9
|
+
- **Guarded filesystem observation:** `@openclaw/fs-safe/watch` observes literal entry and tree scopes beneath an admitted Root and delivers bounded advisory invalidations; guarded scans stay authoritative. One shared Rust event thread per process uses inotify, FSEvents and ReadDirectoryChangesW on Linux, macOS and Windows, sleeps while idle, and falls back to portable polling under `mode: "auto"`. Subscriptions become ready after one baseline scan, stay available under sustained writes, fence stale generations on `setScopes()`, join native work on `close()`, and support `persistent: false` so one-shot Node commands can exit. Thanks @vincentkoc. ([#690](https://github.com/openclaw/fs-safe/pull/690), [#695](https://github.com/openclaw/fs-safe/pull/695), [#697](https://github.com/openclaw/fs-safe/pull/697), [#703](https://github.com/openclaw/fs-safe/pull/703), [#707](https://github.com/openclaw/fs-safe/pull/707))
|
|
10
|
+
- **Linux user namespaces:** private temp workspaces now work beneath unmapped ancestors, including systemd user services with `PrivateUsers=true`, while UID/GID mappings and ancestor permissions are still rechecked. ([#693](https://github.com/openclaw/fs-safe/pull/693))
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
- **Revocable atomic writes:** atomic replacement can recheck caller authority before new effects and report retained destination identities for partial writes and publication, without treating receipts as permission to roll back. ([#694](https://github.com/openclaw/fs-safe/pull/694))
|
|
15
|
+
- **Root walking:** `symlinkPolicy: "include"` reports links without following their targets, preserving sorted traversal, entry budgets, and the existing skip/follow result types. Directory-to-symlink substitutions fail before descent. ([#692](https://github.com/openclaw/fs-safe/pull/692))
|
|
16
|
+
- **Windows file retirement:** the advanced surface can retire existing NTFS files through a retained handle, with exact producer identity, current authority, and separate disposition and settlement facts. There is no persistence guarantee. ([#706](https://github.com/openclaw/fs-safe/pull/706))
|
|
17
|
+
|
|
18
|
+
### Performance
|
|
19
|
+
|
|
20
|
+
- **Temporary filename sanitization:** skip duplicate normalization after reserved-device suffixing while preserving filename admission and fallback rules. ([#689](https://github.com/openclaw/fs-safe/pull/689))
|
|
21
|
+
|
|
22
|
+
### Compatibility
|
|
23
|
+
|
|
24
|
+
- Supplied temp workspace roots must be owned by the effective user and must not be group- or world-writable; use a private per-user root instead of shared `/tmp`. A one-time warning reports host ownership that cannot be verified. ([#693](https://github.com/openclaw/fs-safe/pull/693))
|
|
25
|
+
- On Windows, an events-mode watch holds one handle on each watched Root, so the Root's own ancestor directories cannot be renamed while it is open; renaming inside the Root is unaffected. Use `mode: "poll"` where that matters. ([#697](https://github.com/openclaw/fs-safe/pull/697))
|
|
26
|
+
- `watch()` requires an explicit `mode` (`"auto"`, `"events"` or `"poll"`); `"events"` fails readiness with `helper-unavailable` when no native backend is available, and Bun currently selects polling under `"auto"`. ([#690](https://github.com/openclaw/fs-safe/pull/690))
|
|
27
|
+
|
|
28
|
+
## 0.20.0 - 2026-09-25
|
|
29
|
+
|
|
30
|
+
### Highlights
|
|
31
|
+
|
|
32
|
+
- **Older Linux systems:** when `openat2` is absent (Linux before 5.6) or blocked by seccomp, native operations fall back to a guarded no-follow walk instead of failing with `ENOSYS`. Nested no-clobber moves keep working, and GNU x64/arm64 bindings now target glibc 2.28, so native operations load on RHEL 8-family systems. ([#686](https://github.com/openclaw/fs-safe/pull/686), [#685](https://github.com/openclaw/fs-safe/pull/685); fixes [#572](https://github.com/openclaw/fs-safe/issues/572), [#511](https://github.com/openclaw/fs-safe/issues/511), [#548](https://github.com/openclaw/fs-safe/issues/548))
|
|
33
|
+
- **Path hardening:** trash moves stay inside allowed roots, symlink-parent checks inspect raw segments before dotdot normalization, relative-escape checks recognize either Windows separator and nested escapes, and safe path segments reject Windows reserved device names. Thanks @SebTardif. ([#611](https://github.com/openclaw/fs-safe/pull/611), [#615](https://github.com/openclaw/fs-safe/pull/615), [#614](https://github.com/openclaw/fs-safe/pull/614), [#612](https://github.com/openclaw/fs-safe/pull/612))
|
|
34
|
+
- **ZIP admission per operation:** portable ZIP reads and extraction bind entry selection to the admitted archive, name, and decoder object, and verify payloads against the admitted CRC and size. Substituted, renamed, or replaced decoder entries are rejected. ([#660](https://github.com/openclaw/fs-safe/pull/660))
|
|
35
|
+
|
|
36
|
+
### Features
|
|
37
|
+
|
|
38
|
+
- **Retained symlink publication:** `retainSymlinkInDirectory()` on the advanced surface holds an explicitly identified POSIX symlink through exact-slot no-replace publication and explicit recovery, preserving observed foreign replacements and uncertain outcomes. ([#682](https://github.com/openclaw/fs-safe/pull/682))
|
|
39
|
+
- **Batched Windows ACL facts:** `readOwnerAndDaclBatch()` inspects ordered paths in one isolated native worker or one PowerShell process, with a configurable whole-batch timeout and bounded output. Results are bounded during collection; oversized batches reject with `too-large` before later paths are queried. ([#669](https://github.com/openclaw/fs-safe/pull/669), [#680](https://github.com/openclaw/fs-safe/pull/680))
|
|
40
|
+
- **Atomic text options:** `writeTextAtomic()` forwards the existing `beforeRename` hook and `tempPrefix` option to atomic replacement, keeping its validation and identity checks. ([#580](https://github.com/openclaw/fs-safe/pull/580))
|
|
41
|
+
|
|
42
|
+
### Fixes
|
|
43
|
+
|
|
44
|
+
- **Lock exit cleanup:** leave raw sidecars in place when Windows reports an unknown device or inode, while keeping token-owned cleanup working when the descriptor and path identities legitimately differ (for example on VirtioFS). Thanks @SebTardif. ([#617](https://github.com/openclaw/fs-safe/pull/617))
|
|
45
|
+
- **Retained symlink errors:** preserve uncertain publication outcomes and cached cleanup/recovery failures when inspecting thrown error metadata fails, retaining the original cause without retrying mutations, callbacks, or descriptor closes. ([#684](https://github.com/openclaw/fs-safe/pull/684))
|
|
46
|
+
- **Create collision cleanup:** atomic and streamed creates remove their private stage when the JavaScript fallback observes a competing destination before publication; failures after a link attempt retain their existing recovery evidence. ([#683](https://github.com/openclaw/fs-safe/pull/683))
|
|
47
|
+
|
|
48
|
+
### Compatibility
|
|
49
|
+
|
|
50
|
+
- Safe path segments now reject Windows reserved device names (`CON`, `NUL`, `COM1`, `CON.json`, …) on every platform, because segments are portable identifiers. Temporary filename sanitization still suffixes them (`CON.txt` becomes `CON_.txt`). ([#612](https://github.com/openclaw/fs-safe/pull/612))
|
|
51
|
+
- `assertNoSymlinkParents()` and guarded appends with `rejectSymlinkParents` reject raw spellings whose dotdot segments would cancel a symlink or re-enter the root after leaving it. ([#615](https://github.com/openclaw/fs-safe/pull/615))
|
|
52
|
+
- Without `openat2`, beneath opens report `best-effort` containment instead of `kernel-atomic`, anonymous `O_TMPFILE` opens report `ENOTSUP`, and bounded tree cleanup fails closed with `helper-unavailable` because it requires `RESOLVE_NO_XDEV`. ([#686](https://github.com/openclaw/fs-safe/pull/686))
|
|
53
|
+
- The Linux GNU binding floor is glibc 2.28 for both x64 and arm64 (arm64 was previously built against 2.17), matching Node 22's own Linux baseline. ([#685](https://github.com/openclaw/fs-safe/pull/685))
|
|
54
|
+
|
|
5
55
|
## 0.19.0 - 2026-09-24
|
|
6
56
|
|
|
7
57
|
### Highlights
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
Capability-style filesystem roots for Node.js apps that handle untrusted relative paths.
|
|
12
12
|
|
|
13
|
-
Think Go's `os.Root` / `OpenInRoot` or Rust's [`cap-std`](https://github.com/bytecodealliance/cap-std), but for Node. Hand `root()` a trusted directory and you get back a handle whose every method resolves relative paths against it and defends against `..`, symlink swaps, hardlink aliases, and TOCTOU rename races. The exact containment strength is reported per mechanism: Linux
|
|
13
|
+
Think Go's `os.Root` / `OpenInRoot` or Rust's [`cap-std`](https://github.com/bytecodealliance/cap-std), but for Node. Hand `root()` a trusted directory and you get back a handle whose every method resolves relative paths against it and defends against `..`, symlink swaps, hardlink aliases, and TOCTOU rename races. The exact containment strength is reported per mechanism: Linux `openat2` opens are kernel-atomic; guarded Linux fallback, macOS, Windows, and JavaScript paths are best-effort.
|
|
14
14
|
|
|
15
15
|
```ts
|
|
16
16
|
import { root } from "@openclaw/fs-safe";
|
|
@@ -45,7 +45,7 @@ The same idea has landed in other languages. Go [added `os.Root` and `OpenInRoot
|
|
|
45
45
|
| `path.resolve().startsWith()` | string check only | – | – | – | – |
|
|
46
46
|
| [`write-file-atomic`](https://www.npmjs.com/package/write-file-atomic) | – | ✓ | – | – | – |
|
|
47
47
|
| Go [`os.Root`](https://go.dev/blog/osroot) / Rust [`cap-std`](https://github.com/bytecodealliance/cap-std) | ✓ | platform | ✓ | ✓ | – |
|
|
48
|
-
| **`@openclaw/fs-safe`** | **✓** | **✓** | **✓** | **Linux atomic; others best-effort** | **✓ (ZIP/TAR/gzip/zstd/bzip2)** |
|
|
48
|
+
| **`@openclaw/fs-safe`** | **✓** | **✓** | **✓** | **Linux openat2 atomic; others best-effort** | **✓ (ZIP/TAR/gzip/zstd/bzip2)** |
|
|
49
49
|
|
|
50
50
|
## Not a sandbox
|
|
51
51
|
|
|
@@ -84,7 +84,9 @@ reports the escape. Use `require` when hostile concurrent mutation is in scope.
|
|
|
84
84
|
|
|
85
85
|
Equivalent env var: `FS_SAFE_NATIVE_MODE=auto|off|require`. The seven bindings
|
|
86
86
|
ship as exact-version optional packages filtered by OS, CPU, and Linux libc, so
|
|
87
|
-
a normal install receives only its matching binary.
|
|
87
|
+
a normal install receives only its matching binary. Linux GNU x64/arm64 bindings
|
|
88
|
+
support [glibc 2.28 or newer](docs/install.md#supported-native-platforms), including
|
|
89
|
+
RHEL 8-family systems. There are no postinstall
|
|
88
90
|
steps, runtime downloads, or consumer Rust builds. On a platform without a
|
|
89
91
|
published binding, or when optional dependencies are omitted, `auto` silently retains lexical and canonical root
|
|
90
92
|
checks, no-follow opens, guarded temp+rename writes, and post-write identity
|
|
@@ -94,8 +96,11 @@ tradeoff, and [native architecture](docs/native.md) for the platform mechanisms
|
|
|
94
96
|
and policy ownership model.
|
|
95
97
|
|
|
96
98
|
Open results report the mechanism's containment class as `"kernel-atomic"` or
|
|
97
|
-
`"best-effort"`. Linux native `openBeneath()` is kernel-atomic
|
|
98
|
-
|
|
99
|
+
`"best-effort"`. Linux native `openBeneath()` is kernel-atomic when `openat2`
|
|
100
|
+
is available. Older kernels and syscall-filtered containers use a guarded
|
|
101
|
+
descriptor-relative walk reporting best-effort; nested no-clobber moves keep
|
|
102
|
+
atomic `renameat2(RENAME_NOREPLACE)`. macOS, Windows, and guarded JavaScript
|
|
103
|
+
results are best-effort. See [Linux compatibility](docs/native.md#linux-without-openat2). See the [security model](docs/security-model.md#containment-guarantees-by-platform) before using that fact in higher-level policy.
|
|
99
104
|
|
|
100
105
|
## Migrating from the Python helper
|
|
101
106
|
|
|
@@ -274,6 +279,7 @@ contract. Low-level helpers that OpenClaw needs to compose higher-level APIs are
|
|
|
274
279
|
| `@openclaw/fs-safe/secure-file` | fd-pinned absolute file reads with owner, mode, ACL, trusted-dir, size, and timeout checks |
|
|
275
280
|
| `@openclaw/fs-safe/file-lock` | async/sync sidecar locks, root-bounded sidecars, ownership verification, and stale policy |
|
|
276
281
|
| `@openclaw/fs-safe/permissions` | POSIX mode and Windows ACL inspection, raw owner/ACE facts, private-directory creation, and remediation helpers |
|
|
282
|
+
| [`@openclaw/fs-safe/watch`](docs/watch.md) | Guarded observation with native event hints, bounded scans, and joined close |
|
|
277
283
|
| `@openclaw/fs-safe/walk` | budget-bounded directory walking with symlink policy, filters, and truncation accounting; not root-bounded |
|
|
278
284
|
| `@openclaw/fs-safe/copy` | directory copying with `clone: "auto"`, `"always"`, or `"never"`; native APFS, Btrfs, ReFS, XFS, and ZFS cloning, portable byte copying, and clone metadata; see [directory copying](docs/copy.md) |
|
|
279
285
|
| `@openclaw/fs-safe/archive` | policy-driven ZIP/TAR extraction, clamp/filter policy, metadata/path-depth limits, gzip/zstd/bzip2 support, and bounded entry reads |
|
|
@@ -342,6 +348,11 @@ original directory on Linux/macOS and requires native support for this operation
|
|
|
342
348
|
It offers atomic replace/no-replace publication, not expected-inode replacement
|
|
343
349
|
or a crash-durability promise; application checks and coordination remain yours.
|
|
344
350
|
|
|
351
|
+
For an already staged POSIX symlink, [`retainSymlinkInDirectory()`](docs/staged-symlink.md)
|
|
352
|
+
admits caller-captured identity and retains that exact inode through no-replace
|
|
353
|
+
publication or explicit recovery. Same-target foreign replacements are not adopted.
|
|
354
|
+
Staging, cooperative locking and crash recovery remain application responsibilities.
|
|
355
|
+
|
|
345
356
|
`replaceFileAtomic()` writes a sibling temp file, applies its exact mode through the still-open descriptor, optionally fsyncs it, and renames it over the destination. It never follows the published destination path to set file permissions. Mode preservation inherits only rwx bits from an existing non-symlink regular file; special bits, ownership, ACLs, and extended attributes are not copied. Pinned-destination hardlink rejection, rename retry / copy fallback on `EPERM`, bounded original-content restoration after a torn fallback, parent-directory fsync, and a `beforeRename` hook for backup or observer flows are all opt-in. `movePathWithCopyFallback()` stages cross-device moves before commit and removes only the copied source entries, so concurrent source additions or replacements are preserved. Its optional synchronous `assertBeforeMutation` hook rechecks caller authority before renames and each source removal; `onDestinationPublished` reports an exact bigint destination identity before later checks or cleanup can fail. See [mutation authority and publication receipts](docs/atomic.md#mutation-authority-and-publication-receipts).
|
|
346
357
|
|
|
347
358
|
```ts
|
|
@@ -358,6 +369,12 @@ await replaceFileAtomic({
|
|
|
358
369
|
|
|
359
370
|
`replaceFileAtomicSync()` covers the synchronous case with the same options shape. Both accept an injectable `fileSystem` for tests. Async adapters use `chmod()` on the `FileHandle` returned by their required `open()` operation; custom sync adapters using `mode` or `preserveExistingMode` provide the optional descriptor-bound `fchmodSync` operation.
|
|
360
371
|
|
|
372
|
+
Both variants accept `assertBeforeMutation` for revocable caller authority and
|
|
373
|
+
`onDestinationState` for observed removal, partial-write, and publication facts.
|
|
374
|
+
The observer receives exact bigint identities from retained descriptors, including
|
|
375
|
+
when later completion fails. These facts do not authorize rollback; the caller
|
|
376
|
+
still owns current authority and content checks. See [atomic write authority](docs/atomic.md#atomic-write-authority-and-destination-state).
|
|
377
|
+
|
|
361
378
|
## External outputs
|
|
362
379
|
|
|
363
380
|
Use `writeExternalFileWithinRoot()` when a browser download, renderer, media
|
|
@@ -549,7 +566,8 @@ Check `scan.truncated` before treating the result as complete, and `scan.failedD
|
|
|
549
566
|
`walkDirectory()` accepts asynchronous `include` and `descend` callbacks through `AsyncWalkDirectoryOptions`, so a marker lookup can prune a directory before its children are read. Decisions remain serial and retain the options object as their `this` receiver; `walkDirectorySync()` and its options remain synchronous. See [Directory walking](docs/walk.md) for callback timing, JavaScript result compatibility, and error handling.
|
|
550
567
|
|
|
551
568
|
For caller-controlled paths, `Root.walk()` is the root-bounded async iterator.
|
|
552
|
-
It supports entry/depth budgets,
|
|
569
|
+
It supports entry/depth budgets, including links without following their targets,
|
|
570
|
+
in-root symlink following, cancellation, and a
|
|
553
571
|
truncation marker (or typed error) when a budget is reached. Its `entryFilter`
|
|
554
572
|
accepts `"include"`, `"skip"`, or `"skip-subtree"`, directly or through a Promise.
|
|
555
573
|
After an awaited decision resolves, the walk rechecks cancellation and the
|
package/dist/advanced.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export { resolvePathPrefixSync, type ResolvedPathPrefix } from "./path-prefix.js
|
|
|
8
8
|
export { probePathSuffixAliasesSync, type ProbePathSuffixAliasesOptions } from "./path-suffix-aliases.js";
|
|
9
9
|
export { readDirectoryIdentity, assertDirectoryIdentitySync, type DirectoryIdentity, } from "./directory-guard.js";
|
|
10
10
|
export { stageFileInDirectory, } from "./native-staged-file.js";
|
|
11
|
+
export { retainSymlinkInDirectory } from "./native-staged-symlink.js";
|
|
12
|
+
export type { StagedSymlink, StagedSymlinkExpected, StagedSymlinkReceipt, PublishedSymlinkReceipt, StagedSymlinkPublication, StagedSymlinkRemoval, StagedSymlinkCleanupReceipt, StagedSymlinkFailureDetails, } from "./staged-symlink-types.js";
|
|
11
13
|
export type { StagedFile, StagedFileReceipt, PublishedFileReceipt, StagedFilePublication, StagedFileCleanupReceipt, StagedFileFailureDetails, } from "./staged-file-types.js";
|
|
12
14
|
export { readFileDescriptorBounded, readFileDescriptorBoundedSync, readFileHandleBounded, } from "./bounded-read.js";
|
|
13
15
|
export { readFileWindowFully, readFileWindowFullySync, type ReadFileWindowOptions, } from "./positional-read.js";
|
|
@@ -35,3 +37,5 @@ export { buildRandomTempFilePath, sanitizeTempFileName, type TempFile, tempFile,
|
|
|
35
37
|
export { writeSiblingTempFile, writeViaSiblingTempPath, type WriteSiblingTempFileOptions, type WriteSiblingTempFileResult, } from "./sibling-temp.js";
|
|
36
38
|
export { createIcaclsResetCommand, formatIcaclsResetCommand, formatWindowsAclSummary, inspectWindowsAcl, parseIcaclsOutput, resolveWindowsUserPrincipal, summarizeWindowsAcl, type IcaclsResetCommandOptions, type PermissionExec, type WindowsAclEntry, type WindowsAclSummary, } from "./permissions-windows.js";
|
|
37
39
|
export type { PermissionCommandFailure } from "./permission-exec.js";
|
|
40
|
+
export { retainFileInDirectory } from "./retained-file.js";
|
|
41
|
+
export type { RetainedFile, RetainedFileAdmission, RetainedFileExpected, RetainedFileIssue, RetainedFileReceipt, RetainedFileResult, RetainFileInDirectoryOptions, } from "./retained-file-types.js";
|
package/dist/advanced.js
CHANGED
|
@@ -11,6 +11,7 @@ export { resolvePathPrefixSync } from "./path-prefix.js";
|
|
|
11
11
|
export { probePathSuffixAliasesSync } from "./path-suffix-aliases.js";
|
|
12
12
|
export { readDirectoryIdentity, assertDirectoryIdentitySync, } from "./directory-guard.js";
|
|
13
13
|
export { stageFileInDirectory, } from "./native-staged-file.js";
|
|
14
|
+
export { retainSymlinkInDirectory } from "./native-staged-symlink.js";
|
|
14
15
|
export { readFileDescriptorBounded, readFileDescriptorBoundedSync, readFileHandleBounded, } from "./bounded-read.js";
|
|
15
16
|
export { readFileWindowFully, readFileWindowFullySync, } from "./positional-read.js";
|
|
16
17
|
export { writeFileWindowFully } from "./write-file-handle.js";
|
|
@@ -36,3 +37,4 @@ export { appendRegularFile, appendRegularFileSync, readRegularFile, readRegularF
|
|
|
36
37
|
export { buildRandomTempFilePath, sanitizeTempFileName, tempFile, withTempFile, } from "./temp-target.js";
|
|
37
38
|
export { writeSiblingTempFile, writeViaSiblingTempPath, } from "./sibling-temp.js";
|
|
38
39
|
export { createIcaclsResetCommand, formatIcaclsResetCommand, formatWindowsAclSummary, inspectWindowsAcl, parseIcaclsOutput, resolveWindowsUserPrincipal, summarizeWindowsAcl, } from "./permissions-windows.js";
|
|
40
|
+
export { retainFileInDirectory } from "./retained-file.js";
|
package/dist/archive-plan.d.ts
CHANGED
|
@@ -54,14 +54,9 @@ export type TarEntryInfo = {
|
|
|
54
54
|
mode?: number;
|
|
55
55
|
};
|
|
56
56
|
export declare function createTarEntryPlanner(params: ArchivePlanOptions): (entry: TarEntryInfo) => ArchivePlanEntry | null;
|
|
57
|
-
export declare function createTarEntryPreflightChecker(params: {
|
|
57
|
+
export declare function createTarEntryPreflightChecker(params: Omit<ArchivePlanOptions & {
|
|
58
58
|
rootDir: string;
|
|
59
|
-
|
|
60
|
-
limits?: ArchiveExtractLimits;
|
|
61
|
-
escapeLabel?: string;
|
|
62
|
-
entryFilter?: ArchiveEntryFilter;
|
|
63
|
-
onFiltered?: ArchiveFilteredEntryPolicy;
|
|
64
|
-
}): (entry: TarEntryInfo) => boolean;
|
|
59
|
+
}, "entryModes">): (entry: TarEntryInfo) => boolean;
|
|
65
60
|
export type ArchiveLogger = {
|
|
66
61
|
info?: (message: string) => void;
|
|
67
62
|
warn?: (message: string) => void;
|
package/dist/archive-read.js
CHANGED
|
@@ -11,9 +11,8 @@ import { resolveArchiveKind } from "./archive-kind.js";
|
|
|
11
11
|
import { DEFAULT_MAX_ARCHIVE_BYTES_ZIP, ArchiveLimitError, ARCHIVE_LIMIT_ERROR_CODE, } from "./archive-limits.js";
|
|
12
12
|
import { isGzipBuffer } from "./archive-gzip-tail.js";
|
|
13
13
|
import { inspectTar, replayTar } from "./archive-tar-stream.js";
|
|
14
|
-
import { loadAdmittedZipArchive } from "./archive-zip-loader.js";
|
|
14
|
+
import { loadAdmittedZipArchive, assertZipEntryBinding } from "./archive-zip-loader.js";
|
|
15
15
|
import { createZipIntegrityTransform, normalizeZipIntegrityError, } from "./archive-zip-integrity.js";
|
|
16
|
-
import { isZipSymlinkEntry, zipEntryKind } from "./archive-zip-entry.js";
|
|
17
16
|
import { FsSafeError } from "./errors.js";
|
|
18
17
|
import { inspectFileIdentity } from "./strict-file-identity.js";
|
|
19
18
|
import { resolveReadOpenFlags } from "./read-open-flags.js";
|
|
@@ -93,27 +92,19 @@ async function readArchiveInput(archivePath) {
|
|
|
93
92
|
}
|
|
94
93
|
}
|
|
95
94
|
async function readZipEntry(buffer, entryPath, maxBytes, admitted) {
|
|
96
|
-
const archive = await loadAdmittedZipArchive(buffer, admitted);
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
// effective entries once, after raw ZIP admission has rejected collisions.
|
|
100
|
-
for (const candidate of Object.values(archive.files)) {
|
|
101
|
-
if (canonicalEntryPath(candidate.name) !== entryPath)
|
|
102
|
-
continue;
|
|
103
|
-
if (entry) {
|
|
104
|
-
throw new ArchiveSecurityError("entry-path", `archive contains duplicate entry path: ${formatErrorDetail(entryPath)}`);
|
|
105
|
-
}
|
|
106
|
-
entry = candidate;
|
|
107
|
-
}
|
|
108
|
-
if (!entry || entry.dir) {
|
|
95
|
+
const { archive, entries } = await loadAdmittedZipArchive(buffer, admitted);
|
|
96
|
+
const record = entries.get(entryPath);
|
|
97
|
+
if (!record || record.entry.dir) {
|
|
109
98
|
throw new Error(`archive entry not found: ${formatErrorDetail(entryPath)}`);
|
|
110
99
|
}
|
|
111
|
-
|
|
100
|
+
assertZipEntryBinding(archive, record, entryPath);
|
|
101
|
+
const { entry, kind } = record;
|
|
102
|
+
if (kind === "symlink") {
|
|
112
103
|
throw new Error(`archive entry is a link: ${formatErrorDetail(entryPath)}`);
|
|
113
104
|
}
|
|
114
|
-
if (
|
|
105
|
+
if (kind !== "file")
|
|
115
106
|
throw new Error(`archive entry is not a file: ${formatErrorDetail(entryPath)}`);
|
|
116
|
-
const integrity = createZipIntegrityTransform(
|
|
107
|
+
const integrity = createZipIntegrityTransform(record);
|
|
117
108
|
const stream = typeof entry.nodeStream === "function"
|
|
118
109
|
? entry.nodeStream()
|
|
119
110
|
: Readable.from(await entry.async("nodebuffer"));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type { ArchiveEntryKind } from "./archive-plan.js";
|
|
2
2
|
import type { ZipDirectoryEntry } from "./archive-zip-directory.js";
|
|
3
3
|
export type ZipEntry = {
|
|
4
4
|
name: string;
|
|
@@ -12,13 +12,13 @@ export type ZipEntry = {
|
|
|
12
12
|
nodeStream?: () => NodeJS.ReadableStream;
|
|
13
13
|
async: (type: "nodebuffer") => Promise<Buffer>;
|
|
14
14
|
};
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
export declare function
|
|
15
|
+
export type AdmittedZipEntry = Readonly<{
|
|
16
|
+
entry: ZipEntry;
|
|
17
|
+
name: string;
|
|
18
|
+
kind: ArchiveEntryKind;
|
|
19
|
+
mode: number | undefined;
|
|
20
|
+
size: number;
|
|
21
|
+
crc32: number;
|
|
22
|
+
}>;
|
|
23
|
+
/** Internal: create only after the complete decoder/admission association. */
|
|
24
|
+
export declare function createAdmittedZipEntry(entry: ZipEntry, name: string, physical: ZipDirectoryEntry): AdmittedZipEntry;
|
|
@@ -1,42 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
/** Internal: register only after the complete decoder/admission association. */
|
|
4
|
-
export function registerAdmittedZipEntry(entry, physical) {
|
|
1
|
+
/** Internal: create only after the complete decoder/admission association. */
|
|
2
|
+
export function createAdmittedZipEntry(entry, name, physical) {
|
|
5
3
|
const mode = physical.creatorSystem === 3 ? physical.externalAttributes >>> 16 : undefined;
|
|
6
|
-
admittedMetadata.set(entry, { kind: physical.kind, mode, size: physical.size });
|
|
7
4
|
entry.dir = physical.kind === "directory";
|
|
8
5
|
// Previously unsupported non-UNIX symlinks must also be recognizable to
|
|
9
6
|
// public preflight consumers using JSZip's conventional type inspection.
|
|
10
7
|
entry.unixPermissions = mode ?? (physical.kind === "symlink" ? 0o120000 : null);
|
|
11
8
|
entry.dosPermissions = physical.creatorSystem === 0 ? physical.externalAttributes & 0x3f : null;
|
|
12
|
-
}
|
|
13
|
-
export function zipEntryIntegrityMetadata(entry) {
|
|
14
|
-
const data = entry._data;
|
|
15
|
-
if (!data || "then" in data)
|
|
16
|
-
return undefined;
|
|
17
|
-
return data;
|
|
18
|
-
}
|
|
19
|
-
const ZIP_UNIX_FILE_TYPE_MASK = 0o170000;
|
|
20
|
-
const ZIP_UNIX_SYMLINK_TYPE = 0o120000;
|
|
21
|
-
export function isZipSymlinkEntry(entry) {
|
|
22
|
-
return zipEntryKind(entry) === "symlink";
|
|
23
|
-
}
|
|
24
|
-
export function zipEntryKind(entry) {
|
|
25
|
-
const metadata = admittedMetadata.get(entry);
|
|
26
|
-
if (metadata)
|
|
27
|
-
return metadata.kind;
|
|
28
|
-
return typeof entry.unixPermissions === "number" &&
|
|
29
|
-
(entry.unixPermissions & ZIP_UNIX_FILE_TYPE_MASK) === ZIP_UNIX_SYMLINK_TYPE
|
|
30
|
-
? "symlink" : entry.dir ? "directory" : "file";
|
|
31
|
-
}
|
|
32
|
-
export function zipEntryMode(entry, policy) {
|
|
33
|
-
const metadata = admittedMetadata.get(entry);
|
|
34
|
-
return resolveArchiveEntryMode({
|
|
35
|
-
kind: entry.dir ? "directory" : "file",
|
|
36
|
-
archivedMode: metadata ? metadata.mode : entry.unixPermissions,
|
|
37
|
-
policy,
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
export function zipEntryDeclaredSize(entry) {
|
|
41
|
-
return admittedMetadata.get(entry)?.size ?? Math.max(0, Math.floor(zipEntryIntegrityMetadata(entry)?.uncompressedSize ?? 0));
|
|
9
|
+
return { entry, name, kind: physical.kind, mode, size: physical.size, crc32: physical.crc32 };
|
|
42
10
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { Transform } from "node:stream";
|
|
2
|
-
import {
|
|
2
|
+
import type { AdmittedZipEntry } from "./archive-zip-entry.js";
|
|
3
3
|
export declare function normalizeZipIntegrityError(error: unknown): Error;
|
|
4
|
-
export declare function createZipIntegrityTransform(
|
|
4
|
+
export declare function createZipIntegrityTransform(record: AdmittedZipEntry): Transform;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Transform } from "node:stream";
|
|
2
2
|
import { ArchiveFormatError } from "./archive-errors.js";
|
|
3
|
-
import { zipEntryIntegrityMetadata, } from "./archive-zip-entry.js";
|
|
4
3
|
import { updateCrc32 } from "./archive-crc32.js";
|
|
5
4
|
export function normalizeZipIntegrityError(error) {
|
|
6
5
|
if (error instanceof Error &&
|
|
@@ -9,17 +8,8 @@ export function normalizeZipIntegrityError(error) {
|
|
|
9
8
|
}
|
|
10
9
|
return error instanceof Error ? error : new Error(String(error));
|
|
11
10
|
}
|
|
12
|
-
export function createZipIntegrityTransform(
|
|
13
|
-
const
|
|
14
|
-
const expectedCrc32 = metadata?.crc32;
|
|
15
|
-
const expectedSize = metadata?.uncompressedSize;
|
|
16
|
-
if (typeof expectedCrc32 !== "number" ||
|
|
17
|
-
!Number.isInteger(expectedCrc32) ||
|
|
18
|
-
typeof expectedSize !== "number" ||
|
|
19
|
-
!Number.isSafeInteger(expectedSize) ||
|
|
20
|
-
expectedSize < 0) {
|
|
21
|
-
throw new ArchiveFormatError(`zip entry has invalid integrity metadata: ${entry.name}`);
|
|
22
|
-
}
|
|
11
|
+
export function createZipIntegrityTransform(record) {
|
|
12
|
+
const { entry, crc32: expectedCrc32, size: expectedSize } = record;
|
|
23
13
|
let actualCrc32 = 0;
|
|
24
14
|
let actualSize = 0;
|
|
25
15
|
return new Transform({
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import type { ZipDirectoryEntry } from "./archive-zip-directory.js";
|
|
2
|
-
import { type
|
|
2
|
+
import { type AdmittedZipEntry } from "./archive-zip-entry.js";
|
|
3
3
|
export type ZipArchiveWithFiles = {
|
|
4
4
|
files: Record<string, unknown>;
|
|
5
5
|
};
|
|
6
|
-
export
|
|
6
|
+
export type ZipArchiveAdmission = {
|
|
7
|
+
archive: ZipArchiveWithFiles;
|
|
8
|
+
entries: ReadonlyMap<string, AdmittedZipEntry>;
|
|
9
|
+
};
|
|
10
|
+
export declare function assertZipEntryBinding(archive: ZipArchiveWithFiles, record: AdmittedZipEntry, path: string): void;
|
|
7
11
|
/** Internal: the caller has admitted these unchanged bytes and their metadata. */
|
|
8
|
-
export declare function loadAdmittedZipArchive(buffer: Buffer | Uint8Array, admitted: ZipDirectoryEntry[]): Promise<
|
|
12
|
+
export declare function loadAdmittedZipArchive(buffer: Buffer | Uint8Array, admitted: ZipDirectoryEntry[]): Promise<ZipArchiveAdmission>;
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { ArchiveFormatError, ArchiveSecurityError } from "./archive-errors.js";
|
|
2
2
|
import { validateArchiveEntryPath } from "./archive-entry.js";
|
|
3
|
-
import {
|
|
3
|
+
import { createAdmittedZipEntry } from "./archive-zip-entry.js";
|
|
4
4
|
import { zipPathKey } from "./archive-zip-names.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
export function assertZipEntryBinding(archive, record, path) {
|
|
6
|
+
const { entry } = record;
|
|
7
|
+
const name = entry.name;
|
|
8
|
+
validateArchiveEntryPath(name, { escapeLabel: "archive root" });
|
|
9
|
+
if (archive.files[record.name] !== entry || zipPathKey(name) !== path)
|
|
10
|
+
disagreement();
|
|
8
11
|
}
|
|
9
12
|
function disagreement() {
|
|
10
13
|
throw new ArchiveFormatError("ZIP decoder disagrees with admitted directory metadata");
|
|
@@ -110,7 +113,7 @@ export async function loadAdmittedZipArchive(buffer, admitted) {
|
|
|
110
113
|
if (remaining.size || inserted.size)
|
|
111
114
|
disagreement();
|
|
112
115
|
const files = Object.create(null);
|
|
113
|
-
const entries =
|
|
116
|
+
const entries = new Map();
|
|
114
117
|
for (const [name, entry, physical] of normalized) {
|
|
115
118
|
const appendSlash = physical.kind === "directory" && !name.endsWith("/");
|
|
116
119
|
const normalizedName = appendSlash ? `${name}/` : name;
|
|
@@ -120,12 +123,10 @@ export async function loadAdmittedZipArchive(buffer, admitted) {
|
|
|
120
123
|
entry.name = `${entry.name}/`;
|
|
121
124
|
}
|
|
122
125
|
files[normalizedName] = entry;
|
|
123
|
-
|
|
124
|
-
entries.push(entry);
|
|
126
|
+
entries.set(physical.portableKey, createAdmittedZipEntry(entry, normalizedName, physical));
|
|
125
127
|
}
|
|
126
128
|
archive.files = files;
|
|
127
|
-
|
|
128
|
-
return archive;
|
|
129
|
+
return { archive, entries };
|
|
129
130
|
}
|
|
130
131
|
async function importOptionalJsZip() {
|
|
131
132
|
try {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { type ArchiveExtractLimits } from "./archive-limits.js";
|
|
2
|
-
import { type ZipArchiveWithFiles } from "./archive-zip-loader.js";
|
|
2
|
+
import { type ZipArchiveAdmission, type ZipArchiveWithFiles } from "./archive-zip-loader.js";
|
|
3
|
+
export declare function loadZipArchiveWithAdmission(buffer: Buffer | Uint8Array, limits?: ArchiveExtractLimits): Promise<ZipArchiveAdmission>;
|
|
3
4
|
export declare function loadZipArchiveWithPreflight(buffer: Buffer | Uint8Array, limits?: ArchiveExtractLimits): Promise<ZipArchiveWithFiles>;
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { ARCHIVE_LIMIT_ERROR_CODE, ArchiveLimitError, resolveExtractLimits, } from "./archive-limits.js";
|
|
2
2
|
import { admitZipBuffer } from "./archive-zip-admission.js";
|
|
3
3
|
import { loadAdmittedZipArchive } from "./archive-zip-loader.js";
|
|
4
|
-
export
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
export function loadZipArchiveWithAdmission(buffer, limits) {
|
|
5
|
+
try {
|
|
6
|
+
const resolvedLimits = resolveExtractLimits(limits);
|
|
7
|
+
if (buffer.byteLength > resolvedLimits.maxArchiveBytes) {
|
|
8
|
+
throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ARCHIVE_SIZE_EXCEEDS_LIMIT);
|
|
9
|
+
}
|
|
10
|
+
const entries = [];
|
|
11
|
+
admitZipBuffer(buffer, resolvedLimits, entry => { entries.push(entry); });
|
|
12
|
+
return loadAdmittedZipArchive(buffer, entries);
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
// Keep validation failures in the promise handed to the extraction deadline.
|
|
16
|
+
return Promise.reject(error);
|
|
8
17
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
return await
|
|
18
|
+
}
|
|
19
|
+
export async function loadZipArchiveWithPreflight(buffer, limits) {
|
|
20
|
+
return (await loadZipArchiveWithAdmission(buffer, limits)).archive;
|
|
12
21
|
}
|
package/dist/archive.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createTarEntryPlanner, createArchiveEntrySelector, resolveArchiveFilteredEntryPolicy, createArchiveEntryPlanner, } from "./archive-plan.js";
|
|
1
|
+
import { createTarEntryPlanner, createArchiveEntrySelector, resolveArchiveFilteredEntryPolicy, resolveArchiveEntryMode, createArchiveEntryPlanner, } from "./archive-plan.js";
|
|
2
2
|
import { inspectTar, replayTar } from "./archive-tar-stream.js";
|
|
3
3
|
import { runPinnedWriteHelper } from "./pinned-write.js";
|
|
4
4
|
import { constants as fsConstants } from "node:fs";
|
|
@@ -11,9 +11,7 @@ import { assertArchiveEntryCountWithinLimit, createByteBudgetTracker, createExtr
|
|
|
11
11
|
import { resolveArchiveKind } from "./archive-kind.js";
|
|
12
12
|
import { prepareArchiveDestinationGuard, preparePrivateArchiveOutputPath, } from "./archive-staging.js";
|
|
13
13
|
import { withStagedArchivePublication } from "./archive-merge.js";
|
|
14
|
-
import {
|
|
15
|
-
import { admittedZipEntries } from "./archive-zip-loader.js";
|
|
16
|
-
import { zipEntryKind, zipEntryDeclaredSize, zipEntryMode, } from "./archive-zip-entry.js";
|
|
14
|
+
import { loadZipArchiveWithAdmission } from "./archive-zip-preflight.js";
|
|
17
15
|
import { createZipIntegrityTransform, normalizeZipIntegrityError, } from "./archive-zip-integrity.js";
|
|
18
16
|
import { ArchiveSecurityError, ArchiveFormatError, isArchiveFormatErrorMessage, } from "./archive-errors.js";
|
|
19
17
|
import { stageArchiveFileForExtraction } from "./archive-input.js";
|
|
@@ -51,7 +49,7 @@ async function readZipEntryStream(entry) {
|
|
|
51
49
|
async function writeZipFileEntry(params) {
|
|
52
50
|
params.deadline.check();
|
|
53
51
|
params.budget.startEntry();
|
|
54
|
-
const readable = await readZipEntryStream(params.entry);
|
|
52
|
+
const readable = await readZipEntryStream(params.record.entry);
|
|
55
53
|
const destinationPath = params.outPath;
|
|
56
54
|
let tempHandle = null;
|
|
57
55
|
let handleClosedByStream = false;
|
|
@@ -69,7 +67,7 @@ async function writeZipFileEntry(params) {
|
|
|
69
67
|
handleClosedByStream = true;
|
|
70
68
|
});
|
|
71
69
|
try {
|
|
72
|
-
await pipeline(readable, createExtractBudgetTransform({ onChunkBytes: params.budget.addBytes }), createZipIntegrityTransform(params.
|
|
70
|
+
await pipeline(readable, createExtractBudgetTransform({ onChunkBytes: params.budget.addBytes }), createZipIntegrityTransform(params.record), writable, { signal: params.deadline.signal });
|
|
73
71
|
}
|
|
74
72
|
catch (err) {
|
|
75
73
|
throw normalizeZipIntegrityError(createPipelineTimeoutError(err, params.deadline));
|
|
@@ -103,26 +101,29 @@ async function extractZip(params) {
|
|
|
103
101
|
deadline.check();
|
|
104
102
|
const buffer = await fs.readFile(params.archivePath, { signal: deadline.signal });
|
|
105
103
|
deadline.check();
|
|
106
|
-
const
|
|
104
|
+
const { entries } = await waitForDeadline(loadZipArchiveWithAdmission(buffer, limits), deadline);
|
|
107
105
|
deadline.check();
|
|
108
|
-
|
|
109
|
-
assertArchiveEntryCountWithinLimit(entries.length, limits);
|
|
106
|
+
assertArchiveEntryCountWithinLimit(entries.size, limits);
|
|
110
107
|
const budget = createByteBudgetTracker(limits);
|
|
111
108
|
await withStagedArchivePublication({ ...params, destinationGuard }, async (stagingDir) => {
|
|
112
109
|
const { select } = createArchiveEntrySelector({ ...params, rootDir: stagingDir });
|
|
113
110
|
const acceptedEntries = [];
|
|
114
|
-
for (const
|
|
111
|
+
for (const record of entries.values()) {
|
|
115
112
|
deadline.check();
|
|
116
|
-
const entryKind =
|
|
117
|
-
const relPath = select({ path:
|
|
113
|
+
const { entry, kind: entryKind, size, mode: archivedMode } = record;
|
|
114
|
+
const relPath = select({ path: record.name, kind: entryKind, size });
|
|
118
115
|
if (relPath === null)
|
|
119
116
|
continue;
|
|
120
117
|
if (entryKind === "symlink") {
|
|
121
|
-
throw new ArchiveSecurityError("entry-link", `zip entry is a link: ${
|
|
118
|
+
throw new ArchiveSecurityError("entry-link", `zip entry is a link: ${record.name}`);
|
|
122
119
|
}
|
|
123
120
|
if (entryKind === "other")
|
|
124
121
|
continue;
|
|
125
|
-
const mode =
|
|
122
|
+
const mode = resolveArchiveEntryMode({
|
|
123
|
+
kind: entry.dir ? "directory" : "file",
|
|
124
|
+
archivedMode,
|
|
125
|
+
policy: params.entryModes,
|
|
126
|
+
});
|
|
126
127
|
acceptedEntries.push({ path: relPath, kind: entry.dir ? "directory" : "file", mode });
|
|
127
128
|
const outPath = path.join(stagingDir, relPath);
|
|
128
129
|
await preparePrivateArchiveOutputPath({
|
|
@@ -130,12 +131,12 @@ async function extractZip(params) {
|
|
|
130
131
|
destinationRealDir: stagingDir,
|
|
131
132
|
relPath,
|
|
132
133
|
outPath,
|
|
133
|
-
originalPath:
|
|
134
|
+
originalPath: record.name,
|
|
134
135
|
isDirectory: entry.dir,
|
|
135
136
|
deadline,
|
|
136
137
|
});
|
|
137
138
|
if (!entry.dir) {
|
|
138
|
-
await writeZipFileEntry({
|
|
139
|
+
await writeZipFileEntry({ record, outPath, budget, deadline });
|
|
139
140
|
}
|
|
140
141
|
}
|
|
141
142
|
return acceptedEntries;
|
package/dist/atomic.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { replaceFileAtomic, replaceFileAtomicSync, type ReplaceFileAtomicFileSystem, type ReplaceFileAtomicOptions, type ReplaceFileAtomicResult, type ReplaceFileAtomicSyncFileSystem, type ReplaceFileAtomicSyncOptions, } from "./replace-file.js";
|
|
1
|
+
export { replaceFileAtomic, replaceFileAtomicSync, type ReplaceFileAtomicFileSystem, type ReplaceFileAtomicOptions, type ReplaceFileAtomicResult, type ReplaceFileAtomicDestinationState, type ReplaceFileAtomicSyncFileSystem, type ReplaceFileAtomicSyncOptions, } from "./replace-file.js";
|
|
2
2
|
export type { RenameIdentityPolicy } from "./pinned-write-types.js";
|
|
3
3
|
export type { ReplaceFileAtomicRestoreCleanup, ReplaceFileAtomicRestoreFailureDetails, ReplaceFileCopyFallbackRestorePolicy, ReplaceFileDestinationHardlinkPolicy, } from "./replace-file-copy-fallback.js";
|
|
4
4
|
export { writeTextAtomic, type WriteTextAtomicOptions } from "./text-atomic.js";
|
|
@@ -70,11 +70,6 @@ function snapshotDirectoryMetadata(stat, identity) {
|
|
|
70
70
|
export function directoryReceiptAuthority(receipt) {
|
|
71
71
|
return authorities.get(receipt) ?? snapshotDirectoryReceipt(receipt);
|
|
72
72
|
}
|
|
73
|
-
function rememberReceipt(receipt, authority) {
|
|
74
|
-
authorities.set(receipt, authority);
|
|
75
|
-
identities.set(receipt.identity, authority);
|
|
76
|
-
return receipt;
|
|
77
|
-
}
|
|
78
73
|
export function ownDirectoryReceipt(receipt) {
|
|
79
74
|
return receiptFromAuthority(snapshotDirectoryReceipt(receipt));
|
|
80
75
|
}
|
|
@@ -83,11 +78,14 @@ export function copyRetainedDirectoryReceipt(receipt) {
|
|
|
83
78
|
return receiptFromAuthority(authority);
|
|
84
79
|
}
|
|
85
80
|
function receiptFromAuthority(authority) {
|
|
86
|
-
|
|
81
|
+
const receipt = {
|
|
87
82
|
path: authority.path,
|
|
88
83
|
realPath: authority.realPath,
|
|
89
84
|
identity: Object.assign(Object.create(fs.Stats.prototype), authority.metadata),
|
|
90
|
-
}
|
|
85
|
+
};
|
|
86
|
+
authorities.set(receipt, authority);
|
|
87
|
+
identities.set(receipt.identity, authority);
|
|
88
|
+
return receipt;
|
|
91
89
|
}
|
|
92
90
|
// The caller already admitted the exact observation and its paths.
|
|
93
91
|
export function createDirectoryReceiptFromIdentity(pathname, realPath, exactStat) {
|
package/dist/effective-uid.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
function isValidUid(value) {
|
|
2
|
-
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
3
|
-
}
|
|
4
1
|
function unavailable(cause) {
|
|
5
2
|
return new Error("Effective user identity is unavailable.", {
|
|
6
3
|
...(cause === undefined ? {} : { cause }),
|
|
@@ -18,7 +15,7 @@ export function resolveEffectiveUid() {
|
|
|
18
15
|
catch (cause) {
|
|
19
16
|
throw unavailable(cause);
|
|
20
17
|
}
|
|
21
|
-
if (!
|
|
18
|
+
if (!(typeof uid === "number" && Number.isSafeInteger(uid) && uid >= 0)) {
|
|
22
19
|
throw unavailable();
|
|
23
20
|
}
|
|
24
21
|
return uid;
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
export type FsSafeErrorCode = "already-exists" | "denied-path" | "device-path" | "hardlink" | "
|
|
1
|
+
export type FsSafeErrorCode = (typeof OPERATIONAL_CODE_VALUES)[number] | "already-exists" | "denied-path" | "device-path" | "hardlink" | "invalid-path" | "insecure-permissions" | "not-file" | "not-owned" | "outside-workspace" | "path-alias" | "path-mismatch" | "secret-exists" | "store-reentrant-update" | "symlink" | "too-large";
|
|
2
2
|
export type FsSafeErrorCategory = "policy" | "operational";
|
|
3
3
|
export type FsSafeErrorDetails = Readonly<Record<string, unknown>>;
|
|
4
|
+
declare const OPERATIONAL_CODE_VALUES: readonly ["helper-failed", "helper-unavailable", "not-empty", "not-found", "not-removable", "permission-unverified", "read-failed", "timeout", "unsupported-platform"];
|
|
4
5
|
export declare function categorizeFsSafeError(code: FsSafeErrorCode): FsSafeErrorCategory;
|
|
5
6
|
export declare class FsSafeError extends Error {
|
|
6
7
|
readonly code: FsSafeErrorCode;
|
|
@@ -11,3 +12,4 @@ export declare class FsSafeError extends Error {
|
|
|
11
12
|
details?: FsSafeErrorDetails;
|
|
12
13
|
});
|
|
13
14
|
}
|
|
15
|
+
export {};
|