Skip to main content

mcproto_codec/
error.rs

1use std::{error::Error, fmt, io};
2
3type BoxedError = Box<dyn Error + Send + Sync + 'static>;
4
5/// Identifies the protocol codec that reported an error.
6///
7/// A [`CodecError`] stores the codec that originally reported the error and may
8/// also store enclosing codecs as additional context. Protocol descriptions are
9/// based on the [Minecraft Java Edition protocol packet format].
10///
11/// Signed integer codecs use [two's-complement] representation.
12///
13/// [Minecraft Java Edition protocol packet format]: https://minecraft.wiki/w/Java_Edition_protocol/Packets
14/// [two's-complement]: https://en.wikipedia.org/wiki/Two%27s_complement
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16#[non_exhaustive]
17pub enum CodecKind {
18    /// A variable-length, two's-complement signed 32-bit integer.
19    ///
20    /// Values range from -2,147,483,648 through 2,147,483,647.
21    VarInt,
22    /// A variable-length, two's-complement signed 64-bit integer.
23    ///
24    /// Values range from -9,223,372,036,854,775,808 through
25    /// 9,223,372,036,854,775,807.
26    VarLong,
27    /// A boolean encoded as `0x00` for false or `0x01` for true.
28    Boolean,
29    /// A two's-complement signed 8-bit integer from -128 through 127.
30    Byte,
31    /// An unsigned 8-bit integer from 0 through 255.
32    UnsignedByte,
33    /// A two's-complement signed 16-bit integer from -32,768 through 32,767.
34    Short,
35    /// An unsigned 16-bit integer from 0 through 65,535.
36    UnsignedShort,
37    /// A two's-complement signed 32-bit integer from -2,147,483,648 through
38    /// 2,147,483,647.
39    Int,
40    /// A two's-complement signed 64-bit integer from -9,223,372,036,854,775,808
41    /// through 9,223,372,036,854,775,807.
42    Long,
43    /// A UTF-8 string prefixed by its byte length as a VarInt.
44    ///
45    /// The protocol limits both the UTF-8 payload size and the number of UTF-16
46    /// code units. Supplementary [Unicode scalar values] count as two UTF-16
47    /// code units. The general protocol limit is 32,767 UTF-16 code units and
48    /// three UTF-8 bytes per permitted code unit; a particular field may impose
49    /// a lower limit.
50    ///
51    /// [Unicode scalar values]: https://www.unicode.org/glossary/#unicode_scalar_value
52    String,
53    /// A resource identifier encoded as a [`String`](Self::String).
54    ///
55    /// The namespace permits `[a-z0-9._-]`; the value permits
56    /// `[a-z0-9._/-]`. See the protocol's [identifier format] for details.
57    ///
58    /// [identifier format]: https://minecraft.wiki/w/Java_Edition_protocol/Packets#Identifier
59    Identifier,
60    /// A text component encoded as an NBT tag.
61    ///
62    /// Plain text-only components may use an NBT string tag. Components with
63    /// styling, events, or other data use an NBT compound tag. See the
64    /// [text component format] and [NBT specification].
65    ///
66    /// [text component format]: https://minecraft.wiki/w/Text_component_format
67    /// [NBT specification]: https://minecraft.wiki/w/NBT_format
68    TextComponent,
69    /// A text component encoded as JSON in a protocol string.
70    ///
71    /// Since Java Edition 1.20.3, the vanilla implementation permits up to
72    /// 262,144 UTF-16 code units when decoding but refuses to encode more than
73    /// 32,767. See the [text component format].
74    ///
75    /// [text component format]: https://minecraft.wiki/w/Text_component_format
76    JsonTextComponent,
77}
78
79/// Formats a codec kind using its protocol name, such as `VarInt` or `Boolean`.
80impl fmt::Display for CodecKind {
81    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
82        match self {
83            Self::VarInt => formatter.write_str("VarInt"),
84            Self::VarLong => formatter.write_str("VarLong"),
85            Self::Boolean => formatter.write_str("Boolean"),
86            Self::Byte => formatter.write_str("Byte"),
87            Self::UnsignedByte => formatter.write_str("UnsignedByte"),
88            Self::Short => formatter.write_str("Short"),
89            Self::UnsignedShort => formatter.write_str("UnsignedShort"),
90            Self::Int => formatter.write_str("Int"),
91            Self::Long => formatter.write_str("Long"),
92            Self::String => formatter.write_str("String"),
93            Self::Identifier => formatter.write_str("Identifier"),
94            Self::TextComponent => formatter.write_str("TextComponent"),
95            Self::JsonTextComponent => formatter.write_str("JsonTextComponent"),
96        }
97    }
98}
99
100/// Identifies whether an error occurred while decoding or encoding data.
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
102#[non_exhaustive]
103pub enum CodecOperation {
104    /// A read (decoding) operation.
105    Read,
106    /// A write (encoding) operation.
107    Write,
108}
109
110/// Formats an operation as `reading` or `writing`.
111impl fmt::Display for CodecOperation {
112    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
113        match self {
114            Self::Read => formatter.write_str("reading"),
115            Self::Write => formatter.write_str("writing"),
116        }
117    }
118}
119/// Describes why encoded protocol data is invalid.
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
121#[non_exhaustive]
122pub enum InvalidEncodingReason {
123    /// The encoding exceeds the maximum allowed length in bytes.
124    TooLong {
125        /// The maximum number of bytes permitted for this encoding.
126        max_bytes: usize,
127    },
128    /// The terminal byte of the encoding contains bits outside the allowed mask.
129    ValueOutOfRange {
130        /// The final byte that contains disallowed bits.
131        terminal_byte: u8,
132        /// A mask whose set bits identify the permitted bits in the final byte.
133        allowed_mask: u8,
134    },
135    /// The boolean value is invalid (not 0x00 or 0x01).
136    InvalidBooleanValue {
137        /// The byte read instead of the permitted `0x00` or `0x01`.
138        value: u8,
139    },
140    /// The string exceeds the maximum allowed length in bytes when encoded in UTF-8.
141    StringTooLong {
142        /// The maximum permitted size of the UTF-8 payload, excluding its
143        /// VarInt length prefix.
144        max_bytes: usize,
145    },
146    /// The string exceeds the maximum allowed length in UTF-16 code units.
147    TooManyUtf16CodeUnits {
148        /// The maximum permitted number of UTF-16 code units.
149        max_code_units: usize,
150    },
151    /// The length of the data is negative, which is invalid.
152    NegativeLength {
153        /// The negative length decoded from the data.
154        value: i32,
155    },
156    /// The data contains an invalid UTF-8 sequence.
157    InvalidUtf8 {
158        /// The byte offset in the UTF-8 payload up to which the data is valid.
159        valid_up_to: usize,
160        /// The length of the invalid sequence, or `None` if the input ends in
161        /// an incomplete sequence.
162        error_len: Option<usize>,
163    },
164    /// The data is not a valid Minecraft identifier.
165    InvalidIdentifier,
166    /// The data is not valid NBT (Named Binary Tag) data.
167    InvalidNbt,
168    /// The data is not valid JSON.
169    InvalidJson,
170    /// The root tag of a text component is invalid (not TAG_String or TAG_Compound).
171    InvalidTextComponentRootTag {
172        /// The unsupported NBT root tag identifier.
173        tag: u8,
174    },
175}
176/// Formats an invalid encoding reason as a diagnostic message.
177impl fmt::Display for InvalidEncodingReason {
178    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
179        match self {
180            Self::TooLong { max_bytes } => {
181                write!(formatter, "encoding exceeds the {max_bytes}-byte limit")
182            }
183            Self::ValueOutOfRange {
184                terminal_byte,
185                allowed_mask,
186            } => write!(
187                formatter,
188                "terminal byte 0x{terminal_byte:02X} contains bits outside mask 0x{allowed_mask:02X}"
189            ),
190            Self::InvalidBooleanValue { value } => {
191                write!(formatter, "invalid boolean value 0x{value:02X}")
192            }
193            Self::StringTooLong { max_bytes } => {
194                write!(formatter, "string exceeds the {max_bytes}-byte UTF-8 limit")
195            }
196            Self::TooManyUtf16CodeUnits { max_code_units } => write!(
197                formatter,
198                "string exceeds the {max_code_units}-code-unit UTF-16 limit"
199            ),
200            Self::NegativeLength { value } => {
201                write!(formatter, "length cannot be negative: {value}")
202            }
203            Self::InvalidUtf8 {
204                valid_up_to,
205                error_len: Some(error_len),
206            } => write!(
207                formatter,
208                "invalid UTF-8 sequence of {error_len} bytes at byte {valid_up_to}"
209            ),
210            Self::InvalidUtf8 {
211                valid_up_to,
212                error_len: None,
213            } => write!(
214                formatter,
215                "incomplete UTF-8 sequence starting at byte {valid_up_to}"
216            ),
217            Self::InvalidIdentifier => formatter.write_str("invalid Minecraft identifier"),
218            Self::InvalidNbt => formatter.write_str("invalid NBT data"),
219            Self::InvalidJson => formatter.write_str("invalid JSON data"),
220            Self::InvalidTextComponentRootTag { tag } => write!(
221                formatter,
222                "text component root tag must be TAG_String (8) or TAG_Compound (10), got {tag}"
223            ),
224        }
225    }
226}
227/// Classifies an error reported by a protocol codec.
228#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
229#[non_exhaustive]
230pub enum CodecErrorKind {
231    /// An I/O error other than an unexpected end of input occurred.
232    Io,
233    /// A read ended before the codec received all required bytes.
234    UnexpectedEof,
235    /// The data could not be decoded or encoded according to the codec's
236    /// format or limits.
237    InvalidEncoding(InvalidEncodingReason),
238}
239
240/// An error produced while reading or writing protocol data.
241///
242/// The error records the originating [`CodecKind`], the [`CodecOperation`], the
243/// progress within that codec, and optional enclosing codec contexts. I/O and
244/// parser errors are retained as an error [`source`](Error::source).
245///
246/// Error enums are non-exhaustive, so downstream matches must include a
247/// wildcard arm.
248///
249/// # Example
250///
251/// ```
252/// use mcproto_codec::{
253///     error::{CodecErrorKind, CodecKind, CodecOperation},
254///     varint::VarIntRead,
255/// };
256///
257/// let mut input = [0x80].as_slice();
258/// let error = input
259///     .read_varint()
260///     .unwrap_err()
261///     .with_context(CodecKind::String);
262///
263/// assert_eq!(error.codec(), CodecKind::VarInt);
264/// assert_eq!(error.operation(), CodecOperation::Read);
265/// assert_eq!(error.bytes_processed(), 1);
266/// assert_eq!(error.contexts(), &[CodecKind::String]);
267///
268/// match error.kind() {
269///     CodecErrorKind::UnexpectedEof => {}
270///     _ => panic!("unexpected error: {error}"),
271/// }
272/// ```
273#[derive(Debug)]
274pub struct CodecError {
275    /// The error classification.
276    ///
277    /// This field and [`kind`](Self::kind) expose the same value. The accessor
278    /// is convenient when working through a shared reference.
279    pub kind: CodecErrorKind,
280    codec: CodecKind,
281    contexts: Vec<CodecKind>,
282    operation: CodecOperation,
283    bytes_processed: usize,
284    source: Option<BoxedError>,
285}
286
287impl CodecError {
288    /// Returns the error classification.
289    pub const fn kind(&self) -> CodecErrorKind {
290        self.kind
291    }
292    /// Returns the codec that originally reported the error.
293    pub const fn codec(&self) -> CodecKind {
294        self.codec
295    }
296    /// Returns the outermost enclosing codec context, if one was added.
297    ///
298    /// This is the last element of [`contexts`](Self::contexts), not the
299    /// originating codec returned by [`codec`](Self::codec).
300    pub fn context(&self) -> Option<CodecKind> {
301        self.contexts.last().copied()
302    }
303    /// Returns all enclosing codec contexts, ordered from nearest to outermost.
304    ///
305    /// The originating codec is not included. Each call to
306    /// [`with_context`](Self::with_context) appends one element.
307    pub fn contexts(&self) -> &[CodecKind] {
308        &self.contexts
309    }
310    /// Returns the operation being performed when the error occurred.
311    pub const fn operation(&self) -> CodecOperation {
312        self.operation
313    }
314    /// Returns the byte progress reported by the originating codec.
315    ///
316    /// Built-in codecs count bytes from the start of their encoded value. Bytes
317    /// successfully read or written before an I/O failure are included. A byte
318    /// that was read and then found to be invalid is also included. For a
319    /// length-prefixed value, the originating codec determines whether its
320    /// prefix is part of the count.
321    ///
322    /// Adding an outer context does not translate this value into an offset
323    /// within the enclosing codec.
324    pub const fn bytes_processed(&self) -> usize {
325        self.bytes_processed
326    }
327    /// Returns the underlying [`io::Error`], if the source is an I/O error.
328    ///
329    /// Invalid NBT or JSON errors may have a non-I/O source; access those
330    /// through [`Error::source`] instead.
331    pub fn io_error(&self) -> Option<&io::Error> {
332        self.source.as_deref()?.downcast_ref::<io::Error>()
333    }
334
335    /// Adds an enclosing codec to the error's context chain.
336    ///
337    /// Contexts should be added as the error propagates outward. Repeated calls
338    /// therefore order [`contexts`](Self::contexts) from nearest to outermost,
339    /// and [`context`](Self::context) returns the most recently added context.
340    pub fn with_context(mut self, context: CodecKind) -> Self {
341        self.contexts.push(context);
342        self
343    }
344    /// Creates an error from an I/O failure that occurred while reading.
345    ///
346    /// [`io::ErrorKind::UnexpectedEof`] maps to
347    /// [`CodecErrorKind::UnexpectedEof`]; every other error kind maps to
348    /// [`CodecErrorKind::Io`]. The source error is retained.
349    ///
350    /// `bytes_processed` is the number of bytes read before `source` occurred.
351    pub fn from_read_error(codec: CodecKind, bytes_processed: usize, source: io::Error) -> Self {
352        let kind = if source.kind() == io::ErrorKind::UnexpectedEof {
353            CodecErrorKind::UnexpectedEof
354        } else {
355            CodecErrorKind::Io
356        };
357
358        Self {
359            kind,
360            codec,
361            contexts: Vec::new(),
362            operation: CodecOperation::Read,
363            bytes_processed,
364            source: Some(Box::new(source)),
365        }
366    }
367    /// Creates an error from an I/O failure that occurred while writing.
368    ///
369    /// All write errors map to [`CodecErrorKind::Io`], and the source error is
370    /// retained. `bytes_processed` is the number of bytes written before
371    /// `source` occurred.
372    pub fn from_write_error(codec: CodecKind, bytes_processed: usize, source: io::Error) -> Self {
373        Self {
374            kind: CodecErrorKind::Io,
375            codec,
376            contexts: Vec::new(),
377            operation: CodecOperation::Write,
378            bytes_processed,
379            source: Some(Box::new(source)),
380        }
381    }
382    /// Creates an invalid encoding error for a read operation.
383    ///
384    /// Use [`invalid_encoding_for_operation`](Self::invalid_encoding_for_operation)
385    /// when the operation is not necessarily [`CodecOperation::Read`].
386    pub const fn invalid_encoding(
387        codec: CodecKind,
388        bytes_processed: usize,
389        reason: InvalidEncodingReason,
390    ) -> Self {
391        Self::invalid_encoding_for_operation(codec, CodecOperation::Read, bytes_processed, reason)
392    }
393
394    /// Creates an invalid encoding error for the specified operation.
395    ///
396    /// Unlike [`invalid_encoding`](Self::invalid_encoding), this constructor
397    /// does not assume that the error occurred while reading.
398    pub const fn invalid_encoding_for_operation(
399        codec: CodecKind,
400        operation: CodecOperation,
401        bytes_processed: usize,
402        reason: InvalidEncodingReason,
403    ) -> Self {
404        Self {
405            kind: CodecErrorKind::InvalidEncoding(reason),
406            codec,
407            contexts: Vec::new(),
408            operation,
409            bytes_processed,
410            source: None,
411        }
412    }
413    /// Creates an invalid encoding error with an underlying source error.
414    ///
415    /// `operation` may be either reading or writing. The supplied error is
416    /// available through [`Error::source`]; if it is an [`io::Error`], it is
417    /// also available through [`io_error`](Self::io_error).
418    pub fn invalid_encoding_for_operation_with_source(
419        codec: CodecKind,
420        operation: CodecOperation,
421        bytes_processed: usize,
422        reason: InvalidEncodingReason,
423        source: impl Error + Send + Sync + 'static,
424    ) -> Self {
425        Self {
426            kind: CodecErrorKind::InvalidEncoding(reason),
427            codec,
428            contexts: Vec::new(),
429            operation,
430            bytes_processed,
431            source: Some(Box::new(source)),
432        }
433    }
434}
435
436impl fmt::Display for CodecError {
437    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
438        match self.kind {
439            CodecErrorKind::Io => write!(
440                formatter,
441                "I/O error while {} {} after {} bytes",
442                self.operation, self.codec, self.bytes_processed
443            )?,
444            CodecErrorKind::UnexpectedEof => write!(
445                formatter,
446                "unexpected end of input while reading {} after {} bytes",
447                self.codec, self.bytes_processed
448            )?,
449            CodecErrorKind::InvalidEncoding(reason) => write!(
450                formatter,
451                "invalid {} encoding after {} bytes: {reason}",
452                self.codec, self.bytes_processed
453            )?,
454        }
455
456        for context in &self.contexts {
457            write!(formatter, " while processing {context}")?;
458        }
459
460        if let Some(source) = &self.source {
461            write!(formatter, ": {source}")?;
462        }
463
464        Ok(())
465    }
466}
467
468impl Error for CodecError {
469    fn source(&self) -> Option<&(dyn Error + 'static)> {
470        self.source
471            .as_deref()
472            .map(|source| source as &(dyn Error + 'static))
473    }
474}