Skip to main content

cargo_libc/
version.rs

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    /// Validate a selected target release, not an individual ABI symbol version.
12    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}