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 WindowsNt,
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::WindowsNt)
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::WindowsNt => f.write_str("Windows_NT"),
50 }
51 }
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
56pub enum OsRelease {
57 Unix(String),
59 Windows(String),
61}
62
63impl OsRelease {
64 pub fn from_env() -> Option<Self> {
68 #[cfg(unix)]
69 {
70 let uname = rustix::system::uname();
71 let release = uname.release().to_str().ok()?;
72 Some(Self::Unix(release.to_string()))
73 }
74 #[cfg(windows)]
75 {
76 let key = windows_registry::LOCAL_MACHINE
77 .open(r"SOFTWARE\Microsoft\Windows NT\CurrentVersion")
78 .ok()?;
79 Some(Self::Windows(key.get_string("CurrentBuildNumber").ok()?))
80 }
81 #[cfg(not(any(unix, windows)))]
82 {
83 None
84 }
85 }
86}
87
88impl fmt::Display for OsRelease {
89 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90 match self {
91 Self::Unix(release) => f.write_str(release),
92 Self::Windows(build) => f.write_str(build),
93 }
94 }
95}
96
97#[derive(Debug, Clone, Default)]
99pub struct LinuxOsRelease {
100 pub name: Option<String>,
102 pub version_id: Option<String>,
104 pub version_codename: Option<String>,
106}
107
108impl LinuxOsRelease {
109 pub fn from_env() -> Option<Self> {
115 #[cfg(target_os = "linux")]
116 {
117 let content = fs_err::read_to_string("/etc/os-release")
118 .or_else(|_| fs_err::read_to_string("/usr/lib/os-release"))
119 .ok()?;
120 Some(content.parse().unwrap())
121 }
122 #[cfg(not(target_os = "linux"))]
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 insta::assert_debug_snapshot;
168
169 use super::*;
170
171 #[test]
172 fn test_parse_os_release_ubuntu() {
173 let contents = "\
174NAME=\"Ubuntu\"
175VERSION_ID=\"22.04\"
176VERSION_CODENAME=jammy
177ID=ubuntu
178";
179 let release: LinuxOsRelease = contents.parse().unwrap();
180 assert_debug_snapshot!(release, @r#"
181 LinuxOsRelease {
182 name: Some(
183 "Ubuntu",
184 ),
185 version_id: Some(
186 "22.04",
187 ),
188 version_codename: Some(
189 "jammy",
190 ),
191 }
192 "#);
193 }
194
195 #[test]
196 fn test_parse_os_release_empty() {
197 let release: LinuxOsRelease = "".parse().unwrap();
198 assert_eq!(release.name, None);
199 assert_eq!(release.version_id, None);
200 assert_eq!(release.version_codename, None);
201 }
202
203 #[test]
204 fn test_parse_os_release_comments_and_blanks() {
205 let contents = "\
206# This is a comment
207
208NAME='Fedora Linux'
209VERSION_ID=40
210";
211 let release: LinuxOsRelease = contents.parse().unwrap();
212 assert_eq!(release.name.as_deref(), Some("Fedora Linux"));
213 assert_eq!(release.version_id.as_deref(), Some("40"));
214 assert_eq!(release.version_codename, None);
215 }
216
217 #[test]
218 fn test_unquote() {
219 assert_eq!(unquote("\"hello\""), "hello");
220 assert_eq!(unquote("'hello'"), "hello");
221 assert_eq!(unquote("hello"), "hello");
222 assert_eq!(unquote("\"\""), "");
223 assert_eq!(unquote(""), "");
224 }
225
226 #[test]
227 fn test_os_type_returns_value() {
228 let os_type =
229 OsType::from_env().expect("OsType should be available on supported platforms");
230 #[cfg(target_os = "linux")]
231 assert!(matches!(os_type, OsType::Linux(_)));
232 #[cfg(target_os = "macos")]
233 assert_eq!(os_type, OsType::Darwin);
234 #[cfg(target_os = "windows")]
235 assert_eq!(os_type, OsType::WindowsNt);
236 }
237
238 #[test]
239 fn test_os_release_returns_value() {
240 let os_release =
241 OsRelease::from_env().expect("OsRelease should be available on supported platforms");
242 #[cfg(unix)]
243 assert!(matches!(os_release, OsRelease::Unix(_)));
244 #[cfg(windows)]
245 assert!(matches!(os_release, OsRelease::Windows(_)));
246 }
247}