minerva 0.2.0

Causal ordering for distributed systems
use thiserror::Error;

/// Maximum exception dots a have-set decoder may materialize.
///
/// The budget counts dots expanded from wire runs, not floor-covered dots,
/// stations, runs, or bytes. Zero is valid for a frame with no exceptions.
/// Keeping the unit in the type prevents an embedding protocol from silently
/// substituting its byte or station bound.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct HaveSetDecodeBudget(usize);

impl HaveSetDecodeBudget {
    /// Builds a budget allowing at most `max_dots` expanded exception dots.
    #[must_use]
    pub const fn new(max_dots: usize) -> Self {
        Self(max_dots)
    }

    /// Maximum exception dots the decoder may materialize.
    #[must_use]
    pub const fn max_dots(self) -> usize {
        self.0
    }
}

/// Decode failure for a [`DotSet`](crate::metis::DotSet) have-set wire frame
/// (PRD 0016).
///
/// Distinct from the `VersionVector` [`DecodeError`](crate::metis::DecodeError):
/// the have-set frame is a separate codec with its own canonical-form rules (the
/// run structure), so it owns its own error rather than sharing the vector's.
/// `#[non_exhaustive]` so a future version's validation failure is additive.
#[non_exhaustive]
#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)]
pub enum HaveSetDecodeError {
    /// The leading version byte is not a version this build understands.
    #[error("unknown have-set wire version: {0:#04x}")]
    UnknownVersion(u8),
    /// The input was shorter than a declared station or run count requires, or
    /// (for [`DotSet::from_bytes`](crate::metis::DotSet::from_bytes)) carried
    /// trailing bytes.
    #[error("unexpected have-set frame length: expected at least {expected}, found {found}")]
    UnexpectedLength {
        /// A lower bound on the frame length the declared counts require.
        expected: usize,
        /// The actual length of the supplied input.
        found: usize,
    },
    /// A station entry carried `floor == 0` and no runs. Canonical form holds an
    /// empty station absent, so no encoder produces one.
    #[error("non-canonical have-set: empty entry for station {station}")]
    EmptyStation {
        /// The station whose entry was empty.
        station: u32,
    },
    /// Station entries were not in strictly ascending `station_id` order
    /// (unsorted, or a duplicate station), so the frame is non-canonical.
    #[error("non-canonical have-set: station {found} is not strictly after {previous}")]
    NonAscendingStations {
        /// The preceding entry's `station_id`.
        previous: u32,
        /// The offending `station_id`, not strictly greater than `previous`.
        found: u32,
    },
    /// A run declared a length of zero. Canonical runs carry `len >= 1`, so no
    /// encoder produces one.
    #[error("non-canonical have-set: zero-length run for station {station}")]
    ZeroLengthRun {
        /// The station whose run carried the illegal zero length.
        station: u32,
    },
    /// A run began at or below the point where it (or a merge with its
    /// predecessor, or the floor) would absorb it, so it is not maximal and the
    /// frame is non-canonical: an encoder folds every such dot into the floor or
    /// the neighbouring run.
    #[error("non-canonical have-set: non-maximal run for station {station} starting at {start}")]
    NonMaximalRun {
        /// The station whose run was absorbable.
        station: u32,
        /// The offending run's start dot.
        start: u64,
    },
    /// A run's end (`start + len - 1`) exceeds [`u64::MAX`]. No have-set can hold
    /// such a dot, so the frame names an unrepresentable set.
    #[error("have-set run for station {station} overflows u64: start {start}, len {len}")]
    RunOverflow {
        /// The station whose run overflowed.
        station: u32,
        /// The run's start dot.
        start: u64,
        /// The run's declared length.
        len: u64,
    },
    /// The frame's runs would materialize more exception dots than the selected
    /// decode budget permits. The standalone decoder uses `bytes.len()`; an
    /// embedding protocol may supply a tighter or larger validated bound through
    /// [`HaveSetDecodeBudget`].
    #[error(
        "have-set run for station {station} exceeds the decode dot budget: start {start}, len {len}"
    )]
    RunTooLong {
        /// The station whose run overran the budget.
        station: u32,
        /// The run's start dot.
        start: u64,
        /// The run's declared length.
        len: u64,
    },
}