use anyhow::Result;
use std::io;
use thiserror::Error;
pub type KindlyResult<T> = Result<T, KindlyError>;
pub trait ResultExt<T> {
fn kindly(self) -> KindlyResult<T>;
}
impl<T, E> ResultExt<T> for Result<T, E>
where
E: Into<anyhow::Error>,
{
fn kindly(self) -> KindlyResult<T> {
self.map_err(|e| KindlyError::ConfigError(e.into().to_string()))
}
}
#[derive(Error, Debug)]
pub enum KindlyError {
#[error("Display rendering failed: {0}")]
DisplayError(String),
#[error("Terminal not available")]
TerminalError,
#[error("Command validation failed: {0}")]
ValidationError(String),
#[error("Invalid input: {reason}")]
InvalidInput { reason: String },
#[error("Invalid configuration: {field}: {reason}")]
InvalidConfig { field: String, reason: String },
#[error("File operation failed: {0}")]
FileError(#[from] io::Error),
#[error("Path not found: {path}")]
PathNotFound { path: String },
#[error("JSON serialization failed: {0}")]
SerializationError(#[from] serde_json::Error),
#[error("Format error: expected {expected}, got {actual}")]
FormatError { expected: String, actual: String },
#[error("Scanner initialization failed: {0}")]
ScannerError(String),
#[error("Threat detected: {threat_type} at {location}")]
ThreatDetected {
threat_type: String,
location: String,
},
#[error("Resource limit exceeded: {resource}: {limit}")]
ResourceError { resource: String, limit: String },
#[error("Operation timed out after {0} seconds")]
TimeoutError(u64),
#[error("Network error: {0}")]
NetworkError(String),
#[error("Connection failed to {endpoint}: {reason}")]
ConnectionError { endpoint: String, reason: String },
#[error("Authentication failed: {reason}")]
AuthError { reason: String },
#[error("Unauthorized: {action}")]
Unauthorized { action: String },
#[error("Protocol error: {code}: {message}")]
ProtocolError { code: i32, message: String },
#[error("Method not found: {method}")]
MethodNotFound { method: String },
#[error("Configuration error: {0}")]
ConfigError(String),
#[error("Internal error: {0}")]
Internal(String),
}
impl KindlyError {
pub const fn severity(&self) -> ErrorSeverity {
match self {
Self::ThreatDetected { .. } => ErrorSeverity::Critical,
Self::AuthError { .. } => ErrorSeverity::Critical,
Self::Unauthorized { .. } => ErrorSeverity::Critical,
Self::ScannerError(_) => ErrorSeverity::High,
Self::ResourceError { .. } => ErrorSeverity::High,
Self::TimeoutError(_) => ErrorSeverity::High,
Self::Internal(_) => ErrorSeverity::High,
Self::NetworkError(_) => ErrorSeverity::Medium,
Self::ConnectionError { .. } => ErrorSeverity::Medium,
Self::ProtocolError { .. } => ErrorSeverity::Medium,
Self::ConfigError(_) => ErrorSeverity::Medium,
Self::DisplayError(_) => ErrorSeverity::Low,
Self::TerminalError => ErrorSeverity::Low,
Self::ValidationError(_) => ErrorSeverity::Low,
Self::InvalidInput { .. } => ErrorSeverity::Low,
Self::InvalidConfig { .. } => ErrorSeverity::Low,
Self::FileError(_) => ErrorSeverity::Low,
Self::PathNotFound { .. } => ErrorSeverity::Low,
Self::SerializationError(_) => ErrorSeverity::Low,
Self::FormatError { .. } => ErrorSeverity::Low,
Self::MethodNotFound { .. } => ErrorSeverity::Low,
}
}
pub const fn is_retryable(&self) -> bool {
matches!(
self,
Self::NetworkError(_)
| Self::ConnectionError { .. }
| Self::TimeoutError(_)
| Self::ResourceError { .. }
)
}
pub fn user_message(&self) -> String {
match self {
Self::ThreatDetected { .. } => {
"Security threat detected: policy violation".to_string()
},
Self::AuthError { .. } => {
"Authentication failed. Please check your credentials.".to_string()
},
Self::Unauthorized { .. } => {
"Unauthorized access".to_string()
},
Self::TimeoutError(_) => {
"Operation timed out".to_string()
},
Self::ResourceError { .. } => {
"Resource limit exceeded".to_string()
},
_ => self.to_string(),
}
}
pub const fn to_protocol_code(&self) -> i32 {
match self {
Self::ProtocolError { code, .. } => *code,
Self::MethodNotFound { .. } => -32601,
Self::InvalidInput { .. } => -32602,
Self::AuthError { .. } | Self::Unauthorized { .. } => -32001,
Self::TimeoutError(_) => -32002,
Self::ResourceError { .. } => -32003,
Self::ThreatDetected { .. } => -32004,
_ => -32603, }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ErrorSeverity {
Low,
Medium,
High,
Critical,
}
#[derive(Debug, Clone)]
pub enum RecoveryStrategy {
RetryWithBackoff {
max_attempts: u32,
base_delay_ms: u64,
},
Fallback,
LogAndContinue,
FailFast,
}
pub struct ErrorContext {
pub error: KindlyError,
pub strategy: RecoveryStrategy,
pub user_hint: String,
}
impl ErrorContext {
pub fn new(error: KindlyError, strategy: RecoveryStrategy, hint: &str) -> Self {
Self {
error,
strategy,
user_hint: hint.to_string(),
}
}
pub fn user_message(&self) -> String {
format!("{}\n\nHint: {}", self.error, self.user_hint)
}
}
pub mod recovery {
use super::{KindlyError, Result};
use std::time::Duration;
use tokio::time::sleep;
pub async fn retry_with_backoff<F, T, E>(
mut operation: F,
max_attempts: u32,
base_delay_ms: u64,
) -> Result<T>
where
F: FnMut() -> Result<T, E>,
E: std::error::Error + Send + Sync + 'static,
{
let mut attempt = 0;
let mut delay = base_delay_ms;
loop {
attempt += 1;
match operation() {
Ok(result) => return Ok(result),
Err(e) if attempt >= max_attempts => {
return Err(anyhow::anyhow!(
"Operation failed after {} attempts: {}",
max_attempts,
e
));
},
Err(_) => {
sleep(Duration::from_millis(delay)).await;
delay = (delay * 2).min(30_000); },
}
}
}
pub async fn with_timeout<F, T>(operation: F, timeout_secs: u64) -> anyhow::Result<T>
where
F: std::future::Future<Output = anyhow::Result<T>>,
{
match tokio::time::timeout(Duration::from_secs(timeout_secs), operation).await {
Ok(result) => result,
Err(_) => Err(KindlyError::TimeoutError(timeout_secs).into()),
}
}
}
pub mod handlers {
use super::{io, ErrorContext, KindlyError, RecoveryStrategy};
pub fn handle_display_error(error: anyhow::Error) -> String {
eprintln!("Display error: {error}");
format!(
"KindlyGuard | Status: Error | Message: Display unavailable\n\
Error: {error}\n\
Try running with --format minimal or --no-color"
)
}
pub fn handle_file_error(path: &str, error: io::Error) -> ErrorContext {
let hint = match error.kind() {
io::ErrorKind::NotFound => {
format!("File '{path}' not found. Check the path and try again.")
},
io::ErrorKind::PermissionDenied => {
format!("Permission denied for '{path}'. Check file permissions.")
},
io::ErrorKind::InvalidData => {
"File contains invalid data. It may be corrupted.".to_string()
},
_ => {
format!("Failed to access '{path}': {error}")
},
};
ErrorContext::new(
KindlyError::FileError(error),
RecoveryStrategy::FailFast,
&hint,
)
}
pub fn handle_validation_error(field: &str, value: &str, reason: &str) -> ErrorContext {
let hint = match field {
"path" => "Use absolute paths without '..' and ensure the file exists.".to_string(),
"port" => "Use a port number between 1024 and 65535.".to_string(),
"feature" => "Valid features: unicode, injection, path, advanced.".to_string(),
_ => {
format!("Check the {field} value and try again.")
},
};
ErrorContext::new(
KindlyError::ValidationError(format!("Invalid {field}: '{value}' - {reason}")),
RecoveryStrategy::FailFast,
&hint,
)
}
}
pub mod degradation {
use crate::shield::Shield;
use std::sync::Arc;
pub fn degrade_display_format(
shield: Arc<Shield>,
mut formats: Vec<crate::shield::universal_display::DisplayFormat>,
) -> String {
use crate::shield::{UniversalDisplay, UniversalDisplayConfig};
while let Some(format) = formats.pop() {
let config = UniversalDisplayConfig {
color: false, detailed: false,
format,
status_file: None, };
let display = UniversalDisplay::new(shield.clone(), config);
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| display.render())) {
Ok(output) if !output.is_empty() => return output,
_ => continue,
}
}
"KindlyGuard | Status: Unknown | Error: Display system failure".to_string()
}
}
pub mod security_patterns {
use super::*;
use tracing::{error, warn};
pub fn handle_auth_error(error: anyhow::Error, username: &str) -> Result<(), KindlyError> {
error!(
target: "security",
username = %username,
error = %error,
"Authentication failed"
);
Err(KindlyError::AuthError {
reason: "Authentication failed".to_string(), })
}
pub fn handle_threat(threat: &crate::scanner::Threat, input: &str) -> Result<(), KindlyError> {
error!(
target: "security.threats",
threat_type = ?threat.threat_type,
severity = ?threat.severity,
input_hash = %sha256_hash(input), "Threat detected"
);
Err(KindlyError::ThreatDetected {
threat_type: "policy violation".to_string(), location: "request".to_string(), })
}
pub fn handle_resource_limit(resource: &str, client_id: &str) -> Result<(), KindlyError> {
warn!(
target: "security.resources",
resource = %resource,
client_id = %client_id,
"Resource limit exceeded"
);
Err(KindlyError::ResourceError {
resource: "request".to_string(), limit: "quota exceeded".to_string(), })
}
pub fn handle_timeout(timeout_secs: u64) -> Result<(), KindlyError> {
warn!(
target: "security.timeout",
timeout_secs = timeout_secs,
"Operation timed out"
);
use rand::Rng;
let jitter = rand::thread_rng().gen_range(0..5);
Err(KindlyError::TimeoutError(timeout_secs + jitter))
}
fn sha256_hash(data: &str) -> String {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(data.as_bytes());
format!("{:x}", hasher.finalize())
}
pub fn constant_time_compare(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut result = 0u8;
for (x, y) in a.iter().zip(b.iter()) {
result |= x ^ y;
}
result == 0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_context_formatting() {
let error = KindlyError::ValidationError("Invalid path".to_string());
let context =
ErrorContext::new(error, RecoveryStrategy::FailFast, "Use absolute paths only");
let msg = context.user_message();
assert!(msg.contains("Invalid path"));
assert!(msg.contains("Use absolute paths only"));
}
#[tokio::test]
async fn test_retry_with_backoff() {
let mut attempts = 0;
let result = recovery::retry_with_backoff(
|| {
attempts += 1;
if attempts < 3 {
Err(io::Error::other("temp error"))
} else {
Ok("success")
}
},
5,
10,
)
.await;
assert!(result.is_ok());
assert_eq!(attempts, 3);
}
#[tokio::test]
async fn test_timeout() {
use std::time::Duration;
let result = recovery::with_timeout(
async {
tokio::time::sleep(Duration::from_secs(5)).await;
Ok("should timeout")
},
1,
)
.await;
assert!(result.is_err());
assert!(matches!(
result.unwrap_err().downcast::<KindlyError>().unwrap(),
KindlyError::TimeoutError(_) ));
}
#[test]
fn test_security_error_severity() {
assert_eq!(
KindlyError::ThreatDetected {
threat_type: "sql_injection".to_string(),
location: "input".to_string()
}
.severity(),
ErrorSeverity::Critical
);
assert_eq!(
KindlyError::AuthError {
reason: "invalid_token".to_string()
}
.severity(),
ErrorSeverity::Critical
);
assert_eq!(
KindlyError::Unauthorized {
action: "read_secrets".to_string()
}
.severity(),
ErrorSeverity::Critical
);
assert_eq!(
KindlyError::ResourceError {
resource: "memory".to_string(),
limit: "1GB".to_string()
}
.severity(),
ErrorSeverity::High
);
assert_eq!(
KindlyError::TimeoutError(30).severity(),
ErrorSeverity::High
);
}
#[test]
fn test_constant_time_compare() {
use security_patterns::constant_time_compare;
assert!(constant_time_compare(b"secret123", b"secret123"));
assert!(!constant_time_compare(b"secret123", b"secret124"));
assert!(!constant_time_compare(b"short", b"longer_string"));
assert!(constant_time_compare(b"", b""));
}
#[test]
fn test_error_to_protocol_code() {
assert_eq!(
KindlyError::AuthError {
reason: "test".to_string()
}
.to_protocol_code(),
-32001
);
assert_eq!(
KindlyError::Unauthorized {
action: "test".to_string()
}
.to_protocol_code(),
-32001
);
assert_eq!(
KindlyError::ThreatDetected {
threat_type: "test".to_string(),
location: "test".to_string()
}
.to_protocol_code(),
-32004
);
assert_eq!(KindlyError::TimeoutError(30).to_protocol_code(), -32002);
assert_eq!(
KindlyError::ResourceError {
resource: "test".to_string(),
limit: "test".to_string()
}
.to_protocol_code(),
-32003
);
}
#[test]
fn test_security_error_messages() {
let auth_err = KindlyError::AuthError {
reason: "user_not_found_in_database".to_string(),
};
let user_msg = auth_err.user_message();
assert!(!user_msg.contains("database"));
assert!(!user_msg.contains("not_found"));
assert_eq!(
user_msg,
"Authentication failed. Please check your credentials."
);
let threat_err = KindlyError::ThreatDetected {
threat_type: "sql_injection_union_select".to_string(),
location: "parameter_user_id".to_string(),
};
let user_msg = threat_err.user_message();
assert!(!user_msg.contains("sql"));
assert!(!user_msg.contains("injection"));
assert!(!user_msg.contains("parameter"));
assert!(!user_msg.contains("union"));
assert!(!user_msg.contains("user_id"));
assert_eq!(user_msg, "Security threat detected: policy violation");
let unauth_err = KindlyError::Unauthorized {
action: "delete_all_users".to_string(),
};
let user_msg = unauth_err.user_message();
assert!(!user_msg.contains("delete"));
assert!(!user_msg.contains("users"));
assert_eq!(user_msg, "Unauthorized access");
let resource_err = KindlyError::ResourceError {
resource: "memory_heap".to_string(),
limit: "2GB".to_string(),
};
let user_msg = resource_err.user_message();
assert!(!user_msg.contains("memory"));
assert!(!user_msg.contains("heap"));
assert!(!user_msg.contains("2GB"));
assert_eq!(user_msg, "Resource limit exceeded");
let timeout_err = KindlyError::TimeoutError(30);
let user_msg = timeout_err.user_message();
assert!(!user_msg.contains("30"));
assert_eq!(user_msg, "Operation timed out");
}
}