1use std::convert::Infallible;
4use std::fmt;
5use std::str::FromStr;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum OsType {
10 Linux(String),
12 Darwin,
14 Windows,
16}
17
18impl OsType {
19 pub fn from_env() -> Option<Self> {
23 cfg_select! {
24 target_os = "linux" => {
25 let uname = rustix::system::uname();
26 let os_type = uname.sysname().to_str().ok()?;
27 Some(Self::Linux(os_type.to_string()))
28 },
29 target_os = "macos" => Some(Self::Darwin),
30 target_os = "windows" => Some(Self::Windows),
31 _ => None,
32 }
33 }
34}
35
36impl fmt::Display for OsType {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 match self {
39 Self::Linux(os_type) => f.write_str(os_type),
40 Self::Darwin => f.write_str("Darwin"),
41 Self::Windows => f.write_str("Windows"),
42 }
43 }
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
48pub enum OsRelease {
49 Unix(String),
51 Windows {
53 major: u32,
54 minor: u32,
55 build: u32,
56 revision: u32,
57 },
58}
59
60impl OsRelease {
61 pub fn from_env() -> Option<Self> {
65 cfg_select! {
66 unix => {
67 let uname = rustix::system::uname();
68 let release = uname.release().to_str().ok()?;
69 Some(Self::Unix(release.to_string()))
70 },
71 windows => {
72 let os_version = windows_version::OsVersion::current();
73 Some(Self::Windows {
74 major: os_version.major,
75 minor: os_version.minor,
76 build: os_version.build,
77 revision: windows_version::revision(),
78 })
79 },
80 _ => None,
81 }
82 }
83}
84
85impl fmt::Display for OsRelease {
86 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87 match self {
88 Self::Unix(release) => f.write_str(release),
89 Self::Windows {
90 major,
91 minor,
92 build,
93 revision,
94 } => write!(f, "{major}.{minor}.{build}.{revision}"),
95 }
96 }
97}
98
99#[derive(Debug, Clone, Default)]
101pub struct LinuxOsRelease {
102 pub name: Option<String>,
104 pub version_id: Option<String>,
106 pub version_codename: Option<String>,
108}
109
110impl LinuxOsRelease {
111 pub fn from_env() -> Option<Self> {
117 cfg_select! {
118 target_os = "linux" => {
119 let content = fs_err::read_to_string("/etc/os-release")
120 .or_else(|_| fs_err::read_to_string("/usr/lib/os-release"))
121 .ok()?;
122 Some(content.parse().unwrap())
123 },
124 _ => None,
125 }
126 }
127}
128
129impl FromStr for LinuxOsRelease {
130 type Err = Infallible;
131
132 fn from_str(contents: &str) -> Result<Self, Self::Err> {
134 let mut release = Self::default();
135 for line in contents.lines() {
136 let line = line.trim();
137 if line.is_empty() || line.starts_with('#') {
138 continue;
139 }
140 let Some((key, value)) = line.split_once('=') else {
141 continue;
142 };
143 let value = unquote(value);
144 match key {
145 "NAME" => release.name = Some(value.to_string()),
146 "VERSION_ID" => release.version_id = Some(value.to_string()),
147 "VERSION_CODENAME" => release.version_codename = Some(value.to_string()),
148 _ => {}
149 }
150 }
151 Ok(release)
152 }
153}
154
155fn unquote(s: &str) -> &str {
157 for quote in ['"', '\''] {
158 if let Some(inner) = s.strip_prefix(quote).and_then(|s| s.strip_suffix(quote)) {
159 return inner;
160 }
161 }
162 s
163}
164
165#[cfg(test)]
166mod tests {
167 use std::assert_matches;
168
169 use insta::assert_debug_snapshot;
170
171 use super::*;
172
173 #[test]
174 fn test_parse_os_release_ubuntu() {
175 let contents = "\
176NAME=\"Ubuntu\"
177VERSION_ID=\"22.04\"
178VERSION_CODENAME=jammy
179ID=ubuntu
180";
181 let release: LinuxOsRelease = contents.parse().unwrap();
182 assert_debug_snapshot!(release, @r#"
183 LinuxOsRelease {
184 name: Some(
185 "Ubuntu",
186 ),
187 version_id: Some(
188 "22.04",
189 ),
190 version_codename: Some(
191 "jammy",
192 ),
193 }
194 "#);
195 }
196
197 #[test]
198 fn test_parse_os_release_empty() {
199 let release: LinuxOsRelease = "".parse().unwrap();
200 assert_eq!(release.name, None);
201 assert_eq!(release.version_id, None);
202 assert_eq!(release.version_codename, None);
203 }
204
205 #[test]
206 fn test_parse_os_release_comments_and_blanks() {
207 let contents = "\
208# This is a comment
209
210NAME='Fedora Linux'
211VERSION_ID=40
212";
213 let release: LinuxOsRelease = contents.parse().unwrap();
214 assert_eq!(release.name.as_deref(), Some("Fedora Linux"));
215 assert_eq!(release.version_id.as_deref(), Some("40"));
216 assert_eq!(release.version_codename, None);
217 }
218
219 #[test]
220 fn test_unquote() {
221 assert_eq!(unquote("\"hello\""), "hello");
222 assert_eq!(unquote("'hello'"), "hello");
223 assert_eq!(unquote("hello"), "hello");
224 assert_eq!(unquote("\"\""), "");
225 assert_eq!(unquote(""), "");
226 }
227
228 #[test]
229 fn test_os_type_returns_value() {
230 let os_type =
231 OsType::from_env().expect("OsType should be available on supported platforms");
232 cfg_select! {
233 target_os = "linux" => { assert_matches!(os_type, OsType::Linux(_)); },
234 target_os = "macos" => { assert_eq!(os_type, OsType::Darwin); },
235 target_os = "windows" => { assert_eq!(os_type, OsType::Windows); },
236 _ => {},
237 }
238 }
239
240 #[test]
241 fn test_os_release_returns_value() {
242 let os_release =
243 OsRelease::from_env().expect("OsRelease should be available on supported platforms");
244 cfg_select! {
245 unix => { assert_matches!(os_release, OsRelease::Unix(_)); },
246 windows => { assert_matches!(os_release, OsRelease::Windows { .. }); },
247 _ => {},
248 }
249 }
250}