1use linuxutils_common::man::ManContent;
2
3pub const MAN: ManContent = ManContent::empty();
4
5use clap::Parser;
6use cols::{Cols, print_table};
7use procfs::Current;
8use std::{
9 collections::{BTreeMap, BTreeSet},
10 fs, io,
11 process::ExitCode,
12};
13
14const CPU_SYS: &str = "/sys/devices/system/cpu";
15const NODE_SYS: &str = "/sys/devices/system/node";
16
17#[derive(Parser)]
22#[command(
23 name = "lscpu",
24 about = "Display information about the CPU architecture"
25)]
26pub struct Args {
27 #[arg(short = 'B', long)]
29 bytes: bool,
30}
31
32#[derive(Cols)]
33struct Row {
34 #[column(header = "", width_fixed = 25)]
35 label: String,
36
37 #[column(header = "", wrap)]
38 value: String,
39}
40
41impl Row {
42 fn new(indent: usize, label: &str, value: &str) -> Self {
43 let padding = " ".repeat(indent);
44 Self {
45 label: format!("{padding}{label}"),
46 value: value.to_string(),
47 }
48 }
49
50 fn section(title: &str) -> Self {
51 Self {
52 label: title.to_string(),
53 value: String::new(),
54 }
55 }
56}
57
58fn sysfs_read(path: &str) -> Option<String> {
59 fs::read_to_string(path).ok().map(|s| s.trim().to_string())
60}
61
62fn cpuinfo_field(
63 cpuinfo: &procfs::CpuInfo,
64 cpu: usize,
65 field: &str,
66) -> Option<String> {
67 cpuinfo
68 .get_info(cpu)
69 .and_then(|info| info.get(field).map(|s| s.to_string()))
70}
71
72fn parse_cpu_list(s: &str) -> Vec<u32> {
73 let mut result = Vec::new();
74 for part in s.split(',') {
75 let part = part.trim();
76 if let Some((start, end)) = part.split_once('-') {
77 if let (Ok(s), Ok(e)) = (start.parse::<u32>(), end.parse::<u32>()) {
78 result.extend(s..=e);
79 }
80 } else if let Ok(n) = part.parse::<u32>() {
81 result.push(n);
82 }
83 }
84 result
85}
86
87fn parse_cache_size_kb(s: &str) -> Option<u64> {
88 let s = s.trim();
89 if let Some(num) = s.strip_suffix('K') {
90 num.trim().parse().ok()
91 } else if let Some(num) = s.strip_suffix('M') {
92 num.trim().parse::<u64>().ok().map(|n| n * 1024)
93 } else {
94 s.parse().ok()
95 }
96}
97
98fn format_cache_size(total_kb: u64) -> String {
99 if total_kb >= 1024 && total_kb.is_multiple_of(1024) {
100 format!("{} MiB", total_kb / 1024)
101 } else {
102 format!("{total_kb} KiB")
103 }
104}
105
106struct CacheInfo {
107 name: String,
108 one_size_kb: u64,
109 instances: usize,
110}
111
112fn gather_caches() -> Vec<CacheInfo> {
113 let mut seen: BTreeMap<String, (u64, BTreeSet<String>)> = BTreeMap::new();
114
115 let Ok(entries) = fs::read_dir(CPU_SYS) else {
116 return Vec::new();
117 };
118
119 for entry in entries.flatten() {
120 let cpu_name = entry.file_name();
121 let cpu_str = cpu_name.to_string_lossy();
122 if !cpu_str.starts_with("cpu")
123 || cpu_str[3..]
124 .chars()
125 .next()
126 .is_none_or(|c| !c.is_ascii_digit())
127 {
128 continue;
129 }
130
131 let cache_dir = format!("{CPU_SYS}/{cpu_str}/cache");
132 let Ok(cache_entries) = fs::read_dir(&cache_dir) else {
133 continue;
134 };
135
136 for ce in cache_entries.flatten() {
137 let idx_name = ce.file_name();
138 let idx_str = idx_name.to_string_lossy();
139 if !idx_str.starts_with("index") {
140 continue;
141 }
142
143 let base = format!("{cache_dir}/{idx_str}");
144 let level =
145 sysfs_read(&format!("{base}/level")).unwrap_or_default();
146 let cache_type =
147 sysfs_read(&format!("{base}/type")).unwrap_or_default();
148 let size_str =
149 sysfs_read(&format!("{base}/size")).unwrap_or_default();
150 let shared = sysfs_read(&format!("{base}/shared_cpu_list"))
151 .unwrap_or_default();
152
153 let size_kb = parse_cache_size_kb(&size_str).unwrap_or(0);
154 let type_abbr = match cache_type.as_str() {
155 "Data" => "d",
156 "Instruction" => "i",
157 _ => "",
158 };
159 let name = format!("L{level}{type_abbr}");
160
161 seen.entry(name)
162 .or_insert_with(|| (size_kb, BTreeSet::new()))
163 .1
164 .insert(shared);
165 }
166 }
167
168 seen.into_iter()
169 .map(|(name, (one_size_kb, shared_sets))| CacheInfo {
170 name,
171 one_size_kb,
172 instances: shared_sets.len(),
173 })
174 .collect()
175}
176
177fn gather_numa_nodes() -> Vec<(u32, String)> {
178 let Ok(entries) = fs::read_dir(NODE_SYS) else {
179 return Vec::new();
180 };
181
182 let mut nodes: Vec<(u32, String)> = Vec::new();
183 for entry in entries.flatten() {
184 let name = entry.file_name();
185 let name_str = name.to_string_lossy();
186 if let Some(num_str) = name_str.strip_prefix("node")
187 && let Ok(num) = num_str.parse::<u32>()
188 {
189 let cpulist = sysfs_read(&format!("{NODE_SYS}/{name_str}/cpulist"))
190 .unwrap_or_default();
191 nodes.push((num, cpulist));
192 }
193 }
194 nodes.sort_by_key(|(n, _)| *n);
195 nodes
196}
197
198fn gather_vulnerabilities() -> Vec<(String, String)> {
199 let vuln_dir = format!("{CPU_SYS}/vulnerabilities");
200 let Ok(entries) = fs::read_dir(&vuln_dir) else {
201 return Vec::new();
202 };
203
204 let mut vulns: Vec<(String, String)> = entries
205 .flatten()
206 .filter_map(|e| {
207 let name = e.file_name().to_string_lossy().to_string();
208 let status = sysfs_read(&format!("{vuln_dir}/{name}"))?;
209 Some((name, status))
210 })
211 .collect();
212
213 vulns.sort_by(|(a, _), (b, _)| a.cmp(b));
214 vulns
215}
216
217fn pretty_vuln_name(file_name: &str) -> String {
218 let mut result = file_name.replace('_', " ");
219 if let Some(first) = result.get_mut(..1) {
220 first.make_ascii_uppercase();
221 }
222 result
223}
224
225pub fn run(_args: Args) -> ExitCode {
226 let cpuinfo = match procfs::CpuInfo::current() {
227 Ok(c) => c,
228 Err(e) => {
229 eprintln!("lscpu: failed to read /proc/cpuinfo: {e}");
230 return ExitCode::FAILURE;
231 }
232 };
233
234 let num_cpus = cpuinfo.num_cores();
235 let vendor = cpuinfo_field(&cpuinfo, 0, "vendor_id").unwrap_or_default();
236 let model_name =
237 cpuinfo_field(&cpuinfo, 0, "model name").unwrap_or_default();
238 let cpu_family =
239 cpuinfo_field(&cpuinfo, 0, "cpu family").unwrap_or_default();
240 let model = cpuinfo_field(&cpuinfo, 0, "model").unwrap_or_default();
241 let stepping = cpuinfo_field(&cpuinfo, 0, "stepping").unwrap_or_default();
242 let bogomips = cpuinfo_field(&cpuinfo, 0, "bogomips").unwrap_or_default();
243 let flags_str = cpuinfo_field(&cpuinfo, 0, "flags").unwrap_or_default();
244 let addr_sizes =
245 cpuinfo_field(&cpuinfo, 0, "address sizes").unwrap_or_default();
246
247 let arch = std::env::consts::ARCH;
248 let op_modes = match arch {
249 "x86_64" => "32-bit, 64-bit",
250 "aarch64" => "32-bit, 64-bit",
251 "x86" => "32-bit",
252 _ => arch,
253 };
254
255 let online = sysfs_read(&format!("{CPU_SYS}/online"))
256 .unwrap_or_else(|| format!("0-{}", num_cpus - 1));
257 let offline = sysfs_read(&format!("{CPU_SYS}/offline")).unwrap_or_default();
258
259 let mut sockets: BTreeSet<u32> = BTreeSet::new();
260 let mut cores_per_socket: BTreeMap<u32, BTreeSet<u32>> = BTreeMap::new();
261 let online_cpus = parse_cpu_list(&online);
262
263 for &cpu in &online_cpus {
264 let pkg = sysfs_read(&format!(
265 "{CPU_SYS}/cpu{cpu}/topology/physical_package_id"
266 ))
267 .and_then(|s| s.parse().ok())
268 .unwrap_or(0);
269 let core = sysfs_read(&format!("{CPU_SYS}/cpu{cpu}/topology/core_id"))
270 .and_then(|s| s.parse().ok())
271 .unwrap_or(0);
272 sockets.insert(pkg);
273 cores_per_socket.entry(pkg).or_default().insert(core);
274 }
275
276 let num_sockets = sockets.len().max(1);
277 let cores_per_sock = if !cores_per_socket.is_empty() {
278 cores_per_socket.values().next().unwrap().len()
279 } else {
280 num_cpus
281 };
282 let threads_per_core = if cores_per_sock > 0 && num_sockets > 0 {
283 online_cpus.len() / (num_sockets * cores_per_sock)
284 } else {
285 1
286 }
287 .max(1);
288
289 let max_mhz =
290 sysfs_read(&format!("{CPU_SYS}/cpu0/cpufreq/cpuinfo_max_freq"))
291 .and_then(|s| s.parse::<f64>().ok())
292 .map(|khz| khz / 1000.0);
293 let min_mhz =
294 sysfs_read(&format!("{CPU_SYS}/cpu0/cpufreq/cpuinfo_min_freq"))
295 .and_then(|s| s.parse::<f64>().ok())
296 .map(|khz| khz / 1000.0);
297
298 let virt = if flags_str.split_whitespace().any(|f| f == "svm") {
299 Some("AMD-V")
300 } else if flags_str.split_whitespace().any(|f| f == "vmx") {
301 Some("VT-x")
302 } else {
303 None
304 };
305
306 let mut rows: Vec<Row> = Vec::new();
307
308 rows.push(Row::new(0, "Architecture:", arch));
310 rows.push(Row::new(2, "CPU op-mode(s):", op_modes));
311 if !addr_sizes.is_empty() {
312 rows.push(Row::new(2, "Address sizes:", &addr_sizes));
313 }
314 #[cfg(target_endian = "little")]
315 rows.push(Row::new(2, "Byte Order:", "Little Endian"));
316 #[cfg(target_endian = "big")]
317 rows.push(Row::new(2, "Byte Order:", "Big Endian"));
318
319 rows.push(Row::new(0, "CPU(s):", &online_cpus.len().to_string()));
321 rows.push(Row::new(2, "On-line CPU(s) list:", &online));
322 if !offline.is_empty() {
323 rows.push(Row::new(2, "Off-line CPU(s) list:", &offline));
324 }
325
326 rows.push(Row::new(0, "Vendor ID:", &vendor));
328 rows.push(Row::new(2, "Model name:", &model_name));
329 rows.push(Row::new(4, "CPU family:", &cpu_family));
330 rows.push(Row::new(4, "Model:", &model));
331 rows.push(Row::new(
332 4,
333 "Thread(s) per core:",
334 &threads_per_core.to_string(),
335 ));
336 rows.push(Row::new(
337 4,
338 "Core(s) per socket:",
339 &cores_per_sock.to_string(),
340 ));
341 rows.push(Row::new(4, "Socket(s):", &num_sockets.to_string()));
342 rows.push(Row::new(4, "Stepping:", &stepping));
343 if let Some(max) = max_mhz {
344 rows.push(Row::new(4, "CPU max MHz:", &format!("{max:.4}")));
345 }
346 if let Some(min) = min_mhz {
347 rows.push(Row::new(4, "CPU min MHz:", &format!("{min:.4}")));
348 }
349 rows.push(Row::new(4, "BogoMIPS:", &bogomips));
350 if !flags_str.is_empty() {
351 rows.push(Row::new(4, "Flags:", &flags_str));
352 }
353
354 if let Some(v) = virt {
356 rows.push(Row::section("Virtualization features:"));
357 rows.push(Row::new(2, "Virtualization:", v));
358 }
359
360 let caches = gather_caches();
362 if !caches.is_empty() {
363 rows.push(Row::section("Caches (sum of all):"));
364 for cache in &caches {
365 let total_kb = cache.one_size_kb * cache.instances as u64;
366 let size_str = format_cache_size(total_kb);
367 let value = if cache.instances > 1 {
368 format!("{size_str} ({} instances)", cache.instances)
369 } else {
370 format!("{size_str} (1 instance)")
371 };
372 rows.push(Row::new(2, &format!("{}:", cache.name), &value));
373 }
374 }
375
376 let numa_nodes = gather_numa_nodes();
378 if !numa_nodes.is_empty() {
379 rows.push(Row::section("NUMA:"));
380 rows.push(Row::new(2, "NUMA node(s):", &numa_nodes.len().to_string()));
381 for (node, cpulist) in &numa_nodes {
382 rows.push(Row::new(
383 2,
384 &format!("NUMA node{node} CPU(s):"),
385 cpulist,
386 ));
387 }
388 }
389
390 let vulns = gather_vulnerabilities();
392 if !vulns.is_empty() {
393 rows.push(Row::section("Vulnerabilities:"));
394 for (name, status) in &vulns {
395 let pretty = pretty_vuln_name(name);
396 rows.push(Row::new(2, &format!("{pretty}:"), status));
397 }
398 }
399
400 let mut table = Row::to_table(&rows);
401 table.headings_set(false);
402 let _ = print_table(&table, &mut io::stdout().lock());
403
404 ExitCode::SUCCESS
405}