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}