mirror of
				https://github.com/velopack/velopack.git
				synced 2025-10-25 15:19:22 +00:00 
			
		
		
		
	refactor/inline fastzip implementation for patching (upstream issues)
This commit is contained in:
		
							
								
								
									
										975
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										975
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										11
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								Cargo.toml
									
									
									
									
									
								
							| @@ -54,7 +54,7 @@ strum = { version = "0.27", features = ["derive"] } | ||||
| file-rotate = "0.8" | ||||
| simple-stopwatch = "0.1" | ||||
| enum-flags = "0.4" | ||||
| remove_dir_all = { git = "https://github.com/caesay/remove_dir_all.git", features = ["log"] } | ||||
| remove_dir_all = "1.0" | ||||
| sha1 = "0.10" | ||||
| sha2 = "0.10" | ||||
| sha1_smol = "1.0" | ||||
| @@ -84,9 +84,12 @@ core-foundation = "0.10" | ||||
| core-foundation-sys = "0.8" | ||||
| uuid = { version = "1.13.1", features = ["v4", "fast-rng", "macro-diagnostics"] } | ||||
| walkdir = "2.5" | ||||
| mtzip = "=4.0.2" | ||||
| ripunzip = "=2.0.1" | ||||
| zerofrom = "=0.1.5" | ||||
| rayon = "1.6" | ||||
| progress-streams = "1.1" | ||||
| flate2 = { version = "1.0", default-features = false } | ||||
| # mtzip = "=4.0.2" | ||||
| # ripunzip = "=2.0.1" | ||||
| # zerofrom = "=0.1.5" | ||||
|  | ||||
| # default to small, optimized workspace release binaries | ||||
| [profile.release] | ||||
|   | ||||
| @@ -67,9 +67,9 @@ zstd.workspace = true | ||||
| zip.workspace = true | ||||
| walkdir.workspace = true | ||||
| sha1_smol.workspace = true | ||||
| mtzip.workspace = true | ||||
| ripunzip.workspace = true | ||||
| zerofrom.workspace = true | ||||
| rayon.workspace = true | ||||
| progress-streams.workspace = true | ||||
| flate2.workspace = true | ||||
|  | ||||
| [target.'cfg(target_os="linux")'.dependencies] | ||||
| waitpid-any.workspace = true | ||||
|   | ||||
| @@ -1,12 +1,10 @@ | ||||
| use crate::shared::fastzip; | ||||
| use anyhow::{anyhow, bail, Result}; | ||||
| use mtzip::level::CompressionLevel; | ||||
| use ripunzip::{NullProgressReporter, UnzipEngine, UnzipOptions}; | ||||
| use std::{ | ||||
|     collections::HashSet, | ||||
|     fs, io, | ||||
|     path::{Path, PathBuf}, | ||||
| }; | ||||
| use walkdir::WalkDir; | ||||
|  | ||||
| pub fn zstd_patch_single<P1: AsRef<Path>, P2: AsRef<Path>, P3: AsRef<Path>>(old_file: P1, patch_file: P2, output_file: P3) -> Result<()> { | ||||
|     let old_file = old_file.as_ref(); | ||||
| @@ -53,22 +51,6 @@ fn fio_highbit64(v: u64) -> u32 { | ||||
|     return count; | ||||
| } | ||||
|  | ||||
| fn zip_extract<P1: AsRef<Path>, P2: AsRef<Path>>(archive_file: P1, target_dir: P2) -> Result<()> { | ||||
|     let target_dir = target_dir.as_ref().to_path_buf(); | ||||
|     let file = fs::File::open(archive_file)?; | ||||
|     let engine = UnzipEngine::for_file(file)?; | ||||
|     let null_progress = Box::new(NullProgressReporter {}); | ||||
|     let options = UnzipOptions { | ||||
|         filename_filter: None, | ||||
|         progress_reporter: null_progress, | ||||
|         output_directory: Some(target_dir), | ||||
|         password: None, | ||||
|         single_threaded: false, | ||||
|     }; | ||||
|     engine.unzip(options)?; | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| pub fn delta<P1: AsRef<Path>, P2: AsRef<Path>, P3: AsRef<Path>>( | ||||
|     old_file: P1, | ||||
|     delta_files: Vec<&PathBuf>, | ||||
| @@ -98,7 +80,7 @@ pub fn delta<P1: AsRef<Path>, P2: AsRef<Path>, P3: AsRef<Path>>( | ||||
|     info!("Extracting base package for delta patching: {}", temp_dir.to_string_lossy()); | ||||
|     let work_dir = temp_dir.join("_work"); | ||||
|     fs::create_dir_all(&work_dir)?; | ||||
|     zip_extract(&old_file, &work_dir)?; | ||||
|     fastzip::extract_to_directory(&old_file, &work_dir, None)?; | ||||
|  | ||||
|     info!("Base package extracted. {} delta packages to apply.", delta_files.len()); | ||||
|  | ||||
| @@ -106,9 +88,9 @@ pub fn delta<P1: AsRef<Path>, P2: AsRef<Path>, P3: AsRef<Path>>( | ||||
|         info!("{}: extracting apply delta patch: {}", i, delta_file.to_string_lossy()); | ||||
|         let delta_dir = temp_dir.join(format!("delta_{}", i)); | ||||
|         fs::create_dir_all(&delta_dir)?; | ||||
|         zip_extract(delta_file, &delta_dir)?; | ||||
|         fastzip::extract_to_directory(&delta_file, &delta_dir, None)?; | ||||
|  | ||||
|         let delta_relative_paths = enumerate_files_relative(&delta_dir); | ||||
|         let delta_relative_paths = fastzip::enumerate_files_relative(&delta_dir); | ||||
|         let mut visited_paths = HashSet::new(); | ||||
|  | ||||
|         // apply all the zsdiff patches for files which exist in both the delta and the base package | ||||
| @@ -160,7 +142,7 @@ pub fn delta<P1: AsRef<Path>, P2: AsRef<Path>, P3: AsRef<Path>>( | ||||
|         } | ||||
|  | ||||
|         // anything in the work dir which was not visited is an old / deleted file and should be removed | ||||
|         let workdir_relative_paths = enumerate_files_relative(&work_dir); | ||||
|         let workdir_relative_paths = fastzip::enumerate_files_relative(&work_dir); | ||||
|         for relative_path in &workdir_relative_paths { | ||||
|             if !visited_paths.contains(relative_path) { | ||||
|                 let file_to_delete = work_dir.join(relative_path); | ||||
| @@ -172,32 +154,12 @@ pub fn delta<P1: AsRef<Path>, P2: AsRef<Path>, P3: AsRef<Path>>( | ||||
|  | ||||
|     info!("All delta patches applied. Asembling output package at: {}", output_file.to_string_lossy()); | ||||
|  | ||||
|     let mut zipper = mtzip::ZipArchive::new(); | ||||
|     let workdir_relative_paths = enumerate_files_relative(&work_dir); | ||||
|     for relative_path in &workdir_relative_paths { | ||||
|         zipper | ||||
|             .add_file_from_fs(work_dir.join(&relative_path), relative_path.to_string_lossy().to_string()) | ||||
|             .compression_level(CompressionLevel::fast()) | ||||
|             .done(); | ||||
|     } | ||||
|     let mut file = fs::File::create(&output_file)?; | ||||
|     zipper.write(&mut file)?; | ||||
|     fastzip::compress_directory(&work_dir, &output_file, fastzip::CompressionLevel::fast())?; | ||||
|  | ||||
|     info!("Successfully applied {} delta patches in {}s.", delta_files.len(), time.s()); | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| fn enumerate_files_relative<P: AsRef<Path>>(dir: P) -> Vec<PathBuf> { | ||||
|     WalkDir::new(&dir) | ||||
|         .follow_links(false) | ||||
|         .into_iter() | ||||
|         .filter_map(|entry| entry.ok()) | ||||
|         .filter(|entry| entry.file_type().is_file()) | ||||
|         .map(|entry| entry.path().strip_prefix(&dir).map(|p| p.to_path_buf())) | ||||
|         .filter_map(|entry| entry.ok()) | ||||
|         .collect() | ||||
| } | ||||
|  | ||||
| // NOTE: this is some code to do checksum verification, but it is not being used | ||||
| // by the current implementation because zstd patching already has checksum verification | ||||
| // | ||||
|   | ||||
| @@ -7,49 +7,31 @@ use std::fs::File; | ||||
|  | ||||
| pub fn uninstall(locator: &VelopackLocator, delete_self: bool) -> Result<()> { | ||||
|     info!("Command: Uninstall"); | ||||
|      | ||||
|  | ||||
|     let root_path = locator.get_root_dir(); | ||||
|  | ||||
|     fn _uninstall_impl(locator: &VelopackLocator) -> bool { | ||||
|         let root_path = locator.get_root_dir(); | ||||
|          | ||||
|         // the real app could be running at the moment | ||||
|         let _ = shared::force_stop_package(&root_path); | ||||
|     // the real app could be running at the moment | ||||
|     let _ = shared::force_stop_package(&root_path); | ||||
|  | ||||
|         let mut finished_with_errors = false; | ||||
|     // run uninstall hook | ||||
|     windows::run_hook(&locator, constants::HOOK_CLI_UNINSTALL, 60); | ||||
|  | ||||
|         // run uninstall hook | ||||
|         windows::run_hook(&locator, constants::HOOK_CLI_UNINSTALL, 60); | ||||
|     // remove all shortcuts pointing to the app | ||||
|     windows::remove_all_shortcuts_for_root_dir(&root_path); | ||||
|  | ||||
|         // remove all shortcuts pointing to the app | ||||
|         windows::remove_all_shortcuts_for_root_dir(&root_path); | ||||
|     info!("Removing directory '{}'", root_path.to_string_lossy()); | ||||
|     let _ = remove_dir_all::remove_dir_all(&root_path); | ||||
|  | ||||
|         info!("Removing directory '{}'", root_path.to_string_lossy()); | ||||
|         if let Err(e) = shared::retry_io(|| remove_dir_all::remove_dir_but_not_self(&root_path)) { | ||||
|             error!("Unable to remove directory, some files may be in use ({}).", e); | ||||
|             finished_with_errors = true; | ||||
|         } | ||||
|          | ||||
|         if let Err(e) = windows::registry::remove_uninstall_entry(&locator) { | ||||
|             error!("Unable to remove uninstall registry entry ({}).", e); | ||||
|             // finished_with_errors = true; | ||||
|         } | ||||
|  | ||||
|         !finished_with_errors | ||||
|     if let Err(e) = windows::registry::remove_uninstall_entry(&locator) { | ||||
|         error!("Unable to remove uninstall registry entry ({}).", e); | ||||
|     } | ||||
|  | ||||
|     // if it returns true, it was a success. | ||||
|     // if it returns false, it was completed with errors which the user should be notified of. | ||||
|     let result = _uninstall_impl(&locator); | ||||
|     let app_title = locator.get_manifest_title(); | ||||
|  | ||||
|     if result { | ||||
|         info!("Finished successfully."); | ||||
|         shared::dialogs::show_info(format!("{} Uninstall", app_title).as_str(), None, "The application was successfully uninstalled."); | ||||
|     } else { | ||||
|         error!("Finished with errors."); | ||||
|         shared::dialogs::show_uninstall_complete_with_errors_dialog(&app_title, None); | ||||
|     } | ||||
|     info!("Finished successfully."); | ||||
|     shared::dialogs::show_info(format!("{} Uninstall", app_title).as_str(), None, "The application was successfully uninstalled."); | ||||
|  | ||||
|     let dead_path = root_path.join(".dead"); | ||||
|     let _ = File::create(dead_path); | ||||
|   | ||||
							
								
								
									
										168
									
								
								src/bins/src/shared/fastzip/cloneable_seekable_reader.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										168
									
								
								src/bins/src/shared/fastzip/cloneable_seekable_reader.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,168 @@ | ||||
| // Copyright 2022 Google LLC | ||||
|  | ||||
| // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or | ||||
| // https://www.apache.org/licenses/LICENSE-2.0> or the MIT license | ||||
| // <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your | ||||
| // option. This file may not be copied, modified, or distributed | ||||
| // except according to those terms. | ||||
|  | ||||
| use std::{ | ||||
|     io::{Read, Seek, SeekFrom}, | ||||
|     sync::{Arc, Mutex}, | ||||
| }; | ||||
|  | ||||
| use super::ripunzip::determine_stream_len; | ||||
|  | ||||
| struct Inner<R: Read + Seek> { | ||||
|     /// The underlying Read implementation. | ||||
|     r: R, | ||||
|     /// The position of r. | ||||
|     pos: u64, | ||||
|     /// The length of r, lazily loaded. | ||||
|     len: Option<u64>, | ||||
| } | ||||
|  | ||||
| impl<R: Read + Seek> Inner<R> { | ||||
|     fn new(r: R) -> Self { | ||||
|         Self { r, pos: 0, len: None } | ||||
|     } | ||||
|  | ||||
|     /// Get the length of the data stream. This is assumed to be constant. | ||||
|     fn len(&mut self) -> std::io::Result<u64> { | ||||
|         // Return cached size | ||||
|         if let Some(len) = self.len { | ||||
|             return Ok(len); | ||||
|         } | ||||
|  | ||||
|         let len = determine_stream_len(&mut self.r)?; | ||||
|         self.len = Some(len); | ||||
|         Ok(len) | ||||
|     } | ||||
|  | ||||
|     /// Read into the given buffer, starting at the given offset in the data stream. | ||||
|     fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> std::io::Result<usize> { | ||||
|         if offset != self.pos { | ||||
|             self.r.seek(SeekFrom::Start(offset))?; | ||||
|         } | ||||
|         let read_result = self.r.read(buf); | ||||
|         if let Ok(bytes_read) = read_result { | ||||
|             // TODO, once stabilised, use checked_add_signed | ||||
|             self.pos += bytes_read as u64; | ||||
|         } | ||||
|         read_result | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// A [`Read`] which refers to its underlying stream by reference count, | ||||
| /// and thus can be cloned cheaply. It supports seeking; each cloned instance | ||||
| /// maintains its own pointer into the file, and the underlying instance | ||||
| /// is seeked prior to each read. | ||||
| pub(crate) struct CloneableSeekableReader<R: Read + Seek> { | ||||
|     /// The wrapper around the Read implementation, shared between threads. | ||||
|     inner: Arc<Mutex<Inner<R>>>, | ||||
|     /// The position of _this_ reader. | ||||
|     pos: u64, | ||||
| } | ||||
|  | ||||
| impl<R: Read + Seek> Clone for CloneableSeekableReader<R> { | ||||
|     fn clone(&self) -> Self { | ||||
|         Self { inner: self.inner.clone(), pos: self.pos } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl<R: Read + Seek> CloneableSeekableReader<R> { | ||||
|     /// Constructor. Takes ownership of the underlying `Read`. | ||||
|     /// You should pass in only streams whose total length you expect | ||||
|     /// to be fixed and unchanging. Odd behavior may occur if the length | ||||
|     /// of the stream changes; any subsequent seeks will not take account | ||||
|     /// of the changed stream length. | ||||
|     pub(crate) fn new(r: R) -> Self { | ||||
|         Self { inner: Arc::new(Mutex::new(Inner::new(r))), pos: 0u64 } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl<R: Read + Seek> Read for CloneableSeekableReader<R> { | ||||
|     fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> { | ||||
|         let mut inner = self.inner.lock().unwrap(); | ||||
|         let read_result = inner.read_at(self.pos, buf); | ||||
|         if let Ok(bytes_read) = read_result { | ||||
|             self.pos = self | ||||
|                 .pos | ||||
|                 .checked_add(bytes_read as u64) | ||||
|                 .ok_or(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Read too far forward"))?; | ||||
|         } | ||||
|         read_result | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl<R: Read + Seek> Seek for CloneableSeekableReader<R> { | ||||
|     fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> { | ||||
|         let new_pos = match pos { | ||||
|             SeekFrom::Start(pos) => pos, | ||||
|             SeekFrom::End(offset_from_end) => { | ||||
|                 let file_len = self.inner.lock().unwrap().len()?; | ||||
|                 if -offset_from_end as u64 > file_len { | ||||
|                     return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Seek too far backwards")); | ||||
|                 } | ||||
|                 file_len | ||||
|                     .checked_add_signed(offset_from_end) | ||||
|                     .ok_or(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Seek too far backward from end"))? | ||||
|             } | ||||
|             SeekFrom::Current(offset_from_pos) => self | ||||
|                 .pos | ||||
|                 .checked_add_signed(offset_from_pos) | ||||
|                 .ok_or(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Seek too far forward from current pos"))?, | ||||
|         }; | ||||
|         self.pos = new_pos; | ||||
|         Ok(new_pos) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[cfg(test)] | ||||
| mod test { | ||||
|     use super::CloneableSeekableReader; | ||||
|     use std::io::{Cursor, Read, Seek, SeekFrom}; | ||||
|     // use test_log::test; | ||||
|  | ||||
|     #[test] | ||||
|     fn test_cloneable_seekable_reader() -> std::io::Result<()> { | ||||
|         let buf: Vec<u8> = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; | ||||
|         let buf = Cursor::new(buf); | ||||
|         let mut reader = CloneableSeekableReader::new(buf); | ||||
|         let mut out = vec![0; 2]; | ||||
|         reader.read_exact(&mut out)?; | ||||
|         assert_eq!(&out, &[0, 1]); | ||||
|         reader.rewind()?; | ||||
|         reader.read_exact(&mut out)?; | ||||
|         assert_eq!(&out, &[0, 1]); | ||||
|         reader.stream_position()?; | ||||
|         reader.read_exact(&mut out)?; | ||||
|         assert_eq!(&out, &[2, 3]); | ||||
|         reader.seek(SeekFrom::End(-2))?; | ||||
|         reader.read_exact(&mut out)?; | ||||
|         assert_eq!(&out, &[8, 9]); | ||||
|         assert!(reader.read_exact(&mut out).is_err()); | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_cloned_independent_positions() -> std::io::Result<()> { | ||||
|         let buf: Vec<u8> = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; | ||||
|         let buf = Cursor::new(buf); | ||||
|         let mut r1 = CloneableSeekableReader::new(buf); | ||||
|         let mut r2 = r1.clone(); | ||||
|         let mut out = vec![0; 2]; | ||||
|         r1.read_exact(&mut out)?; | ||||
|         assert_eq!(&out, &[0, 1]); | ||||
|         r2.read_exact(&mut out)?; | ||||
|         assert_eq!(&out, &[0, 1]); | ||||
|         r1.read_exact(&mut out)?; | ||||
|         assert_eq!(&out, &[2, 3]); | ||||
|         r2.seek(SeekFrom::End(-2))?; | ||||
|         r2.read_exact(&mut out)?; | ||||
|         assert_eq!(&out, &[8, 9]); | ||||
|         r1.read_exact(&mut out)?; | ||||
|         assert_eq!(&out, &[4, 5]); | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										86
									
								
								src/bins/src/shared/fastzip/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								src/bins/src/shared/fastzip/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,86 @@ | ||||
| #![allow(dead_code)] | ||||
|  | ||||
| mod cloneable_seekable_reader; | ||||
| mod mtzip; | ||||
| mod progress_updater; | ||||
| mod ripunzip; | ||||
|  | ||||
| use anyhow::Result; | ||||
| pub use mtzip::level::CompressionLevel; | ||||
| use ripunzip::{UnzipEngine, UnzipOptions}; | ||||
| use std::{ | ||||
|     fs::File, | ||||
|     path::{Path, PathBuf}, | ||||
| }; | ||||
| use walkdir::WalkDir; | ||||
|  | ||||
| /// A trait of types which wish to hear progress updates on the unzip. | ||||
| pub trait UnzipProgressReporter: Sync { | ||||
|     /// Extraction has begun on a file. | ||||
|     fn extraction_starting(&self, _display_name: &str) {} | ||||
|     /// Extraction has finished on a file. | ||||
|     fn extraction_finished(&self, _display_name: &str) {} | ||||
|     /// The total number of compressed bytes we expect to extract. | ||||
|     fn total_bytes_expected(&self, _expected: u64) {} | ||||
|     /// Some bytes of a file have been decompressed. This is probably | ||||
|     /// the best way to display an overall progress bar. This should eventually | ||||
|     /// add up to the number you're given using `total_bytes_expected`. | ||||
|     /// The 'count' parameter is _not_ a running total - you must add up | ||||
|     /// each call to this function into the running total. | ||||
|     /// It's a bit unfortunate that we give compressed bytes rather than | ||||
|     /// uncompressed bytes, but currently we can't calculate uncompressed | ||||
|     /// bytes without downloading the whole zip file first, which rather | ||||
|     /// defeats the point. | ||||
|     fn bytes_extracted(&self, _count: u64) {} | ||||
| } | ||||
|  | ||||
| /// A progress reporter which does nothing. | ||||
| struct NullProgressReporter; | ||||
|  | ||||
| impl UnzipProgressReporter for NullProgressReporter {} | ||||
|  | ||||
| pub fn extract_to_directory<'b, P1: AsRef<Path>, P2: AsRef<Path>>( | ||||
|     archive_file: P1, | ||||
|     target_dir: P2, | ||||
|     progress_reporter: Option<Box<dyn UnzipProgressReporter + Sync + 'b>>, | ||||
| ) -> Result<()> { | ||||
|     let target_dir = target_dir.as_ref().to_path_buf(); | ||||
|     let file = File::open(archive_file)?; | ||||
|     let engine = UnzipEngine::for_file(file)?; | ||||
|     let null_progress = Box::new(NullProgressReporter {}); | ||||
|     let options = UnzipOptions { | ||||
|         filename_filter: None, | ||||
|         progress_reporter: progress_reporter.unwrap_or(null_progress), | ||||
|         output_directory: Some(target_dir), | ||||
|         password: None, | ||||
|         single_threaded: false, | ||||
|     }; | ||||
|     engine.unzip(options)?; | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| pub fn compress_directory<'b, P1: AsRef<Path>, P2: AsRef<Path>>(target_dir: P1, output_file: P2, level: CompressionLevel) -> Result<()> { | ||||
|     let target_dir = target_dir.as_ref().to_path_buf(); | ||||
|     let mut zipper = mtzip::ZipArchive::new(); | ||||
|     let workdir_relative_paths = enumerate_files_relative(&target_dir); | ||||
|     for relative_path in &workdir_relative_paths { | ||||
|         zipper | ||||
|             .add_file_from_fs(target_dir.join(&relative_path), relative_path.to_string_lossy().to_string()) | ||||
|             .compression_level(level) | ||||
|             .done(); | ||||
|     } | ||||
|     let mut file = File::create(&output_file)?; | ||||
|     zipper.write_with_rayon(&mut file)?; | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| pub fn enumerate_files_relative<P: AsRef<Path>>(dir: P) -> Vec<PathBuf> { | ||||
|     WalkDir::new(&dir) | ||||
|         .follow_links(false) | ||||
|         .into_iter() | ||||
|         .filter_map(|entry| entry.ok()) | ||||
|         .filter(|entry| entry.file_type().is_file()) | ||||
|         .map(|entry| entry.path().strip_prefix(&dir).map(|p| p.to_path_buf())) | ||||
|         .filter_map(|entry| entry.ok()) | ||||
|         .collect() | ||||
| } | ||||
							
								
								
									
										126
									
								
								src/bins/src/shared/fastzip/mtzip/level.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								src/bins/src/shared/fastzip/mtzip/level.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,126 @@ | ||||
| //! Compression level | ||||
|  | ||||
| use core::fmt::Display; | ||||
| use std::error::Error; | ||||
|  | ||||
| use flate2::Compression; | ||||
|  | ||||
| /// Compression level that should be used when compressing a file or data. | ||||
| /// | ||||
| /// Current compression providers support only levels from 0 to 9, so these are the only ones being | ||||
| /// supported. | ||||
| #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] | ||||
| pub struct CompressionLevel(u8); | ||||
|  | ||||
| impl CompressionLevel { | ||||
|     /// Construct a new value of a compression level setting. | ||||
|     /// | ||||
|     /// The integer value must be less than or equal to 9, otherwise `None` is returned | ||||
|     #[inline] | ||||
|     pub const fn new(level: u8) -> Option<Self> { | ||||
|         if level <= 9 { Some(Self(level)) } else { None } | ||||
|     } | ||||
|  | ||||
|     /// Construct a new value of a compression level setting without checking the value. | ||||
|     /// | ||||
|     /// # Safety | ||||
|     /// | ||||
|     /// The value must be a valid supported compression level | ||||
|     #[inline] | ||||
|     pub const unsafe fn new_unchecked(level: u8) -> Self { | ||||
|         Self(level) | ||||
|     } | ||||
|  | ||||
|     /// No compression | ||||
|     #[inline] | ||||
|     pub const fn none() -> Self { | ||||
|         Self(0) | ||||
|     } | ||||
|  | ||||
|     /// Fastest compression | ||||
|     #[inline] | ||||
|     pub const fn fast() -> Self { | ||||
|         Self(1) | ||||
|     } | ||||
|  | ||||
|     /// Balanced level with moderate compression and speed. The raw value is 6. | ||||
|     #[inline] | ||||
|     pub const fn balanced() -> Self { | ||||
|         Self(6) | ||||
|     } | ||||
|  | ||||
|     /// Best compression ratio, comes at a worse performance | ||||
|     #[inline] | ||||
|     pub const fn best() -> Self { | ||||
|         Self(9) | ||||
|     } | ||||
|  | ||||
|     /// Get the compression level as an integer | ||||
|     #[inline] | ||||
|     pub const fn get(self) -> u8 { | ||||
|         self.0 | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Default for CompressionLevel { | ||||
|     /// Equivalent to [`Self::balanced`] | ||||
|     fn default() -> Self { | ||||
|         Self::balanced() | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// The number for compression level was invalid | ||||
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||||
| pub struct InvalidCompressionLevel(u32); | ||||
|  | ||||
| impl InvalidCompressionLevel { | ||||
|     /// The value which was supplied | ||||
|     pub fn value(self) -> u32 { | ||||
|         self.0 | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Display for InvalidCompressionLevel { | ||||
|     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||
|         write!(f, "Invalid compression level number: {}", self.0) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Error for InvalidCompressionLevel {} | ||||
|  | ||||
| impl From<CompressionLevel> for Compression { | ||||
|     #[inline] | ||||
|     fn from(value: CompressionLevel) -> Self { | ||||
|         Compression::new(value.0.into()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl TryFrom<Compression> for CompressionLevel { | ||||
|     type Error = InvalidCompressionLevel; | ||||
|  | ||||
|     fn try_from(value: Compression) -> Result<Self, Self::Error> { | ||||
|         let level = value.level(); | ||||
|         Self::new( | ||||
|             level | ||||
|                 .try_into() | ||||
|                 .map_err(|_| InvalidCompressionLevel(level))?, | ||||
|         ) | ||||
|         .ok_or(InvalidCompressionLevel(level)) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<CompressionLevel> for u8 { | ||||
|     #[inline] | ||||
|     fn from(value: CompressionLevel) -> Self { | ||||
|         value.0 | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl TryFrom<u8> for CompressionLevel { | ||||
|     type Error = InvalidCompressionLevel; | ||||
|  | ||||
|     #[inline] | ||||
|     fn try_from(value: u8) -> Result<Self, Self::Error> { | ||||
|         Self::new(value).ok_or(InvalidCompressionLevel(value.into())) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										433
									
								
								src/bins/src/shared/fastzip/mtzip/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										433
									
								
								src/bins/src/shared/fastzip/mtzip/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,433 @@ | ||||
| //! # mtzip | ||||
| //! | ||||
| //! MTZIP (Stands for Multi-Threaded ZIP) is a library for making zip archives while utilising all | ||||
| //! available performance available with multithreading. The amount of threads can be limited by | ||||
| //! the user or detected automatically. | ||||
| //! | ||||
| //! Example usage: | ||||
| //! | ||||
| //! ```ignore | ||||
| //! # use std::path::Path; | ||||
| //! # use std::fs::File; | ||||
| //! use mtzip::ZipArchive; | ||||
| //! | ||||
| //! // Creating the zipper that holds data and handles compression | ||||
| //! let mut zipper = ZipArchive::new(); | ||||
| //! | ||||
| //! // Adding a file from filesystem | ||||
| //! zipper.add_file_from_fs( | ||||
| //!     Path::new("input/test_text_file.txt"), | ||||
| //!     "test_text_file.txt".to_owned(), | ||||
| //! ); | ||||
| //! | ||||
| //! // Adding a file with data from a memory location | ||||
| //! zipper.add_file_from_memory(b"Hello, world!", "hello_world.txt".to_owned()); | ||||
| //! | ||||
| //! // Adding a directory and a file to it | ||||
| //! zipper.add_directory("test_dir".to_owned()); | ||||
| //! zipper.add_file_from_fs( | ||||
| //!     Path::new("input/file_that_goes_to_a_dir.txt"), | ||||
| //!     "test_dir/file_that_goes_to_a_dir.txt".to_owned(), | ||||
| //! ); | ||||
| //! | ||||
| //! // Writing to a file | ||||
| //! // First, open the file | ||||
| //! let mut file = File::create("output.zip").unwrap(); | ||||
| //! // Then, write to it | ||||
| //! zipper.write(&mut file); // Amount of threads is chosen automatically | ||||
| //! ``` | ||||
|  | ||||
| use std::{ | ||||
|     borrow::Cow, | ||||
|     io::{Read, Seek, Write}, | ||||
|     num::NonZeroUsize, | ||||
|     panic::{RefUnwindSafe, UnwindSafe}, | ||||
|     path::Path, | ||||
|     sync::{mpsc, Mutex}, | ||||
| }; | ||||
|  | ||||
| use level::CompressionLevel; | ||||
| use rayon::prelude::*; | ||||
| use zip_archive_parts::{ | ||||
|     data::ZipData, | ||||
|     extra_field::{ExtraField, ExtraFields}, | ||||
|     file::ZipFile, | ||||
|     job::{ZipJob, ZipJobOrigin}, | ||||
| }; | ||||
|  | ||||
| pub mod level; | ||||
| mod platform; | ||||
| mod zip_archive_parts; | ||||
|  | ||||
| // TODO: tests, maybe examples | ||||
|  | ||||
| /// Compression type for the file. Directories always use [`Stored`](CompressionType::Stored). | ||||
| /// Default is [`Deflate`](CompressionType::Deflate). | ||||
| #[repr(u16)] | ||||
| #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] | ||||
| pub enum CompressionType { | ||||
|     /// No compression at all, the data is stored as-is. | ||||
|     /// | ||||
|     /// This is used for directories because they have no data (no payload) | ||||
|     Stored = 0, | ||||
|     #[default] | ||||
|     /// Deflate compression, the most common in ZIP files. | ||||
|     Deflate = 8, | ||||
| } | ||||
|  | ||||
| /// Builder used to optionally add additional attributes to a file or directory. | ||||
| /// The default compression type is [`CompressionType::Deflate`] and default compression level is | ||||
| /// [`CompressionLevel::best`] | ||||
| #[must_use] | ||||
| #[derive(Debug)] | ||||
| pub struct ZipFileBuilder<'a, 'b> { | ||||
|     archive_handle: &'a mut ZipArchive<'b>, | ||||
|     job: ZipJob<'b>, | ||||
| } | ||||
|  | ||||
| impl<'a, 'b> ZipFileBuilder<'a, 'b> { | ||||
|     /// Call this when you're done configuring the file entry and it will be added to the job list, | ||||
|     /// or directly into the resulting dataset if it's a directory. Always needs to be called. | ||||
|     pub fn done(self) { | ||||
|         let Self { archive_handle, job } = self; | ||||
|         match &job.data_origin { | ||||
|             ZipJobOrigin::Directory => { | ||||
|                 let file = job.into_file().expect("No failing code path"); | ||||
|                 archive_handle.push_file(file); | ||||
|             } | ||||
|             _ => archive_handle.push_job(job), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Read filesystem metadata from filesystem and add the properties to this file. It sets | ||||
|     /// external attributes (as with [`Self::external_attributes`]) and adds extra fields generated | ||||
|     /// with [`ExtraFields::new_from_fs`] | ||||
|     pub fn metadata_from_fs(self, fs_path: &Path) -> std::io::Result<Self> { | ||||
|         let metadata = std::fs::metadata(fs_path)?; | ||||
|         let external_attributes = platform::attributes_from_fs(&metadata); | ||||
|         let extra_fields = ExtraFields::new_from_fs(&metadata); | ||||
|         Ok(self.external_attributes(external_attributes).extra_fields(extra_fields)) | ||||
|     } | ||||
|  | ||||
|     /// Add a file comment. | ||||
|     pub fn file_comment(mut self, comment: String) -> Self { | ||||
|         self.job.file_comment = Some(comment); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Add additional [`ExtraField`]. | ||||
|     pub fn extra_field(mut self, extra_field: ExtraField) -> Self { | ||||
|         self.job.extra_fields.values.push(extra_field); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Add additional [`ExtraField`]s. | ||||
|     pub fn extra_fields(mut self, extra_fields: impl IntoIterator<Item = ExtraField>) -> Self { | ||||
|         self.job.extra_fields.extend(extra_fields); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Set compression type. Ignored for directories, as they use no compression. | ||||
|     /// | ||||
|     /// Default is [`CompressionType::Deflate`]. | ||||
|     pub fn compression_type(mut self, compression_type: CompressionType) -> Self { | ||||
|         self.job.compression_type = compression_type; | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Set compression level. Ignored for directories, as they use no compression. | ||||
|     /// | ||||
|     /// Default is [`CompressionLevel::best`] | ||||
|     pub fn compression_level(mut self, compression_level: CompressionLevel) -> Self { | ||||
|         self.job.compression_level = compression_level; | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Set external attributes. The format depends on a filesystem and is mostly a legacy | ||||
|     /// mechanism, usually a default value is used if this is not a filesystem source. When a file | ||||
|     /// is added from the filesystem, these attributes will be read and used and the ones set wit | ||||
|     /// hthis method are ignored. | ||||
|     pub fn external_attributes(mut self, external_attributes: u16) -> Self { | ||||
|         self.job.external_attributes = external_attributes; | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Set external file attributes from a filesystem item. Use of this method is discouraged in | ||||
|     /// favor of [`Self::metadata_from_fs`], which also sets extra fields which contain modern | ||||
|     /// filesystem attributes instead of using old 16-bit system-dependent format. | ||||
|     pub fn external_attributes_from_fs(mut self, fs_path: &Path) -> std::io::Result<Self> { | ||||
|         let metadata = std::fs::metadata(fs_path)?; | ||||
|         self.job.external_attributes = platform::attributes_from_fs(&metadata); | ||||
|         Ok(self) | ||||
|     } | ||||
|  | ||||
|     #[inline] | ||||
|     fn new(archive: &'a mut ZipArchive<'b>, filename: String, origin: ZipJobOrigin<'b>) -> Self { | ||||
|         Self { | ||||
|             archive_handle: archive, | ||||
|             job: ZipJob { | ||||
|                 data_origin: origin, | ||||
|                 archive_path: filename, | ||||
|                 extra_fields: ExtraFields::default(), | ||||
|                 file_comment: None, | ||||
|                 external_attributes: platform::default_file_attrs(), | ||||
|                 compression_type: CompressionType::Deflate, | ||||
|                 compression_level: CompressionLevel::best(), | ||||
|             }, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     #[inline] | ||||
|     fn new_dir(archive: &'a mut ZipArchive<'b>, filename: String) -> Self { | ||||
|         Self { | ||||
|             archive_handle: archive, | ||||
|             job: ZipJob { | ||||
|                 data_origin: ZipJobOrigin::Directory, | ||||
|                 archive_path: filename, | ||||
|                 extra_fields: ExtraFields::default(), | ||||
|                 file_comment: None, | ||||
|                 external_attributes: platform::default_dir_attrs(), | ||||
|                 compression_type: CompressionType::Deflate, | ||||
|                 compression_level: CompressionLevel::best(), | ||||
|             }, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Structure that holds the current state of ZIP archive creation. | ||||
| /// | ||||
| /// # Lifetimes | ||||
| /// | ||||
| /// Because some of the methods allow supplying borrowed data, the lifetimes are used to indicate | ||||
| /// that [`Self`](ZipArchive) borrows them. If you only provide owned data, such as | ||||
| /// [`Vec<u8>`](Vec) or [`PathBuf`](std::path::PathBuf), you won't have to worry about lifetimes | ||||
| /// and can simply use `'static`, if you ever need to specify them in your code. | ||||
| /// | ||||
| /// The lifetime `'a` is for the borrowed data passed in | ||||
| /// [`add_file_from_memory`](Self::add_file_from_memory), | ||||
| /// [`add_file_from_fs`](Self::add_file_from_fs) and | ||||
| /// [`add_file_from_reader`](Self::add_file_from_reader) | ||||
| #[derive(Debug, Default)] | ||||
| pub struct ZipArchive<'a> { | ||||
|     jobs_queue: Vec<ZipJob<'a>>, | ||||
|     data: ZipData, | ||||
| } | ||||
|  | ||||
| impl<'a> ZipArchive<'a> { | ||||
|     fn push_job(&mut self, job: ZipJob<'a>) { | ||||
|         self.jobs_queue.push(job); | ||||
|     } | ||||
|  | ||||
|     fn push_file(&mut self, file: ZipFile) { | ||||
|         self.data.files.push(file); | ||||
|     } | ||||
|  | ||||
|     /// Create an empty [`ZipArchive`] | ||||
|     #[inline] | ||||
|     pub fn new() -> Self { | ||||
|         Self::default() | ||||
|     } | ||||
|  | ||||
|     /// Add file from filesystem. | ||||
|     /// | ||||
|     /// Opens the file and reads data from it when [`compress`](Self::compress) is called. | ||||
|     /// | ||||
|     /// ``` | ||||
|     /// # use mtzip::ZipArchive; | ||||
|     /// # use std::path::Path; | ||||
|     /// let mut zipper = ZipArchive::new(); | ||||
|     /// zipper | ||||
|     ///     .add_file_from_fs(Path::new("input.txt"), "input.txt".to_owned()) | ||||
|     ///     .done(); | ||||
|     /// ``` | ||||
|     #[inline] | ||||
|     pub fn add_file_from_fs(&mut self, fs_path: impl Into<Cow<'a, Path>>, archived_path: String) -> ZipFileBuilder<'_, 'a> { | ||||
|         ZipFileBuilder::new(self, archived_path, ZipJobOrigin::Filesystem { path: fs_path.into() }) | ||||
|     } | ||||
|  | ||||
|     /// Add file with data from memory. | ||||
|     /// | ||||
|     /// The data can be either borrowed or owned by the [`ZipArchive`] struct to avoid lifetime | ||||
|     /// hell. | ||||
|     /// | ||||
|     /// ``` | ||||
|     /// # use mtzip::ZipArchive; | ||||
|     /// # use std::path::Path; | ||||
|     /// let mut zipper = ZipArchive::new(); | ||||
|     /// let data: &[u8] = "Hello, world!".as_ref(); | ||||
|     /// zipper | ||||
|     ///     .add_file_from_memory(data, "hello_world.txt".to_owned()) | ||||
|     ///     .done(); | ||||
|     /// ``` | ||||
|     #[inline] | ||||
|     pub fn add_file_from_memory(&mut self, data: impl Into<Cow<'a, [u8]>>, archived_path: String) -> ZipFileBuilder<'_, 'a> { | ||||
|         ZipFileBuilder::new(self, archived_path, ZipJobOrigin::RawData(data.into())) | ||||
|     } | ||||
|  | ||||
|     /// Add a file with data from a reader. | ||||
|     /// | ||||
|     /// This method takes any type implementing [`Read`] and allows it to have borrowed data (`'r`) | ||||
|     /// | ||||
|     /// ``` | ||||
|     /// # use mtzip::ZipArchive; | ||||
|     /// # use std::path::Path; | ||||
|     /// let mut zipper = ZipArchive::new(); | ||||
|     /// let data_input = std::io::stdin(); | ||||
|     /// zipper | ||||
|     ///     .add_file_from_reader(data_input, "stdin_file.txt".to_owned()) | ||||
|     ///     .done(); | ||||
|     /// ``` | ||||
|     #[inline] | ||||
|     pub fn add_file_from_reader<R: Read + Send + Sync + UnwindSafe + RefUnwindSafe + 'a>( | ||||
|         &mut self, | ||||
|         reader: R, | ||||
|         archived_path: String, | ||||
|     ) -> ZipFileBuilder<'_, 'a> { | ||||
|         ZipFileBuilder::new(self, archived_path, ZipJobOrigin::Reader(Box::new(reader))) | ||||
|     } | ||||
|  | ||||
|     /// Add a directory entry. | ||||
|     /// | ||||
|     /// All directories in the tree should be added. This method does not asssociate any filesystem | ||||
|     /// properties to the entry. | ||||
|     /// | ||||
|     /// ``` | ||||
|     /// # use mtzip::ZipArchive; | ||||
|     /// # use std::path::Path; | ||||
|     /// let mut zipper = ZipArchive::new(); | ||||
|     /// zipper.add_directory("test_dir/".to_owned()).done(); | ||||
|     /// ``` | ||||
|     #[inline] | ||||
|     pub fn add_directory(&mut self, archived_path: String) -> ZipFileBuilder<'_, 'a> { | ||||
|         ZipFileBuilder::new_dir(self, archived_path) | ||||
|     } | ||||
|  | ||||
|     /// Compress contents. Will be done automatically on [`write`](Self::write) call if files were | ||||
|     /// added between last compression and [`write`](Self::write) call. Automatically chooses | ||||
|     /// amount of threads to use based on how much are available. | ||||
|     #[inline] | ||||
|     pub fn compress(&mut self) { | ||||
|         self.compress_with_threads(Self::get_threads()); | ||||
|     } | ||||
|  | ||||
|     /// Compress contents. Will be done automatically on | ||||
|     /// [`write_with_threads`](Self::write_with_threads) call if files were added between last | ||||
|     /// compression and [`write`](Self::write). Allows specifying amount of threads that will be | ||||
|     /// used. | ||||
|     /// | ||||
|     /// Example of getting amount of threads that this library uses in | ||||
|     /// [`compress`](Self::compress): | ||||
|     /// | ||||
|     /// ``` | ||||
|     /// # use std::num::NonZeroUsize; | ||||
|     /// # use mtzip::ZipArchive; | ||||
|     /// # let mut zipper = ZipArchive::new(); | ||||
|     /// let threads = std::thread::available_parallelism() | ||||
|     ///     .map(NonZeroUsize::get) | ||||
|     ///     .unwrap_or(1); | ||||
|     /// | ||||
|     /// zipper.compress_with_threads(threads); | ||||
|     /// ``` | ||||
|     #[inline] | ||||
|     pub fn compress_with_threads(&mut self, threads: usize) { | ||||
|         if !self.jobs_queue.is_empty() { | ||||
|             self.compress_with_consumer(threads, |zip_data, rx| zip_data.files.extend(rx)) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Write compressed data to a writer (usually a file). Executes [`compress`](Self::compress) | ||||
|     /// if files were added between last [`compress`](Self::compress) call and this call. | ||||
|     /// Automatically chooses the amount of threads cpu has. | ||||
|     #[inline] | ||||
|     pub fn write<W: Write + Seek>(&mut self, writer: &mut W) -> std::io::Result<()> { | ||||
|         self.write_with_threads(writer, Self::get_threads()) | ||||
|     } | ||||
|  | ||||
|     /// Write compressed data to a writer (usually a file). Executes | ||||
|     /// [`compress_with_threads`](Self::compress_with_threads) if files were added between last | ||||
|     /// [`compress`](Self::compress) call and this call. Allows specifying amount of threads that | ||||
|     /// will be used. | ||||
|     /// | ||||
|     /// Example of getting amount of threads that this library uses in [`write`](Self::write): | ||||
|     /// | ||||
|     /// ``` | ||||
|     /// # use std::num::NonZeroUsize; | ||||
|     /// # use mtzip::ZipArchive; | ||||
|     /// # let mut zipper = ZipArchive::new(); | ||||
|     /// let threads = std::thread::available_parallelism() | ||||
|     ///     .map(NonZeroUsize::get) | ||||
|     ///     .unwrap_or(1); | ||||
|     /// | ||||
|     /// zipper.compress_with_threads(threads); | ||||
|     /// ``` | ||||
|     #[inline] | ||||
|     pub fn write_with_threads<W: Write + Seek>(&mut self, writer: &mut W, threads: usize) -> std::io::Result<()> { | ||||
|         if !self.jobs_queue.is_empty() { | ||||
|             self.compress_with_consumer(threads, |zip_data, rx| zip_data.write(writer, rx)) | ||||
|         } else { | ||||
|             self.data.write(writer, std::iter::empty()) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Starts the compression jobs and passes teh mpsc receiver to teh consumer function, which | ||||
|     /// might either store the data in [`ZipData`] - [`Self::compress_with_threads`]; or write the | ||||
|     /// zip data as soon as it's available - [`Self::write_with_threads`] | ||||
|     fn compress_with_consumer<F, T>(&mut self, threads: usize, consumer: F) -> T | ||||
|     where | ||||
|         F: FnOnce(&mut ZipData, mpsc::Receiver<ZipFile>) -> T, | ||||
|     { | ||||
|         let jobs_drain = Mutex::new(self.jobs_queue.drain(..)); | ||||
|         let jobs_drain_ref = &jobs_drain; | ||||
|         std::thread::scope(|s| { | ||||
|             let rx = { | ||||
|                 let (tx, rx) = mpsc::channel(); | ||||
|                 for _ in 0..threads { | ||||
|                     let thread_tx = tx.clone(); | ||||
|                     s.spawn(move || loop { | ||||
|                         let next_job = jobs_drain_ref.lock().unwrap().next_back(); | ||||
|                         if let Some(job) = next_job { | ||||
|                             thread_tx.send(job.into_file().unwrap()).unwrap(); | ||||
|                         } else { | ||||
|                             break; | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|                 rx | ||||
|             }; | ||||
|             consumer(&mut self.data, rx) | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     fn get_threads() -> usize { | ||||
|         std::thread::available_parallelism().map(NonZeroUsize::get).unwrap_or(1) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl ZipArchive<'_> { | ||||
|     /// Compress contents and use rayon for parallelism. | ||||
|     /// | ||||
|     /// Uses whatever thread pool this function is executed in. | ||||
|     /// | ||||
|     /// If you want to limit the amount of threads to be used, use | ||||
|     /// [`rayon::ThreadPoolBuilder::num_threads`] and either set it as a global pool, or | ||||
|     /// [`rayon::ThreadPool::install`] the call to this method in it. | ||||
|     pub fn compress_with_rayon(&mut self) { | ||||
|         if !self.jobs_queue.is_empty() { | ||||
|             let files_par_iter = self.jobs_queue.par_drain(..).map(|job| job.into_file().unwrap()); | ||||
|             self.data.files.par_extend(files_par_iter) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Write the contents to a writer. | ||||
|     /// | ||||
|     /// This method uses teh same thread logic as [`Self::compress_with_rayon`], refer to  its | ||||
|     /// documentation for details on how to control the parallelism and thread allocation. | ||||
|     pub fn write_with_rayon<W: Write + Seek + Send>(&mut self, writer: &mut W) -> std::io::Result<()> { | ||||
|         if !self.jobs_queue.is_empty() { | ||||
|             let files_par_iter = self.jobs_queue.par_drain(..).map(|job| job.into_file().unwrap()); | ||||
|             self.data.write_rayon(writer, files_par_iter) | ||||
|         } else { | ||||
|             self.data.write_rayon(writer, rayon::iter::empty()) | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										96
									
								
								src/bins/src/shared/fastzip/mtzip/platform/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								src/bins/src/shared/fastzip/mtzip/platform/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,96 @@ | ||||
| //! Platform-specific stuff | ||||
|  | ||||
| use std::fs::Metadata; | ||||
|  | ||||
| #[cfg(target_os = "windows")] | ||||
| /// OS - Windows, id 11 per Info-Zip spec | ||||
| /// Specification version 6.2 | ||||
| pub(crate) const VERSION_MADE_BY: u16 = (11 << 8) + 62; | ||||
|  | ||||
| #[cfg(target_os = "macos")] | ||||
| /// OS - MacOS darwin, id 19 | ||||
| /// Specification version 6.2 | ||||
| pub(crate) const VERSION_MADE_BY: u16 = (19 << 8) + 62; | ||||
|  | ||||
| #[cfg(not(any(target_os = "windows", target_os = "macos")))] | ||||
| // Fallback | ||||
| /// OS - Unix assumed, id 3 | ||||
| /// Specification version 6.2 | ||||
| pub(crate) const VERSION_MADE_BY: u16 = (3 << 8) + 62; | ||||
|  | ||||
| #[allow(dead_code)] | ||||
| pub(crate) const DEFAULT_UNIX_FILE_ATTRS: u16 = 0o100644; | ||||
| #[allow(dead_code)] | ||||
| pub(crate) const DEFAULT_UNIX_DIR_ATTRS: u16 = 0o040755; | ||||
|  | ||||
| #[cfg(target_os = "windows")] | ||||
| pub(crate) const DEFAULT_WINDOWS_FILE_ATTRS: u16 = 128; | ||||
| #[cfg(target_os = "windows")] | ||||
| pub(crate) const DEFAULT_WINDOWS_DIR_ATTRS: u16 = 16; | ||||
|  | ||||
| #[inline] | ||||
| #[allow(dead_code)] | ||||
| const fn convert_attrs(attrs: u32) -> u16 { | ||||
|     attrs as u16 | ||||
| } | ||||
|  | ||||
| pub(crate) fn attributes_from_fs(metadata: &Metadata) -> u16 { | ||||
|     #[cfg(target_os = "windows")] | ||||
|     { | ||||
|         use std::os::windows::fs::MetadataExt; | ||||
|         return convert_attrs(metadata.file_attributes()); | ||||
|     } | ||||
|  | ||||
|     #[cfg(target_os = "linux")] | ||||
|     { | ||||
|         use std::os::linux::fs::MetadataExt; | ||||
|         return convert_attrs(metadata.st_mode()); | ||||
|     } | ||||
|  | ||||
|     #[cfg(target_os = "macos")] | ||||
|     { | ||||
|         use std::os::darwin::fs::MetadataExt; | ||||
|         return convert_attrs(metadata.st_mode()); | ||||
|     } | ||||
|  | ||||
|     #[cfg(all(unix, not(target_os = "linux"), not(target_os = "macos")))] | ||||
|     { | ||||
|         use std::os::unix::fs::PermissionsExt; | ||||
|         return convert_attrs(metadata.permissions().mode()); | ||||
|     } | ||||
|  | ||||
|     #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos", unix)))] | ||||
|     { | ||||
|         if metadata.is_dir() { | ||||
|             return DEFAULT_UNIX_DIR_ATTRS; | ||||
|         } else { | ||||
|             return DEFAULT_UNIX_FILE_ATTRS; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[cfg(target_os = "windows")] | ||||
| pub(crate) const fn default_file_attrs() -> u16 { | ||||
|     DEFAULT_WINDOWS_FILE_ATTRS | ||||
| } | ||||
|  | ||||
| #[cfg(not(windows))] | ||||
| pub(crate) const fn default_file_attrs() -> u16 { | ||||
|     DEFAULT_UNIX_FILE_ATTRS | ||||
| } | ||||
|  | ||||
| #[cfg(target_os = "windows")] | ||||
| pub(crate) const fn default_dir_attrs() -> u16 { | ||||
|     DEFAULT_WINDOWS_DIR_ATTRS | ||||
| } | ||||
|  | ||||
| #[cfg(any(target_os = "linux", unix))] | ||||
| #[cfg(not(target_os = "windows"))] | ||||
| pub(crate) const fn default_dir_attrs() -> u16 { | ||||
|     DEFAULT_UNIX_DIR_ATTRS | ||||
| } | ||||
|  | ||||
| #[cfg(not(any(target_os = "windows", target_os = "linux", unix)))] | ||||
| pub(crate) const fn default_dir_attrs() -> u16 { | ||||
|     0 | ||||
| } | ||||
							
								
								
									
										156
									
								
								src/bins/src/shared/fastzip/mtzip/zip_archive_parts/data.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								src/bins/src/shared/fastzip/mtzip/zip_archive_parts/data.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,156 @@ | ||||
| use std::io::{Seek, Write}; | ||||
| use std::sync::Mutex; | ||||
|  | ||||
| use rayon::prelude::*; | ||||
|  | ||||
| use super::file::{ZipFile, ZipFileNoData}; | ||||
|  | ||||
| const END_OF_CENTRAL_DIR_SIGNATURE: u32 = 0x06054B50; | ||||
|  | ||||
| #[derive(Debug, Default)] | ||||
| pub struct ZipData { | ||||
|     pub files: Vec<ZipFile>, | ||||
| } | ||||
|  | ||||
| impl ZipData { | ||||
|     pub fn write<W: Write + Seek, I: IntoIterator<Item = ZipFile>>( | ||||
|         &mut self, | ||||
|         buf: &mut W, | ||||
|         zip_file_iter: I, | ||||
|     ) -> std::io::Result<()> { | ||||
|         let zip_files = self.write_files_contained_and_iter(buf, zip_file_iter)?; | ||||
|  | ||||
|         let files_amount = super::files_amount_u16(&zip_files); | ||||
|  | ||||
|         let central_dir_offset = super::stream_position_u32(buf)?; | ||||
|  | ||||
|         self.write_central_dir(zip_files, buf)?; | ||||
|  | ||||
|         let central_dir_start = super::stream_position_u32(buf)?; | ||||
|  | ||||
|         self.write_end_of_central_directory( | ||||
|             buf, | ||||
|             central_dir_offset, | ||||
|             central_dir_start, | ||||
|             files_amount, | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     pub fn write_rayon<W: Write + Seek + Send, I: ParallelIterator<Item = ZipFile>>( | ||||
|         &mut self, | ||||
|         buf: &mut W, | ||||
|         zip_file_iter: I, | ||||
|     ) -> std::io::Result<()> { | ||||
|         let zip_files = self.write_files_contained_and_par_iter(buf, zip_file_iter)?; | ||||
|  | ||||
|         let files_amount = super::files_amount_u16(&zip_files); | ||||
|  | ||||
|         let central_dir_offset = super::stream_position_u32(buf)?; | ||||
|  | ||||
|         self.write_central_dir(zip_files, buf)?; | ||||
|  | ||||
|         let central_dir_start = super::stream_position_u32(buf)?; | ||||
|  | ||||
|         self.write_end_of_central_directory( | ||||
|             buf, | ||||
|             central_dir_offset, | ||||
|             central_dir_start, | ||||
|             files_amount, | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     #[inline] | ||||
|     fn write_files_contained_and_iter<W: Write + Seek, I: IntoIterator<Item = ZipFile>>( | ||||
|         &mut self, | ||||
|         buf: &mut W, | ||||
|         zip_files_iter: I, | ||||
|     ) -> std::io::Result<Vec<ZipFileNoData>> { | ||||
|         let zip_files = std::mem::take(&mut self.files); | ||||
|         self.write_files_iter(buf, zip_files.into_iter().chain(zip_files_iter)) | ||||
|     } | ||||
|  | ||||
|     #[inline] | ||||
|     pub fn write_files_contained_and_par_iter< | ||||
|         W: Write + Seek + Send, | ||||
|         I: ParallelIterator<Item = ZipFile>, | ||||
|     >( | ||||
|         &mut self, | ||||
|         buf: &mut W, | ||||
|         zip_files_iter: I, | ||||
|     ) -> std::io::Result<Vec<ZipFileNoData>> { | ||||
|         let zip_files = std::mem::take(&mut self.files); | ||||
|         self.write_files_par_iter(buf, zip_files.into_par_iter().chain(zip_files_iter)) | ||||
|     } | ||||
|  | ||||
|     pub fn write_files_iter<W: Write + Seek, I: IntoIterator<Item = ZipFile>>( | ||||
|         &mut self, | ||||
|         buf: &mut W, | ||||
|         zip_files: I, | ||||
|     ) -> std::io::Result<Vec<ZipFileNoData>> { | ||||
|         zip_files | ||||
|             .into_iter() | ||||
|             .map(|zipfile| zipfile.write_local_file_header_with_data_consuming(buf)) | ||||
|             .collect::<std::io::Result<Vec<_>>>() | ||||
|     } | ||||
|  | ||||
|     pub fn write_files_par_iter<W: Write + Seek + Send, I: ParallelIterator<Item = ZipFile>>( | ||||
|         &mut self, | ||||
|         buf: &mut W, | ||||
|         zip_files: I, | ||||
|     ) -> std::io::Result<Vec<ZipFileNoData>> { | ||||
|         let buf = Mutex::new(buf); | ||||
|         zip_files | ||||
|             .map(|zipfile| { | ||||
|                 let mut buf_lock = buf.lock().unwrap(); | ||||
|                 zipfile.write_local_file_header_with_data_consuming(*buf_lock) | ||||
|             }) | ||||
|             .collect::<std::io::Result<Vec<_>>>() | ||||
|     } | ||||
|  | ||||
|     fn write_central_dir<W: Write, I: IntoIterator<Item = ZipFileNoData>>( | ||||
|         &self, | ||||
|         zip_files: I, | ||||
|         buf: &mut W, | ||||
|     ) -> std::io::Result<()> { | ||||
|         zip_files | ||||
|             .into_iter() | ||||
|             .try_for_each(|zip_file| zip_file.write_central_directory_entry(buf)) | ||||
|     } | ||||
|  | ||||
|     const FOOTER_LENGTH: usize = 22; | ||||
|  | ||||
|     fn write_end_of_central_directory<W: Write>( | ||||
|         &self, | ||||
|         buf: &mut W, | ||||
|         central_dir_offset: u32, | ||||
|         central_dir_start: u32, | ||||
|         files_amount: u16, | ||||
|     ) -> std::io::Result<()> { | ||||
|         // Temporary in-memory statically sized array | ||||
|         let mut central_dir = [0; Self::FOOTER_LENGTH]; | ||||
|         { | ||||
|             let mut central_dir_buf: &mut [u8] = &mut central_dir; | ||||
|  | ||||
|             // Signature | ||||
|             central_dir_buf.write_all(&END_OF_CENTRAL_DIR_SIGNATURE.to_le_bytes())?; | ||||
|             // number of this disk | ||||
|             central_dir_buf.write_all(&0_u16.to_le_bytes())?; | ||||
|             // number of the disk with start | ||||
|             central_dir_buf.write_all(&0_u16.to_le_bytes())?; | ||||
|             // Number of entries on this disk | ||||
|             central_dir_buf.write_all(&files_amount.to_le_bytes())?; | ||||
|             // Number of entries | ||||
|             central_dir_buf.write_all(&files_amount.to_le_bytes())?; | ||||
|             // Central dir size | ||||
|             central_dir_buf.write_all(&(central_dir_start - central_dir_offset).to_le_bytes())?; | ||||
|             // Central dir offset | ||||
|             central_dir_buf.write_all(¢ral_dir_offset.to_le_bytes())?; | ||||
|             // Comment length | ||||
|             central_dir_buf.write_all(&0_u16.to_le_bytes())?; | ||||
|         } | ||||
|  | ||||
|         buf.write_all(¢ral_dir)?; | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,273 @@ | ||||
| //! ZIP file extra field | ||||
|  | ||||
| use std::{fs::Metadata, io::Write}; | ||||
|  | ||||
| /// This is a structure containing [`ExtraField`]s associated with a file or directory in a zip | ||||
| /// file, mostly used for filesystem properties, and this is the only functionality implemented | ||||
| /// here. | ||||
| /// | ||||
| /// The [`new_from_fs`](Self::new_from_fs) method will use the metadata the filesystem provides to | ||||
| /// construct the collection. | ||||
| #[derive(Debug, Clone, Default, PartialEq, Eq)] | ||||
| pub struct ExtraFields { | ||||
|     pub(crate) values: Vec<ExtraField>, | ||||
| } | ||||
|  | ||||
| impl Extend<ExtraField> for ExtraFields { | ||||
|     fn extend<T: IntoIterator<Item = ExtraField>>(&mut self, iter: T) { | ||||
|         self.values.extend(iter) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl IntoIterator for ExtraFields { | ||||
|     type Item = <Vec<ExtraField> as IntoIterator>::Item; | ||||
|     type IntoIter = <Vec<ExtraField> as IntoIterator>::IntoIter; | ||||
|  | ||||
|     fn into_iter(self) -> Self::IntoIter { | ||||
|         self.values.into_iter() | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl ExtraFields { | ||||
|     /// Create a new set of [`ExtraField`]s. [`Self::new_from_fs`] should be preferred. | ||||
|     /// | ||||
|     /// # Safety | ||||
|     /// | ||||
|     /// All fields must have valid values depending on the field type. | ||||
|     pub unsafe fn new<I>(fields: I) -> Self | ||||
|     where | ||||
|         I: IntoIterator<Item = ExtraField>, | ||||
|     { | ||||
|         Self { values: fields.into_iter().collect() } | ||||
|     } | ||||
|  | ||||
|     /// This method will use the filesystem metadata to get the properties that can be stored in | ||||
|     /// ZIP [`ExtraFields`]. | ||||
|     /// | ||||
|     /// The behavior is dependent on the target platform. Will return an empty set if the target os | ||||
|     /// is not Windows or Linux and not of UNIX family. | ||||
|     pub fn new_from_fs(metadata: &Metadata) -> Self { | ||||
|         #[cfg(target_os = "windows")] | ||||
|         { | ||||
|             return Self::new_windows(metadata); | ||||
|         } | ||||
|  | ||||
|         #[cfg(target_os = "linux")] | ||||
|         { | ||||
|             return Self::new_linux(metadata); | ||||
|         } | ||||
|  | ||||
|         #[cfg(all(unix, not(target_os = "linux")))] | ||||
|         { | ||||
|             return Self::new_unix(metadata); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     #[cfg(target_os = "linux")] | ||||
|     fn new_linux(metadata: &Metadata) -> Self { | ||||
|         use std::os::linux::fs::MetadataExt; | ||||
|  | ||||
|         let mod_time = Some(metadata.st_mtime() as i32); | ||||
|         let ac_time = Some(metadata.st_atime() as i32); | ||||
|         let cr_time = Some(metadata.st_ctime() as i32); | ||||
|  | ||||
|         let uid = metadata.st_uid(); | ||||
|         let gid = metadata.st_gid(); | ||||
|  | ||||
|         Self { values: vec![ExtraField::UnixExtendedTimestamp { mod_time, ac_time, cr_time }, ExtraField::UnixAttrs { uid, gid }] } | ||||
|     } | ||||
|  | ||||
|     #[cfg(all(unix, not(target_os = "linux")))] | ||||
|     #[allow(dead_code)] | ||||
|     fn new_unix(metadata: &Metadata) -> Self { | ||||
|         use std::os::unix::fs::MetadataExt; | ||||
|  | ||||
|         let mod_time = Some(metadata.mtime() as i32); | ||||
|         let ac_time = Some(metadata.atime() as i32); | ||||
|         let cr_time = Some(metadata.ctime() as i32); | ||||
|  | ||||
|         let uid = metadata.uid(); | ||||
|         let gid = metadata.gid(); | ||||
|  | ||||
|         Self { values: vec![ExtraField::UnixExtendedTimestamp { mod_time, ac_time, cr_time }, ExtraField::UnixAttrs { uid, gid }] } | ||||
|     } | ||||
|  | ||||
|     #[cfg(target_os = "windows")] | ||||
|     fn new_windows(metadata: &Metadata) -> Self { | ||||
|         use std::os::windows::fs::MetadataExt; | ||||
|  | ||||
|         let mtime = metadata.last_write_time(); | ||||
|         let atime = metadata.last_access_time(); | ||||
|         let ctime = metadata.creation_time(); | ||||
|  | ||||
|         Self { values: vec![ExtraField::Ntfs { mtime, atime, ctime }] } | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn data_length<const CENTRAL_HEADER: bool>(&self) -> u16 { | ||||
|         self.values.iter().map(|f| 4 + f.field_size::<CENTRAL_HEADER>()).sum() | ||||
|     } | ||||
|  | ||||
|     pub(crate) fn write<W: Write, const CENTRAL_HEADER: bool>(&self, writer: &mut W) -> std::io::Result<()> { | ||||
|         for field in &self.values { | ||||
|             field.write::<_, CENTRAL_HEADER>(writer)?; | ||||
|         } | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Extra data that can be associated with a file or directory. | ||||
| /// | ||||
| /// This library only implements the filesystem properties in NTFS and UNIX format. | ||||
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||||
| pub enum ExtraField { | ||||
|     /// NTFS file properties. | ||||
|     Ntfs { | ||||
|         /// Last modification timestamp | ||||
|         mtime: u64, | ||||
|         /// Last access timestamp | ||||
|         atime: u64, | ||||
|         /// File/directory creation timestamp | ||||
|         ctime: u64, | ||||
|     }, | ||||
|     /// Info-Zip extended unix timestamp. Each part is optional by definition, but will be | ||||
|     /// populated by [`ExtraFields::new_from_fs`]. | ||||
|     UnixExtendedTimestamp { | ||||
|         /// Last modification timestamp | ||||
|         mod_time: Option<i32>, | ||||
|         /// Last access timestamp | ||||
|         ac_time: Option<i32>, | ||||
|         /// Creation timestamp | ||||
|         cr_time: Option<i32>, | ||||
|     }, | ||||
|     /// UNIX file/directory attributes defined by Info-Zip. | ||||
|     UnixAttrs { | ||||
|         /// UID of the owner | ||||
|         uid: u32, | ||||
|         /// GID of the group | ||||
|         gid: u32, | ||||
|     }, | ||||
| } | ||||
|  | ||||
| const MOD_TIME_PRESENT: u8 = 1; | ||||
| const AC_TIME_PRESENT: u8 = 1 << 1; | ||||
| const CR_TIME_PRESENT: u8 = 1 << 2; | ||||
|  | ||||
| impl ExtraField { | ||||
|     #[inline] | ||||
|     fn header_id(&self) -> u16 { | ||||
|         match self { | ||||
|             Self::Ntfs { mtime: _, atime: _, ctime: _ } => 0x000a, | ||||
|             Self::UnixExtendedTimestamp { mod_time: _, ac_time: _, cr_time: _ } => 0x5455, | ||||
|             Self::UnixAttrs { uid: _, gid: _ } => 0x7875, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     #[inline] | ||||
|     const fn optional_field_size<T: Sized>(field: &Option<T>) -> u16 { | ||||
|         match field { | ||||
|             Some(_) => std::mem::size_of::<T>() as u16, | ||||
|             None => 0, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     #[inline] | ||||
|     const fn field_size<const CENTRAL_HEADER: bool>(&self) -> u16 { | ||||
|         match self { | ||||
|             Self::Ntfs { mtime: _, atime: _, ctime: _ } => 32, | ||||
|             Self::UnixExtendedTimestamp { mod_time, ac_time, cr_time } => { | ||||
|                 1 + Self::optional_field_size(mod_time) + { | ||||
|                     if !CENTRAL_HEADER { | ||||
|                         Self::optional_field_size(ac_time) + Self::optional_field_size(cr_time) | ||||
|                     } else { | ||||
|                         0 | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             Self::UnixAttrs { uid: _, gid: _ } => 11, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     #[inline] | ||||
|     const fn if_present(val: Option<i32>, if_present: u8) -> u8 { | ||||
|         match val { | ||||
|             Some(_) => if_present, | ||||
|             None => 0, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     const NTFS_FIELD_LEN: usize = 32; | ||||
|     const UNIX_ATTRS_LEN: usize = 11; | ||||
|  | ||||
|     pub(crate) fn write<W: Write, const CENTRAL_HEADER: bool>(self, writer: &mut W) -> std::io::Result<()> { | ||||
|         // Header ID | ||||
|         writer.write_all(&self.header_id().to_le_bytes())?; | ||||
|         // Field data size | ||||
|         writer.write_all(&self.field_size::<CENTRAL_HEADER>().to_le_bytes())?; | ||||
|  | ||||
|         match self { | ||||
|             Self::Ntfs { mtime, atime, ctime } => { | ||||
|                 // Writing to a temporary in-memory array | ||||
|                 let mut field = [0; Self::NTFS_FIELD_LEN]; | ||||
|                 { | ||||
|                     let mut field_buf: &mut [u8] = &mut field; | ||||
|  | ||||
|                     // Reserved field | ||||
|                     field_buf.write_all(&0_u32.to_le_bytes())?; | ||||
|  | ||||
|                     // Tag1 number | ||||
|                     field_buf.write_all(&1_u16.to_le_bytes())?; | ||||
|                     // Tag1 size | ||||
|                     field_buf.write_all(&24_u16.to_le_bytes())?; | ||||
|  | ||||
|                     // Mtime | ||||
|                     field_buf.write_all(&mtime.to_le_bytes())?; | ||||
|                     // Atime | ||||
|                     field_buf.write_all(&atime.to_le_bytes())?; | ||||
|                     // Ctime | ||||
|                     field_buf.write_all(&ctime.to_le_bytes())?; | ||||
|                 } | ||||
|  | ||||
|                 writer.write_all(&field)?; | ||||
|             } | ||||
|             Self::UnixExtendedTimestamp { mod_time, ac_time, cr_time } => { | ||||
|                 let flags = Self::if_present(mod_time, MOD_TIME_PRESENT) | ||||
|                     | Self::if_present(ac_time, AC_TIME_PRESENT) | ||||
|                     | Self::if_present(cr_time, CR_TIME_PRESENT); | ||||
|                 writer.write_all(&[flags])?; | ||||
|                 if let Some(mod_time) = mod_time { | ||||
|                     writer.write_all(&mod_time.to_le_bytes())?; | ||||
|                 } | ||||
|                 if !CENTRAL_HEADER { | ||||
|                     if let Some(ac_time) = ac_time { | ||||
|                         writer.write_all(&ac_time.to_le_bytes())?; | ||||
|                     } | ||||
|                     if let Some(cr_time) = cr_time { | ||||
|                         writer.write_all(&cr_time.to_le_bytes())?; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             Self::UnixAttrs { uid, gid } => { | ||||
|                 // Writing to a temporary in-memory array | ||||
|                 let mut field = [0; Self::UNIX_ATTRS_LEN]; | ||||
|                 { | ||||
|                     let mut field_buf: &mut [u8] = &mut field; | ||||
|  | ||||
|                     // Version of the field | ||||
|                     field_buf.write_all(&[1])?; | ||||
|                     // UID size | ||||
|                     field_buf.write_all(&[4])?; | ||||
|                     // UID | ||||
|                     field_buf.write_all(&uid.to_le_bytes())?; | ||||
|                     // GID size | ||||
|                     field_buf.write_all(&[4])?; | ||||
|                     // GID | ||||
|                     field_buf.write_all(&gid.to_le_bytes())?; | ||||
|                 } | ||||
|  | ||||
|                 writer.write_all(&field)?; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										201
									
								
								src/bins/src/shared/fastzip/mtzip/zip_archive_parts/file.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										201
									
								
								src/bins/src/shared/fastzip/mtzip/zip_archive_parts/file.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,201 @@ | ||||
| use std::io::{Seek, Write}; | ||||
|  | ||||
| use super::extra_field::ExtraFields; | ||||
| use super::super::{CompressionType, platform::VERSION_MADE_BY}; | ||||
|  | ||||
| const LOCAL_FILE_HEADER_SIGNATURE: u32 = 0x04034B50; | ||||
| const CENTRAL_FILE_HEADER_SIGNATURE: u32 = 0x02014B50; | ||||
|  | ||||
| const VERSION_NEEDED_TO_EXTRACT: u16 = 20; | ||||
|  | ||||
| /// Set bit 11 to indicate that the file names are in UTF-8, because all strings in rust are valid | ||||
| /// UTF-8 | ||||
| const GENERAL_PURPOSE_BIT_FLAG: u16 = 1 << 11; | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub struct ZipFile { | ||||
|     pub header: ZipFileHeader, | ||||
|     pub data: Vec<u8>, | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub struct ZipFileHeader { | ||||
|     pub compression_type: CompressionType, | ||||
|     pub crc: u32, | ||||
|     pub uncompressed_size: u32, | ||||
|     pub filename: String, | ||||
|     pub file_comment: Option<String>, | ||||
|     pub external_file_attributes: u32, | ||||
|     pub extra_fields: ExtraFields, | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub struct ZipFileNoData { | ||||
|     pub header: ZipFileHeader, | ||||
|     pub local_header_offset: u32, | ||||
|     pub compressed_size: u32, | ||||
| } | ||||
|  | ||||
| impl ZipFile { | ||||
|     pub fn write_local_file_header_with_data_consuming<W: Write + Seek>( | ||||
|         self, | ||||
|         buf: &mut W, | ||||
|     ) -> std::io::Result<ZipFileNoData> { | ||||
|         let local_header_offset = super::stream_position_u32(buf)?; | ||||
|         self.write_local_file_header_and_data(buf)?; | ||||
|         let Self { header, data } = self; | ||||
|         Ok(ZipFileNoData { | ||||
|             header, | ||||
|             local_header_offset, | ||||
|             compressed_size: data.len() as u32, | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     const LOCAL_FILE_HEADER_LEN: usize = 30; | ||||
|  | ||||
|     pub fn write_local_file_header_and_data<W: Write>(&self, buf: &mut W) -> std::io::Result<()> { | ||||
|         // Writing to a temporary in-memory statically sized array first | ||||
|         let mut header = [0; Self::LOCAL_FILE_HEADER_LEN]; | ||||
|         { | ||||
|             let mut header_buf: &mut [u8] = &mut header; | ||||
|  | ||||
|             // signature | ||||
|             header_buf.write_all(&LOCAL_FILE_HEADER_SIGNATURE.to_le_bytes())?; | ||||
|             // version needed to extract | ||||
|             header_buf.write_all(&VERSION_NEEDED_TO_EXTRACT.to_le_bytes())?; | ||||
|             // general purpose bit flag | ||||
|             header_buf.write_all(&GENERAL_PURPOSE_BIT_FLAG.to_le_bytes())?; | ||||
|             // compression type | ||||
|             header_buf.write_all(&(self.header.compression_type as u16).to_le_bytes())?; | ||||
|             // Last modification time // moved to extra fields | ||||
|             header_buf.write_all(&0_u16.to_le_bytes())?; | ||||
|             // Last modification date // moved to extra fields | ||||
|             header_buf.write_all(&0_u16.to_le_bytes())?; | ||||
|             // crc | ||||
|             header_buf.write_all(&self.header.crc.to_le_bytes())?; | ||||
|             // Compressed size | ||||
|             debug_assert!(self.data.len() <= u32::MAX as usize); | ||||
|             header_buf.write_all(&(self.data.len() as u32).to_le_bytes())?; | ||||
|             // Uncompressed size | ||||
|             header_buf.write_all(&self.header.uncompressed_size.to_le_bytes())?; | ||||
|             // Filename size | ||||
|             debug_assert!(self.header.filename.len() <= u16::MAX as usize); | ||||
|             header_buf.write_all(&(self.header.filename.len() as u16).to_le_bytes())?; | ||||
|             // extra field size | ||||
|             header_buf.write_all( | ||||
|                 &self | ||||
|                     .header | ||||
|                     .extra_fields | ||||
|                     .data_length::<false>() | ||||
|                     .to_le_bytes(), | ||||
|             )?; | ||||
|         } | ||||
|  | ||||
|         buf.write_all(&header)?; | ||||
|  | ||||
|         // Filename | ||||
|         buf.write_all(self.header.filename.as_bytes())?; | ||||
|         // Extra field | ||||
|         self.header.extra_fields.write::<_, false>(buf)?; | ||||
|  | ||||
|         // Data | ||||
|         buf.write_all(&self.data)?; | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     #[inline] | ||||
|     pub fn directory( | ||||
|         mut name: String, | ||||
|         extra_fields: ExtraFields, | ||||
|         external_attributes: u16, | ||||
|         file_comment: Option<String>, | ||||
|     ) -> Self { | ||||
|         if !(name.ends_with('/') || name.ends_with('\\')) { | ||||
|             name += "/" | ||||
|         }; | ||||
|         Self { | ||||
|             header: ZipFileHeader { | ||||
|                 compression_type: CompressionType::Stored, | ||||
|                 crc: 0, | ||||
|                 uncompressed_size: 0, | ||||
|                 filename: name, | ||||
|                 external_file_attributes: (external_attributes as u32) << 16, | ||||
|                 extra_fields, | ||||
|                 file_comment, | ||||
|             }, | ||||
|             data: vec![], | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl ZipFileNoData { | ||||
|     const CENTRAL_DIR_ENTRY_LEN: usize = 46; | ||||
|  | ||||
|     pub fn write_central_directory_entry<W: Write>(&self, buf: &mut W) -> std::io::Result<()> { | ||||
|         // Writing to a temporary in-memory statically sized array first | ||||
|         let mut central_dir_entry_header = [0; Self::CENTRAL_DIR_ENTRY_LEN]; | ||||
|         { | ||||
|             let mut central_dir_entry_buf: &mut [u8] = &mut central_dir_entry_header; | ||||
|  | ||||
|             // signature | ||||
|             central_dir_entry_buf.write_all(&CENTRAL_FILE_HEADER_SIGNATURE.to_le_bytes())?; | ||||
|             // version made by | ||||
|             central_dir_entry_buf.write_all(&VERSION_MADE_BY.to_le_bytes())?; | ||||
|             // version needed to extract | ||||
|             central_dir_entry_buf.write_all(&VERSION_NEEDED_TO_EXTRACT.to_le_bytes())?; | ||||
|             // general purpose bit flag | ||||
|             central_dir_entry_buf.write_all(&GENERAL_PURPOSE_BIT_FLAG.to_le_bytes())?; | ||||
|             // compression type | ||||
|             central_dir_entry_buf | ||||
|                 .write_all(&(self.header.compression_type as u16).to_le_bytes())?; | ||||
|             // Last modification time // moved to extra fields | ||||
|             central_dir_entry_buf.write_all(&0_u16.to_le_bytes())?; | ||||
|             // Last modification date // moved to extra fields | ||||
|             central_dir_entry_buf.write_all(&0_u16.to_le_bytes())?; | ||||
|             // crc | ||||
|             central_dir_entry_buf.write_all(&self.header.crc.to_le_bytes())?; | ||||
|             // Compressed size | ||||
|             central_dir_entry_buf.write_all(&self.compressed_size.to_le_bytes())?; | ||||
|             // Uncompressed size | ||||
|             central_dir_entry_buf.write_all(&self.header.uncompressed_size.to_le_bytes())?; | ||||
|             // Filename size | ||||
|             debug_assert!(self.header.filename.len() <= u16::MAX as usize); | ||||
|             central_dir_entry_buf.write_all(&(self.header.filename.len() as u16).to_le_bytes())?; | ||||
|             // extra field size | ||||
|             central_dir_entry_buf | ||||
|                 .write_all(&self.header.extra_fields.data_length::<true>().to_le_bytes())?; | ||||
|             // comment size | ||||
|             central_dir_entry_buf.write_all( | ||||
|                 &(self | ||||
|                     .header | ||||
|                     .file_comment | ||||
|                     .as_ref() | ||||
|                     .map(|fc| fc.len()) | ||||
|                     .unwrap_or(0) as u16) | ||||
|                     .to_le_bytes(), | ||||
|             )?; | ||||
|             // disk number start | ||||
|             central_dir_entry_buf.write_all(&0_u16.to_le_bytes())?; | ||||
|             // internal file attributes | ||||
|             central_dir_entry_buf.write_all(&0_u16.to_le_bytes())?; | ||||
|             // external file attributes | ||||
|             central_dir_entry_buf.write_all(&self.header.external_file_attributes.to_le_bytes())?; | ||||
|             // relative offset of local header | ||||
|             central_dir_entry_buf.write_all(&self.local_header_offset.to_le_bytes())?; | ||||
|         } | ||||
|  | ||||
|         buf.write_all(¢ral_dir_entry_header)?; | ||||
|  | ||||
|         // Filename | ||||
|         buf.write_all(self.header.filename.as_bytes())?; | ||||
|         // Extra field | ||||
|         self.header.extra_fields.write::<_, true>(buf)?; | ||||
|         // File comment | ||||
|         if let Some(file_comment) = &self.header.file_comment { | ||||
|             buf.write_all(file_comment.as_bytes())?; | ||||
|         } | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
							
								
								
									
										179
									
								
								src/bins/src/shared/fastzip/mtzip/zip_archive_parts/job.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										179
									
								
								src/bins/src/shared/fastzip/mtzip/zip_archive_parts/job.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,179 @@ | ||||
| use std::{ | ||||
|     borrow::Cow, | ||||
|     fs::File, | ||||
|     io::Read, | ||||
|     panic::{RefUnwindSafe, UnwindSafe}, | ||||
|     path::Path, | ||||
| }; | ||||
|  | ||||
| use flate2::{CrcReader, read::DeflateEncoder}; | ||||
|  | ||||
| use super::{extra_field::ExtraFields, file::ZipFile}; | ||||
| use super::super::{ | ||||
|     CompressionType, level::CompressionLevel, platform::attributes_from_fs, | ||||
|     zip_archive_parts::file::ZipFileHeader, | ||||
| }; | ||||
|  | ||||
| pub enum ZipJobOrigin<'a> { | ||||
|     Directory, | ||||
|     Filesystem { path: Cow<'a, Path> }, | ||||
|     RawData(Cow<'a, [u8]>), | ||||
|     Reader(Box<dyn Read + Send + Sync + UnwindSafe + RefUnwindSafe + 'a>), | ||||
| } | ||||
|  | ||||
| impl core::fmt::Debug for ZipJobOrigin<'_> { | ||||
|     #[inline] | ||||
|     fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { | ||||
|         match self { | ||||
|             Self::Directory => f.write_str("Directory"), | ||||
|             Self::Filesystem { path } => f.debug_struct("Filesystem").field("path", &path).finish(), | ||||
|             Self::RawData(raw_data) => f.debug_tuple("RawData").field(&raw_data).finish(), | ||||
|             Self::Reader(_reader) => f.debug_tuple("Reader").finish_non_exhaustive(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| struct FileDigest { | ||||
|     data: Vec<u8>, | ||||
|     uncompressed_size: u32, | ||||
|     crc: u32, | ||||
| } | ||||
|  | ||||
| #[derive(Debug)] | ||||
| pub struct ZipJob<'a> { | ||||
|     pub data_origin: ZipJobOrigin<'a>, | ||||
|     pub extra_fields: ExtraFields, | ||||
|     pub archive_path: String, | ||||
|     pub file_comment: Option<String>, | ||||
|     pub external_attributes: u16, | ||||
|     /// Ignored when [`data_origin`](Self::data_origin) is a [`ZipJobOrigin::Directory`] | ||||
|     pub compression_level: CompressionLevel, | ||||
|     /// Ignored when [`data_origin`](Self::data_origin) is a [`ZipJobOrigin::Directory`] | ||||
|     pub compression_type: CompressionType, | ||||
| } | ||||
|  | ||||
| impl ZipJob<'_> { | ||||
|     fn compress_file<R: Read>( | ||||
|         source: R, | ||||
|         uncompressed_size_approx: Option<u32>, | ||||
|         compression_type: CompressionType, | ||||
|         compression_level: CompressionLevel, | ||||
|     ) -> std::io::Result<FileDigest> { | ||||
|         let mut crc_reader = CrcReader::new(source); | ||||
|         let mut data = Vec::with_capacity(uncompressed_size_approx.unwrap_or(0) as usize); | ||||
|         let uncompressed_size = match compression_type { | ||||
|             CompressionType::Deflate => { | ||||
|                 let mut encoder = DeflateEncoder::new(&mut crc_reader, compression_level.into()); | ||||
|                 encoder.read_to_end(&mut data)?; | ||||
|                 encoder.total_in() as usize | ||||
|             } | ||||
|             CompressionType::Stored => crc_reader.read_to_end(&mut data)?, | ||||
|         }; | ||||
|         debug_assert!(uncompressed_size <= u32::MAX as usize); | ||||
|         let uncompressed_size = uncompressed_size as u32; | ||||
|         data.shrink_to_fit(); | ||||
|         let crc = crc_reader.crc().sum(); | ||||
|         Ok(FileDigest { | ||||
|             data, | ||||
|             uncompressed_size, | ||||
|             crc, | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     pub fn into_file(self) -> std::io::Result<ZipFile> { | ||||
|         match self.data_origin { | ||||
|             ZipJobOrigin::Directory => Ok(ZipFile::directory( | ||||
|                 self.archive_path, | ||||
|                 self.extra_fields, | ||||
|                 self.external_attributes, | ||||
|                 self.file_comment, | ||||
|             )), | ||||
|             ZipJobOrigin::Filesystem { path } => { | ||||
|                 let file = File::open(path).unwrap(); | ||||
|                 let file_metadata = file.metadata().unwrap(); | ||||
|                 let uncompressed_size_approx = file_metadata.len(); | ||||
|                 debug_assert!(uncompressed_size_approx <= u32::MAX.into()); | ||||
|                 let uncompressed_size_approx = uncompressed_size_approx as u32; | ||||
|                 let external_file_attributes = attributes_from_fs(&file_metadata); | ||||
|                 let mut extra_fields = ExtraFields::new_from_fs(&file_metadata); | ||||
|                 extra_fields.extend(self.extra_fields); | ||||
|  | ||||
|                 let FileDigest { | ||||
|                     data, | ||||
|                     uncompressed_size, | ||||
|                     crc, | ||||
|                 } = Self::compress_file( | ||||
|                     file, | ||||
|                     Some(uncompressed_size_approx), | ||||
|                     self.compression_type, | ||||
|                     self.compression_level, | ||||
|                 )?; | ||||
|                 Ok(ZipFile { | ||||
|                     header: ZipFileHeader { | ||||
|                         compression_type: CompressionType::Deflate, | ||||
|                         crc, | ||||
|                         uncompressed_size, | ||||
|                         filename: self.archive_path, | ||||
|                         external_file_attributes: (external_file_attributes as u32) << 16, | ||||
|                         extra_fields, | ||||
|                         file_comment: self.file_comment, | ||||
|                     }, | ||||
|                     data, | ||||
|                 }) | ||||
|             } | ||||
|             ZipJobOrigin::RawData(data) => { | ||||
|                 let uncompressed_size_approx = data.len(); | ||||
|                 debug_assert!(uncompressed_size_approx <= u32::MAX as usize); | ||||
|                 let uncompressed_size_approx = uncompressed_size_approx as u32; | ||||
|  | ||||
|                 let FileDigest { | ||||
|                     data, | ||||
|                     uncompressed_size, | ||||
|                     crc, | ||||
|                 } = Self::compress_file( | ||||
|                     data.as_ref(), | ||||
|                     Some(uncompressed_size_approx), | ||||
|                     self.compression_type, | ||||
|                     self.compression_level, | ||||
|                 )?; | ||||
|                 Ok(ZipFile { | ||||
|                     header: ZipFileHeader { | ||||
|                         compression_type: CompressionType::Deflate, | ||||
|                         crc, | ||||
|                         uncompressed_size, | ||||
|                         filename: self.archive_path, | ||||
|                         external_file_attributes: (self.external_attributes as u32) << 16, | ||||
|                         extra_fields: self.extra_fields, | ||||
|                         file_comment: self.file_comment, | ||||
|                     }, | ||||
|                     data, | ||||
|                 }) | ||||
|             } | ||||
|             ZipJobOrigin::Reader(reader) => { | ||||
|                 let FileDigest { | ||||
|                     data, | ||||
|                     uncompressed_size, | ||||
|                     crc, | ||||
|                 } = Self::compress_file( | ||||
|                     reader, | ||||
|                     None, | ||||
|                     self.compression_type, | ||||
|                     self.compression_level, | ||||
|                 )?; | ||||
|                 Ok(ZipFile { | ||||
|                     header: ZipFileHeader { | ||||
|                         compression_type: CompressionType::Deflate, | ||||
|                         crc, | ||||
|                         uncompressed_size, | ||||
|                         filename: self.archive_path, | ||||
|                         external_file_attributes: (self.external_attributes as u32) << 16, | ||||
|                         extra_fields: self.extra_fields, | ||||
|                         file_comment: self.file_comment, | ||||
|                     }, | ||||
|                     data, | ||||
|                 }) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										17
									
								
								src/bins/src/shared/fastzip/mtzip/zip_archive_parts/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/bins/src/shared/fastzip/mtzip/zip_archive_parts/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| pub mod data; | ||||
| pub mod extra_field; | ||||
| pub mod file; | ||||
| pub mod job; | ||||
| use std::io::Seek; | ||||
| #[inline] | ||||
| pub fn stream_position_u32<W: Seek>(buf: &mut W) -> std::io::Result<u32> { | ||||
|     let offset = buf.stream_position()?; | ||||
|     debug_assert!(offset <= u32::MAX.into()); | ||||
|     Ok(offset as u32) | ||||
| } | ||||
| #[inline] | ||||
| pub fn files_amount_u16<T>(files: &[T]) -> u16 { | ||||
|     let amount = files.len(); | ||||
|     debug_assert!(amount <= u16::MAX as usize); | ||||
|     amount as u16 | ||||
| } | ||||
							
								
								
									
										142
									
								
								src/bins/src/shared/fastzip/progress_updater.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								src/bins/src/shared/fastzip/progress_updater.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,142 @@ | ||||
| // Copyright 2023 Google LLC | ||||
|  | ||||
| // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or | ||||
| // https://www.apache.org/licenses/LICENSE-2.0> or the MIT license | ||||
| // <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your | ||||
| // option. This file may not be copied, modified, or distributed | ||||
| // except according to those terms. | ||||
|  | ||||
| use std::cmp::min; | ||||
|  | ||||
| /// A struct which can issue periodic updates indicating progress towards | ||||
| /// an external total, based on updates towards an internal goal. | ||||
| pub struct ProgressUpdater<F: Fn(u64)> { | ||||
|     callback: F, | ||||
|     internal_progress: u64, | ||||
|     per_update_internal: u64, | ||||
|     update_external_amount: u64, | ||||
|     external_updates_sent: u64, | ||||
|     remainder_external: u64, | ||||
|     internal_total: u64, | ||||
| } | ||||
|  | ||||
| impl<F: Fn(u64)> ProgressUpdater<F> { | ||||
|     /// Create a new progress updater, with a callback to be called periodically. | ||||
|     pub fn new(callback: F, external_total: u64, internal_total: u64, per_update_internal: u64) -> Self { | ||||
|         let per_update_internal = min(internal_total, per_update_internal); | ||||
|         let total_updates_expected = if per_update_internal == 0 { 0 } else { internal_total / per_update_internal }; | ||||
|         let (update_external_amount, remainder_external) = if total_updates_expected == 0 { | ||||
|             (0, external_total) | ||||
|         } else { | ||||
|             (external_total / total_updates_expected, external_total % total_updates_expected) | ||||
|         }; | ||||
|         Self { | ||||
|             callback, | ||||
|             internal_progress: 0u64, | ||||
|             per_update_internal, | ||||
|             update_external_amount, | ||||
|             external_updates_sent: 0u64, | ||||
|             remainder_external, | ||||
|             internal_total, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Indicate some progress towards the internal goal. May call back the | ||||
|     /// external callback function to show some progress towards the external | ||||
|     /// goal. | ||||
|     pub fn progress(&mut self, amount_internal: u64) { | ||||
|         self.internal_progress += amount_internal; | ||||
|         self.send_due_updates(); | ||||
|     } | ||||
|  | ||||
|     fn send_due_updates(&mut self) { | ||||
|         let updates_due = if self.per_update_internal == 0 { 0 } else { self.internal_progress / self.per_update_internal }; | ||||
|         while updates_due > self.external_updates_sent { | ||||
|             (self.callback)(self.update_external_amount); | ||||
|             self.external_updates_sent += 1; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Indicate completion of the task. Fully update the callback towards the | ||||
|     /// external state. | ||||
|     pub fn finish(&mut self) { | ||||
|         self.internal_progress = self.internal_total; | ||||
|         self.send_due_updates(); | ||||
|         if self.remainder_external > 0 { | ||||
|             (self.callback)(self.remainder_external); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_progress_updater() { | ||||
|     let amount_received = std::rc::Rc::new(std::cell::RefCell::new(0u64)); | ||||
|     let mut progresser = ProgressUpdater::new( | ||||
|         |progress| { | ||||
|             *(amount_received.borrow_mut()) += progress; | ||||
|         }, | ||||
|         100, | ||||
|         1000, | ||||
|         100, | ||||
|     ); | ||||
|     assert_eq!(*amount_received.borrow(), 0); | ||||
|     progresser.progress(1); | ||||
|     assert_eq!(*amount_received.borrow(), 0); | ||||
|     progresser.progress(100); | ||||
|     assert_eq!(*amount_received.borrow(), 10); | ||||
|     progresser.progress(800); | ||||
|     assert_eq!(*amount_received.borrow(), 90); | ||||
|     progresser.finish(); | ||||
|     assert_eq!(*amount_received.borrow(), 100); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_progress_updater_zero_external() { | ||||
|     let amount_received = std::rc::Rc::new(std::cell::RefCell::new(0u64)); | ||||
|     let mut progresser = ProgressUpdater::new( | ||||
|         |progress| { | ||||
|             *(amount_received.borrow_mut()) += progress; | ||||
|         }, | ||||
|         0, | ||||
|         1000, | ||||
|         100, | ||||
|     ); | ||||
|     assert_eq!(*amount_received.borrow(), 0); | ||||
|     progresser.progress(1); | ||||
|     progresser.progress(800); | ||||
|     progresser.finish(); | ||||
|     assert_eq!(*amount_received.borrow(), 0); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_progress_updater_small_internal() { | ||||
|     let amount_received = std::rc::Rc::new(std::cell::RefCell::new(0u64)); | ||||
|     let mut progresser = ProgressUpdater::new( | ||||
|         |progress| { | ||||
|             *(amount_received.borrow_mut()) += progress; | ||||
|         }, | ||||
|         100, | ||||
|         5, | ||||
|         100, | ||||
|     ); | ||||
|     assert_eq!(*amount_received.borrow(), 0); | ||||
|     progresser.progress(1); | ||||
|     progresser.finish(); | ||||
|     assert_eq!(*amount_received.borrow(), 100); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_progress_updater_zero_internal() { | ||||
|     let amount_received = std::rc::Rc::new(std::cell::RefCell::new(0u64)); | ||||
|     let mut progresser = ProgressUpdater::new( | ||||
|         |progress| { | ||||
|             *(amount_received.borrow_mut()) += progress; | ||||
|         }, | ||||
|         100, | ||||
|         0, | ||||
|         100, | ||||
|     ); | ||||
|     assert_eq!(*amount_received.borrow(), 0); | ||||
|     progresser.finish(); | ||||
|     assert_eq!(*amount_received.borrow(), 100); | ||||
| } | ||||
							
								
								
									
										327
									
								
								src/bins/src/shared/fastzip/ripunzip.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										327
									
								
								src/bins/src/shared/fastzip/ripunzip.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,327 @@ | ||||
| // Copyright 2022 Google LLC | ||||
|  | ||||
| // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or | ||||
| // https://www.apache.org/licenses/LICENSE-2.0> or the MIT license | ||||
| // <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your | ||||
| // option. This file may not be copied, modified, or distributed | ||||
| // except according to those terms. | ||||
|  | ||||
| use std::{ | ||||
|     borrow::Cow, | ||||
|     fs::File, | ||||
|     io::{ErrorKind, Read, Seek, SeekFrom}, | ||||
|     path::{Path, PathBuf}, | ||||
|     sync::Mutex, | ||||
| }; | ||||
|  | ||||
| use anyhow::{Context, Result}; | ||||
| use rayon::prelude::*; | ||||
| use zip::{read::ZipFile, ZipArchive}; | ||||
|  | ||||
| use super::{cloneable_seekable_reader::CloneableSeekableReader, progress_updater::ProgressUpdater, UnzipProgressReporter}; | ||||
|  | ||||
| pub(crate) fn determine_stream_len<R: Seek>(stream: &mut R) -> std::io::Result<u64> { | ||||
|     let old_pos = stream.stream_position()?; | ||||
|     let len = stream.seek(SeekFrom::End(0))?; | ||||
|     if old_pos != len { | ||||
|         stream.seek(SeekFrom::Start(old_pos))?; | ||||
|     } | ||||
|     Ok(len) | ||||
| } | ||||
|  | ||||
| /// Options for unzipping. | ||||
| pub struct UnzipOptions<'a, 'b> { | ||||
|     /// The destination directory. | ||||
|     pub output_directory: Option<PathBuf>, | ||||
|     /// Password if encrypted. | ||||
|     pub password: Option<String>, | ||||
|     /// Whether to run in single-threaded mode. | ||||
|     pub single_threaded: bool, | ||||
|     /// A filename filter, optionally | ||||
|     pub filename_filter: Option<Box<dyn FilenameFilter + Sync + 'a>>, | ||||
|     /// An object to receive notifications of unzip progress. | ||||
|     pub progress_reporter: Box<dyn UnzipProgressReporter + Sync + 'b>, | ||||
| } | ||||
|  | ||||
| /// An object which can unzip a zip file, in its entirety, from a local | ||||
| /// file or from a network stream. It tries to do this in parallel wherever | ||||
| /// possible. | ||||
| pub struct UnzipEngine { | ||||
|     zipfile: Box<dyn UnzipEngineImpl>, | ||||
|     compressed_length: u64, | ||||
|     directory_creator: DirectoryCreator, | ||||
| } | ||||
|  | ||||
| /// Code which can determine whether to unzip a given filename. | ||||
| pub trait FilenameFilter { | ||||
|     /// Returns true if the given filename should be unzipped. | ||||
|     fn should_unzip(&self, filename: &str) -> bool; | ||||
| } | ||||
|  | ||||
| /// The underlying engine used by the unzipper. This is different | ||||
| /// for files and URIs. | ||||
| trait UnzipEngineImpl { | ||||
|     fn unzip(&mut self, options: UnzipOptions, directory_creator: &DirectoryCreator) -> Vec<anyhow::Error>; | ||||
|  | ||||
|     // Due to lack of RPITIT we'll return a Vec<String> here | ||||
|     fn list(&self) -> Result<Vec<String>, anyhow::Error>; | ||||
| } | ||||
|  | ||||
| /// Engine which knows how to unzip a file. | ||||
| #[derive(Clone)] | ||||
| struct UnzipFileEngine(ZipArchive<CloneableSeekableReader<File>>); | ||||
|  | ||||
| impl UnzipEngineImpl for UnzipFileEngine { | ||||
|     fn unzip(&mut self, options: UnzipOptions, directory_creator: &DirectoryCreator) -> Vec<anyhow::Error> { | ||||
|         unzip_serial_or_parallel(self.0.len(), options, directory_creator, || self.0.clone(), || {}) | ||||
|     } | ||||
|  | ||||
|     fn list(&self) -> Result<Vec<String>, anyhow::Error> { | ||||
|         list(&self.0) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl UnzipEngine { | ||||
|     /// Create an unzip engine which knows how to unzip a file. | ||||
|     pub fn for_file(mut zipfile: File) -> Result<Self> { | ||||
|         // The following line doesn't actually seem to make any significant | ||||
|         // performance difference. | ||||
|         // let zipfile = BufReader::new(zipfile); | ||||
|         let compressed_length = determine_stream_len(&mut zipfile)?; | ||||
|         let zipfile = CloneableSeekableReader::new(zipfile); | ||||
|         Ok(Self { | ||||
|             zipfile: Box::new(UnzipFileEngine(ZipArchive::new(zipfile)?)), | ||||
|             compressed_length, | ||||
|             directory_creator: DirectoryCreator::default(), | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     /// The total compressed length that we expect to retrieve over | ||||
|     /// the network or from the compressed file. | ||||
|     pub fn zip_length(&self) -> u64 { | ||||
|         self.compressed_length | ||||
|     } | ||||
|  | ||||
|     // Perform the unzip. | ||||
|     pub fn unzip(mut self, options: UnzipOptions) -> Result<()> { | ||||
|         log::debug!("Starting extract"); | ||||
|         options.progress_reporter.total_bytes_expected(self.compressed_length); | ||||
|         let errors = self.zipfile.unzip(options, &self.directory_creator); | ||||
|         // Return the first error code, if any. | ||||
|         errors.into_iter().next().map(Result::Err).unwrap_or(Ok(())) | ||||
|     } | ||||
|  | ||||
|     /// List the filenames in the archive | ||||
|     pub fn list(self) -> Result<impl Iterator<Item = String>> { | ||||
|         // In future this might be a more dynamic iterator type. | ||||
|         self.zipfile.list().map(|mut v| { | ||||
|             // Names are returned in a HashMap iteration order so let's | ||||
|             // sort thme to be more reasonable | ||||
|             v.sort(); | ||||
|             v.into_iter() | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Return a list of filenames from the zip. For now this is infallible | ||||
| /// but provide the option of an error code in case we do something | ||||
| /// smarter in future. | ||||
| fn list<'a, T: Read + Seek + 'a>(zip_archive: &ZipArchive<T>) -> Result<Vec<String>> { | ||||
|     Ok(zip_archive.file_names().map(|s| s.to_string()).collect()) | ||||
| } | ||||
|  | ||||
| fn unzip_serial_or_parallel<'a, T: Read + Seek + 'a>( | ||||
|     len: usize, | ||||
|     options: UnzipOptions, | ||||
|     directory_creator: &DirectoryCreator, | ||||
|     get_ziparchive_clone: impl Fn() -> ZipArchive<T> + Sync, | ||||
|     // Call when a file is going to be skipped | ||||
|     file_skip_callback: impl Fn() + Sync + Send + Clone, | ||||
| ) -> Vec<anyhow::Error> { | ||||
|     let progress_reporter: &dyn UnzipProgressReporter = options.progress_reporter.as_ref(); | ||||
|     match (options.filename_filter, options.single_threaded) { | ||||
|         (None, true) => (0..len) | ||||
|             .map(|i| { | ||||
|                 extract_file_by_index( | ||||
|                     &get_ziparchive_clone, | ||||
|                     i, | ||||
|                     &options.output_directory, | ||||
|                     &options.password, | ||||
|                     progress_reporter, | ||||
|                     directory_creator, | ||||
|                 ) | ||||
|             }) | ||||
|             .filter_map(Result::err) | ||||
|             .collect(), | ||||
|         (None, false) => { | ||||
|             // We use par_bridge here rather than into_par_iter because it turns | ||||
|             // out to better preserve ordering of the IDs in the input range, | ||||
|             // i.e. we're more likely to ask our initial threads to act upon | ||||
|             // file IDs 0, 1, 2, 3, 4, 5 rather than 0, 1000, 2000, 3000 etc. | ||||
|             // On a device which is CPU-bound or IO-bound (rather than network | ||||
|             // bound) that's beneficial because we can start to decompress | ||||
|             // and write data to disk as soon as it arrives from the network. | ||||
|             (0..len) | ||||
|                 .par_bridge() | ||||
|                 .map(|i| { | ||||
|                     extract_file_by_index( | ||||
|                         &get_ziparchive_clone, | ||||
|                         i, | ||||
|                         &options.output_directory, | ||||
|                         &options.password, | ||||
|                         progress_reporter, | ||||
|                         directory_creator, | ||||
|                     ) | ||||
|                 }) | ||||
|                 .filter_map(Result::err) | ||||
|                 .collect() | ||||
|         } | ||||
|         (Some(filename_filter), single_threaded) => { | ||||
|             // If we have a filename filter, an easy thing would be to | ||||
|             // iterate through each file index as above, and check to see if its | ||||
|             // name matches. Unfortunately, that seeks all over the place | ||||
|             // to get the filename from the local header. | ||||
|             // Instead, let's get a list of the filenames we need | ||||
|             // and request them from the zip library directly. | ||||
|             // As we can't predict their order in the file, this may involve | ||||
|             // arbitrary rewinds, so let's do it single-threaded. | ||||
|             if !single_threaded { | ||||
|                 log::warn!("Unzipping specific files - assuming --single-threaded since we currently cannot unzip specific files in a multi-threaded mode. If you need that, consider launching multiple copies of ripunzip in parallel."); | ||||
|             } | ||||
|             let mut filenames: Vec<_> = get_ziparchive_clone() | ||||
|                 .file_names() | ||||
|                 .filter(|name| filename_filter.as_ref().should_unzip(name)) | ||||
|                 .map(|s| s.to_string()) | ||||
|                 .collect(); | ||||
|             // The filenames returned by the file_names() method above are in | ||||
|             // HashMap iteration order (i.e. random). To avoid creating lots | ||||
|             // of HTTPS streams for files which are nearby each other in the | ||||
|             // zip, we'd ideally extract them in order of file position. | ||||
|             // We have no way of knowing file position (without iterating the | ||||
|             // whole file) so instead let's sort them and hope that files were | ||||
|             // zipped in alphabetical order, or close to it. If we're wrong, | ||||
|             // we'll just end up rewinding, that is, creating extra redundant | ||||
|             // HTTP(S) streams. | ||||
|             filenames.sort(); | ||||
|             log::info!("Will unzip {} matching filenames", filenames.len()); | ||||
|             file_skip_callback(); | ||||
|  | ||||
|             // let progress_reporter: &dyn UnzipProgressReporter = options.progress_reporter.as_ref(); | ||||
|             filenames | ||||
|                 .into_iter() | ||||
|                 .map(|name| { | ||||
|                     let myzip: &mut zip::ZipArchive<T> = &mut get_ziparchive_clone(); | ||||
|                     let file: ZipFile<T> = 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, progress_reporter, directory_creator); | ||||
|                     file_skip_callback(); | ||||
|                     r | ||||
|                 }) | ||||
|                 .filter_map(Result::err) | ||||
|                 .collect() | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn extract_file_by_index<'a, T: Read + Seek + 'a>( | ||||
|     get_ziparchive_clone: impl Fn() -> ZipArchive<T> + Sync, | ||||
|     i: usize, | ||||
|     output_directory: &Option<PathBuf>, | ||||
|     password: &Option<String>, | ||||
|     progress_reporter: &dyn UnzipProgressReporter, | ||||
|     directory_creator: &DirectoryCreator, | ||||
| ) -> Result<(), anyhow::Error> { | ||||
|     let myzip: &mut zip::ZipArchive<T> = &mut get_ziparchive_clone(); | ||||
|     let file: ZipFile<T> = 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) | ||||
| } | ||||
|  | ||||
| fn extract_file<R: Read>( | ||||
|     file: ZipFile<R>, | ||||
|     output_directory: &Option<PathBuf>, | ||||
|     progress_reporter: &dyn UnzipProgressReporter, | ||||
|     directory_creator: &DirectoryCreator, | ||||
| ) -> Result<(), anyhow::Error> { | ||||
|     let name = file.enclosed_name().as_deref().map(Path::to_string_lossy).unwrap_or_else(|| Cow::Borrowed("<unprintable>")).to_string(); | ||||
|     extract_file_inner(file, output_directory, progress_reporter, directory_creator).with_context(|| format!("Failed to extract {name}")) | ||||
| } | ||||
|  | ||||
| /// Extracts a file from a zip file. | ||||
| fn extract_file_inner<R: Read>( | ||||
|     mut file: ZipFile<R>, | ||||
|     output_directory: &Option<PathBuf>, | ||||
|     progress_reporter: &dyn UnzipProgressReporter, | ||||
|     directory_creator: &DirectoryCreator, | ||||
| ) -> Result<()> { | ||||
|     let name = file.enclosed_name().ok_or_else(|| std::io::Error::new(ErrorKind::Unsupported, "path not safe to extract"))?; | ||||
|     let display_name = name.display().to_string(); | ||||
|     let out_path = match output_directory { | ||||
|         Some(output_directory) => output_directory.join(name), | ||||
|         None => name, | ||||
|     }; | ||||
|     progress_reporter.extraction_starting(&display_name); | ||||
|     log::debug!("Start extract of file at {:x}, length {:x}, name {}", file.data_start(), file.compressed_size(), display_name); | ||||
|     if file.name().ends_with('/') { | ||||
|         directory_creator.create_dir_all(&out_path)?; | ||||
|     } else { | ||||
|         if let Some(parent) = out_path.parent() { | ||||
|             directory_creator.create_dir_all(parent)?; | ||||
|         } | ||||
|         let out_file = File::create(&out_path).with_context(|| "Failed to create file")?; | ||||
|         // Progress bar strategy. The overall progress across the entire zip file must be | ||||
|         // denoted in terms of *compressed* bytes, since at the outset we don't know the uncompressed | ||||
|         // size of each file. Yet, within a given file, we update progress based on the bytes | ||||
|         // of uncompressed data written, once per 1MB, because that's the information that we happen | ||||
|         // to have available. So, calculate how many compressed bytes relate to 1MB of uncompressed | ||||
|         // data, and the remainder. | ||||
|         let uncompressed_size = file.size(); | ||||
|         let compressed_size = file.compressed_size(); | ||||
|         let mut progress_updater = ProgressUpdater::new( | ||||
|             |external_progress| { | ||||
|                 progress_reporter.bytes_extracted(external_progress); | ||||
|             }, | ||||
|             compressed_size, | ||||
|             uncompressed_size, | ||||
|             1024 * 1024, | ||||
|         ); | ||||
|         let mut out_file = progress_streams::ProgressWriter::new(out_file, |bytes_written| progress_updater.progress(bytes_written as u64)); | ||||
|         // Using a BufWriter here doesn't improve performance even on a VM with | ||||
|         // spinny disks. | ||||
|         std::io::copy(&mut file, &mut out_file).with_context(|| "Failed to write directory")?; | ||||
|         progress_updater.finish(); | ||||
|     } | ||||
|     #[cfg(unix)] | ||||
|     { | ||||
|         use std::os::unix::fs::PermissionsExt; | ||||
|         if let Some(mode) = file.unix_mode() { | ||||
|             std::fs::set_permissions(&out_path, std::fs::Permissions::from_mode(mode)).with_context(|| "Failed to set permissions")?; | ||||
|         } | ||||
|     } | ||||
|     log::debug!("Finished extract of file at {:x}, length {:x}, name {}", file.data_start(), file.compressed_size(), display_name); | ||||
|     progress_reporter.extraction_finished(&display_name); | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| /// An engine used to ensure we don't conflict in creating directories | ||||
| /// between threads | ||||
| #[derive(Default)] | ||||
| struct DirectoryCreator(Mutex<()>); | ||||
|  | ||||
| impl DirectoryCreator { | ||||
|     fn create_dir_all(&self, path: &Path) -> Result<()> { | ||||
|         // Fast path - avoid locking if the directory exists | ||||
|         if path.exists() { | ||||
|             return Ok(()); | ||||
|         } | ||||
|         let _exclusivity = self.0.lock().unwrap(); | ||||
|         if path.exists() { | ||||
|             return Ok(()); | ||||
|         } | ||||
|         std::fs::create_dir_all(path).with_context(|| "Failed to create directory") | ||||
|     } | ||||
| } | ||||
| @@ -1,5 +1,6 @@ | ||||
| pub mod runtime_arch; | ||||
| pub mod cli_host; | ||||
| pub mod fastzip; | ||||
|  | ||||
| mod dialogs_const; | ||||
| mod dialogs_common; | ||||
|   | ||||
| @@ -78,7 +78,7 @@ fn check_arch_windows() -> Option<RuntimeArch> { | ||||
|  | ||||
| #[cfg(target_os = "windows")] | ||||
| type IsWow64Process2Fn = unsafe extern "system" fn( | ||||
|     hProcess: windows::Win32::Foundation::HANDLE, | ||||
|     hprocess: windows::Win32::Foundation::HANDLE, | ||||
|     pprocessmachine: *mut windows::Win32::System::SystemInformation::IMAGE_FILE_MACHINE, | ||||
|     pnativemachine: *mut windows::Win32::System::SystemInformation::IMAGE_FILE_MACHINE, | ||||
| ) -> windows::core::BOOL; | ||||
|   | ||||
| @@ -3,7 +3,7 @@ use windows::Win32::System::LibraryLoader::LOAD_LIBRARY_SEARCH_SYSTEM32; | ||||
| use windows::Win32::System::LibraryLoader::LOAD_LIBRARY_FLAGS; | ||||
|  | ||||
| #[cfg(target_os = "windows")] | ||||
| type SetDefaultDllDirectoriesFn = unsafe extern "system" fn(DirectoryFlags: u32) -> BOOL; | ||||
| type SetDefaultDllDirectoriesFn = unsafe extern "system" fn(directory_flags: u32) -> BOOL; | ||||
|  | ||||
| #[cfg(target_os = "windows")] | ||||
| unsafe fn set_default_dll_directories(flags: LOAD_LIBRARY_FLAGS) { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user