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    /// Component planes reused without zeroing were not written in full, so
289    /// samples from a previous image would have reached the output.
290    IncompletePlaneStore,
291}
292
293/// Errors related to color space and component handling.
294#[derive(Debug, Clone, Copy, PartialEq, Eq)]
295#[non_exhaustive]
296pub enum ColorError {
297    /// Multi-component transform failed.
298    Mct,
299    /// Failed to resolve palette indices.
300    PaletteResolutionFailed,
301    /// Failed to convert from sYCC to RGB.
302    SyccConversionFailed,
303    /// Failed to convert from LAB to RGB.
304    LabConversionFailed,
305}
306
307/// Structured reasons why a direct JPEG 2000 device plan cannot be built.
308#[derive(Debug, Clone, Copy, PartialEq, Eq)]
309#[non_exhaustive]
310pub enum DirectPlanUnsupportedReason {
311    /// Grayscale direct plans require grayscale images without alpha.
312    GrayscaleImageWithoutAlpha,
313    /// Grayscale direct plans require a single-tile codestream.
314    GrayscaleSingleTileCodestream,
315    /// Grayscale direct plans require a single-component codestream.
316    GrayscaleSingleComponentCodestream,
317    /// Color direct plans require RGB images without alpha.
318    ColorRgbImageWithoutAlpha,
319    /// Color direct plans require a single-tile codestream.
320    ColorSingleTileCodestream,
321    /// Color direct plans require three RGB components.
322    ColorThreeComponentRgbCodestream,
323    /// RGBA direct plans require an RGB image with explicit alpha.
324    RgbaRgbImageWithAlpha,
325    /// RGBA direct plans require four components in semantic R, G, B, A order.
326    RgbaFourComponentRgbCodestream,
327    /// A direct component plan index did not exist.
328    ComponentIndexOutOfRange,
329    /// Direct component plans require unit-sampled components.
330    ComponentUnitSampled,
331    /// A direct component decomposition index did not exist.
332    ComponentDecompositionIndexOutOfRange,
333    /// Direct device plans do not yet represent classic and HT code-blocks in one sub-band.
334    MixedCodeBlockCoding,
335    /// Component-grid plans require a full unsigned origin-zero image without MCT.
336    ComponentGridFullImage,
337}
338
339impl fmt::Display for DecodeError {
340    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
341        match self {
342            Self::Format(e) => write!(f, "{e}"),
343            Self::Marker(e) => write!(f, "{e}"),
344            Self::Tile(e) => write!(f, "{e}"),
345            Self::Validation(e) => write!(f, "{e}"),
346            Self::Decoding(e) => write!(f, "{e}"),
347            Self::Color(e) => write!(f, "{e}"),
348            Self::AllocationTooLarge {
349                what,
350                requested,
351                cap,
352            } => write!(
353                f,
354                "{what} requires {requested} live host bytes, exceeding the {cap}-byte cap"
355            ),
356            Self::HostAllocationFailed { what, bytes } => {
357                write!(
358                    f,
359                    "host allocation failed for {bytes} bytes while allocating {what}"
360                )
361            }
362        }
363    }
364}
365
366impl fmt::Display for EncodeError {
367    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
368        match self {
369            Self::InvalidInput { what } => write!(f, "invalid encode input: {what}"),
370            Self::Unsupported { what } => write!(f, "unsupported encode request: {what}"),
371            Self::ArithmeticOverflow { what } => {
372                write!(f, "encode size overflow while planning {what}")
373            }
374            Self::AllocationTooLarge {
375                what,
376                requested,
377                cap,
378            } => write!(
379                f,
380                "{what} requires {requested} live host bytes, exceeding the {cap}-byte cap"
381            ),
382            Self::HostAllocationFailed { what, bytes } => {
383                write!(
384                    f,
385                    "host allocation failed for {bytes} bytes while allocating {what}"
386                )
387            }
388            Self::Accelerator { operation, source } => {
389                write!(f, "encode accelerator failed during {operation}: {source}")
390            }
391            Self::CodestreamValidation { detail } => {
392                write!(f, "generated codestream validation failed: {detail}")
393            }
394            Self::InternalInvariant { what } => {
395                write!(f, "native encode invariant failed: {what}")
396            }
397        }
398    }
399}
400
401impl fmt::Display for FormatError {
402    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
403        match self {
404            Self::TooShort { need, have } => {
405                write!(f, "input too short: need {need} bytes, have {have}")
406            }
407            Self::TruncatedAt { offset, segment } => {
408                write!(
409                    f,
410                    "input truncated at offset {offset} while reading {segment}"
411                )
412            }
413            Self::InvalidSignature => write!(f, "invalid JP2 signature"),
414            Self::InvalidFileType => write!(f, "invalid JP2 file type"),
415            Self::InvalidBox => write!(f, "invalid JP2 box"),
416            Self::MissingRequiredBox(box_type) => write!(f, "missing required JP2 box {box_type}"),
417            Self::MissingCodestream => write!(f, "missing codestream data"),
418            Self::Unsupported => write!(f, "unsupported JP2 image"),
419        }
420    }
421}
422
423impl fmt::Display for MarkerError {
424    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
425        match self {
426            Self::Invalid => write!(f, "invalid marker"),
427            Self::Unsupported => write!(f, "unsupported marker"),
428            Self::Expected(marker) => write!(f, "expected {marker} marker"),
429            Self::Missing(marker) => write!(f, "missing {marker} marker"),
430            Self::ParseFailure(marker) => write!(f, "failed to parse {marker} marker"),
431        }
432    }
433}
434
435impl fmt::Display for TileError {
436    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
437        match self {
438            Self::Invalid => write!(f, "image contains no tiles"),
439            Self::InvalidIndex => write!(f, "invalid tile index in tile-part header"),
440            Self::InvalidOffsets => write!(f, "invalid tile offsets"),
441            Self::PpmPptConflict => {
442                write!(
443                    f,
444                    "PPT marker present when PPM marker exists in main header"
445                )
446            }
447        }
448    }
449}
450
451impl fmt::Display for ValidationError {
452    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
453        match self {
454            Self::InvalidDimensions => write!(f, "invalid image dimensions"),
455            Self::ImageTooLarge => write!(f, "image is too large"),
456            Self::TooManyChannels => write!(f, "image has too many channels"),
457            Self::TooManyTiles => write!(f, "image has too many tiles"),
458            Self::InvalidComponentMetadata => write!(f, "invalid component metadata"),
459            Self::InvalidChannelDefinition => write!(f, "invalid channel definition"),
460            Self::InvalidProgressionOrder => write!(f, "invalid progression order"),
461            Self::InvalidTransformation => write!(f, "invalid transformation type"),
462            Self::InvalidQuantizationStyle => write!(f, "invalid quantization style"),
463            Self::MissingPrecinctExponents => {
464                write!(f, "missing exponents for precinct sizes")
465            }
466            Self::InsufficientExponents => {
467                write!(f, "not enough exponents provided in header")
468            }
469            Self::MissingStepSize => write!(f, "missing exponent step size"),
470            Self::InvalidExponents => write!(f, "invalid quantization exponents"),
471        }
472    }
473}
474
475impl fmt::Display for DecodingError {
476    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
477        match self {
478            Self::CodeBlockDecodeFailure => write!(f, "failed to decode code-block"),
479            Self::CodeBlockDecodeFailureWithContext(context) => {
480                write!(f, "failed to decode code-block: {context}")
481            }
482            Self::DirectPlanUnsupported(reason) => {
483                write!(f, "unsupported decoding feature: {reason}")
484            }
485            Self::UnsupportedFeature(feature) => {
486                write!(f, "unsupported decoding feature: {feature}")
487            }
488            Self::TooManyBitplanes => write!(f, "number of bitplanes is too large"),
489            Self::TooManyCodingPasses => {
490                write!(f, "code-block contains too many coding passes")
491            }
492            Self::InvalidBitplaneCount => write!(f, "invalid number of bitplanes"),
493            Self::InvalidPrecinct => write!(f, "a precinct was invalid"),
494            Self::PacketParseFailure(context) => {
495                write!(f, "failed to parse JPEG 2000 packet data: {context}")
496            }
497            Self::InvalidProgressionIterator => {
498                write!(f, "a progression iterator was invalid")
499            }
500            Self::UnexpectedEof => write!(f, "unexpected end of data"),
501            Self::OutputBufferTooSmall => write!(f, "output buffer is too small"),
502            Self::HostAllocationFailed => write!(f, "host decode workspace allocation failed"),
503            Self::IncompletePlaneStore => {
504                write!(
505                    f,
506                    "component planes reused without zeroing were not fully written"
507                )
508            }
509        }
510    }
511}
512
513impl fmt::Display for DirectPlanUnsupportedReason {
514    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
515        f.write_str(direct_plan_unsupported_what(*self))
516    }
517}
518
519const fn direct_plan_unsupported_what(reason: DirectPlanUnsupportedReason) -> &'static str {
520    match reason {
521        DirectPlanUnsupportedReason::GrayscaleImageWithoutAlpha => {
522            "direct grayscale plan only supports grayscale images without alpha"
523        }
524        DirectPlanUnsupportedReason::GrayscaleSingleTileCodestream => {
525            "direct grayscale plan only supports single-tile codestreams"
526        }
527        DirectPlanUnsupportedReason::GrayscaleSingleComponentCodestream => {
528            "direct grayscale plan only supports single-component codestreams"
529        }
530        DirectPlanUnsupportedReason::ColorRgbImageWithoutAlpha => {
531            "direct color plan only supports RGB images without alpha"
532        }
533        DirectPlanUnsupportedReason::ColorSingleTileCodestream => {
534            "direct color plan only supports single-tile codestreams"
535        }
536        DirectPlanUnsupportedReason::ColorThreeComponentRgbCodestream => {
537            "direct color plan only supports three-component RGB codestreams"
538        }
539        DirectPlanUnsupportedReason::RgbaRgbImageWithAlpha => {
540            "direct RGBA plan only supports RGB images with explicit alpha"
541        }
542        DirectPlanUnsupportedReason::RgbaFourComponentRgbCodestream => {
543            "direct RGBA plan only supports four-component RGB-alpha codestreams"
544        }
545        DirectPlanUnsupportedReason::ComponentIndexOutOfRange => {
546            "direct component plan index is out of range"
547        }
548        DirectPlanUnsupportedReason::ComponentGridFullImage => {
549            "component-grid plans require a full unsigned origin-zero image without MCT"
550        }
551        DirectPlanUnsupportedReason::ComponentUnitSampled => {
552            "direct component plan only supports unit-sampled components"
553        }
554        DirectPlanUnsupportedReason::ComponentDecompositionIndexOutOfRange => {
555            "direct component decomposition index is out of range"
556        }
557        DirectPlanUnsupportedReason::MixedCodeBlockCoding => {
558            "direct device plan does not support mixed classic and HT code-block coding in one sub-band"
559        }
560    }
561}
562
563impl fmt::Display for ColorError {
564    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
565        match self {
566            Self::Mct => write!(f, "multi-component transform failed"),
567            Self::PaletteResolutionFailed => write!(f, "failed to resolve palette indices"),
568            Self::SyccConversionFailed => write!(f, "failed to convert from sYCC to RGB"),
569            Self::LabConversionFailed => write!(f, "failed to convert from LAB to RGB"),
570        }
571    }
572}
573
574impl core::error::Error for DecodeError {}
575impl core::error::Error for EncodeError {
576    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
577        match self {
578            Self::Accelerator { source, .. } => Some(source),
579            _ => None,
580        }
581    }
582}
583impl core::error::Error for FormatError {}
584impl core::error::Error for MarkerError {}
585impl core::error::Error for TileError {}
586impl core::error::Error for ValidationError {}
587impl core::error::Error for DecodingError {}
588impl core::error::Error for DirectPlanUnsupportedReason {}
589impl core::error::Error for ColorError {}
590
591impl From<FormatError> for DecodeError {
592    fn from(e: FormatError) -> Self {
593        Self::Format(e)
594    }
595}
596
597impl From<MarkerError> for DecodeError {
598    fn from(e: MarkerError) -> Self {
599        Self::Marker(e)
600    }
601}
602
603impl From<TileError> for DecodeError {
604    fn from(e: TileError) -> Self {
605        Self::Tile(e)
606    }
607}
608
609impl From<ValidationError> for DecodeError {
610    fn from(e: ValidationError) -> Self {
611        Self::Validation(e)
612    }
613}
614
615impl From<j2k_types::DecodePlanAllocationError> for DecodeError {
616    fn from(_: j2k_types::DecodePlanAllocationError) -> Self {
617        Self::Validation(ValidationError::ImageTooLarge)
618    }
619}
620
621impl From<DecodingError> for DecodeError {
622    fn from(e: DecodingError) -> Self {
623        Self::Decoding(e)
624    }
625}
626
627impl From<ColorError> for DecodeError {
628    fn from(e: ColorError) -> Self {
629        Self::Color(e)
630    }
631}
632
633/// Result type for JPEG 2000 decoding operations.
634pub type Result<T> = core::result::Result<T, DecodeError>;
635
636/// Result type for JPEG 2000 and HTJ2K encoding operations.
637pub type EncodeResult<T> = core::result::Result<T, EncodeError>;
638
639macro_rules! bail {
640    ($err:expr) => {
641        return Err($err.into())
642    };
643}
644
645macro_rules! err {
646    ($err:expr) => {
647        Err($err.into())
648    };
649}
650
651pub(crate) use bail;
652pub(crate) use err;
653
654#[cfg(test)]
655mod classification_tests {
656    use alloc::string::ToString;
657
658    use super::{
659        DecodeError, DecodeErrorClass, DecodingError, DirectPlanUnsupportedReason, EncodeError,
660        FormatError, MarkerError, ValidationError,
661    };
662
663    #[test]
664    fn facade_classification_preserves_structured_input_and_support_details() {
665        let cases = [
666            (
667                DecodeError::Format(FormatError::TooShort { need: 9, have: 3 }),
668                DecodeErrorClass::InputTooShort { need: 9, have: 3 },
669            ),
670            (
671                DecodeError::Format(FormatError::TruncatedAt {
672                    offset: 17,
673                    segment: "SIZ",
674                }),
675                DecodeErrorClass::InputTruncatedAt {
676                    offset: 17,
677                    segment: "SIZ",
678                },
679            ),
680            (
681                DecodeError::Format(FormatError::Unsupported),
682                DecodeErrorClass::Unsupported {
683                    what: "JP2 image format",
684                },
685            ),
686            (
687                DecodeError::Marker(MarkerError::Unsupported),
688                DecodeErrorClass::Unsupported {
689                    what: "JPEG 2000 marker",
690                },
691            ),
692            (
693                DecodeError::Decoding(DecodingError::UnsupportedFeature("packet marker")),
694                DecodeErrorClass::Unsupported {
695                    what: "packet marker",
696                },
697            ),
698            (
699                DecodeError::Decoding(DecodingError::UnexpectedEof),
700                DecodeErrorClass::InputTruncatedAt {
701                    offset: 0,
702                    segment: "JPEG 2000 entropy data",
703                },
704            ),
705            (
706                DecodeError::Validation(ValidationError::InvalidDimensions),
707                DecodeErrorClass::Backend,
708            ),
709        ];
710
711        for (error, expected) in cases {
712            assert_eq!(error.classify(), expected, "{error}");
713        }
714    }
715
716    #[test]
717    fn direct_plan_classification_and_display_share_the_same_label() {
718        let reason = DirectPlanUnsupportedReason::ColorThreeComponentRgbCodestream;
719        let error = DecodeError::Decoding(DecodingError::DirectPlanUnsupported(reason));
720        let DecodeErrorClass::Unsupported { what } = error.classify() else {
721            panic!("direct-plan errors must classify as unsupported");
722        };
723
724        assert_eq!(what, reason.to_string());
725    }
726
727    #[test]
728    fn encode_resource_errors_keep_cap_and_allocator_failures_distinct() {
729        let cap_error = EncodeError::AllocationTooLarge {
730            what: "Tier-2 packet assembly",
731            requested: 513,
732            cap: 512,
733        };
734        let allocation_error = EncodeError::HostAllocationFailed {
735            what: "Tier-2 packet body",
736            bytes: 511,
737        };
738
739        assert!(cap_error.to_string().contains("513"));
740        assert!(cap_error.to_string().contains("512-byte cap"));
741        assert!(allocation_error.to_string().contains("511"));
742        assert_ne!(cap_error, allocation_error);
743    }
744}