threecpio-0.3.2/.cargo_vcs_info.json0000644000000001360000000000100130200ustar { "git": { "sha1": "97d53d02a92c57444e43348c1130b02e7e5c6fa4" }, "path_in_vcs": "" }threecpio-0.3.2/.github/workflows/ci.yaml000064400000000000000000000021461046102023000164670ustar 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.3.2/.gitignore000064400000000000000000000000101046102023000135670ustar 00000000000000/target threecpio-0.3.2/Cargo.lock0000644000000010740000000000100107750ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "lexopt" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baff4b617f7df3d896f97fe922b64817f6cd9a756bb81d40f8883f2f66dcb401" [[package]] name = "libc" version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" [[package]] name = "threecpio" version = "0.3.2" dependencies = [ "lexopt", "libc", ] threecpio-0.3.2/Cargo.toml0000644000000020050000000000100110130ustar # 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.3.2" authors = ["Benjamin Drung "] 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" [[bin]] name = "3cpio" path = "src/main.rs" [dependencies.lexopt] version = "0.3" [dependencies.libc] version = "0.2" threecpio-0.3.2/Cargo.toml.orig000064400000000000000000000007601046102023000145020ustar 00000000000000[package] name = "threecpio" version = "0.3.2" 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.3.2/LICENSE000075500000000000000000000014161046102023000126220ustar 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.3.2/NEWS.md000064400000000000000000000031061046102023000127060ustar 00000000000000This file summarizes the major and interesting changes for each release. For a detailed list of changes, please see the git history. 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.3.2/README.md000064400000000000000000000135111046102023000130700ustar 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 -------------- 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.3.2/doc/Benchmarks.md000064400000000000000000000364351046102023000147670ustar 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.3.2/src/lib.rs000064400000000000000000001107431046102023000135210ustar 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, Permissions, }; 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, PermissionsExt}; use std::process::ChildStdout; use std::process::Command; use std::process::Stdio; use std::time::SystemTime; use crate::libc::{set_modified, strftime_local}; mod libc; const CPIO_HEADER_LENGTH: u32 = 110; const CPIO_MAGIC_NUMBER: [u8; 6] = *b"070701"; const PIPE_SIZE: usize = 65536; const MODE_PERMISSION_MASK: u32 = 0o007_777; const MODE_FILETYPE_MASK: u32 = 0o770_000; const FILETYPE_FIFO: u32 = 0o010_000; const FILETYPE_CHARACTER_DEVICE: u32 = 0o020_000; const FILETYPE_DIRECTORY: u32 = 0o040_000; const FILETYPE_BLOCK_DEVICE: u32 = 0o060_000; const FILETYPE_REGULAR_FILE: u32 = 0o100_000; const FILETYPE_SYMLINK: u32 = 0o120_000; const FILETYPE_SOCKET: u32 = 0o140_000; pub const LOG_LEVEL_WARNING: u32 = 5; pub const LOG_LEVEL_INFO: u32 = 7; pub const LOG_LEVEL_DEBUG: u32 = 8; 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(()) } } struct CpioFilenameReader<'a, R: Read + SeekForward> { file: &'a mut R, } impl<'a, R: Read + SeekForward> Iterator for CpioFilenameReader<'a, 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), } } } #[derive(Debug, PartialEq)] struct Header { ino: u32, mode: u32, uid: u32, gid: u32, nlink: u32, mtime: u32, filesize: u32, major: u32, minor: u32, // unused //rmajor: u32, //rminor: u32, filename: String, } impl Header { // Return major and minor combined as u64 fn dev(&self) -> u64 { u64::from(self.major) << 32 | u64::from(self.minor) } fn mode_perm(&self) -> u32 { self.mode & MODE_PERMISSION_MASK } // ls-style ASCII representation of the mode 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'-', }, ] } 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()) } fn mark_seen(&self, seen_files: &mut SeenFiles) { seen_files.insert(self.ino_and_dev(), self.filename.clone()); } 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()) } fn skip_file_content(&self, file: &mut R) -> Result<()> { let skip = self.filesize + align_to_4_bytes(self.filesize); file.seek_forward(skip.into())?; Ok(()) } } 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 } } 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()) } 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 read_cpio_header(file: &mut R) -> std::io::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(Header { 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, }) } /// 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 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)?; 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 read_cpio_header(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)?; }; if 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 )?; } else { 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_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 try_get_hard_link_target<'a>(header: &Header, seen_files: &'a SeenFiles) -> Option<&'a String> { if header.nlink <= 1 { return None; } seen_files.get(&header.ino_and_dev()) } 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) = try_get_hard_link_target(header, 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 read_cpio_and_extract( file: &mut R, preserve_permissions: bool, log_level: u32, ) -> Result<()> { let mut extractor = Extractor::new(); loop { let header = match read_cpio_header(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)?; } match header.mode & MODE_FILETYPE_MASK { 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_CHARACTER_DEVICE | 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 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, preserve_permissions, log_level)?; } else { let mut decompressed = decompress(&mut command, file)?; read_cpio_and_extract(&mut decompressed, 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; use super::*; use std::os::unix::fs::MetadataExt; fn getgid() -> u32 { unsafe { ::libc::getgid() } } fn getuid() -> u32 { unsafe { ::libc::getuid() } } extern "C" { fn tzset(); } 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_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_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_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_read_cpio_header() { // Wrapped before mtime and filename let cpio_data = b"07070100000002000081B4000003E8000007D000000001\ 661BE5C600000008000000000000000000000000000000000000000A00000000\ path/file\0content\0"; let header = read_cpio_header(&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, filename: "path/file".into() } ) } #[test] fn test_read_cpio_header_invalid_magic_number() { let invalid_data = b"abc\tefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\ abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; let got = read_cpio_header(&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_write_directory_with_setuid() { let mut mtimes = BTreeMap::new(); let header = Header { ino: 1, mode: 0o43_777, uid: getuid(), gid: getgid(), nlink: 0, mtime: 1720081471, filesize: 0, major: 0, minor: 0, filename: "./directory_with_setuid".into(), }; 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 { ino: 1, mode: 0o104_755, uid: getuid(), gid: getgid(), nlink: 0, mtime: 1720081471, filesize: 9, major: 0, minor: 0, filename: "./file_with_setuid".into(), }; 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 { ino: 1, mode: 0o120_777, uid: getuid(), gid: getgid(), nlink: 0, mtime: 1721427072, filesize: 12, major: 0, minor: 0, filename: "./dead_symlink".into(), }; 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.3.2/src/libc.rs000064400000000000000000000135751046102023000136710ustar 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())) } 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.3.2/src/main.rs000064400000000000000000000157321046102023000137010ustar 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, LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_WARNING, }; #[derive(Debug)] struct Args { 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} {{-e|--examine}} FILE {executable} {{-t|--list}} FILE {executable} {{-x|--extract}} [-v|--debug] [-C DIR] [-p] [-s NAME] [--force] FILE Optional arguments: -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 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 { 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 examine + extract + list != 1 { return Err("Either --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 { 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.examine { ("examine", examine_cpio_content(file, &mut stdout)) } else if args.extract { ( "extract", extract_cpio_archive(file, args.preserve_permissions, args.subdir, args.log_level), ) } else if args.list { ("list", 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 {} content of '{}': {}", executable, operation, args.file, e ); return ExitCode::FAILURE; } } } ExitCode::SUCCESS } threecpio-0.3.2/tests/bzip2.cpio000064400000000000000000000013411046102023000146530ustar 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.3.2/tests/cli.rs000064400000000000000000000107771046102023000141030ustar 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 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.3.2/tests/generate000075500000000000000000000027601046102023000144770ustar 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 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" find "${input}/single" -depth -exec touch --no-dereference --date="@${SOURCE_DATE_EPOCH}" {} \; { cd "$input/single"; find .; } | LC_ALL=C sort \ | cpio --reproducible --quiet -o -H newc -D "$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" find "${input}/shell" -depth -exec touch --no-dereference --date="@${SOURCE_DATE_EPOCH}" {} \; { cd "$input/shell"; find .; } | LC_ALL=C sort \ | cpio --reproducible --quiet -o -H newc -D "$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 -9 < "$input/shell.cpio" >> xz.cpio cp single.cpio zstd.cpio zstd -q -9 < "$input/shell.cpio" >> zstd.cpio threecpio-0.3.2/tests/gzip.cpio000064400000000000000000000013001046102023000145710ustar 0000000000000007070100000000000041FD000003E8000003E800000002661BE5C600000000000000000000000000000000000000000000000200000000.07070100000001000041FD000003E8000003E800000002661BE5C600000000000000000000000000000000000000000000000500000000path07070100000002000081B4000003E8000003E800000001661BE5C600000008000000000000000000000000000000000000000A00000000path/filecontent 07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!3070704@&n.` m``dffjlf@01 3}&0FiqF4Nǿkm%;&0|yR(Etwq􏵃—Ż&vthreecpio-0.3.2/tests/lzop.cpio000064400000000000000000000014131046102023000146110ustar 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.3.2/tests/single.cpio000064400000000000000000000010001046102023000150760ustar 0000000000000007070100000000000041FD000003E8000003E800000002661BE5C600000000000000000000000000000000000000000000000200000000.07070100000001000041FD000003E8000003E800000002661BE5C600000000000000000000000000000000000000000000000500000000path07070100000002000081B4000003E8000003E800000001661BE5C600000008000000000000000000000000000000000000000A00000000path/filecontent 07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!threecpio-0.3.2/tests/xz.cpio000064400000000000000000000013341046102023000142700ustar 0000000000000007070100000000000041FD000003E8000003E800000002661BE5C600000000000000000000000000000000000000000000000200000000.07070100000001000041FD000003E8000003E800000002661BE5C600000000000000000000000000000000000000000000000500000000path07070100000002000081B4000003E8000003E800000001661BE5C600000008000000000000000000000000000000000000000A00000000path/filecontent 07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!7zXZi"6!X] c 6JDHnLҤe#B ؑ"̓՟=i ,"зZ;Wnm=甆)0Ӌ䷥>ǿkm%;&0|yR(Etwq􏵃—Ż&vٹ#9>0 YZthreecpio-0.3.2/tests/zstd.cpio000064400000000000000000000012651046102023000146160ustar 0000000000000007070100000000000041FD000003E8000003E800000002661BE5C600000000000000000000000000000000000000000000000200000000.07070100000001000041FD000003E8000003E800000002661BE5C600000000000000000000000000000000000000000000000500000000path07070100000002000081B4000003E8000003E800000001661BE5C600000008000000000000000000000000000000000000000A00000000path/filecontent 07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!(/`EG`(>"{k}daPlBHr0nZA,L;yy( L9&T)?"Z=EiLO_fwy$.:lPdS72{V0k邏 t0ZkzcV'v+8Hlll\#'