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}