refac: simplify error treatment
[ouch.git] / src / archive / sevenz.rs
blob8898f5f0133ebbbdda6a6dc2e82bcb51e9b9d6f6
1 //! SevenZip archive format compress function
3 use std::{
4     env,
5     io::{self, Read, Seek, Write},
6     path::{Path, PathBuf},
7 };
9 use bstr::ByteSlice;
10 use fs_err as fs;
11 use same_file::Handle;
12 use sevenz_rust::SevenZArchiveEntry;
14 use crate::{
15     error::{Error, FinalError, Result},
16     list::FileInArchive,
17     utils::{
18         cd_into_same_dir_as,
19         logger::{info, warning},
20         Bytes, EscapedPathDisplay, FileVisibilityPolicy,
21     },
24 pub fn compress_sevenz<W>(
25     files: &[PathBuf],
26     output_path: &Path,
27     writer: W,
28     file_visibility_policy: FileVisibilityPolicy,
29     quiet: bool,
30 ) -> crate::Result<W>
31 where
32     W: Write + Seek,
34     let mut writer = sevenz_rust::SevenZWriter::new(writer)?;
35     let output_handle = Handle::from_path(output_path);
37     for filename in files {
38         let previous_location = cd_into_same_dir_as(filename)?;
40         // Unwrap safety:
41         //   paths should be canonicalized by now, and the root directory rejected.
42         let filename = filename.file_name().unwrap();
44         for entry in file_visibility_policy.build_walker(filename) {
45             let entry = entry?;
46             let path = entry.path();
48             // If the output_path is the same as the input file, warn the user and skip the input (in order to avoid compression recursion)
49             if let Ok(handle) = &output_handle {
50                 if matches!(Handle::from_path(path), Ok(x) if &x == handle) {
51                     warning(format!(
52                         "Cannot compress `{}` into itself, skipping",
53                         output_path.display()
54                     ));
56                     continue;
57                 }
58             }
60             // This is printed for every file in `input_filenames` and has
61             // little importance for most users, but would generate lots of
62             // spoken text for users using screen readers, braille displays
63             // and so on
64             if !quiet {
65                 info(format!("Compressing '{}'", EscapedPathDisplay::new(path)));
66             }
68             let metadata = match path.metadata() {
69                 Ok(metadata) => metadata,
70                 Err(e) => {
71                     if e.kind() == std::io::ErrorKind::NotFound && path.is_symlink() {
72                         // This path is for a broken symlink, ignore it
73                         continue;
74                     }
75                     return Err(e.into());
76                 }
77             };
79             let entry_name = path.to_str().ok_or_else(|| {
80                 FinalError::with_title("7z requires that all entry names are valid UTF-8")
81                     .detail(format!("File at '{path:?}' has a non-UTF-8 name"))
82             })?;
84             let entry = sevenz_rust::SevenZArchiveEntry::from_path(path, entry_name.to_owned());
85             let entry_data = if metadata.is_dir() {
86                 None
87             } else {
88                 Some(fs::File::open(path)?)
89             };
91             writer.push_archive_entry::<fs::File>(entry, entry_data)?;
92         }
94         env::set_current_dir(previous_location)?;
95     }
97     let bytes = writer.finish()?;
98     Ok(bytes)
101 pub fn decompress_sevenz<R>(reader: R, output_path: &Path, password: Option<&[u8]>, quiet: bool) -> crate::Result<usize>
102 where
103     R: Read + Seek,
105     let mut count: usize = 0;
107     let entry_extract_fn = |entry: &SevenZArchiveEntry, reader: &mut dyn Read, path: &PathBuf| {
108         count += 1;
109         // Manually handle writing all files from 7z archive, due to library exluding empty files
110         use std::io::BufWriter;
112         use filetime_creation as ft;
114         let file_path = output_path.join(entry.name());
116         if entry.is_directory() {
117             if !quiet {
118                 info(format!(
119                     "File {} extracted to \"{}\"",
120                     entry.name(),
121                     file_path.display()
122                 ));
123             }
124             if !path.exists() {
125                 fs::create_dir_all(path)?;
126             }
127         } else {
128             if !quiet {
129                 info(format!(
130                     "{:?} extracted. ({})",
131                     file_path.display(),
132                     Bytes::new(entry.size())
133                 ));
134             }
136             if let Some(parent) = path.parent() {
137                 if !parent.exists() {
138                     fs::create_dir_all(parent)?;
139                 }
140             }
142             let file = fs::File::create(path)?;
143             let mut writer = BufWriter::new(file);
144             io::copy(reader, &mut writer)?;
146             ft::set_file_handle_times(
147                 writer.get_ref().file(),
148                 Some(ft::FileTime::from_system_time(entry.access_date().into())),
149                 Some(ft::FileTime::from_system_time(entry.last_modified_date().into())),
150                 Some(ft::FileTime::from_system_time(entry.creation_date().into())),
151             )
152             .unwrap_or_default();
153         }
155         Ok(true)
156     };
158     match password {
159         Some(password) => sevenz_rust::decompress_with_extract_fn_and_password(
160             reader,
161             output_path,
162             sevenz_rust::Password::from(password.to_str().map_err(|err| Error::InvalidPassword {
163                 reason: err.to_string(),
164             })?),
165             entry_extract_fn,
166         )?,
167         None => sevenz_rust::decompress_with_extract_fn(reader, output_path, entry_extract_fn)?,
168     }
170     Ok(count)
173 /// List contents of `archive_path`, returning a vector of archive entries
174 pub fn list_archive(
175     archive_path: &Path,
176     password: Option<&[u8]>,
177 ) -> Result<impl Iterator<Item = crate::Result<FileInArchive>>> {
178     let reader = fs::File::open(archive_path)?;
180     let mut files = Vec::new();
182     let entry_extract_fn = |entry: &SevenZArchiveEntry, _: &mut dyn Read, _: &PathBuf| {
183         files.push(Ok(FileInArchive {
184             path: entry.name().into(),
185             is_dir: entry.is_directory(),
186         }));
187         Ok(true)
188     };
190     match password {
191         Some(password) => {
192             let password = match password.to_str() {
193                 Ok(p) => p,
194                 Err(err) => {
195                     return Err(Error::InvalidPassword {
196                         reason: err.to_string(),
197                     })
198                 }
199             };
200             sevenz_rust::decompress_with_extract_fn_and_password(
201                 reader,
202                 ".",
203                 sevenz_rust::Password::from(password),
204                 entry_extract_fn,
205             )?;
206         }
207         None => sevenz_rust::decompress_with_extract_fn(reader, ".", entry_extract_fn)?,
208     }
210     Ok(files.into_iter())