os_version/
linux.rs

1use anyhow::Result;
2use std::fmt;
3
4#[cfg(target_os = "linux")]
5const PATH: &str = "/etc/os-release";
6
7#[derive(Debug, Clone, PartialEq)]
8pub struct Linux {
9    pub distro: String,
10    pub version: Option<String>,
11    pub version_name: Option<String>,
12}
13
14impl Linux {
15    #[cfg(target_os = "linux")]
16    pub fn detect() -> Result<Linux> {
17        let file = std::fs::read_to_string(PATH)?;
18        parse(&file)
19    }
20
21    #[cfg(not(target_os = "linux"))]
22    pub fn detect() -> Result<Linux> {
23        unreachable!()
24    }
25
26    #[cfg(all(feature = "tokio", target_os = "linux"))]
27    pub async fn detect_async() -> Result<Linux> {
28        let file = tokio::fs::read_to_string(PATH).await?;
29        parse(&file)
30    }
31
32    #[cfg(all(feature = "tokio", not(target_os = "linux")))]
33    pub async fn detect_async() -> Result<Linux> {
34        unreachable!()
35    }
36}
37
38impl fmt::Display for Linux {
39    fn fmt(&self, w: &mut fmt::Formatter<'_>) -> fmt::Result {
40        if let Some(version) = &self.version {
41            write!(w, "{} {}", self.distro, version)
42        } else {
43            write!(w, "{}", self.distro)
44        }
45    }
46}
47
48#[cfg(target_os = "linux")]
49fn parse(file: &str) -> Result<Linux> {
50    use anyhow::Error;
51
52    let mut distro = None;
53    let mut version = None;
54    let mut version_name = None;
55
56    for line in file.lines() {
57        if let Some(remaining) = line.strip_prefix("ID=") {
58            distro = Some(parse_value(remaining)?);
59        } else if let Some(remaining) = line.strip_prefix("VERSION_") {
60            if let Some(remaining) = remaining.strip_prefix("ID=") {
61                version = Some(parse_value(remaining)?);
62            } else if let Some(remaining) = remaining.strip_prefix("CODENAME=") {
63                version_name = Some(parse_value(remaining)?);
64            }
65        }
66    }
67
68    let distro = distro.ok_or_else(|| Error::msg("Mandatory ID= field is missing"))?;
69
70    Ok(Linux {
71        distro,
72        version,
73        version_name,
74    })
75}
76
77#[cfg(target_os = "linux")]
78fn parse_value(mut value: &str) -> Result<String> {
79    if value.starts_with('"') && value.ends_with('"') && value.len() >= 2 {
80        value = &value[1..value.len() - 1];
81    }
82
83    Ok(value.to_string())
84}
85
86#[cfg(test)]
87mod tests {
88    #[cfg(target_os = "linux")]
89    use super::*;
90
91    #[test]
92    #[cfg(target_os = "linux")]
93    fn detect_debian() {
94        let os_release = parse(
95            r#"
96NAME="Debian GNU/Linux"
97VERSION_ID="10"
98VERSION="10 (buster)"
99VERSION_CODENAME=buster
100ID=debian
101HOME_URL="https://www.debian.org/"
102SUPPORT_URL="https://www.debian.org/support"
103BUG_REPORT_URL="https://bugs.debian.org/"
104"#,
105        )
106        .unwrap();
107        assert_eq!(
108            Linux {
109                distro: "debian".to_string(),
110                version: Some("10".to_string()),
111                version_name: Some("buster".to_string()),
112            },
113            os_release
114        );
115    }
116
117    #[test]
118    #[cfg(target_os = "linux")]
119    fn detect_archlinux() {
120        let os_release = parse(
121            r#"
122NAME="Arch Linux"
123PRETTY_NAME="Arch Linux"
124ID=arch
125BUILD_ID=rolling
126ANSI_COLOR="0;36"
127HOME_URL="https://www.archlinux.org/"
128DOCUMENTATION_URL="https://wiki.archlinux.org/"
129SUPPORT_URL="https://bbs.archlinux.org/"
130BUG_REPORT_URL="https://bugs.archlinux.org/"
131LOGO=archlinux
132"#,
133        )
134        .unwrap();
135        assert_eq!(
136            Linux {
137                distro: "arch".to_string(),
138                version: None,
139                version_name: None,
140            },
141            os_release
142        );
143    }
144
145    #[test]
146    #[cfg(target_os = "linux")]
147    fn detect_alpine() {
148        let os_release = parse(
149            r#"
150NAME="Alpine Linux"
151ID=alpine
152VERSION_ID=3.11.5
153PRETTY_NAME="Alpine Linux v3.11"
154HOME_URL="https://alpinelinux.org/"
155BUG_REPORT_URL="https://bugs.alpinelinux.org/"
156"#,
157        )
158        .unwrap();
159        assert_eq!(
160            Linux {
161                distro: "alpine".to_string(),
162                version: Some("3.11.5".to_string()),
163                version_name: None,
164            },
165            os_release
166        );
167    }
168
169    #[test]
170    #[cfg(target_os = "linux")]
171    fn detect_ubuntu() {
172        let os_release = parse(
173            r#"
174NAME="Ubuntu"
175VERSION="18.04.4 LTS (Bionic Beaver)"
176ID=ubuntu
177ID_LIKE=debian
178PRETTY_NAME="Ubuntu 18.04.4 LTS"
179VERSION_ID="18.04"
180HOME_URL="https://www.ubuntu.com/"
181SUPPORT_URL="https://help.ubuntu.com/"
182BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
183PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
184VERSION_CODENAME=bionic
185UBUNTU_CODENAME=bionic
186"#,
187        )
188        .unwrap();
189        assert_eq!(
190            Linux {
191                distro: "ubuntu".to_string(),
192                version: Some("18.04".to_string()),
193                version_name: Some("bionic".to_string()),
194            },
195            os_release
196        );
197    }
198
199    #[test]
200    #[cfg(target_os = "linux")]
201    fn detect_centos() {
202        let os_release = parse(
203            r#"
204NAME="CentOS Linux"
205VERSION="8 (Core)"
206ID="centos"
207ID_LIKE="rhel fedora"
208VERSION_ID="8"
209PLATFORM_ID="platform:el8"
210PRETTY_NAME="CentOS Linux 8 (Core)"
211ANSI_COLOR="0;31"
212CPE_NAME="cpe:/o:centos:centos:8"
213HOME_URL="https://www.centos.org/"
214BUG_REPORT_URL="https://bugs.centos.org/"
215
216CENTOS_MANTISBT_PROJECT="CentOS-8"
217CENTOS_MANTISBT_PROJECT_VERSION="8"
218REDHAT_SUPPORT_PRODUCT="centos"
219REDHAT_SUPPORT_PRODUCT_VERSION="8"
220
221"#,
222        )
223        .unwrap();
224        assert_eq!(
225            Linux {
226                distro: "centos".to_string(),
227                version: Some("8".to_string()),
228                version_name: None,
229            },
230            os_release
231        );
232    }
233}