diff --git a/man/3cpio.1.adoc b/man/3cpio.1.adoc index 1f75ad5..c14e700 100644 --- a/man/3cpio.1.adoc +++ b/man/3cpio.1.adoc @@ -22,7 +22,7 @@ Benjamin Drung *3cpio* {*-t*|*--list*} [*-v*|*--debug*] [*-P* _LIST_] _ARCHIVE_ [_pattern_...] *3cpio* {*-x*|*--extract*} [*-v*|*--debug*] [*-C* _DIR_] [*-P* _LIST_] [*-p*] [*-s* _NAME_] -[*--to-stdout*] [*--force*] _ARCHIVE_ [_pattern_...] +[*--to-stdout*] [*--force*] [*-F*] _ARCHIVE_ [*-F*|*--file* _ARCHIVE_...] [_pattern_...] *3cpio* {*-V*|*--version*} @@ -74,8 +74,10 @@ Following compression formats are supported: bzip2, gzip, lz4, lzma, lzop, xz, z matching at least one of those __pattern__s. These __pattern__s are shell wildcard patterns (see *glob*(7)). -*-x*, *--extract* _ARCHIVE_ [_pattern_...]:: +*-x*, *--extract* [*-F*|*--file*] _ARCHIVE_ [*-F*|*--file* _ARCHIVE_] [_pattern_...]:: Extract cpio archives. + Multiple archives can be specified with *-F*. + The option *-F* can be omitted for the first archive. If one or more __pattern__s are supplied, extract only files matching at least one of those __pattern__s. These __pattern__s are shell wildcard patterns (see *glob*(7)). diff --git a/src/main.rs b/src/main.rs index 44ac507..7ceaffe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,7 +28,7 @@ struct Args { force: bool, list: bool, log_level: Level, - archive: Option, + archives: Vec, make_directories: bool, parts: Option, patterns: Vec, @@ -58,7 +58,9 @@ fn print_help() { {executable} {{-c|--create}} [-v|--debug] [-C DIR] [--data-align BYTES] [ARCHIVE] < manifest {executable} {{-e|--examine}} [--raw] ARCHIVE {executable} {{-t|--list}} [-v|--debug] [-P LIST] ARCHIVE [pattern...] - {executable} {{-x|--extract}} [-v|--debug] [-C DIR] [--make-directories] [-P LIST] [-p] [-s NAME] [--to-stdout] [--force] ARCHIVE [pattern...] + {executable} {{-x|--extract}} [-v|--debug] [-C DIR] [--make-directories] [-P LIST] \ + [-p] [-s NAME] [--to-stdout] [--force] [-F|--file] \ + ARCHIVE [-F|--file ARCHIVE...] [pattern...] Optional arguments: --count Print the number of concatenated cpio archives. @@ -103,7 +105,7 @@ fn parse_args() -> Result { let mut list = 0; let mut log_level = Level::Warning; let mut directory = ".".into(); - let mut archive = None; + let mut archives = Vec::new(); let mut make_directories = false; let mut patterns = Vec::new(); let mut raw = false; @@ -179,8 +181,11 @@ fn parse_args() -> Result { Short('x') | Long("extract") => { extract = 1; } - Value(val) if archive.is_none() => { - archive = Some(val.string()?); + Value(val) if archives.is_empty() => { + archives.push(val.string()?); + } + Short('F') | Long("file") => { + archives.push(parser.value()?.string()?); } Value(val) => arguments.push(val.string()?), _ => return Err(arg.unexpected()), @@ -210,9 +215,12 @@ fn parse_args() -> Result { } } - if create != 1 && archive.is_none() { + if create != 1 && archives.is_empty() { return Err("missing argument ARCHIVE".into()); } + if extract != 1 && archives.len() > 1 { + return Err("specifying multiple --file=ARCHIVE is only supported for --extract".into()); + } Ok(Args { count: count == 1, @@ -224,7 +232,7 @@ fn parse_args() -> Result { force, list: list == 1, log_level, - archive, + archives, make_directories, parts, patterns, @@ -342,7 +350,7 @@ fn main() -> ExitCode { if args.create { let mut archive = None; - if let Some(path) = args.archive.as_ref() { + if let Some(path) = args.archives.first() { archive = match File::create(path) { Ok(f) => Some(f), Err(e) => { @@ -368,7 +376,7 @@ fn main() -> ExitCode { _ => { eprintln!( "{executable}: Error: Failed to create '{}': {error}", - args.archive.unwrap_or("cpio on stdout".into()), + args.archives.first().unwrap_or(&"cpio on stdout".into()), ); return ExitCode::FAILURE; } @@ -377,16 +385,17 @@ fn main() -> ExitCode { 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; - } - }; + let mut opened_archives = Vec::new(); + for path in &args.archives { + let archive = match File::open(path) { + Ok(f) => f, + Err(e) => { + eprintln!("{executable}: Error: Failed to open '{path}': {e}"); + return ExitCode::FAILURE; + } + }; + opened_archives.push((path.clone(), archive)); + } if args.extract && !args.to_stdout { if let Err(e) = create_and_set_current_dir(&args.directory, args.force) { @@ -402,16 +411,15 @@ fn main() -> ExitCode { return ExitCode::FAILURE; } }; - let (operation, result) = operate_on_archive(archive, &args, &cwd, &mut logger); - if let Err(e) = result { - match e.kind() { - ErrorKind::BrokenPipe => {} - _ => { - eprintln!( - "{executable}: Error: Failed to {operation} of '{}': {e}", - args.archive.as_deref().unwrap(), - ); - return ExitCode::FAILURE; + for (path, archive) in opened_archives { + let (operation, result) = operate_on_archive(archive, &args, &cwd, &mut logger); + if let Err(e) = result { + match e.kind() { + ErrorKind::BrokenPipe => break, + _ => { + eprintln!("{executable}: Error: Failed to {operation} of '{path}': {e}"); + return ExitCode::FAILURE; + } } } } diff --git a/tests/cli.rs b/tests/cli.rs index 91efff6..f33c5c4 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -348,6 +348,19 @@ fn test_create_missing_path() -> Result<(), Box> { Ok(()) } +#[test] +fn test_create_multiple() -> Result<(), Box> { + let mut cmd = get_command(); + cmd.args(["--create", "--file", "/tmp/first", "-F", "/tmp/second"]); + cmd.output()? + .assert_failure(2) + .assert_stderr_contains( + "Error: specifying multiple --file=ARCHIVE is only supported for --extract", + ) + .assert_stdout(""); + Ok(()) +} + #[test] fn test_create_uncompressed_plus_zstd_on_stdout() -> Result<(), Box> { let mut cmd = get_command(); @@ -462,6 +475,30 @@ fn test_examine_single_cpio_raw() -> Result<(), Box> { Ok(()) } +#[test] +fn test_extract_multiple() -> Result<(), Box> { + let tempdir = TempDir::new()?; + let mut cmd = get_command(); + cmd.arg("-x") + .arg("-C") + .arg(&tempdir.path) + .arg("-v") + .arg("-F") + .arg("tests/single.cpio") + .arg("--file") + .arg("tests/shell.cpio"); + + println!("tempdir = {:?}", tempdir.path); + cmd.output()? + .assert_stderr(".\npath\npath/file\n.\nusr\nusr/bin\nusr/bin/ash\nusr/bin/sh\n") + .assert_success() + .assert_stdout(""); + assert!(tempdir.path.join("path/file").exists()); + assert!(tempdir.path.join("usr/bin/ash").exists()); + assert!(tempdir.path.join("usr/bin/sh").exists()); + Ok(()) +} + #[test] fn test_extract_to_stdout() -> Result<(), Box> { let mut cmd = get_command(); diff --git a/tests/generate b/tests/generate index 0c816e9..7db9037 100755 --- a/tests/generate +++ b/tests/generate @@ -29,8 +29,8 @@ 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" ln -s sh "$input/shell/usr/bin/ash" -generate_cpio "${input}/shell" > "$input/shell.cpio" -touch --date="@${SOURCE_DATE_EPOCH}" "$input/shell.cpio" +generate_cpio "${input}/shell" > shell.cpio +touch --date="@${SOURCE_DATE_EPOCH}" shell.cpio mkdir -p "$input/bigdata" dd bs=1000 count=102500 if=/dev/zero of="$input/bigdata/zeros" @@ -38,27 +38,27 @@ generate_cpio "${input}/bigdata" > "$input/bigdata.cpio" touch --date="@${SOURCE_DATE_EPOCH}" "$input/bigdata.cpio" cp single.cpio bzip2.cpio -bzip2 -9 < "$input/shell.cpio" >> bzip2.cpio +bzip2 -9 < shell.cpio >> bzip2.cpio cp single.cpio gzip.cpio -gzip -n -9 < "$input/shell.cpio" >> gzip.cpio +gzip -n -9 < shell.cpio >> gzip.cpio cp single.cpio lz4.cpio -lz4 -l -9 < "$input/shell.cpio" >> lz4.cpio +lz4 -l -9 < shell.cpio >> lz4.cpio cp single.cpio lzma.cpio -lzma -9 < "$input/shell.cpio" >> lzma.cpio +lzma -9 < shell.cpio >> lzma.cpio cp single.cpio lzop.cpio -lzop -9 -c "$input/shell.cpio" >> lzop.cpio +lzop -9 -c shell.cpio >> lzop.cpio cp single.cpio xz.cpio -xz --check=crc32 --threads=1 -9 < "$input/shell.cpio" >> xz.cpio +xz --check=crc32 --threads=1 -9 < shell.cpio >> xz.cpio cp single.cpio zstd.cpio -zstd -q -9 < "$input/shell.cpio" >> zstd.cpio +zstd -q -9 < shell.cpio >> zstd.cpio -cat single.cpio "$input/shell.cpio" > bigdata.cpio +cat single.cpio shell.cpio > bigdata.cpio zstd -q -9 < "$input/bigdata.cpio" >> bigdata.cpio mkdir "$input/path-traversal" diff --git a/tests/shell.cpio b/tests/shell.cpio new file mode 100644 index 0000000..a928956 Binary files /dev/null and b/tests/shell.cpio differ