vx_installer/formats/
zip.rs

1//! ZIP archive format handler
2
3use super::FormatHandler;
4use crate::{progress::ProgressContext, Error, Result};
5use std::path::{Path, PathBuf};
6
7/// Handler for ZIP archive format
8pub struct ZipHandler;
9
10impl ZipHandler {
11    /// Create a new ZIP handler
12    pub fn new() -> Self {
13        Self
14    }
15}
16
17#[async_trait::async_trait]
18impl FormatHandler for ZipHandler {
19    fn name(&self) -> &str {
20        "zip"
21    }
22
23    fn can_handle(&self, file_path: &Path) -> bool {
24        if let Some(filename) = file_path.file_name().and_then(|n| n.to_str()) {
25            filename.ends_with(".zip")
26        } else {
27            false
28        }
29    }
30
31    async fn extract(
32        &self,
33        source_path: &Path,
34        target_dir: &Path,
35        progress: &ProgressContext,
36    ) -> Result<Vec<PathBuf>> {
37        // Ensure target directory exists
38        std::fs::create_dir_all(target_dir)?;
39
40        // Open the ZIP file
41        let file = std::fs::File::open(source_path)?;
42        let mut archive = zip::ZipArchive::new(file).map_err(|e| {
43            Error::extraction_failed(source_path, format!("Failed to open ZIP archive: {}", e))
44        })?;
45
46        let total_files = archive.len();
47        progress
48            .start(
49                &format!("Extracting {} files", total_files),
50                Some(total_files as u64),
51            )
52            .await?;
53
54        let mut extracted_files = Vec::new();
55
56        // Extract each file
57        for i in 0..total_files {
58            let mut file = archive.by_index(i).map_err(|e| {
59                Error::extraction_failed(
60                    source_path,
61                    format!("Failed to access file at index {}: {}", i, e),
62                )
63            })?;
64
65            let file_path = match file.enclosed_name() {
66                Some(path) => target_dir.join(path),
67                None => {
68                    // Skip files with invalid names
69                    progress.increment(1).await?;
70                    continue;
71                }
72            };
73
74            // Create parent directories
75            if let Some(parent) = file_path.parent() {
76                std::fs::create_dir_all(parent)?;
77            }
78
79            // Extract the file
80            if file.is_dir() {
81                // Create directory
82                std::fs::create_dir_all(&file_path)?;
83            } else {
84                // Extract file
85                let mut output_file = std::fs::File::create(&file_path)?;
86                std::io::copy(&mut file, &mut output_file)?;
87
88                // Make executable if needed
89                #[cfg(unix)]
90                {
91                    if file.unix_mode().unwrap_or(0) & 0o111 != 0 {
92                        self.make_executable(&file_path)?;
93                    }
94                }
95
96                extracted_files.push(file_path);
97            }
98
99            progress.increment(1).await?;
100        }
101
102        progress.finish("Extraction completed").await?;
103
104        Ok(extracted_files)
105    }
106}
107
108impl Default for ZipHandler {
109    fn default() -> Self {
110        Self::new()
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[tokio::test]
119    async fn test_zip_handler_can_handle() {
120        let handler = ZipHandler::new();
121
122        assert!(handler.can_handle(Path::new("test.zip")));
123        assert!(!handler.can_handle(Path::new("test.tar.gz")));
124        assert!(!handler.can_handle(Path::new("test.exe")));
125    }
126
127    #[tokio::test]
128    async fn test_zip_handler_name() {
129        let handler = ZipHandler::new();
130        assert_eq!(handler.name(), "zip");
131    }
132
133    // Note: More comprehensive tests would require creating actual ZIP files
134    // This is a basic structure test
135}