1use std::{
2 borrow::Cow,
3 ffi::{CStr, OsString},
4 fs, io, mem,
5 os::unix::ffi::OsStringExt,
6 prelude::rust_2021::*,
7 slice,
8};
9
10use crate::{
11 os::{Os, Target},
12 CpuArchitecture, DesktopEnvironment, Error, LanguagePreferences, Platform,
13 Result,
14};
15
16enum Name {
17 User,
18 Real,
19}
20
21trait Terminators {
22 const CHARS: &'static [u8];
23}
24
25struct Nul;
26
27struct NulOrComma;
28
29impl Terminators for Nul {
30 const CHARS: &'static [u8] = b"\0";
31}
32
33impl Terminators for NulOrComma {
34 const CHARS: &'static [u8] = b"\0,";
35}
36
37unsafe fn strlen<T>(mut cs: *const u8) -> usize
38where
39 T: Terminators,
40{
41 let mut len = 0;
42
43 while !T::CHARS.contains(&*cs) {
44 len += 1;
45 cs = cs.offset(1);
46 }
47
48 len
49}
50
51fn os_from_cstring<T>(string: *const u8) -> Result<OsString>
52where
53 T: Terminators,
54{
55 if string.is_null() {
56 return Err(Error::null_record());
57 }
58
59 let slice = unsafe {
61 let length = strlen::<T>(string);
62
63 if length == 0 {
64 return Err(Error::empty_record());
65 }
66
67 slice::from_raw_parts(string, length)
68 };
69
70 Ok(OsString::from_vec(slice.to_vec()))
72}
73
74#[inline(always)]
77fn getpwuid(name: Name) -> Result<OsString> {
78 const BUF_SIZE: usize = 16_384; let mut buffer = mem::MaybeUninit::<[u8; BUF_SIZE]>::uninit();
80 let mut passwd = mem::MaybeUninit::<libc::passwd>::uninit();
81
82 let passwd = unsafe {
84 let mut _passwd = mem::MaybeUninit::<*mut libc::passwd>::uninit();
85 let ret = libc::getpwuid_r(
86 libc::geteuid(),
87 passwd.as_mut_ptr(),
88 buffer.as_mut_ptr().cast(),
89 BUF_SIZE,
90 _passwd.as_mut_ptr(),
91 );
92
93 if ret != 0 {
94 return Err(Error::from_io(io::Error::last_os_error()));
95 }
96
97 let _passwd = _passwd.assume_init();
98
99 if _passwd.is_null() {
100 return Err(Error::null_record());
101 }
102
103 passwd.assume_init()
104 };
105
106 if let Name::Real = name {
108 os_from_cstring::<NulOrComma>(passwd.pw_gecos.cast())
109 } else {
110 os_from_cstring::<Nul>(passwd.pw_name.cast())
111 }
112}
113
114impl Target for Os {
115 fn lang_prefs(self) -> Result<LanguagePreferences> {
116 super::unix_lang()
117 }
118
119 fn realname(self) -> Result<OsString> {
120 getpwuid(Name::Real)
121 }
122
123 fn username(self) -> Result<OsString> {
124 getpwuid(Name::User)
125 }
126
127 fn devicename(self) -> Result<OsString> {
128 #[cfg(target_vendor = "apple")]
129 {
130 use std::ptr::null_mut;
131
132 use objc2_system_configuration::SCDynamicStore;
133
134 let Some(name) =
135 (unsafe { SCDynamicStore::computer_name(None, null_mut()) })
136 else {
137 return Err(Error::missing_record());
138 };
139 let name = name.to_string();
142
143 if name.is_empty() {
144 return Err(Error::empty_record());
145 }
146
147 Ok(name.into())
148 }
149
150 #[cfg(target_os = "illumos")]
151 {
152 let mut nodename =
153 fs::read("/etc/nodename").map_err(Error::from_io)?;
154
155 if let Some(slice) = nodename.split(|x| *x == b'\n').next() {
157 nodename.drain(slice.len()..);
158 }
159
160 if nodename.is_empty() {
161 return Err(Error::empty_record());
162 }
163
164 Ok(OsString::from_vec(nodename))
165 }
166
167 #[cfg(any(
168 target_os = "linux",
169 target_os = "dragonfly",
170 target_os = "freebsd",
171 target_os = "netbsd",
172 target_os = "openbsd",
173 target_os = "hurd",
174 ))]
175 {
176 let machine_info =
177 fs::read("/etc/machine-info").map_err(Error::from_io)?;
178
179 for i in machine_info.split(|b| *b == b'\n') {
180 let mut j = i.split(|b| *b == b'=');
181
182 if j.next() == Some(b"PRETTY_HOSTNAME") {
183 let pretty_hostname = j
184 .next()
185 .ok_or(Error::with_invalid_data("parsing failed"))?;
186 let pretty_hostname = pretty_hostname
187 .strip_prefix(b"\"")
188 .unwrap_or(pretty_hostname)
189 .strip_suffix(b"\"")
190 .unwrap_or(pretty_hostname);
191 let pretty_hostname = {
192 let mut vec = Vec::with_capacity(pretty_hostname.len());
193 let mut pretty_hostname = pretty_hostname.iter();
194
195 while let Some(&c) = pretty_hostname.next() {
196 if c == b'\\' {
197 vec.push(match pretty_hostname.next() {
198 Some(b'\\') => b'\\',
199 Some(b't') => b'\t',
200 Some(b'r') => b'\r',
201 Some(b'n') => b'\n',
202 Some(b'\'') => b'\'',
203 Some(b'"') => b'"',
204 _ => {
205 return Err(Error::with_invalid_data(
206 "parsing failed",
207 ));
208 }
209 });
210 } else {
211 vec.push(c);
212 }
213 }
214
215 vec
216 };
217
218 return Ok(OsString::from_vec(pretty_hostname));
219 }
220 }
221
222 Err(Error::missing_record())
223 }
224 }
225
226 fn hostname(self) -> Result<String> {
227 let mut string = Vec::<u8>::with_capacity(256);
229
230 unsafe {
231 if libc::gethostname(string.as_mut_ptr().cast(), 255) == -1 {
232 return Err(Error::from_io(io::Error::last_os_error()));
233 }
234
235 string.set_len(strlen::<Nul>(string.as_ptr().cast()));
236 };
237
238 String::from_utf8(string)
239 .map_err(|_| Error::with_invalid_data("Hostname not valid UTF-8"))
240 }
241
242 fn distro(self) -> Result<String> {
243 #[cfg(target_vendor = "apple")]
244 {
245 fn distro_xml(data: String) -> Result<String> {
246 let mut product_name = None;
247 let mut user_visible_version = None;
248
249 if let Some(start) = data.find("<dict>") {
250 if let Some(end) = data.find("</dict>") {
251 let mut set_product_name = false;
252 let mut set_user_visible_version = false;
253
254 for line in data[start + "<dict>".len()..end].lines() {
255 let line = line.trim();
256
257 if let Some(key) = line.strip_prefix("<key>") {
258 match key.trim_end_matches("</key>") {
259 "ProductName" => set_product_name = true,
260 "ProductUserVisibleVersion" => {
261 set_user_visible_version = true
262 }
263 "ProductVersion" => {
264 if user_visible_version.is_none() {
265 set_user_visible_version = true
266 }
267 }
268 _ => {}
269 }
270 } else if let Some(value) =
271 line.strip_prefix("<string>")
272 {
273 if set_product_name {
274 product_name = Some(
275 value.trim_end_matches("</string>"),
276 );
277 set_product_name = false;
278 } else if set_user_visible_version {
279 user_visible_version = Some(
280 value.trim_end_matches("</string>"),
281 );
282 set_user_visible_version = false;
283 }
284 }
285 }
286 }
287 }
288
289 Ok(if let Some(product_name) = product_name {
290 if let Some(user_visible_version) = user_visible_version {
291 std::format!("{product_name} {user_visible_version}")
292 } else {
293 product_name.to_string()
294 }
295 } else {
296 user_visible_version
297 .map(|v| std::format!("Mac OS (Unknown) {v}"))
298 .ok_or_else(|| {
299 Error::with_invalid_data("Parsing failed")
300 })?
301 })
302 }
303
304 if let Ok(data) = fs::read_to_string(
305 "/System/Library/CoreServices/ServerVersion.plist",
306 ) {
307 distro_xml(data)
308 } else if let Ok(data) = fs::read_to_string(
309 "/System/Library/CoreServices/SystemVersion.plist",
310 ) {
311 distro_xml(data)
312 } else {
313 Err(Error::missing_record())
314 }
315 }
316
317 #[cfg(not(target_vendor = "apple"))]
318 {
319 let program =
320 fs::read("/etc/os-release").map_err(Error::from_io)?;
321 let distro = String::from_utf8_lossy(&program);
322 let err = || Error::with_invalid_data("Parsing failed");
323 let mut fallback = None;
324
325 for i in distro.split('\n') {
326 let mut j = i.split('=');
327
328 match j.next().ok_or_else(err)? {
329 "PRETTY_NAME" => {
330 return Ok(j
331 .next()
332 .ok_or_else(err)?
333 .trim_matches('"')
334 .to_string());
335 }
336 "NAME" => {
337 fallback = Some(
338 j.next()
339 .ok_or_else(err)?
340 .trim_matches('"')
341 .to_string(),
342 )
343 }
344 _ => {}
345 }
346 }
347
348 fallback.ok_or_else(err)
349 }
350 }
351
352 fn desktop_env(self) -> Option<DesktopEnvironment> {
353 #[cfg(target_vendor = "apple")]
354 let env: Cow<'static, str> = "Aqua".into();
355
356 #[cfg(not(target_vendor = "apple"))]
357 let env: Cow<'static, str> = std::env::var_os("XDG_SESSION_DESKTOP")
358 .or_else(|| std::env::var_os("DESKTOP_SESSION"))
359 .or_else(|| std::env::var_os("XDG_CURRENT_DESKTOP"))?
360 .into_string()
361 .unwrap_or_else(|e| e.to_string_lossy().into_owned())
362 .into();
363
364 Some(if env.eq_ignore_ascii_case("AQUA") {
365 DesktopEnvironment::Aqua
366 } else if env.eq_ignore_ascii_case("GNOME") {
367 DesktopEnvironment::Gnome
368 } else if env.eq_ignore_ascii_case("LXDE") {
369 DesktopEnvironment::Lxde
370 } else if env.eq_ignore_ascii_case("OPENBOX") {
371 DesktopEnvironment::Openbox
372 } else if env.eq_ignore_ascii_case("I3") {
373 DesktopEnvironment::I3
374 } else if env.eq_ignore_ascii_case("UBUNTU") {
375 DesktopEnvironment::Ubuntu
376 } else if env.eq_ignore_ascii_case("PLASMA5") {
377 DesktopEnvironment::Plasma
378 } else if env.eq_ignore_ascii_case("XFCE") {
379 DesktopEnvironment::Xfce
380 } else if env.eq_ignore_ascii_case("NIRI") {
381 DesktopEnvironment::Niri
382 } else if env.eq_ignore_ascii_case("HYPRLAND") {
383 DesktopEnvironment::Hyprland
384 } else if env.eq_ignore_ascii_case("COSMIC") {
385 DesktopEnvironment::Cosmic
386 } else {
387 DesktopEnvironment::Unknown(env.to_string())
388 })
389 }
390
391 #[inline(always)]
392 fn platform(self) -> Platform {
393 #[cfg(target_os = "linux")]
394 {
395 Platform::Linux
396 }
397
398 #[cfg(target_vendor = "apple")]
399 {
400 Platform::Mac
401 }
402
403 #[cfg(any(
404 target_os = "dragonfly",
405 target_os = "freebsd",
406 target_os = "netbsd",
407 target_os = "openbsd",
408 ))]
409 {
410 Platform::Bsd
411 }
412
413 #[cfg(target_os = "illumos")]
414 {
415 Platform::Illumos
416 }
417
418 #[cfg(target_os = "hurd")]
419 {
420 Platform::Hurd
421 }
422 }
423
424 #[inline(always)]
425 fn arch(self) -> Result<CpuArchitecture> {
426 let mut buf: libc::utsname = unsafe { mem::zeroed() };
427
428 if unsafe { libc::uname(&mut buf) } == -1 {
429 return Err(Error::from_io(io::Error::last_os_error()));
430 }
431
432 let arch_str =
433 unsafe { CStr::from_ptr(buf.machine.as_ptr()) }.to_string_lossy();
434
435 Ok(match arch_str.as_ref() {
436 "aarch64" | "arm64" | "aarch64_be" | "armv8b" | "armv8l" => {
437 CpuArchitecture::Arm64
438 }
439 "armv5" => CpuArchitecture::ArmV5,
440 "armv6" | "arm" => CpuArchitecture::ArmV6,
441 "armv7" => CpuArchitecture::ArmV7,
442 "i386" => CpuArchitecture::I386,
443 "i586" => CpuArchitecture::I586,
444 "i686" | "i686-AT386" => CpuArchitecture::I686,
445 "mips" => CpuArchitecture::Mips,
446 "mipsel" => CpuArchitecture::MipsEl,
447 "mips64" => CpuArchitecture::Mips64,
448 "mips64el" => CpuArchitecture::Mips64El,
449 "powerpc" | "ppc" | "ppcle" => CpuArchitecture::PowerPc,
450 "powerpc64" | "ppc64" | "ppc64le" => CpuArchitecture::PowerPc64,
451 "powerpc64le" => CpuArchitecture::PowerPc64Le,
452 "riscv32" => CpuArchitecture::Riscv32,
453 "riscv64" => CpuArchitecture::Riscv64,
454 "s390x" => CpuArchitecture::S390x,
455 "sparc" => CpuArchitecture::Sparc,
456 "sparc64" => CpuArchitecture::Sparc64,
457 "x86_64" | "amd64" | "i86pc" => CpuArchitecture::X64,
458 _ => CpuArchitecture::Unknown(arch_str.into_owned()),
459 })
460 }
461}