vx_installer/formats/
tar.rs

1//! TAR archive format handler (including compressed variants)
2
3use super::FormatHandler;
4use crate::{progress::ProgressContext, Error, Result};
5use std::io::Read;
6use std::path::{Path, PathBuf};
7
8/// Handler for TAR archive formats (tar, tar.gz, tar.xz, tar.bz2)
9pub struct TarHandler;
10
11impl TarHandler {
12    /// Create a new TAR handler
13    pub fn new() -> Self {
14        Self
15    }
16
17    /// Detect the compression type from filename
18    fn detect_compression(&self, file_path: &Path) -> CompressionType {
19        if let Some(filename) = file_path.file_name().and_then(|n| n.to_str()) {
20            if filename.ends_with(".tar.gz") || filename.ends_with(".tgz") {
21                CompressionType::Gzip
22            } else if filename.ends_with(".tar.xz") || filename.ends_with(".txz") {
23                CompressionType::Xz
24            } else if filename.ends_with(".tar.bz2") || filename.ends_with(".tbz2") {
25                CompressionType::Bzip2
26            } else if filename.ends_with(".tar") {
27                CompressionType::None
28            } else {
29                CompressionType::Unknown
30            }
31        } else {
32            CompressionType::Unknown
33        }
34    }
35}
36
37/// Compression types supported for TAR archives
38#[derive(Debug, Clone, Copy)]
39enum CompressionType {
40    None,
41    Gzip,
42    Xz,
43    Bzip2,
44    Unknown,
45}
46
47#[async_trait::async_trait]
48impl FormatHandler for TarHandler {
49    fn name(&self) -> &str {
50        "tar"
51    }
52
53    fn can_handle(&self, file_path: &Path) -> bool {
54        if let Some(filename) = file_path.file_name().and_then(|n| n.to_str()) {
55            filename.ends_with(".tar")
56                || filename.ends_with(".tar.gz")
57                || filename.ends_with(".tgz")
58                || filename.ends_with(".tar.xz")
59                || filename.ends_with(".txz")
60                || filename.ends_with(".tar.bz2")
61                || filename.ends_with(".tbz2")
62        } else {
63            false
64        }
65    }
66
67    async fn extract(
68        &self,
69        source_path: &Path,
70        target_dir: &Path,
71        progress: &ProgressContext,
72    ) -> Result<Vec<PathBuf>> {
73        // Ensure target directory exists
74        std::fs::create_dir_all(target_dir)?;
75
76        let compression = self.detect_compression(source_path);
77
78        progress.start("Extracting TAR archive", None).await?;
79
80        let file = std::fs::File::open(source_path)?;
81        let mut extracted_files = Vec::new();
82
83        match compression {
84            CompressionType::None => {
85                self.extract_tar(file, target_dir, &mut extracted_files)
86                    .await?;
87            }
88            CompressionType::Gzip => {
89                let decoder = flate2::read::GzDecoder::new(file);
90                self.extract_tar(decoder, target_dir, &mut extracted_files)
91                    .await?;
92            }
93            CompressionType::Xz => {
94                // Note: xz support would require additional dependency
95                return Err(Error::unsupported_format("tar.xz"));
96            }
97            CompressionType::Bzip2 => {
98                // Note: bzip2 support would require additional dependency
99                return Err(Error::unsupported_format("tar.bz2"));
100            }
101            CompressionType::Unknown => {
102                return Err(Error::unsupported_format("unknown tar format"));
103            }
104        }
105
106        progress.finish("TAR extraction completed").await?;
107
108        Ok(extracted_files)
109    }
110}
111
112impl TarHandler {
113    /// Extract TAR archive from a reader
114    async fn extract_tar<R: Read>(
115        &self,
116        reader: R,
117        target_dir: &Path,
118        extracted_files: &mut Vec<PathBuf>,
119    ) -> Result<()> {
120        let mut archive = tar::Archive::new(reader);
121
122        for entry in archive.entries()? {
123            let mut entry = entry?;
124            let path = entry.path()?;
125            let target_path = target_dir.join(&path);
126
127            // Create parent directories
128            if let Some(parent) = target_path.parent() {
129                std::fs::create_dir_all(parent)?;
130            }
131
132            // Extract the entry
133            if entry.header().entry_type().is_dir() {
134                std::fs::create_dir_all(&target_path)?;
135            } else {
136                entry.unpack(&target_path)?;
137
138                // Make executable if needed
139                #[cfg(unix)]
140                {
141                    let mode = entry.header().mode()?;
142                    if mode & 0o111 != 0 {
143                        self.make_executable(&target_path)?;
144                    }
145                }
146
147                extracted_files.push(target_path);
148            }
149        }
150
151        Ok(())
152    }
153}
154
155impl Default for TarHandler {
156    fn default() -> Self {
157        Self::new()
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[tokio::test]
166    async fn test_tar_handler_can_handle() {
167        let handler = TarHandler::new();
168
169        assert!(handler.can_handle(Path::new("test.tar")));
170        assert!(handler.can_handle(Path::new("test.tar.gz")));
171        assert!(handler.can_handle(Path::new("test.tgz")));
172        assert!(handler.can_handle(Path::new("test.tar.xz")));
173        assert!(handler.can_handle(Path::new("test.tar.bz2")));
174        assert!(!handler.can_handle(Path::new("test.zip")));
175        assert!(!handler.can_handle(Path::new("test.exe")));
176    }
177
178    #[tokio::test]
179    async fn test_tar_handler_name() {
180        let handler = TarHandler::new();
181        assert_eq!(handler.name(), "tar");
182    }
183
184    #[test]
185    fn test_compression_detection() {
186        let handler = TarHandler::new();
187
188        assert!(matches!(
189            handler.detect_compression(Path::new("test.tar")),
190            CompressionType::None
191        ));
192        assert!(matches!(
193            handler.detect_compression(Path::new("test.tar.gz")),
194            CompressionType::Gzip
195        ));
196        assert!(matches!(
197            handler.detect_compression(Path::new("test.tgz")),
198            CompressionType::Gzip
199        ));
200        assert!(matches!(
201            handler.detect_compression(Path::new("test.tar.xz")),
202            CompressionType::Xz
203        ));
204    }
205}