fluidattacks-blends-domain 0.6.0

Blends functional core: pure AST graph to syntax graph (no_std)
Documentation
//! Helm-template fixup for templated json/yaml.
//!
//! Rewrites `{{ ... }}` into something a parser can accept. Pure business
//! logic: text plus an `under_templates` hint in, fixed text out; the shell
//! owns parsing and decides the hint.

use alloc::boxed::Box;
use alloc::string::String;
use alloc::vec::Vec;
use core::str::Chars;

use once_cell::race::OnceBox;
use regex_automata::meta::Regex;
use regex_automata::util::captures::Captures;

struct Patterns {
    helm_expr: Regex,
    kv: Regex,
    block_comment: Regex,
    expr_normalize: Regex,
    kv_line: Regex,
    required: [Regex; 3],
}

static PATTERNS: OnceBox<Patterns> = OnceBox::new();

impl Patterns {
    fn get() -> Option<&'static Self> {
        PATTERNS
            .get_or_try_init(|| Self::new().map(Box::new).ok_or(()))
            .ok()
    }

    fn new() -> Option<Self> {
        Some(Self {
            helm_expr: Regex::new(r"\{\{-?\s*[^{}]*?\s*-?\}\}").ok()?,
            kv: Regex::new(
                r"^(?P<indent>\s*)(?P<dash>- )?(?P<key>(?:\{\{.*?\}\}|[^:])+):\s*(?P<value>.*)$",
            )
            .ok()?,
            block_comment: Regex::new(r"\{\{\s*-?\s*/\*(?:[\s\S]*?)\*/\s*-?\s*\}\}").ok()?,
            expr_normalize: Regex::new(r"\{\{-?\s*(.*?)\s*-?\}\}").ok()?,
            kv_line: Regex::new(r"^\s*-?\s*(?:\{\{.*?\}\}|[\w.{}$/-])+\s*:").ok()?,
            required: [
                Regex::new(r"\bapiVersion\b\s*:").ok()?,
                Regex::new(r"\bkind\b\s*:").ok()?,
                Regex::new(r"\bmetadata\b\s*:").ok()?,
            ],
        })
    }
}

fn replace_all(
    re: &Regex,
    haystack: &str,
    mut render: impl FnMut(&Captures, &str) -> String,
) -> String {
    let mut out = String::new();
    let mut last = 0_usize;
    for caps in re.captures_iter(haystack) {
        let Some(matched) = caps.get_match() else {
            continue;
        };
        if let Some(pre) = haystack.get(last..matched.start()) {
            out.push_str(pre);
        }
        out.push_str(&render(&caps, haystack));
        last = matched.end();
    }
    if let Some(rest) = haystack.get(last..) {
        out.push_str(rest);
    }
    out
}

fn named<'a>(caps: &Captures, name: &str, haystack: &'a str) -> &'a str {
    caps.get_group_by_name(name)
        .and_then(|span| haystack.get(span.range()))
        .unwrap_or("")
}

fn validate_required_keys(patterns: &Patterns, content: &str) -> bool {
    patterns
        .required
        .iter()
        .all(|pattern| pattern.is_match(content))
}

fn convert_block_comment(patterns: &Patterns, content: &str) -> String {
    replace_all(&patterns.block_comment, content, |caps, haystack| {
        let block = caps
            .get_match()
            .and_then(|matched| haystack.get(matched.range()))
            .unwrap_or("");
        let mut out = String::from("# ");
        out.push_str(&block.replace('\n', "\n# "));
        out
    })
}

fn normalize_expression(patterns: &Patterns, value: &str) -> String {
    replace_all(&patterns.expr_normalize, value, |caps, haystack| {
        let inner = caps
            .get_group(1)
            .and_then(|span| haystack.get(span.range()))
            .unwrap_or("");
        let mut out = String::from("{{ ");
        out.push_str(inner);
        out.push_str(" }}");
        out
    })
}

fn take_until_quote(chars: &mut Chars<'_>) -> (String, bool) {
    let mut inner = String::new();
    for next in chars.by_ref() {
        if next == '"' {
            return (inner, true);
        }
        inner.push(next);
    }
    (inner, false)
}

fn replace_unescaped_double_quotes(input: &str) -> String {
    let mut out = String::with_capacity(input.len());
    let mut chars = input.chars();
    let mut previous: Option<char> = None;
    while let Some(current) = chars.next() {
        if current != '"' || previous == Some('\\') {
            out.push(current);
            previous = Some(current);
            continue;
        }
        let (inner, closed) = take_until_quote(&mut chars);
        if closed {
            out.push('\'');
            out.push_str(&inner);
            out.push('\'');
            previous = Some('"');
        } else {
            out.push('"');
            out.push_str(&inner);
            previous = inner.chars().last().or(Some('"'));
        }
    }
    out
}

fn fix_inner_quotes(patterns: &Patterns, value: &str) -> String {
    replace_all(&patterns.helm_expr, value, |caps, haystack| {
        let expr = caps
            .get_match()
            .and_then(|matched| haystack.get(matched.range()))
            .unwrap_or("");
        let inner = expr
            .strip_prefix("{{")
            .and_then(|rest| rest.strip_suffix("}}"))
            .unwrap_or("");
        let mut out = String::from("{{");
        out.push_str(&replace_unescaped_double_quotes(inner));
        out.push_str("}}");
        out
    })
}

fn process_kv_line(patterns: &Patterns, line: &str) -> String {
    let mut caps = patterns.kv.create_captures();
    patterns.kv.captures(line, &mut caps);
    if !caps.is_match() {
        return String::from(line);
    }

    let indent = named(&caps, "indent", line);
    let dash = named(&caps, "dash", line);
    let key = named(&caps, "key", line).trim();
    let value = named(&caps, "value", line).trim();

    let key_part = if patterns.helm_expr.is_match(key) {
        let mut part = String::from("'");
        part.push_str(&normalize_expression(patterns, key));
        part.push_str("': ");
        part
    } else {
        let mut part = String::from(key);
        part.push_str(": ");
        part
    };
    let mut prefix = String::from(indent);
    prefix.push_str(dash);
    prefix.push_str(&key_part);

    if value.is_empty() {
        return prefix;
    }

    if patterns.helm_expr.is_match(value) {
        let normalized = normalize_expression(patterns, &fix_inner_quotes(patterns, value));
        if normalized.starts_with('"') && normalized.ends_with('"') {
            prefix.push_str(&normalized);
        } else if normalized.contains('"') {
            prefix.push('\'');
            prefix.push_str(&normalized);
            prefix.push('\'');
        } else {
            prefix.push('"');
            prefix.push_str(&normalized);
            prefix.push('"');
        }
        return prefix;
    }

    prefix.push_str(value);
    prefix
}

fn get_indent(line: &str) -> &str {
    let trimmed = line.trim_start();
    line.get(..line.len().saturating_sub(trimmed.len()))
        .unwrap_or("")
}

fn preprocess(patterns: &Patterns, text: &str) -> String {
    let converted = convert_block_comment(patterns, text);
    let mut lines: Vec<String> = Vec::new();
    for line in converted.lines() {
        if patterns.kv_line.is_match(line) {
            lines.push(process_kv_line(patterns, line));
        } else if patterns.helm_expr.is_match(line) {
            let mut commented = String::from(get_indent(line));
            commented.push_str("# ");
            commented.push_str(&normalize_expression(patterns, line.trim()));
            lines.push(commented);
        } else {
            lines.push(String::from(line));
        }
    }
    let trimmed: Vec<&str> = lines.iter().map(|line| line.trim_end()).collect();
    String::from(trimmed.join("\n").trim())
}

#[derive(Clone, Copy)]
pub struct HelmInput<'a> {
    pub text: &'a str,
    pub under_templates: bool,
}

#[must_use]
pub fn fix(input: HelmInput<'_>) -> Option<String> {
    let patterns = Patterns::get()?;
    if !(input.under_templates
        && validate_required_keys(patterns, input.text)
        && patterns.helm_expr.is_match(input.text))
    {
        return None;
    }
    Some(preprocess(patterns, input.text))
}

#[cfg(test)]
mod tests {
    use super::{fix, HelmInput};

    const TEMPLATE: &str = concat!(
        "apiVersion: v1\n",
        "kind: Deployment\n",
        "metadata:\n",
        "  name: test\n",
        "spec:\n",
        "  replicas: {{ .Values.replicas }}\n",
    );

    fn run(text: &str, under_templates: bool) -> Option<String> {
        fix(HelmInput {
            text,
            under_templates,
        })
    }

    #[test]
    fn skips_when_not_under_templates() {
        assert!(run(TEMPLATE, false).is_none());
    }

    #[test]
    fn skips_when_required_keys_missing() {
        assert!(run("foo: {{ .Values.x }}\n", true).is_none());
    }

    #[test]
    fn skips_when_no_helm_expression() {
        let plain = "apiVersion: v1\nkind: Deployment\nmetadata:\n  name: test\n";
        assert!(run(plain, true).is_none());
    }

    #[test]
    fn quotes_helm_value_expression() {
        let fixed = run(TEMPLATE, true).expect("template is fixable");
        assert!(
            fixed.contains(r#"replicas: "{{ .Values.replicas }}""#),
            "unexpected output: {fixed}"
        );
    }

    #[test]
    fn comments_standalone_helm_line() {
        let text = concat!(
            "apiVersion: v1\n",
            "kind: Deployment\n",
            "metadata:\n",
            "  name: test\n",
            "{{- if .Values.enabled }}\n",
        );
        let fixed = run(text, true).expect("template is fixable");
        assert!(
            fixed.contains("# {{ if .Values.enabled }}"),
            "unexpected output: {fixed}"
        );
    }
}