diff --git a/Cargo.lock b/Cargo.lock index 4deada2..48a6768 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -571,7 +571,6 @@ version = "0.4.13" dependencies = [ "criterion", "hotpath", - "libc", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 6b1ae09..5db56fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,6 @@ path = "src/main.rs" [dependencies] hotpath = { optional = true, version = "0.13.0" } -libc = "0.2.183" [dev-dependencies] criterion = "0.8.1" diff --git a/src/colors.rs b/src/colors.rs index 7c65944..0fca89e 100644 --- a/src/colors.rs +++ b/src/colors.rs @@ -37,8 +37,8 @@ impl Colors { } pub static COLORS: LazyLock = LazyLock::new(|| { - const NO_COLOR: *const libc::c_char = c"NO_COLOR".as_ptr(); - let is_no_color = unsafe { !libc::getenv(NO_COLOR).is_null() }; + // Only presence matters; value is irrelevant per the NO_COLOR spec + let is_no_color = std::env::var_os("NO_COLOR").is_some(); Colors::new(is_no_color) }); diff --git a/src/desktop.rs b/src/desktop.rs index 501e967..ea863b4 100644 --- a/src/desktop.rs +++ b/src/desktop.rs @@ -1,27 +1,22 @@ -use std::{ffi::CStr, fmt::Write}; +use std::{ffi::OsStr, fmt::Write}; #[must_use] #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn get_desktop_info() -> String { - // Retrieve the environment variables and handle Result types - let desktop_str = unsafe { - let ptr = libc::getenv(c"XDG_CURRENT_DESKTOP".as_ptr()); - if ptr.is_null() { - "Unknown" - } else { - let s = CStr::from_ptr(ptr).to_str().unwrap_or("Unknown"); - s.strip_prefix("none+").unwrap_or(s) - } - }; + let desktop_os = std::env::var_os("XDG_CURRENT_DESKTOP"); + let session_os = std::env::var_os("XDG_SESSION_TYPE"); - let backend_str = unsafe { - let ptr = libc::getenv(c"XDG_SESSION_TYPE".as_ptr()); - if ptr.is_null() { - "Unknown" - } else { - let s = CStr::from_ptr(ptr).to_str().unwrap_or("Unknown"); - if s.is_empty() { "Unknown" } else { s } - } + let desktop_raw = desktop_os + .as_deref() + .and_then(OsStr::to_str) + .unwrap_or("Unknown"); + let desktop_str = desktop_raw.strip_prefix("none+").unwrap_or(desktop_raw); + + let session_raw = session_os.as_deref().and_then(OsStr::to_str).unwrap_or(""); + let backend_str = if session_raw.is_empty() { + "Unknown" + } else { + session_raw }; // Pre-calculate capacity: desktop_len + " (" + backend_len + ")" diff --git a/src/lib.rs b/src/lib.rs index 1e0f9f3..ab21d03 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,42 +5,44 @@ pub mod syscall; pub mod system; pub mod uptime; -use std::mem::MaybeUninit; +use std::{ffi::CStr, mem::MaybeUninit}; -/// Wrapper for `libc::utsname` with safe accessor methods -pub struct UtsName(libc::utsname); +use crate::syscall::{UtsNameBuf, sys_uname}; + +/// Wrapper for `utsname` with safe accessor methods +pub struct UtsName(UtsNameBuf); impl UtsName { - /// Calls `uname` syscall and returns a `UtsName` wrapper + /// Calls `uname(2)` syscall and returns a `UtsName` wrapper /// /// # Errors /// /// Returns an error if the `uname` syscall fails pub fn uname() -> Result { let mut uts = MaybeUninit::uninit(); - if unsafe { libc::uname(uts.as_mut_ptr()) } != 0 { + if unsafe { sys_uname(uts.as_mut_ptr()) } != 0 { return Err(std::io::Error::last_os_error()); } Ok(Self(unsafe { uts.assume_init() })) } #[must_use] - pub const fn nodename(&self) -> &std::ffi::CStr { - unsafe { std::ffi::CStr::from_ptr(self.0.nodename.as_ptr()) } + pub const fn nodename(&self) -> &CStr { + unsafe { CStr::from_ptr(self.0.nodename.as_ptr().cast()) } } #[must_use] - pub const fn sysname(&self) -> &std::ffi::CStr { - unsafe { std::ffi::CStr::from_ptr(self.0.sysname.as_ptr()) } + pub const fn sysname(&self) -> &CStr { + unsafe { CStr::from_ptr(self.0.sysname.as_ptr().cast()) } } #[must_use] - pub const fn release(&self) -> &std::ffi::CStr { - unsafe { std::ffi::CStr::from_ptr(self.0.release.as_ptr()) } + pub const fn release(&self) -> &CStr { + unsafe { CStr::from_ptr(self.0.release.as_ptr().cast()) } } #[must_use] - pub const fn machine(&self) -> &std::ffi::CStr { - unsafe { std::ffi::CStr::from_ptr(self.0.machine.as_ptr()) } + pub const fn machine(&self) -> &CStr { + unsafe { CStr::from_ptr(self.0.machine.as_ptr().cast()) } } } diff --git a/src/main.rs b/src/main.rs index caa58ec..ebc1e83 100644 --- a/src/main.rs +++ b/src/main.rs @@ -99,14 +99,19 @@ fn print_system_info( {blue} ▟█▘ ▜█▖ {cyan}▝█▛ {cyan} {blue}Colors{reset}  {colors}\n\n" )?; - let len = cursor.position() as usize; + let len = + usize::try_from(cursor.position()).expect("cursor position fits usize"); // Direct syscall to avoid stdout buffering allocation - let written = unsafe { libc::write(libc::STDOUT_FILENO, buf.as_ptr().cast(), len) }; + let written = unsafe { syscall::sys_write(1, buf.as_ptr(), len) }; if written < 0 { return Err(io::Error::last_os_error().into()); } + #[allow(clippy::cast_sign_loss)] // non-negative verified by the guard above if written as usize != len { - return Err(io::Error::new(io::ErrorKind::WriteZero, "partial write to stdout").into()); + return Err( + io::Error::new(io::ErrorKind::WriteZero, "partial write to stdout") + .into(), + ); } Ok(()) } diff --git a/src/syscall.rs b/src/syscall.rs index 0c8634b..cedc68a 100644 --- a/src/syscall.rs +++ b/src/syscall.rs @@ -123,6 +123,63 @@ pub unsafe fn sys_read(fd: i32, buf: *mut u8, count: usize) -> isize { } } +/// Direct syscall to write to a file descriptor +/// +/// # Returns +/// +/// Number of bytes written or -1 on error +/// +/// # Safety +/// +/// The caller must ensure that: +/// +/// - `buf` points to a valid readable buffer of at least `count` bytes +/// - `fd` is a valid open file descriptor +#[inline] +#[must_use] +pub unsafe fn sys_write(fd: i32, buf: *const u8, count: usize) -> isize { + #[cfg(target_arch = "x86_64")] + unsafe { + let ret: i64; + std::arch::asm!( + "syscall", + in("rax") 1i64, // SYS_write + in("rdi") fd, + in("rsi") buf, + in("rdx") count, + lateout("rax") ret, + lateout("rcx") _, + lateout("r11") _, + options(nostack) + ); + #[allow(clippy::cast_possible_truncation)] + { + ret as isize + } + } + #[cfg(target_arch = "aarch64")] + unsafe { + let ret: i64; + std::arch::asm!( + "svc #0", + in("x8") 64i64, // SYS_write + in("x0") fd, + in("x1") buf, + in("x2") count, + lateout("x0") ret, + options(nostack) + ); + #[allow(clippy::cast_possible_truncation)] + { + ret as isize + } + } + #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] + { + compile_error!("Unsupported architecture for inline assembly syscalls"); + } +} + /// Direct syscall to close a file descriptor /// /// # Safety @@ -169,6 +226,149 @@ pub unsafe fn sys_close(fd: i32) -> i32 { } } +/// Raw buffer for the `uname(2)` syscall. +/// +/// Linux ABI hasfive fields of `[i8; 65]`: sysname, nodename, release, version, +/// machine. The `domainname` field (GNU extension, `[i8; 65]`) follows but is +/// not used, nor any useful to us here. +#[repr(C)] +#[allow(dead_code)] +pub struct UtsNameBuf { + pub sysname: [i8; 65], + pub nodename: [i8; 65], + pub release: [i8; 65], + pub version: [i8; 65], + pub machine: [i8; 65], + pub domainname: [i8; 65], // GNU extension, included for correct struct size +} + +/// Direct `uname(2)` syscall +/// +/// # Returns +/// +/// 0 on success, negative on error +/// +/// # Safety +/// +/// The caller must ensure `buf` points to a valid `UtsNameBuf`. +#[inline] +#[allow(dead_code)] +pub unsafe fn sys_uname(buf: *mut UtsNameBuf) -> i32 { + #[cfg(target_arch = "x86_64")] + unsafe { + let ret: i64; + std::arch::asm!( + "syscall", + in("rax") 63i64, // SYS_uname + in("rdi") buf, + lateout("rax") ret, + lateout("rcx") _, + lateout("r11") _, + options(nostack) + ); + #[allow(clippy::cast_possible_truncation)] + { + ret as i32 + } + } + #[cfg(target_arch = "aarch64")] + unsafe { + let ret: i64; + std::arch::asm!( + "svc #0", + in("x8") 160i64, // SYS_uname + in("x0") buf, + lateout("x0") ret, + options(nostack) + ); + #[allow(clippy::cast_possible_truncation)] + { + ret as i32 + } + } + #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] + { + compile_error!("Unsupported architecture for inline assembly syscalls"); + } +} + +/// Raw buffer for the `statfs(2)` syscall. +/// +/// Linux ABI (`x86_64` and `aarch64`): the fields we use are at the same +/// offsets on both architectures. Only the fields needed for disk usage are +/// declared; the remainder of the 120-byte struct is covered by `_pad`. +#[repr(C)] +pub struct StatfsBuf { + pub f_type: i64, + pub f_bsize: i64, + pub f_blocks: u64, + pub f_bfree: u64, + pub f_bavail: u64, + pub f_files: u64, + pub f_ffree: u64, + pub f_fsid: [i32; 2], + pub f_namelen: i64, + pub f_frsize: i64, + pub f_flags: i64, + + #[allow(clippy::pub_underscore_fields, reason = "This is not a public API")] + pub _pad: [i64; 4], +} + +/// Direct `statfs(2)` syscall +/// +/// # Returns +/// +/// 0 on success, negative errno on error +/// +/// # Safety +/// +/// The caller must ensure that: +/// +/// - `path` points to a valid null-terminated string +/// - `buf` points to a valid `StatfsBuf` +#[inline] +pub unsafe fn sys_statfs(path: *const u8, buf: *mut StatfsBuf) -> i32 { + #[cfg(target_arch = "x86_64")] + unsafe { + let ret: i64; + std::arch::asm!( + "syscall", + in("rax") 137i64, // SYS_statfs + in("rdi") path, + in("rsi") buf, + lateout("rax") ret, + lateout("rcx") _, + lateout("r11") _, + options(nostack) + ); + #[allow(clippy::cast_possible_truncation)] + { + ret as i32 + } + } + #[cfg(target_arch = "aarch64")] + unsafe { + let ret: i64; + std::arch::asm!( + "svc #0", + in("x8") 43i64, // SYS_statfs + in("x0") path, + in("x1") buf, + lateout("x0") ret, + options(nostack) + ); + #[allow(clippy::cast_possible_truncation)] + { + ret as i32 + } + } + #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] + { + compile_error!("Unsupported architecture for inline assembly syscalls"); + } +} + /// Read entire file using direct syscalls. This avoids libc overhead and can be /// significantly faster for small files. /// diff --git a/src/system.rs b/src/system.rs index ba8fe79..9dd1ab3 100644 --- a/src/system.rs +++ b/src/system.rs @@ -1,18 +1,19 @@ -use std::{ffi::CStr, fmt::Write as _, io, mem::MaybeUninit}; +use std::{ffi::OsStr, fmt::Write as _, io, mem::MaybeUninit}; -use crate::{UtsName, colors::COLORS, syscall::read_file_fast}; +use crate::{ + UtsName, + colors::COLORS, + syscall::{StatfsBuf, read_file_fast, sys_statfs}, +}; #[must_use] #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn get_username_and_hostname(utsname: &UtsName) -> String { - let username = unsafe { - let ptr = libc::getenv(c"USER".as_ptr()); - if ptr.is_null() { - "unknown_user" - } else { - CStr::from_ptr(ptr).to_str().unwrap_or("unknown_user") - } - }; + let username_os = std::env::var_os("USER"); + let username = username_os + .as_deref() + .and_then(OsStr::to_str) + .unwrap_or("unknown_user"); let hostname = utsname.nodename().to_str().unwrap_or("unknown_host"); let capacity = COLORS.yellow.len() @@ -38,16 +39,13 @@ pub fn get_username_and_hostname(utsname: &UtsName) -> String { #[must_use] #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn get_shell() -> String { - unsafe { - let ptr = libc::getenv(c"SHELL".as_ptr()); - if ptr.is_null() { - return "unknown_shell".into(); - } - - let bytes = CStr::from_ptr(ptr).to_bytes(); - let start = bytes.iter().rposition(|&b| b == b'/').map_or(0, |i| i + 1); - let name = std::str::from_utf8_unchecked(&bytes[start..]); - name.into() + let shell_os = std::env::var_os("SHELL"); + let shell = shell_os.as_deref().and_then(OsStr::to_str).unwrap_or(""); + let start = shell.rfind('/').map_or(0, |i| i + 1); + if shell.is_empty() { + "unknown_shell".into() + } else { + shell[start..].into() } } @@ -59,15 +57,16 @@ pub fn get_shell() -> String { #[cfg_attr(feature = "hotpath", hotpath::measure)] #[allow(clippy::cast_precision_loss)] pub fn get_root_disk_usage() -> Result { - let mut vfs = MaybeUninit::uninit(); + let mut vfs = MaybeUninit::::uninit(); let path = b"/\0"; - if unsafe { libc::statvfs(path.as_ptr().cast(), vfs.as_mut_ptr()) } != 0 { + if unsafe { sys_statfs(path.as_ptr(), vfs.as_mut_ptr()) } != 0 { return Err(io::Error::last_os_error()); } let vfs = unsafe { vfs.assume_init() }; - let block_size = vfs.f_bsize; + #[allow(clippy::cast_sign_loss)] + let block_size = vfs.f_bsize as u64; let total_blocks = vfs.f_blocks; let available_blocks = vfs.f_bavail; @@ -158,12 +157,12 @@ pub fn get_memory_usage() -> Result { } #[allow(clippy::cast_precision_loss)] - let total_memory_gb = total_memory_kb as f64 / 1024.0 / 1024.0; + let total_gb = total_memory_kb as f64 / 1024.0 / 1024.0; #[allow(clippy::cast_precision_loss)] - let available_memory_gb = available_memory_kb as f64 / 1024.0 / 1024.0; - let used_memory_gb = total_memory_gb - available_memory_gb; + let available_gb = available_memory_kb as f64 / 1024.0 / 1024.0; + let used_memory_gb = total_gb - available_gb; - Ok((used_memory_gb, total_memory_gb)) + Ok((used_memory_gb, total_gb)) } let (used_memory, total_memory) = parse_memory_info()?; diff --git a/src/uptime.rs b/src/uptime.rs index 095af7d..c6c4b26 100644 --- a/src/uptime.rs +++ b/src/uptime.rs @@ -17,14 +17,41 @@ fn itoa(mut n: u64, buf: &mut [u8]) -> &str { unsafe { std::str::from_utf8_unchecked(&buf[i..]) } } -/// Direct `sysinfo` syscall using inline assembly +/// Raw buffer for the `sysinfo(2)` syscall. +/// +/// In the Linux ABI `uptime` is a `long` at offset 0. The remaining fields are +/// not needed, but are declared to give the struct its correct size (112 bytes +/// on 64-bit Linux). +/// +/// The layout matches the kernel's `struct sysinfo` *exactly*: +/// `mem_unit` ends at offset 108, then 4 bytes of implicit padding to 112. +#[repr(C)] +struct SysInfo { + uptime: i64, + loads: [u64; 3], + totalram: u64, + freeram: u64, + sharedram: u64, + bufferram: u64, + totalswap: u64, + freeswap: u64, + procs: u16, + _pad: u16, + _pad2: u32, // alignment padding to reach 8-byte boundary for totalhigh + totalhigh: u64, + freehigh: u64, + mem_unit: u32, + // 4 bytes implicit trailing padding to reach 112 bytes total; no field + // needed +} + +/// Direct `sysinfo(2)` syscall using inline assembly /// /// # Safety /// -/// This function uses inline assembly to make a direct syscall. /// The caller must ensure the sysinfo pointer is valid. #[inline] -unsafe fn sys_sysinfo(info: *mut libc::sysinfo) -> i64 { +unsafe fn sys_sysinfo(info: *mut SysInfo) -> i64 { #[cfg(target_arch = "x86_64")] { let ret: i64; @@ -59,7 +86,7 @@ unsafe fn sys_sysinfo(info: *mut libc::sysinfo) -> i64 { #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))] { - unsafe { libc::sysinfo(info) as i64 } + compile_error!("Unsupported architecture for inline assembly syscalls"); } }