1use crate::JavaError;
4use is_executable::is_executable;
5use log::debug;
6use std::cmp::Ordering;
7use std::collections::HashMap;
8use std::fmt;
9use std::fs::{self, File};
10use std::io::{BufRead, BufReader};
11use std::path::{Path, PathBuf};
12use std::process::Command;
13use std::str;
14
15const UNKNOWN: &str = "UNKNOWN";
16
17#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct JavaVersion {
27 pub major: u64,
32
33 pub minor: u64,
37
38 pub patch: u64,
42
43 pub raw: String,
47}
48
49impl JavaVersion {
50 pub fn parse(raw: &str) -> Option<Self> {
53 debug!("JavaVersion::parse: raw input \"{raw}\"");
54 let body = if raw.len() > 2
55 && raw.starts_with("1.")
56 && raw.as_bytes().get(2).is_some_and(|b| b.is_ascii_digit())
57 {
58 &raw[2..]
59 } else {
60 raw
61 };
62
63 let parts: Vec<&str> = body
64 .split(|c: char| !c.is_ascii_digit())
65 .filter(|s| !s.is_empty())
66 .collect();
67
68 let major = parts.first()?.parse().ok()?;
69 let minor = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
70 let patch = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
71
72 let result = JavaVersion {
73 major,
74 minor,
75 patch,
76 raw: raw.to_string(),
77 };
78 debug!("JavaVersion::parse: \"{raw}\" -> {result}");
79 Some(result)
80 }
81}
82
83impl fmt::Display for JavaVersion {
84 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
85 if self.major == 0 && self.minor == 0 && self.patch == 0 && !self.raw.is_empty() {
86 return write!(f, "{}", self.raw);
87 }
88 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
89 }
90}
91
92impl PartialOrd for JavaVersion {
93 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
94 Some(self.cmp(other))
95 }
96}
97
98impl Ord for JavaVersion {
99 fn cmp(&self, other: &Self) -> Ordering {
100 (self.major, self.minor, self.patch).cmp(&(other.major, other.minor, other.patch))
101 }
102}
103
104#[derive(Debug)]
110pub struct JavaInfo {
111 pub name: String,
113 pub version: String,
115 pub parsed_version: Option<JavaVersion>,
117 pub path: PathBuf,
119 pub vendor: String,
121 pub architecture: String,
123 pub java_home: PathBuf,
125}
126
127impl JavaInfo {
128 pub fn new(path: String) -> Result<Self, JavaError> {
152 debug!("JavaInfo::new: resolving path \"{path}\"");
153 let path_obj = Path::new(&path);
154 if !path_obj.exists() {
155 debug!("JavaInfo::new: path does not exist: {path}");
156 return Err(JavaError::InvalidJavaPath(format!(
157 "Path does not exist: {}",
158 path
159 )));
160 }
161
162 let canonical_path = fs::canonicalize(path_obj).map_err(JavaError::IoError)?;
164 debug!(
165 "JavaInfo::new: canonical path: {}",
166 canonical_path.display()
167 );
168
169 let (java_home, exec_path) = if canonical_path.is_file() && is_executable(&canonical_path) {
170 let home = find_java_home_from_exe(&canonical_path).ok_or_else(|| {
172 JavaError::InvalidJavaPath(format!(
173 "Unable to determine JAVA_HOME from executable: {}",
174 canonical_path.display()
175 ))
176 })?;
177 (home, Some(canonical_path))
178 } else {
179 (canonical_path, None)
181 };
182
183 let java_exe = java_home
185 .join("bin")
186 .join(if cfg!(windows) { "java.exe" } else { "java" });
187 let stored_path = exec_path.unwrap_or_else(|| java_exe.clone());
189
190 let mut info = JavaInfo {
191 name: UNKNOWN.to_string(),
192 version: UNKNOWN.to_string(),
193 parsed_version: None,
194 path: stored_path,
195 vendor: UNKNOWN.to_string(),
196 architecture: UNKNOWN.to_string(),
197 java_home,
198 };
199
200 if let Some(release) = read_release(&info.java_home) {
202 if let Some(name) = release.name {
203 info.name = name;
204 }
205 if let Some(version) = release.version {
206 info.version = version;
207 }
208 if let Some(vendor) = release.vendor {
209 info.vendor = vendor;
210 }
211 if let Some(arch) = release.arch {
212 info.architecture = arch;
213 }
214 }
215
216 if info.version != UNKNOWN {
217 info.parsed_version = JavaVersion::parse(&info.version);
218 }
219
220 if info.is_complete() {
222 return Ok(info);
223 }
224
225 let version_info = read_version(&java_exe)?;
227
228 if info.name == UNKNOWN
229 && let Some(name) = version_info.name
230 {
231 info.name = name;
232 }
233 if info.version == UNKNOWN
234 && let Some(ver) = version_info.version
235 {
236 info.version = ver;
237 }
238 if info.vendor == UNKNOWN
239 && let Some(vend) = version_info.vendor
240 {
241 info.vendor = vend;
242 }
243 if info.architecture == UNKNOWN
244 && let Some(arch) = version_info.arch
245 {
246 info.architecture = arch;
247 }
248
249 if info.version != UNKNOWN {
250 info.parsed_version = JavaVersion::parse(&info.version);
251 }
252
253 Ok(info)
254 }
255}
256
257impl Default for JavaInfo {
258 fn default() -> Self {
259 Self {
260 name: UNKNOWN.to_string(),
261 version: UNKNOWN.to_string(),
262 parsed_version: None,
263 path: PathBuf::new(),
264 vendor: UNKNOWN.to_string(),
265 architecture: UNKNOWN.to_string(),
266 java_home: PathBuf::new(),
267 }
268 }
269}
270
271impl JavaInfo {
272 pub fn matches_version(&self, req: &str) -> bool {
297 let parsed = match &self.parsed_version {
298 Some(v) => v,
299 None => return false,
300 };
301
302 let req_parts: Vec<&str> = req.split('.').collect();
303 match req_parts.len() {
304 1 => req_parts[0].parse::<u64>().ok() == Some(parsed.major),
305 2 => {
306 let major = match req_parts[0].parse::<u64>() {
307 Ok(m) => m,
308 Err(_) => return false,
309 };
310 let minor = match req_parts[1].parse::<u64>() {
311 Ok(m) => m,
312 Err(_) => return false,
313 };
314 parsed.major == major && parsed.minor == minor
315 }
316 3 => {
317 let major = match req_parts[0].parse::<u64>() {
318 Ok(m) => m,
319 Err(_) => return false,
320 };
321 let minor = match req_parts[1].parse::<u64>() {
322 Ok(m) => m,
323 Err(_) => return false,
324 };
325 let patch = match req_parts[2].parse::<u64>() {
326 Ok(m) => m,
327 Err(_) => return false,
328 };
329 parsed.major == major && parsed.minor == minor && parsed.patch == patch
330 }
331 _ => false,
332 }
333 }
334
335 fn is_complete(&self) -> bool {
336 self.name != UNKNOWN
337 && self.version != UNKNOWN
338 && self.vendor != UNKNOWN
339 && self.architecture != UNKNOWN
340 }
341
342 pub fn is_jdk(&self) -> bool {
361 let javac =
362 self.java_home
363 .join("bin")
364 .join(if cfg!(windows) { "javac.exe" } else { "javac" });
365 javac.exists()
366 }
367
368 pub fn capabilities(&self) -> Vec<String> {
387 let bin_dir = self.java_home.join("bin");
388 let tools = [
389 "javac",
390 "javadoc",
391 "javap",
392 "jar",
393 "jlink",
394 "jmod",
395 "jpackage",
396 "jshell",
397 "jconsole",
398 "jcmd",
399 "jmap",
400 "jstack",
401 "jstat",
402 "jhsdb",
403 "jfr",
404 "jwebserver",
405 ];
406
407 let mut available = Vec::new();
408 if let Ok(entries) = std::fs::read_dir(&bin_dir) {
409 let names: Vec<String> = entries
410 .filter_map(|e| e.ok())
411 .filter_map(|e| {
412 let name = e.file_name().to_string_lossy().to_string();
413 if cfg!(windows) {
414 name.strip_suffix(".exe").map(|s| s.to_string())
416 } else {
417 Some(name)
418 }
419 })
420 .collect();
421
422 for tool in tools {
423 let tool_name = tool.to_string();
424 if names.contains(&tool_name) || names.contains(&format!("{}.exe", tool_name)) {
425 available.push(tool.to_string());
426 }
427 }
428 }
429 available
430 }
431}
432
433fn find_java_home_from_exe(exec_path: &Path) -> Option<PathBuf> {
437 let mut current = exec_path.parent()?;
438 loop {
439 let bin_java = current
440 .join("bin")
441 .join(if cfg!(windows) { "java.exe" } else { "java" });
442 if bin_java.exists() && is_executable(&bin_java) {
443 return Some(current.to_path_buf());
444 }
445 current = current.parent()?;
446 }
447}
448
449struct ReleaseInfo {
453 name: Option<String>,
454 version: Option<String>,
455 vendor: Option<String>,
456 arch: Option<String>,
457}
458
459fn read_release(java_home: &Path) -> Option<ReleaseInfo> {
460 let release_path = java_home.join("release");
461 debug!("read_release: checking {}", release_path.display());
462 let file = File::open(release_path).ok()?;
463 let reader = BufReader::new(file);
464 let mut properties = HashMap::new();
465
466 for line in reader.lines() {
467 let line = line.ok()?;
468 let line = line.trim();
469 if line.is_empty() || line.starts_with('#') {
470 continue;
471 }
472 let mut parts = line.splitn(2, '=');
473 if let (Some(key), Some(value)) = (parts.next(), parts.next()) {
474 let key = key.trim().to_string();
475 let value = value.trim().trim_matches('"').to_string();
476 properties.insert(key, value);
477 }
478 }
479
480 Some(ReleaseInfo {
481 name: properties.get("IMPLEMENTOR").cloned(),
482 version: properties.get("JAVA_VERSION").cloned(),
483 vendor: properties.get("IMPLEMENTOR").cloned(),
484 arch: properties.get("OS_ARCH").cloned(),
485 })
486}
487
488struct VersionInfo {
492 name: Option<String>,
493 version: Option<String>,
494 vendor: Option<String>,
495 arch: Option<String>,
496}
497
498fn read_version(java_exe: &Path) -> Result<VersionInfo, JavaError> {
499 debug!("read_version: running {}", java_exe.display());
500 let output = Command::new(java_exe)
501 .arg("-version")
502 .output()
503 .map_err(|e| JavaError::ExecuteError(format!("Failed to execute java -version: {}", e)))?;
504
505 if !output.status.success() {
506 return Err(JavaError::ExecuteError(format!(
507 "java -version command failed with status: {}",
508 output.status
509 )));
510 }
511
512 let stderr = str::from_utf8(&output.stderr).map_err(|e| {
513 JavaError::RuntimeError(format!("Failed to decode java -version output: {}", e))
514 })?;
515
516 let mut version = None;
517 let mut vendor = None;
518 let mut arch = None;
519
520 for line in stderr.lines() {
521 if line.contains(" version ")
523 && let Some(start) = line.find('"')
524 && let Some(end) = line[start + 1..].find('"')
525 {
526 version = Some(line[start + 1..start + 1 + end].to_string());
527 }
528
529 if line.contains("Runtime Environment")
531 && let Some(idx) = line.find("Runtime Environment")
532 {
533 let rest = &line[idx + "Runtime Environment".len()..];
534 let vendor_part = rest.split_whitespace().next().unwrap_or("");
535 let vendor_cleaned = vendor_part
536 .split(['-', '('])
537 .next()
538 .unwrap_or("")
539 .to_string();
540 if !vendor_cleaned.is_empty() {
541 vendor = Some(vendor_cleaned);
542 }
543 }
544
545 if line.contains("VM") && line.contains("Bit") {
547 if line.contains("64-Bit") {
548 arch = Some("64-Bit".to_string());
549 } else if line.contains("32-Bit") {
550 arch = Some("32-Bit".to_string());
551 }
552 }
553 }
554
555 if vendor.is_none()
557 && let Some(first_line) = stderr.lines().next()
558 {
559 if first_line.starts_with("openjdk") {
560 vendor = Some("OpenJDK".to_string());
561 } else if first_line.starts_with("java") {
562 vendor = Some("Oracle".to_string());
563 }
564 }
565
566 let name = vendor.clone();
568
569 Ok(VersionInfo {
570 name,
571 version,
572 vendor,
573 arch,
574 })
575}
576
577#[cfg(test)]
578mod tests {
579 use super::*;
580
581 #[test]
582 fn test_parse_java_8() {
583 let v = JavaVersion::parse("1.8.0_202-ea").unwrap();
584 assert_eq!(v.major, 8);
585 assert_eq!(v.minor, 0);
586 assert_eq!(v.patch, 202);
587 assert_eq!(v.raw, "1.8.0_202-ea");
588 }
589
590 #[test]
591 fn test_parse_java_11() {
592 let v = JavaVersion::parse("11.0.2").unwrap();
593 assert_eq!(v.major, 11);
594 assert_eq!(v.minor, 0);
595 assert_eq!(v.patch, 2);
596 }
597
598 #[test]
599 fn test_parse_java_17() {
600 let v = JavaVersion::parse("17.0.2+13-LTS").unwrap();
601 assert_eq!(v.major, 17);
602 assert_eq!(v.minor, 0);
603 assert_eq!(v.patch, 2);
604 }
605
606 #[test]
607 fn test_parse_java_21() {
608 let v = JavaVersion::parse("21.0.1+12").unwrap();
609 assert_eq!(v.major, 21);
610 assert_eq!(v.minor, 0);
611 assert_eq!(v.patch, 1);
612 }
613
614 #[test]
615 fn test_parse_ea_no_patch() {
616 let v = JavaVersion::parse("11-ea").unwrap();
617 assert_eq!(v.major, 11);
618 assert_eq!(v.minor, 0);
619 assert_eq!(v.patch, 0);
620 }
621
622 #[test]
623 fn test_parse_extra_segments() {
624 let v = JavaVersion::parse("17.0.8.1").unwrap();
625 assert_eq!(v.major, 17);
626 assert_eq!(v.minor, 0);
627 assert_eq!(v.patch, 8);
628 }
629
630 #[test]
631 fn test_parse_no_input() {
632 assert!(JavaVersion::parse("").is_none());
633 }
634
635 #[test]
636 fn test_parse_garbage() {
637 assert!(JavaVersion::parse("not.a.version").is_none());
638 }
639
640 #[test]
641 fn test_parse_letters_only() {
642 assert!(JavaVersion::parse("abc.def").is_none());
643 }
644
645 #[test]
646 fn test_display_normal() {
647 let v = JavaVersion::parse("11.0.2").unwrap();
648 assert_eq!(v.to_string(), "11.0.2");
649 }
650
651 #[test]
652 fn test_display_fallback_raw() {
653 let v = JavaVersion {
654 major: 0,
655 minor: 0,
656 patch: 0,
657 raw: "1.8.0_202".into(),
658 };
659 assert_eq!(v.to_string(), "1.8.0_202");
660 }
661
662 #[test]
663 fn test_matches_version_exact() {
664 let mut info = JavaInfo::default();
665 info.version = "11.0.2".into();
666 info.parsed_version = JavaVersion::parse("11.0.2");
667 assert!(info.matches_version("11.0.2"));
668 }
669
670 #[test]
671 fn test_matches_version_major_only() {
672 let mut info = JavaInfo::default();
673 info.version = "11.0.2".into();
674 info.parsed_version = JavaVersion::parse("11.0.2");
675 assert!(info.matches_version("11"));
676 }
677
678 #[test]
679 fn test_matches_version_major_minor() {
680 let mut info = JavaInfo::default();
681 info.version = "11.0.2".into();
682 info.parsed_version = JavaVersion::parse("11.0.2");
683 assert!(info.matches_version("11.0"));
684 }
685
686 #[test]
687 fn test_matches_version_no_match() {
688 let mut info = JavaInfo::default();
689 info.version = "11.0.2".into();
690 info.parsed_version = JavaVersion::parse("11.0.2");
691 assert!(!info.matches_version("17"));
692 }
693
694 #[test]
695 fn test_matches_version_unparseable() {
696 let info = JavaInfo::default();
697 assert!(!info.matches_version("11"));
698 }
699
700 #[test]
701 fn test_matches_version_invalid_req() {
702 let mut info = JavaInfo::default();
703 info.version = "11.0.2".into();
704 info.parsed_version = JavaVersion::parse("11.0.2");
705 assert!(!info.matches_version("abc"));
706 }
707
708 #[test]
709 fn test_matches_version_too_many_parts() {
710 let mut info = JavaInfo::default();
711 info.version = "11.0.2".into();
712 info.parsed_version = JavaVersion::parse("11.0.2");
713 assert!(!info.matches_version("11.0.2.1"));
714 }
715
716 #[test]
717 fn test_java_version_ordering() {
718 let v8 = JavaVersion::parse("1.8.0_202").unwrap();
719 let v11 = JavaVersion::parse("11.0.2").unwrap();
720 let v17 = JavaVersion::parse("17.0.1").unwrap();
721 assert!(v8 < v11);
722 assert!(v11 < v17);
723 assert!(v17 > v11);
724 }
725
726 #[test]
727 fn test_java_version_eq() {
728 let a = JavaVersion::parse("11.0.2").unwrap();
729 let b = JavaVersion::parse("11.0.2").unwrap();
730 assert_eq!(a, b);
731 }
732
733 #[test]
734 fn test_java_version_ordering_same_major() {
735 let a = JavaVersion::parse("11.0.2").unwrap();
736 let b = JavaVersion::parse("11.0.3").unwrap();
737 assert!(a < b);
738 assert!(b > a);
739 }
740
741 #[test]
742 fn test_java_info_default() {
743 let info = JavaInfo::default();
744 assert_eq!(info.name, "UNKNOWN");
745 assert_eq!(info.version, "UNKNOWN");
746 assert!(info.parsed_version.is_none());
747 assert!(info.path.to_string_lossy().is_empty());
748 }
749
750 #[test]
751 fn test_is_jdk_true() {
752 let dir = tempfile::tempdir().unwrap();
753 let bin = dir.path().join("bin");
754 std::fs::create_dir(&bin).unwrap();
755 let javac_path = bin.join(if cfg!(windows) { "javac.exe" } else { "javac" });
756 std::fs::write(&javac_path, "").unwrap();
757
758 let info = JavaInfo {
759 java_home: dir.path().to_path_buf(),
760 ..Default::default()
761 };
762 assert!(info.is_jdk());
763 }
764
765 #[test]
766 fn test_is_jdk_false() {
767 let dir = tempfile::tempdir().unwrap();
768 let info = JavaInfo {
769 java_home: dir.path().to_path_buf(),
770 ..Default::default()
771 };
772 assert!(!info.is_jdk());
773 }
774
775 #[test]
776 fn test_capabilities_returns_matching_tools() {
777 let dir = tempfile::tempdir().unwrap();
778 let bin = dir.path().join("bin");
779 std::fs::create_dir(&bin).unwrap();
780
781 let tool = if cfg!(windows) { "javac.exe" } else { "javac" };
782 std::fs::write(bin.join(tool), "").unwrap();
783
784 let other = if cfg!(windows) { "other.exe" } else { "other" };
786 std::fs::write(bin.join(other), "").unwrap();
787
788 let info = JavaInfo {
789 java_home: dir.path().to_path_buf(),
790 ..Default::default()
791 };
792 let caps = info.capabilities();
793 assert!(caps.contains(&"javac".to_string()));
794 assert!(!caps.contains(&"other".to_string()));
795 }
796
797 #[test]
798 fn test_capabilities_empty_dir() {
799 let dir = tempfile::tempdir().unwrap();
800 let info = JavaInfo {
801 java_home: dir.path().to_path_buf(),
802 ..Default::default()
803 };
804 assert!(info.capabilities().is_empty());
805 }
806
807 #[test]
808 fn test_capabilities_nonexistent_dir() {
809 let info = JavaInfo {
810 java_home: PathBuf::from("/nonexistent/path"),
811 ..Default::default()
812 };
813 assert!(info.capabilities().is_empty());
814 }
815}