1mod native_source;
4
5use j2k::J2kError;
6use j2k_core::{
7 adapter_error_is_buffer_error, adapter_error_is_not_implemented, adapter_error_is_truncated,
8 adapter_error_is_unsupported, AdapterErrorKind, AdapterErrorParts, BackendRequest, BufferError,
9 CodecError,
10};
11use j2k_native::DecodeError as NativeDecodeError;
12
13pub use native_source::NativeBackendError;
14
15#[derive(Debug, thiserror::Error)]
17#[non_exhaustive]
18pub enum Error {
19 #[error(transparent)]
21 Decode(#[from] J2kError),
22 #[error("{context}: {source}")]
24 NativeDecode {
25 context: &'static str,
27 #[source]
29 source: NativeBackendError,
30 },
31 #[error(transparent)]
33 Buffer(#[from] BufferError),
34 #[error("host allocation failed for {what}: {bytes} bytes")]
36 HostAllocationFailed {
37 bytes: usize,
39 what: &'static str,
41 },
42 #[error(
44 "host allocation capacity for {what} is too large: requested {requested} bytes, cap {cap} bytes"
45 )]
46 HostAllocationTooLarge {
47 requested: usize,
49 cap: usize,
51 what: &'static str,
53 },
54 #[error(transparent)]
56 HtJobChunkPlan(#[from] j2k_core::HtGpuJobChunkPlanError),
57 #[error("backend request {request:?} is not supported by j2k-cuda")]
59 UnsupportedBackend {
60 request: BackendRequest,
62 },
63 #[error("unsupported CUDA request: {reason}")]
65 UnsupportedCudaRequest {
66 reason: &'static str,
68 },
69 #[error("CUDA is unavailable on this host")]
71 CudaUnavailable,
72 #[cfg(feature = "cuda-runtime")]
73 #[error("CUDA runtime error: {source}")]
75 CudaRuntime {
76 #[source]
78 source: j2k_cuda_runtime::CudaError,
79 },
80 #[cfg(feature = "cuda-runtime")]
81 #[error(
83 "CUDA tier-1 source {source_index} job {original_job_index} failed during device execution: {source}"
84 )]
85 CudaTier1JobFailed {
86 source_index: usize,
88 original_job_index: usize,
90 #[source]
92 source: j2k_cuda_runtime::CudaError,
93 },
94 #[cfg(feature = "cuda-runtime")]
95 #[error("CUDA operation failed ({primary}); CUDA cleanup also failed ({cleanup})")]
97 CudaCleanupFailed {
98 primary: Box<Error>,
100 cleanup: Box<Error>,
102 },
103}
104
105impl Error {
106 #[cfg(feature = "cuda-runtime")]
109 #[doc(hidden)]
110 pub fn completion_is_uncertain(&self) -> bool {
111 match self {
112 Self::CudaRuntime { source } | Self::CudaTier1JobFailed { source, .. } => {
113 source.completion_is_uncertain()
114 }
115 Self::CudaCleanupFailed { primary, cleanup } => {
116 primary.completion_is_uncertain() || cleanup.completion_is_uncertain()
117 }
118 _ => false,
119 }
120 }
121
122 #[doc(hidden)]
124 #[must_use]
125 pub fn session_is_unusable(&self) -> bool {
126 match self {
127 Self::CudaUnavailable => true,
128 #[cfg(feature = "cuda-runtime")]
129 Self::CudaRuntime { source } | Self::CudaTier1JobFailed { source, .. } => {
130 source.session_is_unusable()
131 }
132 #[cfg(feature = "cuda-runtime")]
133 Self::CudaCleanupFailed { primary, cleanup } => {
134 primary.session_is_unusable() || cleanup.session_is_unusable()
135 }
136 _ => false,
137 }
138 }
139}
140
141#[cfg(feature = "cuda-runtime")]
142pub(crate) fn combine_cuda_cleanup_errors(primary_error: Error, cleanup_error: Error) -> Error {
143 Error::CudaCleanupFailed {
144 primary: Box::new(primary_error),
145 cleanup: Box::new(cleanup_error),
146 }
147}
148
149#[doc(hidden)]
150impl AdapterErrorParts for Error {
151 fn source_codec_error(&self) -> Option<&dyn CodecError> {
152 match self {
153 Self::Decode(inner) => Some(inner),
154 _ => None,
155 }
156 }
157
158 fn adapter_error_kind(&self) -> AdapterErrorKind {
159 match self {
160 Self::Buffer(_) => AdapterErrorKind::Buffer,
161 Self::UnsupportedBackend { .. }
162 | Self::UnsupportedCudaRequest { .. }
163 | Self::CudaUnavailable => AdapterErrorKind::Unsupported,
164 Self::Decode(_)
165 | Self::NativeDecode { .. }
166 | Self::HostAllocationFailed { .. }
167 | Self::HostAllocationTooLarge { .. }
168 | Self::HtJobChunkPlan(_) => AdapterErrorKind::Other,
169 #[cfg(feature = "cuda-runtime")]
170 Self::CudaRuntime { .. }
171 | Self::CudaTier1JobFailed { .. }
172 | Self::CudaCleanupFailed { .. } => AdapterErrorKind::Other,
173 }
174 }
175}
176
177#[doc(hidden)]
178impl CodecError for Error {
179 fn is_truncated(&self) -> bool {
180 matches!(
181 self,
182 Self::NativeDecode { source, .. } if source.is_decode_truncated()
183 ) || adapter_error_is_truncated(self)
184 }
185
186 fn is_not_implemented(&self) -> bool {
187 adapter_error_is_not_implemented(self)
188 }
189
190 fn is_unsupported(&self) -> bool {
191 matches!(
192 self,
193 Self::NativeDecode { source, .. } if source.is_unsupported()
194 ) || adapter_error_is_unsupported(self)
195 }
196
197 fn is_buffer_error(&self) -> bool {
198 adapter_error_is_buffer_error(self)
199 }
200}
201
202#[cfg_attr(
203 not(any(test, feature = "cuda-runtime")),
204 expect(
205 dead_code,
206 reason = "native decode translation is used by CUDA decode and its tests"
207 )
208)]
209pub(crate) fn native_decode_error(error: NativeDecodeError) -> Error {
210 Error::NativeDecode {
211 context: "native JPEG 2000 backend failed",
212 source: NativeBackendError::decode(error),
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use j2k_core::CodecError;
219 use j2k_native::{
220 DecodeError as NativeDecodeError, DecodingError as NativeDecodingError,
221 DirectPlanUnsupportedReason as NativeDirectPlanUnsupportedReason,
222 };
223
224 #[cfg(feature = "cuda-runtime")]
225 use super::combine_cuda_cleanup_errors;
226 use super::{native_decode_error, Error, NativeBackendError};
227 #[cfg(feature = "cuda-runtime")]
228 use j2k_cuda_runtime::CudaError;
229
230 #[cfg(feature = "cuda-runtime")]
231 fn runtime_error(message: &str) -> Error {
232 Error::CudaRuntime {
233 source: CudaError::StatePoisoned {
234 message: message.to_string(),
235 },
236 }
237 }
238
239 #[test]
240 fn host_allocation_failure_is_an_operational_error_not_buffer_misuse() {
241 let error = Error::HostAllocationFailed {
242 bytes: 4096,
243 what: "test staging",
244 };
245 assert!(!error.is_buffer_error());
246 assert!(!error.is_unsupported());
247 assert!(error.to_string().contains("4096"));
248 }
249
250 #[test]
251 fn host_capacity_failure_preserves_actual_phase_budget() {
252 let error = Error::HostAllocationTooLarge {
253 requested: 17,
254 cap: 16,
255 what: "test phase",
256 };
257 assert!(!error.is_buffer_error());
258 assert!(!error.is_unsupported());
259 assert!(error.to_string().contains("17"));
260 assert!(error.to_string().contains("16"));
261 }
262
263 #[test]
264 fn native_decode_unsupported_error_keeps_codec_classification() {
265 let error = native_decode_error(NativeDecodeError::Decoding(
266 NativeDecodingError::UnsupportedFeature("test feature"),
267 ));
268
269 assert!(error.is_unsupported());
270 assert!(!error.is_truncated());
271 assert!(!error.is_not_implemented());
272 }
273
274 #[test]
275 fn native_decode_direct_plan_error_keeps_codec_classification() {
276 let error = native_decode_error(NativeDecodeError::Decoding(
277 NativeDecodingError::DirectPlanUnsupported(
278 NativeDirectPlanUnsupportedReason::ColorSingleTileCodestream,
279 ),
280 ));
281
282 assert!(error.is_unsupported());
283 assert!(!error.is_truncated());
284 assert!(!error.is_not_implemented());
285 }
286
287 #[test]
288 fn native_decode_unexpected_eof_keeps_codec_classification() {
289 let error = native_decode_error(NativeDecodeError::Decoding(
290 NativeDecodingError::UnexpectedEof,
291 ));
292
293 assert!(error.is_truncated());
294 assert!(!error.is_unsupported());
295 }
296
297 #[test]
298 fn native_decode_resource_errors_preserve_typed_sources() {
299 let sources = [
300 NativeDecodeError::AllocationTooLarge {
301 what: "CUDA decode fixture",
302 requested: 9,
303 cap: 8,
304 },
305 NativeDecodeError::HostAllocationFailed {
306 what: "CUDA decode fixture",
307 bytes: 7,
308 },
309 ];
310
311 for source in sources {
312 let error = native_decode_error(source);
313 assert!(matches!(
314 &error,
315 Error::NativeDecode {
316 context: "native JPEG 2000 backend failed",
317 source: stored,
318 } if stored == &NativeBackendError::decode(source)
319 ));
320 let opaque = core::error::Error::source(&error).expect("opaque adapter source");
321 assert!(opaque.downcast_ref::<NativeBackendError>().is_some());
322 let concrete = opaque.source().expect("concrete native decode source");
323 assert_eq!(concrete.downcast_ref::<NativeDecodeError>(), Some(&source));
324 assert!(!error.is_buffer_error());
325 assert!(!error.is_unsupported());
326 }
327 }
328
329 #[cfg(feature = "cuda-runtime")]
330 #[test]
331 fn cuda_cleanup_failure_preserves_both_runtime_diagnostics() {
332 let combined = combine_cuda_cleanup_errors(
333 runtime_error("primary launch failure"),
334 runtime_error("follow-up synchronization failure"),
335 );
336
337 assert!(matches!(&combined, Error::CudaCleanupFailed { .. }));
338 let rendered = combined.to_string();
339 assert!(rendered.contains("primary launch failure"));
340 assert!(rendered.contains("follow-up synchronization failure"));
341 assert!(!combined.is_unsupported());
342 }
343
344 #[cfg(feature = "cuda-runtime")]
345 #[test]
346 fn cuda_cleanup_failure_preserves_non_runtime_primary_and_blocks_fallback() {
347 let combined = combine_cuda_cleanup_errors(
348 Error::UnsupportedCudaRequest {
349 reason: "invalid prepared store",
350 },
351 runtime_error("synchronization failure"),
352 );
353
354 assert!(matches!(&combined, Error::CudaCleanupFailed { .. }));
355 let rendered = combined.to_string();
356 assert!(rendered.contains("invalid prepared store"));
357 assert!(rendered.contains("synchronization failure"));
358 assert!(!combined.is_unsupported());
359 }
360}