1 //! Contains Zip-specific building and unpacking functions
4 use std::os::unix::fs::PermissionsExt;
8 io::{self, prelude::*},
14 use filetime::{set_file_mtime, FileTime};
16 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, to_utf, Bytes,
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>,
35 ) -> crate::Result<Vec<PathBuf>>
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(),
51 let file_path = output_folder.join(file_path);
53 display_zip_comment_if_exists(&file);
55 match file.name().ends_with('/') {
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
61 info!(@log_out, inaccessible, "File {} extracted to \"{}\"", idx, file_path.display());
62 fs::create_dir_all(&file_path)?;
65 if let Some(path) = file_path.parent() {
67 fs::create_dir_all(path)?;
70 let file_path = strip_cur_dir(file_path.as_path());
72 // same reason is in _is_dir: long, often not needed text
76 "{:?} extracted. ({})",
77 file_path.display(), 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)?;
90 unpacked_files.push(file_path);
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 = match file.enclosed_name() {
120 Some(path) => path.to_owned(),
123 let is_dir = file.is_dir();
125 Some(Ok(FileInArchive { path, is_dir }))
127 if let Some(file_in_archive) = maybe_file_in_archive {
128 tx.send(file_in_archive).unwrap();
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],
140 file_visibility_policy: FileVisibilityPolicy,
142 ) -> crate::Result<W>
147 let mut writer = zip::ZipWriter::new(writer);
148 let options = zip::write::FileOptions::default();
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)?;
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) {
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
181 info!(@log_out, inaccessible, "Compressing '{}'.", to_utf(path));
183 let metadata = match path.metadata() {
184 Ok(metadata) => metadata,
186 if e.kind() == std::io::ErrorKind::NotFound && utils::is_symlink(path) {
187 // This path is for a broken symlink
191 return Err(e.into());
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)?;
202 let options = if is_executable::is_executable(path) {
208 let mut file = File::open(entry.path())?;
210 path.to_str().unwrap(),
211 options.last_modified_time(get_last_modified_time(&file)),
213 io::copy(&mut file, &mut writer)?;
217 env::set_current_dir(previous_location)?;
220 let bytes = writer.finish()?;
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
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."
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);
241 fn get_last_modified_time(file: &File) -> DateTime {
243 .and_then(|metadata| metadata.modified())
245 .and_then(|time| DateTime::from_time(time.into()))
249 fn set_last_modified_time(zip_file: &ZipFile, path: &Path) -> crate::Result<()> {
250 let modification_time_in_seconds = zip_file
253 .expect("Zip archive contains a file with broken 'last modified time'")
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)?;
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))?;