Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,49 @@ Utilities to work with CD/DVD media, ISO 9660 and UDF images.
cargo install libcdio-cli
```

## iso-ls
Lists files of an ISO 9660 or UDF filesystem.
```console
$ iso-ls -h
Inspect metadata and list contents of ISO 9660 and UDF files

Usage: iso-ls [OPTIONS] <IMAGE>

Arguments:
<IMAGE> Path to an ISO 9660 or UDF image

Options:
-m, --metadata Print image metadata
-h, --help Print help (see more with '--help')
-V, --version Print version
```

Listing the contents of a UDF filesystem:
```console
$ iso-ls tests/data/udf1.iso
/:
dr-xr-xr-x 2000 3000 2 88 Jun 19 2026 20:42:57 .
dr-xr-xr-x 2000 3000 1 144 Jun 19 2026 20:42:57 licenses

/licenses/:
dr-xr-xr-x 2000 3000 2 88 Jun 19 2026 20:42:57 .
-r--r--r-- 2000 3000 1 35149 Jun 19 2026 20:41:12 COPYING
-r--r--r-- 2000 3000 1 7652 Jun 19 2026 20:41:16 COPYING.LESSER
```

Listing the image metadata of an ISO 9660 filesystem:
```console
$ iso-ls -m tests/data/joliet.iso
Image : tests/data/joliet.iso
Application : K3B THE CD KREATOR VERSION 0.11.12 (C) 2003 SEBASTIAN TRUEG AND THE K3B TEAM
Preparer : K3b - Version 0.11.12
Publisher : Rocky Bernstein
System : LINUX
Volume : K3b data project
Joliet : Level 3
Rock Ridge : no
```

## Development
### Use the provided Git Hooks
These are set to perform lint and formatting checks before every
Expand Down
12 changes: 12 additions & 0 deletions src/drive-info/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,15 @@ pub struct DriveArg {
#[arg(value_name = "DRIVE")]
pub positional: Option<PathBuf>,
}

#[cfg(test)]
mod tests {
use clap::CommandFactory;

use super::*;

#[test]
fn verify_cli() {
Cli::command().debug_assert();
}
}
12 changes: 12 additions & 0 deletions src/iso-cp/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,15 @@ pub struct FileArg {
#[arg(value_name = "FILE")]
pub positional: Option<PathBuf>,
}

#[cfg(test)]
mod tests {
use clap::CommandFactory;

use super::*;

#[test]
fn verify_cli() {
Cli::command().debug_assert();
}
}
51 changes: 15 additions & 36 deletions src/iso-ls/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,50 +17,29 @@

use std::path::PathBuf;

use clap::{Args, Parser};
use clap::Parser;

/// Inspect metadata and list contents of ISO 9660 and UDF files.
#[derive(Parser)]
#[command(arg_required_else_help = true, long_about = libcdio_cli::HEADER, version)]
pub struct Cli {
/// The file argument as an option or a positional argument
#[command(flatten)]
pub file: FileArg,
/// Path to an ISO 9660 or UDF image.
#[arg(value_name = "IMAGE")]
pub image: PathBuf,

/// Show contents of ISO9660 image in long listing format
#[arg(short = 'l', long, group = "listing")]
pub iso9660: bool,

/// Do not use Rock Ridge extensions
#[arg(long)]
pub no_rock_ridge: bool,

/// Do not use CD-ROM XA extensions
#[arg(long)]
pub no_xa: bool,

/// Check if the image uses Rock Ridge extensions by considering a maximum
/// of FILE_COUNT files. Provide '0' to check all files.
#[arg(short = 'r', long, value_name = "FILE_COUNT")]
pub show_rock_ridge: Option<u64>,

/// Produce only error outputs.
/// Print image metadata.
#[arg(short, long)]
pub quiet: bool,

/// Show contents of UDF image in long listing format
#[arg(short = 'U', long, group = "listing")]
pub udf: bool,
pub metadata: bool,
}

#[derive(Args)]
#[group(required = true, multiple = false)]
pub struct FileArg {
/// Path to an ISO9660 and/or UDF image
#[arg(short = 'i', long = "input", value_name = "FILE")]
pub option: Option<PathBuf>,
#[cfg(test)]
mod tests {
use clap::CommandFactory;

use super::*;

/// Path to an ISO9660 and/or UDF image
#[arg(value_name = "FILE")]
pub positional: Option<PathBuf>,
#[test]
fn verify_cli() {
Cli::command().debug_assert();
}
}
89 changes: 29 additions & 60 deletions src/iso-ls/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,53 +33,40 @@ use crate::cli::Cli;

const DATE_FMT: &[BorrowedFormatItem] =
format_description!("[month repr:short] [day] [year] [hour]:[minute]:[second]");
static LINE: &str = "__________________________________";

fn main() -> Result<()> {
let cli = Cli::parse();
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.init();
let mut output: &mut dyn io::Write = if cli.quiet {
&mut io::sink()
} else {
&mut io::stdout()
};
let file = cli.file.positional.or(cli.file.option).expect(
"the cli logic must ensure that the file argument is provided either as a positional or as an option",
);

if cli.udf {
return print_udf_contents(file, &mut output);
}
let mut output = &mut io::stdout();
let file = cli.image;

let iso = Iso::new(file.clone())?;
print_iso9660_metadata(&iso, &file, &mut output)
.context("io error while printing iso9660 metadata")?;

if cli.show_rock_ridge.is_some() {
let file_limit = cli.show_rock_ridge.filter(|file_limit| *file_limit != 0);
print_rock_ridge(&iso, file_limit, &mut output)
.context("io error while printing rock ridge status")?;
if cli.metadata {
let iso = Iso::new(file.clone())?;
return print_iso9660_metadata(&iso, &file, &mut output).map_err(Into::into);
}

print_joliet_level(&iso, &mut output).context("io error while printing joliet level")?;
let Err(udf_err) = print_udf_contents(file.clone(), &mut output) else {
return Ok(());
};

if cli.iso9660 {
print_iso9660_contents(&iso, &mut output, !cli.no_rock_ridge, !cli.no_xa)
.context("error printing iso9660 contents")?;
match Iso::new(file) {
Ok(iso) => {
print_iso9660_contents(&iso, &mut output).context("error printing iso9660 contents")
}
Err(iso_err) => bail!(
"could not open image as UDF or ISO 9660:\n udf error: {udf_err:?}\n iso error: {iso_err:?}",
),
}

Ok(())
}

fn print_iso9660_metadata(
iso: &Iso,
path: &Path,
mut out: impl io::Write,
) -> Result<(), io::Error> {
writeln!(out, "{LINE}")?;
writeln!(out, "ISO 9660 image: {}", path.display())?;
writeln!(out, "Image : {}", path.display())?;
let mut write_if_some = |key, val| {
let Some(val) = val else { return Ok(()) };
writeln!(out, "{key} : {val}")
Expand All @@ -91,36 +78,26 @@ fn print_iso9660_metadata(
write_if_some("Volume ", iso.volume())?;
write_if_some("Volume Set ", iso.volume_set())?;

Ok(())
}
let joliet_level = iso.joliet_level().map(|j| format!("Level {}", u8::from(j)));
writeln!(
out,
"Joliet : {}",
joliet_level.as_deref().unwrap_or("no")
)?;

fn print_rock_ridge(
iso: &Iso,
file_limit: Option<u64>,
mut out: impl io::Write,
) -> Result<(), io::Error> {
let status = match iso.have_rock_ridge(file_limit) {
Ok(true) => "yes",
Ok(false) => "no",
_ => "possibly not",
if let Ok(r) = iso.have_rock_ridge(None) {
writeln!(out, "Rock Ridge : {}", if r { "yes" } else { "no" })?;
};
writeln!(out, "Rock Ridge : {}", status)

Ok(())
}

/// Outputs the file contents of the ISO 9660 image in an ls-like listing format.
fn print_iso9660_contents(
iso: &Iso,
mut out: impl io::Write,
use_rock_ridge: bool,
use_xa: bool,
) -> Result<()> {
fn print_iso9660_contents(iso: &Iso, mut out: impl io::Write) -> Result<()> {
const ISO9660_DEPTH_LIMIT: usize = 512;
let mut dirs = VecDeque::new();
dirs.push_back(("/".to_owned(), 0)); // (path, depth)

writeln!(out, "{}", LINE)?;
writeln!(out, "ISO-9660 Information")?;

while let Some((dir_path, depth)) = dirs.pop_front() {
if depth == ISO9660_DEPTH_LIMIT {
bail!("directory recursion too deep. ISO most probably damaged");
Expand All @@ -129,7 +106,7 @@ fn print_iso9660_contents(
writeln!(out, "{}:", dir_path)?;

for entry in iso.read_dir(dir_path.clone())? {
let rock_ridge = use_rock_ridge.then_some(entry.rock_ridge()).flatten();
let rock_ridge = entry.rock_ridge();
let entry_name = if rock_ridge.is_none() {
entry.filename()?
} else {
Expand All @@ -154,7 +131,7 @@ fn print_iso9660_contents(
write!(out, " {}", rock.group_id)?;
write!(out, " [LSN {:6}]", entry.lsn())?;
write!(out, " {:9}", total_size)?;
} else if use_xa && let Some(xa) = entry.xa() {
} else if let Some(xa) = entry.xa() {
write!(out, " {}", xa_file_mode_str(xa.file_attr))?;
write!(out, " {}", xa.user_id)?;
write!(out, " {}", xa.group_id)?;
Expand Down Expand Up @@ -275,11 +252,3 @@ fn print_udf_contents(path: PathBuf, out: &mut dyn io::Write) -> Result<()> {

Ok(())
}

fn print_joliet_level(iso: &Iso, mut out: impl io::Write) -> Result<(), io::Error> {
let Some(joliet_level) = iso.joliet_level() else {
return writeln!(out, "No Joliet extensions");
};

writeln!(out, "Joliet Level: {}", u8::from(joliet_level))
}
12 changes: 12 additions & 0 deletions src/mmc-cli/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,15 @@ pub struct MmcActions {
#[arg(short = 'S', long)]
pub speed: Option<u16>,
}

#[cfg(test)]
mod tests {
use clap::CommandFactory;

use super::*;

#[test]
fn verify_cli() {
Cli::command().debug_assert();
}
}
Loading