minerva 0.2.0

Causal ordering for distributed systems
use thiserror::Error;

use crate::kairos::DecodeError as KairosDecodeError;

/// Maximum movement testimonies a [`Metatheses`](crate::metis::Metatheses)
/// decoder may materialize.
///
/// The budget counts decoded testimony rows, never bytes or stations, so an
/// embedding protocol states its policy in the frame's own unit (the
/// have-set budget precedent). Zero is valid for the empty record. The
/// unbudgeted door derives its ceiling from the input length instead: every
/// row is backed by at least its fixed minimum of frame bytes, so the input
/// already bounds allocation without any caller policy.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct MetathesesDecodeBudget(usize);

impl MetathesesDecodeBudget {
    /// Builds a budget allowing at most `max_testimonies` decoded rows.
    #[must_use]
    pub const fn new(max_testimonies: usize) -> Self {
        Self(max_testimonies)
    }

    /// Maximum testimony rows the decoder may materialize.
    #[must_use]
    pub const fn max_testimonies(self) -> usize {
        self.0
    }
}

/// Decode failure for a [`Metatheses`](crate::metis::Metatheses) wire frame
/// (S272).
///
/// This error is distinct from two neighbours:
/// [`RhapsodyDecodeError`](crate::metis::RhapsodyDecodeError) beside it, and
/// the `Kairos` [`DecodeError`](crate::kairos::DecodeError) it wraps for a
/// rank. The movement frame is a separate codec, with its own version space
/// and its own canonical-form rules (the testimony-dot order). It therefore
/// owns its own error rather than sharing either neighbour's (ruling R-8).
/// `#[non_exhaustive]` so a future version's validation failure is additive.
#[non_exhaustive]
#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)]
pub enum MetathesesDecodeError {
    /// The leading version byte is not a version this build understands.
    #[error("unknown metatheses wire version: {0:#04x}")]
    UnknownVersion(u8),
    /// The input was shorter than the declared testimony count requires, or
    /// (for [`Metatheses::from_bytes`](crate::metis::Metatheses::from_bytes))
    /// carried trailing bytes past the frame.
    #[error("unexpected metatheses frame length: expected at least {expected}, found {found}")]
    UnexpectedLength {
        /// A lower bound on the frame length the declared count requires.
        expected: usize,
        /// The actual length of the supplied input.
        found: usize,
    },
    /// The declared testimony count exceeds the caller's decode budget:
    /// the frame may be honest, but this decode was not licensed to hold
    /// it (the budgeted door's refusal; the unbudgeted door bounds by
    /// input length instead and refuses through `UnexpectedLength`).
    #[error("metatheses frame declares {count} testimonies, past the budget's {budget}")]
    TooManyTestimonies {
        /// The declared testimony count.
        count: u64,
        /// The caller's testimony budget.
        budget: u64,
    },
    /// A testimony dot carried `index == 0`. A testimony dot is a
    /// [`Dot`](crate::metis::Dot), whose counter is nonzero by type (ruling
    /// R-91), so no encoder produces one. (A *target* or *anchor* dot with
    /// index zero is deliberately not refused: the record stores them
    /// verbatim, so they are representable values the bijection must
    /// carry.)
    #[error("non-canonical metatheses: zero index for station {station}")]
    ZeroDot {
        /// The station of the offending zero-index testimony dot.
        station: u32,
    },
    /// Testimony rows were not in strictly ascending testimony-dot order
    /// (unsorted, or a duplicate dot), so the frame is non-canonical: the
    /// record enumerates ascending by dot, and its encoding walks that
    /// enumeration.
    #[error("non-canonical metatheses: dot {found:?} is not strictly after {previous:?}")]
    NonAscendingTestimonies {
        /// The preceding row's testimony dot.
        previous: (u32, u64),
        /// The offending testimony dot, not strictly greater than `previous`.
        found: (u32, u64),
    },
    /// A row's anchor tag byte was none of `0x00` (origin), `0x01` (after),
    /// or `0x02` (before): the sided-anchor discipline the rhapsody v2
    /// frame fixed (PRD 0018), spelled per codec, agreement pinned by test.
    #[error("non-canonical metatheses: unknown anchor tag {tag:#04x}")]
    BadAnchorTag {
        /// The unrecognized tag byte.
        tag: u8,
    },
    /// A row's 17-byte rank frame failed to decode as a
    /// [`Kairos`](crate::kairos::Kairos) stamp.
    #[error("metatheses rank frame: {0}")]
    Rank(#[source] KairosDecodeError),
}