`--help`: add `.sz` to list of supported formats
[ouch.git] / src / archive / zip.rs
blobfa0ab18f2674acdcfd1a8e3a0e70bfa50b533fbc
1 //! Contains Zip-specific building and unpacking functions
3 #[cfg(unix)]
4 use std::os::unix::fs::PermissionsExt;
5 use std::{
6     env,
7     io::{self, prelude::*},
8     path::{Path, PathBuf},
9     sync::mpsc,
10     thread,
13 use filetime::{set_file_mtime, FileTime};
14 use fs_err as fs;
15 use same_file::Handle;
16 use ubyte::ToByteUnit;
17 use zip::{self, read::ZipFile, DateTime, ZipArchive};
19 use crate::{
20     error::FinalError,
21     info,
22     list::FileInArchive,
23     utils::{
24         self, cd_into_same_dir_as, get_invalid_utf8_paths, pretty_format_list_of_paths, strip_cur_dir,
25         EscapedPathDisplay, FileVisibilityPolicy,
26     },
27     warning,
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<usize>
33 where
34     R: Read + Seek,
36     assert!(output_folder.read_dir().expect("dir exists").count() == 0);
38     let mut unpacked_files = 0;
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(),
44             None => continue,
45         };
47         let file_path = output_folder.join(file_path);
49         display_zip_comment_if_exists(&file);
51         match file.name().ends_with('/') {
52             _is_dir @ true => {
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
56                 // and so on
57                 if !quiet {
58                     info!(inaccessible, "File {} extracted to \"{}\"", idx, file_path.display());
59                 }
60                 fs::create_dir_all(&file_path)?;
61             }
62             _is_file @ false => {
63                 if let Some(path) = file_path.parent() {
64                     if !path.exists() {
65                         fs::create_dir_all(path)?;
66                     }
67                 }
68                 let file_path = strip_cur_dir(file_path.as_path());
70                 // same reason is in _is_dir: long, often not needed text
71                 if !quiet {
72                     info!(
73                         inaccessible,
74                         "{:?} extracted. ({})",
75                         file_path.display(),
76                         file.size().bytes()
77                     );
78                 }
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)?;
84             }
85         }
87         #[cfg(unix)]
88         unix_set_permissions(&file_path, &file)?;
90         unpacked_files += 1;
91     }
93     Ok(unpacked_files)
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>>
98 where
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> {
106             self.0.recv().ok()
107         }
108     }
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) {
115                     Ok(f) => f,
116                     Err(e) => return Some(Err(e.into())),
117                 };
119                 let path = match file.enclosed_name() {
120                     Some(path) => path.to_owned(),
121                     None => return None,
122                 };
123                 let is_dir = file.is_dir();
125                 Some(Ok(FileInArchive { path, is_dir }))
126             })();
127             if let Some(file_in_archive) = maybe_file_in_archive {
128                 tx.send(file_in_archive).unwrap();
129             }
130         }
131     });
133     Files(rx)
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],
139     output_path: &Path,
140     writer: W,
141     file_visibility_policy: FileVisibilityPolicy,
142     quiet: bool,
143 ) -> crate::Result<W>
144 where
145     W: Write + Seek,
147     let mut writer = zip::ZipWriter::new(writer);
148     // always use ZIP64 to allow compression of files larger than 4GB
149     // the format is widely supported and the extra 20B is negligible in most cases
150     let options = zip::write::FileOptions::default().large_file(true);
151     let output_handle = Handle::from_path(output_path);
153     #[cfg(not(unix))]
154     let executable = options.unix_permissions(0o755);
156     // Vec of any filename that failed the UTF-8 check
157     let invalid_unicode_filenames = get_invalid_utf8_paths(input_filenames);
159     if !invalid_unicode_filenames.is_empty() {
160         let error = FinalError::with_title("Cannot build zip archive")
161             .detail("Zip archives require files to have valid UTF-8 paths")
162             .detail(format!(
163                 "Files with invalid paths: {}",
164                 pretty_format_list_of_paths(&invalid_unicode_filenames)
165             ));
167         return Err(error.into());
168     }
170     for filename in input_filenames {
171         let previous_location = cd_into_same_dir_as(filename)?;
173         // Safe unwrap, input shall be treated before
174         let filename = filename.file_name().unwrap();
176         for entry in file_visibility_policy.build_walker(filename) {
177             let entry = entry?;
178             let path = entry.path();
180             // If the output_path is the same as the input file, warn the user and skip the input (in order to avoid compression recursion)
181             if let Ok(ref handle) = output_handle {
182                 if matches!(Handle::from_path(path), Ok(x) if &x == handle) {
183                     warning!(
184                         "The output file and the input file are the same: `{}`, skipping...",
185                         output_path.display()
186                     );
187                     continue;
188                 }
189             }
191             // This is printed for every file in `input_filenames` and has
192             // little importance for most users, but would generate lots of
193             // spoken text for users using screen readers, braille displays
194             // and so on
195             if !quiet {
196                 info!(inaccessible, "Compressing '{}'.", EscapedPathDisplay::new(path));
197             }
199             let metadata = match path.metadata() {
200                 Ok(metadata) => metadata,
201                 Err(e) => {
202                     if e.kind() == std::io::ErrorKind::NotFound && utils::is_symlink(path) {
203                         // This path is for a broken symlink
204                         // We just ignore it
205                         continue;
206                     }
207                     return Err(e.into());
208                 }
209             };
211             #[cfg(unix)]
212             let options = options.unix_permissions(metadata.permissions().mode());
214             if metadata.is_dir() {
215                 writer.add_directory(path.to_str().unwrap().to_owned(), options)?;
216             } else {
217                 #[cfg(not(unix))]
218                 let options = if is_executable::is_executable(path) {
219                     executable
220                 } else {
221                     options
222                 };
224                 let mut file = fs::File::open(path)?;
225                 writer.start_file(
226                     path.to_str().unwrap(),
227                     options.last_modified_time(get_last_modified_time(&file)),
228                 )?;
229                 io::copy(&mut file, &mut writer)?;
230             }
231         }
233         env::set_current_dir(previous_location)?;
234     }
236     let bytes = writer.finish()?;
237     Ok(bytes)
240 fn display_zip_comment_if_exists(file: &ZipFile) {
241     let comment = file.comment();
242     if !comment.is_empty() {
243         // Zip file comments seem to be pretty rare, but if they are used,
244         // they may contain important information, so better show them
245         //
246         // "The .ZIP file format allows for a comment containing up to 65,535 (216−1) bytes
247         // of data to occur at the end of the file after the central directory."
248         //
249         // If there happen to be cases of very long and unnecessary comments in
250         // the future, maybe asking the user if he wants to display the comment
251         // (informing him of its size) would be sensible for both normal and
252         // accessibility mode..
253         info!(accessible, "Found comment in {}: {}", file.name(), comment);
254     }
257 fn get_last_modified_time(file: &fs::File) -> DateTime {
258     file.metadata()
259         .and_then(|metadata| metadata.modified())
260         .map_err(|_| ())
261         .and_then(|time| DateTime::from_time(time.into()))
262         .unwrap_or_default()
265 fn set_last_modified_time(zip_file: &ZipFile, path: &Path) -> crate::Result<()> {
266     let modification_time_in_seconds = zip_file
267         .last_modified()
268         .to_time()
269         .expect("Zip archive contains a file with broken 'last modified time'")
270         .unix_timestamp();
272     // Zip does not support nanoseconds, so we can assume zero here
273     let modification_time = FileTime::from_unix_time(modification_time_in_seconds, 0);
275     set_file_mtime(path, modification_time)?;
277     Ok(())
280 #[cfg(unix)]
281 fn unix_set_permissions(file_path: &Path, file: &ZipFile) -> crate::Result<()> {
282     use std::fs::Permissions;
284     if let Some(mode) = file.unix_mode() {
285         fs::set_permissions(file_path, Permissions::from_mode(mode))?;
286     }
288     Ok(())