1use anyhow::Result;
28use std::io;
29use thiserror::Error;
30
31pub type KindlyResult<T> = Result<T, KindlyError>;
33
34pub trait ResultExt<T> {
36 fn kindly(self) -> KindlyResult<T>;
38}
39
40impl<T, E> ResultExt<T> for Result<T, E>
41where
42 E: Into<anyhow::Error>,
43{
44 fn kindly(self) -> KindlyResult<T> {
45 self.map_err(|e| KindlyError::ConfigError(e.into().to_string()))
46 }
47}
48
49#[derive(Error, Debug)]
51pub enum KindlyError {
52 #[error("Display rendering failed: {0}")]
54 DisplayError(String),
55
56 #[error("Terminal not available")]
57 TerminalError,
58
59 #[error("Command validation failed: {0}")]
67 ValidationError(String),
68
69 #[error("Invalid input: {reason}")]
76 InvalidInput { reason: String },
77
78 #[error("Invalid configuration: {field}: {reason}")]
79 InvalidConfig { field: String, reason: String },
80
81 #[error("File operation failed: {0}")]
83 FileError(#[from] io::Error),
84
85 #[error("Path not found: {path}")]
86 PathNotFound { path: String },
87
88 #[error("JSON serialization failed: {0}")]
90 SerializationError(#[from] serde_json::Error),
91
92 #[error("Format error: expected {expected}, got {actual}")]
93 FormatError { expected: String, actual: String },
94
95 #[error("Scanner initialization failed: {0}")]
97 ScannerError(String),
98
99 #[error("Threat detected: {threat_type} at {location}")]
129 ThreatDetected {
130 threat_type: String,
131 location: String,
132 },
133
134 #[error("Resource limit exceeded: {resource}: {limit}")]
151 ResourceError { resource: String, limit: String },
152
153 #[error("Operation timed out after {0} seconds")]
159 TimeoutError(u64),
160
161 #[error("Network error: {0}")]
163 NetworkError(String),
164
165 #[error("Connection failed to {endpoint}: {reason}")]
166 ConnectionError { endpoint: String, reason: String },
167
168 #[error("Authentication failed: {reason}")]
197 AuthError { reason: String },
198
199 #[error("Unauthorized: {action}")]
215 Unauthorized { action: String },
216
217 #[error("Protocol error: {code}: {message}")]
219 ProtocolError { code: i32, message: String },
220
221 #[error("Method not found: {method}")]
222 MethodNotFound { method: String },
223
224 #[error("Configuration error: {0}")]
226 ConfigError(String),
227
228 #[error("Internal error: {0}")]
230 Internal(String),
231}
232
233impl KindlyError {
234 pub const fn severity(&self) -> ErrorSeverity {
236 match self {
237 Self::ThreatDetected { .. } => ErrorSeverity::Critical,
239 Self::AuthError { .. } => ErrorSeverity::Critical,
240 Self::Unauthorized { .. } => ErrorSeverity::Critical,
241
242 Self::ScannerError(_) => ErrorSeverity::High,
244 Self::ResourceError { .. } => ErrorSeverity::High,
245 Self::TimeoutError(_) => ErrorSeverity::High,
246 Self::Internal(_) => ErrorSeverity::High,
247
248 Self::NetworkError(_) => ErrorSeverity::Medium,
250 Self::ConnectionError { .. } => ErrorSeverity::Medium,
251 Self::ProtocolError { .. } => ErrorSeverity::Medium,
252 Self::ConfigError(_) => ErrorSeverity::Medium,
253
254 Self::DisplayError(_) => ErrorSeverity::Low,
256 Self::TerminalError => ErrorSeverity::Low,
257 Self::ValidationError(_) => ErrorSeverity::Low,
258 Self::InvalidInput { .. } => ErrorSeverity::Low,
259 Self::InvalidConfig { .. } => ErrorSeverity::Low,
260 Self::FileError(_) => ErrorSeverity::Low,
261 Self::PathNotFound { .. } => ErrorSeverity::Low,
262 Self::SerializationError(_) => ErrorSeverity::Low,
263 Self::FormatError { .. } => ErrorSeverity::Low,
264 Self::MethodNotFound { .. } => ErrorSeverity::Low,
265 }
266 }
267
268 pub const fn is_retryable(&self) -> bool {
270 matches!(
271 self,
272 Self::NetworkError(_)
273 | Self::ConnectionError { .. }
274 | Self::TimeoutError(_)
275 | Self::ResourceError { .. }
276 )
277 }
278
279 pub fn user_message(&self) -> String {
281 match self {
282 Self::ThreatDetected { .. } => {
283 "Security threat detected: policy violation".to_string()
285 },
286 Self::AuthError { .. } => {
287 "Authentication failed. Please check your credentials.".to_string()
289 },
290 Self::Unauthorized { .. } => {
291 "Unauthorized access".to_string()
293 },
294 Self::TimeoutError(_) => {
295 "Operation timed out".to_string()
297 },
298 Self::ResourceError { .. } => {
299 "Resource limit exceeded".to_string()
301 },
302 _ => self.to_string(),
303 }
304 }
305
306 pub const fn to_protocol_code(&self) -> i32 {
308 match self {
309 Self::ProtocolError { code, .. } => *code,
310 Self::MethodNotFound { .. } => -32601,
311 Self::InvalidInput { .. } => -32602,
312 Self::AuthError { .. } | Self::Unauthorized { .. } => -32001,
313 Self::TimeoutError(_) => -32002,
314 Self::ResourceError { .. } => -32003,
315 Self::ThreatDetected { .. } => -32004,
316 _ => -32603, }
318 }
319}
320
321#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
323pub enum ErrorSeverity {
324 Low,
325 Medium,
326 High,
327 Critical,
328}
329
330#[derive(Debug, Clone)]
332pub enum RecoveryStrategy {
333 RetryWithBackoff {
335 max_attempts: u32,
336 base_delay_ms: u64,
337 },
338
339 Fallback,
341
342 LogAndContinue,
344
345 FailFast,
347}
348
349pub struct ErrorContext {
351 pub error: KindlyError,
352 pub strategy: RecoveryStrategy,
353 pub user_hint: String,
354}
355
356impl ErrorContext {
357 pub fn new(error: KindlyError, strategy: RecoveryStrategy, hint: &str) -> Self {
359 Self {
360 error,
361 strategy,
362 user_hint: hint.to_string(),
363 }
364 }
365
366 pub fn user_message(&self) -> String {
368 format!("{}\n\nHint: {}", self.error, self.user_hint)
369 }
370}
371
372pub mod recovery {
374 use super::{KindlyError, Result};
375 use std::time::Duration;
376 use tokio::time::sleep;
377
378 pub async fn retry_with_backoff<F, T, E>(
380 mut operation: F,
381 max_attempts: u32,
382 base_delay_ms: u64,
383 ) -> Result<T>
384 where
385 F: FnMut() -> Result<T, E>,
386 E: std::error::Error + Send + Sync + 'static,
387 {
388 let mut attempt = 0;
389 let mut delay = base_delay_ms;
390
391 loop {
392 attempt += 1;
393
394 match operation() {
395 Ok(result) => return Ok(result),
396 Err(e) if attempt >= max_attempts => {
397 return Err(anyhow::anyhow!(
398 "Operation failed after {} attempts: {}",
399 max_attempts,
400 e
401 ));
402 },
403 Err(_) => {
404 sleep(Duration::from_millis(delay)).await;
405 delay = (delay * 2).min(30_000); },
407 }
408 }
409 }
410
411 pub async fn with_timeout<F, T>(operation: F, timeout_secs: u64) -> anyhow::Result<T>
413 where
414 F: std::future::Future<Output = anyhow::Result<T>>,
415 {
416 match tokio::time::timeout(Duration::from_secs(timeout_secs), operation).await {
417 Ok(result) => result,
418 Err(_) => Err(KindlyError::TimeoutError(timeout_secs).into()),
419 }
420 }
421}
422
423pub mod handlers {
425 use super::{io, ErrorContext, KindlyError, RecoveryStrategy};
426
427 pub fn handle_display_error(error: anyhow::Error) -> String {
429 eprintln!("Display error: {error}");
430
431 format!(
433 "KindlyGuard | Status: Error | Message: Display unavailable\n\
434 Error: {error}\n\
435 Try running with --format minimal or --no-color"
436 )
437 }
438
439 pub fn handle_file_error(path: &str, error: io::Error) -> ErrorContext {
441 let hint = match error.kind() {
442 io::ErrorKind::NotFound => {
443 format!("File '{path}' not found. Check the path and try again.")
444 },
445 io::ErrorKind::PermissionDenied => {
446 format!("Permission denied for '{path}'. Check file permissions.")
447 },
448 io::ErrorKind::InvalidData => {
449 "File contains invalid data. It may be corrupted.".to_string()
450 },
451 _ => {
452 format!("Failed to access '{path}': {error}")
453 },
454 };
455
456 ErrorContext::new(
457 KindlyError::FileError(error),
458 RecoveryStrategy::FailFast,
459 &hint,
460 )
461 }
462
463 pub fn handle_validation_error(field: &str, value: &str, reason: &str) -> ErrorContext {
465 let hint = match field {
466 "path" => "Use absolute paths without '..' and ensure the file exists.".to_string(),
467 "port" => "Use a port number between 1024 and 65535.".to_string(),
468 "feature" => "Valid features: unicode, injection, path, advanced.".to_string(),
469 _ => {
470 format!("Check the {field} value and try again.")
471 },
472 };
473
474 ErrorContext::new(
475 KindlyError::ValidationError(format!("Invalid {field}: '{value}' - {reason}")),
476 RecoveryStrategy::FailFast,
477 &hint,
478 )
479 }
480}
481
482pub mod degradation {
484
485 use crate::shield::Shield;
486 use std::sync::Arc;
487
488 pub fn degrade_display_format(
490 shield: Arc<Shield>,
491 mut formats: Vec<crate::shield::universal_display::DisplayFormat>,
492 ) -> String {
493 use crate::shield::{UniversalDisplay, UniversalDisplayConfig};
494
495 while let Some(format) = formats.pop() {
497 let config = UniversalDisplayConfig {
498 color: false, detailed: false,
500 format,
501 status_file: None, };
503
504 let display = UniversalDisplay::new(shield.clone(), config);
505
506 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| display.render())) {
507 Ok(output) if !output.is_empty() => return output,
508 _ => continue,
509 }
510 }
511
512 "KindlyGuard | Status: Unknown | Error: Display system failure".to_string()
514 }
515}
516
517pub mod security_patterns {
519 use super::*;
520 use tracing::{error, warn};
521
522 pub fn handle_auth_error(error: anyhow::Error, username: &str) -> Result<(), KindlyError> {
535 error!(
537 target: "security",
538 username = %username,
539 error = %error,
540 "Authentication failed"
541 );
542
543 Err(KindlyError::AuthError {
548 reason: "Authentication failed".to_string(), })
550 }
551
552 pub fn handle_threat(threat: &crate::scanner::Threat, input: &str) -> Result<(), KindlyError> {
566 error!(
568 target: "security.threats",
569 threat_type = ?threat.threat_type,
570 severity = ?threat.severity,
571 input_hash = %sha256_hash(input), "Threat detected"
573 );
574
575 Err(KindlyError::ThreatDetected {
577 threat_type: "policy violation".to_string(), location: "request".to_string(), })
580 }
581
582 pub fn handle_resource_limit(resource: &str, client_id: &str) -> Result<(), KindlyError> {
595 warn!(
596 target: "security.resources",
597 resource = %resource,
598 client_id = %client_id,
599 "Resource limit exceeded"
600 );
601
602 Err(KindlyError::ResourceError {
606 resource: "request".to_string(), limit: "quota exceeded".to_string(), })
609 }
610
611 pub fn handle_timeout(timeout_secs: u64) -> Result<(), KindlyError> {
625 warn!(
626 target: "security.timeout",
627 timeout_secs = timeout_secs,
628 "Operation timed out"
629 );
630
631 use rand::Rng;
633 let jitter = rand::thread_rng().gen_range(0..5);
634
635 Err(KindlyError::TimeoutError(timeout_secs + jitter))
636 }
637
638 fn sha256_hash(data: &str) -> String {
640 use sha2::{Digest, Sha256};
641 let mut hasher = Sha256::new();
642 hasher.update(data.as_bytes());
643 format!("{:x}", hasher.finalize())
644 }
645
646 pub fn constant_time_compare(a: &[u8], b: &[u8]) -> bool {
656 if a.len() != b.len() {
657 return false;
658 }
659
660 let mut result = 0u8;
661 for (x, y) in a.iter().zip(b.iter()) {
662 result |= x ^ y;
663 }
664
665 result == 0
666 }
667}
668
669#[cfg(test)]
670mod tests {
671 use super::*;
672
673 #[test]
674 fn test_error_context_formatting() {
675 let error = KindlyError::ValidationError("Invalid path".to_string());
676 let context =
677 ErrorContext::new(error, RecoveryStrategy::FailFast, "Use absolute paths only");
678
679 let msg = context.user_message();
680 assert!(msg.contains("Invalid path"));
681 assert!(msg.contains("Use absolute paths only"));
682 }
683
684 #[tokio::test]
685 async fn test_retry_with_backoff() {
686 let mut attempts = 0;
687 let result = recovery::retry_with_backoff(
688 || {
689 attempts += 1;
690 if attempts < 3 {
691 Err(io::Error::other("temp error"))
692 } else {
693 Ok("success")
694 }
695 },
696 5,
697 10,
698 )
699 .await;
700
701 assert!(result.is_ok());
702 assert_eq!(attempts, 3);
703 }
704
705 #[tokio::test]
706 async fn test_timeout() {
707 use std::time::Duration;
708
709 let result = recovery::with_timeout(
710 async {
711 tokio::time::sleep(Duration::from_secs(5)).await;
712 Ok("should timeout")
713 },
714 1,
715 )
716 .await;
717
718 assert!(result.is_err());
719 assert!(matches!(
720 result.unwrap_err().downcast::<KindlyError>().unwrap(),
721 KindlyError::TimeoutError(_) ));
723 }
724
725 #[test]
726 fn test_security_error_severity() {
727 assert_eq!(
729 KindlyError::ThreatDetected {
730 threat_type: "sql_injection".to_string(),
731 location: "input".to_string()
732 }
733 .severity(),
734 ErrorSeverity::Critical
735 );
736
737 assert_eq!(
738 KindlyError::AuthError {
739 reason: "invalid_token".to_string()
740 }
741 .severity(),
742 ErrorSeverity::Critical
743 );
744
745 assert_eq!(
746 KindlyError::Unauthorized {
747 action: "read_secrets".to_string()
748 }
749 .severity(),
750 ErrorSeverity::Critical
751 );
752
753 assert_eq!(
755 KindlyError::ResourceError {
756 resource: "memory".to_string(),
757 limit: "1GB".to_string()
758 }
759 .severity(),
760 ErrorSeverity::High
761 );
762
763 assert_eq!(
764 KindlyError::TimeoutError(30).severity(),
765 ErrorSeverity::High
766 );
767 }
768
769 #[test]
770 fn test_constant_time_compare() {
771 use security_patterns::constant_time_compare;
772
773 assert!(constant_time_compare(b"secret123", b"secret123"));
775
776 assert!(!constant_time_compare(b"secret123", b"secret124"));
778
779 assert!(!constant_time_compare(b"short", b"longer_string"));
781
782 assert!(constant_time_compare(b"", b""));
784 }
785
786 #[test]
787 fn test_error_to_protocol_code() {
788 assert_eq!(
790 KindlyError::AuthError {
791 reason: "test".to_string()
792 }
793 .to_protocol_code(),
794 -32001
795 );
796
797 assert_eq!(
798 KindlyError::Unauthorized {
799 action: "test".to_string()
800 }
801 .to_protocol_code(),
802 -32001
803 );
804
805 assert_eq!(
806 KindlyError::ThreatDetected {
807 threat_type: "test".to_string(),
808 location: "test".to_string()
809 }
810 .to_protocol_code(),
811 -32004
812 );
813
814 assert_eq!(KindlyError::TimeoutError(30).to_protocol_code(), -32002);
815
816 assert_eq!(
817 KindlyError::ResourceError {
818 resource: "test".to_string(),
819 limit: "test".to_string()
820 }
821 .to_protocol_code(),
822 -32003
823 );
824 }
825
826 #[test]
827 fn test_security_error_messages() {
828 let auth_err = KindlyError::AuthError {
830 reason: "user_not_found_in_database".to_string(),
831 };
832 let user_msg = auth_err.user_message();
833 assert!(!user_msg.contains("database"));
834 assert!(!user_msg.contains("not_found"));
835 assert_eq!(
836 user_msg,
837 "Authentication failed. Please check your credentials."
838 );
839
840 let threat_err = KindlyError::ThreatDetected {
842 threat_type: "sql_injection_union_select".to_string(),
843 location: "parameter_user_id".to_string(),
844 };
845 let user_msg = threat_err.user_message();
846 assert!(!user_msg.contains("sql"));
847 assert!(!user_msg.contains("injection"));
848 assert!(!user_msg.contains("parameter"));
849 assert!(!user_msg.contains("union"));
850 assert!(!user_msg.contains("user_id"));
851 assert_eq!(user_msg, "Security threat detected: policy violation");
852
853 let unauth_err = KindlyError::Unauthorized {
855 action: "delete_all_users".to_string(),
856 };
857 let user_msg = unauth_err.user_message();
858 assert!(!user_msg.contains("delete"));
859 assert!(!user_msg.contains("users"));
860 assert_eq!(user_msg, "Unauthorized access");
861
862 let resource_err = KindlyError::ResourceError {
864 resource: "memory_heap".to_string(),
865 limit: "2GB".to_string(),
866 };
867 let user_msg = resource_err.user_message();
868 assert!(!user_msg.contains("memory"));
869 assert!(!user_msg.contains("heap"));
870 assert!(!user_msg.contains("2GB"));
871 assert_eq!(user_msg, "Resource limit exceeded");
872
873 let timeout_err = KindlyError::TimeoutError(30);
875 let user_msg = timeout_err.user_message();
876 assert!(!user_msg.contains("30"));
877 assert_eq!(user_msg, "Operation timed out");
878 }
879}