From fd14207a48cbd2220737e111f857d5abfccadbf7 Mon Sep 17 00:00:00 2001 From: Shiva Kiran Koninty Date: Tue, 25 Aug 2026 09:55:49 +0530 Subject: [PATCH 1/9] Add test `verify_cli()` for all programs --- src/drive-info/cli.rs | 12 ++++++++++++ src/iso-cp/cli.rs | 12 ++++++++++++ src/iso-ls/cli.rs | 12 ++++++++++++ src/mmc-cli/cli.rs | 12 ++++++++++++ 4 files changed, 48 insertions(+) diff --git a/src/drive-info/cli.rs b/src/drive-info/cli.rs index 7480f4d..84ed88a 100644 --- a/src/drive-info/cli.rs +++ b/src/drive-info/cli.rs @@ -39,3 +39,15 @@ pub struct DriveArg { #[arg(value_name = "DRIVE")] pub positional: Option, } + +#[cfg(test)] +mod tests { + use clap::CommandFactory; + + use super::*; + + #[test] + fn verify_cli() { + Cli::command().debug_assert(); + } +} diff --git a/src/iso-cp/cli.rs b/src/iso-cp/cli.rs index f157d02..d9dbc63 100644 --- a/src/iso-cp/cli.rs +++ b/src/iso-cp/cli.rs @@ -51,3 +51,15 @@ pub struct FileArg { #[arg(value_name = "FILE")] pub positional: Option, } + +#[cfg(test)] +mod tests { + use clap::CommandFactory; + + use super::*; + + #[test] + fn verify_cli() { + Cli::command().debug_assert(); + } +} diff --git a/src/iso-ls/cli.rs b/src/iso-ls/cli.rs index bd9d404..4f73492 100644 --- a/src/iso-ls/cli.rs +++ b/src/iso-ls/cli.rs @@ -64,3 +64,15 @@ pub struct FileArg { #[arg(value_name = "FILE")] pub positional: Option, } + +#[cfg(test)] +mod tests { + use clap::CommandFactory; + + use super::*; + + #[test] + fn verify_cli() { + Cli::command().debug_assert(); + } +} diff --git a/src/mmc-cli/cli.rs b/src/mmc-cli/cli.rs index 8f7fc85..0b6dfba 100644 --- a/src/mmc-cli/cli.rs +++ b/src/mmc-cli/cli.rs @@ -59,3 +59,15 @@ pub struct MmcActions { #[arg(short = 'S', long)] pub speed: Option, } + +#[cfg(test)] +mod tests { + use clap::CommandFactory; + + use super::*; + + #[test] + fn verify_cli() { + Cli::command().debug_assert(); + } +} From 107ff9bcb301d827e0f8e473f84095bb843e9a18 Mon Sep 17 00:00:00 2001 From: Shiva Kiran Koninty Date: Tue, 25 Aug 2026 12:34:16 +0530 Subject: [PATCH 2/9] iso-cp: Make image path a strictly positional argument --- src/iso-cp/cli.rs | 22 +++++----------------- src/iso-cp/main.rs | 3 +-- tests/iso-cp.rs | 2 -- 3 files changed, 6 insertions(+), 21 deletions(-) diff --git a/src/iso-cp/cli.rs b/src/iso-cp/cli.rs index d9dbc63..8127be0 100644 --- a/src/iso-cp/cli.rs +++ b/src/iso-cp/cli.rs @@ -17,20 +17,20 @@ use std::path::PathBuf; -use clap::{Args, Parser}; +use clap::Parser; /// Extract files from ISO 9660 and UDF files. #[derive(Parser)] #[command(arg_required_else_help = true, version)] pub struct Cli { + /// Path to an ISO 9660 or UDF image. + #[arg(value_name = "IMAGE")] + pub image: PathBuf, + /// Path to the file in the image to extract #[arg(short, long, value_name = "FILE")] pub extract: String, - /// Path to an ISO9660 and/or UDF image - #[command(flatten)] - pub image: FileArg, - /// Path of the output file. Defaults to name of the extracted file. #[arg(short, long, value_name = "FILE")] pub output_file: Option, @@ -40,18 +40,6 @@ pub struct Cli { pub udf: bool, } -#[derive(Args)] -#[group(required = true, multiple = false)] -pub struct FileArg { - /// Path to an ISO9660 and/or UDF image - #[arg(short = 'i', long = "image", value_name = "FILE")] - pub option: Option, - - /// Path to an ISO9660 and/or UDF image - #[arg(value_name = "FILE")] - pub positional: Option, -} - #[cfg(test)] mod tests { use clap::CommandFactory; diff --git a/src/iso-cp/main.rs b/src/iso-cp/main.rs index 1c21c12..522712e 100644 --- a/src/iso-cp/main.rs +++ b/src/iso-cp/main.rs @@ -31,8 +31,7 @@ fn main() -> Result<()> { tracing_subscriber::fmt() .with_env_filter(EnvFilter::from_default_env()) .init(); - let image = cli.image.positional.or(cli.image.option) - .expect( "the cli logic must ensure that the file argument is provided either as a positional or as an option"); + let image = cli.image; if !image.exists() { bail!("could not open input file at {}", image.display()); } diff --git a/tests/iso-cp.rs b/tests/iso-cp.rs index dc1243d..c0a917b 100644 --- a/tests/iso-cp.rs +++ b/tests/iso-cp.rs @@ -14,7 +14,6 @@ fn extract_udf() { cmd() .arg("-e") .arg("licenses/COPYING") - .arg("-i") .arg(UDF_FILE) .arg("-o") .arg(output.path()) @@ -33,7 +32,6 @@ fn extract_iso9660() { cmd() .arg("-e") .arg("copying") - .arg("-i") .arg(ISO9660_FILE) .arg("-o") .arg(output.path()) From b2dbb351c320d088ee095b40160e8648cae224e4 Mon Sep 17 00:00:00 2001 From: Shiva Kiran Koninty Date: Tue, 25 Aug 2026 12:35:45 +0530 Subject: [PATCH 3/9] iso-cp: Update summary text --- src/iso-cp/cli.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/iso-cp/cli.rs b/src/iso-cp/cli.rs index 8127be0..6a37c13 100644 --- a/src/iso-cp/cli.rs +++ b/src/iso-cp/cli.rs @@ -19,7 +19,7 @@ use std::path::PathBuf; use clap::Parser; -/// Extract files from ISO 9660 and UDF files. +/// Copy files from an ISO 9660 or UDF filesystem. #[derive(Parser)] #[command(arg_required_else_help = true, version)] pub struct Cli { From d560dfa835ae689fb62d0f9cd3e7bd53c54704c2 Mon Sep 17 00:00:00 2001 From: Shiva Kiran Koninty Date: Tue, 25 Aug 2026 12:38:30 +0530 Subject: [PATCH 4/9] iso-cp: Make `extract` option as a positional argument `SOURCE` --- src/iso-cp/cli.rs | 6 +++--- src/iso-cp/main.rs | 14 +++++++------- tests/iso-cp.rs | 6 ++---- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/iso-cp/cli.rs b/src/iso-cp/cli.rs index 6a37c13..b7346f8 100644 --- a/src/iso-cp/cli.rs +++ b/src/iso-cp/cli.rs @@ -27,9 +27,9 @@ pub struct Cli { #[arg(value_name = "IMAGE")] pub image: PathBuf, - /// Path to the file in the image to extract - #[arg(short, long, value_name = "FILE")] - pub extract: String, + /// Path to a source file in the image. + #[arg(value_name = "SOURCE")] + pub source: String, /// Path of the output file. Defaults to name of the extracted file. #[arg(short, long, value_name = "FILE")] diff --git a/src/iso-cp/main.rs b/src/iso-cp/main.rs index 522712e..15ccf28 100644 --- a/src/iso-cp/main.rs +++ b/src/iso-cp/main.rs @@ -36,22 +36,22 @@ fn main() -> Result<()> { bail!("could not open input file at {}", image.display()); } - let output = cli.output_file.unwrap_or(PathBuf::from(&cli.extract)); + let output = cli.output_file.unwrap_or(PathBuf::from(&cli.source)); let mut output = File::create(output).context("could not create output file")?; if cli.udf { - udf_extract(image, cli.extract, &mut output)?; + udf_extract(image, cli.source, &mut output)?; } else { - iso9660_extract(image, cli.extract, &mut output)?; + iso9660_extract(image, cli.source, &mut output)?; } Ok(()) } /// Extract given file from a UDF image. -fn udf_extract(image: PathBuf, extract: String, output: &mut File) -> Result<()> { +fn udf_extract(image: PathBuf, source: String, output: &mut File) -> Result<()> { let udf = Udf::new(image)?; - let entry = udf.entry(extract)?; + let entry = udf.entry(source)?; io::copy(&mut entry.reader(), output)?; @@ -59,9 +59,9 @@ fn udf_extract(image: PathBuf, extract: String, output: &mut File) -> Result<()> } /// Extract given file from an ISO 9660 image. -fn iso9660_extract(image: PathBuf, extract: String, output: &mut File) -> Result<()> { +fn iso9660_extract(image: PathBuf, source: String, output: &mut File) -> Result<()> { let iso = Iso::new(image.clone())?; - let entry = iso.entry(extract)?; + let entry = iso.entry(source)?; io::copy(&mut entry.reader(), output)?; diff --git a/tests/iso-cp.rs b/tests/iso-cp.rs index c0a917b..91811ee 100644 --- a/tests/iso-cp.rs +++ b/tests/iso-cp.rs @@ -12,9 +12,8 @@ static UDF_FILE: &str = "tests/data/udf1.iso"; fn extract_udf() { let output = NamedTempFile::new("out").unwrap(); cmd() - .arg("-e") - .arg("licenses/COPYING") .arg(UDF_FILE) + .arg("licenses/COPYING") .arg("-o") .arg(output.path()) .arg("-U") @@ -30,9 +29,8 @@ static ISO9660_FILE: &str = "tests/data/xa.iso"; fn extract_iso9660() { let output = NamedTempFile::new("out").unwrap(); cmd() - .arg("-e") - .arg("copying") .arg(ISO9660_FILE) + .arg("copying") .arg("-o") .arg(output.path()) .assert() From 64b63dad42cfbb73f6ea555ff1bef6fc6dc8c5b5 Mon Sep 17 00:00:00 2001 From: Shiva Kiran Koninty Date: Tue, 25 Aug 2026 12:44:15 +0530 Subject: [PATCH 5/9] iso-cp: Make `output_file` into a positional argument `DESTINATION` --- src/iso-cp/cli.rs | 6 +++--- src/iso-cp/main.rs | 2 +- tests/iso-cp.rs | 2 -- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/iso-cp/cli.rs b/src/iso-cp/cli.rs index b7346f8..66d95bc 100644 --- a/src/iso-cp/cli.rs +++ b/src/iso-cp/cli.rs @@ -31,9 +31,9 @@ pub struct Cli { #[arg(value_name = "SOURCE")] pub source: String, - /// Path of the output file. Defaults to name of the extracted file. - #[arg(short, long, value_name = "FILE")] - pub output_file: Option, + /// Path to a destination file or directory. + #[arg(value_name = "DESTINATION")] + pub destination: PathBuf, /// Use UDF #[arg(short = 'U', long)] diff --git a/src/iso-cp/main.rs b/src/iso-cp/main.rs index 15ccf28..af71e91 100644 --- a/src/iso-cp/main.rs +++ b/src/iso-cp/main.rs @@ -36,7 +36,7 @@ fn main() -> Result<()> { bail!("could not open input file at {}", image.display()); } - let output = cli.output_file.unwrap_or(PathBuf::from(&cli.source)); + let output = cli.destination; let mut output = File::create(output).context("could not create output file")?; if cli.udf { diff --git a/tests/iso-cp.rs b/tests/iso-cp.rs index 91811ee..e942113 100644 --- a/tests/iso-cp.rs +++ b/tests/iso-cp.rs @@ -14,7 +14,6 @@ fn extract_udf() { cmd() .arg(UDF_FILE) .arg("licenses/COPYING") - .arg("-o") .arg(output.path()) .arg("-U") .assert() @@ -31,7 +30,6 @@ fn extract_iso9660() { cmd() .arg(ISO9660_FILE) .arg("copying") - .arg("-o") .arg(output.path()) .assert() .success(); From 7f6150bb8339200a5eb8d0c38b092abca8e22167 Mon Sep 17 00:00:00 2001 From: Shiva Kiran Koninty Date: Tue, 25 Aug 2026 12:57:43 +0530 Subject: [PATCH 6/9] iso-cp: Remove `udf` option in favor of auto selection --- src/iso-cp/cli.rs | 4 ---- src/iso-cp/main.rs | 24 +++++++++++++----------- tests/iso-cp.rs | 1 - 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/iso-cp/cli.rs b/src/iso-cp/cli.rs index 66d95bc..3f48c68 100644 --- a/src/iso-cp/cli.rs +++ b/src/iso-cp/cli.rs @@ -34,10 +34,6 @@ pub struct Cli { /// Path to a destination file or directory. #[arg(value_name = "DESTINATION")] pub destination: PathBuf, - - /// Use UDF - #[arg(short = 'U', long)] - pub udf: bool, } #[cfg(test)] diff --git a/src/iso-cp/main.rs b/src/iso-cp/main.rs index af71e91..e2ed898 100644 --- a/src/iso-cp/main.rs +++ b/src/iso-cp/main.rs @@ -15,7 +15,7 @@ // You should have received a copy of the GNU General Public License // along with libcdio-cli. If not, see . -use std::{fs::File, io, path::PathBuf}; +use std::{fs::File, io}; use anyhow::{Context, Result, bail}; use clap::Parser; @@ -39,18 +39,21 @@ fn main() -> Result<()> { let output = cli.destination; let mut output = File::create(output).context("could not create output file")?; - if cli.udf { - udf_extract(image, cli.source, &mut output)?; - } else { - iso9660_extract(image, cli.source, &mut output)?; - } + let iso_err = match Iso::new(image.clone()) { + Ok(iso) => return iso9660_extract(&iso, cli.source, &mut output), + Err(err) => err, + }; - Ok(()) + match Udf::new(image) { + Ok(udf) => udf_extract(&udf, cli.source, &mut output), + Err(udf_err) => bail!( + "could not open file as ISO 9660 or UDF\n ISO error: {iso_err:?}\nUDF error: {udf_err:?}", + ), + } } /// Extract given file from a UDF image. -fn udf_extract(image: PathBuf, source: String, output: &mut File) -> Result<()> { - let udf = Udf::new(image)?; +fn udf_extract(udf: &Udf, source: String, output: &mut File) -> Result<()> { let entry = udf.entry(source)?; io::copy(&mut entry.reader(), output)?; @@ -59,8 +62,7 @@ fn udf_extract(image: PathBuf, source: String, output: &mut File) -> Result<()> } /// Extract given file from an ISO 9660 image. -fn iso9660_extract(image: PathBuf, source: String, output: &mut File) -> Result<()> { - let iso = Iso::new(image.clone())?; +fn iso9660_extract(iso: &Iso, source: String, output: &mut File) -> Result<()> { let entry = iso.entry(source)?; io::copy(&mut entry.reader(), output)?; diff --git a/tests/iso-cp.rs b/tests/iso-cp.rs index e942113..54ac3d8 100644 --- a/tests/iso-cp.rs +++ b/tests/iso-cp.rs @@ -15,7 +15,6 @@ fn extract_udf() { .arg(UDF_FILE) .arg("licenses/COPYING") .arg(output.path()) - .arg("-U") .assert() .success(); From 928377fa0eda1674740011b7e4133cac91bcc1bf Mon Sep 17 00:00:00 2001 From: Shiva Kiran Koninty Date: Tue, 25 Aug 2026 14:05:01 +0530 Subject: [PATCH 7/9] iso-cp: Handle directories at `destination` --- src/iso-cp/main.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/iso-cp/main.rs b/src/iso-cp/main.rs index e2ed898..3951f43 100644 --- a/src/iso-cp/main.rs +++ b/src/iso-cp/main.rs @@ -15,7 +15,7 @@ // You should have received a copy of the GNU General Public License // along with libcdio-cli. If not, see . -use std::{fs::File, io}; +use std::{fs::File, io, path::PathBuf}; use anyhow::{Context, Result, bail}; use clap::Parser; @@ -36,7 +36,13 @@ fn main() -> Result<()> { bail!("could not open input file at {}", image.display()); } - let output = cli.destination; + let output = if cli.destination.is_dir() { + let source = PathBuf::from(&cli.source); + let source_file = source.file_name().context("invalid source file name")?; + cli.destination.join(source_file) + } else { + cli.destination + }; let mut output = File::create(output).context("could not create output file")?; let iso_err = match Iso::new(image.clone()) { From b9c50370252a48768ea394047abfabcfe874c1b3 Mon Sep 17 00:00:00 2001 From: Shiva Kiran Koninty Date: Tue, 25 Aug 2026 14:07:35 +0530 Subject: [PATCH 8/9] iso-cp: Return error if source is a directory --- src/iso-cp/cli.rs | 2 ++ src/iso-cp/main.rs | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/src/iso-cp/cli.rs b/src/iso-cp/cli.rs index 3f48c68..4d822e2 100644 --- a/src/iso-cp/cli.rs +++ b/src/iso-cp/cli.rs @@ -28,6 +28,8 @@ pub struct Cli { pub image: PathBuf, /// Path to a source file in the image. + /// + /// Directories are currently not supported. #[arg(value_name = "SOURCE")] pub source: String, diff --git a/src/iso-cp/main.rs b/src/iso-cp/main.rs index 3951f43..98d39d2 100644 --- a/src/iso-cp/main.rs +++ b/src/iso-cp/main.rs @@ -61,6 +61,9 @@ fn main() -> Result<()> { /// Extract given file from a UDF image. fn udf_extract(udf: &Udf, source: String, output: &mut File) -> Result<()> { let entry = udf.entry(source)?; + if entry.is_dir() { + bail!("copying directories is currently not supported"); + } io::copy(&mut entry.reader(), output)?; @@ -70,6 +73,9 @@ fn udf_extract(udf: &Udf, source: String, output: &mut File) -> Result<()> { /// Extract given file from an ISO 9660 image. fn iso9660_extract(iso: &Iso, source: String, output: &mut File) -> Result<()> { let entry = iso.entry(source)?; + if entry.is_dir() { + bail!("copying directories is currently not supported"); + } io::copy(&mut entry.reader(), output)?; From 52bc73155cce67125c729781c0d8c53faefe9729 Mon Sep 17 00:00:00 2001 From: Shiva Kiran Koninty Date: Tue, 25 Aug 2026 15:44:39 +0530 Subject: [PATCH 9/9] iso-cp: Add example in `README.md` --- README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/README.md b/README.md index 0dbb8ed..cd7be8e 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,31 @@ Utilities to work with CD/DVD media, ISO 9660 and UDF images. | iso-ls | List files of ISO 9660 and UDF filesystems. | | mmc-cli | Issue SCSI MMC commands to a drive. | +## iso-cp +Copies files from ISO 9660 or UDF filesystem. +```console +$ iso-cp -h +Copy files from an ISO 9660 or UDF filesystem + +Usage: iso-cp + +Arguments: + Path to an ISO 9660 or UDF image + Path to a source file in the image + Path to a destination file or directory + +Options: + -h, --help Print help (see more with '--help') + -V, --version Print version +$ # Copying a license file from a UDF filesystem +$ iso-cp tests/data/udf1.iso licenses/COPYING.LESSER ./lgpl +$ cat lgpl | head -2 + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 +``` + +Copying whole directories is currently not supported. + ## Install - Install [Rust][rust-install]. - Install [clang][bindgen-reqs].