flintbase 0.3.1

Google / Firebase API key analyzer and APK secret scanner — tests keys against 20+ endpoints and extracts hardcoded credentials from Android apps
use std::path::{Path, PathBuf};
use std::process::Command;

use colored::Colorize;

/// Decompile an APK file using jadx.
///
/// jadx frequently exits with code 1 due to partial decompilation errors — this
/// is normal and does NOT mean the decompilation failed. We check whether output
/// files were actually produced rather than relying on the exit code alone.
///
/// Returns the path to the directory containing decompiled sources.
pub fn decompile_apk(apk_path: &Path, output_dir: &Path, verbose: bool) -> anyhow::Result<PathBuf> {
    std::fs::create_dir_all(output_dir)?;

    let apk_name = apk_path
        .file_name()
        .and_then(|n| n.to_str())
        .unwrap_or("unknown");

    println!(
        "  {} Decompiling {} with jadx...",
        "".cyan(),
        apk_name.bold()
    );

    let output = Command::new("jadx")
        .arg(apk_path)
        .arg("-d")
        .arg(output_dir)
        .arg("--no-debug-info")       // Cleaner output
        .arg("--deobf")               // Attempt deobfuscation
        .arg("--show-bad-code")       // Include code that couldn't fully decompile
        .output()?;

    let exit_code = output.status.code().unwrap_or(-1);
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    // Log verbose output
    if verbose {
        if !stdout.is_empty() {
            println!("  {} jadx stdout:", "verbose".dimmed());
            for line in stdout.lines() {
                println!("    {}", line.dimmed());
            }
        }
        if !stderr.is_empty() {
            println!("  {} jadx stderr:", "verbose".dimmed());
            for line in stderr.lines() {
                println!("    {}", line.dimmed());
            }
        }
        println!("  {} jadx exit code: {}", "verbose".dimmed(), exit_code);
    }

    // Count decompiled files to determine real success
    let file_count = count_files_recursive(output_dir);

    if file_count == 0 {
        // True failure — jadx produced nothing
        let mut err_msg = format!(
            "jadx produced no output files for {}. Exit code: {}",
            apk_name, exit_code
        );

        // Extract useful error info from output
        let error_lines: Vec<&str> = stderr
            .lines()
            .chain(stdout.lines())
            .filter(|l| l.contains("ERROR") || l.contains("error") || l.contains("Exception"))
            .collect();

        if !error_lines.is_empty() {
            err_msg.push_str("\n  jadx errors:");
            for line in error_lines.iter().take(5) {
                err_msg.push_str(&format!("\n    {}", line));
            }
        }

        if !java_available() {
            err_msg.push_str("\n  Note: Java does not appear to be installed (jadx requires it).");
        }

        anyhow::bail!(err_msg);
    }

    // Partial errors are common and OK — report them but don't fail
    let error_count = extract_error_count(&stderr, &stdout);

    if exit_code != 0 && error_count > 0 {
        println!(
            "  {} jadx completed with {} partial error(s) — {} files decompiled successfully",
            "".yellow(),
            error_count,
            file_count
        );
    } else {
        println!(
            "  {} Decompiled {} files into {}",
            "".green(),
            file_count,
            output_dir.display()
        );
    }

    Ok(output_dir.to_path_buf())
}

/// Extract the error count from jadx output.
/// Looks for "finished with errors, count: N" or counts ERROR lines.
fn extract_error_count(stderr: &str, stdout: &str) -> usize {
    let combined = format!("{}\n{}", stderr, stdout);

    // jadx prints "finished with errors, count: N"
    for line in combined.lines() {
        if line.contains("finished with errors, count:") {
            if let Some(count_str) = line.rsplit("count:").next() {
                if let Ok(n) = count_str.trim().parse::<usize>() {
                    return n;
                }
            }
        }
    }

    // Fallback: count ERROR lines
    combined.lines().filter(|l| l.contains("ERROR")).count()
}

fn count_files_recursive(dir: &Path) -> usize {
    let mut count = 0;
    if let Ok(entries) = std::fs::read_dir(dir) {
        for entry in entries.flatten() {
            let path = entry.path();
            if path.is_dir() {
                count += count_files_recursive(&path);
            } else {
                count += 1;
            }
        }
    }
    count
}

fn java_available() -> bool {
    Command::new("which")
        .arg("java")
        .output()
        .map(|o| o.status.success())
        .unwrap_or(false)
}