nbt-rust 0.1.1

Fast, idiomatic NBT library for Rust with Bedrock and Java endian variants.
Documentation
use std::fmt;
use std::io;

use thiserror::Error as ThisError;

pub type Result<T> = std::result::Result<T, Error>;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ErrorContext {
    pub op: &'static str,
    pub offset: usize,
    pub field: Option<&'static str>,
}

impl fmt::Display for ErrorContext {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if let Some(field) = self.field {
            write!(
                f,
                "{} failed at offset={} field={}",
                self.op, self.offset, field
            )
        } else {
            write!(f, "{} failed at offset={}", self.op, self.offset)
        }
    }
}

#[derive(Debug, ThisError)]
pub enum Error {
    #[error("i/o error: {0}")]
    Io(#[from] io::Error),
    #[error("{context}: {source}")]
    Context {
        context: ErrorContext,
        #[source]
        source: Box<Error>,
    },
    #[error("serde error: {message}")]
    Serde { message: String },
    #[error("unknown tag id: {id}")]
    UnknownTag { id: u8 },
    #[error("invalid root tag type id: {id}")]
    InvalidRoot { id: u8 },
    #[error("invalid header ({detail}): expected={expected:?}, actual={actual:?}")]
    InvalidHeader {
        detail: &'static str,
        expected: Option<u32>,
        actual: Option<u32>,
    },
    #[error("maximum depth exceeded: depth={depth}, max_depth={max_depth}")]
    DepthExceeded { depth: usize, max_depth: usize },
    #[error("size exceeded for {field}: max={max}, actual={actual}")]
    SizeExceeded {
        field: &'static str,
        max: usize,
        actual: usize,
    },
    #[error("trailing payload bytes: unread={unread}")]
    TrailingPayloadBytes { unread: usize },
    #[error("unexpected TAG_End payload")]
    UnexpectedEndTagPayload,
    #[error("invalid UTF-8 for {field}")]
    InvalidUtf8 { field: &'static str },
    #[error("invalid list header: element_type_id={element_type_id}, length={length}")]
    InvalidListHeader { element_type_id: u8, length: usize },
    #[error("unexpected type ({context}): expected_id={expected_id}, actual_id={actual_id}")]
    UnexpectedType {
        context: &'static str,
        expected_id: u8,
        actual_id: u8,
    },
    #[error("invalid structure shape: {detail}")]
    InvalidStructureShape { detail: &'static str },
    #[error("invalid palette index: index={index}, palette_len={palette_len}")]
    InvalidPaletteIndex { index: i32, palette_len: usize },
    #[error("negative length for {field}: {value}")]
    NegativeLength { field: &'static str, value: i32 },
    #[error("length overflow for {field}: max={max}, actual={actual}")]
    LengthOverflow {
        field: &'static str,
        max: usize,
        actual: usize,
    },
    #[error("invalid varint: {detail}")]
    InvalidVarint { detail: &'static str },
}

impl Error {
    pub fn with_context(
        self,
        op: &'static str,
        offset: usize,
        field: Option<&'static str>,
    ) -> Self {
        Self::Context {
            context: ErrorContext { op, offset, field },
            source: Box::new(self),
        }
    }

    pub fn innermost(&self) -> &Error {
        match self {
            Error::Context { source, .. } => source.innermost(),
            _ => self,
        }
    }

    pub fn has_context(&self, op: &'static str, field: Option<&'static str>) -> bool {
        match self {
            Error::Context { context, source } => {
                if context.op == op && context.field == field {
                    true
                } else {
                    source.has_context(op, field)
                }
            }
            _ => false,
        }
    }
}