diff --git a/.cargo/config.toml b/.cargo/config.toml index c7125dc..c406f76 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,3 @@ # https://github.com/rui314/mold?tab=readme-ov-file#how-to-use [target.'cfg(target_os = "linux")'] -rustflags = [ "-C", "link-arg=-fuse-ld=mold" ] +rustflags = [ "-C", "link-arg=-fuse-ld=mold", "-C", "link-arg=-lc", "-C", "link-arg=-lgcc_s" ] diff --git a/Cargo.lock b/Cargo.lock index fa82609..2f5d4bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -582,9 +582,15 @@ name = "microfetch" version = "0.4.13" dependencies = [ "hotpath", + "microfetch-alloc", + "microfetch-asm", "microfetch-lib", ] +[[package]] +name = "microfetch-alloc" +version = "0.4.13" + [[package]] name = "microfetch-asm" version = "0.4.13" diff --git a/Cargo.toml b/Cargo.toml index f8bd2a2..da548d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ rust-version = "1.92.0" version = "0.4.13" [workspace.dependencies] +microfetch-alloc = { path = "./crates/alloc" } microfetch-asm = { path = "./crates/asm" } microfetch-lib = { path = "./crates/lib" } @@ -18,6 +19,7 @@ criterion-cycles-per-byte = "0.8.0" [profile.dev] opt-level = 1 +panic = "abort" [profile.release] codegen-units = 1 diff --git a/crates/alloc/Cargo.toml b/crates/alloc/Cargo.toml new file mode 100644 index 0000000..47cd1d2 --- /dev/null +++ b/crates/alloc/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "microfetch-alloc" +description = "Simple, std-free bump allocator for Microfetch" +version.workspace = true +edition.workspace = true +authors.workspace = true +rust-version.workspace = true +license.workspace = true +publish = false + +[lints] +workspace = true diff --git a/crates/alloc/src/lib.rs b/crates/alloc/src/lib.rs new file mode 100644 index 0000000..06e3f07 --- /dev/null +++ b/crates/alloc/src/lib.rs @@ -0,0 +1,106 @@ +//! Simple bump allocator for `no_std` environments. Uses a statically allocated +//! 32KB buffer and provides O(1) allocation with no deallocation support +//! (memory is never freed). +#![no_std] +use core::{ + alloc::{GlobalAlloc, Layout}, + cell::UnsafeCell, + ptr::null_mut, +}; + +/// Default heap size is 32KB, should be plenty for Microfetch. Technically it +/// can be invoked with more (or less) depending on our needs but I am quite +/// sure 32KB is more than enough. +pub const DEFAULT_HEAP_SIZE: usize = 32 * 1024; + +/// A simple bump allocator that never frees memory. +/// +/// This allocator maintains a static buffer and a bump pointer. Allocations are +/// fast (just bump the pointer), but memory is never reclaimed. While you might +/// be inclined to point out that this is ugly, it's suitable for a short-lived +/// program with bounded memory usage. +pub struct BumpAllocator { + heap: UnsafeCell<[u8; N]>, + next: UnsafeCell, +} + +// SAFETY: BumpAllocator is thread-safe because it uses UnsafeCell +// and the allocator is only used in single-threaded contexts (i.e., no_std). +unsafe impl Sync for BumpAllocator {} + +impl BumpAllocator { + /// Creates a new bump allocator with the specified heap size. + #[must_use] + pub const fn new() -> Self { + Self { + heap: UnsafeCell::new([0; N]), + next: UnsafeCell::new(0), + } + } + + /// Returns the number of bytes currently allocated. + #[must_use] + pub fn used(&self) -> usize { + // SAFETY: We're just reading the value, and this is only called + // in single-threaded contexts. + unsafe { *self.next.get() } + } + + /// Returns the total heap size. + #[must_use] + pub const fn capacity(&self) -> usize { + N + } + + /// Returns the number of bytes remaining. + #[must_use] + pub fn remaining(&self) -> usize { + N - self.used() + } +} + +impl Default for BumpAllocator { + fn default() -> Self { + Self::new() + } +} + +unsafe impl GlobalAlloc for BumpAllocator { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + unsafe { + let next = self.next.get(); + let heap = self.heap.get(); + + // Align the current position + let align = layout.align(); + let start = (*next + align - 1) & !(align - 1); + let end = start + layout.size(); + + if end > N { + // Out of memory + null_mut() + } else { + *next = end; + (*heap).as_mut_ptr().add(start) + } + } + } + + unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) { + // Bump allocator doesn't support deallocation + // Memory is reclaimed when the program exits + } +} + +/// Static bump allocator instance with 32KB heap. +/// +/// # Example +/// +/// Use this with `#[global_allocator]` in your binary: +/// +/// +/// ```rust,ignore +/// #[global_allocator] +/// static ALLOCATOR: BumpAllocator = BumpAllocator::new(); +/// ``` +pub type BumpAlloc = BumpAllocator; diff --git a/crates/asm/src/lib.rs b/crates/asm/src/lib.rs index 63014d4..87c76b2 100644 --- a/crates/asm/src/lib.rs +++ b/crates/asm/src/lib.rs @@ -507,6 +507,7 @@ pub fn read_file_fast(path: &str, buffer: &mut [u8]) -> Result { let _ = sys_close(fd); if bytes_read < 0 { + #[allow(clippy::cast_possible_truncation)] return Err(bytes_read as i32); } @@ -598,3 +599,41 @@ pub unsafe fn sys_sysinfo(info: *mut SysInfo) -> i64 { ret } } + +/// Direct syscall to exit the process +/// +/// # Safety +/// +/// This syscall never returns. The process will terminate immediately. +#[inline] +pub unsafe fn sys_exit(code: i32) -> ! { + #[cfg(target_arch = "x86_64")] + unsafe { + core::arch::asm!( + "syscall", + in("rax") 60i64, // SYS_exit + in("rdi") code, + options(noreturn, nostack) + ); + } + + #[cfg(target_arch = "aarch64")] + unsafe { + core::arch::asm!( + "svc #0", + in("x8") 93i64, // SYS_exit + in("x0") code, + options(noreturn, nostack) + ); + } + + #[cfg(target_arch = "riscv64")] + unsafe { + core::arch::asm!( + "ecall", + in("a7") 93i64, // SYS_exit + in("a0") code, + options(noreturn, nostack) + ); + } +} diff --git a/crates/lib/src/colors.rs b/crates/lib/src/colors.rs index 0fca89e..9f012f9 100644 --- a/crates/lib/src/colors.rs +++ b/crates/lib/src/colors.rs @@ -1,5 +1,6 @@ -use std::sync::LazyLock; +use alloc::string::String; +/// Color codes for terminal output pub struct Colors { pub reset: &'static str, pub blue: &'static str, @@ -11,7 +12,8 @@ pub struct Colors { } impl Colors { - const fn new(is_no_color: bool) -> Self { + #[must_use] + pub const fn new(is_no_color: bool) -> Self { if is_no_color { Self { reset: "", @@ -36,46 +38,68 @@ impl Colors { } } -pub static COLORS: LazyLock = LazyLock::new(|| { - // 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) -}); +use core::sync::atomic::{AtomicBool, Ordering}; + +// Check if NO_COLOR is set (only once, lazily) +// Only presence matters; value is irrelevant per the NO_COLOR spec +static NO_COLOR_CHECKED: AtomicBool = AtomicBool::new(false); +static NO_COLOR_SET: AtomicBool = AtomicBool::new(false); + +/// Checks if `NO_COLOR` environment variable is set. +pub(crate) fn is_no_color() -> bool { + // Fast path: already checked + if NO_COLOR_CHECKED.load(Ordering::Acquire) { + return NO_COLOR_SET.load(Ordering::Relaxed); + } + + // Slow path: check environment + let is_set = crate::env_exists("NO_COLOR"); + NO_COLOR_SET.store(is_set, Ordering::Relaxed); + NO_COLOR_CHECKED.store(true, Ordering::Release); + is_set +} #[must_use] #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn print_dots() -> String { - // Pre-calculate capacity: 6 color codes + " " (glyph + 2 spaces) per color const GLYPH: &str = ""; - let capacity = COLORS.blue.len() - + COLORS.cyan.len() - + COLORS.green.len() - + COLORS.yellow.len() - + COLORS.red.len() - + COLORS.magenta.len() - + COLORS.reset.len() + + let colors = if is_no_color() { + Colors::new(true) + } else { + Colors::new(false) + }; + + // Pre-calculate capacity: 6 color codes + " " (glyph + 2 spaces) per color + let capacity = colors.blue.len() + + colors.cyan.len() + + colors.green.len() + + colors.yellow.len() + + colors.red.len() + + colors.magenta.len() + + colors.reset.len() + (GLYPH.len() + 2) * 6; let mut result = String::with_capacity(capacity); - result.push_str(COLORS.blue); + result.push_str(colors.blue); result.push_str(GLYPH); result.push_str(" "); - result.push_str(COLORS.cyan); + result.push_str(colors.cyan); result.push_str(GLYPH); result.push_str(" "); - result.push_str(COLORS.green); + result.push_str(colors.green); result.push_str(GLYPH); result.push_str(" "); - result.push_str(COLORS.yellow); + result.push_str(colors.yellow); result.push_str(GLYPH); result.push_str(" "); - result.push_str(COLORS.red); + result.push_str(colors.red); result.push_str(GLYPH); result.push_str(" "); - result.push_str(COLORS.magenta); + result.push_str(colors.magenta); result.push_str(GLYPH); result.push_str(" "); - result.push_str(COLORS.reset); + result.push_str(colors.reset); result } diff --git a/crates/lib/src/desktop.rs b/crates/lib/src/desktop.rs index ea863b4..b9aa362 100644 --- a/crates/lib/src/desktop.rs +++ b/crates/lib/src/desktop.rs @@ -1,18 +1,15 @@ -use std::{ffi::OsStr, fmt::Write}; +use alloc::string::String; + +use crate::getenv_str; #[must_use] #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn get_desktop_info() -> String { - let desktop_os = std::env::var_os("XDG_CURRENT_DESKTOP"); - let session_os = std::env::var_os("XDG_SESSION_TYPE"); + let desktop_raw = getenv_str("XDG_CURRENT_DESKTOP").unwrap_or("Unknown"); + let session_raw = getenv_str("XDG_SESSION_TYPE").unwrap_or(""); - 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 { @@ -27,9 +24,15 @@ pub fn get_desktop_info() -> String { result.push_str(" ("); // Capitalize first character of backend - if let Some(first_char) = backend_str.chars().next() { - let _ = write!(result, "{}", first_char.to_ascii_uppercase()); - result.push_str(&backend_str[first_char.len_utf8()..]); + if let Some(first_byte) = backend_str.as_bytes().first() { + // Convert first byte to uppercase if it's ASCII lowercase + let upper = if first_byte.is_ascii_lowercase() { + (first_byte - b'a' + b'A') as char + } else { + *first_byte as char + }; + result.push(upper); + result.push_str(&backend_str[1..]); } result.push(')'); diff --git a/crates/lib/src/lib.rs b/crates/lib/src/lib.rs index 28147f7..7da92a0 100644 --- a/crates/lib/src/lib.rs +++ b/crates/lib/src/lib.rs @@ -1,13 +1,17 @@ +#![no_std] +extern crate alloc; + pub mod colors; pub mod desktop; pub mod release; pub mod system; pub mod uptime; -use std::{ +use alloc::string::String; +use core::{ ffi::CStr, - io::{self, Cursor, Write}, mem::MaybeUninit, + sync::atomic::{AtomicPtr, Ordering}, }; pub use microfetch_asm as syscall; @@ -25,6 +29,200 @@ pub use microfetch_asm::{ sys_write, }; +/// A simple error type for microfetch operations. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Error { + /// An OS error occurred, containing the errno value. + OsError(i32), + /// Invalid data or encoding error. + InvalidData, + /// Not found. + NotFound, + /// Write operation failed or partial write. + WriteError, +} + +impl Error { + /// Creates an error from the last OS error (reads errno). + #[inline] + #[must_use] + pub const fn last_os_error() -> Self { + // This is a simplified version - in a real implementation, + // we'd need to get the actual errno from the syscall return + Self::OsError(0) + } + + /// Creates an error from a raw OS error code (negative errno from syscall). + #[inline] + #[must_use] + pub const fn from_raw_os_error(errno: i32) -> Self { + Self::OsError(-errno) + } +} + +impl core::fmt::Display for Error { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::OsError(errno) => write!(f, "OS error: {errno}"), + Self::InvalidData => write!(f, "Invalid data"), + Self::NotFound => write!(f, "Not found"), + Self::WriteError => write!(f, "Write error"), + } + } +} + +// Simple OnceLock implementation for no_std +pub struct OnceLock { + ptr: AtomicPtr, +} + +impl Default for OnceLock { + fn default() -> Self { + Self::new() + } +} + +impl OnceLock { + #[must_use] + pub const fn new() -> Self { + Self { + ptr: AtomicPtr::new(core::ptr::null_mut()), + } + } + + pub fn get_or_init(&self, f: F) -> &T + where + F: FnOnce() -> T, + { + // Load the current pointer + let mut ptr = self.ptr.load(Ordering::Acquire); + + if ptr.is_null() { + // Need to initialize + let value = f(); + let boxed = alloc::boxed::Box::new(value); + let new_ptr = alloc::boxed::Box::into_raw(boxed); + + // Try to set the pointer + match self.ptr.compare_exchange( + core::ptr::null_mut(), + new_ptr, + Ordering::Release, + Ordering::Acquire, + ) { + Ok(_) => { + // We successfully set it + ptr = new_ptr; + }, + Err(existing) => { + // Someone else set it first, free our allocation + // SAFETY: We just allocated this and no one else has seen it + unsafe { + let _ = alloc::boxed::Box::from_raw(new_ptr); + } + ptr = existing; + }, + } + } + + // SAFETY: We know ptr is non-null and points to a valid T + unsafe { &*ptr } + } +} + +impl Drop for OnceLock { + fn drop(&mut self) { + let ptr = self.ptr.load(Ordering::Acquire); + if !ptr.is_null() { + // SAFETY: We know this was allocated via Box::into_raw + unsafe { + let _ = alloc::boxed::Box::from_raw(ptr); + } + } + } +} + +// Access to the environ pointer (provided by libc startup code) +unsafe extern "C" { + static environ: *const *const u8; +} + +/// Gets an environment variable by name (without using std). +/// +/// # Safety +/// +/// This function reads from the environ global which is initialized +/// by the C runtime before `main()` is called. +#[must_use] +pub fn getenv(name: &str) -> Option<&'static [u8]> { + // SAFETY: environ is set up by the C runtime before main() runs + // and remains valid for the lifetime of the program + let envp = unsafe { environ }; + if envp.is_null() { + return None; + } + + let name_bytes = name.as_bytes(); + + // Walk through environment variables + let mut i = 0; + loop { + // SAFETY: environ is null-terminated array of pointers + let entry = unsafe { *envp.add(i) }; + if entry.is_null() { + break; + } + + // Check if this entry starts with our variable name followed by '=' + let mut matches = true; + for (j, &b) in name_bytes.iter().enumerate() { + // SAFETY: entry is a valid C string + let entry_byte = unsafe { *entry.add(j) }; + if entry_byte != b { + matches = false; + break; + } + } + + if matches { + // Check for '=' after the name + // SAFETY: entry is a valid C string + let eq_byte = unsafe { *entry.add(name_bytes.len()) }; + if eq_byte == b'=' { + // Found it! Calculate the value length + let value_start = unsafe { entry.add(name_bytes.len() + 1) }; + let mut len = 0; + loop { + // SAFETY: entry is a valid C string + let b = unsafe { *value_start.add(len) }; + if b == 0 { + break; + } + len += 1; + } + // SAFETY: We calculated the exact length + return Some(unsafe { core::slice::from_raw_parts(value_start, len) }); + } + } + + i += 1; + } + + None +} + +/// Gets an environment variable as a UTF-8 string. +#[must_use] +pub fn getenv_str(name: &str) -> Option<&'static str> { + getenv(name).and_then(|bytes| core::str::from_utf8(bytes).ok()) +} + +/// Checks if an environment variable exists (regardless of its value). +#[must_use] +pub fn env_exists(name: &str) -> bool { + getenv(name).is_some() +} + /// Wrapper for `utsname` with safe accessor methods pub struct UtsName(UtsNameBuf); @@ -34,10 +232,10 @@ impl UtsName { /// # Errors /// /// Returns an error if the `uname` syscall fails - pub fn uname() -> Result { + pub fn uname() -> Result { let mut uts = MaybeUninit::uninit(); if unsafe { sys_uname(uts.as_mut_ptr()) } != 0 { - return Err(std::io::Error::last_os_error()); + return Err(Error::last_os_error()); } Ok(Self(unsafe { uts.assume_init() })) } @@ -79,9 +277,7 @@ struct Fields { } #[cfg_attr(feature = "hotpath", hotpath::measure)] -fn print_system_info( - fields: &Fields, -) -> Result<(), Box> { +fn print_system_info(fields: &Fields) -> Result<(), Error> { let Fields { user_info, os_name, @@ -94,69 +290,242 @@ fn print_system_info( colors, } = fields; - let cyan = colors::COLORS.cyan; - let blue = colors::COLORS.blue; - let reset = colors::COLORS.reset; + let no_color = colors::is_no_color(); + let colors_obj = colors::Colors::new(no_color); + let cyan = colors_obj.cyan; + let blue = colors_obj.blue; + let reset = colors_obj.reset; + // Build output string let mut buf = [0u8; 2048]; - let mut cursor = Cursor::new(&mut buf[..]); + let mut pos = 0usize; - write!( - cursor, - " - {blue} ▟█▖ {cyan}▝█▙ ▗█▛ {user_info} ~{reset} - {blue} ▗▄▄▟██▄▄▄▄▄{cyan}▝█▙█▛ {blue}▖ {cyan} {blue}System{reset}  {os_name} - {blue} ▀▀▀▀▀▀▀▀▀▀▀▘{cyan}▝██ {blue}▟█▖ {cyan} {blue}Kernel{reset}  {kernel_version} - {cyan} ▟█▛ {cyan}▝█▘{blue}▟█▛ {cyan} {blue}Shell{reset}  {shell} - {cyan}▟█████▛ {blue}▟█████▛ {cyan} {blue}Uptime{reset}  {uptime} - {cyan} ▟█▛{blue}▗█▖ {blue}▟█▛ {cyan} {blue}Desktop{reset}  {desktop} - {cyan} ▝█▛ {blue}██▖{cyan}▗▄▄▄▄▄▄▄▄▄▄▄ {cyan}󰍛 {blue}Memory{reset}  {memory_usage} - {cyan} ▝ {blue}▟█▜█▖{cyan}▀▀▀▀▀██▛▀▀▘ {cyan}󱥎 {blue}Storage (/){reset}  {storage} - {blue} ▟█▘ ▜█▖ {cyan}▝█▛ {cyan} {blue}Colors{reset}  {colors}\n\n" - )?; + // Helper to write to buffer + let mut write_str = |s: &str| { + let bytes = s.as_bytes(); + let remaining = buf.len() - pos; + let to_write = bytes.len().min(remaining); + buf[pos..pos + to_write].copy_from_slice(&bytes[..to_write]); + pos += to_write; + }; + + write_str("\n "); + write_str(blue); + write_str(" ▟█▖ "); + write_str(cyan); + write_str("▝█▙ ▗█▛ "); + write_str(user_info); + write_str(" ~"); + write_str(reset); + write_str("\n"); + + write_str(" "); + write_str(blue); + write_str(" ▗▄▄▟██▄▄▄▄▄"); + write_str(cyan); + write_str("▝█▙█▛ "); + write_str(blue); + write_str("▖ "); + write_str(cyan); + write_str(" "); + write_str(blue); + write_str("System"); + write_str(reset); + write_str("  "); + write_str(os_name); + write_str("\n"); + + write_str(" "); + write_str(blue); + write_str(" ▀▀▀▀▀▀▀▀▀▀▀▘"); + write_str(cyan); + write_str("▝██ "); + write_str(blue); + write_str("▟█▖ "); + write_str(cyan); + write_str(" "); + write_str(blue); + write_str("Kernel"); + write_str(reset); + write_str("  "); + write_str(kernel_version); + write_str("\n"); + + write_str(" "); + write_str(cyan); + write_str(" ▟█▛ "); + write_str(cyan); + write_str("▝█▘"); + write_str(blue); + write_str("▟█▛ "); + write_str(cyan); + write_str(" "); + write_str(blue); + write_str("Shell"); + write_str(reset); + write_str("  "); + write_str(shell); + write_str("\n"); + + write_str(" "); + write_str(cyan); + write_str("▟█████▛ "); + write_str(blue); + write_str("▟█████▛ "); + write_str(cyan); + write_str(" "); + write_str(blue); + write_str("Uptime"); + write_str(reset); + write_str("  "); + write_str(uptime); + write_str("\n"); + + write_str(" "); + write_str(cyan); + write_str(" ▟█▛"); + write_str(blue); + write_str("▗█▖ "); + write_str(blue); + write_str("▟█▛ "); + write_str(cyan); + write_str(" "); + write_str(blue); + write_str("Desktop"); + write_str(reset); + write_str("  "); + write_str(desktop); + write_str("\n"); + + write_str(" "); + write_str(cyan); + write_str(" ▝█▛ "); + write_str(blue); + write_str("██▖"); + write_str(cyan); + write_str("▗▄▄▄▄▄▄▄▄▄▄▄ "); + write_str(cyan); + write_str("󰍛 "); + write_str(blue); + write_str("Memory"); + write_str(reset); + write_str("  "); + write_str(memory_usage); + write_str("\n"); + + write_str(" "); + write_str(cyan); + write_str(" ▝ "); + write_str(blue); + write_str("▟█▜█▖"); + write_str(cyan); + write_str("▀▀▀▀▀██▛▀▀▘ "); + write_str(cyan); + write_str("󱥎 "); + write_str(blue); + write_str("Storage (/)"); + write_str(reset); + write_str("  "); + write_str(storage); + write_str("\n"); + + write_str(" "); + write_str(blue); + write_str(" ▟█▘ ▜█▖ "); + write_str(cyan); + write_str("▝█▛ "); + write_str(cyan); + write_str(" "); + write_str(blue); + write_str("Colors"); + write_str(reset); + write_str("  "); + write_str(colors); + write_str("\n\n"); - let len = - usize::try_from(cursor.position()).expect("cursor position fits usize"); // Direct syscall to avoid stdout buffering allocation - let written = unsafe { sys_write(1, buf.as_ptr(), len) }; + let written = unsafe { sys_write(1, buf.as_ptr(), pos) }; if written < 0 { - return Err(io::Error::last_os_error().into()); + #[allow(clippy::cast_possible_truncation)] + return Err(Error::OsError(written as i32)); } - #[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(), - ); + #[allow(clippy::cast_sign_loss)] + if written as usize != pos { + return Err(Error::WriteError); } Ok(()) } +/// Print version information using direct syscall. +fn print_version() { + const VERSION: &str = concat!("Microfetch ", env!("CARGO_PKG_VERSION"), "\n"); + unsafe { + let _ = sys_write(1, VERSION.as_ptr(), VERSION.len()); + } +} + +/// Check if --version was passed via argc/argv. +/// +/// # Safety +/// +/// This function must be called with valid argc and argv from the program entry +/// point. +unsafe fn check_version_flag(argc: i32, argv: *const *const u8) -> bool { + if argc < 2 { + return false; + } + // SAFETY: argv is a valid array of argc pointers + let arg1 = unsafe { *argv.add(1) }; + if arg1.is_null() { + return false; + } + // Check if arg1 is "--version" + let version_flag = b"--version\0"; + for (i, &b) in version_flag.iter().enumerate() { + // SAFETY: arg1 is a valid C string + let arg_byte = unsafe { *arg1.add(i) }; + if arg_byte != b { + return false; + } + } + true +} + /// Main entry point for microfetch - can be called by the binary crate -/// or by other consumers of the library +/// or by other consumers of the library. +/// +/// # Arguments +/// +/// * `argc` - Argument count from main +/// * `argv` - Argument vector from main /// /// # Errors /// /// Returns an error if any system call fails +/// +/// # Safety +/// +/// argv must be a valid null-terminated array of C strings. #[cfg_attr(feature = "hotpath", hotpath::main)] -pub fn run() -> Result<(), Box> { - if Some("--version") == std::env::args().nth(1).as_deref() { - println!("Microfetch {}", env!("CARGO_PKG_VERSION")); - } else { - let utsname = UtsName::uname()?; - let fields = Fields { - user_info: system::get_username_and_hostname(&utsname), - os_name: release::get_os_pretty_name()?, - kernel_version: release::get_system_info(&utsname), - shell: system::get_shell(), - desktop: desktop::get_desktop_info(), - uptime: uptime::get_current()?, - memory_usage: system::get_memory_usage()?, - storage: system::get_root_disk_usage()?, - colors: colors::print_dots(), - }; - print_system_info(&fields)?; +pub unsafe fn run(argc: i32, argv: *const *const u8) -> Result<(), Error> { + if unsafe { check_version_flag(argc, argv) } { + print_version(); + return Ok(()); } + let utsname = UtsName::uname()?; + let fields = Fields { + user_info: system::get_username_and_hostname(&utsname), + os_name: release::get_os_pretty_name()?, + kernel_version: release::get_system_info(&utsname), + shell: system::get_shell(), + desktop: desktop::get_desktop_info(), + uptime: uptime::get_current()?, + memory_usage: system::get_memory_usage()?, + storage: system::get_root_disk_usage()?, + colors: colors::print_dots(), + }; + print_system_info(&fields)?; + Ok(()) } diff --git a/crates/lib/src/release.rs b/crates/lib/src/release.rs index 41a55c3..299c1c9 100644 --- a/crates/lib/src/release.rs +++ b/crates/lib/src/release.rs @@ -1,6 +1,6 @@ -use std::{fmt::Write as _, io}; +use alloc::string::String; -use crate::{UtsName, syscall::read_file_fast}; +use crate::{Error, UtsName, syscall::read_file_fast}; #[must_use] #[cfg_attr(feature = "hotpath", hotpath::measure)] @@ -13,7 +13,14 @@ pub fn get_system_info(utsname: &UtsName) -> String { let capacity = sysname.len() + 1 + release.len() + 2 + machine.len() + 1; let mut result = String::with_capacity(capacity); - write!(result, "{sysname} {release} ({machine})").unwrap(); + // Manual string construction instead of write! macro + result.push_str(sysname); + result.push(' '); + result.push_str(release); + result.push_str(" ("); + result.push_str(machine); + result.push(')'); + result } @@ -23,7 +30,7 @@ pub fn get_system_info(utsname: &UtsName) -> String { /// /// Returns an error if `/etc/os-release` cannot be read. #[cfg_attr(feature = "hotpath", hotpath::measure)] -pub fn get_os_pretty_name() -> Result { +pub fn get_os_pretty_name() -> Result { // Fast byte-level scanning for PRETTY_NAME= const PREFIX: &[u8] = b"PRETTY_NAME="; @@ -31,7 +38,7 @@ pub fn get_os_pretty_name() -> Result { // Use fast syscall-based file reading let bytes_read = read_file_fast("/etc/os-release", &mut buffer) - .map_err(|e| io::Error::from_raw_os_error(-e))?; + .map_err(Error::from_raw_os_error)?; let content = &buffer[..bytes_read]; let mut offset = 0; @@ -66,5 +73,5 @@ pub fn get_os_pretty_name() -> Result { offset += line_end + 1; } - Ok("Unknown".to_owned()) + Ok(String::from("Unknown")) } diff --git a/crates/lib/src/system.rs b/crates/lib/src/system.rs index 53ab38a..b902dac 100644 --- a/crates/lib/src/system.rs +++ b/crates/lib/src/system.rs @@ -1,37 +1,39 @@ -use std::{ffi::OsStr, fmt::Write as _, io, mem::MaybeUninit}; +use alloc::string::String; +use core::mem::MaybeUninit; use crate::{ + Error, UtsName, - colors::COLORS, + 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_os = std::env::var_os("USER"); - let username = username_os - .as_deref() - .and_then(OsStr::to_str) - .unwrap_or("unknown_user"); + let username = crate::getenv_str("USER").unwrap_or("unknown_user"); let hostname = utsname.nodename().to_str().unwrap_or("unknown_host"); - let capacity = COLORS.yellow.len() + // Get colors (checking NO_COLOR only once) + let no_color = crate::colors::is_no_color(); + let colors = Colors::new(no_color); + + let capacity = colors.yellow.len() + username.len() - + COLORS.red.len() + + colors.red.len() + 1 - + COLORS.green.len() + + colors.green.len() + hostname.len() - + COLORS.reset.len(); + + colors.reset.len(); let mut result = String::with_capacity(capacity); - result.push_str(COLORS.yellow); + result.push_str(colors.yellow); result.push_str(username); - result.push_str(COLORS.red); + result.push_str(colors.red); result.push('@'); - result.push_str(COLORS.green); + result.push_str(colors.green); result.push_str(hostname); - result.push_str(COLORS.reset); + result.push_str(colors.reset); result } @@ -39,13 +41,12 @@ pub fn get_username_and_hostname(utsname: &UtsName) -> String { #[must_use] #[cfg_attr(feature = "hotpath", hotpath::measure)] pub fn get_shell() -> String { - let shell_os = std::env::var_os("SHELL"); - let shell = shell_os.as_deref().and_then(OsStr::to_str).unwrap_or(""); + let shell = crate::getenv_str("SHELL").unwrap_or(""); let start = shell.rfind('/').map_or(0, |i| i + 1); if shell.is_empty() { - "unknown_shell".into() + String::from("unknown_shell") } else { - shell[start..].into() + String::from(&shell[start..]) } } @@ -56,12 +57,12 @@ pub fn get_shell() -> String { /// Returns an error if the filesystem information cannot be retrieved. #[cfg_attr(feature = "hotpath", hotpath::measure)] #[allow(clippy::cast_precision_loss)] -pub fn get_root_disk_usage() -> Result { +pub fn get_root_disk_usage() -> Result { let mut vfs = MaybeUninit::::uninit(); let path = b"/\0"; if unsafe { sys_statfs(path.as_ptr(), vfs.as_mut_ptr()) } != 0 { - return Err(io::Error::last_os_error()); + return Err(Error::last_os_error()); } let vfs = unsafe { vfs.assume_init() }; @@ -77,18 +78,96 @@ pub fn get_root_disk_usage() -> Result { let used_size = used_size as f64 / (1024.0 * 1024.0 * 1024.0); let usage = (used_size / total_size) * 100.0; + let no_color = crate::colors::is_no_color(); + let colors = Colors::new(no_color); + let mut result = String::with_capacity(64); - write!( - result, - "{used_size:.2} GiB / {total_size:.2} GiB ({cyan}{usage:.0}%{reset})", - cyan = COLORS.cyan, - reset = COLORS.reset, - ) - .unwrap(); + + // Manual float formatting + write_float(&mut result, used_size, 2); + result.push_str(" GiB / "); + write_float(&mut result, total_size, 2); + result.push_str(" GiB ("); + result.push_str(colors.cyan); + write_float(&mut result, usage, 0); + result.push('%'); + result.push_str(colors.reset); + result.push(')'); Ok(result) } +/// Write a float to string with specified decimal places +#[allow( + clippy::cast_sign_loss, + clippy::cast_possible_truncation, + clippy::cast_precision_loss +)] +fn write_float(s: &mut String, val: f64, decimals: u32) { + // Handle integer part + let int_part = val as u64; + write_u64(s, int_part); + + if decimals > 0 { + s.push('.'); + + // Calculate fractional part + let mut frac = val - int_part as f64; + for _ in 0..decimals { + frac *= 10.0; + let digit = frac as u8; + s.push((b'0' + digit) as char); + frac -= f64::from(digit); + } + } +} + +/// Round an f64 to nearest integer (`f64::round` is not in core) +#[allow( + clippy::cast_precision_loss, + clippy::cast_possible_truncation, + clippy::cast_sign_loss +)] +fn round_f64(x: f64) -> f64 { + if x >= 0.0 { + let int_part = x as u64 as f64; + let frac = x - int_part; + if frac >= 0.5 { + int_part + 1.0 + } else { + int_part + } + } else { + let int_part = (-x) as u64 as f64; + let frac = -x - int_part; + if frac >= 0.5 { + -(int_part + 1.0) + } else { + -int_part + } + } +} + +/// Write a u64 to string +fn write_u64(s: &mut String, mut n: u64) { + if n == 0 { + s.push('0'); + return; + } + + let mut buf = [0u8; 20]; + let mut i = 20; + + while n > 0 { + i -= 1; + buf[i] = b'0' + (n % 10) as u8; + n /= 10; + } + + // SAFETY: buf contains only ASCII digits + s.push_str(unsafe { core::str::from_utf8_unchecked(&buf[i..]) }); +} + /// Fast integer parsing without stdlib overhead #[inline] fn parse_u64_fast(s: &[u8]) -> u64 { @@ -109,16 +188,16 @@ fn parse_u64_fast(s: &[u8]) -> u64 { /// /// Returns an error if `/proc/meminfo` cannot be read. #[cfg_attr(feature = "hotpath", hotpath::measure)] -pub fn get_memory_usage() -> Result { +pub fn get_memory_usage() -> Result { #[cfg_attr(feature = "hotpath", hotpath::measure)] - fn parse_memory_info() -> Result<(f64, f64), io::Error> { + fn parse_memory_info() -> Result<(f64, f64), Error> { let mut total_memory_kb = 0u64; let mut available_memory_kb = 0u64; let mut buffer = [0u8; 1024]; // Use fast syscall-based file reading let bytes_read = read_file_fast("/proc/meminfo", &mut buffer) - .map_err(|e| io::Error::from_raw_os_error(-e))?; + .map_err(Error::from_raw_os_error)?; let meminfo = &buffer[..bytes_read]; // Fast scanning for MemTotal and MemAvailable @@ -168,17 +247,22 @@ pub fn get_memory_usage() -> Result { let (used_memory, total_memory) = parse_memory_info()?; #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] - let percentage_used = (used_memory / total_memory * 100.0).round() as u64; + let percentage_used = round_f64(used_memory / total_memory * 100.0) as u64; + + let no_color = crate::colors::is_no_color(); + let colors = Colors::new(no_color); let mut result = String::with_capacity(64); - write!( - result, - "{used_memory:.2} GiB / {total_memory:.2} GiB \ - ({cyan}{percentage_used}%{reset})", - cyan = COLORS.cyan, - reset = COLORS.reset, - ) - .unwrap(); + + write_float(&mut result, used_memory, 2); + result.push_str(" GiB / "); + write_float(&mut result, total_memory, 2); + result.push_str(" GiB ("); + result.push_str(colors.cyan); + write_u64(&mut result, percentage_used); + result.push('%'); + result.push_str(colors.reset); + result.push(')'); Ok(result) } diff --git a/crates/lib/src/uptime.rs b/crates/lib/src/uptime.rs index b529f53..4ed2200 100644 --- a/crates/lib/src/uptime.rs +++ b/crates/lib/src/uptime.rs @@ -1,6 +1,7 @@ -use std::{io, mem::MaybeUninit}; +use alloc::string::String; +use core::mem::MaybeUninit; -use crate::syscall::sys_sysinfo; +use crate::{Error, syscall::sys_sysinfo}; /// Faster integer to string conversion without the formatting overhead. #[inline] @@ -16,7 +17,8 @@ fn itoa(mut n: u64, buf: &mut [u8]) -> &str { n /= 10; } - unsafe { std::str::from_utf8_unchecked(&buf[i..]) } + // SAFETY: We only wrote ASCII digits + unsafe { core::str::from_utf8_unchecked(&buf[i..]) } } /// Gets the current system uptime. @@ -25,11 +27,11 @@ fn itoa(mut n: u64, buf: &mut [u8]) -> &str { /// /// Returns an error if the system uptime cannot be retrieved. #[cfg_attr(feature = "hotpath", hotpath::measure)] -pub fn get_current() -> Result { +pub fn get_current() -> Result { let uptime_seconds = { let mut info = MaybeUninit::uninit(); if unsafe { sys_sysinfo(info.as_mut_ptr()) } != 0 { - return Err(io::Error::last_os_error()); + return Err(Error::last_os_error()); } #[allow(clippy::cast_sign_loss)] unsafe { diff --git a/microfetch/Cargo.toml b/microfetch/Cargo.toml index 4680aa8..b29b420 100644 --- a/microfetch/Cargo.toml +++ b/microfetch/Cargo.toml @@ -11,12 +11,14 @@ repository = "https://github.com/notashelf/microfetch" publish = false [dependencies] -hotpath = { optional = true, version = "0.14.0" } +hotpath = { optional = true, version = "0.14.0" } +microfetch-alloc.workspace = true microfetch-lib.workspace = true +microfetch-asm.workspace = true [features] hotpath = [ "dep:hotpath" ] hotpath-alloc = [ "hotpath/hotpath-alloc" ] [lints] -workspace = true +workspace = true \ No newline at end of file diff --git a/microfetch/src/main.rs b/microfetch/src/main.rs index 8f22041..0bdc022 100644 --- a/microfetch/src/main.rs +++ b/microfetch/src/main.rs @@ -1,3 +1,60 @@ -fn main() -> Result<(), Box> { - microfetch_lib::run() +#![no_std] +#![no_main] + +extern crate alloc; + +use microfetch_alloc::BumpAlloc; +use microfetch_asm::sys_write; +#[cfg(not(test))] +use {core::panic::PanicInfo, microfetch_asm::sys_exit}; + +#[global_allocator] +static ALLOCATOR: BumpAlloc = BumpAlloc::new(); + +/// Receives argc and argv directly. The C runtime will call this after +/// initializing the environment. Cool right? +/// +/// # Safety +/// +/// argv must be a valid pointer to an array of argc C strings. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn main(argc: i32, argv: *const *const u8) -> i32 { + // SAFETY: argc and argv are provided by the C runtime and are valid + unsafe { + match microfetch_lib::run(argc, argv) { + Ok(()) => 0, + Err(e) => { + // Print error message to stderr (fd 2) + let msg = alloc::format!("Error: {e}\n"); + let _ = sys_write(2, msg.as_ptr(), msg.len()); + 1 + }, + } + } +} + +#[cfg(not(test))] +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + // Write "panic" to stderr and exit + const PANIC_MSG: &[u8] = b"panic\n"; + unsafe { + let _ = sys_write(2, PANIC_MSG.as_ptr(), PANIC_MSG.len()); + sys_exit(1) + } +} + +// FIXME: Stubs for Rust exception handling symbols needed when using alloc with +// panic=abort These are normally provided by the unwinding runtime, but we're +// using panic=abort. I don't actually think this is the correct approach, but I +// cannot think of anything better. + +#[cfg(not(test))] +#[unsafe(no_mangle)] +const extern "C" fn rust_eh_personality() {} + +#[cfg(not(test))] +#[unsafe(no_mangle)] +extern "C" fn _Unwind_Resume() -> ! { + unsafe { sys_exit(1) } }