use crate::{backend, fwft, json, marchid, mvendorid, sbi_ext, sbi_impl, vuln};
use gettextrs::{
LocaleCategory, bind_textdomain_codeset, bindtextdomain, gettext, setlocale, textdomain,
};
use sbi_spec::{
base::Version,
binary::{Error as SbiError, SbiRegister, SbiRet},
};
use std::fs;
use unicode_width::UnicodeWidthStr;
const TEXT_DOMAIN: &str = "lssbi";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum ExtensionProbeStatus {
NotSupported,
Supported,
SupportedWithValue(u64),
Error(i64),
}
pub(crate) fn run(legacy: bool, cpu: Option<&str>, json_output: bool) -> Result<(), String> {
unsafe {
setlocale(LocaleCategory::LcAll, "");
}
init_gettext()?;
let cpu = cpu.map(parse_cpu).transpose()?;
let info = backend::probe(cpu).map_err(localize_probe_error)?;
if json_output {
json::print(&info, legacy)
} else {
print_result(info, legacy);
Ok(())
}
}
fn localize_probe_error(error: backend::ProbeError) -> String {
match error {
backend::ProbeError::ModuleNotLoaded => gettext(
"DKMS backend unavailable: the lssbi_probe module is not loaded; run `sudo modprobe lssbi_probe`",
),
backend::ProbeError::CpuOutOfRange { cpu, max } => gettext(
"Linux CPU #{cpu} exceeds the supported affinity range (maximum #{max})",
)
.replace("{cpu}", &cpu.to_string())
.replace("{max}", &max.to_string()),
backend::ProbeError::CpuNotAllowed(cpu) => gettext(
"Linux CPU #{cpu} is not in this process's allowed CPU set; it may be offline or excluded by an affinity/cpuset restriction",
)
.replace("{cpu}", &cpu.to_string()),
backend::ProbeError::CpuAffinity { cpu, error } => {
gettext("Cannot select Linux CPU #{cpu}: {error}")
.replace("{cpu}", &cpu.to_string())
.replace("{error}", &error)
}
backend::ProbeError::Message(message) => message,
}
}
fn parse_cpu(value: &str) -> Result<usize, String> {
value.parse().map_err(|_| {
gettext("Invalid Linux CPU value \"{value}\": expected a non-negative decimal integer")
.replace("{value}", value)
})
}
fn init_gettext() -> Result<(), String> {
let executable = fs::canonicalize("/proc/self/exe")
.map_err(|error| format!("cannot resolve the running executable: {error}"))?;
let executable_dir = executable
.parent()
.ok_or_else(|| "the running executable has no parent directory".to_owned())?;
let locale_dir = match executable_dir.file_name().and_then(|name| name.to_str()) {
Some("bin" | "sbin") => executable_dir
.parent()
.ok_or_else(|| "the executable directory has no installation prefix".to_owned())?
.join("share/locale"),
_ => executable_dir.join("locale"),
};
bindtextdomain(TEXT_DOMAIN, &locale_dir)
.map_err(|error| format!("cannot bind the translation directory: {error}"))?;
bind_textdomain_codeset(TEXT_DOMAIN, "UTF-8")
.map_err(|error| format!("cannot select the translation encoding: {error}"))?;
textdomain(TEXT_DOMAIN).map_err(|error| format!("cannot select the text domain: {error}"))?;
Ok(())
}
fn print_result(info: backend::SbiInfo, legacy: bool) {
let spec_raw = info.spec_version as usize;
let spec = Version::from_raw(spec_raw);
let impl_id = info.impl_id as usize;
let impl_version = info.impl_version as usize;
let mvendorid = info.mvendorid as usize;
let marchid = info.marchid as usize;
let mimpid = info.mimpid as usize;
let raw = gettext("raw");
let id = gettext("ID");
println!(
"{}: v{spec} ({raw} {spec_raw:#x})",
gettext("SBI specification")
);
println!(
"{}: {} ({id} {impl_id:#x})",
gettext("SBI implementation"),
sbi_impl::name(impl_id)
);
let impl_version_label = gettext("SBI implementation version");
if let Some(version) = sbi_impl::version(impl_id, impl_version) {
println!("{impl_version_label}: v{version} ({raw} {impl_version:#x})");
} else {
println!("{impl_version_label}: {raw} {impl_version:#x}");
}
if let Some(vendor) = mvendorid::vendor_name(mvendorid) {
println!(
"{}: {vendor} ({raw} {mvendorid:#x})",
gettext("Machine vendor ID")
);
} else {
println!("{}: {mvendorid:#x}", gettext("Machine vendor ID"));
}
if let Some(project) = marchid::project_name(marchid) {
println!(
"{}: {project} ({raw} {marchid:#x})",
gettext("Machine architecture ID")
);
} else {
println!("{}: {marchid:#x}", gettext("Machine architecture ID"));
}
println!("{}: {mimpid:#x}", gettext("Machine implementation ID"));
print_extensions(&info.extensions, legacy);
print_vulnerabilities(impl_id, impl_version);
print_fwft(&info.fwft);
}
fn print_extensions(results: &[backend::SbiCallResult; sbi_ext::EXTENSIONS.len()], legacy: bool) {
println!("{}:", gettext("SBI extensions"));
let extensions = sbi_ext::selected(legacy);
let names = extensions
.iter()
.map(|extension| gettext(extension.message))
.collect::<Vec<_>>();
let width = status_width(&names);
for (name, result) in names.iter().zip(&results[..extensions.len()]) {
let status = match classify_extension_probe(result) {
ExtensionProbeStatus::NotSupported => gettext("Not supported"),
ExtensionProbeStatus::Supported => gettext("Supported"),
ExtensionProbeStatus::SupportedWithValue(value) => format!(
"{} ({} {value:#x})",
gettext("Supported"),
gettext("probe value")
),
ExtensionProbeStatus::Error(error) => {
format!("{}: {} ({error})", gettext("Error"), sbi_error_name(error))
}
};
print_status(&format!("{name}:"), &status, width);
}
}
fn classify_extension_probe(result: &backend::SbiCallResult) -> ExtensionProbeStatus {
if result.error != 0 {
ExtensionProbeStatus::Error(result.error)
} else {
match result.value {
0 => ExtensionProbeStatus::NotSupported,
1 => ExtensionProbeStatus::Supported,
value => ExtensionProbeStatus::SupportedWithValue(value),
}
}
}
fn sbi_error_name(error: i64) -> &'static str {
match <i64 as SbiRegister>::into_result(SbiRet { error, value: 0 }) {
Ok(_) => "SBI_SUCCESS",
Err(SbiError::Failed) => "SBI_ERR_FAILED",
Err(SbiError::NotSupported) => "SBI_ERR_NOT_SUPPORTED",
Err(SbiError::InvalidParam) => "SBI_ERR_INVALID_PARAM",
Err(SbiError::Denied) => "SBI_ERR_DENIED",
Err(SbiError::InvalidAddress) => "SBI_ERR_INVALID_ADDRESS",
Err(SbiError::AlreadyAvailable) => "SBI_ERR_ALREADY_AVAILABLE",
Err(SbiError::AlreadyStarted) => "SBI_ERR_ALREADY_STARTED",
Err(SbiError::AlreadyStopped) => "SBI_ERR_ALREADY_STOPPED",
Err(SbiError::NoShmem) => "SBI_ERR_NO_SHMEM",
Err(SbiError::InvalidState) => "SBI_ERR_INVALID_STATE",
Err(SbiError::BadRange) => "SBI_ERR_BAD_RANGE",
Err(SbiError::Timeout) => "SBI_ERR_TIMEOUT",
Err(SbiError::Io) => "SBI_ERR_IO",
Err(SbiError::DeniedLocked) => "SBI_ERR_DENIED_LOCKED",
Err(SbiError::Custom(_)) => "SBI_ERR_UNKNOWN",
}
}
fn print_vulnerabilities(impl_id: usize, impl_version: usize) {
println!("{}:", gettext("Vulnerabilities"));
let status = if vuln::pmu2_crash(impl_id, impl_version) {
gettext("Affected")
} else {
gettext("Not affected")
};
let label = format!("{} (CVE-2025-63913):", gettext("PMU2 Crash"));
print_status(&label, &status, 32);
}
fn print_fwft(info: &backend::FwftInfo) {
let cpu = gettext("Linux CPU");
let hart = gettext("SBI hart");
println!(
"{} ({cpu} #{}, {hart} #{}):",
gettext("Firmware Features"),
info.cpu,
info.hart_id
);
let names = fwft::FEATURES.map(|feature| gettext(feature.message));
let width = status_width(&names);
for ((name, feature), result) in names.iter().zip(fwft::FEATURES.iter()).zip(&info.results) {
let status = if result.error != 0 {
gettext("Not supported")
} else {
match feature.kind {
fwft::Kind::Boolean => {
if result.value != 0 {
gettext("Supported")
} else {
gettext("Not supported")
}
}
fwft::Kind::Pmlen if result.value == 0 => gettext("Disabled"),
fwft::Kind::Pmlen => result.value.to_string(),
}
};
print_status(&format!("{name}:"), &status, width);
}
}
fn status_width(names: &[String]) -> usize {
names
.iter()
.map(|name| UnicodeWidthStr::width(name.as_str()) + 2)
.max()
.unwrap_or(0)
.clamp(32, 48)
}
fn print_status(label: &str, status: &str, width: usize) {
let padding = width.saturating_sub(UnicodeWidthStr::width(label)).max(1);
println!(" {label}{}{status}", " ".repeat(padding));
}
#[cfg(test)]
mod tests {
use super::{ExtensionProbeStatus, classify_extension_probe, parse_cpu, sbi_error_name};
use crate::backend::SbiCallResult;
#[test]
fn classifies_extension_probe_results() {
assert_eq!(
classify_extension_probe(&SbiCallResult { error: 0, value: 0 }),
ExtensionProbeStatus::NotSupported
);
assert_eq!(
classify_extension_probe(&SbiCallResult { error: 0, value: 1 }),
ExtensionProbeStatus::Supported
);
assert_eq!(
classify_extension_probe(&SbiCallResult { error: 0, value: 2 }),
ExtensionProbeStatus::SupportedWithValue(2)
);
assert_eq!(
classify_extension_probe(&SbiCallResult {
error: -4,
value: 0,
}),
ExtensionProbeStatus::Error(-4)
);
}
#[test]
fn names_standard_and_unknown_sbi_errors() {
assert_eq!(sbi_error_name(-1), "SBI_ERR_FAILED");
assert_eq!(sbi_error_name(-2), "SBI_ERR_NOT_SUPPORTED");
assert_eq!(sbi_error_name(-3), "SBI_ERR_INVALID_PARAM");
assert_eq!(sbi_error_name(-4), "SBI_ERR_DENIED");
assert_eq!(sbi_error_name(-5), "SBI_ERR_INVALID_ADDRESS");
assert_eq!(sbi_error_name(-6), "SBI_ERR_ALREADY_AVAILABLE");
assert_eq!(sbi_error_name(-7), "SBI_ERR_ALREADY_STARTED");
assert_eq!(sbi_error_name(-8), "SBI_ERR_ALREADY_STOPPED");
assert_eq!(sbi_error_name(-9), "SBI_ERR_NO_SHMEM");
assert_eq!(sbi_error_name(-10), "SBI_ERR_INVALID_STATE");
assert_eq!(sbi_error_name(-11), "SBI_ERR_BAD_RANGE");
assert_eq!(sbi_error_name(-12), "SBI_ERR_TIMEOUT");
assert_eq!(sbi_error_name(-13), "SBI_ERR_IO");
assert_eq!(sbi_error_name(-14), "SBI_ERR_DENIED_LOCKED");
assert_eq!(sbi_error_name(-99), "SBI_ERR_UNKNOWN");
}
#[test]
fn parses_and_explains_cpu_values() {
assert_eq!(parse_cpu("3"), Ok(3));
for value in ["-1", "first"] {
let error = parse_cpu(value).unwrap_err();
assert!(error.contains(value));
assert!(error.contains("non-negative decimal integer"));
}
}
}