diff --git a/Cargo.toml b/Cargo.toml index 0314d09..40a2849 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,4 +16,5 @@ log = "0.4" simple_logger = "4.0" hex = "0.4" serde = { version = "1.0", features = ["derive"] } -toml = "0.5" \ No newline at end of file +toml = "0.5" +dialoguer = { version = "0.10", default-features = false } \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index f5313e0..1d14b7b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -68,6 +68,11 @@ impl FileOpError { pub fn make_write(name: &'static str, path: PathBuf, error: io::Error) -> Box { Self::boxed(FileOpAction::Write, name, path, error) } + + /// Checks if the underlying error kind is `ErrorKind::AlreadyExists`. + pub fn is_exists(&self) -> bool { + self.error.kind() == io::ErrorKind::AlreadyExists + } } impl fmt::Display for FileOpError { diff --git a/src/main.rs b/src/main.rs index 25d72e7..7a78349 100644 --- a/src/main.rs +++ b/src/main.rs @@ -53,6 +53,7 @@ fn do_unpack<'a>( overwrite: bool, create_parent_dirs: bool, print_header: bool, + silent: bool, ) -> Result<(), UnpackError<'a>> { use UnpackError::*; @@ -93,7 +94,7 @@ fn do_unpack<'a>( filename.push("ApImg4Ticket.der"); let ticket_path = util::qualify_path_if_needed(&filename, out_dir); - util::save_file("ticket", ticket_path, ticket, overwrite)?; + util::save_file("ticket", ticket_path, ticket, overwrite, silent)?; the_manifest.ticket = Some(filename); } @@ -104,7 +105,13 @@ fn do_unpack<'a>( match segments_parser.next_segment()? { None => { let serialized_manifest = toml::to_vec(&the_manifest).unwrap(); - util::save_file("manifest", manifest_path, &serialized_manifest, overwrite)?; + util::save_file( + "manifest", + manifest_path, + &serialized_manifest, + overwrite, + silent, + )?; info!("Done."); @@ -114,7 +121,7 @@ fn do_unpack<'a>( let filename = filename_for_tag(segment.tag); let path = util::qualify_path_if_needed(&filename, out_dir); - util::save_file("segment", path, segment.data, overwrite)?; + util::save_file("segment", path, segment.data, overwrite, silent)?; the_manifest.segments.push(SegmentDesc { path: filename, @@ -130,6 +137,7 @@ fn do_pack<'a>( manifest_path: &'a Path, out_path: Option<&'a Path>, overwrite: bool, + silent: bool, ) -> Result<(), PackError<'a>> { use PackError::*; @@ -141,7 +149,7 @@ fn do_pack<'a>( // create the output file let input_dir = manifest_path.parent(); let out_file_path = util::qualify_path_or_default_if_needed(out_path, input_dir, "ftab.bin"); - let mut out_file = util::create_file("output file", &out_file_path, overwrite)?; + let mut out_file = util::create_file("output file", &out_file_path, overwrite, silent)?; debug!("Writing ftab to {}.", out_file_path.display()); @@ -173,6 +181,11 @@ fn main() { (disables logging entirely), TRACE, DEBUG, INFO, WARN and ERROR.", ), ) + .arg( + arg!(silent: -s --silent).help( + "Makes all user prompts take their default action instead of being displayed.", + ), + ) .subcommand( Command::new("unpack") .arg(arg!(overwrite: -o --overwrite).help( @@ -230,6 +243,7 @@ fn main() { _ => LevelFilter::Warn, }; let print_header = matches.get_flag("print_header"); + let silent = matches.get_flag("silent"); SimpleLogger::new().with_level(log_level).init().unwrap(); @@ -247,6 +261,7 @@ fn main() { overwrite, create_parent_dirs, print_header, + silent, ) { error!("{}", e); } @@ -258,7 +273,7 @@ fn main() { .map(PathBuf::as_path); let overwrite = sub_matches.get_flag("overwrite"); - if let Err(e) = do_pack(manifest_path, out_file, overwrite) { + if let Err(e) = do_pack(manifest_path, out_file, overwrite, silent) { error!("{}", e); } } diff --git a/src/util.rs b/src/util.rs index ddd17e4..116b56b 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,4 +1,5 @@ use crate::error::FileOpError; +use dialoguer::Confirm; use std::{ borrow::Cow, fs::{File, OpenOptions}, @@ -28,20 +29,51 @@ fn create_file_impl( name: &'static str, path: &Path, overwrite: bool, + silent: bool, ) -> Result> { - OpenOptions::new() + let map_error = |error| FileOpError::make_create(name, path.to_path_buf(), error); + let result = OpenOptions::new() .write(true) .create_new(!overwrite) .create(overwrite) .truncate(overwrite) .open(path) - .map_err(|error| FileOpError::make_create(name, path.to_path_buf(), error)) + .map_err(map_error); + + let Err(error) = result else { + return result + }; + + // In case neither the overwrite flag nor the silent flag was passed, we want to ask the user if + // they want to overwrite the file on receiving a "file exists" error. + if !overwrite && !silent && error.is_exists() && path.is_file() { + let response = Confirm::new() + .with_prompt(format!( + "Do you want to overwrite the file at '{}'?", + path.display() + )) + .default(false) + .interact() + .expect("failed to display a prompt to the user"); + + if response { + return OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(path) + .map_err(map_error); + } + } + + Err(error) } /// Creates a file at the specified path. /// /// In case the `overwrite` argument is `true`, the file will be either created or truncated if it -/// exists, otherwise an existing file at the specified path will cause an error to be returned. +/// exists, otherwise in case `silent` is `false` the user will be asked if overwriting the file is +/// ok, otherwise an error will be returned. /// /// # Errors /// This function will return a boxed `FileOpError` with the `FileOpAction::Create` action in case @@ -50,8 +82,9 @@ pub fn create_file>( name: &'static str, path: P, overwrite: bool, + silent: bool, ) -> Result> { - create_file_impl(name, path.as_ref(), overwrite) + create_file_impl(name, path.as_ref(), overwrite, silent) } fn save_file_impl( @@ -59,8 +92,9 @@ fn save_file_impl( path: &Path, data: &[u8], overwrite: bool, + silent: bool, ) -> Result<(), Box> { - create_file(name, path, overwrite)? + create_file(name, path, overwrite, silent)? .write_all(data) .map_err(|error| FileOpError::make_write(name, path.to_path_buf(), error))?; @@ -72,8 +106,9 @@ fn save_file_impl( /// Creates a file at the specified path and writes data from a slice into it. /// /// In case the `overwrite` argument is `true`, the file will be either created or truncated and -/// overwritten if it exists, otherwise an existing file at the specified path will cause an error -/// to be returned. +/// overwritten if it exists. If the `overwrite` argument is false either a prompt will be +/// displayed to the user to try and open an existing file truncating it or, in case the `silent` +/// argument is `true`, an error will be returned. /// /// File creation is handled by the [`create_file`] function internally. /// @@ -86,8 +121,9 @@ pub fn save_file>( path: P, data: &[u8], overwrite: bool, + silent: bool, ) -> Result<(), Box> { - save_file_impl(name, path.as_ref(), data, overwrite) + save_file_impl(name, path.as_ref(), data, overwrite, silent) } fn qualify_path_if_needed_impl<'a>(path: &'a Path, dir: Option<&Path>) -> Cow<'a, Path> {