diff --git a/xdvdfs-cli/src/cmd_build_image.rs b/xdvdfs-cli/src/cmd_build_image.rs index 7f23cfa..e25e8ba 100644 --- a/xdvdfs-cli/src/cmd_build_image.rs +++ b/xdvdfs-cli/src/cmd_build_image.rs @@ -9,6 +9,8 @@ use xdvdfs::write::{ img::ProgressInfo, }; +use crate::img::with_extension; + #[derive(Args)] #[command( about = "Pack an image from a given specification", @@ -201,7 +203,7 @@ pub async fn cmd_build_image(args: &BuildImageArgs) -> Result<(), anyhow::Error> if let Some(output) = output_path { source_path.join(output) } else { - source_path.with_extension("xiso.iso") + with_extension(&source_path, "xiso.iso", true) } }; diff --git a/xdvdfs-cli/src/cmd_compress.rs b/xdvdfs-cli/src/cmd_compress.rs index 5da014d..cb7b1a7 100644 --- a/xdvdfs-cli/src/cmd_compress.rs +++ b/xdvdfs-cli/src/cmd_compress.rs @@ -8,7 +8,7 @@ use xdvdfs::{ write::{self, img::ProgressInfo}, }; -use crate::img::open_image_raw; +use crate::img::{open_image_raw, with_extension}; #[derive(Args)] #[command(about = "Pack and compress an image from a given directory or source ISO image")] @@ -44,9 +44,9 @@ impl ciso::split::SplitFilesystem for SplitStdFs { async fn close(&mut self, _: BufFile) {} } -fn get_default_image_path(source_path: &Path) -> Option { +fn get_default_image_path(source_path: &Path, is_dir: bool) -> Option { let source_file_name = source_path.file_name()?; - let output = PathBuf::from(source_file_name).with_extension("cso"); + let output = with_extension(Path::new(source_file_name), "cso", is_dir); Some(output) } @@ -54,12 +54,14 @@ fn get_default_image_path(source_path: &Path) -> Option { #[maybe_async] pub async fn cmd_compress(args: &CompressArgs) -> Result<(), anyhow::Error> { let source_path = PathBuf::from(&args.source_path); + let meta = std::fs::metadata(&source_path)?; + let is_dir = meta.is_dir(); let image_path = args .image_path .as_ref() .map(PathBuf::from) - .unwrap_or_else(|| get_default_image_path(&source_path).unwrap()); + .unwrap_or_else(|| get_default_image_path(&source_path, is_dir).unwrap()); // This is unlikely to happen, since compressed input is unsupported // and this will fail anyway, but we check to avoid truncating the input accidentally @@ -67,6 +69,12 @@ pub async fn cmd_compress(args: &CompressArgs) -> Result<(), anyhow::Error> { return Err(anyhow::anyhow!("Source and destination paths are the same")); } + if image_path.starts_with(&source_path) { + return Err(anyhow::anyhow!( + "Destination path is contained by source path" + )); + } + let mut output = ciso::split::SplitOutput::new(SplitStdFs, image_path); let progress_callback = |pi| match pi { @@ -94,8 +102,7 @@ pub async fn cmd_compress(args: &CompressArgs) -> Result<(), anyhow::Error> { _ => {} }; - let meta = std::fs::metadata(&source_path)?; - if meta.is_dir() { + if is_dir { let mut fs = write::fs::StdFilesystem::create(&source_path); let mut slbd = write::fs::SectorLinearBlockDevice::default(); let mut slbfs: write::fs::SectorLinearBlockFilesystem< diff --git a/xdvdfs-cli/src/cmd_pack.rs b/xdvdfs-cli/src/cmd_pack.rs index 80e9452..eb74964 100644 --- a/xdvdfs-cli/src/cmd_pack.rs +++ b/xdvdfs-cli/src/cmd_pack.rs @@ -4,6 +4,8 @@ use clap::Args; use maybe_async::maybe_async; use xdvdfs::write::{self, img::ProgressInfo}; +use crate::img::with_extension; + #[derive(Args)] #[command(about = "Pack an image from a given directory or source ISO image")] pub struct PackArgs { @@ -14,14 +16,18 @@ pub struct PackArgs { image_path: Option, } -fn get_default_image_path(source_path: &Path) -> Result { +fn get_default_image_path(source_path: &Path, is_dir: bool) -> Result { let source_file_name = source_path .file_name() .ok_or(anyhow::anyhow!("Failed to get file name from source path"))?; - let output = PathBuf::from(source_file_name).with_extension("iso"); + let output = with_extension(Path::new(source_file_name), "iso", is_dir); if output.exists() && output.canonicalize()? == source_path { - return Ok(PathBuf::from(source_file_name).with_extension("xiso.iso")); + return Ok(with_extension( + Path::new(source_file_name), + "xiso.iso", + is_dir, + )); } Ok(output) @@ -30,13 +36,15 @@ fn get_default_image_path(source_path: &Path) -> Result #[maybe_async] pub async fn cmd_pack(args: &PackArgs) -> Result<(), anyhow::Error> { let source_path = PathBuf::from(&args.source_path).canonicalize()?; + let meta = std::fs::metadata(&source_path)?; + let is_dir = meta.is_dir(); let image_path = args .image_path .as_ref() .map(PathBuf::from) .map(Ok) - .unwrap_or_else(|| get_default_image_path(&source_path))?; + .unwrap_or_else(|| get_default_image_path(&source_path, is_dir))?; if image_path.exists() && image_path.canonicalize()? == source_path { return Err(anyhow::anyhow!("Source and destination paths are the same")); @@ -46,9 +54,15 @@ pub async fn cmd_pack(args: &PackArgs) -> Result<(), anyhow::Error> { .write(true) .truncate(true) .create(true) - .open(image_path)?; + .open(&image_path)?; let mut image = std::io::BufWriter::with_capacity(1024 * 1024, image); + if image_path.canonicalize()?.starts_with(&source_path) { + return Err(anyhow::anyhow!( + "Destination path is contained by source path" + )); + } + let mut file_count: usize = 0; let mut progress_count: usize = 0; let progress_callback = |pi| match pi { @@ -65,8 +79,7 @@ pub async fn cmd_pack(args: &PackArgs) -> Result<(), anyhow::Error> { _ => {} }; - let meta = std::fs::metadata(&source_path)?; - if meta.is_dir() { + if is_dir { let mut fs = write::fs::StdFilesystem::create(&source_path); write::img::create_xdvdfs_image(&mut fs, &mut image, progress_callback).await?; } else if meta.is_file() { diff --git a/xdvdfs-cli/src/img.rs b/xdvdfs-cli/src/img.rs index e14099d..391b9c7 100644 --- a/xdvdfs-cli/src/img.rs +++ b/xdvdfs-cli/src/img.rs @@ -1,6 +1,10 @@ use ciso::read::CSOReader; use maybe_async::maybe_async; -use std::{fs::File, io::BufReader, path::Path}; +use std::{ + fs::File, + io::BufReader, + path::{Path, PathBuf}, +}; use xdvdfs::blockdev::{BlockDeviceRead, OffsetWrapper}; pub struct CSOBlockDevice> { @@ -71,3 +75,52 @@ pub async fn open_image( Ok(image) } } + +/// Similar to Path::with_extension, but will not overwrite the extension for +/// directories +// TODO: Replace with `Path::with_added_extension` after it stabilizes +pub fn with_extension(path: &Path, ext: &str, is_dir: bool) -> PathBuf { + if !is_dir { + return path.with_extension(ext); + } + + let original_ext = path.extension(); + let Some(original_ext) = original_ext else { + return path.with_extension(ext); + }; + + let mut new_ext = original_ext.to_owned(); + new_ext.push("."); + new_ext.push(ext); + path.with_extension(new_ext) +} + +#[cfg(test)] +mod test { + use super::with_extension; + use std::path::Path; + + #[test] + fn with_extension_not_dir() { + assert_eq!( + with_extension(Path::new("file.abc"), "xyz", false), + Path::new("file.xyz") + ); + } + + #[test] + fn with_extension_dir_no_extension() { + assert_eq!( + with_extension(Path::new("dir"), "xyz", true), + Path::new("dir.xyz") + ); + } + + #[test] + fn with_extension_dir_with_extension() { + assert_eq!( + with_extension(Path::new("dir.abc"), "xyz", true), + Path::new("dir.abc.xyz") + ); + } +} diff --git a/xdvdfs-web/src/packing.rs b/xdvdfs-web/src/packing.rs index e933f32..e5725ee 100644 --- a/xdvdfs-web/src/packing.rs +++ b/xdvdfs-web/src/packing.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use crate::ops::XDVDFSOperations; use crate::picker::FilePickerBackend; @@ -9,6 +9,25 @@ use xdvdfs::write::img::ProgressInfo; use yew::prelude::*; use yewprint::{Button, ButtonGroup, Callout, Icon, Intent, ProgressBar, H5}; +/// Similar to Path::with_extension, but will not overwrite the extension for +/// directories +// TODO: Replace with `Path::with_added_extension` after it stabilizes +pub fn with_extension(path: &Path, ext: &str, is_dir: bool) -> PathBuf { + if !is_dir { + return path.with_extension(ext); + } + + let original_ext = path.extension(); + let Some(original_ext) = original_ext else { + return path.with_extension(ext); + }; + + let mut new_ext = original_ext.to_owned(); + new_ext.push("."); + new_ext.push(ext); + path.with_extension(new_ext) +} + #[derive(PartialEq, PartialOrd, Copy, Clone)] pub enum ImageCreationState { CreatingFilesystem, @@ -173,6 +192,10 @@ where PickerResult::FileHandle(fh) => FPB::file_name(fh), }) } + + fn is_input_directory(&self) -> bool { + matches!(self.input_handle_type, Some(InputHandleType::Directory)) + } } impl Component for ImageBuilderWorkflow @@ -231,12 +254,15 @@ where kind={PickerKind::SaveFile( self.input_name().map(|name| - PathBuf::from(name) - .with_extension("xiso.iso") - .file_name() - .and_then(|name| name.to_str()) - .map(|name| name.to_owned()) - .expect("file name should be defined") + with_extension( + Path::new(&name), + "xiso.iso", + self.is_input_directory(), + ) + .file_name() + .and_then(|name| name.to_str()) + .map(|name| name.to_owned()) + .expect("file name should be defined") ))} button_text={"Save image"} disabled={is_packing}