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};
23 cd_into_same_dir_as, get_invalid_utf8_paths,
24 logger::{info, info_accessible, warning},
25 pretty_format_list_of_paths, strip_cur_dir, Bytes, EscapedPathDisplay, FileVisibilityPolicy,
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>(
32 mut archive: ZipArchive<R>,
34 password: Option<&[u8]>,
36 ) -> crate::Result<usize>
40 assert!(output_folder.read_dir().expect("dir exists").count() == 0);
42 let mut unpacked_files = 0;
44 for idx in 0..archive.len() {
45 let mut file = match password {
46 Some(password) => archive
47 .by_index_decrypt(idx, password)?
48 .map_err(|_| zip::result::ZipError::UnsupportedArchive("Password required to decrypt file"))?,
49 None => archive.by_index(idx)?,
51 let file_path = match file.enclosed_name() {
52 Some(path) => path.to_owned(),
56 let file_path = output_folder.join(file_path);
58 display_zip_comment_if_exists(&file);
60 match file.name().ends_with('/') {
62 // This is printed for every file in the archive and has little
63 // importance for most users, but would generate lots of
64 // spoken text for users using screen readers, braille displays
67 info(format!("File {} extracted to \"{}\"", idx, file_path.display()));
69 fs::create_dir_all(&file_path)?;
72 if let Some(path) = file_path.parent() {
74 fs::create_dir_all(path)?;
77 let file_path = strip_cur_dir(file_path.as_path());
79 // same reason is in _is_dir: long, often not needed text
82 "{:?} extracted. ({})",
84 Bytes::new(file.size())
88 let mut output_file = fs::File::create(file_path)?;
89 io::copy(&mut file, &mut output_file)?;
91 set_last_modified_time(&file, file_path)?;
96 unix_set_permissions(&file_path, &file)?;
104 /// List contents of `archive`, returning a vector of archive entries
105 pub fn list_archive<R>(
106 mut archive: ZipArchive<R>,
107 password: Option<&[u8]>,
108 ) -> impl Iterator<Item = crate::Result<FileInArchive>>
110 R: Read + Seek + Send + 'static,
112 struct Files(mpsc::Receiver<crate::Result<FileInArchive>>);
113 impl Iterator for Files {
114 type Item = crate::Result<FileInArchive>;
116 fn next(&mut self) -> Option<Self::Item> {
121 let password = password.map(|p| p.to_owned());
123 let (tx, rx) = mpsc::channel();
124 thread::spawn(move || {
125 for idx in 0..archive.len() {
126 let file_in_archive = (|| {
127 let zip_result = match password.clone() {
128 Some(password) => archive
129 .by_index_decrypt(idx, &password)?
130 .map_err(|_| zip::result::ZipError::UnsupportedArchive("Password required to decrypt file")),
131 None => archive.by_index(idx),
134 let file = match zip_result {
136 Err(e) => return Err(e.into()),
139 let path = file.enclosed_name().unwrap_or(&*file.mangled_name()).to_owned();
140 let is_dir = file.is_dir();
142 Ok(FileInArchive { path, is_dir })
144 tx.send(file_in_archive).unwrap();
151 /// Compresses the archives given by `input_filenames` into the file given previously to `writer`.
152 pub fn build_archive_from_paths<W>(
153 input_filenames: &[PathBuf],
156 file_visibility_policy: FileVisibilityPolicy,
158 ) -> crate::Result<W>
162 let mut writer = zip::ZipWriter::new(writer);
163 // always use ZIP64 to allow compression of files larger than 4GB
164 // the format is widely supported and the extra 20B is negligible in most cases
165 let options = zip::write::FileOptions::default().large_file(true);
166 let output_handle = Handle::from_path(output_path);
169 let executable = options.unix_permissions(0o755);
171 // Vec of any filename that failed the UTF-8 check
172 let invalid_unicode_filenames = get_invalid_utf8_paths(input_filenames);
174 if !invalid_unicode_filenames.is_empty() {
175 let error = FinalError::with_title("Cannot build zip archive")
176 .detail("Zip archives require files to have valid UTF-8 paths")
178 "Files with invalid paths: {}",
179 pretty_format_list_of_paths(&invalid_unicode_filenames)
182 return Err(error.into());
185 for filename in input_filenames {
186 let previous_location = cd_into_same_dir_as(filename)?;
189 // paths should be canonicalized by now, and the root directory rejected.
190 let filename = filename.file_name().unwrap();
192 for entry in file_visibility_policy.build_walker(filename) {
194 let path = entry.path();
196 // If the output_path is the same as the input file, warn the user and skip the input (in order to avoid compression recursion)
197 if let Ok(handle) = &output_handle {
198 if matches!(Handle::from_path(path), Ok(x) if &x == handle) {
200 "Cannot compress `{}` into itself, skipping",
201 output_path.display()
206 // This is printed for every file in `input_filenames` and has
207 // little importance for most users, but would generate lots of
208 // spoken text for users using screen readers, braille displays
211 info(format!("Compressing '{}'", EscapedPathDisplay::new(path)));
214 let metadata = match path.metadata() {
215 Ok(metadata) => metadata,
217 if e.kind() == std::io::ErrorKind::NotFound && path.is_symlink() {
218 // This path is for a broken symlink, ignore it
221 return Err(e.into());
226 let options = options.unix_permissions(metadata.permissions().mode());
228 let entry_name = path.to_str().ok_or_else(|| {
229 FinalError::with_title("Zip requires that all directories names are valid UTF-8")
230 .detail(format!("File at '{path:?}' has a non-UTF-8 name"))
233 if metadata.is_dir() {
234 writer.add_directory(entry_name, options)?;
237 let options = if is_executable::is_executable(path) {
243 let mut file = fs::File::open(path)?;
245 // Updated last modified time
246 let last_modified_time = options.last_modified_time(get_last_modified_time(&file));
248 writer.start_file(entry_name, last_modified_time)?;
249 io::copy(&mut file, &mut writer)?;
253 env::set_current_dir(previous_location)?;
256 let bytes = writer.finish()?;
260 fn display_zip_comment_if_exists(file: &ZipFile) {
261 let comment = file.comment();
262 if !comment.is_empty() {
263 // Zip file comments seem to be pretty rare, but if they are used,
264 // they may contain important information, so better show them
266 // "The .ZIP file format allows for a comment containing up to 65,535 (216−1) bytes
267 // of data to occur at the end of the file after the central directory."
269 // If there happen to be cases of very long and unnecessary comments in
270 // the future, maybe asking the user if he wants to display the comment
271 // (informing him of its size) would be sensible for both normal and
272 // accessibility mode..
273 info_accessible(format!("Found comment in {}: {}", file.name(), comment));
277 fn get_last_modified_time(file: &fs::File) -> DateTime {
279 .and_then(|metadata| metadata.modified())
281 .and_then(|time| DateTime::try_from(OffsetDateTime::from(time)).ok())
285 fn set_last_modified_time(zip_file: &ZipFile, path: &Path) -> crate::Result<()> {
286 let modification_time = zip_file.last_modified().to_time();
288 let Ok(time_in_seconds) = modification_time else {
292 // Zip does not support nanoseconds, so we can assume zero here
293 let modification_time = FileTime::from_unix_time(time_in_seconds.unix_timestamp(), 0);
295 set_file_mtime(path, modification_time)?;
301 fn unix_set_permissions(file_path: &Path, file: &ZipFile) -> crate::Result<()> {
302 use std::fs::Permissions;
304 if let Some(mode) = file.unix_mode() {
305 fs::set_permissions(file_path, Permissions::from_mode(mode))?;