From 090ce0da854730c67f0e8668b3c12f314c0784b3 Mon Sep 17 00:00:00 2001 From: Hung Pham Date: Sun, 6 Sep 2026 22:31:45 +0700 Subject: [PATCH] Integrate with radare2 --- .github/workflows/ci.yml | 4 + Cargo.lock | 154 +++++-- Cargo.toml | 4 + crates/codegraph-binary/Cargo.toml | 23 + crates/codegraph-binary/src/cache.rs | 79 ++++ crates/codegraph-binary/src/config.rs | 64 +++ crates/codegraph-binary/src/extract.rs | 424 +++++++++++++++++++ crates/codegraph-binary/src/lib.rs | 71 ++++ crates/codegraph-binary/src/model.rs | 136 ++++++ crates/codegraph-binary/src/r2.rs | 74 ++++ crates/codegraph-binary/src/scan.rs | 57 +++ crates/codegraph-binary/tests/extract.rs | 208 +++++++++ crates/codegraph-extract/Cargo.toml | 4 +- crates/codegraph-extract/src/config.rs | 30 ++ crates/codegraph-extract/src/orchestrator.rs | 18 +- crates/codegraph-extract/src/walker.rs | 1 + crates/codegraph/src/main.rs | 4 +- 17 files changed, 1321 insertions(+), 34 deletions(-) create mode 100644 crates/codegraph-binary/Cargo.toml create mode 100644 crates/codegraph-binary/src/cache.rs create mode 100644 crates/codegraph-binary/src/config.rs create mode 100644 crates/codegraph-binary/src/extract.rs create mode 100644 crates/codegraph-binary/src/lib.rs create mode 100644 crates/codegraph-binary/src/model.rs create mode 100644 crates/codegraph-binary/src/r2.rs create mode 100644 crates/codegraph-binary/src/scan.rs create mode 100644 crates/codegraph-binary/tests/extract.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6b3a760f55..13098893fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -95,6 +95,8 @@ jobs: - uses: Swatinem/rust-cache@v2 - name: Install DB clients run: sudo apt-get update && sudo apt-get install -y postgresql-client mysql-client + - name: Install radare2 (binary analysis integration tests) + run: sudo apt-get install -y radare2 - name: Apply schema (postgres) run: | psql "postgres://postgres:postgres@127.0.0.1:5432/codegraph" -f sql/postgres/001-initial-schema.sql @@ -128,6 +130,8 @@ jobs: RUSTFLAGS: "-Cinstrument-coverage" TEST_REDIS_DSN: "redis://127.0.0.1:6379" run: cargo llvm-cov test -p codegraph-graph --features redis --test redis --no-report -- --ignored --nocapture + - name: Binary integration tests (radare2) + run: cargo llvm-cov test -p codegraph-binary --test extract --no-report -- --ignored --nocapture - name: Generate coverage report (lcov) run: | mkdir -p ./target/coverage diff --git a/Cargo.lock b/Cargo.lock index 8cd4101f4c..641981dfb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -113,7 +113,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -124,7 +124,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -518,6 +518,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "bstr" version = "1.12.1" @@ -765,6 +774,22 @@ dependencies = [ "tokio", ] +[[package]] +name = "codegraph-binary" +version = "2.0.6" +dependencies = [ + "camino", + "codegraph-core", + "codegraph-graph", + "ignore", + "r2pipe", + "serde", + "serde_json", + "sha2 0.11.0", + "tempfile", + "tracing", +] + [[package]] name = "codegraph-context" version = "2.0.6" @@ -793,6 +818,7 @@ name = "codegraph-extract" version = "2.0.6" dependencies = [ "camino", + "codegraph-binary", "codegraph-core", "codegraph-graph", "getrandom 0.2.17", @@ -1009,7 +1035,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -1068,6 +1094,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "const-random" version = "0.1.18" @@ -1395,6 +1427,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "darling" version = "0.20.11" @@ -1533,7 +1574,7 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid", + "const-oid 0.9.6", "pem-rfc7468 0.7.0", "zeroize", ] @@ -1591,12 +1632,23 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid 0.10.2", + "crypto-common 0.2.2", +] + [[package]] name = "dirs" version = "5.0.1" @@ -1636,7 +1688,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1734,7 +1786,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2284,7 +2336,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -2347,6 +2399,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.11.0" @@ -2681,7 +2742,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2798,6 +2859,16 @@ dependencies = [ "cc", ] +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + [[package]] name = "libm" version = "0.2.16" @@ -2971,7 +3042,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest", + "digest 0.10.7", ] [[package]] @@ -3203,7 +3274,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3820,7 +3891,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3844,6 +3915,20 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "r2pipe" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b823a15a98a6462385ba5568635c1da5bb165066a38a65b5c6004e126ed32c0" +dependencies = [ + "libc", + "libloading", + "serde", + "serde_derive", + "serde_json", + "thiserror 1.0.69", +] + [[package]] name = "rand" version = "0.8.7" @@ -4311,8 +4396,8 @@ version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ - "const-oid", - "digest", + "const-oid 0.9.6", + "digest 0.10.7", "num-bigint-dig", "num-integer", "num-traits", @@ -4355,7 +4440,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4593,7 +4678,7 @@ checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", ] [[package]] @@ -4610,7 +4695,18 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -4634,7 +4730,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", + "digest 0.10.7", "rand_core 0.6.4", ] @@ -4686,7 +4782,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4775,7 +4871,7 @@ dependencies = [ "percent-encoding", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", "thiserror 2.0.18", "tokio", @@ -4812,7 +4908,7 @@ dependencies = [ "quote", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "sqlx-core", "sqlx-mysql", "sqlx-postgres", @@ -4834,7 +4930,7 @@ dependencies = [ "byteorder", "bytes", "crc", - "digest", + "digest 0.10.7", "dotenvy", "either", "futures-channel", @@ -4855,7 +4951,7 @@ dependencies = [ "rsa", "serde", "sha1", - "sha2", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", @@ -4892,7 +4988,7 @@ dependencies = [ "rand 0.8.7", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", @@ -5074,7 +5170,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5084,7 +5180,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -6138,7 +6234,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index bc7b40479c..35c9e17635 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "crates/codegraph-mcp", "crates/codegraph-bench", "crates/codegraph-installer", + "crates/codegraph-binary", "crates/codegraph", ] @@ -78,6 +79,9 @@ ignore = "0.4" globset = "0.4" walkdir = "2" +# radare2 integration +r2pipe = "0.8" + # misc camino = { version = "1", features = ["serde1"] } dashmap = "6" diff --git a/crates/codegraph-binary/Cargo.toml b/crates/codegraph-binary/Cargo.toml new file mode 100644 index 0000000000..8ffd98f2dc --- /dev/null +++ b/crates/codegraph-binary/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "codegraph-binary" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true + +[lints.rust] +warnings = "deny" + +[dependencies] +codegraph-core = { path = "../codegraph-core" } +codegraph-graph = { path = "../codegraph-graph" } +serde = { workspace = true } +serde_json = { workspace = true } +r2pipe = "0.8" +tracing = { workspace = true } +camino = { workspace = true } +ignore = { workspace = true } +sha2 = "0.11" + +[dev-dependencies] +tempfile = "3" diff --git a/crates/codegraph-binary/src/cache.rs b/crates/codegraph-binary/src/cache.rs new file mode 100644 index 0000000000..81c26d425a --- /dev/null +++ b/crates/codegraph-binary/src/cache.rs @@ -0,0 +1,79 @@ +//! Cache kết quả phân tích binary theo (path, mtime, size). +use crate::config::BinaryConfig; +use camino::Utf8Path; +use codegraph_graph::ParseResult; +use sha2::{Digest, Sha256}; +use std::fs; +use std::path::Path; + +pub fn cache_path(root: &Utf8Path, path: &Path) -> camino::Utf8PathBuf { + let key = format!("{}|{}|{}", path.display(), mtime(path), size(path)); + let hash = Sha256::digest(key.as_bytes()); + let hex: String = hash.iter().map(|b| format!("{b:02x}")).collect(); + root.join(".codegraph") + .join("binary-cache") + .join(format!("{hex}.json")) +} + +pub fn load(path: &camino::Utf8Path) -> Option { + let text = fs::read_to_string(path).ok()?; + serde_json::from_str(&text).ok() +} + +pub fn store(path: &camino::Utf8Path, result: &ParseResult) -> std::io::Result<()> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + fs::write(path, serde_json::to_string(result).unwrap()) +} + +pub fn is_cached(root: &Utf8Path, path: &Path, cfg: &BinaryConfig) -> bool { + if !cfg.cache { + return false; + } + let p = cache_path(root, path); + p.exists() +} + +fn mtime(path: &Path) -> u64 { + fs::metadata(path) + .map(|m| { + m.modified() + .map(|t| { + t.duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0) + }) + .unwrap_or(0) + }) + .unwrap_or(0) +} +fn size(path: &Path) -> u64 { + fs::metadata(path).map(|m| m.len()).unwrap_or(0) +} + +#[cfg(test)] +mod tests { + use super::*; + use camino::Utf8PathBuf; + + #[test] + fn roundtrip() { + let dir = tempfile::tempdir().unwrap(); + let root = Utf8PathBuf::from_path_buf(dir.path().to_path_buf()).unwrap(); + let result = ParseResult { + path: "/bin/ls".to_string(), + language: "binary".to_string(), + bytes: 1_000_000, + lines: 0, + symbols: vec![], + chains: Default::default(), + calls: vec![], + }; + let p = cache_path(&root, std::path::Path::new("/bin/ls")); + store(&p, &result).unwrap(); + let loaded = load(&p).unwrap(); + assert_eq!(loaded.path, result.path); + assert_eq!(loaded.bytes, result.bytes); + } +} diff --git a/crates/codegraph-binary/src/config.rs b/crates/codegraph-binary/src/config.rs new file mode 100644 index 0000000000..af2119ad23 --- /dev/null +++ b/crates/codegraph-binary/src/config.rs @@ -0,0 +1,64 @@ +//! Cấu hình phân tích binary (`.codegraph/config.toml` section `[binary]`). + +use serde::Deserialize; + +/// Độ sâu phân tích của radare2 cho một binary. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum AnalysisDepth { + /// Phân tích đầy đủ (`aaa`). Chậm hơn nhưng chính xác nhất. + #[default] + Aaa, + /// Nhanh hơn: `af` + `aar` + `aac` (không chạy `aaaa`). Phù hợp binary lớn. + Fast, +} + +impl AnalysisDepth { + /// Chuỗi lệnh tương ứng với r2. + pub fn command(self) -> &'static str { + match self { + Self::Aaa => "aaa", + Self::Fast => "af; aar; aac", + } + } +} + +impl std::fmt::Display for AnalysisDepth { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +impl AnalysisDepth { + pub fn as_str(self) -> &'static str { + match self { + Self::Aaa => "aaa", + Self::Fast => "fast", + } + } +} + +/// Cấu hình section `[binary]` trong `.codegraph/config.toml`. +#[derive(Debug, Clone, Deserialize)] +#[serde(default)] +pub struct BinaryConfig { + /// Bật tắt việc phân tích binary bằng radare2 khi index (mặc định bật). + pub enabled: bool, + /// Độ sâu phân tích (mặc định `aaa`). + pub depth: AnalysisDepth, + /// Xây dựng marker IF/LOOP/SWITCH từ CFG của mỗi function (`pdfj`). + pub cfg_markers: bool, + /// Cache kết quả phân tích theo (path, mtime, size) để tránh chạy `aaa` lại. + pub cache: bool, +} + +impl Default for BinaryConfig { + fn default() -> Self { + Self { + enabled: true, + depth: AnalysisDepth::default(), + cfg_markers: true, + cache: true, + } + } +} diff --git a/crates/codegraph-binary/src/extract.rs b/crates/codegraph-binary/src/extract.rs new file mode 100644 index 0000000000..27299be13b --- /dev/null +++ b/crates/codegraph-binary/src/extract.rs @@ -0,0 +1,424 @@ +//! Chuyển đổi output r2 → `ParseResult` cho `GraphIndex::ingest`. + +use crate::config::AnalysisDepth; +use crate::model::*; +use crate::r2::R2Session; +use codegraph_core::{Annotation, CallRecord, EffectType, Error, Symbol, SymbolKind, SYMBOL_BASE}; +use codegraph_graph::ParseResult; +use serde_json::Value; +use std::collections::{HashMap, HashSet}; +use std::path::Path; + +/// Trích xuất toàn bộ thông tin từ binary thành `ParseResult`. +/// Gọi `aaa` một lần trong session, rồi query. +pub fn extract_binary( + path: &Path, + depth: AnalysisDepth, + cfg_markers: bool, +) -> Result { + let mut session = R2Session::open(path)?; + let file_len = path.metadata().map(|m| m.len()).unwrap_or(0); + let result = do_extract(&mut session, path, file_len, cfg_markers, depth)?; + Ok(result) +} + +/// Trích xuất với session đã mở (dùng cho cache warm, kiểm thử). +pub fn extract_binary_with_session( + path: &Path, + session: &mut dyn R2Client, + depth: AnalysisDepth, + cfg_markers: bool, +) -> Result { + let file_len = path.metadata().map(|m| m.len()).unwrap_or(0); + do_extract(session, path, file_len, cfg_markers, depth) +} + +/// Trait trừu tượng cho r2 client — giúp mock trong test mà không cần r2 thật. +pub trait R2Client { + fn cmd(&mut self, cmd: &str) -> Result; + fn cmdj(&mut self, cmd: &str) -> Result; + + /// Phân tích binary (chỉ gọi 1 lần trong đời session). + fn analyze(&mut self, depth: AnalysisDepth) -> Result<(), Error> { + self.cmd(depth.command())?; + Ok(()) + } +} + +impl R2Client for R2Session { + fn cmd(&mut self, cmd: &str) -> Result { + R2Session::cmd(self, cmd) + } + fn cmdj(&mut self, cmd: &str) -> Result { + R2Session::cmdj(self, cmd) + } +} + +fn do_extract( + session: &mut dyn R2Client, + path: &Path, + file_len: u64, + cfg_markers: bool, + depth: AnalysisDepth, +) -> Result { + let path_str = path + .to_str() + .ok_or_else(|| Error::Parse("path không phải UTF-8".to_string()))?; + + session.analyze(depth)?; + + // 1. Functions (`aflj`) + let functions = parse_aflj(session)?; + let mut symbols: Vec = Vec::new(); + let mut chains: HashMap> = HashMap::new(); + let mut calls: Vec = Vec::new(); + let mut fn_by_addr: HashMap = HashMap::new(); + let mut fn_id_to_name: HashMap = HashMap::new(); + let mut next_id = SYMBOL_BASE + 1; + + for entry in &functions { + let addr = entry.offset.unwrap_or(0); + let raw_name = entry + .name + .clone() + .unwrap_or_else(|| format!("fcn.{addr:x}")); + // PLT thunk của import — đã có symbol riêng từ `iij`, bỏ qua. + if raw_name.starts_with("sym.imp.") { + continue; + } + let name = strip_r2_prefix(&raw_name); + let size = entry.size.unwrap_or(0); + let sig = build_signature(addr, size, entry); + let id = next_id; + next_id += 1; + fn_by_addr.insert(addr, id); + fn_id_to_name.insert(id, name.clone()); + symbols.push(Symbol { + id, + name, + kind: SymbolKind::Function, + scope: codegraph_core::ScopeLevel::Global, + scope_id: 0, + type_ref: 0, + type_name: None, + file: path_str.to_string(), + line: addr.try_into().unwrap_or(0), + end_line: addr.saturating_add(size).try_into().unwrap_or(u32::MAX), + signature: Some(sig), + doc: None, + annotations: Vec::new(), + language: "binary".to_string(), + }); + } + + // 2. Imports (`iij`) — tạo symbol; bỏ qua function entry "sym.imp." + let imports = parse_iij(session)?; + let mut import_name_to_id: HashMap = HashMap::new(); + let mut plt_by_addr: HashMap = HashMap::new(); + for imp in &imports { + let clean = imp.import.as_deref().unwrap_or("?"); + let count = imports + .iter() + .filter(|i| i.import.as_deref() == Some(clean)) + .count(); + let name = if count > 1 { + format!("{clean} ({})", imp.lib.as_deref().unwrap_or("?")) + } else { + clean.to_string() + }; + let id = next_id; + next_id += 1; + import_name_to_id.insert(clean.to_string(), id); + if let Some(plt) = imp.plt { + plt_by_addr.insert(plt, name.clone()); + } + symbols.push(Symbol { + id, + name, + kind: SymbolKind::Function, + scope: codegraph_core::ScopeLevel::Global, + scope_id: 0, + type_ref: 0, + type_name: None, + file: path_str.to_string(), + line: imp.plt.unwrap_or(0).try_into().unwrap_or(0), + end_line: 0, + signature: Some(format!("import ({})", imp.lib.as_deref().unwrap_or(""))), + doc: imp.lib.clone(), + annotations: vec![Annotation { + name: "import".to_string(), + args: HashMap::new(), + line: 0, + }], + language: "binary".to_string(), + }); + } + + // 3. Strings (`izj`) + let strings = parse_izj(session)?; + for s in &strings { + let id = next_id; + next_id += 1; + let vaddr = s.vaddr.unwrap_or(0); + symbols.push(Symbol { + id, + name: format!("str:{vaddr:x}"), + kind: SymbolKind::Constant, + scope: codegraph_core::ScopeLevel::Global, + scope_id: 0, + type_ref: 0, + type_name: None, + file: path_str.to_string(), + line: vaddr.try_into().unwrap_or(0), + end_line: 0, + signature: s.type_.clone().map(|t| format!("{t} string")), + doc: s.string.as_deref().map(|s| s.chars().take(200).collect()), + annotations: Vec::new(), + language: "binary".to_string(), + }); + } + + // 4. Calls + chains + let maps = FnMaps { + fn_by_addr: &fn_by_addr, + fn_id_to_name: &fn_id_to_name, + plt_by_addr: &plt_by_addr, + import_name_to_id: &import_name_to_id, + }; + if cfg_markers { + build_chains_with_cfg(session, &functions, &maps, &mut chains, &mut calls)?; + } else { + build_chains_from_graph(session, &functions, &maps, &mut chains, &mut calls)?; + } + + // Chain cho symbol không có call (import/string) + for s in &symbols { + chains.entry(s.id).or_insert_with(|| vec![s.id]); + } + + Ok(ParseResult { + path: path_str.to_string(), + language: "binary".to_string(), + bytes: file_len, + lines: 0, + symbols, + chains, + calls, + }) +} + +fn parse_array(v: Value) -> Result, Error> { + match v { + Value::Array(a) => Ok(a + .into_iter() + .filter_map(|v| serde_json::from_value::(v).ok()) + .collect()), + _ => Ok(Vec::new()), + } +} + +fn parse_aflj(session: &mut dyn R2Client) -> Result, Error> { + parse_array(session.cmdj("aflj")?) +} + +fn parse_iij(session: &mut dyn R2Client) -> Result, Error> { + parse_array(session.cmdj("iij")?) +} + +fn parse_izj(session: &mut dyn R2Client) -> Result, Error> { + parse_array(session.cmdj("izj")?) +} + +fn build_signature(addr: u64, size: u64, entry: &FnEntry) -> String { + let mut parts = vec![format!("0x{addr:x}")]; + if size > 0 { + parts.push(format!("sz={size}")); + } + if let Some(cc) = entry.cc { + parts.push(format!("cc={cc}")); + } + if let Some(ct) = &entry.calltype { + parts.push(ct.clone()); + } + if let Some(sig) = &entry.signature { + parts.push(sig.clone()); + } + parts.join(" ") +} + +fn strip_r2_prefix(name: &str) -> String { + name.strip_prefix("sym.").unwrap_or(name).to_string() +} + +/// Bản đồ tra cứu từ address/name sang symbol id — gom parameter cho chain builder. +struct FnMaps<'a> { + fn_by_addr: &'a HashMap, + fn_id_to_name: &'a HashMap, + plt_by_addr: &'a HashMap, + import_name_to_id: &'a HashMap, +} + +/// Xây chain từ `pdfj` từng function (marker từ CFG). +fn build_chains_with_cfg( + session: &mut dyn R2Client, + functions: &[FnEntry], + maps: &FnMaps, + chains: &mut HashMap>, + calls: &mut Vec, +) -> Result<(), Error> { + for entry in functions { + let addr = entry.offset.unwrap_or(0); + let Some(&func_id) = maps.fn_by_addr.get(&addr) else { + continue; + }; + let ops: Vec = session + .cmdj(&format!("pdfj @ {addr}"))? + .get("ops") + .and_then(|o| o.as_array()) + .cloned() + .unwrap_or_default() + .into_iter() + .filter_map(|v| serde_json::from_value::(v).ok()) + .collect(); + let mut chain = vec![func_id]; + let mut local_calls = Vec::new(); + let mut seen = HashSet::new(); + + for op in &ops { + let off = op.offset.unwrap_or(0); + seen.insert(off); + if let Some(t) = &op.type_ { + match t.as_str() { + "call" => { + let (_callee_id, callee_name) = + resolve_call_target(op.jump.or(op.ptr), maps); + let pos = chain.len(); + chain.push(0); + local_calls.push(CallRecord { + caller_id: func_id, + call_name: callee_name, + position: pos, + arg_exprs: Vec::new(), + line: off.try_into().unwrap_or(0), + condition: op.disasm.clone(), + is_loop_body: false, + effect: EffectType::None, + effect_desc: None, + target_class: None, + target_method: None, + }); + } + "cjmp" => { + chain.push(codegraph_core::MARKER_IF_TRUE); + } + "jmp" => { + if let Some(t) = op.jump { + if seen.contains(&t) && t < addr { + chain.push(codegraph_core::MARKER_LOOP_BACK); + } + } + } + "ret" | "uret" => { + chain.push(codegraph_core::MARKER_RETURN); + } + "swi" | "syscall" => { + chain.push(codegraph_core::MARKER_THROW); + } + _ => {} + } + } + } + chains.insert(func_id, chain); + calls.append(&mut local_calls); + } + Ok(()) +} + +/// Xây chain nhẹ từ `agCj` (call graph edges) — không có marker CFG. +fn build_chains_from_graph( + session: &mut dyn R2Client, + functions: &[FnEntry], + maps: &FnMaps, + chains: &mut HashMap>, + calls: &mut Vec, +) -> Result<(), Error> { + let edges: Vec = session + .cmdj("agCj")? + .get("edges") + .and_then(|e| e.as_array()) + .cloned() + .unwrap_or_default() + .into_iter() + .filter_map(|v| serde_json::from_value::(v).ok()) + .collect(); + + let mut by_caller: HashMap> = HashMap::new(); + for edge in &edges { + let from = edge.from.unwrap_or(0); + let to = edge.to.unwrap_or(0); + by_caller.entry(from).or_default().push(to); + } + + for entry in functions { + let addr = entry.offset.unwrap_or(0); + let Some(&func_id) = maps.fn_by_addr.get(&addr) else { + continue; + }; + let mut chain = vec![func_id]; + for &to in by_caller.get(&addr).into_iter().flat_map(|v| v.iter()) { + let call_name = resolve_call_name(to, maps); + let pos = chain.len(); + chain.push(0); + calls.push(CallRecord { + caller_id: func_id, + call_name, + position: pos, + arg_exprs: Vec::new(), + line: addr.try_into().unwrap_or(0), + condition: None, + is_loop_body: false, + effect: EffectType::None, + effect_desc: None, + target_class: None, + target_method: None, + }); + } + chains.insert(func_id, chain); + } + Ok(()) +} + +fn resolve_call_target(target: Option, maps: &FnMaps) -> (u64, String) { + let addr = match target { + Some(a) => a, + None => return (0, String::new()), + }; + // Call tới import đi qua PLT stub — resolve theo plt addr. + if let Some(name) = maps.plt_by_addr.get(&addr) { + let id = maps.import_name_to_id.get(name).copied().unwrap_or(0); + return (id, name.clone()); + } + if let Some(&fid) = maps.fn_by_addr.get(&addr) { + let name = maps + .fn_id_to_name + .get(&fid) + .cloned() + .unwrap_or_else(|| format!("sub_{addr:x}")); + return (fid, name); + } + (0, format!("sub_{addr:x}")) +} + +fn resolve_call_name(addr: u64, maps: &FnMaps) -> String { + if let Some(name) = maps.plt_by_addr.get(&addr) { + return name.clone(); + } + if let Some(&fid) = maps.fn_by_addr.get(&addr) { + return maps + .fn_id_to_name + .get(&fid) + .cloned() + .unwrap_or_else(|| format!("sub_{addr:x}")); + } + format!("sub_{addr:x}") +} diff --git a/crates/codegraph-binary/src/lib.rs b/crates/codegraph-binary/src/lib.rs new file mode 100644 index 0000000000..3f662e287e --- /dev/null +++ b/crates/codegraph-binary/src/lib.rs @@ -0,0 +1,71 @@ +//! Phân tích binary bằng radare2 — chuyển functions/imports/strings/call graph +//! thành `ParseResult` để `GraphIndex::ingest` nạp vào semantic graph. +//! +//! ## Cài đặt +//! Yêu cầu `radare2` trong PATH (check bằng `codegraph doctor`). +//! +//! ## Ví dụ +//! ```rust,no_run +//! use codegraph_binary::{extract_binary, config::AnalysisDepth}; +//! use std::path::Path; +//! +//! let result = extract_binary(Path::new("/bin/ls"), AnalysisDepth::Aaa, true)?; +//! // `result` nạp thẳng vào GraphIndex::ingest +//! # Ok::<_, codegraph_core::Error>(()) +//! ``` + +pub mod cache; +pub mod config; +pub mod extract; +pub mod model; +pub mod r2; +pub mod scan; + +pub use crate::extract::extract_binary; +use camino::Utf8Path; +use codegraph_graph::ParseResult; +use tracing::warn; + +/// Duyệt các file binary trong workspace, phân tích từng file → `ParseResult`. +/// Gọi 1 lần ở orchestrator. +pub fn collect_binaries( + root: &Utf8Path, + cfg: &BinaryConfig, +) -> (Vec, u64 /* skipped */) { + if !cfg.enabled { + return (Vec::new(), 0); + } + if !r2_available() { + warn!( + "radare2 không có trong PATH — bỏ qua phân tích binary. Cài: brew install radare2 / apt install radare2" + ); + return (Vec::new(), 0); + } + let files = scan::find_binaries(root); + let mut results = Vec::new(); + let mut skipped = 0u64; + for path in files { + if cache::is_cached(root, path.as_std_path(), cfg) { + if let Some(cached) = cache::load(&cache::cache_path(root, path.as_std_path())) { + results.push(cached); + continue; + } + } + match extract_binary(path.as_std_path(), cfg.depth, cfg.cfg_markers) { + Ok(res) => { + if cfg.cache { + let _ = cache::store(&cache::cache_path(root, path.as_std_path()), &res); + } + results.push(res); + } + Err(e) => { + tracing::warn!("không phân tích được {}: {e}", path); + skipped += 1; + } + } + } + (results, skipped) +} + +pub use config::{AnalysisDepth, BinaryConfig}; +pub use r2::{r2_available, r2_version}; diff --git a/crates/codegraph-binary/src/model.rs b/crates/codegraph-binary/src/model.rs new file mode 100644 index 0000000000..262db4ff78 --- /dev/null +++ b/crates/codegraph-binary/src/model.rs @@ -0,0 +1,136 @@ +//! Các struct JSON lenient khi parse output r2. +//! Mọi field là `Option` vì schema r2 thay đổi theo version. + +use serde::Deserialize; + +/// Metadata binary từ lệnh `ij`. +#[derive(Debug, Deserialize, Default)] +pub struct BinInfo { + pub core: Option, + pub bin: Option, +} + +#[derive(Debug, Deserialize, Default)] +pub struct CoreInfo { + pub format: Option, + pub arch: Option, + pub bits: Option, + pub os: Option, +} + +#[derive(Debug, Deserialize, Default)] +pub struct BinMeta { + pub arch: Option, + pub bits: Option, + pub os: Option, + pub lang: Option, + pub compiler: Option, + pub machine: Option, + pub libs: Option>, + pub imports: Option, + pub symbols: Option, + pub entries: Option, + pub sections: Option, +} + +/// Danh sách function từ `aflj`. +#[derive(Debug, Deserialize)] +pub struct FnEntry { + pub offset: Option, + pub name: Option, + pub size: Option, + pub realsz: Option, + pub nbbs: Option, + pub edges: Option, + pub cc: Option, + pub calltype: Option, + pub signature: Option, + pub nargs: Option, + pub nlocals: Option, + pub ninstrs: Option, + pub is_noreturn: Option, +} + +/// Một xref từ `axtj` / `axfj`. +#[derive(Debug, Deserialize)] +pub struct Xref { + pub from: Option, + pub to: Option, + #[serde(rename = "type")] + pub type_: Option, + pub fcn_addr: Option, + pub fcn_name: Option, + pub refname: Option, + pub flag: Option, + pub opcode: Option, +} + +/// Entry import từ `iij`. +#[derive(Debug, Deserialize)] +pub struct ImportEntry { + pub import: Option, + pub ordinal: Option, + pub bind: Option, + #[serde(rename = "type")] + pub type_: Option, + pub lib: Option, + pub plt: Option, +} + +/// Symbol từ `isj`. +#[derive(Debug, Deserialize)] +pub struct SymEntry { + pub name: Option, + pub demname: Option, + pub ordinal: Option, + pub bind: Option, + #[serde(rename = "type")] + pub type_: Option, + pub size: Option, + pub addr: Option, + pub is_imported: Option, +} + +/// String từ `izj` / `izzj`. +#[derive(Debug, Deserialize)] +pub struct StrEntry { + pub vaddr: Option, + pub paddr: Option, + pub size: Option, + pub length: Option, + pub section: Option, + #[serde(rename = "type")] + pub type_: Option, + pub string: Option, +} + +/// Call graph edge từ `agCj`. +#[derive(Debug, Deserialize)] +pub struct CallGraphEdge { + pub from: Option, + pub to: Option, +} + +/// Một lệnh disasm trong `pdfj.ops`. +#[derive(Debug, Deserialize)] +pub struct DisasmOp { + pub offset: Option, + pub size: Option, + pub esil: Option, + pub bytes: Option, + #[serde(rename = "type")] + pub type_: Option, + pub disasm: Option, + pub ptr: Option, + pub val: Option, + pub refptr: Option, + pub reference: Option, + pub jump: Option, + pub fail: Option, + pub flag: Option, + pub true_: Option, + pub false_: Option, +} + +/// JSON gốc dạng `Value` cho phép linh hoạt. +pub type Json = serde_json::Value; diff --git a/crates/codegraph-binary/src/r2.rs b/crates/codegraph-binary/src/r2.rs new file mode 100644 index 0000000000..952a90b7a5 --- /dev/null +++ b/crates/codegraph-binary/src/r2.rs @@ -0,0 +1,74 @@ +//! Wrapper quanh r2pipe: phiên `r2 -q0` persistent để query binary. + +use codegraph_core::Error; +use r2pipe::{R2Pipe, R2PipeSpawnOptions}; +use serde_json::Value as Json; +use std::path::Path; +use tracing::debug; + +/// Session r2 — spawn một process `r2 -q0` và giữ kết nối stdin/stdout. +pub struct R2Session { + inner: R2Pipe, +} + +impl R2Session { + /// Mở session với binary tại `path`. Spawn `r2 -q0 `. + /// + /// Lỗi nếu `r2` không có trong PATH — thông báo cài đặt cụ thể. + pub fn open(path: &Path) -> Result { + let path_str = path + .to_str() + .ok_or_else(|| Error::Parse(format!("path không phải UTF-8: {}", path.display())))?; + let opts = R2PipeSpawnOptions { + exepath: "r2".to_string(), + args: vec!["-N", "-e", "scr.color=0", "-e", "scr.utf8=0"], + }; + let inner = R2Pipe::spawn(path_str, Some(opts)) + .map_err(|e| Error::Parse(format!("không thể spawn r2 cho {}: {e}. Hãy cài radare2: brew install radare2 / apt install radare2", path.display())))?; + debug!("r2 session opened for {}", path.display()); + Ok(Self { inner }) + } + + /// Gửi lệnh thô, trả về chuỗi response (đã strip NUL). + pub fn cmd(&mut self, cmd: &str) -> Result { + self.inner + .cmd(cmd) + .map_err(|e| Error::Parse(format!("r2 cmd `{cmd}` failed: {e}"))) + } + + /// Gửi lệnh, parse JSON response. + pub fn cmdj(&mut self, cmd: &str) -> Result { + self.inner + .cmdj(cmd) + .map_err(|e| Error::Parse(format!("r2 cmdj `{cmd}` failed: {e}"))) + } + + /// Phân tích binary theo `depth` (chỉ gọi 1 lần trong đời session). + pub fn analyze(&mut self, depth: crate::config::AnalysisDepth) -> Result<(), Error> { + let cmd = depth.command(); + debug!("running r2 analysis: {cmd}"); + self.cmd(cmd)?; + Ok(()) + } +} + +/// Kiểm tra `r2` có trong PATH không (chạy `r2 -v`). +pub fn r2_available() -> bool { + std::process::Command::new("r2") + .arg("-v") + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status() + .map(|s| s.success()) + .unwrap_or(false) +} + +/// Version của r2 (chuỗi từ `r2 -v`), nếu có. +pub fn r2_version() -> Option { + let out = std::process::Command::new("r2").arg("-v").output().ok()?; + if !out.status.success() { + return None; + } + let s = String::from_utf8(out.stdout).ok()?; + Some(s.lines().next().unwrap_or_default().to_string()) +} diff --git a/crates/codegraph-binary/src/scan.rs b/crates/codegraph-binary/src/scan.rs new file mode 100644 index 0000000000..346b94e603 --- /dev/null +++ b/crates/codegraph-binary/src/scan.rs @@ -0,0 +1,57 @@ +//! Scanner file nhị phân trong workspace (dựa vào magic bytes). +use camino::{Utf8Path, Utf8PathBuf}; +use ignore::WalkBuilder; + +/// Các magic bytes nhận diện binary: ELF, PE (MZ), Mach-O, fat Mach-O. +const MAGICS: &[&[u8]] = &[ + b"\x7fELF", // ELF + b"MZ", // PE / DOS + b"\xfe\xed\xfa\xce", // Mach-O little + b"\xcf\xfa\xed\xfe", // Mach-O big + b"\xca\xfe\xba\xbe", // fat Mach-O +]; + +/// Duyệt `root` (cùng ignore rules với walker) trả về các file nhị phân. +pub fn find_binaries(root: &Utf8Path) -> Vec { + let mut out = Vec::new(); + let walker = WalkBuilder::new(root) + .hidden(true) + .git_ignore(true) + .git_exclude(true) + .parents(true) + .add_custom_ignore_filename(".codegraphignore") + .build(); + for entry in walker.flatten() { + let path = entry.path(); + if !entry.file_type().map(|t| t.is_file()).unwrap_or(false) { + continue; + } + let bytes = match std::fs::read(path) { + Ok(b) if b.len() >= 4 => b, + _ => continue, + }; + if MAGICS.iter().any(|m| bytes.starts_with(m)) { + out.push(Utf8PathBuf::from_path_buf(path.to_path_buf()).unwrap()); + } + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + + #[test] + fn skips_source_and_finds_elf() { + let dir = tempfile::tempdir().unwrap(); + let root = Utf8PathBuf::from_path_buf(dir.path().to_path_buf()).unwrap(); + let mut f = std::fs::File::create(root.join("src.rs")).unwrap(); + f.write_all(b"fn main() {}").unwrap(); + let mut elf = std::fs::File::create(root.join("app")).unwrap(); + elf.write_all(b"\x7fELF\x02\x01\x01\x00").unwrap(); + let found = find_binaries(&root); + assert_eq!(found.len(), 1); + assert!(found[0].ends_with("app")); + } +} diff --git a/crates/codegraph-binary/tests/extract.rs b/crates/codegraph-binary/tests/extract.rs new file mode 100644 index 0000000000..c37d8cffcb --- /dev/null +++ b/crates/codegraph-binary/tests/extract.rs @@ -0,0 +1,208 @@ +//! Unit test cho extract mapping với mock R2Client — không cần r2 thật. + +use codegraph_binary::config::AnalysisDepth; +use codegraph_binary::extract::{extract_binary_with_session, R2Client}; +use codegraph_core::{SymbolKind, MARKER_IF_TRUE, MARKER_RETURN}; +use codegraph_graph::ParseResult; +use serde_json::{json, Value}; +use std::collections::HashMap; + +/// Mock r2 client trả fixture JSON theo command. +struct MockR2 { + responses: HashMap, +} + +impl MockR2 { + fn new() -> Self { + let mut responses = HashMap::new(); + // aflj: main + helper + PLT stub của puts + responses.insert( + "aflj".to_string(), + json!([ + {"offset": 4198496, "name": "main", "size": 64, "cc": 1.0, "calltype": "cdecl"}, + {"offset": 4198560, "name": "fcn.00401160", "size": 32, "cc": 2.0}, + {"offset": 4196112, "name": "sym.imp.LIBC.so.6_puts", "size": 16} + ]), + ); + // iij: 1 import puts + responses.insert( + "iij".to_string(), + json!([ + {"import": "puts", "bind": "NONE", "type": "FUNC", "lib": "LIBC.so.6", "plt": 4196112} + ]), + ); + // izj: 1 string + responses.insert( + "izj".to_string(), + json!([ + {"vaddr": 4202496, "paddr": 8192, "size": 14, "type": "ascii", "string": "hello world\n"} + ]), + ); + // agCj: main → helper, main → puts(plt) + responses.insert( + "agCj".to_string(), + json!({"edges": [ + {"from": 4198496, "to": 4198560}, + {"from": 4198496, "to": 4196112} + ]}), + ); + // pdfj main: call + return + branch + responses.insert( + "pdfj @ 4198496".to_string(), + json!({ + "name": "main", "offset": 4198496, "size": 64, + "ops": [ + {"offset": 4198496, "type": "push", "disasm": "push rbp"}, + {"offset": 4198500, "type": "cjmp", "jump": 4198520, "fail": 4198512, "disasm": "je 0x401018"}, + {"offset": 4198504, "type": "call", "jump": 4196112, "disasm": "call sym.imp.LIBC.so.6_puts"}, + {"offset": 4198510, "type": "jmp", "jump": 4198496, "disasm": "jmp 0x401000"}, + {"offset": 4198560, "type": "ret", "disasm": "ret"} + ] + }), + ); + Self { responses } + } +} + +impl R2Client for MockR2 { + fn cmd(&mut self, cmd: &str) -> Result { + Ok(self + .responses + .get(cmd) + .map(|v| v.to_string()) + .unwrap_or_default()) + } + + fn cmdj(&mut self, cmd: &str) -> Result { + Ok(self.responses.get(cmd).cloned().unwrap_or(Value::Null)) + } +} + +#[test] +fn extract_maps_functions_imports_strings() { + let dir = tempfile::tempdir().unwrap(); + let bin_path = dir.path().join("app"); + std::fs::write(&bin_path, b"\x7fELF\x02\x01\x01fake").unwrap(); + + let mut mock = MockR2::new(); + let result: ParseResult = + extract_binary_with_session(&bin_path, &mut mock, AnalysisDepth::Aaa, false).unwrap(); + + assert_eq!(result.language, "binary"); + assert_eq!(result.path, bin_path.to_str().unwrap()); + + // functions + imports + strings + let funcs: Vec<_> = result + .symbols + .iter() + .filter(|s| { + s.kind == SymbolKind::Function + && !s + .signature + .as_deref() + .is_some_and(|sig| sig.starts_with("import")) + }) + .collect(); + assert_eq!(funcs.len(), 2, "2 hàm thật (main + fcn), PLT bị bỏ qua"); + + let imports: Vec<_> = result + .symbols + .iter() + .filter(|s| s.annotations.iter().any(|a| a.name == "import")) + .collect(); + assert_eq!(imports.len(), 1); + assert_eq!(imports[0].name, "puts"); + assert_eq!(imports[0].doc.as_deref(), Some("LIBC.so.6")); + + let strings: Vec<_> = result + .symbols + .iter() + .filter(|s| s.kind == SymbolKind::Constant) + .collect(); + assert_eq!(strings.len(), 1); + assert!(strings[0].name.starts_with("str:")); + assert_eq!(strings[0].doc.as_deref(), Some("hello world\n")); +} + +#[test] +fn extract_resolves_calls_via_callgraph() { + let dir = tempfile::tempdir().unwrap(); + let bin_path = dir.path().join("app"); + std::fs::write(&bin_path, b"\x7fELF\x02\x01\x01fake").unwrap(); + + let mut mock = MockR2::new(); + let result = + extract_binary_with_session(&bin_path, &mut mock, AnalysisDepth::Aaa, false).unwrap(); + + // main có 2 call: helper (fcn) + puts (import) + let main = result.symbols.iter().find(|s| s.name == "main").unwrap(); + let chain = result.chains.get(&main.id).unwrap(); + assert_eq!(chain[0], main.id); + assert_eq!(chain.len(), 3, "main → 2 placeholder call"); + + let main_calls: Vec<_> = result + .calls + .iter() + .filter(|c| c.caller_id == main.id) + .collect(); + assert_eq!(main_calls.len(), 2); + let names: Vec<_> = main_calls.iter().map(|c| c.call_name.as_str()).collect(); + assert!( + names.contains(&"fcn.00401160"), + "call nội bộ theo name r2: {names:?}" + ); + assert!( + names.contains(&"puts"), + "call import theo tên sạch: {names:?}" + ); +} + +#[test] +fn extract_with_cfg_markers() { + let dir = tempfile::tempdir().unwrap(); + let bin_path = dir.path().join("app"); + std::fs::write(&bin_path, b"\x7fELF\x02\x01\x01fake").unwrap(); + + let mut mock = MockR2::new(); + let result = + extract_binary_with_session(&bin_path, &mut mock, AnalysisDepth::Aaa, true).unwrap(); + + let main = result.symbols.iter().find(|s| s.name == "main").unwrap(); + let chain = result.chains.get(&main.id).unwrap(); + // chain: [main, IF_TRUE, call(puts placeholder), ...] + assert!(chain.contains(&MARKER_IF_TRUE), "cjmp → IF_TRUE: {chain:?}"); + assert!(chain.contains(&MARKER_RETURN), "ret → RETURN: {chain:?}"); + // call tới import trong chain-with-cfg dùng plt addr → "puts" + let main_calls: Vec<_> = result + .calls + .iter() + .filter(|c| c.caller_id == main.id) + .collect(); + assert!(main_calls.iter().any(|c| c.call_name == "puts")); +} + +#[test] +fn depth_commands() { + assert_eq!(AnalysisDepth::Aaa.command(), "aaa"); + assert_eq!(AnalysisDepth::Fast.command(), "af; aar; aac"); +} + +// Integration thật với r2 — bỏ qua nếu không có r2 trong PATH. +#[test] +#[ignore = "cần radare2 trong PATH"] +fn integration_with_real_r2() { + if !codegraph_binary::r2_available() { + eprintln!("r2 không có trong PATH — bỏ qua"); + return; + } + let dir = tempfile::tempdir().unwrap(); + let bin_path = dir.path().join("hello"); + // /bin/ls là ELF/Mach-O có sẵn trên hệ thống + std::fs::copy("/bin/ls", &bin_path).unwrap(); + + let result = codegraph_binary::extract_binary(&bin_path, AnalysisDepth::Fast, true).unwrap(); + assert!( + !result.symbols.is_empty(), + "phải tìm được symbol trong /bin/ls" + ); +} diff --git a/crates/codegraph-extract/Cargo.toml b/crates/codegraph-extract/Cargo.toml index aa0d4aa0a0..d53783aa09 100644 --- a/crates/codegraph-extract/Cargo.toml +++ b/crates/codegraph-extract/Cargo.toml @@ -28,6 +28,7 @@ tree-sitter-swift = { workspace = true, optional = true } # tree-sitter-kotlin uses tree-sitter 0.20 — incompatible. Re-enable when upstream upgrades. # tree-sitter-kotlin = { workspace = true, optional = true } tree-sitter-lua = { workspace = true, optional = true } +codegraph-binary = { path = "../codegraph-binary", optional = true } ignore = { workspace = true } rayon = { workspace = true } camino = { workspace = true } @@ -45,7 +46,7 @@ tempfile = "3" tokio = { version = "1", features = ["macros", "rt"] } [features] -default = ["all-langs"] +default = ["all-langs", "binary"] all-langs = [ "lang-typescript", "lang-javascript", "lang-python", "lang-rust", "lang-go", "lang-java", "lang-c", "lang-cpp", "lang-csharp", "lang-ruby", "lang-php", @@ -66,3 +67,4 @@ lang-scala = ["dep:tree-sitter-scala"] lang-swift = ["dep:tree-sitter-swift"] # lang-kotlin = ["dep:tree-sitter-kotlin"] lang-lua = ["dep:tree-sitter-lua"] +binary = ["dep:codegraph-binary"] diff --git a/crates/codegraph-extract/src/config.rs b/crates/codegraph-extract/src/config.rs index ee627ef850..d30a46c3ed 100644 --- a/crates/codegraph-extract/src/config.rs +++ b/crates/codegraph-extract/src/config.rs @@ -1,6 +1,8 @@ use crate::languages::effects::EffectClassifier; use crate::project::{project_db_path, project_dir}; use camino::Utf8Path; +#[cfg(feature = "binary")] +pub use codegraph_binary::BinaryConfig; use codegraph_core::{EffectCallPattern, EffectRule, EffectType, StorageRoute}; use serde::Deserialize; use std::fs; @@ -64,6 +66,10 @@ struct ConfigFile { /// Embedding backend cho semantic search (fastembed / hashing) + cache model. #[serde(default)] embedding: EmbeddingSection, + /// Phân tích binary (radare2) — feature `binary`. + #[cfg(feature = "binary")] + #[serde(default)] + binary: Option, } #[derive(Debug, Default, Deserialize)] @@ -133,6 +139,9 @@ pub struct ExtractConfig { pub storage: StorageConfig, /// Cấu hình embedding backend (semantic search) — đọc từ `[embedding]`. pub embedding: codegraph_graph::embeddings::EmbeddingConfig, + /// Cấu hình phân tích binary (radare2). + #[cfg(feature = "binary")] + pub binary: BinaryConfig, } /// Storage backend đã parse từ `[storage]` trong config. @@ -182,6 +191,8 @@ impl ExtractConfig { repo_id: file.storage.repo_id, dsns: file.storage.dsns, }, + #[cfg(feature = "binary")] + binary: file.binary.unwrap_or_default(), } } @@ -353,6 +364,25 @@ type = "sqlite" # "metal" → Metal EP (GPU) # Build thiếu `apple-accel`, hoặc platform khác macOS → bỏ qua, chạy CPU. # execution_provider = "cpu" + +[binary] +# Phân tích binary (ELF/Mach-O/PE) bằng radare2 — yêu cầu `r2` trong PATH. +# `codegraph doctor` kiểm tra sự có mặt của r2. +# enabled = true # bỏ comment để bật +# depth = "aaa" # "aaa" (full) hoặc "fast" (af; aar; aac — nhanh hơn cho binary lớn) +# cfg_markers = true # xây marker IF/LOOP/SWITCH từ CFG của mỗi function +# cache = true # cache kết quả phân tích theo (path, mtime, size) +"#; + +/// Default `config.toml` section `[binary]` (ghi chú, thêm bởi `codegraph init`). +pub const BINARY_CONFIG_NOTE: &str = r#" +[binary] +# Phân tích binary (ELF/Mach-O/PE) bằng radare2 — yêu cầu `r2` trong PATH. +# `codegraph doctor` kiểm tra sự có mặt của r2. +# enabled = true # bỏ comment để bật +# depth = "aaa" # "aaa" (full) hoặc "fast" (af; aar; aac — nhanh hơn cho binary lớn) +# cfg_markers = true # xây marker IF/LOOP/SWITCH từ CFG của mỗi function +# cache = true # cache kết quả phân tích theo (path, mtime, size) "#; /// Quick project scan: returns a hint when the tree is clearly C-only or C++-only. diff --git a/crates/codegraph-extract/src/orchestrator.rs b/crates/codegraph-extract/src/orchestrator.rs index 92036ffa92..1c40159012 100644 --- a/crates/codegraph-extract/src/orchestrator.rs +++ b/crates/codegraph-extract/src/orchestrator.rs @@ -43,7 +43,14 @@ impl Orchestrator { pub fn parse_project(&self, root: &Utf8Path) -> Result<(Vec, ExtractStats)> { let config = ExtractConfig::load(root); let files = walker::walk(root, &self.parsers, &config); - let (parsed, skipped) = self.parse_files(&files, None, config.effect_classifier.clone()); + let (mut parsed, mut skipped) = + self.parse_files(&files, None, config.effect_classifier.clone()); + #[cfg(feature = "binary")] + { + let (bin, bin_skipped) = codegraph_binary::collect_binaries(root, &config.binary); + parsed.extend(bin); + skipped += bin_skipped; + } let stats = stats_of(&parsed, skipped); Ok((parsed, stats)) } @@ -74,9 +81,16 @@ impl Orchestrator { ); } - let (parsed, skipped) = + let (mut parsed, mut skipped) = self.parse_files(&files, progress.clone(), config.effect_classifier.clone()); + #[cfg(feature = "binary")] + { + let (bin, bin_skipped) = codegraph_binary::collect_binaries(root, &config.binary); + parsed.extend(bin); + skipped += bin_skipped; + } + // Đưa ProgressBar vào ingest (register → edges → files → engines) — phase // index chiếm phần lớn thời gian, không thể để im trong lúc `GraphIndex` // ghi sqlite. diff --git a/crates/codegraph-extract/src/walker.rs b/crates/codegraph-extract/src/walker.rs index 3c11b11287..4adfad8500 100644 --- a/crates/codegraph-extract/src/walker.rs +++ b/crates/codegraph-extract/src/walker.rs @@ -220,6 +220,7 @@ mod tests { effect_classifier: Default::default(), storage: Default::default(), embedding: Default::default(), + ..Default::default() }; let matches = walk(&root, &parsers, &config); let h = matches diff --git a/crates/codegraph/src/main.rs b/crates/codegraph/src/main.rs index a252865551..dc821f83aa 100644 --- a/crates/codegraph/src/main.rs +++ b/crates/codegraph/src/main.rs @@ -342,9 +342,9 @@ async fn cmd_doctor(root: &Utf8Path) -> Result<()> { // External tools codegraph relies on. On Windows, native package managers // matter for install paths, so surface them too. #[cfg(target_os = "windows")] - let tools: Vec<&str> = vec!["git", "tar", "winget", "choco", "scoop"]; + let tools: Vec<&str> = vec!["git", "tar", "r2"]; #[cfg(not(target_os = "windows"))] - let tools: Vec<&str> = vec!["git", "tar"]; + let tools: Vec<&str> = vec!["git", "tar", "r2"]; println!("Tools on PATH :"); for t in tools { let ok = std::process::Command::new(t)