//! Build script for Rust modules in Tor.
//!
//! We need to use this because some of our Rust tests need to use some
//! of our C modules, which need to link some external libraries.
//!
//! This script works by looking at a "config.rust" file generated by our
//! configure script, and then building a set of options for cargo to pass to
//! the compiler.

use std::collections::HashMap;
use std::env;
use std::fs::File;
use std::io;
use std::io::prelude::*;
use std::path::PathBuf;

/// Wrapper around a key-value map.
struct Config(HashMap<String, String>);

/// Locate a config.rust file generated by autoconf, starting in the OUT_DIR
/// location provided by cargo and recursing up the directory tree.  Note that
/// we need to look in the OUT_DIR, since autoconf will place generated files
/// in the build directory.
fn find_cfg() -> io::Result<String> {
    let mut path = PathBuf::from(env::var("OUT_DIR").unwrap());
    loop {
        path.push("config.rust");
        if path.exists() {
            return Ok(path.to_str().unwrap().to_owned());
        }
        path.pop(); // remove config.rust
        if !path.pop() {
            // can't remove last part of directory
            return Err(io::Error::new(io::ErrorKind::NotFound, "No config.rust"));
        }
    }
}

impl Config {
    /// Find the config.rust file and try to parse it.
    ///
    /// The file format is a series of lines of the form KEY=VAL, with
    /// any blank lines and lines starting with # ignored.
    fn load() -> io::Result<Config> {
        let path = find_cfg()?;
        let f = File::open(&path)?;
        let reader = io::BufReader::new(f);
        let mut map = HashMap::new();
        for line in reader.lines() {
            let s = line?;
            if s.trim().starts_with("#") || s.trim() == "" {
                continue;
            }
            let idx = match s.find("=") {
                None => {
                    return Err(io::Error::new(io::ErrorKind::InvalidData, "missing ="));
                }
                Some(x) => x,
            };
            let (var, eq_val) = s.split_at(idx);
            let val = &eq_val[1..];
            map.insert(var.to_owned(), val.to_owned());
        }
        Ok(Config(map))
    }

    /// Return a reference to the value whose key is 'key'.
    ///
    /// Panics if 'key' is not found in the configuration.
    fn get(&self, key: &str) -> &str {
        self.0.get(key).unwrap()
    }

    /// Add a dependency on a static C library that is part of Tor, by name.
    fn component(&self, s: &str) {
        println!("cargo:rustc-link-lib=static={}", s);
    }

    /// Add a dependency on a native library that is not part of Tor, by name.
    fn dependency(&self, s: &str) {
        println!("cargo:rustc-link-lib={}", s);
    }

    /// Add a link path, relative to Tor's build directory.
    fn link_relpath(&self, s: &str) {
        let builddir = self.get("BUILDDIR");
        println!("cargo:rustc-link-search=native={}/{}", builddir, s);
    }

    /// Add an absolute link path.
    fn link_path(&self, s: &str) {
        println!("cargo:rustc-link-search=native={}", s);
    }

    /// Parse the CFLAGS in s, looking for -l and -L items, and adding
    /// rust configuration as appropriate.
    fn from_cflags(&self, s: &str) {
        let mut next_is_lib = false;
        let mut next_is_path = false;
        for ent in self.get(s).split_whitespace() {
            if next_is_lib {
                self.dependency(ent);
                next_is_lib = false;
            } else if next_is_path {
                self.link_path(ent);
                next_is_path = false;
            } else if ent == "-l" {
                next_is_lib = true;
            } else if ent == "-L" {
                next_is_path = true;
            } else if ent.starts_with("-L") {
                self.link_path(&ent[2..]);
            } else if ent.starts_with("-l") {
                self.dependency(&ent[2..]);
            }
        }
    }
}

pub fn main() {
    let cfg = Config::load().unwrap();
    let package = env::var("CARGO_PKG_NAME").unwrap();

    match package.as_ref() {
        "crypto" => {
            // Right now, I'm having a separate configuration for each Rust
            // package, since I'm hoping we can trim them down.  Once we have a
            // second Rust package that needs to use this build script, let's
            // extract some of this stuff into a module.
            //
            // This is a ridiculous amount of code to be pulling in just
            // to test our crypto library: modularity would be our
            // friend here.
            cfg.from_cflags("TOR_LDFLAGS_zlib");
            cfg.from_cflags("TOR_LDFLAGS_openssl");
            cfg.from_cflags("TOR_LDFLAGS_libevent");

            cfg.link_relpath("src/lib");
            cfg.link_relpath("src/ext/keccak-tiny");
            cfg.link_relpath("src/ext/ed25519/ref10");
            cfg.link_relpath("src/ext/ed25519/donna");
            cfg.link_relpath("src/trunnel");

            // Note that we can't pull in "libtor-testing", or else we
            // will have dependencies on all the other rust packages that
            // tor uses.  We must be careful with factoring and dependencies
            // moving forward!
            cfg.component("tor-crypt-ops-testing");
            cfg.component("tor-sandbox-testing");
            cfg.component("tor-encoding-testing");
            cfg.component("tor-fs-testing");
            cfg.component("tor-net-testing");
            cfg.component("tor-buf-testing");
            cfg.component("tor-time-testing");
            cfg.component("tor-thread-testing");
            cfg.component("tor-memarea-testing");
            cfg.component("tor-log-testing");
            cfg.component("tor-lock-testing");
            cfg.component("tor-fdio-testing");
            cfg.component("tor-container-testing");
            cfg.component("tor-smartlist-core-testing");
            cfg.component("tor-string-testing");
            cfg.component("tor-malloc");
            cfg.component("tor-wallclock");
            cfg.component("tor-err-testing");
            cfg.component("tor-version-testing");
            cfg.component("tor-intmath-testing");
            cfg.component("tor-ctime-testing");
            cfg.component("curve25519_donna");
            cfg.component("keccak-tiny");
            cfg.component("ed25519_ref10");
            cfg.component("ed25519_donna");
            cfg.component("or-trunnel-testing");

            cfg.from_cflags("TOR_ZLIB_LIBS");
            cfg.from_cflags("TOR_LIB_MATH");
            cfg.from_cflags("NSS_LIBS");
            cfg.from_cflags("TOR_OPENSSL_LIBS");
            cfg.from_cflags("TOR_LIBEVENT_LIBS");
            cfg.from_cflags("TOR_LIB_WS32");
            cfg.from_cflags("TOR_LIB_GDI");
            cfg.from_cflags("TOR_LIB_USERENV");
            cfg.from_cflags("CURVE25519_LIBS");
            cfg.from_cflags("TOR_LZMA_LIBS");
            cfg.from_cflags("TOR_ZSTD_LIBS");
            cfg.from_cflags("LIBS");
        }
        _ => {
            panic!("No configuration in build.rs for package {}", package);
        }
    }
}
