1use std::path::Path;
2
3use anyhow::Result;
4use serde::{Deserialize, Serialize};
5
6use crate::model::{Bridge, BridgeKind};
7
8#[derive(Debug, Default, Serialize, Deserialize)]
10pub struct BridgeScanResult {
11 pub bridges: Vec<Bridge>,
12}
13
14impl BridgeScanResult {
15 pub fn by_kind(&self, kind: &BridgeKind) -> Vec<&Bridge> {
16 self.bridges.iter().filter(|b| &b.kind == kind).collect()
17 }
18
19 pub fn ffi_count(&self) -> usize {
20 self.bridges
21 .iter()
22 .filter(|b| b.kind == BridgeKind::Ffi)
23 .count()
24 }
25 pub fn jni_count(&self) -> usize {
26 self.bridges
27 .iter()
28 .filter(|b| b.kind == BridgeKind::Jni)
29 .count()
30 }
31 pub fn grpc_count(&self) -> usize {
32 self.bridges
33 .iter()
34 .filter(|b| b.kind == BridgeKind::Grpc)
35 .count()
36 }
37 pub fn pinvoke_count(&self) -> usize {
38 self.bridges
39 .iter()
40 .filter(|b| b.kind == BridgeKind::PInvoke)
41 .count()
42 }
43 pub fn com_count(&self) -> usize {
44 self.bridges
45 .iter()
46 .filter(|b| b.kind == BridgeKind::Com)
47 .count()
48 }
49}
50
51struct BridgeRule {
56 kind: fn() -> BridgeKind,
57 extensions: &'static [&'static str],
59 pattern: &'static str,
61 source_language: &'static str,
63 target_language: Option<&'static str>,
65 extract: fn(&str) -> String,
67}
68
69fn extract_after_last_space(line: &str) -> String {
70 line.split_whitespace()
71 .last()
72 .unwrap_or("")
73 .trim_matches(|c: char| !c.is_alphanumeric() && c != '_' && c != ':')
74 .to_string()
75}
76
77fn extract_quoted(line: &str) -> String {
78 if let Some(start) = line.find('"') {
80 if let Some(end) = line[start + 1..].find('"') {
81 return line[start + 1..start + 1 + end].to_string();
82 }
83 }
84 line.trim().to_string()
85}
86
87fn extract_vb6_lib(line: &str) -> String {
88 let lower = line.to_ascii_lowercase();
89 if let Some(idx) = lower.find(" lib ") {
90 let after = line[idx + 5..].trim_start();
91 return extract_quoted(after);
92 }
93 extract_after_last_space(line)
94}
95
96fn extract_paren_arg(line: &str) -> String {
97 if let Some(open) = line.find('(') {
99 let after = line[open + 1..].trim_start();
100 let end = after.find([',', ')']).unwrap_or(after.len());
101 return after[..end].trim().trim_matches('"').to_string();
102 }
103 line.trim().to_string()
104}
105
106static BRIDGE_RULES: &[BridgeRule] = &[
107 BridgeRule {
109 kind: || BridgeKind::Ffi,
110 extensions: &["rs"],
111 pattern: "extern \"C\"",
112 source_language: "rust",
113 target_language: Some("c"),
114 extract: extract_after_last_space,
115 },
116 BridgeRule {
117 kind: || BridgeKind::Ffi,
118 extensions: &["rs"],
119 pattern: "#[no_mangle]",
120 source_language: "rust",
121 target_language: Some("c"),
122 extract: |_| "exported_symbol".to_string(),
123 },
124 BridgeRule {
125 kind: || BridgeKind::Ffi,
126 extensions: &["rs"],
127 pattern: "extern \"system\"",
128 source_language: "rust",
129 target_language: Some("c"),
130 extract: extract_after_last_space,
131 },
132 BridgeRule {
134 kind: || BridgeKind::Ffi,
135 extensions: &["c", "h", "cpp", "hpp", "cc"],
136 pattern: "JNIEXPORT",
137 source_language: "c",
138 target_language: Some("java"),
139 extract: extract_after_last_space,
140 },
141 BridgeRule {
142 kind: || BridgeKind::Jni,
143 extensions: &["c", "h", "cpp", "hpp", "cc"],
144 pattern: "JNIEnv",
145 source_language: "c",
146 target_language: Some("java"),
147 extract: extract_after_last_space,
148 },
149 BridgeRule {
151 kind: || BridgeKind::Jni,
152 extensions: &["java"],
153 pattern: "native ",
154 source_language: "java",
155 target_language: Some("c"),
156 extract: extract_after_last_space,
157 },
158 BridgeRule {
159 kind: || BridgeKind::Jni,
160 extensions: &["java"],
161 pattern: "System.loadLibrary(",
162 source_language: "java",
163 target_language: Some("c"),
164 extract: extract_paren_arg,
165 },
166 BridgeRule {
168 kind: || BridgeKind::Cgo,
169 extensions: &["go"],
170 pattern: "import \"C\"",
171 source_language: "go",
172 target_language: Some("c"),
173 extract: |_| "C".to_string(),
174 },
175 BridgeRule {
176 kind: || BridgeKind::Cgo,
177 extensions: &["go"],
178 pattern: "C.",
179 source_language: "go",
180 target_language: Some("c"),
181 extract: |line| {
182 if let Some(idx) = line.find("C.") {
184 let after = &line[idx + 2..];
185 let end = after
186 .find(|c: char| !c.is_alphanumeric() && c != '_')
187 .unwrap_or(after.len());
188 return format!("C.{}", &after[..end]);
189 }
190 "C.unknown".to_string()
191 },
192 },
193 BridgeRule {
195 kind: || BridgeKind::PInvoke,
196 extensions: &["cs"],
197 pattern: "[DllImport(",
198 source_language: "csharp",
199 target_language: Some("c"),
200 extract: extract_paren_arg,
201 },
202 BridgeRule {
203 kind: || BridgeKind::PInvoke,
204 extensions: &["cs"],
205 pattern: "DllImport(",
206 source_language: "csharp",
207 target_language: Some("c"),
208 extract: extract_paren_arg,
209 },
210 BridgeRule {
212 kind: || BridgeKind::Ctypes,
213 extensions: &["py"],
214 pattern: "ctypes.CDLL(",
215 source_language: "python",
216 target_language: Some("c"),
217 extract: extract_paren_arg,
218 },
219 BridgeRule {
220 kind: || BridgeKind::Ctypes,
221 extensions: &["py"],
222 pattern: "ctypes.cdll.LoadLibrary(",
223 source_language: "python",
224 target_language: Some("c"),
225 extract: extract_paren_arg,
226 },
227 BridgeRule {
228 kind: || BridgeKind::Ctypes,
229 extensions: &["py"],
230 pattern: "ffi.cdef(",
231 source_language: "python",
232 target_language: Some("c"),
233 extract: |_| "cffi_binding".to_string(),
234 },
235 BridgeRule {
237 kind: || BridgeKind::Grpc,
238 extensions: &["proto"],
239 pattern: "service ",
240 source_language: "protobuf",
241 target_language: None,
242 extract: |line| {
243 line.trim()
244 .strip_prefix("service ")
245 .map(|s| s.split_whitespace().next().unwrap_or("").to_string())
246 .unwrap_or_default()
247 },
248 },
249 BridgeRule {
250 kind: || BridgeKind::Grpc,
251 extensions: &["py", "js", "ts", "go", "java", "rb", "cs", "rs"],
252 pattern: "_pb2_grpc",
253 source_language: "python",
254 target_language: None,
255 extract: extract_after_last_space,
256 },
257 BridgeRule {
258 kind: || BridgeKind::Grpc,
259 extensions: &["go"],
260 pattern: "grpc.Dial(",
261 source_language: "go",
262 target_language: None,
263 extract: extract_paren_arg,
264 },
265 BridgeRule {
266 kind: || BridgeKind::Grpc,
267 extensions: &["java"],
268 pattern: "ManagedChannelBuilder",
269 source_language: "java",
270 target_language: None,
271 extract: extract_after_last_space,
272 },
273 BridgeRule {
274 kind: || BridgeKind::Grpc,
275 extensions: &["rs"],
276 pattern: "tonic::transport::Channel",
277 source_language: "rust",
278 target_language: None,
279 extract: extract_after_last_space,
280 },
281 BridgeRule {
283 kind: || BridgeKind::Wasm,
284 extensions: &["rs"],
285 pattern: "#[wasm_bindgen]",
286 source_language: "rust",
287 target_language: Some("javascript"),
288 extract: |_| "wasm_export".to_string(),
289 },
290 BridgeRule {
291 kind: || BridgeKind::Wasm,
292 extensions: &["rs"],
293 pattern: "wasm_bindgen::JsValue",
294 source_language: "rust",
295 target_language: Some("javascript"),
296 extract: extract_after_last_space,
297 },
298 BridgeRule {
299 kind: || BridgeKind::Wasm,
300 extensions: &["js", "ts"],
301 pattern: "WebAssembly.instantiate(",
302 source_language: "javascript",
303 target_language: Some("wasm"),
304 extract: extract_paren_arg,
305 },
306 BridgeRule {
307 kind: || BridgeKind::Wasm,
308 extensions: &["js", "ts"],
309 pattern: "WebAssembly.instantiateStreaming(",
310 source_language: "javascript",
311 target_language: Some("wasm"),
312 extract: extract_paren_arg,
313 },
314 BridgeRule {
316 kind: || BridgeKind::Ffi,
317 extensions: &["bas", "cls", "frm", "ctl"],
318 pattern: "Declare Function",
319 source_language: "vb6",
320 target_language: Some("c"),
321 extract: extract_vb6_lib,
322 },
323 BridgeRule {
324 kind: || BridgeKind::Ffi,
325 extensions: &["bas", "cls", "frm", "ctl"],
326 pattern: "Declare Sub",
327 source_language: "vb6",
328 target_language: Some("c"),
329 extract: extract_vb6_lib,
330 },
331 BridgeRule {
332 kind: || BridgeKind::Com,
333 extensions: &["bas", "cls", "frm", "ctl"],
334 pattern: "CreateObject(",
335 source_language: "vb6",
336 target_language: Some("com"),
337 extract: extract_paren_arg,
338 },
339];
340
341pub fn detect_bridges(root: &Path) -> Result<BridgeScanResult> {
347 let mut result = BridgeScanResult::default();
348 scan_dir(root, root, &mut result)?;
349 Ok(result)
350}
351
352fn scan_dir(root: &Path, dir: &Path, result: &mut BridgeScanResult) -> Result<()> {
353 const SKIP_DIRS: &[&str] = &[
354 ".git",
355 ".infigraph",
356 "node_modules",
357 "__pycache__",
358 "target",
359 "build",
360 "dist",
361 ];
362 for entry in std::fs::read_dir(dir)? {
363 let entry = entry?;
364 let path = entry.path();
365 let name = entry.file_name();
366 let name_str = name.to_string_lossy();
367 if path.is_dir() {
368 if !SKIP_DIRS.contains(&name_str.as_ref()) && !name_str.starts_with('.') {
369 scan_dir(root, &path, result)?;
370 }
371 } else if path.is_file() {
372 scan_file(root, &path, result);
373 }
374 }
375 Ok(())
376}
377
378fn scan_file(root: &Path, path: &Path, result: &mut BridgeScanResult) {
379 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
380 let rel = path
381 .strip_prefix(root)
382 .unwrap_or(path)
383 .to_string_lossy()
384 .replace('\\', "/");
385
386 let applicable: Vec<&BridgeRule> = BRIDGE_RULES
388 .iter()
389 .filter(|r| r.extensions.is_empty() || r.extensions.contains(&ext))
390 .collect();
391
392 if applicable.is_empty() {
393 return;
394 }
395
396 let Ok(source) = std::fs::read_to_string(path) else {
397 return;
398 };
399
400 for (line_no, line) in source.lines().enumerate() {
401 for rule in &applicable {
402 if line.contains(rule.pattern) {
403 let foreign_symbol = (rule.extract)(line);
404 result.bridges.push(Bridge {
405 file: rel.clone(),
406 line: line_no as u32 + 1,
407 kind: (rule.kind)(),
408 foreign_symbol,
409 source_language: rule.source_language.to_string(),
410 target_language: rule.target_language.map(str::to_string),
411 detail: line.trim().to_string(),
412 });
413 }
414 }
415 }
416}