Skip to main content

mcproto_codec/
error.rs

1//! Errors reported by Minecraft protocol codecs.
2//!
3//! This module provides structured context for failures while reading and
4//! writing the protocol values implemented by `mcproto-codec`.
5
6use std::{error::Error, fmt, io};
7
8type BoxedError = Box<dyn Error + Send + Sync + 'static>;
9
10/// Identifies the protocol codec that reported an error.
11///
12/// A [`CodecError`] stores the codec that originally reported the error and may
13/// also store enclosing codecs as additional context. Protocol descriptions are
14/// based on the [Minecraft Java Edition protocol packet format].
15///
16/// Signed integer codecs use [two's-complement] representation.
17///
18/// [Minecraft Java Edition protocol packet format]: https://minecraft.wiki/w/Java_Edition_protocol/Packets
19/// [two's-complement]: https://en.wikipedia.org/wiki/Two%27s_complement
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21#[non_exhaustive]
22pub enum CodecKind {
23    /// A variable-length, two's-complement signed 32-bit integer.
24    ///
25    /// Values range from -2,147,483,648 through 2,147,483,647.
26    VarInt,
27    /// A variable-length, two's-complement signed 64-bit integer.
28    ///
29    /// Values range from -9,223,372,036,854,775,808 through
30    /// 9,223,372,036,854,775,807.
31    VarLong,
32    /// A complete Named Binary Tag value.
33    ///
34    /// The value is encoded and decoded using `fastnbt`.
35    Nbt,
36    /// A boolean encoded as `0x00` for false or `0x01` for true.
37    Boolean,
38    /// A two's-complement signed 8-bit integer from -128 through 127.
39    Byte,
40    /// An unsigned 8-bit integer from 0 through 255.
41    UnsignedByte,
42    /// A two's-complement signed 16-bit integer from -32,768 through 32,767.
43    Short,
44    /// An unsigned 16-bit integer from 0 through 65,535.
45    UnsignedShort,
46    /// A two's-complement signed 32-bit integer from -2,147,483,648 through
47    /// 2,147,483,647.
48    Int,
49    /// A two's-complement signed 64-bit integer from -9,223,372,036,854,775,808
50    /// through 9,223,372,036,854,775,807.
51    Long,
52    /// A big-endian IEEE-754 single-precision floating-point number.
53    Float,
54    /// A big-endian IEEE-754 double-precision floating-point number.
55    Double,
56    /// A block position packed into a 64-bit integer.
57    ///
58    /// The x, z, and y coordinates occupy 26, 26, and 12 bits respectively.
59    Position,
60    /// A rotation angle encoded in 1/256 turn steps.
61    Angle,
62    /// Three quantized doubles packed with a shared scale factor.
63    LpVec3,
64    /// An Int bit field controlling relative teleportation behavior.
65    TeleportFlags,
66    /// A named sound with an optional fixed playback range.
67    SoundEvent,
68    /// A direct chat type containing chat and narration decorations.
69    ChatType,
70    /// A chat type decoration containing a translation key, parameters, and style.
71    ChatDecoration,
72    /// A structure encoded field-by-field in declaration order.
73    TypeStruct,
74    /// An inventory item stack and its data component patch.
75    Slot,
76    /// An optional item stack whose added component values are CRC32C hashes.
77    HashedSlot,
78    /// A typed data component attached to an item stack.
79    DataComponent,
80    /// The payload of a typed data component.
81    StructuredComponent,
82    /// A typed, potentially recursive recipe slot display.
83    SlotDisplay,
84    /// Chunk-section sky and block lighting masks and arrays.
85    LightData,
86    /// Exactly 2048 packed bytes containing 4096 four-bit light values.
87    LightArray,
88    /// A 128-bit universally unique identifier.
89    Uuid,
90    /// A length-prefixed bit set of packed 64-bit words.
91    BitSet,
92    /// A fixed-length bit set of packed bytes.
93    FixedBitSet,
94    /// A value whose presence is determined by an enclosing protocol context.
95    Optional,
96    /// An optional value prefixed by an encoded boolean presence marker.
97    PrefixedOptional,
98    /// A boolean-selected value of one of two protocol types.
99    Either,
100    /// A UUID, username, and bounded list of profile properties.
101    GameProfile,
102    /// One name, value, and optional signature in a game profile.
103    GameProfileProperty,
104    /// A partial or complete game profile with optional skin overrides.
105    ResolvableProfile,
106    /// The unresolved identity fields of a resolvable profile.
107    PartialProfile,
108    /// A typed debug subscription event.
109    DebugSubscriptionEvent,
110    /// A typed debug subscription update.
111    DebugSubscriptionUpdate,
112    /// A payload selected by a debug subscription type.
113    DebugSubscriptionData,
114    /// One pathfinding node in debug subscription data.
115    DebugPathNode,
116    /// Structure and piece bounding boxes in debug subscription data.
117    DebugStructureInfo,
118    /// A typed client recipe display.
119    RecipeDisplay,
120    /// A shaped recipe's dimensions and rectangular ingredient array.
121    ShapedRecipeGrid,
122    /// A terminated sequence of indexed entity metadata values.
123    EntityMetadata,
124    /// One indexed value in an entity metadata sequence.
125    EntityMetadataEntry,
126    /// A value selected by an entity metadata type ID.
127    EntityMetadataValue,
128    /// A particle type ID and its type-specific payload.
129    Particle,
130    /// A source selected by a vibration particle's position-source type ID.
131    VibrationSource,
132    /// A non-negative ID in a protocol registry.
133    RegistryId,
134    /// A sequence whose element count is supplied by protocol context.
135    Array,
136    /// A raw sequence of bytes whose length is supplied by protocol context.
137    ByteArray,
138    /// A sequence prefixed by its element count as a VarInt.
139    PrefixedArray,
140    /// A value selected from a fixed protocol enumeration.
141    Enum,
142    /// A registry ID or an inline protocol value.
143    IdOr,
144    /// Registry IDs enumerated inline or referenced through a tag.
145    IdSet,
146    /// A UTF-8 string prefixed by its byte length as a VarInt.
147    ///
148    /// The protocol limits both the UTF-8 payload size and the number of UTF-16
149    /// code units. Supplementary [Unicode scalar values] count as two UTF-16
150    /// code units. The general protocol limit is 32,767 UTF-16 code units and
151    /// three UTF-8 bytes per permitted code unit; a particular field may impose
152    /// a lower limit.
153    ///
154    /// [Unicode scalar values]: https://www.unicode.org/glossary/#unicode_scalar_value
155    String,
156    /// A resource identifier encoded as a [`String`](Self::String).
157    ///
158    /// The namespace permits `[a-z0-9._-]`; the value permits
159    /// `[a-z0-9._/-]`. See the protocol's [identifier format] for details.
160    ///
161    /// [identifier format]: https://minecraft.wiki/w/Java_Edition_protocol/Packets#Identifier
162    Identifier,
163    /// A text component encoded as an NBT tag.
164    ///
165    /// Plain text-only components may use an NBT string tag. Components with
166    /// styling, events, or other data use an NBT compound tag. See the
167    /// [text component format] and [NBT specification].
168    ///
169    /// [text component format]: https://minecraft.wiki/w/Text_component_format
170    /// [NBT specification]: https://minecraft.wiki/w/NBT_format
171    TextComponent,
172    /// A text component encoded as JSON in a protocol string.
173    ///
174    /// Since Java Edition 1.20.3, the vanilla implementation permits up to
175    /// 262,144 UTF-16 code units when decoding but refuses to encode more than
176    /// 32,767. See the [text component format].
177    ///
178    /// [text component format]: https://minecraft.wiki/w/Text_component_format
179    JsonTextComponent,
180}
181
182/// Formats a codec kind using its protocol name, such as `VarInt` or `Boolean`.
183impl fmt::Display for CodecKind {
184    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
185        match self {
186            Self::VarInt => formatter.write_str("VarInt"),
187            Self::VarLong => formatter.write_str("VarLong"),
188            Self::Nbt => formatter.write_str("Nbt"),
189            Self::Boolean => formatter.write_str("Boolean"),
190            Self::Byte => formatter.write_str("Byte"),
191            Self::UnsignedByte => formatter.write_str("UnsignedByte"),
192            Self::Short => formatter.write_str("Short"),
193            Self::UnsignedShort => formatter.write_str("UnsignedShort"),
194            Self::Int => formatter.write_str("Int"),
195            Self::Long => formatter.write_str("Long"),
196            Self::Float => formatter.write_str("Float"),
197            Self::Double => formatter.write_str("Double"),
198            Self::Position => formatter.write_str("Position"),
199            Self::Angle => formatter.write_str("Angle"),
200            Self::LpVec3 => formatter.write_str("LpVec3"),
201            Self::TeleportFlags => formatter.write_str("Teleport Flags"),
202            Self::SoundEvent => formatter.write_str("Sound Event"),
203            Self::ChatType => formatter.write_str("Chat Type"),
204            Self::ChatDecoration => formatter.write_str("Chat Decoration"),
205            Self::TypeStruct => formatter.write_str("Type Struct"),
206            Self::Slot => formatter.write_str("Slot"),
207            Self::HashedSlot => formatter.write_str("Hashed Slot"),
208            Self::DataComponent => formatter.write_str("Data Component"),
209            Self::StructuredComponent => formatter.write_str("Structured Component"),
210            Self::SlotDisplay => formatter.write_str("Slot Display"),
211            Self::LightData => formatter.write_str("Light Data"),
212            Self::LightArray => formatter.write_str("Light Array"),
213            Self::Uuid => formatter.write_str("UUID"),
214            Self::BitSet => formatter.write_str("BitSet"),
215            Self::FixedBitSet => formatter.write_str("Fixed BitSet"),
216            Self::Optional => formatter.write_str("Optional"),
217            Self::PrefixedOptional => formatter.write_str("Prefixed Optional"),
218            Self::Either => formatter.write_str("Either"),
219            Self::GameProfile => formatter.write_str("Game Profile"),
220            Self::GameProfileProperty => formatter.write_str("Game Profile Property"),
221            Self::ResolvableProfile => formatter.write_str("Resolvable Profile"),
222            Self::PartialProfile => formatter.write_str("Partial Profile"),
223            Self::DebugSubscriptionEvent => formatter.write_str("Debug Subscription Event"),
224            Self::DebugSubscriptionUpdate => formatter.write_str("Debug Subscription Update"),
225            Self::DebugSubscriptionData => formatter.write_str("Debug Subscription Data"),
226            Self::DebugPathNode => formatter.write_str("Debug Path Node"),
227            Self::DebugStructureInfo => formatter.write_str("Debug Structure Info"),
228            Self::RecipeDisplay => formatter.write_str("Recipe Display"),
229            Self::ShapedRecipeGrid => formatter.write_str("Shaped Recipe Grid"),
230            Self::EntityMetadata => formatter.write_str("Entity Metadata"),
231            Self::EntityMetadataEntry => formatter.write_str("Entity Metadata Entry"),
232            Self::EntityMetadataValue => formatter.write_str("Entity Metadata Value"),
233            Self::Particle => formatter.write_str("Particle"),
234            Self::VibrationSource => formatter.write_str("Vibration Source"),
235            Self::RegistryId => formatter.write_str("Registry ID"),
236            Self::Array => formatter.write_str("Array"),
237            Self::ByteArray => formatter.write_str("Byte Array"),
238            Self::PrefixedArray => formatter.write_str("Prefixed Array"),
239            Self::Enum => formatter.write_str("Enum"),
240            Self::IdOr => formatter.write_str("ID or X"),
241            Self::IdSet => formatter.write_str("ID Set"),
242            Self::String => formatter.write_str("String"),
243            Self::Identifier => formatter.write_str("Identifier"),
244            Self::TextComponent => formatter.write_str("TextComponent"),
245            Self::JsonTextComponent => formatter.write_str("JsonTextComponent"),
246        }
247    }
248}
249
250/// Identifies whether an error occurred while decoding or encoding data.
251#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
252#[non_exhaustive]
253pub enum CodecOperation {
254    /// A read (decoding) operation.
255    Read,
256    /// A write (encoding) operation.
257    Write,
258}
259
260/// Formats an operation as `reading` or `writing`.
261impl fmt::Display for CodecOperation {
262    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
263        match self {
264            Self::Read => formatter.write_str("reading"),
265            Self::Write => formatter.write_str("writing"),
266        }
267    }
268}
269/// Describes why encoded protocol data is invalid.
270///
271/// Some reasons describe a mismatch between a codec and the context supplied
272/// by its enclosing packet.
273#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
274#[non_exhaustive]
275pub enum InvalidEncodingReason {
276    /// The encoding exceeds the maximum allowed length in bytes.
277    TooLong {
278        /// The maximum number of bytes permitted for this encoding.
279        max_bytes: usize,
280    },
281    /// The terminal byte of the encoding contains bits outside the allowed mask.
282    ValueOutOfRange {
283        /// The final byte that contains disallowed bits.
284        terminal_byte: u8,
285        /// A mask whose set bits identify the permitted bits in the final byte.
286        allowed_mask: u8,
287    },
288    /// The boolean value is invalid (not 0x00 or 0x01).
289    InvalidBooleanValue {
290        /// The byte read instead of the permitted `0x00` or `0x01`.
291        value: u8,
292    },
293    /// The string exceeds the maximum allowed length in bytes when encoded in UTF-8.
294    StringTooLong {
295        /// The maximum permitted size of the UTF-8 payload, excluding its
296        /// VarInt length prefix.
297        max_bytes: usize,
298    },
299    /// The string exceeds the maximum allowed length in UTF-16 code units.
300    TooManyUtf16CodeUnits {
301        /// The maximum permitted number of UTF-16 code units.
302        max_code_units: usize,
303    },
304    /// The length of the data is negative, which is invalid.
305    NegativeLength {
306        /// The negative length decoded from the data.
307        value: i32,
308    },
309    /// The length cannot be represented by the encoded length prefix.
310    LengthOutOfRange {
311        /// The greatest length representable by the prefix.
312        max: usize,
313        /// The length that was to be encoded.
314        actual: usize,
315    },
316    /// A decoded numeric enum value does not name a declared variant.
317    InvalidEnumValue {
318        /// The numeric value decoded from the enum's wire representation.
319        value: i128,
320    },
321    /// An enum variant's numeric discriminant cannot be represented on the wire.
322    EnumDiscriminantOutOfRange {
323        /// The numeric discriminant that cannot be encoded.
324        value: i128,
325    },
326    /// An LpVec3 scale factor exceeds the 34-bit wire representation.
327    LpVec3ScaleOutOfRange {
328        /// The rounded-up scale factor that was to be encoded.
329        scale_factor: u64,
330        /// The greatest scale factor representable by the format.
331        max: u64,
332    },
333    /// A registry ID cannot be represented by the `ID or X` wire format.
334    InvalidRegistryId {
335        /// The invalid registry ID.
336        value: i32,
337        /// The greatest registry ID supported by the enclosing wire format.
338        max: i32,
339    },
340    /// An entity metadata entry uses the reserved `0xff` terminator as its index.
341    InvalidEntityMetadataIndex {
342        /// The invalid entry index.
343        index: u8,
344    },
345    /// An entity metadata sequence contains the same index more than once.
346    DuplicateEntityMetadataIndex {
347        /// The repeated entry index.
348        index: u8,
349    },
350    /// An Optional VarInt selector cannot be mapped to a present value.
351    InvalidOptionalVarInt {
352        /// The invalid selector or in-memory value.
353        value: i32,
354    },
355    /// The decoded `ID or X` selector is negative.
356    InvalidIdOrSelector {
357        /// The invalid selector value read from the wire.
358        value: i32,
359    },
360    /// The decoded `ID Set` type value is negative.
361    InvalidIdSetType {
362        /// The invalid type value read from the wire.
363        value: i32,
364    },
365    /// A non-empty item stack count must fit a positive VarInt.
366    InvalidSlotCount {
367        /// The invalid count.
368        value: i64,
369    },
370    /// The packed byte array does not have the required fixed length.
371    InvalidFixedBitSetLength {
372        /// The expected number of packed bytes.
373        expected: usize,
374        /// The actual number of packed bytes.
375        actual: usize,
376    },
377    /// An optional value does not agree with its externally supplied context.
378    OptionalValueMismatch {
379        /// Whether the context says that the value is present on the wire.
380        context_present: bool,
381        /// Whether the value held by the wrapper is present in memory.
382        value_present: bool,
383    },
384    /// A contextual codec was used without the information it requires.
385    MissingContext {
386        /// The kind of information that was not supplied.
387        required: ContextRequirement,
388    },
389    /// The number of array values does not match the contextual length.
390    ArrayLengthMismatch {
391        /// The element count required by the context.
392        expected: usize,
393        /// The element count held by the array.
394        actual: usize,
395    },
396    /// The data contains an invalid UTF-8 sequence.
397    InvalidUtf8 {
398        /// The byte offset in the UTF-8 payload up to which the data is valid.
399        valid_up_to: usize,
400        /// The length of the invalid sequence, or `None` if the input ends in
401        /// an incomplete sequence.
402        error_len: Option<usize>,
403    },
404    /// The data is not a valid Minecraft identifier.
405    InvalidIdentifier,
406    /// The data is not valid NBT (Named Binary Tag) data.
407    InvalidNbt,
408    /// The data is not valid JSON.
409    InvalidJson,
410    /// The root tag of a text component is invalid (not TAG_String or TAG_Compound).
411    InvalidTextComponentRootTag {
412        /// The unsupported NBT root tag identifier.
413        tag: u8,
414    },
415}
416
417/// Identifies information required by a contextual codec.
418#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
419#[non_exhaustive]
420pub enum ContextRequirement {
421    /// Whether a value is present on the wire.
422    Presence,
423    /// The number of elements in a contextual array.
424    Length,
425    /// A context for an individual array element.
426    ElementContext,
427}
428
429impl fmt::Display for ContextRequirement {
430    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
431        match self {
432            Self::Presence => formatter.write_str("presence"),
433            Self::Length => formatter.write_str("array length"),
434            Self::ElementContext => formatter.write_str("array element context"),
435        }
436    }
437}
438
439/// Formats an invalid encoding reason as a diagnostic message.
440impl fmt::Display for InvalidEncodingReason {
441    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
442        match self {
443            Self::TooLong { max_bytes } => {
444                write!(formatter, "encoding exceeds the {max_bytes}-byte limit")
445            }
446            Self::ValueOutOfRange {
447                terminal_byte,
448                allowed_mask,
449            } => write!(
450                formatter,
451                "terminal byte 0x{terminal_byte:02X} contains bits outside mask 0x{allowed_mask:02X}"
452            ),
453            Self::InvalidBooleanValue { value } => {
454                write!(formatter, "invalid boolean value 0x{value:02X}")
455            }
456            Self::StringTooLong { max_bytes } => {
457                write!(formatter, "string exceeds the {max_bytes}-byte UTF-8 limit")
458            }
459            Self::TooManyUtf16CodeUnits { max_code_units } => write!(
460                formatter,
461                "string exceeds the {max_code_units}-code-unit UTF-16 limit"
462            ),
463            Self::NegativeLength { value } => {
464                write!(formatter, "length cannot be negative: {value}")
465            }
466            Self::LengthOutOfRange { max, actual } => {
467                write!(formatter, "length {actual} exceeds the maximum of {max}")
468            }
469            Self::InvalidEnumValue { value } => {
470                write!(formatter, "invalid enum value: {value}")
471            }
472            Self::EnumDiscriminantOutOfRange { value } => {
473                write!(formatter, "enum discriminant cannot be encoded: {value}")
474            }
475            Self::LpVec3ScaleOutOfRange { scale_factor, max } => write!(
476                formatter,
477                "LpVec3 scale factor {scale_factor} exceeds the maximum of {max}"
478            ),
479            Self::InvalidRegistryId { value, max } => write!(
480                formatter,
481                "registry ID must be between 0 and {max}, got {value}"
482            ),
483            Self::InvalidEntityMetadataIndex { index } => write!(
484                formatter,
485                "entity metadata index 0x{index:02X} is reserved as the terminator"
486            ),
487            Self::DuplicateEntityMetadataIndex { index } => {
488                write!(formatter, "duplicate entity metadata index {index}")
489            }
490            Self::InvalidOptionalVarInt { value } => {
491                write!(formatter, "invalid Optional VarInt value: {value}")
492            }
493            Self::InvalidIdOrSelector { value } => {
494                write!(formatter, "ID or X selector cannot be negative: {value}")
495            }
496            Self::InvalidIdSetType { value } => {
497                write!(formatter, "ID Set type cannot be negative: {value}")
498            }
499            Self::InvalidSlotCount { value } => {
500                write!(
501                    formatter,
502                    "invalid item-stack count {value}; expected 1..={}",
503                    i32::MAX
504                )
505            }
506            Self::InvalidFixedBitSetLength { expected, actual } => write!(
507                formatter,
508                "fixed bit set requires {expected} packed bytes, got {actual}"
509            ),
510            Self::OptionalValueMismatch {
511                context_present,
512                value_present,
513            } => write!(
514                formatter,
515                "optional value presence ({value_present}) does not match context ({context_present})"
516            ),
517            Self::MissingContext { required } => {
518                write!(formatter, "missing required codec context: {required}")
519            }
520            Self::ArrayLengthMismatch { expected, actual } => write!(
521                formatter,
522                "array contains {actual} elements, but context requires {expected}"
523            ),
524            Self::InvalidUtf8 {
525                valid_up_to,
526                error_len: Some(error_len),
527            } => write!(
528                formatter,
529                "invalid UTF-8 sequence of {error_len} bytes at byte {valid_up_to}"
530            ),
531            Self::InvalidUtf8 {
532                valid_up_to,
533                error_len: None,
534            } => write!(
535                formatter,
536                "incomplete UTF-8 sequence starting at byte {valid_up_to}"
537            ),
538            Self::InvalidIdentifier => formatter.write_str("invalid Minecraft identifier"),
539            Self::InvalidNbt => formatter.write_str("invalid NBT data"),
540            Self::InvalidJson => formatter.write_str("invalid JSON data"),
541            Self::InvalidTextComponentRootTag { tag } => write!(
542                formatter,
543                "text component root tag must be TAG_String (8) or TAG_Compound (10), got {tag}"
544            ),
545        }
546    }
547}
548/// Classifies an error reported by a protocol codec.
549#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
550#[non_exhaustive]
551pub enum CodecErrorKind {
552    /// An I/O error other than an unexpected end of input occurred.
553    Io,
554    /// A read ended before the codec received all required bytes.
555    UnexpectedEof,
556    /// The data could not be decoded or encoded according to the codec's
557    /// format or limits.
558    InvalidEncoding(InvalidEncodingReason),
559}
560
561/// An error produced while reading or writing protocol data.
562///
563/// The error records the originating [`CodecKind`], the [`CodecOperation`], the
564/// progress within that codec, and optional enclosing codec contexts. I/O and
565/// parser errors are retained as an error [`source`](Error::source).
566///
567/// Error enums are non-exhaustive, so downstream matches must include a
568/// wildcard arm.
569///
570/// # Example
571///
572/// ```
573/// use mcproto_codec::{
574///     error::{CodecErrorKind, CodecKind, CodecOperation},
575///     varint::VarIntRead,
576/// };
577///
578/// let mut input = [0x80].as_slice();
579/// let error = input
580///     .read_varint()
581///     .unwrap_err()
582///     .with_context(CodecKind::String);
583///
584/// assert_eq!(error.codec(), CodecKind::VarInt);
585/// assert_eq!(error.operation(), CodecOperation::Read);
586/// assert_eq!(error.bytes_processed(), 1);
587/// assert_eq!(error.contexts(), &[CodecKind::String]);
588///
589/// match error.kind() {
590///     CodecErrorKind::UnexpectedEof => {}
591///     _ => panic!("unexpected error: {error}"),
592/// }
593/// ```
594#[derive(Debug)]
595pub struct CodecError {
596    /// The error classification.
597    ///
598    /// This field and [`kind`](Self::kind) expose the same value. The accessor
599    /// is convenient when working through a shared reference.
600    pub kind: CodecErrorKind,
601    codec: CodecKind,
602    contexts: Contexts,
603    operation: CodecOperation,
604    bytes_processed: usize,
605    source: Option<BoxedError>,
606}
607
608/// Stores the enclosing codec contexts of a [`CodecError`].
609///
610/// The common cases of zero or one context are stored without heap allocation;
611/// only longer chains fall back to a [`Vec`].
612#[derive(Debug, Default)]
613enum Contexts {
614    /// No enclosing contexts.
615    #[default]
616    None,
617    /// A single context, stored inline.
618    One(CodecKind),
619    /// Two or more contexts, stored in a heap-allocated vector.
620    Many(Vec<CodecKind>),
621}
622
623impl CodecError {
624    /// Returns the error classification.
625    pub const fn kind(&self) -> CodecErrorKind {
626        self.kind
627    }
628    /// Returns the codec that originally reported the error.
629    pub const fn codec(&self) -> CodecKind {
630        self.codec
631    }
632    /// Returns the outermost enclosing codec context, if one was added.
633    ///
634    /// This is the last element of [`contexts`](Self::contexts), not the
635    /// originating codec returned by [`codec`](Self::codec).
636    pub fn context(&self) -> Option<CodecKind> {
637        self.contexts().last().copied()
638    }
639    /// Returns all enclosing codec contexts, ordered from nearest to outermost.
640    ///
641    /// The originating codec is not included. Each call to
642    /// [`with_context`](Self::with_context) appends one element.
643    pub fn contexts(&self) -> &[CodecKind] {
644        match &self.contexts {
645            Contexts::None => &[],
646            Contexts::One(context) => std::slice::from_ref(context),
647            Contexts::Many(contexts) => contexts,
648        }
649    }
650    /// Returns the operation being performed when the error occurred.
651    pub const fn operation(&self) -> CodecOperation {
652        self.operation
653    }
654    /// Returns the byte progress reported by the originating codec.
655    ///
656    /// Built-in codecs count bytes from the start of their encoded value. Bytes
657    /// successfully read or written before an I/O failure are included. A byte
658    /// that was read and then found to be invalid is also included. For a
659    /// length-prefixed value, the originating codec determines whether its
660    /// prefix is part of the count.
661    ///
662    /// Adding an outer context does not translate this value into an offset
663    /// within the enclosing codec.
664    pub const fn bytes_processed(&self) -> usize {
665        self.bytes_processed
666    }
667    /// Returns the underlying [`io::Error`], if the source is an I/O error.
668    ///
669    /// Invalid NBT or JSON errors may have a non-I/O source; access those
670    /// through [`Error::source`] instead.
671    pub fn io_error(&self) -> Option<&io::Error> {
672        self.source.as_deref()?.downcast_ref::<io::Error>()
673    }
674
675    /// Adds an enclosing codec to the error's context chain.
676    ///
677    /// Contexts should be added as the error propagates outward. Repeated calls
678    /// therefore order [`contexts`](Self::contexts) from nearest to outermost,
679    /// and [`context`](Self::context) returns the most recently added context.
680    pub fn with_context(mut self, context: CodecKind) -> Self {
681        self.contexts = match self.contexts {
682            Contexts::None => Contexts::One(context),
683            Contexts::One(first) => Contexts::Many(vec![first, context]),
684            Contexts::Many(mut contexts) => {
685                contexts.push(context);
686                Contexts::Many(contexts)
687            }
688        };
689        self
690    }
691    /// Creates an error from an I/O failure that occurred while reading.
692    ///
693    /// [`io::ErrorKind::UnexpectedEof`] maps to
694    /// [`CodecErrorKind::UnexpectedEof`]; every other error kind maps to
695    /// [`CodecErrorKind::Io`]. The source error is retained.
696    ///
697    /// `bytes_processed` is the number of bytes read before `source` occurred.
698    pub fn from_read_error(codec: CodecKind, bytes_processed: usize, source: io::Error) -> Self {
699        let kind = if source.kind() == io::ErrorKind::UnexpectedEof {
700            CodecErrorKind::UnexpectedEof
701        } else {
702            CodecErrorKind::Io
703        };
704
705        Self {
706            kind,
707            codec,
708            contexts: Contexts::None,
709            operation: CodecOperation::Read,
710            bytes_processed,
711            source: Some(Box::new(source)),
712        }
713    }
714    /// Creates an error from an I/O failure that occurred while writing.
715    ///
716    /// All write errors map to [`CodecErrorKind::Io`], and the source error is
717    /// retained. `bytes_processed` is the number of bytes written before
718    /// `source` occurred.
719    pub fn from_write_error(codec: CodecKind, bytes_processed: usize, source: io::Error) -> Self {
720        Self {
721            kind: CodecErrorKind::Io,
722            codec,
723            contexts: Contexts::None,
724            operation: CodecOperation::Write,
725            bytes_processed,
726            source: Some(Box::new(source)),
727        }
728    }
729    /// Creates an invalid encoding error for a read operation.
730    ///
731    /// Use [`invalid_encoding_for_operation`](Self::invalid_encoding_for_operation)
732    /// when the operation is not necessarily [`CodecOperation::Read`].
733    pub const fn invalid_encoding(
734        codec: CodecKind,
735        bytes_processed: usize,
736        reason: InvalidEncodingReason,
737    ) -> Self {
738        Self::invalid_encoding_for_operation(codec, CodecOperation::Read, bytes_processed, reason)
739    }
740
741    /// Creates an invalid encoding error for the specified operation.
742    ///
743    /// Unlike [`invalid_encoding`](Self::invalid_encoding), this constructor
744    /// does not assume that the error occurred while reading.
745    pub const fn invalid_encoding_for_operation(
746        codec: CodecKind,
747        operation: CodecOperation,
748        bytes_processed: usize,
749        reason: InvalidEncodingReason,
750    ) -> Self {
751        Self {
752            kind: CodecErrorKind::InvalidEncoding(reason),
753            codec,
754            contexts: Contexts::None,
755            operation,
756            bytes_processed,
757            source: None,
758        }
759    }
760    /// Creates an invalid encoding error with an underlying source error.
761    ///
762    /// `operation` may be either reading or writing. The supplied error is
763    /// available through [`Error::source`]; if it is an [`io::Error`], it is
764    /// also available through [`io_error`](Self::io_error).
765    pub fn invalid_encoding_for_operation_with_source(
766        codec: CodecKind,
767        operation: CodecOperation,
768        bytes_processed: usize,
769        reason: InvalidEncodingReason,
770        source: impl Error + Send + Sync + 'static,
771    ) -> Self {
772        Self {
773            kind: CodecErrorKind::InvalidEncoding(reason),
774            codec,
775            contexts: Contexts::None,
776            operation,
777            bytes_processed,
778            source: Some(Box::new(source)),
779        }
780    }
781}
782
783impl fmt::Display for CodecError {
784    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
785        match self.kind {
786            CodecErrorKind::Io => write!(
787                formatter,
788                "I/O error while {} {} after {} bytes",
789                self.operation, self.codec, self.bytes_processed
790            )?,
791            CodecErrorKind::UnexpectedEof => write!(
792                formatter,
793                "unexpected end of input while reading {} after {} bytes",
794                self.codec, self.bytes_processed
795            )?,
796            CodecErrorKind::InvalidEncoding(reason) => write!(
797                formatter,
798                "invalid {} encoding after {} bytes: {reason}",
799                self.codec, self.bytes_processed
800            )?,
801        }
802
803        for context in self.contexts() {
804            write!(formatter, " while processing {context}")?;
805        }
806
807        if let Some(source) = &self.source {
808            write!(formatter, ": {source}")?;
809        }
810
811        Ok(())
812    }
813}
814
815impl Error for CodecError {
816    fn source(&self) -> Option<&(dyn Error + 'static)> {
817        self.source
818            .as_deref()
819            .map(|source| source as &(dyn Error + 'static))
820    }
821}
822
823#[cfg(test)]
824mod tests {
825    use super::*;
826
827    fn read_error() -> CodecError {
828        CodecError::from_read_error(
829            CodecKind::VarInt,
830            3,
831            io::Error::new(io::ErrorKind::UnexpectedEof, "stream ended"),
832        )
833    }
834
835    fn write_error() -> CodecError {
836        CodecError::from_write_error(CodecKind::String, 5, io::Error::other("disk full"))
837    }
838
839    fn invalid_encoding_error() -> CodecError {
840        CodecError::invalid_encoding_for_operation(
841            CodecKind::Boolean,
842            CodecOperation::Read,
843            1,
844            InvalidEncodingReason::InvalidBooleanValue { value: 2 },
845        )
846    }
847
848    fn invalid_encoding_with_source() -> CodecError {
849        CodecError::invalid_encoding_for_operation_with_source(
850            CodecKind::JsonTextComponent,
851            CodecOperation::Read,
852            4,
853            InvalidEncodingReason::InvalidJson,
854            io::Error::new(io::ErrorKind::InvalidData, "bad json"),
855        )
856    }
857
858    #[test]
859    fn display_reports_unexpected_eof_operation_and_progress() {
860        assert_eq!(
861            read_error().to_string(),
862            "unexpected end of input while reading VarInt after 3 bytes: stream ended"
863        );
864    }
865
866    #[test]
867    fn display_reports_write_io_errors() {
868        assert_eq!(
869            write_error().to_string(),
870            "I/O error while writing String after 5 bytes: disk full"
871        );
872    }
873
874    #[test]
875    fn display_reports_invalid_encoding_reason() {
876        assert_eq!(
877            invalid_encoding_error().to_string(),
878            "invalid Boolean encoding after 1 bytes: invalid boolean value 0x02"
879        );
880    }
881
882    #[test]
883    fn display_appends_contexts_and_source_in_order() {
884        let error = invalid_encoding_with_source()
885            .with_context(CodecKind::String)
886            .with_context(CodecKind::Identifier)
887            .with_context(CodecKind::TextComponent);
888        assert_eq!(
889            error.to_string(),
890            "invalid JsonTextComponent encoding after 4 bytes: invalid JSON data \
891             while processing String while processing Identifier while processing TextComponent: bad json"
892        );
893    }
894
895    #[test]
896    fn display_omits_contexts_and_source_when_absent() {
897        let error = invalid_encoding_error();
898        assert!(!error.to_string().contains("while processing"));
899        assert!(
900            !error.to_string().ends_with(": invalid boolean value 0x02:"),
901            "a source was rendered when none is stored"
902        );
903    }
904
905    #[test]
906    fn contexts_are_empty_by_default() {
907        let error = read_error();
908        assert!(error.contexts().is_empty());
909        assert_eq!(error.context(), None);
910    }
911
912    #[test]
913    fn single_context_is_reported_inline() {
914        let error = read_error().with_context(CodecKind::String);
915        assert_eq!(error.contexts(), &[CodecKind::String]);
916        assert_eq!(error.context(), Some(CodecKind::String));
917    }
918
919    #[test]
920    fn many_contexts_are_reported_nearest_to_outermost() {
921        let error = invalid_encoding_error()
922            .with_context(CodecKind::String)
923            .with_context(CodecKind::Identifier)
924            .with_context(CodecKind::TextComponent);
925        assert_eq!(
926            error.contexts(),
927            &[
928                CodecKind::String,
929                CodecKind::Identifier,
930                CodecKind::TextComponent
931            ]
932        );
933        assert_eq!(error.context(), Some(CodecKind::TextComponent));
934        assert_eq!(error.codec(), CodecKind::Boolean);
935    }
936
937    #[test]
938    fn io_error_returns_the_underlying_io_error() {
939        let error = read_error();
940        let io_error = error.io_error().expect("io_error() should be Some");
941        assert_eq!(io_error.kind(), io::ErrorKind::UnexpectedEof);
942        assert_eq!(io_error.to_string(), "stream ended");
943        assert_eq!(
944            error
945                .source()
946                .and_then(|source| source.downcast_ref::<io::Error>())
947                .map(io::Error::kind),
948            Some(io::ErrorKind::UnexpectedEof)
949        );
950    }
951
952    #[test]
953    fn io_error_returns_none_for_non_io_sources() {
954        let error = CodecError::invalid_encoding_for_operation_with_source(
955            CodecKind::TextComponent,
956            CodecOperation::Read,
957            0,
958            InvalidEncodingReason::InvalidNbt,
959            NonIoSource,
960        );
961        assert!(error.io_error().is_none());
962        assert!(error.source().is_some());
963    }
964
965    #[derive(Debug)]
966    struct NonIoSource;
967
968    impl fmt::Display for NonIoSource {
969        fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
970            formatter.write_str("non-io source")
971        }
972    }
973
974    impl Error for NonIoSource {}
975}