mirror of
https://github.com/ouch-org/ouch.git
synced 2025-06-06 19:45:29 +00:00
Merge pull request #24 from vrmiguel/testing-compression
Testing compression and decompression of formats that support multiple files
This commit is contained in:
commit
afbda444ef
@ -21,10 +21,13 @@ tar = "0.4.33"
|
|||||||
xz2 = "0.1.6"
|
xz2 = "0.1.6"
|
||||||
zip = "0.5.11"
|
zip = "0.5.11"
|
||||||
|
|
||||||
|
|
||||||
# Dependency from workspace
|
# Dependency from workspace
|
||||||
oof = { path = "./oof" }
|
oof = { path = "./oof" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempdir = "0.3.7"
|
||||||
|
rand = { version = "0.8.3", default-features = false, features = ["small_rng", "std"] }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
|
@ -16,7 +16,7 @@ pub enum OofError<'t> {
|
|||||||
UnknownLongFlag(String),
|
UnknownLongFlag(String),
|
||||||
MisplacedShortArgFlagError(char),
|
MisplacedShortArgFlagError(char),
|
||||||
MissingValueToFlag(&'t Flag),
|
MissingValueToFlag(&'t Flag),
|
||||||
DuplicatedFlag(&'t Flag)
|
DuplicatedFlag(&'t Flag),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'t> error::Error for OofError<'t> {
|
impl<'t> error::Error for OofError<'t> {
|
||||||
|
@ -13,11 +13,7 @@ pub struct ArgFlag;
|
|||||||
|
|
||||||
impl ArgFlag {
|
impl ArgFlag {
|
||||||
pub fn long(name: &'static str) -> Flag {
|
pub fn long(name: &'static str) -> Flag {
|
||||||
Flag {
|
Flag { long: name, short: None, takes_value: true }
|
||||||
long: name,
|
|
||||||
short: None,
|
|
||||||
takes_value: true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,11 +36,7 @@ impl std::fmt::Display for Flag {
|
|||||||
|
|
||||||
impl Flag {
|
impl Flag {
|
||||||
pub fn long(name: &'static str) -> Self {
|
pub fn long(name: &'static str) -> Self {
|
||||||
Self {
|
Self { long: name, short: None, takes_value: false }
|
||||||
long: name,
|
|
||||||
short: None,
|
|
||||||
takes_value: false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn short(mut self, short_flag_char: char) -> Self {
|
pub fn short(mut self, short_flag_char: char) -> Self {
|
||||||
|
@ -7,7 +7,10 @@ mod error;
|
|||||||
mod flags;
|
mod flags;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
||||||
use std::{collections::BTreeMap, ffi::{OsStr, OsString}};
|
use std::{
|
||||||
|
collections::BTreeMap,
|
||||||
|
ffi::{OsStr, OsString},
|
||||||
|
};
|
||||||
|
|
||||||
pub use error::OofError;
|
pub use error::OofError;
|
||||||
pub use flags::{ArgFlag, Flag, FlagType, Flags};
|
pub use flags::{ArgFlag, Flag, FlagType, Flags};
|
||||||
@ -103,9 +106,9 @@ pub fn filter_flags(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If it is a flag, now we try to interpret it as valid utf-8
|
// If it is a flag, now we try to interpret it as valid utf-8
|
||||||
let flag= match arg.to_str() {
|
let flag = match arg.to_str() {
|
||||||
Some(arg) => arg,
|
Some(arg) => arg,
|
||||||
None => return Err(OofError::InvalidUnicode(arg))
|
None => return Err(OofError::InvalidUnicode(arg)),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Only one hyphen in the flag
|
// Only one hyphen in the flag
|
||||||
@ -127,12 +130,11 @@ pub fn filter_flags(
|
|||||||
// Safety: this loop only runs when len >= 1, so this subtraction is safe
|
// Safety: this loop only runs when len >= 1, so this subtraction is safe
|
||||||
let is_last_letter = i == letters.len() - 1;
|
let is_last_letter = i == letters.len() - 1;
|
||||||
|
|
||||||
let flag_info = short_flags_info.get(&letter).ok_or(
|
let flag_info =
|
||||||
OofError::UnknownShortFlag(letter)
|
short_flags_info.get(&letter).ok_or(OofError::UnknownShortFlag(letter))?;
|
||||||
)?;
|
|
||||||
|
|
||||||
if !is_last_letter && flag_info.takes_value {
|
if !is_last_letter && flag_info.takes_value {
|
||||||
return Err(OofError::MisplacedShortArgFlagError(letter))
|
return Err(OofError::MisplacedShortArgFlagError(letter));
|
||||||
// Because "-AB argument" only works if B takes values, not A.
|
// Because "-AB argument" only works if B takes values, not A.
|
||||||
// That is, the short flag that takes values needs to come at the end
|
// That is, the short flag that takes values needs to come at the end
|
||||||
// of this piece of text
|
// of this piece of text
|
||||||
@ -147,9 +149,8 @@ pub fn filter_flags(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// pop the next one
|
// pop the next one
|
||||||
let flag_argument = iter.next().ok_or(
|
let flag_argument =
|
||||||
OofError::MissingValueToFlag(flag_info)
|
iter.next().ok_or(OofError::MissingValueToFlag(flag_info))?;
|
||||||
)?;
|
|
||||||
|
|
||||||
// Otherwise, insert it.
|
// Otherwise, insert it.
|
||||||
result_flags.argument_flags.insert(flag_name, flag_argument);
|
result_flags.argument_flags.insert(flag_name, flag_argument);
|
||||||
@ -167,9 +168,9 @@ pub fn filter_flags(
|
|||||||
if let FlagType::Long = flag_type {
|
if let FlagType::Long = flag_type {
|
||||||
let flag = trim_double_hyphen(flag);
|
let flag = trim_double_hyphen(flag);
|
||||||
|
|
||||||
let flag_info = long_flags_info.get(flag).ok_or_else(|| {
|
let flag_info = long_flags_info
|
||||||
OofError::UnknownLongFlag(String::from(flag))
|
.get(flag)
|
||||||
})?;
|
.ok_or_else(|| OofError::UnknownLongFlag(String::from(flag)))?;
|
||||||
|
|
||||||
let flag_name = flag_info.long;
|
let flag_name = flag_info.long;
|
||||||
|
|
||||||
@ -179,9 +180,7 @@ pub fn filter_flags(
|
|||||||
return Err(OofError::DuplicatedFlag(flag_info));
|
return Err(OofError::DuplicatedFlag(flag_info));
|
||||||
}
|
}
|
||||||
|
|
||||||
let flag_argument = iter.next().ok_or(
|
let flag_argument = iter.next().ok_or(OofError::MissingValueToFlag(flag_info))?;
|
||||||
OofError::MissingValueToFlag(flag_info)
|
|
||||||
)?;
|
|
||||||
result_flags.argument_flags.insert(flag_name, flag_argument);
|
result_flags.argument_flags.insert(flag_name, flag_argument);
|
||||||
} else {
|
} else {
|
||||||
// If it was already inserted
|
// If it was already inserted
|
||||||
@ -209,9 +208,7 @@ where
|
|||||||
T: AsRef<OsStr>,
|
T: AsRef<OsStr>,
|
||||||
U: AsRef<str>,
|
U: AsRef<str>,
|
||||||
{
|
{
|
||||||
texts
|
texts.iter().any(|text| args.iter().any(|arg| arg.as_ref() == text.as_ref()))
|
||||||
.iter()
|
|
||||||
.any(|text| args.iter().any(|arg| arg.as_ref() == text.as_ref()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -237,14 +234,8 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(args, gen_args("ouch a.zip b.tar.gz c.tar"));
|
assert_eq!(args, gen_args("ouch a.zip b.tar.gz c.tar"));
|
||||||
assert!(flags.is_present("output_file"));
|
assert!(flags.is_present("output_file"));
|
||||||
assert_eq!(
|
assert_eq!(Some(&OsString::from("new_folder")), flags.arg("output_file"));
|
||||||
Some(&OsString::from("new_folder")),
|
assert_eq!(Some(OsString::from("new_folder")), flags.take_arg("output_file"));
|
||||||
flags.arg("output_file")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
Some(OsString::from("new_folder")),
|
|
||||||
flags.take_arg("output_file")
|
|
||||||
);
|
|
||||||
assert!(!flags.is_present("output_file"));
|
assert!(!flags.is_present("output_file"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -291,10 +282,7 @@ mod tests {
|
|||||||
// TODO: remove should_panic and use proper error handling inside of filter_args
|
// TODO: remove should_panic and use proper error handling inside of filter_args
|
||||||
#[should_panic]
|
#[should_panic]
|
||||||
fn test_flag_info_with_long_flag_conflict() {
|
fn test_flag_info_with_long_flag_conflict() {
|
||||||
let flags_info = [
|
let flags_info = [ArgFlag::long("verbose").short('a'), Flag::long("verbose").short('b')];
|
||||||
ArgFlag::long("verbose").short('a'),
|
|
||||||
Flag::long("verbose").short('b'),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Should panic here
|
// Should panic here
|
||||||
let result = filter_flags(vec![], &flags_info);
|
let result = filter_flags(vec![], &flags_info);
|
||||||
@ -305,10 +293,8 @@ mod tests {
|
|||||||
// TODO: remove should_panic and use proper error handling inside of filter_args
|
// TODO: remove should_panic and use proper error handling inside of filter_args
|
||||||
#[should_panic]
|
#[should_panic]
|
||||||
fn test_flag_info_with_short_flag_conflict() {
|
fn test_flag_info_with_short_flag_conflict() {
|
||||||
let flags_info = [
|
let flags_info =
|
||||||
ArgFlag::long("output_file").short('o'),
|
[ArgFlag::long("output_file").short('o'), Flag::long("verbose").short('o')];
|
||||||
Flag::long("verbose").short('o'),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Should panic here
|
// Should panic here
|
||||||
filter_flags(vec![], &flags_info).unwrap_err();
|
filter_flags(vec![], &flags_info).unwrap_err();
|
||||||
|
13
rustfmt.toml
Normal file
13
rustfmt.toml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Normal features
|
||||||
|
max_width = 100
|
||||||
|
imports_granularity = "Crate"
|
||||||
|
match_block_trailing_comma = true
|
||||||
|
overflow_delimited_expr = true
|
||||||
|
reorder_impl_items = true
|
||||||
|
use_field_init_shorthand = true
|
||||||
|
newline_style = "Unix"
|
||||||
|
edition = "2018"
|
||||||
|
reorder_imports = true
|
||||||
|
reorder_modules = true
|
||||||
|
use_try_shorthand = true
|
||||||
|
use_small_heuristics = "Max"
|
54
src/cli.rs
54
src/cli.rs
@ -5,9 +5,8 @@ use std::{
|
|||||||
vec::Vec,
|
vec::Vec,
|
||||||
};
|
};
|
||||||
|
|
||||||
use strsim::normalized_damerau_levenshtein;
|
|
||||||
use oof::{arg_flag, flag};
|
use oof::{arg_flag, flag};
|
||||||
|
use strsim::normalized_damerau_levenshtein;
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Debug)]
|
#[derive(PartialEq, Eq, Debug)]
|
||||||
pub enum Command {
|
pub enum Command {
|
||||||
@ -36,11 +35,10 @@ pub struct ParsedArgs {
|
|||||||
pub flags: oof::Flags,
|
pub flags: oof::Flags,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// check_for_typo checks if the first argument is
|
||||||
/// check_for_typo checks if the first argument is
|
/// a typo for the compress subcommand.
|
||||||
/// a typo for the compress subcommand.
|
|
||||||
/// Returns true if the arg is probably a typo or false otherwise.
|
/// Returns true if the arg is probably a typo or false otherwise.
|
||||||
fn is_typo<'a, P>(path: P) -> bool
|
fn is_typo<'a, P>(path: P) -> bool
|
||||||
where
|
where
|
||||||
P: AsRef<Path> + 'a,
|
P: AsRef<Path> + 'a,
|
||||||
{
|
{
|
||||||
@ -66,12 +64,10 @@ where
|
|||||||
} else {
|
} else {
|
||||||
Err(crate::Error::IoError(io_err))
|
Err(crate::Error::IoError(io_err))
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fn canonicalize_files<'a, P>(files: Vec<P>) -> crate::Result<Vec<PathBuf>>
|
fn canonicalize_files<'a, P>(files: Vec<P>) -> crate::Result<Vec<PathBuf>>
|
||||||
where
|
where
|
||||||
P: AsRef<Path> + 'a,
|
P: AsRef<Path> + 'a,
|
||||||
@ -81,22 +77,14 @@ where
|
|||||||
|
|
||||||
pub fn parse_args_from(mut args: Vec<OsString>) -> crate::Result<ParsedArgs> {
|
pub fn parse_args_from(mut args: Vec<OsString>) -> crate::Result<ParsedArgs> {
|
||||||
if oof::matches_any_arg(&args, &["--help", "-h"]) || args.is_empty() {
|
if oof::matches_any_arg(&args, &["--help", "-h"]) || args.is_empty() {
|
||||||
return Ok(ParsedArgs {
|
return Ok(ParsedArgs { command: Command::ShowHelp, flags: oof::Flags::default() });
|
||||||
command: Command::ShowHelp,
|
|
||||||
flags: oof::Flags::default(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if oof::matches_any_arg(&args, &["--version"]) {
|
if oof::matches_any_arg(&args, &["--version"]) {
|
||||||
return Ok(ParsedArgs {
|
return Ok(ParsedArgs { command: Command::ShowVersion, flags: oof::Flags::default() });
|
||||||
command: Command::ShowVersion,
|
|
||||||
flags: oof::Flags::default(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let subcommands = &[
|
let subcommands = &["c", "compress"];
|
||||||
"c", "compress"
|
|
||||||
];
|
|
||||||
|
|
||||||
let mut flags_info = vec![flag!('y', "yes"), flag!('n', "no")];
|
let mut flags_info = vec![flag!('y', "yes"), flag!('n', "no")];
|
||||||
|
|
||||||
@ -115,41 +103,33 @@ pub fn parse_args_from(mut args: Vec<OsString>) -> crate::Result<ParsedArgs> {
|
|||||||
|
|
||||||
let files = canonicalize_files(files)?;
|
let files = canonicalize_files(files)?;
|
||||||
|
|
||||||
let command = Command::Compress {
|
let command = Command::Compress { files, compressed_output_path };
|
||||||
files,
|
|
||||||
compressed_output_path,
|
|
||||||
};
|
|
||||||
ParsedArgs { command, flags }
|
ParsedArgs { command, flags }
|
||||||
}
|
},
|
||||||
// Defaults to decompression when there is no subcommand
|
// Defaults to decompression when there is no subcommand
|
||||||
None => {
|
None => {
|
||||||
flags_info.push(arg_flag!('o', "output"));
|
flags_info.push(arg_flag!('o', "output"));
|
||||||
{
|
|
||||||
let first_arg = args.first().unwrap();
|
if let Some(first_arg) = args.first() {
|
||||||
if is_typo(first_arg) {
|
if is_typo(first_arg) {
|
||||||
return Err(crate::Error::CompressionTypo);
|
return Err(crate::Error::CompressionTypo);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
todo!("Complain that no decompression arguments were given.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Parse flags
|
// Parse flags
|
||||||
let (args, mut flags) = oof::filter_flags(args, &flags_info)?;
|
let (args, mut flags) = oof::filter_flags(args, &flags_info)?;
|
||||||
|
|
||||||
let files = args
|
let files = args.into_iter().map(canonicalize).collect::<Result<Vec<_>, _>>()?;
|
||||||
.into_iter()
|
|
||||||
.map(canonicalize)
|
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
|
||||||
|
|
||||||
let output_folder = flags.take_arg("output").map(PathBuf::from);
|
let output_folder = flags.take_arg("output").map(PathBuf::from);
|
||||||
|
|
||||||
// TODO: ensure all files are decompressible
|
// TODO: ensure all files are decompressible
|
||||||
|
|
||||||
let command = Command::Decompress {
|
let command = Command::Decompress { files, output_folder };
|
||||||
files,
|
|
||||||
output_folder,
|
|
||||||
};
|
|
||||||
ParsedArgs { command, flags }
|
ParsedArgs { command, flags }
|
||||||
}
|
},
|
||||||
_ => unreachable!("You should match each subcommand passed."),
|
_ => unreachable!("You should match each subcommand passed."),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -24,20 +24,16 @@ use crate::{
|
|||||||
|
|
||||||
pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> {
|
pub fn run(command: Command, flags: &oof::Flags) -> crate::Result<()> {
|
||||||
match command {
|
match command {
|
||||||
Command::Compress {
|
Command::Compress { files, compressed_output_path } => {
|
||||||
files,
|
compress_files(files, &compressed_output_path, flags)?
|
||||||
compressed_output_path,
|
},
|
||||||
} => compress_files(files, &compressed_output_path, flags)?,
|
Command::Decompress { files, output_folder } => {
|
||||||
Command::Decompress {
|
|
||||||
files,
|
|
||||||
output_folder,
|
|
||||||
} => {
|
|
||||||
// From Option<PathBuf> to Option<&Path>
|
// From Option<PathBuf> to Option<&Path>
|
||||||
let output_folder = output_folder.as_ref().map(|path| Path::new(path));
|
let output_folder = output_folder.as_ref().map(|path| Path::new(path));
|
||||||
for file in files.iter() {
|
for file in files.iter() {
|
||||||
decompress_file(file, output_folder, flags)?;
|
decompress_file(file, output_folder, flags)?;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
Command::ShowHelp => crate::help_command(),
|
Command::ShowHelp => crate::help_command(),
|
||||||
Command::ShowVersion => crate::version_command(),
|
Command::ShowVersion => crate::version_command(),
|
||||||
}
|
}
|
||||||
@ -53,7 +49,7 @@ fn get_compressor(file: &File) -> crate::Result<(Option<BoxedCompressor>, BoxedC
|
|||||||
None => {
|
None => {
|
||||||
// This is reached when the output file given does not have an extension or has an unsupported one
|
// This is reached when the output file given does not have an extension or has an unsupported one
|
||||||
return Err(crate::Error::MissingExtensionError(file.path.to_path_buf()));
|
return Err(crate::Error::MissingExtensionError(file.path.to_path_buf()));
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Supported first compressors:
|
// Supported first compressors:
|
||||||
@ -93,7 +89,7 @@ fn get_decompressor(file: &File) -> crate::Result<(Option<BoxedDecompressor>, Bo
|
|||||||
colors::reset()
|
colors::reset()
|
||||||
);
|
);
|
||||||
return Err(crate::Error::InvalidInput);
|
return Err(crate::Error::InvalidInput);
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let second_decompressor: Box<dyn Decompressor> = match extension.second_ext {
|
let second_decompressor: Box<dyn Decompressor> = match extension.second_ext {
|
||||||
@ -126,10 +122,7 @@ fn decompress_file_in_memory(
|
|||||||
) -> crate::Result<()> {
|
) -> crate::Result<()> {
|
||||||
let output_file_path = utils::get_destination_path(&output_file);
|
let output_file_path = utils::get_destination_path(&output_file);
|
||||||
|
|
||||||
let file_name = file_path
|
let file_name = file_path.file_stem().map(Path::new).unwrap_or(output_file_path);
|
||||||
.file_stem()
|
|
||||||
.map(Path::new)
|
|
||||||
.unwrap_or(output_file_path);
|
|
||||||
|
|
||||||
if "." == file_name.as_os_str() {
|
if "." == file_name.as_os_str() {
|
||||||
// I believe this is only possible when the supplied input has a name
|
// I believe this is only possible when the supplied input has a name
|
||||||
@ -156,14 +149,10 @@ fn decompress_file_in_memory(
|
|||||||
let mut f = fs::File::create(output_file_path.join(file_name))?;
|
let mut f = fs::File::create(output_file_path.join(file_name))?;
|
||||||
f.write_all(&bytes)?;
|
f.write_all(&bytes)?;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let file = File {
|
let file = File { path: file_name, contents_in_memory: Some(bytes), extension };
|
||||||
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 {
|
if let DecompressionResult::FileInMemory(_) = decompression_result {
|
||||||
@ -196,11 +185,11 @@ fn compress_files(
|
|||||||
output.contents_in_memory = Some(bytes);
|
output.contents_in_memory = Some(bytes);
|
||||||
entry = Entry::InMemory(output);
|
entry = Entry::InMemory(output);
|
||||||
second_compressor.compress(entry)?
|
second_compressor.compress(entry)?
|
||||||
}
|
},
|
||||||
None => {
|
None => {
|
||||||
let entry = Entry::Files(files);
|
let entry = Entry::Files(files);
|
||||||
second_compressor.compress(entry)?
|
second_compressor.compress(entry)?
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
@ -224,9 +213,7 @@ fn decompress_file(
|
|||||||
let file = File::from(file_path)?;
|
let file = File::from(file_path)?;
|
||||||
// The file must have a supported decompressible format
|
// The file must have a supported decompressible format
|
||||||
if file.extension == None {
|
if file.extension == None {
|
||||||
return Err(crate::Error::MissingExtensionError(PathBuf::from(
|
return Err(crate::Error::MissingExtensionError(PathBuf::from(file_path)));
|
||||||
file_path,
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let output = match output {
|
let output = match output {
|
||||||
@ -251,7 +238,7 @@ fn decompress_file(
|
|||||||
extension,
|
extension,
|
||||||
flags,
|
flags,
|
||||||
)?;
|
)?;
|
||||||
}
|
},
|
||||||
DecompressionResult::FilesUnpacked(_files) => {
|
DecompressionResult::FilesUnpacked(_files) => {
|
||||||
// If the file's last extension was an archival method,
|
// If the file's last extension was an archival method,
|
||||||
// such as .tar, .zip or (to-do) .rar, then we won't look for
|
// such as .tar, .zip or (to-do) .rar, then we won't look for
|
||||||
@ -260,7 +247,7 @@ fn decompress_file(
|
|||||||
// to worry about, at least at the moment.
|
// to worry about, at least at the moment.
|
||||||
|
|
||||||
// TODO: use the `files` variable for something
|
// TODO: use the `files` variable for something
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -34,7 +34,7 @@ impl BzipCompressor {
|
|||||||
Some(bytes) => bytes,
|
Some(bytes) => bytes,
|
||||||
None => {
|
None => {
|
||||||
return Err(crate::Error::InternalError);
|
return Err(crate::Error::InternalError);
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
Self::compress_bytes(&*bytes)
|
Self::compress_bytes(&*bytes)
|
||||||
|
@ -38,7 +38,7 @@ impl GzipCompressor {
|
|||||||
Some(bytes) => bytes,
|
Some(bytes) => bytes,
|
||||||
None => {
|
None => {
|
||||||
unreachable!();
|
unreachable!();
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
Self::compress_bytes(file_contents)
|
Self::compress_bytes(file_contents)
|
||||||
|
@ -38,7 +38,7 @@ impl LzmaCompressor {
|
|||||||
Some(bytes) => bytes,
|
Some(bytes) => bytes,
|
||||||
None => {
|
None => {
|
||||||
unreachable!();
|
unreachable!();
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
Self::compress_bytes(file_contents)
|
Self::compress_bytes(file_contents)
|
||||||
|
@ -37,7 +37,7 @@ impl ZipCompressor {
|
|||||||
// TODO: error description, although this block should not be
|
// TODO: error description, although this block should not be
|
||||||
// reachable
|
// reachable
|
||||||
return Err(crate::Error::InvalidInput);
|
return Err(crate::Error::InvalidInput);
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
writer.write_all(&*input_bytes)?;
|
writer.write_all(&*input_bytes)?;
|
||||||
|
@ -4,7 +4,6 @@ use std::{
|
|||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
use tar::{self, Archive};
|
use tar::{self, Archive};
|
||||||
use utils::colors;
|
use utils::colors;
|
||||||
|
|
||||||
@ -30,7 +29,7 @@ impl TarDecompressor {
|
|||||||
None => {
|
None => {
|
||||||
let file = fs::File::open(&from.path)?;
|
let file = fs::File::open(&from.path)?;
|
||||||
tar::Archive::new(Box::new(file))
|
tar::Archive::new(Box::new(file))
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
for file in archive.entries()? {
|
for file in archive.entries()? {
|
||||||
|
@ -3,7 +3,6 @@ use std::{
|
|||||||
path::Path,
|
path::Path,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
use utils::colors;
|
use utils::colors;
|
||||||
|
|
||||||
use super::decompressor::{DecompressionResult, Decompressor};
|
use super::decompressor::{DecompressionResult, Decompressor};
|
||||||
|
@ -66,7 +66,7 @@ impl ZipDecompressor {
|
|||||||
_is_dir @ true => {
|
_is_dir @ true => {
|
||||||
println!("File {} extracted to \"{}\"", idx, file_path.display());
|
println!("File {} extracted to \"{}\"", idx, file_path.display());
|
||||||
fs::create_dir_all(&file_path)?;
|
fs::create_dir_all(&file_path)?;
|
||||||
}
|
},
|
||||||
_is_file @ false => {
|
_is_file @ false => {
|
||||||
if let Some(path) = file_path.parent() {
|
if let Some(path) = file_path.parent() {
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
@ -83,7 +83,7 @@ impl ZipDecompressor {
|
|||||||
|
|
||||||
let mut output_file = fs::File::create(&file_path)?;
|
let mut output_file = fs::File::create(&file_path)?;
|
||||||
io::copy(&mut file, &mut output_file)?;
|
io::copy(&mut file, &mut output_file)?;
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
@ -104,14 +104,14 @@ impl ZipDecompressor {
|
|||||||
// Decompressing a .zip archive loaded up in memory
|
// Decompressing a .zip archive loaded up in memory
|
||||||
let mut archive = zip::ZipArchive::new(Cursor::new(bytes))?;
|
let mut archive = zip::ZipArchive::new(Cursor::new(bytes))?;
|
||||||
Ok(Self::zip_decompress(&mut archive, into, flags)?)
|
Ok(Self::zip_decompress(&mut archive, into, flags)?)
|
||||||
}
|
},
|
||||||
None => {
|
None => {
|
||||||
// Decompressing a .zip archive from the file system
|
// Decompressing a .zip archive from the file system
|
||||||
let file = fs::File::open(&from.path)?;
|
let file = fs::File::open(&from.path)?;
|
||||||
let mut archive = zip::ZipArchive::new(file)?;
|
let mut archive = zip::ZipArchive::new(file)?;
|
||||||
|
|
||||||
Ok(Self::zip_decompress(&mut archive, into, flags)?)
|
Ok(Self::zip_decompress(&mut archive, into, flags)?)
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,10 +12,7 @@ pub struct Error;
|
|||||||
|
|
||||||
impl<'a> Confirmation<'a> {
|
impl<'a> Confirmation<'a> {
|
||||||
pub fn new(prompt: &'a str, pattern: Option<&'a str>) -> Self {
|
pub fn new(prompt: &'a str, pattern: Option<&'a str>) -> Self {
|
||||||
Self {
|
Self { prompt, placeholder: pattern }
|
||||||
prompt,
|
|
||||||
placeholder: pattern,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ask(&self, substitute: Option<&'a str>) -> crate::Result<bool> {
|
pub fn ask(&self, substitute: Option<&'a str>) -> crate::Result<bool> {
|
||||||
@ -26,7 +23,14 @@ impl<'a> Confirmation<'a> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
print!("{} [{}Y{}/{}n{}] ", message, colors::green(), colors::reset(), colors::red(), colors::reset());
|
print!(
|
||||||
|
"{} [{}Y{}/{}n{}] ",
|
||||||
|
message,
|
||||||
|
colors::green(),
|
||||||
|
colors::reset(),
|
||||||
|
colors::red(),
|
||||||
|
colors::reset()
|
||||||
|
);
|
||||||
io::stdout().flush()?;
|
io::stdout().flush()?;
|
||||||
|
|
||||||
let mut answer = String::new();
|
let mut answer = String::new();
|
||||||
@ -40,7 +44,7 @@ impl<'a> Confirmation<'a> {
|
|||||||
match trimmed_answer.to_ascii_lowercase().as_ref() {
|
match trimmed_answer.to_ascii_lowercase().as_ref() {
|
||||||
"y" | "yes" => return Ok(true),
|
"y" | "yes" => return Ok(true),
|
||||||
"n" | "no" => return Ok(false),
|
"n" | "no" => return Ok(false),
|
||||||
_ => {}
|
_ => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
32
src/error.rs
32
src/error.rs
@ -1,4 +1,5 @@
|
|||||||
use std::{fmt, path::PathBuf};
|
use std::{fmt, path::PathBuf};
|
||||||
|
|
||||||
use crate::utils::colors;
|
use crate::utils::colors;
|
||||||
|
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
@ -36,18 +37,18 @@ impl fmt::Display for Error {
|
|||||||
write!(f, "{}[ERROR]{} ", colors::red(), colors::reset())?;
|
write!(f, "{}[ERROR]{} ", colors::red(), colors::reset())?;
|
||||||
// TODO: show MIME type of the unsupported file
|
// TODO: show MIME type of the unsupported file
|
||||||
write!(f, "cannot compress to {:?}, likely because it has an unsupported (or missing) extension.", filename)
|
write!(f, "cannot compress to {:?}, likely because it has an unsupported (or missing) extension.", filename)
|
||||||
}
|
},
|
||||||
Error::WalkdirError => {
|
Error::WalkdirError => {
|
||||||
// Already printed in the From block
|
// Already printed in the From block
|
||||||
write!(f, "")
|
write!(f, "")
|
||||||
}
|
},
|
||||||
Error::FileNotFound(file) => {
|
Error::FileNotFound(file) => {
|
||||||
write!(f, "{}[ERROR]{} ", colors::red(), colors::reset())?;
|
write!(f, "{}[ERROR]{} ", colors::red(), colors::reset())?;
|
||||||
if file == &PathBuf::from("") {
|
if file == &PathBuf::from("") {
|
||||||
return write!(f, "file not found!");
|
return write!(f, "file not found!");
|
||||||
}
|
}
|
||||||
write!(f, "file {:?} not found!", file)
|
write!(f, "file {:?} not found!", file)
|
||||||
}
|
},
|
||||||
Error::CompressingRootFolder => {
|
Error::CompressingRootFolder => {
|
||||||
write!(f, "{}[ERROR]{} ", colors::red(), colors::reset())?;
|
write!(f, "{}[ERROR]{} ", colors::red(), colors::reset())?;
|
||||||
let spacing = " ";
|
let spacing = " ";
|
||||||
@ -64,28 +65,27 @@ impl fmt::Display for Error {
|
|||||||
colors::green(),
|
colors::green(),
|
||||||
colors::reset()
|
colors::reset()
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
Error::MissingArgumentsForCompression => {
|
Error::MissingArgumentsForCompression => {
|
||||||
write!(f, "{}[ERROR]{} ", colors::red(), colors::reset())?;
|
write!(f, "{}[ERROR]{} ", colors::red(), colors::reset())?;
|
||||||
let spacing = " ";
|
let spacing = " ";
|
||||||
writeln!(f,"The compress subcommands demands at least 2 arguments, an input file and an output file.")?;
|
writeln!(f,"The compress subcommands demands at least 2 arguments, an input file and an output file.")?;
|
||||||
writeln!(f,"{}Example: `ouch compress img.jpeg img.zip", spacing)?;
|
writeln!(f, "{}Example: `ouch compress img.jpeg img.zip`", spacing)?;
|
||||||
write!(f,"{}For more information, run `ouch --help`", spacing)
|
write!(f, "{}For more information, run `ouch --help`", spacing)
|
||||||
}
|
},
|
||||||
Error::InternalError => {
|
Error::InternalError => {
|
||||||
write!(f, "{}[ERROR]{} ", colors::red(), colors::reset())?;
|
write!(f, "{}[ERROR]{} ", colors::red(), colors::reset())?;
|
||||||
write!(f, "You've reached an internal error! This really should not have happened.\nPlease file an issue at {}https://github.com/vrmiguel/ouch{}", colors::green(), colors::reset())
|
write!(f, "You've reached an internal error! This really should not have happened.\nPlease file an issue at {}https://github.com/vrmiguel/ouch{}", colors::green(), colors::reset())
|
||||||
}
|
},
|
||||||
Error::IoError(io_err) => {
|
Error::IoError(io_err) => {
|
||||||
write!(f, "{}[ERROR]{} {}", colors::red(), colors::reset(), io_err)
|
write!(f, "{}[ERROR]{} {}", colors::red(), colors::reset(), io_err)
|
||||||
}
|
},
|
||||||
Error::CompressionTypo =>{
|
Error::CompressionTypo => {
|
||||||
write!(f, "Did you mean {}ouch compress{}?", colors::magenta(), colors::reset())
|
write!(f, "Did you mean {}ouch compress{}?", colors::magenta(), colors::reset())
|
||||||
}
|
},
|
||||||
_err => {
|
_err => {
|
||||||
// TODO
|
todo!();
|
||||||
write!(f, "")
|
},
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -96,9 +96,7 @@ impl From<std::io::Error> for Error {
|
|||||||
std::io::ErrorKind::NotFound => panic!("{}", err),
|
std::io::ErrorKind::NotFound => panic!("{}", err),
|
||||||
std::io::ErrorKind::PermissionDenied => Self::PermissionDenied,
|
std::io::ErrorKind::PermissionDenied => Self::PermissionDenied,
|
||||||
std::io::ErrorKind::AlreadyExists => Self::AlreadyExists,
|
std::io::ErrorKind::AlreadyExists => Self::AlreadyExists,
|
||||||
_other => {
|
_other => Self::IoError(err),
|
||||||
Self::IoError(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,10 +34,7 @@ pub fn get_extension_from_filename(file_name: &OsStr) -> Option<(&OsStr, &OsStr)
|
|||||||
|
|
||||||
impl From<CompressionFormat> for Extension {
|
impl From<CompressionFormat> for Extension {
|
||||||
fn from(second_ext: CompressionFormat) -> Self {
|
fn from(second_ext: CompressionFormat) -> Self {
|
||||||
Self {
|
Self { first_ext: None, second_ext }
|
||||||
first_ext: None,
|
|
||||||
second_ext,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,29 +54,22 @@ impl Extension {
|
|||||||
(os_str, snd) if os_str.is_empty() => (None, snd),
|
(os_str, snd) if os_str.is_empty() => (None, snd),
|
||||||
(fst, snd) => (Some(fst), snd),
|
(fst, snd) => (Some(fst), snd),
|
||||||
},
|
},
|
||||||
None => {
|
None => return Err(crate::Error::MissingExtensionError(PathBuf::from(file_name))),
|
||||||
return Err(crate::Error::MissingExtensionError(PathBuf::from(
|
|
||||||
file_name,
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let (first_ext, second_ext) = match (first_ext, second_ext) {
|
let (first_ext, second_ext) = match (first_ext, second_ext) {
|
||||||
(None, snd) => {
|
(None, snd) => {
|
||||||
let ext = compression_format_from(snd)?;
|
let ext = compression_format_from(snd)?;
|
||||||
(None, ext)
|
(None, ext)
|
||||||
}
|
},
|
||||||
(Some(fst), snd) => {
|
(Some(fst), snd) => {
|
||||||
let snd = compression_format_from(snd)?;
|
let snd = compression_format_from(snd)?;
|
||||||
let fst = compression_format_from(fst).ok();
|
let fst = compression_format_from(fst).ok();
|
||||||
(fst, snd)
|
(fst, snd)
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self { first_ext, second_ext })
|
||||||
first_ext,
|
|
||||||
second_ext,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,7 +109,7 @@ impl TryFrom<&PathBuf> for CompressionFormat {
|
|||||||
Some(ext) => ext,
|
Some(ext) => ext,
|
||||||
None => {
|
None => {
|
||||||
return Err(crate::Error::MissingExtensionError(PathBuf::new()));
|
return Err(crate::Error::MissingExtensionError(PathBuf::new()));
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
extension_from_os_str(ext)
|
extension_from_os_str(ext)
|
||||||
}
|
}
|
||||||
@ -141,16 +131,12 @@ impl TryFrom<&str> for CompressionFormat {
|
|||||||
|
|
||||||
impl Display for CompressionFormat {
|
impl Display for CompressionFormat {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(
|
write!(f, "{}", match self {
|
||||||
f,
|
Gzip => ".gz",
|
||||||
"{}",
|
Bzip => ".bz",
|
||||||
match self {
|
Lzma => ".lz",
|
||||||
Gzip => ".gz",
|
Tar => ".tar",
|
||||||
Bzip => ".bz",
|
Zip => ".zip",
|
||||||
Lzma => ".lz",
|
})
|
||||||
Tar => ".tar",
|
|
||||||
Zip => ".zip",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,10 +21,6 @@ impl<'a> File<'a> {
|
|||||||
pub fn from(path: &'a Path) -> crate::Result<Self> {
|
pub fn from(path: &'a Path) -> crate::Result<Self> {
|
||||||
let extension = Extension::from(path.as_ref()).ok();
|
let extension = Extension::from(path.as_ref()).ok();
|
||||||
|
|
||||||
Ok(File {
|
Ok(File { path, contents_in_memory: None, extension })
|
||||||
path,
|
|
||||||
contents_in_memory: None,
|
|
||||||
extension,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,6 @@ mod error;
|
|||||||
mod extension;
|
mod extension;
|
||||||
mod file;
|
mod file;
|
||||||
mod utils;
|
mod utils;
|
||||||
mod test;
|
|
||||||
|
|
||||||
pub use error::{Error, Result};
|
pub use error::{Error, Result};
|
||||||
|
|
||||||
@ -73,10 +72,5 @@ Visit https://github.com/vrmiguel/ouch for more usage examples.",
|
|||||||
#[inline]
|
#[inline]
|
||||||
fn version_command() {
|
fn version_command() {
|
||||||
use utils::colors::*;
|
use utils::colors::*;
|
||||||
println!(
|
println!("{green}ouch{reset} {}", crate::VERSION, green = green(), reset = reset(),);
|
||||||
"{green}ouch{reset} {}",
|
|
||||||
crate::VERSION,
|
|
||||||
green = green(),
|
|
||||||
reset = reset(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
351
src/test.rs
351
src/test.rs
@ -1,351 +0,0 @@
|
|||||||
use std::{fs, path::Path};
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
// 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<Path> + 'a,
|
|
||||||
{
|
|
||||||
fs::write(path.as_ref(), &[2, 3, 4, 5, 6, 7, 8, 9, 10])?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
fn make_dummy_files<'a, P>(paths: &[P]) -> crate::Result<()>
|
|
||||||
where
|
|
||||||
P: AsRef<Path> + 'a,
|
|
||||||
{
|
|
||||||
let _ = paths
|
|
||||||
.iter()
|
|
||||||
.map(make_dummy_file)
|
|
||||||
.map(Result::unwrap)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod argparsing {
|
|
||||||
use super::make_dummy_files;
|
|
||||||
use crate::cli;
|
|
||||||
use crate::cli::Command;
|
|
||||||
use std::{ffi::OsString, fs, path::PathBuf};
|
|
||||||
|
|
||||||
fn gen_args(text: &str) -> Vec<OsString> {
|
|
||||||
let args = text.split_whitespace();
|
|
||||||
args.map(OsString::from).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! parse {
|
|
||||||
($input_text:expr) => {{
|
|
||||||
let args = gen_args($input_text);
|
|
||||||
cli::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() -> crate::Result<()> {
|
|
||||||
let files = vec!["a", "b", "c"];
|
|
||||||
make_dummy_files(&*files)?;
|
|
||||||
let files = files
|
|
||||||
.iter()
|
|
||||||
.map(fs::canonicalize)
|
|
||||||
.map(Result::unwrap)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let expected = Command::Compress {
|
|
||||||
files,
|
|
||||||
compressed_output_path: "d".into(),
|
|
||||||
};
|
|
||||||
assert_eq!(expected, parse!("compress a b c d").command);
|
|
||||||
|
|
||||||
fs::remove_file("a")?;
|
|
||||||
fs::remove_file("b")?;
|
|
||||||
fs::remove_file("c")?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_arg_parsing_decompress_subcommand() -> crate::Result<()> {
|
|
||||||
let files = vec!["d", "e", "f"];
|
|
||||||
make_dummy_files(&*files)?;
|
|
||||||
|
|
||||||
let files: Vec<_> = files.iter().map(PathBuf::from).collect();
|
|
||||||
|
|
||||||
let expected = Command::Decompress {
|
|
||||||
files: files
|
|
||||||
.iter()
|
|
||||||
.map(fs::canonicalize)
|
|
||||||
.map(Result::unwrap)
|
|
||||||
.collect(),
|
|
||||||
output_folder: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(expected, parse!("d e f").command);
|
|
||||||
|
|
||||||
let expected = Command::Decompress {
|
|
||||||
files: files.iter().map(fs::canonicalize).map(Result::unwrap).collect(),
|
|
||||||
output_folder: Some("folder".into()),
|
|
||||||
};
|
|
||||||
assert_eq!(expected, parse!("d e f --output folder").command);
|
|
||||||
assert_eq!(expected, parse!("d e --output folder f").command);
|
|
||||||
assert_eq!(expected, parse!("d --output folder e f").command);
|
|
||||||
assert_eq!(expected, parse!("--output folder d e f").command);
|
|
||||||
|
|
||||||
assert_eq!(expected, parse!("d e f -o folder").command);
|
|
||||||
assert_eq!(expected, parse!("d e -o folder f").command);
|
|
||||||
assert_eq!(expected, parse!("d -o folder e f").command);
|
|
||||||
assert_eq!(expected, parse!("-o folder d e f").command);
|
|
||||||
|
|
||||||
fs::remove_file("d")?;
|
|
||||||
fs::remove_file("e")?;
|
|
||||||
fs::remove_file("f")?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod byte_pretty_printing {
|
|
||||||
use crate::utils::Bytes;
|
|
||||||
#[test]
|
|
||||||
fn bytes() {
|
|
||||||
assert_eq!(&format!("{}", Bytes::new(234)), "234.00 B");
|
|
||||||
|
|
||||||
assert_eq!(&format!("{}", Bytes::new(999)), "999.00 B");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn kilobytes() {
|
|
||||||
assert_eq!(&format!("{}", Bytes::new(2234)), "2.23 kB");
|
|
||||||
|
|
||||||
assert_eq!(&format!("{}", Bytes::new(62500)), "62.50 kB");
|
|
||||||
|
|
||||||
assert_eq!(&format!("{}", Bytes::new(329990)), "329.99 kB");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn megabytes() {
|
|
||||||
assert_eq!(&format!("{}", Bytes::new(2750000)), "2.75 MB");
|
|
||||||
|
|
||||||
assert_eq!(&format!("{}", Bytes::new(55000000)), "55.00 MB");
|
|
||||||
|
|
||||||
assert_eq!(&format!("{}", Bytes::new(987654321)), "987.65 MB");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn gigabytes() {
|
|
||||||
assert_eq!(&format!("{}", Bytes::new(5280000000)), "5.28 GB");
|
|
||||||
|
|
||||||
assert_eq!(&format!("{}", Bytes::new(95200000000)), "95.20 GB");
|
|
||||||
|
|
||||||
assert_eq!(&format!("{}", Bytes::new(302000000000)), "302.00 GB");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// #[cfg(test)]
|
|
||||||
// mod cli {
|
|
||||||
// use super::*;
|
|
||||||
|
|
||||||
// #[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)?;
|
|
||||||
|
|
||||||
// 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
|
|
||||||
// // }),
|
|
||||||
// // }
|
|
||||||
// );
|
|
||||||
|
|
||||||
// fs::remove_file("file.zip")?;
|
|
||||||
|
|
||||||
// Ok(())
|
|
||||||
// }
|
|
||||||
|
|
||||||
// #[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)?;
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
|
|
||||||
// fs::remove_file("my-cool-file.zip")?;
|
|
||||||
// fs::remove_file("file.tar")?;
|
|
||||||
|
|
||||||
// Ok(())
|
|
||||||
// }
|
|
||||||
|
|
||||||
// #[test]
|
|
||||||
// fn compress_files() -> crate::Result<()> {
|
|
||||||
// make_dummy_file("file")?;
|
|
||||||
// make_dummy_file("file2.jpeg")?;
|
|
||||||
// make_dummy_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)?;
|
|
||||||
|
|
||||||
// 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))
|
|
||||||
// }),
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
|
|
||||||
// fs::remove_file("file")?;
|
|
||||||
// fs::remove_file("file2.jpeg")?;
|
|
||||||
// fs::remove_file("file3.ok")?;
|
|
||||||
|
|
||||||
// Ok(())
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// #[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);
|
|
||||||
|
|
||||||
// assert_eq!(
|
|
||||||
// res,
|
|
||||||
// Err(crate::Error::InputsMustHaveBeenDecompressible(
|
|
||||||
// "a_file".into()
|
|
||||||
// ))
|
|
||||||
// );
|
|
||||||
|
|
||||||
// Ok(())
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// #[cfg(test)]
|
|
||||||
// mod extension_extraction {
|
|
||||||
|
|
||||||
// #[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() {
|
|
||||||
// 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_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)
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
18
src/utils.rs
18
src/utils.rs
@ -79,7 +79,7 @@ pub fn get_destination_path<'a>(dest: &'a Option<File>) -> &'a Path {
|
|||||||
// Must be None according to the way command-line arg. parsing in Ouch works
|
// Must be None according to the way command-line arg. parsing in Ouch works
|
||||||
assert_eq!(output_file.extension, None);
|
assert_eq!(output_file.extension, None);
|
||||||
Path::new(&output_file.path)
|
Path::new(&output_file.path)
|
||||||
}
|
},
|
||||||
None => Path::new("."),
|
None => Path::new("."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -93,9 +93,7 @@ pub fn change_dir_and_return_parent(filename: &Path) -> crate::Result<PathBuf> {
|
|||||||
return Err(crate::Error::CompressingRootFolder);
|
return Err(crate::Error::CompressingRootFolder);
|
||||||
};
|
};
|
||||||
|
|
||||||
env::set_current_dir(parent)
|
env::set_current_dir(parent).ok().ok_or(crate::Error::CompressingRootFolder)?;
|
||||||
.ok()
|
|
||||||
.ok_or(crate::Error::CompressingRootFolder)?;
|
|
||||||
|
|
||||||
Ok(previous_location)
|
Ok(previous_location)
|
||||||
}
|
}
|
||||||
@ -107,11 +105,13 @@ pub fn permission_for_overwriting(
|
|||||||
) -> crate::Result<bool> {
|
) -> crate::Result<bool> {
|
||||||
match (flags.is_present("yes"), flags.is_present("no")) {
|
match (flags.is_present("yes"), flags.is_present("no")) {
|
||||||
(true, true) => {
|
(true, true) => {
|
||||||
unreachable!("This should've been cutted out in the ~/src/cli.rs filter flags function.")
|
unreachable!(
|
||||||
}
|
"This should've been cutted out in the ~/src/cli.rs filter flags function."
|
||||||
|
)
|
||||||
|
},
|
||||||
(true, _) => return Ok(true),
|
(true, _) => return Ok(true),
|
||||||
(_, true) => return Ok(false),
|
(_, true) => return Ok(false),
|
||||||
_ => {}
|
_ => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
let file_path_str = to_utf(path);
|
let file_path_str = to_utf(path);
|
||||||
@ -181,9 +181,7 @@ impl Bytes {
|
|||||||
const UNIT_PREFIXES: [&'static str; 6] = ["", "k", "M", "G", "T", "P"];
|
const UNIT_PREFIXES: [&'static str; 6] = ["", "k", "M", "G", "T", "P"];
|
||||||
|
|
||||||
pub fn new(bytes: u64) -> Self {
|
pub fn new(bytes: u64) -> Self {
|
||||||
Self {
|
Self { bytes: bytes as f64 }
|
||||||
bytes: bytes as f64,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
139
tests/compress_and_decompress.rs
Normal file
139
tests/compress_and_decompress.rs
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
use std::{
|
||||||
|
env, fs,
|
||||||
|
io::prelude::*,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use ouch::{cli::Command, commands::run};
|
||||||
|
use rand::{rngs::SmallRng, RngCore, SeedableRng};
|
||||||
|
use tempdir::TempDir;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// Tests each format that supports multiple files with random input.
|
||||||
|
/// TODO: test the remaining formats.
|
||||||
|
/// TODO2: Fix testing of .tar.zip and .zip.zip
|
||||||
|
fn test_each_format() {
|
||||||
|
let mut rng = SmallRng::from_entropy();
|
||||||
|
test_compression_and_decompression(&mut rng, "tar");
|
||||||
|
test_compression_and_decompression(&mut rng, "tar.gz");
|
||||||
|
test_compression_and_decompression(&mut rng, "tar.bz");
|
||||||
|
test_compression_and_decompression(&mut rng, "tar.bz2");
|
||||||
|
test_compression_and_decompression(&mut rng, "tar.xz");
|
||||||
|
test_compression_and_decompression(&mut rng, "tar.lz");
|
||||||
|
test_compression_and_decompression(&mut rng, "tar.lzma");
|
||||||
|
// test_compression_and_decompression(&mut rng, "tar.zip");
|
||||||
|
test_compression_and_decompression(&mut rng, "zip");
|
||||||
|
test_compression_and_decompression(&mut rng, "zip.gz");
|
||||||
|
test_compression_and_decompression(&mut rng, "zip.bz");
|
||||||
|
test_compression_and_decompression(&mut rng, "zip.bz2");
|
||||||
|
test_compression_and_decompression(&mut rng, "zip.xz");
|
||||||
|
test_compression_and_decompression(&mut rng, "zip.lz");
|
||||||
|
test_compression_and_decompression(&mut rng, "zip.lzma");
|
||||||
|
// test_compression_and_decompression(&mut rng, "zip.zip");
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileContent = Vec<u8>;
|
||||||
|
|
||||||
|
fn test_compression_and_decompression(rng: &mut impl RngCore, format: &str) {
|
||||||
|
// System temporary directory depends on the platform
|
||||||
|
// For linux it is /tmp
|
||||||
|
let system_tmp = env::temp_dir();
|
||||||
|
// Create a folder that will be deleted on drop
|
||||||
|
let testing_dir = String::from("ouch-testing-") + format;
|
||||||
|
let testing_dir = TempDir::new_in(system_tmp, &testing_dir).expect("Could not create tempdir");
|
||||||
|
let testing_dir = testing_dir.path();
|
||||||
|
|
||||||
|
// Quantity of compressed files vary from 1 to 10
|
||||||
|
let quantity_of_files = rng.next_u32() % 10 + 1;
|
||||||
|
|
||||||
|
let contents_of_files: Vec<FileContent> =
|
||||||
|
(0..quantity_of_files).map(|_| generate_random_file_content(rng)).collect();
|
||||||
|
|
||||||
|
let mut file_paths = create_files(&testing_dir, &contents_of_files);
|
||||||
|
let archive_path = compress_files(&testing_dir, &file_paths, &format);
|
||||||
|
let mut extracted_paths = extract_files(&archive_path);
|
||||||
|
|
||||||
|
// // If you want to visualize the compressed and extracted files before auto-destruction:
|
||||||
|
// std::thread::sleep(std::time::Duration::from_secs(40));
|
||||||
|
|
||||||
|
file_paths.sort();
|
||||||
|
extracted_paths.sort();
|
||||||
|
|
||||||
|
compare_paths(&file_paths, &extracted_paths);
|
||||||
|
compare_file_contents(&extracted_paths, &contents_of_files);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crate file contents from 1024 up to 8192 random bytes
|
||||||
|
fn generate_random_file_content(rng: &mut impl RngCore) -> FileContent {
|
||||||
|
let quantity = 1024 + rng.next_u32() % (8192 - 1024);
|
||||||
|
let mut vec = vec![0; quantity as usize];
|
||||||
|
rng.fill_bytes(&mut vec);
|
||||||
|
vec
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create files using the indexes as file names (eg. 0, 1, 2 and 3)
|
||||||
|
// Returns the paths
|
||||||
|
fn create_files(at: &Path, contents: &[FileContent]) -> Vec<PathBuf> {
|
||||||
|
contents
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, content)| {
|
||||||
|
let path = at.join(i.to_string());
|
||||||
|
let mut file = fs::File::create(&path).expect("Could not create dummy test file");
|
||||||
|
file.write_all(content).expect("Could not write to dummy test file");
|
||||||
|
path
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compress_files(at: &Path, paths_to_compress: &[PathBuf], format: &str) -> PathBuf {
|
||||||
|
let archive_path = String::from("archive.") + format;
|
||||||
|
let archive_path = at.join(archive_path);
|
||||||
|
|
||||||
|
let command = Command::Compress {
|
||||||
|
files: paths_to_compress.to_vec(),
|
||||||
|
compressed_output_path: archive_path.to_path_buf(),
|
||||||
|
};
|
||||||
|
run(command, &oof::Flags::default()).expect("Failed to compress test dummy files");
|
||||||
|
|
||||||
|
archive_path
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_files(archive_path: &Path) -> Vec<PathBuf> {
|
||||||
|
// We will extract in the same folder as the archive
|
||||||
|
// If the archive is at:
|
||||||
|
// /tmp/ouch-testing-tar.Rbq4DusBrtF8/archive.tar
|
||||||
|
// Then the extraction_output_folder will be:
|
||||||
|
// /tmp/ouch-testing-tar.Rbq4DusBrtF8/extraction_results/
|
||||||
|
let mut extraction_output_folder = archive_path.to_path_buf();
|
||||||
|
// Remove the name of the extracted archive
|
||||||
|
assert!(extraction_output_folder.pop());
|
||||||
|
// Add the suffix "results"
|
||||||
|
extraction_output_folder.push("extraction_results");
|
||||||
|
|
||||||
|
let command = Command::Decompress {
|
||||||
|
files: vec![archive_path.to_owned()],
|
||||||
|
output_folder: Some(extraction_output_folder.clone()),
|
||||||
|
};
|
||||||
|
run(command, &oof::Flags::default()).expect("Failed to extract");
|
||||||
|
|
||||||
|
fs::read_dir(extraction_output_folder)
|
||||||
|
.unwrap()
|
||||||
|
.map(Result::unwrap)
|
||||||
|
.map(|entry| entry.path())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compare_paths(original: &[PathBuf], extracted: &[PathBuf]) {
|
||||||
|
assert_eq!(original.len(), extracted.len());
|
||||||
|
for (original, extracted) in original.iter().zip(extracted) {
|
||||||
|
assert_eq!(original.file_name(), extracted.file_name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compare_file_contents(extracted: &[PathBuf], contents: &[FileContent]) {
|
||||||
|
for (extracted_path, expected_content) in extracted.iter().zip(contents) {
|
||||||
|
let read_content = fs::read(extracted_path).expect("Failed to read from file");
|
||||||
|
assert_eq!(&read_content, expected_content);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user