//! Typed failures and coarse process status allocation. use std::collections::BTreeMap; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_json::Value; use thiserror::Error; /// Stable machine-readable error taxonomy available in the foundation. #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum ErrorCode { /// Command-line syntax or validation failed. InvalidArguments, /// The caller's Sample path does not exist. SampleNotFound, /// The caller's Sample cannot be read. SampleUnreadable, /// The opened Sample is not a regular file. InvalidSampleType, /// The Sample exceeded the selected byte ceiling. SampleTooLarge, /// Source metadata changed while snapshotting. SampleChanged, /// The Analysis Store lacks the required free-space reserve. InsufficientStoreSpace, /// Ghidra could not recognize a Target Specification. UnsupportedTarget, /// Ghidra found multiple Target Specification candidates. AmbiguousTarget, /// Ghidra is not installed. GhidraMissing, /// The installed Ghidra version is incompatible. GhidraIncompatible, /// Java is not installed. JavaMissing, /// The installed Java version is incompatible. JavaIncompatible, /// Rust/Java request or response validation failed. ProtocolViolation, /// The child exceeded a native or watchdog timeout. Timeout, /// Native auto-analysis timed out. AnalysisTimeout, /// Ghidra auto-analysis failed. AnalysisFailed, /// Native Function decompilation timed out. DecompileTimeout, /// Ghidra Function decompilation failed. DecompilationFailed, /// The caller interrupted the operation. Interrupted, /// The adapter could not serialize a bounded response. ResultTooLarge, /// The success descriptor cannot fit the requested inline budget. InlineBudgetTooSmall, /// Store-wide cleanup lacks explicit confirmation. ConfirmationRequired, /// Another process holds the selected Analysis lock. AnalysisBusy, /// Stored Analysis validation failed and recovery did not succeed. CorruptAnalysis, /// A cleanup transaction requires a recoverable retry. CleanupIncomplete, /// The resolved Analysis Store path is invalid. InvalidStorePath, /// A Function Selector resolved to no Function. FunctionNotFound, /// A Function Selector resolved to multiple Functions. FunctionSelectorAmbiguous, /// An Address is not a Function entry. FunctionEntryRequired, /// Ghidra does not contain the requested address space. AddressSpaceNotFound, /// Installation diagnosis found an unusable component. DoctorFailed, /// A general runtime or integration failure. Internal, } impl ErrorCode { /// Authoritative coarse status mapping from ADR 0002. #[must_use] pub const fn exit_status(self) -> ExitStatus { match self { Self::InvalidArguments => ExitStatus::InvalidInvocation, Self::Timeout | Self::AnalysisTimeout | Self::DecompileTimeout => ExitStatus::Timeout, Self::Interrupted => ExitStatus::Interrupted, _ => ExitStatus::RuntimeFailure, } } } /// Stable public error body. #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct ErrorBody { /// Stable machine identifier. pub code: ErrorCode, /// Concise context that is not a control-flow interface. pub message: String, /// Whether the same logical request might later succeed unchanged. pub retryable: bool, /// Code-specific structured context, always an object. pub details: BTreeMap, } /// Versioned failure document. #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)] pub struct ErrorEnvelope { /// Public schema major version. pub schema_version: u32, /// Always `error`. pub kind: ErrorKind, /// Typed failure body. pub error: ErrorBody, } /// Error envelope discriminator. #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum ErrorKind { /// A failed invocation. Error, } /// Internal error paired with its public document and status class. #[derive(Clone, Debug, Error)] #[error("{body_message}")] pub struct AppError { body_message: String, envelope: ErrorEnvelope, status: ExitStatus, } impl AppError { /// Constructs a typed failure with empty structured details. #[must_use] pub fn new(code: ErrorCode, message: impl Into, retryable: bool) -> Self { let message = message.into(); let status = code.exit_status(); Self { body_message: message.clone(), envelope: ErrorEnvelope { schema_version: 1, kind: ErrorKind::Error, error: ErrorBody { code, message, retryable, details: BTreeMap::new(), }, }, status, } } /// Constructs an invalid-invocation failure from clap context. #[must_use] pub fn invalid_arguments(message: impl Into) -> Self { Self::new(ErrorCode::InvalidArguments, message, false) } /// Adds one typed detail field. #[must_use] pub fn with_detail(mut self, key: impl Into, value: Value) -> Self { self.envelope.error.details.insert(key.into(), value); self } /// Returns the public error document. #[must_use] pub const fn envelope(&self) -> &ErrorEnvelope { &self.envelope } /// Returns the coarse process status. #[must_use] pub const fn status(&self) -> ExitStatus { self.status } } /// Accepted coarse process statuses from ADR 0002. #[derive(Clone, Copy, Debug, Eq, PartialEq)] #[repr(i32)] pub enum ExitStatus { /// Successful operation. Success = 0, /// Runtime failure. RuntimeFailure = 1, /// Invalid invocation or arguments. InvalidInvocation = 2, /// Native phase or child watchdog timeout. Timeout = 124, /// User interruption. Interrupted = 130, } impl ExitStatus { /// Integer process exit code. #[must_use] pub const fn code(self) -> i32 { self as i32 } } #[cfg(test)] mod tests { use super::{ErrorCode, ExitStatus}; #[test] fn detailed_errors_map_to_only_accepted_coarse_statuses() { assert_eq!( ErrorCode::InvalidArguments.exit_status(), ExitStatus::InvalidInvocation ); assert_eq!( ErrorCode::AnalysisTimeout.exit_status(), ExitStatus::Timeout ); assert_eq!( ErrorCode::Interrupted.exit_status(), ExitStatus::Interrupted ); assert_eq!( ErrorCode::ProtocolViolation.exit_status(), ExitStatus::RuntimeFailure ); } }