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).map_err(JavaError::IoError)?;
70
71 let (java_home, exec_path) = if canonical_path.is_file() && is_executable(&canonical_path) {
72 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 (canonical_path, None)
83 };
84
85 let java_exe = java_home
87 .join("bin")
88 .join(if cfg!(windows) { "java.exe" } else { "java" });
89 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 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 info.is_complete() {
119 return Ok(info);
120 }
121
122 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
172fn 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
188struct 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
226struct 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 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 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 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 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 let name = vendor.clone();
305
306 Ok(VersionInfo {
307 name,
308 version,
309 vendor,
310 arch,
311 })
312}