coraza-sys 3.7.0

Raw FFI bindings to OWASP Coraza WAF
use std::env;
use std::path::{Path, PathBuf};
use std::process::Command;

fn main() {
    let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
    let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());
    let go_dir = manifest_dir.join("go");
    let header_path = manifest_dir.join("headers").join("coraza.h");

    // Build the Go static library
    let _lib_path = build_go_library(&go_dir, &out_dir);

    // Generate Rust bindings from the C header
    generate_bindings(&header_path, &out_dir);

    // Link the static library
    println!("cargo:rustc-link-search=native={}", out_dir.display());
    println!("cargo:rustc-link-lib=static=coraza");

    // Link platform-specific dependencies
    let target = env::var("TARGET").unwrap();
    if target.contains("linux") {
        println!("cargo:rustc-link-lib=dylib=pthread");
        println!("cargo:rustc-link-lib=dylib=dl");
        println!("cargo:rustc-link-lib=dylib=m");
        println!("cargo:rustc-link-lib=dylib=c");
    } else if target.contains("apple") {
        println!("cargo:rustc-link-lib=framework=CoreFoundation");
        println!("cargo:rustc-link-lib=framework=Security");
        println!("cargo:rustc-link-lib=dylib=resolv");
        println!("cargo:rustc-link-lib=dylib=m");
        println!("cargo:rustc-link-lib=dylib=c");
    } else if target.contains("windows") {
        println!("cargo:rustc-link-lib=dylib=ws2_32");
        println!("cargo:rustc-link-lib=dylib=winmm");
        println!("cargo:rustc-link-lib=dylib=advapi32");
        println!("cargo:rustc-link-lib=dylib=ntdll");
        println!("cargo:rustc-link-lib=dylib=bcrypt");
    }

    // Re-run if Go source or header changes
    println!(
        "cargo:rerun-if-changed={}",
        go_dir.join("coraza.go").display()
    );
    println!("cargo:rerun-if-changed={}", go_dir.join("log.go").display());
    println!("cargo:rerun-if-changed={}", go_dir.join("go.mod").display());
    println!("cargo:rerun-if-changed={}", header_path.display());
}

fn build_go_library(go_dir: &Path, out_dir: &Path) -> PathBuf {
    let lib_path = out_dir.join("libcoraza.a");

    // Find go binary
    let go_binary = find_go_binary();

    // Build the Go static library
    let mut cmd = Command::new(&go_binary);
    cmd.arg("build")
        .arg("-buildmode=c-archive")
        .arg("-o")
        .arg(&lib_path)
        .arg("./...");

    cmd.current_dir(go_dir);

    // Set CGO_ENABLED=1 for cross-compilation support
    cmd.env("CGO_ENABLED", "1");

    // Pass through target architecture for cross-compilation
    if let Ok(target) = env::var("TARGET") {
        let (goarch, goos) = parse_target_triple(&target);
        cmd.env("GOARCH", goarch);
        cmd.env("GOOS", goos);
    }

    let output = cmd
        .output()
        .expect("Failed to execute `go build`. Is Go installed and in PATH?");

    if !output.status.success() {
        panic!(
            "Go build failed:\nstdout: {}\nstderr: {}",
            String::from_utf8_lossy(&output.stdout),
            String::from_utf8_lossy(&output.stderr)
        );
    }

    lib_path
}

/// Parse a Rust target triple (e.g., "x86_64-unknown-linux-gnu") into Go GOARCH and GOOS.
fn parse_target_triple(target: &str) -> (&str, &str) {
    let parts: Vec<&str> = target.split('-').collect();

    let goarch = match parts[0] {
        "x86_64" => "amd64",
        "aarch64" => "arm64",
        "i686" | "i386" => "386",
        "armv7" | "arm" => "arm",
        other => other,
    };

    // Find the OS part - skip vendor (e.g., "unknown", "pc")
    let goos = if parts.len() >= 3 {
        // Standard triple: arch-vendor-os
        match parts[2] {
            "linux" => "linux",
            "darwin" => "darwin",
            "windows" => "windows",
            "musl" => "linux",
            other => other,
        }
    } else if parts.len() >= 2 {
        // Short triple: arch-os
        match parts[1] {
            "linux" => "linux",
            "darwin" => "darwin",
            "windows" => "windows",
            "musl" => "linux",
            other => other,
        }
    } else {
        "linux" // default
    };

    (goarch, goos)
}

fn find_go_binary() -> String {
    // Check GO环境变量
    if let Ok(go) = env::var("GO") {
        return go;
    }

    // Try to find go in PATH
    which("go").expect(
        "Go toolchain not found. Please install Go (https://go.dev/dl/) and ensure it is in PATH.",
    )
}

fn which(command: &str) -> Option<String> {
    let mut which_cmd = if cfg!(target_os = "windows") {
        Command::new("where")
    } else {
        Command::new("which")
    };

    let output = which_cmd.arg(command).output().ok()?;
    if output.status.success() {
        let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
        if !path.is_empty() {
            return Some(path);
        }
    }
    None
}

fn generate_bindings(header_path: &Path, out_dir: &Path) {
    let bindings_path = out_dir.join("bindings.rs");

    let bindings = bindgen::Builder::default()
        .header(header_path.to_str().unwrap())
        // Only generate bindings for our types and functions
        .allowlist_function("coraza_.*")
        .allowlist_type("coraza_.*")
        .allowlist_var("CORAZA_.*")
        // Use core types instead of std
        .use_core()
        // Derive common traits
        .derive_debug(true)
        .derive_default(true)
        .derive_eq(true)
        .derive_hash(true)
        // Generate commented bindings
        .generate_comments(true)
        // Layout tests for struct sizes
        .layout_tests(true)
        .generate()
        .expect("Failed to generate bindings. Check that libclang is installed.");

    bindings
        .write_to_file(&bindings_path)
        .expect("Failed to write bindings");

    // Tell cargo to re-run if the header changes
    println!("cargo:rerun-if-changed={}", header_path.display());
}