Skip to main content

aria2_core/config/
option.rs

1//! Configuration option types, validation, and registry.
2//!
3//! This module provides:
4//! - [`OptionType`], [`OptionCategory`], [`OptionValue`], [`OptionDef`] — core type definitions
5//! - [`OptionRegistry`] — the central registry for all aria2 configuration options
6//! - [`OptionValidator`], [`OptionError`], validators — dynamic validation framework
7//! - [`DependencyChecker`] — inter-option dependency checking
8//!
9//! Built-in option registrations are in [`option_definitions`](super::option_definitions).
10
11use std::collections::HashMap;
12use std::fmt;
13use std::path::Path;
14
15use serde_json::Value;
16
17// =========================================================================
18// Core Type Definitions (consolidated from option_types.rs)
19// =========================================================================
20
21/// Represents the data type of a configuration option.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum OptionType {
24    String,
25    Integer,
26    Float,
27    Boolean,
28    List,
29    Enum,
30    Path,
31    Size,
32}
33
34impl fmt::Display for OptionType {
35    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36        match self {
37            Self::String => write!(f, "string"),
38            Self::Integer => write!(f, "integer"),
39            Self::Float => write!(f, "float"),
40            Self::Boolean => write!(f, "boolean"),
41            Self::List => write!(f, "list"),
42            Self::Enum => write!(f, "enum"),
43            Self::Path => write!(f, "path"),
44            Self::Size => write!(f, "size"),
45        }
46    }
47}
48
49/// Logical category/grouping for configuration options.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum OptionCategory {
52    General,
53    HttpFtp,
54    BitTorrent,
55    Rpc,
56    Advanced,
57}
58
59impl fmt::Display for OptionCategory {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        match self {
62            Self::General => write!(f, "general"),
63            Self::HttpFtp => write!(f, "http/ftp"),
64            Self::BitTorrent => write!(f, "bittorrent"),
65            Self::Rpc => write!(f, "rpc"),
66            Self::Advanced => write!(f, "advanced"),
67        }
68    }
69}
70
71/// Runtime value of a configuration option.
72#[derive(Debug, Clone, PartialEq, Default)]
73pub enum OptionValue {
74    Bool(bool),
75    Int(i64),
76    Usize(usize),
77    Float(f64),
78    Str(String),
79    List(Vec<String>),
80    #[default]
81    None,
82}
83
84impl fmt::Display for OptionValue {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        match self {
87            Self::Bool(b) => write!(f, "{}", b),
88            Self::Int(n) => write!(f, "{}", n),
89            Self::Usize(n) => write!(f, "{}", n),
90            Self::Float(v) => write!(f, "{}", v),
91            Self::Str(s) => write!(f, "{}", s),
92            Self::List(items) => write!(f, "{}", items.join(",")),
93            Self::None => write!(f, ""),
94        }
95    }
96}
97
98impl From<&OptionValue> for serde_json::Value {
99    fn from(v: &OptionValue) -> Self {
100        match v {
101            OptionValue::Bool(b) => serde_json::json!(*b),
102            OptionValue::Int(n) => serde_json::json!(*n),
103            OptionValue::Usize(n) => serde_json::json!(*n),
104            OptionValue::Float(v) => serde_json::json!(*v),
105            OptionValue::Str(s) => serde_json::json!(s),
106            OptionValue::List(items) => serde_json::json!(items),
107            OptionValue::None => serde_json::Value::Null,
108        }
109    }
110}
111
112impl From<serde_json::Value> for OptionValue {
113    fn from(val: serde_json::Value) -> Self {
114        match val {
115            serde_json::Value::Bool(b) => Self::Bool(b),
116            serde_json::Value::Number(n) if n.is_i64() => Self::Int(n.as_i64().unwrap()),
117            serde_json::Value::Number(n) if n.is_f64() => Self::Float(n.as_f64().unwrap()),
118            serde_json::Value::Number(n) if n.is_u64() => Self::Usize(n.as_u64().unwrap() as usize),
119            serde_json::Value::String(s) => Self::Str(s),
120            serde_json::Value::Array(arr) => Self::List(
121                arr.iter()
122                    .filter_map(|v| v.as_str().map(String::from))
123                    .collect(),
124            ),
125            _ => Self::None,
126        }
127    }
128}
129
130impl OptionValue {
131    pub fn as_str(&self) -> Option<&str> {
132        if let Self::Str(s) = self {
133            Some(s)
134        } else {
135            None
136        }
137    }
138    pub fn as_i64(&self) -> Option<i64> {
139        if let Self::Int(n) = self {
140            Some(*n)
141        } else {
142            None
143        }
144    }
145    pub fn as_f64(&self) -> Option<f64> {
146        if let Self::Float(v) = self {
147            Some(*v)
148        } else {
149            None
150        }
151    }
152    pub fn as_bool(&self) -> Option<bool> {
153        if let Self::Bool(b) = self {
154            Some(*b)
155        } else {
156            None
157        }
158    }
159    pub fn as_list(&self) -> Option<&Vec<String>> {
160        if let Self::List(l) = self {
161            Some(l)
162        } else {
163            None
164        }
165    }
166    pub fn as_usize(&self) -> usize {
167        match self {
168            Self::Usize(n) => *n,
169            _ => 0,
170        }
171    }
172    pub fn as_str_vec(&self) -> &[String] {
173        match self {
174            Self::List(v) => v.as_slice(),
175            _ => &[],
176        }
177    }
178    pub fn is_none(&self) -> bool {
179        matches!(self, Self::None)
180    }
181
182    pub fn parse_size_str(s: &str) -> u64 {
183        let s = s.trim();
184        let (num_part, suffix) = if s.len() > 1 {
185            let last_char = s.chars().last().unwrap();
186            match last_char {
187                'K' | 'k' => (&s[..s.len() - 1], 1024u64),
188                'M' | 'm' => (&s[..s.len() - 1], 1024 * 1024),
189                'G' | 'g' => (&s[..s.len() - 1], 1024u64 * 1024 * 1024),
190                'T' | 't' => (&s[..s.len() - 1], 1024u64 * 1024 * 1024 * 1024),
191                _ => (s, 1u64),
192            }
193        } else {
194            (s, 1u64)
195        };
196        num_part
197            .parse::<f64>()
198            .map(|n| (n * suffix as f64) as u64)
199            .unwrap_or(0)
200    }
201
202    pub fn to_size_string(bytes: u64) -> String {
203        const K: u64 = 1024;
204        const M: u64 = K * K;
205        const G: u64 = M * K;
206        const T: u64 = G * K;
207        if bytes >= T {
208            format!("{}T", bytes as f64 / T as f64)
209        } else if bytes >= G {
210            format!("{}G", bytes as f64 / G as f64)
211        } else if bytes >= M {
212            format!("{}M", bytes as f64 / M as f64)
213        } else if bytes >= K {
214            format!("{}K", bytes as f64 / K as f64)
215        } else {
216            format!("{}", bytes)
217        }
218    }
219}
220
221/// Definition/metadata for a single configuration option.
222#[derive(Debug, Clone)]
223pub struct OptionDef {
224    pub name: String,
225    pub short_name: Option<char>,
226    pub opt_type: OptionType,
227    pub default_value: OptionValue,
228    pub description: String,
229    pub category: OptionCategory,
230    pub min: Option<i64>,
231    pub max: Option<u64>,
232    pub deprecated: bool,
233    pub hidden: bool,
234}
235
236impl Default for OptionDef {
237    fn default() -> Self {
238        Self {
239            name: String::new(),
240            short_name: None,
241            opt_type: OptionType::String,
242            default_value: OptionValue::None,
243            description: String::new(),
244            category: OptionCategory::General,
245            min: None,
246            max: None,
247            deprecated: false,
248            hidden: false,
249        }
250    }
251}
252
253impl OptionDef {
254    pub fn new(name: impl Into<String>, opt_type: OptionType) -> Self {
255        Self {
256            name: name.into(),
257            opt_type,
258            ..Default::default()
259        }
260    }
261
262    pub fn name(&self) -> &str {
263        &self.name
264    }
265    pub fn short_name(&self) -> Option<char> {
266        self.short_name
267    }
268    pub fn opt_type(&self) -> OptionType {
269        self.opt_type
270    }
271    pub fn default_value(&self) -> &OptionValue {
272        &self.default_value
273    }
274    pub fn get_category(&self) -> OptionCategory {
275        self.category
276    }
277    pub fn is_deprecated(&self) -> bool {
278        self.deprecated
279    }
280    pub fn is_hidden(&self) -> bool {
281        self.hidden
282    }
283
284    pub fn description(&self) -> &str {
285        &self.description
286    }
287
288    pub fn parse_value(&self, s: &str) -> Result<OptionValue, String> {
289        if s.is_empty() {
290            return Ok(self.default_value.clone());
291        }
292        match self.opt_type {
293            OptionType::String | OptionType::Path | OptionType::Enum => {
294                Ok(OptionValue::Str(s.to_string()))
295            }
296            OptionType::Integer => s
297                .parse::<i64>()
298                .map(|n| {
299                    if let Some(min) = self.min
300                        && n < min
301                    {
302                        return Err(format!("value {} < minimum {}", n, min));
303                    }
304                    if let Some(max) = self.max
305                        && (n < 0 || n as u64 > max)
306                    {
307                        return Err(format!("value {} exceeds maximum {}", n, max));
308                    }
309                    Ok(OptionValue::Int(n))
310                })
311                .map_err(|e| format!("invalid integer '{}': {}", s, e))?,
312            OptionType::Size => Ok(OptionValue::Int(OptionValue::parse_size_str(s) as i64)),
313            OptionType::Float => s
314                .parse::<f64>()
315                .map(OptionValue::Float)
316                .map_err(|e| format!("invalid float '{}': {}", s, e)),
317            OptionType::Boolean => match s.to_lowercase().as_str() {
318                "true" | "yes" | "1" | "on" => Ok(OptionValue::Bool(true)),
319                "false" | "no" | "0" | "off" => Ok(OptionValue::Bool(false)),
320                _ => Err(format!("invalid boolean '{}'", s)),
321            },
322            OptionType::List => Ok(OptionValue::List(
323                s.split(',').map(|x| x.trim().to_string()).collect(),
324            )),
325        }
326    }
327}
328
329// =========================================================================
330// Validation Framework (consolidated from option_validator.rs)
331// =========================================================================
332
333/// Error type for option validation failures.
334#[derive(Debug, Clone)]
335pub enum OptionError {
336    TypeMismatch {
337        expected: String,
338        got: String,
339    },
340    OutOfRange {
341        value: String,
342        min: String,
343        max: String,
344    },
345    InvalidChoice {
346        value: String,
347        allowed: Vec<String>,
348    },
349    InvalidUrl {
350        url: String,
351        reason: String,
352    },
353    InvalidPath {
354        path: String,
355        reason: String,
356    },
357    PatternMismatch {
358        value: String,
359        pattern: String,
360    },
361    DependencyConflict {
362        option: String,
363        conflicts_with: String,
364    },
365    MissingDependency {
366        option: String,
367        requires: String,
368    },
369}
370
371impl fmt::Display for OptionError {
372    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
373        match self {
374            Self::TypeMismatch { expected, got } => {
375                write!(
376                    f,
377                    "type mismatch for option: expected '{}', got '{}'",
378                    expected, got
379                )
380            }
381            Self::OutOfRange { value, min, max } => {
382                write!(f, "value '{}' is out of range [{}..{}]", value, min, max)
383            }
384            Self::InvalidChoice { value, allowed } => {
385                write!(
386                    f,
387                    "invalid choice '{}', allowed values: {}",
388                    value,
389                    allowed.join(", ")
390                )
391            }
392            Self::InvalidUrl { url, reason } => {
393                write!(f, "invalid URL '{}': {}", url, reason)
394            }
395            Self::InvalidPath { path, reason } => {
396                write!(f, "invalid path '{}': {}", path, reason)
397            }
398            Self::PatternMismatch { value, pattern } => {
399                write!(f, "value '{}' does not match pattern '{}'", value, pattern)
400            }
401            Self::DependencyConflict {
402                option,
403                conflicts_with,
404            } => {
405                write!(f, "option '{}' conflicts with '{}'", option, conflicts_with)
406            }
407            Self::MissingDependency { option, requires } => {
408                write!(f, "option '{}' requires '{}' to be set", option, requires)
409            }
410        }
411    }
412}
413
414impl std::error::Error for OptionError {}
415
416/// Trait for validating configuration option values.
417pub trait OptionValidator: Send + Sync {
418    fn validate(&self, name: &str, value: &Value) -> Result<(), OptionError>;
419    fn description(&self) -> &str;
420}
421
422/// Validates that numeric values fall within a specified range.
423#[derive(Debug, Clone)]
424pub struct RangeValidator<T> {
425    min: T,
426    max: T,
427}
428
429impl<T> RangeValidator<T>
430where
431    T: PartialOrd + fmt::Display + Clone + 'static,
432{
433    pub fn new(min: T, max: T) -> Self {
434        Self { min, max }
435    }
436}
437
438impl OptionValidator for RangeValidator<i64> {
439    fn validate(&self, _name: &str, value: &Value) -> Result<(), OptionError> {
440        match value.as_i64() {
441            Some(n) if n >= self.min && n <= self.max => Ok(()),
442            Some(n) => Err(OptionError::OutOfRange {
443                value: n.to_string(),
444                min: self.min.to_string(),
445                max: self.max.to_string(),
446            }),
447            None => Err(OptionError::TypeMismatch {
448                expected: "integer".to_string(),
449                got: format!("{:?}", value),
450            }),
451        }
452    }
453
454    fn description(&self) -> &str {
455        "range validator (inclusive bounds)"
456    }
457}
458
459impl OptionValidator for RangeValidator<f64> {
460    fn validate(&self, _name: &str, value: &Value) -> Result<(), OptionError> {
461        match value.as_f64() {
462            Some(v) if v >= self.min && v <= self.max => Ok(()),
463            Some(v) => Err(OptionError::OutOfRange {
464                value: format!("{}", v),
465                min: format!("{}", self.min),
466                max: format!("{}", self.max),
467            }),
468            None => Err(OptionError::TypeMismatch {
469                expected: "float".to_string(),
470                got: format!("{:?}", value),
471            }),
472        }
473    }
474
475    fn description(&self) -> &str {
476        "range validator for floating-point numbers"
477    }
478}
479
480impl OptionValidator for RangeValidator<u64> {
481    fn validate(&self, _name: &str, value: &Value) -> Result<(), OptionError> {
482        match value.as_u64() {
483            Some(n) if n >= self.min && n <= self.max => Ok(()),
484            Some(n) => Err(OptionError::OutOfRange {
485                value: n.to_string(),
486                min: self.min.to_string(),
487                max: self.max.to_string(),
488            }),
489            None => Err(OptionError::TypeMismatch {
490                expected: "unsigned integer".to_string(),
491                got: format!("{:?}", value),
492            }),
493        }
494    }
495
496    fn description(&self) -> &str {
497        "range validator for unsigned integers"
498    }
499}
500
501/// Validates that string values are in a predefined whitelist of choices.
502#[derive(Debug, Clone)]
503pub struct ChoiceValidator {
504    allowed: Vec<String>,
505}
506
507impl ChoiceValidator {
508    pub fn new(allowed: Vec<String>) -> Self {
509        Self { allowed }
510    }
511
512    pub fn allowed_values(&self) -> &[String] {
513        &self.allowed
514    }
515}
516
517impl OptionValidator for ChoiceValidator {
518    fn validate(&self, _name: &str, value: &Value) -> Result<(), OptionError> {
519        match value.as_str() {
520            Some(s) if self.allowed.iter().any(|a| a == s) => Ok(()),
521            Some(s) => Err(OptionError::InvalidChoice {
522                value: s.to_string(),
523                allowed: self.allowed.clone(),
524            }),
525            None => Err(OptionError::TypeMismatch {
526                expected: "string".to_string(),
527                got: format!("{:?}", value),
528            }),
529        }
530    }
531
532    fn description(&self) -> &str {
533        "choice validator (enum whitelist)"
534    }
535}
536
537/// Validates URL strings for proper format and supported schemes.
538#[derive(Debug, Clone, Copy)]
539pub struct UrlValidator;
540
541impl UrlValidator {
542    pub fn new() -> Self {
543        Self
544    }
545
546    fn is_valid_url(url: &str) -> Result<(), String> {
547        if url.is_empty() {
548            return Err("URL is empty".to_string());
549        }
550
551        if !url.contains("://") {
552            return Err(format!("missing scheme separator in '{}'", url));
553        }
554
555        let scheme = url.split("://").next().unwrap_or("");
556        if scheme.is_empty() {
557            return Err("scheme is empty".to_string());
558        }
559
560        if !scheme
561            .chars()
562            .all(|c| c.is_alphanumeric() || c == '+' || c == '-' || c == '.')
563        {
564            return Err(format!("invalid scheme '{}'", scheme));
565        }
566
567        let after_scheme = url.split_once("://").map(|x| x.1).unwrap_or("");
568        if after_scheme.is_empty() {
569            return Err("no host/path after scheme".to_string());
570        }
571
572        Ok(())
573    }
574}
575
576impl Default for UrlValidator {
577    fn default() -> Self {
578        Self::new()
579    }
580}
581
582impl OptionValidator for UrlValidator {
583    fn validate(&self, _name: &str, value: &Value) -> Result<(), OptionError> {
584        match value.as_str() {
585            Some(url) => Self::is_valid_url(url).map_err(|reason| OptionError::InvalidUrl {
586                url: url.to_string(),
587                reason,
588            }),
589            None => Err(OptionError::TypeMismatch {
590                expected: "string (URL)".to_string(),
591                got: format!("{:?}", value),
592            }),
593        }
594    }
595
596    fn description(&self) -> &str {
597        "URL format validator"
598    }
599}
600
601/// Validates file system paths for existence and writability.
602#[derive(Debug, Clone)]
603pub struct PathValidator {
604    must_exist: bool,
605    writable: bool,
606}
607
608impl PathValidator {
609    pub fn new(must_exist: bool, writable: bool) -> Self {
610        Self {
611            must_exist,
612            writable,
613        }
614    }
615}
616
617impl OptionValidator for PathValidator {
618    fn validate(&self, _name: &str, value: &Value) -> Result<(), OptionError> {
619        match value.as_str() {
620            Some(path_str) => {
621                let path = Path::new(path_str);
622
623                if self.must_exist && !path.exists() {
624                    return Err(OptionError::InvalidPath {
625                        path: path_str.to_string(),
626                        reason: "path does not exist".to_string(),
627                    });
628                }
629
630                if self.writable {
631                    let check_path = if path.exists() {
632                        path.as_os_str().to_owned()
633                    } else {
634                        match path.parent() {
635                            Some(parent) if !parent.as_os_str().is_empty() => {
636                                parent.as_os_str().to_owned()
637                            }
638                            _ => path.as_os_str().to_owned(),
639                        }
640                    };
641
642                    if let Some(check_path_str) = check_path.to_str() {
643                        let check_path = Path::new(check_path_str);
644                        if check_path.exists() {
645                            match std::fs::metadata(check_path) {
646                                Ok(meta) => {
647                                    #[cfg(unix)]
648                                    {
649                                        use std::os::unix::fs::PermissionsExt;
650                                        let mode = meta.permissions().mode();
651                                        let user_writable = mode & 0o200 != 0;
652                                        if !user_writable && meta.is_dir() {
653                                            return Err(OptionError::InvalidPath {
654                                                path: path_str.to_string(),
655                                                reason: "path is not writable".to_string(),
656                                            });
657                                        }
658                                    }
659                                    #[cfg(not(unix))]
660                                    {
661                                        let _ = &meta;
662                                    }
663                                }
664                                Err(e) => {
665                                    return Err(OptionError::InvalidPath {
666                                        path: path_str.to_string(),
667                                        reason: format!("cannot access path: {}", e),
668                                    });
669                                }
670                            }
671                        }
672                    }
673                }
674
675                Ok(())
676            }
677            None => Err(OptionError::TypeMismatch {
678                expected: "string (path)".to_string(),
679                got: format!("{:?}", value),
680            }),
681        }
682    }
683
684    fn description(&self) -> &str {
685        "file system path validator"
686    }
687}
688
689/// Validates string values against a custom regular expression pattern.
690#[derive(Debug, Clone)]
691pub struct RegexValidator {
692    pattern: String,
693    compiled: regex::Regex,
694}
695
696impl RegexValidator {
697    pub fn new(pattern: &str) -> Self {
698        let compiled = regex::Regex::new(pattern).expect("Invalid regex pattern in RegexValidator");
699        Self {
700            pattern: pattern.to_string(),
701            compiled,
702        }
703    }
704
705    pub fn pattern(&self) -> &str {
706        &self.pattern
707    }
708}
709
710impl OptionValidator for RegexValidator {
711    fn validate(&self, _name: &str, value: &Value) -> Result<(), OptionError> {
712        match value.as_str() {
713            Some(s) if self.compiled.is_match(s) => Ok(()),
714            Some(s) => Err(OptionError::PatternMismatch {
715                value: s.to_string(),
716                pattern: self.pattern.clone(),
717            }),
718            None => Err(OptionError::TypeMismatch {
719                expected: "string".to_string(),
720                got: format!("{:?}", value),
721            }),
722        }
723    }
724
725    fn description(&self) -> &str {
726        "custom regex pattern validator"
727    }
728}
729
730/// Extended definition for a configuration option with dynamic validation support.
731pub struct OptionDefinition {
732    pub name: &'static str,
733    pub description: &'static str,
734    pub default_value: Value,
735    pub validator: Option<Box<dyn OptionValidator>>,
736}
737
738impl OptionDefinition {
739    pub fn new(name: &'static str, description: &'static str, default_value: Value) -> Self {
740        Self {
741            name,
742            description,
743            default_value,
744            validator: None,
745        }
746    }
747
748    pub fn with_validator(mut self, validator: Box<dyn OptionValidator>) -> Self {
749        self.validator = Some(validator);
750        self
751    }
752
753    pub fn validate(&self, value: &Value) -> Result<(), OptionError> {
754        match &self.validator {
755            Some(validator) => validator.validate(self.name, value),
756            None => Ok(()),
757        }
758    }
759
760    pub fn get_default_or_fallback<'a>(&'a self, fallback: &'a Value) -> &'a Value {
761        match &self.default_value {
762            Value::Null => fallback,
763            other => other,
764        }
765    }
766}
767
768/// Checks dependencies between configuration options.
769pub struct DependencyChecker {
770    mutual_exclusions: Vec<(String, String)>,
771    requirements: Vec<(String, String)>,
772}
773
774impl DependencyChecker {
775    pub fn new() -> Self {
776        Self {
777            mutual_exclusions: Vec::new(),
778            requirements: Vec::new(),
779        }
780    }
781
782    pub fn add_mutual_exclusion(&mut self, opt_a: String, opt_b: String) {
783        self.mutual_exclusions.push((opt_a, opt_b));
784    }
785
786    pub fn add_requirement(&mut self, option: String, requires: String) {
787        self.requirements.push((option, requires));
788    }
789
790    pub fn check(&self, options: &HashMap<String, Value>) -> Vec<OptionError> {
791        let mut errors = Vec::new();
792
793        for (opt_a, opt_b) in &self.mutual_exclusions {
794            let a_set = options.get(opt_a).is_some_and(|v| !v.is_null());
795            let b_set = options.get(opt_b).is_some_and(|v| !v.is_null());
796
797            if a_set && b_set {
798                errors.push(OptionError::DependencyConflict {
799                    option: opt_a.clone(),
800                    conflicts_with: opt_b.clone(),
801                });
802            }
803        }
804
805        for (option, requires) in &self.requirements {
806            let option_set = options.get(option).is_some_and(|v| !v.is_null());
807            let required_set = options.get(requires).is_some_and(|v| !v.is_null());
808
809            if option_set && !required_set {
810                errors.push(OptionError::MissingDependency {
811                    option: option.clone(),
812                    requires: requires.clone(),
813                });
814            }
815        }
816
817        errors
818    }
819
820    pub fn mutual_exclusion_count(&self) -> usize {
821        self.mutual_exclusions.len()
822    }
823
824    pub fn requirement_count(&self) -> usize {
825        self.requirements.len()
826    }
827}
828
829impl Default for DependencyChecker {
830    fn default() -> Self {
831        Self::new()
832    }
833}
834
835// =========================================================================
836// Option Registry
837// =========================================================================
838
839/// Registry of all known configuration options.
840#[derive(Clone)]
841pub struct OptionRegistry {
842    options: HashMap<String, OptionDef>,
843}
844
845impl OptionRegistry {
846    pub fn new() -> Self {
847        let mut reg = Self {
848            options: HashMap::new(),
849        };
850        reg.register_general_options();
851        reg.register_http_ftp_options();
852        reg.register_bt_options();
853        reg.register_rpc_options();
854        reg.register_advanced_options();
855        reg
856    }
857
858    pub fn register(&mut self, def: OptionDef) {
859        self.options.insert(def.name().to_string(), def);
860    }
861
862    pub fn get(&self, name: &str) -> Option<&OptionDef> {
863        self.options.get(name)
864    }
865
866    pub fn contains(&self, name: &str) -> bool {
867        self.options.contains_key(name)
868    }
869
870    pub fn all(&self) -> &HashMap<String, OptionDef> {
871        &self.options
872    }
873
874    pub fn count(&self) -> usize {
875        self.options.len()
876    }
877
878    pub fn by_category(&self, cat: OptionCategory) -> Vec<&OptionDef> {
879        self.options
880            .values()
881            .filter(|d| d.get_category() == cat)
882            .collect()
883    }
884}
885
886impl Default for OptionRegistry {
887    fn default() -> Self {
888        Self::new()
889    }
890}
891
892#[cfg(test)]
893mod tests {
894    use super::*;
895
896    #[test]
897    fn test_option_type_display() {
898        assert_eq!(OptionType::String.to_string(), "string");
899        assert_eq!(OptionType::Boolean.to_string(), "boolean");
900        assert_eq!(OptionType::Size.to_string(), "size");
901    }
902
903    #[test]
904    fn test_option_category_display() {
905        assert_eq!(OptionCategory::General.to_string(), "general");
906        assert_eq!(OptionCategory::BitTorrent.to_string(), "bittorrent");
907    }
908
909    #[test]
910    fn test_option_value_variants() {
911        let s = OptionValue::Str("hello".into());
912        assert_eq!(s.as_str().unwrap(), "hello");
913
914        let n = OptionValue::Int(42);
915        assert_eq!(n.as_i64().unwrap(), 42);
916
917        let b = OptionValue::Bool(true);
918        assert!(b.as_bool().unwrap());
919
920        let l = OptionValue::List(vec!["a".into(), "b".into()]);
921        assert_eq!(l.as_list().unwrap().len(), 2);
922
923        let none = OptionValue::None;
924        assert!(none.is_none());
925    }
926
927    #[test]
928    fn test_option_value_display() {
929        assert_eq!(OptionValue::Str("test".into()).to_string(), "test");
930        assert_eq!(OptionValue::Int(99).to_string(), "99");
931        assert_eq!(OptionValue::Bool(true).to_string(), "true");
932        assert_eq!(
933            OptionValue::List(vec!["x".into(), "y".into()]).to_string(),
934            "x,y"
935        );
936    }
937
938    #[test]
939    fn test_option_value_to_json() {
940        let v = OptionValue::Str("hello".into());
941        let jv: serde_json::Value = (&v).into();
942        assert_eq!(jv, "hello");
943
944        let v2 = OptionValue::Int(123);
945        let jv2: serde_json::Value = (&v2).into();
946        assert_eq!(jv2, 123);
947
948        let v3 = OptionValue::Bool(false);
949        let jv3: serde_json::Value = (&v3).into();
950        assert_eq!(jv3, false);
951
952        let v4 = OptionValue::List(vec!["a".into()]);
953        let jv4: serde_json::Value = (&v4).into();
954        assert!(jv4.is_array());
955    }
956
957    #[test]
958    fn test_option_value_from_json() {
959        let ov: OptionValue = serde_json::json!("test string").into();
960        assert_eq!(ov.as_str().unwrap(), "test string");
961
962        let ov2: OptionValue = serde_json::json!(42).into();
963        assert_eq!(ov2.as_i64().unwrap(), 42);
964
965        let ov3: OptionValue = serde_json::json!(true).into();
966        assert!(ov3.as_bool().unwrap());
967
968        let ov4: OptionValue = serde_json::json!(["a", "b"]).into();
969        assert_eq!(ov4.as_list().unwrap().len(), 2);
970    }
971
972    #[test]
973    fn test_size_parsing() {
974        assert_eq!(OptionValue::parse_size_str("100"), 100);
975        assert_eq!(OptionValue::parse_size_str("1K"), 1024);
976        assert_eq!(OptionValue::parse_size_str("2M"), 2 * 1024 * 1024);
977        assert_eq!(OptionValue::parse_size_str("1G"), 1024u64 * 1024 * 1024);
978        assert_eq!(OptionValue::parse_size_str("0"), 0);
979    }
980
981    #[test]
982    fn test_size_display() {
983        assert!(OptionValue::to_size_string(500).contains("500"));
984        assert!(OptionValue::to_size_string(2048).contains("K"));
985        assert!(OptionValue::to_size_string(3 * 1024 * 1024).contains("M"));
986    }
987
988    #[test]
989    fn test_option_def_builder() {
990        let def = OptionDef {
991            name: "split".into(),
992            opt_type: OptionType::Integer,
993            short_name: Some('s'),
994            default_value: OptionValue::Int(5),
995            description: "Connections per download".into(),
996            min: Some(1),
997            max: Some(16),
998            category: OptionCategory::HttpFtp,
999            ..Default::default()
1000        };
1001        assert_eq!(def.name(), "split");
1002        assert_eq!(def.short_name(), Some('s'));
1003        assert_eq!(def.opt_type(), OptionType::Integer);
1004        assert!(!def.is_deprecated());
1005        assert!(!def.is_hidden());
1006    }
1007
1008    #[test]
1009    fn test_option_def_parse_integer() {
1010        let def = OptionDef {
1011            name: "split".into(),
1012            opt_type: OptionType::Integer,
1013            min: Some(1),
1014            max: Some(16),
1015            ..Default::default()
1016        };
1017        let v = def.parse_value("5").unwrap();
1018        assert_eq!(v.as_i64().unwrap(), 5);
1019
1020        let err = def.parse_value("0");
1021        assert!(err.is_err());
1022
1023        let err2 = def.parse_value("abc");
1024        assert!(err2.is_err());
1025    }
1026
1027    #[test]
1028    fn test_option_def_parse_boolean() {
1029        let def = OptionDef::new("verbose", OptionType::Boolean);
1030        assert!(def.parse_value("true").unwrap().as_bool().unwrap());
1031        assert!(def.parse_value("yes").unwrap().as_bool().unwrap());
1032        assert!(def.parse_value("1").unwrap().as_bool().unwrap());
1033        assert!(!def.parse_value("false").unwrap().as_bool().unwrap());
1034        assert!(!def.parse_value("no").unwrap().as_bool().unwrap());
1035        assert!(def.parse_value("invalid").is_err());
1036    }
1037
1038    #[test]
1039    fn test_option_def_parse_list() {
1040        let def = OptionDef::new("header", OptionType::List);
1041        let v = def.parse_value("X-Custom:foo,X-Bar:baz").unwrap();
1042        assert_eq!(v.as_list().unwrap().len(), 2);
1043    }
1044
1045    #[test]
1046    fn test_option_def_parse_empty_uses_default() {
1047        let def = OptionDef {
1048            name: "dir".into(),
1049            opt_type: OptionType::Path,
1050            default_value: OptionValue::Str("/tmp".into()),
1051            ..Default::default()
1052        };
1053        let v = def.parse_value("").unwrap();
1054        assert_eq!(v.as_str().unwrap(), "/tmp");
1055    }
1056
1057    #[test]
1058    fn test_registry_creation() {
1059        let reg = OptionRegistry::new();
1060        assert!(reg.count() >= 60);
1061        assert!(reg.get("split").is_some());
1062        assert!(reg.get("nonexistent-option").is_none());
1063    }
1064
1065    #[test]
1066    fn test_registry_by_category() {
1067        let reg = OptionRegistry::new();
1068        let general = reg.by_category(OptionCategory::General);
1069        let bt = reg.by_category(OptionCategory::BitTorrent);
1070        let rpc = reg.by_category(OptionCategory::Rpc);
1071        assert!(!general.is_empty());
1072        assert!(!bt.is_empty());
1073        assert!(!rpc.is_empty());
1074    }
1075
1076    #[test]
1077    fn test_registry_defaults_are_valid() {
1078        let reg = OptionRegistry::new();
1079        for def in reg.all().values() {
1080            if !matches!(def.default_value(), OptionValue::None) {
1081                let parsed = def.parse_value(&def.default_value().to_string());
1082                assert!(
1083                    parsed.is_ok(),
1084                    "Default value for '{}' failed to re-parse: {:?}",
1085                    def.name(),
1086                    parsed.err()
1087                );
1088            }
1089        }
1090    }
1091
1092    #[test]
1093    fn test_default_registry() {
1094        let reg = OptionRegistry::default();
1095        assert!(reg.count() > 0);
1096    }
1097
1098    // ==================== Validator Tests ====================
1099
1100    #[test]
1101    fn test_range_validator_in_range() {
1102        let validator = RangeValidator::<i64>::new(1, 16);
1103        assert!(validator.validate("split", &Value::from(1)).is_ok());
1104        assert!(validator.validate("split", &Value::from(8)).is_ok());
1105        assert!(validator.validate("split", &Value::from(16)).is_ok());
1106
1107        let float_validator = RangeValidator::<f64>::new(0.0, 1.0);
1108        assert!(float_validator.validate("ratio", &Value::from(0.5)).is_ok());
1109
1110        let u64_validator = RangeValidator::<u64>::new(1024, 1024 * 1024);
1111        assert!(
1112            u64_validator
1113                .validate("size", &Value::from(4096u64))
1114                .is_ok()
1115        );
1116    }
1117
1118    #[test]
1119    fn test_range_validator_out_of_range() {
1120        let validator = RangeValidator::<i64>::new(1, 16);
1121        let result = validator.validate("split", &Value::from(0));
1122        assert!(result.is_err());
1123        match result.unwrap_err() {
1124            OptionError::OutOfRange { value, min, max } => {
1125                assert_eq!(value, "0");
1126                assert_eq!(min, "1");
1127                assert_eq!(max, "16");
1128            }
1129            other => panic!("Expected OutOfRange error, got {:?}", other),
1130        }
1131    }
1132
1133    #[test]
1134    fn test_choice_validator_enum() {
1135        let validator = ChoiceValidator::new(vec![
1136            "debug".to_string(),
1137            "info".to_string(),
1138            "warn".to_string(),
1139            "error".to_string(),
1140        ]);
1141        assert!(
1142            validator
1143                .validate("log-level", &Value::String("debug".into()))
1144                .is_ok()
1145        );
1146        assert!(
1147            validator
1148                .validate("log-level", &Value::String("verbose".into()))
1149                .is_err()
1150        );
1151    }
1152
1153    #[test]
1154    fn test_url_validator_malformed() {
1155        let validator = UrlValidator::new();
1156        assert!(
1157            validator
1158                .validate(
1159                    "tracker",
1160                    &Value::String("http://example.com:6969/announce".into())
1161                )
1162                .is_ok()
1163        );
1164        assert!(
1165            validator
1166                .validate("url", &Value::String("not-a-url".into()))
1167                .is_err()
1168        );
1169    }
1170
1171    #[test]
1172    fn test_regex_validator_pattern_match() {
1173        let validator = RegexValidator::new(r"^[a-zA-Z0-9.-]+:\d+$");
1174        assert!(
1175            validator
1176                .validate("proxy", &Value::String("proxy.example.com:8080".into()))
1177                .is_ok()
1178        );
1179        assert!(
1180            validator
1181                .validate("proxy", &Value::String("not-valid".into()))
1182                .is_err()
1183        );
1184    }
1185
1186    #[test]
1187    fn test_dependency_checker() {
1188        let mut checker = DependencyChecker::new();
1189        checker.add_mutual_exclusion("ftp-pasv".to_string(), "ftp-port".to_string());
1190        checker.add_requirement("bt-enable-lpd".to_string(), "enable-dht".to_string());
1191
1192        let mut opts = HashMap::new();
1193        opts.insert("ftp-pasv".to_string(), Value::Bool(true));
1194        opts.insert("ftp-port".to_string(), Value::from(8021));
1195        let errors = checker.check(&opts);
1196        assert_eq!(errors.len(), 1);
1197    }
1198
1199    #[test]
1200    fn test_option_definition_validation() {
1201        let def = OptionDefinition {
1202            name: "max-connections",
1203            description: "Maximum connections per server",
1204            default_value: Value::from(16),
1205            validator: Some(Box::new(RangeValidator::<i64>::new(1, 32))),
1206        };
1207        assert!(def.validate(&Value::from(8)).is_ok());
1208        assert!(def.validate(&Value::from(0)).is_err());
1209    }
1210
1211    #[test]
1212    fn test_option_error_display() {
1213        let err = OptionError::TypeMismatch {
1214            expected: "integer".to_string(),
1215            got: "string".to_string(),
1216        };
1217        let msg = format!("{}", err);
1218        assert!(msg.contains("type mismatch"));
1219    }
1220}