From f499c74cc745c6894df190d729b98772dc12bb50 Mon Sep 17 00:00:00 2001 From: Jeron Aldaron Lau Date: Mon, 13 Nov 2023 18:42:21 -0600 Subject: [PATCH] Implement Fallible API (#71) * Fallible for `username()`, `realname()` and `_os` variants * Minor fixes * Add remaining planned fallible APIs * Compile error fixes * Fix unused imports * Address self-feedback * Address `clippy::needless_doctest_main` https://rust-lang.github.io/rust-clippy/master/index.html#needless_doctest_main * Fix possible memory leak in `distro()` on Windows * More windows bugs * Move fallbacks to lib.rs * Clean up * Fix wasm build --- WASM.md | 2 +- src/fake.rs | 38 ++++----- src/fallible.rs | 97 +++++++++++++++++++++ src/lib.rs | 127 +++++++++++++++------------- src/unix.rs | 220 ++++++++++++++++++++++-------------------------- src/web.rs | 78 +++++++++-------- src/windows.rs | 157 ++++++++++++++++++++-------------- 7 files changed, 422 insertions(+), 297 deletions(-) create mode 100644 src/fallible.rs diff --git a/WASM.md b/WASM.md index 4c8aaf2..1cd9995 100644 --- a/WASM.md +++ b/WASM.md @@ -24,7 +24,7 @@ web-sys, and will instead return these mock values: - `devicename()`: "Unknown" - `hostname()`: "localhost" - `platform()`: "Unknown" - - `distro()`: "Unknown Unknown" + - `distro()`: "Emulated" - `desktop_env()`: "Unknown WebAssembly" - `arch()`: "wasm32" diff --git a/src/fake.rs b/src/fake.rs index eb3a897..0fe5e1a 100644 --- a/src/fake.rs +++ b/src/fake.rs @@ -5,7 +5,7 @@ compile_error!("Unexpected pointer width for target platform"); use std::ffi::OsString; -use crate::{Arch, DesktopEnv, Platform}; +use crate::{Arch, DesktopEnv, Platform, Result}; #[inline(always)] pub(crate) fn lang() -> impl Iterator { @@ -13,48 +13,48 @@ pub(crate) fn lang() -> impl Iterator { } #[inline(always)] -pub(crate) fn username_os() -> OsString { - username().into() +pub(crate) fn username_os() -> Result { + Ok(username()?.into()) } #[inline(always)] -pub(crate) fn realname_os() -> OsString { - realname().into() +pub(crate) fn realname_os() -> Result { + Ok(realname()?.into()) } #[inline(always)] -pub(crate) fn devicename_os() -> OsString { - devicename().into() +pub(crate) fn devicename_os() -> Result { + Ok(devicename()?.into()) } #[inline(always)] -pub(crate) fn distro_os() -> Option { - distro().map(|a| a.into()) +pub(crate) fn distro_os() -> Result { + Ok(distro()?.into()) } #[inline(always)] -pub(crate) fn username() -> String { - "anonymous".to_string() +pub(crate) fn username() -> Result { + Ok("anonymous".to_string()) } #[inline(always)] -pub(crate) fn realname() -> String { - "Anonymous".to_string() +pub(crate) fn realname() -> Result { + Ok("Anonymous".to_string()) } #[inline(always)] -pub(crate) fn devicename() -> String { - "Unknown".to_string() +pub(crate) fn devicename() -> Result { + Ok("Unknown".to_string()) } #[inline(always)] -pub(crate) fn hostname() -> String { - "localhost".to_string() +pub(crate) fn hostname() -> Result { + Ok("localhost".to_string()) } #[inline(always)] -pub(crate) fn distro() -> Option { - None +pub(crate) fn distro() -> Result { + Ok("Emulated".to_string()) } #[inline(always)] diff --git a/src/fallible.rs b/src/fallible.rs new file mode 100644 index 0000000..5279c99 --- /dev/null +++ b/src/fallible.rs @@ -0,0 +1,97 @@ +//! Fallible versions of the whoami APIs. +//! +//! Some of the functions in the root module will return "Unknown" or +//! "localhost" on error. This might not be desirable in some situations. The +//! functions in this module all return a [`Result`]. + +use std::ffi::OsString; + +use crate::{platform, Result}; + +/// Get the user's username. +/// +/// On unix-systems this differs from [`realname()`] most notably in that spaces +/// are not allowed in the username. +#[inline(always)] +pub fn username() -> Result { + platform::username() +} + +/// Get the user's username. +/// +/// On unix-systems this differs from [`realname_os()`] most notably in that +/// spaces are not allowed in the username. +#[inline(always)] +pub fn username_os() -> Result { + platform::username_os() +} + +/// Get the user's real (full) name. +#[inline(always)] +pub fn realname() -> Result { + platform::realname() +} + +/// Get the user's real (full) name. +#[inline(always)] +pub fn realname_os() -> Result { + platform::realname_os() +} + +/// Get the name of the operating system distribution and (possibly) version. +/// +/// Example: "Windows 10" or "Fedora 26 (Workstation Edition)" +#[inline(always)] +pub fn distro() -> Result { + platform::distro() +} + +/// Get the name of the operating system distribution and (possibly) version. +/// +/// Example: "Windows 10" or "Fedora 26 (Workstation Edition)" +#[inline(always)] +pub fn distro_os() -> Result { + platform::distro_os() +} + +/// Get the device name (also known as "Pretty Name"). +/// +/// Often used to identify device for bluetooth pairing. +#[inline(always)] +pub fn devicename() -> Result { + platform::devicename() +} + +/// Get the device name (also known as "Pretty Name"). +/// +/// Often used to identify device for bluetooth pairing. +#[inline(always)] +pub fn devicename_os() -> Result { + platform::devicename_os() +} + +/// Get the host device's hostname. +/// +/// Limited to a-z (case insensitve), 0-9, and dashes. This limit also applies +/// to `devicename()` when targeting Windows. Since the hostname is +/// case-insensitive, this method normalizes to lowercase (unlike +/// [`devicename()`]). +#[inline(always)] +pub fn hostname() -> Result { + let mut hostname = platform::hostname()?; + + hostname.make_ascii_lowercase(); + + Ok(hostname) +} + +/// Get the host device's hostname. +/// +/// Limited to a-z (case insensitve), 0-9, and dashes. This limit also applies +/// to `devicename()` when targeting Windows. Since the hostname is +/// case-insensitive, this method normalizes to lowercase (unlike +/// [`devicename()`]). +#[inline(always)] +pub fn hostname_os() -> Result { + Ok(hostname()?.into()) +} diff --git a/src/lib.rs b/src/lib.rs index f27acd5..d8deb57 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,44 +9,42 @@ //! return [`OsString`]): //! //! ```rust -//! fn main() { -//! println!( -//! "User's Name whoami::realname(): {}", -//! whoami::realname(), -//! ); -//! println!( -//! "User's Username whoami::username(): {}", -//! whoami::username(), -//! ); -//! println!( -//! "User's Language whoami::lang(): {:?}", -//! whoami::lang().collect::>(), -//! ); -//! println!( -//! "Device's Pretty Name whoami::devicename(): {}", -//! whoami::devicename(), -//! ); -//! println!( -//! "Device's Hostname whoami::hostname(): {}", -//! whoami::hostname(), -//! ); -//! println!( -//! "Device's Platform whoami::platform(): {}", -//! whoami::platform(), -//! ); -//! println!( -//! "Device's OS Distro whoami::distro(): {}", -//! whoami::distro(), -//! ); -//! println!( -//! "Device's Desktop Env. whoami::desktop_env(): {}", -//! whoami::desktop_env(), -//! ); -//! println!( -//! "Device's CPU Arch whoami::arch(): {}", -//! whoami::arch(), -//! ); -//! } +//! println!( +//! "User's Name whoami::realname(): {}", +//! whoami::realname(), +//! ); +//! println!( +//! "User's Username whoami::username(): {}", +//! whoami::username(), +//! ); +//! println!( +//! "User's Language whoami::lang(): {:?}", +//! whoami::lang().collect::>(), +//! ); +//! println!( +//! "Device's Pretty Name whoami::devicename(): {}", +//! whoami::devicename(), +//! ); +//! println!( +//! "Device's Hostname whoami::hostname(): {}", +//! whoami::hostname(), +//! ); +//! println!( +//! "Device's Platform whoami::platform(): {}", +//! whoami::platform(), +//! ); +//! println!( +//! "Device's OS Distro whoami::distro(): {}", +//! whoami::distro(), +//! ); +//! println!( +//! "Device's Desktop Env. whoami::desktop_env(): {}", +//! whoami::desktop_env(), +//! ); +//! println!( +//! "Device's CPU Arch whoami::arch(): {}", +//! whoami::arch(), +//! ); //! ``` #![warn( @@ -70,6 +68,11 @@ html_favicon_url = "https://raw.githubusercontent.com/ardaku/whoami/stable/res/icon.svg" )] +const DEFAULT_USERNAME: &str = "Unknown"; +const DEFAULT_HOSTNAME: &str = "Localhost"; + +pub mod fallible; + #[allow(unsafe_code)] // Unix #[cfg_attr( @@ -337,12 +340,10 @@ pub enum Width { impl Display for Width { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let bits = match self { + f.write_str(match self { Width::Bits32 => "32 bits", Width::Bits64 => "64 bits", - }; - - f.write_str(bits) + }) } } @@ -376,7 +377,7 @@ impl Arch { ErrorKind::InvalidData, format!( "Tried getting width of unknown arch ({})", - unknown_arch + unknown_arch, ), )), } @@ -392,31 +393,36 @@ pub fn arch() -> Arch { /// Get the user's username. /// /// On unix-systems this differs from [`realname()`] most notably in that spaces -/// are not allowed. +/// are not allowed in the username. #[inline(always)] pub fn username() -> String { - platform::username() + fallible::username().unwrap_or_else(|_| DEFAULT_USERNAME.to_lowercase()) } /// Get the user's username. /// -/// On unix-systems this differs from [`realname()`] most notably in that spaces -/// are not allowed. +/// On unix-systems this differs from [`realname_os()`] most notably in that +/// spaces are not allowed in the username. #[inline(always)] pub fn username_os() -> OsString { - platform::username_os() + fallible::username_os() + .unwrap_or_else(|_| DEFAULT_USERNAME.to_lowercase().into()) } /// Get the user's real (full) name. #[inline(always)] pub fn realname() -> String { - platform::realname() + fallible::realname() + .or_else(|_| fallible::username()) + .unwrap_or_else(|_| DEFAULT_USERNAME.to_owned()) } /// Get the user's real (full) name. #[inline(always)] pub fn realname_os() -> OsString { - platform::realname_os() + fallible::realname_os() + .or_else(|_| fallible::username_os()) + .unwrap_or_else(|_| DEFAULT_USERNAME.to_owned().into()) } /// Get the device name (also known as "Pretty Name"). @@ -424,7 +430,9 @@ pub fn realname_os() -> OsString { /// Often used to identify device for bluetooth pairing. #[inline(always)] pub fn devicename() -> String { - platform::devicename() + fallible::devicename() + .or_else(|_| fallible::hostname()) + .unwrap_or_else(|_| DEFAULT_HOSTNAME.to_string()) } /// Get the device name (also known as "Pretty Name"). @@ -432,7 +440,9 @@ pub fn devicename() -> String { /// Often used to identify device for bluetooth pairing. #[inline(always)] pub fn devicename_os() -> OsString { - platform::devicename_os() + fallible::devicename_os() + .or_else(|_| fallible::hostname_os()) + .unwrap_or_else(|_| DEFAULT_HOSTNAME.to_string().into()) } /// Get the host device's hostname. @@ -443,9 +453,7 @@ pub fn devicename_os() -> OsString { /// [`devicename()`]). #[inline(always)] pub fn hostname() -> String { - let mut hostname = platform::hostname(); - hostname.make_ascii_lowercase(); - hostname + fallible::hostname().unwrap_or_else(|_| DEFAULT_HOSTNAME.to_lowercase()) } /// Get the host device's hostname. @@ -456,7 +464,8 @@ pub fn hostname() -> String { /// [`devicename()`]). #[inline(always)] pub fn hostname_os() -> OsString { - hostname().into() + fallible::hostname_os() + .unwrap_or_else(|_| DEFAULT_HOSTNAME.to_lowercase().into()) } /// Get the name of the operating system distribution and (possibly) version. @@ -464,7 +473,7 @@ pub fn hostname_os() -> OsString { /// Example: "Windows 10" or "Fedora 26 (Workstation Edition)" #[inline(always)] pub fn distro() -> String { - platform::distro().unwrap_or_else(|| format!("Unknown {}", platform())) + fallible::distro().unwrap_or_else(|_| format!("Unknown {}", platform())) } /// Get the name of the operating system distribution and (possibly) version. @@ -472,8 +481,8 @@ pub fn distro() -> String { /// Example: "Windows 10" or "Fedora 26 (Workstation Edition)" #[inline(always)] pub fn distro_os() -> OsString { - platform::distro_os() - .unwrap_or_else(|| format!("Unknown {}", platform()).into()) + fallible::distro_os() + .unwrap_or_else(|_| format!("Unknown {}", platform()).into()) } /// Get the desktop environment. diff --git a/src/unix.rs b/src/unix.rs index 99257ca..3f867c7 100644 --- a/src/unix.rs +++ b/src/unix.rs @@ -1,6 +1,7 @@ use std::{ borrow::Cow, ffi::{c_void, CStr, OsString}, + io::{Error, ErrorKind}, mem, os::{ raw::{c_char, c_int}, @@ -16,7 +17,7 @@ use std::{ ptr::null_mut, }; -use crate::{Arch, DesktopEnv, Platform}; +use crate::{Arch, DesktopEnv, Platform, Result}; #[repr(C)] struct PassWd { @@ -127,37 +128,44 @@ fn string_from_os(string: OsString) -> String { } } -fn os_from_cstring_gecos(string: *const c_void) -> Option { +fn os_from_cstring_gecos(string: *const c_void) -> Result { if string.is_null() { - return None; + return Err(Error::new(ErrorKind::NotFound, "Null record")); } // Get a byte slice of the c string. let slice = unsafe { let length = strlen_gecos(string); + if length == 0 { - return None; + return Err(Error::new(ErrorKind::InvalidData, "Empty record")); } - std::slice::from_raw_parts(string as *const u8, length) + + std::slice::from_raw_parts(string.cast(), length) }; // Turn byte slice into Rust String. - Some(OsString::from_vec(slice.to_vec())) + Ok(OsString::from_vec(slice.to_vec())) } -fn os_from_cstring(string: *const c_void) -> OsString { +fn os_from_cstring(string: *const c_void) -> Result { if string.is_null() { - return "".to_string().into(); + return Err(Error::new(ErrorKind::NotFound, "Null record")); } // Get a byte slice of the c string. let slice = unsafe { let length = strlen(string); - std::slice::from_raw_parts(string as *const u8, length) + + if length == 0 { + return Err(Error::new(ErrorKind::InvalidData, "Empty record")); + } + + std::slice::from_raw_parts(string.cast(), length) }; // Turn byte slice into Rust String. - OsString::from_vec(slice.to_vec()) + Ok(OsString::from_vec(slice.to_vec())) } #[cfg(target_os = "macos")] @@ -192,7 +200,7 @@ fn os_from_cfstring(string: *mut c_void) -> OsString { // This function must allocate, because a slice or Cow would still // reference `passwd` which is dropped when this function returns. #[inline(always)] -fn getpwuid(real: bool) -> OsString { +fn getpwuid(real: bool) -> Result { const BUF_SIZE: usize = 16_384; // size from the man page let mut buffer = mem::MaybeUninit::<[u8; BUF_SIZE]>::uninit(); let mut passwd = mem::MaybeUninit::::uninit(); @@ -209,13 +217,13 @@ fn getpwuid(real: bool) -> OsString { ); if ret != 0 { - return "Unknown".to_string().into(); + return Err(Error::last_os_error()); } let _passwd = _passwd.assume_init(); if _passwd.is_null() { - return "Unknown".to_string().into(); + return Err(Error::new(ErrorKind::NotFound, "Null record")); } passwd.assume_init() @@ -223,156 +231,116 @@ fn getpwuid(real: bool) -> OsString { // Extract names. if real { - let string = os_from_cstring_gecos(passwd.pw_gecos); - let result = if let Some(string) = string { - Ok(string) - } else { - Err(os_from_cstring(passwd.pw_name)) - }; - fancy_fallback_os(result) + os_from_cstring_gecos(passwd.pw_gecos) } else { os_from_cstring(passwd.pw_name) } } -pub(crate) fn username() -> String { - string_from_os(username_os()) +pub(crate) fn username() -> Result { + Ok(string_from_os(username_os()?)) } -pub(crate) fn username_os() -> OsString { +pub(crate) fn username_os() -> Result { getpwuid(false) } -fn fancy_fallback(result: Result<&str, String>) -> String { - let mut cap = true; - let iter = match result { - Ok(a) => a.chars(), - Err(ref b) => b.chars(), - }; - let mut new = String::new(); - for c in iter { - match c { - '.' | '-' | '_' => { - new.push(' '); - cap = true; - } - a => { - if cap { - cap = false; - for i in a.to_uppercase() { - new.push(i); - } - } else { - new.push(a); - } - } - } - } - new -} - -fn fancy_fallback_os(result: Result) -> OsString { - match result { - Ok(success) => success, - Err(fallback) => { - let cs = match fallback.to_str() { - Some(a) => Ok(a), - None => Err(fallback.to_string_lossy().to_string()), - }; - - fancy_fallback(cs).into() - } - } -} - -pub(crate) fn realname() -> String { - string_from_os(realname_os()) +pub(crate) fn realname() -> Result { + Ok(string_from_os(realname_os()?)) } -pub(crate) fn realname_os() -> OsString { +pub(crate) fn realname_os() -> Result { getpwuid(true) } #[cfg(not(target_os = "macos"))] -pub(crate) fn devicename_os() -> OsString { - devicename().into() +pub(crate) fn devicename_os() -> Result { + Ok(devicename()?.into()) } #[cfg(not(any(target_os = "macos", target_os = "illumos")))] -pub(crate) fn devicename() -> String { - if let Ok(program) = std::fs::read("/etc/machine-info") { - let distro = String::from_utf8_lossy(&program); +pub(crate) fn devicename() -> Result { + let machine_info = std::fs::read("/etc/machine-info")?; + let machine_info = String::from_utf8_lossy(&machine_info); - for i in distro.split('\n') { - let mut j = i.split('='); + for i in machine_info.split('\n') { + let mut j = i.split('='); - if j.next() == Some("PRETTY_HOSTNAME") { - if let Some(value) = j.next() { - return value.trim_matches('"').to_string(); - } + if j.next() == Some("PRETTY_HOSTNAME") { + if let Some(value) = j.next() { + // FIXME: Can " be escaped in pretty name? + return Ok(value.trim_matches('"').to_string()); } } } - fancy_fallback(Err(hostname())) + + Err(Error::new(ErrorKind::NotFound, "Missing record")) } #[cfg(target_os = "macos")] -pub(crate) fn devicename() -> String { - string_from_os(devicename_os()) +pub(crate) fn devicename() -> Result { + Ok(string_from_os(devicename_os()?)) } #[cfg(target_os = "macos")] -pub(crate) fn devicename_os() -> OsString { +pub(crate) fn devicename_os() -> Result { let out = os_from_cfstring(unsafe { SCDynamicStoreCopyComputerName(null_mut(), null_mut()) }); - let computer = if out.as_bytes().is_empty() { - Err(hostname_os()) - } else { - Ok(out) - }; - fancy_fallback_os(computer) + if out.as_bytes().is_empty() { + return Err(Error::new(ErrorKind::InvalidData, "Empty record")); + } + + Ok(out) } #[cfg(target_os = "illumos")] -pub(crate) fn devicename() -> String { - if let Ok(program) = std::fs::read("/etc/nodename") { - let mut nodename = String::from_utf8_lossy(&program).to_string(); - - // Remove the trailing newline - nodename.pop(); - - return nodename; +pub(crate) fn devicename() -> Result { + let program = std::fs::read("/etc/nodename")?; + let mut nodename = String::from_utf8_lossy(&program).to_string(); + // Remove the trailing newline + let _ = nodename.pop(); + + if nodename.is_empty() { + return Err(Error::new(ErrorKind::InvalidData, "Empty record")); } - fancy_fallback(Err(hostname())) + Ok(nodename) } -pub(crate) fn hostname() -> String { - string_from_os(hostname_os()) +pub(crate) fn hostname() -> Result { + Ok(string_from_os(hostname_os()?)) } -fn hostname_os() -> OsString { +fn hostname_os() -> Result { // Maximum hostname length = 255, plus a NULL byte. let mut string = Vec::::with_capacity(256); + unsafe { - gethostname(string.as_mut_ptr() as *mut c_void, 255); + if gethostname(string.as_mut_ptr() as *mut c_void, 255) == -1 { + return Err(Error::last_os_error()); + } + string.set_len(strlen(string.as_ptr() as *const c_void)); }; - OsString::from_vec(string) + + Ok(OsString::from_vec(string)) } #[cfg(target_os = "macos")] -fn distro_xml(data: String) -> Option { +fn distro_xml(data: String) -> Result { let mut product_name = None; let mut user_visible_version = None; + if let Some(start) = data.find("") { if let Some(end) = data.find("") { let mut set_product_name = false; let mut set_user_visible_version = false; + for line in data[start + "".len()..end].lines() { let line = line.trim(); + if line.starts_with("") { match line["".len()..].trim_end_matches("") { "ProductName" => set_product_name = true, @@ -404,24 +372,29 @@ fn distro_xml(data: String) -> Option { } } } - if let Some(product_name) = product_name { + + Ok(if let Some(product_name) = product_name { if let Some(user_visible_version) = user_visible_version { - Some(format!("{} {}", product_name, user_visible_version)) + format!("{} {}", product_name, user_visible_version) } else { - Some(product_name.to_string()) + product_name.to_string() } } else { - user_visible_version.map(|v| format!("Mac OS (Unknown) {}", v)) - } + user_visible_version + .map(|v| format!("Mac OS (Unknown) {}", v)) + .ok_or_else(|| { + Error::new(ErrorKind::InvalidData, "Parsing failed") + })? + }) } #[cfg(target_os = "macos")] -pub(crate) fn distro_os() -> Option { +pub(crate) fn distro_os() -> Result { distro().map(|a| a.into()) } #[cfg(target_os = "macos")] -pub(crate) fn distro() -> Option { +pub(crate) fn distro() -> Result { if let Ok(data) = std::fs::read_to_string( "/System/Library/CoreServices/ServerVersion.plist", ) { @@ -431,34 +404,43 @@ pub(crate) fn distro() -> Option { ) { distro_xml(data) } else { - None + Err(Error::new(ErrorKind::NotFound, "Missing record")) } } #[cfg(not(target_os = "macos"))] -pub(crate) fn distro_os() -> Option { +pub(crate) fn distro_os() -> Result { distro().map(|a| a.into()) } #[cfg(not(target_os = "macos"))] -pub(crate) fn distro() -> Option { - let program = std::fs::read("/etc/os-release").ok()?; +pub(crate) fn distro() -> Result { + let program = std::fs::read("/etc/os-release")?; let distro = String::from_utf8_lossy(&program); + let err = || Error::new(ErrorKind::InvalidData, "Parsing failed"); let mut fallback = None; for i in distro.split('\n') { let mut j = i.split('='); - match j.next()? { + match j.next().ok_or_else(err)? { "PRETTY_NAME" => { - return Some(j.next()?.trim_matches('"').to_string()); + return Ok(j + .next() + .ok_or_else(err)? + .trim_matches('"') + .to_string()); + } + "NAME" => { + fallback = Some( + j.next().ok_or_else(err)?.trim_matches('"').to_string(), + ) } - "NAME" => fallback = Some(j.next()?.trim_matches('"').to_string()), _ => {} } } - fallback + fallback.ok_or_else(err) } #[cfg(target_os = "macos")] diff --git a/src/web.rs b/src/web.rs index 0cc24c5..04f93e8 100644 --- a/src/web.rs +++ b/src/web.rs @@ -1,12 +1,15 @@ #[cfg(not(any(target_pointer_width = "32", target_pointer_width = "64")))] compile_error!("Unexpected pointer width for target platform"); -use std::ffi::OsString; +use std::{ + ffi::OsString, + io::{Error, ErrorKind}, +}; use wasm_bindgen::JsValue; use web_sys::window; -use crate::{Arch, DesktopEnv, Platform}; +use crate::{Arch, DesktopEnv, Platform, Result}; // Get the user agent fn user_agent() -> Option { @@ -53,42 +56,42 @@ pub(crate) fn lang() -> impl Iterator { } #[inline(always)] -pub(crate) fn username_os() -> OsString { - username().into() +pub(crate) fn username_os() -> Result { + Ok(username()?.into()) } #[inline(always)] -pub(crate) fn realname_os() -> OsString { - realname().into() +pub(crate) fn realname_os() -> Result { + Ok(realname()?.into()) } #[inline(always)] -pub(crate) fn devicename_os() -> OsString { - devicename().into() +pub(crate) fn devicename_os() -> Result { + Ok(devicename()?.into()) } #[inline(always)] -pub(crate) fn distro_os() -> Option { - distro().map(|a| a.into()) +pub(crate) fn distro_os() -> Result { + Ok(distro()?.into()) } #[inline(always)] -pub(crate) fn username() -> String { - "anonymous".to_string() +pub(crate) fn username() -> Result { + Ok("anonymous".to_string()) } #[inline(always)] -pub(crate) fn realname() -> String { - "Anonymous".to_string() +pub(crate) fn realname() -> Result { + Ok("Anonymous".to_string()) } -pub(crate) fn devicename() -> String { +pub(crate) fn devicename() -> Result { let orig_string = user_agent().unwrap_or_default(); let start = if let Some(s) = orig_string.rfind(' ') { s } else { - return "Unknown Browser".to_string(); + return Ok("Unknown Browser".to_string()); }; let string = orig_string @@ -96,7 +99,7 @@ pub(crate) fn devicename() -> String { .unwrap_or("Unknown Browser") .replace('/', " "); - if let Some(s) = string.rfind("Safari") { + Ok(if let Some(s) = string.rfind("Safari") { if let Some(s) = orig_string.rfind("Chrome") { if let Some(e) = orig_string.get(s..).unwrap_or_default().find(' ') { @@ -120,43 +123,44 @@ pub(crate) fn devicename() -> String { string.replace("OPR ", "Opera ") } else { string - } + }) } #[inline(always)] -pub(crate) fn hostname() -> String { +pub(crate) fn hostname() -> Result { document_domain() .filter(|x| !x.is_empty()) - .unwrap_or_else(|| "localhost".to_string()) + .ok_or_else(|| Error::new(ErrorKind::NotFound, "Domain missing")) } -pub(crate) fn distro() -> Option { - let string = user_agent()?; - - let begin = string.find('(')?; - let end = string.find(')')?; +pub(crate) fn distro() -> Result { + let string = + user_agent().ok_or_else(|| Error::from(ErrorKind::PermissionDenied))?; + let err = || Error::new(ErrorKind::InvalidData, "Parsing failed"); + let begin = string.find('(').ok_or_else(err)?; + let end = string.find(')').ok_or_else(err)?; let string = &string[begin + 1..end]; - if string.contains("Win32") || string.contains("Win64") { + Ok(if string.contains("Win32") || string.contains("Win64") { let begin = if let Some(b) = string.find("NT") { b } else { - return Some("Windows".to_string()); + return Ok("Windows".to_string()); }; let end = if let Some(e) = string.find('.') { e } else { - return Some("Windows".to_string()); + return Ok("Windows".to_string()); }; let string = &string[begin + 3..end]; - Some(format!("Windows {}", string)) + format!("Windows {}", string) } else if string.contains("Linux") { let string = if string.contains("X11") || string.contains("Wayland") { let begin = if let Some(b) = string.find(';') { b } else { - return Some("Unknown Linux".to_string()); + return Ok("Unknown Linux".to_string()); }; &string[begin + 2..] } else { @@ -164,21 +168,21 @@ pub(crate) fn distro() -> Option { }; if string.starts_with("Linux") { - Some("Unknown Linux".to_string()) + "Unknown Linux".to_string() } else { let end = if let Some(e) = string.find(';') { e } else { - return Some("Unknown Linux".to_string()); + return Ok("Unknown Linux".to_string()); }; - Some(string[..end].to_string()) + string[..end].to_string() } } else if let Some(begin) = string.find("Mac OS X") { - Some(if let Some(end) = string[begin..].find(';') { + if let Some(end) = string[begin..].find(';') { string[begin..begin + end].to_string() } else { string[begin..].to_string().replace('_', ".") - }) + } } else { // TODO: // Platform::FreeBsd, @@ -190,8 +194,8 @@ pub(crate) fn distro() -> Option { // Platform::Dive, // Platform::Fuchsia, // Platform::Redox - Some(string.to_string()) - } + string.to_string() + }) } pub(crate) const fn desktop_env() -> DesktopEnv { diff --git a/src/windows.rs b/src/windows.rs index 20d731d..cb14273 100644 --- a/src/windows.rs +++ b/src/windows.rs @@ -1,6 +1,7 @@ use std::{ convert::TryInto, ffi::OsString, + io::Error, mem::MaybeUninit, os::{ raw::{c_char, c_int, c_uchar, c_ulong, c_ushort, c_void}, @@ -9,7 +10,7 @@ use std::{ ptr, }; -use crate::{Arch, DesktopEnv, Platform}; +use crate::{Arch, DesktopEnv, Platform, Result}; #[repr(C)] struct OsVersionInfoEx { @@ -74,9 +75,11 @@ enum ComputerNameFormat { Max, } +const ERR_MORE_DATA: i32 = 0xEA; +const ERR_INSUFFICIENT_BUFFER: i32 = 0x7A; + #[link(name = "secur32")] extern "system" { - fn GetLastError() -> c_ulong; fn GetUserNameExW( a: ExtendedNameFormat, b: *mut c_char, @@ -109,18 +112,22 @@ fn string_from_os(string: OsString) -> String { } } -pub(crate) fn username() -> String { - string_from_os(username_os()) +pub(crate) fn username() -> Result { + Ok(string_from_os(username_os()?)) } -pub(crate) fn username_os() -> OsString { +pub(crate) fn username_os() -> Result { // Step 1. Retreive the entire length of the username let mut size = 0; - let success = unsafe { + let fail = unsafe { // Ignore error, we know that it will be ERROR_INSUFFICIENT_BUFFER GetUserNameW(ptr::null_mut(), &mut size) == 0 }; - assert!(success); + assert!(fail); + + if Error::last_os_error().raw_os_error() != Some(ERR_INSUFFICIENT_BUFFER) { + return Err(Error::last_os_error()); + } // Step 2. Allocate memory to put the Windows (UTF-16) string. let mut name: Vec = @@ -130,7 +137,7 @@ pub(crate) fn username_os() -> OsString { let fail = unsafe { GetUserNameW(name.as_mut_ptr().cast(), &mut size) == 0 }; if fail { - return "unknown".to_string().into(); + return Err(Error::last_os_error()); } debug_assert_eq!(orig_size, size); unsafe { @@ -140,32 +147,30 @@ pub(crate) fn username_os() -> OsString { debug_assert_eq!(terminator, Some(0u16)); // Step 3. Convert to Rust String - OsString::from_wide(&name) + Ok(OsString::from_wide(&name)) } #[inline(always)] -pub(crate) fn realname() -> String { - string_from_os(realname_os()) +pub(crate) fn realname() -> Result { + Ok(string_from_os(realname_os()?)) } #[inline(always)] -pub(crate) fn realname_os() -> OsString { +pub(crate) fn realname_os() -> Result { // Step 1. Retrieve the entire length of the username let mut buf_size = 0; - let success = unsafe { + let fail = unsafe { GetUserNameExW( ExtendedNameFormat::Display, ptr::null_mut(), &mut buf_size, ) == 0 }; - assert!(success); - match unsafe { GetLastError() } { - 0x00EA /* more data */ => { /* Success, continue */ } - _ /* network error or none mapped */ => { - // Fallback to username - return username_os(); - } + + assert!(fail); + + if Error::last_os_error().raw_os_error() != Some(ERR_MORE_DATA) { + return Err(Error::last_os_error()); } // Step 2. Allocate memory to put the Windows (UTF-16) string. @@ -180,27 +185,29 @@ pub(crate) fn realname_os() -> OsString { ) == 0 }; if fail { - return "Unknown".to_string().into(); + return Err(Error::last_os_error()); } - debug_assert_eq!(buf_size, name_len + 1); + + assert_eq!(buf_size, name_len + 1); + unsafe { name.set_len(name_len.try_into().unwrap_or(std::usize::MAX)); } // Step 3. Convert to Rust String - OsString::from_wide(&name) + Ok(OsString::from_wide(&name)) } #[inline(always)] -pub(crate) fn devicename() -> String { - string_from_os(devicename_os()) +pub(crate) fn devicename() -> Result { + Ok(string_from_os(devicename_os()?)) } #[inline(always)] -pub(crate) fn devicename_os() -> OsString { +pub(crate) fn devicename_os() -> Result { // Step 1. Retreive the entire length of the device name let mut size = 0; - let success = unsafe { + let fail = unsafe { // Ignore error, we know that it will be ERROR_INSUFFICIENT_BUFFER GetComputerNameExW( ComputerNameFormat::DnsHostname, @@ -208,35 +215,41 @@ pub(crate) fn devicename_os() -> OsString { &mut size, ) == 0 }; - assert!(success); + + assert!(fail); + + if Error::last_os_error().raw_os_error() != Some(ERR_INSUFFICIENT_BUFFER) { + return Err(Error::last_os_error()); + } // Step 2. Allocate memory to put the Windows (UTF-16) string. let mut name: Vec = Vec::with_capacity(size.try_into().unwrap_or(std::usize::MAX)); - size = name.capacity().try_into().unwrap_or(std::u32::MAX); - let fail = unsafe { + let mut size = name.capacity().try_into().unwrap_or(std::u32::MAX); + + if unsafe { GetComputerNameExW( ComputerNameFormat::DnsHostname, name.as_mut_ptr().cast(), &mut size, ) == 0 - }; - if fail { - return "Unknown".to_string().into(); + } { + return Err(Error::last_os_error()); } + unsafe { name.set_len(size.try_into().unwrap_or(std::usize::MAX)); } // Step 3. Convert to Rust String - OsString::from_wide(&name) + Ok(OsString::from_wide(&name)) } -pub(crate) fn hostname() -> String { - string_from_os(hostname_os()) +pub(crate) fn hostname() -> Result { + Ok(string_from_os(hostname_os()?)) } -fn hostname_os() -> OsString { +fn hostname_os() -> Result { // Step 1. Retreive the entire length of the username let mut size = 0; let fail = unsafe { @@ -247,57 +260,74 @@ fn hostname_os() -> OsString { &mut size, ) == 0 }; - debug_assert!(fail); + + assert!(fail); + + if Error::last_os_error().raw_os_error() != Some(ERR_INSUFFICIENT_BUFFER) { + return Err(Error::last_os_error()); + } // Step 2. Allocate memory to put the Windows (UTF-16) string. let mut name: Vec = Vec::with_capacity(size.try_into().unwrap_or(std::usize::MAX)); - size = name.capacity().try_into().unwrap_or(std::u32::MAX); - let fail = unsafe { + let mut size = name.capacity().try_into().unwrap_or(std::u32::MAX); + + if unsafe { GetComputerNameExW( ComputerNameFormat::NetBIOS, name.as_mut_ptr().cast(), &mut size, ) == 0 - }; - if fail { - return "localhost".to_string().into(); + } { + return Err(Error::last_os_error()); } + unsafe { name.set_len(size.try_into().unwrap_or(std::usize::MAX)); } // Step 3. Convert to Rust String - OsString::from_wide(&name) + Ok(OsString::from_wide(&name)) } -pub(crate) fn distro_os() -> Option { +pub(crate) fn distro_os() -> Result { distro().map(|a| a.into()) } -pub(crate) fn distro() -> Option { +pub(crate) fn distro() -> Result { // Due to MingW Limitations, we must dynamically load ntdll.dll extern "system" { fn LoadLibraryExW( - filename: *mut u16, - hfile: isize, - dwflags: u32, - ) -> isize; - fn FreeLibrary(hmodule: isize) -> i32; - fn GetProcAddress(hmodule: isize, procname: *mut u8) -> *mut c_void; + filename: *const u16, + hfile: *mut c_void, + dwflags: c_ulong, + ) -> *mut c_void; + fn FreeLibrary(hmodule: *mut c_void) -> i32; + fn GetProcAddress( + hmodule: *mut c_void, + procname: *const c_char, + ) -> *mut c_void; } let mut path = "ntdll.dll\0".encode_utf16().collect::>(); let path = path.as_mut_ptr(); - let inst = unsafe { LoadLibraryExW(path, 0, 0x0000_0800) }; + let inst = unsafe { LoadLibraryExW(path, ptr::null_mut(), 0x0000_0800) }; + + if inst.is_null() { + return Err(Error::last_os_error()); + } let mut path = "RtlGetVersion\0".bytes().collect::>(); - let path = path.as_mut_ptr(); + let path = path.as_mut_ptr().cast(); let func = unsafe { GetProcAddress(inst, path) }; if func.is_null() { - return Some("Windows (Unknown)".to_string()); + if unsafe { FreeLibrary(inst) } == 0 { + return Err(Error::last_os_error()); + } + + return Err(Error::last_os_error()); } let get_version: unsafe extern "system" fn(a: *mut OsVersionInfoEx) -> u32 = @@ -309,7 +339,11 @@ pub(crate) fn distro() -> Option { (*version.as_mut_ptr()).os_version_info_size = std::mem::size_of::() as u32; get_version(version.as_mut_ptr()); - FreeLibrary(inst); + + if FreeLibrary(inst) == 0 { + return Err(Error::last_os_error()); + } + version.assume_init() }; @@ -320,15 +354,13 @@ pub(crate) fn distro() -> Option { _ => "Unknown", }; - let out = format!( + Ok(format!( "Windows {}.{}.{} ({})", version.major_version, version.minor_version, version.build_number, - product - ); - - Some(out) + product, + )) } #[inline(always)] @@ -352,6 +384,7 @@ impl Iterator for LangIter { fn next(&mut self) -> Option { if let Some(value) = self.array.get(self.index) { self.index += 1; + Some(value.to_string()) } else { None @@ -370,7 +403,7 @@ pub(crate) fn lang() -> impl Iterator { GetUserPreferredUILanguages( 0x08, /* MUI_LANGUAGE_NAME */ &mut num_languages, - std::ptr::null_mut(), // List of languages. + ptr::null_mut(), // List of languages. &mut buffer_size, ), 0