diff --git a/Cargo.lock b/Cargo.lock index 8dd26aba..4421a024 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1616,6 +1616,7 @@ dependencies = [ "serde_json", "sysinfo", "uucore 0.2.2", + "uulinux", ] [[package]] @@ -1648,6 +1649,7 @@ dependencies = [ "serde", "serde_json", "uucore 0.2.2", + "uulinux", ] [[package]] @@ -1836,6 +1838,10 @@ dependencies = [ "rand 0.10.0", ] +[[package]] +name = "uulinux" +version = "0.0.1" + [[package]] name = "uutests" version = "0.7.0" diff --git a/Cargo.toml b/Cargo.toml index b05af895..eafb0650 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,6 +76,7 @@ tempfile = "3.9.0" textwrap = { version = "0.16.0", features = ["terminal_size"] } thiserror = "2.0" uucore = "0.2.2" +uulinux = { version = "0.0.1", path = "src/uulinux" } uuid = { version = "1.16.0", features = ["rng-rand"] } uutests = "0.7.0" windows = { version = "0.62.2" } diff --git a/src/uu/lscpu/Cargo.toml b/src/uu/lscpu/Cargo.toml index 2ece0525..66c455ce 100644 --- a/src/uu/lscpu/Cargo.toml +++ b/src/uu/lscpu/Cargo.toml @@ -14,6 +14,7 @@ path = "src/main.rs" regex = { workspace = true } sysinfo = { workspace = true } uucore = { workspace = true, features = ["parser"] } +uulinux = { workspace = true } clap = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/src/uu/lscpu/src/lscpu.rs b/src/uu/lscpu/src/lscpu.rs index a7a29723..d33d6f8a 100644 --- a/src/uu/lscpu/src/lscpu.rs +++ b/src/uu/lscpu/src/lscpu.rs @@ -6,14 +6,16 @@ use clap::{crate_version, Arg, ArgAction, Command}; use regex::RegexBuilder; use serde::Serialize; -use std::{cmp, collections::HashMap, fs}; +use std::{cmp, collections::HashMap, fs, path::Path}; use sysfs::CacheSize; use uucore::{error::UResult, format_usage, help_about, help_usage}; +use uulinux::join_under_root; mod options { pub const BYTES: &str = "bytes"; pub const HEX: &str = "hex"; pub const JSON: &str = "json"; + pub const SYSROOT: &str = "sysroot"; } mod sysfs; @@ -80,30 +82,37 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { json: matches.get_flag(options::JSON), }; + let sysroot = matches + .get_one::(options::SYSROOT) + .map(std::path::PathBuf::from) + .unwrap_or_else(|| std::path::PathBuf::from("/")); + let root = sysroot.as_path(); + let mut cpu_infos = CpuInfos::new(); let mut arch_info = CpuInfo::new("Architecture", &get_architecture()); // TODO: We just silently ignore failures to read `/proc/cpuinfo` currently and treat it as empty // Perhaps a better solution should be put in place, but what? - let contents = fs::read_to_string("/proc/cpuinfo").unwrap_or_default(); + let proc_cpuinfo = join_under_root(root, Path::new("/proc/cpuinfo")); + let contents = fs::read_to_string(proc_cpuinfo).unwrap_or_default(); if let Some(addr_sizes) = find_cpuinfo_value(&contents, "address sizes") { arch_info.add_child(CpuInfo::new("Address sizes", &addr_sizes)) } - if let Some(byte_order) = sysfs::read_cpu_byte_order() { + if let Some(byte_order) = sysfs::read_cpu_byte_order(root) { arch_info.add_child(CpuInfo::new("Byte Order", byte_order)); } cpu_infos.push(arch_info); - let cpu_topology = sysfs::CpuTopology::new(); + let cpu_topology = sysfs::CpuTopology::new(root); let mut cores_info = CpuInfo::new("CPU(s)", &format!("{}", cpu_topology.cpus.len())); cores_info.add_child(CpuInfo::new( "On-line CPU(s) list", - &sysfs::read_online_cpus(), + &sysfs::read_online_cpus(root), )); cpu_infos.push(cores_info); @@ -139,7 +148,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { )); model_name_info.add_child(CpuInfo::new("Socket(s)", &socket_count.to_string())); - if let Some(freq_boost_enabled) = sysfs::read_freq_boost_state() { + if let Some(freq_boost_enabled) = sysfs::read_freq_boost_state(root) { let s = if freq_boost_enabled { "enabled" } else { @@ -158,7 +167,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { cpu_infos.push(cache_info); } - let vulns = sysfs::read_cpu_vulnerabilities(); + let vulns = sysfs::read_cpu_vulnerabilities(root); if !vulns.is_empty() { let mut vuln_info = CpuInfo::new("Vulnerabilities", ""); for vuln in vulns { @@ -350,4 +359,12 @@ pub fn uu_app() -> Command { Setting this flag instead prints the decimal amount of bytes with no suffix.", ), ) + .arg( + Arg::new(options::SYSROOT) + .short('s') + .long("sysroot") + .action(ArgAction::Set) + .value_name("dir") + .help("Gather CPU data from the specified directory as the system root."), + ) } diff --git a/src/uu/lscpu/src/sysfs.rs b/src/uu/lscpu/src/sysfs.rs index e8a8e1a1..2163675e 100644 --- a/src/uu/lscpu/src/sysfs.rs +++ b/src/uu/lscpu/src/sysfs.rs @@ -3,8 +3,13 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use std::{collections::HashSet, fs, path::PathBuf}; +use std::{ + collections::HashSet, + fs, + path::{Path, PathBuf}, +}; use uucore::parser::parse_size; +use uulinux::join_under_root; pub struct CpuVulnerability { pub name: String, @@ -42,13 +47,16 @@ pub enum CacheType { } impl CpuTopology { - pub fn new() -> Self { + pub fn new(root: &Path) -> Self { let mut out: Vec = vec![]; - let online_cpus = parse_cpu_list(&read_online_cpus()); + let online_cpus = parse_cpu_list(&read_online_cpus(root)); for cpu_index in online_cpus { - let cpu_dir = PathBuf::from(format!("/sys/devices/system/cpu/cpu{cpu_index}/")); + let cpu_dir = join_under_root( + root, + &PathBuf::from(format!("/sys/devices/system/cpu/cpu{cpu_index}/")), + ); let pkg_id = fs::read_to_string(cpu_dir.join("topology/physical_package_id")) .unwrap() @@ -62,7 +70,7 @@ impl CpuTopology { .parse::() .unwrap(); - let caches = read_cpu_caches(cpu_index); + let caches = read_cpu_caches(root, cpu_index); out.push(Cpu { _index: cpu_index, @@ -120,15 +128,19 @@ impl CacheSize { } // TODO: respect `--hex` option and output the bitmask instead of human-readable range -pub fn read_online_cpus() -> String { - fs::read_to_string("/sys/devices/system/cpu/online") +pub fn read_online_cpus(root: &Path) -> String { + let path = join_under_root(root, Path::new("/sys/devices/system/cpu/online")); + fs::read_to_string(path) .expect("Could not read sysfs") .trim() .to_string() } -fn read_cpu_caches(cpu_index: usize) -> Vec { - let cpu_dir = PathBuf::from(format!("/sys/devices/system/cpu/cpu{cpu_index}/")); +fn read_cpu_caches(root: &Path, cpu_index: usize) -> Vec { + let cpu_dir = join_under_root( + root, + &PathBuf::from(format!("/sys/devices/system/cpu/cpu{cpu_index}/")), + ); let cache_dir = fs::read_dir(cpu_dir.join("cache")).unwrap(); let cache_paths = cache_dir .flatten() @@ -170,16 +182,18 @@ fn read_cpu_caches(cpu_index: usize) -> Vec { caches } -pub fn read_freq_boost_state() -> Option { - fs::read_to_string("/sys/devices/system/cpu/cpufreq/boost") +pub fn read_freq_boost_state(root: &Path) -> Option { + let path = join_under_root(root, Path::new("/sys/devices/system/cpu/cpufreq/boost")); + fs::read_to_string(path) .map(|content| content.trim() == "1") .ok() } -pub fn read_cpu_vulnerabilities() -> Vec { +pub fn read_cpu_vulnerabilities(root: &Path) -> Vec { let mut out: Vec = vec![]; - if let Ok(dir) = fs::read_dir("/sys/devices/system/cpu/vulnerabilities") { + let path = join_under_root(root, Path::new("/sys/devices/system/cpu/vulnerabilities")); + if let Ok(dir) = fs::read_dir(path) { let mut files: Vec<_> = dir .flatten() .map(|x| x.path()) @@ -203,8 +217,9 @@ pub fn read_cpu_vulnerabilities() -> Vec { out } -pub fn read_cpu_byte_order() -> Option<&'static str> { - if let Ok(byte_order) = fs::read_to_string("/sys/kernel/cpu_byteorder") { +pub fn read_cpu_byte_order(root: &Path) -> Option<&'static str> { + let path = join_under_root(root, Path::new("/sys/kernel/cpu_byteorder")); + if let Ok(byte_order) = fs::read_to_string(path) { match byte_order.trim() { "big" => return Some("Big Endian"), "little" => return Some("Little Endian"), diff --git a/src/uu/lsmem/Cargo.toml b/src/uu/lsmem/Cargo.toml index 47cd8d0d..2a7cbe5f 100644 --- a/src/uu/lsmem/Cargo.toml +++ b/src/uu/lsmem/Cargo.toml @@ -12,6 +12,7 @@ path = "src/main.rs" [dependencies] uucore = { workspace = true } +uulinux = { workspace = true } clap = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/src/uu/lsmem/src/lsmem.rs b/src/uu/lsmem/src/lsmem.rs index 11f30a26..ead5bb2d 100644 --- a/src/uu/lsmem/src/lsmem.rs +++ b/src/uu/lsmem/src/lsmem.rs @@ -12,9 +12,10 @@ use serde::{Deserialize, Serialize}; use std::borrow::Borrow; use std::fs; use std::io::{self, BufRead, BufReader}; -use std::path::{Path, PathBuf, MAIN_SEPARATOR}; +use std::path::{Path, PathBuf}; use std::str::FromStr; use uucore::{error::UResult, format_usage, help_about, help_usage}; +use uulinux::join_under_root; const ABOUT: &str = help_about!("lsmem.md"); const USAGE: &str = help_usage!("lsmem.md"); @@ -783,12 +784,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } if let Some(sysroot) = matches.get_one::(options::SYSROOT) { - opts.sysmem = format!( - "{}{}{}", - sysroot.trim_end_matches(MAIN_SEPARATOR), - MAIN_SEPARATOR, - opts.sysmem.trim_start_matches(MAIN_SEPARATOR) - ); + opts.sysmem = join_under_root(Path::new(sysroot), Path::new(&opts.sysmem)) + .display() + .to_string(); } read_info(&mut lsmem, &mut opts); diff --git a/src/uulinux/Cargo.toml b/src/uulinux/Cargo.toml new file mode 100644 index 00000000..f1e4fa1c --- /dev/null +++ b/src/uulinux/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "uulinux" +version = "0.0.1" +edition = "2021" + +[dependencies] diff --git a/src/uulinux/src/lib.rs b/src/uulinux/src/lib.rs new file mode 100644 index 00000000..9ee14e65 --- /dev/null +++ b/src/uulinux/src/lib.rs @@ -0,0 +1,55 @@ +// This file is part of the uutils util-linux package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use std::path::{Path, PathBuf}; + +/// Join `path` under `root`, ignoring any leading `/` in `path`. +/// +/// Unlike [`Path::join`], this never discards `root` when `path` is absolute. +/// Useful for prepending a sysroot to a system path like `/sys/devices/...`. +pub fn join_under_root(root: &Path, path: &Path) -> PathBuf { + let relative = path.strip_prefix("/").unwrap_or(path); + root.join(relative) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn absolute_path_is_joined_under_root() { + assert_eq!( + join_under_root( + Path::new("/sysroot"), + Path::new("/sys/devices/system/memory") + ), + PathBuf::from("/sysroot/sys/devices/system/memory"), + ); + } + + #[test] + fn relative_path_is_joined_normally() { + assert_eq!( + join_under_root(Path::new("/sysroot"), Path::new("sys/devices")), + PathBuf::from("/sysroot/sys/devices"), + ); + } + + #[test] + fn root_slash_alone_gives_root() { + assert_eq!( + join_under_root(Path::new("/sysroot"), Path::new("/")), + PathBuf::from("/sysroot"), + ); + } + + #[test] + fn trailing_slash_on_root_is_handled() { + assert_eq!( + join_under_root(Path::new("/sysroot/"), Path::new("/sys/devices")), + PathBuf::from("/sysroot/sys/devices"), + ); + } +} diff --git a/tests/by-util/test_lscpu.rs b/tests/by-util/test_lscpu.rs index 47342d65..a7d5ccc4 100644 --- a/tests/by-util/test_lscpu.rs +++ b/tests/by-util/test_lscpu.rs @@ -3,6 +3,8 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +#[cfg(target_os = "linux")] +use std::path::Path; use uutests::new_ucmd; #[test] @@ -47,3 +49,143 @@ fn test_output() { assert!(stdout.contains("\n Model name:")); assert!(stdout.contains("\n CPU Family:")); } + +#[cfg(target_os = "linux")] +fn write_file(dir: &Path, name: &str, content: &str) { + std::fs::create_dir_all(dir).unwrap(); + std::fs::write(dir.join(name), content).unwrap(); +} + +#[cfg(target_os = "linux")] +/// Builds a minimal fake sysfs/procfs tree for lscpu testing. +/// +/// /proc/cpuinfo +/// /sys/devices/system/cpu/online +/// /sys/devices/system/cpu/cpu{N}/topology/{physical_package_id,core_id} +/// /sys/devices/system/cpu/cpu{N}/cache/index0/{type,level,size,shared_cpu_map} +/// /sys/kernel/cpu_byteorder +struct TestSysCpu { + sysroot: tempfile::TempDir, +} + +#[cfg(target_os = "linux")] +impl TestSysCpu { + fn new() -> Self { + let sysroot = tempfile::TempDir::new().unwrap(); + let root = sysroot.path(); + + // /proc/cpuinfo + let proc_dir = root.join("proc"); + write_file( + &proc_dir, + "cpuinfo", + "processor\t: 0\n\ + vendor_id\t: GenuineIntel\n\ + cpu family\t: 6\n\ + model\t\t: 142\n\ + model name\t: Test CPU @ 1.00GHz\n\ + address sizes\t: 39 bits physical, 48 bits virtual\n\ + \n\ + processor\t: 1\n\ + vendor_id\t: GenuineIntel\n\ + cpu family\t: 6\n\ + model\t\t: 142\n\ + model name\t: Test CPU @ 1.00GHz\n\ + address sizes\t: 39 bits physical, 48 bits virtual\n", + ); + + // /sys/devices/system/cpu/ + let cpu_base = root.join("sys").join("devices").join("system").join("cpu"); + write_file(&cpu_base, "online", "0-1"); + + // Two CPUs, one socket, two cores + for (cpu, core_id, cpu_map) in [("cpu0", "0", "00000001"), ("cpu1", "1", "00000002")] { + let topo = cpu_base.join(cpu).join("topology"); + write_file(&topo, "physical_package_id", "0"); + write_file(&topo, "core_id", core_id); + + let cache = cpu_base.join(cpu).join("cache").join("index0"); + write_file(&cache, "type", "Unified"); + write_file(&cache, "level", "1"); + write_file(&cache, "size", "512K"); + write_file(&cache, "shared_cpu_map", cpu_map); + } + + // /sys/kernel/cpu_byteorder + let kernel_dir = root.join("sys").join("kernel"); + write_file(&kernel_dir, "cpu_byteorder", "little"); + + TestSysCpu { sysroot } + } + + fn path(&self) -> &Path { + self.sysroot.path() + } +} + +#[test] +#[cfg(target_os = "linux")] +fn test_sysroot_basic() { + let sys = TestSysCpu::new(); + new_ucmd!() + .args(&["-s", sys.path().to_str().unwrap()]) + .succeeds() + .no_stderr() + .stdout_contains("CPU(s):") + .stdout_contains("2") + .stdout_contains("0-1") + .stdout_contains("Little Endian") + .stdout_contains("GenuineIntel") + .stdout_contains("Test CPU @ 1.00GHz") + .stdout_contains("Socket(s):"); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_sysroot_json() { + let sys = TestSysCpu::new(); + let res = new_ucmd!() + .args(&["-s", sys.path().to_str().unwrap(), "--json"]) + .succeeds(); + res.no_stderr(); + + let stdout = res.stdout_str(); + assert!(stdout.starts_with("{")); + assert!(stdout.ends_with("}\n")); + + res.stdout_contains("\"field\": \"CPU(s)\"") + .stdout_contains("\"data\": \"2\"") + .stdout_contains("GenuineIntel") + .stdout_contains("Little Endian"); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_sysroot_long_flag() { + // --sysroot and -s should behave identically + let sys = TestSysCpu::new(); + let out_short = new_ucmd!() + .args(&["-s", sys.path().to_str().unwrap()]) + .succeeds() + .stdout_str() + .to_string(); + let out_long = new_ucmd!() + .args(&["--sysroot", sys.path().to_str().unwrap()]) + .succeeds() + .stdout_str() + .to_string(); + assert_eq!(out_short, out_long); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_sysroot_cache() { + let sys = TestSysCpu::new(); + // Two CPUs each with a 512K L1 cache and unique shared_cpu_map, so 2 instances total + new_ucmd!() + .args(&["-s", sys.path().to_str().unwrap()]) + .succeeds() + .no_stderr() + .stdout_contains("Caches (sum of all):") + .stdout_contains("(2 instances)"); +}