Skip to main content

java_manager/
utils.rs

1// Copyright 2026 TaimWay
2//
3// @file: utils.rs
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17use std::process::Command;
18use std::str;
19
20use crate::errors::{JavaLocatorError, Result};
21use crate::info::JavaInfo;
22
23/// Determines the architecture (32-bit or 64-bit) of a Java installation.
24///
25/// This function runs various Java commands to determine the architecture.
26/// It tries multiple approaches:
27/// 1. Attempts to run Java with `-d64` flag
28/// 2. Attempts to run Java with `-d32` flag
29/// 3. Parses system properties from `-XshowSettings:properties`
30///
31/// # Arguments
32///
33/// * `java_path` - Path to the Java executable
34///
35/// # Returns
36///
37/// - `Ok(String)` containing "64-bit", "32-bit", or "Unknown"
38/// - `Err(JavaLocatorError)` if the command fails or output cannot be parsed
39///
40/// # Examples
41///
42/// ```rust
43/// use java_manager;
44///
45/// fn main() -> java_manager::Result<()> {
46///     let java_path = "/usr/bin/java";
47///     let arch = java_manager::get_java_architecture(java_path)?;
48///     println!("Java architecture: {}", arch);
49///     Ok(())
50/// }
51/// ```
52pub fn get_java_architecture(java_path: &str) -> Result<String> {
53    // Try -d64 flag
54    let output = Command::new(java_path)
55        .arg("-d64")
56        .arg("-version")
57        .output()
58        .map_err(|e| JavaLocatorError::new(format!("Failed to run Java command: {}", e)))?;
59
60    if output.status.success() {
61        return Ok("64-bit".to_string());
62    }
63
64    // Try -d32 flag
65    let output = Command::new(java_path)
66        .arg("-d32")
67        .arg("-version")
68        .output()
69        .map_err(|e| JavaLocatorError::new(format!("Failed to run Java command: {}", e)))?;
70
71    if output.status.success() {
72        return Ok("32-bit".to_string());
73    }
74
75    // Try to get architecture from system properties
76    let output = Command::new(java_path)
77        .arg("-XshowSettings:properties")
78        .arg("-version")
79        .output()
80        .map_err(|e| JavaLocatorError::new(format!("Failed to run Java command: {}", e)))?;
81
82    let output_str = str::from_utf8(&output.stderr)?;
83    
84    for line in output_str.lines() {
85        if line.contains("os.arch") {
86            let parts: Vec<&str> = line.split('=').collect();
87            if parts.len() == 2 {
88                let arch = parts[1].trim();
89                return if arch.contains("64") {
90                    Ok("64-bit".to_string())
91                } else {
92                    Ok("32-bit".to_string())
93                };
94            }
95        }
96    }
97
98    Ok("Unknown".to_string())
99}
100
101/// Extracts the version string from a Java installation.
102///
103/// Runs `java -version` and parses the output to extract the version string.
104/// Supports various version string formats from different Java vendors.
105///
106/// # Arguments
107///
108/// * `java_path` - Path to the Java executable
109///
110/// # Returns
111///
112/// - `Ok(String)` containing the version string (e.g., "11.0.12", "1.8.0_312")
113/// - `Err(JavaLocatorError)` if version cannot be determined
114///
115/// # Examples
116///
117/// ```rust
118/// use java_manager;
119///
120/// fn main() -> java_manager::Result<()> {
121///     let java_path = "/usr/bin/java";
122///     let version = java_manager::get_java_version(java_path)?;
123///     println!("Java version: {}", version);
124///     Ok(())
125/// }
126/// ```
127pub fn get_java_version(java_path: &str) -> Result<String> {
128    let output = Command::new(java_path)
129        .arg("-version")
130        .output()
131        .map_err(|e| JavaLocatorError::new(format!("Failed to run Java command: {}", e)))?;
132
133    let output_str = str::from_utf8(&output.stderr)?;
134    
135    // Try various version string patterns
136    for line in output_str.lines() {
137        // Check for common version string patterns
138        if line.starts_with("java version") 
139            || line.starts_with("openjdk version") 
140            || line.starts_with("java version")
141            || line.contains("version \"")
142        {
143            // Extract version string using more robust parsing
144            let line = line.trim();
145            
146            // Find the version within quotes
147            if let Some(start) = line.find('\"') {
148                if let Some(end) = line[start + 1..].find('\"') {
149                    let version = &line[start + 1..start + 1 + end];
150                    return Ok(version.to_string());
151                }
152            }
153            
154            // Fallback: split by whitespace
155            let parts: Vec<&str> = line.split_whitespace().collect();
156            for (i, part) in parts.iter().enumerate() {
157                if part.contains("version") && i + 1 < parts.len() {
158                    let version = parts[i + 1].trim_matches('\"');
159                    return Ok(version.to_string());
160                }
161            }
162            
163            // Last resort: take the third word
164            if parts.len() >= 3 {
165                let version = parts[2].trim_matches('\"');
166                return Ok(version.to_string());
167            }
168        }
169    }
170
171    Err(JavaLocatorError::new(
172        "Could not determine Java version".to_string(),
173    ))
174}
175
176/// Identifies the supplier/vendor of a Java installation.
177///
178/// Analyzes the output of `java -version` to determine the Java supplier.
179/// Supports various Java vendors including OpenJDK, Oracle, IBM, Azul, etc.
180///
181/// # Arguments
182///
183/// * `java_path` - Path to the Java executable
184///
185/// # Returns
186///
187/// - `Ok(String)` containing the supplier name
188/// - `Err(JavaLocatorError)` if supplier cannot be determined
189///
190/// # Examples
191///
192/// ```rust
193/// use java_manager;
194///
195/// fn main() -> java_manager::Result<()> {
196///     let java_path = "/usr/bin/java";
197///     let supplier = java_manager::get_java_suppliers(java_path)?;
198///     println!("Java supplier: {}", supplier);
199///     Ok(())
200/// }
201/// ```
202pub fn get_java_suppliers(java_path: &str) -> Result<String> {
203    let output = Command::new(java_path)
204        .arg("-version")
205        .output()
206        .map_err(|e| JavaLocatorError::new(format!("Failed to run Java command: {}", e)))?;
207
208    let output_str = str::from_utf8(&output.stderr)?;
209    
210    // Check for specific vendor patterns in the output
211    for line in output_str.lines() {
212        let line_lower = line.to_lowercase();
213        
214        if line_lower.contains("openjdk") && !line_lower.contains("adopt") {
215            return Ok("OpenJDK".to_string());
216        } else if line_lower.contains("oracle") {
217            return Ok("Oracle".to_string());
218        } else if line_lower.contains("ibm") {
219            return Ok("IBM".to_string());
220        } else if line_lower.contains("azul") {
221            return Ok("Azul".to_string());
222        } else if line_lower.contains("adoptopenjdk") || line_lower.contains("adoptium") {
223            return Ok("AdoptOpenJDK/Adoptium".to_string());
224        } else if line_lower.contains("amazon") || line_lower.contains("corretto") {
225            return Ok("Amazon Corretto".to_string());
226        } else if line_lower.contains("microsoft") {
227            return Ok("Microsoft".to_string());
228        } else if line_lower.contains("sap") {
229            return Ok("SAP".to_string());
230        } else if line_lower.contains("graalvm") {
231            return Ok("GraalVM".to_string());
232        } else if line_lower.contains("bellsoft") {
233            return Ok("BellSoft Liberica".to_string());
234        }
235    }
236
237    // Try to get vendor from system properties
238    let output = Command::new(java_path)
239        .arg("-XshowSettings:properties")
240        .arg("-version")
241        .output()
242        .map_err(|e| JavaLocatorError::new(format!("Failed to run Java command: {}", e)))?;
243
244    let output_str = str::from_utf8(&output.stderr)?;
245    
246    for line in output_str.lines() {
247        if line.contains("java.vendor") {
248            let parts: Vec<&str> = line.split('=').collect();
249            if parts.len() == 2 {
250                return Ok(parts[1].trim().to_string());
251            }
252        }
253    }
254
255    Ok("Unknown".to_string())
256}
257
258/// Creates a comprehensive `JavaInfo` object for a Java installation.
259///
260/// This function gathers all information about a Java installation by
261/// calling the individual information extraction functions.
262///
263/// # Arguments
264///
265/// * `java_exec_path` - Path to the Java executable
266///
267/// # Returns
268///
269/// - `Ok(JavaInfo)` containing all Java information
270/// - `Err(JavaLocatorError)` if any information cannot be gathered
271///
272/// # Examples
273///
274/// ```rust
275/// use java_manager;
276///
277/// fn main() -> java_manager::Result<()> {
278///     let java_path = "/usr/bin/java";
279///     let info = java_manager::get_java_info(java_path)?;
280///     println!("Java Info: {}", info);
281///     Ok(())
282/// }
283/// ```
284pub fn get_java_info(java_exec_path: &str) -> Result<JavaInfo> {
285    let version = get_java_version(java_exec_path)?;
286    let architecture = get_java_architecture(java_exec_path)?;
287    let suppliers = get_java_suppliers(java_exec_path)?;
288    
289    let name = std::path::Path::new(java_exec_path)
290        .file_stem()
291        .and_then(|s| s.to_str())
292        .unwrap_or("java")
293        .to_string();
294
295    Ok(JavaInfo::new(
296        &name,
297        java_exec_path,
298        &version,
299        &architecture,
300        &suppliers,
301    ))
302}
303
304/// Validates that a Java executable exists and can be executed.
305///
306/// # Arguments
307///
308/// * `java_path` - Path to the Java executable
309///
310/// # Returns
311///
312/// - `Ok(())` if Java exists and can be executed
313/// - `Err(JavaLocatorError)` if Java cannot be found or executed
314///
315/// # Examples
316///
317/// ```rust
318/// use java_manager;
319///
320/// fn main() -> java_manager::Result<()> {
321///     let java_path = "/usr/bin/java";
322///     java_manager::validate_java_executable(java_path)?;
323///     println!("Java executable is valid");
324///     Ok(())
325/// }
326/// ```
327pub fn validate_java_executable(java_path: &str) -> Result<()> {
328    let path = std::path::Path::new(java_path);
329    
330    if !path.exists() {
331        return Err(JavaLocatorError::new(
332            format!("Java executable not found: {}", java_path)
333        ));
334    }
335    
336    // Try to execute java -version to verify it works
337    let output = Command::new(java_path)
338        .arg("-version")
339        .output()
340        .map_err(|e| JavaLocatorError::new(format!("Failed to execute Java: {}", e)))?;
341    
342    if !output.status.success() {
343        return Err(JavaLocatorError::new(
344            format!("Java executable failed to run: {}", java_path)
345        ));
346    }
347    
348    Ok(())
349}
350
351#[cfg(test)]
352mod tests {
353    use super::*;
354
355    /// Tests Java version extraction
356    #[test]
357    fn test_get_java_version() {
358        // Only run this test if Java is available
359        if let Ok(java_home) = crate::locate_java_home() {
360            let java_exec_path = format!("{}/bin/java", java_home);
361            if std::path::Path::new(&java_exec_path).exists() {
362                let version = get_java_version(&java_exec_path);
363                assert!(version.is_ok());
364                let version_str = version.unwrap();
365                println!("Java version: {}", version_str);
366                assert!(!version_str.is_empty());
367                
368                // Version string should contain at least one dot or underscore
369                assert!(version_str.contains('.') || version_str.contains('_') || 
370                       version_str.chars().any(|c| c.is_digit(10)));
371            }
372        }
373    }
374
375    /// Tests Java architecture detection
376    #[test]
377    fn test_get_java_architecture() {
378        if let Ok(java_home) = crate::locate_java_home() {
379            let java_exec_path = format!("{}/bin/java", java_home);
380            if std::path::Path::new(&java_exec_path).exists() {
381                let arch = get_java_architecture(&java_exec_path);
382                assert!(arch.is_ok());
383                let arch_str = arch.unwrap();
384                println!("Java architecture: {}", arch_str);
385                
386                // Should be one of these values
387                assert!(arch_str == "64-bit" || arch_str == "32-bit" || arch_str == "Unknown");
388            }
389        }
390    }
391
392    /// Tests Java supplier detection
393    #[test]
394    fn test_get_java_suppliers() {
395        if let Ok(java_home) = crate::locate_java_home() {
396            let java_exec_path = format!("{}/bin/java", java_home);
397            if std::path::Path::new(&java_exec_path).exists() {
398                let supplier = get_java_suppliers(&java_exec_path);
399                assert!(supplier.is_ok());
400                let supplier_str = supplier.unwrap();
401                println!("Java supplier: {}", supplier_str);
402                assert!(!supplier_str.is_empty());
403            }
404        }
405    }
406
407    /// Tests comprehensive Java info gathering
408    #[test]
409    fn test_get_java_info() {
410        if let Ok(java_home) = crate::locate_java_home() {
411            let java_exec_path = format!("{}/bin/java", java_home);
412            if std::path::Path::new(&java_exec_path).exists() {
413                let info = get_java_info(&java_exec_path);
414                assert!(info.is_ok());
415                let info = info.unwrap();
416                println!("Java Info: {:?}", info);
417                
418                // Verify all fields are populated
419                assert!(!info.name.is_empty());
420                assert!(!info.path.is_empty());
421                assert!(!info.version.is_empty());
422                assert!(!info.architecture.is_empty());
423                assert!(!info.suppliers.is_empty());
424            }
425        }
426    }
427
428    /// Tests Java executable validation
429    #[test]
430    fn test_validate_java_executable() {
431        // Test with system Java if available
432        if let Ok(java_home) = crate::locate_java_home() {
433            let java_exec_path = format!("{}/bin/java", java_home);
434            if std::path::Path::new(&java_exec_path).exists() {
435                let result = validate_java_executable(&java_exec_path);
436                assert!(result.is_ok());
437            }
438        }
439        
440        // Test with non-existent path
441        let result = validate_java_executable("/path/that/does/not/exist/java");
442        assert!(result.is_err());
443    }
444
445    /// Tests version parsing with various formats
446    #[test]
447    fn test_version_parsing() {
448        // Simulate different version string formats
449        let test_cases = vec![
450            ("java version \"1.8.0_312\"", "1.8.0_312"),
451            ("openjdk version \"11.0.12\" 2021-07-20", "11.0.12"),
452            ("java version \"17.0.1\" 2021-10-19 LTS", "17.0.1"),
453            ("openjdk version \"1.8.0_302\"", "1.8.0_302"),
454        ];
455        
456        // Note: This test doesn't actually run Java, just tests our understanding
457        // of the version string patterns
458        for (input, expected) in test_cases {
459            println!("Testing version parsing: {}", input);
460            // We can't easily test the actual function without running Java,
461            // but we can verify our understanding of the patterns
462        }
463    }
464
465    /// Tests supplier detection patterns
466    #[test]
467    fn test_supplier_patterns() {
468        let test_cases = vec![
469            ("OpenJDK Runtime Environment", "OpenJDK"),
470            ("Java(TM) SE Runtime Environment", "Oracle"),
471            ("IBM J9 VM", "IBM"),
472            ("Zulu", "Azul"),
473            ("AdoptOpenJDK", "AdoptOpenJDK/Adoptium"),
474            ("Corretto", "Amazon Corretto"),
475            ("Microsoft", "Microsoft"),
476        ];
477        
478        for (input, expected) in test_cases {
479            println!("Testing supplier pattern: {} -> {}", input, expected);
480            // This is just for documentation - actual detection happens in get_java_suppliers
481        }
482    }
483}