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 line in logical_lines(toml) {
33
34 if line.is_empty() {
35 continue;
36 }
37
38 if line.starts_with("[[") && line.ends_with("]]") {
39 let name = line[2..line.len() - 2].trim().to_string();
41
42 if let Some(ref on) = outer_name.clone() {
44 let prefix = format!("{}.", on);
45 if name.starts_with(&prefix) {
46 let base = format!("{}{}", outer_section_base(on, outer_idx), &name[on.len()..]);
48 let cnt = inner_counters.entry(base.clone()).or_insert(0);
49 let section_path = format!("{}[{}]", base, cnt);
50 *cnt += 1;
51 current_section = section_path.clone();
52 map.entry(current_section.clone()).or_default();
53 continue;
54 }
55 }
56
57 let idx = outer_counters.entry(name.clone()).or_insert(0);
59 outer_idx = *idx;
60 *idx += 1;
61 outer_name = Some(name.clone());
62 inner_counters.clear();
63 current_section = outer_section_base(&name, outer_idx);
64 map.entry(current_section.clone()).or_default();
65 } else if line.starts_with('[') && line.ends_with(']') {
66 let name = line[1..line.len() - 1].trim().to_string();
68
69 if let Some(ref on) = outer_name.clone() {
70 let prefix = format!("{}.", on);
71 if name.starts_with(&prefix) {
72 current_section = format!(
74 "{}{}",
75 outer_section_base(on, outer_idx),
76 &name[on.len()..]
77 );
78 map.entry(current_section.clone()).or_default();
79 continue;
80 }
81 }
82
83 outer_name = None;
85 inner_counters.clear();
86 current_section = name.clone();
87 map.entry(current_section.clone()).or_default();
88 } else if let Some(eq) = line.find('=') {
89 let key = line[..eq].trim().to_string();
91 let raw_val = line[eq + 1..].trim().to_string();
92
93 if raw_val.starts_with('{') {
94 let inner = &raw_val[1..raw_val.rfind('}').unwrap_or(raw_val.len())];
100 let sub_section = if current_section.is_empty() { key.clone() } else { format!("{}.{}", current_section, key) };
101 let entries = map.entry(sub_section).or_default();
102 for part in split_inline_table(inner) {
103 if let Some(ieq) = part.find('=') {
104 let ik = part[..ieq].trim().to_string();
105 let iv = parse_value(part[ieq + 1..].trim());
106 entries.push((ik, iv));
107 }
108 }
109 } else {
110 let value = parse_value(&raw_val);
111 map.entry(current_section.clone())
112 .or_default()
113 .push((key, value));
114 }
115 }
116 }
117
118 map
119}
120
121fn outer_section_base(name: &str, idx: usize) -> String {
123 format!("{}[{}]", name, idx)
124}
125
126pub(crate) fn logical_lines(toml: &str) -> Vec<String> {
132 let mut out = Vec::new();
133 let mut pending: Option<(String, i64)> = None;
134 for raw in toml.lines() {
135 let line = strip_comment(raw).trim();
136 if let Some((joined, depth)) = pending.as_mut() {
137 if !line.is_empty() {
138 joined.push(' ');
139 joined.push_str(line);
140 *depth += bracket_depth(line);
141 }
142 if *depth <= 0 {
143 out.push(pending.take().expect("pending").0);
144 }
145 continue;
146 }
147 let opens_array = line.split_once('=').is_some_and(|(key, value)| !key.trim().starts_with('[') && value.trim_start().starts_with('['));
148 let depth = if opens_array { bracket_depth(line) } else { 0 };
149 if depth > 0 {
150 pending = Some((line.to_string(), depth));
151 } else {
152 out.push(line.to_string());
153 }
154 }
155 if let Some((joined, _)) = pending {
156 out.push(joined);
157 }
158 out
159}
160
161fn bracket_depth(line: &str) -> i64 {
163 let mut depth = 0;
164 let mut quote: Option<char> = None;
165 let mut escaped = false;
166 for c in line.chars() {
167 match quote {
168 Some(q) => {
169 if escaped {
170 escaped = false;
171 } else if c == '\\' {
172 escaped = true;
173 } else if c == q {
174 quote = None;
175 }
176 }
177 None => match c {
178 '"' | '\'' => quote = Some(c),
179 '[' => depth += 1,
180 ']' => depth -= 1,
181 _ => {}
182 },
183 }
184 }
185 depth
186}
187
188pub(crate) fn strip_comment(line: &str) -> &str {
189 let bytes = line.as_bytes();
190 let mut in_quote = false;
191 let mut quote_char = b'"';
192 let mut i = 0;
193 while i < bytes.len() {
194 let b = bytes[i];
195 if in_quote {
196 if b == quote_char && (i == 0 || bytes[i - 1] != b'\\') {
197 in_quote = false;
198 }
199 } else if b == b'"' || b == b'\'' {
200 in_quote = true;
201 quote_char = b;
202 } else if b == b'#' {
203 return &line[..i];
204 }
205 i += 1;
206 }
207 line
208}
209
210pub(crate) fn parse_value(raw: &str) -> String {
216 let raw = raw.trim();
217 if raw.starts_with('[') && raw.ends_with(']') {
218 let inner = &raw[1..raw.len() - 1];
220 let items: Vec<String> = split_array_items(inner)
221 .into_iter()
222 .map(|s| strip_quotes(s.trim()))
223 .collect();
224 return items.join(",");
225 }
226 strip_quotes(raw)
227}
228
229fn strip_quotes(s: &str) -> String {
231 let s = s.trim();
232 if s.len() >= 2 && ((s.starts_with('"') && s.ends_with('"')) ||
234 (s.starts_with('\'') && s.ends_with('\''))) {
235 s[1..s.len() - 1].to_string()
236 } else {
237 s.to_string()
238 }
239}
240
241fn split_array_items(inner: &str) -> Vec<&str> {
244 let mut items = Vec::new();
245 let mut depth = 0i32;
246 let mut in_quote = false;
247 let mut quote_char = b'"';
248 let mut start = 0;
249 let bytes = inner.as_bytes();
250
251 for (i, &b) in bytes.iter().enumerate() {
252 if in_quote {
253 if b == quote_char {
254 in_quote = false;
255 }
256 } else if b == b'"' || b == b'\'' {
257 in_quote = true;
258 quote_char = b;
259 } else if b == b'[' || b == b'{' {
260 depth += 1;
261 } else if b == b']' || b == b'}' {
262 depth -= 1;
263 } else if b == b',' && depth == 0 {
264 items.push(inner[start..i].trim());
265 start = i + 1;
266 }
267 }
268 let last = inner[start..].trim();
269 if !last.is_empty() {
270 items.push(last);
271 }
272 items
273}
274
275fn split_inline_table(inner: &str) -> Vec<String> {
277 let mut parts = Vec::new();
278 let mut depth = 0i32;
279 let mut in_quote = false;
280 let mut quote_char = b'"';
281 let mut current = String::new();
282
283 for b in inner.bytes() {
284 if in_quote {
285 current.push(b as char);
286 if b == quote_char {
287 in_quote = false;
288 }
289 } else if b == b'"' || b == b'\'' {
290 in_quote = true;
291 quote_char = b;
292 current.push(b as char);
293 } else if b == b'[' || b == b'{' {
294 depth += 1;
295 current.push(b as char);
296 } else if b == b']' || b == b'}' {
297 depth -= 1;
298 current.push(b as char);
299 } else if b == b',' && depth == 0 {
300 let part = current.trim().to_string();
301 if !part.is_empty() {
302 parts.push(part);
303 }
304 current = String::new();
305 } else {
306 current.push(b as char);
307 }
308 }
309 let part = current.trim().to_string();
310 if !part.is_empty() {
311 parts.push(part);
312 }
313 parts
314}
315
316pub(crate) fn get(map: &SectionMap, section: &str, key: &str) -> Option<String> {
320 map.get(section)?.iter().find(|(k, _)| k == key).map(|(_, v)| v.clone())
321}
322
323pub(crate) fn get_str(map: &SectionMap, section: &str, key: &str) -> String {
325 get(map, section, key).unwrap_or_default()
326}
327
328pub(crate) fn get_u64(map: &SectionMap, section: &str, key: &str, default: u64) -> u64 {
330 get(map, section, key)
331 .and_then(|v| v.parse().ok())
332 .unwrap_or(default)
333}
334
335pub(crate) fn get_u32(map: &SectionMap, section: &str, key: &str, default: u32) -> u32 {
337 get(map, section, key)
338 .and_then(|v| v.parse().ok())
339 .unwrap_or(default)
340}
341
342pub(crate) fn get_array(map: &SectionMap, section: &str, key: &str) -> Vec<String> {
344 match get(map, section, key) {
345 Some(v) if !v.is_empty() => v.split(',').map(|s| s.trim().to_string()).collect(),
346 _ => vec![],
347 }
348}
349
350pub(crate) fn section_exists(map: &SectionMap, section: &str) -> bool {
352 map.contains_key(section)
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358
359 const SAMPLE: &str = r#"
360[server]
361ip = "0.0.0.0"
362port = 8080
363
364[[upstream]]
365name = "api"
366backends = ["backend1:8080", "backend2:8080"]
367strategy = "round_robin"
368
369[upstream.health_check]
370path = "/health"
371interval_secs = 10
372timeout_ms = 2000
373healthy_threshold = 2
374unhealthy_threshold = 3
375
376[[route]]
377name = "api-route"
378
379[route.match]
380path = "/api/*"
381method = "GET"
382
383[route.action]
384type = "proxy"
385
386[route.action.proxy]
387upstream = "api"
388connect_timeout_ms = 5000
389read_timeout_ms = 30000
390
391[route.middleware]
392
393[route.middleware.rate_limit]
394max_requests = 100
395window_secs = 60
396
397[[route.middleware.rewrite.request]]
398type = "header_set"
399name = "X-Env"
400value = "production"
401
402[[tcp_proxy]]
403name = "db-proxy"
404listen = "0.0.0.0:5432"
405backends = ["db1:5432", "db2:5432"]
406connect_timeout_ms = 3000
407"#;
408
409 #[test]
410 fn parse_server_section() {
411 let map = parse(SAMPLE);
412 assert_eq!(get(&map, "server", "ip").as_deref(), Some("0.0.0.0"));
413 assert_eq!(get_u64(&map, "server", "port", 0), 8080);
414 }
415
416 #[test]
417 fn parse_upstream() {
418 let map = parse(SAMPLE);
419 assert!(section_exists(&map, "upstream[0]"));
420 assert_eq!(get_str(&map, "upstream[0]", "name"), "api");
421 assert_eq!(
422 get_array(&map, "upstream[0]", "backends"),
423 vec!["backend1:8080", "backend2:8080"]
424 );
425 }
426
427 #[test]
428 fn parse_upstream_health_check() {
429 let map = parse(SAMPLE);
430 assert!(section_exists(&map, "upstream[0].health_check"));
431 assert_eq!(get_str(&map, "upstream[0].health_check", "path"), "/health");
432 assert_eq!(get_u64(&map, "upstream[0].health_check", "interval_secs", 0), 10);
433 }
434
435 #[test]
436 fn parse_route() {
437 let map = parse(SAMPLE);
438 assert!(section_exists(&map, "route[0]"));
439 assert_eq!(get_str(&map, "route[0]", "name"), "api-route");
440 assert_eq!(get_str(&map, "route[0].match", "path"), "/api/*");
441 assert_eq!(get_str(&map, "route[0].match", "method"), "GET");
442 assert_eq!(get_str(&map, "route[0].action.proxy", "upstream"), "api");
443 assert_eq!(get_u64(&map, "route[0].action.proxy", "connect_timeout_ms", 0), 5000);
444 }
445
446 #[test]
447 fn parse_route_middleware() {
448 let map = parse(SAMPLE);
449 assert_eq!(get_u32(&map, "route[0].middleware.rate_limit", "max_requests", 0), 100);
450 assert_eq!(get_u64(&map, "route[0].middleware.rate_limit", "window_secs", 0), 60);
451 }
452
453 #[test]
454 fn parse_nested_rewrite_array() {
455 let map = parse(SAMPLE);
456 assert!(section_exists(&map, "route[0].middleware.rewrite.request[0]"));
457 assert_eq!(get_str(&map, "route[0].middleware.rewrite.request[0]", "type"), "header_set");
458 assert_eq!(get_str(&map, "route[0].middleware.rewrite.request[0]", "name"), "X-Env");
459 }
460
461 #[test]
462 fn parse_tcp_proxy() {
463 let map = parse(SAMPLE);
464 assert!(section_exists(&map, "tcp_proxy[0]"));
465 assert_eq!(get_str(&map, "tcp_proxy[0]", "name"), "db-proxy");
466 assert_eq!(get_str(&map, "tcp_proxy[0]", "listen"), "0.0.0.0:5432");
467 assert_eq!(
468 get_array(&map, "tcp_proxy[0]", "backends"),
469 vec!["db1:5432", "db2:5432"]
470 );
471 }
472
473 #[test]
474 fn multi_line_arrays_are_joined() {
475 let toml = "[authz]\ntuples = [\n \"doc:1#owner@user:a\", # first\n \"doc:2#owner@user:[b]\",\n]\nstore = \"memory\"\n";
476 let map = parse(toml);
477 assert_eq!(get_array(&map, "authz", "tuples"), ["doc:1#owner@user:a", "doc:2#owner@user:[b]"]);
478 assert_eq!(get_str(&map, "authz", "store"), "memory");
479 assert_eq!(logical_lines("a = [1,\n2]\n[t]\nb = []"), ["a = [1, 2]", "[t]", "b = []"]);
480 assert_eq!(logical_lines("a = [1,\n2"), ["a = [1, 2"], "an unclosed array ends with the file");
481 }
482
483 #[test]
484 fn strip_comments_test() {
485 assert_eq!(strip_comment("key = \"value\" # a comment"), "key = \"value\" ");
486 assert_eq!(strip_comment("# full comment"), "");
487 assert_eq!(strip_comment("url = \"http://example.com#fragment\""), "url = \"http://example.com#fragment\"");
488 }
489
490 #[test]
491 fn parse_value_array() {
492 assert_eq!(parse_value(r#"["a", "b", "c"]"#), "a,b,c");
493 }
494
495 #[test]
496 fn parse_value_string() {
497 assert_eq!(parse_value(r#""hello""#), "hello");
498 }
499}