diff --git a/Cargo.lock b/Cargo.lock index 9fa3968..697e1ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -486,15 +486,10 @@ name = "microfetch" version = "1.2.0" dependencies = [ "hotpath", - "microfetch-alloc", "microfetch-asm", "microfetch-lib", ] -[[package]] -name = "microfetch-alloc" -version = "1.2.0" - [[package]] name = "microfetch-asm" version = "1.2.0" diff --git a/Cargo.toml b/Cargo.toml index a6111fd..3b703c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = [ "crates/*", "microfetch", "crates/benchmarks" ] +members = [ "crates/asm", "crates/benchmarks", "crates/lib", "microfetch" ] resolver = "3" [workspace.package] @@ -9,9 +9,8 @@ rust-version = "1.95.0" version = "1.2.0" [workspace.dependencies] -microfetch-alloc = { path = "./crates/alloc", version = "1.2.0" } -microfetch-asm = { path = "./crates/asm", version = "1.2.0" } -microfetch-lib = { path = "./crates/lib", version = "1.2.0" } +microfetch-asm = { path = "./crates/asm", version = "1.2.0" } +microfetch-lib = { path = "./crates/lib", version = "1.2.0" } criterion = { default-features = false, features = [ "cargo_bench_support" ], version = "0.8.2" } criterion-cycles-per-byte = "0.8.0" diff --git a/crates/alloc/Cargo.toml b/crates/alloc/Cargo.toml deleted file mode 100644 index 26c13e4..0000000 --- a/crates/alloc/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "microfetch-alloc" -description = "Simple, std-free bump allocator for Microfetch" -version.workspace = true -edition.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 deleted file mode 100644 index 06e3f07..0000000 --- a/crates/alloc/src/lib.rs +++ /dev/null @@ -1,106 +0,0 @@ -//! 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/darwin.rs b/crates/asm/src/darwin.rs index 33625da..4c15518 100644 --- a/crates/asm/src/darwin.rs +++ b/crates/asm/src/darwin.rs @@ -175,7 +175,7 @@ const HOST_VM_INFO64: c_int = 4; /// `host_statistics64` matches the kernel's expectation exactly (38 ints). #[repr(C)] #[derive(Default)] -#[expect(dead_code, reason = "most fields exist only to fix the struct layout")] +#[allow(dead_code, reason = "most fields exist only to fix the struct layout")] struct VmStatistics64 { free_count: u32, active_count: u32, @@ -240,7 +240,7 @@ pub fn macos_meminfo() -> Option<(u64, u64)> { /// `struct timeval` (`time_t` is 64-bit, `suseconds_t` is 32-bit on macOS). #[repr(C)] #[derive(Default)] -#[expect( +#[allow( dead_code, reason = "tv_usec/_pad exist only to fix the struct layout" )] diff --git a/crates/benchmarks/benches/microfetch.rs b/crates/benchmarks/benches/microfetch.rs index 8d62082..7755786 100644 --- a/crates/benchmarks/benches/microfetch.rs +++ b/crates/benchmarks/benches/microfetch.rs @@ -1,32 +1,82 @@ use criterion::{Criterion, criterion_group, criterion_main}; use microfetch_lib::{ + StackWriter, UtsName, - colors::print_dots, - desktop::get_desktop_info, - release::{get_os_pretty_name, get_system_info}, - system::{ - get_memory_usage, - get_root_disk_usage, - get_shell, - get_username_and_hostname, - }, - uptime::get_current, + colors::{self, Colors}, + cpu, + desktop, + release, + system, + uptime, }; fn main_benchmark(c: &mut Criterion) { let utsname = UtsName::uname().expect("Failed to get uname"); + let colors = Colors::new(false); + c.bench_function("user_info", |b| { - b.iter(|| get_username_and_hostname(&utsname)); + b.iter(|| { + let mut buf = [0u8; 256]; + let mut w = StackWriter::new(&mut buf); + system::write_username_and_hostname(&mut w, &colors, &utsname); + }); + }); + c.bench_function("os_name", |b| { + b.iter(|| { + let mut buf = [0u8; 256]; + let mut w = StackWriter::new(&mut buf); + let _ = release::write_os_pretty_name(&mut w); + }); + }); + c.bench_function("kernel_version", |b| { + b.iter(|| { + let mut buf = [0u8; 256]; + let mut w = StackWriter::new(&mut buf); + release::write_system_info(&mut w, &utsname); + }); + }); + c.bench_function("shell", |b| { + b.iter(|| { + let mut buf = [0u8; 256]; + let mut w = StackWriter::new(&mut buf); + system::write_shell(&mut w); + }); + }); + c.bench_function("desktop", |b| { + b.iter(|| { + let mut buf = [0u8; 256]; + let mut w = StackWriter::new(&mut buf); + desktop::write_desktop_info(&mut w); + }); + }); + c.bench_function("uptime", |b| { + b.iter(|| { + let mut buf = [0u8; 256]; + let mut w = StackWriter::new(&mut buf); + let _ = uptime::write_uptime(&mut w); + }); + }); + c.bench_function("memory_usage", |b| { + b.iter(|| { + let mut buf = [0u8; 256]; + let mut w = StackWriter::new(&mut buf); + let _ = system::write_memory_usage(&mut w, &colors); + }); + }); + c.bench_function("storage", |b| { + b.iter(|| { + let mut buf = [0u8; 256]; + let mut w = StackWriter::new(&mut buf); + let _ = system::write_root_disk_usage(&mut w, &colors); + }); + }); + c.bench_function("colors", |b| { + b.iter(|| { + let mut buf = [0u8; 256]; + let mut w = StackWriter::new(&mut buf); + colors::write_dots(&mut w, &colors); + }); }); - c.bench_function("os_name", |b| b.iter(get_os_pretty_name)); - c.bench_function("kernel_version", |b| b.iter(|| get_system_info(&utsname))); - c.bench_function("shell", |b| b.iter(get_shell)); - - c.bench_function("desktop", |b| b.iter(get_desktop_info)); - c.bench_function("uptime", |b| b.iter(get_current)); - c.bench_function("memory_usage", |b| b.iter(get_memory_usage)); - c.bench_function("storage", |b| b.iter(get_root_disk_usage)); - c.bench_function("colors", |b| b.iter(print_dots)); } criterion_group!(benches, main_benchmark); diff --git a/crates/lib/src/colors.rs b/crates/lib/src/colors.rs index 9f012f9..5e76298 100644 --- a/crates/lib/src/colors.rs +++ b/crates/lib/src/colors.rs @@ -1,4 +1,4 @@ -use alloc::string::String; +use crate::StackWriter; /// Color codes for terminal output pub struct Colors { @@ -59,47 +59,27 @@ pub(crate) fn is_no_color() -> bool { is_set } -#[must_use] #[cfg_attr(feature = "hotpath", hotpath::measure)] -pub fn print_dots() -> String { - const GLYPH: &str = ""; +pub fn write_dots(w: &mut StackWriter, colors: &Colors) { + const GLYPH: &str = "●"; - 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(GLYPH); - result.push_str(" "); - result.push_str(colors.cyan); - result.push_str(GLYPH); - result.push_str(" "); - result.push_str(colors.green); - result.push_str(GLYPH); - result.push_str(" "); - result.push_str(colors.yellow); - result.push_str(GLYPH); - result.push_str(" "); - result.push_str(colors.red); - result.push_str(GLYPH); - result.push_str(" "); - result.push_str(colors.magenta); - result.push_str(GLYPH); - result.push_str(" "); - result.push_str(colors.reset); - - result + w.push_str(colors.blue); + w.push_str(GLYPH); + w.push_str(" "); + w.push_str(colors.cyan); + w.push_str(GLYPH); + w.push_str(" "); + w.push_str(colors.green); + w.push_str(GLYPH); + w.push_str(" "); + w.push_str(colors.yellow); + w.push_str(GLYPH); + w.push_str(" "); + w.push_str(colors.red); + w.push_str(GLYPH); + w.push_str(" "); + w.push_str(colors.magenta); + w.push_str(GLYPH); + w.push_str(" "); + w.push_str(colors.reset); } diff --git a/crates/lib/src/cpu.rs b/crates/lib/src/cpu.rs index 689607f..5b49775 100644 --- a/crates/lib/src/cpu.rs +++ b/crates/lib/src/cpu.rs @@ -1,37 +1,30 @@ -use alloc::string::String; - #[cfg(target_os = "linux")] use crate::syscall::read_file_fast; -use crate::{Error, system::write_u64}; +use crate::{Error, StackWriter}; -/// Gets CPU model name (trimmed), or empty string if unavailable. +/// Writes CPU model name (trimmed) to the writer. #[cfg(target_os = "linux")] #[cfg_attr(feature = "hotpath", hotpath::measure)] -#[must_use] -pub fn get_cpu_name() -> String { - get_model_name().unwrap_or_default() +pub fn write_cpu_name(w: &mut StackWriter) { + write_model_name(w); } -/// Gets CPU model name from `machdep.cpu.brand_string` (macOS), +/// Writes CPU model name from `machdep.cpu.brand_string` (macOS), /// e.g. `Apple M2 Pro`. Returns an empty string if unavailable. #[cfg(target_os = "macos")] #[cfg_attr(feature = "hotpath", hotpath::measure)] -pub fn get_cpu_name() -> String { +pub fn write_cpu_name(w: &mut StackWriter) { let mut buf = [0u8; 128]; - match crate::syscall::macos_sysctl_str( - b"machdep.cpu.brand_string\0", - &mut buf, - ) { - Some(n) => { - core::str::from_utf8(&buf[..n]) - .map(String::from) - .unwrap_or_default() - }, - None => String::new(), + if let Some(n) = + crate::syscall::macos_sysctl_str(b"machdep.cpu.brand_string\0", &mut buf) + { + if let Ok(name) = core::str::from_utf8(&buf[..n]) { + w.push_str(name); + } } } -/// Gets CPU core/thread info as a string. +/// Writes CPU core/thread info string. /// /// Format: `{cores} cores ({p}p/{e}e), {threads} threads` on hybrid Intel, /// `{cores} cores, {threads} threads` otherwise. @@ -41,13 +34,14 @@ pub fn get_cpu_name() -> String { /// Returns an error if the thread count cannot be determined. #[cfg(target_os = "linux")] #[cfg_attr(feature = "hotpath", hotpath::measure)] -pub fn get_cpu_cores() -> Result { +pub fn write_cpu_cores(w: &mut StackWriter) -> Result<(), Error> { let threads = get_thread_count()?; let cores = get_core_count(threads); - Ok(format_cores(cores, get_pe_cores(), threads)) + write_cores(w, cores, get_pe_cores(), threads); + Ok(()) } -/// Gets CPU core/thread info via `sysctl` (macOS). +/// Writes CPU core/thread info via `sysctl` (macOS). /// /// On Apple Silicon `hw.perflevel0`/`hw.perflevel1` expose the performance /// (P) and efficiency (E) core counts respectively. @@ -57,7 +51,7 @@ pub fn get_cpu_cores() -> Result { /// Returns an error if the logical CPU count cannot be determined. #[cfg(target_os = "macos")] #[cfg_attr(feature = "hotpath", hotpath::measure)] -pub fn get_cpu_cores() -> Result { +pub fn write_cpu_cores(w: &mut StackWriter) -> Result<(), Error> { use crate::syscall::macos_sysctl_u32; let threads = @@ -74,33 +68,35 @@ pub fn get_cpu_cores() -> Result { _ => None, }; - Ok(format_cores(cores, pe, threads)) + write_cores(w, cores, pe, threads); + Ok(()) } -/// Formats core/thread counts identically across platforms: +/// Writes core/thread counts identically across platforms: /// `{cores} cores ({p}p/{e}e), {threads} threads`, omitting the P/E group and /// the thread suffix when not applicable. -fn format_cores(cores: u32, pe: Option<(u32, u32)>, threads: u32) -> String { - let mut result = String::new(); - - write_u64(&mut result, u64::from(cores)); - result.push_str(" cores"); +fn write_cores( + w: &mut StackWriter, + cores: u32, + pe: Option<(u32, u32)>, + threads: u32, +) { + w.push_u64(u64::from(cores)); + w.push_str(" cores"); if let Some((p, e)) = pe { - result.push_str(" ("); - write_u64(&mut result, u64::from(p)); - result.push_str("p/"); - write_u64(&mut result, u64::from(e)); - result.push_str("e)"); + w.push_str(" ("); + w.push_u64(u64::from(p)); + w.push_str("p/"); + w.push_u64(u64::from(e)); + w.push_str("e)"); } if threads != cores { - result.push_str(", "); - write_u64(&mut result, u64::from(threads)); - result.push_str(" threads"); + w.push_str(", "); + w.push_u64(u64::from(threads)); + w.push_str(" threads"); } - - result } /// Count online threads via `sched_getaffinity(2)`. @@ -313,38 +309,46 @@ fn get_cpu_freq_mhz() -> Option { None } -/// Parse CPU model name from `/proc/cpuinfo` and append frequency. +/// Parse CPU model name from `/proc/cpuinfo` and write it directly. +/// Appends CPU frequency if available. #[cfg(target_os = "linux")] -fn get_model_name() -> Option { +fn write_model_name(w: &mut StackWriter) { let mut buf = [0u8; 2048]; - let n = read_file_fast("/proc/cpuinfo", &mut buf).ok()?; + let Ok(n) = read_file_fast("/proc/cpuinfo", &mut buf) else { + return; + }; let data = &buf[..n]; - let base = extract_name(data)?; - let mut name = base; + let found = if let Some(name) = extract_name(data) { + w.push_str(name); + true + } else { + write_dt_compatible(w) + }; + if !found { + return; + } + if let Some(mhz) = get_cpu_freq_mhz() { - name.push_str(" @ "); + w.push_str(" @ "); // Round to nearest 0.01 GHz, then split so carries (e.g. 1999 MHz) // roll into the integer part instead of overflowing the fraction. let rounded_centesimal = (mhz + 5) / 10; let ghz_int = rounded_centesimal / 100; let ghz_frac = rounded_centesimal % 100; - write_u64(&mut name, u64::from(ghz_int)); - name.push('.'); + w.push_u64(u64::from(ghz_int)); + w.push_byte(b'.'); if ghz_frac < 10 { - name.push('0'); + w.push_byte(b'0'); } - write_u64(&mut name, u64::from(ghz_frac)); - name.push_str(" GHz"); + w.push_u64(u64::from(ghz_frac)); + w.push_str(" GHz"); } - Some(name) } -/// Extract a human-readable CPU name. Tries cpuinfo fields first, then -/// falls back to the device-tree `compatible` string on `SoCs` that don't -/// expose a model through cpuinfo. +/// Extract a human-readable CPU name from cpuinfo fields. #[cfg(target_os = "linux")] -fn extract_name(data: &[u8]) -> Option { +fn extract_name(data: &[u8]) -> Option<&str> { for key in &[ b"model name" as &[u8], b"Model Name", @@ -358,35 +362,42 @@ fn extract_name(data: &[u8]) -> Option { if let Some(val) = extract_field(data, key) { let trimmed = trim(val); if !trimmed.is_empty() { - return Some(String::from(trimmed)); + return Some(trimmed); } } } - parse_dt_compatible() + None } -/// Parse the `SoC` name from `/sys/firmware/devicetree/base/compatible`. +/// Write the `SoC` name from `/sys/firmware/devicetree/base/compatible`. /// The file holds NUL-separated `vendor,model` strings from most-specific -/// (board) to most-generic (`SoC`); we take the last entry and return just +/// (board) to most-generic (`SoC`); we take the last entry and write just /// the model portion after the comma. #[cfg(target_os = "linux")] -fn parse_dt_compatible() -> Option { +fn write_dt_compatible(w: &mut StackWriter) -> bool { let mut buf = [0u8; 256]; - let n = read_file_fast("/sys/firmware/devicetree/base/compatible", &mut buf) - .ok()?; + let Ok(n) = + read_file_fast("/sys/firmware/devicetree/base/compatible", &mut buf) + else { + return false; + }; // Drop the terminating NUL so the rposition below locates the entry // separator rather than the end-of-string marker. let end = if n > 0 && buf[n - 1] == 0 { n - 1 } else { n }; let data = &buf[..end]; let start = data.iter().rposition(|&b| b == 0).map_or(0, |p| p + 1); let entry = &data[start..]; - let comma = entry.iter().position(|&b| b == b',')?; - let model = core::str::from_utf8(&entry[comma + 1..]).ok()?; + let Some(comma) = entry.iter().position(|&b| b == b',') else { + return false; + }; + let Ok(model) = core::str::from_utf8(&entry[comma + 1..]) else { + return false; + }; if model.is_empty() { - None - } else { - Some(String::from(model)) + return false; } + w.push_str(model); + true } /// Extract value of first occurrence of `key` in cpuinfo. @@ -411,7 +422,8 @@ fn extract_field<'a>(data: &'a [u8], key: &[u8]) -> Option<&'a str> { while p < line.len() && line[p] == b' ' { p += 1; } - return core::str::from_utf8(&line[p..]).ok(); + // SAFETY: cpuinfo fields are ASCII + return Some(unsafe { core::str::from_utf8_unchecked(&line[p..]) }); } } diff --git a/crates/lib/src/desktop.rs b/crates/lib/src/desktop.rs index c4317ec..3b5871a 100644 --- a/crates/lib/src/desktop.rs +++ b/crates/lib/src/desktop.rs @@ -1,10 +1,7 @@ -use alloc::string::String; +use crate::{StackWriter, getenv_str}; -use crate::getenv_str; - -#[must_use] #[cfg_attr(feature = "hotpath", hotpath::measure)] -pub fn get_desktop_info() -> String { +pub fn write_desktop_info(w: &mut StackWriter) { let desktop_raw = getenv_str("XDG_CURRENT_DESKTOP"); let session_raw = getenv_str("XDG_SESSION_TYPE"); @@ -27,25 +24,20 @@ pub fn get_desktop_info() -> String { } }); - // Pre-calculate capacity: desktop_len + " (" + backend_len + ")" - // Capitalize first char needs temporary allocation only if backend exists - let mut result = - String::with_capacity(desktop_str.len() + backend_str.len() + 3); - result.push_str(desktop_str); - result.push_str(" ("); + w.push_str(desktop_str); + w.push_str(" ("); // Capitalize first character of backend - if let Some(first_byte) = backend_str.as_bytes().first() { + 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 + first_byte - b'a' + b'A' } else { - *first_byte as char + first_byte }; - result.push(upper); - result.push_str(&backend_str[1..]); + w.push_byte(upper); + w.push_str(&backend_str[1..]); } - result.push(')'); - result + w.push_byte(b')'); } diff --git a/crates/lib/src/lib.rs b/crates/lib/src/lib.rs index d33bee1..40e69e8 100644 --- a/crates/lib/src/lib.rs +++ b/crates/lib/src/lib.rs @@ -1,5 +1,4 @@ #![no_std] -extern crate alloc; pub mod colors; pub mod cpu; @@ -8,7 +7,6 @@ pub mod release; pub mod system; pub mod uptime; -use alloc::string::String; use core::{ ffi::CStr, mem::MaybeUninit, @@ -62,88 +60,6 @@ impl Error { } } -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); - } - } - } -} - // Store the environment pointer internally,initialized from `main()`. This // helps avoid the libc dependency *completely*. static ENVP: AtomicPtr<*const u8> = AtomicPtr::new(core::ptr::null_mut()); @@ -226,9 +142,15 @@ pub fn getenv(name: &str) -> Option<&'static [u8]> { } /// Gets an environment variable as a UTF-8 string. +/// +/// # Safety guarantee +/// +/// Environment variables set by the shell / login process are always valid +/// UTF-8 on any real Linux system. We skip the `from_utf8` validation and use +/// `from_utf8_unchecked`. #[must_use] pub fn getenv_str(name: &str) -> Option<&'static str> { - getenv(name).and_then(|bytes| core::str::from_utf8(bytes).ok()) + getenv(name).map(|bytes| unsafe { core::str::from_utf8_unchecked(bytes) }) } /// Checks if an environment variable exists (regardless of its value). @@ -276,50 +198,64 @@ impl UtsName { } } -// Struct to hold all the fields we need in order to print the fetch. This -// helps avoid Clippy warnings about argument count, and makes it slightly -// easier to pass data around. Though, it is not like we really need to. -struct Fields { - user_info: String, - os_name: String, - kernel_version: String, - cpu_name: String, - cpu_cores: String, - shell: String, - uptime: String, - desktop: String, - memory_usage: String, - storage: String, - colors: String, -} - -/// Minimal, stack-allocated writer implementing `core::fmt::Write`. Avoids heap -/// allocation for the output buffer. -struct StackWriter<'a> { +/// Minimal, stack-allocated writer. +pub struct StackWriter<'a> { buf: &'a mut [u8], pos: usize, } impl<'a> StackWriter<'a> { #[inline] - const fn new(buf: &'a mut [u8]) -> Self { + pub const fn new(buf: &'a mut [u8]) -> Self { Self { buf, pos: 0 } } #[inline] - fn written(&self) -> &[u8] { + #[must_use] + pub fn written(&self) -> &[u8] { &self.buf[..self.pos] } -} -impl core::fmt::Write for StackWriter<'_> { #[inline] - fn write_str(&mut self, s: &str) -> core::fmt::Result { - let bytes = s.as_bytes(); - let to_write = bytes.len().min(self.buf.len() - self.pos); - self.buf[self.pos..self.pos + to_write].copy_from_slice(&bytes[..to_write]); - self.pos += to_write; - Ok(()) + pub fn push_str(&mut self, s: &str) { + self.push_bytes(s.as_bytes()); + } + + #[inline] + pub fn push_bytes(&mut self, bytes: &[u8]) { + let n = bytes.len().min(self.buf.len() - self.pos); + self.buf[self.pos..self.pos + n].copy_from_slice(&bytes[..n]); + self.pos += n; + } + + #[inline] + pub fn push_byte(&mut self, b: u8) { + if self.pos < self.buf.len() { + self.buf[self.pos] = b; + self.pos += 1; + } + } + + /// Write a [`CStr`]'s bytes (excluding the null terminator). + #[inline] + pub fn push_cstr(&mut self, s: &CStr) { + self.push_bytes(s.to_bytes()); + } + + /// Write a u64 as decimal ASCII. + pub fn push_u64(&mut self, mut n: u64) { + if n == 0 { + self.push_byte(b'0'); + return; + } + let mut tmp = [0u8; 20]; + let mut i = 20; + while n > 0 { + i -= 1; + tmp[i] = b'0' + (n % 10) as u8; + n /= 10; + } + self.push_bytes(&tmp[i..]); } } @@ -336,197 +272,193 @@ const CUSTOM_LOGO: &str = match option_env!("MICROFETCH_LOGO") { None => "", }; -#[cfg_attr(feature = "hotpath", hotpath::measure)] -fn print_system_info(fields: &Fields) -> Result<(), Error> { - let Fields { - user_info, - os_name, - kernel_version, - cpu_name, - cpu_cores, - shell, - uptime, - desktop, - memory_usage, - storage, - colors, - } = fields; - - let no_color = colors::is_no_color(); - let c = colors::Colors::new(no_color); +/// Write the default two-tone NixOS braille logo for one row. +/// Color assignments derived from flood-fill decomposition of the two lambda +/// shapes. +#[allow(clippy::too_many_lines)] +fn write_logo(w: &mut StackWriter, c: &colors::Colors, row: usize) { + let (b, cy) = (c.blue, c.cyan); + match row { + 0 => { + w.push_str(b); + w.push_str("⠀⠀⠀⠀⠀⠀⢼⣿⣄⠀⠀⠀"); + w.push_str(cy); + w.push_str("⠹⣿⣷⡀⠀⣠⣿⡧⠀⠀⠀⠀⠀⠀"); + }, + 1 => { + w.push_str(b); + w.push_str("⠀⠀⠀⠀⠀⠀⠈⢿⣿⣆⠀⠀⠀"); + w.push_str(cy); + w.push_str("⠘⣿⣿⣴⣿⡿⠁⠀⠀⠀⠀⠀⠀"); + }, + 2 => { + w.push_str(b); + w.push_str("⠀⠀⠀⢠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡜"); + w.push_str(cy); + w.push_str("⢿⣿⣟⠀⠀⠀"); + w.push_str(b); + w.push_str("⢀⡄⠀⠀⠀"); + }, + 3 => { + w.push_str(b); + w.push_str("⠀⠀⠀⠉⠉⠉⠉"); + w.push_str(cy); + w.push_str("⣩⣭⡭"); + w.push_str(b); + w.push_str("⠉⠉⠉⠉⠉"); + w.push_str(cy); + w.push_str("⠈⢿⣿⣆⠀"); + w.push_str(b); + w.push_str("⢠⣿⣿⠂⠀⠀"); + }, + 4 => { + w.push_str(cy); + w.push_str("⠀⠀⠀⠀⠀⠀⣼⣿⡟⠀⠀⠀⠀⠀⠀⠀⠀⢻⡟"); + w.push_str(b); + w.push_str("⣡⣿⣿⠃⠀⠀⠀"); + }, + 5 => { + w.push_str(cy); + w.push_str("⢸⣿⣿⣿⣿⣿⣿⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀"); + w.push_str(b); + w.push_str("⣰⣿⣿⣿⣿⣿⣿⡇"); + }, + 6 => { + w.push_str(cy); + w.push_str("⠀⠀⠀⢠⣿⣿⢋"); + w.push_str(b); + w.push_str("⣼⣧⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⡟⠀⠀⠀⠀⠀⠀"); + }, + 7 => { + w.push_str(cy); + w.push_str("⠀⠀⠠⣿⣿⠃⠀"); + w.push_str(b); + w.push_str("⠹⣿⣷⡀"); + w.push_str(cy); + w.push_str("⣀⣀⣀⣀⣀"); + w.push_str(b); + w.push_str("⣚⣛⣋"); + w.push_str(cy); + w.push_str("⣀⣀⣀⣀⠀⠀⠀"); + }, + 8 => { + w.push_str(cy); + w.push_str("⠀⠀⠀⠘⠁⠀⠀⠀"); + w.push_str(b); + w.push_str("⣽⣿⣷⡜"); + w.push_str(cy); + w.push_str("⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃⠀⠀⠀"); + }, + 9 => { + w.push_str(b); + w.push_str("⠀⠀⠀⠀⠀⠀⢀⣾⣿⠟⣿⣿⡄⠀⠀⠀"); + w.push_str(cy); + w.push_str("⠹⣿⣷⡀⠀⠀⠀⠀⠀⠀"); + }, + _ => { + w.push_str(b); + w.push_str("⠀⠀⠀⠀⠀⠀⢺⣿⠋⠀⠈⢿⣿⣆⠀⠀⠀"); + w.push_str(cy); + w.push_str("⠙⣿⡗⠀⠀⠀⠀⠀⠀"); + }, + } + w.push_str(c.reset); +} - let mut buf = [0u8; 2560]; - let mut w = StackWriter::new(&mut buf); +// Info row labels +struct RowLabel { + icon: &'static str, + key: &'static str, + spacing: &'static str, +} - if CUSTOM_LOGO.is_empty() { - // Default two-tone NixOS logo rendered as a single write! pass. - core::fmt::write( - &mut w, - format_args!( - "\n {b}⠀⠀⠀⠀⠀⠀⢼⣿⣄⠀⠀⠀{cy}⠹⣿⣷⡀⠀⣠⣿⡧⠀⠀⠀⠀⠀⠀{rs} {user_info} ~{rs}\ - \n {b}⠀⠀⠀⠀⠀⠀⠈⢿⣿⣆⠀⠀⠀{cy}⠘⣿⣿⣴⣿⡿⠁⠀⠀⠀⠀⠀⠀{rs} {cy}\u{F313} {b}System{rs} \u{E621} {os_name}\ - \n {b}⠀⠀⠀⢠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡜{cy}⢿⣿⣟⠀⠀⠀{b}⢀⡄⠀⠀⠀{rs} {cy}\u{E712} {b}Kernel{rs} \u{E621} {kernel_version}\ - \n {b}⠀⠀⠀⠉⠉⠉⠉{cy}⣩⣭⡭{b}⠉⠉⠉⠉⠉{cy}⠈⢿⣿⣆⠀{b}⢠⣿⣿⠂⠀⠀{rs} {cy}\u{F2DB} {b}CPU{rs} \u{E621} {cpu_name}\ - \n {cy}⠀⠀⠀⠀⠀⠀⣼⣿⡟⠀⠀⠀⠀⠀⠀⠀⠀⢻⡟{b}⣡⣿⣿⠃⠀⠀⠀{rs} {cy}\u{F4BC} {b}Topology{rs} \u{E621} {cpu_cores}\ - \n {cy}⢸⣿⣿⣿⣿⣿⣿⠏⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀{b}⣰⣿⣿⣿⣿⣿⣿⡇{rs} {cy}\u{E795} {b}Shell{rs} \u{E621} {shell}\ - \n {cy}⠀⠀⠀⢠⣿⣿⢋{b}⣼⣧⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⡟⠀⠀⠀⠀⠀⠀{rs} {cy}\u{F017} {b}Uptime{rs} \u{E621} {uptime}\ - \n {cy}⠀⠀⠠⣿⣿⠃⠀{b}⠹⣿⣷⡀{cy}⣀⣀⣀⣀⣀{b}⣚⣛⣋{cy}⣀⣀⣀⣀⠀⠀⠀{rs} {cy}\u{F2D2} {b}Desktop{rs} \u{E621} {desktop}\ - \n {cy}⠀⠀⠀⠘⠁⠀⠀⠀{b}⣽⣿⣷⡜{cy}⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃⠀⠀⠀{rs} {cy}\u{F035B} {b}Memory{rs} \u{E621} {memory_usage}\ - \n {b}⠀⠀⠀⠀⠀⠀⢀⣾⣿⠟⣿⣿⡄⠀⠀⠀{cy}⠹⣿⣷⡀⠀⠀⠀⠀⠀⠀{rs} {cy}\u{F194E} {b}Storage (/){rs} \u{E621} {storage}\ - \n {b}⠀⠀⠀⠀⠀⠀⢺⣿⠋⠀⠈⢿⣿⣆⠀⠀⠀{cy}⠙⣿⡗⠀⠀⠀⠀⠀⠀{rs} {cy}\u{E22B} {b}Colors{rs} \u{E621} {colors}\n\n", - b = c.blue, - cy = c.cyan, - rs = c.reset, - user_info = user_info, - os_name = os_name, - kernel_version = kernel_version, - cpu_name = cpu_name, - cpu_cores = cpu_cores, - shell = shell, - uptime = uptime, - desktop = desktop, - memory_usage = memory_usage, - storage = storage, - colors = colors, - ), - ) - .ok(); +const ROW_LABELS: [Option; 11] = [ + None, // row 0: user@host + Some(RowLabel { + icon: "\u{F313} ", + key: "System", + spacing: " \u{E621} ", + }), + Some(RowLabel { + icon: "\u{E712} ", + key: "Kernel", + spacing: " \u{E621} ", + }), + Some(RowLabel { + icon: "\u{F2DB} ", + key: "CPU", + spacing: " \u{E621} ", + }), + Some(RowLabel { + icon: "\u{F4BC} ", + key: "Topology", + spacing: " \u{E621} ", + }), + Some(RowLabel { + icon: "\u{E795} ", + key: "Shell", + spacing: " \u{E621} ", + }), + Some(RowLabel { + icon: "\u{F017} ", + key: "Uptime", + spacing: " \u{E621} ", + }), + Some(RowLabel { + icon: "\u{F2D2} ", + key: "Desktop", + spacing: " \u{E621} ", + }), + Some(RowLabel { + icon: "\u{F035B} ", + key: "Memory", + spacing: " \u{E621} ", + }), + Some(RowLabel { + icon: "\u{F194E} ", + key: "Storage (/)", + spacing: " \u{E621} ", + }), + Some(RowLabel { + icon: "\u{E22B} ", + key: "Colors", + spacing: " \u{E621} ", + }), +]; + +/// Write one row: logo + label + value. +#[inline] +#[allow(clippy::ref_option)] +fn write_row( + w: &mut StackWriter, + c: &colors::Colors, + row: usize, + custom_logo: &str, + use_custom: bool, + label: &Option, + write_value: impl FnOnce(&mut StackWriter), + suffix: &str, +) { + w.push_str(" "); + if use_custom { + w.push_str(c.cyan); + w.push_str(custom_logo); + w.push_str(c.reset); } else { - // Custom logo is 11 lines from MICROFETCH_LOGO env var, one per info row. - // Lines beyond 11 are ignored; missing lines render as empty. - let mut lines = CUSTOM_LOGO.split('\n'); - let logo_rows: [&str; 11] = - core::array::from_fn(|_| lines.next().unwrap_or("")); - - // Row format mirrors the default logo path exactly. - let rows: [(&str, &str, &str, &str, &str); 11] = [ - ("", "", user_info.as_str(), " ", " ~"), - ( - "\u{F313} ", - "System", - os_name.as_str(), - " \u{E621} ", - "", - ), - ( - "\u{E712} ", - "Kernel", - kernel_version.as_str(), - " \u{E621} ", - "", - ), - ( - "\u{F2DB} ", - "CPU", - cpu_name.as_str(), - " \u{E621} ", - "", - ), - ( - "\u{F4BC} ", - "Topology", - cpu_cores.as_str(), - " \u{E621} ", - "", - ), - ( - "\u{E795} ", - "Shell", - shell.as_str(), - " \u{E621} ", - "", - ), - ( - "\u{F017} ", - "Uptime", - uptime.as_str(), - " \u{E621} ", - "", - ), - ( - "\u{F2D2} ", - "Desktop", - desktop.as_str(), - " \u{E621} ", - "", - ), - ( - "\u{F035B} ", - "Memory", - memory_usage.as_str(), - " \u{E621} ", - "", - ), - ( - "\u{F194E} ", - "Storage (/)", - storage.as_str(), - " \u{E621} ", - "", - ), - ( - "\u{E22B} ", - "Colors", - colors.as_str(), - " \u{E621} ", - "", - ), - ]; - - core::fmt::write(&mut w, format_args!("\n")).ok(); - for i in 0..11 { - let (icon, key, value, spacing, suffix) = rows[i]; - if key.is_empty() { - // Row 1 has no icon/key, just logo + user_info - core::fmt::write( - &mut w, - format_args!( - " {cy}{logo}{rs} {value}{suffix}\n", - cy = c.cyan, - rs = c.reset, - logo = logo_rows[i], - value = value, - suffix = suffix, - ), - ) - .ok(); - } else { - core::fmt::write( - &mut w, - format_args!( - " {cy}{logo}{rs} \ - {cy}{icon}{b}{key}{rs}{spacing}{value}{suffix}\n", - cy = c.cyan, - b = c.blue, - rs = c.reset, - logo = logo_rows[i], - icon = icon, - key = key, - spacing = spacing, - value = value, - suffix = suffix, - ), - ) - .ok(); - } - } - core::fmt::write(&mut w, format_args!("\n")).ok(); + write_logo(w, c, row); } - - // Single syscall for the entire output. - let out = w.written(); - let written = unsafe { sys_write(1, out.as_ptr(), out.len()) }; - if written < 0 { - #[allow(clippy::cast_possible_truncation)] - return Err(Error::OsError(written as i32)); + w.push_str(" "); + if let Some(l) = label { + w.push_str(c.cyan); + w.push_str(l.icon); + w.push_str(c.blue); + w.push_str(l.key); + w.push_str(c.reset); + w.push_str(l.spacing); } - - #[allow(clippy::cast_sign_loss)] - if written as usize != out.len() { - return Err(Error::WriteError); - } - - Ok(()) + write_value(w); + w.push_str(suffix); + w.push_byte(b'\n'); } /// Print version information using direct syscall. @@ -587,20 +519,135 @@ pub unsafe fn run(argc: i32, argv: *const *const u8) -> Result<(), Error> { } 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), - cpu_name: cpu::get_cpu_name(), - cpu_cores: cpu::get_cpu_cores()?, - 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(), + let no_color = colors::is_no_color(); + let c = colors::Colors::new(no_color); + + let mut buf = [0u8; 2560]; + let mut w = StackWriter::new(&mut buf); + + // Custom logo is 11 lines from MICROFETCH_LOGO env var, one per info row. + // Lines beyond 11 are ignored; missing lines render as empty. + let custom_lines: [&str; 11]; + let use_custom = !CUSTOM_LOGO.is_empty(); + let logo_lines = if use_custom { + let mut lines_iter = CUSTOM_LOGO.split('\n'); + custom_lines = core::array::from_fn(|_| lines_iter.next().unwrap_or("")); + &custom_lines + } else { + &[""; 11] // unused, we use LOGO pairs below }; - print_system_info(&fields)?; + + w.push_byte(b'\n'); + + macro_rules! row { + ($idx:expr, $write_value:expr, $suffix:expr) => { + write_row( + &mut w, + &c, + $idx, + if use_custom { logo_lines[$idx] } else { "" }, + use_custom, + &ROW_LABELS[$idx], + $write_value, + $suffix, + ); + }; + } + + row!( + 0, + |w: &mut StackWriter| { + system::write_username_and_hostname(w, &c, &utsname); + w.push_str(" ~"); + w.push_str(c.reset); + }, + "" + ); + row!( + 1, + |w: &mut StackWriter| { + let _ = release::write_os_pretty_name(w); + }, + "" + ); + row!( + 2, + |w: &mut StackWriter| { + release::write_system_info(w, &utsname); + }, + "" + ); + row!( + 3, + |w: &mut StackWriter| { + cpu::write_cpu_name(w); + }, + "" + ); + row!( + 4, + |w: &mut StackWriter| { + let _ = cpu::write_cpu_cores(w); + }, + "" + ); + row!( + 5, + |w: &mut StackWriter| { + system::write_shell(w); + }, + "" + ); + row!( + 6, + |w: &mut StackWriter| { + let _ = uptime::write_uptime(w); + }, + "" + ); + row!( + 7, + |w: &mut StackWriter| { + desktop::write_desktop_info(w); + }, + "" + ); + row!( + 8, + |w: &mut StackWriter| { + let _ = system::write_memory_usage(w, &c); + }, + "" + ); + row!( + 9, + |w: &mut StackWriter| { + let _ = system::write_root_disk_usage(w, &c); + }, + "" + ); + row!( + 10, + |w: &mut StackWriter| { + colors::write_dots(w, &c); + }, + "" + ); + + w.push_byte(b'\n'); + + // Single syscall for the entire output. + let out = w.written(); + let written = unsafe { sys_write(1, out.as_ptr(), out.len()) }; + if written < 0 { + #[allow(clippy::cast_possible_truncation)] + return Err(Error::OsError(written as i32)); + } + + #[allow(clippy::cast_sign_loss)] + if written as usize != out.len() { + return Err(Error::WriteError); + } Ok(()) } diff --git a/crates/lib/src/release.rs b/crates/lib/src/release.rs index 881e73c..cdcef26 100644 --- a/crates/lib/src/release.rs +++ b/crates/lib/src/release.rs @@ -1,63 +1,48 @@ -use alloc::string::String; - #[cfg(target_os = "linux")] use crate::syscall::read_file_fast; -use crate::{Error, UtsName}; +use crate::{Error, StackWriter, UtsName}; -#[must_use] #[cfg_attr(feature = "hotpath", hotpath::measure)] -pub fn get_system_info(utsname: &UtsName) -> String { - let sysname = utsname.sysname().to_str().unwrap_or("Unknown"); - let release = utsname.release().to_str().unwrap_or("Unknown"); - let machine = utsname.machine().to_str().unwrap_or("Unknown"); - - // Pre-allocate capacity: sysname + " " + release + " (" + machine + ")" - let capacity = sysname.len() + 1 + release.len() + 2 + machine.len() + 1; - let mut result = String::with_capacity(capacity); - - // 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 +pub fn write_system_info(w: &mut StackWriter, utsname: &UtsName) { + w.push_cstr(utsname.sysname()); + w.push_byte(b' '); + w.push_cstr(utsname.release()); + w.push_str(" ("); + w.push_cstr(utsname.machine()); + w.push_byte(b')'); } -/// Gets the pretty name of the OS via `kern.osproductversion` (macOS), -/// e.g. `macOS 14.5`. +/// Writes the OS name from `kern.osproductversion` (macOS), e.g. `macOS 14.5`. /// /// # Errors /// /// Never errors; falls back to `macOS` if the version sysctl is unavailable. #[cfg(target_os = "macos")] #[cfg_attr(feature = "hotpath", hotpath::measure)] -pub fn get_os_pretty_name() -> Result { - let mut name = String::from("macOS"); +pub fn write_os_pretty_name(w: &mut StackWriter) -> Result<(), Error> { + w.push_str("macOS"); let mut buf = [0u8; 64]; if let Some(n) = crate::syscall::macos_sysctl_str(b"kern.osproductversion\0", &mut buf) { - if let Ok(ver) = core::str::from_utf8(&buf[..n]) { - if !ver.is_empty() { - name.push(' '); - name.push_str(ver); + if let Ok(version) = core::str::from_utf8(&buf[..n]) { + if !version.is_empty() { + w.push_byte(b' '); + w.push_str(version); } } } - Ok(name) + Ok(()) } -/// Gets the pretty name of the OS from `/etc/os-release`. +/// Writes the pretty name of the OS from `/etc/os-release`. /// /// # Errors /// /// Returns an error if `/etc/os-release` cannot be read. #[cfg(target_os = "linux")] #[cfg_attr(feature = "hotpath", hotpath::measure)] -pub fn get_os_pretty_name() -> Result { +pub fn write_os_pretty_name(w: &mut StackWriter) -> Result<(), Error> { // Fast byte-level scanning for PRETTY_NAME= const PREFIX: &[u8] = b"PRETTY_NAME="; @@ -67,7 +52,6 @@ pub fn get_os_pretty_name() -> Result { let bytes_read = read_file_fast("/etc/os-release", &mut buffer) .map_err(Error::from_raw_os_error)?; let content = &buffer[..bytes_read]; - let mut offset = 0; while offset < content.len() { @@ -93,12 +77,16 @@ pub fn get_os_pretty_name() -> Result { value }; - // Convert to String - should be valid UTF-8 - return Ok(String::from_utf8_lossy(trimmed).into_owned()); + // Write bytes directly — os-release values are effectively always + // valid UTF-8 (ASCII names, version numbers). No lossy conversion + // needed. + w.push_bytes(trimmed); + return Ok(()); } offset += line_end + 1; } - Ok(String::from("Unknown")) + w.push_str("Unknown"); + Ok(()) } diff --git a/crates/lib/src/system.rs b/crates/lib/src/system.rs index f69dd23..6bf431d 100644 --- a/crates/lib/src/system.rs +++ b/crates/lib/src/system.rs @@ -1,54 +1,41 @@ -use alloc::string::String; use core::mem::MaybeUninit; #[cfg(target_os = "linux")] use crate::syscall::read_file_fast; use crate::{ Error, + StackWriter, UtsName, colors::Colors, syscall::{StatfsBuf, sys_statfs}, }; -#[must_use] #[cfg_attr(feature = "hotpath", hotpath::measure)] -pub fn get_username_and_hostname(utsname: &UtsName) -> String { +pub fn write_username_and_hostname( + w: &mut StackWriter, + colors: &Colors, + utsname: &UtsName, +) { let username = crate::getenv_str("USER").unwrap_or("unknown_user"); - let hostname = utsname.nodename().to_str().unwrap_or("unknown_host"); - - // 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() - + 1 - + colors.green.len() - + hostname.len() - + colors.reset.len(); - let mut result = String::with_capacity(capacity); - - result.push_str(colors.yellow); - result.push_str(username); - result.push_str(colors.red); - result.push('@'); - result.push_str(colors.green); - result.push_str(hostname); - result.push_str(colors.reset); - - result + let hostname = utsname.nodename(); + + w.push_str(colors.yellow); + w.push_str(username); + w.push_str(colors.red); + w.push_byte(b'@'); + w.push_str(colors.green); + w.push_cstr(hostname); + w.push_str(colors.reset); } -#[must_use] #[cfg_attr(feature = "hotpath", hotpath::measure)] -pub fn get_shell() -> String { +pub fn write_shell(w: &mut StackWriter) { let shell = crate::getenv_str("SHELL").unwrap_or(""); - let start = shell.rfind('/').map_or(0, |i| i + 1); if shell.is_empty() { - String::from("unknown_shell") + w.push_str("unknown_shell"); } else { - String::from(&shell[start..]) + let start = shell.rfind('/').map_or(0, |i| i + 1); + w.push_str(&shell[start..]); } } @@ -58,8 +45,10 @@ 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 write_root_disk_usage( + w: &mut StackWriter, + colors: &Colors, +) -> Result<(), Error> { let mut vfs = MaybeUninit::::uninit(); let path = b"/\0"; @@ -73,101 +62,42 @@ pub fn get_root_disk_usage() -> Result { let total_blocks = vfs.f_blocks; let available_blocks = vfs.f_bavail; - let total_size = block_size * total_blocks; - let used_size = total_size - (block_size * available_blocks); + let total_bytes = block_size * total_blocks; + let used_bytes = total_bytes - (block_size * available_blocks); - let total_size = total_size as f64 / (1024.0 * 1024.0 * 1024.0); - 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); - - // 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_gib(w, used_bytes); + w.push_str(" GiB / "); + write_gib(w, total_bytes); + w.push_str(" GiB ("); + w.push_str(colors.cyan); + let pct = if total_bytes > 0 { + used_bytes * 100 / total_bytes + } else { + 0 + }; + w.push_u64(pct); + w.push_byte(b'%'); + w.push_str(colors.reset); + w.push_byte(b')'); + + Ok(()) } -/// 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); - } - } +fn write_centi_gib(w: &mut StackWriter, centi_gib: u64) { + w.push_u64(centi_gib / 100); + w.push_byte(b'.'); + let frac = (centi_gib % 100) as u8; + w.push_byte(b'0' + frac / 10); + w.push_byte(b'0' + frac % 10); } -/// 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 - } - } +fn write_gib(w: &mut StackWriter, bytes: u64) { + write_centi_gib(w, (bytes * 100 + (1 << 29)) >> 30); } -/// Write a u64 to string -pub 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..]) }); +#[cfg(target_os = "linux")] +fn write_kb_as_gib(w: &mut StackWriter, kb: u64) { + write_centi_gib(w, (kb * 100 + (1 << 19)) >> 20); } /// Fast integer parsing without stdlib overhead @@ -185,30 +115,36 @@ fn parse_u64_fast(s: &[u8]) -> u64 { result } -/// Gets the system memory usage information via `sysctl`/Mach (macOS). +/// Writes the system memory usage information via `sysctl`/Mach (macOS). /// /// # Errors /// /// Returns an error if the memory statistics cannot be retrieved. #[cfg(target_os = "macos")] #[cfg_attr(feature = "hotpath", hotpath::measure)] -pub fn get_memory_usage() -> Result { +pub fn write_memory_usage( + w: &mut StackWriter, + colors: &Colors, +) -> Result<(), Error> { let (used_bytes, total_bytes) = crate::syscall::macos_meminfo().ok_or(Error::OsError(0))?; - - const GIB: f64 = 1024.0 * 1024.0 * 1024.0; - #[expect( - clippy::cast_precision_loss, - reason = "GiB display tolerates f64 rounding" - )] - let used_memory = used_bytes as f64 / GIB; - #[expect( - clippy::cast_precision_loss, - reason = "GiB display tolerates f64 rounding" - )] - let total_memory = total_bytes as f64 / GIB; - - Ok(format_memory(used_memory, total_memory)) + let percentage_used = if total_bytes > 0 { + (used_bytes * 100 + total_bytes / 2) / total_bytes + } else { + 0 + }; + + write_gib(w, used_bytes); + w.push_str(" GiB / "); + write_gib(w, total_bytes); + w.push_str(" GiB ("); + w.push_str(colors.cyan); + w.push_u64(percentage_used); + w.push_byte(b'%'); + w.push_str(colors.reset); + w.push_byte(b')'); + + Ok(()) } /// Gets the system memory usage information. @@ -218,9 +154,12 @@ pub fn get_memory_usage() -> Result { /// Returns an error if `/proc/meminfo` cannot be read. #[cfg(target_os = "linux")] #[cfg_attr(feature = "hotpath", hotpath::measure)] -pub fn get_memory_usage() -> Result { +pub fn write_memory_usage( + w: &mut StackWriter, + colors: &Colors, +) -> Result<(), Error> { #[cfg_attr(feature = "hotpath", hotpath::measure)] - fn parse_memory_info() -> Result<(f64, f64), Error> { + fn parse_memory_info() -> Result<(u64, u64), Error> { let mut total_memory_kb = 0u64; let mut available_memory_kb = 0u64; let mut buffer = [0u8; 1024]; @@ -266,38 +205,25 @@ pub fn get_memory_usage() -> Result { offset += line_end + 1; } - #[allow(clippy::cast_precision_loss)] - let total_gb = total_memory_kb as f64 / 1024.0 / 1024.0; - #[allow(clippy::cast_precision_loss)] - 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_gb)) + Ok((total_memory_kb - available_memory_kb, total_memory_kb)) } - let (used_memory, total_memory) = parse_memory_info()?; - Ok(format_memory(used_memory, total_memory)) -} - -/// Formats `used`/`total` memory (both in GiB) into the display string. -fn format_memory(used_memory: f64, total_memory: f64) -> String { - #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] - 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_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(')'); - - result + let (used_kb, total_kb) = parse_memory_info()?; + let percentage_used = if total_kb > 0 { + (used_kb * 100 + total_kb / 2) / total_kb + } else { + 0 + }; + + write_kb_as_gib(w, used_kb); + w.push_str(" GiB / "); + write_kb_as_gib(w, total_kb); + w.push_str(" GiB ("); + w.push_str(colors.cyan); + w.push_u64(percentage_used); + w.push_byte(b'%'); + w.push_str(colors.reset); + w.push_byte(b')'); + + Ok(()) } diff --git a/crates/lib/src/uptime.rs b/crates/lib/src/uptime.rs index 1b503d4..55ced5f 100644 --- a/crates/lib/src/uptime.rs +++ b/crates/lib/src/uptime.rs @@ -1,38 +1,19 @@ -use alloc::string::String; +#[cfg(target_os = "linux")] use core::mem::MaybeUninit; -use crate::Error; - -/// Faster integer to string conversion without the formatting overhead. -#[inline] -fn itoa(mut n: u64, buf: &mut [u8]) -> &str { - if n == 0 { - return "0"; - } - - let mut i = buf.len(); - while n > 0 { - i -= 1; - buf[i] = b'0' + (n % 10) as u8; - n /= 10; - } - - // SAFETY: We only wrote ASCII digits - unsafe { core::str::from_utf8_unchecked(&buf[i..]) } -} +#[cfg(target_os = "linux")] use crate::syscall::sys_sysinfo; +use crate::{Error, StackWriter}; /// Gets the current system uptime. /// /// # Errors /// /// Returns an error if the system uptime cannot be retrieved. -#[cfg(target_os = "linux")] #[cfg_attr(feature = "hotpath", hotpath::measure)] -pub fn get_current() -> Result { - use core::mem::MaybeUninit; - +pub fn write_uptime(w: &mut StackWriter) -> Result<(), Error> { + #[cfg(target_os = "linux")] let uptime_seconds = { let mut info = MaybeUninit::uninit(); - if unsafe { crate::syscall::sys_sysinfo(info.as_mut_ptr()) } != 0 { + if unsafe { sys_sysinfo(info.as_mut_ptr()) } != 0 { return Err(Error::last_os_error()); } #[allow(clippy::cast_sign_loss)] @@ -40,53 +21,39 @@ pub fn get_current() -> Result { info.assume_init().uptime as u64 } }; - - Ok(format_uptime(uptime_seconds)) -} - -/// Gets the current system uptime via `kern.boottime` (macOS). -/// -/// # Errors -/// -/// Returns an error if the uptime cannot be retrieved. -#[cfg(target_os = "macos")] -#[cfg_attr(feature = "hotpath", hotpath::measure)] -pub fn get_current() -> Result { + #[cfg(target_os = "macos")] let uptime_seconds = crate::syscall::macos_uptime_secs().ok_or(Error::OsError(0))?; - Ok(format_uptime(uptime_seconds)) -} -/// Formats a duration in seconds as a human-readable uptime string. -fn format_uptime(uptime_seconds: u64) -> String { let days = uptime_seconds / 86400; let hours = (uptime_seconds / 3600) % 24; let minutes = (uptime_seconds / 60) % 60; - - let mut result = String::with_capacity(32); - let mut buf = [0u8; 20]; // Enough for u64::MAX + let mut any = false; if days > 0 { - result.push_str(itoa(days, &mut buf)); - result.push_str(if days == 1 { " day" } else { " days" }); + w.push_u64(days); + w.push_str(if days == 1 { " day" } else { " days" }); + any = true; } if hours > 0 { - if !result.is_empty() { - result.push_str(", "); + if any { + w.push_str(", "); } - result.push_str(itoa(hours, &mut buf)); - result.push_str(if hours == 1 { " hour" } else { " hours" }); + w.push_u64(hours); + w.push_str(if hours == 1 { " hour" } else { " hours" }); + any = true; } if minutes > 0 { - if !result.is_empty() { - result.push_str(", "); + if any { + w.push_str(", "); } - result.push_str(itoa(minutes, &mut buf)); - result.push_str(if minutes == 1 { " minute" } else { " minutes" }); + w.push_u64(minutes); + w.push_str(if minutes == 1 { " minute" } else { " minutes" }); + any = true; } - if result.is_empty() { - result.push_str("less than a minute"); + if !any { + w.push_str("less than a minute"); } - result + Ok(()) } diff --git a/microfetch/Cargo.toml b/microfetch/Cargo.toml index 6d8f422..ba38dbe 100644 --- a/microfetch/Cargo.toml +++ b/microfetch/Cargo.toml @@ -15,7 +15,6 @@ test = false [dependencies] hotpath = { optional = true, version = "0.21.2" } -microfetch-alloc.workspace = true microfetch-asm.workspace = true microfetch-lib.workspace = true diff --git a/microfetch/src/main.rs b/microfetch/src/main.rs index cdd5793..1641e20 100644 --- a/microfetch/src/main.rs +++ b/microfetch/src/main.rs @@ -10,20 +10,16 @@ feature(asm_experimental_arch) )] -extern crate alloc; - #[cfg(not(target_os = "macos"))] use core::arch::naked_asm; -use core::panic::PanicInfo; -use microfetch_alloc::BumpAllocator; // The custom `_start` (and the `entry_rust` it calls) is Linux-only. #[cfg(not(target_os = "macos"))] use microfetch_asm::entry_rust; +use microfetch_asm::sys_write; // Re-export libc replacement functions from asm crate. On macOS these are // provided by libSystem, so we don't define (or re-export) our own. #[cfg(not(target_os = "macos"))] pub use microfetch_asm::{memcpy, memset, strlen}; -use microfetch_asm::{sys_exit, sys_write}; #[cfg(target_arch = "x86_64")] #[unsafe(no_mangle)] @@ -303,10 +299,6 @@ unsafe extern "C" fn _start() { ); } -// Global allocator -#[global_allocator] -static ALLOCATOR: BumpAllocator = BumpAllocator::new(); - /// Main application entry point. Called by the asm crate's entry point /// after setting up argc, argv, and envp. /// @@ -328,9 +320,9 @@ pub unsafe extern "C" fn main(argc: i32, argv: *const *const u8) -> i32 { // Run the main application logic match unsafe { microfetch_lib::run(argc, argv) } { Ok(()) => 0, - Err(e) => { - let msg = alloc::format!("Error: {e}\n"); - let _ = unsafe { sys_write(2, msg.as_ptr(), msg.len()) }; + Err(_) => { + const ERR_MSG: &[u8] = b"Error\n"; + let _ = unsafe { sys_write(2, ERR_MSG.as_ptr(), ERR_MSG.len()) }; 1 }, } @@ -338,11 +330,11 @@ pub unsafe extern "C" fn main(argc: i32, argv: *const *const u8) -> i32 { #[cfg(not(test))] #[panic_handler] -fn panic(_info: &PanicInfo) -> ! { +fn panic(_info: &core::panic::PanicInfo) -> ! { const PANIC_MSG: &[u8] = b"panic\n"; unsafe { let _ = sys_write(2, PANIC_MSG.as_ptr(), PANIC_MSG.len()); - sys_exit(1) + microfetch_asm::sys_exit(1) } } @@ -354,7 +346,7 @@ const extern "C" fn rust_eh_personality() {} #[cfg(not(test))] #[unsafe(no_mangle)] extern "C" fn _Unwind_Resume() -> ! { - unsafe { sys_exit(1) } + unsafe { microfetch_asm::sys_exit(1) } } // compiler_builtins emits `.ARM.exidx` entries that reference these even