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}