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