1use std::fmt;
21
22#[derive(Debug, Clone)]
24pub struct JavaSpec {
25 pub vendor: Option<String>,
27 pub version: Option<VersionReq>,
29 pub arch: Option<String>,
31 pub raw: String,
33}
34
35#[derive(Debug, Clone, PartialEq)]
37pub enum VersionReq {
38 Compatible { major: u64, minor: u64, patch: u64 },
40 Approx { major: u64, minor: u64, patch: u64 },
42 Gte { major: u64, minor: u64, patch: u64 },
44 Lte { major: u64, minor: u64, patch: u64 },
46 Gt { major: u64, minor: u64, patch: u64 },
48 Lt { major: u64, minor: u64, patch: u64 },
50 Exact { major: u64, minor: u64, patch: u64 },
52}
53
54impl fmt::Display for VersionReq {
55 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
56 match self {
57 VersionReq::Compatible {
58 major,
59 minor,
60 patch,
61 } => {
62 write!(f, "^{}.{}.{}", major, minor, patch)
63 }
64 VersionReq::Approx {
65 major,
66 minor,
67 patch,
68 } => {
69 write!(f, "~{}.{}.{}", major, minor, patch)
70 }
71 VersionReq::Gte {
72 major,
73 minor,
74 patch,
75 } => write!(f, ">={}.{}.{}", major, minor, patch),
76 VersionReq::Lte {
77 major,
78 minor,
79 patch,
80 } => write!(f, "<={}.{}.{}", major, minor, patch),
81 VersionReq::Gt {
82 major,
83 minor,
84 patch,
85 } => write!(f, ">{}.{}.{}", major, minor, patch),
86 VersionReq::Lt {
87 major,
88 minor,
89 patch,
90 } => write!(f, "<{}.{}.{}", major, minor, patch),
91 VersionReq::Exact {
92 major,
93 minor,
94 patch,
95 } => write!(f, "{}.{}.{}", major, minor, patch),
96 }
97 }
98}
99
100impl JavaSpec {
101 pub fn parse(input: &str) -> Result<Self, String> {
120 let trimmed = input.trim();
121 let raw = trimmed.to_string();
122
123 let (vendor, rest) = split_vendor(trimmed);
124 let (version, rest) = parse_version(rest);
125
126 if vendor.is_none() && version.is_none() && !rest.is_empty() {
129 if looks_like_arch(rest) {
131 let arch = parse_arch(rest);
132 return Ok(JavaSpec {
133 vendor: None,
134 version: None,
135 arch,
136 raw,
137 });
138 }
139 return Ok(JavaSpec {
141 vendor: Some(rest.to_string()),
142 version: None,
143 arch: None,
144 raw,
145 });
146 }
147
148 let arch = parse_arch(rest);
149
150 Ok(JavaSpec {
151 vendor,
152 version,
153 arch,
154 raw,
155 })
156 }
157
158 pub fn matches(&self, info: &crate::JavaInfo) -> bool {
167 if let Some(ref v) = self.vendor {
168 let name_match = info.name.to_lowercase().contains(&v.to_lowercase());
169 let vendor_match = info.vendor.to_lowercase().contains(&v.to_lowercase());
170 if !name_match && !vendor_match {
171 return false;
172 }
173 }
174
175 if let Some(ref req) = self.version
176 && !matches_version_req(&info.parsed_version, req)
177 {
178 return false;
179 }
180
181 if let Some(ref a) = self.arch {
182 let arch_lower = info.architecture.to_lowercase();
183 if !arch_lower.contains(&a.to_lowercase()) {
184 return false;
185 }
186 }
187
188 true
189 }
190}
191
192fn split_vendor(input: &str) -> (Option<String>, &str) {
197 if let Some(idx) = find_version_prefix(input) {
199 let vendor = input[..idx].trim();
200 let rest = input[idx + 1..].trim(); let vendor = if vendor.is_empty() {
202 None
203 } else {
204 Some(vendor.to_string())
205 };
206 (vendor, rest)
207 } else {
208 (None, input)
209 }
210}
211
212fn find_version_prefix(input: &str) -> Option<usize> {
214 let bytes = input.as_bytes();
215 for i in 0..bytes.len().saturating_sub(2) {
217 if bytes[i] == b' ' && bytes[i + 1] == b'v' {
218 let after = &bytes[i + 2..];
220 if after.is_empty() {
221 continue;
222 }
223 let next = after[0];
225 if next.is_ascii_digit() || next == b'^' || next == b'~' || next == b'>' || next == b'<'
226 {
227 return Some(i);
228 }
229 if after.len() >= 2 && (next == b'>' || next == b'<') && after[1] == b'=' {
231 return Some(i);
232 }
233 }
234 }
235 None
236}
237
238fn parse_version(input: &str) -> (Option<VersionReq>, &str) {
242 let input = input.strip_prefix('v').unwrap_or(input).trim_start();
244
245 if input.is_empty() {
246 return (None, input);
247 }
248
249 if let Some(rest) = input.strip_prefix("^") {
251 let (v, rest) = parse_version_number(rest);
252 let req = v.map(|(major, minor, patch)| VersionReq::Compatible {
253 major,
254 minor,
255 patch,
256 });
257 return (req, rest);
258 }
259 if let Some(rest) = input.strip_prefix("~") {
260 let (v, rest) = parse_version_number(rest);
261 let req = v.map(|(major, minor, patch)| VersionReq::Approx {
262 major,
263 minor,
264 patch,
265 });
266 return (req, rest);
267 }
268 if let Some(rest) = input.strip_prefix(">=") {
269 let (v, rest) = parse_version_number(rest);
270 let req = v.map(|(major, minor, patch)| VersionReq::Gte {
271 major,
272 minor,
273 patch,
274 });
275 return (req, rest);
276 }
277 if let Some(rest) = input.strip_prefix("<=") {
278 let (v, rest) = parse_version_number(rest);
279 let req = v.map(|(major, minor, patch)| VersionReq::Lte {
280 major,
281 minor,
282 patch,
283 });
284 return (req, rest);
285 }
286 if let Some(rest) = input.strip_prefix(">") {
287 let (v, rest) = parse_version_number(rest);
288 let req = v.map(|(major, minor, patch)| VersionReq::Gt {
289 major,
290 minor,
291 patch,
292 });
293 return (req, rest);
294 }
295 if let Some(rest) = input.strip_prefix("<") {
296 let (v, rest) = parse_version_number(rest);
297 let req = v.map(|(major, minor, patch)| VersionReq::Lt {
298 major,
299 minor,
300 patch,
301 });
302 return (req, rest);
303 }
304
305 let (v, rest) = parse_version_number(input);
307 (
308 v.map(|(maj, min, patch)| VersionReq::Exact {
309 major: maj,
310 minor: min,
311 patch,
312 }),
313 rest,
314 )
315}
316
317fn parse_version_number(input: &str) -> (Option<(u64, u64, u64)>, &str) {
321 let trimmed = input.trim_start();
322
323 let version_end = trimmed
325 .find(|c: char| !c.is_ascii_digit() && c != '.')
326 .unwrap_or(trimmed.len());
327 let version_part = &trimmed[..version_end];
328 let rest = trimmed[version_end..].trim_start();
329
330 let parts: Vec<&str> = version_part.split('.').collect();
331 if parts.is_empty() || parts[0].is_empty() {
332 return (None, input);
333 }
334
335 let major = match parts[0].parse() {
336 Ok(m) => m,
337 Err(_) => return (None, input),
338 };
339 let minor = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
340 let patch = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
341
342 (Some((major, minor, patch)), rest)
343}
344
345fn parse_arch(input: &str) -> Option<String> {
347 let trimmed = input.trim();
348 if trimmed.is_empty() {
349 return None;
350 }
351
352 let arch = if trimmed.len() > 1 && trimmed.starts_with('a') && trimmed != "aarch64" {
355 trimmed[1..].to_string()
356 } else {
357 trimmed.to_string()
358 };
359
360 if arch.is_empty() { None } else { Some(arch) }
361}
362
363fn looks_like_arch(input: &str) -> bool {
365 let trimmed = input.trim();
366 if trimmed.contains(' ') {
367 return false; }
369 let lower = trimmed.to_lowercase();
370 let known = ["x86_64", "amd64", "aarch64", "arm64", "x86", "i386", "i686"];
371 if known.contains(&lower.as_str()) {
372 return true;
373 }
374 if trimmed.starts_with('a') || trimmed.starts_with('A') {
376 let rest = &trimmed[1..];
377 let rest_lower = rest.to_lowercase();
378 if known.contains(&rest_lower.as_str()) {
379 return true;
380 }
381 }
382 false
383}
384
385fn matches_version_req(parsed: &Option<crate::JavaVersion>, req: &VersionReq) -> bool {
387 let parsed = match parsed {
388 Some(v) => v,
389 None => return false,
390 };
391
392 let (req_major, req_minor, req_patch) = match *req {
393 VersionReq::Compatible {
394 major,
395 minor,
396 patch,
397 }
398 | VersionReq::Approx {
399 major,
400 minor,
401 patch,
402 }
403 | VersionReq::Gte {
404 major,
405 minor,
406 patch,
407 }
408 | VersionReq::Lte {
409 major,
410 minor,
411 patch,
412 }
413 | VersionReq::Gt {
414 major,
415 minor,
416 patch,
417 }
418 | VersionReq::Lt {
419 major,
420 minor,
421 patch,
422 }
423 | VersionReq::Exact {
424 major,
425 minor,
426 patch,
427 } => (major, minor, patch),
428 };
429
430 let parsed_tuple = (parsed.major, parsed.minor, parsed.patch);
431
432 match *req {
433 VersionReq::Compatible { .. } => {
434 parsed_tuple >= (req_major, req_minor, req_patch)
435 && (parsed.major, 0u64, 0u64) < (req_major + 1, 0, 0)
436 }
437 VersionReq::Approx { .. } => {
438 parsed_tuple >= (req_major, req_minor, req_patch)
439 && parsed_tuple < (req_major, req_minor + 1, 0)
440 }
441 VersionReq::Gte { .. } => parsed_tuple >= (req_major, req_minor, req_patch),
442 VersionReq::Lte { .. } => parsed_tuple <= (req_major, req_minor, req_patch),
443 VersionReq::Gt { .. } => parsed_tuple > (req_major, req_minor, req_patch),
444 VersionReq::Lt { .. } => parsed_tuple < (req_major, req_minor, req_patch),
445 VersionReq::Exact { .. } => parsed_tuple == (req_major, req_minor, req_patch),
446 }
447}
448
449#[cfg(test)]
450mod tests {
451 use super::*;
452 use crate::JavaInfo;
453
454 #[test]
459 fn test_parse_full_spec() {
460 let spec = JavaSpec::parse("Eclipse Adoptium v^17.0.0 ax86_64").unwrap();
461 assert_eq!(spec.vendor.as_deref(), Some("Eclipse Adoptium"));
462 assert_eq!(
463 spec.version,
464 Some(VersionReq::Compatible {
465 major: 17,
466 minor: 0,
467 patch: 0
468 })
469 );
470 assert_eq!(spec.arch.as_deref(), Some("x86_64"));
471 assert_eq!(spec.raw, "Eclipse Adoptium v^17.0.0 ax86_64");
472 }
473
474 #[test]
475 fn test_parse_vendor_only() {
476 let spec = JavaSpec::parse("Azul Zulu").unwrap();
477 assert_eq!(spec.vendor.as_deref(), Some("Azul Zulu"));
478 assert!(spec.version.is_none());
479 assert!(spec.arch.is_none());
480 }
481
482 #[test]
483 fn test_parse_version_only() {
484 let spec = JavaSpec::parse("v^11.0.2").unwrap();
485 assert!(spec.vendor.is_none());
486 assert_eq!(
487 spec.version,
488 Some(VersionReq::Compatible {
489 major: 11,
490 minor: 0,
491 patch: 2
492 })
493 );
494 assert!(spec.arch.is_none());
495 }
496
497 #[test]
498 fn test_parse_exact_version() {
499 let spec = JavaSpec::parse("v17.0.1").unwrap();
500 assert_eq!(
501 spec.version,
502 Some(VersionReq::Exact {
503 major: 17,
504 minor: 0,
505 patch: 1
506 })
507 );
508 }
509
510 #[test]
511 fn test_parse_tilde_version() {
512 let spec = JavaSpec::parse("v~21.0.0").unwrap();
513 assert_eq!(
514 spec.version,
515 Some(VersionReq::Approx {
516 major: 21,
517 minor: 0,
518 patch: 0
519 })
520 );
521 }
522
523 #[test]
524 fn test_parse_gte_version() {
525 let spec = JavaSpec::parse("v>=8.0.0").unwrap();
526 assert_eq!(
527 spec.version,
528 Some(VersionReq::Gte {
529 major: 8,
530 minor: 0,
531 patch: 0
532 })
533 );
534 }
535
536 #[test]
537 fn test_parse_arch_only() {
538 let spec = JavaSpec::parse("aaarch64").unwrap();
539 assert!(spec.vendor.is_none());
540 assert!(spec.version.is_none());
541 assert_eq!(spec.arch.as_deref(), Some("aarch64"));
542 }
543
544 #[test]
545 fn test_parse_invalid_version() {
546 let spec = JavaSpec::parse("vabc").unwrap();
547 assert!(spec.version.is_none());
548 }
549
550 #[test]
551 fn test_parse_empty() {
552 let spec = JavaSpec::parse("").unwrap();
553 assert!(spec.vendor.is_none());
554 assert!(spec.version.is_none());
555 assert!(spec.arch.is_none());
556 }
557
558 #[test]
559 fn test_parse_arch_x86() {
560 let spec = JavaSpec::parse("ax86_64").unwrap();
561 assert_eq!(spec.arch.as_deref(), Some("x86_64"));
562 }
563
564 fn make_info(name: &str, version: &str, arch: &str) -> JavaInfo {
569 JavaInfo {
570 name: name.to_string(),
571 vendor: name.to_string(),
572 version: version.to_string(),
573 parsed_version: crate::JavaVersion::parse(version),
574 architecture: arch.to_string(),
575 ..Default::default()
576 }
577 }
578
579 #[test]
580 fn test_match_vendor_substring() {
581 let spec = JavaSpec::parse("Adoptium v^17.0.0 ax86_64").unwrap();
582 let info = make_info("Eclipse Adoptium", "17.0.2", "x86_64");
583 assert!(spec.matches(&info));
584 }
585
586 #[test]
587 fn test_match_vendor_no_match() {
588 let spec = JavaSpec::parse("Azul").unwrap();
589 let info = make_info("Eclipse Adoptium", "17.0.2", "x86_64");
590 assert!(!spec.matches(&info));
591 }
592
593 #[test]
594 fn test_match_version_compatible() {
595 let spec = JavaSpec::parse("v^17.0.0").unwrap();
596 assert!(spec.matches(&make_info("Any", "17.0.2", "x86_64")));
598 assert!(!spec.matches(&make_info("Any", "18.0.0", "x86_64")));
600 assert!(!spec.matches(&make_info("Any", "11.0.0", "x86_64")));
602 }
603
604 #[test]
605 fn test_match_version_exact() {
606 let spec = JavaSpec::parse("v17.0.2").unwrap();
607 assert!(spec.matches(&make_info("Any", "17.0.2", "x86_64")));
608 assert!(!spec.matches(&make_info("Any", "17.0.3", "x86_64")));
609 }
610
611 #[test]
612 fn test_match_version_approx() {
613 let spec = JavaSpec::parse("v~17.0.0").unwrap();
614 assert!(spec.matches(&make_info("Any", "17.0.2", "x86_64")));
615 assert!(!spec.matches(&make_info("Any", "17.1.0", "x86_64")));
616 }
617
618 #[test]
619 fn test_match_version_gte() {
620 let spec = JavaSpec::parse("v>=17.0.0").unwrap();
621 assert!(spec.matches(&make_info("Any", "17.0.0", "x86_64")));
622 assert!(spec.matches(&make_info("Any", "21.0.1", "x86_64")));
623 assert!(!spec.matches(&make_info("Any", "11.0.0", "x86_64")));
624 }
625
626 #[test]
627 fn test_match_arch_substring() {
628 let spec = JavaSpec::parse("aarch64").unwrap();
629 assert!(spec.matches(&make_info("Any", "17.0.0", "aarch64")));
630 assert!(spec.matches(&make_info("Any", "17.0.0", "ARM 64 aarch64")));
631 assert!(!spec.matches(&make_info("Any", "17.0.0", "x86_64")));
632 }
633
634 #[test]
635 fn test_match_all_criteria() {
636 let spec = JavaSpec::parse("Eclipse Adoptium v^17.0.0 ax86_64").unwrap();
637 let info = make_info("Eclipse Adoptium", "17.0.2", "x86_64");
638 assert!(spec.matches(&info));
639 }
640
641 #[test]
642 fn test_match_all_criteria_fails_vendor() {
643 let spec = JavaSpec::parse("Eclipse Adoptium v^17.0.0 ax86_64").unwrap();
644 let info = make_info("Azul Zulu", "17.0.2", "x86_64");
645 assert!(!spec.matches(&info));
646 }
647
648 #[test]
649 fn test_match_all_criteria_fails_arch() {
650 let spec = JavaSpec::parse("Eclipse Adoptium v^17.0.0 ax86_64").unwrap();
651 let info = make_info("Eclipse Adoptium", "17.0.2", "aarch64");
652 assert!(!spec.matches(&info));
653 }
654
655 #[test]
656 fn test_match_empty_spec() {
657 let spec = JavaSpec::parse("").unwrap();
658 assert!(spec.matches(&make_info("Anything", "17.0.0", "any")));
659 }
660
661 #[test]
662 fn test_match_no_parsed_version() {
663 let spec = JavaSpec::parse("v^17.0.0").unwrap();
664 let info = JavaInfo::default();
665 assert!(!spec.matches(&info));
666 }
667
668 #[test]
669 fn test_version_req_display() {
670 assert_eq!(
671 VersionReq::Compatible {
672 major: 17,
673 minor: 0,
674 patch: 0
675 }
676 .to_string(),
677 "^17.0.0"
678 );
679 assert_eq!(
680 VersionReq::Exact {
681 major: 11,
682 minor: 0,
683 patch: 2
684 }
685 .to_string(),
686 "11.0.2"
687 );
688 assert_eq!(
689 VersionReq::Gte {
690 major: 8,
691 minor: 0,
692 patch: 0
693 }
694 .to_string(),
695 ">=8.0.0"
696 );
697 }
698}