@openclaw/fs-safe 0.8.1 → 0.8.3
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 +20 -0
- package/README.md +2 -2
- package/dist/advanced.d.ts +2 -2
- package/dist/advanced.d.ts.map +1 -1
- package/dist/advanced.js +2 -2
- package/dist/archive-entry.d.ts.map +1 -1
- package/dist/archive-entry.js +5 -0
- package/dist/archive-gzip-tail.d.ts +18 -0
- package/dist/archive-gzip-tail.d.ts.map +1 -0
- package/dist/archive-gzip-tail.js +74 -0
- package/dist/archive-kind.d.ts +1 -0
- package/dist/archive-kind.d.ts.map +1 -1
- package/dist/archive-kind.js +6 -0
- package/dist/archive-limits.d.ts +0 -1
- package/dist/archive-limits.d.ts.map +1 -1
- package/dist/archive-limits.js +0 -3
- package/dist/archive-native.d.ts.map +1 -1
- package/dist/archive-native.js +5 -8
- package/dist/archive-parser-errors.d.ts +2 -0
- package/dist/archive-parser-errors.d.ts.map +1 -0
- package/dist/archive-parser-errors.js +12 -0
- package/dist/archive-parser.wasm +0 -0
- package/dist/archive-read.d.ts.map +1 -1
- package/dist/archive-read.js +25 -73
- package/dist/archive-tar-stream.d.ts +18 -0
- package/dist/archive-tar-stream.d.ts.map +1 -0
- package/dist/archive-tar-stream.js +95 -0
- package/dist/archive-tar-wasm.d.ts +18 -0
- package/dist/archive-tar-wasm.d.ts.map +1 -0
- package/dist/archive-tar-wasm.js +99 -0
- package/dist/archive-tar.d.ts +0 -1
- package/dist/archive-tar.d.ts.map +1 -1
- package/dist/archive-tar.js +0 -22
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +44 -114
- package/dist/atomic.d.ts +1 -1
- package/dist/atomic.d.ts.map +1 -1
- package/dist/atomic.js +1 -1
- package/dist/bounded-read-stream.d.ts +1 -5
- package/dist/bounded-read-stream.d.ts.map +1 -1
- package/dist/bounded-read-stream.js +5 -13
- package/dist/byte-budget.d.ts +0 -1
- package/dist/byte-budget.d.ts.map +1 -1
- package/dist/byte-budget.js +1 -1
- package/dist/device-path.d.ts +1 -0
- package/dist/device-path.d.ts.map +1 -1
- package/dist/device-path.js +5 -2
- package/dist/directory-mode-owner.d.ts.map +1 -1
- package/dist/directory-mode-owner.js +5 -0
- package/dist/error-detail.d.ts +1 -0
- package/dist/error-detail.d.ts.map +1 -1
- package/dist/error-detail.js +6 -0
- package/dist/file-store-boundary.d.ts +1 -1
- package/dist/file-store-boundary.d.ts.map +1 -1
- package/dist/file-store-boundary.js +2 -13
- package/dist/file-store.js +1 -1
- package/dist/file-sync.d.ts +5 -0
- package/dist/file-sync.d.ts.map +1 -0
- package/dist/file-sync.js +19 -0
- package/dist/guarded-mutation.d.ts +3 -0
- package/dist/guarded-mutation.d.ts.map +1 -1
- package/dist/guarded-mutation.js +5 -1
- package/dist/home-dir.d.ts +0 -10
- package/dist/home-dir.d.ts.map +1 -1
- package/dist/home-dir.js +0 -28
- package/dist/json-durable-queue-ownership.d.ts +1 -0
- package/dist/json-durable-queue-ownership.d.ts.map +1 -1
- package/dist/json-durable-queue-ownership.js +139 -18
- package/dist/json-durable-queue.d.ts.map +1 -1
- package/dist/json-durable-queue.js +5 -10
- package/dist/json-store.d.ts.map +1 -1
- package/dist/json-store.js +1 -1
- package/dist/local-roots.d.ts.map +1 -1
- package/dist/local-roots.js +2 -3
- package/dist/move-path-cleanup.d.ts +1 -1
- package/dist/move-path-cleanup.d.ts.map +1 -1
- package/dist/move-path-cleanup.js +4 -2
- package/dist/move-path.d.ts +9 -0
- package/dist/move-path.d.ts.map +1 -1
- package/dist/move-path.js +53 -16
- package/dist/native-operations.d.ts +0 -1
- package/dist/native-operations.d.ts.map +1 -1
- package/dist/native-operations.js +0 -10
- package/dist/native-pinned-write-windows.d.ts.map +1 -1
- package/dist/native-pinned-write-windows.js +5 -4
- package/dist/native-staged-file.d.ts +8 -1
- package/dist/native-staged-file.d.ts.map +1 -1
- package/dist/native-staged-file.js +30 -9
- package/dist/opened-realpath.d.ts +12 -1
- package/dist/opened-realpath.d.ts.map +1 -1
- package/dist/opened-realpath.js +8 -7
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +26 -2
- package/dist/path-policy.js +1 -1
- package/dist/permissions.d.ts +1 -0
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +3 -0
- package/dist/pinned-write.d.ts +1 -2
- package/dist/pinned-write.d.ts.map +1 -1
- package/dist/pinned-write.js +2 -11
- package/dist/private-temp-workspace.js +1 -1
- package/dist/publish-file.js +2 -2
- package/dist/replace-file-copy-source.d.ts.map +1 -1
- package/dist/replace-file-copy-source.js +2 -7
- package/dist/replace-file-descriptor.d.ts.map +1 -1
- package/dist/replace-file-descriptor.js +5 -17
- package/dist/replace-file-temp-owner.d.ts.map +1 -1
- package/dist/replace-file-temp-owner.js +2 -7
- package/dist/replace-file.d.ts.map +1 -1
- package/dist/replace-file.js +21 -8
- package/dist/root-errors.d.ts +1 -0
- package/dist/root-errors.d.ts.map +1 -1
- package/dist/root-errors.js +9 -0
- package/dist/root-file.d.ts.map +1 -1
- package/dist/root-file.js +17 -28
- package/dist/root-impl.d.ts.map +1 -1
- package/dist/root-impl.js +12 -33
- package/dist/root-path.d.ts.map +1 -1
- package/dist/root-path.js +49 -94
- package/dist/root-write-mode.d.ts +6 -0
- package/dist/root-write-mode.d.ts.map +1 -0
- package/dist/root-write-mode.js +60 -0
- package/dist/root-write-verification.js +2 -2
- package/dist/safe-path-segment.d.ts +1 -0
- package/dist/safe-path-segment.d.ts.map +1 -1
- package/dist/safe-path-segment.js +1 -1
- package/dist/secret-file.d.ts.map +1 -1
- package/dist/secret-file.js +4 -9
- package/dist/secret-read-async.d.ts.map +1 -1
- package/dist/secret-read-async.js +5 -10
- package/dist/secret-read-policy.d.ts +4 -0
- package/dist/secret-read-policy.d.ts.map +1 -1
- package/dist/secret-read-policy.js +12 -0
- package/dist/sibling-staged-file.d.ts.map +1 -1
- package/dist/sibling-staged-file.js +2 -7
- package/dist/sidecar-lock-policy.d.ts +0 -1
- package/dist/sidecar-lock-policy.d.ts.map +1 -1
- package/dist/sidecar-lock-policy.js +0 -4
- package/dist/string-coerce.d.ts +0 -9
- package/dist/string-coerce.d.ts.map +1 -1
- package/dist/string-coerce.js +0 -56
- package/dist/temp-target.d.ts.map +1 -1
- package/dist/temp-target.js +1 -12
- package/docs/archive.md +66 -61
- package/docs/atomic.md +53 -19
- package/docs/contributing.md +35 -1
- package/docs/install.md +1 -1
- package/docs/native-helper.md +5 -0
- package/docs/native.md +16 -10
- package/docs/secret-file.md +5 -0
- package/docs/staged-file.md +6 -2
- package/docs/writing.md +5 -0
- package/package.json +19 -16
- package/dist/archive-tar-admission.d.ts +0 -7
- package/dist/archive-tar-admission.d.ts.map +0 -1
- package/dist/archive-tar-admission.js +0 -43
- package/dist/archive-tar-gnu.d.ts +0 -2
- package/dist/archive-tar-gnu.d.ts.map +0 -1
- package/dist/archive-tar-gnu.js +0 -20
- package/dist/archive-tar-header.d.ts +0 -8
- package/dist/archive-tar-header.d.ts.map +0 -1
- package/dist/archive-tar-header.js +0 -47
- package/dist/archive-tar-meta.d.ts +0 -34
- package/dist/archive-tar-meta.d.ts.map +0 -1
- package/dist/archive-tar-meta.js +0 -277
- package/dist/archive-tar-pax.d.ts +0 -8
- package/dist/archive-tar-pax.d.ts.map +0 -1
- package/dist/archive-tar-pax.js +0 -100
- package/dist/archive-tar-runtime.d.ts +0 -49
- package/dist/archive-tar-runtime.d.ts.map +0 -1
- package/dist/archive-tar-runtime.js +0 -22
- package/dist/fsync.d.ts +0 -2
- package/dist/fsync.d.ts.map +0 -1
- package/dist/fsync.js +0 -1
- package/dist/json-durable-queue-retirement.d.ts +0 -9
- package/dist/json-durable-queue-retirement.d.ts.map +0 -1
- package/dist/json-durable-queue-retirement.js +0 -126
- package/dist/json-durable-queue-transfer-lock.d.ts +0 -2
- package/dist/json-durable-queue-transfer-lock.d.ts.map +0 -1
- package/dist/json-durable-queue-transfer-lock.js +0 -19
- package/dist/mode.d.ts +0 -2
- package/dist/mode.d.ts.map +0 -1
- package/dist/mode.js +0 -3
- package/dist/output-sibling.d.ts +0 -8
- package/dist/output-sibling.d.ts.map +0 -1
- package/dist/output-sibling.js +0 -30
- package/dist/read-error.d.ts +0 -2
- package/dist/read-error.d.ts.map +0 -1
- package/dist/read-error.js +0 -11
- package/dist/short-path.d.ts +0 -2
- package/dist/short-path.d.ts.map +0 -1
- package/dist/short-path.js +0 -7
- package/dist/staged-file.d.ts +0 -10
- package/dist/staged-file.d.ts.map +0 -1
- package/dist/staged-file.js +0 -15
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.3 - 2026-09-06
|
|
4
|
+
|
|
5
|
+
- Add optional synchronous move authority checks before renames and each copied-source removal, plus an exact bigint destination-publication receipt for caller-owned recovery after later failure; retain cross-device and Windows `EPERM` copy fallbacks and the existing `Promise<void>` contract.
|
|
6
|
+
|
|
7
|
+
- Simplify internals without changing public behavior: remove unused string/home helpers, `resolveUserPath`, `createBoundedReadStream`, `sidecarLockPayloadIsStale`, and `tarManifestEntryCost`; share filesystem utilities and merge private modules.
|
|
8
|
+
- Reduce filesystem calls per read and write while retaining boundary, hardlink, hook, retry, and post-mutation identity checks.
|
|
9
|
+
- Skip native publication's extra mode-only file fsync for modes that retain owner read/write when staging permissions have not widened; after a crash, a file may retain staged `0o600` instead of the wider requested mode, while restrictive modes retain the extra fsync.
|
|
10
|
+
- Add 1 MiB read/write and existing-mode inheritance benchmarks, with native-mode metadata and per-case iteration counts.
|
|
11
|
+
|
|
12
|
+
## 0.8.2 - 2026-09-05
|
|
13
|
+
|
|
14
|
+
**Highlights:** TAR/gzip extraction now shares one parser across native and fallback modes, improving compatibility with system-tar output while keeping admission and publication guarded.
|
|
15
|
+
|
|
16
|
+
- Reject reserved archive device names and ignored-space aliases on Windows while preserving ordinary POSIX members, and guard secret reads with the documented `device-path` rejection. Thanks @SebTardif.
|
|
17
|
+
- Unify native and guarded JavaScript TAR admission on one Rust core, bundle its WASM build, and accept strict UTF-8/newline PAX paths without a runtime `tar` dependency; retain bounded framing, raw-field validation, and guarded publication.
|
|
18
|
+
- Accept bounded all-zero gzip container padding from system-tar stdout after validated member trailers, and reject nonzero data hidden after padding on both native and guarded JavaScript TAR routes.
|
|
19
|
+
- Fix `replaceFileAtomic({ dirMode })` rejecting a raw `fs.stat` mode: directory modes are masked to permission bits (`0o7777`) before application and verification, matching chmod semantics; 0.8.0 regressed this input tolerance with `directory final mode could not be verified`.
|
|
20
|
+
- Standardize malformed TAR mode fields on the native zero fallback while preserving ordinary octal, absent, zero, and safe GNU binary modes.
|
|
21
|
+
- Refresh the native binding build tool to `@napi-rs/cli` 3.9.0 and the test/coverage toolchain to Vitest 5.0.0. Thanks @dependabot.
|
|
22
|
+
|
|
3
23
|
## 0.8.1 - 2026-09-04
|
|
4
24
|
|
|
5
25
|
- Add `retainOnExit` to sidecar lock acquisition so deliberately retained ownership records (for example fail-closed build locks) survive natural process exit; default process-exit release behavior is unchanged.
|
package/README.md
CHANGED
|
@@ -57,7 +57,7 @@ This is a **library-level guardrail**, not OS-level isolation. It does not repla
|
|
|
57
57
|
pnpm add @openclaw/fs-safe
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
Node 22 or newer. Core root/path/json/temp helpers avoid framework dependencies. With all optional dependencies omitted, public subpaths remain safe to import and non-archive fallback-capable operations work in `auto` or `off`. Native-only features remain unavailable, and operations needing the binding in `require` mode fail with `helper-unavailable`.
|
|
60
|
+
Node 22 or newer. Core root/path/json/temp helpers avoid framework dependencies. With all optional dependencies omitted, public subpaths remain safe to import and non-archive fallback-capable operations work in `auto` or `off`. Native-only features remain unavailable, and operations needing the binding in `require` mode fail with `helper-unavailable`. TAR/gzip fallback uses the bundled WASM build of the same Rust parser as native and works with optional dependencies omitted. ZIP fallback still needs optional `jszip`. See the [0.6 migration guide](docs/migrating-to-0.6.md).
|
|
61
61
|
|
|
62
62
|
The package installs one prebuilt native binding for the current supported target. It
|
|
63
63
|
supplies fd-relative and atomic no-replace primitives that Node does not expose
|
|
@@ -294,7 +294,7 @@ original directory on Linux/macOS and requires native support for this operation
|
|
|
294
294
|
It offers atomic replace/no-replace publication, not expected-inode replacement
|
|
295
295
|
or a crash-durability promise; application checks and coordination remain yours.
|
|
296
296
|
|
|
297
|
-
`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, 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.
|
|
297
|
+
`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, 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).
|
|
298
298
|
|
|
299
299
|
```ts
|
|
300
300
|
import { replaceFileAtomic } from "@openclaw/fs-safe/atomic";
|
package/dist/advanced.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { createAsyncLock } from "./async-lock.js";
|
|
2
|
-
export { stageFileInDirectory, type StagedFile, type StagedFileReceipt, type PublishedFileReceipt, type StagedFilePublication, type StagedFileCleanupReceipt, type StagedFileFailureDetails, } from "./staged-file.js";
|
|
2
|
+
export { stageFileInDirectory, type StagedFile, type StagedFileReceipt, type PublishedFileReceipt, type StagedFilePublication, type StagedFileCleanupReceipt, type StagedFileFailureDetails, } from "./native-staged-file.js";
|
|
3
3
|
export { readFileDescriptorBounded, readFileDescriptorBoundedSync, readFileHandleBounded, } from "./bounded-read.js";
|
|
4
4
|
export { assertNoUnsafeDeviceReadPath, isUnsafeDeviceReadPath, matchUnsafeDeviceReadPath, type UnsafeDeviceReadPathMatch, type UnsafeDeviceReadPathOptions, type UnsafeDeviceReadPathReason, } from "./device-path.js";
|
|
5
5
|
export { assertAbsolutePathInput, canonicalPathFromExistingAncestor, ensureAbsoluteDirectory, findExistingAncestor, resolveAbsolutePathForRead, resolveAbsolutePathForWrite, type AbsolutePathSymlinkPolicy, type EnsureAbsoluteDirectoryOptions, type EnsureAbsoluteDirectoryResult, type ResolvedAbsolutePath, type ResolvedWritableAbsolutePath, } from "./absolute-path.js";
|
|
@@ -8,7 +8,7 @@ export { sanitizeUntrustedFileName } from "./filename.js";
|
|
|
8
8
|
export { pathExists, pathExistsSync } from "./fs.js";
|
|
9
9
|
export { resolveLocalPathFromRootsSync, readLocalFileFromRoots, type LocalRootsInputOptions, type LocalRootsPathResult, type LocalRootsReadResult, type ReadLocalFileFromRootsOptions, type ResolveLocalPathFromRootsSyncOptions, } from "./local-roots.js";
|
|
10
10
|
export { assertNoWindowsNetworkPath, basenameFromMediaSource, hasEncodedFileUrlSeparator, isWindowsDriveLetterPath, isWindowsNetworkPath, safeFileURLToPath, trySafeFileURLToPath, } from "./local-file-access.js";
|
|
11
|
-
export { formatPosixMode } from "./
|
|
11
|
+
export { formatPosixMode } from "./permissions.js";
|
|
12
12
|
export { configureFsSafeLocks, getFsSafeLockConfig, type FsSafeLockConfig, } from "./lock-config.js";
|
|
13
13
|
export { assertNoHardlinkedFinalPath, assertNoPathAliasEscape, PATH_ALIAS_POLICIES, type PathAliasPolicy, } from "./path-policy.js";
|
|
14
14
|
export { openRootFile, openRootFileSync, canUseRootFileOpen, matchRootFileOpenFailure, type OpenRootFileParams, type OpenRootFileSyncParams, type RootFileOpenFailure, type RootFileOpenFailureReason, type RootFileOpenResult, } from "./root-file.js";
|
package/dist/advanced.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"advanced.d.ts","sourceRoot":"","sources":["../src/advanced.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EACL,oBAAoB,EACpB,KAAK,UAAU,EACf,KAAK,iBAAiB,EACtB,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,wBAAwB,EAC7B,KAAK,wBAAwB,GAC9B,MAAM,
|
|
1
|
+
{"version":3,"file":"advanced.d.ts","sourceRoot":"","sources":["../src/advanced.ts"],"names":[],"mappings":"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"}
|
package/dist/advanced.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// public subpaths; prefer root/json/store/temp/archive unless you are building a
|
|
3
3
|
// higher-level primitive.
|
|
4
4
|
export { createAsyncLock } from "./async-lock.js";
|
|
5
|
-
export { stageFileInDirectory, } from "./staged-file.js";
|
|
5
|
+
export { stageFileInDirectory, } from "./native-staged-file.js";
|
|
6
6
|
export { readFileDescriptorBounded, readFileDescriptorBoundedSync, readFileHandleBounded, } from "./bounded-read.js";
|
|
7
7
|
export { assertNoUnsafeDeviceReadPath, isUnsafeDeviceReadPath, matchUnsafeDeviceReadPath, } from "./device-path.js";
|
|
8
8
|
export { assertAbsolutePathInput, canonicalPathFromExistingAncestor, ensureAbsoluteDirectory, findExistingAncestor, resolveAbsolutePathForRead, resolveAbsolutePathForWrite, } from "./absolute-path.js";
|
|
@@ -11,7 +11,7 @@ export { sanitizeUntrustedFileName } from "./filename.js";
|
|
|
11
11
|
export { pathExists, pathExistsSync } from "./fs.js";
|
|
12
12
|
export { resolveLocalPathFromRootsSync, readLocalFileFromRoots, } from "./local-roots.js";
|
|
13
13
|
export { assertNoWindowsNetworkPath, basenameFromMediaSource, hasEncodedFileUrlSeparator, isWindowsDriveLetterPath, isWindowsNetworkPath, safeFileURLToPath, trySafeFileURLToPath, } from "./local-file-access.js";
|
|
14
|
-
export { formatPosixMode } from "./
|
|
14
|
+
export { formatPosixMode } from "./permissions.js";
|
|
15
15
|
export { configureFsSafeLocks, getFsSafeLockConfig, } from "./lock-config.js";
|
|
16
16
|
export { assertNoHardlinkedFinalPath, assertNoPathAliasEscape, PATH_ALIAS_POLICIES, } from "./path-policy.js";
|
|
17
17
|
export { openRootFile, openRootFileSync, canUseRootFileOpen, matchRootFileOpenFailure, } from "./root-file.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive-entry.d.ts","sourceRoot":"","sources":["../src/archive-entry.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"archive-entry.d.ts","sourceRoot":"","sources":["../src/archive-entry.ts"],"names":[],"mappings":"AAMA,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAIzD;AAED,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,GAChC,IAAI,CAoEN;AAGD,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAc1F;AAED,wBAAgB,8BAA8B,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,IAAI,CAgBlG;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,MAAM,CAWT"}
|
package/dist/archive-entry.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { ArchiveSecurityError } from "./archive-errors.js";
|
|
3
|
+
import { isWindowsReservedDeviceName } from "./device-path.js";
|
|
3
4
|
import { formatErrorDetail } from "./error-detail.js";
|
|
4
5
|
import { resolveSafeBaseDir } from "./path.js";
|
|
5
6
|
export function isWindowsDrivePath(value) {
|
|
@@ -25,6 +26,10 @@ export function validateArchiveEntryPath(entryPath, params) {
|
|
|
25
26
|
slashNormalized.split("/").some((segment) => segment.includes(":"))) {
|
|
26
27
|
throw new ArchiveSecurityError("entry-path", `archive entry uses a Windows alternate data stream path: ${formatErrorDetail(entryPath)}`);
|
|
27
28
|
}
|
|
29
|
+
if (process.platform === "win32" &&
|
|
30
|
+
slashNormalized.split("/").some((segment) => isWindowsReservedDeviceName(segment))) {
|
|
31
|
+
throw new ArchiveSecurityError("entry-path", `archive entry uses a reserved device path: ${formatErrorDetail(entryPath)}`);
|
|
32
|
+
}
|
|
28
33
|
const normalized = path.posix.normalize(slashNormalized);
|
|
29
34
|
if (normalized.split("/").some((segment) => Math.max(Buffer.byteLength(segment.normalize("NFC")), Buffer.byteLength(segment.normalize("NFD"))) > 255)) {
|
|
30
35
|
throw new ArchiveSecurityError("entry-path", `archive entry has an overlong path component: ${formatErrorDetail(entryPath)}`);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Writable } from "node:stream";
|
|
2
|
+
import type { Gunzip } from "node:zlib";
|
|
3
|
+
/** Track physical input, not the sum of bytes consumed across separate writes:
|
|
4
|
+
* gunzip can resume on a later chunk after leaving an earlier padding gap. */
|
|
5
|
+
export declare class GzipInput extends Writable {
|
|
6
|
+
private readonly decoder;
|
|
7
|
+
private position;
|
|
8
|
+
private firstUnused;
|
|
9
|
+
constructor(decoder: Gunzip);
|
|
10
|
+
get tailOffset(): number;
|
|
11
|
+
_write(chunk: Buffer, _encoding: BufferEncoding, callback: (error?: Error | null) => void): void;
|
|
12
|
+
_final(callback: (error?: Error | null) => void): void;
|
|
13
|
+
_destroy(error: Error | null, callback: (error: Error | null) => void): void;
|
|
14
|
+
}
|
|
15
|
+
/** Check the immutable staged suffix from the first unused physical byte,
|
|
16
|
+
* including later chunks the decoder may have consumed, before returning. */
|
|
17
|
+
export declare function validateGzipContainerTail(filePath: string, consumed: number, signal?: AbortSignal): Promise<void>;
|
|
18
|
+
//# sourceMappingURL=archive-gzip-tail.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive-gzip-tail.d.ts","sourceRoot":"","sources":["../src/archive-gzip-tail.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAGxC;8EAC8E;AAC9E,qBAAa,SAAU,SAAQ,QAAQ;IAIzB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAHpC,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,WAAW,CAAqB;IAExC,YAA6B,OAAO,EAAE,MAAM,EAE3C;IAED,IAAI,UAAU,IAAI,MAAM,CAEvB;IAEQ,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,QAAQ,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI,GAAG,IAAI,CAcxG;IAEQ,MAAM,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI,GAAG,IAAI,CAE9D;IAEQ,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI,GAAG,IAAI,CAGpF;CACF;AAED;6EAC6E;AAC7E,wBAAsB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAsBvH"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { Writable } from "node:stream";
|
|
3
|
+
import { ArchiveFormatError } from "./archive-errors.js";
|
|
4
|
+
/** Track physical input, not the sum of bytes consumed across separate writes:
|
|
5
|
+
* gunzip can resume on a later chunk after leaving an earlier padding gap. */
|
|
6
|
+
export class GzipInput extends Writable {
|
|
7
|
+
decoder;
|
|
8
|
+
position = 0;
|
|
9
|
+
firstUnused;
|
|
10
|
+
constructor(decoder) {
|
|
11
|
+
super({ highWaterMark: 65536 });
|
|
12
|
+
this.decoder = decoder;
|
|
13
|
+
}
|
|
14
|
+
get tailOffset() {
|
|
15
|
+
return this.firstUnused ?? this.position;
|
|
16
|
+
}
|
|
17
|
+
_write(chunk, _encoding, callback) {
|
|
18
|
+
const before = this.decoder.bytesWritten;
|
|
19
|
+
this.decoder.write(chunk, (error) => {
|
|
20
|
+
if (error) {
|
|
21
|
+
callback(error);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const used = this.decoder.bytesWritten - before;
|
|
25
|
+
if (!Number.isSafeInteger(used) || used < 0 || used > chunk.length ||
|
|
26
|
+
!Number.isSafeInteger(this.position + chunk.length)) {
|
|
27
|
+
callback(new ArchiveFormatError("invalid gzip consumed-input boundary"));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (used < chunk.length)
|
|
31
|
+
this.firstUnused ??= this.position + used;
|
|
32
|
+
this.position += chunk.length;
|
|
33
|
+
callback();
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
_final(callback) {
|
|
37
|
+
this.decoder.end(callback);
|
|
38
|
+
}
|
|
39
|
+
_destroy(error, callback) {
|
|
40
|
+
if (error || !this.writableFinished)
|
|
41
|
+
this.decoder.destroy(error ?? undefined);
|
|
42
|
+
callback(error);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** Check the immutable staged suffix from the first unused physical byte,
|
|
46
|
+
* including later chunks the decoder may have consumed, before returning. */
|
|
47
|
+
export async function validateGzipContainerTail(filePath, consumed, signal) {
|
|
48
|
+
signal?.throwIfAborted();
|
|
49
|
+
const handle = await fs.open(filePath, "r");
|
|
50
|
+
try {
|
|
51
|
+
const { size } = await handle.stat();
|
|
52
|
+
if (!Number.isSafeInteger(consumed) || consumed <= 0 || consumed > size) {
|
|
53
|
+
throw new ArchiveFormatError("invalid gzip consumed-input boundary");
|
|
54
|
+
}
|
|
55
|
+
if (consumed === size)
|
|
56
|
+
return;
|
|
57
|
+
const buffer = Buffer.allocUnsafe(65536);
|
|
58
|
+
let position = consumed;
|
|
59
|
+
while (position < size) {
|
|
60
|
+
signal?.throwIfAborted();
|
|
61
|
+
const { bytesRead } = await handle.read(buffer, 0, Math.min(buffer.length, size - position), position);
|
|
62
|
+
signal?.throwIfAborted();
|
|
63
|
+
if (bytesRead === 0)
|
|
64
|
+
throw new ArchiveFormatError("truncated gzip container padding");
|
|
65
|
+
if (buffer.subarray(0, bytesRead).some((byte) => byte !== 0)) {
|
|
66
|
+
throw new ArchiveFormatError("nonzero gzip container padding");
|
|
67
|
+
}
|
|
68
|
+
position += bytesRead;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
await handle.close();
|
|
73
|
+
}
|
|
74
|
+
}
|
package/dist/archive-kind.d.ts
CHANGED
|
@@ -4,5 +4,6 @@ type ResolvePackedRootDirOptions = {
|
|
|
4
4
|
rootMarkers?: string[];
|
|
5
5
|
};
|
|
6
6
|
export declare function resolvePackedRootDir(extractDir: string, options?: ResolvePackedRootDirOptions): Promise<string>;
|
|
7
|
+
export declare function assertPortableArchiveKind(kind: ArchiveKind): void;
|
|
7
8
|
export {};
|
|
8
9
|
//# sourceMappingURL=archive-kind.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive-kind.d.ts","sourceRoot":"","sources":["../src/archive-kind.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,WAAW,GAAG,UAAU,GAAG,KAAK,CAAC;AAmBnE,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAcvE;AAED,KAAK,2BAA2B,GAAG;IACjC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB,CAAC;AAkBF,wBAAsB,oBAAoB,CACxC,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,2BAA2B,GACpC,OAAO,CAAC,MAAM,CAAC,CA4BjB"}
|
|
1
|
+
{"version":3,"file":"archive-kind.d.ts","sourceRoot":"","sources":["../src/archive-kind.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,WAAW,GAAG,UAAU,GAAG,KAAK,CAAC;AAmBnE,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAcvE;AAED,KAAK,2BAA2B,GAAG;IACjC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB,CAAC;AAkBF,wBAAsB,oBAAoB,CACxC,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,2BAA2B,GACpC,OAAO,CAAC,MAAM,CAAC,CA4BjB;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAQjE"}
|
package/dist/archive-kind.js
CHANGED
|
@@ -74,3 +74,9 @@ export async function resolvePackedRootDir(extractDir, options) {
|
|
|
74
74
|
}
|
|
75
75
|
return path.join(extractDir, onlyDir);
|
|
76
76
|
}
|
|
77
|
+
export function assertPortableArchiveKind(kind) {
|
|
78
|
+
if (kind === "tar-zstd" || kind === "tar-bzip2") {
|
|
79
|
+
throw new FsSafeError("helper-unavailable", `${kind} archives require the matching optional native platform package; ` +
|
|
80
|
+
"install @openclaw/fs-safe with optional dependencies enabled on a supported platform and use FS_SAFE_NATIVE_MODE=auto or require");
|
|
81
|
+
}
|
|
82
|
+
}
|
package/dist/archive-limits.d.ts
CHANGED
|
@@ -42,7 +42,6 @@ export type TarMeterLimits = Pick<ResolvedArchiveExtractLimits, "maxEntries" | "
|
|
|
42
42
|
maxManifestBytes: number;
|
|
43
43
|
};
|
|
44
44
|
export declare const MAX_TAR_MANIFEST_BYTES: number;
|
|
45
|
-
export declare function tarManifestEntryCost(path: string): number;
|
|
46
45
|
export declare function resolveTarMeterLimits(options?: ArchiveExtractLimits): TarMeterLimits;
|
|
47
46
|
export declare function resolveExtractLimits(limits?: ArchiveExtractLimits): ResolvedArchiveExtractLimits;
|
|
48
47
|
export declare function assertArchiveEntryPathComponentsWithinLimit(entryPath: string, limits: ResolvedArchiveExtractLimits): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive-limits.d.ts","sourceRoot":"","sources":["../src/archive-limits.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,MAAM,MAAM,oBAAoB,GAAG;IACjC;;OAEG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,gEAAgE;IAChE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0EAA0E;IAC1E,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,wEAAwE;IACxE,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC,CAAC;AAEF,eAAO,MAAM,6BAA6B,QAAoB,CAAC;AAC/D,eAAO,MAAM,mBAAmB,QAAS,CAAC;AAC1C,eAAO,MAAM,2BAA2B,QAAoB,CAAC;AAC7D,eAAO,MAAM,uBAAuB,QAAoB,CAAC;AACzD,eAAO,MAAM,4BAA4B,QAAc,CAAC;AACxD,eAAO,MAAM,iCAAiC,MAAM,CAAC;AAErD,eAAO,MAAM,wBAAwB;aACnC,0BAA0B,EAAE,4BAA4B;aACxD,yBAAyB,EAAE,mCAAmC;aAC9D,kCAAkC,EAAE,4CAA4C;aAChF,4BAA4B,EAAE,sCAAsC;aACpE,0BAA0B,EAAE,oCAAoC;aAChE,6BAA6B,EAAE,uCAAuC;aACtE,2BAA2B,EAAE,qCAAqC;aAClE,mCAAmC,EAAE,6CAA6C;CAC1E,CAAC;AAEX,MAAM,MAAM,qBAAqB,GAC/B,CAAC,OAAO,wBAAwB,CAAC,CAAC,MAAM,OAAO,wBAAwB,CAAC,CAAC;AAiB3E,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,QAAQ,CAAC,IAAI,EAAE,qBAAqB,CAAC;IAErC,YAAY,IAAI,EAAE,qBAAqB,EAItC;CACF;AAED,MAAM,MAAM,4BAA4B,GAAG,QAAQ,CAAC,oBAAoB,CAAC,CAAC;AAE1E,MAAM,MAAM,cAAc,GAAG,IAAI,CAAC,4BAA4B,EAC5D,YAAY,GAAG,mBAAmB,CAAC,GAAG;IAAE,eAAe,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9F,eAAO,MAAM,sBAAsB,QAAmB,CAAC;AAEvD,wBAAgB,
|
|
1
|
+
{"version":3,"file":"archive-limits.d.ts","sourceRoot":"","sources":["../src/archive-limits.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAExC,MAAM,MAAM,oBAAoB,GAAG;IACjC;;OAEG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,gEAAgE;IAChE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0EAA0E;IAC1E,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,wEAAwE;IACxE,sBAAsB,CAAC,EAAE,MAAM,CAAC;CACjC,CAAC;AAEF,eAAO,MAAM,6BAA6B,QAAoB,CAAC;AAC/D,eAAO,MAAM,mBAAmB,QAAS,CAAC;AAC1C,eAAO,MAAM,2BAA2B,QAAoB,CAAC;AAC7D,eAAO,MAAM,uBAAuB,QAAoB,CAAC;AACzD,eAAO,MAAM,4BAA4B,QAAc,CAAC;AACxD,eAAO,MAAM,iCAAiC,MAAM,CAAC;AAErD,eAAO,MAAM,wBAAwB;aACnC,0BAA0B,EAAE,4BAA4B;aACxD,yBAAyB,EAAE,mCAAmC;aAC9D,kCAAkC,EAAE,4CAA4C;aAChF,4BAA4B,EAAE,sCAAsC;aACpE,0BAA0B,EAAE,oCAAoC;aAChE,6BAA6B,EAAE,uCAAuC;aACtE,2BAA2B,EAAE,qCAAqC;aAClE,mCAAmC,EAAE,6CAA6C;CAC1E,CAAC;AAEX,MAAM,MAAM,qBAAqB,GAC/B,CAAC,OAAO,wBAAwB,CAAC,CAAC,MAAM,OAAO,wBAAwB,CAAC,CAAC;AAiB3E,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,QAAQ,CAAC,IAAI,EAAE,qBAAqB,CAAC;IAErC,YAAY,IAAI,EAAE,qBAAqB,EAItC;CACF;AAED,MAAM,MAAM,4BAA4B,GAAG,QAAQ,CAAC,oBAAoB,CAAC,CAAC;AAE1E,MAAM,MAAM,cAAc,GAAG,IAAI,CAAC,4BAA4B,EAC5D,YAAY,GAAG,mBAAmB,CAAC,GAAG;IAAE,eAAe,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9F,eAAO,MAAM,sBAAsB,QAAmB,CAAC;AAEvD,wBAAgB,qBAAqB,CAAC,OAAO,CAAC,EAAE,oBAAoB,GAAG,cAAc,CAkBpF;AAUD,wBAAgB,oBAAoB,CAClC,MAAM,CAAC,EAAE,oBAAoB,GAC5B,4BAA4B,CAY9B;AAED,wBAAgB,2CAA2C,CACzD,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,4BAA4B,GACnC,IAAI,CASN;AAED,wBAAgB,kCAAkC,CAChD,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,4BAA4B,QAKrC;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,4BAA4B,GAAG;IAC7E,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC,CAiCA;AAED,wBAAgB,4BAA4B,CAAC,MAAM,EAAE;IACnD,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACvC,GAAG,SAAS,CAYZ"}
|
package/dist/archive-limits.js
CHANGED
|
@@ -34,9 +34,6 @@ export class ArchiveLimitError extends Error {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
export const MAX_TAR_MANIFEST_BYTES = 64 * 1024 * 1024;
|
|
37
|
-
export function tarManifestEntryCost(path) {
|
|
38
|
-
return 64 + 2 * Buffer.byteLength(path, "utf8");
|
|
39
|
-
}
|
|
40
37
|
export function resolveTarMeterLimits(options) {
|
|
41
38
|
const limits = resolveExtractLimits(options);
|
|
42
39
|
const payload = Math.min(limits.maxExtractedBytes, Number.MAX_SAFE_INTEGER);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive-native.d.ts","sourceRoot":"","sources":["../src/archive-native.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAEhE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAML,KAAK,4BAA4B,EACjC,KAAK,cAAc,EACpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAQlE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"archive-native.d.ts","sourceRoot":"","sources":["../src/archive-native.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAEhE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EAML,KAAK,4BAA4B,EACjC,KAAK,cAAc,EACpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAQlE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAuBjD,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,OAAO,EAAE,aAAa,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,WAAW,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,4BAA4B,CAAC;IACrC,SAAS,EAAE,cAAc,CAAC;IAC1B,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,UAAU,CAAC,EAAE,qBAAqB,CAAC,YAAY,CAAC,CAAC;IACjD,WAAW,CAAC,EAAE,qBAAqB,CAAC,aAAa,CAAC,CAAC;IACnD,UAAU,CAAC,EAAE,qBAAqB,CAAC,YAAY,CAAC,CAAC;CAClD,GAAG,OAAO,CAAC,IAAI,CAAC,CAyJhB"}
|
package/dist/archive-native.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { classifyArchiveParserError } from "./archive-parser-errors.js";
|
|
1
2
|
import { constants as fsConstants } from "node:fs";
|
|
2
3
|
import fs from "node:fs/promises";
|
|
3
|
-
import { ArchiveFormatError, ArchiveSecurityError, isArchiveFormatErrorMessage,
|
|
4
|
+
import { ArchiveFormatError, ArchiveSecurityError, isArchiveFormatErrorMessage, } from "./archive-errors.js";
|
|
4
5
|
import { formatErrorDetail } from "./error-detail.js";
|
|
5
6
|
import { createArchiveOutputPathTracker, resolveArchiveOutputPath, stripArchivePath, validateArchiveEntryPath, } from "./archive-entry.js";
|
|
6
7
|
import { stageArchiveFileForExtraction } from "./archive-input.js";
|
|
@@ -18,13 +19,9 @@ function policyKind(kind) {
|
|
|
18
19
|
}
|
|
19
20
|
function throwMappedNativeError(error) {
|
|
20
21
|
if (error instanceof Error) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
for (const code of Object.values(ARCHIVE_LIMIT_ERROR_CODE)) {
|
|
25
|
-
if (error.message.includes(code))
|
|
26
|
-
throw new ArchiveLimitError(code);
|
|
27
|
-
}
|
|
22
|
+
const mapped = classifyArchiveParserError(error.message, { cause: error });
|
|
23
|
+
if (mapped)
|
|
24
|
+
throw mapped;
|
|
28
25
|
if (isArchiveFormatErrorMessage(error.message)) {
|
|
29
26
|
throw new ArchiveFormatError(error.message, { cause: error });
|
|
30
27
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive-parser-errors.d.ts","sourceRoot":"","sources":["../src/archive-parser-errors.ts"],"names":[],"mappings":"AAGA,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,GAAG,KAAK,GAAG,SAAS,CAQrG"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { ArchiveSecurityError, isArchiveTarPathErrorMessage } from "./archive-errors.js";
|
|
2
|
+
import { ARCHIVE_LIMIT_ERROR_CODE, ArchiveLimitError } from "./archive-limits.js";
|
|
3
|
+
export function classifyArchiveParserError(message, options) {
|
|
4
|
+
if (isArchiveTarPathErrorMessage(message)) {
|
|
5
|
+
return new ArchiveSecurityError("entry-path", message, options);
|
|
6
|
+
}
|
|
7
|
+
for (const code of Object.values(ARCHIVE_LIMIT_ERROR_CODE)) {
|
|
8
|
+
if (message.includes(code))
|
|
9
|
+
return new ArchiveLimitError(code);
|
|
10
|
+
}
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
Binary file
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive-read.d.ts","sourceRoot":"","sources":["../src/archive-read.ts"],"names":[],"mappings":"AAcA,OAAO,
|
|
1
|
+
{"version":3,"file":"archive-read.d.ts","sourceRoot":"","sources":["../src/archive-read.ts"],"names":[],"mappings":"AAcA,OAAO,EAAiD,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAkLpG,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,WAAW,CAAA;CAAE,GAChD,OAAO,CAAC,MAAM,CAAC,CA6EjB"}
|
package/dist/archive-read.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
+
import { classifyArchiveParserError } from "./archive-parser-errors.js";
|
|
1
2
|
import fs from "node:fs/promises";
|
|
2
3
|
import { Readable } from "node:stream";
|
|
3
4
|
import { readFileHandleBounded } from "./bounded-read.js";
|
|
4
|
-
import { ArchiveFormatError, ArchiveSecurityError, isArchiveFormatErrorMessage,
|
|
5
|
+
import { ArchiveFormatError, ArchiveSecurityError, isArchiveFormatErrorMessage, } from "./archive-errors.js";
|
|
5
6
|
import { formatErrorDetail } from "./error-detail.js";
|
|
6
7
|
import { stripArchivePath, validateArchiveEntryPath, } from "./archive-entry.js";
|
|
7
|
-
import { resolveArchiveKind } from "./archive-kind.js";
|
|
8
|
-
import { DEFAULT_MAX_ARCHIVE_BYTES_ZIP,
|
|
9
|
-
import {
|
|
10
|
-
import { preflightTarMetadata } from "./archive-tar-meta.js";
|
|
11
|
-
import { importOptionalTar, NODE_TAR_RATIO_DISABLED, normalizeTarParserError } from "./archive-tar-runtime.js";
|
|
8
|
+
import { assertPortableArchiveKind, resolveArchiveKind } from "./archive-kind.js";
|
|
9
|
+
import { DEFAULT_MAX_ARCHIVE_BYTES_ZIP, ArchiveLimitError, ARCHIVE_LIMIT_ERROR_CODE, } from "./archive-limits.js";
|
|
10
|
+
import { inspectTar, replayTar } from "./archive-tar-stream.js";
|
|
12
11
|
import { loadZipArchiveWithPreflight } from "./archive-zip-preflight.js";
|
|
13
12
|
import { createZipIntegrityTransform, normalizeZipIntegrityError, } from "./archive-zip-integrity.js";
|
|
14
13
|
import { FsSafeError } from "./errors.js";
|
|
@@ -135,70 +134,30 @@ async function readZipEntry(buffer, entryPath, maxBytes) {
|
|
|
135
134
|
return await readStreamBounded(stream.pipe(integrity), maxBytes);
|
|
136
135
|
}
|
|
137
136
|
async function readTarEntry(archivePath, entryPath, maxBytes) {
|
|
138
|
-
const tar = await importOptionalTar();
|
|
139
137
|
const seenPaths = new Set();
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
onMember(info) {
|
|
138
|
+
let selected;
|
|
139
|
+
const limits = resolveTarMeterLimits();
|
|
140
|
+
await inspectTar({ archivePath, limits, onMember(info) {
|
|
144
141
|
const normalized = canonicalEntryPath(info.path);
|
|
145
142
|
if (seenPaths.has(normalized)) {
|
|
146
143
|
throw new ArchiveSecurityError("entry-path", `archive contains duplicate entry path: ${formatErrorDetail(normalized)}`);
|
|
147
144
|
}
|
|
148
145
|
seenPaths.add(normalized);
|
|
149
|
-
if (normalized === entryPath
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
});
|
|
154
|
-
let matched;
|
|
155
|
-
let entryError;
|
|
156
|
-
try {
|
|
157
|
-
await tar.t({
|
|
158
|
-
file: archivePath,
|
|
159
|
-
strict: true,
|
|
160
|
-
maxMetaEntrySize: DEFAULT_MAX_META_ENTRY_BYTES,
|
|
161
|
-
maxDecompressionRatio: NODE_TAR_RATIO_DISABLED,
|
|
162
|
-
onReadEntry(entry) {
|
|
163
|
-
const info = readTarEntryInfo(entry);
|
|
164
|
-
let normalized;
|
|
165
|
-
try {
|
|
166
|
-
normalized = canonicalEntryPath(info.path);
|
|
167
|
-
}
|
|
168
|
-
catch (error) {
|
|
169
|
-
// Throws escaping node-tar's callback do not reject its promise.
|
|
170
|
-
entryError ??= error instanceof Error ? error : new Error(String(error));
|
|
171
|
-
entry.resume();
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
if (normalized !== entryPath) {
|
|
175
|
-
entry.resume();
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
if (info.type !== "File" && info.type !== "OldFile" && info.type !== "ContiguousFile") {
|
|
179
|
-
entryError ??= new Error(`archive entry is not a file: ${formatErrorDetail(entryPath)}`);
|
|
180
|
-
entry.resume();
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
if (info.size > maxBytes) {
|
|
184
|
-
entryError ??= new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT);
|
|
185
|
-
entry.resume();
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
matched = readStreamBounded(entry, maxBytes);
|
|
189
|
-
},
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
catch (error) {
|
|
193
|
-
throw normalizeTarParserError(error);
|
|
194
|
-
}
|
|
195
|
-
if (entryError) {
|
|
196
|
-
throw entryError;
|
|
197
|
-
}
|
|
198
|
-
if (!matched) {
|
|
146
|
+
if (normalized === entryPath)
|
|
147
|
+
selected = info;
|
|
148
|
+
} });
|
|
149
|
+
if (!selected)
|
|
199
150
|
throw new Error(`archive entry not found: ${formatErrorDetail(entryPath)}`);
|
|
151
|
+
if (!["File", "OldFile", "ContiguousFile"].includes(selected.type)) {
|
|
152
|
+
throw new Error(`archive entry is not a file: ${formatErrorDetail(entryPath)}`);
|
|
200
153
|
}
|
|
201
|
-
|
|
154
|
+
if (selected.size > maxBytes)
|
|
155
|
+
throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_EXTRACTED_SIZE_EXCEEDS_LIMIT);
|
|
156
|
+
let result;
|
|
157
|
+
await replayTar({ archivePath, limits, members: [selected], async consume(_member, payload) {
|
|
158
|
+
result = await readStreamBounded(payload, maxBytes);
|
|
159
|
+
} });
|
|
160
|
+
return result;
|
|
202
161
|
}
|
|
203
162
|
export async function readArchiveEntry(archivePath, entryPath, options) {
|
|
204
163
|
if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 0) {
|
|
@@ -244,14 +203,10 @@ export async function readArchiveEntry(archivePath, entryPath, options) {
|
|
|
244
203
|
return await native.readArchiveEntryNative(staged.path, kind, rawEntryPath, options.maxBytes, limits, signal);
|
|
245
204
|
}
|
|
246
205
|
catch (error) {
|
|
247
|
-
if (error instanceof Error && isArchiveTarPathErrorMessage(error.message)) {
|
|
248
|
-
throw new ArchiveSecurityError("entry-path", error.message, { cause: error });
|
|
249
|
-
}
|
|
250
206
|
if (error instanceof Error) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
207
|
+
const mapped = classifyArchiveParserError(error.message, { cause: error });
|
|
208
|
+
if (mapped)
|
|
209
|
+
throw mapped;
|
|
255
210
|
}
|
|
256
211
|
if (error instanceof Error && isArchiveFormatErrorMessage(error.message)) {
|
|
257
212
|
throw new ArchiveFormatError(error.message, { cause: error });
|
|
@@ -259,10 +214,7 @@ export async function readArchiveEntry(archivePath, entryPath, options) {
|
|
|
259
214
|
throw error;
|
|
260
215
|
}
|
|
261
216
|
}
|
|
262
|
-
|
|
263
|
-
throw new FsSafeError("helper-unavailable", `${kind} archives require the matching optional native platform package; ` +
|
|
264
|
-
"install @openclaw/fs-safe with optional dependencies enabled on a supported platform and use FS_SAFE_NATIVE_MODE=auto or require");
|
|
265
|
-
}
|
|
217
|
+
assertPortableArchiveKind(kind);
|
|
266
218
|
return kind === "zip"
|
|
267
219
|
? await readZipEntry(staged.buffer, requestedEntry, options.maxBytes)
|
|
268
220
|
: await readTarEntry(staged.path, requestedEntry, options.maxBytes);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { TarMeterLimits } from "./archive-limits.js";
|
|
2
|
+
import { type AdmittedTarMember } from "./archive-tar-wasm.js";
|
|
3
|
+
export declare function inspectTar(params: {
|
|
4
|
+
archivePath: string;
|
|
5
|
+
limits: TarMeterLimits;
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
onMember?: (entry: AdmittedTarMember) => void;
|
|
8
|
+
}): Promise<void>;
|
|
9
|
+
/** Replay in physical order, retaining at most one decoded chunk. Every range
|
|
10
|
+
* comes from complete admission of the immutable staged input. */
|
|
11
|
+
export declare function replayTar<T extends AdmittedTarMember>(params: {
|
|
12
|
+
archivePath: string;
|
|
13
|
+
limits: TarMeterLimits;
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
members: readonly T[];
|
|
16
|
+
consume(member: T, payload: AsyncIterable<Buffer>): Promise<void>;
|
|
17
|
+
}): Promise<void>;
|
|
18
|
+
//# sourceMappingURL=archive-tar-stream.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive-tar-stream.d.ts","sourceRoot":"","sources":["../src/archive-tar-stream.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAmB,KAAK,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AA4ChF,wBAAsB,UAAU,CAAC,MAAM,EAAE;IACvC,WAAW,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,cAAc,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAClE,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;CAC/C,GAAG,OAAO,CAAC,IAAI,CAAC,CAIhB;AAED;kEACkE;AAClE,wBAAsB,SAAS,CAAC,CAAC,SAAS,iBAAiB,EAAE,MAAM,EAAE;IACnE,WAAW,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,cAAc,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAClE,OAAO,EAAE,SAAS,CAAC,EAAE,CAAC;IACtB,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACnE,GAAG,OAAO,CAAC,IAAI,CAAC,CA8BhB"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { Writable } from "node:stream";
|
|
3
|
+
import { pipeline } from "node:stream/promises";
|
|
4
|
+
import { createGunzip } from "node:zlib";
|
|
5
|
+
import { GzipInput, validateGzipContainerTail } from "./archive-gzip-tail.js";
|
|
6
|
+
import { ArchiveFormatError } from "./archive-errors.js";
|
|
7
|
+
import { TarParserStream } from "./archive-tar-wasm.js";
|
|
8
|
+
async function gzipFile(filePath) {
|
|
9
|
+
const handle = await fs.promises.open(filePath, "r");
|
|
10
|
+
try {
|
|
11
|
+
const magic = Buffer.alloc(2);
|
|
12
|
+
const { bytesRead } = await handle.read(magic, 0, 2, 0);
|
|
13
|
+
return bytesRead === 2 && magic[0] === 31 && magic[1] === 139;
|
|
14
|
+
}
|
|
15
|
+
finally {
|
|
16
|
+
await handle.close();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async function withTarStream(params, consume) {
|
|
20
|
+
const gzip = await gzipFile(params.archivePath);
|
|
21
|
+
const parser = new TarParserStream(params.limits, params.onMember);
|
|
22
|
+
const input = fs.createReadStream(params.archivePath, { highWaterMark: 65536 });
|
|
23
|
+
const decoder = gzip ? createGunzip() : undefined;
|
|
24
|
+
const gzipInput = decoder ? new GzipInput(decoder) : undefined;
|
|
25
|
+
const destroy = (error) => {
|
|
26
|
+
input.destroy(error);
|
|
27
|
+
gzipInput?.destroy(error);
|
|
28
|
+
decoder?.destroy(error);
|
|
29
|
+
parser.destroy(error);
|
|
30
|
+
};
|
|
31
|
+
const pumps = decoder && gzipInput
|
|
32
|
+
? [pipeline(decoder, parser, { signal: params.signal }), pipeline(input, gzipInput, { signal: params.signal })]
|
|
33
|
+
: [pipeline(input, parser, { signal: params.signal })];
|
|
34
|
+
// Either pump tears down both routes; join every pump even after the first failure.
|
|
35
|
+
const settled = Promise.all(pumps.map((pump) => pump.then(() => undefined, (cause) => {
|
|
36
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
37
|
+
destroy(error);
|
|
38
|
+
return error;
|
|
39
|
+
}))).then((errors) => errors.find((error) => error !== undefined));
|
|
40
|
+
try {
|
|
41
|
+
const result = await consume(parser);
|
|
42
|
+
const error = await settled;
|
|
43
|
+
if (error)
|
|
44
|
+
throw error;
|
|
45
|
+
if (gzipInput)
|
|
46
|
+
await validateGzipContainerTail(params.archivePath, gzipInput.tailOffset, params.signal);
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
destroy();
|
|
51
|
+
await settled;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export async function inspectTar(params) {
|
|
55
|
+
await withTarStream(params, async (parser) => {
|
|
56
|
+
await pipeline(parser, new Writable({ write(_chunk, _encoding, callback) { callback(); } }));
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/** Replay in physical order, retaining at most one decoded chunk. Every range
|
|
60
|
+
* comes from complete admission of the immutable staged input. */
|
|
61
|
+
export async function replayTar(params) {
|
|
62
|
+
await withTarStream(params, async (parser) => {
|
|
63
|
+
const iterator = parser[Symbol.asyncIterator]();
|
|
64
|
+
let chunk = Buffer.alloc(0);
|
|
65
|
+
let position = 0;
|
|
66
|
+
async function* take(length) {
|
|
67
|
+
while (length > 0) {
|
|
68
|
+
if (!chunk.length) {
|
|
69
|
+
const next = await iterator.next();
|
|
70
|
+
if (next.done)
|
|
71
|
+
throw new ArchiveFormatError("truncated admitted TAR range");
|
|
72
|
+
chunk = next.value;
|
|
73
|
+
}
|
|
74
|
+
const count = Math.min(length, chunk.length);
|
|
75
|
+
const bytes = chunk.subarray(0, count);
|
|
76
|
+
chunk = chunk.subarray(count);
|
|
77
|
+
position += count;
|
|
78
|
+
length -= count;
|
|
79
|
+
yield bytes;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
for (const member of params.members) {
|
|
83
|
+
if (member.offset < position)
|
|
84
|
+
throw new ArchiveFormatError("invalid admitted TAR range order");
|
|
85
|
+
for await (const _ of take(member.offset - position)) { /* Skip admitted gaps. */ }
|
|
86
|
+
const payload = take(member.size);
|
|
87
|
+
await params.consume(member, payload);
|
|
88
|
+
for await (const _ of payload) { /* Directories may carry ignored dump data. */ }
|
|
89
|
+
if (position !== member.offset + member.size)
|
|
90
|
+
throw new ArchiveFormatError("incomplete TAR range consumption");
|
|
91
|
+
}
|
|
92
|
+
// Includes unrequested/skipped members, trailer checks, and physical EOF.
|
|
93
|
+
while (!(await iterator.next()).done) { /* Drain the bounded parser stream. */ }
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Transform, type TransformCallback } from "node:stream";
|
|
2
|
+
import type { TarMeterLimits } from "./archive-limits.js";
|
|
3
|
+
import type { TarEntryInfo } from "./archive-tar.js";
|
|
4
|
+
export type AdmittedTarMember = TarEntryInfo & {
|
|
5
|
+
offset: number;
|
|
6
|
+
};
|
|
7
|
+
/** Backpressure-aware transport only; all TAR semantics live in the Rust core. */
|
|
8
|
+
export declare class TarParserStream extends Transform {
|
|
9
|
+
private readonly onMember?;
|
|
10
|
+
private abi;
|
|
11
|
+
constructor(limits: TarMeterLimits, onMember?: ((entry: AdmittedTarMember) => void) | undefined);
|
|
12
|
+
private bytes;
|
|
13
|
+
private text;
|
|
14
|
+
_transform(chunk: Buffer, _encoding: BufferEncoding, callback: TransformCallback): void;
|
|
15
|
+
_flush(callback: TransformCallback): void;
|
|
16
|
+
_destroy(error: Error | null, callback: (error: Error | null) => void): void;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=archive-tar-wasm.d.ts.map
|