From 28901ec44efc184b64b56ef9953e20eb9d17a90d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=2E=20Bezerra?= Date: Sun, 4 Apr 2021 23:23:51 -0300 Subject: [PATCH] Binary size decreased by 35% (-400KB) Huge refactor, removed totally `clap` to use our argparsing instead. 500+ modified. --- Cargo.toml | 1 - src/cli.rs | 369 +++++++++++++---------------- src/compressors/compressor.rs | 4 +- src/compressors/tar.rs | 3 +- src/decompressors/decompressor.rs | 4 +- src/decompressors/tar.rs | 6 +- src/decompressors/to_memory.rs | 14 +- src/decompressors/zip.rs | 14 +- src/error.rs | 14 +- src/evaluator.rs | 113 ++++----- src/extension.rs | 49 ++-- src/file.rs | 19 +- src/main.rs | 7 +- src/test.rs | 370 +++++++++++++++--------------- src/utils.rs | 31 +-- 15 files changed, 487 insertions(+), 531 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 725be2f..dd05ee3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,6 @@ description = "A command-line utility for easily compressing and decompressing f [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" diff --git a/src/cli.rs b/src/cli.rs index cb31aba..9aa040d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,260 +1,199 @@ -use std::{ - convert::TryFrom, - env, - ffi::OsString, - fs, - path::{Path, PathBuf}, - vec::Vec, -}; +use std::{env, ffi::OsString, io, path::PathBuf, vec::Vec}; use oof::{arg_flag, flag}; -// use clap::{Arg, Values}; -// use colored::Colorize; - -// use crate::{extension::Extension, file::File}; -use crate::file::File; - #[derive(PartialEq, Eq, Debug)] pub enum Command { /// Files to be compressed Compress { files: Vec, - flags: oof::Flags, + compressed_output_path: PathBuf, }, /// Files to be decompressed and their extensions Decompress { files: Vec, output_folder: Option, - flags: oof::Flags, }, ShowHelp, ShowVersion, } -// #[derive(PartialEq, Eq, Debug)] -// pub struct Command { -// pub kind: CommandKind, -// pub output: Option, -// } +#[derive(PartialEq, Eq, Debug)] +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>) -> { -// 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() +} -// Ok((Command::try_from(matches)?, flag)) -// } - -// impl TryFrom> for Command { -// type Error = crate::Error; - -// 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, -// }) -// } -// } -// } - -pub fn parse_args_and_flags() -> crate::Result { - let args: Vec = env::args_os().skip(1).collect(); - - if oof::matches_any_arg(&args, &["--help", "-h"]) { - return Ok(Command::ShowHelp); +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(Command::ShowHelp); + return Ok(ParsedArgs { + command: Command::ShowVersion, + flags: oof::Flags::default(), + }); } let subcommands = &["compress"]; - let mut flags_info = vec![ - flag!('y', "yes"), - flag!('n', "no"), - // flag!('v', "verbose"), - ]; + let mut flags_info = vec![flag!('y', "yes"), flag!('n', "no")]; - match oof::pop_subcommand(&mut args, subcommands) { + let parsed_args = match oof::pop_subcommand(&mut args, subcommands) { Some(&"compress") => { let (args, flags) = oof::filter_flags(args, &flags_info)?; - let files = args.into_iter().map(PathBuf::from).collect(); + let mut files: Vec = args.into_iter().map(PathBuf::from).collect(); - todo!("Adicionar output_file, que é o files.pop() do fim"); - Ok(Command::Compress { files, flags }) + 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 => { - // Specific flag - flags_info.push(arg_flag!('o', "output_file")); + flags_info.push(arg_flag!('o', "output")); // Parse flags - let (args, flags) = oof::filter_flags(args, &flags_info)?; + let (args, mut flags) = oof::filter_flags(args, &flags_info)?; - let files = args.into_iter().map(PathBuf::from).collect(); - let output_folder = flags.take_arg("output_folder").map(PathBuf::from); + let files: Vec<_> = args.into_iter().map(PathBuf::from).collect(); + let output_folder = flags.take_arg("output").map(PathBuf::from); - Ok(Command::Decompress { + // Is the output here fully correct? + // With the paths not canonicalized? + + let command = Command::Decompress { files, output_folder, - flags, - }) + }; + ParsedArgs { command, flags } } _ => unreachable!("You should match each subcommand passed."), - } + }; + + Ok(parsed_args) +} + +#[cfg(test)] +mod tests { + use super::*; + + 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 ef5086e..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(), @@ -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 fb81341..48971a8 100644 --- a/src/decompressors/zip.rs +++ b/src/decompressors/zip.rs @@ -8,7 +8,7 @@ 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: &Path, file: &ZipFile) { @@ -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![]; @@ -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 5767370..1ffd474 100644 --- a/src/error.rs +++ b/src/error.rs @@ -15,7 +15,7 @@ pub enum Error { InvalidZipArchive(&'static str), PermissionDenied, UnsupportedZipArchive(&'static str), - InputsMustHaveBeenDecompressible(PathBuf), + // InputsMustBeDecompressible(PathBuf), InternalError, CompressingRootFolder, WalkdirError, @@ -44,10 +44,10 @@ 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::InputsMustBeDecompressible(file) => { + // write!(f, "{} ", "[ERROR]".red())?; + // write!(f, "file '{:?}' is not decompressible", file) + // } Error::WalkdirError => { // Already printed in the From block write!(f, "") @@ -88,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 => { @@ -119,7 +119,7 @@ impl From for Error { } impl From for Error { - fn from(err: oof::OofError) -> Self { + fn from(_err: oof::OofError) -> Self { todo!("We need to implement this properly"); } } diff --git a/src/evaluator.rs b/src/evaluator.rs index a44c1b9..b77b7df 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -1,4 +1,8 @@ -use std::{ffi::OsStr, fs, io::Write, path::PathBuf}; +use std::{ + fs, + io::Write, + path::{Path, PathBuf}, +}; use colored::Colorize; @@ -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,43 +133,47 @@ 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() && !utils::permission_for_overwriting(&output_path, flags, &confirm)? { @@ -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,43 +250,24 @@ impl Evaluator { Ok(()) } - pub fn evaluate(command: Command) -> crate::Result<()> { - // Compress { - // files: Vec, - // flags: oof::Flags, - // }, - // /// Files to be decompressed and their extensions - // Decompress { - // files: Vec, - // output_folder: Option, - // flags: oof::Flags, - // }, - // ShowHelp, - // ShowVersion, + pub fn evaluate(command: Command, flags: &oof::Flags) -> crate::Result<()> { match command { - Command::Compress { files, flags } => {} + Command::Compress { + files, + compressed_output_path, + } => Self::compress_files(files, &compressed_output_path, flags)?, Command::Decompress { files, output_folder, - flags, } => { - // for file in files { decompress } - } - Command::ShowHelp => todo!(), - Command::ShowVersion => todo!(), - } - - 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)?; + // 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 15cc88d..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,6 +22,7 @@ fn main() { } fn run() -> crate::Result<()> { - let command = cli::parse_args_and_flags()?; - Evaluator::evaluate(command) + 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 98e260a..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,13 +49,12 @@ 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("."), } @@ -68,19 +69,23 @@ pub(crate) fn change_dir_and_return_parent(filename: &Path) -> crate::Result 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 = to_utf(path);