whoami/os/
unix.rs

1#[cfg(target_os = "illumos")]
2use std::convert::TryInto;
3#[cfg(any(
4    target_os = "linux",
5    target_os = "dragonfly",
6    target_os = "freebsd",
7    target_os = "netbsd",
8    target_os = "openbsd",
9    target_os = "illumos",
10))]
11use std::env;
12use std::{
13    ffi::{c_void, CStr, OsString},
14    fs,
15    io::{Error, ErrorKind},
16    mem,
17    os::{
18        raw::{c_char, c_int},
19        unix::ffi::OsStringExt,
20    },
21    slice,
22};
23#[cfg(target_os = "macos")]
24use std::{
25    os::{
26        raw::{c_long, c_uchar},
27        unix::ffi::OsStrExt,
28    },
29    ptr::null_mut,
30};
31
32use crate::{
33    os::{Os, Target},
34    Arch, DesktopEnv, Platform, Result,
35};
36
37#[cfg(target_os = "linux")]
38#[repr(C)]
39struct PassWd {
40    pw_name: *const c_void,
41    pw_passwd: *const c_void,
42    pw_uid: u32,
43    pw_gid: u32,
44    pw_gecos: *const c_void,
45    pw_dir: *const c_void,
46    pw_shell: *const c_void,
47}
48
49#[cfg(any(
50    target_os = "macos",
51    target_os = "dragonfly",
52    target_os = "freebsd",
53    target_os = "openbsd",
54    target_os = "netbsd"
55))]
56#[repr(C)]
57struct PassWd {
58    pw_name: *const c_void,
59    pw_passwd: *const c_void,
60    pw_uid: u32,
61    pw_gid: u32,
62    pw_change: isize,
63    pw_class: *const c_void,
64    pw_gecos: *const c_void,
65    pw_dir: *const c_void,
66    pw_shell: *const c_void,
67    pw_expire: isize,
68    pw_fields: i32,
69}
70
71#[cfg(target_os = "illumos")]
72#[repr(C)]
73struct PassWd {
74    pw_name: *const c_void,
75    pw_passwd: *const c_void,
76    pw_uid: u32,
77    pw_gid: u32,
78    pw_age: *const c_void,
79    pw_comment: *const c_void,
80    pw_gecos: *const c_void,
81    pw_dir: *const c_void,
82    pw_shell: *const c_void,
83}
84
85#[cfg(target_os = "illumos")]
86extern "system" {
87    fn getpwuid_r(
88        uid: u32,
89        pwd: *mut PassWd,
90        buf: *mut c_void,
91        buflen: c_int,
92    ) -> *mut PassWd;
93}
94
95#[cfg(any(
96    target_os = "linux",
97    target_os = "macos",
98    target_os = "dragonfly",
99    target_os = "freebsd",
100    target_os = "netbsd",
101    target_os = "openbsd",
102))]
103extern "system" {
104    fn getpwuid_r(
105        uid: u32,
106        pwd: *mut PassWd,
107        buf: *mut c_void,
108        buflen: usize,
109        result: *mut *mut PassWd,
110    ) -> i32;
111}
112
113extern "system" {
114    fn geteuid() -> u32;
115    fn gethostname(name: *mut c_void, len: usize) -> i32;
116}
117
118#[cfg(target_os = "macos")]
119#[link(name = "CoreFoundation", kind = "framework")]
120#[link(name = "SystemConfiguration", kind = "framework")]
121extern "system" {
122    fn CFStringGetCString(
123        the_string: *mut c_void,
124        buffer: *mut u8,
125        buffer_size: c_long,
126        encoding: u32,
127    ) -> c_uchar;
128    fn CFStringGetLength(the_string: *mut c_void) -> c_long;
129    fn CFStringGetMaximumSizeForEncoding(
130        length: c_long,
131        encoding: u32,
132    ) -> c_long;
133    fn SCDynamicStoreCopyComputerName(
134        store: *mut c_void,
135        encoding: *mut u32,
136    ) -> *mut c_void;
137    fn CFRelease(cf: *const c_void);
138}
139
140enum Name {
141    User,
142    Real,
143}
144
145unsafe fn strlen(cs: *const c_void) -> usize {
146    let mut len = 0;
147    let mut cs: *const u8 = cs.cast();
148    while *cs != 0 {
149        len += 1;
150        cs = cs.offset(1);
151    }
152    len
153}
154
155unsafe fn strlen_gecos(cs: *const c_void) -> usize {
156    let mut len = 0;
157    let mut cs: *const u8 = cs.cast();
158    while *cs != 0 && *cs != b',' {
159        len += 1;
160        cs = cs.offset(1);
161    }
162    len
163}
164
165fn os_from_cstring_gecos(string: *const c_void) -> Result<OsString> {
166    if string.is_null() {
167        return Err(super::err_null_record());
168    }
169
170    // Get a byte slice of the c string.
171    let slice = unsafe {
172        let length = strlen_gecos(string);
173
174        if length == 0 {
175            return Err(super::err_empty_record());
176        }
177
178        slice::from_raw_parts(string.cast(), length)
179    };
180
181    // Turn byte slice into Rust String.
182    Ok(OsString::from_vec(slice.to_vec()))
183}
184
185fn os_from_cstring(string: *const c_void) -> Result<OsString> {
186    if string.is_null() {
187        return Err(super::err_null_record());
188    }
189
190    // Get a byte slice of the c string.
191    let slice = unsafe {
192        let length = strlen(string);
193
194        if length == 0 {
195            return Err(super::err_empty_record());
196        }
197
198        slice::from_raw_parts(string.cast(), length)
199    };
200
201    // Turn byte slice into Rust String.
202    Ok(OsString::from_vec(slice.to_vec()))
203}
204
205#[cfg(target_os = "macos")]
206fn os_from_cfstring(string: *mut c_void) -> OsString {
207    if string.is_null() {
208        return "".to_string().into();
209    }
210
211    unsafe {
212        let len = CFStringGetLength(string);
213        let capacity =
214            CFStringGetMaximumSizeForEncoding(len, 134_217_984 /* UTF8 */) + 1;
215        let mut out = Vec::with_capacity(capacity as usize);
216        if CFStringGetCString(
217            string,
218            out.as_mut_ptr(),
219            capacity,
220            134_217_984, /* UTF8 */
221        ) != 0
222        {
223            out.set_len(strlen(out.as_ptr().cast())); // Remove trailing NUL byte
224            out.shrink_to_fit();
225            CFRelease(string);
226            OsString::from_vec(out)
227        } else {
228            CFRelease(string);
229            "".to_string().into()
230        }
231    }
232}
233
234// This function must allocate, because a slice or `Cow<OsStr>` would still
235// reference `passwd` which is dropped when this function returns.
236#[inline(always)]
237fn getpwuid(name: Name) -> Result<OsString> {
238    const BUF_SIZE: usize = 16_384; // size from the man page
239    let mut buffer = mem::MaybeUninit::<[u8; BUF_SIZE]>::uninit();
240    let mut passwd = mem::MaybeUninit::<PassWd>::uninit();
241
242    // Get PassWd `struct`.
243    let passwd = unsafe {
244        #[cfg(any(
245            target_os = "linux",
246            target_os = "macos",
247            target_os = "dragonfly",
248            target_os = "freebsd",
249            target_os = "netbsd",
250            target_os = "openbsd",
251        ))]
252        {
253            let mut _passwd = mem::MaybeUninit::<*mut PassWd>::uninit();
254            let ret = getpwuid_r(
255                geteuid(),
256                passwd.as_mut_ptr(),
257                buffer.as_mut_ptr() as *mut c_void,
258                BUF_SIZE,
259                _passwd.as_mut_ptr(),
260            );
261
262            if ret != 0 {
263                return Err(Error::last_os_error());
264            }
265
266            let _passwd = _passwd.assume_init();
267
268            if _passwd.is_null() {
269                return Err(super::err_null_record());
270            }
271            passwd.assume_init()
272        }
273
274        #[cfg(target_os = "illumos")]
275        {
276            let ret = getpwuid_r(
277                geteuid(),
278                passwd.as_mut_ptr(),
279                buffer.as_mut_ptr() as *mut c_void,
280                BUF_SIZE.try_into().unwrap_or(c_int::MAX),
281            );
282
283            if ret.is_null() {
284                return Err(Error::last_os_error());
285            }
286            passwd.assume_init()
287        }
288    };
289
290    // Extract names.
291    if let Name::Real = name {
292        os_from_cstring_gecos(passwd.pw_gecos)
293    } else {
294        os_from_cstring(passwd.pw_name)
295    }
296}
297
298#[cfg(target_os = "macos")]
299fn distro_xml(data: String) -> Result<String> {
300    let mut product_name = None;
301    let mut user_visible_version = None;
302
303    if let Some(start) = data.find("<dict>") {
304        if let Some(end) = data.find("</dict>") {
305            let mut set_product_name = false;
306            let mut set_user_visible_version = false;
307
308            for line in data[start + "<dict>".len()..end].lines() {
309                let line = line.trim();
310
311                if line.starts_with("<key>") {
312                    match line["<key>".len()..].trim_end_matches("</key>") {
313                        "ProductName" => set_product_name = true,
314                        "ProductUserVisibleVersion" => {
315                            set_user_visible_version = true
316                        }
317                        "ProductVersion" => {
318                            if user_visible_version.is_none() {
319                                set_user_visible_version = true
320                            }
321                        }
322                        _ => {}
323                    }
324                } else if line.starts_with("<string>") {
325                    if set_product_name {
326                        product_name = Some(
327                            line["<string>".len()..]
328                                .trim_end_matches("</string>"),
329                        );
330                        set_product_name = false;
331                    } else if set_user_visible_version {
332                        user_visible_version = Some(
333                            line["<string>".len()..]
334                                .trim_end_matches("</string>"),
335                        );
336                        set_user_visible_version = false;
337                    }
338                }
339            }
340        }
341    }
342
343    Ok(if let Some(product_name) = product_name {
344        if let Some(user_visible_version) = user_visible_version {
345            format!("{} {}", product_name, user_visible_version)
346        } else {
347            product_name.to_string()
348        }
349    } else {
350        user_visible_version
351            .map(|v| format!("Mac OS (Unknown) {}", v))
352            .ok_or_else(|| {
353                Error::new(ErrorKind::InvalidData, "Parsing failed")
354            })?
355    })
356}
357
358#[cfg(any(
359    target_os = "macos",
360    target_os = "freebsd",
361    target_os = "netbsd",
362    target_os = "openbsd",
363))]
364#[repr(C)]
365struct UtsName {
366    sysname: [c_char; 256],
367    nodename: [c_char; 256],
368    release: [c_char; 256],
369    version: [c_char; 256],
370    machine: [c_char; 256],
371}
372
373#[cfg(target_os = "illumos")]
374#[repr(C)]
375struct UtsName {
376    sysname: [c_char; 257],
377    nodename: [c_char; 257],
378    release: [c_char; 257],
379    version: [c_char; 257],
380    machine: [c_char; 257],
381}
382
383#[cfg(target_os = "dragonfly")]
384#[repr(C)]
385struct UtsName {
386    sysname: [c_char; 32],
387    nodename: [c_char; 32],
388    release: [c_char; 32],
389    version: [c_char; 32],
390    machine: [c_char; 32],
391}
392
393#[cfg(any(target_os = "linux", target_os = "android",))]
394#[repr(C)]
395struct UtsName {
396    sysname: [c_char; 65],
397    nodename: [c_char; 65],
398    release: [c_char; 65],
399    version: [c_char; 65],
400    machine: [c_char; 65],
401    domainname: [c_char; 65],
402}
403
404// Buffer initialization
405impl Default for UtsName {
406    fn default() -> Self {
407        unsafe { mem::zeroed() }
408    }
409}
410
411#[inline(always)]
412unsafe fn uname(buf: *mut UtsName) -> c_int {
413    extern "C" {
414        #[cfg(any(
415            target_os = "linux",
416            target_os = "macos",
417            target_os = "dragonfly",
418            target_os = "netbsd",
419            target_os = "openbsd",
420            target_os = "illumos",
421        ))]
422        fn uname(buf: *mut UtsName) -> c_int;
423
424        #[cfg(target_os = "freebsd")]
425        fn __xuname(nmln: c_int, buf: *mut c_void) -> c_int;
426    }
427
428    // Polyfill `uname()` for FreeBSD
429    #[inline(always)]
430    #[cfg(target_os = "freebsd")]
431    unsafe extern "C" fn uname(buf: *mut UtsName) -> c_int {
432        __xuname(256, buf.cast())
433    }
434
435    uname(buf)
436}
437
438impl Target for Os {
439    fn langs(self) -> Result<String> {
440        super::unix_lang()
441    }
442
443    fn realname(self) -> Result<OsString> {
444        getpwuid(Name::Real)
445    }
446
447    fn username(self) -> Result<OsString> {
448        getpwuid(Name::User)
449    }
450
451    fn devicename(self) -> Result<OsString> {
452        #[cfg(target_os = "macos")]
453        {
454            let out = os_from_cfstring(unsafe {
455                SCDynamicStoreCopyComputerName(null_mut(), null_mut())
456            });
457
458            if out.as_bytes().is_empty() {
459                return Err(super::err_empty_record());
460            }
461
462            Ok(out)
463        }
464
465        #[cfg(target_os = "illumos")]
466        {
467            let mut nodename = fs::read("/etc/nodename")?;
468
469            // Remove all at and after the first newline (before end of file)
470            if let Some(slice) = nodename.split(|x| *x == b'\n').next() {
471                nodename.drain(slice.len()..);
472            }
473
474            if nodename.is_empty() {
475                return Err(super::err_empty_record());
476            }
477
478            Ok(OsString::from_vec(nodename))
479        }
480
481        #[cfg(any(
482            target_os = "linux",
483            target_os = "dragonfly",
484            target_os = "freebsd",
485            target_os = "netbsd",
486            target_os = "openbsd",
487        ))]
488        {
489            let machine_info = fs::read("/etc/machine-info")?;
490
491            for i in machine_info.split(|b| *b == b'\n') {
492                let mut j = i.split(|b| *b == b'=');
493
494                if j.next() == Some(b"PRETTY_HOSTNAME") {
495                    if let Some(value) = j.next() {
496                        // FIXME: Can " be escaped in pretty name?
497                        return Ok(OsString::from_vec(value.to_vec()));
498                    }
499                }
500            }
501
502            Err(super::err_missing_record())
503        }
504    }
505
506    fn hostname(self) -> Result<String> {
507        // Maximum hostname length = 255, plus a NULL byte.
508        let mut string = Vec::<u8>::with_capacity(256);
509
510        unsafe {
511            if gethostname(string.as_mut_ptr().cast(), 255) == -1 {
512                return Err(Error::last_os_error());
513            }
514
515            string.set_len(strlen(string.as_ptr().cast()));
516        };
517
518        String::from_utf8(string).map_err(|_| {
519            Error::new(ErrorKind::InvalidData, "Hostname not valid UTF-8")
520        })
521    }
522
523    fn distro(self) -> Result<String> {
524        #[cfg(target_os = "macos")]
525        {
526            if let Ok(data) = fs::read_to_string(
527                "/System/Library/CoreServices/ServerVersion.plist",
528            ) {
529                distro_xml(data)
530            } else if let Ok(data) = fs::read_to_string(
531                "/System/Library/CoreServices/SystemVersion.plist",
532            ) {
533                distro_xml(data)
534            } else {
535                Err(super::err_missing_record())
536            }
537        }
538
539        #[cfg(any(
540            target_os = "linux",
541            target_os = "dragonfly",
542            target_os = "freebsd",
543            target_os = "netbsd",
544            target_os = "openbsd",
545            target_os = "illumos",
546        ))]
547        {
548            let program = fs::read("/etc/os-release")?;
549            let distro = String::from_utf8_lossy(&program);
550            let err = || Error::new(ErrorKind::InvalidData, "Parsing failed");
551            let mut fallback = None;
552
553            for i in distro.split('\n') {
554                let mut j = i.split('=');
555
556                match j.next().ok_or_else(err)? {
557                    "PRETTY_NAME" => {
558                        return Ok(j
559                            .next()
560                            .ok_or_else(err)?
561                            .trim_matches('"')
562                            .to_string());
563                    }
564                    "NAME" => {
565                        fallback = Some(
566                            j.next()
567                                .ok_or_else(err)?
568                                .trim_matches('"')
569                                .to_string(),
570                        )
571                    }
572                    _ => {}
573                }
574            }
575
576            fallback.ok_or_else(err)
577        }
578    }
579
580    fn desktop_env(self) -> DesktopEnv {
581        #[cfg(target_os = "macos")]
582        let env = "Aqua";
583
584        // FIXME: WhoAmI 2.0: use `let else`
585        #[cfg(any(
586            target_os = "linux",
587            target_os = "dragonfly",
588            target_os = "freebsd",
589            target_os = "netbsd",
590            target_os = "openbsd",
591            target_os = "illumos",
592        ))]
593        let env = env::var_os("DESKTOP_SESSION");
594        #[cfg(any(
595            target_os = "linux",
596            target_os = "dragonfly",
597            target_os = "freebsd",
598            target_os = "netbsd",
599            target_os = "openbsd",
600            target_os = "illumos",
601        ))]
602        let env = if let Some(ref env) = env {
603            env.to_string_lossy()
604        } else {
605            return DesktopEnv::Unknown("Unknown".to_string());
606        };
607
608        if env.eq_ignore_ascii_case("AQUA") {
609            DesktopEnv::Aqua
610        } else if env.eq_ignore_ascii_case("GNOME") {
611            DesktopEnv::Gnome
612        } else if env.eq_ignore_ascii_case("LXDE") {
613            DesktopEnv::Lxde
614        } else if env.eq_ignore_ascii_case("OPENBOX") {
615            DesktopEnv::Openbox
616        } else if env.eq_ignore_ascii_case("I3") {
617            DesktopEnv::I3
618        } else if env.eq_ignore_ascii_case("UBUNTU") {
619            DesktopEnv::Ubuntu
620        } else if env.eq_ignore_ascii_case("PLASMA5") {
621            DesktopEnv::Kde
622        // TODO: Other Linux Desktop Environments
623        } else {
624            DesktopEnv::Unknown(env.to_string())
625        }
626    }
627
628    #[inline(always)]
629    fn platform(self) -> Platform {
630        #[cfg(target_os = "linux")]
631        {
632            Platform::Linux
633        }
634
635        #[cfg(target_os = "macos")]
636        {
637            Platform::MacOS
638        }
639
640        #[cfg(any(
641            target_os = "dragonfly",
642            target_os = "freebsd",
643            target_os = "netbsd",
644            target_os = "openbsd",
645        ))]
646        {
647            Platform::Bsd
648        }
649
650        #[cfg(target_os = "illumos")]
651        {
652            Platform::Illumos
653        }
654    }
655
656    #[inline(always)]
657    fn arch(self) -> Result<Arch> {
658        let mut buf = UtsName::default();
659
660        if unsafe { uname(&mut buf) } == -1 {
661            return Err(Error::last_os_error());
662        }
663
664        let arch_str =
665            unsafe { CStr::from_ptr(buf.machine.as_ptr()) }.to_string_lossy();
666
667        Ok(match arch_str.as_ref() {
668            "aarch64" | "arm64" | "aarch64_be" | "armv8b" | "armv8l" => {
669                Arch::Arm64
670            }
671            "armv5" => Arch::ArmV5,
672            "armv6" | "arm" => Arch::ArmV6,
673            "armv7" => Arch::ArmV7,
674            "i386" => Arch::I386,
675            "i586" => Arch::I586,
676            "i686" | "i686-AT386" => Arch::I686,
677            "mips" => Arch::Mips,
678            "mipsel" => Arch::MipsEl,
679            "mips64" => Arch::Mips64,
680            "mips64el" => Arch::Mips64El,
681            "powerpc" | "ppc" | "ppcle" => Arch::PowerPc,
682            "powerpc64" | "ppc64" | "ppc64le" => Arch::PowerPc64,
683            "powerpc64le" => Arch::PowerPc64Le,
684            "riscv32" => Arch::Riscv32,
685            "riscv64" => Arch::Riscv64,
686            "s390x" => Arch::S390x,
687            "sparc" => Arch::Sparc,
688            "sparc64" => Arch::Sparc64,
689            "x86_64" | "amd64" => Arch::X64,
690            _ => Arch::Unknown(arch_str.into_owned()),
691        })
692    }
693}