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