saorsa_core/
config.rs

1// Copyright 2024 Saorsa Labs Limited
2//
3// This software is dual-licensed under:
4// - GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later)
5// - Commercial License
6//
7// For AGPL-3.0 license, see LICENSE-AGPL-3.0
8// For commercial licensing, contact: saorsalabs@gmail.com
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under these licenses is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
14//! # Configuration Management System
15//!
16//! This module provides comprehensive configuration management for the P2P network,
17//! supporting layered configuration (environment > file > defaults) with validation.
18//!
19//! ## Features
20//! - Environment variable override support
21//! - TOML/JSON configuration file support
22//! - Production and development profiles
23//! - IPv4/IPv6 address validation
24//! - Secure defaults for production
25
26use crate::address::NetworkAddress;
27use crate::error::ConfigError;
28use crate::validation::{
29    ValidationContext, validate_config_value, validate_file_path, validate_network_address,
30};
31use crate::{P2PError, Result};
32use regex::Regex;
33use serde::{Deserialize, Serialize};
34use std::env;
35use std::fs;
36use std::net::SocketAddr;
37use std::path::{Path, PathBuf};
38use std::str::FromStr;
39use tracing::info;
40
41/// Main configuration structure for the P2P network
42#[derive(Debug, Clone, Serialize, Deserialize)]
43#[serde(default)]
44#[derive(Default)]
45pub struct Config {
46    /// Network configuration
47    pub network: NetworkConfig,
48    /// Security configuration
49    pub security: SecurityConfig,
50    /// Storage configuration
51    pub storage: StorageConfig,
52    /// MCP (Model Context Protocol) configuration
53    pub mcp: McpConfig,
54    /// DHT configuration
55    pub dht: DhtConfig,
56    /// Transport configuration
57    pub transport: TransportConfig,
58    /// Identity configuration
59    pub identity: IdentityConfig,
60}
61
62/// Network configuration
63#[derive(Debug, Clone, Serialize, Deserialize)]
64#[serde(default)]
65pub struct NetworkConfig {
66    /// Bootstrap nodes for network discovery
67    pub bootstrap_nodes: Vec<String>,
68    /// Local listen address (0.0.0.0:9000 for all interfaces)
69    pub listen_address: String,
70    /// Public address for external connections (auto-detected if empty)
71    pub public_address: Option<String>,
72    /// Enable IPv6 support
73    pub ipv6_enabled: bool,
74    /// Maximum concurrent connections
75    pub max_connections: usize,
76    /// Connection timeout in seconds
77    pub connection_timeout: u64,
78    /// Keepalive interval in seconds
79    pub keepalive_interval: u64,
80}
81
82/// Security configuration
83#[derive(Debug, Clone, Serialize, Deserialize)]
84#[serde(default)]
85pub struct SecurityConfig {
86    /// Rate limit (requests per second)
87    pub rate_limit: u32,
88    /// Connection limit per IP
89    pub connection_limit: u32,
90    /// Enable TLS/encryption
91    pub encryption_enabled: bool,
92    /// Minimum TLS version (e.g., "1.3")
93    pub min_tls_version: String,
94    /// Security level for identity management
95    pub identity_security_level: String,
96}
97
98/// Storage configuration
99#[derive(Debug, Clone, Serialize, Deserialize)]
100#[serde(default)]
101pub struct StorageConfig {
102    /// Base path for data storage
103    pub path: PathBuf,
104    /// Maximum storage size (e.g., "10GB")
105    pub max_size: String,
106    /// Cache size in MB
107    pub cache_size: u64,
108    /// Enable compression
109    pub compression_enabled: bool,
110}
111
112/// MCP configuration
113#[derive(Debug, Clone, Serialize, Deserialize)]
114#[serde(default)]
115pub struct McpConfig {
116    /// Enable MCP server
117    pub enabled: bool,
118    /// MCP server port
119    pub port: u16,
120    /// Maximum tool execution time in seconds
121    pub max_execution_time: u64,
122    /// Enable resource monitoring
123    pub monitoring_enabled: bool,
124}
125
126/// DHT configuration
127#[derive(Debug, Clone, Serialize, Deserialize)]
128#[serde(default)]
129pub struct DhtConfig {
130    /// Replication factor (K value)
131    pub replication_factor: u8,
132    /// Alpha value for parallel queries
133    pub alpha: u8,
134    /// Beta value for routing
135    pub beta: u8,
136    /// Record TTL in seconds
137    pub record_ttl: u64,
138    /// Enable adaptive routing
139    pub adaptive_routing: bool,
140}
141
142/// Transport configuration
143#[derive(Debug, Clone, Serialize, Deserialize)]
144#[serde(default)]
145pub struct TransportConfig {
146    /// Preferred transport protocol
147    pub protocol: String,
148    /// Enable QUIC transport
149    pub quic_enabled: bool,
150    /// Enable TCP transport
151    pub tcp_enabled: bool,
152    /// Enable WebRTC transport
153    pub webrtc_enabled: bool,
154    /// Transport buffer size
155    pub buffer_size: usize,
156    /// Server name for TLS (SNI)
157    pub server_name: String,
158}
159
160/// Identity configuration
161#[derive(Debug, Clone, Serialize, Deserialize)]
162#[serde(default)]
163pub struct IdentityConfig {
164    /// Default key derivation path
165    pub derivation_path: String,
166    /// Key rotation interval in days
167    pub rotation_interval: u32,
168    /// Enable automatic backups
169    pub backup_enabled: bool,
170    /// Backup interval in hours
171    pub backup_interval: u32,
172}
173
174// Default implementations
175
176impl Default for NetworkConfig {
177    fn default() -> Self {
178        Self {
179            bootstrap_nodes: vec![],
180            // Use environment variable or fallback to localhost for development
181            listen_address: env::var("SAORSA_LISTEN_ADDRESS")
182                .unwrap_or_else(|_| "127.0.0.1:9000".to_string()),
183            public_address: None,
184            ipv6_enabled: true,
185            max_connections: 10000,
186            connection_timeout: 30,
187            keepalive_interval: 60,
188        }
189    }
190}
191
192impl Default for SecurityConfig {
193    fn default() -> Self {
194        Self {
195            rate_limit: 1000,
196            connection_limit: 100,
197            encryption_enabled: true,
198            min_tls_version: "1.3".to_string(),
199            identity_security_level: "High".to_string(),
200        }
201    }
202}
203
204impl Default for StorageConfig {
205    fn default() -> Self {
206        Self {
207            path: PathBuf::from("./data"),
208            max_size: "10GB".to_string(),
209            cache_size: 256,
210            compression_enabled: true,
211        }
212    }
213}
214
215impl Default for McpConfig {
216    fn default() -> Self {
217        Self {
218            enabled: true,
219            port: 9001,
220            max_execution_time: 300,
221            monitoring_enabled: true,
222        }
223    }
224}
225
226impl Default for DhtConfig {
227    fn default() -> Self {
228        Self {
229            replication_factor: 8,
230            alpha: 3,
231            beta: 1,
232            record_ttl: 3600,
233            adaptive_routing: true,
234        }
235    }
236}
237
238impl Default for TransportConfig {
239    fn default() -> Self {
240        Self {
241            protocol: "quic".to_string(),
242            quic_enabled: true,
243            tcp_enabled: true,
244            webrtc_enabled: false,
245            buffer_size: 65536,
246            server_name: "p2p.local".to_string(),
247        }
248    }
249}
250
251impl Default for IdentityConfig {
252    fn default() -> Self {
253        Self {
254            derivation_path: "m/44'/0'/0'/0/0".to_string(),
255            rotation_interval: 90,
256            backup_enabled: true,
257            backup_interval: 24,
258        }
259    }
260}
261
262impl Config {
263    /// Load configuration from multiple sources with precedence:
264    /// 1. Environment variables (highest)
265    /// 2. Configuration file
266    /// 3. Default values (lowest)
267    ///
268    /// # Examples
269    ///
270    /// ```no_run
271    /// use saorsa_core::config::Config;
272    ///
273    /// // Load with default locations
274    /// let config = Config::load()?;
275    ///
276    /// // Access configuration values
277    /// println!("Listen address: {}", config.network.listen_address);
278    /// println!("Rate limit: {}", config.security.rate_limit);
279    /// # Ok::<(), saorsa_core::P2PError>(())
280    /// ```
281    pub fn load() -> Result<Self> {
282        Self::load_with_path::<&str>(None)
283    }
284
285    /// Load configuration with a specific config file path
286    ///
287    /// # Examples
288    ///
289    /// ```no_run
290    /// use saorsa_core::config::Config;
291    ///
292    /// // Load from specific file
293    /// let config = Config::load_with_path(Some("custom.toml"))?;
294    ///
295    /// // Load from optional path
296    /// let path = std::env::var("CONFIG_PATH").ok();
297    /// let config = Config::load_with_path(path.as_ref())?;
298    /// # Ok::<(), saorsa_core::P2PError>(())
299    /// ```
300    pub fn load_with_path<P: AsRef<Path>>(path: Option<P>) -> Result<Self> {
301        // Start with defaults
302        let mut config = Self::default();
303
304        // Load from file if provided or look for default locations
305        if let Some(path) = path {
306            config = Self::load_from_file(path)?;
307        } else {
308            // Try default config locations
309            for location in &["saorsa.toml", "config.toml", "/etc/saorsa/config.toml"] {
310                if Path::new(location).exists() {
311                    info!("Loading config from: {}", location);
312                    config = Self::load_from_file(location)?;
313                    break;
314                }
315            }
316        }
317
318        // Override with environment variables
319        config.apply_env_overrides()?;
320
321        // Validate the final configuration
322        config.validate()?;
323
324        Ok(config)
325    }
326
327    /// Load configuration from a TOML file
328    pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
329        let content = fs::read_to_string(&path).map_err(|e| {
330            P2PError::Config(ConfigError::IoError {
331                path: path.as_ref().to_string_lossy().to_string().into(),
332                source: e,
333            })
334        })?;
335
336        toml::from_str(&content)
337            .map_err(|e| P2PError::Config(ConfigError::ParseError(e.to_string().into())))
338    }
339
340    /// Save configuration to a TOML file
341    pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
342        let content = toml::to_string_pretty(self)
343            .map_err(|e| P2PError::Config(ConfigError::ParseError(e.to_string().into())))?;
344
345        fs::write(&path, content).map_err(|e| {
346            P2PError::Config(ConfigError::IoError {
347                path: path.as_ref().to_string_lossy().to_string().into(),
348                source: e,
349            })
350        })?;
351
352        Ok(())
353    }
354
355    /// Apply environment variable overrides
356    fn apply_env_overrides(&mut self) -> Result<()> {
357        // Network overrides
358        if let Ok(val) = env::var("SAORSA_LISTEN_ADDRESS") {
359            self.network.listen_address = val;
360        }
361        if let Ok(val) = env::var("SAORSA_PUBLIC_ADDRESS") {
362            self.network.public_address = Some(val);
363        }
364        if let Ok(val) = env::var("SAORSA_BOOTSTRAP_NODES") {
365            self.network.bootstrap_nodes = val.split(',').map(String::from).collect();
366        }
367        if let Ok(val) = env::var("SAORSA_MAX_CONNECTIONS") {
368            self.network.max_connections = val.parse().map_err(|_| {
369                P2PError::Config(ConfigError::InvalidValue {
370                    field: "max_connections".to_string().into(),
371                    reason: "Invalid value".to_string().into(),
372                })
373            })?;
374        }
375
376        // Security overrides
377        if let Ok(val) = env::var("SAORSA_RATE_LIMIT") {
378            self.security.rate_limit = val.parse().map_err(|_| {
379                P2PError::Config(ConfigError::InvalidValue {
380                    field: "rate_limit".to_string().into(),
381                    reason: "Invalid value".to_string().into(),
382                })
383            })?;
384        }
385        if let Ok(val) = env::var("SAORSA_ENCRYPTION_ENABLED") {
386            self.security.encryption_enabled = val.parse().map_err(|_| {
387                P2PError::Config(ConfigError::InvalidValue {
388                    field: "encryption_enabled".to_string().into(),
389                    reason: "Invalid value".to_string().into(),
390                })
391            })?;
392        }
393
394        // Storage overrides
395        if let Ok(val) = env::var("SAORSA_DATA_PATH") {
396            self.storage.path = PathBuf::from(val);
397        }
398        if let Ok(val) = env::var("SAORSA_MAX_STORAGE") {
399            self.storage.max_size = val;
400        }
401
402        // MCP overrides
403        if let Ok(val) = env::var("SAORSA_MCP_ENABLED") {
404            self.mcp.enabled = val.parse().map_err(|_| {
405                P2PError::Config(ConfigError::InvalidValue {
406                    field: "mcp_enabled".to_string().into(),
407                    reason: "Invalid value".to_string().into(),
408                })
409            })?;
410        }
411        if let Ok(val) = env::var("SAORSA_MCP_PORT") {
412            self.mcp.port = val.parse().map_err(|_| {
413                P2PError::Config(ConfigError::InvalidValue {
414                    field: "mcp_port".to_string().into(),
415                    reason: "Invalid value".to_string().into(),
416                })
417            })?;
418        }
419
420        Ok(())
421    }
422
423    /// Validate the configuration
424    pub fn validate(&self) -> Result<()> {
425        let mut errors = Vec::new();
426
427        // Validate network addresses
428        if let Err(e) = self.validate_address(&self.network.listen_address, "listen_address") {
429            errors.push(e);
430        }
431
432        if let Some(addr) = &self.network.public_address
433            && let Err(e) = self.validate_address(addr, "public_address")
434        {
435            errors.push(e);
436        }
437
438        for (i, node) in self.network.bootstrap_nodes.iter().enumerate() {
439            if let Err(e) = self.validate_address(node, &format!("bootstrap_node[{}]", i)) {
440                errors.push(e);
441            }
442        }
443
444        // Validate ranges using validation framework
445        if let Err(e) = validate_config_value(
446            &self.network.max_connections.to_string(),
447            Some(1_usize),
448            Some(100_000_usize),
449        ) {
450            errors.push(P2PError::Config(ConfigError::InvalidValue {
451                field: "max_connections".to_string().into(),
452                reason: e.to_string().into(),
453            }));
454        }
455
456        if let Err(e) = validate_config_value(
457            &self.security.rate_limit.to_string(),
458            Some(1_u32),
459            Some(1_000_000_u32),
460        ) {
461            errors.push(P2PError::Config(ConfigError::InvalidValue {
462                field: "rate_limit".to_string().into(),
463                reason: e.to_string().into(),
464            }));
465        }
466
467        // Validate storage path
468        if let Err(e) = validate_file_path(&self.storage.path) {
469            errors.push(P2PError::Config(ConfigError::InvalidValue {
470                field: "storage.path".to_string().into(),
471                reason: e.to_string().into(),
472            }));
473        }
474
475        // Validate storage size format
476        if !self.validate_size_format(&self.storage.max_size) {
477            errors.push(P2PError::Config(ConfigError::InvalidValue {
478                field: "max_size".to_string().into(),
479                reason: format!("Invalid storage size format: {}", self.storage.max_size).into(),
480            }));
481        }
482
483        // Validate transport protocol
484        match self.transport.protocol.as_str() {
485            "quic" | "tcp" | "webrtc" => {}
486            _ => errors.push(P2PError::Config(ConfigError::InvalidValue {
487                field: "protocol".to_string().into(),
488                reason: format!("Invalid transport protocol: {}", self.transport.protocol).into(),
489            })),
490        }
491
492        if errors.is_empty() {
493            Ok(())
494        } else {
495            // Return the first error, or a generic error if somehow the vec is empty
496            Err(errors.into_iter().next().unwrap_or_else(|| {
497                P2PError::Config(ConfigError::InvalidValue {
498                    field: "unknown".to_string().into(),
499                    reason: "Validation failed with unknown error".to_string().into(),
500                })
501            }))
502        }
503    }
504
505    /// Validate network address format
506    fn validate_address(&self, addr: &str, field: &str) -> Result<()> {
507        // Try parsing as SocketAddr first
508        if let Ok(socket_addr) = SocketAddr::from_str(addr) {
509            // Use our validation framework
510            let ctx = ValidationContext::default()
511                .allow_localhost() // Allow localhost for development
512                .allow_private_ips(); // Allow private IPs for development
513
514            return validate_network_address(&socket_addr, &ctx).map_err(|e| {
515                P2PError::Config(ConfigError::InvalidValue {
516                    field: field.to_string().into(),
517                    reason: e.to_string().into(),
518                })
519            });
520        }
521
522        // Try parsing as four-word address format (always enabled)
523        if let Ok(network_addr) = crate::NetworkAddress::from_four_words(addr) {
524            // Validate the parsed socket address
525            let ctx = ValidationContext::default()
526                .allow_localhost()
527                .allow_private_ips();
528
529            return validate_network_address(&network_addr.socket_addr(), &ctx).map_err(|e| {
530                P2PError::Config(ConfigError::InvalidValue {
531                    field: field.to_string().into(),
532                    reason: e.to_string().into(),
533                })
534            });
535        }
536
537        // Try parsing as multiaddr format
538        if addr.starts_with("/ip4/") || addr.starts_with("/ip6/") {
539            // Basic multiaddr validation
540            return Ok(());
541        }
542
543        Err(P2PError::Config(ConfigError::InvalidValue {
544            field: field.to_string().into(),
545            reason: format!("Invalid address format: {}", addr).into(),
546        }))
547    }
548
549    /// Validate size format (e.g., "10GB", "500MB")
550    fn validate_size_format(&self, size: &str) -> bool {
551        thread_local! {
552            static SIZE_REGEX: std::result::Result<Regex, P2PError> = Regex::new(r"^\\d+(?:\\.\\d+)?\\s*(?:B|KB|MB|GB|TB)$")
553                .map_err(|e| P2PError::Config(ConfigError::InvalidValue { field: "size".to_string().into(), reason: e.to_string().into() }));
554        }
555        SIZE_REGEX.with(|re| re.as_ref().ok().map(|r| r.is_match(size)).unwrap_or(false))
556    }
557
558    /// Create development configuration
559    pub fn development() -> Self {
560        let mut config = Self::default();
561        config.network.listen_address = "127.0.0.1:9000".to_string();
562        config.security.rate_limit = 10000;
563        config.security.connection_limit = 1000;
564        config.storage.path = PathBuf::from("./dev-data");
565        config
566    }
567
568    /// Create production configuration with secure defaults
569    pub fn production() -> Self {
570        let mut config = Self::default();
571        // Use environment variable or fallback to secure default
572        config.network.listen_address =
573            env::var("SAORSA_LISTEN_ADDRESS").unwrap_or_else(|_| "0.0.0.0:9000".to_string());
574        config.security.rate_limit = 1000;
575        config.security.connection_limit = 100;
576        config.storage.path = PathBuf::from("/var/lib/saorsa");
577        config
578    }
579
580    /// Get parsed listen address
581    pub fn listen_socket_addr(&self) -> Result<SocketAddr> {
582        SocketAddr::from_str(&self.network.listen_address).map_err(|e| {
583            P2PError::Config(ConfigError::InvalidValue {
584                field: "listen_address".to_string().into(),
585                reason: format!("Invalid address: {}", e).into(),
586            })
587        })
588    }
589
590    /// Get parsed bootstrap addresses
591    pub fn bootstrap_addrs(&self) -> Result<Vec<NetworkAddress>> {
592        self.network
593            .bootstrap_nodes
594            .iter()
595            .map(|addr| {
596                addr.parse::<NetworkAddress>().map_err(|e| {
597                    P2PError::Config(ConfigError::InvalidValue {
598                        field: "bootstrap_nodes".to_string().into(),
599                        reason: format!("Invalid address: {}", e).into(),
600                    })
601                })
602            })
603            .collect()
604    }
605
606    /// Parse a size string (e.g., "10GB", "500MB") to bytes
607    ///
608    /// # Examples
609    ///
610    /// ```ignore
611    /// use saorsa_core::config::Config;
612    ///
613    /// assert_eq!(Config::parse_size("10B").unwrap(), 10);
614    /// assert_eq!(Config::parse_size("1KB").unwrap(), 1024);
615    /// assert_eq!(Config::parse_size("5MB").unwrap(), 5 * 1024 * 1024);
616    /// ```
617    pub fn parse_size(size: &str) -> Result<u64> {
618        thread_local! {
619            static SIZE_REGEX: std::result::Result<Regex, P2PError> = Regex::new(r"^(\\d+(?:\\.\\d+)?)\\s*(B|KB|MB|GB|TB)$")
620                .map_err(|e| P2PError::Config(ConfigError::InvalidValue { field: "size".to_string().into(), reason: e.to_string().into() }));
621        }
622
623        SIZE_REGEX.with(|re| -> Result<u64> {
624            let re = match re {
625                Ok(r) => r,
626                Err(e) => {
627                    return Err(P2PError::Config(ConfigError::InvalidValue {
628                        field: "size".to_string().into(),
629                        reason: e.to_string().into(),
630                    }));
631                }
632            };
633            if let Some(captures) = re.captures(size) {
634                let value: f64 = captures
635                    .get(1)
636                    .and_then(|m| m.as_str().parse().ok())
637                    .ok_or_else(|| {
638                        P2PError::Config(ConfigError::InvalidValue {
639                            field: "size".to_string().into(),
640                            reason: "Invalid numeric value".to_string().into(),
641                        })
642                    })?;
643
644                let unit = captures.get(2).map(|m| m.as_str()).unwrap_or("B");
645                let multiplier = match unit {
646                    "B" => 1u64,
647                    "KB" => 1024,
648                    "MB" => 1024 * 1024,
649                    "GB" => 1024 * 1024 * 1024,
650                    "TB" => 1024u64.pow(4),
651                    _ => {
652                        return Err(P2PError::Config(ConfigError::InvalidValue {
653                            field: "size".to_string().into(),
654                            reason: format!("Unknown unit: {}", unit).into(),
655                        }));
656                    }
657                };
658
659                Ok((value * multiplier as f64) as u64)
660            } else {
661                Err(P2PError::Config(ConfigError::InvalidValue {
662                    field: "size".to_string().into(),
663                    reason: format!("Invalid size format: {}", size).into(),
664                }))
665            }
666        })
667    }
668
669    /// Get storage max size in bytes
670    pub fn storage_max_size_bytes(&self) -> Result<u64> {
671        Self::parse_size(&self.storage.max_size)
672    }
673}
674
675#[cfg(test)]
676mod tests {
677    use super::*;
678    use tempfile::NamedTempFile;
679
680    #[test]
681    fn test_default_config() {
682        let config = Config::default();
683        assert_eq!(config.network.listen_address, "0.0.0.0:9000");
684        assert_eq!(config.security.rate_limit, 1000);
685        assert!(config.security.encryption_enabled);
686    }
687
688    #[test]
689    fn test_development_config() {
690        let config = Config::development();
691        assert_eq!(config.network.listen_address, "127.0.0.1:9000");
692        assert_eq!(config.security.rate_limit, 10000);
693    }
694
695    #[test]
696    fn test_production_config() {
697        let config = Config::production();
698        assert_eq!(config.network.listen_address, "0.0.0.0:9000");
699        assert_eq!(config.transport.buffer_size, 131072);
700    }
701
702    #[test]
703    fn test_config_validation() {
704        let mut config = Config::default();
705        assert!(config.validate().is_ok());
706
707        // Invalid address
708        config.network.listen_address = "invalid".to_string();
709        assert!(config.validate().is_err());
710
711        // Valid multiaddr
712        config.network.listen_address = "/ip4/127.0.0.1/tcp/9000".to_string();
713        assert!(config.validate().is_ok());
714    }
715
716    #[test]
717    fn test_save_and_load_config() {
718        let config = Config::development();
719        let file = NamedTempFile::new().unwrap();
720
721        config.save_to_file(file.path()).unwrap();
722
723        let loaded = Config::load_from_file(file.path()).unwrap();
724        assert_eq!(loaded.network.listen_address, config.network.listen_address);
725    }
726
727    #[test]
728    #[serial_test::serial]
729    #[allow(unsafe_code)] // Required for env::set_var in tests only
730    fn test_env_overrides() {
731        use std::sync::Mutex;
732
733        // Use a static mutex to ensure thread safety
734        static ENV_MUTEX: Mutex<()> = Mutex::new(());
735        let _guard = ENV_MUTEX.lock().unwrap();
736
737        // Save original values
738        let orig_listen = env::var("SAORSA_LISTEN_ADDRESS").ok();
739        let orig_rate = env::var("SAORSA_RATE_LIMIT").ok();
740
741        // Set test values - unsafe blocks required in Rust 2024
742        unsafe {
743            env::set_var("SAORSA_LISTEN_ADDRESS", "127.0.0.1:8000");
744            env::set_var("SAORSA_RATE_LIMIT", "5000");
745        }
746
747        let config = Config::load().unwrap();
748        assert_eq!(config.network.listen_address, "127.0.0.1:8000");
749        assert_eq!(config.security.rate_limit, 5000);
750
751        // Restore original values
752        unsafe {
753            match orig_listen {
754                Some(val) => env::set_var("SAORSA_LISTEN_ADDRESS", val),
755                None => env::remove_var("SAORSA_LISTEN_ADDRESS"),
756            }
757            match orig_rate {
758                Some(val) => env::set_var("SAORSA_RATE_LIMIT", val),
759                None => env::remove_var("SAORSA_RATE_LIMIT"),
760            }
761        }
762    }
763
764    #[test]
765    fn test_size_validation() {
766        let config = Config::default();
767        assert!(config.validate_size_format("10GB"));
768        assert!(config.validate_size_format("500MB"));
769        assert!(config.validate_size_format("1.5TB"));
770        assert!(!config.validate_size_format("10XB"));
771        assert!(!config.validate_size_format("invalid"));
772    }
773
774    #[test]
775    fn test_size_parsing() {
776        assert_eq!(Config::parse_size("10B").unwrap(), 10);
777        assert_eq!(Config::parse_size("1KB").unwrap(), 1024);
778        assert_eq!(Config::parse_size("5MB").unwrap(), 5 * 1024 * 1024);
779        assert_eq!(Config::parse_size("1GB").unwrap(), 1024 * 1024 * 1024);
780        assert_eq!(Config::parse_size("1.5GB").unwrap(), 1610612736);
781        assert_eq!(Config::parse_size("1TB").unwrap(), 1024u64.pow(4));
782
783        // Test error cases
784        assert!(Config::parse_size("invalid").is_err());
785        assert!(Config::parse_size("10XB").is_err());
786        assert!(Config::parse_size("GB").is_err());
787    }
788}