Skip to main content

cargo_caps/
capability.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use anyhow::Context as _;
4use itertools::Itertools as _;
5use serde::{Deserialize, Serialize};
6
7use crate::{
8    CrateName, Symbol, cap_rule::SymbolRules, reservoir_sample::ReservoirSampleExt as _,
9    rust_path::RustPath, symbol::FunctionOrPath,
10};
11
12pub type CapabilitySet = BTreeSet<Capability>;
13
14/// A capability a crate can be granted,
15/// or is suspected of having.
16#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
17pub enum Capability {
18    /// This crate has a custom build step (build.rs)
19    ///
20    /// NOT contagious!
21    /// Depending on a crate with a build.rs file does not give you the `BuildRs` capability.
22    #[serde(rename = "build.rs")]
23    BuildRs,
24
25    /// Allocate memory (`Box::new`, `Vec::new`, โ€ฆ)
26    #[serde(rename = "alloc")]
27    Alloc,
28
29    /// Call [`panic!`]
30    #[serde(rename = "panic")]
31    Panic,
32
33    /// Read the current time and/or date
34    #[serde(rename = "time")]
35    Time,
36
37    /// Read environment variables, process info, โ€ฆ
38    #[serde(rename = "sysinfo")]
39    Sysinfo,
40
41    /// Read and write to stdin, stdout, stderr
42    #[serde(rename = "stdio")]
43    Stdio,
44
45    /// Spawn thread
46    #[serde(rename = "thread")]
47    Thread,
48
49    /// Connect over the network and/or listen for incoming network traffic
50    #[serde(rename = "net")]
51    Net,
52
53    /// Open a file on disk for reading or writing
54    #[serde(rename = "fs")]
55    FS,
56
57    /// Anything is possible, including everything else in this enum.
58    #[serde(rename = "*")]
59    Any,
60}
61
62impl std::fmt::Display for Capability {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        match self {
65            Self::BuildRs => write!(f, "build.rs"),
66            Self::Alloc => write!(f, "alloc"),
67            Self::Panic => write!(f, "panic"),
68            Self::Time => write!(f, "time"),
69            Self::Sysinfo => write!(f, "sysinfo"),
70            Self::Stdio => write!(f, "stdio"),
71            Self::Thread => write!(f, "thread"),
72            Self::Net => write!(f, "net"),
73            Self::FS => write!(f, "fs"),
74            Self::Any => write!(f, "any"),
75        }
76    }
77}
78
79impl Capability {
80    pub fn emoji(&self) -> &'static str {
81        match self {
82            Self::BuildRs => "๐Ÿ› ๏ธ ",
83            Self::Alloc => "๐Ÿ“ฆ",
84            Self::Panic => "โ—๏ธ",
85            Self::Time => "โฐ",
86            Self::Sysinfo => "๐Ÿ–ฅ๏ธ ",
87            Self::Stdio => "๐Ÿ“",
88            Self::Thread => "๐Ÿงต",
89            Self::Net => "๐ŸŒ",
90            Self::FS => "๐Ÿ“",
91            Self::Any => "โš ๏ธ ",
92        }
93    }
94}
95
96#[derive(Clone, Debug, Default)]
97pub struct DeducedCaps {
98    /// The capabilities of this crate
99    pub caps: BTreeMap<Capability, Reasons>,
100
101    /// We need to resolve these crates to see what their capabilities are.
102    ///
103    /// The value of the map is what indicated that we were using this crate in the first place.
104    pub unresolved_crates: BTreeMap<CrateName, BTreeSet<RustPath>>,
105}
106
107/// Why do we have this capability?
108pub type Reasons = BTreeSet<Reason>;
109
110#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
111pub enum Reason {
112    /// This path matches a rule. TODO: which rule? Where?
113    PathMatchedRule(RustPath),
114
115    /// This symbol matches a rule. TODO: which rule? Where?
116    SymbolMatchedRule(Symbol),
117
118    /// The reason we have this high capability is because we didn't succeed in understanding the source code.
119    SourceParseError(String),
120
121    /// The reason we have this high capability is because we couldn't match this symbol to any rule.
122    ///
123    /// If you hit this, you need to extend `default_rules.eon`
124    UnmatchedSymbol(Symbol),
125
126    /// Path to `alloc`, `core`, or `std` that didn't match any rule in `default_rules.eon`.
127    UmatchedStandardPath(RustPath),
128
129    /// We have this capability because we depend on this crate, which has that capability.
130    Crate(CrateName),
131}
132
133impl std::fmt::Display for Reason {
134    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135        match self {
136            Self::PathMatchedRule(path) | Self::UmatchedStandardPath(path) => path.fmt(f),
137            Self::SourceParseError(err) => write!(f, "{err:#?}"),
138            Self::SymbolMatchedRule(symbol) | Self::UnmatchedSymbol(symbol) => {
139                write!(f, "{}", symbol.format(false))
140            }
141            Self::Crate(crate_name) => crate_name.fmt(f),
142        }
143    }
144}
145
146impl DeducedCaps {
147    pub fn from_symbols(
148        rules: &SymbolRules,
149        symbols: impl IntoIterator<Item = Symbol>,
150    ) -> anyhow::Result<Self> {
151        let mut slf = Self::default();
152        for symbol in symbols {
153            slf.add_symbol(rules, &symbol)?;
154        }
155        Ok(slf)
156    }
157
158    pub fn from_paths(
159        rules: &SymbolRules,
160        paths: impl IntoIterator<Item = RustPath>,
161    ) -> anyhow::Result<Self> {
162        let mut slf = Self::default();
163        for path in paths {
164            slf.add_path(rules, path)?;
165        }
166        Ok(slf)
167    }
168
169    /// Capability from symbol
170    fn add_symbol(&mut self, rules: &SymbolRules, symbol: &Symbol) -> anyhow::Result<()> {
171        for path in symbol.paths() {
172            match path {
173                FunctionOrPath::Function(fun_name) => {
174                    let fun_name = fun_name.trim_start_matches('_');
175
176                    // Check rules for the symbol
177                    if let Some(capabilities) = rules.match_symbol(fun_name) {
178                        for &capability in capabilities {
179                            self.caps
180                                .entry(capability)
181                                .or_default()
182                                .insert(Reason::SymbolMatchedRule(symbol.clone()));
183                        }
184                    } else {
185                        self.caps
186                            .entry(Capability::Any)
187                            .or_default()
188                            .insert(Reason::UnmatchedSymbol(symbol.clone()));
189                    }
190                }
191
192                FunctionOrPath::RustPath(rust_path) => {
193                    let path_str = rust_path.to_string();
194                    // Check rules for the path
195                    if let Some(capabilities) = rules.match_symbol(&path_str) {
196                        for &capability in capabilities {
197                            self.caps
198                                .entry(capability)
199                                .or_default()
200                                .insert(Reason::PathMatchedRule(rust_path.clone()));
201                        }
202                    } else {
203                        // No rule matched
204
205                        let segments = rust_path.segments();
206                        let crate_name = CrateName::new(segments[0])
207                            .with_context(|| format!("mangled: {:?}", symbol.mangled))
208                            .with_context(|| format!("demangled: {:?}", symbol.demangled))?;
209
210                        if crate_name.is_standard_crate() {
211                            self.caps
212                                .entry(Capability::Any)
213                                .or_default()
214                                .insert(Reason::UmatchedStandardPath(rust_path.clone()));
215                        } else {
216                            // assume an external crate:
217                            self.unresolved_crates
218                                .entry(crate_name)
219                                .or_default()
220                                .insert(rust_path);
221                        }
222                    }
223                }
224            }
225        }
226
227        Ok(())
228    }
229
230    fn add_path(&mut self, rules: &SymbolRules, rust_path: RustPath) -> anyhow::Result<()> {
231        let path_str = rust_path.to_string();
232        // Check rules for the path
233        if let Some(capabilities) = rules.match_symbol(&path_str) {
234            for &capability in capabilities {
235                self.caps
236                    .entry(capability)
237                    .or_default()
238                    .insert(Reason::PathMatchedRule(rust_path.clone()));
239            }
240        } else {
241            // No rule matched - assume an external crate:
242            let segments = rust_path.segments();
243
244            let crate_name = segments[0];
245            let crate_name =
246                CrateName::new(crate_name).with_context(|| format!("path: {rust_path}"))?;
247            self.unresolved_crates
248                .entry(crate_name)
249                .or_default()
250                .insert(rust_path);
251        }
252
253        Ok(())
254    }
255}
256
257pub fn format_reasons(reasons: &Reasons) -> String {
258    let mut crates = vec![];
259    let mut path_matched_rules = vec![];
260    let mut symbol_matched_rules = vec![];
261    let mut unmatched_paths = vec![];
262    let mut unmatched_symbols = vec![];
263    let mut source_parse_errors = vec![];
264
265    for reason in reasons {
266        match reason {
267            Reason::Crate(crate_name) => {
268                crates.push(crate_name);
269            }
270            Reason::UmatchedStandardPath(path) => {
271                unmatched_paths.push(path);
272            }
273            Reason::UnmatchedSymbol(symbol) => {
274                unmatched_symbols.push(symbol);
275            }
276            Reason::PathMatchedRule(rust_path) => {
277                path_matched_rules.push(rust_path);
278            }
279            Reason::SymbolMatchedRule(symbol) => {
280                symbol_matched_rules.push(symbol);
281            }
282            Reason::SourceParseError(error) => {
283                source_parse_errors.push(error);
284            }
285        }
286    }
287
288    fn format_long_list<T: std::fmt::Display>(header: &str, reasons: &[T]) -> String {
289        let max_width = 60;
290        let mut string = format!("{header}:");
291        let mut num_left = reasons.len();
292        for reason in reasons.iter().reservoir_sample(5) {
293            if string.len() < max_width {
294                string += &format!(" {reason}");
295                num_left -= 1;
296            } else {
297                string += &format!(" โ€ฆ + {num_left} more");
298                break;
299            }
300        }
301        string
302    }
303
304    if !crates.is_empty() {
305        format_long_list("dependencies", &crates)
306    } else if !path_matched_rules.is_empty() {
307        format_long_list("rule for", &path_matched_rules)
308    } else if !symbol_matched_rules.is_empty() {
309        let symbol_matched_rules = symbol_matched_rules
310            .into_iter()
311            .map(|s| &s.demangled)
312            .collect_vec();
313        format_long_list("rule for", &symbol_matched_rules)
314    } else if !unmatched_paths.is_empty() {
315        format_long_list("unknown paths", &unmatched_paths)
316    } else if !unmatched_symbols.is_empty() {
317        let unmatched_symbols = unmatched_symbols
318            .into_iter()
319            .map(|s| &s.demangled)
320            .collect_vec();
321        format_long_list("unknown symbols", &unmatched_symbols)
322    } else if !source_parse_errors.is_empty() {
323        format_long_list("source parse error", &source_parse_errors)
324    } else {
325        unreachable!()
326    }
327}