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())];
102 let sub_section = if current_section.is_empty() { key.clone() } else { format!("{}.{}", current_section, key) };
103 let entries = map.entry(sub_section).or_default();
104 for part in split_inline_table(inner) {
105 if let Some(ieq) = part.find('=') {
106 let ik = part[..ieq].trim().to_string();
107 let iv = parse_value(part[ieq + 1..].trim());
108 entries.push((ik, iv));
109 }
110 }
111 } else {
112 let value = parse_value(&raw_val);
113 map.entry(current_section.clone())
114 .or_default()
115 .push((key, value));
116 }
117 }
118 }
119
120 map
121}
122
123fn outer_section_base(name: &str, idx: usize) -> String {
125 format!("{}[{}]", name, idx)
126}
127
128pub(crate) fn strip_comment(line: &str) -> &str {
130 let bytes = line.as_bytes();
131 let mut in_quote = false;
132 let mut quote_char = b'"';
133 let mut i = 0;
134 while i < bytes.len() {
135 let b = bytes[i];
136 if in_quote {
137 if b == quote_char && (i == 0 || bytes[i - 1] != b'\\') {
138 in_quote = false;
139 }
140 } else if b == b'"' || b == b'\'' {
141 in_quote = true;
142 quote_char = b;
143 } else if b == b'#' {
144 return &line[..i];
145 }
146 i += 1;
147 }
148 line
149}
150
151pub(crate) fn parse_value(raw: &str) -> String {
157 let raw = raw.trim();
158 if raw.starts_with('[') && raw.ends_with(']') {
159 let inner = &raw[1..raw.len() - 1];
161 let items: Vec<String> = split_array_items(inner)
162 .into_iter()
163 .map(|s| strip_quotes(s.trim()))
164 .collect();
165 return items.join(",");
166 }
167 strip_quotes(raw)
168}
169
170fn strip_quotes(s: &str) -> String {
172 let s = s.trim();
173 if s.len() >= 2 && ((s.starts_with('"') && s.ends_with('"')) ||
175 (s.starts_with('\'') && s.ends_with('\''))) {
176 s[1..s.len() - 1].to_string()
177 } else {
178 s.to_string()
179 }
180}
181
182fn split_array_items(inner: &str) -> Vec<&str> {
185 let mut items = Vec::new();
186 let mut depth = 0i32;
187 let mut in_quote = false;
188 let mut quote_char = b'"';
189 let mut start = 0;
190 let bytes = inner.as_bytes();
191
192 for (i, &b) in bytes.iter().enumerate() {
193 if in_quote {
194 if b == quote_char {
195 in_quote = false;
196 }
197 } else if b == b'"' || b == b'\'' {
198 in_quote = true;
199 quote_char = b;
200 } else if b == b'[' || b == b'{' {
201 depth += 1;
202 } else if b == b']' || b == b'}' {
203 depth -= 1;
204 } else if b == b',' && depth == 0 {
205 items.push(inner[start..i].trim());
206 start = i + 1;
207 }
208 }
209 let last = inner[start..].trim();
210 if !last.is_empty() {
211 items.push(last);
212 }
213 items
214}
215
216fn split_inline_table(inner: &str) -> Vec<String> {
218 let mut parts = Vec::new();
219 let mut depth = 0i32;
220 let mut in_quote = false;
221 let mut quote_char = b'"';
222 let mut current = String::new();
223
224 for b in inner.bytes() {
225 if in_quote {
226 current.push(b as char);
227 if b == quote_char {
228 in_quote = false;
229 }
230 } else if b == b'"' || b == b'\'' {
231 in_quote = true;
232 quote_char = b;
233 current.push(b as char);
234 } else if b == b'[' || b == b'{' {
235 depth += 1;
236 current.push(b as char);
237 } else if b == b']' || b == b'}' {
238 depth -= 1;
239 current.push(b as char);
240 } else if b == b',' && depth == 0 {
241 let part = current.trim().to_string();
242 if !part.is_empty() {
243 parts.push(part);
244 }
245 current = String::new();
246 } else {
247 current.push(b as char);
248 }
249 }
250 let part = current.trim().to_string();
251 if !part.is_empty() {
252 parts.push(part);
253 }
254 parts
255}
256
257pub(crate) fn get(map: &SectionMap, section: &str, key: &str) -> Option<String> {
261 map.get(section)?.iter().find(|(k, _)| k == key).map(|(_, v)| v.clone())
262}
263
264pub(crate) fn get_str(map: &SectionMap, section: &str, key: &str) -> String {
266 get(map, section, key).unwrap_or_default()
267}
268
269pub(crate) fn get_u64(map: &SectionMap, section: &str, key: &str, default: u64) -> u64 {
271 get(map, section, key)
272 .and_then(|v| v.parse().ok())
273 .unwrap_or(default)
274}
275
276pub(crate) fn get_u32(map: &SectionMap, section: &str, key: &str, default: u32) -> u32 {
278 get(map, section, key)
279 .and_then(|v| v.parse().ok())
280 .unwrap_or(default)
281}
282
283pub(crate) fn get_array(map: &SectionMap, section: &str, key: &str) -> Vec<String> {
285 match get(map, section, key) {
286 Some(v) if !v.is_empty() => v.split(',').map(|s| s.trim().to_string()).collect(),
287 _ => vec![],
288 }
289}
290
291pub(crate) fn section_exists(map: &SectionMap, section: &str) -> bool {
293 map.contains_key(section)
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299
300 const SAMPLE: &str = r#"
301[server]
302ip = "0.0.0.0"
303port = 8080
304
305[[upstream]]
306name = "api"
307backends = ["backend1:8080", "backend2:8080"]
308strategy = "round_robin"
309
310[upstream.health_check]
311path = "/health"
312interval_secs = 10
313timeout_ms = 2000
314healthy_threshold = 2
315unhealthy_threshold = 3
316
317[[route]]
318name = "api-route"
319
320[route.match]
321path = "/api/*"
322method = "GET"
323
324[route.action]
325type = "proxy"
326
327[route.action.proxy]
328upstream = "api"
329connect_timeout_ms = 5000
330read_timeout_ms = 30000
331
332[route.middleware]
333
334[route.middleware.rate_limit]
335max_requests = 100
336window_secs = 60
337
338[[route.middleware.rewrite.request]]
339type = "header_set"
340name = "X-Env"
341value = "production"
342
343[[tcp_proxy]]
344name = "db-proxy"
345listen = "0.0.0.0:5432"
346backends = ["db1:5432", "db2:5432"]
347connect_timeout_ms = 3000
348"#;
349
350 #[test]
351 fn parse_server_section() {
352 let map = parse(SAMPLE);
353 assert_eq!(get(&map, "server", "ip").as_deref(), Some("0.0.0.0"));
354 assert_eq!(get_u64(&map, "server", "port", 0), 8080);
355 }
356
357 #[test]
358 fn parse_upstream() {
359 let map = parse(SAMPLE);
360 assert!(section_exists(&map, "upstream[0]"));
361 assert_eq!(get_str(&map, "upstream[0]", "name"), "api");
362 assert_eq!(
363 get_array(&map, "upstream[0]", "backends"),
364 vec!["backend1:8080", "backend2:8080"]
365 );
366 }
367
368 #[test]
369 fn parse_upstream_health_check() {
370 let map = parse(SAMPLE);
371 assert!(section_exists(&map, "upstream[0].health_check"));
372 assert_eq!(get_str(&map, "upstream[0].health_check", "path"), "/health");
373 assert_eq!(get_u64(&map, "upstream[0].health_check", "interval_secs", 0), 10);
374 }
375
376 #[test]
377 fn parse_route() {
378 let map = parse(SAMPLE);
379 assert!(section_exists(&map, "route[0]"));
380 assert_eq!(get_str(&map, "route[0]", "name"), "api-route");
381 assert_eq!(get_str(&map, "route[0].match", "path"), "/api/*");
382 assert_eq!(get_str(&map, "route[0].match", "method"), "GET");
383 assert_eq!(get_str(&map, "route[0].action.proxy", "upstream"), "api");
384 assert_eq!(get_u64(&map, "route[0].action.proxy", "connect_timeout_ms", 0), 5000);
385 }
386
387 #[test]
388 fn parse_route_middleware() {
389 let map = parse(SAMPLE);
390 assert_eq!(get_u32(&map, "route[0].middleware.rate_limit", "max_requests", 0), 100);
391 assert_eq!(get_u64(&map, "route[0].middleware.rate_limit", "window_secs", 0), 60);
392 }
393
394 #[test]
395 fn parse_nested_rewrite_array() {
396 let map = parse(SAMPLE);
397 assert!(section_exists(&map, "route[0].middleware.rewrite.request[0]"));
398 assert_eq!(get_str(&map, "route[0].middleware.rewrite.request[0]", "type"), "header_set");
399 assert_eq!(get_str(&map, "route[0].middleware.rewrite.request[0]", "name"), "X-Env");
400 }
401
402 #[test]
403 fn parse_tcp_proxy() {
404 let map = parse(SAMPLE);
405 assert!(section_exists(&map, "tcp_proxy[0]"));
406 assert_eq!(get_str(&map, "tcp_proxy[0]", "name"), "db-proxy");
407 assert_eq!(get_str(&map, "tcp_proxy[0]", "listen"), "0.0.0.0:5432");
408 assert_eq!(
409 get_array(&map, "tcp_proxy[0]", "backends"),
410 vec!["db1:5432", "db2:5432"]
411 );
412 }
413
414 #[test]
415 fn strip_comments_test() {
416 assert_eq!(strip_comment("key = \"value\" # a comment"), "key = \"value\" ");
417 assert_eq!(strip_comment("# full comment"), "");
418 assert_eq!(strip_comment("url = \"http://example.com#fragment\""), "url = \"http://example.com#fragment\"");
419 }
420
421 #[test]
422 fn parse_value_array() {
423 assert_eq!(parse_value(r#"["a", "b", "c"]"#), "a,b,c");
424 }
425
426 #[test]
427 fn parse_value_string() {
428 assert_eq!(parse_value(r#""hello""#), "hello");
429 }
430}