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