1use std::path::PathBuf;
31use thiserror::Error;
32
33#[derive(Debug, Error)]
35pub enum EngineError {
36 #[error("Network error: {message}")]
38 Network {
39 kind: NetworkErrorKind,
40 message: String,
41 retryable: bool,
42 },
43
44 #[error("Storage error at {path:?}: {message}")]
46 Storage {
47 kind: StorageErrorKind,
48 path: PathBuf,
49 message: String,
50 },
51
52 #[error("Protocol error: {message}")]
54 Protocol {
55 kind: ProtocolErrorKind,
56 message: String,
57 },
58
59 #[error("Invalid input for '{field}': {message}")]
61 InvalidInput {
62 field: &'static str,
63 message: String,
64 },
65
66 #[error("Resource limit exceeded: {resource} (limit: {limit})")]
68 ResourceLimit {
69 resource: &'static str,
70 limit: usize,
71 },
72
73 #[error("Download not found: {0}")]
75 NotFound(String),
76
77 #[error("Download already exists: {0}")]
79 AlreadyExists(String),
80
81 #[error("Invalid state: cannot {action} while {current_state}")]
83 InvalidState {
84 action: &'static str,
85 current_state: String,
86 },
87
88 #[error("Engine is shutting down")]
90 Shutdown,
91
92 #[error("Database error: {0}")]
94 Database(String),
95
96 #[error("Internal error: {0}")]
98 Internal(String),
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub enum NetworkErrorKind {
104 DnsResolution,
106 ConnectionRefused,
108 ConnectionReset,
110 Timeout,
112 Tls,
114 HttpStatus(u16),
116 Unreachable,
118 TooManyRedirects,
120 Other,
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126pub enum StorageErrorKind {
127 NotFound,
129 PermissionDenied,
131 DiskFull,
133 PathTraversal,
135 AlreadyExists,
137 InvalidPath,
139 Io,
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145pub enum ProtocolErrorKind {
146 InvalidUrl,
148 RangeNotSupported,
150 InvalidResponse,
152 InvalidTorrent,
154 InvalidMagnet,
156 HashMismatch,
158 TrackerError,
160 PeerProtocol,
162 BencodeParse,
164 PexError,
166 DhtError,
168 LpdError,
170 MetadataError,
172}
173
174impl EngineError {
175 pub fn is_retryable(&self) -> bool {
177 match self {
178 Self::Network { retryable, .. } => *retryable,
179 Self::Storage { kind, .. } => matches!(kind, StorageErrorKind::Io),
180 Self::Protocol { kind, .. } => matches!(
181 kind,
182 ProtocolErrorKind::TrackerError | ProtocolErrorKind::PeerProtocol
183 ),
184 _ => false,
185 }
186 }
187
188 pub fn network(kind: NetworkErrorKind, message: impl Into<String>) -> Self {
190 let retryable = matches!(
191 kind,
192 NetworkErrorKind::Timeout
193 | NetworkErrorKind::ConnectionReset
194 | NetworkErrorKind::Unreachable
195 | NetworkErrorKind::HttpStatus(408)
196 | NetworkErrorKind::HttpStatus(429)
197 | NetworkErrorKind::HttpStatus(500..=599)
198 );
199 Self::Network {
200 kind,
201 message: message.into(),
202 retryable,
203 }
204 }
205
206 pub fn storage(
208 kind: StorageErrorKind,
209 path: impl Into<PathBuf>,
210 message: impl Into<String>,
211 ) -> Self {
212 Self::Storage {
213 kind,
214 path: path.into(),
215 message: message.into(),
216 }
217 }
218
219 pub fn protocol(kind: ProtocolErrorKind, message: impl Into<String>) -> Self {
221 Self::Protocol {
222 kind,
223 message: message.into(),
224 }
225 }
226
227 pub fn invalid_input(field: &'static str, message: impl Into<String>) -> Self {
229 Self::InvalidInput {
230 field,
231 message: message.into(),
232 }
233 }
234
235 pub fn is_not_found(&self) -> bool {
237 matches!(self, Self::NotFound(_))
238 }
239
240 pub fn is_network(&self) -> bool {
242 matches!(self, Self::Network { .. })
243 }
244
245 pub fn is_shutdown(&self) -> bool {
247 matches!(self, Self::Shutdown)
248 }
249}
250
251pub type Result<T> = std::result::Result<T, EngineError>;
253
254impl From<std::io::Error> for EngineError {
257 fn from(err: std::io::Error) -> Self {
258 use std::io::ErrorKind;
259 let kind = match err.kind() {
260 ErrorKind::NotFound => StorageErrorKind::NotFound,
261 ErrorKind::PermissionDenied => StorageErrorKind::PermissionDenied,
262 ErrorKind::AlreadyExists => StorageErrorKind::AlreadyExists,
263 _ => StorageErrorKind::Io,
264 };
265 Self::Storage {
266 kind,
267 path: PathBuf::new(),
268 message: err.to_string(),
269 }
270 }
271}
272
273#[cfg(any(feature = "http", feature = "torrent"))]
274impl From<reqwest::Error> for EngineError {
275 fn from(err: reqwest::Error) -> Self {
276 let kind = if err.is_timeout() {
277 NetworkErrorKind::Timeout
278 } else if err.is_connect() {
279 NetworkErrorKind::ConnectionRefused
280 } else if err.is_redirect() {
281 NetworkErrorKind::TooManyRedirects
282 } else if err.is_body() || err.is_decode() {
283 NetworkErrorKind::ConnectionReset
285 } else if let Some(status) = err.status() {
286 NetworkErrorKind::HttpStatus(status.as_u16())
287 } else {
288 NetworkErrorKind::Other
289 };
290
291 Self::network(kind, err.to_string())
293 }
294}
295
296impl From<url::ParseError> for EngineError {
297 fn from(err: url::ParseError) -> Self {
298 Self::Protocol {
299 kind: ProtocolErrorKind::InvalidUrl,
300 message: err.to_string(),
301 }
302 }
303}
304
305#[cfg(feature = "storage")]
306impl From<rusqlite::Error> for EngineError {
307 fn from(err: rusqlite::Error) -> Self {
308 Self::Database(err.to_string())
309 }
310}
311
312impl From<serde_json::Error> for EngineError {
313 fn from(err: serde_json::Error) -> Self {
314 Self::Internal(format!("JSON error: {}", err))
315 }
316}
317
318impl From<tokio::sync::broadcast::error::SendError<crate::protocol::DownloadEvent>>
319 for EngineError
320{
321 fn from(_: tokio::sync::broadcast::error::SendError<crate::protocol::DownloadEvent>) -> Self {
322 Self::Shutdown
323 }
324}
325
326impl From<EngineError> for crate::protocol::ProtocolError {
328 fn from(e: EngineError) -> Self {
329 use crate::protocol::ProtocolError;
330 match e {
331 EngineError::NotFound(id) => ProtocolError::NotFound { id },
332 EngineError::InvalidState {
333 action,
334 current_state,
335 } => ProtocolError::InvalidState {
336 action: action.to_string(),
337 current_state,
338 },
339 EngineError::InvalidInput { field, message } => ProtocolError::InvalidInput {
340 field: field.to_string(),
341 message,
342 },
343 EngineError::Network {
344 message, retryable, ..
345 } => ProtocolError::Network { message, retryable },
346 EngineError::Storage { message, .. } => ProtocolError::Storage { message },
347 EngineError::Protocol { message, .. } => ProtocolError::Network {
348 message,
349 retryable: false,
350 },
351 EngineError::Shutdown => ProtocolError::Shutdown,
352 EngineError::AlreadyExists(id) => ProtocolError::InvalidInput {
353 field: "id".to_string(),
354 message: format!("Download already exists: {}", id),
355 },
356 EngineError::ResourceLimit { resource, limit } => ProtocolError::InvalidInput {
357 field: resource.to_string(),
358 message: format!("Resource limit exceeded (limit: {})", limit),
359 },
360 EngineError::Database(msg) => ProtocolError::Storage { message: msg },
361 EngineError::Internal(msg) => ProtocolError::Internal { message: msg },
362 }
363 }
364}