From 98c6d20586a1ba29137d583c2dc801fcb708b062 Mon Sep 17 00:00:00 2001 From: Yang Hau Date: Fri, 2 Aug 2024 21:47:29 +0800 Subject: [PATCH] lsmem: Impl lsmem --- Cargo.lock | 144 +++++++++- Cargo.toml | 4 +- src/uu/lsmem/Cargo.toml | 3 + src/uu/lsmem/src/lsmem.rs | 537 +++++++++++++++++++++++++++++++++++- src/uu/lsmem/src/utils.rs | 79 ++++++ tests/by-util/test_lsmem.rs | 8 + tests/tests.rs | 4 + 7 files changed, 775 insertions(+), 4 deletions(-) create mode 100644 src/uu/lsmem/src/utils.rs diff --git a/Cargo.lock b/Cargo.lock index 07d2f4e..a8b437b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,6 +71,12 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +[[package]] +name = "bytecount" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" + [[package]] name = "cfg-if" version = "1.0.0" @@ -195,6 +201,12 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "getrandom" version = "0.2.12" @@ -212,6 +224,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "hermit-abi" version = "0.3.4" @@ -235,6 +253,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + [[package]] name = "lazy_static" version = "1.4.0" @@ -307,6 +331,17 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "papergrid" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7419ad52a7de9b60d33e11085a0fe3df1fbd5926aa3f93d3dd53afbc9e86725" +dependencies = [ + "bytecount", + "fnv", + "unicode-width", +] + [[package]] name = "phf" version = "0.11.2" @@ -361,6 +396,30 @@ dependencies = [ "yansi", ] +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.78" @@ -523,6 +582,44 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "serde" +version = "1.0.205" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e33aedb1a7135da52b7c21791455563facbbcc43d0f0f66165b42c21b3dfb150" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.205" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692d6f5ac90220161d6774db30c662202721e64aed9058d2c394f451261420c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.58", +] + +[[package]] +name = "serde_json" +version = "1.0.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -541,6 +638,17 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.58" @@ -566,6 +674,29 @@ dependencies = [ "windows", ] +[[package]] +name = "tabled" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c9303ee60b9bedf722012ea29ae3711ba13a67c9b9ae28993838b63057cb1b" +dependencies = [ + "papergrid", + "tabled_derive", +] + +[[package]] +name = "tabled_derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf0fb8bfdc709786c154e24a66777493fb63ae97e3036d914c8666774c477069" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "tempfile" version = "3.10.1" @@ -683,6 +814,9 @@ name = "uu_lsmem" version = "0.0.1" dependencies = [ "clap", + "serde", + "serde_json", + "tabled", "uucore", ] @@ -736,6 +870,12 @@ version = "0.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d841f8408028085ca65896cdd60b9925d4e407cb69989a64889f2bebbb51147b" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -803,7 +943,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.58", ] [[package]] @@ -814,7 +954,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.58", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 0e685f5..984008e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,9 @@ textwrap = { version = "0.16.0", features = ["terminal_size"] } xattr = "1.3.1" tempfile = "3.9.0" rand = { version = "0.8", features = ["small_rng"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0.122" +tabled = "0.16.0" [dependencies] clap = { workspace = true } @@ -56,7 +59,6 @@ uucore = { workspace = true } phf = { workspace = true } textwrap = { workspace = true } - # lscpu = { optional = true, version = "0.0.1", package = "uu_lscpu", path = "src/uu/lscpu" } lsmem = { optional = true, version = "0.0.1", package = "uu_lsmem", path = "src/uu/lsmem" } diff --git a/src/uu/lsmem/Cargo.toml b/src/uu/lsmem/Cargo.toml index a76660a..516aa1d 100644 --- a/src/uu/lsmem/Cargo.toml +++ b/src/uu/lsmem/Cargo.toml @@ -13,3 +13,6 @@ path = "src/main.rs" [dependencies] uucore = { workspace = true } clap = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tabled = { workspace = true } diff --git a/src/uu/lsmem/src/lsmem.rs b/src/uu/lsmem/src/lsmem.rs index d8f3388..500608b 100644 --- a/src/uu/lsmem/src/lsmem.rs +++ b/src/uu/lsmem/src/lsmem.rs @@ -3,15 +3,543 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +mod utils; + use clap::{crate_version, Command}; +use clap::{Arg, ArgAction}; +use serde::Deserialize; +use std::borrow::Borrow; +use std::fs; +use std::io::{self, BufRead, BufReader}; +use std::path::{Path, PathBuf}; +use std::str::FromStr; use uucore::{error::UResult, format_usage, help_about, help_usage}; +use tabled::{ + settings::{location::ByColumnName, object, Alignment, Disable, Modify, Style}, + Table, Tabled, +}; + const ABOUT: &str = help_about!("lsmem.md"); const USAGE: &str = help_usage!("lsmem.md"); +mod options { + pub const BYTES: &str = "bytes"; +} + +// const BUFSIZ: usize = 1024; + +const PATH_SYS_MEMORY: &str = "/sys/devices/system/memory"; +const PATH_BLOCK_SIZE_BYTES: &str = "/sys/devices/system/memory/block_size_bytes"; +const PATH_VALID_ZONES: &str = "/sys/devices/system/memory/valid_zones"; +const PATH_SUB_REMOVABLE: &str = "removable"; +const PATH_SUB_STATE: &str = "state"; +const NAME_MEMORY: &str = "memory"; + +// struct ColDesc { +// name: &'static str, // Rust's equivalent to `const char *` +// whint: f64, // Rust uses `f64` for double precision floating-point numbers +// flags: i32, // Using `i32` for integers +// help: &'static str, // Rust's equivalent to `const char *` +// } + +#[derive(Debug, Deserialize)] +enum Columns { + #[serde(rename = "RANGE")] + Range, + #[serde(rename = "SIZE")] + Size, + #[serde(rename = "STATE")] + State, + #[serde(rename = "REMOVABLE")] + Removable, + #[serde(rename = "BLOCK")] + Block, + #[serde(rename = "NODE")] + Node, + #[serde(rename = "ZONES")] + Zones, +} +// const SCOLS_FL_RIGHT: i32 = 1; + +// static COLDESCS: [ColDesc; 7] = [ +// ColDesc { +// name: "RANGE", +// whint: 0.0, +// flags: 0, +// help: "start and end address of the memory range", +// }, +// ColDesc { +// name: "SIZE", +// whint: 5.0, +// flags: SCOLS_FL_RIGHT, +// help: "size of the memory range", +// }, +// ColDesc { +// name: "STATE", +// whint: 0.0, +// flags: SCOLS_FL_RIGHT, +// help: "online status of the memory range", +// }, +// ColDesc { +// name: "REMOVABLE", +// whint: 0.0, +// flags: SCOLS_FL_RIGHT, +// help: "memory is removable", +// }, +// ColDesc { +// name: "BLOCK", +// whint: 0.0, +// flags: SCOLS_FL_RIGHT, +// help: "memory block number or blocks range", +// }, +// ColDesc { +// name: "NODE", +// whint: 0.0, +// flags: SCOLS_FL_RIGHT, +// help: "numa node of memory", +// }, +// ColDesc { +// name: "ZONES", +// whint: 0.0, +// flags: SCOLS_FL_RIGHT, +// help: "valid zones for the memory range", +// }, +// ]; + +#[derive(Debug, Deserialize, PartialEq, Clone, Copy)] +enum ZoneId { + #[serde(rename = "ZONE_DMA")] + ZoneDma, + #[serde(rename = "ZONE_DMA32")] + ZoneDma32, + #[serde(rename = "ZONE_NORMAL")] + ZoneNormal, + #[serde(rename = "ZONE_HIGHMEM")] + ZoneHighmem, + #[serde(rename = "ZONE_MOVABLE")] + ZoneMovable, + #[serde(rename = "ZONE_DEVICE")] + ZoneDevice, + #[serde(rename = "ZONE_NONE")] + ZoneNone, + #[serde(rename = "ZONE_UNKNOWN")] + ZoneUnknown, + #[serde(rename = "MAX_NR_ZONES")] + MaxNrZones, +} + +#[derive(PartialEq, Clone)] +enum MemoryState { + Online, + Offline, + GoingOffline, + Unknown, +} + +impl core::fmt::Display for MemoryState { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + match self { + MemoryState::Online => write!(f, "online"), + MemoryState::Offline => write!(f, "offline"), + MemoryState::GoingOffline => write!(f, "going-offline"), + MemoryState::Unknown => write!(f, "unknown"), + } + } +} + +impl FromStr for MemoryState { + type Err = (); + fn from_str(input: &str) -> Result { + match input { + "online" => Ok(MemoryState::Online), + "offline" => Ok(MemoryState::Offline), + "going-offline" => Ok(MemoryState::GoingOffline), + "unknown" => Ok(MemoryState::Unknown), + _ => Err(()), + } + } +} + +#[derive(Clone)] +struct MemoryBlock { + index: u64, + count: u64, + state: MemoryState, + node: i32, + nr_zones: usize, + zones: [ZoneId; ZoneId::MaxNrZones as usize], + removable: bool, +} + +impl MemoryBlock { + fn new() -> Self { + MemoryBlock { + index: 0, + count: 0, + state: MemoryState::Unknown, + node: 0, + nr_zones: 0, + zones: [ZoneId::ZoneUnknown; ZoneId::MaxNrZones as usize], + removable: true, + } + } +} + +#[derive(Tabled)] +struct TableRow { + #[tabled(rename = "RANGE")] + range: String, + #[tabled(rename = "SIZE")] + size: String, + #[tabled(rename = "STATE")] + state: String, + #[tabled(rename = "REMOVABLE")] + removable: String, + #[tabled(rename = "BLOCK")] + block: String, + #[tabled(rename = "NODE")] + node: String, + #[tabled(rename = "ZONES")] + zones: String, +} + +impl TableRow { + fn new() -> Self { + TableRow { + range: String::new(), + size: String::new(), + state: String::new(), + removable: String::new(), + block: String::new(), + node: String::new(), + zones: String::new(), + } + } +} + +struct Lsmem { + ndirs: usize, + dirs: Vec, + blocks: Vec, + nblocks: usize, + block_size: u64, + mem_online: u64, + mem_offline: u64, + + have_nodes: bool, + // raw: bool, + // export: bool, + // json: bool, + // noheadings: bool, + // summary: bool, + list_all: bool, + bytes: bool, + want_summary: bool, + want_table: bool, + split_by_node: bool, + split_by_state: bool, + split_by_removable: bool, + split_by_zones: bool, + have_zones: bool, + // table: Vec>, +} + +impl Lsmem { + fn new() -> Lsmem { + Lsmem { + ndirs: 0, + dirs: Vec::default(), + blocks: Vec::default(), + nblocks: 0, + block_size: 0, + mem_online: 0, + mem_offline: 0, + + have_nodes: false, + // raw: false, + // export: false, + // json: false, + // noheadings: false, + // summary: false, + list_all: false, + bytes: false, + want_summary: true, // default true + want_table: true, // default true + split_by_node: false, + split_by_state: false, + split_by_removable: false, + split_by_zones: false, + have_zones: false, + // table: Vec::default(), + } + } +} + +fn read_info(lsmem: &mut Lsmem) { + lsmem.block_size = u64::from_str_radix( + &read_file_content::(Path::new(PATH_BLOCK_SIZE_BYTES)).unwrap(), + 16, + ) + .unwrap(); + lsmem.dirs = get_block_paths(); + lsmem.dirs.sort_by(|a, b| { + let filename_a = a.to_str().unwrap().split('/').last().unwrap(); + let filename_b = b.to_str().unwrap().split('/').last().unwrap(); + let idx_a: u64 = filename_a[NAME_MEMORY.len()..].parse().unwrap(); + let idx_b: u64 = filename_b[NAME_MEMORY.len()..].parse().unwrap(); + idx_a.cmp(&idx_b) + }); + lsmem.ndirs = lsmem.dirs.len(); + for path in lsmem.dirs.iter() { + if memory_block_get_node(path).is_ok() { + lsmem.have_nodes = true; + } + + let mut p = path.clone(); + p.push("valid_zones"); + if fs::read_dir(p).is_ok() { + lsmem.have_zones = true; + } + + if lsmem.have_nodes && lsmem.have_zones { + break; + } + } + + for i in 0..lsmem.ndirs { + let blk = memory_block_read_attrs(lsmem, &lsmem.dirs[i]); + if blk.state == MemoryState::Online { + lsmem.mem_online += lsmem.block_size; + } else { + lsmem.mem_offline += lsmem.block_size; + } + if is_mergeable(lsmem, &blk) { + lsmem.blocks[lsmem.nblocks - 1].count += 1; + continue; + } + lsmem.nblocks += 1; + lsmem.blocks.push(blk.clone()); + } +} + +fn get_block_paths() -> Vec { + let mut paths = Vec::::new(); + for entry in fs::read_dir(PATH_SYS_MEMORY).unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + let filename = path.to_str().unwrap().split('/').last().unwrap(); + if path.is_dir() && filename.starts_with(NAME_MEMORY) { + paths.push(path); + } + } + paths +} + +fn is_mergeable(lsmem: &Lsmem, blk: &MemoryBlock) -> bool { + if lsmem.nblocks == 0 { + return false; + } + + let curr_block = &lsmem.blocks[lsmem.nblocks - 1]; + if lsmem.list_all { + return false; + } + if curr_block.index + curr_block.count != blk.index { + return false; + } + if lsmem.split_by_state && curr_block.state != blk.state { + return false; + } + if lsmem.split_by_removable && curr_block.removable != blk.removable { + return false; + } + if lsmem.split_by_node && lsmem.have_nodes && (curr_block.node != blk.node) { + return false; + } + if lsmem.split_by_zones && lsmem.have_zones { + if curr_block.nr_zones != blk.nr_zones { + return false; + } + + for i in 0..curr_block.nr_zones { + if curr_block.zones[i] == ZoneId::ZoneUnknown || curr_block.zones[i] != blk.zones[i] { + return false; + } + } + } + true +} + +fn memory_block_get_node(path: &PathBuf) -> Result::Err> { + for entry in fs::read_dir(path).unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + let filename = path.to_str().unwrap().split('/').last().unwrap(); + if path.is_dir() && filename.starts_with("node") { + return filename["node".len()..].parse(); + } + } + Ok(-1) +} + +fn memory_block_read_attrs(lsmem: &Lsmem, path: &PathBuf) -> MemoryBlock { + let mut blk = MemoryBlock::new(); + blk.count = 1; + blk.state = MemoryState::Unknown; + let filename = path.to_str().unwrap().split('/').last().unwrap(); + blk.index = filename[NAME_MEMORY.len()..].parse().unwrap(); + + let mut removable_path = path.clone(); + removable_path.push(PATH_SUB_REMOVABLE); + blk.removable = read_file_content::(&removable_path).is_ok(); + + let mut state_path = path.clone(); + state_path.push(PATH_SUB_STATE); + if let Ok(state_raw) = read_file_content::(&state_path) { + blk.state = MemoryState::from_str(&state_raw).unwrap(); + } + + if lsmem.have_nodes { + blk.node = memory_block_get_node(path).unwrap(); + } + + blk.nr_zones = 0; + if lsmem.have_zones { + if let Ok(raw_content) = read_file_content::(Path::new(PATH_VALID_ZONES)) { + let zone_toks = raw_content.split(' ').collect::>(); + for (i, zone_tok) in zone_toks + .iter() + .enumerate() + .take(std::cmp::min(zone_toks.len(), ZoneId::MaxNrZones as usize)) + { + blk.zones[i] = serde_json::from_str(zone_tok).unwrap(); + blk.nr_zones += 1; + } + } + } + blk +} + +fn set_summary(lsmem: &Lsmem) -> Vec { + let mut table = Vec::::new(); + + for i in 0..lsmem.nblocks { + let mut row = TableRow::new(); + + let blk = lsmem.blocks[i].borrow(); + + // Range + let start = blk.index * lsmem.block_size; + let size = blk.count * lsmem.block_size; + row.range = format!("0x{:016x}-0x{:016x}", start, start + size - 1); + + // Size + row.size = if lsmem.bytes { + format!("{}", blk.count * lsmem.block_size) + } else { + utils::size_to_human_string(blk.count * lsmem.block_size) + }; + + // State + row.state = match blk.state { + MemoryState::Online => MemoryState::Online.to_string(), + MemoryState::Offline => MemoryState::Offline.to_string(), + MemoryState::GoingOffline => MemoryState::GoingOffline.to_string(), + _ => "?".to_string(), + }; + + // Removable + row.removable = if blk.removable { + "yes".to_string() + } else { + "no".to_string() + }; + + // Block + row.block = if blk.count == 1 { + format!("{}", blk.index) + } else { + format!("{}-{}", blk.index, blk.index + blk.count - 1) + }; + + // Node + if lsmem.have_nodes { + row.node = format!("{}", blk.node); + } + + table.push(row); + } + table +} + +fn print_table(lsmem: &Lsmem) { + let table_row_vec: Vec = set_summary(lsmem); + let mut table = Table::new(table_row_vec); + table + .with(Style::blank()) + .with(Modify::new(object::Columns::new(1..)).with(Alignment::right())); + + // the default version + table.with(Disable::column(ByColumnName::new("NODE"))); + table.with(Disable::column(ByColumnName::new("ZONES"))); + + println!("{table}"); +} + +fn print_summary(lsmem: &Lsmem) { + if lsmem.bytes { + println!("{:<23} {:>15}", "Memory block, size:", lsmem.block_size); + println!("{:<23} {:>15}", "Total online memory:", lsmem.mem_online); + println!("{:<23} {:>15}", "Total offline memory:", lsmem.mem_offline); + } else { + println!( + "{:<23} {:>15}", + "Memory block size:", + utils::size_to_human_string(lsmem.block_size) + ); + println!( + "{:<23} {:>15}", + "Total online memory:", + utils::size_to_human_string(lsmem.mem_online) + ); + println!( + "{:<23} {:>15}", + "Total offline memory:", + utils::size_to_human_string(lsmem.mem_offline) + ); + } +} + +fn read_file_content(path: &Path) -> io::Result +where + T::Err: std::fmt::Debug, // Required to unwrap the result of T::from_str +{ + let file = fs::File::open(path)?; + let mut reader = BufReader::new(file); + let mut content = String::new(); + reader.read_line(&mut content)?; + Ok(content.trim().to_string().parse().unwrap()) +} + #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let _matches: clap::ArgMatches = uu_app().try_get_matches_from(args)?; + let matches: clap::ArgMatches = uu_app().try_get_matches_from(args)?; + let opt_bytes = matches.get_flag(options::BYTES); + + let mut lsmem = Lsmem::new(); + lsmem.bytes = opt_bytes; + + read_info(&mut lsmem); + + if lsmem.want_table { + print_table(&lsmem); + } + + if lsmem.want_summary { + print_summary(&lsmem); + } + Ok(()) } @@ -21,4 +549,11 @@ pub fn uu_app() -> Command { .about(ABOUT) .override_usage(format_usage(USAGE)) .infer_long_args(true) + .arg( + Arg::new(options::BYTES) + .short('b') + .long("bytes") + .help("print SIZE in bytes rather than in human readable format") + .action(ArgAction::SetTrue), + ) } diff --git a/src/uu/lsmem/src/utils.rs b/src/uu/lsmem/src/utils.rs new file mode 100644 index 0000000..044d48f --- /dev/null +++ b/src/uu/lsmem/src/utils.rs @@ -0,0 +1,79 @@ +// enum HumanStringSize { +// Suffix1Letter = 0, +// Suffix3Letter = (1 << 0), +// SuffixSpace = (1 << 1), +// Decimal2Digits = (1 << 2), +// } + +pub fn size_to_human_string(bytes: u64) -> String { + let mut buf = String::with_capacity(32); + let mut dec; + let mut frac; + let letters = "BKMGTPE"; + let mut suffix = String::with_capacity(4); + + let exp = get_exp(bytes); + let c = letters + .chars() + .nth(if exp != 0 { exp / 10 } else { 0 }) + .unwrap_or('B'); + dec = if exp != 0 { + bytes / (1_u64 << exp) + } else { + bytes + }; + frac = if exp != 0 { bytes % (1_u64 << exp) } else { 0 }; + + suffix.push(c); + + // Rounding logic + if frac != 0 { + if frac >= u64::MAX / 1000 { + frac = ((frac / 1024) * 1000) / (1 << (exp - 10)); + } else { + frac = (frac * 1000) / (1 << exp); + } + + // Round to 1 decimal place + frac = ((frac + 50) / 100) * 10; + + // Check for overflow due to rounding + if frac == 100 { + dec += 1; + frac = 0; + } + } + + // Format the result + if frac != 0 { + let decimal_point = "."; + buf = format!("{}{}{:02}", dec, decimal_point, frac); + if buf.ends_with('0') { + buf.pop(); // Remove extraneous zero + } + buf += &suffix; + } else { + buf += &format!("{dec}"); + buf += &suffix; + } + + buf +} + +fn get_exp(n: u64) -> usize { + let mut shft = 10; + while shft <= 60 { + if n < (1 << shft) { + break; + } + shft += 10; + } + shft - 10 +} + +#[test] +fn test_size_to_human_string() { + assert!("11.7K" == size_to_human_string(12000)); + assert!("11.4M" == size_to_human_string(12000000)); + assert!("11.2G" == size_to_human_string(12000000000)); +} diff --git a/tests/by-util/test_lsmem.rs b/tests/by-util/test_lsmem.rs index 33c603c..7ca9b77 100644 --- a/tests/by-util/test_lsmem.rs +++ b/tests/by-util/test_lsmem.rs @@ -2,3 +2,11 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. + +use crate::common::util::TestScenario; + +#[test] +#[ignore] +fn test_invalid_arg() { + new_ucmd!().arg("--definitely-invalid").fails().code_is(1); +} diff --git a/tests/tests.rs b/tests/tests.rs index 1862b23..435dc14 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -9,6 +9,10 @@ mod common; #[path = "by-util/test_lscpu.rs"] mod test_lscpu; +#[cfg(feature = "lsmem")] +#[path = "by-util/test_lsmem.rs"] +mod test_lsmem; + #[cfg(feature = "mountpoint")] #[path = "by-util/test_mountpoint.rs"] mod test_mountpoint;