rust_web_server/proxy_config/
parser.rs1use std::collections::HashMap;
10
11pub type SectionMap = HashMap<String, Vec<(String, String)>>;
13
14pub fn parse(toml: &str) -> SectionMap {
21 let mut map: SectionMap = HashMap::new();
22
23 let mut outer_name: Option<String> = None; let mut outer_idx: usize = 0;
26 let mut outer_counters: HashMap<String, usize> = HashMap::new(); let mut inner_counters: HashMap<String, usize> = HashMap::new(); let mut current_section: String = String::new();
31
32 for raw_line in toml.lines() {
33 let line = strip_comment(raw_line).trim().to_string();
35
36 if line.is_empty() {
37 continue;
38 }
39
40 if line.starts_with("[[") && line.ends_with("]]") {
41 let name = line[2..line.len() - 2].trim().to_string();
43
44 if let Some(ref on) = outer_name.clone() {
46 let prefix = format!("{}.", on);
47 if name.starts_with(&prefix) {
48 let base = format!("{}{}", outer_section_base(on, outer_idx), &name[on.len()..]);
50 let cnt = inner_counters.entry(base.clone()).or_insert(0);
51 let section_path = format!("{}[{}]", base, cnt);
52 *cnt += 1;
53 current_section = section_path.clone();
54 map.entry(current_section.clone()).or_default();
55 continue;
56 }
57 }
58
59 let idx = outer_counters.entry(name.clone()).or_insert(0);
61 outer_idx = *idx;
62 *idx += 1;
63 outer_name = Some(name.clone());
64 inner_counters.clear();
65 current_section = outer_section_base(&name, outer_idx);
66 map.entry(current_section.clone()).or_default();
67 } else if line.starts_with('[') && line.ends_with(']') {
68 let name = line[1..line.len() - 1].trim().to_string();
70
71 if let Some(ref on) = outer_name.clone() {
72 let prefix = format!("{}.", on);
73 if name.starts_with(&prefix) {
74 current_section = format!(
76 "{}{}",
77 outer_section_base(on, outer_idx),
78 &name[on.len()..]
79 );
80 map.entry(current_section.clone()).or_default();
81 continue;
82 }
83 }
84
85 outer_name = None;
87 inner_counters.clear();
88 current_section = name.clone();
89 map.entry(current_section.clone()).or_default();
90 } else if let Some(eq) = line.find('=') {
91 let key = line[..eq].trim().to_string();
93 let raw_val = line[eq + 1..].trim().to_string();
94
95 if raw_val.starts_with('{') {
96 let inner = &raw_val[1..raw_val.rfind('}').unwrap_or(raw_val.len())];
98 for part in split_inline_table(inner) {
99 if let Some(ieq) = part.find('=') {
100 let ik = part[..ieq].trim();
101 let iv = parse_value(part[ieq + 1..].trim());
102 let subkey = format!("{}.{}", key, ik);
103 map.entry(current_section.clone())
104 .or_default()
105 .push((subkey, iv));
106 }
107 }
108 } else {
109 let value = parse_value(&raw_val);
110 map.entry(current_section.clone())
111 .or_default()
112 .push((key, value));
113 }
114 }
115 }
116
117 map
118}
119
120fn outer_section_base(name: &str, idx: usize) -> String {
122 format!("{}[{}]", name, idx)
123}
124
125pub(crate) fn strip_comment(line: &str) -> &str {
127 let bytes = line.as_bytes();
128 let mut in_quote = false;
129 let mut quote_char = b'"';
130 let mut i = 0;
131 while i < bytes.len() {
132 let b = bytes[i];
133 if in_quote {
134 if b == quote_char && (i == 0 || bytes[i - 1] != b'\\') {
135 in_quote = false;
136 }
137 } else if b == b'"' || b == b'\'' {
138 in_quote = true;
139 quote_char = b;
140 } else if b == b'#' {
141 return &line[..i];
142 }
143 i += 1;
144 }
145 line
146}
147
148pub(crate) fn parse_value(raw: &str) -> String {
154 let raw = raw.trim();
155 if raw.starts_with('[') && raw.ends_with(']') {
156 let inner = &raw[1..raw.len() - 1];
158 let items: Vec<String> = split_array_items(inner)
159 .into_iter()
160 .map(|s| strip_quotes(s.trim()))
161 .collect();
162 return items.join(",");
163 }
164 strip_quotes(raw)
165}
166
167fn strip_quotes(s: &str) -> String {
169 let s = s.trim();
170 if (s.starts_with('"') && s.ends_with('"')) ||
171 (s.starts_with('\'') && s.ends_with('\'')) {
172 s[1..s.len() - 1].to_string()
173 } else {
174 s.to_string()
175 }
176}
177
178fn split_array_items(inner: &str) -> Vec<&str> {
181 let mut items = Vec::new();
182 let mut depth = 0i32;
183 let mut in_quote = false;
184 let mut quote_char = b'"';
185 let mut start = 0;
186 let bytes = inner.as_bytes();
187
188 for (i, &b) in bytes.iter().enumerate() {
189 if in_quote {
190 if b == quote_char {
191 in_quote = false;
192 }
193 } else if b == b'"' || b == b'\'' {
194 in_quote = true;
195 quote_char = b;
196 } else if b == b'[' || b == b'{' {
197 depth += 1;
198 } else if b == b']' || b == b'}' {
199 depth -= 1;
200 } else if b == b',' && depth == 0 {
201 items.push(inner[start..i].trim());
202 start = i + 1;
203 }
204 }
205 let last = inner[start..].trim();
206 if !last.is_empty() {
207 items.push(last);
208 }
209 items
210}
211
212fn split_inline_table(inner: &str) -> Vec<String> {
214 let mut parts = Vec::new();
215 let mut depth = 0i32;
216 let mut in_quote = false;
217 let mut quote_char = b'"';
218 let mut current = String::new();
219
220 for b in inner.bytes() {
221 if in_quote {
222 current.push(b as char);
223 if b == quote_char {
224 in_quote = false;
225 }
226 } else if b == b'"' || b == b'\'' {
227 in_quote = true;
228 quote_char = b;
229 current.push(b as char);
230 } else if b == b'[' || b == b'{' {
231 depth += 1;
232 current.push(b as char);
233 } else if b == b']' || b == b'}' {
234 depth -= 1;
235 current.push(b as char);
236 } else if b == b',' && depth == 0 {
237 let part = current.trim().to_string();
238 if !part.is_empty() {
239 parts.push(part);
240 }
241 current = String::new();
242 } else {
243 current.push(b as char);
244 }
245 }
246 let part = current.trim().to_string();
247 if !part.is_empty() {
248 parts.push(part);
249 }
250 parts
251}
252
253pub(crate) fn get(map: &SectionMap, section: &str, key: &str) -> Option<String> {
257 map.get(section)?.iter().find(|(k, _)| k == key).map(|(_, v)| v.clone())
258}
259
260pub(crate) fn get_str(map: &SectionMap, section: &str, key: &str) -> String {
262 get(map, section, key).unwrap_or_default()
263}
264
265pub(crate) fn get_u64(map: &SectionMap, section: &str, key: &str, default: u64) -> u64 {
267 get(map, section, key)
268 .and_then(|v| v.parse().ok())
269 .unwrap_or(default)
270}
271
272pub(crate) fn get_u32(map: &SectionMap, section: &str, key: &str, default: u32) -> u32 {
274 get(map, section, key)
275 .and_then(|v| v.parse().ok())
276 .unwrap_or(default)
277}
278
279pub(crate) fn get_array(map: &SectionMap, section: &str, key: &str) -> Vec<String> {
281 match get(map, section, key) {
282 Some(v) if !v.is_empty() => v.split(',').map(|s| s.trim().to_string()).collect(),
283 _ => vec![],
284 }
285}
286
287pub(crate) fn section_exists(map: &SectionMap, section: &str) -> bool {
289 map.contains_key(section)
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295
296 const SAMPLE: &str = r#"
297[server]
298ip = "0.0.0.0"
299port = 8080
300
301[[upstream]]
302name = "api"
303backends = ["backend1:8080", "backend2:8080"]
304strategy = "round_robin"
305
306[upstream.health_check]
307path = "/health"
308interval_secs = 10
309timeout_ms = 2000
310healthy_threshold = 2
311unhealthy_threshold = 3
312
313[[route]]
314name = "api-route"
315
316[route.match]
317path = "/api/*"
318method = "GET"
319
320[route.action]
321type = "proxy"
322
323[route.action.proxy]
324upstream = "api"
325connect_timeout_ms = 5000
326read_timeout_ms = 30000
327
328[route.middleware]
329
330[route.middleware.rate_limit]
331max_requests = 100
332window_secs = 60
333
334[[route.middleware.rewrite.request]]
335type = "header_set"
336name = "X-Env"
337value = "production"
338
339[[tcp_proxy]]
340name = "db-proxy"
341listen = "0.0.0.0:5432"
342backends = ["db1:5432", "db2:5432"]
343connect_timeout_ms = 3000
344"#;
345
346 #[test]
347 fn parse_server_section() {
348 let map = parse(SAMPLE);
349 assert_eq!(get(&map, "server", "ip").as_deref(), Some("0.0.0.0"));
350 assert_eq!(get_u64(&map, "server", "port", 0), 8080);
351 }
352
353 #[test]
354 fn parse_upstream() {
355 let map = parse(SAMPLE);
356 assert!(section_exists(&map, "upstream[0]"));
357 assert_eq!(get_str(&map, "upstream[0]", "name"), "api");
358 assert_eq!(
359 get_array(&map, "upstream[0]", "backends"),
360 vec!["backend1:8080", "backend2:8080"]
361 );
362 }
363
364 #[test]
365 fn parse_upstream_health_check() {
366 let map = parse(SAMPLE);
367 assert!(section_exists(&map, "upstream[0].health_check"));
368 assert_eq!(get_str(&map, "upstream[0].health_check", "path"), "/health");
369 assert_eq!(get_u64(&map, "upstream[0].health_check", "interval_secs", 0), 10);
370 }
371
372 #[test]
373 fn parse_route() {
374 let map = parse(SAMPLE);
375 assert!(section_exists(&map, "route[0]"));
376 assert_eq!(get_str(&map, "route[0]", "name"), "api-route");
377 assert_eq!(get_str(&map, "route[0].match", "path"), "/api/*");
378 assert_eq!(get_str(&map, "route[0].match", "method"), "GET");
379 assert_eq!(get_str(&map, "route[0].action.proxy", "upstream"), "api");
380 assert_eq!(get_u64(&map, "route[0].action.proxy", "connect_timeout_ms", 0), 5000);
381 }
382
383 #[test]
384 fn parse_route_middleware() {
385 let map = parse(SAMPLE);
386 assert_eq!(get_u32(&map, "route[0].middleware.rate_limit", "max_requests", 0), 100);
387 assert_eq!(get_u64(&map, "route[0].middleware.rate_limit", "window_secs", 0), 60);
388 }
389
390 #[test]
391 fn parse_nested_rewrite_array() {
392 let map = parse(SAMPLE);
393 assert!(section_exists(&map, "route[0].middleware.rewrite.request[0]"));
394 assert_eq!(get_str(&map, "route[0].middleware.rewrite.request[0]", "type"), "header_set");
395 assert_eq!(get_str(&map, "route[0].middleware.rewrite.request[0]", "name"), "X-Env");
396 }
397
398 #[test]
399 fn parse_tcp_proxy() {
400 let map = parse(SAMPLE);
401 assert!(section_exists(&map, "tcp_proxy[0]"));
402 assert_eq!(get_str(&map, "tcp_proxy[0]", "name"), "db-proxy");
403 assert_eq!(get_str(&map, "tcp_proxy[0]", "listen"), "0.0.0.0:5432");
404 assert_eq!(
405 get_array(&map, "tcp_proxy[0]", "backends"),
406 vec!["db1:5432", "db2:5432"]
407 );
408 }
409
410 #[test]
411 fn strip_comments_test() {
412 assert_eq!(strip_comment("key = \"value\" # a comment"), "key = \"value\" ");
413 assert_eq!(strip_comment("# full comment"), "");
414 assert_eq!(strip_comment("url = \"http://example.com#fragment\""), "url = \"http://example.com#fragment\"");
415 }
416
417 #[test]
418 fn parse_value_array() {
419 assert_eq!(parse_value(r#"["a", "b", "c"]"#), "a,b,c");
420 }
421
422 #[test]
423 fn parse_value_string() {
424 assert_eq!(parse_value(r#""hello""#), "hello");
425 }
426}