Merge pull request #330 from figsoda/ext
[ouch.git] / src / archive / zip.rs
blobe48df7bbd1e84ee3f0611b5ef4372d0b05686c72
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 humansize::{format_size, DECIMAL};
16 use same_file::Handle;
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, to_utf,
25         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<Vec<PathBuf>>
33 where
34     R: Read + Seek,
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(),
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                         format_size(file.size(), DECIMAL),
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.push(file_path);
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     let options = zip::write::FileOptions::default();
149     let output_handle = Handle::from_path(output_path);
151     #[cfg(not(unix))]
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")
160             .detail(format!(
161                 "Files with invalid paths: {}",
162                 pretty_format_list_of_paths(&invalid_unicode_filenames)
163             ));
165         return Err(error.into());
166     }
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) {
175             let entry = entry?;
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) {
181                     warning!(
182                         "The output file and the input file are the same: `{}`, skipping...",
183                         output_path.display()
184                     );
185                     continue;
186                 }
187             }
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
192             // and so on
193             if !quiet {
194                 info!(inaccessible, "Compressing '{}'.", to_utf(path));
195             }
197             let metadata = match path.metadata() {
198                 Ok(metadata) => metadata,
199                 Err(e) => {
200                     if e.kind() == std::io::ErrorKind::NotFound && utils::is_symlink(path) {
201                         // This path is for a broken symlink
202                         // We just ignore it
203                         continue;
204                     }
205                     return Err(e.into());
206                 }
207             };
209             #[cfg(unix)]
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)?;
214             } else {
215                 #[cfg(not(unix))]
216                 let options = if is_executable::is_executable(path) {
217                     executable
218                 } else {
219                     options
220                 };
222                 let mut file = fs::File::open(entry.path())?;
223                 writer.start_file(
224                     path.to_str().unwrap(),
225                     options.last_modified_time(get_last_modified_time(&file)),
226                 )?;
227                 io::copy(&mut file, &mut writer)?;
228             }
229         }
231         env::set_current_dir(previous_location)?;
232     }
234     let bytes = writer.finish()?;
235     Ok(bytes)
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
243         //
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."
246         //
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);
252     }
255 fn get_last_modified_time(file: &fs::File) -> DateTime {
256     file.metadata()
257         .and_then(|metadata| metadata.modified())
258         .map_err(|_| ())
259         .and_then(|time| DateTime::from_time(time.into()))
260         .unwrap_or_default()
263 fn set_last_modified_time(zip_file: &ZipFile, path: &Path) -> crate::Result<()> {
264     let modification_time_in_seconds = zip_file
265         .last_modified()
266         .to_time()
267         .expect("Zip archive contains a file with broken 'last modified time'")
268         .unix_timestamp();
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)?;
275     Ok(())
278 #[cfg(unix)]
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))?;
284     }
286     Ok(())