use std::{
fs,
path::{Path, PathBuf},
process::Command,
};
use anyhow::{Context, Result, bail, ensure};
use crate::{
Category, CategorySet, FuncKey, run,
util::{Map, Set},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Verdict {
Confirmed,
Absent,
Unverified,
}
impl Verdict {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Confirmed => "confirmed",
Self::Absent => "absent",
Self::Unverified => "unverified",
}
}
}
#[derive(Debug, Clone, Copy, Default)]
struct Facts {
reaches: CategorySet,
open: bool,
}
#[derive(Debug, Default)]
pub struct Verdicts {
facts: Map<String, Facts>,
}
impl Verdicts {
#[must_use]
pub fn of(&self, key: &FuncKey, category: Category) -> Verdict {
if CategorySet::assumed().contains(category)
|| category == Category::RefCountOverflow
{
return Verdict::Unverified;
}
let Some(facts) = self.facts.get(&key.0) else {
return Verdict::Unverified;
};
if facts.reaches.contains(category) {
return Verdict::Confirmed;
}
if facts.open {
return Verdict::Unverified;
}
Verdict::Absent
}
}
fn entry_categories(demangled: &str) -> Option<CategorySet> {
use Category::{
AllocFailure, Borrow, CapacityOverflow, DivideByZero, Explicit, Fmt,
Index, MisalignedRef, NullDeref, Overflow, Poison, RemainderByZero,
StrBoundary, UbCheck, Unwrap,
};
let table: &[(&str, &[Category])] = &[
("core::panicking::panic_bounds_check", &[Index]),
("core::slice::index::slice_", &[Index]),
("core::str::slice_error_fail", &[StrBoundary]),
("core::option::unwrap_failed", &[Unwrap, Poison, Fmt]),
("core::option::expect_failed", &[Unwrap, Poison, Fmt]),
("core::result::unwrap_failed", &[Unwrap, Poison, Fmt]),
("core::cell::panic_already", &[Borrow]),
("alloc::raw_vec::capacity_overflow", &[CapacityOverflow]),
(
"alloc::raw_vec::handle_error",
&[CapacityOverflow, AllocFailure],
),
(
"alloc::raw_vec::handle_reserve",
&[CapacityOverflow, AllocFailure],
),
("handle_alloc_error", &[AllocFailure]),
("panic_const_div_by_zero", &[DivideByZero]),
("panic_const_rem_by_zero", &[RemainderByZero]),
("panic_const_coroutine", &[Explicit]),
("panic_const_async", &[Explicit]),
("panic_const_gen_fn", &[Explicit]),
("panic_const_", &[Overflow]),
("panic_misaligned_pointer", &[MisalignedRef]),
("panic_null_pointer", &[NullDeref]),
("core::panicking::panic_nounwind", &[Explicit, UbCheck]),
("resume_unwind", &[Explicit]),
("len_mismatch_fail", &[Explicit]),
("core::panicking::", &[Explicit]),
];
table
.iter()
.find(|(name, _)| demangled.contains(name))
.map(|(_, set)| set.iter().copied().collect())
}
pub fn sweep(tree: &Path, profile: &str) -> Result<Verdicts> {
let objects = libraries_in(tree, run::profile_dir(profile));
if objects.is_empty() {
bail!(
"no compiled library found under {} for the {profile} profile; \
run the analysis first",
tree.display()
);
}
let objdump = llvm_tool("llvm-objdump")?;
let mut graph = Graphed::default();
for object in &objects {
let listing = Command::new(&objdump)
.arg("-d")
.arg("-r")
.arg("--no-show-raw-insn")
.arg(object)
.output()
.with_context(|| {
format!("could not disassemble {}", object.display())
})?;
ensure!(
listing.status.success(),
"disassembling {} failed: {}",
object.display(),
String::from_utf8_lossy(&listing.stderr).trim()
);
graph.read(&String::from_utf8_lossy(&listing.stdout));
}
Ok(graph.resolve())
}
#[derive(Debug, Default)]
struct Graphed {
calls: Map<String, Set<String>>,
open: Set<String>,
}
impl Graphed {
fn read(&mut self, listing: &str) {
let mut current: Option<String> = None;
let mut taking = false;
for line in listing.lines() {
if let Some(label) = function_label(line) {
self.calls.entry(label.clone()).or_default();
current = Some(label);
taking = false;
continue;
}
let Some(function) = ¤t else { continue };
if let Some(target) = relocation_target(line) {
if taking {
self.open.insert(function.clone());
} else {
self.calls
.entry(function.clone())
.or_default()
.insert(target);
}
continue;
}
if let Some(word) = mnemonic(line) {
taking = word.starts_with("lea");
}
if line.contains("call") && line.contains("*%") {
self.open.insert(function.clone());
}
}
}
fn resolve(self) -> Verdicts {
let mut out = Verdicts::default();
for name in self.calls.keys() {
let mut facts = Facts::default();
let mut seen: Set<&str> = Set::default();
let mut stack: Vec<&str> = vec![name];
while let Some(at) = stack.pop() {
if !seen.insert(at) {
continue;
}
if self.open.contains(at) {
facts.open = true;
}
let Some(callees) = self.calls.get(at) else {
let readable =
format!("{:#}", rustc_demangle::demangle(at));
match entry_categories(&readable) {
Some(set) => {
facts.reaches = facts.reaches.union(set);
}
None => facts.open = true,
}
continue;
};
for callee in callees {
stack.push(callee);
}
}
out.facts.insert(name.clone(), facts);
}
out
}
}
fn function_label(line: &str) -> Option<String> {
let rest = line.strip_suffix(">:")?;
let (address, name) = rest.split_once(" <")?;
if address.is_empty() || !address.bytes().all(|b| b.is_ascii_hexdigit()) {
return None;
}
Some(name.to_owned())
}
fn mnemonic(line: &str) -> Option<&str> {
let (address, rest) = line.split_once(':')?;
let address = address.trim();
if address.is_empty() || !address.bytes().all(|b| b.is_ascii_hexdigit()) {
return None;
}
let word = rest.split_whitespace().next()?;
(!word.starts_with("R_")).then_some(word)
}
fn relocation_target(line: &str) -> Option<String> {
if !line.contains(": R_") {
return None;
}
let raw = line.split_whitespace().last()?;
let raw = raw
.rfind(['+', '-'])
.filter(|at| raw[at + 1..].starts_with("0x"))
.map_or(raw, |at| &raw[..at]);
if let Some(inner) = raw.split_once("._R").map(|(_, sym)| sym) {
return raw.starts_with(".text").then(|| format!("_R{inner}"));
}
if let Some(inner) = raw.split_once("._ZN").map(|(_, sym)| sym) {
return raw.starts_with(".text").then(|| format!("_ZN{inner}"));
}
if raw.starts_with('.') {
return None;
}
Some(raw.to_owned())
}
fn libraries_in(tree: &Path, profile_dir: &str) -> Vec<PathBuf> {
let mut out = Vec::new();
let mut stack = vec![tree.to_path_buf()];
while let Some(dir) = stack.pop() {
let Ok(entries) = fs::read_dir(&dir) else {
continue;
};
for entry in entries.filter_map(Result::ok) {
let path = entry.path();
let Ok(kind) = entry.file_type() else {
continue;
};
if kind.is_dir() {
if path.file_name().is_some_and(|name| name == "deps") {
continue;
}
stack.push(path);
continue;
}
let named_lib = path
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| {
name.starts_with("lib")
&& Path::new(name)
.extension()
.is_some_and(|ext| ext == "rlib")
});
let under_profile = path
.parent()
.and_then(Path::file_name)
.is_some_and(|name| name == profile_dir);
if named_lib && under_profile {
out.push(path);
}
}
}
out
}
fn llvm_tool(name: &str) -> Result<PathBuf> {
let sysroot = run::sysroot()?.context("rustc did not report a sysroot")?;
let tool = PathBuf::from(sysroot)
.join("lib")
.join("rustlib")
.join(run::host_triple()?)
.join("bin")
.join(name);
if !tool.exists() {
bail!(
"{name} is missing from the toolchain; install the llvm-tools \
component"
);
}
Ok(tool)
}