//
// hardened-malloc-sys: Rust bindings for GrapheneOS allocator
// build.rs: Helper file for build-time information
//
// Copyright (c) 2025, 2026 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: MIT

use std::{
    env, fs,
    io::Write,
    path::{Path, PathBuf},
};

fn read_config_file(path: &PathBuf) -> Vec<(String, String)> {
    let content =
        fs::read_to_string(path).expect(&format!("Failed to read config file {:?}", path));

    content
        .lines()
        .filter(|line| {
            let trimmed = line.trim();
            !trimmed.starts_with("#") && !trimmed.is_empty()
        })
        .filter_map(|line| {
            let parts: Vec<&str> = line.splitn(2, '=').collect();
            if parts.len() == 2 {
                Some((parts[0].trim().to_string(), parts[1].trim().to_string()))
            } else {
                panic!("Invalid config line: {line}!");
            }
        })
        .collect()
}

fn main() {
    // Path to the config directory (relative to the project root).
    let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));

    // Determine the config file based on features.
    //
    // "light" and "tiny" each have their own config.
    // "default" is the fallback.
    let feature_light = env::var_os("CARGO_FEATURE_LIGHT").is_some();
    let feature_tiny = env::var_os("CARGO_FEATURE_TINY").is_some();
    if feature_light && feature_tiny {
        panic!("At most one of light and tiny features must be specified!");
    }
    let config_file = if feature_tiny {
        root.join("config-tiny.conf")
    } else if feature_light {
        root.join("config-light.conf")
    } else {
        root.join("config-default.conf")
    };

    // Determine CONFIG_SEAL_METADATA based on "seal" feature and target_env=gnu.
    let target_env = env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default();
    let feature_seal = target_env == "gnu" && env::var_os("CARGO_FEATURE_SEAL").is_some();
    let seal_metadata = if feature_seal { "true" } else { "false" };

    // Check for C23 compliant compiler, panic on error.
    let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());
    let std_c23 = check_c23(&out_dir);

    // Initialize the cc build system.
    let mut build = cc::Build::new();
    build.flag(std_c23);
    build.define("_GNU_SOURCE", Some("1"));

    // Read configuration key-value pairs from the selected config file.
    // Add each config as a preprocessor define (-Dkey=value).
    let config = read_config_file(&config_file);
    for (key, value) in config {
        if key == "CONFIG_SEAL_METADATA" {
            // Override CONFIG_SEAL_METADATA based on "seal" feature,
            // and target_env=gnu.
            build.define(&key, Some(seal_metadata));
            continue;
        }
        build.define(&key, Some(value.as_str()));
    }

    // Set source files directory (vendor/hardened-malloc).
    let vendor_dir = root.join("vendor").join("hardened-malloc");

    // Set source files.
    build.file(vendor_dir.join("chacha.c"));
    build.file(vendor_dir.join("h_malloc.c"));
    build.file(vendor_dir.join("memory.c"));
    build.file(vendor_dir.join("pages.c"));
    build.file(vendor_dir.join("random.c"));
    build.file(vendor_dir.join("util.c"));

    // Set include files.
    build.include(&vendor_dir);
    build.include(vendor_dir.join("include"));
    build.include(vendor_dir.join("third_party"));

    // Add LDFLAGS equivalent for linker flags.
    println!("cargo:rustc-link-arg=-Wl,-O1");
    println!("cargo:rustc-link-arg=-Wl,--as-needed");
    println!("cargo:rustc-link-arg=-Wl,-z,defs");
    println!("cargo:rustc-link-arg=-Wl,-z,relro");
    println!("cargo:rustc-link-arg=-Wl,-z,now");
    println!("cargo:rustc-link-arg=-Wl,-z,nodlopen");
    println!("cargo:rustc-link-arg=-Wl,-z,text");

    // Compile.
    build.compile("hardened_malloc");

    // Link statically.
    println!("cargo:rustc-link-lib=static=hardened_malloc");
    println!("cargo:rustc-link-search={}", out_dir.display());

    // Export paths and configuration for integration tests.
    println!(
        "cargo:rustc-env=HARDENED_MALLOC_VENDOR_DIR={}",
        vendor_dir.display()
    );
    println!(
        "cargo:rustc-env=HARDENED_MALLOC_OUT_DIR={}",
        out_dir.display()
    );
    println!(
        "cargo:rustc-env=HARDENED_MALLOC_CONFIG_FILE={}",
        config_file.display()
    );

    // Rerun the build script if config files change.
    println!("cargo:rerun-if-changed={}", config_file.display());
    println!(
        "cargo:rerun-if-changed={}",
        root.join("config-default.conf").display()
    );
    println!(
        "cargo:rerun-if-changed={}",
        root.join("config-light.conf").display()
    );
    println!(
        "cargo:rerun-if-changed={}",
        root.join("config-tiny.conf").display()
    );
    println!("cargo:rerun-if-changed={}", vendor_dir.display());
}

// Check for C23 compliant compiler, panic on error.
fn check_c23<P: AsRef<Path>>(out_dir: P) -> &'static str {
    // Write test file.
    let test = PathBuf::from(out_dir.as_ref()).join("test_c23.c");
    let mut file = fs::File::create(&test).unwrap();
    writeln!(file, "int main() {{ return 0; }}").unwrap();

    for flag in ["-std=c23", "-std=c2x"] {
        // Initialize cc build system.
        let mut build = cc::Build::new();
        build.file(&test);
        build.flag(flag);

        // Attempt to compile test file.
        if build.try_compile("test_c23").is_ok() {
            return flag;
        }
    }

    panic!("hardened-malloc-sys requires a C23 supporting compiler!");
}
