1use thiserror::Error;
4
5pub type Result<T> = std::result::Result<T, Error>;
7
8#[derive(Error, Debug)]
10pub enum Error {
11 #[error("Invalid image reference '{0}': {1}")]
13 InvalidReference(String, String),
14
15 #[error("Authentication failed for registry '{0}': {1}")]
17 AuthenticationFailed(String, String),
18
19 #[error("Image not found: {0}")]
21 ImageNotFound(String),
22
23 #[error("Platform '{platform}' not available for image '{image}'")]
25 PlatformNotAvailable {
26 image: String,
28 platform: String,
30 },
31
32 #[error("Failed to pull blob {digest}: {message}")]
34 BlobPullFailed {
35 digest: String,
37 message: String,
39 },
40
41 #[error("Failed to extract binary '{binary}' from archive: {message}")]
43 ExtractionFailed {
44 binary: String,
46 message: String,
48 },
49
50 #[error("Binary '{0}' not found in archive")]
52 BinaryNotFound(String),
53
54 #[error("Cache error: {0}")]
56 CacheError(String),
57
58 #[error("IO error: {0}")]
60 Io(#[from] std::io::Error),
61
62 #[error("OCI error: {0}")]
64 Oci(String),
65
66 #[error("JSON error: {0}")]
68 Json(#[from] serde_json::Error),
69
70 #[error("Digest mismatch for blob: expected {expected}, got {actual}")]
72 DigestMismatch {
73 expected: String,
75 actual: String,
77 },
78}
79
80impl Error {
81 #[must_use]
83 pub fn invalid_reference(reference: impl Into<String>, message: impl Into<String>) -> Self {
84 Self::InvalidReference(reference.into(), message.into())
85 }
86
87 #[must_use]
89 pub fn platform_not_available(image: impl Into<String>, platform: impl Into<String>) -> Self {
90 Self::PlatformNotAvailable {
91 image: image.into(),
92 platform: platform.into(),
93 }
94 }
95
96 #[must_use]
98 pub fn extraction_failed(binary: impl Into<String>, message: impl Into<String>) -> Self {
99 Self::ExtractionFailed {
100 binary: binary.into(),
101 message: message.into(),
102 }
103 }
104
105 #[must_use]
107 pub fn blob_pull_failed(digest: impl Into<String>, message: impl Into<String>) -> Self {
108 Self::BlobPullFailed {
109 digest: digest.into(),
110 message: message.into(),
111 }
112 }
113
114 #[must_use]
116 pub fn digest_mismatch(expected: impl Into<String>, actual: impl Into<String>) -> Self {
117 Self::DigestMismatch {
118 expected: expected.into(),
119 actual: actual.into(),
120 }
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn test_invalid_reference_error() {
130 let err = Error::invalid_reference("nginx:latest", "invalid syntax");
131 let msg = err.to_string();
132 assert!(msg.contains("nginx:latest"));
133 assert!(msg.contains("invalid syntax"));
134 }
135
136 #[test]
137 fn test_platform_not_available_error() {
138 let err = Error::platform_not_available("nginx:latest", "windows-arm64");
139 let msg = err.to_string();
140 assert!(msg.contains("windows-arm64"));
141 assert!(msg.contains("nginx:latest"));
142 }
143
144 #[test]
145 fn test_extraction_failed_error() {
146 let err = Error::extraction_failed("nginx", "file not found");
147 let msg = err.to_string();
148 assert!(msg.contains("nginx"));
149 assert!(msg.contains("file not found"));
150 }
151
152 #[test]
153 fn test_blob_pull_failed_error() {
154 let err = Error::blob_pull_failed("sha256:abc123", "network timeout");
155 let msg = err.to_string();
156 assert!(msg.contains("sha256:abc123"));
157 assert!(msg.contains("network timeout"));
158 }
159
160 #[test]
161 fn test_digest_mismatch_error() {
162 let err = Error::digest_mismatch("sha256:expected", "sha256:actual");
163 let msg = err.to_string();
164 assert!(msg.contains("expected"));
165 assert!(msg.contains("actual"));
166 }
167
168 #[test]
169 fn test_authentication_failed_error() {
170 let err = Error::AuthenticationFailed("ghcr.io".to_string(), "invalid token".to_string());
171 let msg = err.to_string();
172 assert!(msg.contains("ghcr.io"));
173 assert!(msg.contains("invalid token"));
174 }
175
176 #[test]
177 fn test_image_not_found_error() {
178 let err = Error::ImageNotFound("nginx:nonexistent".to_string());
179 let msg = err.to_string();
180 assert!(msg.contains("nginx:nonexistent"));
181 }
182
183 #[test]
184 fn test_binary_not_found_error() {
185 let err = Error::BinaryNotFound("/usr/bin/missing".to_string());
186 let msg = err.to_string();
187 assert!(msg.contains("/usr/bin/missing"));
188 }
189
190 #[test]
191 fn test_cache_error() {
192 let err = Error::CacheError("disk full".to_string());
193 let msg = err.to_string();
194 assert!(msg.contains("disk full"));
195 }
196
197 #[test]
198 fn test_oci_error() {
199 let err = Error::Oci("manifest not found".to_string());
200 let msg = err.to_string();
201 assert!(msg.contains("manifest not found"));
202 }
203
204 #[test]
205 fn test_io_error_conversion() {
206 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
207 let err: Error = io_err.into();
208 let msg = err.to_string();
209 assert!(msg.contains("file missing") || msg.contains("IO error"));
210 }
211
212 #[test]
213 fn test_json_error_conversion() {
214 let json_str = "not valid json {";
215 let json_err = serde_json::from_str::<serde_json::Value>(json_str).unwrap_err();
216 let err: Error = json_err.into();
217 let msg = err.to_string();
218 assert!(msg.contains("JSON error"));
219 }
220
221 #[test]
222 fn test_error_debug_impl() {
223 let err = Error::invalid_reference("test", "reason");
224 let debug = format!("{err:?}");
225 assert!(debug.contains("InvalidReference"));
226 }
227}