threecpio-0.5.1/.cargo_vcs_info.json0000644000000001360000000000100130210ustar { "git": { "sha1": "21545224cb5ec72f8fe99417bd78410e2e8496cf" }, "path_in_vcs": "" }threecpio-0.5.1/.github/workflows/ci.yaml000064400000000000000000000021461046102023000164700ustar 00000000000000--- name: Cargo Build & Test on: # yamllint disable-line rule:truthy push: pull_request: env: CARGO_TERM_COLOR: always # Make sure CI fails on all warnings, including Clippy lints RUSTFLAGS: "-Dwarnings" jobs: build_and_test: name: Rust project - latest runs-on: ubuntu-latest strategy: matrix: toolchain: - stable - beta - nightly steps: - name: Install dependencies run: > sudo apt-get update && sudo apt-get install --no-install-recommends --yes bzip2 lz4 lzop xz-utils zstd - uses: actions/checkout@v4 - run: > rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }} - run: cargo build --verbose - run: cargo test --verbose clippy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run Clippy run: cargo clippy --all-targets --all-features rustfmt: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run rustfmt run: cargo fmt --all --check threecpio-0.5.1/.gitignore000064400000000000000000000000101046102023000135700ustar 00000000000000/target threecpio-0.5.1/Cargo.lock0000644000000010740000000000100107760ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "lexopt" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa0e2a1fcbe2f6be6c42e342259976206b383122fc152e872795338b5a3f3a7" [[package]] name = "libc" version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" [[package]] name = "threecpio" version = "0.5.1" dependencies = [ "lexopt", "libc", ] threecpio-0.5.1/Cargo.toml0000644000000023120000000000100110150ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "threecpio" version = "0.5.1" authors = ["Benjamin Drung "] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "manage initrd cpio archives" homepage = "https://github.com/bdrung/3cpio" readme = "README.md" keywords = [ "archive", "cpio", "initrd", ] categories = [ "command-line-utilities", "compression", "encoding", "filesystem", ] license = "ISC" repository = "https://github.com/bdrung/3cpio" [lib] name = "threecpio" path = "src/lib.rs" [[bin]] name = "3cpio" path = "src/main.rs" [[test]] name = "cli" path = "tests/cli.rs" [dependencies.lexopt] version = "0.3" [dependencies.libc] version = "0.2" threecpio-0.5.1/Cargo.toml.orig000064400000000000000000000007601046102023000145030ustar 00000000000000[package] name = "threecpio" version = "0.5.1" edition = "2021" authors = ["Benjamin Drung "] license = "ISC" homepage = "https://github.com/bdrung/3cpio" repository = "https://github.com/bdrung/3cpio" description = "manage initrd cpio archives" readme = "README.md" categories = ["command-line-utilities", "compression", "encoding", "filesystem"] keywords = ["archive", "cpio", "initrd"] [[bin]] name = "3cpio" path = "src/main.rs" [dependencies] libc = "0.2" lexopt = "0.3" threecpio-0.5.1/LICENSE000075500000000000000000000014161046102023000126230ustar 000000000000003cpio is licensed under ISC: Copyright (C) 2024, Benjamin Drung Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. threecpio-0.5.1/NEWS.md000064400000000000000000000041121046102023000127050ustar 00000000000000This file summarizes the major and interesting changes for each release. For a detailed list of changes, please see the git history. 0.5.1 (2025-04-11) ------------------ ### Fixed * Fix directory traversal vulnerability: Prevent extracting CPIOs outside of the destination directory to prevent directory traversal attacks. This new behaviour is similar to `cpio --no-absolute-filenames`. 0.5.0 (2025-03-30) ------------------ ### What's new * add `--count` parameter 0.4.0 (2025-03-11) ------------------ ### What's new * add support for extracting character devices ### Fixed * print major/minor of character devices in long format 0.3.2 (2024-08-19) ------------------ ### What's new * Support lzma compression ([bug #8](https://github.com/bdrung/3cpio/issues/8)) ### Fixed * Avoid `timespec` struct literal ([LP: #2076903](https://launchpad.net/bugs/2076903)) * Include missing helper program name in error message ([bug #4](https://github.com/bdrung/3cpio/issues/4)) 0.3.1 (2024-08-06) ------------------ ### What's new * Various changes to speed up `3cpio --list --verbose` to make 3cpio faster than bsdcpio in all benchmarks. 0.3.0 (2024-08-03) ------------------ ### What's new * support preserving the owner/group of symlinks * Add `--verbose` mode to `--list` mode. The output will be similar to `cpio --list --verbose` and `ls -l`. ### Fixed * 3cpio: fix setting the directory/file permissions ([bug #5](https://github.com/bdrung/3cpio/issues/5)) 0.2.0 (2024-07-05) ------------------ ### What's new * Add support for extracting (`--extract`) cpio archives. New parameters are `--directory`, `--preserve-permissions`, and `--subdir`. * Add `--verbose` and `--debug` log levels ### Changed * Replace command line argument parser `gumdrop` by `lexopt`, because the latter has no dependencies. * Drop `assert_cmd` and `predicates` dev dependencies. ### Fixed * 3cpio: fix binary name in `--version` output 0.1.0 (2024-04-18) ------------------ Initial release. 3cpio only supports examining (`--examine`) and listing (`--list`) the content of the initramfs cpio. threecpio-0.5.1/README.md000064400000000000000000000136701046102023000130770ustar 000000000000003cpio ===== 3cpio is a tool to manage initramfs cpio files for the Linux kernel. The Linux kernel's [initramfs buffer format](https://www.kernel.org/doc/html/latest/driver-api/early-userspace/buffer-format.html) is based around the `newc` or `crc` cpio formats. Multiple cpio archives can be concatenated and the last archive can be compressed. Different compression algorithms can be used depending on what support was compiled into the Linux kernel. 3cpio is tailored to initramfs cpio files and will not gain support for other cpio formats. As of now, 3cpio supports examining, listing, and extracting the content of the initramfs cpio. **Note**: The Rust crate is named threecpio, because package names are not allowed to start with numbers. Usage examples -------------- List the number of cpio archives that an initramfs file contains: ``` $ 3cpio --count /boot/initrd.img 4 ``` Examine the content of the initramfs cpio on an Ubuntu 24.04 system: ``` $ 3cpio --examine /boot/initrd.img 0 cpio 77312 cpio 7286272 cpio 85523968 zstd ``` This initramfs cpio consists of three uncompressed cpio archives followed by a Zstandard-compressed cpio archive. List the content of the initramfs cpio on an Ubuntu 24.04 system: ``` $ 3cpio --list /boot/initrd.img . kernel kernel/x86 kernel/x86/microcode kernel/x86/microcode/AuthenticAMD.bin kernel kernel/x86 kernel/x86/microcode kernel/x86/microcode/.enuineIntel.align.0123456789abc kernel/x86/microcode/GenuineIntel.bin . usr usr/lib usr/lib/firmware usr/lib/firmware/3com usr/lib/firmware/3com/typhoon.bin.zst [...] ``` The first cpio contains only the AMD microcode. The second cpio contains only the Intel microcode. The third cpio contains firmware files and kernel modules. Extract the content of the initramfs cpio to the `initrd` subdirectory on an Ubuntu 24.04 system: ``` $ 3cpio --extract -C initrd /boot/initrd.img $ ls initrd bin cryptroot init lib lib.usr-is-merged run scripts var conf etc kernel lib64 libx32 sbin usr ``` Benchmark results ----------------- ### Listing the content of the initrd Runtime comparison measured with `time` over five runs on different initramfs cpios: | System | Kernel | Comp. | Size | Files | 3cpio | lsinitramfs | lsinitrd | | ---------------- | ---------------- | -------- | ------ | ----- | ------ | ----------- | -------- | | Ryzen 7 5700G | 6.5.0-27-generic | zstd¹ | 102 MB | 3496 | 0.052s | 14.243s | –³ | | Ryzen 7 5700G VM | 6.8.0-22-generic | zstd¹ | 63 MB | 1934 | 0.042s | 7.239s | –³ | | Ryzen 7 5700G VM | 6.8.0-22-generic | zstd² | 53 MB | 1783 | 0.061s | 0.452s | 0.560s | | RasPi Zero 2W | 6.5.0-1012-raspi | zstd¹ | 24 MB | 1538 | 0.647s | 56.253s | –³ | | RasPi Zero 2W | 6.5.0-1012-raspi | zstd² | 30 MB | 2028 | 1.141s | 2.286s | 6.118s | | RasPi Zero 2W | 6.8.0-1002-raspi | zstd¹ | 51 MB | 2532 | 0.713s | 164.575s | –³ | | RasPi Zero 2W | 6.8.0-1002-raspi | zstd -1² | 47 MB | 2778 | 1.156s | 2.842s | 9.508s | | RasPi Zero 2W | 6.8.0-1002-raspi | xz² | 41 MB | 2778 | 6.922s | 13.451s | 35.184s | **Legend**: 1. generated by initramfs-tools 2. generated by `dracut --force --${compression}`. On Raspberry Pi Zero 2W there is not enough memory for the default `zstd -15`. So using the default from initramfs-tools there: `dracut --force --compress "zstd -1 -q -T0"` 3. lsinitrd only reads the first two cpio archives of the file, but the initramfs consists of four cpios. **Results**: * 3cpio is 87 to 274 times faster than lsinitramfs for images generated by initramfs-tools. * 3cpio is two to eight times faster than lsinitramfs for images generated by dracut. * 3cpio five to nine times faster than lsinitrd for images generated by dracut. Commands used: ``` 3cpio -t /boot/initrd.img-${version} | wc -l time 3cpio -t /boot/initrd.img-${version} > /dev/null time lsinitramfs /boot/initrd.img-${version} > /dev/null time lsinitrd /boot/initrd.img-${version} > /dev/null ``` List the content of single cpio archive that is not compressed (see [doc/Benchmarks.md](doc/Benchmarks.md) for details) on a Raspberry Pi Zero 2W: | Command | Mean [ms] | Min [ms] | Max [ms] | Relative | |:---|---:|---:|---:|---:| | `3cpio -t initrd.img` | 84.3 ± 1.1 | 82.1 | 87.0 | 1.00 | | `bsdcpio -itF initrd.img` | 98.4 ± 0.9 | 96.4 | 101.0 | 1.17 ± 0.02 | | `cpio -t --file initrd.img` | 1321.2 ± 2.8 | 1314.6 | 1327.6 | 15.68 ± 0.20 | | Command | Mean [ms] | Min [ms] | Max [ms] | Relative | |:---|---:|---:|---:|---:| | `3cpio -tv initrd.img` | 109.2 ± 1.1 | 106.9 | 111.7 | 1.00 | | `bsdcpio -itvF initrd.img` | 114.9 ± 1.1 | 112.6 | 117.4 | 1.05 ± 0.01 | | `cpio -tv --file initrd.img` | 1423.0 ± 3.5 | 1417.1 | 1440.6 | 13.03 ± 0.13 | ### Extracting the content of the initrd Benchmarking the time to extraction initrd: | System | Distro | Kernel | Size | Files | 3cpio | unmkinitramfs | | ------------- | -------- | ---------------- | ------ | ----- | ------ | ------------- | | Ryzen 7 5700G | noble | 6.8.0-35-generic | 70 MB | 2097 | 0.107s | 6.698s | | Ryzen 7 5700G | jammy | 6.8.0-35-generic | 112 MB | 3789 | 0.455s | 2.217s | | Ryzen 7 5700G | bookworm | 6.1.0-21-amd64 | 62 MB | 2935 | 0.268s | 1.362s | | RasPi Zero 2W | noble | 6.8.0-1005-raspi | 53 MB | 2534 | 5.075s | 173.847s | Raw measurements can be found in [doc/Benchmarks.md](doc/Benchmarks.md). Naming and alternatives ----------------------- The tool is named 3cpio because it is the third cpio tool besides [GNU cpio](https://www.gnu.org/software/cpio/) and `bsdcpio` provided by [libarchive](https://www.libarchive.org/). 3cpio is also the third tool that can list the content of initramfs cpio archives besides `lsinitramfs` from [initramfs-tools](https://tracker.debian.org/pkg/initramfs-tools) and `lsinitrd` from [dracut](https://github.com/dracut-ng/dracut-ng). threecpio-0.5.1/doc/Benchmarks.md000064400000000000000000000364351046102023000147700ustar 00000000000000Benchmarks ========== This page contains the raw measurements. Raspberry Pi Zero 2W -------------------- Benchmark results on a Raspberry Pi Zero 2W running Ubuntu 24.04 (noble) arm64 on 2024-06-05: ``` $ ls -l /boot/initrd.img* lrwxrwxrwx 1 root root 27 Jun 4 06:50 /boot/initrd.img -> initrd.img-6.8.0-1005-raspi -rw-r--r-- 1 root root 52794656 Jun 4 18:29 /boot/initrd.img-6.8.0-1005-raspi $ 3cpio -t /boot/initrd.img | wc -l 2534 $ hyperfine -p "rm -rf initrd" "3cpio -x /boot/initrd.img -C initrd" "unmkinitramfs /boot/initrd.img initrd" --export-markdown extract.md Benchmark 1: 3cpio -x /boot/initrd.img -C initrd Time (mean ± σ): 5.075 s ± 0.247 s [User: 0.116 s, System: 1.932 s] Range (min … max): 4.631 s … 5.591 s 10 runs Benchmark 2: unmkinitramfs /boot/initrd.img initrd Time (mean ± σ): 173.847 s ± 8.368 s [User: 31.155 s, System: 269.939 s] Range (min … max): 162.180 s … 183.792 s 10 runs Summary 3cpio -x /boot/initrd.img -C initrd ran 34.25 ± 2.34 times faster than unmkinitramfs /boot/initrd.img initrd ``` | Command | Mean [s] | Min [s] | Max [s] | Relative | |:---|---:|---:|---:|---:| | `3cpio -x /boot/initrd.img -C initrd` | 5.075 ± 0.247 | 4.631 | 5.591 | 1.00 | | `unmkinitramfs /boot/initrd.img initrd` | 173.847 ± 8.368 | 162.180 | 183.792 | 34.25 ± 2.34 | ``` $ hyperfine --warmup 1 "3cpio -t /boot/initrd.img" "lsinitramfs /boot/initrd.img" -u second --export-markdown list.md Benchmark 1: 3cpio -t /boot/initrd.img Time (mean ± σ): 0.697 s ± 0.003 s [User: 0.039 s, System: 0.265 s] Range (min … max): 0.692 s … 0.703 s 10 runs Benchmark 2: lsinitramfs /boot/initrd.img Time (mean ± σ): 165.425 s ± 7.986 s [User: 30.696 s, System: 259.767 s] Range (min … max): 154.661 s … 176.996 s 10 runs Summary 3cpio -t /boot/initrd.img ran 237.45 ± 11.51 times faster than lsinitramfs /boot/initrd.img ``` | Command | Mean [s] | Min [s] | Max [s] | Relative | |:---|---:|---:|---:|---:| | `3cpio -t /boot/initrd.img` | 0.697 ± 0.003 | 0.692 | 0.703 | 1.00 | | `lsinitramfs /boot/initrd.img` | 165.425 ± 7.986 | 154.661 | 176.996 | 237.45 ± 11.51 | Benchmark results on a Raspberry Pi Zero 2W running Ubuntu 24.04 (noble) arm64 on 2024-08-06: ``` $ sudo 3cpio -x /boot/initrd.img -C /var/tmp/initrd $ ( cd /var/tmp/initrd && find . | LC_ALL=C sort | sudo cpio --reproducible --quiet -o -H newc ) > initrd.img $ ls -l initrd.img -rw-rw-r-- 1 user user 75868160 Aug 3 02:10 initrd.img $ 3cpio -t initrd.img | wc -l 2529 $ 3cpio -e initrd.img 0 cpio $ hyperfine -N -w 2 -r 100 "3cpio -t initrd.img" "bsdcpio -itF initrd.img" "cpio -t --file initrd.img" --export-markdown list.md Benchmark 1: 3cpio -t initrd.img Time (mean ± σ): 84.3 ms ± 1.1 ms [User: 25.6 ms, System: 57.5 ms] Range (min … max): 82.1 ms … 87.0 ms 100 runs Benchmark 2: bsdcpio -itF initrd.img Time (mean ± σ): 98.4 ms ± 0.9 ms [User: 29.1 ms, System: 67.6 ms] Range (min … max): 96.4 ms … 101.0 ms 100 runs Benchmark 3: cpio -t --file initrd.img Time (mean ± σ): 1.321 s ± 0.003 s [User: 0.277 s, System: 1.039 s] Range (min … max): 1.315 s … 1.328 s 100 runs Summary 3cpio -t initrd.img ran 1.17 ± 0.02 times faster than bsdcpio -itF initrd.img 15.68 ± 0.20 times faster than cpio -t --file initrd.img $ hyperfine -N -w 2 -r 100 "3cpio -tv initrd.img" "bsdcpio -itvF initrd.img" "cpio -tv --file initrd.img" --export-markdown list-verbose.md Benchmark 1: 3cpio -tv initrd.img Time (mean ± σ): 109.2 ms ± 1.1 ms [User: 46.3 ms, System: 61.7 ms] Range (min … max): 106.9 ms … 111.7 ms 100 runs Benchmark 2: bsdcpio -itvF initrd.img Time (mean ± σ): 114.9 ms ± 1.1 ms [User: 44.2 ms, System: 69.0 ms] Range (min … max): 112.6 ms … 117.4 ms 100 runs Benchmark 3: cpio -tv --file initrd.img Time (mean ± σ): 1.423 s ± 0.004 s [User: 0.318 s, System: 1.099 s] Range (min … max): 1.417 s … 1.441 s 100 runs Summary 3cpio -tv initrd.img ran 1.05 ± 0.01 times faster than bsdcpio -itvF initrd.img 13.03 ± 0.13 times faster than cpio -tv --file initrd.img ``` | Command | Mean [ms] | Min [ms] | Max [ms] | Relative | |:---|---:|---:|---:|---:| | `3cpio -t initrd.img` | 84.3 ± 1.1 | 82.1 | 87.0 | 1.00 | | `bsdcpio -itF initrd.img` | 98.4 ± 0.9 | 96.4 | 101.0 | 1.17 ± 0.02 | | `cpio -t --file initrd.img` | 1321.2 ± 2.8 | 1314.6 | 1327.6 | 15.68 ± 0.20 | | Command | Mean [ms] | Min [ms] | Max [ms] | Relative | |:---|---:|---:|---:|---:| | `3cpio -tv initrd.img` | 109.2 ± 1.1 | 106.9 | 111.7 | 1.00 | | `bsdcpio -itvF initrd.img` | 114.9 ± 1.1 | 112.6 | 117.4 | 1.05 ± 0.01 | | `cpio -tv --file initrd.img` | 1423.0 ± 3.5 | 1417.1 | 1440.6 | 13.03 ± 0.13 | AMD Ryzen 7 5700G ----------------- Benchmark results on a desktop machine with an AMD Ryzen 7 5700G running Ubuntu 24.04 (noble) on 2024-06-09. The tests were done in chroots that use overlayfs on tmpfs for writes. ``` $ schroot-wrapper -p initramfs-tools,linux-image-generic,zstd,busybox-initramfs,cryptsetup-initramfs,kbd,lvm2,mdadm,ntfs-3g,plymouth,plymouth-theme-spinner,hyperfine -u root -c noble (noble)root@desktop:~# ls -l /boot/initrd.img* lrwxrwxrwx 1 root root 27 Jun 4 23:37 /boot/initrd.img -> initrd.img-6.8.0-35-generic -rw-r--r-- 1 root root 70220742 Jun 4 23:37 /boot/initrd.img-6.8.0-35-generic (noble)root@desktop:~# 3cpio -t /boot/initrd.img | wc -l 2097 (noble)root@desktop:~# 3cpio -e /boot/initrd.img 0 cpio 77312 cpio 8033792 cpio 51411456 zstd (noble)root@desktop:~# hyperfine -p "rm -rf initrd" "3cpio -x /boot/initrd.img -C initrd" "unmkinitramfs /boot/initrd.img initrd" --export-markdown extract.md Benchmark 1: 3cpio -x /boot/initrd.img -C initrd Time (mean ± σ): 107.2 ms ± 1.0 ms [User: 3.8 ms, System: 91.9 ms] Range (min … max): 105.8 ms … 110.2 ms 27 runs Benchmark 2: unmkinitramfs /boot/initrd.img initrd Time (mean ± σ): 6.698 s ± 0.026 s [User: 5.106 s, System: 5.639 s] Range (min … max): 6.648 s … 6.724 s 10 runs Summary 3cpio -x /boot/initrd.img -C initrd ran 62.48 ± 0.62 times faster than unmkinitramfs /boot/initrd.img initrd ``` | Command | Mean [ms] | Min [ms] | Max [ms] | Relative | |:---|---:|---:|---:|---:| | `3cpio -x /boot/initrd.img -C initrd` | 107.2 ± 1.0 | 105.8 | 110.2 | 1.00 | | `unmkinitramfs /boot/initrd.img initrd` | 6697.5 ± 25.6 | 6647.8 | 6723.6 | 62.48 ± 0.62 | ``` (noble)root@desktop:~# hyperfine --warmup 1 "3cpio -t /boot/initrd.img" "lsinitramfs /boot/initrd.img" --export-markdown list.md Benchmark 1: 3cpio -t /boot/initrd.img Time (mean ± σ): 42.9 ms ± 0.5 ms [User: 1.7 ms, System: 13.9 ms] Range (min … max): 42.0 ms … 43.9 ms 68 runs Benchmark 2: lsinitramfs /boot/initrd.img Time (mean ± σ): 6.471 s ± 0.041 s [User: 5.054 s, System: 5.323 s] Range (min … max): 6.408 s … 6.536 s 10 runs Summary 3cpio -t /boot/initrd.img ran 150.68 ± 1.88 times faster than lsinitramfs /boot/initrd.img ``` | Command | Mean [ms] | Min [ms] | Max [ms] | Relative | |:---|---:|---:|---:|---:| | `3cpio -t /boot/initrd.img` | 42.9 ± 0.5 | 42.0 | 43.9 | 1.00 | | `lsinitramfs /boot/initrd.img` | 6471.0 ± 41.0 | 6408.1 | 6536.3 | 150.68 ± 1.88 | ``` $ schroot-wrapper -p initramfs-tools,linux-image-generic,zstd,busybox-initramfs,cryptsetup-initramfs,kbd,lvm2,mdadm,ntfs-3g,plymouth,plymouth-theme-spinner,hyperfine -u root -c jammy (jammy)root@desktop:~# ls -l /boot/initrd.img* lrwxrwxrwx 1 root root 29 Jun 4 23:49 /boot/initrd.img -> initrd.img-5.15.0-107-generic -rw-r--r-- 1 root root 112100650 Jun 4 23:50 /boot/initrd.img-5.15.0-107-generic (jammy)root@desktop:~# 3cpio -t /boot/initrd.img | wc -l 3789 (jammy)root@desktop:~# 3cpio -e /boot/initrd.img 0 cpio 77312 cpio 8033792 zstd (jammy)root@desktop:~# hyperfine -p "rm -rf initrd" "3cpio -x /boot/initrd.img -C initrd" "unmkinitramfs /boot/initrd.img initrd" --export-markdown extract.md Benchmark 1: 3cpio -x /boot/initrd.img -C initrd Time (mean ± σ): 455.1 ms ± 3.6 ms [User: 10.7 ms, System: 263.4 ms] Range (min … max): 451.5 ms … 464.5 ms 10 runs Benchmark 2: unmkinitramfs /boot/initrd.img initrd Time (mean ± σ): 2.217 s ± 0.008 s [User: 0.878 s, System: 2.264 s] Range (min … max): 2.198 s … 2.227 s 10 runs Summary '3cpio -x /boot/initrd.img -C initrd' ran 4.87 ± 0.04 times faster than 'unmkinitramfs /boot/initrd.img initrd' ``` | Command | Mean [ms] | Min [ms] | Max [ms] | Relative | |:---|---:|---:|---:|---:| | `3cpio -x /boot/initrd.img -C initrd` | 455.1 ± 3.6 | 451.5 | 464.5 | 1.00 | | `unmkinitramfs /boot/initrd.img initrd` | 2216.5 ± 8.3 | 2198.2 | 2227.3 | 4.87 ± 0.04 | ``` (jammy)root@desktop:~# hyperfine --warmup 1 "3cpio -t /boot/initrd.img" "lsinitramfs /boot/initrd.img" --export-markdown list.md Benchmark 1: 3cpio -t /boot/initrd.img Time (mean ± σ): 336.0 ms ± 6.3 ms [User: 5.5 ms, System: 77.8 ms] Range (min … max): 326.5 ms … 345.0 ms 10 runs Benchmark 2: lsinitramfs /boot/initrd.img Time (mean ± σ): 1.374 s ± 0.010 s [User: 0.725 s, System: 1.050 s] Range (min … max): 1.354 s … 1.393 s 10 runs Summary '3cpio -t /boot/initrd.img' ran 4.09 ± 0.08 times faster than 'lsinitramfs /boot/initrd.img' ``` | Command | Mean [ms] | Min [ms] | Max [ms] | Relative | |:---|---:|---:|---:|---:| | `3cpio -t /boot/initrd.img` | 336.0 ± 6.3 | 326.5 | 345.0 | 1.00 | | `lsinitramfs /boot/initrd.img` | 1374.3 ± 10.3 | 1354.0 | 1392.9 | 4.09 ± 0.08 | ``` $ schroot-wrapper -p initramfs-tools,linux-image-generic,firmware-linux,zstd,cryptsetup-initramfs,lvm2,kbd,mdadm,ntfs-3g,plymouth,console-setup,hyperfine -u root -c bookworm (bookworm)root@desktop:~# ( cd /boot && ln -s initrd.img-* initrd.img ) (bookworm)root@desktop:~# ls -l /boot/initrd.img* lrwxrwxrwx 1 root root 25 Jun 9 15:55 /boot/initrd.img -> initrd.img-6.1.0-21-amd64 -rw-r--r-- 1 root root 62448197 Jun 9 15:53 /boot/initrd.img-6.1.0-21-amd64 (bookworm)root@desktop:~# 3cpio -t /boot/initrd.img | wc -l 2935 (bookworm)root@desktop:~# 3cpio -e /boot/initrd.img 0 zstd (bookworm)root@desktop:~# hyperfine -p "rm -rf initrd" "3cpio -x /boot/initrd.img -C initrd" "unmkinitramfs /boot/initrd.img-6.1.0-21-amd64 initrd" --export-markdown extract.md Benchmark 1: 3cpio -x /boot/initrd.img -C initrd Time (mean ± σ): 267.5 ms ± 2.4 ms [User: 7.6 ms, System: 209.0 ms] Range (min … max): 264.8 ms … 273.2 ms 10 runs Benchmark 2: unmkinitramfs /boot/initrd.img-6.1.0-21-amd64 initrd Time (mean ± σ): 1.362 s ± 0.004 s [User: 0.681 s, System: 1.513 s] Range (min … max): 1.355 s … 1.368 s 10 runs Summary '3cpio -x /boot/initrd.img -C initrd' ran 5.09 ± 0.05 times faster than 'unmkinitramfs /boot/initrd.img-6.1.0-21-amd64 initrd' ``` | Command | Mean [ms] | Min [ms] | Max [ms] | Relative | |:---|---:|---:|---:|---:| | `3cpio -x /boot/initrd.img -C initrd` | 267.5 ± 2.4 | 264.8 | 273.2 | 1.00 | | `unmkinitramfs /boot/initrd.img-6.1.0-21-amd64 initrd` | 1361.7 ± 4.4 | 1354.6 | 1368.4 | 5.09 ± 0.05 | ``` (bookworm)root@desktop:~# hyperfine --warmup 1 "3cpio -t /boot/initrd.img" "lsinitramfs /boot/initrd.img-6.1.0-21-amd64" --export-markdown list.md Benchmark 1: 3cpio -t /boot/initrd.img Time (mean ± σ): 210.0 ms ± 2.3 ms [User: 4.8 ms, System: 66.2 ms] Range (min … max): 207.1 ms … 214.7 ms 14 runs Benchmark 2: lsinitramfs /boot/initrd.img-6.1.0-21-amd64 Time (mean ± σ): 571.8 ms ± 1.9 ms [User: 515.7 ms, System: 496.8 ms] Range (min … max): 568.7 ms … 574.5 ms 10 runs Summary '3cpio -t /boot/initrd.img' ran 2.72 ± 0.03 times faster than 'lsinitramfs /boot/initrd.img-6.1.0-21-amd64' ``` | Command | Mean [ms] | Min [ms] | Max [ms] | Relative | |:---|---:|---:|---:|---:| | `3cpio -t /boot/initrd.img` | 210.0 ± 2.3 | 207.1 | 214.7 | 1.00 | | `lsinitramfs /boot/initrd.img-6.1.0-21-amd64` | 571.8 ± 1.9 | 568.7 | 574.5 | 2.72 ± 0.03 | Benchmark results on a desktop machine with an AMD Ryzen 7 5700G running Ubuntu 24.04 (noble) on 2024-08-06. The tests were done in chroots that use overlayfs on tmpfs for writes: ``` $ schroot-wrapper -p initramfs-tools,linux-image-generic,firmware-linux,zstd,cryptsetup-initramfs,lvm2,kbd,mdadm,ntfs-3g,plymouth,console-setup,libarchive-tools,hyperfine -u root -c bookworm (bookworm)root@desktop:~# mv /boot/initrd.img-6.1.0-23-amd64{,.zstd} (bookworm)root@desktop:~# zstd --rm -d /boot/initrd.img-6.1.0-23-amd64.zstd (bookworm)root@desktop:~# ( cd /boot && ln -s initrd.img-* initrd.img ) (bookworm)root@desktop:~# ls -l /boot/initrd.img* lrwxrwxrwx 1 root root 25 Aug 6 01:57 /boot/initrd.img -> initrd.img-6.1.0-23-amd64 -rw-r--r-- 1 root root 282020864 Aug 6 01:56 /boot/initrd.img-6.1.0-23-amd64 (bookworm)root@desktop:~# 3cpio -t /boot/initrd.img | wc -l 2935 (bookworm)root@desktop:~# 3cpio -e /boot/initrd.img 0 cpio (bookworm)root@desktop:~# hyperfine -N -w 2 -r 100 "3cpio -t /boot/initrd.img" "bsdcpio -itF /boot/initrd.img" "cpio -t --file /boot/initrd.img" --export-markdown list.md Benchmark 1: 3cpio -t /boot/initrd.img Time (mean ± σ): 7.1 ms ± 0.1 ms [User: 1.4 ms, System: 5.6 ms] Range (min … max): 6.9 ms … 7.4 ms 100 runs Benchmark 2: bsdcpio -itF /boot/initrd.img Time (mean ± σ): 12.2 ms ± 0.3 ms [User: 2.4 ms, System: 9.7 ms] Range (min … max): 11.4 ms … 13.0 ms 100 runs Benchmark 3: cpio -t --file /boot/initrd.img Time (mean ± σ): 370.8 ms ± 2.7 ms [User: 41.7 ms, System: 329.0 ms] Range (min … max): 366.7 ms … 381.3 ms 100 runs Summary '3cpio -t /boot/initrd.img' ran 1.70 ± 0.05 times faster than 'bsdcpio -itF /boot/initrd.img' 51.96 ± 0.82 times faster than 'cpio -t --file /boot/initrd.img' (bookworm)root@desktop:~# hyperfine -N -w 2 -r 100 "3cpio -tv /boot/initrd.img" "bsdcpio -itvF /boot/initrd.img" "cpio -tv --file /boot/initrd.img" --export-markdown list-verbose.md Benchmark 1: 3cpio -tv /boot/initrd.img Time (mean ± σ): 9.1 ms ± 0.1 ms [User: 2.9 ms, System: 6.2 ms] Range (min … max): 8.8 ms … 9.5 ms 100 runs Benchmark 2: bsdcpio -itvF /boot/initrd.img Time (mean ± σ): 13.5 ms ± 0.4 ms [User: 4.1 ms, System: 9.3 ms] Range (min … max): 12.7 ms … 14.9 ms 100 runs Benchmark 3: cpio -tv --file /boot/initrd.img Time (mean ± σ): 383.3 ms ± 2.2 ms [User: 45.1 ms, System: 338.1 ms] Range (min … max): 379.6 ms … 390.0 ms 100 runs Summary '3cpio -tv /boot/initrd.img' ran 1.48 ± 0.05 times faster than 'bsdcpio -itvF /boot/initrd.img' 42.14 ± 0.58 times faster than 'cpio -tv --file /boot/initrd.img' ``` | Command | Mean [ms] | Min [ms] | Max [ms] | Relative | |:---|---:|---:|---:|---:| | `3cpio -t /boot/initrd.img` | 7.2 ± 0.1 | 6.9 | 7.5 | 1.00 | | `bsdcpio -itF /boot/initrd.img` | 12.6 ± 0.6 | 11.3 | 14.0 | 1.77 ± 0.09 | | `cpio -t --file /boot/initrd.img` | 375.1 ± 4.8 | 368.2 | 390.6 | 52.45 ± 1.00 | | Command | Mean [ms] | Min [ms] | Max [ms] | Relative | |:---|---:|---:|---:|---:| | `3cpio -tv /boot/initrd.img` | 9.1 ± 0.1 | 8.8 | 9.5 | 1.00 | | `bsdcpio -itvF /boot/initrd.img` | 13.5 ± 0.4 | 12.7 | 14.9 | 1.48 ± 0.05 | | `cpio -tv --file /boot/initrd.img` | 383.3 ± 2.2 | 379.6 | 390.0 | 42.14 ± 0.58 | threecpio-0.5.1/src/header.rs000064400000000000000000000250421046102023000142010ustar 00000000000000// Copyright (C) 2024, Benjamin Drung // SPDX-License-Identifier: ISC use std::fs::Permissions; use std::io::{Error, ErrorKind, Read, Result}; use std::os::unix::fs::PermissionsExt; use crate::seek_forward::SeekForward; use crate::{align_to_4_bytes, SeenFiles}; const CPIO_HEADER_LENGTH: u32 = 110; const CPIO_MAGIC_NUMBER: [u8; 6] = *b"070701"; const MODE_PERMISSION_MASK: u32 = 0o007_777; pub const MODE_FILETYPE_MASK: u32 = 0o770_000; pub const FILETYPE_FIFO: u32 = 0o010_000; pub const FILETYPE_CHARACTER_DEVICE: u32 = 0o020_000; pub const FILETYPE_DIRECTORY: u32 = 0o040_000; pub const FILETYPE_BLOCK_DEVICE: u32 = 0o060_000; pub const FILETYPE_REGULAR_FILE: u32 = 0o100_000; pub const FILETYPE_SYMLINK: u32 = 0o120_000; pub const FILETYPE_SOCKET: u32 = 0o140_000; #[derive(Debug, PartialEq)] pub struct Header { pub ino: u32, pub mode: u32, pub uid: u32, pub gid: u32, pub nlink: u32, pub mtime: u32, pub filesize: u32, major: u32, minor: u32, pub rmajor: u32, pub rminor: u32, pub filename: String, } impl Header { #![allow(clippy::too_many_arguments)] #[cfg(test)] pub fn new( ino: u32, mode: u32, uid: u32, gid: u32, nlink: u32, mtime: u32, filesize: u32, filename: S, ) -> Self where S: Into, { Self { ino, mode, uid, gid, nlink, mtime, filesize, major: 0, minor: 0, rmajor: 0, rminor: 0, filename: filename.into(), } } // Return major and minor combined as u64 fn dev(&self) -> u64 { (u64::from(self.major) << 32) | u64::from(self.minor) } pub fn is_root_directory(&self) -> bool { self.filename == "." && self.mode & MODE_FILETYPE_MASK == FILETYPE_DIRECTORY } pub fn mode_perm(&self) -> u32 { self.mode & MODE_PERMISSION_MASK } // ls-style ASCII representation of the mode pub fn mode_string(&self) -> [u8; 10] { [ match self.mode & MODE_FILETYPE_MASK { FILETYPE_FIFO => b'p', FILETYPE_CHARACTER_DEVICE => b'c', FILETYPE_DIRECTORY => b'd', FILETYPE_BLOCK_DEVICE => b'b', FILETYPE_REGULAR_FILE => b'-', FILETYPE_SYMLINK => b'l', FILETYPE_SOCKET => b's', _ => b'?', }, if self.mode & 0o400 != 0 { b'r' } else { b'-' }, if self.mode & 0o200 != 0 { b'w' } else { b'-' }, match self.mode & 0o4100 { 0o4100 => b's', // set-uid and executable by owner 0o4000 => b'S', // set-uid but not executable by owner 0o0100 => b'x', _ => b'-', }, if self.mode & 0o040 != 0 { b'r' } else { b'-' }, if self.mode & 0o020 != 0 { b'w' } else { b'-' }, match self.mode & 0o2010 { 0o2010 => b's', // set-gid and executable by group 0o2000 => b'S', // set-gid but not executable by group 0o0010 => b'x', _ => b'-', }, if self.mode & 0o004 != 0 { b'r' } else { b'-' }, if self.mode & 0o002 != 0 { b'w' } else { b'-' }, match self.mode & 0o1001 { 0o1001 => b't', // sticky and executable by others 0o1000 => b'T', // sticky but not executable by others 0o0001 => b'x', _ => b'-', }, ] } pub fn permission(&self) -> Permissions { PermissionsExt::from_mode(self.mode & MODE_PERMISSION_MASK) } fn ino_and_dev(&self) -> u128 { (u128::from(self.ino) << 64) | u128::from(self.dev()) } pub fn mark_seen(&self, seen_files: &mut SeenFiles) { seen_files.insert(self.ino_and_dev(), self.filename.clone()); } pub fn read(file: &mut R) -> Result { let mut buffer = [0; CPIO_HEADER_LENGTH as usize]; file.read_exact(&mut buffer)?; check_begins_with_cpio_magic_header(&buffer)?; let namesize = hex_str_to_u32(&buffer[94..102])?; let filename = read_filename(file, namesize)?; Ok(Self { ino: hex_str_to_u32(&buffer[6..14])?, mode: hex_str_to_u32(&buffer[14..22])?, uid: hex_str_to_u32(&buffer[22..30])?, gid: hex_str_to_u32(&buffer[30..38])?, nlink: hex_str_to_u32(&buffer[38..46])?, mtime: hex_str_to_u32(&buffer[46..54])?, filesize: hex_str_to_u32(&buffer[54..62])?, major: hex_str_to_u32(&buffer[62..70])?, minor: hex_str_to_u32(&buffer[70..78])?, rmajor: hex_str_to_u32(&buffer[78..86])?, rminor: hex_str_to_u32(&buffer[86..94])?, filename, }) } pub fn read_only_filesize_and_filename(file: &mut R) -> Result<(u32, String)> { let mut header = [0; CPIO_HEADER_LENGTH as usize]; file.read_exact(&mut header)?; check_begins_with_cpio_magic_header(&header)?; let filesize = hex_str_to_u32(&header[54..62])?; let namesize = hex_str_to_u32(&header[94..102])?; let filename = read_filename(file, namesize)?; Ok((filesize, filename)) } pub fn read_symlink_target(&self, file: &mut R) -> Result { let align = align_to_4_bytes(self.filesize); let mut target_bytes = vec![0u8; (self.filesize + align).try_into().unwrap()]; file.read_exact(&mut target_bytes)?; target_bytes.truncate(self.filesize.try_into().unwrap()); // TODO: propper name reading handling let target = std::str::from_utf8(&target_bytes).unwrap(); Ok(target.into()) } pub fn skip_file_content(&self, file: &mut R) -> Result<()> { if self.filesize == 0 { return Ok(()); }; let skip = self.filesize + align_to_4_bytes(self.filesize); file.seek_forward(skip.into())?; Ok(()) } pub fn try_get_hard_link_target<'a>(&self, seen_files: &'a SeenFiles) -> Option<&'a String> { if self.nlink <= 1 { return None; } seen_files.get(&self.ino_and_dev()) } } fn check_begins_with_cpio_magic_header(header: &[u8]) -> std::io::Result<()> { if header[0..6] != CPIO_MAGIC_NUMBER { return Err(Error::new( ErrorKind::InvalidData, format!( "Invalid CPIO magic number '{}'. Expected {}", &header[0..6].escape_ascii(), std::str::from_utf8(&CPIO_MAGIC_NUMBER).unwrap(), ), )); } Ok(()) } fn hex_str_to_u32(bytes: &[u8]) -> Result { let s = match std::str::from_utf8(bytes) { Err(_) => { return Err(Error::new( ErrorKind::InvalidData, format!("Invalid hexadecimal value '{}'", bytes.escape_ascii()), )) } Ok(value) => value, }; match u32::from_str_radix(s, 16) { Err(_) => Err(Error::new( ErrorKind::InvalidData, format!("Invalid hexadecimal value '{}'", s), )), Ok(value) => Ok(value), } } fn read_filename(file: &mut R, namesize: u32) -> Result { let header_align = align_to_4_bytes(CPIO_HEADER_LENGTH + namesize); let mut filename_bytes = vec![0u8; (namesize + header_align).try_into().unwrap()]; let filename_length: usize = (namesize - 1).try_into().unwrap(); file.read_exact(&mut filename_bytes)?; if filename_bytes[filename_length] != 0 { return Err(Error::new( ErrorKind::InvalidData, format!( "Entry name '{:?}' is not NULL-terminated", &filename_bytes[0..filename_length] ), )); } filename_bytes.truncate(filename_length); // TODO: propper name reading handling let filename = std::str::from_utf8(&filename_bytes).unwrap(); Ok(filename.to_string()) } #[cfg(test)] mod tests { use super::*; #[test] fn test_header_read() { // Wrapped before mtime and filename let cpio_data = b"07070100000002000081B4000003E8000007D000000001\ 661BE5C600000008000000000000000000000000000000000000000A00000000\ path/file\0content\0"; let header = Header::read(&mut cpio_data.as_ref()).unwrap(); assert_eq!( header, Header { ino: 2, mode: 0o100664, uid: 1000, gid: 2000, nlink: 1, mtime: 1713104326, filesize: 8, major: 0, minor: 0, rmajor: 0, rminor: 0, filename: "path/file".into() } ) } #[test] fn test_header_read_invalid_magic_number() { let invalid_data = b"abc\tefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\ abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; let got = Header::read(&mut invalid_data.as_ref()).unwrap_err(); assert_eq!(got.kind(), ErrorKind::InvalidData); assert_eq!( got.to_string(), "Invalid CPIO magic number 'abc\\tef'. Expected 070701" ); } #[test] fn test_hex_str_to_u32() { let value = hex_str_to_u32(b"000003E8").unwrap(); assert_eq!(value, 1000); } #[test] fn test_hex_str_to_u32_invalid_hex() { let got = hex_str_to_u32(b"something").unwrap_err(); assert_eq!(got.kind(), ErrorKind::InvalidData); assert_eq!(got.to_string(), "Invalid hexadecimal value 'something'"); } #[test] fn test_hex_str_to_u32_invalid_utf8() { let got = hex_str_to_u32(b"no\xc3\x28utf8").unwrap_err(); assert_eq!(got.kind(), ErrorKind::InvalidData); assert_eq!(got.to_string(), "Invalid hexadecimal value 'no\\xc3(utf8'"); } #[test] fn test_is_root_directory() { let header = Header::new(0, 0o040_755, 0, 0, 1, 1744150584, 0, "."); assert!(header.is_root_directory()); } #[test] fn test_is_root_directory_not_root_path() { let header = Header::new(0, 0o040_755, 0, 0, 1, 1744150584, 0, "path"); assert!(!header.is_root_directory()); } #[test] fn test_is_root_directory_is_file() { let header = Header::new(0, 0o100_644, 0, 0, 1, 1744150584, 0, "."); assert!(!header.is_root_directory()); } } threecpio-0.5.1/src/lib.rs000064400000000000000000001072761046102023000135310ustar 00000000000000// Copyright (C) 2024, Benjamin Drung // SPDX-License-Identifier: ISC use std::collections::{BTreeMap, HashMap}; use std::fs::{ create_dir, hard_link, remove_file, set_permissions, symlink_metadata, File, OpenOptions, }; use std::io::prelude::*; use std::io::Error; use std::io::ErrorKind; use std::io::Result; use std::io::SeekFrom; use std::os::unix::fs::{chown, fchown, lchown, symlink}; use std::path::{Path, PathBuf}; use std::process::ChildStdout; use std::process::Command; use std::process::Stdio; use std::time::SystemTime; use crate::header::*; use crate::libc::{mknod, set_modified, strftime_local}; use crate::seek_forward::SeekForward; mod header; mod libc; mod seek_forward; pub const LOG_LEVEL_WARNING: u32 = 5; pub const LOG_LEVEL_INFO: u32 = 7; pub const LOG_LEVEL_DEBUG: u32 = 8; struct CpioFilenameReader<'a, R: Read + SeekForward> { file: &'a mut R, } impl Iterator for CpioFilenameReader<'_, R> { type Item = Result; fn next(&mut self) -> Option { match read_filename_from_next_cpio_object(self.file) { Ok(filename) => { if filename == "TRAILER!!!" { None } else { Some(Ok(filename)) } } x => Some(x), } } } struct UserGroupCache { user_cache: HashMap>, group_cache: HashMap>, } impl UserGroupCache { fn new() -> Self { Self { user_cache: HashMap::new(), group_cache: HashMap::new(), } } /// Translate user ID (UID) to user name and cache result. fn get_user(&mut self, uid: u32) -> Result> { match self.user_cache.get(&uid) { Some(name) => Ok(name.clone()), None => { let name = libc::getpwuid_name(uid)?; self.user_cache.insert(uid, name.clone()); Ok(name) } } } /// Translate group ID (GID) to group name and cache result. fn get_group(&mut self, gid: u32) -> Result> { match self.group_cache.get(&gid) { Some(name) => Ok(name.clone()), None => { let name = libc::getgrgid_name(gid)?; self.group_cache.insert(gid, name.clone()); Ok(name) } } } } /// Format the time in a similar way to coreutils' ls command. fn format_time(timestamp: u32, now: i64) -> Result { // Logic from coreutils ls command: // Consider a time to be recent if it is within the past six months. // A Gregorian year has 365.2425 * 24 * 60 * 60 == 31556952 seconds // on the average. let recent = now - i64::from(timestamp) <= 15778476; if recent { strftime_local(b"%b %e %H:%M\0", timestamp) } else { strftime_local(b"%b %e %Y\0", timestamp) } } // TODO: Document hardlink structure type SeenFiles = HashMap; struct Extractor { seen_files: SeenFiles, mtimes: BTreeMap, } impl Extractor { fn new() -> Extractor { Extractor { seen_files: SeenFiles::new(), mtimes: BTreeMap::new(), } } fn set_modified_times(&self, log_level: u32) -> Result<()> { for (path, mtime) in self.mtimes.iter().rev() { if log_level >= LOG_LEVEL_DEBUG { writeln!(std::io::stderr(), "set mtime {} for '{}'", mtime, path)?; }; set_modified(path, *mtime)?; } Ok(()) } } fn align_to_4_bytes(length: u32) -> u32 { let unaligned = length % 4; if unaligned == 0 { 0 } else { 4 - unaligned } } /// Read only the file name from the next cpio object. /// /// Read the next cpio object header, check the magic, skip the file data. /// Return the file name. fn read_filename_from_next_cpio_object(file: &mut R) -> Result { let (filesize, filename) = Header::read_only_filesize_and_filename(file)?; let skip = filesize + align_to_4_bytes(filesize); file.seek_forward(skip.into())?; Ok(filename) } fn read_magic_header(file: &mut R) -> Option> { let mut buffer = [0; 4]; while buffer == [0, 0, 0, 0] { match file.read_exact(&mut buffer) { Ok(()) => {} Err(e) => match e.kind() { ErrorKind::UnexpectedEof => return None, _ => return Some(Err(e)), }, }; } let command = match buffer { [0x42, 0x5A, 0x68, _] => { let mut cmd = Command::new("bzip2"); cmd.arg("-cd"); cmd } [0x30, 0x37, 0x30, 0x37] => Command::new("cpio"), [0x1F, 0x8B, _, _] => { let mut cmd = Command::new("gzip"); cmd.arg("-cd"); cmd } // Different magic numbers (little endian) for lz4: // v0.1-v0.9: 0x184C2102 // v1.0-v1.3: 0x184C2103 // v1.4+: 0x184D2204 [0x02, 0x21, 0x4C, 0x18] | [0x03, 0x21, 0x4C, 0x18] | [0x04, 0x22, 0x4D, 0x18] => { let mut cmd = Command::new("lz4"); cmd.arg("-cd"); cmd } [0x5D, _, _, _] => { let mut cmd = Command::new("lzma"); cmd.arg("-cd"); cmd } // Full magic number for lzop: [0x89, 0x4C, 0x5A, 0x4F, 0x00, 0x0D, 0x0A, 0x1A, 0x0A] [0x89, 0x4C, 0x5A, 0x4F] => { let mut cmd = Command::new("lzop"); cmd.arg("-cd"); cmd } // Full magic number for xz: [0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00] [0xFD, 0x37, 0x7A, 0x58] => { let mut cmd = Command::new("xz"); cmd.arg("-cd"); cmd } [0x28, 0xB5, 0x2F, 0xFD] => { let mut cmd = Command::new("zstd"); cmd.arg("-cdq"); cmd } _ => { return Some(Err(Error::new( ErrorKind::InvalidData, format!( "Failed to determine CPIO or compression magic number: 0x{:02x}{:02x}{:02x}{:02x} (big endian)", buffer[0], buffer[1], buffer[2], buffer[3] ), ))); } }; match file.seek(SeekFrom::Current(-4)) { Ok(_) => {} Err(e) => { return Some(Err(e)); } }; Some(Ok(command)) } fn decompress(command: &mut Command, file: File) -> Result { // TODO: Propper error message if spawn fails let cmd = command .stdin(file) .stdout(Stdio::piped()) .spawn() .map_err(|e| match e.kind() { ErrorKind::NotFound => Error::other(format!( "Program '{}' not found in PATH.", command.get_program().to_str().unwrap() )), _ => e, })?; // TODO: Should unwrap be replaced by returning Result? Ok(cmd.stdout.unwrap()) } fn read_cpio_and_print_filenames( file: &mut R, out: &mut W, ) -> Result<()> { let cpio = CpioFilenameReader { file }; for f in cpio { let filename = f?; writeln!(out, "{}", filename)?; } Ok(()) } fn read_cpio_and_print_long_format( file: &mut R, out: &mut W, now: i64, user_group_cache: &mut UserGroupCache, ) -> Result<()> { // Files can have the same mtime (especially when using SOURCE_DATE_EPOCH). // Cache the time string of the last mtime. let mut last_mtime = 0; let mut time_string: String = "".into(); loop { let header = match Header::read(file) { Ok(header) => { if header.filename == "TRAILER!!!" { break; } else { header } } Err(e) => return Err(e), }; let user = match user_group_cache.get_user(header.uid)? { Some(name) => name, None => header.uid.to_string(), }; let group = match user_group_cache.get_group(header.gid)? { Some(name) => name, None => header.gid.to_string(), }; let mode_string = header.mode_string(); if header.mtime != last_mtime || time_string.is_empty() { last_mtime = header.mtime; time_string = format_time(header.mtime, now)?; }; match header.mode & MODE_FILETYPE_MASK { FILETYPE_SYMLINK => { let target = header.read_symlink_target(file)?; writeln!( out, "{} {:>3} {:<8} {:<8} {:>8} {} {} -> {}", std::str::from_utf8(&mode_string).unwrap(), header.nlink, user, group, header.filesize, time_string, header.filename, target )?; } FILETYPE_BLOCK_DEVICE | FILETYPE_CHARACTER_DEVICE => { header.skip_file_content(file)?; writeln!( out, "{} {:>3} {:<8} {:<8} {:>3}, {:>3} {} {}", std::str::from_utf8(&mode_string).unwrap(), header.nlink, user, group, header.rmajor, header.rminor, time_string, header.filename )?; } _ => { header.skip_file_content(file)?; writeln!( out, "{} {:>3} {:<8} {:<8} {:>8} {} {}", std::str::from_utf8(&mode_string).unwrap(), header.nlink, user, group, header.filesize, time_string, header.filename )?; } }; } Ok(()) } fn create_dir_ignore_existing>(path: P) -> Result<()> { if let Err(e) = create_dir(&path) { if e.kind() != ErrorKind::AlreadyExists { return Err(e); } let stat = symlink_metadata(&path)?; if !stat.is_dir() { remove_file(&path)?; create_dir(&path)?; } }; Ok(()) } fn write_character_device( header: &Header, preserve_permissions: bool, log_level: u32, ) -> Result<()> { if header.filesize != 0 { return Err(Error::new( ErrorKind::InvalidData, format!( "Invalid size for character device '{}': {} bytes instead of 0.", header.filename, header.filesize ), )); }; if log_level >= LOG_LEVEL_DEBUG { writeln!( std::io::stderr(), "Creating character device '{}' with mode {:o}", header.filename, header.mode_perm(), )?; }; if let Err(e) = mknod(&header.filename, header.mode, header.rmajor, header.rminor) { match e.kind() { ErrorKind::AlreadyExists => { remove_file(&header.filename)?; mknod(&header.filename, header.mode, header.rmajor, header.rminor)?; } _ => { return Err(e); } } }; if preserve_permissions { lchown(&header.filename, Some(header.uid), Some(header.gid))?; }; set_permissions(&header.filename, header.permission())?; set_modified(&header.filename, header.mtime.into())?; Ok(()) } fn write_directory( header: &Header, preserve_permissions: bool, log_level: u32, mtimes: &mut BTreeMap, ) -> Result<()> { if header.filesize != 0 { return Err(Error::new( ErrorKind::InvalidData, format!( "Invalid size for directory '{}': {} bytes instead of 0.", header.filename, header.filesize ), )); }; if log_level >= LOG_LEVEL_DEBUG { writeln!( std::io::stderr(), "Creating directory '{}' with mode {:o}{}", header.filename, header.mode_perm(), if preserve_permissions { format!(" and owner {}:{}", header.uid, header.gid) } else { String::new() }, )?; }; create_dir_ignore_existing(&header.filename)?; if preserve_permissions { chown(&header.filename, Some(header.uid), Some(header.gid))?; } set_permissions(&header.filename, header.permission())?; mtimes.insert(header.filename.to_string(), header.mtime.into()); Ok(()) } fn from_mtime(mtime: u32) -> SystemTime { std::time::UNIX_EPOCH + std::time::Duration::from_secs(mtime.into()) } fn write_file( cpio_file: &mut R, header: &Header, preserve_permissions: bool, seen_files: &mut SeenFiles, log_level: u32, ) -> Result<()> { let mut file; if let Some(target) = header.try_get_hard_link_target(seen_files) { if log_level >= LOG_LEVEL_DEBUG { writeln!( std::io::stderr(), "Creating hard-link '{}' -> '{}' with permission {:o}{} and {} bytes", header.filename, target, header.mode_perm(), if preserve_permissions { format!(" and owner {}:{}", header.uid, header.gid) } else { String::new() }, header.filesize, )?; }; if let Err(e) = hard_link(target, &header.filename) { match e.kind() { ErrorKind::AlreadyExists => { remove_file(&header.filename)?; hard_link(target, &header.filename)?; } _ => { return Err(e); } } } file = OpenOptions::new().write(true).open(&header.filename)? } else { if log_level >= LOG_LEVEL_DEBUG { writeln!( std::io::stderr(), "Creating file '{}' with permission {:o}{} and {} bytes", header.filename, header.mode_perm(), if preserve_permissions { format!(" and owner {}:{}", header.uid, header.gid) } else { String::new() }, header.filesize, )?; }; file = File::create(&header.filename)? }; header.mark_seen(seen_files); let mut reader = cpio_file.take(header.filesize.into()); // TODO: check writing hard-link with length == 0 // TODO: check overwriting existing files/hardlinks let written = std::io::copy(&mut reader, &mut file)?; if written != header.filesize.into() { return Err(Error::other(format!( "Wrong amound of bytes written to '{}': {} != {}.", header.filename, written, header.filesize ))); } let skip = align_to_4_bytes(header.filesize); cpio_file.seek_forward(skip.into())?; if preserve_permissions { fchown(&file, Some(header.uid), Some(header.gid))?; } file.set_permissions(header.permission())?; file.set_modified(from_mtime(header.mtime))?; Ok(()) } fn write_symbolic_link( cpio_file: &mut R, header: &Header, preserve_permissions: bool, log_level: u32, ) -> Result<()> { let target = header.read_symlink_target(cpio_file)?; if log_level >= LOG_LEVEL_DEBUG { writeln!( std::io::stderr(), "Creating symlink '{}' -> '{}' with mode {:o}", header.filename, &target, header.mode_perm(), )?; }; if let Err(e) = symlink(&target, &header.filename) { match e.kind() { ErrorKind::AlreadyExists => { remove_file(&header.filename)?; symlink(&target, &header.filename)?; } _ => { return Err(e); } } } if preserve_permissions { lchown(&header.filename, Some(header.uid), Some(header.gid))?; } if header.mode_perm() != 0o777 { return Err(Error::new( ErrorKind::Unsupported, format!( "Symlink '{}' has mode {:o}, but only mode 777 is supported.", header.filename, header.mode_perm() ), )); }; set_modified(&header.filename, header.mtime.into())?; Ok(()) } fn absolute_parent_directory>(path: S, base_dir: &Path) -> Result where PathBuf: From, { let abspath = if path.as_ref().starts_with("/") { PathBuf::from(path) } else { base_dir.join(path.as_ref()) }; match abspath.parent() { Some(d) => Ok(d.into()), // TODO: Use ErrorKind::InvalidFilename once stable. None => Err(Error::new( ErrorKind::InvalidData, format!("Path {:#?} has no parent directory.", abspath), )), } } fn check_path_is_canonical_subdir + std::fmt::Display>( path: S, dir: &Path, base_dir: &PathBuf, ) -> Result { let canonicalized_path = dir.canonicalize()?; if !canonicalized_path.starts_with(base_dir) { return Err(Error::new( ErrorKind::InvalidData, format!( "The parent directory of \"{}\" (resolved to {:#?}) is not within the directory {:#?}.", path, canonicalized_path, base_dir ), )); } Ok(canonicalized_path) } fn read_cpio_and_extract( file: &mut R, base_dir: &PathBuf, preserve_permissions: bool, log_level: u32, ) -> Result<()> { let mut extractor = Extractor::new(); let mut previous_checked_dir = PathBuf::new(); loop { let header = match Header::read(file) { Ok(header) => { if header.filename == "TRAILER!!!" { break; } else { header } } Err(e) => return Err(e), }; if log_level >= LOG_LEVEL_DEBUG { writeln!(std::io::stderr(), "{:?}", header)?; } else if log_level >= LOG_LEVEL_INFO { writeln!(std::io::stderr(), "{}", header.filename)?; } if !header.is_root_directory() { let absdir = absolute_parent_directory(&header.filename, base_dir)?; // canonicalize() is an expensive call. So cache the previously resolved // parent directory. Skip the path traversal check in case the absolute // parent directory has no symlinks and matches the previouly checked directory. if absdir != previous_checked_dir { previous_checked_dir = check_path_is_canonical_subdir(&header.filename, &absdir, base_dir)?; } } match header.mode & MODE_FILETYPE_MASK { FILETYPE_CHARACTER_DEVICE => { write_character_device(&header, preserve_permissions, log_level)? } FILETYPE_DIRECTORY => write_directory( &header, preserve_permissions, log_level, &mut extractor.mtimes, )?, FILETYPE_REGULAR_FILE => write_file( file, &header, preserve_permissions, &mut extractor.seen_files, log_level, )?, FILETYPE_SYMLINK => { write_symbolic_link(file, &header, preserve_permissions, log_level)? } FILETYPE_FIFO | FILETYPE_BLOCK_DEVICE | FILETYPE_SOCKET => { unimplemented!( "Mode {:o} (file {}) not implemented. Please open a bug report requesting support for this type.", header.mode, header.filename ) } _ => { return Err(Error::new( ErrorKind::InvalidData, format!( "Invalid/unknown filetype {:o}: {}", header.mode, header.filename ), )) } }; } extractor.set_modified_times(log_level)?; Ok(()) } fn seek_to_cpio_end(file: &mut File) -> Result<()> { let cpio = CpioFilenameReader { file }; for f in cpio { f?; } Ok(()) } pub fn get_cpio_archive_count(file: &mut File) -> Result { let mut count = 0; loop { let command = match read_magic_header(file) { None => return Ok(count), Some(x) => x?, }; count += 1; if command.get_program() == "cpio" { seek_to_cpio_end(file)?; } else { break; } } Ok(count) } pub fn print_cpio_archive_count(mut file: File, out: &mut W) -> Result<()> { let count = get_cpio_archive_count(&mut file)?; writeln!(out, "{}", count)?; Ok(()) } pub fn examine_cpio_content(mut file: File, out: &mut W) -> Result<()> { loop { let command = match read_magic_header(&mut file) { None => return Ok(()), Some(x) => x?, }; writeln!( out, "{}\t{}", file.stream_position()?, command.get_program().to_str().unwrap() )?; if command.get_program() == "cpio" { seek_to_cpio_end(&mut file)?; } else { break; } } Ok(()) } pub fn extract_cpio_archive( mut file: File, preserve_permissions: bool, subdir: Option, log_level: u32, ) -> Result<()> { let mut count = 1; let base_dir = std::env::current_dir()?; loop { if let Some(ref s) = subdir { let mut dir = base_dir.clone(); dir.push(format!("{s}{count}")); create_dir_ignore_existing(&dir)?; std::env::set_current_dir(&dir)?; } let mut command = match read_magic_header(&mut file) { None => return Ok(()), Some(x) => x?, }; if command.get_program() == "cpio" { read_cpio_and_extract(&mut file, &base_dir, preserve_permissions, log_level)?; } else { let mut decompressed = decompress(&mut command, file)?; read_cpio_and_extract( &mut decompressed, &base_dir, preserve_permissions, log_level, )?; break; } count += 1; } Ok(()) } pub fn list_cpio_content(mut file: File, out: &mut W, log_level: u32) -> Result<()> { let mut user_group_cache = UserGroupCache::new(); let now = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap() .as_secs() .try_into() .unwrap(); loop { let mut command = match read_magic_header(&mut file) { None => return Ok(()), Some(x) => x?, }; if command.get_program() == "cpio" { if log_level >= LOG_LEVEL_INFO { read_cpio_and_print_long_format(&mut file, out, now, &mut user_group_cache)?; } else { read_cpio_and_print_filenames(&mut file, out)?; } } else { let mut decompressed = decompress(&mut command, file)?; if log_level >= LOG_LEVEL_INFO { read_cpio_and_print_long_format( &mut decompressed, out, now, &mut user_group_cache, )?; } else { read_cpio_and_print_filenames(&mut decompressed, out)?; } break; } } Ok(()) } #[cfg(test)] mod tests { use std::env::{self, current_dir, set_current_dir}; use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt}; use super::*; use crate::libc::{major, minor}; fn getgid() -> u32 { unsafe { ::libc::getgid() } } fn getuid() -> u32 { unsafe { ::libc::getuid() } } extern "C" { fn tzset(); } struct TempDir { path: PathBuf, cwd: PathBuf, } impl TempDir { fn new() -> Result { // Use some very pseudo-random number let cwd = current_dir()?; let epoch = SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap(); let name = std::option_env!("CARGO_PKG_NAME").unwrap(); let dir_builder = std::fs::DirBuilder::new(); let mut path = env::temp_dir(); path.push(format!("{}-{}", name, epoch.subsec_nanos())); dir_builder.create(&path).map(|_| Self { path, cwd }) } } impl Drop for TempDir { fn drop(&mut self) { let _ = set_current_dir(&self.cwd); let _ = std::fs::remove_dir_all(&self.path); } } impl UserGroupCache { fn insert_test_data(&mut self) { self.user_cache.insert(1000, Some("user".into())); self.group_cache.insert(123, Some("whoopsie".into())); self.group_cache.insert(2000, None); } } #[test] fn test_absolute_parent_directory() { let base_dir = Path::new("/nonexistent/arthur"); assert_eq!( absolute_parent_directory("usr/bin/true", base_dir).unwrap(), PathBuf::from("/nonexistent/arthur/usr/bin") ); assert_eq!( absolute_parent_directory("/usr/bin/true", base_dir).unwrap(), PathBuf::from("/usr/bin") ); assert_eq!( absolute_parent_directory(".", base_dir).unwrap(), PathBuf::from("/nonexistent") ); } // Test detecting path traversal attacks like CVE-2015-1197 #[test] fn test_read_cpio_and_extract_path_traversal() { let mut file = File::open("tests/path-traversal.cpio").unwrap(); let tempdir = TempDir::new().unwrap(); set_current_dir(&tempdir.path).unwrap(); let got = read_cpio_and_extract(&mut file, &tempdir.path, false, LOG_LEVEL_WARNING).unwrap_err(); assert_eq!(got.kind(), ErrorKind::InvalidData); assert_eq!(got.to_string(), format!( "The parent directory of \"tmp/trav.txt\" (resolved to \"/tmp\") is not within the directory {:#?}.", &tempdir.path )); } #[test] fn test_absolute_parent_directory_error() { let got = absolute_parent_directory(".", Path::new("/")).unwrap_err(); assert_eq!(got.kind(), ErrorKind::InvalidData); assert_eq!(got.to_string(), "Path \"/.\" has no parent directory."); } #[test] fn test_align_to_4_bytes() { assert_eq!(align_to_4_bytes(110), 2); } #[test] fn test_align_to_4_bytes_is_aligned() { assert_eq!(align_to_4_bytes(32), 0); } #[test] fn test_decompress_program_not_found() { let file = File::open("tests/single.cpio").expect("test cpio should be present"); let mut cmd = Command::new("non-existing-program"); let got = decompress(&mut cmd, file).unwrap_err(); assert_eq!(got.kind(), ErrorKind::Other); assert_eq!( got.to_string(), "Program 'non-existing-program' not found in PATH." ); } #[test] fn test_get_cpio_archive_count_single() { let mut file = File::open("tests/single.cpio").expect("test cpio should be present"); let count = get_cpio_archive_count(&mut file).unwrap(); assert_eq!(count, 1); } #[test] fn test_print_cpio_archive_count() { let mut file = File::open("tests/zstd.cpio").expect("test cpio should be present"); let mut output = Vec::new(); let count = get_cpio_archive_count(&mut file).unwrap(); assert_eq!(count, 2); file.seek(SeekFrom::Start(0)).unwrap(); print_cpio_archive_count(file, &mut output).unwrap(); assert_eq!(String::from_utf8(output).unwrap(), "2\n"); } #[test] fn test_read_cpio_and_print_long_format_character_device() { // Wrapped before mtime and filename let cpio_data = b"07070100000003000021A4000000000000\ 00000000000167055BC800000000000000000000000000000005000000010000\ 000C00000000dev/console\0\0\0\ 0707010000000000000000000000000000000000000001\ 0000000000000000000000000000000000000000000000000000000B00000000\ TRAILER!!!\0\0\0\0"; let mut output = Vec::new(); let mut user_group_cache = UserGroupCache::new(); env::set_var("TZ", "UTC"); unsafe { tzset() }; read_cpio_and_print_long_format( &mut cpio_data.as_ref(), &mut output, 1728486311, &mut user_group_cache, ) .unwrap(); assert_eq!( String::from_utf8(output).unwrap(), "crw-r--r-- 1 root root 5, 1 Oct 8 16:20 dev/console\n" ); } #[test] fn test_read_cpio_and_print_long_format_directory() { // Wrapped before mtime and filename let cpio_data = b"07070100000001000047FF000000000000007B00000002\ 66A6E40400000000000000000000000000000000000000000000000B00000000\ /var/crash\0\0\0\0\ 0707010000000000000000000000000000000000000001\ 0000000000000000000000000000000000000000000000000000000B00000000\ TRAILER!!!\0\0\0\0"; let mut output = Vec::new(); let mut user_group_cache = UserGroupCache::new(); user_group_cache.insert_test_data(); env::set_var("TZ", "UTC"); unsafe { tzset() }; read_cpio_and_print_long_format( &mut cpio_data.as_ref(), &mut output, 1722389471, &mut user_group_cache, ) .unwrap(); assert_eq!( String::from_utf8(output).unwrap(), "drwxrwsrwt 2 root whoopsie 0 Jul 29 00:36 /var/crash\n" ); } #[test] fn test_read_cpio_and_print_long_format_file() { // Wrapped before mtime and filename let cpio_data = b"070701000036E4000081A4000003E8000007D000000001\ 66A3285300000041000000000000002400000000000000000000000D00000000\ conf/modules\0\0\ linear\nmultipath\nraid0\nraid1\nraid456\nraid5\nraid6\nraid10\nefivarfs\0\0\0\0\ 0707010000000000000000000000000000000000000001\ 0000000000000000000000000000000000000000000000000000000B00000000\ TRAILER!!!\0\0\0\0"; let mut output = Vec::new(); let mut user_group_cache = UserGroupCache::new(); user_group_cache.insert_test_data(); env::set_var("TZ", "UTC"); unsafe { tzset() }; read_cpio_and_print_long_format( &mut cpio_data.as_ref(), &mut output, 1722645915, &mut user_group_cache, ) .unwrap(); assert_eq!( String::from_utf8(output).unwrap(), "-rw-r--r-- 1 user 2000 65 Jul 26 04:38 conf/modules\n" ); } #[test] fn test_read_cpio_and_print_long_format_symlink() { // Wrapped before mtime and filename let cpio_data = b"0707010000000D0000A1FF000000000000000000000001\ 6237389400000007000000000000000000000000000000000000000400000000\ bin\0\0\0usr/bin\0\ 0707010000000000000000000000000000000000000001\ 0000000000000000000000000000000000000000000000000000000B00000000\ TRAILER!!!\0\0\0\0"; let mut output = Vec::new(); let mut user_group_cache = UserGroupCache::new(); user_group_cache.insert_test_data(); read_cpio_and_print_long_format( &mut cpio_data.as_ref(), &mut output, 1722645915, &mut user_group_cache, ) .unwrap(); assert_eq!( String::from_utf8(output).unwrap(), "lrwxrwxrwx 1 root root 7 Mar 20 2022 bin -> usr/bin\n" ); } #[test] fn test_write_character_device() { if getuid() != 0 { // This test needs to run as root. return; } let mut header = Header::new(1, 0o20_644, 0, 0, 0, 1740402179, 0, "./null"); header.rmajor = 1; header.rminor = 3; write_character_device(&header, true, LOG_LEVEL_WARNING).unwrap(); let attr = std::fs::metadata("null").unwrap(); assert_eq!(attr.len(), header.filesize.into()); assert!(attr.file_type().is_char_device()); assert_eq!(attr.modified().unwrap(), from_mtime(header.mtime)); assert_eq!(attr.permissions(), PermissionsExt::from_mode(header.mode)); assert_eq!(attr.uid(), header.uid); assert_eq!(attr.gid(), header.gid); assert_eq!(major(attr.rdev()), header.rmajor); assert_eq!(minor(attr.rdev()), header.rminor); std::fs::remove_file("null").unwrap(); } #[test] fn test_write_directory_with_setuid() { let mut mtimes = BTreeMap::new(); let header = Header::new( 1, 0o43_777, getuid(), getgid(), 0, 1720081471, 0, "./directory_with_setuid", ); write_directory(&header, true, LOG_LEVEL_WARNING, &mut mtimes).unwrap(); let attr = std::fs::metadata("directory_with_setuid").unwrap(); assert!(attr.is_dir()); assert_eq!(attr.permissions(), PermissionsExt::from_mode(header.mode)); assert_eq!(attr.uid(), header.uid); assert_eq!(attr.gid(), header.gid); std::fs::remove_dir("directory_with_setuid").unwrap(); let mut expected_mtimes: BTreeMap = BTreeMap::new(); expected_mtimes.insert("./directory_with_setuid".into(), header.mtime.into()); assert_eq!(mtimes, expected_mtimes); } #[test] fn test_write_file_with_setuid() { let mut seen_files = SeenFiles::new(); let header = Header::new( 1, 0o104_755, getuid(), getgid(), 0, 1720081471, 9, "./file_with_setuid", ); let cpio = b"!/bin/sh\n\0\0\0"; write_file( &mut cpio.as_ref(), &header, true, &mut seen_files, LOG_LEVEL_WARNING, ) .unwrap(); let attr = std::fs::metadata("file_with_setuid").unwrap(); assert_eq!(attr.len(), header.filesize.into()); assert!(attr.is_file()); assert_eq!(attr.modified().unwrap(), from_mtime(header.mtime)); assert_eq!(attr.permissions(), PermissionsExt::from_mode(header.mode)); assert_eq!(attr.uid(), header.uid); assert_eq!(attr.gid(), header.gid); std::fs::remove_file("file_with_setuid").unwrap(); } #[test] fn test_write_symbolic_link() { let header = Header::new( 1, 0o120_777, getuid(), getgid(), 0, 1721427072, 12, "./dead_symlink", ); let cpio = b"/nonexistent"; write_symbolic_link(&mut cpio.as_ref(), &header, true, LOG_LEVEL_WARNING).unwrap(); let attr = std::fs::symlink_metadata("dead_symlink").unwrap(); assert_eq!(attr.len(), header.filesize.into()); assert!(attr.is_symlink()); assert_eq!(attr.modified().unwrap(), from_mtime(header.mtime)); assert_eq!(attr.permissions(), PermissionsExt::from_mode(header.mode)); assert_eq!(attr.uid(), header.uid); assert_eq!(attr.gid(), header.gid); std::fs::remove_file("dead_symlink").unwrap(); } } threecpio-0.5.1/src/libc.rs000064400000000000000000000144531046102023000136660ustar 00000000000000use std::ffi::{CStr, CString}; use std::io::{Error, Result}; /// Get password file entry and return user name. /// /// This function wraps the standard C library function getpwuid(). /// The getpwuid() function returns a pointer to a structure containing the /// broken-out fields of the record in the password database (e.g., the local /// password file /etc/passwd, NIS, and LDAP) that matches the user ID uid. pub fn getpwuid_name(uid: u32) -> Result> { let mut pwd = std::mem::MaybeUninit::::uninit(); let mut buf = [0u8; 2048]; let mut result = std::ptr::null_mut::(); let rc = unsafe { libc::getpwuid_r( uid, pwd.as_mut_ptr(), buf.as_mut_ptr() as *mut libc::c_char, buf.len(), &mut result, ) }; if rc != 0 { return Err(Error::last_os_error()); } if result.is_null() { return Ok(None); } let name = unsafe { core::ffi::CStr::from_ptr((*result).pw_name) }; Ok(Some(name.to_string_lossy().to_string())) } /// Get group file entry and return group name. /// /// This function wraps the standard C library function getgrgid(). /// The getgrgid() function returns a pointer to a structure containing the /// broken-out fields of the record in the group database (e.g., the local /// group file /etc/group, NIS, and LDAP) that matches the group ID gid. pub fn getgrgid_name(gid: u32) -> Result> { let mut group = std::mem::MaybeUninit::::uninit(); let mut buf = [0u8; 2048]; let mut result = std::ptr::null_mut::(); let rc = unsafe { libc::getgrgid_r( gid, group.as_mut_ptr(), buf.as_mut_ptr() as *mut libc::c_char, buf.len(), &mut result, ) }; if rc != 0 { return Err(Error::last_os_error()); } if result.is_null() { return Ok(None); } let name = unsafe { core::ffi::CStr::from_ptr((*result).gr_name) }; Ok(Some(name.to_string_lossy().to_string())) } #[cfg(test)] pub fn major(dev: u64) -> u32 { libc::major(dev) } #[cfg(test)] pub fn minor(dev: u64) -> u32 { libc::minor(dev) } pub fn mknod(pathname: &str, mode: libc::mode_t, major: u32, minor: u32) -> Result<()> { let p = CString::new(pathname)?; let rc = unsafe { libc::mknod(p.as_ptr(), mode, libc::makedev(major, minor)) }; if rc != 0 { return Err(Error::last_os_error()); }; Ok(()) } pub fn set_modified(path: &str, mtime: i64) -> Result<()> { let p = CString::new(path)?; let mut modified: libc::timespec = unsafe { std::mem::zeroed() }; modified.tv_sec = mtime; // times contains the access time followed by modfied time let times = [modified, modified]; let rc = unsafe { libc::utimensat( libc::AT_FDCWD, p.as_ptr(), times.as_ptr(), libc::AT_SYMLINK_NOFOLLOW, ) }; if rc != 0 { return Err(Error::last_os_error()); }; Ok(()) } // TODO: Use c"…" string literal for `format` once stable fn strftime(format: &[u8], tm: *mut libc::tm) -> Result { let mut s = [0u8; 19]; let length = unsafe { libc::strftime( s.as_mut_ptr() as *mut libc::c_char, s.len(), CStr::from_bytes_with_nul_unchecked(format).as_ptr(), tm, ) }; if length == 0 { return Err(Error::other("strftime returned 0")); } Ok(String::from_utf8_lossy(&s[..length]).to_string()) } pub fn strftime_local(format: &[u8], timestamp: u32) -> Result { let mut tm = std::mem::MaybeUninit::::uninit(); let result = unsafe { libc::localtime_r(×tamp.into(), tm.as_mut_ptr()) }; if result.is_null() { return Err(Error::last_os_error()); }; strftime(format, result) } #[cfg(test)] mod tests { use super::*; use std::env::temp_dir; use std::fs::{self, create_dir}; use std::path::PathBuf; use std::time::{Duration, SystemTime}; fn make_temp_dir() -> Result { let mut dir = temp_dir(); let now = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap(); dir.push(format!("3cpio-{:?}", now)); create_dir(&dir)?; Ok(dir) } extern "C" { fn tzset(); } #[test] fn test_getpwuid_name_root() { let got = getpwuid_name(0).unwrap(); assert_eq!(got, Some("root".to_string())); } #[test] fn test_getpwuid_name_non_existing() { // Assume that this UID is not in /etc/passwd (nobody is 65534) let got = getpwuid_name(65520).unwrap(); assert_eq!(got, None); } #[test] fn test_getgrgid_name_root() { let got = getgrgid_name(0).unwrap(); assert_eq!(got, Some("root".to_string())); } #[test] fn test_getgrgid_name_non_existing() { // Assume that this GID is not in /etc/passwd (nogroup is 65534) let got = getgrgid_name(65520).unwrap(); assert_eq!(got, None); } #[test] // Create a temporary directory and set the mtime 10 seconds earlier // than the current mtime of the directory. fn test_set_modified() { let dir: PathBuf = make_temp_dir().unwrap(); let modified = dir.metadata().unwrap().modified().unwrap(); let duration = modified.duration_since(SystemTime::UNIX_EPOCH).unwrap(); let new_modified = SystemTime::UNIX_EPOCH .checked_add(Duration::new(duration.as_secs() - 10, 0)) .unwrap(); let mtime = new_modified.duration_since(SystemTime::UNIX_EPOCH).unwrap(); let p = dir.clone().into_os_string().into_string().unwrap(); set_modified(&p, mtime.as_secs().try_into().unwrap()).unwrap(); assert_eq!(dir.metadata().unwrap().modified().unwrap(), new_modified); fs::remove_dir(dir).unwrap(); } #[test] fn test_strftime_local_year() { let time = strftime_local(b"%b %e %Y\0", 2278410030).unwrap(); assert_eq!(time, "Mar 14 2042"); } #[test] fn test_strftime_local_hour() { std::env::set_var("TZ", "UTC"); unsafe { tzset() }; let time = strftime_local(b"%b %e %H:%M\0", 1720735264).unwrap(); assert_eq!(time, "Jul 11 22:01"); } } threecpio-0.5.1/src/main.rs000064400000000000000000000166751046102023000137110ustar 00000000000000// Copyright (C) 2024, Benjamin Drung // SPDX-License-Identifier: ISC use std::env::set_current_dir; use std::fs::{create_dir, read_dir, File}; use std::io::ErrorKind; use std::path::Path; use std::process::ExitCode; use lexopt::prelude::*; use threecpio::{ examine_cpio_content, extract_cpio_archive, list_cpio_content, print_cpio_archive_count, LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_WARNING, }; #[derive(Debug)] struct Args { count: bool, directory: String, examine: bool, extract: bool, force: bool, list: bool, log_level: u32, file: String, preserve_permissions: bool, subdir: Option, } fn print_help() { let executable = std::env::args().next().unwrap(); println!( "Usage: {executable} --count FILE {executable} {{-e|--examine}} FILE {executable} {{-t|--list}} [-v] FILE {executable} {{-x|--extract}} [-v|--debug] [-C DIR] [-p] [-s NAME] [--force] FILE Optional arguments: --count Print the number of concatenated cpio archives. -e, --examine List the offsets of the cpio archives and their compression. -t, --list List the contents of the cpio archives. -x, --extract Extract cpio archives. -C, --directory=DIR Change directory before performing any operation. -p, --preserve-permissions Set permissions of extracted files to those recorded in the archive (default for superuser). -s, --subdir Extract the cpio archives into separate directories (using the given name plus an incrementing number) -v, --verbose Verbose output --debug Debug output --force Force overwriting existing files -h, --help print help message -V, --version print version number and exit", ); } fn print_version() { let name = std::option_env!("CARGO_BIN_NAME").unwrap(); let version = std::option_env!("CARGO_PKG_VERSION").unwrap(); println!("{} {}", name, version); } fn parse_args() -> Result { let mut count = 0; let mut examine = 0; let mut extract = 0; let mut force = false; let mut preserve_permissions = is_root(); let mut list = 0; let mut log_level = LOG_LEVEL_WARNING; let mut directory = ".".into(); let mut file = None; let mut subdir: Option = None; let mut parser = lexopt::Parser::from_env(); while let Some(arg) = parser.next()? { match arg { Long("count") => { count = 1; } Short('C') | Long("directory") => { directory = parser.value()?.string()?; } Long("debug") => { log_level = LOG_LEVEL_DEBUG; } Short('e') | Long("examine") => { examine = 1; } Long("force") => { force = true; } Short('h') | Long("help") => { print_help(); std::process::exit(0); } Short('p') | Long("preserve-permissions") => { preserve_permissions = true; } Short('s') | Long("subdir") => { subdir = Some(parser.value()?.string()?); } Short('t') | Long("list") => { list = 1; } Short('v') | Long("verbose") => { if log_level <= LOG_LEVEL_INFO { log_level = LOG_LEVEL_INFO; } } Short('V') | Long("version") => { print_version(); std::process::exit(0); } Short('x') | Long("extract") => { extract = 1; } Value(val) if file.is_none() => { file = Some(val.string()?); } _ => return Err(arg.unexpected()), } } if count + examine + extract + list != 1 { return Err("Either --count, --examine, --extract, or --list must be specified!".into()); } if let Some(ref s) = subdir { if s.contains('/') { return Err(format!("Subdir '{}' must not contain slashes!", s).into()); } } Ok(Args { count: count == 1, directory, examine: examine == 1, extract: extract == 1, force, list: list == 1, log_level, file: file.ok_or("missing argument FILE")?, preserve_permissions, subdir, }) } fn is_empty_directory>(path: P) -> std::io::Result { Ok(read_dir(path)?.next().is_none()) } fn is_root() -> bool { let uid = unsafe { libc::getuid() }; uid == 0 } fn create_and_set_current_dir(path: &str, force: bool) -> Result<(), String> { if let Err(e) = set_current_dir(path) { if e.kind() != ErrorKind::NotFound { return Err(format!("Failed to change directory to '{}': {}", path, e)); } if let Err(e) = create_dir(path) { return Err(format!("Failed to create directory '{}': {}", path, e)); } if let Err(e) = set_current_dir(path) { return Err(format!("Failed to change directory to '{}': {}", path, e)); } } if !force { match is_empty_directory(".") { Err(e) => { return Err(format!( "Failed to check content of directory '{}': {}", path, e )); } Ok(false) => { return Err(format!( "Target directory '{}' is not empty. Use --force to overwrite existing files!", path )); } Ok(true) => {} } } Ok(()) } fn main() -> ExitCode { let executable = std::env::args().next().unwrap(); let args = match parse_args() { Ok(a) => a, Err(e) => { eprintln!("{}: Error: {}", executable, e); return ExitCode::from(2); } }; let file = match File::open(&args.file) { Ok(f) => f, Err(e) => { eprintln!( "{}: Error: Failed to open '{}': {}", executable, args.file, e ); return ExitCode::FAILURE; } }; if args.extract { if let Err(e) = create_and_set_current_dir(&args.directory, args.force) { eprintln!("{}: Error: {}", executable, e); return ExitCode::FAILURE; } } let mut stdout = std::io::stdout(); let (operation, result) = if args.count { ( "count number of cpio archives", print_cpio_archive_count(file, &mut stdout), ) } else if args.examine { ("examine content", examine_cpio_content(file, &mut stdout)) } else if args.extract { ( "extract content", extract_cpio_archive(file, args.preserve_permissions, args.subdir, args.log_level), ) } else if args.list { ( "list content", list_cpio_content(file, &mut stdout, args.log_level), ) } else { unreachable!("no operation specified"); }; if let Err(e) = result { match e.kind() { ErrorKind::BrokenPipe => {} _ => { eprintln!( "{}: Error: Failed to {} of '{}': {}", executable, operation, args.file, e ); return ExitCode::FAILURE; } } } ExitCode::SUCCESS } threecpio-0.5.1/src/seek_forward.rs000064400000000000000000000032301046102023000154170ustar 00000000000000// Copyright (C) 2024, Benjamin Drung // SPDX-License-Identifier: ISC use std::fs::File; use std::io::{Error, ErrorKind, Read, Result, Seek, SeekFrom}; use std::process::ChildStdout; const PIPE_SIZE: usize = 65536; pub trait SeekForward { /// Seek forward to an offset, in bytes, in a stream. /// /// A seek beyond the end of a stream is allowed, but behavior is defined /// by the implementation. /// /// # Errors /// /// Seeking can fail, for example because it might involve flushing a buffer. fn seek_forward(&mut self, offset: u64) -> Result<()>; } impl SeekForward for File { fn seek_forward(&mut self, offset: u64) -> Result<()> { self.seek(SeekFrom::Current(offset.try_into().unwrap()))?; Ok(()) } } impl SeekForward for ChildStdout { fn seek_forward(&mut self, offset: u64) -> Result<()> { let mut seek_reader = self.take(offset); let mut remaining: usize = offset.try_into().unwrap(); let mut buffer = [0; PIPE_SIZE]; while remaining > 0 { let read = seek_reader.read(&mut buffer)?; remaining -= read; } Ok(()) } } impl SeekForward for &[u8] { fn seek_forward(&mut self, offset: u64) -> Result<()> { let mut seek_reader = std::io::Read::take(self, offset); let mut buffer = Vec::new(); let read = seek_reader.read_to_end(&mut buffer)?; if read < offset.try_into().unwrap() { return Err(Error::new( ErrorKind::UnexpectedEof, format!("read only {} bytes, but {} wanted", read, offset), )); } Ok(()) } } threecpio-0.5.1/tests/bzip2.cpio000064400000000000000000000013411046102023000146540ustar 0000000000000007070100000000000041FD000003E8000003E800000002661BE5C600000000000000000000000000000000000000000000000200000000.07070100000001000041FD000003E8000003E800000002661BE5C600000000000000000000000000000000000000000000000500000000path07070100000002000081B4000003E8000003E800000001661BE5C600000008000000000000000000000000000000000000000A00000000path/filecontent 07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!BZh91AY&SY=Hf߀Ph ?$@3o`0[#@ѣ@EOMO"2 @dhJMMѠ  *ea2b`okkdoD m!Fk`*ĄB(7Ӿ+W w)H)PTSFvhr9 "ׂVkM fw=2"s p~r~ow$S ԊPthreecpio-0.5.1/tests/cli.rs000064400000000000000000000113301046102023000140660ustar 00000000000000// Copyright (C) 2024, Benjamin Drung // SPDX-License-Identifier: ISC use std::env; use std::error::Error; use std::process::{Command, Output}; // Derive target directory (e.g. `target/debug`) from current executable fn get_target_dir() -> std::path::PathBuf { let mut path = env::current_exe().expect("env::current_exe not set"); path.pop(); if path.ends_with("deps") { path.pop(); } path } fn get_command() -> Command { let mut program = get_target_dir(); program.push("3cpio"); Command::new(program) } trait ExitCodeAssertion { fn assert_failure(self, expected_code: i32) -> Self; fn assert_success(self) -> Self; } impl ExitCodeAssertion for Output { fn assert_failure(self, expected_code: i32) -> Self { assert_eq!(self.status.code().expect("exit code"), expected_code); self } fn assert_success(self) -> Self { assert!(self.status.success()); self } } trait OutputAssertion { fn assert_stderr(self, expected: S) -> Self; fn assert_stdout(self, expected: S) -> Self; } impl OutputAssertion for Output where String: PartialEq, S: std::fmt::Debug, { fn assert_stderr(self, expected: S) -> Self { let stderr = String::from_utf8(self.stderr.clone()).expect("stderr"); assert_eq!(stderr, expected); self } fn assert_stdout(self, expected: S) -> Self { let stdout = String::from_utf8(self.stdout.clone()).expect("stdout"); assert_eq!(stdout, expected); self } } trait OutputContainsAssertion { fn assert_stderr_contains(self, expected: &str) -> Self; } impl OutputContainsAssertion for Output { fn assert_stderr_contains(self, expected: &str) -> Self { let stderr = String::from_utf8(self.stderr.clone()).expect("stderr"); assert!( stderr.contains(expected), "'{}' not found in '{}'", expected, stderr ); self } } #[test] fn count_cpio_archives() -> Result<(), Box> { let mut cmd = get_command(); cmd.arg("--count").arg("tests/zstd.cpio"); cmd.output()?.assert_success().assert_stdout("2\n"); Ok(()) } #[test] fn examine_compressed_cpio() -> Result<(), Box> { for compression in ["bzip2", "gzip", "lz4", "lzop", "xz", "zstd"] { let mut cmd = get_command(); cmd.arg("-e").arg(format!("tests/{}.cpio", compression)); cmd.output()? .assert_stderr("") .assert_success() .assert_stdout(format!("0\tcpio\n512\t{}\n", compression)); } Ok(()) } #[test] fn examine_single_cpio() -> Result<(), Box> { let mut cmd = get_command(); cmd.arg("-e").arg("tests/single.cpio"); cmd.output()?.assert_success().assert_stdout("0\tcpio\n"); Ok(()) } #[test] fn file_doesnt_exist() -> Result<(), Box> { let mut cmd = get_command(); cmd.arg("-t").arg("test/file/does/not/exist"); cmd.output()? .assert_failure(1) .assert_stderr_contains("No such file or directory") .assert_stdout(""); Ok(()) } #[test] fn list_content_compressed_cpio() -> Result<(), Box> { for compression in ["bzip2", "gzip", "lz4", "lzma", "lzop", "xz", "zstd"] { let mut cmd = get_command(); cmd.arg("-t").arg(format!("tests/{}.cpio", compression)); cmd.output()? .assert_stderr("") .assert_success() .assert_stdout(".\npath\npath/file\n.\nusr\nusr/bin\nusr/bin/sh\n"); } Ok(()) } #[test] fn list_content_single_cpio() -> Result<(), Box> { let mut cmd = get_command(); cmd.arg("-t").arg("tests/single.cpio"); cmd.output()? .assert_stderr("") .assert_success() .assert_stdout(".\npath\npath/file\n"); Ok(()) } #[test] fn missing_file_argument() -> Result<(), Box> { let mut cmd = get_command(); cmd.arg("-t"); cmd.output()? .assert_failure(2) .assert_stderr_contains("missing argument FILE") .assert_stdout(""); Ok(()) } #[test] fn print_version() -> Result<(), Box> { let mut cmd = get_command(); cmd.arg("--version"); let stdout = cmd.output()?.assert_stderr("").assert_success().stdout; let stdout = String::from_utf8(stdout).expect("stdout"); let words: Vec<&str> = stdout.split_whitespace().collect(); assert_eq!(words.len(), 2, "not two words: '{}'", stdout); assert_eq!(words[0], "3cpio"); let version = words[1]; // Simple implementation for regular expression match: [0-9.]+ let mut matches = String::from(version); matches.retain(|c| c.is_ascii_digit() || c == '.'); assert_eq!(matches, version); Ok(()) } threecpio-0.5.1/tests/generate000075500000000000000000000031521046102023000144740ustar 00000000000000#!/bin/sh set -eu # Copyright (C) 2024, Benjamin Drung # SPDX-License-Identifier: ISC # Generate the test cpio files export SOURCE_DATE_EPOCH=1713104326 # Generate the test data generate_cpio() { input_dir="$1" find "$input_dir" -depth -exec touch --no-dereference --date="@${SOURCE_DATE_EPOCH}" {} \; { cd "$input_dir"; find .; } | LC_ALL=C sort \ | cpio --reproducible --quiet -o -H newc -D "$input_dir" } cd "$(dirname "$0")" input="$(mktemp -d "${TMPDIR-/tmp}/3cpio_XXXXXX")" trap 'rm -rf "${input}"' 0 1 2 3 6 mkdir -p "$input/single/path" echo "content" > "$input/single/path/file" generate_cpio "${input}/single" > single.cpio mkdir -p "$input/shell/usr/bin/" echo "This is a fake busybox binary to simulate a POSIX shell" > "$input/shell/usr/bin/sh" generate_cpio "${input}/shell" > "$input/shell.cpio" touch --date="@${SOURCE_DATE_EPOCH}" "$input/shell.cpio" cp single.cpio bzip2.cpio bzip2 -9 < "$input/shell.cpio" >> bzip2.cpio cp single.cpio gzip.cpio gzip -n -9 < "$input/shell.cpio" >> gzip.cpio cp single.cpio lz4.cpio lz4 -l -9 < "$input/shell.cpio" >> lz4.cpio cp single.cpio lzma.cpio lzma -9 < "$input/shell.cpio" >> lzma.cpio cp single.cpio lzop.cpio lzop -9 -c "$input/shell.cpio" >> lzop.cpio cp single.cpio xz.cpio xz --check=crc32 --threads=1 -9 < "$input/shell.cpio" >> xz.cpio cp single.cpio zstd.cpio zstd -q -9 < "$input/shell.cpio" >> zstd.cpio mkdir "$input/path-traversal" ln -s /tmp "$input/path-traversal/tmp" echo "TEST Traversal" > "$input/path-traversal/tmpYtrav.txt" generate_cpio "$input/path-traversal" | sed "s@tmpY@tmp/@g" > path-traversal.cpio threecpio-0.5.1/tests/gzip.cpio000064400000000000000000000013001046102023000145720ustar 0000000000000007070100000000000041FD000003E8000003E800000002661BE5C600000000000000000000000000000000000000000000000200000000.07070100000001000041FD000003E8000003E800000002661BE5C600000000000000000000000000000000000000000000000500000000path07070100000002000081B4000003E8000003E800000001661BE5C600000008000000000000000000000000000000000000000A00000000path/filecontent 07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!3070704@&n.` m``dffjlf@01 3}&0FiqF4Nǿkm%;&0|yR(Etwq􏵃—Ż&vthreecpio-0.5.1/tests/lzop.cpio000064400000000000000000000014131046102023000146120ustar 0000000000000007070100000000000041FD000003E8000003E800000002661BE5C600000000000000000000000000000000000000000000000200000000.07070100000001000041FD000003E8000003E800000002661BE5C600000000000000000000000000000000000000000000000500000000path07070100000002000081B4000003E8000003E800000001661BE5C600000008000000000000000000000000000000000000000A00000000path/filecontent 07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!LZO  @ @  f shell.cpio#Y07F10)41FD3E8+ 002661BE5C60 2.+1 64usr,2 68)/bin.31B451,38 B-/shHThis H a fake busybox @ ary to simulateD POSIX shell 0<910 'TRAILER!!! Wthreecpio-0.5.1/tests/path-traversal.cpio000064400000000000000000000010001046102023000165530ustar 0000000000000007070100000000000041FD000003E8000003E800000002661BE5C600000000000000000000000000000000000000000000000200000000.070701000000010000A1FF000003E8000003E800000001661BE5C600000004000000000000000000000000000000000000000400000000tmp/tmp07070100000002000081B4000003E8000003E800000001661BE5C60000000F000000000000000000000000000000000000000D00000000tmp/trav.txtTEST Traversal 07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!threecpio-0.5.1/tests/single.cpio000064400000000000000000000010001046102023000150770ustar 0000000000000007070100000000000041FD000003E8000003E800000002661BE5C600000000000000000000000000000000000000000000000200000000.07070100000001000041FD000003E8000003E800000002661BE5C600000000000000000000000000000000000000000000000500000000path07070100000002000081B4000003E8000003E800000001661BE5C600000008000000000000000000000000000000000000000A00000000path/filecontent 07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!threecpio-0.5.1/tests/xz.cpio000064400000000000000000000013341046102023000142710ustar 0000000000000007070100000000000041FD000003E8000003E800000002661BE5C600000000000000000000000000000000000000000000000200000000.07070100000001000041FD000003E8000003E800000002661BE5C600000000000000000000000000000000000000000000000500000000path07070100000002000081B4000003E8000003E800000001661BE5C600000008000000000000000000000000000000000000000A00000000path/filecontent 07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!7zXZi"6!X] c 6JDHnLҤe#B ؑ"̓՟=i ,"зZ;Wnm=甆)0Ӌ䷥>ǿkm%;&0|yR(Etwq􏵃—Ż&vٹ#9>0 YZthreecpio-0.5.1/tests/zstd.cpio000064400000000000000000000012651046102023000146170ustar 0000000000000007070100000000000041FD000003E8000003E800000002661BE5C600000000000000000000000000000000000000000000000200000000.07070100000001000041FD000003E8000003E800000002661BE5C600000000000000000000000000000000000000000000000500000000path07070100000002000081B4000003E8000003E800000001661BE5C600000008000000000000000000000000000000000000000A00000000path/filecontent 07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!(/`EG`(>"{k}daPlBHr0nZA,L;yy( L9&T)?"Z=EiLO_fwy$.:lPdS72{V0k邏 t0ZkzcV'v+8Hlll\#'