threecpio-0.8.1/.cargo_vcs_info.json0000644000000001360000000000100130240ustar { "git": { "sha1": "0fe9e652e484b0b00be8a4869e01ad476c26fcad" }, "path_in_vcs": "" }threecpio-0.8.1/.github/workflows/ci.yaml000064400000000000000000000041571046102023000164770ustar 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 }} - name: Install cargo-llvm-cov uses: taiki-e/install-action@cargo-llvm-cov - run: cargo build --verbose - run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info - name: Upload coverage uses: actions/upload-artifact@v4 with: name: coverage-${{ matrix.toolchain }} path: ./lcov.info 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 upload-to-codecov: if: ${{ always() }} needs: - build_and_test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Download artifacts uses: actions/download-artifact@v4 - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true man: runs-on: ubuntu-latest steps: - name: Install asciidoctor run: > sudo apt-get update && sudo apt-get install --no-install-recommends --yes asciidoctor - uses: actions/checkout@v4 - name: Build man pages run: asciidoctor -b manpage man/3cpio.1.adoc threecpio-0.8.1/.gitignore000064400000000000000000000000141046102023000135770ustar 00000000000000*.1 /target threecpio-0.8.1/Cargo.lock0000644000000010740000000000100110010ustar # 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.174" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "threecpio" version = "0.8.1" dependencies = [ "lexopt", "libc", ] threecpio-0.8.1/Cargo.toml0000644000000023120000000000100110200ustar # 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.8.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.8.1/Cargo.toml.orig000064400000000000000000000007601046102023000145060ustar 00000000000000[package] name = "threecpio" version = "0.8.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.8.1/LICENSE000075500000000000000000000014161046102023000126260ustar 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.8.1/NEWS.md000064400000000000000000000057601046102023000127220ustar 00000000000000This file summarizes the major and interesting changes for each release. For a detailed list of changes, please see the git history. 0.8.0 (2025-07-31) ------------------ ### Fixed * test: - use temporary directory for write tests - canonicalize `/dev/console` ([bug #16](https://github.com/bdrung/3cpio/issues/16)) 0.8.0 (2025-07-11) ------------------ ### What's new * Use a write buffer for `--create` for a massive performance improvement ### Fixed * Check exit status of compressor commands * test: - Use `gzip` instead of `true` which might be a symlink - Skip `test_file_from_line_location_*` if required file is missing 0.7.0 (2025-07-10) ------------------ ### What's new * Add support for creating cpio archives from a manifest file ([feature #3](https://github.com/bdrung/3cpio/issues/3)) * Print inode on `--list --debug` 0.6.0 (2025-06-30) ------------------ ### What's new * doc: add 3cpio man page ### Fixed * Fix "No such file or directory" error when using `--subdir` * test: fix race condition in tests by using a lock 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.8.1/README.md000064400000000000000000000222551046102023000131010ustar 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. 3cpio supports creating, 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 ``` Create a cpio archive similar to the other cpio tools using the `find` command: ``` $ cd inputdir && find . | sort | 3cpio --create ../example.cpio ``` Due to its manifest file format support, 3cpio can create cpio archives without the need of copying files into a temporary directory first. Example for creating an early microcode cpio image directly using the system installed files: ``` $ cat manifest - kernel dir 755 0 0 1751654557 - kernel/x86 dir 755 0 0 1752011622 /usr/lib/firmware/amd-ucode kernel/x86/microcode /usr/lib/firmware/amd-ucode/microcode_amd_fam19h.bin kernel/x86/microcode/AuthenticAMD.bin $ 3cpio --create amd-ucode.img < manifest $ 3cpio --list --verbose amd-ucode.img drwxr-xr-x 2 root root 0 Jul 4 20:42 kernel drwxr-xr-x 2 root root 0 Jul 8 23:53 kernel/x86 drwxr-xr-x 2 root root 0 Jun 10 10:51 kernel/x86/microcode -rw-r--r-- 1 root root 100684 Mar 23 22:42 kernel/x86/microcode/AuthenticAMD.bin ``` Example for creating an initrd image containing of an uncompressed early microcode cpio followed by a Zstandard-compressed cpio: ``` $ cat manifest #cpio - kernel dir 755 0 0 1751654557 - kernel/x86 dir 755 0 0 1752011622 /usr/lib/firmware/amd-ucode kernel/x86/microcode /usr/lib/firmware/amd-ucode/microcode_amd_fam19h.bin kernel/x86/microcode/AuthenticAMD.bin #cpio: zstd -9 / /bin /usr /usr/bin /usr/bin/bash # This is a comment. Leaving the remaining files as task for the reader. $ 3cpio --create initrd.img < manifest $ 3cpio --examine initrd.img 0 cpio 101332 zstd $ 3cpio --list --verbose initrd.img drwxr-xr-x 2 root root 0 Jul 4 20:42 kernel drwxr-xr-x 2 root root 0 Jul 8 23:53 kernel/x86 drwxr-xr-x 2 root root 0 Jun 10 10:51 kernel/x86/microcode -rw-r--r-- 1 root root 100684 Mar 23 22:42 kernel/x86/microcode/AuthenticAMD.bin drwxr-xr-x 2 root root 0 Jun 5 14:11 . lrwxrwxrwx 1 root root 7 Mar 20 2022 bin -> usr/bin drwxr-xr-x 2 root root 0 Apr 20 2023 usr drwxr-xr-x 2 root root 0 Jul 9 09:56 usr/bin -rwxr-xr-x 1 root root 1740896 Mar 5 03:35 usr/bin/bash ``` 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). ### Creating cpio archives 3cpio is the fastest tool by far in all tested scenarios (the other tools are 1.13 to 4.48 times slower with a cold cache and 1.52 to 5.87 times slower with a warm cache): | System | Distro | Kernel | Size | Cache | 3cpio | bsdcpio | cpio | | ------------- | ------ | ----------------- | ------ | ----- | ------- | ------- | ------- | | Ryzen 7 5700G | noble* | 6.8.0-63-generic | 84 MB | warm | 0.061s | 0.237s | 0.323s | | Ryzen 7 5700G | noble* | 6.8.0-63-generic | 84 MB | cold | 0.068s | 0.257s | 0.337s | | Ryzen 7 5700G | plucky | 6.14.0-23-generic | 68 MB | warm | 0.065s | 0.299s | 0.383s | | Ryzen 7 5700G | plucky | 6.14.0-23-generic | 68 MB | cold | 0.257s | 0.491s | 0.559s | | RasPi Zero 2W | noble | 6.8.0-1030-raspi | 80 MB | warm | 2.460s | 3.733s | 4.833s | | RasPi Zero 2W | noble | 6.8.0-1030-raspi | 80 MB | cold | 10.743s | 12.200s | 12.154s | The Ryzen 7 5700G noble tests were done in chroots with tmpfs. 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.8.1/doc/Benchmarks.md000064400000000000000000001101701046102023000147600ustar 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 | Benchmark results on a Raspberry Pi Zero 2W running Ubuntu 24.04 (noble) arm64 on 2025-07-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 80422400 Jul 6 11:49 initrd.img $ 3cpio -t initrd.img | wc -l 2542 $ 3cpio -e initrd.img 0 cpio $ 3cpio -e /boot/initrd.img 0 cpio 42943488 zstd $ hyperfine -N -w 2 -r 100 "3cpio -t initrd.img" "3cpio -tv initrd.img" "3cpio -t --debug initrd.img" "3cpio -t /boot/initrd.img" "3cpio -tv /boot/initrd.img" "3cpio -t --debug /boot/initrd.img" --export-markdown list-variants.md Benchmark 1: 3cpio -t initrd.img Time (mean ± σ): 89.7 ms ± 1.1 ms [User: 26.8 ms, System: 61.7 ms] Range (min … max): 87.6 ms … 92.8 ms 100 runs Benchmark 2: 3cpio -tv initrd.img Time (mean ± σ): 112.4 ms ± 1.2 ms [User: 47.8 ms, System: 63.4 ms] Range (min … max): 110.4 ms … 115.2 ms 100 runs Benchmark 3: 3cpio -t --debug initrd.img Time (mean ± σ): 114.3 ms ± 1.1 ms [User: 49.4 ms, System: 63.6 ms] Range (min … max): 112.1 ms … 117.7 ms 100 runs Benchmark 4: 3cpio -t /boot/initrd.img Time (mean ± σ): 703.8 ms ± 2.6 ms [User: 39.4 ms, System: 267.5 ms] Range (min … max): 699.1 ms … 712.0 ms 100 runs Benchmark 5: 3cpio -tv /boot/initrd.img Time (mean ± σ): 722.5 ms ± 3.1 ms [User: 61.9 ms, System: 268.3 ms] Range (min … max): 715.9 ms … 742.8 ms 100 runs Benchmark 6: 3cpio -t --debug /boot/initrd.img Time (mean ± σ): 724.4 ms ± 2.5 ms [User: 65.6 ms, System: 267.1 ms] Range (min … max): 719.2 ms … 733.6 ms 100 runs Summary 3cpio -t initrd.img ran 1.25 ± 0.02 times faster than 3cpio -tv initrd.img 1.27 ± 0.02 times faster than 3cpio -t --debug initrd.img 7.85 ± 0.10 times faster than 3cpio -t /boot/initrd.img 8.05 ± 0.10 times faster than 3cpio -tv /boot/initrd.img 8.08 ± 0.10 times faster than 3cpio -t --debug /boot/initrd.img ``` | Command | Mean [ms] | Min [ms] | Max [ms] | Relative | |:---|---:|---:|---:|---:| | `3cpio -t initrd.img` | 89.7 ± 1.1 | 87.6 | 92.8 | 1.00 | | `3cpio -tv initrd.img` | 112.4 ± 1.2 | 110.4 | 115.2 | 1.25 ± 0.02 | | `3cpio -t --debug initrd.img` | 114.3 ± 1.1 | 112.1 | 117.7 | 1.27 ± 0.02 | | `3cpio -t /boot/initrd.img` | 703.8 ± 2.6 | 699.1 | 712.0 | 7.85 ± 0.10 | | `3cpio -tv /boot/initrd.img` | 722.5 ± 3.1 | 715.9 | 742.8 | 8.05 ± 0.10 | | `3cpio -t --debug /boot/initrd.img` | 724.4 ± 2.5 | 719.2 | 733.6 | 8.08 ± 0.10 | Benchmark results on a Raspberry Pi Zero 2W running Ubuntu 24.04 (noble) arm64 on 2025-07-10: ``` $ ls -l /boot/initrd.img* lrwxrwxrwx 1 root root 27 Jul 3 08:18 /boot/initrd.img -> initrd.img-6.8.0-1030-raspi -rw-r--r-- 1 root root 57286143 Jul 3 08:23 /boot/initrd.img-6.8.0-1030-raspi $ sudo 3cpio -x /boot/initrd.img -C initrd $ ( cd initrd && find . ) | sed -e 's,\./,,g' | sort > files $ wc -l < files 2542 $ sudo hyperfine -w 1 -p "rm -f initrd.img && sync && echo 3 > /proc/sys/vm/drop_caches" "3cpio -c initrd.img -C initrd < files" "cd initrd && bsdcpio -o -H newc > ../initrd.img < ../files" "cd initrd && cpio -o -H newc --reproducible > ../initrd.img < ../files" --export-markdown create-cold.md Benchmark 1: 3cpio -c initrd.img -C initrd < files Time (mean ± σ): 10.743 s ± 0.213 s [User: 0.140 s, System: 2.264 s] Range (min … max): 10.470 s … 11.176 s 10 runs Benchmark 2: cd initrd && bsdcpio -o -H newc > ../initrd.img < ../files Time (mean ± σ): 12.200 s ± 0.339 s [User: 0.576 s, System: 4.840 s] Range (min … max): 11.603 s … 12.749 s 10 runs Benchmark 3: cd initrd && cpio -o -H newc --reproducible > ../initrd.img < ../files Time (mean ± σ): 12.154 s ± 0.502 s [User: 0.839 s, System: 5.494 s] Range (min … max): 11.549 s … 12.946 s 10 runs Summary 3cpio -c initrd.img -C initrd < files ran 1.13 ± 0.05 times faster than cd initrd && cpio -o -H newc --reproducible > ../initrd.img < ../files 1.14 ± 0.04 times faster than cd initrd && bsdcpio -o -H newc > ../initrd.img < ../files $ sudo hyperfine -w 2 -p "rm -f initrd.img && sync" "3cpio -c initrd.img -C initrd < files" "cd initrd && bsdcpio -o -H newc > ../initrd.img < ../files" "cd initrd && cpio -o -H newc --reproducible > ../initrd.img < ../files" --export-markdown create-warm.md Benchmark 1: 3cpio -c initrd.img -C initrd < files Time (mean ± σ): 2.460 s ± 0.192 s [User: 0.103 s, System: 1.129 s] Range (min … max): 2.266 s … 2.778 s 10 runs Benchmark 2: cd initrd && bsdcpio -o -H newc > ../initrd.img < ../files Time (mean ± σ): 3.733 s ± 0.013 s [User: 0.453 s, System: 3.257 s] Range (min … max): 3.716 s … 3.762 s 10 runs Benchmark 3: cd initrd && cpio -o -H newc --reproducible > ../initrd.img < ../files Time (mean ± σ): 4.833 s ± 0.009 s [User: 0.737 s, System: 4.069 s] Range (min … max): 4.821 s … 4.845 s 10 runs Summary 3cpio -c initrd.img -C initrd < files ran 1.52 ± 0.12 times faster than cd initrd && bsdcpio -o -H newc > ../initrd.img < ../files 1.96 ± 0.15 times faster than cd initrd && cpio -o -H newc --reproducible > ../initrd.img < ../files $ stat -c %s initrd.img 80422400 $ { echo "#cpio: zstd -1" && cat files; } > manifest $ sudo hyperfine -w 1 -p "rm -f initrd.img && sync && echo 3 > /proc/sys/vm/drop_caches" "3cpio -c initrd.img -C initrd < manifest" "cd initrd && bsdcpio -o -H newc < ../files | zstd -q1 -T0 > ../initrd.img" "cd initrd && cpio -o -H newc --reproducible < ../files | zstd -q1 -T0 > ../initrd.img" --export-markdown create-cold.md Benchmark 1: 3cpio -c initrd.img -C initrd < manifest Time (mean ± σ): 8.667 s ± 0.291 s [User: 0.197 s, System: 2.036 s] Range (min … max): 8.364 s … 9.127 s 10 runs Benchmark 2: cd initrd && bsdcpio -o -H newc < ../files | zstd -q1 -T0 > ../initrd.img Time (mean ± σ): 10.367 s ± 0.891 s [User: 3.300 s, System: 5.913 s] Range (min … max): 9.507 s … 11.742 s 10 runs Benchmark 3: cd initrd && cpio -o -H newc --reproducible < ../files | zstd -q1 -T0 > ../initrd.img Time (mean ± σ): 10.276 s ± 0.921 s [User: 3.612 s, System: 7.762 s] Range (min … max): 9.461 s … 12.092 s 10 runs Summary 3cpio -c initrd.img -C initrd < manifest ran 1.19 ± 0.11 times faster than cd initrd && cpio -o -H newc --reproducible < ../files | zstd -q1 -T0 > ../initrd.img 1.20 ± 0.11 times faster than cd initrd && bsdcpio -o -H newc < ../files | zstd -q1 -T0 > ../initrd.img $ sudo hyperfine -w 1 -p "rm -f initrd.img && sync" "3cpio -c initrd.img -C initrd < manifest" "cd initrd && bsdcpio -o -H newc < ../files | zstd -q1 -T0 > ../initrd.img" "cd initrd && cpio -o -H newc --reproducible < ../files | zstd -q1 -T0 > ../initrd.img" --export-markdown create-warm.md Benchmark 1: 3cpio -c initrd.img -C initrd < manifest Time (mean ± σ): 2.107 s ± 0.087 s [User: 0.153 s, System: 0.942 s] Range (min … max): 2.024 s … 2.260 s 10 runs Benchmark 2: cd initrd && bsdcpio -o -H newc < ../files | zstd -q1 -T0 > ../initrd.img Time (mean ± σ): 2.874 s ± 0.029 s [User: 3.237 s, System: 4.182 s] Range (min … max): 2.801 s … 2.903 s 10 runs Benchmark 3: cd initrd && cpio -o -H newc --reproducible < ../files | zstd -q1 -T0 > ../initrd.img Time (mean ± σ): 3.785 s ± 0.012 s [User: 3.428 s, System: 5.966 s] Range (min … max): 3.767 s … 3.801 s 10 runs Summary 3cpio -c initrd.img -C initrd < manifest ran 1.36 ± 0.06 times faster than cd initrd && bsdcpio -o -H newc < ../files | zstd -q1 -T0 > ../initrd.img 1.80 ± 0.07 times faster than cd initrd && cpio -o -H newc --reproducible < ../files | zstd -q1 -T0 > ../initrd.img $ stat -c %s initrd.img 57021773 ``` Cold caches: | Command | Mean [s] | Min [s] | Max [s] | Relative | |:---|---:|---:|---:|---:| | `3cpio -c initrd.img -C initrd < files` | 10.743 ± 0.213 | 10.470 | 11.176 | 1.00 | | `cd initrd && bsdcpio -o -H newc > ../initrd.img < ../files` | 12.200 ± 0.339 | 11.603 | 12.749 | 1.14 ± 0.04 | | `cd initrd && cpio -o -H newc --reproducible > ../initrd.img < ../files` | 12.154 ± 0.502 | 11.549 | 12.946 | 1.13 ± 0.05 | | Command | Mean [s] | Min [s] | Max [s] | Relative | |:---|---:|---:|---:|---:| | `3cpio -c initrd.img -C initrd < manifest` | 8.667 ± 0.291 | 8.364 | 9.127 | 1.00 | | `cd initrd && bsdcpio -o -H newc < ../files \| zstd -q1 -T0 > ../initrd.img` | 10.367 ± 0.891 | 9.507 | 11.742 | 1.20 ± 0.11 | | `cd initrd && cpio -o -H newc --reproducible < ../files \| zstd -q1 -T0 > ../initrd.img` | 10.276 ± 0.921 | 9.461 | 12.092 | 1.19 ± 0.11 | Warm caches (results rely heavily on the available amount of memory): | Command | Mean [s] | Min [s] | Max [s] | Relative | |:---|---:|---:|---:|---:| | `3cpio -c initrd.img -C initrd < files` | 2.460 ± 0.192 | 2.266 | 2.778 | 1.00 | | `cd initrd && bsdcpio -o -H newc > ../initrd.img < ../files` | 3.733 ± 0.013 | 3.716 | 3.762 | 1.52 ± 0.12 | | `cd initrd && cpio -o -H newc --reproducible > ../initrd.img < ../files` | 4.833 ± 0.009 | 4.821 | 4.845 | 1.96 ± 0.15 | | Command | Mean [s] | Min [s] | Max [s] | Relative | |:---|---:|---:|---:|---:| | `3cpio -c initrd.img -C initrd < manifest` | 2.107 ± 0.087 | 2.024 | 2.260 | 1.00 | | `cd initrd && bsdcpio -o -H newc < ../files \| zstd -q1 -T0 > ../initrd.img` | 2.874 ± 0.029 | 2.801 | 2.903 | 1.36 ± 0.06 | | `cd initrd && cpio -o -H newc --reproducible < ../files \| zstd -q1 -T0 > ../initrd.img` | 3.785 ± 0.012 | 3.767 | 3.801 | 1.80 ± 0.07 | The manifest parsing in 3cpio took 740 ms with a cold cache and 140 ms with a warm cache. 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 | Benchmark results on a desktop machine with an AMD Ryzen 7 5700G running Ubuntu 25.04 (plucky) on 2025-07-10. The tests were done in chroots that use overlayfs on tmpfs for writes. ``` $ schroot-wrapper -p initramfs-tools,linux-image-generic,cryptsetup-initramfs,lvm2,kbd,mdadm,ntfs-3g,plymouth,console-setup,libarchive-tools,hyperfine -u root -c noble (noble)root@desktop:~# ls -l /boot/initrd.img* lrwxrwxrwx 1 root root 27 Jul 9 23:47 /boot/initrd.img -> initrd.img-6.8.0-63-generic -rw-r--r-- 1 root root 67139659 Jul 9 23:47 /boot/initrd.img-6.8.0-63-generic (noble)root@desktop:~# 3cpio -x /boot/initrd.img -C initrd (noble)root@desktop:~# ( cd initrd && find . ) | sed -e 's,\./,,g' | sort > files (noble)root@desktop:~# wc -l < files 1901 (noble)root@desktop:~# hyperfine -w 2 -r 100 -p "rm -f initrd.img && sync && echo 3 > /proc/sys/vm/drop_caches" "3cpio -c initrd.img -C initrd < files" "cd initrd && bsdcpio -o -H newc > ../initrd.img < ../files" "cd initrd && cpio -o -H newc --reproducible > ../initrd.img < ../files" --export-markdown create-cold.md Benchmark 1: 3cpio -c initrd.img -C initrd < files Time (mean ± σ): 75.1 ms ± 4.3 ms [User: 3.9 ms, System: 63.8 ms] Range (min … max): 67.5 ms … 80.7 ms 100 runs Benchmark 2: cd initrd && bsdcpio -o -H newc > ../initrd.img < ../files Time (mean ± σ): 270.0 ms ± 6.5 ms [User: 19.1 ms, System: 230.5 ms] Range (min … max): 256.6 ms … 281.4 ms 100 runs Benchmark 3: cd initrd && cpio -o -H newc --reproducible > ../initrd.img < ../files Time (mean ± σ): 336.7 ms ± 5.0 ms [User: 31.2 ms, System: 298.6 ms] Range (min … max): 328.2 ms … 348.8 ms 100 runs Summary 3cpio -c initrd.img -C initrd < files ran 3.59 ± 0.22 times faster than cd initrd && bsdcpio -o -H newc > ../initrd.img < ../files 4.48 ± 0.27 times faster than cd initrd && cpio -o -H newc --reproducible > ../initrd.img < ../files (noble)root@desktop:~# hyperfine -w 2 -r 100 -p "rm -f initrd.img && sync" "3cpio -c initrd.img -C initrd < files" "cd initrd && bsdcpio -o -H newc > ../initrd.img < ../files" "cd initrd && cpio -o -H newc --reproducible > ../initrd.img < ../files" --export-markdown create-warm.md Benchmark 1: 3cpio -c initrd.img -C initrd < files Time (mean ± σ): 60.8 ms ± 0.9 ms [User: 3.9 ms, System: 56.9 ms] Range (min … max): 58.5 ms … 62.5 ms 100 runs Benchmark 2: cd initrd && bsdcpio -o -H newc > ../initrd.img < ../files Time (mean ± σ): 237.2 ms ± 3.2 ms [User: 18.3 ms, System: 218.8 ms] Range (min … max): 231.6 ms … 244.8 ms 100 runs Benchmark 3: cd initrd && cpio -o -H newc --reproducible > ../initrd.img < ../files Time (mean ± σ): 322.5 ms ± 4.1 ms [User: 29.6 ms, System: 292.8 ms] Range (min … max): 315.4 ms … 336.1 ms 100 runs Summary 3cpio -c initrd.img -C initrd < files ran 3.90 ± 0.08 times faster than cd initrd && bsdcpio -o -H newc > ../initrd.img < ../files 5.30 ± 0.10 times faster than cd initrd && cpio -o -H newc --reproducible > ../initrd.img < ../files (noble)root@desktop:~# stat -c %s initrd.img 83589120 (noble)root@desktop:~# { echo "#cpio: zstd -1" && cat files; } > manifest (noble)root@desktop:~# hyperfine -w 2 -r 100 -p "rm -f initrd.img && sync && echo 3 > /proc/sys/vm/drop_caches" "3cpio -c initrd.img -C initrd < manifest" "cd initrd && bsdcpio -o -H newc < ../files | zstd -q1 -T0 > ../initrd.img" "cd initrd && cpio -o -H newc --reproducible < ../files | zstd -q1 -T0 > ../initrd.img" --export-markdown create-cold.md Benchmark 1: 3cpio -c initrd.img -C initrd < manifest Time (mean ± σ): 80.8 ms ± 4.6 ms [User: 6.0 ms, System: 62.7 ms] Range (min … max): 72.9 ms … 89.6 ms 100 runs Benchmark 2: cd initrd && bsdcpio -o -H newc < ../files | zstd -q1 -T0 > ../initrd.img Time (mean ± σ): 179.6 ms ± 6.2 ms [User: 161.9 ms, System: 260.8 ms] Range (min … max): 167.9 ms … 192.9 ms 100 runs Benchmark 3: cd initrd && cpio -o -H newc --reproducible < ../files | zstd -q1 -T0 > ../initrd.img Time (mean ± σ): 322.6 ms ± 4.8 ms [User: 193.4 ms, System: 464.3 ms] Range (min … max): 312.5 ms … 333.1 ms 100 runs Summary 3cpio -c initrd.img -C initrd < manifest ran 2.22 ± 0.15 times faster than cd initrd && bsdcpio -o -H newc < ../files | zstd -q1 -T0 > ../initrd.img 3.99 ± 0.24 times faster than cd initrd && cpio -o -H newc --reproducible < ../files | zstd -q1 -T0 > ../initrd.img (noble)root@desktop:~# hyperfine -w 2 -r 100 -p "rm -f initrd.img && sync" "3cpio -c initrd.img -C initrd < manifest" "cd initrd && bsdcpio -o -H newc < ../files | zstd -q1 -T0 > ../initrd.img" "cd initrd && cpio -o -H newc --reproducible < ../files | zstd -q1 -T0 > ../initrd.img" --export-markdown create-warm.md Benchmark 1: 3cpio -c initrd.img -C initrd < manifest Time (mean ± σ): 63.2 ms ± 1.7 ms [User: 6.6 ms, System: 54.1 ms] Range (min … max): 60.0 ms … 68.1 ms 100 runs Benchmark 2: cd initrd && bsdcpio -o -H newc < ../files | zstd -q1 -T0 > ../initrd.img Time (mean ± σ): 151.7 ms ± 2.3 ms [User: 160.4 ms, System: 248.2 ms] Range (min … max): 147.1 ms … 158.2 ms 100 runs Benchmark 3: cd initrd && cpio -o -H newc --reproducible < ../files | zstd -q1 -T0 > ../initrd.img Time (mean ± σ): 306.1 ms ± 3.0 ms [User: 194.7 ms, System: 456.0 ms] Range (min … max): 300.2 ms … 313.6 ms 100 runs Summary 3cpio -c initrd.img -C initrd < manifest ran 2.40 ± 0.07 times faster than cd initrd && bsdcpio -o -H newc < ../files | zstd -q1 -T0 > ../initrd.img 4.84 ± 0.14 times faster than cd initrd && cpio -o -H newc --reproducible < ../files | zstd -q1 -T0 > ../initrd.img (noble)root@desktop:~# stat -c %s initrd.img 67215993 ``` Cold cache: | Command | Mean [ms] | Min [ms] | Max [ms] | Relative | |:---|---:|---:|---:|---:| | `3cpio -c initrd.img -C initrd < files` | 75.1 ± 4.3 | 67.5 | 80.7 | 1.00 | | `cd initrd && bsdcpio -o -H newc > ../initrd.img < ../files` | 270.0 ± 6.5 | 256.6 | 281.4 | 3.59 ± 0.22 | | `cd initrd && cpio -o -H newc --reproducible > ../initrd.img < ../files` | 336.7 ± 5.0 | 328.2 | 348.8 | 4.48 ± 0.27 | | Command | Mean [ms] | Min [ms] | Max [ms] | Relative | |:---|---:|---:|---:|---:| | `3cpio -c initrd.img -C initrd < manifest` | 80.8 ± 4.6 | 72.9 | 89.6 | 1.00 | | `cd initrd && bsdcpio -o -H newc < ../files \| zstd -q1 -T0 > ../initrd.img` | 179.6 ± 6.2 | 167.9 | 192.9 | 2.22 ± 0.15 | | `cd initrd && cpio -o -H newc --reproducible < ../files \| zstd -q1 -T0 > ../initrd.img` | 322.6 ± 4.8 | 312.5 | 333.1 | 3.99 ± 0.24 | Warm cache: | Command | Mean [ms] | Min [ms] | Max [ms] | Relative | |:---|---:|---:|---:|---:| | `3cpio -c initrd.img -C initrd < files` | 60.8 ± 0.9 | 58.5 | 62.5 | 1.00 | | `cd initrd && bsdcpio -o -H newc > ../initrd.img < ../files` | 237.2 ± 3.2 | 231.6 | 244.8 | 3.90 ± 0.08 | | `cd initrd && cpio -o -H newc --reproducible > ../initrd.img < ../files` | 322.5 ± 4.1 | 315.4 | 336.1 | 5.30 ± 0.10 | | Command | Mean [ms] | Min [ms] | Max [ms] | Relative | |:---|---:|---:|---:|---:| | `3cpio -c initrd.img -C initrd < manifest` | 63.2 ± 1.7 | 60.0 | 68.1 | 1.00 | | `cd initrd && bsdcpio -o -H newc < ../files \| zstd -q1 -T0 > ../initrd.img` | 151.7 ± 2.3 | 147.1 | 158.2 | 2.40 ± 0.07 | | `cd initrd && cpio -o -H newc --reproducible < ../files \| zstd -q1 -T0 > ../initrd.img` | 306.1 ± 3.0 | 300.2 | 313.6 | 4.84 ± 0.14 | The manifest parsing in 3cpio took 21 ms with a cold cache and 6 ms with a warm cache. Benchmark results on a desktop machine with an AMD Ryzen 7 5700G running Ubuntu 25.04 (plucky) on a Samsung SSD 980 PRO NMVe with Dracut on 2025-07-10: ``` $ ls -l /boot/initrd.img* lrwxrwxrwx 1 root root 28 Jul 2 11:35 /boot/initrd.img -> initrd.img-6.14.0-23-generic -rw------- 1 root root 28276693 Jul 4 09:58 /boot/initrd.img-6.14.0-23-generic $ sudo 3cpio -x /boot/initrd.img -C initrd $ ( cd initrd && find . ) | sed -e 's,\./,,g' | sort > files $ wc -l < files 1714 $ sudo hyperfine -w 1 -p "rm -f initrd.img && sync && echo 3 > /proc/sys/vm/drop_caches" "3cpio -c initrd.img -C initrd < files" "cd initrd && bsdcpio -o -H newc > ../initrd.img < ../files" "cd initrd && cpio -o -H newc --reproducible > ../initrd.img < ../files" --export-markdown create-cold.md Benchmark 1: 3cpio -c initrd.img -C initrd < files Time (mean ± σ): 257.2 ms ± 3.4 ms [User: 5.4 ms, System: 111.2 ms] Range (min … max): 252.8 ms … 262.4 ms 10 runs Benchmark 2: cd initrd && bsdcpio -o -H newc > ../initrd.img < ../files Time (mean ± σ): 490.5 ms ± 5.8 ms [User: 19.7 ms, System: 341.8 ms] Range (min … max): 482.1 ms … 497.7 ms 10 runs Benchmark 3: cd initrd && cpio -o -H newc --reproducible > ../initrd.img < ../files Time (mean ± σ): 559.3 ms ± 6.3 ms [User: 33.9 ms, System: 416.1 ms] Range (min … max): 547.5 ms … 570.8 ms 10 runs Summary 3cpio -c initrd.img -C initrd < files ran 1.91 ± 0.03 times faster than cd initrd && bsdcpio -o -H newc > ../initrd.img < ../files 2.17 ± 0.04 times faster than cd initrd && cpio -o -H newc --reproducible > ../initrd.img < ../files $ sudo hyperfine -w 2 -p "rm -f initrd.img && sync" "3cpio -c initrd.img -C initrd < files" "cd initrd && bsdcpio -o -H newc > ../initrd.img < ../files" "cd initrd && cpio -o -H newc --reproducible > ../initrd.img < ../files" --export-markdown create-warm.md Benchmark 1: 3cpio -c initrd.img -C initrd < files Time (mean ± σ): 65.1 ms ± 1.4 ms [User: 3.6 ms, System: 61.4 ms] Range (min … max): 63.1 ms … 68.2 ms 33 runs Benchmark 2: cd initrd && bsdcpio -o -H newc > ../initrd.img < ../files Time (mean ± σ): 298.8 ms ± 3.2 ms [User: 16.0 ms, System: 282.7 ms] Range (min … max): 295.6 ms … 304.3 ms 10 runs Benchmark 3: cd initrd && cpio -o -H newc --reproducible > ../initrd.img < ../files Time (mean ± σ): 382.6 ms ± 7.6 ms [User: 30.8 ms, System: 351.7 ms] Range (min … max): 370.2 ms … 393.2 ms 10 runs Summary 3cpio -c initrd.img -C initrd < files ran 4.59 ± 0.11 times faster than cd initrd && bsdcpio -o -H newc > ../initrd.img < ../files 5.87 ± 0.17 times faster than cd initrd && cpio -o -H newc --reproducible > ../initrd.img < ../files $ stat -c %s initrd.img 68406784 ``` Cold cache: | Command | Mean [ms] | Min [ms] | Max [ms] | Relative | |:---|---:|---:|---:|---:| | `3cpio -c initrd.img -C initrd < files` | 257.2 ± 3.4 | 252.8 | 262.4 | 1.00 | | `cd initrd && bsdcpio -o -H newc > ../initrd.img < ../files` | 490.5 ± 5.8 | 482.1 | 497.7 | 1.91 ± 0.03 | | `cd initrd && cpio -o -H newc --reproducible > ../initrd.img < ../files` | 559.3 ± 6.3 | 547.5 | 570.8 | 2.17 ± 0.04 | Warm cache: | Command | Mean [ms] | Min [ms] | Max [ms] | Relative | |:---|---:|---:|---:|---:| | `3cpio -c initrd.img -C initrd < files` | 65.1 ± 1.4 | 63.1 | 68.2 | 1.00 | | `cd initrd && bsdcpio -o -H newc > ../initrd.img < ../files` | 298.8 ± 3.2 | 295.6 | 304.3 | 4.59 ± 0.11 | | `cd initrd && cpio -o -H newc --reproducible > ../initrd.img < ../files` | 382.6 ± 7.6 | 370.2 | 393.2 | 5.87 ± 0.17 | The manifest parsing in 3cpio took 40 ms with a cold cache and 5 ms with a warm cache. threecpio-0.8.1/man/3cpio.1.adoc000064400000000000000000000234621046102023000144020ustar 000000000000003cpio(1) ======== Benjamin Drung :doctype: manpage :manmanual: 3cpio :mansource: 3cpio 0.5.1 :manversion: 0.5.1 == Name 3cpio - manage initrd cpio archives == Synopsis *3cpio* *--count* _ARCHIVE_ *3cpio* {*-c*|*--create*} [*-v*|*--debug*] [*-C* _DIR_] [_ARCHIVE_] < _manifest_ *3cpio* {*-e*|*--examine*} _ARCHIVE_ *3cpio* {*-t*|*--list*} [*-v*|*--debug*] _ARCHIVE_ *3cpio* {*-x*|*--extract*} [*-v*|*--debug*] [*-C* _DIR_] [*-p*] [*-s* _NAME_] [*--force*] _ARCHIVE_ *3cpio* {*-V*|*--version*} *3cpio* {*-h*|*--help*} == Description *3cpio* is a tool to manage initramfs cpio files for the Linux kernel. The Linux kernel's https://www.kernel.org/doc/html/latest/driver-api/early-userspace/buffer-format.html[initramfs buffer format] 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. Following compression formats are supported: bzip2, gzip, lz4, lzma, lzop, xz, zstd. == Modes *--count* _ARCHIVE_:: Print the number of concatenated cpio archives. *-c*, *--create* [_ARCHIVE_]:: Create a new cpio archive. Read the manifest from the standard input. See the MANIFEST section for the description of the manifest format. Write the cpio archive to standard output or to the specified _ARCHIVE_ file if provided. The permission of the _ARCHIVE_ file will be determined by the permission of the input files (to avoid leaking sensitive information). *-e*, *--examine* _ARCHIVE_:: List the offsets of the cpio archives and their compression. *-t*, *--list* _ARCHIVE_:: List the contents of the cpio archives. By default only the file names are printed. If *--verbose* is specified, the long listing format is used (similar to ls --long). If *--debug* is specified, the inode is printed in addition to the long format. *-x*, *--extract* _ARCHIVE_:: Extract cpio archives. *-V*, *--version*:: Print version number. *-h*, *--help*:: Print help message. == Options *-C* _DIR_, *--directory*=_DIR_:: Change directory before performing any operation, but after opening the _ARCHIVE_. This option is only taken into account in the *--extract* mode. *-p*, *--preserve-permissions*:: Set permissions of extracted files to those recorded in the archive (default for superuser). This option is only taken into account in the *--extract* mode. *-s* _NAME_, *--subdir*=_NAME_:: Extract the cpio archives into separate sub-directories (using the given _NAME_ plus an incrementing number). This option is only taken into account in the *--extract* mode. *-v*, *--verbose*:: Verbose output. This option is only taken into account in the *--extract* and *--list* modes. *--debug*:: Debug output. This option is only taken into account in the *--extract* and *--list* modes. *--force*:: Force overwriting existing files. This option is only taken into account in the *--extract* mode. == Manifest When generating initrd cpio archives, following manifest format will be used. The manifest is a text format that is parsed line by line. If the line starts with _#cpio_ it is interpreted as section marker to start a new cpio. A compression may be specified by adding a colon followed by the compression format and an optional compression level. Example for a Zstandard-compressed cpio with compression level 9: ---- #cpio: zstd -9 ---- All lines starting with _#_ excluding _#cpio_ (see above) will be treated as comments and will be ignored. Each element in the line is separated by a tab and is expected to be one of the following file types: ---- file dir block char link fifo sock ---- fifo is also known as named pipe (see fifo(7)). In case an element is empty or equal to - it is treated as not specified and it is derived from the input file. :: Path of the input file. It can be left unspecified in case all other needed fields are specified (and the file is otherwise empty). *Limitation*: The path must not start with #, be equal to -, or contain tabs. :: Path of the file inside the cpio. If the name is left unspecified it will be derived from . *Limitation*: The path must not be equal to - or contain tabs. :: File mode specified in octal. :: User ID (owner) of the file specified in decimal. :: Group ID of the file specified in decimal. :: Modification time of the file specified as seconds since the Epoch (1970-01-01 00:00 UTC). The specified time might be clamped by the time set in the SOURCE_DATE_EPOCH environment variable. :: Size of the input file in bytes. 3cpio will fail in case the input file is smaller than the provided file size. :: Major block/character device number in decimal. :: Minor block/character device number in decimal. :: Target of the symbolic link. *Limitation*: The target path must not be equal to - or contain tabs. *Limitations*: Files cannot start with # (will be treated as comment), be equal to - (will be treated as not specified), or contain tabs (will be split by tabs). These limitations of the manifest file are not expected to cause problems in practice. == Environment variables SOURCE_DATE_EPOCH:: This environment variable will be taken into account when creating cpio archive. All modification times that are newer than the time specified in "SOURCE_DATE_EPOCH" will be clamped. Compressors will run with only one thread in case their multithreading implementation is not reproducible. The created cpio archive will be reproducible across multiple runs. == Exit status *0*:: Success. *1*:: Failure. == Examples List the number of cpio archives that an initramfs file contains: [example,shell] ---- $ 3cpio --count /boot/initrd.img 4 ---- Examine the content of the initramfs cpio on an Ubuntu 24.04 system: [example,shell] ---- $ 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: [example,shell] ---- $ 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: [example,shell] ---- $ 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 ---- Create a cpio archive similar to the other cpio tools using the `find` command: [example,shell] ---- $ cd inputdir && find . | sort | 3cpio --create ../example.cpio ---- Due to its manifest file format support, 3cpio can create cpio archives without the need of copying files into a temporary directory first. Example for creating an early microcode cpio image directly using the system installed files: [example,shell] ---- $ cat manifest - kernel dir 755 0 0 1751654557 - kernel/x86 dir 755 0 0 1752011622 /usr/lib/firmware/amd-ucode kernel/x86/microcode /usr/lib/firmware/amd-ucode/microcode_amd_fam19h.bin kernel/x86/microcode/AuthenticAMD.bin $ 3cpio --create amd-ucode.img < manifest $ 3cpio --list --verbose amd-ucode.img drwxr-xr-x 2 root root 0 Jul 4 20:42 kernel drwxr-xr-x 2 root root 0 Jul 8 23:53 kernel/x86 drwxr-xr-x 2 root root 0 Jun 10 10:51 kernel/x86/microcode -rw-r--r-- 1 root root 100684 Mar 23 22:42 kernel/x86/microcode/AuthenticAMD.bin ---- Example for creating an initrd image containing of an uncompressed early microcode cpio followed by a Zstandard-compressed cpio: [example,shell] ---- $ cat manifest #cpio - kernel dir 755 0 0 1751654557 - kernel/x86 dir 755 0 0 1752011622 /usr/lib/firmware/amd-ucode kernel/x86/microcode /usr/lib/firmware/amd-ucode/microcode_amd_fam19h.bin kernel/x86/microcode/AuthenticAMD.bin #cpio: zstd -9 / /bin /usr /usr/bin /usr/bin/bash # This is a comment. Leaving the remaining files as task for the reader. $ 3cpio --create initrd.img < manifest $ 3cpio --examine initrd.img 0 cpio 101332 zstd $ 3cpio --list --verbose initrd.img drwxr-xr-x 2 root root 0 Jul 4 20:42 kernel drwxr-xr-x 2 root root 0 Jul 8 23:53 kernel/x86 drwxr-xr-x 2 root root 0 Jun 10 10:51 kernel/x86/microcode -rw-r--r-- 1 root root 100684 Mar 23 22:42 kernel/x86/microcode/AuthenticAMD.bin drwxr-xr-x 2 root root 0 Jun 5 14:11 . lrwxrwxrwx 1 root root 7 Mar 20 2022 bin -> usr/bin drwxr-xr-x 2 root root 0 Apr 20 2023 usr drwxr-xr-x 2 root root 0 Jul 9 09:56 usr/bin -rwxr-xr-x 1 root root 1740896 Mar 5 03:35 usr/bin/bash ---- == See also bsdcpio(1), cpio(1), lsinitramfs(8), lsinitrd(1) == Copying Copyright (C) 2024-2025 Benjamin Drung. Free use of this software is granted under the terms of the ISC License. threecpio-0.8.1/man/README.md000064400000000000000000000002361046102023000136470ustar 000000000000003cpio man pages =============== The 3cpio man page can be build with [Asciidoctor](https://asciidoctor.org/): ```sh asciidoctor -b manpage 3cpio.1.adoc ``` threecpio-0.8.1/src/compression.rs000064400000000000000000000255761046102023000153310ustar 00000000000000// Copyright (C) 2025, Benjamin Drung // SPDX-License-Identifier: ISC use std::fs::File; use std::io::{Error, ErrorKind, Result}; use std::process::{Child, ChildStdout, Command, Stdio}; #[derive(Debug, PartialEq)] pub enum Compression { Uncompressed, Bzip2 { level: Option, }, Gzip { level: Option, }, Lz4 { level: Option, }, Lzma { level: Option, }, Lzop { level: Option, }, Xz { level: Option, }, Zstd { level: Option, }, #[cfg(test)] NonExistent, #[cfg(test)] Failing, } impl Compression { pub fn from_magic_number(magic_number: [u8; 4]) -> Result { let compression = match magic_number { [0x42, 0x5A, 0x68, _] => Compression::Bzip2 { level: None }, [0x30, 0x37, 0x30, 0x37] => Compression::Uncompressed, [0x1F, 0x8B, _, _] => Compression::Gzip { level: None }, // 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] => { Compression::Lz4 { level: None } } [0x5D, _, _, _] => Compression::Lzma { level: None }, // Full magic number for lzop: [0x89, 0x4C, 0x5A, 0x4F, 0x00, 0x0D, 0x0A, 0x1A, 0x0A] [0x89, 0x4C, 0x5A, 0x4F] => Compression::Lzop { level: None }, // Full magic number for xz: [0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00] [0xFD, 0x37, 0x7A, 0x58] => Compression::Xz { level: None }, [0x28, 0xB5, 0x2F, 0xFD] => Compression::Zstd { level: None }, _ => { return Err(Error::new( ErrorKind::InvalidData, format!( "Failed to determine CPIO or compression magic number: 0x{:02x}{:02x}{:02x}{:02x} (big endian)", magic_number[0], magic_number[1], magic_number[2], magic_number[3] ), )); } }; Ok(compression) } fn from_str(name: &str) -> Result { let compression = match name { "" => Self::Uncompressed, "bzip2" => Self::Bzip2 { level: None }, "gzip" => Self::Gzip { level: None }, "lz4" => Self::Lz4 { level: None }, "lzma" => Self::Lzma { level: None }, "lzop" => Self::Lzop { level: None }, "xz" => Self::Xz { level: None }, "zstd" => Self::Zstd { level: None }, _ => { return Err(Error::new( ErrorKind::InvalidData, format!("Unknown compression format: {name}"), )); } }; Ok(compression) } fn set_level(&mut self, new_level: u32) { match self { Self::Bzip2 { level } | Self::Gzip { level } | Self::Lz4 { level } | Self::Lzma { level } | Self::Lzop { level } | Self::Xz { level } | Self::Zstd { level } => { *level = Some(new_level); } Self::Uncompressed => {} #[cfg(test)] Self::NonExistent | Self::Failing => {} }; } pub fn from_command_line(line: &str) -> Result { let mut iter = line.split_whitespace(); let mut compression = if let Some(cmd) = iter.next() { Self::from_str(cmd)? } else { Self::Uncompressed }; for parameter in iter { match parameter.strip_prefix("-") { Some(value) => { if let Ok(level) = value.parse::() { let (min, max) = match compression { Self::Uncompressed => (0, 0), Self::Bzip2 { level: _ } => (1, 9), Self::Gzip { level: _ } => (1, 9), Self::Lz4 { level: _ } => (1, 12), Self::Lzma { level: _ } => (0, 9), Self::Lzop { level: _ } => (1, 9), Self::Xz { level: _ } => (0, 9), Self::Zstd { level: _ } => (1, 19), #[cfg(test)] Self::NonExistent | Self::Failing => (0, 0), }; if level >= min || level <= max { compression.set_level(level.try_into().unwrap()); } else { eprintln!("Compression level '{level}' outside of range from {min} to {max}. Ignoring it.") } } else { eprintln!( "Unknown/unsupported compression parameter '{parameter}'. Ignoring it.", ) } } None => { eprintln!( "Unknown/unsupported compression parameter '{parameter}'. Ignoring it.", ) } } } Ok(compression) } pub fn command(&self) -> &str { match self { Self::Uncompressed => "cpio", Self::Bzip2 { level: _ } => "bzip2", Self::Gzip { level: _ } => "gzip", Self::Lz4 { level: _ } => "lz4", Self::Lzma { level: _ } => "lzma", Self::Lzop { level: _ } => "lzop", Self::Xz { level: _ } => "xz", Self::Zstd { level: _ } => "zstd", #[cfg(test)] Self::NonExistent => "non-existing-program", #[cfg(test)] Self::Failing => "false", } } pub fn compress(&self, file: Option, source_date_epoch: Option) -> Result { let mut command = self.compress_command(source_date_epoch); // TODO: Propper error message if spawn fails command.stdin(Stdio::piped()); if let Some(file) = file { command.stdout(file); } let cmd = command.spawn().map_err(|e| match e.kind() { ErrorKind::NotFound => Error::other(format!( "Program '{}' not found in PATH.", command.get_program().to_str().unwrap() )), _ => e, })?; Ok(cmd) } fn compress_command(&self, source_date_epoch: Option) -> Command { let mut command = Command::new(self.command()); match self { Self::Gzip { level: _ } => { command.arg("-n"); } Self::Lz4 { level: _ } => { command.arg("-l"); } Self::Xz { level: _ } => { command.arg("--check=crc32"); } Self::Zstd { level: _ } => { command.arg("-q"); } Self::Uncompressed | Self::Bzip2 { level: _ } | Self::Lzma { level: _ } | Self::Lzop { level: _ } => {} #[cfg(test)] Self::NonExistent | Self::Failing => {} }; match self { Self::Bzip2 { level: Some(level) } | Self::Gzip { level: Some(level) } | Self::Lz4 { level: Some(level) } | Self::Lzma { level: Some(level) } | Self::Lzop { level: Some(level) } | Self::Xz { level: Some(level) } | Self::Zstd { level: Some(level) } => { command.arg(format!("-{level}")); } Self::Uncompressed | Self::Bzip2 { level: None } | Self::Gzip { level: None } | Self::Lz4 { level: None } | Self::Lzma { level: None } | Self::Lzop { level: None } | Self::Xz { level: None } | Self::Zstd { level: None } => {} #[cfg(test)] Self::NonExistent | Self::Failing => {} }; // If we're not doing a reproducible build, enable multithreading if source_date_epoch.is_none() && matches!( self, Self::Lzma { level: _ } | Self::Xz { level: _ } | Self::Zstd { level: _ } ) { command.arg("-T0"); } else if source_date_epoch.is_some() && matches!(self, Self::Lzma { level: _ } | Self::Xz { level: _ }) { command.arg("-T1"); } command } pub fn decompress(&self, file: File) -> Result { let mut command = self.decompress_command(); // 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 decompress_command(&self) -> Command { let mut command = Command::new(self.command()); match self { Self::Bzip2 { level: _ } | Self::Gzip { level: _ } | Self::Lz4 { level: _ } | Self::Lzma { level: _ } | Self::Lzop { level: _ } | Self::Xz { level: _ } => { command.arg("-cd"); } Self::Zstd { level: _ } => { command.arg("-cdq"); } Self::Uncompressed => {} #[cfg(test)] Self::NonExistent | Self::Failing => {} }; command } pub fn is_uncompressed(&self) -> bool { matches!(self, Self::Uncompressed) } } #[cfg(test)] mod tests { use super::*; use crate::tests::TEST_LOCK; #[test] fn test_compression_decompress_program_not_found() { let _lock = TEST_LOCK.lock().unwrap(); let archive = File::open("tests/single.cpio").expect("test cpio should be present"); let compression = Compression::NonExistent; let got = compression.decompress(archive).unwrap_err(); assert_eq!(got.kind(), ErrorKind::Other); assert_eq!( got.to_string(), "Program 'non-existing-program' not found in PATH." ); } #[test] fn test_compression_from_command_line_lz4() { let compression = Compression::from_command_line(" lz4 ").unwrap(); assert_eq!(compression, Compression::Lz4 { level: None }); } #[test] fn test_compression_from_command_line_xz_6() { let compression = Compression::from_command_line(" xz \t -6 ").unwrap(); assert_eq!(compression, Compression::Xz { level: Some(6) }); } } threecpio-0.8.1/src/extended_error.rs000064400000000000000000000010471046102023000157640ustar 00000000000000// Copyright (C) 2025, Benjamin Drung // SPDX-License-Identifier: ISC use std::io::Error; pub trait ExtendedError { fn add_prefix>(self, filename: S) -> Self; fn add_line(self, line_number: usize) -> Self; } impl ExtendedError for Error { fn add_prefix>(self, prefix: S) -> Self { Self::new(self.kind(), format!("{}: {self}", prefix.as_ref())) } fn add_line(self, line_number: usize) -> Self { Self::new(self.kind(), format!("line {line_number}: {self}")) } } threecpio-0.8.1/src/filetype.rs000064400000000000000000000010061046102023000145670ustar 00000000000000// Copyright (C) 2025, Benjamin Drung // SPDX-License-Identifier: ISC pub 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; threecpio-0.8.1/src/header.rs000064400000000000000000000312301046102023000142000ustar 00000000000000// Copyright (C) 2024, Benjamin Drung // SPDX-License-Identifier: ISC use std::fs::Permissions; use std::io::{Error, ErrorKind, Read, Result, Write}; use std::os::unix::fs::PermissionsExt; use crate::filetype::*; 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"; #[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)] pub fn new( ino: u32, mode: u32, uid: u32, gid: u32, nlink: u32, mtime: u32, filesize: u32, rmajor: u32, rminor: u32, filename: S, ) -> Self where S: Into, { Self { ino, mode, uid, gid, nlink, mtime, filesize, major: 0, minor: 0, rmajor, rminor, filename: filename.into(), } } pub fn trailer() -> Self { Self { ino: 0, mode: 0, uid: 0, gid: 0, nlink: 1, mtime: 0, filesize: 0, major: 0, minor: 0, rmajor: 0, rminor: 0, filename: "TRAILER!!!".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(archive: &mut R) -> Result { let mut buffer = [0; CPIO_HEADER_LENGTH as usize]; archive.read_exact(&mut buffer)?; check_begins_with_cpio_magic_header(&buffer)?; let namesize = hex_str_to_u32(&buffer[94..102])?; let filename = read_filename(archive, 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(archive: &mut R) -> Result<(u32, String)> { let mut header = [0; CPIO_HEADER_LENGTH as usize]; archive.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(archive, namesize)?; Ok((filesize, filename)) } pub fn read_symlink_target(&self, archive: &mut R) -> Result { let align = align_to_4_bytes(self.filesize); let mut target_bytes = vec![0u8; (self.filesize + align).try_into().unwrap()]; archive.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, archive: &mut R) -> Result<()> { if self.filesize == 0 { return Ok(()); }; let skip = self.filesize + align_to_4_bytes(self.filesize); archive.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()) } pub fn write(&self, file: &mut W) -> Result<()> { // The filename needs to be terminated with \0. let filename_len: u32 = match (self.filename.len() + 1).try_into() { Ok(l) => l, Err(_) => { return Err(Error::new( ErrorKind::InvalidData, format!("Path '{}' exceeds filename length limit", self.filename), )) } }; let padding_len = align_to_4_bytes(CPIO_HEADER_LENGTH + filename_len) as usize; let padding = [0u8; 5]; write!( file, "{}{:08X}{:08X}{:08X}{:08X}{:08X}{:08X}{:08X}{:08X}{:08X}{:08X}{:08X}{:08X}00000000{}{}", std::str::from_utf8(&CPIO_MAGIC_NUMBER).unwrap(), self.ino, self.mode, self.uid, self.gid, self.nlink, self.mtime, self.filesize, self.major, self.minor, self.rmajor, self.rminor, filename_len, self.filename, std::str::from_utf8(&padding[0..padding_len+1]).unwrap(), ) } } 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(archive: &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(); archive.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 archive = b"07070100000002000081B4000003E8000007D000000001\ 661BE5C600000008000000000000000000000000000000000000000A00000000\ path/file\0content\0"; let header = Header::read(&mut archive.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 writing the header and get the original data back let mut output = Vec::new(); header.write(&mut output).unwrap(); output.write_all(b"content\0").unwrap(); assert_eq!( std::str::from_utf8(&output).unwrap(), std::str::from_utf8(archive).unwrap(), ); } #[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_header_write() { let header = Header { ino: 42, mode: 0o43_777, uid: 1000, gid: 2001, nlink: 2, mtime: 1720081471, filesize: 0, major: 3, minor: 7, rmajor: 42, rminor: 153, filename: "./directory_with_setuid".into(), }; let mut output = Vec::new(); header.write(&mut output).unwrap(); assert_eq!( std::str::from_utf8(&output).unwrap(), "0707010000002A000047FF000003E8000007D10000000266865C3F00000000\ 00000003000000070000002A000000990000001800000000\ ./directory_with_setuid\0\0\0", ); } #[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, 0, 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, 0, 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, 0, 0, "."); assert!(!header.is_root_directory()); } } threecpio-0.8.1/src/lib.rs000064400000000000000000001124231046102023000135220ustar 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::time::SystemTime; use crate::compression::Compression; use crate::filetype::*; use crate::header::Header; use crate::libc::{mknod, set_modified, strftime_local}; use crate::manifest::Manifest; use crate::seek_forward::SeekForward; mod compression; mod extended_error; mod filetype; mod header; mod libc; mod manifest; 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> { archive: &'a mut R, } impl Iterator for CpioFilenameReader<'_, R> { type Item = Result; fn next(&mut self) -> Option { match read_filename_from_next_cpio_object(self.archive) { 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 {mtime} for '{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(archive: &mut R) -> Result { let (filesize, filename) = Header::read_only_filesize_and_filename(archive)?; let skip = filesize + align_to_4_bytes(filesize); archive.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)), }, }; } match file.seek(SeekFrom::Current(-4)) { Ok(_) => {} Err(e) => { return Some(Err(e)); } }; let compression = match Compression::from_magic_number(buffer) { Ok(compression) => compression, Err(e) => { return Some(Err(e)); } }; Some(Ok(compression)) } fn read_cpio_and_print_filenames( archive: &mut R, out: &mut W, ) -> Result<()> { let cpio = CpioFilenameReader { archive }; for f in cpio { let filename = f?; writeln!(out, "{filename}")?; } Ok(()) } fn read_cpio_and_print_long_format( archive: &mut R, out: &mut W, now: i64, user_group_cache: &mut UserGroupCache, print_ino: bool, ) -> 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(archive) { 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 print_ino { write!(out, "{:>4} ", header.ino)?; } match header.mode & MODE_FILETYPE_MASK { FILETYPE_SYMLINK => { let target = header.read_symlink_target(archive)?; 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(archive)?; 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(archive)?; 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( archive: &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 = archive.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); archive.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( archive: &mut R, header: &Header, preserve_permissions: bool, log_level: u32, ) -> Result<()> { let target = header.read_symlink_target(archive)?; 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 {abspath:#?} has no parent directory."), )), } } 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 \"{path}\" (resolved to {canonicalized_path:#?}) \ is not within the directory {base_dir:#?}.", ), )); } Ok(canonicalized_path) } fn read_cpio_and_extract( archive: &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(archive) { 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( archive, &header, preserve_permissions, &mut extractor.seen_files, log_level, )?, FILETYPE_SYMLINK => { write_symbolic_link(archive, &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(archive: &mut File) -> Result<()> { let cpio = CpioFilenameReader { archive }; for f in cpio { f?; } Ok(()) } pub fn get_cpio_archive_count(archive: &mut File) -> Result { let mut count = 0; loop { let compression = match read_magic_header(archive) { None => return Ok(count), Some(x) => x?, }; count += 1; if compression.is_uncompressed() { seek_to_cpio_end(archive)?; } else { break; } } Ok(count) } // Parse SOURCE_DATE_EPOCH environment variable (if set and valid integer) fn get_source_date_epoch() -> Option { match std::env::var("SOURCE_DATE_EPOCH") { Ok(value) => match value.parse::() { Ok(source_date_epoch) => { if let Ok(x) = source_date_epoch.try_into() { Some(x) } else if source_date_epoch < 0 { Some(0) } else { Some(u32::MAX) } } Err(_) => None, }, Err(_) => None, } } pub fn print_cpio_archive_count(mut archive: File, out: &mut W) -> Result<()> { let count = get_cpio_archive_count(&mut archive)?; writeln!(out, "{count}")?; Ok(()) } pub fn create_cpio_archive(archive: Option, log_level: u32) -> Result<()> { let source_date_epoch = get_source_date_epoch(); let stdin = std::io::stdin(); let buf_reader = std::io::BufReader::new(stdin); if log_level >= LOG_LEVEL_DEBUG { eprintln!("Parsing manifest from stdin..."); } let manifest = Manifest::from_input(buf_reader, log_level)?; if log_level >= LOG_LEVEL_DEBUG { eprintln!("Writing cpio..."); } manifest.write_archive(archive, source_date_epoch, log_level)?; Ok(()) } pub fn examine_cpio_content(mut archive: File, out: &mut W) -> Result<()> { loop { let compression = match read_magic_header(&mut archive) { None => return Ok(()), Some(x) => x?, }; writeln!( out, "{}\t{}", archive.stream_position()?, compression.command() )?; if compression.is_uncompressed() { seek_to_cpio_end(&mut archive)?; } else { break; } } Ok(()) } pub fn extract_cpio_archive( mut archive: File, preserve_permissions: bool, subdir: Option, log_level: u32, ) -> Result<()> { let mut count = 1; let base_dir = std::env::current_dir()?; loop { let mut dir = base_dir.clone(); if let Some(ref s) = subdir { dir.push(format!("{s}{count}")); create_dir_ignore_existing(&dir)?; std::env::set_current_dir(&dir)?; } let compression = match read_magic_header(&mut archive) { None => return Ok(()), Some(x) => x?, }; if compression.is_uncompressed() { read_cpio_and_extract(&mut archive, &dir, preserve_permissions, log_level)?; } else { let mut decompressed = compression.decompress(archive)?; read_cpio_and_extract(&mut decompressed, &dir, preserve_permissions, log_level)?; break; } count += 1; } Ok(()) } pub fn list_cpio_content(mut archive: 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 compression = match read_magic_header(&mut archive) { None => return Ok(()), Some(x) => x?, }; if compression.is_uncompressed() { if log_level >= LOG_LEVEL_INFO { read_cpio_and_print_long_format( &mut archive, out, now, &mut user_group_cache, log_level >= LOG_LEVEL_DEBUG, )?; } else { read_cpio_and_print_filenames(&mut archive, out)?; } } else { let mut decompressed = compression.decompress(archive)?; if log_level >= LOG_LEVEL_INFO { read_cpio_and_print_long_format( &mut decompressed, out, now, &mut user_group_cache, log_level >= LOG_LEVEL_DEBUG, )?; } 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}; // Lock for tests that rely on / change the current directory pub static TEST_LOCK: std::sync::Mutex = std::sync::Mutex::new(0); 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 _lock = TEST_LOCK.lock().unwrap(); let mut archive = 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 archive, &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_get_cpio_archive_count_single() { let _lock = TEST_LOCK.lock().unwrap(); let mut archive = File::open("tests/single.cpio").expect("test cpio should be present"); let count = get_cpio_archive_count(&mut archive).unwrap(); assert_eq!(count, 1); } #[test] fn test_extract_cpio_archive_with_subdir() { let _lock = TEST_LOCK.lock().unwrap(); let archive = File::open("tests/single.cpio").unwrap(); let tempdir = TempDir::new().unwrap(); set_current_dir(&tempdir.path).unwrap(); extract_cpio_archive(archive, false, Some("cpio".into()), LOG_LEVEL_WARNING).unwrap(); let path = tempdir.path.join("cpio1/path/file"); assert!(path.exists()); } #[test] fn test_print_cpio_archive_count() { let _lock = TEST_LOCK.lock().unwrap(); let mut archive = File::open("tests/zstd.cpio").expect("test cpio should be present"); let mut output = Vec::new(); let count = get_cpio_archive_count(&mut archive).unwrap(); assert_eq!(count, 2); archive.seek(SeekFrom::Start(0)).unwrap(); print_cpio_archive_count(archive, &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 archive = 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 archive.as_ref(), &mut output, 1728486311, &mut user_group_cache, false, ) .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 archive = 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 archive.as_ref(), &mut output, 1722389471, &mut user_group_cache, false, ) .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 archive = 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 archive.as_ref(), &mut output, 1722645915, &mut user_group_cache, false, ) .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 archive = 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 archive.as_ref(), &mut output, 1722645915, &mut user_group_cache, false, ) .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_and_print_long_format_print_ino() { // Wrapped after mtime let archive = b"07070100000000000041ED00000000000000000000000265307180\ 00000000000000000000000000000000000000000000000200000000.\0\ 07070100000001000041ED00000000000000000000000265307180\ 00000000000000000000000000000000000000000000000700000000kernel\0\0\0\0\ 070701000000000000000000000000000000000000000100000000\ 00000000000000000000000000000000000000000000000B00000000TRAILER!!!\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 archive.as_ref(), &mut output, 1722645915, &mut user_group_cache, true, ) .unwrap(); assert_eq!( String::from_utf8(output).unwrap(), concat!( " 0 drwxr-xr-x 2 root root 0 Oct 19 2023 .\n", " 1 drwxr-xr-x 2 root root 0 Oct 19 2023 kernel\n" ) ); } #[test] fn test_write_character_device() { let _lock = TEST_LOCK.lock().unwrap(); if getuid() != 0 { // This test needs to run as root. return; } let tempdir = TempDir::new().unwrap(); set_current_dir(&tempdir.path).unwrap(); let mut header = Header::new(1, 0o20_644, 0, 0, 0, 1740402179, 0, 0, 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 _lock = TEST_LOCK.lock().unwrap(); let tempdir = TempDir::new().unwrap(); set_current_dir(&tempdir.path).unwrap(); let mut mtimes = BTreeMap::new(); let header = Header::new( 1, 0o43_777, getuid(), getgid(), 0, 1720081471, 0, 0, 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 _lock = TEST_LOCK.lock().unwrap(); let tempdir = TempDir::new().unwrap(); set_current_dir(&tempdir.path).unwrap(); let mut seen_files = SeenFiles::new(); let header = Header::new( 1, 0o104_755, getuid(), getgid(), 0, 1720081471, 9, 0, 0, "./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 _lock = TEST_LOCK.lock().unwrap(); let tempdir = TempDir::new().unwrap(); set_current_dir(&tempdir.path).unwrap(); let header = Header::new( 1, 0o120_777, getuid(), getgid(), 0, 1721427072, 12, 0, 0, "./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.8.1/src/libc.rs000064400000000000000000000144271046102023000136720ustar 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 major(dev: u64) -> u32 { libc::major(dev) } 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)] pub mod tests { use super::*; use std::env::temp_dir; use std::fs::{self, create_dir}; use std::path::PathBuf; use std::time::{Duration, SystemTime}; pub 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.8.1/src/main.rs000064400000000000000000000224741046102023000137060ustar 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::{ create_cpio_archive, 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, create: bool, directory: String, examine: bool, extract: bool, force: bool, list: bool, log_level: u32, archive: Option, preserve_permissions: bool, subdir: Option, } fn print_help() { let executable = std::env::args().next().unwrap(); println!( "Usage: {executable} --count ARCHIVE {executable} {{-c|--create}} [-v|--debug] [-C DIR] [ARCHIVE] < manifest {executable} {{-e|--examine}} ARCHIVE {executable} {{-t|--list}} [-v|--debug] ARCHIVE {executable} {{-x|--extract}} [-v|--debug] [-C DIR] [-p] [-s NAME] [--force] ARCHIVE Optional arguments: --count Print the number of concatenated cpio archives. -c, --create Create a new cpio archive from the manifest on stdin. -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 create = 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 archive = 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("create") => { create = 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 archive.is_none() => { archive = Some(val.string()?); } _ => return Err(arg.unexpected()), } } if count + create + examine + extract + list != 1 { return Err( "Either --count, --create, --examine, --extract, or --list must be specified!".into(), ); } if let Some(ref s) = subdir { if s.contains('/') { return Err(format!("Subdir '{s}' must not contain slashes!").into()); } } if create != 1 && archive.is_none() { return Err("missing argument ARCHIVE".into()); } Ok(Args { count: count == 1, create: create == 1, directory, examine: examine == 1, extract: extract == 1, force, list: list == 1, log_level, archive, 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 '{path}' is not empty. \ Use --force to overwrite existing files!", )); } Ok(true) => {} } } Ok(()) } fn main() -> ExitCode { let executable = std::env::args().next().unwrap(); let args = match parse_args() { Ok(a) => a, Err(e) => { eprintln!("{executable}: Error: {e}"); return ExitCode::from(2); } }; if args.create { let mut archive = None; if let Some(path) = args.archive.as_ref() { archive = match File::create(path) { Ok(f) => Some(f), Err(e) => { eprintln!("{executable}: Error: Failed to create '{path}': {e}"); return ExitCode::FAILURE; } }; if args.log_level >= LOG_LEVEL_DEBUG { eprintln!("{executable}: Opened '{path}' for writing."); } } if let Err(e) = set_current_dir(&args.directory) { eprintln!( "{executable}: Error: Failed to change directory to '{}': {e}", args.directory, ); return ExitCode::FAILURE; } let result = create_cpio_archive(archive, args.log_level); if let Err(error) = result { match error.kind() { ErrorKind::BrokenPipe => {} _ => { eprintln!( "{executable}: Error: Failed to create '{}': {error}", args.archive.unwrap_or("cpio on stdout".into()), ); return ExitCode::FAILURE; } } } return ExitCode::SUCCESS; }; let archive = match File::open(args.archive.as_ref().unwrap()) { Ok(f) => f, Err(e) => { eprintln!( "{executable}: Error: Failed to open '{}': {e}", args.archive.unwrap(), ); return ExitCode::FAILURE; } }; if args.extract { if let Err(e) = create_and_set_current_dir(&args.directory, args.force) { eprintln!("{executable}: Error: {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(archive, &mut stdout), ) } else if args.examine { ( "examine content", examine_cpio_content(archive, &mut stdout), ) } else if args.extract { ( "extract content", extract_cpio_archive( archive, args.preserve_permissions, args.subdir, args.log_level, ), ) } else if args.list { ( "list content", list_cpio_content(archive, &mut stdout, args.log_level), ) } else { unreachable!("no operation specified"); }; if let Err(e) = result { match e.kind() { ErrorKind::BrokenPipe => {} _ => { eprintln!( "{executable}: Error: Failed to {operation} of '{}': {e}", args.archive.unwrap(), ); return ExitCode::FAILURE; } } } ExitCode::SUCCESS } threecpio-0.8.1/src/manifest.rs000064400000000000000000001377241046102023000145750ustar 00000000000000// Copyright (C) 2025, Benjamin Drung // SPDX-License-Identifier: ISC use std::collections::HashMap; use std::fs::{symlink_metadata, Metadata}; use std::io::{BufRead, BufWriter, Error, ErrorKind, Result, Write}; use std::os::unix::fs::{MetadataExt, PermissionsExt}; use crate::compression::Compression; use crate::extended_error::ExtendedError; use crate::filetype::*; use crate::header::Header; use crate::libc::{major, minor}; use crate::{align_to_4_bytes, LOG_LEVEL_DEBUG, LOG_LEVEL_INFO}; #[derive(Debug, PartialEq)] struct Hardlink { location: String, filesize: u32, references: u32, } fn get_hardlink_key(stat: &Metadata) -> u128 { (u128::from(stat.ino()) << 64) | u128::from(stat.dev()) } impl Hardlink { fn new>(location: S, filesize: u32) -> Self { Self { location: location.into(), filesize, references: 1, } } #[cfg(test)] fn with_references>(location: S, filesize: u32, references: u32) -> Self { Self { location: location.into(), filesize, references, } } } #[derive(Debug, PartialEq)] enum Filetype { Hardlink { key: u128, index: u32 }, EmptyFile, Directory, BlockDevice { major: u32, minor: u32 }, CharacterDevice { major: u32, minor: u32 }, Fifo, Socket, Symlink { target: String }, } #[derive(Debug, PartialEq)] struct File { filetype: Filetype, name: String, mode: u16, uid: u32, gid: u32, mtime: u32, } #[derive(Debug, PartialEq)] pub struct Archive { compression: Compression, files: Vec, hardlinks: HashMap, } #[derive(Debug, PartialEq)] pub struct Manifest { archives: Vec, umask: u32, } struct LazyMetadata<'a> { location: Option<&'a str>, metadata: Option, } impl<'a> LazyMetadata<'a> { fn new(location: Option<&'a str>) -> Self { LazyMetadata { location, metadata: None, } } fn get_metadata(&mut self, name: &str) -> Result<&Metadata> { if self.metadata.is_none() { let stat = match self.location { None => { return Err(Error::new( ErrorKind::InvalidInput, format!("Neither {name} nor location specified."), )) } Some(path) => symlink_metadata(path).map_err(|e| e.add_prefix(path))?, }; self.metadata = Some(stat); } Ok(self.metadata.as_ref().unwrap()) } fn parse_u32( &mut self, entry: Option<&str>, name: &str, f: impl Fn(&Metadata) -> Result, ) -> Result { match entry { Some("-") | Some("") | None => Ok(f(self.get_metadata(name)?)?), Some(x) => match x.parse() { Ok(y) => Ok(y), Err(e) => Err(Error::new( ErrorKind::InvalidInput, format!("invalid {name}: {e}"), )), }, } } fn parse_octal( &mut self, entry: Option<&str>, name: &str, f: impl Fn(&Metadata) -> u16, ) -> Result { match entry { Some("-") | Some("") | None => Ok(f(self.get_metadata(name)?)), Some(x) => match u16::from_str_radix(x, 8) { Ok(y) => Ok(y), Err(e) => Err(Error::new( ErrorKind::InvalidInput, format!("invalid {name}: {e}"), )), }, } } fn parse_filetype(&mut self, entry: Option<&str>, name: &str) -> Result { let filetype = match entry { Some("file") => FILETYPE_REGULAR_FILE, Some("dir") => FILETYPE_DIRECTORY, Some("block") => FILETYPE_BLOCK_DEVICE, Some("char") => FILETYPE_CHARACTER_DEVICE, Some("link") => FILETYPE_SYMLINK, Some("fifo") => FILETYPE_FIFO, Some("sock") => FILETYPE_SOCKET, Some("-") | Some("") | None => self.get_metadata(name)?.mode() & MODE_FILETYPE_MASK, Some(x) => { return Err(Error::new( ErrorKind::InvalidInput, format!("Unknown filetype '{x}'"), )) } }; Ok(filetype) } } fn pathbuf_to_string(path: std::path::PathBuf) -> Result { path.into_os_string().into_string().map_err(|e| { Error::new( ErrorKind::InvalidInput, format!("failed to convert path {e:#?} to string"), ) }) } fn parse_symlink(entry: Option<&str>, location: Option<&str>) -> Result { match entry { Some("-") | Some("") | None => match location { None => Err(Error::new( ErrorKind::InvalidInput, "Neither symlink nor location specified.", )), Some(path) => Ok(pathbuf_to_string(std::fs::read_link(path)?)?), }, Some(x) => Ok(x.into()), } } fn replace_empty(entry: Option<&str>) -> Option<&str> { match entry { Some("-") | Some("") | None => None, Some(x) => Some(x), } } fn sanitize_path(path: &str) -> &str { match path.strip_prefix("./") { Some(p) => { if p.is_empty() { "." } else { p } } None => match path.strip_prefix("/") { Some(p) => { if p.is_empty() { "." } else { p } } None => path, }, } } // Return the permission bits from Metadata.mode fn get_permission(mode: u32) -> u16 { (mode & MODE_PERMISSION_MASK) as u16 } // Return the rdev major from Metadata fn get_rmajor(metadata: &Metadata) -> Result { Ok(major(metadata.rdev())) } // Return the rdev major from Metadata fn get_rminor(metadata: &Metadata) -> Result { Ok(minor(metadata.rdev())) } fn get_mtime(metadata: &Metadata) -> Result { metadata.mtime().try_into().map_err(|_| { Error::new( ErrorKind::InvalidData, format!( "mtime {} outside of supported range from 0 to 4,294,967,295.", metadata.mtime() ), ) }) } // Determine umask for creating the cpio file based on the given file mode. // Since the "group" mode of the file can differ from the cpio writer group, // use the umask from "other" for "group". fn determine_umask(mode: u32) -> u32 { let other_umask = !mode & 0o7; (other_umask << 3) | other_umask } impl File { fn new>( filetype: Filetype, name: S, mode: u16, uid: u32, gid: u32, mtime: u32, ) -> Self { Self { filetype, name: name.into(), mode, uid, gid, mtime, } } /* Description from the 3cpio man page: The manifest is a text format that is parsed line by line. If the line starts with _#cpio_ it is interpreted as section marker to start a new cpio. A compression may be specified by adding a colon followed by the compression format and an optional compression level. Example for a Zstandard-compressed cpio with compression level 9: ---- #cpio: zstd -9 ---- All lines starting with _#_ excluding _#cpio_ (see above) will be treated as comments and will be ignored. Each element in the line is separated by a tab and is expected to be one of the following file types: ---- file dir block char link fifo sock ---- fifo is also known as named pipe (see fifo(7)). In case an element is empty or equal to - it is treated as not specified and it is derived from the input file. :: Path of the input file. It can be left unspecified in case all other needed fields are specified (and the file is otherwise empty). *Limitation*: The path must not start with #, be equal to -, or contain tabs. :: Path of the file inside the cpio. If the name is left unspecified it will be derived from . *Limitation*: The path must not be equal to - or contain tabs. :: File mode specified in octal. :: User ID (owner) of the file specified in decimal. :: Group ID of the file specified in decimal. :: Modification time of the file specified as seconds since the Epoch (1970-01-01 00:00 UTC). The specified time might be clamped by the time set in the SOURCE_DATE_EPOCH environment variable. :: Size of the input file in bytes. 3cpio will fail in case the input file is smaller than the provided file size. :: Major block/character device number in decimal. :: Minor block/character device number in decimal. :: Target of the symbolic link. *Limitation*: The target path must not be equal to - or contain tabs. *Limitations*: Files cannot start with # (will be treated as comment), be equal to - (will be treated as not specified), or contain tabs (will be split by tabs). These limitations of the manifest file are not expected to cause problems in practice. */ fn from_line>( line: S, hardlinks: &mut HashMap, ) -> Result<(Self, u32)> { let mut umask = 0; let mut iter = line.as_ref().split('\t'); let location = replace_empty(iter.next()); let name = match replace_empty(iter.next()) { Some(name) => name, None => match location { Some(path) => sanitize_path(path), None => { return Err(Error::new( ErrorKind::InvalidInput, "Neither location nor name were specified.", )) } }, }; let mut lazy_metadata = LazyMetadata::new(location); let filetype_value = lazy_metadata.parse_filetype(iter.next(), "filetype")?; let mode = lazy_metadata.parse_octal(iter.next(), "mode", |m| get_permission(m.mode()))?; let uid = lazy_metadata.parse_u32(iter.next(), "uid", |m| Ok(m.uid()))?; let gid = lazy_metadata.parse_u32(iter.next(), "gid", |m| Ok(m.gid()))?; let mtime = lazy_metadata.parse_u32(iter.next(), "mtime", get_mtime)?; let filetype = match filetype_value { FILETYPE_REGULAR_FILE => { let filesize = lazy_metadata.parse_u32(iter.next(), "filesize", |m| { m.size().try_into().map_err(|_| { Error::new( ErrorKind::InvalidData, format!( "File '{}' exceeds file size limit of 4 GiB.", location.unwrap() ), ) }) })?; if filesize == 0 { Filetype::EmptyFile } else { let stat = lazy_metadata.get_metadata("filetype")?; umask = determine_umask(stat.mode()); let key = get_hardlink_key(stat); let index = match hardlinks.get_mut(&key) { Some(hardlink) => { hardlink.references += 1; hardlink.references } None => { // Defer writing the hardlink hardlinks.insert(key, Hardlink::new(location.unwrap(), filesize)); 1 } }; Filetype::Hardlink { key, index } } } FILETYPE_DIRECTORY => Filetype::Directory, FILETYPE_BLOCK_DEVICE => Filetype::BlockDevice { major: lazy_metadata.parse_u32(iter.next(), "major", get_rmajor)?, minor: lazy_metadata.parse_u32(iter.next(), "minor", get_rminor)?, }, FILETYPE_CHARACTER_DEVICE => Filetype::CharacterDevice { major: lazy_metadata.parse_u32(iter.next(), "major", get_rmajor)?, minor: lazy_metadata.parse_u32(iter.next(), "minor", get_rminor)?, }, FILETYPE_SYMLINK => Filetype::Symlink { target: parse_symlink(iter.next(), location)?, }, FILETYPE_FIFO => Filetype::Fifo, FILETYPE_SOCKET => Filetype::Socket, unknown => { return Err(Error::new( ErrorKind::InvalidInput, format!("Unknown filetype '{unknown}'"), )) } }; Ok((Self::new(filetype, name, mode, uid, gid, mtime), umask)) } fn generate_header( &self, next_free_ino: u32, hardlinks: &HashMap, hardlinks2ino: &mut HashMap, ) -> (Header, u32) { let mut nlink = 1; let mut filesize = 0; let mut rmajor = 0; let mut rminor = 0; let mut ino = next_free_ino; let mut next_ino = next_free_ino + 1; let filetype; match &self.filetype { Filetype::EmptyFile => filetype = FILETYPE_REGULAR_FILE, Filetype::Hardlink { key, index } => { filetype = FILETYPE_REGULAR_FILE; if let Some(existing_ino) = hardlinks2ino.get(key) { ino = *existing_ino; next_ino = next_free_ino; } else { hardlinks2ino.insert(*key, ino); } let hardlink = hardlinks.get(key).unwrap(); nlink = hardlink.references; // last reference will write the hardlink filesize = if *index == nlink { hardlink.filesize } else { 0 }; } Filetype::Directory => { filetype = FILETYPE_DIRECTORY; nlink = 2; } Filetype::BlockDevice { major, minor } => { filetype = FILETYPE_BLOCK_DEVICE; rmajor = *major; rminor = *minor; } Filetype::CharacterDevice { major, minor } => { filetype = FILETYPE_CHARACTER_DEVICE; rmajor = *major; rminor = *minor; } Filetype::Symlink { target } => { filetype = FILETYPE_SYMLINK; filesize = target.len().try_into().unwrap(); } Filetype::Fifo => filetype = FILETYPE_FIFO, Filetype::Socket => filetype = FILETYPE_SOCKET, } ( Header::new( ino, filetype | u32::from(self.mode), self.uid, self.gid, nlink, self.mtime, filesize, rmajor, rminor, self.name.clone(), ), next_ino, ) } } impl Archive { fn new() -> Self { Self { compression: Compression::Uncompressed, files: Vec::new(), hardlinks: HashMap::new(), } } #[cfg(test)] fn with_files(files: Vec) -> Self { Self { compression: Compression::Uncompressed, files, hardlinks: HashMap::new(), } } #[cfg(test)] fn with_files_and_hardlinks(files: Vec, hardlinks: HashMap) -> Self { Self { compression: Compression::Uncompressed, files, hardlinks, } } #[cfg(test)] fn with_files_compressed(files: Vec, compression: Compression) -> Self { Self { compression, files, hardlinks: HashMap::new(), } } fn add_line>(&mut self, line: S) -> Result { let (file, umask) = File::from_line(line, &mut self.hardlinks)?; self.files.push(file); Ok(umask) } fn is_empty(&self) -> bool { self.files.is_empty() } fn set_compression(&mut self, compression: Compression) { self.compression = compression; } fn write( &self, output_file: &mut W, source_date_epoch: Option, log_level: u32, ) -> Result<()> { let mut next_ino = 0; let mut hardlink_ino = HashMap::new(); let mut header; for file in &self.files { if log_level >= LOG_LEVEL_INFO { writeln!(std::io::stderr(), "{}", file.name)?; } (header, next_ino) = file.generate_header(next_ino, &self.hardlinks, &mut hardlink_ino); if let Some(epoch) = source_date_epoch { if header.mtime > epoch { header.mtime = epoch; } } if log_level >= LOG_LEVEL_DEBUG { writeln!(std::io::stderr(), "{header:?}")?; }; header.write(output_file)?; match &file.filetype { Filetype::Hardlink { key, index: _ } => { if header.filesize > 0 { let hardlink = self.hardlinks.get(key).unwrap(); copy_file_with_padding(&hardlink.location, hardlink.filesize, output_file)?; } } Filetype::Symlink { target } => { output_file.write_all(target.as_bytes())?; write_padding(output_file, header.filesize)?; } Filetype::EmptyFile | Filetype::Directory | Filetype::BlockDevice { major: _, minor: _ } | Filetype::CharacterDevice { major: _, minor: _ } | Filetype::Fifo | Filetype::Socket => {} } } Header::trailer().write(output_file)?; Ok(()) } } impl Manifest { fn new(archives: Vec, umask: u32) -> Self { Self { archives, umask } } pub fn from_input(reader: R, log_level: u32) -> Result { let mut archives = vec![Archive::new()]; let mut current_archive = archives.last_mut().unwrap(); let mut umask = 0; for (line_number, line) in reader.lines().enumerate() { let line = line.map_err(|e| e.add_line(line_number + 1))?; let line = line.trim(); if line.starts_with("#") || line.is_empty() { if line.starts_with("#cpio") { if log_level >= LOG_LEVEL_DEBUG { eprintln!("Parsing line {}: {line}", line_number + 1); } if !current_archive.is_empty() { archives.push(Archive::new()); current_archive = archives.last_mut().unwrap(); }; match line.strip_prefix("#cpio:") { Some(compression_str) => { let compression = Compression::from_command_line(compression_str) .map_err(|e| e.add_line(line_number + 1))?; current_archive.set_compression(compression); } None => { if line != "#cpio" { return Err(Error::new( ErrorKind::InvalidInput, format!( "line {}: Unknown cpio archive directive: {line}", line_number + 1, ), )); } } } } continue; } if log_level >= LOG_LEVEL_DEBUG { eprintln!("Parsing line {}: {line}", line_number + 1); } let file_mask = current_archive .add_line(line) .map_err(|e| e.add_line(line_number + 1))?; umask |= file_mask; } Ok(Self::new(archives, umask)) } fn apply_umask(&self, file: &std::fs::File) -> Result<()> { let mode = file.metadata()?.mode(); let new_mode = mode & !self.umask; if mode != new_mode { file.set_permissions(PermissionsExt::from_mode(new_mode))?; } Ok(()) } pub fn write_archive( self, mut file: Option, source_date_epoch: Option, log_level: u32, ) -> Result<()> { if let Some(file) = file.as_ref() { self.apply_umask(file)?; } for archive in self.archives { if archive.compression.is_uncompressed() { if let Some(file) = file.as_mut() { let mut writer = BufWriter::new(file); archive.write(&mut writer, source_date_epoch, log_level)?; writer.flush()?; } else { let mut stdout = std::io::stdout().lock(); archive.write(&mut stdout, source_date_epoch, log_level)?; } } else { let mut compressor = archive.compression.compress(file, source_date_epoch)?; let mut writer = BufWriter::new(compressor.stdin.as_ref().unwrap()); archive.write(&mut writer, source_date_epoch, log_level)?; writer.flush()?; drop(writer); let exit_status = compressor.wait()?; if !exit_status.success() { return Err(Error::other(format!( "{} failed: {exit_status}", archive.compression.command() ))); } // TODO: Check that the compressed cpio is the last break; } } Ok(()) } } fn copy_file_with_padding(path: &str, filesize: u32, writer: &mut W) -> Result<()> { let file = std::fs::File::open(path).map_err(|e| e.add_prefix(path))?; let mut reader = std::io::BufReader::new(file); let copied_bytes = std::io::copy(&mut reader, writer)?; if copied_bytes != filesize.into() { return Err(Error::new( ErrorKind::UnexpectedEof, format!("Copied {copied_bytes} bytes from {path} but expected {filesize} bytes."), )); } write_padding(writer, filesize)?; Ok(()) } pub fn write_padding(file: &mut W, written_bytes: u32) -> Result<()> { let padding_len = align_to_4_bytes(written_bytes); if padding_len == 0 { return Ok(()); } let padding = vec![0u8; padding_len.try_into().unwrap()]; file.write_all(&padding) } #[cfg(test)] mod tests { use std::fs::{canonicalize, hard_link}; use std::path::PathBuf; use super::*; use crate::libc::tests::make_temp_dir; use crate::LOG_LEVEL_WARNING; pub fn make_temp_dir_with_hardlinks() -> Result { let temp_dir = make_temp_dir()?; let path = temp_dir.join("a"); let mut file = std::fs::File::create(&path)?; file.set_permissions(PermissionsExt::from_mode(0o755))?; file.write_all(b"content")?; hard_link(&path, temp_dir.join("b"))?; hard_link(&path, temp_dir.join("c"))?; Ok(temp_dir) } #[test] fn test_determine_umask_all_read() { assert_eq!(determine_umask(0o755), 0o022); } #[test] fn test_determine_umask_only_root() { assert_eq!(determine_umask(0o640), 0o077); } #[test] fn test_sanitize_path_absolute_path() { assert_eq!(sanitize_path("/path/to/file"), "path/to/file"); } #[test] fn test_sanitize_path_dot() { assert_eq!(sanitize_path("."), "."); } #[test] fn test_sanitize_path_dot_slash() { assert_eq!(sanitize_path("./"), "."); } #[test] fn test_sanitize_path_dot_slash_path() { assert_eq!(sanitize_path("./path/to/file"), "path/to/file"); } #[test] fn test_sanitize_path_relative_path() { assert_eq!(sanitize_path("path/to/file"), "path/to/file"); } #[test] fn test_sanitize_path_root() { assert_eq!(sanitize_path("/"), "."); } #[test] fn test_file_from_line_full_regular_file() { let line = "/usr/bin/gzip\tusr/bin/gzip\tfile\t755\t0\t0\t1739259005\t35288"; let stat = symlink_metadata("/usr/bin/gzip").unwrap(); let key = get_hardlink_key(&stat); let mut hardlinks = HashMap::new(); let (file, umask) = File::from_line(line, &mut hardlinks).unwrap(); assert_eq!( file, File::new( Filetype::Hardlink { key, index: 1 }, "usr/bin/gzip", 0o755, 0, 0, 1739259005 ) ); assert_eq!(umask, 0o022); assert_eq!( hardlinks, HashMap::from([(key, Hardlink::new("/usr/bin/gzip", 35288))]) ); } #[test] fn test_file_from_line_full_directory() { let line = "/usr\tusr\tdir\t755\t0\t0\t1681992796"; let mut hardlinks = HashMap::new(); let (file, umask) = File::from_line(line, &mut hardlinks).unwrap(); assert_eq!( file, File::new(Filetype::Directory, "usr", 0o755, 0, 0, 1681992796) ); assert_eq!(umask, 0); assert!(hardlinks.is_empty()); } #[test] fn test_file_from_line_full_block_device() { let line = "/dev/nvme0n1p2\tdev/nvme0n1p2\tblock\t660\t0\t0\t1745246683\t259\t2"; let mut hardlinks = HashMap::new(); let (file, umask) = File::from_line(line, &mut hardlinks).unwrap(); assert_eq!( file, File::new( Filetype::BlockDevice { major: 259, minor: 2 }, "dev/nvme0n1p2", 0o660, 0, 0, 1745246683 ) ); assert_eq!(umask, 0); assert!(hardlinks.is_empty()); } #[test] fn test_file_from_line_full_character_device() { let line = "/dev/console\tdev/console\tchar\t600\t0\t5\t1745246724\t5\t1"; let mut hardlinks = HashMap::new(); let (file, umask) = File::from_line(line, &mut hardlinks).unwrap(); assert_eq!( file, File::new( Filetype::CharacterDevice { major: 5, minor: 1 }, "dev/console", 0o600, 0, 5, 1745246724 ) ); assert_eq!(umask, 0); assert!(hardlinks.is_empty()); } #[test] fn test_file_from_line_full_symlink() { let line = "/bin\tbin\tlink\t777\t0\t0\t1647786132\tusr/bin"; let mut hardlinks = HashMap::new(); let (file, umask) = File::from_line(line, &mut hardlinks).unwrap(); assert_eq!( file, File::new( Filetype::Symlink { target: "usr/bin".into() }, "bin", 0o777, 0, 0, 1647786132 ) ); assert_eq!(umask, 0); assert!(hardlinks.is_empty()); } #[test] fn test_file_from_line_full_fifo() { let line = "/run/initctl\trun/initctl\tfifo\t0600\t0\t0\t1746789067"; let mut hardlinks = HashMap::new(); let (file, umask) = File::from_line(line, &mut hardlinks).unwrap(); assert_eq!( file, File::new(Filetype::Fifo, "run/initctl", 0o600, 0, 0, 1746789067) ); assert_eq!(umask, 0); assert!(hardlinks.is_empty()); } #[test] fn test_file_from_line_full_socket() { let line = "/run/systemd/notify\trun/systemd/notify\tsock\t777\t0\t0\t1746789058"; let mut hardlinks = HashMap::new(); let (file, umask) = File::from_line(line, &mut hardlinks).unwrap(); assert_eq!( file, File::new( Filetype::Socket, "run/systemd/notify", 0o777, 0, 0, 1746789058, ) ); assert_eq!(umask, 0); assert!(hardlinks.is_empty()); } #[test] fn test_file_from_line_empty_file() { let line = "\tetc/fstab.empty\tfile\t644\t0\t0\t1744705149\t0"; let mut hardlinks = HashMap::new(); let (file, umask) = File::from_line(line, &mut hardlinks).unwrap(); assert_eq!( file, File::new( Filetype::EmptyFile, "etc/fstab.empty", 0o644, 0, 0, 1744705149 ) ); assert_eq!(umask, 0); assert!(hardlinks.is_empty()); } #[test] fn test_file_from_line_location_regular_file() { let line = "/usr/bin/gzip"; let stat = symlink_metadata("/usr/bin/gzip").unwrap(); let key = get_hardlink_key(&stat); let mtime = stat.mtime().try_into().unwrap(); let size = stat.size().try_into().unwrap(); let mut hardlinks = HashMap::new(); let (file, umask) = File::from_line(line, &mut hardlinks).unwrap(); assert_eq!( file, File::new( Filetype::Hardlink { key, index: 1 }, "usr/bin/gzip", 0o755, 0, 0, mtime, ) ); assert_eq!(umask, 0o022); assert_eq!( hardlinks, HashMap::from([(key, Hardlink::new("/usr/bin/gzip", size))]) ); } #[test] fn test_file_from_line_location_duplicate_file() { let line = "/usr/bin/gzip\tgzip\t\t\t\t\t1745485084"; let stat = symlink_metadata("/usr/bin/gzip").unwrap(); let key = get_hardlink_key(&stat); let size = stat.size().try_into().unwrap(); let mut hardlinks = HashMap::new(); let (file, umask) = File::from_line(line, &mut hardlinks).unwrap(); assert_eq!( file, File::new( Filetype::Hardlink { key, index: 1 }, "gzip", 0o755, 0, 0, 1745485084, ) ); assert_eq!(umask, 0o022); assert_eq!( hardlinks, HashMap::from([(key, Hardlink::new("/usr/bin/gzip", size))]) ); let (file, umask) = File::from_line(line, &mut hardlinks).unwrap(); assert_eq!( file, File::new( Filetype::Hardlink { key, index: 2 }, "gzip", 0o755, 0, 0, 1745485084, ) ); assert_eq!(umask, 0o022); assert_eq!( hardlinks, HashMap::from([(key, Hardlink::with_references("/usr/bin/gzip", size, 2))]) ); } #[test] fn test_file_from_line_location_hardlink() { let temp_dir = make_temp_dir_with_hardlinks().unwrap(); let path = temp_dir.join("a").to_str().unwrap().to_owned(); let line = format!("{path}\ta\t\t644\t1\t2"); let stat = symlink_metadata(&path).unwrap(); let mtime = stat.mtime().try_into().unwrap(); let key = get_hardlink_key(&stat); let mut hardlinks = HashMap::new(); let (file, umask) = File::from_line(line, &mut hardlinks).unwrap(); assert_eq!( file, File::new( Filetype::Hardlink { key, index: 1 }, "a", 0o644, 1, 2, mtime, ) ); assert_eq!(umask, 0o022); assert_eq!(hardlinks, HashMap::from([(key, Hardlink::new(&path, 7))])); let line = format!( "{}/b\tb\t\t640\t3\t4\t1751413453", temp_dir.to_str().unwrap() ); let (file, umask) = File::from_line(line, &mut hardlinks).unwrap(); assert_eq!( file, File::new( Filetype::Hardlink { key, index: 2 }, "b", 0o640, 3, 4, 1751413453, ) ); assert_eq!(umask, 0o022); assert_eq!( hardlinks, HashMap::from([(key, Hardlink::with_references(&path, 7, 2))]) ); } #[test] fn test_file_from_line_location_relative_directory() { let line = "./tests\t\t\t510\t7\t42"; let stat = symlink_metadata("tests").unwrap(); let mtime = stat.mtime().try_into().unwrap(); let mut hardlinks = HashMap::new(); let (file, umask) = File::from_line(line, &mut hardlinks).unwrap(); assert_eq!( file, File::new(Filetype::Directory, "tests", 0o510, 7, 42, mtime) ); assert_eq!(umask, 0); assert!(hardlinks.is_empty()); } #[test] fn test_file_from_line_location_directory() { let line = "/usr"; let stat = symlink_metadata("/usr").unwrap(); let mtime = stat.mtime().try_into().unwrap(); let mut hardlinks = HashMap::new(); let (file, umask) = File::from_line(line, &mut hardlinks).unwrap(); assert_eq!( file, File::new(Filetype::Directory, "usr", 0o755, 0, 0, mtime) ); assert_eq!(umask, 0); assert!(hardlinks.is_empty()); } #[test] fn test_file_from_line_location_block_device() { let line = "/dev/loop0"; let stat = match symlink_metadata("/dev/loop0") { Ok(s) => s, // This test expects a block device like /dev/loop0 being present. Err(_) => return, }; let mtime = stat.mtime().try_into().unwrap(); let mut hardlinks = HashMap::new(); let (file, umask) = File::from_line(line, &mut hardlinks).unwrap(); assert_eq!( file, File::new( Filetype::BlockDevice { major: major(stat.rdev()), minor: minor(stat.rdev()), }, "dev/loop0", 0o660, 0, 6, mtime, ) ); assert_eq!(umask, 0); assert!(hardlinks.is_empty()); } #[test] fn test_file_from_line_location_character_device() { let path = canonicalize("/dev/console").unwrap(); let line = path.clone().into_os_string().into_string().unwrap(); let stat = path.symlink_metadata().unwrap(); let rdev = stat.rdev(); let mode = (stat.mode() & MODE_PERMISSION_MASK).try_into().unwrap(); let mtime = stat.mtime().try_into().unwrap(); let mut hardlinks = HashMap::new(); let (file, umask) = File::from_line(&line, &mut hardlinks).unwrap(); assert_eq!( file, File::new( Filetype::CharacterDevice { major: major(rdev), minor: minor(rdev) }, line.strip_prefix("/").unwrap(), mode, stat.uid(), stat.gid(), mtime, ) ); assert_eq!(umask, 0); assert!(hardlinks.is_empty()); } #[test] fn test_file_from_line_location_symlink() { let line = "/bin"; let stat = symlink_metadata("/bin").unwrap(); let mtime = stat.mtime().try_into().unwrap(); let mut hardlinks = HashMap::new(); let (file, umask) = File::from_line(line, &mut hardlinks).unwrap(); assert_eq!( file, File::new( Filetype::Symlink { target: "usr/bin".into() }, "bin", 0o777, 0, 0, mtime, ) ); assert_eq!(umask, 0); assert!(hardlinks.is_empty()); } #[test] fn test_file_from_line_location_fifo() { let line = "/run/initctl"; let stat = match symlink_metadata("/run/initctl") { Ok(s) => s, // This test expects a fifo like /run/initctl being present. Err(_) => return, }; let mtime = stat.mtime().try_into().unwrap(); let mut hardlinks = HashMap::new(); let (file, umask) = File::from_line(line, &mut hardlinks).unwrap(); assert_eq!( file, File::new(Filetype::Fifo, "run/initctl", 0o600, 0, 0, mtime) ); assert_eq!(umask, 0); assert!(hardlinks.is_empty()); } #[test] fn test_file_from_line_location_socket() { let line = "/run/systemd/notify"; let stat = match symlink_metadata("/run/systemd/notify") { Ok(s) => s, // This test expects a socket like /run/systemd/notify being present. Err(_) => return, }; let mtime = stat.mtime().try_into().unwrap(); let mut hardlinks = HashMap::new(); let (file, umask) = File::from_line(line, &mut hardlinks).unwrap(); assert_eq!( file, File::new(Filetype::Socket, "run/systemd/notify", 0o777, 0, 0, mtime) ); assert_eq!(umask, 0); assert!(hardlinks.is_empty()); } #[test] fn test_file_from_line_empty_fields() { let line = "/run\t\t\t\t\t\t"; let stat = symlink_metadata("/run").unwrap(); let mtime = stat.mtime().try_into().unwrap(); let mut hardlinks = HashMap::new(); let (file, umask) = File::from_line(line, &mut hardlinks).unwrap(); assert_eq!( file, File::new(Filetype::Directory, "run", 0o755, 0, 0, mtime) ); assert_eq!(umask, 0); assert!(hardlinks.is_empty()); } #[test] fn test_file_from_line_fields_with_dash() { let line = "/etc\t-\t-\t-\t-\t-\t"; let stat = symlink_metadata("/etc").unwrap(); let mtime = stat.mtime().try_into().unwrap(); let mut hardlinks = HashMap::new(); let (file, umask) = File::from_line(line, &mut hardlinks).unwrap(); assert_eq!( file, File::new(Filetype::Directory, "etc", 0o755, 0, 0, mtime) ); assert_eq!(umask, 0); assert!(hardlinks.is_empty()); } #[test] fn test_manifest_from_input() { let input = b"\ # This is a comment\n\n\ /bin\tbin\tdir\t755\t0\t0\t1681992796\n\ /usr/bin/gzip\tbin/gzip\tfile\t755\t0\t0\t1739259005\t35288\n"; let manifest = Manifest::from_input(input.as_ref(), LOG_LEVEL_WARNING).unwrap(); let stat = symlink_metadata("/usr/bin/gzip").unwrap(); let key = get_hardlink_key(&stat); let expected_archive = Archive::with_files_and_hardlinks( vec![ File::new(Filetype::Directory, "bin", 0o755, 0, 0, 1681992796), File::new( Filetype::Hardlink { key, index: 1 }, "bin/gzip", 0o755, 0, 0, 1739259005, ), ], HashMap::from([(key, Hardlink::new("/usr/bin/gzip", 35288))]), ); assert_eq!(manifest, Manifest::new(vec![expected_archive], 0o022)); } #[test] fn test_manifest_from_input_compressed() { let input = b"\ #cpio: zstd -1\n\ /bin\tbin\tdir\t755\t0\t0\t1681992796\n"; let manifest = Manifest::from_input(input.as_ref(), LOG_LEVEL_WARNING).unwrap(); let expected_archive = Archive::with_files_compressed( vec![File::new( Filetype::Directory, "bin", 0o755, 0, 0, 1681992796, )], Compression::Zstd { level: Some(1) }, ); assert_eq!(manifest, Manifest::new(vec![expected_archive], 0)); } #[test] fn test_manifest_from_input_multiple_uncompressed() { let input = b"\ # This is a comment\n\n\ #cpio\n\ /bin\tbin\tdir\t755\t0\t0\t1681992796\n\ #cpio\n\ /\t.\tdir\t755\t0\t0\t1732230747\n"; let manifest = Manifest::from_input(input.as_ref(), LOG_LEVEL_WARNING).unwrap(); let expected_manifest = Manifest::new( vec![ Archive::with_files(vec![File::new( Filetype::Directory, "bin", 0o755, 0, 0, 1681992796, )]), Archive::with_files(vec![File::new( Filetype::Directory, ".", 0o755, 0, 0, 1732230747, )]), ], 0, ); assert_eq!(manifest, expected_manifest); } #[test] fn test_manifest_from_input_file_not_found() { let input = b"/nonexistent\n"; let got = Manifest::from_input(input.as_ref(), LOG_LEVEL_WARNING).unwrap_err(); assert_eq!(got.kind(), ErrorKind::NotFound); assert_eq!( got.to_string(), "line 1: /nonexistent: No such file or directory (os error 2)" ); } #[test] fn test_manifest_from_input_invalid_cpio_directive() { let input = b" #cpio \n #cpio: zstd \n #cpio something -42 "; let got = Manifest::from_input(input.as_ref(), LOG_LEVEL_WARNING).unwrap_err(); assert_eq!(got.kind(), ErrorKind::InvalidInput); assert_eq!( got.to_string(), "line 3: Unknown cpio archive directive: #cpio something -42" ); } #[test] fn test_manifest_from_input_unknown_compressor() { let input = b"#cpio: brotli\n"; let got = Manifest::from_input(input.as_ref(), LOG_LEVEL_WARNING).unwrap_err(); assert_eq!(got.kind(), ErrorKind::InvalidData); assert_eq!( got.to_string(), "line 1: Unknown compression format: brotli" ); } #[test] fn test_manifest_write_fail_compression() { let temp_dir = make_temp_dir().unwrap(); let root_dir = File::new(Filetype::Directory, ".", 0o755, 0x333, 0x42, 0x6841897B); let archive = Archive::with_files_compressed(vec![root_dir], Compression::Failing); let manifest = Manifest::new(vec![archive], 0o022); let file = std::fs::File::create(temp_dir.join("initrd.img")).unwrap(); let got = manifest .write_archive(Some(file), None, LOG_LEVEL_WARNING) .unwrap_err(); assert_eq!(got.to_string(), "false failed: exit status: 1"); assert_eq!(got.kind(), ErrorKind::Other); } #[test] fn test_archive_write() { let archive = Archive::with_files(vec![ File::new(Filetype::Directory, ".", 0o755, 0x333, 0x42, 0x6841897B), File::new( Filetype::BlockDevice { major: 0x6425, minor: 0x1437, }, "loop0", 0o660, 0x334, 0x43, 0x6862B88B, ), File::new( Filetype::CharacterDevice { major: 0x2E0E, minor: 0x8C75, }, "console", 0o600, 0x335, 0x44, 0x6862B8B4, ), File::new( Filetype::Symlink { target: "usr/bin".into(), }, "bin", 0o777, 0x336, 0x45, 0x62373894, ), File::new(Filetype::Fifo, "initctl", 0o600, 0x337, 0x46, 0x6862B88A), File::new(Filetype::Socket, "notify", 0o777, 0x338, 0x47, 0x681DE2C2), File::new(Filetype::EmptyFile, "fstab", 0x644, 0x339, 0x48, 0x6E44C280), ]); let mut output = Vec::new(); archive .write(&mut output, Some(0x6B49D200), LOG_LEVEL_WARNING) .unwrap(); assert_eq!( std::str::from_utf8(&output).unwrap(), "07070100000000000041ED0000033300000042000000026841897B\ 00000000000000000000000000000000000000000000000200000000\ .\0\ 07070100000001000061B00000033400000043000000016862B88B\ 00000000000000000000000000006425000014370000000600000000\ loop0\0\ 07070100000002000021800000033500000044000000016862B8B4\ 00000000000000000000000000002E0E00008C750000000800000000\ console\0\0\0\ 070701000000030000A1FF00000336000000450000000162373894\ 00000007000000000000000000000000000000000000000400000000\ bin\0\0\0usr/bin\0\ 07070100000004000011800000033700000046000000016862B88A\ 00000000000000000000000000000000000000000000000800000000\ initctl\0\0\0\ 070701000000050000C1FF000003380000004700000001681DE2C2\ 00000000000000000000000000000000000000000000000700000000\ notify\0\0\0\0\ 07070100000006000086440000033900000048000000016B49D200\ 00000000000000000000000000000000000000000000000600000000\ fstab\0\ 070701000000000000000000000000000000000000000100000000\ 00000000000000000000000000000000000000000000000B00000000\ TRAILER!!!\0\0\0\0", ); } #[test] fn test_archive_write_hardlinks() { let temp_dir = make_temp_dir_with_hardlinks().unwrap(); let path = temp_dir.join("a").to_str().unwrap().to_owned(); // This archive data is the output of test_file_from_line_location_hardlink. let archive = Archive::with_files_and_hardlinks( vec![ File::new( Filetype::Hardlink { key: 8921120, index: 1, }, "a", 0o644, 1, 2, 0x6861C7C5, ), File::new( Filetype::Hardlink { key: 8921120, index: 2, }, "b", 0o640, 3, 4, 0x686472CD, ), ], HashMap::from([(8921120, Hardlink::with_references(&path, 7, 2))]), ); let mut output = Vec::new(); archive.write(&mut output, None, LOG_LEVEL_WARNING).unwrap(); assert_eq!( std::str::from_utf8(&output).unwrap(), "07070100000000000081A40000000100000002000000026861C7C5\ 00000000000000000000000000000000000000000000000200000000\ a\0\ 07070100000000000081A0000000030000000400000002686472CD\ 00000007000000000000000000000000000000000000000200000000\ b\0content\0\ 070701000000000000000000000000000000000000000100000000\ 00000000000000000000000000000000000000000000000B00000000\ TRAILER!!!\0\0\0\0", ); } } threecpio-0.8.1/src/seek_forward.rs000064400000000000000000000032241046102023000154250ustar 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 {read} bytes, but {offset} wanted"), )); } Ok(()) } } threecpio-0.8.1/tests/bzip2.cpio000064400000000000000000000013411046102023000146570ustar 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.8.1/tests/cli.rs000064400000000000000000000153441046102023000141020ustar 00000000000000// Copyright (C) 2024, Benjamin Drung // SPDX-License-Identifier: ISC use std::env::{self, temp_dir}; use std::error::Error; use std::fs::File; use std::io::{Read, Write}; use std::process::{Command, Output, Stdio}; use std::time::SystemTime; // 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), "'{expected}' not found in '{stderr}'", ); self } } #[test] fn create_cpio_on_stdout() -> Result<(), Box> { let mut cmd = get_command(); cmd.arg("--create"); let mut process = cmd.stdin(Stdio::piped()).stdout(Stdio::piped()).spawn()?; let mut stdin = process.stdin.as_ref().unwrap(); stdin.write_all(b"/usr\t\t\t\t\t\t1681992796\n")?; let _status = process.wait()?; //let mut stdout = process.stdout.unwrap().take(1000); let mut stdout = process.stdout.unwrap(); let mut cpio = Vec::new(); stdout.read_to_end(&mut cpio)?; assert_eq!( std::str::from_utf8(&cpio).unwrap(), "07070100000000000041ED00000000000000000000000264412C5C\ 00000000000000000000000000000000000000000000000400000000\ usr\0\0\0\ 070701000000000000000000000000000000000000000100000000\ 00000000000000000000000000000000000000000000000B00000000\ TRAILER!!!\0\0\0\0", ); Ok(()) } #[test] fn create_cpio_file() -> Result<(), Box> { let mut path = temp_dir(); let now = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap(); path.push(format!("3cpio-{now:?}.cpio")); let path = path.into_os_string().into_string().unwrap(); let mut cmd = get_command(); cmd.args(["--create", &path]); let mut process = cmd.stdin(Stdio::piped()).spawn()?; let mut stdin = process.stdin.as_ref().unwrap(); stdin.write_all(b"/usr\t\t\t\t\t\t1681992796\n")?; let _status = process.wait()?; let mut cpio = Vec::new(); let mut cpio_file = File::open(&path)?; cpio_file.read_to_end(&mut cpio)?; assert_eq!( std::str::from_utf8(&cpio).unwrap(), "07070100000000000041ED00000000000000000000000264412C5C\ 00000000000000000000000000000000000000000000000400000000\ usr\0\0\0\ 070701000000000000000000000000000000000000000100000000\ 00000000000000000000000000000000000000000000000B00000000\ TRAILER!!!\0\0\0\0", ); Ok(()) } #[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/{compression}.cpio")); cmd.output()? .assert_stderr("") .assert_success() .assert_stdout(format!("0\tcpio\n512\t{compression}\n")); } 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 archive_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/{compression}.cpio")); 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_archive_argument() -> Result<(), Box> { let mut cmd = get_command(); cmd.arg("-t"); cmd.output()? .assert_failure(2) .assert_stderr_contains("missing argument ARCHIVE") .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.8.1/tests/generate000075500000000000000000000031521046102023000144770ustar 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.8.1/tests/gzip.cpio000064400000000000000000000013001046102023000145750ustar 0000000000000007070100000000000041FD000003E8000003E800000002661BE5C600000000000000000000000000000000000000000000000200000000.07070100000001000041FD000003E8000003E800000002661BE5C600000000000000000000000000000000000000000000000500000000path07070100000002000081B4000003E8000003E800000001661BE5C600000008000000000000000000000000000000000000000A00000000path/filecontent 07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!3070704@&n.` m``dffjlf@01 3}&0FiqF4Nǿkm%;&0|yR(Etwq􏵃—Ż&vthreecpio-0.8.1/tests/lzop.cpio000064400000000000000000000014131046102023000146150ustar 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.8.1/tests/path-traversal.cpio000064400000000000000000000010001046102023000165560ustar 0000000000000007070100000000000041FD000003E8000003E800000002661BE5C600000000000000000000000000000000000000000000000200000000.070701000000010000A1FF000003E8000003E800000001661BE5C600000004000000000000000000000000000000000000000400000000tmp/tmp07070100000002000081B4000003E8000003E800000001661BE5C60000000F000000000000000000000000000000000000000D00000000tmp/trav.txtTEST Traversal 07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!threecpio-0.8.1/tests/single.cpio000064400000000000000000000010001046102023000151020ustar 0000000000000007070100000000000041FD000003E8000003E800000002661BE5C600000000000000000000000000000000000000000000000200000000.07070100000001000041FD000003E8000003E800000002661BE5C600000000000000000000000000000000000000000000000500000000path07070100000002000081B4000003E8000003E800000001661BE5C600000008000000000000000000000000000000000000000A00000000path/filecontent 07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!threecpio-0.8.1/tests/xz.cpio000064400000000000000000000013341046102023000142740ustar 0000000000000007070100000000000041FD000003E8000003E800000002661BE5C600000000000000000000000000000000000000000000000200000000.07070100000001000041FD000003E8000003E800000002661BE5C600000000000000000000000000000000000000000000000500000000path07070100000002000081B4000003E8000003E800000001661BE5C600000008000000000000000000000000000000000000000A00000000path/filecontent 07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!7zXZi"6!X] c 6JDHnLҤe#B ؑ"̓՟=i ,"зZ;Wnm=甆)0Ӌ䷥>ǿkm%;&0|yR(Etwq􏵃—Ż&vٹ#9>0 YZthreecpio-0.8.1/tests/zstd.cpio000064400000000000000000000012651046102023000146220ustar 0000000000000007070100000000000041FD000003E8000003E800000002661BE5C600000000000000000000000000000000000000000000000200000000.07070100000001000041FD000003E8000003E800000002661BE5C600000000000000000000000000000000000000000000000500000000path07070100000002000081B4000003E8000003E800000001661BE5C600000008000000000000000000000000000000000000000A00000000path/filecontent 07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!(/`EG`(>"{k}daPlBHr0nZA,L;yy( L9&T)?"Z=EiLO_fwy$.:lPdS72{V0k邏 t0ZkzcV'v+8Hlll\#'