add OutputLine trait for performance improvements
[ouch.git] / src / archive / zip.rs
blobc804a916bd552fe1f3ccf82dc8a96962a16278ce
1 //! Contains Zip-specific building and unpacking functions
3 #[cfg(unix)]
4 use std::os::unix::fs::PermissionsExt;
5 use std::{
6     env,
7     fs::File,
8     io::{self, prelude::*},
9     path::{Path, PathBuf},
10     sync::mpsc,
11     thread,
14 use filetime::{set_file_mtime, FileTime};
15 use fs_err as fs;
16 use zip::{self, read::ZipFile, DateTime, ZipArchive};
18 use crate::{
19     error::FinalError,
20     info,
21     list::FileInArchive,
22     progress::OutputLine,
23     utils::{
24         self, cd_into_same_dir_as, get_invalid_utf8_paths, pretty_format_list_of_paths, strip_cur_dir, to_utf, Bytes,
25         FileVisibilityPolicy,
26     },
29 /// Unpacks the archive given by `archive` into the folder given by `output_folder`.
30 /// Assumes that output_folder is empty
31 pub fn unpack_archive<R, D>(
32     mut archive: ZipArchive<R>,
33     output_folder: &Path,
34     mut log_out: D,
35 ) -> crate::Result<Vec<PathBuf>>
36 where
37     R: Read + Seek,
38     D: OutputLine,
40     assert!(output_folder.read_dir().expect("dir exists").count() == 0);
42     let mut unpacked_files = Vec::with_capacity(archive.len());
44     for idx in 0..archive.len() {
45         let mut file = archive.by_index(idx)?;
46         let file_path = match file.enclosed_name() {
47             Some(path) => path.to_owned(),
48             None => continue,
49         };
51         let file_path = output_folder.join(file_path);
53         display_zip_comment_if_exists(&file);
55         match file.name().ends_with('/') {
56             _is_dir @ true => {
57                 // This is printed for every file in the archive and has little
58                 // importance for most users, but would generate lots of
59                 // spoken text for users using screen readers, braille displays
60                 // and so on
61                 info!(@log_out, inaccessible, "File {} extracted to \"{}\"", idx, file_path.display());
62                 fs::create_dir_all(&file_path)?;
63             }
64             _is_file @ false => {
65                 if let Some(path) = file_path.parent() {
66                     if !path.exists() {
67                         fs::create_dir_all(path)?;
68                     }
69                 }
70                 let file_path = strip_cur_dir(file_path.as_path());
72                 // same reason is in _is_dir: long, often not needed text
73                 info!(
74                     @log_out,
75                     inaccessible,
76                     "{:?} extracted. ({})",
77                     file_path.display(), Bytes::new(file.size())
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, D>(
138     input_filenames: &[PathBuf],
139     writer: W,
140     file_visibility_policy: FileVisibilityPolicy,
141     mut log_out: D,
142 ) -> crate::Result<W>
143 where
144     W: Write + Seek,
145     D: OutputLine,
147     let mut writer = zip::ZipWriter::new(writer);
148     let options = zip::write::FileOptions::default();
150     #[cfg(not(unix))]
151     let executable = options.unix_permissions(0o755);
153     // Vec of any filename that failed the UTF-8 check
154     let invalid_unicode_filenames = get_invalid_utf8_paths(input_filenames);
156     if !invalid_unicode_filenames.is_empty() {
157         let error = FinalError::with_title("Cannot build zip archive")
158             .detail("Zip archives require files to have valid UTF-8 paths")
159             .detail(format!(
160                 "Files with invalid paths: {}",
161                 pretty_format_list_of_paths(&invalid_unicode_filenames)
162             ));
164         return Err(error.into());
165     }
167     for filename in input_filenames {
168         let previous_location = cd_into_same_dir_as(filename)?;
170         // Safe unwrap, input shall be treated before
171         let filename = filename.file_name().unwrap();
173         for entry in file_visibility_policy.build_walker(filename) {
174             let entry = entry?;
175             let path = entry.path();
177             // This is printed for every file in `input_filenames` and has
178             // little importance for most users, but would generate lots of
179             // spoken text for users using screen readers, braille displays
180             // and so on
181             info!(@log_out, inaccessible, "Compressing '{}'.", to_utf(path));
183             let metadata = match path.metadata() {
184                 Ok(metadata) => metadata,
185                 Err(e) => {
186                     if e.kind() == std::io::ErrorKind::NotFound && utils::is_symlink(path) {
187                         // This path is for a broken symlink
188                         // We just ignore it
189                         continue;
190                     }
191                     return Err(e.into());
192                 }
193             };
195             #[cfg(unix)]
196             let options = options.unix_permissions(metadata.permissions().mode());
198             if metadata.is_dir() {
199                 writer.add_directory(path.to_str().unwrap().to_owned(), options)?;
200             } else {
201                 #[cfg(not(unix))]
202                 let options = if is_executable::is_executable(path) {
203                     executable
204                 } else {
205                     options
206                 };
208                 let mut file = File::open(entry.path())?;
209                 writer.start_file(
210                     path.to_str().unwrap(),
211                     options.last_modified_time(get_last_modified_time(&file)),
212                 )?;
213                 io::copy(&mut file, &mut writer)?;
214             }
215         }
217         env::set_current_dir(previous_location)?;
218     }
220     let bytes = writer.finish()?;
221     Ok(bytes)
224 fn display_zip_comment_if_exists(file: &ZipFile) {
225     let comment = file.comment();
226     if !comment.is_empty() {
227         // Zip file comments seem to be pretty rare, but if they are used,
228         // they may contain important information, so better show them
229         //
230         // "The .ZIP file format allows for a comment containing up to 65,535 (216−1) bytes
231         // of data to occur at the end of the file after the central directory."
232         //
233         // If there happen to be cases of very long and unnecessary comments in
234         // the future, maybe asking the user if he wants to display the comment
235         // (informing him of its size) would be sensible for both normal and
236         // accessibility mode..
237         info!(accessible, "Found comment in {}: {}", file.name(), comment);
238     }
241 fn get_last_modified_time(file: &File) -> DateTime {
242     file.metadata()
243         .and_then(|metadata| metadata.modified())
244         .map_err(|_| ())
245         .and_then(|time| DateTime::from_time(time.into()))
246         .unwrap_or_default()
249 fn set_last_modified_time(zip_file: &ZipFile, path: &Path) -> crate::Result<()> {
250     let modification_time_in_seconds = zip_file
251         .last_modified()
252         .to_time()
253         .expect("Zip archive contains a file with broken 'last modified time'")
254         .unix_timestamp();
256     // Zip does not support nanoseconds, so we can assume zero here
257     let modification_time = FileTime::from_unix_time(modification_time_in_seconds, 0);
259     set_file_mtime(path, modification_time)?;
261     Ok(())
264 #[cfg(unix)]
265 fn unix_set_permissions(file_path: &Path, file: &ZipFile) -> crate::Result<()> {
266     use std::fs::Permissions;
268     if let Some(mode) = file.unix_mode() {
269         fs::set_permissions(file_path, Permissions::from_mode(mode))?;
270     }
272     Ok(())