1 //! Contains Zip-specific building and unpacking functions
4 use std::os::unix::fs::PermissionsExt;
7 io::{self, prelude::*},
13 use filetime::{set_file_mtime, FileTime};
15 use humansize::{format_size, DECIMAL};
16 use same_file::Handle;
17 use zip::{self, read::ZipFile, DateTime, ZipArchive};
24 self, cd_into_same_dir_as, get_invalid_utf8_paths, pretty_format_list_of_paths, strip_cur_dir, to_utf,
30 /// Unpacks the archive given by `archive` into the folder given by `output_folder`.
31 /// Assumes that output_folder is empty
32 pub fn unpack_archive<R>(mut archive: ZipArchive<R>, output_folder: &Path, quiet: bool) -> crate::Result<Vec<PathBuf>>
36 assert!(output_folder.read_dir().expect("dir exists").count() == 0);
38 let mut unpacked_files = Vec::with_capacity(archive.len());
40 for idx in 0..archive.len() {
41 let mut file = archive.by_index(idx)?;
42 let file_path = match file.enclosed_name() {
43 Some(path) => path.to_owned(),
47 let file_path = output_folder.join(file_path);
49 display_zip_comment_if_exists(&file);
51 match file.name().ends_with('/') {
53 // This is printed for every file in the archive and has little
54 // importance for most users, but would generate lots of
55 // spoken text for users using screen readers, braille displays
58 info!(inaccessible, "File {} extracted to \"{}\"", idx, file_path.display());
60 fs::create_dir_all(&file_path)?;
63 if let Some(path) = file_path.parent() {
65 fs::create_dir_all(path)?;
68 let file_path = strip_cur_dir(file_path.as_path());
70 // same reason is in _is_dir: long, often not needed text
74 "{:?} extracted. ({})",
76 format_size(file.size(), DECIMAL),
80 let mut output_file = fs::File::create(file_path)?;
81 io::copy(&mut file, &mut output_file)?;
83 set_last_modified_time(&file, file_path)?;
88 unix_set_permissions(&file_path, &file)?;
90 unpacked_files.push(file_path);
96 /// List contents of `archive`, returning a vector of archive entries
97 pub fn list_archive<R>(mut archive: ZipArchive<R>) -> impl Iterator<Item = crate::Result<FileInArchive>>
99 R: Read + Seek + Send + 'static,
101 struct Files(mpsc::Receiver<crate::Result<FileInArchive>>);
102 impl Iterator for Files {
103 type Item = crate::Result<FileInArchive>;
105 fn next(&mut self) -> Option<Self::Item> {
110 let (tx, rx) = mpsc::channel();
111 thread::spawn(move || {
112 for idx in 0..archive.len() {
113 let maybe_file_in_archive = (|| {
114 let file = match archive.by_index(idx) {
116 Err(e) => return Some(Err(e.into())),
119 let path = match file.enclosed_name() {
120 Some(path) => path.to_owned(),
123 let is_dir = file.is_dir();
125 Some(Ok(FileInArchive { path, is_dir }))
127 if let Some(file_in_archive) = maybe_file_in_archive {
128 tx.send(file_in_archive).unwrap();
136 /// Compresses the archives given by `input_filenames` into the file given previously to `writer`.
137 pub fn build_archive_from_paths<W>(
138 input_filenames: &[PathBuf],
141 file_visibility_policy: FileVisibilityPolicy,
143 ) -> crate::Result<W>
147 let mut writer = zip::ZipWriter::new(writer);
148 let options = zip::write::FileOptions::default();
149 let output_handle = Handle::from_path(output_path);
152 let executable = options.unix_permissions(0o755);
154 // Vec of any filename that failed the UTF-8 check
155 let invalid_unicode_filenames = get_invalid_utf8_paths(input_filenames);
157 if !invalid_unicode_filenames.is_empty() {
158 let error = FinalError::with_title("Cannot build zip archive")
159 .detail("Zip archives require files to have valid UTF-8 paths")
161 "Files with invalid paths: {}",
162 pretty_format_list_of_paths(&invalid_unicode_filenames)
165 return Err(error.into());
168 for filename in input_filenames {
169 let previous_location = cd_into_same_dir_as(filename)?;
171 // Safe unwrap, input shall be treated before
172 let filename = filename.file_name().unwrap();
174 for entry in file_visibility_policy.build_walker(filename) {
176 let path = entry.path();
178 // If the output_path is the same as the input file, warn the user and skip the input (in order to avoid compression recursion)
179 if let Ok(ref handle) = output_handle {
180 if matches!(Handle::from_path(path), Ok(x) if &x == handle) {
182 "The output file and the input file are the same: `{}`, skipping...",
183 output_path.display()
189 // This is printed for every file in `input_filenames` and has
190 // little importance for most users, but would generate lots of
191 // spoken text for users using screen readers, braille displays
194 info!(inaccessible, "Compressing '{}'.", to_utf(path));
197 let metadata = match path.metadata() {
198 Ok(metadata) => metadata,
200 if e.kind() == std::io::ErrorKind::NotFound && utils::is_symlink(path) {
201 // This path is for a broken symlink
205 return Err(e.into());
210 let options = options.unix_permissions(metadata.permissions().mode());
212 if metadata.is_dir() {
213 writer.add_directory(path.to_str().unwrap().to_owned(), options)?;
216 let options = if is_executable::is_executable(path) {
222 let mut file = fs::File::open(entry.path())?;
224 path.to_str().unwrap(),
225 options.last_modified_time(get_last_modified_time(&file)),
227 io::copy(&mut file, &mut writer)?;
231 env::set_current_dir(previous_location)?;
234 let bytes = writer.finish()?;
238 fn display_zip_comment_if_exists(file: &ZipFile) {
239 let comment = file.comment();
240 if !comment.is_empty() {
241 // Zip file comments seem to be pretty rare, but if they are used,
242 // they may contain important information, so better show them
244 // "The .ZIP file format allows for a comment containing up to 65,535 (216−1) bytes
245 // of data to occur at the end of the file after the central directory."
247 // If there happen to be cases of very long and unnecessary comments in
248 // the future, maybe asking the user if he wants to display the comment
249 // (informing him of its size) would be sensible for both normal and
250 // accessibility mode..
251 info!(accessible, "Found comment in {}: {}", file.name(), comment);
255 fn get_last_modified_time(file: &fs::File) -> DateTime {
257 .and_then(|metadata| metadata.modified())
259 .and_then(|time| DateTime::from_time(time.into()))
263 fn set_last_modified_time(zip_file: &ZipFile, path: &Path) -> crate::Result<()> {
264 let modification_time_in_seconds = zip_file
267 .expect("Zip archive contains a file with broken 'last modified time'")
270 // Zip does not support nanoseconds, so we can assume zero here
271 let modification_time = FileTime::from_unix_time(modification_time_in_seconds, 0);
273 set_file_mtime(path, modification_time)?;
279 fn unix_set_permissions(file_path: &Path, file: &ZipFile) -> crate::Result<()> {
280 use std::fs::Permissions;
282 if let Some(mode) = file.unix_mode() {
283 fs::set_permissions(file_path, Permissions::from_mode(mode))?;