tpack-core 0.1.0

Core wire codec, schema AST, validation, and native traits for TPACK
Documentation
use alloc::{borrow::Cow, boxed::Box, string::String, vec::Vec};
use core::{fmt, fmt::Write};

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

#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ErrorPath {
    segments: Vec<PathSegment>,
}

impl ErrorPath {
    pub fn is_empty(&self) -> bool {
        self.segments.is_empty()
    }

    pub fn segments(&self) -> &[PathSegment] {
        &self.segments
    }

    pub fn prepend(&mut self, segment: PathSegment) {
        self.segments.insert(0, segment);
    }
}

impl fmt::Display for ErrorPath {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        for segment in &self.segments {
            f.write_str("/")?;
            match segment {
                PathSegment::Field(name) => write_pointer_segment(f, name)?,
                PathSegment::Index(index) => write!(f, "{index}")?,
                PathSegment::Key => f.write_str("<key>")?,
                PathSegment::Value => f.write_str("<value>")?,
            }
        }
        Ok(())
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PathSegment {
    Field(String),
    Index(usize),
    Key,
    Value,
}

impl PathSegment {
    pub fn field(name: impl Into<String>) -> Self {
        Self::Field(name.into())
    }

    pub fn index(index: usize) -> Self {
        Self::Index(index)
    }
}

fn write_pointer_segment(f: &mut fmt::Formatter<'_>, segment: &str) -> fmt::Result {
    for ch in segment.chars() {
        match ch {
            '~' => f.write_str("~0")?,
            '/' => f.write_str("~1")?,
            _ => f.write_char(ch)?,
        }
    }
    Ok(())
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Error {
    kind: ErrorKind,
    path: ErrorPath,
    source: Option<ErrorSource>,
}

impl Error {
    pub fn new(kind: ErrorKind) -> Self {
        Self {
            kind,
            path: ErrorPath::default(),
            source: None,
        }
    }

    pub fn at_field(mut self, field: impl Into<String>) -> Self {
        self.path.prepend(PathSegment::field(field));
        self
    }

    pub fn at_index(mut self, index: usize) -> Self {
        self.path.prepend(PathSegment::index(index));
        self
    }

    pub fn at_key(mut self) -> Self {
        self.path.prepend(PathSegment::Key);
        self
    }

    pub fn at_value(mut self) -> Self {
        self.path.prepend(PathSegment::Value);
        self
    }

    pub fn with_source(mut self, source: impl Into<ErrorSource>) -> Self {
        self.source = Some(source.into());
        self
    }

    pub fn type_mismatch(expected: &'static str) -> Self {
        Self::new(ErrorKind::TypeMismatch { expected })
    }

    pub fn kind(&self) -> &ErrorKind {
        &self.kind
    }

    pub fn path(&self) -> &ErrorPath {
        &self.path
    }

    pub fn source_ref(&self) -> Option<&ErrorSource> {
        self.source.as_ref()
    }

    pub(crate) fn invalid(message: impl Into<Cow<'static, str>>) -> Self {
        Self::new(ErrorKind::Invalid(message.into()))
    }

    pub(crate) fn limit(name: &'static str) -> Self {
        Self::new(ErrorKind::LimitExceeded(name))
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ErrorSource {
    Core(Box<Error>),
    Utf8(core::str::Utf8Error),
}

impl From<Error> for ErrorSource {
    fn from(error: Error) -> Self {
        Self::Core(Box::new(error))
    }
}

impl From<core::str::Utf8Error> for ErrorSource {
    fn from(error: core::str::Utf8Error) -> Self {
        Self::Utf8(error)
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ErrorKind {
    UnexpectedEof,
    TrailingBytes,
    InvalidMagic,
    UnsupportedVersion(u8),
    UnknownEnvelopeMode(u8),
    UnknownTypeTag(u8),
    OverlongVarint,
    VarintOverflow,
    SchemaLengthMismatch,
    SchemaLengthExceeded,
    InvalidSchemaId,
    UnknownSchemaId,
    SchemaRefNotAllowed,
    EmbeddedSchemaMismatch,
    InvalidDecimalParameters,
    InvalidTimestampPrecision(u8),
    StructFieldIdZero,
    StructFieldNameEmpty,
    StructFieldFlagsNonZero(u64),
    DuplicateStructFieldDefinition,
    DuplicateStructFieldValue,
    MissingStructFieldValue,
    InvalidMapKeyType,
    DuplicateMapKey,
    NonCanonicalMapKeyOrder,
    NaNMapKey,
    UnionVariantNameEmpty,
    DuplicateUnionVariantName,
    UnionVariantIndexOutOfRange,
    EnumSymbolEmpty,
    DuplicateEnumSymbol,
    EnumSymbolIndexOutOfRange,
    InvalidBoolValue(u8),
    NonCanonicalF32NaN,
    NonCanonicalF64NaN,
    DecimalCoefficientExceedsPrecision,
    TimeOutOfRange,
    DateTimeTimeOutOfRange,
    DateTimeTzTimeOutOfRange,
    InvalidOptionalPresenceMarker(u8),
    DurationNanosOutOfRange,
    DurationSignMismatch,
    Invalid(Cow<'static, str>),
    LimitExceeded(&'static str),
    Utf8,
    TypeMismatch { expected: &'static str },
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &self.kind {
            ErrorKind::UnexpectedEof => f.write_str("unexpected end of input")?,
            ErrorKind::TrailingBytes => f.write_str("trailing bytes after value")?,
            ErrorKind::InvalidMagic => f.write_str("invalid TPACK magic")?,
            ErrorKind::UnsupportedVersion(version) => {
                write!(f, "unsupported TPACK version {version}")?
            }
            ErrorKind::UnknownEnvelopeMode(mode) => {
                write!(f, "unknown envelope mode 0x{mode:02X}")?
            }
            ErrorKind::UnknownTypeTag(tag) => write!(f, "unknown type tag 0x{tag:02X}")?,
            ErrorKind::OverlongVarint => f.write_str("overlong variable-length integer")?,
            ErrorKind::VarintOverflow => {
                f.write_str("variable-length integer exceeds supported size")?
            }
            ErrorKind::SchemaLengthMismatch => {
                f.write_str("schema length does not match descriptor")?
            }
            ErrorKind::SchemaLengthExceeded => {
                f.write_str("schema length exceeds configured limit")?
            }
            ErrorKind::InvalidSchemaId => f.write_str("invalid schema id")?,
            ErrorKind::UnknownSchemaId => f.write_str("unknown schema id")?,
            ErrorKind::SchemaRefNotAllowed => f.write_str("schema references are not allowed")?,
            ErrorKind::EmbeddedSchemaMismatch => {
                f.write_str("embedded schema does not match cached schema")?
            }
            ErrorKind::InvalidDecimalParameters => {
                f.write_str("invalid Decimal(P,S) parameters")?
            }
            ErrorKind::InvalidTimestampPrecision(precision) => {
                write!(f, "invalid timestamp precision {precision}")?
            }
            ErrorKind::StructFieldIdZero => {
                f.write_str("struct FieldId must be greater than zero")?
            }
            ErrorKind::StructFieldNameEmpty => {
                f.write_str("struct field name must be non-empty")?
            }
            ErrorKind::StructFieldFlagsNonZero(flags) => {
                write!(f, "struct field flags must be zero, got {flags}")?
            }
            ErrorKind::DuplicateStructFieldDefinition => {
                f.write_str("duplicate struct field identifier or name")?
            }
            ErrorKind::DuplicateStructFieldValue => f.write_str("duplicate struct field value")?,
            ErrorKind::MissingStructFieldValue => f.write_str("missing struct field value")?,
            ErrorKind::InvalidMapKeyType => f.write_str("invalid map key type")?,
            ErrorKind::DuplicateMapKey => f.write_str("duplicate map key")?,
            ErrorKind::NonCanonicalMapKeyOrder => f.write_str("non-canonical map key order")?,
            ErrorKind::NaNMapKey => f.write_str("NaN map key")?,
            ErrorKind::UnionVariantNameEmpty => {
                f.write_str("union variant name must be non-empty")?
            }
            ErrorKind::DuplicateUnionVariantName => f.write_str("duplicate union variant name")?,
            ErrorKind::UnionVariantIndexOutOfRange => {
                f.write_str("union variant index out of range")?
            }
            ErrorKind::EnumSymbolEmpty => f.write_str("enum symbol must be non-empty")?,
            ErrorKind::DuplicateEnumSymbol => f.write_str("duplicate enum symbol")?,
            ErrorKind::EnumSymbolIndexOutOfRange => {
                f.write_str("enum symbol index out of range")?
            }
            ErrorKind::InvalidBoolValue(value) => write!(f, "invalid bool value {value}")?,
            ErrorKind::NonCanonicalF32NaN => f.write_str("non-canonical f32 NaN")?,
            ErrorKind::NonCanonicalF64NaN => f.write_str("non-canonical f64 NaN")?,
            ErrorKind::DecimalCoefficientExceedsPrecision => {
                f.write_str("Decimal(P,S) coefficient exceeds precision")?
            }
            ErrorKind::TimeOutOfRange => f.write_str("time value exceeds nanos-per-day")?,
            ErrorKind::DateTimeTimeOutOfRange => {
                f.write_str("datetime time value exceeds nanos-per-day")?
            }
            ErrorKind::DateTimeTzTimeOutOfRange => {
                f.write_str("datetime-tz time value exceeds nanos-per-day")?
            }
            ErrorKind::InvalidOptionalPresenceMarker(marker) => {
                write!(f, "invalid optional presence marker {marker}")?
            }
            ErrorKind::DurationNanosOutOfRange => f.write_str("duration nanos out of range")?,
            ErrorKind::DurationSignMismatch => {
                f.write_str("duration seconds and nanos signs differ")?
            }
            ErrorKind::Invalid(message) => f.write_str(message)?,
            ErrorKind::LimitExceeded(name) => write!(f, "{name} limit exceeded")?,
            ErrorKind::Utf8 => f.write_str("invalid UTF-8")?,
            ErrorKind::TypeMismatch { expected } => {
                write!(f, "TPACK type mismatch, expected {expected}")?
            }
        }
        if !self.path.is_empty() {
            write!(f, " at {}", self.path)?;
        }
        Ok(())
    }
}

#[cfg(feature = "std")]
impl std::error::Error for Error {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self.source.as_ref()? {
            ErrorSource::Core(error) => Some(error),
            ErrorSource::Utf8(error) => Some(error),
        }
    }
}

impl From<core::str::Utf8Error> for Error {
    fn from(error: core::str::Utf8Error) -> Self {
        Self::new(ErrorKind::Utf8).with_source(error)
    }
}