Merge pull request #261 from ouch-org/refac/optimize-current-dir-call
[ouch.git] / src / archive / zip.rs
bloba16468ef091c2438537acbb2771501458e1363df
1 //! Contains Zip-specific building and unpacking functions
3 use std::{
4     env,
5     io::{self, prelude::*},
6     path::{Path, PathBuf},
7     sync::mpsc,
8     thread,
9 };
11 use fs_err as fs;
12 use zip::{self, read::ZipFile, ZipArchive};
14 use crate::{
15     error::FinalError,
16     info,
17     list::FileInArchive,
18     utils::{
19         self, cd_into_same_dir_as, get_invalid_utf8_paths, pretty_format_list_of_paths, strip_cur_dir, to_utf, Bytes,
20         FileVisibilityPolicy,
21     },
24 /// Unpacks the archive given by `archive` into the folder given by `output_folder`.
25 /// Assumes that output_folder is empty
26 pub fn unpack_archive<R, D>(
27     mut archive: ZipArchive<R>,
28     output_folder: &Path,
29     mut display_handle: D,
30 ) -> crate::Result<Vec<PathBuf>>
31 where
32     R: Read + Seek,
33     D: Write,
35     assert!(output_folder.read_dir().expect("dir exists").count() == 0);
37     let mut unpacked_files = Vec::with_capacity(archive.len());
39     for idx in 0..archive.len() {
40         let mut file = archive.by_index(idx)?;
41         let file_path = match file.enclosed_name() {
42             Some(path) => path.to_owned(),
43             None => continue,
44         };
46         let file_path = output_folder.join(file_path);
48         display_zip_comment_if_exists(&file);
50         match file.name().ends_with('/') {
51             _is_dir @ true => {
52                 // This is printed for every file in the archive and has little
53                 // importance for most users, but would generate lots of
54                 // spoken text for users using screen readers, braille displays
55                 // and so on
56                 info!(@display_handle, inaccessible, "File {} extracted to \"{}\"", idx, file_path.display());
57                 fs::create_dir_all(&file_path)?;
58             }
59             _is_file @ false => {
60                 if let Some(path) = file_path.parent() {
61                     if !path.exists() {
62                         fs::create_dir_all(&path)?;
63                     }
64                 }
65                 let file_path = strip_cur_dir(file_path.as_path());
67                 // same reason is in _is_dir: long, often not needed text
68                 info!(@display_handle, inaccessible, "{:?} extracted. ({})", file_path.display(), Bytes::new(file.size()));
70                 let mut output_file = fs::File::create(&file_path)?;
71                 io::copy(&mut file, &mut output_file)?;
73                 #[cfg(unix)]
74                 set_last_modified_time(&output_file, &file)?;
75             }
76         }
78         #[cfg(unix)]
79         __unix_set_permissions(&file_path, &file)?;
81         unpacked_files.push(file_path);
82     }
84     Ok(unpacked_files)
87 /// List contents of `archive`, returning a vector of archive entries
88 pub fn list_archive<R>(mut archive: ZipArchive<R>) -> impl Iterator<Item = crate::Result<FileInArchive>>
89 where
90     R: Read + Seek + Send + 'static,
92     struct Files(mpsc::Receiver<crate::Result<FileInArchive>>);
93     impl Iterator for Files {
94         type Item = crate::Result<FileInArchive>;
96         fn next(&mut self) -> Option<Self::Item> {
97             self.0.recv().ok()
98         }
99     }
101     let (tx, rx) = mpsc::channel();
102     thread::spawn(move || {
103         for idx in 0..archive.len() {
104             let maybe_file_in_archive = (|| {
105                 let file = match archive.by_index(idx) {
106                     Ok(f) => f,
107                     Err(e) => return Some(Err(e.into())),
108                 };
110                 let path = match file.enclosed_name() {
111                     Some(path) => path.to_owned(),
112                     None => return None,
113                 };
114                 let is_dir = file.is_dir();
116                 Some(Ok(FileInArchive { path, is_dir }))
117             })();
118             if let Some(file_in_archive) = maybe_file_in_archive {
119                 tx.send(file_in_archive).unwrap();
120             }
121         }
122     });
124     Files(rx)
127 /// Compresses the archives given by `input_filenames` into the file given previously to `writer`.
128 pub fn build_archive_from_paths<W, D>(
129     input_filenames: &[PathBuf],
130     writer: W,
131     file_visibility_policy: FileVisibilityPolicy,
132     mut display_handle: D,
133 ) -> crate::Result<W>
134 where
135     W: Write + Seek,
136     D: Write,
138     let mut writer = zip::ZipWriter::new(writer);
139     let options = zip::write::FileOptions::default();
141     // Vec of any filename that failed the UTF-8 check
142     let invalid_unicode_filenames = get_invalid_utf8_paths(input_filenames);
144     if !invalid_unicode_filenames.is_empty() {
145         let error = FinalError::with_title("Cannot build zip archive")
146             .detail("Zip archives require files to have valid UTF-8 paths")
147             .detail(format!(
148                 "Files with invalid paths: {}",
149                 pretty_format_list_of_paths(&invalid_unicode_filenames)
150             ));
152         return Err(error.into());
153     }
155     for filename in input_filenames {
156         let previous_location = cd_into_same_dir_as(filename)?;
158         // Safe unwrap, input shall be treated before
159         let filename = filename.file_name().unwrap();
161         for entry in file_visibility_policy.build_walker(filename) {
162             let entry = entry?;
163             let path = entry.path();
165             // This is printed for every file in `input_filenames` and has
166             // little importance for most users, but would generate lots of
167             // spoken text for users using screen readers, braille displays
168             // and so on
169             info!(@display_handle, inaccessible, "Compressing '{}'.", to_utf(path));
171             if path.is_dir() {
172                 writer.add_directory(path.to_str().unwrap().to_owned(), options)?;
173             } else {
174                 writer.start_file(path.to_str().unwrap().to_owned(), options)?;
175                 let file_bytes = match fs::read(entry.path()) {
176                     Ok(b) => b,
177                     Err(e) => {
178                         if e.kind() == std::io::ErrorKind::NotFound && utils::is_symlink(path) {
179                             // This path is for a broken symlink
180                             // We just ignore it
181                             continue;
182                         }
183                         return Err(e.into());
184                     }
185                 };
186                 writer.write_all(&file_bytes)?;
187             }
188         }
190         env::set_current_dir(previous_location)?;
191     }
193     let bytes = writer.finish()?;
194     Ok(bytes)
197 fn display_zip_comment_if_exists(file: &ZipFile) {
198     let comment = file.comment();
199     if !comment.is_empty() {
200         // Zip file comments seem to be pretty rare, but if they are used,
201         // they may contain important information, so better show them
202         //
203         // "The .ZIP file format allows for a comment containing up to 65,535 (216−1) bytes
204         // of data to occur at the end of the file after the central directory."
205         //
206         // If there happen to be cases of very long and unnecessary comments in
207         // the future, maybe asking the user if he wants to display the comment
208         // (informing him of its size) would be sensible for both normal and
209         // accessibility mode..
210         info!(accessible, "Found comment in {}: {}", file.name(), comment);
211     }
214 #[cfg(unix)]
215 /// Attempts to convert a [`zip::DateTime`] to a [`libc::timespec`].
216 fn convert_zip_date_time(date_time: zip::DateTime) -> Option<libc::timespec> {
217     use time::{Date, Month, PrimitiveDateTime, Time};
219     // Safety: time::Month is repr(u8) and goes from 1 to 12
220     let month: Month = unsafe { std::mem::transmute(date_time.month()) };
222     let date = Date::from_calendar_date(date_time.year() as _, month, date_time.day()).ok()?;
224     let time = Time::from_hms(date_time.hour(), date_time.minute(), date_time.second()).ok()?;
226     let date_time = PrimitiveDateTime::new(date, time);
227     let timestamp = date_time.assume_utc().unix_timestamp();
229     Some(libc::timespec {
230         tv_sec: timestamp,
231         tv_nsec: 0,
232     })
235 #[cfg(unix)]
236 fn set_last_modified_time(file: &fs::File, zip_file: &ZipFile) -> crate::Result<()> {
237     use std::os::unix::prelude::AsRawFd;
239     use libc::UTIME_NOW;
241     let now = libc::timespec {
242         tv_sec: 0,
243         tv_nsec: UTIME_NOW,
244     };
246     let last_modified = zip_file.last_modified();
247     let last_modified = convert_zip_date_time(last_modified).unwrap_or(now);
249     // The first value is the last accessed time, which we'll set as being right now.
250     // The second value is the last modified time, which we'll copy over from the zip archive
251     let times = [now, last_modified];
253     let output_fd = file.as_raw_fd();
255     // TODO: check for -1
256     unsafe { libc::futimens(output_fd, &times as *const _) };
258     Ok(())
261 #[cfg(unix)]
262 fn __unix_set_permissions(file_path: &Path, file: &ZipFile) -> crate::Result<()> {
263     use std::{fs::Permissions, os::unix::fs::PermissionsExt};
265     if let Some(mode) = file.unix_mode() {
266         fs::set_permissions(file_path, Permissions::from_mode(mode))?;
267     }
269     Ok(())