vx_installer/formats/
binary.rs1use super::FormatHandler;
4use crate::{progress::ProgressContext, Result};
5use std::path::{Path, PathBuf};
6
7pub struct BinaryHandler;
9
10impl BinaryHandler {
11 pub fn new() -> Self {
13 Self
14 }
15
16 fn is_likely_binary(&self, file_path: &Path) -> bool {
18 if let Some(ext) = file_path.extension().and_then(|e| e.to_str()) {
20 let ext_lower = ext.to_lowercase();
21
22 if cfg!(windows) && matches!(ext_lower.as_str(), "exe" | "msi" | "bat" | "cmd") {
24 return true;
25 }
26
27 if matches!(ext_lower.as_str(), "bin" | "run" | "app") {
29 return true;
30 }
31 }
32
33 if let Some(filename) = file_path.file_name().and_then(|n| n.to_str()) {
35 if !filename.contains('.') && !cfg!(windows) {
37 return true;
38 }
39 }
40
41 false
42 }
43
44 fn get_target_name(&self, tool_name: &str, source_path: &Path) -> String {
46 if let Some(filename) = source_path.file_name().and_then(|n| n.to_str()) {
48 let name_without_ext =
50 if let Some(stem) = source_path.file_stem().and_then(|s| s.to_str()) {
51 stem
52 } else {
53 filename
54 };
55
56 if name_without_ext.starts_with(tool_name) {
57 if cfg!(windows) && !filename.ends_with(".exe") {
59 return format!("{}.exe", filename);
60 }
61 return filename.to_string();
62 }
63 }
64
65 self.get_executable_name(tool_name)
67 }
68}
69
70#[async_trait::async_trait]
71impl FormatHandler for BinaryHandler {
72 fn name(&self) -> &str {
73 "binary"
74 }
75
76 fn can_handle(&self, file_path: &Path) -> bool {
77 self.is_likely_binary(file_path)
80 }
81
82 async fn extract(
83 &self,
84 source_path: &Path,
85 target_dir: &Path,
86 progress: &ProgressContext,
87 ) -> Result<Vec<PathBuf>> {
88 let bin_dir = target_dir.join("bin");
90 std::fs::create_dir_all(&bin_dir)?;
91
92 progress.start("Installing binary", Some(1)).await?;
93
94 let tool_name = target_dir
96 .parent()
97 .and_then(|p| p.file_name())
98 .and_then(|n| n.to_str())
99 .unwrap_or("tool");
100
101 let target_name = self.get_target_name(tool_name, source_path);
102 let target_path = bin_dir.join(target_name);
103
104 std::fs::copy(source_path, &target_path)?;
106
107 self.make_executable(&target_path)?;
109
110 progress.increment(1).await?;
111 progress.finish("Binary installation completed").await?;
112
113 Ok(vec![target_path])
114 }
115}
116
117impl Default for BinaryHandler {
118 fn default() -> Self {
119 Self::new()
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use crate::progress::ProgressContext;
127 use std::io::Write;
128 use tempfile::TempDir;
129
130 #[tokio::test]
131 async fn test_binary_handler_name() {
132 let handler = BinaryHandler::new();
133 assert_eq!(handler.name(), "binary");
134 }
135
136 #[test]
137 fn test_is_likely_binary() {
138 let handler = BinaryHandler::new();
139
140 if cfg!(windows) {
142 assert!(handler.is_likely_binary(Path::new("tool.exe")));
143 assert!(handler.is_likely_binary(Path::new("installer.msi")));
144 assert!(handler.is_likely_binary(Path::new("script.bat")));
145 }
146
147 assert!(handler.is_likely_binary(Path::new("tool.bin")));
149 assert!(handler.is_likely_binary(Path::new("app.run")));
150
151 if !cfg!(windows) {
153 assert!(handler.is_likely_binary(Path::new("node")));
154 assert!(handler.is_likely_binary(Path::new("go")));
155 }
156
157 assert!(!handler.is_likely_binary(Path::new("archive.zip")));
159 assert!(!handler.is_likely_binary(Path::new("source.tar.gz")));
160 assert!(!handler.is_likely_binary(Path::new("readme.txt")));
161 }
162
163 #[test]
164 fn test_get_target_name() {
165 let handler = BinaryHandler::new();
166
167 let expected = if cfg!(windows) {
169 "node-v18.17.0.exe"
170 } else {
171 "node-v18.17.0"
172 };
173 assert_eq!(
174 handler.get_target_name("node", Path::new("node-v18.17.0")),
175 expected
176 );
177
178 if cfg!(windows) {
180 assert_eq!(
181 handler.get_target_name("go", Path::new("golang.exe")),
182 "golang.exe" );
184 } else {
185 assert_eq!(handler.get_target_name("go", Path::new("golang")), "golang");
186 }
187
188 if cfg!(windows) {
190 assert_eq!(
191 handler.get_target_name("go", Path::new("python.exe")),
192 "go.exe" );
194 } else {
195 assert_eq!(handler.get_target_name("go", Path::new("python")), "go");
196 }
197 }
198
199 #[tokio::test]
200 async fn test_binary_extraction() {
201 let handler = BinaryHandler::new();
202 let temp_dir = TempDir::new().unwrap();
203 let source_dir = temp_dir.path().join("source");
204 let target_dir = temp_dir.path().join("target").join("tool").join("1.0.0");
205
206 std::fs::create_dir_all(&source_dir).unwrap();
207
208 let source_file = source_dir.join("tool");
210 let mut file = std::fs::File::create(&source_file).unwrap();
211 file.write_all(b"#!/bin/bash\necho 'Hello World'").unwrap();
212
213 let progress = ProgressContext::disabled();
214
215 let result = handler.extract(&source_file, &target_dir, &progress).await;
216 assert!(result.is_ok());
217
218 let extracted_files = result.unwrap();
219 assert_eq!(extracted_files.len(), 1);
220
221 let expected_path =
222 target_dir
223 .join("bin")
224 .join(if cfg!(windows) { "tool.exe" } else { "tool" });
225 assert_eq!(extracted_files[0], expected_path);
226 assert!(expected_path.exists());
227 }
228}