cc-audit 3.2.14

Security auditor for Claude Code skills, hooks, and MCP servers
Documentation
//! Error types for cc-audit.
//!
//! This module provides a unified error handling system with:
//! - `CcAuditError`: The new unified error type with full context preservation
//! - `AuditError`: Legacy error type for backwards compatibility
//! - Context types for better error messages

mod audit;
mod context;

pub use audit::CcAuditError;
pub use context::{IoOperation, ParseFormat};

use crate::hooks::HookError;
use crate::malware_db::MalwareDbError;
use thiserror::Error;

/// Legacy error type for backwards compatibility.
///
/// New code should prefer using `CcAuditError` for better error context.
#[derive(Error, Debug)]
pub enum AuditError {
    #[error("File not found: {0}")]
    FileNotFound(String),

    #[error("Failed to read file: {path}")]
    ReadError {
        path: String,
        #[source]
        source: std::io::Error,
    },

    #[error("Failed to parse YAML frontmatter: {path}")]
    YamlParseError {
        path: String,
        #[source]
        source: serde_yml::Error,
    },

    #[error("Invalid SKILL.md format: {0}")]
    InvalidSkillFormat(String),

    #[error("Regex compilation error: {0}")]
    RegexError(#[from] regex::Error),

    #[error("Path is not a directory: {0}")]
    NotADirectory(String),

    #[error("JSON serialization error: {0}")]
    JsonError(#[from] serde_json::Error),

    #[error("Failed to parse file: {path} - {message}")]
    ParseError { path: String, message: String },

    #[error("Hook operation failed: {0}")]
    Hook(#[from] HookError),

    #[error("Malware database error: {0}")]
    MalwareDb(#[from] MalwareDbError),

    #[error("File watch error: {0}")]
    Watch(#[from] notify::Error),

    #[error("Configuration error: {0}")]
    Config(String),
}

/// Result type alias for operations using the legacy AuditError.
pub type Result<T> = std::result::Result<T, AuditError>;

/// Result type alias for operations using the new CcAuditError.
pub type CcResult<T> = std::result::Result<T, CcAuditError>;

/// Convert from CcAuditError to AuditError for backwards compatibility.
impl From<CcAuditError> for AuditError {
    fn from(err: CcAuditError) -> Self {
        match err {
            CcAuditError::Io { path, source, .. } => AuditError::ReadError {
                path: path.display().to_string(),
                source,
            },
            CcAuditError::Parse { path, .. } => AuditError::ParseError {
                path: path.display().to_string(),
                message: "parse error".to_string(),
            },
            CcAuditError::FileNotFound(path) => {
                AuditError::FileNotFound(path.display().to_string())
            }
            CcAuditError::NotADirectory(path) => {
                AuditError::NotADirectory(path.display().to_string())
            }
            CcAuditError::NotAFile(path) => AuditError::NotADirectory(path.display().to_string()),
            CcAuditError::InvalidFormat { path, message } => AuditError::ParseError {
                path: path.display().to_string(),
                message,
            },
            CcAuditError::Regex(e) => AuditError::RegexError(e),
            CcAuditError::Hook(e) => AuditError::Hook(e),
            CcAuditError::MalwareDb(e) => AuditError::MalwareDb(e),
            CcAuditError::Watch(e) => AuditError::Watch(e),
            CcAuditError::Config(s) => AuditError::Config(s),
            CcAuditError::YamlParse { path, source } => AuditError::YamlParseError { path, source },
            CcAuditError::InvalidSkillFormat(s) => AuditError::InvalidSkillFormat(s),
            CcAuditError::Json(e) => AuditError::JsonError(e),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_error_display_file_not_found() {
        let err = AuditError::FileNotFound("/path/to/file".to_string());
        assert_eq!(err.to_string(), "File not found: /path/to/file");
    }

    #[test]
    fn test_error_display_read_error() {
        let err = AuditError::ReadError {
            path: "/path/to/file".to_string(),
            source: std::io::Error::new(std::io::ErrorKind::NotFound, "not found"),
        };
        assert_eq!(err.to_string(), "Failed to read file: /path/to/file");
    }

    #[test]
    fn test_error_display_invalid_skill_format() {
        let err = AuditError::InvalidSkillFormat("missing frontmatter".to_string());
        assert_eq!(
            err.to_string(),
            "Invalid SKILL.md format: missing frontmatter"
        );
    }

    #[test]
    fn test_error_display_not_a_directory() {
        let err = AuditError::NotADirectory("/path/to/file".to_string());
        assert_eq!(err.to_string(), "Path is not a directory: /path/to/file");
    }

    #[test]
    fn test_error_display_parse_error() {
        let err = AuditError::ParseError {
            path: "/path/to/file".to_string(),
            message: "invalid JSON".to_string(),
        };
        assert_eq!(
            err.to_string(),
            "Failed to parse file: /path/to/file - invalid JSON"
        );
    }

    #[test]
    fn test_error_from_hook_error() {
        let hook_error = HookError::NotAGitRepository;
        let err: AuditError = hook_error.into();
        assert!(err.to_string().contains("Hook operation failed"));
    }

    #[test]
    fn test_error_from_malware_db_error() {
        let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "not found");
        let malware_error = MalwareDbError::ReadFile(io_error);
        let err: AuditError = malware_error.into();
        assert!(err.to_string().contains("Malware database error"));
    }

    #[test]
    fn test_error_display_config() {
        let err = AuditError::Config("invalid value".to_string());
        assert_eq!(err.to_string(), "Configuration error: invalid value");
    }

    #[test]
    fn test_cc_audit_error_to_audit_error() {
        let cc_err = CcAuditError::FileNotFound(std::path::PathBuf::from("/test/path"));
        let audit_err: AuditError = cc_err.into();
        assert!(audit_err.to_string().contains("/test/path"));
    }

    #[test]
    fn test_cc_audit_error_io_to_audit_error() {
        let cc_err = CcAuditError::Io {
            path: std::path::PathBuf::from("/test/path"),
            operation: IoOperation::Read,
            source: std::io::Error::new(std::io::ErrorKind::NotFound, "not found"),
        };
        let audit_err: AuditError = cc_err.into();
        assert!(matches!(audit_err, AuditError::ReadError { .. }));
    }

    #[test]
    fn test_cc_audit_error_parse_to_audit_error() {
        let cc_err = CcAuditError::Parse {
            path: std::path::PathBuf::from("/test/file.json"),
            format: ParseFormat::Json,
            source: Box::new(std::io::Error::new(
                std::io::ErrorKind::InvalidData,
                "syntax error",
            )),
        };
        let audit_err: AuditError = cc_err.into();
        assert!(matches!(audit_err, AuditError::ParseError { .. }));
    }

    #[test]
    fn test_cc_audit_error_not_a_directory() {
        let cc_err = CcAuditError::NotADirectory(std::path::PathBuf::from("/test/file"));
        let audit_err: AuditError = cc_err.into();
        assert!(matches!(audit_err, AuditError::NotADirectory(_)));
    }

    #[test]
    fn test_cc_audit_error_not_a_file() {
        let cc_err = CcAuditError::NotAFile(std::path::PathBuf::from("/test/dir"));
        let audit_err: AuditError = cc_err.into();
        // NotAFile maps to NotADirectory for backwards compat
        assert!(matches!(audit_err, AuditError::NotADirectory(_)));
    }

    #[test]
    fn test_cc_audit_error_invalid_format() {
        let cc_err = CcAuditError::InvalidFormat {
            path: std::path::PathBuf::from("/test/file"),
            message: "invalid format".to_string(),
        };
        let audit_err: AuditError = cc_err.into();
        assert!(matches!(audit_err, AuditError::ParseError { .. }));
    }

    #[test]
    #[allow(clippy::invalid_regex)]
    fn test_cc_audit_error_regex() {
        let regex_err = regex::Regex::new(r"[invalid\[").unwrap_err();
        let cc_err = CcAuditError::Regex(regex_err);
        let audit_err: AuditError = cc_err.into();
        assert!(matches!(audit_err, AuditError::RegexError(_)));
    }

    #[test]
    fn test_cc_audit_error_config() {
        let cc_err = CcAuditError::Config("bad config".to_string());
        let audit_err: AuditError = cc_err.into();
        assert!(matches!(audit_err, AuditError::Config(_)));
    }

    #[test]
    fn test_cc_audit_error_invalid_skill_format() {
        let cc_err = CcAuditError::InvalidSkillFormat("missing frontmatter".to_string());
        let audit_err: AuditError = cc_err.into();
        assert!(matches!(audit_err, AuditError::InvalidSkillFormat(_)));
    }

    #[test]
    #[allow(clippy::invalid_regex)]
    fn test_error_from_regex_error() {
        let regex_err = regex::Regex::new(r"[invalid\[").unwrap_err();
        let err: AuditError = regex_err.into();
        assert!(err.to_string().contains("Regex compilation error"));
    }

    #[test]
    fn test_error_debug_trait() {
        let err = AuditError::FileNotFound("/test".to_string());
        let debug_str = format!("{:?}", err);
        assert!(debug_str.contains("FileNotFound"));
    }
}