1 // WhoAmI
2 // Copyright © 2017-2021 Jeron Aldaron Lau.
3 //
4 // Licensed under any of:
5 //  - Apache License, Version 2.0 (https://www.apache.org/licenses/LICENSE-2.0)
6 //  - MIT License (https://mit-license.org/)
7 //  - Boost Software License, Version 1.0 (https://www.boost.org/LICENSE_1_0.txt)
8 // At your choosing (See accompanying files LICENSE_APACHE_2_0.txt,
9 // LICENSE_MIT.txt and LICENSE_BOOST_1_0.txt).
10 
11 use crate::{DesktopEnv, Platform};
12 
13 use std::ffi::{c_void, OsString};
14 use std::mem;
15 use std::os::unix::ffi::OsStringExt;
16 
17 #[cfg(target_os = "macos")]
18 use std::{
19     os::{
20         raw::{c_long, c_uchar},
21         unix::ffi::OsStrExt,
22     },
23     ptr::null_mut,
24 };
25 
26 #[repr(C)]
27 struct PassWd {
28     pw_name: *const c_void,
29     pw_passwd: *const c_void,
30     pw_uid: u32,
31     pw_gid: u32,
32     #[cfg(any(
33         target_os = "macos",
34         target_os = "freebsd",
35         target_os = "dragonfly",
36         target_os = "bitrig",
37         target_os = "openbsd",
38         target_os = "netbsd"
39     ))]
40     pw_change: isize,
41     #[cfg(any(
42         target_os = "macos",
43         target_os = "freebsd",
44         target_os = "dragonfly",
45         target_os = "bitrig",
46         target_os = "openbsd",
47         target_os = "netbsd"
48     ))]
49     pw_class: *const c_void,
50     pw_gecos: *const c_void,
51     pw_dir: *const c_void,
52     pw_shell: *const c_void,
53     #[cfg(any(
54         target_os = "macos",
55         target_os = "freebsd",
56         target_os = "dragonfly",
57         target_os = "bitrig",
58         target_os = "openbsd",
59         target_os = "netbsd"
60     ))]
61     pw_expire: isize,
62     #[cfg(any(
63         target_os = "macos",
64         target_os = "freebsd",
65         target_os = "dragonfly",
66         target_os = "bitrig",
67         target_os = "openbsd",
68         target_os = "netbsd"
69     ))]
70     pw_fields: i32,
71 }
72 
73 extern "system" {
getpwuid_r( uid: u32, pwd: *mut PassWd, buf: *mut c_void, buflen: usize, result: *mut *mut PassWd, ) -> i3274     fn getpwuid_r(
75         uid: u32,
76         pwd: *mut PassWd,
77         buf: *mut c_void,
78         buflen: usize,
79         result: *mut *mut PassWd,
80     ) -> i32;
geteuid() -> u3281     fn geteuid() -> u32;
gethostname(name: *mut c_void, len: usize) -> i3282     fn gethostname(name: *mut c_void, len: usize) -> i32;
83 }
84 
85 #[cfg(target_os = "macos")]
86 #[link(name = "CoreFoundation", kind = "framework")]
87 #[link(name = "SystemConfiguration", kind = "framework")]
88 extern "system" {
CFStringGetCString( the_string: *mut c_void, buffer: *mut u8, buffer_size: c_long, encoding: u32, ) -> c_uchar89     fn CFStringGetCString(
90         the_string: *mut c_void,
91         buffer: *mut u8,
92         buffer_size: c_long,
93         encoding: u32,
94     ) -> c_uchar;
CFStringGetLength(the_string: *mut c_void) -> c_long95     fn CFStringGetLength(the_string: *mut c_void) -> c_long;
CFStringGetMaximumSizeForEncoding( length: c_long, encoding: u32, ) -> c_long96     fn CFStringGetMaximumSizeForEncoding(
97         length: c_long,
98         encoding: u32,
99     ) -> c_long;
SCDynamicStoreCopyComputerName( store: *mut c_void, encoding: *mut u32, ) -> *mut c_void100     fn SCDynamicStoreCopyComputerName(
101         store: *mut c_void,
102         encoding: *mut u32,
103     ) -> *mut c_void;
CFRelease(cf: *const c_void)104     fn CFRelease(cf: *const c_void);
105 }
106 
strlen(cs: *const c_void) -> usize107 unsafe fn strlen(cs: *const c_void) -> usize {
108     let mut len = 0;
109     let mut cs: *const u8 = cs.cast();
110     while *cs != 0 {
111         len += 1;
112         cs = cs.offset(1);
113     }
114     len
115 }
116 
strlen_gecos(cs: *const c_void) -> usize117 unsafe fn strlen_gecos(cs: *const c_void) -> usize {
118     let mut len = 0;
119     let mut cs: *const u8 = cs.cast();
120     while *cs != 0 && *cs != b',' {
121         len += 1;
122         cs = cs.offset(1);
123     }
124     len
125 }
126 
127 // Convert an OsString into a String
string_from_os(string: OsString) -> String128 fn string_from_os(string: OsString) -> String {
129     match string.into_string() {
130         Ok(string) => string,
131         Err(string) => string.to_string_lossy().to_string(),
132     }
133 }
134 
os_from_cstring_gecos(string: *const c_void) -> OsString135 fn os_from_cstring_gecos(string: *const c_void) -> OsString {
136     if string.is_null() {
137         return "".to_string().into();
138     }
139 
140     // Get a byte slice of the c string.
141     let slice = unsafe {
142         let length = strlen_gecos(string);
143         std::slice::from_raw_parts(string as *const u8, length)
144     };
145 
146     // Turn byte slice into Rust String.
147     OsString::from_vec(slice.to_vec())
148 }
149 
os_from_cstring(string: *const c_void) -> OsString150 fn os_from_cstring(string: *const c_void) -> OsString {
151     if string.is_null() {
152         return "".to_string().into();
153     }
154 
155     // Get a byte slice of the c string.
156     let slice = unsafe {
157         let length = strlen(string);
158         std::slice::from_raw_parts(string as *const u8, length)
159     };
160 
161     // Turn byte slice into Rust String.
162     OsString::from_vec(slice.to_vec())
163 }
164 
165 #[cfg(target_os = "macos")]
os_from_cfstring(string: *mut c_void) -> OsString166 fn os_from_cfstring(string: *mut c_void) -> OsString {
167     if string.is_null() {
168         return "".to_string().into();
169     }
170 
171     unsafe {
172         let len = CFStringGetLength(string);
173         let capacity =
174             CFStringGetMaximumSizeForEncoding(len, 134_217_984 /*UTF8*/) + 1;
175         let mut out = Vec::with_capacity(capacity as usize);
176         if CFStringGetCString(
177             string,
178             out.as_mut_ptr(),
179             capacity,
180             134_217_984, /*UTF8*/
181         ) != 0
182         {
183             out.set_len(strlen(out.as_ptr().cast())); // Remove trailing NUL byte
184             out.shrink_to_fit();
185             CFRelease(string);
186             OsString::from_vec(out)
187         } else {
188             CFRelease(string);
189             "".to_string().into()
190         }
191     }
192 }
193 
194 // This function must allocate, because a slice or Cow<OsStr> would still
195 // reference `passwd` which is dropped when this function returns.
196 #[inline(always)]
getpwuid(real: bool) -> Result<OsString, OsString>197 fn getpwuid(real: bool) -> Result<OsString, OsString> {
198     const BUF_SIZE: usize = 16_384; // size from the man page
199     let mut buffer = mem::MaybeUninit::<[u8; BUF_SIZE]>::uninit();
200     let mut passwd = mem::MaybeUninit::<PassWd>::uninit();
201     let mut _passwd = mem::MaybeUninit::<*mut PassWd>::uninit();
202 
203     // Get PassWd `struct`.
204     let passwd = unsafe {
205         let ret = getpwuid_r(
206             geteuid(),
207             passwd.as_mut_ptr(),
208             buffer.as_mut_ptr() as *mut c_void,
209             BUF_SIZE,
210             _passwd.as_mut_ptr(),
211         );
212 
213         if ret != 0 {
214             return Ok("".to_string().into());
215         }
216 
217         passwd.assume_init()
218     };
219 
220     // Extract names.
221     if real {
222         let string = os_from_cstring_gecos(passwd.pw_gecos);
223         if string.is_empty() {
224             Err(os_from_cstring(passwd.pw_name))
225         } else {
226             Ok(string)
227         }
228     } else {
229         Ok(os_from_cstring(passwd.pw_name))
230     }
231 }
232 
username() -> String233 pub fn username() -> String {
234     string_from_os(username_os())
235 }
236 
username_os() -> OsString237 pub fn username_os() -> OsString {
238     // Unwrap never fails
239     getpwuid(false).unwrap()
240 }
241 
fancy_fallback(result: Result<&str, String>) -> String242 fn fancy_fallback(result: Result<&str, String>) -> String {
243     let mut cap = true;
244     let iter = match result {
245         Ok(a) => a.chars(),
246         Err(ref b) => b.chars(),
247     };
248     let mut new = String::new();
249     for c in iter {
250         match c {
251             '.' | '-' | '_' => {
252                 new.push(' ');
253                 cap = true;
254             }
255             a => {
256                 if cap {
257                     cap = false;
258                     for i in a.to_uppercase() {
259                         new.push(i);
260                     }
261                 } else {
262                     new.push(a);
263                 }
264             }
265         }
266     }
267     new
268 }
269 
fancy_fallback_os(result: Result<OsString, OsString>) -> OsString270 fn fancy_fallback_os(result: Result<OsString, OsString>) -> OsString {
271     match result {
272         Ok(success) => success,
273         Err(fallback) => {
274             let cs = match fallback.to_str() {
275                 Some(a) => Ok(a),
276                 None => Err(fallback.to_string_lossy().to_string()),
277             };
278 
279             fancy_fallback(cs).into()
280         }
281     }
282 }
283 
realname() -> String284 pub fn realname() -> String {
285     string_from_os(realname_os())
286 }
287 
realname_os() -> OsString288 pub fn realname_os() -> OsString {
289     // If no real name is provided, guess based on username.
290     fancy_fallback_os(getpwuid(true))
291 }
292 
293 #[cfg(not(target_os = "macos"))]
devicename_os() -> OsString294 pub fn devicename_os() -> OsString {
295     devicename().into()
296 }
297 
298 #[cfg(not(target_os = "macos"))]
devicename() -> String299 pub fn devicename() -> String {
300     let mut distro = String::new();
301 
302     if let Ok(program) = std::fs::read_to_string("/etc/machine-info") {
303         let program = program.into_bytes();
304 
305         distro.push_str(&String::from_utf8(program).unwrap());
306 
307         for i in distro.split('\n') {
308             let mut j = i.split('=');
309 
310             if j.next().unwrap() == "PRETTY_HOSTNAME" {
311                 return j.next().unwrap().trim_matches('"').to_string();
312             }
313         }
314     }
315     fancy_fallback(Err(hostname()))
316 }
317 
318 #[cfg(target_os = "macos")]
devicename() -> String319 pub fn devicename() -> String {
320     string_from_os(devicename_os())
321 }
322 
323 #[cfg(target_os = "macos")]
devicename_os() -> OsString324 pub fn devicename_os() -> OsString {
325     let out = os_from_cfstring(unsafe {
326         SCDynamicStoreCopyComputerName(null_mut(), null_mut())
327     });
328 
329     let computer = if out.as_bytes().is_empty() {
330         Err(hostname_os())
331     } else {
332         Ok(out)
333     };
334     fancy_fallback_os(computer)
335 }
336 
hostname() -> String337 pub fn hostname() -> String {
338     string_from_os(hostname_os())
339 }
340 
hostname_os() -> OsString341 pub fn hostname_os() -> OsString {
342     // Maximum hostname length = 255, plus a NULL byte.
343     let mut string = Vec::<u8>::with_capacity(256);
344     unsafe {
345         gethostname(string.as_mut_ptr() as *mut c_void, 255);
346         string.set_len(strlen(string.as_ptr() as *const c_void));
347     };
348     OsString::from_vec(string)
349 }
350 
351 #[cfg(target_os = "macos")]
distro_xml(data: String) -> Option<String>352 fn distro_xml(data: String) -> Option<String> {
353     let mut product_name = None;
354     let mut user_visible_version = None;
355     if let Some(start) = data.find("<dict>") {
356         if let Some(end) = data.find("</dict>") {
357             let mut set_product_name = false;
358             let mut set_user_visible_version = false;
359             for line in data[start + "<dict>".len()..end].lines() {
360                 let line = line.trim();
361                 if line.starts_with("<key>") {
362                     match line["<key>".len()..].trim_end_matches("</key>") {
363                         "ProductName" => set_product_name = true,
364                         "ProductUserVisibleVersion" => {
365                             set_user_visible_version = true
366                         }
367                         "ProductVersion" => {
368                             if user_visible_version.is_none() {
369                                 set_user_visible_version = true
370                             }
371                         }
372                         _ => {}
373                     }
374                 } else if line.starts_with("<string>") {
375                     if set_product_name {
376                         product_name = Some(
377                             line["<string>".len()..]
378                                 .trim_end_matches("</string>"),
379                         );
380                         set_product_name = false;
381                     } else if set_user_visible_version {
382                         user_visible_version = Some(
383                             line["<string>".len()..]
384                                 .trim_end_matches("</string>"),
385                         );
386                         set_user_visible_version = false;
387                     }
388                 }
389             }
390         }
391     }
392     if let Some(product_name) = product_name {
393         if let Some(user_visible_version) = user_visible_version {
394             Some(format!("{} {}", product_name, user_visible_version))
395         } else {
396             Some(product_name.to_string())
397         }
398     } else if let Some(user_visible_version) = user_visible_version {
399         Some(format!("Mac OS (Unknown) {}", user_visible_version))
400     } else {
401         None
402     }
403 }
404 
405 #[cfg(target_os = "macos")]
distro_os() -> Option<OsString>406 pub fn distro_os() -> Option<OsString> {
407     distro().map(|a| a.into())
408 }
409 
410 #[cfg(target_os = "macos")]
distro() -> Option<String>411 pub fn distro() -> Option<String> {
412     if let Ok(data) = std::fs::read_to_string(
413         "/System/Library/CoreServices/ServerVersion.plist",
414     ) {
415         distro_xml(data)
416     } else if let Ok(data) = std::fs::read_to_string(
417         "/System/Library/CoreServices/SystemVersion.plist",
418     ) {
419         distro_xml(data)
420     } else {
421         None
422     }
423 }
424 
425 #[cfg(not(target_os = "macos"))]
distro_os() -> Option<OsString>426 pub fn distro_os() -> Option<OsString> {
427     distro().map(|a| a.into())
428 }
429 
430 #[cfg(not(target_os = "macos"))]
distro() -> Option<String>431 pub fn distro() -> Option<String> {
432     let mut distro = String::new();
433 
434     let program = std::fs::read_to_string("/etc/os-release")
435         .expect("Couldn't read file /etc/os-release")
436         .into_bytes();
437 
438     distro.push_str(&String::from_utf8_lossy(&program));
439 
440     let mut fallback = None;
441 
442     for i in distro.split('\n') {
443         let mut j = i.split('=');
444 
445         match j.next()? {
446             "PRETTY_NAME" => {
447                 return Some(j.next()?.trim_matches('"').to_string())
448             }
449             "NAME" => fallback = Some(j.next()?.trim_matches('"').to_string()),
450             _ => {}
451         }
452     }
453 
454     if let Some(x) = fallback {
455         Some(x)
456     } else {
457         None
458     }
459 }
460 
461 #[cfg(target_os = "macos")]
462 #[inline(always)]
desktop_env() -> DesktopEnv463 pub const fn desktop_env() -> DesktopEnv {
464     DesktopEnv::Aqua
465 }
466 
467 #[cfg(not(target_os = "macos"))]
468 #[inline(always)]
desktop_env() -> DesktopEnv469 pub fn desktop_env() -> DesktopEnv {
470     match std::env::var_os("DESKTOP_SESSION")
471         .map(|env| env.to_string_lossy().to_string())
472     {
473         Some(env_orig) => {
474             let env = env_orig.to_uppercase();
475 
476             if env.contains("GNOME") {
477                 DesktopEnv::Gnome
478             } else if env.contains("LXDE") {
479                 DesktopEnv::Lxde
480             } else if env.contains("OPENBOX") {
481                 DesktopEnv::Openbox
482             } else if env.contains("I3") {
483                 DesktopEnv::I3
484             } else if env.contains("UBUNTU") {
485                 DesktopEnv::Ubuntu
486             } else if env.contains("PLASMA5") {
487                 DesktopEnv::Kde
488             } else {
489                 DesktopEnv::Unknown(env_orig)
490             }
491         }
492         // TODO: Other Linux Desktop Environments
493         None => DesktopEnv::Unknown("Unknown".to_string()),
494     }
495 }
496 
497 #[cfg(target_os = "macos")]
498 #[inline(always)]
platform() -> Platform499 pub const fn platform() -> Platform {
500     Platform::MacOS
501 }
502 
503 #[cfg(not(any(
504     target_os = "macos",
505     target_os = "freebsd",
506     target_os = "dragonfly",
507     target_os = "bitrig",
508     target_os = "openbsd",
509     target_os = "netbsd"
510 )))]
511 #[inline(always)]
platform() -> Platform512 pub const fn platform() -> Platform {
513     Platform::Linux
514 }
515 
516 #[cfg(any(
517     target_os = "freebsd",
518     target_os = "dragonfly",
519     target_os = "bitrig",
520     target_os = "openbsd",
521     target_os = "netbsd"
522 ))]
523 #[inline(always)]
platform() -> Platform524 pub const fn platform() -> Platform {
525     Platform::Bsd
526 }
527 
528 struct LangIter {
529     array: String,
530     index: Option<bool>,
531 }
532 
533 impl Iterator for LangIter {
534     type Item = String;
535 
next(&mut self) -> Option<Self::Item>536     fn next(&mut self) -> Option<Self::Item> {
537         if self.index? {
538             self.index = Some(false);
539             let mut temp = self.array.split('-').next().unwrap().to_string();
540             std::mem::swap(&mut temp, &mut self.array);
541             Some(temp)
542         } else {
543             self.index = None;
544             let mut temp = String::new();
545             std::mem::swap(&mut temp, &mut self.array);
546             Some(temp)
547         }
548     }
549 }
550 
551 #[inline(always)]
lang() -> impl Iterator<Item = String>552 pub fn lang() -> impl Iterator<Item = String> {
553     let array = std::env::var("LANG")
554         .unwrap_or_default()
555         .split('.')
556         .next()
557         .unwrap_or("en_US")
558         .to_string()
559         .replace("_", "-");
560     LangIter {
561         array,
562         index: Some(true),
563     }
564 }
565