From a47f41a257c03df8fe7fdaf6c8f158696d2aa405 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Fri, 19 Mar 2021 11:40:17 -0300 Subject: [PATCH 01/67] typo: Decompressable -> Decompressible --- README.md | 6 +++--- src/cli.rs | 8 ++++---- src/error.rs | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c1c50bfc4..a2c47da22 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ info: attempting to decompress input files into single_folder info: done! ``` -When no output file is supplied, `ouch` infers that it must decompress all of its input files. This will error if any of the input files are not decompressable. +When no output file is supplied, `ouch` infers that it must decompress all of its input files. This will error if any of the input files are not decompressible. #### Decompressing a bunch of files into a folder @@ -42,7 +42,7 @@ info: attempting to decompress input files into single_folder info: done! ``` -When the output file is not a compressed file, `ouch` will check if all input files are decompressable and infer that it must decompress them into the output file. +When the output file is not a compressed file, `ouch` will check if all input files are decompressible and infer that it must decompress them into the output file. #### Compressing files @@ -58,7 +58,7 @@ info: done! ```bash $ ouch -i some-file -o some-folder -error: file 'some-file' is not decompressable. +error: file 'some-file' is not decompressible. ``` `ouch` might (TODO!) be able to sniff a file's compression format if it isn't supplied in the future, but that is not currently implemented. diff --git a/src/cli.rs b/src/cli.rs index e6f9e9d45..cd9ca2bb2 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -63,7 +63,7 @@ impl TryFrom> for Command { fn try_from(matches: clap::ArgMatches<'static>) -> error::OuchResult { // Possibilities: - // * Case 1: output not supplied, therefore try to infer output by checking if all input files are decompressable + // * 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"); @@ -98,14 +98,14 @@ impl TryFrom> for Command { } else { - // Checking if input files are decompressable + // Checking if input files are decompressible let input_files = input_files .map(|filename| (filename, CompressionExtension::try_from(filename))); for file in input_files.clone() { if let (file, Err(_)) = file { - eprintln!("{}: file '{}' is not decompressable.", "error".red(), file); - return Err(error::Error::InputsMustHaveBeenDecompressable(file.into())); + // eprintln!("{}: file '{}' is not decompressible.", "error".red(), file); + return Err(error::Error::InputsMustHaveBeenDecompressible(file.into())); } } diff --git a/src/error.rs b/src/error.rs index 9cf1b4766..ee9e73b3b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -8,7 +8,7 @@ pub enum Error { MissingExtensionError(String), InvalidUnicode, InvalidInput, - InputsMustHaveBeenDecompressable(String) + InputsMustHaveBeenDecompressible(String) } // This should be placed somewhere else @@ -26,8 +26,8 @@ impl fmt::Display for Error { Error::MissingExtensionError(filename) => { write!(f, "cannot compress to \'{}\', likely because it has an unsupported (or missing) extension.", filename) } - Error::InputsMustHaveBeenDecompressable(file) => { - write!(f, "file '{}' is not decompressable", file.red()) + Error::InputsMustHaveBeenDecompressible(file) => { + write!(f, "file '{}' is not decompressible", file.red()) } _ => { // TODO From 39abfdffde3dc8939a6e9a69d8232875022e9439 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Fri, 19 Mar 2021 11:41:38 -0300 Subject: [PATCH 02/67] typo: Compressable -> Compressible --- src/cli.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index cd9ca2bb2..ee50bb029 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -81,8 +81,8 @@ impl TryFrom> for Command { let output_file_extension = CompressionExtension::try_from(output_file); - let output_is_compressable = output_file_extension.is_ok(); - if output_is_compressable { + let output_is_compressible = output_file_extension.is_ok(); + if output_is_compressible { println!("{}: trying to compress input files into '{}'", "info".yellow(), output_file); let input_files = input_files.map(PathBuf::from).collect(); From 65bc13e8fa6f6c7df475ea9dfcf2db63c4fbe1a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Fri, 19 Mar 2021 12:40:49 -0300 Subject: [PATCH 03/67] tests: Start adding test for the command-line interface --- src/cli.rs | 162 +++++++++++++--------------------------------- src/extensions.rs | 2 +- src/file.rs | 2 +- src/main.rs | 1 + src/test.rs | 64 ++++++++++++++++++ 5 files changed, 113 insertions(+), 118 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index ee50bb029..75daad791 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,13 +1,13 @@ use std::{convert::TryFrom, ffi::OsStr, path::PathBuf, vec::Vec}; -use clap::{Arg}; +use clap::{Arg, Values}; use colored::Colorize; use crate::error; use crate::extensions::CompressionExtension; use crate::file::File; -#[derive(Debug)] +#[derive(PartialEq, Eq, Debug)] pub enum CommandType { Compression( // Files to be compressed @@ -19,13 +19,13 @@ pub enum CommandType { ), } -#[derive(Debug)] +#[derive(PartialEq, Eq, Debug)] pub struct Command { pub command_type: CommandType, pub output: Option, } -pub fn get_matches() -> clap::ArgMatches<'static> { +pub fn clap_app<'a, 'b>() -> clap::App<'a, 'b> { clap::App::new("ouch") .version("0.1.0") .about("ouch is a unified compression & decompression utility") @@ -53,7 +53,10 @@ pub fn get_matches() -> clap::ArgMatches<'static> { .help("Output file (TODO description)") .takes_value(true), ) - .get_matches() +} + +pub fn get_matches() -> clap::ArgMatches<'static> { + clap_app().get_matches() } // holy spaghetti code @@ -62,24 +65,42 @@ impl TryFrom> for Command { type Error = error::Error; fn try_from(matches: clap::ArgMatches<'static>) -> error::OuchResult { + + let process_decompressible_input = |input_files: Values| { + let input_files = input_files + .map(|filename| (filename, CompressionExtension::try_from(filename))); + + for file in input_files.clone() { + if let (file, Err(_)) = file { + // eprintln!("{}: file '{}' is not decompressible.", "error".red(), file); + return Err(error::Error::InputsMustHaveBeenDecompressible(file.into())); + } + } + + + Ok(input_files + .map(|(filename, extension)| + (PathBuf::from(filename), extension.unwrap()) + ) + .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 input_files = matches - .values_of("input") - .unwrap(); // Safe to unwrap since input is an obligatory argument - // .map(PathBuf::from) - // .collect(); - - let output_file_extension = CompressionExtension::try_from(output_file); let output_is_compressible = output_file_extension.is_ok(); if output_is_compressible { @@ -99,22 +120,8 @@ impl TryFrom> for Command { } else { // Checking if input files are decompressible - let input_files = input_files - .map(|filename| (filename, CompressionExtension::try_from(filename))); - for file in input_files.clone() { - if let (file, Err(_)) = file { - // eprintln!("{}: file '{}' is not decompressible.", "error".red(), file); - return Err(error::Error::InputsMustHaveBeenDecompressible(file.into())); - } - } - - let input_files = - input_files - .map(|(filename, extension)| - (PathBuf::from(filename), extension.unwrap()) - ) - .collect(); + let input_files = process_decompressible_input(input_files)?; println!("{}: attempting to decompress input files into {}", "info".yellow(), output_file); return Ok( @@ -125,94 +132,17 @@ impl TryFrom> for Command { ); } } else { - // BIG TODO - Err(error::Error::MissingExtensionError("placeholder result".into())) + // else: output file not supplied + // Case 1: all input files are decompressible + // Case 2: error + let input_files = process_decompressible_input(input_files)?; + return Ok( + Command { + command_type: CommandType::Decompression(input_files), + output: None + } + ); + } } -} - -// impl TryFrom> for ArgValues { -// type Error = error::OuchError; -// fn try_from(matches: clap::ArgMatches<'static>) -> error::OuchResult { -// // Case 1: -o was set -// // Case 1.1: -o was set and has a (supported) compression file extension -// // |--> Compress all input files into the supplied output file (no extension checks on inputs) -// // Case 1.2: -o was set and is not a supported expression -// // Case 2: -o was not set -// // Case 2.1: -o was not set and all input files are (supported) compression file extensions -// // |--> Decompress input files into inferred filenames or directories -// // Case 2.2: -o was not set and not all input files are (supported) compression file extensions -// // |--> Issue an error - -// let inputs = matches -// .values_of("input") -// .unwrap() // Safe to unwrap since this is a required argument -// .map(|input: &str| { -// ( -// PathBuf::from(input), -// CompressionExtension::try_from(input).ok(), -// ) -// }); - -// let output_was_supplied = matches.is_present("output"); -// let inputs_are_compressed_files = inputs.clone().all(|(_, ext)| ext.is_some()); - -// match (output_was_supplied, inputs_are_compressed_files) { -// (true, true) => { -// // -o was set and inputs are all valid compressed files - -// let output = matches.value_of("output").unwrap(); -// let output = PathBuf::from(output); -// match CompressionExtension::try_from(&output) { -// Ok(ext) => { -// // If the output file is a valid compressed file, then we compress the input files into it -// Ok(Self { -// command_type: CommandType::Compress( -// inputs.map(|(path, _)| path).collect(), -// ), -// output: Some((output, ext)), -// }) -// } -// Err(_) => { -// // If the output file is not a compressed file, then we decompress the input files into it -// Ok(Self { -// command_type: CommandType::Decompress( -// inputs.map(|(path, ext)| (path, ext.unwrap())).collect(), -// ), -// output: Some((output, CompressionExtension::NotCompressed)), -// }) -// } -// } -// } -// (true, false) => { -// // -o was set and inputs are not (all) valid compressed files -// let output_str = matches.value_of("output").unwrap(); -// let output = PathBuf::from(output_str); -// let output_ext = match CompressionExtension::try_from(&output) { -// Ok(ext) => ext, -// Err(_) => { -// return Err(error::OuchError::MissingExtensionError(output_str.into())); -// } -// }; - -// Ok(Self { -// command_type: CommandType::Compress(inputs.map(|(path, _)| path).collect()), -// output: Some((output, output_ext)), -// }) -// } -// (false, true) => { -// // Case 2.1: -o was not set and all input files are (supported) compression file extensions -// Ok(Self { -// command_type: CommandType::Decompress( -// inputs.map(|(path, ext)| (path, ext.unwrap())).collect(), -// ), -// output: None, -// }) -// } -// (false, false) => { -// // Case 2.2: -o was not set and not all input files are not (supported) compression file extensions -// Err(error::OuchError::InvalidInput) -// } -// } -// } -// } \ No newline at end of file +} \ No newline at end of file diff --git a/src/extensions.rs b/src/extensions.rs index dc8500842..e4afb80ea 100644 --- a/src/extensions.rs +++ b/src/extensions.rs @@ -4,7 +4,7 @@ use std::{ }; use crate::error; -#[derive(Debug)] +#[derive(PartialEq, Eq, Debug)] /// Accepted extensions for input and output pub enum CompressionExtension { // .gz diff --git a/src/file.rs b/src/file.rs index 325e8baf9..89e73991c 100644 --- a/src/file.rs +++ b/src/file.rs @@ -12,7 +12,7 @@ use crate::extensions::CompressionExtension; // pub filename: PathBuf, // } -#[derive(Debug)] +#[derive(PartialEq, Eq, Debug)] pub enum File { WithExtension((PathBuf, CompressionExtension)), WithoutExtension(PathBuf) diff --git a/src/main.rs b/src/main.rs index 34e9a5069..e04b98e77 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ mod cli; mod file; mod extensions; mod error; +mod test; fn main() { diff --git a/src/test.rs b/src/test.rs index e69de29bb..3f523d59f 100644 --- a/src/test.rs +++ b/src/test.rs @@ -0,0 +1,64 @@ +#[cfg(test)] +mod cli { + + use std::{convert::TryFrom}; + + use crate::cli::clap_app; + use crate::cli::Command; + use crate::file::File; + use crate::cli::CommandType::*; + use crate::extensions::CompressionExtension::*; + use crate::error::OuchResult; + + #[test] + fn decompress_files_into_folder() -> OuchResult<()> { + let matches = clap_app(). + get_matches_from( + vec!["ouch", "-i", "file.zip", "-o", "folder/"] + ); + let command_from_matches = Command::try_from(matches)?; + + assert_eq!( + command_from_matches, + Command { + command_type: Decompression( + vec![ + ( + "file.zip".into(), + Zip, + ), + ], + ), + output: Some(File::WithoutExtension("folder".into())), + } + ); + + Ok(()) + } + + #[test] + fn decompress_files() -> OuchResult<()> { + let matches = clap_app(). + get_matches_from( + vec!["ouch", "-i", "file.zip"] + ); + let command_from_matches = Command::try_from(matches)?; + + assert_eq!( + command_from_matches, + Command { + command_type: Decompression( + vec![ + ( + "file.zip".into(), + Zip, + ), + ], + ), + output: None, + } + ); + + Ok(()) + } +} \ No newline at end of file From e04aba8d5348c30c2060f1cff413b17d4e680f9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Fri, 19 Mar 2021 13:20:22 -0300 Subject: [PATCH 04/67] cli: More tests --- src/error.rs | 2 +- src/test.rs | 61 +++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/src/error.rs b/src/error.rs index ee9e73b3b..556031563 100644 --- a/src/error.rs +++ b/src/error.rs @@ -2,7 +2,7 @@ use std::{fmt, path::PathBuf}; use colored::Colorize; -#[derive(Debug)] +#[derive(PartialEq, Eq, Debug)] pub enum Error { UnknownExtensionError(String), MissingExtensionError(String), diff --git a/src/test.rs b/src/test.rs index 3f523d59f..2dcabe4e7 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,8 +1,11 @@ +use std::{convert::TryFrom}; + + + #[cfg(test)] mod cli { - - use std::{convert::TryFrom}; + use std::convert::TryFrom; use crate::cli::clap_app; use crate::cli::Command; use crate::file::File; @@ -10,6 +13,7 @@ mod cli { use crate::extensions::CompressionExtension::*; use crate::error::OuchResult; + #[test] fn decompress_files_into_folder() -> OuchResult<()> { let matches = clap_app(). @@ -40,7 +44,7 @@ mod cli { fn decompress_files() -> OuchResult<()> { let matches = clap_app(). get_matches_from( - vec!["ouch", "-i", "file.zip"] + vec!["ouch", "-i", "file.zip", "file.tar"] ); let command_from_matches = Command::try_from(matches)?; @@ -53,6 +57,10 @@ mod cli { "file.zip".into(), Zip, ), + ( + "file.tar".into(), + Tar, + ), ], ), output: None, @@ -61,4 +69,51 @@ mod cli { Ok(()) } + + #[test] + fn compress_files() -> OuchResult<()> { + 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 { command_type: Compression(vec!["file".into(), "file2.jpeg".into(), "file3.ok".into()]), output: Some(File::WithExtension(("file.tar".into(), Tar))) } + ); + + Ok(()) + } +} + +#[cfg(test)] +mod cli_errors { + + use std::convert::TryFrom; + + use crate::cli::clap_app; + use crate::cli::Command; + use crate::file::File; + use crate::cli::CommandType::*; + use crate::extensions::CompressionExtension::*; + use crate::error::OuchResult; + use crate::error::Error; + + + #[test] + fn compress_files() -> OuchResult<()> { + 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(Error::InputsMustHaveBeenDecompressible("a_file".into())) + ); + + Ok(()) + } } \ No newline at end of file From 73398c2d5081483e83eb9fae690e4932f7386268 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Fri, 19 Mar 2021 15:18:32 -0300 Subject: [PATCH 05/67] tests: Add tests for extension extraction --- src/cli.rs | 146 +++++++++++++++++++++------------------------- src/error.rs | 5 +- src/evaluator.rs | 0 src/extensions.rs | 3 + src/file.rs | 40 +------------ src/main.rs | 19 +++--- src/test.rs | 146 +++++++++++++++++++++++++++++----------------- 7 files changed, 180 insertions(+), 179 deletions(-) create mode 100644 src/evaluator.rs diff --git a/src/cli.rs b/src/cli.rs index 75daad791..f8d5c3e2a 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,4 +1,4 @@ -use std::{convert::TryFrom, ffi::OsStr, path::PathBuf, vec::Vec}; +use std::{convert::TryFrom, path::PathBuf, vec::Vec}; use clap::{Arg, Values}; use colored::Colorize; @@ -61,88 +61,78 @@ pub fn get_matches() -> clap::ArgMatches<'static> { // holy spaghetti code impl TryFrom> for Command { - type Error = error::Error; - fn try_from(matches: clap::ArgMatches<'static>) -> error::OuchResult { - - let process_decompressible_input = |input_files: Values| { - let input_files = input_files - .map(|filename| (filename, CompressionExtension::try_from(filename))); - - for file in input_files.clone() { - if let (file, Err(_)) = file { - // eprintln!("{}: file '{}' is not decompressible.", "error".red(), file); - return Err(error::Error::InputsMustHaveBeenDecompressible(file.into())); - } - } - - - Ok(input_files - .map(|(filename, extension)| - (PathBuf::from(filename), extension.unwrap()) - ) - .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 = CompressionExtension::try_from(output_file); - let output_is_compressible = output_file_extension.is_ok(); - if output_is_compressible { - println!("{}: trying to compress input files into '{}'", "info".yellow(), output_file); - - let input_files = input_files.map(PathBuf::from).collect(); - - return Ok( - Command { - command_type: CommandType::Compression(input_files), - output: Some(File::WithExtension( - (output_file.into(), output_file_extension.unwrap()) - )) - } - ); - - } - else { - // Checking if input files are decompressible - - let input_files = process_decompressible_input(input_files)?; - - println!("{}: attempting to decompress input files into {}", "info".yellow(), output_file); - return Ok( - Command { - command_type: CommandType::Decompression(input_files), - output: Some(File::WithoutExtension(output_file.into())) - } - ); + fn try_from(matches: clap::ArgMatches<'static>) -> error::OuchResult { + let process_decompressible_input = |input_files: Values| { + let input_files = + input_files.map(|filename| (filename, CompressionExtension::try_from(filename))); + + for file in input_files.clone() { + if let (file, Err(_)) = file { + // eprintln!("{}: file '{}' is not decompressible.", "error".red(), file); + return Err(error::Error::InputsMustHaveBeenDecompressible(file.into())); } + } + + Ok(input_files + .map(|(filename, extension)| (PathBuf::from(filename), extension.unwrap())) + .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 = CompressionExtension::try_from(output_file); + let output_is_compressible = output_file_extension.is_ok(); + if output_is_compressible { + println!( + "{}: trying to compress input files into '{}'", + "info".yellow(), + output_file + ); + + let input_files = input_files.map(PathBuf::from).collect(); + + return Ok(Command { + command_type: CommandType::Compression(input_files), + output: Some(File::WithExtension(( + output_file.into(), + output_file_extension.unwrap(), + ))), + }); } else { - // else: output file not supplied - // Case 1: all input files are decompressible - // Case 2: error + // Checking if input files are decompressible + let input_files = process_decompressible_input(input_files)?; - return Ok( - Command { - command_type: CommandType::Decompression(input_files), - output: None - } + + println!( + "{}: attempting to decompress input files into {}", + "info".yellow(), + output_file ); - + return Ok(Command { + command_type: CommandType::Decompression(input_files), + output: Some(File::WithoutExtension(output_file.into())), + }); } + } else { + // else: output file not supplied + // Case 1: all input files are decompressible + // Case 2: error + let input_files = process_decompressible_input(input_files)?; + return Ok(Command { + command_type: CommandType::Decompression(input_files), + output: None, + }); } -} \ No newline at end of file + } +} diff --git a/src/error.rs b/src/error.rs index 556031563..2ca7502dd 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,4 @@ -use std::{fmt, path::PathBuf}; +use std::fmt; use colored::Colorize; @@ -6,9 +6,10 @@ use colored::Colorize; pub enum Error { UnknownExtensionError(String), MissingExtensionError(String), + // TODO: get rid of this error variant InvalidUnicode, InvalidInput, - InputsMustHaveBeenDecompressible(String) + InputsMustHaveBeenDecompressible(String), } // This should be placed somewhere else diff --git a/src/evaluator.rs b/src/evaluator.rs new file mode 100644 index 000000000..e69de29bb diff --git a/src/extensions.rs b/src/extensions.rs index e4afb80ea..227618ee2 100644 --- a/src/extensions.rs +++ b/src/extensions.rs @@ -43,6 +43,9 @@ impl TryFrom<&PathBuf> for CompressionExtension { match ext { "zip" => Ok(Zip), "tar" => Ok(Tar), + "gz" => Ok(Gzip), + "bz" => Ok(Bzip), + "lzma" => Ok(Lzma), other => Err(error::Error::UnknownExtensionError(other.into())), } } diff --git a/src/file.rs b/src/file.rs index 89e73991c..873c945fe 100644 --- a/src/file.rs +++ b/src/file.rs @@ -1,43 +1,9 @@ -use std::{convert::TryFrom, path::PathBuf, str::FromStr}; +use std::path::PathBuf; -use crate::error::Error as OuchError; -use crate::error; use crate::extensions::CompressionExtension; -// pub type File = (PathBuf, CompressionExtension); - -// #[derive(Debug)] -// pub struct FileWithExtension { -// pub extension: CompressionExtension, -// pub filename: PathBuf, -// } - #[derive(PartialEq, Eq, Debug)] pub enum File { WithExtension((PathBuf, CompressionExtension)), - WithoutExtension(PathBuf) -} - -// impl TryFrom for FileWithExtension { -// type Error = OuchError; - -// fn try_from(filename: String) -> error::OuchResult { -// // Safe to unwrap (infallible operation) -// let filename = PathBuf::from_str(&filename).unwrap(); - -// let os_str = match filename.extension() { -// Some(os_str) => os_str, -// None => return Err(OuchError::MissingExtensionError(filename.to_string_lossy().to_string())), -// }; - -// let extension = match CompressionExtension::try_from(os_str.into()) { -// Ok(ext) => ext, -// Err(err) => return Err(err), -// }; - -// Ok(Self { -// filename, -// extension, -// }) -// } -// } + WithoutExtension(PathBuf), +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index e04b98e77..3a9b3cede 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,13 @@ -use std::{convert::TryFrom, fs::File}; - -use cli::get_matches; +use std::convert::TryFrom; mod cli; -mod file; -mod extensions; mod error; +mod extensions; +mod file; mod test; +mod evaluator; fn main() { - // Just testing // let args: Vec = std::env::args().collect(); @@ -20,7 +18,7 @@ fn main() { // Ok((reader, compression)) => {}, // Err(err) => {} // } - + // let (mut reader, compression) = niffler::sniff(Box::new(&file[..])).unwrap(); // match compression { @@ -35,12 +33,13 @@ fn main() { // dbg!(compression); - let matches = get_matches(); + let matches = cli::get_matches(); match cli::Command::try_from(matches) { - Ok(vals) => { dbg!(vals); }, + Ok(vals) => { + dbg!(vals); + } Err(err) => { print!("{}\n", err); } } - } diff --git a/src/test.rs b/src/test.rs index 2dcabe4e7..30c41593d 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,38 +1,23 @@ -use std::{convert::TryFrom}; - - - #[cfg(test)] mod cli { - use std::convert::TryFrom; use crate::cli::clap_app; use crate::cli::Command; - use crate::file::File; use crate::cli::CommandType::*; - use crate::extensions::CompressionExtension::*; use crate::error::OuchResult; - + use crate::extensions::CompressionExtension::*; + use crate::file::File; + use std::convert::TryFrom; #[test] fn decompress_files_into_folder() -> OuchResult<()> { - let matches = clap_app(). - get_matches_from( - vec!["ouch", "-i", "file.zip", "-o", "folder/"] - ); + let matches = clap_app().get_matches_from(vec!["ouch", "-i", "file.zip", "-o", "folder/"]); let command_from_matches = Command::try_from(matches)?; assert_eq!( command_from_matches, Command { - command_type: Decompression( - vec![ - ( - "file.zip".into(), - Zip, - ), - ], - ), + command_type: Decompression(vec![("file.zip".into(), Zip,),],), output: Some(File::WithoutExtension("folder".into())), } ); @@ -42,27 +27,16 @@ mod cli { #[test] fn decompress_files() -> OuchResult<()> { - let matches = clap_app(). - get_matches_from( - vec!["ouch", "-i", "file.zip", "file.tar"] - ); + let matches = clap_app().get_matches_from(vec!["ouch", "-i", "file.zip", "file.tar"]); let command_from_matches = Command::try_from(matches)?; assert_eq!( command_from_matches, Command { - command_type: Decompression( - vec![ - ( - "file.zip".into(), - Zip, - ), - ( - "file.tar".into(), - Tar, - ), - ], - ), + command_type: Decompression(vec![ + ("file.zip".into(), Zip,), + ("file.tar".into(), Tar,), + ],), output: None, } ); @@ -72,15 +46,27 @@ mod cli { #[test] fn compress_files() -> OuchResult<()> { - let matches = clap_app(). - get_matches_from( - vec!["ouch", "-i", "file", "file2.jpeg", "file3.ok", "-o", "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)?; assert_eq!( command_from_matches, - Command { command_type: Compression(vec!["file".into(), "file2.jpeg".into(), "file3.ok".into()]), output: Some(File::WithExtension(("file.tar".into(), Tar))) } + Command { + command_type: Compression(vec![ + "file".into(), + "file2.jpeg".into(), + "file3.ok".into() + ]), + output: Some(File::WithExtension(("file.tar".into(), Tar))) + } ); Ok(()) @@ -89,24 +75,18 @@ mod cli { #[cfg(test)] mod cli_errors { - + use std::convert::TryFrom; use crate::cli::clap_app; use crate::cli::Command; - use crate::file::File; - use crate::cli::CommandType::*; - use crate::extensions::CompressionExtension::*; - use crate::error::OuchResult; use crate::error::Error; - + use crate::error::OuchResult; #[test] fn compress_files() -> OuchResult<()> { - let matches = clap_app(). - get_matches_from( - vec!["ouch", "-i", "a_file", "file2.jpeg", "file3.ok"] - ); + let matches = + clap_app().get_matches_from(vec!["ouch", "-i", "a_file", "file2.jpeg", "file3.ok"]); let res = Command::try_from(matches); assert_eq!( @@ -116,4 +96,66 @@ mod cli_errors { Ok(()) } -} \ No newline at end of file +} + +#[cfg(test)] +mod extension_extraction { + use crate::error::OuchResult; + use crate::extensions::CompressionExtension; + use std::{convert::TryFrom, path::PathBuf, str::FromStr}; + + #[test] + fn zip() -> OuchResult<()> { + let path = PathBuf::from_str("filename.tar.zip").unwrap(); + assert_eq!( + CompressionExtension::try_from(&path)?, + CompressionExtension::Zip + ); + + Ok(()) + } + + #[test] + fn tar() -> OuchResult<()> { + let path = PathBuf::from_str("pictures.tar").unwrap(); + assert_eq!( + CompressionExtension::try_from(&path)?, + CompressionExtension::Tar + ); + + Ok(()) + } + + #[test] + fn gz() -> OuchResult<()> { + let path = PathBuf::from_str("passwords.tar.gz").unwrap(); + assert_eq!( + CompressionExtension::try_from(&path)?, + CompressionExtension::Gzip + ); + + Ok(()) + } + + #[test] + fn lzma() -> OuchResult<()> { + let path = PathBuf::from_str("mygame.tar.lzma").unwrap(); + assert_eq!( + CompressionExtension::try_from(&path)?, + CompressionExtension::Lzma + ); + + Ok(()) + } + + #[test] + fn bz() -> OuchResult<()> { + let path = PathBuf::from_str("songs.tar.bz").unwrap(); + assert_eq!( + CompressionExtension::try_from(&path)?, + CompressionExtension::Bzip + ); + + Ok(()) + } +} From 7fd6020d99ca50754c5b48f95deb91898345c808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Sat, 20 Mar 2021 18:58:26 -0300 Subject: [PATCH 06/67] Add struct Extension --- src/cli.rs | 18 +++---- src/evaluator.rs | 62 ++++++++++++++++++++++ src/extensions.rs | 131 ++++++++++++++++++++++++++++++++++------------ src/file.rs | 4 +- src/main.rs | 47 +++++------------ src/test.rs | 32 +++++------ 6 files changed, 201 insertions(+), 93 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index f8d5c3e2a..5be83bfaf 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -4,24 +4,24 @@ use clap::{Arg, Values}; use colored::Colorize; use crate::error; -use crate::extensions::CompressionExtension; +use crate::extensions::CompressionFormat; use crate::file::File; #[derive(PartialEq, Eq, Debug)] -pub enum CommandType { +pub enum CommandKind { Compression( // Files to be compressed Vec, ), Decompression( // Files to be decompressed and their extensions - Vec<(PathBuf, CompressionExtension)>, + Vec<(PathBuf, CompressionFormat)>, ), } #[derive(PartialEq, Eq, Debug)] pub struct Command { - pub command_type: CommandType, + pub kind: CommandKind, pub output: Option, } @@ -66,7 +66,7 @@ impl TryFrom> for Command { fn try_from(matches: clap::ArgMatches<'static>) -> error::OuchResult { let process_decompressible_input = |input_files: Values| { let input_files = - input_files.map(|filename| (filename, CompressionExtension::try_from(filename))); + input_files.map(|filename| (filename, CompressionFormat::try_from(filename))); for file in input_files.clone() { if let (file, Err(_)) = file { @@ -91,7 +91,7 @@ impl TryFrom> for Command { 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 = CompressionExtension::try_from(output_file); + let output_file_extension = CompressionFormat::try_from(output_file); let output_is_compressible = output_file_extension.is_ok(); if output_is_compressible { println!( @@ -103,7 +103,7 @@ impl TryFrom> for Command { let input_files = input_files.map(PathBuf::from).collect(); return Ok(Command { - command_type: CommandType::Compression(input_files), + kind: CommandKind::Compression(input_files), output: Some(File::WithExtension(( output_file.into(), output_file_extension.unwrap(), @@ -120,7 +120,7 @@ impl TryFrom> for Command { output_file ); return Ok(Command { - command_type: CommandType::Decompression(input_files), + kind: CommandKind::Decompression(input_files), output: Some(File::WithoutExtension(output_file.into())), }); } @@ -130,7 +130,7 @@ impl TryFrom> for Command { // Case 2: error let input_files = process_decompressible_input(input_files)?; return Ok(Command { - command_type: CommandType::Decompression(input_files), + kind: CommandKind::Decompression(input_files), output: None, }); } diff --git a/src/evaluator.rs b/src/evaluator.rs index e69de29bb..e250f6800 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -0,0 +1,62 @@ +use std::{convert::TryFrom, path::{PathBuf}}; + +use colored::Colorize; + +use crate::{cli::{Command, CommandKind}, error, extensions::CompressionFormat, file::File}; + +pub struct Evaluator { + command: Command, + // verbosity: Verbosity +} + +impl Evaluator { + pub fn new(command: Command) -> Self { + Self { + command + } + } + + fn handle_compression(files_to_compress: &[PathBuf], output_file: &Option) { + + } + + fn decompress_file(mut filename: &PathBuf, mut extension: CompressionFormat, output_file: &Option) -> error::OuchResult<()> { + loop { + println!("{}: attempting to decompress '{:?}'", "ouch".bright_blue(), filename); + } + } + + fn handle_decompression(files_to_decompress: &[(PathBuf, CompressionFormat)], output_file: &Option) { + for (filename, extension) in files_to_decompress { + // println!("file: {:?}, extension: {:?}", filename, extension); + + // TODO: actually decompress anything ;-; + + // Once decompressed, check if the file can be decompressed further + // e.g.: "foobar.tar.gz" -> "foobar.tar" + + + + let filename: &PathBuf = &filename.as_path().file_stem().unwrap().into(); + match CompressionFormat::try_from(filename) { + Ok(extension) => { + println!("{}: attempting to decompress {:?}, ext: {:?}", "info".yellow(), filename, extension); + }, + Err(err) => { + continue; + } + } + } + } + + pub fn evaluate(&mut self) { + match &self.command.kind { + CommandKind::Compression(files_to_compress) => { + Evaluator::handle_compression(files_to_compress, &self.command.output); + } + CommandKind::Decompression(files_to_decompress) => { + Evaluator::handle_decompression(files_to_decompress, &self.command.output); + } + } + } +} \ No newline at end of file diff --git a/src/extensions.rs b/src/extensions.rs index 227618ee2..a0ee06ecf 100644 --- a/src/extensions.rs +++ b/src/extensions.rs @@ -1,12 +1,84 @@ -use std::{ - convert::TryFrom, - path::{Path, PathBuf}, -}; +use std::{convert::TryFrom, ffi::OsStr, path::{Path, PathBuf}}; use crate::error; +use CompressionFormat::*; + +/// 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 } +struct Extension { + first_ext: Option, + second_ext: CompressionFormat +} + +impl Extension { + pub fn new(filename: &str) -> error::OuchResult { + let ext_from_str = |ext| { + match ext { + "zip" => Ok(Zip), + "tar" => Ok(Tar), + "gz" => Ok(Gzip), + "bz" => Ok(Bzip), + "lzma" => Ok(Lzma), + other => Err(error::Error::UnknownExtensionError(other.into())), + } + }; + + let (first_ext, second_ext) = match get_extension_from_filename(filename) { + Some(extension_tuple) => { + match extension_tuple { + ("", snd) => (None, snd), + (fst, snd)=> (Some(fst), snd) + } + }, + None => { + return Err(error::Error::MissingExtensionError(filename.into())) + } + }; + + let (first_ext, second_ext) = match (first_ext, second_ext) { + (None, snd) => { + let ext = ext_from_str(snd)?; + (None, ext) + } + (Some(fst), snd) => { + let snd = ext_from_str(snd)?; + let fst = ext_from_str(fst).ok(); + (fst, snd) + } + }; + + Ok( + Self { + first_ext, + second_ext + } + ) + } +} + +pub fn get_extension_from_filename(filename: &str) -> Option<(&str, &str)> { + let path = Path::new(filename); + + let ext = path + .extension() + .and_then(OsStr::to_str)?; + + let previous_extension = path + .file_stem() + .and_then(OsStr::to_str) + .and_then(get_extension_from_filename); + + if let Some((_, prev)) = previous_extension { + Some((prev, ext)) + } else { + Some(("", ext)) + } +} + #[derive(PartialEq, Eq, Debug)] /// Accepted extensions for input and output -pub enum CompressionExtension { +pub enum CompressionFormat { // .gz Gzip, // .bz @@ -22,11 +94,27 @@ pub enum CompressionExtension { // NotCompressed } -impl TryFrom<&PathBuf> for CompressionExtension { +fn extension_from_os_str(ext: &OsStr) -> Result { + + let ext = match ext.to_str() { + Some(str) => str, + None => return Err(error::Error::InvalidUnicode), + }; + + match ext { + "zip" => Ok(Zip), + "tar" => Ok(Tar), + "gz" => Ok(Gzip), + "bz" => Ok(Bzip), + "lzma" => Ok(Lzma), + other => Err(error::Error::UnknownExtensionError(other.into())), + } +} + +impl TryFrom<&PathBuf> for CompressionFormat { type Error = error::Error; fn try_from(ext: &PathBuf) -> Result { - use CompressionExtension::*; let ext = match ext.extension() { Some(ext) => ext, @@ -35,43 +123,20 @@ impl TryFrom<&PathBuf> for CompressionExtension { } }; - let ext = match ext.to_str() { - Some(str) => str, - None => return Err(error::Error::InvalidUnicode), - }; - - match ext { - "zip" => Ok(Zip), - "tar" => Ok(Tar), - "gz" => Ok(Gzip), - "bz" => Ok(Bzip), - "lzma" => Ok(Lzma), - other => Err(error::Error::UnknownExtensionError(other.into())), - } + extension_from_os_str(ext) } } -impl TryFrom<&str> for CompressionExtension { +impl TryFrom<&str> for CompressionFormat { type Error = error::Error; fn try_from(filename: &str) -> Result { - use CompressionExtension::*; - let filename = Path::new(filename); let ext = match filename.extension() { Some(ext) => ext, None => return Err(error::Error::MissingExtensionError(String::new())), }; - let ext = match ext.to_str() { - Some(str) => str, - None => return Err(error::Error::InvalidUnicode), - }; - - match ext { - "zip" => Ok(Zip), - "tar" => Ok(Tar), - other => Err(error::Error::UnknownExtensionError(other.into())), - } + extension_from_os_str(ext) } } diff --git a/src/file.rs b/src/file.rs index 873c945fe..ead7b688f 100644 --- a/src/file.rs +++ b/src/file.rs @@ -1,9 +1,9 @@ use std::path::PathBuf; -use crate::extensions::CompressionExtension; +use crate::extensions::CompressionFormat; #[derive(PartialEq, Eq, Debug)] pub enum File { - WithExtension((PathBuf, CompressionExtension)), + WithExtension((PathBuf, CompressionFormat)), WithoutExtension(PathBuf), } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 3a9b3cede..716579a8d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,6 @@ -use std::convert::TryFrom; +use std::{convert::TryFrom, ffi::OsStr, path::Path}; + +use colored::Colorize; mod cli; mod error; @@ -8,38 +10,17 @@ mod test; mod evaluator; fn main() { - // Just testing - - // let args: Vec = std::env::args().collect(); - - // let file = std::fs::read(args[1].clone()).unwrap(); - - // match niffler::sniff(Box::new(&file[..])) { - // Ok((reader, compression)) => {}, - // Err(err) => {} + // let matches = cli::get_matches(); + // match cli::Command::try_from(matches) { + // Ok(command) => { + // let mut eval = evaluator::Evaluator::new(command); + // eval.evaluate(); + // } + // Err(err) => { + // print!("{}: {}\n", "error".red(), err); + // } // } - // let (mut reader, compression) = niffler::sniff(Box::new(&file[..])).unwrap(); - - // match compression { - // niffler::Format::Gzip => {} - // niffler::Format::Bzip => {} - // niffler::Format::Lzma => {} - // niffler::Format::No => {} - // } - - // let mut contents = String::new(); - // println!("Contents: {}", reader.read_to_string(&mut contents).unwrap()); - - // dbg!(compression); - - let matches = cli::get_matches(); - match cli::Command::try_from(matches) { - Ok(vals) => { - dbg!(vals); - } - Err(err) => { - print!("{}\n", err); - } - } + dbg!(extensions::get_extension_from_filename("file")); + // dbg!(get_extension_from_filename("file.zip")); } diff --git a/src/test.rs b/src/test.rs index 30c41593d..0db972a17 100644 --- a/src/test.rs +++ b/src/test.rs @@ -3,9 +3,9 @@ mod cli { use crate::cli::clap_app; use crate::cli::Command; - use crate::cli::CommandType::*; + use crate::cli::CommandKind::*; use crate::error::OuchResult; - use crate::extensions::CompressionExtension::*; + use crate::extensions::CompressionFormat::*; use crate::file::File; use std::convert::TryFrom; @@ -17,7 +17,7 @@ mod cli { assert_eq!( command_from_matches, Command { - command_type: Decompression(vec![("file.zip".into(), Zip,),],), + kind: Decompression(vec![("file.zip".into(), Zip,),],), output: Some(File::WithoutExtension("folder".into())), } ); @@ -33,7 +33,7 @@ mod cli { assert_eq!( command_from_matches, Command { - command_type: Decompression(vec![ + kind: Decompression(vec![ ("file.zip".into(), Zip,), ("file.tar".into(), Tar,), ],), @@ -60,7 +60,7 @@ mod cli { assert_eq!( command_from_matches, Command { - command_type: Compression(vec![ + kind: Compression(vec![ "file".into(), "file2.jpeg".into(), "file3.ok".into() @@ -101,15 +101,15 @@ mod cli_errors { #[cfg(test)] mod extension_extraction { use crate::error::OuchResult; - use crate::extensions::CompressionExtension; + use crate::extensions::CompressionFormat; use std::{convert::TryFrom, path::PathBuf, str::FromStr}; #[test] fn zip() -> OuchResult<()> { let path = PathBuf::from_str("filename.tar.zip").unwrap(); assert_eq!( - CompressionExtension::try_from(&path)?, - CompressionExtension::Zip + CompressionFormat::try_from(&path)?, + CompressionFormat::Zip ); Ok(()) @@ -119,8 +119,8 @@ mod extension_extraction { fn tar() -> OuchResult<()> { let path = PathBuf::from_str("pictures.tar").unwrap(); assert_eq!( - CompressionExtension::try_from(&path)?, - CompressionExtension::Tar + CompressionFormat::try_from(&path)?, + CompressionFormat::Tar ); Ok(()) @@ -130,8 +130,8 @@ mod extension_extraction { fn gz() -> OuchResult<()> { let path = PathBuf::from_str("passwords.tar.gz").unwrap(); assert_eq!( - CompressionExtension::try_from(&path)?, - CompressionExtension::Gzip + CompressionFormat::try_from(&path)?, + CompressionFormat::Gzip ); Ok(()) @@ -141,8 +141,8 @@ mod extension_extraction { fn lzma() -> OuchResult<()> { let path = PathBuf::from_str("mygame.tar.lzma").unwrap(); assert_eq!( - CompressionExtension::try_from(&path)?, - CompressionExtension::Lzma + CompressionFormat::try_from(&path)?, + CompressionFormat::Lzma ); Ok(()) @@ -152,8 +152,8 @@ mod extension_extraction { fn bz() -> OuchResult<()> { let path = PathBuf::from_str("songs.tar.bz").unwrap(); assert_eq!( - CompressionExtension::try_from(&path)?, - CompressionExtension::Bzip + CompressionFormat::try_from(&path)?, + CompressionFormat::Bzip ); Ok(()) From 155fca45264896a5450ac9ce3df6f8fd3ebc2b10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Sun, 21 Mar 2021 00:53:54 -0300 Subject: [PATCH 07/67] refactor: New File struct and switch to use Extension --- src/cli.rs | 35 ++++++++++++++++++++------- src/evaluator.rs | 34 +++++++++++++------------- src/{extensions.rs => extension.rs} | 12 +++++++++- src/file.rs | 28 +++++++++++++++++----- src/main.rs | 27 ++++++++++----------- src/test.rs | 37 ++++++++++++++++++++++------- 6 files changed, 117 insertions(+), 56 deletions(-) rename src/{extensions.rs => extension.rs} (94%) diff --git a/src/cli.rs b/src/cli.rs index 5be83bfaf..0b595a7e3 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -4,7 +4,7 @@ use clap::{Arg, Values}; use colored::Colorize; use crate::error; -use crate::extensions::CompressionFormat; +use crate::extension::{Extension, CompressionFormat}; use crate::file::File; #[derive(PartialEq, Eq, Debug)] @@ -15,7 +15,7 @@ pub enum CommandKind { ), Decompression( // Files to be decompressed and their extensions - Vec<(PathBuf, CompressionFormat)>, + Vec, ), } @@ -70,7 +70,6 @@ impl TryFrom> for Command { for file in input_files.clone() { if let (file, Err(_)) = file { - // eprintln!("{}: file '{}' is not decompressible.", "error".red(), file); return Err(error::Error::InputsMustHaveBeenDecompressible(file.into())); } } @@ -91,9 +90,11 @@ impl TryFrom> for Command { 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 = CompressionFormat::try_from(output_file); + 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 + println!( "{}: trying to compress input files into '{}'", "info".yellow(), @@ -102,13 +103,22 @@ impl TryFrom> for Command { let input_files = input_files.map(PathBuf::from).collect(); + // return Ok(Command { + // kind: CommandKind::Compression(input_files), + // output: Some(File::WithExtension(( + // output_file.into(), + // output_file_extension.unwrap(), + // ))), + // }); + return Ok(Command { kind: CommandKind::Compression(input_files), - output: Some(File::WithExtension(( - output_file.into(), - output_file_extension.unwrap(), - ))), + output: Some(File { + path: output_file.into(), + extension: Some(output_file_extension.unwrap()) + }), }); + } else { // Checking if input files are decompressible @@ -119,9 +129,15 @@ impl TryFrom> for Command { "info".yellow(), output_file ); + + let input_files = input_files.into_iter().map(File::from).collect(); + return Ok(Command { kind: CommandKind::Decompression(input_files), - output: Some(File::WithoutExtension(output_file.into())), + output: Some(File { + path: output_file.into(), + extension: None + }) }); } } else { @@ -129,6 +145,7 @@ impl TryFrom> for Command { // Case 1: all input files are decompressible // Case 2: error let input_files = process_decompressible_input(input_files)?; + let input_files = input_files.into_iter().map(File::from).collect(); return Ok(Command { kind: CommandKind::Decompression(input_files), output: None, diff --git a/src/evaluator.rs b/src/evaluator.rs index e250f6800..1f5eea1c3 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -2,7 +2,7 @@ use std::{convert::TryFrom, path::{PathBuf}}; use colored::Colorize; -use crate::{cli::{Command, CommandKind}, error, extensions::CompressionFormat, file::File}; +use crate::{cli::{Command, CommandKind}, error, extension::CompressionFormat, file::File}; pub struct Evaluator { command: Command, @@ -26,27 +26,27 @@ impl Evaluator { } } - fn handle_decompression(files_to_decompress: &[(PathBuf, CompressionFormat)], output_file: &Option) { - for (filename, extension) in files_to_decompress { - // println!("file: {:?}, extension: {:?}", filename, extension); + fn handle_decompression(files_to_decompress: &[File], output_file: &Option) { + // for (filename, extension) in files_to_decompress { + // // println!("file: {:?}, extension: {:?}", filename, extension); - // TODO: actually decompress anything ;-; + // // TODO: actually decompress anything ;-; - // Once decompressed, check if the file can be decompressed further - // e.g.: "foobar.tar.gz" -> "foobar.tar" + // // Once decompressed, check if the file can be decompressed further + // // e.g.: "foobar.tar.gz" -> "foobar.tar" - let filename: &PathBuf = &filename.as_path().file_stem().unwrap().into(); - match CompressionFormat::try_from(filename) { - Ok(extension) => { - println!("{}: attempting to decompress {:?}, ext: {:?}", "info".yellow(), filename, extension); - }, - Err(err) => { - continue; - } - } - } + // let filename: &PathBuf = &filename.as_path().file_stem().unwrap().into(); + // match CompressionFormat::try_from(filename) { + // Ok(extension) => { + // println!("{}: attempting to decompress {:?}, ext: {:?}", "info".yellow(), filename, extension); + // }, + // Err(err) => { + // continue; + // } + // } + // } } pub fn evaluate(&mut self) { diff --git a/src/extensions.rs b/src/extension.rs similarity index 94% rename from src/extensions.rs rename to src/extension.rs index a0ee06ecf..ff22cf1fe 100644 --- a/src/extensions.rs +++ b/src/extension.rs @@ -6,11 +6,21 @@ use CompressionFormat::*; /// 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 } -struct Extension { +#[derive(Debug, PartialEq, Eq)] +pub struct Extension { first_ext: Option, second_ext: CompressionFormat } +impl From for Extension { + fn from(second_ext: CompressionFormat) -> Self { + Self { + first_ext: None, + second_ext + } + } +} + impl Extension { pub fn new(filename: &str) -> error::OuchResult { let ext_from_str = |ext| { diff --git a/src/file.rs b/src/file.rs index ead7b688f..327e1b95d 100644 --- a/src/file.rs +++ b/src/file.rs @@ -1,9 +1,25 @@ -use std::path::PathBuf; +use std::{path::PathBuf, process::Command}; -use crate::extensions::CompressionFormat; +use crate::extension::{CompressionFormat, Extension}; -#[derive(PartialEq, Eq, Debug)] -pub enum File { - WithExtension((PathBuf, CompressionFormat)), - WithoutExtension(PathBuf), + +#[derive(Debug, PartialEq, Eq)] +pub struct File { + /// File's (relative) path + pub path: PathBuf, + /// Note: extension here might be a misleading name since + /// we don't really care about any extension other than supported compression ones. + /// + /// So, for example, if a file has pathname "image.jpeg", it does have a JPEG extension but will + /// be represented as a None over here since that's not an extension we're particularly interested in + pub extension: Option +} + +impl From<(PathBuf, CompressionFormat)> for File { + fn from((path, format): (PathBuf, CompressionFormat)) -> Self { + Self { + path, + extension: Some(Extension::from(format)), + } + } } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 716579a8d..8977925ae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,26 +1,23 @@ -use std::{convert::TryFrom, ffi::OsStr, path::Path}; +use std::{convert::TryFrom}; use colored::Colorize; mod cli; mod error; -mod extensions; +mod extension; mod file; mod test; mod evaluator; fn main() { - // let matches = cli::get_matches(); - // match cli::Command::try_from(matches) { - // Ok(command) => { - // let mut eval = evaluator::Evaluator::new(command); - // eval.evaluate(); - // } - // Err(err) => { - // print!("{}: {}\n", "error".red(), err); - // } - // } - - dbg!(extensions::get_extension_from_filename("file")); - // dbg!(get_extension_from_filename("file.zip")); + let matches = cli::get_matches(); + match cli::Command::try_from(matches) { + Ok(command) => { + let mut eval = evaluator::Evaluator::new(command); + eval.evaluate(); + } + Err(err) => { + print!("{}: {}\n", "error".red(), err); + } + } } diff --git a/src/test.rs b/src/test.rs index 0db972a17..e6364a76b 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,11 +1,12 @@ #[cfg(test)] mod cli { - use crate::cli::clap_app; + use crate::{cli::clap_app, extension}; use crate::cli::Command; use crate::cli::CommandKind::*; use crate::error::OuchResult; - use crate::extensions::CompressionFormat::*; + use crate::extension::CompressionFormat::*; + use crate::extension::Extension; use crate::file::File; use std::convert::TryFrom; @@ -17,8 +18,16 @@ mod cli { assert_eq!( command_from_matches, Command { - kind: Decompression(vec![("file.zip".into(), Zip,),],), - output: Some(File::WithoutExtension("folder".into())), + kind: Decompression(vec![ + File { + path: "file.zip".into(), + extension: Some(Extension::from(Zip)) + } + ]), + output: Some(File { + path: "folder".into(), + extension: None + }), } ); @@ -34,8 +43,14 @@ mod cli { command_from_matches, Command { kind: Decompression(vec![ - ("file.zip".into(), Zip,), - ("file.tar".into(), Tar,), + File { + path: "file.zip".into(), + extension: Some(Extension::from(Zip)) + }, + File { + path: "file.tar".into(), + extension: Some(Extension::from(Tar)) + } ],), output: None, } @@ -65,7 +80,13 @@ mod cli { "file2.jpeg".into(), "file3.ok".into() ]), - output: Some(File::WithExtension(("file.tar".into(), Tar))) + // output: Some(File::WithExtension(("file.tar".into(), Extension::from(Tar)))) + output: Some( + File { + path: "file.tar".into(), + extension: Some(Extension::from(Tar)) + } + ), } ); @@ -101,7 +122,7 @@ mod cli_errors { #[cfg(test)] mod extension_extraction { use crate::error::OuchResult; - use crate::extensions::CompressionFormat; + use crate::extension::CompressionFormat; use std::{convert::TryFrom, path::PathBuf, str::FromStr}; #[test] From b6d4e50ccac7dcc84a6ff5f5ffc86b2e8a3e6f0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Sun, 21 Mar 2021 04:09:28 -0300 Subject: [PATCH 08/67] Early progress in supporting .tar files --- Cargo.lock | 129 +++++++++++++++++++++++++++--- Cargo.toml | 2 + src/cli.rs | 6 -- src/decompressors/decompressor.rs | 1 + src/decompressors/mod.rs | 1 + src/decompressors/tar.rs | 33 ++++++++ src/error.rs | 15 +++- src/evaluator.rs | 57 ++++++------- src/extension.rs | 8 +- src/main.rs | 42 +++++++--- 10 files changed, 233 insertions(+), 61 deletions(-) create mode 100644 src/decompressors/decompressor.rs create mode 100644 src/decompressors/mod.rs create mode 100644 src/decompressors/tar.rs diff --git a/Cargo.lock b/Cargo.lock index 1a13baada..1b5fa107b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15,6 +15,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler32" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" + [[package]] name = "ansi_term" version = "0.11.0" @@ -48,9 +54,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d117600f438b1707d4e4ae15d3595657288f8235a0eb593e80ecc98ab34e1bc" dependencies = [ "addr2line", - "cfg-if", + "cfg-if 1.0.0", "libc", - "miniz_oxide", + "miniz_oxide 0.4.4", "object", "rustc-demangle", ] @@ -71,6 +77,22 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bzip2" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b7c3cbf0fa9c1b82308d57191728ca0256cb821220f4e2fd410a72ade26e3b" +dependencies = [ + "bzip2-sys", + "libc", +] + [[package]] name = "bzip2" version = "0.4.2" @@ -98,6 +120,12 @@ version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + [[package]] name = "cfg-if" version = "1.0.0" @@ -136,7 +164,7 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -170,16 +198,28 @@ dependencies = [ "synstructure", ] +[[package]] +name = "filetime" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall", + "winapi", +] + [[package]] name = "flate2" -version = "1.0.20" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3aec53de10fe96d7d8c565eb17f2c687bb5518a2ec453b5b1252964526abe0" +checksum = "2cfff41391129e0a856d6d822600b8d71179d46879e310417eb9c762eb178b42" dependencies = [ - "cfg-if", + "cfg-if 0.1.10", "crc32fast", "libc", - "miniz_oxide", + "miniz_oxide 0.3.7", ] [[package]] @@ -220,6 +260,15 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "miniz_oxide" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" +dependencies = [ + "adler32", +] + [[package]] name = "miniz_oxide" version = "0.4.4" @@ -237,8 +286,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ea40fb13399dd0e9780ea3d82cb6cf27f489f63d820aa7dc9ec967750dc6d58" dependencies = [ "bgzip", - "bzip2", - "cfg-if", + "bzip2 0.4.2", + "cfg-if 1.0.0", "enum_primitive", "flate2", "thiserror", @@ -276,6 +325,8 @@ dependencies = [ "clap", "colored", "niffler", + "tar", + "zip", ] [[package]] @@ -302,6 +353,15 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9" +dependencies = [ + "bitflags", +] + [[package]] name = "rustc-demangle" version = "0.1.18" @@ -337,6 +397,17 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "tar" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0bcfbd6a598361fda270d82469fff3d65089dc33e175c9a131f7b4cd395f228" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "textwrap" version = "0.11.0" @@ -366,6 +437,17 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi", + "winapi", +] + [[package]] name = "unicode-width" version = "0.1.8" @@ -384,6 +466,12 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + [[package]] name = "winapi" version = "0.3.9" @@ -406,6 +494,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "xattr" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c" +dependencies = [ + "libc", +] + [[package]] name = "xz2" version = "0.1.6" @@ -414,3 +511,17 @@ checksum = "c179869f34fc7c01830d3ce7ea2086bc3a07e0d35289b667d0a8bf910258926c" dependencies = [ "lzma-sys", ] + +[[package]] +name = "zip" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8264fcea9b7a036a4a5103d7153e988dbc2ebbafb34f68a3c2d404b6b82d74b6" +dependencies = [ + "byteorder", + "bzip2 0.3.3", + "crc32fast", + "flate2", + "thiserror", + "time", +] diff --git a/Cargo.toml b/Cargo.toml index a90125941..d90553859 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,5 @@ edition = "2018" colored = "2.0.0" niffler = "2.3.1" clap = "2.33.3" +zip = "0.5.11" +tar = "0.4.33" \ No newline at end of file diff --git a/src/cli.rs b/src/cli.rs index 0b595a7e3..6571c5538 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -124,12 +124,6 @@ impl TryFrom> for Command { let input_files = process_decompressible_input(input_files)?; - println!( - "{}: attempting to decompress input files into {}", - "info".yellow(), - output_file - ); - let input_files = input_files.into_iter().map(File::from).collect(); return Ok(Command { diff --git a/src/decompressors/decompressor.rs b/src/decompressors/decompressor.rs new file mode 100644 index 000000000..20b1051d9 --- /dev/null +++ b/src/decompressors/decompressor.rs @@ -0,0 +1 @@ +/// This file should/could store a Decompressor trait \ No newline at end of file diff --git a/src/decompressors/mod.rs b/src/decompressors/mod.rs new file mode 100644 index 000000000..8e97713f6 --- /dev/null +++ b/src/decompressors/mod.rs @@ -0,0 +1 @@ +pub mod tar; \ No newline at end of file diff --git a/src/decompressors/tar.rs b/src/decompressors/tar.rs new file mode 100644 index 000000000..b2b146596 --- /dev/null +++ b/src/decompressors/tar.rs @@ -0,0 +1,33 @@ +use std::{fs, path::{Path, PathBuf}}; + +use crate::file::File; +use crate::error::OuchResult; + +use colored::Colorize; +pub struct Decompressor {} + +impl Decompressor { + pub fn decompress(from: &File, into: &Option) -> OuchResult<()> { + let destination_path = match into { + Some(output) => { + // Must be None according to the way command-line arg. parsing in Ouch works + assert_eq!(output.extension, None); + + Path::new(&output.path) + } + None => Path::new(".") + }; + + if !destination_path.exists() { + println!("{}: attempting to create folder {:?}.", "info".yellow(), &destination_path); + std::fs::create_dir_all(destination_path)?; + println!("{}: directory {:#?} created.", "info".yellow(), fs::canonicalize(&destination_path)?); + } + + + + + + Ok(()) + } +} \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index 2ca7502dd..80d4c1bf0 100644 --- a/src/error.rs +++ b/src/error.rs @@ -9,6 +9,7 @@ pub enum Error { // TODO: get rid of this error variant InvalidUnicode, InvalidInput, + IOError, InputsMustHaveBeenDecompressible(String), } @@ -32,8 +33,20 @@ impl fmt::Display for Error { } _ => { // TODO - write!(f, "") + write!(f, "todo: missing description for error") } } } } + + +impl From for Error { + fn from(err: std::io::Error) -> Self { + // Ideally I'd store `err` as a variant of ouch's Error + // but I need Error to have Eq, which std::io::Error does not + // implement. + println!("{}: {:#?}", "error".red(), err); + + Self::IOError + } +} \ No newline at end of file diff --git a/src/evaluator.rs b/src/evaluator.rs index 1f5eea1c3..797a15861 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -1,8 +1,8 @@ -use std::{convert::TryFrom, path::{PathBuf}}; use colored::Colorize; use crate::{cli::{Command, CommandKind}, error, extension::CompressionFormat, file::File}; +use crate::decompressors::tar; pub struct Evaluator { command: Command, @@ -10,53 +10,48 @@ pub struct Evaluator { } impl Evaluator { + // todo: remove this? pub fn new(command: Command) -> Self { Self { command } } - fn handle_compression(files_to_compress: &[PathBuf], output_file: &Option) { - - } - - fn decompress_file(mut filename: &PathBuf, mut extension: CompressionFormat, output_file: &Option) -> error::OuchResult<()> { - loop { - println!("{}: attempting to decompress '{:?}'", "ouch".bright_blue(), filename); + fn decompress_file(&self, file: &File) -> error::OuchResult<()> { + println!("{}: attempting to decompress {:?}", "ouch".bright_blue(), file.path); + if file.extension.is_none() { + // This block *should* be unreachable + eprintln!("{}: reached Evaluator::decompress_file without known extension.", "internal error".red()); + return Err(error::Error::InvalidInput); } - } + let extension = file.extension.clone().unwrap(); + let output_file = &self.command.output; - fn handle_decompression(files_to_decompress: &[File], output_file: &Option) { - // for (filename, extension) in files_to_decompress { - // // println!("file: {:?}, extension: {:?}", filename, extension); - - // // TODO: actually decompress anything ;-; - - // // Once decompressed, check if the file can be decompressed further - // // e.g.: "foobar.tar.gz" -> "foobar.tar" + match extension.second_ext { + CompressionFormat::Tar => tar::Decompressor::decompress(file, output_file)?, + _ => { + todo!() + } + } - + // TODO: decompress first extension - // let filename: &PathBuf = &filename.as_path().file_stem().unwrap().into(); - // match CompressionFormat::try_from(filename) { - // Ok(extension) => { - // println!("{}: attempting to decompress {:?}, ext: {:?}", "info".yellow(), filename, extension); - // }, - // Err(err) => { - // continue; - // } - // } - // } + Ok(()) } - pub fn evaluate(&mut self) { + pub fn evaluate(&mut self) -> error::OuchResult<()> { match &self.command.kind { CommandKind::Compression(files_to_compress) => { - Evaluator::handle_compression(files_to_compress, &self.command.output); + for _file in files_to_compress { + todo!(); + } } CommandKind::Decompression(files_to_decompress) => { - Evaluator::handle_decompression(files_to_decompress, &self.command.output); + for file in files_to_decompress { + self.decompress_file(file)?; + } } } + Ok(()) } } \ No newline at end of file diff --git a/src/extension.rs b/src/extension.rs index ff22cf1fe..a2790417e 100644 --- a/src/extension.rs +++ b/src/extension.rs @@ -6,10 +6,10 @@ use CompressionFormat::*; /// 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 } -#[derive(Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Extension { - first_ext: Option, - second_ext: CompressionFormat + pub first_ext: Option, + pub second_ext: CompressionFormat } impl From for Extension { @@ -86,7 +86,7 @@ pub fn get_extension_from_filename(filename: &str) -> Option<(&str, &str)> { } } -#[derive(PartialEq, Eq, Debug)] +#[derive(Clone, PartialEq, Eq, Debug)] /// Accepted extensions for input and output pub enum CompressionFormat { // .gz diff --git a/src/main.rs b/src/main.rs index 8977925ae..06744633f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ -use std::{convert::TryFrom}; +use std::{convert::TryFrom, fs::File, path::{Path, PathBuf}}; use colored::Colorize; +use tar::Archive; mod cli; mod error; @@ -9,15 +10,36 @@ mod file; mod test; mod evaluator; +mod decompressors; + fn main() { - let matches = cli::get_matches(); - match cli::Command::try_from(matches) { - Ok(command) => { - let mut eval = evaluator::Evaluator::new(command); - eval.evaluate(); - } - Err(err) => { - print!("{}: {}\n", "error".red(), err); - } + // let matches = cli::get_matches(); + // match cli::Command::try_from(matches) { + // Ok(command) => { + // let mut eval = evaluator::Evaluator::new(command); + // eval.evaluate(); + // } + // Err(err) => { + // print!("{}: {}\n", "error".red(), err); + // } + // } + + + // Testing tar unarchival + let file = File::open("ouch.tar").unwrap(); + let mut a = Archive::new(file); + + for file in a.entries().unwrap() { + // Make sure there wasn't an I/O error + let mut file = file.unwrap(); + + let path = if let Ok(path) = file.path() { + path + } else { + continue; + }; + let name = path.to_string_lossy(); + + file.unpack(format!("{}", name)).unwrap(); } } From d351a8ef7bc91c66ec17724b073147c271784913 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Sun, 21 Mar 2021 04:23:00 -0300 Subject: [PATCH 09/67] decompressors.tar: now working! --- src/decompressors/tar.rs | 40 +++++++++++++++++++++++++++++---------- src/main.rs | 41 ++++++++++++---------------------------- 2 files changed, 42 insertions(+), 39 deletions(-) diff --git a/src/decompressors/tar.rs b/src/decompressors/tar.rs index b2b146596..94eb59f0c 100644 --- a/src/decompressors/tar.rs +++ b/src/decompressors/tar.rs @@ -1,33 +1,53 @@ -use std::{fs, path::{Path, PathBuf}}; +use std::{ + fs, + path::{Path, PathBuf}, +}; + +use colored::Colorize; +use tar; -use crate::file::File; use crate::error::OuchResult; +use crate::file::File; -use colored::Colorize; pub struct Decompressor {} impl Decompressor { - pub fn decompress(from: &File, into: &Option) -> OuchResult<()> { + pub fn decompress(from: &File, into: &Option) -> OuchResult<()> { let destination_path = match into { Some(output) => { // Must be None according to the way command-line arg. parsing in Ouch works assert_eq!(output.extension, None); - + Path::new(&output.path) } - None => Path::new(".") + None => Path::new("."), }; if !destination_path.exists() { - println!("{}: attempting to create folder {:?}.", "info".yellow(), &destination_path); + println!( + "{}: attempting to create folder {:?}.", + "info".yellow(), + &destination_path + ); std::fs::create_dir_all(destination_path)?; - println!("{}: directory {:#?} created.", "info".yellow(), fs::canonicalize(&destination_path)?); + println!( + "{}: directory {:#?} created.", + "info".yellow(), + fs::canonicalize(&destination_path)? + ); } + let file = fs::File::open(&from.path)?; + let mut archive = tar::Archive::new(file); + for file in archive.entries().unwrap() { + let mut file = file?; - + file.unpack_in(destination_path)?; + + // TODO: save all unpacked files into a 'transaction' metadata + } Ok(()) } -} \ No newline at end of file +} diff --git a/src/main.rs b/src/main.rs index 06744633f..fca88cf82 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ use std::{convert::TryFrom, fs::File, path::{Path, PathBuf}}; use colored::Colorize; +use error::{Error, OuchResult}; use tar::Archive; mod cli; @@ -12,34 +13,16 @@ mod evaluator; mod decompressors; -fn main() { - // let matches = cli::get_matches(); - // match cli::Command::try_from(matches) { - // Ok(command) => { - // let mut eval = evaluator::Evaluator::new(command); - // eval.evaluate(); - // } - // Err(err) => { - // print!("{}: {}\n", "error".red(), err); - // } - // } - - - // Testing tar unarchival - let file = File::open("ouch.tar").unwrap(); - let mut a = Archive::new(file); - - for file in a.entries().unwrap() { - // Make sure there wasn't an I/O error - let mut file = file.unwrap(); - - let path = if let Ok(path) = file.path() { - path - } else { - continue; - }; - let name = path.to_string_lossy(); - - file.unpack(format!("{}", name)).unwrap(); +fn main() -> OuchResult<()>{ + let matches = cli::get_matches(); + match cli::Command::try_from(matches) { + Ok(command) => { + let mut eval = evaluator::Evaluator::new(command); + eval.evaluate()?; + } + Err(err) => { + print!("{}: {}\n", "error".red(), err); + } } + Ok(()) } From 8d361206903a8ca152f8073d58eaaeee17b2bc0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Sun, 21 Mar 2021 14:03:15 -0300 Subject: [PATCH 10/67] refactor decompressors/tar.rs --- src/decompressors/tar.rs | 57 ++++++++++++++++++++++++++-------------- src/evaluator.rs | 4 ++- 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/src/decompressors/tar.rs b/src/decompressors/tar.rs index 94eb59f0c..88d2a50b5 100644 --- a/src/decompressors/tar.rs +++ b/src/decompressors/tar.rs @@ -12,42 +12,59 @@ use crate::file::File; pub struct Decompressor {} impl Decompressor { - pub fn decompress(from: &File, into: &Option) -> OuchResult<()> { - let destination_path = match into { - Some(output) => { - // Must be None according to the way command-line arg. parsing in Ouch works - assert_eq!(output.extension, None); - Path::new(&output.path) - } - None => Path::new("."), - }; - - if !destination_path.exists() { + fn create_path_if_non_existent(path: &Path) -> OuchResult<()> { + if !path.exists() { println!( "{}: attempting to create folder {:?}.", "info".yellow(), - &destination_path + &path ); - std::fs::create_dir_all(destination_path)?; + std::fs::create_dir_all(path)?; println!( "{}: directory {:#?} created.", "info".yellow(), - fs::canonicalize(&destination_path)? + fs::canonicalize(&path)? ); } + Ok(()) + } + + fn unpack_files(from: &Path, into: &Path) -> OuchResult> { - let file = fs::File::open(&from.path)?; + let mut files_unpacked = vec![]; + + let file = fs::File::open(from)?; let mut archive = tar::Archive::new(file); - for file in archive.entries().unwrap() { + for file in archive.entries()? { let mut file = file?; - file.unpack_in(destination_path)?; - - // TODO: save all unpacked files into a 'transaction' metadata + // TODO: check if file/folder already exists and ask user's permission for overwriting + file.unpack_in(into)?; + + let file_path = fs::canonicalize(into.join(file.path()?))?; + files_unpacked.push(file_path); } - Ok(()) + Ok(files_unpacked) + } + + pub fn decompress(from: &File, into: &Option) -> OuchResult> { + let destination_path = match into { + Some(output) => { + // Must be None according to the way command-line arg. parsing in Ouch works + assert_eq!(output.extension, None); + + Path::new(&output.path) + } + None => Path::new("."), + }; + + Self::create_path_if_non_existent(destination_path)?; + + let files_unpacked = Self::unpack_files(&from.path, destination_path)?; + + Ok(files_unpacked) } } diff --git a/src/evaluator.rs b/src/evaluator.rs index 797a15861..ff4af4c4a 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -28,7 +28,9 @@ impl Evaluator { let output_file = &self.command.output; match extension.second_ext { - CompressionFormat::Tar => tar::Decompressor::decompress(file, output_file)?, + CompressionFormat::Tar => { + let _ = tar::Decompressor::decompress(file, output_file)?; + }, _ => { todo!() } From 837a6a6a572194d3da8a61467696ea390ce31725 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Sun, 21 Mar 2021 15:23:00 -0300 Subject: [PATCH 11/67] Get Decompressors as Trait Objects, start working on Zip decompressor --- src/decompressors/decompressor.rs | 10 +++++++++- src/decompressors/mod.rs | 7 ++++++- src/decompressors/tar.rs | 12 ++++++++---- src/decompressors/zip.rs | 20 ++++++++++++++++++++ src/evaluator.rs | 28 ++++++++++++++++++---------- 5 files changed, 61 insertions(+), 16 deletions(-) create mode 100644 src/decompressors/zip.rs diff --git a/src/decompressors/decompressor.rs b/src/decompressors/decompressor.rs index 20b1051d9..c160216bb 100644 --- a/src/decompressors/decompressor.rs +++ b/src/decompressors/decompressor.rs @@ -1 +1,9 @@ -/// This file should/could store a Decompressor trait \ No newline at end of file +use std::path::PathBuf; + +use crate::{error::OuchResult, file::File}; + +/// This file should/could store a Decompressor trait + +pub trait Decompressor { + fn decompress(&self, from: &File, into: &Option) -> OuchResult>; +} \ No newline at end of file diff --git a/src/decompressors/mod.rs b/src/decompressors/mod.rs index 8e97713f6..8f9fa4b2a 100644 --- a/src/decompressors/mod.rs +++ b/src/decompressors/mod.rs @@ -1 +1,6 @@ -pub mod tar; \ No newline at end of file +mod decompressor; +mod tar; +mod zip; + +pub use decompressor::Decompressor; +pub use self::tar::TarDecompressor; diff --git a/src/decompressors/tar.rs b/src/decompressors/tar.rs index 88d2a50b5..7787fc6d8 100644 --- a/src/decompressors/tar.rs +++ b/src/decompressors/tar.rs @@ -9,9 +9,11 @@ use tar; use crate::error::OuchResult; use crate::file::File; -pub struct Decompressor {} +use super::decompressor::Decompressor; -impl Decompressor { +pub struct TarDecompressor {} + +impl TarDecompressor { fn create_path_if_non_existent(path: &Path) -> OuchResult<()> { if !path.exists() { @@ -49,8 +51,10 @@ impl Decompressor { Ok(files_unpacked) } +} - pub fn decompress(from: &File, into: &Option) -> OuchResult> { +impl Decompressor for TarDecompressor { + fn decompress(&self, from: &File, into: &Option) -> OuchResult> { let destination_path = match into { Some(output) => { // Must be None according to the way command-line arg. parsing in Ouch works @@ -67,4 +71,4 @@ impl Decompressor { Ok(files_unpacked) } -} +} \ No newline at end of file diff --git a/src/decompressors/zip.rs b/src/decompressors/zip.rs new file mode 100644 index 000000000..20e094d9b --- /dev/null +++ b/src/decompressors/zip.rs @@ -0,0 +1,20 @@ +use std::{ + fs, + path::{Path, PathBuf}, +}; + +use colored::Colorize; +use zip; + +use crate::error::OuchResult; +use crate::file::File; + +use super::decompressor::Decompressor; + +pub struct ZipDecompressor {} + +impl Decompressor for ZipDecompressor { + fn decompress(&self, from: &File, into: &Option) -> OuchResult> { + todo!() + } +} \ No newline at end of file diff --git a/src/evaluator.rs b/src/evaluator.rs index ff4af4c4a..90336f07e 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -2,7 +2,8 @@ use colored::Colorize; use crate::{cli::{Command, CommandKind}, error, extension::CompressionFormat, file::File}; -use crate::decompressors::tar; +use crate::decompressors::TarDecompressor; +use crate::decompressors::Decompressor; pub struct Evaluator { command: Command, @@ -17,26 +18,33 @@ impl Evaluator { } } - fn decompress_file(&self, file: &File) -> error::OuchResult<()> { - println!("{}: attempting to decompress {:?}", "ouch".bright_blue(), file.path); + fn get_decompressor(&self, file: &File) -> error::OuchResult> { if file.extension.is_none() { // This block *should* be unreachable - eprintln!("{}: reached Evaluator::decompress_file without known extension.", "internal error".red()); + eprintln!("{}: reached Evaluator::get_decompressor without known extension.", "internal error".red()); return Err(error::Error::InvalidInput); } let extension = file.extension.clone().unwrap(); - let output_file = &self.command.output; - - match extension.second_ext { + let decompressor = match extension.second_ext { CompressionFormat::Tar => { - let _ = tar::Decompressor::decompress(file, output_file)?; + Box::new(TarDecompressor{}) }, _ => { todo!() } - } + }; + + + Ok(decompressor) + } + + fn decompress_file(&self, file: &File) -> error::OuchResult<()> { + println!("{}: attempting to decompress {:?}", "ouch".bright_blue(), file.path); + let output_file = &self.command.output; + let decompressor = self.get_decompressor(file)?; + let files_unpacked = decompressor.decompress(file, output_file)?; - // TODO: decompress first extension + // TODO: decompress the first extension if it exists Ok(()) } From 0a81384dd82e891df79e5664da63c87e89caab55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Sun, 21 Mar 2021 15:41:05 -0300 Subject: [PATCH 12/67] (Small) Zip decompressor progress --- src/decompressors/mod.rs | 1 + src/decompressors/tar.rs | 31 +++---------------------------- src/decompressors/zip.rs | 19 +++++++++++++++++-- src/evaluator.rs | 6 +++++- src/extension.rs | 39 ++++++++++++++++++++------------------- src/file.rs | 2 +- src/main.rs | 8 ++++---- src/test.rs | 2 +- src/utils.rs | 33 +++++++++++++++++++++++++++++++++ 9 files changed, 85 insertions(+), 56 deletions(-) create mode 100644 src/utils.rs diff --git a/src/decompressors/mod.rs b/src/decompressors/mod.rs index 8f9fa4b2a..4edba7ca2 100644 --- a/src/decompressors/mod.rs +++ b/src/decompressors/mod.rs @@ -4,3 +4,4 @@ mod zip; pub use decompressor::Decompressor; pub use self::tar::TarDecompressor; +pub use self::zip::ZipDecompressor; \ No newline at end of file diff --git a/src/decompressors/tar.rs b/src/decompressors/tar.rs index 7787fc6d8..1bc1ec873 100644 --- a/src/decompressors/tar.rs +++ b/src/decompressors/tar.rs @@ -6,7 +6,7 @@ use std::{ use colored::Colorize; use tar; -use crate::error::OuchResult; +use crate::{error::OuchResult, utils}; use crate::file::File; use super::decompressor::Decompressor; @@ -15,23 +15,6 @@ pub struct TarDecompressor {} impl TarDecompressor { - fn create_path_if_non_existent(path: &Path) -> OuchResult<()> { - if !path.exists() { - println!( - "{}: attempting to create folder {:?}.", - "info".yellow(), - &path - ); - std::fs::create_dir_all(path)?; - println!( - "{}: directory {:#?} created.", - "info".yellow(), - fs::canonicalize(&path)? - ); - } - Ok(()) - } - fn unpack_files(from: &Path, into: &Path) -> OuchResult> { let mut files_unpacked = vec![]; @@ -55,17 +38,9 @@ impl TarDecompressor { impl Decompressor for TarDecompressor { fn decompress(&self, from: &File, into: &Option) -> OuchResult> { - let destination_path = match into { - Some(output) => { - // Must be None according to the way command-line arg. parsing in Ouch works - assert_eq!(output.extension, None); - - Path::new(&output.path) - } - None => Path::new("."), - }; + let destination_path = utils::get_destination_path(into); - Self::create_path_if_non_existent(destination_path)?; + utils::create_path_if_non_existent(destination_path)?; let files_unpacked = Self::unpack_files(&from.path, destination_path)?; diff --git a/src/decompressors/zip.rs b/src/decompressors/zip.rs index 20e094d9b..0cdf48d92 100644 --- a/src/decompressors/zip.rs +++ b/src/decompressors/zip.rs @@ -6,15 +6,30 @@ use std::{ use colored::Colorize; use zip; -use crate::error::OuchResult; +use crate::{error::{self, OuchResult}, utils}; use crate::file::File; use super::decompressor::Decompressor; pub struct ZipDecompressor {} +impl ZipDecompressor { + fn unpack_files(from: &Path, into: &Path) -> OuchResult> { + // placeholder return + Err(error::Error::IOError) + } +} + + impl Decompressor for ZipDecompressor { fn decompress(&self, from: &File, into: &Option) -> OuchResult> { - todo!() + let destination_path = utils::get_destination_path(into); + + utils::create_path_if_non_existent(destination_path)?; + + let files_unpacked = Self::unpack_files(&from.path, destination_path)?; + + // placeholder return + Err(error::Error::IOError) } } \ No newline at end of file diff --git a/src/evaluator.rs b/src/evaluator.rs index 90336f07e..b9927050a 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -2,8 +2,9 @@ use colored::Colorize; use crate::{cli::{Command, CommandKind}, error, extension::CompressionFormat, file::File}; -use crate::decompressors::TarDecompressor; use crate::decompressors::Decompressor; +use crate::decompressors::TarDecompressor; +use crate::decompressors::ZipDecompressor; pub struct Evaluator { command: Command, @@ -29,6 +30,9 @@ impl Evaluator { CompressionFormat::Tar => { Box::new(TarDecompressor{}) }, + // CompressionFormat::Zip => { + // Box::new(ZipDecompressor{}) + // } _ => { todo!() } diff --git a/src/extension.rs b/src/extension.rs index a2790417e..d4880a901 100644 --- a/src/extension.rs +++ b/src/extension.rs @@ -12,6 +12,26 @@ pub struct Extension { pub second_ext: CompressionFormat } +pub fn get_extension_from_filename(filename: &str) -> Option<(&str, &str)> { + let path = Path::new(filename); + + let ext = path + .extension() + .and_then(OsStr::to_str)?; + + let previous_extension = path + .file_stem() + .and_then(OsStr::to_str) + .and_then(get_extension_from_filename); + + if let Some((_, prev)) = previous_extension { + Some((prev, ext)) + } else { + Some(("", ext)) + } +} + + impl From for Extension { fn from(second_ext: CompressionFormat) -> Self { Self { @@ -67,25 +87,6 @@ impl Extension { } } -pub fn get_extension_from_filename(filename: &str) -> Option<(&str, &str)> { - let path = Path::new(filename); - - let ext = path - .extension() - .and_then(OsStr::to_str)?; - - let previous_extension = path - .file_stem() - .and_then(OsStr::to_str) - .and_then(get_extension_from_filename); - - if let Some((_, prev)) = previous_extension { - Some((prev, ext)) - } else { - Some(("", ext)) - } -} - #[derive(Clone, PartialEq, Eq, Debug)] /// Accepted extensions for input and output pub enum CompressionFormat { diff --git a/src/file.rs b/src/file.rs index 327e1b95d..7066934ca 100644 --- a/src/file.rs +++ b/src/file.rs @@ -1,4 +1,4 @@ -use std::{path::PathBuf, process::Command}; +use std::path::PathBuf; use crate::extension::{CompressionFormat, Extension}; diff --git a/src/main.rs b/src/main.rs index fca88cf82..2b2bac52c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,6 @@ -use std::{convert::TryFrom, fs::File, path::{Path, PathBuf}}; +use std::convert::TryFrom; use colored::Colorize; -use error::{Error, OuchResult}; -use tar::Archive; mod cli; mod error; @@ -10,9 +8,11 @@ mod extension; mod file; mod test; mod evaluator; - +mod utils; mod decompressors; +use error::OuchResult; + fn main() -> OuchResult<()>{ let matches = cli::get_matches(); match cli::Command::try_from(matches) { diff --git a/src/test.rs b/src/test.rs index e6364a76b..401991a5e 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,7 +1,7 @@ #[cfg(test)] mod cli { - use crate::{cli::clap_app, extension}; + use crate::cli::clap_app; use crate::cli::Command; use crate::cli::CommandKind::*; use crate::error::OuchResult; diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 000000000..82babb946 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,33 @@ +use std::{fs, path::Path}; + +use colored::Colorize; +use crate::{error::OuchResult, file::File}; + +pub (crate) fn create_path_if_non_existent(path: &Path) -> OuchResult<()> { + if !path.exists() { + println!( + "{}: attempting to create folder {:?}.", + "info".yellow(), + &path + ); + std::fs::create_dir_all(path)?; + println!( + "{}: directory {:#?} created.", + "info".yellow(), + fs::canonicalize(&path)? + ); + } + Ok(()) +} + +pub (crate) fn get_destination_path(dest: &Option) -> &Path { + match dest { + Some(output) => { + // Must be None according to the way command-line arg. parsing in Ouch works + assert_eq!(output.extension, None); + + Path::new(&output.path) + } + None => Path::new("."), + } +} \ No newline at end of file From e705024c61698171a1d2a3a8f8469694937c2d75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Sun, 21 Mar 2021 19:11:27 -0300 Subject: [PATCH 13/67] decompressors.zip: now working --- src/decompressors/zip.rs | 61 +++++++++++++++++++++++++++++++++++----- src/error.rs | 45 +++++++++++++++++++++-------- src/evaluator.rs | 9 +++--- src/main.rs | 10 +++++-- src/utils.rs | 2 +- 5 files changed, 100 insertions(+), 27 deletions(-) diff --git a/src/decompressors/zip.rs b/src/decompressors/zip.rs index 0cdf48d92..ba3ec8513 100644 --- a/src/decompressors/zip.rs +++ b/src/decompressors/zip.rs @@ -1,10 +1,7 @@ -use std::{ - fs, - path::{Path, PathBuf}, -}; +use std::{fs, io, path::{Path, PathBuf}}; use colored::Colorize; -use zip; +use zip::{self, read::ZipFile}; use crate::{error::{self, OuchResult}, utils}; use crate::file::File; @@ -14,9 +11,59 @@ use super::decompressor::Decompressor; pub struct ZipDecompressor {} impl ZipDecompressor { + + fn check_for_comments(file: &ZipFile) { + let comment = file.comment(); + if !comment.is_empty() { + println!("{}: Comment in {}: {}", "info".yellow(), file.name(), comment); + } + } + fn unpack_files(from: &Path, into: &Path) -> OuchResult> { + + let mut unpacked_files = vec![]; + // placeholder return - Err(error::Error::IOError) + println!("{}: attempting to decompress {:?}", "ouch".bright_blue(), from); + + let file = fs::File::open(from)?; + let mut archive = zip::ZipArchive::new(file)?; + + for idx in 0..archive.len() { + let mut file = archive.by_index(idx)?; + let file_path = match file.enclosed_name() { + Some(path) => path.to_owned(), + None => continue, + }; + + let file_path = into.join(file_path); + + Self::check_for_comments(&file); + + if (&*file.name()).ends_with('/') { + println!("File {} extracted to \"{}\"", idx, file_path.display()); + fs::create_dir_all(&file_path)?; + } else { + println!( + "{}: \"{}\" extracted. ({} bytes)", + "info".yellow(), + file_path.display(), + file.size() + ); + if let Some(p) = file_path.parent() { + if !p.exists() { + fs::create_dir_all(&p).unwrap(); + } + } + let mut outfile = fs::File::create(&file_path).unwrap(); + io::copy(&mut file, &mut outfile).unwrap(); + } + + let file_path = fs::canonicalize(file_path.clone())?; + unpacked_files.push(file_path); + } + + Ok(unpacked_files) } } @@ -30,6 +77,6 @@ impl Decompressor for ZipDecompressor { let files_unpacked = Self::unpack_files(&from.path, destination_path)?; // placeholder return - Err(error::Error::IOError) + Ok(files_unpacked) } } \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index 80d4c1bf0..16bac57e3 100644 --- a/src/error.rs +++ b/src/error.rs @@ -10,43 +10,64 @@ pub enum Error { InvalidUnicode, InvalidInput, IOError, + FileNotFound, + AlreadyExists, + InvalidZipArchive(&'static str), + PermissionDenied, + UnsupportedZipArchive(&'static str), InputsMustHaveBeenDecompressible(String), } -// This should be placed somewhere else pub type OuchResult = Result; impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use Error::*; match self { - InvalidInput => write!( + Error::InvalidInput => write!( f, "When `-o/--output` is omitted, all input files should be compressed files." ), Error::MissingExtensionError(filename) => { write!(f, "cannot compress to \'{}\', likely because it has an unsupported (or missing) extension.", filename) - } + }, Error::InputsMustHaveBeenDecompressible(file) => { write!(f, "file '{}' is not decompressible", file.red()) + }, + // TODO: find out a way to attach the missing file in question here + Error::FileNotFound => { + write!(f, "file not found!") } - _ => { + err => { // TODO - write!(f, "todo: missing description for error") + write!(f, "todo: missing description for error {:?}", err) } } } } - impl From for Error { fn from(err: std::io::Error) -> Self { - // Ideally I'd store `err` as a variant of ouch's Error - // but I need Error to have Eq, which std::io::Error does not - // implement. - println!("{}: {:#?}", "error".red(), err); + match err.kind() { + std::io::ErrorKind::NotFound => Self::FileNotFound, + std::io::ErrorKind::PermissionDenied => Self::PermissionDenied, + std::io::ErrorKind::AlreadyExists => Self::AlreadyExists, + _other => { + println!("{}: {:#?}", "IO error".red(), err); + Self::IOError + } + } + } +} - Self::IOError +impl From for Error { + fn from(err: zip::result::ZipError) -> Self { + use zip::result::ZipError::*; + match err { + Io(io_err) => Self::from(io_err), + InvalidArchive(filename) => Self::InvalidZipArchive(filename), + FileNotFound => Self::FileNotFound, + UnsupportedArchive(filename) => Self::UnsupportedZipArchive(filename) + } } } \ No newline at end of file diff --git a/src/evaluator.rs b/src/evaluator.rs index b9927050a..bdb6d44ab 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -26,13 +26,13 @@ impl Evaluator { return Err(error::Error::InvalidInput); } let extension = file.extension.clone().unwrap(); - let decompressor = match extension.second_ext { + let decompressor: Box = match extension.second_ext { CompressionFormat::Tar => { Box::new(TarDecompressor{}) }, - // CompressionFormat::Zip => { - // Box::new(ZipDecompressor{}) - // } + CompressionFormat::Zip => { + Box::new(ZipDecompressor{}) + } _ => { todo!() } @@ -43,7 +43,6 @@ impl Evaluator { } fn decompress_file(&self, file: &File) -> error::OuchResult<()> { - println!("{}: attempting to decompress {:?}", "ouch".bright_blue(), file.path); let output_file = &self.command.output; let decompressor = self.get_decompressor(file)?; let files_unpacked = decompressor.decompress(file, output_file)?; diff --git a/src/main.rs b/src/main.rs index 2b2bac52c..ab6775650 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,14 +14,20 @@ mod decompressors; use error::OuchResult; fn main() -> OuchResult<()>{ + let print_error = |err| { + println!("{}: {}", "error".red(), err); + }; let matches = cli::get_matches(); match cli::Command::try_from(matches) { Ok(command) => { let mut eval = evaluator::Evaluator::new(command); - eval.evaluate()?; + match eval.evaluate() { + Ok(_) => {}, + Err(err) => print_error(err) + } } Err(err) => { - print!("{}: {}\n", "error".red(), err); + print_error(err) } } Ok(()) diff --git a/src/utils.rs b/src/utils.rs index 82babb946..e6d1d1f07 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,4 @@ -use std::{fs, path::Path}; +use std::{fs, path::{Component, Path, PathBuf}}; use colored::Colorize; use crate::{error::OuchResult, file::File}; From 23c8f567fcfe6d0756eae70a603a879f78ba0f23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Sun, 21 Mar 2021 19:18:32 -0300 Subject: [PATCH 14/67] Minor changes in decompressors/zip.rs --- src/decompressors/zip.rs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/decompressors/zip.rs b/src/decompressors/zip.rs index ba3ec8513..cfdd4c904 100644 --- a/src/decompressors/zip.rs +++ b/src/decompressors/zip.rs @@ -40,23 +40,25 @@ impl ZipDecompressor { Self::check_for_comments(&file); - if (&*file.name()).ends_with('/') { + let is_dir = (&*file.name()).ends_with('/'); + + if is_dir { println!("File {} extracted to \"{}\"", idx, file_path.display()); fs::create_dir_all(&file_path)?; } else { + if let Some(p) = file_path.parent() { + if !p.exists() { + fs::create_dir_all(&p)?; + } + } println!( "{}: \"{}\" extracted. ({} bytes)", "info".yellow(), file_path.display(), file.size() ); - if let Some(p) = file_path.parent() { - if !p.exists() { - fs::create_dir_all(&p).unwrap(); - } - } - let mut outfile = fs::File::create(&file_path).unwrap(); - io::copy(&mut file, &mut outfile).unwrap(); + let mut outfile = fs::File::create(&file_path)?; + io::copy(&mut file, &mut outfile)?; } let file_path = fs::canonicalize(file_path.clone())?; @@ -76,7 +78,6 @@ impl Decompressor for ZipDecompressor { let files_unpacked = Self::unpack_files(&from.path, destination_path)?; - // placeholder return Ok(files_unpacked) } } \ No newline at end of file From 6c7d24084f5ea1c8bf874965570c07e3e80da555 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Sun, 21 Mar 2021 21:42:13 -0300 Subject: [PATCH 15/67] decompressors.tar: Add info messages --- src/decompressors/tar.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/decompressors/tar.rs b/src/decompressors/tar.rs index 1bc1ec873..c7a39cf99 100644 --- a/src/decompressors/tar.rs +++ b/src/decompressors/tar.rs @@ -17,6 +17,7 @@ impl TarDecompressor { fn unpack_files(from: &Path, into: &Path) -> OuchResult> { + println!("{}: attempting to decompress {:?}", "ouch".bright_blue(), from); let mut files_unpacked = vec![]; let file = fs::File::open(from)?; @@ -27,7 +28,14 @@ impl TarDecompressor { // TODO: check if file/folder already exists and ask user's permission for overwriting file.unpack_in(into)?; - + + println!( + "{}: {:?} extracted. ({} bytes)", + "info".yellow(), + into.join(file.path()?), + file.size() + ); + let file_path = fs::canonicalize(into.join(file.path()?))?; files_unpacked.push(file_path); } From 90ad9d8f8ae2c5ed427937c49299793641c83a26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Mon, 22 Mar 2021 02:18:35 -0300 Subject: [PATCH 16/67] WIP decompression support for .xz, .bz, .lzma --- src/decompressors/decompressor.rs | 7 +- src/decompressors/mod.rs | 5 +- src/decompressors/niffler.rs | 54 +++++++++++++++ src/decompressors/tar.rs | 6 +- src/decompressors/zip.rs | 8 ++- src/error.rs | 16 +++++ src/evaluator.rs | 111 ++++++++++++++++++++++++------ src/extension.rs | 2 +- 8 files changed, 178 insertions(+), 31 deletions(-) create mode 100644 src/decompressors/niffler.rs diff --git a/src/decompressors/decompressor.rs b/src/decompressors/decompressor.rs index c160216bb..b6a734102 100644 --- a/src/decompressors/decompressor.rs +++ b/src/decompressors/decompressor.rs @@ -2,8 +2,11 @@ use std::path::PathBuf; use crate::{error::OuchResult, file::File}; -/// This file should/could store a Decompressor trait +pub enum DecompressionResult { + FilesUnpacked(Vec), + FileInMemory(Vec) +} pub trait Decompressor { - fn decompress(&self, from: &File, into: &Option) -> OuchResult>; + fn decompress(&self, from: &File, into: &Option) -> OuchResult; } \ No newline at end of file diff --git a/src/decompressors/mod.rs b/src/decompressors/mod.rs index 4edba7ca2..8c3880af5 100644 --- a/src/decompressors/mod.rs +++ b/src/decompressors/mod.rs @@ -1,7 +1,10 @@ mod decompressor; mod tar; mod zip; +mod niffler; pub use decompressor::Decompressor; +pub use decompressor::DecompressionResult; pub use self::tar::TarDecompressor; -pub use self::zip::ZipDecompressor; \ No newline at end of file +pub use self::zip::ZipDecompressor; +pub use self::niffler::NifflerDecompressor; \ No newline at end of file diff --git a/src/decompressors/niffler.rs b/src/decompressors/niffler.rs new file mode 100644 index 000000000..d51be0621 --- /dev/null +++ b/src/decompressors/niffler.rs @@ -0,0 +1,54 @@ +use std::{io::Read, path::Path}; + +use colored::Colorize; +use niffler; + +use crate::file::File; +use crate::{ + error::{self, OuchResult}, + utils, +}; + +use super::decompressor::Decompressor; +use super::decompressor::DecompressionResult; + +pub struct NifflerDecompressor {} + +impl NifflerDecompressor { + fn unpack_file(from: &Path) -> OuchResult> { + + println!("{}: trying to decompress {:?}", "info".yellow(), from); + + let file = std::fs::read(from)?; + + let (mut reader, compression) = niffler::get_reader(Box::new(&file[..]))?; + + match compression { + niffler::Format::No => { + return Err(error::Error::InvalidInput); + }, + other => { + println!("{}: {:?} detected.", "info".yellow(), other); + } + } + + let mut buffer = Vec::new(); + let bytes_read = reader.read_to_end(&mut buffer)?; + + println!("{}: {:?} extracted into memory ({} bytes).", "info".yellow(), from, bytes_read); + + Ok(buffer) + } +} + +impl Decompressor for NifflerDecompressor { + fn decompress(&self, from: &File, into: &Option) -> OuchResult { + let destination_path = utils::get_destination_path(into); + + utils::create_path_if_non_existent(destination_path)?; + + let bytes = Self::unpack_file(&from.path)?; + + Ok(DecompressionResult::FileInMemory(bytes)) + } +} diff --git a/src/decompressors/tar.rs b/src/decompressors/tar.rs index c7a39cf99..913bdebb0 100644 --- a/src/decompressors/tar.rs +++ b/src/decompressors/tar.rs @@ -9,7 +9,7 @@ use tar; use crate::{error::OuchResult, utils}; use crate::file::File; -use super::decompressor::Decompressor; +use super::decompressor::{DecompressionResult, Decompressor}; pub struct TarDecompressor {} @@ -45,13 +45,13 @@ impl TarDecompressor { } impl Decompressor for TarDecompressor { - fn decompress(&self, from: &File, into: &Option) -> OuchResult> { + fn decompress(&self, from: &File, into: &Option) -> OuchResult { let destination_path = utils::get_destination_path(into); utils::create_path_if_non_existent(destination_path)?; let files_unpacked = Self::unpack_files(&from.path, destination_path)?; - Ok(files_unpacked) + Ok(DecompressionResult::FilesUnpacked(files_unpacked)) } } \ No newline at end of file diff --git a/src/decompressors/zip.rs b/src/decompressors/zip.rs index cfdd4c904..c3ce796c0 100644 --- a/src/decompressors/zip.rs +++ b/src/decompressors/zip.rs @@ -6,7 +6,7 @@ use zip::{self, read::ZipFile}; use crate::{error::{self, OuchResult}, utils}; use crate::file::File; -use super::decompressor::Decompressor; +use super::decompressor::{DecompressionResult, Decompressor}; pub struct ZipDecompressor {} @@ -61,6 +61,8 @@ impl ZipDecompressor { io::copy(&mut file, &mut outfile)?; } + // TODO: check if permissions are correct when on Unix + let file_path = fs::canonicalize(file_path.clone())?; unpacked_files.push(file_path); } @@ -71,13 +73,13 @@ impl ZipDecompressor { impl Decompressor for ZipDecompressor { - fn decompress(&self, from: &File, into: &Option) -> OuchResult> { + fn decompress(&self, from: &File, into: &Option) -> OuchResult { let destination_path = utils::get_destination_path(into); utils::create_path_if_non_existent(destination_path)?; let files_unpacked = Self::unpack_files(&from.path, destination_path)?; - Ok(files_unpacked) + Ok(DecompressionResult::FilesUnpacked(files_unpacked)) } } \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index 16bac57e3..63c6ba4f2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -15,6 +15,7 @@ pub enum Error { InvalidZipArchive(&'static str), PermissionDenied, UnsupportedZipArchive(&'static str), + FileTooShort, InputsMustHaveBeenDecompressible(String), } @@ -70,4 +71,19 @@ impl From for Error { UnsupportedArchive(filename) => Self::UnsupportedZipArchive(filename) } } +} + +impl From for Error { + fn from(err: niffler::error::Error) -> Self { + use niffler::error::Error as NifErr; + match err { + NifErr::FeatureDisabled => { + // Ouch is using Niffler with all its features so + // this should be unreachable. + unreachable!(); + }, + NifErr::FileTooShort => Self::FileTooShort, + NifErr::IOError(io_err) => Self::from(io_err) + } + } } \ No newline at end of file diff --git a/src/evaluator.rs b/src/evaluator.rs index bdb6d44ab..c1a7fbe4a 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -1,12 +1,20 @@ +use std::{ffi::OsStr, fs, io::Write}; use colored::Colorize; -use crate::{cli::{Command, CommandKind}, error, extension::CompressionFormat, file::File}; use crate::decompressors::Decompressor; use crate::decompressors::TarDecompressor; use crate::decompressors::ZipDecompressor; +use crate::{ + cli::{Command, CommandKind}, + decompressors::{DecompressionResult, NifflerDecompressor}, + error::{self, OuchResult}, + extension::CompressionFormat, + file::File, + utils, +}; -pub struct Evaluator { +pub struct Evaluator { command: Command, // verbosity: Verbosity } @@ -14,40 +22,101 @@ pub struct Evaluator { impl Evaluator { // todo: remove this? pub fn new(command: Command) -> Self { - Self { - command - } + Self { command } } - fn get_decompressor(&self, file: &File) -> error::OuchResult> { + fn get_decompressor( + &self, + file: &File, + ) -> error::OuchResult<(Option>, Box)> { if file.extension.is_none() { // This block *should* be unreachable - eprintln!("{}: reached Evaluator::get_decompressor without known extension.", "internal error".red()); + eprintln!( + "{}: reached Evaluator::get_decompressor without known extension.", + "internal error".red() + ); return Err(error::Error::InvalidInput); } let extension = file.extension.clone().unwrap(); - let decompressor: Box = match extension.second_ext { - CompressionFormat::Tar => { - Box::new(TarDecompressor{}) - }, - CompressionFormat::Zip => { - Box::new(ZipDecompressor{}) - } - _ => { - todo!() + + let decompressor_from_format = |ext| -> Box { + match ext { + CompressionFormat::Tar => Box::new(TarDecompressor {}), + + CompressionFormat::Zip => Box::new(ZipDecompressor {}), + + CompressionFormat::Gzip | CompressionFormat::Lzma | CompressionFormat::Bzip => { + Box::new(NifflerDecompressor {}) + } } }; + let second_decompressor = decompressor_from_format(extension.second_ext); + + let first_decompressor = match extension.first_ext { + Some(ext) => Some(decompressor_from_format(ext)), + None => None, + }; + + Ok((first_decompressor, second_decompressor)) + } + + // todo: move this folder into decompressors/ later on + fn decompress_file_in_memory( + bytes: Vec, + file: &File, + decompressor: Option>, + output_file: &Option, + ) -> OuchResult<()> { + + let output_file = utils::get_destination_path(output_file); + + let mut filename = file.path.file_stem().unwrap_or(output_file.as_os_str()); + if filename == "." { + // I believe this is only possible when the supplied inout has a name + // of the sort `.tar` or `.zip' and no output has been supplied. + filename = OsStr::new("ouch-output"); + } + + if decompressor.is_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. - Ok(decompressor) + println!("{}: saving to {:?}.", "info".yellow(), filename); + + let mut f = fs::File::create(output_file.join(filename))?; + f.write_all(&bytes)?; + return Ok(()); + } + + // If there is a decompressor to use, we'll create a file in-memory (to-do) and decompress it + // TODO: change decompressor logic to use BufReader or something like that + + Ok(()) } fn decompress_file(&self, file: &File) -> error::OuchResult<()> { let output_file = &self.command.output; - let decompressor = self.get_decompressor(file)?; - let files_unpacked = decompressor.decompress(file, output_file)?; + let (first_decompressor, second_decompressor) = self.get_decompressor(file)?; - // TODO: decompress the first extension if it exists + let decompression_result = second_decompressor.decompress(file, output_file)?; + + match decompression_result { + DecompressionResult::FileInMemory(bytes) => { + // We'll now decompress a file currently in memory. + // This will currently happen in the case of .bz, .xz and .lzma + Self::decompress_file_in_memory(bytes, file, first_decompressor, output_file)?; + } + DecompressionResult::FilesUnpacked(files) => { + // If the file's last extension was an archival method, + // such as .tar, .zip or (to-do) .rar, then we won't look for + // further processing. + // The reason for this is that cases such as "file.xz.tar" are too rare + // to worry about, at least at the moment. + + // TODO: use the `files` variable for something + } + } Ok(()) } @@ -67,4 +136,4 @@ impl Evaluator { } Ok(()) } -} \ No newline at end of file +} diff --git a/src/extension.rs b/src/extension.rs index d4880a901..30fe428ff 100644 --- a/src/extension.rs +++ b/src/extension.rs @@ -49,7 +49,7 @@ impl Extension { "tar" => Ok(Tar), "gz" => Ok(Gzip), "bz" => Ok(Bzip), - "lzma" => Ok(Lzma), + "lz" | "lzma" => Ok(Lzma), other => Err(error::Error::UnknownExtensionError(other.into())), } }; From 77d7613967f2717b85cfc49a08cb52f32fd1f6f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Mon, 22 Mar 2021 03:44:56 -0300 Subject: [PATCH 17/67] WIP refactor --- src/cli.rs | 2 ++ src/decompressors/tar.rs | 13 +++++-------- src/decompressors/zip.rs | 1 - src/evaluator.rs | 24 ++++++++++-------------- src/file.rs | 6 +++++- src/main.rs | 4 ++-- src/test.rs | 5 +++++ 7 files changed, 29 insertions(+), 26 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 6571c5538..80c1722cf 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -115,6 +115,7 @@ impl TryFrom> for Command { kind: CommandKind::Compression(input_files), output: Some(File { path: output_file.into(), + contents: None, extension: Some(output_file_extension.unwrap()) }), }); @@ -130,6 +131,7 @@ impl TryFrom> for Command { kind: CommandKind::Decompression(input_files), output: Some(File { path: output_file.into(), + contents: None, extension: None }) }); diff --git a/src/decompressors/tar.rs b/src/decompressors/tar.rs index 913bdebb0..eee2f9d3e 100644 --- a/src/decompressors/tar.rs +++ b/src/decompressors/tar.rs @@ -1,10 +1,7 @@ -use std::{ - fs, - path::{Path, PathBuf}, -}; +use std::{fs, io::{Cursor, Read}, path::{Path, PathBuf}}; use colored::Colorize; -use tar; +use tar::{self, Archive}; use crate::{error::OuchResult, utils}; use crate::file::File; @@ -15,12 +12,12 @@ pub struct TarDecompressor {} impl TarDecompressor { - fn unpack_files(from: &Path, into: &Path) -> OuchResult> { + fn unpack_files(from: &File, into: &Path) -> OuchResult> { println!("{}: attempting to decompress {:?}", "ouch".bright_blue(), from); let mut files_unpacked = vec![]; - let file = fs::File::open(from)?; + let file = fs::File::open(&from.path)?; let mut archive = tar::Archive::new(file); for file in archive.entries()? { @@ -50,7 +47,7 @@ impl Decompressor for TarDecompressor { utils::create_path_if_non_existent(destination_path)?; - let files_unpacked = Self::unpack_files(&from.path, destination_path)?; + let files_unpacked = Self::unpack_files(&from, destination_path)?; Ok(DecompressionResult::FilesUnpacked(files_unpacked)) } diff --git a/src/decompressors/zip.rs b/src/decompressors/zip.rs index c3ce796c0..c7f5fffe9 100644 --- a/src/decompressors/zip.rs +++ b/src/decompressors/zip.rs @@ -23,7 +23,6 @@ impl ZipDecompressor { let mut unpacked_files = vec![]; - // placeholder return println!("{}: attempting to decompress {:?}", "ouch".bright_blue(), from); let file = fs::File::open(from)?; diff --git a/src/evaluator.rs b/src/evaluator.rs index c1a7fbe4a..90165356d 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -20,13 +20,7 @@ pub struct Evaluator { } impl Evaluator { - // todo: remove this? - pub fn new(command: Command) -> Self { - Self { command } - } - fn get_decompressor( - &self, file: &File, ) -> error::OuchResult<(Option>, Box)> { if file.extension.is_none() { @@ -95,17 +89,17 @@ impl Evaluator { Ok(()) } - fn decompress_file(&self, file: &File) -> error::OuchResult<()> { - let output_file = &self.command.output; - let (first_decompressor, second_decompressor) = self.get_decompressor(file)?; + fn decompress_file(file: File, output: &Option) -> error::OuchResult<()> { + // let output_file = &command.output; + let (first_decompressor, second_decompressor) = Self::get_decompressor(&file)?; - let decompression_result = second_decompressor.decompress(file, output_file)?; + let decompression_result = second_decompressor.decompress(&file, output)?; match decompression_result { DecompressionResult::FileInMemory(bytes) => { // We'll now decompress a file currently in memory. // This will currently happen in the case of .bz, .xz and .lzma - Self::decompress_file_in_memory(bytes, file, first_decompressor, output_file)?; + Self::decompress_file_in_memory(bytes, &file, first_decompressor, output)?; } DecompressionResult::FilesUnpacked(files) => { // If the file's last extension was an archival method, @@ -121,8 +115,10 @@ impl Evaluator { Ok(()) } - pub fn evaluate(&mut self) -> error::OuchResult<()> { - match &self.command.kind { + pub fn evaluate(command: Command) -> error::OuchResult<()> { + let output = command.output.clone(); + + match command.kind { CommandKind::Compression(files_to_compress) => { for _file in files_to_compress { todo!(); @@ -130,7 +126,7 @@ impl Evaluator { } CommandKind::Decompression(files_to_decompress) => { for file in files_to_decompress { - self.decompress_file(file)?; + Self::decompress_file(file, &output)?; } } } diff --git a/src/file.rs b/src/file.rs index 7066934ca..3b50a6c79 100644 --- a/src/file.rs +++ b/src/file.rs @@ -3,10 +3,13 @@ use std::path::PathBuf; use crate::extension::{CompressionFormat, Extension}; -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct File { /// File's (relative) path pub path: PathBuf, + /// The bytes that compose the file. + /// Only used when the whole file is kept in-memory + pub contents: Option>, /// Note: extension here might be a misleading name since /// we don't really care about any extension other than supported compression ones. /// @@ -19,6 +22,7 @@ impl From<(PathBuf, CompressionFormat)> for File { fn from((path, format): (PathBuf, CompressionFormat)) -> Self { Self { path, + contents: None, extension: Some(Extension::from(format)), } } diff --git a/src/main.rs b/src/main.rs index ab6775650..e8739604e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,8 +20,8 @@ fn main() -> OuchResult<()>{ let matches = cli::get_matches(); match cli::Command::try_from(matches) { Ok(command) => { - let mut eval = evaluator::Evaluator::new(command); - match eval.evaluate() { + // let mut eval = evaluator::Evaluator::new(); + match evaluator::Evaluator::evaluate(command) { Ok(_) => {}, Err(err) => print_error(err) } diff --git a/src/test.rs b/src/test.rs index 401991a5e..7703215b0 100644 --- a/src/test.rs +++ b/src/test.rs @@ -21,11 +21,13 @@ mod cli { kind: Decompression(vec![ File { path: "file.zip".into(), + contents: None, extension: Some(Extension::from(Zip)) } ]), output: Some(File { path: "folder".into(), + contents: None, extension: None }), } @@ -45,10 +47,12 @@ mod cli { kind: Decompression(vec![ File { path: "file.zip".into(), + contents: None, extension: Some(Extension::from(Zip)) }, File { path: "file.tar".into(), + contents: None, extension: Some(Extension::from(Tar)) } ],), @@ -84,6 +88,7 @@ mod cli { output: Some( File { path: "file.tar".into(), + contents: None, extension: Some(Extension::from(Tar)) } ), From e08703850ce208cc1258e3516d32f7b93c465fc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Mon, 22 Mar 2021 04:46:54 -0300 Subject: [PATCH 18/67] Add support for decompressing .tar.{bz, xz, lz} and .zip.{bz, xz, lz} --- src/cli.rs | 9 +---- src/decompressors/decompressor.rs | 2 +- src/decompressors/niffler.rs | 2 +- src/decompressors/tar.rs | 19 +++++++--- src/decompressors/zip.rs | 62 ++++++++++++++++++++++--------- src/evaluator.rs | 41 +++++++++++++------- src/main.rs | 3 ++ src/test.rs | 18 ++++++++- src/utils.rs | 2 +- 9 files changed, 108 insertions(+), 50 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 80c1722cf..1da9d1261 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -91,6 +91,7 @@ impl TryFrom> for Command { 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 @@ -103,14 +104,6 @@ impl TryFrom> for Command { let input_files = input_files.map(PathBuf::from).collect(); - // return Ok(Command { - // kind: CommandKind::Compression(input_files), - // output: Some(File::WithExtension(( - // output_file.into(), - // output_file_extension.unwrap(), - // ))), - // }); - return Ok(Command { kind: CommandKind::Compression(input_files), output: Some(File { diff --git a/src/decompressors/decompressor.rs b/src/decompressors/decompressor.rs index b6a734102..fc20ad2a4 100644 --- a/src/decompressors/decompressor.rs +++ b/src/decompressors/decompressor.rs @@ -8,5 +8,5 @@ pub enum DecompressionResult { } pub trait Decompressor { - fn decompress(&self, from: &File, into: &Option) -> OuchResult; + fn decompress(&self, from: File, into: &Option) -> OuchResult; } \ No newline at end of file diff --git a/src/decompressors/niffler.rs b/src/decompressors/niffler.rs index d51be0621..3539c6150 100644 --- a/src/decompressors/niffler.rs +++ b/src/decompressors/niffler.rs @@ -42,7 +42,7 @@ impl NifflerDecompressor { } impl Decompressor for NifflerDecompressor { - fn decompress(&self, from: &File, into: &Option) -> OuchResult { + fn decompress(&self, from: File, into: &Option) -> OuchResult { let destination_path = utils::get_destination_path(into); utils::create_path_if_non_existent(destination_path)?; diff --git a/src/decompressors/tar.rs b/src/decompressors/tar.rs index eee2f9d3e..5b23e9df5 100644 --- a/src/decompressors/tar.rs +++ b/src/decompressors/tar.rs @@ -12,13 +12,20 @@ pub struct TarDecompressor {} impl TarDecompressor { - fn unpack_files(from: &File, into: &Path) -> OuchResult> { + fn unpack_files(from: File, into: &Path) -> OuchResult> { - println!("{}: attempting to decompress {:?}", "ouch".bright_blue(), from); + println!("{}: attempting to decompress {:?}", "ouch".bright_blue(), &from.path); let mut files_unpacked = vec![]; - let file = fs::File::open(&from.path)?; - let mut archive = tar::Archive::new(file); + let mut archive: Archive> = match from.contents { + Some(bytes) => { + tar::Archive::new(Box::new(Cursor::new(bytes))) + } + None => { + let file = fs::File::open(&from.path)?; + tar::Archive::new(Box::new(file)) + } + }; for file in archive.entries()? { let mut file = file?; @@ -42,12 +49,12 @@ impl TarDecompressor { } impl Decompressor for TarDecompressor { - fn decompress(&self, from: &File, into: &Option) -> OuchResult { + fn decompress(&self, from: File, into: &Option) -> OuchResult { let destination_path = utils::get_destination_path(into); utils::create_path_if_non_existent(destination_path)?; - let files_unpacked = Self::unpack_files(&from, destination_path)?; + let files_unpacked = Self::unpack_files(from, destination_path)?; Ok(DecompressionResult::FilesUnpacked(files_unpacked)) } diff --git a/src/decompressors/zip.rs b/src/decompressors/zip.rs index c7f5fffe9..7eb32d983 100644 --- a/src/decompressors/zip.rs +++ b/src/decompressors/zip.rs @@ -1,33 +1,36 @@ -use std::{fs, io, path::{Path, PathBuf}}; +use std::{fs, io::{self, Cursor, Read, Seek}, path::{Path, PathBuf}}; use colored::Colorize; -use zip::{self, read::ZipFile}; +use zip::{self, ZipArchive, read::ZipFile}; -use crate::{error::{self, OuchResult}, utils}; -use crate::file::File; +use crate::{error, file::File}; +use crate::{error::OuchResult, utils}; use super::decompressor::{DecompressionResult, Decompressor}; pub struct ZipDecompressor {} impl ZipDecompressor { - fn check_for_comments(file: &ZipFile) { let comment = file.comment(); if !comment.is_empty() { - println!("{}: Comment in {}: {}", "info".yellow(), file.name(), comment); + println!( + "{}: Comment in {}: {}", + "info".yellow(), + file.name(), + comment + ); } } - fn unpack_files(from: &Path, into: &Path) -> OuchResult> { - + pub fn zip_decompress( + archive: &mut ZipArchive, + into: &Path, + ) -> error::OuchResult> + where + T: Read + Seek, + { let mut unpacked_files = vec![]; - - println!("{}: attempting to decompress {:?}", "ouch".bright_blue(), from); - - let file = fs::File::open(from)?; - let mut archive = zip::ZipArchive::new(file)?; - for idx in 0..archive.len() { let mut file = archive.by_index(idx)?; let file_path = match file.enclosed_name() { @@ -68,17 +71,40 @@ impl ZipDecompressor { Ok(unpacked_files) } -} + fn unpack_files(from: File, into: &Path) -> OuchResult> { + + println!( + "{}: attempting to decompress {:?}", + "ouch".bright_blue(), + from + ); + + match from.contents { + Some(bytes) => { + let mut archive = zip::ZipArchive::new(Cursor::new(bytes))?; + Ok(Self::zip_decompress(&mut archive, into)?) + }, + None => { + let file = fs::File::open(&from.path)?; + let mut archive = zip::ZipArchive::new(file)?; + Ok(Self::zip_decompress(&mut archive, into)?) + } + } + + + + } +} impl Decompressor for ZipDecompressor { - fn decompress(&self, from: &File, into: &Option) -> OuchResult { + fn decompress(&self, from: File, into: &Option) -> OuchResult { let destination_path = utils::get_destination_path(into); utils::create_path_if_non_existent(destination_path)?; - let files_unpacked = Self::unpack_files(&from.path, destination_path)?; + let files_unpacked = Self::unpack_files(from, destination_path)?; Ok(DecompressionResult::FilesUnpacked(files_unpacked)) } -} \ No newline at end of file +} diff --git a/src/evaluator.rs b/src/evaluator.rs index 90165356d..dbfe03b98 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -1,8 +1,8 @@ -use std::{ffi::OsStr, fs, io::Write}; +use std::{ffi::OsStr, fs, io::Write, path::PathBuf}; use colored::Colorize; -use crate::decompressors::Decompressor; +use crate::{decompressors::Decompressor, extension::Extension}; use crate::decompressors::TarDecompressor; use crate::decompressors::ZipDecompressor; use crate::{ @@ -15,7 +15,6 @@ use crate::{ }; pub struct Evaluator { - command: Command, // verbosity: Verbosity } @@ -31,7 +30,7 @@ impl Evaluator { ); return Err(error::Error::InvalidInput); } - let extension = file.extension.clone().unwrap(); + let extension = Extension::new(&file.path.to_str().unwrap())?; let decompressor_from_format = |ext| -> Box { match ext { @@ -58,33 +57,46 @@ impl Evaluator { // todo: move this folder into decompressors/ later on fn decompress_file_in_memory( bytes: Vec, - file: &File, + file_path: PathBuf, decompressor: Option>, output_file: &Option, + extension: Option, ) -> OuchResult<()> { - let output_file = utils::get_destination_path(output_file); + let output_file_path = utils::get_destination_path(output_file); - let mut filename = file.path.file_stem().unwrap_or(output_file.as_os_str()); + let mut filename = file_path.file_stem().unwrap_or(output_file_path.as_os_str()); if filename == "." { // I believe this is only possible when the supplied inout has a name // of the sort `.tar` or `.zip' and no output has been supplied. filename = OsStr::new("ouch-output"); } + let filename = PathBuf::from(filename); + if decompressor.is_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); - let mut f = fs::File::create(output_file.join(filename))?; + let mut f = fs::File::create(output_file_path.join(filename))?; f.write_all(&bytes)?; return Ok(()); } - // If there is a decompressor to use, we'll create a file in-memory (to-do) and decompress it - // TODO: change decompressor logic to use BufReader or something like that + let file = File { + path: filename, + contents: Some(bytes), + extension, + }; + + let decompressor = decompressor.unwrap(); + + // If there is a decompressor to use, we'll create a file in-memory and decompress it + + + let decompression_result = decompressor.decompress(file, output_file)?; Ok(()) } @@ -93,15 +105,18 @@ impl Evaluator { // let output_file = &command.output; let (first_decompressor, second_decompressor) = Self::get_decompressor(&file)?; - let decompression_result = second_decompressor.decompress(&file, output)?; + let file_path = file.path.clone(); + let extension = file.extension.clone(); + + let decompression_result = second_decompressor.decompress(file, output)?; match decompression_result { DecompressionResult::FileInMemory(bytes) => { // We'll now decompress a file currently in memory. // This will currently happen in the case of .bz, .xz and .lzma - Self::decompress_file_in_memory(bytes, &file, first_decompressor, output)?; + Self::decompress_file_in_memory(bytes, file_path, first_decompressor, output, extension)?; } - DecompressionResult::FilesUnpacked(files) => { + DecompressionResult::FilesUnpacked(_files) => { // If the file's last extension was an archival method, // such as .tar, .zip or (to-do) .rar, then we won't look for // further processing. diff --git a/src/main.rs b/src/main.rs index e8739604e..511aef236 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,5 +30,8 @@ fn main() -> OuchResult<()>{ print_error(err) } } + + // let extension = dbg!(Extension::new("file.tar.gz")); + Ok(()) } diff --git a/src/test.rs b/src/test.rs index 7703215b0..64fcbbcd1 100644 --- a/src/test.rs +++ b/src/test.rs @@ -84,7 +84,6 @@ mod cli { "file2.jpeg".into(), "file3.ok".into() ]), - // output: Some(File::WithExtension(("file.tar".into(), Extension::from(Tar)))) output: Some( File { path: "file.tar".into(), @@ -126,7 +125,7 @@ mod cli_errors { #[cfg(test)] mod extension_extraction { - use crate::error::OuchResult; + use crate::{error::OuchResult, extension::Extension}; use crate::extension::CompressionFormat; use std::{convert::TryFrom, path::PathBuf, str::FromStr}; @@ -140,6 +139,21 @@ mod extension_extraction { Ok(()) } + + #[test] + fn tar_gz() -> OuchResult<()> { + let extension = Extension::new("folder.tar.gz")?; + + assert_eq!( + extension, + Extension { + first_ext: Some(CompressionFormat::Tar), + second_ext: CompressionFormat::Gzip + } + ); + + Ok(()) + } #[test] fn tar() -> OuchResult<()> { diff --git a/src/utils.rs b/src/utils.rs index e6d1d1f07..82babb946 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,4 @@ -use std::{fs, path::{Component, Path, PathBuf}}; +use std::{fs, path::Path}; use colored::Colorize; use crate::{error::OuchResult, file::File}; From 52afe3afd855600c7eaa2b7d0427fb9cd4307449 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Mon, 22 Mar 2021 15:12:11 -0300 Subject: [PATCH 19/67] Update README & minor code cleanup --- README.md | 38 +++++++++++++++++++++++++++++++++++--- src/decompressors/zip.rs | 2 +- src/evaluator.rs | 30 ++++++++++++++++++------------ src/extension.rs | 3 --- src/test.rs | 4 +++- 5 files changed, 57 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index a2c47da22..6e87aebc7 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,12 @@ `ouch` is the Obvious Unified Compression (and decompression) Helper. + +| Supported formats | .tar | .zip | .tar.{.lz, .lzma, .gz, .bz} | .zip.{.lz, .lzma, .gz, .bz} | .bz | .gz | .lz, .lzma | +|-------------------|------|------|------------------------------|------------------------------|-----|-----|------------| +| Decompression | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Compression | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | + ## How does it work? `ouch` infers commands from the extensions of its command-line options. @@ -27,9 +33,7 @@ OPTIONS: #### Decompressing a bunch of files ```bash -$ ouch -i file{1..5}.zip -info: attempting to decompress input files into single_folder -info: done! +$ ouch -i file{1..5}.zip another_file.tar.gz yet_another_file.tar.bz ``` When no output file is supplied, `ouch` infers that it must decompress all of its input files. This will error if any of the input files are not decompressible. @@ -63,5 +67,33 @@ error: file 'some-file' is not decompressible. `ouch` might (TODO!) be able to sniff a file's compression format if it isn't supplied in the future, but that is not currently implemented. +## Installation + +### Runtime dependencies + +`ouch` depends on a few widespread libraries: +* libbz2 +* liblzma + +Both should be already installed in any mainstream Linux distribution. + +If they're not, then: +* On Debian-based distros +`sudo apt install liblzma-dev libbz2-dev` + +* On Arch-based distros + +`sudo pacman -S xz bzip2` + +The last dependency is a recent [Rust](https://www.rust-lang.org/) toolchain. If you don't have one installed, follow the instructions at [rustup.rs](https://rustup.rs/). + +### Build process + +Once the dependency requirements are met: + +```bash +git clone https://github.com/vrmiguel/jacarex # Clone the repo. +cargo install --path ouch # .. and install it +``` \ No newline at end of file diff --git a/src/decompressors/zip.rs b/src/decompressors/zip.rs index 7eb32d983..64405ae25 100644 --- a/src/decompressors/zip.rs +++ b/src/decompressors/zip.rs @@ -77,7 +77,7 @@ impl ZipDecompressor { println!( "{}: attempting to decompress {:?}", "ouch".bright_blue(), - from + &from.path ); match from.contents { diff --git a/src/evaluator.rs b/src/evaluator.rs index dbfe03b98..644875fd6 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -2,7 +2,7 @@ use std::{ffi::OsStr, fs, io::Write, path::PathBuf}; use colored::Colorize; -use crate::{decompressors::Decompressor, extension::Extension}; +use crate::{decompressors::Decompressor, extension::{self, Extension}}; use crate::decompressors::TarDecompressor; use crate::decompressors::ZipDecompressor; use crate::{ @@ -31,23 +31,25 @@ impl Evaluator { return Err(error::Error::InvalidInput); } let extension = Extension::new(&file.path.to_str().unwrap())?; + + let second_decompressor: Box = match extension.second_ext { + CompressionFormat::Tar => Box::new(TarDecompressor {}), - let decompressor_from_format = |ext| -> Box { - match ext { - CompressionFormat::Tar => Box::new(TarDecompressor {}), - - CompressionFormat::Zip => Box::new(ZipDecompressor {}), + CompressionFormat::Zip => Box::new(ZipDecompressor {}), - CompressionFormat::Gzip | CompressionFormat::Lzma | CompressionFormat::Bzip => { - Box::new(NifflerDecompressor {}) - } + CompressionFormat::Gzip | CompressionFormat::Lzma | CompressionFormat::Bzip => { + Box::new(NifflerDecompressor {}) } }; - let second_decompressor = decompressor_from_format(extension.second_ext); + let first_decompressor: Option> = match extension.first_ext { + Some(ext) => match ext { + CompressionFormat::Tar => Some(Box::new(TarDecompressor {})), - let first_decompressor = match extension.first_ext { - Some(ext) => Some(decompressor_from_format(ext)), + CompressionFormat::Zip => Some(Box::new(ZipDecompressor {})), + + _other => None, + }, None => None, }; @@ -97,6 +99,10 @@ impl Evaluator { let decompression_result = decompressor.decompress(file, output_file)?; + if let DecompressionResult::FileInMemory(_) = decompression_result { + // Should not be reachable. + unreachable!(); + } Ok(()) } diff --git a/src/extension.rs b/src/extension.rs index 30fe428ff..38e682c3b 100644 --- a/src/extension.rs +++ b/src/extension.rs @@ -100,9 +100,6 @@ pub enum CompressionFormat { Tar, // .zip Zip, - // Not a supported compressed file extension (any other file) - // TODO: it makes no sense for this variant to exist here - // NotCompressed } fn extension_from_os_str(ext: &OsStr) -> Result { diff --git a/src/test.rs b/src/test.rs index 64fcbbcd1..e7e151752 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,3 +1,5 @@ +// TODO: remove tests of CompressionFormat::try_from since that's no longer used anywhere + #[cfg(test)] mod cli { @@ -125,7 +127,7 @@ mod cli_errors { #[cfg(test)] mod extension_extraction { - use crate::{error::OuchResult, extension::Extension}; + use crate::{error::OuchResult, extension::Extension} ; use crate::extension::CompressionFormat; use std::{convert::TryFrom, path::PathBuf, str::FromStr}; From 9429fd8d6787c31b1a41ab4df2e7b047b8e003cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Mon, 22 Mar 2021 23:39:08 -0300 Subject: [PATCH 20/67] Fix CLI parsing of decompression commands & early Compressor work --- src/cli.rs | 8 ++++---- src/compressors/compressor.rs | 22 ++++++++++++++++++++++ src/compressors/mod.rs | 0 src/evaluator.rs | 2 +- src/extension.rs | 11 +++++------ src/file.rs | 6 +++--- src/main.rs | 5 ++--- 7 files changed, 37 insertions(+), 17 deletions(-) create mode 100644 src/compressors/compressor.rs create mode 100644 src/compressors/mod.rs diff --git a/src/cli.rs b/src/cli.rs index 1da9d1261..69cb25645 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -66,7 +66,7 @@ impl TryFrom> for Command { fn try_from(matches: clap::ArgMatches<'static>) -> error::OuchResult { let process_decompressible_input = |input_files: Values| { let input_files = - input_files.map(|filename| (filename, CompressionFormat::try_from(filename))); + input_files.map(|filename| (filename, Extension::new(filename))); for file in input_files.clone() { if let (file, Err(_)) = file { @@ -76,6 +76,7 @@ impl TryFrom> for Command { Ok(input_files .map(|(filename, extension)| (PathBuf::from(filename), extension.unwrap())) + .map(File::from) .collect::>()) }; @@ -114,12 +115,11 @@ impl TryFrom> for Command { }); } else { + // Output not supplied // Checking if input files are decompressible let input_files = process_decompressible_input(input_files)?; - let input_files = input_files.into_iter().map(File::from).collect(); - return Ok(Command { kind: CommandKind::Decompression(input_files), output: Some(File { @@ -134,7 +134,7 @@ impl TryFrom> for Command { // Case 1: all input files are decompressible // Case 2: error let input_files = process_decompressible_input(input_files)?; - let input_files = input_files.into_iter().map(File::from).collect(); + return Ok(Command { kind: CommandKind::Decompression(input_files), output: None, diff --git a/src/compressors/compressor.rs b/src/compressors/compressor.rs new file mode 100644 index 000000000..253008546 --- /dev/null +++ b/src/compressors/compressor.rs @@ -0,0 +1,22 @@ +use std::path::PathBuf; + +use crate::{error::OuchResult, file::File}; + +pub enum CompressionResult { + FilesUnpacked(Vec), + FileInMemory(Vec) +} + +pub trait Compressor { + fn compress(&self, from: Vec, into: &Option) -> OuchResult; +} + +// +// +// +// +// +// +// +// +// \ No newline at end of file diff --git a/src/compressors/mod.rs b/src/compressors/mod.rs new file mode 100644 index 000000000..e69de29bb diff --git a/src/evaluator.rs b/src/evaluator.rs index 644875fd6..d9cedb931 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -30,7 +30,7 @@ impl Evaluator { ); return Err(error::Error::InvalidInput); } - let extension = Extension::new(&file.path.to_str().unwrap())?; + let extension = file.extension.clone().unwrap(); let second_decompressor: Box = match extension.second_ext { CompressionFormat::Tar => Box::new(TarDecompressor {}), diff --git a/src/extension.rs b/src/extension.rs index 38e682c3b..ee4f86d93 100644 --- a/src/extension.rs +++ b/src/extension.rs @@ -31,7 +31,6 @@ pub fn get_extension_from_filename(filename: &str) -> Option<(&str, &str)> { } } - impl From for Extension { fn from(second_ext: CompressionFormat) -> Self { Self { @@ -48,7 +47,7 @@ impl Extension { "zip" => Ok(Zip), "tar" => Ok(Tar), "gz" => Ok(Gzip), - "bz" => Ok(Bzip), + "bz" | "bz2" => Ok(Bzip), "lz" | "lzma" => Ok(Lzma), other => Err(error::Error::UnknownExtensionError(other.into())), } @@ -103,7 +102,8 @@ pub enum CompressionFormat { } fn extension_from_os_str(ext: &OsStr) -> Result { - + // let ext = Path::new(ext); + let ext = match ext.to_str() { Some(str) => str, None => return Err(error::Error::InvalidUnicode), @@ -113,8 +113,8 @@ fn extension_from_os_str(ext: &OsStr) -> Result "zip" => Ok(Zip), "tar" => Ok(Tar), "gz" => Ok(Gzip), - "bz" => Ok(Bzip), - "lzma" => Ok(Lzma), + "bz" | "bz2" => Ok(Bzip), + "lzma" | "lz" => Ok(Lzma), other => Err(error::Error::UnknownExtensionError(other.into())), } } @@ -130,7 +130,6 @@ impl TryFrom<&PathBuf> for CompressionFormat { return Err(error::Error::MissingExtensionError(String::new())); } }; - extension_from_os_str(ext) } } diff --git a/src/file.rs b/src/file.rs index 3b50a6c79..78b87a545 100644 --- a/src/file.rs +++ b/src/file.rs @@ -18,12 +18,12 @@ pub struct File { pub extension: Option } -impl From<(PathBuf, CompressionFormat)> for File { - fn from((path, format): (PathBuf, CompressionFormat)) -> Self { +impl From<(PathBuf, Extension)> for File { + fn from((path, format): (PathBuf, Extension)) -> Self { Self { path, contents: None, - extension: Some(Extension::from(format)), + extension: Some(format), } } } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 511aef236..8a2f94228 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,8 @@ mod file; mod test; mod evaluator; mod utils; + +mod compressors; mod decompressors; use error::OuchResult; @@ -20,7 +22,6 @@ fn main() -> OuchResult<()>{ let matches = cli::get_matches(); match cli::Command::try_from(matches) { Ok(command) => { - // let mut eval = evaluator::Evaluator::new(); match evaluator::Evaluator::evaluate(command) { Ok(_) => {}, Err(err) => print_error(err) @@ -31,7 +32,5 @@ fn main() -> OuchResult<()>{ } } - // let extension = dbg!(Extension::new("file.tar.gz")); - Ok(()) } From fa2fb675de38a13e088821846e18350b3c1ebcf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Tue, 23 Mar 2021 01:06:57 -0300 Subject: [PATCH 21/67] WIP Tar compression --- Cargo.lock | 30 +++++++++++++++++++++ Cargo.toml | 1 + src/compressors/compressor.rs | 5 ++-- src/compressors/mod.rs | 5 ++++ src/compressors/tar.rs | 42 ++++++++++++++++++++++++++++++ src/main.rs | 49 ++++++++++++++++++++++++----------- 6 files changed, 115 insertions(+), 17 deletions(-) create mode 100644 src/compressors/tar.rs diff --git a/Cargo.lock b/Cargo.lock index 1b5fa107b..fbf82891d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -326,6 +326,7 @@ dependencies = [ "colored", "niffler", "tar", + "walkdir", "zip", ] @@ -368,6 +369,15 @@ version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "strsim" version = "0.8.0" @@ -466,6 +476,17 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + [[package]] name = "wasi" version = "0.10.0+wasi-snapshot-preview1" @@ -488,6 +509,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index d90553859..2a0317e35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ edition = "2018" [dependencies] colored = "2.0.0" niffler = "2.3.1" +walkdir = "2.3.2" clap = "2.33.3" zip = "0.5.11" tar = "0.4.33" \ No newline at end of file diff --git a/src/compressors/compressor.rs b/src/compressors/compressor.rs index 253008546..4ca801fb6 100644 --- a/src/compressors/compressor.rs +++ b/src/compressors/compressor.rs @@ -3,12 +3,13 @@ use std::path::PathBuf; use crate::{error::OuchResult, file::File}; pub enum CompressionResult { - FilesUnpacked(Vec), + ZipArchive(Vec), + TarArchive(Vec), FileInMemory(Vec) } pub trait Compressor { - fn compress(&self, from: Vec, into: &Option) -> OuchResult; + fn compress(&self, from: Vec) -> OuchResult; } // diff --git a/src/compressors/mod.rs b/src/compressors/mod.rs index e69de29bb..7d48bcaca 100644 --- a/src/compressors/mod.rs +++ b/src/compressors/mod.rs @@ -0,0 +1,5 @@ +mod tar; +mod compressor; + +pub use compressor::{Compressor, CompressionResult}; +pub use self::tar::TarCompressor; \ No newline at end of file diff --git a/src/compressors/tar.rs b/src/compressors/tar.rs new file mode 100644 index 000000000..572118c6e --- /dev/null +++ b/src/compressors/tar.rs @@ -0,0 +1,42 @@ +use std::{fs::File, path::Path}; + +use tar::Builder; +use walkdir::WalkDir; + +use crate::{decompressors::TarDecompressor, error::OuchResult}; +use crate::compressors::Compressor; +use super::compressor::CompressionResult; + +pub struct TarCompressor {} + +impl TarCompressor { + fn make_archive_in_memory(input_files: Vec) -> OuchResult> { + + let buf = Vec::new(); + let mut b = Builder::new(buf); + + for file in input_files { + for entry in WalkDir::new(&file.path) { + let entry = entry.unwrap(); + let path = entry.path(); + if path.is_dir() { + continue; + } + b.append_file(path, &mut File::open(path).unwrap()).unwrap(); + } + } + + + Ok(b.into_inner()?) + } +} + +impl Compressor for TarCompressor { + fn compress(&self, from: Vec) -> OuchResult { + Ok(CompressionResult::TarArchive( + TarCompressor::make_archive_in_memory(from)? + )) + } +} + +// fn compress(&self, from: Vec, into: &Option) -> OuchResult; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 8a2f94228..046089d6b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use std::convert::TryFrom; +use std::{convert::TryFrom, fs, path::{Path, PathBuf}}; use colored::Colorize; @@ -13,24 +13,43 @@ mod utils; mod compressors; mod decompressors; +use compressors::{CompressionResult, Compressor, TarCompressor}; use error::OuchResult; +use file::File; fn main() -> OuchResult<()>{ - let print_error = |err| { - println!("{}: {}", "error".red(), err); + // let print_error = |err| { + // println!("{}: {}", "error".red(), err); + // }; + // let matches = cli::get_matches(); + // match cli::Command::try_from(matches) { + // Ok(command) => { + // match evaluator::Evaluator::evaluate(command) { + // Ok(_) => {}, + // Err(err) => print_error(err) + // } + // } + // Err(err) => { + // print_error(err) + // } + // } + + let compressor = TarCompressor {}; + + let file = File { + path: PathBuf::from("target"), + contents: None, + extension: None, + }; + + let ok = compressor.compress(vec![file])?; + + let ok = match ok { + CompressionResult::TarArchive(bytes) => bytes, + _ => unreachable!() }; - let matches = cli::get_matches(); - match cli::Command::try_from(matches) { - Ok(command) => { - match evaluator::Evaluator::evaluate(command) { - Ok(_) => {}, - Err(err) => print_error(err) - } - } - Err(err) => { - print_error(err) - } - } + + fs::write(Path::new("great.tar"), ok)?; Ok(()) } From 2c0f2b380c719b3f0360d451856c0209d068d2a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Tue, 23 Mar 2021 02:15:06 -0300 Subject: [PATCH 22/67] Add Tar compression for in-memory buffers --- src/cli.rs | 2 +- src/compressors/compressor.rs | 27 +++++------ src/compressors/mod.rs | 2 +- src/compressors/tar.rs | 52 ++++++++++++++------- src/evaluator.rs | 85 +++++++++++++++++++++++++++++------ src/file.rs | 2 +- src/main.rs | 64 +++++++++++++------------- 7 files changed, 152 insertions(+), 82 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 69cb25645..7e9a28468 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -4,7 +4,7 @@ use clap::{Arg, Values}; use colored::Colorize; use crate::error; -use crate::extension::{Extension, CompressionFormat}; +use crate::extension::Extension; use crate::file::File; #[derive(PartialEq, Eq, Debug)] diff --git a/src/compressors/compressor.rs b/src/compressors/compressor.rs index 4ca801fb6..729d7706c 100644 --- a/src/compressors/compressor.rs +++ b/src/compressors/compressor.rs @@ -2,22 +2,17 @@ use std::path::PathBuf; use crate::{error::OuchResult, file::File}; -pub enum CompressionResult { - ZipArchive(Vec), - TarArchive(Vec), - FileInMemory(Vec) -} +// pub enum CompressionResult { +// ZipArchive(Vec), +// TarArchive(Vec), +// FileInMemory(Vec) +// } -pub trait Compressor { - fn compress(&self, from: Vec) -> OuchResult; +pub enum Entry { + Files(Vec), + InMemory(File) } -// -// -// -// -// -// -// -// -// \ No newline at end of file +pub trait Compressor { + fn compress(&self, from: Vec) -> OuchResult>; +} \ No newline at end of file diff --git a/src/compressors/mod.rs b/src/compressors/mod.rs index 7d48bcaca..2d18d41a3 100644 --- a/src/compressors/mod.rs +++ b/src/compressors/mod.rs @@ -1,5 +1,5 @@ mod tar; mod compressor; -pub use compressor::{Compressor, CompressionResult}; +pub use compressor::{Compressor}; pub use self::tar::TarCompressor; \ No newline at end of file diff --git a/src/compressors/tar.rs b/src/compressors/tar.rs index 572118c6e..2e800b3a0 100644 --- a/src/compressors/tar.rs +++ b/src/compressors/tar.rs @@ -1,42 +1,62 @@ -use std::{fs::File, path::Path}; +use std::{fs, path::PathBuf}; -use tar::Builder; +use colored::Colorize; +use tar::{Builder, Header}; use walkdir::WalkDir; -use crate::{decompressors::TarDecompressor, error::OuchResult}; -use crate::compressors::Compressor; -use super::compressor::CompressionResult; +use crate::{compressors::Compressor, error::{Error, OuchResult}, file::{self, File}}; pub struct TarCompressor {} impl TarCompressor { - fn make_archive_in_memory(input_files: Vec) -> OuchResult> { + + fn make_archive_from_memory(input: File) -> OuchResult> { + + let contents = match input.contents { + Some(bytes) => bytes, + None => { + eprintln!("{}: reached TarCompressor::make_archive_from_memory without known content.", "internal error".red()); + return Err(Error::InvalidInput); + } + }; + + let mut header = Header::new_gnu(); + + header.set_path(&input.path).unwrap(); + header.set_size(contents.len() as u64); + header.set_cksum(); + + + let mut b = Builder::new(Vec::new()); + b.append_data(&mut header, &input.path, &*contents)?; + + Ok(b.into_inner()?) + } + + fn make_archive_from_files(input_files: Vec) -> OuchResult> { let buf = Vec::new(); let mut b = Builder::new(buf); for file in input_files { - for entry in WalkDir::new(&file.path) { + for entry in WalkDir::new(&file) { let entry = entry.unwrap(); let path = entry.path(); if path.is_dir() { continue; } - b.append_file(path, &mut File::open(path).unwrap()).unwrap(); + b.append_file(path, &mut fs::File::open(path).unwrap()).unwrap(); } } - Ok(b.into_inner()?) } } impl Compressor for TarCompressor { - fn compress(&self, from: Vec) -> OuchResult { - Ok(CompressionResult::TarArchive( - TarCompressor::make_archive_in_memory(from)? - )) + fn compress(&self, from: Vec) -> OuchResult> { + Ok( + TarCompressor::make_archive_from_files(from)? + ) } -} - -// fn compress(&self, from: Vec, into: &Option) -> OuchResult; \ No newline at end of file +} \ No newline at end of file diff --git a/src/evaluator.rs b/src/evaluator.rs index d9cedb931..8aee8a8e8 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -2,23 +2,71 @@ use std::{ffi::OsStr, fs, io::Write, path::PathBuf}; use colored::Colorize; -use crate::{decompressors::Decompressor, extension::{self, Extension}}; -use crate::decompressors::TarDecompressor; +use crate::{compressors::TarCompressor, decompressors::TarDecompressor}; use crate::decompressors::ZipDecompressor; use crate::{ cli::{Command, CommandKind}, - decompressors::{DecompressionResult, NifflerDecompressor}, + decompressors::{ + Decompressor, + DecompressionResult, + NifflerDecompressor + }, + compressors::Compressor, error::{self, OuchResult}, - extension::CompressionFormat, + extension::{ + Extension, + CompressionFormat, + }, file::File, utils, }; + pub struct Evaluator { // verbosity: Verbosity } impl Evaluator { + fn get_compressor( + file: &File, + ) -> error::OuchResult<(Option>, Box)> { + if file.extension.is_none() { + // This block *should* be unreachable + eprintln!( + "{}: reached Evaluator::get_decompressor without known extension.", + "internal error".red() + ); + return Err(error::Error::InvalidInput); + } + let extension = file.extension.clone().unwrap(); + + // Supported first compressors: + // .tar and .zip + let first_compressor: Option> = match extension.first_ext { + Some(ext) => match ext { + CompressionFormat::Tar => Some(Box::new(TarCompressor {})), + + // CompressionFormat::Zip => Some(Box::new(ZipCompressor {})), + + // _other => Some(Box::new(NifflerCompressor {})), + _other => { + todo!(); + } + }, + None => None, + }; + + // Supported second compressors: + // any + let second_compressor: Box = match extension.second_ext { + CompressionFormat::Tar => Box::new(TarCompressor {}), + _other => todo!() + // + }; + + Ok((first_compressor, second_compressor)) + } + fn get_decompressor( file: &File, ) -> error::OuchResult<(Option>, Box)> { @@ -31,7 +79,7 @@ impl Evaluator { return Err(error::Error::InvalidInput); } let extension = file.extension.clone().unwrap(); - + let second_decompressor: Box = match extension.second_ext { CompressionFormat::Tar => Box::new(TarDecompressor {}), @@ -64,10 +112,11 @@ impl Evaluator { output_file: &Option, extension: Option, ) -> OuchResult<()> { - let output_file_path = utils::get_destination_path(output_file); - let mut filename = file_path.file_stem().unwrap_or(output_file_path.as_os_str()); + let mut filename = file_path + .file_stem() + .unwrap_or(output_file_path.as_os_str()); if filename == "." { // I believe this is only possible when the supplied inout has a name // of the sort `.tar` or `.zip' and no output has been supplied. @@ -97,7 +146,6 @@ impl Evaluator { // If there is a decompressor to use, we'll create a file in-memory and decompress it - let decompression_result = decompressor.decompress(file, output_file)?; if let DecompressionResult::FileInMemory(_) = decompression_result { // Should not be reachable. @@ -107,6 +155,11 @@ impl Evaluator { Ok(()) } + fn compress_files(files: Vec, output: File) -> error::OuchResult<()> { + let (first_decompressor, second_decompressor) = Self::get_compressor(&output)?; + Ok(()) + } + fn decompress_file(file: File, output: &Option) -> error::OuchResult<()> { // let output_file = &command.output; let (first_decompressor, second_decompressor) = Self::get_decompressor(&file)?; @@ -120,7 +173,13 @@ impl Evaluator { DecompressionResult::FileInMemory(bytes) => { // We'll now decompress a file currently in memory. // This will currently happen in the case of .bz, .xz and .lzma - Self::decompress_file_in_memory(bytes, file_path, first_decompressor, output, extension)?; + Self::decompress_file_in_memory( + bytes, + file_path, + first_decompressor, + output, + extension, + )?; } DecompressionResult::FilesUnpacked(_files) => { // If the file's last extension was an archival method, @@ -138,12 +197,12 @@ impl Evaluator { pub fn evaluate(command: Command) -> error::OuchResult<()> { let output = command.output.clone(); - + match command.kind { CommandKind::Compression(files_to_compress) => { - for _file in files_to_compress { - todo!(); - } + // Safe to unwrap since output is mandatory for compression + let output = output.unwrap(); + Self::compress_files(files_to_compress, output)?; } CommandKind::Decompression(files_to_decompress) => { for file in files_to_decompress { diff --git a/src/file.rs b/src/file.rs index 78b87a545..22391acf1 100644 --- a/src/file.rs +++ b/src/file.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use crate::extension::{CompressionFormat, Extension}; +use crate::extension::Extension; #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/src/main.rs b/src/main.rs index 046089d6b..02a80205d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use std::{convert::TryFrom, fs, path::{Path, PathBuf}}; +use std::{convert::TryFrom}; use colored::Colorize; @@ -13,43 +13,39 @@ mod utils; mod compressors; mod decompressors; -use compressors::{CompressionResult, Compressor, TarCompressor}; -use error::OuchResult; -use file::File; - -fn main() -> OuchResult<()>{ - // let print_error = |err| { - // println!("{}: {}", "error".red(), err); - // }; - // let matches = cli::get_matches(); - // match cli::Command::try_from(matches) { - // Ok(command) => { - // match evaluator::Evaluator::evaluate(command) { - // Ok(_) => {}, - // Err(err) => print_error(err) - // } - // } - // Err(err) => { - // print_error(err) - // } - // } - - let compressor = TarCompressor {}; - - let file = File { - path: PathBuf::from("target"), - contents: None, - extension: None, +fn main() -> error::OuchResult<()>{ + let print_error = |err| { + println!("{}: {}", "error".red(), err); }; + let matches = cli::get_matches(); + match cli::Command::try_from(matches) { + Ok(command) => { + match evaluator::Evaluator::evaluate(command) { + Ok(_) => {}, + Err(err) => print_error(err) + } + } + Err(err) => { + print_error(err) + } + } + + // let compressor = TarCompressor {}; + + // let file = File { + // path: PathBuf::from("target"), + // contents: None, + // extension: None, + // }; - let ok = compressor.compress(vec![file])?; + // let ok = compressor.compress(vec![file])?; - let ok = match ok { - CompressionResult::TarArchive(bytes) => bytes, - _ => unreachable!() - }; + // let ok = match ok { + // CompressionResult::TarArchive(bytes) => bytes, + // _ => unreachable!() + // }; - fs::write(Path::new("great.tar"), ok)?; + // fs::write(Path::new("great.tar"), ok)?; Ok(()) } From d72ca9eeae6bb627db285eaaba2b20ccfa14627d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Tue, 23 Mar 2021 03:10:25 -0300 Subject: [PATCH 23/67] Tar compression seemingly working --- src/compressors/compressor.rs | 2 +- src/compressors/mod.rs | 3 ++- src/compressors/tar.rs | 32 ++++++++++++++++++++++---------- src/error.rs | 8 ++++++++ src/evaluator.rs | 32 +++++++++++++++++++++++++++++--- src/main.rs | 19 +------------------ 6 files changed, 63 insertions(+), 33 deletions(-) diff --git a/src/compressors/compressor.rs b/src/compressors/compressor.rs index 729d7706c..5998bef84 100644 --- a/src/compressors/compressor.rs +++ b/src/compressors/compressor.rs @@ -14,5 +14,5 @@ pub enum Entry { } pub trait Compressor { - fn compress(&self, from: Vec) -> OuchResult>; + fn compress(&self, from: Entry) -> OuchResult>; } \ No newline at end of file diff --git a/src/compressors/mod.rs b/src/compressors/mod.rs index 2d18d41a3..739a34e18 100644 --- a/src/compressors/mod.rs +++ b/src/compressors/mod.rs @@ -2,4 +2,5 @@ mod tar; mod compressor; pub use compressor::{Compressor}; -pub use self::tar::TarCompressor; \ No newline at end of file +pub use self::tar::TarCompressor; +pub use self::compressor::Entry; \ No newline at end of file diff --git a/src/compressors/tar.rs b/src/compressors/tar.rs index 2e800b3a0..dc18c1391 100644 --- a/src/compressors/tar.rs +++ b/src/compressors/tar.rs @@ -4,7 +4,9 @@ use colored::Colorize; use tar::{Builder, Header}; use walkdir::WalkDir; -use crate::{compressors::Compressor, error::{Error, OuchResult}, file::{self, File}}; +use crate::{compressors::Compressor, error::{Error, OuchResult}, file::File}; + +use super::compressor::Entry; pub struct TarCompressor {} @@ -33,19 +35,19 @@ impl TarCompressor { Ok(b.into_inner()?) } - fn make_archive_from_files(input_files: Vec) -> OuchResult> { + fn make_archive_from_files(input_filenames: Vec) -> OuchResult> { let buf = Vec::new(); let mut b = Builder::new(buf); - for file in input_files { - for entry in WalkDir::new(&file) { - let entry = entry.unwrap(); + for filename in input_filenames { + for entry in WalkDir::new(&filename) { + let entry = entry?; let path = entry.path(); if path.is_dir() { continue; } - b.append_file(path, &mut fs::File::open(path).unwrap()).unwrap(); + b.append_file(path, &mut fs::File::open(path)?)?; } } @@ -54,9 +56,19 @@ impl TarCompressor { } impl Compressor for TarCompressor { - fn compress(&self, from: Vec) -> OuchResult> { - Ok( - TarCompressor::make_archive_from_files(from)? - ) + fn compress(&self, from: Entry) -> OuchResult> { + + match from { + Entry::Files(filenames) => { + Ok( + Self::make_archive_from_files(filenames)? + ) + }, + Entry::InMemory(file) => { + Ok( + Self::make_archive_from_memory(file)? + ) + } + } } } \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index 63c6ba4f2..9cd2223a1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -86,4 +86,12 @@ impl From for Error { NifErr::IOError(io_err) => Self::from(io_err) } } +} + +impl From for Error { + fn from(err: walkdir::Error) -> Self { + eprintln!("{}: {}", "error".red(), err); + + Self::InvalidInput + } } \ No newline at end of file diff --git a/src/evaluator.rs b/src/evaluator.rs index 8aee8a8e8..9b715dc7c 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -2,7 +2,7 @@ use std::{ffi::OsStr, fs, io::Write, path::PathBuf}; use colored::Colorize; -use crate::{compressors::TarCompressor, decompressors::TarDecompressor}; +use crate::{compressors::{Entry, TarCompressor}, decompressors::TarDecompressor}; use crate::decompressors::ZipDecompressor; use crate::{ cli::{Command, CommandKind}, @@ -155,8 +155,34 @@ impl Evaluator { Ok(()) } - fn compress_files(files: Vec, output: File) -> error::OuchResult<()> { - let (first_decompressor, second_decompressor) = Self::get_compressor(&output)?; + fn compress_files(files: Vec, mut output: File) -> error::OuchResult<()> { + let (first_compressor, second_compressor) = Self::get_compressor(&output)?; + + + let output_path = output.path.clone(); + + let bytes = match first_compressor { + Some(first_compressor) => { + let mut entry = Entry::Files(files); + let bytes = first_compressor.compress(entry)?; + + output.contents = Some(bytes); + + entry = Entry::InMemory(output); + + second_compressor.compress(entry)? + }, + None => { + let entry = Entry::Files(files); + second_compressor.compress(entry)? + } + }; + + println!("{}: writing to {:?}. ({} bytes)", "info".yellow(), &output_path, bytes.len()); + fs::write( + output_path, + bytes)?; + Ok(()) } diff --git a/src/main.rs b/src/main.rs index 02a80205d..02e4375c7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,23 +29,6 @@ fn main() -> error::OuchResult<()>{ print_error(err) } } - - // let compressor = TarCompressor {}; - - // let file = File { - // path: PathBuf::from("target"), - // contents: None, - // extension: None, - // }; - - // let ok = compressor.compress(vec![file])?; - - // let ok = match ok { - // CompressionResult::TarArchive(bytes) => bytes, - // _ => unreachable!() - // }; - - // fs::write(Path::new("great.tar"), ok)?; - + Ok(()) } From d05784de3e0e24acbd28ebff7593b427ae7ed796 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Miguel?= <36349314+vrmiguel@users.noreply.github.com> Date: Tue, 23 Mar 2021 10:51:08 -0300 Subject: [PATCH 24/67] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6e87aebc7..ff0a36487 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ouch +# ouch (_work in progress_) `ouch` is the Obvious Unified Compression (and decompression) Helper. @@ -6,7 +6,7 @@ | Supported formats | .tar | .zip | .tar.{.lz, .lzma, .gz, .bz} | .zip.{.lz, .lzma, .gz, .bz} | .bz | .gz | .lz, .lzma | |-------------------|------|------|------------------------------|------------------------------|-----|-----|------------| | Decompression | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Compression | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | +| Compression | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ## How does it work? @@ -96,4 +96,4 @@ Once the dependency requirements are met: ```bash git clone https://github.com/vrmiguel/jacarex # Clone the repo. cargo install --path ouch # .. and install it -``` \ No newline at end of file +``` From 22e131fb460935d5f472b3e3b034cbe7f6d9305c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Tue, 23 Mar 2021 21:28:22 -0300 Subject: [PATCH 25/67] Add support for zip (and... .zip.zip) compression --- src/cli.rs | 16 +++---- src/compressors/mod.rs | 7 ++- src/compressors/tar.rs | 15 ++++-- src/compressors/zip.rs | 93 ++++++++++++++++++++++++++++++++++++ src/decompressors/niffler.rs | 2 +- src/decompressors/tar.rs | 2 +- src/decompressors/zip.rs | 2 +- src/evaluator.rs | 49 +++++++++++-------- src/file.rs | 4 +- src/main.rs | 30 +++++++++++- src/test.rs | 10 ++-- 11 files changed, 185 insertions(+), 45 deletions(-) create mode 100644 src/compressors/zip.rs diff --git a/src/cli.rs b/src/cli.rs index 7e9a28468..c16866799 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,7 +1,7 @@ use std::{convert::TryFrom, path::PathBuf, vec::Vec}; use clap::{Arg, Values}; -use colored::Colorize; +// use colored::Colorize; use crate::error; use crate::extension::Extension; @@ -97,11 +97,11 @@ impl TryFrom> for Command { if output_is_compressible { // The supplied output is compressible, so we'll compress our inputs to it - println!( - "{}: trying to compress input files into '{}'", - "info".yellow(), - output_file - ); + // println!( + // "{}: trying to compress input files into '{}'", + // "info".yellow(), + // output_file + // ); let input_files = input_files.map(PathBuf::from).collect(); @@ -109,7 +109,7 @@ impl TryFrom> for Command { kind: CommandKind::Compression(input_files), output: Some(File { path: output_file.into(), - contents: None, + contents_in_memory: None, extension: Some(output_file_extension.unwrap()) }), }); @@ -124,7 +124,7 @@ impl TryFrom> for Command { kind: CommandKind::Decompression(input_files), output: Some(File { path: output_file.into(), - contents: None, + contents_in_memory: None, extension: None }) }); diff --git a/src/compressors/mod.rs b/src/compressors/mod.rs index 739a34e18..064a9543b 100644 --- a/src/compressors/mod.rs +++ b/src/compressors/mod.rs @@ -1,6 +1,9 @@ mod tar; +mod zip; mod compressor; -pub use compressor::{Compressor}; +pub use compressor::Compressor; +pub use self::compressor::Entry; pub use self::tar::TarCompressor; -pub use self::compressor::Entry; \ No newline at end of file +pub use self::zip::ZipCompressor; + diff --git a/src/compressors/tar.rs b/src/compressors/tar.rs index dc18c1391..8af4f55dc 100644 --- a/src/compressors/tar.rs +++ b/src/compressors/tar.rs @@ -12,9 +12,10 @@ pub struct TarCompressor {} impl TarCompressor { + // TODO: this function does not seem to be working correctly ;/ fn make_archive_from_memory(input: File) -> OuchResult> { - let contents = match input.contents { + let contents = match input.contents_in_memory { Some(bytes) => bytes, None => { eprintln!("{}: reached TarCompressor::make_archive_from_memory without known content.", "internal error".red()); @@ -24,13 +25,19 @@ impl TarCompressor { let mut header = Header::new_gnu(); - header.set_path(&input.path).unwrap(); + // header.set_path(&input.path.file_stem().unwrap())?; + header.set_path(".")?; header.set_size(contents.len() as u64); header.set_cksum(); + header.set_mode(644); let mut b = Builder::new(Vec::new()); - b.append_data(&mut header, &input.path, &*contents)?; + b.append_data( + &mut header, + &input.path.file_stem().unwrap(), + &*contents + )?; Ok(b.into_inner()?) } @@ -41,6 +48,8 @@ impl TarCompressor { let mut b = Builder::new(buf); for filename in input_filenames { + // TODO: check if filename is a file or a directory + for entry in WalkDir::new(&filename) { let entry = entry?; let path = entry.path(); diff --git a/src/compressors/zip.rs b/src/compressors/zip.rs new file mode 100644 index 000000000..811a1970a --- /dev/null +++ b/src/compressors/zip.rs @@ -0,0 +1,93 @@ +use std::{io::{Cursor, Write}, path::PathBuf}; + +use walkdir::WalkDir; + +use crate::{ + compressors::Compressor, + error::{Error, OuchResult}, + file::File, +}; + +use super::compressor::Entry; + +pub struct ZipCompressor {} + +impl ZipCompressor { + // TODO: this function does not seem to be working correctly ;/ + fn make_archive_from_memory(input: File) -> OuchResult> { + let buffer = vec![]; + let mut writer = zip::ZipWriter::new(std::io::Cursor::new(buffer)); + + let inner_file_path: Box = input + .path + .file_stem() + .ok_or( + // TODO: Is this reachable? + Error::InvalidInput + )? + .to_string_lossy() + .into(); + + let options = + zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Deflated); + + // let ok = Box::from(inner_file_path.to_string_lossy()); + writer.start_file(inner_file_path, options)?; + + let input_bytes = match input.contents_in_memory { + Some(bytes) => bytes, + None => { + // TODO: error description, although this block should not be + // reachable + return Err(Error::InvalidInput); + } + }; + + writer.write(&*input_bytes)?; + + + + let bytes = writer.finish().unwrap(); + + Ok(bytes.into_inner()) + } + + fn make_archive_from_files(input_filenames: Vec) -> OuchResult> { + let buffer = vec![]; + let mut writer = zip::ZipWriter::new(Cursor::new(buffer)); + + let options = + zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Deflated); + + for filename in input_filenames { + for entry in WalkDir::new(filename) { + let entry = entry?; + let entry_path = &entry.path(); + if entry_path.is_dir() { + continue; + } + writer + .start_file( + entry_path.to_string_lossy(), + options + )?; + let file_bytes = std::fs::read(entry.path())?; + writer.write(&*file_bytes)?; + } + } + + + let bytes = writer.finish().unwrap(); + + Ok(bytes.into_inner()) + } +} + +impl Compressor for ZipCompressor { + fn compress(&self, from: Entry) -> OuchResult> { + match from { + Entry::Files(filenames) => Ok(Self::make_archive_from_files(filenames)?), + Entry::InMemory(file) => Ok(Self::make_archive_from_memory(file)?), + } + } +} diff --git a/src/decompressors/niffler.rs b/src/decompressors/niffler.rs index 3539c6150..3d665832d 100644 --- a/src/decompressors/niffler.rs +++ b/src/decompressors/niffler.rs @@ -17,7 +17,7 @@ pub struct NifflerDecompressor {} impl NifflerDecompressor { fn unpack_file(from: &Path) -> OuchResult> { - println!("{}: trying to decompress {:?}", "info".yellow(), from); + // println!("{}: trying to decompress {:?}", "info".yellow(), from); let file = std::fs::read(from)?; diff --git a/src/decompressors/tar.rs b/src/decompressors/tar.rs index 5b23e9df5..7e11d6e4a 100644 --- a/src/decompressors/tar.rs +++ b/src/decompressors/tar.rs @@ -17,7 +17,7 @@ impl TarDecompressor { println!("{}: attempting to decompress {:?}", "ouch".bright_blue(), &from.path); let mut files_unpacked = vec![]; - let mut archive: Archive> = match from.contents { + let mut archive: Archive> = match from.contents_in_memory { Some(bytes) => { tar::Archive::new(Box::new(Cursor::new(bytes))) } diff --git a/src/decompressors/zip.rs b/src/decompressors/zip.rs index 64405ae25..4e5bc5ed5 100644 --- a/src/decompressors/zip.rs +++ b/src/decompressors/zip.rs @@ -80,7 +80,7 @@ impl ZipDecompressor { &from.path ); - match from.contents { + match from.contents_in_memory { Some(bytes) => { let mut archive = zip::ZipArchive::new(Cursor::new(bytes))?; Ok(Self::zip_decompress(&mut archive, into)?) diff --git a/src/evaluator.rs b/src/evaluator.rs index 9b715dc7c..446bf583b 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -2,25 +2,33 @@ use std::{ffi::OsStr, fs, io::Write, path::PathBuf}; use colored::Colorize; -use crate::{compressors::{Entry, TarCompressor}, decompressors::TarDecompressor}; -use crate::decompressors::ZipDecompressor; -use crate::{ - cli::{Command, CommandKind}, - decompressors::{ - Decompressor, - DecompressionResult, - NifflerDecompressor - }, - compressors::Compressor, - error::{self, OuchResult}, - extension::{ - Extension, - CompressionFormat, - }, - file::File, - utils, +use crate::compressors::{ + Entry, + Compressor, + TarCompressor, + ZipCompressor }; +use crate::decompressors::{ + Decompressor, + TarDecompressor, + ZipDecompressor, + NifflerDecompressor, + DecompressionResult +}; + +use crate::extension::{ + Extension, + CompressionFormat +}; + +use crate::cli::{Command, CommandKind}; + +use crate::error::{self, OuchResult}; + +use crate::file::File; + +use crate::utils; pub struct Evaluator { // verbosity: Verbosity @@ -46,7 +54,7 @@ impl Evaluator { Some(ext) => match ext { CompressionFormat::Tar => Some(Box::new(TarCompressor {})), - // CompressionFormat::Zip => Some(Box::new(ZipCompressor {})), + CompressionFormat::Zip => Some(Box::new(ZipCompressor {})), // _other => Some(Box::new(NifflerCompressor {})), _other => { @@ -60,6 +68,7 @@ impl Evaluator { // any let second_compressor: Box = match extension.second_ext { CompressionFormat::Tar => Box::new(TarCompressor {}), + CompressionFormat::Zip => Box::new(ZipCompressor {}), _other => todo!() // }; @@ -138,7 +147,7 @@ impl Evaluator { let file = File { path: filename, - contents: Some(bytes), + contents_in_memory: Some(bytes), extension, }; @@ -166,7 +175,7 @@ impl Evaluator { let mut entry = Entry::Files(files); let bytes = first_compressor.compress(entry)?; - output.contents = Some(bytes); + output.contents_in_memory = Some(bytes); entry = Entry::InMemory(output); diff --git a/src/file.rs b/src/file.rs index 22391acf1..fb4e634b6 100644 --- a/src/file.rs +++ b/src/file.rs @@ -9,7 +9,7 @@ pub struct File { pub path: PathBuf, /// The bytes that compose the file. /// Only used when the whole file is kept in-memory - pub contents: Option>, + pub contents_in_memory: Option>, /// Note: extension here might be a misleading name since /// we don't really care about any extension other than supported compression ones. /// @@ -22,7 +22,7 @@ impl From<(PathBuf, Extension)> for File { fn from((path, format): (PathBuf, Extension)) -> Self { Self { path, - contents: None, + contents_in_memory: None, extension: Some(format), } } diff --git a/src/main.rs b/src/main.rs index 02e4375c7..02c4d88da 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ -use std::{convert::TryFrom}; +use std::{convert::TryFrom, io::Write}; use colored::Colorize; +use walkdir::WalkDir; mod cli; mod error; @@ -29,6 +30,31 @@ fn main() -> error::OuchResult<()>{ print_error(err) } } - + Ok(()) } + +// fn main() { +// use zip::ZipWriter; + +// let buf = vec![]; +// let mut writer = zip::ZipWriter::new(std::io::Cursor::new(buf)); + +// let options = zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Deflated); + + +// for entry in WalkDir::new("src/compressors/compressor.rs") { +// let entry = entry.unwrap(); +// let entry_path = entry.path().clone(); +// if entry_path.is_dir() { +// continue; +// } +// writer.start_file(entry_path.to_string_lossy(), options).unwrap(); +// let file_bytes = std::fs::read(entry.path()).unwrap(); +// writer.write(&*file_bytes).unwrap(); +// } + +// let bytes = writer.finish().unwrap(); + +// std::fs::write("mainmain.rar", bytes.into_inner()).unwrap(); +// } \ No newline at end of file diff --git a/src/test.rs b/src/test.rs index e7e151752..1aed1926d 100644 --- a/src/test.rs +++ b/src/test.rs @@ -23,13 +23,13 @@ mod cli { kind: Decompression(vec![ File { path: "file.zip".into(), - contents: None, + contents_in_memory: None, extension: Some(Extension::from(Zip)) } ]), output: Some(File { path: "folder".into(), - contents: None, + contents_in_memory: None, extension: None }), } @@ -49,12 +49,12 @@ mod cli { kind: Decompression(vec![ File { path: "file.zip".into(), - contents: None, + contents_in_memory: None, extension: Some(Extension::from(Zip)) }, File { path: "file.tar".into(), - contents: None, + contents_in_memory: None, extension: Some(Extension::from(Tar)) } ],), @@ -89,7 +89,7 @@ mod cli { output: Some( File { path: "file.tar".into(), - contents: None, + contents_in_memory: None, extension: Some(Extension::from(Tar)) } ), From f8ca0e3c56f77eb644a3fa4682587432f1f15913 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Tue, 23 Mar 2021 23:17:48 -0300 Subject: [PATCH 26/67] Turns out LZMA decompression is not working --- Cargo.lock | 168 +--- Cargo.toml | 12 +- README.md | 6 +- src/compressors/mod.rs | 1 + src/compressors/unified.rs | 0 src/compressors/zip.rs | 1 - src/decompressors/mod.rs | 6 +- src/decompressors/niffler.rs | 54 -- src/decompressors/unified.rs | 81 ++ src/error.rs | 34 +- src/evaluator.rs | 13 +- third-party/zip/.gitignore | 5 + third-party/zip/CODE_OF_CONDUCT.md | 77 ++ third-party/zip/Cargo.toml | 35 + third-party/zip/LICENSE | 21 + third-party/zip/README.md | 70 ++ third-party/zip/benches/read_entry.rs | 43 + third-party/zip/examples/extract.rs | 63 ++ third-party/zip/examples/extract_lorem.rs | 31 + third-party/zip/examples/file_info.rs | 53 + third-party/zip/examples/stdin_info.rs | 34 + third-party/zip/examples/write_dir.rs | 120 +++ third-party/zip/examples/write_sample.rs | 71 ++ third-party/zip/src/compression.rs | 180 ++++ third-party/zip/src/cp437.rs | 203 ++++ third-party/zip/src/crc32.rs | 93 ++ third-party/zip/src/lib.rs | 21 + third-party/zip/src/read.rs | 1078 +++++++++++++++++++++ third-party/zip/src/result.rs | 39 + third-party/zip/src/spec.rs | 182 ++++ third-party/zip/src/types.rs | 474 +++++++++ third-party/zip/src/write.rs | 821 ++++++++++++++++ third-party/zip/src/zipcrypto.rs | 162 ++++ 33 files changed, 4013 insertions(+), 239 deletions(-) create mode 100644 src/compressors/unified.rs delete mode 100644 src/decompressors/niffler.rs create mode 100644 src/decompressors/unified.rs create mode 100644 third-party/zip/.gitignore create mode 100644 third-party/zip/CODE_OF_CONDUCT.md create mode 100644 third-party/zip/Cargo.toml create mode 100644 third-party/zip/LICENSE create mode 100644 third-party/zip/README.md create mode 100644 third-party/zip/benches/read_entry.rs create mode 100644 third-party/zip/examples/extract.rs create mode 100644 third-party/zip/examples/extract_lorem.rs create mode 100644 third-party/zip/examples/file_info.rs create mode 100644 third-party/zip/examples/stdin_info.rs create mode 100644 third-party/zip/examples/write_dir.rs create mode 100644 third-party/zip/examples/write_sample.rs create mode 100644 third-party/zip/src/compression.rs create mode 100644 third-party/zip/src/cp437.rs create mode 100644 third-party/zip/src/crc32.rs create mode 100644 third-party/zip/src/lib.rs create mode 100644 third-party/zip/src/read.rs create mode 100644 third-party/zip/src/result.rs create mode 100644 third-party/zip/src/spec.rs create mode 100644 third-party/zip/src/types.rs create mode 100644 third-party/zip/src/write.rs create mode 100644 third-party/zip/src/zipcrypto.rs diff --git a/Cargo.lock b/Cargo.lock index fbf82891d..699d6fa5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,26 +1,11 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -[[package]] -name = "addr2line" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55f82cfe485775d02112886f4169bde0c5894d75e79ead7eafe7e40a25e45f7" -dependencies = [ - "gimli", -] - [[package]] name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" -[[package]] -name = "adler32" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" - [[package]] name = "ansi_term" version = "0.11.0" @@ -47,30 +32,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" -[[package]] -name = "backtrace" -version = "0.3.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d117600f438b1707d4e4ae15d3595657288f8235a0eb593e80ecc98ab34e1bc" -dependencies = [ - "addr2line", - "cfg-if 1.0.0", - "libc", - "miniz_oxide 0.4.4", - "object", - "rustc-demangle", -] - -[[package]] -name = "bgzip" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adff113e9fe73a6d0c4efd0f143857bd6bdad40743542f3f57464398c532234f" -dependencies = [ - "failure", - "flate2", -] - [[package]] name = "bitflags" version = "1.2.1" @@ -120,12 +81,6 @@ version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" -[[package]] -name = "cfg-if" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" - [[package]] name = "cfg-if" version = "1.0.0" @@ -164,38 +119,7 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" dependencies = [ - "cfg-if 1.0.0", -] - -[[package]] -name = "enum_primitive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4551092f4d519593039259a9ed8daedf0da12e5109c5280338073eaeb81180" -dependencies = [ - "num-traits 0.1.43", -] - -[[package]] -name = "failure" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" -dependencies = [ - "backtrace", - "failure_derive", -] - -[[package]] -name = "failure_derive" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", + "cfg-if", ] [[package]] @@ -204,7 +128,7 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", "redox_syscall", "winapi", @@ -212,22 +136,16 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.14" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cfff41391129e0a856d6d822600b8d71179d46879e310417eb9c762eb178b42" +checksum = "cd3aec53de10fe96d7d8c565eb17f2c687bb5518a2ec453b5b1252964526abe0" dependencies = [ - "cfg-if 0.1.10", + "cfg-if", "crc32fast", "libc", - "miniz_oxide 0.3.7", + "miniz_oxide", ] -[[package]] -name = "gimli" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce" - [[package]] name = "hermit-abi" version = "0.1.18" @@ -260,15 +178,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "miniz_oxide" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" -dependencies = [ - "adler32", -] - [[package]] name = "miniz_oxide" version = "0.4.4" @@ -279,54 +188,17 @@ dependencies = [ "autocfg", ] -[[package]] -name = "niffler" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ea40fb13399dd0e9780ea3d82cb6cf27f489f63d820aa7dc9ec967750dc6d58" -dependencies = [ - "bgzip", - "bzip2 0.4.2", - "cfg-if 1.0.0", - "enum_primitive", - "flate2", - "thiserror", - "xz2", -] - -[[package]] -name = "num-traits" -version = "0.1.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31" -dependencies = [ - "num-traits 0.2.14", -] - -[[package]] -name = "num-traits" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" -dependencies = [ - "autocfg", -] - -[[package]] -name = "object" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a7ab5d64814df0fe4a4b5ead45ed6c5f181ee3ff04ba344313a6c80446c5d4" - [[package]] name = "ouch" version = "0.1.0" dependencies = [ + "bzip2 0.4.2", "clap", "colored", - "niffler", + "flate2", "tar", "walkdir", + "xz2", "zip", ] @@ -363,12 +235,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "rustc-demangle" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232" - [[package]] name = "same-file" version = "1.0.6" @@ -395,18 +261,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "synstructure" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "unicode-xid", -] - [[package]] name = "tar" version = "0.4.33" @@ -544,9 +398,7 @@ dependencies = [ [[package]] name = "zip" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8264fcea9b7a036a4a5103d7153e988dbc2ebbafb34f68a3c2d404b6b82d74b6" +version = "0.5.10" dependencies = [ "byteorder", "bzip2 0.3.3", diff --git a/Cargo.toml b/Cargo.toml index 2a0317e35..e6162a9bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,8 +8,14 @@ edition = "2018" [dependencies] colored = "2.0.0" -niffler = "2.3.1" walkdir = "2.3.2" clap = "2.33.3" -zip = "0.5.11" -tar = "0.4.33" \ No newline at end of file +tar = "0.4.33" +xz2 = "0.1" +bzip2 = "0.4.2" +flate2 = "1.0.20" + +# Keeping zip locally since upstream zip is staying on an older flate2 version +# in order to not increase MSRV, which is not something I particularly care about +# for ouch +zip = { path = "./third-party/zip" } \ No newline at end of file diff --git a/README.md b/README.md index ff0a36487..8f1dc6f16 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,10 @@ `ouch` is the Obvious Unified Compression (and decompression) Helper. -| Supported formats | .tar | .zip | .tar.{.lz, .lzma, .gz, .bz} | .zip.{.lz, .lzma, .gz, .bz} | .bz | .gz | .lz, .lzma | +| Supported formats | .tar | .zip | .tar.{.gz, .bz} | .zip.{.gz, .bz, .bz2} | .bz | .gz | .lz, .lzma | |-------------------|------|------|------------------------------|------------------------------|-----|-----|------------| -| Decompression | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Compression | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | +| Decompression | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | +| Compression | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ## How does it work? diff --git a/src/compressors/mod.rs b/src/compressors/mod.rs index 064a9543b..c9c9d1320 100644 --- a/src/compressors/mod.rs +++ b/src/compressors/mod.rs @@ -1,5 +1,6 @@ mod tar; mod zip; +mod unified; mod compressor; pub use compressor::Compressor; diff --git a/src/compressors/unified.rs b/src/compressors/unified.rs new file mode 100644 index 000000000..e69de29bb diff --git a/src/compressors/zip.rs b/src/compressors/zip.rs index 811a1970a..4b4dff816 100644 --- a/src/compressors/zip.rs +++ b/src/compressors/zip.rs @@ -31,7 +31,6 @@ impl ZipCompressor { let options = zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Deflated); - // let ok = Box::from(inner_file_path.to_string_lossy()); writer.start_file(inner_file_path, options)?; let input_bytes = match input.contents_in_memory { diff --git a/src/decompressors/mod.rs b/src/decompressors/mod.rs index 8c3880af5..b50067856 100644 --- a/src/decompressors/mod.rs +++ b/src/decompressors/mod.rs @@ -1,10 +1,12 @@ mod decompressor; mod tar; mod zip; -mod niffler; +mod unified; pub use decompressor::Decompressor; pub use decompressor::DecompressionResult; pub use self::tar::TarDecompressor; pub use self::zip::ZipDecompressor; -pub use self::niffler::NifflerDecompressor; \ No newline at end of file +pub use self::unified::GzipDecompressor; +pub use self::unified::BzipDecompressor; +pub use self::unified::LzmaDecompressor; \ No newline at end of file diff --git a/src/decompressors/niffler.rs b/src/decompressors/niffler.rs deleted file mode 100644 index 3d665832d..000000000 --- a/src/decompressors/niffler.rs +++ /dev/null @@ -1,54 +0,0 @@ -use std::{io::Read, path::Path}; - -use colored::Colorize; -use niffler; - -use crate::file::File; -use crate::{ - error::{self, OuchResult}, - utils, -}; - -use super::decompressor::Decompressor; -use super::decompressor::DecompressionResult; - -pub struct NifflerDecompressor {} - -impl NifflerDecompressor { - fn unpack_file(from: &Path) -> OuchResult> { - - // println!("{}: trying to decompress {:?}", "info".yellow(), from); - - let file = std::fs::read(from)?; - - let (mut reader, compression) = niffler::get_reader(Box::new(&file[..]))?; - - match compression { - niffler::Format::No => { - return Err(error::Error::InvalidInput); - }, - other => { - println!("{}: {:?} detected.", "info".yellow(), other); - } - } - - let mut buffer = Vec::new(); - let bytes_read = reader.read_to_end(&mut buffer)?; - - println!("{}: {:?} extracted into memory ({} bytes).", "info".yellow(), from, bytes_read); - - Ok(buffer) - } -} - -impl Decompressor for NifflerDecompressor { - fn decompress(&self, from: File, into: &Option) -> OuchResult { - let destination_path = utils::get_destination_path(into); - - utils::create_path_if_non_existent(destination_path)?; - - let bytes = Self::unpack_file(&from.path)?; - - Ok(DecompressionResult::FileInMemory(bytes)) - } -} diff --git a/src/decompressors/unified.rs b/src/decompressors/unified.rs new file mode 100644 index 000000000..c116be1df --- /dev/null +++ b/src/decompressors/unified.rs @@ -0,0 +1,81 @@ +use std::{ + io::{self, Read}, + path::{Path, PathBuf}, +}; + +use bzip2::Compress; +use colored::Colorize; +// use niffler; + +use crate::{extension::CompressionFormat, file::File}; +use crate::{ + error::{self, OuchResult}, + utils, +}; + +use super::decompressor::DecompressionResult; +use super::decompressor::Decompressor; + +pub struct UnifiedDecompressor {} +pub struct LzmaDecompressor {} +pub struct GzipDecompressor {} +pub struct BzipDecompressor {} + +fn get_decoder<'a>(format: CompressionFormat, buffer: Box) -> Box { + match format { + CompressionFormat::Lzma => Box::new(xz2::read::XzDecoder::new(buffer)), + CompressionFormat::Bzip => Box::new(bzip2::read::BzDecoder::new(buffer)), + CompressionFormat::Gzip => Box::new(flate2::read::MultiGzDecoder::new(buffer)), + other => unreachable!() + } +} + +impl UnifiedDecompressor { + fn unpack_file(from: &Path, format: CompressionFormat) -> OuchResult> { + // println!("{}: trying to decompress {:?}", "info".yellow(), from); + + let file = std::fs::read(from)?; + + let mut reader = get_decoder(format, Box::new(&file[..])); + + let mut buffer = Vec::new(); + let bytes_read = reader.read_to_end(&mut buffer)?; + + println!( + "{}: {:?} extracted into memory ({} bytes).", + "info".yellow(), + from, + bytes_read + ); + + Ok(buffer) + } + + fn decompress(from: File, format: CompressionFormat, into: &Option) -> OuchResult { + let destination_path = utils::get_destination_path(into); + + utils::create_path_if_non_existent(destination_path)?; + + let bytes = Self::unpack_file(&from.path, format)?; + + Ok(DecompressionResult::FileInMemory(bytes)) + } +} + +impl Decompressor for LzmaDecompressor { + fn decompress(&self, from: File, into: &Option) -> OuchResult { + UnifiedDecompressor::decompress(from, CompressionFormat::Lzma, into) + } +} + +impl Decompressor for GzipDecompressor { + fn decompress(&self, from: File, into: &Option) -> OuchResult { + UnifiedDecompressor::decompress(from, CompressionFormat::Gzip, into) + } +} + +impl Decompressor for BzipDecompressor { + fn decompress(&self, from: File, into: &Option) -> OuchResult { + UnifiedDecompressor::decompress(from, CompressionFormat::Bzip, into) + } +} \ No newline at end of file diff --git a/src/error.rs b/src/error.rs index 9cd2223a1..008393b51 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,4 @@ -use std::fmt; +use std::{fmt, path::PathBuf}; use colored::Colorize; @@ -13,6 +13,7 @@ pub enum Error { FileNotFound, AlreadyExists, InvalidZipArchive(&'static str), + UnsupportedArchive(PathBuf), PermissionDenied, UnsupportedZipArchive(&'static str), FileTooShort, @@ -39,6 +40,9 @@ impl fmt::Display for Error { Error::FileNotFound => { write!(f, "file not found!") } + Error::UnsupportedArchive(path) => { + write!(f, "ouch is currently uncapable of decompressing {:?}", path) + } err => { // TODO write!(f, "todo: missing description for error {:?}", err) @@ -73,20 +77,20 @@ impl From for Error { } } -impl From for Error { - fn from(err: niffler::error::Error) -> Self { - use niffler::error::Error as NifErr; - match err { - NifErr::FeatureDisabled => { - // Ouch is using Niffler with all its features so - // this should be unreachable. - unreachable!(); - }, - NifErr::FileTooShort => Self::FileTooShort, - NifErr::IOError(io_err) => Self::from(io_err) - } - } -} +// impl From for Error { +// fn from(err: niffler::error::Error) -> Self { +// use niffler::error::Error as NifErr; +// match err { +// NifErr::FeatureDisabled => { +// // Ouch is using Niffler with all its features so +// // this should be unreachable. +// unreachable!(); +// }, +// NifErr::FileTooShort => Self::FileTooShort, +// NifErr::IOError(io_err) => Self::from(io_err) +// } +// } +// } impl From for Error { fn from(err: walkdir::Error) -> Self { diff --git a/src/evaluator.rs b/src/evaluator.rs index 446bf583b..1eb1be542 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -13,7 +13,8 @@ use crate::decompressors::{ Decompressor, TarDecompressor, ZipDecompressor, - NifflerDecompressor, + GzipDecompressor, + BzipDecompressor, DecompressionResult }; @@ -94,8 +95,14 @@ impl Evaluator { CompressionFormat::Zip => Box::new(ZipDecompressor {}), - CompressionFormat::Gzip | CompressionFormat::Lzma | CompressionFormat::Bzip => { - Box::new(NifflerDecompressor {}) + CompressionFormat::Gzip => Box::new(GzipDecompressor {}), + + CompressionFormat::Lzma => { + todo!() + } + + CompressionFormat::Bzip => { + Box::new(BzipDecompressor {}) } }; diff --git a/third-party/zip/.gitignore b/third-party/zip/.gitignore new file mode 100644 index 000000000..0a8fc033c --- /dev/null +++ b/third-party/zip/.gitignore @@ -0,0 +1,5 @@ +Cargo.lock +target + +\.idea/ +tests/ diff --git a/third-party/zip/CODE_OF_CONDUCT.md b/third-party/zip/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..845634eb5 --- /dev/null +++ b/third-party/zip/CODE_OF_CONDUCT.md @@ -0,0 +1,77 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to make participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies within all project spaces, and it also applies when +an individual is representing the project or its community in public spaces. +Examples of representing a project or community include using an official +project e-mail address, posting via an official social media account, or acting +as an appointed representative at an online or offline event. Representation of +a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at ryan.levick@gmail.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/third-party/zip/Cargo.toml b/third-party/zip/Cargo.toml new file mode 100644 index 000000000..0a8583a1b --- /dev/null +++ b/third-party/zip/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "zip" +version = "0.5.10" +authors = ["Mathijs van de Nes ", "Marli Frost ", "Ryan Levick "] +license = "MIT" +repository = "https://github.com/zip-rs/zip.git" +keywords = ["zip", "archive"] +description = """ +Library to support the reading and writing of zip files. +""" +edition = "2018" + +[dependencies] + +flate2 = { version = "1.0.20", default-features = false, optional = true } +time = { version = "0.1", optional = true } +byteorder = "1.3" +bzip2 = { version = "0.3", optional = true } +crc32fast = "1.0" +thiserror = "1.0" + +[dev-dependencies] +bencher = "0.1" +rand = "0.7" +walkdir = "2" + +[features] +deflate = ["flate2/rust_backend"] +deflate-miniz = ["flate2/default"] +deflate-zlib = ["flate2/zlib"] +default = ["bzip2", "deflate", "time"] + +[[bench]] +name = "read_entry" +harness = false diff --git a/third-party/zip/LICENSE b/third-party/zip/LICENSE new file mode 100644 index 000000000..b2d7f7bbd --- /dev/null +++ b/third-party/zip/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Mathijs van de Nes + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/third-party/zip/README.md b/third-party/zip/README.md new file mode 100644 index 000000000..79d2dcc7c --- /dev/null +++ b/third-party/zip/README.md @@ -0,0 +1,70 @@ +zip-rs +====== + +[![Build Status](https://img.shields.io/github/workflow/status/zip-rs/zip/CI)](https://github.com/zip-rs/zip/actions?query=branch%3Amaster+workflow%3ACI) +[![Crates.io version](https://img.shields.io/crates/v/zip.svg)](https://crates.io/crates/zip) + +[Documentation](https://docs.rs/zip/0.5.10/zip/) + + +Info +---- + +A zip library for rust which supports reading and writing of simple ZIP files. + +Supported compression formats: + +* stored (i.e. none) +* deflate +* bzip2 + +Currently unsupported zip extensions: + +* Encryption +* Multi-disk + +Usage +----- + +With all default features: + +```toml +[dependencies] +zip = "0.5" +``` + +Without the default features: + +```toml +[dependencies] +zip = { version = "0.5", default-features = false } +``` + +The features available are: + +* `deflate`: Enables the deflate compression algorithm, which is the default for zipfiles +* `bzip2`: Enables the BZip2 compression algorithm. +* `time`: Enables features using the [time](https://github.com/rust-lang-deprecated/time) crate. + +All of these are enabled by default. + +MSRV +---- + +Our current Minimum Supported Rust Version is **1.34.0**. When adding features, +we will follow these guidelines: + +- We will always support the latest four minor Rust versions. This gives you a 6 + month window to upgrade your compiler. +- Any change to the MSRV will be accompanied with a **minor** version bump + - While the crate is pre-1.0, this will be a change to the PATCH version. + +Examples +-------- + +See the [examples directory](examples) for: + * How to write a file to a zip. + * How to write a directory of files to a zip (using [walkdir](https://github.com/BurntSushi/walkdir)). + * How to extract a zip file. + * How to extract a single file from a zip. + * How to read a zip from the standard input. diff --git a/third-party/zip/benches/read_entry.rs b/third-party/zip/benches/read_entry.rs new file mode 100644 index 000000000..25c0b94ae --- /dev/null +++ b/third-party/zip/benches/read_entry.rs @@ -0,0 +1,43 @@ +use bencher::{benchmark_group, benchmark_main}; + +use std::io::{Cursor, Read, Write}; + +use bencher::Bencher; +use rand::Rng; +use zip::{ZipArchive, ZipWriter}; + +fn generate_random_archive(size: usize) -> Vec { + let data = Vec::new(); + let mut writer = ZipWriter::new(Cursor::new(data)); + let options = + zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Stored); + + writer.start_file("random.dat", options).unwrap(); + let mut bytes = vec![0u8; size]; + rand::thread_rng().fill_bytes(&mut bytes); + writer.write_all(&bytes).unwrap(); + + writer.finish().unwrap().into_inner() +} + +fn read_entry(bench: &mut Bencher) { + let size = 1024 * 1024; + let bytes = generate_random_archive(size); + let mut archive = ZipArchive::new(Cursor::new(bytes.as_slice())).unwrap(); + + bench.iter(|| { + let mut file = archive.by_name("random.dat").unwrap(); + let mut buf = [0u8; 1024]; + loop { + let n = file.read(&mut buf).unwrap(); + if n == 0 { + break; + } + } + }); + + bench.bytes = size as u64; +} + +benchmark_group!(benches, read_entry); +benchmark_main!(benches); diff --git a/third-party/zip/examples/extract.rs b/third-party/zip/examples/extract.rs new file mode 100644 index 000000000..05c5a4aad --- /dev/null +++ b/third-party/zip/examples/extract.rs @@ -0,0 +1,63 @@ +use std::fs; +use std::io; + +fn main() { + std::process::exit(real_main()); +} + +fn real_main() -> i32 { + let args: Vec<_> = std::env::args().collect(); + if args.len() < 2 { + println!("Usage: {} ", args[0]); + return 1; + } + let fname = std::path::Path::new(&*args[1]); + let file = fs::File::open(&fname).unwrap(); + + let mut archive = zip::ZipArchive::new(file).unwrap(); + + for i in 0..archive.len() { + let mut file = archive.by_index(i).unwrap(); + let outpath = match file.enclosed_name() { + Some(path) => path.to_owned(), + None => continue, + }; + + { + let comment = file.comment(); + if !comment.is_empty() { + println!("File {} comment: {}", i, comment); + } + } + + if (&*file.name()).ends_with('/') { + println!("File {} extracted to \"{}\"", i, outpath.display()); + fs::create_dir_all(&outpath).unwrap(); + } else { + println!( + "File {} extracted to \"{}\" ({} bytes)", + i, + outpath.display(), + file.size() + ); + if let Some(p) = outpath.parent() { + if !p.exists() { + fs::create_dir_all(&p).unwrap(); + } + } + let mut outfile = fs::File::create(&outpath).unwrap(); + io::copy(&mut file, &mut outfile).unwrap(); + } + + // Get and Set permissions + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + + if let Some(mode) = file.unix_mode() { + fs::set_permissions(&outpath, fs::Permissions::from_mode(mode)).unwrap(); + } + } + } + return 0; +} diff --git a/third-party/zip/examples/extract_lorem.rs b/third-party/zip/examples/extract_lorem.rs new file mode 100644 index 000000000..89e33ef9a --- /dev/null +++ b/third-party/zip/examples/extract_lorem.rs @@ -0,0 +1,31 @@ +use std::io::prelude::*; + +fn main() { + std::process::exit(real_main()); +} + +fn real_main() -> i32 { + let args: Vec<_> = std::env::args().collect(); + if args.len() < 2 { + println!("Usage: {} ", args[0]); + return 1; + } + let fname = std::path::Path::new(&*args[1]); + let zipfile = std::fs::File::open(&fname).unwrap(); + + let mut archive = zip::ZipArchive::new(zipfile).unwrap(); + + let mut file = match archive.by_name("test/lorem_ipsum.txt") { + Ok(file) => file, + Err(..) => { + println!("File test/lorem_ipsum.txt not found"); + return 2; + } + }; + + let mut contents = String::new(); + file.read_to_string(&mut contents).unwrap(); + println!("{}", contents); + + return 0; +} diff --git a/third-party/zip/examples/file_info.rs b/third-party/zip/examples/file_info.rs new file mode 100644 index 000000000..315b5c386 --- /dev/null +++ b/third-party/zip/examples/file_info.rs @@ -0,0 +1,53 @@ +use std::fs; +use std::io::BufReader; + +fn main() { + std::process::exit(real_main()); +} + +fn real_main() -> i32 { + let args: Vec<_> = std::env::args().collect(); + if args.len() < 2 { + println!("Usage: {} ", args[0]); + return 1; + } + let fname = std::path::Path::new(&*args[1]); + let file = fs::File::open(&fname).unwrap(); + let reader = BufReader::new(file); + + let mut archive = zip::ZipArchive::new(reader).unwrap(); + + for i in 0..archive.len() { + let file = archive.by_index(i).unwrap(); + let outpath = match file.enclosed_name() { + Some(path) => path, + None => { + println!("Entry {} has a suspicious path", file.name()); + continue; + } + }; + + { + let comment = file.comment(); + if !comment.is_empty() { + println!("Entry {} comment: {}", i, comment); + } + } + + if (&*file.name()).ends_with('/') { + println!( + "Entry {} is a directory with name \"{}\"", + i, + outpath.display() + ); + } else { + println!( + "Entry {} is a file with name \"{}\" ({} bytes)", + i, + outpath.display(), + file.size() + ); + } + } + return 0; +} diff --git a/third-party/zip/examples/stdin_info.rs b/third-party/zip/examples/stdin_info.rs new file mode 100644 index 000000000..606944ce0 --- /dev/null +++ b/third-party/zip/examples/stdin_info.rs @@ -0,0 +1,34 @@ +use std::io::{self, Read}; + +fn main() { + std::process::exit(real_main()); +} + +fn real_main() -> i32 { + let stdin = io::stdin(); + let mut stdin_handle = stdin.lock(); + let mut buf = [0u8; 16]; + + loop { + match zip::read::read_zipfile_from_stream(&mut stdin_handle) { + Ok(Some(mut file)) => { + println!( + "{}: {} bytes ({} bytes packed)", + file.name(), + file.size(), + file.compressed_size() + ); + match file.read(&mut buf) { + Ok(n) => println!("The first {} bytes are: {:?}", n, &buf[0..n]), + Err(e) => println!("Could not read the file: {:?}", e), + }; + } + Ok(None) => break, + Err(e) => { + println!("Error encountered while reading zip: {:?}", e); + return 1; + } + } + } + return 0; +} diff --git a/third-party/zip/examples/write_dir.rs b/third-party/zip/examples/write_dir.rs new file mode 100644 index 000000000..793bd6ba6 --- /dev/null +++ b/third-party/zip/examples/write_dir.rs @@ -0,0 +1,120 @@ +use std::io::prelude::*; +use std::io::{Seek, Write}; +use std::iter::Iterator; +use zip::result::ZipError; +use zip::write::FileOptions; + +use std::fs::File; +use std::path::Path; +use walkdir::{DirEntry, WalkDir}; + +fn main() { + std::process::exit(real_main()); +} + +const METHOD_STORED: Option = Some(zip::CompressionMethod::Stored); + +#[cfg(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib" +))] +const METHOD_DEFLATED: Option = Some(zip::CompressionMethod::Deflated); +#[cfg(not(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib" +)))] +const METHOD_DEFLATED: Option = None; + +#[cfg(feature = "bzip2")] +const METHOD_BZIP2: Option = Some(zip::CompressionMethod::Bzip2); +#[cfg(not(feature = "bzip2"))] +const METHOD_BZIP2: Option = None; + +fn real_main() -> i32 { + let args: Vec<_> = std::env::args().collect(); + if args.len() < 3 { + println!( + "Usage: {} ", + args[0] + ); + return 1; + } + + let src_dir = &*args[1]; + let dst_file = &*args[2]; + for &method in [METHOD_STORED, METHOD_DEFLATED, METHOD_BZIP2].iter() { + if method.is_none() { + continue; + } + match doit(src_dir, dst_file, method.unwrap()) { + Ok(_) => println!("done: {} written to {}", src_dir, dst_file), + Err(e) => println!("Error: {:?}", e), + } + } + + return 0; +} + +fn zip_dir( + it: &mut dyn Iterator, + prefix: &str, + writer: T, + method: zip::CompressionMethod, +) -> zip::result::ZipResult<()> +where + T: Write + Seek, +{ + let mut zip = zip::ZipWriter::new(writer); + let options = FileOptions::default() + .compression_method(method) + .unix_permissions(0o755); + + let mut buffer = Vec::new(); + for entry in it { + let path = entry.path(); + let name = path.strip_prefix(Path::new(prefix)).unwrap(); + + // Write file or directory explicitly + // Some unzip tools unzip files with directory paths correctly, some do not! + if path.is_file() { + println!("adding file {:?} as {:?} ...", path, name); + #[allow(deprecated)] + zip.start_file_from_path(name, options)?; + let mut f = File::open(path)?; + + f.read_to_end(&mut buffer)?; + zip.write_all(&*buffer)?; + buffer.clear(); + } else if name.as_os_str().len() != 0 { + // Only if not root! Avoids path spec / warning + // and mapname conversion failed error on unzip + println!("adding dir {:?} as {:?} ...", path, name); + #[allow(deprecated)] + zip.add_directory_from_path(name, options)?; + } + } + zip.finish()?; + Result::Ok(()) +} + +fn doit( + src_dir: &str, + dst_file: &str, + method: zip::CompressionMethod, +) -> zip::result::ZipResult<()> { + if !Path::new(src_dir).is_dir() { + return Err(ZipError::FileNotFound); + } + + let path = Path::new(dst_file); + let file = File::create(&path).unwrap(); + + let walkdir = WalkDir::new(src_dir.to_string()); + let it = walkdir.into_iter(); + + zip_dir(&mut it.filter_map(|e| e.ok()), src_dir, file, method)?; + + Ok(()) +} diff --git a/third-party/zip/examples/write_sample.rs b/third-party/zip/examples/write_sample.rs new file mode 100644 index 000000000..4ef5ce347 --- /dev/null +++ b/third-party/zip/examples/write_sample.rs @@ -0,0 +1,71 @@ +use std::io::prelude::*; +use zip::write::FileOptions; + +fn main() { + std::process::exit(real_main()); +} + +fn real_main() -> i32 { + let args: Vec<_> = std::env::args().collect(); + if args.len() < 2 { + println!("Usage: {} ", args[0]); + return 1; + } + + let filename = &*args[1]; + match doit(filename) { + Ok(_) => println!("File written to {}", filename), + Err(e) => println!("Error: {:?}", e), + } + + return 0; +} + +fn doit(filename: &str) -> zip::result::ZipResult<()> { + let path = std::path::Path::new(filename); + let file = std::fs::File::create(&path).unwrap(); + + let mut zip = zip::ZipWriter::new(file); + + zip.add_directory("test/", Default::default())?; + + let options = FileOptions::default() + .compression_method(zip::CompressionMethod::Stored) + .unix_permissions(0o755); + zip.start_file("test/☃.txt", options)?; + zip.write_all(b"Hello, World!\n")?; + + zip.start_file("test/lorem_ipsum.txt", Default::default())?; + zip.write_all(LOREM_IPSUM)?; + + zip.finish()?; + Ok(()) +} + +const LOREM_IPSUM : &'static [u8] = b"Lorem ipsum dolor sit amet, consectetur adipiscing elit. In tellus elit, tristique vitae mattis egestas, ultricies vitae risus. Quisque sit amet quam ut urna aliquet +molestie. Proin blandit ornare dui, a tempor nisl accumsan in. Praesent a consequat felis. Morbi metus diam, auctor in auctor vel, feugiat id odio. Curabitur ex ex, +dictum quis auctor quis, suscipit id lorem. Aliquam vestibulum dolor nec enim vehicula, porta tristique augue tincidunt. Vivamus ut gravida est. Sed pellentesque, dolor +vitae tristique consectetur, neque lectus pulvinar dui, sed feugiat purus diam id lectus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per +inceptos himenaeos. Maecenas feugiat velit in ex ultrices scelerisque id id neque. + +Phasellus sed nisi in augue sodales pulvinar ut et leo. Pellentesque eget leo vitae massa bibendum sollicitudin. Curabitur erat lectus, congue quis auctor sed, aliquet +bibendum est. Ut porta ultricies turpis at maximus. Cras non lobortis justo. Duis rutrum magna sed velit facilisis, et sagittis metus laoreet. Pellentesque quam ligula, +dapibus vitae mauris quis, dapibus cursus leo. Sed sit amet condimentum eros. Nulla vestibulum enim sit amet lorem pharetra, eu fringilla nisl posuere. Sed tristique non +nibh at viverra. Vivamus sed accumsan lacus, nec pretium eros. Mauris elementum arcu eu risus fermentum, tempor ullamcorper neque aliquam. Sed tempor in erat eu +suscipit. In euismod in libero in facilisis. Donec sagittis, odio et fermentum dignissim, risus justo pretium nibh, eget vestibulum lectus metus vel lacus. + +Quisque feugiat, magna ac feugiat ullamcorper, augue justo consequat felis, ut fermentum arcu lorem vitae ligula. Quisque iaculis tempor maximus. In quis eros ac tellus +aliquam placerat quis id tellus. Donec non gravida nulla. Morbi faucibus neque sed faucibus aliquam. Sed accumsan mattis nunc, non interdum justo. Cras vitae facilisis +leo. Fusce sollicitudin ultrices sagittis. Maecenas eget massa id lorem dignissim ultrices non et ligula. Pellentesque aliquam mi ac neque tempus ornare. Morbi non enim +vulputate quam ullamcorper finibus id non neque. Quisque malesuada commodo lorem, ut ornare velit iaculis rhoncus. Mauris vel maximus ex. + +Morbi eleifend blandit diam, non vulputate ante iaculis in. Donec pellentesque augue id enim suscipit, eget suscipit lacus commodo. Ut vel ex vitae elit imperdiet +vulputate. Nunc eu mattis orci, ut pretium sem. Nam vitae purus mollis ante tempus malesuada a at magna. Integer mattis lectus non luctus lobortis. In a cursus quam, +eget faucibus sem. + +Donec vitae condimentum nisi, non efficitur massa. Praesent sed mi in massa sollicitudin iaculis. Pellentesque a libero ultrices, sodales lacus eu, ornare dui. In +laoreet est nec dolor aliquam consectetur. Integer iaculis felis venenatis libero pulvinar, ut pretium odio interdum. Donec in nisi eu dolor varius vestibulum eget vel +nunc. Morbi a venenatis quam, in vehicula justo. Nam risus dui, auctor eu accumsan at, sagittis ac lectus. Mauris iaculis dignissim interdum. Cras cursus dapibus auctor. +Donec sagittis massa vitae tortor viverra vehicula. Mauris fringilla nunc eu lorem ultrices placerat. Maecenas posuere porta quam at semper. Praesent eu bibendum eros. +Nunc congue sollicitudin ante, sollicitudin lacinia magna cursus vitae. +"; diff --git a/third-party/zip/src/compression.rs b/third-party/zip/src/compression.rs new file mode 100644 index 000000000..5fdde0705 --- /dev/null +++ b/third-party/zip/src/compression.rs @@ -0,0 +1,180 @@ +//! Possible ZIP compression methods. + +use std::fmt; + +#[allow(deprecated)] +/// Identifies the storage format used to compress a file within a ZIP archive. +/// +/// Each file's compression method is stored alongside it, allowing the +/// contents to be read without context. +/// +/// When creating ZIP files, you may choose the method to use with +/// [`zip::write::FileOptions::compression_method`] +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum CompressionMethod { + /// Store the file as is + Stored, + /// Compress the file using Deflate + #[cfg(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib" + ))] + Deflated, + /// Compress the file using BZIP2 + #[cfg(feature = "bzip2")] + Bzip2, + /// Unsupported compression method + #[deprecated(since = "0.5.7", note = "use the constants instead")] + Unsupported(u16), +} +#[allow(deprecated, missing_docs)] +/// All compression methods defined for the ZIP format +impl CompressionMethod { + pub const STORE: Self = CompressionMethod::Stored; + pub const SHRINK: Self = CompressionMethod::Unsupported(1); + pub const REDUCE_1: Self = CompressionMethod::Unsupported(2); + pub const REDUCE_2: Self = CompressionMethod::Unsupported(3); + pub const REDUCE_3: Self = CompressionMethod::Unsupported(4); + pub const REDUCE_4: Self = CompressionMethod::Unsupported(5); + pub const IMPLODE: Self = CompressionMethod::Unsupported(6); + #[cfg(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib" + ))] + pub const DEFLATE: Self = CompressionMethod::Deflated; + #[cfg(not(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib" + )))] + pub const DEFLATE: Self = CompressionMethod::Unsupported(8); + pub const DEFLATE64: Self = CompressionMethod::Unsupported(9); + pub const PKWARE_IMPLODE: Self = CompressionMethod::Unsupported(10); + #[cfg(feature = "bzip2")] + pub const BZIP2: Self = CompressionMethod::Bzip2; + #[cfg(not(feature = "bzip2"))] + pub const BZIP2: Self = CompressionMethod::Unsupported(12); + pub const LZMA: Self = CompressionMethod::Unsupported(14); + pub const IBM_ZOS_CMPSC: Self = CompressionMethod::Unsupported(16); + pub const IBM_TERSE: Self = CompressionMethod::Unsupported(18); + pub const ZSTD_DEPRECATED: Self = CompressionMethod::Unsupported(20); + pub const ZSTD: Self = CompressionMethod::Unsupported(93); + pub const MP3: Self = CompressionMethod::Unsupported(94); + pub const XZ: Self = CompressionMethod::Unsupported(95); + pub const JPEG: Self = CompressionMethod::Unsupported(96); + pub const WAVPACK: Self = CompressionMethod::Unsupported(97); + pub const PPMD: Self = CompressionMethod::Unsupported(98); +} +impl CompressionMethod { + /// Converts an u16 to its corresponding CompressionMethod + #[deprecated( + since = "0.5.7", + note = "use a constant to construct a compression method" + )] + pub fn from_u16(val: u16) -> CompressionMethod { + #[allow(deprecated)] + match val { + 0 => CompressionMethod::Stored, + #[cfg(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib" + ))] + 8 => CompressionMethod::Deflated, + #[cfg(feature = "bzip2")] + 12 => CompressionMethod::Bzip2, + + v => CompressionMethod::Unsupported(v), + } + } + + /// Converts a CompressionMethod to a u16 + #[deprecated( + since = "0.5.7", + note = "to match on other compression methods, use a constant" + )] + pub fn to_u16(self) -> u16 { + #[allow(deprecated)] + match self { + CompressionMethod::Stored => 0, + #[cfg(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib" + ))] + CompressionMethod::Deflated => 8, + #[cfg(feature = "bzip2")] + CompressionMethod::Bzip2 => 12, + CompressionMethod::Unsupported(v) => v, + } + } +} + +impl fmt::Display for CompressionMethod { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Just duplicate what the Debug format looks like, i.e, the enum key: + write!(f, "{:?}", self) + } +} + +#[cfg(test)] +mod test { + use super::CompressionMethod; + + #[test] + fn from_eq_to() { + for v in 0..(::std::u16::MAX as u32 + 1) { + #[allow(deprecated)] + let from = CompressionMethod::from_u16(v as u16); + #[allow(deprecated)] + let to = from.to_u16() as u32; + assert_eq!(v, to); + } + } + + fn methods() -> Vec { + let mut methods = Vec::new(); + methods.push(CompressionMethod::Stored); + #[cfg(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib" + ))] + methods.push(CompressionMethod::Deflated); + #[cfg(feature = "bzip2")] + methods.push(CompressionMethod::Bzip2); + methods + } + + #[test] + fn to_eq_from() { + fn check_match(method: CompressionMethod) { + #[allow(deprecated)] + let to = method.to_u16(); + #[allow(deprecated)] + let from = CompressionMethod::from_u16(to); + #[allow(deprecated)] + let back = from.to_u16(); + assert_eq!(to, back); + } + + for method in methods() { + check_match(method); + } + } + + #[test] + fn to_display_fmt() { + fn check_match(method: CompressionMethod) { + let debug_str = format!("{:?}", method); + let display_str = format!("{}", method); + assert_eq!(debug_str, display_str); + } + + for method in methods() { + check_match(method); + } + } +} diff --git a/third-party/zip/src/cp437.rs b/third-party/zip/src/cp437.rs new file mode 100644 index 000000000..f99481431 --- /dev/null +++ b/third-party/zip/src/cp437.rs @@ -0,0 +1,203 @@ +//! Convert a string in IBM codepage 437 to UTF-8 + +/// Trait to convert IBM codepage 437 to the target type +pub trait FromCp437 { + /// Target type + type Target; + + /// Function that does the conversion from cp437. + /// Gennerally allocations will be avoided if all data falls into the ASCII range. + fn from_cp437(self) -> Self::Target; +} + +impl<'a> FromCp437 for &'a [u8] { + type Target = ::std::borrow::Cow<'a, str>; + + fn from_cp437(self) -> Self::Target { + if self.iter().all(|c| *c < 0x80) { + ::std::str::from_utf8(self).unwrap().into() + } else { + self.iter().map(|c| to_char(*c)).collect::().into() + } + } +} + +impl FromCp437 for Vec { + type Target = String; + + fn from_cp437(self) -> Self::Target { + if self.iter().all(|c| *c < 0x80) { + String::from_utf8(self).unwrap() + } else { + self.into_iter().map(to_char).collect() + } + } +} + +fn to_char(input: u8) -> char { + let output = match input { + 0x00..=0x7f => input as u32, + 0x80 => 0x00c7, + 0x81 => 0x00fc, + 0x82 => 0x00e9, + 0x83 => 0x00e2, + 0x84 => 0x00e4, + 0x85 => 0x00e0, + 0x86 => 0x00e5, + 0x87 => 0x00e7, + 0x88 => 0x00ea, + 0x89 => 0x00eb, + 0x8a => 0x00e8, + 0x8b => 0x00ef, + 0x8c => 0x00ee, + 0x8d => 0x00ec, + 0x8e => 0x00c4, + 0x8f => 0x00c5, + 0x90 => 0x00c9, + 0x91 => 0x00e6, + 0x92 => 0x00c6, + 0x93 => 0x00f4, + 0x94 => 0x00f6, + 0x95 => 0x00f2, + 0x96 => 0x00fb, + 0x97 => 0x00f9, + 0x98 => 0x00ff, + 0x99 => 0x00d6, + 0x9a => 0x00dc, + 0x9b => 0x00a2, + 0x9c => 0x00a3, + 0x9d => 0x00a5, + 0x9e => 0x20a7, + 0x9f => 0x0192, + 0xa0 => 0x00e1, + 0xa1 => 0x00ed, + 0xa2 => 0x00f3, + 0xa3 => 0x00fa, + 0xa4 => 0x00f1, + 0xa5 => 0x00d1, + 0xa6 => 0x00aa, + 0xa7 => 0x00ba, + 0xa8 => 0x00bf, + 0xa9 => 0x2310, + 0xaa => 0x00ac, + 0xab => 0x00bd, + 0xac => 0x00bc, + 0xad => 0x00a1, + 0xae => 0x00ab, + 0xaf => 0x00bb, + 0xb0 => 0x2591, + 0xb1 => 0x2592, + 0xb2 => 0x2593, + 0xb3 => 0x2502, + 0xb4 => 0x2524, + 0xb5 => 0x2561, + 0xb6 => 0x2562, + 0xb7 => 0x2556, + 0xb8 => 0x2555, + 0xb9 => 0x2563, + 0xba => 0x2551, + 0xbb => 0x2557, + 0xbc => 0x255d, + 0xbd => 0x255c, + 0xbe => 0x255b, + 0xbf => 0x2510, + 0xc0 => 0x2514, + 0xc1 => 0x2534, + 0xc2 => 0x252c, + 0xc3 => 0x251c, + 0xc4 => 0x2500, + 0xc5 => 0x253c, + 0xc6 => 0x255e, + 0xc7 => 0x255f, + 0xc8 => 0x255a, + 0xc9 => 0x2554, + 0xca => 0x2569, + 0xcb => 0x2566, + 0xcc => 0x2560, + 0xcd => 0x2550, + 0xce => 0x256c, + 0xcf => 0x2567, + 0xd0 => 0x2568, + 0xd1 => 0x2564, + 0xd2 => 0x2565, + 0xd3 => 0x2559, + 0xd4 => 0x2558, + 0xd5 => 0x2552, + 0xd6 => 0x2553, + 0xd7 => 0x256b, + 0xd8 => 0x256a, + 0xd9 => 0x2518, + 0xda => 0x250c, + 0xdb => 0x2588, + 0xdc => 0x2584, + 0xdd => 0x258c, + 0xde => 0x2590, + 0xdf => 0x2580, + 0xe0 => 0x03b1, + 0xe1 => 0x00df, + 0xe2 => 0x0393, + 0xe3 => 0x03c0, + 0xe4 => 0x03a3, + 0xe5 => 0x03c3, + 0xe6 => 0x00b5, + 0xe7 => 0x03c4, + 0xe8 => 0x03a6, + 0xe9 => 0x0398, + 0xea => 0x03a9, + 0xeb => 0x03b4, + 0xec => 0x221e, + 0xed => 0x03c6, + 0xee => 0x03b5, + 0xef => 0x2229, + 0xf0 => 0x2261, + 0xf1 => 0x00b1, + 0xf2 => 0x2265, + 0xf3 => 0x2264, + 0xf4 => 0x2320, + 0xf5 => 0x2321, + 0xf6 => 0x00f7, + 0xf7 => 0x2248, + 0xf8 => 0x00b0, + 0xf9 => 0x2219, + 0xfa => 0x00b7, + 0xfb => 0x221a, + 0xfc => 0x207f, + 0xfd => 0x00b2, + 0xfe => 0x25a0, + 0xff => 0x00a0, + }; + ::std::char::from_u32(output).unwrap() +} + +#[cfg(test)] +mod test { + #[test] + fn to_char_valid() { + for i in 0x00_u32..0x100 { + super::to_char(i as u8); + } + } + + #[test] + fn ascii() { + for i in 0x00..0x80 { + assert_eq!(super::to_char(i), i as char); + } + } + + #[test] + fn example_slice() { + use super::FromCp437; + let data = b"Cura\x87ao"; + assert!(::std::str::from_utf8(data).is_err()); + assert_eq!(data.from_cp437(), "Curaçao"); + } + + #[test] + fn example_vec() { + use super::FromCp437; + let data = vec![0xCC, 0xCD, 0xCD, 0xB9]; + assert!(String::from_utf8(data.clone()).is_err()); + assert_eq!(&data.from_cp437(), "╠══╣"); + } +} diff --git a/third-party/zip/src/crc32.rs b/third-party/zip/src/crc32.rs new file mode 100644 index 000000000..b351aa01b --- /dev/null +++ b/third-party/zip/src/crc32.rs @@ -0,0 +1,93 @@ +//! Helper module to compute a CRC32 checksum + +use std::io; +use std::io::prelude::*; + +use crc32fast::Hasher; + +/// Reader that validates the CRC32 when it reaches the EOF. +pub struct Crc32Reader { + inner: R, + hasher: Hasher, + check: u32, +} + +impl Crc32Reader { + /// Get a new Crc32Reader which check the inner reader against checksum. + pub fn new(inner: R, checksum: u32) -> Crc32Reader { + Crc32Reader { + inner, + hasher: Hasher::new(), + check: checksum, + } + } + + fn check_matches(&self) -> bool { + self.check == self.hasher.clone().finalize() + } + + pub fn into_inner(self) -> R { + self.inner + } +} + +impl Read for Crc32Reader { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + let count = match self.inner.read(buf) { + Ok(0) if !buf.is_empty() && !self.check_matches() => { + return Err(io::Error::new(io::ErrorKind::Other, "Invalid checksum")) + } + Ok(n) => n, + Err(e) => return Err(e), + }; + self.hasher.update(&buf[0..count]); + Ok(count) + } +} + +#[cfg(test)] +mod test { + use super::*; + use std::io::Read; + + #[test] + fn test_empty_reader() { + let data: &[u8] = b""; + let mut buf = [0; 1]; + + let mut reader = Crc32Reader::new(data, 0); + assert_eq!(reader.read(&mut buf).unwrap(), 0); + + let mut reader = Crc32Reader::new(data, 1); + assert!(reader + .read(&mut buf) + .unwrap_err() + .to_string() + .contains("Invalid checksum")); + } + + #[test] + fn test_byte_by_byte() { + let data: &[u8] = b"1234"; + let mut buf = [0; 1]; + + let mut reader = Crc32Reader::new(data, 0x9be3e0a3); + assert_eq!(reader.read(&mut buf).unwrap(), 1); + assert_eq!(reader.read(&mut buf).unwrap(), 1); + assert_eq!(reader.read(&mut buf).unwrap(), 1); + assert_eq!(reader.read(&mut buf).unwrap(), 1); + assert_eq!(reader.read(&mut buf).unwrap(), 0); + // Can keep reading 0 bytes after the end + assert_eq!(reader.read(&mut buf).unwrap(), 0); + } + + #[test] + fn test_zero_read() { + let data: &[u8] = b"1234"; + let mut buf = [0; 5]; + + let mut reader = Crc32Reader::new(data, 0x9be3e0a3); + assert_eq!(reader.read(&mut buf[..0]).unwrap(), 0); + assert_eq!(reader.read(&mut buf).unwrap(), 4); + } +} diff --git a/third-party/zip/src/lib.rs b/third-party/zip/src/lib.rs new file mode 100644 index 000000000..3b39ab4f9 --- /dev/null +++ b/third-party/zip/src/lib.rs @@ -0,0 +1,21 @@ +//! An ergonomic API for reading and writing ZIP files. +//! +//! The current implementation is based on [PKWARE's APPNOTE.TXT v6.3.9](https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT) +// TODO(#184): Decide on the crate's bias: Do we prioritise permissiveness/correctness/speed/ergonomics? + +#![warn(missing_docs)] + +pub use crate::compression::CompressionMethod; +pub use crate::read::ZipArchive; +pub use crate::types::DateTime; +pub use crate::write::ZipWriter; + +mod compression; +mod cp437; +mod crc32; +pub mod read; +pub mod result; +mod spec; +mod types; +pub mod write; +mod zipcrypto; diff --git a/third-party/zip/src/read.rs b/third-party/zip/src/read.rs new file mode 100644 index 000000000..3aac00f65 --- /dev/null +++ b/third-party/zip/src/read.rs @@ -0,0 +1,1078 @@ +//! Types for reading ZIP archives + +use crate::compression::CompressionMethod; +use crate::crc32::Crc32Reader; +use crate::result::{InvalidPassword, ZipError, ZipResult}; +use crate::spec; +use crate::zipcrypto::ZipCryptoReader; +use crate::zipcrypto::ZipCryptoReaderValid; +use std::borrow::Cow; +use std::collections::HashMap; +use std::io::{self, prelude::*}; +use std::path::{Component, Path}; + +use crate::cp437::FromCp437; +use crate::types::{DateTime, System, ZipFileData}; +use byteorder::{LittleEndian, ReadBytesExt}; + +#[cfg(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib" +))] +use flate2::read::DeflateDecoder; + +#[cfg(feature = "bzip2")] +use bzip2::read::BzDecoder; + +mod ffi { + pub const S_IFDIR: u32 = 0o0040000; + pub const S_IFREG: u32 = 0o0100000; +} + +/// ZIP archive reader +/// +/// ```no_run +/// use std::io::prelude::*; +/// fn list_zip_contents(reader: impl Read + Seek) -> zip::result::ZipResult<()> { +/// let mut zip = zip::ZipArchive::new(reader)?; +/// +/// for i in 0..zip.len() { +/// let mut file = zip.by_index(i)?; +/// println!("Filename: {}", file.name()); +/// std::io::copy(&mut file, &mut std::io::stdout()); +/// } +/// +/// Ok(()) +/// } +/// ``` +#[derive(Clone, Debug)] +pub struct ZipArchive { + reader: R, + files: Vec, + names_map: HashMap, + offset: u64, + comment: Vec, +} + +enum CryptoReader<'a> { + Plaintext(io::Take<&'a mut dyn Read>), + ZipCrypto(ZipCryptoReaderValid>), +} + +impl<'a> Read for CryptoReader<'a> { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + match self { + CryptoReader::Plaintext(r) => r.read(buf), + CryptoReader::ZipCrypto(r) => r.read(buf), + } + } +} + +impl<'a> CryptoReader<'a> { + /// Consumes this decoder, returning the underlying reader. + pub fn into_inner(self) -> io::Take<&'a mut dyn Read> { + match self { + CryptoReader::Plaintext(r) => r, + CryptoReader::ZipCrypto(r) => r.into_inner(), + } + } +} + +enum ZipFileReader<'a> { + NoReader, + Raw(io::Take<&'a mut dyn io::Read>), + Stored(Crc32Reader>), + #[cfg(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib" + ))] + Deflated(Crc32Reader>>), + #[cfg(feature = "bzip2")] + Bzip2(Crc32Reader>>), +} + +impl<'a> Read for ZipFileReader<'a> { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + match self { + ZipFileReader::NoReader => panic!("ZipFileReader was in an invalid state"), + ZipFileReader::Raw(r) => r.read(buf), + ZipFileReader::Stored(r) => r.read(buf), + #[cfg(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib" + ))] + ZipFileReader::Deflated(r) => r.read(buf), + #[cfg(feature = "bzip2")] + ZipFileReader::Bzip2(r) => r.read(buf), + } + } +} + +impl<'a> ZipFileReader<'a> { + /// Consumes this decoder, returning the underlying reader. + pub fn into_inner(self) -> io::Take<&'a mut dyn Read> { + match self { + ZipFileReader::NoReader => panic!("ZipFileReader was in an invalid state"), + ZipFileReader::Raw(r) => r, + ZipFileReader::Stored(r) => r.into_inner().into_inner(), + #[cfg(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib" + ))] + ZipFileReader::Deflated(r) => r.into_inner().into_inner().into_inner(), + #[cfg(feature = "bzip2")] + ZipFileReader::Bzip2(r) => r.into_inner().into_inner().into_inner(), + } + } +} + +/// A struct for reading a zip file +pub struct ZipFile<'a> { + data: Cow<'a, ZipFileData>, + crypto_reader: Option>, + reader: ZipFileReader<'a>, +} + +fn find_content<'a>( + data: &mut ZipFileData, + reader: &'a mut (impl Read + Seek), +) -> ZipResult> { + // Parse local header + reader.seek(io::SeekFrom::Start(data.header_start))?; + let signature = reader.read_u32::()?; + if signature != spec::LOCAL_FILE_HEADER_SIGNATURE { + return Err(ZipError::InvalidArchive("Invalid local file header")); + } + + reader.seek(io::SeekFrom::Current(22))?; + let file_name_length = reader.read_u16::()? as u64; + let extra_field_length = reader.read_u16::()? as u64; + let magic_and_header = 4 + 22 + 2 + 2; + data.data_start = data.header_start + magic_and_header + file_name_length + extra_field_length; + + reader.seek(io::SeekFrom::Start(data.data_start))?; + Ok((reader as &mut dyn Read).take(data.compressed_size)) +} + +fn make_crypto_reader<'a>( + compression_method: crate::compression::CompressionMethod, + crc32: u32, + reader: io::Take<&'a mut dyn io::Read>, + password: Option<&[u8]>, +) -> ZipResult, InvalidPassword>> { + #[allow(deprecated)] + { + if let CompressionMethod::Unsupported(_) = compression_method { + return unsupported_zip_error("Compression method not supported"); + } + } + + let reader = match password { + None => CryptoReader::Plaintext(reader), + Some(password) => match ZipCryptoReader::new(reader, password).validate(crc32)? { + None => return Ok(Err(InvalidPassword)), + Some(r) => CryptoReader::ZipCrypto(r), + }, + }; + Ok(Ok(reader)) +} + +fn make_reader<'a>( + compression_method: CompressionMethod, + crc32: u32, + reader: CryptoReader<'a>, +) -> ZipFileReader<'a> { + match compression_method { + CompressionMethod::Stored => ZipFileReader::Stored(Crc32Reader::new(reader, crc32)), + #[cfg(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib" + ))] + CompressionMethod::Deflated => { + let deflate_reader = DeflateDecoder::new(reader); + ZipFileReader::Deflated(Crc32Reader::new(deflate_reader, crc32)) + } + #[cfg(feature = "bzip2")] + CompressionMethod::Bzip2 => { + let bzip2_reader = BzDecoder::new(reader); + ZipFileReader::Bzip2(Crc32Reader::new(bzip2_reader, crc32)) + } + _ => panic!("Compression method not supported"), + } +} + +impl ZipArchive { + /// Get the directory start offset and number of files. This is done in a + /// separate function to ease the control flow design. + fn get_directory_counts( + reader: &mut R, + footer: &spec::CentralDirectoryEnd, + cde_start_pos: u64, + ) -> ZipResult<(u64, u64, usize)> { + // See if there's a ZIP64 footer. The ZIP64 locator if present will + // have its signature 20 bytes in front of the standard footer. The + // standard footer, in turn, is 22+N bytes large, where N is the + // comment length. Therefore: + let zip64locator = if reader + .seek(io::SeekFrom::End( + -(20 + 22 + footer.zip_file_comment.len() as i64), + )) + .is_ok() + { + match spec::Zip64CentralDirectoryEndLocator::parse(reader) { + Ok(loc) => Some(loc), + Err(ZipError::InvalidArchive(_)) => { + // No ZIP64 header; that's actually fine. We're done here. + None + } + Err(e) => { + // Yikes, a real problem + return Err(e); + } + } + } else { + // Empty Zip files will have nothing else so this error might be fine. If + // not, we'll find out soon. + None + }; + + match zip64locator { + None => { + // Some zip files have data prepended to them, resulting in the + // offsets all being too small. Get the amount of error by comparing + // the actual file position we found the CDE at with the offset + // recorded in the CDE. + let archive_offset = cde_start_pos + .checked_sub(footer.central_directory_size as u64) + .and_then(|x| x.checked_sub(footer.central_directory_offset as u64)) + .ok_or(ZipError::InvalidArchive( + "Invalid central directory size or offset", + ))?; + + let directory_start = footer.central_directory_offset as u64 + archive_offset; + let number_of_files = footer.number_of_files_on_this_disk as usize; + Ok((archive_offset, directory_start, number_of_files)) + } + Some(locator64) => { + // If we got here, this is indeed a ZIP64 file. + + if footer.disk_number as u32 != locator64.disk_with_central_directory { + return unsupported_zip_error( + "Support for multi-disk files is not implemented", + ); + } + + // We need to reassess `archive_offset`. We know where the ZIP64 + // central-directory-end structure *should* be, but unfortunately we + // don't know how to precisely relate that location to our current + // actual offset in the file, since there may be junk at its + // beginning. Therefore we need to perform another search, as in + // read::CentralDirectoryEnd::find_and_parse, except now we search + // forward. + + let search_upper_bound = cde_start_pos + .checked_sub(60) // minimum size of Zip64CentralDirectoryEnd + Zip64CentralDirectoryEndLocator + .ok_or(ZipError::InvalidArchive( + "File cannot contain ZIP64 central directory end", + ))?; + let (footer, archive_offset) = spec::Zip64CentralDirectoryEnd::find_and_parse( + reader, + locator64.end_of_central_directory_offset, + search_upper_bound, + )?; + + if footer.disk_number != footer.disk_with_central_directory { + return unsupported_zip_error( + "Support for multi-disk files is not implemented", + ); + } + + let directory_start = footer + .central_directory_offset + .checked_add(archive_offset) + .ok_or_else(|| { + ZipError::InvalidArchive("Invalid central directory size or offset") + })?; + + Ok(( + archive_offset, + directory_start, + footer.number_of_files as usize, + )) + } + } + } + + /// Read a ZIP archive, collecting the files it contains + /// + /// This uses the central directory record of the ZIP file, and ignores local file headers + pub fn new(mut reader: R) -> ZipResult> { + let (footer, cde_start_pos) = spec::CentralDirectoryEnd::find_and_parse(&mut reader)?; + + if footer.disk_number != footer.disk_with_central_directory { + return unsupported_zip_error("Support for multi-disk files is not implemented"); + } + + let (archive_offset, directory_start, number_of_files) = + Self::get_directory_counts(&mut reader, &footer, cde_start_pos)?; + + let mut files = Vec::new(); + let mut names_map = HashMap::new(); + + if let Err(_) = reader.seek(io::SeekFrom::Start(directory_start)) { + return Err(ZipError::InvalidArchive( + "Could not seek to start of central directory", + )); + } + + for _ in 0..number_of_files { + let file = central_header_to_zip_file(&mut reader, archive_offset)?; + names_map.insert(file.file_name.clone(), files.len()); + files.push(file); + } + + Ok(ZipArchive { + reader, + files, + names_map, + offset: archive_offset, + comment: footer.zip_file_comment, + }) + } + /// Extract a Zip archive into a directory, overwriting files if they + /// already exist. Paths are sanitized with [`ZipFile::enclosed_name`]. + /// + /// Extraction is not atomic; If an error is encountered, some of the files + /// may be left on disk. + pub fn extract>(&mut self, directory: P) -> ZipResult<()> { + use std::fs; + + for i in 0..self.len() { + let mut file = self.by_index(i)?; + let filepath = file + .enclosed_name() + .ok_or(ZipError::InvalidArchive("Invalid file path"))?; + + let outpath = directory.as_ref().join(filepath); + + if file.name().ends_with('/') { + fs::create_dir_all(&outpath)?; + } else { + if let Some(p) = outpath.parent() { + if !p.exists() { + fs::create_dir_all(&p)?; + } + } + let mut outfile = fs::File::create(&outpath)?; + io::copy(&mut file, &mut outfile)?; + } + // Get and Set permissions + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + if let Some(mode) = file.unix_mode() { + fs::set_permissions(&outpath, fs::Permissions::from_mode(mode))?; + } + } + } + Ok(()) + } + + /// Number of files contained in this zip. + pub fn len(&self) -> usize { + self.files.len() + } + + /// Whether this zip archive contains no files + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Get the offset from the beginning of the underlying reader that this zip begins at, in bytes. + /// + /// Normally this value is zero, but if the zip has arbitrary data prepended to it, then this value will be the size + /// of that prepended data. + pub fn offset(&self) -> u64 { + self.offset + } + + /// Get the comment of the zip archive. + pub fn comment(&self) -> &[u8] { + &self.comment + } + + /// Returns an iterator over all the file and directory names in this archive. + pub fn file_names(&self) -> impl Iterator { + self.names_map.keys().map(|s| s.as_str()) + } + + /// Search for a file entry by name, decrypt with given password + pub fn by_name_decrypt<'a>( + &'a mut self, + name: &str, + password: &[u8], + ) -> ZipResult, InvalidPassword>> { + self.by_name_with_optional_password(name, Some(password)) + } + + /// Search for a file entry by name + pub fn by_name<'a>(&'a mut self, name: &str) -> ZipResult> { + Ok(self.by_name_with_optional_password(name, None)?.unwrap()) + } + + fn by_name_with_optional_password<'a>( + &'a mut self, + name: &str, + password: Option<&[u8]>, + ) -> ZipResult, InvalidPassword>> { + let index = match self.names_map.get(name) { + Some(index) => *index, + None => { + return Err(ZipError::FileNotFound); + } + }; + self.by_index_with_optional_password(index, password) + } + + /// Get a contained file by index, decrypt with given password + pub fn by_index_decrypt<'a>( + &'a mut self, + file_number: usize, + password: &[u8], + ) -> ZipResult, InvalidPassword>> { + self.by_index_with_optional_password(file_number, Some(password)) + } + + /// Get a contained file by index + pub fn by_index<'a>(&'a mut self, file_number: usize) -> ZipResult> { + Ok(self + .by_index_with_optional_password(file_number, None)? + .unwrap()) + } + + /// Get a contained file by index without decompressing it + pub fn by_index_raw<'a>(&'a mut self, file_number: usize) -> ZipResult> { + let reader = &mut self.reader; + self.files + .get_mut(file_number) + .ok_or(ZipError::FileNotFound) + .and_then(move |data| { + Ok(ZipFile { + crypto_reader: None, + reader: ZipFileReader::Raw(find_content(data, reader)?), + data: Cow::Borrowed(data), + }) + }) + } + + fn by_index_with_optional_password<'a>( + &'a mut self, + file_number: usize, + mut password: Option<&[u8]>, + ) -> ZipResult, InvalidPassword>> { + if file_number >= self.files.len() { + return Err(ZipError::FileNotFound); + } + let data = &mut self.files[file_number]; + + match (password, data.encrypted) { + (None, true) => { + return Err(ZipError::UnsupportedArchive( + "Password required to decrypt file", + )) + } + (Some(_), false) => password = None, //Password supplied, but none needed! Discard. + _ => {} + } + let limit_reader = find_content(data, &mut self.reader)?; + + match make_crypto_reader(data.compression_method, data.crc32, limit_reader, password) { + Ok(Ok(crypto_reader)) => Ok(Ok(ZipFile { + crypto_reader: Some(crypto_reader), + reader: ZipFileReader::NoReader, + data: Cow::Borrowed(data), + })), + Err(e) => Err(e), + Ok(Err(e)) => Ok(Err(e)), + } + } + + /// Unwrap and return the inner reader object + /// + /// The position of the reader is undefined. + pub fn into_inner(self) -> R { + self.reader + } +} + +fn unsupported_zip_error(detail: &'static str) -> ZipResult { + Err(ZipError::UnsupportedArchive(detail)) +} + +fn central_header_to_zip_file( + reader: &mut R, + archive_offset: u64, +) -> ZipResult { + let central_header_start = reader.seek(io::SeekFrom::Current(0))?; + // Parse central header + let signature = reader.read_u32::()?; + if signature != spec::CENTRAL_DIRECTORY_HEADER_SIGNATURE { + return Err(ZipError::InvalidArchive("Invalid Central Directory header")); + } + + let version_made_by = reader.read_u16::()?; + let _version_to_extract = reader.read_u16::()?; + let flags = reader.read_u16::()?; + let encrypted = flags & 1 == 1; + let is_utf8 = flags & (1 << 11) != 0; + let compression_method = reader.read_u16::()?; + let last_mod_time = reader.read_u16::()?; + let last_mod_date = reader.read_u16::()?; + let crc32 = reader.read_u32::()?; + let compressed_size = reader.read_u32::()?; + let uncompressed_size = reader.read_u32::()?; + let file_name_length = reader.read_u16::()? as usize; + let extra_field_length = reader.read_u16::()? as usize; + let file_comment_length = reader.read_u16::()? as usize; + let _disk_number = reader.read_u16::()?; + let _internal_file_attributes = reader.read_u16::()?; + let external_file_attributes = reader.read_u32::()?; + let offset = reader.read_u32::()? as u64; + let mut file_name_raw = vec![0; file_name_length]; + reader.read_exact(&mut file_name_raw)?; + let mut extra_field = vec![0; extra_field_length]; + reader.read_exact(&mut extra_field)?; + let mut file_comment_raw = vec![0; file_comment_length]; + reader.read_exact(&mut file_comment_raw)?; + + let file_name = match is_utf8 { + true => String::from_utf8_lossy(&*file_name_raw).into_owned(), + false => file_name_raw.clone().from_cp437(), + }; + let file_comment = match is_utf8 { + true => String::from_utf8_lossy(&*file_comment_raw).into_owned(), + false => file_comment_raw.from_cp437(), + }; + + // Construct the result + let mut result = ZipFileData { + system: System::from_u8((version_made_by >> 8) as u8), + version_made_by: version_made_by as u8, + encrypted, + compression_method: { + #[allow(deprecated)] + CompressionMethod::from_u16(compression_method) + }, + last_modified_time: DateTime::from_msdos(last_mod_date, last_mod_time), + crc32, + compressed_size: compressed_size as u64, + uncompressed_size: uncompressed_size as u64, + file_name, + file_name_raw, + file_comment, + header_start: offset, + central_header_start, + data_start: 0, + external_attributes: external_file_attributes, + }; + + match parse_extra_field(&mut result, &*extra_field) { + Ok(..) | Err(ZipError::Io(..)) => {} + Err(e) => return Err(e), + } + + // Account for shifted zip offsets. + result.header_start += archive_offset; + + Ok(result) +} + +fn parse_extra_field(file: &mut ZipFileData, data: &[u8]) -> ZipResult<()> { + let mut reader = io::Cursor::new(data); + + while (reader.position() as usize) < data.len() { + let kind = reader.read_u16::()?; + let len = reader.read_u16::()?; + let mut len_left = len as i64; + // Zip64 extended information extra field + if kind == 0x0001 { + if file.uncompressed_size == 0xFFFFFFFF { + file.uncompressed_size = reader.read_u64::()?; + len_left -= 8; + } + if file.compressed_size == 0xFFFFFFFF { + file.compressed_size = reader.read_u64::()?; + len_left -= 8; + } + if file.header_start == 0xFFFFFFFF { + file.header_start = reader.read_u64::()?; + len_left -= 8; + } + // Unparsed fields: + // u32: disk start number + } + + // We could also check for < 0 to check for errors + if len_left > 0 { + reader.seek(io::SeekFrom::Current(len_left))?; + } + } + Ok(()) +} + +/// Methods for retrieving information on zip files +impl<'a> ZipFile<'a> { + fn get_reader(&mut self) -> &mut ZipFileReader<'a> { + if let ZipFileReader::NoReader = self.reader { + let data = &self.data; + let crypto_reader = self.crypto_reader.take().expect("Invalid reader state"); + self.reader = make_reader(data.compression_method, data.crc32, crypto_reader) + } + &mut self.reader + } + + pub(crate) fn get_raw_reader(&mut self) -> &mut dyn Read { + if let ZipFileReader::NoReader = self.reader { + let crypto_reader = self.crypto_reader.take().expect("Invalid reader state"); + self.reader = ZipFileReader::Raw(crypto_reader.into_inner()) + } + &mut self.reader + } + + /// Get the version of the file + pub fn version_made_by(&self) -> (u8, u8) { + ( + self.data.version_made_by / 10, + self.data.version_made_by % 10, + ) + } + + /// Get the name of the file + /// + /// # Warnings + /// + /// It is dangerous to use this name directly when extracting an archive. + /// It may contain an absolute path (`/etc/shadow`), or break out of the + /// current directory (`../runtime`). Carelessly writing to these paths + /// allows an attacker to craft a ZIP archive that will overwrite critical + /// files. + /// + /// You can use the [`ZipFile::enclosed_name`] method to validate the name + /// as a safe path. + pub fn name(&self) -> &str { + &self.data.file_name + } + + /// Get the name of the file, in the raw (internal) byte representation. + /// + /// The encoding of this data is currently undefined. + pub fn name_raw(&self) -> &[u8] { + &self.data.file_name_raw + } + + /// Get the name of the file in a sanitized form. It truncates the name to the first NULL byte, + /// removes a leading '/' and removes '..' parts. + #[deprecated( + since = "0.5.7", + note = "by stripping `..`s from the path, the meaning of paths can change. + `mangled_name` can be used if this behaviour is desirable" + )] + pub fn sanitized_name(&self) -> ::std::path::PathBuf { + self.mangled_name() + } + + /// Rewrite the path, ignoring any path components with special meaning. + /// + /// - Absolute paths are made relative + /// - [`ParentDir`]s are ignored + /// - Truncates the filename at a NULL byte + /// + /// This is appropriate if you need to be able to extract *something* from + /// any archive, but will easily misrepresent trivial paths like + /// `foo/../bar` as `foo/bar` (instead of `bar`). Because of this, + /// [`ZipFile::enclosed_name`] is the better option in most scenarios. + /// + /// [`ParentDir`]: `Component::ParentDir` + pub fn mangled_name(&self) -> ::std::path::PathBuf { + self.data.file_name_sanitized() + } + + /// Ensure the file path is safe to use as a [`Path`]. + /// + /// - It can't contain NULL bytes + /// - It can't resolve to a path outside the current directory + /// > `foo/../bar` is fine, `foo/../../bar` is not. + /// - It can't be an absolute path + /// + /// This will read well-formed ZIP files correctly, and is resistant + /// to path-based exploits. It is recommended over + /// [`ZipFile::mangled_name`]. + pub fn enclosed_name(&self) -> Option<&Path> { + if self.data.file_name.contains('\0') { + return None; + } + let path = Path::new(&self.data.file_name); + let mut depth = 0usize; + for component in path.components() { + match component { + Component::Prefix(_) | Component::RootDir => return None, + Component::ParentDir => depth = depth.checked_sub(1)?, + Component::Normal(_) => depth += 1, + Component::CurDir => (), + } + } + Some(path) + } + + /// Get the comment of the file + pub fn comment(&self) -> &str { + &self.data.file_comment + } + + /// Get the compression method used to store the file + pub fn compression(&self) -> CompressionMethod { + self.data.compression_method + } + + /// Get the size of the file in the archive + pub fn compressed_size(&self) -> u64 { + self.data.compressed_size + } + + /// Get the size of the file when uncompressed + pub fn size(&self) -> u64 { + self.data.uncompressed_size + } + + /// Get the time the file was last modified + pub fn last_modified(&self) -> DateTime { + self.data.last_modified_time + } + /// Returns whether the file is actually a directory + pub fn is_dir(&self) -> bool { + self.name() + .chars() + .rev() + .next() + .map_or(false, |c| c == '/' || c == '\\') + } + + /// Returns whether the file is a regular file + pub fn is_file(&self) -> bool { + !self.is_dir() + } + + /// Get unix mode for the file + pub fn unix_mode(&self) -> Option { + if self.data.external_attributes == 0 { + return None; + } + + match self.data.system { + System::Unix => Some(self.data.external_attributes >> 16), + System::Dos => { + // Interpret MSDOS directory bit + let mut mode = if 0x10 == (self.data.external_attributes & 0x10) { + ffi::S_IFDIR | 0o0775 + } else { + ffi::S_IFREG | 0o0664 + }; + if 0x01 == (self.data.external_attributes & 0x01) { + // Read-only bit; strip write permissions + mode &= 0o0555; + } + Some(mode) + } + _ => None, + } + } + + /// Get the CRC32 hash of the original file + pub fn crc32(&self) -> u32 { + self.data.crc32 + } + + /// Get the starting offset of the data of the compressed file + pub fn data_start(&self) -> u64 { + self.data.data_start + } + + /// Get the starting offset of the zip header for this file + pub fn header_start(&self) -> u64 { + self.data.header_start + } + /// Get the starting offset of the zip header in the central directory for this file + pub fn central_header_start(&self) -> u64 { + self.data.central_header_start + } +} + +impl<'a> Read for ZipFile<'a> { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + self.get_reader().read(buf) + } +} + +impl<'a> Drop for ZipFile<'a> { + fn drop(&mut self) { + // self.data is Owned, this reader is constructed by a streaming reader. + // In this case, we want to exhaust the reader so that the next file is accessible. + if let Cow::Owned(_) = self.data { + let mut buffer = [0; 1 << 16]; + + // Get the inner `Take` reader so all decryption, decompression and CRC calculation is skipped. + let mut reader: std::io::Take<&mut dyn std::io::Read> = match &mut self.reader { + ZipFileReader::NoReader => { + let innerreader = ::std::mem::replace(&mut self.crypto_reader, None); + innerreader.expect("Invalid reader state").into_inner() + } + reader => { + let innerreader = ::std::mem::replace(reader, ZipFileReader::NoReader); + innerreader.into_inner() + } + }; + + loop { + match reader.read(&mut buffer) { + Ok(0) => break, + Ok(_) => (), + Err(e) => panic!( + "Could not consume all of the output of the current ZipFile: {:?}", + e + ), + } + } + } + } +} + +/// Read ZipFile structures from a non-seekable reader. +/// +/// This is an alternative method to read a zip file. If possible, use the ZipArchive functions +/// as some information will be missing when reading this manner. +/// +/// Reads a file header from the start of the stream. Will return `Ok(Some(..))` if a file is +/// present at the start of the stream. Returns `Ok(None)` if the start of the central directory +/// is encountered. No more files should be read after this. +/// +/// The Drop implementation of ZipFile ensures that the reader will be correctly positioned after +/// the structure is done. +/// +/// Missing fields are: +/// * `comment`: set to an empty string +/// * `data_start`: set to 0 +/// * `external_attributes`: `unix_mode()`: will return None +pub fn read_zipfile_from_stream<'a, R: io::Read>( + reader: &'a mut R, +) -> ZipResult>> { + let signature = reader.read_u32::()?; + + match signature { + spec::LOCAL_FILE_HEADER_SIGNATURE => (), + spec::CENTRAL_DIRECTORY_HEADER_SIGNATURE => return Ok(None), + _ => return Err(ZipError::InvalidArchive("Invalid local file header")), + } + + let version_made_by = reader.read_u16::()?; + let flags = reader.read_u16::()?; + let encrypted = flags & 1 == 1; + let is_utf8 = flags & (1 << 11) != 0; + let using_data_descriptor = flags & (1 << 3) != 0; + #[allow(deprecated)] + let compression_method = CompressionMethod::from_u16(reader.read_u16::()?); + let last_mod_time = reader.read_u16::()?; + let last_mod_date = reader.read_u16::()?; + let crc32 = reader.read_u32::()?; + let compressed_size = reader.read_u32::()?; + let uncompressed_size = reader.read_u32::()?; + let file_name_length = reader.read_u16::()? as usize; + let extra_field_length = reader.read_u16::()? as usize; + + let mut file_name_raw = vec![0; file_name_length]; + reader.read_exact(&mut file_name_raw)?; + let mut extra_field = vec![0; extra_field_length]; + reader.read_exact(&mut extra_field)?; + + let file_name = match is_utf8 { + true => String::from_utf8_lossy(&*file_name_raw).into_owned(), + false => file_name_raw.clone().from_cp437(), + }; + + let mut result = ZipFileData { + system: System::from_u8((version_made_by >> 8) as u8), + version_made_by: version_made_by as u8, + encrypted, + compression_method, + last_modified_time: DateTime::from_msdos(last_mod_date, last_mod_time), + crc32, + compressed_size: compressed_size as u64, + uncompressed_size: uncompressed_size as u64, + file_name, + file_name_raw, + file_comment: String::new(), // file comment is only available in the central directory + // header_start and data start are not available, but also don't matter, since seeking is + // not available. + header_start: 0, + data_start: 0, + central_header_start: 0, + // The external_attributes field is only available in the central directory. + // We set this to zero, which should be valid as the docs state 'If input came + // from standard input, this field is set to zero.' + external_attributes: 0, + }; + + match parse_extra_field(&mut result, &extra_field) { + Ok(..) | Err(ZipError::Io(..)) => {} + Err(e) => return Err(e), + } + + if encrypted { + return unsupported_zip_error("Encrypted files are not supported"); + } + if using_data_descriptor { + return unsupported_zip_error("The file length is not available in the local header"); + } + + let limit_reader = (reader as &'a mut dyn io::Read).take(result.compressed_size as u64); + + let result_crc32 = result.crc32; + let result_compression_method = result.compression_method; + let crypto_reader = + make_crypto_reader(result_compression_method, result_crc32, limit_reader, None)?.unwrap(); + + Ok(Some(ZipFile { + data: Cow::Owned(result), + crypto_reader: None, + reader: make_reader(result_compression_method, result_crc32, crypto_reader), + })) +} + +#[cfg(test)] +mod test { + #[test] + fn invalid_offset() { + use super::ZipArchive; + use std::io; + + let mut v = Vec::new(); + v.extend_from_slice(include_bytes!("../tests/data/invalid_offset.zip")); + let reader = ZipArchive::new(io::Cursor::new(v)); + assert!(reader.is_err()); + } + + #[test] + fn invalid_offset2() { + use super::ZipArchive; + use std::io; + + let mut v = Vec::new(); + v.extend_from_slice(include_bytes!("../tests/data/invalid_offset2.zip")); + let reader = ZipArchive::new(io::Cursor::new(v)); + assert!(reader.is_err()); + } + + #[test] + fn zip64_with_leading_junk() { + use super::ZipArchive; + use std::io; + + let mut v = Vec::new(); + v.extend_from_slice(include_bytes!("../tests/data/zip64_demo.zip")); + let reader = ZipArchive::new(io::Cursor::new(v)).unwrap(); + assert!(reader.len() == 1); + } + + #[test] + fn zip_contents() { + use super::ZipArchive; + use std::io; + + let mut v = Vec::new(); + v.extend_from_slice(include_bytes!("../tests/data/mimetype.zip")); + let mut reader = ZipArchive::new(io::Cursor::new(v)).unwrap(); + assert!(reader.comment() == b""); + assert_eq!(reader.by_index(0).unwrap().central_header_start(), 77); + } + + #[test] + fn zip_read_streaming() { + use super::read_zipfile_from_stream; + use std::io; + + let mut v = Vec::new(); + v.extend_from_slice(include_bytes!("../tests/data/mimetype.zip")); + let mut reader = io::Cursor::new(v); + loop { + match read_zipfile_from_stream(&mut reader).unwrap() { + None => break, + _ => (), + } + } + } + + #[test] + fn zip_clone() { + use super::ZipArchive; + use std::io::{self, Read}; + + let mut v = Vec::new(); + v.extend_from_slice(include_bytes!("../tests/data/mimetype.zip")); + let mut reader1 = ZipArchive::new(io::Cursor::new(v)).unwrap(); + let mut reader2 = reader1.clone(); + + let mut file1 = reader1.by_index(0).unwrap(); + let mut file2 = reader2.by_index(0).unwrap(); + + let t = file1.last_modified(); + assert_eq!( + ( + t.year(), + t.month(), + t.day(), + t.hour(), + t.minute(), + t.second() + ), + (1980, 1, 1, 0, 0, 0) + ); + + let mut buf1 = [0; 5]; + let mut buf2 = [0; 5]; + let mut buf3 = [0; 5]; + let mut buf4 = [0; 5]; + + file1.read(&mut buf1).unwrap(); + file2.read(&mut buf2).unwrap(); + file1.read(&mut buf3).unwrap(); + file2.read(&mut buf4).unwrap(); + + assert_eq!(buf1, buf2); + assert_eq!(buf3, buf4); + assert!(buf1 != buf3); + } + + #[test] + fn file_and_dir_predicates() { + use super::ZipArchive; + use std::io; + + let mut v = Vec::new(); + v.extend_from_slice(include_bytes!("../tests/data/files_and_dirs.zip")); + let mut zip = ZipArchive::new(io::Cursor::new(v)).unwrap(); + + for i in 0..zip.len() { + let zip_file = zip.by_index(i).unwrap(); + let full_name = zip_file.enclosed_name().unwrap(); + let file_name = full_name.file_name().unwrap().to_str().unwrap(); + assert!( + (file_name.starts_with("dir") && zip_file.is_dir()) + || (file_name.starts_with("file") && zip_file.is_file()) + ); + } + } +} diff --git a/third-party/zip/src/result.rs b/third-party/zip/src/result.rs new file mode 100644 index 000000000..e8b7d0521 --- /dev/null +++ b/third-party/zip/src/result.rs @@ -0,0 +1,39 @@ +//! Error types that can be emitted from this library + +use std::io; + +use thiserror::Error; + +/// Generic result type with ZipError as its error variant +pub type ZipResult = Result; + +/// The given password is wrong +#[derive(Error, Debug)] +#[error("invalid password for file in archive")] +pub struct InvalidPassword; + +/// Error type for Zip +#[derive(Debug, Error)] +pub enum ZipError { + /// An Error caused by I/O + #[error(transparent)] + Io(#[from] io::Error), + + /// This file is probably not a zip archive + #[error("invalid Zip archive")] + InvalidArchive(&'static str), + + /// This archive is not supported + #[error("unsupported Zip archive")] + UnsupportedArchive(&'static str), + + /// The requested file could not be found in the archive + #[error("specified file not found in archive")] + FileNotFound, +} + +impl From for io::Error { + fn from(err: ZipError) -> io::Error { + io::Error::new(io::ErrorKind::Other, err) + } +} diff --git a/third-party/zip/src/spec.rs b/third-party/zip/src/spec.rs new file mode 100644 index 000000000..91966b67f --- /dev/null +++ b/third-party/zip/src/spec.rs @@ -0,0 +1,182 @@ +use crate::result::{ZipError, ZipResult}; +use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; +use std::io; +use std::io::prelude::*; + +pub const LOCAL_FILE_HEADER_SIGNATURE: u32 = 0x04034b50; +pub const CENTRAL_DIRECTORY_HEADER_SIGNATURE: u32 = 0x02014b50; +const CENTRAL_DIRECTORY_END_SIGNATURE: u32 = 0x06054b50; +pub const ZIP64_CENTRAL_DIRECTORY_END_SIGNATURE: u32 = 0x06064b50; +const ZIP64_CENTRAL_DIRECTORY_END_LOCATOR_SIGNATURE: u32 = 0x07064b50; + +pub struct CentralDirectoryEnd { + pub disk_number: u16, + pub disk_with_central_directory: u16, + pub number_of_files_on_this_disk: u16, + pub number_of_files: u16, + pub central_directory_size: u32, + pub central_directory_offset: u32, + pub zip_file_comment: Vec, +} + +impl CentralDirectoryEnd { + pub fn parse(reader: &mut T) -> ZipResult { + let magic = reader.read_u32::()?; + if magic != CENTRAL_DIRECTORY_END_SIGNATURE { + return Err(ZipError::InvalidArchive("Invalid digital signature header")); + } + let disk_number = reader.read_u16::()?; + let disk_with_central_directory = reader.read_u16::()?; + let number_of_files_on_this_disk = reader.read_u16::()?; + let number_of_files = reader.read_u16::()?; + let central_directory_size = reader.read_u32::()?; + let central_directory_offset = reader.read_u32::()?; + let zip_file_comment_length = reader.read_u16::()? as usize; + let mut zip_file_comment = vec![0; zip_file_comment_length]; + reader.read_exact(&mut zip_file_comment)?; + + Ok(CentralDirectoryEnd { + disk_number, + disk_with_central_directory, + number_of_files_on_this_disk, + number_of_files, + central_directory_size, + central_directory_offset, + zip_file_comment, + }) + } + + pub fn find_and_parse( + reader: &mut T, + ) -> ZipResult<(CentralDirectoryEnd, u64)> { + const HEADER_SIZE: u64 = 22; + const BYTES_BETWEEN_MAGIC_AND_COMMENT_SIZE: u64 = HEADER_SIZE - 6; + let file_length = reader.seek(io::SeekFrom::End(0))?; + + let search_upper_bound = file_length.saturating_sub(HEADER_SIZE + ::std::u16::MAX as u64); + + if file_length < HEADER_SIZE { + return Err(ZipError::InvalidArchive("Invalid zip header")); + } + + let mut pos = file_length - HEADER_SIZE; + while pos >= search_upper_bound { + reader.seek(io::SeekFrom::Start(pos as u64))?; + if reader.read_u32::()? == CENTRAL_DIRECTORY_END_SIGNATURE { + reader.seek(io::SeekFrom::Current( + BYTES_BETWEEN_MAGIC_AND_COMMENT_SIZE as i64, + ))?; + let cde_start_pos = reader.seek(io::SeekFrom::Start(pos as u64))?; + return CentralDirectoryEnd::parse(reader).map(|cde| (cde, cde_start_pos)); + } + pos = match pos.checked_sub(1) { + Some(p) => p, + None => break, + }; + } + Err(ZipError::InvalidArchive( + "Could not find central directory end", + )) + } + + pub fn write(&self, writer: &mut T) -> ZipResult<()> { + writer.write_u32::(CENTRAL_DIRECTORY_END_SIGNATURE)?; + writer.write_u16::(self.disk_number)?; + writer.write_u16::(self.disk_with_central_directory)?; + writer.write_u16::(self.number_of_files_on_this_disk)?; + writer.write_u16::(self.number_of_files)?; + writer.write_u32::(self.central_directory_size)?; + writer.write_u32::(self.central_directory_offset)?; + writer.write_u16::(self.zip_file_comment.len() as u16)?; + writer.write_all(&self.zip_file_comment)?; + Ok(()) + } +} + +pub struct Zip64CentralDirectoryEndLocator { + pub disk_with_central_directory: u32, + pub end_of_central_directory_offset: u64, + pub number_of_disks: u32, +} + +impl Zip64CentralDirectoryEndLocator { + pub fn parse(reader: &mut T) -> ZipResult { + let magic = reader.read_u32::()?; + if magic != ZIP64_CENTRAL_DIRECTORY_END_LOCATOR_SIGNATURE { + return Err(ZipError::InvalidArchive( + "Invalid zip64 locator digital signature header", + )); + } + let disk_with_central_directory = reader.read_u32::()?; + let end_of_central_directory_offset = reader.read_u64::()?; + let number_of_disks = reader.read_u32::()?; + + Ok(Zip64CentralDirectoryEndLocator { + disk_with_central_directory, + end_of_central_directory_offset, + number_of_disks, + }) + } +} + +pub struct Zip64CentralDirectoryEnd { + pub version_made_by: u16, + pub version_needed_to_extract: u16, + pub disk_number: u32, + pub disk_with_central_directory: u32, + pub number_of_files_on_this_disk: u64, + pub number_of_files: u64, + pub central_directory_size: u64, + pub central_directory_offset: u64, + //pub extensible_data_sector: Vec, <-- We don't do anything with this at the moment. +} + +impl Zip64CentralDirectoryEnd { + pub fn find_and_parse( + reader: &mut T, + nominal_offset: u64, + search_upper_bound: u64, + ) -> ZipResult<(Zip64CentralDirectoryEnd, u64)> { + let mut pos = nominal_offset; + + while pos <= search_upper_bound { + reader.seek(io::SeekFrom::Start(pos))?; + + if reader.read_u32::()? == ZIP64_CENTRAL_DIRECTORY_END_SIGNATURE { + let archive_offset = pos - nominal_offset; + + let _record_size = reader.read_u64::()?; + // We would use this value if we did anything with the "zip64 extensible data sector". + + let version_made_by = reader.read_u16::()?; + let version_needed_to_extract = reader.read_u16::()?; + let disk_number = reader.read_u32::()?; + let disk_with_central_directory = reader.read_u32::()?; + let number_of_files_on_this_disk = reader.read_u64::()?; + let number_of_files = reader.read_u64::()?; + let central_directory_size = reader.read_u64::()?; + let central_directory_offset = reader.read_u64::()?; + + return Ok(( + Zip64CentralDirectoryEnd { + version_made_by, + version_needed_to_extract, + disk_number, + disk_with_central_directory, + number_of_files_on_this_disk, + number_of_files, + central_directory_size, + central_directory_offset, + }, + archive_offset, + )); + } + + pos += 1; + } + + Err(ZipError::InvalidArchive( + "Could not find ZIP64 central directory end", + )) + } +} diff --git a/third-party/zip/src/types.rs b/third-party/zip/src/types.rs new file mode 100644 index 000000000..154e3c23e --- /dev/null +++ b/third-party/zip/src/types.rs @@ -0,0 +1,474 @@ +//! Types that specify what is contained in a ZIP. + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum System { + Dos = 0, + Unix = 3, + Unknown, +} + +impl System { + pub fn from_u8(system: u8) -> System { + use self::System::*; + + match system { + 0 => Dos, + 3 => Unix, + _ => Unknown, + } + } +} + +/// A DateTime field to be used for storing timestamps in a zip file +/// +/// This structure does bounds checking to ensure the date is able to be stored in a zip file. +/// +/// When constructed manually from a date and time, it will also check if the input is sensible +/// (e.g. months are from [1, 12]), but when read from a zip some parts may be out of their normal +/// bounds (e.g. month 0, or hour 31). +/// +/// # Warning +/// +/// Some utilities use alternative timestamps to improve the accuracy of their +/// ZIPs, but we don't parse them yet. [We're working on this](https://github.com/zip-rs/zip/issues/156#issuecomment-652981904), +/// however this API shouldn't be considered complete. +#[derive(Debug, Clone, Copy)] +pub struct DateTime { + year: u16, + month: u8, + day: u8, + hour: u8, + minute: u8, + second: u8, +} + +impl ::std::default::Default for DateTime { + /// Constructs an 'default' datetime of 1980-01-01 00:00:00 + fn default() -> DateTime { + DateTime { + year: 1980, + month: 1, + day: 1, + hour: 0, + minute: 0, + second: 0, + } + } +} + +impl DateTime { + /// Converts an msdos (u16, u16) pair to a DateTime object + pub fn from_msdos(datepart: u16, timepart: u16) -> DateTime { + let seconds = (timepart & 0b0000000000011111) << 1; + let minutes = (timepart & 0b0000011111100000) >> 5; + let hours = (timepart & 0b1111100000000000) >> 11; + let days = (datepart & 0b0000000000011111) >> 0; + let months = (datepart & 0b0000000111100000) >> 5; + let years = (datepart & 0b1111111000000000) >> 9; + + DateTime { + year: (years + 1980) as u16, + month: months as u8, + day: days as u8, + hour: hours as u8, + minute: minutes as u8, + second: seconds as u8, + } + } + + /// Constructs a DateTime from a specific date and time + /// + /// The bounds are: + /// * year: [1980, 2107] + /// * month: [1, 12] + /// * day: [1, 31] + /// * hour: [0, 23] + /// * minute: [0, 59] + /// * second: [0, 60] + pub fn from_date_and_time( + year: u16, + month: u8, + day: u8, + hour: u8, + minute: u8, + second: u8, + ) -> Result { + if year >= 1980 + && year <= 2107 + && month >= 1 + && month <= 12 + && day >= 1 + && day <= 31 + && hour <= 23 + && minute <= 59 + && second <= 60 + { + Ok(DateTime { + year, + month, + day, + hour, + minute, + second, + }) + } else { + Err(()) + } + } + + #[cfg(feature = "time")] + /// Converts a ::time::Tm object to a DateTime + /// + /// Returns `Err` when this object is out of bounds + pub fn from_time(tm: ::time::Tm) -> Result { + if tm.tm_year >= 80 + && tm.tm_year <= 207 + && tm.tm_mon >= 0 + && tm.tm_mon <= 11 + && tm.tm_mday >= 1 + && tm.tm_mday <= 31 + && tm.tm_hour >= 0 + && tm.tm_hour <= 23 + && tm.tm_min >= 0 + && tm.tm_min <= 59 + && tm.tm_sec >= 0 + && tm.tm_sec <= 60 + { + Ok(DateTime { + year: (tm.tm_year + 1900) as u16, + month: (tm.tm_mon + 1) as u8, + day: tm.tm_mday as u8, + hour: tm.tm_hour as u8, + minute: tm.tm_min as u8, + second: tm.tm_sec as u8, + }) + } else { + Err(()) + } + } + + /// Gets the time portion of this datetime in the msdos representation + pub fn timepart(&self) -> u16 { + ((self.second as u16) >> 1) | ((self.minute as u16) << 5) | ((self.hour as u16) << 11) + } + + /// Gets the date portion of this datetime in the msdos representation + pub fn datepart(&self) -> u16 { + (self.day as u16) | ((self.month as u16) << 5) | ((self.year - 1980) << 9) + } + + #[cfg(feature = "time")] + /// Converts the datetime to a Tm structure + /// + /// The fields `tm_wday`, `tm_yday`, `tm_utcoff` and `tm_nsec` are set to their defaults. + pub fn to_time(&self) -> ::time::Tm { + ::time::Tm { + tm_sec: self.second as i32, + tm_min: self.minute as i32, + tm_hour: self.hour as i32, + tm_mday: self.day as i32, + tm_mon: self.month as i32 - 1, + tm_year: self.year as i32 - 1900, + tm_isdst: -1, + ..::time::empty_tm() + } + } + + /// Get the year. There is no epoch, i.e. 2018 will be returned as 2018. + pub fn year(&self) -> u16 { + self.year + } + + /// Get the month, where 1 = january and 12 = december + pub fn month(&self) -> u8 { + self.month + } + + /// Get the day + pub fn day(&self) -> u8 { + self.day + } + + /// Get the hour + pub fn hour(&self) -> u8 { + self.hour + } + + /// Get the minute + pub fn minute(&self) -> u8 { + self.minute + } + + /// Get the second + pub fn second(&self) -> u8 { + self.second + } +} + +pub const DEFAULT_VERSION: u8 = 46; + +/// Structure representing a ZIP file. +#[derive(Debug, Clone)] +pub struct ZipFileData { + /// Compatibility of the file attribute information + pub system: System, + /// Specification version + pub version_made_by: u8, + /// True if the file is encrypted. + pub encrypted: bool, + /// Compression method used to store the file + pub compression_method: crate::compression::CompressionMethod, + /// Last modified time. This will only have a 2 second precision. + pub last_modified_time: DateTime, + /// CRC32 checksum + pub crc32: u32, + /// Size of the file in the ZIP + pub compressed_size: u64, + /// Size of the file when extracted + pub uncompressed_size: u64, + /// Name of the file + pub file_name: String, + /// Raw file name. To be used when file_name was incorrectly decoded. + pub file_name_raw: Vec, + /// File comment + pub file_comment: String, + /// Specifies where the local header of the file starts + pub header_start: u64, + /// Specifies where the central header of the file starts + /// + /// Note that when this is not known, it is set to 0 + pub central_header_start: u64, + /// Specifies where the compressed data of the file starts + pub data_start: u64, + /// External file attributes + pub external_attributes: u32, +} + +impl ZipFileData { + pub fn file_name_sanitized(&self) -> ::std::path::PathBuf { + let no_null_filename = match self.file_name.find('\0') { + Some(index) => &self.file_name[0..index], + None => &self.file_name, + } + .to_string(); + + // zip files can contain both / and \ as separators regardless of the OS + // and as we want to return a sanitized PathBuf that only supports the + // OS separator let's convert incompatible separators to compatible ones + let separator = ::std::path::MAIN_SEPARATOR; + let opposite_separator = match separator { + '/' => '\\', + _ => '/', + }; + let filename = + no_null_filename.replace(&opposite_separator.to_string(), &separator.to_string()); + + ::std::path::Path::new(&filename) + .components() + .filter(|component| match *component { + ::std::path::Component::Normal(..) => true, + _ => false, + }) + .fold(::std::path::PathBuf::new(), |mut path, ref cur| { + path.push(cur.as_os_str()); + path + }) + } + + pub fn version_needed(&self) -> u16 { + match self.compression_method { + #[cfg(feature = "bzip2")] + crate::compression::CompressionMethod::Bzip2 => 46, + _ => 20, + } + } +} + +#[cfg(test)] +mod test { + #[test] + fn system() { + use super::System; + assert_eq!(System::Dos as u16, 0u16); + assert_eq!(System::Unix as u16, 3u16); + assert_eq!(System::from_u8(0), System::Dos); + assert_eq!(System::from_u8(3), System::Unix); + } + + #[test] + fn sanitize() { + use super::*; + let file_name = "/path/../../../../etc/./passwd\0/etc/shadow".to_string(); + let data = ZipFileData { + system: System::Dos, + version_made_by: 0, + encrypted: false, + compression_method: crate::compression::CompressionMethod::Stored, + last_modified_time: DateTime::default(), + crc32: 0, + compressed_size: 0, + uncompressed_size: 0, + file_name: file_name.clone(), + file_name_raw: file_name.into_bytes(), + file_comment: String::new(), + header_start: 0, + data_start: 0, + central_header_start: 0, + external_attributes: 0, + }; + assert_eq!( + data.file_name_sanitized(), + ::std::path::PathBuf::from("path/etc/passwd") + ); + } + + #[test] + fn datetime_default() { + use super::DateTime; + let dt = DateTime::default(); + assert_eq!(dt.timepart(), 0); + assert_eq!(dt.datepart(), 0b0000000_0001_00001); + } + + #[test] + fn datetime_max() { + use super::DateTime; + let dt = DateTime::from_date_and_time(2107, 12, 31, 23, 59, 60).unwrap(); + assert_eq!(dt.timepart(), 0b10111_111011_11110); + assert_eq!(dt.datepart(), 0b1111111_1100_11111); + } + + #[test] + fn datetime_bounds() { + use super::DateTime; + + assert!(DateTime::from_date_and_time(2000, 1, 1, 23, 59, 60).is_ok()); + assert!(DateTime::from_date_and_time(2000, 1, 1, 24, 0, 0).is_err()); + assert!(DateTime::from_date_and_time(2000, 1, 1, 0, 60, 0).is_err()); + assert!(DateTime::from_date_and_time(2000, 1, 1, 0, 0, 61).is_err()); + + assert!(DateTime::from_date_and_time(2107, 12, 31, 0, 0, 0).is_ok()); + assert!(DateTime::from_date_and_time(1980, 1, 1, 0, 0, 0).is_ok()); + assert!(DateTime::from_date_and_time(1979, 1, 1, 0, 0, 0).is_err()); + assert!(DateTime::from_date_and_time(1980, 0, 1, 0, 0, 0).is_err()); + assert!(DateTime::from_date_and_time(1980, 1, 0, 0, 0, 0).is_err()); + assert!(DateTime::from_date_and_time(2108, 12, 31, 0, 0, 0).is_err()); + assert!(DateTime::from_date_and_time(2107, 13, 31, 0, 0, 0).is_err()); + assert!(DateTime::from_date_and_time(2107, 12, 32, 0, 0, 0).is_err()); + } + + #[cfg(feature = "time")] + #[test] + fn datetime_from_time_bounds() { + use super::DateTime; + + // 1979-12-31 23:59:59 + assert!(DateTime::from_time(::time::Tm { + tm_sec: 59, + tm_min: 59, + tm_hour: 23, + tm_mday: 31, + tm_mon: 11, // tm_mon has number range [0, 11] + tm_year: 79, // 1979 - 1900 = 79 + ..::time::empty_tm() + }) + .is_err()); + + // 1980-01-01 00:00:00 + assert!(DateTime::from_time(::time::Tm { + tm_sec: 0, + tm_min: 0, + tm_hour: 0, + tm_mday: 1, + tm_mon: 0, // tm_mon has number range [0, 11] + tm_year: 80, // 1980 - 1900 = 80 + ..::time::empty_tm() + }) + .is_ok()); + + // 2107-12-31 23:59:59 + assert!(DateTime::from_time(::time::Tm { + tm_sec: 59, + tm_min: 59, + tm_hour: 23, + tm_mday: 31, + tm_mon: 11, // tm_mon has number range [0, 11] + tm_year: 207, // 2107 - 1900 = 207 + ..::time::empty_tm() + }) + .is_ok()); + + // 2108-01-01 00:00:00 + assert!(DateTime::from_time(::time::Tm { + tm_sec: 0, + tm_min: 0, + tm_hour: 0, + tm_mday: 1, + tm_mon: 0, // tm_mon has number range [0, 11] + tm_year: 208, // 2108 - 1900 = 208 + ..::time::empty_tm() + }) + .is_err()); + } + + #[test] + fn time_conversion() { + use super::DateTime; + let dt = DateTime::from_msdos(0x4D71, 0x54CF); + assert_eq!(dt.year(), 2018); + assert_eq!(dt.month(), 11); + assert_eq!(dt.day(), 17); + assert_eq!(dt.hour(), 10); + assert_eq!(dt.minute(), 38); + assert_eq!(dt.second(), 30); + + #[cfg(feature = "time")] + assert_eq!( + format!("{}", dt.to_time().rfc3339()), + "2018-11-17T10:38:30Z" + ); + } + + #[test] + fn time_out_of_bounds() { + use super::DateTime; + let dt = DateTime::from_msdos(0xFFFF, 0xFFFF); + assert_eq!(dt.year(), 2107); + assert_eq!(dt.month(), 15); + assert_eq!(dt.day(), 31); + assert_eq!(dt.hour(), 31); + assert_eq!(dt.minute(), 63); + assert_eq!(dt.second(), 62); + + #[cfg(feature = "time")] + assert_eq!( + format!("{}", dt.to_time().rfc3339()), + "2107-15-31T31:63:62Z" + ); + + let dt = DateTime::from_msdos(0x0000, 0x0000); + assert_eq!(dt.year(), 1980); + assert_eq!(dt.month(), 0); + assert_eq!(dt.day(), 0); + assert_eq!(dt.hour(), 0); + assert_eq!(dt.minute(), 0); + assert_eq!(dt.second(), 0); + + #[cfg(feature = "time")] + assert_eq!( + format!("{}", dt.to_time().rfc3339()), + "1980-00-00T00:00:00Z" + ); + } + + #[cfg(feature = "time")] + #[test] + fn time_at_january() { + use super::DateTime; + + // 2020-01-01 00:00:00 + let clock = ::time::Timespec::new(1577836800, 0); + let tm = ::time::at_utc(clock); + assert!(DateTime::from_time(tm).is_ok()); + } +} diff --git a/third-party/zip/src/write.rs b/third-party/zip/src/write.rs new file mode 100644 index 000000000..bc6881727 --- /dev/null +++ b/third-party/zip/src/write.rs @@ -0,0 +1,821 @@ +//! Types for creating ZIP archives + +use crate::compression::CompressionMethod; +use crate::read::ZipFile; +use crate::result::{ZipError, ZipResult}; +use crate::spec; +use crate::types::{DateTime, System, ZipFileData, DEFAULT_VERSION}; +use byteorder::{LittleEndian, WriteBytesExt}; +use crc32fast::Hasher; +use std::default::Default; +use std::io; +use std::io::prelude::*; +use std::mem; + +#[cfg(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib" +))] +use flate2::write::DeflateEncoder; + +#[cfg(feature = "bzip2")] +use bzip2::write::BzEncoder; + +enum GenericZipWriter { + Closed, + Storer(W), + #[cfg(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib" + ))] + Deflater(DeflateEncoder), + #[cfg(feature = "bzip2")] + Bzip2(BzEncoder), +} + +/// ZIP archive generator +/// +/// Handles the bookkeeping involved in building an archive, and provides an +/// API to edit its contents. +/// +/// ``` +/// # fn doit() -> zip::result::ZipResult<()> +/// # { +/// # use zip::ZipWriter; +/// use std::io::Write; +/// use zip::write::FileOptions; +/// +/// // We use a buffer here, though you'd normally use a `File` +/// let mut buf = [0; 65536]; +/// let mut zip = zip::ZipWriter::new(std::io::Cursor::new(&mut buf[..])); +/// +/// let options = zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Stored); +/// zip.start_file("hello_world.txt", options)?; +/// zip.write(b"Hello, World!")?; +/// +/// // Apply the changes you've made. +/// // Dropping the `ZipWriter` will have the same effect, but may silently fail +/// zip.finish()?; +/// +/// # Ok(()) +/// # } +/// # doit().unwrap(); +/// ``` +pub struct ZipWriter { + inner: GenericZipWriter, + files: Vec, + stats: ZipWriterStats, + writing_to_file: bool, + comment: String, + writing_raw: bool, +} + +#[derive(Default)] +struct ZipWriterStats { + hasher: Hasher, + start: u64, + bytes_written: u64, +} + +struct ZipRawValues { + crc32: u32, + compressed_size: u64, + uncompressed_size: u64, +} + +/// Metadata for a file to be written +#[derive(Copy, Clone)] +pub struct FileOptions { + compression_method: CompressionMethod, + last_modified_time: DateTime, + permissions: Option, +} + +impl FileOptions { + /// Construct a new FileOptions object + pub fn default() -> FileOptions { + FileOptions { + #[cfg(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib" + ))] + compression_method: CompressionMethod::Deflated, + #[cfg(not(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib" + )))] + compression_method: CompressionMethod::Stored, + #[cfg(feature = "time")] + last_modified_time: DateTime::from_time(time::now()).unwrap_or_default(), + #[cfg(not(feature = "time"))] + last_modified_time: DateTime::default(), + permissions: None, + } + } + + /// Set the compression method for the new file + /// + /// The default is `CompressionMethod::Deflated`. If the deflate compression feature is + /// disabled, `CompressionMethod::Stored` becomes the default. + /// otherwise. + pub fn compression_method(mut self, method: CompressionMethod) -> FileOptions { + self.compression_method = method; + self + } + + /// Set the last modified time + /// + /// The default is the current timestamp if the 'time' feature is enabled, and 1980-01-01 + /// otherwise + pub fn last_modified_time(mut self, mod_time: DateTime) -> FileOptions { + self.last_modified_time = mod_time; + self + } + + /// Set the permissions for the new file. + /// + /// The format is represented with unix-style permissions. + /// The default is `0o644`, which represents `rw-r--r--` for files, + /// and `0o755`, which represents `rwxr-xr-x` for directories + pub fn unix_permissions(mut self, mode: u32) -> FileOptions { + self.permissions = Some(mode & 0o777); + self + } +} + +impl Default for FileOptions { + fn default() -> Self { + Self::default() + } +} + +impl Write for ZipWriter { + fn write(&mut self, buf: &[u8]) -> io::Result { + if !self.writing_to_file { + return Err(io::Error::new( + io::ErrorKind::Other, + "No file has been started", + )); + } + match self.inner.ref_mut() { + Some(ref mut w) => { + let write_result = w.write(buf); + if let Ok(count) = write_result { + self.stats.update(&buf[0..count]); + } + write_result + } + None => Err(io::Error::new( + io::ErrorKind::BrokenPipe, + "ZipWriter was already closed", + )), + } + } + + fn flush(&mut self) -> io::Result<()> { + match self.inner.ref_mut() { + Some(ref mut w) => w.flush(), + None => Err(io::Error::new( + io::ErrorKind::BrokenPipe, + "ZipWriter was already closed", + )), + } + } +} + +impl ZipWriterStats { + fn update(&mut self, buf: &[u8]) { + self.hasher.update(buf); + self.bytes_written += buf.len() as u64; + } +} + +impl ZipWriter { + /// Initializes the archive. + /// + /// Before writing to this object, the [`ZipWriter::start_file`] function should be called. + pub fn new(inner: W) -> ZipWriter { + ZipWriter { + inner: GenericZipWriter::Storer(inner), + files: Vec::new(), + stats: Default::default(), + writing_to_file: false, + comment: String::new(), + writing_raw: false, + } + } + + /// Set ZIP archive comment. + pub fn set_comment(&mut self, comment: S) + where + S: Into, + { + self.comment = comment.into(); + } + + /// Start a new file for with the requested options. + fn start_entry( + &mut self, + name: S, + options: FileOptions, + raw_values: Option, + ) -> ZipResult<()> + where + S: Into, + { + self.finish_file()?; + + let is_raw = raw_values.is_some(); + let raw_values = raw_values.unwrap_or_else(|| ZipRawValues { + crc32: 0, + compressed_size: 0, + uncompressed_size: 0, + }); + + { + let writer = self.inner.get_plain(); + let header_start = writer.seek(io::SeekFrom::Current(0))?; + + let permissions = options.permissions.unwrap_or(0o100644); + let mut file = ZipFileData { + system: System::Unix, + version_made_by: DEFAULT_VERSION, + encrypted: false, + compression_method: options.compression_method, + last_modified_time: options.last_modified_time, + crc32: raw_values.crc32, + compressed_size: raw_values.compressed_size, + uncompressed_size: raw_values.uncompressed_size, + file_name: name.into(), + file_name_raw: Vec::new(), // Never used for saving + file_comment: String::new(), + header_start, + data_start: 0, + central_header_start: 0, + external_attributes: permissions << 16, + }; + write_local_file_header(writer, &file)?; + + let header_end = writer.seek(io::SeekFrom::Current(0))?; + self.stats.start = header_end; + file.data_start = header_end; + + self.stats.bytes_written = 0; + self.stats.hasher = Hasher::new(); + + self.files.push(file); + } + + self.writing_raw = is_raw; + self.inner.switch_to(if is_raw { + CompressionMethod::Stored + } else { + options.compression_method + })?; + + Ok(()) + } + + fn finish_file(&mut self) -> ZipResult<()> { + self.inner.switch_to(CompressionMethod::Stored)?; + let writer = self.inner.get_plain(); + + if !self.writing_raw { + let file = match self.files.last_mut() { + None => return Ok(()), + Some(f) => f, + }; + file.crc32 = self.stats.hasher.clone().finalize(); + file.uncompressed_size = self.stats.bytes_written; + + let file_end = writer.seek(io::SeekFrom::Current(0))?; + file.compressed_size = file_end - self.stats.start; + + update_local_file_header(writer, file)?; + writer.seek(io::SeekFrom::Start(file_end))?; + } + + self.writing_to_file = false; + self.writing_raw = false; + Ok(()) + } + + /// Create a file in the archive and start writing its' contents. + /// + /// The data should be written using the [`io::Write`] implementation on this [`ZipWriter`] + pub fn start_file(&mut self, name: S, mut options: FileOptions) -> ZipResult<()> + where + S: Into, + { + if options.permissions.is_none() { + options.permissions = Some(0o644); + } + *options.permissions.as_mut().unwrap() |= 0o100000; + self.start_entry(name, options, None)?; + self.writing_to_file = true; + Ok(()) + } + + /// Starts a file, taking a Path as argument. + /// + /// This function ensures that the '/' path seperator is used. It also ignores all non 'Normal' + /// Components, such as a starting '/' or '..' and '.'. + #[deprecated( + since = "0.5.7", + note = "by stripping `..`s from the path, the meaning of paths can change. Use `start_file` instead." + )] + pub fn start_file_from_path( + &mut self, + path: &std::path::Path, + options: FileOptions, + ) -> ZipResult<()> { + self.start_file(path_to_string(path), options) + } + + /// Add a new file using the already compressed data from a ZIP file being read and renames it, this + /// allows faster copies of the `ZipFile` since there is no need to decompress and compress it again. + /// Any `ZipFile` metadata is copied and not checked, for example the file CRC. + + /// ```no_run + /// use std::fs::File; + /// use std::io::{Read, Seek, Write}; + /// use zip::{ZipArchive, ZipWriter}; + /// + /// fn copy_rename( + /// src: &mut ZipArchive, + /// dst: &mut ZipWriter, + /// ) -> zip::result::ZipResult<()> + /// where + /// R: Read + Seek, + /// W: Write + Seek, + /// { + /// // Retrieve file entry by name + /// let file = src.by_name("src_file.txt")?; + /// + /// // Copy and rename the previously obtained file entry to the destination zip archive + /// dst.raw_copy_file_rename(file, "new_name.txt")?; + /// + /// Ok(()) + /// } + /// ``` + pub fn raw_copy_file_rename(&mut self, mut file: ZipFile, name: S) -> ZipResult<()> + where + S: Into, + { + let options = FileOptions::default() + .last_modified_time(file.last_modified()) + .compression_method(file.compression()); + if let Some(perms) = file.unix_mode() { + options.unix_permissions(perms); + } + + let raw_values = ZipRawValues { + crc32: file.crc32(), + compressed_size: file.compressed_size(), + uncompressed_size: file.size(), + }; + + self.start_entry(name, options, Some(raw_values))?; + self.writing_to_file = true; + + io::copy(file.get_raw_reader(), self)?; + + Ok(()) + } + + /// Add a new file using the already compressed data from a ZIP file being read, this allows faster + /// copies of the `ZipFile` since there is no need to decompress and compress it again. Any `ZipFile` + /// metadata is copied and not checked, for example the file CRC. + /// + /// ```no_run + /// use std::fs::File; + /// use std::io::{Read, Seek, Write}; + /// use zip::{ZipArchive, ZipWriter}; + /// + /// fn copy(src: &mut ZipArchive, dst: &mut ZipWriter) -> zip::result::ZipResult<()> + /// where + /// R: Read + Seek, + /// W: Write + Seek, + /// { + /// // Retrieve file entry by name + /// let file = src.by_name("src_file.txt")?; + /// + /// // Copy the previously obtained file entry to the destination zip archive + /// dst.raw_copy_file(file)?; + /// + /// Ok(()) + /// } + /// ``` + pub fn raw_copy_file(&mut self, file: ZipFile) -> ZipResult<()> { + let name = file.name().to_owned(); + self.raw_copy_file_rename(file, name) + } + + /// Add a directory entry. + /// + /// You can't write data to the file afterwards. + pub fn add_directory(&mut self, name: S, mut options: FileOptions) -> ZipResult<()> + where + S: Into, + { + if options.permissions.is_none() { + options.permissions = Some(0o755); + } + *options.permissions.as_mut().unwrap() |= 0o40000; + options.compression_method = CompressionMethod::Stored; + + let name_as_string = name.into(); + // Append a slash to the filename if it does not end with it. + let name_with_slash = match name_as_string.chars().last() { + Some('/') | Some('\\') => name_as_string, + _ => name_as_string + "/", + }; + + self.start_entry(name_with_slash, options, None)?; + self.writing_to_file = false; + Ok(()) + } + + /// Add a directory entry, taking a Path as argument. + /// + /// This function ensures that the '/' path seperator is used. It also ignores all non 'Normal' + /// Components, such as a starting '/' or '..' and '.'. + #[deprecated( + since = "0.5.7", + note = "by stripping `..`s from the path, the meaning of paths can change. Use `add_directory` instead." + )] + pub fn add_directory_from_path( + &mut self, + path: &std::path::Path, + options: FileOptions, + ) -> ZipResult<()> { + self.add_directory(path_to_string(path), options) + } + + /// Finish the last file and write all other zip-structures + /// + /// This will return the writer, but one should normally not append any data to the end of the file. + /// Note that the zipfile will also be finished on drop. + pub fn finish(&mut self) -> ZipResult { + self.finalize()?; + let inner = mem::replace(&mut self.inner, GenericZipWriter::Closed); + Ok(inner.unwrap()) + } + + fn finalize(&mut self) -> ZipResult<()> { + self.finish_file()?; + + { + let writer = self.inner.get_plain(); + + let central_start = writer.seek(io::SeekFrom::Current(0))?; + for file in self.files.iter() { + write_central_directory_header(writer, file)?; + } + let central_size = writer.seek(io::SeekFrom::Current(0))? - central_start; + + let footer = spec::CentralDirectoryEnd { + disk_number: 0, + disk_with_central_directory: 0, + number_of_files_on_this_disk: self.files.len() as u16, + number_of_files: self.files.len() as u16, + central_directory_size: central_size as u32, + central_directory_offset: central_start as u32, + zip_file_comment: self.comment.as_bytes().to_vec(), + }; + + footer.write(writer)?; + } + + Ok(()) + } +} + +impl Drop for ZipWriter { + fn drop(&mut self) { + if !self.inner.is_closed() { + if let Err(e) = self.finalize() { + let _ = write!(&mut io::stderr(), "ZipWriter drop failed: {:?}", e); + } + } + } +} + +impl GenericZipWriter { + fn switch_to(&mut self, compression: CompressionMethod) -> ZipResult<()> { + match self.current_compression() { + Some(method) if method == compression => return Ok(()), + None => { + return Err(io::Error::new( + io::ErrorKind::BrokenPipe, + "ZipWriter was already closed", + ) + .into()) + } + _ => {} + } + + let bare = match mem::replace(self, GenericZipWriter::Closed) { + GenericZipWriter::Storer(w) => w, + #[cfg(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib" + ))] + GenericZipWriter::Deflater(w) => w.finish()?, + #[cfg(feature = "bzip2")] + GenericZipWriter::Bzip2(w) => w.finish()?, + GenericZipWriter::Closed => { + return Err(io::Error::new( + io::ErrorKind::BrokenPipe, + "ZipWriter was already closed", + ) + .into()) + } + }; + + *self = { + #[allow(deprecated)] + match compression { + CompressionMethod::Stored => GenericZipWriter::Storer(bare), + #[cfg(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib" + ))] + CompressionMethod::Deflated => GenericZipWriter::Deflater(DeflateEncoder::new( + bare, + flate2::Compression::default(), + )), + #[cfg(feature = "bzip2")] + CompressionMethod::Bzip2 => { + GenericZipWriter::Bzip2(BzEncoder::new(bare, bzip2::Compression::Default)) + } + CompressionMethod::Unsupported(..) => { + return Err(ZipError::UnsupportedArchive("Unsupported compression")) + } + } + }; + + Ok(()) + } + + fn ref_mut(&mut self) -> Option<&mut dyn Write> { + match *self { + GenericZipWriter::Storer(ref mut w) => Some(w as &mut dyn Write), + #[cfg(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib" + ))] + GenericZipWriter::Deflater(ref mut w) => Some(w as &mut dyn Write), + #[cfg(feature = "bzip2")] + GenericZipWriter::Bzip2(ref mut w) => Some(w as &mut dyn Write), + GenericZipWriter::Closed => None, + } + } + + fn is_closed(&self) -> bool { + match *self { + GenericZipWriter::Closed => true, + _ => false, + } + } + + fn get_plain(&mut self) -> &mut W { + match *self { + GenericZipWriter::Storer(ref mut w) => w, + _ => panic!("Should have switched to stored beforehand"), + } + } + + fn current_compression(&self) -> Option { + match *self { + GenericZipWriter::Storer(..) => Some(CompressionMethod::Stored), + #[cfg(any( + feature = "deflate", + feature = "deflate-miniz", + feature = "deflate-zlib" + ))] + GenericZipWriter::Deflater(..) => Some(CompressionMethod::Deflated), + #[cfg(feature = "bzip2")] + GenericZipWriter::Bzip2(..) => Some(CompressionMethod::Bzip2), + GenericZipWriter::Closed => None, + } + } + + fn unwrap(self) -> W { + match self { + GenericZipWriter::Storer(w) => w, + _ => panic!("Should have switched to stored beforehand"), + } + } +} + +fn write_local_file_header(writer: &mut T, file: &ZipFileData) -> ZipResult<()> { + // local file header signature + writer.write_u32::(spec::LOCAL_FILE_HEADER_SIGNATURE)?; + // version needed to extract + writer.write_u16::(file.version_needed())?; + // general purpose bit flag + let flag = if !file.file_name.is_ascii() { + 1u16 << 11 + } else { + 0 + }; + writer.write_u16::(flag)?; + // Compression method + #[allow(deprecated)] + writer.write_u16::(file.compression_method.to_u16())?; + // last mod file time and last mod file date + writer.write_u16::(file.last_modified_time.timepart())?; + writer.write_u16::(file.last_modified_time.datepart())?; + // crc-32 + writer.write_u32::(file.crc32)?; + // compressed size + writer.write_u32::(file.compressed_size as u32)?; + // uncompressed size + writer.write_u32::(file.uncompressed_size as u32)?; + // file name length + writer.write_u16::(file.file_name.as_bytes().len() as u16)?; + // extra field length + let extra_field = build_extra_field(file)?; + writer.write_u16::(extra_field.len() as u16)?; + // file name + writer.write_all(file.file_name.as_bytes())?; + // extra field + writer.write_all(&extra_field)?; + + Ok(()) +} + +fn update_local_file_header( + writer: &mut T, + file: &ZipFileData, +) -> ZipResult<()> { + const CRC32_OFFSET: u64 = 14; + writer.seek(io::SeekFrom::Start(file.header_start + CRC32_OFFSET))?; + writer.write_u32::(file.crc32)?; + writer.write_u32::(file.compressed_size as u32)?; + writer.write_u32::(file.uncompressed_size as u32)?; + Ok(()) +} + +fn write_central_directory_header(writer: &mut T, file: &ZipFileData) -> ZipResult<()> { + // central file header signature + writer.write_u32::(spec::CENTRAL_DIRECTORY_HEADER_SIGNATURE)?; + // version made by + let version_made_by = (file.system as u16) << 8 | (file.version_made_by as u16); + writer.write_u16::(version_made_by)?; + // version needed to extract + writer.write_u16::(file.version_needed())?; + // general puprose bit flag + let flag = if !file.file_name.is_ascii() { + 1u16 << 11 + } else { + 0 + }; + writer.write_u16::(flag)?; + // compression method + #[allow(deprecated)] + writer.write_u16::(file.compression_method.to_u16())?; + // last mod file time + date + writer.write_u16::(file.last_modified_time.timepart())?; + writer.write_u16::(file.last_modified_time.datepart())?; + // crc-32 + writer.write_u32::(file.crc32)?; + // compressed size + writer.write_u32::(file.compressed_size as u32)?; + // uncompressed size + writer.write_u32::(file.uncompressed_size as u32)?; + // file name length + writer.write_u16::(file.file_name.as_bytes().len() as u16)?; + // extra field length + let extra_field = build_extra_field(file)?; + writer.write_u16::(extra_field.len() as u16)?; + // file comment length + writer.write_u16::(0)?; + // disk number start + writer.write_u16::(0)?; + // internal file attribytes + writer.write_u16::(0)?; + // external file attributes + writer.write_u32::(file.external_attributes)?; + // relative offset of local header + writer.write_u32::(file.header_start as u32)?; + // file name + writer.write_all(file.file_name.as_bytes())?; + // extra field + writer.write_all(&extra_field)?; + // file comment + // + + Ok(()) +} + +fn build_extra_field(_file: &ZipFileData) -> ZipResult> { + let writer = Vec::new(); + // Future work + Ok(writer) +} + +fn path_to_string(path: &std::path::Path) -> String { + let mut path_str = String::new(); + for component in path.components() { + if let std::path::Component::Normal(os_str) = component { + if !path_str.is_empty() { + path_str.push('/'); + } + path_str.push_str(&*os_str.to_string_lossy()); + } + } + path_str +} + +#[cfg(test)] +mod test { + use super::{FileOptions, ZipWriter}; + use crate::compression::CompressionMethod; + use crate::types::DateTime; + use std::io; + use std::io::Write; + + #[test] + fn write_empty_zip() { + let mut writer = ZipWriter::new(io::Cursor::new(Vec::new())); + writer.set_comment("ZIP"); + let result = writer.finish().unwrap(); + assert_eq!(result.get_ref().len(), 25); + assert_eq!( + *result.get_ref(), + [80, 75, 5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 90, 73, 80] + ); + } + + #[test] + fn write_zip_dir() { + let mut writer = ZipWriter::new(io::Cursor::new(Vec::new())); + writer + .add_directory( + "test", + FileOptions::default().last_modified_time( + DateTime::from_date_and_time(2018, 8, 15, 20, 45, 6).unwrap(), + ), + ) + .unwrap(); + assert!(writer + .write(b"writing to a directory is not allowed, and will not write any data") + .is_err()); + let result = writer.finish().unwrap(); + assert_eq!(result.get_ref().len(), 108); + assert_eq!( + *result.get_ref(), + &[ + 80u8, 75, 3, 4, 20, 0, 0, 0, 0, 0, 163, 165, 15, 77, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 5, 0, 0, 0, 116, 101, 115, 116, 47, 80, 75, 1, 2, 46, 3, 20, 0, 0, 0, 0, 0, + 163, 165, 15, 77, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 237, 65, 0, 0, 0, 0, 116, 101, 115, 116, 47, 80, 75, 5, 6, 0, 0, 0, 0, 1, 0, + 1, 0, 51, 0, 0, 0, 35, 0, 0, 0, 0, 0, + ] as &[u8] + ); + } + + #[test] + fn write_mimetype_zip() { + let mut writer = ZipWriter::new(io::Cursor::new(Vec::new())); + let options = FileOptions { + compression_method: CompressionMethod::Stored, + last_modified_time: DateTime::default(), + permissions: Some(33188), + }; + writer.start_file("mimetype", options).unwrap(); + writer + .write(b"application/vnd.oasis.opendocument.text") + .unwrap(); + let result = writer.finish().unwrap(); + + assert_eq!(result.get_ref().len(), 153); + let mut v = Vec::new(); + v.extend_from_slice(include_bytes!("../tests/data/mimetype.zip")); + assert_eq!(result.get_ref(), &v); + } + + #[test] + fn path_to_string() { + let mut path = std::path::PathBuf::new(); + #[cfg(windows)] + path.push(r"C:\"); + #[cfg(unix)] + path.push("/"); + path.push("windows"); + path.push(".."); + path.push("."); + path.push("system32"); + let path_str = super::path_to_string(&path); + assert_eq!(path_str, "windows/system32"); + } +} diff --git a/third-party/zip/src/zipcrypto.rs b/third-party/zip/src/zipcrypto.rs new file mode 100644 index 000000000..32e8af8cf --- /dev/null +++ b/third-party/zip/src/zipcrypto.rs @@ -0,0 +1,162 @@ +//! Implementation of the ZipCrypto algorithm +//! +//! The following paper was used to implement the ZipCrypto algorithm: +//! [https://courses.cs.ut.ee/MTAT.07.022/2015_fall/uploads/Main/dmitri-report-f15-16.pdf](https://courses.cs.ut.ee/MTAT.07.022/2015_fall/uploads/Main/dmitri-report-f15-16.pdf) + +use std::num::Wrapping; + +/// A container to hold the current key state +struct ZipCryptoKeys { + key_0: Wrapping, + key_1: Wrapping, + key_2: Wrapping, +} + +impl ZipCryptoKeys { + fn new() -> ZipCryptoKeys { + ZipCryptoKeys { + key_0: Wrapping(0x12345678), + key_1: Wrapping(0x23456789), + key_2: Wrapping(0x34567890), + } + } + + fn update(&mut self, input: u8) { + self.key_0 = ZipCryptoKeys::crc32(self.key_0, input); + self.key_1 = + (self.key_1 + (self.key_0 & Wrapping(0xff))) * Wrapping(0x08088405) + Wrapping(1); + self.key_2 = ZipCryptoKeys::crc32(self.key_2, (self.key_1 >> 24).0 as u8); + } + + fn stream_byte(&mut self) -> u8 { + let temp: Wrapping = Wrapping(self.key_2.0 as u16) | Wrapping(3); + ((temp * (temp ^ Wrapping(1))) >> 8).0 as u8 + } + + fn decrypt_byte(&mut self, cipher_byte: u8) -> u8 { + let plain_byte: u8 = self.stream_byte() ^ cipher_byte; + self.update(plain_byte); + plain_byte + } + + #[allow(dead_code)] + fn encrypt_byte(&mut self, plain_byte: u8) -> u8 { + let cipher_byte: u8 = self.stream_byte() ^ plain_byte; + self.update(plain_byte); + cipher_byte + } + + fn crc32(crc: Wrapping, input: u8) -> Wrapping { + return (crc >> 8) ^ Wrapping(CRCTABLE[((crc & Wrapping(0xff)).0 as u8 ^ input) as usize]); + } +} + +/// A ZipCrypto reader with unverified password +pub struct ZipCryptoReader { + file: R, + keys: ZipCryptoKeys, +} + +impl ZipCryptoReader { + /// Note: The password is `&[u8]` and not `&str` because the + /// [zip specification](https://pkware.cachefly.net/webdocs/APPNOTE/APPNOTE-6.3.3.TXT) + /// does not specify password encoding (see function `update_keys` in the specification). + /// Therefore, if `&str` was used, the password would be UTF-8 and it + /// would be impossible to decrypt files that were encrypted with a + /// password byte sequence that is unrepresentable in UTF-8. + pub fn new(file: R, password: &[u8]) -> ZipCryptoReader { + let mut result = ZipCryptoReader { + file: file, + keys: ZipCryptoKeys::new(), + }; + + // Key the cipher by updating the keys with the password. + for byte in password.iter() { + result.keys.update(*byte); + } + + result + } + + /// Read the ZipCrypto header bytes and validate the password. + pub fn validate( + mut self, + crc32_plaintext: u32, + ) -> Result>, std::io::Error> { + // ZipCrypto prefixes a file with a 12 byte header + let mut header_buf = [0u8; 12]; + self.file.read_exact(&mut header_buf)?; + for byte in header_buf.iter_mut() { + *byte = self.keys.decrypt_byte(*byte); + } + + // PKZIP before 2.0 used 2 byte CRC check. + // PKZIP 2.0+ used 1 byte CRC check. It's more secure. + // We also use 1 byte CRC. + + if (crc32_plaintext >> 24) as u8 != header_buf[11] { + return Ok(None); // Wrong password + } + Ok(Some(ZipCryptoReaderValid { reader: self })) + } +} + +/// A ZipCrypto reader with verified password +pub struct ZipCryptoReaderValid { + reader: ZipCryptoReader, +} + +impl std::io::Read for ZipCryptoReaderValid { + fn read(&mut self, mut buf: &mut [u8]) -> std::io::Result { + // Note: There might be potential for optimization. Inspiration can be found at: + // https://github.com/kornelski/7z/blob/master/CPP/7zip/Crypto/ZipCrypto.cpp + + let result = self.reader.file.read(&mut buf); + for byte in buf.iter_mut() { + *byte = self.reader.keys.decrypt_byte(*byte); + } + result + } +} + +impl ZipCryptoReaderValid { + /// Consumes this decoder, returning the underlying reader. + pub fn into_inner(self) -> R { + self.reader.file + } +} + +static CRCTABLE: [u32; 256] = [ + 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, + 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, + 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, + 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, + 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, + 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, + 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, + 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, + 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, + 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, + 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, + 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, + 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, + 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, + 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f, + 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, + 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, + 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, + 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, + 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, + 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, + 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, + 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, + 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, + 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, + 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, + 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, + 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, + 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, + 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, + 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, + 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d, +]; From 5842669ad77afc03522b56efa9ac11c8fa2d024e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Wed, 24 Mar 2021 00:08:04 -0300 Subject: [PATCH 27/67] Update Cargo.toml --- Cargo.toml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e6162a9bf..6c88a097e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,14 @@ [package] name = "ouch" version = "0.1.0" -authors = ["Vinícius Rodrigues Miguel "] +authors = ["Vinícius Rodrigues Miguel "] edition = "2018" +readme = "README.md" +homepage = "https://github.com/vrmiguel/ouch" +license = "MIT" +keywords = ["decompression", "compression", "zip", "tar", "gzip"] +categories = ["command-line-utilities", "compression", "encoding"] + # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From e18dbbd667a80affbb284b11c6036e4b27a90685 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Wed, 24 Mar 2021 00:12:07 -0300 Subject: [PATCH 28/67] Update Cargo.toml --- Cargo.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 6c88a097e..9a8c4d1ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,9 +5,11 @@ authors = ["Vinícius Rodrigues Miguel "] edition = "2018" readme = "README.md" homepage = "https://github.com/vrmiguel/ouch" +repository = "https://github.com/vrmiguel/ouch" license = "MIT" keywords = ["decompression", "compression", "zip", "tar", "gzip"] categories = ["command-line-utilities", "compression", "encoding"] +description = "A command-line utility for easily compressing and decompressing files and directories." # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 729dda819e48532a952f475da129748daff48a92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Wed, 24 Mar 2021 01:40:16 -0300 Subject: [PATCH 29/67] Add support for Lzma decompression --- .gitignore | 1 + Cargo.toml | 4 ++-- README.md | 4 ++-- src/cli.rs | 11 +++++++-- src/decompressors/lzma.rs | 45 ++++++++++++++++++++++++++++++++++++ src/decompressors/mod.rs | 6 +++-- src/decompressors/tar.rs | 1 + src/decompressors/unified.rs | 18 ++++----------- src/error.rs | 7 +----- src/evaluator.rs | 16 +++++-------- src/main.rs | 31 ++++++++----------------- src/test.rs | 31 +++++++++++++++++++++++++ 12 files changed, 116 insertions(+), 59 deletions(-) create mode 100644 src/decompressors/lzma.rs diff --git a/.gitignore b/.gitignore index ea8c4bf7f..54b52f61c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +/tests diff --git a/Cargo.toml b/Cargo.toml index 9a8c4d1ac..af1f663ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,11 +19,11 @@ colored = "2.0.0" walkdir = "2.3.2" clap = "2.33.3" tar = "0.4.33" -xz2 = "0.1" +xz2 = "0.1.6" bzip2 = "0.4.2" flate2 = "1.0.20" # Keeping zip locally since upstream zip is staying on an older flate2 version # in order to not increase MSRV, which is not something I particularly care about # for ouch -zip = { path = "./third-party/zip" } \ No newline at end of file +zip = { version = "0.5.10", path = "./third-party/zip" } diff --git a/README.md b/README.md index 8f1dc6f16..720069f5b 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ `ouch` is the Obvious Unified Compression (and decompression) Helper. -| Supported formats | .tar | .zip | .tar.{.gz, .bz} | .zip.{.gz, .bz, .bz2} | .bz | .gz | .lz, .lzma | +| Supported formats | .tar | .zip | .tar.{.lz,.gz, .bz} | .zip.{.lz, .gz, .bz, .bz2} | .bz | .gz | .lz, .lzma | |-------------------|------|------|------------------------------|------------------------------|-----|-----|------------| -| Decompression | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | +| Decompression | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | Compression | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ## How does it work? diff --git a/src/cli.rs b/src/cli.rs index c16866799..fbde86509 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -29,6 +29,13 @@ pub fn clap_app<'a, 'b>() -> clap::App<'a, 'b> { clap::App::new("ouch") .version("0.1.0") .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, @@ -40,7 +47,7 @@ pub fn clap_app<'a, 'b>() -> clap::App<'a, 'b> { .multiple(true) .long("input") .short("i") - .help("Input files (TODO description)") + .help("The input files or directories.") .takes_value(true), ) .arg( @@ -50,7 +57,7 @@ pub fn clap_app<'a, 'b>() -> clap::App<'a, 'b> { .multiple(false) .long("output") .short("o") - .help("Output file (TODO description)") + .help("The output directory or compressed file.") .takes_value(true), ) } diff --git a/src/decompressors/lzma.rs b/src/decompressors/lzma.rs new file mode 100644 index 000000000..a970feb91 --- /dev/null +++ b/src/decompressors/lzma.rs @@ -0,0 +1,45 @@ +use std::{fs, io::Read}; + +use colored::Colorize; + +use crate::{error::OuchResult, utils}; +use crate::file::File; + +use super::decompressor::{DecompressionResult, Decompressor}; + +pub struct LzmaDecompressor {} + +impl LzmaDecompressor { + fn extract_to_memory(from: File) -> OuchResult> { + let mut ret = vec![]; + + let from_path = from.path; + if !from_path.exists() { + eprintln!("{}: could not find {:?}", "error".red(), from_path); + } + + let input_bytes = fs::read(&from_path)?; + + + xz2::read::XzDecoder::new_multi_decoder(&*input_bytes) + .read_to_end(&mut ret)?; + + println!("{}: extracted {:?} into memory. ({} bytes)", "info".yellow(), from_path, ret.len()); + + Ok(ret) + } +} + +impl Decompressor for LzmaDecompressor { + fn decompress(&self, from: File, into: &Option) -> OuchResult { + let destination_path = utils::get_destination_path(into); + + utils::create_path_if_non_existent(destination_path)?; + + Ok( + DecompressionResult::FileInMemory( + Self::extract_to_memory(from)? + ) + ) + } +} \ No newline at end of file diff --git a/src/decompressors/mod.rs b/src/decompressors/mod.rs index b50067856..5b9c1b072 100644 --- a/src/decompressors/mod.rs +++ b/src/decompressors/mod.rs @@ -1,7 +1,9 @@ mod decompressor; +mod unified; +mod lzma; mod tar; mod zip; -mod unified; + pub use decompressor::Decompressor; pub use decompressor::DecompressionResult; @@ -9,4 +11,4 @@ pub use self::tar::TarDecompressor; pub use self::zip::ZipDecompressor; pub use self::unified::GzipDecompressor; pub use self::unified::BzipDecompressor; -pub use self::unified::LzmaDecompressor; \ No newline at end of file +pub use self::lzma::LzmaDecompressor; \ No newline at end of file diff --git a/src/decompressors/tar.rs b/src/decompressors/tar.rs index 7e11d6e4a..0c6e2a498 100644 --- a/src/decompressors/tar.rs +++ b/src/decompressors/tar.rs @@ -8,6 +8,7 @@ use crate::file::File; use super::decompressor::{DecompressionResult, Decompressor}; +#[derive(Debug)] pub struct TarDecompressor {} impl TarDecompressor { diff --git a/src/decompressors/unified.rs b/src/decompressors/unified.rs index c116be1df..463270ec4 100644 --- a/src/decompressors/unified.rs +++ b/src/decompressors/unified.rs @@ -1,15 +1,15 @@ use std::{ io::{self, Read}, - path::{Path, PathBuf}, + path::Path, }; -use bzip2::Compress; + use colored::Colorize; // use niffler; use crate::{extension::CompressionFormat, file::File}; use crate::{ - error::{self, OuchResult}, + error::OuchResult, utils, }; @@ -17,23 +17,19 @@ use super::decompressor::DecompressionResult; use super::decompressor::Decompressor; pub struct UnifiedDecompressor {} -pub struct LzmaDecompressor {} pub struct GzipDecompressor {} pub struct BzipDecompressor {} fn get_decoder<'a>(format: CompressionFormat, buffer: Box) -> Box { match format { - CompressionFormat::Lzma => Box::new(xz2::read::XzDecoder::new(buffer)), CompressionFormat::Bzip => Box::new(bzip2::read::BzDecoder::new(buffer)), CompressionFormat::Gzip => Box::new(flate2::read::MultiGzDecoder::new(buffer)), - other => unreachable!() + _other => unreachable!() } } impl UnifiedDecompressor { fn unpack_file(from: &Path, format: CompressionFormat) -> OuchResult> { - // println!("{}: trying to decompress {:?}", "info".yellow(), from); - let file = std::fs::read(from)?; let mut reader = get_decoder(format, Box::new(&file[..])); @@ -62,12 +58,6 @@ impl UnifiedDecompressor { } } -impl Decompressor for LzmaDecompressor { - fn decompress(&self, from: File, into: &Option) -> OuchResult { - UnifiedDecompressor::decompress(from, CompressionFormat::Lzma, into) - } -} - impl Decompressor for GzipDecompressor { fn decompress(&self, from: File, into: &Option) -> OuchResult { UnifiedDecompressor::decompress(from, CompressionFormat::Gzip, into) diff --git a/src/error.rs b/src/error.rs index 008393b51..c3b37bc42 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,4 @@ -use std::{fmt, path::PathBuf}; +use std::fmt; use colored::Colorize; @@ -13,10 +13,8 @@ pub enum Error { FileNotFound, AlreadyExists, InvalidZipArchive(&'static str), - UnsupportedArchive(PathBuf), PermissionDenied, UnsupportedZipArchive(&'static str), - FileTooShort, InputsMustHaveBeenDecompressible(String), } @@ -40,9 +38,6 @@ impl fmt::Display for Error { Error::FileNotFound => { write!(f, "file not found!") } - Error::UnsupportedArchive(path) => { - write!(f, "ouch is currently uncapable of decompressing {:?}", path) - } err => { // TODO write!(f, "todo: missing description for error {:?}", err) diff --git a/src/evaluator.rs b/src/evaluator.rs index 1eb1be542..6a2d22fd1 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -15,6 +15,7 @@ use crate::decompressors::{ ZipDecompressor, GzipDecompressor, BzipDecompressor, + LzmaDecompressor, DecompressionResult }; @@ -31,12 +32,10 @@ use crate::file::File; use crate::utils; -pub struct Evaluator { - // verbosity: Verbosity -} +pub struct Evaluator {} impl Evaluator { - fn get_compressor( + pub fn get_compressor( file: &File, ) -> error::OuchResult<(Option>, Box)> { if file.extension.is_none() { @@ -71,13 +70,12 @@ impl Evaluator { CompressionFormat::Tar => Box::new(TarCompressor {}), CompressionFormat::Zip => Box::new(ZipCompressor {}), _other => todo!() - // }; Ok((first_compressor, second_compressor)) } - fn get_decompressor( + pub fn get_decompressor( file: &File, ) -> error::OuchResult<(Option>, Box)> { if file.extension.is_none() { @@ -95,11 +93,9 @@ impl Evaluator { CompressionFormat::Zip => Box::new(ZipDecompressor {}), - CompressionFormat::Gzip => Box::new(GzipDecompressor {}), + CompressionFormat::Gzip => Box::new(GzipDecompressor{}), - CompressionFormat::Lzma => { - todo!() - } + CompressionFormat::Lzma => Box::new(LzmaDecompressor{}), CompressionFormat::Bzip => { Box::new(BzipDecompressor {}) diff --git a/src/main.rs b/src/main.rs index 02c4d88da..eabeafb19 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,6 @@ -use std::{convert::TryFrom, io::Write}; +use std::convert::TryFrom; use colored::Colorize; -use walkdir::WalkDir; mod cli; mod error; @@ -34,27 +33,17 @@ fn main() -> error::OuchResult<()>{ Ok(()) } -// fn main() { -// use zip::ZipWriter; +// fn main() -> error::OuchResult<()> { +// let bytes = fs::read("extension.tar.lzma")?; -// let buf = vec![]; -// let mut writer = zip::ZipWriter::new(std::io::Cursor::new(buf)); +// let mut ret = vec![]; -// let options = zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Deflated); +// xz2::read::XzDecoder::new_multi_decoder(&*bytes) +// .read_to_end(&mut ret) +// .unwrap(); + +// fs::write("extension.tar", &*bytes).unwrap(); -// for entry in WalkDir::new("src/compressors/compressor.rs") { -// let entry = entry.unwrap(); -// let entry_path = entry.path().clone(); -// if entry_path.is_dir() { -// continue; -// } -// writer.start_file(entry_path.to_string_lossy(), options).unwrap(); -// let file_bytes = std::fs::read(entry.path()).unwrap(); -// writer.write(&*file_bytes).unwrap(); -// } - -// let bytes = writer.finish().unwrap(); - -// std::fs::write("mainmain.rar", bytes.into_inner()).unwrap(); +// Ok(()) // } \ No newline at end of file diff --git a/src/test.rs b/src/test.rs index 1aed1926d..4e612172f 100644 --- a/src/test.rs +++ b/src/test.rs @@ -201,3 +201,34 @@ mod extension_extraction { Ok(()) } } + +// #[cfg(test)] +// mod evaluator { +// use crate::extension::Extension; +// use crate::error::OuchResult; +// use crate::file::File; +// use crate::evaluator::Evaluator; +// use crate::decompressors::{Decompressor, TarDecompressor, GzipDecompressor}; + +// #[test] +// fn test() -> OuchResult<()> { +// let extension = Extension::new("folder.tar.gz")?; + +// let file = File { +// path: "folder.tar.gz".into(), +// contents_in_memory: None, +// extension: Some(extension), +// }; + +// let (fst, snd) = Evaluator::get_decompressor(&file)?; + +// let fst = fst.unwrap(); + +// assert_eq!( +// fst, +// Some(Box::new(TarDecompressor::{}) +// ); + +// Ok(()) +// } +// } \ No newline at end of file From 320f27ff8f6accbeab4457643384078bfb188fa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Wed, 24 Mar 2021 01:56:18 -0300 Subject: [PATCH 30/67] Updates README.md and Cargo.toml --- Cargo.lock | 39 +- Cargo.toml | 12 +- README.md | 13 +- third-party/zip/.gitignore | 5 - third-party/zip/CODE_OF_CONDUCT.md | 77 -- third-party/zip/Cargo.toml | 35 - third-party/zip/LICENSE | 21 - third-party/zip/README.md | 70 -- third-party/zip/benches/read_entry.rs | 43 - third-party/zip/examples/extract.rs | 63 -- third-party/zip/examples/extract_lorem.rs | 31 - third-party/zip/examples/file_info.rs | 53 - third-party/zip/examples/stdin_info.rs | 34 - third-party/zip/examples/write_dir.rs | 120 --- third-party/zip/examples/write_sample.rs | 71 -- third-party/zip/src/compression.rs | 180 ---- third-party/zip/src/cp437.rs | 203 ---- third-party/zip/src/crc32.rs | 93 -- third-party/zip/src/lib.rs | 21 - third-party/zip/src/read.rs | 1078 --------------------- third-party/zip/src/result.rs | 39 - third-party/zip/src/spec.rs | 182 ---- third-party/zip/src/types.rs | 474 --------- third-party/zip/src/write.rs | 821 ---------------- third-party/zip/src/zipcrypto.rs | 162 ---- 25 files changed, 33 insertions(+), 3907 deletions(-) delete mode 100644 third-party/zip/.gitignore delete mode 100644 third-party/zip/CODE_OF_CONDUCT.md delete mode 100644 third-party/zip/Cargo.toml delete mode 100644 third-party/zip/LICENSE delete mode 100644 third-party/zip/README.md delete mode 100644 third-party/zip/benches/read_entry.rs delete mode 100644 third-party/zip/examples/extract.rs delete mode 100644 third-party/zip/examples/extract_lorem.rs delete mode 100644 third-party/zip/examples/file_info.rs delete mode 100644 third-party/zip/examples/stdin_info.rs delete mode 100644 third-party/zip/examples/write_dir.rs delete mode 100644 third-party/zip/examples/write_sample.rs delete mode 100644 third-party/zip/src/compression.rs delete mode 100644 third-party/zip/src/cp437.rs delete mode 100644 third-party/zip/src/crc32.rs delete mode 100644 third-party/zip/src/lib.rs delete mode 100644 third-party/zip/src/read.rs delete mode 100644 third-party/zip/src/result.rs delete mode 100644 third-party/zip/src/spec.rs delete mode 100644 third-party/zip/src/types.rs delete mode 100644 third-party/zip/src/write.rs delete mode 100644 third-party/zip/src/zipcrypto.rs diff --git a/Cargo.lock b/Cargo.lock index 699d6fa5e..564aa3ee6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,10 +1,10 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. [[package]] -name = "adler" -version = "1.0.2" +name = "adler32" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" [[package]] name = "ansi_term" @@ -26,12 +26,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "autocfg" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" - [[package]] name = "bitflags" version = "1.2.1" @@ -81,6 +75,12 @@ version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + [[package]] name = "cfg-if" version = "1.0.0" @@ -119,7 +119,7 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -128,7 +128,7 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "libc", "redox_syscall", "winapi", @@ -136,11 +136,11 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.20" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3aec53de10fe96d7d8c565eb17f2c687bb5518a2ec453b5b1252964526abe0" +checksum = "2cfff41391129e0a856d6d822600b8d71179d46879e310417eb9c762eb178b42" dependencies = [ - "cfg-if", + "cfg-if 0.1.10", "crc32fast", "libc", "miniz_oxide", @@ -180,12 +180,11 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.4.4" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" dependencies = [ - "adler", - "autocfg", + "adler32", ] [[package]] @@ -398,7 +397,9 @@ dependencies = [ [[package]] name = "zip" -version = "0.5.10" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8264fcea9b7a036a4a5103d7153e988dbc2ebbafb34f68a3c2d404b6b82d74b6" dependencies = [ "byteorder", "bzip2 0.3.3", diff --git a/Cargo.toml b/Cargo.toml index af1f663ca..f72c75870 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,9 +21,11 @@ clap = "2.33.3" tar = "0.4.33" xz2 = "0.1.6" bzip2 = "0.4.2" -flate2 = "1.0.20" +flate2 = "1.0.14" +zip = "0.5.11" -# Keeping zip locally since upstream zip is staying on an older flate2 version -# in order to not increase MSRV, which is not something I particularly care about -# for ouch -zip = { version = "0.5.10", path = "./third-party/zip" } + +[profile.release] +lto = true +codegen-units = 1 +opt-level = 3 diff --git a/README.md b/README.md index 720069f5b..28b8fc7d4 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ ``` ouch 0.1.0 +Vinícius R. Miguel ouch is a unified compression & decompression utility USAGE: @@ -24,8 +25,8 @@ FLAGS: -V, --version Prints version information OPTIONS: - -i, --input ... Input files (TODO description) - -o, --output Output file (TODO description) + -i, --input ... The input files or directories. + -o, --output The output directory or compressed file. ``` ### Examples @@ -42,8 +43,8 @@ When no output file is supplied, `ouch` infers that it must decompress all of it ```bash $ ouch -i file{1..5}.tar.gz -o some-folder -info: attempting to decompress input files into single_folder -info: done! +# Decompresses file1.tar.gz, file2.tar.gz, file3.tar.gz, file4.tar.gz and file5.tar.gz to some-folder +# The folder `ouch` saves to will be created if it doesn't already exist ``` When the output file is not a compressed file, `ouch` will check if all input files are decompressible and infer that it must decompress them into the output file. @@ -52,8 +53,6 @@ When the output file is not a compressed file, `ouch` will check if all input fi ```bash $ ouch -i file{1..20} -o archive.tar -info: trying to compress input files into 'archive.tar' -info: done! ``` ### Error scenarios @@ -65,7 +64,7 @@ $ ouch -i some-file -o some-folder error: file 'some-file' is not decompressible. ``` -`ouch` might (TODO!) be able to sniff a file's compression format if it isn't supplied in the future, but that is not currently implemented. +`ouch` cannot infer `some-file`'s compression format since it lacks an extension. Likewise, `ouch` cannot infer that the output file given is a compressed file, so it shows the user an error. ## Installation diff --git a/third-party/zip/.gitignore b/third-party/zip/.gitignore deleted file mode 100644 index 0a8fc033c..000000000 --- a/third-party/zip/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -Cargo.lock -target - -\.idea/ -tests/ diff --git a/third-party/zip/CODE_OF_CONDUCT.md b/third-party/zip/CODE_OF_CONDUCT.md deleted file mode 100644 index 845634eb5..000000000 --- a/third-party/zip/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,77 +0,0 @@ - -# Contributor Covenant Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to make participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, sex characteristics, gender identity and expression, -level of experience, education, socio-economic status, nationality, personal -appearance, race, religion, or sexual identity and orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment -include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies within all project spaces, and it also applies when -an individual is representing the project or its community in public spaces. -Examples of representing a project or community include using an official -project e-mail address, posting via an official social media account, or acting -as an appointed representative at an online or offline event. Representation of -a project may be further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at ryan.levick@gmail.com. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html - -[homepage]: https://www.contributor-covenant.org - -For answers to common questions about this code of conduct, see -https://www.contributor-covenant.org/faq diff --git a/third-party/zip/Cargo.toml b/third-party/zip/Cargo.toml deleted file mode 100644 index 0a8583a1b..000000000 --- a/third-party/zip/Cargo.toml +++ /dev/null @@ -1,35 +0,0 @@ -[package] -name = "zip" -version = "0.5.10" -authors = ["Mathijs van de Nes ", "Marli Frost ", "Ryan Levick "] -license = "MIT" -repository = "https://github.com/zip-rs/zip.git" -keywords = ["zip", "archive"] -description = """ -Library to support the reading and writing of zip files. -""" -edition = "2018" - -[dependencies] - -flate2 = { version = "1.0.20", default-features = false, optional = true } -time = { version = "0.1", optional = true } -byteorder = "1.3" -bzip2 = { version = "0.3", optional = true } -crc32fast = "1.0" -thiserror = "1.0" - -[dev-dependencies] -bencher = "0.1" -rand = "0.7" -walkdir = "2" - -[features] -deflate = ["flate2/rust_backend"] -deflate-miniz = ["flate2/default"] -deflate-zlib = ["flate2/zlib"] -default = ["bzip2", "deflate", "time"] - -[[bench]] -name = "read_entry" -harness = false diff --git a/third-party/zip/LICENSE b/third-party/zip/LICENSE deleted file mode 100644 index b2d7f7bbd..000000000 --- a/third-party/zip/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2014 Mathijs van de Nes - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file diff --git a/third-party/zip/README.md b/third-party/zip/README.md deleted file mode 100644 index 79d2dcc7c..000000000 --- a/third-party/zip/README.md +++ /dev/null @@ -1,70 +0,0 @@ -zip-rs -====== - -[![Build Status](https://img.shields.io/github/workflow/status/zip-rs/zip/CI)](https://github.com/zip-rs/zip/actions?query=branch%3Amaster+workflow%3ACI) -[![Crates.io version](https://img.shields.io/crates/v/zip.svg)](https://crates.io/crates/zip) - -[Documentation](https://docs.rs/zip/0.5.10/zip/) - - -Info ----- - -A zip library for rust which supports reading and writing of simple ZIP files. - -Supported compression formats: - -* stored (i.e. none) -* deflate -* bzip2 - -Currently unsupported zip extensions: - -* Encryption -* Multi-disk - -Usage ------ - -With all default features: - -```toml -[dependencies] -zip = "0.5" -``` - -Without the default features: - -```toml -[dependencies] -zip = { version = "0.5", default-features = false } -``` - -The features available are: - -* `deflate`: Enables the deflate compression algorithm, which is the default for zipfiles -* `bzip2`: Enables the BZip2 compression algorithm. -* `time`: Enables features using the [time](https://github.com/rust-lang-deprecated/time) crate. - -All of these are enabled by default. - -MSRV ----- - -Our current Minimum Supported Rust Version is **1.34.0**. When adding features, -we will follow these guidelines: - -- We will always support the latest four minor Rust versions. This gives you a 6 - month window to upgrade your compiler. -- Any change to the MSRV will be accompanied with a **minor** version bump - - While the crate is pre-1.0, this will be a change to the PATCH version. - -Examples --------- - -See the [examples directory](examples) for: - * How to write a file to a zip. - * How to write a directory of files to a zip (using [walkdir](https://github.com/BurntSushi/walkdir)). - * How to extract a zip file. - * How to extract a single file from a zip. - * How to read a zip from the standard input. diff --git a/third-party/zip/benches/read_entry.rs b/third-party/zip/benches/read_entry.rs deleted file mode 100644 index 25c0b94ae..000000000 --- a/third-party/zip/benches/read_entry.rs +++ /dev/null @@ -1,43 +0,0 @@ -use bencher::{benchmark_group, benchmark_main}; - -use std::io::{Cursor, Read, Write}; - -use bencher::Bencher; -use rand::Rng; -use zip::{ZipArchive, ZipWriter}; - -fn generate_random_archive(size: usize) -> Vec { - let data = Vec::new(); - let mut writer = ZipWriter::new(Cursor::new(data)); - let options = - zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Stored); - - writer.start_file("random.dat", options).unwrap(); - let mut bytes = vec![0u8; size]; - rand::thread_rng().fill_bytes(&mut bytes); - writer.write_all(&bytes).unwrap(); - - writer.finish().unwrap().into_inner() -} - -fn read_entry(bench: &mut Bencher) { - let size = 1024 * 1024; - let bytes = generate_random_archive(size); - let mut archive = ZipArchive::new(Cursor::new(bytes.as_slice())).unwrap(); - - bench.iter(|| { - let mut file = archive.by_name("random.dat").unwrap(); - let mut buf = [0u8; 1024]; - loop { - let n = file.read(&mut buf).unwrap(); - if n == 0 { - break; - } - } - }); - - bench.bytes = size as u64; -} - -benchmark_group!(benches, read_entry); -benchmark_main!(benches); diff --git a/third-party/zip/examples/extract.rs b/third-party/zip/examples/extract.rs deleted file mode 100644 index 05c5a4aad..000000000 --- a/third-party/zip/examples/extract.rs +++ /dev/null @@ -1,63 +0,0 @@ -use std::fs; -use std::io; - -fn main() { - std::process::exit(real_main()); -} - -fn real_main() -> i32 { - let args: Vec<_> = std::env::args().collect(); - if args.len() < 2 { - println!("Usage: {} ", args[0]); - return 1; - } - let fname = std::path::Path::new(&*args[1]); - let file = fs::File::open(&fname).unwrap(); - - let mut archive = zip::ZipArchive::new(file).unwrap(); - - for i in 0..archive.len() { - let mut file = archive.by_index(i).unwrap(); - let outpath = match file.enclosed_name() { - Some(path) => path.to_owned(), - None => continue, - }; - - { - let comment = file.comment(); - if !comment.is_empty() { - println!("File {} comment: {}", i, comment); - } - } - - if (&*file.name()).ends_with('/') { - println!("File {} extracted to \"{}\"", i, outpath.display()); - fs::create_dir_all(&outpath).unwrap(); - } else { - println!( - "File {} extracted to \"{}\" ({} bytes)", - i, - outpath.display(), - file.size() - ); - if let Some(p) = outpath.parent() { - if !p.exists() { - fs::create_dir_all(&p).unwrap(); - } - } - let mut outfile = fs::File::create(&outpath).unwrap(); - io::copy(&mut file, &mut outfile).unwrap(); - } - - // Get and Set permissions - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - - if let Some(mode) = file.unix_mode() { - fs::set_permissions(&outpath, fs::Permissions::from_mode(mode)).unwrap(); - } - } - } - return 0; -} diff --git a/third-party/zip/examples/extract_lorem.rs b/third-party/zip/examples/extract_lorem.rs deleted file mode 100644 index 89e33ef9a..000000000 --- a/third-party/zip/examples/extract_lorem.rs +++ /dev/null @@ -1,31 +0,0 @@ -use std::io::prelude::*; - -fn main() { - std::process::exit(real_main()); -} - -fn real_main() -> i32 { - let args: Vec<_> = std::env::args().collect(); - if args.len() < 2 { - println!("Usage: {} ", args[0]); - return 1; - } - let fname = std::path::Path::new(&*args[1]); - let zipfile = std::fs::File::open(&fname).unwrap(); - - let mut archive = zip::ZipArchive::new(zipfile).unwrap(); - - let mut file = match archive.by_name("test/lorem_ipsum.txt") { - Ok(file) => file, - Err(..) => { - println!("File test/lorem_ipsum.txt not found"); - return 2; - } - }; - - let mut contents = String::new(); - file.read_to_string(&mut contents).unwrap(); - println!("{}", contents); - - return 0; -} diff --git a/third-party/zip/examples/file_info.rs b/third-party/zip/examples/file_info.rs deleted file mode 100644 index 315b5c386..000000000 --- a/third-party/zip/examples/file_info.rs +++ /dev/null @@ -1,53 +0,0 @@ -use std::fs; -use std::io::BufReader; - -fn main() { - std::process::exit(real_main()); -} - -fn real_main() -> i32 { - let args: Vec<_> = std::env::args().collect(); - if args.len() < 2 { - println!("Usage: {} ", args[0]); - return 1; - } - let fname = std::path::Path::new(&*args[1]); - let file = fs::File::open(&fname).unwrap(); - let reader = BufReader::new(file); - - let mut archive = zip::ZipArchive::new(reader).unwrap(); - - for i in 0..archive.len() { - let file = archive.by_index(i).unwrap(); - let outpath = match file.enclosed_name() { - Some(path) => path, - None => { - println!("Entry {} has a suspicious path", file.name()); - continue; - } - }; - - { - let comment = file.comment(); - if !comment.is_empty() { - println!("Entry {} comment: {}", i, comment); - } - } - - if (&*file.name()).ends_with('/') { - println!( - "Entry {} is a directory with name \"{}\"", - i, - outpath.display() - ); - } else { - println!( - "Entry {} is a file with name \"{}\" ({} bytes)", - i, - outpath.display(), - file.size() - ); - } - } - return 0; -} diff --git a/third-party/zip/examples/stdin_info.rs b/third-party/zip/examples/stdin_info.rs deleted file mode 100644 index 606944ce0..000000000 --- a/third-party/zip/examples/stdin_info.rs +++ /dev/null @@ -1,34 +0,0 @@ -use std::io::{self, Read}; - -fn main() { - std::process::exit(real_main()); -} - -fn real_main() -> i32 { - let stdin = io::stdin(); - let mut stdin_handle = stdin.lock(); - let mut buf = [0u8; 16]; - - loop { - match zip::read::read_zipfile_from_stream(&mut stdin_handle) { - Ok(Some(mut file)) => { - println!( - "{}: {} bytes ({} bytes packed)", - file.name(), - file.size(), - file.compressed_size() - ); - match file.read(&mut buf) { - Ok(n) => println!("The first {} bytes are: {:?}", n, &buf[0..n]), - Err(e) => println!("Could not read the file: {:?}", e), - }; - } - Ok(None) => break, - Err(e) => { - println!("Error encountered while reading zip: {:?}", e); - return 1; - } - } - } - return 0; -} diff --git a/third-party/zip/examples/write_dir.rs b/third-party/zip/examples/write_dir.rs deleted file mode 100644 index 793bd6ba6..000000000 --- a/third-party/zip/examples/write_dir.rs +++ /dev/null @@ -1,120 +0,0 @@ -use std::io::prelude::*; -use std::io::{Seek, Write}; -use std::iter::Iterator; -use zip::result::ZipError; -use zip::write::FileOptions; - -use std::fs::File; -use std::path::Path; -use walkdir::{DirEntry, WalkDir}; - -fn main() { - std::process::exit(real_main()); -} - -const METHOD_STORED: Option = Some(zip::CompressionMethod::Stored); - -#[cfg(any( - feature = "deflate", - feature = "deflate-miniz", - feature = "deflate-zlib" -))] -const METHOD_DEFLATED: Option = Some(zip::CompressionMethod::Deflated); -#[cfg(not(any( - feature = "deflate", - feature = "deflate-miniz", - feature = "deflate-zlib" -)))] -const METHOD_DEFLATED: Option = None; - -#[cfg(feature = "bzip2")] -const METHOD_BZIP2: Option = Some(zip::CompressionMethod::Bzip2); -#[cfg(not(feature = "bzip2"))] -const METHOD_BZIP2: Option = None; - -fn real_main() -> i32 { - let args: Vec<_> = std::env::args().collect(); - if args.len() < 3 { - println!( - "Usage: {} ", - args[0] - ); - return 1; - } - - let src_dir = &*args[1]; - let dst_file = &*args[2]; - for &method in [METHOD_STORED, METHOD_DEFLATED, METHOD_BZIP2].iter() { - if method.is_none() { - continue; - } - match doit(src_dir, dst_file, method.unwrap()) { - Ok(_) => println!("done: {} written to {}", src_dir, dst_file), - Err(e) => println!("Error: {:?}", e), - } - } - - return 0; -} - -fn zip_dir( - it: &mut dyn Iterator, - prefix: &str, - writer: T, - method: zip::CompressionMethod, -) -> zip::result::ZipResult<()> -where - T: Write + Seek, -{ - let mut zip = zip::ZipWriter::new(writer); - let options = FileOptions::default() - .compression_method(method) - .unix_permissions(0o755); - - let mut buffer = Vec::new(); - for entry in it { - let path = entry.path(); - let name = path.strip_prefix(Path::new(prefix)).unwrap(); - - // Write file or directory explicitly - // Some unzip tools unzip files with directory paths correctly, some do not! - if path.is_file() { - println!("adding file {:?} as {:?} ...", path, name); - #[allow(deprecated)] - zip.start_file_from_path(name, options)?; - let mut f = File::open(path)?; - - f.read_to_end(&mut buffer)?; - zip.write_all(&*buffer)?; - buffer.clear(); - } else if name.as_os_str().len() != 0 { - // Only if not root! Avoids path spec / warning - // and mapname conversion failed error on unzip - println!("adding dir {:?} as {:?} ...", path, name); - #[allow(deprecated)] - zip.add_directory_from_path(name, options)?; - } - } - zip.finish()?; - Result::Ok(()) -} - -fn doit( - src_dir: &str, - dst_file: &str, - method: zip::CompressionMethod, -) -> zip::result::ZipResult<()> { - if !Path::new(src_dir).is_dir() { - return Err(ZipError::FileNotFound); - } - - let path = Path::new(dst_file); - let file = File::create(&path).unwrap(); - - let walkdir = WalkDir::new(src_dir.to_string()); - let it = walkdir.into_iter(); - - zip_dir(&mut it.filter_map(|e| e.ok()), src_dir, file, method)?; - - Ok(()) -} diff --git a/third-party/zip/examples/write_sample.rs b/third-party/zip/examples/write_sample.rs deleted file mode 100644 index 4ef5ce347..000000000 --- a/third-party/zip/examples/write_sample.rs +++ /dev/null @@ -1,71 +0,0 @@ -use std::io::prelude::*; -use zip::write::FileOptions; - -fn main() { - std::process::exit(real_main()); -} - -fn real_main() -> i32 { - let args: Vec<_> = std::env::args().collect(); - if args.len() < 2 { - println!("Usage: {} ", args[0]); - return 1; - } - - let filename = &*args[1]; - match doit(filename) { - Ok(_) => println!("File written to {}", filename), - Err(e) => println!("Error: {:?}", e), - } - - return 0; -} - -fn doit(filename: &str) -> zip::result::ZipResult<()> { - let path = std::path::Path::new(filename); - let file = std::fs::File::create(&path).unwrap(); - - let mut zip = zip::ZipWriter::new(file); - - zip.add_directory("test/", Default::default())?; - - let options = FileOptions::default() - .compression_method(zip::CompressionMethod::Stored) - .unix_permissions(0o755); - zip.start_file("test/☃.txt", options)?; - zip.write_all(b"Hello, World!\n")?; - - zip.start_file("test/lorem_ipsum.txt", Default::default())?; - zip.write_all(LOREM_IPSUM)?; - - zip.finish()?; - Ok(()) -} - -const LOREM_IPSUM : &'static [u8] = b"Lorem ipsum dolor sit amet, consectetur adipiscing elit. In tellus elit, tristique vitae mattis egestas, ultricies vitae risus. Quisque sit amet quam ut urna aliquet -molestie. Proin blandit ornare dui, a tempor nisl accumsan in. Praesent a consequat felis. Morbi metus diam, auctor in auctor vel, feugiat id odio. Curabitur ex ex, -dictum quis auctor quis, suscipit id lorem. Aliquam vestibulum dolor nec enim vehicula, porta tristique augue tincidunt. Vivamus ut gravida est. Sed pellentesque, dolor -vitae tristique consectetur, neque lectus pulvinar dui, sed feugiat purus diam id lectus. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per -inceptos himenaeos. Maecenas feugiat velit in ex ultrices scelerisque id id neque. - -Phasellus sed nisi in augue sodales pulvinar ut et leo. Pellentesque eget leo vitae massa bibendum sollicitudin. Curabitur erat lectus, congue quis auctor sed, aliquet -bibendum est. Ut porta ultricies turpis at maximus. Cras non lobortis justo. Duis rutrum magna sed velit facilisis, et sagittis metus laoreet. Pellentesque quam ligula, -dapibus vitae mauris quis, dapibus cursus leo. Sed sit amet condimentum eros. Nulla vestibulum enim sit amet lorem pharetra, eu fringilla nisl posuere. Sed tristique non -nibh at viverra. Vivamus sed accumsan lacus, nec pretium eros. Mauris elementum arcu eu risus fermentum, tempor ullamcorper neque aliquam. Sed tempor in erat eu -suscipit. In euismod in libero in facilisis. Donec sagittis, odio et fermentum dignissim, risus justo pretium nibh, eget vestibulum lectus metus vel lacus. - -Quisque feugiat, magna ac feugiat ullamcorper, augue justo consequat felis, ut fermentum arcu lorem vitae ligula. Quisque iaculis tempor maximus. In quis eros ac tellus -aliquam placerat quis id tellus. Donec non gravida nulla. Morbi faucibus neque sed faucibus aliquam. Sed accumsan mattis nunc, non interdum justo. Cras vitae facilisis -leo. Fusce sollicitudin ultrices sagittis. Maecenas eget massa id lorem dignissim ultrices non et ligula. Pellentesque aliquam mi ac neque tempus ornare. Morbi non enim -vulputate quam ullamcorper finibus id non neque. Quisque malesuada commodo lorem, ut ornare velit iaculis rhoncus. Mauris vel maximus ex. - -Morbi eleifend blandit diam, non vulputate ante iaculis in. Donec pellentesque augue id enim suscipit, eget suscipit lacus commodo. Ut vel ex vitae elit imperdiet -vulputate. Nunc eu mattis orci, ut pretium sem. Nam vitae purus mollis ante tempus malesuada a at magna. Integer mattis lectus non luctus lobortis. In a cursus quam, -eget faucibus sem. - -Donec vitae condimentum nisi, non efficitur massa. Praesent sed mi in massa sollicitudin iaculis. Pellentesque a libero ultrices, sodales lacus eu, ornare dui. In -laoreet est nec dolor aliquam consectetur. Integer iaculis felis venenatis libero pulvinar, ut pretium odio interdum. Donec in nisi eu dolor varius vestibulum eget vel -nunc. Morbi a venenatis quam, in vehicula justo. Nam risus dui, auctor eu accumsan at, sagittis ac lectus. Mauris iaculis dignissim interdum. Cras cursus dapibus auctor. -Donec sagittis massa vitae tortor viverra vehicula. Mauris fringilla nunc eu lorem ultrices placerat. Maecenas posuere porta quam at semper. Praesent eu bibendum eros. -Nunc congue sollicitudin ante, sollicitudin lacinia magna cursus vitae. -"; diff --git a/third-party/zip/src/compression.rs b/third-party/zip/src/compression.rs deleted file mode 100644 index 5fdde0705..000000000 --- a/third-party/zip/src/compression.rs +++ /dev/null @@ -1,180 +0,0 @@ -//! Possible ZIP compression methods. - -use std::fmt; - -#[allow(deprecated)] -/// Identifies the storage format used to compress a file within a ZIP archive. -/// -/// Each file's compression method is stored alongside it, allowing the -/// contents to be read without context. -/// -/// When creating ZIP files, you may choose the method to use with -/// [`zip::write::FileOptions::compression_method`] -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub enum CompressionMethod { - /// Store the file as is - Stored, - /// Compress the file using Deflate - #[cfg(any( - feature = "deflate", - feature = "deflate-miniz", - feature = "deflate-zlib" - ))] - Deflated, - /// Compress the file using BZIP2 - #[cfg(feature = "bzip2")] - Bzip2, - /// Unsupported compression method - #[deprecated(since = "0.5.7", note = "use the constants instead")] - Unsupported(u16), -} -#[allow(deprecated, missing_docs)] -/// All compression methods defined for the ZIP format -impl CompressionMethod { - pub const STORE: Self = CompressionMethod::Stored; - pub const SHRINK: Self = CompressionMethod::Unsupported(1); - pub const REDUCE_1: Self = CompressionMethod::Unsupported(2); - pub const REDUCE_2: Self = CompressionMethod::Unsupported(3); - pub const REDUCE_3: Self = CompressionMethod::Unsupported(4); - pub const REDUCE_4: Self = CompressionMethod::Unsupported(5); - pub const IMPLODE: Self = CompressionMethod::Unsupported(6); - #[cfg(any( - feature = "deflate", - feature = "deflate-miniz", - feature = "deflate-zlib" - ))] - pub const DEFLATE: Self = CompressionMethod::Deflated; - #[cfg(not(any( - feature = "deflate", - feature = "deflate-miniz", - feature = "deflate-zlib" - )))] - pub const DEFLATE: Self = CompressionMethod::Unsupported(8); - pub const DEFLATE64: Self = CompressionMethod::Unsupported(9); - pub const PKWARE_IMPLODE: Self = CompressionMethod::Unsupported(10); - #[cfg(feature = "bzip2")] - pub const BZIP2: Self = CompressionMethod::Bzip2; - #[cfg(not(feature = "bzip2"))] - pub const BZIP2: Self = CompressionMethod::Unsupported(12); - pub const LZMA: Self = CompressionMethod::Unsupported(14); - pub const IBM_ZOS_CMPSC: Self = CompressionMethod::Unsupported(16); - pub const IBM_TERSE: Self = CompressionMethod::Unsupported(18); - pub const ZSTD_DEPRECATED: Self = CompressionMethod::Unsupported(20); - pub const ZSTD: Self = CompressionMethod::Unsupported(93); - pub const MP3: Self = CompressionMethod::Unsupported(94); - pub const XZ: Self = CompressionMethod::Unsupported(95); - pub const JPEG: Self = CompressionMethod::Unsupported(96); - pub const WAVPACK: Self = CompressionMethod::Unsupported(97); - pub const PPMD: Self = CompressionMethod::Unsupported(98); -} -impl CompressionMethod { - /// Converts an u16 to its corresponding CompressionMethod - #[deprecated( - since = "0.5.7", - note = "use a constant to construct a compression method" - )] - pub fn from_u16(val: u16) -> CompressionMethod { - #[allow(deprecated)] - match val { - 0 => CompressionMethod::Stored, - #[cfg(any( - feature = "deflate", - feature = "deflate-miniz", - feature = "deflate-zlib" - ))] - 8 => CompressionMethod::Deflated, - #[cfg(feature = "bzip2")] - 12 => CompressionMethod::Bzip2, - - v => CompressionMethod::Unsupported(v), - } - } - - /// Converts a CompressionMethod to a u16 - #[deprecated( - since = "0.5.7", - note = "to match on other compression methods, use a constant" - )] - pub fn to_u16(self) -> u16 { - #[allow(deprecated)] - match self { - CompressionMethod::Stored => 0, - #[cfg(any( - feature = "deflate", - feature = "deflate-miniz", - feature = "deflate-zlib" - ))] - CompressionMethod::Deflated => 8, - #[cfg(feature = "bzip2")] - CompressionMethod::Bzip2 => 12, - CompressionMethod::Unsupported(v) => v, - } - } -} - -impl fmt::Display for CompressionMethod { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // Just duplicate what the Debug format looks like, i.e, the enum key: - write!(f, "{:?}", self) - } -} - -#[cfg(test)] -mod test { - use super::CompressionMethod; - - #[test] - fn from_eq_to() { - for v in 0..(::std::u16::MAX as u32 + 1) { - #[allow(deprecated)] - let from = CompressionMethod::from_u16(v as u16); - #[allow(deprecated)] - let to = from.to_u16() as u32; - assert_eq!(v, to); - } - } - - fn methods() -> Vec { - let mut methods = Vec::new(); - methods.push(CompressionMethod::Stored); - #[cfg(any( - feature = "deflate", - feature = "deflate-miniz", - feature = "deflate-zlib" - ))] - methods.push(CompressionMethod::Deflated); - #[cfg(feature = "bzip2")] - methods.push(CompressionMethod::Bzip2); - methods - } - - #[test] - fn to_eq_from() { - fn check_match(method: CompressionMethod) { - #[allow(deprecated)] - let to = method.to_u16(); - #[allow(deprecated)] - let from = CompressionMethod::from_u16(to); - #[allow(deprecated)] - let back = from.to_u16(); - assert_eq!(to, back); - } - - for method in methods() { - check_match(method); - } - } - - #[test] - fn to_display_fmt() { - fn check_match(method: CompressionMethod) { - let debug_str = format!("{:?}", method); - let display_str = format!("{}", method); - assert_eq!(debug_str, display_str); - } - - for method in methods() { - check_match(method); - } - } -} diff --git a/third-party/zip/src/cp437.rs b/third-party/zip/src/cp437.rs deleted file mode 100644 index f99481431..000000000 --- a/third-party/zip/src/cp437.rs +++ /dev/null @@ -1,203 +0,0 @@ -//! Convert a string in IBM codepage 437 to UTF-8 - -/// Trait to convert IBM codepage 437 to the target type -pub trait FromCp437 { - /// Target type - type Target; - - /// Function that does the conversion from cp437. - /// Gennerally allocations will be avoided if all data falls into the ASCII range. - fn from_cp437(self) -> Self::Target; -} - -impl<'a> FromCp437 for &'a [u8] { - type Target = ::std::borrow::Cow<'a, str>; - - fn from_cp437(self) -> Self::Target { - if self.iter().all(|c| *c < 0x80) { - ::std::str::from_utf8(self).unwrap().into() - } else { - self.iter().map(|c| to_char(*c)).collect::().into() - } - } -} - -impl FromCp437 for Vec { - type Target = String; - - fn from_cp437(self) -> Self::Target { - if self.iter().all(|c| *c < 0x80) { - String::from_utf8(self).unwrap() - } else { - self.into_iter().map(to_char).collect() - } - } -} - -fn to_char(input: u8) -> char { - let output = match input { - 0x00..=0x7f => input as u32, - 0x80 => 0x00c7, - 0x81 => 0x00fc, - 0x82 => 0x00e9, - 0x83 => 0x00e2, - 0x84 => 0x00e4, - 0x85 => 0x00e0, - 0x86 => 0x00e5, - 0x87 => 0x00e7, - 0x88 => 0x00ea, - 0x89 => 0x00eb, - 0x8a => 0x00e8, - 0x8b => 0x00ef, - 0x8c => 0x00ee, - 0x8d => 0x00ec, - 0x8e => 0x00c4, - 0x8f => 0x00c5, - 0x90 => 0x00c9, - 0x91 => 0x00e6, - 0x92 => 0x00c6, - 0x93 => 0x00f4, - 0x94 => 0x00f6, - 0x95 => 0x00f2, - 0x96 => 0x00fb, - 0x97 => 0x00f9, - 0x98 => 0x00ff, - 0x99 => 0x00d6, - 0x9a => 0x00dc, - 0x9b => 0x00a2, - 0x9c => 0x00a3, - 0x9d => 0x00a5, - 0x9e => 0x20a7, - 0x9f => 0x0192, - 0xa0 => 0x00e1, - 0xa1 => 0x00ed, - 0xa2 => 0x00f3, - 0xa3 => 0x00fa, - 0xa4 => 0x00f1, - 0xa5 => 0x00d1, - 0xa6 => 0x00aa, - 0xa7 => 0x00ba, - 0xa8 => 0x00bf, - 0xa9 => 0x2310, - 0xaa => 0x00ac, - 0xab => 0x00bd, - 0xac => 0x00bc, - 0xad => 0x00a1, - 0xae => 0x00ab, - 0xaf => 0x00bb, - 0xb0 => 0x2591, - 0xb1 => 0x2592, - 0xb2 => 0x2593, - 0xb3 => 0x2502, - 0xb4 => 0x2524, - 0xb5 => 0x2561, - 0xb6 => 0x2562, - 0xb7 => 0x2556, - 0xb8 => 0x2555, - 0xb9 => 0x2563, - 0xba => 0x2551, - 0xbb => 0x2557, - 0xbc => 0x255d, - 0xbd => 0x255c, - 0xbe => 0x255b, - 0xbf => 0x2510, - 0xc0 => 0x2514, - 0xc1 => 0x2534, - 0xc2 => 0x252c, - 0xc3 => 0x251c, - 0xc4 => 0x2500, - 0xc5 => 0x253c, - 0xc6 => 0x255e, - 0xc7 => 0x255f, - 0xc8 => 0x255a, - 0xc9 => 0x2554, - 0xca => 0x2569, - 0xcb => 0x2566, - 0xcc => 0x2560, - 0xcd => 0x2550, - 0xce => 0x256c, - 0xcf => 0x2567, - 0xd0 => 0x2568, - 0xd1 => 0x2564, - 0xd2 => 0x2565, - 0xd3 => 0x2559, - 0xd4 => 0x2558, - 0xd5 => 0x2552, - 0xd6 => 0x2553, - 0xd7 => 0x256b, - 0xd8 => 0x256a, - 0xd9 => 0x2518, - 0xda => 0x250c, - 0xdb => 0x2588, - 0xdc => 0x2584, - 0xdd => 0x258c, - 0xde => 0x2590, - 0xdf => 0x2580, - 0xe0 => 0x03b1, - 0xe1 => 0x00df, - 0xe2 => 0x0393, - 0xe3 => 0x03c0, - 0xe4 => 0x03a3, - 0xe5 => 0x03c3, - 0xe6 => 0x00b5, - 0xe7 => 0x03c4, - 0xe8 => 0x03a6, - 0xe9 => 0x0398, - 0xea => 0x03a9, - 0xeb => 0x03b4, - 0xec => 0x221e, - 0xed => 0x03c6, - 0xee => 0x03b5, - 0xef => 0x2229, - 0xf0 => 0x2261, - 0xf1 => 0x00b1, - 0xf2 => 0x2265, - 0xf3 => 0x2264, - 0xf4 => 0x2320, - 0xf5 => 0x2321, - 0xf6 => 0x00f7, - 0xf7 => 0x2248, - 0xf8 => 0x00b0, - 0xf9 => 0x2219, - 0xfa => 0x00b7, - 0xfb => 0x221a, - 0xfc => 0x207f, - 0xfd => 0x00b2, - 0xfe => 0x25a0, - 0xff => 0x00a0, - }; - ::std::char::from_u32(output).unwrap() -} - -#[cfg(test)] -mod test { - #[test] - fn to_char_valid() { - for i in 0x00_u32..0x100 { - super::to_char(i as u8); - } - } - - #[test] - fn ascii() { - for i in 0x00..0x80 { - assert_eq!(super::to_char(i), i as char); - } - } - - #[test] - fn example_slice() { - use super::FromCp437; - let data = b"Cura\x87ao"; - assert!(::std::str::from_utf8(data).is_err()); - assert_eq!(data.from_cp437(), "Curaçao"); - } - - #[test] - fn example_vec() { - use super::FromCp437; - let data = vec![0xCC, 0xCD, 0xCD, 0xB9]; - assert!(String::from_utf8(data.clone()).is_err()); - assert_eq!(&data.from_cp437(), "╠══╣"); - } -} diff --git a/third-party/zip/src/crc32.rs b/third-party/zip/src/crc32.rs deleted file mode 100644 index b351aa01b..000000000 --- a/third-party/zip/src/crc32.rs +++ /dev/null @@ -1,93 +0,0 @@ -//! Helper module to compute a CRC32 checksum - -use std::io; -use std::io::prelude::*; - -use crc32fast::Hasher; - -/// Reader that validates the CRC32 when it reaches the EOF. -pub struct Crc32Reader { - inner: R, - hasher: Hasher, - check: u32, -} - -impl Crc32Reader { - /// Get a new Crc32Reader which check the inner reader against checksum. - pub fn new(inner: R, checksum: u32) -> Crc32Reader { - Crc32Reader { - inner, - hasher: Hasher::new(), - check: checksum, - } - } - - fn check_matches(&self) -> bool { - self.check == self.hasher.clone().finalize() - } - - pub fn into_inner(self) -> R { - self.inner - } -} - -impl Read for Crc32Reader { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - let count = match self.inner.read(buf) { - Ok(0) if !buf.is_empty() && !self.check_matches() => { - return Err(io::Error::new(io::ErrorKind::Other, "Invalid checksum")) - } - Ok(n) => n, - Err(e) => return Err(e), - }; - self.hasher.update(&buf[0..count]); - Ok(count) - } -} - -#[cfg(test)] -mod test { - use super::*; - use std::io::Read; - - #[test] - fn test_empty_reader() { - let data: &[u8] = b""; - let mut buf = [0; 1]; - - let mut reader = Crc32Reader::new(data, 0); - assert_eq!(reader.read(&mut buf).unwrap(), 0); - - let mut reader = Crc32Reader::new(data, 1); - assert!(reader - .read(&mut buf) - .unwrap_err() - .to_string() - .contains("Invalid checksum")); - } - - #[test] - fn test_byte_by_byte() { - let data: &[u8] = b"1234"; - let mut buf = [0; 1]; - - let mut reader = Crc32Reader::new(data, 0x9be3e0a3); - assert_eq!(reader.read(&mut buf).unwrap(), 1); - assert_eq!(reader.read(&mut buf).unwrap(), 1); - assert_eq!(reader.read(&mut buf).unwrap(), 1); - assert_eq!(reader.read(&mut buf).unwrap(), 1); - assert_eq!(reader.read(&mut buf).unwrap(), 0); - // Can keep reading 0 bytes after the end - assert_eq!(reader.read(&mut buf).unwrap(), 0); - } - - #[test] - fn test_zero_read() { - let data: &[u8] = b"1234"; - let mut buf = [0; 5]; - - let mut reader = Crc32Reader::new(data, 0x9be3e0a3); - assert_eq!(reader.read(&mut buf[..0]).unwrap(), 0); - assert_eq!(reader.read(&mut buf).unwrap(), 4); - } -} diff --git a/third-party/zip/src/lib.rs b/third-party/zip/src/lib.rs deleted file mode 100644 index 3b39ab4f9..000000000 --- a/third-party/zip/src/lib.rs +++ /dev/null @@ -1,21 +0,0 @@ -//! An ergonomic API for reading and writing ZIP files. -//! -//! The current implementation is based on [PKWARE's APPNOTE.TXT v6.3.9](https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT) -// TODO(#184): Decide on the crate's bias: Do we prioritise permissiveness/correctness/speed/ergonomics? - -#![warn(missing_docs)] - -pub use crate::compression::CompressionMethod; -pub use crate::read::ZipArchive; -pub use crate::types::DateTime; -pub use crate::write::ZipWriter; - -mod compression; -mod cp437; -mod crc32; -pub mod read; -pub mod result; -mod spec; -mod types; -pub mod write; -mod zipcrypto; diff --git a/third-party/zip/src/read.rs b/third-party/zip/src/read.rs deleted file mode 100644 index 3aac00f65..000000000 --- a/third-party/zip/src/read.rs +++ /dev/null @@ -1,1078 +0,0 @@ -//! Types for reading ZIP archives - -use crate::compression::CompressionMethod; -use crate::crc32::Crc32Reader; -use crate::result::{InvalidPassword, ZipError, ZipResult}; -use crate::spec; -use crate::zipcrypto::ZipCryptoReader; -use crate::zipcrypto::ZipCryptoReaderValid; -use std::borrow::Cow; -use std::collections::HashMap; -use std::io::{self, prelude::*}; -use std::path::{Component, Path}; - -use crate::cp437::FromCp437; -use crate::types::{DateTime, System, ZipFileData}; -use byteorder::{LittleEndian, ReadBytesExt}; - -#[cfg(any( - feature = "deflate", - feature = "deflate-miniz", - feature = "deflate-zlib" -))] -use flate2::read::DeflateDecoder; - -#[cfg(feature = "bzip2")] -use bzip2::read::BzDecoder; - -mod ffi { - pub const S_IFDIR: u32 = 0o0040000; - pub const S_IFREG: u32 = 0o0100000; -} - -/// ZIP archive reader -/// -/// ```no_run -/// use std::io::prelude::*; -/// fn list_zip_contents(reader: impl Read + Seek) -> zip::result::ZipResult<()> { -/// let mut zip = zip::ZipArchive::new(reader)?; -/// -/// for i in 0..zip.len() { -/// let mut file = zip.by_index(i)?; -/// println!("Filename: {}", file.name()); -/// std::io::copy(&mut file, &mut std::io::stdout()); -/// } -/// -/// Ok(()) -/// } -/// ``` -#[derive(Clone, Debug)] -pub struct ZipArchive { - reader: R, - files: Vec, - names_map: HashMap, - offset: u64, - comment: Vec, -} - -enum CryptoReader<'a> { - Plaintext(io::Take<&'a mut dyn Read>), - ZipCrypto(ZipCryptoReaderValid>), -} - -impl<'a> Read for CryptoReader<'a> { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - match self { - CryptoReader::Plaintext(r) => r.read(buf), - CryptoReader::ZipCrypto(r) => r.read(buf), - } - } -} - -impl<'a> CryptoReader<'a> { - /// Consumes this decoder, returning the underlying reader. - pub fn into_inner(self) -> io::Take<&'a mut dyn Read> { - match self { - CryptoReader::Plaintext(r) => r, - CryptoReader::ZipCrypto(r) => r.into_inner(), - } - } -} - -enum ZipFileReader<'a> { - NoReader, - Raw(io::Take<&'a mut dyn io::Read>), - Stored(Crc32Reader>), - #[cfg(any( - feature = "deflate", - feature = "deflate-miniz", - feature = "deflate-zlib" - ))] - Deflated(Crc32Reader>>), - #[cfg(feature = "bzip2")] - Bzip2(Crc32Reader>>), -} - -impl<'a> Read for ZipFileReader<'a> { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - match self { - ZipFileReader::NoReader => panic!("ZipFileReader was in an invalid state"), - ZipFileReader::Raw(r) => r.read(buf), - ZipFileReader::Stored(r) => r.read(buf), - #[cfg(any( - feature = "deflate", - feature = "deflate-miniz", - feature = "deflate-zlib" - ))] - ZipFileReader::Deflated(r) => r.read(buf), - #[cfg(feature = "bzip2")] - ZipFileReader::Bzip2(r) => r.read(buf), - } - } -} - -impl<'a> ZipFileReader<'a> { - /// Consumes this decoder, returning the underlying reader. - pub fn into_inner(self) -> io::Take<&'a mut dyn Read> { - match self { - ZipFileReader::NoReader => panic!("ZipFileReader was in an invalid state"), - ZipFileReader::Raw(r) => r, - ZipFileReader::Stored(r) => r.into_inner().into_inner(), - #[cfg(any( - feature = "deflate", - feature = "deflate-miniz", - feature = "deflate-zlib" - ))] - ZipFileReader::Deflated(r) => r.into_inner().into_inner().into_inner(), - #[cfg(feature = "bzip2")] - ZipFileReader::Bzip2(r) => r.into_inner().into_inner().into_inner(), - } - } -} - -/// A struct for reading a zip file -pub struct ZipFile<'a> { - data: Cow<'a, ZipFileData>, - crypto_reader: Option>, - reader: ZipFileReader<'a>, -} - -fn find_content<'a>( - data: &mut ZipFileData, - reader: &'a mut (impl Read + Seek), -) -> ZipResult> { - // Parse local header - reader.seek(io::SeekFrom::Start(data.header_start))?; - let signature = reader.read_u32::()?; - if signature != spec::LOCAL_FILE_HEADER_SIGNATURE { - return Err(ZipError::InvalidArchive("Invalid local file header")); - } - - reader.seek(io::SeekFrom::Current(22))?; - let file_name_length = reader.read_u16::()? as u64; - let extra_field_length = reader.read_u16::()? as u64; - let magic_and_header = 4 + 22 + 2 + 2; - data.data_start = data.header_start + magic_and_header + file_name_length + extra_field_length; - - reader.seek(io::SeekFrom::Start(data.data_start))?; - Ok((reader as &mut dyn Read).take(data.compressed_size)) -} - -fn make_crypto_reader<'a>( - compression_method: crate::compression::CompressionMethod, - crc32: u32, - reader: io::Take<&'a mut dyn io::Read>, - password: Option<&[u8]>, -) -> ZipResult, InvalidPassword>> { - #[allow(deprecated)] - { - if let CompressionMethod::Unsupported(_) = compression_method { - return unsupported_zip_error("Compression method not supported"); - } - } - - let reader = match password { - None => CryptoReader::Plaintext(reader), - Some(password) => match ZipCryptoReader::new(reader, password).validate(crc32)? { - None => return Ok(Err(InvalidPassword)), - Some(r) => CryptoReader::ZipCrypto(r), - }, - }; - Ok(Ok(reader)) -} - -fn make_reader<'a>( - compression_method: CompressionMethod, - crc32: u32, - reader: CryptoReader<'a>, -) -> ZipFileReader<'a> { - match compression_method { - CompressionMethod::Stored => ZipFileReader::Stored(Crc32Reader::new(reader, crc32)), - #[cfg(any( - feature = "deflate", - feature = "deflate-miniz", - feature = "deflate-zlib" - ))] - CompressionMethod::Deflated => { - let deflate_reader = DeflateDecoder::new(reader); - ZipFileReader::Deflated(Crc32Reader::new(deflate_reader, crc32)) - } - #[cfg(feature = "bzip2")] - CompressionMethod::Bzip2 => { - let bzip2_reader = BzDecoder::new(reader); - ZipFileReader::Bzip2(Crc32Reader::new(bzip2_reader, crc32)) - } - _ => panic!("Compression method not supported"), - } -} - -impl ZipArchive { - /// Get the directory start offset and number of files. This is done in a - /// separate function to ease the control flow design. - fn get_directory_counts( - reader: &mut R, - footer: &spec::CentralDirectoryEnd, - cde_start_pos: u64, - ) -> ZipResult<(u64, u64, usize)> { - // See if there's a ZIP64 footer. The ZIP64 locator if present will - // have its signature 20 bytes in front of the standard footer. The - // standard footer, in turn, is 22+N bytes large, where N is the - // comment length. Therefore: - let zip64locator = if reader - .seek(io::SeekFrom::End( - -(20 + 22 + footer.zip_file_comment.len() as i64), - )) - .is_ok() - { - match spec::Zip64CentralDirectoryEndLocator::parse(reader) { - Ok(loc) => Some(loc), - Err(ZipError::InvalidArchive(_)) => { - // No ZIP64 header; that's actually fine. We're done here. - None - } - Err(e) => { - // Yikes, a real problem - return Err(e); - } - } - } else { - // Empty Zip files will have nothing else so this error might be fine. If - // not, we'll find out soon. - None - }; - - match zip64locator { - None => { - // Some zip files have data prepended to them, resulting in the - // offsets all being too small. Get the amount of error by comparing - // the actual file position we found the CDE at with the offset - // recorded in the CDE. - let archive_offset = cde_start_pos - .checked_sub(footer.central_directory_size as u64) - .and_then(|x| x.checked_sub(footer.central_directory_offset as u64)) - .ok_or(ZipError::InvalidArchive( - "Invalid central directory size or offset", - ))?; - - let directory_start = footer.central_directory_offset as u64 + archive_offset; - let number_of_files = footer.number_of_files_on_this_disk as usize; - Ok((archive_offset, directory_start, number_of_files)) - } - Some(locator64) => { - // If we got here, this is indeed a ZIP64 file. - - if footer.disk_number as u32 != locator64.disk_with_central_directory { - return unsupported_zip_error( - "Support for multi-disk files is not implemented", - ); - } - - // We need to reassess `archive_offset`. We know where the ZIP64 - // central-directory-end structure *should* be, but unfortunately we - // don't know how to precisely relate that location to our current - // actual offset in the file, since there may be junk at its - // beginning. Therefore we need to perform another search, as in - // read::CentralDirectoryEnd::find_and_parse, except now we search - // forward. - - let search_upper_bound = cde_start_pos - .checked_sub(60) // minimum size of Zip64CentralDirectoryEnd + Zip64CentralDirectoryEndLocator - .ok_or(ZipError::InvalidArchive( - "File cannot contain ZIP64 central directory end", - ))?; - let (footer, archive_offset) = spec::Zip64CentralDirectoryEnd::find_and_parse( - reader, - locator64.end_of_central_directory_offset, - search_upper_bound, - )?; - - if footer.disk_number != footer.disk_with_central_directory { - return unsupported_zip_error( - "Support for multi-disk files is not implemented", - ); - } - - let directory_start = footer - .central_directory_offset - .checked_add(archive_offset) - .ok_or_else(|| { - ZipError::InvalidArchive("Invalid central directory size or offset") - })?; - - Ok(( - archive_offset, - directory_start, - footer.number_of_files as usize, - )) - } - } - } - - /// Read a ZIP archive, collecting the files it contains - /// - /// This uses the central directory record of the ZIP file, and ignores local file headers - pub fn new(mut reader: R) -> ZipResult> { - let (footer, cde_start_pos) = spec::CentralDirectoryEnd::find_and_parse(&mut reader)?; - - if footer.disk_number != footer.disk_with_central_directory { - return unsupported_zip_error("Support for multi-disk files is not implemented"); - } - - let (archive_offset, directory_start, number_of_files) = - Self::get_directory_counts(&mut reader, &footer, cde_start_pos)?; - - let mut files = Vec::new(); - let mut names_map = HashMap::new(); - - if let Err(_) = reader.seek(io::SeekFrom::Start(directory_start)) { - return Err(ZipError::InvalidArchive( - "Could not seek to start of central directory", - )); - } - - for _ in 0..number_of_files { - let file = central_header_to_zip_file(&mut reader, archive_offset)?; - names_map.insert(file.file_name.clone(), files.len()); - files.push(file); - } - - Ok(ZipArchive { - reader, - files, - names_map, - offset: archive_offset, - comment: footer.zip_file_comment, - }) - } - /// Extract a Zip archive into a directory, overwriting files if they - /// already exist. Paths are sanitized with [`ZipFile::enclosed_name`]. - /// - /// Extraction is not atomic; If an error is encountered, some of the files - /// may be left on disk. - pub fn extract>(&mut self, directory: P) -> ZipResult<()> { - use std::fs; - - for i in 0..self.len() { - let mut file = self.by_index(i)?; - let filepath = file - .enclosed_name() - .ok_or(ZipError::InvalidArchive("Invalid file path"))?; - - let outpath = directory.as_ref().join(filepath); - - if file.name().ends_with('/') { - fs::create_dir_all(&outpath)?; - } else { - if let Some(p) = outpath.parent() { - if !p.exists() { - fs::create_dir_all(&p)?; - } - } - let mut outfile = fs::File::create(&outpath)?; - io::copy(&mut file, &mut outfile)?; - } - // Get and Set permissions - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - if let Some(mode) = file.unix_mode() { - fs::set_permissions(&outpath, fs::Permissions::from_mode(mode))?; - } - } - } - Ok(()) - } - - /// Number of files contained in this zip. - pub fn len(&self) -> usize { - self.files.len() - } - - /// Whether this zip archive contains no files - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - /// Get the offset from the beginning of the underlying reader that this zip begins at, in bytes. - /// - /// Normally this value is zero, but if the zip has arbitrary data prepended to it, then this value will be the size - /// of that prepended data. - pub fn offset(&self) -> u64 { - self.offset - } - - /// Get the comment of the zip archive. - pub fn comment(&self) -> &[u8] { - &self.comment - } - - /// Returns an iterator over all the file and directory names in this archive. - pub fn file_names(&self) -> impl Iterator { - self.names_map.keys().map(|s| s.as_str()) - } - - /// Search for a file entry by name, decrypt with given password - pub fn by_name_decrypt<'a>( - &'a mut self, - name: &str, - password: &[u8], - ) -> ZipResult, InvalidPassword>> { - self.by_name_with_optional_password(name, Some(password)) - } - - /// Search for a file entry by name - pub fn by_name<'a>(&'a mut self, name: &str) -> ZipResult> { - Ok(self.by_name_with_optional_password(name, None)?.unwrap()) - } - - fn by_name_with_optional_password<'a>( - &'a mut self, - name: &str, - password: Option<&[u8]>, - ) -> ZipResult, InvalidPassword>> { - let index = match self.names_map.get(name) { - Some(index) => *index, - None => { - return Err(ZipError::FileNotFound); - } - }; - self.by_index_with_optional_password(index, password) - } - - /// Get a contained file by index, decrypt with given password - pub fn by_index_decrypt<'a>( - &'a mut self, - file_number: usize, - password: &[u8], - ) -> ZipResult, InvalidPassword>> { - self.by_index_with_optional_password(file_number, Some(password)) - } - - /// Get a contained file by index - pub fn by_index<'a>(&'a mut self, file_number: usize) -> ZipResult> { - Ok(self - .by_index_with_optional_password(file_number, None)? - .unwrap()) - } - - /// Get a contained file by index without decompressing it - pub fn by_index_raw<'a>(&'a mut self, file_number: usize) -> ZipResult> { - let reader = &mut self.reader; - self.files - .get_mut(file_number) - .ok_or(ZipError::FileNotFound) - .and_then(move |data| { - Ok(ZipFile { - crypto_reader: None, - reader: ZipFileReader::Raw(find_content(data, reader)?), - data: Cow::Borrowed(data), - }) - }) - } - - fn by_index_with_optional_password<'a>( - &'a mut self, - file_number: usize, - mut password: Option<&[u8]>, - ) -> ZipResult, InvalidPassword>> { - if file_number >= self.files.len() { - return Err(ZipError::FileNotFound); - } - let data = &mut self.files[file_number]; - - match (password, data.encrypted) { - (None, true) => { - return Err(ZipError::UnsupportedArchive( - "Password required to decrypt file", - )) - } - (Some(_), false) => password = None, //Password supplied, but none needed! Discard. - _ => {} - } - let limit_reader = find_content(data, &mut self.reader)?; - - match make_crypto_reader(data.compression_method, data.crc32, limit_reader, password) { - Ok(Ok(crypto_reader)) => Ok(Ok(ZipFile { - crypto_reader: Some(crypto_reader), - reader: ZipFileReader::NoReader, - data: Cow::Borrowed(data), - })), - Err(e) => Err(e), - Ok(Err(e)) => Ok(Err(e)), - } - } - - /// Unwrap and return the inner reader object - /// - /// The position of the reader is undefined. - pub fn into_inner(self) -> R { - self.reader - } -} - -fn unsupported_zip_error(detail: &'static str) -> ZipResult { - Err(ZipError::UnsupportedArchive(detail)) -} - -fn central_header_to_zip_file( - reader: &mut R, - archive_offset: u64, -) -> ZipResult { - let central_header_start = reader.seek(io::SeekFrom::Current(0))?; - // Parse central header - let signature = reader.read_u32::()?; - if signature != spec::CENTRAL_DIRECTORY_HEADER_SIGNATURE { - return Err(ZipError::InvalidArchive("Invalid Central Directory header")); - } - - let version_made_by = reader.read_u16::()?; - let _version_to_extract = reader.read_u16::()?; - let flags = reader.read_u16::()?; - let encrypted = flags & 1 == 1; - let is_utf8 = flags & (1 << 11) != 0; - let compression_method = reader.read_u16::()?; - let last_mod_time = reader.read_u16::()?; - let last_mod_date = reader.read_u16::()?; - let crc32 = reader.read_u32::()?; - let compressed_size = reader.read_u32::()?; - let uncompressed_size = reader.read_u32::()?; - let file_name_length = reader.read_u16::()? as usize; - let extra_field_length = reader.read_u16::()? as usize; - let file_comment_length = reader.read_u16::()? as usize; - let _disk_number = reader.read_u16::()?; - let _internal_file_attributes = reader.read_u16::()?; - let external_file_attributes = reader.read_u32::()?; - let offset = reader.read_u32::()? as u64; - let mut file_name_raw = vec![0; file_name_length]; - reader.read_exact(&mut file_name_raw)?; - let mut extra_field = vec![0; extra_field_length]; - reader.read_exact(&mut extra_field)?; - let mut file_comment_raw = vec![0; file_comment_length]; - reader.read_exact(&mut file_comment_raw)?; - - let file_name = match is_utf8 { - true => String::from_utf8_lossy(&*file_name_raw).into_owned(), - false => file_name_raw.clone().from_cp437(), - }; - let file_comment = match is_utf8 { - true => String::from_utf8_lossy(&*file_comment_raw).into_owned(), - false => file_comment_raw.from_cp437(), - }; - - // Construct the result - let mut result = ZipFileData { - system: System::from_u8((version_made_by >> 8) as u8), - version_made_by: version_made_by as u8, - encrypted, - compression_method: { - #[allow(deprecated)] - CompressionMethod::from_u16(compression_method) - }, - last_modified_time: DateTime::from_msdos(last_mod_date, last_mod_time), - crc32, - compressed_size: compressed_size as u64, - uncompressed_size: uncompressed_size as u64, - file_name, - file_name_raw, - file_comment, - header_start: offset, - central_header_start, - data_start: 0, - external_attributes: external_file_attributes, - }; - - match parse_extra_field(&mut result, &*extra_field) { - Ok(..) | Err(ZipError::Io(..)) => {} - Err(e) => return Err(e), - } - - // Account for shifted zip offsets. - result.header_start += archive_offset; - - Ok(result) -} - -fn parse_extra_field(file: &mut ZipFileData, data: &[u8]) -> ZipResult<()> { - let mut reader = io::Cursor::new(data); - - while (reader.position() as usize) < data.len() { - let kind = reader.read_u16::()?; - let len = reader.read_u16::()?; - let mut len_left = len as i64; - // Zip64 extended information extra field - if kind == 0x0001 { - if file.uncompressed_size == 0xFFFFFFFF { - file.uncompressed_size = reader.read_u64::()?; - len_left -= 8; - } - if file.compressed_size == 0xFFFFFFFF { - file.compressed_size = reader.read_u64::()?; - len_left -= 8; - } - if file.header_start == 0xFFFFFFFF { - file.header_start = reader.read_u64::()?; - len_left -= 8; - } - // Unparsed fields: - // u32: disk start number - } - - // We could also check for < 0 to check for errors - if len_left > 0 { - reader.seek(io::SeekFrom::Current(len_left))?; - } - } - Ok(()) -} - -/// Methods for retrieving information on zip files -impl<'a> ZipFile<'a> { - fn get_reader(&mut self) -> &mut ZipFileReader<'a> { - if let ZipFileReader::NoReader = self.reader { - let data = &self.data; - let crypto_reader = self.crypto_reader.take().expect("Invalid reader state"); - self.reader = make_reader(data.compression_method, data.crc32, crypto_reader) - } - &mut self.reader - } - - pub(crate) fn get_raw_reader(&mut self) -> &mut dyn Read { - if let ZipFileReader::NoReader = self.reader { - let crypto_reader = self.crypto_reader.take().expect("Invalid reader state"); - self.reader = ZipFileReader::Raw(crypto_reader.into_inner()) - } - &mut self.reader - } - - /// Get the version of the file - pub fn version_made_by(&self) -> (u8, u8) { - ( - self.data.version_made_by / 10, - self.data.version_made_by % 10, - ) - } - - /// Get the name of the file - /// - /// # Warnings - /// - /// It is dangerous to use this name directly when extracting an archive. - /// It may contain an absolute path (`/etc/shadow`), or break out of the - /// current directory (`../runtime`). Carelessly writing to these paths - /// allows an attacker to craft a ZIP archive that will overwrite critical - /// files. - /// - /// You can use the [`ZipFile::enclosed_name`] method to validate the name - /// as a safe path. - pub fn name(&self) -> &str { - &self.data.file_name - } - - /// Get the name of the file, in the raw (internal) byte representation. - /// - /// The encoding of this data is currently undefined. - pub fn name_raw(&self) -> &[u8] { - &self.data.file_name_raw - } - - /// Get the name of the file in a sanitized form. It truncates the name to the first NULL byte, - /// removes a leading '/' and removes '..' parts. - #[deprecated( - since = "0.5.7", - note = "by stripping `..`s from the path, the meaning of paths can change. - `mangled_name` can be used if this behaviour is desirable" - )] - pub fn sanitized_name(&self) -> ::std::path::PathBuf { - self.mangled_name() - } - - /// Rewrite the path, ignoring any path components with special meaning. - /// - /// - Absolute paths are made relative - /// - [`ParentDir`]s are ignored - /// - Truncates the filename at a NULL byte - /// - /// This is appropriate if you need to be able to extract *something* from - /// any archive, but will easily misrepresent trivial paths like - /// `foo/../bar` as `foo/bar` (instead of `bar`). Because of this, - /// [`ZipFile::enclosed_name`] is the better option in most scenarios. - /// - /// [`ParentDir`]: `Component::ParentDir` - pub fn mangled_name(&self) -> ::std::path::PathBuf { - self.data.file_name_sanitized() - } - - /// Ensure the file path is safe to use as a [`Path`]. - /// - /// - It can't contain NULL bytes - /// - It can't resolve to a path outside the current directory - /// > `foo/../bar` is fine, `foo/../../bar` is not. - /// - It can't be an absolute path - /// - /// This will read well-formed ZIP files correctly, and is resistant - /// to path-based exploits. It is recommended over - /// [`ZipFile::mangled_name`]. - pub fn enclosed_name(&self) -> Option<&Path> { - if self.data.file_name.contains('\0') { - return None; - } - let path = Path::new(&self.data.file_name); - let mut depth = 0usize; - for component in path.components() { - match component { - Component::Prefix(_) | Component::RootDir => return None, - Component::ParentDir => depth = depth.checked_sub(1)?, - Component::Normal(_) => depth += 1, - Component::CurDir => (), - } - } - Some(path) - } - - /// Get the comment of the file - pub fn comment(&self) -> &str { - &self.data.file_comment - } - - /// Get the compression method used to store the file - pub fn compression(&self) -> CompressionMethod { - self.data.compression_method - } - - /// Get the size of the file in the archive - pub fn compressed_size(&self) -> u64 { - self.data.compressed_size - } - - /// Get the size of the file when uncompressed - pub fn size(&self) -> u64 { - self.data.uncompressed_size - } - - /// Get the time the file was last modified - pub fn last_modified(&self) -> DateTime { - self.data.last_modified_time - } - /// Returns whether the file is actually a directory - pub fn is_dir(&self) -> bool { - self.name() - .chars() - .rev() - .next() - .map_or(false, |c| c == '/' || c == '\\') - } - - /// Returns whether the file is a regular file - pub fn is_file(&self) -> bool { - !self.is_dir() - } - - /// Get unix mode for the file - pub fn unix_mode(&self) -> Option { - if self.data.external_attributes == 0 { - return None; - } - - match self.data.system { - System::Unix => Some(self.data.external_attributes >> 16), - System::Dos => { - // Interpret MSDOS directory bit - let mut mode = if 0x10 == (self.data.external_attributes & 0x10) { - ffi::S_IFDIR | 0o0775 - } else { - ffi::S_IFREG | 0o0664 - }; - if 0x01 == (self.data.external_attributes & 0x01) { - // Read-only bit; strip write permissions - mode &= 0o0555; - } - Some(mode) - } - _ => None, - } - } - - /// Get the CRC32 hash of the original file - pub fn crc32(&self) -> u32 { - self.data.crc32 - } - - /// Get the starting offset of the data of the compressed file - pub fn data_start(&self) -> u64 { - self.data.data_start - } - - /// Get the starting offset of the zip header for this file - pub fn header_start(&self) -> u64 { - self.data.header_start - } - /// Get the starting offset of the zip header in the central directory for this file - pub fn central_header_start(&self) -> u64 { - self.data.central_header_start - } -} - -impl<'a> Read for ZipFile<'a> { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - self.get_reader().read(buf) - } -} - -impl<'a> Drop for ZipFile<'a> { - fn drop(&mut self) { - // self.data is Owned, this reader is constructed by a streaming reader. - // In this case, we want to exhaust the reader so that the next file is accessible. - if let Cow::Owned(_) = self.data { - let mut buffer = [0; 1 << 16]; - - // Get the inner `Take` reader so all decryption, decompression and CRC calculation is skipped. - let mut reader: std::io::Take<&mut dyn std::io::Read> = match &mut self.reader { - ZipFileReader::NoReader => { - let innerreader = ::std::mem::replace(&mut self.crypto_reader, None); - innerreader.expect("Invalid reader state").into_inner() - } - reader => { - let innerreader = ::std::mem::replace(reader, ZipFileReader::NoReader); - innerreader.into_inner() - } - }; - - loop { - match reader.read(&mut buffer) { - Ok(0) => break, - Ok(_) => (), - Err(e) => panic!( - "Could not consume all of the output of the current ZipFile: {:?}", - e - ), - } - } - } - } -} - -/// Read ZipFile structures from a non-seekable reader. -/// -/// This is an alternative method to read a zip file. If possible, use the ZipArchive functions -/// as some information will be missing when reading this manner. -/// -/// Reads a file header from the start of the stream. Will return `Ok(Some(..))` if a file is -/// present at the start of the stream. Returns `Ok(None)` if the start of the central directory -/// is encountered. No more files should be read after this. -/// -/// The Drop implementation of ZipFile ensures that the reader will be correctly positioned after -/// the structure is done. -/// -/// Missing fields are: -/// * `comment`: set to an empty string -/// * `data_start`: set to 0 -/// * `external_attributes`: `unix_mode()`: will return None -pub fn read_zipfile_from_stream<'a, R: io::Read>( - reader: &'a mut R, -) -> ZipResult>> { - let signature = reader.read_u32::()?; - - match signature { - spec::LOCAL_FILE_HEADER_SIGNATURE => (), - spec::CENTRAL_DIRECTORY_HEADER_SIGNATURE => return Ok(None), - _ => return Err(ZipError::InvalidArchive("Invalid local file header")), - } - - let version_made_by = reader.read_u16::()?; - let flags = reader.read_u16::()?; - let encrypted = flags & 1 == 1; - let is_utf8 = flags & (1 << 11) != 0; - let using_data_descriptor = flags & (1 << 3) != 0; - #[allow(deprecated)] - let compression_method = CompressionMethod::from_u16(reader.read_u16::()?); - let last_mod_time = reader.read_u16::()?; - let last_mod_date = reader.read_u16::()?; - let crc32 = reader.read_u32::()?; - let compressed_size = reader.read_u32::()?; - let uncompressed_size = reader.read_u32::()?; - let file_name_length = reader.read_u16::()? as usize; - let extra_field_length = reader.read_u16::()? as usize; - - let mut file_name_raw = vec![0; file_name_length]; - reader.read_exact(&mut file_name_raw)?; - let mut extra_field = vec![0; extra_field_length]; - reader.read_exact(&mut extra_field)?; - - let file_name = match is_utf8 { - true => String::from_utf8_lossy(&*file_name_raw).into_owned(), - false => file_name_raw.clone().from_cp437(), - }; - - let mut result = ZipFileData { - system: System::from_u8((version_made_by >> 8) as u8), - version_made_by: version_made_by as u8, - encrypted, - compression_method, - last_modified_time: DateTime::from_msdos(last_mod_date, last_mod_time), - crc32, - compressed_size: compressed_size as u64, - uncompressed_size: uncompressed_size as u64, - file_name, - file_name_raw, - file_comment: String::new(), // file comment is only available in the central directory - // header_start and data start are not available, but also don't matter, since seeking is - // not available. - header_start: 0, - data_start: 0, - central_header_start: 0, - // The external_attributes field is only available in the central directory. - // We set this to zero, which should be valid as the docs state 'If input came - // from standard input, this field is set to zero.' - external_attributes: 0, - }; - - match parse_extra_field(&mut result, &extra_field) { - Ok(..) | Err(ZipError::Io(..)) => {} - Err(e) => return Err(e), - } - - if encrypted { - return unsupported_zip_error("Encrypted files are not supported"); - } - if using_data_descriptor { - return unsupported_zip_error("The file length is not available in the local header"); - } - - let limit_reader = (reader as &'a mut dyn io::Read).take(result.compressed_size as u64); - - let result_crc32 = result.crc32; - let result_compression_method = result.compression_method; - let crypto_reader = - make_crypto_reader(result_compression_method, result_crc32, limit_reader, None)?.unwrap(); - - Ok(Some(ZipFile { - data: Cow::Owned(result), - crypto_reader: None, - reader: make_reader(result_compression_method, result_crc32, crypto_reader), - })) -} - -#[cfg(test)] -mod test { - #[test] - fn invalid_offset() { - use super::ZipArchive; - use std::io; - - let mut v = Vec::new(); - v.extend_from_slice(include_bytes!("../tests/data/invalid_offset.zip")); - let reader = ZipArchive::new(io::Cursor::new(v)); - assert!(reader.is_err()); - } - - #[test] - fn invalid_offset2() { - use super::ZipArchive; - use std::io; - - let mut v = Vec::new(); - v.extend_from_slice(include_bytes!("../tests/data/invalid_offset2.zip")); - let reader = ZipArchive::new(io::Cursor::new(v)); - assert!(reader.is_err()); - } - - #[test] - fn zip64_with_leading_junk() { - use super::ZipArchive; - use std::io; - - let mut v = Vec::new(); - v.extend_from_slice(include_bytes!("../tests/data/zip64_demo.zip")); - let reader = ZipArchive::new(io::Cursor::new(v)).unwrap(); - assert!(reader.len() == 1); - } - - #[test] - fn zip_contents() { - use super::ZipArchive; - use std::io; - - let mut v = Vec::new(); - v.extend_from_slice(include_bytes!("../tests/data/mimetype.zip")); - let mut reader = ZipArchive::new(io::Cursor::new(v)).unwrap(); - assert!(reader.comment() == b""); - assert_eq!(reader.by_index(0).unwrap().central_header_start(), 77); - } - - #[test] - fn zip_read_streaming() { - use super::read_zipfile_from_stream; - use std::io; - - let mut v = Vec::new(); - v.extend_from_slice(include_bytes!("../tests/data/mimetype.zip")); - let mut reader = io::Cursor::new(v); - loop { - match read_zipfile_from_stream(&mut reader).unwrap() { - None => break, - _ => (), - } - } - } - - #[test] - fn zip_clone() { - use super::ZipArchive; - use std::io::{self, Read}; - - let mut v = Vec::new(); - v.extend_from_slice(include_bytes!("../tests/data/mimetype.zip")); - let mut reader1 = ZipArchive::new(io::Cursor::new(v)).unwrap(); - let mut reader2 = reader1.clone(); - - let mut file1 = reader1.by_index(0).unwrap(); - let mut file2 = reader2.by_index(0).unwrap(); - - let t = file1.last_modified(); - assert_eq!( - ( - t.year(), - t.month(), - t.day(), - t.hour(), - t.minute(), - t.second() - ), - (1980, 1, 1, 0, 0, 0) - ); - - let mut buf1 = [0; 5]; - let mut buf2 = [0; 5]; - let mut buf3 = [0; 5]; - let mut buf4 = [0; 5]; - - file1.read(&mut buf1).unwrap(); - file2.read(&mut buf2).unwrap(); - file1.read(&mut buf3).unwrap(); - file2.read(&mut buf4).unwrap(); - - assert_eq!(buf1, buf2); - assert_eq!(buf3, buf4); - assert!(buf1 != buf3); - } - - #[test] - fn file_and_dir_predicates() { - use super::ZipArchive; - use std::io; - - let mut v = Vec::new(); - v.extend_from_slice(include_bytes!("../tests/data/files_and_dirs.zip")); - let mut zip = ZipArchive::new(io::Cursor::new(v)).unwrap(); - - for i in 0..zip.len() { - let zip_file = zip.by_index(i).unwrap(); - let full_name = zip_file.enclosed_name().unwrap(); - let file_name = full_name.file_name().unwrap().to_str().unwrap(); - assert!( - (file_name.starts_with("dir") && zip_file.is_dir()) - || (file_name.starts_with("file") && zip_file.is_file()) - ); - } - } -} diff --git a/third-party/zip/src/result.rs b/third-party/zip/src/result.rs deleted file mode 100644 index e8b7d0521..000000000 --- a/third-party/zip/src/result.rs +++ /dev/null @@ -1,39 +0,0 @@ -//! Error types that can be emitted from this library - -use std::io; - -use thiserror::Error; - -/// Generic result type with ZipError as its error variant -pub type ZipResult = Result; - -/// The given password is wrong -#[derive(Error, Debug)] -#[error("invalid password for file in archive")] -pub struct InvalidPassword; - -/// Error type for Zip -#[derive(Debug, Error)] -pub enum ZipError { - /// An Error caused by I/O - #[error(transparent)] - Io(#[from] io::Error), - - /// This file is probably not a zip archive - #[error("invalid Zip archive")] - InvalidArchive(&'static str), - - /// This archive is not supported - #[error("unsupported Zip archive")] - UnsupportedArchive(&'static str), - - /// The requested file could not be found in the archive - #[error("specified file not found in archive")] - FileNotFound, -} - -impl From for io::Error { - fn from(err: ZipError) -> io::Error { - io::Error::new(io::ErrorKind::Other, err) - } -} diff --git a/third-party/zip/src/spec.rs b/third-party/zip/src/spec.rs deleted file mode 100644 index 91966b67f..000000000 --- a/third-party/zip/src/spec.rs +++ /dev/null @@ -1,182 +0,0 @@ -use crate::result::{ZipError, ZipResult}; -use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; -use std::io; -use std::io::prelude::*; - -pub const LOCAL_FILE_HEADER_SIGNATURE: u32 = 0x04034b50; -pub const CENTRAL_DIRECTORY_HEADER_SIGNATURE: u32 = 0x02014b50; -const CENTRAL_DIRECTORY_END_SIGNATURE: u32 = 0x06054b50; -pub const ZIP64_CENTRAL_DIRECTORY_END_SIGNATURE: u32 = 0x06064b50; -const ZIP64_CENTRAL_DIRECTORY_END_LOCATOR_SIGNATURE: u32 = 0x07064b50; - -pub struct CentralDirectoryEnd { - pub disk_number: u16, - pub disk_with_central_directory: u16, - pub number_of_files_on_this_disk: u16, - pub number_of_files: u16, - pub central_directory_size: u32, - pub central_directory_offset: u32, - pub zip_file_comment: Vec, -} - -impl CentralDirectoryEnd { - pub fn parse(reader: &mut T) -> ZipResult { - let magic = reader.read_u32::()?; - if magic != CENTRAL_DIRECTORY_END_SIGNATURE { - return Err(ZipError::InvalidArchive("Invalid digital signature header")); - } - let disk_number = reader.read_u16::()?; - let disk_with_central_directory = reader.read_u16::()?; - let number_of_files_on_this_disk = reader.read_u16::()?; - let number_of_files = reader.read_u16::()?; - let central_directory_size = reader.read_u32::()?; - let central_directory_offset = reader.read_u32::()?; - let zip_file_comment_length = reader.read_u16::()? as usize; - let mut zip_file_comment = vec![0; zip_file_comment_length]; - reader.read_exact(&mut zip_file_comment)?; - - Ok(CentralDirectoryEnd { - disk_number, - disk_with_central_directory, - number_of_files_on_this_disk, - number_of_files, - central_directory_size, - central_directory_offset, - zip_file_comment, - }) - } - - pub fn find_and_parse( - reader: &mut T, - ) -> ZipResult<(CentralDirectoryEnd, u64)> { - const HEADER_SIZE: u64 = 22; - const BYTES_BETWEEN_MAGIC_AND_COMMENT_SIZE: u64 = HEADER_SIZE - 6; - let file_length = reader.seek(io::SeekFrom::End(0))?; - - let search_upper_bound = file_length.saturating_sub(HEADER_SIZE + ::std::u16::MAX as u64); - - if file_length < HEADER_SIZE { - return Err(ZipError::InvalidArchive("Invalid zip header")); - } - - let mut pos = file_length - HEADER_SIZE; - while pos >= search_upper_bound { - reader.seek(io::SeekFrom::Start(pos as u64))?; - if reader.read_u32::()? == CENTRAL_DIRECTORY_END_SIGNATURE { - reader.seek(io::SeekFrom::Current( - BYTES_BETWEEN_MAGIC_AND_COMMENT_SIZE as i64, - ))?; - let cde_start_pos = reader.seek(io::SeekFrom::Start(pos as u64))?; - return CentralDirectoryEnd::parse(reader).map(|cde| (cde, cde_start_pos)); - } - pos = match pos.checked_sub(1) { - Some(p) => p, - None => break, - }; - } - Err(ZipError::InvalidArchive( - "Could not find central directory end", - )) - } - - pub fn write(&self, writer: &mut T) -> ZipResult<()> { - writer.write_u32::(CENTRAL_DIRECTORY_END_SIGNATURE)?; - writer.write_u16::(self.disk_number)?; - writer.write_u16::(self.disk_with_central_directory)?; - writer.write_u16::(self.number_of_files_on_this_disk)?; - writer.write_u16::(self.number_of_files)?; - writer.write_u32::(self.central_directory_size)?; - writer.write_u32::(self.central_directory_offset)?; - writer.write_u16::(self.zip_file_comment.len() as u16)?; - writer.write_all(&self.zip_file_comment)?; - Ok(()) - } -} - -pub struct Zip64CentralDirectoryEndLocator { - pub disk_with_central_directory: u32, - pub end_of_central_directory_offset: u64, - pub number_of_disks: u32, -} - -impl Zip64CentralDirectoryEndLocator { - pub fn parse(reader: &mut T) -> ZipResult { - let magic = reader.read_u32::()?; - if magic != ZIP64_CENTRAL_DIRECTORY_END_LOCATOR_SIGNATURE { - return Err(ZipError::InvalidArchive( - "Invalid zip64 locator digital signature header", - )); - } - let disk_with_central_directory = reader.read_u32::()?; - let end_of_central_directory_offset = reader.read_u64::()?; - let number_of_disks = reader.read_u32::()?; - - Ok(Zip64CentralDirectoryEndLocator { - disk_with_central_directory, - end_of_central_directory_offset, - number_of_disks, - }) - } -} - -pub struct Zip64CentralDirectoryEnd { - pub version_made_by: u16, - pub version_needed_to_extract: u16, - pub disk_number: u32, - pub disk_with_central_directory: u32, - pub number_of_files_on_this_disk: u64, - pub number_of_files: u64, - pub central_directory_size: u64, - pub central_directory_offset: u64, - //pub extensible_data_sector: Vec, <-- We don't do anything with this at the moment. -} - -impl Zip64CentralDirectoryEnd { - pub fn find_and_parse( - reader: &mut T, - nominal_offset: u64, - search_upper_bound: u64, - ) -> ZipResult<(Zip64CentralDirectoryEnd, u64)> { - let mut pos = nominal_offset; - - while pos <= search_upper_bound { - reader.seek(io::SeekFrom::Start(pos))?; - - if reader.read_u32::()? == ZIP64_CENTRAL_DIRECTORY_END_SIGNATURE { - let archive_offset = pos - nominal_offset; - - let _record_size = reader.read_u64::()?; - // We would use this value if we did anything with the "zip64 extensible data sector". - - let version_made_by = reader.read_u16::()?; - let version_needed_to_extract = reader.read_u16::()?; - let disk_number = reader.read_u32::()?; - let disk_with_central_directory = reader.read_u32::()?; - let number_of_files_on_this_disk = reader.read_u64::()?; - let number_of_files = reader.read_u64::()?; - let central_directory_size = reader.read_u64::()?; - let central_directory_offset = reader.read_u64::()?; - - return Ok(( - Zip64CentralDirectoryEnd { - version_made_by, - version_needed_to_extract, - disk_number, - disk_with_central_directory, - number_of_files_on_this_disk, - number_of_files, - central_directory_size, - central_directory_offset, - }, - archive_offset, - )); - } - - pos += 1; - } - - Err(ZipError::InvalidArchive( - "Could not find ZIP64 central directory end", - )) - } -} diff --git a/third-party/zip/src/types.rs b/third-party/zip/src/types.rs deleted file mode 100644 index 154e3c23e..000000000 --- a/third-party/zip/src/types.rs +++ /dev/null @@ -1,474 +0,0 @@ -//! Types that specify what is contained in a ZIP. - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum System { - Dos = 0, - Unix = 3, - Unknown, -} - -impl System { - pub fn from_u8(system: u8) -> System { - use self::System::*; - - match system { - 0 => Dos, - 3 => Unix, - _ => Unknown, - } - } -} - -/// A DateTime field to be used for storing timestamps in a zip file -/// -/// This structure does bounds checking to ensure the date is able to be stored in a zip file. -/// -/// When constructed manually from a date and time, it will also check if the input is sensible -/// (e.g. months are from [1, 12]), but when read from a zip some parts may be out of their normal -/// bounds (e.g. month 0, or hour 31). -/// -/// # Warning -/// -/// Some utilities use alternative timestamps to improve the accuracy of their -/// ZIPs, but we don't parse them yet. [We're working on this](https://github.com/zip-rs/zip/issues/156#issuecomment-652981904), -/// however this API shouldn't be considered complete. -#[derive(Debug, Clone, Copy)] -pub struct DateTime { - year: u16, - month: u8, - day: u8, - hour: u8, - minute: u8, - second: u8, -} - -impl ::std::default::Default for DateTime { - /// Constructs an 'default' datetime of 1980-01-01 00:00:00 - fn default() -> DateTime { - DateTime { - year: 1980, - month: 1, - day: 1, - hour: 0, - minute: 0, - second: 0, - } - } -} - -impl DateTime { - /// Converts an msdos (u16, u16) pair to a DateTime object - pub fn from_msdos(datepart: u16, timepart: u16) -> DateTime { - let seconds = (timepart & 0b0000000000011111) << 1; - let minutes = (timepart & 0b0000011111100000) >> 5; - let hours = (timepart & 0b1111100000000000) >> 11; - let days = (datepart & 0b0000000000011111) >> 0; - let months = (datepart & 0b0000000111100000) >> 5; - let years = (datepart & 0b1111111000000000) >> 9; - - DateTime { - year: (years + 1980) as u16, - month: months as u8, - day: days as u8, - hour: hours as u8, - minute: minutes as u8, - second: seconds as u8, - } - } - - /// Constructs a DateTime from a specific date and time - /// - /// The bounds are: - /// * year: [1980, 2107] - /// * month: [1, 12] - /// * day: [1, 31] - /// * hour: [0, 23] - /// * minute: [0, 59] - /// * second: [0, 60] - pub fn from_date_and_time( - year: u16, - month: u8, - day: u8, - hour: u8, - minute: u8, - second: u8, - ) -> Result { - if year >= 1980 - && year <= 2107 - && month >= 1 - && month <= 12 - && day >= 1 - && day <= 31 - && hour <= 23 - && minute <= 59 - && second <= 60 - { - Ok(DateTime { - year, - month, - day, - hour, - minute, - second, - }) - } else { - Err(()) - } - } - - #[cfg(feature = "time")] - /// Converts a ::time::Tm object to a DateTime - /// - /// Returns `Err` when this object is out of bounds - pub fn from_time(tm: ::time::Tm) -> Result { - if tm.tm_year >= 80 - && tm.tm_year <= 207 - && tm.tm_mon >= 0 - && tm.tm_mon <= 11 - && tm.tm_mday >= 1 - && tm.tm_mday <= 31 - && tm.tm_hour >= 0 - && tm.tm_hour <= 23 - && tm.tm_min >= 0 - && tm.tm_min <= 59 - && tm.tm_sec >= 0 - && tm.tm_sec <= 60 - { - Ok(DateTime { - year: (tm.tm_year + 1900) as u16, - month: (tm.tm_mon + 1) as u8, - day: tm.tm_mday as u8, - hour: tm.tm_hour as u8, - minute: tm.tm_min as u8, - second: tm.tm_sec as u8, - }) - } else { - Err(()) - } - } - - /// Gets the time portion of this datetime in the msdos representation - pub fn timepart(&self) -> u16 { - ((self.second as u16) >> 1) | ((self.minute as u16) << 5) | ((self.hour as u16) << 11) - } - - /// Gets the date portion of this datetime in the msdos representation - pub fn datepart(&self) -> u16 { - (self.day as u16) | ((self.month as u16) << 5) | ((self.year - 1980) << 9) - } - - #[cfg(feature = "time")] - /// Converts the datetime to a Tm structure - /// - /// The fields `tm_wday`, `tm_yday`, `tm_utcoff` and `tm_nsec` are set to their defaults. - pub fn to_time(&self) -> ::time::Tm { - ::time::Tm { - tm_sec: self.second as i32, - tm_min: self.minute as i32, - tm_hour: self.hour as i32, - tm_mday: self.day as i32, - tm_mon: self.month as i32 - 1, - tm_year: self.year as i32 - 1900, - tm_isdst: -1, - ..::time::empty_tm() - } - } - - /// Get the year. There is no epoch, i.e. 2018 will be returned as 2018. - pub fn year(&self) -> u16 { - self.year - } - - /// Get the month, where 1 = january and 12 = december - pub fn month(&self) -> u8 { - self.month - } - - /// Get the day - pub fn day(&self) -> u8 { - self.day - } - - /// Get the hour - pub fn hour(&self) -> u8 { - self.hour - } - - /// Get the minute - pub fn minute(&self) -> u8 { - self.minute - } - - /// Get the second - pub fn second(&self) -> u8 { - self.second - } -} - -pub const DEFAULT_VERSION: u8 = 46; - -/// Structure representing a ZIP file. -#[derive(Debug, Clone)] -pub struct ZipFileData { - /// Compatibility of the file attribute information - pub system: System, - /// Specification version - pub version_made_by: u8, - /// True if the file is encrypted. - pub encrypted: bool, - /// Compression method used to store the file - pub compression_method: crate::compression::CompressionMethod, - /// Last modified time. This will only have a 2 second precision. - pub last_modified_time: DateTime, - /// CRC32 checksum - pub crc32: u32, - /// Size of the file in the ZIP - pub compressed_size: u64, - /// Size of the file when extracted - pub uncompressed_size: u64, - /// Name of the file - pub file_name: String, - /// Raw file name. To be used when file_name was incorrectly decoded. - pub file_name_raw: Vec, - /// File comment - pub file_comment: String, - /// Specifies where the local header of the file starts - pub header_start: u64, - /// Specifies where the central header of the file starts - /// - /// Note that when this is not known, it is set to 0 - pub central_header_start: u64, - /// Specifies where the compressed data of the file starts - pub data_start: u64, - /// External file attributes - pub external_attributes: u32, -} - -impl ZipFileData { - pub fn file_name_sanitized(&self) -> ::std::path::PathBuf { - let no_null_filename = match self.file_name.find('\0') { - Some(index) => &self.file_name[0..index], - None => &self.file_name, - } - .to_string(); - - // zip files can contain both / and \ as separators regardless of the OS - // and as we want to return a sanitized PathBuf that only supports the - // OS separator let's convert incompatible separators to compatible ones - let separator = ::std::path::MAIN_SEPARATOR; - let opposite_separator = match separator { - '/' => '\\', - _ => '/', - }; - let filename = - no_null_filename.replace(&opposite_separator.to_string(), &separator.to_string()); - - ::std::path::Path::new(&filename) - .components() - .filter(|component| match *component { - ::std::path::Component::Normal(..) => true, - _ => false, - }) - .fold(::std::path::PathBuf::new(), |mut path, ref cur| { - path.push(cur.as_os_str()); - path - }) - } - - pub fn version_needed(&self) -> u16 { - match self.compression_method { - #[cfg(feature = "bzip2")] - crate::compression::CompressionMethod::Bzip2 => 46, - _ => 20, - } - } -} - -#[cfg(test)] -mod test { - #[test] - fn system() { - use super::System; - assert_eq!(System::Dos as u16, 0u16); - assert_eq!(System::Unix as u16, 3u16); - assert_eq!(System::from_u8(0), System::Dos); - assert_eq!(System::from_u8(3), System::Unix); - } - - #[test] - fn sanitize() { - use super::*; - let file_name = "/path/../../../../etc/./passwd\0/etc/shadow".to_string(); - let data = ZipFileData { - system: System::Dos, - version_made_by: 0, - encrypted: false, - compression_method: crate::compression::CompressionMethod::Stored, - last_modified_time: DateTime::default(), - crc32: 0, - compressed_size: 0, - uncompressed_size: 0, - file_name: file_name.clone(), - file_name_raw: file_name.into_bytes(), - file_comment: String::new(), - header_start: 0, - data_start: 0, - central_header_start: 0, - external_attributes: 0, - }; - assert_eq!( - data.file_name_sanitized(), - ::std::path::PathBuf::from("path/etc/passwd") - ); - } - - #[test] - fn datetime_default() { - use super::DateTime; - let dt = DateTime::default(); - assert_eq!(dt.timepart(), 0); - assert_eq!(dt.datepart(), 0b0000000_0001_00001); - } - - #[test] - fn datetime_max() { - use super::DateTime; - let dt = DateTime::from_date_and_time(2107, 12, 31, 23, 59, 60).unwrap(); - assert_eq!(dt.timepart(), 0b10111_111011_11110); - assert_eq!(dt.datepart(), 0b1111111_1100_11111); - } - - #[test] - fn datetime_bounds() { - use super::DateTime; - - assert!(DateTime::from_date_and_time(2000, 1, 1, 23, 59, 60).is_ok()); - assert!(DateTime::from_date_and_time(2000, 1, 1, 24, 0, 0).is_err()); - assert!(DateTime::from_date_and_time(2000, 1, 1, 0, 60, 0).is_err()); - assert!(DateTime::from_date_and_time(2000, 1, 1, 0, 0, 61).is_err()); - - assert!(DateTime::from_date_and_time(2107, 12, 31, 0, 0, 0).is_ok()); - assert!(DateTime::from_date_and_time(1980, 1, 1, 0, 0, 0).is_ok()); - assert!(DateTime::from_date_and_time(1979, 1, 1, 0, 0, 0).is_err()); - assert!(DateTime::from_date_and_time(1980, 0, 1, 0, 0, 0).is_err()); - assert!(DateTime::from_date_and_time(1980, 1, 0, 0, 0, 0).is_err()); - assert!(DateTime::from_date_and_time(2108, 12, 31, 0, 0, 0).is_err()); - assert!(DateTime::from_date_and_time(2107, 13, 31, 0, 0, 0).is_err()); - assert!(DateTime::from_date_and_time(2107, 12, 32, 0, 0, 0).is_err()); - } - - #[cfg(feature = "time")] - #[test] - fn datetime_from_time_bounds() { - use super::DateTime; - - // 1979-12-31 23:59:59 - assert!(DateTime::from_time(::time::Tm { - tm_sec: 59, - tm_min: 59, - tm_hour: 23, - tm_mday: 31, - tm_mon: 11, // tm_mon has number range [0, 11] - tm_year: 79, // 1979 - 1900 = 79 - ..::time::empty_tm() - }) - .is_err()); - - // 1980-01-01 00:00:00 - assert!(DateTime::from_time(::time::Tm { - tm_sec: 0, - tm_min: 0, - tm_hour: 0, - tm_mday: 1, - tm_mon: 0, // tm_mon has number range [0, 11] - tm_year: 80, // 1980 - 1900 = 80 - ..::time::empty_tm() - }) - .is_ok()); - - // 2107-12-31 23:59:59 - assert!(DateTime::from_time(::time::Tm { - tm_sec: 59, - tm_min: 59, - tm_hour: 23, - tm_mday: 31, - tm_mon: 11, // tm_mon has number range [0, 11] - tm_year: 207, // 2107 - 1900 = 207 - ..::time::empty_tm() - }) - .is_ok()); - - // 2108-01-01 00:00:00 - assert!(DateTime::from_time(::time::Tm { - tm_sec: 0, - tm_min: 0, - tm_hour: 0, - tm_mday: 1, - tm_mon: 0, // tm_mon has number range [0, 11] - tm_year: 208, // 2108 - 1900 = 208 - ..::time::empty_tm() - }) - .is_err()); - } - - #[test] - fn time_conversion() { - use super::DateTime; - let dt = DateTime::from_msdos(0x4D71, 0x54CF); - assert_eq!(dt.year(), 2018); - assert_eq!(dt.month(), 11); - assert_eq!(dt.day(), 17); - assert_eq!(dt.hour(), 10); - assert_eq!(dt.minute(), 38); - assert_eq!(dt.second(), 30); - - #[cfg(feature = "time")] - assert_eq!( - format!("{}", dt.to_time().rfc3339()), - "2018-11-17T10:38:30Z" - ); - } - - #[test] - fn time_out_of_bounds() { - use super::DateTime; - let dt = DateTime::from_msdos(0xFFFF, 0xFFFF); - assert_eq!(dt.year(), 2107); - assert_eq!(dt.month(), 15); - assert_eq!(dt.day(), 31); - assert_eq!(dt.hour(), 31); - assert_eq!(dt.minute(), 63); - assert_eq!(dt.second(), 62); - - #[cfg(feature = "time")] - assert_eq!( - format!("{}", dt.to_time().rfc3339()), - "2107-15-31T31:63:62Z" - ); - - let dt = DateTime::from_msdos(0x0000, 0x0000); - assert_eq!(dt.year(), 1980); - assert_eq!(dt.month(), 0); - assert_eq!(dt.day(), 0); - assert_eq!(dt.hour(), 0); - assert_eq!(dt.minute(), 0); - assert_eq!(dt.second(), 0); - - #[cfg(feature = "time")] - assert_eq!( - format!("{}", dt.to_time().rfc3339()), - "1980-00-00T00:00:00Z" - ); - } - - #[cfg(feature = "time")] - #[test] - fn time_at_january() { - use super::DateTime; - - // 2020-01-01 00:00:00 - let clock = ::time::Timespec::new(1577836800, 0); - let tm = ::time::at_utc(clock); - assert!(DateTime::from_time(tm).is_ok()); - } -} diff --git a/third-party/zip/src/write.rs b/third-party/zip/src/write.rs deleted file mode 100644 index bc6881727..000000000 --- a/third-party/zip/src/write.rs +++ /dev/null @@ -1,821 +0,0 @@ -//! Types for creating ZIP archives - -use crate::compression::CompressionMethod; -use crate::read::ZipFile; -use crate::result::{ZipError, ZipResult}; -use crate::spec; -use crate::types::{DateTime, System, ZipFileData, DEFAULT_VERSION}; -use byteorder::{LittleEndian, WriteBytesExt}; -use crc32fast::Hasher; -use std::default::Default; -use std::io; -use std::io::prelude::*; -use std::mem; - -#[cfg(any( - feature = "deflate", - feature = "deflate-miniz", - feature = "deflate-zlib" -))] -use flate2::write::DeflateEncoder; - -#[cfg(feature = "bzip2")] -use bzip2::write::BzEncoder; - -enum GenericZipWriter { - Closed, - Storer(W), - #[cfg(any( - feature = "deflate", - feature = "deflate-miniz", - feature = "deflate-zlib" - ))] - Deflater(DeflateEncoder), - #[cfg(feature = "bzip2")] - Bzip2(BzEncoder), -} - -/// ZIP archive generator -/// -/// Handles the bookkeeping involved in building an archive, and provides an -/// API to edit its contents. -/// -/// ``` -/// # fn doit() -> zip::result::ZipResult<()> -/// # { -/// # use zip::ZipWriter; -/// use std::io::Write; -/// use zip::write::FileOptions; -/// -/// // We use a buffer here, though you'd normally use a `File` -/// let mut buf = [0; 65536]; -/// let mut zip = zip::ZipWriter::new(std::io::Cursor::new(&mut buf[..])); -/// -/// let options = zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Stored); -/// zip.start_file("hello_world.txt", options)?; -/// zip.write(b"Hello, World!")?; -/// -/// // Apply the changes you've made. -/// // Dropping the `ZipWriter` will have the same effect, but may silently fail -/// zip.finish()?; -/// -/// # Ok(()) -/// # } -/// # doit().unwrap(); -/// ``` -pub struct ZipWriter { - inner: GenericZipWriter, - files: Vec, - stats: ZipWriterStats, - writing_to_file: bool, - comment: String, - writing_raw: bool, -} - -#[derive(Default)] -struct ZipWriterStats { - hasher: Hasher, - start: u64, - bytes_written: u64, -} - -struct ZipRawValues { - crc32: u32, - compressed_size: u64, - uncompressed_size: u64, -} - -/// Metadata for a file to be written -#[derive(Copy, Clone)] -pub struct FileOptions { - compression_method: CompressionMethod, - last_modified_time: DateTime, - permissions: Option, -} - -impl FileOptions { - /// Construct a new FileOptions object - pub fn default() -> FileOptions { - FileOptions { - #[cfg(any( - feature = "deflate", - feature = "deflate-miniz", - feature = "deflate-zlib" - ))] - compression_method: CompressionMethod::Deflated, - #[cfg(not(any( - feature = "deflate", - feature = "deflate-miniz", - feature = "deflate-zlib" - )))] - compression_method: CompressionMethod::Stored, - #[cfg(feature = "time")] - last_modified_time: DateTime::from_time(time::now()).unwrap_or_default(), - #[cfg(not(feature = "time"))] - last_modified_time: DateTime::default(), - permissions: None, - } - } - - /// Set the compression method for the new file - /// - /// The default is `CompressionMethod::Deflated`. If the deflate compression feature is - /// disabled, `CompressionMethod::Stored` becomes the default. - /// otherwise. - pub fn compression_method(mut self, method: CompressionMethod) -> FileOptions { - self.compression_method = method; - self - } - - /// Set the last modified time - /// - /// The default is the current timestamp if the 'time' feature is enabled, and 1980-01-01 - /// otherwise - pub fn last_modified_time(mut self, mod_time: DateTime) -> FileOptions { - self.last_modified_time = mod_time; - self - } - - /// Set the permissions for the new file. - /// - /// The format is represented with unix-style permissions. - /// The default is `0o644`, which represents `rw-r--r--` for files, - /// and `0o755`, which represents `rwxr-xr-x` for directories - pub fn unix_permissions(mut self, mode: u32) -> FileOptions { - self.permissions = Some(mode & 0o777); - self - } -} - -impl Default for FileOptions { - fn default() -> Self { - Self::default() - } -} - -impl Write for ZipWriter { - fn write(&mut self, buf: &[u8]) -> io::Result { - if !self.writing_to_file { - return Err(io::Error::new( - io::ErrorKind::Other, - "No file has been started", - )); - } - match self.inner.ref_mut() { - Some(ref mut w) => { - let write_result = w.write(buf); - if let Ok(count) = write_result { - self.stats.update(&buf[0..count]); - } - write_result - } - None => Err(io::Error::new( - io::ErrorKind::BrokenPipe, - "ZipWriter was already closed", - )), - } - } - - fn flush(&mut self) -> io::Result<()> { - match self.inner.ref_mut() { - Some(ref mut w) => w.flush(), - None => Err(io::Error::new( - io::ErrorKind::BrokenPipe, - "ZipWriter was already closed", - )), - } - } -} - -impl ZipWriterStats { - fn update(&mut self, buf: &[u8]) { - self.hasher.update(buf); - self.bytes_written += buf.len() as u64; - } -} - -impl ZipWriter { - /// Initializes the archive. - /// - /// Before writing to this object, the [`ZipWriter::start_file`] function should be called. - pub fn new(inner: W) -> ZipWriter { - ZipWriter { - inner: GenericZipWriter::Storer(inner), - files: Vec::new(), - stats: Default::default(), - writing_to_file: false, - comment: String::new(), - writing_raw: false, - } - } - - /// Set ZIP archive comment. - pub fn set_comment(&mut self, comment: S) - where - S: Into, - { - self.comment = comment.into(); - } - - /// Start a new file for with the requested options. - fn start_entry( - &mut self, - name: S, - options: FileOptions, - raw_values: Option, - ) -> ZipResult<()> - where - S: Into, - { - self.finish_file()?; - - let is_raw = raw_values.is_some(); - let raw_values = raw_values.unwrap_or_else(|| ZipRawValues { - crc32: 0, - compressed_size: 0, - uncompressed_size: 0, - }); - - { - let writer = self.inner.get_plain(); - let header_start = writer.seek(io::SeekFrom::Current(0))?; - - let permissions = options.permissions.unwrap_or(0o100644); - let mut file = ZipFileData { - system: System::Unix, - version_made_by: DEFAULT_VERSION, - encrypted: false, - compression_method: options.compression_method, - last_modified_time: options.last_modified_time, - crc32: raw_values.crc32, - compressed_size: raw_values.compressed_size, - uncompressed_size: raw_values.uncompressed_size, - file_name: name.into(), - file_name_raw: Vec::new(), // Never used for saving - file_comment: String::new(), - header_start, - data_start: 0, - central_header_start: 0, - external_attributes: permissions << 16, - }; - write_local_file_header(writer, &file)?; - - let header_end = writer.seek(io::SeekFrom::Current(0))?; - self.stats.start = header_end; - file.data_start = header_end; - - self.stats.bytes_written = 0; - self.stats.hasher = Hasher::new(); - - self.files.push(file); - } - - self.writing_raw = is_raw; - self.inner.switch_to(if is_raw { - CompressionMethod::Stored - } else { - options.compression_method - })?; - - Ok(()) - } - - fn finish_file(&mut self) -> ZipResult<()> { - self.inner.switch_to(CompressionMethod::Stored)?; - let writer = self.inner.get_plain(); - - if !self.writing_raw { - let file = match self.files.last_mut() { - None => return Ok(()), - Some(f) => f, - }; - file.crc32 = self.stats.hasher.clone().finalize(); - file.uncompressed_size = self.stats.bytes_written; - - let file_end = writer.seek(io::SeekFrom::Current(0))?; - file.compressed_size = file_end - self.stats.start; - - update_local_file_header(writer, file)?; - writer.seek(io::SeekFrom::Start(file_end))?; - } - - self.writing_to_file = false; - self.writing_raw = false; - Ok(()) - } - - /// Create a file in the archive and start writing its' contents. - /// - /// The data should be written using the [`io::Write`] implementation on this [`ZipWriter`] - pub fn start_file(&mut self, name: S, mut options: FileOptions) -> ZipResult<()> - where - S: Into, - { - if options.permissions.is_none() { - options.permissions = Some(0o644); - } - *options.permissions.as_mut().unwrap() |= 0o100000; - self.start_entry(name, options, None)?; - self.writing_to_file = true; - Ok(()) - } - - /// Starts a file, taking a Path as argument. - /// - /// This function ensures that the '/' path seperator is used. It also ignores all non 'Normal' - /// Components, such as a starting '/' or '..' and '.'. - #[deprecated( - since = "0.5.7", - note = "by stripping `..`s from the path, the meaning of paths can change. Use `start_file` instead." - )] - pub fn start_file_from_path( - &mut self, - path: &std::path::Path, - options: FileOptions, - ) -> ZipResult<()> { - self.start_file(path_to_string(path), options) - } - - /// Add a new file using the already compressed data from a ZIP file being read and renames it, this - /// allows faster copies of the `ZipFile` since there is no need to decompress and compress it again. - /// Any `ZipFile` metadata is copied and not checked, for example the file CRC. - - /// ```no_run - /// use std::fs::File; - /// use std::io::{Read, Seek, Write}; - /// use zip::{ZipArchive, ZipWriter}; - /// - /// fn copy_rename( - /// src: &mut ZipArchive, - /// dst: &mut ZipWriter, - /// ) -> zip::result::ZipResult<()> - /// where - /// R: Read + Seek, - /// W: Write + Seek, - /// { - /// // Retrieve file entry by name - /// let file = src.by_name("src_file.txt")?; - /// - /// // Copy and rename the previously obtained file entry to the destination zip archive - /// dst.raw_copy_file_rename(file, "new_name.txt")?; - /// - /// Ok(()) - /// } - /// ``` - pub fn raw_copy_file_rename(&mut self, mut file: ZipFile, name: S) -> ZipResult<()> - where - S: Into, - { - let options = FileOptions::default() - .last_modified_time(file.last_modified()) - .compression_method(file.compression()); - if let Some(perms) = file.unix_mode() { - options.unix_permissions(perms); - } - - let raw_values = ZipRawValues { - crc32: file.crc32(), - compressed_size: file.compressed_size(), - uncompressed_size: file.size(), - }; - - self.start_entry(name, options, Some(raw_values))?; - self.writing_to_file = true; - - io::copy(file.get_raw_reader(), self)?; - - Ok(()) - } - - /// Add a new file using the already compressed data from a ZIP file being read, this allows faster - /// copies of the `ZipFile` since there is no need to decompress and compress it again. Any `ZipFile` - /// metadata is copied and not checked, for example the file CRC. - /// - /// ```no_run - /// use std::fs::File; - /// use std::io::{Read, Seek, Write}; - /// use zip::{ZipArchive, ZipWriter}; - /// - /// fn copy(src: &mut ZipArchive, dst: &mut ZipWriter) -> zip::result::ZipResult<()> - /// where - /// R: Read + Seek, - /// W: Write + Seek, - /// { - /// // Retrieve file entry by name - /// let file = src.by_name("src_file.txt")?; - /// - /// // Copy the previously obtained file entry to the destination zip archive - /// dst.raw_copy_file(file)?; - /// - /// Ok(()) - /// } - /// ``` - pub fn raw_copy_file(&mut self, file: ZipFile) -> ZipResult<()> { - let name = file.name().to_owned(); - self.raw_copy_file_rename(file, name) - } - - /// Add a directory entry. - /// - /// You can't write data to the file afterwards. - pub fn add_directory(&mut self, name: S, mut options: FileOptions) -> ZipResult<()> - where - S: Into, - { - if options.permissions.is_none() { - options.permissions = Some(0o755); - } - *options.permissions.as_mut().unwrap() |= 0o40000; - options.compression_method = CompressionMethod::Stored; - - let name_as_string = name.into(); - // Append a slash to the filename if it does not end with it. - let name_with_slash = match name_as_string.chars().last() { - Some('/') | Some('\\') => name_as_string, - _ => name_as_string + "/", - }; - - self.start_entry(name_with_slash, options, None)?; - self.writing_to_file = false; - Ok(()) - } - - /// Add a directory entry, taking a Path as argument. - /// - /// This function ensures that the '/' path seperator is used. It also ignores all non 'Normal' - /// Components, such as a starting '/' or '..' and '.'. - #[deprecated( - since = "0.5.7", - note = "by stripping `..`s from the path, the meaning of paths can change. Use `add_directory` instead." - )] - pub fn add_directory_from_path( - &mut self, - path: &std::path::Path, - options: FileOptions, - ) -> ZipResult<()> { - self.add_directory(path_to_string(path), options) - } - - /// Finish the last file and write all other zip-structures - /// - /// This will return the writer, but one should normally not append any data to the end of the file. - /// Note that the zipfile will also be finished on drop. - pub fn finish(&mut self) -> ZipResult { - self.finalize()?; - let inner = mem::replace(&mut self.inner, GenericZipWriter::Closed); - Ok(inner.unwrap()) - } - - fn finalize(&mut self) -> ZipResult<()> { - self.finish_file()?; - - { - let writer = self.inner.get_plain(); - - let central_start = writer.seek(io::SeekFrom::Current(0))?; - for file in self.files.iter() { - write_central_directory_header(writer, file)?; - } - let central_size = writer.seek(io::SeekFrom::Current(0))? - central_start; - - let footer = spec::CentralDirectoryEnd { - disk_number: 0, - disk_with_central_directory: 0, - number_of_files_on_this_disk: self.files.len() as u16, - number_of_files: self.files.len() as u16, - central_directory_size: central_size as u32, - central_directory_offset: central_start as u32, - zip_file_comment: self.comment.as_bytes().to_vec(), - }; - - footer.write(writer)?; - } - - Ok(()) - } -} - -impl Drop for ZipWriter { - fn drop(&mut self) { - if !self.inner.is_closed() { - if let Err(e) = self.finalize() { - let _ = write!(&mut io::stderr(), "ZipWriter drop failed: {:?}", e); - } - } - } -} - -impl GenericZipWriter { - fn switch_to(&mut self, compression: CompressionMethod) -> ZipResult<()> { - match self.current_compression() { - Some(method) if method == compression => return Ok(()), - None => { - return Err(io::Error::new( - io::ErrorKind::BrokenPipe, - "ZipWriter was already closed", - ) - .into()) - } - _ => {} - } - - let bare = match mem::replace(self, GenericZipWriter::Closed) { - GenericZipWriter::Storer(w) => w, - #[cfg(any( - feature = "deflate", - feature = "deflate-miniz", - feature = "deflate-zlib" - ))] - GenericZipWriter::Deflater(w) => w.finish()?, - #[cfg(feature = "bzip2")] - GenericZipWriter::Bzip2(w) => w.finish()?, - GenericZipWriter::Closed => { - return Err(io::Error::new( - io::ErrorKind::BrokenPipe, - "ZipWriter was already closed", - ) - .into()) - } - }; - - *self = { - #[allow(deprecated)] - match compression { - CompressionMethod::Stored => GenericZipWriter::Storer(bare), - #[cfg(any( - feature = "deflate", - feature = "deflate-miniz", - feature = "deflate-zlib" - ))] - CompressionMethod::Deflated => GenericZipWriter::Deflater(DeflateEncoder::new( - bare, - flate2::Compression::default(), - )), - #[cfg(feature = "bzip2")] - CompressionMethod::Bzip2 => { - GenericZipWriter::Bzip2(BzEncoder::new(bare, bzip2::Compression::Default)) - } - CompressionMethod::Unsupported(..) => { - return Err(ZipError::UnsupportedArchive("Unsupported compression")) - } - } - }; - - Ok(()) - } - - fn ref_mut(&mut self) -> Option<&mut dyn Write> { - match *self { - GenericZipWriter::Storer(ref mut w) => Some(w as &mut dyn Write), - #[cfg(any( - feature = "deflate", - feature = "deflate-miniz", - feature = "deflate-zlib" - ))] - GenericZipWriter::Deflater(ref mut w) => Some(w as &mut dyn Write), - #[cfg(feature = "bzip2")] - GenericZipWriter::Bzip2(ref mut w) => Some(w as &mut dyn Write), - GenericZipWriter::Closed => None, - } - } - - fn is_closed(&self) -> bool { - match *self { - GenericZipWriter::Closed => true, - _ => false, - } - } - - fn get_plain(&mut self) -> &mut W { - match *self { - GenericZipWriter::Storer(ref mut w) => w, - _ => panic!("Should have switched to stored beforehand"), - } - } - - fn current_compression(&self) -> Option { - match *self { - GenericZipWriter::Storer(..) => Some(CompressionMethod::Stored), - #[cfg(any( - feature = "deflate", - feature = "deflate-miniz", - feature = "deflate-zlib" - ))] - GenericZipWriter::Deflater(..) => Some(CompressionMethod::Deflated), - #[cfg(feature = "bzip2")] - GenericZipWriter::Bzip2(..) => Some(CompressionMethod::Bzip2), - GenericZipWriter::Closed => None, - } - } - - fn unwrap(self) -> W { - match self { - GenericZipWriter::Storer(w) => w, - _ => panic!("Should have switched to stored beforehand"), - } - } -} - -fn write_local_file_header(writer: &mut T, file: &ZipFileData) -> ZipResult<()> { - // local file header signature - writer.write_u32::(spec::LOCAL_FILE_HEADER_SIGNATURE)?; - // version needed to extract - writer.write_u16::(file.version_needed())?; - // general purpose bit flag - let flag = if !file.file_name.is_ascii() { - 1u16 << 11 - } else { - 0 - }; - writer.write_u16::(flag)?; - // Compression method - #[allow(deprecated)] - writer.write_u16::(file.compression_method.to_u16())?; - // last mod file time and last mod file date - writer.write_u16::(file.last_modified_time.timepart())?; - writer.write_u16::(file.last_modified_time.datepart())?; - // crc-32 - writer.write_u32::(file.crc32)?; - // compressed size - writer.write_u32::(file.compressed_size as u32)?; - // uncompressed size - writer.write_u32::(file.uncompressed_size as u32)?; - // file name length - writer.write_u16::(file.file_name.as_bytes().len() as u16)?; - // extra field length - let extra_field = build_extra_field(file)?; - writer.write_u16::(extra_field.len() as u16)?; - // file name - writer.write_all(file.file_name.as_bytes())?; - // extra field - writer.write_all(&extra_field)?; - - Ok(()) -} - -fn update_local_file_header( - writer: &mut T, - file: &ZipFileData, -) -> ZipResult<()> { - const CRC32_OFFSET: u64 = 14; - writer.seek(io::SeekFrom::Start(file.header_start + CRC32_OFFSET))?; - writer.write_u32::(file.crc32)?; - writer.write_u32::(file.compressed_size as u32)?; - writer.write_u32::(file.uncompressed_size as u32)?; - Ok(()) -} - -fn write_central_directory_header(writer: &mut T, file: &ZipFileData) -> ZipResult<()> { - // central file header signature - writer.write_u32::(spec::CENTRAL_DIRECTORY_HEADER_SIGNATURE)?; - // version made by - let version_made_by = (file.system as u16) << 8 | (file.version_made_by as u16); - writer.write_u16::(version_made_by)?; - // version needed to extract - writer.write_u16::(file.version_needed())?; - // general puprose bit flag - let flag = if !file.file_name.is_ascii() { - 1u16 << 11 - } else { - 0 - }; - writer.write_u16::(flag)?; - // compression method - #[allow(deprecated)] - writer.write_u16::(file.compression_method.to_u16())?; - // last mod file time + date - writer.write_u16::(file.last_modified_time.timepart())?; - writer.write_u16::(file.last_modified_time.datepart())?; - // crc-32 - writer.write_u32::(file.crc32)?; - // compressed size - writer.write_u32::(file.compressed_size as u32)?; - // uncompressed size - writer.write_u32::(file.uncompressed_size as u32)?; - // file name length - writer.write_u16::(file.file_name.as_bytes().len() as u16)?; - // extra field length - let extra_field = build_extra_field(file)?; - writer.write_u16::(extra_field.len() as u16)?; - // file comment length - writer.write_u16::(0)?; - // disk number start - writer.write_u16::(0)?; - // internal file attribytes - writer.write_u16::(0)?; - // external file attributes - writer.write_u32::(file.external_attributes)?; - // relative offset of local header - writer.write_u32::(file.header_start as u32)?; - // file name - writer.write_all(file.file_name.as_bytes())?; - // extra field - writer.write_all(&extra_field)?; - // file comment - // - - Ok(()) -} - -fn build_extra_field(_file: &ZipFileData) -> ZipResult> { - let writer = Vec::new(); - // Future work - Ok(writer) -} - -fn path_to_string(path: &std::path::Path) -> String { - let mut path_str = String::new(); - for component in path.components() { - if let std::path::Component::Normal(os_str) = component { - if !path_str.is_empty() { - path_str.push('/'); - } - path_str.push_str(&*os_str.to_string_lossy()); - } - } - path_str -} - -#[cfg(test)] -mod test { - use super::{FileOptions, ZipWriter}; - use crate::compression::CompressionMethod; - use crate::types::DateTime; - use std::io; - use std::io::Write; - - #[test] - fn write_empty_zip() { - let mut writer = ZipWriter::new(io::Cursor::new(Vec::new())); - writer.set_comment("ZIP"); - let result = writer.finish().unwrap(); - assert_eq!(result.get_ref().len(), 25); - assert_eq!( - *result.get_ref(), - [80, 75, 5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 90, 73, 80] - ); - } - - #[test] - fn write_zip_dir() { - let mut writer = ZipWriter::new(io::Cursor::new(Vec::new())); - writer - .add_directory( - "test", - FileOptions::default().last_modified_time( - DateTime::from_date_and_time(2018, 8, 15, 20, 45, 6).unwrap(), - ), - ) - .unwrap(); - assert!(writer - .write(b"writing to a directory is not allowed, and will not write any data") - .is_err()); - let result = writer.finish().unwrap(); - assert_eq!(result.get_ref().len(), 108); - assert_eq!( - *result.get_ref(), - &[ - 80u8, 75, 3, 4, 20, 0, 0, 0, 0, 0, 163, 165, 15, 77, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 5, 0, 0, 0, 116, 101, 115, 116, 47, 80, 75, 1, 2, 46, 3, 20, 0, 0, 0, 0, 0, - 163, 165, 15, 77, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 237, 65, 0, 0, 0, 0, 116, 101, 115, 116, 47, 80, 75, 5, 6, 0, 0, 0, 0, 1, 0, - 1, 0, 51, 0, 0, 0, 35, 0, 0, 0, 0, 0, - ] as &[u8] - ); - } - - #[test] - fn write_mimetype_zip() { - let mut writer = ZipWriter::new(io::Cursor::new(Vec::new())); - let options = FileOptions { - compression_method: CompressionMethod::Stored, - last_modified_time: DateTime::default(), - permissions: Some(33188), - }; - writer.start_file("mimetype", options).unwrap(); - writer - .write(b"application/vnd.oasis.opendocument.text") - .unwrap(); - let result = writer.finish().unwrap(); - - assert_eq!(result.get_ref().len(), 153); - let mut v = Vec::new(); - v.extend_from_slice(include_bytes!("../tests/data/mimetype.zip")); - assert_eq!(result.get_ref(), &v); - } - - #[test] - fn path_to_string() { - let mut path = std::path::PathBuf::new(); - #[cfg(windows)] - path.push(r"C:\"); - #[cfg(unix)] - path.push("/"); - path.push("windows"); - path.push(".."); - path.push("."); - path.push("system32"); - let path_str = super::path_to_string(&path); - assert_eq!(path_str, "windows/system32"); - } -} diff --git a/third-party/zip/src/zipcrypto.rs b/third-party/zip/src/zipcrypto.rs deleted file mode 100644 index 32e8af8cf..000000000 --- a/third-party/zip/src/zipcrypto.rs +++ /dev/null @@ -1,162 +0,0 @@ -//! Implementation of the ZipCrypto algorithm -//! -//! The following paper was used to implement the ZipCrypto algorithm: -//! [https://courses.cs.ut.ee/MTAT.07.022/2015_fall/uploads/Main/dmitri-report-f15-16.pdf](https://courses.cs.ut.ee/MTAT.07.022/2015_fall/uploads/Main/dmitri-report-f15-16.pdf) - -use std::num::Wrapping; - -/// A container to hold the current key state -struct ZipCryptoKeys { - key_0: Wrapping, - key_1: Wrapping, - key_2: Wrapping, -} - -impl ZipCryptoKeys { - fn new() -> ZipCryptoKeys { - ZipCryptoKeys { - key_0: Wrapping(0x12345678), - key_1: Wrapping(0x23456789), - key_2: Wrapping(0x34567890), - } - } - - fn update(&mut self, input: u8) { - self.key_0 = ZipCryptoKeys::crc32(self.key_0, input); - self.key_1 = - (self.key_1 + (self.key_0 & Wrapping(0xff))) * Wrapping(0x08088405) + Wrapping(1); - self.key_2 = ZipCryptoKeys::crc32(self.key_2, (self.key_1 >> 24).0 as u8); - } - - fn stream_byte(&mut self) -> u8 { - let temp: Wrapping = Wrapping(self.key_2.0 as u16) | Wrapping(3); - ((temp * (temp ^ Wrapping(1))) >> 8).0 as u8 - } - - fn decrypt_byte(&mut self, cipher_byte: u8) -> u8 { - let plain_byte: u8 = self.stream_byte() ^ cipher_byte; - self.update(plain_byte); - plain_byte - } - - #[allow(dead_code)] - fn encrypt_byte(&mut self, plain_byte: u8) -> u8 { - let cipher_byte: u8 = self.stream_byte() ^ plain_byte; - self.update(plain_byte); - cipher_byte - } - - fn crc32(crc: Wrapping, input: u8) -> Wrapping { - return (crc >> 8) ^ Wrapping(CRCTABLE[((crc & Wrapping(0xff)).0 as u8 ^ input) as usize]); - } -} - -/// A ZipCrypto reader with unverified password -pub struct ZipCryptoReader { - file: R, - keys: ZipCryptoKeys, -} - -impl ZipCryptoReader { - /// Note: The password is `&[u8]` and not `&str` because the - /// [zip specification](https://pkware.cachefly.net/webdocs/APPNOTE/APPNOTE-6.3.3.TXT) - /// does not specify password encoding (see function `update_keys` in the specification). - /// Therefore, if `&str` was used, the password would be UTF-8 and it - /// would be impossible to decrypt files that were encrypted with a - /// password byte sequence that is unrepresentable in UTF-8. - pub fn new(file: R, password: &[u8]) -> ZipCryptoReader { - let mut result = ZipCryptoReader { - file: file, - keys: ZipCryptoKeys::new(), - }; - - // Key the cipher by updating the keys with the password. - for byte in password.iter() { - result.keys.update(*byte); - } - - result - } - - /// Read the ZipCrypto header bytes and validate the password. - pub fn validate( - mut self, - crc32_plaintext: u32, - ) -> Result>, std::io::Error> { - // ZipCrypto prefixes a file with a 12 byte header - let mut header_buf = [0u8; 12]; - self.file.read_exact(&mut header_buf)?; - for byte in header_buf.iter_mut() { - *byte = self.keys.decrypt_byte(*byte); - } - - // PKZIP before 2.0 used 2 byte CRC check. - // PKZIP 2.0+ used 1 byte CRC check. It's more secure. - // We also use 1 byte CRC. - - if (crc32_plaintext >> 24) as u8 != header_buf[11] { - return Ok(None); // Wrong password - } - Ok(Some(ZipCryptoReaderValid { reader: self })) - } -} - -/// A ZipCrypto reader with verified password -pub struct ZipCryptoReaderValid { - reader: ZipCryptoReader, -} - -impl std::io::Read for ZipCryptoReaderValid { - fn read(&mut self, mut buf: &mut [u8]) -> std::io::Result { - // Note: There might be potential for optimization. Inspiration can be found at: - // https://github.com/kornelski/7z/blob/master/CPP/7zip/Crypto/ZipCrypto.cpp - - let result = self.reader.file.read(&mut buf); - for byte in buf.iter_mut() { - *byte = self.reader.keys.decrypt_byte(*byte); - } - result - } -} - -impl ZipCryptoReaderValid { - /// Consumes this decoder, returning the underlying reader. - pub fn into_inner(self) -> R { - self.reader.file - } -} - -static CRCTABLE: [u32; 256] = [ - 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, - 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, - 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, - 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, - 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, - 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, - 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, - 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, - 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, - 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, - 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, - 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, - 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, - 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, - 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f, - 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, - 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, - 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, - 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, - 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, - 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, - 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, - 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, - 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, - 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, - 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, - 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, - 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, - 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, - 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, - 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, - 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d, -]; From 433f8b05b08da9c2f314389a61015486918958af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Wed, 24 Mar 2021 12:56:12 -0300 Subject: [PATCH 31/67] Simplify Lzma decompression logic --- src/compressors/tar.rs | 35 ++++++++------- src/decompressors/lzma.rs | 45 ------------------- src/decompressors/mod.rs | 14 +++--- src/decompressors/{unified.rs => tomemory.rs} | 8 ++++ src/evaluator.rs | 9 ++-- src/main.rs | 20 ++++++--- 6 files changed, 54 insertions(+), 77 deletions(-) delete mode 100644 src/decompressors/lzma.rs rename src/decompressors/{unified.rs => tomemory.rs} (85%) diff --git a/src/compressors/tar.rs b/src/compressors/tar.rs index 8af4f55dc..84d53e102 100644 --- a/src/compressors/tar.rs +++ b/src/compressors/tar.rs @@ -1,7 +1,7 @@ use std::{fs, path::PathBuf}; use colored::Colorize; -use tar::{Builder, Header}; +use tar::{Builder, EntryType, Header}; use walkdir::WalkDir; use crate::{compressors::Compressor, error::{Error, OuchResult}, file::File}; @@ -23,23 +23,26 @@ impl TarCompressor { } }; - let mut header = Header::new_gnu(); - - // header.set_path(&input.path.file_stem().unwrap())?; - header.set_path(".")?; - header.set_size(contents.len() as u64); - header.set_cksum(); - header.set_mode(644); + // let ok = EntryType:: - let mut b = Builder::new(Vec::new()); - b.append_data( - &mut header, - &input.path.file_stem().unwrap(), - &*contents - )?; + // let mut b = Builder::new(Vec::new()); + // let mut header = Header::new_gnu(); + // let name = b"././@LongLink"; + // header.as_gnu_mut().unwrap().name[..name.len()].clone_from_slice(&name[..]); + // header.set_mode(0o644); + // header.set_uid(0); + // header.set_gid(0); + // header.set_mtime(0); + // // + 1 to be compliant with GNU tar + // header.set_size(size + 1); + // header.set_entry_type(EntryType::new(entry_type)); + // header.set_cksum(); - Ok(b.into_inner()?) + + + // Ok(b.into_inner()?) + Ok(vec![]) } fn make_archive_from_files(input_filenames: Vec) -> OuchResult> { @@ -48,7 +51,7 @@ impl TarCompressor { let mut b = Builder::new(buf); for filename in input_filenames { - // TODO: check if filename is a file or a directory + // TODO: check if file exists for entry in WalkDir::new(&filename) { let entry = entry?; diff --git a/src/decompressors/lzma.rs b/src/decompressors/lzma.rs deleted file mode 100644 index a970feb91..000000000 --- a/src/decompressors/lzma.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::{fs, io::Read}; - -use colored::Colorize; - -use crate::{error::OuchResult, utils}; -use crate::file::File; - -use super::decompressor::{DecompressionResult, Decompressor}; - -pub struct LzmaDecompressor {} - -impl LzmaDecompressor { - fn extract_to_memory(from: File) -> OuchResult> { - let mut ret = vec![]; - - let from_path = from.path; - if !from_path.exists() { - eprintln!("{}: could not find {:?}", "error".red(), from_path); - } - - let input_bytes = fs::read(&from_path)?; - - - xz2::read::XzDecoder::new_multi_decoder(&*input_bytes) - .read_to_end(&mut ret)?; - - println!("{}: extracted {:?} into memory. ({} bytes)", "info".yellow(), from_path, ret.len()); - - Ok(ret) - } -} - -impl Decompressor for LzmaDecompressor { - fn decompress(&self, from: File, into: &Option) -> OuchResult { - let destination_path = utils::get_destination_path(into); - - utils::create_path_if_non_existent(destination_path)?; - - Ok( - DecompressionResult::FileInMemory( - Self::extract_to_memory(from)? - ) - ) - } -} \ No newline at end of file diff --git a/src/decompressors/mod.rs b/src/decompressors/mod.rs index 5b9c1b072..4a88a49ad 100644 --- a/src/decompressors/mod.rs +++ b/src/decompressors/mod.rs @@ -1,14 +1,18 @@ mod decompressor; -mod unified; -mod lzma; +mod tomemory; mod tar; mod zip; pub use decompressor::Decompressor; pub use decompressor::DecompressionResult; + pub use self::tar::TarDecompressor; pub use self::zip::ZipDecompressor; -pub use self::unified::GzipDecompressor; -pub use self::unified::BzipDecompressor; -pub use self::lzma::LzmaDecompressor; \ No newline at end of file + +// These decompressors only decompress to memory, +// unlike {Tar, Zip}Decompressor which are capable of +// decompressing directly to storage +pub use self::tomemory::GzipDecompressor; +pub use self::tomemory::BzipDecompressor; +pub use self::tomemory::LzmaDecompressor; \ No newline at end of file diff --git a/src/decompressors/unified.rs b/src/decompressors/tomemory.rs similarity index 85% rename from src/decompressors/unified.rs rename to src/decompressors/tomemory.rs index 463270ec4..919cbb29c 100644 --- a/src/decompressors/unified.rs +++ b/src/decompressors/tomemory.rs @@ -18,12 +18,14 @@ use super::decompressor::Decompressor; pub struct UnifiedDecompressor {} pub struct GzipDecompressor {} +pub struct LzmaDecompressor {} pub struct BzipDecompressor {} fn get_decoder<'a>(format: CompressionFormat, buffer: Box) -> Box { match format { CompressionFormat::Bzip => Box::new(bzip2::read::BzDecoder::new(buffer)), CompressionFormat::Gzip => Box::new(flate2::read::MultiGzDecoder::new(buffer)), + CompressionFormat::Lzma => Box::new(xz2::read::XzDecoder::new_multi_decoder(buffer)), _other => unreachable!() } } @@ -68,4 +70,10 @@ impl Decompressor for BzipDecompressor { fn decompress(&self, from: File, into: &Option) -> OuchResult { UnifiedDecompressor::decompress(from, CompressionFormat::Bzip, into) } +} + +impl Decompressor for LzmaDecompressor { + fn decompress(&self, from: File, into: &Option) -> OuchResult { + UnifiedDecompressor::decompress(from, CompressionFormat::Lzma, into) + } } \ No newline at end of file diff --git a/src/evaluator.rs b/src/evaluator.rs index 6a2d22fd1..b0849f117 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -93,13 +93,12 @@ impl Evaluator { CompressionFormat::Zip => Box::new(ZipDecompressor {}), - CompressionFormat::Gzip => Box::new(GzipDecompressor{}), + CompressionFormat::Gzip => Box::new(GzipDecompressor {}), - CompressionFormat::Lzma => Box::new(LzmaDecompressor{}), + CompressionFormat::Lzma => Box::new(LzmaDecompressor {}), - CompressionFormat::Bzip => { - Box::new(BzipDecompressor {}) - } + CompressionFormat::Bzip => Box::new(BzipDecompressor {}) + }; let first_decompressor: Option> = match extension.first_ext { diff --git a/src/main.rs b/src/main.rs index eabeafb19..a00387cea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,16 +34,24 @@ fn main() -> error::OuchResult<()>{ } // fn main() -> error::OuchResult<()> { -// let bytes = fs::read("extension.tar.lzma")?; -// let mut ret = vec![]; +// use tar::{Builder}; +// use walkdir::WalkDir; -// xz2::read::XzDecoder::new_multi_decoder(&*bytes) -// .read_to_end(&mut ret) -// .unwrap(); +// let mut b = Builder::new(Vec::new()); +// for entry in WalkDir::new("src") { +// let entry = entry?; +// let mut file = std::fs::File::open(entry.path())?; +// b.append_file(entry.path(), &mut file)?; +// } -// fs::write("extension.tar", &*bytes).unwrap(); +// // let mut file = std::fs::File::open("Cargo.toml")?; +// // b.append_file("Cargo.toml", &mut file)?; + +// let bytes = b.into_inner()?; + +// std::fs::write("daaaaamn.tar", bytes)?; // Ok(()) // } \ No newline at end of file From bdc16fdb17818d373a61edc1b4047856ebb22544 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Wed, 24 Mar 2021 14:33:43 -0300 Subject: [PATCH 32/67] Add support for Bzip compression (includes .tar.bz2 and .zip.bz2 and etc) --- src/compressors/bzip.rs | 103 ++++++++++++++++++++++++++++++++++ src/compressors/mod.rs | 4 +- src/compressors/unified.rs | 0 src/decompressors/tomemory.rs | 10 ++-- src/evaluator.rs | 8 ++- src/extension.rs | 78 ++++++++++++++----------- src/utils.rs | 10 ++++ 7 files changed, 169 insertions(+), 44 deletions(-) create mode 100644 src/compressors/bzip.rs delete mode 100644 src/compressors/unified.rs diff --git a/src/compressors/bzip.rs b/src/compressors/bzip.rs new file mode 100644 index 000000000..f66ef8d9c --- /dev/null +++ b/src/compressors/bzip.rs @@ -0,0 +1,103 @@ +use std::{fs, io::{self, Read, Write}, path::PathBuf}; + +use colored::Colorize; + +use crate::{error::{Error, OuchResult}, extension::CompressionFormat, file::File}; +use crate::utils::ensure_exists; + +use super::{Compressor, Entry}; + +pub struct BzipCompressor {} + +struct CompressorToMemory {} + +// impl CompressorToMemory { +// pub fn compress_files(files: Vec, format: CompressionFormat) -> OuchResult> { +// let mut buffer = vec![]; + +// if files.len() != 1 { +// eprintln!("{}: cannot compress multiple files directly to {:#?}.\n Try using an intermediate archival method such as Tar.\n Example: filename.tar{}", "error".red(), format, format); +// return Err(Error::InvalidInput); +// } + +// let mut contents = Vec::new(); +// let path = &files[0]; +// ensure_exists(path)?; + +// let bytes_read = { +// let bytes = fs::read(path)?; +// let mut encoder = get_encoder(&format, Box::new(&mut buffer)); +// encoder.write_all(&*bytes)?; +// bytes.as_slice().read_to_end(&mut contents)? +// }; + +// println!("{}: compressed {:?} into memory ({} bytes)", "info".yellow(), &path, bytes_read); + +// Ok(contents) +// } + +// pub fn compress_bytes(file: File) { + +// } +// } + +impl BzipCompressor { + fn compress_files(files: Vec, format: CompressionFormat) -> OuchResult> { + if files.len() != 1 { + eprintln!("{}: cannot compress multiple files directly to {:#?}.\n Try using an intermediate archival method such as Tar.\n Example: filename.tar{}", "error".red(), format, format); + return Err(Error::InvalidInput); + } + let path = &files[0]; + ensure_exists(path)?; + let contents = { + let bytes = fs::read(path)?; + Self::compress_bytes(&*bytes)? + }; + + println!("{}: compressed {:?} into memory ({} bytes)", "info".yellow(), &path, contents.len()); + + Ok(contents) + } + + fn compress_file_in_memory(file: File) -> OuchResult> { + // Ensure that our file has in-memory content + let bytes = match file.contents_in_memory { + Some(bytes) => bytes, + None => { + // TODO: error message, + return Err(Error::InvalidInput); + } + }; + + Ok(Self::compress_bytes(&*bytes)?) + } + + fn compress_bytes(bytes: &[u8]) -> OuchResult> { + let buffer = vec![]; + let mut encoder = bzip2::write::BzEncoder::new(buffer, bzip2::Compression::new(6)); + encoder.write_all(bytes)?; + Ok(encoder.finish()?) + } + +} + +// TODO: customizable compression level +fn get_encoder<'a>(format: &CompressionFormat, buffer: Box) -> Box { + match format { + CompressionFormat::Bzip => Box::new(bzip2::write::BzEncoder::new(buffer, bzip2::Compression::new(4))), + _other => unreachable!() + } +} + +impl Compressor for BzipCompressor { + fn compress(&self, from: Entry) -> OuchResult> { + match from { + Entry::Files(files) => Ok( + Self::compress_files(files, CompressionFormat::Bzip)? + ), + Entry::InMemory(file) => Ok( + Self::compress_file_in_memory(file)? + ), + } + } +} \ No newline at end of file diff --git a/src/compressors/mod.rs b/src/compressors/mod.rs index c9c9d1320..fd1e83cf0 100644 --- a/src/compressors/mod.rs +++ b/src/compressors/mod.rs @@ -1,10 +1,10 @@ mod tar; mod zip; -mod unified; +mod bzip; mod compressor; pub use compressor::Compressor; pub use self::compressor::Entry; pub use self::tar::TarCompressor; pub use self::zip::ZipCompressor; - +pub use self::bzip::BzipCompressor; diff --git a/src/compressors/unified.rs b/src/compressors/unified.rs deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/decompressors/tomemory.rs b/src/decompressors/tomemory.rs index 919cbb29c..faf2a57b8 100644 --- a/src/decompressors/tomemory.rs +++ b/src/decompressors/tomemory.rs @@ -16,7 +16,7 @@ use crate::{ use super::decompressor::DecompressionResult; use super::decompressor::Decompressor; -pub struct UnifiedDecompressor {} +struct DecompressorToMemory {} pub struct GzipDecompressor {} pub struct LzmaDecompressor {} pub struct BzipDecompressor {} @@ -30,7 +30,7 @@ fn get_decoder<'a>(format: CompressionFormat, buffer: Box OuchResult> { let file = std::fs::read(from)?; @@ -62,18 +62,18 @@ impl UnifiedDecompressor { impl Decompressor for GzipDecompressor { fn decompress(&self, from: File, into: &Option) -> OuchResult { - UnifiedDecompressor::decompress(from, CompressionFormat::Gzip, into) + DecompressorToMemory::decompress(from, CompressionFormat::Gzip, into) } } impl Decompressor for BzipDecompressor { fn decompress(&self, from: File, into: &Option) -> OuchResult { - UnifiedDecompressor::decompress(from, CompressionFormat::Bzip, into) + DecompressorToMemory::decompress(from, CompressionFormat::Bzip, into) } } impl Decompressor for LzmaDecompressor { fn decompress(&self, from: File, into: &Option) -> OuchResult { - UnifiedDecompressor::decompress(from, CompressionFormat::Lzma, into) + DecompressorToMemory::decompress(from, CompressionFormat::Lzma, into) } } \ No newline at end of file diff --git a/src/evaluator.rs b/src/evaluator.rs index b0849f117..963448b47 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -6,7 +6,8 @@ use crate::compressors::{ Entry, Compressor, TarCompressor, - ZipCompressor + ZipCompressor, + BzipCompressor, }; use crate::decompressors::{ @@ -67,8 +68,9 @@ impl Evaluator { // Supported second compressors: // any let second_compressor: Box = match extension.second_ext { - CompressionFormat::Tar => Box::new(TarCompressor {}), - CompressionFormat::Zip => Box::new(ZipCompressor {}), + CompressionFormat::Tar => Box::new(TarCompressor {}), + CompressionFormat::Zip => Box::new(ZipCompressor {}), + CompressionFormat::Bzip => Box::new(BzipCompressor {}), _other => todo!() }; diff --git a/src/extension.rs b/src/extension.rs index ee4f86d93..49915e1f1 100644 --- a/src/extension.rs +++ b/src/extension.rs @@ -1,4 +1,9 @@ -use std::{convert::TryFrom, ffi::OsStr, path::{Path, PathBuf}}; +use std::{ + convert::TryFrom, + ffi::OsStr, + fmt::Display, + path::{Path, PathBuf}, +}; use crate::error; use CompressionFormat::*; @@ -9,16 +14,14 @@ use CompressionFormat::*; #[derive(Clone, Debug, PartialEq, Eq)] pub struct Extension { pub first_ext: Option, - pub second_ext: CompressionFormat + pub second_ext: CompressionFormat, } pub fn get_extension_from_filename(filename: &str) -> Option<(&str, &str)> { - let path = Path::new(filename); - - let ext = path - .extension() - .and_then(OsStr::to_str)?; - + let path = Path::new(filename); + + let ext = path.extension().and_then(OsStr::to_str)?; + let previous_extension = path .file_stem() .and_then(OsStr::to_str) @@ -35,34 +38,28 @@ impl From for Extension { fn from(second_ext: CompressionFormat) -> Self { Self { first_ext: None, - second_ext + second_ext, } } } impl Extension { pub fn new(filename: &str) -> error::OuchResult { - let ext_from_str = |ext| { - match ext { - "zip" => Ok(Zip), - "tar" => Ok(Tar), - "gz" => Ok(Gzip), - "bz" | "bz2" => Ok(Bzip), - "lz" | "lzma" => Ok(Lzma), - other => Err(error::Error::UnknownExtensionError(other.into())), - } + let ext_from_str = |ext| match ext { + "zip" => Ok(Zip), + "tar" => Ok(Tar), + "gz" => Ok(Gzip), + "bz" | "bz2" => Ok(Bzip), + "lz" | "lzma" => Ok(Lzma), + other => Err(error::Error::UnknownExtensionError(other.into())), }; let (first_ext, second_ext) = match get_extension_from_filename(filename) { - Some(extension_tuple) => { - match extension_tuple { - ("", snd) => (None, snd), - (fst, snd)=> (Some(fst), snd) - } + Some(extension_tuple) => match extension_tuple { + ("", snd) => (None, snd), + (fst, snd) => (Some(fst), snd), }, - None => { - return Err(error::Error::MissingExtensionError(filename.into())) - } + None => return Err(error::Error::MissingExtensionError(filename.into())), }; let (first_ext, second_ext) = match (first_ext, second_ext) { @@ -77,12 +74,10 @@ impl Extension { } }; - Ok( - Self { - first_ext, - second_ext - } - ) + Ok(Self { + first_ext, + second_ext, + }) } } @@ -108,7 +103,7 @@ fn extension_from_os_str(ext: &OsStr) -> Result Some(str) => str, None => return Err(error::Error::InvalidUnicode), }; - + match ext { "zip" => Ok(Zip), "tar" => Ok(Tar), @@ -123,7 +118,6 @@ impl TryFrom<&PathBuf> for CompressionFormat { type Error = error::Error; fn try_from(ext: &PathBuf) -> Result { - let ext = match ext.extension() { Some(ext) => ext, None => { @@ -147,3 +141,19 @@ impl TryFrom<&str> for CompressionFormat { extension_from_os_str(ext) } } + +impl Display for CompressionFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Gzip => ".gz", + Bzip => ".bz", + Lzma => ".lz", + Tar => ".tar", + Zip => ".zip", + } + ) + } +} diff --git a/src/utils.rs b/src/utils.rs index 82babb946..bb907698c 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -3,6 +3,16 @@ use std::{fs, path::Path}; use colored::Colorize; use crate::{error::OuchResult, file::File}; +pub (crate) fn ensure_exists<'a, P>(path: P) -> OuchResult<()> +where + P: AsRef + 'a { + let exists = path.as_ref().exists(); + if !exists { + eprintln!("{}: could not find file {:?}", "error".red(), path.as_ref()); + } + Ok(()) + } + pub (crate) fn create_path_if_non_existent(path: &Path) -> OuchResult<()> { if !path.exists() { println!( From 3fa939ac9025a58592f52e0399f0bfafa1a325d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Wed, 24 Mar 2021 17:03:49 -0300 Subject: [PATCH 33/67] Add support for Gzip compression (includes .tar.gz) --- src/compressors/bzip.rs | 41 +--------------- src/compressors/mod.rs | 2 + src/compressors/tar.rs | 19 +------- src/compressors/tomemory.rs | 97 +++++++++++++++++++++++++++++++++++++ src/evaluator.rs | 2 + 5 files changed, 103 insertions(+), 58 deletions(-) create mode 100644 src/compressors/tomemory.rs diff --git a/src/compressors/bzip.rs b/src/compressors/bzip.rs index f66ef8d9c..d34866d15 100644 --- a/src/compressors/bzip.rs +++ b/src/compressors/bzip.rs @@ -1,4 +1,4 @@ -use std::{fs, io::{self, Read, Write}, path::PathBuf}; +use std::{fs, io::Write, path::PathBuf}; use colored::Colorize; @@ -9,38 +9,6 @@ use super::{Compressor, Entry}; pub struct BzipCompressor {} -struct CompressorToMemory {} - -// impl CompressorToMemory { -// pub fn compress_files(files: Vec, format: CompressionFormat) -> OuchResult> { -// let mut buffer = vec![]; - -// if files.len() != 1 { -// eprintln!("{}: cannot compress multiple files directly to {:#?}.\n Try using an intermediate archival method such as Tar.\n Example: filename.tar{}", "error".red(), format, format); -// return Err(Error::InvalidInput); -// } - -// let mut contents = Vec::new(); -// let path = &files[0]; -// ensure_exists(path)?; - -// let bytes_read = { -// let bytes = fs::read(path)?; -// let mut encoder = get_encoder(&format, Box::new(&mut buffer)); -// encoder.write_all(&*bytes)?; -// bytes.as_slice().read_to_end(&mut contents)? -// }; - -// println!("{}: compressed {:?} into memory ({} bytes)", "info".yellow(), &path, bytes_read); - -// Ok(contents) -// } - -// pub fn compress_bytes(file: File) { - -// } -// } - impl BzipCompressor { fn compress_files(files: Vec, format: CompressionFormat) -> OuchResult> { if files.len() != 1 { @@ -82,13 +50,6 @@ impl BzipCompressor { } // TODO: customizable compression level -fn get_encoder<'a>(format: &CompressionFormat, buffer: Box) -> Box { - match format { - CompressionFormat::Bzip => Box::new(bzip2::write::BzEncoder::new(buffer, bzip2::Compression::new(4))), - _other => unreachable!() - } -} - impl Compressor for BzipCompressor { fn compress(&self, from: Entry) -> OuchResult> { match from { diff --git a/src/compressors/mod.rs b/src/compressors/mod.rs index fd1e83cf0..93336d468 100644 --- a/src/compressors/mod.rs +++ b/src/compressors/mod.rs @@ -1,6 +1,7 @@ mod tar; mod zip; mod bzip; +mod tomemory; mod compressor; pub use compressor::Compressor; @@ -8,3 +9,4 @@ pub use self::compressor::Entry; pub use self::tar::TarCompressor; pub use self::zip::ZipCompressor; pub use self::bzip::BzipCompressor; +pub use self::tomemory::GzipCompressor; \ No newline at end of file diff --git a/src/compressors/tar.rs b/src/compressors/tar.rs index 84d53e102..fa5717566 100644 --- a/src/compressors/tar.rs +++ b/src/compressors/tar.rs @@ -23,25 +23,8 @@ impl TarCompressor { } }; - // let ok = EntryType:: + println!("todo"); - - // let mut b = Builder::new(Vec::new()); - // let mut header = Header::new_gnu(); - // let name = b"././@LongLink"; - // header.as_gnu_mut().unwrap().name[..name.len()].clone_from_slice(&name[..]); - // header.set_mode(0o644); - // header.set_uid(0); - // header.set_gid(0); - // header.set_mtime(0); - // // + 1 to be compliant with GNU tar - // header.set_size(size + 1); - // header.set_entry_type(EntryType::new(entry_type)); - // header.set_cksum(); - - - - // Ok(b.into_inner()?) Ok(vec![]) } diff --git a/src/compressors/tomemory.rs b/src/compressors/tomemory.rs new file mode 100644 index 000000000..66db3eac4 --- /dev/null +++ b/src/compressors/tomemory.rs @@ -0,0 +1,97 @@ +use std::{fs, io::{self, Read}, path::PathBuf}; + +use colored::Colorize; + +use crate::{error::{Error, OuchResult}, extension::CompressionFormat, file::File}; +use crate::utils::ensure_exists; + +use super::{Compressor, Entry}; + +pub struct GzipCompressor {} + +struct CompressorToMemory {} + +impl CompressorToMemory { + pub fn compress_files(files: Vec, format: CompressionFormat) -> OuchResult> { + + + if files.len() != 1 { + eprintln!("{}: cannot compress multiple files directly to {:#?}.\n Try using an intermediate archival method such as Tar.\n Example: filename.tar{}", "error".red(), format, format); + return Err(Error::InvalidInput); + } + + let mut contents = Vec::new(); + let path = &files[0]; + ensure_exists(path)?; + + let bytes_written = { + let bytes = fs::read(path)?; + + // let mut buffer = vec![]; + // let mut encoder = get_encoder(&format, Box::new(&mut buffer)); + // encoder.write_all(&*bytes)?; + // bytes.as_slice().read_to_end(&mut contents)? + Self::compress_bytes(&mut contents, bytes, format)? + }; + + println!( + "{}: compressed {:?} into memory ({} bytes)", + "info".yellow(), + &path, + bytes_written + ); + + Ok(contents) + } + + pub fn compress_file_in_memory(file: File, format:CompressionFormat ) -> OuchResult> { + let mut compressed_contents = Vec::new(); + let file_contents = match file.contents_in_memory { + Some(bytes) => bytes, + None => { + unreachable!(); + } + }; + + let _bytes_written = Self::compress_bytes(&mut compressed_contents, file_contents, format); + + Ok(compressed_contents) + } + + pub fn compress_bytes(mut contents: &mut Vec, bytes_to_compress: Vec, format: CompressionFormat) -> OuchResult { + let mut buffer = vec![]; + let mut encoder = get_encoder(&format, Box::new(&mut buffer)); + encoder.write_all(&*bytes_to_compress)?; + + Ok(bytes_to_compress.as_slice().read_to_end(&mut contents)?) + } + + +} + +fn get_encoder<'a>( + format: &CompressionFormat, + buffer: Box, +) -> Box { + match format { + CompressionFormat::Gzip => Box::new(flate2::write::GzEncoder::new( + buffer, + flate2::Compression::default(), + )), + _other => unreachable!(), + } +} + +impl Compressor for GzipCompressor { + fn compress(&self, from: Entry) -> OuchResult> { + let format = CompressionFormat::Gzip; + match from { + Entry::Files(files) => Ok( + CompressorToMemory::compress_files(files, format)? + ), + Entry::InMemory(file) => Ok( + CompressorToMemory::compress_file_in_memory(file, format)? + ), + } + } +} \ No newline at end of file diff --git a/src/evaluator.rs b/src/evaluator.rs index 963448b47..b245d8269 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -7,6 +7,7 @@ use crate::compressors::{ Compressor, TarCompressor, ZipCompressor, + GzipCompressor, BzipCompressor, }; @@ -71,6 +72,7 @@ impl Evaluator { CompressionFormat::Tar => Box::new(TarCompressor {}), CompressionFormat::Zip => Box::new(ZipCompressor {}), CompressionFormat::Bzip => Box::new(BzipCompressor {}), + CompressionFormat::Gzip => Box::new(GzipCompressor {}), _other => todo!() }; From f3dd4d9804b1c894783b684e4fed454e83194237 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Wed, 24 Mar 2021 17:21:00 -0300 Subject: [PATCH 34/67] Progress in Lzma compression --- src/compressors/mod.rs | 3 ++- src/compressors/tomemory.rs | 17 +++++++++++++++++ src/evaluator.rs | 3 ++- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/compressors/mod.rs b/src/compressors/mod.rs index 93336d468..ad0ec85c0 100644 --- a/src/compressors/mod.rs +++ b/src/compressors/mod.rs @@ -9,4 +9,5 @@ pub use self::compressor::Entry; pub use self::tar::TarCompressor; pub use self::zip::ZipCompressor; pub use self::bzip::BzipCompressor; -pub use self::tomemory::GzipCompressor; \ No newline at end of file +pub use self::tomemory::GzipCompressor; +pub use self::tomemory::LzmaCompressor; \ No newline at end of file diff --git a/src/compressors/tomemory.rs b/src/compressors/tomemory.rs index 66db3eac4..152d7ee56 100644 --- a/src/compressors/tomemory.rs +++ b/src/compressors/tomemory.rs @@ -8,6 +8,7 @@ use crate::utils::ensure_exists; use super::{Compressor, Entry}; pub struct GzipCompressor {} +pub struct LzmaCompressor {} struct CompressorToMemory {} @@ -78,6 +79,7 @@ fn get_encoder<'a>( buffer, flate2::Compression::default(), )), + CompressionFormat::Lzma => Box::new(xz2::write::XzEncoder::new(buffer, 6)), _other => unreachable!(), } } @@ -94,4 +96,19 @@ impl Compressor for GzipCompressor { ), } } +} + + +impl Compressor for LzmaCompressor { + fn compress(&self, from: Entry) -> OuchResult> { + let format = CompressionFormat::Lzma; + match from { + Entry::Files(files) => Ok( + CompressorToMemory::compress_files(files, format)? + ), + Entry::InMemory(file) => Ok( + CompressorToMemory::compress_file_in_memory(file, format)? + ), + } + } } \ No newline at end of file diff --git a/src/evaluator.rs b/src/evaluator.rs index b245d8269..d87c684d2 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -9,6 +9,7 @@ use crate::compressors::{ ZipCompressor, GzipCompressor, BzipCompressor, + LzmaCompressor }; use crate::decompressors::{ @@ -73,7 +74,7 @@ impl Evaluator { CompressionFormat::Zip => Box::new(ZipCompressor {}), CompressionFormat::Bzip => Box::new(BzipCompressor {}), CompressionFormat::Gzip => Box::new(GzipCompressor {}), - _other => todo!() + CompressionFormat::Lzma => Box::new(LzmaCompressor {}), }; Ok((first_compressor, second_compressor)) From 0b346eee3d7dac6afd23c18cecb9e45323fac7b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Wed, 24 Mar 2021 17:40:38 -0300 Subject: [PATCH 35/67] Fixes Gzip and Lzma compression --- src/compressors/gzip.rs | 72 +++++++++++++++++++++++ src/compressors/lzma.rs | 69 ++++++++++++++++++++++ src/compressors/mod.rs | 7 ++- src/compressors/tar.rs | 4 +- src/compressors/tomemory.rs | 114 ------------------------------------ 5 files changed, 147 insertions(+), 119 deletions(-) create mode 100644 src/compressors/gzip.rs create mode 100644 src/compressors/lzma.rs delete mode 100644 src/compressors/tomemory.rs diff --git a/src/compressors/gzip.rs b/src/compressors/gzip.rs new file mode 100644 index 000000000..135d8cfce --- /dev/null +++ b/src/compressors/gzip.rs @@ -0,0 +1,72 @@ +use std::{fs, io::Write, path::PathBuf}; + +use colored::Colorize; + +use crate::{error::{Error, OuchResult}, extension::CompressionFormat, file::File}; +use crate::utils::ensure_exists; + +use super::{Compressor, Entry}; + +pub struct GzipCompressor {} + +impl GzipCompressor { + pub fn compress_files(files: Vec, format: CompressionFormat) -> OuchResult> { + if files.len() != 1 { + eprintln!("{}: cannot compress multiple files directly to {:#?}.\n Try using an intermediate archival method such as Tar.\n Example: filename.tar{}", "error".red(), format, format); + return Err(Error::InvalidInput); + } + + let path = &files[0]; + ensure_exists(path)?; + + let bytes = { + let bytes = fs::read(path)?; + Self::compress_bytes(bytes)? + }; + + println!( + "{}: compressed {:?} into memory ({} bytes)", + "info".yellow(), + &path, + bytes.len() + ); + + Ok(bytes) + } + + pub fn compress_file_in_memory(file: File) -> OuchResult> { + let file_contents = match file.contents_in_memory { + Some(bytes) => bytes, + None => { + unreachable!(); + } + }; + + Ok(Self::compress_bytes(file_contents)?) + } + + pub fn compress_bytes(bytes_to_compress: Vec) -> OuchResult> { + let buffer = vec![]; + let mut encoder = flate2::write::GzEncoder::new( + buffer, + flate2::Compression::default(), + ); + encoder.write_all(&*bytes_to_compress)?; + + Ok(encoder.finish()?) + } +} + +impl Compressor for GzipCompressor { + fn compress(&self, from: Entry) -> OuchResult> { + let format = CompressionFormat::Gzip; + match from { + Entry::Files(files) => Ok( + Self::compress_files(files, format)? + ), + Entry::InMemory(file) => Ok( + Self::compress_file_in_memory(file)? + ), + } + } +} diff --git a/src/compressors/lzma.rs b/src/compressors/lzma.rs new file mode 100644 index 000000000..8d3722468 --- /dev/null +++ b/src/compressors/lzma.rs @@ -0,0 +1,69 @@ +use std::{fs, io::Write, path::PathBuf}; + +use colored::Colorize; + +use crate::{error::{Error, OuchResult}, extension::CompressionFormat, file::File}; +use crate::utils::ensure_exists; + +use super::{Compressor, Entry}; + +pub struct LzmaCompressor {} + +impl LzmaCompressor { + pub fn compress_files(files: Vec, format: CompressionFormat) -> OuchResult> { + if files.len() != 1 { + eprintln!("{}: cannot compress multiple files directly to {:#?}.\n Try using an intermediate archival method such as Tar.\n Example: filename.tar{}", "error".red(), format, format); + return Err(Error::InvalidInput); + } + + let path = &files[0]; + ensure_exists(path)?; + + let bytes = { + let bytes = fs::read(path)?; + Self::compress_bytes(bytes)? + }; + + println!( + "{}: compressed {:?} into memory ({} bytes)", + "info".yellow(), + &path, + bytes.len() + ); + + Ok(bytes) + } + + pub fn compress_file_in_memory(file: File) -> OuchResult> { + let file_contents = match file.contents_in_memory { + Some(bytes) => bytes, + None => { + unreachable!(); + } + }; + + Ok(Self::compress_bytes(file_contents)?) + } + + pub fn compress_bytes(bytes_to_compress: Vec) -> OuchResult> { + let buffer = vec![]; + let mut encoder = xz2::write::XzEncoder::new(buffer, 6); + encoder.write_all(&*bytes_to_compress)?; + + Ok(encoder.finish()?) + } +} + +impl Compressor for LzmaCompressor { + fn compress(&self, from: Entry) -> OuchResult> { + let format = CompressionFormat::Lzma; + match from { + Entry::Files(files) => Ok( + Self::compress_files(files, format)? + ), + Entry::InMemory(file) => Ok( + Self::compress_file_in_memory(file)? + ), + } + } +} \ No newline at end of file diff --git a/src/compressors/mod.rs b/src/compressors/mod.rs index ad0ec85c0..d8b075ae1 100644 --- a/src/compressors/mod.rs +++ b/src/compressors/mod.rs @@ -1,7 +1,8 @@ mod tar; mod zip; mod bzip; -mod tomemory; +mod gzip; +mod lzma; mod compressor; pub use compressor::Compressor; @@ -9,5 +10,5 @@ pub use self::compressor::Entry; pub use self::tar::TarCompressor; pub use self::zip::ZipCompressor; pub use self::bzip::BzipCompressor; -pub use self::tomemory::GzipCompressor; -pub use self::tomemory::LzmaCompressor; \ No newline at end of file +pub use self::gzip::GzipCompressor; +pub use self::lzma::LzmaCompressor; \ No newline at end of file diff --git a/src/compressors/tar.rs b/src/compressors/tar.rs index fa5717566..8900c16c8 100644 --- a/src/compressors/tar.rs +++ b/src/compressors/tar.rs @@ -1,7 +1,7 @@ use std::{fs, path::PathBuf}; use colored::Colorize; -use tar::{Builder, EntryType, Header}; +use tar::Builder; use walkdir::WalkDir; use crate::{compressors::Compressor, error::{Error, OuchResult}, file::File}; @@ -15,7 +15,7 @@ impl TarCompressor { // TODO: this function does not seem to be working correctly ;/ fn make_archive_from_memory(input: File) -> OuchResult> { - let contents = match input.contents_in_memory { + let _contents = match input.contents_in_memory { Some(bytes) => bytes, None => { eprintln!("{}: reached TarCompressor::make_archive_from_memory without known content.", "internal error".red()); diff --git a/src/compressors/tomemory.rs b/src/compressors/tomemory.rs deleted file mode 100644 index 152d7ee56..000000000 --- a/src/compressors/tomemory.rs +++ /dev/null @@ -1,114 +0,0 @@ -use std::{fs, io::{self, Read}, path::PathBuf}; - -use colored::Colorize; - -use crate::{error::{Error, OuchResult}, extension::CompressionFormat, file::File}; -use crate::utils::ensure_exists; - -use super::{Compressor, Entry}; - -pub struct GzipCompressor {} -pub struct LzmaCompressor {} - -struct CompressorToMemory {} - -impl CompressorToMemory { - pub fn compress_files(files: Vec, format: CompressionFormat) -> OuchResult> { - - - if files.len() != 1 { - eprintln!("{}: cannot compress multiple files directly to {:#?}.\n Try using an intermediate archival method such as Tar.\n Example: filename.tar{}", "error".red(), format, format); - return Err(Error::InvalidInput); - } - - let mut contents = Vec::new(); - let path = &files[0]; - ensure_exists(path)?; - - let bytes_written = { - let bytes = fs::read(path)?; - - // let mut buffer = vec![]; - // let mut encoder = get_encoder(&format, Box::new(&mut buffer)); - // encoder.write_all(&*bytes)?; - // bytes.as_slice().read_to_end(&mut contents)? - Self::compress_bytes(&mut contents, bytes, format)? - }; - - println!( - "{}: compressed {:?} into memory ({} bytes)", - "info".yellow(), - &path, - bytes_written - ); - - Ok(contents) - } - - pub fn compress_file_in_memory(file: File, format:CompressionFormat ) -> OuchResult> { - let mut compressed_contents = Vec::new(); - let file_contents = match file.contents_in_memory { - Some(bytes) => bytes, - None => { - unreachable!(); - } - }; - - let _bytes_written = Self::compress_bytes(&mut compressed_contents, file_contents, format); - - Ok(compressed_contents) - } - - pub fn compress_bytes(mut contents: &mut Vec, bytes_to_compress: Vec, format: CompressionFormat) -> OuchResult { - let mut buffer = vec![]; - let mut encoder = get_encoder(&format, Box::new(&mut buffer)); - encoder.write_all(&*bytes_to_compress)?; - - Ok(bytes_to_compress.as_slice().read_to_end(&mut contents)?) - } - - -} - -fn get_encoder<'a>( - format: &CompressionFormat, - buffer: Box, -) -> Box { - match format { - CompressionFormat::Gzip => Box::new(flate2::write::GzEncoder::new( - buffer, - flate2::Compression::default(), - )), - CompressionFormat::Lzma => Box::new(xz2::write::XzEncoder::new(buffer, 6)), - _other => unreachable!(), - } -} - -impl Compressor for GzipCompressor { - fn compress(&self, from: Entry) -> OuchResult> { - let format = CompressionFormat::Gzip; - match from { - Entry::Files(files) => Ok( - CompressorToMemory::compress_files(files, format)? - ), - Entry::InMemory(file) => Ok( - CompressorToMemory::compress_file_in_memory(file, format)? - ), - } - } -} - - -impl Compressor for LzmaCompressor { - fn compress(&self, from: Entry) -> OuchResult> { - let format = CompressionFormat::Lzma; - match from { - Entry::Files(files) => Ok( - CompressorToMemory::compress_files(files, format)? - ), - Entry::InMemory(file) => Ok( - CompressorToMemory::compress_file_in_memory(file, format)? - ), - } - } -} \ No newline at end of file From 6eee06a51a31377acad650dabd089bdc12b5ca6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Wed, 24 Mar 2021 17:51:59 -0300 Subject: [PATCH 36/67] Bump app version --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 18 +++++++++++++++--- src/cli.rs | 2 +- 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 564aa3ee6..d7b0e3bc7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -189,7 +189,7 @@ dependencies = [ [[package]] name = "ouch" -version = "0.1.0" +version = "0.1.1" dependencies = [ "bzip2 0.4.2", "clap", diff --git a/Cargo.toml b/Cargo.toml index f72c75870..8fe65de77 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ouch" -version = "0.1.0" +version = "0.1.1" authors = ["Vinícius Rodrigues Miguel "] edition = "2018" readme = "README.md" diff --git a/README.md b/README.md index 28b8fc7d4..2c191f361 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ `ouch` infers commands from the extensions of its command-line options. ``` -ouch 0.1.0 +ouch 0.1.1 Vinícius R. Miguel ouch is a unified compression & decompression utility @@ -92,7 +92,19 @@ The last dependency is a recent [Rust](https://www.rust-lang.org/) toolchain. If Once the dependency requirements are met: +* Installing from [Crates.io](https://crates.io) + +```bash +cargo install ouch +``` + +* Cloning and building + ```bash -git clone https://github.com/vrmiguel/jacarex # Clone the repo. -cargo install --path ouch # .. and install it +git clone https://github.com/vrmiguel/ouch +cargo install --path ouch +# or +cd ouch && cargo run --release ``` + +I also recommend stripping the release binary. `ouch`'s release binary (at the time of writing) only takes up a megabyte in space when stripped. diff --git a/src/cli.rs b/src/cli.rs index fbde86509..210730f92 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -27,7 +27,7 @@ pub struct Command { pub fn clap_app<'a, 'b>() -> clap::App<'a, 'b> { clap::App::new("ouch") - .version("0.1.0") + .version("0.1.1") .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. From 9ea18659e5dd638ef1c6a7c08531acd4114d2d2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Wed, 24 Mar 2021 18:35:01 -0300 Subject: [PATCH 37/67] Update README, slightly reduce code repetition --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 6 +++--- src/cli.rs | 2 +- src/compressors/bzip.rs | 7 ++----- src/compressors/gzip.rs | 12 ++++++------ src/compressors/lzma.rs | 12 ++++++------ src/compressors/tar.rs | 18 ++++-------------- src/error.rs | 8 ++------ src/utils.rs | 14 ++++++++++++-- 10 files changed, 38 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d7b0e3bc7..9253985b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -189,7 +189,7 @@ dependencies = [ [[package]] name = "ouch" -version = "0.1.1" +version = "0.1.2" dependencies = [ "bzip2 0.4.2", "clap", diff --git a/Cargo.toml b/Cargo.toml index 8fe65de77..b392b8ddf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ouch" -version = "0.1.1" +version = "0.1.2" authors = ["Vinícius Rodrigues Miguel "] edition = "2018" readme = "README.md" diff --git a/README.md b/README.md index 2c191f361..5cda38f85 100644 --- a/README.md +++ b/README.md @@ -3,17 +3,17 @@ `ouch` is the Obvious Unified Compression (and decompression) Helper. -| Supported formats | .tar | .zip | .tar.{.lz,.gz, .bz} | .zip.{.lz, .gz, .bz, .bz2} | .bz | .gz | .lz, .lzma | +| Supported formats | .tar | .zip | .tar.{.lz*,.gz, .bz} | .zip.{.lz*, .gz, .bz*} | .bz | .gz | .lz, .lzma | |-------------------|------|------|------------------------------|------------------------------|-----|-----|------------| | Decompression | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | -| Compression | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | +| Compression | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ## How does it work? `ouch` infers commands from the extensions of its command-line options. ``` -ouch 0.1.1 +ouch 0.1.2 Vinícius R. Miguel ouch is a unified compression & decompression utility diff --git a/src/cli.rs b/src/cli.rs index 210730f92..fd0fdf647 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -27,7 +27,7 @@ pub struct Command { pub fn clap_app<'a, 'b>() -> clap::App<'a, 'b> { clap::App::new("ouch") - .version("0.1.1") + .version("0.1.2") .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. diff --git a/src/compressors/bzip.rs b/src/compressors/bzip.rs index d34866d15..16dd99f58 100644 --- a/src/compressors/bzip.rs +++ b/src/compressors/bzip.rs @@ -3,7 +3,7 @@ use std::{fs, io::Write, path::PathBuf}; use colored::Colorize; use crate::{error::{Error, OuchResult}, extension::CompressionFormat, file::File}; -use crate::utils::ensure_exists; +use crate::utils::{ensure_exists, check_for_multiple_files}; use super::{Compressor, Entry}; @@ -11,10 +11,7 @@ pub struct BzipCompressor {} impl BzipCompressor { fn compress_files(files: Vec, format: CompressionFormat) -> OuchResult> { - if files.len() != 1 { - eprintln!("{}: cannot compress multiple files directly to {:#?}.\n Try using an intermediate archival method such as Tar.\n Example: filename.tar{}", "error".red(), format, format); - return Err(Error::InvalidInput); - } + check_for_multiple_files(&files, &format)?; let path = &files[0]; ensure_exists(path)?; let contents = { diff --git a/src/compressors/gzip.rs b/src/compressors/gzip.rs index 135d8cfce..09bac815c 100644 --- a/src/compressors/gzip.rs +++ b/src/compressors/gzip.rs @@ -2,8 +2,11 @@ use std::{fs, io::Write, path::PathBuf}; use colored::Colorize; -use crate::{error::{Error, OuchResult}, extension::CompressionFormat, file::File}; -use crate::utils::ensure_exists; +use crate::{error::OuchResult, extension::CompressionFormat, file::File}; +use crate::utils::{ + ensure_exists, + check_for_multiple_files +}; use super::{Compressor, Entry}; @@ -11,10 +14,7 @@ pub struct GzipCompressor {} impl GzipCompressor { pub fn compress_files(files: Vec, format: CompressionFormat) -> OuchResult> { - if files.len() != 1 { - eprintln!("{}: cannot compress multiple files directly to {:#?}.\n Try using an intermediate archival method such as Tar.\n Example: filename.tar{}", "error".red(), format, format); - return Err(Error::InvalidInput); - } + check_for_multiple_files(&files, &format)?; let path = &files[0]; ensure_exists(path)?; diff --git a/src/compressors/lzma.rs b/src/compressors/lzma.rs index 8d3722468..6c70c7cf8 100644 --- a/src/compressors/lzma.rs +++ b/src/compressors/lzma.rs @@ -2,8 +2,11 @@ use std::{fs, io::Write, path::PathBuf}; use colored::Colorize; -use crate::{error::{Error, OuchResult}, extension::CompressionFormat, file::File}; -use crate::utils::ensure_exists; +use crate::{error::{OuchResult}, extension::CompressionFormat, file::File}; +use crate::utils::{ + ensure_exists, + check_for_multiple_files +}; use super::{Compressor, Entry}; @@ -11,10 +14,7 @@ pub struct LzmaCompressor {} impl LzmaCompressor { pub fn compress_files(files: Vec, format: CompressionFormat) -> OuchResult> { - if files.len() != 1 { - eprintln!("{}: cannot compress multiple files directly to {:#?}.\n Try using an intermediate archival method such as Tar.\n Example: filename.tar{}", "error".red(), format, format); - return Err(Error::InvalidInput); - } + check_for_multiple_files(&files, &format)?; let path = &files[0]; ensure_exists(path)?; diff --git a/src/compressors/tar.rs b/src/compressors/tar.rs index 8900c16c8..b2ed02f83 100644 --- a/src/compressors/tar.rs +++ b/src/compressors/tar.rs @@ -12,20 +12,10 @@ pub struct TarCompressor {} impl TarCompressor { - // TODO: this function does not seem to be working correctly ;/ - fn make_archive_from_memory(input: File) -> OuchResult> { - - let _contents = match input.contents_in_memory { - Some(bytes) => bytes, - None => { - eprintln!("{}: reached TarCompressor::make_archive_from_memory without known content.", "internal error".red()); - return Err(Error::InvalidInput); - } - }; - - println!("todo"); - - Ok(vec![]) + // TODO: implement this + fn make_archive_from_memory(_input: File) -> OuchResult> { + println!("{}: .tar.tar and .zip.tar is currently unimplemented.", "error".red()); + Err(Error::InvalidZipArchive("")) } fn make_archive_from_files(input_filenames: Vec) -> OuchResult> { diff --git a/src/error.rs b/src/error.rs index c3b37bc42..2d6e9d070 100644 --- a/src/error.rs +++ b/src/error.rs @@ -24,10 +24,6 @@ impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { - Error::InvalidInput => write!( - f, - "When `-o/--output` is omitted, all input files should be compressed files." - ), Error::MissingExtensionError(filename) => { write!(f, "cannot compress to \'{}\', likely because it has an unsupported (or missing) extension.", filename) }, @@ -38,9 +34,9 @@ impl fmt::Display for Error { Error::FileNotFound => { write!(f, "file not found!") } - err => { + _err => { // TODO - write!(f, "todo: missing description for error {:?}", err) + write!(f, "") } } } diff --git a/src/utils.rs b/src/utils.rs index bb907698c..9412e6c51 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,7 +1,7 @@ -use std::{fs, path::Path}; +use std::{fs, path::{Path, PathBuf}}; use colored::Colorize; -use crate::{error::OuchResult, file::File}; +use crate::{error::{Error, OuchResult}, extension::CompressionFormat, file::File}; pub (crate) fn ensure_exists<'a, P>(path: P) -> OuchResult<()> where @@ -13,6 +13,16 @@ where Ok(()) } +pub (crate) fn check_for_multiple_files(files: &Vec, format: &CompressionFormat) -> OuchResult<()> { + + if files.len() != 1 { + eprintln!("{}: cannot compress multiple files directly to {:#?}.\n Try using an intermediate archival method such as Tar.\n Example: filename.tar{}", "error".red(), format, format); + return Err(Error::InvalidInput); + } + + Ok(()) +} + pub (crate) fn create_path_if_non_existent(path: &Path) -> OuchResult<()> { if !path.exists() { println!( From b343e7872c3a4f64b20b83c800df56d00c2d3e78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Miguel?= <36349314+vrmiguel@users.noreply.github.com> Date: Wed, 24 Mar 2021 19:04:14 -0300 Subject: [PATCH 38/67] Update README.md --- README.md | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5cda38f85..001f0a1a4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ouch (_work in progress_) -`ouch` is the Obvious Unified Compression (and decompression) Helper. +`ouch` is the Obvious Unified Compression (_and decompression_) Helper. | Supported formats | .tar | .zip | .tar.{.lz*,.gz, .bz} | .zip.{.lz*, .gz, .bz*} | .bz | .gz | .lz, .lzma | @@ -37,22 +37,24 @@ OPTIONS: $ ouch -i file{1..5}.zip another_file.tar.gz yet_another_file.tar.bz ``` -When no output file is supplied, `ouch` infers that it must decompress all of its input files. This will error if any of the input files are not decompressible. +When no output file is supplied, `ouch` infers that it must decompress all of its input files into the current folder. This will error if any of the input files are not decompressible. #### Decompressing a bunch of files into a folder ```bash -$ ouch -i file{1..5}.tar.gz -o some-folder -# Decompresses file1.tar.gz, file2.tar.gz, file3.tar.gz, file4.tar.gz and file5.tar.gz to some-folder +$ ouch -i file{1..3}.tar.gz videos.tar.bz2 -o some-folder +# Decompresses file1.tar.gz, file2.tar.gz, file3.tar.gz and videos.tar.bz2 to some-folder # The folder `ouch` saves to will be created if it doesn't already exist ``` -When the output file is not a compressed file, `ouch` will check if all input files are decompressible and infer that it must decompress them into the output file. +When the output file is not a compressed file, `ouch` will check if all input files are decompressible and infer that it must decompress them into the output folder. #### Compressing files ```bash $ ouch -i file{1..20} -o archive.tar +$ ouch -i Videos/ Movies/ -o media.tar.lzma +$ ouch -i src/ Cargo.toml Cargo.lock -o my_project.tar.gz ``` ### Error scenarios @@ -66,6 +68,15 @@ error: file 'some-file' is not decompressible. `ouch` cannot infer `some-file`'s compression format since it lacks an extension. Likewise, `ouch` cannot infer that the output file given is a compressed file, so it shows the user an error. +```bash +$ ouch -i file other-file -o files.gz +error: cannot compress multiple files directly to Gzip. + Try using an intermediate archival method such as Tar. + Example: filename.tar.gz +``` + +Similar errors are shown if the same scenario is applied to `.lz/.lzma` and `.bz/.bz2`. + ## Installation ### Runtime dependencies @@ -108,3 +119,15 @@ cd ouch && cargo run --release ``` I also recommend stripping the release binary. `ouch`'s release binary (at the time of writing) only takes up a megabyte in space when stripped. + +## Supported operating systems + +`ouch` _should_ be cross-platform but is currently only tested (and developed) on Linux, on both x64-64 and ARM. + +## Limitations + +`ouch` does encoding and decoding in-memory, so decompressing very large files with `ouch` is not advisable. + +## Contributions + +Any contributions and suggestions are welcome! From 965041310c920f037376cd8048d1cdeea08b651a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Thu, 25 Mar 2021 03:20:20 -0300 Subject: [PATCH 39/67] Make ouch support paths with dot-dot (..) for input files/directories --- src/cli.rs | 45 ++++++++++++++++++++++++++++-------------- src/compressors/tar.rs | 17 ++++++++++++---- src/error.rs | 37 +++++++++++----------------------- src/main.rs | 2 +- src/utils.rs | 9 +++++---- 5 files changed, 60 insertions(+), 50 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index fd0fdf647..8d355e25f 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,7 +1,8 @@ -use std::{convert::TryFrom, path::PathBuf, vec::Vec}; +use std::{convert::TryFrom, fs, path::{Path, PathBuf}, vec::Vec}; use clap::{Arg, Values}; -// use colored::Colorize; +use colored::Colorize; +use error::Error; use crate::error; use crate::extension::Extension; @@ -10,11 +11,11 @@ use crate::file::File; #[derive(PartialEq, Eq, Debug)] pub enum CommandKind { Compression( - // Files to be compressed + /// Files to be compressed Vec, ), Decompression( - // Files to be decompressed and their extensions + /// Files to be decompressed and their extensions Vec, ), } @@ -66,23 +67,30 @@ pub fn get_matches() -> clap::ArgMatches<'static> { clap_app().get_matches() } -// holy spaghetti code impl TryFrom> for Command { type Error = error::Error; fn try_from(matches: clap::ArgMatches<'static>) -> error::OuchResult { let process_decompressible_input = |input_files: Values| { let input_files = - input_files.map(|filename| (filename, Extension::new(filename))); + input_files.map(|filename| (Path::new(filename), Extension::new(filename))); for file in input_files.clone() { - if let (file, Err(_)) = file { - return Err(error::Error::InputsMustHaveBeenDecompressible(file.into())); + match file { + (filename, Ok(_)) => { + let path = Path::new(filename); + if !path.exists() { + return Err(error::Error::FileNotFound(filename.into())) + } + }, + (filename, Err(_)) => { + return Err(error::Error::InputsMustHaveBeenDecompressible(filename.into())); + } } } Ok(input_files - .map(|(filename, extension)| (PathBuf::from(filename), extension.unwrap())) + .map(|(filename, extension)| (fs::canonicalize(filename).unwrap(), extension.unwrap())) .map(File::from) .collect::>()) }; @@ -104,13 +112,20 @@ impl TryFrom> for Command { if output_is_compressible { // The supplied output is compressible, so we'll compress our inputs to it - // println!( - // "{}: trying to compress input files into '{}'", - // "info".yellow(), - // output_file - // ); + 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(Error::FileNotFound(path)) + } + + eprintln!("{} {}", "[ERROR]".red(), err); + return Err(Error::IOError); + } + } - let input_files = input_files.map(PathBuf::from).collect(); + let input_files = canonical_paths.map(Result::unwrap).collect(); return Ok(Command { kind: CommandKind::Compression(input_files), diff --git a/src/compressors/tar.rs b/src/compressors/tar.rs index b2ed02f83..945f3d3ef 100644 --- a/src/compressors/tar.rs +++ b/src/compressors/tar.rs @@ -1,4 +1,4 @@ -use std::{fs, path::PathBuf}; +use std::{env, fs, path::PathBuf}; use colored::Colorize; use tar::Builder; @@ -19,13 +19,21 @@ impl TarCompressor { } fn make_archive_from_files(input_filenames: Vec) -> OuchResult> { - + + let change_dir_and_return_parent = |filename: &PathBuf| -> OuchResult { + let previous_location = env::current_dir()?; + let parent = filename.parent().unwrap(); + env::set_current_dir(parent)?; + Ok(previous_location) + }; + let buf = Vec::new(); let mut b = Builder::new(buf); for filename in input_filenames { - // TODO: check if file exists - + let previous_location = change_dir_and_return_parent(&filename)?; + // Safe unwrap since this filename came from `fs::canonicalize`. + let filename = filename.file_name().unwrap(); for entry in WalkDir::new(&filename) { let entry = entry?; let path = entry.path(); @@ -34,6 +42,7 @@ impl TarCompressor { } b.append_file(path, &mut fs::File::open(path)?)?; } + env::set_current_dir(previous_location)?; } Ok(b.into_inner()?) diff --git a/src/error.rs b/src/error.rs index 2d6e9d070..f1b99edba 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,4 @@ -use std::fmt; +use std::{fmt, path::PathBuf}; use colored::Colorize; @@ -10,29 +10,29 @@ pub enum Error { InvalidUnicode, InvalidInput, IOError, - FileNotFound, + FileNotFound(PathBuf), AlreadyExists, InvalidZipArchive(&'static str), PermissionDenied, UnsupportedZipArchive(&'static str), - InputsMustHaveBeenDecompressible(String), + InputsMustHaveBeenDecompressible(PathBuf), } pub type OuchResult = Result; impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - + write!(f, "{} ", "[ERROR]".red())?; match self { Error::MissingExtensionError(filename) => { write!(f, "cannot compress to \'{}\', likely because it has an unsupported (or missing) extension.", filename) }, Error::InputsMustHaveBeenDecompressible(file) => { - write!(f, "file '{}' is not decompressible", file.red()) + write!(f, "file '{:?}' is not decompressible", file) }, - // TODO: find out a way to attach the missing file in question here - Error::FileNotFound => { - write!(f, "file not found!") + Error::FileNotFound(file) => { + // TODO: check if file == "" + write!(f, "file {:?} not found!", file) } _err => { // TODO @@ -45,11 +45,11 @@ 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, + std::io::ErrorKind::NotFound => Self::FileNotFound("".into()), std::io::ErrorKind::PermissionDenied => Self::PermissionDenied, std::io::ErrorKind::AlreadyExists => Self::AlreadyExists, _other => { - println!("{}: {:#?}", "IO error".red(), err); + println!("{}: {}", "IO error".red(), err); Self::IOError } } @@ -62,27 +62,12 @@ impl From for Error { match err { Io(io_err) => Self::from(io_err), InvalidArchive(filename) => Self::InvalidZipArchive(filename), - FileNotFound => Self::FileNotFound, + FileNotFound => Self::FileNotFound("".into()), UnsupportedArchive(filename) => Self::UnsupportedZipArchive(filename) } } } -// impl From for Error { -// fn from(err: niffler::error::Error) -> Self { -// use niffler::error::Error as NifErr; -// match err { -// NifErr::FeatureDisabled => { -// // Ouch is using Niffler with all its features so -// // this should be unreachable. -// unreachable!(); -// }, -// NifErr::FileTooShort => Self::FileTooShort, -// NifErr::IOError(io_err) => Self::from(io_err) -// } -// } -// } - impl From for Error { fn from(err: walkdir::Error) -> Self { eprintln!("{}: {}", "error".red(), err); diff --git a/src/main.rs b/src/main.rs index a00387cea..29408f06e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,7 +15,7 @@ mod decompressors; fn main() -> error::OuchResult<()>{ let print_error = |err| { - println!("{}: {}", "error".red(), err); + println!("{}", err); }; let matches = cli::get_matches(); match cli::Command::try_from(matches) { diff --git a/src/utils.rs b/src/utils.rs index 9412e6c51..5c7225fbe 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -8,7 +8,8 @@ where P: AsRef + 'a { let exists = path.as_ref().exists(); if !exists { - eprintln!("{}: could not find file {:?}", "error".red(), path.as_ref()); + eprintln!("{}: could not find file {:?}", "[ERROR]".red(), path.as_ref()); + return Err(Error::FileNotFound(PathBuf::from(path.as_ref()))); } Ok(()) } @@ -16,7 +17,7 @@ where pub (crate) fn check_for_multiple_files(files: &Vec, format: &CompressionFormat) -> OuchResult<()> { if files.len() != 1 { - eprintln!("{}: cannot compress multiple files directly to {:#?}.\n Try using an intermediate archival method such as Tar.\n Example: filename.tar{}", "error".red(), format, format); + eprintln!("{}: cannot compress multiple files directly to {:#?}.\n Try using an intermediate archival method such as Tar.\n Example: filename.tar{}", "[ERROR]".red(), format, format); return Err(Error::InvalidInput); } @@ -27,13 +28,13 @@ pub (crate) fn create_path_if_non_existent(path: &Path) -> OuchResult<()> { if !path.exists() { println!( "{}: attempting to create folder {:?}.", - "info".yellow(), + "[INFO]".yellow(), &path ); std::fs::create_dir_all(path)?; println!( "{}: directory {:#?} created.", - "info".yellow(), + "[INFO]".yellow(), fs::canonicalize(&path)? ); } From e1da79c2ac51ebf9811b1cdff5ef1b0f7040a873 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=2E=20Bezerra?= Date: Thu, 25 Mar 2021 20:23:00 -0300 Subject: [PATCH 40/67] Added .gitignore --- .gitignore | 12 +- Cargo.lock | 410 ----------------------------------------------------- 2 files changed, 10 insertions(+), 412 deletions(-) delete mode 100644 Cargo.lock diff --git a/.gitignore b/.gitignore index 54b52f61c..767dae236 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,10 @@ -/target -/tests +# Generated by Cargo +# will have compiled files and executables +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index 9253985b7..000000000 --- a/Cargo.lock +++ /dev/null @@ -1,410 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -[[package]] -name = "adler32" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" - -[[package]] -name = "ansi_term" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" -dependencies = [ - "winapi", -] - -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi", - "libc", - "winapi", -] - -[[package]] -name = "bitflags" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" - -[[package]] -name = "byteorder" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" - -[[package]] -name = "bzip2" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42b7c3cbf0fa9c1b82308d57191728ca0256cb821220f4e2fd410a72ade26e3b" -dependencies = [ - "bzip2-sys", - "libc", -] - -[[package]] -name = "bzip2" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abf8012c8a15d5df745fcf258d93e6149dcf102882c8d8702d9cff778eab43a8" -dependencies = [ - "bzip2-sys", - "libc", -] - -[[package]] -name = "bzip2-sys" -version = "0.1.10+1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17fa3d1ac1ca21c5c4e36a97f3c3eb25084576f6fc47bf0139c1123434216c6c" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - -[[package]] -name = "cc" -version = "1.0.67" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" - -[[package]] -name = "cfg-if" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "clap" -version = "2.33.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" -dependencies = [ - "ansi_term", - "atty", - "bitflags", - "strsim", - "textwrap", - "unicode-width", - "vec_map", -] - -[[package]] -name = "colored" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" -dependencies = [ - "atty", - "lazy_static", - "winapi", -] - -[[package]] -name = "crc32fast" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a" -dependencies = [ - "cfg-if 1.0.0", -] - -[[package]] -name = "filetime" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8" -dependencies = [ - "cfg-if 1.0.0", - "libc", - "redox_syscall", - "winapi", -] - -[[package]] -name = "flate2" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cfff41391129e0a856d6d822600b8d71179d46879e310417eb9c762eb178b42" -dependencies = [ - "cfg-if 0.1.10", - "crc32fast", - "libc", - "miniz_oxide", -] - -[[package]] -name = "hermit-abi" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" -dependencies = [ - "libc", -] - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "libc" -version = "0.2.90" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4aede83fc3617411dc6993bc8c70919750c1c257c6ca6a502aed6e0e2394ae" - -[[package]] -name = "lzma-sys" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdb4b7c3eddad11d3af9e86c487607d2d2442d185d848575365c4856ba96d619" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - -[[package]] -name = "miniz_oxide" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" -dependencies = [ - "adler32", -] - -[[package]] -name = "ouch" -version = "0.1.2" -dependencies = [ - "bzip2 0.4.2", - "clap", - "colored", - "flate2", - "tar", - "walkdir", - "xz2", - "zip", -] - -[[package]] -name = "pkg-config" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" - -[[package]] -name = "proc-macro2" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" -dependencies = [ - "unicode-xid", -] - -[[package]] -name = "quote" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "redox_syscall" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9" -dependencies = [ - "bitflags", -] - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "strsim" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" - -[[package]] -name = "syn" -version = "1.0.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fd9d1e9976102a03c542daa2eff1b43f9d72306342f3f8b3ed5fb8908195d6f" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - -[[package]] -name = "tar" -version = "0.4.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0bcfbd6a598361fda270d82469fff3d65089dc33e175c9a131f7b4cd395f228" -dependencies = [ - "filetime", - "libc", - "xattr", -] - -[[package]] -name = "textwrap" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -dependencies = [ - "unicode-width", -] - -[[package]] -name = "thiserror" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "time" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" -dependencies = [ - "libc", - "wasi", - "winapi", -] - -[[package]] -name = "unicode-width" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" - -[[package]] -name = "unicode-xid" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" - -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - -[[package]] -name = "walkdir" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" -dependencies = [ - "same-file", - "winapi", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "xattr" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c" -dependencies = [ - "libc", -] - -[[package]] -name = "xz2" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c179869f34fc7c01830d3ce7ea2086bc3a07e0d35289b667d0a8bf910258926c" -dependencies = [ - "lzma-sys", -] - -[[package]] -name = "zip" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8264fcea9b7a036a4a5103d7153e988dbc2ebbafb34f68a3c2d404b6b82d74b6" -dependencies = [ - "byteorder", - "bzip2 0.3.3", - "crc32fast", - "flate2", - "thiserror", - "time", -] From 755cc2a40d58ed96ef1fe70f9126be060de929e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=2E=20Bezerra?= Date: Thu, 25 Mar 2021 20:50:42 -0300 Subject: [PATCH 41/67] Refactoring and Clippy warnings --- src/cli.rs | 17 +++--- src/compressors/bzip.rs | 6 +-- src/compressors/gzip.rs | 2 +- src/compressors/lzma.rs | 14 ++--- src/compressors/tar.rs | 38 ++++++-------- src/compressors/zip.rs | 10 ++-- src/evaluator.rs | 113 ++++++++++++++++++---------------------- src/main.rs | 36 +++++-------- src/utils.rs | 5 +- 9 files changed, 107 insertions(+), 134 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 8d355e25f..21a68edb1 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -33,7 +33,7 @@ pub fn clap_app<'a, 'b>() -> clap::App<'a, 'b> { .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 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") @@ -107,7 +107,7 @@ impl TryFrom> for Command { 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 @@ -127,14 +127,15 @@ impl TryFrom> for Command { let input_files = canonical_paths.map(Result::unwrap).collect(); - return Ok(Command { + Ok(Command { kind: CommandKind::Compression(input_files), output: Some(File { path: output_file.into(), contents_in_memory: None, + // extension: output_file_extension.ok(), extension: Some(output_file_extension.unwrap()) }), - }); + }) } else { // Output not supplied @@ -142,14 +143,14 @@ impl TryFrom> for Command { let input_files = process_decompressible_input(input_files)?; - return Ok(Command { + 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 @@ -157,10 +158,10 @@ impl TryFrom> for Command { // Case 2: error let input_files = process_decompressible_input(input_files)?; - return Ok(Command { + Ok(Command { kind: CommandKind::Decompression(input_files), output: None, - }); + }) } } } diff --git a/src/compressors/bzip.rs b/src/compressors/bzip.rs index 16dd99f58..e3854bf38 100644 --- a/src/compressors/bzip.rs +++ b/src/compressors/bzip.rs @@ -20,7 +20,7 @@ impl BzipCompressor { }; println!("{}: compressed {:?} into memory ({} bytes)", "info".yellow(), &path, contents.len()); - + Ok(contents) } @@ -34,7 +34,7 @@ impl BzipCompressor { } }; - Ok(Self::compress_bytes(&*bytes)?) + Self::compress_bytes(&*bytes) } fn compress_bytes(bytes: &[u8]) -> OuchResult> { @@ -58,4 +58,4 @@ impl Compressor for BzipCompressor { ), } } -} \ No newline at end of file +} diff --git a/src/compressors/gzip.rs b/src/compressors/gzip.rs index 09bac815c..05419461b 100644 --- a/src/compressors/gzip.rs +++ b/src/compressors/gzip.rs @@ -42,7 +42,7 @@ impl GzipCompressor { } }; - Ok(Self::compress_bytes(file_contents)?) + Self::compress_bytes(file_contents) } pub fn compress_bytes(bytes_to_compress: Vec) -> OuchResult> { diff --git a/src/compressors/lzma.rs b/src/compressors/lzma.rs index 6c70c7cf8..be4387781 100644 --- a/src/compressors/lzma.rs +++ b/src/compressors/lzma.rs @@ -42,9 +42,9 @@ impl LzmaCompressor { } }; - Ok(Self::compress_bytes(file_contents)?) + Self::compress_bytes(file_contents) } - + pub fn compress_bytes(bytes_to_compress: Vec) -> OuchResult> { let buffer = vec![]; let mut encoder = xz2::write::XzEncoder::new(buffer, 6); @@ -58,12 +58,8 @@ impl Compressor for LzmaCompressor { fn compress(&self, from: Entry) -> OuchResult> { let format = CompressionFormat::Lzma; match from { - Entry::Files(files) => Ok( - Self::compress_files(files, format)? - ), - Entry::InMemory(file) => Ok( - Self::compress_file_in_memory(file)? - ), + Entry::Files(files) => Self::compress_files(files, format), + Entry::InMemory(file) => Self::compress_file_in_memory(file), } } -} \ No newline at end of file +} diff --git a/src/compressors/tar.rs b/src/compressors/tar.rs index 945f3d3ef..5088e9750 100644 --- a/src/compressors/tar.rs +++ b/src/compressors/tar.rs @@ -4,32 +4,37 @@ use colored::Colorize; use tar::Builder; use walkdir::WalkDir; -use crate::{compressors::Compressor, error::{Error, OuchResult}, file::File}; +use crate::{ + compressors::Compressor, + error::{Error, OuchResult}, + file::File, +}; use super::compressor::Entry; pub struct TarCompressor {} impl TarCompressor { - // TODO: implement this fn make_archive_from_memory(_input: File) -> OuchResult> { - println!("{}: .tar.tar and .zip.tar is currently unimplemented.", "error".red()); + println!( + "{}: .tar.tar and .zip.tar is currently unimplemented.", + "error".red() + ); Err(Error::InvalidZipArchive("")) } fn make_archive_from_files(input_filenames: Vec) -> OuchResult> { - - let change_dir_and_return_parent = |filename: &PathBuf| -> OuchResult { + let change_dir_and_return_parent = |filename: &PathBuf| -> OuchResult { let previous_location = env::current_dir()?; let parent = filename.parent().unwrap(); env::set_current_dir(parent)?; Ok(previous_location) }; - + let buf = Vec::new(); let mut b = Builder::new(buf); - + for filename in input_filenames { let previous_location = change_dir_and_return_parent(&filename)?; // Safe unwrap since this filename came from `fs::canonicalize`. @@ -44,25 +49,16 @@ impl TarCompressor { } env::set_current_dir(previous_location)?; } - + Ok(b.into_inner()?) } } impl Compressor for TarCompressor { fn compress(&self, from: Entry) -> OuchResult> { - match from { - Entry::Files(filenames) => { - Ok( - Self::make_archive_from_files(filenames)? - ) - }, - Entry::InMemory(file) => { - Ok( - Self::make_archive_from_memory(file)? - ) - } - } + Entry::Files(filenames) => Self::make_archive_from_files(filenames), + Entry::InMemory(file) => Self::make_archive_from_memory(file), + } } -} \ No newline at end of file +} diff --git a/src/compressors/zip.rs b/src/compressors/zip.rs index 4b4dff816..bd851f79c 100644 --- a/src/compressors/zip.rs +++ b/src/compressors/zip.rs @@ -42,10 +42,10 @@ impl ZipCompressor { } }; - writer.write(&*input_bytes)?; + writer.write_all(&*input_bytes)?; + - let bytes = writer.finish().unwrap(); Ok(bytes.into_inner()) @@ -67,14 +67,14 @@ impl ZipCompressor { } writer .start_file( - entry_path.to_string_lossy(), + entry_path.to_string_lossy(), options )?; let file_bytes = std::fs::read(entry.path())?; - writer.write(&*file_bytes)?; + writer.write_all(&*file_bytes)?; } } - + let bytes = writer.finish().unwrap(); diff --git a/src/evaluator.rs b/src/evaluator.rs index d87c684d2..58cd5448e 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -3,29 +3,15 @@ use std::{ffi::OsStr, fs, io::Write, path::PathBuf}; use colored::Colorize; use crate::compressors::{ - Entry, - Compressor, - TarCompressor, - ZipCompressor, - GzipCompressor, - BzipCompressor, - LzmaCompressor + BzipCompressor, Compressor, Entry, GzipCompressor, LzmaCompressor, TarCompressor, ZipCompressor, }; use crate::decompressors::{ - Decompressor, - TarDecompressor, - ZipDecompressor, - GzipDecompressor, - BzipDecompressor, - LzmaDecompressor, - DecompressionResult + BzipDecompressor, DecompressionResult, Decompressor, GzipDecompressor, LzmaDecompressor, + TarDecompressor, ZipDecompressor, }; -use crate::extension::{ - Extension, - CompressionFormat -}; +use crate::extension::{CompressionFormat, Extension}; use crate::cli::{Command, CommandKind}; @@ -41,19 +27,21 @@ impl Evaluator { pub fn get_compressor( file: &File, ) -> error::OuchResult<(Option>, Box)> { - if file.extension.is_none() { - // This block *should* be unreachable - eprintln!( - "{}: reached Evaluator::get_decompressor without known extension.", - "internal error".red() - ); - return Err(error::Error::InvalidInput); - } - let extension = file.extension.clone().unwrap(); - + let extension = match &file.extension { + Some(extension) => extension.clone(), + None => { + // This block *should* be unreachable + eprintln!( + "{}: reached Evaluator::get_decompressor without known extension.", + "internal error".red() + ); + return Err(error::Error::InvalidInput); + } + }; + // Supported first compressors: // .tar and .zip - let first_compressor: Option> = match extension.first_ext { + let first_compressor: Option> = match extension.first_ext { Some(ext) => match ext { CompressionFormat::Tar => Some(Box::new(TarCompressor {})), @@ -70,8 +58,8 @@ impl Evaluator { // Supported second compressors: // any let second_compressor: Box = match extension.second_ext { - CompressionFormat::Tar => Box::new(TarCompressor {}), - CompressionFormat::Zip => Box::new(ZipCompressor {}), + CompressionFormat::Tar => Box::new(TarCompressor {}), + CompressionFormat::Zip => Box::new(ZipCompressor {}), CompressionFormat::Bzip => Box::new(BzipCompressor {}), CompressionFormat::Gzip => Box::new(GzipCompressor {}), CompressionFormat::Lzma => Box::new(LzmaCompressor {}), @@ -83,15 +71,17 @@ impl Evaluator { pub fn get_decompressor( file: &File, ) -> error::OuchResult<(Option>, Box)> { - if file.extension.is_none() { - // This block *should* be unreachable - eprintln!( - "{}: reached Evaluator::get_decompressor without known extension.", - "internal error".red() - ); - return Err(error::Error::InvalidInput); - } - let extension = file.extension.clone().unwrap(); + let extension = match &file.extension { + Some(extension) => extension.clone(), + None => { + // This block *should* be unreachable + eprintln!( + "{}: reached Evaluator::get_decompressor without known extension.", + "internal error".red() + ); + return Err(error::Error::InvalidInput); + } + }; let second_decompressor: Box = match extension.second_ext { CompressionFormat::Tar => Box::new(TarDecompressor {}), @@ -102,8 +92,7 @@ impl Evaluator { CompressionFormat::Lzma => Box::new(LzmaDecompressor {}), - CompressionFormat::Bzip => Box::new(BzipDecompressor {}) - + CompressionFormat::Bzip => Box::new(BzipDecompressor {}), }; let first_decompressor: Option> = match extension.first_ext { @@ -132,7 +121,7 @@ impl Evaluator { let mut filename = file_path .file_stem() - .unwrap_or(output_file_path.as_os_str()); + .unwrap_or_else(|| output_file_path.as_os_str()); if filename == "." { // I believe this is only possible when the supplied inout has a name // of the sort `.tar` or `.zip' and no output has been supplied. @@ -141,16 +130,20 @@ impl Evaluator { let filename = PathBuf::from(filename); - if decompressor.is_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. + // If there is a decompressor to use, we'll create a file in-memory and decompress it + let decompressor = match decompressor { + Some(decompressor) => decompressor, + 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(), filename); - let mut f = fs::File::create(output_file_path.join(filename))?; - f.write_all(&bytes)?; - return Ok(()); - } + let mut f = fs::File::create(output_file_path.join(filename))?; + f.write_all(&bytes)?; + return Ok(()); + } + }; let file = File { path: filename, @@ -158,10 +151,6 @@ impl Evaluator { extension, }; - let decompressor = decompressor.unwrap(); - - // If there is a decompressor to use, we'll create a file in-memory and decompress it - let decompression_result = decompressor.decompress(file, output_file)?; if let DecompressionResult::FileInMemory(_) = decompression_result { // Should not be reachable. @@ -174,7 +163,6 @@ impl Evaluator { fn compress_files(files: Vec, mut output: File) -> error::OuchResult<()> { let (first_compressor, second_compressor) = Self::get_compressor(&output)?; - let output_path = output.path.clone(); let bytes = match first_compressor { @@ -187,17 +175,20 @@ impl Evaluator { entry = Entry::InMemory(output); second_compressor.compress(entry)? - }, + } None => { let entry = Entry::Files(files); second_compressor.compress(entry)? } }; - println!("{}: writing to {:?}. ({} bytes)", "info".yellow(), &output_path, bytes.len()); - fs::write( - output_path, - bytes)?; + println!( + "{}: writing to {:?}. ({} bytes)", + "info".yellow(), + &output_path, + bytes.len() + ); + fs::write(output_path, bytes)?; Ok(()) } diff --git a/src/main.rs b/src/main.rs index 29408f06e..4a6f249d1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,57 +1,47 @@ use std::convert::TryFrom; -use colored::Colorize; - mod cli; mod error; +mod evaluator; mod extension; mod file; mod test; -mod evaluator; mod utils; mod compressors; mod decompressors; -fn main() -> error::OuchResult<()>{ +fn main() -> error::OuchResult<()> { let print_error = |err| { println!("{}", err); }; + let matches = cli::get_matches(); - match cli::Command::try_from(matches) { - Ok(command) => { - match evaluator::Evaluator::evaluate(command) { - Ok(_) => {}, - Err(err) => print_error(err) - } - } - Err(err) => { - print_error(err) - } - } + cli::Command::try_from(matches) + .map(|command| evaluator::Evaluator::evaluate(command).unwrap_or_else(print_error)) + .unwrap_or_else(print_error); Ok(()) } // fn main() -> error::OuchResult<()> { - // use tar::{Builder}; // use walkdir::WalkDir; - +// // let mut b = Builder::new(Vec::new()); - +// // for entry in WalkDir::new("src") { // let entry = entry?; // let mut file = std::fs::File::open(entry.path())?; // b.append_file(entry.path(), &mut file)?; // } - +// // // let mut file = std::fs::File::open("Cargo.toml")?; // // b.append_file("Cargo.toml", &mut file)?; - +// // let bytes = b.into_inner()?; - +// // std::fs::write("daaaaamn.tar", bytes)?; - +// // Ok(()) -// } \ No newline at end of file +// } diff --git a/src/utils.rs b/src/utils.rs index 5c7225fbe..6f37830be 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -14,8 +14,7 @@ where Ok(()) } -pub (crate) fn check_for_multiple_files(files: &Vec, format: &CompressionFormat) -> OuchResult<()> { - +pub (crate) fn check_for_multiple_files(files: &[PathBuf], format: &CompressionFormat) -> OuchResult<()> { if files.len() != 1 { eprintln!("{}: cannot compress multiple files directly to {:#?}.\n Try using an intermediate archival method such as Tar.\n Example: filename.tar{}", "[ERROR]".red(), format, format); return Err(Error::InvalidInput); @@ -51,4 +50,4 @@ pub (crate) fn get_destination_path(dest: &Option) -> &Path { } None => Path::new("."), } -} \ No newline at end of file +} From c14874072d6a6e52d335d420190f50c9615bb43a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Thu, 25 Mar 2021 21:34:43 -0300 Subject: [PATCH 42/67] Add LICENSE file --- LICENSE | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..66a271bd7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2021 Vinícius R. Miguel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + From ceb507fd148916453b364be1fd1bd298e56176b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Thu, 25 Mar 2021 21:43:45 -0300 Subject: [PATCH 43/67] main: Make ouch return 1 upon failure --- src/main.rs | 37 +++++++++++-------------------------- 1 file changed, 11 insertions(+), 26 deletions(-) diff --git a/src/main.rs b/src/main.rs index 4a6f249d1..2189b510c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,37 +11,22 @@ mod utils; mod compressors; mod decompressors; +use evaluator::Evaluator; + fn main() -> error::OuchResult<()> { let print_error = |err| { println!("{}", err); + err }; let matches = cli::get_matches(); - cli::Command::try_from(matches) - .map(|command| evaluator::Evaluator::evaluate(command).unwrap_or_else(print_error)) - .unwrap_or_else(print_error); + let command = match cli::Command::try_from(matches) { + Ok(command) => command, + Err(err) => return Err(print_error(err)) + }; - Ok(()) + match Evaluator::evaluate(command) { + Ok(_) => Ok(()), + Err(err) => Err(print_error(err)) + } } - -// fn main() -> error::OuchResult<()> { -// use tar::{Builder}; -// use walkdir::WalkDir; -// -// let mut b = Builder::new(Vec::new()); -// -// for entry in WalkDir::new("src") { -// let entry = entry?; -// let mut file = std::fs::File::open(entry.path())?; -// b.append_file(entry.path(), &mut file)?; -// } -// -// // let mut file = std::fs::File::open("Cargo.toml")?; -// // b.append_file("Cargo.toml", &mut file)?; -// -// let bytes = b.into_inner()?; -// -// std::fs::write("daaaaamn.tar", bytes)?; -// -// Ok(()) -// } From d99d8e71d330e0a6c07906a471ba3ff3c96e19d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Thu, 25 Mar 2021 22:47:34 -0300 Subject: [PATCH 44/67] Add a sad Python script for Ouch testing --- makeshift_testing.py | 88 ++++++++++++++++++++++++++++++++++++++++++++ src/extension.rs | 4 +- 2 files changed, 90 insertions(+), 2 deletions(-) create mode 100755 makeshift_testing.py diff --git a/makeshift_testing.py b/makeshift_testing.py new file mode 100755 index 000000000..e8130bf71 --- /dev/null +++ b/makeshift_testing.py @@ -0,0 +1,88 @@ +#!/usr/bin/python3 +""" +Little integration testing script while proper integration tests in Rust aren't implemented. +""" + +import magic, os, hashlib + +def make_random_file(): + with open('test-file', 'wb') as fout: + fout.write(os.urandom(2048)) + +def sanity_check_format(format: str): + make_random_file() + md5sum = hashlib.md5(open('test-file', 'rb').read()).hexdigest() + os.system(f"cargo run -- -i test-file -o test-file.{format}") + os.remove('test-file') + os.system(f"cargo run -- -i test-file.{format}") + if md5sum != hashlib.md5(open('test-file', 'rb').read()).hexdigest(): + print("Something went wrong with tar (de)compression.") + os._exit(2) + os.remove('test-file') + + +if __name__ == "__main__": + + # We'll use MIME sniffing through magic numbers to + # verify if ouch is actually outputting the file formats + # that it should + + m = magic.open(magic.MAGIC_MIME) + + try: + os.mkdir("testbuilds") + except OSError: + print ("Could not make testbuilds folder. Exiting.") + os._exit(2) + + os.chdir("testbuilds") + + m.load() + files = [ + "src.tar", + "src.zip", + "src.tar.gz", + "src.tar.bz", + "src.tar.bz2", + "src.tar.lz", + "src.tar.lzma", + ] + + expected_mime_types = [ + "application/x-tar", + "application/zip", + "application/gzip", + "application/x-bzip2", + "application/x-bzip2", + "application/x-xz", + "application/x-xz" + ] + + for file in files: + rv = os.system(f"cargo run -- -i ../src/ -o {file}") + if rv != 0: + print(f"Failed while compressing {file}") + + for (file, expected_mime) in zip(files, expected_mime_types): + if m.file(file) != expected_mime: + print(f"Test failed at file {file}") + os._exit(2) + + for (idx, file) in enumerate(files): + rv = os.system(f"cargo run -- -i {file} -o out{idx}/") + if rv != 0: + print(f"Failed while decompressing {file}") + os._exit(2) + + os.chdir("..") + os.system("rm -rf testbuilds") + + # We'll now verify if ouch is not altering the data it is compressing + # and decompressing + + sanity_check_format("tar") + sanity_check_format("tar.gz") + sanity_check_format("tar.bz") + sanity_check_format("tar.bz2") + sanity_check_format("tar.lz") + sanity_check_format("tar.lzma") \ No newline at end of file diff --git a/src/extension.rs b/src/extension.rs index 49915e1f1..35a43963c 100644 --- a/src/extension.rs +++ b/src/extension.rs @@ -50,7 +50,7 @@ impl Extension { "tar" => Ok(Tar), "gz" => Ok(Gzip), "bz" | "bz2" => Ok(Bzip), - "lz" | "lzma" => Ok(Lzma), + "xz" | "lz" | "lzma" => Ok(Lzma), other => Err(error::Error::UnknownExtensionError(other.into())), }; @@ -109,7 +109,7 @@ fn extension_from_os_str(ext: &OsStr) -> Result "tar" => Ok(Tar), "gz" => Ok(Gzip), "bz" | "bz2" => Ok(Bzip), - "lzma" | "lz" => Ok(Lzma), + "xz" | "lzma" | "lz" => Ok(Lzma), other => Err(error::Error::UnknownExtensionError(other.into())), } } From 71be492cd4ad4ab2dcb9afd8c35ae1f29397dfd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Thu, 25 Mar 2021 23:33:27 -0300 Subject: [PATCH 45/67] Fix CLI tests --- src/test.rs | 75 +++++++++++++++++++++++++---------------------------- 1 file changed, 35 insertions(+), 40 deletions(-) diff --git a/src/test.rs b/src/test.rs index 4e612172f..e3f5f596c 100644 --- a/src/test.rs +++ b/src/test.rs @@ -10,10 +10,20 @@ mod cli { use crate::extension::CompressionFormat::*; use crate::extension::Extension; use crate::file::File; - use std::convert::TryFrom; + use std::{convert::TryFrom, fs, path::Path}; + + // 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) -> OuchResult<()> + where P: AsRef + 'a { + fs::write(path.as_ref(), &[2, 3, 4, 5, 6, 7, 8, 9, 10])?; + Ok(()) + } #[test] fn decompress_files_into_folder() -> OuchResult<()> { + 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)?; @@ -22,7 +32,7 @@ mod cli { Command { kind: Decompression(vec![ File { - path: "file.zip".into(), + path: fs::canonicalize("file.zip")?, contents_in_memory: None, extension: Some(Extension::from(Zip)) } @@ -35,12 +45,16 @@ mod cli { } ); + fs::remove_file("file.zip")?; + Ok(()) } #[test] fn decompress_files() -> OuchResult<()> { - let matches = clap_app().get_matches_from(vec!["ouch", "-i", "file.zip", "file.tar"]); + 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!( @@ -48,12 +62,12 @@ mod cli { Command { kind: Decompression(vec![ File { - path: "file.zip".into(), + path: fs::canonicalize("my-cool-file.zip")?, contents_in_memory: None, extension: Some(Extension::from(Zip)) }, File { - path: "file.tar".into(), + path: fs::canonicalize("file.tar")?, contents_in_memory: None, extension: Some(Extension::from(Tar)) } @@ -62,11 +76,19 @@ mod cli { } ); + fs::remove_file("my-cool-file.zip")?; + fs::remove_file("file.tar")?; + Ok(()) } #[test] fn compress_files() -> OuchResult<()> { + + make_dummy_file("file")?; + make_dummy_file("file2.jpeg")?; + make_dummy_file("file3.ok")?; + let matches = clap_app().get_matches_from(vec![ "ouch", "-i", @@ -82,9 +104,9 @@ mod cli { command_from_matches, Command { kind: Compression(vec![ - "file".into(), - "file2.jpeg".into(), - "file3.ok".into() + fs::canonicalize("file")?, + fs::canonicalize("file2.jpeg")?, + fs::canonicalize("file3.ok")? ]), output: Some( File { @@ -96,6 +118,10 @@ mod cli { } ); + fs::remove_file("file")?; + fs::remove_file("file2.jpeg")?; + fs::remove_file("file3.ok")?; + Ok(()) } } @@ -200,35 +226,4 @@ mod extension_extraction { Ok(()) } -} - -// #[cfg(test)] -// mod evaluator { -// use crate::extension::Extension; -// use crate::error::OuchResult; -// use crate::file::File; -// use crate::evaluator::Evaluator; -// use crate::decompressors::{Decompressor, TarDecompressor, GzipDecompressor}; - -// #[test] -// fn test() -> OuchResult<()> { -// let extension = Extension::new("folder.tar.gz")?; - -// let file = File { -// path: "folder.tar.gz".into(), -// contents_in_memory: None, -// extension: Some(extension), -// }; - -// let (fst, snd) = Evaluator::get_decompressor(&file)?; - -// let fst = fst.unwrap(); - -// assert_eq!( -// fst, -// Some(Box::new(TarDecompressor::{}) -// ); - -// Ok(()) -// } -// } \ No newline at end of file +} \ No newline at end of file From 8f078aeca4edfa57e86b8cec5e5e0eb1248ad923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20R=2E=20Miguel?= Date: Fri, 26 Mar 2021 00:45:37 -0300 Subject: [PATCH 46/67] Add GitHub Actions integration --- .github/workflows/build.yml | 129 ++++++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..946a001f7 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,129 @@ +on: [push, pull_request] + +name: build-and-test + +jobs: + ubuntu: + name: Ubuntu 18.04 + runs-on: ubuntu-18.04 + strategy: + matrix: + rust: + - stable + steps: + - name: Checkout sources + uses: actions/checkout@v2 + + - name: Install toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.rust }} + override: true + + - name: Run cargo build + uses: actions-rs/cargo@v1 + with: + command: build + args: --release + + - name: Run cargo test + uses: actions-rs/cargo@v1 + with: + command: test + + - name: Install dependencies for Python test script + run: | + sudo apt-get update + sudo apt-get install libmagic1 + python3 -m pip install python-magic + + - name: Run test script + run: python3 makeshift_testing.py + + - name: Strip binary + run: strip target/release/ouch + + - name: Upload binary + uses: actions/upload-artifact@v2 + with: + name: 'ouch-ubuntu-18.04-glibc' + path: target/release/ouch + + macos: + name: macOS + runs-on: macos-latest + strategy: + matrix: + rust: + - stable + steps: + - name: Checkout sources + uses: actions/checkout@v2 + + - name: Install toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.rust }} + override: true + + - name: Run cargo build + uses: actions-rs/cargo@v1 + with: + command: build + args: --release + + - name: Run cargo test + uses: actions-rs/cargo@v1 + with: + command: test + + - name: Install dependencies for Python test script + run: | + brew install libmagic + python3 -m pip install python-magic + + - name: Strip binary + run: strip target/release/ouch + + - name: Upload binary + uses: actions/upload-artifact@v2 + with: + name: 'ouch-macOS' + path: target/release/ouch + + - name: Run test script + run: python3 makeshift_testing.py + + windows: + name: Windows Server + runs-on: windows-2019 + strategy: + matrix: + rust: + - stable + steps: + - name: Checkout sources + uses: actions/checkout@v2 + + - name: Install toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.rust }} + override: true + + - name: Run cargo build + uses: actions-rs/cargo@v1 + with: + command: build + args: --release + + - name: Run cargo test + uses: actions-rs/cargo@v1 + with: + command: test + + - name: Upload binary + uses: actions/upload-artifact@v2 + with: + name: 'ouch-windows' + path: target/release/ouch \ No newline at end of file From 3e241f8b082e1e81a9992340386b169922ba677f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Fri, 26 Mar 2021 13:32:19 -0300 Subject: [PATCH 47/67] CI: Start uploading the Windows binary as an artifact --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 946a001f7..bf585ebda 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -126,4 +126,4 @@ jobs: uses: actions/upload-artifact@v2 with: name: 'ouch-windows' - path: target/release/ouch \ No newline at end of file + path: target\release\ouch.exe From ce26246fd42d550dd74e800d11f432ea5200028d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=2E=20Bezerra?= Date: Fri, 26 Mar 2021 13:46:28 -0300 Subject: [PATCH 48/67] Changing the error imports --- src/cli.rs | 39 +++++----- src/compressors/bzip.rs | 34 +++++---- src/compressors/compressor.rs | 8 +- src/compressors/gzip.rs | 35 ++++----- src/compressors/lzma.rs | 22 +++--- src/compressors/mod.rs | 17 ++--- src/compressors/tar.rs | 17 ++--- src/compressors/zip.rs | 31 +++----- src/decompressors/decompressor.rs | 8 +- src/decompressors/mod.rs | 17 ++--- src/decompressors/tar.rs | 28 +++---- src/decompressors/tomemory.rs | 35 ++++----- src/decompressors/zip.rs | 26 +++---- src/error.rs | 11 ++- src/evaluator.rs | 59 ++++++--------- src/extension.rs | 21 +++--- src/file.rs | 5 +- src/main.rs | 26 ++----- src/test.rs | 120 +++++++++++++----------------- src/utils.rs | 40 ++++++---- 20 files changed, 277 insertions(+), 322 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 21a68edb1..bc2fae2f4 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,12 +1,14 @@ -use std::{convert::TryFrom, fs, path::{Path, PathBuf}, vec::Vec}; +use std::{ + convert::TryFrom, + fs, + path::{Path, PathBuf}, + vec::Vec, +}; use clap::{Arg, Values}; use colored::Colorize; -use error::Error; -use crate::error; -use crate::extension::Extension; -use crate::file::File; +use crate::{extension::Extension, file::File}; #[derive(PartialEq, Eq, Debug)] pub enum CommandKind { @@ -68,9 +70,9 @@ pub fn get_matches() -> clap::ArgMatches<'static> { } impl TryFrom> for Command { - type Error = error::Error; + type Error = crate::Error; - fn try_from(matches: clap::ArgMatches<'static>) -> error::OuchResult { + 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))); @@ -80,17 +82,21 @@ impl TryFrom> for Command { (filename, Ok(_)) => { let path = Path::new(filename); if !path.exists() { - return Err(error::Error::FileNotFound(filename.into())) + return Err(crate::Error::FileNotFound(filename.into())); } - }, + } (filename, Err(_)) => { - return Err(error::Error::InputsMustHaveBeenDecompressible(filename.into())); + return Err(crate::Error::InputsMustHaveBeenDecompressible( + filename.into(), + )); } } } Ok(input_files - .map(|(filename, extension)| (fs::canonicalize(filename).unwrap(), extension.unwrap())) + .map(|(filename, extension)| { + (fs::canonicalize(filename).unwrap(), extension.unwrap()) + }) .map(File::from) .collect::>()) }; @@ -117,11 +123,11 @@ impl TryFrom> for Command { if let Err(err) = canonical_path { let path = PathBuf::from(filename); if !path.exists() { - return Err(Error::FileNotFound(path)) + return Err(crate::Error::FileNotFound(path)); } eprintln!("{} {}", "[ERROR]".red(), err); - return Err(Error::IOError); + return Err(crate::Error::IOError); } } @@ -133,10 +139,9 @@ impl TryFrom> for Command { path: output_file.into(), contents_in_memory: None, // extension: output_file_extension.ok(), - extension: Some(output_file_extension.unwrap()) + extension: Some(output_file_extension.unwrap()), }), }) - } else { // Output not supplied // Checking if input files are decompressible @@ -148,8 +153,8 @@ impl TryFrom> for Command { output: Some(File { path: output_file.into(), contents_in_memory: None, - extension: None - }) + extension: None, + }), }) } } else { diff --git a/src/compressors/bzip.rs b/src/compressors/bzip.rs index e3854bf38..cd5e58c6e 100644 --- a/src/compressors/bzip.rs +++ b/src/compressors/bzip.rs @@ -2,15 +2,17 @@ use std::{fs, io::Write, path::PathBuf}; use colored::Colorize; -use crate::{error::{Error, OuchResult}, extension::CompressionFormat, file::File}; -use crate::utils::{ensure_exists, check_for_multiple_files}; - use super::{Compressor, Entry}; +use crate::{ + extension::CompressionFormat, + file::File, + utils::{check_for_multiple_files, ensure_exists}, +}; pub struct BzipCompressor {} impl BzipCompressor { - fn compress_files(files: Vec, format: CompressionFormat) -> OuchResult> { + fn compress_files(files: Vec, format: CompressionFormat) -> crate::Result> { check_for_multiple_files(&files, &format)?; let path = &files[0]; ensure_exists(path)?; @@ -19,43 +21,43 @@ impl BzipCompressor { Self::compress_bytes(&*bytes)? }; - println!("{}: compressed {:?} into memory ({} bytes)", "info".yellow(), &path, contents.len()); + println!( + "{}: compressed {:?} into memory ({} bytes)", + "info".yellow(), + &path, + contents.len() + ); Ok(contents) } - fn compress_file_in_memory(file: File) -> OuchResult> { + fn compress_file_in_memory(file: File) -> crate::Result> { // Ensure that our file has in-memory content let bytes = match file.contents_in_memory { Some(bytes) => bytes, None => { // TODO: error message, - return Err(Error::InvalidInput); + return Err(crate::Error::InvalidInput); } }; Self::compress_bytes(&*bytes) } - fn compress_bytes(bytes: &[u8]) -> OuchResult> { + fn compress_bytes(bytes: &[u8]) -> crate::Result> { let buffer = vec![]; let mut encoder = bzip2::write::BzEncoder::new(buffer, bzip2::Compression::new(6)); encoder.write_all(bytes)?; Ok(encoder.finish()?) } - } // TODO: customizable compression level impl Compressor for BzipCompressor { - fn compress(&self, from: Entry) -> OuchResult> { + fn compress(&self, from: Entry) -> crate::Result> { match from { - Entry::Files(files) => Ok( - Self::compress_files(files, CompressionFormat::Bzip)? - ), - Entry::InMemory(file) => Ok( - Self::compress_file_in_memory(file)? - ), + Entry::Files(files) => Ok(Self::compress_files(files, CompressionFormat::Bzip)?), + Entry::InMemory(file) => Ok(Self::compress_file_in_memory(file)?), } } } diff --git a/src/compressors/compressor.rs b/src/compressors/compressor.rs index 5998bef84..8f885fd3b 100644 --- a/src/compressors/compressor.rs +++ b/src/compressors/compressor.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use crate::{error::OuchResult, file::File}; +use crate::file::File; // pub enum CompressionResult { // ZipArchive(Vec), @@ -10,9 +10,9 @@ use crate::{error::OuchResult, file::File}; pub enum Entry { Files(Vec), - InMemory(File) + InMemory(File), } pub trait Compressor { - fn compress(&self, from: Entry) -> OuchResult>; -} \ No newline at end of file + fn compress(&self, from: Entry) -> crate::Result>; +} diff --git a/src/compressors/gzip.rs b/src/compressors/gzip.rs index 05419461b..5dc0ee273 100644 --- a/src/compressors/gzip.rs +++ b/src/compressors/gzip.rs @@ -2,18 +2,20 @@ use std::{fs, io::Write, path::PathBuf}; use colored::Colorize; -use crate::{error::OuchResult, extension::CompressionFormat, file::File}; -use crate::utils::{ - ensure_exists, - check_for_multiple_files -}; - use super::{Compressor, Entry}; +use crate::{ + extension::CompressionFormat, + file::File, + utils::{check_for_multiple_files, ensure_exists}, +}; pub struct GzipCompressor {} impl GzipCompressor { - pub fn compress_files(files: Vec, format: CompressionFormat) -> OuchResult> { + pub fn compress_files( + files: Vec, + format: CompressionFormat, + ) -> crate::Result> { check_for_multiple_files(&files, &format)?; let path = &files[0]; @@ -34,7 +36,7 @@ impl GzipCompressor { Ok(bytes) } - pub fn compress_file_in_memory(file: File) -> OuchResult> { + pub fn compress_file_in_memory(file: File) -> crate::Result> { let file_contents = match file.contents_in_memory { Some(bytes) => bytes, None => { @@ -45,12 +47,9 @@ impl GzipCompressor { Self::compress_bytes(file_contents) } - pub fn compress_bytes(bytes_to_compress: Vec) -> OuchResult> { + pub fn compress_bytes(bytes_to_compress: Vec) -> crate::Result> { let buffer = vec![]; - let mut encoder = flate2::write::GzEncoder::new( - buffer, - flate2::Compression::default(), - ); + let mut encoder = flate2::write::GzEncoder::new(buffer, flate2::Compression::default()); encoder.write_all(&*bytes_to_compress)?; Ok(encoder.finish()?) @@ -58,15 +57,11 @@ impl GzipCompressor { } impl Compressor for GzipCompressor { - fn compress(&self, from: Entry) -> OuchResult> { + fn compress(&self, from: Entry) -> crate::Result> { let format = CompressionFormat::Gzip; match from { - Entry::Files(files) => Ok( - Self::compress_files(files, format)? - ), - Entry::InMemory(file) => Ok( - Self::compress_file_in_memory(file)? - ), + Entry::Files(files) => Ok(Self::compress_files(files, format)?), + Entry::InMemory(file) => Ok(Self::compress_file_in_memory(file)?), } } } diff --git a/src/compressors/lzma.rs b/src/compressors/lzma.rs index be4387781..f68a19755 100644 --- a/src/compressors/lzma.rs +++ b/src/compressors/lzma.rs @@ -2,18 +2,20 @@ use std::{fs, io::Write, path::PathBuf}; use colored::Colorize; -use crate::{error::{OuchResult}, extension::CompressionFormat, file::File}; -use crate::utils::{ - ensure_exists, - check_for_multiple_files -}; - use super::{Compressor, Entry}; +use crate::{ + extension::CompressionFormat, + file::File, + utils::{check_for_multiple_files, ensure_exists}, +}; pub struct LzmaCompressor {} impl LzmaCompressor { - pub fn compress_files(files: Vec, format: CompressionFormat) -> OuchResult> { + pub fn compress_files( + files: Vec, + format: CompressionFormat, + ) -> crate::Result> { check_for_multiple_files(&files, &format)?; let path = &files[0]; @@ -34,7 +36,7 @@ impl LzmaCompressor { Ok(bytes) } - pub fn compress_file_in_memory(file: File) -> OuchResult> { + pub fn compress_file_in_memory(file: File) -> crate::Result> { let file_contents = match file.contents_in_memory { Some(bytes) => bytes, None => { @@ -45,7 +47,7 @@ impl LzmaCompressor { Self::compress_bytes(file_contents) } - pub fn compress_bytes(bytes_to_compress: Vec) -> OuchResult> { + pub fn compress_bytes(bytes_to_compress: Vec) -> crate::Result> { let buffer = vec![]; let mut encoder = xz2::write::XzEncoder::new(buffer, 6); encoder.write_all(&*bytes_to_compress)?; @@ -55,7 +57,7 @@ impl LzmaCompressor { } impl Compressor for LzmaCompressor { - fn compress(&self, from: Entry) -> OuchResult> { + fn compress(&self, from: Entry) -> crate::Result> { let format = CompressionFormat::Lzma; match from { Entry::Files(files) => Self::compress_files(files, format), diff --git a/src/compressors/mod.rs b/src/compressors/mod.rs index d8b075ae1..f4c73bb60 100644 --- a/src/compressors/mod.rs +++ b/src/compressors/mod.rs @@ -1,14 +1,13 @@ -mod tar; -mod zip; mod bzip; +mod compressor; mod gzip; mod lzma; -mod compressor; +mod tar; +mod zip; pub use compressor::Compressor; -pub use self::compressor::Entry; -pub use self::tar::TarCompressor; -pub use self::zip::ZipCompressor; -pub use self::bzip::BzipCompressor; -pub use self::gzip::GzipCompressor; -pub use self::lzma::LzmaCompressor; \ No newline at end of file + +pub use self::{ + bzip::BzipCompressor, compressor::Entry, gzip::GzipCompressor, lzma::LzmaCompressor, + tar::TarCompressor, zip::ZipCompressor, +}; diff --git a/src/compressors/tar.rs b/src/compressors/tar.rs index 5088e9750..53ccb8715 100644 --- a/src/compressors/tar.rs +++ b/src/compressors/tar.rs @@ -4,28 +4,23 @@ use colored::Colorize; use tar::Builder; use walkdir::WalkDir; -use crate::{ - compressors::Compressor, - error::{Error, OuchResult}, - file::File, -}; - use super::compressor::Entry; +use crate::{compressors::Compressor, file::File}; pub struct TarCompressor {} impl TarCompressor { // TODO: implement this - fn make_archive_from_memory(_input: File) -> OuchResult> { + fn make_archive_from_memory(_input: File) -> crate::Result> { println!( "{}: .tar.tar and .zip.tar is currently unimplemented.", "error".red() ); - Err(Error::InvalidZipArchive("")) + Err(crate::Error::InvalidZipArchive("")) } - fn make_archive_from_files(input_filenames: Vec) -> OuchResult> { - let change_dir_and_return_parent = |filename: &PathBuf| -> OuchResult { + fn make_archive_from_files(input_filenames: Vec) -> crate::Result> { + let change_dir_and_return_parent = |filename: &PathBuf| -> crate::Result { let previous_location = env::current_dir()?; let parent = filename.parent().unwrap(); env::set_current_dir(parent)?; @@ -55,7 +50,7 @@ impl TarCompressor { } impl Compressor for TarCompressor { - fn compress(&self, from: Entry) -> OuchResult> { + fn compress(&self, from: Entry) -> crate::Result> { match from { Entry::Files(filenames) => Self::make_archive_from_files(filenames), Entry::InMemory(file) => Self::make_archive_from_memory(file), diff --git a/src/compressors/zip.rs b/src/compressors/zip.rs index bd851f79c..5a8c55ec4 100644 --- a/src/compressors/zip.rs +++ b/src/compressors/zip.rs @@ -1,20 +1,18 @@ -use std::{io::{Cursor, Write}, path::PathBuf}; +use std::{ + io::{Cursor, Write}, + path::PathBuf, +}; use walkdir::WalkDir; -use crate::{ - compressors::Compressor, - error::{Error, OuchResult}, - file::File, -}; - use super::compressor::Entry; +use crate::{compressors::Compressor, file::File}; pub struct ZipCompressor {} impl ZipCompressor { // TODO: this function does not seem to be working correctly ;/ - fn make_archive_from_memory(input: File) -> OuchResult> { + fn make_archive_from_memory(input: File) -> crate::Result> { let buffer = vec![]; let mut writer = zip::ZipWriter::new(std::io::Cursor::new(buffer)); @@ -23,7 +21,7 @@ impl ZipCompressor { .file_stem() .ok_or( // TODO: Is this reachable? - Error::InvalidInput + crate::Error::InvalidInput, )? .to_string_lossy() .into(); @@ -38,20 +36,18 @@ impl ZipCompressor { None => { // TODO: error description, although this block should not be // reachable - return Err(Error::InvalidInput); + return Err(crate::Error::InvalidInput); } }; writer.write_all(&*input_bytes)?; - - let bytes = writer.finish().unwrap(); Ok(bytes.into_inner()) } - fn make_archive_from_files(input_filenames: Vec) -> OuchResult> { + fn make_archive_from_files(input_filenames: Vec) -> crate::Result> { let buffer = vec![]; let mut writer = zip::ZipWriter::new(Cursor::new(buffer)); @@ -65,17 +61,12 @@ impl ZipCompressor { if entry_path.is_dir() { continue; } - writer - .start_file( - entry_path.to_string_lossy(), - options - )?; + writer.start_file(entry_path.to_string_lossy(), options)?; let file_bytes = std::fs::read(entry.path())?; writer.write_all(&*file_bytes)?; } } - let bytes = writer.finish().unwrap(); Ok(bytes.into_inner()) @@ -83,7 +74,7 @@ impl ZipCompressor { } impl Compressor for ZipCompressor { - fn compress(&self, from: Entry) -> OuchResult> { + fn compress(&self, from: Entry) -> crate::Result> { match from { Entry::Files(filenames) => Ok(Self::make_archive_from_files(filenames)?), Entry::InMemory(file) => Ok(Self::make_archive_from_memory(file)?), diff --git a/src/decompressors/decompressor.rs b/src/decompressors/decompressor.rs index fc20ad2a4..5d2def1ba 100644 --- a/src/decompressors/decompressor.rs +++ b/src/decompressors/decompressor.rs @@ -1,12 +1,12 @@ use std::path::PathBuf; -use crate::{error::OuchResult, file::File}; +use crate::file::File; pub enum DecompressionResult { FilesUnpacked(Vec), - FileInMemory(Vec) + FileInMemory(Vec), } pub trait Decompressor { - fn decompress(&self, from: File, into: &Option) -> OuchResult; -} \ No newline at end of file + fn decompress(&self, from: File, into: &Option) -> crate::Result; +} diff --git a/src/decompressors/mod.rs b/src/decompressors/mod.rs index 4a88a49ad..b3091818e 100644 --- a/src/decompressors/mod.rs +++ b/src/decompressors/mod.rs @@ -1,18 +1,15 @@ mod decompressor; -mod tomemory; mod tar; +mod tomemory; mod zip; - -pub use decompressor::Decompressor; -pub use decompressor::DecompressionResult; - -pub use self::tar::TarDecompressor; -pub use self::zip::ZipDecompressor; +pub use decompressor::{DecompressionResult, Decompressor}; // These decompressors only decompress to memory, // unlike {Tar, Zip}Decompressor which are capable of // decompressing directly to storage -pub use self::tomemory::GzipDecompressor; -pub use self::tomemory::BzipDecompressor; -pub use self::tomemory::LzmaDecompressor; \ No newline at end of file +pub use self::{ + tar::TarDecompressor, + tomemory::{BzipDecompressor, GzipDecompressor, LzmaDecompressor}, + zip::ZipDecompressor, +}; diff --git a/src/decompressors/tar.rs b/src/decompressors/tar.rs index 0c6e2a498..a613b0cbf 100644 --- a/src/decompressors/tar.rs +++ b/src/decompressors/tar.rs @@ -1,27 +1,29 @@ -use std::{fs, io::{Cursor, Read}, path::{Path, PathBuf}}; +use std::{ + fs, + io::{Cursor, Read}, + path::{Path, PathBuf}, +}; use colored::Colorize; use tar::{self, Archive}; -use crate::{error::OuchResult, utils}; -use crate::file::File; - use super::decompressor::{DecompressionResult, Decompressor}; +use crate::{file::File, utils}; #[derive(Debug)] pub struct TarDecompressor {} impl TarDecompressor { - - fn unpack_files(from: File, into: &Path) -> OuchResult> { - - println!("{}: attempting to decompress {:?}", "ouch".bright_blue(), &from.path); + fn unpack_files(from: File, into: &Path) -> crate::Result> { + println!( + "{}: attempting to decompress {:?}", + "ouch".bright_blue(), + &from.path + ); let mut files_unpacked = vec![]; let mut archive: Archive> = match from.contents_in_memory { - Some(bytes) => { - tar::Archive::new(Box::new(Cursor::new(bytes))) - } + Some(bytes) => tar::Archive::new(Box::new(Cursor::new(bytes))), None => { let file = fs::File::open(&from.path)?; tar::Archive::new(Box::new(file)) @@ -50,7 +52,7 @@ impl TarDecompressor { } impl Decompressor for TarDecompressor { - fn decompress(&self, from: File, into: &Option) -> OuchResult { + fn decompress(&self, from: File, into: &Option) -> crate::Result { let destination_path = utils::get_destination_path(into); utils::create_path_if_non_existent(destination_path)?; @@ -59,4 +61,4 @@ impl Decompressor for TarDecompressor { Ok(DecompressionResult::FilesUnpacked(files_unpacked)) } -} \ No newline at end of file +} diff --git a/src/decompressors/tomemory.rs b/src/decompressors/tomemory.rs index faf2a57b8..81a0e27bf 100644 --- a/src/decompressors/tomemory.rs +++ b/src/decompressors/tomemory.rs @@ -3,35 +3,32 @@ use std::{ path::Path, }; - use colored::Colorize; -// use niffler; +use super::decompressor::{DecompressionResult, Decompressor}; +use crate::utils; +// use niffler; use crate::{extension::CompressionFormat, file::File}; -use crate::{ - error::OuchResult, - utils, -}; - -use super::decompressor::DecompressionResult; -use super::decompressor::Decompressor; struct DecompressorToMemory {} pub struct GzipDecompressor {} pub struct LzmaDecompressor {} pub struct BzipDecompressor {} -fn get_decoder<'a>(format: CompressionFormat, buffer: Box) -> Box { +fn get_decoder<'a>( + format: CompressionFormat, + buffer: Box, +) -> Box { match format { CompressionFormat::Bzip => Box::new(bzip2::read::BzDecoder::new(buffer)), CompressionFormat::Gzip => Box::new(flate2::read::MultiGzDecoder::new(buffer)), CompressionFormat::Lzma => Box::new(xz2::read::XzDecoder::new_multi_decoder(buffer)), - _other => unreachable!() + _other => unreachable!(), } } impl DecompressorToMemory { - fn unpack_file(from: &Path, format: CompressionFormat) -> OuchResult> { + fn unpack_file(from: &Path, format: CompressionFormat) -> crate::Result> { let file = std::fs::read(from)?; let mut reader = get_decoder(format, Box::new(&file[..])); @@ -49,7 +46,11 @@ impl DecompressorToMemory { Ok(buffer) } - fn decompress(from: File, format: CompressionFormat, into: &Option) -> OuchResult { + fn decompress( + from: File, + format: CompressionFormat, + into: &Option, + ) -> crate::Result { let destination_path = utils::get_destination_path(into); utils::create_path_if_non_existent(destination_path)?; @@ -61,19 +62,19 @@ impl DecompressorToMemory { } impl Decompressor for GzipDecompressor { - fn decompress(&self, from: File, into: &Option) -> OuchResult { + fn decompress(&self, from: File, into: &Option) -> crate::Result { DecompressorToMemory::decompress(from, CompressionFormat::Gzip, into) } } impl Decompressor for BzipDecompressor { - fn decompress(&self, from: File, into: &Option) -> OuchResult { + fn decompress(&self, from: File, into: &Option) -> crate::Result { DecompressorToMemory::decompress(from, CompressionFormat::Bzip, into) } } impl Decompressor for LzmaDecompressor { - fn decompress(&self, from: File, into: &Option) -> OuchResult { + fn decompress(&self, from: File, into: &Option) -> crate::Result { DecompressorToMemory::decompress(from, CompressionFormat::Lzma, into) } -} \ No newline at end of file +} diff --git a/src/decompressors/zip.rs b/src/decompressors/zip.rs index 4e5bc5ed5..1882248b3 100644 --- a/src/decompressors/zip.rs +++ b/src/decompressors/zip.rs @@ -1,12 +1,14 @@ -use std::{fs, io::{self, Cursor, Read, Seek}, path::{Path, PathBuf}}; +use std::{ + fs, + io::{self, Cursor, Read, Seek}, + path::{Path, PathBuf}, +}; use colored::Colorize; -use zip::{self, ZipArchive, read::ZipFile}; - -use crate::{error, file::File}; -use crate::{error::OuchResult, utils}; +use zip::{self, read::ZipFile, ZipArchive}; use super::decompressor::{DecompressionResult, Decompressor}; +use crate::{file::File, utils}; pub struct ZipDecompressor {} @@ -26,7 +28,7 @@ impl ZipDecompressor { pub fn zip_decompress( archive: &mut ZipArchive, into: &Path, - ) -> error::OuchResult> + ) -> crate::Result> where T: Read + Seek, { @@ -72,33 +74,29 @@ impl ZipDecompressor { Ok(unpacked_files) } - fn unpack_files(from: File, into: &Path) -> OuchResult> { - + fn unpack_files(from: File, into: &Path) -> crate::Result> { println!( "{}: attempting to decompress {:?}", "ouch".bright_blue(), &from.path ); - + match from.contents_in_memory { Some(bytes) => { let mut archive = zip::ZipArchive::new(Cursor::new(bytes))?; Ok(Self::zip_decompress(&mut archive, into)?) - }, + } None => { let file = fs::File::open(&from.path)?; let mut archive = zip::ZipArchive::new(file)?; Ok(Self::zip_decompress(&mut archive, into)?) } } - - - } } impl Decompressor for ZipDecompressor { - fn decompress(&self, from: File, into: &Option) -> OuchResult { + fn decompress(&self, from: File, into: &Option) -> crate::Result { let destination_path = utils::get_destination_path(into); utils::create_path_if_non_existent(destination_path)?; diff --git a/src/error.rs b/src/error.rs index f1b99edba..19a7ea30f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -18,7 +18,7 @@ pub enum Error { InputsMustHaveBeenDecompressible(PathBuf), } -pub type OuchResult = Result; +pub type Result = std::result::Result; impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { @@ -26,10 +26,10 @@ impl fmt::Display for Error { match self { Error::MissingExtensionError(filename) => { write!(f, "cannot compress to \'{}\', likely because it has an unsupported (or missing) extension.", filename) - }, + } Error::InputsMustHaveBeenDecompressible(file) => { write!(f, "file '{:?}' is not decompressible", file) - }, + } Error::FileNotFound(file) => { // TODO: check if file == "" write!(f, "file {:?} not found!", file) @@ -63,7 +63,7 @@ impl From for Error { Io(io_err) => Self::from(io_err), InvalidArchive(filename) => Self::InvalidZipArchive(filename), FileNotFound => Self::FileNotFound("".into()), - UnsupportedArchive(filename) => Self::UnsupportedZipArchive(filename) + UnsupportedArchive(filename) => Self::UnsupportedZipArchive(filename), } } } @@ -71,7 +71,6 @@ impl From for Error { impl From for Error { fn from(err: walkdir::Error) -> Self { eprintln!("{}: {}", "error".red(), err); - Self::InvalidInput } -} \ No newline at end of file +} diff --git a/src/evaluator.rs b/src/evaluator.rs index 58cd5448e..8e923b143 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -2,31 +2,27 @@ use std::{ffi::OsStr, fs, io::Write, path::PathBuf}; use colored::Colorize; -use crate::compressors::{ - BzipCompressor, Compressor, Entry, GzipCompressor, LzmaCompressor, TarCompressor, ZipCompressor, +use crate::{ + cli::{Command, CommandKind}, + compressors::{ + BzipCompressor, Compressor, Entry, GzipCompressor, LzmaCompressor, TarCompressor, + ZipCompressor, + }, + decompressors::{ + BzipDecompressor, DecompressionResult, Decompressor, GzipDecompressor, LzmaDecompressor, + TarDecompressor, ZipDecompressor, + }, + extension::{CompressionFormat, Extension}, + file::File, + utils, }; -use crate::decompressors::{ - BzipDecompressor, DecompressionResult, Decompressor, GzipDecompressor, LzmaDecompressor, - TarDecompressor, ZipDecompressor, -}; - -use crate::extension::{CompressionFormat, Extension}; - -use crate::cli::{Command, CommandKind}; - -use crate::error::{self, OuchResult}; - -use crate::file::File; - -use crate::utils; - pub struct Evaluator {} impl Evaluator { pub fn get_compressor( file: &File, - ) -> error::OuchResult<(Option>, Box)> { + ) -> crate::Result<(Option>, Box)> { let extension = match &file.extension { Some(extension) => extension.clone(), None => { @@ -35,7 +31,7 @@ impl Evaluator { "{}: reached Evaluator::get_decompressor without known extension.", "internal error".red() ); - return Err(error::Error::InvalidInput); + return Err(crate::Error::InvalidInput); } }; @@ -44,9 +40,7 @@ impl Evaluator { let first_compressor: Option> = match extension.first_ext { Some(ext) => match ext { CompressionFormat::Tar => Some(Box::new(TarCompressor {})), - CompressionFormat::Zip => Some(Box::new(ZipCompressor {})), - // _other => Some(Box::new(NifflerCompressor {})), _other => { todo!(); @@ -70,7 +64,7 @@ impl Evaluator { pub fn get_decompressor( file: &File, - ) -> error::OuchResult<(Option>, Box)> { + ) -> crate::Result<(Option>, Box)> { let extension = match &file.extension { Some(extension) => extension.clone(), None => { @@ -79,28 +73,22 @@ impl Evaluator { "{}: reached Evaluator::get_decompressor without known extension.", "internal error".red() ); - return Err(error::Error::InvalidInput); + return Err(crate::Error::InvalidInput); } }; let second_decompressor: Box = match extension.second_ext { CompressionFormat::Tar => Box::new(TarDecompressor {}), - CompressionFormat::Zip => Box::new(ZipDecompressor {}), - CompressionFormat::Gzip => Box::new(GzipDecompressor {}), - CompressionFormat::Lzma => Box::new(LzmaDecompressor {}), - CompressionFormat::Bzip => Box::new(BzipDecompressor {}), }; let first_decompressor: Option> = match extension.first_ext { Some(ext) => match ext { CompressionFormat::Tar => Some(Box::new(TarDecompressor {})), - CompressionFormat::Zip => Some(Box::new(ZipDecompressor {})), - _other => None, }, None => None, @@ -116,18 +104,18 @@ impl Evaluator { decompressor: Option>, output_file: &Option, extension: Option, - ) -> OuchResult<()> { + ) -> crate::Result<()> { let output_file_path = utils::get_destination_path(output_file); let mut filename = file_path .file_stem() .unwrap_or_else(|| output_file_path.as_os_str()); + if filename == "." { // I believe this is only possible when the supplied inout has a name // of the sort `.tar` or `.zip' and no output has been supplied. filename = OsStr::new("ouch-output"); } - let filename = PathBuf::from(filename); // If there is a decompressor to use, we'll create a file in-memory and decompress it @@ -136,7 +124,6 @@ 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); let mut f = fs::File::create(output_file_path.join(filename))?; @@ -160,7 +147,7 @@ impl Evaluator { Ok(()) } - fn compress_files(files: Vec, mut output: File) -> error::OuchResult<()> { + fn compress_files(files: Vec, mut output: File) -> crate::Result<()> { let (first_compressor, second_compressor) = Self::get_compressor(&output)?; let output_path = output.path.clone(); @@ -171,9 +158,7 @@ impl Evaluator { let bytes = first_compressor.compress(entry)?; output.contents_in_memory = Some(bytes); - entry = Entry::InMemory(output); - second_compressor.compress(entry)? } None => { @@ -193,7 +178,7 @@ impl Evaluator { Ok(()) } - fn decompress_file(file: File, output: &Option) -> error::OuchResult<()> { + fn decompress_file(file: File, output: &Option) -> crate::Result<()> { // let output_file = &command.output; let (first_decompressor, second_decompressor) = Self::get_decompressor(&file)?; @@ -228,7 +213,7 @@ impl Evaluator { Ok(()) } - pub fn evaluate(command: Command) -> error::OuchResult<()> { + pub fn evaluate(command: Command) -> crate::Result<()> { let output = command.output.clone(); match command.kind { diff --git a/src/extension.rs b/src/extension.rs index 35a43963c..0510efcbb 100644 --- a/src/extension.rs +++ b/src/extension.rs @@ -5,7 +5,6 @@ use std::{ path::{Path, PathBuf}, }; -use crate::error; use CompressionFormat::*; /// Represents the extension of a file, but only really caring about @@ -44,14 +43,14 @@ impl From for Extension { } impl Extension { - pub fn new(filename: &str) -> error::OuchResult { + 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(error::Error::UnknownExtensionError(other.into())), + other => Err(crate::Error::UnknownExtensionError(other.into())), }; let (first_ext, second_ext) = match get_extension_from_filename(filename) { @@ -59,7 +58,7 @@ impl Extension { ("", snd) => (None, snd), (fst, snd) => (Some(fst), snd), }, - None => return Err(error::Error::MissingExtensionError(filename.into())), + None => return Err(crate::Error::MissingExtensionError(filename.into())), }; let (first_ext, second_ext) = match (first_ext, second_ext) { @@ -96,12 +95,12 @@ pub enum CompressionFormat { Zip, } -fn extension_from_os_str(ext: &OsStr) -> Result { +fn extension_from_os_str(ext: &OsStr) -> Result { // let ext = Path::new(ext); let ext = match ext.to_str() { Some(str) => str, - None => return Err(error::Error::InvalidUnicode), + None => return Err(crate::Error::InvalidUnicode), }; match ext { @@ -110,18 +109,18 @@ fn extension_from_os_str(ext: &OsStr) -> Result "gz" => Ok(Gzip), "bz" | "bz2" => Ok(Bzip), "xz" | "lzma" | "lz" => Ok(Lzma), - other => Err(error::Error::UnknownExtensionError(other.into())), + other => Err(crate::Error::UnknownExtensionError(other.into())), } } impl TryFrom<&PathBuf> for CompressionFormat { - type Error = error::Error; + type Error = crate::Error; fn try_from(ext: &PathBuf) -> Result { let ext = match ext.extension() { Some(ext) => ext, None => { - return Err(error::Error::MissingExtensionError(String::new())); + return Err(crate::Error::MissingExtensionError(String::new())); } }; extension_from_os_str(ext) @@ -129,13 +128,13 @@ impl TryFrom<&PathBuf> for CompressionFormat { } impl TryFrom<&str> for CompressionFormat { - type Error = error::Error; + type Error = crate::Error; fn try_from(filename: &str) -> Result { let filename = Path::new(filename); let ext = match filename.extension() { Some(ext) => ext, - None => return Err(error::Error::MissingExtensionError(String::new())), + None => return Err(crate::Error::MissingExtensionError(String::new())), }; extension_from_os_str(ext) diff --git a/src/file.rs b/src/file.rs index fb4e634b6..ddb31e516 100644 --- a/src/file.rs +++ b/src/file.rs @@ -2,7 +2,6 @@ use std::path::PathBuf; use crate::extension::Extension; - #[derive(Debug, Clone, PartialEq, Eq)] pub struct File { /// File's (relative) path @@ -15,7 +14,7 @@ pub struct File { /// /// So, for example, if a file has pathname "image.jpeg", it does have a JPEG extension but will /// be represented as a None over here since that's not an extension we're particularly interested in - pub extension: Option + pub extension: Option, } impl From<(PathBuf, Extension)> for File { @@ -26,4 +25,4 @@ impl From<(PathBuf, Extension)> for File { extension: Some(format), } } -} \ No newline at end of file +} diff --git a/src/main.rs b/src/main.rs index 2189b510c..9c333b527 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ -use std::convert::TryFrom; - mod cli; +mod compressors; +mod decompressors; mod error; mod evaluator; mod extension; @@ -8,25 +8,13 @@ mod file; mod test; mod utils; -mod compressors; -mod decompressors; +use std::convert::TryFrom; +pub use error::{Error, Result}; use evaluator::Evaluator; -fn main() -> error::OuchResult<()> { - let print_error = |err| { - println!("{}", err); - err - }; - +fn main() -> crate::Result<()> { let matches = cli::get_matches(); - let command = match cli::Command::try_from(matches) { - Ok(command) => command, - Err(err) => return Err(print_error(err)) - }; - - match Evaluator::evaluate(command) { - Ok(_) => Ok(()), - Err(err) => Err(print_error(err)) - } + let command = cli::Command::try_from(matches)?; + Evaluator::evaluate(command) } diff --git a/src/test.rs b/src/test.rs index e3f5f596c..fb532150d 100644 --- a/src/test.rs +++ b/src/test.rs @@ -2,27 +2,27 @@ #[cfg(test)] mod cli { - - use crate::cli::clap_app; - use crate::cli::Command; - use crate::cli::CommandKind::*; - use crate::error::OuchResult; - use crate::extension::CompressionFormat::*; - use crate::extension::Extension; - use crate::file::File; use std::{convert::TryFrom, fs, path::Path}; + use crate::{ + cli::{clap_app, Command, CommandKind::*}, + extension::{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) -> OuchResult<()> - where P: AsRef + 'a { + 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(()) } #[test] - fn decompress_files_into_folder() -> OuchResult<()> { + 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)?; @@ -30,13 +30,11 @@ mod cli { assert_eq!( command_from_matches, Command { - kind: Decompression(vec![ - File { - path: fs::canonicalize("file.zip")?, - contents_in_memory: None, - extension: Some(Extension::from(Zip)) - } - ]), + 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, @@ -51,22 +49,23 @@ mod cli { } #[test] - fn decompress_files() -> OuchResult<()> { + 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 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: Decompression(vec![ - File { + File { path: fs::canonicalize("my-cool-file.zip")?, contents_in_memory: None, extension: Some(Extension::from(Zip)) }, - File { + File { path: fs::canonicalize("file.tar")?, contents_in_memory: None, extension: Some(Extension::from(Tar)) @@ -83,8 +82,7 @@ mod cli { } #[test] - fn compress_files() -> OuchResult<()> { - + fn compress_files() -> crate::Result<()> { make_dummy_file("file")?; make_dummy_file("file2.jpeg")?; make_dummy_file("file3.ok")?; @@ -108,13 +106,11 @@ mod cli { fs::canonicalize("file2.jpeg")?, fs::canonicalize("file3.ok")? ]), - output: Some( - File { - path: "file.tar".into(), - contents_in_memory: None, - extension: Some(Extension::from(Tar)) - } - ), + output: Some(File { + path: "file.tar".into(), + contents_in_memory: None, + extension: Some(Extension::from(Tar)) + }), } ); @@ -131,20 +127,22 @@ mod cli_errors { use std::convert::TryFrom; - use crate::cli::clap_app; - use crate::cli::Command; - use crate::error::Error; - use crate::error::OuchResult; + use crate::{ + cli::{clap_app, Command}, + error::crate::{Error, Result}, + }; #[test] - fn compress_files() -> OuchResult<()> { + 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(Error::InputsMustHaveBeenDecompressible("a_file".into())) + Err(crate::Error::InputsMustHaveBeenDecompressible( + "a_file".into() + )) ); Ok(()) @@ -153,23 +151,23 @@ mod cli_errors { #[cfg(test)] mod extension_extraction { - use crate::{error::OuchResult, extension::Extension} ; - use crate::extension::CompressionFormat; use std::{convert::TryFrom, path::PathBuf, str::FromStr}; + use crate::{ + error::crate::Result, + extension::{CompressionFormat, Extension}, + }; + #[test] - fn zip() -> OuchResult<()> { + fn zip() -> crate::Result<()> { let path = PathBuf::from_str("filename.tar.zip").unwrap(); - assert_eq!( - CompressionFormat::try_from(&path)?, - CompressionFormat::Zip - ); + assert_eq!(CompressionFormat::try_from(&path)?, CompressionFormat::Zip); Ok(()) } - + #[test] - fn tar_gz() -> OuchResult<()> { + fn tar_gz() -> crate::Result<()> { let extension = Extension::new("folder.tar.gz")?; assert_eq!( @@ -184,46 +182,34 @@ mod extension_extraction { } #[test] - fn tar() -> OuchResult<()> { + fn tar() -> crate::Result<()> { let path = PathBuf::from_str("pictures.tar").unwrap(); - assert_eq!( - CompressionFormat::try_from(&path)?, - CompressionFormat::Tar - ); + assert_eq!(CompressionFormat::try_from(&path)?, CompressionFormat::Tar); Ok(()) } #[test] - fn gz() -> OuchResult<()> { + fn gz() -> crate::Result<()> { let path = PathBuf::from_str("passwords.tar.gz").unwrap(); - assert_eq!( - CompressionFormat::try_from(&path)?, - CompressionFormat::Gzip - ); + assert_eq!(CompressionFormat::try_from(&path)?, CompressionFormat::Gzip); Ok(()) } #[test] - fn lzma() -> OuchResult<()> { + fn lzma() -> crate::Result<()> { let path = PathBuf::from_str("mygame.tar.lzma").unwrap(); - assert_eq!( - CompressionFormat::try_from(&path)?, - CompressionFormat::Lzma - ); + assert_eq!(CompressionFormat::try_from(&path)?, CompressionFormat::Lzma); Ok(()) } #[test] - fn bz() -> OuchResult<()> { + fn bz() -> crate::Result<()> { let path = PathBuf::from_str("songs.tar.bz").unwrap(); - assert_eq!( - CompressionFormat::try_from(&path)?, - CompressionFormat::Bzip - ); + assert_eq!(CompressionFormat::try_from(&path)?, CompressionFormat::Bzip); Ok(()) } -} \ No newline at end of file +} diff --git a/src/utils.rs b/src/utils.rs index 6f37830be..2c8193d51 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,29 +1,41 @@ -use std::{fs, path::{Path, PathBuf}}; +use std::{ + fs, + path::{Path, PathBuf}, +}; use colored::Colorize; -use crate::{error::{Error, OuchResult}, extension::CompressionFormat, file::File}; -pub (crate) fn ensure_exists<'a, P>(path: P) -> OuchResult<()> +use crate::{extension::CompressionFormat, file::File}; + +pub(crate) fn ensure_exists<'a, P>(path: P) -> crate::Result<()> where - P: AsRef + 'a { - let exists = path.as_ref().exists(); - if !exists { - eprintln!("{}: could not find file {:?}", "[ERROR]".red(), path.as_ref()); - return Err(Error::FileNotFound(PathBuf::from(path.as_ref()))); - } - Ok(()) + P: AsRef + 'a, +{ + let exists = path.as_ref().exists(); + if !exists { + eprintln!( + "{}: could not find file {:?}", + "[ERROR]".red(), + path.as_ref() + ); + return Err(crate::Error::FileNotFound(PathBuf::from(path.as_ref()))); } + Ok(()) +} -pub (crate) fn check_for_multiple_files(files: &[PathBuf], format: &CompressionFormat) -> OuchResult<()> { +pub(crate) fn check_for_multiple_files( + files: &[PathBuf], + format: &CompressionFormat, +) -> crate::Result<()> { if files.len() != 1 { eprintln!("{}: cannot compress multiple files directly to {:#?}.\n Try using an intermediate archival method such as Tar.\n Example: filename.tar{}", "[ERROR]".red(), format, format); - return Err(Error::InvalidInput); + return Err(crate::Error::InvalidInput); } Ok(()) } -pub (crate) fn create_path_if_non_existent(path: &Path) -> OuchResult<()> { +pub(crate) fn create_path_if_non_existent(path: &Path) -> crate::Result<()> { if !path.exists() { println!( "{}: attempting to create folder {:?}.", @@ -40,7 +52,7 @@ pub (crate) fn create_path_if_non_existent(path: &Path) -> OuchResult<()> { Ok(()) } -pub (crate) fn get_destination_path(dest: &Option) -> &Path { +pub(crate) fn get_destination_path(dest: &Option) -> &Path { match dest { Some(output) => { // Must be None according to the way command-line arg. parsing in Ouch works From dd9b6dd65f48c1abddf450f73396a2df97840874 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=2E=20Bezerra?= Date: Fri, 26 Mar 2021 13:46:45 -0300 Subject: [PATCH 49/67] Renaming tomemory.rs to to_memory.rs --- src/decompressors/mod.rs | 4 ++-- src/decompressors/{tomemory.rs => to_memory.rs} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename src/decompressors/{tomemory.rs => to_memory.rs} (100%) diff --git a/src/decompressors/mod.rs b/src/decompressors/mod.rs index b3091818e..689109efd 100644 --- a/src/decompressors/mod.rs +++ b/src/decompressors/mod.rs @@ -1,6 +1,6 @@ mod decompressor; mod tar; -mod tomemory; +mod to_memory; mod zip; pub use decompressor::{DecompressionResult, Decompressor}; @@ -10,6 +10,6 @@ pub use decompressor::{DecompressionResult, Decompressor}; // decompressing directly to storage pub use self::{ tar::TarDecompressor, - tomemory::{BzipDecompressor, GzipDecompressor, LzmaDecompressor}, + to_memory::{BzipDecompressor, GzipDecompressor, LzmaDecompressor}, zip::ZipDecompressor, }; diff --git a/src/decompressors/tomemory.rs b/src/decompressors/to_memory.rs similarity index 100% rename from src/decompressors/tomemory.rs rename to src/decompressors/to_memory.rs From 36db7d721d3b047d270b16eacfa8e3f54a4a3166 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Fri, 26 Mar 2021 14:11:13 -0300 Subject: [PATCH 50/67] Fix tests --- src/test.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/test.rs b/src/test.rs index fb532150d..cf35bcf6e 100644 --- a/src/test.rs +++ b/src/test.rs @@ -129,7 +129,6 @@ mod cli_errors { use crate::{ cli::{clap_app, Command}, - error::crate::{Error, Result}, }; #[test] @@ -154,7 +153,6 @@ mod extension_extraction { use std::{convert::TryFrom, path::PathBuf, str::FromStr}; use crate::{ - error::crate::Result, extension::{CompressionFormat, Extension}, }; From 41a81b75a60623fce65f7ddfc999b2e950f334b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=2E=20Bezerra?= Date: Fri, 26 Mar 2021 14:24:24 -0300 Subject: [PATCH 51/67] Simplyfying tests --- src/main.rs | 2 +- src/test.rs | 72 ++++++++++++++++++++++++++--------------------------- 2 files changed, 36 insertions(+), 38 deletions(-) diff --git a/src/main.rs b/src/main.rs index 9c333b527..690287625 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,7 +10,7 @@ mod utils; use std::convert::TryFrom; -pub use error::{Error, Result}; +use error::{Error, Result}; use evaluator::Evaluator; fn main() -> crate::Result<()> { diff --git a/src/test.rs b/src/test.rs index cf35bcf6e..269f31ed1 100644 --- a/src/test.rs +++ b/src/test.rs @@ -127,9 +127,7 @@ mod cli_errors { use std::convert::TryFrom; - use crate::{ - cli::{clap_app, Command}, - }; + use crate::cli::{clap_app, Command}; #[test] fn compress_files() -> crate::Result<()> { @@ -150,24 +148,22 @@ mod cli_errors { #[cfg(test)] mod extension_extraction { - use std::{convert::TryFrom, path::PathBuf, str::FromStr}; + use std::convert::TryFrom; - use crate::{ - extension::{CompressionFormat, Extension}, - }; + use crate::extension::{CompressionFormat, Extension}; #[test] - fn zip() -> crate::Result<()> { - let path = PathBuf::from_str("filename.tar.zip").unwrap(); - assert_eq!(CompressionFormat::try_from(&path)?, CompressionFormat::Zip); - - Ok(()) + fn test_extension_zip() { + let path = "filename.tar.zip"; + assert_eq!( + CompressionFormat::try_from(path), + Ok(CompressionFormat::Zip) + ); } #[test] - fn tar_gz() -> crate::Result<()> { - let extension = Extension::new("folder.tar.gz")?; - + fn test_extension_tar_gz() { + let extension = Extension::new("folder.tar.gz").unwrap(); assert_eq!( extension, Extension { @@ -175,39 +171,41 @@ mod extension_extraction { second_ext: CompressionFormat::Gzip } ); - - Ok(()) } #[test] - fn tar() -> crate::Result<()> { - let path = PathBuf::from_str("pictures.tar").unwrap(); - assert_eq!(CompressionFormat::try_from(&path)?, CompressionFormat::Tar); - - Ok(()) + fn test_extension_tar() { + let path = "pictures.tar"; + assert_eq!( + CompressionFormat::try_from(path), + Ok(CompressionFormat::Tar) + ); } #[test] - fn gz() -> crate::Result<()> { - let path = PathBuf::from_str("passwords.tar.gz").unwrap(); - assert_eq!(CompressionFormat::try_from(&path)?, CompressionFormat::Gzip); - - Ok(()) + fn test_extension_gz() { + let path = "passwords.tar.gz"; + assert_eq!( + CompressionFormat::try_from(path), + Ok(CompressionFormat::Gzip) + ); } #[test] - fn lzma() -> crate::Result<()> { - let path = PathBuf::from_str("mygame.tar.lzma").unwrap(); - assert_eq!(CompressionFormat::try_from(&path)?, CompressionFormat::Lzma); - - Ok(()) + fn test_extension_lzma() { + let path = "mygame.tar.lzma"; + assert_eq!( + CompressionFormat::try_from(path), + Ok(CompressionFormat::Lzma) + ); } #[test] - fn bz() -> crate::Result<()> { - let path = PathBuf::from_str("songs.tar.bz").unwrap(); - assert_eq!(CompressionFormat::try_from(&path)?, CompressionFormat::Bzip); - - Ok(()) + fn test_extension_bz() { + let path = "songs.tar.bz"; + assert_eq!( + CompressionFormat::try_from(path), + Ok(CompressionFormat::Bzip) + ); } } From 96a4e8bb7861802306450ac09edaf75f1d878f56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Fri, 26 Mar 2021 21:32:54 -0300 Subject: [PATCH 52/67] Bump version to 0.1.3 --- Cargo.toml | 4 ++-- README.md | 2 +- src/cli.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b392b8ddf..a4b856d2b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ouch" -version = "0.1.2" -authors = ["Vinícius Rodrigues Miguel "] +version = "0.1.3" +authors = ["Vinícius Rodrigues Miguel ", "João M. Bezerra "] edition = "2018" readme = "README.md" homepage = "https://github.com/vrmiguel/ouch" diff --git a/README.md b/README.md index 001f0a1a4..4a33c07be 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ `ouch` infers commands from the extensions of its command-line options. ``` -ouch 0.1.2 +ouch 0.1.3 Vinícius R. Miguel ouch is a unified compression & decompression utility diff --git a/src/cli.rs b/src/cli.rs index bc2fae2f4..b46ce71fd 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -30,7 +30,7 @@ pub struct Command { pub fn clap_app<'a, 'b>() -> clap::App<'a, 'b> { clap::App::new("ouch") - .version("0.1.2") + .version("0.1.3") .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. From 19f12ff791ffbde6afaf7790028efa6254d2d195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Fri, 26 Mar 2021 22:39:23 -0300 Subject: [PATCH 53/67] Ensure correct permissions for decompressed files from .zip on Unix --- src/cli.rs | 2 +- src/decompressors/zip.rs | 13 ++++++++++++- src/error.rs | 4 ++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index b46ce71fd..fb49d5136 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -127,7 +127,7 @@ impl TryFrom> for Command { } eprintln!("{} {}", "[ERROR]".red(), err); - return Err(crate::Error::IOError); + return Err(crate::Error::IoError); } } diff --git a/src/decompressors/zip.rs b/src/decompressors/zip.rs index 1882248b3..01f8a653b 100644 --- a/src/decompressors/zip.rs +++ b/src/decompressors/zip.rs @@ -10,6 +10,15 @@ use zip::{self, read::ZipFile, ZipArchive}; use super::decompressor::{DecompressionResult, Decompressor}; use crate::{file::File, utils}; +#[cfg(unix)] +fn __unix_set_permissions(file_path: &PathBuf, file: &ZipFile) { + use std::os::unix::fs::PermissionsExt; + + if let Some(mode) = file.unix_mode() { + fs::set_permissions(&file_path, fs::Permissions::from_mode(mode)).unwrap(); + } +} + pub struct ZipDecompressor {} impl ZipDecompressor { @@ -65,7 +74,8 @@ impl ZipDecompressor { io::copy(&mut file, &mut outfile)?; } - // TODO: check if permissions are correct when on Unix + #[cfg(unix)] + __unix_set_permissions(&file_path, &file); let file_path = fs::canonicalize(file_path.clone())?; unpacked_files.push(file_path); @@ -89,6 +99,7 @@ impl ZipDecompressor { None => { let file = fs::File::open(&from.path)?; let mut archive = zip::ZipArchive::new(file)?; + Ok(Self::zip_decompress(&mut archive, into)?) } } diff --git a/src/error.rs b/src/error.rs index 19a7ea30f..6680600f1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -9,7 +9,7 @@ pub enum Error { // TODO: get rid of this error variant InvalidUnicode, InvalidInput, - IOError, + IoError, FileNotFound(PathBuf), AlreadyExists, InvalidZipArchive(&'static str), @@ -50,7 +50,7 @@ impl From for Error { std::io::ErrorKind::AlreadyExists => Self::AlreadyExists, _other => { println!("{}: {}", "IO error".red(), err); - Self::IOError + Self::IoError } } } From 3687e7255d20d51dec92dcde2f4daa3cc4b025bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Fri, 26 Mar 2021 22:54:22 -0300 Subject: [PATCH 54/67] Python test script: fix the cleaning-up of test files --- makeshift_testing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/makeshift_testing.py b/makeshift_testing.py index e8130bf71..9790699db 100755 --- a/makeshift_testing.py +++ b/makeshift_testing.py @@ -19,6 +19,7 @@ def sanity_check_format(format: str): print("Something went wrong with tar (de)compression.") os._exit(2) os.remove('test-file') + os.remove(f'test-file.{format}') if __name__ == "__main__": From 8e370780439c570e9f1b499e5b354c81143bf55f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Fri, 26 Mar 2021 22:56:57 -0300 Subject: [PATCH 55/67] zip: ensure usage of relational paths during compression --- src/compressors/tar.rs | 12 +++--------- src/compressors/zip.rs | 8 +++++++- src/utils.rs | 12 ++++++++---- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/compressors/tar.rs b/src/compressors/tar.rs index 53ccb8715..2ca322d5a 100644 --- a/src/compressors/tar.rs +++ b/src/compressors/tar.rs @@ -6,6 +6,7 @@ use walkdir::WalkDir; use super::compressor::Entry; use crate::{compressors::Compressor, file::File}; +use crate::utils; pub struct TarCompressor {} @@ -20,19 +21,12 @@ impl TarCompressor { } fn make_archive_from_files(input_filenames: Vec) -> crate::Result> { - let change_dir_and_return_parent = |filename: &PathBuf| -> crate::Result { - let previous_location = env::current_dir()?; - let parent = filename.parent().unwrap(); - env::set_current_dir(parent)?; - Ok(previous_location) - }; - + let buf = Vec::new(); let mut b = Builder::new(buf); for filename in input_filenames { - let previous_location = change_dir_and_return_parent(&filename)?; - // Safe unwrap since this filename came from `fs::canonicalize`. + let previous_location = utils::change_dir_and_return_parent(&filename)?; let filename = filename.file_name().unwrap(); for entry in WalkDir::new(&filename) { let entry = entry?; diff --git a/src/compressors/zip.rs b/src/compressors/zip.rs index 5a8c55ec4..316d9c7d1 100644 --- a/src/compressors/zip.rs +++ b/src/compressors/zip.rs @@ -6,7 +6,7 @@ use std::{ use walkdir::WalkDir; use super::compressor::Entry; -use crate::{compressors::Compressor, file::File}; +use crate::{compressors::Compressor, file::File, utils}; pub struct ZipCompressor {} @@ -55,6 +55,10 @@ impl ZipCompressor { zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Deflated); for filename in input_filenames { + + let previous_location = utils::change_dir_and_return_parent(&filename)?; + let filename = filename.file_name().unwrap(); + for entry in WalkDir::new(filename) { let entry = entry?; let entry_path = &entry.path(); @@ -65,6 +69,8 @@ impl ZipCompressor { let file_bytes = std::fs::read(entry.path())?; writer.write_all(&*file_bytes)?; } + + std::env::set_current_dir(previous_location)?; } let bytes = writer.finish().unwrap(); diff --git a/src/utils.rs b/src/utils.rs index 2c8193d51..1ec935e01 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,7 +1,4 @@ -use std::{ - fs, - path::{Path, PathBuf}, -}; +use std::{env, fs, path::{Path, PathBuf}}; use colored::Colorize; @@ -63,3 +60,10 @@ pub(crate) fn get_destination_path(dest: &Option) -> &Path { None => Path::new("."), } } + +pub (crate) fn change_dir_and_return_parent(filename: &PathBuf) -> crate::Result { + let previous_location = env::current_dir()?; + let parent = filename.parent().unwrap(); + env::set_current_dir(parent)?; + Ok(previous_location) +} \ No newline at end of file From 49e4c4afcd75afed4c00053e2557478aea9c3bae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Fri, 26 Mar 2021 23:47:42 -0300 Subject: [PATCH 56/67] compressors/zip: replace a couple of unwraps to the question mark op. --- src/compressors/zip.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/compressors/zip.rs b/src/compressors/zip.rs index 316d9c7d1..bcfea08f7 100644 --- a/src/compressors/zip.rs +++ b/src/compressors/zip.rs @@ -57,7 +57,7 @@ impl ZipCompressor { for filename in input_filenames { let previous_location = utils::change_dir_and_return_parent(&filename)?; - let filename = filename.file_name().unwrap(); + let filename = filename.file_name()?; for entry in WalkDir::new(filename) { let entry = entry?; @@ -73,7 +73,7 @@ impl ZipCompressor { std::env::set_current_dir(previous_location)?; } - let bytes = writer.finish().unwrap(); + let bytes = writer.finish()?; Ok(bytes.into_inner()) } From 234e0406a1c4709cf5fa4b2261d5d6fd9f6c1e7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Sat, 27 Mar 2021 00:37:47 -0300 Subject: [PATCH 57/67] Don't allow `ouch` to compress the root folder --- src/compressors/zip.rs | 11 ++++++----- src/error.rs | 2 +- src/utils.rs | 12 +++++++++++- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/compressors/zip.rs b/src/compressors/zip.rs index bcfea08f7..377ef7ffb 100644 --- a/src/compressors/zip.rs +++ b/src/compressors/zip.rs @@ -1,7 +1,4 @@ -use std::{ - io::{Cursor, Write}, - path::PathBuf, -}; +use std::{io::{Cursor, Write}, path::PathBuf}; use walkdir::WalkDir; @@ -57,7 +54,11 @@ impl ZipCompressor { for filename in input_filenames { let previous_location = utils::change_dir_and_return_parent(&filename)?; - let filename = filename.file_name()?; + let filename = filename + .file_name() + // Safe unwrap since the function call above would fail in scenarios + // where this unwrap would panic + .unwrap(); for entry in WalkDir::new(filename) { let entry = entry?; diff --git a/src/error.rs b/src/error.rs index 6680600f1..152f05277 100644 --- a/src/error.rs +++ b/src/error.rs @@ -73,4 +73,4 @@ impl From for Error { eprintln!("{}: {}", "error".red(), err); Self::InvalidInput } -} +} \ No newline at end of file diff --git a/src/utils.rs b/src/utils.rs index 1ec935e01..7ed08817a 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -63,7 +63,17 @@ pub(crate) fn get_destination_path(dest: &Option) -> &Path { pub (crate) fn change_dir_and_return_parent(filename: &PathBuf) -> crate::Result { let previous_location = env::current_dir()?; - let parent = filename.parent().unwrap(); + + let parent = if let Some(parent) = filename.parent() { + parent + } else { + let spacing = " "; + println!("{} It seems you're trying to compress the root folder.", "[WARNING]".red()); + println!("{}This is unadvisable since ouch does compressions in-memory.", spacing); + println!("{}Use a more appropriate tool for this, such as {}.", spacing, "rsync".green()); + return Err(crate::Error::InvalidInput); + }; + env::set_current_dir(parent)?; Ok(previous_location) } \ No newline at end of file From 91f411439a7c5fb26ee2a849edd2b08205261da6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Sat, 27 Mar 2021 01:34:43 -0300 Subject: [PATCH 58/67] rustfmt --- src/compressors/tar.rs | 3 +-- src/compressors/zip.rs | 8 +++++--- src/error.rs | 2 +- src/utils.rs | 27 ++++++++++++++++++++------- 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/compressors/tar.rs b/src/compressors/tar.rs index 2ca322d5a..d7a014b1f 100644 --- a/src/compressors/tar.rs +++ b/src/compressors/tar.rs @@ -5,8 +5,8 @@ use tar::Builder; use walkdir::WalkDir; use super::compressor::Entry; -use crate::{compressors::Compressor, file::File}; use crate::utils; +use crate::{compressors::Compressor, file::File}; pub struct TarCompressor {} @@ -21,7 +21,6 @@ impl TarCompressor { } fn make_archive_from_files(input_filenames: Vec) -> crate::Result> { - let buf = Vec::new(); let mut b = Builder::new(buf); diff --git a/src/compressors/zip.rs b/src/compressors/zip.rs index 377ef7ffb..21d3c9b4e 100644 --- a/src/compressors/zip.rs +++ b/src/compressors/zip.rs @@ -1,4 +1,7 @@ -use std::{io::{Cursor, Write}, path::PathBuf}; +use std::{ + io::{Cursor, Write}, + path::PathBuf, +}; use walkdir::WalkDir; @@ -52,11 +55,10 @@ impl ZipCompressor { zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Deflated); for filename in input_filenames { - let previous_location = utils::change_dir_and_return_parent(&filename)?; let filename = filename .file_name() - // Safe unwrap since the function call above would fail in scenarios + // Safe unwrap since the function call above would fail in scenarios // where this unwrap would panic .unwrap(); diff --git a/src/error.rs b/src/error.rs index 152f05277..6680600f1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -73,4 +73,4 @@ impl From for Error { eprintln!("{}: {}", "error".red(), err); Self::InvalidInput } -} \ No newline at end of file +} diff --git a/src/utils.rs b/src/utils.rs index 7ed08817a..60bdb2ccd 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,7 @@ -use std::{env, fs, path::{Path, PathBuf}}; +use std::{ + env, fs, + path::{Path, PathBuf}, +}; use colored::Colorize; @@ -61,19 +64,29 @@ pub(crate) fn get_destination_path(dest: &Option) -> &Path { } } -pub (crate) fn change_dir_and_return_parent(filename: &PathBuf) -> crate::Result { +pub(crate) fn change_dir_and_return_parent(filename: &PathBuf) -> crate::Result { let previous_location = env::current_dir()?; - + let parent = if let Some(parent) = filename.parent() { parent } else { let spacing = " "; - println!("{} It seems you're trying to compress the root folder.", "[WARNING]".red()); - println!("{}This is unadvisable since ouch does compressions in-memory.", spacing); - println!("{}Use a more appropriate tool for this, such as {}.", spacing, "rsync".green()); + println!( + "{} It seems you're trying to compress the root folder.", + "[WARNING]".red() + ); + println!( + "{}This is unadvisable since ouch does compressions in-memory.", + spacing + ); + println!( + "{}Use a more appropriate tool for this, such as {}.", + spacing, + "rsync".green() + ); return Err(crate::Error::InvalidInput); }; env::set_current_dir(parent)?; Ok(previous_location) -} \ No newline at end of file +} From 7954eb07fdcd13a14416aa9e8dc3179e59429a08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Sun, 28 Mar 2021 13:56:00 -0300 Subject: [PATCH 59/67] decompressors/zip: Add confirmation dialog for file overwriting --- src/decompressors/zip.rs | 45 +++++++++++++++++++++++--------------- src/dialogs.rs | 47 ++++++++++++++++++++++++++++++++++++++++ src/error.rs | 1 + src/main.rs | 12 ++++++++++ 4 files changed, 87 insertions(+), 18 deletions(-) create mode 100644 src/dialogs.rs diff --git a/src/decompressors/zip.rs b/src/decompressors/zip.rs index 01f8a653b..6e414d2c1 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::{file::File, utils}; +use crate::{dialogs::Confirmation, file::File, utils}; #[cfg(unix)] fn __unix_set_permissions(file_path: &PathBuf, file: &ZipFile) { @@ -41,6 +41,7 @@ impl ZipDecompressor { where T: Read + Seek, { + let confirm = Confirmation::new("Do you want to overwrite 'FILE'?", Some("FILE")); let mut unpacked_files = vec![]; for idx in 0..archive.len() { let mut file = archive.by_index(idx)?; @@ -50,28 +51,36 @@ impl ZipDecompressor { }; let file_path = into.join(file_path); + if file_path.exists() { + let file_path_str = &*file_path.as_path().to_string_lossy(); + if confirm.ask(Some(file_path_str))? { + continue; + } + } Self::check_for_comments(&file); - let is_dir = (&*file.name()).ends_with('/'); - - if is_dir { - println!("File {} extracted to \"{}\"", idx, file_path.display()); - fs::create_dir_all(&file_path)?; - } else { - if let Some(p) = file_path.parent() { - if !p.exists() { - fs::create_dir_all(&p)?; + match (&*file.name()).ends_with('/') { + _is_dir @ true => { + println!("File {} extracted to \"{}\"", idx, file_path.display()); + fs::create_dir_all(&file_path)?; + } + _is_file @ false => { + if let Some(path) = file_path.parent() { + if !path.exists() { + fs::create_dir_all(&path)?; + } } + println!( + "{}: \"{}\" extracted. ({} bytes)", + "info".yellow(), + file_path.display(), + file.size() + ); + + let mut output_file = fs::File::create(&file_path)?; + io::copy(&mut file, &mut output_file)?; } - println!( - "{}: \"{}\" extracted. ({} bytes)", - "info".yellow(), - file_path.display(), - file.size() - ); - let mut outfile = fs::File::create(&file_path)?; - io::copy(&mut file, &mut outfile)?; } #[cfg(unix)] diff --git a/src/dialogs.rs b/src/dialogs.rs new file mode 100644 index 000000000..b603b9ab3 --- /dev/null +++ b/src/dialogs.rs @@ -0,0 +1,47 @@ +use std::io::{self, Write}; + +use colored::Colorize; + +pub struct Confirmation<'a> { + pub prompt: &'a str, + pub placeholder: Option<&'a str>, +} + +#[derive(Debug)] +pub struct Error; + +impl<'a> Confirmation<'a> { + pub fn new(prompt: &'a str, pattern: Option<&'a str>) -> Self { + Self { + prompt, + placeholder: pattern, + } + } + + pub fn ask(&self, substitute: Option<&'a str>) -> crate::Result { + let message = match (self.placeholder, substitute) { + (None, _) => self.prompt.into(), + (Some(_), None) => return Err(crate::Error::InternalError), + (Some(placeholder), Some(subs)) => self.prompt.replace(placeholder, subs), + }; + + loop { + print!("{} [{}/{}] ", message, "Y".bright_green(), "n".bright_red()); + io::stdout().flush()?; + + let mut answer = String::new(); + io::stdin().read_line(&mut answer)?; + let trimmed_answer = answer.trim().to_ascii_lowercase(); + + if trimmed_answer.is_empty() { + return Ok(true); + } + + match trimmed_answer.to_ascii_lowercase().as_ref() { + "y" | "yes" => return Ok(true), + "n" | "no" => return Ok(false), + _ => {} + } + } + } +} diff --git a/src/error.rs b/src/error.rs index 6680600f1..2b58a5ceb 100644 --- a/src/error.rs +++ b/src/error.rs @@ -16,6 +16,7 @@ pub enum Error { PermissionDenied, UnsupportedZipArchive(&'static str), InputsMustHaveBeenDecompressible(PathBuf), + InternalError, } pub type Result = std::result::Result; diff --git a/src/main.rs b/src/main.rs index 690287625..a13504f6d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ mod cli; mod compressors; mod decompressors; +mod dialogs; mod error; mod evaluator; mod extension; @@ -18,3 +19,14 @@ fn main() -> crate::Result<()> { let command = cli::Command::try_from(matches)?; Evaluator::evaluate(command) } + +// fn main() -> crate::Result<()> { +// let dialog = dialogs::Confirmation::new("Do you want to overwrite 'FILE'?", Some("FILE")); + +// match dialog.ask(Some("file.tar.gz"))? { +// true => println!("deleting"), +// false => println!("keeping") +// }; + +// Ok(()) +// } From 03d6fc1e605920f870ebc4e3dc721d27163edd4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Sun, 28 Mar 2021 14:28:07 -0300 Subject: [PATCH 60/67] decompressors/tar: Add confirmation dialog for file overwriting --- src/decompressors/tar.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/decompressors/tar.rs b/src/decompressors/tar.rs index a613b0cbf..c384ada8b 100644 --- a/src/decompressors/tar.rs +++ b/src/decompressors/tar.rs @@ -8,7 +8,7 @@ use colored::Colorize; use tar::{self, Archive}; use super::decompressor::{DecompressionResult, Decompressor}; -use crate::{file::File, utils}; +use crate::{file::File, utils, dialogs::Confirmation}; #[derive(Debug)] pub struct TarDecompressor {} @@ -21,6 +21,7 @@ impl TarDecompressor { &from.path ); let mut files_unpacked = vec![]; + let confirm = Confirmation::new("Do you want to overwrite 'FILE'?", Some("FILE")); let mut archive: Archive> = match from.contents_in_memory { Some(bytes) => tar::Archive::new(Box::new(Cursor::new(bytes))), @@ -32,8 +33,15 @@ impl TarDecompressor { for file in archive.entries()? { let mut file = file?; - - // TODO: check if file/folder already exists and ask user's permission for overwriting + + let file_path = PathBuf::from(into).join(file.path()?); + if file_path.exists() { + let file_path_str = &*file_path.to_string_lossy(); + if confirm.ask(Some(file_path_str))? { + continue; + } + } + file.unpack_in(into)?; println!( From 40fb926d8081cecb9c856e39ee880086eb91887d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Sun, 28 Mar 2021 14:52:09 -0300 Subject: [PATCH 61/67] evaluator: Add confirmation dialog for file overwriting --- src/decompressors/tar.rs | 5 +++-- src/decompressors/zip.rs | 3 ++- src/evaluator.rs | 10 ++++++++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/decompressors/tar.rs b/src/decompressors/tar.rs index c384ada8b..13ef97987 100644 --- a/src/decompressors/tar.rs +++ b/src/decompressors/tar.rs @@ -37,7 +37,8 @@ impl TarDecompressor { let file_path = PathBuf::from(into).join(file.path()?); if file_path.exists() { let file_path_str = &*file_path.to_string_lossy(); - if confirm.ask(Some(file_path_str))? { + if !confirm.ask(Some(file_path_str))? { + // The user does not want to overwrite the file continue; } } @@ -51,7 +52,7 @@ impl TarDecompressor { file.size() ); - let file_path = fs::canonicalize(into.join(file.path()?))?; + let file_path = fs::canonicalize(file_path)?; files_unpacked.push(file_path); } diff --git a/src/decompressors/zip.rs b/src/decompressors/zip.rs index 6e414d2c1..40e8441b8 100644 --- a/src/decompressors/zip.rs +++ b/src/decompressors/zip.rs @@ -53,7 +53,8 @@ impl ZipDecompressor { let file_path = into.join(file_path); if file_path.exists() { let file_path_str = &*file_path.as_path().to_string_lossy(); - if confirm.ask(Some(file_path_str))? { + if !confirm.ask(Some(file_path_str))? { + // The user does not want to overwrite the file continue; } } diff --git a/src/evaluator.rs b/src/evaluator.rs index 8e923b143..d913bea53 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -13,6 +13,7 @@ use crate::{ TarDecompressor, ZipDecompressor, }, extension::{CompressionFormat, Extension}, + dialogs::Confirmation, file::File, utils, }; @@ -148,9 +149,18 @@ impl Evaluator { } fn compress_files(files: Vec, mut output: File) -> crate::Result<()> { + let confirm = Confirmation::new("Do you want to overwrite 'FILE'?", Some("FILE")); let (first_compressor, second_compressor) = Self::get_compressor(&output)?; let output_path = output.path.clone(); + if output_path.exists() { + let output_path_str = &*output_path.to_string_lossy(); + if !confirm.ask(Some(output_path_str))? { + // The user does not want to overwrite the file + return Ok(()); + } + } + let bytes = match first_compressor { Some(first_compressor) => { From c7cf1112b6c7c161d89f0e401188cdb3c89ebbe7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20R=2E=20Miguel?= Date: Sun, 28 Mar 2021 23:50:28 -0300 Subject: [PATCH 62/67] Add -y, --yes and -n, --no flags (currently unused) --- src/cli.rs | 41 +++++++++++++++++++++++++++++++++++++++++ src/main.rs | 16 ++-------------- 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index fb49d5136..8a46675f1 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -22,6 +22,16 @@ pub enum CommandKind { ), } +#[derive(PartialEq, Eq, Debug)] +pub enum Flags { + // No flags supplied + None, + // Flag -y, --yes supplied + AlwaysYes, + // Flag -n, --no supplied + AlwaysNo +} + #[derive(PartialEq, Eq, Debug)] pub struct Command { pub kind: CommandKind, @@ -63,12 +73,43 @@ Please relate any issues or contribute at https://github.com/vrmiguel/ouch") .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), + ) } pub fn get_matches() -> clap::ArgMatches<'static> { clap_app().get_matches() } +pub fn parse_matches(matches: clap::ArgMatches<'static>) -> crate::Result<(Command, Flags)> { + let flag = match (matches.is_present("yes"), matches.is_present("no")) { + (true, true) => unreachable!(), + (true, _) => Flags::AlwaysYes, + (_, true) => Flags::AlwaysNo, + (_, _) => Flags::None + }; + + Ok((Command::try_from(matches)?, flag)) +} + impl TryFrom> for Command { type Error = crate::Error; diff --git a/src/main.rs b/src/main.rs index a13504f6d..3be1a77e0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,24 +9,12 @@ mod file; mod test; mod utils; -use std::convert::TryFrom; - use error::{Error, Result}; use evaluator::Evaluator; fn main() -> crate::Result<()> { let matches = cli::get_matches(); - let command = cli::Command::try_from(matches)?; + // let command = cli::Command::try_from(matches)?; + let (command, _flags) = cli::parse_matches(matches)?; Evaluator::evaluate(command) } - -// fn main() -> crate::Result<()> { -// let dialog = dialogs::Confirmation::new("Do you want to overwrite 'FILE'?", Some("FILE")); - -// match dialog.ask(Some("file.tar.gz"))? { -// true => println!("deleting"), -// false => println!("keeping") -// }; - -// Ok(()) -// } From 0f0b0869433337ff78f927a0536e4a092a17e5ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Mon, 29 Mar 2021 01:37:01 -0300 Subject: [PATCH 63/67] Use the the -y and -n flags when decompressing .tar and .zip --- src/cli.rs | 2 +- src/decompressors/decompressor.rs | 7 +++++-- src/decompressors/tar.rs | 15 ++++++++------- src/decompressors/to_memory.rs | 8 ++++---- src/decompressors/zip.rs | 27 +++++++++++++++++---------- src/evaluator.rs | 17 +++++++++-------- src/main.rs | 5 ++--- src/utils.rs | 13 ++++++++++++- 8 files changed, 58 insertions(+), 36 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 8a46675f1..af6222f50 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -22,7 +22,7 @@ pub enum CommandKind { ), } -#[derive(PartialEq, Eq, Debug)] +#[derive(PartialEq, Eq, Copy, Clone, Debug)] pub enum Flags { // No flags supplied None, diff --git a/src/decompressors/decompressor.rs b/src/decompressors/decompressor.rs index 5d2def1ba..ea9436b50 100644 --- a/src/decompressors/decompressor.rs +++ b/src/decompressors/decompressor.rs @@ -1,6 +1,9 @@ use std::path::PathBuf; -use crate::file::File; +use crate::{ + cli::Flags, + file::File +}; pub enum DecompressionResult { FilesUnpacked(Vec), @@ -8,5 +11,5 @@ pub enum DecompressionResult { } pub trait Decompressor { - fn decompress(&self, from: File, into: &Option) -> crate::Result; + fn decompress(&self, from: File, into: &Option, flags: Flags) -> crate::Result; } diff --git a/src/decompressors/tar.rs b/src/decompressors/tar.rs index 13ef97987..b3947e33b 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::{file::File, utils, dialogs::Confirmation}; +use crate::{cli::Flags, dialogs::Confirmation, file::File, utils}; #[derive(Debug)] pub struct TarDecompressor {} impl TarDecompressor { - fn unpack_files(from: File, into: &Path) -> crate::Result> { + fn unpack_files(from: File, into: &Path, flags: Flags) -> crate::Result> { println!( "{}: attempting to decompress {:?}", "ouch".bright_blue(), @@ -24,7 +24,9 @@ impl TarDecompressor { let confirm = Confirmation::new("Do you want to overwrite 'FILE'?", Some("FILE")); let mut archive: Archive> = match from.contents_in_memory { - Some(bytes) => tar::Archive::new(Box::new(Cursor::new(bytes))), + Some(bytes) => { + tar::Archive::new(Box::new(Cursor::new(bytes))) + }, None => { let file = fs::File::open(&from.path)?; tar::Archive::new(Box::new(file)) @@ -36,8 +38,7 @@ impl TarDecompressor { let file_path = PathBuf::from(into).join(file.path()?); if file_path.exists() { - let file_path_str = &*file_path.to_string_lossy(); - if !confirm.ask(Some(file_path_str))? { + if !utils::permission_for_overwriting(&file_path, flags, &confirm)? { // The user does not want to overwrite the file continue; } @@ -61,12 +62,12 @@ impl TarDecompressor { } impl Decompressor for TarDecompressor { - fn decompress(&self, from: File, into: &Option) -> crate::Result { + fn decompress(&self, from: File, into: &Option, flags: Flags) -> crate::Result { let destination_path = utils::get_destination_path(into); utils::create_path_if_non_existent(destination_path)?; - let files_unpacked = Self::unpack_files(from, destination_path)?; + let files_unpacked = Self::unpack_files(from, destination_path, flags)?; Ok(DecompressionResult::FilesUnpacked(files_unpacked)) } diff --git a/src/decompressors/to_memory.rs b/src/decompressors/to_memory.rs index 81a0e27bf..e8b9fb9ce 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::utils; +use crate::{cli::Flags, utils}; // use niffler; use crate::{extension::CompressionFormat, file::File}; @@ -62,19 +62,19 @@ impl DecompressorToMemory { } impl Decompressor for GzipDecompressor { - fn decompress(&self, from: File, into: &Option) -> crate::Result { + fn decompress(&self, from: File, into: &Option, _: Flags) -> crate::Result { DecompressorToMemory::decompress(from, CompressionFormat::Gzip, into) } } impl Decompressor for BzipDecompressor { - fn decompress(&self, from: File, into: &Option) -> crate::Result { + fn decompress(&self, from: File, into: &Option, _: Flags) -> crate::Result { DecompressorToMemory::decompress(from, CompressionFormat::Bzip, into) } } impl Decompressor for LzmaDecompressor { - fn decompress(&self, from: File, into: &Option) -> crate::Result { + fn decompress(&self, from: File, into: &Option, _: Flags) -> crate::Result { DecompressorToMemory::decompress(from, CompressionFormat::Lzma, into) } } diff --git a/src/decompressors/zip.rs b/src/decompressors/zip.rs index 40e8441b8..50dd278f2 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::{dialogs::Confirmation, file::File, utils}; +use crate::{cli::Flags, dialogs::Confirmation, file::File, utils}; #[cfg(unix)] fn __unix_set_permissions(file_path: &PathBuf, file: &ZipFile) { @@ -37,6 +37,7 @@ impl ZipDecompressor { pub fn zip_decompress( archive: &mut ZipArchive, into: &Path, + flags: Flags, ) -> crate::Result> where T: Read + Seek, @@ -52,8 +53,7 @@ impl ZipDecompressor { let file_path = into.join(file_path); if file_path.exists() { - let file_path_str = &*file_path.as_path().to_string_lossy(); - if !confirm.ask(Some(file_path_str))? { + if !utils::permission_for_overwriting(&file_path, flags, &confirm)? { // The user does not want to overwrite the file continue; } @@ -94,35 +94,42 @@ impl ZipDecompressor { Ok(unpacked_files) } - fn unpack_files(from: File, into: &Path) -> crate::Result> { + fn unpack_files(from: File, into: &Path, flags: Flags) -> crate::Result> { println!( - "{}: attempting to decompress {:?}", - "ouch".bright_blue(), + "{} decompressing {:?}", + "[OUCH]".bright_blue(), &from.path ); match from.contents_in_memory { Some(bytes) => { + // Decompressing a .zip archive loaded up in memory let mut archive = zip::ZipArchive::new(Cursor::new(bytes))?; - Ok(Self::zip_decompress(&mut archive, into)?) + Ok(Self::zip_decompress(&mut archive, into, flags)?) } None => { + // Decompressing a .zip archive from the file system let file = fs::File::open(&from.path)?; let mut archive = zip::ZipArchive::new(file)?; - Ok(Self::zip_decompress(&mut archive, into)?) + Ok(Self::zip_decompress(&mut archive, into, flags)?) } } } } impl Decompressor for ZipDecompressor { - fn decompress(&self, from: File, into: &Option) -> crate::Result { + fn decompress( + &self, + from: File, + into: &Option, + flags: Flags, + ) -> crate::Result { let destination_path = utils::get_destination_path(into); utils::create_path_if_non_existent(destination_path)?; - let files_unpacked = Self::unpack_files(from, destination_path)?; + let files_unpacked = Self::unpack_files(from, destination_path, flags)?; Ok(DecompressionResult::FilesUnpacked(files_unpacked)) } diff --git a/src/evaluator.rs b/src/evaluator.rs index d913bea53..5c7328684 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -3,7 +3,7 @@ use std::{ffi::OsStr, fs, io::Write, path::PathBuf}; use colored::Colorize; use crate::{ - cli::{Command, CommandKind}, + cli::{Flags, Command, CommandKind}, compressors::{ BzipCompressor, Compressor, Entry, GzipCompressor, LzmaCompressor, TarCompressor, ZipCompressor, @@ -98,13 +98,13 @@ impl Evaluator { Ok((first_decompressor, second_decompressor)) } - // todo: move this folder into decompressors/ later on fn decompress_file_in_memory( bytes: Vec, file_path: PathBuf, decompressor: Option>, output_file: &Option, extension: Option, + flags: Flags ) -> crate::Result<()> { let output_file_path = utils::get_destination_path(output_file); @@ -139,7 +139,7 @@ impl Evaluator { extension, }; - let decompression_result = decompressor.decompress(file, output_file)?; + let decompression_result = decompressor.decompress(file, output_file, flags)?; if let DecompressionResult::FileInMemory(_) = decompression_result { // Should not be reachable. unreachable!(); @@ -152,6 +152,7 @@ impl Evaluator { 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() { let output_path_str = &*output_path.to_string_lossy(); @@ -188,14 +189,13 @@ impl Evaluator { Ok(()) } - fn decompress_file(file: File, output: &Option) -> crate::Result<()> { - // let output_file = &command.output; + fn decompress_file(file: File, output: &Option, flags: Flags) -> crate::Result<()> { 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)?; + let decompression_result = second_decompressor.decompress(file, output, flags)?; match decompression_result { DecompressionResult::FileInMemory(bytes) => { @@ -207,6 +207,7 @@ impl Evaluator { first_decompressor, output, extension, + flags )?; } DecompressionResult::FilesUnpacked(_files) => { @@ -223,7 +224,7 @@ impl Evaluator { Ok(()) } - pub fn evaluate(command: Command) -> crate::Result<()> { + pub fn evaluate(command: Command, flags: Flags) -> crate::Result<()> { let output = command.output.clone(); match command.kind { @@ -234,7 +235,7 @@ impl Evaluator { } CommandKind::Decompression(files_to_decompress) => { for file in files_to_decompress { - Self::decompress_file(file, &output)?; + Self::decompress_file(file, &output, flags)?; } } } diff --git a/src/main.rs b/src/main.rs index 3be1a77e0..4df4f69b7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,7 +14,6 @@ use evaluator::Evaluator; fn main() -> crate::Result<()> { let matches = cli::get_matches(); - // let command = cli::Command::try_from(matches)?; - let (command, _flags) = cli::parse_matches(matches)?; - Evaluator::evaluate(command) + let (command, flags) = cli::parse_matches(matches)?; + Evaluator::evaluate(command, flags) } diff --git a/src/utils.rs b/src/utils.rs index 60bdb2ccd..cc1afe593 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -5,7 +5,7 @@ use std::{ use colored::Colorize; -use crate::{extension::CompressionFormat, file::File}; +use crate::{cli::Flags, dialogs::Confirmation, extension::CompressionFormat, file::File}; pub(crate) fn ensure_exists<'a, P>(path: P) -> crate::Result<()> where @@ -90,3 +90,14 @@ pub(crate) fn change_dir_and_return_parent(filename: &PathBuf) -> crate::Result< env::set_current_dir(parent)?; Ok(previous_location) } + +pub fn permission_for_overwriting(path: &PathBuf, flags: Flags, confirm: &Confirmation) -> crate::Result { + match flags { + Flags::AlwaysYes => return Ok(true), + Flags::AlwaysNo => return Ok(false), + Flags::None => {} + } + + let file_path_str = &*path.as_path().to_string_lossy(); + Ok(confirm.ask(Some(file_path_str))?) +} \ No newline at end of file From 5ac35401452ec70d8c7d6e08e8d799d6ae21038a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Mon, 29 Mar 2021 01:52:52 -0300 Subject: [PATCH 64/67] Use the -y and -n flags when compressing to a file that already exists --- src/evaluator.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/evaluator.rs b/src/evaluator.rs index 5c7328684..05557e248 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -113,12 +113,13 @@ impl Evaluator { .unwrap_or_else(|| output_file_path.as_os_str()); if filename == "." { - // I believe this is only possible when the supplied inout has a name + // 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"); } 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 { Some(decompressor) => decompressor, @@ -126,7 +127,7 @@ impl Evaluator { // 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); - + // TODO: use -y and -n flags let mut f = fs::File::create(output_file_path.join(filename))?; f.write_all(&bytes)?; return Ok(()); @@ -148,15 +149,14 @@ impl Evaluator { Ok(()) } - fn compress_files(files: Vec, mut output: File) -> crate::Result<()> { + fn compress_files(files: Vec, mut output: File, flags: Flags) -> crate::Result<()> { 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() { - let output_path_str = &*output_path.to_string_lossy(); - if !confirm.ask(Some(output_path_str))? { + if !utils::permission_for_overwriting(&output_path, flags, &confirm)? { // The user does not want to overwrite the file return Ok(()); } @@ -231,7 +231,7 @@ impl Evaluator { 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)?; + Self::compress_files(files_to_compress, output, flags)?; } CommandKind::Decompression(files_to_decompress) => { for file in files_to_decompress { From 1c0e883d9964ff643a71ef2b320d080897ac2f2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20Rodrigues=20Miguel?= Date: Mon, 29 Mar 2021 02:00:47 -0300 Subject: [PATCH 65/67] dialogs: Remove duplicated to_ascii_lowercase --- src/dialogs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dialogs.rs b/src/dialogs.rs index b603b9ab3..2902a4142 100644 --- a/src/dialogs.rs +++ b/src/dialogs.rs @@ -31,7 +31,7 @@ impl<'a> Confirmation<'a> { let mut answer = String::new(); io::stdin().read_line(&mut answer)?; - let trimmed_answer = answer.trim().to_ascii_lowercase(); + let trimmed_answer = answer.trim(); if trimmed_answer.is_empty() { return Ok(true); From cb10a45661754ef640a9b526869eb5eb3ba98b2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20R=2E=20Miguel?= Date: Mon, 29 Mar 2021 02:44:29 -0300 Subject: [PATCH 66/67] Use the -y and -n flags when decompressing single-file compression formats --- src/evaluator.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/evaluator.rs b/src/evaluator.rs index 05557e248..959a939ce 100644 --- a/src/evaluator.rs +++ b/src/evaluator.rs @@ -3,7 +3,7 @@ use std::{ffi::OsStr, fs, io::Write, path::PathBuf}; use colored::Colorize; use crate::{ - cli::{Flags, Command, CommandKind}, + cli::{Command, CommandKind, Flags}, compressors::{ BzipCompressor, Compressor, Entry, GzipCompressor, LzmaCompressor, TarCompressor, ZipCompressor, @@ -12,8 +12,8 @@ use crate::{ BzipDecompressor, DecompressionResult, Decompressor, GzipDecompressor, LzmaDecompressor, TarDecompressor, ZipDecompressor, }, - extension::{CompressionFormat, Extension}, dialogs::Confirmation, + extension::{CompressionFormat, Extension}, file::File, utils, }; @@ -104,7 +104,7 @@ impl Evaluator { decompressor: Option>, output_file: &Option, extension: Option, - flags: Flags + flags: Flags, ) -> crate::Result<()> { let output_file_path = utils::get_destination_path(output_file); @@ -119,7 +119,6 @@ impl Evaluator { } 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 { Some(decompressor) => decompressor, @@ -127,7 +126,15 @@ impl Evaluator { // 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); - // TODO: use -y and -n flags + + if filename.exists() { + let confirm = + Confirmation::new("Do you want to overwrite 'FILE'?", Some("FILE")); + if !utils::permission_for_overwriting(&filename, flags, &confirm)? { + return Ok(()); + } + } + let mut f = fs::File::create(output_file_path.join(filename))?; f.write_all(&bytes)?; return Ok(()); @@ -162,7 +169,6 @@ impl Evaluator { } } - let bytes = match first_compressor { Some(first_compressor) => { let mut entry = Entry::Files(files); @@ -207,7 +213,7 @@ impl Evaluator { first_decompressor, output, extension, - flags + flags, )?; } DecompressionResult::FilesUnpacked(_files) => { From a96d26eba68421f9e61eeea9c25c02721654fbd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20R=2E=20Miguel?= Date: Mon, 29 Mar 2021 02:58:16 -0300 Subject: [PATCH 67/67] Bump to version 0.1.4 --- Cargo.toml | 2 +- README.md | 2 +- src/cli.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a4b856d2b..dc1acf32f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ouch" -version = "0.1.3" +version = "0.1.4" authors = ["Vinícius Rodrigues Miguel ", "João M. Bezerra "] edition = "2018" readme = "README.md" diff --git a/README.md b/README.md index 4a33c07be..6a00e55ea 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ `ouch` infers commands from the extensions of its command-line options. ``` -ouch 0.1.3 +ouch 0.1.4 Vinícius R. Miguel ouch is a unified compression & decompression utility diff --git a/src/cli.rs b/src/cli.rs index af6222f50..15214f5c4 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -40,7 +40,7 @@ pub struct Command { pub fn clap_app<'a, 'b>() -> clap::App<'a, 'b> { clap::App::new("ouch") - .version("0.1.3") + .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.