fluidattacks_blends_domain/content/
helm.rs1use alloc::boxed::Box;
8use alloc::string::String;
9use alloc::vec::Vec;
10use core::str::Chars;
11
12use once_cell::race::OnceBox;
13use regex_automata::meta::Regex;
14use regex_automata::util::captures::Captures;
15
16struct Patterns {
17 helm_expr: Regex,
18 kv: Regex,
19 block_comment: Regex,
20 expr_normalize: Regex,
21 kv_line: Regex,
22 required: [Regex; 3],
23}
24
25static PATTERNS: OnceBox<Patterns> = OnceBox::new();
26
27impl Patterns {
28 fn get() -> Option<&'static Self> {
29 PATTERNS
30 .get_or_try_init(|| Self::new().map(Box::new).ok_or(()))
31 .ok()
32 }
33
34 fn new() -> Option<Self> {
35 Some(Self {
36 helm_expr: Regex::new(r"\{\{-?\s*[^{}]*?\s*-?\}\}").ok()?,
37 kv: Regex::new(
38 r"^(?P<indent>\s*)(?P<dash>- )?(?P<key>(?:\{\{.*?\}\}|[^:])+):\s*(?P<value>.*)$",
39 )
40 .ok()?,
41 block_comment: Regex::new(r"\{\{\s*-?\s*/\*(?:[\s\S]*?)\*/\s*-?\s*\}\}").ok()?,
42 expr_normalize: Regex::new(r"\{\{-?\s*(.*?)\s*-?\}\}").ok()?,
43 kv_line: Regex::new(r"^\s*-?\s*(?:\{\{.*?\}\}|[\w.{}$/-])+\s*:").ok()?,
44 required: [
45 Regex::new(r"\bapiVersion\b\s*:").ok()?,
46 Regex::new(r"\bkind\b\s*:").ok()?,
47 Regex::new(r"\bmetadata\b\s*:").ok()?,
48 ],
49 })
50 }
51}
52
53fn replace_all(
54 re: &Regex,
55 haystack: &str,
56 mut render: impl FnMut(&Captures, &str) -> String,
57) -> String {
58 let mut out = String::new();
59 let mut last = 0_usize;
60 for caps in re.captures_iter(haystack) {
61 let Some(matched) = caps.get_match() else {
62 continue;
63 };
64 if let Some(pre) = haystack.get(last..matched.start()) {
65 out.push_str(pre);
66 }
67 out.push_str(&render(&caps, haystack));
68 last = matched.end();
69 }
70 if let Some(rest) = haystack.get(last..) {
71 out.push_str(rest);
72 }
73 out
74}
75
76fn named<'a>(caps: &Captures, name: &str, haystack: &'a str) -> &'a str {
77 caps.get_group_by_name(name)
78 .and_then(|span| haystack.get(span.range()))
79 .unwrap_or("")
80}
81
82fn validate_required_keys(patterns: &Patterns, content: &str) -> bool {
83 patterns
84 .required
85 .iter()
86 .all(|pattern| pattern.is_match(content))
87}
88
89fn convert_block_comment(patterns: &Patterns, content: &str) -> String {
90 replace_all(&patterns.block_comment, content, |caps, haystack| {
91 let block = caps
92 .get_match()
93 .and_then(|matched| haystack.get(matched.range()))
94 .unwrap_or("");
95 let mut out = String::from("# ");
96 out.push_str(&block.replace('\n', "\n# "));
97 out
98 })
99}
100
101fn normalize_expression(patterns: &Patterns, value: &str) -> String {
102 replace_all(&patterns.expr_normalize, value, |caps, haystack| {
103 let inner = caps
104 .get_group(1)
105 .and_then(|span| haystack.get(span.range()))
106 .unwrap_or("");
107 let mut out = String::from("{{ ");
108 out.push_str(inner);
109 out.push_str(" }}");
110 out
111 })
112}
113
114fn take_until_quote(chars: &mut Chars<'_>) -> (String, bool) {
115 let mut inner = String::new();
116 for next in chars.by_ref() {
117 if next == '"' {
118 return (inner, true);
119 }
120 inner.push(next);
121 }
122 (inner, false)
123}
124
125fn replace_unescaped_double_quotes(input: &str) -> String {
126 let mut out = String::with_capacity(input.len());
127 let mut chars = input.chars();
128 let mut previous: Option<char> = None;
129 while let Some(current) = chars.next() {
130 if current != '"' || previous == Some('\\') {
131 out.push(current);
132 previous = Some(current);
133 continue;
134 }
135 let (inner, closed) = take_until_quote(&mut chars);
136 if closed {
137 out.push('\'');
138 out.push_str(&inner);
139 out.push('\'');
140 previous = Some('"');
141 } else {
142 out.push('"');
143 out.push_str(&inner);
144 previous = inner.chars().last().or(Some('"'));
145 }
146 }
147 out
148}
149
150fn fix_inner_quotes(patterns: &Patterns, value: &str) -> String {
151 replace_all(&patterns.helm_expr, value, |caps, haystack| {
152 let expr = caps
153 .get_match()
154 .and_then(|matched| haystack.get(matched.range()))
155 .unwrap_or("");
156 let inner = expr
157 .strip_prefix("{{")
158 .and_then(|rest| rest.strip_suffix("}}"))
159 .unwrap_or("");
160 let mut out = String::from("{{");
161 out.push_str(&replace_unescaped_double_quotes(inner));
162 out.push_str("}}");
163 out
164 })
165}
166
167fn process_kv_line(patterns: &Patterns, line: &str) -> String {
168 let mut caps = patterns.kv.create_captures();
169 patterns.kv.captures(line, &mut caps);
170 if !caps.is_match() {
171 return String::from(line);
172 }
173
174 let indent = named(&caps, "indent", line);
175 let dash = named(&caps, "dash", line);
176 let key = named(&caps, "key", line).trim();
177 let value = named(&caps, "value", line).trim();
178
179 let key_part = if patterns.helm_expr.is_match(key) {
180 let mut part = String::from("'");
181 part.push_str(&normalize_expression(patterns, key));
182 part.push_str("': ");
183 part
184 } else {
185 let mut part = String::from(key);
186 part.push_str(": ");
187 part
188 };
189 let mut prefix = String::from(indent);
190 prefix.push_str(dash);
191 prefix.push_str(&key_part);
192
193 if value.is_empty() {
194 return prefix;
195 }
196
197 if patterns.helm_expr.is_match(value) {
198 let normalized = normalize_expression(patterns, &fix_inner_quotes(patterns, value));
199 if normalized.starts_with('"') && normalized.ends_with('"') {
200 prefix.push_str(&normalized);
201 } else if normalized.contains('"') {
202 prefix.push('\'');
203 prefix.push_str(&normalized);
204 prefix.push('\'');
205 } else {
206 prefix.push('"');
207 prefix.push_str(&normalized);
208 prefix.push('"');
209 }
210 return prefix;
211 }
212
213 prefix.push_str(value);
214 prefix
215}
216
217fn get_indent(line: &str) -> &str {
218 let trimmed = line.trim_start();
219 line.get(..line.len().saturating_sub(trimmed.len()))
220 .unwrap_or("")
221}
222
223fn preprocess(patterns: &Patterns, text: &str) -> String {
224 let converted = convert_block_comment(patterns, text);
225 let mut lines: Vec<String> = Vec::new();
226 for line in converted.lines() {
227 if patterns.kv_line.is_match(line) {
228 lines.push(process_kv_line(patterns, line));
229 } else if patterns.helm_expr.is_match(line) {
230 let mut commented = String::from(get_indent(line));
231 commented.push_str("# ");
232 commented.push_str(&normalize_expression(patterns, line.trim()));
233 lines.push(commented);
234 } else {
235 lines.push(String::from(line));
236 }
237 }
238 let trimmed: Vec<&str> = lines.iter().map(|line| line.trim_end()).collect();
239 String::from(trimmed.join("\n").trim())
240}
241
242#[derive(Clone, Copy)]
243pub struct HelmInput<'a> {
244 pub text: &'a str,
245 pub under_templates: bool,
246}
247
248#[must_use]
249pub fn fix(input: HelmInput<'_>) -> Option<String> {
250 let patterns = Patterns::get()?;
251 if !(input.under_templates
252 && validate_required_keys(patterns, input.text)
253 && patterns.helm_expr.is_match(input.text))
254 {
255 return None;
256 }
257 Some(preprocess(patterns, input.text))
258}
259
260#[cfg(test)]
261mod tests {
262 use super::{fix, HelmInput};
263
264 const TEMPLATE: &str = concat!(
265 "apiVersion: v1\n",
266 "kind: Deployment\n",
267 "metadata:\n",
268 " name: test\n",
269 "spec:\n",
270 " replicas: {{ .Values.replicas }}\n",
271 );
272
273 fn run(text: &str, under_templates: bool) -> Option<String> {
274 fix(HelmInput {
275 text,
276 under_templates,
277 })
278 }
279
280 #[test]
281 fn skips_when_not_under_templates() {
282 assert!(run(TEMPLATE, false).is_none());
283 }
284
285 #[test]
286 fn skips_when_required_keys_missing() {
287 assert!(run("foo: {{ .Values.x }}\n", true).is_none());
288 }
289
290 #[test]
291 fn skips_when_no_helm_expression() {
292 let plain = "apiVersion: v1\nkind: Deployment\nmetadata:\n name: test\n";
293 assert!(run(plain, true).is_none());
294 }
295
296 #[test]
297 fn quotes_helm_value_expression() {
298 let fixed = run(TEMPLATE, true).expect("template is fixable");
299 assert!(
300 fixed.contains(r#"replicas: "{{ .Values.replicas }}""#),
301 "unexpected output: {fixed}"
302 );
303 }
304
305 #[test]
306 fn comments_standalone_helm_line() {
307 let text = concat!(
308 "apiVersion: v1\n",
309 "kind: Deployment\n",
310 "metadata:\n",
311 " name: test\n",
312 "{{- if .Values.enabled }}\n",
313 );
314 let fixed = run(text, true).expect("template is fixable");
315 assert!(
316 fixed.contains("# {{ if .Values.enabled }}"),
317 "unexpected output: {fixed}"
318 );
319 }
320}