diff --git a/Cargo.toml b/Cargo.toml index 7b91cbb..f86fa1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "totebag" version = "0.4.6" -description = "A tool for archiving files and directories and extracting several archive formats." +description = "A tool for extracting/archiving files and directories in multiple formats." repository = "https://github.com/tamada/totebag" readme = "README.md" authors = [ @@ -16,13 +16,15 @@ edition = "2021" [dependencies] bzip2 = "0.4.4" clap = { version = "4.5.4", features = ["derive"] } +delharc = "0.6.1" flate2 = "1.0.29" sevenz-rust = "0.6.0" tar = "0.4.40" time = "0.3.36" unrar = "0.5.3" xz2 = "0.1.7" -zip = "1.1.1" +zip = "2.1.0" +zstd = "0.13.1" [build-dependencies] clap = { version = "4.5.4", features = ["derive"] } diff --git a/src/archiver.rs b/src/archiver.rs index 9041e8d..caeb987 100644 --- a/src/archiver.rs +++ b/src/archiver.rs @@ -1,15 +1,17 @@ use std::fs::{create_dir_all, File}; use std::path::PathBuf; +use crate::archiver::lha::LhaArchiver; use crate::archiver::rar::RarArchiver; use crate::archiver::sevenz::SevenZArchiver; -use crate::archiver::tar::{TarArchiver, TarBz2Archiver, TarGzArchiver, TarXzArchiver}; +use crate::archiver::tar::{TarArchiver, TarBz2Archiver, TarGzArchiver, TarXzArchiver, TarZstdArchiver}; use crate::archiver::zip::ZipArchiver; use crate::cli::{Result, ToteError}; use crate::format::{find_format, Format}; use crate::verboser::{create_verboser, Verboser}; use crate::CliOpts; +mod lha; mod os; mod rar; mod sevenz; @@ -31,6 +33,8 @@ pub fn create_archiver(dest: &PathBuf) -> Result> { Format::TarGz => Ok(Box::new(TarGzArchiver {})), Format::TarBz2 => Ok(Box::new(TarBz2Archiver {})), Format::TarXz => Ok(Box::new(TarXzArchiver {})), + Format::TarZstd => Ok(Box::new(TarZstdArchiver {})), + Format::LHA => Ok(Box::new(LhaArchiver {})), Format::Rar => Ok(Box::new(RarArchiver {})), Format::SevenZ => Ok(Box::new(SevenZArchiver {})), _ => Err(ToteError::UnknownFormat(format.to_string())), @@ -160,9 +164,17 @@ mod tests { assert!(a7.is_ok()); assert_eq!(a7.unwrap().format(), Format::SevenZ); - let a8 = create_archiver(&PathBuf::from("results/test.unknown")); - assert!(a8.is_err()); - if let Err(e) = a8 { + let a8 = create_archiver(&PathBuf::from("results/test.tar.zst")); + assert!(a8.is_ok()); + assert_eq!(a8.unwrap().format(), Format::TarZstd); + + let a9 = create_archiver(&PathBuf::from("results/test.lha")); + assert!(a9.is_ok()); + assert_eq!(a9.unwrap().format(), Format::LHA); + + let a10 = create_archiver(&PathBuf::from("results/test.unknown")); + assert!(a10.is_err()); + if let Err(e) = a10 { if let ToteError::UnknownFormat(msg) = e { assert_eq!(msg, "test.unknown: unknown format".to_string()); } else { diff --git a/src/archiver/lha.rs b/src/archiver/lha.rs new file mode 100644 index 0000000..8014bc8 --- /dev/null +++ b/src/archiver/lha.rs @@ -0,0 +1,42 @@ +use crate::archiver::{Archiver, Format, ArchiverOpts}; +use crate::cli::{ToteError, Result}; + +pub(super) struct LhaArchiver { +} + +impl Archiver for LhaArchiver { + fn perform(&self, _: &ArchiverOpts) -> Result<()> { + Err(ToteError::UnsupportedFormat("only extraction support for lha".to_string())) + } + fn format(&self) -> Format { + Format::LHA + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use std::path::PathBuf; + use crate::verboser::create_verboser; + + #[test] + fn test_format() { + let archiver = LhaArchiver{}; + assert_eq!(archiver.format(), Format::LHA); + } + + #[test] + fn test_archive() { + let archiver = LhaArchiver{}; + let opts = ArchiverOpts { + dest: PathBuf::from("results/test.rar"), + targets: vec![], + overwrite: false, + recursive: false, + v: create_verboser(false), + }; + let r = archiver.perform(&opts); + assert!(r.is_err()); + } +} diff --git a/src/archiver/tar.rs b/src/archiver/tar.rs index c74f148..6d37f42 100644 --- a/src/archiver/tar.rs +++ b/src/archiver/tar.rs @@ -15,9 +15,10 @@ pub(super) struct TarGzArchiver { } pub(super) struct TarBz2Archiver { } - pub(super) struct TarXzArchiver { } +pub(super) struct TarZstdArchiver { +} impl Archiver for TarArchiver { fn perform(&self, inout: &ArchiverOpts) -> Result<()> { @@ -36,6 +37,7 @@ impl Archiver for TarGzArchiver{ Format::TarGz } } + impl Archiver for TarBz2Archiver { fn perform(&self, opts: &ArchiverOpts) -> Result<()> { write_tar(opts, |file| BzEncoder::new(file, bzip2::Compression::best())) @@ -49,11 +51,23 @@ impl Archiver for TarXzArchiver { fn perform(&self, inout: &ArchiverOpts) -> Result<()> { write_tar(inout, |file| XzEncoder::new(file, 9)) } + fn format(&self) -> Format { Format::TarXz } } +impl Archiver for TarZstdArchiver { + fn perform(&self, inout: &ArchiverOpts) -> Result<()> { + write_tar(inout, |file| + zstd::Encoder::new(file, 9).unwrap()) + } + + fn format(&self) -> Format { + Format::TarZstd + } +} + fn write_tar(opts: &ArchiverOpts, f: F) -> Result<()> where F: FnOnce(File) -> W { match opts.destination() { @@ -110,8 +124,8 @@ fn process_file(builder: &mut Builder, target: PathBuf) -> Result<( mod tests { use std::path::PathBuf; + use super::*; use crate::archiver::Archiver; - use crate::archiver::tar::{TarArchiver, TarGzArchiver, TarBz2Archiver}; use crate::archiver::ArchiverOpts; use crate::format::Format; @@ -119,7 +133,7 @@ mod tests { where F: FnOnce() -> PathBuf, { - // setup(); // 予めやりたい処理 + // setup(); // preprocessing process let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)); match result { Ok(path) => teardown(path), @@ -170,6 +184,34 @@ mod tests { }); } + #[test] + fn test_tarxz() { + run_test(|| { + let archiver = TarXzArchiver{}; + let inout = ArchiverOpts::create(PathBuf::from("results/test.tar.xz"), vec![PathBuf::from("src"), PathBuf::from("Cargo.toml")], true, true, false); + let result = archiver.perform(&inout); + let path = PathBuf::from("results/test.tar.xz"); + assert!(result.is_ok()); + assert!(path.exists()); + assert_eq!(archiver.format(), Format::TarXz); + path + }); + } + + #[test] + fn test_tarzstd() { + run_test(|| { + let archiver = TarZstdArchiver{}; + let inout = ArchiverOpts::create(PathBuf::from("results/test.tar.zst"), vec![PathBuf::from("src"), PathBuf::from("Cargo.toml")], true, true, false); + let result = archiver.perform(&inout); + let path = PathBuf::from("results/test.tar.zst"); + assert!(result.is_ok()); + assert!(path.exists()); + assert_eq!(archiver.format(), Format::TarZstd); + path + }); + } + fn teardown(path: PathBuf) { let _ = std::fs::remove_file(path); } diff --git a/src/extractor.rs b/src/extractor.rs index 9bd436f..caebdb2 100644 --- a/src/extractor.rs +++ b/src/extractor.rs @@ -1,14 +1,15 @@ use std::path::PathBuf; -use crate::format::{find_format, Format}; use crate::cli::{Result, ToteError}; -use crate::CliOpts; +use crate::format::{find_format, Format}; use crate::verboser::{create_verboser, Verboser}; +use crate::CliOpts; -mod zip; +mod lha; mod rar; -mod tar; mod sevenz; +mod tar; +mod zip; pub struct ExtractorOpts { pub dest: PathBuf, @@ -21,23 +22,23 @@ impl ExtractorOpts { pub fn new(opts: &CliOpts) -> ExtractorOpts { let d = opts.output.clone(); ExtractorOpts { - dest: d.unwrap_or_else(|| { - PathBuf::from(".") - }), + dest: d.unwrap_or_else(|| PathBuf::from(".")), use_archive_name_dir: opts.to_archive_name_dir, overwrite: opts.overwrite, v: create_verboser(opts.verbose), } } - + /// Returns the base of the destination directory for the archive file. /// The target is the archive file name of source. pub fn destination(&self, target: &PathBuf) -> PathBuf { if self.use_archive_name_dir { let file_name = target.file_name().unwrap().to_str().unwrap(); let ext = target.extension().unwrap().to_str().unwrap(); - let dir_name = file_name.trim_end_matches(ext) - .trim_end_matches(".").to_string(); + let dir_name = file_name + .trim_end_matches(ext) + .trim_end_matches(".") + .to_string(); self.dest.join(dir_name) } else { self.dest.clone() @@ -56,21 +57,30 @@ pub fn create_extractor(file: &PathBuf) -> Result> { match format { Ok(format) => { return match format { - Format::Zip => Ok(Box::new(zip::ZipExtractor{})), - Format::Rar => Ok(Box::new(rar::RarExtractor{})), - Format::Tar => Ok(Box::new(tar::TarExtractor{})), - Format::TarGz => Ok(Box::new(tar::TarGzExtractor{})), - Format::TarBz2 => Ok(Box::new(tar::TarBz2Extractor{})), - Format::TarXz => Ok(Box::new(tar::TarXzExtractor{})), - Format::SevenZ => Ok(Box::new(sevenz::SevenZExtractor{})), - Format::Unknown(s) => Err(ToteError::UnknownFormat(format!("{}: unsupported format", s))), + Format::Zip => Ok(Box::new(zip::ZipExtractor {})), + Format::Rar => Ok(Box::new(rar::RarExtractor {})), + Format::Tar => Ok(Box::new(tar::TarExtractor {})), + Format::TarGz => Ok(Box::new(tar::TarGzExtractor {})), + Format::TarBz2 => Ok(Box::new(tar::TarBz2Extractor {})), + Format::TarXz => Ok(Box::new(tar::TarXzExtractor {})), + Format::TarZstd => Ok(Box::new(tar::TarZstdExtractor {})), + Format::LHA => Ok(Box::new(lha::LhaExtractor {})), + Format::SevenZ => Ok(Box::new(sevenz::SevenZExtractor {})), + Format::Unknown(s) => Err(ToteError::UnknownFormat(format!( + "{}: unsupported format", + s + ))), } } Err(msg) => Err(msg), } } -pub fn extractor_info(extractor: &Box, target: &PathBuf, opts: &ExtractorOpts) -> String { +pub fn extractor_info( + extractor: &Box, + target: &PathBuf, + opts: &ExtractorOpts, +) -> String { format!( "Format: {:?}\nFile: {:?}\nDestination: {:?}", extractor.format(), @@ -142,4 +152,4 @@ mod tests { assert!(false); } } -} \ No newline at end of file +} diff --git a/src/extractor/lha.rs b/src/extractor/lha.rs new file mode 100644 index 0000000..b61cdcf --- /dev/null +++ b/src/extractor/lha.rs @@ -0,0 +1,135 @@ +use std::fs::{create_dir_all, File}; +use std::io::copy; +use std::path::PathBuf; + +use crate::cli::{Result, ToteError}; +use crate::extractor::{Extractor, ExtractorOpts}; + +pub(super) struct LhaExtractor {} + +impl Extractor for LhaExtractor { + fn list_archives(&self, archive_file: PathBuf) -> Result> { + let mut result = Vec::::new(); + let mut reader = match delharc::parse_file(&archive_file) { + Err(e) => return Err(ToteError::IOError(e)), + Ok(h) => h, + }; + loop { + let header = reader.header(); + let name = header.parse_pathname(); + if !header.is_directory() { + result.push(name.to_str().unwrap().to_string()); + } + match reader.next_file() { + Ok(r) => { + if !r { + break; + } + } + Err(e) => return Err(ToteError::SomeError(Box::new(e))), + } + } + Ok(result) + } + + fn perform(&self, archive_file: PathBuf, opts: &ExtractorOpts) -> Result<()> { + let mut reader = match delharc::parse_file(&archive_file) { + Err(e) => return Err(ToteError::IOError(e)), + Ok(h) => h, + }; + loop { + let header = reader.header(); + let name = header.parse_pathname(); + let dest = opts.destination(&archive_file).join(&name); + if reader.is_decoder_supported() { + opts.v.verbose(format!( + "extracting {} ({} bytes)", + &name.to_str().unwrap().to_string(), + header.original_size + )); + create_dir_all(dest.parent().unwrap()).unwrap(); + let mut dest = match File::create(dest) { + Ok(f) => f, + Err(e) => return Err(ToteError::IOError(e)), + }; + match copy(&mut reader, &mut dest) { + Ok(_) => {} + Err(e) => return Err(ToteError::IOError(e)), + } + if let Err(e) = reader.crc_check() { + return Err(ToteError::SomeError(Box::new(e))); + }; + } else if !header.is_directory() { + opts.v.verbose(format!( + "{:?}: unsupported compression method ({:?})", + &name, header.compression + )); + } + match reader.next_file() { + Ok(r) => { + if !r { + break; + } + } + Err(e) => return Err(ToteError::SomeError(Box::new(e))), + } + } + Ok(()) + } + + fn format(&self) -> crate::format::Format { + crate::format::Format::LHA + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::format::Format; + use crate::verboser::create_verboser; + + #[test] + fn test_list_archives() { + let extractor = LhaExtractor {}; + let file = PathBuf::from("testdata/test.lzh"); + match extractor.list_archives(file) { + Ok(r) => { + assert_eq!(r.len(), 23); + assert_eq!(r.get(0), Some("Cargo.toml".to_string()).as_ref()); + assert_eq!(r.get(1), Some("LICENSE".to_string()).as_ref()); + assert_eq!(r.get(2), Some("README.md".to_string()).as_ref()); + assert_eq!(r.get(3), Some("build.rs".to_string()).as_ref()); + } + Err(_) => assert!(false), + } + } + + #[test] + fn test_extract_archive() { + let e = LhaExtractor {}; + let file = PathBuf::from("testdata/test.lzh"); + let opts = ExtractorOpts { + dest: PathBuf::from("results/lha"), + use_archive_name_dir: true, + overwrite: true, + v: create_verboser(false), + }; + match e.perform(file, &opts) { + Ok(_) => { + assert!(true); + assert!(PathBuf::from("results/lha/test/Cargo.toml").exists()); + std::fs::remove_dir_all(PathBuf::from("results/lha")).unwrap(); + } + Err(e) => { + eprintln!("{:?}", e); + assert!(false); + } + }; + } + + #[test] + fn test_format() { + let extractor = LhaExtractor {}; + assert_eq!(extractor.format(), Format::LHA); + } +} diff --git a/src/extractor/tar.rs b/src/extractor/tar.rs index f262745..7796296 100644 --- a/src/extractor/tar.rs +++ b/src/extractor/tar.rs @@ -5,18 +5,15 @@ use std::{fs::File, path::PathBuf}; use tar::Archive; use xz2::read::XzDecoder; -use crate::cli::{ToteError, Result}; +use crate::cli::{Result, ToteError}; use crate::extractor::{Extractor, ExtractorOpts}; use crate::format::Format; -pub(super) struct TarExtractor { -} -pub(super) struct TarGzExtractor { -} -pub(super) struct TarBz2Extractor { -} -pub(super) struct TarXzExtractor { -} +pub(super) struct TarExtractor {} +pub(super) struct TarGzExtractor {} +pub(super) struct TarBz2Extractor {} +pub(super) struct TarXzExtractor {} +pub(super) struct TarZstdExtractor {} impl Extractor for TarExtractor { fn list_archives(&self, archive_file: PathBuf) -> Result> { @@ -90,8 +87,29 @@ impl Extractor for TarXzExtractor { } } -fn open_tar_file(file: &PathBuf, opener: F) -> Result> - where F: FnOnce(File) -> R { +impl Extractor for TarZstdExtractor { + fn list_archives(&self, archive_file: PathBuf) -> Result> { + match open_tar_file(&archive_file, |f| zstd::Decoder::new(f).unwrap()) { + Err(e) => Err(e), + Ok(archive) => list_tar(archive), + } + } + + fn perform(&self, archive_file: PathBuf, opts: &ExtractorOpts) -> Result<()> { + match open_tar_file(&archive_file, |f| zstd::Decoder::new(f).unwrap()) { + Err(e) => Err(e), + Ok(archive) => extract_tar(archive, archive_file, opts), + } + } + fn format(&self) -> Format { + Format::TarZstd + } +} + +fn open_tar_file(file: &PathBuf, opener: F) -> Result> +where + F: FnOnce(File) -> R, +{ let file = match File::open(file) { Ok(f) => f, Err(e) => return Err(ToteError::IOError(e)), @@ -100,7 +118,11 @@ fn open_tar_file(file: &PathBuf, opener: F) -> Result> Ok(Archive::new(writer)) } -fn extract_tar(mut archive: tar::Archive, original: PathBuf, opts: &ExtractorOpts) -> Result<()> { +fn extract_tar( + mut archive: tar::Archive, + original: PathBuf, + opts: &ExtractorOpts, +) -> Result<()> { for entry in archive.entries().unwrap() { let mut entry = entry.unwrap(); let path = entry.header().path().unwrap(); @@ -109,7 +131,8 @@ fn extract_tar(mut archive: tar::Archive, original: PathBuf, opts: & continue; } let size = entry.header().size().unwrap(); - opts.v.verbose(format!("extracting {:?} ({} bytes)", path, size)); + opts.v + .verbose(format!("extracting {:?} ({} bytes)", path, size)); let dest = opts.destination(&original).join(path); if entry.header().entry_type().is_file() { @@ -142,7 +165,7 @@ mod tests { #[test] fn test_list_tar_file() { - let extractor = TarExtractor{}; + let extractor = TarExtractor {}; let file = PathBuf::from("testdata/test.tar"); match extractor.list_archives(file) { Ok(r) => { @@ -151,14 +174,14 @@ mod tests { assert_eq!(r.get(1), Some("build.rs".to_string()).as_ref()); assert_eq!(r.get(2), Some("LICENSE".to_string()).as_ref()); assert_eq!(r.get(3), Some("README.md".to_string()).as_ref()); - }, + } Err(_) => assert!(false), } } #[test] fn test_extract_archive() { - let e = TarExtractor{}; + let e = TarExtractor {}; let file = PathBuf::from("testdata/test.tar"); let opts = ExtractorOpts { dest: PathBuf::from("results/tar"), @@ -167,19 +190,18 @@ mod tests { v: create_verboser(false), }; match e.perform(file, &opts) { - Ok(_) => { + Ok(_) => { assert!(true); assert!(PathBuf::from("results/tar/Cargo.toml").exists()); std::fs::remove_dir_all(PathBuf::from("results/tar")).unwrap(); - }, + } Err(_) => assert!(false), }; } - #[test] fn test_list_tarbz2_file() { - let extractor = TarBz2Extractor{}; + let extractor = TarBz2Extractor {}; let file = PathBuf::from("testdata/test.tar.bz2"); match extractor.list_archives(file) { Ok(r) => { @@ -188,14 +210,14 @@ mod tests { assert_eq!(r.get(1), Some("build.rs".to_string()).as_ref()); assert_eq!(r.get(2), Some("LICENSE".to_string()).as_ref()); assert_eq!(r.get(3), Some("README.md".to_string()).as_ref()); - }, + } Err(_) => assert!(false), } } #[test] fn test_list_targz_file() { - let extractor = TarGzExtractor{}; + let extractor = TarGzExtractor {}; let file = PathBuf::from("testdata/test.tar.gz"); match extractor.list_archives(file) { Ok(r) => { @@ -204,20 +226,37 @@ mod tests { assert_eq!(r.get(1), Some("build.rs".to_string()).as_ref()); assert_eq!(r.get(2), Some("LICENSE".to_string()).as_ref()); assert_eq!(r.get(3), Some("README.md".to_string()).as_ref()); - }, + } Err(_) => assert!(false), } } + #[test] + fn test_list_tarzstd_file() { + let extractor = TarZstdExtractor {}; + let file = PathBuf::from("testdata/test.tar.zst"); + match extractor.list_archives(file) { + Ok(r) => { + assert_eq!(r.len(), 16); + assert_eq!(r.get(0), Some("Cargo.toml".to_string()).as_ref()); + assert_eq!(r.get(1), Some("build.rs".to_string()).as_ref()); + assert_eq!(r.get(2), Some("LICENSE".to_string()).as_ref()); + assert_eq!(r.get(3), Some("README.md".to_string()).as_ref()); + } + Err(_) => assert!(false), + } + } + + #[test] fn test_format() { - let e1 = TarExtractor{}; + let e1 = TarExtractor {}; assert_eq!(e1.format(), Format::Tar); - let e2 = TarGzExtractor{}; + let e2 = TarGzExtractor {}; assert_eq!(e2.format(), Format::TarGz); - let e3 = TarBz2Extractor{}; + let e3 = TarBz2Extractor {}; assert_eq!(e3.format(), Format::TarBz2); } -} \ No newline at end of file +} diff --git a/src/format.rs b/src/format.rs index 820d648..fdf6886 100644 --- a/src/format.rs +++ b/src/format.rs @@ -1,6 +1,7 @@ use std::ffi::OsStr; use std::fmt::Display; -use crate::cli::{ToteError, Result}; + +use crate::cli::{Result, ToteError}; pub fn find_format(file_name: Option<&OsStr>) -> Result { match file_name { @@ -12,10 +13,14 @@ pub fn find_format(file_name: Option<&OsStr>) -> Result { return Ok(Format::TarBz2); } else if name.ends_with(".tar.xz") || name.ends_with(".txz") { return Ok(Format::TarXz); + } else if name.ends_with(".tar.zst") || name.ends_with(".tzst") { + return Ok(Format::TarZstd); } else if name.ends_with(".7z") { return Ok(Format::SevenZ); } else if name.ends_with(".tar") { return Ok(Format::Tar); + } else if name.ends_with(".lha") || name.ends_with(".lzh") { + return Ok(Format::LHA); } else if name.ends_with(".rar") { return Ok(Format::Rar); } else if name.ends_with(".zip") || name.ends_with(".jar") || name.ends_with(".war") || name.ends_with(".ear") { @@ -28,7 +33,6 @@ pub fn find_format(file_name: Option<&OsStr>) -> Result { } } - #[derive(Debug, PartialEq)] pub enum Format { Zip, @@ -36,7 +40,9 @@ pub enum Format { TarGz, TarBz2, TarXz, + TarZstd, SevenZ, + LHA, Rar, Unknown(String), } @@ -49,7 +55,9 @@ impl Display for Format { Format::TarGz => write!(f, "TarGz"), Format::TarBz2 => write!(f, "TarBz2"), Format::TarXz => write!(f, "TarXz"), + Format::TarZstd => write!(f, "TarZstd"), Format::SevenZ => write!(f, "SevenZ"), + Format::LHA => write!(f, "LHA"), Format::Rar => write!(f, "Rar"), Format::Unknown(s) => write!(f, "{}: unknown format", s), } @@ -102,4 +110,4 @@ mod tests { } } } -} \ No newline at end of file +} diff --git a/templates/README.md b/templates/README.md index 8aafdc5..4108b82 100644 --- a/templates/README.md +++ b/templates/README.md @@ -8,7 +8,7 @@ [![Rust Report Card](https://rust-reportcard.xuri.me/badge/github.com/tamada/totebag)](https://rust-reportcard.xuri.me/report/github.com/tamada/totebag) [![Coverage Status](https://coveralls.io/repos/github/tamada/totebag/badge.svg)](https://coveralls.io/github/tamada/totebag) -A tool for archiving files and directories and extracting several archive formats. +A tool for extracting/archiving files and directories in multiple formats. ## Description @@ -19,7 +19,7 @@ The tool can extract archive files and archive files and directories. ## Usage ```sh -A tool for archiving files and directories and extracting several archive formats. +A tool for extracting/archiving files and directories in multiple formats. Usage: totebag [OPTIONS] [ARGUMENTS]... @@ -43,8 +43,10 @@ Supported archive formats: - Tar+Gzip - Tar+Bzip2 - Tar+Xz +- Tar+Zstd - Zip - 7z +- Lha, Lzh (extraction only) - Rar (extraction only) ## Install diff --git a/testdata/test.lzh b/testdata/test.lzh new file mode 100644 index 0000000..f2af3cd Binary files /dev/null and b/testdata/test.lzh differ diff --git a/testdata/test.tar.zst b/testdata/test.tar.zst new file mode 100644 index 0000000..e0e8b14 Binary files /dev/null and b/testdata/test.tar.zst differ