Improve error message for Zip invalid encoding errors
[ouch.git] / src / archive / zip.rs
blobd4a6de840d48c27d8a4ff4322f40217ae3c662e1
1 //! Contains Zip-specific building and unpacking functions
3 use std::{
4     env,
5     io::{self, prelude::*},
6     path::{Path, PathBuf},
7 };
9 use fs_err as fs;
10 use walkdir::WalkDir;
11 use zip::{self, read::ZipFile, ZipArchive};
13 use self::utf8::get_invalid_utf8_paths;
14 use crate::{
15     error::FinalError,
16     info,
17     list::FileInArchive,
18     utils::{
19         cd_into_same_dir_as, concatenate_os_str_list, dir_is_empty, strip_cur_dir, to_utf, user_wants_to_overwrite,
20         Bytes,
21     },
22     QuestionPolicy,
25 /// Unpacks the archive given by `archive` into the folder given by `into`.
26 pub fn unpack_archive<R>(
27     mut archive: ZipArchive<R>,
28     into: &Path,
29     question_policy: QuestionPolicy,
30 ) -> crate::Result<Vec<PathBuf>>
31 where
32     R: Read + Seek,
34     let mut unpacked_files = vec![];
35     for idx in 0..archive.len() {
36         let mut file = archive.by_index(idx)?;
37         let file_path = match file.enclosed_name() {
38             Some(path) => path.to_owned(),
39             None => continue,
40         };
42         let file_path = into.join(file_path);
43         if file_path.exists() && !user_wants_to_overwrite(&file_path, question_policy)? {
44             continue;
45         }
47         if file_path.is_dir() {
48             // ToDo: Maybe we should emphasise that `file_path` is a directory and everything inside it will be gone?
49             fs::remove_dir_all(&file_path)?;
50         } else if file_path.is_file() {
51             fs::remove_file(&file_path)?;
52         }
54         check_for_comments(&file);
56         match (&*file.name()).ends_with('/') {
57             _is_dir @ true => {
58                 println!("File {} extracted to \"{}\"", idx, file_path.display());
59                 fs::create_dir_all(&file_path)?;
60             }
61             _is_file @ false => {
62                 if let Some(path) = file_path.parent() {
63                     if !path.exists() {
64                         fs::create_dir_all(&path)?;
65                     }
66                 }
67                 let file_path = strip_cur_dir(file_path.as_path());
69                 info!("{:?} extracted. ({})", file_path.display(), Bytes::new(file.size()));
71                 let mut output_file = fs::File::create(&file_path)?;
72                 io::copy(&mut file, &mut output_file)?;
73             }
74         }
76         #[cfg(unix)]
77         __unix_set_permissions(&file_path, &file)?;
79         let file_path = fs::canonicalize(&file_path)?;
80         unpacked_files.push(file_path);
81     }
83     Ok(unpacked_files)
86 /// List contents of `archive`, returning a vector of archive entries
87 pub fn list_archive<R>(mut archive: ZipArchive<R>) -> crate::Result<Vec<FileInArchive>>
88 where
89     R: Read + Seek,
91     let mut files = vec![];
92     for idx in 0..archive.len() {
93         let file = archive.by_index(idx)?;
95         let path = match file.enclosed_name() {
96             Some(path) => path.to_owned(),
97             None => continue,
98         };
99         let is_dir = file.is_dir();
101         files.push(FileInArchive { path, is_dir });
102     }
103     Ok(files)
106 /// Compresses the archives given by `input_filenames` into the file given previously to `writer`.
107 pub fn build_archive_from_paths<W>(input_filenames: &[PathBuf], writer: W) -> crate::Result<W>
108 where
109     W: Write + Seek,
111     let mut writer = zip::ZipWriter::new(writer);
112     let options = zip::write::FileOptions::default();
114     // Vec of any filename that failed the UTF-8 check
115     let invalid_unicode_filenames = get_invalid_utf8_paths(input_filenames);
117     if !invalid_unicode_filenames.is_empty() {
118         let error = FinalError::with_title("Cannot build zip archive")
119             .detail("Zip archives require files to have valid UTF-8 paths")
120             .detail(format!("Files with invalid paths: {}", concatenate_os_str_list(&invalid_unicode_filenames)));
122         return Err(error.into());
123     }
125     for filename in input_filenames {
126         let previous_location = cd_into_same_dir_as(filename)?;
128         // Safe unwrap, input shall be treated before
129         let filename = filename.file_name().unwrap();
131         for entry in WalkDir::new(filename) {
132             let entry = entry?;
133             let path = entry.path();
135             info!("Compressing '{}'.", to_utf(path));
137             if path.is_dir() {
138                 if dir_is_empty(path) {
139                     writer.add_directory(path.to_str().unwrap().to_owned(), options)?;
140                 }
141                 // If a dir has files, the files are responsible for creating them.
142             } else {
143                 writer.start_file(path.to_str().unwrap().to_owned(), options)?;
144                 let file_bytes = fs::read(entry.path())?;
145                 writer.write_all(&*file_bytes)?;
146             }
147         }
149         env::set_current_dir(previous_location)?;
150     }
152     let bytes = writer.finish()?;
153     Ok(bytes)
156 fn check_for_comments(file: &ZipFile) {
157     let comment = file.comment();
158     if !comment.is_empty() {
159         info!("Found comment in {}: {}", file.name(), comment);
160     }
163 #[cfg(unix)]
164 fn __unix_set_permissions(file_path: &Path, file: &ZipFile) -> crate::Result<()> {
165     use std::{fs::Permissions, os::unix::fs::PermissionsExt};
167     if let Some(mode) = file.unix_mode() {
168         fs::set_permissions(file_path, Permissions::from_mode(mode))?;
169     }
171     Ok(())
174 mod utf8 {
175     use std::path::{Path, PathBuf};
177     fn is_invalid_utf8(path: &Path) -> bool {
178         #[cfg(unix)]
179         {
180             use std::{os::unix::prelude::OsStrExt, str};
182             let bytes = path.as_os_str().as_bytes();
183             str::from_utf8(bytes).is_err()
184         }
185         #[cfg(not(unix))]
186         {
187             path.to_str().is_none()
188         }
189     }
191     pub fn get_invalid_utf8_paths(paths: &[PathBuf]) -> Vec<PathBuf> {
192         paths.iter().filter_map(|path| is_invalid_utf8(&path).then(|| path.clone())).collect()
193     }