1 //! Contains Zip-specific building and unpacking functions
4 use std::os::unix::fs::PermissionsExt;
7 io::{self, prelude::*},
13 use filetime_creation::{set_file_mtime, FileTime};
15 use same_file::Handle;
16 use time::OffsetDateTime;
17 use zip::{self, read::ZipFile, DateTime, ZipArchive};
24 self, cd_into_same_dir_as, get_invalid_utf8_paths, pretty_format_list_of_paths, strip_cur_dir, Bytes,
25 EscapedPathDisplay, FileVisibilityPolicy,
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>
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(),
47 let file_path = output_folder.join(file_path);
49 display_zip_comment_if_exists(&file);
51 match file.name().ends_with('/') {
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
58 info!(inaccessible, "File {} extracted to \"{}\"", idx, file_path.display());
60 fs::create_dir_all(&file_path)?;
63 if let Some(path) = file_path.parent() {
65 fs::create_dir_all(path)?;
68 let file_path = strip_cur_dir(file_path.as_path());
70 // same reason is in _is_dir: long, often not needed text
74 "{:?} extracted. ({})",
76 Bytes::new(file.size()),
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)?;
88 unix_set_permissions(&file_path, &file)?;
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>>
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> {
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) {
116 Err(e) => return Some(Err(e.into())),
119 let path = file.enclosed_name()?.to_owned();
120 let is_dir = file.is_dir();
122 Some(Ok(FileInArchive { path, is_dir }))
124 if let Some(file_in_archive) = maybe_file_in_archive {
125 tx.send(file_in_archive).unwrap();
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],
138 file_visibility_policy: FileVisibilityPolicy,
140 ) -> crate::Result<W>
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);
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")
160 "Files with invalid paths: {}",
161 pretty_format_list_of_paths(&invalid_unicode_filenames)
164 return Err(error.into());
167 for filename in input_filenames {
168 let previous_location = cd_into_same_dir_as(filename)?;
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) {
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) {
182 "The output file and the input file are the same: `{}`, skipping...",
183 output_path.display()
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
194 info!(inaccessible, "Compressing '{}'.", EscapedPathDisplay::new(path));
197 let metadata = match path.metadata() {
198 Ok(metadata) => metadata,
200 if e.kind() == std::io::ErrorKind::NotFound && utils::is_symlink(path) {
201 // This path is for a broken symlink
205 return Err(e.into());
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"))
217 if metadata.is_dir() {
218 writer.add_directory(entry_name, options)?;
221 let options = if is_executable::is_executable(path) {
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)?;
237 env::set_current_dir(previous_location)?;
240 let bytes = writer.finish()?;
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
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."
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);
261 fn get_last_modified_time(file: &fs::File) -> DateTime {
263 .and_then(|metadata| metadata.modified())
265 .and_then(|time| DateTime::try_from(OffsetDateTime::from(time)).ok())
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 {
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)?;
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))?;