1use std::path::PathBuf;
7use thiserror::Error;
8
9#[derive(Debug, Error)]
11pub enum EngineError {
12 #[error("Network error: {message}")]
14 Network {
15 kind: NetworkErrorKind,
16 message: String,
17 retryable: bool,
18 },
19
20 #[error("Storage error at {path:?}: {message}")]
22 Storage {
23 kind: StorageErrorKind,
24 path: PathBuf,
25 message: String,
26 },
27
28 #[error("Protocol error: {message}")]
30 Protocol {
31 kind: ProtocolErrorKind,
32 message: String,
33 },
34
35 #[error("Invalid input for '{field}': {message}")]
37 InvalidInput {
38 field: &'static str,
39 message: String,
40 },
41
42 #[error("Resource limit exceeded: {resource} (limit: {limit})")]
44 ResourceLimit {
45 resource: &'static str,
46 limit: usize,
47 },
48
49 #[error("Download not found: {0}")]
51 NotFound(String),
52
53 #[error("Download already exists: {0}")]
55 AlreadyExists(String),
56
57 #[error("Invalid state: cannot {action} while {current_state}")]
59 InvalidState {
60 action: &'static str,
61 current_state: String,
62 },
63
64 #[error("Engine is shutting down")]
66 Shutdown,
67
68 #[error("Database error: {0}")]
70 Database(String),
71
72 #[error("Internal error: {0}")]
74 Internal(String),
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum NetworkErrorKind {
80 DnsResolution,
82 ConnectionRefused,
84 ConnectionReset,
86 Timeout,
88 Tls,
90 HttpStatus(u16),
92 Unreachable,
94 TooManyRedirects,
96 Other,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum StorageErrorKind {
103 NotFound,
105 PermissionDenied,
107 DiskFull,
109 PathTraversal,
111 AlreadyExists,
113 InvalidPath,
115 Io,
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub enum ProtocolErrorKind {
122 InvalidUrl,
124 RangeNotSupported,
126 InvalidResponse,
128 InvalidTorrent,
130 InvalidMagnet,
132 HashMismatch,
134 TrackerError,
136 PeerProtocol,
138 BencodeParse,
140 PexError,
142 DhtError,
144 LpdError,
146 MetadataError,
148}
149
150impl EngineError {
151 pub fn is_retryable(&self) -> bool {
153 match self {
154 Self::Network { retryable, .. } => *retryable,
155 Self::Storage { kind, .. } => matches!(kind, StorageErrorKind::Io),
156 Self::Protocol { kind, .. } => matches!(
157 kind,
158 ProtocolErrorKind::TrackerError | ProtocolErrorKind::PeerProtocol
159 ),
160 _ => false,
161 }
162 }
163
164 pub fn network(kind: NetworkErrorKind, message: impl Into<String>) -> Self {
166 let retryable = matches!(
167 kind,
168 NetworkErrorKind::Timeout
169 | NetworkErrorKind::ConnectionReset
170 | NetworkErrorKind::Unreachable
171 );
172 Self::Network {
173 kind,
174 message: message.into(),
175 retryable,
176 }
177 }
178
179 pub fn storage(
181 kind: StorageErrorKind,
182 path: impl Into<PathBuf>,
183 message: impl Into<String>,
184 ) -> Self {
185 Self::Storage {
186 kind,
187 path: path.into(),
188 message: message.into(),
189 }
190 }
191
192 pub fn protocol(kind: ProtocolErrorKind, message: impl Into<String>) -> Self {
194 Self::Protocol {
195 kind,
196 message: message.into(),
197 }
198 }
199
200 pub fn invalid_input(field: &'static str, message: impl Into<String>) -> Self {
202 Self::InvalidInput {
203 field,
204 message: message.into(),
205 }
206 }
207}
208
209pub type Result<T> = std::result::Result<T, EngineError>;
211
212impl From<std::io::Error> for EngineError {
215 fn from(err: std::io::Error) -> Self {
216 use std::io::ErrorKind;
217 let kind = match err.kind() {
218 ErrorKind::NotFound => StorageErrorKind::NotFound,
219 ErrorKind::PermissionDenied => StorageErrorKind::PermissionDenied,
220 ErrorKind::AlreadyExists => StorageErrorKind::AlreadyExists,
221 _ => StorageErrorKind::Io,
222 };
223 Self::Storage {
224 kind,
225 path: PathBuf::new(),
226 message: err.to_string(),
227 }
228 }
229}
230
231impl From<reqwest::Error> for EngineError {
232 fn from(err: reqwest::Error) -> Self {
233 let kind = if err.is_timeout() {
234 NetworkErrorKind::Timeout
235 } else if err.is_connect() {
236 NetworkErrorKind::ConnectionRefused
237 } else if err.is_redirect() {
238 NetworkErrorKind::TooManyRedirects
239 } else if let Some(status) = err.status() {
240 NetworkErrorKind::HttpStatus(status.as_u16())
241 } else {
242 NetworkErrorKind::Other
243 };
244
245 let retryable = matches!(
246 kind,
247 NetworkErrorKind::Timeout | NetworkErrorKind::ConnectionRefused
248 );
249
250 Self::Network {
251 kind,
252 message: err.to_string(),
253 retryable,
254 }
255 }
256}
257
258impl From<url::ParseError> for EngineError {
259 fn from(err: url::ParseError) -> Self {
260 Self::Protocol {
261 kind: ProtocolErrorKind::InvalidUrl,
262 message: err.to_string(),
263 }
264 }
265}
266
267impl From<rusqlite::Error> for EngineError {
268 fn from(err: rusqlite::Error) -> Self {
269 Self::Database(err.to_string())
270 }
271}
272
273impl From<serde_json::Error> for EngineError {
274 fn from(err: serde_json::Error) -> Self {
275 Self::Internal(format!("JSON error: {}", err))
276 }
277}
278
279impl From<tokio::sync::broadcast::error::SendError<crate::protocol::DownloadEvent>>
280 for EngineError
281{
282 fn from(_: tokio::sync::broadcast::error::SendError<crate::protocol::DownloadEvent>) -> Self {
283 Self::Shutdown
284 }
285}
286
287impl From<EngineError> for crate::protocol::ProtocolError {
289 fn from(e: EngineError) -> Self {
290 use crate::protocol::ProtocolError;
291 match e {
292 EngineError::NotFound(id) => ProtocolError::NotFound { id },
293 EngineError::InvalidState {
294 action,
295 current_state,
296 } => ProtocolError::InvalidState {
297 action: action.to_string(),
298 current_state,
299 },
300 EngineError::InvalidInput { field, message } => ProtocolError::InvalidInput {
301 field: field.to_string(),
302 message,
303 },
304 EngineError::Network {
305 message, retryable, ..
306 } => ProtocolError::Network { message, retryable },
307 EngineError::Storage { message, .. } => ProtocolError::Storage { message },
308 EngineError::Protocol { message, .. } => ProtocolError::Network {
309 message,
310 retryable: false,
311 },
312 EngineError::Shutdown => ProtocolError::Shutdown,
313 EngineError::AlreadyExists(id) => ProtocolError::InvalidInput {
314 field: "id".to_string(),
315 message: format!("Download already exists: {}", id),
316 },
317 EngineError::ResourceLimit { resource, limit } => ProtocolError::InvalidInput {
318 field: resource.to_string(),
319 message: format!("Resource limit exceeded (limit: {})", limit),
320 },
321 EngineError::Database(msg) => ProtocolError::Storage { message: msg },
322 EngineError::Internal(msg) => ProtocolError::Internal { message: msg },
323 }
324 }
325}