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 rust_type;
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            if let Ok(file) = object::File::parse(&*obj_data) {
69                collect_file_symbols(&mut symbols, &file);
70            } else {
71                // TODO: Return error
72            }
73        }
74    } else {
75        // Assume an executable or dylib
76        let file =
77            object::File::parse(&*file_bytes).with_context(|| "Failed to parse binary file")?;
78        collect_file_symbols(&mut symbols, &file);
79    }
80
81    Ok(symbols)
82}
83
84/// Filter symbols based on scope and kind preferences
85pub fn filter_symbols(
86    symbols: Vec<Symbol>,
87    include_local: bool,
88    include_all_kinds: bool,
89) -> Vec<Symbol> {
90    symbols
91        .into_iter()
92        .filter(|symbol| {
93            // Filter by scope - exclude local compilation symbols by default
94            let scope_allowed = include_local || !matches!(symbol.scope, SymbolScope::Compilation);
95
96            // Filter by kind - only include executable code and unknown by default
97            let kind_allowed = include_all_kinds
98                || matches!(
99                    symbol.kind,
100                    SymbolKind::Text | SymbolKind::Label | SymbolKind::Unknown
101                );
102
103            scope_allowed && kind_allowed
104        })
105        .collect()
106}
107
108fn collect_file_symbols(all_symbols: &mut Vec<Symbol>, file: &object::File<'_>) {
109    for symbol in file.symbols() {
110        if let Ok(name) = symbol.name()
111            && !name.is_empty()
112        {
113            let scope = match symbol.scope() {
114                ObjectSymbolScope::Unknown => SymbolScope::Unknown,
115                ObjectSymbolScope::Compilation => SymbolScope::Compilation,
116                ObjectSymbolScope::Linkage => SymbolScope::Linkage,
117                ObjectSymbolScope::Dynamic => SymbolScope::Dynamic,
118            };
119
120            #[expect(clippy::match_same_arms)]
121            let kind = match symbol.kind() {
122                ObjectSymbolKind::Unknown => SymbolKind::Unknown,
123                ObjectSymbolKind::Text => SymbolKind::Text,
124                ObjectSymbolKind::Data => SymbolKind::Data,
125                ObjectSymbolKind::Section => SymbolKind::Section,
126                ObjectSymbolKind::File => SymbolKind::File,
127                ObjectSymbolKind::Label => SymbolKind::Label,
128                ObjectSymbolKind::Tls => SymbolKind::Tls,
129                _ => SymbolKind::Unknown,
130            };
131
132            all_symbols.push(Symbol::with_metadata(name.to_owned(), scope, kind));
133        }
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn test_filter_symbols() {
143        let symbols = vec![
144            Symbol::with_metadata("func1".to_owned(), SymbolScope::Linkage, SymbolKind::Text),
145            Symbol::with_metadata(
146                "local_func".to_owned(),
147                SymbolScope::Compilation,
148                SymbolKind::Text,
149            ),
150            Symbol::with_metadata(
151                "data_var".to_owned(),
152                SymbolScope::Linkage,
153                SymbolKind::Data,
154            ),
155            Symbol::with_metadata("label1".to_owned(), SymbolScope::Dynamic, SymbolKind::Label),
156            Symbol::with_metadata(
157                "unknown_sym".to_owned(),
158                SymbolScope::Linkage,
159                SymbolKind::Unknown,
160            ),
161        ];
162
163        // Default filtering: exclude local compilation and non-executable (except unknown)
164        let filtered = filter_symbols(symbols.clone(), false, false);
165        assert_eq!(filtered.len(), 3); // func1, label1, unknown_sym
166
167        // Include local symbols
168        let filtered = filter_symbols(symbols.clone(), true, false);
169        assert_eq!(filtered.len(), 4); // func1, local_func, label1, unknown_sym
170
171        // Include all kinds
172        let filtered = filter_symbols(symbols.clone(), false, true);
173        assert_eq!(filtered.len(), 4); // func1, data_var, label1, unknown_sym
174
175        // Include everything
176        let filtered = filter_symbols(symbols, true, true);
177        assert_eq!(filtered.len(), 5); // all symbols
178    }
179}