agesafetch/
lib.rs

1// SPDX-FileCopyrightText: Benedikt Vollmerhaus <benedikt@vollmerhaus.org>
2// SPDX-License-Identifier: MIT
3#[cfg(feature = "python-bindings")]
4mod python;
5
6use std::error::Error;
7use std::ffi::OsString;
8use std::io::{self, IsTerminal};
9use std::sync::LazyLock;
10use std::time::{Duration, Instant};
11
12use caps::{CapSet, Capability};
13use clap::Parser;
14use colored::{ColoredString, Colorize};
15use indoc::{eprintdoc, indoc, printdoc};
16use linux_memutils::agesa::{
17    AgesaVersion, SearchError, SearchResult, find_agesa_version,
18    find_agesa_version_in_memory_region, get_reserved_regions_in_extended_memory,
19};
20use linux_memutils::iomem::MemoryRegion;
21
22#[derive(Parser, Debug)]
23#[command(version, about, after_help = indoc! {"
24    Exit Codes:
25      0: An AGESA version was found
26      1: No version was found in any searched memory region
27      2: /proc/iomem could not be read, e.g. due to insufficient permissions
28      3: /dev/mem could not be opened, e.g. due to insufficient permissions
29      4: An unhandled error occurred while reading a byte in /dev/mem
30"})]
31struct Cli {
32    /// Print only the found version (default if not in a TTY)
33    #[arg(short, long, conflicts_with = "verbose")]
34    quiet: bool,
35    /// Print further information and a closing search summary
36    #[arg(short, long)]
37    verbose: bool,
38}
39
40#[repr(u8)]
41pub enum CliExitCode {
42    VersionFound = 0,
43    NoVersionFound = 1,
44    ProcIomemReadFailure = 2,
45    DevMemOpenFailure = 3,
46    DevMemReadFailure = 4,
47}
48
49static STATUS_PREFIX: LazyLock<ColoredString> = LazyLock::new(|| "::".blue().bold());
50static RESULT_PREFIX: LazyLock<ColoredString> = LazyLock::new(|| "->".yellow().bold());
51static ERROR_PREFIX: LazyLock<ColoredString> = LazyLock::new(|| "ERR".red().bold());
52
53#[must_use]
54#[allow(clippy::missing_panics_doc)]
55pub fn run_cli(args_os: Vec<OsString>) -> CliExitCode {
56    let cli = Cli::parse_from(args_os);
57
58    if !caps::has_cap(None, CapSet::Effective, Capability::CAP_SYS_ADMIN).unwrap() {
59        eprintdoc! {"
60            {} Missing privileges for reading a memory map from /proc/iomem.
61                Please run agesafetch as root or add the SYS_ADMIN capability.
62            ",
63            *ERROR_PREFIX,
64        }
65        return CliExitCode::ProcIomemReadFailure;
66    }
67
68    match find_and_print_agesa_version(&cli) {
69        Ok(Some(_)) => CliExitCode::VersionFound,
70        Ok(None) => CliExitCode::NoVersionFound,
71        Err(SearchError::IomemUnreadable(_)) => {
72            eprintln!(
73                "{} Could not read /proc/iomem. Are its permissions correct?",
74                *ERROR_PREFIX,
75            );
76            CliExitCode::ProcIomemReadFailure
77        }
78        Err(SearchError::DevMemUnopenable(_)) => {
79            eprintdoc! {"
80                {} Could not open /dev/mem.
81                    Please run agesafetch as root or add suitable capabilities.
82                ",
83                *ERROR_PREFIX,
84            }
85            CliExitCode::DevMemOpenFailure
86        }
87        Err(err @ SearchError::ByteUnreadable(_)) => {
88            eprintln!(
89                "{} Unhandled error while reading byte in physical memory: {}",
90                *ERROR_PREFIX,
91                err.source().expect("search error should have a source"),
92            );
93            CliExitCode::DevMemReadFailure
94        }
95    }
96}
97
98fn find_and_print_agesa_version(cli: &Cli) -> SearchResult {
99    if !io::stdout().is_terminal() || cli.quiet {
100        let maybe_found_version = find_agesa_version()?;
101        match maybe_found_version {
102            Some(ref found_version) => println!("{}", found_version.version_string.trim_end()),
103            None => eprintln!("Did not find AGESA version."),
104        }
105        return Ok(maybe_found_version);
106    }
107
108    let reserved_regions =
109        get_reserved_regions_in_extended_memory().map_err(SearchError::IomemUnreadable)?;
110
111    if cli.verbose {
112        println!(
113            "{} Memory map lists {} regions of type {} in extended memory.",
114            *STATUS_PREFIX,
115            reserved_regions.len().to_string().blue(),
116            "Reserved".italic(),
117        );
118    }
119
120    let search_start_time = Instant::now();
121    let maybe_found_version = search_regions_and_print_statuses(reserved_regions)?;
122    let search_duration = search_start_time.elapsed();
123
124    match maybe_found_version {
125        Some(ref found_version) => {
126            println!(
127                "{} Found AGESA version: {}",
128                *RESULT_PREFIX,
129                found_version.version_string.trim_end().green().bold(),
130            );
131
132            if cli.verbose {
133                print_search_summary(found_version, &search_duration);
134            }
135        }
136        None => eprintln!("{} Did not find AGESA version.", *RESULT_PREFIX),
137    }
138
139    Ok(maybe_found_version)
140}
141
142/// Sequentially search each of the given regions for an AGESA version.
143///
144/// This returns as soon as a version is found or an error has occurred.
145fn search_regions_and_print_statuses(regions: Vec<MemoryRegion>) -> SearchResult {
146    for (i, region) in regions.into_iter().enumerate() {
147        println!(
148            "{} Searching {} region {} ({} KiB)...",
149            *STATUS_PREFIX,
150            region.region_type.to_string().italic(),
151            format!("#{}", i + 1).blue(),
152            region.size() / 1024,
153        );
154
155        match find_agesa_version_in_memory_region(region) {
156            Ok(None) => (),
157            result => return result,
158        }
159    }
160
161    Ok(None)
162}
163
164/// Print a concise summary with information about the completed search.
165#[allow(clippy::cast_precision_loss)]
166fn print_search_summary(found_version: &AgesaVersion, search_duration: &Duration) {
167    printdoc! {"
168        {} Search Summary:
169           * Found at Physical Address: {:#x} (in {dev_mem})
170           * Surrounding Memory Region: {}
171           * Region Size:               {} KiB
172           * Bytes Processed in Region: {} KiB
173           * Search Time:               {:.1} ms
174        ",
175        *STATUS_PREFIX,
176        found_version.absolute_address,
177        found_version.surrounding_region,
178        found_version.surrounding_region.size() / 1024,
179        found_version.offset_in_region() / 1024,
180        search_duration.as_micros() as f64 / 1000.0,
181        dev_mem = "/dev/mem".dimmed(),
182    }
183}