1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
use chrono::{Datelike, Utc};
use os_release::OsRelease;
use std::{
    fmt::{self, Display, Formatter},
    io,
    str::FromStr,
};

#[derive(Debug, Error)]
pub enum VersionError {
    #[error("failed to fetch /etc/os-release: {}", _0)]
    OsRelease(io::Error),
    #[error("version parsing error: {}", _0)]
    Parse(VersionParseError),
}

#[derive(Debug, Error)]
pub enum VersionParseError {
    #[error("release version component was not a number: found {}", _0)]
    VersionNaN(String),
    #[error("invalid minor release version: expected 4 or 10, found {}", _0)]
    InvalidMinorVersion(u8),
    #[error("major version does not exist")]
    NoMajor,
    #[error("minor version does not exist")]
    NoMinor,
    #[error("release version is empty")]
    NoVersion,
}

/// The version of an Ubuntu release, which is based on the date of release.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct Version {
    pub major: u8,
    pub minor: u8,
    pub patch: u8,
}

impl Version {
    /// Reads the `/etc/os-release` file and determines which version of Ubuntu is in use.
    pub fn detect() -> Result<Self, VersionError> {
        let release = OsRelease::new().map_err(VersionError::OsRelease)?;
        release.version.parse::<Version>().map_err(VersionError::Parse)
    }

    /// Returns `true` if this is a LTS release.
    pub fn is_lts(self) -> bool { self.major % 2 == 0 && self.minor == 4 }

    /// The number of months that have passed since this version was released.
    pub fn months_since(self) -> i32 {
        let today = Utc::today();

        let major = 2000 - today.year() as u32;
        let minor = today.month() as u32;

        months_since(self, major, minor)
    }

    /// Increments the major / minor version to the next expected release version.
    pub fn next_release(self) -> Self {
        let (major, minor) = if self.minor == 10 { (self.major + 1, 4) } else { (self.major, 10) };

        Version { major, minor, patch: 0 }
    }
}

impl Display for Version {
    fn fmt(&self, fmt: &mut Formatter) -> fmt::Result {
        write!(fmt, "{}.{:02}", self.major, self.minor)?;

        if self.patch != 0 {
            write!(fmt, "{}", self.patch)?;
        }

        Ok(())
    }
}

impl FromStr for Version {
    type Err = VersionParseError;

    fn from_str(input: &str) -> Result<Self, Self::Err> {
        let version = input.split_whitespace().next().ok_or(VersionParseError::NoVersion)?;
        if version.is_empty() {
            return Err(VersionParseError::NoVersion);
        }

        let mut iter = version.split('.');

        let major = iter.next().ok_or(VersionParseError::NoMajor)?;
        let major =
            major.parse::<u8>().map_err(|_| VersionParseError::VersionNaN(major.to_owned()))?;
        let minor = iter.next().ok_or(VersionParseError::NoMinor)?;
        let minor =
            minor.parse::<u8>().map_err(|_| VersionParseError::VersionNaN(minor.to_owned()))?;
        let patch = iter.next().and_then(|p| p.parse::<u8>().ok()).unwrap_or(0);

        Ok(Version { major, minor, patch })
    }
}

fn months_since(version: Version, major: u32, minor: u32) -> i32 {
    ((major as i32 - version.major as i32) * 12) + minor as i32 - version.minor as i32
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn months_since_release() {
        assert_eq!(18, months_since(Version { major: 18, minor: 4, patch: 0 }, 19, 10));
        assert_eq!(3, months_since(Version { major: 19, minor: 10, patch: 0 }, 20, 1));
        assert_eq!(-3, months_since(Version { major: 18, minor: 4, patch: 0 }, 18, 1))
    }

    #[test]
    pub fn lts_check() {
        assert!(Version { major: 18, minor: 4, patch: 0 }.is_lts());
        assert!(!Version { major: 18, minor: 10, patch: 0 }.is_lts());
        assert!(!Version { major: 19, minor: 4, patch: 0 }.is_lts());
        assert!(!Version { major: 19, minor: 10, patch: 0 }.is_lts());
        assert!(Version { major: 20, minor: 4, patch: 0 }.is_lts());
    }

    #[test]
    pub fn lts_parse() {
        assert_eq!(
            Version { major: 18, minor: 4, patch: 1 },
            "18.04.1 LTS".parse::<Version>().unwrap()
        )
    }

    #[test]
    pub fn lts_next() {
        assert_eq!(
            Version { major: 18, minor: 10, patch: 0 },
            Version { major: 18, minor: 4, patch: 1 }.next_release()
        )
    }

    #[test]
    pub fn non_lts_parse() {
        assert_eq!(Version { major: 18, minor: 10, patch: 0 }, "18.10".parse::<Version>().unwrap())
    }

    #[test]
    pub fn non_lts_next() {
        assert_eq!(
            Version { major: 19, minor: 4, patch: 0 },
            Version { major: 18, minor: 10, patch: 0 }.next_release()
        )
    }
}