Skip to main content

soar_dl/
error.rs

1use miette::Diagnostic;
2use thiserror::Error;
3
4#[derive(Error, Diagnostic, Debug)]
5pub enum DownloadError {
6    #[error("Invalid URL: {url}")]
7    #[diagnostic(code(soar_dl::invalid_url))]
8    InvalidUrl {
9        url: String,
10        #[source]
11        source: url::ParseError,
12    },
13
14    #[error(transparent)]
15    #[diagnostic(code(soar_dl::extract_error))]
16    ExtractError(#[from] compak::error::ArchiveError),
17
18    #[error(transparent)]
19    #[diagnostic(
20        code(soar_dl::network),
21        help("Check your internet connection or try again later")
22    )]
23    Network(#[from] Box<ureq::Error>),
24
25    #[error("HTTP {status}: {url}")]
26    #[diagnostic(code(soar_dl::http_error))]
27    HttpError { status: u16, url: String },
28
29    #[error(transparent)]
30    #[diagnostic(code(soar_dl::io))]
31    Io(#[from] std::io::Error),
32
33    #[error("No matching assets found")]
34    #[diagnostic(
35        code(soar_dl::no_match),
36        help("Available assets:\n{}", .available.join("\n"))
37    )]
38    NoMatch { available: Vec<String> },
39
40    #[error("Layer not found")]
41    #[diagnostic(code(soar_dl::layer_not_found))]
42    LayerNotFound,
43
44    #[error("Unsafe layer path in manifest: {title}")]
45    #[diagnostic(code(soar_dl::unsafe_layer_path))]
46    UnsafeLayerPath { title: String },
47
48    #[error("Checksum mismatch: expected {expected}, got {got}")]
49    #[diagnostic(code(soar_dl::checksum_mismatch))]
50    ChecksumMismatch { expected: String, got: String },
51
52    #[error("Digest mismatch: expected {expected}, got {got}")]
53    #[diagnostic(code(soar_dl::digest_mismatch))]
54    DigestMismatch { expected: String, got: String },
55
56    #[error("Invalid response from server")]
57    #[diagnostic(code(soar_dl::invalid_response))]
58    InvalidResponse,
59
60    #[error("File name could not be determined")]
61    #[diagnostic(
62        code(soar_dl::no_filename),
63        help("Try specifying an output path explicitly")
64    )]
65    NoFilename,
66
67    #[error("Resume metadata mismatch")]
68    #[diagnostic(code(soar_dl::resume_mismatch))]
69    ResumeMismatch,
70
71    #[error("{}", .errors.join("; "))]
72    #[diagnostic(code(soar_dl::multiple_errors))]
73    Multiple { errors: Vec<String> },
74
75    #[error("zsync: {0}")]
76    #[diagnostic(
77        code(soar_dl::zsync),
78        help("The publisher's zsync feed may be stale; a plain download still works")
79    )]
80    Zsync(String),
81}
82
83pub type Result<T> = miette::Result<T>;
84
85impl From<ureq::Error> for DownloadError {
86    /// Converts a `ureq::Error` into a `DownloadError::Network` variant.
87    ///
88    /// # Examples
89    ///
90    /// ```no_run
91    /// use soar_dl::error::DownloadError;
92    ///
93    /// // Given a `ureq::Error` `e`, convert it into a `DownloadError`
94    /// let e: ureq::Error = /* obtained from a ureq request */ unimplemented!();
95    /// let err: DownloadError = DownloadError::from(e);
96    /// match err {
97    ///     DownloadError::Network(_) => (),
98    ///     _ => panic!("expected DownloadError::Network"),
99    /// }
100    /// ```
101    fn from(e: ureq::Error) -> Self {
102        Self::Network(Box::new(e))
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn test_download_error_invalid_url() {
112        let err = DownloadError::InvalidUrl {
113            url: "invalid".to_string(),
114            source: url::ParseError::RelativeUrlWithoutBase,
115        };
116        let msg = format!("{}", err);
117        assert!(msg.contains("Invalid URL"));
118        assert!(msg.contains("invalid"));
119    }
120
121    #[test]
122    fn test_download_error_http_error() {
123        let err = DownloadError::HttpError {
124            status: 404,
125            url: "https://example.com/notfound".to_string(),
126        };
127        let msg = format!("{}", err);
128        assert!(msg.contains("HTTP 404"));
129        assert!(msg.contains("https://example.com/notfound"));
130    }
131
132    #[test]
133    fn test_download_error_no_match() {
134        let err = DownloadError::NoMatch {
135            available: vec!["file1.zip".to_string(), "file2.tar.gz".to_string()],
136        };
137        let msg = format!("{}", err);
138        assert!(msg.contains("No matching assets found"));
139    }
140
141    #[test]
142    fn test_download_error_layer_not_found() {
143        let err = DownloadError::LayerNotFound;
144        let msg = format!("{}", err);
145        assert_eq!(msg, "Layer not found");
146    }
147
148    #[test]
149    fn test_download_error_invalid_response() {
150        let err = DownloadError::InvalidResponse;
151        let msg = format!("{}", err);
152        assert_eq!(msg, "Invalid response from server");
153    }
154
155    #[test]
156    fn test_download_error_no_filename() {
157        let err = DownloadError::NoFilename;
158        let msg = format!("{}", err);
159        assert_eq!(msg, "File name could not be determined");
160    }
161
162    #[test]
163    fn test_download_error_resume_mismatch() {
164        let err = DownloadError::ResumeMismatch;
165        let msg = format!("{}", err);
166        assert_eq!(msg, "Resume metadata mismatch");
167    }
168
169    #[test]
170    fn test_download_error_multiple() {
171        let err = DownloadError::Multiple {
172            errors: vec!["Error 1".to_string(), "Error 2".to_string()],
173        };
174        let msg = format!("{}", err);
175        assert_eq!(msg, "Error 1; Error 2");
176    }
177
178    #[test]
179    fn test_download_error_io() {
180        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
181        let err = DownloadError::Io(io_err);
182        let msg = format!("{}", err);
183        assert!(msg.contains("file not found"));
184    }
185
186    #[test]
187    fn test_download_error_debug() {
188        let err = DownloadError::LayerNotFound;
189        let debug = format!("{:?}", err);
190        assert!(debug.contains("LayerNotFound"));
191    }
192
193    #[test]
194    fn test_from_ureq_error() {
195        let ureq_err = ureq::Error::ConnectionFailed;
196        let download_err: DownloadError = ureq_err.into();
197
198        match download_err {
199            DownloadError::Network(_) => (),
200            _ => panic!("Expected Network error variant"),
201        }
202    }
203
204    #[test]
205    fn test_error_source_chain() {
206        let err = DownloadError::InvalidUrl {
207            url: "invalid".to_string(),
208            source: url::ParseError::RelativeUrlWithoutBase,
209        };
210
211        // Check that we can get the source
212        assert!(std::error::Error::source(&err).is_some());
213    }
214}