this repo has no description
1use super::containers::total_input_bytes;
2use crate::progress::{OutputTarget, ProgressArgs, ProgressReader, create_progress_bar};
3use crate::utils::{
4 CmprssInput, CmprssOutput, CommonArgs, CompressionLevelValidator, Compressor,
5 DefaultCompressionValidator, ExtractedTarget, LevelArgs, Result,
6};
7use anyhow::{anyhow, bail};
8use clap::Args;
9use indicatif::ProgressBar;
10use sevenz_rust2::{
11 ArchiveEntry, ArchiveReader, ArchiveWriter, Password, decompress, encoder_options::Lzma2Options,
12};
13use std::fs::File;
14use std::io::{self, Empty, Seek, SeekFrom, Write};
15use std::path::Path;
16use tempfile::tempfile;
17
18#[derive(Args, Debug)]
19pub struct SevenZArgs {
20 #[clap(flatten)]
21 pub common_args: CommonArgs,
22
23 #[clap(flatten)]
24 pub level_args: LevelArgs,
25
26 #[clap(flatten)]
27 pub progress_args: ProgressArgs,
28}
29
30#[derive(Clone)]
31pub struct SevenZ {
32 pub compression_level: i32,
33 pub progress_args: ProgressArgs,
34}
35
36impl Default for SevenZ {
37 fn default() -> Self {
38 SevenZ {
39 compression_level: DefaultCompressionValidator.default_level(),
40 progress_args: ProgressArgs::default(),
41 }
42 }
43}
44
45impl SevenZ {
46 pub fn new(args: &SevenZArgs) -> SevenZ {
47 SevenZ {
48 compression_level: args.level_args.resolve(&DefaultCompressionValidator),
49 progress_args: args.progress_args,
50 }
51 }
52
53 /// Extract a seekable 7z input with a byte-level progress bar keyed to
54 /// the compressed archive size.
55 fn decompress_seekable<R: io::Read + Seek>(
56 &self,
57 reader: R,
58 size: u64,
59 out_dir: &Path,
60 ) -> Result {
61 let bar = create_progress_bar(Some(size), self.progress_args.progress, OutputTarget::File);
62 let reader = ProgressReader::new(reader, bar.clone());
63 decompress(reader, out_dir)?;
64 if let Some(b) = bar {
65 b.finish();
66 }
67 Ok(())
68 }
69
70 /// Compress to the given seekable writer, walking path inputs ourselves
71 /// so each file's read goes through `ProgressReader` sharing `bar`.
72 fn compress_to_file<W: Write + Seek>(
73 &self,
74 input: CmprssInput,
75 writer: W,
76 bar: Option<&ProgressBar>,
77 ) -> Result {
78 let mut aw = ArchiveWriter::new(writer)?;
79 let lzma = Lzma2Options::from_level(self.compression_level as u32);
80 aw.set_content_methods(vec![lzma.into()]);
81
82 match input {
83 CmprssInput::Path(paths) => {
84 for path in paths {
85 let name = path
86 .file_name()
87 .ok_or_else(|| anyhow!("input path has no file name: {:?}", path))?
88 .to_string_lossy()
89 .to_string();
90 if path.is_file() {
91 push_file_entry(&mut aw, &name, &path, bar)?;
92 } else if path.is_dir() {
93 push_dir_entries(&mut aw, &name, &path, bar)?;
94 } else {
95 bail!("7z does not support this file type");
96 }
97 }
98 }
99 CmprssInput::Pipe(pipe) => {
100 let entry = ArchiveEntry::new_file("archive");
101 aw.push_archive_entry(entry, Some(pipe))?;
102 }
103 CmprssInput::Reader(_) => {
104 bail!("7z does not accept an in-memory reader input");
105 }
106 }
107
108 aw.finish()?;
109 Ok(())
110 }
111}
112
113impl Compressor for SevenZ {
114 fn name(&self) -> &str {
115 "7z"
116 }
117
118 fn default_extracted_target(&self) -> ExtractedTarget {
119 ExtractedTarget::Directory
120 }
121
122 fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result {
123 match output {
124 CmprssOutput::Path(ref path) => {
125 let total = match &input {
126 CmprssInput::Path(paths) => Some(total_input_bytes(paths)),
127 _ => None,
128 };
129 let bar =
130 create_progress_bar(total, self.progress_args.progress, OutputTarget::File);
131 let file = File::create(path)?;
132 self.compress_to_file(input, file, bar.as_ref())?;
133 if let Some(b) = bar {
134 b.finish();
135 }
136 Ok(())
137 }
138 CmprssOutput::Pipe(mut pipe) => {
139 let mut temp_file = tempfile()?;
140 self.compress_to_file(input, &mut temp_file, None)?;
141 temp_file.seek(SeekFrom::Start(0))?;
142 io::copy(&mut temp_file, &mut pipe)?;
143 Ok(())
144 }
145 CmprssOutput::Writer(mut writer) => {
146 let mut temp_file = tempfile()?;
147 self.compress_to_file(input, &mut temp_file, None)?;
148 temp_file.seek(SeekFrom::Start(0))?;
149 io::copy(&mut temp_file, &mut writer)?;
150 Ok(())
151 }
152 }
153 }
154
155 fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result {
156 match output {
157 CmprssOutput::Path(ref out_dir) => {
158 if !out_dir.exists() {
159 std::fs::create_dir_all(out_dir)?;
160 } else if !out_dir.is_dir() {
161 bail!("7z extraction output must be a directory");
162 }
163
164 match input {
165 CmprssInput::Path(paths) => {
166 if paths.len() != 1 {
167 bail!("7z extraction expects exactly one archive file");
168 }
169 let file = File::open(&paths[0])?;
170 let size = file.metadata()?.len();
171 self.decompress_seekable(file, size, out_dir)
172 }
173 CmprssInput::Pipe(mut pipe) => {
174 let mut temp_file = tempfile()?;
175 io::copy(&mut pipe, &mut temp_file)?;
176 temp_file.seek(SeekFrom::Start(0))?;
177 let size = temp_file.metadata()?.len();
178 self.decompress_seekable(temp_file, size, out_dir)
179 }
180 CmprssInput::Reader(_) => {
181 bail!(
182 "7z extraction does not accept an in-memory reader input (requires seekable input)"
183 )
184 }
185 }
186 }
187 CmprssOutput::Pipe(_) => bail!("7z extraction to stdout is not supported"),
188 CmprssOutput::Writer(mut writer) => match input {
189 CmprssInput::Path(paths) => {
190 if paths.len() != 1 {
191 bail!("7z extraction expects exactly one archive file");
192 }
193 let mut file = File::open(&paths[0])?;
194 io::copy(&mut file, &mut writer)?;
195 Ok(())
196 }
197 CmprssInput::Pipe(mut pipe) => {
198 io::copy(&mut pipe, &mut writer)?;
199 Ok(())
200 }
201 CmprssInput::Reader(mut reader) => {
202 io::copy(&mut reader, &mut writer)?;
203 Ok(())
204 }
205 },
206 }
207 }
208
209 fn list(&self, input: CmprssInput) -> Result {
210 let stdout = io::stdout();
211 let mut out = stdout.lock();
212 match input {
213 CmprssInput::Path(paths) => {
214 if paths.len() != 1 {
215 bail!("7z listing expects exactly one archive file");
216 }
217 let reader = ArchiveReader::open(&paths[0], Password::empty())?;
218 for entry in &reader.archive().files {
219 writeln!(out, "{}", entry.name())?;
220 }
221 }
222 CmprssInput::Pipe(mut pipe) => {
223 let mut temp = tempfile()?;
224 io::copy(&mut pipe, &mut temp)?;
225 temp.seek(SeekFrom::Start(0))?;
226 let reader = ArchiveReader::new(temp, Password::empty())?;
227 for entry in &reader.archive().files {
228 writeln!(out, "{}", entry.name())?;
229 }
230 }
231 CmprssInput::Reader(mut reader) => {
232 let mut temp = tempfile()?;
233 io::copy(&mut reader, &mut temp)?;
234 temp.seek(SeekFrom::Start(0))?;
235 let ar = ArchiveReader::new(temp, Password::empty())?;
236 for entry in &ar.archive().files {
237 writeln!(out, "{}", entry.name())?;
238 }
239 }
240 }
241 Ok(())
242 }
243}
244
245/// Push a single regular file as an archive entry, with reads flowing
246/// through `ProgressReader` so they tick the shared bar.
247fn push_file_entry<W: Write + Seek>(
248 aw: &mut ArchiveWriter<W>,
249 archive_name: &str,
250 disk_path: &Path,
251 bar: Option<&ProgressBar>,
252) -> Result {
253 let entry = ArchiveEntry::from_path(disk_path, archive_name.to_string());
254 let file = File::open(disk_path)?;
255 let reader = ProgressReader::new(file, bar.cloned());
256 aw.push_archive_entry(entry, Some(reader))?;
257 Ok(())
258}
259
260/// Push a directory entry, then recurse into its children. Mirrors the
261/// layout that `push_source_path` would produce (entries named
262/// `<dir>/<child>`), but gives us a read hook for each file.
263fn push_dir_entries<W: Write + Seek>(
264 aw: &mut ArchiveWriter<W>,
265 archive_name: &str,
266 disk_path: &Path,
267 bar: Option<&ProgressBar>,
268) -> Result {
269 let entry = ArchiveEntry::from_path(disk_path, archive_name.to_string());
270 aw.push_archive_entry::<Empty>(entry, None)?;
271 for child in std::fs::read_dir(disk_path)? {
272 let child = child?;
273 let child_path = child.path();
274 let child_name = format!("{}/{}", archive_name, child.file_name().to_string_lossy());
275 if child_path.is_file() {
276 push_file_entry(aw, &child_name, &child_path, bar)?;
277 } else if child_path.is_dir() {
278 push_dir_entries(aw, &child_name, &child_path, bar)?;
279 }
280 // Skip symlinks/other types — parity with prior behavior.
281 }
282 Ok(())
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288 use crate::test_utils::*;
289 use assert_fs::prelude::*;
290 use predicates::prelude::*;
291 use std::path::PathBuf;
292
293 #[test]
294 fn test_sevenz_interface() {
295 let compressor = SevenZ::default();
296 test_compressor_interface(&compressor, "7z", Some("7z"));
297 }
298
299 #[test]
300 fn test_sevenz_default_compression() -> Result {
301 let compressor = SevenZ::default();
302 test_compression(&compressor)
303 }
304
305 #[test]
306 fn test_sevenz_fast_compression() -> Result {
307 let fast_compressor = SevenZ {
308 compression_level: 1,
309 progress_args: ProgressArgs::default(),
310 };
311 test_compression(&fast_compressor)
312 }
313
314 #[test]
315 fn test_sevenz_best_compression() -> Result {
316 let best_compressor = SevenZ {
317 compression_level: 9,
318 progress_args: ProgressArgs::default(),
319 };
320 test_compression(&best_compressor)
321 }
322
323 #[test]
324 fn test_directory_handling() -> Result {
325 let compressor = SevenZ::default();
326 let dir = assert_fs::TempDir::new()?;
327 let file_path = dir.child("file.txt");
328 file_path.write_str("directory test data")?;
329 let working_dir = assert_fs::TempDir::new()?;
330 let archive = working_dir.child("dir_archive.7z");
331 archive.assert(predicate::path::missing());
332
333 compressor.compress(
334 CmprssInput::Path(vec![dir.path().to_path_buf()]),
335 CmprssOutput::Path(archive.path().to_path_buf()),
336 )?;
337 archive.assert(predicate::path::is_file());
338
339 let extract_dir = working_dir.child("extracted");
340 std::fs::create_dir_all(extract_dir.path())?;
341 compressor.extract(
342 CmprssInput::Path(vec![archive.path().to_path_buf()]),
343 CmprssOutput::Path(extract_dir.path().to_path_buf()),
344 )?;
345 let dir_name: PathBuf = dir.path().file_name().unwrap().into();
346 extract_dir
347 .child(dir_name)
348 .child("file.txt")
349 .assert(predicate::path::eq_file(file_path.path()));
350 Ok(())
351 }
352}