sntrup-sys 0.1.1

Streamlined NTRU Prime, extracted from SUPERCOP with a deduplicated vendor tree, compiled via the cc crate
Documentation
use std::env;
use std::fs;
use std::path::Path;

// vendor/common/ is the generic helper layer (SHA-512, sort networks,
// crypto_declassify, the cryptoint headers) -- compiled exactly once and
// shared by every parameter set. vendor/algo/ is the actual Streamlined
// NTRU Prime implementation -- one shared copy of the source, but compiled
// once *per parameter set* (each compilation picks up a different
// vendor/sets/<set>/{api.h,paramsmenu.h}, which is what actually produces
// different object code per set; params.h dispatches on the SIZEnnn macro
// paramsmenu.h defines). See vendor/NOTICE.md for why the split is drawn
// exactly there.
const SETS: &[&str] = &["sntrup653", "sntrup761", "sntrup857", "sntrup953", "sntrup1013", "sntrup1277"];

fn cargo_feature_enabled(name: &str) -> bool {
    env::var_os(format!("CARGO_FEATURE_{}", name.to_uppercase())).is_some()
}

/// Parses a `-DNAME(x)=value` / `-DNAME=value` line (as written into each
/// BUILD_FLAGS.txt by the extraction script) into a (name, value) pair.
/// Kept as a plain define -- not a raw `.flag()` -- specifically so
/// `cc::Build` picks the right command-line spelling per compiler: `-D` for
/// GCC/Clang, `/D` for cl.exe. Both accept function-like macros this way.
fn parse_define(line: &str) -> (String, String) {
    let body = line.strip_prefix("-D").unwrap_or(line);
    let (name, value) = body
        .split_once('=')
        .unwrap_or_else(|| panic!("expected NAME=VALUE in define line: {line}"));
    (name.to_string(), value.to_string())
}

fn compile_dir(
    dir: &Path,
    includes: &[&Path],
    namespace_headers: &[&Path],
    defines: &[(String, String)],
    is_msvc: bool,
    out_name: &str,
) {
    let mut build = cc::Build::new();
    build.std("c11");
    build.warnings(false);
    for inc in includes {
        build.include(inc);
    }
    for ns in namespace_headers {
        // Force-includes a header ahead of everything else in the
        // translation unit: "-include <path>" for GCC/Clang, "/FI <path>"
        // for cl.exe. Either way the two must land as separate argv
        // entries, which is what two `.flag()` calls produce.
        build.flag(if is_msvc { "/FI" } else { "-include" });
        build.flag(ns.to_str().unwrap());
    }
    for (name, value) in defines {
        build.define(name, Some(value.as_str()));
    }

    let mut file_count = 0;
    for entry in fs::read_dir(dir).unwrap() {
        let entry = entry.unwrap();
        let path = entry.path();
        let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
        // Skip AppleDouble sidecar files (e.g. "._foo.c"): macOS writes one
        // alongside any file carrying extended attributes whenever it's
        // copied onto a filesystem that can't store them natively (network
        // shares, exFAT, some cloud-sync folders). They hold binary xattr
        // data, not source, but still end in ".c" and would otherwise get
        // handed to the compiler -- see vendor/NOTICE.md.
        if file_name.starts_with("._") {
            continue;
        }
        if path.extension().and_then(|e| e.to_str()) == Some("c") {
            build.file(&path);
            file_count += 1;
        }
        println!("cargo:rerun-if-changed={}", path.display());
    }
    assert!(file_count > 0, "no .c files found in {}", dir.display());

    build.compile(out_name);
}

fn main() {
    let vendor = Path::new(env!("CARGO_MANIFEST_DIR")).join("vendor");
    let common_dir = vendor.join("common");
    let algo_dir = vendor.join("algo");
    let common_ns = common_dir.join("namespace.h");

    // cc::Build always resolves *some* compiler for TARGET, but the two
    // force-include spellings ("-include" vs "/FI") differ specifically
    // between cl.exe and everything else (GCC, Clang, and clang-cl, which
    // still accepts GCC-style flags). TARGET containing "msvc" is exactly
    // the case that needs the cl.exe spelling.
    let is_msvc = env::var("TARGET").map(|t| t.contains("msvc")).unwrap_or(false);

    // Only worth compiling if at least one parameter set needs it.
    if SETS.iter().any(|s| cargo_feature_enabled(s)) {
        compile_dir(&common_dir, &[&common_dir], &[&common_ns], &[], is_msvc, "sntrup_common");
    }

    for &set in SETS {
        if !cargo_feature_enabled(set) {
            continue;
        }
        let set_dir = vendor.join("sets").join(set);
        let flags_path = set_dir.join("BUILD_FLAGS.txt");
        let defines: Vec<(String, String)> = fs::read_to_string(&flags_path)
            .unwrap_or_else(|e| panic!("reading {}: {}", flags_path.display(), e))
            .lines()
            .map(str::trim)
            .filter(|l| !l.is_empty())
            .map(parse_define)
            .collect();
        let set_ns = set_dir.join("namespace.h");

        // Include order matters here only in that vendor/sets/<set> must be
        // searched so "paramsmenu.h"/"api.h" resolve to this set's copies;
        // vendor/algo has no files of those names, so there's no ambiguity.
        compile_dir(
            &algo_dir,
            &[&set_dir, &algo_dir, &common_dir],
            &[&common_ns, &set_ns],
            &defines,
            is_msvc,
            &format!("{}_ref", set),
        );
    }
}