Skip to main content

j2k_native/
error.rs

1//! Error types for JPEG 2000 codec operations.
2
3use core::fmt;
4use j2k_types::J2kEncodeStageError;
5
6/// The main error type for JPEG 2000 decoding operations.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8#[non_exhaustive]
9pub enum DecodeError {
10    /// Errors related to JP2 file format and box parsing.
11    Format(FormatError),
12    /// Errors related to codestream markers.
13    Marker(MarkerError),
14    /// Errors related to tile processing.
15    Tile(TileError),
16    /// Errors related to image dimensions and validation.
17    Validation(ValidationError),
18    /// Errors related to decoding operations.
19    Decoding(DecodingError),
20    /// Errors related to color space and component handling.
21    Color(ColorError),
22    /// Simultaneously live decode/container allocations exceed the shared cap.
23    AllocationTooLarge {
24        /// Allocation family or phase being checked.
25        what: &'static str,
26        /// Requested live bytes, saturated on arithmetic overflow.
27        requested: usize,
28        /// Maximum permitted live bytes.
29        cap: usize,
30    },
31    /// The host allocator rejected a checked, cap-valid decode allocation.
32    HostAllocationFailed {
33        /// Allocation being attempted.
34        what: &'static str,
35        /// Requested allocation bytes, saturated on arithmetic overflow.
36        bytes: usize,
37    },
38}
39
40/// Backend-neutral classification used by codec adapters.
41///
42/// This preserves the small amount of structured information that adapters
43/// need without requiring each adapter to match the complete native error
44/// hierarchy independently.
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46#[non_exhaustive]
47pub enum DecodeErrorClass {
48    /// Input ended before a required fixed-size read.
49    InputTooShort {
50        /// Required byte count.
51        need: usize,
52        /// Available byte count.
53        have: usize,
54    },
55    /// Input ended while reading a named segment.
56    InputTruncatedAt {
57        /// Byte offset where truncation was detected.
58        offset: usize,
59        /// Stable segment label.
60        segment: &'static str,
61    },
62    /// The codestream or container uses an unsupported feature.
63    Unsupported {
64        /// Stable user-facing feature label.
65        what: &'static str,
66    },
67    /// All other native decoder failures.
68    Backend,
69}
70
71/// Error returned by native JPEG 2000 and HTJ2K encode operations.
72///
73/// The variants deliberately keep resource failures distinct from malformed
74/// requests, accelerator failures, and generated-codestream validation. This
75/// lets facade and transcode callers preserve actionable failure categories
76/// without parsing display strings.
77#[derive(Debug, PartialEq, Eq)]
78#[non_exhaustive]
79pub enum EncodeError {
80    /// Caller-provided samples, geometry, metadata, or options are invalid.
81    InvalidInput {
82        /// Stable description of the invalid condition.
83        what: &'static str,
84    },
85    /// The requested encode feature or shape is not implemented.
86    Unsupported {
87        /// Stable description of the unsupported condition.
88        what: &'static str,
89    },
90    /// Checked arithmetic overflowed while planning an encode phase.
91    ArithmeticOverflow {
92        /// Name of the value or phase whose size overflowed.
93        what: &'static str,
94    },
95    /// Simultaneously live host allocations would exceed the shared cap.
96    AllocationTooLarge {
97        /// Name of the encode phase being checked.
98        what: &'static str,
99        /// Checked requested live host bytes at the rejected allocation boundary.
100        requested: usize,
101        /// Maximum permitted live host bytes.
102        cap: usize,
103    },
104    /// The allocator could not reserve a checked, cap-valid host allocation.
105    HostAllocationFailed {
106        /// Name of the allocation that failed.
107        what: &'static str,
108        /// Requested allocation bytes, saturated on element-size overflow.
109        bytes: usize,
110    },
111    /// An optional encode-stage accelerator accepted work but failed it.
112    Accelerator {
113        /// Encode-stage operation that failed.
114        operation: &'static str,
115        /// Structured stage failure with an optional concrete backend source.
116        source: J2kEncodeStageError,
117    },
118    /// A generated codestream failed the requested validation contract.
119    CodestreamValidation {
120        /// Stable validation failure detail.
121        detail: &'static str,
122    },
123    /// Internal encode state violated an invariant.
124    InternalInvariant {
125        /// Stable description of the violated invariant.
126        what: &'static str,
127    },
128}
129
130impl DecodeError {
131    /// Classify this error for a facade or accelerator adapter.
132    #[must_use]
133    pub const fn classify(&self) -> DecodeErrorClass {
134        match *self {
135            Self::Format(FormatError::TooShort { need, have }) => {
136                DecodeErrorClass::InputTooShort { need, have }
137            }
138            Self::Format(FormatError::TruncatedAt { offset, segment }) => {
139                DecodeErrorClass::InputTruncatedAt { offset, segment }
140            }
141            Self::Format(FormatError::Unsupported) => DecodeErrorClass::Unsupported {
142                what: "JP2 image format",
143            },
144            Self::Marker(MarkerError::Unsupported) => DecodeErrorClass::Unsupported {
145                what: "JPEG 2000 marker",
146            },
147            Self::Decoding(DecodingError::DirectPlanUnsupported(reason)) => {
148                DecodeErrorClass::Unsupported {
149                    what: direct_plan_unsupported_what(reason),
150                }
151            }
152            Self::Decoding(DecodingError::UnsupportedFeature(what)) => {
153                DecodeErrorClass::Unsupported { what }
154            }
155            Self::Decoding(DecodingError::UnexpectedEof) => DecodeErrorClass::InputTruncatedAt {
156                offset: 0,
157                segment: "JPEG 2000 entropy data",
158            },
159            _ => DecodeErrorClass::Backend,
160        }
161    }
162}
163
164/// Errors related to JP2 file format and box parsing.
165#[derive(Debug, Clone, Copy, PartialEq, Eq)]
166#[non_exhaustive]
167pub enum FormatError {
168    /// Input ended before a required fixed-size box read.
169    TooShort {
170        /// Required byte count.
171        need: usize,
172        /// Available byte count.
173        have: usize,
174    },
175    /// Input ended while reading a named box segment.
176    TruncatedAt {
177        /// Byte offset where truncation was detected.
178        offset: usize,
179        /// Name of the segment being read.
180        segment: &'static str,
181    },
182    /// Invalid JP2 signature.
183    InvalidSignature,
184    /// Invalid JP2 file type.
185    InvalidFileType,
186    /// Invalid or malformed JP2 box.
187    InvalidBox,
188    /// Required JP2 box is absent.
189    MissingRequiredBox(&'static str),
190    /// Missing codestream data.
191    MissingCodestream,
192    /// Unsupported JP2 image format.
193    Unsupported,
194}
195
196/// Errors related to codestream markers.
197#[derive(Debug, Clone, Copy, PartialEq, Eq)]
198#[non_exhaustive]
199pub enum MarkerError {
200    /// Invalid marker encountered.
201    Invalid,
202    /// Unsupported marker encountered.
203    Unsupported,
204    /// Expected a specific marker.
205    Expected(&'static str),
206    /// Missing a required marker.
207    Missing(&'static str),
208    /// Failed to read or parse a marker.
209    ParseFailure(&'static str),
210}
211
212/// Errors related to tile processing.
213#[derive(Debug, Clone, Copy, PartialEq, Eq)]
214#[non_exhaustive]
215pub enum TileError {
216    /// Invalid image tile was encountered.
217    Invalid,
218    /// Invalid tile index in tile-part header.
219    InvalidIndex,
220    /// Invalid tile or image offsets.
221    InvalidOffsets,
222    /// PPT marker present when PPM marker exists in main header.
223    PpmPptConflict,
224}
225
226/// Errors related to image dimensions and validation.
227#[derive(Debug, Clone, Copy, PartialEq, Eq)]
228#[non_exhaustive]
229pub enum ValidationError {
230    /// Invalid image dimensions.
231    InvalidDimensions,
232    /// Image dimensions exceed supported limits.
233    ImageTooLarge,
234    /// Image has too many channels.
235    TooManyChannels,
236    /// The SIZ tile grid implies more tiles than any conforming codestream can address.
237    TooManyTiles,
238    /// Invalid component metadata.
239    InvalidComponentMetadata,
240    /// Invalid JP2 channel definition metadata.
241    InvalidChannelDefinition,
242    /// Invalid progression order.
243    InvalidProgressionOrder,
244    /// Invalid transformation type.
245    InvalidTransformation,
246    /// Invalid quantization style.
247    InvalidQuantizationStyle,
248    /// Missing exponents for precinct sizes.
249    MissingPrecinctExponents,
250    /// Not enough exponents provided in header.
251    InsufficientExponents,
252    /// Missing exponent step size.
253    MissingStepSize,
254    /// Invalid quantization exponents.
255    InvalidExponents,
256}
257
258/// Errors related to decoding operations.
259#[derive(Debug, Clone, Copy, PartialEq, Eq)]
260#[non_exhaustive]
261pub enum DecodingError {
262    /// An error occurred while decoding a code-block.
263    CodeBlockDecodeFailure,
264    /// A backend-specific code-block decode failure with user-visible context.
265    CodeBlockDecodeFailureWithContext(&'static str),
266    /// A direct-plan builder rejected an unsupported image or codestream shape.
267    DirectPlanUnsupported(DirectPlanUnsupportedReason),
268    /// The codestream uses a feature that this decoder does not implement yet.
269    UnsupportedFeature(&'static str),
270    /// Number of bitplanes in a code-block is too large.
271    TooManyBitplanes,
272    /// A code-block contains too many coding passes.
273    TooManyCodingPasses,
274    /// Invalid number of bitplanes in a code-block.
275    InvalidBitplaneCount,
276    /// A precinct was invalid.
277    InvalidPrecinct,
278    /// A packet header or its declared body could not be parsed completely.
279    PacketParseFailure(&'static str),
280    /// A progression iterator ver invalid.
281    InvalidProgressionIterator,
282    /// Unexpected end of data.
283    UnexpectedEof,
284    /// Caller-provided output buffer is too small for the decoded image.
285    OutputBufferTooSmall,
286    /// A bounded host decode workspace could not be allocated.
287    HostAllocationFailed,
288}
289
290/// Errors related to color space and component handling.
291#[derive(Debug, Clone, Copy, PartialEq, Eq)]
292#[non_exhaustive]
293pub enum ColorError {
294    /// Multi-component transform failed.
295    Mct,
296    /// Failed to resolve palette indices.
297    PaletteResolutionFailed,
298    /// Failed to convert from sYCC to RGB.
299    SyccConversionFailed,
300    /// Failed to convert from LAB to RGB.
301    LabConversionFailed,
302}
303
304/// Structured reasons why a direct JPEG 2000 device plan cannot be built.
305#[derive(Debug, Clone, Copy, PartialEq, Eq)]
306#[non_exhaustive]
307pub enum DirectPlanUnsupportedReason {
308    /// Grayscale direct plans require grayscale images without alpha.
309    GrayscaleImageWithoutAlpha,
310    /// Grayscale direct plans require a single-tile codestream.
311    GrayscaleSingleTileCodestream,
312    /// Grayscale direct plans require a single-component codestream.
313    GrayscaleSingleComponentCodestream,
314    /// Color direct plans require RGB images without alpha.
315    ColorRgbImageWithoutAlpha,
316    /// Color direct plans require a single-tile codestream.
317    ColorSingleTileCodestream,
318    /// Color direct plans require three RGB components.
319    ColorThreeComponentRgbCodestream,
320    /// RGBA direct plans require an RGB image with explicit alpha.
321    RgbaRgbImageWithAlpha,
322    /// RGBA direct plans require four components in semantic R, G, B, A order.
323    RgbaFourComponentRgbCodestream,
324    /// A direct component plan index did not exist.
325    ComponentIndexOutOfRange,
326    /// Direct component plans require unit-sampled components.
327    ComponentUnitSampled,
328    /// A direct component decomposition index did not exist.
329    ComponentDecompositionIndexOutOfRange,
330    /// Direct device plans do not yet represent classic and HT code-blocks in one sub-band.
331    MixedCodeBlockCoding,
332    /// Component-grid plans require a full unsigned origin-zero image without MCT.
333    ComponentGridFullImage,
334}
335
336impl fmt::Display for DecodeError {
337    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
338        match self {
339            Self::Format(e) => write!(f, "{e}"),
340            Self::Marker(e) => write!(f, "{e}"),
341            Self::Tile(e) => write!(f, "{e}"),
342            Self::Validation(e) => write!(f, "{e}"),
343            Self::Decoding(e) => write!(f, "{e}"),
344            Self::Color(e) => write!(f, "{e}"),
345            Self::AllocationTooLarge {
346                what,
347                requested,
348                cap,
349            } => write!(
350                f,
351                "{what} requires {requested} live host bytes, exceeding the {cap}-byte cap"
352            ),
353            Self::HostAllocationFailed { what, bytes } => {
354                write!(
355                    f,
356                    "host allocation failed for {bytes} bytes while allocating {what}"
357                )
358            }
359        }
360    }
361}
362
363impl fmt::Display for EncodeError {
364    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
365        match self {
366            Self::InvalidInput { what } => write!(f, "invalid encode input: {what}"),
367            Self::Unsupported { what } => write!(f, "unsupported encode request: {what}"),
368            Self::ArithmeticOverflow { what } => {
369                write!(f, "encode size overflow while planning {what}")
370            }
371            Self::AllocationTooLarge {
372                what,
373                requested,
374                cap,
375            } => write!(
376                f,
377                "{what} requires {requested} live host bytes, exceeding the {cap}-byte cap"
378            ),
379            Self::HostAllocationFailed { what, bytes } => {
380                write!(
381                    f,
382                    "host allocation failed for {bytes} bytes while allocating {what}"
383                )
384            }
385            Self::Accelerator { operation, source } => {
386                write!(f, "encode accelerator failed during {operation}: {source}")
387            }
388            Self::CodestreamValidation { detail } => {
389                write!(f, "generated codestream validation failed: {detail}")
390            }
391            Self::InternalInvariant { what } => {
392                write!(f, "native encode invariant failed: {what}")
393            }
394        }
395    }
396}
397
398impl fmt::Display for FormatError {
399    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
400        match self {
401            Self::TooShort { need, have } => {
402                write!(f, "input too short: need {need} bytes, have {have}")
403            }
404            Self::TruncatedAt { offset, segment } => {
405                write!(
406                    f,
407                    "input truncated at offset {offset} while reading {segment}"
408                )
409            }
410            Self::InvalidSignature => write!(f, "invalid JP2 signature"),
411            Self::InvalidFileType => write!(f, "invalid JP2 file type"),
412            Self::InvalidBox => write!(f, "invalid JP2 box"),
413            Self::MissingRequiredBox(box_type) => write!(f, "missing required JP2 box {box_type}"),
414            Self::MissingCodestream => write!(f, "missing codestream data"),
415            Self::Unsupported => write!(f, "unsupported JP2 image"),
416        }
417    }
418}
419
420impl fmt::Display for MarkerError {
421    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
422        match self {
423            Self::Invalid => write!(f, "invalid marker"),
424            Self::Unsupported => write!(f, "unsupported marker"),
425            Self::Expected(marker) => write!(f, "expected {marker} marker"),
426            Self::Missing(marker) => write!(f, "missing {marker} marker"),
427            Self::ParseFailure(marker) => write!(f, "failed to parse {marker} marker"),
428        }
429    }
430}
431
432impl fmt::Display for TileError {
433    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
434        match self {
435            Self::Invalid => write!(f, "image contains no tiles"),
436            Self::InvalidIndex => write!(f, "invalid tile index in tile-part header"),
437            Self::InvalidOffsets => write!(f, "invalid tile offsets"),
438            Self::PpmPptConflict => {
439                write!(
440                    f,
441                    "PPT marker present when PPM marker exists in main header"
442                )
443            }
444        }
445    }
446}
447
448impl fmt::Display for ValidationError {
449    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
450        match self {
451            Self::InvalidDimensions => write!(f, "invalid image dimensions"),
452            Self::ImageTooLarge => write!(f, "image is too large"),
453            Self::TooManyChannels => write!(f, "image has too many channels"),
454            Self::TooManyTiles => write!(f, "image has too many tiles"),
455            Self::InvalidComponentMetadata => write!(f, "invalid component metadata"),
456            Self::InvalidChannelDefinition => write!(f, "invalid channel definition"),
457            Self::InvalidProgressionOrder => write!(f, "invalid progression order"),
458            Self::InvalidTransformation => write!(f, "invalid transformation type"),
459            Self::InvalidQuantizationStyle => write!(f, "invalid quantization style"),
460            Self::MissingPrecinctExponents => {
461                write!(f, "missing exponents for precinct sizes")
462            }
463            Self::InsufficientExponents => {
464                write!(f, "not enough exponents provided in header")
465            }
466            Self::MissingStepSize => write!(f, "missing exponent step size"),
467            Self::InvalidExponents => write!(f, "invalid quantization exponents"),
468        }
469    }
470}
471
472impl fmt::Display for DecodingError {
473    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
474        match self {
475            Self::CodeBlockDecodeFailure => write!(f, "failed to decode code-block"),
476            Self::CodeBlockDecodeFailureWithContext(context) => {
477                write!(f, "failed to decode code-block: {context}")
478            }
479            Self::DirectPlanUnsupported(reason) => {
480                write!(f, "unsupported decoding feature: {reason}")
481            }
482            Self::UnsupportedFeature(feature) => {
483                write!(f, "unsupported decoding feature: {feature}")
484            }
485            Self::TooManyBitplanes => write!(f, "number of bitplanes is too large"),
486            Self::TooManyCodingPasses => {
487                write!(f, "code-block contains too many coding passes")
488            }
489            Self::InvalidBitplaneCount => write!(f, "invalid number of bitplanes"),
490            Self::InvalidPrecinct => write!(f, "a precinct was invalid"),
491            Self::PacketParseFailure(context) => {
492                write!(f, "failed to parse JPEG 2000 packet data: {context}")
493            }
494            Self::InvalidProgressionIterator => {
495                write!(f, "a progression iterator was invalid")
496            }
497            Self::UnexpectedEof => write!(f, "unexpected end of data"),
498            Self::OutputBufferTooSmall => write!(f, "output buffer is too small"),
499            Self::HostAllocationFailed => write!(f, "host decode workspace allocation failed"),
500        }
501    }
502}
503
504impl fmt::Display for DirectPlanUnsupportedReason {
505    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
506        f.write_str(direct_plan_unsupported_what(*self))
507    }
508}
509
510const fn direct_plan_unsupported_what(reason: DirectPlanUnsupportedReason) -> &'static str {
511    match reason {
512        DirectPlanUnsupportedReason::GrayscaleImageWithoutAlpha => {
513            "direct grayscale plan only supports grayscale images without alpha"
514        }
515        DirectPlanUnsupportedReason::GrayscaleSingleTileCodestream => {
516            "direct grayscale plan only supports single-tile codestreams"
517        }
518        DirectPlanUnsupportedReason::GrayscaleSingleComponentCodestream => {
519            "direct grayscale plan only supports single-component codestreams"
520        }
521        DirectPlanUnsupportedReason::ColorRgbImageWithoutAlpha => {
522            "direct color plan only supports RGB images without alpha"
523        }
524        DirectPlanUnsupportedReason::ColorSingleTileCodestream => {
525            "direct color plan only supports single-tile codestreams"
526        }
527        DirectPlanUnsupportedReason::ColorThreeComponentRgbCodestream => {
528            "direct color plan only supports three-component RGB codestreams"
529        }
530        DirectPlanUnsupportedReason::RgbaRgbImageWithAlpha => {
531            "direct RGBA plan only supports RGB images with explicit alpha"
532        }
533        DirectPlanUnsupportedReason::RgbaFourComponentRgbCodestream => {
534            "direct RGBA plan only supports four-component RGB-alpha codestreams"
535        }
536        DirectPlanUnsupportedReason::ComponentIndexOutOfRange => {
537            "direct component plan index is out of range"
538        }
539        DirectPlanUnsupportedReason::ComponentGridFullImage => {
540            "component-grid plans require a full unsigned origin-zero image without MCT"
541        }
542        DirectPlanUnsupportedReason::ComponentUnitSampled => {
543            "direct component plan only supports unit-sampled components"
544        }
545        DirectPlanUnsupportedReason::ComponentDecompositionIndexOutOfRange => {
546            "direct component decomposition index is out of range"
547        }
548        DirectPlanUnsupportedReason::MixedCodeBlockCoding => {
549            "direct device plan does not support mixed classic and HT code-block coding in one sub-band"
550        }
551    }
552}
553
554impl fmt::Display for ColorError {
555    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
556        match self {
557            Self::Mct => write!(f, "multi-component transform failed"),
558            Self::PaletteResolutionFailed => write!(f, "failed to resolve palette indices"),
559            Self::SyccConversionFailed => write!(f, "failed to convert from sYCC to RGB"),
560            Self::LabConversionFailed => write!(f, "failed to convert from LAB to RGB"),
561        }
562    }
563}
564
565impl core::error::Error for DecodeError {}
566impl core::error::Error for EncodeError {
567    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
568        match self {
569            Self::Accelerator { source, .. } => Some(source),
570            _ => None,
571        }
572    }
573}
574impl core::error::Error for FormatError {}
575impl core::error::Error for MarkerError {}
576impl core::error::Error for TileError {}
577impl core::error::Error for ValidationError {}
578impl core::error::Error for DecodingError {}
579impl core::error::Error for DirectPlanUnsupportedReason {}
580impl core::error::Error for ColorError {}
581
582impl From<FormatError> for DecodeError {
583    fn from(e: FormatError) -> Self {
584        Self::Format(e)
585    }
586}
587
588impl From<MarkerError> for DecodeError {
589    fn from(e: MarkerError) -> Self {
590        Self::Marker(e)
591    }
592}
593
594impl From<TileError> for DecodeError {
595    fn from(e: TileError) -> Self {
596        Self::Tile(e)
597    }
598}
599
600impl From<ValidationError> for DecodeError {
601    fn from(e: ValidationError) -> Self {
602        Self::Validation(e)
603    }
604}
605
606impl From<j2k_types::DecodePlanAllocationError> for DecodeError {
607    fn from(_: j2k_types::DecodePlanAllocationError) -> Self {
608        Self::Validation(ValidationError::ImageTooLarge)
609    }
610}
611
612impl From<DecodingError> for DecodeError {
613    fn from(e: DecodingError) -> Self {
614        Self::Decoding(e)
615    }
616}
617
618impl From<ColorError> for DecodeError {
619    fn from(e: ColorError) -> Self {
620        Self::Color(e)
621    }
622}
623
624/// Result type for JPEG 2000 decoding operations.
625pub type Result<T> = core::result::Result<T, DecodeError>;
626
627/// Result type for JPEG 2000 and HTJ2K encoding operations.
628pub type EncodeResult<T> = core::result::Result<T, EncodeError>;
629
630macro_rules! bail {
631    ($err:expr) => {
632        return Err($err.into())
633    };
634}
635
636macro_rules! err {
637    ($err:expr) => {
638        Err($err.into())
639    };
640}
641
642pub(crate) use bail;
643pub(crate) use err;
644
645#[cfg(test)]
646mod classification_tests {
647    use alloc::string::ToString;
648
649    use super::{
650        DecodeError, DecodeErrorClass, DecodingError, DirectPlanUnsupportedReason, EncodeError,
651        FormatError, MarkerError, ValidationError,
652    };
653
654    #[test]
655    fn facade_classification_preserves_structured_input_and_support_details() {
656        let cases = [
657            (
658                DecodeError::Format(FormatError::TooShort { need: 9, have: 3 }),
659                DecodeErrorClass::InputTooShort { need: 9, have: 3 },
660            ),
661            (
662                DecodeError::Format(FormatError::TruncatedAt {
663                    offset: 17,
664                    segment: "SIZ",
665                }),
666                DecodeErrorClass::InputTruncatedAt {
667                    offset: 17,
668                    segment: "SIZ",
669                },
670            ),
671            (
672                DecodeError::Format(FormatError::Unsupported),
673                DecodeErrorClass::Unsupported {
674                    what: "JP2 image format",
675                },
676            ),
677            (
678                DecodeError::Marker(MarkerError::Unsupported),
679                DecodeErrorClass::Unsupported {
680                    what: "JPEG 2000 marker",
681                },
682            ),
683            (
684                DecodeError::Decoding(DecodingError::UnsupportedFeature("packet marker")),
685                DecodeErrorClass::Unsupported {
686                    what: "packet marker",
687                },
688            ),
689            (
690                DecodeError::Decoding(DecodingError::UnexpectedEof),
691                DecodeErrorClass::InputTruncatedAt {
692                    offset: 0,
693                    segment: "JPEG 2000 entropy data",
694                },
695            ),
696            (
697                DecodeError::Validation(ValidationError::InvalidDimensions),
698                DecodeErrorClass::Backend,
699            ),
700        ];
701
702        for (error, expected) in cases {
703            assert_eq!(error.classify(), expected, "{error}");
704        }
705    }
706
707    #[test]
708    fn direct_plan_classification_and_display_share_the_same_label() {
709        let reason = DirectPlanUnsupportedReason::ColorThreeComponentRgbCodestream;
710        let error = DecodeError::Decoding(DecodingError::DirectPlanUnsupported(reason));
711        let DecodeErrorClass::Unsupported { what } = error.classify() else {
712            panic!("direct-plan errors must classify as unsupported");
713        };
714
715        assert_eq!(what, reason.to_string());
716    }
717
718    #[test]
719    fn encode_resource_errors_keep_cap_and_allocator_failures_distinct() {
720        let cap_error = EncodeError::AllocationTooLarge {
721            what: "Tier-2 packet assembly",
722            requested: 513,
723            cap: 512,
724        };
725        let allocation_error = EncodeError::HostAllocationFailed {
726            what: "Tier-2 packet body",
727            bytes: 511,
728        };
729
730        assert!(cap_error.to_string().contains("513"));
731        assert!(cap_error.to_string().contains("512-byte cap"));
732        assert!(allocation_error.to_string().contains("511"));
733        assert_ne!(cap_error, allocation_error);
734    }
735}