Skip to main content

cuenv_tools_oci/
error.rs

1//! Error types for OCI provider operations.
2
3use thiserror::Error;
4
5/// Result type for OCI provider operations.
6pub type Result<T> = std::result::Result<T, Error>;
7
8/// Errors that can occur during OCI operations.
9#[derive(Error, Debug)]
10pub enum Error {
11    /// Failed to parse image reference.
12    #[error("Invalid image reference '{0}': {1}")]
13    InvalidReference(String, String),
14
15    /// Registry authentication failed.
16    #[error("Authentication failed for registry '{0}': {1}")]
17    AuthenticationFailed(String, String),
18
19    /// Image or tag not found.
20    #[error("Image not found: {0}")]
21    ImageNotFound(String),
22
23    /// Platform not available for image.
24    #[error("Platform '{platform}' not available for image '{image}'")]
25    PlatformNotAvailable {
26        /// The image reference.
27        image: String,
28        /// The requested platform.
29        platform: String,
30    },
31
32    /// Failed to pull blob from registry.
33    #[error("Failed to pull blob {digest}: {message}")]
34    BlobPullFailed {
35        /// The blob digest.
36        digest: String,
37        /// Error message.
38        message: String,
39    },
40
41    /// Failed to extract binary from archive.
42    #[error("Failed to extract binary '{binary}' from archive: {message}")]
43    ExtractionFailed {
44        /// The binary name.
45        binary: String,
46        /// Error message.
47        message: String,
48    },
49
50    /// Binary not found in archive.
51    #[error("Binary '{0}' not found in archive")]
52    BinaryNotFound(String),
53
54    /// Cache operation failed.
55    #[error("Cache error: {0}")]
56    CacheError(String),
57
58    /// IO error.
59    #[error("IO error: {0}")]
60    Io(#[from] std::io::Error),
61
62    /// OCI distribution error.
63    #[error("OCI error: {0}")]
64    Oci(String),
65
66    /// JSON parsing error.
67    #[error("JSON error: {0}")]
68    Json(#[from] serde_json::Error),
69
70    /// Digest mismatch after download.
71    #[error("Digest mismatch for blob: expected {expected}, got {actual}")]
72    DigestMismatch {
73        /// The expected digest.
74        expected: String,
75        /// The computed digest.
76        actual: String,
77    },
78}
79
80impl Error {
81    /// Create an invalid reference error.
82    #[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    /// Create a platform not available error.
88    #[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    /// Create an extraction failed error.
97    #[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    /// Create a blob pull failed error.
106    #[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    /// Create a digest mismatch error.
115    #[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}