From 0bcb7adffbe3d24d8fe4e7bb8a99b98aa83c7437 Mon Sep 17 00:00:00 2001 From: Ciphercat <78522797+0xrsydn@users.noreply.github.com> Date: Thu, 12 Mar 2026 17:16:51 +0000 Subject: [PATCH] fix(config): validate known config keys at write time Reject invalid values for known config keys in 'idx config set': - general.provider: only yahoo|msn - general.history_provider: only auto|yahoo|msn - general.output: only table|json - general.color: only true|false - cache TTL fields: non-negative integers only - Unknown keys: rejected with clear error listing valid keys --- src/config.rs | 205 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) diff --git a/src/config.rs b/src/config.rs index 6339d49..74af721 100644 --- a/src/config.rs +++ b/src/config.rs @@ -233,7 +233,74 @@ pub fn get_config_value(key: &str) -> Result, IdxError> { Ok(Some(cur.to_string().trim_matches('"').to_string())) } +/// Known configuration keys and their valid values +const KNOWN_CONFIG_KEYS: &[&str] = &[ + "general.provider", + "general.history_provider", + "general.exchange", + "general.output", + "general.color", + "cache.quote_ttl", + "cache.fundamental_ttl", +]; + +/// Validates a config key and value before writing +fn validate_config_key_value(key: &str, value: &str) -> Result<(), IdxError> { + match key { + "general.provider" => { + ProviderKind::parse(value)?; + } + "general.history_provider" => { + HistoryProviderKind::parse(value)?; + } + "general.output" => { + if !value.eq_ignore_ascii_case("table") && !value.eq_ignore_ascii_case("json") { + return Err(IdxError::InvalidInput(format!( + "invalid output format '{}': expected 'table' or 'json'", + value + ))); + } + } + "cache.quote_ttl" | "cache.fundamental_ttl" => { + let parsed: i64 = value.parse().map_err(|_| { + IdxError::InvalidInput(format!( + "invalid TTL value '{}': must be a non-negative integer", + value + )) + })?; + if parsed < 0 { + return Err(IdxError::InvalidInput(format!( + "invalid TTL value '{}': must be non-negative", + value + ))); + } + } + "general.exchange" => { + // Exchange is free-form (e.g. JK, US, etc.) + } + "general.color" => { + if !value.eq_ignore_ascii_case("true") && !value.eq_ignore_ascii_case("false") { + return Err(IdxError::InvalidInput(format!( + "invalid color value '{}': expected 'true' or 'false'", + value + ))); + } + } + _ => { + return Err(IdxError::InvalidInput(format!( + "unknown config key '{}'. Valid keys: {}", + key, + KNOWN_CONFIG_KEYS.join(", ") + ))); + } + } + Ok(()) +} + pub fn set_config_value(key: &str, value: &str) -> Result<(), IdxError> { + // Validate the key and value before writing + validate_config_key_value(key, value)?; + let path = ensure_default_config()?; let raw = fs::read_to_string(&path).map_err(|e| IdxError::Io(e.to_string()))?; let mut root: toml::Value = @@ -322,4 +389,142 @@ mod tests { ); assert!(super::HistoryProviderKind::parse("unknown").is_err()); } + + #[test] + fn validate_rejects_unknown_keys() { + let result = super::validate_config_key_value("unknown.key", "value"); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(matches!(err, super::IdxError::InvalidInput(_))); + assert!(err.to_string().contains("unknown config key")); + } + + #[test] + fn validate_rejects_invalid_provider() { + let result = super::validate_config_key_value("general.provider", "bogus"); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("invalid provider")); + } + + #[test] + fn validate_accepts_valid_provider_yahoo() { + assert!(super::validate_config_key_value("general.provider", "yahoo").is_ok()); + } + + #[test] + fn validate_accepts_valid_provider_msn() { + assert!(super::validate_config_key_value("general.provider", "msn").is_ok()); + } + + #[test] + fn validate_accepts_valid_provider_case_insensitive() { + assert!(super::validate_config_key_value("general.provider", "YAHOO").is_ok()); + assert!(super::validate_config_key_value("general.provider", "Msn").is_ok()); + } + + #[test] + fn validate_rejects_invalid_history_provider() { + let result = super::validate_config_key_value("general.history_provider", "bogus"); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("invalid history provider")); + } + + #[test] + fn validate_accepts_valid_history_provider_auto() { + assert!( + super::validate_config_key_value("general.history_provider", "auto").is_ok() + ); + } + + #[test] + fn validate_accepts_valid_history_provider_yahoo() { + assert!( + super::validate_config_key_value("general.history_provider", "yahoo").is_ok() + ); + } + + #[test] + fn validate_accepts_valid_history_provider_msn() { + assert!( + super::validate_config_key_value("general.history_provider", "msn").is_ok() + ); + } + + #[test] + fn validate_rejects_invalid_output() { + let result = super::validate_config_key_value("general.output", "bogus"); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("invalid output format")); + } + + #[test] + fn validate_accepts_valid_output_table() { + assert!(super::validate_config_key_value("general.output", "table").is_ok()); + } + + #[test] + fn validate_accepts_valid_output_json() { + assert!(super::validate_config_key_value("general.output", "json").is_ok()); + } + + #[test] + fn validate_accepts_valid_output_case_insensitive() { + assert!(super::validate_config_key_value("general.output", "TABLE").is_ok()); + assert!(super::validate_config_key_value("general.output", "Json").is_ok()); + } + + #[test] + fn validate_rejects_negative_ttl() { + let result = super::validate_config_key_value("cache.quote_ttl", "-1"); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("must be non-negative")); + } + + #[test] + fn validate_rejects_non_numeric_ttl() { + let result = super::validate_config_key_value("cache.quote_ttl", "not-a-number"); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("must be a non-negative integer")); + } + + #[test] + fn validate_accepts_valid_ttl() { + assert!(super::validate_config_key_value("cache.quote_ttl", "300").is_ok()); + assert!(super::validate_config_key_value("cache.fundamental_ttl", "3600").is_ok()); + } + + #[test] + fn validate_accepts_zero_ttl() { + assert!(super::validate_config_key_value("cache.quote_ttl", "0").is_ok()); + } + + #[test] + fn validate_accepts_exchange_any_value() { + assert!(super::validate_config_key_value("general.exchange", "JK").is_ok()); + assert!(super::validate_config_key_value("general.exchange", "US").is_ok()); + } + + #[test] + fn validate_accepts_valid_color() { + assert!(super::validate_config_key_value("general.color", "true").is_ok()); + assert!(super::validate_config_key_value("general.color", "false").is_ok()); + assert!(super::validate_config_key_value("general.color", "TRUE").is_ok()); + assert!(super::validate_config_key_value("general.color", "False").is_ok()); + } + + #[test] + fn validate_rejects_invalid_color() { + let result = super::validate_config_key_value("general.color", "foo"); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("invalid color value")); + + let result = super::validate_config_key_value("general.color", "123"); + assert!(result.is_err()); + } }