use std::path::PathBuf;
use thiserror::Error;
#[derive(Debug, Error, Clone)]
pub enum ConfigError {
#[error("Configuration file not found: {path}")]
FileNotFound { path: PathBuf },
#[error("Format detection failed: {0}")]
FormatDetectionFailed(String),
#[error("Parse error: {0}")]
ParseError(String),
#[error("Validation error: {0}")]
ValidationError(String),
#[error("Unsafe path: {0}")]
UnsafePath(PathBuf),
#[error("Remote configuration load failed: {0}")]
RemoteError(String),
#[error("Configuration load failed")]
LoadError,
#[error("Runtime error: {0}")]
RuntimeError(String),
#[error("Serialization error: {0}")]
SerializationError(String),
#[error("IO error: {0}")]
IoError(String),
#[error("Memory limit exceeded: limit {limit}MB, current {current}MB")]
MemoryLimitExceeded { limit: usize, current: usize },
#[error("Configuration file too large: {path} ({size_mb}MB exceeds limit {limit_mb}MB)")]
ConfigTooLarge {
path: PathBuf,
size_mb: usize,
limit_mb: usize,
},
#[error("Key error: {0}")]
KeyError(String),
#[error("Key not found: {key_id}")]
KeyNotFound { key_id: String },
#[error("Key version mismatch: expected {expected}, actual {actual}")]
KeyVersionMismatch { expected: u32, actual: u32 },
#[error("Key rotation failed: {0}")]
KeyRotationFailed(String),
#[error("Key storage error: {0}")]
KeyStorageError(String),
#[error("Key verification failed: checksum mismatch")]
KeyChecksumMismatch,
#[error("Key expired: {key_id}, version {version}")]
KeyExpired { key_id: String, version: u32 },
#[error("Key deprecated: {key_id}, version {version}")]
KeyDeprecated { key_id: String, version: u32 },
#[error("Invalid master key: {0}")]
InvalidMasterKey(String),
#[error("Key policy error: {0}")]
KeyPolicyError(String),
#[error("Environment variable security validation failed: {0}")]
EnvSecurityError(String),
#[error("Encryption error: {0}")]
EncryptionError(String),
#[error("Decryption error: {0}")]
DecryptionError(String),
#[error("Other error: {0}")]
Other(String),
}
#[cfg(feature = "validation")]
impl From<validator::ValidationErrors> for ConfigError {
fn from(_err: validator::ValidationErrors) -> Self {
ConfigError::ValidationError("Validation failed".to_string())
}
}
impl From<figment::Error> for ConfigError {
fn from(_err: figment::Error) -> Self {
ConfigError::LoadError
}
}
impl From<std::io::Error> for ConfigError {
fn from(err: std::io::Error) -> Self {
ConfigError::IoError(err.to_string())
}
}
impl From<String> for ConfigError {
fn from(s: String) -> Self {
ConfigError::FormatDetectionFailed(s)
}
}
impl From<crate::security::EnvSecurityError> for ConfigError {
fn from(err: crate::security::EnvSecurityError) -> Self {
ConfigError::EnvSecurityError(err.to_string())
}
}
impl ConfigError {
pub fn remote_safe(message: impl Into<String>) -> Self {
ConfigError::RemoteError(message.into())
}
pub fn remote_with_url(url: impl Into<String>, message: impl Into<String>) -> Self {
ConfigError::RemoteError(format!("{} (URL: {})", message.into(), url.into()))
}
pub fn safe_display(&self) -> String {
match self {
ConfigError::RemoteError(msg) => {
let safe_msg = Self::sanitize_url(msg);
format!("Remote configuration load failed: {}", safe_msg)
}
ConfigError::FileNotFound { path } => {
if let Some(filename) = path.file_name() {
format!(
"Configuration file not found: {}",
filename.to_string_lossy()
)
} else {
"Configuration file not found".to_string()
}
}
ConfigError::KeyNotFound { key_id } => {
format!("Key not found: {}", Self::mask_key_id(key_id))
}
ConfigError::KeyExpired { key_id, version } => {
format!(
"Key expired: {}, version {}",
Self::mask_key_id(key_id),
version
)
}
ConfigError::KeyDeprecated { key_id, version } => {
format!(
"Key deprecated: {}, version {}",
Self::mask_key_id(key_id),
version
)
}
ConfigError::IoError(msg) => {
let sanitized = msg
.split(['/', '\\'])
.next_back()
.unwrap_or(msg)
.to_string();
format!("IO error: {}", sanitized)
}
ConfigError::ParseError(msg) => {
let sanitized = msg
.split_whitespace()
.take(10)
.collect::<Vec<_>>()
.join(" ");
format!("Parse error: {}", sanitized)
}
ConfigError::EnvSecurityError(msg) => {
let sanitized = msg.split('=').next().unwrap_or("ENVIRONMENT").to_string();
format!("Environment security validation failed: {}", sanitized)
}
ConfigError::EncryptionError(msg) => {
let sanitized = msg.chars().take(100).collect::<String>();
format!("Encryption error: {}", sanitized)
}
ConfigError::DecryptionError(msg) => {
let sanitized = msg.chars().take(100).collect::<String>();
format!("Decryption error: {}", sanitized)
}
ConfigError::RuntimeError(msg) => {
let sanitized = msg
.split(['/', '\\'])
.next_back()
.unwrap_or(msg)
.to_string();
format!("Runtime error: {}", sanitized)
}
ConfigError::MemoryLimitExceeded { limit, current } => {
format!(
"Memory limit exceeded: limit {}MB, current {}MB",
limit, current
)
}
ConfigError::ConfigTooLarge {
path,
size_mb,
limit_mb,
} => {
let filename = path
.file_name()
.map(|f| f.to_string_lossy().to_string())
.unwrap_or_else(|| "config file".to_string());
format!(
"Configuration file too large: {} ({}MB exceeds limit {}MB)",
filename, size_mb, limit_mb
)
}
ConfigError::KeyError(msg) => {
let sanitized = msg.chars().take(50).collect::<String>();
format!("Key error: {}", sanitized)
}
ConfigError::KeyVersionMismatch { expected, actual } => {
format!(
"Key version mismatch: expected {}, actual {}",
expected, actual
)
}
ConfigError::KeyRotationFailed(msg) => {
let sanitized = msg.chars().take(50).collect::<String>();
format!("Key rotation failed: {}", sanitized)
}
ConfigError::KeyStorageError(msg) => {
let sanitized = msg
.split(['/', '\\'])
.next_back()
.unwrap_or(msg)
.to_string();
format!("Key storage error: {}", sanitized)
}
ConfigError::InvalidMasterKey(msg) => {
let sanitized = msg.chars().take(50).collect::<String>();
format!("Invalid master key: {}", sanitized)
}
ConfigError::KeyPolicyError(msg) => {
format!("Key policy error: {}", msg)
}
ConfigError::KeyChecksumMismatch => {
"Key verification failed: checksum mismatch".to_string()
}
ConfigError::FormatDetectionFailed(msg) => {
format!("Format detection failed: {}", msg)
}
ConfigError::ValidationError(msg) => {
format!("Validation error: {}", msg)
}
ConfigError::UnsafePath(path) => {
if let Some(filename) = path.file_name() {
format!("Unsafe path: {}", filename.to_string_lossy())
} else {
"Unsafe path".to_string()
}
}
ConfigError::LoadError => "Configuration load failed".to_string(),
ConfigError::SerializationError(msg) => {
let sanitized = msg.chars().take(100).collect::<String>();
format!("Serialization error: {}", sanitized)
}
ConfigError::Other(msg) => {
let sanitized = msg.chars().take(100).collect::<String>();
format!("Error: {}", sanitized)
}
}
}
fn sanitize_url(msg: &str) -> String {
use regex::Regex;
static URL_PATTERN: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
let url_regex = URL_PATTERN.get_or_init(|| {
Regex::new(r"(?i)(https?://)([^:/\s]+):([^@/\s]+)@([^/\s]+)(/\S*)?")
.unwrap_or_else(|_| Regex::new(r"https?://\S+").unwrap())
});
let result = url_regex.replace_all(msg, |caps: ®ex::Captures| {
let protocol = caps.get(1).map(|m| m.as_str()).unwrap_or("");
let _username = caps.get(2).map(|m| m.as_str()).unwrap_or("");
let _password = caps.get(3).map(|m| m.as_str()).unwrap_or("");
let host = caps.get(4).map(|m| m.as_str()).unwrap_or("");
let path = caps.get(5).map(|m| m.as_str()).unwrap_or("");
format!("{}***:***@{}{}", protocol, host, path)
});
static IP_PATTERN: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
let ip_regex = IP_PATTERN.get_or_init(|| {
Regex::new(r"\b(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\b")
.unwrap_or_else(|_| Regex::new(r"\d+\.\d+\.\d+\.\d+").unwrap())
});
let result = ip_regex.replace_all(&result, |caps: ®ex::Captures| {
format!(
"{}.{}.*.*",
caps.get(1).map(|m| m.as_str()).unwrap_or("x"),
caps.get(2).map(|m| m.as_str()).unwrap_or("x")
)
});
result.to_string()
}
fn mask_key_id(key_id: &str) -> String {
if key_id.len() <= 8 {
"***".to_string()
} else {
format!("{}***{}", &key_id[..4], &key_id[key_id.len() - 4..])
}
}
}