Skip to main content

rns_net/common/
config.rs

1//! ConfigObj parser for RNS config files.
2//!
3//! Python RNS uses ConfigObj format — NOT TOML, NOT standard INI.
4//! Key differences: nested `[[sections]]`, booleans `Yes`/`No`/`True`/`False`,
5//! comments with `#`, unquoted string values.
6
7use std::collections::HashMap;
8use std::fmt;
9use std::io;
10use std::path::Path;
11
12/// Parsed RNS configuration.
13#[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/// A parsed hook from `[[subsection]]` within `[hooks]`.
22#[derive(Debug, Clone)]
23pub struct ParsedHook {
24    pub name: String,
25    pub path: String,
26    pub hook_type: String,
27    pub builtin_id: Option<String>,
28    pub attach_point: String,
29    pub priority: i32,
30    pub enabled: bool,
31}
32
33/// The `[reticulum]` section.
34#[derive(Debug, Clone)]
35pub struct ReticulumSection {
36    pub enable_transport: bool,
37    pub share_instance: bool,
38    pub instance_name: String,
39    pub shared_instance_port: u16,
40    pub instance_control_port: u16,
41    pub panic_on_interface_error: bool,
42    pub use_implicit_proof: bool,
43    pub network_identity: Option<String>,
44    pub respond_to_probes: bool,
45    pub enable_remote_management: bool,
46    pub remote_management_allowed: Vec<String>,
47    pub publish_blackhole: bool,
48    pub probe_port: Option<u16>,
49    pub probe_addr: Option<String>,
50    /// Protocol for endpoint discovery: "rnsp" (default) or "stun".
51    pub probe_protocol: Option<String>,
52    /// Network interface to bind outbound sockets to (e.g. "usb0").
53    pub device: Option<String>,
54    /// Enable interface discovery (advertise discoverable interfaces and
55    /// listen for discovery announces from the network).
56    pub discover_interfaces: bool,
57    /// Minimum stamp value for accepting discovered interfaces.
58    pub required_discovery_value: Option<u8>,
59    /// Accept an announce with strictly fewer hops even when the random_blob
60    /// is a duplicate of the existing path entry.
61    pub prefer_shorter_path: bool,
62    /// Maximum number of alternative paths stored per destination.
63    /// Default 1 (single path, backward-compatible).
64    pub max_paths_per_destination: usize,
65    /// Maximum number of packet hashes retained for duplicate suppression.
66    pub packet_hashlist_max_entries: usize,
67    /// Maximum number of discovery path-request tags remembered.
68    pub max_discovery_pr_tags: usize,
69    /// Maximum number of destinations retained in the live path table.
70    pub max_path_destinations: usize,
71    /// Maximum number of destinations retained across tunnel-known paths.
72    pub max_tunnel_destinations_total: usize,
73    /// TTL for recalled known destinations without an active path, in seconds.
74    pub known_destinations_ttl: u64,
75    /// Maximum number of recalled known destinations retained.
76    pub known_destinations_max_entries: usize,
77    /// TTL for received ratchets, in seconds.
78    pub ratchet_expiry: u64,
79    /// TTL for announce retransmission state, in seconds.
80    pub announce_table_ttl: u64,
81    /// Maximum retained bytes for announce retransmission state.
82    pub announce_table_max_bytes: usize,
83    /// Whether the announce signature verification cache is enabled.
84    pub announce_sig_cache_enabled: bool,
85    /// Maximum entries in the announce signature verification cache.
86    pub announce_sig_cache_max_entries: usize,
87    /// TTL for announce signature cache entries, in seconds.
88    pub announce_sig_cache_ttl: u64,
89    /// Maximum entries in the async announce verification queue.
90    pub announce_queue_max_entries: usize,
91    /// Maximum interface-scoped announce queues retained.
92    pub announce_queue_max_interfaces: usize,
93    /// Maximum retained bytes in the async announce verification queue.
94    pub announce_queue_max_bytes: usize,
95    /// TTL for queued async announce verification entries, in seconds.
96    pub announce_queue_ttl: u64,
97    /// Overflow policy for the async announce verification queue.
98    pub announce_queue_overflow_policy: String,
99    /// Maximum queued events awaiting driver processing.
100    pub driver_event_queue_capacity: usize,
101    /// Maximum queued outbound frames per interface writer worker.
102    pub interface_writer_queue_capacity: usize,
103    /// Maximum active outbound Backbone peer-pool connections. Zero disables pooling.
104    pub backbone_peer_pool_max_connected: usize,
105    /// Failures within the failure window before a pooled Backbone peer enters cooldown.
106    pub backbone_peer_pool_failure_threshold: usize,
107    /// Failure accounting window for pooled Backbone peers, in seconds.
108    pub backbone_peer_pool_failure_window: u64,
109    /// Cooldown duration for failed pooled Backbone peers, in seconds.
110    pub backbone_peer_pool_cooldown: u64,
111    #[cfg(feature = "hooks")]
112    pub provider_bridge: bool,
113    #[cfg(feature = "hooks")]
114    pub provider_socket_path: Option<String>,
115    #[cfg(feature = "hooks")]
116    pub provider_queue_max_events: usize,
117    #[cfg(feature = "hooks")]
118    pub provider_queue_max_bytes: usize,
119    #[cfg(feature = "hooks")]
120    pub provider_overflow_policy: String,
121}
122
123impl Default for ReticulumSection {
124    fn default() -> Self {
125        ReticulumSection {
126            enable_transport: false,
127            share_instance: true,
128            instance_name: "default".into(),
129            shared_instance_port: 37428,
130            instance_control_port: 37429,
131            panic_on_interface_error: false,
132            use_implicit_proof: true,
133            network_identity: None,
134            respond_to_probes: false,
135            enable_remote_management: false,
136            remote_management_allowed: Vec::new(),
137            publish_blackhole: false,
138            probe_port: None,
139            probe_addr: None,
140            probe_protocol: None,
141            device: None,
142            discover_interfaces: false,
143            required_discovery_value: None,
144            prefer_shorter_path: false,
145            max_paths_per_destination: 1,
146            packet_hashlist_max_entries: rns_core::constants::HASHLIST_MAXSIZE,
147            max_discovery_pr_tags: rns_core::constants::MAX_PR_TAGS,
148            max_path_destinations: rns_core::transport::types::DEFAULT_MAX_PATH_DESTINATIONS,
149            max_tunnel_destinations_total: usize::MAX,
150            known_destinations_ttl: 48 * 60 * 60,
151            known_destinations_max_entries: 8192,
152            ratchet_expiry: rns_core::constants::RATCHET_EXPIRY,
153            announce_table_ttl: rns_core::constants::ANNOUNCE_TABLE_TTL as u64,
154            announce_table_max_bytes: rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES,
155            announce_sig_cache_enabled: true,
156            announce_sig_cache_max_entries: rns_core::constants::ANNOUNCE_SIG_CACHE_MAXSIZE,
157            announce_sig_cache_ttl: rns_core::constants::ANNOUNCE_SIG_CACHE_TTL as u64,
158            announce_queue_max_entries: 256,
159            announce_queue_max_interfaces: 1024,
160            announce_queue_max_bytes: 256 * 1024,
161            announce_queue_ttl: 30,
162            announce_queue_overflow_policy: "drop_worst".into(),
163            driver_event_queue_capacity: crate::event::DEFAULT_EVENT_QUEUE_CAPACITY,
164            interface_writer_queue_capacity: crate::interface::DEFAULT_ASYNC_WRITER_QUEUE_CAPACITY,
165            backbone_peer_pool_max_connected: 0,
166            backbone_peer_pool_failure_threshold: 3,
167            backbone_peer_pool_failure_window: 600,
168            backbone_peer_pool_cooldown: 900,
169            #[cfg(feature = "hooks")]
170            provider_bridge: false,
171            #[cfg(feature = "hooks")]
172            provider_socket_path: None,
173            #[cfg(feature = "hooks")]
174            provider_queue_max_events: 16384,
175            #[cfg(feature = "hooks")]
176            provider_queue_max_bytes: 8 * 1024 * 1024,
177            #[cfg(feature = "hooks")]
178            provider_overflow_policy: "drop_newest".into(),
179        }
180    }
181}
182
183/// The `[logging]` section.
184#[derive(Debug, Clone)]
185pub struct LoggingSection {
186    pub loglevel: u8,
187}
188
189impl Default for LoggingSection {
190    fn default() -> Self {
191        LoggingSection { loglevel: 4 }
192    }
193}
194
195/// A parsed interface from `[[subsection]]` within `[interfaces]`.
196#[derive(Debug, Clone)]
197pub struct ParsedInterface {
198    pub name: String,
199    pub interface_type: String,
200    pub enabled: bool,
201    pub mode: String,
202    pub params: HashMap<String, String>,
203}
204
205/// Configuration parse error.
206#[derive(Debug, Clone)]
207pub enum ConfigError {
208    Io(String),
209    Parse(String),
210    InvalidValue { key: String, value: String },
211}
212
213impl fmt::Display for ConfigError {
214    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215        match self {
216            ConfigError::Io(msg) => write!(f, "Config I/O error: {}", msg),
217            ConfigError::Parse(msg) => write!(f, "Config parse error: {}", msg),
218            ConfigError::InvalidValue { key, value } => {
219                write!(f, "Invalid value for '{}': '{}'", key, value)
220            }
221        }
222    }
223}
224
225impl From<io::Error> for ConfigError {
226    fn from(e: io::Error) -> Self {
227        ConfigError::Io(e.to_string())
228    }
229}
230
231/// Parse a config string into an `RnsConfig`.
232pub fn parse(input: &str) -> Result<RnsConfig, ConfigError> {
233    let mut current_section: Option<String> = None;
234    let mut current_subsection: Option<String> = None;
235
236    let mut reticulum_kvs: HashMap<String, String> = HashMap::new();
237    let mut logging_kvs: HashMap<String, String> = HashMap::new();
238    let mut interfaces: Vec<ParsedInterface> = Vec::new();
239    let mut current_iface_kvs: Option<HashMap<String, String>> = None;
240    let mut current_iface_name: Option<String> = None;
241    let mut hooks: Vec<ParsedHook> = Vec::new();
242    let mut current_hook_kvs: Option<HashMap<String, String>> = None;
243    let mut current_hook_name: Option<String> = None;
244
245    for line in input.lines() {
246        // Strip comments (# to end of line, unless inside quotes)
247        let line = strip_comment(line);
248        let trimmed = line.trim();
249
250        // Skip empty lines
251        if trimmed.is_empty() {
252            continue;
253        }
254
255        // Check for subsection [[name]]
256        if trimmed.starts_with("[[") && trimmed.ends_with("]]") {
257            let name = trimmed[2..trimmed.len() - 2].trim().to_string();
258            // Finalize previous interface subsection if any
259            if let (Some(iface_name), Some(kvs)) =
260                (current_iface_name.take(), current_iface_kvs.take())
261            {
262                interfaces.push(build_parsed_interface(iface_name, kvs));
263            }
264            // Finalize previous hook subsection if any
265            if let (Some(hook_name), Some(kvs)) =
266                (current_hook_name.take(), current_hook_kvs.take())
267            {
268                hooks.push(build_parsed_hook(hook_name, kvs));
269            }
270            current_subsection = Some(name.clone());
271            // Determine which section we're in to know subsection type
272            if current_section.as_deref() == Some("hooks") {
273                current_hook_name = Some(name);
274                current_hook_kvs = Some(HashMap::new());
275            } else {
276                current_iface_name = Some(name);
277                current_iface_kvs = Some(HashMap::new());
278            }
279            continue;
280        }
281
282        // Check for section [name]
283        if trimmed.starts_with('[') && trimmed.ends_with(']') {
284            // Finalize previous interface subsection if any
285            if let (Some(iface_name), Some(kvs)) =
286                (current_iface_name.take(), current_iface_kvs.take())
287            {
288                interfaces.push(build_parsed_interface(iface_name, kvs));
289            }
290            // Finalize previous hook subsection if any
291            if let (Some(hook_name), Some(kvs)) =
292                (current_hook_name.take(), current_hook_kvs.take())
293            {
294                hooks.push(build_parsed_hook(hook_name, kvs));
295            }
296            current_subsection = None;
297
298            let name = trimmed[1..trimmed.len() - 1].trim().to_lowercase();
299            current_section = Some(name);
300            continue;
301        }
302
303        // Parse key = value
304        if let Some(eq_pos) = trimmed.find('=') {
305            let key = trimmed[..eq_pos].trim().to_string();
306            let value = trimmed[eq_pos + 1..].trim().to_string();
307
308            if current_subsection.is_some() {
309                // Inside a [[subsection]] — exactly one of these should be Some
310                debug_assert!(
311                    !(current_hook_kvs.is_some() && current_iface_kvs.is_some()),
312                    "hook and interface subsections should never be active simultaneously"
313                );
314                if let Some(ref mut kvs) = current_hook_kvs {
315                    kvs.insert(key, value);
316                } else if let Some(ref mut kvs) = current_iface_kvs {
317                    kvs.insert(key, value);
318                }
319            } else if let Some(ref section) = current_section {
320                match section.as_str() {
321                    "reticulum" => {
322                        reticulum_kvs.insert(key, value);
323                    }
324                    "logging" => {
325                        logging_kvs.insert(key, value);
326                    }
327                    _ => {} // ignore unknown sections
328                }
329            }
330        }
331    }
332
333    // Finalize last subsections
334    if let (Some(iface_name), Some(kvs)) = (current_iface_name.take(), current_iface_kvs.take()) {
335        interfaces.push(build_parsed_interface(iface_name, kvs));
336    }
337    if let (Some(hook_name), Some(kvs)) = (current_hook_name.take(), current_hook_kvs.take()) {
338        hooks.push(build_parsed_hook(hook_name, kvs));
339    }
340
341    // Build typed sections
342    let reticulum = build_reticulum_section(&reticulum_kvs)?;
343    let logging = build_logging_section(&logging_kvs)?;
344
345    Ok(RnsConfig {
346        reticulum,
347        logging,
348        interfaces,
349        hooks,
350    })
351}
352
353/// Parse a config file from disk.
354pub fn parse_file(path: &Path) -> Result<RnsConfig, ConfigError> {
355    let content = std::fs::read_to_string(path)?;
356    parse(&content)
357}
358
359/// Strip `#` comments from a line (simple: not inside quotes).
360fn strip_comment(line: &str) -> &str {
361    // Find # that is not inside quotes
362    let mut in_quote = false;
363    let mut quote_char = '"';
364    for (i, ch) in line.char_indices() {
365        if !in_quote && (ch == '"' || ch == '\'') {
366            in_quote = true;
367            quote_char = ch;
368        } else if in_quote && ch == quote_char {
369            in_quote = false;
370        } else if !in_quote && ch == '#' {
371            return &line[..i];
372        }
373    }
374    line
375}
376
377/// Parse a string as a boolean (ConfigObj style). Public API for use by node.rs.
378pub fn parse_bool_pub(value: &str) -> Option<bool> {
379    parse_bool(value)
380}
381
382/// Parse a string as a boolean (ConfigObj style).
383fn parse_bool(value: &str) -> Option<bool> {
384    match value.to_lowercase().as_str() {
385        "yes" | "true" | "1" | "on" => Some(true),
386        "no" | "false" | "0" | "off" => Some(false),
387        _ => None,
388    }
389}
390
391fn build_parsed_interface(name: String, mut kvs: HashMap<String, String>) -> ParsedInterface {
392    let interface_type = kvs.remove("type").unwrap_or_default();
393    let enabled = kvs
394        .remove("enabled")
395        .and_then(|v| parse_bool(&v))
396        .unwrap_or(true);
397    // Python checks `interface_mode` first, then falls back to `mode`
398    let mode = kvs
399        .remove("interface_mode")
400        .or_else(|| kvs.remove("mode"))
401        .unwrap_or_else(|| "full".into());
402
403    ParsedInterface {
404        name,
405        interface_type,
406        enabled,
407        mode,
408        params: kvs,
409    }
410}
411
412fn build_parsed_hook(name: String, mut kvs: HashMap<String, String>) -> ParsedHook {
413    let path = kvs.remove("path").unwrap_or_default();
414    let hook_type = kvs
415        .remove("type")
416        .or_else(|| kvs.remove("backend"))
417        .unwrap_or_else(|| default_hook_type().into());
418    let builtin_id = kvs
419        .remove("builtin")
420        .or_else(|| kvs.remove("builtin_id"))
421        .or_else(|| kvs.remove("id"));
422    let attach_point = kvs.remove("attach_point").unwrap_or_default();
423    let priority = kvs
424        .remove("priority")
425        .and_then(|v| v.parse::<i32>().ok())
426        .unwrap_or(0);
427    let enabled = kvs
428        .remove("enabled")
429        .and_then(|v| parse_bool(&v))
430        .unwrap_or(true);
431
432    ParsedHook {
433        name,
434        path,
435        hook_type,
436        builtin_id,
437        attach_point,
438        priority,
439        enabled,
440    }
441}
442
443fn default_hook_type() -> &'static str {
444    #[cfg(feature = "rns-hooks-native")]
445    {
446        return "native";
447    }
448    #[cfg(all(not(feature = "rns-hooks-native"), feature = "rns-hooks-wasm"))]
449    {
450        return "wasm";
451    }
452    #[cfg(all(not(feature = "rns-hooks-native"), not(feature = "rns-hooks-wasm")))]
453    {
454        "wasm"
455    }
456}
457
458/// Map a hook point name string to its index. Returns None for unknown names.
459pub fn parse_hook_point(s: &str) -> Option<usize> {
460    match s {
461        "PreIngress" => Some(0),
462        "PreDispatch" => Some(1),
463        "AnnounceReceived" => Some(2),
464        "PathUpdated" => Some(3),
465        "AnnounceRetransmit" => Some(4),
466        "LinkRequestReceived" => Some(5),
467        "LinkEstablished" => Some(6),
468        "LinkClosed" => Some(7),
469        "InterfaceUp" => Some(8),
470        "InterfaceDown" => Some(9),
471        "InterfaceConfigChanged" => Some(10),
472        "BackbonePeerConnected" => Some(11),
473        "BackbonePeerDisconnected" => Some(12),
474        "BackbonePeerIdleTimeout" => Some(13),
475        "BackbonePeerWriteStall" => Some(14),
476        "BackbonePeerPenalty" => Some(15),
477        "SendOnInterface" => Some(16),
478        "BroadcastOnAllInterfaces" => Some(17),
479        "DeliverLocal" => Some(18),
480        "TunnelSynthesize" => Some(19),
481        "Tick" => Some(20),
482        _ => None,
483    }
484}
485
486#[cfg(feature = "hooks")]
487pub fn parse_hook_backend(s: &str) -> Result<rns_hooks::HookBackend, String> {
488    s.parse()
489}
490
491fn build_reticulum_section(kvs: &HashMap<String, String>) -> Result<ReticulumSection, ConfigError> {
492    let mut section = ReticulumSection::default();
493
494    if let Some(v) = kvs.get("enable_transport") {
495        section.enable_transport = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
496            key: "enable_transport".into(),
497            value: v.clone(),
498        })?;
499    }
500    if let Some(v) = kvs.get("share_instance") {
501        section.share_instance = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
502            key: "share_instance".into(),
503            value: v.clone(),
504        })?;
505    }
506    if let Some(v) = kvs.get("instance_name") {
507        section.instance_name = v.clone();
508    }
509    if let Some(v) = kvs.get("shared_instance_port") {
510        section.shared_instance_port = v.parse::<u16>().map_err(|_| ConfigError::InvalidValue {
511            key: "shared_instance_port".into(),
512            value: v.clone(),
513        })?;
514    }
515    if let Some(v) = kvs.get("instance_control_port") {
516        section.instance_control_port =
517            v.parse::<u16>().map_err(|_| ConfigError::InvalidValue {
518                key: "instance_control_port".into(),
519                value: v.clone(),
520            })?;
521    }
522    if let Some(v) = kvs.get("panic_on_interface_error") {
523        section.panic_on_interface_error =
524            parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
525                key: "panic_on_interface_error".into(),
526                value: v.clone(),
527            })?;
528    }
529    if let Some(v) = kvs.get("use_implicit_proof") {
530        section.use_implicit_proof = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
531            key: "use_implicit_proof".into(),
532            value: v.clone(),
533        })?;
534    }
535    if let Some(v) = kvs.get("network_identity") {
536        section.network_identity = Some(v.clone());
537    }
538    if let Some(v) = kvs.get("respond_to_probes") {
539        section.respond_to_probes = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
540            key: "respond_to_probes".into(),
541            value: v.clone(),
542        })?;
543    }
544    if let Some(v) = kvs.get("enable_remote_management") {
545        section.enable_remote_management =
546            parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
547                key: "enable_remote_management".into(),
548                value: v.clone(),
549            })?;
550    }
551    if let Some(v) = kvs.get("remote_management_allowed") {
552        // Value is a comma-separated list of hex identity hashes
553        for item in v.split(',') {
554            let trimmed = item.trim();
555            if !trimmed.is_empty() {
556                section.remote_management_allowed.push(trimmed.to_string());
557            }
558        }
559    }
560    if let Some(v) = kvs.get("publish_blackhole") {
561        section.publish_blackhole = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
562            key: "publish_blackhole".into(),
563            value: v.clone(),
564        })?;
565    }
566    if let Some(v) = kvs.get("probe_port") {
567        section.probe_port = Some(v.parse::<u16>().map_err(|_| ConfigError::InvalidValue {
568            key: "probe_port".into(),
569            value: v.clone(),
570        })?);
571    }
572    if let Some(v) = kvs.get("probe_addr") {
573        section.probe_addr = Some(v.clone());
574    }
575    if let Some(v) = kvs.get("probe_protocol") {
576        section.probe_protocol = Some(v.clone());
577    }
578    if let Some(v) = kvs.get("device") {
579        section.device = Some(v.clone());
580    }
581    if let Some(v) = kvs.get("discover_interfaces") {
582        section.discover_interfaces = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
583            key: "discover_interfaces".into(),
584            value: v.clone(),
585        })?;
586    }
587    if let Some(v) = kvs.get("required_discovery_value") {
588        section.required_discovery_value =
589            Some(v.parse::<u8>().map_err(|_| ConfigError::InvalidValue {
590                key: "required_discovery_value".into(),
591                value: v.clone(),
592            })?);
593    }
594    if let Some(v) = kvs.get("prefer_shorter_path") {
595        section.prefer_shorter_path = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
596            key: "prefer_shorter_path".into(),
597            value: v.clone(),
598        })?;
599    }
600    if let Some(v) = kvs.get("max_paths_per_destination") {
601        let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
602            key: "max_paths_per_destination".into(),
603            value: v.clone(),
604        })?;
605        section.max_paths_per_destination = n.max(1);
606    }
607    if let Some(v) = kvs.get("packet_hashlist_max_entries") {
608        let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
609            key: "packet_hashlist_max_entries".into(),
610            value: v.clone(),
611        })?;
612        section.packet_hashlist_max_entries = n.max(1);
613    }
614    if let Some(v) = kvs.get("max_discovery_pr_tags") {
615        let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
616            key: "max_discovery_pr_tags".into(),
617            value: v.clone(),
618        })?;
619        section.max_discovery_pr_tags = n.max(1);
620    }
621    if let Some(v) = kvs.get("max_path_destinations") {
622        let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
623            key: "max_path_destinations".into(),
624            value: v.clone(),
625        })?;
626        section.max_path_destinations = n.max(1);
627    }
628    if let Some(v) = kvs.get("max_tunnel_destinations_total") {
629        let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
630            key: "max_tunnel_destinations_total".into(),
631            value: v.clone(),
632        })?;
633        section.max_tunnel_destinations_total = n.max(1);
634    }
635    if let Some(v) = kvs.get("known_destinations_ttl") {
636        section.known_destinations_ttl =
637            v.parse::<u64>().map_err(|_| ConfigError::InvalidValue {
638                key: "known_destinations_ttl".into(),
639                value: v.clone(),
640            })?;
641    }
642    if let Some(v) = kvs.get("known_destinations_max_entries") {
643        let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
644            key: "known_destinations_max_entries".into(),
645            value: v.clone(),
646        })?;
647        if n == 0 {
648            return Err(ConfigError::InvalidValue {
649                key: "known_destinations_max_entries".into(),
650                value: v.clone(),
651            });
652        }
653        section.known_destinations_max_entries = n;
654    }
655    if let Some(v) = kvs.get("ratchet_expiry") {
656        let expiry = v.parse::<u64>().map_err(|_| ConfigError::InvalidValue {
657            key: "ratchet_expiry".into(),
658            value: v.clone(),
659        })?;
660        if expiry == 0 {
661            return Err(ConfigError::InvalidValue {
662                key: "ratchet_expiry".into(),
663                value: v.clone(),
664            });
665        }
666        section.ratchet_expiry = expiry;
667    }
668    if let Some(v) = kvs.get("destination_timeout_secs") {
669        section.known_destinations_ttl =
670            v.parse::<u64>().map_err(|_| ConfigError::InvalidValue {
671                key: "destination_timeout_secs".into(),
672                value: v.clone(),
673            })?;
674    }
675    if let Some(v) = kvs.get("announce_table_ttl") {
676        let ttl = v.parse::<u64>().map_err(|_| ConfigError::InvalidValue {
677            key: "announce_table_ttl".into(),
678            value: v.clone(),
679        })?;
680        if ttl == 0 {
681            return Err(ConfigError::InvalidValue {
682                key: "announce_table_ttl".into(),
683                value: v.clone(),
684            });
685        }
686        section.announce_table_ttl = ttl;
687    }
688    if let Some(v) = kvs.get("announce_table_max_bytes") {
689        let max_bytes = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
690            key: "announce_table_max_bytes".into(),
691            value: v.clone(),
692        })?;
693        if max_bytes == 0 {
694            return Err(ConfigError::InvalidValue {
695                key: "announce_table_max_bytes".into(),
696                value: v.clone(),
697            });
698        }
699        section.announce_table_max_bytes = max_bytes;
700    }
701    if let Some(v) = kvs.get("announce_signature_cache_enabled") {
702        section.announce_sig_cache_enabled = match v.as_str() {
703            "true" | "yes" | "True" | "Yes" => true,
704            "false" | "no" | "False" | "No" => false,
705            _ => {
706                return Err(ConfigError::InvalidValue {
707                    key: "announce_signature_cache_enabled".into(),
708                    value: v.clone(),
709                })
710            }
711        };
712    }
713    if let Some(v) = kvs.get("announce_signature_cache_max_entries") {
714        let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
715            key: "announce_signature_cache_max_entries".into(),
716            value: v.clone(),
717        })?;
718        section.announce_sig_cache_max_entries = n;
719    }
720    if let Some(v) = kvs.get("announce_signature_cache_ttl") {
721        let ttl = v.parse::<u64>().map_err(|_| ConfigError::InvalidValue {
722            key: "announce_signature_cache_ttl".into(),
723            value: v.clone(),
724        })?;
725        section.announce_sig_cache_ttl = ttl;
726    }
727    if let Some(v) = kvs.get("announce_queue_max_entries") {
728        let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
729            key: "announce_queue_max_entries".into(),
730            value: v.clone(),
731        })?;
732        if n == 0 {
733            return Err(ConfigError::InvalidValue {
734                key: "announce_queue_max_entries".into(),
735                value: v.clone(),
736            });
737        }
738        section.announce_queue_max_entries = n;
739    }
740    if let Some(v) = kvs.get("announce_queue_max_interfaces") {
741        let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
742            key: "announce_queue_max_interfaces".into(),
743            value: v.clone(),
744        })?;
745        if n == 0 {
746            return Err(ConfigError::InvalidValue {
747                key: "announce_queue_max_interfaces".into(),
748                value: v.clone(),
749            });
750        }
751        section.announce_queue_max_interfaces = n;
752    }
753    if let Some(v) = kvs.get("announce_queue_max_bytes") {
754        let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
755            key: "announce_queue_max_bytes".into(),
756            value: v.clone(),
757        })?;
758        if n == 0 {
759            return Err(ConfigError::InvalidValue {
760                key: "announce_queue_max_bytes".into(),
761                value: v.clone(),
762            });
763        }
764        section.announce_queue_max_bytes = n;
765    }
766    if let Some(v) = kvs.get("announce_queue_ttl") {
767        let ttl = v.parse::<u64>().map_err(|_| ConfigError::InvalidValue {
768            key: "announce_queue_ttl".into(),
769            value: v.clone(),
770        })?;
771        if ttl == 0 {
772            return Err(ConfigError::InvalidValue {
773                key: "announce_queue_ttl".into(),
774                value: v.clone(),
775            });
776        }
777        section.announce_queue_ttl = ttl;
778    }
779    if let Some(v) = kvs.get("announce_queue_overflow_policy") {
780        let normalized = v.to_lowercase();
781        if normalized != "drop_newest" && normalized != "drop_oldest" && normalized != "drop_worst"
782        {
783            return Err(ConfigError::InvalidValue {
784                key: "announce_queue_overflow_policy".into(),
785                value: v.clone(),
786            });
787        }
788        section.announce_queue_overflow_policy = normalized;
789    }
790    if let Some(v) = kvs.get("driver_event_queue_capacity") {
791        let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
792            key: "driver_event_queue_capacity".into(),
793            value: v.clone(),
794        })?;
795        if n == 0 {
796            return Err(ConfigError::InvalidValue {
797                key: "driver_event_queue_capacity".into(),
798                value: v.clone(),
799            });
800        }
801        section.driver_event_queue_capacity = n;
802    }
803    if let Some(v) = kvs.get("interface_writer_queue_capacity") {
804        let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
805            key: "interface_writer_queue_capacity".into(),
806            value: v.clone(),
807        })?;
808        if n == 0 {
809            return Err(ConfigError::InvalidValue {
810                key: "interface_writer_queue_capacity".into(),
811                value: v.clone(),
812            });
813        }
814        section.interface_writer_queue_capacity = n;
815    }
816    if let Some(v) = kvs.get("backbone_peer_pool_max_connected") {
817        section.backbone_peer_pool_max_connected =
818            v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
819                key: "backbone_peer_pool_max_connected".into(),
820                value: v.clone(),
821            })?;
822    }
823    if let Some(v) = kvs.get("backbone_peer_pool_failure_threshold") {
824        let n = v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
825            key: "backbone_peer_pool_failure_threshold".into(),
826            value: v.clone(),
827        })?;
828        if n == 0 {
829            return Err(ConfigError::InvalidValue {
830                key: "backbone_peer_pool_failure_threshold".into(),
831                value: v.clone(),
832            });
833        }
834        section.backbone_peer_pool_failure_threshold = n;
835    }
836    if let Some(v) = kvs.get("backbone_peer_pool_failure_window") {
837        section.backbone_peer_pool_failure_window =
838            v.parse::<u64>().map_err(|_| ConfigError::InvalidValue {
839                key: "backbone_peer_pool_failure_window".into(),
840                value: v.clone(),
841            })?;
842    }
843    if let Some(v) = kvs.get("backbone_peer_pool_cooldown") {
844        section.backbone_peer_pool_cooldown =
845            v.parse::<u64>().map_err(|_| ConfigError::InvalidValue {
846                key: "backbone_peer_pool_cooldown".into(),
847                value: v.clone(),
848            })?;
849    }
850    #[cfg(feature = "hooks")]
851    if let Some(v) = kvs.get("provider_bridge") {
852        section.provider_bridge = parse_bool(v).ok_or_else(|| ConfigError::InvalidValue {
853            key: "provider_bridge".into(),
854            value: v.clone(),
855        })?;
856    }
857    #[cfg(feature = "hooks")]
858    if let Some(v) = kvs.get("provider_socket_path") {
859        section.provider_socket_path = Some(v.clone());
860    }
861    #[cfg(feature = "hooks")]
862    if let Some(v) = kvs.get("provider_queue_max_events") {
863        section.provider_queue_max_events =
864            v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
865                key: "provider_queue_max_events".into(),
866                value: v.clone(),
867            })?;
868    }
869    #[cfg(feature = "hooks")]
870    if let Some(v) = kvs.get("provider_queue_max_bytes") {
871        section.provider_queue_max_bytes =
872            v.parse::<usize>().map_err(|_| ConfigError::InvalidValue {
873                key: "provider_queue_max_bytes".into(),
874                value: v.clone(),
875            })?;
876    }
877    #[cfg(feature = "hooks")]
878    if let Some(v) = kvs.get("provider_overflow_policy") {
879        let normalized = v.to_lowercase();
880        if normalized != "drop_newest" && normalized != "drop_oldest" {
881            return Err(ConfigError::InvalidValue {
882                key: "provider_overflow_policy".into(),
883                value: v.clone(),
884            });
885        }
886        section.provider_overflow_policy = normalized;
887    }
888
889    Ok(section)
890}
891
892fn build_logging_section(kvs: &HashMap<String, String>) -> Result<LoggingSection, ConfigError> {
893    let mut section = LoggingSection::default();
894
895    if let Some(v) = kvs.get("loglevel") {
896        section.loglevel = v.parse::<u8>().map_err(|_| ConfigError::InvalidValue {
897            key: "loglevel".into(),
898            value: v.clone(),
899        })?;
900    }
901
902    Ok(section)
903}
904
905#[cfg(test)]
906mod tests {
907    use super::*;
908
909    #[test]
910    fn parse_empty() {
911        let config = parse("").unwrap();
912        assert!(!config.reticulum.enable_transport);
913        assert!(config.reticulum.share_instance);
914        assert_eq!(config.reticulum.instance_name, "default");
915        assert_eq!(config.logging.loglevel, 4);
916        assert!(config.interfaces.is_empty());
917        assert_eq!(
918            config.reticulum.packet_hashlist_max_entries,
919            rns_core::constants::HASHLIST_MAXSIZE
920        );
921        assert_eq!(
922            config.reticulum.announce_table_ttl,
923            rns_core::constants::ANNOUNCE_TABLE_TTL as u64
924        );
925        assert_eq!(
926            config.reticulum.announce_table_max_bytes,
927            rns_core::constants::ANNOUNCE_TABLE_MAX_BYTES
928        );
929        assert_eq!(
930            config.reticulum.ratchet_expiry,
931            rns_core::constants::RATCHET_EXPIRY
932        );
933    }
934
935    #[cfg(feature = "hooks")]
936    #[test]
937    fn parse_provider_bridge_config() {
938        let config = parse(
939            r#"
940[reticulum]
941provider_bridge = yes
942provider_socket_path = /tmp/rns-provider.sock
943provider_queue_max_events = 42
944provider_queue_max_bytes = 8192
945provider_overflow_policy = drop_oldest
946"#,
947        )
948        .unwrap();
949
950        assert!(config.reticulum.provider_bridge);
951        assert_eq!(
952            config.reticulum.provider_socket_path.as_deref(),
953            Some("/tmp/rns-provider.sock")
954        );
955        assert_eq!(config.reticulum.provider_queue_max_events, 42);
956        assert_eq!(config.reticulum.provider_queue_max_bytes, 8192);
957        assert_eq!(config.reticulum.provider_overflow_policy, "drop_oldest");
958    }
959
960    #[test]
961    fn parse_default_config() {
962        // The default config from Python's __default_rns_config__
963        let input = r#"
964[reticulum]
965enable_transport = False
966share_instance = Yes
967instance_name = default
968
969[logging]
970loglevel = 4
971
972[interfaces]
973
974  [[Default Interface]]
975    type = AutoInterface
976    enabled = Yes
977"#;
978        let config = parse(input).unwrap();
979        assert!(!config.reticulum.enable_transport);
980        assert!(config.reticulum.share_instance);
981        assert_eq!(config.reticulum.instance_name, "default");
982        assert_eq!(config.logging.loglevel, 4);
983        assert_eq!(config.interfaces.len(), 1);
984        assert_eq!(config.interfaces[0].name, "Default Interface");
985        assert_eq!(config.interfaces[0].interface_type, "AutoInterface");
986        assert!(config.interfaces[0].enabled);
987    }
988
989    #[test]
990    fn parse_reticulum_section() {
991        let input = r#"
992[reticulum]
993enable_transport = True
994share_instance = No
995instance_name = mynode
996shared_instance_port = 12345
997instance_control_port = 12346
998panic_on_interface_error = Yes
999use_implicit_proof = False
1000respond_to_probes = True
1001network_identity = /home/user/.reticulum/identity
1002known_destinations_ttl = 1234
1003known_destinations_max_entries = 4321
1004ratchet_expiry = 9876
1005announce_table_ttl = 45
1006announce_table_max_bytes = 65536
1007packet_hashlist_max_entries = 321
1008max_discovery_pr_tags = 222
1009max_path_destinations = 111
1010max_tunnel_destinations_total = 99
1011announce_signature_cache_enabled = false
1012announce_signature_cache_max_entries = 500
1013announce_signature_cache_ttl = 300
1014announce_queue_max_entries = 123
1015announce_queue_max_interfaces = 321
1016announce_queue_max_bytes = 4567
1017announce_queue_ttl = 89
1018announce_queue_overflow_policy = drop_oldest
1019driver_event_queue_capacity = 6543
1020interface_writer_queue_capacity = 210
1021backbone_peer_pool_max_connected = 6
1022backbone_peer_pool_failure_threshold = 4
1023backbone_peer_pool_failure_window = 120
1024backbone_peer_pool_cooldown = 300
1025"#;
1026        let config = parse(input).unwrap();
1027        assert!(config.reticulum.enable_transport);
1028        assert!(!config.reticulum.share_instance);
1029        assert_eq!(config.reticulum.instance_name, "mynode");
1030        assert_eq!(config.reticulum.shared_instance_port, 12345);
1031        assert_eq!(config.reticulum.instance_control_port, 12346);
1032        assert!(config.reticulum.panic_on_interface_error);
1033        assert!(!config.reticulum.use_implicit_proof);
1034        assert!(config.reticulum.respond_to_probes);
1035        assert_eq!(
1036            config.reticulum.network_identity.as_deref(),
1037            Some("/home/user/.reticulum/identity")
1038        );
1039        assert_eq!(config.reticulum.known_destinations_ttl, 1234);
1040        assert_eq!(config.reticulum.known_destinations_max_entries, 4321);
1041        assert_eq!(config.reticulum.ratchet_expiry, 9876);
1042        assert_eq!(config.reticulum.announce_table_ttl, 45);
1043        assert_eq!(config.reticulum.announce_table_max_bytes, 65536);
1044        assert_eq!(config.reticulum.packet_hashlist_max_entries, 321);
1045        assert_eq!(config.reticulum.max_discovery_pr_tags, 222);
1046        assert_eq!(config.reticulum.max_path_destinations, 111);
1047        assert_eq!(config.reticulum.max_tunnel_destinations_total, 99);
1048        assert!(!config.reticulum.announce_sig_cache_enabled);
1049        assert_eq!(config.reticulum.announce_sig_cache_max_entries, 500);
1050        assert_eq!(config.reticulum.announce_sig_cache_ttl, 300);
1051        assert_eq!(config.reticulum.announce_queue_max_entries, 123);
1052        assert_eq!(config.reticulum.announce_queue_max_interfaces, 321);
1053        assert_eq!(config.reticulum.announce_queue_max_bytes, 4567);
1054        assert_eq!(config.reticulum.announce_queue_ttl, 89);
1055        assert_eq!(
1056            config.reticulum.announce_queue_overflow_policy,
1057            "drop_oldest"
1058        );
1059        assert_eq!(config.reticulum.driver_event_queue_capacity, 6543);
1060        assert_eq!(config.reticulum.interface_writer_queue_capacity, 210);
1061        assert_eq!(config.reticulum.backbone_peer_pool_max_connected, 6);
1062        assert_eq!(config.reticulum.backbone_peer_pool_failure_threshold, 4);
1063        assert_eq!(config.reticulum.backbone_peer_pool_failure_window, 120);
1064        assert_eq!(config.reticulum.backbone_peer_pool_cooldown, 300);
1065    }
1066
1067    #[test]
1068    fn parse_backbone_peer_pool_defaults_disabled() {
1069        let config = parse("[reticulum]\n").unwrap();
1070        assert_eq!(config.reticulum.backbone_peer_pool_max_connected, 0);
1071        assert_eq!(config.reticulum.backbone_peer_pool_failure_threshold, 3);
1072        assert_eq!(config.reticulum.backbone_peer_pool_failure_window, 600);
1073        assert_eq!(config.reticulum.backbone_peer_pool_cooldown, 900);
1074    }
1075
1076    #[test]
1077    fn parse_announce_table_limits_reject_zero() {
1078        let err = parse(
1079            r#"
1080[reticulum]
1081announce_table_ttl = 0
1082"#,
1083        )
1084        .unwrap_err();
1085        assert!(matches!(
1086            err,
1087            ConfigError::InvalidValue { key, .. } if key == "announce_table_ttl"
1088        ));
1089
1090        let err = parse(
1091            r#"
1092[reticulum]
1093known_destinations_max_entries = 0
1094"#,
1095        )
1096        .unwrap_err();
1097        assert!(matches!(
1098            err,
1099            ConfigError::InvalidValue { key, .. } if key == "known_destinations_max_entries"
1100        ));
1101
1102        let err = parse(
1103            r#"
1104[reticulum]
1105ratchet_expiry = 0
1106"#,
1107        )
1108        .unwrap_err();
1109        assert!(matches!(
1110            err,
1111            ConfigError::InvalidValue { key, .. } if key == "ratchet_expiry"
1112        ));
1113
1114        let err = parse(
1115            r#"
1116[reticulum]
1117announce_table_max_bytes = 0
1118"#,
1119        )
1120        .unwrap_err();
1121        assert!(matches!(
1122            err,
1123            ConfigError::InvalidValue { key, .. } if key == "announce_table_max_bytes"
1124        ));
1125
1126        let err = parse(
1127            r#"
1128[reticulum]
1129announce_queue_max_entries = 0
1130"#,
1131        )
1132        .unwrap_err();
1133        assert!(matches!(
1134            err,
1135            ConfigError::InvalidValue { key, .. } if key == "announce_queue_max_entries"
1136        ));
1137
1138        let err = parse(
1139            r#"
1140[reticulum]
1141announce_queue_max_interfaces = 0
1142"#,
1143        )
1144        .unwrap_err();
1145        assert!(matches!(
1146            err,
1147            ConfigError::InvalidValue { key, .. } if key == "announce_queue_max_interfaces"
1148        ));
1149
1150        let err = parse(
1151            r#"
1152[reticulum]
1153announce_queue_max_bytes = 0
1154"#,
1155        )
1156        .unwrap_err();
1157        assert!(matches!(
1158            err,
1159            ConfigError::InvalidValue { key, .. } if key == "announce_queue_max_bytes"
1160        ));
1161
1162        let err = parse(
1163            r#"
1164[reticulum]
1165driver_event_queue_capacity = 0
1166"#,
1167        )
1168        .unwrap_err();
1169        assert!(matches!(
1170            err,
1171            ConfigError::InvalidValue { key, .. } if key == "driver_event_queue_capacity"
1172        ));
1173
1174        let err = parse(
1175            r#"
1176[reticulum]
1177interface_writer_queue_capacity = 0
1178"#,
1179        )
1180        .unwrap_err();
1181        assert!(matches!(
1182            err,
1183            ConfigError::InvalidValue { key, .. } if key == "interface_writer_queue_capacity"
1184        ));
1185
1186        let err = parse(
1187            r#"
1188[reticulum]
1189announce_queue_ttl = 0
1190"#,
1191        )
1192        .unwrap_err();
1193        assert!(matches!(
1194            err,
1195            ConfigError::InvalidValue { key, .. } if key == "announce_queue_ttl"
1196        ));
1197    }
1198
1199    #[test]
1200    fn parse_announce_queue_overflow_policy_rejects_invalid() {
1201        let err = parse(
1202            r#"
1203[reticulum]
1204announce_queue_overflow_policy = keep_everything
1205"#,
1206        )
1207        .unwrap_err();
1208        assert!(matches!(
1209            err,
1210            ConfigError::InvalidValue { key, .. } if key == "announce_queue_overflow_policy"
1211        ));
1212    }
1213
1214    #[test]
1215    fn parse_destination_timeout_secs_alias() {
1216        let config = parse(
1217            r#"
1218[reticulum]
1219destination_timeout_secs = 777
1220"#,
1221        )
1222        .unwrap();
1223
1224        assert_eq!(config.reticulum.known_destinations_ttl, 777);
1225    }
1226
1227    #[test]
1228    fn parse_logging_section() {
1229        let input = "[logging]\nloglevel = 6\n";
1230        let config = parse(input).unwrap();
1231        assert_eq!(config.logging.loglevel, 6);
1232    }
1233
1234    #[test]
1235    fn parse_interface_tcp_client() {
1236        let input = r#"
1237[interfaces]
1238  [[TCP Client]]
1239    type = TCPClientInterface
1240    enabled = Yes
1241    target_host = 87.106.8.245
1242    target_port = 4242
1243"#;
1244        let config = parse(input).unwrap();
1245        assert_eq!(config.interfaces.len(), 1);
1246        let iface = &config.interfaces[0];
1247        assert_eq!(iface.name, "TCP Client");
1248        assert_eq!(iface.interface_type, "TCPClientInterface");
1249        assert!(iface.enabled);
1250        assert_eq!(iface.params.get("target_host").unwrap(), "87.106.8.245");
1251        assert_eq!(iface.params.get("target_port").unwrap(), "4242");
1252    }
1253
1254    #[test]
1255    fn parse_interface_tcp_server() {
1256        let input = r#"
1257[interfaces]
1258  [[TCP Server]]
1259    type = TCPServerInterface
1260    enabled = Yes
1261    listen_ip = 0.0.0.0
1262    listen_port = 4242
1263"#;
1264        let config = parse(input).unwrap();
1265        assert_eq!(config.interfaces.len(), 1);
1266        let iface = &config.interfaces[0];
1267        assert_eq!(iface.name, "TCP Server");
1268        assert_eq!(iface.interface_type, "TCPServerInterface");
1269        assert_eq!(iface.params.get("listen_ip").unwrap(), "0.0.0.0");
1270        assert_eq!(iface.params.get("listen_port").unwrap(), "4242");
1271    }
1272
1273    #[test]
1274    fn parse_interface_udp() {
1275        let input = r#"
1276[interfaces]
1277  [[UDP Interface]]
1278    type = UDPInterface
1279    enabled = Yes
1280    listen_ip = 0.0.0.0
1281    listen_port = 4242
1282    forward_ip = 255.255.255.255
1283    forward_port = 4242
1284"#;
1285        let config = parse(input).unwrap();
1286        assert_eq!(config.interfaces.len(), 1);
1287        let iface = &config.interfaces[0];
1288        assert_eq!(iface.name, "UDP Interface");
1289        assert_eq!(iface.interface_type, "UDPInterface");
1290        assert_eq!(iface.params.get("listen_ip").unwrap(), "0.0.0.0");
1291        assert_eq!(iface.params.get("forward_ip").unwrap(), "255.255.255.255");
1292    }
1293
1294    #[test]
1295    fn parse_multiple_interfaces() {
1296        let input = r#"
1297[interfaces]
1298  [[TCP Client]]
1299    type = TCPClientInterface
1300    target_host = 10.0.0.1
1301    target_port = 4242
1302
1303  [[UDP Broadcast]]
1304    type = UDPInterface
1305    listen_ip = 0.0.0.0
1306    listen_port = 5555
1307    forward_ip = 255.255.255.255
1308    forward_port = 5555
1309"#;
1310        let config = parse(input).unwrap();
1311        assert_eq!(config.interfaces.len(), 2);
1312        assert_eq!(config.interfaces[0].name, "TCP Client");
1313        assert_eq!(config.interfaces[0].interface_type, "TCPClientInterface");
1314        assert_eq!(config.interfaces[1].name, "UDP Broadcast");
1315        assert_eq!(config.interfaces[1].interface_type, "UDPInterface");
1316    }
1317
1318    #[test]
1319    fn parse_booleans() {
1320        // Test all boolean variants
1321        for (input, expected) in &[
1322            ("Yes", true),
1323            ("No", false),
1324            ("True", true),
1325            ("False", false),
1326            ("true", true),
1327            ("false", false),
1328            ("1", true),
1329            ("0", false),
1330            ("on", true),
1331            ("off", false),
1332        ] {
1333            let result = parse_bool(input);
1334            assert_eq!(result, Some(*expected), "parse_bool({}) failed", input);
1335        }
1336    }
1337
1338    #[test]
1339    fn parse_comments() {
1340        let input = r#"
1341# This is a comment
1342[reticulum]
1343enable_transport = True  # inline comment
1344# share_instance = No
1345instance_name = test
1346"#;
1347        let config = parse(input).unwrap();
1348        assert!(config.reticulum.enable_transport);
1349        assert!(config.reticulum.share_instance); // commented out line should be ignored
1350        assert_eq!(config.reticulum.instance_name, "test");
1351    }
1352
1353    #[test]
1354    fn parse_interface_mode_field() {
1355        let input = r#"
1356[interfaces]
1357  [[TCP Client]]
1358    type = TCPClientInterface
1359    interface_mode = access_point
1360    target_host = 10.0.0.1
1361    target_port = 4242
1362"#;
1363        let config = parse(input).unwrap();
1364        assert_eq!(config.interfaces[0].mode, "access_point");
1365    }
1366
1367    #[test]
1368    fn parse_mode_fallback() {
1369        // Python also accepts "mode" as fallback for "interface_mode"
1370        let input = r#"
1371[interfaces]
1372  [[TCP Client]]
1373    type = TCPClientInterface
1374    mode = gateway
1375    target_host = 10.0.0.1
1376    target_port = 4242
1377"#;
1378        let config = parse(input).unwrap();
1379        assert_eq!(config.interfaces[0].mode, "gateway");
1380    }
1381
1382    #[test]
1383    fn parse_interface_mode_takes_precedence() {
1384        // If both interface_mode and mode are set, interface_mode wins
1385        let input = r#"
1386[interfaces]
1387  [[TCP Client]]
1388    type = TCPClientInterface
1389    interface_mode = roaming
1390    mode = boundary
1391    target_host = 10.0.0.1
1392    target_port = 4242
1393"#;
1394        let config = parse(input).unwrap();
1395        assert_eq!(config.interfaces[0].mode, "roaming");
1396    }
1397
1398    #[test]
1399    fn parse_disabled_interface() {
1400        let input = r#"
1401[interfaces]
1402  [[Disabled TCP]]
1403    type = TCPClientInterface
1404    enabled = No
1405    target_host = 10.0.0.1
1406    target_port = 4242
1407"#;
1408        let config = parse(input).unwrap();
1409        assert_eq!(config.interfaces.len(), 1);
1410        assert!(!config.interfaces[0].enabled);
1411    }
1412
1413    #[test]
1414    fn parse_serial_interface() {
1415        let input = r#"
1416[interfaces]
1417  [[Serial Port]]
1418    type = SerialInterface
1419    enabled = Yes
1420    port = /dev/ttyUSB0
1421    speed = 115200
1422    databits = 8
1423    parity = N
1424    stopbits = 1
1425"#;
1426        let config = parse(input).unwrap();
1427        assert_eq!(config.interfaces.len(), 1);
1428        let iface = &config.interfaces[0];
1429        assert_eq!(iface.name, "Serial Port");
1430        assert_eq!(iface.interface_type, "SerialInterface");
1431        assert!(iface.enabled);
1432        assert_eq!(iface.params.get("port").unwrap(), "/dev/ttyUSB0");
1433        assert_eq!(iface.params.get("speed").unwrap(), "115200");
1434        assert_eq!(iface.params.get("databits").unwrap(), "8");
1435        assert_eq!(iface.params.get("parity").unwrap(), "N");
1436        assert_eq!(iface.params.get("stopbits").unwrap(), "1");
1437    }
1438
1439    #[test]
1440    fn parse_kiss_interface() {
1441        let input = r#"
1442[interfaces]
1443  [[KISS TNC]]
1444    type = KISSInterface
1445    enabled = Yes
1446    port = /dev/ttyUSB1
1447    speed = 9600
1448    preamble = 350
1449    txtail = 20
1450    persistence = 64
1451    slottime = 20
1452    flow_control = True
1453    id_interval = 600
1454    id_callsign = MYCALL
1455"#;
1456        let config = parse(input).unwrap();
1457        assert_eq!(config.interfaces.len(), 1);
1458        let iface = &config.interfaces[0];
1459        assert_eq!(iface.name, "KISS TNC");
1460        assert_eq!(iface.interface_type, "KISSInterface");
1461        assert_eq!(iface.params.get("port").unwrap(), "/dev/ttyUSB1");
1462        assert_eq!(iface.params.get("speed").unwrap(), "9600");
1463        assert_eq!(iface.params.get("preamble").unwrap(), "350");
1464        assert_eq!(iface.params.get("txtail").unwrap(), "20");
1465        assert_eq!(iface.params.get("persistence").unwrap(), "64");
1466        assert_eq!(iface.params.get("slottime").unwrap(), "20");
1467        assert_eq!(iface.params.get("flow_control").unwrap(), "True");
1468        assert_eq!(iface.params.get("id_interval").unwrap(), "600");
1469        assert_eq!(iface.params.get("id_callsign").unwrap(), "MYCALL");
1470    }
1471
1472    #[test]
1473    fn parse_ifac_networkname() {
1474        let input = r#"
1475[interfaces]
1476  [[TCP Client]]
1477    type = TCPClientInterface
1478    target_host = 10.0.0.1
1479    target_port = 4242
1480    networkname = testnet
1481"#;
1482        let config = parse(input).unwrap();
1483        assert_eq!(
1484            config.interfaces[0].params.get("networkname").unwrap(),
1485            "testnet"
1486        );
1487    }
1488
1489    #[test]
1490    fn parse_ifac_passphrase() {
1491        let input = r#"
1492[interfaces]
1493  [[TCP Client]]
1494    type = TCPClientInterface
1495    target_host = 10.0.0.1
1496    target_port = 4242
1497    passphrase = secret123
1498    ifac_size = 64
1499"#;
1500        let config = parse(input).unwrap();
1501        assert_eq!(
1502            config.interfaces[0].params.get("passphrase").unwrap(),
1503            "secret123"
1504        );
1505        assert_eq!(config.interfaces[0].params.get("ifac_size").unwrap(), "64");
1506    }
1507
1508    #[test]
1509    fn parse_remote_management_config() {
1510        let input = r#"
1511[reticulum]
1512enable_transport = True
1513enable_remote_management = Yes
1514remote_management_allowed = aabbccdd00112233aabbccdd00112233, 11223344556677881122334455667788
1515publish_blackhole = Yes
1516"#;
1517        let config = parse(input).unwrap();
1518        assert!(config.reticulum.enable_remote_management);
1519        assert!(config.reticulum.publish_blackhole);
1520        assert_eq!(config.reticulum.remote_management_allowed.len(), 2);
1521        assert_eq!(
1522            config.reticulum.remote_management_allowed[0],
1523            "aabbccdd00112233aabbccdd00112233"
1524        );
1525        assert_eq!(
1526            config.reticulum.remote_management_allowed[1],
1527            "11223344556677881122334455667788"
1528        );
1529    }
1530
1531    #[test]
1532    fn parse_remote_management_defaults() {
1533        let input = "[reticulum]\n";
1534        let config = parse(input).unwrap();
1535        assert!(!config.reticulum.enable_remote_management);
1536        assert!(!config.reticulum.publish_blackhole);
1537        assert!(config.reticulum.remote_management_allowed.is_empty());
1538    }
1539
1540    #[test]
1541    fn parse_hooks_section() {
1542        let input = r#"
1543[hooks]
1544  [[drop_tick]]
1545    path = /tmp/drop_tick.wasm
1546    attach_point = Tick
1547    priority = 10
1548    enabled = Yes
1549
1550  [[log_announce]]
1551    path = /tmp/log_announce.wasm
1552    type = native
1553    attach_point = AnnounceReceived
1554    priority = 5
1555    enabled = No
1556
1557  [[builtin_tick]]
1558    builtin = example.tick
1559    type = builtin
1560    attach_point = Tick
1561"#;
1562        let config = parse(input).unwrap();
1563        assert_eq!(config.hooks.len(), 3);
1564        assert_eq!(config.hooks[0].name, "drop_tick");
1565        assert_eq!(config.hooks[0].path, "/tmp/drop_tick.wasm");
1566        assert_eq!(config.hooks[0].hook_type, default_hook_type());
1567        assert_eq!(config.hooks[0].attach_point, "Tick");
1568        assert_eq!(config.hooks[0].priority, 10);
1569        assert!(config.hooks[0].enabled);
1570        assert_eq!(config.hooks[1].name, "log_announce");
1571        assert_eq!(config.hooks[1].hook_type, "native");
1572        assert_eq!(config.hooks[1].attach_point, "AnnounceReceived");
1573        assert!(!config.hooks[1].enabled);
1574        assert_eq!(config.hooks[2].hook_type, "builtin");
1575        assert_eq!(config.hooks[2].builtin_id.as_deref(), Some("example.tick"));
1576    }
1577
1578    #[test]
1579    fn parse_empty_hooks() {
1580        let input = "[hooks]\n";
1581        let config = parse(input).unwrap();
1582        assert!(config.hooks.is_empty());
1583    }
1584
1585    #[test]
1586    fn parse_hook_point_names() {
1587        assert_eq!(parse_hook_point("PreIngress"), Some(0));
1588        assert_eq!(parse_hook_point("PreDispatch"), Some(1));
1589        assert_eq!(parse_hook_point("AnnounceReceived"), Some(2));
1590        assert_eq!(parse_hook_point("PathUpdated"), Some(3));
1591        assert_eq!(parse_hook_point("AnnounceRetransmit"), Some(4));
1592        assert_eq!(parse_hook_point("LinkRequestReceived"), Some(5));
1593        assert_eq!(parse_hook_point("LinkEstablished"), Some(6));
1594        assert_eq!(parse_hook_point("LinkClosed"), Some(7));
1595        assert_eq!(parse_hook_point("InterfaceUp"), Some(8));
1596        assert_eq!(parse_hook_point("InterfaceDown"), Some(9));
1597        assert_eq!(parse_hook_point("InterfaceConfigChanged"), Some(10));
1598        assert_eq!(parse_hook_point("BackbonePeerConnected"), Some(11));
1599        assert_eq!(parse_hook_point("BackbonePeerDisconnected"), Some(12));
1600        assert_eq!(parse_hook_point("BackbonePeerIdleTimeout"), Some(13));
1601        assert_eq!(parse_hook_point("BackbonePeerWriteStall"), Some(14));
1602        assert_eq!(parse_hook_point("BackbonePeerPenalty"), Some(15));
1603        assert_eq!(parse_hook_point("SendOnInterface"), Some(16));
1604        assert_eq!(parse_hook_point("BroadcastOnAllInterfaces"), Some(17));
1605        assert_eq!(parse_hook_point("DeliverLocal"), Some(18));
1606        assert_eq!(parse_hook_point("TunnelSynthesize"), Some(19));
1607        assert_eq!(parse_hook_point("Tick"), Some(20));
1608        assert_eq!(parse_hook_point("Unknown"), None);
1609    }
1610
1611    #[test]
1612    fn backbone_extra_params_preserved() {
1613        let config = r#"
1614[reticulum]
1615enable_transport = True
1616
1617[interfaces]
1618  [[Public Entrypoint]]
1619    type = BackboneInterface
1620    enabled = yes
1621    listen_ip = 0.0.0.0
1622    listen_port = 4242
1623    interface_mode = gateway
1624    discoverable = Yes
1625    discovery_name = PizzaSpaghettiMandolino
1626    announce_interval = 600
1627    discovery_stamp_value = 24
1628    reachable_on = 87.106.8.245
1629"#;
1630        let parsed = parse(config).unwrap();
1631        assert_eq!(parsed.interfaces.len(), 1);
1632        let iface = &parsed.interfaces[0];
1633        assert_eq!(iface.name, "Public Entrypoint");
1634        assert_eq!(iface.interface_type, "BackboneInterface");
1635        // After removing type, enabled, interface_mode, remaining params should include discovery keys
1636        assert_eq!(
1637            iface.params.get("discoverable").map(|s| s.as_str()),
1638            Some("Yes")
1639        );
1640        assert_eq!(
1641            iface.params.get("discovery_name").map(|s| s.as_str()),
1642            Some("PizzaSpaghettiMandolino")
1643        );
1644        assert_eq!(
1645            iface.params.get("announce_interval").map(|s| s.as_str()),
1646            Some("600")
1647        );
1648        assert_eq!(
1649            iface
1650                .params
1651                .get("discovery_stamp_value")
1652                .map(|s| s.as_str()),
1653            Some("24")
1654        );
1655        assert_eq!(
1656            iface.params.get("reachable_on").map(|s| s.as_str()),
1657            Some("87.106.8.245")
1658        );
1659        assert_eq!(
1660            iface.params.get("listen_ip").map(|s| s.as_str()),
1661            Some("0.0.0.0")
1662        );
1663        assert_eq!(
1664            iface.params.get("listen_port").map(|s| s.as_str()),
1665            Some("4242")
1666        );
1667    }
1668
1669    #[test]
1670    fn parse_probe_protocol() {
1671        let input = r#"
1672[reticulum]
1673probe_addr = 1.2.3.4:19302
1674probe_protocol = stun
1675"#;
1676        let config = parse(input).unwrap();
1677        assert_eq!(
1678            config.reticulum.probe_addr.as_deref(),
1679            Some("1.2.3.4:19302")
1680        );
1681        assert_eq!(config.reticulum.probe_protocol.as_deref(), Some("stun"));
1682    }
1683
1684    #[test]
1685    fn parse_probe_protocol_defaults_to_none() {
1686        let input = r#"
1687[reticulum]
1688probe_addr = 1.2.3.4:4343
1689"#;
1690        let config = parse(input).unwrap();
1691        assert_eq!(config.reticulum.probe_addr.as_deref(), Some("1.2.3.4:4343"));
1692        assert!(config.reticulum.probe_protocol.is_none());
1693    }
1694}