1use std::collections::HashMap;
8use std::fmt;
9use std::path::Path;
10use std::io;
11
12#[derive(Debug, Clone)]
14pub struct RnsConfig {
15 pub reticulum: ReticulumSection,
16 pub logging: LoggingSection,
17 pub interfaces: Vec<ParsedInterface>,
18}
19
20#[derive(Debug, Clone)]
22pub struct ReticulumSection {
23 pub enable_transport: bool,
24 pub share_instance: bool,
25 pub instance_name: String,
26 pub shared_instance_port: u16,
27 pub instance_control_port: u16,
28 pub panic_on_interface_error: bool,
29 pub use_implicit_proof: bool,
30 pub network_identity: Option<String>,
31 pub respond_to_probes: bool,
32 pub enable_remote_management: bool,
33 pub remote_management_allowed: Vec<String>,
34 pub publish_blackhole: bool,
35 pub probe_port: Option<u16>,
36 pub probe_addr: Option<String>,
37 pub device: Option<String>,
39}
40
41impl Default for ReticulumSection {
42 fn default() -> Self {
43 ReticulumSection {
44 enable_transport: false,
45 share_instance: true,
46 instance_name: "default".into(),
47 shared_instance_port: 37428,
48 instance_control_port: 37429,
49 panic_on_interface_error: false,
50 use_implicit_proof: true,
51 network_identity: None,
52 respond_to_probes: false,
53 enable_remote_management: false,
54 remote_management_allowed: Vec::new(),
55 publish_blackhole: false,
56 probe_port: None,
57 probe_addr: None,
58 device: None,
59 }
60 }
61}
62
63#[derive(Debug, Clone)]
65pub struct LoggingSection {
66 pub loglevel: u8,
67}
68
69impl Default for LoggingSection {
70 fn default() -> Self {
71 LoggingSection { loglevel: 4 }
72 }
73}
74
75#[derive(Debug, Clone)]
77pub struct ParsedInterface {
78 pub name: String,
79 pub interface_type: String,
80 pub enabled: bool,
81 pub mode: String,
82 pub params: HashMap<String, String>,
83}
84
85#[derive(Debug, Clone)]
87pub enum ConfigError {
88 Io(String),
89 Parse(String),
90 InvalidValue { key: String, value: String },
91}
92
93impl fmt::Display for ConfigError {
94 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95 match self {
96 ConfigError::Io(msg) => write!(f, "Config I/O error: {}", msg),
97 ConfigError::Parse(msg) => write!(f, "Config parse error: {}", msg),
98 ConfigError::InvalidValue { key, value } => {
99 write!(f, "Invalid value for '{}': '{}'", key, value)
100 }
101 }
102 }
103}
104
105impl From<io::Error> for ConfigError {
106 fn from(e: io::Error) -> Self {
107 ConfigError::Io(e.to_string())
108 }
109}
110
111pub fn parse(input: &str) -> Result<RnsConfig, ConfigError> {
113 let mut current_section: Option<String> = None;
114 let mut current_subsection: Option<String> = None;
115
116 let mut reticulum_kvs: HashMap<String, String> = HashMap::new();
117 let mut logging_kvs: HashMap<String, String> = HashMap::new();
118 let mut interfaces: Vec<ParsedInterface> = Vec::new();
119 let mut current_iface_kvs: Option<HashMap<String, String>> = None;
120 let mut current_iface_name: Option<String> = None;
121
122 for line in input.lines() {
123 let line = strip_comment(line);
125 let trimmed = line.trim();
126
127 if trimmed.is_empty() {
129 continue;
130 }
131
132 if trimmed.starts_with("[[") && trimmed.ends_with("]]") {
134 let name = trimmed[2..trimmed.len() - 2].trim().to_string();
135 if let (Some(iface_name), Some(kvs)) =
137 (current_iface_name.take(), current_iface_kvs.take())
138 {
139 interfaces.push(build_parsed_interface(iface_name, kvs));
140 }
141 current_subsection = Some(name.clone());
142 current_iface_name = Some(name);
143 current_iface_kvs = Some(HashMap::new());
144 continue;
145 }
146
147 if trimmed.starts_with('[') && trimmed.ends_with(']') {
149 if let (Some(iface_name), Some(kvs)) =
151 (current_iface_name.take(), current_iface_kvs.take())
152 {
153 interfaces.push(build_parsed_interface(iface_name, kvs));
154 }
155 current_subsection = None;
156
157 let name = trimmed[1..trimmed.len() - 1].trim().to_lowercase();
158 current_section = Some(name);
159 continue;
160 }
161
162 if let Some(eq_pos) = trimmed.find('=') {
164 let key = trimmed[..eq_pos].trim().to_string();
165 let value = trimmed[eq_pos + 1..].trim().to_string();
166
167 if current_subsection.is_some() {
168 if let Some(ref mut kvs) = current_iface_kvs {
170 kvs.insert(key, value);
171 }
172 } else if let Some(ref section) = current_section {
173 match section.as_str() {
174 "reticulum" => {
175 reticulum_kvs.insert(key, value);
176 }
177 "logging" => {
178 logging_kvs.insert(key, value);
179 }
180 _ => {} }
182 }
183 }
184 }
185
186 if let (Some(iface_name), Some(kvs)) = (current_iface_name.take(), current_iface_kvs.take()) {
188 interfaces.push(build_parsed_interface(iface_name, kvs));
189 }
190
191 let reticulum = build_reticulum_section(&reticulum_kvs)?;
193 let logging = build_logging_section(&logging_kvs)?;
194
195 Ok(RnsConfig {
196 reticulum,
197 logging,
198 interfaces,
199 })
200}
201
202pub fn parse_file(path: &Path) -> Result<RnsConfig, ConfigError> {
204 let content = std::fs::read_to_string(path)?;
205 parse(&content)
206}
207
208fn strip_comment(line: &str) -> &str {
210 let mut in_quote = false;
212 let mut quote_char = '"';
213 for (i, ch) in line.char_indices() {
214 if !in_quote && (ch == '"' || ch == '\'') {
215 in_quote = true;
216 quote_char = ch;
217 } else if in_quote && ch == quote_char {
218 in_quote = false;
219 } else if !in_quote && ch == '#' {
220 return &line[..i];
221 }
222 }
223 line
224}
225
226pub fn parse_bool_pub(value: &str) -> Option<bool> {
228 parse_bool(value)
229}
230
231fn parse_bool(value: &str) -> Option<bool> {
233 match value.to_lowercase().as_str() {
234 "yes" | "true" | "1" | "on" => Some(true),
235 "no" | "false" | "0" | "off" => Some(false),
236 _ => None,
237 }
238}
239
240fn build_parsed_interface(name: String, mut kvs: HashMap<String, String>) -> ParsedInterface {
241 let interface_type = kvs.remove("type").unwrap_or_default();
242 let enabled = kvs
243 .remove("enabled")
244 .and_then(|v| parse_bool(&v))
245 .unwrap_or(true);
246 let mode = kvs
248 .remove("interface_mode")
249 .or_else(|| kvs.remove("mode"))
250 .unwrap_or_else(|| "full".into());
251
252 ParsedInterface {
253 name,
254 interface_type,
255 enabled,
256 mode,
257 params: kvs,
258 }
259}
260
261fn build_reticulum_section(
262 kvs: &HashMap<String, String>,
263) -> Result<ReticulumSection, ConfigError> {
264 let mut section = ReticulumSection::default();
265
266 if let Some(v) = kvs.get("enable_transport") {
267 section.enable_transport = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
268 key: "enable_transport".into(),
269 value: v.clone(),
270 })?;
271 }
272 if let Some(v) = kvs.get("share_instance") {
273 section.share_instance = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
274 key: "share_instance".into(),
275 value: v.clone(),
276 })?;
277 }
278 if let Some(v) = kvs.get("instance_name") {
279 section.instance_name = v.clone();
280 }
281 if let Some(v) = kvs.get("shared_instance_port") {
282 section.shared_instance_port =
283 v.parse::<u16>().map_err(|_| ConfigError::InvalidValue {
284 key: "shared_instance_port".into(),
285 value: v.clone(),
286 })?;
287 }
288 if let Some(v) = kvs.get("instance_control_port") {
289 section.instance_control_port =
290 v.parse::<u16>().map_err(|_| ConfigError::InvalidValue {
291 key: "instance_control_port".into(),
292 value: v.clone(),
293 })?;
294 }
295 if let Some(v) = kvs.get("panic_on_interface_error") {
296 section.panic_on_interface_error =
297 parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
298 key: "panic_on_interface_error".into(),
299 value: v.clone(),
300 })?;
301 }
302 if let Some(v) = kvs.get("use_implicit_proof") {
303 section.use_implicit_proof = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
304 key: "use_implicit_proof".into(),
305 value: v.clone(),
306 })?;
307 }
308 if let Some(v) = kvs.get("network_identity") {
309 section.network_identity = Some(v.clone());
310 }
311 if let Some(v) = kvs.get("respond_to_probes") {
312 section.respond_to_probes = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
313 key: "respond_to_probes".into(),
314 value: v.clone(),
315 })?;
316 }
317 if let Some(v) = kvs.get("enable_remote_management") {
318 section.enable_remote_management = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
319 key: "enable_remote_management".into(),
320 value: v.clone(),
321 })?;
322 }
323 if let Some(v) = kvs.get("remote_management_allowed") {
324 for item in v.split(',') {
326 let trimmed = item.trim();
327 if !trimmed.is_empty() {
328 section.remote_management_allowed.push(trimmed.to_string());
329 }
330 }
331 }
332 if let Some(v) = kvs.get("publish_blackhole") {
333 section.publish_blackhole = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
334 key: "publish_blackhole".into(),
335 value: v.clone(),
336 })?;
337 }
338 if let Some(v) = kvs.get("probe_port") {
339 section.probe_port = Some(v.parse::<u16>().map_err(|_| ConfigError::InvalidValue {
340 key: "probe_port".into(),
341 value: v.clone(),
342 })?);
343 }
344 if let Some(v) = kvs.get("probe_addr") {
345 section.probe_addr = Some(v.clone());
346 }
347 if let Some(v) = kvs.get("device") {
348 section.device = Some(v.clone());
349 }
350
351 Ok(section)
352}
353
354fn build_logging_section(kvs: &HashMap<String, String>) -> Result<LoggingSection, ConfigError> {
355 let mut section = LoggingSection::default();
356
357 if let Some(v) = kvs.get("loglevel") {
358 section.loglevel = v.parse::<u8>().map_err(|_| ConfigError::InvalidValue {
359 key: "loglevel".into(),
360 value: v.clone(),
361 })?;
362 }
363
364 Ok(section)
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370
371 #[test]
372 fn parse_empty() {
373 let config = parse("").unwrap();
374 assert!(!config.reticulum.enable_transport);
375 assert!(config.reticulum.share_instance);
376 assert_eq!(config.reticulum.instance_name, "default");
377 assert_eq!(config.logging.loglevel, 4);
378 assert!(config.interfaces.is_empty());
379 }
380
381 #[test]
382 fn parse_default_config() {
383 let input = r#"
385[reticulum]
386enable_transport = False
387share_instance = Yes
388instance_name = default
389
390[logging]
391loglevel = 4
392
393[interfaces]
394
395 [[Default Interface]]
396 type = AutoInterface
397 enabled = Yes
398"#;
399 let config = parse(input).unwrap();
400 assert!(!config.reticulum.enable_transport);
401 assert!(config.reticulum.share_instance);
402 assert_eq!(config.reticulum.instance_name, "default");
403 assert_eq!(config.logging.loglevel, 4);
404 assert_eq!(config.interfaces.len(), 1);
405 assert_eq!(config.interfaces[0].name, "Default Interface");
406 assert_eq!(config.interfaces[0].interface_type, "AutoInterface");
407 assert!(config.interfaces[0].enabled);
408 }
409
410 #[test]
411 fn parse_reticulum_section() {
412 let input = r#"
413[reticulum]
414enable_transport = True
415share_instance = No
416instance_name = mynode
417shared_instance_port = 12345
418instance_control_port = 12346
419panic_on_interface_error = Yes
420use_implicit_proof = False
421respond_to_probes = True
422network_identity = /home/user/.reticulum/identity
423"#;
424 let config = parse(input).unwrap();
425 assert!(config.reticulum.enable_transport);
426 assert!(!config.reticulum.share_instance);
427 assert_eq!(config.reticulum.instance_name, "mynode");
428 assert_eq!(config.reticulum.shared_instance_port, 12345);
429 assert_eq!(config.reticulum.instance_control_port, 12346);
430 assert!(config.reticulum.panic_on_interface_error);
431 assert!(!config.reticulum.use_implicit_proof);
432 assert!(config.reticulum.respond_to_probes);
433 assert_eq!(
434 config.reticulum.network_identity.as_deref(),
435 Some("/home/user/.reticulum/identity")
436 );
437 }
438
439 #[test]
440 fn parse_logging_section() {
441 let input = "[logging]\nloglevel = 6\n";
442 let config = parse(input).unwrap();
443 assert_eq!(config.logging.loglevel, 6);
444 }
445
446 #[test]
447 fn parse_interface_tcp_client() {
448 let input = r#"
449[interfaces]
450 [[TCP Client]]
451 type = TCPClientInterface
452 enabled = Yes
453 target_host = 87.106.8.245
454 target_port = 4242
455"#;
456 let config = parse(input).unwrap();
457 assert_eq!(config.interfaces.len(), 1);
458 let iface = &config.interfaces[0];
459 assert_eq!(iface.name, "TCP Client");
460 assert_eq!(iface.interface_type, "TCPClientInterface");
461 assert!(iface.enabled);
462 assert_eq!(iface.params.get("target_host").unwrap(), "87.106.8.245");
463 assert_eq!(iface.params.get("target_port").unwrap(), "4242");
464 }
465
466 #[test]
467 fn parse_interface_tcp_server() {
468 let input = r#"
469[interfaces]
470 [[TCP Server]]
471 type = TCPServerInterface
472 enabled = Yes
473 listen_ip = 0.0.0.0
474 listen_port = 4242
475"#;
476 let config = parse(input).unwrap();
477 assert_eq!(config.interfaces.len(), 1);
478 let iface = &config.interfaces[0];
479 assert_eq!(iface.name, "TCP Server");
480 assert_eq!(iface.interface_type, "TCPServerInterface");
481 assert_eq!(iface.params.get("listen_ip").unwrap(), "0.0.0.0");
482 assert_eq!(iface.params.get("listen_port").unwrap(), "4242");
483 }
484
485 #[test]
486 fn parse_interface_udp() {
487 let input = r#"
488[interfaces]
489 [[UDP Interface]]
490 type = UDPInterface
491 enabled = Yes
492 listen_ip = 0.0.0.0
493 listen_port = 4242
494 forward_ip = 255.255.255.255
495 forward_port = 4242
496"#;
497 let config = parse(input).unwrap();
498 assert_eq!(config.interfaces.len(), 1);
499 let iface = &config.interfaces[0];
500 assert_eq!(iface.name, "UDP Interface");
501 assert_eq!(iface.interface_type, "UDPInterface");
502 assert_eq!(iface.params.get("listen_ip").unwrap(), "0.0.0.0");
503 assert_eq!(iface.params.get("forward_ip").unwrap(), "255.255.255.255");
504 }
505
506 #[test]
507 fn parse_multiple_interfaces() {
508 let input = r#"
509[interfaces]
510 [[TCP Client]]
511 type = TCPClientInterface
512 target_host = 10.0.0.1
513 target_port = 4242
514
515 [[UDP Broadcast]]
516 type = UDPInterface
517 listen_ip = 0.0.0.0
518 listen_port = 5555
519 forward_ip = 255.255.255.255
520 forward_port = 5555
521"#;
522 let config = parse(input).unwrap();
523 assert_eq!(config.interfaces.len(), 2);
524 assert_eq!(config.interfaces[0].name, "TCP Client");
525 assert_eq!(config.interfaces[0].interface_type, "TCPClientInterface");
526 assert_eq!(config.interfaces[1].name, "UDP Broadcast");
527 assert_eq!(config.interfaces[1].interface_type, "UDPInterface");
528 }
529
530 #[test]
531 fn parse_booleans() {
532 for (input, expected) in &[
534 ("Yes", true),
535 ("No", false),
536 ("True", true),
537 ("False", false),
538 ("true", true),
539 ("false", false),
540 ("1", true),
541 ("0", false),
542 ("on", true),
543 ("off", false),
544 ] {
545 let result = parse_bool(input);
546 assert_eq!(result, Some(*expected), "parse_bool({}) failed", input);
547 }
548 }
549
550 #[test]
551 fn parse_comments() {
552 let input = r#"
553# This is a comment
554[reticulum]
555enable_transport = True # inline comment
556# share_instance = No
557instance_name = test
558"#;
559 let config = parse(input).unwrap();
560 assert!(config.reticulum.enable_transport);
561 assert!(config.reticulum.share_instance); assert_eq!(config.reticulum.instance_name, "test");
563 }
564
565 #[test]
566 fn parse_interface_mode_field() {
567 let input = r#"
568[interfaces]
569 [[TCP Client]]
570 type = TCPClientInterface
571 interface_mode = access_point
572 target_host = 10.0.0.1
573 target_port = 4242
574"#;
575 let config = parse(input).unwrap();
576 assert_eq!(config.interfaces[0].mode, "access_point");
577 }
578
579 #[test]
580 fn parse_mode_fallback() {
581 let input = r#"
583[interfaces]
584 [[TCP Client]]
585 type = TCPClientInterface
586 mode = gateway
587 target_host = 10.0.0.1
588 target_port = 4242
589"#;
590 let config = parse(input).unwrap();
591 assert_eq!(config.interfaces[0].mode, "gateway");
592 }
593
594 #[test]
595 fn parse_interface_mode_takes_precedence() {
596 let input = r#"
598[interfaces]
599 [[TCP Client]]
600 type = TCPClientInterface
601 interface_mode = roaming
602 mode = boundary
603 target_host = 10.0.0.1
604 target_port = 4242
605"#;
606 let config = parse(input).unwrap();
607 assert_eq!(config.interfaces[0].mode, "roaming");
608 }
609
610 #[test]
611 fn parse_disabled_interface() {
612 let input = r#"
613[interfaces]
614 [[Disabled TCP]]
615 type = TCPClientInterface
616 enabled = No
617 target_host = 10.0.0.1
618 target_port = 4242
619"#;
620 let config = parse(input).unwrap();
621 assert_eq!(config.interfaces.len(), 1);
622 assert!(!config.interfaces[0].enabled);
623 }
624
625 #[test]
626 fn parse_serial_interface() {
627 let input = r#"
628[interfaces]
629 [[Serial Port]]
630 type = SerialInterface
631 enabled = Yes
632 port = /dev/ttyUSB0
633 speed = 115200
634 databits = 8
635 parity = N
636 stopbits = 1
637"#;
638 let config = parse(input).unwrap();
639 assert_eq!(config.interfaces.len(), 1);
640 let iface = &config.interfaces[0];
641 assert_eq!(iface.name, "Serial Port");
642 assert_eq!(iface.interface_type, "SerialInterface");
643 assert!(iface.enabled);
644 assert_eq!(iface.params.get("port").unwrap(), "/dev/ttyUSB0");
645 assert_eq!(iface.params.get("speed").unwrap(), "115200");
646 assert_eq!(iface.params.get("databits").unwrap(), "8");
647 assert_eq!(iface.params.get("parity").unwrap(), "N");
648 assert_eq!(iface.params.get("stopbits").unwrap(), "1");
649 }
650
651 #[test]
652 fn parse_kiss_interface() {
653 let input = r#"
654[interfaces]
655 [[KISS TNC]]
656 type = KISSInterface
657 enabled = Yes
658 port = /dev/ttyUSB1
659 speed = 9600
660 preamble = 350
661 txtail = 20
662 persistence = 64
663 slottime = 20
664 flow_control = True
665 id_interval = 600
666 id_callsign = MYCALL
667"#;
668 let config = parse(input).unwrap();
669 assert_eq!(config.interfaces.len(), 1);
670 let iface = &config.interfaces[0];
671 assert_eq!(iface.name, "KISS TNC");
672 assert_eq!(iface.interface_type, "KISSInterface");
673 assert_eq!(iface.params.get("port").unwrap(), "/dev/ttyUSB1");
674 assert_eq!(iface.params.get("speed").unwrap(), "9600");
675 assert_eq!(iface.params.get("preamble").unwrap(), "350");
676 assert_eq!(iface.params.get("txtail").unwrap(), "20");
677 assert_eq!(iface.params.get("persistence").unwrap(), "64");
678 assert_eq!(iface.params.get("slottime").unwrap(), "20");
679 assert_eq!(iface.params.get("flow_control").unwrap(), "True");
680 assert_eq!(iface.params.get("id_interval").unwrap(), "600");
681 assert_eq!(iface.params.get("id_callsign").unwrap(), "MYCALL");
682 }
683
684 #[test]
685 fn parse_ifac_networkname() {
686 let input = r#"
687[interfaces]
688 [[TCP Client]]
689 type = TCPClientInterface
690 target_host = 10.0.0.1
691 target_port = 4242
692 networkname = testnet
693"#;
694 let config = parse(input).unwrap();
695 assert_eq!(config.interfaces[0].params.get("networkname").unwrap(), "testnet");
696 }
697
698 #[test]
699 fn parse_ifac_passphrase() {
700 let input = r#"
701[interfaces]
702 [[TCP Client]]
703 type = TCPClientInterface
704 target_host = 10.0.0.1
705 target_port = 4242
706 passphrase = secret123
707 ifac_size = 64
708"#;
709 let config = parse(input).unwrap();
710 assert_eq!(config.interfaces[0].params.get("passphrase").unwrap(), "secret123");
711 assert_eq!(config.interfaces[0].params.get("ifac_size").unwrap(), "64");
712 }
713
714 #[test]
715 fn parse_remote_management_config() {
716 let input = r#"
717[reticulum]
718enable_transport = True
719enable_remote_management = Yes
720remote_management_allowed = aabbccdd00112233aabbccdd00112233, 11223344556677881122334455667788
721publish_blackhole = Yes
722"#;
723 let config = parse(input).unwrap();
724 assert!(config.reticulum.enable_remote_management);
725 assert!(config.reticulum.publish_blackhole);
726 assert_eq!(config.reticulum.remote_management_allowed.len(), 2);
727 assert_eq!(
728 config.reticulum.remote_management_allowed[0],
729 "aabbccdd00112233aabbccdd00112233"
730 );
731 assert_eq!(
732 config.reticulum.remote_management_allowed[1],
733 "11223344556677881122334455667788"
734 );
735 }
736
737 #[test]
738 fn parse_remote_management_defaults() {
739 let input = "[reticulum]\n";
740 let config = parse(input).unwrap();
741 assert!(!config.reticulum.enable_remote_management);
742 assert!(!config.reticulum.publish_blackhole);
743 assert!(config.reticulum.remote_management_allowed.is_empty());
744 }
745}