Skip to main content

platform_info/platform/
unix.rs

1// This file is part of the uutils coreutils package.
2//
3// (c) Jian Zeng <anonymousknight96 AT gmail.com>
4// (c) Alex Lyon <arcterus@mail.com>
5//
6// For the full copyright and license information, please view the LICENSE file
7// that was distributed with this source code.
8
9// spell-checker:ignore (API) domainname nodename osname sysname
10// spell-checker:ignore (libc) libc utsname
11// spell-checker:ignore (jargon) hasher
12// spell-checker:ignore (names) Jian Zeng * anonymousknight96
13// spell-checker:ignore (rust) uninit
14// spell-checker:ignore (uutils) coreutils uutils
15// spell-checker:ignore (VSCode) endregion
16
17// refs:
18// [Byte-to/from-String Conversions](https://nicholasbishop.github.io/rust-conversions) @@ <https://archive.is/AnDCY>
19
20#![warn(unused_results)] // enable warnings for unused results
21
22use std::ffi::{OsStr, OsString};
23use std::fmt;
24use std::fmt::{Debug, Formatter};
25
26use crate::{PlatformInfoAPI, PlatformInfoError, UNameAPI};
27
28use unix_safe::{oss_from_cstr, utsname};
29
30// PlatformInfo
31/// Handles initial retrieval and holds cached information for the current platform (a Unix-like OS in this case).
32#[derive(Clone, Debug, PartialEq, Eq)]
33pub struct PlatformInfo {
34    /// Contains the cached results of the `utsname()` system call.
35    // ref: <https://docs.rs/libc/latest/i686-unknown-linux-gnu/libc/struct.utsname.html>
36    pub utsname: UTSName, /* aka "Unix Time-sharing System Name"; ref: <https://stackoverflow.com/questions/41669397/whats-the-meaning-of-utsname-in-linux> */
37    // * private-use fields
38    sysname: OsString,
39    nodename: OsString,
40    release: OsString,
41    version: OsString,
42    machine: OsString,
43    processor: OsString,
44    osname: OsString,
45}
46
47impl PlatformInfoAPI for PlatformInfo {
48    // * note: this function *should* never fail
49    fn new() -> Result<Self, PlatformInfoError> {
50        let utsname = UTSName(utsname()?);
51        let machine = oss_from_cstr(&utsname.0.machine);
52        let processor = OsString::from(crate::lib_impl::map_processor(&machine.to_string_lossy()));
53        Ok(Self {
54            utsname,
55            sysname: oss_from_cstr(&utsname.0.sysname),
56            nodename: oss_from_cstr(&utsname.0.nodename),
57            release: oss_from_cstr(&utsname.0.release),
58            version: oss_from_cstr(&utsname.0.version),
59            machine,
60            processor,
61            osname: OsString::from(crate::lib_impl::HOST_OS_NAME),
62        })
63    }
64}
65
66impl UNameAPI for PlatformInfo {
67    fn sysname(&self) -> &OsStr {
68        &self.sysname
69    }
70
71    fn nodename(&self) -> &OsStr {
72        &self.nodename
73    }
74
75    fn release(&self) -> &OsStr {
76        &self.release
77    }
78
79    fn version(&self) -> &OsStr {
80        &self.version
81    }
82
83    fn machine(&self) -> &OsStr {
84        &self.machine
85    }
86
87    fn processor(&self) -> &OsStr {
88        &self.processor
89    }
90
91    fn osname(&self) -> &OsStr {
92        &self.osname
93    }
94}
95
96//===
97
98// UTSName
99/// Contains information about the current computer system.
100///
101/// Wraps [`libc::utsname`].
102// ref: <https://docs.rs/libc/latest/i686-unknown-linux-gnu/libc/struct.utsname.html>
103/*
104    pub struct utsname {
105        pub sysname: [::c_char; 65],
106        pub nodename: [::c_char; 65],
107        pub release: [::c_char; 65],
108        pub version: [::c_char; 65],
109        pub machine: [::c_char; 65],
110        pub domainname: [::c_char; 65]
111    }
112*/
113// aka "Unix Time-sharing System Name"; ref: <https://stackoverflow.com/questions/41669397/whats-the-meaning-of-utsname-in-linux>
114#[derive(Clone, Copy /* , Debug, PartialEq, Eq */)]
115pub struct UTSName(libc::utsname);
116
117impl Debug for UTSName {
118    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
119        let mut debug_struct = &mut f.debug_struct("UTSName");
120        debug_struct = debug_struct
121            .field("sysname", &oss_from_cstr(&self.0.sysname))
122            .field("nodename", &oss_from_cstr(&self.0.nodename))
123            .field("release", &oss_from_cstr(&self.0.release))
124            .field("version", &oss_from_cstr(&self.0.version))
125            .field("machine", &oss_from_cstr(&self.0.machine));
126        // The domainname field is not part of the POSIX standard but a GNU extension. Therefore
127        // BSD-like platforms and solaris/illumos are missing the domainname field.
128        #[cfg(not(any(
129            target_os = "aix",
130            target_os = "illumos",
131            target_os = "solaris",
132            target_os = "macos",
133            target_os = "ios",
134            target_os = "dragonfly",
135            target_os = "freebsd",
136            target_os = "openbsd",
137            target_os = "netbsd",
138            target_os = "haiku"
139        )))]
140        {
141            debug_struct = debug_struct.field("domainname", &oss_from_cstr(&self.0.domainname));
142        }
143        debug_struct.finish()
144    }
145}
146
147impl PartialEq for UTSName {
148    fn eq(&self, other: &Self) -> bool {
149        let mut equal = true; // avoid 'unused-mut' and 'clippy::let-and-return' warnings on MacOS
150        equal = equal
151            && (
152                self.0.sysname,
153                self.0.nodename,
154                self.0.release,
155                self.0.version,
156                self.0.machine,
157            ) == (
158                other.0.sysname,
159                other.0.nodename,
160                other.0.release,
161                other.0.version,
162                other.0.machine,
163            );
164        // The domainname field is not part of the POSIX standard but a GNU extension. Therefore
165        // BSD-like platforms and solaris/illumos are missing the domainname field.
166        #[cfg(not(any(
167            target_os = "aix",
168            target_os = "illumos",
169            target_os = "solaris",
170            target_os = "macos",
171            target_os = "ios",
172            target_os = "dragonfly",
173            target_os = "freebsd",
174            target_os = "openbsd",
175            target_os = "netbsd",
176            target_os = "haiku"
177        )))]
178        {
179            equal = equal && (self.0.domainname == other.0.domainname);
180        }
181        equal
182    }
183}
184
185impl Eq for UTSName {}
186
187//===
188
189//#region unsafe code
190mod unix_safe {
191    use std::convert::TryFrom;
192    use std::ffi::{CStr, OsStr, OsString};
193    use std::io;
194    use std::mem::MaybeUninit;
195    use std::os::unix::ffi::OsStrExt;
196
197    // oss_from_str()
198    /// *Returns* an `OsString` created from a `libc::c_char` slice.
199    pub fn oss_from_cstr(slice: &[libc::c_char]) -> OsString {
200        assert!(slice.len() < usize::try_from(isize::MAX).unwrap());
201        assert!(slice.iter().position(|&c| c == 0 /* NUL */).unwrap() < slice.len());
202        OsString::from(OsStr::from_bytes(
203            unsafe { CStr::from_ptr(slice.as_ptr()) }.to_bytes(),
204        ))
205    }
206
207    // utsname()
208    /// *Returns* a `libc::utsname` structure containing `uname`-like OS system information.
209    pub fn utsname() -> Result<libc::utsname, std::io::Error> {
210        // ref: <https://docs.rs/libc/latest/i686-unknown-linux-gnu/libc/fn.uname.html>
211        // ref: <https://docs.rs/libc/latest/i686-unknown-linux-gnu/libc/struct.utsname.html>
212        let mut uts = MaybeUninit::<libc::utsname>::uninit();
213        let result = unsafe { libc::uname(uts.as_mut_ptr()) };
214        if result != -1 {
215            // SAFETY: `libc::uname()` succeeded => `uts` was initialized
216            Ok(unsafe { uts.assume_init() })
217        } else {
218            Err(io::Error::last_os_error())
219        }
220    }
221}
222//#endregion (unsafe code)
223
224//=== Tests
225
226#[test]
227fn test_osname() {
228    let info = PlatformInfo::new().unwrap();
229    let osname = info.osname().to_string_lossy();
230    assert!(osname.starts_with(crate::lib_impl::HOST_OS_NAME));
231}
232
233#[test]
234fn test_processor() {
235    let info = PlatformInfo::new().unwrap();
236    let processor = info.processor().to_string_lossy();
237
238    // Processor should not be empty
239    assert!(!processor.is_empty());
240
241    // On common platforms, verify expected mappings
242    #[cfg(all(target_arch = "aarch64", target_os = "macos"))]
243    assert_eq!(processor, "arm", "macOS arm64 should map to 'arm'");
244
245    #[cfg(all(target_arch = "aarch64", target_os = "linux"))]
246    assert_eq!(processor, "aarch64", "Linux aarch64 should pass through");
247
248    #[cfg(target_arch = "x86_64")]
249    assert_eq!(processor, "x86_64", "x86_64 should pass through");
250
251    #[cfg(target_arch = "x86")]
252    assert_eq!(processor, "i686", "x86 variants should normalize to i686");
253}
254
255#[test]
256fn structure_clone() {
257    let info = PlatformInfo::new().unwrap();
258    println!("{info:?}");
259    #[allow(clippy::redundant_clone)] // ignore `clippy::redundant_clone` warning for direct testing
260    let info_copy = info.clone();
261    assert_eq!(info_copy, info);
262}