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 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 assert!(std::error::Error::source(&err).is_some());
213 }
214}