mcproto-codec 0.5.0

Minecraft protocol codecs.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
//! Errors reported by Minecraft protocol codecs.
//!
//! This module provides structured context for failures while reading and
//! writing the protocol values implemented by `mcproto-codec`.

use std::{error::Error, fmt, io};

type BoxedError = Box<dyn Error + Send + Sync + 'static>;

/// Identifies the protocol codec that reported an error.
///
/// A [`CodecError`] stores the codec that originally reported the error and may
/// also store enclosing codecs as additional context. Protocol descriptions are
/// based on the [Minecraft Java Edition protocol packet format].
///
/// Signed integer codecs use [two's-complement] representation.
///
/// [Minecraft Java Edition protocol packet format]: https://minecraft.wiki/w/Java_Edition_protocol/Packets
/// [two's-complement]: https://en.wikipedia.org/wiki/Two%27s_complement
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CodecKind {
    /// A variable-length, two's-complement signed 32-bit integer.
    ///
    /// Values range from -2,147,483,648 through 2,147,483,647.
    VarInt,
    /// A variable-length, two's-complement signed 64-bit integer.
    ///
    /// Values range from -9,223,372,036,854,775,808 through
    /// 9,223,372,036,854,775,807.
    VarLong,
    /// A complete Named Binary Tag value.
    ///
    /// The value is encoded and decoded using `fastnbt`.
    Nbt,
    /// A boolean encoded as `0x00` for false or `0x01` for true.
    Boolean,
    /// A two's-complement signed 8-bit integer from -128 through 127.
    Byte,
    /// An unsigned 8-bit integer from 0 through 255.
    UnsignedByte,
    /// A two's-complement signed 16-bit integer from -32,768 through 32,767.
    Short,
    /// An unsigned 16-bit integer from 0 through 65,535.
    UnsignedShort,
    /// A two's-complement signed 32-bit integer from -2,147,483,648 through
    /// 2,147,483,647.
    Int,
    /// A two's-complement signed 64-bit integer from -9,223,372,036,854,775,808
    /// through 9,223,372,036,854,775,807.
    Long,
    /// A big-endian IEEE-754 single-precision floating-point number.
    Float,
    /// A big-endian IEEE-754 double-precision floating-point number.
    Double,
    /// A block position packed into a 64-bit integer.
    ///
    /// The x, z, and y coordinates occupy 26, 26, and 12 bits respectively.
    Position,
    /// A rotation angle encoded in 1/256 turn steps.
    Angle,
    /// Three quantized doubles packed with a shared scale factor.
    LpVec3,
    /// An Int bit field controlling relative teleportation behavior.
    TeleportFlags,
    /// A named sound with an optional fixed playback range.
    SoundEvent,
    /// A direct chat type containing chat and narration decorations.
    ChatType,
    /// A chat type decoration containing a translation key, parameters, and style.
    ChatDecoration,
    /// A structure encoded field-by-field in declaration order.
    TypeStruct,
    /// An inventory item stack and its data component patch.
    Slot,
    /// An optional item stack whose added component values are CRC32C hashes.
    HashedSlot,
    /// A typed data component attached to an item stack.
    DataComponent,
    /// The payload of a typed data component.
    StructuredComponent,
    /// A typed, potentially recursive recipe slot display.
    SlotDisplay,
    /// Chunk-section sky and block lighting masks and arrays.
    LightData,
    /// Exactly 2048 packed bytes containing 4096 four-bit light values.
    LightArray,
    /// A 128-bit universally unique identifier.
    Uuid,
    /// A length-prefixed bit set of packed 64-bit words.
    BitSet,
    /// A fixed-length bit set of packed bytes.
    FixedBitSet,
    /// A value whose presence is determined by an enclosing protocol context.
    Optional,
    /// An optional value prefixed by an encoded boolean presence marker.
    PrefixedOptional,
    /// A boolean-selected value of one of two protocol types.
    Either,
    /// A UUID, username, and bounded list of profile properties.
    GameProfile,
    /// One name, value, and optional signature in a game profile.
    GameProfileProperty,
    /// A partial or complete game profile with optional skin overrides.
    ResolvableProfile,
    /// The unresolved identity fields of a resolvable profile.
    PartialProfile,
    /// A typed debug subscription event.
    DebugSubscriptionEvent,
    /// A typed debug subscription update.
    DebugSubscriptionUpdate,
    /// A payload selected by a debug subscription type.
    DebugSubscriptionData,
    /// One pathfinding node in debug subscription data.
    DebugPathNode,
    /// Structure and piece bounding boxes in debug subscription data.
    DebugStructureInfo,
    /// A typed client recipe display.
    RecipeDisplay,
    /// A shaped recipe's dimensions and rectangular ingredient array.
    ShapedRecipeGrid,
    /// A terminated sequence of indexed entity metadata values.
    EntityMetadata,
    /// One indexed value in an entity metadata sequence.
    EntityMetadataEntry,
    /// A value selected by an entity metadata type ID.
    EntityMetadataValue,
    /// A particle type ID and its type-specific payload.
    Particle,
    /// A source selected by a vibration particle's position-source type ID.
    VibrationSource,
    /// A non-negative ID in a protocol registry.
    RegistryId,
    /// A sequence whose element count is supplied by protocol context.
    Array,
    /// A raw sequence of bytes whose length is supplied by protocol context.
    ByteArray,
    /// A sequence prefixed by its element count as a VarInt.
    PrefixedArray,
    /// A value selected from a fixed protocol enumeration.
    Enum,
    /// A registry ID or an inline protocol value.
    IdOr,
    /// Registry IDs enumerated inline or referenced through a tag.
    IdSet,
    /// A UTF-8 string prefixed by its byte length as a VarInt.
    ///
    /// The protocol limits both the UTF-8 payload size and the number of UTF-16
    /// code units. Supplementary [Unicode scalar values] count as two UTF-16
    /// code units. The general protocol limit is 32,767 UTF-16 code units and
    /// three UTF-8 bytes per permitted code unit; a particular field may impose
    /// a lower limit.
    ///
    /// [Unicode scalar values]: https://www.unicode.org/glossary/#unicode_scalar_value
    String,
    /// A resource identifier encoded as a [`String`](Self::String).
    ///
    /// The namespace permits `[a-z0-9._-]`; the value permits
    /// `[a-z0-9._/-]`. See the protocol's [identifier format] for details.
    ///
    /// [identifier format]: https://minecraft.wiki/w/Java_Edition_protocol/Packets#Identifier
    Identifier,
    /// A text component encoded as an NBT tag.
    ///
    /// Plain text-only components may use an NBT string tag. Components with
    /// styling, events, or other data use an NBT compound tag. See the
    /// [text component format] and [NBT specification].
    ///
    /// [text component format]: https://minecraft.wiki/w/Text_component_format
    /// [NBT specification]: https://minecraft.wiki/w/NBT_format
    TextComponent,
    /// A text component encoded as JSON in a protocol string.
    ///
    /// Since Java Edition 1.20.3, the vanilla implementation permits up to
    /// 262,144 UTF-16 code units when decoding but refuses to encode more than
    /// 32,767. See the [text component format].
    ///
    /// [text component format]: https://minecraft.wiki/w/Text_component_format
    JsonTextComponent,
}

/// Formats a codec kind using its protocol name, such as `VarInt` or `Boolean`.
impl fmt::Display for CodecKind {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::VarInt => formatter.write_str("VarInt"),
            Self::VarLong => formatter.write_str("VarLong"),
            Self::Nbt => formatter.write_str("Nbt"),
            Self::Boolean => formatter.write_str("Boolean"),
            Self::Byte => formatter.write_str("Byte"),
            Self::UnsignedByte => formatter.write_str("UnsignedByte"),
            Self::Short => formatter.write_str("Short"),
            Self::UnsignedShort => formatter.write_str("UnsignedShort"),
            Self::Int => formatter.write_str("Int"),
            Self::Long => formatter.write_str("Long"),
            Self::Float => formatter.write_str("Float"),
            Self::Double => formatter.write_str("Double"),
            Self::Position => formatter.write_str("Position"),
            Self::Angle => formatter.write_str("Angle"),
            Self::LpVec3 => formatter.write_str("LpVec3"),
            Self::TeleportFlags => formatter.write_str("Teleport Flags"),
            Self::SoundEvent => formatter.write_str("Sound Event"),
            Self::ChatType => formatter.write_str("Chat Type"),
            Self::ChatDecoration => formatter.write_str("Chat Decoration"),
            Self::TypeStruct => formatter.write_str("Type Struct"),
            Self::Slot => formatter.write_str("Slot"),
            Self::HashedSlot => formatter.write_str("Hashed Slot"),
            Self::DataComponent => formatter.write_str("Data Component"),
            Self::StructuredComponent => formatter.write_str("Structured Component"),
            Self::SlotDisplay => formatter.write_str("Slot Display"),
            Self::LightData => formatter.write_str("Light Data"),
            Self::LightArray => formatter.write_str("Light Array"),
            Self::Uuid => formatter.write_str("UUID"),
            Self::BitSet => formatter.write_str("BitSet"),
            Self::FixedBitSet => formatter.write_str("Fixed BitSet"),
            Self::Optional => formatter.write_str("Optional"),
            Self::PrefixedOptional => formatter.write_str("Prefixed Optional"),
            Self::Either => formatter.write_str("Either"),
            Self::GameProfile => formatter.write_str("Game Profile"),
            Self::GameProfileProperty => formatter.write_str("Game Profile Property"),
            Self::ResolvableProfile => formatter.write_str("Resolvable Profile"),
            Self::PartialProfile => formatter.write_str("Partial Profile"),
            Self::DebugSubscriptionEvent => formatter.write_str("Debug Subscription Event"),
            Self::DebugSubscriptionUpdate => formatter.write_str("Debug Subscription Update"),
            Self::DebugSubscriptionData => formatter.write_str("Debug Subscription Data"),
            Self::DebugPathNode => formatter.write_str("Debug Path Node"),
            Self::DebugStructureInfo => formatter.write_str("Debug Structure Info"),
            Self::RecipeDisplay => formatter.write_str("Recipe Display"),
            Self::ShapedRecipeGrid => formatter.write_str("Shaped Recipe Grid"),
            Self::EntityMetadata => formatter.write_str("Entity Metadata"),
            Self::EntityMetadataEntry => formatter.write_str("Entity Metadata Entry"),
            Self::EntityMetadataValue => formatter.write_str("Entity Metadata Value"),
            Self::Particle => formatter.write_str("Particle"),
            Self::VibrationSource => formatter.write_str("Vibration Source"),
            Self::RegistryId => formatter.write_str("Registry ID"),
            Self::Array => formatter.write_str("Array"),
            Self::ByteArray => formatter.write_str("Byte Array"),
            Self::PrefixedArray => formatter.write_str("Prefixed Array"),
            Self::Enum => formatter.write_str("Enum"),
            Self::IdOr => formatter.write_str("ID or X"),
            Self::IdSet => formatter.write_str("ID Set"),
            Self::String => formatter.write_str("String"),
            Self::Identifier => formatter.write_str("Identifier"),
            Self::TextComponent => formatter.write_str("TextComponent"),
            Self::JsonTextComponent => formatter.write_str("JsonTextComponent"),
        }
    }
}

/// Identifies whether an error occurred while decoding or encoding data.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CodecOperation {
    /// A read (decoding) operation.
    Read,
    /// A write (encoding) operation.
    Write,
}

/// Formats an operation as `reading` or `writing`.
impl fmt::Display for CodecOperation {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Read => formatter.write_str("reading"),
            Self::Write => formatter.write_str("writing"),
        }
    }
}
/// Describes why encoded protocol data is invalid.
///
/// Some reasons describe a mismatch between a codec and the context supplied
/// by its enclosing packet.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum InvalidEncodingReason {
    /// The encoding exceeds the maximum allowed length in bytes.
    TooLong {
        /// The maximum number of bytes permitted for this encoding.
        max_bytes: usize,
    },
    /// The terminal byte of the encoding contains bits outside the allowed mask.
    ValueOutOfRange {
        /// The final byte that contains disallowed bits.
        terminal_byte: u8,
        /// A mask whose set bits identify the permitted bits in the final byte.
        allowed_mask: u8,
    },
    /// The boolean value is invalid (not 0x00 or 0x01).
    InvalidBooleanValue {
        /// The byte read instead of the permitted `0x00` or `0x01`.
        value: u8,
    },
    /// The string exceeds the maximum allowed length in bytes when encoded in UTF-8.
    StringTooLong {
        /// The maximum permitted size of the UTF-8 payload, excluding its
        /// VarInt length prefix.
        max_bytes: usize,
    },
    /// The string exceeds the maximum allowed length in UTF-16 code units.
    TooManyUtf16CodeUnits {
        /// The maximum permitted number of UTF-16 code units.
        max_code_units: usize,
    },
    /// The length of the data is negative, which is invalid.
    NegativeLength {
        /// The negative length decoded from the data.
        value: i32,
    },
    /// The length cannot be represented by the encoded length prefix.
    LengthOutOfRange {
        /// The greatest length representable by the prefix.
        max: usize,
        /// The length that was to be encoded.
        actual: usize,
    },
    /// A decoded numeric enum value does not name a declared variant.
    InvalidEnumValue {
        /// The numeric value decoded from the enum's wire representation.
        value: i128,
    },
    /// An enum variant's numeric discriminant cannot be represented on the wire.
    EnumDiscriminantOutOfRange {
        /// The numeric discriminant that cannot be encoded.
        value: i128,
    },
    /// An LpVec3 scale factor exceeds the 34-bit wire representation.
    LpVec3ScaleOutOfRange {
        /// The rounded-up scale factor that was to be encoded.
        scale_factor: u64,
        /// The greatest scale factor representable by the format.
        max: u64,
    },
    /// A registry ID cannot be represented by the `ID or X` wire format.
    InvalidRegistryId {
        /// The invalid registry ID.
        value: i32,
        /// The greatest registry ID supported by the enclosing wire format.
        max: i32,
    },
    /// An entity metadata entry uses the reserved `0xff` terminator as its index.
    InvalidEntityMetadataIndex {
        /// The invalid entry index.
        index: u8,
    },
    /// An entity metadata sequence contains the same index more than once.
    DuplicateEntityMetadataIndex {
        /// The repeated entry index.
        index: u8,
    },
    /// An Optional VarInt selector cannot be mapped to a present value.
    InvalidOptionalVarInt {
        /// The invalid selector or in-memory value.
        value: i32,
    },
    /// The decoded `ID or X` selector is negative.
    InvalidIdOrSelector {
        /// The invalid selector value read from the wire.
        value: i32,
    },
    /// The decoded `ID Set` type value is negative.
    InvalidIdSetType {
        /// The invalid type value read from the wire.
        value: i32,
    },
    /// A non-empty item stack count must fit a positive VarInt.
    InvalidSlotCount {
        /// The invalid count.
        value: i64,
    },
    /// The packed byte array does not have the required fixed length.
    InvalidFixedBitSetLength {
        /// The expected number of packed bytes.
        expected: usize,
        /// The actual number of packed bytes.
        actual: usize,
    },
    /// An optional value does not agree with its externally supplied context.
    OptionalValueMismatch {
        /// Whether the context says that the value is present on the wire.
        context_present: bool,
        /// Whether the value held by the wrapper is present in memory.
        value_present: bool,
    },
    /// A contextual codec was used without the information it requires.
    MissingContext {
        /// The kind of information that was not supplied.
        required: ContextRequirement,
    },
    /// The number of array values does not match the contextual length.
    ArrayLengthMismatch {
        /// The element count required by the context.
        expected: usize,
        /// The element count held by the array.
        actual: usize,
    },
    /// The data contains an invalid UTF-8 sequence.
    InvalidUtf8 {
        /// The byte offset in the UTF-8 payload up to which the data is valid.
        valid_up_to: usize,
        /// The length of the invalid sequence, or `None` if the input ends in
        /// an incomplete sequence.
        error_len: Option<usize>,
    },
    /// The data is not a valid Minecraft identifier.
    InvalidIdentifier,
    /// The data is not valid NBT (Named Binary Tag) data.
    InvalidNbt,
    /// The data is not valid JSON.
    InvalidJson,
    /// The root tag of a text component is invalid (not TAG_String or TAG_Compound).
    InvalidTextComponentRootTag {
        /// The unsupported NBT root tag identifier.
        tag: u8,
    },
}

/// Identifies information required by a contextual codec.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ContextRequirement {
    /// Whether a value is present on the wire.
    Presence,
    /// The number of elements in a contextual array.
    Length,
    /// A context for an individual array element.
    ElementContext,
}

impl fmt::Display for ContextRequirement {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Presence => formatter.write_str("presence"),
            Self::Length => formatter.write_str("array length"),
            Self::ElementContext => formatter.write_str("array element context"),
        }
    }
}

/// Formats an invalid encoding reason as a diagnostic message.
impl fmt::Display for InvalidEncodingReason {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::TooLong { max_bytes } => {
                write!(formatter, "encoding exceeds the {max_bytes}-byte limit")
            }
            Self::ValueOutOfRange {
                terminal_byte,
                allowed_mask,
            } => write!(
                formatter,
                "terminal byte 0x{terminal_byte:02X} contains bits outside mask 0x{allowed_mask:02X}"
            ),
            Self::InvalidBooleanValue { value } => {
                write!(formatter, "invalid boolean value 0x{value:02X}")
            }
            Self::StringTooLong { max_bytes } => {
                write!(formatter, "string exceeds the {max_bytes}-byte UTF-8 limit")
            }
            Self::TooManyUtf16CodeUnits { max_code_units } => write!(
                formatter,
                "string exceeds the {max_code_units}-code-unit UTF-16 limit"
            ),
            Self::NegativeLength { value } => {
                write!(formatter, "length cannot be negative: {value}")
            }
            Self::LengthOutOfRange { max, actual } => {
                write!(formatter, "length {actual} exceeds the maximum of {max}")
            }
            Self::InvalidEnumValue { value } => {
                write!(formatter, "invalid enum value: {value}")
            }
            Self::EnumDiscriminantOutOfRange { value } => {
                write!(formatter, "enum discriminant cannot be encoded: {value}")
            }
            Self::LpVec3ScaleOutOfRange { scale_factor, max } => write!(
                formatter,
                "LpVec3 scale factor {scale_factor} exceeds the maximum of {max}"
            ),
            Self::InvalidRegistryId { value, max } => write!(
                formatter,
                "registry ID must be between 0 and {max}, got {value}"
            ),
            Self::InvalidEntityMetadataIndex { index } => write!(
                formatter,
                "entity metadata index 0x{index:02X} is reserved as the terminator"
            ),
            Self::DuplicateEntityMetadataIndex { index } => {
                write!(formatter, "duplicate entity metadata index {index}")
            }
            Self::InvalidOptionalVarInt { value } => {
                write!(formatter, "invalid Optional VarInt value: {value}")
            }
            Self::InvalidIdOrSelector { value } => {
                write!(formatter, "ID or X selector cannot be negative: {value}")
            }
            Self::InvalidIdSetType { value } => {
                write!(formatter, "ID Set type cannot be negative: {value}")
            }
            Self::InvalidSlotCount { value } => {
                write!(
                    formatter,
                    "invalid item-stack count {value}; expected 1..={}",
                    i32::MAX
                )
            }
            Self::InvalidFixedBitSetLength { expected, actual } => write!(
                formatter,
                "fixed bit set requires {expected} packed bytes, got {actual}"
            ),
            Self::OptionalValueMismatch {
                context_present,
                value_present,
            } => write!(
                formatter,
                "optional value presence ({value_present}) does not match context ({context_present})"
            ),
            Self::MissingContext { required } => {
                write!(formatter, "missing required codec context: {required}")
            }
            Self::ArrayLengthMismatch { expected, actual } => write!(
                formatter,
                "array contains {actual} elements, but context requires {expected}"
            ),
            Self::InvalidUtf8 {
                valid_up_to,
                error_len: Some(error_len),
            } => write!(
                formatter,
                "invalid UTF-8 sequence of {error_len} bytes at byte {valid_up_to}"
            ),
            Self::InvalidUtf8 {
                valid_up_to,
                error_len: None,
            } => write!(
                formatter,
                "incomplete UTF-8 sequence starting at byte {valid_up_to}"
            ),
            Self::InvalidIdentifier => formatter.write_str("invalid Minecraft identifier"),
            Self::InvalidNbt => formatter.write_str("invalid NBT data"),
            Self::InvalidJson => formatter.write_str("invalid JSON data"),
            Self::InvalidTextComponentRootTag { tag } => write!(
                formatter,
                "text component root tag must be TAG_String (8) or TAG_Compound (10), got {tag}"
            ),
        }
    }
}
/// Classifies an error reported by a protocol codec.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CodecErrorKind {
    /// An I/O error other than an unexpected end of input occurred.
    Io,
    /// A read ended before the codec received all required bytes.
    UnexpectedEof,
    /// The data could not be decoded or encoded according to the codec's
    /// format or limits.
    InvalidEncoding(InvalidEncodingReason),
}

/// An error produced while reading or writing protocol data.
///
/// The error records the originating [`CodecKind`], the [`CodecOperation`], the
/// progress within that codec, and optional enclosing codec contexts. I/O and
/// parser errors are retained as an error [`source`](Error::source).
///
/// Error enums are non-exhaustive, so downstream matches must include a
/// wildcard arm.
///
/// # Example
///
/// ```
/// use mcproto_codec::{
///     error::{CodecErrorKind, CodecKind, CodecOperation},
///     varint::VarIntRead,
/// };
///
/// let mut input = [0x80].as_slice();
/// let error = input
///     .read_varint()
///     .unwrap_err()
///     .with_context(CodecKind::String);
///
/// assert_eq!(error.codec(), CodecKind::VarInt);
/// assert_eq!(error.operation(), CodecOperation::Read);
/// assert_eq!(error.bytes_processed(), 1);
/// assert_eq!(error.contexts(), &[CodecKind::String]);
///
/// match error.kind() {
///     CodecErrorKind::UnexpectedEof => {}
///     _ => panic!("unexpected error: {error}"),
/// }
/// ```
#[derive(Debug)]
pub struct CodecError {
    /// The error classification.
    ///
    /// This field and [`kind`](Self::kind) expose the same value. The accessor
    /// is convenient when working through a shared reference.
    pub kind: CodecErrorKind,
    codec: CodecKind,
    contexts: Contexts,
    operation: CodecOperation,
    bytes_processed: usize,
    source: Option<BoxedError>,
}

/// Stores the enclosing codec contexts of a [`CodecError`].
///
/// The common cases of zero or one context are stored without heap allocation;
/// only longer chains fall back to a [`Vec`].
#[derive(Debug, Default)]
enum Contexts {
    /// No enclosing contexts.
    #[default]
    None,
    /// A single context, stored inline.
    One(CodecKind),
    /// Two or more contexts, stored in a heap-allocated vector.
    Many(Vec<CodecKind>),
}

impl CodecError {
    /// Returns the error classification.
    pub const fn kind(&self) -> CodecErrorKind {
        self.kind
    }
    /// Returns the codec that originally reported the error.
    pub const fn codec(&self) -> CodecKind {
        self.codec
    }
    /// Returns the outermost enclosing codec context, if one was added.
    ///
    /// This is the last element of [`contexts`](Self::contexts), not the
    /// originating codec returned by [`codec`](Self::codec).
    pub fn context(&self) -> Option<CodecKind> {
        self.contexts().last().copied()
    }
    /// Returns all enclosing codec contexts, ordered from nearest to outermost.
    ///
    /// The originating codec is not included. Each call to
    /// [`with_context`](Self::with_context) appends one element.
    pub fn contexts(&self) -> &[CodecKind] {
        match &self.contexts {
            Contexts::None => &[],
            Contexts::One(context) => std::slice::from_ref(context),
            Contexts::Many(contexts) => contexts,
        }
    }
    /// Returns the operation being performed when the error occurred.
    pub const fn operation(&self) -> CodecOperation {
        self.operation
    }
    /// Returns the byte progress reported by the originating codec.
    ///
    /// Built-in codecs count bytes from the start of their encoded value. Bytes
    /// successfully read or written before an I/O failure are included. A byte
    /// that was read and then found to be invalid is also included. For a
    /// length-prefixed value, the originating codec determines whether its
    /// prefix is part of the count.
    ///
    /// Adding an outer context does not translate this value into an offset
    /// within the enclosing codec.
    pub const fn bytes_processed(&self) -> usize {
        self.bytes_processed
    }
    /// Returns the underlying [`io::Error`], if the source is an I/O error.
    ///
    /// Invalid NBT or JSON errors may have a non-I/O source; access those
    /// through [`Error::source`] instead.
    pub fn io_error(&self) -> Option<&io::Error> {
        self.source.as_deref()?.downcast_ref::<io::Error>()
    }

    /// Adds an enclosing codec to the error's context chain.
    ///
    /// Contexts should be added as the error propagates outward. Repeated calls
    /// therefore order [`contexts`](Self::contexts) from nearest to outermost,
    /// and [`context`](Self::context) returns the most recently added context.
    pub fn with_context(mut self, context: CodecKind) -> Self {
        self.contexts = match self.contexts {
            Contexts::None => Contexts::One(context),
            Contexts::One(first) => Contexts::Many(vec![first, context]),
            Contexts::Many(mut contexts) => {
                contexts.push(context);
                Contexts::Many(contexts)
            }
        };
        self
    }
    /// Creates an error from an I/O failure that occurred while reading.
    ///
    /// [`io::ErrorKind::UnexpectedEof`] maps to
    /// [`CodecErrorKind::UnexpectedEof`]; every other error kind maps to
    /// [`CodecErrorKind::Io`]. The source error is retained.
    ///
    /// `bytes_processed` is the number of bytes read before `source` occurred.
    pub fn from_read_error(codec: CodecKind, bytes_processed: usize, source: io::Error) -> Self {
        let kind = if source.kind() == io::ErrorKind::UnexpectedEof {
            CodecErrorKind::UnexpectedEof
        } else {
            CodecErrorKind::Io
        };

        Self {
            kind,
            codec,
            contexts: Contexts::None,
            operation: CodecOperation::Read,
            bytes_processed,
            source: Some(Box::new(source)),
        }
    }
    /// Creates an error from an I/O failure that occurred while writing.
    ///
    /// All write errors map to [`CodecErrorKind::Io`], and the source error is
    /// retained. `bytes_processed` is the number of bytes written before
    /// `source` occurred.
    pub fn from_write_error(codec: CodecKind, bytes_processed: usize, source: io::Error) -> Self {
        Self {
            kind: CodecErrorKind::Io,
            codec,
            contexts: Contexts::None,
            operation: CodecOperation::Write,
            bytes_processed,
            source: Some(Box::new(source)),
        }
    }
    /// Creates an invalid encoding error for a read operation.
    ///
    /// Use [`invalid_encoding_for_operation`](Self::invalid_encoding_for_operation)
    /// when the operation is not necessarily [`CodecOperation::Read`].
    pub const fn invalid_encoding(
        codec: CodecKind,
        bytes_processed: usize,
        reason: InvalidEncodingReason,
    ) -> Self {
        Self::invalid_encoding_for_operation(codec, CodecOperation::Read, bytes_processed, reason)
    }

    /// Creates an invalid encoding error for the specified operation.
    ///
    /// Unlike [`invalid_encoding`](Self::invalid_encoding), this constructor
    /// does not assume that the error occurred while reading.
    pub const fn invalid_encoding_for_operation(
        codec: CodecKind,
        operation: CodecOperation,
        bytes_processed: usize,
        reason: InvalidEncodingReason,
    ) -> Self {
        Self {
            kind: CodecErrorKind::InvalidEncoding(reason),
            codec,
            contexts: Contexts::None,
            operation,
            bytes_processed,
            source: None,
        }
    }
    /// Creates an invalid encoding error with an underlying source error.
    ///
    /// `operation` may be either reading or writing. The supplied error is
    /// available through [`Error::source`]; if it is an [`io::Error`], it is
    /// also available through [`io_error`](Self::io_error).
    pub fn invalid_encoding_for_operation_with_source(
        codec: CodecKind,
        operation: CodecOperation,
        bytes_processed: usize,
        reason: InvalidEncodingReason,
        source: impl Error + Send + Sync + 'static,
    ) -> Self {
        Self {
            kind: CodecErrorKind::InvalidEncoding(reason),
            codec,
            contexts: Contexts::None,
            operation,
            bytes_processed,
            source: Some(Box::new(source)),
        }
    }
}

impl fmt::Display for CodecError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self.kind {
            CodecErrorKind::Io => write!(
                formatter,
                "I/O error while {} {} after {} bytes",
                self.operation, self.codec, self.bytes_processed
            )?,
            CodecErrorKind::UnexpectedEof => write!(
                formatter,
                "unexpected end of input while reading {} after {} bytes",
                self.codec, self.bytes_processed
            )?,
            CodecErrorKind::InvalidEncoding(reason) => write!(
                formatter,
                "invalid {} encoding after {} bytes: {reason}",
                self.codec, self.bytes_processed
            )?,
        }

        for context in self.contexts() {
            write!(formatter, " while processing {context}")?;
        }

        if let Some(source) = &self.source {
            write!(formatter, ": {source}")?;
        }

        Ok(())
    }
}

impl Error for CodecError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        self.source
            .as_deref()
            .map(|source| source as &(dyn Error + 'static))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn read_error() -> CodecError {
        CodecError::from_read_error(
            CodecKind::VarInt,
            3,
            io::Error::new(io::ErrorKind::UnexpectedEof, "stream ended"),
        )
    }

    fn write_error() -> CodecError {
        CodecError::from_write_error(CodecKind::String, 5, io::Error::other("disk full"))
    }

    fn invalid_encoding_error() -> CodecError {
        CodecError::invalid_encoding_for_operation(
            CodecKind::Boolean,
            CodecOperation::Read,
            1,
            InvalidEncodingReason::InvalidBooleanValue { value: 2 },
        )
    }

    fn invalid_encoding_with_source() -> CodecError {
        CodecError::invalid_encoding_for_operation_with_source(
            CodecKind::JsonTextComponent,
            CodecOperation::Read,
            4,
            InvalidEncodingReason::InvalidJson,
            io::Error::new(io::ErrorKind::InvalidData, "bad json"),
        )
    }

    #[test]
    fn display_reports_unexpected_eof_operation_and_progress() {
        assert_eq!(
            read_error().to_string(),
            "unexpected end of input while reading VarInt after 3 bytes: stream ended"
        );
    }

    #[test]
    fn display_reports_write_io_errors() {
        assert_eq!(
            write_error().to_string(),
            "I/O error while writing String after 5 bytes: disk full"
        );
    }

    #[test]
    fn display_reports_invalid_encoding_reason() {
        assert_eq!(
            invalid_encoding_error().to_string(),
            "invalid Boolean encoding after 1 bytes: invalid boolean value 0x02"
        );
    }

    #[test]
    fn display_appends_contexts_and_source_in_order() {
        let error = invalid_encoding_with_source()
            .with_context(CodecKind::String)
            .with_context(CodecKind::Identifier)
            .with_context(CodecKind::TextComponent);
        assert_eq!(
            error.to_string(),
            "invalid JsonTextComponent encoding after 4 bytes: invalid JSON data \
             while processing String while processing Identifier while processing TextComponent: bad json"
        );
    }

    #[test]
    fn display_omits_contexts_and_source_when_absent() {
        let error = invalid_encoding_error();
        assert!(!error.to_string().contains("while processing"));
        assert!(
            !error.to_string().ends_with(": invalid boolean value 0x02:"),
            "a source was rendered when none is stored"
        );
    }

    #[test]
    fn contexts_are_empty_by_default() {
        let error = read_error();
        assert!(error.contexts().is_empty());
        assert_eq!(error.context(), None);
    }

    #[test]
    fn single_context_is_reported_inline() {
        let error = read_error().with_context(CodecKind::String);
        assert_eq!(error.contexts(), &[CodecKind::String]);
        assert_eq!(error.context(), Some(CodecKind::String));
    }

    #[test]
    fn many_contexts_are_reported_nearest_to_outermost() {
        let error = invalid_encoding_error()
            .with_context(CodecKind::String)
            .with_context(CodecKind::Identifier)
            .with_context(CodecKind::TextComponent);
        assert_eq!(
            error.contexts(),
            &[
                CodecKind::String,
                CodecKind::Identifier,
                CodecKind::TextComponent
            ]
        );
        assert_eq!(error.context(), Some(CodecKind::TextComponent));
        assert_eq!(error.codec(), CodecKind::Boolean);
    }

    #[test]
    fn io_error_returns_the_underlying_io_error() {
        let error = read_error();
        let io_error = error.io_error().expect("io_error() should be Some");
        assert_eq!(io_error.kind(), io::ErrorKind::UnexpectedEof);
        assert_eq!(io_error.to_string(), "stream ended");
        assert_eq!(
            error
                .source()
                .and_then(|source| source.downcast_ref::<io::Error>())
                .map(io::Error::kind),
            Some(io::ErrorKind::UnexpectedEof)
        );
    }

    #[test]
    fn io_error_returns_none_for_non_io_sources() {
        let error = CodecError::invalid_encoding_for_operation_with_source(
            CodecKind::TextComponent,
            CodecOperation::Read,
            0,
            InvalidEncodingReason::InvalidNbt,
            NonIoSource,
        );
        assert!(error.io_error().is_none());
        assert!(error.source().is_some());
    }

    #[derive(Debug)]
    struct NonIoSource;

    impl fmt::Display for NonIoSource {
        fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
            formatter.write_str("non-io source")
        }
    }

    impl Error for NonIoSource {}
}