1 //! Contains Zip-specific building and unpacking functions
5 io::{self, prelude::*},
12 use zip::{self, read::ZipFile, ZipArchive};
19 self, cd_into_same_dir_as, get_invalid_utf8_paths, pretty_format_list_of_paths, strip_cur_dir, to_utf, Bytes,
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>,
29 mut display_handle: D,
30 ) -> crate::Result<Vec<PathBuf>>
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(),
46 let file_path = output_folder.join(file_path);
48 display_zip_comment_if_exists(&file);
50 match file.name().ends_with('/') {
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
56 info!(@display_handle, inaccessible, "File {} extracted to \"{}\"", idx, file_path.display());
57 fs::create_dir_all(&file_path)?;
60 if let Some(path) = file_path.parent() {
62 fs::create_dir_all(&path)?;
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)?;
74 set_last_modified_time(&output_file, &file)?;
79 __unix_set_permissions(&file_path, &file)?;
81 unpacked_files.push(file_path);
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>>
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> {
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) {
107 Err(e) => return Some(Err(e.into())),
110 let path = match file.enclosed_name() {
111 Some(path) => path.to_owned(),
114 let is_dir = file.is_dir();
116 Some(Ok(FileInArchive { path, is_dir }))
118 if let Some(file_in_archive) = maybe_file_in_archive {
119 tx.send(file_in_archive).unwrap();
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],
131 file_visibility_policy: FileVisibilityPolicy,
132 mut display_handle: D,
133 ) -> crate::Result<W>
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")
148 "Files with invalid paths: {}",
149 pretty_format_list_of_paths(&invalid_unicode_filenames)
152 return Err(error.into());
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) {
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
169 info!(@display_handle, inaccessible, "Compressing '{}'.", to_utf(path));
172 writer.add_directory(path.to_str().unwrap().to_owned(), options)?;
174 writer.start_file(path.to_str().unwrap().to_owned(), options)?;
175 let file_bytes = match fs::read(entry.path()) {
178 if e.kind() == std::io::ErrorKind::NotFound && utils::is_symlink(path) {
179 // This path is for a broken symlink
183 return Err(e.into());
186 writer.write_all(&file_bytes)?;
190 env::set_current_dir(previous_location)?;
193 let bytes = writer.finish()?;
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
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."
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);
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 {
236 fn set_last_modified_time(file: &fs::File, zip_file: &ZipFile) -> crate::Result<()> {
237 use std::os::unix::prelude::AsRawFd;
241 let now = libc::timespec {
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, × as *const _) };
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))?;