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