use std::collections::{BTreeMap, BTreeSet};
use anyhow::Context as _;
use itertools::Itertools as _;
use serde::{Deserialize, Serialize};
use crate::{
CrateName, Symbol, cap_rule::SymbolRules, reservoir_sample::ReservoirSampleExt as _,
rust_path::RustPath, symbol::FunctionOrPath,
};
pub type CapabilitySet = BTreeSet<Capability>;
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum Capability {
#[serde(rename = "build.rs")]
BuildRs,
#[serde(rename = "alloc")]
Alloc,
#[serde(rename = "panic")]
Panic,
#[serde(rename = "time")]
Time,
#[serde(rename = "sysinfo")]
Sysinfo,
#[serde(rename = "stdio")]
Stdio,
#[serde(rename = "thread")]
Thread,
#[serde(rename = "net")]
Net,
#[serde(rename = "fs")]
FS,
#[serde(rename = "*")]
Any,
}
impl std::fmt::Display for Capability {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::BuildRs => write!(f, "build.rs"),
Self::Alloc => write!(f, "alloc"),
Self::Panic => write!(f, "panic"),
Self::Time => write!(f, "time"),
Self::Sysinfo => write!(f, "sysinfo"),
Self::Stdio => write!(f, "stdio"),
Self::Thread => write!(f, "thread"),
Self::Net => write!(f, "net"),
Self::FS => write!(f, "fs"),
Self::Any => write!(f, "any"),
}
}
}
impl Capability {
pub fn emoji(&self) -> &'static str {
match self {
Self::BuildRs => "🛠️ ",
Self::Alloc => "📦",
Self::Panic => "❗️",
Self::Time => "⏰",
Self::Sysinfo => "🖥️ ",
Self::Stdio => "📝",
Self::Thread => "🧵",
Self::Net => "🌐",
Self::FS => "📁",
Self::Any => "⚠️ ",
}
}
}
#[derive(Clone, Debug, Default)]
pub struct DeducedCaps {
pub caps: BTreeMap<Capability, Reasons>,
pub unresolved_crates: BTreeMap<CrateName, BTreeSet<RustPath>>,
}
pub type Reasons = BTreeSet<Reason>;
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Reason {
PathMatchedRule(RustPath),
SymbolMatchedRule(Symbol),
SourceParseError(String),
UnmatchedSymbol(Symbol),
UmatchedStandardPath(RustPath),
Crate(CrateName),
}
impl std::fmt::Display for Reason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::PathMatchedRule(path) | Self::UmatchedStandardPath(path) => path.fmt(f),
Self::SourceParseError(err) => write!(f, "{err:#?}"),
Self::SymbolMatchedRule(symbol) | Self::UnmatchedSymbol(symbol) => {
write!(f, "{}", symbol.format(false))
}
Self::Crate(crate_name) => crate_name.fmt(f),
}
}
}
impl DeducedCaps {
pub fn from_symbols(
rules: &SymbolRules,
symbols: impl IntoIterator<Item = Symbol>,
) -> anyhow::Result<Self> {
let mut slf = Self::default();
for symbol in symbols {
slf.add_symbol(rules, &symbol)?;
}
Ok(slf)
}
pub fn from_paths(
rules: &SymbolRules,
paths: impl IntoIterator<Item = RustPath>,
) -> anyhow::Result<Self> {
let mut slf = Self::default();
for path in paths {
slf.add_path(rules, path)?;
}
Ok(slf)
}
fn add_symbol(&mut self, rules: &SymbolRules, symbol: &Symbol) -> anyhow::Result<()> {
for path in symbol.paths() {
match path {
FunctionOrPath::Function(fun_name) => {
let fun_name = fun_name.trim_start_matches('_');
if let Some(capabilities) = rules.match_symbol(fun_name) {
for &capability in capabilities {
self.caps
.entry(capability)
.or_default()
.insert(Reason::SymbolMatchedRule(symbol.clone()));
}
} else {
self.caps
.entry(Capability::Any)
.or_default()
.insert(Reason::UnmatchedSymbol(symbol.clone()));
}
}
FunctionOrPath::RustPath(rust_path) => {
let path_str = rust_path.to_string();
if let Some(capabilities) = rules.match_symbol(&path_str) {
for &capability in capabilities {
self.caps
.entry(capability)
.or_default()
.insert(Reason::PathMatchedRule(rust_path.clone()));
}
} else {
let segments = rust_path.segments();
let crate_name = CrateName::new(segments[0])
.with_context(|| format!("mangled: {:?}", symbol.mangled))
.with_context(|| format!("demangled: {:?}", symbol.demangled))?;
if crate_name.is_standard_crate() {
self.caps
.entry(Capability::Any)
.or_default()
.insert(Reason::UmatchedStandardPath(rust_path.clone()));
} else {
self.unresolved_crates
.entry(crate_name)
.or_default()
.insert(rust_path);
}
}
}
}
}
Ok(())
}
fn add_path(&mut self, rules: &SymbolRules, rust_path: RustPath) -> anyhow::Result<()> {
let path_str = rust_path.to_string();
if let Some(capabilities) = rules.match_symbol(&path_str) {
for &capability in capabilities {
self.caps
.entry(capability)
.or_default()
.insert(Reason::PathMatchedRule(rust_path.clone()));
}
} else {
let segments = rust_path.segments();
let crate_name = segments[0];
let crate_name =
CrateName::new(crate_name).with_context(|| format!("path: {rust_path}"))?;
self.unresolved_crates
.entry(crate_name)
.or_default()
.insert(rust_path);
}
Ok(())
}
}
pub fn format_reasons(reasons: &Reasons) -> String {
let mut crates = vec![];
let mut path_matched_rules = vec![];
let mut symbol_matched_rules = vec![];
let mut unmatched_paths = vec![];
let mut unmatched_symbols = vec![];
let mut source_parse_errors = vec![];
for reason in reasons {
match reason {
Reason::Crate(crate_name) => {
crates.push(crate_name);
}
Reason::UmatchedStandardPath(path) => {
unmatched_paths.push(path);
}
Reason::UnmatchedSymbol(symbol) => {
unmatched_symbols.push(symbol);
}
Reason::PathMatchedRule(rust_path) => {
path_matched_rules.push(rust_path);
}
Reason::SymbolMatchedRule(symbol) => {
symbol_matched_rules.push(symbol);
}
Reason::SourceParseError(error) => {
source_parse_errors.push(error);
}
}
}
fn format_long_list<T: std::fmt::Display>(header: &str, reasons: &[T]) -> String {
let max_width = 60;
let mut string = format!("{header}:");
let mut num_left = reasons.len();
for reason in reasons.iter().reservoir_sample(5) {
if string.len() < max_width {
string += &format!(" {reason}");
num_left -= 1;
} else {
string += &format!(" … + {num_left} more");
break;
}
}
string
}
if !crates.is_empty() {
format_long_list("dependencies", &crates)
} else if !path_matched_rules.is_empty() {
format_long_list("rule for", &path_matched_rules)
} else if !symbol_matched_rules.is_empty() {
let symbol_matched_rules = symbol_matched_rules
.into_iter()
.map(|s| &s.demangled)
.collect_vec();
format_long_list("rule for", &symbol_matched_rules)
} else if !unmatched_paths.is_empty() {
format_long_list("unknown paths", &unmatched_paths)
} else if !unmatched_symbols.is_empty() {
let unmatched_symbols = unmatched_symbols
.into_iter()
.map(|s| &s.demangled)
.collect_vec();
format_long_list("unknown symbols", &unmatched_symbols)
} else if !source_parse_errors.is_empty() {
format_long_list("source parse error", &source_parse_errors)
} else {
unreachable!()
}
}