Skip to main content

pulith_fetch/
error.rs

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        // The error would be returned when trying to send the request
223        // For testing purposes, we'll create an error directly
224        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}