Skip to main content

exarch-cli

Extract, create, list, and verify TAR (plus gz/bz2/xz/zst), ZIP, and 7z archives securely using the `exarch` CLI. Use whenever a task involves unpacking or building archives, especially from untrusted sources — exarch blocks path traversal, symlink/hardlink escapes, zip bombs, and oversized archives by default, and reports rejections as structured errors instead of silently succeeding.

معلومات المصدر

المستودع
bug-ops/exarch
آخر نشاط في المصدر
٢ نوفمبر ٢٠٢٦ في ٢٠:٠٣
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٥
التفرعات
٣

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
exarch-cli
description
Extract, create, list, and verify TAR (plus gz/bz2/xz/zst), ZIP, and 7z archives securely using the `exarch` CLI. Use whenever a task involves unpacking or building archives, especially from untrusted sources — exarch blocks path traversal, symlink/hardlink escapes, zip bombs, and oversized archives by default, and reports rejections as structured errors instead of silently succeeding.
license
MIT OR Apache-2.0
compatibility
Requires the `exarch` binary (cargo install exarch-cli, or build from this workspace with cargo build -p exarch-cli). MSRV 1.98.0 if building from source.
metadata
{"author":"bug-ops","version":"0.7.0"}
# exarch CLI `exarch` is a security-first archive tool. It extracts, creates, lists, and verifies TAR (plain, `.gz`, `.bz2`, `.xz`, `.zst`), ZIP, and 7z archives. Every extraction runs through a validation pipeline that is deny-by-default: path traversal, symlink/hardlink escapes outside the destination directory, zip bombs (excessive compression ratio), oversized files/archives, and unsafe permission bits (setuid/setgid, world-writable) are all rejected unless explicitly allowed. If an extraction fails with a security-related error, treat that as the tool doing its job, not a bug to route around (see "Do not reach for `--allow-*` reflexively" below). ## Install First check whether a Rust toolchain is present: `command -v cargo`. **`cargo` available (recommended):** ```bash cargo install exarch-cli ``` This installs a binary named `exarch` (not `exarch-cli`) and is the fastest path to a working binary since it doesn't require identifying the target triple. Inside this workspace, use the debug build instead: ```bash cargo build -p exarch-cli ./target/debug/exarch --help ``` **`cargo` NOT available:** *Linux / macOS* — run the install script, no toolchain required, checksum-verified: ```bash curl -fsSL https://raw.githubusercontent.com/bug-ops/exarch/main/scripts/install.sh | sh ``` Installs to `~/.local/bin` (override with `EXARCH_INSTALL_DIR`). If the script itself can't run (e.g. a restricted shell), fall back to a manual download: get `exarch-<version>-<target>.tar.gz` plus its `.sha256` from [GitHub Releases](https://github.com/bug-ops/exarch/releases) — `<target>` is `x86_64-unknown-linux-gnu`, `aarch64-unknown-linux-gnu`, `x86_64-apple-darwin`, or `aarch64-apple-darwin` — then verify and extract: ```bash sha256sum -c exarch-<version>-<target>.tar.gz.sha256 # macOS: shasum -a 256 -c tar -xzf exarch-<version>-<target>.tar.gz ``` *Windows* — no install script; download `exarch-<version>-x86_64-pc-windows-msvc.zip` plus its `.sha256` from [GitHub Releases](https://github.com/bug-ops/exarch/releases), then verify and extract in PowerShell: ```powershell (Get-FileHash exarch-<version>-x86_64-pc-windows-msvc.zip -Algorithm SHA256).Hash # compare (case-insensitively) against the hash in the .sha256 file, then: Expand-Archive exarch-<version>-x86_64-pc-windows-msvc.zip -DestinationPath . ``` `CertUtil -hashfile exarch-<version>-x86_64-pc-windows-msvc.zip SHA256` is an equivalent, cmd-only alternative to `Get-FileHash`. ## Global flags Available on every subcommand: | Flag | Effect | |---|---| | `-v, --verbose` | Verbose output. Conflicts with `--quiet`. | | `-q, --quiet` | Suppress non-error output. Conflicts with `--verbose`. | | `-j, --json` | Emit a machine-readable JSON envelope instead of human-readable text (see below). Always prefer this when parsing output programmatically. | ## Subcommands ### `extract <ARCHIVE> [OUTPUT_DIR]` ```bash exarch extract archive.tar.gz ./out --json ``` `OUTPUT_DIR` defaults to the **current directory** if omitted — always pass an explicit output directory rather than relying on the default, so extraction is predictable and easy to clean up. Key flags (all optional): | Flag | Default | Purpose | |---|---|---| | `--force` | off | Overwrite existing files at the destination. A regular file is replaced by a new file (content written to a sibling temp file, then renamed over it, like `mv -f`), so other hardlinks to the old file keep their content (verified live on TAR and ZIP). The destination's parent directory must be writable; a read-only existing file is replaced; owner, ACLs and extended attributes of the old file are not preserved. The new file gets the archive entry's mode, never the old file's (a ZIP entry without Unix attributes gets `0664`, verified live); where no mode is passed at all it is `0666 & ~umask`. | | `--atomic` | off | Extract to a temp dir, rename into place on success, clean up on failure. With `--force` and a pre-existing destination, the swap is a backup-then-replace: the existing destination is renamed aside first, the new content is renamed into its place second, and only once that swap succeeds is the old destination removed — never before, so a failure at any point still leaves either the old or the new content in place under the destination name. On Unix, `--force` also requires read permission on the destination's parent directory (used to pin it by file descriptor for the swap); the destination itself does not need to be readable. With `--force`, a destination that is itself a symlink is now rejected — pass the resolved target path instead. Without `--force`, a symlinked destination isn't rejected as such, but the final rename onto it fails (`ENOTDIR` on Unix, `OutputExists` on Windows), since `rename` doesn't follow a symlink at the target — pass the resolved target path here too. Plain (non-atomic) `extract` is unaffected: it resolves a symlinked destination via `canonicalize()` and writes through it, matching `tar -C`/`unzip -d` (see #533). If a failed `--atomic --force` run leaves temp/backup content behind (e.g. an intermediate parent-path component was redirected mid-extraction), the error message discloses that content's actual location — see #530. | | `--preserve-permissions` | off | Preserve file permissions from the archive (setuid/setgid are still stripped regardless). | | `--max-files` | 10000 | Max number of entries (files, symlinks, hardlinks and directories); extraction fails with `QuotaExceeded` past it. | | `--max-total-size` | 500 MB | Max cumulative extracted size. Accepts `K`/`M`/`G`/`T` suffixes, e.g. `2G`. | | `--max-file-size` | 50 MB | Max size of any single extracted file. Same suffix syntax. | | `--max-compression-ratio` | 100 | Max uncompressed/compressed ratio before an entry is treated as a zip bomb. Applies to ZIP and non-solid 7z; it does **not** apply to solid 7z blocks. With `--allow-solid-archives`, each solid block is checked as a whole against a fixed 1000x limit (`max_solid_compression_ratio`, Rust API only, not configurable from the CLI). An entry that declares data but zero compressed bytes is always a zip bomb with ratio `unbounded` and no ratio limit admits it; its `HINT` says the size metadata is inconsistent rather than suggesting this flag. The ratio of a 7z block counts only the pack streams its coders consume (derived from the coder method IDs, as the stream counts are not public in `sevenz-rust2`); a crafted header that declares a coder with a BCJ2 ID but never decodes it can still add up to three unreferenced pack streams to the last block's total, so that ratio stays an upper bound for such archives (the padding must be real bytes of the file and the other quotas still apply). | | `--max-path-depth` | 32 | Max path component depth allowed per entry. | | `--allowed-extensions <EXT>` | all allowed | Restrict extraction to matching extensions. Repeatable or comma-separated (`txt,pdf`); leading dot optional, case-insensitive. | | `--allow-symlinks` | off | Allow symlinks that resolve within the extraction directory. `extract` and `verify` reject symlink and hardlink targets whose `..` components cannot be resolved against the extracted tree (TAR, ZIP and 7z). `extract` never writes through a symlink that already sits below the destination: by default it fails with `path component is a symlink: <path>` (`SecurityViolation`, `relaxable_by` `symlinks`); with this flag a pre-existing link is accepted only if it resolves to a directory inside the destination; one that leaves the destination fails with `SymlinkEscape`, and a dangling, looping or file-pointing one fails with `Security violation: refusing to create <entry> under the existing symlink at <parent>` (not relaxable, also by default for a looping link). A symlinked destination root is still accepted (#533). Verified live on TAR, ZIP and 7z. | | `--allow-hardlinks` | off | Allow hardlinks that resolve within the extraction directory. | | `--allow-absolute-paths` | off | Allow archive entries with absolute paths. For ZIP the leading root and any drive, UNC, verbatim (`\\?\`) or device (`\\.\`) prefix is stripped, so the entry lands under the destination (`/tmp/x` extracts to `<dest>/tmp/x`). | | `--allow-world-writable` | off | Allow files with world-writable (mode `0o002`) permission bits. | | `--allow-solid-archives` | off | Allow solid 7z archives (see Security model below). | | `--parallel-decode-memory <SIZE\|off>` | 128M | 7z only. Memory for decoding independent LZMA2 runs ahead on worker threads (K, M, G, T suffixes); `off` decodes on one thread; `0` exits 2 ("greater than zero"). | | `--zip-consistency <enforce\|ignore>` | enforce | ZIP only. `enforce` rejects an archive whose structure different ZIP readers interpret differently (see "ZIP structure" under `verify`); `ignore` keeps the central-directory interpretation and raises no structural finding. Relax it only for archives from a producer you trust. | | `--banned-component <COMPONENT>` | see below | Block entries whose path contains this component. **Replaces** the default list — see warning below. Repeatable. | > [!WARNING] > **`--banned-component` REPLACES the default ban list, it does not append to it.** The default > blocks path components `.git`, `.ssh`, `.gnupg`, `.aws`, `.kube`, `.docker`, `.env`. Passing > `--banned-component foo` drops ALL of those defaults and blocks only `foo` — silently letting > `.ssh`, `.aws`, etc. through. If you need one extra banned component, repeat the flag with the > full desired set, not just the addition: `--banned-component .git --banned-component .ssh > --banned-component .gnupg --banned-component .aws --banned-component .kube --banned-component > .docker --banned-component .env --banned-component foo`. Passing an empty value disables all > banning. ### `create <OUTPUT> <SOURCE>...` ```bash exarch create out.tar.gz src/ --json ``` | Flag | Default | Purpose | |---|---|---| | `-f, --force` | off | Overwrite `OUTPUT` if it already exists. | | `-l, --compression-level <1-9>` | format default | Compression level. | | `-x, --exclude <PATTERN>` | none | Glob exclude pattern; repeatable. | | `--strip-prefix <PREFIX>` | none | Strip a path prefix from entry names. | | `--include-hidden` | off | Include dotfiles/hidden entries. | | `--follow-symlinks` | off | Follow symlinks when walking source trees (stores target contents, not the link). | | `--max-file-size <BYTES>` | none | Skip source files larger than this (`K`/`M`/`G`/`T` suffixes). | | `--preserve-permissions[=true\|false]` | `true` | Preserve platform permission bits. Pass `--preserve-permissions=false` for a portable, permission-agnostic archive. | > [!NOTE] > With `--follow-symlinks` off (default), a symlink passed directly as a source (file or > directory) is archived as a link in TAR; ZIP has no symlink entry type, so it is skipped > instead (warning + `files_skipped`). With `--follow-symlinks` on, both TAR and ZIP dereference > the symlink and store the target's content — for a directory symlink, this means walking and > archiving the entire target tree. A ZIP entry that compresses more than 100x (repetitive logs, zero-filled data, a 5 KB run of one character) is refused by default `extract` and `verify` as a zip bomb, while the same content in a `.tar.gz` extracts. `create` of a `.zip` therefore adds a `warnings` entry (text and `data.warnings` in `--json`) with the number of such entries and the smallest ratio that admits them (`ZIP archive has 1 entry over the default extraction ratio limit of 100x; ... at least 218`); extract or verify that archive with `--max-compression-ratio 218`. Only a path create is scanned (the archive is read back once); a library caller that supplies its own writer gets no warning. Verified live: `exarch create r.zip src` for a 5000-byte `B` run prints the warning and exits `0`. `exarch` refuses to *create* 7z archives (7z is extract-only) and refuses to create archives under ZIP-family extensions that carry extra format semantics: `.jar`, `.war`, `.ear`, `.nar`, `.nbm`, `.apk`, `.aab`, `.ipa`, `.appx`, `.msix`, `.whl`, `.vsix`, `.xpi`, `.epub` — use plain `.zip` for those cases. ### `list <ARCHIVE>` ```bash exarch list archive.zip -l -H --json ``` | Flag | Default | Purpose | |---|---|---| | `-l, --long` | off | Detailed per-entry info (type, size, mode, timestamps, link targets). A link whose target was not read shows `(target not read)`, one whose read failed `(target unreadable)`. | | `-H, --human-readable` | off | Human-readable sizes in text mode (ignored in `--json` mode, which always emits raw byte counts). | | `--max-files` | 10000 | Hard limit on entries scanned; listing fails with `QuotaExceeded` past this count, it does not truncate. | | `--max-total-size` | 500 MB | Hard limit on cumulative listed size; listing fails with `QuotaExceeded` past this size. Raise it explicitly for larger archives. | | `--max-file-size` | 50 MB | Hard limit on any single entry's size; listing fails with `QuotaExceeded` (`FileSize`) past this size. Raise it explicitly for archives with large individual files. | | `--allow-solid-archives` | off | Allow listing solid 7z archives (higher memory use). | | `--read-link-targets` | off | Read 7z symlink targets, which decodes the blocks that hold them (in a large solid block, most of the archive). By default `list` decodes nothing for links and `symlink_target` is `null`. TAR and ZIP always show their targets. | | `--zip-consistency <enforce\|ignore>` | enforce | Same as `extract`: under `enforce` a ZIP with ambiguous structure fails with `ZipInconsistency` instead of being listed. | Verified live: a 51 MB tar entry fails `exarch list` with `"quota exceeded: single file size (53477376 > 52428800)"` at the 50 MB default, and succeeds once `--max-file-size 100M` is passed. A TAR entry that is not a file, directory or link (FIFO, character or block device, or a typeflag the format does not define, such as GNU `V`, `M` or `D`) fails `list`, `verify` and `extract` alike with `error.kind` `SecurityViolation` and exit `1`, whatever flags are set (`list` and `verify` used to list such an entry as a file, or call a FIFO `InvalidArchive`). Verified with the debug binary on a TAR holding `ok.txt` and a FIFO: `list` and `verify` give `operation denied by security policy: FIFO entries not supported`; with a `V` entry the message reads `unsupported entry type: __Nonexhaustive(86)`, and `extract` gives the same message with its `HINT` (the list pass no longer prefixes errors with `failed to list archive`).
عرض على GitHub
ملف SKILL.md هذا كبير جدا، لذلك يعرض SkillsMP القسم الاول فقط هنا. عرض على GitHub