mirror of
https://github.com/NotAShelf/microfetch.git
synced 2026-04-12 12:57:41 +00:00
Merge pull request #54 from NotAShelf/notashelf/push-ypvyyznuosqk
various: replace libc invocations with `std::env` and handwritten syscall wrappers
This commit is contained in:
commit
bf1b319971
9 changed files with 296 additions and 70 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -571,7 +571,6 @@ version = "0.4.13"
|
|||
dependencies = [
|
||||
"criterion",
|
||||
"hotpath",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -37,8 +37,8 @@ impl Colors {
|
|||
}
|
||||
|
||||
pub static COLORS: LazyLock<Colors> = 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)
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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 + ")"
|
||||
|
|
|
|||
28
src/lib.rs
28
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<Self, std::io::Error> {
|
||||
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()) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
11
src/main.rs
11
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(())
|
||||
}
|
||||
|
|
|
|||
200
src/syscall.rs
200
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.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -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<String, io::Error> {
|
||||
let mut vfs = MaybeUninit::uninit();
|
||||
let mut vfs = MaybeUninit::<StatfsBuf>::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<String, io::Error> {
|
|||
}
|
||||
|
||||
#[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()?;
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue