Skip to main content

java_manager/
info.rs

1//! Core type representing a Java installation and its metadata.
2
3use crate::JavaError;
4use is_executable::is_executable;
5use std::collections::HashMap;
6use std::fs::{self, File};
7use std::io::{BufRead, BufReader};
8use std::path::{Path, PathBuf};
9use std::process::Command;
10use std::str;
11
12const UNKNOWN: &str = "UNKNOWN";
13
14/// Represents a discovered Java installation.
15///
16/// This struct holds metadata about a Java runtime, such as its version,
17/// vendor, architecture, and the location of its `java` executable and
18/// `JAVA_HOME` directory.
19#[derive(Debug)]
20pub struct JavaInfo {
21    /// Human-readable name of the Java implementation (e.g., "OpenJDK").
22    pub name: String,
23    /// Version string (e.g., "11.0.2").
24    pub version: String,
25    /// Full path to the `java` executable (or the path originally provided).
26    pub path: PathBuf,
27    /// Vendor name (e.g., "Oracle", "OpenJDK").
28    pub vendor: String,
29    /// Architecture (e.g., "64-Bit", "32-Bit").
30    pub architecture: String,
31    /// The `JAVA_HOME` directory corresponding to this installation.
32    pub java_home: PathBuf,
33}
34
35impl JavaInfo {
36    /// Creates a new `JavaInfo` from a path pointing either to a `java` executable
37    /// or directly to a `JAVA_HOME` directory.
38    ///
39    /// The path is canonicalized, and if it is an executable, the `JAVA_HOME` is
40    /// located by walking up the directory tree until a `bin/java` (or `java.exe`)
41    /// is found. Metadata is then extracted from the `release` file inside
42    /// `JAVA_HOME`, and any missing fields are filled by running `java -version`.
43    ///
44    /// # Errors
45    ///
46    /// Returns `JavaError::InvalidJavaPath` if the path does not exist,
47    /// or if `JAVA_HOME` cannot be determined from an executable.
48    /// Returns other `JavaError` variants if I/O or command execution fails.
49    ///
50    /// # Examples
51    ///
52    /// ```no_run
53    /// use java_manager::JavaInfo;
54    ///
55    /// let info = JavaInfo::new("/usr/lib/jvm/java-11-openjdk/bin/java".into())?;
56    /// println!("Java version: {}", info.version);
57    /// # Ok::<_, java_manager::JavaError>(())
58    /// ```
59    pub fn new(path: String) -> Result<Self, JavaError> {
60        let path_obj = Path::new(&path);
61        if !path_obj.exists() {
62            return Err(JavaError::InvalidJavaPath(format!(
63                "Path does not exist: {}",
64                path
65            )));
66        }
67
68        // Resolve symlinks to get the real absolute path
69        let canonical_path = fs::canonicalize(path_obj).map_err(JavaError::IoError)?;
70
71        let (java_home, exec_path) = if canonical_path.is_file() && is_executable(&canonical_path) {
72            // It's an executable – locate JAVA_HOME by walking up the tree
73            let home = find_java_home_from_exe(&canonical_path).ok_or_else(|| {
74                JavaError::InvalidJavaPath(format!(
75                    "Unable to determine JAVA_HOME from executable: {}",
76                    canonical_path.display()
77                ))
78            })?;
79            (home, Some(canonical_path))
80        } else {
81            // Assume it's a directory (JAVA_HOME itself)
82            (canonical_path, None)
83        };
84
85        // Path to the java executable inside JAVA_HOME
86        let java_exe = java_home
87            .join("bin")
88            .join(if cfg!(windows) { "java.exe" } else { "java" });
89        // Store either the original executable path or the default one from bin
90        let stored_path = exec_path.unwrap_or_else(|| java_exe.clone());
91
92        let mut info = JavaInfo {
93            name: UNKNOWN.to_string(),
94            version: UNKNOWN.to_string(),
95            path: stored_path,
96            vendor: UNKNOWN.to_string(),
97            architecture: UNKNOWN.to_string(),
98            java_home,
99        };
100
101        // --- Step 1: read from release file (if possible) ---
102        if let Some(release) = read_release(&info.java_home) {
103            if let Some(name) = release.name {
104                info.name = name;
105            }
106            if let Some(version) = release.version {
107                info.version = version;
108            }
109            if let Some(vendor) = release.vendor {
110                info.vendor = vendor;
111            }
112            if let Some(arch) = release.arch {
113                info.architecture = arch;
114            }
115        }
116
117        // If all fields are known, we are done
118        if info.is_complete() {
119            return Ok(info);
120        }
121
122        // --- Step 2: fill missing fields from `java -version` ---
123        let version_info = read_version(&java_exe)?;
124
125        if info.name == UNKNOWN
126            && let Some(name) = version_info.name
127        {
128            info.name = name;
129        }
130        if info.version == UNKNOWN
131            && let Some(ver) = version_info.version
132        {
133            info.version = ver;
134        }
135        if info.vendor == UNKNOWN
136            && let Some(vend) = version_info.vendor
137        {
138            info.vendor = vend;
139        }
140        if info.architecture == UNKNOWN
141            && let Some(arch) = version_info.arch
142        {
143            info.architecture = arch;
144        }
145
146        Ok(info)
147    }
148}
149
150impl Default for JavaInfo {
151    fn default() -> Self {
152        Self {
153            name: UNKNOWN.to_string(),
154            version: UNKNOWN.to_string(),
155            path: PathBuf::new(),
156            vendor: UNKNOWN.to_string(),
157            architecture: UNKNOWN.to_string(),
158            java_home: PathBuf::new(),
159        }
160    }
161}
162
163impl JavaInfo {
164    fn is_complete(&self) -> bool {
165        self.name != UNKNOWN
166            && self.version != UNKNOWN
167            && self.vendor != UNKNOWN
168            && self.architecture != UNKNOWN
169    }
170}
171
172// -----------------------------------------------------------------------------
173// Helper: locate JAVA_HOME from a java executable path
174// -----------------------------------------------------------------------------
175fn find_java_home_from_exe(exec_path: &Path) -> Option<PathBuf> {
176    let mut current = exec_path.parent()?;
177    loop {
178        let bin_java = current
179            .join("bin")
180            .join(if cfg!(windows) { "java.exe" } else { "java" });
181        if bin_java.exists() && is_executable(&bin_java) {
182            return Some(current.to_path_buf());
183        }
184        current = current.parent()?;
185    }
186}
187
188// -----------------------------------------------------------------------------
189// Data extracted from the release file
190// -----------------------------------------------------------------------------
191struct ReleaseInfo {
192    name: Option<String>,
193    version: Option<String>,
194    vendor: Option<String>,
195    arch: Option<String>,
196}
197
198fn read_release(java_home: &Path) -> Option<ReleaseInfo> {
199    let release_path = java_home.join("release");
200    let file = File::open(release_path).ok()?;
201    let reader = BufReader::new(file);
202    let mut properties = HashMap::new();
203
204    for line in reader.lines() {
205        let line = line.ok()?;
206        let line = line.trim();
207        if line.is_empty() || line.starts_with('#') {
208            continue;
209        }
210        let mut parts = line.splitn(2, '=');
211        if let (Some(key), Some(value)) = (parts.next(), parts.next()) {
212            let key = key.trim().to_string();
213            let value = value.trim().trim_matches('"').to_string();
214            properties.insert(key, value);
215        }
216    }
217
218    Some(ReleaseInfo {
219        name: properties.get("IMPLEMENTOR").cloned(),
220        version: properties.get("JAVA_VERSION").cloned(),
221        vendor: properties.get("IMPLEMENTOR").cloned(),
222        arch: properties.get("OS_ARCH").cloned(),
223    })
224}
225
226// -----------------------------------------------------------------------------
227// Data extracted from `java -version` output
228// -----------------------------------------------------------------------------
229struct VersionInfo {
230    name: Option<String>,
231    version: Option<String>,
232    vendor: Option<String>,
233    arch: Option<String>,
234}
235
236fn read_version(java_exe: &Path) -> Result<VersionInfo, JavaError> {
237    let output = Command::new(java_exe)
238        .arg("-version")
239        .output()
240        .map_err(|e| JavaError::ExecuteError(format!("Failed to execute java -version: {}", e)))?;
241
242    if !output.status.success() {
243        return Err(JavaError::ExecuteError(format!(
244            "java -version command failed with status: {}",
245            output.status
246        )));
247    }
248
249    let stderr = str::from_utf8(&output.stderr).map_err(|e| {
250        JavaError::RuntimeError(format!("Failed to decode java -version output: {}", e))
251    })?;
252
253    let mut version = None;
254    let mut vendor = None;
255    let mut arch = None;
256
257    for line in stderr.lines() {
258        // Extract version from lines like `openjdk version "11.0.2" 2019-01-15`
259        if line.contains(" version ")
260            && let Some(start) = line.find('"')
261            && let Some(end) = line[start + 1..].find('"')
262        {
263            version = Some(line[start + 1..start + 1 + end].to_string());
264        }
265
266        // Extract vendor from "Runtime Environment" line
267        if line.contains("Runtime Environment")
268            && let Some(idx) = line.find("Runtime Environment")
269        {
270            let rest = &line[idx + "Runtime Environment".len()..];
271            let vendor_part = rest.split_whitespace().next().unwrap_or("");
272            let vendor_cleaned = vendor_part
273                .split(['-', '('])
274                .next()
275                .unwrap_or("")
276                .to_string();
277            if !vendor_cleaned.is_empty() {
278                vendor = Some(vendor_cleaned);
279            }
280        }
281
282        // Extract architecture (64‑Bit / 32‑Bit)
283        if line.contains("VM") && line.contains("Bit") {
284            if line.contains("64-Bit") {
285                arch = Some("64-Bit".to_string());
286            } else if line.contains("32-Bit") {
287                arch = Some("32-Bit".to_string());
288            }
289        }
290    }
291
292    // Fallback vendor from first line
293    if vendor.is_none()
294        && let Some(first_line) = stderr.lines().next()
295    {
296        if first_line.starts_with("openjdk") {
297            vendor = Some("OpenJDK".to_string());
298        } else if first_line.starts_with("java") {
299            vendor = Some("Oracle".to_string());
300        }
301    }
302
303    // Name is derived from vendor (keeps original behaviour)
304    let name = vendor.clone();
305
306    Ok(VersionInfo {
307        name,
308        version,
309        vendor,
310        arch,
311    })
312}