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}"
);
}
}