Skip to main content

zzz_arc/formats/
mod.rs

1//! compression format abstraction
2
3use 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/// compression options for creating archives
45#[derive(Debug, Clone)]
46pub struct CompressionOptions {
47    pub level: i32,                  // 1-22, default 19
48    pub threads: u32,                // 0 = auto-detect CPU cores
49    pub normalize_permissions: bool, // security: normalize permissions
50    pub normalize_ownership: bool,   // security: normalize ownership (uid/gid)
51    pub strip_xattrs: bool,          // security: strip extended attributes (xattrs)
52    pub strip_timestamps: bool,      // security: strip filesystem timestamps
53    pub follow_symlinks: bool,       // follow symlinks when walking input
54    pub allow_symlink_escape: bool,  // allow symlink targets outside input root
55    pub deterministic: bool,         // sort files for reproducible archives
56    pub password: Option<String>,
57}
58
59impl Default for CompressionOptions {
60    fn default() -> Self {
61        Self {
62            level: 19,
63            threads: 0, // auto-detect
64            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/// extraction options for extracting archives
77#[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/// Supported compression formats
103#[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    /// Detect format from file path, with magic number validation
115    pub fn detect(path: &Path) -> Result<Self> {
116        // Try magic number detection first (most reliable)
117        if let Ok(format) = Self::from_magic(path) {
118            return Ok(format);
119        }
120
121        // Fall back to extension-based detection
122        if let Some(format) = Self::from_extension(path) {
123            return Ok(format);
124        }
125
126        Err(anyhow::anyhow!("unsupported archive format"))
127    }
128
129    /// Detect format from file extension
130    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    /// Detect format using magic number detection
157    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            // Check magic numbers
172            match &buffer[..4] {
173                [0x28, 0xB5, 0x2F, 0xFD] => return Ok(Format::Zstd), // Zstandard
174                [0x1F, 0x8B, _, _] => return Ok(Format::Gzip),       // Gzip
175                [0xFD, 0x37, 0x7A, 0x58] => return Ok(Format::Xz),   // XZ
176                [0x50, 0x4B, 0x03, 0x04] | [0x50, 0x4B, 0x05, 0x06] | [0x50, 0x4B, 0x07, 0x08] => {
177                    return Ok(Format::Zip); // ZIP
178                }
179                _ => {}
180            }
181        }
182
183        if bytes_read >= 6 && &buffer[..6] == b"7z\xBC\xAF\x27\x1C" {
184            return Ok(Format::SevenZ); // 7-Zip
185        }
186
187        if bytes_read >= 7 && &buffer[..7] == b"Rar!\x1A\x07\x00" {
188            return Ok(Format::Rar); // RAR v4
189        }
190
191        if bytes_read >= 8 && &buffer[..8] == b"Rar!\x1A\x07\x01\x00" {
192            return Ok(Format::Rar); // RAR v5
193        }
194
195        // Use tree_magic_mini as final fallback
196        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    /// Get the default file extension for this format
215    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    /// Get format name for display
227    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
239/// trait for compression formats
240pub 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}