os-version 0.2.0

Get the operating system version
Documentation
use anyhow::Result;

#[derive(Debug, PartialEq)]
pub struct Linux {
    pub distro: String,
    pub version: Option<String>,
    pub version_name: Option<String>,
}

impl Linux {
    #[cfg(target_os = "linux")]
    pub fn detect() -> Result<Linux> {
        let file = std::fs::read_to_string("/etc/os-release")?;
        parse(&file)
    }

    #[cfg(not(target_os = "linux"))]
    pub fn detect() -> Result<Linux> {
        unreachable!()
    }
}

impl ToString for Linux {
    fn to_string(&self) -> String {
        if let Some(version) = &self.version {
            format!("{} {}", self.distro, version)
        } else {
            self.distro.to_string()
        }
    }
}

#[cfg(target_os = "linux")]
fn parse(file: &str) -> Result<Linux> {
    use anyhow::Error;

    let mut distro = None;
    let mut version = None;
    let mut version_name = None;

    for line in file.lines() {
        if let Some(remaining) = line.strip_prefix("ID=") {
            distro = Some(parse_value(remaining)?);
        } else if let Some(remaining) = line.strip_prefix("VERSION_") {
            if let Some(remaining) = remaining.strip_prefix("ID=") {
                version = Some(parse_value(remaining)?);
            } else if let Some(remaining) = remaining.strip_prefix("CODENAME=") {
                version_name = Some(parse_value(remaining)?);
            }
        }
    }

    let distro = distro.ok_or_else(|| Error::msg("Mandatory ID= field is missing"))?;

    Ok(Linux {
        distro,
        version,
        version_name,
    })
}

#[cfg(target_os = "linux")]
fn parse_value(mut value: &str) -> Result<String> {
    if value.starts_with('"') && value.ends_with('"') && value.len() >= 2 {
        value = &value[1..value.len() - 1];
    }

    Ok(value.to_string())
}

#[cfg(test)]
mod tests {
    #[cfg(target_os = "linux")]
    use super::*;

    #[test]
    #[cfg(target_os = "linux")]
    fn detect_debian() {
        let os_release = parse(
            r#"
NAME="Debian GNU/Linux"
VERSION_ID="10"
VERSION="10 (buster)"
VERSION_CODENAME=buster
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"
"#,
        )
        .unwrap();
        assert_eq!(
            Linux {
                distro: "debian".to_string(),
                version: Some("10".to_string()),
                version_name: Some("buster".to_string()),
            },
            os_release
        );
    }

    #[test]
    #[cfg(target_os = "linux")]
    fn detect_archlinux() {
        let os_release = parse(
            r#"
NAME="Arch Linux"
PRETTY_NAME="Arch Linux"
ID=arch
BUILD_ID=rolling
ANSI_COLOR="0;36"
HOME_URL="https://www.archlinux.org/"
DOCUMENTATION_URL="https://wiki.archlinux.org/"
SUPPORT_URL="https://bbs.archlinux.org/"
BUG_REPORT_URL="https://bugs.archlinux.org/"
LOGO=archlinux
"#,
        )
        .unwrap();
        assert_eq!(
            Linux {
                distro: "arch".to_string(),
                version: None,
                version_name: None,
            },
            os_release
        );
    }

    #[test]
    #[cfg(target_os = "linux")]
    fn detect_alpine() {
        let os_release = parse(
            r#"
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.11.5
PRETTY_NAME="Alpine Linux v3.11"
HOME_URL="https://alpinelinux.org/"
BUG_REPORT_URL="https://bugs.alpinelinux.org/"
"#,
        )
        .unwrap();
        assert_eq!(
            Linux {
                distro: "alpine".to_string(),
                version: Some("3.11.5".to_string()),
                version_name: None,
            },
            os_release
        );
    }

    #[test]
    #[cfg(target_os = "linux")]
    fn detect_ubuntu() {
        let os_release = parse(
            r#"
NAME="Ubuntu"
VERSION="18.04.4 LTS (Bionic Beaver)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 18.04.4 LTS"
VERSION_ID="18.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=bionic
UBUNTU_CODENAME=bionic
"#,
        )
        .unwrap();
        assert_eq!(
            Linux {
                distro: "ubuntu".to_string(),
                version: Some("18.04".to_string()),
                version_name: Some("bionic".to_string()),
            },
            os_release
        );
    }

    #[test]
    #[cfg(target_os = "linux")]
    fn detect_centos() {
        let os_release = parse(
            r#"
NAME="CentOS Linux"
VERSION="8 (Core)"
ID="centos"
ID_LIKE="rhel fedora"
VERSION_ID="8"
PLATFORM_ID="platform:el8"
PRETTY_NAME="CentOS Linux 8 (Core)"
ANSI_COLOR="0;31"
CPE_NAME="cpe:/o:centos:centos:8"
HOME_URL="https://www.centos.org/"
BUG_REPORT_URL="https://bugs.centos.org/"

CENTOS_MANTISBT_PROJECT="CentOS-8"
CENTOS_MANTISBT_PROJECT_VERSION="8"
REDHAT_SUPPORT_PRODUCT="centos"
REDHAT_SUPPORT_PRODUCT_VERSION="8"

"#,
        )
        .unwrap();
        assert_eq!(
            Linux {
                distro: "centos".to_string(),
                version: Some("8".to_string()),
                version_name: None,
            },
            os_release
        );
    }
}