diff --git a/src/main.rs b/src/main.rs index a24d802..7aeea78 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,7 +19,7 @@ use ripunzip::{ use wildmatch::WildMatch; const LONG_ABOUT: &str = - "ripunzip is a tool to unzip zip files in parallel, possibly from a remote server. + "ripunzip is a tool to unzip zip files in parallel, possibly from a remote server. It works best with HTTP(S) servers that support Range requests."; /// Unzip all files within a zip file as quickly as possible. @@ -73,6 +73,11 @@ struct UnzipArgs { #[arg(short = 'd', long, value_name = "DIRECTORY")] output_directory: Option, + /// Password to decrypt encrypted zipfile entries (if any). + /// Both ZipCrypto and AES encrypted zipfiles are supported. + #[arg(short = 'P', long, value_name = "PASSWORD")] + password: Option, + /// Whether to decompress on a single thread. By default, /// multiple threads are used, but this can lead to more network traffic. #[arg(long)] @@ -152,6 +157,7 @@ fn unzip(engine: UnzipEngine, unzip_args: UnzipArgs, is_silent: bool) -> Result< }; let options = UnzipOptions { output_directory: unzip_args.output_directory, + password: unzip_args.password, single_threaded: unzip_args.single_threaded, filename_filter, progress_reporter, diff --git a/src/unzip/mod.rs b/src/unzip/mod.rs index 840c81c..8f16ef5 100644 --- a/src/unzip/mod.rs +++ b/src/unzip/mod.rs @@ -42,6 +42,8 @@ pub(crate) fn determine_stream_len(stream: &mut R) -> std::io::Result { /// The destination directory. pub output_directory: Option, + /// Password if encrypted. + pub password: Option, /// Whether to run in single-threaded mode. pub single_threaded: bool, /// A filename filter, optionally @@ -282,6 +284,7 @@ fn unzip_serial_or_parallel<'a, T: Read + Seek + 'a>( &get_ziparchive_clone, i, &options.output_directory, + &options.password, progress_reporter, directory_creator, ) @@ -303,6 +306,7 @@ fn unzip_serial_or_parallel<'a, T: Read + Seek + 'a>( &get_ziparchive_clone, i, &options.output_directory, + &options.password, progress_reporter, directory_creator, ) @@ -345,7 +349,10 @@ fn unzip_serial_or_parallel<'a, T: Read + Seek + 'a>( .into_iter() .map(|name| { let myzip: &mut zip::ZipArchive = &mut get_ziparchive_clone(); - let file = myzip.by_name(&name)?; + let file: ZipFile = match &options.password { + None => myzip.by_name(&name)?, + Some(string) => myzip.by_name_decrypt(&name, string.as_bytes())??, + }; let r = extract_file( file, &options.output_directory, @@ -365,11 +372,15 @@ fn extract_file_by_index<'a, T: Read + Seek + 'a>( get_ziparchive_clone: impl Fn() -> ZipArchive + Sync, i: usize, output_directory: &Option, + password: &Option, progress_reporter: &dyn UnzipProgressReporter, directory_creator: &DirectoryCreator, ) -> Result<(), anyhow::Error> { let myzip: &mut zip::ZipArchive = &mut get_ziparchive_clone(); - let file = myzip.by_index(i)?; + let file: ZipFile = match password { + None => myzip.by_index(i)?, + Some(string) => myzip.by_index_decrypt(i, string.as_bytes())??, + }; extract_file(file, output_directory, progress_reporter, directory_creator) } @@ -480,6 +491,10 @@ impl DirectoryCreator { #[cfg(test)] mod tests { + use super::FilenameFilter; + use crate::{NullProgressReporter, UnzipEngine, UnzipOptions}; + use httptest::Server; + use ripunzip_test_utils::*; use std::{ collections::HashSet, env::{current_dir, set_current_dir}, @@ -489,11 +504,9 @@ mod tests { }; use tempfile::tempdir; use test_log::test; + use zip::unstable::write::FileOptionsExt; use zip::{write::FileOptions, ZipWriter}; - use crate::{NullProgressReporter, UnzipEngine, UnzipOptions}; - use ripunzip_test_utils::*; - struct UnzipSomeFilter; impl FilenameFilter for UnzipSomeFilter { fn should_unzip(&self, filename: &str) -> bool { @@ -512,16 +525,28 @@ mod tests { fn create_zip_file(path: &Path, include_a_txt: bool) { let file = File::create(path).unwrap(); - create_zip(file, include_a_txt) + create_zip(file, include_a_txt, None) } - fn create_zip(w: impl Write + Seek, include_a_txt: bool) { + fn create_encrypted_zip_file(path: &Path, include_a_txt: bool) { + let file = File::create(path).unwrap(); + let options = FileOptions::default() + .compression_method(zip::CompressionMethod::Stored) + .unix_permissions(0o755) + .with_deprecated_encryption("1Password".as_ref()); + create_zip(file, include_a_txt, Some(options)) + } + + fn create_zip(w: impl Write + Seek, include_a_txt: bool, custom_options: Option) { let mut zip = ZipWriter::new(w); + let options = custom_options.unwrap_or_else(|| { + FileOptions::default() + .compression_method(zip::CompressionMethod::Stored) + .unix_permissions(0o755) + }); zip.add_directory("test/", Default::default()).unwrap(); - let options = FileOptions::default() - .compression_method(zip::CompressionMethod::Stored) - .unix_permissions(0o755); + if include_a_txt { zip.start_file("test/a.txt", options).unwrap(); zip.write_all(b"Contents of A\n").unwrap(); @@ -558,6 +583,7 @@ mod tests { set_current_dir(td.path()).unwrap(); let options = UnzipOptions { output_directory: None, + password: None, single_threaded: false, filename_filter, progress_reporter: Box::new(NullProgressReporter), @@ -578,6 +604,27 @@ mod tests { let outdir = td.path().join("outdir"); let options = UnzipOptions { output_directory: Some(outdir.clone()), + password: None, + single_threaded: false, + filename_filter, + progress_reporter: Box::new(NullProgressReporter), + }; + UnzipEngine::for_file(zf).unwrap().unzip(options).unwrap(); + check_files_exist(&outdir, create_a); + }); + } + + #[test] + fn test_extract_encrypted_with_path() { + run_with_and_without_a_filename_filter(|create_a, filename_filter| { + let td = tempdir().unwrap(); + let zf = td.path().join("z.zip"); + create_encrypted_zip_file(&zf, create_a); + let zf = File::open(zf).unwrap(); + let outdir = td.path().join("outdir"); + let options = UnzipOptions { + output_directory: Some(outdir.clone()), + password: Some("1Password".to_string()), single_threaded: false, filename_filter, progress_reporter: Box::new(NullProgressReporter), @@ -603,16 +650,12 @@ mod tests { ) } - use httptest::Server; - - use super::FilenameFilter; - #[test] fn test_extract_from_server() { run_with_and_without_a_filename_filter(|create_a, filename_filter| { let td = tempdir().unwrap(); let mut zip_data = Cursor::new(Vec::new()); - create_zip(&mut zip_data, create_a); + create_zip(&mut zip_data, create_a, None); let body = zip_data.into_inner(); println!("Whole zip:"); hexdump::hexdump(&body); @@ -623,6 +666,7 @@ mod tests { let outdir = td.path().join("outdir"); let options = UnzipOptions { output_directory: Some(outdir.clone()), + password: None, single_threaded: false, filename_filter, progress_reporter: Box::new(NullProgressReporter), @@ -645,6 +689,7 @@ mod tests { let outdir = td.path().join("outdir"); let options = UnzipOptions { output_directory: Some(outdir), + password: None, single_threaded: false, filename_filter: None, progress_reporter: Box::new(NullProgressReporter), diff --git a/trial-uri.sh b/trial-uri.sh index 3b03432..d3b7c35 100755 --- a/trial-uri.sh +++ b/trial-uri.sh @@ -20,7 +20,7 @@ rm -Rf /tmp/testb mkdir /tmp/testb pushd /tmp/testb echo ripunzip: -time sh -c "$MYDIR/target/release/ripunzip uri \"$URI\" " +time sh -c "$MYDIR/target/release/ripunzip unzip-uri \"$URI\" " popd rm -Rf /tmp/testb