use std::{
borrow::Cow,
ffi::{CStr, OsString},
fs, io, mem,
os::unix::ffi::OsStringExt,
prelude::rust_2021::*,
slice,
};
use crate::{
os::{Os, Target},
CpuArchitecture, DesktopEnvironment, Error, LanguagePreferences, Platform,
Result,
};
enum Name {
User,
Real,
}
trait Terminators {
const CHARS: &'static [u8];
}
struct Nul;
struct NulOrComma;
impl Terminators for Nul {
const CHARS: &'static [u8] = b"\0";
}
impl Terminators for NulOrComma {
const CHARS: &'static [u8] = b"\0,";
}
unsafe fn strlen<T>(mut cs: *const u8) -> usize
where
T: Terminators,
{
let mut len = 0;
while !T::CHARS.contains(&*cs) {
len += 1;
cs = cs.offset(1);
}
len
}
fn os_from_cstring<T>(string: *const u8) -> Result<OsString>
where
T: Terminators,
{
if string.is_null() {
return Err(Error::null_record());
}
let slice = unsafe {
let length = strlen::<T>(string);
if length == 0 {
return Err(Error::empty_record());
}
slice::from_raw_parts(string, length)
};
Ok(OsString::from_vec(slice.to_vec()))
}
#[inline(always)]
fn getpwuid(name: Name) -> Result<OsString> {
const BUF_SIZE: usize = 16_384; let mut buffer = mem::MaybeUninit::<[u8; BUF_SIZE]>::uninit();
let mut passwd = mem::MaybeUninit::<libc::passwd>::uninit();
let passwd = unsafe {
let mut _passwd = mem::MaybeUninit::<*mut libc::passwd>::uninit();
let ret = libc::getpwuid_r(
libc::geteuid(),
passwd.as_mut_ptr(),
buffer.as_mut_ptr().cast(),
BUF_SIZE,
_passwd.as_mut_ptr(),
);
if ret != 0 {
return Err(Error::from_io(io::Error::last_os_error()));
}
let _passwd = _passwd.assume_init();
if _passwd.is_null() {
return Err(Error::null_record());
}
passwd.assume_init()
};
if let Name::Real = name {
os_from_cstring::<NulOrComma>(passwd.pw_gecos.cast())
} else {
os_from_cstring::<Nul>(passwd.pw_name.cast())
}
}
impl Target for Os {
fn lang_prefs(self) -> Result<LanguagePreferences> {
super::unix_lang()
}
fn realname(self) -> Result<OsString> {
getpwuid(Name::Real)
}
fn username(self) -> Result<OsString> {
getpwuid(Name::User)
}
fn devicename(self) -> Result<OsString> {
#[cfg(target_vendor = "apple")]
{
use std::ptr::null_mut;
use objc2_system_configuration::SCDynamicStore;
let Some(name) =
(unsafe { SCDynamicStore::computer_name(None, null_mut()) })
else {
return Err(Error::missing_record());
};
let name = name.to_string();
if name.is_empty() {
return Err(Error::empty_record());
}
Ok(name.into())
}
#[cfg(target_os = "illumos")]
{
let mut nodename =
fs::read("/etc/nodename").map_err(Error::from_io)?;
if let Some(slice) = nodename.split(|x| *x == b'\n').next() {
nodename.drain(slice.len()..);
}
if nodename.is_empty() {
return Err(Error::empty_record());
}
Ok(OsString::from_vec(nodename))
}
#[cfg(any(
target_os = "linux",
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "hurd",
))]
{
let machine_info =
fs::read("/etc/machine-info").map_err(Error::from_io)?;
for i in machine_info.split(|b| *b == b'\n') {
let mut j = i.split(|b| *b == b'=');
if j.next() == Some(b"PRETTY_HOSTNAME") {
let pretty_hostname = j
.next()
.ok_or(Error::with_invalid_data("parsing failed"))?;
let pretty_hostname = pretty_hostname
.strip_prefix(b"\"")
.unwrap_or(pretty_hostname)
.strip_suffix(b"\"")
.unwrap_or(pretty_hostname);
let pretty_hostname = {
let mut vec = Vec::with_capacity(pretty_hostname.len());
let mut pretty_hostname = pretty_hostname.iter();
while let Some(&c) = pretty_hostname.next() {
if c == b'\\' {
vec.push(match pretty_hostname.next() {
Some(b'\\') => b'\\',
Some(b't') => b'\t',
Some(b'r') => b'\r',
Some(b'n') => b'\n',
Some(b'\'') => b'\'',
Some(b'"') => b'"',
_ => {
return Err(Error::with_invalid_data(
"parsing failed",
));
}
});
} else {
vec.push(c);
}
}
vec
};
return Ok(OsString::from_vec(pretty_hostname));
}
}
Err(Error::missing_record())
}
}
fn hostname(self) -> Result<String> {
let mut string = Vec::<u8>::with_capacity(256);
unsafe {
if libc::gethostname(string.as_mut_ptr().cast(), 255) == -1 {
return Err(Error::from_io(io::Error::last_os_error()));
}
string.set_len(strlen::<Nul>(string.as_ptr().cast()));
};
String::from_utf8(string)
.map_err(|_| Error::with_invalid_data("Hostname not valid UTF-8"))
}
fn distro(self) -> Result<String> {
#[cfg(target_vendor = "apple")]
{
fn distro_xml(data: String) -> Result<String> {
let mut product_name = None;
let mut user_visible_version = None;
if let Some(start) = data.find("<dict>") {
if let Some(end) = data.find("</dict>") {
let mut set_product_name = false;
let mut set_user_visible_version = false;
for line in data[start + "<dict>".len()..end].lines() {
let line = line.trim();
if let Some(key) = line.strip_prefix("<key>") {
match key.trim_end_matches("</key>") {
"ProductName" => set_product_name = true,
"ProductUserVisibleVersion" => {
set_user_visible_version = true
}
"ProductVersion"
if user_visible_version.is_none() =>
{
set_user_visible_version = true
}
_ => {}
}
} else if let Some(value) =
line.strip_prefix("<string>")
{
if set_product_name {
product_name = Some(
value.trim_end_matches("</string>"),
);
set_product_name = false;
} else if set_user_visible_version {
user_visible_version = Some(
value.trim_end_matches("</string>"),
);
set_user_visible_version = false;
}
}
}
}
}
Ok(if let Some(product_name) = product_name {
if let Some(user_visible_version) = user_visible_version {
std::format!("{product_name} {user_visible_version}")
} else {
product_name.to_string()
}
} else {
user_visible_version
.map(|v| std::format!("Mac OS (Unknown) {v}"))
.ok_or_else(|| {
Error::with_invalid_data("Parsing failed")
})?
})
}
if let Ok(data) = fs::read_to_string(
"/System/Library/CoreServices/ServerVersion.plist",
) {
distro_xml(data)
} else if let Ok(data) = fs::read_to_string(
"/System/Library/CoreServices/SystemVersion.plist",
) {
distro_xml(data)
} else {
Err(Error::missing_record())
}
}
#[cfg(not(target_vendor = "apple"))]
{
let program =
fs::read("/etc/os-release").map_err(Error::from_io)?;
let distro = String::from_utf8_lossy(&program);
let err = || Error::with_invalid_data("Parsing failed");
let mut fallback = None;
for i in distro.split('\n') {
let mut j = i.split('=');
match j.next().ok_or_else(err)? {
"PRETTY_NAME" => {
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(),
)
}
_ => {}
}
}
fallback.ok_or_else(err)
}
}
fn desktop_env(self) -> Option<DesktopEnvironment> {
#[cfg(target_vendor = "apple")]
let env: Cow<'static, str> = "Aqua".into();
#[cfg(not(target_vendor = "apple"))]
let env: Cow<'static, str> = std::env::var_os("XDG_SESSION_DESKTOP")
.or_else(|| std::env::var_os("DESKTOP_SESSION"))
.or_else(|| std::env::var_os("XDG_CURRENT_DESKTOP"))?
.into_string()
.unwrap_or_else(|e| e.to_string_lossy().into_owned())
.into();
Some(if env.eq_ignore_ascii_case("AQUA") {
DesktopEnvironment::Aqua
} else if env.eq_ignore_ascii_case("GNOME") {
DesktopEnvironment::Gnome
} else if env.eq_ignore_ascii_case("LXDE") {
DesktopEnvironment::Lxde
} else if env.eq_ignore_ascii_case("OPENBOX") {
DesktopEnvironment::Openbox
} else if env.eq_ignore_ascii_case("I3") {
DesktopEnvironment::I3
} else if env.eq_ignore_ascii_case("UBUNTU") {
DesktopEnvironment::Ubuntu
} else if env.eq_ignore_ascii_case("PLASMA5") {
DesktopEnvironment::Plasma
} else if env.eq_ignore_ascii_case("XFCE") {
DesktopEnvironment::Xfce
} else if env.eq_ignore_ascii_case("NIRI") {
DesktopEnvironment::Niri
} else if env.eq_ignore_ascii_case("HYPRLAND") {
DesktopEnvironment::Hyprland
} else if env.eq_ignore_ascii_case("COSMIC") {
DesktopEnvironment::Cosmic
} else {
DesktopEnvironment::Unknown(env.to_string())
})
}
#[inline(always)]
fn platform(self) -> Platform {
#[cfg(target_os = "linux")]
{
Platform::Linux
}
#[cfg(target_vendor = "apple")]
{
Platform::Mac
}
#[cfg(any(
target_os = "dragonfly",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
))]
{
Platform::Bsd
}
#[cfg(target_os = "illumos")]
{
Platform::Illumos
}
#[cfg(target_os = "hurd")]
{
Platform::Hurd
}
}
#[inline(always)]
fn arch(self) -> Result<CpuArchitecture> {
let mut buf: libc::utsname = unsafe { mem::zeroed() };
if unsafe { libc::uname(&mut buf) } == -1 {
return Err(Error::from_io(io::Error::last_os_error()));
}
let arch_str =
unsafe { CStr::from_ptr(buf.machine.as_ptr()) }.to_string_lossy();
Ok(match arch_str.as_ref() {
"aarch64" | "arm64" | "aarch64_be" | "armv8b" | "armv8l" => {
CpuArchitecture::Arm64
}
"armv5" => CpuArchitecture::ArmV5,
"armv6" | "arm" => CpuArchitecture::ArmV6,
"armv7" => CpuArchitecture::ArmV7,
"i386" => CpuArchitecture::I386,
"i586" => CpuArchitecture::I586,
"i686" | "i686-AT386" => CpuArchitecture::I686,
"mips" => CpuArchitecture::Mips,
"mipsel" => CpuArchitecture::MipsEl,
"mips64" => CpuArchitecture::Mips64,
"mips64el" => CpuArchitecture::Mips64El,
"powerpc" | "ppc" | "ppcle" => CpuArchitecture::PowerPc,
"powerpc64" | "ppc64" | "ppc64le" => CpuArchitecture::PowerPc64,
"powerpc64le" => CpuArchitecture::PowerPc64Le,
"riscv32" => CpuArchitecture::Riscv32,
"riscv64" => CpuArchitecture::Riscv64,
"s390x" => CpuArchitecture::S390x,
"sparc" => CpuArchitecture::Sparc,
"sparc64" => CpuArchitecture::Sparc64,
"x86_64" | "amd64" | "i86pc" => CpuArchitecture::X64,
_ => CpuArchitecture::Unknown(arch_str.into_owned()),
})
}
}