riscfetch_core/
lib.rs

1//! RISC-V system information library
2//!
3//! Provides functions to detect and query RISC-V specific system information
4//! including ISA extensions, hardware IDs, vector capabilities, and more.
5//!
6//! # Example
7//!
8//! ```no_run
9//! use riscfetch_core::*;
10//!
11//! if is_riscv() {
12//!     println!("ISA: {}", get_isa_string());
13//!     println!("Extensions: {}", get_extensions_compact());
14//! }
15//! ```
16
17mod hardware;
18mod parsing;
19mod system;
20mod types;
21
22// Re-export types
23pub use types::{CacheInfo, HardwareIds, RiscvInfo, SystemInfo, VectorInfo};
24
25// Re-export parsing functions
26pub use parsing::{
27    parse_extensions_compact, parse_extensions_explained, parse_vector_from_isa,
28    parse_z_extensions, parse_z_extensions_explained,
29};
30
31// Re-export hardware functions
32pub use hardware::{
33    get_board_info, get_cache_info, get_hardware_ids, get_hart_count, get_hart_count_num,
34    get_isa_string, get_vector_detail,
35};
36
37// Re-export system functions
38pub use system::{
39    get_kernel_info, get_memory_bytes, get_memory_info, get_os_info, get_uptime, get_uptime_seconds,
40};
41
42use std::fs;
43use std::process::Command;
44use sysinfo::System;
45
46/// Check if the current system is RISC-V architecture
47pub fn is_riscv() -> bool {
48    if let Ok(output) = Command::new("uname").arg("-m").output() {
49        let arch = String::from_utf8_lossy(&output.stdout);
50        if arch.contains("riscv") {
51            return true;
52        }
53    }
54
55    if let Ok(content) = fs::read_to_string("/proc/cpuinfo") {
56        if content.contains("riscv") || content.contains("RISC-V") {
57            return true;
58        }
59    }
60
61    false
62}
63
64/// Get compact extension list (e.g., "I M A F D C V")
65pub fn get_extensions_compact() -> String {
66    parse_extensions_compact(&get_isa_string())
67}
68
69/// Get Z-extensions as compact string
70pub fn get_z_extensions() -> String {
71    parse_z_extensions(&get_isa_string())
72}
73
74/// Get extensions with explanations
75pub fn get_extensions_explained() -> Vec<(String, String)> {
76    parse_extensions_explained(&get_isa_string())
77}
78
79/// Get Z-extensions with explanations
80pub fn get_z_extensions_explained() -> Vec<(String, String)> {
81    parse_z_extensions_explained(&get_isa_string())
82}
83
84/// Collect RISC-V specific information only (excludes generic system info)
85pub fn collect_riscv_info() -> RiscvInfo {
86    let mut sys = System::new();
87    sys.refresh_cpu_all();
88
89    let isa = get_isa_string();
90    let exts: Vec<String> = get_extensions_explained()
91        .into_iter()
92        .map(|(name, _)| name)
93        .collect();
94    let z_exts: Vec<String> = get_z_extensions_explained()
95        .into_iter()
96        .map(|(name, _)| name)
97        .collect();
98
99    let hw_ids = get_hardware_ids();
100    let isa_lower = isa.to_lowercase();
101    let base = isa_lower.split('_').next().unwrap_or(&isa_lower);
102
103    RiscvInfo {
104        isa,
105        extensions: exts,
106        z_extensions: z_exts,
107        vector: VectorInfo {
108            enabled: base.contains('v') || isa_lower.contains("zve"),
109            vlen: None,
110            elen: None,
111        },
112        hart_count: sys.cpus().len(),
113        hardware_ids: hw_ids,
114        cache: CacheInfo::default(),
115    }
116}
117
118/// Collect all information into a single struct
119pub fn collect_all_info() -> SystemInfo {
120    let mut sys = System::new();
121    sys.refresh_memory();
122    sys.refresh_cpu_all();
123
124    let isa = get_isa_string();
125    let exts: Vec<String> = get_extensions_explained()
126        .into_iter()
127        .map(|(name, _)| name)
128        .collect();
129    let z_exts: Vec<String> = get_z_extensions_explained()
130        .into_iter()
131        .map(|(name, _)| name)
132        .collect();
133
134    let hw_ids = get_hardware_ids();
135    let isa_lower = isa.to_lowercase();
136    let base = isa_lower.split('_').next().unwrap_or(&isa_lower);
137
138    SystemInfo {
139        isa,
140        extensions: exts,
141        z_extensions: z_exts,
142        vector: VectorInfo {
143            enabled: base.contains('v') || isa_lower.contains("zve"),
144            vlen: None,
145            elen: None,
146        },
147        hart_count: sys.cpus().len(),
148        hardware_ids: hw_ids,
149        cache: CacheInfo::default(),
150        board: get_board_info(),
151        memory_used_bytes: sys.used_memory(),
152        memory_total_bytes: sys.total_memory(),
153        kernel: get_kernel_info(),
154        os: get_os_info(),
155        uptime_seconds: System::uptime(),
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    // Real ISA strings from actual RISC-V systems for testing
164    const ISA_VISIONFIVE2: &str = "rv64imafdc_zicntr_zicsr_zifencei_zihpm_zba_zbb";
165    const ISA_SPACEMIT_K1: &str = "rv64imafdcv_zicbom_zicboz_zicntr_zicsr_zifencei_zihintpause_zihpm_zba_zbb_zbc_zbs_zkt_zvkt_zvl128b_zvl256b_zvl32b_zvl64b";
166    const ISA_MINIMAL: &str = "rv64imac";
167    const ISA_RV32: &str = "rv32imc";
168
169    // === ISA Parsing Tests ===
170
171    #[test]
172    fn test_parse_extensions_compact_visionfive2() {
173        let result = parse_extensions_compact(ISA_VISIONFIVE2);
174        assert_eq!(result, "I M A F D C");
175    }
176
177    #[test]
178    fn test_parse_extensions_compact_spacemit() {
179        let result = parse_extensions_compact(ISA_SPACEMIT_K1);
180        assert_eq!(result, "I M A F D C V");
181    }
182
183    #[test]
184    fn test_parse_extensions_compact_minimal() {
185        let result = parse_extensions_compact(ISA_MINIMAL);
186        assert_eq!(result, "I M A C");
187    }
188
189    #[test]
190    fn test_parse_extensions_compact_rv32() {
191        let result = parse_extensions_compact(ISA_RV32);
192        assert_eq!(result, "I M C");
193    }
194
195    #[test]
196    fn test_parse_extensions_compact_unknown() {
197        let result = parse_extensions_compact("unknown");
198        assert_eq!(result, "");
199    }
200
201    // === Z-Extension Tests ===
202
203    #[test]
204    fn test_parse_z_extensions_visionfive2() {
205        let result = parse_z_extensions(ISA_VISIONFIVE2);
206        assert!(result.contains("zicntr"));
207        assert!(result.contains("zicsr"));
208        assert!(result.contains("zifencei"));
209        assert!(result.contains("zba"));
210        assert!(result.contains("zbb"));
211    }
212
213    #[test]
214    fn test_parse_z_extensions_spacemit() {
215        let result = parse_z_extensions(ISA_SPACEMIT_K1);
216        assert!(result.contains("zicbom"));
217        assert!(result.contains("zicboz"));
218        assert!(result.contains("zbc"));
219        assert!(result.contains("zbs"));
220        assert!(result.contains("zvl256b"));
221    }
222
223    #[test]
224    fn test_parse_z_extensions_minimal() {
225        let result = parse_z_extensions(ISA_MINIMAL);
226        assert!(result.is_empty());
227    }
228
229    // === Explained Extensions Tests ===
230
231    #[test]
232    fn test_parse_extensions_explained_visionfive2() {
233        let result = parse_extensions_explained(ISA_VISIONFIVE2);
234        assert_eq!(result.len(), 6); // I M A F D C
235        assert!(result.iter().any(|(n, _)| n == "I"));
236        assert!(result.iter().any(|(n, _)| n == "M"));
237        assert!(result.iter().any(|(n, _)| n == "F"));
238        assert!(result.iter().any(|(n, _)| n == "D"));
239        assert!(result.iter().any(|(n, _)| n == "C"));
240    }
241
242    #[test]
243    fn test_parse_z_extensions_explained_spacemit() {
244        let result = parse_z_extensions_explained(ISA_SPACEMIT_K1);
245        assert!(result
246            .iter()
247            .any(|(n, d)| n == "Zba" && d == "Address Generation"));
248        assert!(result
249            .iter()
250            .any(|(n, d)| n == "Zbb" && d == "Basic Bit Manipulation"));
251        assert!(result
252            .iter()
253            .any(|(n, d)| n == "Zbc" && d == "Carry-less Multiply"));
254    }
255
256    // === Vector Tests ===
257
258    #[test]
259    fn test_parse_vector_no_vector() {
260        let result = parse_vector_from_isa(ISA_VISIONFIVE2);
261        assert!(result.is_none());
262    }
263
264    #[test]
265    fn test_parse_vector_with_v() {
266        let result = parse_vector_from_isa(ISA_SPACEMIT_K1);
267        assert!(result.is_some());
268        let detail = result.unwrap();
269        assert!(detail.contains("Enabled"));
270        assert!(detail.contains("VLEN>=256"));
271    }
272
273    #[test]
274    fn test_parse_vector_zve_only() {
275        let isa = "rv64imac_zve32x";
276        let result = parse_vector_from_isa(isa);
277        assert!(result.is_some());
278        assert!(result.unwrap().contains("Enabled"));
279    }
280
281    // === System Info Tests (these work on any system) ===
282
283    #[test]
284    fn test_get_uptime() {
285        let uptime = get_uptime();
286        assert!(!uptime.is_empty());
287    }
288
289    #[test]
290    fn test_get_uptime_seconds() {
291        let secs = get_uptime_seconds();
292        assert!(secs > 0);
293    }
294
295    #[test]
296    fn test_get_memory_bytes() {
297        let (used, total) = get_memory_bytes();
298        assert!(total > 0);
299        assert!(used <= total);
300    }
301
302    #[test]
303    fn test_get_kernel_info() {
304        let kernel = get_kernel_info();
305        assert!(!kernel.is_empty());
306    }
307
308    #[test]
309    fn test_get_os_info() {
310        let os = get_os_info();
311        assert!(!os.is_empty());
312    }
313
314    #[test]
315    fn test_hardware_ids_default() {
316        let ids = HardwareIds::default();
317        assert!(ids.mvendorid.is_empty());
318        assert!(ids.marchid.is_empty());
319        assert!(ids.mimpid.is_empty());
320    }
321
322    // === Edge Cases ===
323
324    #[test]
325    fn test_parse_extensions_case_insensitive() {
326        let upper = parse_extensions_compact("RV64IMAFDC");
327        let lower = parse_extensions_compact("rv64imafdc");
328        assert_eq!(upper, lower);
329    }
330
331    #[test]
332    fn test_parse_empty_isa() {
333        let result = parse_extensions_compact("");
334        assert_eq!(result, "");
335    }
336
337    #[test]
338    fn test_parse_z_extensions_order_preserved() {
339        let result = parse_z_extensions("rv64i_zba_zbb_zbc");
340        let parts: Vec<&str> = result.split(' ').collect();
341        assert_eq!(parts, vec!["zba", "zbb", "zbc"]);
342    }
343
344    // =======================================================
345    // Specification-based tests (from SPEC.md)
346    // =======================================================
347
348    mod spec_tests {
349        use super::*;
350
351        #[test]
352        fn spec_g_expansion() {
353            assert_eq!(parse_extensions_compact("rv64gc"), "I M A F D C");
354        }
355
356        #[test]
357        fn spec_g_expansion_uppercase() {
358            assert_eq!(parse_extensions_compact("RV64GC"), "I M A F D C");
359        }
360
361        #[test]
362        fn spec_e_extension() {
363            assert_eq!(parse_extensions_compact("rv32e"), "E");
364        }
365
366        #[test]
367        fn spec_e_with_c() {
368            assert_eq!(parse_extensions_compact("rv32ec"), "E C");
369        }
370
371        #[test]
372        fn spec_standard_extensions() {
373            assert_eq!(parse_extensions_compact("rv64imafdc"), "I M A F D C");
374        }
375
376        #[test]
377        fn spec_with_vector() {
378            assert_eq!(parse_extensions_compact("rv64imafdcv"), "I M A F D C V");
379        }
380
381        #[test]
382        fn spec_rv64_prefix_not_vector() {
383            let result = parse_extensions_compact("rv64imafdc");
384            assert!(!result.contains("V"));
385        }
386
387        #[test]
388        fn spec_z_extensions_ignored() {
389            assert_eq!(
390                parse_extensions_compact("rv64imafdc_zba_zbb"),
391                "I M A F D C"
392            );
393        }
394
395        #[test]
396        fn spec_empty_input() {
397            assert_eq!(parse_extensions_compact(""), "");
398        }
399
400        #[test]
401        fn spec_invalid_input() {
402            assert_eq!(parse_extensions_compact("unknown"), "");
403        }
404
405        #[test]
406        fn spec_rv64_only() {
407            assert_eq!(parse_extensions_compact("rv64"), "");
408        }
409
410        #[test]
411        fn spec_z_extensions_basic() {
412            assert_eq!(parse_z_extensions("rv64i_zicsr_zifencei"), "zicsr zifencei");
413        }
414
415        #[test]
416        fn spec_z_extensions_order() {
417            assert_eq!(parse_z_extensions("rv64i_zba_zbb_zbc"), "zba zbb zbc");
418        }
419
420        #[test]
421        fn spec_s_extensions() {
422            let result = parse_z_extensions("rv64i_sstc");
423            assert!(result.contains("sstc"));
424        }
425
426        #[test]
427        fn spec_z_extensions_none() {
428            assert_eq!(parse_z_extensions("rv64imafdc"), "");
429        }
430
431        #[test]
432        fn spec_z_extensions_g_implies() {
433            assert_eq!(parse_z_extensions("rv64gc"), "zicsr zifencei");
434        }
435
436        #[test]
437        fn spec_z_extensions_case() {
438            let result = parse_z_extensions("rv64i_Zicsr");
439            assert_eq!(result, "zicsr");
440        }
441
442        #[test]
443        fn spec_vector_with_v() {
444            let result = parse_vector_from_isa("rv64imafdcv");
445            assert!(result.is_some());
446            assert!(result.unwrap().contains("Enabled"));
447        }
448
449        #[test]
450        fn spec_vector_none() {
451            let result = parse_vector_from_isa("rv64imafdc");
452            assert!(result.is_none());
453        }
454
455        #[test]
456        fn spec_vector_zve() {
457            let result = parse_vector_from_isa("rv64imac_zve32x");
458            assert!(result.is_some());
459        }
460
461        #[test]
462        fn spec_vector_vlen_256() {
463            let result = parse_vector_from_isa("rv64imafdcv_zvl256b");
464            assert!(result.is_some());
465            assert!(result.unwrap().contains("VLEN>=256"));
466        }
467
468        #[test]
469        fn spec_vector_vlen_largest() {
470            let result = parse_vector_from_isa("rv64imafdcv_zvl128b_zvl256b");
471            assert!(result.is_some());
472            assert!(result.unwrap().contains("VLEN>=256"));
473        }
474
475        #[test]
476        fn spec_vector_no_default_vlen() {
477            let result = parse_vector_from_isa("rv64imafdcv");
478            assert!(result.is_some());
479            let detail = result.unwrap();
480            assert!(detail.contains("Enabled"));
481            assert!(!detail.contains("VLEN"));
482        }
483    }
484
485    // =======================================================
486    // RISC-V Hardware Tests (only run on actual RISC-V)
487    // =======================================================
488
489    #[cfg(target_arch = "riscv64")]
490    mod riscv_hardware_tests {
491        use super::*;
492
493        #[test]
494        fn hw_is_riscv() {
495            assert!(is_riscv());
496        }
497
498        #[test]
499        fn hw_isa_string_valid() {
500            let isa = get_isa_string();
501            assert!(isa.starts_with("rv64") || isa.starts_with("rv32"));
502        }
503
504        #[test]
505        fn hw_isa_string_has_base() {
506            let isa = get_isa_string();
507            assert!(isa.contains('i') || isa.contains('e'));
508        }
509
510        #[test]
511        fn hw_extensions_not_empty() {
512            let ext = get_extensions_compact();
513            assert!(!ext.is_empty());
514            assert!(ext.contains('I') || ext.contains('E'));
515        }
516
517        #[test]
518        fn hw_hart_count_positive() {
519            let hart_str = get_hart_count();
520            assert!(hart_str.contains("hart"));
521            let num: String = hart_str
522                .chars()
523                .take_while(|c| c.is_ascii_digit())
524                .collect();
525            let count: usize = num.parse().unwrap_or(0);
526            assert!(count > 0);
527        }
528
529        #[test]
530        fn hw_hardware_ids_present() {
531            let ids = get_hardware_ids();
532            let has_any =
533                !ids.mvendorid.is_empty() || !ids.marchid.is_empty() || !ids.mimpid.is_empty();
534            assert!(has_any);
535        }
536
537        #[test]
538        fn hw_collect_all_info() {
539            let info = collect_all_info();
540            assert!(!info.isa.is_empty());
541            assert!(!info.extensions.is_empty());
542            assert!(info.hart_count > 0);
543        }
544
545        #[test]
546        fn hw_collect_riscv_info() {
547            let info = collect_riscv_info();
548            assert!(!info.isa.is_empty());
549            assert!(!info.extensions.is_empty());
550            assert!(info.hart_count > 0);
551        }
552
553        #[test]
554        fn hw_riscv_info_excludes_system_fields() {
555            let riscv_info = collect_riscv_info();
556            let all_info = collect_all_info();
557
558            // RiscvInfo should have the same RISC-V specific fields
559            assert_eq!(riscv_info.isa, all_info.isa);
560            assert_eq!(riscv_info.extensions, all_info.extensions);
561            assert_eq!(riscv_info.z_extensions, all_info.z_extensions);
562            assert_eq!(riscv_info.hart_count, all_info.hart_count);
563
564            // SystemInfo has additional fields that RiscvInfo doesn't have
565            // (board, memory_*, kernel, os, uptime_seconds)
566            // This is verified by the type system - RiscvInfo simply doesn't have these fields
567        }
568    }
569}