this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(backends): add 7zip backend with sevenz-rust2

+489 -1
+38
Cargo.lock
··· 151 151 ] 152 152 153 153 [[package]] 154 + name = "block-padding" 155 + version = "0.3.3" 156 + source = "registry+https://github.com/rust-lang/crates.io-index" 157 + checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" 158 + dependencies = [ 159 + "generic-array", 160 + ] 161 + 162 + [[package]] 154 163 name = "brotli" 155 164 version = "8.0.2" 156 165 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 206 215 dependencies = [ 207 216 "cc", 208 217 "pkg-config", 218 + ] 219 + 220 + [[package]] 221 + name = "cbc" 222 + version = "0.1.2" 223 + source = "registry+https://github.com/rust-lang/crates.io-index" 224 + checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" 225 + dependencies = [ 226 + "cipher", 209 227 ] 210 228 211 229 [[package]] ··· 324 342 "lz4_flex", 325 343 "predicates", 326 344 "rand", 345 + "sevenz-rust2", 327 346 "snap", 328 347 "tar", 329 348 "tempfile", ··· 678 697 source = "registry+https://github.com/rust-lang/crates.io-index" 679 698 checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" 680 699 dependencies = [ 700 + "block-padding", 681 701 "generic-array", 682 702 ] 683 703 ··· 1089 1109 "serde", 1090 1110 "serde_core", 1091 1111 "zmij", 1112 + ] 1113 + 1114 + [[package]] 1115 + name = "sevenz-rust2" 1116 + version = "0.20.2" 1117 + source = "registry+https://github.com/rust-lang/crates.io-index" 1118 + checksum = "29225600349ef74beda5a9fffb36ac660a24613c0bde9315d0c49be1d51e9c24" 1119 + dependencies = [ 1120 + "aes", 1121 + "bzip2", 1122 + "cbc", 1123 + "crc32fast", 1124 + "getrandom 0.4.2", 1125 + "js-sys", 1126 + "lzma-rust2", 1127 + "ppmd-rust", 1128 + "sha2", 1129 + "wasm-bindgen", 1092 1130 ] 1093 1131 1094 1132 [[package]]
+1
Cargo.toml
··· 21 21 indicatif = "0.18" 22 22 is-terminal = "0.4" 23 23 lz4_flex = "0.13" 24 + sevenz-rust2 = "0.20" 24 25 snap = "1" 25 26 tar = "0.4" 26 27 tempfile = "3"
+22 -1
bin/test.sh
··· 350 350 echo "No errors detected" 351 351 } 352 352 353 + # Test 7z archive interop with the 7z CLI (from p7zip). 7z has no --progress 354 + # flag, so it isn't passed here. -bso0/-bsp0 silence 7z's output/progress noise. 355 + test_7z() { 356 + tmpdir 357 + echo "Testing 7z in $PWD" 358 + echo "Creating random data" 359 + random_dir 10 indir 360 + echo "Creating 7z archives with each tool" 361 + 7z a -bso0 -bsp0 sevenz_archive.7z indir 362 + cmprss 7z indir cmprss_archive.7z 363 + echo "Extracting each archive with the opposite tool" 364 + mkdir -p sevenz_from_cmprss 365 + 7z x -bso0 -bsp0 "-osevenz_from_cmprss" cmprss_archive.7z 366 + mkdir -p cmprss_from_sevenz 367 + cmprss 7z --extract sevenz_archive.7z cmprss_from_sevenz 368 + echo "Comparing the extracted contents" 369 + compare indir sevenz_from_cmprss/indir 370 + compare indir cmprss_from_sevenz/indir 371 + echo "No errors detected" 372 + } 373 + 353 374 # Shared helper for tar.<codec> pipeline interop. The first arg is the compound 354 375 # extension; the rest are the tar flags used to compress/extract that codec 355 376 # (e.g. `-z`, `--zstd`, or `-I lzma` for codecs without a short flag). ··· 390 411 391 412 # Run all the tests if no arguments are given 392 413 if [ $# -eq 0 ]; then 393 - set -- gzip xz bzip2 zstd lz4 lzma brotli snappy tar zip \ 414 + set -- gzip xz bzip2 zstd lz4 lzma brotli snappy tar zip 7z \ 394 415 tar_gz tar_xz tar_bz2 tar_zst tar_lzma tar_br tar_lz4 tar_sz 395 416 fi 396 417
+1
flake.nix
··· 288 288 gnutar 289 289 gzip 290 290 lz4 291 + p7zip 291 292 snzip 292 293 unzip 293 294 xz
+3
src/backends/mod.rs
··· 4 4 mod lz4; 5 5 mod lzma; 6 6 mod pipeline; 7 + mod sevenz; 7 8 mod snappy; 8 9 mod stream; 9 10 mod tar; ··· 17 18 pub use lz4::{Lz4, Lz4Args}; 18 19 pub use lzma::{Lzma, LzmaArgs}; 19 20 pub use pipeline::Pipeline; 21 + pub use sevenz::{SevenZ, SevenZArgs}; 20 22 pub use snappy::{Snappy, SnappyArgs}; 21 23 pub use tar::{Tar, TarArgs}; 22 24 pub use xz::{Xz, XzArgs}; ··· 39 41 "brotli" | "br" => Some(Box::<Brotli>::default()), 40 42 "snappy" | "sz" => Some(Box::<Snappy>::default()), 41 43 "lzma" => Some(Box::<Lzma>::default()), 44 + "7z" | "sevenz" => Some(Box::<SevenZ>::default()), 42 45 _ => None, 43 46 } 44 47 }
+249
src/backends/sevenz.rs
··· 1 + use crate::utils::{CmprssInput, CmprssOutput, CommonArgs, Compressor, ExtractedTarget, Result}; 2 + use anyhow::bail; 3 + use clap::Args; 4 + use sevenz_rust2::{ 5 + ArchiveEntry, ArchiveReader, ArchiveWriter, Password, decompress, decompress_file, 6 + }; 7 + use std::fs::File; 8 + use std::io::{self, Seek, SeekFrom, Write}; 9 + use std::path::{Path, PathBuf}; 10 + use tempfile::tempfile; 11 + 12 + #[derive(Args, Debug)] 13 + pub struct SevenZArgs { 14 + #[clap(flatten)] 15 + pub common_args: CommonArgs, 16 + } 17 + 18 + #[derive(Default, Clone)] 19 + pub struct SevenZ {} 20 + 21 + impl SevenZ { 22 + pub fn new(_args: &SevenZArgs) -> SevenZ { 23 + SevenZ {} 24 + } 25 + 26 + fn compress_to_file<W: Write + Seek>(&self, input: CmprssInput, writer: W) -> Result { 27 + let mut aw = ArchiveWriter::new(writer)?; 28 + 29 + match input { 30 + CmprssInput::Path(paths) => { 31 + for path in paths { 32 + if path.is_file() { 33 + aw.push_source_path(&path, |_| true)?; 34 + } else if path.is_dir() { 35 + add_directory_entries(&mut aw, &path)?; 36 + } else { 37 + bail!("7z does not support this file type"); 38 + } 39 + } 40 + } 41 + CmprssInput::Pipe(pipe) => { 42 + let entry = ArchiveEntry::new_file("archive"); 43 + aw.push_archive_entry(entry, Some(pipe))?; 44 + } 45 + CmprssInput::Reader(_) => { 46 + bail!("7z does not accept an in-memory reader input"); 47 + } 48 + } 49 + 50 + aw.finish()?; 51 + Ok(()) 52 + } 53 + } 54 + 55 + impl Compressor for SevenZ { 56 + fn name(&self) -> &str { 57 + "7z" 58 + } 59 + 60 + fn clone_boxed(&self) -> Box<dyn Compressor> { 61 + Box::new(self.clone()) 62 + } 63 + 64 + fn default_extracted_target(&self) -> ExtractedTarget { 65 + ExtractedTarget::Directory 66 + } 67 + 68 + fn compress(&self, input: CmprssInput, output: CmprssOutput) -> Result { 69 + match output { 70 + CmprssOutput::Path(ref path) => { 71 + let file = File::create(path)?; 72 + self.compress_to_file(input, file) 73 + } 74 + CmprssOutput::Pipe(mut pipe) => { 75 + let mut temp_file = tempfile()?; 76 + self.compress_to_file(input, &mut temp_file)?; 77 + temp_file.seek(SeekFrom::Start(0))?; 78 + io::copy(&mut temp_file, &mut pipe)?; 79 + Ok(()) 80 + } 81 + CmprssOutput::Writer(mut writer) => { 82 + let mut temp_file = tempfile()?; 83 + self.compress_to_file(input, &mut temp_file)?; 84 + temp_file.seek(SeekFrom::Start(0))?; 85 + io::copy(&mut temp_file, &mut writer)?; 86 + Ok(()) 87 + } 88 + } 89 + } 90 + 91 + fn extract(&self, input: CmprssInput, output: CmprssOutput) -> Result { 92 + match output { 93 + CmprssOutput::Path(ref out_dir) => { 94 + if !out_dir.exists() { 95 + std::fs::create_dir_all(out_dir)?; 96 + } else if !out_dir.is_dir() { 97 + bail!("7z extraction output must be a directory"); 98 + } 99 + 100 + match input { 101 + CmprssInput::Path(paths) => { 102 + if paths.len() != 1 { 103 + bail!("7z extraction expects exactly one archive file"); 104 + } 105 + decompress_file(&paths[0], out_dir)?; 106 + Ok(()) 107 + } 108 + CmprssInput::Pipe(mut pipe) => { 109 + let mut temp_file = tempfile()?; 110 + io::copy(&mut pipe, &mut temp_file)?; 111 + temp_file.seek(SeekFrom::Start(0))?; 112 + decompress(temp_file, out_dir)?; 113 + Ok(()) 114 + } 115 + CmprssInput::Reader(_) => { 116 + bail!( 117 + "7z extraction does not accept an in-memory reader input (requires seekable input)" 118 + ) 119 + } 120 + } 121 + } 122 + CmprssOutput::Pipe(_) => bail!("7z extraction to stdout is not supported"), 123 + CmprssOutput::Writer(mut writer) => match input { 124 + CmprssInput::Path(paths) => { 125 + if paths.len() != 1 { 126 + bail!("7z extraction expects exactly one archive file"); 127 + } 128 + let mut file = File::open(&paths[0])?; 129 + io::copy(&mut file, &mut writer)?; 130 + Ok(()) 131 + } 132 + CmprssInput::Pipe(mut pipe) => { 133 + io::copy(&mut pipe, &mut writer)?; 134 + Ok(()) 135 + } 136 + CmprssInput::Reader(mut reader) => { 137 + io::copy(&mut reader, &mut writer)?; 138 + Ok(()) 139 + } 140 + }, 141 + } 142 + } 143 + 144 + fn list(&self, input: CmprssInput) -> Result { 145 + let stdout = io::stdout(); 146 + let mut out = stdout.lock(); 147 + match input { 148 + CmprssInput::Path(paths) => { 149 + if paths.len() != 1 { 150 + bail!("7z listing expects exactly one archive file"); 151 + } 152 + let reader = ArchiveReader::open(&paths[0], Password::empty())?; 153 + for entry in &reader.archive().files { 154 + writeln!(out, "{}", entry.name())?; 155 + } 156 + } 157 + CmprssInput::Pipe(mut pipe) => { 158 + let mut temp = tempfile()?; 159 + io::copy(&mut pipe, &mut temp)?; 160 + temp.seek(SeekFrom::Start(0))?; 161 + let reader = ArchiveReader::new(temp, Password::empty())?; 162 + for entry in &reader.archive().files { 163 + writeln!(out, "{}", entry.name())?; 164 + } 165 + } 166 + CmprssInput::Reader(mut reader) => { 167 + let mut temp = tempfile()?; 168 + io::copy(&mut reader, &mut temp)?; 169 + temp.seek(SeekFrom::Start(0))?; 170 + let ar = ArchiveReader::new(temp, Password::empty())?; 171 + for entry in &ar.archive().files { 172 + writeln!(out, "{}", entry.name())?; 173 + } 174 + } 175 + } 176 + Ok(()) 177 + } 178 + } 179 + 180 + /// Archive the contents of `dir` under the directory's basename. 181 + /// 182 + /// `push_source_path` strips the src prefix from each entry name, so to keep 183 + /// the directory itself as a prefix in the archive (e.g. `indir/file.txt` 184 + /// instead of `file.txt`), we pass the *parent* as the src and filter to just 185 + /// `dir`'s subtree. 186 + fn add_directory_entries<W: Write + Seek>(aw: &mut ArchiveWriter<W>, dir: &Path) -> Result { 187 + let abs_dir = std::path::absolute(dir).unwrap_or_else(|_| dir.to_path_buf()); 188 + let base: PathBuf = abs_dir 189 + .parent() 190 + .map(Path::to_path_buf) 191 + .unwrap_or_else(|| abs_dir.clone()); 192 + let base_filter = base.clone(); 193 + let target = abs_dir.clone(); 194 + aw.push_source_path(&base, move |p| { 195 + p == base_filter.as_path() || p.starts_with(&target) 196 + })?; 197 + Ok(()) 198 + } 199 + 200 + #[cfg(test)] 201 + mod tests { 202 + use super::*; 203 + use crate::test_utils::*; 204 + use assert_fs::prelude::*; 205 + use predicates::prelude::*; 206 + use std::path::PathBuf; 207 + 208 + #[test] 209 + fn test_sevenz_interface() { 210 + let compressor = SevenZ::default(); 211 + test_compressor_interface(&compressor, "7z", Some("7z")); 212 + } 213 + 214 + #[test] 215 + fn test_sevenz_default_compression() -> Result { 216 + let compressor = SevenZ::default(); 217 + test_compression(&compressor) 218 + } 219 + 220 + #[test] 221 + fn test_directory_handling() -> Result { 222 + let compressor = SevenZ::default(); 223 + let dir = assert_fs::TempDir::new()?; 224 + let file_path = dir.child("file.txt"); 225 + file_path.write_str("directory test data")?; 226 + let working_dir = assert_fs::TempDir::new()?; 227 + let archive = working_dir.child("dir_archive.7z"); 228 + archive.assert(predicate::path::missing()); 229 + 230 + compressor.compress( 231 + CmprssInput::Path(vec![dir.path().to_path_buf()]), 232 + CmprssOutput::Path(archive.path().to_path_buf()), 233 + )?; 234 + archive.assert(predicate::path::is_file()); 235 + 236 + let extract_dir = working_dir.child("extracted"); 237 + std::fs::create_dir_all(extract_dir.path())?; 238 + compressor.extract( 239 + CmprssInput::Path(vec![archive.path().to_path_buf()]), 240 + CmprssOutput::Path(extract_dir.path().to_path_buf()), 241 + )?; 242 + let dir_name: PathBuf = dir.path().file_name().unwrap().into(); 243 + extract_dir 244 + .child(dir_name) 245 + .child("file.txt") 246 + .assert(predicate::path::eq_file(file_path.path())); 247 + Ok(()) 248 + } 249 + }
+5
src/main.rs
··· 61 61 /// lzma (legacy LZMA1) compression 62 62 Lzma(LzmaArgs), 63 63 64 + /// 7-Zip archive format 65 + #[clap(name = "7z", visible_alias = "sevenz")] 66 + SevenZ(SevenZArgs), 67 + 64 68 /// Print a shell completion script to stdout. 65 69 #[clap(hide = true)] 66 70 Completions { ··· 108 112 Some(Format::Brotli(a)) => command(Some(Box::new(Brotli::new(&a))), &a.common_args), 109 113 Some(Format::Snappy(a)) => command(Some(Box::new(Snappy::new(&a))), &a.common_args), 110 114 Some(Format::Lzma(a)) => command(Some(Box::new(Lzma::new(&a))), &a.common_args), 115 + Some(Format::SevenZ(a)) => command(Some(Box::new(SevenZ::new(&a))), &a.common_args), 111 116 Some(Format::Completions { shell }) => write_completions(shell), 112 117 Some(Format::Manpage) => write_manpage(), 113 118 None => command(None, &args.base_args),
+170
tests/sevenz.rs
··· 1 + use assert_cmd::prelude::*; 2 + use assert_fs::prelude::*; 3 + use predicates::prelude::*; 4 + use std::{path::PathBuf, process::Command}; 5 + 6 + mod common; 7 + use common::*; 8 + 9 + mod sevenz { 10 + use super::*; 11 + 12 + mod roundtrip { 13 + use super::*; 14 + 15 + /// 7z roundtrip with a single file 16 + /// 17 + /// ``` bash 18 + /// cmprss 7z test.txt archive.7z 19 + /// cmprss 7z --extract archive.7z . 20 + /// ``` 21 + #[test] 22 + fn explicit() -> Result<(), Box<dyn std::error::Error>> { 23 + let file = create_test_file("test.txt", "garbage data for testing")?; 24 + let working_dir = create_working_dir()?; 25 + let archive = working_dir.child("archive.7z"); 26 + archive.assert(predicate::path::missing()); 27 + 28 + let mut compress = Command::cargo_bin("cmprss")?; 29 + compress.arg("7z").arg(file.path()).arg(archive.path()); 30 + compress.assert().success(); 31 + archive.assert(predicate::path::is_file()); 32 + 33 + let mut extract = Command::cargo_bin("cmprss")?; 34 + extract 35 + .arg("7z") 36 + .arg("--extract") 37 + .arg(archive.path()) 38 + .arg(working_dir.path()); 39 + extract.assert().success(); 40 + 41 + assert_files_equal(file.path(), &working_dir.child("test.txt")); 42 + 43 + Ok(()) 44 + } 45 + 46 + /// 7z roundtrip with multiple files 47 + /// 48 + /// ``` bash 49 + /// cmprss 7z test.txt test2.txt archive.7z 50 + /// cmprss 7z --extract archive.7z . 51 + /// ``` 52 + #[test] 53 + fn explicit_two() -> Result<(), Box<dyn std::error::Error>> { 54 + let file = create_test_file("test.txt", "garbage data for testing")?; 55 + let file2 = create_test_file("test2.txt", "more garbage data for testing")?; 56 + let working_dir = create_working_dir()?; 57 + let archive = working_dir.child("archive.7z"); 58 + archive.assert(predicate::path::missing()); 59 + 60 + let mut compress = Command::cargo_bin("cmprss")?; 61 + compress 62 + .arg("7z") 63 + .arg(file.path()) 64 + .arg(file2.path()) 65 + .arg(archive.path()); 66 + compress.assert().success(); 67 + archive.assert(predicate::path::is_file()); 68 + 69 + let mut extract = Command::cargo_bin("cmprss")?; 70 + extract 71 + .arg("7z") 72 + .arg("--extract") 73 + .arg(archive.path()) 74 + .arg(working_dir.path()); 75 + extract.assert().success(); 76 + 77 + assert_files_equal(file.path(), &working_dir.child("test.txt")); 78 + assert_files_equal(file2.path(), &working_dir.child("test2.txt")); 79 + 80 + Ok(()) 81 + } 82 + 83 + /// 7z roundtrip with a directory 84 + /// 85 + /// ``` bash 86 + /// cmprss 7z directory archive.7z 87 + /// cmprss 7z --extract archive.7z output_dir 88 + /// ``` 89 + #[test] 90 + fn directory() -> Result<(), Box<dyn std::error::Error>> { 91 + let dir = create_working_dir()?; 92 + let file = dir.child("test.txt"); 93 + file.write_str("garbage data for testing")?; 94 + let file2 = dir.child("test2.txt"); 95 + file2.write_str("more garbage data for testing")?; 96 + 97 + let working_dir = create_working_dir()?; 98 + let archive = working_dir.child("archive.7z"); 99 + archive.assert(predicate::path::missing()); 100 + 101 + let mut compress = Command::cargo_bin("cmprss")?; 102 + compress.arg("7z").arg(dir.path()).arg(archive.path()); 103 + compress.assert().success(); 104 + archive.assert(predicate::path::is_file()); 105 + 106 + let extract_dir = working_dir.child("output"); 107 + std::fs::create_dir_all(extract_dir.path())?; 108 + 109 + let mut extract = Command::cargo_bin("cmprss")?; 110 + extract 111 + .arg("7z") 112 + .arg("--extract") 113 + .arg(archive.path()) 114 + .arg(extract_dir.path()); 115 + extract.assert().success(); 116 + 117 + let dir_name: PathBuf = dir.path().file_name().unwrap().into(); 118 + assert_files_equal(file.path(), &extract_dir.child(&dir_name).child("test.txt")); 119 + assert_files_equal( 120 + file2.path(), 121 + &extract_dir.child(&dir_name).child("test2.txt"), 122 + ); 123 + 124 + Ok(()) 125 + } 126 + 127 + /// 7z listing the contents of an archive 128 + /// 129 + /// ``` bash 130 + /// cmprss 7z --list archive.7z 131 + /// ``` 132 + #[test] 133 + fn list() -> Result<(), Box<dyn std::error::Error>> { 134 + let file = create_test_file("listed.txt", "entry data")?; 135 + let working_dir = create_working_dir()?; 136 + let archive = working_dir.child("archive.7z"); 137 + 138 + let mut compress = Command::cargo_bin("cmprss")?; 139 + compress.arg("7z").arg(file.path()).arg(archive.path()); 140 + compress.assert().success(); 141 + 142 + let mut list = Command::cargo_bin("cmprss")?; 143 + list.arg("7z").arg("--list").arg(archive.path()); 144 + list.assert() 145 + .success() 146 + .stdout(predicate::str::contains("listed.txt")); 147 + 148 + Ok(()) 149 + } 150 + 151 + /// 7z via the `sevenz` alias 152 + /// 153 + /// ``` bash 154 + /// cmprss sevenz test.txt archive.7z 155 + /// ``` 156 + #[test] 157 + fn alias() -> Result<(), Box<dyn std::error::Error>> { 158 + let file = create_test_file("test.txt", "garbage data for the alias test")?; 159 + let working_dir = create_working_dir()?; 160 + let archive = working_dir.child("archive.7z"); 161 + 162 + let mut compress = Command::cargo_bin("cmprss")?; 163 + compress.arg("sevenz").arg(file.path()).arg(archive.path()); 164 + compress.assert().success(); 165 + archive.assert(predicate::path::is_file()); 166 + 167 + Ok(()) 168 + } 169 + } 170 + }