1use crate::Result;
4use anyhow::Context;
5use std::path::Path;
6
7pub mod gz;
8pub mod rar;
9pub mod sevenz;
10pub mod tarball;
11pub mod xz;
12pub mod zip;
13pub mod zstd;
14
15#[derive(Debug)]
16pub struct CompressionStats {
17 pub input_size: u64,
18 pub output_size: u64,
19 pub compression_ratio: f64,
20}
21
22impl CompressionStats {
23 pub fn new(input_size: u64, output_size: u64) -> Self {
24 let compression_ratio = if input_size > 0 {
25 output_size as f64 / input_size as f64
26 } else {
27 0.0
28 };
29 Self {
30 input_size,
31 output_size,
32 compression_ratio,
33 }
34 }
35}
36
37#[derive(Debug)]
38pub struct ArchiveEntry {
39 pub path: String,
40 pub size: u64,
41 pub is_file: bool,
42}
43
44#[derive(Debug, Clone)]
46pub struct CompressionOptions {
47 pub level: i32, pub threads: u32, pub normalize_permissions: bool, pub normalize_ownership: bool, pub strip_xattrs: bool, pub strip_timestamps: bool, pub follow_symlinks: bool, pub allow_symlink_escape: bool, pub deterministic: bool, pub password: Option<String>,
57}
58
59impl Default for CompressionOptions {
60 fn default() -> Self {
61 Self {
62 level: 19,
63 threads: 0, normalize_permissions: true,
65 normalize_ownership: true,
66 strip_xattrs: true,
67 strip_timestamps: false,
68 follow_symlinks: false,
69 allow_symlink_escape: false,
70 deterministic: true,
71 password: None,
72 }
73 }
74}
75
76#[derive(Debug, Clone)]
78pub struct ExtractionOptions {
79 pub overwrite: bool,
80 pub strip_components: usize,
81 pub strip_xattrs: bool,
82 pub strip_timestamps: bool,
83 pub preserve_permissions: bool,
84 pub preserve_ownership: bool,
85 pub password: Option<String>,
86}
87
88impl Default for ExtractionOptions {
89 fn default() -> Self {
90 Self {
91 overwrite: false,
92 strip_components: 0,
93 strip_xattrs: true,
94 strip_timestamps: false,
95 preserve_permissions: false,
96 preserve_ownership: false,
97 password: None,
98 }
99 }
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum Format {
105 Zstd,
106 Gzip,
107 Xz,
108 Zip,
109 SevenZ,
110 Rar,
111}
112
113impl Format {
114 pub fn detect(path: &Path) -> Result<Self> {
116 if let Ok(format) = Self::from_magic(path) {
118 return Ok(format);
119 }
120
121 if let Some(format) = Self::from_extension(path) {
123 return Ok(format);
124 }
125
126 Err(anyhow::anyhow!("unsupported archive format"))
127 }
128
129 pub fn from_extension(path: &Path) -> Option<Self> {
131 let filename = path.file_name()?.to_str()?.to_lowercase();
132
133 if filename.ends_with(".zst") || filename.ends_with(".zstd") {
134 Some(Format::Zstd)
135 } else if filename.ends_with(".tgz")
136 || filename.ends_with(".tar.gz")
137 || filename.ends_with(".gz")
138 {
139 Some(Format::Gzip)
140 } else if filename.ends_with(".txz")
141 || filename.ends_with(".tar.xz")
142 || filename.ends_with(".xz")
143 {
144 Some(Format::Xz)
145 } else if filename.ends_with(".zip") {
146 Some(Format::Zip)
147 } else if filename.ends_with(".7z") {
148 Some(Format::SevenZ)
149 } else if filename.ends_with(".rar") {
150 Some(Format::Rar)
151 } else {
152 None
153 }
154 }
155
156 fn from_magic(path: &Path) -> Result<Self> {
158 use std::fs::File;
159 use std::io::Read;
160
161 let mut file = File::open(path).with_context(|| {
162 format!(
163 "Failed to open file for magic number detection: {}",
164 path.display()
165 )
166 })?;
167 let mut buffer = [0u8; 16];
168 let bytes_read = file.read(&mut buffer)?;
169
170 if bytes_read >= 4 {
171 match &buffer[..4] {
173 [0x28, 0xB5, 0x2F, 0xFD] => return Ok(Format::Zstd), [0x1F, 0x8B, _, _] => return Ok(Format::Gzip), [0xFD, 0x37, 0x7A, 0x58] => return Ok(Format::Xz), [0x50, 0x4B, 0x03, 0x04] | [0x50, 0x4B, 0x05, 0x06] | [0x50, 0x4B, 0x07, 0x08] => {
177 return Ok(Format::Zip); }
179 _ => {}
180 }
181 }
182
183 if bytes_read >= 6 && &buffer[..6] == b"7z\xBC\xAF\x27\x1C" {
184 return Ok(Format::SevenZ); }
186
187 if bytes_read >= 7 && &buffer[..7] == b"Rar!\x1A\x07\x00" {
188 return Ok(Format::Rar); }
190
191 if bytes_read >= 8 && &buffer[..8] == b"Rar!\x1A\x07\x01\x00" {
192 return Ok(Format::Rar); }
194
195 match tree_magic_mini::from_filepath(path) {
197 Some(mime_type) => match mime_type {
198 "application/zstd" => Ok(Format::Zstd),
199 "application/gzip" | "application/x-gzip" => Ok(Format::Gzip),
200 "application/x-xz" => Ok(Format::Xz),
201 "application/zip" => Ok(Format::Zip),
202 "application/x-7z-compressed" => Ok(Format::SevenZ),
203 "application/x-rar-compressed" | "application/vnd.rar" => Ok(Format::Rar),
204 _ => Err(anyhow::anyhow!(
205 "unsupported archive format (unknown mime type from tree_magic_mini: {mime_type})"
206 )),
207 },
208 None => Err(anyhow::anyhow!(
209 "failed to determine mime type using tree_magic_mini (returned None)"
210 )),
211 }
212 }
213
214 pub fn extension(&self) -> &'static str {
216 match self {
217 Format::Zstd => "zst",
218 Format::Gzip => "tgz",
219 Format::Xz => "txz",
220 Format::Zip => "zip",
221 Format::SevenZ => "7z",
222 Format::Rar => "rar",
223 }
224 }
225
226 pub fn name(&self) -> &'static str {
228 match self {
229 Format::Zstd => "Zstandard",
230 Format::Gzip => "Gzip",
231 Format::Xz => "XZ",
232 Format::Zip => "ZIP",
233 Format::SevenZ => "7-Zip",
234 Format::Rar => "RAR",
235 }
236 }
237}
238
239pub trait CompressionFormat {
241 fn compress(
242 input_path: &Path,
243 output_path: &Path,
244 options: &CompressionOptions,
245 filter: &crate::filter::FileFilter,
246 progress: Option<&crate::progress::Progress>,
247 ) -> Result<CompressionStats>;
248
249 fn extract(
250 archive_path: &Path,
251 output_dir: &Path,
252 options: &ExtractionOptions,
253 progress: Option<&crate::progress::Progress>,
254 ) -> Result<()>;
255
256 fn list(archive_path: &Path) -> Result<Vec<ArchiveEntry>>;
257
258 fn extension() -> &'static str;
259
260 fn test_integrity(archive_path: &Path) -> Result<()>;
261}