1use std::{fmt, str::FromStr};
2
3#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
4pub struct Version {
5 pub major: u32,
6 pub minor: u32,
7 pub patch: u32,
8}
9
10impl Version {
11 pub fn validate_glibc_target(self) -> Result<Self, String> {
13 if self.major != 2 || self.minor < 17 {
14 return Err("supported glibc target releases start at 2.17 (glibc 2.x only)".into());
15 }
16 Ok(self)
17 }
18}
19
20impl FromStr for Version {
21 type Err = String;
22
23 fn from_str(text: &str) -> Result<Self, String> {
24 let parts: Vec<&str> = text.split('.').collect();
25 if !(2..=3).contains(&parts.len()) {
26 return Err(format!(
27 "invalid version {text:?}: expected MAJOR.MINOR or MAJOR.MINOR.PATCH"
28 ));
29 }
30 let number = |part: &str| -> Result<u32, String> {
31 if part.is_empty() || !part.bytes().all(|b| b.is_ascii_digit()) {
32 return Err(format!("invalid numeric version {text:?}"));
33 }
34 part.parse()
35 .map_err(|_| format!("version component too large in {text:?}"))
36 };
37 Ok(Self {
38 major: number(parts[0])?,
39 minor: number(parts[1])?,
40 patch: if parts.len() == 3 {
41 number(parts[2])?
42 } else {
43 0
44 },
45 })
46 }
47}
48
49impl fmt::Display for Version {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 if self.patch == 0 {
52 write!(f, "{}.{}", self.major, self.minor)
53 } else {
54 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
55 }
56 }
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62
63 #[test]
64 fn target_floor_does_not_reject_historical_symbol_versions() {
65 let old: Version = "2.2.5".parse().unwrap();
66 assert!(old.validate_glibc_target().is_err());
67 assert!("2.16"
68 .parse::<Version>()
69 .unwrap()
70 .validate_glibc_target()
71 .is_err());
72 assert!("3.0"
73 .parse::<Version>()
74 .unwrap()
75 .validate_glibc_target()
76 .is_err());
77 assert!("2.17"
78 .parse::<Version>()
79 .unwrap()
80 .validate_glibc_target()
81 .is_ok());
82 }
83
84 #[test]
85 fn compares_versions_numerically() {
86 assert!("2.9".parse::<Version>().unwrap() < "2.22".parse().unwrap());
87 assert!("2.2.5".parse::<Version>().unwrap() < "2.17".parse().unwrap());
88 assert_eq!(
89 "2.22.0".parse::<Version>().unwrap(),
90 "2.22".parse().unwrap()
91 );
92 }
93
94 #[test]
95 fn rejects_non_numeric_and_partial_versions() {
96 for value in [
97 "",
98 "2",
99 "2.",
100 ".22",
101 "2.22.1.4",
102 "+2.22",
103 "GLIBC_2.22",
104 "2.22x",
105 "4294967296.22",
106 ] {
107 assert!(value.parse::<Version>().is_err(), "{value}");
108 }
109 }
110}