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