mod native_source;
use j2k::J2kError;
use j2k_core::{
adapter_error_is_buffer_error, adapter_error_is_not_implemented, adapter_error_is_truncated,
adapter_error_is_unsupported, AdapterErrorKind, AdapterErrorParts, BackendRequest, BufferError,
CodecError,
};
use j2k_native::DecodeError as NativeDecodeError;
pub use native_source::NativeBackendError;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error(transparent)]
Decode(#[from] J2kError),
#[error("{context}: {source}")]
NativeDecode {
context: &'static str,
#[source]
source: NativeBackendError,
},
#[error(transparent)]
Buffer(#[from] BufferError),
#[error("host allocation failed for {what}: {bytes} bytes")]
HostAllocationFailed {
bytes: usize,
what: &'static str,
},
#[error(
"host allocation capacity for {what} is too large: requested {requested} bytes, cap {cap} bytes"
)]
HostAllocationTooLarge {
requested: usize,
cap: usize,
what: &'static str,
},
#[error("backend request {request:?} is not supported by j2k-cuda")]
UnsupportedBackend {
request: BackendRequest,
},
#[error("unsupported CUDA request: {reason}")]
UnsupportedCudaRequest {
reason: &'static str,
},
#[error("CUDA is unavailable on this host")]
CudaUnavailable,
#[cfg(feature = "cuda-runtime")]
#[error("CUDA runtime error: {source}")]
CudaRuntime {
#[source]
source: j2k_cuda_runtime::CudaError,
},
#[cfg(feature = "cuda-runtime")]
#[error("CUDA operation failed ({primary}); CUDA cleanup also failed ({cleanup})")]
CudaCleanupFailed {
primary: Box<Error>,
cleanup: Box<Error>,
},
}
#[cfg(feature = "cuda-runtime")]
pub(crate) fn combine_cuda_cleanup_errors(primary_error: Error, cleanup_error: Error) -> Error {
Error::CudaCleanupFailed {
primary: Box::new(primary_error),
cleanup: Box::new(cleanup_error),
}
}
#[doc(hidden)]
impl AdapterErrorParts for Error {
fn source_codec_error(&self) -> Option<&dyn CodecError> {
match self {
Self::Decode(inner) => Some(inner),
_ => None,
}
}
fn adapter_error_kind(&self) -> AdapterErrorKind {
match self {
Self::Buffer(_) => AdapterErrorKind::Buffer,
Self::UnsupportedBackend { .. }
| Self::UnsupportedCudaRequest { .. }
| Self::CudaUnavailable => AdapterErrorKind::Unsupported,
Self::Decode(_)
| Self::NativeDecode { .. }
| Self::HostAllocationFailed { .. }
| Self::HostAllocationTooLarge { .. } => AdapterErrorKind::Other,
#[cfg(feature = "cuda-runtime")]
Self::CudaRuntime { .. } | Self::CudaCleanupFailed { .. } => AdapterErrorKind::Other,
}
}
}
#[doc(hidden)]
impl CodecError for Error {
fn is_truncated(&self) -> bool {
matches!(
self,
Self::NativeDecode { source, .. } if source.is_decode_truncated()
) || adapter_error_is_truncated(self)
}
fn is_not_implemented(&self) -> bool {
adapter_error_is_not_implemented(self)
}
fn is_unsupported(&self) -> bool {
matches!(
self,
Self::NativeDecode { source, .. } if source.is_unsupported()
) || adapter_error_is_unsupported(self)
}
fn is_buffer_error(&self) -> bool {
adapter_error_is_buffer_error(self)
}
}
#[cfg_attr(
not(any(test, feature = "cuda-runtime")),
expect(
dead_code,
reason = "native decode translation is used by CUDA decode and its tests"
)
)]
pub(crate) fn native_decode_error(error: NativeDecodeError) -> Error {
Error::NativeDecode {
context: "native JPEG 2000 backend failed",
source: NativeBackendError::decode(error),
}
}
#[cfg(test)]
mod tests {
use j2k_core::CodecError;
use j2k_native::{
DecodeError as NativeDecodeError, DecodingError as NativeDecodingError,
DirectPlanUnsupportedReason as NativeDirectPlanUnsupportedReason,
};
#[cfg(feature = "cuda-runtime")]
use super::combine_cuda_cleanup_errors;
use super::{native_decode_error, Error, NativeBackendError};
#[cfg(feature = "cuda-runtime")]
use j2k_cuda_runtime::CudaError;
#[cfg(feature = "cuda-runtime")]
fn runtime_error(message: &str) -> Error {
Error::CudaRuntime {
source: CudaError::StatePoisoned {
message: message.to_string(),
},
}
}
#[test]
fn host_allocation_failure_is_an_operational_error_not_buffer_misuse() {
let error = Error::HostAllocationFailed {
bytes: 4096,
what: "test staging",
};
assert!(!error.is_buffer_error());
assert!(!error.is_unsupported());
assert!(error.to_string().contains("4096"));
}
#[test]
fn host_capacity_failure_preserves_actual_phase_budget() {
let error = Error::HostAllocationTooLarge {
requested: 17,
cap: 16,
what: "test phase",
};
assert!(!error.is_buffer_error());
assert!(!error.is_unsupported());
assert!(error.to_string().contains("17"));
assert!(error.to_string().contains("16"));
}
#[test]
fn native_decode_unsupported_error_keeps_codec_classification() {
let error = native_decode_error(NativeDecodeError::Decoding(
NativeDecodingError::UnsupportedFeature("test feature"),
));
assert!(error.is_unsupported());
assert!(!error.is_truncated());
assert!(!error.is_not_implemented());
}
#[test]
fn native_decode_direct_plan_error_keeps_codec_classification() {
let error = native_decode_error(NativeDecodeError::Decoding(
NativeDecodingError::DirectPlanUnsupported(
NativeDirectPlanUnsupportedReason::ColorSingleTileCodestream,
),
));
assert!(error.is_unsupported());
assert!(!error.is_truncated());
assert!(!error.is_not_implemented());
}
#[test]
fn native_decode_unexpected_eof_keeps_codec_classification() {
let error = native_decode_error(NativeDecodeError::Decoding(
NativeDecodingError::UnexpectedEof,
));
assert!(error.is_truncated());
assert!(!error.is_unsupported());
}
#[test]
fn native_decode_resource_errors_preserve_typed_sources() {
let sources = [
NativeDecodeError::AllocationTooLarge {
what: "CUDA decode fixture",
requested: 9,
cap: 8,
},
NativeDecodeError::HostAllocationFailed {
what: "CUDA decode fixture",
bytes: 7,
},
];
for source in sources {
let error = native_decode_error(source);
assert!(matches!(
&error,
Error::NativeDecode {
context: "native JPEG 2000 backend failed",
source: stored,
} if stored == &NativeBackendError::decode(source)
));
let opaque = core::error::Error::source(&error).expect("opaque adapter source");
assert!(opaque.downcast_ref::<NativeBackendError>().is_some());
let concrete = opaque.source().expect("concrete native decode source");
assert_eq!(concrete.downcast_ref::<NativeDecodeError>(), Some(&source));
assert!(!error.is_buffer_error());
assert!(!error.is_unsupported());
}
}
#[cfg(feature = "cuda-runtime")]
#[test]
fn cuda_cleanup_failure_preserves_both_runtime_diagnostics() {
let combined = combine_cuda_cleanup_errors(
runtime_error("primary launch failure"),
runtime_error("follow-up synchronization failure"),
);
assert!(matches!(&combined, Error::CudaCleanupFailed { .. }));
let rendered = combined.to_string();
assert!(rendered.contains("primary launch failure"));
assert!(rendered.contains("follow-up synchronization failure"));
assert!(!combined.is_unsupported());
}
#[cfg(feature = "cuda-runtime")]
#[test]
fn cuda_cleanup_failure_preserves_non_runtime_primary_and_blocks_fallback() {
let combined = combine_cuda_cleanup_errors(
Error::UnsupportedCudaRequest {
reason: "invalid prepared store",
},
runtime_error("synchronization failure"),
);
assert!(matches!(&combined, Error::CudaCleanupFailed { .. }));
let rendered = combined.to_string();
assert!(rendered.contains("invalid prepared store"));
assert!(rendered.contains("synchronization failure"));
assert!(!combined.is_unsupported());
}
}