diff --git a/Cargo.toml b/Cargo.toml index dc1acf3..dd05ee3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,28 +4,33 @@ version = "0.1.4" authors = ["Vinícius Rodrigues Miguel ", "João M. Bezerra "] edition = "2018" readme = "README.md" -homepage = "https://github.com/vrmiguel/ouch" repository = "https://github.com/vrmiguel/ouch" license = "MIT" keywords = ["decompression", "compression", "zip", "tar", "gzip"] categories = ["command-line-utilities", "compression", "encoding"] description = "A command-line utility for easily compressing and decompressing files and directories." - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] colored = "2.0.0" walkdir = "2.3.2" -clap = "2.33.3" tar = "0.4.33" xz2 = "0.1.6" bzip2 = "0.4.2" flate2 = "1.0.14" zip = "0.5.11" +# Dependency from workspace +oof = { path = "./oof" } [profile.release] lto = true codegen-units = 1 opt-level = 3 + +[workspace] +members = [ + ".", + "oof", +] diff --git a/oof/Cargo.toml b/oof/Cargo.toml new file mode 100644 index 0000000..c134bc3 --- /dev/null +++ b/oof/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "oof" +version = "0.1.0" +authors = ["João M. Bezerra "] +edition = "2018" +description = "Ouch's argparsing library" +repository = "https://github.com/vrmiguel/ouch" + +[dependencies] diff --git a/oof/src/error.rs b/oof/src/error.rs new file mode 100644 index 0000000..abc8671 --- /dev/null +++ b/oof/src/error.rs @@ -0,0 +1,35 @@ +use std::{error, ffi::OsString, fmt}; + +use crate::Flag; + +#[derive(Debug)] +pub enum OofError { + FlagValueConflict { + flag: Flag, + previous_value: OsString, + new_value: OsString, + }, +} + +impl error::Error for OofError { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + None + } +} + +impl fmt::Display for OofError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // TODO: implement proper debug messages + match self { + OofError::FlagValueConflict { + flag, + previous_value, + new_value, + } => write!( + f, + "CLI flag value conflicted for flag '--{}', previous: {:?}, new: {:?}.", + flag.long, previous_value, new_value + ), + } + } +} diff --git a/oof/src/flags.rs b/oof/src/flags.rs new file mode 100644 index 0000000..571713c --- /dev/null +++ b/oof/src/flags.rs @@ -0,0 +1,110 @@ +use std::{ + collections::{BTreeMap, BTreeSet}, + ffi::{OsStr, OsString}, +}; + +/// Shallow type, created to indicate a `Flag` that accepts a argument. +/// +/// ArgFlag::long(), is actually a Flag::long(), but sets a internal attribute. +/// +/// Examples in here pls +#[derive(Debug)] +pub struct ArgFlag; + +impl ArgFlag { + pub fn long(name: &'static str) -> Flag { + Flag { + long: name, + short: None, + takes_value: true, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Flag { + // Also the name + pub long: &'static str, + pub short: Option, + pub takes_value: bool, +} + +impl Flag { + pub fn long(name: &'static str) -> Self { + Self { + long: name, + short: None, + takes_value: false, + } + } + + pub fn short(mut self, short_flag_char: char) -> Self { + self.short = Some(short_flag_char); + self + } +} + +#[derive(Default, PartialEq, Eq, Debug)] +pub struct Flags { + pub boolean_flags: BTreeSet<&'static str>, + pub argument_flags: BTreeMap<&'static str, OsString>, +} + +impl Flags { + pub fn new() -> Self { + Self::default() + } +} + +impl Flags { + pub fn is_present(&self, flag_name: &str) -> bool { + self.boolean_flags.contains(flag_name) || self.argument_flags.contains_key(flag_name) + } + + pub fn arg(&self, flag_name: &str) -> Option<&OsString> { + self.argument_flags.get(flag_name) + } + + pub fn take_arg(&mut self, flag_name: &str) -> Option { + self.argument_flags.remove(flag_name) + } +} + +#[derive(Debug)] +pub enum FlagType { + None, + Short, + Long, +} + +impl FlagType { + pub fn from(text: impl AsRef) -> Self { + let text = text.as_ref(); + + let mut iter; + + #[cfg(target_family = "unix")] + { + use std::os::unix::ffi::OsStrExt; + iter = text.as_bytes().iter(); + } + #[cfg(target_family = "windows")] + { + use std::os::windows::ffi::OsStrExt; + iter = text.encode_wide + } + + // 45 is the code for a hyphen + // Typed as 45_u16 for Windows + // Typed as 45_u8 for Unix + if let Some(45) = iter.next() { + if let Some(45) = iter.next() { + Self::Long + } else { + Self::Short + } + } else { + Self::None + } + } +} diff --git a/oof/src/lib.rs b/oof/src/lib.rs new file mode 100644 index 0000000..327d53e --- /dev/null +++ b/oof/src/lib.rs @@ -0,0 +1,371 @@ +//! Ouch's argparsing crate. +//! +//! The usage of this crate is heavily based on boolean_flags and +//! argument_flags, there should be an more _obvious_ naming. + +mod error; +mod flags; +pub mod util; + +use std::{ + collections::BTreeMap, + ffi::{OsStr, OsString}, +}; + +pub use error::OofError; +pub use flags::{ArgFlag, Flag, FlagType, Flags}; +use util::trim_double_hyphen; + +/// Pop leading application `subcommand`, if valid. +/// +/// `args` can be a Vec of `OsString` or `OsStr` +/// `subcommands` is any container that can yield `&str` through `AsRef`, can be `Vec<&str>` or +/// a GREAT `BTreeSet` (or `BTreeSet<&str>`). +pub fn pop_subcommand<'a, T, I, II>(args: &mut Vec, subcommands: I) -> Option<&'a II> +where + I: IntoIterator, + II: AsRef, + T: AsRef, +{ + if args.is_empty() { + return None; + } + + for subcommand in subcommands.into_iter() { + if subcommand.as_ref() == args[0].as_ref() { + args.remove(0); + return Some(subcommand); + } + } + + None +} + +/// Detect flags from args and filter from args. +/// +/// Each flag received via flags_info should must have unique long and short identifiers. +/// +/// # Panics (Developer errors) +/// - If there are duplicated short flag identifiers. +/// - If there are duplicated long flag identifiers. +/// +/// Both conditions cause panic because your program's flags specification is meant to have unique +/// flags. There shouldn't be two "--verbose" flags, for example. +/// Caller should guarantee it, fortunately, this can almost always be caught while prototyping in +/// debug mode, test your CLI flags once, if it works once, you're good, +/// +/// # Errors (User errors) +/// - Argument flag comes at last arg, so there's no way to provide an argument. +/// - Or if it doesn't comes at last, but the rest are just flags, no possible and valid arg. +/// - Short flags with multiple letters in the same arg contain a argument flag that does not come +/// as the last one in the list (example "-oahc", where 'o', 'a', or 'h' is a argument flag, but do +/// not comes at last, so it is impossible for them to receive the required argument. +/// - User passes same flag twice (short or long, boolean or arg). +/// +/// ... +pub fn filter_flags( + args: Vec, + flags_info: &[Flag], +) -> Result<(Vec, Flags), OofError> { + let mut short_flags_info = BTreeMap::::new(); + let mut long_flags_info = BTreeMap::<&'static str, &Flag>::new(); + + for flag in flags_info.iter() { + // Panics if duplicated/conflicts + assert!( + !long_flags_info.contains_key(flag.long), + "DEV ERROR: duplicated long flag '{}'.", + flag.long + ); + + long_flags_info.insert(flag.long, &flag); + + if let Some(short) = flag.short { + // Panics if duplicated/conflicts + assert!( + !short_flags_info.contains_key(&short), + "DEV ERROR: duplicated short flag '-{}'.", + short + ); + short_flags_info.insert(short, &flag); + } + } + + // Consume args, filter out flags, and add back to args new vec + let mut iter = args.into_iter(); + let mut new_args = vec![]; + let mut result_flags = Flags::new(); + + while let Some(arg) = iter.next() { + let flag_type = FlagType::from(&arg); + + // If it isn't a flag, retrieve to `args` and skip this iteration + if let FlagType::None = flag_type { + new_args.push(arg); + continue; + } + + // If it is a flag, now we try to interpret as valid utf-8 + let flag: &str = arg + .to_str() + .unwrap_or_else(|| panic!("User error: The flag needs to be valid utf8")); + + // Only one hyphen in the flag + // A short flag can be of form "-", "-abcd", "-h", "-v", etc + if let FlagType::Short = flag_type { + assert_eq!(flag.chars().next(), Some('-')); + + // TODO + // TODO: what should happen if the flag is empty????? + // if flags.chars().skip(1).next().is_none() { + // panic!("User error: flag is empty???"); + // } + + // Skip hyphen and get all letters + let letters = flag.chars().skip(1).collect::>(); + + // For each letter in the short arg, except the last one + for (i, letter) in letters.iter().copied().enumerate() { + // Safety: this loop only runs when len >= 1 + let is_last_letter = i == letters.len() - 1; + + let flag_info = short_flags_info.get(&letter).unwrap_or_else(|| { + panic!("User error: Unexpected/UNKNOWN flag `letter`, error") + }); + + if !is_last_letter && flag_info.takes_value { + panic!("User error: Only the last letter can refer to flag that takes values"); + // Because "-AB argument" only works if B takes values, not A. + // That is, the short flag that takes values need to come at the end + // of this piece of text + } + + let flag_name: &'static str = flag_info.long; + + if flag_info.takes_value { + // If it was already inserted + if result_flags.argument_flags.contains_key(flag_name) { + panic!("User error: duplicated, found this flag TWICE!"); + } + + // pop the next one + let flag_argument = iter.next(); + flag_argument.unwrap_or_else(|| { + panic!( + "USer errror: argument flag `argument_flag` came at last, but it \ + requires an argument" + ) + }); + + // Otherwise, insert it (TODO: grab next one and add it) + // result_flags.argument_flags.insert(flag_info.long); + } else { + // If it was already inserted + if result_flags.boolean_flags.contains(flag_name) { + panic!("User error: duplicated, found this flag TWICE!"); + } + // Otherwise, insert it + result_flags.boolean_flags.insert(flag_name); + } + } + } + + if let FlagType::Long = flag_type { + let flag = trim_double_hyphen(flag); + + let flag_info = long_flags_info.get(flag).unwrap_or_else(|| { + panic!("User error: Unexpected/UNKNOWN flag '{}'", flag); + }); + + let flag_name = flag_info.long; + + if flag_info.takes_value { + // If it was already inserted + if result_flags.argument_flags.contains_key(&flag_name) { + panic!("User error: duplicated, found this flag TWICE!"); + } + + let flag_argument = iter.next().unwrap_or_else(|| { + panic!( + "USer errror: argument flag `argument_flag` came at last, but it requires \ + an argument" + ) + }); + result_flags.argument_flags.insert(flag_name, flag_argument); + } else { + // If it was already inserted + if result_flags.boolean_flags.contains(&flag_name) { + panic!("User error: duplicated, found this flag TWICE!"); + } + // Otherwise, insert it + result_flags.boolean_flags.insert(&flag_name); + } + + // // TODO + // TODO: what should happen if the flag is empty????? + // if flag.is_empty() { + // panic!("Is this an error?"); + // } + } + } + + Ok((new_args, result_flags)) +} + +/// Says if any text matches any arg +pub fn matches_any_arg(args: &[T], texts: &[U]) -> bool +where + T: AsRef, + U: AsRef, +{ + texts + .iter() + .any(|text| args.iter().any(|arg| arg.as_ref() == text.as_ref())) +} + +#[cfg(test)] +mod tests { + use crate::*; + + fn gen_args(text: &str) -> Vec { + let args = text.split_whitespace(); + args.map(OsString::from).collect() + } + + // asdasdsa + #[test] + fn test_filter_flags() { + let flags_info = [ + ArgFlag::long("output_file").short('o'), + Flag::long("verbose").short('v'), + Flag::long("help").short('h'), + ]; + let args = gen_args("ouch a.zip -v b.tar.gz --output_file new_folder c.tar"); + + let (args, mut flags) = filter_flags(args, &flags_info).unwrap(); + + assert_eq!(args, gen_args("ouch a.zip b.tar.gz c.tar")); + assert!(flags.is_present("output_file")); + assert_eq!( + Some(&OsString::from("new_folder")), + flags.arg("output_file") + ); + assert_eq!( + Some(OsString::from("new_folder")), + flags.take_arg("output_file") + ); + assert!(!flags.is_present("output_file")); + } + + #[test] + fn test_pop_subcommand() { + let subcommands = &["commit", "add", "push", "remote"]; + let mut args = gen_args("add a b c"); + + let result = pop_subcommand(&mut args, subcommands); + + assert_eq!(result, Some(&"add")); + assert_eq!(args[0], "a"); + + // Check when no subcommand matches + let mut args = gen_args("a b c"); + let result = pop_subcommand(&mut args, subcommands); + + assert_eq!(result, None); + assert_eq!(args[0], "a"); + } + + // #[test] + // fn test_flag_info_macros() { + // let flags_info = [ + // arg_flag!('o', "output_file"), + // arg_flag!("delay"), + // flag!('v', "verbose"), + // flag!('h', "help"), + // flag!("version"), + // ]; + + // let expected = [ + // ArgFlag::long("output_file").short('o'), + // ArgFlag::long("delay"), + // Flag::long("verbose").short('v'), + // Flag::long("help").short('h'), + // Flag::long("version"), + // ]; + + // assert_eq!(flags_info, expected); + // } + + #[test] + // TODO: remove should_panic and use proper error handling inside of filter_args + #[should_panic] + fn test_flag_info_with_long_flag_conflict() { + let flags_info = [ + ArgFlag::long("verbose").short('a'), + Flag::long("verbose").short('b'), + ]; + + // Should panic here + let result = filter_flags(vec![], &flags_info); + assert!(matches!(result, Err(OofError::FlagValueConflict { .. }))); + } + + #[test] + // TODO: remove should_panic and use proper error handling inside of filter_args + #[should_panic] + fn test_flag_info_with_short_flag_conflict() { + let flags_info = [ + ArgFlag::long("output_file").short('o'), + Flag::long("verbose").short('o'), + ]; + + // Should panic here + filter_flags(vec![], &flags_info).unwrap_err(); + } + + #[test] + fn test_matches_any_arg_function() { + let args = gen_args("program a -h b"); + assert!(matches_any_arg(&args, &["--help", "-h"])); + + let args = gen_args("program a b --help"); + assert!(matches_any_arg(&args, &["--help", "-h"])); + + let args = gen_args("--version program a b"); + assert!(matches_any_arg(&args, &["--version", "-v"])); + + let args = gen_args("program -v a --version b"); + assert!(matches_any_arg(&args, &["--version", "-v"])); + + // Cases without it + let args = gen_args("program a b c"); + assert!(!matches_any_arg(&args, &["--help", "-h"])); + + let args = gen_args("program a --version -v b c"); + assert!(!matches_any_arg(&args, &["--help", "-h"])); + } +} + +/// Create a flag with long flag (?). +#[macro_export] +macro_rules! flag { + ($short:expr, $long:expr) => {{ + oof::Flag::long($long).short($short) + }}; + + ($long:expr) => {{ + oof::Flag::long($long) + }}; +} + +/// Create a flag with long flag (?), receives argument (?). +#[macro_export] +macro_rules! arg_flag { + ($short:expr, $long:expr) => {{ + oof::ArgFlag::long($long).short($short) + }}; + + ($long:expr) => {{ + oof::ArgFlag::long($long) + }}; +} diff --git a/oof/src/util.rs b/oof/src/util.rs new file mode 100644 index 0000000..dddfb3d --- /dev/null +++ b/oof/src/util.rs @@ -0,0 +1,14 @@ +/// Util function to skip the two leading long flag hyphens. +pub fn trim_double_hyphen(flag_text: &str) -> &str { + let mut chars = flag_text.chars(); + chars.nth(1); // Skipping 2 chars + chars.as_str() +} + +/// Util function to skip the single leading short flag hyphen. +pub fn trim_single_hyphen(flag_text: &str) -> &str { + let mut chars = flag_text.chars(); + + chars.next(); // Skipping 1 char + chars.as_str() +} diff --git a/src/cli.rs b/src/cli.rs index efab31e..9aa040d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,212 +1,199 @@ -use std::{ - convert::TryFrom, - fs, - path::{Path, PathBuf}, - vec::Vec, -}; +use std::{env, ffi::OsString, io, path::PathBuf, vec::Vec}; -use clap::{Arg, Values}; -use colored::Colorize; - -use crate::{extension::Extension, file::File}; +use oof::{arg_flag, flag}; #[derive(PartialEq, Eq, Debug)] -pub enum CommandKind { - Compression( - /// Files to be compressed - Vec, - ), - Decompression( - /// Files to be decompressed and their extensions - Vec, - ), -} - -#[derive(PartialEq, Eq, Copy, Clone, Debug)] -pub enum Flags { - // No flags supplied - None, - // Flag -y, --yes supplied - AlwaysYes, - // Flag -n, --no supplied - AlwaysNo, +pub enum Command { + /// Files to be compressed + Compress { + files: Vec, + compressed_output_path: PathBuf, + }, + /// Files to be decompressed and their extensions + Decompress { + files: Vec, + output_folder: Option, + }, + ShowHelp, + ShowVersion, } #[derive(PartialEq, Eq, Debug)] -pub struct Command { - pub kind: CommandKind, - pub output: Option, +pub struct CommandInfo { + pub command: Command, + pub flags: oof::Flags, + // pub config: Config, // From .TOML, maybe, in the future } -pub fn clap_app<'a, 'b>() -> clap::App<'a, 'b> { - clap::App::new("ouch") - .version("0.1.4") - .about("ouch is a unified compression & decompression utility") - .after_help( -"ouch infers what to based on the extensions of the input files and output file received. -Examples: `ouch -i movies.tar.gz classes.zip -o Videos/` in order to decompress files into a folder. - `ouch -i headers/ sources/ Makefile -o my-project.tar.gz` - `ouch -i image{1..50}.jpeg -o images.zip` -Please relate any issues or contribute at https://github.com/vrmiguel/ouch") - .author("Vinícius R. Miguel") - .help_message("Displays this message and exits") - .settings(&[ - clap::AppSettings::ColoredHelp, - clap::AppSettings::ArgRequiredElseHelp, - ]) - .arg( - Arg::with_name("input") - .required(true) - .multiple(true) - .long("input") - .short("i") - .help("The input files or directories.") - .takes_value(true), - ) - .arg( - Arg::with_name("output") - // --output/-o not required when output can be inferred from the input files - .required(false) - .multiple(false) - .long("output") - .short("o") - .help("The output directory or compressed file.") - .takes_value(true), - ) - .arg( - Arg::with_name("yes") - .required(false) - .multiple(false) - .long("yes") - .short("y") - .help("Says yes to all confirmation dialogs.") - .conflicts_with("no") - .takes_value(false), - ) - .arg( - Arg::with_name("no") - .required(false) - .multiple(false) - .long("no") - .short("n") - .help("Says no to all confirmation dialogs.") - .conflicts_with("yes") - .takes_value(false), - ) +/// Calls parse_args_and_flags_from using std::env::args_os ( argv ) +pub fn parse_args() -> crate::Result { + let args = env::args_os().skip(1).collect(); + parse_args_from(args) } -pub fn get_matches() -> clap::ArgMatches<'static> { - clap_app().get_matches() +pub struct ParsedArgs { + pub command: Command, + pub flags: oof::Flags, + // pub program_called: OsString, // Useful? } -pub fn parse_matches(matches: clap::ArgMatches<'static>) -> crate::Result<(Command, Flags)> { - let flag = match (matches.is_present("yes"), matches.is_present("no")) { - (true, true) => unreachable!(), - (true, _) => Flags::AlwaysYes, - (_, true) => Flags::AlwaysNo, - (_, _) => Flags::None, +fn canonicalize_files(files: Vec) -> io::Result> { + files.into_iter().map(|path| path.canonicalize()).collect() +} + +pub fn parse_args_from(mut args: Vec) -> crate::Result { + if oof::matches_any_arg(&args, &["--help", "-h"]) || args.is_empty() { + return Ok(ParsedArgs { + command: Command::ShowHelp, + flags: oof::Flags::default(), + }); + } + + if oof::matches_any_arg(&args, &["--version"]) { + return Ok(ParsedArgs { + command: Command::ShowVersion, + flags: oof::Flags::default(), + }); + } + + let subcommands = &["compress"]; + + let mut flags_info = vec![flag!('y', "yes"), flag!('n', "no")]; + + let parsed_args = match oof::pop_subcommand(&mut args, subcommands) { + Some(&"compress") => { + let (args, flags) = oof::filter_flags(args, &flags_info)?; + let mut files: Vec = args.into_iter().map(PathBuf::from).collect(); + + if files.len() < 2 { + panic!("The compress subcommands demands at least 2 arguments, see usage:......."); + } + // Safety: we checked that args.len() >= 2 + let compressed_output_path = files.pop().unwrap(); + + let files = canonicalize_files(files)?; + + let command = Command::Compress { + files, + compressed_output_path, + }; + ParsedArgs { command, flags } + } + // Defaults to decompression when there is no subcommand + None => { + flags_info.push(arg_flag!('o', "output")); + + // Parse flags + let (args, mut flags) = oof::filter_flags(args, &flags_info)?; + + let files: Vec<_> = args.into_iter().map(PathBuf::from).collect(); + let output_folder = flags.take_arg("output").map(PathBuf::from); + + // Is the output here fully correct? + // With the paths not canonicalized? + + let command = Command::Decompress { + files, + output_folder, + }; + ParsedArgs { command, flags } + } + _ => unreachable!("You should match each subcommand passed."), }; - Ok((Command::try_from(matches)?, flag)) + Ok(parsed_args) } -impl TryFrom> for Command { - type Error = crate::Error; +#[cfg(test)] +mod tests { + use super::*; - fn try_from(matches: clap::ArgMatches<'static>) -> crate::Result { - let process_decompressible_input = |input_files: Values| { - let input_files = - input_files.map(|filename| (Path::new(filename), Extension::new(filename))); - - for file in input_files.clone() { - match file { - (filename, Ok(_)) => { - let path = Path::new(filename); - if !path.exists() { - return Err(crate::Error::FileNotFound(filename.into())); - } - } - (filename, Err(_)) => { - return Err(crate::Error::InputsMustHaveBeenDecompressible( - filename.into(), - )); - } - } - } - - Ok(input_files - .map(|(filename, extension)| { - (fs::canonicalize(filename).unwrap(), extension.unwrap()) - }) - .map(File::from) - .collect::>()) - }; - - // Possibilities: - // * Case 1: output not supplied, therefore try to infer output by checking if all input files are decompressible - // * Case 2: output supplied - - let output_was_supplied = matches.is_present("output"); - - let input_files = matches.values_of("input").unwrap(); // Safe to unwrap since input is an obligatory argument - - if output_was_supplied { - let output_file = matches.value_of("output").unwrap(); // Safe unwrap since we've established that output was supplied - - let output_file_extension = Extension::new(output_file); - - let output_is_compressible = output_file_extension.is_ok(); - if output_is_compressible { - // The supplied output is compressible, so we'll compress our inputs to it - - let canonical_paths = input_files.clone().map(Path::new).map(fs::canonicalize); - for (filename, canonical_path) in input_files.zip(canonical_paths.clone()) { - if let Err(err) = canonical_path { - let path = PathBuf::from(filename); - if !path.exists() { - return Err(crate::Error::FileNotFound(path)); - } - - eprintln!("{} {}", "[ERROR]".red(), err); - return Err(crate::Error::IoError); - } - } - - let input_files = canonical_paths.map(Result::unwrap).collect(); - - Ok(Command { - kind: CommandKind::Compression(input_files), - output: Some(File { - path: output_file.into(), - contents_in_memory: None, - extension: Some(output_file_extension.unwrap()), - }), - }) - } else { - // Output not supplied - // Checking if input files are decompressible - - let input_files = process_decompressible_input(input_files)?; - - Ok(Command { - kind: CommandKind::Decompression(input_files), - output: Some(File { - path: output_file.into(), - contents_in_memory: None, - extension: None, - }), - }) - } - } else { - // else: output file not supplied - // Case 1: all input files are decompressible - // Case 2: error - let input_files = process_decompressible_input(input_files)?; - - Ok(Command { - kind: CommandKind::Decompression(input_files), - output: None, - }) - } + fn gen_args(text: &str) -> Vec { + let args = text.split_whitespace(); + args.map(OsString::from).collect() } + + // // util for test the argument parsing + // macro_rules! test { + // ($expected_command:expr, $input_text:expr) => {{ + // assert_eq!( + // $expected_command, + // oof::try_arg_parsing($input_text.split_whitespace()) + // ) + // }}; + // } + + macro_rules! parse { + ($input_text:expr) => {{ + let args = gen_args($input_text); + parse_args_from(args).unwrap() + }}; + } + + #[test] + // The absolute flags that ignore all the other argparsing rules are --help and --version + fn test_absolute_flags() { + let expected = Command::ShowHelp; + assert_eq!(expected, parse!("").command); + assert_eq!(expected, parse!("-h").command); + assert_eq!(expected, parse!("--help").command); + assert_eq!(expected, parse!("aaaaaaaa --help -o -e aaa").command); + assert_eq!(expected, parse!("aaaaaaaa -h").command); + assert_eq!(expected, parse!("--help compress aaaaaaaa").command); + assert_eq!(expected, parse!("compress --help").command); + assert_eq!(expected, parse!("--version --help").command); + assert_eq!(expected, parse!("aaaaaaaa -v aaaa -h").command); + + let expected = Command::ShowVersion; + assert_eq!(expected, parse!("ouch --version").command); + assert_eq!(expected, parse!("ouch a --version b").command); + } + + #[test] + fn test_arg_parsing_compress_subcommand() { + let files = ["a", "b", "c"].iter().map(PathBuf::from).collect(); + + let expected = Command::Compress { + files, + compressed_output_path: "d".into(), + }; + assert_eq!(expected, parse!("compress a b c d").command); + } + + #[test] + fn test_arg_parsing_decompress_subcommand() { + let files: Vec<_> = ["a", "b", "c"].iter().map(PathBuf::from).collect(); + + let expected = Command::Decompress { + files: files.clone(), + output_folder: None, + }; + assert_eq!(expected, parse!("a b c").command); + + let expected = Command::Decompress { + files, + output_folder: Some("folder".into()), + }; + assert_eq!(expected, parse!("a b c --output folder").command); + assert_eq!(expected, parse!("a b --output folder c").command); + assert_eq!(expected, parse!("a --output folder b c").command); + assert_eq!(expected, parse!("--output folder a b c").command); + } + + // #[test] + // fn test_arg_parsing_decompress_subcommand() { + // let files: Vec = ["a", "b", "c"].iter().map(PathBuf::from).collect(); + + // let expected = Ok(Command::Decompress { + // files: files.clone(), + // }); + // test!(expected, "ouch a b c"); + + // let files: Vec = ["a", "b", "c", "d"].iter().map(PathBuf::from).collect(); + + // let expected = Ok(Command::Decompress { + // files: files.clone(), + // }); + // test!(expected, "ouch a b c d"); + // } } diff --git a/src/compressors/compressor.rs b/src/compressors/compressor.rs index 8f885fd..77ff2b8 100644 --- a/src/compressors/compressor.rs +++ b/src/compressors/compressor.rs @@ -8,9 +8,9 @@ use crate::file::File; // FileInMemory(Vec) // } -pub enum Entry { +pub enum Entry<'a> { Files(Vec), - InMemory(File), + InMemory(File<'a>), } pub trait Compressor { diff --git a/src/compressors/tar.rs b/src/compressors/tar.rs index 0bf89ff..67573a6 100644 --- a/src/compressors/tar.rs +++ b/src/compressors/tar.rs @@ -5,8 +5,7 @@ use tar::Builder; use walkdir::WalkDir; use super::compressor::Entry; -use crate::utils; -use crate::{compressors::Compressor, file::File}; +use crate::{compressors::Compressor, file::File, utils}; pub struct TarCompressor {} diff --git a/src/decompressors/decompressor.rs b/src/decompressors/decompressor.rs index 9d004c9..d14bd69 100644 --- a/src/decompressors/decompressor.rs +++ b/src/decompressors/decompressor.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use crate::{cli::Flags, file::File}; +use crate::file::File; pub enum DecompressionResult { FilesUnpacked(Vec), @@ -12,6 +12,6 @@ pub trait Decompressor { &self, from: File, into: &Option, - flags: Flags, + flags: &oof::Flags, ) -> crate::Result; } diff --git a/src/decompressors/tar.rs b/src/decompressors/tar.rs index 272b9b2..c52868c 100644 --- a/src/decompressors/tar.rs +++ b/src/decompressors/tar.rs @@ -8,13 +8,13 @@ use colored::Colorize; use tar::{self, Archive}; use super::decompressor::{DecompressionResult, Decompressor}; -use crate::{cli::Flags, dialogs::Confirmation, file::File, utils}; +use crate::{dialogs::Confirmation, file::File, utils}; #[derive(Debug)] pub struct TarDecompressor {} impl TarDecompressor { - fn unpack_files(from: File, into: &Path, flags: Flags) -> crate::Result> { + fn unpack_files(from: File, into: &Path, flags: &oof::Flags) -> crate::Result> { println!( "{}: attempting to decompress {:?}", "ouch".bright_blue(), @@ -35,11 +35,11 @@ impl TarDecompressor { let mut file = file?; let file_path = PathBuf::from(into).join(file.path()?); - if file_path.exists() { - if !utils::permission_for_overwriting(&file_path, flags, &confirm)? { - // The user does not want to overwrite the file - continue; - } + if file_path.exists() + && !utils::permission_for_overwriting(&file_path, flags, &confirm)? + { + // The user does not want to overwrite the file + continue; } file.unpack_in(into)?; @@ -64,7 +64,7 @@ impl Decompressor for TarDecompressor { &self, from: File, into: &Option, - flags: Flags, + flags: &oof::Flags, ) -> crate::Result { let destination_path = utils::get_destination_path(into); diff --git a/src/decompressors/to_memory.rs b/src/decompressors/to_memory.rs index 5ac3f31..810a29c 100644 --- a/src/decompressors/to_memory.rs +++ b/src/decompressors/to_memory.rs @@ -6,7 +6,7 @@ use std::{ use colored::Colorize; use super::decompressor::{DecompressionResult, Decompressor}; -use crate::{cli::Flags, utils}; +use crate::utils; // use niffler; use crate::{extension::CompressionFormat, file::File}; @@ -28,8 +28,8 @@ fn get_decoder<'a>( } impl DecompressorToMemory { - fn unpack_file(from: &Path, format: CompressionFormat) -> crate::Result> { - let file = std::fs::read(from)?; + fn unpack_file(path: &Path, format: CompressionFormat) -> crate::Result> { + let file = std::fs::read(path)?; let mut reader = get_decoder(format, Box::new(&file[..])); @@ -39,7 +39,7 @@ impl DecompressorToMemory { println!( "{}: {:?} extracted into memory ({} bytes).", "info".yellow(), - from, + path, bytes_read ); @@ -66,7 +66,7 @@ impl Decompressor for GzipDecompressor { &self, from: File, into: &Option, - _: Flags, + _: &oof::Flags, ) -> crate::Result { DecompressorToMemory::decompress(from, CompressionFormat::Gzip, into) } @@ -77,7 +77,7 @@ impl Decompressor for BzipDecompressor { &self, from: File, into: &Option, - _: Flags, + _: &oof::Flags, ) -> crate::Result { DecompressorToMemory::decompress(from, CompressionFormat::Bzip, into) } @@ -88,7 +88,7 @@ impl Decompressor for LzmaDecompressor { &self, from: File, into: &Option, - _: Flags, + _: &oof::Flags, ) -> crate::Result { DecompressorToMemory::decompress(from, CompressionFormat::Lzma, into) } diff --git a/src/decompressors/zip.rs b/src/decompressors/zip.rs index 3c4a5a4..48971a8 100644 --- a/src/decompressors/zip.rs +++ b/src/decompressors/zip.rs @@ -8,10 +8,10 @@ use colored::Colorize; use zip::{self, read::ZipFile, ZipArchive}; use super::decompressor::{DecompressionResult, Decompressor}; -use crate::{cli::Flags, dialogs::Confirmation, file::File, utils}; +use crate::{dialogs::Confirmation, file::File, utils}; #[cfg(unix)] -fn __unix_set_permissions(file_path: &PathBuf, file: &ZipFile) { +fn __unix_set_permissions(file_path: &Path, file: &ZipFile) { use std::os::unix::fs::PermissionsExt; if let Some(mode) = file.unix_mode() { @@ -34,13 +34,13 @@ impl ZipDecompressor { } } - pub fn zip_decompress( - archive: &mut ZipArchive, + pub fn zip_decompress( + archive: &mut ZipArchive, into: &Path, - flags: Flags, + flags: &oof::Flags, ) -> crate::Result> where - T: Read + Seek, + R: Read + Seek, { let confirm = Confirmation::new("Do you want to overwrite 'FILE'?", Some("FILE")); let mut unpacked_files = vec![]; @@ -52,11 +52,11 @@ impl ZipDecompressor { }; let file_path = into.join(file_path); - if file_path.exists() { - if !utils::permission_for_overwriting(&file_path, flags, &confirm)? { - // The user does not want to overwrite the file - continue; - } + if file_path.exists() + && !utils::permission_for_overwriting(&file_path, flags, &confirm)? + { + // The user does not want to overwrite the file + continue; } Self::check_for_comments(&file); @@ -94,7 +94,7 @@ impl ZipDecompressor { Ok(unpacked_files) } - fn unpack_files(from: File, into: &Path, flags: Flags) -> crate::Result> { + fn unpack_files(from: File, into: &Path, flags: &oof::Flags) -> crate::Result> { println!("{} decompressing {:?}", "[OUCH]".bright_blue(), &from.path); match from.contents_in_memory { @@ -119,7 +119,7 @@ impl Decompressor for ZipDecompressor { &self, from: File, into: &Option, - flags: Flags, + flags: &oof::Flags, ) -> crate::Result { let destination_path = utils::get_destination_path(into); diff --git a/src/error.rs b/src/error.rs index 60f8316..1ffd474 100644 --- a/src/error.rs +++ b/src/error.rs @@ -15,10 +15,10 @@ pub enum Error { InvalidZipArchive(&'static str), PermissionDenied, UnsupportedZipArchive(&'static str), - InputsMustHaveBeenDecompressible(PathBuf), + // InputsMustBeDecompressible(PathBuf), InternalError, CompressingRootFolder, - WalkdirError + WalkdirError, } pub type Result = std::result::Result; @@ -44,11 +44,11 @@ impl fmt::Display for Error { write!(f, "{} ", "[ERROR]".red())?; write!(f, "cannot compress to \'{}\', likely because it has an unsupported (or missing) extension.", filename) } - Error::InputsMustHaveBeenDecompressible(file) => { - write!(f, "{} ", "[ERROR]".red())?; - write!(f, "file '{:?}' is not decompressible", file) - } - Error::WalkdirError => { + // Error::InputsMustBeDecompressible(file) => { + // write!(f, "{} ", "[ERROR]".red())?; + // write!(f, "file '{:?}' is not decompressible", file) + // } + Error::WalkdirError => { // Already printed in the From block write!(f, "") } @@ -61,8 +61,17 @@ impl fmt::Display for Error { write!(f, "{} ", "[ERROR]".red())?; let spacing = " "; writeln!(f, "It seems you're trying to compress the root folder.")?; - writeln!(f, "{}This is unadvisable since ouch does compressions in-memory.", spacing)?; - write!(f, "{}Use a more appropriate tool for this, such as {}.", spacing, "rsync".green()) + writeln!( + f, + "{}This is unadvisable since ouch does compressions in-memory.", + spacing + )?; + write!( + f, + "{}Use a more appropriate tool for this, such as {}.", + spacing, + "rsync".green() + ) } Error::InternalError => { write!(f, "{} ", "[ERROR]".red())?; @@ -79,7 +88,7 @@ impl fmt::Display for Error { impl From for Error { fn from(err: std::io::Error) -> Self { match err.kind() { - std::io::ErrorKind::NotFound => Self::FileNotFound("".into()), + std::io::ErrorKind::NotFound => panic!("{}", err), std::io::ErrorKind::PermissionDenied => Self::PermissionDenied, std::io::ErrorKind::AlreadyExists => Self::AlreadyExists, _other => { @@ -108,3 +117,9 @@ impl From for Error { Self::WalkdirError } } + +impl From for Error { + fn from(_err: oof::OofError) -> Self { + todo!("We need to implement this properly"); + } +} diff --git a/src/evaluator.rs b/src/evaluator.rs index 105ed26..b77b7df 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -1,9 +1,13 @@ -use std::{ffi::OsStr, fs, io::Write, path::PathBuf}; +use std::{ + fs, + io::Write, + path::{Path, PathBuf}, +}; use colored::Colorize; use crate::{ - cli::{Command, CommandKind, Flags}, + cli::Command, compressors::{ BzipCompressor, Compressor, Entry, GzipCompressor, LzmaCompressor, TarCompressor, ZipCompressor, @@ -20,10 +24,13 @@ use crate::{ pub struct Evaluator {} +type BoxedCompressor = Box; +type BoxedDecompressor = Box; + impl Evaluator { pub fn get_compressor( file: &File, - ) -> crate::Result<(Option>, Box)> { + ) -> crate::Result<(Option, BoxedCompressor)> { let extension = match &file.extension { Some(extension) => extension.clone(), None => { @@ -65,7 +72,7 @@ impl Evaluator { pub fn get_decompressor( file: &File, - ) -> crate::Result<(Option>, Box)> { + ) -> crate::Result<(Option, BoxedDecompressor)> { let extension = match &file.extension { Some(extension) => extension.clone(), None => { @@ -100,24 +107,25 @@ impl Evaluator { fn decompress_file_in_memory( bytes: Vec, - file_path: PathBuf, + file_path: &Path, decompressor: Option>, - output_file: &Option, + output_file: Option, extension: Option, - flags: Flags, + flags: &oof::Flags, ) -> crate::Result<()> { - let output_file_path = utils::get_destination_path(output_file); + let output_file_path = utils::get_destination_path(&output_file); - let mut filename = file_path + let file_name = file_path .file_stem() - .unwrap_or_else(|| output_file_path.as_os_str()); + .map(Path::new) + .unwrap_or(output_file_path); - if filename == "." { + if "." == file_name.as_os_str() { // I believe this is only possible when the supplied input has a name // of the sort `.tar` or `.zip' and no output has been supplied. - filename = OsStr::new("ouch-output"); + // file_name = OsStr::new("ouch-output"); + todo!("Pending review, what is this supposed to do??"); } - let filename = PathBuf::from(filename); // If there is a decompressor to use, we'll create a file in-memory and decompress it let decompressor = match decompressor { @@ -125,48 +133,52 @@ impl Evaluator { None => { // There is no more processing to be done on the input file (or there is but currently unsupported) // Therefore, we'll save what we have in memory into a file. - println!("{}: saving to {:?}.", "info".yellow(), filename); + println!("{}: saving to {:?}.", "info".yellow(), file_name); - if filename.exists() { + if file_name.exists() { let confirm = Confirmation::new("Do you want to overwrite 'FILE'?", Some("FILE")); - if !utils::permission_for_overwriting(&filename, flags, &confirm)? { + if !utils::permission_for_overwriting(&file_name, flags, &confirm)? { return Ok(()); } } - let mut f = fs::File::create(output_file_path.join(filename))?; + let mut f = fs::File::create(output_file_path.join(file_name))?; f.write_all(&bytes)?; return Ok(()); } }; let file = File { - path: filename, + path: file_name, contents_in_memory: Some(bytes), extension, }; - let decompression_result = decompressor.decompress(file, output_file, flags)?; + let decompression_result = decompressor.decompress(file, &output_file, flags)?; if let DecompressionResult::FileInMemory(_) = decompression_result { - // Should not be reachable. - unreachable!(); + unreachable!("Shouldn't"); } Ok(()) } - fn compress_files(files: Vec, mut output: File, flags: Flags) -> crate::Result<()> { + fn compress_files( + files: Vec, + output_path: &Path, + flags: &oof::Flags, + ) -> crate::Result<()> { + let mut output = File::from(output_path)?; + let confirm = Confirmation::new("Do you want to overwrite 'FILE'?", Some("FILE")); let (first_compressor, second_compressor) = Self::get_compressor(&output)?; // TODO: use -y and -n here - let output_path = output.path.clone(); - if output_path.exists() { - if !utils::permission_for_overwriting(&output_path, flags, &confirm)? { - // The user does not want to overwrite the file - return Ok(()); - } + if output_path.exists() + && !utils::permission_for_overwriting(&output_path, flags, &confirm)? + { + // The user does not want to overwrite the file + return Ok(()); } let bytes = match first_compressor { @@ -187,7 +199,7 @@ impl Evaluator { println!( "{}: writing to {:?}. ({} bytes)", "info".yellow(), - &output_path, + output_path, bytes.len() ); fs::write(output_path, bytes)?; @@ -195,13 +207,21 @@ impl Evaluator { Ok(()) } - fn decompress_file(file: File, output: &Option, flags: Flags) -> crate::Result<()> { + fn decompress_file( + file_path: &Path, + output: Option<&Path>, + flags: &oof::Flags, + ) -> crate::Result<()> { + let file = File::from(file_path)?; + let output = match output { + Some(inner) => Some(File::from(inner)?), + None => None, + }; let (first_decompressor, second_decompressor) = Self::get_decompressor(&file)?; - let file_path = file.path.clone(); let extension = file.extension.clone(); - let decompression_result = second_decompressor.decompress(file, output, flags)?; + let decompression_result = second_decompressor.decompress(file, &output, &flags)?; match decompression_result { DecompressionResult::FileInMemory(bytes) => { @@ -230,20 +250,24 @@ impl Evaluator { Ok(()) } - pub fn evaluate(command: Command, flags: Flags) -> crate::Result<()> { - let output = command.output.clone(); - - match command.kind { - CommandKind::Compression(files_to_compress) => { - // Safe to unwrap since output is mandatory for compression - let output = output.unwrap(); - Self::compress_files(files_to_compress, output, flags)?; - } - CommandKind::Decompression(files_to_decompress) => { - for file in files_to_decompress { - Self::decompress_file(file, &output, flags)?; + pub fn evaluate(command: Command, flags: &oof::Flags) -> crate::Result<()> { + match command { + Command::Compress { + files, + compressed_output_path, + } => Self::compress_files(files, &compressed_output_path, flags)?, + Command::Decompress { + files, + output_folder, + } => { + // From Option to Option<&Path> + let output_folder = output_folder.as_ref().map(|path| Path::new(path)); + for file in files.iter() { + Self::decompress_file(file, output_folder, flags)?; } } + Command::ShowHelp => todo!("call help function"), + Command::ShowVersion => todo!("call version function"), } Ok(()) } diff --git a/src/extension.rs b/src/extension.rs index 0510efc..f8a4029 100644 --- a/src/extension.rs +++ b/src/extension.rs @@ -7,6 +7,8 @@ use std::{ use CompressionFormat::*; +use crate::utils::to_utf; + /// Represents the extension of a file, but only really caring about /// compression formats (and .tar). /// Ex.: Extension::new("file.tar.gz") == Extension { first_ext: Some(Tar), second_ext: Gzip } @@ -16,20 +18,17 @@ pub struct Extension { pub second_ext: CompressionFormat, } -pub fn get_extension_from_filename(filename: &str) -> Option<(&str, &str)> { - let path = Path::new(filename); +pub fn get_extension_from_filename(file_name: &OsStr) -> Option<(&OsStr, &OsStr)> { + let path = Path::new(file_name); - let ext = path.extension().and_then(OsStr::to_str)?; + let ext = path.extension()?; - let previous_extension = path - .file_stem() - .and_then(OsStr::to_str) - .and_then(get_extension_from_filename); + let previous_extension = path.file_stem().and_then(get_extension_from_filename); if let Some((_, prev)) = previous_extension { Some((prev, ext)) } else { - Some(("", ext)) + Some((OsStr::new(""), ext)) } } @@ -43,32 +42,32 @@ impl From for Extension { } impl Extension { - pub fn new(filename: &str) -> crate::Result { - let ext_from_str = |ext| match ext { - "zip" => Ok(Zip), - "tar" => Ok(Tar), - "gz" => Ok(Gzip), - "bz" | "bz2" => Ok(Bzip), - "xz" | "lz" | "lzma" => Ok(Lzma), - other => Err(crate::Error::UnknownExtensionError(other.into())), + pub fn from(file_name: &OsStr) -> crate::Result { + let compression_format_from = |ext: &OsStr| match ext { + _ if ext == "zip" => Ok(Zip), + _ if ext == "tar" => Ok(Tar), + _ if ext == "gz" => Ok(Gzip), + _ if ext == "bz" || ext == "bz2" => Ok(Bzip), + _ if ext == "xz" || ext == "lz" || ext == "lzma" => Ok(Lzma), + other => Err(crate::Error::UnknownExtensionError(to_utf(other))), }; - let (first_ext, second_ext) = match get_extension_from_filename(filename) { + let (first_ext, second_ext) = match get_extension_from_filename(&file_name) { Some(extension_tuple) => match extension_tuple { - ("", snd) => (None, snd), + (os_str, snd) if os_str.is_empty() => (None, snd), (fst, snd) => (Some(fst), snd), }, - None => return Err(crate::Error::MissingExtensionError(filename.into())), + None => return Err(crate::Error::MissingExtensionError(to_utf(file_name))), }; let (first_ext, second_ext) = match (first_ext, second_ext) { (None, snd) => { - let ext = ext_from_str(snd)?; + let ext = compression_format_from(snd)?; (None, ext) } (Some(fst), snd) => { - let snd = ext_from_str(snd)?; - let fst = ext_from_str(fst).ok(); + let snd = compression_format_from(snd)?; + let fst = compression_format_from(fst).ok(); (fst, snd) } }; @@ -130,9 +129,9 @@ impl TryFrom<&PathBuf> for CompressionFormat { impl TryFrom<&str> for CompressionFormat { type Error = crate::Error; - fn try_from(filename: &str) -> Result { - let filename = Path::new(filename); - let ext = match filename.extension() { + fn try_from(file_name: &str) -> Result { + let file_name = Path::new(file_name); + let ext = match file_name.extension() { Some(ext) => ext, None => return Err(crate::Error::MissingExtensionError(String::new())), }; diff --git a/src/file.rs b/src/file.rs index ddb31e5..56d2c26 100644 --- a/src/file.rs +++ b/src/file.rs @@ -1,11 +1,11 @@ -use std::path::PathBuf; +use std::path::Path; use crate::extension::Extension; #[derive(Debug, Clone, PartialEq, Eq)] -pub struct File { +pub struct File<'a> { /// File's (relative) path - pub path: PathBuf, + pub path: &'a Path, /// The bytes that compose the file. /// Only used when the whole file is kept in-memory pub contents_in_memory: Option>, @@ -17,12 +17,15 @@ pub struct File { pub extension: Option, } -impl From<(PathBuf, Extension)> for File { - fn from((path, format): (PathBuf, Extension)) -> Self { - Self { +impl<'a> File<'a> { + pub fn from(path: &'a Path) -> crate::Result { + let extension = Extension::from(path.as_ref())?; + eprintln!("dev warning: Should we really ignore the errors from the convertion above?"); + + Ok(File { path, contents_in_memory: None, - extension: Some(format), - } + extension: Some(extension), + }) } } diff --git a/src/main.rs b/src/main.rs index 4400359..164dfce 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,8 @@ mod utils; use error::{Error, Result}; use evaluator::Evaluator; +use crate::cli::ParsedArgs; + fn main() { if let Err(err) = run() { println!("{}", err); @@ -20,7 +22,7 @@ fn main() { } fn run() -> crate::Result<()> { - let matches = cli::get_matches(); - let (command, flags) = cli::parse_matches(matches)?; - Evaluator::evaluate(command, flags) + let ParsedArgs { command, flags } = cli::parse_args()?; + dbg!(&command); + Evaluator::evaluate(command, &flags) } diff --git a/src/test.rs b/src/test.rs index 269f31e..322606d 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,211 +1,219 @@ // TODO: remove tests of CompressionFormat::try_from since that's no longer used anywhere -#[cfg(test)] -mod cli { - use std::{convert::TryFrom, fs, path::Path}; +// use std::{ +// convert::TryFrom, +// ffi::{OsStr, OsString}, +// fs, +// path::Path, +// }; - use crate::{ - cli::{clap_app, Command, CommandKind::*}, - extension::{CompressionFormat::*, Extension}, - file::File, - }; +// use crate::{ +// cli::Command, +// extension::{CompressionFormat, CompressionFormat::*, Extension}, +// file::File, +// }; - // ouch's command-line logic uses fs::canonicalize on its inputs so we cannot - // use made-up files for testing. - // make_dummy_file therefores creates a small temporary file to bypass fs::canonicalize errors - fn make_dummy_file<'a, P>(path: P) -> crate::Result<()> - where - P: AsRef + 'a, - { - fs::write(path.as_ref(), &[2, 3, 4, 5, 6, 7, 8, 9, 10])?; - Ok(()) - } +// // Helper +// fn gen_args(text: &str) -> Vec { +// let args = text.split_whitespace(); +// args.map(OsString::from).collect() +// } - #[test] - fn decompress_files_into_folder() -> crate::Result<()> { - make_dummy_file("file.zip")?; - let matches = clap_app().get_matches_from(vec!["ouch", "-i", "file.zip", "-o", "folder/"]); - let command_from_matches = Command::try_from(matches)?; +// #[cfg(test)] +// mod cli { +// use super::*; - assert_eq!( - command_from_matches, - Command { - kind: Decompression(vec![File { - path: fs::canonicalize("file.zip")?, - contents_in_memory: None, - extension: Some(Extension::from(Zip)) - }]), - output: Some(File { - path: "folder".into(), - contents_in_memory: None, - extension: None - }), - } - ); +// // ouch's command-line logic uses fs::canonicalize on its inputs so we cannot +// // use made-up files for testing. +// // make_dummy_file therefores creates a small temporary file to bypass fs::canonicalize errors +// fn make_dummy_file<'a, P>(path: P) -> crate::Result<()> +// where +// P: AsRef + 'a, +// { +// fs::write(path.as_ref(), &[2, 3, 4, 5, 6, 7, 8, 9, 10])?; +// Ok(()) +// } - fs::remove_file("file.zip")?; +// #[test] +// fn decompress_files_into_folder() -> crate::Result<()> { +// make_dummy_file("file.zip")?; +// let args = gen_args("ouch -i file.zip -o folder/"); +// let (command, flags) = cli::parse_args_and_flags_from(args)?; - Ok(()) - } +// assert_eq!( +// command, +// Command::Decompress { +// files: args, +// compressed_output_path: PathBuf, +// } // kind: Decompress(vec![File { +// // path: fs::canonicalize("file.zip")?, +// // contents_in_memory: None, +// // extension: Some(Extension::from(Zip)) +// // }]), +// // output: Some(File { +// // path: "folder".into(), +// // contents_in_memory: None, +// // extension: None +// // }), +// // } +// ); - #[test] - fn decompress_files() -> crate::Result<()> { - make_dummy_file("my-cool-file.zip")?; - make_dummy_file("file.tar")?; - let matches = - clap_app().get_matches_from(vec!["ouch", "-i", "my-cool-file.zip", "file.tar"]); - let command_from_matches = Command::try_from(matches)?; +// fs::remove_file("file.zip")?; - assert_eq!( - command_from_matches, - Command { - kind: Decompression(vec![ - File { - path: fs::canonicalize("my-cool-file.zip")?, - contents_in_memory: None, - extension: Some(Extension::from(Zip)) - }, - File { - path: fs::canonicalize("file.tar")?, - contents_in_memory: None, - extension: Some(Extension::from(Tar)) - } - ],), - output: None, - } - ); +// Ok(()) +// } - fs::remove_file("my-cool-file.zip")?; - fs::remove_file("file.tar")?; +// #[test] +// fn decompress_files() -> crate::Result<()> { +// make_dummy_file("my-cool-file.zip")?; +// make_dummy_file("file.tar")?; +// let matches = +// clap_app().get_matches_from(vec!["ouch", "-i", "my-cool-file.zip", "file.tar"]); +// let command_from_matches = Command::try_from(matches)?; - Ok(()) - } +// assert_eq!( +// command_from_matches, +// Command { +// kind: Decompress(vec![ +// File { +// path: fs::canonicalize("my-cool-file.zip")?, +// contents_in_memory: None, +// extension: Some(Extension::from(Zip)) +// }, +// File { +// path: fs::canonicalize("file.tar")?, +// contents_in_memory: None, +// extension: Some(Extension::from(Tar)) +// } +// ],), +// output: None, +// } +// ); - #[test] - fn compress_files() -> crate::Result<()> { - make_dummy_file("file")?; - make_dummy_file("file2.jpeg")?; - make_dummy_file("file3.ok")?; +// fs::remove_file("my-cool-file.zip")?; +// fs::remove_file("file.tar")?; - let matches = clap_app().get_matches_from(vec![ - "ouch", - "-i", - "file", - "file2.jpeg", - "file3.ok", - "-o", - "file.tar", - ]); - let command_from_matches = Command::try_from(matches)?; +// Ok(()) +// } - assert_eq!( - command_from_matches, - Command { - kind: Compression(vec![ - fs::canonicalize("file")?, - fs::canonicalize("file2.jpeg")?, - fs::canonicalize("file3.ok")? - ]), - output: Some(File { - path: "file.tar".into(), - contents_in_memory: None, - extension: Some(Extension::from(Tar)) - }), - } - ); +// #[test] +// fn compress_files() -> crate::Result<()> { +// make_dummy_file("file")?; +// make_dummy_file("file2.jpeg")?; +// make_dummy_file("file3.ok")?; - fs::remove_file("file")?; - fs::remove_file("file2.jpeg")?; - fs::remove_file("file3.ok")?; +// let matches = clap_app().get_matches_from(vec![ +// "ouch", +// "-i", +// "file", +// "file2.jpeg", +// "file3.ok", +// "-o", +// "file.tar", +// ]); +// let command_from_matches = Command::try_from(matches)?; - Ok(()) - } -} +// assert_eq!( +// command_from_matches, +// Command { +// kind: Compress(vec![ +// fs::canonicalize("file")?, +// fs::canonicalize("file2.jpeg")?, +// fs::canonicalize("file3.ok")? +// ]), +// output: Some(File { +// path: "file.tar".into(), +// contents_in_memory: None, +// extension: Some(Extension::from(Tar)) +// }), +// } +// ); -#[cfg(test)] -mod cli_errors { +// fs::remove_file("file")?; +// fs::remove_file("file2.jpeg")?; +// fs::remove_file("file3.ok")?; - use std::convert::TryFrom; +// Ok(()) +// } +// } - use crate::cli::{clap_app, Command}; +// #[cfg(test)] +// mod cli_errors { - #[test] - fn compress_files() -> crate::Result<()> { - let matches = - clap_app().get_matches_from(vec!["ouch", "-i", "a_file", "file2.jpeg", "file3.ok"]); - let res = Command::try_from(matches); +// #[test] +// fn compress_files() -> crate::Result<()> { +// let matches = +// clap_app().get_matches_from(vec!["ouch", "-i", "a_file", "file2.jpeg", "file3.ok"]); +// let res = Command::try_from(matches); - assert_eq!( - res, - Err(crate::Error::InputsMustHaveBeenDecompressible( - "a_file".into() - )) - ); +// assert_eq!( +// res, +// Err(crate::Error::InputsMustHaveBeenDecompressible( +// "a_file".into() +// )) +// ); - Ok(()) - } -} +// Ok(()) +// } +// } -#[cfg(test)] -mod extension_extraction { - use std::convert::TryFrom; +// #[cfg(test)] +// mod extension_extraction { - use crate::extension::{CompressionFormat, Extension}; +// #[test] +// fn test_extension_zip() { +// let path = "filename.tar.zip"; +// assert_eq!( +// CompressionFormat::try_from(path), +// Ok(CompressionFormat::Zip) +// ); +// } - #[test] - fn test_extension_zip() { - let path = "filename.tar.zip"; - assert_eq!( - CompressionFormat::try_from(path), - Ok(CompressionFormat::Zip) - ); - } +// #[test] +// fn test_extension_tar_gz() { +// let extension = Extension::from(OsStr::new("folder.tar.gz")).unwrap(); +// assert_eq!( +// extension, +// Extension { +// first_ext: Some(CompressionFormat::Tar), +// second_ext: CompressionFormat::Gzip +// } +// ); +// } - #[test] - fn test_extension_tar_gz() { - let extension = Extension::new("folder.tar.gz").unwrap(); - assert_eq!( - extension, - Extension { - first_ext: Some(CompressionFormat::Tar), - second_ext: CompressionFormat::Gzip - } - ); - } +// #[test] +// fn test_extension_tar() { +// let path = "pictures.tar"; +// assert_eq!( +// CompressionFormat::try_from(path), +// Ok(CompressionFormat::Tar) +// ); +// } - #[test] - fn test_extension_tar() { - let path = "pictures.tar"; - assert_eq!( - CompressionFormat::try_from(path), - Ok(CompressionFormat::Tar) - ); - } +// #[test] +// fn test_extension_gz() { +// let path = "passwords.tar.gz"; +// assert_eq!( +// CompressionFormat::try_from(path), +// Ok(CompressionFormat::Gzip) +// ); +// } - #[test] - fn test_extension_gz() { - let path = "passwords.tar.gz"; - assert_eq!( - CompressionFormat::try_from(path), - Ok(CompressionFormat::Gzip) - ); - } +// #[test] +// fn test_extension_lzma() { +// let path = "mygame.tar.lzma"; +// assert_eq!( +// CompressionFormat::try_from(path), +// Ok(CompressionFormat::Lzma) +// ); +// } - #[test] - fn test_extension_lzma() { - let path = "mygame.tar.lzma"; - assert_eq!( - CompressionFormat::try_from(path), - Ok(CompressionFormat::Lzma) - ); - } - - #[test] - fn test_extension_bz() { - let path = "songs.tar.bz"; - assert_eq!( - CompressionFormat::try_from(path), - Ok(CompressionFormat::Bzip) - ); - } -} +// #[test] +// fn test_extension_bz() { +// let path = "songs.tar.bz"; +// assert_eq!( +// CompressionFormat::try_from(path), +// Ok(CompressionFormat::Bzip) +// ); +// } +// } diff --git a/src/utils.rs b/src/utils.rs index c131fce..ab9fc79 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,11 +1,13 @@ use std::{ - env, fs, + env, + ffi::OsStr, + fs, path::{Path, PathBuf}, }; use colored::Colorize; -use crate::{cli::Flags, dialogs::Confirmation, extension::CompressionFormat, file::File}; +use crate::{dialogs::Confirmation, extension::CompressionFormat, file::File}; pub(crate) fn ensure_exists<'a, P>(path: P) -> crate::Result<()> where @@ -47,19 +49,18 @@ pub(crate) fn create_path_if_non_existent(path: &Path) -> crate::Result<()> { Ok(()) } -pub(crate) fn get_destination_path(dest: &Option) -> &Path { +pub(crate) fn get_destination_path<'a>(dest: &'a Option) -> &'a Path { match dest { - Some(output) => { + Some(output_file) => { // Must be None according to the way command-line arg. parsing in Ouch works - assert_eq!(output.extension, None); - - Path::new(&output.path) + assert_eq!(output_file.extension, None); + Path::new(&output_file.path) } None => Path::new("."), } } -pub(crate) fn change_dir_and_return_parent(filename: &PathBuf) -> crate::Result { +pub(crate) fn change_dir_and_return_parent(filename: &Path) -> crate::Result { let previous_location = env::current_dir()?; let parent = if let Some(parent) = filename.parent() { @@ -68,21 +69,30 @@ pub(crate) fn change_dir_and_return_parent(filename: &PathBuf) -> crate::Result< return Err(crate::Error::CompressingRootFolder); }; - env::set_current_dir(parent)?; + env::set_current_dir(parent).unwrap(); + Ok(previous_location) } pub fn permission_for_overwriting( - path: &PathBuf, - flags: Flags, + path: &Path, + flags: &oof::Flags, confirm: &Confirmation, ) -> crate::Result { - match flags { - Flags::AlwaysYes => return Ok(true), - Flags::AlwaysNo => return Ok(false), - Flags::None => {} + match (flags.is_present("yes"), flags.is_present("false")) { + (true, true) => { + unreachable!("This shoul've been cutted out in the ~/src/cli.rs filter flags function.") + } + (true, _) => return Ok(true), + (_, true) => return Ok(false), + _ => {} } - let file_path_str = &*path.as_path().to_string_lossy(); - Ok(confirm.ask(Some(file_path_str))?) + let file_path_str = to_utf(path); + confirm.ask(Some(&file_path_str)) +} + +pub fn to_utf(os_str: impl AsRef) -> String { + let text = format!("{:?}", os_str.as_ref()); + text.trim_matches('"').to_string() }