Skip to main content

rsleigh_cli/
cli.rs

1//! rsleigh CLI — decompile any binary to C-like pseudocode.
2//!
3//! Usage:
4//!   rsleigh <binary>                    # list functions
5//!   rsleigh <binary> <func>             # decompile one function
6//!   rsleigh <binary> --all              # decompile all functions
7//!   rsleigh <binary> --json             # list functions as JSON
8//!   rsleigh <binary> <func> --json      # decompile as JSON
9//!   rsleigh <binary> --disasm <func>    # disassemble (P-code)
10
11use crate::wasm;
12
13use std::path::Path;
14use std::sync::atomic::{AtomicBool, Ordering};
15
16static ANNOTATE_CRYPTO: AtomicBool = AtomicBool::new(false);
17
18fn maybe_annotate_crypto(s: String) -> String {
19    if ANNOTATE_CRYPTO.load(Ordering::Relaxed) {
20        rsleigh_decompile::crypto_constants::rewrite_text(&s)
21    } else {
22        s
23    }
24}
25
26/// Demangle a Swift symbol name to a human-readable form.
27/// Returns None if not a Swift symbol.
28fn demangle_swift_symbol(name: &str) -> Option<String> {
29    let s = name
30        .strip_prefix("$s")
31        .or_else(|| name.strip_prefix("$S"))?;
32
33    // Parse module name: <length><name>
34    let (module, rest) = parse_swift_id(s)?;
35
36    // Try class + method/property
37    if let Some((class_name, after_class)) = parse_swift_id(rest) {
38        if after_class.starts_with('C') {
39            let after_c = &after_class[1..];
40            if after_c.starts_with("ACycfC") || after_c.starts_with("ACycfc") {
41                return Some(format!("{}.init", class_name));
42            }
43            if after_c == "fd" || after_c == "fD" {
44                return Some(format!("{}.deinit", class_name));
45            }
46            if after_c == "Ma" {
47                return Some(format!("{}.__metadata", class_name));
48            }
49            if after_c == "MF" {
50                return Some(format!("{}.__fields", class_name));
51            }
52            if after_c == "Mm" || after_c == "Mf" || after_c == "N" {
53                return Some(format!("{}.__metadata", class_name));
54            }
55
56            if let Some((prop_name, after_prop)) = parse_swift_id(after_c) {
57                if after_prop.contains("vg") {
58                    return Some(format!("{}.{}.getter", class_name, prop_name));
59                }
60                if after_prop.contains("vs") {
61                    return Some(format!("{}.{}.setter", class_name, prop_name));
62                }
63                if after_prop.contains("vM") {
64                    return Some(format!("{}.{}.modify", class_name, prop_name));
65                }
66                if after_prop.contains("Wvd") {
67                    return Some(format!("{}.{}", class_name, prop_name));
68                }
69                return Some(format!("{}.{}", class_name, prop_name));
70            }
71            return Some(class_name.to_string());
72        }
73        // Free function
74        if after_class.ends_with('F') || after_class.contains("yS") {
75            return Some(class_name.to_string());
76        }
77    }
78
79    // stdlib ($ss prefix)
80    if module == "s" {
81        if let Some((entity, _)) = parse_swift_id(rest) {
82            return Some(format!("Swift.{}", entity));
83        }
84    }
85
86    None
87}
88
89fn parse_swift_id(s: &str) -> Option<(&str, &str)> {
90    let mut len_end = 0;
91    while len_end < s.len() && s.as_bytes()[len_end].is_ascii_digit() {
92        len_end += 1;
93    }
94    if len_end == 0 {
95        return None;
96    }
97    let len: usize = s[..len_end].parse().ok()?;
98    if len_end + len > s.len() {
99        return None;
100    }
101    Some((&s[len_end..len_end + len], &s[len_end + len..]))
102}
103
104pub fn entrypoint() {
105    let args: Vec<String> = std::env::args().collect();
106    if args.len() < 2 {
107        eprintln!("rsleigh — pure Rust decompiler");
108        eprintln!("Usage:");
109        eprintln!("  rsleigh <binary>                  List functions");
110        eprintln!("  rsleigh <binary> <func> [func2..] Decompile functions");
111        eprintln!("  rsleigh <binary> --all             Decompile all functions");
112        eprintln!("  rsleigh <binary> --json             List functions as JSON");
113        eprintln!("  rsleigh <binary> <func> --json     Decompile as JSON");
114        eprintln!("  rsleigh <binary> --disasm <func>   Disassemble with P-code");
115        eprintln!("  rsleigh <binary> --sigs <file.json> Load extra signatures");
116        eprintln!("  rsleigh <binary> --yara             Generate YARA detection rule");
117        eprintln!("  rsleigh <binary> --imphash          Compute imphash (Mandiant) for PE");
118        eprintln!("  rsleigh <binary> --hashes           Print sha256, md5, imphash, size");
119        eprintln!("  rsleigh old.bin --diff new.bin      Diff decompilation (show changes)");
120        eprintln!(
121            "  rsleigh <binary> --taint            Taint analysis (trace user input to sinks)"
122        );
123        eprintln!("  rsleigh <binary> --summary          AI summary (one-line per function)");
124        eprintln!("  rsleigh <binary> --xrefs <func>     Cross-references (callers + callees)");
125        eprintln!("  rsleigh <binary> --search <query>   Find functions by string/pattern");
126        eprintln!("  rsleigh <binary> --search --api <name>  Find functions calling API");
127        eprintln!("  rsleigh <binary> --search --const <hex> Find functions with constant");
128        eprintln!("  rsleigh <binary> --seh-fixpoint      Apply SEH-driven SMC patches until fixpoint, report new functions");
129        eprintln!("  rsleigh <binary> --vulnscan          Scan for vulnerability patterns");
130        eprintln!("  rsleigh <binary> --smt-explore <func> [--json]  SMT taint-flow CVE proof (requires --features smt)");
131        eprintln!("  rsleigh <binary> --ioc [--json]      Extract IOCs (URLs, IPs, paths, registry keys)");
132        eprintln!("  rsleigh <binary> --xor-strings [--json]  Brute single-byte XOR string recovery");
133        eprintln!("  rsleigh <binary> --sigcheck [--json] Parse Authenticode signature (signer, timestamp, chain)");
134        eprintln!("  rsleigh <binary> --resources [--dump DIR] [--json]  Walk PE resource directory; --dump extracts blobs");
135        eprintln!(
136            "  rsleigh <binary> --all --compact     Token-efficient output (no decls/blanks)"
137        );
138        eprintln!("  rsleigh <binary> --all --brief       Calls + strings only (minimal tokens)");
139        eprintln!("  rsleigh <binary> --all --min-complexity 10  Skip trivial functions");
140        eprintln!("  rsleigh <binary> --callgraph         Export call graph as JSON");
141        eprintln!("  rsleigh <binary> --classes           Recover C++ classes from RTTI");
142        eprintln!(
143            "  rsleigh <binary> --raw <arch>       Load raw binary (mips32/arm32/x86-64/...)"
144        );
145        std::process::exit(1);
146    }
147
148    let binary_path = &args[1];
149    let json_mode = args.iter().any(|a| a == "--json");
150    let all_mode = args.iter().any(|a| a == "--all");
151    let disasm_mode = args.iter().any(|a| a == "--disasm");
152    let pcode_json_mode = args.iter().any(|a| a == "--pcode-json");
153    let ssa_json_mode = args.iter().any(|a| a == "--ssa-json");
154    let yara_mode = args.iter().any(|a| a == "--yara");
155    let imphash_mode = args.iter().any(|a| a == "--imphash");
156    let hashes_mode = args.iter().any(|a| a == "--hashes");
157    let summary_mode = args.iter().any(|a| a == "--summary");
158    let xrefs_mode = args.iter().any(|a| a == "--xrefs");
159    let search_mode = args.iter().any(|a| a == "--search");
160    let vulnscan_mode = args.iter().any(|a| a == "--vulnscan");
161    let ioc_mode = args.iter().any(|a| a == "--ioc");
162    let xor_strings_mode = args.iter().any(|a| a == "--xor-strings");
163    let sigcheck_mode = args.iter().any(|a| a == "--sigcheck");
164    let resources_mode = args.iter().any(|a| a == "--resources");
165    let classes_mode = args.iter().any(|a| a == "--classes");
166    let compact_mode = args.iter().any(|a| a == "--compact");
167    let brief_mode = args.iter().any(|a| a == "--brief");
168    let annotate_crypto_mode = args.iter().any(|a| a == "--annotate-crypto");
169    ANNOTATE_CRYPTO.store(annotate_crypto_mode, Ordering::Relaxed);
170    let min_complexity: usize = args
171        .iter()
172        .position(|a| a == "--min-complexity")
173        .and_then(|i| args.get(i + 1))
174        .and_then(|s| s.parse().ok())
175        .unwrap_or(0);
176    let callgraph_mode = args.iter().any(|a| a == "--callgraph");
177    let seh_fixpoint_mode = args.iter().any(|a| a == "--seh-fixpoint");
178    let sections_mode = args.iter().any(|a| a == "--sections");
179
180    // VM-helper flags. All take a comma-separated list of hex addresses
181    // (or a single address) and emit one line per handler.
182    let vm_classify_arg = args
183        .iter()
184        .position(|a| a == "--vm-classify-handlers")
185        .and_then(|i| args.get(i + 1))
186        .cloned();
187    let tag_dispatch_arg = args
188        .iter()
189        .position(|a| a == "--tag-dispatch")
190        .and_then(|i| args.get(i + 1))
191        .cloned();
192    let summarise_arg = args
193        .iter()
194        .position(|a| a == "--summarise-handlers")
195        .and_then(|i| args.get(i + 1))
196        .cloned();
197    let vm_dispatch_arg = args
198        .iter()
199        .position(|a| a == "--vm-dispatch")
200        .and_then(|i| args.get(i + 1))
201        .cloned();
202    let vm_bytecode_arg = args
203        .iter()
204        .position(|a| a == "--vm-bytecode")
205        .and_then(|i| args.get(i + 1))
206        .cloned();
207    let vm_handlers_arg = args
208        .iter()
209        .position(|a| a == "--vm-handlers")
210        .and_then(|i| args.get(i + 1))
211        .cloned();
212
213    if sections_mode {
214        let data = match std::fs::read(binary_path) {
215            Ok(d) => d,
216            Err(e) => {
217                eprintln!("Error: {}", e);
218                std::process::exit(1);
219            }
220        };
221        run_section_scan(binary_path, &data);
222        return;
223    }
224
225    // --vm-classify-handlers / --tag-dispatch / --summarise-handlers:
226    // VM-RE helper flags. Each takes a comma-separated list of hex
227    // addresses (or single address). Emit one line per handler and
228    // exit.
229    if vm_classify_arg.is_some()
230        || tag_dispatch_arg.is_some()
231        || summarise_arg.is_some()
232        || vm_dispatch_arg.is_some()
233        || vm_bytecode_arg.is_some()
234    {
235        let data = match std::fs::read(binary_path) {
236            Ok(d) => d,
237            Err(e) => {
238                eprintln!("Error: {}", e);
239                std::process::exit(1);
240            }
241        };
242        let obj = match goblin::Object::parse(&data) {
243            Ok(o) => o,
244            Err(e) => {
245                eprintln!("Error: cannot parse binary: {}", e);
246                std::process::exit(1);
247            }
248        };
249        let parse_addrs = |s: &str| -> Vec<u64> {
250            s.split(',')
251                .filter_map(|t| {
252                    let t = t.trim();
253                    let t = t.trim_start_matches("0x").trim_start_matches("0X");
254                    u64::from_str_radix(t, 16).ok()
255                })
256                .collect()
257        };
258        if let Some(arg) = vm_classify_arg.as_ref() {
259            let addrs = parse_addrs(arg);
260            let encs = rsleigh_decompile::vm_handler_classify::classify_all(&obj, &data, &addrs);
261            for line in rsleigh_decompile::vm_handler_classify::render(&encs) {
262                println!("{}", line);
263            }
264            return;
265        }
266        if let Some(arg) = tag_dispatch_arg.as_ref() {
267            let addrs = parse_addrs(arg);
268            for &a in &addrs {
269                let cases = rsleigh_decompile::tag_dispatch::scan_function(&obj, &data, a);
270                println!("=== {:#x} — {} cases ===", a, cases.len());
271                for line in rsleigh_decompile::tag_dispatch::render(&cases) {
272                    println!("  {}", line);
273                }
274            }
275            return;
276        }
277        if let Some(arg) = summarise_arg.as_ref() {
278            let addrs = parse_addrs(arg);
279            let summaries = rsleigh_decompile::handler_summary::summarise_all(&obj, &data, &addrs);
280            for line in rsleigh_decompile::handler_summary::render(&summaries) {
281                println!("{}", line);
282            }
283            return;
284        }
285        if let Some(arg) = vm_dispatch_arg.as_ref() {
286            let addrs = parse_addrs(arg);
287            for &a in &addrs {
288                if let Some(info) = rsleigh_decompile::vm_dispatch_extract::extract(&obj, &data, a)
289                {
290                    for line in rsleigh_decompile::vm_dispatch_extract::render(&info) {
291                        println!("{}", line);
292                    }
293                } else {
294                    println!("dispatcher @ {:#x}: extraction failed", a);
295                }
296            }
297            return;
298        }
299        if let Some(arg) = vm_bytecode_arg.as_ref() {
300            // Format: <bc_va>:<size> e.g. 0x180018000:0x400
301            let parts: Vec<&str> = arg.split(':').collect();
302            if parts.len() != 2 {
303                eprintln!("--vm-bytecode expects <bc_va>:<size> (hex), got {arg}");
304                std::process::exit(1);
305            }
306            let parse_hex = |s: &str| -> Option<u64> {
307                let s = s.trim().trim_start_matches("0x").trim_start_matches("0X");
308                u64::from_str_radix(s, 16).ok()
309            };
310            let bc_va = match parse_hex(parts[0]) {
311                Some(v) => v,
312                None => {
313                    eprintln!("--vm-bytecode: bad VA {}", parts[0]);
314                    std::process::exit(1);
315                }
316            };
317            let bc_size = match parse_hex(parts[1]) {
318                Some(v) => v as usize,
319                None => {
320                    eprintln!("--vm-bytecode: bad size {}", parts[1]);
321                    std::process::exit(1);
322                }
323            };
324            let handlers_path = match vm_handlers_arg.as_ref() {
325                Some(p) => p,
326                None => {
327                    eprintln!("--vm-bytecode requires --vm-handlers <path.json>");
328                    std::process::exit(1);
329                }
330            };
331            let json = match std::fs::read_to_string(handlers_path) {
332                Ok(s) => s,
333                Err(e) => {
334                    eprintln!("--vm-handlers: cannot read {handlers_path}: {e}");
335                    std::process::exit(1);
336                }
337            };
338            let vtable = match rsleigh_decompile::vm_bytecode_disasm::parse_handlers_json(&json) {
339                Ok(v) => v,
340                Err(e) => {
341                    eprintln!("--vm-handlers: parse error: {e}");
342                    std::process::exit(1);
343                }
344            };
345            // Resolve bc_va → file slice via PE sections.
346            let bytecode = if let goblin::Object::PE(pe) = &obj {
347                let mut found: Option<&[u8]> = None;
348                for sec in &pe.sections {
349                    let svaddr = pe.image_base as u64 + sec.virtual_address as u64;
350                    let sv = sec.virtual_size as u64;
351                    if bc_va >= svaddr && bc_va < svaddr + sv {
352                        let raddr = sec.pointer_to_raw_data as usize;
353                        let rsize = sec.size_of_raw_data as usize;
354                        let off_in_section = (bc_va - svaddr) as usize;
355                        if off_in_section >= rsize {
356                            // VA is in virtual_size but past size_of_raw_data —
357                            // BSS-style uninitialised tail, no file bytes.
358                            break;
359                        }
360                        let off = raddr + off_in_section;
361                        if off >= data.len() {
362                            break;
363                        }
364                        let avail = (rsize - off_in_section).min(data.len() - off);
365                        let end = off + bc_size.min(avail);
366                        found = Some(&data[off..end]);
367                        break;
368                    }
369                }
370                found
371            } else {
372                None
373            };
374            let bytecode = match bytecode {
375                Some(b) => b,
376                None => {
377                    eprintln!("--vm-bytecode: VA {:#x} not in any PE section", bc_va);
378                    std::process::exit(1);
379                }
380            };
381            let insts =
382                rsleigh_decompile::vm_bytecode_disasm::disassemble(bytecode, bc_va, &vtable);
383            for line in rsleigh_decompile::vm_bytecode_disasm::render(&insts) {
384                println!("{}", line);
385            }
386            return;
387        }
388    }
389
390    if seh_fixpoint_mode {
391        let data = match std::fs::read(binary_path) {
392            Ok(d) => d,
393            Err(e) => {
394                eprintln!("Error: {}", e);
395                std::process::exit(1);
396            }
397        };
398        // Full-discovery fixpoint: at each step, re-run the CLI's complete
399        // function-discovery pipeline against the mutated image.  This
400        // picks up not just SEH handlers and scope-table filters but also
401        // PyMethodDef registrations, RIP-relative function pointers in
402        // .rdata, and the prologue / CALL-descent passes that run in
403        // `discover_pe_functions`.
404        let result = rsleigh_decompile::seh_static::smc_fixpoint(&data, 16, |img| {
405            let Ok(obj) = goblin::Object::parse(img) else {
406                return vec![];
407            };
408            let Some((arch, segs, mut symbols)) = parse_binary(&obj, img) else {
409                return vec![];
410            };
411            if symbols.is_empty() {
412                if let goblin::Object::PE(pe) = &obj {
413                    let base = pe.image_base as u64;
414                    if let Some(optional) = pe.header.optional_header {
415                        let entry = base + optional.standard_fields.address_of_entry_point as u64;
416                        symbols = discover_pe_functions(entry, &segs, img, arch);
417                    }
418                }
419            }
420            if let goblin::Object::PE(pe) = &obj {
421                if pe.is_64 {
422                    for (addr, _) in scan_pymethoddef(&segs, img) {
423                        symbols.push((addr, String::new()));
424                    }
425                    let seh = rsleigh_decompile::seh_static::parse_pe64_seh(img);
426                    for a in rsleigh_decompile::seh_static::handler_addresses(&seh) {
427                        symbols.push((a, String::new()));
428                    }
429                    for a in rsleigh_decompile::seh_static::scope_table_addresses(img) {
430                        symbols.push((a, String::new()));
431                    }
432                }
433            }
434            let mut addrs: Vec<u64> = symbols.into_iter().map(|(a, _)| a).collect();
435            addrs.sort_unstable();
436            addrs.dedup();
437            addrs
438        });
439        println!(
440            "iterations: {}  converged: {}",
441            result.iterations, result.converged
442        );
443        println!("patches applied: {}", result.patches.len());
444        for p in &result.patches {
445            let preview: String = p
446                .bytes
447                .iter()
448                .take(16)
449                .map(|b| format!("{:02x}", b))
450                .collect::<Vec<_>>()
451                .join(" ");
452            let more = if p.bytes.len() > 16 { " .." } else { "" };
453            println!(
454                "  patch @ {:#x}  len={:4}  from handler {:#x}  [{}{}]",
455                p.target_va,
456                p.bytes.len(),
457                p.handler_va,
458                preview,
459                more
460            );
461        }
462        println!(
463            "newly discovered functions: {}",
464            result.newly_discovered_fns.len()
465        );
466        for va in &result.newly_discovered_fns {
467            println!("  {:#x}", va);
468        }
469        return;
470    }
471
472    if yara_mode {
473        let data = match std::fs::read(binary_path) {
474            Ok(d) => d,
475            Err(e) => {
476                eprintln!("Error: {}", e);
477                std::process::exit(1);
478            }
479        };
480        generate_yara_rule(binary_path, &data);
481        return;
482    }
483
484    if imphash_mode {
485        let data = match std::fs::read(binary_path) {
486            Ok(d) => d,
487            Err(e) => {
488                eprintln!("Error: {}", e);
489                std::process::exit(1);
490            }
491        };
492        match compute_imphash(&data) {
493            Some(h) => println!("{}", h),
494            None => {
495                eprintln!("imphash: not a PE binary with imports");
496                std::process::exit(1);
497            }
498        }
499        return;
500    }
501
502    if hashes_mode {
503        let data = match std::fs::read(binary_path) {
504            Ok(d) => d,
505            Err(e) => {
506                eprintln!("Error: {}", e);
507                std::process::exit(1);
508            }
509        };
510        let sha256 = compute_sha256(&data);
511        let md5 = compute_md5(&data);
512        let imphash = compute_imphash(&data);
513        println!("file:    {}", binary_path);
514        println!("size:    {}", data.len());
515        println!("md5:     {}", md5);
516        println!("sha256:  {}", sha256);
517        if let Some(h) = imphash {
518            println!("imphash: {}", h);
519        }
520        return;
521    }
522
523    // C++ class recovery
524    if classes_mode {
525        let data = match std::fs::read(binary_path) {
526            Ok(d) => d,
527            Err(e) => {
528                eprintln!("Error: {}", e);
529                std::process::exit(1);
530            }
531        };
532        // Try MSVC RTTI first, then GCC RTTI
533        let mut classes = rsleigh_decompile::cpp_class::recover_msvc_classes(&data);
534        if classes.is_empty() {
535            classes = rsleigh_decompile::cpp_class::recover_gcc_classes(&data);
536        }
537        if classes.is_empty() {
538            eprintln!("No C++ RTTI classes found (binary may not have RTTI, or is stripped)");
539        } else {
540            eprintln!("{} C++ classes recovered from RTTI", classes.len());
541            if json_mode {
542                println!("{}", serde_json::to_string_pretty(&classes).unwrap());
543            } else {
544                print!("{}", rsleigh_decompile::cpp_class::format_classes(&classes));
545            }
546        }
547        return;
548    }
549
550    // Summary/Xrefs/Search/Vulnscan/Callgraph modes
551    if summary_mode || xrefs_mode || search_mode || vulnscan_mode || callgraph_mode || ioc_mode || xor_strings_mode || sigcheck_mode || resources_mode {
552        let data = match std::fs::read(binary_path) {
553            Ok(d) => d,
554            Err(e) => {
555                eprintln!("Error: {}", e);
556                std::process::exit(1);
557            }
558        };
559        let bp = binary_path.clone();
560        let args_clone = args.clone();
561        let t = std::thread::Builder::new()
562            .stack_size(256 * 1024 * 1024)
563            .spawn(move || {
564                if summary_mode {
565                    run_summary(&bp, &data);
566                } else if xrefs_mode {
567                    let target = args_clone
568                        .iter()
569                        .position(|a| a == "--xrefs")
570                        .and_then(|i| args_clone.get(i + 1))
571                        .cloned()
572                        .unwrap_or_default();
573                    run_xrefs(&bp, &data, &target);
574                } else if vulnscan_mode {
575                    run_vulnscan(&bp, &data);
576                } else if ioc_mode {
577                    let json = args_clone.iter().any(|a| a == "--json");
578                    run_ioc(&bp, &data, json);
579                } else if xor_strings_mode {
580                    let json = args_clone.iter().any(|a| a == "--json");
581                    run_xor_strings(&bp, &data, json);
582                } else if sigcheck_mode {
583                    let json = args_clone.iter().any(|a| a == "--json");
584                    run_sigcheck(&bp, &data, json);
585                } else if resources_mode {
586                    let json = args_clone.iter().any(|a| a == "--json");
587                    let dump_dir = args_clone
588                        .iter()
589                        .position(|a| a == "--dump")
590                        .and_then(|i| args_clone.get(i + 1))
591                        .cloned();
592                    run_resources(&bp, &data, json, dump_dir.as_deref());
593                } else if callgraph_mode {
594                    run_callgraph(&bp, &data);
595                } else {
596                    // Search mode
597                    let search_idx = args_clone.iter().position(|a| a == "--search").unwrap();
598                    let api_mode = args_clone.iter().any(|a| a == "--api");
599                    let const_mode = args_clone.iter().any(|a| a == "--const");
600                    let tag_mode = args_clone.iter().any(|a| a == "--tag");
601                    let decompile_results = args_clone.iter().any(|a| a == "--decompile");
602                    let json_output = args_clone.iter().any(|a| a == "--json");
603                    let query = args_clone
604                        .iter()
605                        .skip(search_idx + 1)
606                        .find(|a| !a.starts_with("--"))
607                        .cloned()
608                        .unwrap_or_default();
609                    if query.is_empty() {
610                        eprintln!("Usage: rsleigh <binary> --search <query>");
611                        eprintln!("       rsleigh <binary> --search --api <func_name>");
612                        eprintln!("       rsleigh <binary> --search --const <hex_value>");
613                        eprintln!("       rsleigh <binary> --search --tag network,crypto");
614                        eprintln!("       rsleigh <binary> --search <query> --json");
615                        eprintln!("       rsleigh <binary> --search <query> --decompile");
616                        return;
617                    }
618                    run_search(
619                        &bp,
620                        &data,
621                        &query,
622                        api_mode,
623                        const_mode,
624                        tag_mode,
625                        decompile_results,
626                        json_output,
627                    );
628                }
629            })
630            .unwrap();
631        if let Err(e) = t.join() {
632            eprintln!("Panic: {:?}", e);
633        }
634        return;
635    }
636
637    // Diff mode: compare two binaries
638    if let Some(diff_idx) = args.iter().position(|a| a == "--diff") {
639        let new_path = args.get(diff_idx + 1).cloned().unwrap_or_else(|| {
640            eprintln!("Usage: rsleigh old.bin --diff new.bin [func_name]");
641            std::process::exit(1);
642        });
643        // Optional: specific function to diff
644        let func_filter: Vec<String> = args
645            .iter()
646            .enumerate()
647            .filter(|(i, a)| {
648                *i >= 2
649                    && !a.starts_with("--")
650                    && a.as_str() != new_path
651                    && a.as_str() != binary_path
652            })
653            .map(|(_, a)| a.clone())
654            .collect();
655        let old_path = binary_path.clone();
656        let t = std::thread::Builder::new()
657            .stack_size(256 * 1024 * 1024)
658            .spawn(move || diff_binaries(&old_path, &new_path, &func_filter))
659            .unwrap();
660        if let Err(e) = t.join() {
661            eprintln!("Panic: {:?}", e);
662        }
663        return;
664    }
665
666    // Load external signature database if --sigs provided
667    if let Some(pos) = args.iter().position(|a| a == "--sigs") {
668        if let Some(sigs_path) = args.get(pos + 1) {
669            match rsleigh_decompile::signatures::load_json_file(std::path::Path::new(sigs_path)) {
670                Ok(n) => eprintln!("Loaded {} signatures from {}", n, sigs_path),
671                Err(e) => eprintln!("Warning: {}", e),
672            }
673        }
674    }
675
676    let t = std::thread::Builder::new()
677        .stack_size(64 * 1024 * 1024)
678        .spawn({
679            let binary_path = binary_path.clone();
680            let args = args.clone();
681            move || run(&binary_path, &args, json_mode, all_mode, disasm_mode)
682        })
683        .unwrap();
684
685    match t.join() {
686        Ok(()) => {}
687        Err(_) => {
688            eprintln!("Error: stack overflow during decompilation");
689            std::process::exit(1);
690        }
691    }
692}
693
694/// Hidden GCC runtime symbols to exclude from listing.
695const HIDDEN: &[&str] = &[
696    "deregister_tm_clones",
697    "register_tm_clones",
698    "frame_dummy",
699    "__do_global_dtors_aux",
700    "__libc_csu_init",
701    "__libc_csu_fini",
702    "_dl_relocate_static_pie",
703    "__do_global_ctors_aux",
704];
705
706/// Compact pseudocode for token efficiency: strip declarations, blank lines, reduce indent.
707fn compact_output(output: &str) -> String {
708    output
709        .lines()
710        .filter(|l| {
711            let t = l.trim();
712            // Skip empty lines
713            if t.is_empty() {
714                return false;
715            }
716            // Skip variable declarations (type varN;)
717            if t.ends_with(';')
718                && !t.contains('=')
719                && !t.contains('(')
720                && (t.starts_with("int ")
721                    || t.starts_with("long ")
722                    || t.starts_with("uint")
723                    || t.starts_with("char ")
724                    || t.starts_with("float ")
725                    || t.starts_with("double ")
726                    || t.starts_with("bool "))
727            {
728                return false;
729            }
730            true
731        })
732        .map(|l| {
733            // Reduce indent: 4 spaces → 2 spaces
734            let indent = l.len() - l.trim_start().len();
735            let new_indent = indent / 2;
736            format!("{}{}", " ".repeat(new_indent), l.trim())
737        })
738        .collect::<Vec<_>>()
739        .join("\n")
740}
741
742/// Brief mode: show only calls, comparisons, strings, returns — skip assignments.
743fn brief_output(output: &str) -> String {
744    let mut result = Vec::new();
745    for line in output.lines() {
746        let t = line.trim();
747        // Keep function signature
748        if t.contains("func_") && t.contains('(') && t.ends_with('{') {
749            result.push(line.to_string());
750            continue;
751        }
752        // Keep closing brace
753        if t == "}" {
754            result.push(line.to_string());
755            continue;
756        }
757        // Keep calls (lines with function_name() pattern)
758        if t.contains('(')
759            && t.contains(')')
760            && !t.starts_with("//")
761            && (t.ends_with(';') || t.ends_with('{'))
762        {
763            // Skip pure assignments: var = expr; (no function call)
764            if t.contains(" = ") {
765                let rhs = &t[t.find(" = ").unwrap() + 3..];
766                if !rhs.contains('(') {
767                    continue;
768                } // pure assignment
769            }
770            result.push(line.to_string());
771            continue;
772        }
773        // Keep control flow
774        if t.starts_with("if (")
775            || t.starts_with("} else")
776            || t.starts_with("while (")
777            || t.starts_with("for (")
778            || t.starts_with("switch (")
779            || t.starts_with("return ")
780            || t.starts_with("break")
781            || t.starts_with("case ")
782        {
783            result.push(line.to_string());
784            continue;
785        }
786        // Keep string references
787        if t.contains('"') {
788            result.push(line.to_string());
789            continue;
790        }
791        // Keep comments (annotations, crypto, taint)
792        if t.starts_with("//")
793            && (t.contains("TAINT")
794                || t.contains("XOR")
795                || t.contains("stack string")
796                || t.contains("AES")
797                || t.contains("SHA"))
798        {
799            result.push(line.to_string());
800        }
801    }
802    result.join("\n")
803}
804
805fn run(binary_path: &str, args: &[String], json_mode: bool, all_mode: bool, disasm_mode: bool) {
806    let data = match std::fs::read(binary_path) {
807        Ok(d) => d,
808        Err(e) => {
809            eprintln!("Error: cannot read {}: {}", binary_path, e);
810            std::process::exit(1);
811        }
812    };
813
814    // WebAssembly detection: magic bytes \0asm
815    if data.len() >= 4 && &data[0..4] == b"\0asm" {
816        run_wasm(&data, args, all_mode);
817        return;
818    }
819
820    // Raw binary mode: --raw <arch> [--base <addr>]
821    let raw_arch_idx = args.iter().position(|a| a == "--raw");
822    if let Some(idx) = raw_arch_idx {
823        let arch_str = args.get(idx + 1).map(|s| s.as_str()).unwrap_or("mips32");
824        let base_idx = args.iter().position(|a| a == "--base");
825        let base = base_idx
826            .and_then(|i| args.get(i + 1))
827            .and_then(|s| {
828                if let Some(hex) = s.strip_prefix("0x") {
829                    u64::from_str_radix(hex, 16).ok()
830                } else {
831                    s.parse::<u64>().ok()
832                }
833            })
834            .unwrap_or(0);
835        let arch = match arch_str {
836            "x86-64" | "x86_64" | "x64" => rsleigh_api::Architecture::X86_64,
837            "x86-32" | "x86" | "i386" => rsleigh_api::Architecture::X86_32,
838            "arm32" | "arm" | "ARM32" => rsleigh_api::Architecture::ARM32,
839            "aarch64" | "arm64" | "AArch64" => rsleigh_api::Architecture::AArch64,
840            "mips32" | "mips" | "MIPS32" => rsleigh_api::Architecture::MIPS32,
841            "riscv64" | "riscv" | "RISCV64" => rsleigh_api::Architecture::RiscV64,
842            _ => {
843                eprintln!(
844                    "Unknown arch: {}. Use: x86-64, x86-32, arm32, aarch64, mips32, riscv64",
845                    arch_str
846                );
847                std::process::exit(1);
848            }
849        };
850        run_raw(&data, arch, base, args, all_mode);
851        return;
852    }
853
854    let path = Path::new(binary_path);
855    let obj = match goblin::Object::parse(&data) {
856        Ok(o) => o,
857        Err(e) => {
858            eprintln!("Error: cannot parse binary: {}", e);
859            std::process::exit(1);
860        }
861    };
862
863    // VM-packer family fingerprint. Currently detects PyVMProtect via PE
864    // section-table layout. Emits a one-shot advisory banner so the
865    // analyst knows what scheme they're up against before they sink hours
866    // into manual reversing.
867    if let Some(fp) = rsleigh_decompile::vm_fingerprint::detect(&obj) {
868        eprint!("{}", rsleigh_decompile::vm_fingerprint::banner(&fp));
869    }
870
871    // JMP <reg> tail-call trampolines: 1- or 2-byte gadgets every IAT
872    // call routes through. PyVMProtect uses one at `0x180040770` etc.
873    let trampolines = rsleigh_decompile::jmp_rax_trampoline::scan(&obj, &data);
874    if !trampolines.is_empty() {
875        eprintln!(
876            "// [trampoline] found {} `JMP <reg>` gadget(s):",
877            trampolines.len()
878        );
879        for t in trampolines.iter().take(8) {
880            eprintln!("//   - {:#x}: JMP {}", t.addr, t.reg);
881        }
882        if trampolines.len() > 8 {
883            eprintln!("//   ... and {} more", trampolines.len() - 8);
884        }
885    }
886
887    // XOR-encoded vtable dispatch — VM packers route every handler
888    // through a single CALL [trampoline] preceded by a key+vtable XOR
889    // chain. Detect those dispatchers given the trampoline gadgets we
890    // already found.
891    if !trampolines.is_empty() {
892        let tramp_vas: Vec<u64> = trampolines.iter().map(|t| t.addr).collect();
893        let iat_slots =
894            rsleigh_decompile::xor_vtable::iat_slots_for_trampolines(&obj, &data, &tramp_vas);
895        if !iat_slots.is_empty() {
896            let dispatchers = rsleigh_decompile::xor_vtable::scan(&obj, &data, &iat_slots);
897            if !dispatchers.is_empty() {
898                eprintln!(
899                    "// [xor-vtable] found {} XOR-encoded dispatcher site(s):",
900                    dispatchers.len()
901                );
902                for d in dispatchers.iter().take(8) {
903                    eprintln!(
904                        "//   - call@{:#x} → trampoline_slot={:#x}, key/table slots={:?}",
905                        d.call_site_va,
906                        d.trampoline_slot,
907                        d.data_slots
908                            .iter()
909                            .take(4)
910                            .map(|s| format!("{:#x}", s))
911                            .collect::<Vec<_>>(),
912                    );
913                }
914                if dispatchers.len() > 8 {
915                    eprintln!("//   ... and {} more", dispatchers.len() - 8);
916                }
917                eprintln!(
918                    "// hint: emulate init chain to extract runtime values \
919                     of the listed slots; XOR them to recover the cleartext \
920                     vtable base + handler key."
921                );
922            }
923        }
924    }
925
926    // Hash-resolved API resolver classifier — combines PEB walk with
927    // hash-multiply detection (ROR13 / DJB2 / FNV-1) to label the
928    // resolver function with its hash variant.
929    let resolvers = rsleigh_decompile::api_resolver::scan(&obj, &data);
930    if !resolvers.is_empty() {
931        eprintln!(
932            "// [api-resolver] found {} hash-resolved API resolver(s):",
933            resolvers.len()
934        );
935        for line in rsleigh_decompile::api_resolver::render(&resolvers)
936            .iter()
937            .take(8)
938        {
939            eprintln!("//   - {}", line);
940        }
941        if resolvers.len() > 8 {
942            eprintln!("//   ... and {} more", resolvers.len() - 8);
943        }
944    }
945
946    // PEB-walk anti-debug + API-resolver pattern.
947    let peb_hits = rsleigh_decompile::peb_walk_detect::scan(&obj, &data);
948    if !peb_hits.is_empty() {
949        eprintln!("// [peb-walk] found {} PEB-access site(s):", peb_hits.len());
950        for line in rsleigh_decompile::peb_walk_detect::render(&peb_hits)
951            .iter()
952            .take(8)
953        {
954            eprintln!("//   - {}", line);
955        }
956        if peb_hits.len() > 8 {
957            eprintln!("//   ... and {} more", peb_hits.len() - 8);
958        }
959    }
960
961    // Anti-debug timing probes: RDTSC/RDPMC/RDTSCP pairs within ~256B.
962    if let goblin::Object::PE(pe) = &obj {
963        const IMAGE_SCN_MEM_EXECUTE: u32 = 0x2000_0000;
964        let mut all_probes = Vec::new();
965        for sec in &pe.sections {
966            if sec.characteristics & IMAGE_SCN_MEM_EXECUTE == 0 {
967                continue;
968            }
969            let raddr = sec.pointer_to_raw_data as usize;
970            let rsize = sec.size_of_raw_data as usize;
971            if raddr + rsize > data.len() {
972                continue;
973            }
974            let base_va = pe.image_base as u64 + sec.virtual_address as u64;
975            let (_reads, probes) = rsleigh_decompile::antidebug_timing::scan_region(
976                &data[raddr..raddr + rsize],
977                base_va,
978            );
979            all_probes.extend(probes);
980        }
981        if !all_probes.is_empty() {
982            eprintln!(
983                "// [anti-debug] found {} timing-counter probe(s):",
984                all_probes.len()
985            );
986            for p in all_probes.iter().take(8) {
987                eprintln!(
988                    "//   - {}",
989                    rsleigh_decompile::antidebug_timing::render_probe(p)
990                );
991            }
992            if all_probes.len() > 8 {
993                eprintln!("//   ... and {} more", all_probes.len() - 8);
994            }
995        }
996    }
997
998    // SHA-256 implementation detection via H0/K constant density.
999    let sha_hits = rsleigh_decompile::sha256_func_detect::scan(&obj, &data);
1000    if !sha_hits.is_empty() {
1001        eprintln!("// [sha256] found {} SHA-256 region(s):", sha_hits.len());
1002        for line in rsleigh_decompile::sha256_func_detect::render(&sha_hits)
1003            .iter()
1004            .take(8)
1005        {
1006            eprintln!("//   - {}", line);
1007        }
1008        if sha_hits.len() > 8 {
1009            eprintln!("//   ... and {} more", sha_hits.len() - 8);
1010        }
1011    }
1012
1013    let (arch, segs, mut symbols) = match parse_binary(&obj, &data) {
1014        Some(r) => r,
1015        None => {
1016            eprintln!("Error: unsupported binary format");
1017            std::process::exit(1);
1018        }
1019    };
1020
1021    // Apply FID databases (if --fid passed) to rename anonymous funcs.
1022    apply_fid_to_symbols(&data, arch, &segs, &mut symbols, args);
1023
1024    // Go `.gopclntab` name recovery. Stripped Go binaries carry full
1025    // runtime symbol info in this section; merge into symbols list so
1026    // anonymous func_* entries get their real names (main.main, etc.).
1027    {
1028        let go_syms = rsleigh_decompile::go_pclntab::parse(&data);
1029        if !go_syms.is_empty() {
1030            eprintln!("[go] .gopclntab: {} symbols", go_syms.len());
1031            let existing: std::collections::HashSet<u64> =
1032                symbols.iter().map(|(a, _)| *a).collect();
1033            let pclntab_set: std::collections::HashSet<u64> = go_syms.keys().copied().collect();
1034            for (pc, name) in &go_syms {
1035                if !existing.contains(pc) {
1036                    symbols.push((*pc, name.clone()));
1037                }
1038            }
1039            // Drop anonymous FUN_* / func_* entries that sit inside a
1040            // Go function's stack-check preamble. Go funcs begin with
1041            //   4 bytes: CMP RSP, [R14+0x10]
1042            //   6 bytes: JBE rel32 morestack
1043            // so real body starts at entry+10. The prior function-
1044            // discovery pass treats the body as a separate function via
1045            // CALL-target scan. Remove those spurious entries.
1046            symbols.retain(|(a, n)| {
1047                let is_anon =
1048                    n.starts_with("FUN_") || n.starts_with("func_") || n.starts_with("sub_");
1049                if !is_anon {
1050                    return true;
1051                }
1052                // Check any pclntab entry E where E + 1..=16 == a.
1053                let base = a.saturating_sub(16);
1054                !(base..*a).any(|candidate| pclntab_set.contains(&candidate))
1055            });
1056        }
1057    }
1058
1059    // For stripped PE binaries: discover functions from entry point + CALL targets
1060    if symbols.is_empty() {
1061        if let goblin::Object::PE(pe) = &obj {
1062            let base = pe.image_base as u64;
1063            let entry = base
1064                + pe.header
1065                    .optional_header
1066                    .unwrap()
1067                    .standard_fields
1068                    .address_of_entry_point as u64;
1069            symbols = discover_pe_functions(entry, &segs, &data, arch);
1070        }
1071    }
1072
1073    // Always run PyMethodDef scan for PE64 — even when the export table is
1074    // non-empty, Python C-extensions register most of their methods through
1075    // PyMethodDef arrays rather than direct exports.
1076    if let goblin::Object::PE(pe) = &obj {
1077        if pe.is_64 {
1078            let mut seen: std::collections::HashSet<u64> =
1079                symbols.iter().map(|(a, _)| *a).collect();
1080            for (addr, name) in scan_pymethoddef(&segs, &data) {
1081                if seen.insert(addr) {
1082                    symbols.push((addr, name));
1083                }
1084            }
1085            // PE64 SEH handlers live in .text but are never reached by CALL
1086            // descent, vtable scans, or prologue heuristics — they are only
1087            // visible to the OS exception dispatcher. Enumerate them from
1088            // UNWIND_INFO and register as functions.
1089            let seh = rsleigh_decompile::seh_static::parse_pe64_seh(&data);
1090            for addr in rsleigh_decompile::seh_static::handler_addresses(&seh) {
1091                if seen.insert(addr) {
1092                    symbols.push((addr, format!("seh_handler_{:x}", addr)));
1093                }
1094            }
1095            // Filter functions and __except resumption blocks from
1096            // SCOPE_TABLE — these are reached only by the exception
1097            // dispatcher, never by CALL descent.
1098            for addr in rsleigh_decompile::seh_static::scope_table_addresses(&data) {
1099                if seen.insert(addr) {
1100                    symbols.push((addr, format!("seh_scope_{:x}", addr)));
1101                }
1102            }
1103        }
1104    }
1105
1106    // For stripped ELF binaries: discover functions via entry point, CALL scanning, prologues
1107    // Also trigger for ELF with only import symbols (dynsym but no symtab)
1108    let is_elf_stripped = if let goblin::Object::Elf(elf) = &obj {
1109        elf.syms.len() == 0 || symbols.iter().all(|(_, n)| n.starts_with("FUN_"))
1110    } else {
1111        false
1112    };
1113    if is_elf_stripped || (symbols.is_empty() && matches!(&obj, goblin::Object::Elf(_))) {
1114        if let goblin::Object::Elf(elf) = &obj {
1115            let discovered = discover_elf_functions(elf, &segs, &data, arch);
1116            // Merge: keep existing named symbols, add discovered ones
1117            let existing: std::collections::BTreeSet<u64> =
1118                symbols.iter().map(|(a, _)| *a).collect();
1119            for (addr, name) in discovered {
1120                if !existing.contains(&addr) {
1121                    symbols.push((addr, name));
1122                }
1123            }
1124        }
1125    }
1126
1127    // Scratch-buffer leak detector — alloc + write + return-Py_None
1128    // pattern (PyVMProtect v5 anti-emu trick).
1129    if let goblin::Object::PE(pe) = &obj {
1130        if pe.is_64 {
1131            let iat = rsleigh_decompile::handler_summary::build_iat_map(&obj, &data);
1132            if !iat.is_empty() {
1133                // Sweep CALL rel32 targets in executable sections — symbol
1134                // discovery on PyVMProtect-style binaries is sparse, so the
1135                // raw call-graph is the better candidate set.
1136                let mut call_targets: std::collections::HashSet<u64> =
1137                    symbols.iter().map(|(a, _)| *a).collect();
1138                const IMAGE_SCN_MEM_EXECUTE: u32 = 0x2000_0000;
1139                for sec in &pe.sections {
1140                    if sec.characteristics & IMAGE_SCN_MEM_EXECUTE == 0 {
1141                        continue;
1142                    }
1143                    let raddr = sec.pointer_to_raw_data as usize;
1144                    let rsize = sec.size_of_raw_data as usize;
1145                    if raddr + rsize > data.len() {
1146                        continue;
1147                    }
1148                    let base_va = pe.image_base as u64 + sec.virtual_address as u64;
1149                    let body = &data[raddr..raddr + rsize];
1150                    let mut k = 0;
1151                    while k + 5 <= body.len() {
1152                        if body[k] == 0xe8 {
1153                            let d32 = i32::from_le_bytes([
1154                                body[k + 1],
1155                                body[k + 2],
1156                                body[k + 3],
1157                                body[k + 4],
1158                            ]);
1159                            let next_rip = base_va.wrapping_add((k + 5) as u64);
1160                            let target = next_rip.wrapping_add(d32 as i64 as u64);
1161                            call_targets.insert(target);
1162                        }
1163                        k += 1;
1164                    }
1165                    // Prologue sweep — PyVMProtect indirect-only callees
1166                    // (e.g. const-pool resolver) are missed by call-graph
1167                    // alone. Catch common 64-bit prologues at 16B align.
1168                    let mut k = 0;
1169                    while k + 8 <= body.len() {
1170                        let is_prologue = (body[k] == 0x48
1171                            && body[k + 1] == 0x89
1172                            && body[k + 2] == 0x5c
1173                            && body[k + 3] == 0x24)
1174                            || (body[k] == 0x48 && body[k + 1] == 0x83 && body[k + 2] == 0xec)
1175                            || (body[k] == 0x48 && body[k + 1] == 0x81 && body[k + 2] == 0xec)
1176                            || (body[k] == 0x40 && body[k + 1] == 0x53)
1177                            || (body[k] == 0x40 && body[k + 1] == 0x55)
1178                            || (body[k] == 0x40 && body[k + 1] == 0x57);
1179                        if is_prologue {
1180                            call_targets.insert(base_va + k as u64);
1181                        }
1182                        k += 16;
1183                    }
1184                }
1185                let func_vas: Vec<u64> = call_targets.into_iter().collect();
1186                let leaks = rsleigh_decompile::scratch_leak::scan_functions(
1187                    &obj, &data, &iat, &func_vas, 0x800,
1188                );
1189                if !leaks.is_empty() {
1190                    eprintln!(
1191                        "// [scratch-leak] found {} alloc+return-None pattern(s):",
1192                        leaks.len()
1193                    );
1194                    for line in rsleigh_decompile::scratch_leak::render(&leaks)
1195                        .iter()
1196                        .take(16)
1197                    {
1198                        eprintln!("//   - {}", line);
1199                    }
1200                    if leaks.len() > 16 {
1201                        eprintln!("//   ... and {} more", leaks.len() - 16);
1202                    }
1203                }
1204            }
1205        }
1206    }
1207
1208    // Determine which functions to process
1209    // Skip --flag arguments and their values (e.g., --sigs path.json, --fid file.fidb)
1210    let value_flag_positions: std::collections::HashSet<usize> = args
1211        .iter()
1212        .enumerate()
1213        .filter_map(|(i, a)| {
1214            if a == "--sigs" || a == "--fid" {
1215                Some(i + 1)
1216            } else {
1217                None
1218            }
1219        })
1220        .collect();
1221    let func_args: Vec<&str> = args[2..]
1222        .iter()
1223        .enumerate()
1224        .filter(|(i, a)| {
1225            if a.starts_with("--") {
1226                return false;
1227            }
1228            // Index in the full args array is i + 2.
1229            if value_flag_positions.contains(&(*i + 2)) {
1230                return false;
1231            }
1232            true
1233        })
1234        .map(|(_, a)| a.as_str())
1235        .collect();
1236
1237    if func_args.is_empty() && !all_mode && !disasm_mode {
1238        // List functions. Hide CRT-internal / runtime glue whose names start
1239        // with a single `_` (`_init`, `_fini`, `_start`, `_dl_*`, etc.) but
1240        // KEEP demangled-candidate symbols starting with `_Z` / `__Z` (C++
1241        // Itanium mangling) / `_GLOBAL_` (GCC static init) since those are
1242        // the real program surface area.
1243        let funcs: Vec<(&str, u64)> = symbols
1244            .iter()
1245            .filter(|(_, n)| {
1246                if n.is_empty() {
1247                    return false;
1248                }
1249                if n.starts_with("dyld") {
1250                    return false;
1251                }
1252                if HIDDEN.contains(&n.as_str()) {
1253                    return false;
1254                }
1255                // Allow C++ / Itanium / Swift / static-init names.
1256                if n.starts_with("_Z")
1257                    || n.starts_with("__Z")
1258                    || n.starts_with("_GLOBAL_")
1259                    || n.starts_with("$s")
1260                    || n.starts_with("_$s")
1261                {
1262                    return true;
1263                }
1264                // Hide well-known CRT glue by prefix. Python-visible method
1265                // names (e.g. `_ttokwy5gsm`, `__name__`) start with `_` too,
1266                // so a blanket underscore filter is wrong.
1267                if n.starts_with("_dl_")
1268                    || n.starts_with("__do_global")
1269                    || n.starts_with("__libc_")
1270                    || n.starts_with("__pthread_")
1271                    || n.starts_with("_GLOBAL__sub_I_")
1272                    || matches!(
1273                        n.as_str(),
1274                        "_init" | "_fini" | "_start" | "_DYNAMIC" | "_GLOBAL_OFFSET_TABLE_"
1275                    )
1276                {
1277                    return false;
1278                }
1279                true
1280            })
1281            .map(|(a, n)| (n.as_str(), *a))
1282            .collect();
1283
1284        if json_mode {
1285            // Rich JSON: decompile each function and extract metadata
1286            let path = std::path::Path::new(binary_path);
1287            let mut dec = rsleigh_api::Decoder::new(arch);
1288            let entries: Vec<serde_json::Value> = funcs
1289                .iter()
1290                .map(|(name, addr)| {
1291                    let insts = decode_func(*addr, &symbols, &segs, &data, &mut dec);
1292                    if insts.is_empty() {
1293                        return serde_json::json!({
1294                            "name": name, "address": format!("0x{:x}", addr),
1295                            "size": 0, "calls": [], "strings": [], "return_type": "void"
1296                        });
1297                    }
1298                    let output = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1299                        rsleigh_decompile::decompile_with_binary(
1300                            arch,
1301                            &insts,
1302                            Some(&data),
1303                            Some(path),
1304                        )
1305                    }))
1306                    .map(maybe_annotate_crypto)
1307                    .unwrap_or_default();
1308
1309                    // Extract metadata from decompiled output
1310                    let mut calls = Vec::new();
1311                    let mut strings = Vec::new();
1312                    let mut line_count = 0;
1313                    for line in output.lines() {
1314                        let t = line.trim();
1315                        if t.is_empty() || t.starts_with("//") {
1316                            continue;
1317                        }
1318                        line_count += 1;
1319                        // Extract calls
1320                        if t.contains('(') {
1321                            let check = if let Some(eq) = t.find(" = ") {
1322                                &t[eq + 3..]
1323                            } else {
1324                                t
1325                            };
1326                            if let Some(p) = check.find('(') {
1327                                let callee = check[..p].trim().trim_start_matches("return ");
1328                                if !callee.is_empty()
1329                                    && !callee.contains(' ')
1330                                    && !callee.starts_with('*')
1331                                    && !callee.starts_with('(')
1332                                    && !callee.starts_with("if")
1333                                    && !callee.starts_with("while")
1334                                    && !callee.starts_with("switch")
1335                                    && callee.len() < 50
1336                                    && !calls.contains(&callee.to_string())
1337                                {
1338                                    calls.push(callee.to_string());
1339                                }
1340                            }
1341                        }
1342                        // Extract strings
1343                        if let Some(q1) = t.find('"') {
1344                            if let Some(q2) = t[q1 + 1..].find('"') {
1345                                let s = &t[q1 + 1..q1 + 1 + q2];
1346                                if s.len() >= 2
1347                                    && s.len() <= 80
1348                                    && !strings.contains(&s.to_string())
1349                                {
1350                                    strings.push(s.to_string());
1351                                }
1352                            }
1353                        }
1354                    }
1355                    // Extract return type from first line
1356                    let return_type = output
1357                        .lines()
1358                        .next()
1359                        .and_then(|l| l.split_whitespace().next())
1360                        .unwrap_or("void");
1361                    // Extract param count from signature
1362                    let params = output
1363                        .lines()
1364                        .next()
1365                        .map(|l| l.matches("param_").count())
1366                        .unwrap_or(0);
1367                    let size = insts
1368                        .last()
1369                        .map(|(a, i)| (*a + i.len - addr) as u64)
1370                        .unwrap_or(0);
1371
1372                    serde_json::json!({
1373                        "name": name,
1374                        "address": format!("0x{:x}", addr),
1375                        "size": size,
1376                        "params": params,
1377                        "return_type": return_type,
1378                        "calls": calls,
1379                        "strings": strings,
1380                        "complexity": line_count,
1381                        "pseudocode": output,
1382                    })
1383                })
1384                .collect();
1385            println!(
1386                "{}",
1387                serde_json::to_string_pretty(&serde_json::json!({
1388                    "binary": binary_path,
1389                    "arch": format!("{:?}", arch),
1390                    "function_count": entries.len(),
1391                    "functions": entries,
1392                }))
1393                .unwrap()
1394            );
1395        } else {
1396            eprintln!("Architecture: {:?}", arch);
1397            eprintln!("{} functions:", funcs.len());
1398            for (name, addr) in &funcs {
1399                println!("  0x{:08x}  {}", addr, name);
1400            }
1401        }
1402        return;
1403    }
1404
1405    // Determine target functions
1406    let targets: Vec<String> = if all_mode {
1407        symbols
1408            .iter()
1409            .filter(|(_, n)| {
1410                !n.starts_with('_')
1411                    && !n.starts_with("dyld")
1412                    && !HIDDEN.contains(&n.as_str())
1413                    && !n.is_empty()
1414            })
1415            .map(|(_, n)| n.clone())
1416            .collect()
1417    } else {
1418        func_args.iter().map(|s| s.to_string()).collect()
1419    };
1420
1421    let mut dec = rsleigh_api::Decoder::new(arch);
1422
1423    if disasm_mode {
1424        // Disassembly mode
1425        for name in &targets {
1426            let func_addr =
1427                if let Some(hex) = name.strip_prefix("0x").or_else(|| name.strip_prefix("0X")) {
1428                    u64::from_str_radix(hex, 16).ok()
1429                } else {
1430                    symbols.iter().find(|(_, n)| n == name).map(|(a, _)| *a)
1431                };
1432            if let Some(func_addr) = func_addr {
1433                let insts = decode_func(func_addr, &symbols, &segs, &data, &mut dec);
1434                if json_mode {
1435                    let entries: Vec<serde_json::Value> = insts
1436                        .iter()
1437                        .map(|(a, inst)| {
1438                            serde_json::json!({
1439                                "address": format!("0x{:x}", a),
1440                                "disassembly": inst.disassembly,
1441                                "length": inst.len,
1442                                "pcode_ops": inst.ops.len(),
1443                            })
1444                        })
1445                        .collect();
1446                    println!(
1447                        "{}",
1448                        serde_json::to_string_pretty(&serde_json::json!({
1449                            "function": name, "instructions": entries
1450                        }))
1451                        .unwrap()
1452                    );
1453                } else {
1454                    println!("=== {} (0x{:x}) ===", name, func_addr);
1455                    for (a, inst) in &insts {
1456                        println!("  0x{:08x}  {}", a, inst.disassembly);
1457                    }
1458                }
1459            } else {
1460                eprintln!("Function '{}' not found", name);
1461            }
1462        }
1463        return;
1464    }
1465
1466    // --pcode-json and --ssa-json: dump intermediate state for one or
1467    // more functions. Useful for bench debugging — see exactly what
1468    // P-code the lifter produced and what SSA fold did with it.
1469    let pcode_json = args.iter().any(|a| a == "--pcode-json");
1470    let ssa_json = args.iter().any(|a| a == "--ssa-json");
1471    let opaque_scan = args.iter().any(|a| a == "--opaque-scan");
1472    let smt_explore = args.iter().any(|a| a == "--smt-explore");
1473    if pcode_json || ssa_json || opaque_scan || smt_explore {
1474        for name in &targets {
1475            let func_addr =
1476                if let Some(hex) = name.strip_prefix("0x").or_else(|| name.strip_prefix("0X")) {
1477                    u64::from_str_radix(hex, 16).ok()
1478                } else {
1479                    symbols.iter().find(|(_, n)| n == name).map(|(a, _)| *a)
1480                };
1481            let Some(func_addr) = func_addr else {
1482                eprintln!("Function '{}' not found", name);
1483                continue;
1484            };
1485            let insts = decode_func(func_addr, &symbols, &segs, &data, &mut dec);
1486            if insts.is_empty() {
1487                eprintln!("// {} — no instructions", name);
1488                continue;
1489            }
1490            let func_name = symbols
1491                .iter()
1492                .find(|(a, _)| *a == func_addr)
1493                .map(|(_, n)| n.clone())
1494                .unwrap_or_else(|| format!("func_{:x}", func_addr));
1495            if pcode_json {
1496                let entries: Vec<serde_json::Value> = insts
1497                    .iter()
1498                    .map(|(a, inst)| {
1499                        serde_json::json!({
1500                            "address":     format!("0x{:x}", a),
1501                            "disassembly": inst.disassembly,
1502                            "length":      inst.len,
1503                            "ops":         inst.ops.iter()
1504                                .map(|op| serde_json::json!({ "op": format!("{:?}", op) }))
1505                                .collect::<Vec<_>>(),
1506                        })
1507                    })
1508                    .collect();
1509                println!(
1510                    "{}",
1511                    serde_json::to_string_pretty(&serde_json::json!({
1512                        "function":     func_name,
1513                        "address":      format!("0x{:x}", func_addr),
1514                        "instructions": entries,
1515                    }))
1516                    .unwrap()
1517                );
1518            }
1519            if ssa_json {
1520                let cfg = rsleigh_decompile::cfg::build_cfg(&insts);
1521                let cc = match arch {
1522                    rsleigh_api::Architecture::X86_64
1523                        if rsleigh_decompile::go_pclntab::parse(&data)
1524                            .keys()
1525                            .next()
1526                            .is_some() =>
1527                    {
1528                        rsleigh_decompile::fold::CallingConv::GoAmd64
1529                    }
1530                    rsleigh_api::Architecture::X86_32 | rsleigh_api::Architecture::MIPS32 => {
1531                        rsleigh_decompile::fold::CallingConv::Cdecl32
1532                    }
1533                    rsleigh_api::Architecture::ARM32 => rsleigh_decompile::fold::CallingConv::Arm32,
1534                    rsleigh_api::Architecture::AArch64 => {
1535                        rsleigh_decompile::fold::CallingConv::AArch64
1536                    }
1537                    _ => rsleigh_decompile::fold::CallingConv::SysV,
1538                };
1539                let mut ssa = rsleigh_decompile::ssa::build_ssa_with_cc(&cfg, cc);
1540                rsleigh_decompile::fold::fold_with_cc(&mut ssa, cc);
1541                let blocks: Vec<serde_json::Value> = ssa
1542                    .blocks
1543                    .iter()
1544                    .enumerate()
1545                    .map(|(bi, blk)| {
1546                        let stmts: Vec<serde_json::Value> = blk
1547                            .stmts
1548                            .iter()
1549                            .map(|s| serde_json::json!({ "stmt": format!("{:?}", s) }))
1550                            .collect();
1551                        serde_json::json!({
1552                            "id":         bi,
1553                            "addr":       format!("0x{:x}", blk.addr),
1554                            "stmts":      stmts,
1555                            "terminator": format!("{:?}", blk.terminator),
1556                        })
1557                    })
1558                    .collect();
1559                let vars: Vec<serde_json::Value> = ssa
1560                    .vars
1561                    .iter()
1562                    .enumerate()
1563                    .map(|(vi, v)| {
1564                        serde_json::json!({
1565                            "id":           vi,
1566                            "varnode":      format!("{:?}", v.varnode),
1567                            "expr":         format!("{:?}", v.expr),
1568                            "size":         v.size,
1569                            "param_name":   v.param_name,
1570                            "inferred":     format!("{:?}", v.inferred_type),
1571                            "call_return":  v.call_return,
1572                        })
1573                    })
1574                    .collect();
1575                println!(
1576                    "{}",
1577                    serde_json::to_string_pretty(&serde_json::json!({
1578                        "function": func_name,
1579                        "address":  format!("0x{:x}", func_addr),
1580                        "blocks":   blocks,
1581                        "vars":     vars,
1582                    }))
1583                    .unwrap()
1584                );
1585            }
1586            if opaque_scan {
1587                let cfg = rsleigh_decompile::cfg::build_cfg(&insts);
1588                let cc = match arch {
1589                    rsleigh_api::Architecture::X86_64
1590                        if rsleigh_decompile::go_pclntab::parse(&data)
1591                            .keys()
1592                            .next()
1593                            .is_some() =>
1594                    {
1595                        rsleigh_decompile::fold::CallingConv::GoAmd64
1596                    }
1597                    rsleigh_api::Architecture::X86_32 | rsleigh_api::Architecture::MIPS32 => {
1598                        rsleigh_decompile::fold::CallingConv::Cdecl32
1599                    }
1600                    rsleigh_api::Architecture::ARM32 => rsleigh_decompile::fold::CallingConv::Arm32,
1601                    rsleigh_api::Architecture::AArch64 => {
1602                        rsleigh_decompile::fold::CallingConv::AArch64
1603                    }
1604                    _ => rsleigh_decompile::fold::CallingConv::SysV,
1605                };
1606                let mut ssa = rsleigh_decompile::ssa::build_ssa_with_cc(&cfg, cc);
1607                rsleigh_decompile::fold::fold_with_cc(&mut ssa, cc);
1608                let findings = rsleigh_decompile::opaque_pred::scan_opaque_branches(&ssa);
1609                if findings.is_empty() {
1610                    println!("// {} 0x{:x} — no opaque branches", func_name, func_addr);
1611                } else {
1612                    println!(
1613                        "// {} 0x{:x} — {} opaque branch(es)",
1614                        func_name,
1615                        func_addr,
1616                        findings.len()
1617                    );
1618                    for f in findings {
1619                        println!(
1620                            "  block 0x{:x}  cond=v{}  free_vars={}  -> {:?}",
1621                            f.block_addr, f.cond.0, f.free_var_count, f.class
1622                        );
1623                    }
1624                }
1625            }
1626            if smt_explore {
1627                run_smt_explore(&data, arch, func_addr, &func_name, &insts, json_mode);
1628            }
1629        }
1630        return;
1631    }
1632
1633    // Decompile mode — two-pass for interprocedural type propagation
1634    // Pass 1: quick decompile all targets to learn parameter/return types + struct params
1635    if all_mode && targets.len() > 1 {
1636        let mut learned: Vec<rsleigh_decompile::LearnedFuncType> = Vec::new();
1637        let mut callsite_returns: Vec<(u64, &'static str)> = Vec::new();
1638        let mut learned_structs: Vec<rsleigh_decompile::LearnedStructParam> = Vec::new();
1639
1640        for name in &targets {
1641            let func_addr =
1642                if let Some(hex) = name.strip_prefix("0x").or_else(|| name.strip_prefix("0X")) {
1643                    u64::from_str_radix(hex, 16).ok()
1644                } else {
1645                    symbols.iter().find(|(_, n)| n == name).map(|(a, _)| *a)
1646                };
1647            if let Some(func_addr) = func_addr {
1648                let insts = decode_func(func_addr, &symbols, &segs, &data, &mut dec);
1649                if !insts.is_empty() {
1650                    // Extract learned types from this function
1651                    if let Some(lt) =
1652                        rsleigh_decompile::extract_learned_types(arch, &insts, Some(&data))
1653                    {
1654                        learned.push(lt);
1655                    }
1656                    // Infer callee return types from how this function uses call results
1657                    let returns =
1658                        rsleigh_decompile::infer_returns_from_callsites(arch, &insts, Some(&data));
1659                    callsite_returns.extend(returns);
1660
1661                    // Extract struct param identifications from decompiled output
1662                    let output = rsleigh_decompile::decompile_with_binary(
1663                        arch,
1664                        &insts,
1665                        Some(&data),
1666                        Some(path),
1667                    );
1668                    let structs = rsleigh_decompile::extract_learned_structs(func_addr, &output);
1669                    learned_structs.extend(structs);
1670                }
1671            }
1672        }
1673
1674        // Merge call-site inferred returns into learned types
1675        callsite_returns.sort_by_key(|(a, _)| *a);
1676        callsite_returns.dedup_by_key(|(a, _)| *a);
1677        for (addr, ret_type) in &callsite_returns {
1678            // Only add if we don't already have a return type for this function
1679            if !learned
1680                .iter()
1681                .any(|lt| lt.addr == *addr && lt.return_type.is_some())
1682            {
1683                learned.push(rsleigh_decompile::LearnedFuncType {
1684                    addr: *addr,
1685                    param_types: Vec::new(),
1686                    return_type: Some(ret_type),
1687                });
1688            }
1689        }
1690
1691        if !learned.is_empty() {
1692            rsleigh_decompile::signatures::register_learned_types(&learned);
1693        }
1694        if !learned_structs.is_empty() {
1695            rsleigh_decompile::signatures::register_learned_structs(&learned_structs);
1696        }
1697    }
1698
1699    // Pass 2: full decompilation with learned types available
1700    let mut results: Vec<serde_json::Value> = Vec::new();
1701
1702    for name in &targets {
1703        // Support hex addresses like 0x1400013f0
1704        let func_addr =
1705            if let Some(hex) = name.strip_prefix("0x").or_else(|| name.strip_prefix("0X")) {
1706                u64::from_str_radix(hex, 16).ok()
1707            } else {
1708                symbols.iter().find(|(_, n)| n == name).map(|(a, _)| *a)
1709            };
1710        if let Some(func_addr) = func_addr {
1711            let output = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1712                decompile_func(func_addr, &symbols, &segs, &data, &mut dec, arch, path)
1713            })) {
1714                Ok(o) => o,
1715                Err(_) => "// decompilation failed (stack overflow)\n".to_string(),
1716            };
1717
1718            if json_mode {
1719                // Extract rich metadata from pseudocode
1720                let mut calls = Vec::new();
1721                let mut strings = Vec::new();
1722                let mut line_count = 0;
1723                for line in output.lines() {
1724                    let t = line.trim();
1725                    if t.is_empty() || t.starts_with("//") {
1726                        continue;
1727                    }
1728                    line_count += 1;
1729                    if t.contains('(') {
1730                        let check = if let Some(eq) = t.find(" = ") {
1731                            &t[eq + 3..]
1732                        } else {
1733                            t
1734                        };
1735                        if let Some(p) = check.find('(') {
1736                            let callee = check[..p].trim().trim_start_matches("return ");
1737                            if !callee.is_empty()
1738                                && !callee.contains(' ')
1739                                && !callee.starts_with('*')
1740                                && !callee.starts_with('(')
1741                                && !callee.starts_with("if")
1742                                && !callee.starts_with("while")
1743                                && !callee.starts_with("switch")
1744                                && callee.len() < 50
1745                                && !calls.contains(&callee.to_string())
1746                            {
1747                                calls.push(callee.to_string());
1748                            }
1749                        }
1750                    }
1751                    if let Some(q1) = t.find('"') {
1752                        if let Some(q2) = t[q1 + 1..].find('"') {
1753                            let s = &t[q1 + 1..q1 + 1 + q2];
1754                            if s.len() >= 2 && s.len() <= 80 && !strings.contains(&s.to_string()) {
1755                                strings.push(s.to_string());
1756                            }
1757                        }
1758                    }
1759                }
1760                let return_type = output
1761                    .lines()
1762                    .next()
1763                    .and_then(|l| l.split_whitespace().next())
1764                    .unwrap_or("void");
1765                let params = output
1766                    .lines()
1767                    .next()
1768                    .map(|l| l.matches("param_").count())
1769                    .unwrap_or(0);
1770                results.push(serde_json::json!({
1771                    "name": name,
1772                    "address": format!("0x{:x}", func_addr),
1773                    "params": params,
1774                    "return_type": return_type,
1775                    "calls": calls,
1776                    "strings": strings,
1777                    "complexity": line_count,
1778                    "pseudocode": output.trim(),
1779                }));
1780            } else {
1781                // Apply token-efficiency modes
1782                let is_compact = args.iter().any(|a| a == "--compact");
1783                let is_brief = args.iter().any(|a| a == "--brief");
1784                let min_comp: usize = args
1785                    .iter()
1786                    .position(|a| a == "--min-complexity")
1787                    .and_then(|i| args.get(i + 1))
1788                    .and_then(|s| s.parse().ok())
1789                    .unwrap_or(0);
1790
1791                // Skip trivial functions
1792                let line_count = output
1793                    .lines()
1794                    .filter(|l| !l.trim().is_empty() && !l.trim().starts_with("//"))
1795                    .count();
1796                if min_comp > 0 && line_count < min_comp {
1797                    continue;
1798                }
1799
1800                let display = if is_brief {
1801                    brief_output(&output)
1802                } else if is_compact {
1803                    compact_output(&output)
1804                } else {
1805                    output.clone()
1806                };
1807
1808                if !display.trim().is_empty() {
1809                    println!("// {}", name);
1810                    for line in display.lines() {
1811                        if !line.trim().is_empty() {
1812                            println!("{}", line);
1813                        }
1814                    }
1815                    println!();
1816                }
1817            }
1818        } else {
1819            eprintln!("Function '{}' not found", name);
1820        }
1821    }
1822
1823    if json_mode {
1824        println!(
1825            "{}",
1826            serde_json::to_string_pretty(&serde_json::json!({
1827                "binary": binary_path,
1828                "arch": format!("{:?}", arch),
1829                "functions": results,
1830            }))
1831            .unwrap()
1832        );
1833    }
1834}
1835
1836/// `--smt-explore <func>`: build SSA, walk straight-line for
1837/// Source -> Sink pairs, ask Z3 to produce a SAT trigger or fail.
1838/// Output: human-readable per-path verdict by default, JSON with
1839/// `--json`.
1840///
1841/// Without `--features smt` this prints a clear "rebuild" hint and
1842/// exits zero (so scripted callers can probe for support).
1843fn run_smt_explore(
1844    data: &[u8],
1845    arch: rsleigh_api::Architecture,
1846    func_addr: u64,
1847    func_name: &str,
1848    insts: &[(u64, pcode_ir::Instruction)],
1849    json: bool,
1850) {
1851    let cfg = rsleigh_decompile::cfg::build_cfg(insts);
1852    let cc = match arch {
1853        rsleigh_api::Architecture::X86_64
1854            if rsleigh_decompile::go_pclntab::parse(data)
1855                .keys()
1856                .next()
1857                .is_some() =>
1858        {
1859            rsleigh_decompile::fold::CallingConv::GoAmd64
1860        }
1861        rsleigh_api::Architecture::X86_32 | rsleigh_api::Architecture::MIPS32 => {
1862            rsleigh_decompile::fold::CallingConv::Cdecl32
1863        }
1864        rsleigh_api::Architecture::ARM32 => rsleigh_decompile::fold::CallingConv::Arm32,
1865        rsleigh_api::Architecture::AArch64 => rsleigh_decompile::fold::CallingConv::AArch64,
1866        _ => rsleigh_decompile::fold::CallingConv::SysV,
1867    };
1868    let mut ssa = rsleigh_decompile::ssa::build_ssa_with_cc(&cfg, cc);
1869    rsleigh_decompile::fold::fold_with_cc(&mut ssa, cc);
1870
1871    let imports = rsleigh_decompile::imports::resolve_imports(data);
1872
1873    use rsleigh_decompile::smt_explore::{collect_paths, solve, PathRejection, SmtFinding};
1874
1875    let paths = match collect_paths(&ssa, &imports) {
1876        Ok(p) => p,
1877        Err(reason) => {
1878            if json {
1879                let payload = serde_json::json!({
1880                    "function": func_name,
1881                    "address":  format!("0x{:x}", func_addr),
1882                    "rejected": format!("{:?}", reason),
1883                    "paths":    [],
1884                });
1885                println!("{}", serde_json::to_string_pretty(&payload).unwrap());
1886            } else {
1887                println!(
1888                    "// {func_name} 0x{func_addr:x} — no v0 paths ({})",
1889                    match reason {
1890                        PathRejection::UnsupportedTerminator(t) => format!("UnsupportedTerminator({t})"),
1891                        PathRejection::PhiInPath => "PhiInPath".to_string(),
1892                        PathRejection::IndirectCall => "IndirectCall".to_string(),
1893                        PathRejection::NoSinkFound => "NoSinkFound".to_string(),
1894                    }
1895                );
1896            }
1897            return;
1898        }
1899    };
1900
1901    let mut findings = Vec::with_capacity(paths.len());
1902    for path in &paths {
1903        let verdict = solve(path, &ssa);
1904        findings.push(serde_json::json!({
1905            "source":  path.source.name,
1906            "source_event": path.source_event,
1907            "sink":    path.sink.name,
1908            "sink_event":   path.sink_event,
1909            "kind":    format!("{:?}", path.sink.kind),
1910            "verdict": match &verdict {
1911                SmtFinding::Reachable { input_bytes } => serde_json::json!({
1912                    "kind":  "Reachable",
1913                    "input": input_bytes
1914                        .iter()
1915                        .map(|(o, b)| serde_json::json!({
1916                            "offset": o,
1917                            "byte":   format!("0x{:02x}", b),
1918                        }))
1919                        .collect::<Vec<_>>(),
1920                }),
1921                SmtFinding::NotReachable => serde_json::json!({ "kind": "NotReachable" }),
1922                SmtFinding::Unsupported(why) => serde_json::json!({
1923                    "kind":   "Unsupported",
1924                    "reason": *why,
1925                }),
1926            },
1927        }));
1928    }
1929
1930    if json {
1931        let payload = serde_json::json!({
1932            "function": func_name,
1933            "address":  format!("0x{:x}", func_addr),
1934            "paths":    findings,
1935        });
1936        println!("{}", serde_json::to_string_pretty(&payload).unwrap());
1937    } else {
1938        println!("// {func_name} 0x{func_addr:x} — {} v0 path(s)", paths.len());
1939        for (i, path) in paths.iter().enumerate() {
1940            let verdict = solve(path, &ssa);
1941            print!(
1942                "  [{i}] {} -> {}  ({:?})  ",
1943                path.source.name, path.sink.name, path.sink.kind
1944            );
1945            match verdict {
1946                SmtFinding::Reachable { input_bytes } => {
1947                    let preview: String = input_bytes
1948                        .iter()
1949                        .take(8)
1950                        .map(|(_, b)| format!("{:02x}", b))
1951                        .collect::<Vec<_>>()
1952                        .join(" ");
1953                    println!(
1954                        "REACHABLE — trigger: {}{}",
1955                        preview,
1956                        if input_bytes.len() > 8 { " ..." } else { "" }
1957                    );
1958                }
1959                SmtFinding::NotReachable => println!("not reachable"),
1960                SmtFinding::Unsupported(why) => println!("unsupported ({why})"),
1961            }
1962        }
1963    }
1964}
1965
1966fn decode_func(
1967    fa: u64,
1968    symbols: &[(u64, String)],
1969    segs: &[(u64, u64, u64)],
1970    data: &[u8],
1971    dec: &mut rsleigh_api::Decoder,
1972) -> Vec<(u64, pcode_ir::Instruction)> {
1973    let off = segs.iter().find_map(|(va, sz, fo)| {
1974        if fa >= *va && fa < va + sz {
1975            Some(fo + (fa - va))
1976        } else {
1977            None
1978        }
1979    });
1980    let Some(off) = off else {
1981        return vec![];
1982    };
1983    let max = 4096.min(data.len() - off as usize);
1984    let raw_bytes = &data[off as usize..off as usize + max];
1985    // Function-start padding skip. When a CALL rel32 target lands on
1986    // inter-function zero padding (or .pdata reports a stale entry into
1987    // an unmapped slot), the bogus decode floods the disassembly. Only
1988    // skip when leading 4 bytes are all 0x00 — that pattern is benign
1989    // padding on x86/x86-64 and the AArch64 `udf #0` trap, neither of
1990    // which is a real function start.
1991    let pad_skip: usize = if raw_bytes.len() >= 4 && raw_bytes[..4] == [0u8; 4] {
1992        raw_bytes
1993            .iter()
1994            .take(32)
1995            .position(|&b| b != 0x00)
1996            .unwrap_or(raw_bytes.len().min(32))
1997    } else {
1998        0
1999    };
2000    let fa = fa + pad_skip as u64;
2001    let bytes = &raw_bytes[pad_skip..];
2002    let max = bytes.len();
2003    let next_func = symbols
2004        .iter()
2005        .filter(|(a, name)| *a > fa && !name.starts_with("seh_scope_"))
2006        .map(|(a, _)| *a)
2007        .min()
2008        .unwrap_or(fa + max as u64);
2009    let decode_max = ((next_func - fa) as usize).min(max);
2010
2011    // Go stack-check preamble extension. Three known shapes on amd64:
2012    //   A. Small frame (0-128 bytes):
2013    //        49 3b 66 10        cmp rsp, [r14+0x10]
2014    //        0f 86 rr rr rr rr  jbe morestack
2015    //   B. Medium frame via LEA (uses RSP-N as comparison value):
2016    //        4c 8d 64 24 ii     lea r12, [rsp-ii]
2017    //        4d 3b 66 10        cmp r12, [r14+0x10]
2018    //        0f 86 rr rr rr rr  jbe morestack
2019    //   C. Large frame (>32K) uses 32-bit displacement in LEA:
2020    //        4c 8d a4 24 ii ii ii ii  lea r12, [rsp-iiiiiiii]
2021    //        4d 3b 66 10
2022    //        0f 86 rr rr rr rr
2023    //
2024    // Function-discovery (CALL-target scan) plants a spurious FUN_
2025    // symbol at the byte past the JBE because morestack never returns
2026    // to the JBE; it jumps back to the function entry. Extend decode_max
2027    // past that FUN_ boundary when a preamble is detected.
2028    let extended_max = {
2029        // Go preamble compares RSP against g.stackguard0 (at offset 0x10)
2030        // OR g.preempt (at 0x18, used for cooperative preemption). Both
2031        // bytes are valid for the ModR/M displacement after `[R14+disp8]`.
2032        let is_stackguard_off = |b: u8| b == 0x10 || b == 0x18;
2033        let is_small = bytes.len() >= 10
2034            && bytes[0] == 0x49
2035            && bytes[1] == 0x3b
2036            && bytes[2] == 0x66
2037            && is_stackguard_off(bytes[3])
2038            && bytes[4] == 0x0f
2039            && bytes[5] == 0x86;
2040        let is_lea8 = bytes.len() >= 15
2041            && bytes[0] == 0x4c
2042            && bytes[1] == 0x8d
2043            && bytes[2] == 0x64
2044            && bytes[3] == 0x24
2045            && bytes[5] == 0x4d
2046            && bytes[6] == 0x3b
2047            && bytes[7] == 0x66
2048            && is_stackguard_off(bytes[8])
2049            && bytes[9] == 0x0f
2050            && bytes[10] == 0x86;
2051        let is_lea32 = bytes.len() >= 18
2052            && bytes[0] == 0x4c
2053            && bytes[1] == 0x8d
2054            && bytes[2] == 0xa4
2055            && bytes[3] == 0x24
2056            && bytes[8] == 0x4d
2057            && bytes[9] == 0x3b
2058            && bytes[10] == 0x66
2059            && is_stackguard_off(bytes[11])
2060            && bytes[12] == 0x0f
2061            && bytes[13] == 0x86;
2062        let mut ext = decode_max;
2063        if is_small || is_lea8 || is_lea32 {
2064            let scan_start = if is_small {
2065                10
2066            } else if is_lea8 {
2067                15
2068            } else {
2069                18
2070            };
2071            let scan_max = max.min(8192);
2072            // Walk forward looking for the NEXT Go preamble (= next
2073            // function boundary). Don't stop on RET — Go funcs have
2074            // early returns, panic exits, and morestack tails BEFORE
2075            // the real function end. Use the next-preamble pattern as
2076            // the only firm boundary.
2077            let mut found_boundary = false;
2078            for i in scan_start..scan_max {
2079                let next_small = bytes[i] == 0x49
2080                    && i + 3 < scan_max
2081                    && bytes[i + 1] == 0x3b
2082                    && bytes[i + 2] == 0x66
2083                    && (bytes[i + 3] == 0x10 || bytes[i + 3] == 0x18);
2084                let next_lea8 = bytes[i] == 0x4c
2085                    && i + 8 < scan_max
2086                    && bytes[i + 1] == 0x8d
2087                    && bytes[i + 2] == 0x64
2088                    && bytes[i + 3] == 0x24
2089                    && bytes[i + 5] == 0x4d
2090                    && bytes[i + 6] == 0x3b
2091                    && bytes[i + 7] == 0x66
2092                    && (bytes[i + 8] == 0x10 || bytes[i + 8] == 0x18);
2093                let next_lea32 = bytes[i] == 0x4c
2094                    && i + 11 < scan_max
2095                    && bytes[i + 1] == 0x8d
2096                    && bytes[i + 2] == 0xa4
2097                    && bytes[i + 3] == 0x24
2098                    && bytes[i + 8] == 0x4d
2099                    && bytes[i + 9] == 0x3b
2100                    && bytes[i + 10] == 0x66
2101                    && (bytes[i + 11] == 0x10 || bytes[i + 11] == 0x18);
2102                if next_small || next_lea8 || next_lea32 {
2103                    ext = ext.max(i);
2104                    found_boundary = true;
2105                    break;
2106                }
2107            }
2108            if !found_boundary {
2109                ext = scan_max;
2110            }
2111        }
2112        ext
2113    };
2114    let decode_max = extended_max;
2115    let mut insts = Vec::new();
2116    let mut io = 0;
2117    while io < decode_max {
2118        match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2119            dec.decode(&bytes[io..], fa + io as u64)
2120        })) {
2121            Ok(Ok(inst)) => {
2122                let l = inst.len as usize;
2123                if l == 0 {
2124                    io += 1;
2125                    continue;
2126                }
2127                insts.push((fa + io as u64, inst));
2128                io += l;
2129            }
2130            Ok(Err(_)) => break,
2131            Err(_) => {
2132                io += 1;
2133            }
2134        }
2135    }
2136    insts
2137}
2138
2139fn decompile_func(
2140    fa: u64,
2141    symbols: &[(u64, String)],
2142    segs: &[(u64, u64, u64)],
2143    data: &[u8],
2144    dec: &mut rsleigh_api::Decoder,
2145    arch: rsleigh_api::Architecture,
2146    path: &Path,
2147) -> String {
2148    let insts = decode_func(fa, symbols, segs, data, dec);
2149    if insts.is_empty() {
2150        return "// no instructions\n".to_string();
2151    }
2152    maybe_annotate_crypto(rsleigh_decompile::decompile_with_binary(
2153        arch,
2154        &insts,
2155        Some(data),
2156        Some(path),
2157    ))
2158}
2159
2160/// Generate a YARA detection rule from binary analysis.
2161/// Extracts unique strings, imports, hex patterns, and crypto signatures.
2162/// Diff two binaries: decompile both, match functions, show unified diff of changes.
2163fn diff_binaries(old_path: &str, new_path: &str, func_filter: &[String]) {
2164    use std::collections::BTreeMap;
2165
2166    eprintln!("Comparing: {} vs {}", old_path, new_path);
2167
2168    // Helper: decompile all functions in a binary, return map of name → pseudocode
2169    let decompile_all = |path: &str| -> BTreeMap<String, String> {
2170        let data = match std::fs::read(path) {
2171            Ok(d) => d,
2172            Err(e) => {
2173                eprintln!("Error reading {}: {}", path, e);
2174                return BTreeMap::new();
2175            }
2176        };
2177        let obj = match goblin::Object::parse(&data) {
2178            Ok(o) => o,
2179            Err(e) => {
2180                eprintln!("Error parsing {}: {}", path, e);
2181                return BTreeMap::new();
2182            }
2183        };
2184        let (arch, segs, mut symbols) = match parse_binary(&obj, &data) {
2185            Some(r) => r,
2186            None => {
2187                eprintln!("Unsupported format: {}", path);
2188                return BTreeMap::new();
2189            }
2190        };
2191
2192        // Discover functions for stripped binaries
2193        if symbols.is_empty() {
2194            if let goblin::Object::PE(pe) = &obj {
2195                let base = pe.image_base as u64;
2196                let entry = base
2197                    + pe.header
2198                        .optional_header
2199                        .unwrap()
2200                        .standard_fields
2201                        .address_of_entry_point as u64;
2202                symbols = discover_pe_functions(entry, &segs, &data, arch);
2203            }
2204        }
2205        let is_elf_stripped = if let goblin::Object::Elf(elf) = &obj {
2206            elf.syms.len() == 0
2207        } else {
2208            false
2209        };
2210        if is_elf_stripped {
2211            if let goblin::Object::Elf(elf) = &obj {
2212                let discovered = discover_elf_functions(elf, &segs, &data, arch);
2213                let existing: std::collections::BTreeSet<u64> =
2214                    symbols.iter().map(|(a, _)| *a).collect();
2215                for (addr, name) in discovered {
2216                    if !existing.contains(&addr) {
2217                        symbols.push((addr, name));
2218                    }
2219                }
2220            }
2221        }
2222
2223        let p = std::path::Path::new(path);
2224        let import_map = build_import_map(&obj, &data);
2225        let mut dec = rsleigh_api::Decoder::new(arch);
2226        let mut result = BTreeMap::new();
2227
2228        for (func_addr, func_name) in &symbols {
2229            let off = segs.iter().find_map(|(va, sz, fo)| {
2230                if *func_addr >= *va && *func_addr < va + sz {
2231                    Some(fo + (func_addr - va))
2232                } else {
2233                    None
2234                }
2235            });
2236            let Some(off) = off else { continue };
2237            let max = 8192.min(data.len().saturating_sub(off as usize));
2238            if max < 2 {
2239                continue;
2240            }
2241            let bytes = &data[off as usize..off as usize + max];
2242
2243            let next_func = symbols
2244                .iter()
2245                .filter(|(a, _)| *a > *func_addr)
2246                .map(|(a, _)| *a)
2247                .min()
2248                .unwrap_or(func_addr + max as u64);
2249            let decode_max = ((next_func - func_addr) as usize).min(max);
2250
2251            let mut insts = Vec::new();
2252            let mut pos = 0;
2253            while pos < decode_max {
2254                match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2255                    dec.decode(&bytes[pos..], func_addr + pos as u64)
2256                })) {
2257                    Ok(Ok(inst)) => {
2258                        let l = inst.len as usize;
2259                        if l == 0 {
2260                            pos += 1;
2261                            continue;
2262                        }
2263                        insts.push((func_addr + pos as u64, inst));
2264                        pos += l;
2265                    }
2266                    _ => {
2267                        pos += 1;
2268                    }
2269                }
2270            }
2271
2272            if !insts.is_empty() {
2273                let output = maybe_annotate_crypto(rsleigh_decompile::decompile_with_binary(
2274                    arch,
2275                    &insts,
2276                    Some(&data),
2277                    Some(p),
2278                ));
2279                if !output.trim().is_empty() {
2280                    result.insert(func_name.clone(), output);
2281                }
2282            }
2283        }
2284        eprintln!("  {}: {} functions decompiled", path, result.len());
2285        result
2286    };
2287
2288    let old_funcs = decompile_all(old_path);
2289    let new_funcs = decompile_all(new_path);
2290
2291    // Match functions and compute diffs
2292    let mut all_names: std::collections::BTreeSet<&String> = std::collections::BTreeSet::new();
2293    for k in old_funcs.keys() {
2294        all_names.insert(k);
2295    }
2296    for k in new_funcs.keys() {
2297        all_names.insert(k);
2298    }
2299
2300    let mut added = 0usize;
2301    let mut removed = 0usize;
2302    let mut changed = 0usize;
2303    let mut unchanged = 0usize;
2304
2305    for name in &all_names {
2306        // Filter if specific functions requested
2307        if !func_filter.is_empty() && !func_filter.iter().any(|f| f.as_str() == name.as_str()) {
2308            continue;
2309        }
2310
2311        let old_code = old_funcs.get(*name);
2312        let new_code = new_funcs.get(*name);
2313
2314        match (old_code, new_code) {
2315            (None, Some(new)) => {
2316                added += 1;
2317                println!("=== ADDED: {} ===", name);
2318                for line in new.lines() {
2319                    println!("\x1b[32m+ {}\x1b[0m", line); // green
2320                }
2321                println!();
2322            }
2323            (Some(old), None) => {
2324                removed += 1;
2325                println!("=== REMOVED: {} ===", name);
2326                for line in old.lines() {
2327                    println!("\x1b[31m- {}\x1b[0m", line); // red
2328                }
2329                println!();
2330            }
2331            (Some(old), Some(new)) => {
2332                if old == new {
2333                    unchanged += 1;
2334                    continue;
2335                }
2336                changed += 1;
2337                println!("=== CHANGED: {} ===", name);
2338                // Simple line-by-line diff
2339                let old_lines: Vec<&str> = old.lines().collect();
2340                let new_lines: Vec<&str> = new.lines().collect();
2341                // Use longest common subsequence for basic diff
2342                let diff = simple_diff(&old_lines, &new_lines);
2343                for (tag, line) in &diff {
2344                    match tag {
2345                        '-' => println!("\x1b[31m- {}\x1b[0m", line),
2346                        '+' => println!("\x1b[32m+ {}\x1b[0m", line),
2347                        ' ' => println!("  {}", line),
2348                        _ => {}
2349                    }
2350                }
2351                println!();
2352            }
2353            (None, None) => {}
2354        }
2355    }
2356
2357    println!("--- Summary ---");
2358    println!("Unchanged: {}", unchanged);
2359    println!("Changed:   {}", changed);
2360    println!("Added:     {}", added);
2361    println!("Removed:   {}", removed);
2362}
2363
2364/// Simple line diff using LCS (longest common subsequence).
2365fn simple_diff<'a>(old: &[&'a str], new: &[&'a str]) -> Vec<(char, &'a str)> {
2366    // Build LCS table
2367    let m = old.len();
2368    let n = new.len();
2369    let mut dp = vec![vec![0u32; n + 1]; m + 1];
2370    for i in 1..=m {
2371        for j in 1..=n {
2372            if old[i - 1] == new[j - 1] {
2373                dp[i][j] = dp[i - 1][j - 1] + 1;
2374            } else {
2375                dp[i][j] = dp[i - 1][j].max(dp[i][j - 1]);
2376            }
2377        }
2378    }
2379
2380    // Backtrack to produce diff
2381    let mut result = Vec::new();
2382    let mut i = m;
2383    let mut j = n;
2384    while i > 0 || j > 0 {
2385        if i > 0 && j > 0 && old[i - 1] == new[j - 1] {
2386            result.push((' ', old[i - 1]));
2387            i -= 1;
2388            j -= 1;
2389        } else if j > 0 && (i == 0 || dp[i][j - 1] >= dp[i - 1][j]) {
2390            result.push(('+', new[j - 1]));
2391            j -= 1;
2392        } else {
2393            result.push(('-', old[i - 1]));
2394            i -= 1;
2395        }
2396    }
2397    result.reverse();
2398    result
2399}
2400
2401/// Build import map from binary for decompilation.
2402fn build_import_map(obj: &goblin::Object, data: &[u8]) -> std::collections::HashMap<u64, String> {
2403    let mut map = std::collections::HashMap::new();
2404    match obj {
2405        goblin::Object::PE(pe) => {
2406            for imp in &pe.imports {
2407                if imp.rva != 0 {
2408                    map.insert(pe.image_base as u64 + imp.rva as u64, imp.name.to_string());
2409                }
2410            }
2411        }
2412        goblin::Object::Elf(elf) => {
2413            for sym in elf.dynsyms.iter() {
2414                if sym.st_value != 0 {
2415                    if let Some(name) = elf.dynstrtab.get_at(sym.st_name) {
2416                        if !name.is_empty() {
2417                            map.insert(sym.st_value, name.to_string());
2418                        }
2419                    }
2420                }
2421            }
2422        }
2423        _ => {}
2424    }
2425    map
2426}
2427
2428/// Generate a one-line summary per function for AI-assisted triage.
2429/// Shows: function name, calls made, strings referenced, patterns detected.
2430fn run_summary(binary_path: &str, data: &[u8]) {
2431    let obj = match goblin::Object::parse(data) {
2432        Ok(o) => o,
2433        Err(e) => {
2434            eprintln!("Error: {}", e);
2435            return;
2436        }
2437    };
2438    let (arch, segs, mut symbols) = match parse_binary(&obj, data) {
2439        Some(r) => r,
2440        None => {
2441            eprintln!("Unsupported format");
2442            return;
2443        }
2444    };
2445    // Discover functions for stripped binaries
2446    if symbols.is_empty() {
2447        if let goblin::Object::PE(pe) = &obj {
2448            let base = pe.image_base as u64;
2449            let entry = base
2450                + pe.header
2451                    .optional_header
2452                    .unwrap()
2453                    .standard_fields
2454                    .address_of_entry_point as u64;
2455            symbols = discover_pe_functions(entry, &segs, data, arch);
2456        }
2457    }
2458    let is_elf_stripped = if let goblin::Object::Elf(elf) = &obj {
2459        elf.syms.len() == 0
2460    } else {
2461        false
2462    };
2463    if is_elf_stripped {
2464        if let goblin::Object::Elf(elf) = &obj {
2465            let discovered = discover_elf_functions(elf, &segs, data, arch);
2466            let existing: std::collections::BTreeSet<u64> =
2467                symbols.iter().map(|(a, _)| *a).collect();
2468            for (addr, name) in discovered {
2469                if !existing.contains(&addr) {
2470                    symbols.push((addr, name));
2471                }
2472            }
2473        }
2474    }
2475
2476    let path = std::path::Path::new(binary_path);
2477    let import_map = build_import_map(&obj, data);
2478    let mut dec = rsleigh_api::Decoder::new(arch);
2479
2480    eprintln!("{} functions in {}", symbols.len(), binary_path);
2481    println!(
2482        "{:<14} {:<25} {:<40} {}",
2483        "Address", "Name", "Calls", "Strings/Patterns"
2484    );
2485    println!("{}", "-".repeat(100));
2486
2487    for (func_addr, func_name) in &symbols {
2488        let off = segs.iter().find_map(|(va, sz, fo)| {
2489            if *func_addr >= *va && *func_addr < va + sz {
2490                Some(fo + (func_addr - va))
2491            } else {
2492                None
2493            }
2494        });
2495        let Some(off) = off else { continue };
2496        let max = 4096.min(data.len().saturating_sub(off as usize));
2497        if max < 2 {
2498            continue;
2499        }
2500        let bytes = &data[off as usize..off as usize + max];
2501
2502        let next_func = symbols
2503            .iter()
2504            .filter(|(a, _)| *a > *func_addr)
2505            .map(|(a, _)| *a)
2506            .min()
2507            .unwrap_or(func_addr + max as u64);
2508        let decode_max = ((next_func - func_addr) as usize).min(max);
2509
2510        let mut insts = Vec::new();
2511        let mut pos = 0;
2512        while pos < decode_max {
2513            match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2514                dec.decode(&bytes[pos..], func_addr + pos as u64)
2515            })) {
2516                Ok(Ok(inst)) => {
2517                    let l = inst.len as usize;
2518                    if l == 0 {
2519                        pos += 1;
2520                        continue;
2521                    }
2522                    insts.push((func_addr + pos as u64, inst));
2523                    pos += l;
2524                }
2525                _ => {
2526                    pos += 1;
2527                }
2528            }
2529        }
2530
2531        if insts.is_empty() {
2532            continue;
2533        }
2534
2535        // Decompile and extract metadata
2536        let output = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2537            rsleigh_decompile::decompile_with_binary(arch, &insts, Some(data), Some(path))
2538        }));
2539        let output = match output {
2540            Ok(o) => o,
2541            Err(_) => continue,
2542        };
2543
2544        // Extract calls
2545        let mut calls = Vec::new();
2546        for line in output.lines() {
2547            let t = line.trim();
2548            if t.contains('(')
2549                && t.contains(')')
2550                && !t.starts_with("//")
2551                && !t.starts_with("if ")
2552                && !t.starts_with("while ")
2553                && !t.starts_with("for ")
2554                && !t.contains(" = ")
2555            {
2556                // Standalone call: func_name(args);
2557                if let Some(paren) = t.find('(') {
2558                    let callee = t[..paren].trim().trim_start_matches("return ");
2559                    if !callee.is_empty() && !callee.contains(' ') && callee.len() < 40 {
2560                        calls.push(callee.to_string());
2561                    }
2562                }
2563            }
2564            // Also extract from assignments: var = func(args);
2565            if let Some(eq) = t.find(" = ") {
2566                let rhs = &t[eq + 3..];
2567                if let Some(paren) = rhs.find('(') {
2568                    let callee = rhs[..paren].trim();
2569                    if !callee.is_empty()
2570                        && !callee.starts_with('*')
2571                        && !callee.starts_with('(')
2572                        && !callee.contains(' ')
2573                        && callee.len() < 40
2574                    {
2575                        if !calls.contains(&callee.to_string()) {
2576                            calls.push(callee.to_string());
2577                        }
2578                    }
2579                }
2580            }
2581        }
2582
2583        // Extract strings
2584        let mut strings = Vec::new();
2585        for line in output.lines() {
2586            let t = line.trim();
2587            if let Some(q1) = t.find('"') {
2588                if let Some(q2) = t[q1 + 1..].find('"') {
2589                    let s = &t[q1 + 1..q1 + 1 + q2];
2590                    if s.len() >= 3 && s.len() <= 40 && !strings.contains(&s.to_string()) {
2591                        strings.push(s.to_string());
2592                    }
2593                }
2594            }
2595        }
2596
2597        // Detect patterns
2598        let mut patterns = Vec::new();
2599        if output.contains("XOR") || output.contains("^ 0x") {
2600            patterns.push("xor");
2601        }
2602        if output.contains("AES") || output.contains("SHA") || output.contains("CRC32") {
2603            patterns.push("crypto");
2604        }
2605        if output.contains("TAINT") {
2606            patterns.push("taint");
2607        }
2608        if output.contains("stack cookie") {
2609            patterns.push("canary");
2610        }
2611        if output.contains("VirtualAlloc") || output.contains("mmap") {
2612            patterns.push("alloc");
2613        }
2614        if output.contains("recv") || output.contains("send") || output.contains("socket") {
2615            patterns.push("network");
2616        }
2617        if output.contains("RegSetValue") || output.contains("RegCreateKey") {
2618            patterns.push("registry");
2619        }
2620        if output.contains("CreateFile") || output.contains("fopen") {
2621            patterns.push("file");
2622        }
2623        if output.contains("system(") || output.contains("exec(") || output.contains("popen(") {
2624            patterns.push("exec");
2625        }
2626
2627        // Format output
2628        let calls_str = if calls.len() > 3 {
2629            format!("{}, +{} more", calls[..3].join(", "), calls.len() - 3)
2630        } else {
2631            calls.join(", ")
2632        };
2633
2634        let mut info_parts = Vec::new();
2635        if !strings.is_empty() {
2636            let s = if strings.len() > 2 {
2637                format!("\"{}\" +{}", strings[0], strings.len() - 1)
2638            } else {
2639                strings
2640                    .iter()
2641                    .map(|s| format!("\"{}\"", s))
2642                    .collect::<Vec<_>>()
2643                    .join(" ")
2644            };
2645            info_parts.push(s);
2646        }
2647        if !patterns.is_empty() {
2648            info_parts.push(format!("[{}]", patterns.join(",")));
2649        }
2650
2651        println!(
2652            "0x{:012x} {:<25} {:<40} {}",
2653            func_addr,
2654            func_name,
2655            calls_str,
2656            info_parts.join(" ")
2657        );
2658    }
2659}
2660
2661/// Show cross-references for a function: callers, callees, strings, data refs.
2662fn run_xrefs(binary_path: &str, data: &[u8], target_name: &str) {
2663    if target_name.is_empty() {
2664        eprintln!("Usage: rsleigh <binary> --xrefs <func_name>");
2665        return;
2666    }
2667
2668    let obj = match goblin::Object::parse(data) {
2669        Ok(o) => o,
2670        Err(e) => {
2671            eprintln!("Error: {}", e);
2672            return;
2673        }
2674    };
2675    let (arch, segs, mut symbols) = match parse_binary(&obj, data) {
2676        Some(r) => r,
2677        None => {
2678            eprintln!("Unsupported format");
2679            return;
2680        }
2681    };
2682    if symbols.is_empty() {
2683        if let goblin::Object::PE(pe) = &obj {
2684            let base = pe.image_base as u64;
2685            let entry = base
2686                + pe.header
2687                    .optional_header
2688                    .unwrap()
2689                    .standard_fields
2690                    .address_of_entry_point as u64;
2691            symbols = discover_pe_functions(entry, &segs, data, arch);
2692        }
2693    }
2694    let is_elf_stripped = if let goblin::Object::Elf(elf) = &obj {
2695        elf.syms.len() == 0
2696    } else {
2697        false
2698    };
2699    if is_elf_stripped {
2700        if let goblin::Object::Elf(elf) = &obj {
2701            let discovered = discover_elf_functions(elf, &segs, data, arch);
2702            let existing: std::collections::BTreeSet<u64> =
2703                symbols.iter().map(|(a, _)| *a).collect();
2704            for (addr, name) in discovered {
2705                if !existing.contains(&addr) {
2706                    symbols.push((addr, name));
2707                }
2708            }
2709        }
2710    }
2711
2712    // Find the target function
2713    let target_addr = if let Some(hex) = target_name.strip_prefix("0x") {
2714        u64::from_str_radix(hex, 16).ok()
2715    } else {
2716        symbols
2717            .iter()
2718            .find(|(_, n)| n == target_name)
2719            .map(|(a, _)| *a)
2720    };
2721    let Some(target_addr) = target_addr else {
2722        eprintln!("Function '{}' not found", target_name);
2723        return;
2724    };
2725    let target_display = symbols
2726        .iter()
2727        .find(|(a, _)| *a == target_addr)
2728        .map(|(_, n)| n.as_str())
2729        .unwrap_or(target_name);
2730
2731    let path = std::path::Path::new(binary_path);
2732    let mut dec = rsleigh_api::Decoder::new(arch);
2733
2734    // Phase 1: Decompile target function to find its callees and strings
2735    let mut callees = Vec::new();
2736    let mut strings_in_target = Vec::new();
2737    let mut target_output = String::new();
2738    {
2739        let off = segs.iter().find_map(|(va, sz, fo)| {
2740            if target_addr >= *va && target_addr < va + sz {
2741                Some(fo + (target_addr - va))
2742            } else {
2743                None
2744            }
2745        });
2746        if let Some(off) = off {
2747            let max = 8192.min(data.len().saturating_sub(off as usize));
2748            let bytes = &data[off as usize..off as usize + max];
2749            let next_func = symbols
2750                .iter()
2751                .filter(|(a, _)| *a > target_addr)
2752                .map(|(a, _)| *a)
2753                .min()
2754                .unwrap_or(target_addr + max as u64);
2755            let decode_max = ((next_func - target_addr) as usize).min(max);
2756            let mut insts = Vec::new();
2757            let mut pos = 0;
2758            while pos < decode_max {
2759                match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2760                    dec.decode(&bytes[pos..], target_addr + pos as u64)
2761                })) {
2762                    Ok(Ok(inst)) => {
2763                        let l = inst.len as usize;
2764                        if l == 0 {
2765                            pos += 1;
2766                            continue;
2767                        }
2768                        insts.push((target_addr + pos as u64, inst));
2769                        pos += l;
2770                    }
2771                    _ => {
2772                        pos += 1;
2773                    }
2774                }
2775            }
2776            if !insts.is_empty() {
2777                target_output = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2778                    rsleigh_decompile::decompile_with_binary(arch, &insts, Some(data), Some(path))
2779                }))
2780                .unwrap_or_default();
2781            }
2782        }
2783        // Extract callees and strings from decompiled output
2784        for line in target_output.lines() {
2785            let t = line.trim();
2786            // Extract function calls
2787            if t.contains('(') && !t.starts_with("//") {
2788                if let Some(paren) = t.find('(') {
2789                    let before = match t.find(" = ") {
2790                        Some(eq) if eq + 3 <= paren => &t[eq + 3..paren],
2791                        _ => &t[..paren],
2792                    };
2793                    let callee = before.trim().trim_start_matches("return ");
2794                    if !callee.is_empty()
2795                        && !callee.contains(' ')
2796                        && !callee.starts_with('*')
2797                        && !callee.starts_with('(')
2798                        && !callee.starts_with("if")
2799                        && !callee.starts_with("while")
2800                        && callee.len() < 50
2801                        && !callees.contains(&callee.to_string())
2802                    {
2803                        callees.push(callee.to_string());
2804                    }
2805                }
2806            }
2807            // Extract strings
2808            if let Some(q1) = t.find('"') {
2809                if let Some(q2) = t[q1 + 1..].find('"') {
2810                    let s = &t[q1 + 1..q1 + 1 + q2];
2811                    if s.len() >= 2 && s.len() <= 60 {
2812                        strings_in_target.push(s.to_string());
2813                    }
2814                }
2815            }
2816        }
2817    }
2818
2819    // Phase 2: Scan ALL functions to find callers (functions that call target)
2820    let mut callers = Vec::new();
2821    for (func_addr, func_name) in &symbols {
2822        if *func_addr == target_addr {
2823            continue;
2824        }
2825        let off = segs.iter().find_map(|(va, sz, fo)| {
2826            if *func_addr >= *va && *func_addr < va + sz {
2827                Some(fo + (func_addr - va))
2828            } else {
2829                None
2830            }
2831        });
2832        let Some(off) = off else { continue };
2833        let max = 4096.min(data.len().saturating_sub(off as usize));
2834        if max < 2 {
2835            continue;
2836        }
2837        let bytes = &data[off as usize..off as usize + max];
2838        let next_func = symbols
2839            .iter()
2840            .filter(|(a, _)| *a > *func_addr)
2841            .map(|(a, _)| *a)
2842            .min()
2843            .unwrap_or(func_addr + max as u64);
2844        let decode_max = ((next_func - func_addr) as usize).min(max);
2845        let mut insts = Vec::new();
2846        let mut pos = 0;
2847        while pos < decode_max {
2848            match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
2849                dec.decode(&bytes[pos..], func_addr + pos as u64)
2850            })) {
2851                Ok(Ok(inst)) => {
2852                    let l = inst.len as usize;
2853                    if l == 0 {
2854                        pos += 1;
2855                        continue;
2856                    }
2857                    // Check if this instruction calls the target
2858                    let dis = &inst.disassembly;
2859                    if dis.starts_with("CALL ") || dis.starts_with("BL ") {
2860                        if let Some(target_str) = dis.split_whitespace().nth(1) {
2861                            if let Some(hex) = target_str.strip_prefix("0x") {
2862                                if let Ok(addr) = u64::from_str_radix(hex, 16) {
2863                                    if addr == target_addr {
2864                                        callers.push((func_addr.clone(), func_name.clone()));
2865                                    }
2866                                }
2867                            }
2868                        }
2869                    }
2870                    insts.push((func_addr + pos as u64, inst));
2871                    pos += l;
2872                }
2873                _ => {
2874                    pos += 1;
2875                }
2876            }
2877        }
2878    }
2879
2880    // Output
2881    println!(
2882        "=== Cross-references for {} (0x{:x}) ===",
2883        target_display, target_addr
2884    );
2885    println!();
2886    let mut caller_counts: std::collections::BTreeMap<(u64, String), usize> =
2887        std::collections::BTreeMap::new();
2888    for (addr, name) in &callers {
2889        *caller_counts.entry((*addr, name.clone())).or_insert(0) += 1;
2890    }
2891    let n_callers = caller_counts.len();
2892    let n_sites = callers.len();
2893    println!(
2894        "Called by ({} {}, {} call site{}):",
2895        n_callers,
2896        if n_callers == 1 { "caller" } else { "callers" },
2897        n_sites,
2898        if n_sites == 1 { "" } else { "s" }
2899    );
2900    if caller_counts.is_empty() {
2901        println!("  (none found — may be called indirectly or is entry point)");
2902    }
2903    for ((addr, name), count) in &caller_counts {
2904        if *count > 1 {
2905            println!("  0x{:012x}  {}  (×{})", addr, name, count);
2906        } else {
2907            println!("  0x{:012x}  {}", addr, name);
2908        }
2909    }
2910    println!();
2911    println!("Calls ({} callees):", callees.len());
2912    for callee in &callees {
2913        println!("  {}", callee);
2914    }
2915    println!();
2916    if !strings_in_target.is_empty() {
2917        println!("Strings ({}):", strings_in_target.len());
2918        for s in &strings_in_target {
2919            println!("  \"{}\"", s);
2920        }
2921        println!();
2922    }
2923    println!("Decompiled output:");
2924    println!("{}", target_output);
2925}
2926
2927/// Search for functions matching a query: string, API call, or hex constant.
2928fn run_search(
2929    binary_path: &str,
2930    data: &[u8],
2931    query: &str,
2932    api_mode: bool,
2933    const_mode: bool,
2934    tag_mode: bool,
2935    decompile_results: bool,
2936    json_output: bool,
2937) {
2938    let obj = match goblin::Object::parse(data) {
2939        Ok(o) => o,
2940        Err(e) => {
2941            eprintln!("Error: {}", e);
2942            return;
2943        }
2944    };
2945    let (arch, segs, mut symbols) = match parse_binary(&obj, data) {
2946        Some(r) => r,
2947        None => {
2948            eprintln!("Unsupported format");
2949            return;
2950        }
2951    };
2952    if symbols.is_empty() {
2953        if let goblin::Object::PE(pe) = &obj {
2954            let base = pe.image_base as u64;
2955            let entry = base
2956                + pe.header
2957                    .optional_header
2958                    .unwrap()
2959                    .standard_fields
2960                    .address_of_entry_point as u64;
2961            symbols = discover_pe_functions(entry, &segs, data, arch);
2962        }
2963    }
2964    let is_elf_stripped = if let goblin::Object::Elf(elf) = &obj {
2965        elf.syms.len() == 0
2966    } else {
2967        false
2968    };
2969    if is_elf_stripped {
2970        if let goblin::Object::Elf(elf) = &obj {
2971            let discovered = discover_elf_functions(elf, &segs, data, arch);
2972            let existing: std::collections::BTreeSet<u64> =
2973                symbols.iter().map(|(a, _)| *a).collect();
2974            for (addr, name) in discovered {
2975                if !existing.contains(&addr) {
2976                    symbols.push((addr, name));
2977                }
2978            }
2979        }
2980    }
2981
2982    let path = std::path::Path::new(binary_path);
2983    let mut dec = rsleigh_api::Decoder::new(arch);
2984    let query_lower = query.to_lowercase();
2985
2986    let mode_str = if api_mode {
2987        " (API)"
2988    } else if const_mode {
2989        " (const)"
2990    } else if tag_mode {
2991        " (tag)"
2992    } else {
2993        ""
2994    };
2995    eprintln!(
2996        "Searching {} functions for '{}'{}...",
2997        symbols.len(),
2998        query,
2999        mode_str
3000    );
3001
3002    // matches: (addr, name, reason, context, pseudocode)
3003    let mut matches: Vec<(u64, String, String, String, String)> = Vec::new();
3004
3005    // Tag-based search: decompile all, extract tags, filter
3006    if tag_mode {
3007        let search_tags: Vec<&str> = query.split(',').map(|s| s.trim()).collect();
3008        for (func_addr, func_name) in &symbols {
3009            let insts = decode_func(*func_addr, &symbols, &segs, data, &mut dec);
3010            if insts.is_empty() {
3011                continue;
3012            }
3013            let output = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3014                rsleigh_decompile::decompile_with_binary(arch, &insts, Some(data), Some(path))
3015            })) {
3016                Ok(o) => o,
3017                Err(_) => continue,
3018            };
3019
3020            let meta =
3021                rsleigh_decompile::analysis::extract_function_meta(func_name, *func_addr, &output);
3022            let has_tag = search_tags
3023                .iter()
3024                .any(|t| meta.tags.iter().any(|mt| mt == t));
3025            if has_tag {
3026                let matched_tags: Vec<&str> = meta
3027                    .tags
3028                    .iter()
3029                    .filter(|t| search_tags.contains(&t.as_str()))
3030                    .map(|t| t.as_str())
3031                    .collect();
3032                let calls_str = if meta.calls.len() > 3 {
3033                    format!("{}, +{}", meta.calls[..3].join(", "), meta.calls.len() - 3)
3034                } else {
3035                    meta.calls.join(", ")
3036                };
3037                matches.push((
3038                    *func_addr,
3039                    func_name.clone(),
3040                    format!("tags: [{}]", matched_tags.join(",")),
3041                    calls_str,
3042                    output,
3043                ));
3044            }
3045        }
3046        // Skip the rest of the function and go to output
3047        return output_search_results(&matches, query, json_output, decompile_results);
3048    }
3049
3050    for (func_addr, func_name) in &symbols {
3051        // Quick pre-filter: check function name first
3052        if !api_mode && !const_mode && func_name.to_lowercase().contains(&query_lower) {
3053            matches.push((
3054                func_addr.clone(),
3055                func_name.clone(),
3056                "name match".to_string(),
3057                String::new(),
3058                String::new(),
3059            ));
3060            continue;
3061        }
3062
3063        let off = segs.iter().find_map(|(va, sz, fo)| {
3064            if *func_addr >= *va && *func_addr < va + sz {
3065                Some(fo + (func_addr - va))
3066            } else {
3067                None
3068            }
3069        });
3070        let Some(off) = off else { continue };
3071        let max = 4096.min(data.len().saturating_sub(off as usize));
3072        if max < 2 {
3073            continue;
3074        }
3075        let bytes = &data[off as usize..off as usize + max];
3076
3077        let next_func = symbols
3078            .iter()
3079            .filter(|(a, _)| *a > *func_addr)
3080            .map(|(a, _)| *a)
3081            .min()
3082            .unwrap_or(func_addr + max as u64);
3083        let decode_max = ((next_func - func_addr) as usize).min(max);
3084
3085        // For API mode: decompile and search for function call pattern "api_name("
3086        if api_mode {
3087            let mut insts = Vec::new();
3088            let mut pos = 0;
3089            while pos < decode_max {
3090                match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3091                    dec.decode(&bytes[pos..], func_addr + pos as u64)
3092                })) {
3093                    Ok(Ok(inst)) => {
3094                        let l = inst.len as usize;
3095                        if l == 0 {
3096                            pos += 1;
3097                            continue;
3098                        }
3099                        insts.push((func_addr + pos as u64, inst));
3100                        pos += l;
3101                    }
3102                    _ => {
3103                        pos += 1;
3104                    }
3105                }
3106            }
3107            if insts.is_empty() {
3108                continue;
3109            }
3110            let output = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3111                rsleigh_decompile::decompile_with_binary(arch, &insts, Some(data), Some(path))
3112            })) {
3113                Ok(o) => o,
3114                Err(_) => continue,
3115            };
3116            // Search for "api_name(" pattern — must be a call, not just a substring
3117            let call_pattern = format!("{}(", query);
3118            if output.contains(&call_pattern) {
3119                let context_line = output
3120                    .lines()
3121                    .find(|l| l.contains(&call_pattern) && !l.trim().starts_with("//"))
3122                    .unwrap_or("")
3123                    .trim()
3124                    .to_string();
3125                let context = if context_line.len() > 80 {
3126                    format!("{}...", &context_line[..80])
3127                } else {
3128                    context_line
3129                };
3130                matches.push((
3131                    *func_addr,
3132                    func_name.clone(),
3133                    format!("calls {}", query),
3134                    context,
3135                    output,
3136                ));
3137            }
3138            continue;
3139        }
3140
3141        // For const mode: search for the hex constant in instruction bytes
3142        if const_mode {
3143            let const_val = if let Some(hex) = query.strip_prefix("0x") {
3144                u64::from_str_radix(hex, 16).ok()
3145            } else {
3146                query.parse::<u64>().ok()
3147            };
3148            if let Some(val) = const_val {
3149                // Search for the constant in instruction immediates
3150                let val_le4 = (val as u32).to_le_bytes();
3151                let val_le8 = val.to_le_bytes();
3152                let val_be4 = (val as u32).to_be_bytes();
3153                let found = if val <= 0xFFFFFFFF {
3154                    bytes[..decode_max]
3155                        .windows(4)
3156                        .any(|w| w == val_le4 || w == val_be4)
3157                } else {
3158                    bytes[..decode_max].windows(8).any(|w| w == val_le8)
3159                };
3160                if found {
3161                    matches.push((
3162                        *func_addr,
3163                        func_name.clone(),
3164                        format!("contains 0x{:x}", val),
3165                        String::new(),
3166                        String::new(),
3167                    ));
3168                }
3169            }
3170            continue;
3171        }
3172
3173        // Default string search: first check raw bytes for the query string
3174        // (much faster than decompiling). If found, decompile for context.
3175        let query_bytes = query.as_bytes();
3176        let has_raw_match = bytes[..decode_max]
3177            .windows(query_bytes.len())
3178            .any(|w| w.eq_ignore_ascii_case(query_bytes));
3179        // Also check for wide string (UTF-16LE)
3180        let wide_query: Vec<u8> = query.bytes().flat_map(|b| [b, 0]).collect();
3181        let has_wide_match = if wide_query.len() <= decode_max {
3182            bytes[..decode_max]
3183                .windows(wide_query.len())
3184                .any(|w| w == wide_query.as_slice())
3185        } else {
3186            false
3187        };
3188
3189        if has_raw_match || has_wide_match {
3190            // Quick match from raw bytes — decompile for context
3191            let mut insts = Vec::new();
3192            let mut pos = 0;
3193            while pos < decode_max {
3194                match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3195                    dec.decode(&bytes[pos..], func_addr + pos as u64)
3196                })) {
3197                    Ok(Ok(inst)) => {
3198                        let l = inst.len as usize;
3199                        if l == 0 {
3200                            pos += 1;
3201                            continue;
3202                        }
3203                        insts.push((func_addr + pos as u64, inst));
3204                        pos += l;
3205                    }
3206                    _ => {
3207                        pos += 1;
3208                    }
3209                }
3210            }
3211            let (context, full_output) = if !insts.is_empty() {
3212                match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3213                    rsleigh_decompile::decompile_with_binary(arch, &insts, Some(data), Some(path))
3214                })) {
3215                    Ok(output) => {
3216                        let ctx = output
3217                            .lines()
3218                            .find(|l| l.to_lowercase().contains(&query_lower))
3219                            .unwrap_or("")
3220                            .trim()
3221                            .to_string();
3222                        (ctx, output)
3223                    }
3224                    Err(_) => (String::new(), String::new()),
3225                }
3226            } else {
3227                (String::new(), String::new())
3228            };
3229            let context = if context.len() > 80 {
3230                format!("{}...", &context[..80])
3231            } else {
3232                context
3233            };
3234            let match_type = if has_wide_match && !has_raw_match {
3235                "wide string"
3236            } else {
3237                "string"
3238            };
3239            matches.push((
3240                *func_addr,
3241                func_name.clone(),
3242                match_type.to_string(),
3243                context,
3244                full_output,
3245            ));
3246            continue;
3247        }
3248
3249        // Fallback: also search by decompiling if no raw match
3250        // (catches computed strings, API names from import resolution, etc.)
3251        // Only do this for short queries that might be API names
3252        if query.len() >= 4 && query.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
3253            let mut insts = Vec::new();
3254            let mut pos = 0;
3255            while pos < decode_max {
3256                match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3257                    dec.decode(&bytes[pos..], func_addr + pos as u64)
3258                })) {
3259                    Ok(Ok(inst)) => {
3260                        let l = inst.len as usize;
3261                        if l == 0 {
3262                            pos += 1;
3263                            continue;
3264                        }
3265                        insts.push((func_addr + pos as u64, inst));
3266                        pos += l;
3267                    }
3268                    _ => {
3269                        pos += 1;
3270                    }
3271                }
3272            }
3273            if !insts.is_empty() {
3274                if let Ok(output) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3275                    rsleigh_decompile::decompile_with_binary(arch, &insts, Some(data), Some(path))
3276                })) {
3277                    if output.to_lowercase().contains(&query_lower) {
3278                        let context = output
3279                            .lines()
3280                            .find(|l| l.to_lowercase().contains(&query_lower))
3281                            .unwrap_or("")
3282                            .trim()
3283                            .to_string();
3284                        let context = if context.len() > 80 {
3285                            format!("{}...", &context[..80])
3286                        } else {
3287                            context
3288                        };
3289                        matches.push((
3290                            *func_addr,
3291                            func_name.clone(),
3292                            "pseudocode match".to_string(),
3293                            context,
3294                            output.clone(),
3295                        ));
3296                    }
3297                }
3298            }
3299        }
3300    }
3301
3302    output_search_results(&matches, query, json_output, decompile_results);
3303}
3304
3305/// Format and display search results.
3306fn output_search_results(
3307    matches: &[(u64, String, String, String, String)],
3308    query: &str,
3309    json_output: bool,
3310    decompile_results: bool,
3311) {
3312    if json_output {
3313        let entries: Vec<serde_json::Value> = matches
3314            .iter()
3315            .map(|(addr, name, reason, context, pseudocode)| {
3316                let mut entry = serde_json::json!({
3317                    "address": format!("0x{:x}", addr),
3318                    "name": name,
3319                    "match_type": reason,
3320                });
3321                if !context.is_empty() {
3322                    entry
3323                        .as_object_mut()
3324                        .unwrap()
3325                        .insert("context".to_string(), serde_json::json!(context));
3326                }
3327                if decompile_results && !pseudocode.is_empty() {
3328                    entry
3329                        .as_object_mut()
3330                        .unwrap()
3331                        .insert("pseudocode".to_string(), serde_json::json!(pseudocode));
3332                }
3333                entry
3334            })
3335            .collect();
3336        println!(
3337            "{}",
3338            serde_json::to_string_pretty(&serde_json::json!({
3339                "query": query,
3340                "match_count": matches.len(),
3341                "results": entries,
3342            }))
3343            .unwrap()
3344        );
3345    } else {
3346        println!("{} matches for '{}':", matches.len(), query);
3347        println!();
3348        for (addr, name, reason, context, pseudocode) in matches {
3349            println!("  0x{:012x}  {:<25} {}", addr, name, reason);
3350            if !context.is_empty() {
3351                println!("                  {}", context);
3352            }
3353            if decompile_results && !pseudocode.is_empty() {
3354                println!();
3355                for line in pseudocode.lines() {
3356                    println!("    {}", line);
3357                }
3358                println!();
3359            }
3360        }
3361    }
3362}
3363
3364/// Scan for common vulnerability patterns in decompiled output.
3365fn run_section_scan(binary_path: &str, data: &[u8]) {
3366    let obj = match goblin::Object::parse(data) {
3367        Ok(o) => o,
3368        Err(e) => {
3369            eprintln!("Error: {}", e);
3370            return;
3371        }
3372    };
3373    let mut sec_list: Vec<(String, &[u8], u64)> = Vec::new();
3374    let mut overlay: Option<&[u8]> = None;
3375    match &obj {
3376        goblin::Object::PE(pe) => {
3377            let mut end_fo: usize = 0;
3378            for sec in &pe.sections {
3379                let name = String::from_utf8_lossy(&sec.name)
3380                    .trim_end_matches('\0')
3381                    .to_string();
3382                let fo = sec.pointer_to_raw_data as usize;
3383                let sz = sec.size_of_raw_data as usize;
3384                if fo == 0 || sz == 0 {
3385                    continue;
3386                }
3387                let end = fo.saturating_add(sz).min(data.len());
3388                if fo < end {
3389                    sec_list.push((
3390                        name,
3391                        &data[fo..end],
3392                        sec.virtual_address as u64 + pe.image_base as u64,
3393                    ));
3394                    if end > end_fo {
3395                        end_fo = end;
3396                    }
3397                }
3398            }
3399            if end_fo < data.len() {
3400                overlay = Some(&data[end_fo..]);
3401            }
3402        }
3403        goblin::Object::Elf(elf) => {
3404            for sh in &elf.section_headers {
3405                if sh.sh_type != goblin::elf::section_header::SHT_PROGBITS {
3406                    continue;
3407                }
3408                let fo = sh.sh_offset as usize;
3409                let sz = sh.sh_size as usize;
3410                if sz == 0 {
3411                    continue;
3412                }
3413                let end = fo.saturating_add(sz).min(data.len());
3414                let name = elf.shdr_strtab.get_at(sh.sh_name).unwrap_or("").to_string();
3415                if fo < end {
3416                    sec_list.push((name, &data[fo..end], sh.sh_addr));
3417                }
3418            }
3419        }
3420        goblin::Object::Mach(goblin::mach::Mach::Binary(m)) => {
3421            for seg in &m.segments {
3422                for sec_result in seg {
3423                    if let Ok((sec, sec_data)) = sec_result {
3424                        let name = sec.name().unwrap_or("").to_string();
3425                        if !sec_data.is_empty() {
3426                            sec_list.push((name, sec_data, sec.addr));
3427                        }
3428                    }
3429                }
3430            }
3431        }
3432        _ => {}
3433    }
3434
3435    println!("=== Section Anomaly Scan: {} ===", binary_path);
3436    println!();
3437    println!("{:<24} {:>10}  entropy", "section", "bytes");
3438    for (name, bytes, _va) in &sec_list {
3439        let h = rsleigh_decompile::analysis::shannon_entropy(bytes);
3440        let flag = if h > 7.9 {
3441            " ** HIGH"
3442        } else if h > 7.5 {
3443            " * elevated"
3444        } else {
3445            ""
3446        };
3447        println!("  {:<22} {:>10}  {:>5.2}{}", name, bytes.len(), h, flag);
3448    }
3449    if let Some(ov) = overlay {
3450        if !ov.is_empty() {
3451            let h = rsleigh_decompile::analysis::shannon_entropy(ov);
3452            println!(
3453                "  {:<22} {:>10}  {:>5.2}  (PE overlay)",
3454                "<overlay>",
3455                ov.len(),
3456                h
3457            );
3458        }
3459    }
3460    println!();
3461    let findings = rsleigh_decompile::analysis::scan_section_anomalies(&sec_list, overlay);
3462    if findings.is_empty() {
3463        println!("No anomalies.");
3464    } else {
3465        println!("Findings:");
3466        for f in &findings {
3467            println!("  [{}] {} — {}", f.severity, f.function, f.description);
3468        }
3469    }
3470}
3471
3472fn run_vulnscan(binary_path: &str, data: &[u8]) {
3473    let obj = match goblin::Object::parse(data) {
3474        Ok(o) => o,
3475        Err(e) => {
3476            eprintln!("Error: {}", e);
3477            return;
3478        }
3479    };
3480    let (arch, segs, mut symbols) = match parse_binary(&obj, data) {
3481        Some(r) => r,
3482        None => {
3483            eprintln!("Unsupported");
3484            return;
3485        }
3486    };
3487    if symbols.is_empty() {
3488        if let goblin::Object::PE(pe) = &obj {
3489            let base = pe.image_base as u64;
3490            let entry = base
3491                + pe.header
3492                    .optional_header
3493                    .unwrap()
3494                    .standard_fields
3495                    .address_of_entry_point as u64;
3496            symbols = discover_pe_functions(entry, &segs, data, arch);
3497        }
3498    }
3499    let is_elf_stripped = if let goblin::Object::Elf(elf) = &obj {
3500        elf.syms.len() == 0
3501    } else {
3502        false
3503    };
3504    if is_elf_stripped {
3505        if let goblin::Object::Elf(elf) = &obj {
3506            let discovered = discover_elf_functions(elf, &segs, data, arch);
3507            let existing: std::collections::BTreeSet<u64> =
3508                symbols.iter().map(|(a, _)| *a).collect();
3509            for (addr, name) in discovered {
3510                if !existing.contains(&addr) {
3511                    symbols.push((addr, name));
3512                }
3513            }
3514        }
3515    }
3516
3517    let path = std::path::Path::new(binary_path);
3518    let mut dec = rsleigh_api::Decoder::new(arch);
3519
3520    // Vulnerability patterns: (pattern_in_pseudocode, severity, description)
3521    let vuln_patterns: &[(&str, &str, &str)] = &[
3522        // Buffer overflows
3523        (
3524            "gets(",
3525            "HIGH",
3526            "buffer overflow: gets() has no bounds check",
3527        ),
3528        (
3529            "strcpy(",
3530            "MED",
3531            "buffer overflow: strcpy() has no bounds check",
3532        ),
3533        (
3534            "strcat(",
3535            "MED",
3536            "buffer overflow: strcat() has no bounds check",
3537        ),
3538        (
3539            "sprintf(",
3540            "MED",
3541            "buffer overflow/format string: sprintf() no bounds check",
3542        ),
3543        (
3544            "vsprintf(",
3545            "MED",
3546            "buffer overflow/format string: vsprintf()",
3547        ),
3548        // Format strings
3549        (
3550            "printf(param_",
3551            "HIGH",
3552            "format string: printf() with user-controlled format",
3553        ),
3554        (
3555            "printf(local_",
3556            "HIGH",
3557            "format string: printf() with stack variable format",
3558        ),
3559        (
3560            "fprintf(param_",
3561            "HIGH",
3562            "format string: fprintf() with user-controlled format",
3563        ),
3564        (
3565            "syslog(param_",
3566            "MED",
3567            "format string: syslog() with user-controlled format",
3568        ),
3569        // Command injection
3570        (
3571            "system(param_",
3572            "CRIT",
3573            "command injection: system() with user-controlled argument",
3574        ),
3575        (
3576            "system(local_",
3577            "HIGH",
3578            "command injection: system() with stack variable",
3579        ),
3580        (
3581            "popen(param_",
3582            "CRIT",
3583            "command injection: popen() with user-controlled argument",
3584        ),
3585        (
3586            "exec(param_",
3587            "CRIT",
3588            "command execution: exec() with user-controlled argument",
3589        ),
3590        ("ShellExecute", "MED", "command execution: ShellExecute()"),
3591        ("WinExec(", "MED", "command execution: WinExec()"),
3592        ("CreateProcess", "MED", "process creation: CreateProcess()"),
3593        // Memory issues
3594        (
3595            "free(",
3596            "LOW",
3597            "potential use-after-free: check if pointer used after free()",
3598        ),
3599        ("VirtualAlloc(", "LOW", "executable memory allocation"),
3600        (
3601            "VirtualProtect(",
3602            "MED",
3603            "memory protection change (DEP bypass)",
3604        ),
3605        ("mmap(", "LOW", "memory mapping"),
3606        // Integer issues
3607        (
3608            "malloc(param_",
3609            "MED",
3610            "unchecked allocation: malloc() with user-controlled size",
3611        ),
3612        (
3613            "realloc(param_",
3614            "MED",
3615            "unchecked reallocation with user-controlled size",
3616        ),
3617        // Crypto issues
3618        (
3619            "rand()",
3620            "LOW",
3621            "weak randomness: rand() is not cryptographically secure",
3622        ),
3623        ("srand(", "LOW", "weak randomness: srand() seed"),
3624        // Info disclosure
3625        (
3626            "GetProcAddress(",
3627            "LOW",
3628            "dynamic API resolution (anti-analysis)",
3629        ),
3630        ("LoadLibrary", "LOW", "dynamic library loading"),
3631        // SQL injection
3632        (
3633            "sqlite3_exec(",
3634            "MED",
3635            "potential SQL injection if query contains user input",
3636        ),
3637        ("mysql_query(", "MED", "potential SQL injection"),
3638    ];
3639
3640    eprintln!(
3641        "Scanning {} functions for vulnerability patterns...",
3642        symbols.len()
3643    );
3644    let mut findings: Vec<(String, u64, String, String, String)> = Vec::new(); // (severity, addr, name, vuln, context)
3645
3646    for (func_addr, func_name) in &symbols {
3647        let off = segs.iter().find_map(|(va, sz, fo)| {
3648            if *func_addr >= *va && *func_addr < va + sz {
3649                Some(fo + (func_addr - va))
3650            } else {
3651                None
3652            }
3653        });
3654        let Some(off) = off else { continue };
3655        let max = 4096.min(data.len().saturating_sub(off as usize));
3656        if max < 2 {
3657            continue;
3658        }
3659
3660        let insts = decode_func(*func_addr, &symbols, &segs, data, &mut dec);
3661        if insts.is_empty() {
3662            continue;
3663        }
3664
3665        let output = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
3666            rsleigh_decompile::decompile_with_binary(arch, &insts, Some(data), Some(path))
3667        })) {
3668            Ok(o) => o,
3669            Err(_) => continue,
3670        };
3671
3672        for &(pattern, severity, description) in vuln_patterns {
3673            if output.contains(pattern) {
3674                let context = output
3675                    .lines()
3676                    .find(|l| l.contains(pattern))
3677                    .unwrap_or("")
3678                    .trim()
3679                    .to_string();
3680                let context = if context.len() > 70 {
3681                    format!("{}...", &context[..70])
3682                } else {
3683                    context
3684                };
3685                findings.push((
3686                    severity.to_string(),
3687                    *func_addr,
3688                    func_name.clone(),
3689                    description.to_string(),
3690                    context,
3691                ));
3692            }
3693        }
3694
3695        // Special: check for missing stack cookie in large functions
3696        let has_cookie = output.contains("stack cookie")
3697            || output.contains("__security_check_cookie")
3698            || output.contains("__stack_chk_fail");
3699        let line_count = output.lines().filter(|l| !l.trim().is_empty()).count();
3700        if line_count > 20 && !has_cookie {
3701            findings.push((
3702                "INFO".to_string(),
3703                *func_addr,
3704                func_name.clone(),
3705                "missing stack cookie in large function".to_string(),
3706                String::new(),
3707            ));
3708        }
3709    }
3710
3711    // Section-level anomaly scan: entropy (packed/encrypted) + PE overlay
3712    let mut sec_list: Vec<(String, &[u8], u64)> = Vec::new();
3713    let mut overlay: Option<&[u8]> = None;
3714    match &obj {
3715        goblin::Object::PE(pe) => {
3716            let mut end_fo: usize = 0;
3717            for sec in &pe.sections {
3718                let name = String::from_utf8_lossy(&sec.name)
3719                    .trim_end_matches('\0')
3720                    .to_string();
3721                let fo = sec.pointer_to_raw_data as usize;
3722                let sz = sec.size_of_raw_data as usize;
3723                if fo == 0 || sz == 0 {
3724                    continue;
3725                }
3726                let end = fo.saturating_add(sz).min(data.len());
3727                if fo < end {
3728                    sec_list.push((
3729                        name,
3730                        &data[fo..end],
3731                        sec.virtual_address as u64 + pe.image_base as u64,
3732                    ));
3733                    if end > end_fo {
3734                        end_fo = end;
3735                    }
3736                }
3737            }
3738            if end_fo < data.len() {
3739                overlay = Some(&data[end_fo..]);
3740            }
3741        }
3742        goblin::Object::Elf(elf) => {
3743            for sh in &elf.section_headers {
3744                if sh.sh_type != goblin::elf::section_header::SHT_PROGBITS {
3745                    continue;
3746                }
3747                let fo = sh.sh_offset as usize;
3748                let sz = sh.sh_size as usize;
3749                if sz == 0 {
3750                    continue;
3751                }
3752                let end = fo.saturating_add(sz).min(data.len());
3753                let name = elf.shdr_strtab.get_at(sh.sh_name).unwrap_or("").to_string();
3754                if fo < end {
3755                    sec_list.push((name, &data[fo..end], sh.sh_addr));
3756                }
3757            }
3758        }
3759        goblin::Object::Mach(goblin::mach::Mach::Binary(m)) => {
3760            for seg in &m.segments {
3761                for sec_result in seg {
3762                    if let Ok((sec, sec_data)) = sec_result {
3763                        let name = sec.name().unwrap_or("").to_string();
3764                        if !sec_data.is_empty() {
3765                            sec_list.push((name, sec_data, sec.addr));
3766                        }
3767                    }
3768                }
3769            }
3770        }
3771        _ => {}
3772    }
3773    let sec_findings = rsleigh_decompile::analysis::scan_section_anomalies(&sec_list, overlay);
3774    for f in sec_findings {
3775        findings.push((f.severity, f.address, f.function, f.description, f.context));
3776    }
3777
3778    // Sort by severity
3779    let severity_order = |s: &str| match s {
3780        "CRIT" => 0,
3781        "HIGH" => 1,
3782        "MED" => 2,
3783        "LOW" => 3,
3784        _ => 4,
3785    };
3786    findings.sort_by(|a, b| severity_order(&a.0).cmp(&severity_order(&b.0)));
3787
3788    // Output
3789    println!(
3790        "=== Vulnerability Scan: {} ({} functions) ===",
3791        binary_path,
3792        symbols.len()
3793    );
3794    println!();
3795    let crit = findings.iter().filter(|f| f.0 == "CRIT").count();
3796    let high = findings.iter().filter(|f| f.0 == "HIGH").count();
3797    let med = findings.iter().filter(|f| f.0 == "MED").count();
3798    let low = findings.iter().filter(|f| f.0 == "LOW").count();
3799    println!(
3800        "Summary: {} CRIT, {} HIGH, {} MED, {} LOW ({} total findings)",
3801        crit,
3802        high,
3803        med,
3804        low,
3805        findings.len()
3806    );
3807    println!();
3808    for (severity, addr, name, vuln, context) in &findings {
3809        let color = match severity.as_str() {
3810            "CRIT" => "\x1b[91m",
3811            "HIGH" => "\x1b[31m",
3812            "MED" => "\x1b[33m",
3813            "LOW" => "\x1b[36m",
3814            _ => "\x1b[37m",
3815        };
3816        println!(
3817            "  {}{:<4}\x1b[0m  0x{:012x}  {:<25} {}",
3818            color, severity, addr, name, vuln
3819        );
3820        if !context.is_empty() {
3821            println!("        {}", context);
3822        }
3823    }
3824}
3825
3826/// Extract indicators of compromise (IOCs) from a binary's strings.
3827///
3828/// Pulls ASCII and UTF-16LE strings out of the raw image and bins each
3829/// match into a category that a triage workflow actually wants:
3830///   * URLs (http/https/ftp)
3831///   * IPv4 literals (with octet validation)
3832///   * Domain names (TLD-anchored, length-bounded)
3833///   * Filesystem paths (Win drive letters, %ENVVAR%, /tmp /etc /var /usr)
3834///   * Registry keys (HKEY_*, HKLM\, HKCU\)
3835///   * Windows mutex / kernel object names (Global\, Local\, Session\)
3836///   * Credential/secret-related keywords (token=, password=, api_key=, ...)
3837///
3838/// The point is to give an analyst a one-pager of "where does this binary
3839/// reach out to and what does it touch" without making them grep the strings
3840/// dump by hand. Output is grouped by category, deduped, and sorted.
3841/// Brute single-byte-XOR string recovery. Walks every key 1..=255,
3842/// reports decoded runs of length >= 8 whose source bytes are
3843/// mostly non-printable (so plaintext doesn't dominate output).
3844fn run_xor_strings(binary_path: &str, data: &[u8], json: bool) {
3845    let hits = rsleigh_decompile::xor_strings::brute_decode(data, 8);
3846    if json {
3847        let payload = serde_json::json!({
3848            "binary": binary_path,
3849            "decoded": hits.iter().map(|d| serde_json::json!({
3850                "key":    format!("0x{}", d.key_hex()),
3851                "offset": format!("0x{:x}", d.offset),
3852                "text":   d.text,
3853            })).collect::<Vec<_>>(),
3854        });
3855        println!("{}", serde_json::to_string_pretty(&payload).unwrap());
3856        return;
3857    }
3858    println!("=== XOR-decoded strings from {} ===", binary_path);
3859    if hits.is_empty() {
3860        println!("\n(no decoded strings)");
3861        return;
3862    }
3863    let mut by_key: std::collections::BTreeMap<
3864        Vec<u8>,
3865        Vec<&rsleigh_decompile::xor_strings::Decoded>,
3866    > = std::collections::BTreeMap::new();
3867    for h in &hits {
3868        by_key.entry(h.key.clone()).or_default().push(h);
3869    }
3870
3871    // Brute scan generates many qualifying runs on real binaries.
3872    // For triage, surface the top-5 keys by hit-count (real obfuscated
3873    // tables produce concentrated decode under one key) and cap
3874    // per-key output at 30 lines. Multi-byte keys (Mirai-class) ALWAYS
3875    // print regardless of rank — they're rare hits and very high signal.
3876    let multi_byte: Vec<_> = by_key.iter().filter(|(k, _)| k.len() > 1).collect();
3877    let mut single_byte_ranked: Vec<_> = by_key.iter().filter(|(k, _)| k.len() == 1).collect();
3878    single_byte_ranked.sort_by(|a, b| b.1.len().cmp(&a.1.len()));
3879    let top_single: Vec<_> = single_byte_ranked.into_iter().take(5).collect();
3880
3881    let total_hits = hits.len();
3882    let total_keys = by_key.len();
3883    println!(
3884        "\n{} run(s) across {} key(s); showing all {} multi-byte keys + top 5 single-byte by hit-count.",
3885        total_hits, total_keys, multi_byte.len()
3886    );
3887    let print_group = |key: &[u8], group: &[&rsleigh_decompile::xor_strings::Decoded]| {
3888        let kh: String = key.iter().map(|b| format!("{:02x}", b)).collect();
3889        println!("\nKey 0x{} ({} hit(s)):", kh, group.len());
3890        for d in group.iter().take(30) {
3891            println!("  +{:08x}  {}", d.offset, d.text);
3892        }
3893        if group.len() > 30 {
3894            println!("  ... and {} more", group.len() - 30);
3895        }
3896    };
3897    for (key, group) in &multi_byte {
3898        print_group(key, group);
3899    }
3900    for (key, group) in &top_single {
3901        print_group(key, group);
3902    }
3903}
3904
3905fn run_ioc(binary_path: &str, data: &[u8], json: bool) {
3906    use std::collections::BTreeSet;
3907
3908    // ASCII pass: extract once at min len 4 (to catch short arch
3909    // tokens like `.mips`, `.sh4`), then re-use for both the IOC
3910    // categorizer (filtered to >=6 chars below) and the
3911    // capability/family classifiers. UTF-16LE pass appended after.
3912    let mut texts =
3913        rsleigh_decompile::iot_capabilities::extract_printable_runs(data, 4);
3914    // UTF-16LE pass: read pairs (b, 0x00) of printable bytes.
3915    let mut wide_run: Vec<u8> = Vec::with_capacity(64);
3916    let mut i = 0usize;
3917    while i + 1 < data.len() {
3918        let lo = data[i];
3919        let hi = data[i + 1];
3920        if hi == 0 && ((0x20..0x7f).contains(&lo) || lo == b'\t') {
3921            wide_run.push(lo);
3922            i += 2;
3923        } else {
3924            if wide_run.len() >= 4 {
3925                if let Ok(s) = std::str::from_utf8(&wide_run) {
3926                    texts.push(s.to_string());
3927                }
3928            }
3929            wide_run.clear();
3930            i += 1;
3931        }
3932    }
3933    if wide_run.len() >= 4 {
3934        if let Ok(s) = std::str::from_utf8(&wide_run) {
3935            texts.push(s.to_string());
3936        }
3937    }
3938
3939    let mut urls: BTreeSet<String> = BTreeSet::new();
3940    let mut ips: BTreeSet<String> = BTreeSet::new();
3941    let mut domains: BTreeSet<String> = BTreeSet::new();
3942    let mut paths: BTreeSet<String> = BTreeSet::new();
3943    let mut registry: BTreeSet<String> = BTreeSet::new();
3944    let mut mutexes: BTreeSet<String> = BTreeSet::new();
3945    let mut secrets: BTreeSet<String> = BTreeSet::new();
3946
3947    // TLDs that show up in real malware C2 — short list keeps domain
3948    // detection from matching every "foo.exe" / "bar.dll" string. Add to
3949    // taste; staying conservative beats false positives.
3950    const TLDS: &[&str] = &[
3951        "com", "net", "org", "io", "co", "ru", "cn", "tk", "ml", "ga", "cf",
3952        "xyz", "info", "biz", "top", "online", "site", "pro", "dev", "app",
3953        "gov", "edu", "mil", "uk", "de", "fr", "jp", "br", "in", "us",
3954        "ws", "to", "cc", "su", "icu", "club", "host", "live", "fun", "shop",
3955    ];
3956    let known_tld = |label: &str| TLDS.iter().any(|t| label.eq_ignore_ascii_case(t));
3957
3958    let valid_ipv4 = |s: &str| {
3959        let parts: Vec<&str> = s.split('.').collect();
3960        if parts.len() != 4 {
3961            return false;
3962        }
3963        let octets: Vec<u16> = match parts
3964            .iter()
3965            .map(|p| {
3966                if p.is_empty() || p.len() > 3 || !p.bytes().all(|b| b.is_ascii_digit()) {
3967                    return None;
3968                }
3969                p.parse::<u16>().ok().filter(|n| *n <= 255)
3970            })
3971            .collect::<Option<Vec<_>>>()
3972        {
3973            Some(o) => o,
3974            None => return false,
3975        };
3976        // .NET / file-format versions encode as W.X.Y.Z and routinely have
3977        // multiple zero octets ("4.0.0.0", "3.5.0.0", "2.0.0.0"). Real
3978        // routable IPs almost never have 2+ zero octets — public address
3979        // space won't have a trailing .0 host octet, and netblocks don't
3980        // collapse to .0.0 except as network addresses (still not IOCs).
3981        // Reject when 2 or more octets are zero.
3982        let zeros = octets.iter().filter(|&&n| n == 0).count();
3983        if zeros >= 2 {
3984            return false;
3985        }
3986        if octets.iter().all(|&n| n == 255) {
3987            return false;
3988        }
3989        true
3990    };
3991
3992    let push_domain_if_useful = |domains: &mut BTreeSet<String>, host: &str| {
3993        // Reject obvious file refs and intra-binary noise.
3994        let lower = host.to_ascii_lowercase();
3995        if lower.ends_with(".dll") || lower.ends_with(".exe") || lower.ends_with(".sys")
3996            || lower.ends_with(".pdb") || lower.ends_with(".cpp") || lower.ends_with(".c")
3997            || lower.ends_with(".h") || lower.ends_with(".obj") || lower.ends_with(".lib")
3998            || lower.ends_with(".cs") || lower.ends_with(".vb") || lower.ends_with(".rs")
3999            || lower.ends_with(".py") || lower.ends_with(".js") || lower.ends_with(".ts")
4000            || lower.ends_with(".cab") || lower.ends_with(".msi") || lower.ends_with(".log")
4001            || lower.ends_with(".tmp") || lower.ends_with(".dat") || lower.ends_with(".ini")
4002            || lower.ends_with(".xml") || lower.ends_with(".json") || lower.ends_with(".txt")
4003            || lower.ends_with(".crl") || lower.ends_with(".crt") || lower.ends_with(".cer")
4004            || lower.ends_with(".bak")
4005        {
4006            return;
4007        }
4008        // Domain must have at least one fully-lowercase label (real domains
4009        // are case-insensitive but always written lowercase in malware
4010        // configs). PascalCase identifiers like "System.IO" or
4011        // "MyApplication.app" are CLR namespaces / bundle IDs, not hosts.
4012        if !lower.bytes().any(|b| b.is_ascii_lowercase()) {
4013            return;
4014        }
4015        if !host.split('.').all(|label| {
4016            !label.is_empty() && label.bytes().any(|b| b.is_ascii_lowercase() || b.is_ascii_digit())
4017        }) {
4018            return;
4019        }
4020        // Reject tokens that contain `/` or `\` — that's a Go import path
4021        // ("os/exec.in"), a file path, or other non-host noise.
4022        if host.contains('/') || host.contains('\\') || host.contains(':') {
4023            return;
4024        }
4025        // Reject namespace-like tokens with PascalCase labels.
4026        let pascal_labels = host
4027            .split('.')
4028            .filter(|l| {
4029                l.chars().next().map(|c| c.is_ascii_uppercase()).unwrap_or(false)
4030                    && l.bytes().any(|b| b.is_ascii_lowercase())
4031            })
4032            .count();
4033        if pascal_labels > 0 {
4034            return;
4035        }
4036        if let Some(last_dot) = host.rfind('.') {
4037            let tld = &host[last_dot + 1..];
4038            if known_tld(tld) && host.len() >= 5 && host.len() <= 253 {
4039                domains.insert(host.to_string());
4040            }
4041        }
4042    };
4043
4044    for s in &texts {
4045        // URLs
4046        let mut idx = 0;
4047        let bytes = s.as_bytes();
4048        while idx < bytes.len() {
4049            let rest = &s[idx..];
4050            let url_start = ["http://", "https://", "ftp://"]
4051                .iter()
4052                .filter_map(|p| rest.find(p).map(|n| (n, p.len())))
4053                .min_by_key(|(n, _)| *n);
4054            let Some((rel, plen)) = url_start else { break };
4055            let abs = idx + rel;
4056            let scheme_end = abs + plen;
4057            // Read URL chars until whitespace, control, or " ' < > )
4058            let url_end = s[scheme_end..]
4059                .find(|c: char| {
4060                    c.is_whitespace() || c.is_control()
4061                        || matches!(c, '"' | '\'' | '<' | '>' | ')' | '(' | '`')
4062                })
4063                .map(|n| scheme_end + n)
4064                .unwrap_or(s.len());
4065            if url_end > scheme_end && url_end - abs >= 10 && url_end - abs <= 2048 {
4066                let mut u = s[abs..url_end]
4067                    .trim_end_matches(|c: char| matches!(c, '.' | ',' | ';' | ':' | '/' | ']' | '}'))
4068                    .to_string();
4069                // PE security-directory PKCS7 blobs often immediately follow
4070                // an embedded URL with one or two ASN.1 DER tag bytes (e.g.
4071                // 0x30 0x45 = "0E", 0x30 0x53 = "0S"), which our character
4072                // set treats as continuation. Strip a trailing 1-2 alnum
4073                // tail when the URL ends in a known cert file extension
4074                // suffixed with that tail.
4075                for ext in &["crl", "crt", "cer", "axd", "p7s", "p7b"] {
4076                    let needle = format!(".{}", ext);
4077                    if let Some(idx) = u.to_ascii_lowercase().rfind(&needle) {
4078                        let end = idx + needle.len();
4079                        let tail = &u[end..];
4080                        if !tail.is_empty()
4081                            && tail.len() <= 2
4082                            && tail.bytes().all(|b| b.is_ascii_alphanumeric())
4083                        {
4084                            u.truncate(end);
4085                            break;
4086                        }
4087                    }
4088                }
4089                // Bare-host URL with no path (http://host[junk]) — trim a
4090                // trailing 1-2 char tail of alnum or backslash that the
4091                // PE security blob's DER tags routinely append.
4092                if let Some(scheme_idx) = u.find("://") {
4093                    let host_start = scheme_idx + 3;
4094                    if !u[host_start..].contains('/') && !u[host_start..].contains('?') {
4095                        // Only trim if the host body has a digit-or-letter
4096                        // tail of length 1-2 after the last '.'.
4097                        if let Some(last_dot) = u[host_start..].rfind('.') {
4098                            let abs_dot = host_start + last_dot;
4099                            let after_dot = &u[abs_dot + 1..];
4100                            // TLD is the part after last dot; if there's a
4101                            // tail of >=3 chars where the last 1-2 are
4102                            // single-uppercase or backslash and the prefix
4103                            // looks like a TLD, strip the tail.
4104                            if after_dot.len() >= 3 {
4105                                let tail_start = after_dot
4106                                    .bytes()
4107                                    .position(|b| b == b'\\' || b.is_ascii_uppercase() || b.is_ascii_digit())
4108                                    .unwrap_or(after_dot.len());
4109                                let probable_tld_len = tail_start;
4110                                let tail_len = after_dot.len() - tail_start;
4111                                if probable_tld_len >= 2
4112                                    && tail_len >= 1
4113                                    && tail_len <= 2
4114                                    && after_dot[..probable_tld_len]
4115                                        .bytes()
4116                                        .all(|b| b.is_ascii_lowercase())
4117                                    && known_tld(&after_dot[..probable_tld_len])
4118                                {
4119                                    u.truncate(abs_dot + 1 + probable_tld_len);
4120                                }
4121                            }
4122                        }
4123                    }
4124                }
4125                if u.len() >= 10 {
4126                    urls.insert(u);
4127                }
4128            }
4129            idx = url_end.max(abs + 1);
4130        }
4131
4132        // IPv4 + domain extraction over tokens
4133        for tok in s.split(|c: char| {
4134            !(c.is_alphanumeric() || matches!(c, '.' | '-' | '_' | '\\' | '/' | ':' | '%' | '$'))
4135        }) {
4136            if tok.len() < 4 || tok.len() > 256 {
4137                continue;
4138            }
4139            // IPv4 (allow optional :port suffix)
4140            let (host_part, _) = tok.split_once(':').unwrap_or((tok, ""));
4141            if valid_ipv4(host_part) {
4142                // Reject 0.0.0.0 / 127.0.0.1 obvious-noise filters off — they
4143                // can still be IOC-relevant (e.g. localhost C2 in dev malware).
4144                ips.insert(host_part.to_string());
4145            } else if host_part.contains('.') {
4146                push_domain_if_useful(&mut domains, host_part);
4147            }
4148        }
4149
4150        // File paths. Each candidate must consist of path-valid characters
4151        // throughout, otherwise we end up emitting random binary tokens like
4152        // `%GYK%+caWN,\` or `p:/H(k6` that just happen to start with an
4153        // env-var or drive-letter prefix.
4154        let path_valid = |s: &str| {
4155            s.chars().all(|c| {
4156                c.is_ascii_alphanumeric()
4157                    || matches!(c, '\\' | '/' | '.' | '_' | '-' | ' ' | '%' | '(' | ')'
4158                                 | '$' | '~' | '+' | ':' | '@' | '#' | '{' | '}')
4159            })
4160        };
4161        for tok in s.split_whitespace() {
4162            if tok.len() < 5 || tok.len() > 260 {
4163                continue;
4164            }
4165            // Windows drive-letter paths. Drive letter is conventionally
4166            // uppercase; lowercase first char is almost always a token-split
4167            // artifact ("p:/" out of `cpp:/foo` etc.).
4168            if tok.len() >= 4
4169                && tok.as_bytes()[1] == b':'
4170                && (tok.as_bytes()[2] == b'\\' || tok.as_bytes()[2] == b'/')
4171                && tok.as_bytes()[0].is_ascii_uppercase()
4172                && path_valid(tok)
4173            {
4174                paths.insert(tok.to_string());
4175            }
4176            // Env-var paths. Reject printf-style format strings:
4177            // %s, %d, %lu, %ls, %u, %x, %.3f, %02d etc. carry no IOC value.
4178            // The variable name between the two % must be valid env-var
4179            // syntax (uppercase letters/digits/underscore) AND the remainder
4180            // of the token must be path-valid.
4181            else if tok.starts_with('%')
4182                && tok.len() >= 5
4183                && tok.find('%').and_then(|first| {
4184                    tok[first + 1..].find('%').map(|rel| {
4185                        let inner = &tok[first + 1..first + 1 + rel];
4186                        !inner.is_empty()
4187                            && inner.len() >= 2
4188                            && inner.len() <= 32
4189                            && inner.bytes().all(|b| {
4190                                b.is_ascii_uppercase() || b.is_ascii_digit() || b == b'_'
4191                            })
4192                    })
4193                }).unwrap_or(false)
4194                && path_valid(tok)
4195            {
4196                paths.insert(tok.to_string());
4197            }
4198            // Unix-ish absolute paths into common locations
4199            else if (tok.starts_with("/tmp/") || tok.starts_with("/var/")
4200                || tok.starts_with("/etc/") || tok.starts_with("/usr/")
4201                || tok.starts_with("/home/") || tok.starts_with("/root/")
4202                || tok.starts_with("/dev/") || tok.starts_with("/proc/"))
4203                && path_valid(tok)
4204            {
4205                paths.insert(tok.to_string());
4206            }
4207        }
4208
4209        // Registry keys — match "HKEY_*", "HKLM\", "HKCU\" prefix and grab
4210        // the rest of the alphabetic / backslash run.
4211        for prefix in &[
4212            "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER", "HKEY_CLASSES_ROOT",
4213            "HKEY_USERS", "HKEY_CURRENT_CONFIG", "HKLM\\", "HKCU\\", "HKCR\\",
4214        ] {
4215            let mut start = 0;
4216            while let Some(rel) = s[start..].find(prefix) {
4217                let abs = start + rel;
4218                let end = s[abs..]
4219                    .find(|c: char| {
4220                        !(c.is_alphanumeric()
4221                            || matches!(c, '\\' | '_' | '-' | '.' | '{' | '}' | '/' | ' '))
4222                    })
4223                    .map(|n| abs + n)
4224                    .unwrap_or(s.len());
4225                let key = s[abs..end].trim_end();
4226                if key.len() >= prefix.len() + 2 && key.len() <= 256 {
4227                    registry.insert(key.to_string());
4228                }
4229                start = end.max(abs + 1);
4230            }
4231        }
4232
4233        // Mutex / named-object paths.
4234        for prefix in &["Global\\", "Local\\", "Session\\", "BaseNamedObjects\\"] {
4235            let mut start = 0;
4236            while let Some(rel) = s[start..].find(prefix) {
4237                let abs = start + rel;
4238                let end = s[abs..]
4239                    .find(|c: char| c.is_whitespace() || matches!(c, '"' | '\'' | '\0'))
4240                    .map(|n| abs + n)
4241                    .unwrap_or(s.len());
4242                let m = s[abs..end].trim();
4243                if m.len() >= prefix.len() + 2 && m.len() <= 200 {
4244                    mutexes.insert(m.to_string());
4245                }
4246                start = end.max(abs + 1);
4247            }
4248        }
4249
4250        // Credential / secret-ish keywords, embedded in config-like strings.
4251        // Reject .NET assembly identity strings whose `PublicKeyToken=` /
4252        // `Culture=neutral, ...` syntax otherwise produces a flood of
4253        // not-really-secrets in any managed binary.
4254        let lower = s.to_ascii_lowercase();
4255        let is_dotnet_identity = lower.contains("publickeytoken=")
4256            || lower.contains("culture=neutral")
4257            || lower.contains(", version=") && lower.contains(", culture=");
4258        // Skip very long blob-like strings (terminfo databases, embedded
4259        // resource catalogs) that catch unrelated env-var fragments.
4260        if !is_dotnet_identity && s.len() <= 256 {
4261            for needle in &[
4262                "password=", "passwd=",
4263                "api_key=", "apikey=", "api-key=",
4264                "access_token=", "auth_token=", "auth-token=", "bearer_token=",
4265                "bearer ", "client_secret=", "client-secret=",
4266                "private_key=", "ssh-rsa ", "-----BEGIN PRIVATE",
4267            ] {
4268                if lower.contains(needle) {
4269                    secrets.insert(s.trim().to_string());
4270                    break;
4271                }
4272            }
4273        }
4274    }
4275
4276    if json {
4277        let payload = serde_json::json!({
4278            "binary": binary_path,
4279            "urls":     urls.iter().collect::<Vec<_>>(),
4280            "ips":      ips.iter().collect::<Vec<_>>(),
4281            "domains":  domains.iter().collect::<Vec<_>>(),
4282            "paths":    paths.iter().collect::<Vec<_>>(),
4283            "registry": registry.iter().collect::<Vec<_>>(),
4284            "mutexes":  mutexes.iter().collect::<Vec<_>>(),
4285            "secrets":  secrets.iter().collect::<Vec<_>>(),
4286            "family": rsleigh_decompile::iot_family::classify_bytes(data)
4287                .map(|f| serde_json::json!({
4288                    "id": f.id,
4289                    "label": f.label,
4290                    "variant": f.variant,
4291                    "evidence": f.evidence,
4292                })),
4293            "capabilities": rsleigh_decompile::iot_capabilities::classify_bytes(data)
4294                .iter()
4295                .map(|c| serde_json::json!({
4296                    "id": c.id,
4297                    "label": c.label,
4298                    "evidence": c.evidence,
4299                }))
4300                .collect::<Vec<_>>(),
4301        });
4302        println!("{}", serde_json::to_string_pretty(&payload).unwrap());
4303        return;
4304    }
4305
4306    println!("=== IOCs from {} ===", binary_path);
4307    let print_section = |label: &str, set: &BTreeSet<String>| {
4308        if set.is_empty() {
4309            return;
4310        }
4311        println!("\n{} ({})", label, set.len());
4312        for v in set {
4313            println!("  {}", v);
4314        }
4315    };
4316    print_section("URLs", &urls);
4317    print_section("IPv4", &ips);
4318    print_section("Domains", &domains);
4319    print_section("Paths", &paths);
4320    print_section("Registry", &registry);
4321    print_section("Mutexes/Named Objects", &mutexes);
4322    print_section("Secret-like strings", &secrets);
4323
4324    if let Some(fam) = rsleigh_decompile::iot_family::classify_bytes(data) {
4325        let variant = fam
4326            .variant
4327            .as_ref()
4328            .map(|v| format!(" (variant: {})", v))
4329            .unwrap_or_default();
4330        println!("\nFamily: [{}] {}{}", fam.id, fam.label, variant);
4331        for ev in &fam.evidence {
4332            println!("  evidence: {}", ev);
4333        }
4334    }
4335
4336    let caps = rsleigh_decompile::iot_capabilities::classify_bytes(data);
4337    if !caps.is_empty() {
4338        println!("\nCapabilities ({})", caps.len());
4339        for c in &caps {
4340            println!("  [{}] {}", c.id, c.label);
4341            for ev in &c.evidence {
4342                println!("      - {}", ev);
4343            }
4344        }
4345    }
4346
4347    let total = urls.len() + ips.len() + domains.len() + paths.len()
4348        + registry.len() + mutexes.len() + secrets.len();
4349    if total == 0 && caps.is_empty() {
4350        println!("\n(no IOCs found)");
4351    } else {
4352        println!("\nTotal: {} indicators, {} capabilities", total, caps.len());
4353    }
4354}
4355
4356/// Parse the PE Authenticode signature embedded in the Security data
4357/// directory and surface the parts a triage analyst actually wants.
4358///
4359/// Authenticode signatures live in a WIN_CERTIFICATE structure pointed to
4360/// by the optional header's data directory entry #4. The certificate
4361/// itself is a PKCS#7 SignedData blob (BER-encoded). Rather than pull in
4362/// a full ASN.1 / CMS crate, we walk the bytes and pattern-match three
4363/// well-known OID prefixes:
4364///
4365///   * 2.5.4.3 commonName  (06 03 55 04 03 ...) — every Subject/Issuer CN
4366///   * 1.2.840.113549.1.9.5 signingTime (06 09 2A 86 48 86 F7 0D 01 09 05)
4367///   * 1.2.840.113549.1.7.2 signedData (validates the blob shape)
4368///
4369/// The leaf signer is the FIRST commonName encountered after the
4370/// signedData OID — the certificates list is ordered leaf-first in
4371/// practice for code-signing use, with the rest of the chain following.
4372fn run_sigcheck(binary_path: &str, data: &[u8], json: bool) {
4373    let result = sigcheck_parse(data);
4374
4375    if json {
4376        let signed = result.is_some();
4377        let payload = match &result {
4378            Some(r) => serde_json::json!({
4379                "binary": binary_path,
4380                "signed": signed,
4381                "signer_cn": r.signer_cn.clone(),
4382                "issuer_cn": r.issuer_cn.clone(),
4383                "signing_time": r.signing_time.clone(),
4384                "timestamp_signer_cn": r.timestamp_signer_cn.clone(),
4385                "all_cns": r.all_cns.clone(),
4386                "cert_blob_size": r.cert_blob_size,
4387                "win_cert_revision": format!("0x{:04x}", r.revision),
4388                "win_cert_type": format!("0x{:04x}", r.cert_type),
4389            }),
4390            None => serde_json::json!({
4391                "binary": binary_path,
4392                "signed": false,
4393                "reason": "no PE Security directory entry, or directory was empty",
4394            }),
4395        };
4396        println!("{}", serde_json::to_string_pretty(&payload).unwrap());
4397        return;
4398    }
4399
4400    println!("=== Authenticode signature for {} ===", binary_path);
4401    let Some(r) = result else {
4402        println!("\nUNSIGNED — no PE Security directory entry, or directory was empty.");
4403        return;
4404    };
4405    println!(
4406        "\n  Cert blob size:  {} bytes  (revision 0x{:04x}, type 0x{:04x})",
4407        r.cert_blob_size, r.revision, r.cert_type
4408    );
4409    if let Some(cn) = &r.signer_cn {
4410        println!("  Signer CN:       {}", cn);
4411    } else {
4412        println!("  Signer CN:       (not extracted)");
4413    }
4414    if let Some(cn) = &r.issuer_cn {
4415        println!("  Issuer CN:       {}", cn);
4416    }
4417    if let Some(t) = &r.signing_time {
4418        println!("  Signing time:    {}", t);
4419    }
4420    if let Some(cn) = &r.timestamp_signer_cn {
4421        println!("  Timestamp by:    {}", cn);
4422    }
4423    if !r.all_cns.is_empty() {
4424        println!("\n  Cert chain CNs ({}):", r.all_cns.len());
4425        for (i, cn) in r.all_cns.iter().enumerate() {
4426            println!("    [{}] {}", i, cn);
4427        }
4428    }
4429}
4430
4431#[derive(Debug)]
4432struct SigInfo {
4433    cert_blob_size: usize,
4434    revision: u16,
4435    cert_type: u16,
4436    signer_cn: Option<String>,
4437    issuer_cn: Option<String>,
4438    signing_time: Option<String>,
4439    timestamp_signer_cn: Option<String>,
4440    all_cns: Vec<String>,
4441}
4442
4443fn sigcheck_parse(data: &[u8]) -> Option<SigInfo> {
4444    // PE32 / PE32+ both put the Security data directory at the same
4445    // offset relative to the optional header start (data dir entry 4 ×
4446    // 8 bytes per entry past the magic-dependent fixed-size area).
4447    if data.len() < 0x40 || &data[..2] != b"MZ" {
4448        return None;
4449    }
4450    let e_lfanew =
4451        u32::from_le_bytes(data[0x3C..0x40].try_into().ok()?) as usize;
4452    if e_lfanew + 24 > data.len() {
4453        return None;
4454    }
4455    if &data[e_lfanew..e_lfanew + 4] != b"PE\0\0" {
4456        return None;
4457    }
4458    let opt_off = e_lfanew + 24;
4459    let magic = u16::from_le_bytes(data[opt_off..opt_off + 2].try_into().ok()?);
4460    // Security data directory is RVA 4 in the data directories array.
4461    // Offsets are 0x80 (PE32) / 0x90 (PE32+) past opt_off — Microsoft
4462    // PE/COFF spec 6.4.2.
4463    let sec_dir_off = match magic {
4464        0x10b => opt_off + 0x80, // PE32
4465        0x20b => opt_off + 0x90, // PE32+
4466        _ => return None,
4467    };
4468    if sec_dir_off + 8 > data.len() {
4469        return None;
4470    }
4471    // The Security directory's Address is a FILE OFFSET, not an RVA —
4472    // documented quirk of PE format. dwLength is byte length.
4473    let cert_off =
4474        u32::from_le_bytes(data[sec_dir_off..sec_dir_off + 4].try_into().ok()?) as usize;
4475    let cert_size =
4476        u32::from_le_bytes(data[sec_dir_off + 4..sec_dir_off + 8].try_into().ok()?) as usize;
4477    if cert_off == 0 || cert_size == 0 || cert_off + cert_size > data.len() {
4478        return None;
4479    }
4480    let cert_blob = &data[cert_off..cert_off + cert_size];
4481    if cert_blob.len() < 8 {
4482        return None;
4483    }
4484    // WIN_CERTIFICATE = { dwLength, wRevision, wCertificateType, bCertificate[] }
4485    let dw_length = u32::from_le_bytes(cert_blob[0..4].try_into().ok()?) as usize;
4486    let revision = u16::from_le_bytes(cert_blob[4..6].try_into().ok()?);
4487    let cert_type = u16::from_le_bytes(cert_blob[6..8].try_into().ok()?);
4488    if dw_length < 8 || dw_length > cert_blob.len() {
4489        return None;
4490    }
4491    let pkcs7 = &cert_blob[8..dw_length];
4492
4493    // OIDs we care about (as DER-encoded prefixes including the 0x06 tag):
4494    //   2.5.4.3   commonName        06 03 55 04 03
4495    //   1.2.840.113549.1.9.5  signingTime
4496    //                                06 09 2A 86 48 86 F7 0D 01 09 05
4497    //   1.2.840.113549.1.9.6  counterSignature
4498    //                                06 09 2A 86 48 86 F7 0D 01 09 06
4499    //   1.2.840.113549.1.7.2  signedData
4500    //                                06 09 2A 86 48 86 F7 0D 01 07 02
4501    const CN_OID: &[u8] = &[0x06, 0x03, 0x55, 0x04, 0x03];
4502    const SIGNING_TIME_OID: &[u8] = &[
4503        0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x05,
4504    ];
4505    const COUNTER_SIG_OID: &[u8] = &[
4506        0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x06,
4507    ];
4508
4509    // Parse a DER length starting at `i`; returns (length, header_size).
4510    fn der_len(buf: &[u8], i: usize) -> Option<(usize, usize)> {
4511        if i >= buf.len() {
4512            return None;
4513        }
4514        let b0 = buf[i];
4515        if b0 < 0x80 {
4516            return Some((b0 as usize, 1));
4517        }
4518        let n = (b0 & 0x7f) as usize;
4519        if n == 0 || n > 4 || i + 1 + n > buf.len() {
4520            return None;
4521        }
4522        let mut len = 0usize;
4523        for k in 0..n {
4524            len = (len << 8) | buf[i + 1 + k] as usize;
4525        }
4526        Some((len, 1 + n))
4527    }
4528
4529    // Extract a DER string after the OID's value tag. Tag bytes that
4530    // hold human-readable text in this context: 0x13 PrintableString,
4531    // 0x0C UTF8String, 0x16 IA5String, 0x14 T61String, 0x1E BMPString
4532    // (UTF-16BE). Returns the decoded string + bytes consumed.
4533    fn read_der_string(buf: &[u8], i: usize) -> Option<(String, usize)> {
4534        if i >= buf.len() {
4535            return None;
4536        }
4537        let tag = buf[i];
4538        let (len, hdr) = der_len(buf, i + 1)?;
4539        let start = i + 1 + hdr;
4540        if start + len > buf.len() {
4541            return None;
4542        }
4543        let body = &buf[start..start + len];
4544        let s = match tag {
4545            0x13 | 0x0c | 0x16 | 0x14 => {
4546                std::str::from_utf8(body).ok().map(|s| s.to_string())
4547            }
4548            0x1e => {
4549                // BMPString = UTF-16BE
4550                if body.len() % 2 != 0 {
4551                    return None;
4552                }
4553                let mut u16s: Vec<u16> = Vec::with_capacity(body.len() / 2);
4554                for chunk in body.chunks_exact(2) {
4555                    u16s.push(u16::from_be_bytes([chunk[0], chunk[1]]));
4556                }
4557                String::from_utf16(&u16s).ok()
4558            }
4559            _ => None,
4560        }?;
4561        Some((s, 1 + hdr + len))
4562    }
4563
4564    fn read_der_time(buf: &[u8], i: usize) -> Option<String> {
4565        if i >= buf.len() {
4566            return None;
4567        }
4568        let tag = buf[i];
4569        let (len, hdr) = der_len(buf, i + 1)?;
4570        let start = i + 1 + hdr;
4571        if start + len > buf.len() {
4572            return None;
4573        }
4574        let body = std::str::from_utf8(&buf[start..start + len]).ok()?;
4575        match tag {
4576            0x17 => {
4577                // UTCTime: YYMMDDHHMMSSZ — render as 20YY-MM-DD HH:MM:SS UTC
4578                if body.len() < 11 {
4579                    return None;
4580                }
4581                let yy: u32 = body[0..2].parse().ok()?;
4582                let yyyy = if yy < 50 { 2000 + yy } else { 1900 + yy };
4583                Some(format!(
4584                    "{:04}-{}-{} {}:{}:{}{}UTC",
4585                    yyyy,
4586                    &body[2..4],
4587                    &body[4..6],
4588                    &body[6..8],
4589                    &body[8..10],
4590                    &body[10..body.len().saturating_sub(1).min(12)],
4591                    if body.len() >= 13 { " " } else { "" }
4592                ))
4593            }
4594            0x18 => {
4595                // GeneralizedTime: YYYYMMDDHHMMSSZ
4596                if body.len() < 14 {
4597                    return None;
4598                }
4599                Some(format!(
4600                    "{}-{}-{} {}:{}:{} UTC",
4601                    &body[0..4],
4602                    &body[4..6],
4603                    &body[6..8],
4604                    &body[8..10],
4605                    &body[10..12],
4606                    &body[12..14],
4607                ))
4608            }
4609            _ => None,
4610        }
4611    }
4612
4613    let mut all_cns: Vec<String> = Vec::new();
4614    let mut signing_time: Option<String> = None;
4615    let mut after_counter_sig = false;
4616    let mut timestamp_signer_cn: Option<String> = None;
4617
4618    let mut i = 0usize;
4619    while i + 5 <= pkcs7.len() {
4620        if pkcs7[i..].starts_with(CN_OID) {
4621            // CN OID is followed by one of: tag 0x13/0x0c/0x16/0x14/0x1e
4622            // wrapped in either 0x30 SET or directly. The byte right
4623            // after the OID is the value tag for the CN body in the
4624            // RDN SET in practice.
4625            let after_oid = i + CN_OID.len();
4626            if let Some((cn, _)) = read_der_string(pkcs7, after_oid) {
4627                let trimmed = cn.trim().to_string();
4628                if !trimmed.is_empty() && trimmed.len() <= 200 {
4629                    if after_counter_sig && timestamp_signer_cn.is_none() {
4630                        timestamp_signer_cn = Some(trimmed.clone());
4631                    }
4632                    if !all_cns.contains(&trimmed) {
4633                        all_cns.push(trimmed);
4634                    }
4635                }
4636            }
4637            i += CN_OID.len();
4638            continue;
4639        }
4640        if signing_time.is_none() && pkcs7[i..].starts_with(SIGNING_TIME_OID) {
4641            // signingTime OID is followed by SET { UTCTime | GeneralizedTime }.
4642            // Skip the SET tag + length to land on the time tag.
4643            let mut j = i + SIGNING_TIME_OID.len();
4644            // Optional SET (0x31) wrapper.
4645            if j < pkcs7.len() && pkcs7[j] == 0x31 {
4646                if let Some((_, hdr)) = der_len(pkcs7, j + 1) {
4647                    j += 1 + hdr;
4648                }
4649            }
4650            if let Some(t) = read_der_time(pkcs7, j) {
4651                signing_time = Some(t);
4652            }
4653            i += SIGNING_TIME_OID.len();
4654            continue;
4655        }
4656        if pkcs7[i..].starts_with(COUNTER_SIG_OID) {
4657            after_counter_sig = true;
4658            i += COUNTER_SIG_OID.len();
4659            continue;
4660        }
4661        i += 1;
4662    }
4663
4664    // Heuristic: leaf signer = first CN that is NOT a known intermediate
4665    // CA name. Code-signing leaf certs almost always carry the publisher
4666    // name; intermediates carry "... CA" / "... Code Signing" / "... Root".
4667    let is_ca_like = |cn: &str| {
4668        let l = cn.to_ascii_lowercase();
4669        l.contains(" ca ")
4670            || l.ends_with(" ca")
4671            || l.contains("code signing")
4672            || l.contains("root")
4673            || l.contains("timestamping")
4674            || l.contains("time stamping")
4675    };
4676    let signer_cn = all_cns.iter().find(|c| !is_ca_like(c)).cloned();
4677    // Issuer = the CA-like CN immediately preceding the signer in the
4678    // chain. Authenticode certificate sequences typically appear in
4679    // root → intermediate → leaf order, so the entry just before the
4680    // signer is the direct issuer.
4681    let issuer_cn = if let Some(s) = &signer_cn {
4682        let pos = all_cns.iter().position(|c| c == s).unwrap_or(0);
4683        all_cns
4684            .iter()
4685            .take(pos)
4686            .rev()
4687            .find(|c| is_ca_like(c))
4688            .cloned()
4689    } else {
4690        None
4691    };
4692
4693    Some(SigInfo {
4694        cert_blob_size: dw_length,
4695        revision,
4696        cert_type,
4697        signer_cn,
4698        issuer_cn,
4699        signing_time,
4700        timestamp_signer_cn,
4701        all_cns,
4702    })
4703}
4704
4705/// Walk the PE resource directory tree and surface a triage-friendly
4706/// listing of every embedded resource. Optionally dump each resource's
4707/// raw bytes to disk for downstream analysis (icon extraction, MSI
4708/// peeking, embedded-payload recovery).
4709///
4710/// Three-level tree as documented in the PE/COFF spec: TYPE → NAME/ID →
4711/// LANGUAGE → IMAGE_RESOURCE_DATA_ENTRY. This function walks each level
4712/// using only the spec-defined offsets — it does not depend on goblin's
4713/// resource parser, which only exposes the raw section.
4714fn run_resources(binary_path: &str, data: &[u8], json: bool, dump_dir: Option<&str>) {
4715    let entries = match resources_parse(data) {
4716        Some(e) => e,
4717        None => {
4718            if json {
4719                println!(
4720                    "{}",
4721                    serde_json::to_string_pretty(&serde_json::json!({
4722                        "binary": binary_path,
4723                        "has_resources": false,
4724                    })).unwrap()
4725                );
4726            } else {
4727                println!(
4728                    "=== Resources for {} ===\n\n(no resource directory; PE has no .rsrc, or directory was empty)",
4729                    binary_path
4730                );
4731            }
4732            return;
4733        }
4734    };
4735
4736    if let Some(dir) = dump_dir {
4737        if let Err(e) = std::fs::create_dir_all(dir) {
4738            eprintln!("error: cannot create dump dir {}: {}", dir, e);
4739            return;
4740        }
4741        for r in &entries {
4742            let fname = format!(
4743                "{}/{}_{}_{}.bin",
4744                dir.trim_end_matches('/'),
4745                r.type_name(),
4746                r.id_label(),
4747                r.lang
4748            );
4749            if let Some(blob) = data.get(r.file_offset..r.file_offset + r.size) {
4750                if let Err(e) = std::fs::write(&fname, blob) {
4751                    eprintln!("warn: write {}: {}", fname, e);
4752                }
4753            }
4754        }
4755    }
4756
4757    if json {
4758        let arr: Vec<_> = entries
4759            .iter()
4760            .map(|r| {
4761                serde_json::json!({
4762                    "type": r.type_name(),
4763                    "type_id": r.type_id,
4764                    "id": r.id_label(),
4765                    "id_raw": r.id_raw,
4766                    "lang": r.lang,
4767                    "rva": format!("0x{:x}", r.rva),
4768                    "file_offset": format!("0x{:x}", r.file_offset),
4769                    "size": r.size,
4770                    "preview": r.preview(data),
4771                })
4772            })
4773            .collect();
4774        let payload = serde_json::json!({
4775            "binary": binary_path,
4776            "has_resources": true,
4777            "count": entries.len(),
4778            "entries": arr,
4779        });
4780        println!("{}", serde_json::to_string_pretty(&payload).unwrap());
4781        return;
4782    }
4783
4784    println!("=== Resources for {} ===", binary_path);
4785    println!("\n{} entries\n", entries.len());
4786    println!(
4787        "{:<14} {:<24} {:<6} {:>8}  preview",
4788        "type", "id", "lang", "size"
4789    );
4790    println!("{}", "-".repeat(78));
4791    for r in &entries {
4792        println!(
4793            "{:<14} {:<24} {:<6} {:>8}  {}",
4794            r.type_name(),
4795            r.id_label(),
4796            r.lang,
4797            r.size,
4798            r.preview(data),
4799        );
4800    }
4801    if let Some(dir) = dump_dir {
4802        println!("\nResources dumped to {}/", dir);
4803    }
4804}
4805
4806#[derive(Debug)]
4807struct ResEntry {
4808    type_id: u32,
4809    type_name_str: Option<String>,
4810    id_raw: u32,
4811    id_name: Option<String>,
4812    lang: u32,
4813    rva: u32,
4814    file_offset: usize,
4815    size: usize,
4816}
4817
4818impl ResEntry {
4819    fn type_name(&self) -> String {
4820        if let Some(n) = &self.type_name_str {
4821            return n.clone();
4822        }
4823        match self.type_id {
4824            1 => "CURSOR",
4825            2 => "BITMAP",
4826            3 => "ICON",
4827            4 => "MENU",
4828            5 => "DIALOG",
4829            6 => "STRING",
4830            7 => "FONTDIR",
4831            8 => "FONT",
4832            9 => "ACCELERATOR",
4833            10 => "RCDATA",
4834            11 => "MESSAGETABLE",
4835            12 => "GROUP_CURSOR",
4836            14 => "GROUP_ICON",
4837            16 => "VERSION",
4838            17 => "DLGINCLUDE",
4839            19 => "PLUGPLAY",
4840            20 => "VXD",
4841            21 => "ANICURSOR",
4842            22 => "ANIICON",
4843            23 => "HTML",
4844            24 => "MANIFEST",
4845            _ => return format!("TYPE_{}", self.type_id),
4846        }
4847        .to_string()
4848    }
4849    fn id_label(&self) -> String {
4850        match &self.id_name {
4851            Some(n) => n.clone(),
4852            None => format!("#{}", self.id_raw),
4853        }
4854    }
4855    fn preview(&self, data: &[u8]) -> String {
4856        let blob = match data.get(self.file_offset..self.file_offset + self.size.min(96)) {
4857            Some(b) => b,
4858            None => return String::new(),
4859        };
4860        // PE / MZ embedded payload — high signal, surface immediately.
4861        if blob.len() >= 2 && &blob[..2] == b"MZ" {
4862            return format!("[embedded PE/EXE, {} bytes]", self.size);
4863        }
4864        // MSI / OLE compound document.
4865        if blob.len() >= 8 && blob[..8] == [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1] {
4866            return format!("[OLE compound (likely MSI), {} bytes]", self.size);
4867        }
4868        // CAB.
4869        if blob.len() >= 4 && &blob[..4] == b"MSCF" {
4870            return format!("[CAB archive, {} bytes]", self.size);
4871        }
4872        // PNG / JPG / GIF.
4873        if blob.len() >= 8 && &blob[..8] == &[0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A] {
4874            return format!("[PNG image, {} bytes]", self.size);
4875        }
4876        if blob.len() >= 3 && &blob[..3] == b"\xff\xd8\xff" {
4877            return format!("[JPEG image, {} bytes]", self.size);
4878        }
4879        // Manifest / XML — UTF-8 text.
4880        if matches!(self.type_id, 24) {
4881            let text = String::from_utf8_lossy(blob);
4882            let cleaned: String = text
4883                .chars()
4884                .filter(|c| !c.is_control() || *c == ' ')
4885                .take(80)
4886                .collect();
4887            return cleaned;
4888        }
4889        // VS_VERSIONINFO — UTF-16LE keyed structure starting with len/value-len.
4890        if self.type_id == 16 && blob.len() >= 6 {
4891            // Decode UTF-16LE printable-ish run as a hint.
4892            let mut out = String::new();
4893            let mut i = 6;
4894            while i + 1 < blob.len() && out.len() < 60 {
4895                let lo = blob[i];
4896                let hi = blob[i + 1];
4897                if hi == 0 && (0x20..0x7f).contains(&lo) {
4898                    out.push(lo as char);
4899                } else if lo == 0 && hi == 0 {
4900                    if !out.is_empty() && !out.ends_with(' ') {
4901                        out.push(' ');
4902                    }
4903                }
4904                i += 2;
4905            }
4906            return out.trim().to_string();
4907        }
4908        // Generic preview: first 32 bytes hex + ascii.
4909        let n = blob.len().min(32);
4910        let hex: String = blob[..n].iter().map(|b| format!("{:02x}", b)).collect::<Vec<_>>().join("");
4911        let ascii: String = blob[..n]
4912            .iter()
4913            .map(|&b| if (0x20..0x7f).contains(&b) { b as char } else { '.' })
4914            .collect();
4915        format!("{}  |{}|", hex, ascii)
4916    }
4917}
4918
4919fn resources_parse(data: &[u8]) -> Option<Vec<ResEntry>> {
4920    if data.len() < 0x40 || &data[..2] != b"MZ" {
4921        return None;
4922    }
4923    let e_lfanew = u32::from_le_bytes(data[0x3C..0x40].try_into().ok()?) as usize;
4924    if e_lfanew + 24 > data.len() || &data[e_lfanew..e_lfanew + 4] != b"PE\0\0" {
4925        return None;
4926    }
4927    let opt_off = e_lfanew + 24;
4928    let magic = u16::from_le_bytes(data[opt_off..opt_off + 2].try_into().ok()?);
4929    let dir_off = match magic {
4930        0x10b => opt_off + 0x70 + 2 * 8, // PE32: data dirs start at +0x60, entry 2 = resources
4931        0x20b => opt_off + 0x70 + 2 * 8 + 16, // PE32+: shifted +16
4932        _ => return None,
4933    };
4934    // Recompute properly: data dirs start at opt_off + (0x60 PE32 / 0x70 PE32+), resources is index 2.
4935    let data_dir_base = match magic {
4936        0x10b => opt_off + 0x60,
4937        0x20b => opt_off + 0x70,
4938        _ => return None,
4939    };
4940    let res_dir_entry = data_dir_base + 2 * 8;
4941    if res_dir_entry + 8 > data.len() {
4942        return None;
4943    }
4944    let res_rva =
4945        u32::from_le_bytes(data[res_dir_entry..res_dir_entry + 4].try_into().ok()?) as u64;
4946    let res_size =
4947        u32::from_le_bytes(data[res_dir_entry + 4..res_dir_entry + 8].try_into().ok()?) as usize;
4948    if res_rva == 0 || res_size == 0 {
4949        return None;
4950    }
4951    let _ = dir_off;
4952
4953    // Walk section table to map RVA → file offset and locate the .rsrc base.
4954    let n_sec = u16::from_le_bytes(data[e_lfanew + 6..e_lfanew + 8].try_into().ok()?) as usize;
4955    let opt_size = u16::from_le_bytes(data[e_lfanew + 20..e_lfanew + 22].try_into().ok()?) as usize;
4956    let sec_off = opt_off + opt_size;
4957    if sec_off + n_sec * 40 > data.len() {
4958        return None;
4959    }
4960    let mut sections: Vec<(u64, u64, u64)> = Vec::new(); // (va, vsz, fo)
4961    for i in 0..n_sec {
4962        let off = sec_off + i * 40;
4963        let vsz = u32::from_le_bytes(data[off + 8..off + 12].try_into().ok()?) as u64;
4964        let va = u32::from_le_bytes(data[off + 12..off + 16].try_into().ok()?) as u64;
4965        let fo = u32::from_le_bytes(data[off + 20..off + 24].try_into().ok()?) as u64;
4966        sections.push((va, vsz, fo));
4967    }
4968    let rva_to_fo = |rva: u64| -> Option<usize> {
4969        for (va, vsz, fo) in &sections {
4970            if rva >= *va && rva < va + vsz {
4971                return Some((fo + (rva - va)) as usize);
4972            }
4973        }
4974        None
4975    };
4976    let res_base_fo = rva_to_fo(res_rva)?;
4977    let res_blob_end = res_base_fo + res_size;
4978    if res_blob_end > data.len() {
4979        return None;
4980    }
4981    let rsrc = &data[res_base_fo..res_blob_end];
4982
4983    // Read a UTF-16LE pascal-style name (length-prefixed) from the
4984    // resource section. Used for named resources.
4985    let read_name = |offset: usize| -> Option<String> {
4986        if offset + 2 > rsrc.len() {
4987            return None;
4988        }
4989        let n = u16::from_le_bytes(rsrc[offset..offset + 2].try_into().ok()?) as usize;
4990        let start = offset + 2;
4991        if start + n * 2 > rsrc.len() {
4992            return None;
4993        }
4994        let mut u16s: Vec<u16> = Vec::with_capacity(n);
4995        for chunk in rsrc[start..start + n * 2].chunks_exact(2) {
4996            u16s.push(u16::from_le_bytes([chunk[0], chunk[1]]));
4997        }
4998        String::from_utf16(&u16s).ok()
4999    };
5000
5001    // Walk a directory at `dir_off` (relative to rsrc base). Returns
5002    // a list of (id_or_name, child_offset, is_dir). Limit recursion to
5003    // depth 3 (TYPE → NAME → LANG → DATA).
5004    fn walk_dir(rsrc: &[u8], dir_off: usize) -> Option<Vec<(u32, Option<String>, u32, bool)>> {
5005        if dir_off + 16 > rsrc.len() {
5006            return None;
5007        }
5008        let n_named = u16::from_le_bytes(rsrc[dir_off + 12..dir_off + 14].try_into().ok()?);
5009        let n_id = u16::from_le_bytes(rsrc[dir_off + 14..dir_off + 16].try_into().ok()?);
5010        let total = n_named as usize + n_id as usize;
5011        let entries_off = dir_off + 16;
5012        if entries_off + total * 8 > rsrc.len() {
5013            return None;
5014        }
5015        let mut out = Vec::with_capacity(total);
5016        for i in 0..total {
5017            let e = entries_off + i * 8;
5018            let name_or_id = u32::from_le_bytes(rsrc[e..e + 4].try_into().ok()?);
5019            let off = u32::from_le_bytes(rsrc[e + 4..e + 8].try_into().ok()?);
5020            let is_dir = (off & 0x8000_0000) != 0;
5021            let child_off = (off & 0x7fff_ffff) as u32;
5022            out.push((name_or_id, None, child_off, is_dir));
5023            // Caller will translate Name pointer (high bit set on
5024            // name_or_id) into a string separately to keep this fn
5025            // borrow-free.
5026            let _ = name_or_id;
5027        }
5028        Some(out)
5029    }
5030
5031    let mut entries: Vec<ResEntry> = Vec::new();
5032
5033    // Level 1: TYPE
5034    let level1 = walk_dir(rsrc, 0)?;
5035    for &(type_raw, _, type_child_off, type_is_dir) in &level1 {
5036        if !type_is_dir {
5037            continue;
5038        }
5039        let type_name_str = if (type_raw & 0x8000_0000) != 0 {
5040            read_name((type_raw & 0x7fff_ffff) as usize)
5041        } else {
5042            None
5043        };
5044        let type_id = type_raw & 0x7fff_ffff;
5045        let level2 = match walk_dir(rsrc, type_child_off as usize) {
5046            Some(v) => v,
5047            None => continue,
5048        };
5049        // Level 2: NAME / ID
5050        for &(id_raw, _, id_child_off, id_is_dir) in &level2 {
5051            if !id_is_dir {
5052                continue;
5053            }
5054            let id_name = if (id_raw & 0x8000_0000) != 0 {
5055                read_name((id_raw & 0x7fff_ffff) as usize)
5056            } else {
5057                None
5058            };
5059            let id_val = id_raw & 0x7fff_ffff;
5060            let level3 = match walk_dir(rsrc, id_child_off as usize) {
5061                Some(v) => v,
5062                None => continue,
5063            };
5064            // Level 3: LANGUAGE → DATA_ENTRY
5065            for &(lang_raw, _, lang_child_off, lang_is_dir) in &level3 {
5066                if lang_is_dir {
5067                    continue;
5068                }
5069                let lang = lang_raw & 0x7fff_ffff;
5070                // IMAGE_RESOURCE_DATA_ENTRY = { OffsetToData(rva), Size, CodePage, Reserved }
5071                let de = lang_child_off as usize;
5072                if de + 16 > rsrc.len() {
5073                    continue;
5074                }
5075                let data_rva = match u32::from_le_bytes(rsrc[de..de + 4].try_into().ok()?) {
5076                    v => v as u64,
5077                };
5078                let data_sz = u32::from_le_bytes(rsrc[de + 4..de + 8].try_into().ok()?) as usize;
5079                let Some(fo) = rva_to_fo(data_rva) else { continue };
5080                if fo + data_sz > data.len() {
5081                    continue;
5082                }
5083                entries.push(ResEntry {
5084                    type_id,
5085                    type_name_str: type_name_str.clone(),
5086                    id_raw: id_val,
5087                    id_name: id_name.clone(),
5088                    lang,
5089                    rva: data_rva as u32,
5090                    file_offset: fo,
5091                    size: data_sz,
5092                });
5093            }
5094        }
5095    }
5096
5097    if entries.is_empty() {
5098        return None;
5099    }
5100    Some(entries)
5101}
5102
5103/// Export full call graph as JSON.
5104fn run_callgraph(binary_path: &str, data: &[u8]) {
5105    let obj = match goblin::Object::parse(data) {
5106        Ok(o) => o,
5107        Err(e) => {
5108            eprintln!("Error: {}", e);
5109            return;
5110        }
5111    };
5112    let (arch, segs, mut symbols) = match parse_binary(&obj, data) {
5113        Some(r) => r,
5114        None => {
5115            eprintln!("Unsupported");
5116            return;
5117        }
5118    };
5119    if symbols.is_empty() {
5120        if let goblin::Object::PE(pe) = &obj {
5121            let base = pe.image_base as u64;
5122            let entry = base
5123                + pe.header
5124                    .optional_header
5125                    .unwrap()
5126                    .standard_fields
5127                    .address_of_entry_point as u64;
5128            symbols = discover_pe_functions(entry, &segs, data, arch);
5129        }
5130    }
5131    let is_elf_stripped = if let goblin::Object::Elf(elf) = &obj {
5132        elf.syms.len() == 0
5133    } else {
5134        false
5135    };
5136    if is_elf_stripped {
5137        if let goblin::Object::Elf(elf) = &obj {
5138            let discovered = discover_elf_functions(elf, &segs, data, arch);
5139            let existing: std::collections::BTreeSet<u64> =
5140                symbols.iter().map(|(a, _)| *a).collect();
5141            for (addr, name) in discovered {
5142                if !existing.contains(&addr) {
5143                    symbols.push((addr, name));
5144                }
5145            }
5146        }
5147    }
5148
5149    let path = std::path::Path::new(binary_path);
5150    let mut dec = rsleigh_api::Decoder::new(arch);
5151    let mut graph: std::collections::BTreeMap<String, serde_json::Value> =
5152        std::collections::BTreeMap::new();
5153
5154    eprintln!("Building call graph for {} functions...", symbols.len());
5155
5156    for (func_addr, func_name) in &symbols {
5157        let insts = decode_func(*func_addr, &symbols, &segs, data, &mut dec);
5158        if insts.is_empty() {
5159            continue;
5160        }
5161
5162        let output = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
5163            rsleigh_decompile::decompile_with_binary(arch, &insts, Some(data), Some(path))
5164        })) {
5165            Ok(o) => o,
5166            Err(_) => continue,
5167        };
5168
5169        // Extract callees from pseudocode
5170        let mut calls = Vec::new();
5171        for line in output.lines() {
5172            let t = line.trim();
5173            if t.contains('(') && !t.starts_with("//") {
5174                let check = if let Some(eq) = t.find(" = ") {
5175                    &t[eq + 3..]
5176                } else {
5177                    t
5178                };
5179                if let Some(p) = check.find('(') {
5180                    let callee = check[..p].trim().trim_start_matches("return ");
5181                    if !callee.is_empty()
5182                        && !callee.contains(' ')
5183                        && !callee.starts_with('*')
5184                        && !callee.starts_with('(')
5185                        && !callee.starts_with("if")
5186                        && !callee.starts_with("while")
5187                        && !callee.starts_with("switch")
5188                        && !callee.starts_with("for")
5189                        && callee.len() < 50
5190                        && !calls.contains(&callee.to_string())
5191                    {
5192                        calls.push(callee.to_string());
5193                    }
5194                }
5195            }
5196        }
5197
5198        // Classify function behavior
5199        let mut tags = Vec::new();
5200        if calls.iter().any(|c| {
5201            [
5202                "recv", "send", "socket", "connect", "accept", "bind", "listen",
5203            ]
5204            .contains(&c.as_str())
5205        }) {
5206            tags.push("network");
5207        }
5208        if calls.iter().any(|c| {
5209            [
5210                "CreateFile",
5211                "fopen",
5212                "ReadFile",
5213                "WriteFile",
5214                "fread",
5215                "fwrite",
5216                "open",
5217                "read",
5218                "write",
5219            ]
5220            .contains(&c.as_str())
5221        }) {
5222            tags.push("file_io");
5223        }
5224        if calls
5225            .iter()
5226            .any(|c| c.contains("Reg") || c.contains("Registry"))
5227        {
5228            tags.push("registry");
5229        }
5230        if calls.iter().any(|c| {
5231            [
5232                "system",
5233                "exec",
5234                "execve",
5235                "popen",
5236                "ShellExecute",
5237                "WinExec",
5238                "CreateProcess",
5239            ]
5240            .contains(&c.as_str())
5241        }) {
5242            tags.push("exec");
5243        }
5244        if calls.iter().any(|c| {
5245            [
5246                "malloc",
5247                "free",
5248                "realloc",
5249                "VirtualAlloc",
5250                "mmap",
5251                "HeapAlloc",
5252            ]
5253            .contains(&c.as_str())
5254        }) {
5255            tags.push("memory");
5256        }
5257        if output.contains("AES")
5258            || output.contains("SHA")
5259            || output.contains("CRC")
5260            || output.contains("^ 0x")
5261        {
5262            tags.push("crypto");
5263        }
5264        if calls
5265            .iter()
5266            .any(|c| ["printf", "puts", "fprintf", "sprintf", "snprintf"].contains(&c.as_str()))
5267        {
5268            tags.push("output");
5269        }
5270        if calls
5271            .iter()
5272            .any(|c| ["scanf", "gets", "fgets", "getenv", "getchar"].contains(&c.as_str()))
5273        {
5274            tags.push("input");
5275        }
5276
5277        let return_type = output
5278            .lines()
5279            .next()
5280            .and_then(|l| l.split_whitespace().next())
5281            .unwrap_or("void");
5282
5283        graph.insert(
5284            func_name.clone(),
5285            serde_json::json!({
5286                "address": format!("0x{:x}", func_addr),
5287                "calls": calls,
5288                "return_type": return_type,
5289                "tags": tags,
5290            }),
5291        );
5292    }
5293
5294    // Build called_by reverse map
5295    let mut called_by: std::collections::BTreeMap<String, Vec<String>> =
5296        std::collections::BTreeMap::new();
5297    for (func_name, info) in &graph {
5298        if let Some(calls) = info.get("calls").and_then(|c| c.as_array()) {
5299            for callee in calls {
5300                if let Some(callee_name) = callee.as_str() {
5301                    called_by
5302                        .entry(callee_name.to_string())
5303                        .or_default()
5304                        .push(func_name.clone());
5305                }
5306            }
5307        }
5308    }
5309
5310    // Merge called_by into graph
5311    let mut final_graph = serde_json::Map::new();
5312    for (name, info) in &graph {
5313        let mut entry = info.clone();
5314        if let Some(callers) = called_by.get(name) {
5315            entry
5316                .as_object_mut()
5317                .unwrap()
5318                .insert("called_by".to_string(), serde_json::json!(callers));
5319        }
5320        final_graph.insert(name.clone(), entry);
5321    }
5322
5323    println!(
5324        "{}",
5325        serde_json::to_string_pretty(&serde_json::json!({
5326            "binary": binary_path,
5327            "arch": format!("{:?}", arch),
5328            "function_count": graph.len(),
5329            "callgraph": final_graph,
5330        }))
5331        .unwrap()
5332    );
5333}
5334
5335/// Demangle a C++/Swift symbol name for display, falling back to the raw
5336/// name when demangling fails. Strips the parameter list tail to keep the
5337/// one-line listing compact: `QObject::connect(QObject const*, ...)` →
5338/// `QObject::connect`. `.cold.NN` / `.part.NN` / `.constprop.N` suffixes
5339/// (GCC IPA clones) are preserved so the analyst can distinguish variants.
5340fn demangle_symbol(name: &str) -> String {
5341    // Keep GCC/clang IPA suffixes intact on the original-name fallback.
5342    let (core, suffix) = match name.find('.') {
5343        Some(p)
5344            if name[p..].starts_with(".cold")
5345                || name[p..].starts_with(".part")
5346                || name[p..].starts_with(".constprop")
5347                || name[p..].starts_with(".isra")
5348                || name[p..].starts_with(".lto_priv") =>
5349        {
5350            (&name[..p], &name[p..])
5351        }
5352        _ => (name, ""),
5353    };
5354    if !(core.starts_with("_Z") || core.starts_with("__Z")) {
5355        return name.to_string();
5356    }
5357    let Ok(sym) = cpp_demangle::Symbol::new(core.as_bytes()) else {
5358        return name.to_string();
5359    };
5360    let Ok(demangled) = sym.demangle(&cpp_demangle::DemangleOptions::default()) else {
5361        return name.to_string();
5362    };
5363    // Trim parameter list — matching rsleigh-decompile::imports::demangle_name.
5364    let pretty = if let Some(paren) = demangled.find('(') {
5365        let before = &demangled[..paren];
5366        if !before.is_empty() && !before.ends_with('>') {
5367            before.to_string()
5368        } else {
5369            demangled
5370        }
5371    } else {
5372        demangled
5373    };
5374    if suffix.is_empty() {
5375        pretty
5376    } else {
5377        format!("{}{}", pretty, suffix)
5378    }
5379}
5380
5381/// Load FID databases from `--fid <path>` args and apply fingerprint
5382/// matches to anonymous `func_*` / `sub_*` / `FUN_*` symbols.
5383fn apply_fid_to_symbols(
5384    data: &[u8],
5385    arch: rsleigh_api::Architecture,
5386    segs: &[(u64, u64, u64)],
5387    symbols: &mut [(u64, String)],
5388    args: &[String],
5389) {
5390    let mut dbs: Vec<rsleigh_fid::FidDb> = Vec::new();
5391    // Auto-load bundled glibc/musl/libstdc++ DBs unless --no-fid-auto.
5392    if !args.iter().any(|a| a == "--no-fid-auto") {
5393        for (lib, db) in rsleigh_fid::bundled_dbs(arch) {
5394            eprintln!("[fid] bundled {}: {} entries", lib, db.entries.len());
5395            dbs.push(db);
5396        }
5397    }
5398    let mut i = 0;
5399    while i < args.len() {
5400        if args[i] == "--fid" {
5401            if let Some(p) = args.get(i + 1) {
5402                match std::fs::File::open(p)
5403                    .and_then(|f| rsleigh_fid::FidDb::read(f).map_err(Into::into))
5404                {
5405                    Ok(db) => {
5406                        eprintln!("[fid] loaded {} entries from {}", db.entries.len(), p);
5407                        dbs.push(db);
5408                    }
5409                    Err(e) => eprintln!("[fid] skip {}: {}", p, e),
5410                }
5411                i += 2;
5412                continue;
5413            }
5414        }
5415        i += 1;
5416    }
5417    if dbs.is_empty() {
5418        return;
5419    }
5420    let quiet_banner = args.iter().any(|a| a == "--fid-quiet");
5421    let _ = quiet_banner;
5422    let va_slice = |va: u64| -> Option<&[u8]> {
5423        for (vstart, vend, foff) in segs {
5424            if va >= *vstart && va < *vend {
5425                let rel = (va - vstart) as usize;
5426                let fstart = *foff as usize + rel;
5427                let vsize = (vend - va) as usize;
5428                let end = fstart.saturating_add(vsize).min(data.len());
5429                if fstart < data.len() {
5430                    return Some(&data[fstart..end]);
5431                }
5432            }
5433        }
5434        None
5435    };
5436    let mut hits = 0usize;
5437    for (addr, name) in symbols.iter_mut() {
5438        let anon =
5439            name.starts_with("func_") || name.starts_with("sub_") || name.starts_with("FUN_");
5440        if !anon {
5441            continue;
5442        }
5443        let Some(body) = va_slice(*addr) else {
5444            continue;
5445        };
5446        // Cap body at 4KB — most real funcs are well under this.
5447        let body = &body[..body.len().min(4096)];
5448        for db in &dbs {
5449            if let Some(matched) = rsleigh_fid::identify(arch, body, *addr, db) {
5450                *name = matched.to_string();
5451                hits += 1;
5452                break;
5453            }
5454        }
5455    }
5456    if hits > 0 {
5457        eprintln!("[fid] matched {} anonymous symbols", hits);
5458    }
5459}
5460
5461/// Compute MD5 hash of data, return lowercase hex string.
5462fn compute_md5(data: &[u8]) -> String {
5463    use md5::{Digest, Md5};
5464    let mut h = Md5::new();
5465    h.update(data);
5466    h.finalize().iter().map(|b| format!("{:02x}", b)).collect()
5467}
5468
5469/// Compute SHA-256 hash of data, return lowercase hex string.
5470fn compute_sha256(data: &[u8]) -> String {
5471    use sha2::{Digest, Sha256};
5472    let mut h = Sha256::new();
5473    h.update(data);
5474    h.finalize().iter().map(|b| format!("{:02x}", b)).collect()
5475}
5476
5477/// Mandiant imphash: MD5 of the comma-joined, lowercased `dll.function`
5478/// entries built from the PE import table. DLL extensions are stripped to
5479/// a known short set; ordinal-only imports are encoded as `ord<N>`.
5480///
5481/// Returns None for non-PE binaries or PE files with no imports.
5482///
5483/// Spec: github.com/mandiant/pefile (imphash()).
5484fn compute_imphash(data: &[u8]) -> Option<String> {
5485    use md5::{Digest, Md5};
5486    let obj = goblin::Object::parse(data).ok()?;
5487    let pe = match obj {
5488        goblin::Object::PE(pe) => pe,
5489        _ => return None,
5490    };
5491    if pe.imports.is_empty() {
5492        return None;
5493    }
5494
5495    // Mandiant's normalization:
5496    //  - lowercase DLL name
5497    //  - strip extension if it's one of:
5498    //      .dll, .ocx, .sys, .drv, .cpl, .exe
5499    //  - function name: lowercase as-is; ordinal → "ord<num>"
5500    let strip_exts = [".dll", ".ocx", ".sys", ".drv", ".cpl", ".exe"];
5501    let mut entries: Vec<String> = Vec::new();
5502    for imp in &pe.imports {
5503        let mut dll = imp.dll.to_ascii_lowercase();
5504        for ext in &strip_exts {
5505            if dll.ends_with(ext) {
5506                dll.truncate(dll.len() - ext.len());
5507                break;
5508            }
5509        }
5510        // goblin's pe.imports gives named symbols directly; ordinals come
5511        // through as names like "Ordinal_123" or empty. Use the Import's
5512        // name field: if it looks like an ordinal placeholder, rewrite.
5513        let name = imp.name.to_ascii_lowercase();
5514        let fn_name = if name.starts_with("ordinal_") {
5515            // "ordinal_123" → "ord123"
5516            format!("ord{}", &name[8..])
5517        } else if name.is_empty() {
5518            // Truly unnamed ordinal — fall back to ordinal field
5519            format!("ord{}", imp.ordinal)
5520        } else {
5521            name
5522        };
5523        entries.push(format!("{}.{}", dll, fn_name));
5524    }
5525
5526    // Mandiant preserves import-table order (NOT sorted). Deduplication: no.
5527    let joined = entries.join(",");
5528    let mut h = Md5::new();
5529    h.update(joined.as_bytes());
5530    Some(h.finalize().iter().map(|b| format!("{:02x}", b)).collect())
5531}
5532
5533fn generate_yara_rule(binary_path: &str, data: &[u8]) {
5534    use std::collections::{BTreeMap, BTreeSet};
5535
5536    let filename = std::path::Path::new(binary_path)
5537        .file_stem()
5538        .unwrap_or_default()
5539        .to_string_lossy()
5540        .replace(|c: char| !c.is_ascii_alphanumeric() && c != '_', "_");
5541    let rule_name = format!("rsleigh_{}", filename);
5542
5543    let mut strings: BTreeSet<String> = BTreeSet::new();
5544    let mut wide_strings: BTreeSet<String> = BTreeSet::new();
5545    let mut hex_patterns: Vec<(String, String)> = Vec::new(); // (name, hex)
5546    let mut imports: BTreeSet<String> = BTreeSet::new();
5547    let mut meta: BTreeMap<String, String> = BTreeMap::new();
5548
5549    // Meta information
5550    meta.insert("tool".into(), "rsleigh".into());
5551    meta.insert("date".into(), chrono_date());
5552    let file_size = data.len();
5553    meta.insert("filesize".into(), format!("{}", file_size));
5554
5555    // Detect format
5556    let is_pe = data.len() > 2 && &data[0..2] == b"MZ";
5557    let is_elf = data.len() > 4 && &data[0..4] == b"\x7fELF";
5558    if is_pe {
5559        meta.insert("filetype".into(), "PE".into());
5560    }
5561    if is_elf {
5562        meta.insert("filetype".into(), "ELF".into());
5563    }
5564
5565    // 1. Extract ASCII strings (6+ chars, printable, not too common)
5566    {
5567        let mut pos = 0;
5568        while pos < data.len() {
5569            if data[pos] >= 0x20 && data[pos] < 0x7f {
5570                let start = pos;
5571                while pos < data.len() && data[pos] >= 0x20 && data[pos] < 0x7f {
5572                    pos += 1;
5573                }
5574                let len = pos - start;
5575                if len >= 6 && len <= 200 {
5576                    if let Ok(s) = std::str::from_utf8(&data[start..pos]) {
5577                        let s = s.trim();
5578                        // Filter out common/generic strings
5579                        let is_charset = s.contains("ABCDEFGHIJ") && s.contains("abcdefghij");
5580                        let is_sequential = s
5581                            .bytes()
5582                            .zip(s.bytes().skip(1))
5583                            .filter(|(a, b)| *b == a + 1)
5584                            .count()
5585                            > s.len() / 2;
5586                        if s.len() >= 6
5587                            && !is_charset && !is_sequential
5588                            && !s.chars().all(|c| c == ' ' || c == '.' || c == '-' || c == '0')
5589                            && !s.starts_with("GCC:")
5590                            && !s.starts_with("GNU ")
5591                            && !s.starts_with("!This program")
5592                            && !s.contains("Copyright")
5593                            && !s.contains("GLIBC")
5594                            && !s.starts_with(".debug")
5595                            && !s.starts_with(".note")
5596                            && !s.starts_with(".symtab")
5597                            && !s.starts_with(".strtab")
5598                            && !s.starts_with('`') // MSVC demangled names (generic)
5599                            && !s.contains("Descriptor")
5600                            && !s.contains("constructor")
5601                            && !s.contains("destructor")
5602                            && !s.starts_with("AppPolicy") // CRT internal
5603                            && !s.contains("template-parameter")
5604                            && !s.contains("Hierarchy")
5605                        {
5606                            strings.insert(s.to_string());
5607                        }
5608                    }
5609                }
5610            } else {
5611                pos += 1;
5612            }
5613        }
5614    }
5615
5616    // 2. Extract wide strings (UTF-16LE, for PE binaries)
5617    if is_pe {
5618        let mut pos = 0;
5619        while pos + 1 < data.len() {
5620            if data[pos] >= 0x20 && data[pos] < 0x7f && data[pos + 1] == 0 {
5621                let start = pos;
5622                while pos + 1 < data.len()
5623                    && data[pos] >= 0x20
5624                    && data[pos] < 0x7f
5625                    && data[pos + 1] == 0
5626                {
5627                    pos += 2;
5628                }
5629                let char_count = (pos - start) / 2;
5630                if char_count >= 6 && char_count <= 100 {
5631                    let chars: String = data[start..pos]
5632                        .chunks(2)
5633                        .filter_map(|c| {
5634                            if c.len() == 2 {
5635                                Some(c[0] as char)
5636                            } else {
5637                                None
5638                            }
5639                        })
5640                        .collect();
5641                    if !strings.contains(&chars) {
5642                        // don't duplicate ASCII
5643                        wide_strings.insert(chars);
5644                    }
5645                }
5646            } else {
5647                pos += 1;
5648            }
5649        }
5650    }
5651
5652    // 3. Extract imports (PE + ELF)
5653    if let Ok(obj) = goblin::Object::parse(data) {
5654        match &obj {
5655            goblin::Object::PE(pe) => {
5656                for imp in &pe.imports {
5657                    imports.insert(imp.name.to_string());
5658                }
5659                if let Some(name) = pe.name {
5660                    meta.insert("original_name".into(), name.to_string());
5661                }
5662            }
5663            goblin::Object::Elf(elf) => {
5664                for sym in elf.dynsyms.iter() {
5665                    if let Some(name) = elf.dynstrtab.get_at(sym.st_name) {
5666                        if !name.is_empty() && name.len() > 3 {
5667                            imports.insert(name.to_string());
5668                        }
5669                    }
5670                }
5671            }
5672            _ => {}
5673        }
5674    }
5675
5676    // 4. Detect crypto constants
5677    let crypto_sigs: &[(&str, &[u8])] = &[
5678        (
5679            "aes_sbox",
5680            &[0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5],
5681        ),
5682        (
5683            "sha256_k",
5684            &[0x98, 0x2f, 0x8a, 0x42, 0x91, 0x44, 0x37, 0x71],
5685        ),
5686        (
5687            "sha256_k_be",
5688            &[0x42, 0x8a, 0x2f, 0x98, 0x71, 0x37, 0x44, 0x91],
5689        ),
5690        ("md5_t", &[0x78, 0xa4, 0x6a, 0xd7, 0x56, 0xb7, 0xc7, 0xe8]),
5691        (
5692            "crc32_table",
5693            &[0x00, 0x00, 0x00, 0x00, 0x96, 0x30, 0x07, 0x77],
5694        ),
5695        ("chacha20", b"expand 32-byte k"),
5696        (
5697            "blowfish_p",
5698            &[0x24, 0x3f, 0x6a, 0x88, 0x85, 0xa3, 0x08, 0xd3],
5699        ),
5700    ];
5701    for (name, pattern) in crypto_sigs {
5702        if data.windows(pattern.len()).any(|w| w == *pattern) {
5703            let hex = pattern
5704                .iter()
5705                .map(|b| format!("{:02X}", b))
5706                .collect::<Vec<_>>()
5707                .join(" ");
5708            hex_patterns.push((format!("crypto_{}", name), hex));
5709        }
5710    }
5711
5712    // 5. Extract unique byte patterns from entry point / first function
5713    if let Ok(obj) = goblin::Object::parse(data) {
5714        let entry_bytes = match &obj {
5715            goblin::Object::PE(pe) => {
5716                let entry_rva = pe
5717                    .header
5718                    .optional_header
5719                    .map(|h| h.standard_fields.address_of_entry_point as usize)
5720                    .unwrap_or(0);
5721                pe.sections.iter().find_map(|s| {
5722                    let sr = s.virtual_address as usize;
5723                    if entry_rva >= sr && entry_rva < sr + s.virtual_size as usize {
5724                        let fo = s.pointer_to_raw_data as usize + (entry_rva - sr);
5725                        if fo + 32 <= data.len() {
5726                            Some(&data[fo..fo + 32])
5727                        } else {
5728                            None
5729                        }
5730                    } else {
5731                        None
5732                    }
5733                })
5734            }
5735            goblin::Object::Elf(elf) => {
5736                let entry = elf.header.e_entry as usize;
5737                elf.section_headers.iter().find_map(|sh| {
5738                    if entry >= sh.sh_addr as usize && entry < (sh.sh_addr + sh.sh_size) as usize {
5739                        let fo = sh.sh_offset as usize + (entry - sh.sh_addr as usize);
5740                        if fo + 32 <= data.len() {
5741                            Some(&data[fo..fo + 32])
5742                        } else {
5743                            None
5744                        }
5745                    } else {
5746                        None
5747                    }
5748                })
5749            }
5750            _ => None,
5751        };
5752        if let Some(bytes) = entry_bytes {
5753            let hex = bytes
5754                .iter()
5755                .map(|b| format!("{:02X}", b))
5756                .collect::<Vec<_>>()
5757                .join(" ");
5758            hex_patterns.push(("entry_point".into(), hex));
5759        }
5760    }
5761
5762    // 6. Select best strings for the rule (most unique, not too long)
5763    // Score strings: prefer longer, with special chars, not common words
5764    let mut scored_strings: Vec<(i32, &String)> = strings
5765        .iter()
5766        .map(|s| {
5767            let mut score = s.len() as i32;
5768            if s.contains('/') || s.contains('\\') {
5769                score += 5;
5770            } // paths
5771            if s.contains("http") || s.contains("://") {
5772                score += 10;
5773            } // URLs
5774            if s.contains(".dll") || s.contains(".exe") || s.contains(".sys") {
5775                score += 10;
5776            }
5777            if s.contains("password") || s.contains("secret") || s.contains("key") {
5778                score += 15;
5779            }
5780            if s.contains("cmd") || s.contains("shell") || s.contains("exec") {
5781                score += 10;
5782            }
5783            if s.starts_with("Error") || s.starts_with("Warning") {
5784                score -= 5;
5785            }
5786            // Penalize very common strings
5787            if s.len() > 50 {
5788                score -= 10;
5789            }
5790            (score, s)
5791        })
5792        .collect();
5793    scored_strings.sort_by(|a, b| b.0.cmp(&a.0));
5794
5795    // Select top 20 strings
5796    let selected_strings: Vec<&String> = scored_strings.iter().take(20).map(|(_, s)| *s).collect();
5797
5798    // Select top 5 wide strings
5799    let selected_wide: Vec<&String> = wide_strings.iter().take(5).collect();
5800
5801    // Select suspicious imports
5802    let suspicious_imports: Vec<&String> = imports
5803        .iter()
5804        .filter(|i| {
5805            let il = i.to_lowercase();
5806            il.contains("virtualalloc")
5807                || il.contains("writeprocessmemory")
5808                || il.contains("createremotethread")
5809                || il.contains("ntcreatethreadex")
5810                || il.contains("loadlibrary")
5811                || il.contains("getprocaddress")
5812                || il.contains("cryptencrypt")
5813                || il.contains("internetopen")
5814                || il.contains("urldownload")
5815                || il.contains("shellexecute")
5816                || il.contains("regsetvalue")
5817                || il.contains("createservice")
5818                || il.contains("socket")
5819                || il.contains("connect")
5820                || il.contains("recv")
5821                || il.contains("send")
5822                || il.contains("exec")
5823                || il.contains("system")
5824                || il.contains("popen")
5825                || il.contains("fork")
5826        })
5827        .take(10)
5828        .collect();
5829
5830    // Output YARA rule
5831    println!("rule {} {{", rule_name);
5832    println!("    meta:");
5833    for (k, v) in &meta {
5834        println!("        {} = \"{}\"", k, v);
5835    }
5836    println!("        description = \"Auto-generated by rsleigh decompiler\"");
5837    println!();
5838
5839    println!("    strings:");
5840    let mut str_idx = 0;
5841    for s in &selected_strings {
5842        let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
5843        println!("        $s{} = \"{}\"", str_idx, escaped);
5844        str_idx += 1;
5845    }
5846    for s in &selected_wide {
5847        let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
5848        println!("        $w{} = \"{}\" wide", str_idx, escaped);
5849        str_idx += 1;
5850    }
5851    for (name, hex) in &hex_patterns {
5852        println!("        $h_{} = {{ {} }}", name, hex);
5853    }
5854    for (i, imp) in suspicious_imports.iter().enumerate() {
5855        println!("        $imp{} = \"{}\"", i, imp);
5856    }
5857    println!();
5858
5859    // Condition: require several strings + optional hex patterns
5860    let total_str_count = selected_strings.len() + selected_wide.len();
5861    let min_match = (total_str_count / 3).max(3).min(total_str_count);
5862    println!("    condition:");
5863    let mut conditions = Vec::new();
5864    if is_pe {
5865        conditions.push("uint16(0) == 0x5A4D".to_string()); // MZ header
5866    } else if is_elf {
5867        conditions.push("uint32(0) == 0x464C457F".to_string()); // \x7fELF
5868    }
5869    if total_str_count > 0 {
5870        conditions.push(format!("{} of ($s*, $w*)", min_match));
5871    }
5872    if !hex_patterns.is_empty() {
5873        conditions.push("any of ($h_*)".to_string());
5874    }
5875    if !suspicious_imports.is_empty() {
5876        conditions.push(format!("{} of ($imp*)", suspicious_imports.len().min(3)));
5877    }
5878    if conditions.is_empty() {
5879        conditions.push("true".to_string());
5880    }
5881    println!("        {}", conditions.join(" and\n        "));
5882    println!("}}");
5883}
5884
5885fn chrono_date() -> String {
5886    // Simple date without chrono dependency
5887    "2026-04-13".to_string()
5888}
5889
5890fn run_raw(
5891    data: &[u8],
5892    arch: rsleigh_api::Architecture,
5893    base: u64,
5894    args: &[String],
5895    all_mode: bool,
5896) {
5897    eprintln!(
5898        "Architecture: {:?} (raw binary, base=0x{:x}, size={})",
5899        arch,
5900        base,
5901        data.len()
5902    );
5903
5904    // Treat entire file as one code segment
5905    let segs = vec![(base, data.len() as u64, 0u64)];
5906
5907    // Discover functions via CALL scanning
5908    let mut found = std::collections::BTreeSet::new();
5909    found.insert(base); // entry at base
5910
5911    let mut dec = rsleigh_api::Decoder::new(arch);
5912    let code_end = base + data.len() as u64;
5913
5914    // Architecture-specific CALL scanning
5915    match arch {
5916        rsleigh_api::Architecture::MIPS32 => {
5917            // MIPS JAL: 000011 imm26 → opcode 0x0C000000
5918            for i in (0..data.len().saturating_sub(3)).step_by(4) {
5919                let word = u32::from_be_bytes(data[i..i + 4].try_into().unwrap_or([0; 4]));
5920                if (word >> 26) == 3 {
5921                    // JAL
5922                    let target =
5923                        ((base + i as u64) & 0xF0000000) | ((word & 0x03FFFFFF) as u64) << 2;
5924                    if target >= base && target < code_end {
5925                        found.insert(target);
5926                    }
5927                }
5928            }
5929            // Also try little-endian MIPS
5930            let mut found_le = std::collections::BTreeSet::new();
5931            for i in (0..data.len().saturating_sub(3)).step_by(4) {
5932                let word = u32::from_le_bytes(data[i..i + 4].try_into().unwrap_or([0; 4]));
5933                if (word >> 26) == 3 {
5934                    let target =
5935                        ((base + i as u64) & 0xF0000000) | ((word & 0x03FFFFFF) as u64) << 2;
5936                    if target >= base && target < code_end {
5937                        found_le.insert(target);
5938                    }
5939                }
5940            }
5941            // Use whichever endianness found more targets
5942            if found_le.len() > found.len() * 2 {
5943                found = found_le;
5944                found.insert(base);
5945                eprintln!("Detected: MIPS little-endian ({} JAL targets)", found.len());
5946            } else {
5947                eprintln!("Detected: MIPS big-endian ({} JAL targets)", found.len());
5948            }
5949        }
5950        rsleigh_api::Architecture::ARM32 => {
5951            for i in (0..data.len().saturating_sub(3)).step_by(4) {
5952                let word = u32::from_le_bytes(data[i..i + 4].try_into().unwrap_or([0; 4]));
5953                if (word & 0x0F000000) == 0x0B000000 {
5954                    // BL
5955                    let imm24 = word & 0x00FFFFFF;
5956                    let offset = if imm24 & 0x800000 != 0 {
5957                        ((imm24 | 0xFF000000) as i32) << 2
5958                    } else {
5959                        (imm24 as i32) << 2
5960                    };
5961                    let target = (base as i64 + i as i64 + 8 + offset as i64) as u64;
5962                    if target >= base && target < code_end {
5963                        found.insert(target);
5964                    }
5965                }
5966            }
5967        }
5968        rsleigh_api::Architecture::X86_64 | rsleigh_api::Architecture::X86_32 => {
5969            for i in 0..data.len().saturating_sub(5) {
5970                if data[i] == 0xE8 {
5971                    let rel = i32::from_le_bytes(data[i + 1..i + 5].try_into().unwrap_or([0; 4]));
5972                    let target = (base as i64 + i as i64 + 5 + rel as i64) as u64;
5973                    if target >= base && target < code_end {
5974                        found.insert(target);
5975                    }
5976                }
5977            }
5978        }
5979        _ => {}
5980    }
5981
5982    let symbols: Vec<(u64, String)> = found
5983        .into_iter()
5984        .map(|addr| (addr, format!("FUN_{:08x}", addr)))
5985        .collect();
5986
5987    // Which functions to process? Skip --raw/--base and their values.
5988    let skip_values: std::collections::HashSet<usize> = {
5989        let mut s = std::collections::HashSet::new();
5990        for (i, a) in args.iter().enumerate() {
5991            if a == "--raw" || a == "--base" {
5992                s.insert(i);
5993                s.insert(i + 1);
5994            }
5995            if a == "--all" || a == "--json" || a == "--disasm" || a == "--sigs" {
5996                s.insert(i);
5997            }
5998        }
5999        s
6000    };
6001    let func_args: Vec<&str> = args
6002        .iter()
6003        .enumerate()
6004        .filter(|(i, a)| *i >= 2 && !a.starts_with("--") && !skip_values.contains(i))
6005        .map(|(_, a)| a.as_str())
6006        .collect();
6007
6008    if func_args.is_empty() && !all_mode {
6009        eprintln!("{} functions:", symbols.len());
6010        for (addr, name) in &symbols {
6011            println!("  0x{:08x}  {}", addr, name);
6012        }
6013    } else {
6014        let to_decompile: Vec<&(u64, String)> = if all_mode {
6015            symbols.iter().collect()
6016        } else {
6017            symbols
6018                .iter()
6019                .filter(|(_, n)| func_args.iter().any(|a| n == a))
6020                .collect()
6021        };
6022        let path = std::path::Path::new("raw.bin");
6023        for (addr, name) in to_decompile {
6024            let off = (*addr - base) as usize;
6025            let max = 4096.min(data.len().saturating_sub(off));
6026            if max < 4 {
6027                continue;
6028            }
6029            let bytes = &data[off..off + max];
6030            let mut pos = 0;
6031            let mut insts = Vec::new();
6032            let next_func = symbols
6033                .iter()
6034                .filter(|(a, _)| *a > *addr)
6035                .map(|(a, _)| *a)
6036                .min()
6037                .unwrap_or(*addr + max as u64);
6038            let decode_max = ((next_func - *addr) as usize).min(max);
6039            while pos < decode_max {
6040                match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
6041                    dec.decode(&bytes[pos..], *addr + pos as u64)
6042                })) {
6043                    Ok(Ok(inst)) => {
6044                        let l = inst.len as usize;
6045                        if l == 0 {
6046                            pos += 4;
6047                            continue;
6048                        }
6049                        insts.push((*addr + pos as u64, inst));
6050                        pos += l;
6051                    }
6052                    Ok(Err(_)) | Err(_) => {
6053                        pos += 4;
6054                    }
6055                }
6056            }
6057            if !insts.is_empty() {
6058                let output = maybe_annotate_crypto(rsleigh_decompile::decompile_with_binary(
6059                    arch,
6060                    &insts,
6061                    Some(data),
6062                    Some(path),
6063                ));
6064                if !output.trim().is_empty() {
6065                    println!("// {}", name);
6066                    println!("{}", output);
6067                }
6068            }
6069        }
6070    }
6071}
6072
6073fn run_wasm(data: &[u8], args: &[String], all_mode: bool) {
6074    eprintln!("Architecture: WebAssembly");
6075    let funcs = wasm::parse_wasm(data);
6076
6077    // Which functions to decompile?
6078    let func_args: Vec<&str> = args[2..]
6079        .iter()
6080        .filter(|a| !a.starts_with("--"))
6081        .map(|a| a.as_str())
6082        .collect();
6083
6084    if func_args.is_empty() && !all_mode {
6085        // List functions
6086        println!("{} functions:", funcs.len());
6087        for f in &funcs {
6088            let params: Vec<&str> = f
6089                .params
6090                .iter()
6091                .map(|t| match t {
6092                    wasmparser::ValType::I32 => "i32",
6093                    wasmparser::ValType::I64 => "i64",
6094                    wasmparser::ValType::F32 => "f32",
6095                    wasmparser::ValType::F64 => "f64",
6096                    _ => "?",
6097                })
6098                .collect();
6099            let ret = f
6100                .results
6101                .first()
6102                .map(|t| match t {
6103                    wasmparser::ValType::I32 => "i32",
6104                    wasmparser::ValType::I64 => "i64",
6105                    wasmparser::ValType::F32 => "f32",
6106                    wasmparser::ValType::F64 => "f64",
6107                    _ => "?",
6108                })
6109                .unwrap_or("void");
6110            println!(
6111                "  func[{}]  {:20} ({}) -> {}",
6112                f.index,
6113                f.name,
6114                params.join(", "),
6115                ret
6116            );
6117        }
6118    } else {
6119        // Decompile
6120        let to_decompile: Vec<&wasm::WasmFunc> = if all_mode {
6121            funcs.iter().collect()
6122        } else {
6123            funcs
6124                .iter()
6125                .filter(|f| {
6126                    func_args
6127                        .iter()
6128                        .any(|a| f.name == *a || format!("func_{}", f.index) == *a)
6129                })
6130                .collect()
6131        };
6132
6133        for f in &to_decompile {
6134            let code = wasm::decompile_wasm_func(data, f, &funcs);
6135            println!("{}", code);
6136        }
6137    }
6138}
6139
6140fn parse_binary(
6141    obj: &goblin::Object,
6142    _data: &[u8],
6143) -> Option<(
6144    rsleigh_api::Architecture,
6145    Vec<(u64, u64, u64)>,
6146    Vec<(u64, String)>,
6147)> {
6148    match obj {
6149        goblin::Object::Mach(goblin::mach::Mach::Binary(m)) => {
6150            let arch = match m.header.cputype() {
6151                7 | 0x01000007 => rsleigh_api::Architecture::X86_64,
6152                12 | 0x0100000c => rsleigh_api::Architecture::AArch64,
6153                _ => return None,
6154            };
6155            let mut segs = Vec::new();
6156            for seg in &m.segments {
6157                if let Ok(secs) = seg.sections() {
6158                    for sec in secs {
6159                        segs.push((sec.0.addr, sec.0.size, sec.0.offset as u64));
6160                    }
6161                }
6162            }
6163            let mut syms = Vec::new();
6164            // Exported/defined symbols
6165            if let Some(ref st) = m.symbols {
6166                for s in st.iter() {
6167                    if let Ok((name, nlist)) = s {
6168                        if nlist.n_type & 0xe == 0xe && nlist.n_value != 0 {
6169                            let clean = name.strip_prefix('_').unwrap_or(name);
6170                            let display =
6171                                demangle_swift_symbol(clean).unwrap_or_else(|| clean.to_string());
6172                            syms.push((nlist.n_value, display));
6173                        }
6174                    }
6175                }
6176            }
6177            // Parse LC_FUNCTION_STARTS — gives ALL function entry points as ULEB128 deltas.
6178            // This is the Mach-O equivalent of PE .pdata — the most reliable function discovery.
6179            let text_vmaddr = m
6180                .segments
6181                .iter()
6182                .find(|s| s.name().ok() == Some("__TEXT"))
6183                .map(|s| s.vmaddr)
6184                .unwrap_or(0);
6185            if text_vmaddr > 0 {
6186                for lc in &m.load_commands {
6187                    if let goblin::mach::load_command::CommandVariant::FunctionStarts(ref fs) =
6188                        lc.command
6189                    {
6190                        let off = fs.dataoff as usize;
6191                        let size = fs.datasize as usize;
6192                        if off + size <= _data.len() {
6193                            let mut pos = off;
6194                            let end = off + size;
6195                            let mut addr = text_vmaddr;
6196                            while pos < end {
6197                                // ULEB128 decode
6198                                let mut delta: u64 = 0;
6199                                let mut shift = 0;
6200                                loop {
6201                                    if pos >= end {
6202                                        break;
6203                                    }
6204                                    let b = _data[pos] as u64;
6205                                    pos += 1;
6206                                    delta |= (b & 0x7f) << shift;
6207                                    shift += 7;
6208                                    if b & 0x80 == 0 {
6209                                        break;
6210                                    }
6211                                }
6212                                if delta == 0 {
6213                                    break;
6214                                }
6215                                addr += delta;
6216                                // Add if not already in symbol list
6217                                if !syms.iter().any(|(a, _)| *a == addr) {
6218                                    syms.push((addr, format!("FUN_{:x}", addr)));
6219                                }
6220                            }
6221                        }
6222                    }
6223                }
6224            }
6225            // Parse ObjC method lists for implementation addresses.
6226            // __objc_methlist contains relative method lists with IMP pointers.
6227            // __objc_const in __DATA contains class_ro_t with baseMethods pointers.
6228            for seg in &m.segments {
6229                if let Ok(secs) = seg.sections() {
6230                    for (sec, _sec_data) in secs {
6231                        let sname = std::str::from_utf8(&sec.sectname)
6232                            .unwrap_or("")
6233                            .trim_end_matches('\0');
6234                        // __objc_stubs: each entry is a small stub (ADRP+LDR+BR on ARM64,
6235                        // JMP on x86_64). Every stub_size-aligned address is a function.
6236                        if sname == "__objc_stubs" || sname == "__stubs" {
6237                            let _soff = sec.offset as usize;
6238                            let ssize = sec.size as usize;
6239                            let saddr = sec.addr;
6240                            // Determine stub size: ARM64=12 bytes, x86_64=8 bytes
6241                            let stub_size: usize =
6242                                if matches!(arch, rsleigh_api::Architecture::AArch64) {
6243                                    12
6244                                } else {
6245                                    8
6246                                };
6247                            let mut pos = 0usize;
6248                            while pos + stub_size <= ssize {
6249                                let addr = saddr + pos as u64;
6250                                if !syms.iter().any(|(a, _)| *a == addr) {
6251                                    syms.push((addr, format!("objc_stub_{:x}", addr)));
6252                                }
6253                                pos += stub_size;
6254                            }
6255                        }
6256                        if sname == "__objc_methlist" {
6257                            // Relative method lists (modern ObjC, ARM64)
6258                            // Each method_list_t: uint32_t entsize_and_flags, uint32_t count
6259                            // Then count × method_t entries (relative offsets)
6260                            let soff = sec.offset as usize;
6261                            let ssize = sec.size as usize;
6262                            let saddr = sec.addr;
6263                            let mut pos = 0usize;
6264                            while pos + 8 <= ssize && soff + pos + 8 <= _data.len() {
6265                                let entsize_flags = u32::from_le_bytes(
6266                                    _data[soff + pos..soff + pos + 4]
6267                                        .try_into()
6268                                        .unwrap_or([0; 4]),
6269                                );
6270                                let count = u32::from_le_bytes(
6271                                    _data[soff + pos + 4..soff + pos + 8]
6272                                        .try_into()
6273                                        .unwrap_or([0; 4]),
6274                                );
6275                                let entsize = (entsize_flags & 0x3FFFFFFF) as usize;
6276                                let is_relative = entsize_flags & 0x80000000 != 0;
6277
6278                                if count > 1000 || entsize == 0 || entsize > 64 {
6279                                    pos += 8;
6280                                    continue;
6281                                }
6282                                let _list_start = pos;
6283
6284                                for m_idx in 0..count as usize {
6285                                    let m_off = soff + pos + 8 + m_idx * entsize;
6286                                    if m_off + entsize > _data.len() {
6287                                        break;
6288                                    }
6289
6290                                    if is_relative && entsize >= 12 {
6291                                        // Relative method_t: int32_t name, int32_t types, int32_t imp
6292                                        // imp is relative to its own address
6293                                        let imp_field_addr =
6294                                            saddr + (pos + 8 + m_idx * entsize + 8) as u64;
6295                                        let imp_rel = i32::from_le_bytes(
6296                                            _data[m_off + 8..m_off + 12]
6297                                                .try_into()
6298                                                .unwrap_or([0; 4]),
6299                                        );
6300                                        let imp =
6301                                            imp_field_addr.wrapping_add(imp_rel as i64 as u64);
6302                                        if !syms.iter().any(|(a, _)| *a == imp) {
6303                                            syms.push((imp, format!("objc_method_{:x}", imp)));
6304                                        }
6305                                    } else if !is_relative && entsize >= 24 {
6306                                        // Absolute method_t: ptr name, ptr types, ptr imp
6307                                        let imp = u64::from_le_bytes(
6308                                            _data[m_off + 16..m_off + 24]
6309                                                .try_into()
6310                                                .unwrap_or([0; 8]),
6311                                        );
6312                                        if imp > 0 && !syms.iter().any(|(a, _)| *a == imp) {
6313                                            syms.push((imp, format!("objc_method_{:x}", imp)));
6314                                        }
6315                                    }
6316                                }
6317                                pos += 8 + count as usize * entsize;
6318                                // Align to 4 bytes
6319                                if pos % 4 != 0 {
6320                                    pos += 4 - (pos % 4);
6321                                }
6322                            }
6323                        }
6324                    }
6325                }
6326            }
6327
6328            Some((arch, segs, syms))
6329        }
6330        goblin::Object::Elf(elf) => {
6331            let arch = match elf.header.e_machine {
6332                0x3E => rsleigh_api::Architecture::X86_64,
6333                0xB7 => rsleigh_api::Architecture::AArch64,
6334                0x28 => rsleigh_api::Architecture::ARM32,
6335                0x08 => rsleigh_api::Architecture::MIPS32,
6336                0xF3 => rsleigh_api::Architecture::RiscV64,
6337                _ => return None,
6338            };
6339            let segs = elf
6340                .section_headers
6341                .iter()
6342                .filter(|sh| sh.sh_flags & 0x4 != 0)
6343                .map(|sh| (sh.sh_addr, sh.sh_size, sh.sh_offset))
6344                .collect();
6345            let mut syms = Vec::new();
6346            for sym in elf.syms.iter() {
6347                if sym.st_type() == goblin::elf::sym::STT_FUNC && sym.st_value != 0 {
6348                    if let Some(name) = elf.strtab.get_at(sym.st_name) {
6349                        if !name.is_empty() {
6350                            syms.push((sym.st_value, demangle_symbol(name)));
6351                        }
6352                    }
6353                }
6354            }
6355            for sym in elf.dynsyms.iter() {
6356                if sym.st_type() == goblin::elf::sym::STT_FUNC && sym.st_value != 0 {
6357                    if let Some(name) = elf.dynstrtab.get_at(sym.st_name) {
6358                        if !name.is_empty() {
6359                            syms.push((sym.st_value, demangle_symbol(name)));
6360                        }
6361                    }
6362                }
6363            }
6364            Some((arch, segs, syms))
6365        }
6366        goblin::Object::PE(pe) => {
6367            // Detect architecture from PE machine type
6368            let arch = match pe.header.coff_header.machine {
6369                0xAA64 => rsleigh_api::Architecture::AArch64, // ARM64
6370                0x8664 => rsleigh_api::Architecture::X86_64,  // AMD64
6371                0x014C => rsleigh_api::Architecture::X86_32,  // i386
6372                0x01C4 => rsleigh_api::Architecture::ARM32,   // ARMv7
6373                _ => {
6374                    if pe.is_64 {
6375                        rsleigh_api::Architecture::X86_64
6376                    } else {
6377                        rsleigh_api::Architecture::X86_32
6378                    }
6379                }
6380            };
6381            let base = pe.image_base as u64;
6382            let segs = pe
6383                .sections
6384                .iter()
6385                .filter(|s| s.characteristics & 0x20000000 != 0)
6386                .map(|s| {
6387                    (
6388                        base + s.virtual_address as u64,
6389                        s.virtual_size as u64,
6390                        s.pointer_to_raw_data as u64,
6391                    )
6392                })
6393                .collect();
6394            let mut syms = Vec::new();
6395            for exp in pe.exports.iter() {
6396                if let Some(name) = exp.name {
6397                    if exp.rva != 0 {
6398                        syms.push((base + exp.rva as u64, name.to_string()));
6399                    }
6400                }
6401            }
6402            Some((arch, segs, syms))
6403        }
6404        _ => None,
6405    }
6406}
6407
6408/// Discover functions in a stripped PE by recursive descent from entry point.
6409/// Follows direct CALL targets to find function boundaries.
6410fn discover_pe_functions(
6411    entry: u64,
6412    segs: &[(u64, u64, u64)],
6413    data: &[u8],
6414    arch: rsleigh_api::Architecture,
6415) -> Vec<(u64, String)> {
6416    use std::collections::{BTreeSet, VecDeque};
6417
6418    let mut found = BTreeSet::new();
6419    let mut queue = VecDeque::new();
6420    found.insert(entry);
6421    queue.push_back(entry);
6422
6423    let mut dec = rsleigh_api::Decoder::new(arch);
6424
6425    while let Some(func_addr) = queue.pop_front() {
6426        // Translate VA to file offset
6427        let off = segs.iter().find_map(|(va, sz, fo)| {
6428            if func_addr >= *va && func_addr < va + sz {
6429                Some(fo + (func_addr - va))
6430            } else {
6431                None
6432            }
6433        });
6434        let Some(off) = off else { continue };
6435        let max = 4096.min(data.len().saturating_sub(off as usize));
6436        if max == 0 {
6437            continue;
6438        }
6439        let bytes = &data[off as usize..off as usize + max];
6440
6441        let mut io = 0usize;
6442        while io < max {
6443            let ok = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
6444                dec.decode(&bytes[io..], func_addr + io as u64)
6445            }));
6446            match ok {
6447                Ok(Ok(inst)) => {
6448                    let l = inst.len as usize;
6449                    if l == 0 {
6450                        io += 1;
6451                        continue;
6452                    }
6453                    // Look for CALL with direct target
6454                    for op in &inst.ops {
6455                        if let pcode_ir::PcodeOp::Call { dest, .. } = op {
6456                            if dest.space == pcode_ir::AddressSpaceId::Ram {
6457                                let call_target = dest.offset;
6458                                // Only follow targets in executable segments
6459                                let in_seg = segs
6460                                    .iter()
6461                                    .any(|(va, sz, _)| call_target >= *va && call_target < va + sz);
6462                                if in_seg && !found.contains(&call_target) {
6463                                    found.insert(call_target);
6464                                    queue.push_back(call_target);
6465                                }
6466                            }
6467                        }
6468                    }
6469                    // Stop at RET
6470                    if inst
6471                        .ops
6472                        .iter()
6473                        .any(|op| matches!(op, pcode_ir::PcodeOp::Return { .. }))
6474                    {
6475                        break;
6476                    }
6477                    io += l;
6478                }
6479                Ok(Err(_)) => break,
6480                Err(_) => {
6481                    io += 1;
6482                }
6483            }
6484        }
6485    }
6486
6487    // Phase 2a: Parse .pdata exception directory for PE64 (gives exact function boundaries)
6488    if let Ok(obj) = goblin::Object::parse(data) {
6489        if let goblin::Object::PE(pe) = &obj {
6490            if pe.is_64 {
6491                let base = pe.image_base as u64;
6492                for sec in &pe.sections {
6493                    let name = std::str::from_utf8(&sec.name)
6494                        .unwrap_or("")
6495                        .trim_end_matches('\0');
6496                    if name == ".pdata" {
6497                        let fo = sec.pointer_to_raw_data as usize;
6498                        let sz = sec.virtual_size.min(sec.size_of_raw_data) as usize;
6499                        if fo + sz <= data.len() {
6500                            // Entry size depends on architecture:
6501                            // x86-64: 12 bytes (BeginAddress:4, EndAddress:4, UnwindData:4)
6502                            // ARM64:  8 bytes (BeginAddress:4, UnwindData:4)
6503                            let pe_off_local =
6504                                u32::from_le_bytes(data[0x3c..0x40].try_into().unwrap_or([0; 4]))
6505                                    as usize;
6506                            let machine = u16::from_le_bytes([
6507                                data[pe_off_local + 4],
6508                                data[pe_off_local + 5],
6509                            ]);
6510                            let entry_size: usize = if machine == 0xAA64 { 8 } else { 12 };
6511
6512                            let mut off = 0;
6513                            while off + entry_size <= sz {
6514                                let begin_rva = u32::from_le_bytes([
6515                                    data[fo + off],
6516                                    data[fo + off + 1],
6517                                    data[fo + off + 2],
6518                                    data[fo + off + 3],
6519                                ]) as u64;
6520                                if begin_rva == 0 {
6521                                    break;
6522                                }
6523                                let func_va = base + begin_rva;
6524                                if !found.contains(&func_va) {
6525                                    let in_seg = segs
6526                                        .iter()
6527                                        .any(|(va, sz, _)| func_va >= *va && func_va < va + sz);
6528                                    if in_seg {
6529                                        found.insert(func_va);
6530                                    }
6531                                }
6532                                off += entry_size;
6533                            }
6534                        }
6535                    }
6536                }
6537            }
6538        }
6539    }
6540
6541    let is_aarch64 = matches!(
6542        arch,
6543        rsleigh_api::Architecture::AArch64 | rsleigh_api::Architecture::ARM32
6544    );
6545
6546    // Phase 2b: Prologue scanning — find functions not reached by direct CALL.
6547    // Scan executable sections for common function prologues:
6548    //   55 8B EC       push ebp; mov ebp, esp  (x86-32 standard)
6549    //   55 89 E5       push ebp; mov esp, ebp  (GCC variant)
6550    //   48 89 5C 24    mov [rsp+...], rbx      (x86-64 MS ABI)
6551    //   48 83 EC       sub rsp, imm8           (x86-64 leaf)
6552    for (seg_va, seg_sz, seg_fo) in segs {
6553        let fo = *seg_fo as usize;
6554        let sz = (*seg_sz as usize).min(data.len().saturating_sub(fo));
6555        if fo + sz > data.len() {
6556            continue;
6557        }
6558        let bytes = &data[fo..fo + sz];
6559
6560        let mut off = 0usize;
6561        while off + 3 <= sz {
6562            let va = seg_va + off as u64;
6563            if !found.contains(&va) {
6564                let boundary = off == 0 || matches!(bytes[off - 1], 0xC3 | 0xCC | 0x90 | 0x00);
6565                let is_prologue =
6566                    // === x86-32 patterns ===
6567                    // push ebp; mov ebp, esp (55 8B EC / 55 89 E5)
6568                    (bytes[off] == 0x55 && off + 3 <= sz
6569                        && ((bytes[off+1] == 0x8B && bytes[off+2] == 0xEC)
6570                            || (bytes[off+1] == 0x89 && bytes[off+2] == 0xE5)))
6571                    // push esi/edi at boundary — only if followed by another push or sub esp
6572                    || (off + 2 <= sz && (bytes[off] == 0x56 || bytes[off] == 0x57)
6573                        && boundary && off > 0
6574                        && matches!(bytes[off+1], 0x53 | 0x55 | 0x56 | 0x57 | 0x83 | 0x8B))
6575                    // mov reg, [esp+4] at boundary
6576                    || (off + 4 <= sz && bytes[off] == 0x8B
6577                        && (bytes[off+1] == 0x44 || bytes[off+1] == 0x4C)
6578                        && bytes[off+2] == 0x24 && bytes[off+3] == 0x04
6579                        && boundary && off > 0)
6580                    // === x86-64 patterns ===
6581                    // sub rsp, imm8 (48 83 EC xx) — standard x86-64 prologue
6582                    || (off + 4 <= sz && bytes[off] == 0x48
6583                        && bytes[off+1] == 0x83 && bytes[off+2] == 0xEC
6584                        && boundary)
6585                    // sub rsp, imm32 (48 81 EC xx xx xx xx) — large frame
6586                    || (off + 7 <= sz && bytes[off] == 0x48
6587                        && bytes[off+1] == 0x81 && bytes[off+2] == 0xEC
6588                        && boundary)
6589                    // push rbp (55) at boundary in 64-bit context
6590                    || (bytes[off] == 0x55 && off + 2 <= sz
6591                        && bytes[off+1] == 0x48  // followed by REX prefix (mov rbp, rsp)
6592                        && boundary)
6593                    // mov [rsp+N], rbx (48 89 5C 24 xx) — Windows x64 ABI
6594                    || (off + 5 <= sz && bytes[off] == 0x48
6595                        && bytes[off+1] == 0x89 && bytes[off+2] == 0x5C
6596                        && bytes[off+3] == 0x24
6597                        && boundary)
6598                    // mov [rsp+N], rdi (48 89 7C 24 xx) — save first param
6599                    || (off + 5 <= sz && bytes[off] == 0x48
6600                        && bytes[off+1] == 0x89 && bytes[off+2] == 0x7C
6601                        && bytes[off+3] == 0x24
6602                        && boundary)
6603                    // push rbx (53) at boundary with REX following (common Win64 prologue)
6604                    || (off + 2 <= sz && bytes[off] == 0x53
6605                        && boundary && off > 0
6606                        && bytes[off+1] == 0x48)
6607                    // push r-prefixed (41 5x) at boundary — push r12..r15
6608                    || (off + 3 <= sz && bytes[off] == 0x41
6609                        && matches!(bytes[off+1], 0x54 | 0x55 | 0x56 | 0x57)
6610                        && boundary && off > 0);
6611
6612                if is_prologue {
6613                    let valid_boundary =
6614                        off == 0 || matches!(bytes[off - 1], 0xC3 | 0xCC | 0x90 | 0x00);
6615                    if valid_boundary {
6616                        found.insert(va);
6617                    }
6618                }
6619            }
6620            off += 1;
6621        }
6622    }
6623
6624    // AArch64 prologue scanning (4-byte aligned instructions)
6625    if is_aarch64 {
6626        for (seg_va, seg_sz, seg_fo) in segs {
6627            let fo = *seg_fo as usize;
6628            let sz = (*seg_sz as usize).min(data.len().saturating_sub(fo));
6629            if fo + sz > data.len() {
6630                continue;
6631            }
6632            let bytes = &data[fo..fo + sz];
6633
6634            let mut off = 0usize;
6635            while off + 4 <= sz {
6636                let va = seg_va + off as u64;
6637                if !found.contains(&va) {
6638                    let insn = u32::from_le_bytes([
6639                        bytes[off],
6640                        bytes[off + 1],
6641                        bytes[off + 2],
6642                        bytes[off + 3],
6643                    ]);
6644
6645                    // Check for AArch64 function prologues:
6646                    // STP X29, X30, [SP, #off] — save FP+LR (both pre-index and signed offset)
6647                    //   Pre-index: A98xxxxx (STP X29,X30,[SP,#-N]!)
6648                    //   Signed offset: A9BF7BFD etc. (STP X29,X30,[SP,#-16])
6649                    // Check: Rt=29(FP), Rt2=30(LR), Rn=31(SP), opc=10 (64-bit)
6650                    let rt = insn & 0x1F;
6651                    let rt2 = (insn >> 10) & 0x1F;
6652                    let rn = (insn >> 5) & 0x1F;
6653                    let is_stp_fp_lr =
6654                        // STP pre-index: A98xxxxx
6655                        ((insn & 0xFFE00000) == 0xA9800000 && rt == 29 && rt2 == 30)
6656                        // STP signed offset: A9xxxxxx where Rt=29, Rt2=30, Rn=31
6657                        || ((insn & 0xFFC00000) == 0xA9000000 && rt == 29 && rt2 == 30 && rn == 31);
6658
6659                    // SUB SP, SP, #imm — stack frame allocation
6660                    let is_sub_sp = (insn & 0xFF0003E0) == 0xD10003E0 && ((insn >> 5) & 0x1F) == 31;
6661
6662                    // STP with SP base (callee-saved register saves, any register pair)
6663                    let _is_stp_sp = (insn & 0xFFC00000) == 0xA9000000 && rn == 31;
6664
6665                    // ADRP — common leaf function start (loads page address)
6666                    let is_adrp = (insn & 0x9F000000) == 0x90000000;
6667
6668                    // MOV X29, SP (set frame pointer without STP — some leaf functions)
6669                    let is_mov_fp_sp = insn == 0x910003FD; // ADD X29, SP, #0
6670
6671                    // LDR from literal pool or GOT — common in position-independent thunks
6672                    let is_ldr_lit = (insn & 0xFF000000) == 0x58000000; // LDR Xt, label
6673
6674                    // Boundary check: previous instruction should be RET (D65F03C0) or 0/padding
6675                    let prev_ok = if off >= 4 {
6676                        let prev_insn = u32::from_le_bytes([
6677                            bytes[off - 4],
6678                            bytes[off - 3],
6679                            bytes[off - 2],
6680                            bytes[off - 1],
6681                        ]);
6682                        prev_insn == 0xD65F03C0  // RET
6683                            || prev_insn == 0x00000000  // padding
6684                            || prev_insn == 0xD503201F  // NOP
6685                            || (prev_insn >> 26) == 0b000101 // B (unconditional branch)
6686                    } else {
6687                        true // start of section
6688                    };
6689
6690                    if is_stp_fp_lr || is_sub_sp {
6691                        // STP FP/LR and SUB SP are strong prologues — accept with loose boundary
6692                        found.insert(va);
6693                    } else if prev_ok && (is_adrp || is_mov_fp_sp || is_ldr_lit) {
6694                        // Weaker patterns — require boundary check
6695                        found.insert(va);
6696                    }
6697                }
6698                off += 4;
6699            }
6700        }
6701    }
6702
6703    // Phase 2c: Exhaustive CALL target scanning.
6704    // Scan all executable sections for CALL instructions and collect targets.
6705    // x86: E8 rel32 (5 bytes)
6706    // AArch64: BL imm26 (4 bytes, opcode 10010100 + 26-bit signed offset)
6707    for (seg_va, seg_sz, seg_fo) in segs {
6708        let fo = *seg_fo as usize;
6709        let sz = (*seg_sz as usize).min(data.len().saturating_sub(fo));
6710        if fo + sz > data.len() {
6711            continue;
6712        }
6713        let bytes = &data[fo..fo + sz];
6714
6715        if is_aarch64 {
6716            // AArch64: BL imm26 — instruction format: 1001_01xx_xxxx_xxxx_xxxx_xxxx_xxxx_xxxx
6717            // Top 6 bits = 100101, bottom 26 bits = signed offset (in instructions, × 4)
6718            let mut off = 0usize;
6719            while off + 4 <= sz {
6720                let insn = u32::from_le_bytes([
6721                    bytes[off],
6722                    bytes[off + 1],
6723                    bytes[off + 2],
6724                    bytes[off + 3],
6725                ]);
6726                if (insn >> 26) == 0b100101 {
6727                    // BL
6728                    let imm26 = insn & 0x03FF_FFFF;
6729                    // Sign-extend 26-bit to 64-bit, multiply by 4
6730                    let offset = if imm26 & 0x0200_0000 != 0 {
6731                        ((imm26 | 0xFC00_0000) as i32 as i64) * 4
6732                    } else {
6733                        (imm26 as i64) * 4
6734                    };
6735                    let target = (seg_va + off as u64).wrapping_add(offset as u64);
6736                    let in_seg = segs
6737                        .iter()
6738                        .any(|(va, sz, _)| target >= *va && target < va + sz);
6739                    if in_seg && !found.contains(&target) {
6740                        found.insert(target);
6741                    }
6742                }
6743                off += 4; // AArch64 instructions are 4-byte aligned
6744            }
6745        } else {
6746            // x86: E8 rel32 (CALL)
6747            let mut off = 0usize;
6748            while off + 5 <= sz {
6749                if bytes[off] == 0xE8 {
6750                    let disp = i32::from_le_bytes([
6751                        bytes[off + 1],
6752                        bytes[off + 2],
6753                        bytes[off + 3],
6754                        bytes[off + 4],
6755                    ]);
6756                    let target = (seg_va + off as u64 + 5).wrapping_add(disp as i64 as u64);
6757                    let in_seg = segs
6758                        .iter()
6759                        .any(|(va, sz, _)| target >= *va && target < va + sz);
6760                    if in_seg && !found.contains(&target) {
6761                        found.insert(target);
6762                    }
6763                }
6764                off += 1;
6765            }
6766        }
6767    }
6768
6769    // Phase 3: Thunk discovery — find JMP [rip+disp] import thunks at function boundaries.
6770    // Only for PE64 — PE32 thunks are already found by the prologue scanner or import resolution.
6771    let is_pe64 = goblin::Object::parse(data)
6772        .ok()
6773        .and_then(|o| {
6774            if let goblin::Object::PE(pe) = o {
6775                Some(pe.is_64)
6776            } else {
6777                None
6778            }
6779        })
6780        .unwrap_or(false);
6781    if is_pe64 {
6782        for (seg_va, seg_sz, seg_fo) in segs {
6783            let fo = *seg_fo as usize;
6784            let sz = (*seg_sz as usize).min(data.len().saturating_sub(fo));
6785            if fo + sz > data.len() {
6786                continue;
6787            }
6788            let bytes = &data[fo..fo + sz];
6789
6790            let mut off = 0usize;
6791            while off + 2 <= sz {
6792                let va = seg_va + off as u64;
6793                if !found.contains(&va) {
6794                    let boundary = off == 0 || matches!(bytes[off - 1], 0xC3 | 0xCC | 0x90 | 0x00);
6795                    if boundary {
6796                        let is_thunk =
6797                        // JMP [rip+disp32]: FF 25 xx xx xx xx (import thunks)
6798                        (off + 6 <= sz && bytes[off] == 0xFF && bytes[off+1] == 0x25)
6799                        // JMP rel32: E9 xx xx xx xx (C++ virtual thunks, tail calls)
6800                        // At function boundaries — preceded by RET/INT3/NOP.
6801                        || (off + 5 <= sz && bytes[off] == 0xE9
6802                            && off > 0 && matches!(bytes[off - 1], 0xC3 | 0xCC | 0x90));
6803
6804                        if is_thunk {
6805                            found.insert(va);
6806                        }
6807                    }
6808                }
6809                off += 1;
6810            }
6811        }
6812    } // end if is_pe64
6813
6814    // Phase 4: Data reference scanning — find function pointers in .rdata/.data sections.
6815    // Vtable entries, C++ exception handler tables, and callback registrations point to
6816    // code addresses that aren't reached by CALL descent.
6817    // Only for PE64 — PE32 has too many false positives from 32-bit values that look like pointers.
6818    if let Ok(obj) = goblin::Object::parse(data) {
6819        if let goblin::Object::PE(pe) = &obj {
6820            if !pe.is_64 { /* skip PE32 */
6821            } else {
6822                let _base = pe.image_base as u64;
6823                // Identify executable address range
6824                let mut text_start = u64::MAX;
6825                let mut text_end = 0u64;
6826                for seg in segs.iter() {
6827                    text_start = text_start.min(seg.0);
6828                    text_end = text_end.max(seg.0 + seg.1);
6829                }
6830
6831                for sec in &pe.sections {
6832                    let name = std::str::from_utf8(&sec.name)
6833                        .unwrap_or("")
6834                        .trim_end_matches('\0');
6835                    if name == ".rdata" || name == ".data" || name == "_RDATA" {
6836                        let fo = sec.pointer_to_raw_data as usize;
6837                        let sz = sec.virtual_size.min(sec.size_of_raw_data) as usize;
6838                        if fo + sz > data.len() {
6839                            continue;
6840                        }
6841                        let ptr_size: usize = 8; // PE64 only
6842
6843                        // Phase 4a: Vtable detection — consecutive function pointer arrays.
6844                        // A vtable is 2+ consecutive 8-byte pointers into .text.
6845                        // All pointers in a vtable are accepted without prologue check
6846                        // (vtable entries include tiny thunks like "mov al, 1; ret" and
6847                        // C++ adjustment thunks like "sub rcx, N; jmp real_method").
6848                        {
6849                            let mut consecutive = 0usize;
6850                            let mut vtable_ptrs: Vec<u64> = Vec::new();
6851                            let mut off = 0usize;
6852                            while off + ptr_size <= sz {
6853                                let ptr = u64::from_le_bytes(
6854                                    data[fo + off..fo + off + 8].try_into().unwrap_or([0; 8]),
6855                                );
6856                                if ptr >= text_start && ptr < text_end {
6857                                    vtable_ptrs.push(ptr);
6858                                    consecutive += 1;
6859                                } else {
6860                                    if consecutive >= 2 {
6861                                        for &vptr in &vtable_ptrs[vtable_ptrs.len() - consecutive..]
6862                                        {
6863                                            found.insert(vptr);
6864                                        }
6865                                    }
6866                                    consecutive = 0;
6867                                }
6868                                off += ptr_size;
6869                            }
6870                            if consecutive >= 2 {
6871                                for &vptr in &vtable_ptrs[vtable_ptrs.len() - consecutive..] {
6872                                    found.insert(vptr);
6873                                }
6874                            }
6875                        }
6876
6877                        // Phase 4b: Single function pointers with strict prologue verification.
6878                        let mut off = 0usize;
6879                        while off + ptr_size <= sz {
6880                            let ptr = u64::from_le_bytes(
6881                                data[fo + off..fo + off + 8].try_into().unwrap_or([0; 8]),
6882                            );
6883
6884                            if ptr >= text_start && ptr < text_end && !found.contains(&ptr) {
6885                                let target_fo = segs.iter().find_map(|(va, sz, sfo)| {
6886                                    if ptr >= *va && ptr < va + sz {
6887                                        Some(sfo + (ptr - va))
6888                                    } else {
6889                                        None
6890                                    }
6891                                });
6892                                if let Some(target_fo) = target_fo {
6893                                    let tfo = target_fo as usize;
6894                                    if tfo + 3 <= data.len() {
6895                                        let (b0, b1, b2) =
6896                                            (data[tfo], data[tfo + 1], data[tfo + 2]);
6897                                        let looks_like_func = (b0 == 0x48 && b1 == 0x83 && b2 == 0xEC)     // sub rsp, imm8
6898                                        || (b0 == 0x48 && b1 == 0x81 && b2 == 0xEC)   // sub rsp, imm32
6899                                        || (b0 == 0x55 && b1 == 0x48)                 // push rbp; REX
6900                                        || (b0 == 0x48 && b1 == 0x89 && (b2 == 0x5C || b2 == 0x7C)) // mov [rsp+N]
6901                                        || (b0 == 0xFF && b1 == 0x25)                 // JMP [rip+disp]
6902                                        || b0 == 0xE9                                 // JMP rel32
6903                                        || (b0 == 0x55 && b1 == 0x8B && b2 == 0xEC)   // push ebp; mov
6904                                        // MSVC: push <reg>; sub rsp, imm8 (reg = rbx/rbp/rsi/rdi)
6905                                        || ((b0 == 0x53 || b0 == 0x55 || b0 == 0x56 || b0 == 0x57)
6906                                            && b1 == 0x48 && b2 == 0x83)
6907                                        // MSVC: push <reg>; sub rsp, imm32
6908                                        || ((b0 == 0x53 || b0 == 0x55 || b0 == 0x56 || b0 == 0x57)
6909                                            && b1 == 0x48 && b2 == 0x81)
6910                                        // MSVC: mov [rsp+0x10], rdx / [rsp+0x18], r8 (arg home)
6911                                        || (b0 == 0x48 && b1 == 0x89 && b2 == 0x54)
6912                                        || (b0 == 0x4C && b1 == 0x89 && b2 == 0x44);
6913                                        if looks_like_func {
6914                                            found.insert(ptr);
6915                                        }
6916                                    }
6917                                }
6918                            }
6919                            off += ptr_size;
6920                        }
6921                    }
6922                }
6923            }
6924        }
6925    }
6926
6927    // Also run PyMethodDef scan during the stripped-PE path so `--all`
6928    // automatically picks up Python-registered methods.
6929    for (addr, name) in scan_pymethoddef(segs, data) {
6930        found.insert(addr);
6931        // Names attached here lose to existing FUN_xxx in the final map;
6932        // that's OK — the standalone caller above owns name attribution.
6933        let _ = name;
6934    }
6935
6936    let sorted: Vec<u64> = found.into_iter().collect();
6937    // Attach import names to thunk stubs so `CALL <thunk_va>` callers
6938    // render as the resolved import (e.g. `URLDownloadToFileW`) rather than
6939    // an opaque `FUN_004038f2` for a 6-byte `FF 25 disp32` JMP-thunk that
6940    // imports.rs has already resolved.
6941    let import_map = rsleigh_decompile::imports::resolve_imports(data);
6942    sorted
6943        .iter()
6944        .map(|addr| {
6945            let name = import_map
6946                .get(addr)
6947                .cloned()
6948                .unwrap_or_else(|| format!("FUN_{:08x}", addr));
6949            (*addr, name)
6950        })
6951        .collect()
6952}
6953
6954/// Scan PE64 data sections for PyMethodDef arrays.
6955///
6956/// A PyMethodDef entry is a 32-byte struct:
6957///   { const char *ml_name; PyCFunction ml_meth; int ml_flags; const char *ml_doc; }
6958/// Arrays are terminated by a zeroed sentinel. Python C-extensions use this to
6959/// expose methods that would otherwise never be called by any function inside
6960/// the module, so neither CALL-target descent nor vtable scanning finds them.
6961///
6962/// Validation rules per entry, all of which must hold:
6963///   * ml_meth  — within the executable address range
6964///   * ml_name  — points to a short (<=64 byte) ASCII identifier, non-empty
6965///   * ml_flags — fits in a u32 and its value is a plausible METH_* bitmask
6966///   * ml_doc   — NULL, or points to ASCII text
6967///
6968/// Returns a list of (function_va, method_name) for each discovered method.
6969fn scan_pymethoddef(segs: &[(u64, u64, u64)], data: &[u8]) -> Vec<(u64, String)> {
6970    let obj = match goblin::Object::parse(data) {
6971        Ok(o) => o,
6972        Err(_) => return vec![],
6973    };
6974    let pe = match obj {
6975        goblin::Object::PE(pe) => pe,
6976        _ => return vec![],
6977    };
6978    if !pe.is_64 {
6979        return vec![];
6980    }
6981
6982    // segs contains only executable sections — used for the .text range
6983    // check. For string lookups we need all readable sections.
6984    let mut text_start = u64::MAX;
6985    let mut text_end = 0u64;
6986    for seg in segs.iter() {
6987        text_start = text_start.min(seg.0);
6988        text_end = text_end.max(seg.0 + seg.1);
6989    }
6990    let base = pe.image_base as u64;
6991    let all_segs: Vec<(u64, u64, u64)> = pe
6992        .sections
6993        .iter()
6994        .filter(|s| (s.characteristics & 0x40000000) != 0) // readable
6995        .map(|s| {
6996            (
6997                base + s.virtual_address as u64,
6998                s.virtual_size.min(s.size_of_raw_data) as u64,
6999                s.pointer_to_raw_data as u64,
7000            )
7001        })
7002        .collect();
7003    let va_to_fo = |va: u64| -> Option<usize> {
7004        all_segs.iter().find_map(|(v, s, fo)| {
7005            if va >= *v && va < v + s {
7006                Some(*fo as usize + (va - v) as usize)
7007            } else {
7008                None
7009            }
7010        })
7011    };
7012    // Strict C identifier (used for ml_name)
7013    let read_ident = |va: u64| -> Option<String> {
7014        let fo = va_to_fo(va)?;
7015        if fo >= data.len() {
7016            return None;
7017        }
7018        let slice = &data[fo..data.len().min(fo + 128)];
7019        let end = slice.iter().position(|&b| b == 0)?;
7020        if end == 0 || end > 64 {
7021            return None;
7022        }
7023        let s = &slice[..end];
7024        if !s.iter().all(|&b| b == b'_' || b.is_ascii_alphanumeric()) {
7025            return None;
7026        }
7027        Some(String::from_utf8_lossy(s).into_owned())
7028    };
7029    // Loose printable-ASCII check (used for ml_doc — doc strings contain
7030    // spaces, punctuation, newlines). An empty string (first byte is NUL)
7031    // is accepted as equivalent to a NULL doc.
7032    let read_text_ok = |va: u64| -> bool {
7033        let Some(fo) = va_to_fo(va) else {
7034            return false;
7035        };
7036        if fo >= data.len() {
7037            return false;
7038        }
7039        let slice = &data[fo..data.len().min(fo + 512)];
7040        let end = match slice.iter().position(|&b| b == 0) {
7041            Some(e) => e,
7042            None => return false,
7043        };
7044        slice[..end]
7045            .iter()
7046            .all(|&b| b == b'\n' || b == b'\t' || (0x20..=0x7e).contains(&b))
7047    };
7048
7049    let mut out: Vec<(u64, String)> = Vec::new();
7050    for sec in &pe.sections {
7051        let ch = sec.characteristics;
7052        let is_read = (ch & 0x40000000) != 0;
7053        let is_exec = (ch & 0x20000000) != 0;
7054        let is_init = (ch & 0x00000040) != 0;
7055        if !is_read || is_exec || !is_init {
7056            continue;
7057        }
7058        let fo = sec.pointer_to_raw_data as usize;
7059        let sz = sec.virtual_size.min(sec.size_of_raw_data) as usize;
7060        if fo + sz > data.len() || sz < 32 {
7061            continue;
7062        }
7063
7064        let mut off = 0usize;
7065        while off + 32 <= sz {
7066            let rd_q = |o: usize| {
7067                u64::from_le_bytes(data[fo + o..fo + o + 8].try_into().unwrap_or([0; 8]))
7068            };
7069            let ml_name = rd_q(off);
7070            let ml_meth = rd_q(off + 8);
7071            let ml_flags_q = rd_q(off + 16);
7072            let ml_doc = rd_q(off + 24);
7073
7074            let ml_flags_hi = (ml_flags_q >> 32) as u32;
7075            let ml_flags = ml_flags_q as u32;
7076
7077            let meth_ok = ml_meth >= text_start && ml_meth < text_end;
7078            let flags_ok = ml_flags_hi == 0 && ml_flags < 0x1000;
7079            let name_str = if meth_ok && flags_ok {
7080                read_ident(ml_name)
7081            } else {
7082                None
7083            };
7084            let doc_ok = ml_doc == 0 || read_text_ok(ml_doc);
7085
7086            if meth_ok && flags_ok && doc_ok && name_str.is_some() {
7087                out.push((ml_meth, name_str.unwrap()));
7088                off += 32;
7089                continue;
7090            }
7091            off += 8;
7092        }
7093    }
7094    out
7095}
7096
7097/// Discover functions in a stripped ELF binary.
7098/// Uses entry point, CALL scanning, prologue patterns, PLT enumeration, and .init_array.
7099fn discover_elf_functions(
7100    elf: &goblin::elf::Elf,
7101    segs: &[(u64, u64, u64)],
7102    data: &[u8],
7103    arch: rsleigh_api::Architecture,
7104) -> Vec<(u64, String)> {
7105    use std::collections::BTreeSet;
7106
7107    let mut found = BTreeSet::new();
7108
7109    // Detect endianness and pointer size from ELF header
7110    let is_big_endian = elf
7111        .header
7112        .endianness()
7113        .unwrap_or(goblin::container::Endian::Little)
7114        == goblin::container::Endian::Big;
7115    let is_32bit = elf.header.e_machine == 0x08 // MIPS
7116        || elf.header.e_machine == 0x28          // ARM
7117        || (elf.header.e_machine == 0x03 && elf.header.e_ident[4] == 1); // x86 32-bit
7118    let ptr_size: usize = if is_32bit { 4 } else { 8 };
7119
7120    // Endian-aware pointer reading helpers
7121    let read_u32_elf = |bytes: &[u8]| -> u32 {
7122        if is_big_endian {
7123            u32::from_be_bytes(bytes[..4].try_into().unwrap_or([0; 4]))
7124        } else {
7125            u32::from_le_bytes(bytes[..4].try_into().unwrap_or([0; 4]))
7126        }
7127    };
7128    let read_u64_elf = |bytes: &[u8]| -> u64 {
7129        if is_big_endian {
7130            u64::from_be_bytes(bytes[..8].try_into().unwrap_or([0; 8]))
7131        } else {
7132            u64::from_le_bytes(bytes[..8].try_into().unwrap_or([0; 8]))
7133        }
7134    };
7135    let read_i32_elf = |bytes: &[u8]| -> i32 {
7136        if is_big_endian {
7137            i32::from_be_bytes(bytes[..4].try_into().unwrap_or([0; 4]))
7138        } else {
7139            i32::from_le_bytes(bytes[..4].try_into().unwrap_or([0; 4]))
7140        }
7141    };
7142    let read_ptr_elf = |bytes: &[u8]| -> u64 {
7143        if is_32bit {
7144            read_u32_elf(bytes) as u64
7145        } else {
7146            read_u64_elf(bytes)
7147        }
7148    };
7149    let read_i64_elf = |bytes: &[u8]| -> i64 {
7150        if is_big_endian {
7151            i64::from_be_bytes(bytes[..8].try_into().unwrap_or([0; 8]))
7152        } else {
7153            i64::from_le_bytes(bytes[..8].try_into().unwrap_or([0; 8]))
7154        }
7155    };
7156
7157    // 1. Entry point
7158    let entry = elf.header.e_entry;
7159    if entry != 0 {
7160        found.insert(entry);
7161    }
7162
7163    // 2. .init and .fini section addresses
7164    for sh in &elf.section_headers {
7165        let name = elf.shdr_strtab.get_at(sh.sh_name).unwrap_or("");
7166        if (name == ".init" || name == ".fini") && sh.sh_addr != 0 {
7167            found.insert(sh.sh_addr);
7168        }
7169        // .init_array / .fini_array contain function pointers
7170        if (name == ".init_array" || name == ".fini_array") && sh.sh_size > 0 {
7171            let fo = sh.sh_offset as usize;
7172            let count = (sh.sh_size as usize) / ptr_size;
7173            for i in 0..count {
7174                if fo + i * ptr_size + ptr_size <= data.len() {
7175                    let ptr = read_ptr_elf(&data[fo + i * ptr_size..]);
7176                    if ptr != 0 && ptr != u64::MAX && ptr != 0xFFFFFFFF {
7177                        found.insert(ptr);
7178                    }
7179                }
7180            }
7181        }
7182    }
7183
7184    // 3. PLT entries — each is a small stub that jumps to a GOT entry
7185    for sh in &elf.section_headers {
7186        let name = elf.shdr_strtab.get_at(sh.sh_name).unwrap_or("");
7187        if name.starts_with(".plt") && sh.sh_addr != 0 && sh.sh_size > 0 {
7188            // PLT entries are typically 16 bytes each (first entry is special)
7189            let entry_size = if sh.sh_entsize > 0 { sh.sh_entsize } else { 16 };
7190            let mut addr = sh.sh_addr + entry_size; // skip PLT[0]
7191            while addr < sh.sh_addr + sh.sh_size {
7192                found.insert(addr);
7193                addr += entry_size;
7194            }
7195        }
7196    }
7197
7198    // 4. Find .text section bounds for CALL scanning
7199    let text_section = elf
7200        .section_headers
7201        .iter()
7202        .find(|sh| elf.shdr_strtab.get_at(sh.sh_name).unwrap_or("") == ".text");
7203
7204    if let Some(text) = text_section {
7205        let text_addr = text.sh_addr;
7206        let text_size = text.sh_size;
7207        let text_fo = text.sh_offset as usize;
7208        let text_end = text_addr + text_size;
7209
7210        // 4b. Architecture-specific raw CALL scanning for initial seeds.
7211        if text_fo + text_size as usize <= data.len() {
7212            let text_bytes = &data[text_fo..text_fo + text_size as usize];
7213
7214            if matches!(arch, rsleigh_api::Architecture::ARM32) {
7215                // ARM32 BL (Branch with Link): condition[31:28] 1011 imm24
7216                // Encoding: cccc 1011 xxxx xxxx xxxx xxxx xxxx xxxx
7217                // Byte pattern: xx xx xx xB (little-endian, top nibble of byte[3] is cond, byte[3]&0x0F == 0x0B)
7218                // Most common: 0xEB (AL condition = always)
7219                for i in (0..text_bytes.len().saturating_sub(3)).step_by(4) {
7220                    let word =
7221                        u32::from_le_bytes(text_bytes[i..i + 4].try_into().unwrap_or([0; 4]));
7222                    let is_bl = (word & 0x0F000000) == 0x0B000000; // BL opcode
7223                    if is_bl {
7224                        let imm24 = word & 0x00FFFFFF;
7225                        // Sign-extend 24-bit immediate
7226                        let offset = if imm24 & 0x800000 != 0 {
7227                            ((imm24 | 0xFF000000) as i32) << 2
7228                        } else {
7229                            (imm24 as i32) << 2
7230                        };
7231                        // PC is at instruction + 8 in ARM mode
7232                        let target = (text_addr as i64 + i as i64 + 8 + offset as i64) as u64;
7233                        if target >= text_addr && target < text_end {
7234                            found.insert(target);
7235                        }
7236                    }
7237                }
7238
7239                // ARM32 PUSH {regs, lr} prologue: E92D xxxx where xxxx has bit 14 set (LR)
7240                for i in (0..text_bytes.len().saturating_sub(3)).step_by(4) {
7241                    let word =
7242                        u32::from_le_bytes(text_bytes[i..i + 4].try_into().unwrap_or([0; 4]));
7243                    // STMDB SP!, {regs} = E92D xxxx (PUSH)
7244                    if (word & 0xFFFF0000) == 0xE92D0000 {
7245                        let reglist = word & 0xFFFF;
7246                        if reglist & (1 << 14) != 0 {
7247                            // LR in register list
7248                            // Verify: preceded by function boundary (previous word is a return)
7249                            if i == 0 || {
7250                                let prev = u32::from_le_bytes(
7251                                    text_bytes[i - 4..i].try_into().unwrap_or([0; 4]),
7252                                );
7253                                // BX LR = E12FFF1E, POP {pc} = E8BD8xxx, MOV PC, LR = E1A0F00E
7254                                (prev & 0x0FFFFFFF) == 0x012FFF1E // BX LR
7255                                || (prev & 0xFFFF0000) == 0xE8BD0000 && (prev & 0x8000) != 0 // POP {.., PC}
7256                                || prev == 0xE1A0F00E // MOV PC, LR
7257                                || prev == 0x00000000 // padding
7258                            } {
7259                                found.insert(text_addr + i as u64);
7260                            }
7261                        }
7262                    }
7263
7264                    // Thumb PUSH {regs, lr}: B5xx (16-bit)
7265                    // Check both halfwords in this 4-byte window
7266                    for off in [0usize, 2] {
7267                        if i + off + 1 < text_bytes.len() {
7268                            let hw = u16::from_le_bytes(
7269                                text_bytes[i + off..i + off + 2]
7270                                    .try_into()
7271                                    .unwrap_or([0; 2]),
7272                            );
7273                            if (hw & 0xFF00) == 0xB500 {
7274                                // PUSH {.., LR}
7275                                let addr = text_addr + (i + off) as u64;
7276                                if !found.contains(&addr) {
7277                                    // Thumb PUSH at aligned boundary
7278                                    if off == 0 || {
7279                                        let prev_hw = u16::from_le_bytes(
7280                                            text_bytes[i + off - 2..i + off]
7281                                                .try_into()
7282                                                .unwrap_or([0; 2]),
7283                                        );
7284                                        // POP {.., PC} = BDxx, BX LR = 4770
7285                                        (prev_hw & 0xFF00) == 0xBD00
7286                                            || prev_hw == 0x4770
7287                                            || prev_hw == 0x0000
7288                                    } {
7289                                        found.insert(addr);
7290                                    }
7291                                }
7292                            }
7293                        }
7294                    }
7295                }
7296
7297                // Thumb BL: F000 F800-FFFF (32-bit Thumb instruction)
7298                for i in 0..text_bytes.len().saturating_sub(3) {
7299                    let hw1 = u16::from_le_bytes(text_bytes[i..i + 2].try_into().unwrap_or([0; 2]));
7300                    let hw2 =
7301                        u16::from_le_bytes(text_bytes[i + 2..i + 4].try_into().unwrap_or([0; 2]));
7302                    // BL: hw1[15:11] = 11110, hw2[15:12] = 1101 (BL) or 1100 (BLX)
7303                    if (hw1 & 0xF800) == 0xF000 && (hw2 & 0xD000) == 0xD000 {
7304                        let s = ((hw1 >> 10) & 1) as i32;
7305                        let imm10 = (hw1 & 0x3FF) as i32;
7306                        let j1 = ((hw2 >> 13) & 1) as i32;
7307                        let j2 = ((hw2 >> 11) & 1) as i32;
7308                        let imm11 = (hw2 & 0x7FF) as i32;
7309                        let i1 = !(j1 ^ s) & 1;
7310                        let i2 = !(j2 ^ s) & 1;
7311                        let offset = if s != 0 {
7312                            (0xFF000000u32 as i32)
7313                                | (s << 24)
7314                                | (i1 << 23)
7315                                | (i2 << 22)
7316                                | (imm10 << 12)
7317                                | (imm11 << 1)
7318                        } else {
7319                            (i1 << 23) | (i2 << 22) | (imm10 << 12) | (imm11 << 1)
7320                        };
7321                        let target = (text_addr as i64 + i as i64 + 4 + offset as i64) as u64;
7322                        if target >= text_addr && target < text_end {
7323                            found.insert(target);
7324                        }
7325                    }
7326                }
7327            }
7328
7329            if matches!(arch, rsleigh_api::Architecture::AArch64) {
7330                // AArch64 BL: 1001 01xx xxxx xxxx xxxx xxxx xxxx xxxx = 0x94000000 mask 0xFC000000
7331                for i in (0..text_bytes.len().saturating_sub(3)).step_by(4) {
7332                    let word =
7333                        u32::from_le_bytes(text_bytes[i..i + 4].try_into().unwrap_or([0; 4]));
7334                    if (word & 0xFC000000) == 0x94000000 {
7335                        let imm26 = word & 0x03FFFFFF;
7336                        let offset = if imm26 & 0x02000000 != 0 {
7337                            ((imm26 | 0xFC000000) as i32) << 2
7338                        } else {
7339                            (imm26 as i32) << 2
7340                        };
7341                        let target = (text_addr as i64 + i as i64 + offset as i64) as u64;
7342                        if target >= text_addr && target < text_end {
7343                            found.insert(target);
7344                        }
7345                    }
7346                }
7347            }
7348
7349            if matches!(arch, rsleigh_api::Architecture::MIPS32) {
7350                // MIPS JAL (Jump And Link): opcode 000011 imm26
7351                // Target: (PC & 0xF0000000) | (imm26 << 2)
7352                // Endianness comes from the ELF header — both BE (mips)
7353                // and LE (mipsel) are common in IoT samples.
7354                for i in (0..text_bytes.len().saturating_sub(3)).step_by(4) {
7355                    let word = read_u32_elf(&text_bytes[i..i + 4]);
7356                    if (word >> 26) == 3 {
7357                        // JAL opcode
7358                        let imm26 = word & 0x03FFFFFF;
7359                        let target = ((text_addr + i as u64) & 0xF0000000) | ((imm26 as u64) << 2);
7360                        if target >= text_addr && target < text_end && target % 4 == 0 {
7361                            found.insert(target);
7362                        }
7363                    }
7364                }
7365
7366                // BAL (Branch And Link) / BGEZAL: opcode=000001 rt=10001 imm16
7367                for i in (0..text_bytes.len().saturating_sub(3)).step_by(4) {
7368                    let word = read_u32_elf(&text_bytes[i..i + 4]);
7369                    let opcode = word >> 26;
7370                    let rt = (word >> 16) & 0x1F;
7371                    if opcode == 1 && rt == 17 {
7372                        // BGEZAL/BAL
7373                        let imm16 = (word & 0xFFFF) as i16;
7374                        let offset = (imm16 as i64) << 2;
7375                        let pc = text_addr + i as u64 + 4; // delay slot: PC+4
7376                        let target = (pc as i64 + offset) as u64;
7377                        if target >= text_addr && target < text_end && target % 4 == 0 {
7378                            found.insert(target);
7379                        }
7380                    }
7381                }
7382            }
7383        }
7384
7385        // 5. Decoder-based CALL target discovery with indirect call resolution.
7386        // Decode from known function starts, track register values via LEA/MOV,
7387        // and resolve both direct CALL 0xNNNN and indirect CALL RAX/CALL [RIP+N].
7388        {
7389            let mut dec = rsleigh_api::Decoder::new(arch);
7390            let mut new_targets = BTreeSet::new();
7391            let max_seeds = 2000;
7392
7393            // Helper: read a pointer from a virtual address in the binary
7394            let read_ptr = |va: u64| -> Option<u64> {
7395                let off = segs.iter().find_map(|(sva, sz, fo)| {
7396                    if va >= *sva && va < sva + sz {
7397                        Some((fo + (va - sva)) as usize)
7398                    } else {
7399                        None
7400                    }
7401                })?;
7402                if off + ptr_size <= data.len() {
7403                    Some(read_ptr_elf(&data[off..]))
7404                } else {
7405                    None
7406                }
7407            };
7408
7409            // Also scan .text raw bytes for CALL [RIP+disp32] (FF 15 XX XX XX XX)
7410            // These are indirect calls through GOT — the GOT entry may contain
7411            // a resolved function address (for statically linked or pre-resolved).
7412            if text_fo + text_size as usize <= data.len() {
7413                let text_bytes = &data[text_fo..text_fo + text_size as usize];
7414                for i in 0..text_bytes.len().saturating_sub(6) {
7415                    if text_bytes[i] == 0xFF && text_bytes[i + 1] == 0x15 {
7416                        // CALL [RIP+disp32]
7417                        let disp = i32::from_le_bytes(
7418                            text_bytes[i + 2..i + 6].try_into().unwrap_or([0; 4]),
7419                        );
7420                        let got_va = (text_addr as i64 + i as i64 + 6 + disp as i64) as u64;
7421                        if let Some(target) = read_ptr(got_va) {
7422                            if target >= text_addr && target < text_end {
7423                                new_targets.insert(target);
7424                            }
7425                        }
7426                    }
7427                    // JMP [RIP+disp32] (FF 25 XX XX XX XX) — PLT-style indirect jump
7428                    if text_bytes[i] == 0xFF && text_bytes[i + 1] == 0x25 {
7429                        let disp = i32::from_le_bytes(
7430                            text_bytes[i + 2..i + 6].try_into().unwrap_or([0; 4]),
7431                        );
7432                        let got_va = (text_addr as i64 + i as i64 + 6 + disp as i64) as u64;
7433                        if let Some(target) = read_ptr(got_va) {
7434                            if target >= text_addr && target < text_end {
7435                                new_targets.insert(target);
7436                            }
7437                        }
7438                    }
7439                }
7440            }
7441            for t in &new_targets {
7442                found.insert(*t);
7443            }
7444            new_targets.clear();
7445
7446            // Decoder-based discovery with register tracking
7447            for _round in 0..2 {
7448                let start_count = found.len();
7449                let seeds: Vec<u64> = found
7450                    .iter()
7451                    .filter(|a| **a >= text_addr && **a < text_end)
7452                    .take(max_seeds)
7453                    .copied()
7454                    .collect();
7455                for func_addr in seeds {
7456                    let off = segs.iter().find_map(|(va, sz, fo)| {
7457                        if func_addr >= *va && func_addr < va + sz {
7458                            Some((fo + (func_addr - va)) as usize)
7459                        } else {
7460                            None
7461                        }
7462                    });
7463                    let Some(off) = off else { continue };
7464                    let max = 4096.min(data.len().saturating_sub(off));
7465                    if max < 2 {
7466                        continue;
7467                    }
7468                    let bytes = &data[off..off + max];
7469
7470                    // Mini register tracker: maps register name → known address value
7471                    let mut reg_vals: std::collections::HashMap<String, u64> =
7472                        std::collections::HashMap::new();
7473                    let mut pos = 0;
7474                    for _ in 0..500 {
7475                        if pos + 1 >= bytes.len() {
7476                            break;
7477                        }
7478                        if let Ok(inst) = dec.decode(&bytes[pos..], func_addr + pos as u64) {
7479                            let sz = inst.len as usize;
7480                            if sz == 0 {
7481                                break;
7482                            }
7483                            let dis = &inst.disassembly;
7484                            let _inst_addr = func_addr + pos as u64;
7485
7486                            // Track LEA reg, [RIP+disp] → reg = computed address
7487                            if dis.starts_with("LEA ") {
7488                                let parts: Vec<&str> =
7489                                    dis.splitn(3, |c: char| c == ',' || c == ' ').collect();
7490                                if parts.len() >= 3 {
7491                                    let dest = parts[1].trim().trim_end_matches(',');
7492                                    let src = parts[2].trim();
7493                                    // LEA with immediate address: "LEA RAX,0xNNNN" or "LEA RAX,[0xNNNN]"
7494                                    let addr_str =
7495                                        src.trim_start_matches('[').trim_end_matches(']');
7496                                    if let Some(hex) = addr_str.strip_prefix("0x") {
7497                                        if let Ok(addr) = u64::from_str_radix(hex, 16) {
7498                                            reg_vals.insert(dest.to_string(), addr);
7499                                        }
7500                                    }
7501                                }
7502                            }
7503
7504                            // Track MOV reg, imm → reg = constant
7505                            if dis.starts_with("MOV ") && !dis.contains('[') {
7506                                let parts: Vec<&str> =
7507                                    dis.splitn(3, |c: char| c == ',' || c == ' ').collect();
7508                                if parts.len() >= 3 {
7509                                    let dest = parts[1].trim().trim_end_matches(',');
7510                                    let src = parts[2].trim();
7511                                    if let Some(hex) = src.strip_prefix("0x") {
7512                                        if let Ok(val) = u64::from_str_radix(hex, 16) {
7513                                            reg_vals.insert(dest.to_string(), val);
7514                                        }
7515                                    }
7516                                }
7517                            }
7518
7519                            // Collect CALL targets — both direct and indirect
7520                            if dis.starts_with("CALL ") {
7521                                let target_part = &dis[5..];
7522                                if let Some(hex) = target_part.trim().strip_prefix("0x") {
7523                                    // Direct CALL 0xNNNN
7524                                    if let Ok(target) = u64::from_str_radix(hex, 16) {
7525                                        if target >= text_addr && target < text_end {
7526                                            new_targets.insert(target);
7527                                        }
7528                                    }
7529                                } else if target_part.contains('[') {
7530                                    // Indirect CALL [addr] — try to resolve via P-code ops
7531                                    // Parse: "CALL dword ptr [0xNNNN]" or "CALL qword ptr [RIP + 0xNN]"
7532                                    let bracket_content = target_part
7533                                        .split('[')
7534                                        .nth(1)
7535                                        .unwrap_or("")
7536                                        .split(']')
7537                                        .next()
7538                                        .unwrap_or("");
7539                                    if let Some(hex) = bracket_content.strip_prefix("0x") {
7540                                        if let Ok(mem_addr) = u64::from_str_radix(hex, 16) {
7541                                            if let Some(target) = read_ptr(mem_addr) {
7542                                                if target >= text_addr && target < text_end {
7543                                                    new_targets.insert(target);
7544                                                }
7545                                            }
7546                                        }
7547                                    }
7548                                } else {
7549                                    // Indirect CALL REG — resolve from tracked register value
7550                                    let reg = target_part.trim();
7551                                    if let Some(&val) = reg_vals.get(reg) {
7552                                        if val >= text_addr && val < text_end {
7553                                            new_targets.insert(val);
7554                                        }
7555                                    }
7556                                }
7557                            }
7558
7559                            // MIPS: collect JAL targets from decoded instructions
7560                            // MIPS JAL disassembles as "jal 0xNNNNNNNN"
7561                            if dis.starts_with("jal ") {
7562                                let target_part = &dis[4..];
7563                                if let Some(hex) = target_part.trim().strip_prefix("0x") {
7564                                    if let Ok(target) = u64::from_str_radix(hex, 16) {
7565                                        if target >= text_addr && target < text_end {
7566                                            new_targets.insert(target);
7567                                        }
7568                                    }
7569                                }
7570                            }
7571                            // MIPS: "bal 0xNNNN" (branch and link)
7572                            if dis.starts_with("bal ")
7573                                || dis.starts_with("bgezal ")
7574                                || dis.starts_with("bltzal ")
7575                            {
7576                                let target_part = dis.split_whitespace().last().unwrap_or("");
7577                                if let Some(hex) = target_part.strip_prefix("0x") {
7578                                    if let Ok(target) = u64::from_str_radix(hex, 16) {
7579                                        if target >= text_addr && target < text_end {
7580                                            new_targets.insert(target);
7581                                        }
7582                                    }
7583                                }
7584                            }
7585
7586                            // Invalidate destination register on any other write
7587                            // (simplistic: CALL clobbers RAX, other writes clobber dest)
7588                            if dis.starts_with("CALL ") {
7589                                reg_vals.remove("RAX");
7590                                reg_vals.remove("RCX");
7591                                reg_vals.remove("RDX");
7592                                reg_vals.remove("RSI");
7593                                reg_vals.remove("RDI");
7594                                reg_vals.remove("R8");
7595                                reg_vals.remove("R9");
7596                                reg_vals.remove("R10");
7597                                reg_vals.remove("R11");
7598                            }
7599
7600                            // Terminators: x86 RET/HLT, MIPS JR RA (jr ra)
7601                            if dis.starts_with("RET") || dis.starts_with("HLT") || dis == "jr ra" {
7602                                break;
7603                            }
7604                            pos += sz;
7605                        } else {
7606                            break;
7607                        }
7608                    }
7609                }
7610                for t in &new_targets {
7611                    found.insert(*t);
7612                }
7613                new_targets.clear();
7614                if found.len() == start_count {
7615                    break;
7616                }
7617            }
7618        }
7619
7620        // 5b. Parse .eh_frame_hdr for function addresses.
7621        // The .eh_frame_hdr contains a sorted table of (PC, FDE) pairs — every function
7622        // with exception handling or unwind info has an entry here. This is authoritative.
7623        for sh in &elf.section_headers {
7624            let name = elf.shdr_strtab.get_at(sh.sh_name).unwrap_or("");
7625            if name != ".eh_frame_hdr" {
7626                continue;
7627            }
7628            let fo = sh.sh_offset as usize;
7629            let hdr_addr = sh.sh_addr;
7630            if fo + 12 > data.len() {
7631                break;
7632            }
7633            let version = data[fo];
7634            if version != 1 {
7635                break;
7636            }
7637            let fde_count_enc = data[fo + 2];
7638            let table_enc = data[fo + 3];
7639            // Read FDE count (offset 8, encoding determines size)
7640            let fde_count = match fde_count_enc {
7641                0x03 => read_i32_elf(&data[fo + 8..]) as usize,
7642                _ => read_u32_elf(&data[fo + 8..]) as usize,
7643            };
7644            if fde_count == 0 || fde_count > 100_000 {
7645                break;
7646            }
7647            let table_start = fo + 12;
7648            // Table encoding 0x3b = DW_EH_PE_datarel | DW_EH_PE_sdata4 (most common)
7649            if table_enc == 0x3b {
7650                for i in 0..fde_count {
7651                    let entry_off = table_start + i * 8;
7652                    if entry_off + 4 > data.len() {
7653                        break;
7654                    }
7655                    let pc_rel = read_i32_elf(&data[entry_off..]);
7656                    let pc = (hdr_addr as i64 + pc_rel as i64) as u64;
7657                    if pc > 0 && pc < text_end + 0x10000 {
7658                        found.insert(pc);
7659                    }
7660                }
7661            }
7662        }
7663
7664        // 5b2. Parse .eh_frame directly for FDE initial_location addresses.
7665        // Complements .eh_frame_hdr: catches FDEs not in the index and works when
7666        // .eh_frame_hdr is missing. Each FDE has a PC-relative initial_location.
7667        for sh in &elf.section_headers {
7668            let name = elf.shdr_strtab.get_at(sh.sh_name).unwrap_or("");
7669            if name != ".eh_frame" {
7670                continue;
7671            }
7672            let ef_addr = sh.sh_addr;
7673            let ef_off = sh.sh_offset as usize;
7674            let ef_size = sh.sh_size as usize;
7675            if ef_off + ef_size > data.len() {
7676                break;
7677            }
7678
7679            let mut pos = 0;
7680            while pos + 8 < ef_size {
7681                let fo = ef_off + pos;
7682                let length = read_u32_elf(&data[fo..]) as usize;
7683                if length == 0 {
7684                    break;
7685                } // terminator
7686                if length > ef_size - pos {
7687                    break;
7688                } // corrupt
7689                let record_start = pos + 4;
7690                let cie_id = read_u32_elf(&data[fo + 4..]);
7691
7692                if cie_id != 0 {
7693                    // FDE: initial_location is at offset 8 from record start,
7694                    // encoded as sdata4 PC-relative (most common for gcc/clang)
7695                    let iloc_off = fo + 8;
7696                    if iloc_off + 4 <= data.len() {
7697                        let iloc_rel = read_i32_elf(&data[iloc_off..]);
7698                        let iloc =
7699                            (ef_addr as i64 + (iloc_off - ef_off) as i64 + iloc_rel as i64) as u64;
7700                        if iloc > 0 && iloc < text_end + 0x10000 {
7701                            found.insert(iloc);
7702                        }
7703                    }
7704                }
7705                pos = record_start + length;
7706            }
7707        }
7708
7709        // 5b3. C++ RTTI vtable chain walking.
7710        // Vtable layout: [offset_to_top(8)] [typeinfo_ptr(8)] [vfunc0(8)] [vfunc1(8)] ...
7711        // Identify vtables by: offset_to_top is 0 or small, typeinfo_ptr points to
7712        // .data.rel.ro/.rodata, and next entries point into .text.
7713        for sh in &elf.section_headers {
7714            let name = elf.shdr_strtab.get_at(sh.sh_name).unwrap_or("");
7715            if name != ".data.rel.ro" {
7716                continue;
7717            }
7718            let _sec_addr = sh.sh_addr;
7719            let sec_off = sh.sh_offset as usize;
7720            let sec_size = sh.sh_size as usize;
7721            if sec_off + sec_size > data.len() || sec_size < 24 {
7722                continue;
7723            }
7724
7725            // Collect all data section address ranges for typeinfo pointer validation
7726            let data_sections: Vec<(u64, u64)> = elf
7727                .section_headers
7728                .iter()
7729                .filter(|s| {
7730                    let n = elf.shdr_strtab.get_at(s.sh_name).unwrap_or("");
7731                    matches!(n, ".data.rel.ro" | ".rodata" | ".data")
7732                })
7733                .map(|s| (s.sh_addr, s.sh_addr + s.sh_size))
7734                .collect();
7735            let in_data = |addr: u64| -> bool {
7736                data_sections
7737                    .iter()
7738                    .any(|(start, end)| addr >= *start && addr < *end)
7739            };
7740
7741            let mut i = 0;
7742            while i + 3 * ptr_size <= sec_size {
7743                let offset_to_top = if is_32bit {
7744                    read_i32_elf(&data[sec_off + i..]) as i64
7745                } else {
7746                    read_i64_elf(&data[sec_off + i..])
7747                };
7748                let typeinfo_ptr = read_ptr_elf(&data[sec_off + i + ptr_size..]);
7749                let first_entry = read_ptr_elf(&data[sec_off + i + 2 * ptr_size..]);
7750
7751                // Vtable heuristic: offset_to_top is 0 or small, typeinfo points to data,
7752                // first entry points to executable code
7753                if offset_to_top.unsigned_abs() <= 1024
7754                    && in_data(typeinfo_ptr)
7755                    && first_entry >= text_addr
7756                    && first_entry < text_end
7757                {
7758                    // Walk virtual function entries
7759                    let mut j = 2 * ptr_size;
7760                    while i + j + ptr_size <= sec_size {
7761                        let vfunc = read_ptr_elf(&data[sec_off + i + j..]);
7762                        if vfunc >= text_addr && vfunc < text_end {
7763                            found.insert(vfunc);
7764                            j += ptr_size;
7765                        } else {
7766                            break;
7767                        }
7768                    }
7769                    i += j; // skip past vtable
7770                } else {
7771                    i += ptr_size;
7772                }
7773            }
7774        }
7775
7776        // 5c. Full data section pointer scan — find ALL 8-byte values pointing into executable code
7777        // Covers vtables, function pointer arrays, switch jump tables, C++ RTTI
7778        {
7779            let _all_exec_start = elf
7780                .section_headers
7781                .iter()
7782                .filter(|sh| sh.sh_flags & 0x4 != 0 && sh.sh_addr > 0)
7783                .map(|sh| sh.sh_addr)
7784                .min()
7785                .unwrap_or(text_addr);
7786            let _all_exec_end = elf
7787                .section_headers
7788                .iter()
7789                .filter(|sh| sh.sh_flags & 0x4 != 0 && sh.sh_addr > 0)
7790                .map(|sh| sh.sh_addr + sh.sh_size)
7791                .max()
7792                .unwrap_or(text_end);
7793
7794            for sh in &elf.section_headers {
7795                let name = elf.shdr_strtab.get_at(sh.sh_name).unwrap_or("");
7796                if !matches!(name, ".rodata" | ".data.rel.ro") {
7797                    continue;
7798                }
7799                let fo = sh.sh_offset as usize;
7800                let sz = sh.sh_size as usize;
7801                if fo + sz > data.len() || sz < ptr_size {
7802                    continue;
7803                }
7804                let is_vtable_section = name == ".data.rel.ro";
7805                let ps = ptr_size; // local alias
7806                for i in (0..sz.saturating_sub(ps - 1)).step_by(ps) {
7807                    let ptr = read_ptr_elf(&data[fo + i..]);
7808                    if ptr >= text_addr && ptr < text_end {
7809                        if is_vtable_section {
7810                            // .data.rel.ro: vtable entries are always function pointers
7811                            found.insert(ptr);
7812                        } else if matches!(arch, rsleigh_api::Architecture::MIPS32) {
7813                            // MIPS: validate with prologue check
7814                            let target_idx = (ptr - text_addr) as usize;
7815                            if text_fo + target_idx + 4 < data.len() {
7816                                let word = read_u32_elf(
7817                                    &data[text_fo + target_idx..text_fo + target_idx + 4],
7818                                );
7819                                let strong = (word & 0xFFFF0000) == 0x27BD0000  // addiu sp, sp, -N
7820                                    || (word & 0xFFFF0000) == 0x3C1C0000         // lui gp, N
7821                                    || (word & 0xFFFF0000) == 0xAFBF0000; // sw ra, N(sp)
7822                                if strong {
7823                                    found.insert(ptr);
7824                                } else {
7825                                    let mut run = 0;
7826                                    let psi = ps as i64;
7827                                    for k in [-psi, psi, 2 * psi].iter() {
7828                                        let neighbor = i as i64 + k;
7829                                        if neighbor >= 0 && (neighbor as usize) + ps <= sz {
7830                                            let np = read_ptr_elf(&data[fo + neighbor as usize..]);
7831                                            if np >= text_addr && np < text_end {
7832                                                run += 1;
7833                                            }
7834                                        }
7835                                    }
7836                                    if run >= 2 {
7837                                        found.insert(ptr);
7838                                    }
7839                                }
7840                            }
7841                        } else {
7842                            // x86/ARM: existing prologue checks
7843                            let target_idx = (ptr - text_addr) as usize;
7844                            if text_fo + target_idx + 4 < data.len() {
7845                                let b0 = data[text_fo + target_idx];
7846                                let b1 = data[text_fo + target_idx + 1];
7847                                let strong = matches!(
7848                                    (b0, b1),
7849                                    (0x55, 0x48)
7850                                        | (0x55, 0x53)
7851                                        | (0x53, 0x48)
7852                                        | (0x53, 0x55)
7853                                        | (0x41, 0x54)
7854                                        | (0x41, 0x55)
7855                                        | (0x41, 0x56)
7856                                        | (0x41, 0x57)
7857                                        | (0x48, 0x83)
7858                                        | (0x48, 0x81)
7859                                        | (0xF3, 0x0F)
7860                                        | (0x55, 0x41)
7861                                );
7862                                if strong {
7863                                    found.insert(ptr);
7864                                } else {
7865                                    let mut run = 0;
7866                                    let psi = ps as i64;
7867                                    for k in [-psi, psi, 2 * psi].iter() {
7868                                        let neighbor = i as i64 + k;
7869                                        if neighbor >= 0 && (neighbor as usize) + ps <= sz {
7870                                            let np = read_ptr_elf(&data[fo + neighbor as usize..]);
7871                                            if np >= text_addr && np < text_end {
7872                                                run += 1;
7873                                            }
7874                                        }
7875                                    }
7876                                    if run >= 2 {
7877                                        found.insert(ptr);
7878                                    }
7879                                }
7880                            }
7881                        }
7882                    }
7883                }
7884            }
7885        }
7886
7887        // 5d. E9 JMP rel32 pass — add tail call thunks at function boundaries.
7888        // Only add when JMP is preceded by strict terminators (RET/INT3/NOP padding).
7889        // Do NOT include 0xFF — it's the last byte of many multi-byte instructions.
7890        if text_fo + text_size as usize <= data.len() {
7891            let text_bytes = &data[text_fo..text_fo + text_size as usize];
7892            for i in 0..text_bytes.len().saturating_sub(5) {
7893                if text_bytes[i] == 0xE9 {
7894                    let rel =
7895                        i32::from_le_bytes(text_bytes[i + 1..i + 5].try_into().unwrap_or([0; 4]));
7896                    let target = (text_addr as i64 + i as i64 + 5 + rel as i64) as u64;
7897                    if target < text_addr || target >= text_end {
7898                        continue;
7899                    }
7900                    let at_boundary =
7901                        i == 0 || matches!(text_bytes[i - 1], 0xC3 | 0x90 | 0xCC | 0x00);
7902                    if at_boundary {
7903                        found.insert(text_addr + i as u64);
7904                    }
7905                }
7906            }
7907        }
7908
7909        // 6. Prologue pattern scanning in .text
7910        if text_fo + text_size as usize <= data.len() {
7911            let text_bytes = &data[text_fo..text_fo + text_size as usize];
7912            let is_boundary = |i: usize| -> bool {
7913                i == 0
7914                    || matches!(
7915                        text_bytes[i - 1],
7916                        0xC3 | 0x90 | 0xCC | 0x00 | 0xC2 | 0xCB | 0xCA
7917                    )
7918            };
7919            // Also accept NOP padding sequences (66 66 2e 0f 1f etc.)
7920            let is_boundary_or_nop = |i: usize| -> bool {
7921                if is_boundary(i) {
7922                    return true;
7923                }
7924                // Multi-byte NOP: 66 90, 0f 1f XX, 66 2e 0f 1f
7925                if i >= 2 && text_bytes[i - 1] == 0x90 && text_bytes[i - 2] == 0x66 {
7926                    return true;
7927                }
7928                if i >= 1 && text_bytes[i - 1] == 0x90 {
7929                    return true;
7930                }
7931                false
7932            };
7933
7934            for i in 0..text_bytes.len().saturating_sub(4) {
7935                let addr = text_addr + i as u64;
7936                if found.contains(&addr) {
7937                    continue;
7938                }
7939
7940                let b0 = text_bytes[i];
7941                let b1 = if i + 1 < text_bytes.len() {
7942                    text_bytes[i + 1]
7943                } else {
7944                    0
7945                };
7946                let b2 = if i + 2 < text_bytes.len() {
7947                    text_bytes[i + 2]
7948                } else {
7949                    0
7950                };
7951                let b3 = if i + 3 < text_bytes.len() {
7952                    text_bytes[i + 3]
7953                } else {
7954                    0
7955                };
7956
7957                let matched = match (b0, b1, b2, b3) {
7958                    // push rbp; mov rbp, rsp (55 48 89 e5)
7959                    (0x55, 0x48, 0x89, 0xe5) => true,
7960                    // push rbp; mov rbp, rsp (55 48 8b ec)
7961                    (0x55, 0x48, 0x8b, 0xec) => true,
7962                    // push rbx; sub rsp (53 48 83 ec)
7963                    (0x53, 0x48, 0x83, 0xec) => true,
7964                    // push rbx; push rbp (53 55 ..) — C++ common
7965                    (0x53, 0x55, _, _) => true,
7966                    // push r12; push rbp (41 54 55 ..)
7967                    (0x41, 0x54, 0x55, _) => true,
7968                    // push r12; push rbx (41 54 53 ..)
7969                    (0x41, 0x54, 0x53, _) => true,
7970                    // push r13; push r12 (41 55 41 54)
7971                    (0x41, 0x55, 0x41, 0x54) => true,
7972                    // push r14; push r13 (41 56 41 55)
7973                    (0x41, 0x56, 0x41, 0x55) => true,
7974                    // push r15; push r14 (41 57 41 56)
7975                    (0x41, 0x57, 0x41, 0x56) => true,
7976                    // sub rsp, imm8 (48 83 ec NN) — leaf function
7977                    (0x48, 0x83, 0xEC, _) => true,
7978                    // sub rsp, imm32 (48 81 ec NN NN NN NN) — large stack frame
7979                    (0x48, 0x81, 0xEC, _) => true,
7980                    // push rbp; push rbx (55 53 ..)
7981                    (0x55, 0x53, _, _) => true,
7982                    // push rbp; push r12 (55 41 54 ..)
7983                    (0x55, 0x41, 0x54, _) => true,
7984                    // push rbp; sub rsp (55 48 83 ec) — already covered by push rbp patterns
7985                    // mov rdi, rsi or similar arg setup as first instruction (rare standalone)
7986                    _ => false,
7987                };
7988
7989                if matched && is_boundary_or_nop(i) {
7990                    found.insert(addr);
7991                }
7992
7993                // endbr64 (f3 0f 1e fa) — CET indirect branch target
7994                // Only count as function if preceded by a function terminator or NOP padding.
7995                // Switch case targets also have endbr64 but are NOT function entries.
7996                if b0 == 0xF3 && b1 == 0x0F && b2 == 0x1E && b3 == 0xFA {
7997                    if is_boundary_or_nop(i) {
7998                        found.insert(addr);
7999                    }
8000                }
8001            }
8002        }
8003
8004        // 6a. MIPS prologue pattern scanning in .text
8005        if matches!(arch, rsleigh_api::Architecture::MIPS32) {
8006            if text_fo + text_size as usize <= data.len() {
8007                let text_bytes = &data[text_fo..text_fo + text_size as usize];
8008
8009                // MIPS function boundary detection: JR RA (0x03E00008) or
8010                // JR RA in delay slot pair (JR RA + NOP = 03E00008 00000000)
8011                // Word reads use ELF endianness so mipsel works too.
8012                let is_mips_boundary = |i: usize| -> bool {
8013                    if i == 0 {
8014                        return true;
8015                    }
8016                    if i >= 8 {
8017                        let prev2 = read_u32_elf(&text_bytes[i - 8..i - 4]);
8018                        let prev1 = read_u32_elf(&text_bytes[i - 4..i]);
8019                        if prev2 == 0x03E00008 {
8020                            return true;
8021                        }
8022                        if prev1 == 0x03E00008 {
8023                            return true;
8024                        }
8025                    }
8026                    if i >= 4 {
8027                        let prev = read_u32_elf(&text_bytes[i - 4..i]);
8028                        if prev == 0x00000000 {
8029                            return true;
8030                        }
8031                        if prev == 0x03E00008 {
8032                            return true;
8033                        }
8034                    }
8035                    false
8036                };
8037
8038                for i in (0..text_bytes.len().saturating_sub(7)).step_by(4) {
8039                    let addr = text_addr + i as u64;
8040                    if found.contains(&addr) {
8041                        continue;
8042                    }
8043
8044                    let word = read_u32_elf(&text_bytes[i..i + 4]);
8045                    let next_word = read_u32_elf(&text_bytes[i + 4..i + 8]);
8046
8047                    // Pattern 1: addiu sp, sp, -N (0x27BDxxxx where xxxx is negative = high bit set)
8048                    // This is the most common MIPS function prologue
8049                    let is_addiu_sp = (word & 0xFFFF0000) == 0x27BD0000 && (word & 0x8000) != 0;
8050
8051                    if is_addiu_sp {
8052                        // Strong: addiu sp followed by sw ra (save return address)
8053                        let next_is_sw_ra = (next_word & 0xFFFF0000) == 0xAFBF0000;
8054                        // Also strong: addiu sp followed by sw s8/fp
8055                        let next_is_sw_fp = (next_word & 0xFFFF0000) == 0xAFBE0000;
8056                        // Also accept: addiu sp followed by lui gp (PIC prologue)
8057                        let next_is_lui_gp = (next_word & 0xFFFF0000) == 0x3C1C0000;
8058
8059                        if next_is_sw_ra || next_is_sw_fp || next_is_lui_gp {
8060                            // Strong prologue — always add
8061                            found.insert(addr);
8062                        } else if is_mips_boundary(i) {
8063                            // Weaker prologue but at a function boundary
8064                            found.insert(addr);
8065                        }
8066                    }
8067
8068                    // Pattern 2: lui gp, N followed by addiu gp (PIC code, GP setup)
8069                    // Some functions start with GP setup before stack allocation
8070                    let is_lui_gp = (word & 0xFFFF0000) == 0x3C1C0000;
8071                    if is_lui_gp && is_mips_boundary(i) {
8072                        let next_is_addiu_gp = (next_word & 0xFFFF0000) == 0x279C0000;
8073                        if next_is_addiu_gp {
8074                            found.insert(addr);
8075                        }
8076                    }
8077                }
8078            }
8079        }
8080
8081        // 6b. Scan for endbr64 in all executable sections (not just .text)
8082        // This catches .plt.sec and .plt.got entries
8083        for sh in &elf.section_headers {
8084            if sh.sh_flags & 0x4 == 0 {
8085                continue;
8086            } // not executable
8087            let name = elf.shdr_strtab.get_at(sh.sh_name).unwrap_or("");
8088            if name == ".text" {
8089                continue;
8090            } // already scanned
8091            let fo = sh.sh_offset as usize;
8092            let sz = sh.sh_size as usize;
8093            if fo + sz > data.len() {
8094                continue;
8095            }
8096            let sec_bytes = &data[fo..fo + sz];
8097            for i in (0..sz.saturating_sub(4)).step_by(1) {
8098                if sec_bytes[i] == 0xF3
8099                    && sec_bytes[i + 1] == 0x0F
8100                    && sec_bytes[i + 2] == 0x1E
8101                    && sec_bytes[i + 3] == 0xFA
8102                {
8103                    found.insert(sh.sh_addr + i as u64);
8104                }
8105            }
8106        }
8107
8108        // 7. Gap analysis: scan gaps between known functions for valid prologues.
8109        if text_fo + text_size as usize <= data.len() {
8110            let text_bytes = &data[text_fo..text_fo + text_size as usize];
8111            let mut sorted_addrs: Vec<u64> = found
8112                .iter()
8113                .filter(|a| **a >= text_addr && **a < text_end)
8114                .copied()
8115                .collect();
8116            sorted_addrs.sort();
8117
8118            for window in sorted_addrs.windows(2) {
8119                let gap_start = window[0];
8120                let gap_end = window[1];
8121                let gap_size = gap_end - gap_start;
8122                // Only analyze gaps > 16 bytes (room for a real function)
8123                if gap_size < 32 || gap_size > 2048 {
8124                    continue;
8125                }
8126                // Scan inside the gap for function prologues after terminators
8127                let start_idx = (gap_start - text_addr) as usize;
8128                let end_idx = (gap_end - text_addr) as usize;
8129                if end_idx > text_bytes.len() {
8130                    continue;
8131                }
8132                let mut i = start_idx;
8133                while i + 4 < end_idx {
8134                    let b = text_bytes[i];
8135                    // Look for RET (C3) or unconditional JMP (E9/EB/FF) followed by valid code
8136                    if matches!(b, 0xC3 | 0xCC) {
8137                        // Skip NOP/INT3/alignment padding (require 2+ padding bytes)
8138                        let mut j = i + 1;
8139                        while j < end_idx && matches!(text_bytes[j], 0x90 | 0xCC | 0x00) {
8140                            j += 1;
8141                        }
8142                        // Also skip multi-byte NOPs: 66 90, 0f 1f XX, 66 2e 0f 1f
8143                        while j + 1 < end_idx && text_bytes[j] == 0x66 && text_bytes[j + 1] == 0x90
8144                        {
8145                            j += 2;
8146                        }
8147                        while j + 2 < end_idx && text_bytes[j] == 0x0F && text_bytes[j + 1] == 0x1F
8148                        {
8149                            j += 3;
8150                        }
8151                        if j < end_idx && j >= i + 2 {
8152                            // require 2+ padding bytes
8153                            let candidate = text_addr + j as u64;
8154                            if !found.contains(&candidate) {
8155                                // Verify: must start with a strong prologue pattern
8156                                let fb = text_bytes[j];
8157                                let fb1 = if j + 1 < end_idx {
8158                                    text_bytes[j + 1]
8159                                } else {
8160                                    0
8161                                };
8162                                let valid_start = matches!(
8163                                    (fb, fb1),
8164                                    (0x55, 0x48) | (0x55, 0x53) | (0x55, 0x41) | // push rbp; ...
8165                                    (0x53, 0x48) | (0x53, 0x55) |                 // push rbx; ...
8166                                    (0x41, 0x54) | (0x41, 0x55) | (0x41, 0x56) | (0x41, 0x57) | // push r12-r15
8167                                    (0x48, 0x83) | (0x48, 0x81) |                 // sub rsp
8168                                    (0xF3, 0x0F) // endbr64
8169                                );
8170                                if valid_start {
8171                                    found.insert(candidate);
8172                                }
8173                            }
8174                            i = j;
8175                            continue;
8176                        }
8177                    }
8178                    i += 1;
8179                }
8180            }
8181        }
8182
8183        // 7b. MIPS gap analysis: scan gaps between known functions for prologues after JR RA.
8184        if matches!(arch, rsleigh_api::Architecture::MIPS32) {
8185            if text_fo + text_size as usize <= data.len() {
8186                let text_bytes = &data[text_fo..text_fo + text_size as usize];
8187                let mut sorted_addrs: Vec<u64> = found
8188                    .iter()
8189                    .filter(|a| **a >= text_addr && **a < text_end)
8190                    .copied()
8191                    .collect();
8192                sorted_addrs.sort();
8193
8194                for window in sorted_addrs.windows(2) {
8195                    let gap_start = window[0];
8196                    let gap_end = window[1];
8197                    let gap_size = gap_end - gap_start;
8198                    if gap_size < 16 || gap_size > 4096 {
8199                        continue;
8200                    }
8201                    let start_idx = (gap_start - text_addr) as usize;
8202                    let end_idx = (gap_end - text_addr) as usize;
8203                    if end_idx + 4 > text_bytes.len() {
8204                        continue;
8205                    }
8206
8207                    // Scan for JR RA (0x03E00008) + delay slot, then prologue.
8208                    // ELF endianness drives word reads (BE 'mips' vs LE 'mipsel').
8209                    let mut i = start_idx;
8210                    while i + 12 <= end_idx {
8211                        let word = read_u32_elf(&text_bytes[i..i + 4]);
8212                        if word == 0x03E00008 {
8213                            // JR RA
8214                            // Skip delay slot + any NOP padding
8215                            let mut j = i + 8; // past JR RA + delay slot
8216                            while j + 4 <= end_idx {
8217                                let w = read_u32_elf(&text_bytes[j..j + 4]);
8218                                if w == 0x00000000 {
8219                                    j += 4;
8220                                } else {
8221                                    break;
8222                                }
8223                            }
8224                            if j + 8 <= end_idx && j % 4 == 0 {
8225                                let candidate_word = read_u32_elf(&text_bytes[j..j + 4]);
8226                                let is_prologue = (candidate_word & 0xFFFF0000) == 0x27BD0000
8227                                    && (candidate_word & 0x8000) != 0
8228                                    || (candidate_word & 0xFFFF0000) == 0x3C1C0000;
8229                                if is_prologue {
8230                                    let candidate_addr = text_addr + j as u64;
8231                                    if !found.contains(&candidate_addr) {
8232                                        found.insert(candidate_addr);
8233                                    }
8234                                }
8235                            }
8236                            i = j;
8237                        } else {
8238                            i += 4;
8239                        }
8240                    }
8241                }
8242            }
8243        }
8244
8245        // Step 5 (decoder-based CALL discovery) already covers recursive descent.
8246        // No separate pass needed.
8247    }
8248
8249    // Filter: remove addresses in PLT range that aren't PLT entries
8250    // and sort results
8251    let mut result: Vec<(u64, String)> = found
8252        .into_iter()
8253        .map(|addr| {
8254            // Try to resolve PLT names from dynamic relocations
8255            let plt_name = resolve_plt_name(elf, addr);
8256            let name = plt_name.unwrap_or_else(|| format!("FUN_{:08x}", addr));
8257            (addr, name)
8258        })
8259        .collect();
8260    result.sort_by_key(|(addr, _)| *addr);
8261    result
8262}
8263
8264/// Try to resolve a PLT entry address to its import name via .rela.plt relocations.
8265fn resolve_plt_name(elf: &goblin::elf::Elf, addr: u64) -> Option<String> {
8266    // Check if addr is in a PLT section
8267    let in_plt = elf.section_headers.iter().any(|sh| {
8268        let name = elf.shdr_strtab.get_at(sh.sh_name).unwrap_or("");
8269        name.starts_with(".plt") && addr >= sh.sh_addr && addr < sh.sh_addr + sh.sh_size
8270    });
8271    if !in_plt {
8272        return None;
8273    }
8274
8275    // Find which PLT slot this is (by index)
8276    let plt_sec = elf.section_headers.iter().find(|sh| {
8277        let name = elf.shdr_strtab.get_at(sh.sh_name).unwrap_or("");
8278        name == ".plt.sec" || name == ".plt"
8279    })?;
8280    let entry_size = if plt_sec.sh_entsize > 0 {
8281        plt_sec.sh_entsize
8282    } else {
8283        16
8284    };
8285    let plt_name = elf.shdr_strtab.get_at(plt_sec.sh_name).unwrap_or("");
8286    let base = if plt_name == ".plt.sec" {
8287        plt_sec.sh_addr
8288    } else {
8289        plt_sec.sh_addr + entry_size
8290    };
8291    if addr < base {
8292        return None;
8293    }
8294    let idx = ((addr - base) / entry_size) as usize;
8295
8296    // Match against .rela.plt relocations
8297    for rel in &elf.pltrelocs {
8298        // The PLT index corresponds to the relocation index
8299        let sym = &elf.dynsyms.get(rel.r_sym)?;
8300        let name = elf.dynstrtab.get_at(sym.st_name)?;
8301        if !name.is_empty() {
8302            // Count which relocation this is
8303            let rel_idx = elf
8304                .pltrelocs
8305                .iter()
8306                .position(|r| r.r_offset == rel.r_offset)?;
8307            if rel_idx == idx {
8308                return Some(name.to_string());
8309            }
8310        }
8311    }
8312    None
8313}