Decrease memory usage in ARMv7 CI
[ouch.git] / src / archive / zip.rs
blobe9bde15d67719054bf77d88854be02e5515e799c
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_creation::{set_file_mtime, FileTime};
14 use fs_err as fs;
15 use same_file::Handle;
16 use time::OffsetDateTime;
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, Bytes,
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                         Bytes::new(file.size()),
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 = file.enclosed_name()?.to_owned();
120                 let is_dir = file.is_dir();
122                 Some(Ok(FileInArchive { path, is_dir }))
123             })();
124             if let Some(file_in_archive) = maybe_file_in_archive {
125                 tx.send(file_in_archive).unwrap();
126             }
127         }
128     });
130     Files(rx)
133 /// Compresses the archives given by `input_filenames` into the file given previously to `writer`.
134 pub fn build_archive_from_paths<W>(
135     input_filenames: &[PathBuf],
136     output_path: &Path,
137     writer: W,
138     file_visibility_policy: FileVisibilityPolicy,
139     quiet: bool,
140 ) -> crate::Result<W>
141 where
142     W: Write + Seek,
144     let mut writer = zip::ZipWriter::new(writer);
145     // always use ZIP64 to allow compression of files larger than 4GB
146     // the format is widely supported and the extra 20B is negligible in most cases
147     let options = zip::write::FileOptions::default().large_file(true);
148     let output_handle = Handle::from_path(output_path);
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         // Unwrap safety:
171         //   paths should be canonicalized by now, and the root directory rejected.
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(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 '{}'.", EscapedPathDisplay::new(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             let entry_name = path.to_str().ok_or_else(|| {
213                 FinalError::with_title("Zip requires that all directories names are valid UTF-8")
214                     .detail(format!("File at '{path:?}' has a non-UTF-8 name"))
215             })?;
217             if metadata.is_dir() {
218                 writer.add_directory(entry_name, options)?;
219             } else {
220                 #[cfg(not(unix))]
221                 let options = if is_executable::is_executable(path) {
222                     executable
223                 } else {
224                     options
225                 };
227                 let mut file = fs::File::open(path)?;
229                 // Updated last modified time
230                 let last_modified_time = options.last_modified_time(get_last_modified_time(&file));
232                 writer.start_file(entry_name, last_modified_time)?;
233                 io::copy(&mut file, &mut writer)?;
234             }
235         }
237         env::set_current_dir(previous_location)?;
238     }
240     let bytes = writer.finish()?;
241     Ok(bytes)
244 fn display_zip_comment_if_exists(file: &ZipFile) {
245     let comment = file.comment();
246     if !comment.is_empty() {
247         // Zip file comments seem to be pretty rare, but if they are used,
248         // they may contain important information, so better show them
249         //
250         // "The .ZIP file format allows for a comment containing up to 65,535 (216−1) bytes
251         // of data to occur at the end of the file after the central directory."
252         //
253         // If there happen to be cases of very long and unnecessary comments in
254         // the future, maybe asking the user if he wants to display the comment
255         // (informing him of its size) would be sensible for both normal and
256         // accessibility mode..
257         info!(accessible, "Found comment in {}: {}", file.name(), comment);
258     }
261 fn get_last_modified_time(file: &fs::File) -> DateTime {
262     file.metadata()
263         .and_then(|metadata| metadata.modified())
264         .ok()
265         .and_then(|time| DateTime::try_from(OffsetDateTime::from(time)).ok())
266         .unwrap_or_default()
269 fn set_last_modified_time(zip_file: &ZipFile, path: &Path) -> crate::Result<()> {
270     let modification_time = zip_file.last_modified().to_time();
272     let Ok(time_in_seconds) = modification_time else {
273         return Ok(());
274     };
276     // Zip does not support nanoseconds, so we can assume zero here
277     let modification_time = FileTime::from_unix_time(time_in_seconds.unix_timestamp(), 0);
279     set_file_mtime(path, modification_time)?;
281     Ok(())
284 #[cfg(unix)]
285 fn unix_set_permissions(file_path: &Path, file: &ZipFile) -> crate::Result<()> {
286     use std::fs::Permissions;
288     if let Some(mode) = file.unix_mode() {
289         fs::set_permissions(file_path, Permissions::from_mode(mode))?;
290     }
292     Ok(())