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