use std::{error::Error, fmt, io};
type BoxedError = Box<dyn Error + Send + Sync + 'static>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CodecKind {
VarInt,
VarLong,
Nbt,
Boolean,
Byte,
UnsignedByte,
Short,
UnsignedShort,
Int,
Long,
Float,
Double,
Position,
Angle,
LpVec3,
TeleportFlags,
SoundEvent,
ChatType,
ChatDecoration,
TypeStruct,
Slot,
HashedSlot,
DataComponent,
StructuredComponent,
SlotDisplay,
LightData,
LightArray,
Uuid,
BitSet,
FixedBitSet,
Optional,
PrefixedOptional,
Either,
GameProfile,
GameProfileProperty,
ResolvableProfile,
PartialProfile,
DebugSubscriptionEvent,
DebugSubscriptionUpdate,
DebugSubscriptionData,
DebugPathNode,
DebugStructureInfo,
RecipeDisplay,
ShapedRecipeGrid,
EntityMetadata,
EntityMetadataEntry,
EntityMetadataValue,
Particle,
VibrationSource,
RegistryId,
Array,
ByteArray,
PrefixedArray,
Enum,
IdOr,
IdSet,
String,
Identifier,
TextComponent,
JsonTextComponent,
}
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"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CodecOperation {
Read,
Write,
}
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"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum InvalidEncodingReason {
TooLong {
max_bytes: usize,
},
ValueOutOfRange {
terminal_byte: u8,
allowed_mask: u8,
},
InvalidBooleanValue {
value: u8,
},
StringTooLong {
max_bytes: usize,
},
TooManyUtf16CodeUnits {
max_code_units: usize,
},
NegativeLength {
value: i32,
},
LengthOutOfRange {
max: usize,
actual: usize,
},
InvalidEnumValue {
value: i128,
},
EnumDiscriminantOutOfRange {
value: i128,
},
LpVec3ScaleOutOfRange {
scale_factor: u64,
max: u64,
},
InvalidRegistryId {
value: i32,
max: i32,
},
InvalidEntityMetadataIndex {
index: u8,
},
DuplicateEntityMetadataIndex {
index: u8,
},
InvalidOptionalVarInt {
value: i32,
},
InvalidIdOrSelector {
value: i32,
},
InvalidIdSetType {
value: i32,
},
InvalidSlotCount {
value: i64,
},
InvalidFixedBitSetLength {
expected: usize,
actual: usize,
},
OptionalValueMismatch {
context_present: bool,
value_present: bool,
},
MissingContext {
required: ContextRequirement,
},
ArrayLengthMismatch {
expected: usize,
actual: usize,
},
InvalidUtf8 {
valid_up_to: usize,
error_len: Option<usize>,
},
InvalidIdentifier,
InvalidNbt,
InvalidJson,
InvalidTextComponentRootTag {
tag: u8,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ContextRequirement {
Presence,
Length,
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"),
}
}
}
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}"
),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CodecErrorKind {
Io,
UnexpectedEof,
InvalidEncoding(InvalidEncodingReason),
}
#[derive(Debug)]
pub struct CodecError {
pub kind: CodecErrorKind,
codec: CodecKind,
contexts: Contexts,
operation: CodecOperation,
bytes_processed: usize,
source: Option<BoxedError>,
}
#[derive(Debug, Default)]
enum Contexts {
#[default]
None,
One(CodecKind),
Many(Vec<CodecKind>),
}
impl CodecError {
pub const fn kind(&self) -> CodecErrorKind {
self.kind
}
pub const fn codec(&self) -> CodecKind {
self.codec
}
pub fn context(&self) -> Option<CodecKind> {
self.contexts().last().copied()
}
pub fn contexts(&self) -> &[CodecKind] {
match &self.contexts {
Contexts::None => &[],
Contexts::One(context) => std::slice::from_ref(context),
Contexts::Many(contexts) => contexts,
}
}
pub const fn operation(&self) -> CodecOperation {
self.operation
}
pub const fn bytes_processed(&self) -> usize {
self.bytes_processed
}
pub fn io_error(&self) -> Option<&io::Error> {
self.source.as_deref()?.downcast_ref::<io::Error>()
}
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
}
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)),
}
}
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)),
}
}
pub const fn invalid_encoding(
codec: CodecKind,
bytes_processed: usize,
reason: InvalidEncodingReason,
) -> Self {
Self::invalid_encoding_for_operation(codec, CodecOperation::Read, bytes_processed, reason)
}
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,
}
}
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 {}
}