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