1use thiserror::Error;
2
3pub type Result<T> = std::result::Result<T, Error>;
4
5#[derive(Debug, Error)]
6pub enum Error {
7 #[error("invalid URL: {0}")]
8 InvalidUrl(String),
9
10 #[error("HTTP error: {status} {message}")]
11 Http { status: u16, message: String },
12
13 #[error("checksum mismatch: expected {expected}, got {actual}")]
14 ChecksumMismatch { expected: String, actual: String },
15
16 #[error("max retries exceeded ({count} attempts)")]
17 MaxRetriesExceeded { count: u32 },
18
19 #[error("too many redirects ({count})")]
20 TooManyRedirects { count: u32 },
21
22 #[error("redirect loop detected")]
23 RedirectLoop,
24
25 #[error("destination is a directory")]
26 DestinationIsDirectory,
27
28 #[error("invalid state: {0}")]
29 InvalidState(String),
30
31 #[error(transparent)]
32 Fs(#[from] pulith_fs::Error),
33
34 #[error("network error: {0}")]
35 Network(String),
36
37 #[error("timeout: {0}")]
38 Timeout(String),
39
40 #[error("transform error: {0}")]
41 Transform(#[from] crate::codec::decompress::TransformError),
42}
43
44impl From<std::io::Error> for Error {
45 fn from(e: std::io::Error) -> Self {
46 Error::Network(e.to_string())
47 }
48}
49
50impl From<pulith_verify::VerifyError> for Error {
51 fn from(e: pulith_verify::VerifyError) -> Self {
52 match e {
53 pulith_verify::VerifyError::HashMismatch { expected, actual } => {
54 Error::ChecksumMismatch {
55 expected: hex::encode(expected),
56 actual: hex::encode(actual),
57 }
58 }
59 pulith_verify::VerifyError::SizeMismatch { expected, actual } => Error::InvalidState(
60 format!("verified stream length mismatch: expected {expected} bytes, got {actual}"),
61 ),
62 pulith_verify::VerifyError::Io(e) => Error::Network(e.to_string()),
63 pulith_verify::VerifyError::HexDecode(e) => Error::Network(e.to_string()),
64 }
65 }
66}
67
68#[cfg(feature = "reqwest")]
69impl From<reqwest::Error> for Error {
70 fn from(e: reqwest::Error) -> Self {
71 Error::Network(e.to_string())
72 }
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78 use std::io;
79
80 #[test]
81 fn test_error_display() {
82 assert_eq!(
83 Error::InvalidUrl("invalid".to_string()).to_string(),
84 "invalid URL: invalid"
85 );
86
87 assert_eq!(
88 Error::Http {
89 status: 404,
90 message: "Not Found".to_string()
91 }
92 .to_string(),
93 "HTTP error: 404 Not Found"
94 );
95
96 assert_eq!(
97 Error::ChecksumMismatch {
98 expected: "abc123".to_string(),
99 actual: "def456".to_string(),
100 }
101 .to_string(),
102 "checksum mismatch: expected abc123, got def456"
103 );
104
105 assert_eq!(
106 Error::MaxRetriesExceeded { count: 3 }.to_string(),
107 "max retries exceeded (3 attempts)"
108 );
109
110 assert_eq!(
111 Error::TooManyRedirects { count: 5 }.to_string(),
112 "too many redirects (5)"
113 );
114
115 assert_eq!(Error::RedirectLoop.to_string(), "redirect loop detected");
116
117 assert_eq!(
118 Error::DestinationIsDirectory.to_string(),
119 "destination is a directory"
120 );
121
122 assert_eq!(
123 Error::InvalidState("bad state".to_string()).to_string(),
124 "invalid state: bad state"
125 );
126
127 assert_eq!(
128 Error::Network("connection failed".to_string()).to_string(),
129 "network error: connection failed"
130 );
131
132 assert_eq!(
133 Error::Timeout("request timed out".to_string()).to_string(),
134 "timeout: request timed out"
135 );
136 }
137
138 #[test]
139 fn test_error_debug() {
140 let error = Error::InvalidUrl("test".to_string());
141 assert!(format!("{:?}", error).contains("InvalidUrl"));
142 }
143
144 #[test]
145 fn test_result_type_alias() {
146 let ok: Result<()> = Ok(());
147 assert!(ok.is_ok());
148
149 let err: Result<()> = Err(Error::InvalidUrl("test".to_string()));
150 assert!(err.is_err());
151 }
152
153 #[test]
154 fn test_from_io_error() {
155 let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
156 let error: Error = io_err.into();
157 match error {
158 Error::Network(msg) => assert!(msg.contains("file not found")),
159 _ => panic!("Expected Network error"),
160 }
161 }
162
163 #[test]
164 fn test_from_verify_error_hash_mismatch() {
165 let verify_err = pulith_verify::VerifyError::HashMismatch {
166 expected: b"abc123".to_vec(),
167 actual: b"def456".to_vec(),
168 };
169 let error: Error = verify_err.into();
170 match error {
171 Error::ChecksumMismatch { expected, actual } => {
172 assert_eq!(expected, "616263313233");
173 assert_eq!(actual, "646566343536");
174 }
175 _ => panic!("Expected ChecksumMismatch error"),
176 }
177 }
178
179 #[test]
180 fn test_from_verify_error_io() {
181 let io_err = io::Error::new(io::ErrorKind::PermissionDenied, "access denied");
182 let verify_err = pulith_verify::VerifyError::Io(io_err);
183 let error: Error = verify_err.into();
184 match error {
185 Error::Network(msg) => assert!(msg.contains("access denied")),
186 _ => panic!("Expected Network error"),
187 }
188 }
189
190 #[test]
191 fn test_from_verify_error_size_mismatch() {
192 let verify_err = pulith_verify::VerifyError::SizeMismatch {
193 expected: 100,
194 actual: 98,
195 };
196 let error: Error = verify_err.into();
197 match error {
198 Error::InvalidState(msg) => {
199 assert!(msg.contains("expected 100 bytes"));
200 assert!(msg.contains("got 98"));
201 }
202 _ => panic!("Expected InvalidState error"),
203 }
204 }
205
206 #[test]
207 fn test_from_verify_error_hex_decode() {
208 let hex_err = hex::FromHexError::OddLength;
209 let verify_err = pulith_verify::VerifyError::HexDecode(hex_err);
210 let error: Error = verify_err.into();
211 match error {
212 Error::Network(_) => (),
213 _ => panic!("Expected Network error"),
214 }
215 }
216
217 #[cfg(feature = "reqwest")]
218 #[test]
219 fn test_from_reqwest_error() {
220 let client = reqwest::Client::new();
221 let _ = client.get("invalid-url");
222 let error: Error = Error::Network("invalid URL".to_string());
225 match error {
226 Error::Network(_) => (),
227 _ => panic!("Expected Network error"),
228 }
229 }
230
231 #[test]
232 fn test_from_transform_error() {
233 let transform_err =
234 crate::codec::decompress::TransformError::Transform("unsupported".to_string());
235 let error: Error = transform_err.into();
236 match error {
237 Error::Transform(_) => (),
238 _ => panic!("Expected Transform error"),
239 }
240 }
241
242 #[test]
243 fn test_fs_error_transparent() {
244 let fs_err = pulith_fs::Error::NotFound(std::path::PathBuf::from("file.txt"));
245 let error: Error = fs_err.into();
246 assert!(error.to_string().contains("file.txt"));
247 }
248}