1use 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#[derive(Debug)]
20pub struct JavaInfo {
21 pub name: String,
23 pub version: String,
25 pub path: PathBuf,
27 pub vendor: String,
29 pub architecture: String,
31 pub java_home: PathBuf,
33}
34
35impl JavaInfo {
36 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 let canonical_path = fs::canonicalize(path_obj)
70 .map_err(|e| JavaError::IoError(e))?;
71
72 let (java_home, exec_path) = if canonical_path.is_file() && is_executable(&canonical_path) {
73 let home = find_java_home_from_exe(&canonical_path)
75 .ok_or_else(|| JavaError::InvalidJavaPath(format!(
76 "Unable to determine JAVA_HOME from executable: {}",
77 canonical_path.display()
78 )))?;
79 (home, Some(canonical_path))
80 } else {
81 (canonical_path, None)
83 };
84
85 let java_exe = java_home.join("bin").join(if cfg!(windows) { "java.exe" } else { "java" });
87 let stored_path = exec_path.unwrap_or_else(|| java_exe.clone());
89
90 let mut info = JavaInfo {
91 name: UNKNOWN.to_string(),
92 version: UNKNOWN.to_string(),
93 path: stored_path,
94 vendor: UNKNOWN.to_string(),
95 architecture: UNKNOWN.to_string(),
96 java_home,
97 };
98
99 if let Some(release) = read_release(&info.java_home) {
101 if let Some(name) = release.name {
102 info.name = name;
103 }
104 if let Some(version) = release.version {
105 info.version = version;
106 }
107 if let Some(vendor) = release.vendor {
108 info.vendor = vendor;
109 }
110 if let Some(arch) = release.arch {
111 info.architecture = arch;
112 }
113 }
114
115 if info.is_complete() {
117 return Ok(info);
118 }
119
120 let version_info = read_version(&java_exe)?;
122
123 if info.name == UNKNOWN {
124 if let Some(name) = version_info.name {
125 info.name = name;
126 }
127 }
128 if info.version == UNKNOWN {
129 if let Some(ver) = version_info.version {
130 info.version = ver;
131 }
132 }
133 if info.vendor == UNKNOWN {
134 if let Some(vend) = version_info.vendor {
135 info.vendor = vend;
136 }
137 }
138 if info.architecture == UNKNOWN {
139 if let Some(arch) = version_info.arch {
140 info.architecture = arch;
141 }
142 }
143
144 Ok(info)
145 }
146
147 pub fn default() -> Self {
151 Self {
152 name: UNKNOWN.to_string(),
153 version: UNKNOWN.to_string(),
154 path: PathBuf::new(),
155 vendor: UNKNOWN.to_string(),
156 architecture: UNKNOWN.to_string(),
157 java_home: PathBuf::new(),
158 }
159 }
160
161 fn is_complete(&self) -> bool {
164 self.name != UNKNOWN
165 && self.version != UNKNOWN
166 && self.vendor != UNKNOWN
167 && self.architecture != UNKNOWN
168 }
169}
170
171fn find_java_home_from_exe(exec_path: &Path) -> Option<PathBuf> {
175 let mut current = exec_path.parent()?;
176 loop {
177 let bin_java = current.join("bin").join(if cfg!(windows) { "java.exe" } else { "java" });
178 if bin_java.exists() && is_executable(&bin_java) {
179 return Some(current.to_path_buf());
180 }
181 current = current.parent()?;
182 }
183}
184
185struct ReleaseInfo {
189 name: Option<String>,
190 version: Option<String>,
191 vendor: Option<String>,
192 arch: Option<String>,
193}
194
195fn read_release(java_home: &Path) -> Option<ReleaseInfo> {
196 let release_path = java_home.join("release");
197 let file = File::open(release_path).ok()?;
198 let reader = BufReader::new(file);
199 let mut properties = HashMap::new();
200
201 for line in reader.lines() {
202 let line = line.ok()?;
203 let line = line.trim();
204 if line.is_empty() || line.starts_with('#') {
205 continue;
206 }
207 let mut parts = line.splitn(2, '=');
208 if let (Some(key), Some(value)) = (parts.next(), parts.next()) {
209 let key = key.trim().to_string();
210 let value = value.trim().trim_matches('"').to_string();
211 properties.insert(key, value);
212 }
213 }
214
215 Some(ReleaseInfo {
216 name: properties.get("IMPLEMENTOR").cloned(),
217 version: properties.get("JAVA_VERSION").cloned(),
218 vendor: properties.get("IMPLEMENTOR").cloned(),
219 arch: properties.get("OS_ARCH").cloned(),
220 })
221}
222
223struct VersionInfo {
227 name: Option<String>,
228 version: Option<String>,
229 vendor: Option<String>,
230 arch: Option<String>,
231}
232
233fn read_version(java_exe: &Path) -> Result<VersionInfo, JavaError> {
234 let output = Command::new(java_exe)
235 .arg("-version")
236 .output()
237 .map_err(|e| JavaError::ExecuteError(format!("Failed to execute java -version: {}", e)))?;
238
239 if !output.status.success() {
240 return Err(JavaError::ExecuteError(format!(
241 "java -version command failed with status: {}",
242 output.status
243 )));
244 }
245
246 let stderr = str::from_utf8(&output.stderr)
247 .map_err(|e| JavaError::RuntimeError(format!("Failed to decode java -version output: {}", e)))?;
248
249 let mut version = None;
250 let mut vendor = None;
251 let mut arch = None;
252
253 for line in stderr.lines() {
254 if line.contains(" version ") {
256 if let Some(start) = line.find('"') {
257 if let Some(end) = line[start + 1..].find('"') {
258 version = Some(line[start + 1..start + 1 + end].to_string());
259 }
260 }
261 }
262
263 if line.contains("Runtime Environment") {
265 if let Some(idx) = line.find("Runtime Environment") {
266 let rest = &line[idx + "Runtime Environment".len()..];
267 let vendor_part = rest.split_whitespace().next().unwrap_or("");
268 let vendor_cleaned = vendor_part
269 .split(|c| c == '-' || c == '(')
270 .next()
271 .unwrap_or("")
272 .to_string();
273 if !vendor_cleaned.is_empty() {
274 vendor = Some(vendor_cleaned);
275 }
276 }
277 }
278
279 if line.contains("VM") && line.contains("Bit") {
281 if line.contains("64-Bit") {
282 arch = Some("64-Bit".to_string());
283 } else if line.contains("32-Bit") {
284 arch = Some("32-Bit".to_string());
285 }
286 }
287 }
288
289 if vendor.is_none() {
291 if let Some(first_line) = stderr.lines().next() {
292 if first_line.starts_with("openjdk") {
293 vendor = Some("OpenJDK".to_string());
294 } else if first_line.starts_with("java") {
295 vendor = Some("Oracle".to_string());
296 }
297 }
298 }
299
300 let name = vendor.clone();
302
303 Ok(VersionInfo {
304 name,
305 version,
306 vendor,
307 arch,
308 })
309}