Skip to main content

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: david@saorsalabs.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
53    /// DHT configuration
54    pub dht: DhtConfig,
55    /// Transport configuration
56    pub transport: TransportConfig,
57    /// Identity configuration
58    pub identity: IdentityConfig,
59}
60
61/// Network configuration
62#[derive(Debug, Clone, Serialize, Deserialize)]
63#[serde(default)]
64pub struct NetworkConfig {
65    /// Bootstrap nodes for network discovery
66    pub bootstrap_nodes: Vec<String>,
67    /// Local listen address (0.0.0.0:9000 for all interfaces)
68    pub listen_address: String,
69    /// Public address for external connections (auto-detected if empty)
70    pub public_address: Option<String>,
71    /// Enable IPv6 support
72    pub ipv6_enabled: bool,
73    /// Maximum concurrent connections
74    pub max_connections: usize,
75    /// Connection timeout in seconds
76    pub connection_timeout: u64,
77    /// Keepalive interval in seconds
78    pub keepalive_interval: u64,
79}
80
81/// Security configuration
82#[derive(Debug, Clone, Serialize, Deserialize)]
83#[serde(default)]
84pub struct SecurityConfig {
85    /// Rate limit (requests per second)
86    pub rate_limit: u32,
87    /// Connection limit per IP
88    pub connection_limit: u32,
89    /// Enable TLS/encryption
90    pub encryption_enabled: bool,
91    /// Minimum TLS version (e.g., "1.3")
92    pub min_tls_version: String,
93    /// Security level for identity management
94    pub identity_security_level: String,
95}
96
97/// Storage configuration
98#[derive(Debug, Clone, Serialize, Deserialize)]
99#[serde(default)]
100pub struct StorageConfig {
101    /// Base path for data storage
102    pub path: PathBuf,
103    /// Maximum storage size (e.g., "10GB")
104    pub max_size: String,
105    /// Cache size in MB
106    pub cache_size: u64,
107    /// Enable compression
108    pub compression_enabled: bool,
109}
110
111/// DHT configuration
112#[derive(Debug, Clone, Serialize, Deserialize)]
113#[serde(default)]
114pub struct DhtConfig {
115    /// Replication factor (K value)
116    pub replication_factor: u8,
117    /// Alpha value for parallel queries
118    pub alpha: u8,
119    /// Beta value for routing
120    pub beta: u8,
121    /// Record TTL in seconds
122    pub record_ttl: u64,
123    /// Enable adaptive routing
124    pub adaptive_routing: bool,
125
126    // Trust-weighted peer selection configuration
127    /// Enable trust-weighted peer selection
128    /// When enabled, peer selection combines XOR distance with EigenTrust scores
129    pub trust_selection_enabled: bool,
130
131    /// Weight given to trust in peer selection (0.0-1.0)
132    /// Higher values = prefer trusted nodes over closer nodes
133    /// Default: 0.3 (30% weight to trust factor)
134    pub trust_weight: f64,
135
136    /// Minimum trust threshold for peer selection
137    /// Nodes below this trust score are deprioritized
138    /// Default: 0.1
139    pub min_trust_threshold: f64,
140
141    /// Exclude untrusted nodes from storage operations
142    /// When true, nodes below min_trust_threshold are excluded from storage targets
143    /// Default: false
144    pub exclude_untrusted_for_storage: bool,
145}
146
147/// Transport configuration
148#[derive(Debug, Clone, Serialize, Deserialize)]
149#[serde(default)]
150pub struct TransportConfig {
151    /// Preferred transport protocol
152    pub protocol: String,
153    /// Enable QUIC transport
154    pub quic_enabled: bool,
155    /// Enable TCP transport
156    pub tcp_enabled: bool,
157    /// Enable WebRTC transport
158    pub webrtc_enabled: bool,
159    /// Transport buffer size
160    pub buffer_size: usize,
161    /// Server name for TLS (SNI)
162    pub server_name: String,
163}
164
165/// Identity configuration
166#[derive(Debug, Clone, Serialize, Deserialize)]
167#[serde(default)]
168pub struct IdentityConfig {
169    /// Default key derivation path
170    pub derivation_path: String,
171    /// Key rotation interval in days
172    pub rotation_interval: u32,
173    /// Enable automatic backups
174    pub backup_enabled: bool,
175    /// Backup interval in hours
176    pub backup_interval: u32,
177}
178
179// Default implementations
180
181impl Default for NetworkConfig {
182    fn default() -> Self {
183        Self {
184            bootstrap_nodes: vec![],
185            // Bind all interfaces by default; env can override via Config::load()
186            listen_address: "0.0.0.0:9000".to_string(),
187            public_address: None,
188            ipv6_enabled: true,
189            max_connections: 10000,
190            connection_timeout: 30,
191            keepalive_interval: 60,
192        }
193    }
194}
195
196impl Default for SecurityConfig {
197    fn default() -> Self {
198        Self {
199            rate_limit: 1000,
200            connection_limit: 100,
201            encryption_enabled: true,
202            min_tls_version: "1.3".to_string(),
203            identity_security_level: "High".to_string(),
204        }
205    }
206}
207
208impl Default for StorageConfig {
209    fn default() -> Self {
210        Self {
211            path: PathBuf::from("./data"),
212            max_size: "10GB".to_string(),
213            cache_size: 256,
214            compression_enabled: true,
215        }
216    }
217}
218
219impl Default for DhtConfig {
220    fn default() -> Self {
221        Self {
222            replication_factor: 8,
223            alpha: 3,
224            beta: 1,
225            record_ttl: 3600,
226            adaptive_routing: true,
227            // Trust selection defaults
228            trust_selection_enabled: true,
229            trust_weight: 0.3,
230            min_trust_threshold: 0.1,
231            exclude_untrusted_for_storage: false,
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        Ok(())
401    }
402
403    /// Validate the configuration
404    pub fn validate(&self) -> Result<()> {
405        let mut errors = Vec::new();
406
407        // Validate network addresses
408        if let Err(e) = self.validate_address(&self.network.listen_address, "listen_address") {
409            errors.push(e);
410        }
411
412        if let Some(addr) = &self.network.public_address
413            && let Err(e) = self.validate_address(addr, "public_address")
414        {
415            errors.push(e);
416        }
417
418        for (i, node) in self.network.bootstrap_nodes.iter().enumerate() {
419            if let Err(e) = self.validate_address(node, &format!("bootstrap_node[{}]", i)) {
420                errors.push(e);
421            }
422        }
423
424        // Validate ranges using validation framework
425        if let Err(e) = validate_config_value(
426            &self.network.max_connections.to_string(),
427            Some(1_usize),
428            Some(100_000_usize),
429        ) {
430            errors.push(P2PError::Config(ConfigError::InvalidValue {
431                field: "max_connections".to_string().into(),
432                reason: e.to_string().into(),
433            }));
434        }
435
436        if let Err(e) = validate_config_value(
437            &self.security.rate_limit.to_string(),
438            Some(1_u32),
439            Some(1_000_000_u32),
440        ) {
441            errors.push(P2PError::Config(ConfigError::InvalidValue {
442                field: "rate_limit".to_string().into(),
443                reason: e.to_string().into(),
444            }));
445        }
446
447        // Validate storage path only if it exists; skip strict checks in non-existent dirs
448        if self.storage.path.exists()
449            && let Err(e) = validate_file_path(&self.storage.path)
450        {
451            errors.push(P2PError::Config(ConfigError::InvalidValue {
452                field: "storage.path".to_string().into(),
453                reason: format!("{:?}: {}", self.storage.path, e).into(),
454            }));
455        }
456
457        // Validate storage size format
458        if !self.validate_size_format(&self.storage.max_size) {
459            errors.push(P2PError::Config(ConfigError::InvalidValue {
460                field: "max_size".to_string().into(),
461                reason: format!("Invalid storage size format: {}", self.storage.max_size).into(),
462            }));
463        }
464
465        // Validate transport protocol
466        match self.transport.protocol.as_str() {
467            "quic" | "tcp" | "webrtc" => {}
468            _ => errors.push(P2PError::Config(ConfigError::InvalidValue {
469                field: "protocol".to_string().into(),
470                reason: format!("Invalid transport protocol: {}", self.transport.protocol).into(),
471            })),
472        }
473
474        if errors.is_empty() {
475            Ok(())
476        } else {
477            // Return the first error, or a generic error if somehow the vec is empty
478            Err(errors.into_iter().next().unwrap_or_else(|| {
479                P2PError::Config(ConfigError::InvalidValue {
480                    field: "unknown".to_string().into(),
481                    reason: "Validation failed with unknown error".to_string().into(),
482                })
483            }))
484        }
485    }
486
487    /// Validate network address format
488    fn validate_address(&self, addr: &str, field: &str) -> Result<()> {
489        // Try parsing as SocketAddr first
490        if let Ok(socket_addr) = SocketAddr::from_str(addr) {
491            // Use our validation framework
492            let ctx = ValidationContext::default()
493                .allow_localhost() // Allow localhost for development
494                .allow_private_ips(); // Allow private IPs for development
495
496            return validate_network_address(&socket_addr, &ctx).map_err(|e| {
497                P2PError::Config(ConfigError::InvalidValue {
498                    field: field.to_string().into(),
499                    reason: e.to_string().into(),
500                })
501            });
502        }
503
504        // Try parsing as four-word address format (always enabled)
505        if let Ok(network_addr) = crate::NetworkAddress::from_four_words(addr) {
506            // Validate the parsed socket address
507            let ctx = ValidationContext::default()
508                .allow_localhost()
509                .allow_private_ips();
510
511            return validate_network_address(&network_addr.socket_addr(), &ctx).map_err(|e| {
512                P2PError::Config(ConfigError::InvalidValue {
513                    field: field.to_string().into(),
514                    reason: e.to_string().into(),
515                })
516            });
517        }
518
519        // Try parsing as multiaddr format
520        if addr.starts_with("/ip4/") || addr.starts_with("/ip6/") {
521            // Basic multiaddr validation
522            return Ok(());
523        }
524
525        Err(P2PError::Config(ConfigError::InvalidValue {
526            field: field.to_string().into(),
527            reason: format!("Invalid address format: {}", addr).into(),
528        }))
529    }
530
531    /// Validate size format (e.g., "10GB", "500MB")
532    fn validate_size_format(&self, size: &str) -> bool {
533        thread_local! {
534            // Raw string with single backslashes for regex tokens
535            static SIZE_REGEX: std::result::Result<Regex, P2PError> = Regex::new(r"^\d+(?:\.\d+)?\s*(?:B|KB|MB|GB|TB)$")
536                .map_err(|e| P2PError::Config(ConfigError::InvalidValue { field: "size".to_string().into(), reason: e.to_string().into() }));
537        }
538        SIZE_REGEX.with(|re| re.as_ref().ok().map(|r| r.is_match(size)).unwrap_or(false))
539    }
540
541    /// Create development configuration
542    pub fn development() -> Self {
543        let mut config = Self::default();
544        config.network.listen_address = "127.0.0.1:9000".to_string();
545        config.security.rate_limit = 10000;
546        config.security.connection_limit = 1000;
547        config.storage.path = PathBuf::from("./dev-data");
548        config
549    }
550
551    /// Create production configuration with secure defaults
552    pub fn production() -> Self {
553        let mut config = Self::default();
554        // Use environment variable or fallback to secure default
555        config.network.listen_address =
556            env::var("SAORSA_LISTEN_ADDRESS").unwrap_or_else(|_| "0.0.0.0:9000".to_string());
557        config.security.rate_limit = 1000;
558        config.security.connection_limit = 100;
559        config.storage.path = PathBuf::from("/var/lib/saorsa");
560        // Larger buffers in production
561        config.transport.buffer_size = 131072;
562        config
563    }
564
565    /// Get parsed listen address
566    pub fn listen_socket_addr(&self) -> Result<SocketAddr> {
567        SocketAddr::from_str(&self.network.listen_address).map_err(|e| {
568            P2PError::Config(ConfigError::InvalidValue {
569                field: "listen_address".to_string().into(),
570                reason: format!("Invalid address: {}", e).into(),
571            })
572        })
573    }
574
575    /// Get parsed bootstrap addresses
576    pub fn bootstrap_addrs(&self) -> Result<Vec<NetworkAddress>> {
577        self.network
578            .bootstrap_nodes
579            .iter()
580            .map(|addr| {
581                addr.parse::<NetworkAddress>().map_err(|e| {
582                    P2PError::Config(ConfigError::InvalidValue {
583                        field: "bootstrap_nodes".to_string().into(),
584                        reason: format!("Invalid address: {}", e).into(),
585                    })
586                })
587            })
588            .collect()
589    }
590
591    /// Parse a size string (e.g., "10GB", "500MB") to bytes
592    ///
593    /// # Examples
594    ///
595    /// ```ignore
596    /// use saorsa_core::config::Config;
597    ///
598    /// assert_eq!(Config::parse_size("10B").unwrap(), 10);
599    /// assert_eq!(Config::parse_size("1KB").unwrap(), 1024);
600    /// assert_eq!(Config::parse_size("5MB").unwrap(), 5 * 1024 * 1024);
601    /// ```
602    pub fn parse_size(size: &str) -> Result<u64> {
603        thread_local! {
604            static SIZE_REGEX: std::result::Result<Regex, P2PError> = Regex::new(r"^(\d+(?:\.\d+)?)\s*(B|KB|MB|GB|TB)$")
605                .map_err(|e| P2PError::Config(ConfigError::InvalidValue { field: "size".to_string().into(), reason: e.to_string().into() }));
606        }
607
608        SIZE_REGEX.with(|re| -> Result<u64> {
609            let re = match re {
610                Ok(r) => r,
611                Err(e) => {
612                    return Err(P2PError::Config(ConfigError::InvalidValue {
613                        field: "size".to_string().into(),
614                        reason: e.to_string().into(),
615                    }));
616                }
617            };
618            if let Some(captures) = re.captures(size) {
619                let value: f64 = captures
620                    .get(1)
621                    .and_then(|m| m.as_str().parse().ok())
622                    .ok_or_else(|| {
623                        P2PError::Config(ConfigError::InvalidValue {
624                            field: "size".to_string().into(),
625                            reason: "Invalid numeric value".to_string().into(),
626                        })
627                    })?;
628
629                let unit = captures.get(2).map(|m| m.as_str()).unwrap_or("B");
630                let multiplier = match unit {
631                    "B" => 1u64,
632                    "KB" => 1024,
633                    "MB" => 1024 * 1024,
634                    "GB" => 1024 * 1024 * 1024,
635                    "TB" => 1024u64.pow(4),
636                    _ => {
637                        return Err(P2PError::Config(ConfigError::InvalidValue {
638                            field: "size".to_string().into(),
639                            reason: format!("Unknown unit: {}", unit).into(),
640                        }));
641                    }
642                };
643
644                Ok((value * multiplier as f64) as u64)
645            } else {
646                Err(P2PError::Config(ConfigError::InvalidValue {
647                    field: "size".to_string().into(),
648                    reason: format!("Invalid size format: {}", size).into(),
649                }))
650            }
651        })
652    }
653
654    /// Get storage max size in bytes
655    pub fn storage_max_size_bytes(&self) -> Result<u64> {
656        Self::parse_size(&self.storage.max_size)
657    }
658}
659
660#[cfg(test)]
661mod tests {
662    use super::*;
663    use tempfile::NamedTempFile;
664
665    #[test]
666    fn test_default_config() {
667        let config = Config::default();
668        assert_eq!(config.network.listen_address, "0.0.0.0:9000");
669        assert_eq!(config.security.rate_limit, 1000);
670        assert!(config.security.encryption_enabled);
671    }
672
673    #[test]
674    fn test_development_config() {
675        let config = Config::development();
676        assert_eq!(config.network.listen_address, "127.0.0.1:9000");
677        assert_eq!(config.security.rate_limit, 10000);
678    }
679
680    #[test]
681    fn test_production_config() {
682        let config = Config::production();
683        // Production config should have larger buffer size
684        assert_eq!(config.transport.buffer_size, 131072);
685        // Listen address should contain a port
686        assert!(config.network.listen_address.contains(':'));
687    }
688
689    #[test]
690    fn test_config_validation() {
691        let mut config = Config::default();
692        assert!(config.validate().is_ok());
693
694        // Invalid address
695        config.network.listen_address = "invalid".to_string();
696        assert!(config.validate().is_err());
697
698        // Valid multiaddr
699        config.network.listen_address = "/ip4/127.0.0.1/tcp/9000".to_string();
700        assert!(config.validate().is_ok());
701    }
702
703    #[test]
704    fn test_save_and_load_config() {
705        let config = Config::development();
706        let file = NamedTempFile::new().unwrap();
707
708        config.save_to_file(file.path()).unwrap();
709
710        let loaded = Config::load_from_file(file.path()).unwrap();
711        assert_eq!(loaded.network.listen_address, config.network.listen_address);
712    }
713
714    #[test]
715    #[serial_test::serial]
716    #[allow(unsafe_code)] // Required for env::set_var in tests only
717    fn test_env_overrides() {
718        use std::sync::Mutex;
719
720        // Use a static mutex to ensure thread safety
721        static ENV_MUTEX: Mutex<()> = Mutex::new(());
722        let _guard = ENV_MUTEX.lock().unwrap();
723
724        // Save original values
725        let orig_listen = env::var("SAORSA_LISTEN_ADDRESS").ok();
726        let orig_rate = env::var("SAORSA_RATE_LIMIT").ok();
727
728        // Set test values - unsafe blocks required in Rust 2024
729        unsafe {
730            env::set_var("SAORSA_LISTEN_ADDRESS", "127.0.0.1:8000");
731            env::set_var("SAORSA_RATE_LIMIT", "5000");
732        }
733
734        let config = Config::load().unwrap();
735        assert_eq!(config.network.listen_address, "127.0.0.1:8000");
736        assert_eq!(config.security.rate_limit, 5000);
737
738        // Restore original values
739        unsafe {
740            match orig_listen {
741                Some(val) => env::set_var("SAORSA_LISTEN_ADDRESS", val),
742                None => env::remove_var("SAORSA_LISTEN_ADDRESS"),
743            }
744            match orig_rate {
745                Some(val) => env::set_var("SAORSA_RATE_LIMIT", val),
746                None => env::remove_var("SAORSA_RATE_LIMIT"),
747            }
748        }
749    }
750
751    #[test]
752    fn test_size_validation() {
753        let config = Config::default();
754        assert!(config.validate_size_format("10GB"));
755        assert!(config.validate_size_format("500MB"));
756        assert!(config.validate_size_format("1.5TB"));
757        assert!(!config.validate_size_format("10XB"));
758        assert!(!config.validate_size_format("invalid"));
759    }
760
761    #[test]
762    fn test_size_parsing() {
763        assert_eq!(Config::parse_size("10B").unwrap(), 10);
764        assert_eq!(Config::parse_size("1KB").unwrap(), 1024);
765        assert_eq!(Config::parse_size("5MB").unwrap(), 5 * 1024 * 1024);
766        assert_eq!(Config::parse_size("1GB").unwrap(), 1024 * 1024 * 1024);
767        assert_eq!(Config::parse_size("1.5GB").unwrap(), 1610612736);
768        assert_eq!(Config::parse_size("1TB").unwrap(), 1024u64.pow(4));
769
770        // Test error cases
771        assert!(Config::parse_size("invalid").is_err());
772        assert!(Config::parse_size("10XB").is_err());
773        assert!(Config::parse_size("GB").is_err());
774    }
775}