keyhog_scanner/structured/parsers/
hcl.rs1use super::ExtractedPair;
2
3pub fn parse_hcl(text: &str) -> Vec<ExtractedPair> {
7 let mut pairs = Vec::new();
8 let lines: Vec<&str> = text.lines().collect();
9 let mut index = 0;
10 while index < lines.len() {
11 let line = lines[index];
12 let trimmed = line.trim_start();
13 if let Some((var_name, _start_line)) = parse_variable_header(trimmed) {
14 let mut depth = 1usize;
15 let mut consumed = 1usize;
16 for offset in 1..MAX_VARIABLE_BLOCK_LINES {
17 if index + offset >= lines.len() {
18 break;
19 }
20 let inner = lines[index + offset];
21 let body = inner.trim();
22 if body.contains('{') {
23 depth += body.matches('{').count();
24 }
25 if body.contains('}') {
26 depth = depth.saturating_sub(body.matches('}').count());
27 if depth == 0 {
28 consumed = offset + 1;
29 break;
30 }
31 }
32 if let Some(value) = parse_hcl_default(body) {
33 if !value.is_empty() {
34 pairs.push(ExtractedPair {
35 context: var_name.clone(),
36 value,
37 line: index + offset + 1,
38 });
39 }
40 }
41 }
42 index += consumed;
43 continue;
44 }
45 if let Some((name, value)) = parse_hcl_assignment(trimmed) {
46 if !name.is_empty() && !value.is_empty() {
47 pairs.push(ExtractedPair {
48 context: name,
49 value,
50 line: index + 1,
51 });
52 }
53 }
54 index += 1;
55 }
56 pairs
57}
58
59const MAX_VARIABLE_BLOCK_LINES: usize = 16;
62
63fn parse_variable_header(line: &str) -> Option<(String, usize)> {
64 let rest = line.strip_prefix("variable")?;
65 if !rest.starts_with(|c: char| c.is_ascii_whitespace()) {
66 return None;
67 }
68 let rest = rest.trim_start();
69 let rest = rest.strip_prefix('"')?;
70 let end = rest.find('"')?;
71 let name = &rest[..end];
72 if name.is_empty() {
73 return None;
74 }
75 Some((name.to_string(), 0))
76}
77
78fn parse_hcl_default(line: &str) -> Option<String> {
79 let trimmed = line.trim_start();
80 let rest = trimmed.strip_prefix("default")?;
81 let rest = rest.trim_start();
82 let rest = rest.strip_prefix('=')?.trim_start();
83 extract_quoted_value(rest)
84}
85
86fn parse_hcl_assignment(line: &str) -> Option<(String, String)> {
87 if line.starts_with('#') || line.starts_with("//") || line.ends_with('{') || !line.contains('=')
88 {
89 return None;
90 }
91 for kw in [
92 "variable",
93 "locals",
94 "resource",
95 "module",
96 "provider",
97 "data",
98 "output",
99 "terraform",
100 ] {
101 if line.starts_with(kw)
102 && line[kw.len()..]
103 .chars()
104 .next()
105 .is_some_and(|c| c.is_ascii_whitespace() || c == '{')
106 {
107 return None;
108 }
109 }
110 let (name_part, value_part) = line.split_once('=')?;
111 let name = name_part.trim();
112 if name.is_empty()
113 || !name
114 .chars()
115 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
116 {
117 return None;
118 }
119 let value = extract_quoted_value(value_part.trim_start())?;
120 Some((name.to_string(), value))
121}
122
123fn extract_quoted_value(s: &str) -> Option<String> {
124 let bytes = s.as_bytes();
125 if bytes.is_empty() {
126 return None;
127 }
128 let quote = bytes[0];
129 if !matches!(quote, b'"' | b'\'' | b'`') {
130 return None;
131 }
132 let body = &s[1..];
133 let end = body.find(quote as char)?;
134 Some(body[..end].to_string())
135}