use alloc::string::String;
use core::fmt;
use thiserror::Error;
use whereat::At;
use whereat::at;
pub type Result<T> = core::result::Result<T, Error>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScanRead<T> {
Value(T),
EndOfScan,
Truncated,
}
#[allow(dead_code)]
impl<T> ScanRead<T> {
#[inline]
pub fn unwrap_or(self, default: T) -> T {
match self {
Self::Value(v) => v,
Self::EndOfScan | Self::Truncated => default,
}
}
#[inline]
pub fn unwrap_or_else<F: FnOnce() -> T>(self, f: F) -> T {
match self {
Self::Value(v) => v,
Self::EndOfScan | Self::Truncated => f(),
}
}
#[inline]
pub fn is_end_of_scan(&self) -> bool {
matches!(self, Self::EndOfScan)
}
#[inline]
pub fn is_truncated(&self) -> bool {
matches!(self, Self::Truncated)
}
#[inline]
pub fn is_value(&self) -> bool {
matches!(self, Self::Value(_))
}
#[inline]
pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> ScanRead<U> {
match self {
Self::Value(v) => ScanRead::Value(f(v)),
Self::EndOfScan => ScanRead::EndOfScan,
Self::Truncated => ScanRead::Truncated,
}
}
}
pub type ScanResult<T> = Result<ScanRead<T>>;
#[derive(Debug, Clone, PartialEq, Error)]
#[non_exhaustive]
pub enum ArgumentError {
#[error("invalid dimensions {width}x{height}: {reason}")]
InvalidDimensions {
width: u32,
height: u32,
reason: &'static str,
},
#[error("invalid color format: {reason}")]
InvalidColorFormat { reason: &'static str },
#[error("invalid buffer size: expected {expected} bytes, got {actual}")]
InvalidBufferSize { expected: usize, actual: usize },
#[error("unsupported feature: {feature}")]
UnsupportedFeature { feature: &'static str },
#[error("pixel format {format:?} not supported")]
UnsupportedPixelFormat { format: crate::types::PixelFormat },
}
#[derive(Debug, Clone, PartialEq, Error)]
#[non_exhaustive]
pub enum ResourceError {
#[error("allocation of {bytes} bytes failed while {context}")]
AllocationFailed { bytes: usize, context: &'static str },
#[error("size calculation overflow while {context}")]
SizeOverflow { context: &'static str },
#[error("image too large: {pixels} pixels exceeds limit of {limit}")]
ImageTooLarge { pixels: u64, limit: u64 },
#[error("I/O error: {reason}")]
IoError { reason: String },
}
#[derive(Debug, Clone, PartialEq, Error)]
#[non_exhaustive]
pub enum ErrorKind {
#[error("invalid dimensions {width}x{height}: {reason}")]
InvalidDimensions {
width: u32,
height: u32,
reason: &'static str,
},
#[error("invalid color format: {reason}")]
InvalidColorFormat { reason: &'static str },
#[error("invalid buffer size: expected {expected} bytes, got {actual}")]
InvalidBufferSize { expected: usize, actual: usize },
#[error("unsupported feature: {feature}")]
UnsupportedFeature { feature: &'static str },
#[error("pixel format {format:?} not supported")]
UnsupportedPixelFormat { format: crate::types::PixelFormat },
#[error("allocation of {bytes} bytes failed while {context}")]
AllocationFailed { bytes: usize, context: &'static str },
#[error("size calculation overflow while {context}")]
SizeOverflow { context: &'static str },
#[error("image too large: {pixels} pixels exceeds limit of {limit}")]
ImageTooLarge { pixels: u64, limit: u64 },
#[error("I/O error: {reason}")]
IoError { reason: String },
#[error("ICC error: {0}")]
IccError(String),
#[error("internal error: {reason}")]
InternalError { reason: &'static str },
#[error("operation cancelled")]
Cancelled,
#[error("invalid JPEG data: {reason}")]
InvalidJpegData { reason: &'static str },
#[error("truncated data while {context}")]
TruncatedData { context: &'static str },
#[error("invalid marker 0x{marker:02X} while {context}")]
InvalidMarker { marker: u8, context: &'static str },
#[error("invalid Huffman table {table_idx}: {reason}")]
InvalidHuffmanTable { table_idx: u8, reason: &'static str },
#[error("invalid quantization table {table_idx}: {reason}")]
InvalidQuantTable { table_idx: u8, reason: &'static str },
#[error("too many scans: {count} exceeds limit of {limit}")]
TooManyScans { count: usize, limit: usize },
#[error("decode error: {0}")]
DecodeError(String),
#[error("invalid quality {value}: must be in {valid_range}")]
InvalidQuality {
value: f32,
valid_range: &'static str,
},
#[error("invalid scan script: {0}")]
InvalidScanScript(String),
#[error("invalid encoder configuration: {0}")]
InvalidConfig(String),
#[error("stride {stride} is too small for width {width} pixels")]
StrideTooSmall { width: u32, stride: usize },
#[error("pushed {pushed} rows but image height is only {height}")]
TooManyRows { height: u32, pushed: u32 },
#[error("encoding finished after {pushed} rows but image height is {height}")]
IncompleteImage { height: u32, pushed: u32 },
#[error("unsupported operation: {0}")]
UnsupportedOperation(zencodec::UnsupportedOperation),
}
impl ErrorKind {
pub fn as_argument_error(&self) -> Option<ArgumentError> {
match self {
Self::InvalidDimensions {
width,
height,
reason,
} => Some(ArgumentError::InvalidDimensions {
width: *width,
height: *height,
reason,
}),
Self::InvalidColorFormat { reason } => {
Some(ArgumentError::InvalidColorFormat { reason })
}
Self::InvalidBufferSize { expected, actual } => {
Some(ArgumentError::InvalidBufferSize {
expected: *expected,
actual: *actual,
})
}
Self::UnsupportedFeature { feature } => {
Some(ArgumentError::UnsupportedFeature { feature })
}
Self::UnsupportedPixelFormat { format } => {
Some(ArgumentError::UnsupportedPixelFormat { format: *format })
}
_ => None,
}
}
pub fn as_resource_error(&self) -> Option<ResourceError> {
match self {
Self::AllocationFailed { bytes, context } => Some(ResourceError::AllocationFailed {
bytes: *bytes,
context,
}),
Self::SizeOverflow { context } => Some(ResourceError::SizeOverflow { context }),
Self::ImageTooLarge { pixels, limit } => Some(ResourceError::ImageTooLarge {
pixels: *pixels,
limit: *limit,
}),
Self::IoError { reason } => Some(ResourceError::IoError {
reason: reason.clone(),
}),
_ => None,
}
}
}
impl From<ArgumentError> for ErrorKind {
fn from(err: ArgumentError) -> Self {
match err {
ArgumentError::InvalidDimensions {
width,
height,
reason,
} => Self::InvalidDimensions {
width,
height,
reason,
},
ArgumentError::InvalidColorFormat { reason } => Self::InvalidColorFormat { reason },
ArgumentError::InvalidBufferSize { expected, actual } => {
Self::InvalidBufferSize { expected, actual }
}
ArgumentError::UnsupportedFeature { feature } => Self::UnsupportedFeature { feature },
ArgumentError::UnsupportedPixelFormat { format } => {
Self::UnsupportedPixelFormat { format }
}
}
}
}
impl From<ResourceError> for ErrorKind {
fn from(err: ResourceError) -> Self {
match err {
ResourceError::AllocationFailed { bytes, context } => {
Self::AllocationFailed { bytes, context }
}
ResourceError::SizeOverflow { context } => Self::SizeOverflow { context },
ResourceError::ImageTooLarge { pixels, limit } => Self::ImageTooLarge { pixels, limit },
ResourceError::IoError { reason } => Self::IoError { reason },
}
}
}
#[derive(Debug)]
pub struct Error(pub At<ErrorKind>);
impl Error {
#[track_caller]
#[inline]
pub fn new(kind: ErrorKind) -> Self {
Self(at!(kind))
}
#[inline]
pub const fn new_untraced(kind: ErrorKind) -> Self {
Self(At::wrap(kind))
}
#[inline]
pub fn kind(&self) -> &ErrorKind {
self.0.error()
}
#[inline]
pub fn into_kind(self) -> ErrorKind {
self.0.decompose().0
}
#[inline]
pub fn inner(&self) -> &At<ErrorKind> {
&self.0
}
#[inline]
pub fn into_inner(self) -> At<ErrorKind> {
self.0
}
#[track_caller]
#[inline]
pub fn at(self) -> Self {
Self(self.0.at())
}
#[track_caller]
pub fn invalid_dimensions(width: u32, height: u32, reason: &'static str) -> Self {
Self::new(ErrorKind::InvalidDimensions {
width,
height,
reason,
})
}
#[track_caller]
pub fn invalid_color_format(reason: &'static str) -> Self {
Self::new(ErrorKind::InvalidColorFormat { reason })
}
#[track_caller]
pub fn invalid_buffer_size(expected: usize, actual: usize) -> Self {
Self::new(ErrorKind::InvalidBufferSize { expected, actual })
}
#[track_caller]
pub fn unsupported_feature(feature: &'static str) -> Self {
Self::new(ErrorKind::UnsupportedFeature { feature })
}
#[track_caller]
pub fn unsupported_pixel_format(format: crate::types::PixelFormat) -> Self {
Self::new(ErrorKind::UnsupportedPixelFormat { format })
}
#[track_caller]
pub fn allocation_failed(bytes: usize, context: &'static str) -> Self {
Self::new(ErrorKind::AllocationFailed { bytes, context })
}
#[track_caller]
pub fn size_overflow(context: &'static str) -> Self {
Self::new(ErrorKind::SizeOverflow { context })
}
#[track_caller]
pub fn image_too_large(pixels: u64, limit: u64) -> Self {
Self::new(ErrorKind::ImageTooLarge { pixels, limit })
}
#[track_caller]
pub fn io_error(reason: String) -> Self {
Self::new(ErrorKind::IoError { reason })
}
#[track_caller]
pub fn icc_error(reason: String) -> Self {
Self::new(ErrorKind::IccError(reason))
}
#[track_caller]
pub fn internal(reason: &'static str) -> Self {
Self::new(ErrorKind::InternalError { reason })
}
#[track_caller]
pub fn cancelled() -> Self {
Self::new(ErrorKind::Cancelled)
}
#[track_caller]
pub fn invalid_jpeg_data(reason: &'static str) -> Self {
Self::new(ErrorKind::InvalidJpegData { reason })
}
#[track_caller]
pub fn truncated_data(context: &'static str) -> Self {
Self::new(ErrorKind::TruncatedData { context })
}
#[track_caller]
pub fn invalid_marker(marker: u8, context: &'static str) -> Self {
Self::new(ErrorKind::InvalidMarker { marker, context })
}
#[track_caller]
pub fn invalid_huffman_table(table_idx: u8, reason: &'static str) -> Self {
Self::new(ErrorKind::InvalidHuffmanTable { table_idx, reason })
}
#[track_caller]
pub fn invalid_quant_table(table_idx: u8, reason: &'static str) -> Self {
Self::new(ErrorKind::InvalidQuantTable { table_idx, reason })
}
#[track_caller]
pub fn too_many_scans(count: usize, limit: usize) -> Self {
Self::new(ErrorKind::TooManyScans { count, limit })
}
#[track_caller]
pub fn decode_error(reason: String) -> Self {
Self::new(ErrorKind::DecodeError(reason))
}
#[track_caller]
pub fn invalid_quality(value: f32, valid_range: &'static str) -> Self {
Self::new(ErrorKind::InvalidQuality { value, valid_range })
}
#[track_caller]
pub fn invalid_scan_script(reason: String) -> Self {
Self::new(ErrorKind::InvalidScanScript(reason))
}
#[track_caller]
pub fn invalid_config(reason: String) -> Self {
Self::new(ErrorKind::InvalidConfig(reason))
}
#[track_caller]
pub fn stride_too_small(width: u32, stride: usize) -> Self {
Self::new(ErrorKind::StrideTooSmall { width, stride })
}
#[track_caller]
pub fn too_many_rows(height: u32, pushed: u32) -> Self {
Self::new(ErrorKind::TooManyRows { height, pushed })
}
#[track_caller]
pub fn incomplete_image(height: u32, pushed: u32) -> Self {
Self::new(ErrorKind::IncompleteImage { height, pushed })
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
impl core::error::Error for Error {
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
self.0.error().source()
}
}
impl PartialEq for Error {
fn eq(&self, other: &Self) -> bool {
self.0.error() == other.0.error()
}
}
impl From<ErrorKind> for Error {
#[track_caller]
#[inline]
fn from(kind: ErrorKind) -> Self {
Self::new(kind)
}
}
impl From<At<ErrorKind>> for Error {
#[inline]
fn from(at: At<ErrorKind>) -> Self {
Self(at)
}
}
impl From<Error> for At<ErrorKind> {
#[inline]
fn from(err: Error) -> Self {
err.0
}
}
impl From<ArgumentError> for Error {
#[track_caller]
fn from(err: ArgumentError) -> Self {
Self::new(err.into())
}
}
impl From<ResourceError> for Error {
#[track_caller]
fn from(err: ResourceError) -> Self {
Self::new(err.into())
}
}
impl From<enough::StopReason> for Error {
#[track_caller]
fn from(_: enough::StopReason) -> Self {
Self::cancelled()
}
}
impl From<zencodec::LimitExceeded> for Error {
#[track_caller]
fn from(err: zencodec::LimitExceeded) -> Self {
use zencodec::LimitExceeded;
match err {
LimitExceeded::Width { actual, .. } => {
Self::invalid_dimensions(actual, 0, "width exceeds limit")
}
LimitExceeded::Height { actual, .. } => {
Self::invalid_dimensions(0, actual, "height exceeds limit")
}
LimitExceeded::Pixels { actual, max } => Self::image_too_large(actual, max),
LimitExceeded::Memory { actual, max } => Self::new(ErrorKind::AllocationFailed {
bytes: actual as usize,
context: if max > 0 {
"memory limit exceeded"
} else {
"allocation failed"
},
}),
_ => Self::decode_error(format!("{err}")),
}
}
}
impl From<zencodec::UnsupportedOperation> for Error {
#[track_caller]
fn from(op: zencodec::UnsupportedOperation) -> Self {
Self::new(ErrorKind::UnsupportedOperation(op))
}
}
impl From<std::io::Error> for Error {
#[track_caller]
fn from(err: std::io::Error) -> Self {
Self::io_error(err.to_string())
}
}
#[cfg(feature = "ultrahdr")]
impl From<ultrahdr_core::Error> for Error {
#[track_caller]
fn from(err: ultrahdr_core::Error) -> Self {
use ultrahdr_core::Error as UhdrError;
match err {
UhdrError::Stopped(reason) => Self::from(reason),
UhdrError::InvalidDimensions(w, h) => {
Self::invalid_dimensions(w, h, "invalid dimensions for UltraHDR")
}
UhdrError::DimensionMismatch { .. } => Self::decode_error(err.to_string()),
UhdrError::AllocationFailed(bytes) => {
Self::allocation_failed(bytes, "UltraHDR operation")
}
UhdrError::LimitExceeded(msg) => Self::decode_error(msg),
_ => Self::decode_error(err.to_string()),
}
}
}
impl From<crate::foundation::aligned_alloc::AllocError> for Error {
#[track_caller]
fn from(err: crate::foundation::aligned_alloc::AllocError) -> Self {
match err {
crate::foundation::aligned_alloc::AllocError::OutOfMemory => {
Self::allocation_failed(0, "adaptive quantization")
}
crate::foundation::aligned_alloc::AllocError::Overflow => {
Self::size_overflow("adaptive quantization size calculation")
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use whereat::ResultAtExt;
#[test]
fn test_error_size() {
let size = core::mem::size_of::<Error>();
println!("\n=== ERROR SIZES ===");
println!("Error: {} bytes", size);
println!("ErrorKind: {} bytes", core::mem::size_of::<ErrorKind>());
println!(
"ArgumentError: {} bytes",
core::mem::size_of::<ArgumentError>()
);
println!(
"ResourceError: {} bytes",
core::mem::size_of::<ResourceError>()
);
assert!(size <= 48, "Error is {} bytes, consider optimizing", size);
}
#[test]
fn test_argument_error_display() {
let err = ArgumentError::InvalidDimensions {
width: 0,
height: 100,
reason: "width cannot be zero",
};
assert!(err.to_string().contains("width cannot be zero"));
}
#[test]
fn test_resource_error_display() {
let err = ResourceError::AllocationFailed {
bytes: 1024,
context: "allocating buffer",
};
assert!(err.to_string().contains("1024 bytes"));
}
#[test]
fn test_error_from_argument_error() {
let arg_err = ArgumentError::InvalidDimensions {
width: 0,
height: 100,
reason: "width cannot be zero",
};
let err: Error = arg_err.into();
assert!(matches!(err.kind(), ErrorKind::InvalidDimensions { .. }));
}
#[test]
fn test_error_has_trace() {
let err = Error::invalid_dimensions(0, 100, "width cannot be zero");
assert!(err.0.frame_count() >= 1);
}
#[test]
fn test_error_trace_propagation() {
fn inner() -> Result<()> {
Err(Error::invalid_dimensions(0, 100, "width cannot be zero"))
}
fn outer() -> Result<()> {
inner().map_err(|e| e.at())?;
Ok(())
}
let err = outer().unwrap_err();
assert!(
err.0.frame_count() >= 2,
"trace should have at least 2 entries (inner + outer), got {}",
err.0.frame_count()
);
}
#[test]
fn test_error_kind_is_error_trait() {
fn assert_error<E: core::error::Error>(_: &E) {}
let kind = ErrorKind::Cancelled;
assert_error(&kind);
}
#[test]
fn test_at_error_kind_roundtrip() {
let err = Error::cancelled();
let at: At<ErrorKind> = err.into();
let err2: Error = at.into();
assert_eq!(err2.kind(), &ErrorKind::Cancelled);
}
#[test]
fn test_result_at_ext() {
fn inner() -> core::result::Result<(), At<ErrorKind>> {
Err(at!(ErrorKind::Cancelled))
}
fn outer() -> core::result::Result<(), At<ErrorKind>> {
inner().at()?;
Ok(())
}
let err = outer().unwrap_err();
assert!(err.frame_count() >= 2);
}
#[test]
fn test_io_error_conversion() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let err: Error = io_err.into();
assert!(matches!(err.kind(), ErrorKind::IoError { .. }));
}
}