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            listen_address: "0.0.0.0:9000".to_string(),
181            public_address: None,
182            ipv6_enabled: true,
183            max_connections: 10000,
184            connection_timeout: 30,
185            keepalive_interval: 60,
186        }
187    }
188}
189
190impl Default for SecurityConfig {
191    fn default() -> Self {
192        Self {
193            rate_limit: 1000,
194            connection_limit: 100,
195            encryption_enabled: true,
196            min_tls_version: "1.3".to_string(),
197            identity_security_level: "High".to_string(),
198        }
199    }
200}
201
202impl Default for StorageConfig {
203    fn default() -> Self {
204        Self {
205            path: PathBuf::from("./data"),
206            max_size: "10GB".to_string(),
207            cache_size: 256,
208            compression_enabled: true,
209        }
210    }
211}
212
213impl Default for McpConfig {
214    fn default() -> Self {
215        Self {
216            enabled: true,
217            port: 9001,
218            max_execution_time: 300,
219            monitoring_enabled: true,
220        }
221    }
222}
223
224impl Default for DhtConfig {
225    fn default() -> Self {
226        Self {
227            replication_factor: 8,
228            alpha: 3,
229            beta: 1,
230            record_ttl: 3600,
231            adaptive_routing: true,
232        }
233    }
234}
235
236impl Default for TransportConfig {
237    fn default() -> Self {
238        Self {
239            protocol: "quic".to_string(),
240            quic_enabled: true,
241            tcp_enabled: true,
242            webrtc_enabled: false,
243            buffer_size: 65536,
244            server_name: "p2p.local".to_string(),
245        }
246    }
247}
248
249impl Default for IdentityConfig {
250    fn default() -> Self {
251        Self {
252            derivation_path: "m/44'/0'/0'/0/0".to_string(),
253            rotation_interval: 90,
254            backup_enabled: true,
255            backup_interval: 24,
256        }
257    }
258}
259
260impl Config {
261    /// Load configuration from multiple sources with precedence:
262    /// 1. Environment variables (highest)
263    /// 2. Configuration file
264    /// 3. Default values (lowest)
265    ///
266    /// # Examples
267    ///
268    /// ```no_run
269    /// use saorsa_core::config::Config;
270    ///
271    /// // Load with default locations
272    /// let config = Config::load()?;
273    ///
274    /// // Access configuration values
275    /// println!("Listen address: {}", config.network.listen_address);
276    /// println!("Rate limit: {}", config.security.rate_limit);
277    /// # Ok::<(), saorsa_core::P2PError>(())
278    /// ```
279    pub fn load() -> Result<Self> {
280        Self::load_with_path::<&str>(None)
281    }
282
283    /// Load configuration with a specific config file path
284    ///
285    /// # Examples
286    ///
287    /// ```no_run
288    /// use saorsa_core::config::Config;
289    ///
290    /// // Load from specific file
291    /// let config = Config::load_with_path(Some("custom.toml"))?;
292    ///
293    /// // Load from optional path
294    /// let path = std::env::var("CONFIG_PATH").ok();
295    /// let config = Config::load_with_path(path.as_ref())?;
296    /// # Ok::<(), saorsa_core::P2PError>(())
297    /// ```
298    pub fn load_with_path<P: AsRef<Path>>(path: Option<P>) -> Result<Self> {
299        // Start with defaults
300        let mut config = Self::default();
301
302        // Load from file if provided or look for default locations
303        if let Some(path) = path {
304            config = Self::load_from_file(path)?;
305        } else {
306            // Try default config locations
307            for location in &["saorsa.toml", "config.toml", "/etc/saorsa/config.toml"] {
308                if Path::new(location).exists() {
309                    info!("Loading config from: {}", location);
310                    config = Self::load_from_file(location)?;
311                    break;
312                }
313            }
314        }
315
316        // Override with environment variables
317        config.apply_env_overrides()?;
318
319        // Validate the final configuration
320        config.validate()?;
321
322        Ok(config)
323    }
324
325    /// Load configuration from a TOML file
326    pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
327        let content = fs::read_to_string(&path).map_err(|e| {
328            P2PError::Config(ConfigError::IoError {
329                path: path.as_ref().to_string_lossy().to_string().into(),
330                source: e,
331            })
332        })?;
333
334        toml::from_str(&content)
335            .map_err(|e| P2PError::Config(ConfigError::ParseError(e.to_string().into())))
336    }
337
338    /// Save configuration to a TOML file
339    pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
340        let content = toml::to_string_pretty(self)
341            .map_err(|e| P2PError::Config(ConfigError::ParseError(e.to_string().into())))?;
342
343        fs::write(&path, content).map_err(|e| {
344            P2PError::Config(ConfigError::IoError {
345                path: path.as_ref().to_string_lossy().to_string().into(),
346                source: e,
347            })
348        })?;
349
350        Ok(())
351    }
352
353    /// Apply environment variable overrides
354    fn apply_env_overrides(&mut self) -> Result<()> {
355        // Network overrides
356        if let Ok(val) = env::var("SAORSA_LISTEN_ADDRESS") {
357            self.network.listen_address = val;
358        }
359        if let Ok(val) = env::var("SAORSA_PUBLIC_ADDRESS") {
360            self.network.public_address = Some(val);
361        }
362        if let Ok(val) = env::var("SAORSA_BOOTSTRAP_NODES") {
363            self.network.bootstrap_nodes = val.split(',').map(String::from).collect();
364        }
365        if let Ok(val) = env::var("SAORSA_MAX_CONNECTIONS") {
366            self.network.max_connections = val.parse().map_err(|_| {
367                P2PError::Config(ConfigError::InvalidValue {
368                    field: "max_connections".to_string().into(),
369                    reason: "Invalid value".to_string().into(),
370                })
371            })?;
372        }
373
374        // Security overrides
375        if let Ok(val) = env::var("SAORSA_RATE_LIMIT") {
376            self.security.rate_limit = val.parse().map_err(|_| {
377                P2PError::Config(ConfigError::InvalidValue {
378                    field: "rate_limit".to_string().into(),
379                    reason: "Invalid value".to_string().into(),
380                })
381            })?;
382        }
383        if let Ok(val) = env::var("SAORSA_ENCRYPTION_ENABLED") {
384            self.security.encryption_enabled = val.parse().map_err(|_| {
385                P2PError::Config(ConfigError::InvalidValue {
386                    field: "encryption_enabled".to_string().into(),
387                    reason: "Invalid value".to_string().into(),
388                })
389            })?;
390        }
391
392        // Storage overrides
393        if let Ok(val) = env::var("SAORSA_DATA_PATH") {
394            self.storage.path = PathBuf::from(val);
395        }
396        if let Ok(val) = env::var("SAORSA_MAX_STORAGE") {
397            self.storage.max_size = val;
398        }
399
400        // MCP overrides
401        if let Ok(val) = env::var("SAORSA_MCP_ENABLED") {
402            self.mcp.enabled = val.parse().map_err(|_| {
403                P2PError::Config(ConfigError::InvalidValue {
404                    field: "mcp_enabled".to_string().into(),
405                    reason: "Invalid value".to_string().into(),
406                })
407            })?;
408        }
409        if let Ok(val) = env::var("SAORSA_MCP_PORT") {
410            self.mcp.port = val.parse().map_err(|_| {
411                P2PError::Config(ConfigError::InvalidValue {
412                    field: "mcp_port".to_string().into(),
413                    reason: "Invalid value".to_string().into(),
414                })
415            })?;
416        }
417
418        Ok(())
419    }
420
421    /// Validate the configuration
422    pub fn validate(&self) -> Result<()> {
423        let mut errors = Vec::new();
424
425        // Validate network addresses
426        if let Err(e) = self.validate_address(&self.network.listen_address, "listen_address") {
427            errors.push(e);
428        }
429
430        if let Some(addr) = &self.network.public_address
431            && let Err(e) = self.validate_address(addr, "public_address")
432        {
433            errors.push(e);
434        }
435
436        for (i, node) in self.network.bootstrap_nodes.iter().enumerate() {
437            if let Err(e) = self.validate_address(node, &format!("bootstrap_node[{}]", i)) {
438                errors.push(e);
439            }
440        }
441
442        // Validate ranges using validation framework
443        if let Err(e) = validate_config_value(
444            &self.network.max_connections.to_string(),
445            Some(1_usize),
446            Some(100000_usize),
447        ) {
448            errors.push(P2PError::Config(ConfigError::InvalidValue {
449                field: "max_connections".to_string().into(),
450                reason: e.to_string().into(),
451            }));
452        }
453
454        if let Err(e) = validate_config_value(
455            &self.security.rate_limit.to_string(),
456            Some(1_u32),
457            Some(1000000_u32),
458        ) {
459            errors.push(P2PError::Config(ConfigError::InvalidValue {
460                field: "rate_limit".to_string().into(),
461                reason: e.to_string().into(),
462            }));
463        }
464
465        // Validate storage path
466        if let Err(e) = validate_file_path(&self.storage.path) {
467            errors.push(P2PError::Config(ConfigError::InvalidValue {
468                field: "storage.path".to_string().into(),
469                reason: e.to_string().into(),
470            }));
471        }
472
473        // Validate storage size format
474        if !self.validate_size_format(&self.storage.max_size) {
475            errors.push(P2PError::Config(ConfigError::InvalidValue {
476                field: "max_size".to_string().into(),
477                reason: format!("Invalid storage size format: {}", self.storage.max_size).into(),
478            }));
479        }
480
481        // Validate transport protocol
482        match self.transport.protocol.as_str() {
483            "quic" | "tcp" | "webrtc" => {}
484            _ => errors.push(P2PError::Config(ConfigError::InvalidValue {
485                field: "protocol".to_string().into(),
486                reason: format!("Invalid transport protocol: {}", self.transport.protocol).into(),
487            })),
488        }
489
490        if errors.is_empty() {
491            Ok(())
492        } else {
493            // Return the first error, or a generic error if somehow the vec is empty
494            Err(errors.into_iter().next().unwrap_or_else(|| {
495                P2PError::Config(ConfigError::InvalidValue {
496                    field: "unknown".to_string().into(),
497                    reason: "Validation failed with unknown error".to_string().into(),
498                })
499            }))
500        }
501    }
502
503    /// Validate network address format
504    fn validate_address(&self, addr: &str, field: &str) -> Result<()> {
505        // Try parsing as SocketAddr first
506        if let Ok(socket_addr) = SocketAddr::from_str(addr) {
507            // Use our validation framework
508            let ctx = ValidationContext::default()
509                .allow_localhost() // Allow localhost for development
510                .allow_private_ips(); // Allow private IPs for development
511
512            return validate_network_address(&socket_addr, &ctx).map_err(|e| {
513                P2PError::Config(ConfigError::InvalidValue {
514                    field: field.to_string().into(),
515                    reason: e.to_string().into(),
516                })
517            });
518        }
519
520        // Try parsing as four-word address format (always enabled)
521        if let Ok(network_addr) = crate::NetworkAddress::from_four_words(addr) {
522            // Validate the parsed socket address
523            let ctx = ValidationContext::default()
524                .allow_localhost()
525                .allow_private_ips();
526
527            return validate_network_address(&network_addr.socket_addr(), &ctx).map_err(|e| {
528                P2PError::Config(ConfigError::InvalidValue {
529                    field: field.to_string().into(),
530                    reason: e.to_string().into(),
531                })
532            });
533        }
534
535        // Try parsing as multiaddr format
536        if addr.starts_with("/ip4/") || addr.starts_with("/ip6/") {
537            // Basic multiaddr validation
538            return Ok(());
539        }
540
541        Err(P2PError::Config(ConfigError::InvalidValue {
542            field: field.to_string().into(),
543            reason: format!("Invalid address format: {}", addr).into(),
544        }))
545    }
546
547    /// Validate size format (e.g., "10GB", "500MB")
548    fn validate_size_format(&self, size: &str) -> bool {
549        thread_local! {
550            static SIZE_REGEX: std::result::Result<Regex, P2PError> = Regex::new(r"^\\d+(?:\\.\\d+)?\\s*(?:B|KB|MB|GB|TB)$")
551                .map_err(|e| P2PError::Config(ConfigError::InvalidValue { field: "size".to_string().into(), reason: e.to_string().into() }));
552        }
553        SIZE_REGEX.with(|re| re.as_ref().ok().map(|r| r.is_match(size)).unwrap_or(false))
554    }
555
556    /// Create development configuration
557    pub fn development() -> Self {
558        let mut config = Self::default();
559        config.network.listen_address = "127.0.0.1:9000".to_string();
560        config.security.rate_limit = 10000;
561        config.security.connection_limit = 1000;
562        config.storage.path = PathBuf::from("./dev-data");
563        config
564    }
565
566    /// Create production configuration
567    pub fn production() -> Self {
568        let mut config = Self::default();
569        config.network.listen_address = "0.0.0.0:9000".to_string();
570        config.security.min_tls_version = "1.3".to_string();
571        config.security.encryption_enabled = true;
572        config.storage.compression_enabled = true;
573        config.transport.buffer_size = 131072; // 128KB
574        config
575    }
576
577    /// Get parsed listen address
578    pub fn listen_socket_addr(&self) -> Result<SocketAddr> {
579        SocketAddr::from_str(&self.network.listen_address).map_err(|e| {
580            P2PError::Config(ConfigError::InvalidValue {
581                field: "listen_address".to_string().into(),
582                reason: format!("Invalid address: {}", e).into(),
583            })
584        })
585    }
586
587    /// Get parsed bootstrap addresses
588    pub fn bootstrap_addrs(&self) -> Result<Vec<NetworkAddress>> {
589        self.network
590            .bootstrap_nodes
591            .iter()
592            .map(|addr| {
593                addr.parse::<NetworkAddress>().map_err(|e| {
594                    P2PError::Config(ConfigError::InvalidValue {
595                        field: "bootstrap_nodes".to_string().into(),
596                        reason: format!("Invalid address: {}", e).into(),
597                    })
598                })
599            })
600            .collect()
601    }
602
603    /// Parse a size string (e.g., "10GB", "500MB") to bytes
604    ///
605    /// # Examples
606    ///
607    /// ```
608    /// use saorsa_core::config::Config;
609    ///
610    /// assert_eq!(Config::parse_size("10B").unwrap(), 10);
611    /// assert_eq!(Config::parse_size("1KB").unwrap(), 1024);
612    /// assert_eq!(Config::parse_size("5MB").unwrap(), 5 * 1024 * 1024);
613    /// assert_eq!(Config::parse_size("1.5GB").unwrap(), 1610612736);
614    /// ```
615    pub fn parse_size(size: &str) -> Result<u64> {
616        thread_local! {
617            static SIZE_REGEX: std::result::Result<Regex, P2PError> = Regex::new(r"^(\\d+(?:\\.\\d+)?)\\s*(B|KB|MB|GB|TB)$")
618                .map_err(|e| P2PError::Config(ConfigError::InvalidValue { field: "size".to_string().into(), reason: e.to_string().into() }));
619        }
620
621        SIZE_REGEX.with(|re| -> Result<u64> {
622            let re = match re {
623                Ok(r) => r,
624                Err(e) => {
625                    return Err(P2PError::Config(ConfigError::InvalidValue {
626                        field: "size".to_string().into(),
627                        reason: e.to_string().into(),
628                    }));
629                }
630            };
631            if let Some(captures) = re.captures(size) {
632                let value: f64 = captures
633                    .get(1)
634                    .and_then(|m| m.as_str().parse().ok())
635                    .ok_or_else(|| {
636                        P2PError::Config(ConfigError::InvalidValue {
637                            field: "size".to_string().into(),
638                            reason: "Invalid numeric value".to_string().into(),
639                        })
640                    })?;
641
642                let unit = captures.get(2).map(|m| m.as_str()).unwrap_or("B");
643                let multiplier = match unit {
644                    "B" => 1u64,
645                    "KB" => 1024,
646                    "MB" => 1024 * 1024,
647                    "GB" => 1024 * 1024 * 1024,
648                    "TB" => 1024u64.pow(4),
649                    _ => {
650                        return Err(P2PError::Config(ConfigError::InvalidValue {
651                            field: "size".to_string().into(),
652                            reason: format!("Unknown unit: {}", unit).into(),
653                        }));
654                    }
655                };
656
657                Ok((value * multiplier as f64) as u64)
658            } else {
659                Err(P2PError::Config(ConfigError::InvalidValue {
660                    field: "size".to_string().into(),
661                    reason: format!("Invalid size format: {}", size).into(),
662                }))
663            }
664        })
665    }
666
667    /// Get storage max size in bytes
668    pub fn storage_max_size_bytes(&self) -> Result<u64> {
669        Self::parse_size(&self.storage.max_size)
670    }
671}
672
673#[cfg(test)]
674mod tests {
675    use super::*;
676    use tempfile::NamedTempFile;
677
678    #[test]
679    fn test_default_config() {
680        let config = Config::default();
681        assert_eq!(config.network.listen_address, "0.0.0.0:9000");
682        assert_eq!(config.security.rate_limit, 1000);
683        assert!(config.security.encryption_enabled);
684    }
685
686    #[test]
687    fn test_development_config() {
688        let config = Config::development();
689        assert_eq!(config.network.listen_address, "127.0.0.1:9000");
690        assert_eq!(config.security.rate_limit, 10000);
691    }
692
693    #[test]
694    fn test_production_config() {
695        let config = Config::production();
696        assert_eq!(config.network.listen_address, "0.0.0.0:9000");
697        assert_eq!(config.transport.buffer_size, 131072);
698    }
699
700    #[test]
701    fn test_config_validation() {
702        let mut config = Config::default();
703        assert!(config.validate().is_ok());
704
705        // Invalid address
706        config.network.listen_address = "invalid".to_string();
707        assert!(config.validate().is_err());
708
709        // Valid multiaddr
710        config.network.listen_address = "/ip4/127.0.0.1/tcp/9000".to_string();
711        assert!(config.validate().is_ok());
712    }
713
714    #[test]
715    fn test_save_and_load_config() {
716        let config = Config::development();
717        let file = NamedTempFile::new().unwrap();
718
719        config.save_to_file(file.path()).unwrap();
720
721        let loaded = Config::load_from_file(file.path()).unwrap();
722        assert_eq!(loaded.network.listen_address, config.network.listen_address);
723    }
724
725    #[test]
726    #[serial_test::serial]
727    #[allow(unsafe_code)] // Required for env::set_var in tests only
728    fn test_env_overrides() {
729        use std::sync::Mutex;
730
731        // Use a static mutex to ensure thread safety
732        static ENV_MUTEX: Mutex<()> = Mutex::new(());
733        let _guard = ENV_MUTEX.lock().unwrap();
734
735        // Save original values
736        let orig_listen = env::var("SAORSA_LISTEN_ADDRESS").ok();
737        let orig_rate = env::var("SAORSA_RATE_LIMIT").ok();
738
739        // Set test values - unsafe blocks required in Rust 2024
740        unsafe {
741            env::set_var("SAORSA_LISTEN_ADDRESS", "127.0.0.1:8000");
742            env::set_var("SAORSA_RATE_LIMIT", "5000");
743        }
744
745        let config = Config::load().unwrap();
746        assert_eq!(config.network.listen_address, "127.0.0.1:8000");
747        assert_eq!(config.security.rate_limit, 5000);
748
749        // Restore original values
750        unsafe {
751            match orig_listen {
752                Some(val) => env::set_var("SAORSA_LISTEN_ADDRESS", val),
753                None => env::remove_var("SAORSA_LISTEN_ADDRESS"),
754            }
755            match orig_rate {
756                Some(val) => env::set_var("SAORSA_RATE_LIMIT", val),
757                None => env::remove_var("SAORSA_RATE_LIMIT"),
758            }
759        }
760    }
761
762    #[test]
763    fn test_size_validation() {
764        let config = Config::default();
765        assert!(config.validate_size_format("10GB"));
766        assert!(config.validate_size_format("500MB"));
767        assert!(config.validate_size_format("1.5TB"));
768        assert!(!config.validate_size_format("10XB"));
769        assert!(!config.validate_size_format("invalid"));
770    }
771
772    #[test]
773    fn test_size_parsing() {
774        assert_eq!(Config::parse_size("10B").unwrap(), 10);
775        assert_eq!(Config::parse_size("1KB").unwrap(), 1024);
776        assert_eq!(Config::parse_size("5MB").unwrap(), 5 * 1024 * 1024);
777        assert_eq!(Config::parse_size("1GB").unwrap(), 1024 * 1024 * 1024);
778        assert_eq!(Config::parse_size("1.5GB").unwrap(), 1610612736);
779        assert_eq!(Config::parse_size("1TB").unwrap(), 1024u64.pow(4));
780
781        // Test error cases
782        assert!(Config::parse_size("invalid").is_err());
783        assert!(Config::parse_size("10XB").is_err());
784        assert!(Config::parse_size("GB").is_err());
785    }
786}