fabro/lib/crates/fabro-redact/build.rs
Bryan Helmkamp 56de394e01
refactor(redact): extract redaction into dedicated crate
Move secret redaction and DisplaySafeUrl into fabro-redact so credential handling has a narrow ownership boundary. Update direct consumers and docs to depend on fabro_redact instead of fabro_util::redact.
2026-04-24 15:02:23 -04:00

181 lines
5.9 KiB
Rust

#![expect(
clippy::disallowed_methods,
reason = "build script: runs at compile time outside any runtime"
)]
use std::fmt::Write;
use std::path::Path;
use std::{env, fs};
use serde::Deserialize;
#[derive(Deserialize)]
struct Config {
allowlist: Option<GlobalAllowlist>,
rules: Vec<Rule>,
}
#[derive(Deserialize)]
struct GlobalAllowlist {
regexes: Option<Vec<String>>,
stopwords: Option<Vec<String>>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Rule {
id: String,
regex: String,
#[serde(default)]
keywords: Vec<String>,
entropy: Option<f64>,
#[serde(default)]
allowlist: Option<RuleAllowlist>,
#[allow(
dead_code,
reason = "The source TOML carries descriptions even though codegen ignores them."
)]
description: Option<String>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct RuleAllowlist {
regexes: Option<Vec<String>>,
stopwords: Option<Vec<String>>,
regex_target: Option<String>,
}
fn escape_rust_string(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 10);
for ch in s.chars() {
match ch {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c => out.push(c),
}
}
out
}
fn main() {
println!("cargo:rerun-if-changed=data/gitleaks.toml");
let toml_path = Path::new("data/gitleaks.toml");
let toml_content = fs::read_to_string(toml_path).expect("failed to read gitleaks.toml");
let config: Config = toml::from_str(&toml_content).expect("failed to parse gitleaks.toml");
let out_dir = env::var("OUT_DIR").expect("OUT_DIR should be set for build scripts");
let out_path = Path::new(&out_dir).join("rules_generated.rs");
let mut code = String::new();
// Global allowlist regexes
code.push_str("#[allow(unreachable_pub, reason = \"Generated constants are included through a private module boundary.\")]\npub const GLOBAL_ALLOWLIST_REGEXES: &[&str] = &[\n");
if let Some(ref al) = config.allowlist {
if let Some(ref regexes) = al.regexes {
for r in regexes {
let _ = writeln!(code, " \"{}\",", escape_rust_string(r));
}
}
}
code.push_str("];\n\n");
// Global allowlist stopwords
code.push_str(
"#[allow(unreachable_pub, reason = \"Generated constants are included through a private module boundary.\")]\npub const GLOBAL_ALLOWLIST_STOPWORDS: &[&str] = &[\n",
);
if let Some(ref al) = config.allowlist {
if let Some(ref stopwords) = al.stopwords {
for sw in stopwords {
let _ = writeln!(code, " \"{}\",", escape_rust_string(sw));
}
}
}
code.push_str("];\n\n");
// Rule definitions
code.push_str("#[allow(dead_code, unreachable_pub, reason = \"Generated metadata is included through a private module boundary and not every field is read directly.\")]\n");
code.push_str("pub struct RuleDef {\n");
code.push_str(" pub id: &'static str,\n");
code.push_str(" pub regex_pattern: &'static str,\n");
code.push_str(" pub keywords: &'static [&'static str],\n");
code.push_str(" pub entropy: Option<f64>,\n");
code.push_str(" pub allowlist_regexes: &'static [&'static str],\n");
code.push_str(" pub allowlist_stopwords: &'static [&'static str],\n");
code.push_str(" pub allowlist_regex_target: Option<&'static str>,\n");
code.push_str("}\n\n");
code.push_str("#[allow(unreachable_pub, reason = \"Generated constants are included through a private module boundary.\")]\npub const RULES: &[RuleDef] = &[\n");
for rule in &config.rules {
code.push_str(" RuleDef {\n");
let _ = writeln!(code, " id: \"{}\",", escape_rust_string(&rule.id));
let _ = writeln!(
code,
" regex_pattern: \"{}\",",
escape_rust_string(&rule.regex)
);
// Keywords
code.push_str(" keywords: &[");
for kw in &rule.keywords {
let _ = write!(code, "\"{}\", ", escape_rust_string(kw));
}
code.push_str("],\n");
// Entropy
match rule.entropy {
Some(e) => {
let _ = writeln!(code, " entropy: Some({e:.1}),");
}
None => code.push_str(" entropy: None,\n"),
}
// Allowlist regexes
code.push_str(" allowlist_regexes: &[");
if let Some(ref al) = rule.allowlist {
if let Some(ref regexes) = al.regexes {
for r in regexes {
let _ = write!(code, "\"{}\", ", escape_rust_string(r));
}
}
}
code.push_str("],\n");
// Allowlist stopwords
code.push_str(" allowlist_stopwords: &[");
if let Some(ref al) = rule.allowlist {
if let Some(ref stopwords) = al.stopwords {
for sw in stopwords {
let _ = write!(code, "\"{}\", ", escape_rust_string(sw));
}
}
}
code.push_str("],\n");
// Allowlist regex target
if let Some(ref al) = rule.allowlist {
if let Some(ref target) = al.regex_target {
let _ = writeln!(
code,
" allowlist_regex_target: Some(\"{}\"),",
escape_rust_string(target)
);
} else {
code.push_str(" allowlist_regex_target: None,\n");
}
} else {
code.push_str(" allowlist_regex_target: None,\n");
}
code.push_str(" },\n");
}
code.push_str("];\n");
fs::write(&out_path, code).expect("failed to write generated rules");
}