vx_installer/formats/
mod.rs1use crate::{progress::ProgressContext, Error, Result};
8use std::path::{Path, PathBuf};
9
10pub mod binary;
11pub mod tar;
12pub mod zip;
13
14#[async_trait::async_trait]
16pub trait FormatHandler: Send + Sync {
17 fn name(&self) -> &str;
19
20 fn can_handle(&self, file_path: &Path) -> bool;
22
23 async fn extract(
25 &self,
26 source_path: &Path,
27 target_dir: &Path,
28 progress: &ProgressContext,
29 ) -> Result<Vec<PathBuf>>;
30
31 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 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 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 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 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 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 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 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 path.is_file()
117 }
118 }
119
120 #[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 #[cfg(not(unix))]
135 fn make_executable(&self, _path: &Path) -> Result<()> {
136 Ok(())
137 }
138}
139
140pub struct ArchiveExtractor {
142 handlers: Vec<Box<dyn FormatHandler>>,
143}
144
145impl ArchiveExtractor {
146 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 pub fn with_handler(mut self, handler: Box<dyn FormatHandler>) -> Self {
159 self.handlers.push(handler);
160 self
161 }
162
163 pub async fn extract(
165 &self,
166 source_path: &Path,
167 target_dir: &Path,
168 progress: &ProgressContext,
169 ) -> Result<Vec<PathBuf>> {
170 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 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 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 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 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 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
270pub 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}