//! Configuration parsing for Aphoria. use std::path::{Path, PathBuf}; use serde::Deserialize; use crate::AphoriaError; /// Top-level Aphoria configuration. /// /// Loaded from `aphoria.toml` at the project root. #[derive(Debug, Clone, Default, Deserialize)] #[serde(default)] pub struct AphoriaConfig { /// Project settings. pub project: ProjectConfig, /// Episteme instance settings. pub episteme: EpistemeConfig, /// Conflict threshold settings. pub thresholds: ThresholdConfig, /// Extractor settings. pub extractors: ExtractorConfig, /// Scan settings. pub scan: ScanConfig, /// Alias suggestion settings. pub aliases: AliasConfig, /// Corpus builder settings. pub corpus: CorpusConfig, } impl AphoriaConfig { /// Load configuration from a TOML file. pub fn from_file(path: &Path) -> Result { if !path.exists() { return Err(AphoriaError::ConfigNotFound(path.to_path_buf())); } let content = std::fs::read_to_string(path)?; let config: AphoriaConfig = toml::from_str(&content)?; Ok(config) } } /// Project identification settings. #[derive(Debug, Clone, Default, Deserialize)] #[serde(default)] pub struct ProjectConfig { /// Project name (auto-detected if not specified). pub name: Option, /// Primary language (auto-detected if not specified). pub language: Option, } /// Episteme instance configuration. #[derive(Debug, Clone, Deserialize)] #[serde(default)] pub struct EpistemeConfig { /// Path to local Episteme data directory. pub data_dir: PathBuf, /// Remote Episteme URL (future feature). pub url: Option, } impl Default for EpistemeConfig { fn default() -> Self { Self { data_dir: dirs_default_data_dir(), url: None } } } /// Conflict threshold configuration. #[derive(Debug, Clone, Deserialize)] #[serde(default)] pub struct ThresholdConfig { /// Conflict score at or above which to BLOCK. pub block: f32, /// Conflict score at or above which to FLAG. pub flag: f32, } impl Default for ThresholdConfig { fn default() -> Self { Self { block: 0.7, flag: 0.4 } } } /// Extractor configuration. #[derive(Debug, Clone, Deserialize)] #[serde(default)] pub struct ExtractorConfig { /// Enabled extractors. pub enabled: Vec, /// Disabled extractors (alternative to enabled list). pub disabled: Vec, /// Timeout extractor settings. pub timeout_config: TimeoutExtractorConfig, /// Dependency version extractor settings. pub dep_versions: DepVersionConfig, } impl Default for ExtractorConfig { fn default() -> Self { Self { enabled: vec![ "tls_verify".to_string(), "jwt_config".to_string(), "hardcoded_secrets".to_string(), "timeout_config".to_string(), "dep_versions".to_string(), "cors_config".to_string(), "rate_limit".to_string(), ], disabled: vec![], timeout_config: TimeoutExtractorConfig::default(), dep_versions: DepVersionConfig::default(), } } } /// Timeout extractor configuration. #[derive(Debug, Clone, Deserialize)] #[serde(default)] pub struct TimeoutExtractorConfig { /// Minimum reasonable timeout in milliseconds. pub min_reasonable_ms: u64, /// Maximum reasonable timeout in milliseconds. pub max_reasonable_ms: u64, } impl Default for TimeoutExtractorConfig { fn default() -> Self { Self { min_reasonable_ms: 1000, max_reasonable_ms: 300_000 } } } /// Dependency version extractor configuration. #[derive(Debug, Clone, Deserialize)] #[serde(default)] pub struct DepVersionConfig { /// Path to advisory database. pub advisory_db: PathBuf, } impl Default for DepVersionConfig { fn default() -> Self { Self { advisory_db: dirs_default_advisory_db() } } } /// Scan configuration. #[derive(Debug, Clone, Deserialize)] #[serde(default)] pub struct ScanConfig { /// Directories to exclude from scanning. pub exclude: Vec, /// Maximum file size to scan (bytes). pub max_file_size: u64, /// Whether to include test files. pub include_tests: bool, } impl Default for ScanConfig { fn default() -> Self { Self { exclude: vec![ "target/".to_string(), "node_modules/".to_string(), ".git/".to_string(), "vendor/".to_string(), ], max_file_size: 1_048_576, // 1MB include_tests: false, } } } /// Alias suggestion configuration. #[derive(Debug, Clone, Deserialize)] #[serde(default)] pub struct AliasConfig { /// Whether to auto-suggest aliases for shared concepts. pub auto_suggest: bool, /// Whether to auto-accept aliases to Tier 0 sources. pub auto_accept_tier0: bool, /// Whether to automatically create aliases when conflicts are detected. /// /// When enabled, tail-path matching during conflict detection will /// persist aliases (e.g., `code://rust/tls/cert_verification` → /// `rfc://5246/tls/cert_verification`) for faster future queries. pub auto_create_aliases: bool, } impl Default for AliasConfig { fn default() -> Self { Self { auto_suggest: true, auto_accept_tier0: true, auto_create_aliases: true } } } /// Corpus builder configuration. #[derive(Debug, Clone, Deserialize)] #[serde(default)] pub struct CorpusConfig { /// Directory for caching downloaded RFCs and OWASP cheat sheets. pub cache_dir: PathBuf, /// Whether to include the hardcoded corpus (built-in assertions). pub include_hardcoded: bool, /// Whether to include RFC normative statements. pub include_rfc: bool, /// Whether to include OWASP cheat sheet recommendations. pub include_owasp: bool, /// Whether to include vendor documentation claims. pub include_vendor: bool, /// Override the default RFC list (if None, uses default list). pub rfc_list: Option>, } impl Default for CorpusConfig { fn default() -> Self { Self { cache_dir: dirs_default_cache_dir(), include_hardcoded: true, include_rfc: true, include_owasp: true, include_vendor: true, rfc_list: None, } } } /// Get the default Aphoria data directory. fn dirs_default_data_dir() -> PathBuf { if let Some(home) = dirs::home_dir() { home.join(".aphoria").join("db") } else { PathBuf::from(".aphoria/db") } } /// Get the default advisory database directory. fn dirs_default_advisory_db() -> PathBuf { if let Some(home) = dirs::home_dir() { home.join(".aphoria").join("advisory-db") } else { PathBuf::from(".aphoria/advisory-db") } } /// Get the default cache directory for corpus downloads. fn dirs_default_cache_dir() -> PathBuf { if let Some(cache) = dirs::cache_dir() { cache.join("aphoria") } else if let Some(home) = dirs::home_dir() { home.join(".cache").join("aphoria") } else { PathBuf::from(".aphoria/cache") } } #[cfg(test)] mod tests { use super::*; #[test] fn test_default_config() { let config = AphoriaConfig::default(); assert_eq!(config.thresholds.block, 0.7); assert_eq!(config.thresholds.flag, 0.4); assert!(config.extractors.enabled.contains(&"tls_verify".to_string())); assert!(config.scan.exclude.contains(&"target/".to_string())); } #[test] fn test_config_parse() { let toml = r#" [project] name = "testproject" language = "rust" [thresholds] block = 0.8 flag = 0.5 [scan] exclude = ["build/", "dist/"] "#; let config: AphoriaConfig = toml::from_str(toml).expect("should parse"); assert_eq!(config.project.name, Some("testproject".to_string())); assert_eq!(config.project.language, Some("rust".to_string())); assert_eq!(config.thresholds.block, 0.8); assert_eq!(config.thresholds.flag, 0.5); assert!(config.scan.exclude.contains(&"build/".to_string())); } }