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 block position packed into a 64-bit integer.
53 ///
54 /// The x, z, and y coordinates occupy 26, 26, and 12 bits respectively.
55 Position,
56 /// A rotation angle encoded in 1/256 turn steps.
57 Angle,
58 /// A 128-bit universally unique identifier.
59 Uuid,
60 /// A length-prefixed bit set of packed 64-bit words.
61 BitSet,
62 /// A fixed-length bit set of packed bytes.
63 FixedBitSet,
64 /// A UTF-8 string prefixed by its byte length as a VarInt.
65 ///
66 /// The protocol limits both the UTF-8 payload size and the number of UTF-16
67 /// code units. Supplementary [Unicode scalar values] count as two UTF-16
68 /// code units. The general protocol limit is 32,767 UTF-16 code units and
69 /// three UTF-8 bytes per permitted code unit; a particular field may impose
70 /// a lower limit.
71 ///
72 /// [Unicode scalar values]: https://www.unicode.org/glossary/#unicode_scalar_value
73 String,
74 /// A resource identifier encoded as a [`String`](Self::String).
75 ///
76 /// The namespace permits `[a-z0-9._-]`; the value permits
77 /// `[a-z0-9._/-]`. See the protocol's [identifier format] for details.
78 ///
79 /// [identifier format]: https://minecraft.wiki/w/Java_Edition_protocol/Packets#Identifier
80 Identifier,
81 /// A text component encoded as an NBT tag.
82 ///
83 /// Plain text-only components may use an NBT string tag. Components with
84 /// styling, events, or other data use an NBT compound tag. See the
85 /// [text component format] and [NBT specification].
86 ///
87 /// [text component format]: https://minecraft.wiki/w/Text_component_format
88 /// [NBT specification]: https://minecraft.wiki/w/NBT_format
89 TextComponent,
90 /// A text component encoded as JSON in a protocol string.
91 ///
92 /// Since Java Edition 1.20.3, the vanilla implementation permits up to
93 /// 262,144 UTF-16 code units when decoding but refuses to encode more than
94 /// 32,767. See the [text component format].
95 ///
96 /// [text component format]: https://minecraft.wiki/w/Text_component_format
97 JsonTextComponent,
98}
99
100/// Formats a codec kind using its protocol name, such as `VarInt` or `Boolean`.
101impl fmt::Display for CodecKind {
102 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
103 match self {
104 Self::VarInt => formatter.write_str("VarInt"),
105 Self::VarLong => formatter.write_str("VarLong"),
106 Self::Nbt => formatter.write_str("Nbt"),
107 Self::Boolean => formatter.write_str("Boolean"),
108 Self::Byte => formatter.write_str("Byte"),
109 Self::UnsignedByte => formatter.write_str("UnsignedByte"),
110 Self::Short => formatter.write_str("Short"),
111 Self::UnsignedShort => formatter.write_str("UnsignedShort"),
112 Self::Int => formatter.write_str("Int"),
113 Self::Long => formatter.write_str("Long"),
114 Self::Position => formatter.write_str("Position"),
115 Self::Angle => formatter.write_str("Angle"),
116 Self::Uuid => formatter.write_str("UUID"),
117 Self::BitSet => formatter.write_str("BitSet"),
118 Self::FixedBitSet => formatter.write_str("Fixed BitSet"),
119 Self::String => formatter.write_str("String"),
120 Self::Identifier => formatter.write_str("Identifier"),
121 Self::TextComponent => formatter.write_str("TextComponent"),
122 Self::JsonTextComponent => formatter.write_str("JsonTextComponent"),
123 }
124 }
125}
126
127/// Identifies whether an error occurred while decoding or encoding data.
128#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
129#[non_exhaustive]
130pub enum CodecOperation {
131 /// A read (decoding) operation.
132 Read,
133 /// A write (encoding) operation.
134 Write,
135}
136
137/// Formats an operation as `reading` or `writing`.
138impl fmt::Display for CodecOperation {
139 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
140 match self {
141 Self::Read => formatter.write_str("reading"),
142 Self::Write => formatter.write_str("writing"),
143 }
144 }
145}
146/// Describes why encoded protocol data is invalid.
147#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
148#[non_exhaustive]
149pub enum InvalidEncodingReason {
150 /// The encoding exceeds the maximum allowed length in bytes.
151 TooLong {
152 /// The maximum number of bytes permitted for this encoding.
153 max_bytes: usize,
154 },
155 /// The terminal byte of the encoding contains bits outside the allowed mask.
156 ValueOutOfRange {
157 /// The final byte that contains disallowed bits.
158 terminal_byte: u8,
159 /// A mask whose set bits identify the permitted bits in the final byte.
160 allowed_mask: u8,
161 },
162 /// The boolean value is invalid (not 0x00 or 0x01).
163 InvalidBooleanValue {
164 /// The byte read instead of the permitted `0x00` or `0x01`.
165 value: u8,
166 },
167 /// The string exceeds the maximum allowed length in bytes when encoded in UTF-8.
168 StringTooLong {
169 /// The maximum permitted size of the UTF-8 payload, excluding its
170 /// VarInt length prefix.
171 max_bytes: usize,
172 },
173 /// The string exceeds the maximum allowed length in UTF-16 code units.
174 TooManyUtf16CodeUnits {
175 /// The maximum permitted number of UTF-16 code units.
176 max_code_units: usize,
177 },
178 /// The length of the data is negative, which is invalid.
179 NegativeLength {
180 /// The negative length decoded from the data.
181 value: i32,
182 },
183 /// The packed byte array does not have the required fixed length.
184 InvalidFixedBitSetLength {
185 /// The expected number of packed bytes.
186 expected: usize,
187 /// The actual number of packed bytes.
188 actual: usize,
189 },
190 /// The data contains an invalid UTF-8 sequence.
191 InvalidUtf8 {
192 /// The byte offset in the UTF-8 payload up to which the data is valid.
193 valid_up_to: usize,
194 /// The length of the invalid sequence, or `None` if the input ends in
195 /// an incomplete sequence.
196 error_len: Option<usize>,
197 },
198 /// The data is not a valid Minecraft identifier.
199 InvalidIdentifier,
200 /// The data is not valid NBT (Named Binary Tag) data.
201 InvalidNbt,
202 /// The data is not valid JSON.
203 InvalidJson,
204 /// The root tag of a text component is invalid (not TAG_String or TAG_Compound).
205 InvalidTextComponentRootTag {
206 /// The unsupported NBT root tag identifier.
207 tag: u8,
208 },
209}
210/// Formats an invalid encoding reason as a diagnostic message.
211impl fmt::Display for InvalidEncodingReason {
212 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
213 match self {
214 Self::TooLong { max_bytes } => {
215 write!(formatter, "encoding exceeds the {max_bytes}-byte limit")
216 }
217 Self::ValueOutOfRange {
218 terminal_byte,
219 allowed_mask,
220 } => write!(
221 formatter,
222 "terminal byte 0x{terminal_byte:02X} contains bits outside mask 0x{allowed_mask:02X}"
223 ),
224 Self::InvalidBooleanValue { value } => {
225 write!(formatter, "invalid boolean value 0x{value:02X}")
226 }
227 Self::StringTooLong { max_bytes } => {
228 write!(formatter, "string exceeds the {max_bytes}-byte UTF-8 limit")
229 }
230 Self::TooManyUtf16CodeUnits { max_code_units } => write!(
231 formatter,
232 "string exceeds the {max_code_units}-code-unit UTF-16 limit"
233 ),
234 Self::NegativeLength { value } => {
235 write!(formatter, "length cannot be negative: {value}")
236 }
237 Self::InvalidFixedBitSetLength { expected, actual } => write!(
238 formatter,
239 "fixed bit set requires {expected} packed bytes, got {actual}"
240 ),
241 Self::InvalidUtf8 {
242 valid_up_to,
243 error_len: Some(error_len),
244 } => write!(
245 formatter,
246 "invalid UTF-8 sequence of {error_len} bytes at byte {valid_up_to}"
247 ),
248 Self::InvalidUtf8 {
249 valid_up_to,
250 error_len: None,
251 } => write!(
252 formatter,
253 "incomplete UTF-8 sequence starting at byte {valid_up_to}"
254 ),
255 Self::InvalidIdentifier => formatter.write_str("invalid Minecraft identifier"),
256 Self::InvalidNbt => formatter.write_str("invalid NBT data"),
257 Self::InvalidJson => formatter.write_str("invalid JSON data"),
258 Self::InvalidTextComponentRootTag { tag } => write!(
259 formatter,
260 "text component root tag must be TAG_String (8) or TAG_Compound (10), got {tag}"
261 ),
262 }
263 }
264}
265/// Classifies an error reported by a protocol codec.
266#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
267#[non_exhaustive]
268pub enum CodecErrorKind {
269 /// An I/O error other than an unexpected end of input occurred.
270 Io,
271 /// A read ended before the codec received all required bytes.
272 UnexpectedEof,
273 /// The data could not be decoded or encoded according to the codec's
274 /// format or limits.
275 InvalidEncoding(InvalidEncodingReason),
276}
277
278/// An error produced while reading or writing protocol data.
279///
280/// The error records the originating [`CodecKind`], the [`CodecOperation`], the
281/// progress within that codec, and optional enclosing codec contexts. I/O and
282/// parser errors are retained as an error [`source`](Error::source).
283///
284/// Error enums are non-exhaustive, so downstream matches must include a
285/// wildcard arm.
286///
287/// # Example
288///
289/// ```
290/// use mcproto_codec::{
291/// error::{CodecErrorKind, CodecKind, CodecOperation},
292/// varint::VarIntRead,
293/// };
294///
295/// let mut input = [0x80].as_slice();
296/// let error = input
297/// .read_varint()
298/// .unwrap_err()
299/// .with_context(CodecKind::String);
300///
301/// assert_eq!(error.codec(), CodecKind::VarInt);
302/// assert_eq!(error.operation(), CodecOperation::Read);
303/// assert_eq!(error.bytes_processed(), 1);
304/// assert_eq!(error.contexts(), &[CodecKind::String]);
305///
306/// match error.kind() {
307/// CodecErrorKind::UnexpectedEof => {}
308/// _ => panic!("unexpected error: {error}"),
309/// }
310/// ```
311#[derive(Debug)]
312pub struct CodecError {
313 /// The error classification.
314 ///
315 /// This field and [`kind`](Self::kind) expose the same value. The accessor
316 /// is convenient when working through a shared reference.
317 pub kind: CodecErrorKind,
318 codec: CodecKind,
319 contexts: Contexts,
320 operation: CodecOperation,
321 bytes_processed: usize,
322 source: Option<BoxedError>,
323}
324
325/// Stores the enclosing codec contexts of a [`CodecError`].
326///
327/// The common cases of zero or one context are stored without heap allocation;
328/// only longer chains fall back to a [`Vec`].
329#[derive(Debug, Default)]
330enum Contexts {
331 /// No enclosing contexts.
332 #[default]
333 None,
334 /// A single context, stored inline.
335 One(CodecKind),
336 /// Two or more contexts, stored in a heap-allocated vector.
337 Many(Vec<CodecKind>),
338}
339
340impl CodecError {
341 /// Returns the error classification.
342 pub const fn kind(&self) -> CodecErrorKind {
343 self.kind
344 }
345 /// Returns the codec that originally reported the error.
346 pub const fn codec(&self) -> CodecKind {
347 self.codec
348 }
349 /// Returns the outermost enclosing codec context, if one was added.
350 ///
351 /// This is the last element of [`contexts`](Self::contexts), not the
352 /// originating codec returned by [`codec`](Self::codec).
353 pub fn context(&self) -> Option<CodecKind> {
354 self.contexts().last().copied()
355 }
356 /// Returns all enclosing codec contexts, ordered from nearest to outermost.
357 ///
358 /// The originating codec is not included. Each call to
359 /// [`with_context`](Self::with_context) appends one element.
360 pub fn contexts(&self) -> &[CodecKind] {
361 match &self.contexts {
362 Contexts::None => &[],
363 Contexts::One(context) => std::slice::from_ref(context),
364 Contexts::Many(contexts) => contexts,
365 }
366 }
367 /// Returns the operation being performed when the error occurred.
368 pub const fn operation(&self) -> CodecOperation {
369 self.operation
370 }
371 /// Returns the byte progress reported by the originating codec.
372 ///
373 /// Built-in codecs count bytes from the start of their encoded value. Bytes
374 /// successfully read or written before an I/O failure are included. A byte
375 /// that was read and then found to be invalid is also included. For a
376 /// length-prefixed value, the originating codec determines whether its
377 /// prefix is part of the count.
378 ///
379 /// Adding an outer context does not translate this value into an offset
380 /// within the enclosing codec.
381 pub const fn bytes_processed(&self) -> usize {
382 self.bytes_processed
383 }
384 /// Returns the underlying [`io::Error`], if the source is an I/O error.
385 ///
386 /// Invalid NBT or JSON errors may have a non-I/O source; access those
387 /// through [`Error::source`] instead.
388 pub fn io_error(&self) -> Option<&io::Error> {
389 self.source.as_deref()?.downcast_ref::<io::Error>()
390 }
391
392 /// Adds an enclosing codec to the error's context chain.
393 ///
394 /// Contexts should be added as the error propagates outward. Repeated calls
395 /// therefore order [`contexts`](Self::contexts) from nearest to outermost,
396 /// and [`context`](Self::context) returns the most recently added context.
397 pub fn with_context(mut self, context: CodecKind) -> Self {
398 self.contexts = match self.contexts {
399 Contexts::None => Contexts::One(context),
400 Contexts::One(first) => Contexts::Many(vec![first, context]),
401 Contexts::Many(mut contexts) => {
402 contexts.push(context);
403 Contexts::Many(contexts)
404 }
405 };
406 self
407 }
408 /// Creates an error from an I/O failure that occurred while reading.
409 ///
410 /// [`io::ErrorKind::UnexpectedEof`] maps to
411 /// [`CodecErrorKind::UnexpectedEof`]; every other error kind maps to
412 /// [`CodecErrorKind::Io`]. The source error is retained.
413 ///
414 /// `bytes_processed` is the number of bytes read before `source` occurred.
415 pub fn from_read_error(codec: CodecKind, bytes_processed: usize, source: io::Error) -> Self {
416 let kind = if source.kind() == io::ErrorKind::UnexpectedEof {
417 CodecErrorKind::UnexpectedEof
418 } else {
419 CodecErrorKind::Io
420 };
421
422 Self {
423 kind,
424 codec,
425 contexts: Contexts::None,
426 operation: CodecOperation::Read,
427 bytes_processed,
428 source: Some(Box::new(source)),
429 }
430 }
431 /// Creates an error from an I/O failure that occurred while writing.
432 ///
433 /// All write errors map to [`CodecErrorKind::Io`], and the source error is
434 /// retained. `bytes_processed` is the number of bytes written before
435 /// `source` occurred.
436 pub fn from_write_error(codec: CodecKind, bytes_processed: usize, source: io::Error) -> Self {
437 Self {
438 kind: CodecErrorKind::Io,
439 codec,
440 contexts: Contexts::None,
441 operation: CodecOperation::Write,
442 bytes_processed,
443 source: Some(Box::new(source)),
444 }
445 }
446 /// Creates an invalid encoding error for a read operation.
447 ///
448 /// Use [`invalid_encoding_for_operation`](Self::invalid_encoding_for_operation)
449 /// when the operation is not necessarily [`CodecOperation::Read`].
450 pub const fn invalid_encoding(
451 codec: CodecKind,
452 bytes_processed: usize,
453 reason: InvalidEncodingReason,
454 ) -> Self {
455 Self::invalid_encoding_for_operation(codec, CodecOperation::Read, bytes_processed, reason)
456 }
457
458 /// Creates an invalid encoding error for the specified operation.
459 ///
460 /// Unlike [`invalid_encoding`](Self::invalid_encoding), this constructor
461 /// does not assume that the error occurred while reading.
462 pub const fn invalid_encoding_for_operation(
463 codec: CodecKind,
464 operation: CodecOperation,
465 bytes_processed: usize,
466 reason: InvalidEncodingReason,
467 ) -> Self {
468 Self {
469 kind: CodecErrorKind::InvalidEncoding(reason),
470 codec,
471 contexts: Contexts::None,
472 operation,
473 bytes_processed,
474 source: None,
475 }
476 }
477 /// Creates an invalid encoding error with an underlying source error.
478 ///
479 /// `operation` may be either reading or writing. The supplied error is
480 /// available through [`Error::source`]; if it is an [`io::Error`], it is
481 /// also available through [`io_error`](Self::io_error).
482 pub fn invalid_encoding_for_operation_with_source(
483 codec: CodecKind,
484 operation: CodecOperation,
485 bytes_processed: usize,
486 reason: InvalidEncodingReason,
487 source: impl Error + Send + Sync + 'static,
488 ) -> Self {
489 Self {
490 kind: CodecErrorKind::InvalidEncoding(reason),
491 codec,
492 contexts: Contexts::None,
493 operation,
494 bytes_processed,
495 source: Some(Box::new(source)),
496 }
497 }
498}
499
500impl fmt::Display for CodecError {
501 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
502 match self.kind {
503 CodecErrorKind::Io => write!(
504 formatter,
505 "I/O error while {} {} after {} bytes",
506 self.operation, self.codec, self.bytes_processed
507 )?,
508 CodecErrorKind::UnexpectedEof => write!(
509 formatter,
510 "unexpected end of input while reading {} after {} bytes",
511 self.codec, self.bytes_processed
512 )?,
513 CodecErrorKind::InvalidEncoding(reason) => write!(
514 formatter,
515 "invalid {} encoding after {} bytes: {reason}",
516 self.codec, self.bytes_processed
517 )?,
518 }
519
520 for context in self.contexts() {
521 write!(formatter, " while processing {context}")?;
522 }
523
524 if let Some(source) = &self.source {
525 write!(formatter, ": {source}")?;
526 }
527
528 Ok(())
529 }
530}
531
532impl Error for CodecError {
533 fn source(&self) -> Option<&(dyn Error + 'static)> {
534 self.source
535 .as_deref()
536 .map(|source| source as &(dyn Error + 'static))
537 }
538}
539
540#[cfg(test)]
541mod tests {
542 use super::*;
543
544 fn read_error() -> CodecError {
545 CodecError::from_read_error(
546 CodecKind::VarInt,
547 3,
548 io::Error::new(io::ErrorKind::UnexpectedEof, "stream ended"),
549 )
550 }
551
552 fn write_error() -> CodecError {
553 CodecError::from_write_error(CodecKind::String, 5, io::Error::other("disk full"))
554 }
555
556 fn invalid_encoding_error() -> CodecError {
557 CodecError::invalid_encoding_for_operation(
558 CodecKind::Boolean,
559 CodecOperation::Read,
560 1,
561 InvalidEncodingReason::InvalidBooleanValue { value: 2 },
562 )
563 }
564
565 fn invalid_encoding_with_source() -> CodecError {
566 CodecError::invalid_encoding_for_operation_with_source(
567 CodecKind::JsonTextComponent,
568 CodecOperation::Read,
569 4,
570 InvalidEncodingReason::InvalidJson,
571 io::Error::new(io::ErrorKind::InvalidData, "bad json"),
572 )
573 }
574
575 #[test]
576 fn display_reports_unexpected_eof_operation_and_progress() {
577 assert_eq!(
578 read_error().to_string(),
579 "unexpected end of input while reading VarInt after 3 bytes: stream ended"
580 );
581 }
582
583 #[test]
584 fn display_reports_write_io_errors() {
585 assert_eq!(
586 write_error().to_string(),
587 "I/O error while writing String after 5 bytes: disk full"
588 );
589 }
590
591 #[test]
592 fn display_reports_invalid_encoding_reason() {
593 assert_eq!(
594 invalid_encoding_error().to_string(),
595 "invalid Boolean encoding after 1 bytes: invalid boolean value 0x02"
596 );
597 }
598
599 #[test]
600 fn display_appends_contexts_and_source_in_order() {
601 let error = invalid_encoding_with_source()
602 .with_context(CodecKind::String)
603 .with_context(CodecKind::Identifier)
604 .with_context(CodecKind::TextComponent);
605 assert_eq!(
606 error.to_string(),
607 "invalid JsonTextComponent encoding after 4 bytes: invalid JSON data \
608 while processing String while processing Identifier while processing TextComponent: bad json"
609 );
610 }
611
612 #[test]
613 fn display_omits_contexts_and_source_when_absent() {
614 let error = invalid_encoding_error();
615 assert!(!error.to_string().contains("while processing"));
616 assert!(
617 !error.to_string().ends_with(": invalid boolean value 0x02:"),
618 "a source was rendered when none is stored"
619 );
620 }
621
622 #[test]
623 fn contexts_are_empty_by_default() {
624 let error = read_error();
625 assert!(error.contexts().is_empty());
626 assert_eq!(error.context(), None);
627 }
628
629 #[test]
630 fn single_context_is_reported_inline() {
631 let error = read_error().with_context(CodecKind::String);
632 assert_eq!(error.contexts(), &[CodecKind::String]);
633 assert_eq!(error.context(), Some(CodecKind::String));
634 }
635
636 #[test]
637 fn many_contexts_are_reported_nearest_to_outermost() {
638 let error = invalid_encoding_error()
639 .with_context(CodecKind::String)
640 .with_context(CodecKind::Identifier)
641 .with_context(CodecKind::TextComponent);
642 assert_eq!(
643 error.contexts(),
644 &[
645 CodecKind::String,
646 CodecKind::Identifier,
647 CodecKind::TextComponent
648 ]
649 );
650 assert_eq!(error.context(), Some(CodecKind::TextComponent));
651 assert_eq!(error.codec(), CodecKind::Boolean);
652 }
653
654 #[test]
655 fn io_error_returns_the_underlying_io_error() {
656 let error = read_error();
657 let io_error = error.io_error().expect("io_error() should be Some");
658 assert_eq!(io_error.kind(), io::ErrorKind::UnexpectedEof);
659 assert_eq!(io_error.to_string(), "stream ended");
660 assert_eq!(
661 error
662 .source()
663 .and_then(|source| source.downcast_ref::<io::Error>())
664 .map(io::Error::kind),
665 Some(io::ErrorKind::UnexpectedEof)
666 );
667 }
668
669 #[test]
670 fn io_error_returns_none_for_non_io_sources() {
671 let error = CodecError::invalid_encoding_for_operation_with_source(
672 CodecKind::TextComponent,
673 CodecOperation::Read,
674 0,
675 InvalidEncodingReason::InvalidNbt,
676 NonIoSource,
677 );
678 assert!(error.io_error().is_none());
679 assert!(error.source().is_some());
680 }
681
682 #[derive(Debug)]
683 struct NonIoSource;
684
685 impl fmt::Display for NonIoSource {
686 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
687 formatter.write_str("non-io source")
688 }
689 }
690
691 impl Error for NonIoSource {}
692}