1use alloc::string::String;
10use core::fmt;
11use thiserror::Error;
12use whereat::{AtTrace, AtTraceBoxed, AtTraceable};
13
14pub type Result<T> = core::result::Result<T, Error>;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum ScanRead<T> {
28 Value(T),
30 EndOfScan,
33 Truncated,
36}
37
38#[allow(dead_code)]
41impl<T> ScanRead<T> {
42 #[inline]
44 pub fn unwrap_or(self, default: T) -> T {
45 match self {
46 Self::Value(v) => v,
47 Self::EndOfScan | Self::Truncated => default,
48 }
49 }
50
51 #[inline]
53 pub fn unwrap_or_else<F: FnOnce() -> T>(self, f: F) -> T {
54 match self {
55 Self::Value(v) => v,
56 Self::EndOfScan | Self::Truncated => f(),
57 }
58 }
59
60 #[inline]
62 pub fn is_end_of_scan(&self) -> bool {
63 matches!(self, Self::EndOfScan)
64 }
65
66 #[inline]
68 pub fn is_truncated(&self) -> bool {
69 matches!(self, Self::Truncated)
70 }
71
72 #[inline]
74 pub fn is_value(&self) -> bool {
75 matches!(self, Self::Value(_))
76 }
77
78 #[inline]
80 pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> ScanRead<U> {
81 match self {
82 Self::Value(v) => ScanRead::Value(f(v)),
83 Self::EndOfScan => ScanRead::EndOfScan,
84 Self::Truncated => ScanRead::Truncated,
85 }
86 }
87}
88
89pub type ScanResult<T> = Result<ScanRead<T>>;
96
97#[derive(Debug, Clone, PartialEq, Error)]
105#[non_exhaustive]
106pub enum ArgumentError {
107 #[error("invalid dimensions {width}x{height}: {reason}")]
109 InvalidDimensions {
110 width: u32,
111 height: u32,
112 reason: &'static str,
113 },
114
115 #[error("invalid color format: {reason}")]
117 InvalidColorFormat { reason: &'static str },
118
119 #[error("invalid buffer size: expected {expected} bytes, got {actual}")]
121 InvalidBufferSize { expected: usize, actual: usize },
122
123 #[error("unsupported feature: {feature}")]
125 UnsupportedFeature { feature: &'static str },
126
127 #[error("pixel format {format:?} not supported")]
129 UnsupportedPixelFormat { format: crate::types::PixelFormat },
130}
131
132#[derive(Debug, Clone, PartialEq, Error)]
136#[non_exhaustive]
137pub enum ResourceError {
138 #[error("allocation of {bytes} bytes failed while {context}")]
140 AllocationFailed { bytes: usize, context: &'static str },
141
142 #[error("size calculation overflow while {context}")]
144 SizeOverflow { context: &'static str },
145
146 #[error("image too large: {pixels} pixels exceeds limit of {limit}")]
148 ImageTooLarge { pixels: u64, limit: u64 },
149
150 #[error("I/O error: {reason}")]
152 IoError { reason: String },
153}
154
155#[derive(Debug, Clone, PartialEq)]
165#[non_exhaustive]
166pub enum ErrorKind {
167 InvalidDimensions {
170 width: u32,
171 height: u32,
172 reason: &'static str,
173 },
174 InvalidColorFormat { reason: &'static str },
176 InvalidBufferSize { expected: usize, actual: usize },
178 UnsupportedFeature { feature: &'static str },
180 UnsupportedPixelFormat { format: crate::types::PixelFormat },
182
183 AllocationFailed { bytes: usize, context: &'static str },
186 SizeOverflow { context: &'static str },
188 ImageTooLarge { pixels: u64, limit: u64 },
190 IoError { reason: String },
192
193 IccError(String),
196 InternalError { reason: &'static str },
198 Cancelled,
200
201 InvalidJpegData { reason: &'static str },
204 TruncatedData { context: &'static str },
206 InvalidMarker { marker: u8, context: &'static str },
208 InvalidHuffmanTable { table_idx: u8, reason: &'static str },
210 InvalidQuantTable { table_idx: u8, reason: &'static str },
212 TooManyScans { count: usize, limit: usize },
214 DecodeError(String),
216
217 InvalidQuality {
220 value: f32,
221 valid_range: &'static str,
222 },
223 InvalidScanScript(String),
225 InvalidConfig(String),
227 StrideTooSmall { width: u32, stride: usize },
229
230 TooManyRows { height: u32, pushed: u32 },
233 IncompleteImage { height: u32, pushed: u32 },
235}
236
237impl ErrorKind {
238 pub fn as_argument_error(&self) -> Option<ArgumentError> {
240 match self {
241 Self::InvalidDimensions {
242 width,
243 height,
244 reason,
245 } => Some(ArgumentError::InvalidDimensions {
246 width: *width,
247 height: *height,
248 reason,
249 }),
250 Self::InvalidColorFormat { reason } => {
251 Some(ArgumentError::InvalidColorFormat { reason })
252 }
253 Self::InvalidBufferSize { expected, actual } => {
254 Some(ArgumentError::InvalidBufferSize {
255 expected: *expected,
256 actual: *actual,
257 })
258 }
259 Self::UnsupportedFeature { feature } => {
260 Some(ArgumentError::UnsupportedFeature { feature })
261 }
262 Self::UnsupportedPixelFormat { format } => {
263 Some(ArgumentError::UnsupportedPixelFormat { format: *format })
264 }
265 _ => None,
266 }
267 }
268
269 pub fn as_resource_error(&self) -> Option<ResourceError> {
271 match self {
272 Self::AllocationFailed { bytes, context } => Some(ResourceError::AllocationFailed {
273 bytes: *bytes,
274 context,
275 }),
276 Self::SizeOverflow { context } => Some(ResourceError::SizeOverflow { context }),
277 Self::ImageTooLarge { pixels, limit } => Some(ResourceError::ImageTooLarge {
278 pixels: *pixels,
279 limit: *limit,
280 }),
281 Self::IoError { reason } => Some(ResourceError::IoError {
282 reason: reason.clone(),
283 }),
284 _ => None,
285 }
286 }
287}
288
289impl fmt::Display for ErrorKind {
290 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
291 match self {
292 Self::InvalidDimensions {
294 width,
295 height,
296 reason,
297 } => write!(f, "invalid dimensions {}x{}: {}", width, height, reason),
298 Self::InvalidColorFormat { reason } => write!(f, "invalid color format: {}", reason),
299 Self::InvalidBufferSize { expected, actual } => {
300 write!(
301 f,
302 "invalid buffer size: expected {} bytes, got {}",
303 expected, actual
304 )
305 }
306 Self::UnsupportedFeature { feature } => write!(f, "unsupported feature: {}", feature),
307 Self::UnsupportedPixelFormat { format } => {
308 write!(f, "pixel format {:?} not supported", format)
309 }
310
311 Self::AllocationFailed { bytes, context } => {
313 write!(f, "allocation of {} bytes failed while {}", bytes, context)
314 }
315 Self::SizeOverflow { context } => {
316 write!(f, "size calculation overflow while {}", context)
317 }
318 Self::ImageTooLarge { pixels, limit } => {
319 write!(
320 f,
321 "image too large: {} pixels exceeds limit of {}",
322 pixels, limit
323 )
324 }
325 Self::IoError { reason } => write!(f, "I/O error: {}", reason),
326
327 Self::IccError(reason) => write!(f, "ICC error: {}", reason),
329 Self::InternalError { reason } => write!(f, "internal error: {}", reason),
330 Self::Cancelled => write!(f, "operation cancelled"),
331
332 Self::InvalidJpegData { reason } => write!(f, "invalid JPEG data: {}", reason),
334 Self::TruncatedData { context } => write!(f, "truncated data while {}", context),
335 Self::InvalidMarker { marker, context } => {
336 write!(f, "invalid marker 0x{:02X} while {}", marker, context)
337 }
338 Self::InvalidHuffmanTable { table_idx, reason } => {
339 write!(f, "invalid Huffman table {}: {}", table_idx, reason)
340 }
341 Self::InvalidQuantTable { table_idx, reason } => {
342 write!(f, "invalid quantization table {}: {}", table_idx, reason)
343 }
344 Self::TooManyScans { count, limit } => {
345 write!(f, "too many scans: {} exceeds limit of {}", count, limit)
346 }
347 Self::DecodeError(reason) => write!(f, "decode error: {}", reason),
348
349 Self::InvalidQuality { value, valid_range } => {
351 write!(f, "invalid quality {}: must be in {}", value, valid_range)
352 }
353 Self::InvalidScanScript(reason) => write!(f, "invalid scan script: {}", reason),
354 Self::InvalidConfig(reason) => write!(f, "invalid encoder configuration: {}", reason),
355 Self::StrideTooSmall { width, stride } => {
356 write!(
357 f,
358 "stride {} is too small for width {} pixels",
359 stride, width
360 )
361 }
362 Self::TooManyRows { height, pushed } => {
363 write!(
364 f,
365 "pushed {} rows but image height is only {}",
366 pushed, height
367 )
368 }
369 Self::IncompleteImage { height, pushed } => {
370 write!(
371 f,
372 "encoding finished after {} rows but image height is {}",
373 pushed, height
374 )
375 }
376 }
377 }
378}
379
380impl From<ArgumentError> for ErrorKind {
385 fn from(err: ArgumentError) -> Self {
386 match err {
387 ArgumentError::InvalidDimensions {
388 width,
389 height,
390 reason,
391 } => Self::InvalidDimensions {
392 width,
393 height,
394 reason,
395 },
396 ArgumentError::InvalidColorFormat { reason } => Self::InvalidColorFormat { reason },
397 ArgumentError::InvalidBufferSize { expected, actual } => {
398 Self::InvalidBufferSize { expected, actual }
399 }
400 ArgumentError::UnsupportedFeature { feature } => Self::UnsupportedFeature { feature },
401 ArgumentError::UnsupportedPixelFormat { format } => {
402 Self::UnsupportedPixelFormat { format }
403 }
404 }
405 }
406}
407
408impl From<ResourceError> for ErrorKind {
409 fn from(err: ResourceError) -> Self {
410 match err {
411 ResourceError::AllocationFailed { bytes, context } => {
412 Self::AllocationFailed { bytes, context }
413 }
414 ResourceError::SizeOverflow { context } => Self::SizeOverflow { context },
415 ResourceError::ImageTooLarge { pixels, limit } => Self::ImageTooLarge { pixels, limit },
416 ResourceError::IoError { reason } => Self::IoError { reason },
417 }
418 }
419}
420
421#[derive(Debug)]
429pub struct Error {
430 kind: ErrorKind,
431 trace: AtTraceBoxed,
432}
433
434impl Error {
435 #[track_caller]
437 pub fn new(kind: ErrorKind) -> Self {
438 Self {
439 kind,
440 trace: AtTraceBoxed::capture(),
441 }
442 }
443
444 #[inline]
446 pub const fn new_untraced(kind: ErrorKind) -> Self {
447 Self {
448 kind,
449 trace: AtTraceBoxed::new(),
450 }
451 }
452
453 #[inline]
455 pub fn kind(&self) -> &ErrorKind {
456 &self.kind
457 }
458
459 #[inline]
461 pub fn into_kind(self) -> ErrorKind {
462 self.kind
463 }
464
465 #[track_caller]
471 pub fn invalid_dimensions(width: u32, height: u32, reason: &'static str) -> Self {
472 Self::new(ErrorKind::InvalidDimensions {
473 width,
474 height,
475 reason,
476 })
477 }
478
479 #[track_caller]
481 pub fn invalid_color_format(reason: &'static str) -> Self {
482 Self::new(ErrorKind::InvalidColorFormat { reason })
483 }
484
485 #[track_caller]
487 pub fn invalid_buffer_size(expected: usize, actual: usize) -> Self {
488 Self::new(ErrorKind::InvalidBufferSize { expected, actual })
489 }
490
491 #[track_caller]
493 pub fn unsupported_feature(feature: &'static str) -> Self {
494 Self::new(ErrorKind::UnsupportedFeature { feature })
495 }
496
497 #[track_caller]
499 pub fn unsupported_pixel_format(format: crate::types::PixelFormat) -> Self {
500 Self::new(ErrorKind::UnsupportedPixelFormat { format })
501 }
502
503 #[track_caller]
509 pub fn allocation_failed(bytes: usize, context: &'static str) -> Self {
510 Self::new(ErrorKind::AllocationFailed { bytes, context })
511 }
512
513 #[track_caller]
515 pub fn size_overflow(context: &'static str) -> Self {
516 Self::new(ErrorKind::SizeOverflow { context })
517 }
518
519 #[track_caller]
521 pub fn image_too_large(pixels: u64, limit: u64) -> Self {
522 Self::new(ErrorKind::ImageTooLarge { pixels, limit })
523 }
524
525 #[track_caller]
527 pub fn io_error(reason: String) -> Self {
528 Self::new(ErrorKind::IoError { reason })
529 }
530
531 #[track_caller]
537 pub fn icc_error(reason: String) -> Self {
538 Self::new(ErrorKind::IccError(reason))
539 }
540
541 #[track_caller]
543 pub fn internal(reason: &'static str) -> Self {
544 Self::new(ErrorKind::InternalError { reason })
545 }
546
547 #[track_caller]
549 pub fn cancelled() -> Self {
550 Self::new(ErrorKind::Cancelled)
551 }
552
553 #[track_caller]
559 pub fn invalid_jpeg_data(reason: &'static str) -> Self {
560 Self::new(ErrorKind::InvalidJpegData { reason })
561 }
562
563 #[track_caller]
565 pub fn truncated_data(context: &'static str) -> Self {
566 Self::new(ErrorKind::TruncatedData { context })
567 }
568
569 #[track_caller]
571 pub fn invalid_marker(marker: u8, context: &'static str) -> Self {
572 Self::new(ErrorKind::InvalidMarker { marker, context })
573 }
574
575 #[track_caller]
577 pub fn invalid_huffman_table(table_idx: u8, reason: &'static str) -> Self {
578 Self::new(ErrorKind::InvalidHuffmanTable { table_idx, reason })
579 }
580
581 #[track_caller]
583 pub fn invalid_quant_table(table_idx: u8, reason: &'static str) -> Self {
584 Self::new(ErrorKind::InvalidQuantTable { table_idx, reason })
585 }
586
587 #[track_caller]
589 pub fn too_many_scans(count: usize, limit: usize) -> Self {
590 Self::new(ErrorKind::TooManyScans { count, limit })
591 }
592
593 #[track_caller]
595 pub fn decode_error(reason: String) -> Self {
596 Self::new(ErrorKind::DecodeError(reason))
597 }
598
599 #[track_caller]
605 pub fn invalid_quality(value: f32, valid_range: &'static str) -> Self {
606 Self::new(ErrorKind::InvalidQuality { value, valid_range })
607 }
608
609 #[track_caller]
611 pub fn invalid_scan_script(reason: String) -> Self {
612 Self::new(ErrorKind::InvalidScanScript(reason))
613 }
614
615 #[track_caller]
617 pub fn invalid_config(reason: String) -> Self {
618 Self::new(ErrorKind::InvalidConfig(reason))
619 }
620
621 #[track_caller]
623 pub fn stride_too_small(width: u32, stride: usize) -> Self {
624 Self::new(ErrorKind::StrideTooSmall { width, stride })
625 }
626
627 #[track_caller]
629 pub fn too_many_rows(height: u32, pushed: u32) -> Self {
630 Self::new(ErrorKind::TooManyRows { height, pushed })
631 }
632
633 #[track_caller]
635 pub fn incomplete_image(height: u32, pushed: u32) -> Self {
636 Self::new(ErrorKind::IncompleteImage { height, pushed })
637 }
638}
639
640impl AtTraceable for Error {
641 fn trace_mut(&mut self) -> &mut AtTrace {
642 self.trace.get_or_insert_mut()
643 }
644
645 fn trace(&self) -> Option<&AtTrace> {
646 self.trace.as_ref()
647 }
648
649 fn fmt_message(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
650 fmt::Display::fmt(&self.kind, f)
651 }
652}
653
654impl fmt::Display for Error {
655 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
656 fmt::Display::fmt(&self.kind, f)
657 }
658}
659
660impl From<ArgumentError> for Error {
665 #[track_caller]
666 fn from(err: ArgumentError) -> Self {
667 Self::new(err.into())
668 }
669}
670
671impl From<ResourceError> for Error {
672 #[track_caller]
673 fn from(err: ResourceError) -> Self {
674 Self::new(err.into())
675 }
676}
677
678impl From<enough::StopReason> for Error {
679 #[track_caller]
680 fn from(_: enough::StopReason) -> Self {
681 Self::cancelled()
682 }
683}
684
685#[cfg(feature = "std")]
686impl std::error::Error for Error {}
687
688#[cfg(feature = "std")]
689impl From<std::io::Error> for Error {
690 #[track_caller]
691 fn from(err: std::io::Error) -> Self {
692 Self::io_error(err.to_string())
693 }
694}
695
696#[cfg(feature = "ultrahdr")]
697impl From<ultrahdr_core::Error> for Error {
698 #[track_caller]
699 fn from(err: ultrahdr_core::Error) -> Self {
700 use ultrahdr_core::Error as UhdrError;
701 match err {
702 UhdrError::Stopped(reason) => Self::from(reason),
703 UhdrError::InvalidDimensions(w, h) => {
704 Self::invalid_dimensions(w, h, "invalid dimensions for UltraHDR")
705 }
706 UhdrError::DimensionMismatch { .. } => Self::decode_error(err.to_string()),
707 UhdrError::AllocationFailed(bytes) => {
708 Self::allocation_failed(bytes, "UltraHDR operation")
709 }
710 UhdrError::LimitExceeded(msg) => Self::decode_error(msg),
711 _ => Self::decode_error(err.to_string()),
712 }
713 }
714}
715
716impl From<crate::foundation::aligned_alloc::AllocError> for Error {
717 #[track_caller]
718 fn from(err: crate::foundation::aligned_alloc::AllocError) -> Self {
719 match err {
720 crate::foundation::aligned_alloc::AllocError::OutOfMemory => {
721 Self::allocation_failed(0, "adaptive quantization")
722 }
723 crate::foundation::aligned_alloc::AllocError::Overflow => {
724 Self::size_overflow("adaptive quantization size calculation")
725 }
726 }
727 }
728}
729
730impl Clone for Error {
735 fn clone(&self) -> Self {
736 Self {
737 kind: self.kind.clone(),
738 trace: AtTraceBoxed::new(), }
740 }
741}
742
743impl PartialEq for Error {
744 fn eq(&self, other: &Self) -> bool {
745 self.kind == other.kind
746 }
747}
748
749#[cfg(test)]
754mod tests {
755 use super::*;
756 use whereat::ResultAtTraceableExt;
757
758 #[test]
759 fn test_error_size() {
760 let size = core::mem::size_of::<Error>();
761 println!("\n=== ERROR SIZES ===");
762 println!("Error: {} bytes", size);
763 println!("ErrorKind: {} bytes", core::mem::size_of::<ErrorKind>());
764 println!(
765 "ArgumentError: {} bytes",
766 core::mem::size_of::<ArgumentError>()
767 );
768 println!(
769 "ResourceError: {} bytes",
770 core::mem::size_of::<ResourceError>()
771 );
772 assert!(size <= 48, "Error is {} bytes, consider optimizing", size);
773 }
774
775 #[test]
776 fn test_argument_error_display() {
777 let err = ArgumentError::InvalidDimensions {
778 width: 0,
779 height: 100,
780 reason: "width cannot be zero",
781 };
782 assert!(err.to_string().contains("width cannot be zero"));
783 }
784
785 #[test]
786 fn test_resource_error_display() {
787 let err = ResourceError::AllocationFailed {
788 bytes: 1024,
789 context: "allocating buffer",
790 };
791 assert!(err.to_string().contains("1024 bytes"));
792 }
793
794 #[test]
795 fn test_error_from_argument_error() {
796 let arg_err = ArgumentError::InvalidDimensions {
797 width: 0,
798 height: 100,
799 reason: "width cannot be zero",
800 };
801 let err: Error = arg_err.into();
802 assert!(matches!(err.kind(), ErrorKind::InvalidDimensions { .. }));
803 }
804
805 #[test]
806 fn test_error_has_trace() {
807 let err = Error::invalid_dimensions(0, 100, "width cannot be zero");
808 assert!(!err.trace.is_empty());
809 }
810
811 #[test]
812 fn test_error_trace_propagation() {
813 fn inner() -> Result<()> {
814 Err(Error::invalid_dimensions(0, 100, "width cannot be zero"))
815 }
816
817 fn outer() -> Result<()> {
818 inner().at()?;
819 Ok(())
820 }
821
822 let err = outer().unwrap_err();
823 assert!(
824 err.trace.frame_count() >= 1,
825 "trace should have at least 1 entry"
826 );
827 }
828
829 #[cfg(feature = "std")]
830 #[test]
831 fn test_io_error_conversion() {
832 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
833 let err: Error = io_err.into();
834 assert!(matches!(err.kind(), ErrorKind::IoError { .. }));
835 }
836}