vx_installer/formats/
mod.rs

1//! Archive format handling for vx-installer
2//!
3//! This module provides a unified interface for handling different archive formats
4//! and installation methods. It abstracts the complexity of different compression
5//! formats and provides a consistent API for extraction and installation.
6
7use crate::{progress::ProgressContext, Error, Result};
8use std::path::{Path, PathBuf};
9
10pub mod binary;
11pub mod tar;
12pub mod zip;
13
14/// Trait for handling different archive formats and installation methods
15#[async_trait::async_trait]
16pub trait FormatHandler: Send + Sync {
17    /// Get the name of this format handler
18    fn name(&self) -> &str;
19
20    /// Check if this handler can process the given file
21    fn can_handle(&self, file_path: &Path) -> bool;
22
23    /// Extract or install the file to the target directory
24    async fn extract(
25        &self,
26        source_path: &Path,
27        target_dir: &Path,
28        progress: &ProgressContext,
29    ) -> Result<Vec<PathBuf>>;
30
31    /// Get the expected executable name for a tool
32    fn get_executable_name(&self, tool_name: &str) -> String {
33        if cfg!(windows) {
34            format!("{}.exe", tool_name)
35        } else {
36            tool_name.to_string()
37        }
38    }
39
40    /// Find executable files in the extracted directory
41    fn find_executables(&self, dir: &Path, tool_name: &str) -> Result<Vec<PathBuf>> {
42        let exe_name = self.get_executable_name(tool_name);
43        let mut executables = Vec::new();
44
45        // Search for the executable in common locations
46        let search_paths = vec![
47            dir.to_path_buf(),
48            dir.join("bin"),
49            dir.join("usr").join("bin"),
50            dir.join("usr").join("local").join("bin"),
51        ];
52
53        for search_path in search_paths {
54            if !search_path.exists() {
55                continue;
56            }
57
58            // Direct match
59            let exe_path = search_path.join(&exe_name);
60            if exe_path.exists() && exe_path.is_file() {
61                executables.push(exe_path);
62                continue;
63            }
64
65            // Search in subdirectories
66            if let Ok(entries) = std::fs::read_dir(&search_path) {
67                for entry in entries.flatten() {
68                    let path = entry.path();
69                    if path.is_file() {
70                        if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
71                            // Exact match or partial match (for tools with version suffixes)
72                            if filename == exe_name
73                                || (filename.starts_with(tool_name) && self.is_executable(&path))
74                            {
75                                executables.push(path);
76                            }
77                        }
78                    }
79                }
80            }
81        }
82
83        if executables.is_empty() {
84            return Err(Error::executable_not_found(tool_name, dir));
85        }
86
87        Ok(executables)
88    }
89
90    /// Check if a file is executable
91    fn is_executable(&self, path: &Path) -> bool {
92        #[cfg(unix)]
93        {
94            use std::os::unix::fs::PermissionsExt;
95            if let Ok(metadata) = std::fs::metadata(path) {
96                let permissions = metadata.permissions();
97                permissions.mode() & 0o111 != 0
98            } else {
99                false
100            }
101        }
102
103        #[cfg(windows)]
104        {
105            // On Windows, check file extension
106            if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
107                matches!(ext.to_lowercase().as_str(), "exe" | "bat" | "cmd" | "com")
108            } else {
109                false
110            }
111        }
112
113        #[cfg(not(any(unix, windows)))]
114        {
115            // Fallback: assume it's executable if it's a file
116            path.is_file()
117        }
118    }
119
120    /// Make a file executable on Unix systems
121    #[cfg(unix)]
122    fn make_executable(&self, path: &Path) -> Result<()> {
123        use std::os::unix::fs::PermissionsExt;
124
125        let metadata = std::fs::metadata(path)?;
126        let mut permissions = metadata.permissions();
127        permissions.set_mode(0o755);
128        std::fs::set_permissions(path, permissions)?;
129
130        Ok(())
131    }
132
133    /// Make a file executable (no-op on Windows)
134    #[cfg(not(unix))]
135    fn make_executable(&self, _path: &Path) -> Result<()> {
136        Ok(())
137    }
138}
139
140/// Archive extractor that delegates to specific format handlers
141pub struct ArchiveExtractor {
142    handlers: Vec<Box<dyn FormatHandler>>,
143}
144
145impl ArchiveExtractor {
146    /// Create a new archive extractor with default handlers
147    pub fn new() -> Self {
148        let handlers: Vec<Box<dyn FormatHandler>> = vec![
149            Box::new(zip::ZipHandler::new()),
150            Box::new(tar::TarHandler::new()),
151            Box::new(binary::BinaryHandler::new()),
152        ];
153
154        Self { handlers }
155    }
156
157    /// Add a custom format handler
158    pub fn with_handler(mut self, handler: Box<dyn FormatHandler>) -> Self {
159        self.handlers.push(handler);
160        self
161    }
162
163    /// Extract an archive using the appropriate handler
164    pub async fn extract(
165        &self,
166        source_path: &Path,
167        target_dir: &Path,
168        progress: &ProgressContext,
169    ) -> Result<Vec<PathBuf>> {
170        // Find a handler that can process this file
171        for handler in &self.handlers {
172            if handler.can_handle(source_path) {
173                return handler.extract(source_path, target_dir, progress).await;
174            }
175        }
176
177        Err(Error::unsupported_format(
178            source_path
179                .extension()
180                .and_then(|e| e.to_str())
181                .unwrap_or("unknown"),
182        ))
183    }
184
185    /// Find the best executable from extracted files
186    pub fn find_best_executable(
187        &self,
188        extracted_files: &[PathBuf],
189        tool_name: &str,
190    ) -> Result<PathBuf> {
191        let exe_name = if cfg!(windows) {
192            format!("{}.exe", tool_name)
193        } else {
194            tool_name.to_string()
195        };
196
197        // First, look for exact matches
198        for file in extracted_files {
199            if let Some(filename) = file.file_name().and_then(|n| n.to_str()) {
200                if filename == exe_name {
201                    return Ok(file.clone());
202                }
203            }
204        }
205
206        // Then, look for partial matches
207        for file in extracted_files {
208            if let Some(filename) = file.file_name().and_then(|n| n.to_str()) {
209                if filename.starts_with(tool_name) && self.is_executable_file(file) {
210                    return Ok(file.clone());
211                }
212            }
213        }
214
215        // Finally, look for any executable in bin directories
216        for file in extracted_files {
217            if let Some(parent) = file.parent() {
218                if let Some(dir_name) = parent.file_name().and_then(|n| n.to_str()) {
219                    if dir_name == "bin" && self.is_executable_file(file) {
220                        return Ok(file.clone());
221                    }
222                }
223            }
224        }
225
226        Err(Error::executable_not_found(
227            tool_name,
228            extracted_files
229                .first()
230                .and_then(|p| p.parent())
231                .unwrap_or_else(|| Path::new(".")),
232        ))
233    }
234
235    /// Check if a file is executable
236    fn is_executable_file(&self, path: &Path) -> bool {
237        #[cfg(unix)]
238        {
239            use std::os::unix::fs::PermissionsExt;
240            if let Ok(metadata) = std::fs::metadata(path) {
241                let permissions = metadata.permissions();
242                permissions.mode() & 0o111 != 0
243            } else {
244                false
245            }
246        }
247
248        #[cfg(windows)]
249        {
250            if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
251                matches!(ext.to_lowercase().as_str(), "exe" | "bat" | "cmd" | "com")
252            } else {
253                false
254            }
255        }
256
257        #[cfg(not(any(unix, windows)))]
258        {
259            path.is_file()
260        }
261    }
262}
263
264impl Default for ArchiveExtractor {
265    fn default() -> Self {
266        Self::new()
267    }
268}
269
270/// Utility function to detect archive format from file extension
271pub fn detect_format(file_path: &Path) -> Option<&str> {
272    let filename = file_path.file_name()?.to_str()?;
273
274    if filename.ends_with(".tar.gz") || filename.ends_with(".tgz") {
275        Some("tar.gz")
276    } else if filename.ends_with(".tar.xz") || filename.ends_with(".txz") {
277        Some("tar.xz")
278    } else if filename.ends_with(".tar.bz2") || filename.ends_with(".tbz2") {
279        Some("tar.bz2")
280    } else if filename.ends_with(".zip") {
281        Some("zip")
282    } else if filename.ends_with(".7z") {
283        Some("7z")
284    } else {
285        file_path.extension()?.to_str()
286    }
287}