whoami/os/
unix.rs

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    // Get a byte slice of the c string.
60    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    // Turn byte slice into Rust String.
71    Ok(OsString::from_vec(slice.to_vec()))
72}
73
74// This function must allocate, because a slice or `Cow<OsStr>` would still
75// reference `passwd` which is dropped when this function returns.
76#[inline(always)]
77fn getpwuid(name: Name) -> Result<OsString> {
78    const BUF_SIZE: usize = 16_384; // size from the man page
79    let mut buffer = mem::MaybeUninit::<[u8; BUF_SIZE]>::uninit();
80    let mut passwd = mem::MaybeUninit::<libc::passwd>::uninit();
81
82    // Get passwd `struct`.
83    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    // Extract names.
107    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            // this should be able to convert whichever encoding is being used
140            // to UTF-8, so we shouldn't have to worry about invalid codepoints.
141            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            // Remove all at and after the first newline (before end of file)
156            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        // Maximum hostname length = 255, plus a NULL byte.
228        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}