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 pub hooks: Vec<ParsedHook>,
19}
20
21#[derive(Debug, Clone)]
23pub struct ParsedHook {
24 pub name: String,
25 pub path: String,
26 pub attach_point: String,
27 pub priority: i32,
28 pub enabled: bool,
29}
30
31#[derive(Debug, Clone)]
33pub struct ReticulumSection {
34 pub enable_transport: bool,
35 pub share_instance: bool,
36 pub instance_name: String,
37 pub shared_instance_port: u16,
38 pub instance_control_port: u16,
39 pub panic_on_interface_error: bool,
40 pub use_implicit_proof: bool,
41 pub network_identity: Option<String>,
42 pub respond_to_probes: bool,
43 pub enable_remote_management: bool,
44 pub remote_management_allowed: Vec<String>,
45 pub publish_blackhole: bool,
46 pub probe_port: Option<u16>,
47 pub probe_addr: Option<String>,
48 pub device: Option<String>,
50 pub discover_interfaces: bool,
53 pub required_discovery_value: Option<u8>,
55}
56
57impl Default for ReticulumSection {
58 fn default() -> Self {
59 ReticulumSection {
60 enable_transport: false,
61 share_instance: true,
62 instance_name: "default".into(),
63 shared_instance_port: 37428,
64 instance_control_port: 37429,
65 panic_on_interface_error: false,
66 use_implicit_proof: true,
67 network_identity: None,
68 respond_to_probes: false,
69 enable_remote_management: false,
70 remote_management_allowed: Vec::new(),
71 publish_blackhole: false,
72 probe_port: None,
73 probe_addr: None,
74 device: None,
75 discover_interfaces: false,
76 required_discovery_value: None,
77 }
78 }
79}
80
81#[derive(Debug, Clone)]
83pub struct LoggingSection {
84 pub loglevel: u8,
85}
86
87impl Default for LoggingSection {
88 fn default() -> Self {
89 LoggingSection { loglevel: 4 }
90 }
91}
92
93#[derive(Debug, Clone)]
95pub struct ParsedInterface {
96 pub name: String,
97 pub interface_type: String,
98 pub enabled: bool,
99 pub mode: String,
100 pub params: HashMap<String, String>,
101}
102
103#[derive(Debug, Clone)]
105pub enum ConfigError {
106 Io(String),
107 Parse(String),
108 InvalidValue { key: String, value: String },
109}
110
111impl fmt::Display for ConfigError {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 match self {
114 ConfigError::Io(msg) => write!(f, "Config I/O error: {}", msg),
115 ConfigError::Parse(msg) => write!(f, "Config parse error: {}", msg),
116 ConfigError::InvalidValue { key, value } => {
117 write!(f, "Invalid value for '{}': '{}'", key, value)
118 }
119 }
120 }
121}
122
123impl From<io::Error> for ConfigError {
124 fn from(e: io::Error) -> Self {
125 ConfigError::Io(e.to_string())
126 }
127}
128
129pub fn parse(input: &str) -> Result<RnsConfig, ConfigError> {
131 let mut current_section: Option<String> = None;
132 let mut current_subsection: Option<String> = None;
133
134 let mut reticulum_kvs: HashMap<String, String> = HashMap::new();
135 let mut logging_kvs: HashMap<String, String> = HashMap::new();
136 let mut interfaces: Vec<ParsedInterface> = Vec::new();
137 let mut current_iface_kvs: Option<HashMap<String, String>> = None;
138 let mut current_iface_name: Option<String> = None;
139 let mut hooks: Vec<ParsedHook> = Vec::new();
140 let mut current_hook_kvs: Option<HashMap<String, String>> = None;
141 let mut current_hook_name: Option<String> = None;
142
143 for line in input.lines() {
144 let line = strip_comment(line);
146 let trimmed = line.trim();
147
148 if trimmed.is_empty() {
150 continue;
151 }
152
153 if trimmed.starts_with("[[") && trimmed.ends_with("]]") {
155 let name = trimmed[2..trimmed.len() - 2].trim().to_string();
156 if let (Some(iface_name), Some(kvs)) =
158 (current_iface_name.take(), current_iface_kvs.take())
159 {
160 interfaces.push(build_parsed_interface(iface_name, kvs));
161 }
162 if let (Some(hook_name), Some(kvs)) =
164 (current_hook_name.take(), current_hook_kvs.take())
165 {
166 hooks.push(build_parsed_hook(hook_name, kvs));
167 }
168 current_subsection = Some(name.clone());
169 if current_section.as_deref() == Some("hooks") {
171 current_hook_name = Some(name);
172 current_hook_kvs = Some(HashMap::new());
173 } else {
174 current_iface_name = Some(name);
175 current_iface_kvs = Some(HashMap::new());
176 }
177 continue;
178 }
179
180 if trimmed.starts_with('[') && trimmed.ends_with(']') {
182 if let (Some(iface_name), Some(kvs)) =
184 (current_iface_name.take(), current_iface_kvs.take())
185 {
186 interfaces.push(build_parsed_interface(iface_name, kvs));
187 }
188 if let (Some(hook_name), Some(kvs)) =
190 (current_hook_name.take(), current_hook_kvs.take())
191 {
192 hooks.push(build_parsed_hook(hook_name, kvs));
193 }
194 current_subsection = None;
195
196 let name = trimmed[1..trimmed.len() - 1].trim().to_lowercase();
197 current_section = Some(name);
198 continue;
199 }
200
201 if let Some(eq_pos) = trimmed.find('=') {
203 let key = trimmed[..eq_pos].trim().to_string();
204 let value = trimmed[eq_pos + 1..].trim().to_string();
205
206 if current_subsection.is_some() {
207 debug_assert!(
209 !(current_hook_kvs.is_some() && current_iface_kvs.is_some()),
210 "hook and interface subsections should never be active simultaneously"
211 );
212 if let Some(ref mut kvs) = current_hook_kvs {
213 kvs.insert(key, value);
214 } else if let Some(ref mut kvs) = current_iface_kvs {
215 kvs.insert(key, value);
216 }
217 } else if let Some(ref section) = current_section {
218 match section.as_str() {
219 "reticulum" => {
220 reticulum_kvs.insert(key, value);
221 }
222 "logging" => {
223 logging_kvs.insert(key, value);
224 }
225 _ => {} }
227 }
228 }
229 }
230
231 if let (Some(iface_name), Some(kvs)) = (current_iface_name.take(), current_iface_kvs.take()) {
233 interfaces.push(build_parsed_interface(iface_name, kvs));
234 }
235 if let (Some(hook_name), Some(kvs)) = (current_hook_name.take(), current_hook_kvs.take()) {
236 hooks.push(build_parsed_hook(hook_name, kvs));
237 }
238
239 let reticulum = build_reticulum_section(&reticulum_kvs)?;
241 let logging = build_logging_section(&logging_kvs)?;
242
243 Ok(RnsConfig {
244 reticulum,
245 logging,
246 interfaces,
247 hooks,
248 })
249}
250
251pub fn parse_file(path: &Path) -> Result<RnsConfig, ConfigError> {
253 let content = std::fs::read_to_string(path)?;
254 parse(&content)
255}
256
257fn strip_comment(line: &str) -> &str {
259 let mut in_quote = false;
261 let mut quote_char = '"';
262 for (i, ch) in line.char_indices() {
263 if !in_quote && (ch == '"' || ch == '\'') {
264 in_quote = true;
265 quote_char = ch;
266 } else if in_quote && ch == quote_char {
267 in_quote = false;
268 } else if !in_quote && ch == '#' {
269 return &line[..i];
270 }
271 }
272 line
273}
274
275pub fn parse_bool_pub(value: &str) -> Option<bool> {
277 parse_bool(value)
278}
279
280fn parse_bool(value: &str) -> Option<bool> {
282 match value.to_lowercase().as_str() {
283 "yes" | "true" | "1" | "on" => Some(true),
284 "no" | "false" | "0" | "off" => Some(false),
285 _ => None,
286 }
287}
288
289fn build_parsed_interface(name: String, mut kvs: HashMap<String, String>) -> ParsedInterface {
290 let interface_type = kvs.remove("type").unwrap_or_default();
291 let enabled = kvs
292 .remove("enabled")
293 .and_then(|v| parse_bool(&v))
294 .unwrap_or(true);
295 let mode = kvs
297 .remove("interface_mode")
298 .or_else(|| kvs.remove("mode"))
299 .unwrap_or_else(|| "full".into());
300
301 ParsedInterface {
302 name,
303 interface_type,
304 enabled,
305 mode,
306 params: kvs,
307 }
308}
309
310fn build_parsed_hook(name: String, mut kvs: HashMap<String, String>) -> ParsedHook {
311 let path = kvs.remove("path").unwrap_or_default();
312 let attach_point = kvs.remove("attach_point").unwrap_or_default();
313 let priority = kvs
314 .remove("priority")
315 .and_then(|v| v.parse::<i32>().ok())
316 .unwrap_or(0);
317 let enabled = kvs
318 .remove("enabled")
319 .and_then(|v| parse_bool(&v))
320 .unwrap_or(true);
321
322 ParsedHook {
323 name,
324 path,
325 attach_point,
326 priority,
327 enabled,
328 }
329}
330
331pub fn parse_hook_point(s: &str) -> Option<usize> {
333 match s {
334 "PreIngress" => Some(0),
335 "PreDispatch" => Some(1),
336 "AnnounceReceived" => Some(2),
337 "PathUpdated" => Some(3),
338 "AnnounceRetransmit" => Some(4),
339 "LinkRequestReceived" => Some(5),
340 "LinkEstablished" => Some(6),
341 "LinkClosed" => Some(7),
342 "InterfaceUp" => Some(8),
343 "InterfaceDown" => Some(9),
344 "InterfaceConfigChanged" => Some(10),
345 "SendOnInterface" => Some(11),
346 "BroadcastOnAllInterfaces" => Some(12),
347 "DeliverLocal" => Some(13),
348 "TunnelSynthesize" => Some(14),
349 "Tick" => Some(15),
350 _ => None,
351 }
352}
353
354fn build_reticulum_section(
355 kvs: &HashMap<String, String>,
356) -> Result<ReticulumSection, ConfigError> {
357 let mut section = ReticulumSection::default();
358
359 if let Some(v) = kvs.get("enable_transport") {
360 section.enable_transport = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
361 key: "enable_transport".into(),
362 value: v.clone(),
363 })?;
364 }
365 if let Some(v) = kvs.get("share_instance") {
366 section.share_instance = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
367 key: "share_instance".into(),
368 value: v.clone(),
369 })?;
370 }
371 if let Some(v) = kvs.get("instance_name") {
372 section.instance_name = v.clone();
373 }
374 if let Some(v) = kvs.get("shared_instance_port") {
375 section.shared_instance_port =
376 v.parse::<u16>().map_err(|_| ConfigError::InvalidValue {
377 key: "shared_instance_port".into(),
378 value: v.clone(),
379 })?;
380 }
381 if let Some(v) = kvs.get("instance_control_port") {
382 section.instance_control_port =
383 v.parse::<u16>().map_err(|_| ConfigError::InvalidValue {
384 key: "instance_control_port".into(),
385 value: v.clone(),
386 })?;
387 }
388 if let Some(v) = kvs.get("panic_on_interface_error") {
389 section.panic_on_interface_error =
390 parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
391 key: "panic_on_interface_error".into(),
392 value: v.clone(),
393 })?;
394 }
395 if let Some(v) = kvs.get("use_implicit_proof") {
396 section.use_implicit_proof = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
397 key: "use_implicit_proof".into(),
398 value: v.clone(),
399 })?;
400 }
401 if let Some(v) = kvs.get("network_identity") {
402 section.network_identity = Some(v.clone());
403 }
404 if let Some(v) = kvs.get("respond_to_probes") {
405 section.respond_to_probes = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
406 key: "respond_to_probes".into(),
407 value: v.clone(),
408 })?;
409 }
410 if let Some(v) = kvs.get("enable_remote_management") {
411 section.enable_remote_management = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
412 key: "enable_remote_management".into(),
413 value: v.clone(),
414 })?;
415 }
416 if let Some(v) = kvs.get("remote_management_allowed") {
417 for item in v.split(',') {
419 let trimmed = item.trim();
420 if !trimmed.is_empty() {
421 section.remote_management_allowed.push(trimmed.to_string());
422 }
423 }
424 }
425 if let Some(v) = kvs.get("publish_blackhole") {
426 section.publish_blackhole = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
427 key: "publish_blackhole".into(),
428 value: v.clone(),
429 })?;
430 }
431 if let Some(v) = kvs.get("probe_port") {
432 section.probe_port = Some(v.parse::<u16>().map_err(|_| ConfigError::InvalidValue {
433 key: "probe_port".into(),
434 value: v.clone(),
435 })?);
436 }
437 if let Some(v) = kvs.get("probe_addr") {
438 section.probe_addr = Some(v.clone());
439 }
440 if let Some(v) = kvs.get("device") {
441 section.device = Some(v.clone());
442 }
443 if let Some(v) = kvs.get("discover_interfaces") {
444 section.discover_interfaces = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
445 key: "discover_interfaces".into(),
446 value: v.clone(),
447 })?;
448 }
449 if let Some(v) = kvs.get("required_discovery_value") {
450 section.required_discovery_value = Some(v.parse::<u8>().map_err(|_| ConfigError::InvalidValue {
451 key: "required_discovery_value".into(),
452 value: v.clone(),
453 })?);
454 }
455
456 Ok(section)
457}
458
459fn build_logging_section(kvs: &HashMap<String, String>) -> Result<LoggingSection, ConfigError> {
460 let mut section = LoggingSection::default();
461
462 if let Some(v) = kvs.get("loglevel") {
463 section.loglevel = v.parse::<u8>().map_err(|_| ConfigError::InvalidValue {
464 key: "loglevel".into(),
465 value: v.clone(),
466 })?;
467 }
468
469 Ok(section)
470}
471
472#[cfg(test)]
473mod tests {
474 use super::*;
475
476 #[test]
477 fn parse_empty() {
478 let config = parse("").unwrap();
479 assert!(!config.reticulum.enable_transport);
480 assert!(config.reticulum.share_instance);
481 assert_eq!(config.reticulum.instance_name, "default");
482 assert_eq!(config.logging.loglevel, 4);
483 assert!(config.interfaces.is_empty());
484 }
485
486 #[test]
487 fn parse_default_config() {
488 let input = r#"
490[reticulum]
491enable_transport = False
492share_instance = Yes
493instance_name = default
494
495[logging]
496loglevel = 4
497
498[interfaces]
499
500 [[Default Interface]]
501 type = AutoInterface
502 enabled = Yes
503"#;
504 let config = parse(input).unwrap();
505 assert!(!config.reticulum.enable_transport);
506 assert!(config.reticulum.share_instance);
507 assert_eq!(config.reticulum.instance_name, "default");
508 assert_eq!(config.logging.loglevel, 4);
509 assert_eq!(config.interfaces.len(), 1);
510 assert_eq!(config.interfaces[0].name, "Default Interface");
511 assert_eq!(config.interfaces[0].interface_type, "AutoInterface");
512 assert!(config.interfaces[0].enabled);
513 }
514
515 #[test]
516 fn parse_reticulum_section() {
517 let input = r#"
518[reticulum]
519enable_transport = True
520share_instance = No
521instance_name = mynode
522shared_instance_port = 12345
523instance_control_port = 12346
524panic_on_interface_error = Yes
525use_implicit_proof = False
526respond_to_probes = True
527network_identity = /home/user/.reticulum/identity
528"#;
529 let config = parse(input).unwrap();
530 assert!(config.reticulum.enable_transport);
531 assert!(!config.reticulum.share_instance);
532 assert_eq!(config.reticulum.instance_name, "mynode");
533 assert_eq!(config.reticulum.shared_instance_port, 12345);
534 assert_eq!(config.reticulum.instance_control_port, 12346);
535 assert!(config.reticulum.panic_on_interface_error);
536 assert!(!config.reticulum.use_implicit_proof);
537 assert!(config.reticulum.respond_to_probes);
538 assert_eq!(
539 config.reticulum.network_identity.as_deref(),
540 Some("/home/user/.reticulum/identity")
541 );
542 }
543
544 #[test]
545 fn parse_logging_section() {
546 let input = "[logging]\nloglevel = 6\n";
547 let config = parse(input).unwrap();
548 assert_eq!(config.logging.loglevel, 6);
549 }
550
551 #[test]
552 fn parse_interface_tcp_client() {
553 let input = r#"
554[interfaces]
555 [[TCP Client]]
556 type = TCPClientInterface
557 enabled = Yes
558 target_host = 87.106.8.245
559 target_port = 4242
560"#;
561 let config = parse(input).unwrap();
562 assert_eq!(config.interfaces.len(), 1);
563 let iface = &config.interfaces[0];
564 assert_eq!(iface.name, "TCP Client");
565 assert_eq!(iface.interface_type, "TCPClientInterface");
566 assert!(iface.enabled);
567 assert_eq!(iface.params.get("target_host").unwrap(), "87.106.8.245");
568 assert_eq!(iface.params.get("target_port").unwrap(), "4242");
569 }
570
571 #[test]
572 fn parse_interface_tcp_server() {
573 let input = r#"
574[interfaces]
575 [[TCP Server]]
576 type = TCPServerInterface
577 enabled = Yes
578 listen_ip = 0.0.0.0
579 listen_port = 4242
580"#;
581 let config = parse(input).unwrap();
582 assert_eq!(config.interfaces.len(), 1);
583 let iface = &config.interfaces[0];
584 assert_eq!(iface.name, "TCP Server");
585 assert_eq!(iface.interface_type, "TCPServerInterface");
586 assert_eq!(iface.params.get("listen_ip").unwrap(), "0.0.0.0");
587 assert_eq!(iface.params.get("listen_port").unwrap(), "4242");
588 }
589
590 #[test]
591 fn parse_interface_udp() {
592 let input = r#"
593[interfaces]
594 [[UDP Interface]]
595 type = UDPInterface
596 enabled = Yes
597 listen_ip = 0.0.0.0
598 listen_port = 4242
599 forward_ip = 255.255.255.255
600 forward_port = 4242
601"#;
602 let config = parse(input).unwrap();
603 assert_eq!(config.interfaces.len(), 1);
604 let iface = &config.interfaces[0];
605 assert_eq!(iface.name, "UDP Interface");
606 assert_eq!(iface.interface_type, "UDPInterface");
607 assert_eq!(iface.params.get("listen_ip").unwrap(), "0.0.0.0");
608 assert_eq!(iface.params.get("forward_ip").unwrap(), "255.255.255.255");
609 }
610
611 #[test]
612 fn parse_multiple_interfaces() {
613 let input = r#"
614[interfaces]
615 [[TCP Client]]
616 type = TCPClientInterface
617 target_host = 10.0.0.1
618 target_port = 4242
619
620 [[UDP Broadcast]]
621 type = UDPInterface
622 listen_ip = 0.0.0.0
623 listen_port = 5555
624 forward_ip = 255.255.255.255
625 forward_port = 5555
626"#;
627 let config = parse(input).unwrap();
628 assert_eq!(config.interfaces.len(), 2);
629 assert_eq!(config.interfaces[0].name, "TCP Client");
630 assert_eq!(config.interfaces[0].interface_type, "TCPClientInterface");
631 assert_eq!(config.interfaces[1].name, "UDP Broadcast");
632 assert_eq!(config.interfaces[1].interface_type, "UDPInterface");
633 }
634
635 #[test]
636 fn parse_booleans() {
637 for (input, expected) in &[
639 ("Yes", true),
640 ("No", false),
641 ("True", true),
642 ("False", false),
643 ("true", true),
644 ("false", false),
645 ("1", true),
646 ("0", false),
647 ("on", true),
648 ("off", false),
649 ] {
650 let result = parse_bool(input);
651 assert_eq!(result, Some(*expected), "parse_bool({}) failed", input);
652 }
653 }
654
655 #[test]
656 fn parse_comments() {
657 let input = r#"
658# This is a comment
659[reticulum]
660enable_transport = True # inline comment
661# share_instance = No
662instance_name = test
663"#;
664 let config = parse(input).unwrap();
665 assert!(config.reticulum.enable_transport);
666 assert!(config.reticulum.share_instance); assert_eq!(config.reticulum.instance_name, "test");
668 }
669
670 #[test]
671 fn parse_interface_mode_field() {
672 let input = r#"
673[interfaces]
674 [[TCP Client]]
675 type = TCPClientInterface
676 interface_mode = access_point
677 target_host = 10.0.0.1
678 target_port = 4242
679"#;
680 let config = parse(input).unwrap();
681 assert_eq!(config.interfaces[0].mode, "access_point");
682 }
683
684 #[test]
685 fn parse_mode_fallback() {
686 let input = r#"
688[interfaces]
689 [[TCP Client]]
690 type = TCPClientInterface
691 mode = gateway
692 target_host = 10.0.0.1
693 target_port = 4242
694"#;
695 let config = parse(input).unwrap();
696 assert_eq!(config.interfaces[0].mode, "gateway");
697 }
698
699 #[test]
700 fn parse_interface_mode_takes_precedence() {
701 let input = r#"
703[interfaces]
704 [[TCP Client]]
705 type = TCPClientInterface
706 interface_mode = roaming
707 mode = boundary
708 target_host = 10.0.0.1
709 target_port = 4242
710"#;
711 let config = parse(input).unwrap();
712 assert_eq!(config.interfaces[0].mode, "roaming");
713 }
714
715 #[test]
716 fn parse_disabled_interface() {
717 let input = r#"
718[interfaces]
719 [[Disabled TCP]]
720 type = TCPClientInterface
721 enabled = No
722 target_host = 10.0.0.1
723 target_port = 4242
724"#;
725 let config = parse(input).unwrap();
726 assert_eq!(config.interfaces.len(), 1);
727 assert!(!config.interfaces[0].enabled);
728 }
729
730 #[test]
731 fn parse_serial_interface() {
732 let input = r#"
733[interfaces]
734 [[Serial Port]]
735 type = SerialInterface
736 enabled = Yes
737 port = /dev/ttyUSB0
738 speed = 115200
739 databits = 8
740 parity = N
741 stopbits = 1
742"#;
743 let config = parse(input).unwrap();
744 assert_eq!(config.interfaces.len(), 1);
745 let iface = &config.interfaces[0];
746 assert_eq!(iface.name, "Serial Port");
747 assert_eq!(iface.interface_type, "SerialInterface");
748 assert!(iface.enabled);
749 assert_eq!(iface.params.get("port").unwrap(), "/dev/ttyUSB0");
750 assert_eq!(iface.params.get("speed").unwrap(), "115200");
751 assert_eq!(iface.params.get("databits").unwrap(), "8");
752 assert_eq!(iface.params.get("parity").unwrap(), "N");
753 assert_eq!(iface.params.get("stopbits").unwrap(), "1");
754 }
755
756 #[test]
757 fn parse_kiss_interface() {
758 let input = r#"
759[interfaces]
760 [[KISS TNC]]
761 type = KISSInterface
762 enabled = Yes
763 port = /dev/ttyUSB1
764 speed = 9600
765 preamble = 350
766 txtail = 20
767 persistence = 64
768 slottime = 20
769 flow_control = True
770 id_interval = 600
771 id_callsign = MYCALL
772"#;
773 let config = parse(input).unwrap();
774 assert_eq!(config.interfaces.len(), 1);
775 let iface = &config.interfaces[0];
776 assert_eq!(iface.name, "KISS TNC");
777 assert_eq!(iface.interface_type, "KISSInterface");
778 assert_eq!(iface.params.get("port").unwrap(), "/dev/ttyUSB1");
779 assert_eq!(iface.params.get("speed").unwrap(), "9600");
780 assert_eq!(iface.params.get("preamble").unwrap(), "350");
781 assert_eq!(iface.params.get("txtail").unwrap(), "20");
782 assert_eq!(iface.params.get("persistence").unwrap(), "64");
783 assert_eq!(iface.params.get("slottime").unwrap(), "20");
784 assert_eq!(iface.params.get("flow_control").unwrap(), "True");
785 assert_eq!(iface.params.get("id_interval").unwrap(), "600");
786 assert_eq!(iface.params.get("id_callsign").unwrap(), "MYCALL");
787 }
788
789 #[test]
790 fn parse_ifac_networkname() {
791 let input = r#"
792[interfaces]
793 [[TCP Client]]
794 type = TCPClientInterface
795 target_host = 10.0.0.1
796 target_port = 4242
797 networkname = testnet
798"#;
799 let config = parse(input).unwrap();
800 assert_eq!(config.interfaces[0].params.get("networkname").unwrap(), "testnet");
801 }
802
803 #[test]
804 fn parse_ifac_passphrase() {
805 let input = r#"
806[interfaces]
807 [[TCP Client]]
808 type = TCPClientInterface
809 target_host = 10.0.0.1
810 target_port = 4242
811 passphrase = secret123
812 ifac_size = 64
813"#;
814 let config = parse(input).unwrap();
815 assert_eq!(config.interfaces[0].params.get("passphrase").unwrap(), "secret123");
816 assert_eq!(config.interfaces[0].params.get("ifac_size").unwrap(), "64");
817 }
818
819 #[test]
820 fn parse_remote_management_config() {
821 let input = r#"
822[reticulum]
823enable_transport = True
824enable_remote_management = Yes
825remote_management_allowed = aabbccdd00112233aabbccdd00112233, 11223344556677881122334455667788
826publish_blackhole = Yes
827"#;
828 let config = parse(input).unwrap();
829 assert!(config.reticulum.enable_remote_management);
830 assert!(config.reticulum.publish_blackhole);
831 assert_eq!(config.reticulum.remote_management_allowed.len(), 2);
832 assert_eq!(
833 config.reticulum.remote_management_allowed[0],
834 "aabbccdd00112233aabbccdd00112233"
835 );
836 assert_eq!(
837 config.reticulum.remote_management_allowed[1],
838 "11223344556677881122334455667788"
839 );
840 }
841
842 #[test]
843 fn parse_remote_management_defaults() {
844 let input = "[reticulum]\n";
845 let config = parse(input).unwrap();
846 assert!(!config.reticulum.enable_remote_management);
847 assert!(!config.reticulum.publish_blackhole);
848 assert!(config.reticulum.remote_management_allowed.is_empty());
849 }
850
851 #[test]
852 fn parse_hooks_section() {
853 let input = r#"
854[hooks]
855 [[drop_tick]]
856 path = /tmp/drop_tick.wasm
857 attach_point = Tick
858 priority = 10
859 enabled = Yes
860
861 [[log_announce]]
862 path = /tmp/log_announce.wasm
863 attach_point = AnnounceReceived
864 priority = 5
865 enabled = No
866"#;
867 let config = parse(input).unwrap();
868 assert_eq!(config.hooks.len(), 2);
869 assert_eq!(config.hooks[0].name, "drop_tick");
870 assert_eq!(config.hooks[0].path, "/tmp/drop_tick.wasm");
871 assert_eq!(config.hooks[0].attach_point, "Tick");
872 assert_eq!(config.hooks[0].priority, 10);
873 assert!(config.hooks[0].enabled);
874 assert_eq!(config.hooks[1].name, "log_announce");
875 assert_eq!(config.hooks[1].attach_point, "AnnounceReceived");
876 assert!(!config.hooks[1].enabled);
877 }
878
879 #[test]
880 fn parse_empty_hooks() {
881 let input = "[hooks]\n";
882 let config = parse(input).unwrap();
883 assert!(config.hooks.is_empty());
884 }
885
886 #[test]
887 fn parse_hook_point_names() {
888 assert_eq!(parse_hook_point("PreIngress"), Some(0));
889 assert_eq!(parse_hook_point("PreDispatch"), Some(1));
890 assert_eq!(parse_hook_point("AnnounceReceived"), Some(2));
891 assert_eq!(parse_hook_point("PathUpdated"), Some(3));
892 assert_eq!(parse_hook_point("AnnounceRetransmit"), Some(4));
893 assert_eq!(parse_hook_point("LinkRequestReceived"), Some(5));
894 assert_eq!(parse_hook_point("LinkEstablished"), Some(6));
895 assert_eq!(parse_hook_point("LinkClosed"), Some(7));
896 assert_eq!(parse_hook_point("InterfaceUp"), Some(8));
897 assert_eq!(parse_hook_point("InterfaceDown"), Some(9));
898 assert_eq!(parse_hook_point("InterfaceConfigChanged"), Some(10));
899 assert_eq!(parse_hook_point("SendOnInterface"), Some(11));
900 assert_eq!(parse_hook_point("BroadcastOnAllInterfaces"), Some(12));
901 assert_eq!(parse_hook_point("DeliverLocal"), Some(13));
902 assert_eq!(parse_hook_point("TunnelSynthesize"), Some(14));
903 assert_eq!(parse_hook_point("Tick"), Some(15));
904 assert_eq!(parse_hook_point("Unknown"), None);
905 }
906
907 #[test]
908 fn backbone_extra_params_preserved() {
909 let config = r#"
910[reticulum]
911enable_transport = True
912
913[interfaces]
914 [[Public Entrypoint]]
915 type = BackboneInterface
916 enabled = yes
917 listen_ip = 0.0.0.0
918 listen_port = 4242
919 interface_mode = gateway
920 discoverable = Yes
921 discovery_name = PizzaSpaghettiMandolino
922 announce_interval = 600
923 discovery_stamp_value = 24
924 reachable_on = 87.106.8.245
925"#;
926 let parsed = parse(config).unwrap();
927 assert_eq!(parsed.interfaces.len(), 1);
928 let iface = &parsed.interfaces[0];
929 assert_eq!(iface.name, "Public Entrypoint");
930 assert_eq!(iface.interface_type, "BackboneInterface");
931 assert_eq!(iface.params.get("discoverable").map(|s| s.as_str()), Some("Yes"));
933 assert_eq!(iface.params.get("discovery_name").map(|s| s.as_str()), Some("PizzaSpaghettiMandolino"));
934 assert_eq!(iface.params.get("announce_interval").map(|s| s.as_str()), Some("600"));
935 assert_eq!(iface.params.get("discovery_stamp_value").map(|s| s.as_str()), Some("24"));
936 assert_eq!(iface.params.get("reachable_on").map(|s| s.as_str()), Some("87.106.8.245"));
937 assert_eq!(iface.params.get("listen_ip").map(|s| s.as_str()), Some("0.0.0.0"));
938 assert_eq!(iface.params.get("listen_port").map(|s| s.as_str()), Some("4242"));
939 }
940}