diff --git a/libcdio-cli/src/iso-info/cli.rs b/libcdio-cli/src/iso-info/cli.rs index 258a8b7..bd9d404 100644 --- a/libcdio-cli/src/iso-info/cli.rs +++ b/libcdio-cli/src/iso-info/cli.rs @@ -31,10 +31,6 @@ pub struct Cli { #[arg(short = 'l', long, group = "listing")] pub iso9660: bool, - /// Do not use Joliet extensions - #[arg(long)] - pub no_joliet: bool, - /// Do not use Rock Ridge extensions #[arg(long)] pub no_rock_ridge: bool, diff --git a/libcdio-cli/src/iso-info/main.rs b/libcdio-cli/src/iso-info/main.rs index 7a0f509..7d251da 100644 --- a/libcdio-cli/src/iso-info/main.rs +++ b/libcdio-cli/src/iso-info/main.rs @@ -25,10 +25,7 @@ use std::{ use anyhow::{Context, Result, bail}; use clap::Parser; -use libcdio_rs::{ - Iso9660, Udf, - iso9660::{Iso9660Extensions, xa::XaFileAttributes}, -}; +use libcdio_rs::{Iso, Udf, iso9660::XaFileAttributes}; use time::{UtcOffset, format_description::BorrowedFormatItem, macros::format_description}; use tracing_subscriber::EnvFilter; @@ -51,42 +48,33 @@ fn main() -> Result<()> { 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", ); - let mut extensions = Iso9660Extensions::all(); - if cli.no_joliet { - extensions -= Iso9660Extensions::JolietLevel1; - extensions -= Iso9660Extensions::JolietLevel2; - extensions -= Iso9660Extensions::JolietLevel3; + + if cli.udf { + return print_udf_contents(file, &mut output); } - if let Some(iso) = Iso9660::builder(&file).extensions(extensions).build() { - print_iso9660_metadata(&iso, &file, &mut output) - .context("io error while printing iso9660 metadata")?; + 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.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")?; + } - print_joliet_level(&iso, &mut output).context("io error while printing joliet level")?; + print_joliet_level(&iso, &mut output).context("io error while printing joliet level")?; - if cli.iso9660 { - print_iso9660_contents(&iso, &mut output, !cli.no_rock_ridge, !cli.no_xa) - .context("error printing iso9660 contents")?; - } - } else if !cli.udf { - bail!("error opening iso9660 image: {}", file.display()); - }; - - if cli.udf { - print_udf_contents(file, &mut output)?; + if cli.iso9660 { + print_iso9660_contents(&iso, &mut output, !cli.no_rock_ridge, !cli.no_xa) + .context("error printing iso9660 contents")?; } Ok(()) } fn print_iso9660_metadata( - iso: &Iso9660, + iso: &Iso, path: &Path, mut out: impl io::Write, ) -> Result<(), io::Error> { @@ -107,21 +95,21 @@ fn print_iso9660_metadata( } fn print_rock_ridge( - iso: &Iso9660, + iso: &Iso, file_limit: Option, mut out: impl io::Write, ) -> Result<(), io::Error> { let status = match iso.have_rock_ridge(file_limit) { - Some(true) => "yes", - Some(false) => "no", - None => "possibly not", + Ok(true) => "yes", + Ok(false) => "no", + _ => "possibly not", }; writeln!(out, "Rock Ridge : {}", status) } /// Outputs the file contents of the ISO 9660 image in an ls-like listing format. fn print_iso9660_contents( - iso: &Iso9660, + iso: &Iso, mut out: impl io::Write, use_rock_ridge: bool, use_xa: bool, @@ -140,20 +128,15 @@ fn print_iso9660_contents( writeln!(out, "{}:", dir_path)?; - for entry in iso - .read_dir(&dir_path) - .with_context(|| format!("could not read entry '{}' from iso", dir_path))? - { + for entry in iso.read_dir(dir_path.clone())? { let rock_ridge = use_rock_ridge.then_some(entry.rock_ridge()).flatten(); - let translated_name = entry.filename(); let entry_name = if rock_ridge.is_none() { - translated_name.as_deref() + entry.filename()? } else { - entry.filename_raw() - } - .with_context(|| format!("could not get file name of lsn: {}", entry.lsn()))?; + entry.filename_raw().map(String::from)? + }; - let full_path = dir_path.clone() + entry_name + "/"; + let full_path = dir_path.clone() + &entry_name + "/"; if entry.is_dir() && entry_name != "." && entry_name != ".." { dirs.push_back((full_path.clone(), depth + 1)); } @@ -194,9 +177,7 @@ fn print_iso9660_contents( { mtime } else { - entry - .timestamp() - .with_context(|| format!("got invalid timestamp: {}", full_path))? + entry.timestamp()? }; let local = UtcOffset::current_local_offset() @@ -295,7 +276,7 @@ fn print_udf_contents(path: PathBuf, out: &mut dyn io::Write) -> Result<()> { Ok(()) } -fn print_joliet_level(iso: &Iso9660, mut out: impl io::Write) -> Result<(), io::Error> { +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"); }; diff --git a/libcdio-cli/src/iso-read/main.rs b/libcdio-cli/src/iso-read/main.rs index 329cebd..1c21c12 100644 --- a/libcdio-cli/src/iso-read/main.rs +++ b/libcdio-cli/src/iso-read/main.rs @@ -15,15 +15,11 @@ // 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::{Path, PathBuf}, -}; +use std::{fs::File, io, path::PathBuf}; use anyhow::{Context, Result, bail}; use clap::Parser; -use libcdio_rs::{Iso9660, Udf}; +use libcdio_rs::{Iso, Udf}; use tracing_subscriber::EnvFilter; use crate::cli::Cli; @@ -47,7 +43,7 @@ fn main() -> Result<()> { if cli.udf { udf_extract(image, cli.extract, &mut output)?; } else { - iso9660_extract(&image, &cli.extract, &mut output)?; + iso9660_extract(image, cli.extract, &mut output)?; } Ok(()) @@ -64,19 +60,11 @@ fn udf_extract(image: PathBuf, extract: String, output: &mut File) -> Result<()> } /// Extract given file from an ISO 9660 image. -fn iso9660_extract(image: &Path, extract: &str, output: &mut File) -> Result<()> { - let iso = Iso9660::new(image) - .with_context(|| format!("could not open image '{}' as iso9660", image.display()))?; - let entry = iso.entry(extract).with_context(|| { - format!( - "could not open file '{}' from iso9660 image: {}", - extract, - image.display() - ) - })?; +fn iso9660_extract(image: PathBuf, extract: String, output: &mut File) -> Result<()> { + let iso = Iso::new(image.clone())?; + let entry = iso.entry(extract)?; - io::copy(&mut entry.reader(), output) - .with_context(|| format!("error extracting file '{}' from iso9660", extract))?; + io::copy(&mut entry.reader(), output)?; Ok(()) } diff --git a/libcdio-rs/src/iso9660.rs b/libcdio-rs/src/iso9660.rs index 5272bc0..8e4f7db 100644 --- a/libcdio-rs/src/iso9660.rs +++ b/libcdio-rs/src/iso9660.rs @@ -15,95 +15,65 @@ // You should have received a copy of the GNU General Public License // along with libcdio-rs. If not, see . -//! ISO 9660 filesystem related routines. +//! Routines related to the ISO 9660 filesystem. + +pub use entry::*; +pub use rock::*; +pub use xa::*; -mod ds; mod entry; mod rock; mod util; -pub mod xa; - -pub use entry::Iso9660Entry; -pub use rock::RockRidge; -#[doc(inline)] -pub use xa::CdRomXa; +mod xa; use std::{ - ffi::{CStr, CString, c_char}, - path::Path, + error::Error, + ffi::{CStr, CString, OsString, c_char}, + path::{Path, PathBuf}, ptr::{self, NonNull}, }; -use bitflags::bitflags; -use libcdio_sys::{ - iso_extension_enum_s_ISO_EXTENSION_HIGH_SIERRA, - iso_extension_enum_s_ISO_EXTENSION_JOLIET_LEVEL1, - iso_extension_enum_s_ISO_EXTENSION_JOLIET_LEVEL2, - iso_extension_enum_s_ISO_EXTENSION_JOLIET_LEVEL3, - iso_extension_enum_s_ISO_EXTENSION_ROCK_RIDGE, iso9660_t, -}; +use libcdio_sys::iso9660_t; use num_enum::{IntoPrimitive, TryFromPrimitive}; +use thiserror::Error; +use tracing::error; use crate::logging::init_logger; -/// The main ISO 9660 type -pub struct Iso9660 { +/// An ISO 9660 filesystem instance. +pub struct Iso { pub(crate) ptr: NonNull, } -/// A builder for [Iso9660]. -#[derive(Clone, Debug)] -pub struct Iso9660Builder<'a> { - extensions: Iso9660Extensions, - path: &'a Path, -} - -bitflags! { - /// ISO 9660 Extensions. - /// # Examples - /// ```rust, no_run - /// use libcdio_rs::iso9660::Iso9660Extensions; - /// // pick HighSierra and RockRidge - /// let extensions = Iso9660Extensions::HighSierra & Iso9660Extensions::RockRidge; - /// // pick everything except RockRidge - /// let extensions = Iso9660Extensions::all() - Iso9660Extensions::RockRidge; - /// // pick nothing - /// let extensions = Iso9660Extensions::empty(); - /// ``` - #[derive(Clone, Copy, Debug)] - pub struct Iso9660Extensions: u8 { - const HighSierra = iso_extension_enum_s_ISO_EXTENSION_HIGH_SIERRA as _; - const JolietLevel1 = iso_extension_enum_s_ISO_EXTENSION_JOLIET_LEVEL1 as _; - const JolietLevel2 = iso_extension_enum_s_ISO_EXTENSION_JOLIET_LEVEL2 as _; - const JolietLevel3 = iso_extension_enum_s_ISO_EXTENSION_JOLIET_LEVEL3 as _; - const RockRidge = iso_extension_enum_s_ISO_EXTENSION_ROCK_RIDGE as _; - } -} - -/// Joliet level. -#[repr(u8)] -#[derive(Clone, Copy, Debug, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)] -pub enum JolietLevel { - One = 1, - Two, - Three, -} - -impl Iso9660 { +impl Iso { /// The number of bytes used by an ISO 9660 block. pub const BLOCK_SIZE: usize = 2048; - /// Open an ISO 9660 image for reading at given `path`, with all iso9660 - /// extension flags enabled. Returns `None` on error. - pub fn new(path: &Path) -> Option { - let path = CString::new(path.to_str()?).ok()?; - - Self::open(&path, Iso9660Extensions::all()) - } + /// Opens an ISO 9660 image at given `path`. + pub fn new(path: PathBuf) -> Result { + init_logger(); - /// Returns a builder object. See [`Iso9660Builder`]. - pub fn builder<'a>(path: &'a Path) -> Iso9660Builder<'a> { - Iso9660Builder::new(path) + let path = CString::new(path.into_os_string().as_encoded_bytes()) + .inspect_err(|err| error!(%err, "invalid ISO 9660 path")) + .map_err(|err| IsoOpenError::new(err.clone().into_vec(), err.into()))?; + let iso9660_ptr = unsafe { + // enable all extensions + libcdio_sys::iso9660_open_ext( + path.as_ptr(), + (libcdio_sys::iso_extension_enum_s_ISO_EXTENSION_HIGH_SIERRA + | libcdio_sys::iso_extension_enum_s_ISO_EXTENSION_JOLIET_LEVEL1 + | libcdio_sys::iso_extension_enum_s_ISO_EXTENSION_JOLIET_LEVEL2 + | libcdio_sys::iso_extension_enum_s_ISO_EXTENSION_JOLIET_LEVEL3 + | libcdio_sys::iso_extension_enum_s_ISO_EXTENSION_ROCK_RIDGE) + as _, + ) + }; + + NonNull::new(iso9660_ptr) + .map(|ptr| Self { ptr }) + .ok_or_else(|| { + IsoOpenError::new(path.into_bytes(), "iso9660_open_ext() returned NULL".into()) + }) } /// Returns the Application Identifier. @@ -111,6 +81,30 @@ impl Iso9660 { self.get_identifier(libcdio_sys::iso9660_ifs_get_application_id) } + /// Helper for the methods that return ISO 9660 identifiers. + fn get_identifier( + &self, + func: unsafe extern "C" fn(*mut iso9660_t, *mut *mut c_char) -> bool, + ) -> Option { + let mut identifier_ptr = ptr::null_mut(); + + // SAFETY: identifier_ptr must be freed after use. + let success = unsafe { func(self.ptr.as_ptr(), &raw mut identifier_ptr) }; + if !success || identifier_ptr.is_null() { + return None; + } + + let identifier = unsafe { CStr::from_ptr(identifier_ptr) }; + let identifier = identifier.to_string_lossy().to_string(); + + // SAFETY: identifier_ptr is already copied to a Rust string. + unsafe { + libcdio_sys::cdio_free(identifier_ptr.cast()); + } + + Some(identifier) + } + /// Returns the Data Preparer Identifier. pub fn data_preparer(&self) -> Option { self.get_identifier(libcdio_sys::iso9660_ifs_get_preparer_id) @@ -137,9 +131,6 @@ impl Iso9660 { } /// Returns the Joliet level. - /// # Note - /// [`Self`] must be constructed with the joliet extension enabled, - /// otherwise this will return `None` even if the file has Joliet. pub fn joliet_level(&self) -> Option { let joliet_level = unsafe { libcdio_sys::iso9660_ifs_get_joliet_level(self.ptr.as_ptr()) }; if joliet_level == 0 { @@ -150,110 +141,74 @@ impl Iso9660 { Some(joliet_level) } +} - fn open(path: &CStr, extensions: Iso9660Extensions) -> Option { - init_logger(); - - // SAFETY: path is duplicated by the method, so its safe to drop afterwards - let iso9660_ptr = - unsafe { libcdio_sys::iso9660_open_ext(path.as_ptr(), extensions.bits()) }; - - Some(Self { - ptr: NonNull::new(iso9660_ptr)?, - }) +impl Drop for Iso { + fn drop(&mut self) { + let _ = unsafe { libcdio_sys::iso9660_close(self.ptr.as_ptr()) }; } +} - /// Helper for the methods that return iso9660 identifiers. - fn get_identifier( - &self, - func: unsafe extern "C" fn(*mut iso9660_t, *mut *mut c_char) -> bool, - ) -> Option { - let mut identifier_ptr = ptr::null_mut(); - - // SAFETY: The method allocates a string and points the identifier_ptr to it. - // It must be freed after use. - let success = unsafe { func(self.ptr.as_ptr(), &raw mut identifier_ptr) }; - if !success || identifier_ptr.is_null() { - return None; - } - - let identifier = unsafe { CStr::from_ptr(identifier_ptr) }; - let identifier = identifier.to_string_lossy().to_string(); - - // SAFETY: application_id has been duplicated into a Rust string - // above, thus safe to free - unsafe { - libcdio_sys::cdio_free(identifier_ptr.cast()); - } +#[derive(Debug, Error)] +#[error(transparent)] +pub struct IsoOpenError(Box); - Some(identifier) - } +#[derive(Debug, Error)] +#[error("could not open ISO 9660 file at `{path}`")] +struct OpenErrRepr { + path: PathBuf, + source: Box, } -impl<'a> Iso9660Builder<'a> { - pub fn new(path: &'a Path) -> Self { - Self { - path, - extensions: Iso9660Extensions::empty(), - } +impl IsoOpenError { + /// Returns the path of the ISO 9660 file. + pub fn path(&self) -> &Path { + &self.0.path } - /// Set the extensions to be activated. This is set to be empty by default. - pub fn extensions(mut self, extensions: Iso9660Extensions) -> Self { - self.extensions = extensions; - self - } - - /// Build the iso9660 type with the set options. - /// Returns `None` on error. - pub fn build(self) -> Option { - let path = CString::new(self.path.to_str()?).ok()?; - - Iso9660::open(&path, self.extensions) + fn new(path_bytes: Vec, source: Box) -> Self { + Self(Box::new(OpenErrRepr { + // SAFETY: path_bytes originate from a PathBuf + path: unsafe { OsString::from_encoded_bytes_unchecked(path_bytes) }.into(), + source, + })) } } -impl Drop for Iso9660 { - fn drop(&mut self) { - let _ = unsafe { libcdio_sys::iso9660_close(self.ptr.as_ptr()) }; - } +/// Joliet level. +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)] +pub enum JolietLevel { + One = 1, + Two, + Three, } #[cfg(test)] pub(crate) mod tests { use super::*; - pub fn test_rockridge_file() -> &'static Path { - Path::new("../test-data/rock-ridge.iso") + pub fn test_rockridge_file() -> PathBuf { + PathBuf::from("../test-data/rock-ridge.iso") } - pub fn test_joliet_file() -> &'static Path { - Path::new("../test-data/joliet.iso") + pub fn test_joliet_file() -> PathBuf { + PathBuf::from("../test-data/joliet.iso") } #[test_log::test(test)] fn new() { - let iso = Iso9660::new(test_rockridge_file()); - assert!(iso.is_some()); - } - - #[test] - fn builder() { - let extensions = Iso9660Extensions::HighSierra & Iso9660Extensions::RockRidge; - let iso = Iso9660::builder(test_rockridge_file()) - .extensions(extensions) - .build(); - assert!(iso.is_some()); + Iso::new(test_rockridge_file()).unwrap(); } #[test] fn joliet_level() { - let iso = Iso9660::new(test_joliet_file()).unwrap(); + let iso = Iso::new(test_joliet_file()).unwrap(); assert_eq!(iso.joliet_level().unwrap(), JolietLevel::Three); } #[test] fn application() { - let iso = Iso9660::new(test_rockridge_file()).unwrap(); + let iso = Iso::new(test_rockridge_file()).unwrap(); assert_eq!( &iso.application().unwrap(), "K3B THE CD KREATOR VERSION 0.11.20 (C) 2003 SEBASTIAN TRUEG AND THE K3B TEAM" @@ -262,31 +217,31 @@ pub(crate) mod tests { #[test] fn data_preparer() { - let iso = Iso9660::new(test_rockridge_file()).unwrap(); + let iso = Iso::new(test_rockridge_file()).unwrap(); assert_eq!(&iso.data_preparer().unwrap(), "K3b - Version 0.11.20",); } #[test] fn publisher() { - let iso = Iso9660::new(test_rockridge_file()).unwrap(); + let iso = Iso::new(test_rockridge_file()).unwrap(); assert_eq!(&iso.publisher().unwrap(), "Rocky Bernstein"); } #[test] fn system() { - let iso = Iso9660::new(test_rockridge_file()).unwrap(); + let iso = Iso::new(test_rockridge_file()).unwrap(); assert_eq!(&iso.system().unwrap(), "LINUX"); } #[test] fn volume() { - let iso = Iso9660::new(test_rockridge_file()).unwrap(); + let iso = Iso::new(test_rockridge_file()).unwrap(); assert_eq!(&iso.volume().unwrap(), "Rock Ridge Copy test"); } #[test] fn volume_set() { - let iso = Iso9660::new(test_rockridge_file()).unwrap(); + let iso = Iso::new(test_rockridge_file()).unwrap(); assert!(&iso.volume_set().is_none()); } } diff --git a/libcdio-rs/src/iso9660/ds.rs b/libcdio-rs/src/iso9660/ds.rs deleted file mode 100644 index 73bed1d..0000000 --- a/libcdio-rs/src/iso9660/ds.rs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (C) 2026 Shiva Kiran Koninty -// -// This file is part of libcdio-rs. -// -// libcdio-rs is free software: you can redistribute it and/or -// modify it under the terms of the GNU General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// libcdio-rs is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with libcdio-rs. If not, see . - -//! Data structure conversion methods from libcdio's ds.c - -use std::ffi::c_void; - -use libcdio_sys::_CdioList; - -/// Returns a vec of pointers to the data of the cdio list. -/// Frees the list nodes, without freeing the data. -/// # Safety -/// - `cdio_list` must not be null. -/// - The list data must be owned by the caller. -pub unsafe fn cdiolist_to_vec(cdio_list: *mut _CdioList) -> Vec<*mut c_void> { - let mut list = Vec::new(); - let mut cur = unsafe { libcdio_sys::_cdio_list_begin(cdio_list) }; - while !cur.is_null() { - let data = unsafe { libcdio_sys::_cdio_list_node_data(cur) }; - list.push(data); - cur = unsafe { libcdio_sys::_cdio_list_node_next(cur) }; - } - - unsafe { - libcdio_sys::_cdio_list_free(cdio_list, 0, None); - } - - list -} - -#[cfg(test)] -mod tests { - use std::ffi::CString; - - #[test] - fn cdiolist_to_vec() { - let a = CString::new("This is A").unwrap(); - let b = CString::new("This is B").unwrap(); - - let cdiolist = unsafe { libcdio_sys::_cdio_list_new() }; - unsafe { libcdio_sys::_cdio_list_append(cdiolist, a.into_raw().cast()) }; - unsafe { libcdio_sys::_cdio_list_append(cdiolist, b.into_raw().cast()) }; - - let list = unsafe { super::cdiolist_to_vec(cdiolist) }; - let a = unsafe { CString::from_raw(list[0].cast()) }; - let b = unsafe { CString::from_raw(list[1].cast()) }; - - assert_eq!(&a, c"This is A"); - assert_eq!(&b, c"This is B"); - } -} diff --git a/libcdio-rs/src/iso9660/entry.rs b/libcdio-rs/src/iso9660/entry.rs index 0056940..b2f8782 100644 --- a/libcdio-rs/src/iso9660/entry.rs +++ b/libcdio-rs/src/iso9660/entry.rs @@ -15,95 +15,134 @@ // You should have received a copy of the GNU General Public License // along with libcdio-rs. If not, see . -//! ISO 9660 file/directory entry object. +//! Routines related to ISO 9660 entries. use std::{ + error::Error, ffi::{CStr, CString}, io, ptr::NonNull, }; use libcdio_sys::{iso9660_stat_s, iso9660_stat_s__STAT_DIR}; +use thiserror::Error; use time::OffsetDateTime; -use crate::iso9660::{Iso9660, ds, util}; +use crate::iso9660::{Iso, util}; -/// ISO 9660 file/directory entry. -pub struct Iso9660Entry<'a> { - /// The parent ISO 9660 object - pub(crate) iso: &'a Iso9660, - pub(crate) stat: NonNull, -} - -/// A type that implements [`io::Read`], for reading an ISO9660 entry. -pub struct Iso9660EntryReader<'a> { - bytes_read: usize, - entry: &'a Iso9660Entry<'a>, -} - -impl Iso9660 { - /// Read directory at `path` and return a list of entries. +impl Iso { + /// Returns a list of entries under `path`. /// - /// Only '/' may be used for path separators. - /// Returns `None` on error. - pub fn read_dir(&self, path: &str) -> Option>> { - let path = CString::new(path).ok()?; + /// Only Unix-style `/` may be used as a separator. + pub fn read_dir(&self, path: String) -> Result>, IsoGetEntryError> { + let path = CString::new(path).map_err(|err| { + IsoGetEntryError::new( + String::from_utf8(err.clone().into_vec()).expect("path was a valid string"), + err.into(), + ) + })?; let dirlist = unsafe { libcdio_sys::iso9660_ifs_readdir(self.ptr.as_ptr(), path.as_ptr()) }; if dirlist.is_null() { - return None; + return Err(IsoGetEntryError::new( + path.into_string().expect("path was a valid string"), + "iso9660_ifs_readdir() returned NULL".into(), + )); } - // SAFETY: dirlist is not null and the data will be owned by `Iso9660Entry`. - let dirlist = unsafe { ds::cdiolist_to_vec(dirlist) }; + // SAFETY: dirlist is not null and the data will be owned by `IsoEntry`. + let dirlist = unsafe { util::cdiolist_to_vec(dirlist) }; let dirlist = dirlist .into_iter() .filter_map(|entry| { - Some(Iso9660Entry { + Some(IsoEntry { iso: self, stat: NonNull::new(entry.cast())?, }) }) .collect(); - Some(dirlist) + Ok(dirlist) } - /// Return entry for `path`. `None` is returned on error. - pub fn entry(&self, path: &str) -> Option> { - let path = CString::new(path).ok()?; + /// Returns ISO 9660 entry at `path`. + pub fn entry(&self, path: String) -> Result, IsoGetEntryError> { + let path = CString::new(path).map_err(|err| { + IsoGetEntryError::new( + String::from_utf8(err.clone().into_vec()).expect("path was a valid string"), + err.into(), + ) + })?; let stat = unsafe { libcdio_sys::iso9660_ifs_stat(self.ptr.as_ptr(), path.as_ptr()) }; - Some(Iso9660Entry { - iso: self, - stat: NonNull::new(stat)?, - }) + NonNull::new(stat) + .ok_or_else(|| { + IsoGetEntryError::new( + path.into_string().expect("path was a valid string"), + "iso9660_ifs_stat() returned NULL".into(), + ) + }) + .map(|stat| IsoEntry { iso: self, stat }) + } +} + +#[derive(Debug, Error)] +#[error(transparent)] +pub struct IsoGetEntryError(Box); + +#[derive(Debug, Error)] +#[error("could not get ISO 9660 entry at `{path}`")] +struct GetEntryErrRepr { + path: String, + source: Box, +} + +impl IsoGetEntryError { + /// Returns the path of the ISO 9660 entry. + pub fn path(&self) -> &str { + &self.0.path + } + + fn new(path: impl Into, source: Box) -> Self { + Self(Box::new(GetEntryErrRepr { + path: path.into(), + source, + })) } } -impl Iso9660Entry<'_> { +/// ISO 9660 file/directory entry. +pub struct IsoEntry<'a> { + /// The parent ISO 9660 object + pub(crate) iso: &'a Iso, + pub(crate) stat: NonNull, +} + +impl IsoEntry<'_> { /// Returns the raw filename of the entry. - /// Returns `None` if the filename has non UTF-8 characters or on error. - pub fn filename_raw(&self) -> Option<&str> { + /// + /// See [`Self::filename()`] + pub fn filename_raw(&self) -> Result<&str, IsoInvalidEntryError> { // SAFETY: self.entry is not null since its behind a NonNull let name = unsafe { (*self.stat.as_ptr()).filename.as_ptr() }; if name.is_null() { - return None; + return Err(IsoInvalidEntryError::new( + Default::default(), + "iso9660_stat_s.filename is NULL".into(), + )); }; - // SAFETY: The filename should be a null terminated string - let name = unsafe { CStr::from_ptr(name).to_str() }; - name.inspect_err(|err| tracing::error!(%err)).ok() + // SAFETY: The filename should be a null terminated string + unsafe { CStr::from_ptr(name).to_str() } + .map_err(|err| IsoInvalidEntryError::new(Default::default(), err.into())) } - /// Returns a filename in a format used for a listing. - /// - Lowercase name if no Joliet Extension interpretation. - /// - Remove trailing ;1 or .;1 - /// - Turn the other ; into version numbers. - /// - /// Returns `None` if the string has non UTF-8 characters or on error. - pub fn filename(&self) -> Option { + /// Returns the entry's filename. + pub fn filename(&self) -> Result { let filename = unsafe { (*self.stat.as_ptr()).filename.as_ptr() }; if filename.is_null() { - return None; + return Err(IsoInvalidEntryError::new( + Default::default(), + "iso9660_stat_s.filename is NULL".into(), + )); } let filename = unsafe { CStr::from_ptr(filename) }; @@ -117,12 +156,15 @@ impl Iso9660Entry<'_> { joliet_level, ) }; + // iso9660_name_translate_ext will not return negative numbers, + // therefore the cast should be safe translated_name.truncate(len as usize); - String::from_utf8(translated_name).ok() + String::from_utf8(translated_name) + .map_err(|err| IsoInvalidEntryError::new(Default::default(), err.into())) } - /// Multi-extent aware size, in bytes. + /// Returns Multi-extent aware file size, in bytes. pub fn total_size(&self) -> u64 { unsafe { (*self.stat.as_ptr()).total_size } } @@ -132,41 +174,68 @@ impl Iso9660Entry<'_> { unsafe { (*self.stat.as_ptr()).lsn } } - /// Returns `true` if self is a directory. + /// Returns `true` if the stat represents a directory. pub fn is_dir(&self) -> bool { unsafe { (*self.stat.as_ptr()).type_ == iso9660_stat_s__STAT_DIR } } /// Returns the timestamp on the entry. - /// `None` if the timestamp is invalid. - pub fn timestamp(&self) -> Option { + pub fn timestamp(&self) -> Result { let tm = unsafe { (*self.stat.as_ptr()).tm }; - util::convert_tm_local(tm).ok() + util::convert_tm_local(tm) + .map_err(|err| IsoInvalidEntryError::new(self.filename().unwrap_or_default(), err)) } - /// A type that implements [`io::Read`], for reading an ISO9660 entry. - /// Returns `None` on error. - pub fn reader(&self) -> Iso9660EntryReader<'_> { - Iso9660EntryReader { + /// Returns a type that implements [`io::Read`], for reading an ISO 9660 entry. + pub fn reader(&self) -> IsoEntryReader<'_> { + IsoEntryReader { bytes_read: 0, entry: self, } } } -impl Drop for Iso9660Entry<'_> { +impl Drop for IsoEntry<'_> { fn drop(&mut self) { unsafe { libcdio_sys::iso9660_stat_free(self.stat.as_ptr()) } } } -impl io::Read for Iso9660EntryReader<'_> { +#[derive(Debug, Error)] +#[error(transparent)] +pub struct IsoInvalidEntryError(Box); + +#[derive(Debug, Error)] +#[error("inavlid data in ISO 9660 entry named `{name}`")] +struct InvalidEntryErrRepr { + name: String, + source: Box, +} + +impl IsoInvalidEntryError { + /// Returns the name of the ISO 9660 entry. + pub fn name(&self) -> &str { + &self.0.name + } + + fn new(name: String, source: Box) -> Self { + Self(Box::new(InvalidEntryErrRepr { name, source })) + } +} + +/// A type that implements [`io::Read`], for reading an ISO 9660 entry. +pub struct IsoEntryReader<'a> { + bytes_read: usize, + entry: &'a IsoEntry<'a>, +} + +impl io::Read for IsoEntryReader<'_> { fn read(&mut self, buf: &mut [u8]) -> io::Result { let file_size = self.entry.total_size() as usize; let mut buf_read = 0; while self.bytes_read < file_size && buf_read < buf.len() { - let lsn = self.entry.lsn() + (self.bytes_read / Iso9660::BLOCK_SIZE) as i32; - let mut block = [0_u8; Iso9660::BLOCK_SIZE]; + let lsn = self.entry.lsn() + (self.bytes_read / Iso::BLOCK_SIZE) as i32; + let mut block = [0_u8; Iso::BLOCK_SIZE]; let ret = unsafe { libcdio_sys::iso9660_iso_seek_read( self.entry.iso.ptr.as_ptr(), @@ -177,7 +246,7 @@ impl io::Read for Iso9660EntryReader<'_> { }; // the returned value is either BLOCK_SIZE or zero on error, thus // excess bytes past the last read must be handled. - // cast is safe as Iso9660::BLOCK_SIZE < i16::MAX + // cast is safe as Iso::BLOCK_SIZE < i16::MAX if ret != block.len() as _ { return Err(io::Error::other(format!( "error reading block at lsn: {lsn}", @@ -200,7 +269,7 @@ impl io::Read for Iso9660EntryReader<'_> { } } -impl io::Seek for Iso9660EntryReader<'_> { +impl io::Seek for IsoEntryReader<'_> { fn seek(&mut self, pos: io::SeekFrom) -> io::Result { self.bytes_read = match pos { io::SeekFrom::Start(offset) => offset as usize, @@ -216,26 +285,26 @@ impl io::Seek for Iso9660EntryReader<'_> { #[cfg(test)] mod tests { - use std::{io::Read, path::Path}; + use std::{io::Read, path::PathBuf}; use time::macros::datetime; use crate::iso9660::{ - Iso9660, + Iso, tests::{test_joliet_file, test_rockridge_file}, }; #[test] fn read_dir() { - let iso = Iso9660::new(test_joliet_file()).unwrap(); - let entries = iso.read_dir("/").unwrap(); + let iso = Iso::new(test_joliet_file()).unwrap(); + let entries = iso.read_dir("/".to_owned()).unwrap(); assert_eq!(entries.len(), 3); } #[test] fn filename() { - let iso = Iso9660::new(test_rockridge_file()).unwrap(); - let entries = iso.read_dir("/").unwrap(); + let iso = Iso::new(test_rockridge_file()).unwrap(); + let entries = iso.read_dir("/".to_owned()).unwrap(); let names: Vec<_> = entries.iter().map(|e| e.filename_raw().unwrap()).collect(); assert_eq!( &names, @@ -245,8 +314,8 @@ mod tests { #[test] fn filename_translated() { - let iso = Iso9660::new(test_rockridge_file()).unwrap(); - let entries = iso.read_dir("/").unwrap(); + let iso = Iso::new(test_rockridge_file()).unwrap(); + let entries = iso.read_dir("/".to_owned()).unwrap(); let names: Vec<_> = entries.iter().map(|e| e.filename().unwrap()).collect(); assert_eq!( &names, @@ -256,39 +325,39 @@ mod tests { #[test] fn entry() { - let iso = Iso9660::new(test_rockridge_file()).unwrap(); - let entry = iso.entry("/copy").unwrap(); + let iso = Iso::new(test_rockridge_file()).unwrap(); + let entry = iso.entry("/copy".to_string()).unwrap(); assert_eq!(entry.filename().unwrap(), "copy"); } #[test] fn total_size() { - let iso = Iso9660::new(test_rockridge_file()).unwrap(); - let entry = iso.entry("/COPYING").unwrap(); + let iso = Iso::new(test_rockridge_file()).unwrap(); + let entry = iso.entry("/COPYING".to_string()).unwrap(); assert_eq!(entry.total_size(), 17992); } #[test] fn lsn() { - let iso = Iso9660::new(test_rockridge_file()).unwrap(); - let entry = iso.entry("/COPYING").unwrap(); + let iso = Iso::new(test_rockridge_file()).unwrap(); + let entry = iso.entry("/COPYING".to_string()).unwrap(); assert_eq!(entry.lsn(), 27); } #[test] fn is_dir() { - let iso = Iso9660::new(test_rockridge_file()).unwrap(); - let file = iso.entry("/COPYING").unwrap(); + let iso = Iso::new(test_rockridge_file()).unwrap(); + let file = iso.entry("/COPYING".to_string()).unwrap(); assert!(!file.is_dir()); - let dir = iso.entry("/copy").unwrap(); + let dir = iso.entry("/copy".to_string()).unwrap(); assert!(dir.is_dir()); } #[test] fn timestamp() { - let iso = Iso9660::new(test_rockridge_file()).unwrap(); - let entry = iso.entry("/COPYING").unwrap(); + let iso = Iso::new(test_rockridge_file()).unwrap(); + let entry = iso.entry("/COPYING".to_string()).unwrap(); assert_eq!( entry.timestamp().unwrap(), datetime!(2005-03-05 20:55:51.0 +05:30:00), @@ -297,8 +366,8 @@ mod tests { #[test] fn read() { - let iso = Iso9660::new(Path::new("../test-data/xa.iso")).unwrap(); - let entry = iso.entry("copying").unwrap(); + let iso = Iso::new(PathBuf::from("../test-data/xa.iso")).unwrap(); + let entry = iso.entry("copying".to_string()).unwrap(); let gpl = std::fs::read_to_string("../COPYING").unwrap(); let mut reader = entry.reader(); diff --git a/libcdio-rs/src/iso9660/rock.rs b/libcdio-rs/src/iso9660/rock.rs index 28e823f..dbc06b8 100644 --- a/libcdio-rs/src/iso9660/rock.rs +++ b/libcdio-rs/src/iso9660/rock.rs @@ -15,64 +15,49 @@ // You should have received a copy of the GNU General Public License // along with libcdio-rs. If not, see . -//! ISO 9660 Rock Ridge extensions. +//! Routines related to ISO 9660 Rock Ridge extensions. use std::{ffi::CStr, mem::MaybeUninit}; use file_mode::Mode; use libcdio_sys::{bool_3way_t_nope, bool_3way_t_yep, iso_rock_time_s}; +use thiserror::Error; use time::OffsetDateTime; -use crate::iso9660::{Iso9660, entry::Iso9660Entry, util}; +use crate::iso9660::{Iso, entry::IsoEntry, util}; -/// ISO 9660 Rock Ridge extensions. -#[derive(Clone, Debug)] -#[non_exhaustive] -pub struct RockRidge { - /// Create time - pub create_time: Option, - /// Group ID - pub group_id: u32, - /// Number of hard links - pub hard_links: u32, - /// Unix file mode - pub mode: Mode, - /// Modify time - pub modify_time: Option, - /// Symlink target - pub symlink_to: Option, - /// User ID - pub user_id: u32, -} - -impl Iso9660 { - /// Checks if any file has Rock Ridge extensions. Returns `None` on error. +impl Iso { + /// Checks if any file has Rock Ridge extensions. + /// /// This can be time consuming, therefore `file_limit` can be provided to /// limit the number of files to scan. - pub fn have_rock_ridge(&self, file_limit: Option) -> Option { + pub fn have_rock_ridge(&self, file_limit: Option) -> Result { let file_limit = file_limit.unwrap_or(u64::MAX); let result = unsafe { libcdio_sys::iso9660_have_rr(self.ptr.as_ptr(), file_limit) }; #[allow(non_upper_case_globals)] match result { - bool_3way_t_yep => Some(true), - bool_3way_t_nope => Some(false), - _ => None, + bool_3way_t_yep => Ok(true), + bool_3way_t_nope => Ok(false), + _ => Err(RockRidgeSearchError), } } } -impl Iso9660Entry<'_> { - /// Rock Ridge extensions. - /// `None` is returned if Rock ridge extensions are missing, or if it - /// could not be determined. - pub fn rock_ridge(&self) -> Option { +#[non_exhaustive] +#[derive(Debug, Error)] +#[error("error searching for rock ridge extensions: file limit reached")] +pub struct RockRidgeSearchError; + +impl IsoEntry<'_> { + /// Returns the Rock Ridge attributes of the entry. + pub fn rock_ridge(&self) -> Option { let rock = unsafe { (*self.stat.as_ptr()).rr }; if rock.b3_rock != bool_3way_t_yep { return None; } - Some(RockRidge { + Some(RockRidgeAttributes { create_time: convert_rock_timefield(rock.create), group_id: rock.st_gid, hard_links: rock.st_nlinks, @@ -95,6 +80,19 @@ impl Iso9660Entry<'_> { } } +/// ISO 9660 Rock Ridge extensions. +#[derive(Clone, Debug)] +#[non_exhaustive] +pub struct RockRidgeAttributes { + pub create_time: Option, + pub group_id: u32, + pub hard_links: u32, + pub mode: Mode, + pub modify_time: Option, + pub symlink_to: Option, + pub user_id: u32, +} + fn convert_rock_timefield(field: iso_rock_time_s) -> Option { if !field.b_used { return None; @@ -124,91 +122,91 @@ mod tests { #[test] fn have_rock_ridge() { - let iso = Iso9660::new(test_rockridge_file()).unwrap(); + let iso = Iso::new(test_rockridge_file()).unwrap(); assert!(iso.have_rock_ridge(None).unwrap()); } #[test] fn rock_ridge() { - let iso = Iso9660::new(test_rockridge_file()).unwrap(); - let entry = iso.entry("/COPYING").unwrap(); + let iso = Iso::new(test_rockridge_file()).unwrap(); + let entry = iso.entry("/COPYING".to_string()).unwrap(); assert!(entry.rock_ridge().is_some()); - let iso = Iso9660::new(test_joliet_file()).unwrap(); - let entry = iso.entry("/libcdio/COPYING").unwrap(); + let iso = Iso::new(test_joliet_file()).unwrap(); + let entry = iso.entry("/libcdio/COPYING".to_string()).unwrap(); assert!(entry.rock_ridge().is_none()); } #[test] fn mode() { - let iso = Iso9660::new(test_rockridge_file()).unwrap(); + let iso = Iso::new(test_rockridge_file()).unwrap(); - let entry = iso.entry("/zero").unwrap(); + let entry = iso.entry("/zero".to_string()).unwrap(); let mode = entry.rock_ridge().unwrap().mode; assert_eq!(&mode.to_string(), "cr--r--r--"); - let entry = iso.entry("/fd0").unwrap(); + let entry = iso.entry("/fd0".to_string()).unwrap(); let mode = entry.rock_ridge().unwrap().mode; assert_eq!(&mode.to_string(), "br--r--r--"); - let entry = iso.entry("/Copy2").unwrap(); + let entry = iso.entry("/Copy2".to_string()).unwrap(); let mode = entry.rock_ridge().unwrap().mode; assert_eq!(&mode.to_string(), "lr-xr-xr-x"); - let entry = iso.entry("/copy").unwrap(); + let entry = iso.entry("/copy".to_string()).unwrap(); let mode = entry.rock_ridge().unwrap().mode; assert_eq!(&mode.to_string(), "dr-xr-xr-x"); } #[test] fn symlink_to() { - let iso = Iso9660::new(test_rockridge_file()).unwrap(); + let iso = Iso::new(test_rockridge_file()).unwrap(); - let entry = iso.entry("/COPYING").unwrap(); + let entry = iso.entry("/COPYING".to_string()).unwrap(); let rock = entry.rock_ridge().unwrap(); assert!(rock.symlink_to.is_none()); - let entry = iso.entry("/Copy2").unwrap(); + let entry = iso.entry("/Copy2".to_string()).unwrap(); let rock = entry.rock_ridge().unwrap(); assert_eq!(rock.symlink_to.unwrap(), "COPYING"); - let entry = iso.entry("/tmp/COPYING").unwrap(); + let entry = iso.entry("/tmp/COPYING".to_string()).unwrap(); let rock = entry.rock_ridge().unwrap(); assert_eq!(rock.symlink_to.unwrap(), "../copying/COPYING"); } #[test] fn hard_links() { - let iso = Iso9660::new(test_rockridge_file()).unwrap(); - let entry = iso.entry("/COPYING").unwrap(); + let iso = Iso::new(test_rockridge_file()).unwrap(); + let entry = iso.entry("/COPYING".to_string()).unwrap(); let rock = entry.rock_ridge().unwrap(); assert_eq!(rock.hard_links, 1); - let entry = iso.entry("/copy").unwrap(); + let entry = iso.entry("/copy".to_string()).unwrap(); let rock = entry.rock_ridge().unwrap(); assert_eq!(rock.hard_links, 2); } #[test] fn user_id() { - let iso = Iso9660::new(test_rockridge_file()).unwrap(); - let entry = iso.entry("/COPYING").unwrap(); + let iso = Iso::new(test_rockridge_file()).unwrap(); + let entry = iso.entry("/COPYING".to_string()).unwrap(); let rock = entry.rock_ridge().unwrap(); assert_eq!(rock.user_id, 0); } #[test] fn group_id() { - let iso = Iso9660::new(test_rockridge_file()).unwrap(); - let entry = iso.entry("/COPYING").unwrap(); + let iso = Iso::new(test_rockridge_file()).unwrap(); + let entry = iso.entry("/COPYING".to_string()).unwrap(); let rock = entry.rock_ridge().unwrap(); assert_eq!(rock.group_id, 0); } #[test] fn time() { - let iso = Iso9660::new(test_rockridge_file()).unwrap(); - let entry = iso.entry("/COPYING").unwrap(); + let iso = Iso::new(test_rockridge_file()).unwrap(); + let entry = iso.entry("/COPYING".to_string()).unwrap(); let rock = entry.rock_ridge().unwrap(); assert_eq!( rock.modify_time.unwrap(), diff --git a/libcdio-rs/src/iso9660/util.rs b/libcdio-rs/src/iso9660/util.rs index 3c36f6b..6893006 100644 --- a/libcdio-rs/src/iso9660/util.rs +++ b/libcdio-rs/src/iso9660/util.rs @@ -15,26 +15,75 @@ // You should have received a copy of the GNU General Public License // along with libcdio-rs. If not, see . -//! Utility methods such as conversions +//! Utility and data structure conversion routines. -use time::{Date, OffsetDateTime, Time, UtcOffset, error}; +use std::{error::Error, ffi::c_void}; + +use libcdio_sys::_CdioList; +use time::{Date, OffsetDateTime, Time, UtcOffset}; /// Convert `tm` representing local time to a `OffsetDateTime`. pub(crate) fn convert_tm_local( tm: libcdio_sys::tm, -) -> Result { +) -> Result> { const TM_YEAR_OFFSET: i32 = 1900; const TM_ORDINAL_DAY_OFFSET: u16 = 1; let date = Date::from_ordinal_date( tm.tm_year + TM_YEAR_OFFSET, - tm.tm_yday as u16 + TM_ORDINAL_DAY_OFFSET, + u16::try_from(tm.tm_yday)? + TM_ORDINAL_DAY_OFFSET, + )?; + let time = Time::from_hms( + u8::try_from(tm.tm_hour)?, + u8::try_from(tm.tm_min)?, + u8::try_from(tm.tm_sec)?, )?; - let time = Time::from_hms(tm.tm_hour as _, tm.tm_min as _, tm.tm_sec as _)?; Ok(OffsetDateTime::new_in_offset( date, time, - UtcOffset::local_offset_at(OffsetDateTime::new_utc(date, time)) - .expect("could not obtain the system offset"), + UtcOffset::local_offset_at(OffsetDateTime::new_utc(date, time))?, )) } + +/// Returns a vec of pointers to the data of the cdio list. +/// Frees the list nodes, without freeing the data. +/// # Safety +/// - `cdio_list` must not be null. +/// - The list data must be owned by the caller. +pub unsafe fn cdiolist_to_vec(cdio_list: *mut _CdioList) -> Vec<*mut c_void> { + let mut list = Vec::new(); + let mut cur = unsafe { libcdio_sys::_cdio_list_begin(cdio_list) }; + while !cur.is_null() { + let data = unsafe { libcdio_sys::_cdio_list_node_data(cur) }; + list.push(data); + cur = unsafe { libcdio_sys::_cdio_list_node_next(cur) }; + } + + unsafe { + libcdio_sys::_cdio_list_free(cdio_list, 0, None); + } + + list +} + +#[cfg(test)] +mod tests { + use std::ffi::CString; + + #[test] + fn cdiolist_to_vec() { + let a = CString::new("This is A").unwrap(); + let b = CString::new("This is B").unwrap(); + + let cdiolist = unsafe { libcdio_sys::_cdio_list_new() }; + unsafe { libcdio_sys::_cdio_list_append(cdiolist, a.into_raw().cast()) }; + unsafe { libcdio_sys::_cdio_list_append(cdiolist, b.into_raw().cast()) }; + + let list = unsafe { super::cdiolist_to_vec(cdiolist) }; + let a = unsafe { CString::from_raw(list[0].cast()) }; + let b = unsafe { CString::from_raw(list[1].cast()) }; + + assert_eq!(&a, c"This is A"); + assert_eq!(&b, c"This is B"); + } +} diff --git a/libcdio-rs/src/iso9660/xa.rs b/libcdio-rs/src/iso9660/xa.rs index 03ca91d..0d62383 100644 --- a/libcdio-rs/src/iso9660/xa.rs +++ b/libcdio-rs/src/iso9660/xa.rs @@ -15,46 +15,15 @@ // You should have received a copy of the GNU General Public License // along with libcdio-rs. If not, see . -//! CD-ROM XA (eXtended Architecture) +//! Routines related to CD-ROM XA (eXtended Architecture). use bitflags::bitflags; -use crate::iso9660::entry::Iso9660Entry; +use crate::iso9660::entry::IsoEntry; -/// CD-ROM XA (eXtended Architecture) attributes -#[derive(Clone, Debug)] -#[non_exhaustive] -pub struct CdRomXa { - pub file_attr: XaFileAttributes, - pub file_num: u8, - pub group_id: u16, - pub user_id: u16, - total_size: u64, -} - -bitflags! { - /// XA File Attributes. - /// For more information: https://psx-spx.consoledev.net/cdromformat/#cdrom-iso-file-and-directory-descriptors - #[derive(Clone, Copy, Debug)] - pub struct XaFileAttributes: u16 { - const OwnerRead = 1 << 0; - const OwnerExecute = 1 << 2; - const GroupRead = 1 << 4; - const GroupExecute = 1 << 6; - const WorldRead = 1 << 8; - const WorldExecute = 1 << 10; - const Mode2 = 1 << 11; - const Mode2Form2 = 1 << 12; - const Interleaved = 1 << 13; - const Cdda = 1 << 14; - const Directory = 1 << 15; - } -} - -impl Iso9660Entry<'_> { - /// Return CD-ROM XA (eXtended Architecture) attributes. - /// `None` is returned if the attributes are not present. - pub fn xa(&self) -> Option { +impl IsoEntry<'_> { + /// Returns CD-ROM XA (eXtended Architecture) attributes of the entry. + pub fn xa(&self) -> Option { let have_xa = unsafe { (*self.stat.as_ptr()).b_xa }; if !have_xa { return None; @@ -63,7 +32,7 @@ impl Iso9660Entry<'_> { // SAFETY: The above check confirms that xa are present. let xa = unsafe { (*self.stat.as_ptr()).xa }; - Some(CdRomXa { + Some(XaAttributes { file_attr: XaFileAttributes::from_bits_retain(u16::from_be(xa.attributes)), file_num: u8::from_be(xa.filenum), group_id: u16::from_be(xa.group_id), @@ -73,8 +42,20 @@ impl Iso9660Entry<'_> { } } -impl CdRomXa { - /// Return multi extent size. +/// CD-ROM XA (eXtended Architecture) attributes. +#[derive(Clone, Debug)] +#[non_exhaustive] +pub struct XaAttributes { + pub file_attr: XaFileAttributes, + pub file_num: u8, + pub group_id: u16, + pub user_id: u16, + total_size: u64, +} + +impl XaAttributes { + /// Returns multi extent size. + /// /// Returns `None` if not using Mode2/Form2 encoding. // TODO: Add unit test pub const fn mode2form2_size(&self) -> Option { @@ -91,18 +72,38 @@ impl CdRomXa { } } +bitflags! { + /// XA File Attributes. + /// + /// See: https://psx-spx.consoledev.net/cdromformat/#cdrom-iso-file-and-directory-descriptors + #[derive(Clone, Copy, Debug)] + pub struct XaFileAttributes: u16 { + const OwnerRead = 1 << 0; + const OwnerExecute = 1 << 2; + const GroupRead = 1 << 4; + const GroupExecute = 1 << 6; + const WorldRead = 1 << 8; + const WorldExecute = 1 << 10; + const Mode2 = 1 << 11; + const Mode2Form2 = 1 << 12; + const Interleaved = 1 << 13; + const Cdda = 1 << 14; + const Directory = 1 << 15; + } +} + #[cfg(test)] mod tests { - use std::path::Path; + use std::path::PathBuf; - use crate::iso9660::Iso9660; + use crate::iso9660::Iso; use super::*; #[test] fn xa() { - let iso = Iso9660::new(Path::new("../test-data/xa.iso")).unwrap(); - let entry = iso.entry("/copying").unwrap(); + let iso = Iso::new(PathBuf::from("../test-data/xa.iso")).unwrap(); + let entry = iso.entry("/copying".to_string()).unwrap(); let xa = entry.xa().unwrap(); assert_eq!(xa.file_num, 0); assert_eq!(xa.group_id, 3000); diff --git a/libcdio-rs/src/lib.rs b/libcdio-rs/src/lib.rs index 657fdfe..90927d2 100644 --- a/libcdio-rs/src/lib.rs +++ b/libcdio-rs/src/lib.rs @@ -34,7 +34,7 @@ pub use crate::{drive::Drive, mmc::Mmc}; #[cfg(feature = "iso9660")] #[doc(inline)] -pub use crate::iso9660::Iso9660; +pub use crate::iso9660::Iso; #[cfg(feature = "udf")] #[doc(inline)]