soar_core/
error.rs

1//! Error types for soar-core.
2
3#![expect(unused_assignments)]
4
5use std::error::Error;
6
7use miette::Diagnostic;
8use soar_config::error::ConfigError;
9use soar_utils::error::{FileSystemError, HashError, PathError};
10use thiserror::Error;
11
12/// Core error type for soar package manager operations.
13#[derive(Error, Diagnostic, Debug)]
14pub enum SoarError {
15    #[error(transparent)]
16    #[diagnostic(transparent)]
17    Config(#[from] ConfigError),
18
19    #[error(transparent)]
20    #[diagnostic(code(soar::system), help("Check system permissions and resources"))]
21    Errno(#[from] nix::errno::Errno),
22
23    #[error(transparent)]
24    #[diagnostic(
25        code(soar::env_var),
26        help("Set the required environment variable before running")
27    )]
28    VarError(#[from] std::env::VarError),
29
30    #[error(transparent)]
31    #[diagnostic(transparent)]
32    FileSystemError(#[from] FileSystemError),
33
34    #[error(transparent)]
35    #[diagnostic(transparent)]
36    HashError(#[from] HashError),
37
38    #[error(transparent)]
39    #[diagnostic(transparent)]
40    PathError(#[from] PathError),
41
42    #[error("Error while {action}")]
43    #[diagnostic(code(soar::io), help("Check file permissions and disk space"))]
44    IoError {
45        action: String,
46        #[source]
47        source: std::io::Error,
48    },
49
50    #[error(transparent)]
51    #[diagnostic(code(soar::time))]
52    SystemTimeError(#[from] std::time::SystemTimeError),
53
54    #[error(transparent)]
55    #[diagnostic(code(soar::toml), help("Check your configuration syntax"))]
56    TomlError(#[from] toml::ser::Error),
57
58    #[error("Database operation failed: {0}")]
59    #[diagnostic(
60        code(soar::database),
61        help("Try running 'soar sync' to refresh the database")
62    )]
63    DatabaseError(String),
64
65    #[error(transparent)]
66    #[diagnostic(
67        code(soar::network),
68        help("Check your internet connection and try again")
69    )]
70    UreqError(#[from] ureq::Error),
71
72    #[error(transparent)]
73    #[diagnostic(transparent)]
74    DownloadError(#[from] soar_dl::error::DownloadError),
75
76    #[error(transparent)]
77    #[diagnostic(transparent)]
78    PackageError(#[from] soar_package::PackageError),
79
80    #[error("Package integration failed: {0}")]
81    #[diagnostic(
82        code(soar::integration),
83        help("Check if the package format is supported")
84    )]
85    PackageIntegrationFailed(String),
86
87    #[error("Package '{0}' not found")]
88    #[diagnostic(
89        code(soar::package_not_found),
90        help("Run 'soar sync' to update package list, or check the package name")
91    )]
92    PackageNotFound(String),
93
94    #[error("Failed to fetch from remote source: {0}")]
95    #[diagnostic(
96        code(soar::fetch),
97        help("Check your internet connection and repository URL")
98    )]
99    FailedToFetchRemote(String),
100
101    #[error("Invalid path specified")]
102    #[diagnostic(
103        code(soar::invalid_path),
104        help("Provide a valid file or directory path")
105    )]
106    InvalidPath,
107
108    #[error("Thread lock poison error")]
109    #[diagnostic(
110        code(soar::poison),
111        help("This is an internal error, please report it")
112    )]
113    PoisonError,
114
115    #[error("Invalid checksum detected")]
116    #[diagnostic(
117        code(soar::checksum),
118        help("The downloaded file may be corrupted. Try downloading again.")
119    )]
120    InvalidChecksum,
121
122    #[error("Invalid package query: {0}")]
123    #[diagnostic(
124        code(soar::invalid_query),
125        help("Use format: name#pkg_id@version:repo (e.g., 'curl', 'curl#bin', 'curl@8.0.0')")
126    )]
127    InvalidPackageQuery(String),
128
129    #[error("{0}")]
130    #[diagnostic(code(soar::error))]
131    Custom(String),
132
133    #[error("{0}")]
134    #[diagnostic(code(soar::warning), severity(warning))]
135    Warning(String),
136
137    #[error(transparent)]
138    #[diagnostic(code(soar::regex), help("Check your regex pattern syntax"))]
139    RegexError(#[from] regex::Error),
140
141    #[error("Landlock is not supported on this system")]
142    #[diagnostic(
143        code(soar::sandbox::not_supported),
144        help("Landlock requires Linux kernel 5.13+. Hooks will run without sandboxing.")
145    )]
146    SandboxNotSupported,
147
148    #[error("Failed to create Landlock ruleset: {0}")]
149    #[diagnostic(
150        code(soar::sandbox::ruleset),
151        help("This may indicate a kernel or permission issue")
152    )]
153    SandboxRulesetCreation(String),
154
155    #[error("Failed to add sandbox rule for path '{path}': {reason}")]
156    #[diagnostic(
157        code(soar::sandbox::path_rule),
158        help("Check if the path exists and is accessible")
159    )]
160    SandboxPathRule { path: String, reason: String },
161
162    #[error("Failed to add sandbox network rule for port {port}: {reason}")]
163    #[diagnostic(
164        code(soar::sandbox::network_rule),
165        help("Network restrictions require Landlock V4+ (kernel 6.7+)")
166    )]
167    SandboxNetworkRule { port: u16, reason: String },
168
169    #[error("Failed to enforce Landlock sandbox: {0}")]
170    #[diagnostic(
171        code(soar::sandbox::enforcement),
172        help("This may indicate a kernel or permission issue")
173    )]
174    SandboxEnforcement(String),
175
176    #[error("Sandboxed command execution failed: {0}")]
177    #[diagnostic(
178        code(soar::sandbox::execution),
179        help("Check the command and sandbox configuration")
180    )]
181    SandboxExecution(String),
182}
183
184impl SoarError {
185    pub fn message(&self) -> String {
186        self.to_string()
187    }
188
189    pub fn root_cause(&self) -> String {
190        match self {
191            Self::UreqError(e) => {
192                format!(
193                    "Root cause: {}",
194                    e.source()
195                        .map_or_else(|| e.to_string(), |source| source.to_string())
196                )
197            }
198            Self::Config(err) => err.to_string(),
199            _ => self.to_string(),
200        }
201    }
202}
203
204impl<T> From<std::sync::PoisonError<T>> for SoarError {
205    fn from(_: std::sync::PoisonError<T>) -> Self {
206        Self::PoisonError
207    }
208}
209
210/// Trait for adding context to IO errors.
211pub trait ErrorContext<T> {
212    fn with_context<C>(self, context: C) -> std::result::Result<T, SoarError>
213    where
214        C: FnOnce() -> String;
215}
216
217impl<T> ErrorContext<T> for std::io::Result<T> {
218    fn with_context<C>(self, context: C) -> std::result::Result<T, SoarError>
219    where
220        C: FnOnce() -> String,
221    {
222        self.map_err(|err| {
223            SoarError::IoError {
224                action: context(),
225                source: err,
226            }
227        })
228    }
229}