Skip to main content

cargo_caps/
lib.rs

1use std::{
2    fs,
3    io::{Cursor, Read as _},
4};
5
6use anyhow::{Context as _, Result};
7use cargo_metadata::camino::Utf8Path;
8use object::{
9    Object as _, ObjectSymbol as _, SymbolKind as ObjectSymbolKind,
10    SymbolScope as ObjectSymbolScope,
11};
12
13pub use capability::{Capability, CapabilitySet};
14
15use crate::symbol::{Symbol, SymbolKind, SymbolScope};
16
17mod build_graph_analysis;
18mod cap_rule;
19mod capability;
20mod checker;
21mod commands;
22mod config;
23mod crate_name;
24mod demangle;
25mod print;
26mod reservoir_sample;
27mod rust_path;
28mod src_analysis;
29mod symbol;
30mod tree;
31
32pub use commands::Commands;
33use crate_name::CrateName;
34
35/// Extract symbols from an binary, e..g an executable, `.dylib`, or an `.rlib`.
36fn extract_symbols(binary_path: &Utf8Path) -> Result<Vec<Symbol>> {
37    let file_bytes =
38        fs::read(binary_path).with_context(|| format!("Failed to read {binary_path}"))?;
39
40    // Check if it's an ar archive (rlib files are ar archives)
41    let is_ar_archive = file_bytes.starts_with(b"!<arch>\n");
42
43    let mut symbols = Vec::new();
44
45    if is_ar_archive {
46        // .rlib
47        let mut archive = ar::Archive::new(Cursor::new(file_bytes));
48
49        // Extract and process each object file in the archive
50        while let Some(entry_result) = archive.next_entry() {
51            let mut entry = entry_result.context("Failed to read archive entry")?;
52
53            // Skip non-object files (like metadata files)
54            let header = entry.header();
55            let filename = String::from_utf8_lossy(header.identifier());
56
57            if !filename.ends_with(".o") {
58                continue;
59            }
60
61            // Read the object file data
62            let mut obj_data = Vec::new();
63            entry
64                .read_to_end(&mut obj_data)
65                .context("Failed to read object file from archive")?;
66
67            // Parse the object file and extract symbols
68            match object::File::parse(&*obj_data) {
69                Ok(file) => {
70                    collect_file_symbols(&mut symbols, &file);
71                }
72                Err(err) => {
73                    anyhow::bail!("Failed to pearse entry in {binary_path:?}: {err}")
74                }
75            }
76        }
77    } else {
78        // Assume an executable or dylib
79        let file =
80            object::File::parse(&*file_bytes).with_context(|| "Failed to parse binary file")?;
81        collect_file_symbols(&mut symbols, &file);
82    }
83
84    Ok(symbols)
85}
86
87/// Filter symbols based on scope and kind preferences
88pub fn filter_symbols(
89    symbols: Vec<Symbol>,
90    include_local: bool,     // TODO: remove bool args
91    include_all_kinds: bool, // TODO: remove bool args
92) -> Vec<Symbol> {
93    symbols
94        .into_iter()
95        .filter(|symbol| {
96            // Filter by scope - exclude local compilation symbols by default
97            let scope_allowed = include_local || !matches!(symbol.scope, SymbolScope::Compilation);
98
99            // Filter by kind - only include executable code and unknown by default
100            let kind_allowed = include_all_kinds
101                || matches!(
102                    symbol.kind,
103                    SymbolKind::Text | SymbolKind::Label | SymbolKind::Unknown
104                );
105
106            scope_allowed && kind_allowed
107        })
108        .collect()
109}
110
111fn collect_file_symbols(all_symbols: &mut Vec<Symbol>, file: &object::File<'_>) {
112    for symbol in file.symbols() {
113        if let Ok(name) = symbol.name()
114            && !name.is_empty()
115        {
116            let scope = match symbol.scope() {
117                ObjectSymbolScope::Unknown => SymbolScope::Unknown,
118                ObjectSymbolScope::Compilation => SymbolScope::Compilation,
119                ObjectSymbolScope::Linkage => SymbolScope::Linkage,
120                ObjectSymbolScope::Dynamic => SymbolScope::Dynamic,
121            };
122
123            #[expect(clippy::match_same_arms)]
124            let kind = match symbol.kind() {
125                ObjectSymbolKind::Unknown => SymbolKind::Unknown,
126                ObjectSymbolKind::Text => SymbolKind::Text,
127                ObjectSymbolKind::Data => SymbolKind::Data,
128                ObjectSymbolKind::Section => SymbolKind::Section,
129                ObjectSymbolKind::File => SymbolKind::File,
130                ObjectSymbolKind::Label => SymbolKind::Label,
131                ObjectSymbolKind::Tls => SymbolKind::Tls,
132                _ => SymbolKind::Unknown,
133            };
134
135            all_symbols.push(Symbol::with_metadata(name.to_owned(), scope, kind));
136        }
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn test_filter_symbols() {
146        let symbols = vec![
147            Symbol::with_metadata("func1".to_owned(), SymbolScope::Linkage, SymbolKind::Text),
148            Symbol::with_metadata(
149                "local_func".to_owned(),
150                SymbolScope::Compilation,
151                SymbolKind::Text,
152            ),
153            Symbol::with_metadata(
154                "data_var".to_owned(),
155                SymbolScope::Linkage,
156                SymbolKind::Data,
157            ),
158            Symbol::with_metadata("label1".to_owned(), SymbolScope::Dynamic, SymbolKind::Label),
159            Symbol::with_metadata(
160                "unknown_sym".to_owned(),
161                SymbolScope::Linkage,
162                SymbolKind::Unknown,
163            ),
164        ];
165
166        // Default filtering: exclude local compilation and non-executable (except unknown)
167        let filtered = filter_symbols(symbols.clone(), false, false);
168        assert_eq!(filtered.len(), 3); // func1, label1, unknown_sym
169
170        // Include local symbols
171        let filtered = filter_symbols(symbols.clone(), true, false);
172        assert_eq!(filtered.len(), 4); // func1, local_func, label1, unknown_sym
173
174        // Include all kinds
175        let filtered = filter_symbols(symbols.clone(), false, true);
176        assert_eq!(filtered.len(), 4); // func1, data_var, label1, unknown_sym
177
178        // Include everything
179        let filtered = filter_symbols(symbols, true, true);
180        assert_eq!(filtered.len(), 5); // all symbols
181    }
182}