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