minerva 0.2.0

Causal ordering for distributed systems
use thiserror::Error;

/// Ceilings for what a [`RefoundMap`](crate::metis::RefoundMap) decoder may
/// materialize.
///
/// The budget counts decoded *station rows* and decoded *translation
/// entries* (the frame's own units, the have-set precedent), never bytes:
/// an embedding protocol that caps its roster or its document size states
/// the caps in the vocabulary its policy already speaks. Zero rows and zero
/// entries are valid for the empty map. The unbudgeted door derives its
/// ceiling from the input length instead: every row is backed by at least
/// its fixed minimum of frame bytes and every entry by its fixed width, so
/// the input already bounds allocation without any caller policy.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct RefoundMapDecodeBudget {
    max_stations: usize,
    max_entries: usize,
}

impl RefoundMapDecodeBudget {
    /// Builds a budget allowing at most `max_stations` decoded station rows
    /// and at most `max_entries` decoded translation entries across them.
    #[must_use]
    pub const fn new(max_stations: usize, max_entries: usize) -> Self {
        Self {
            max_stations,
            max_entries,
        }
    }

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

    /// Maximum translation entries the decoder may materialize, summed
    /// across every station row.
    #[must_use]
    pub const fn max_entries(self) -> usize {
        self.max_entries
    }
}

/// Decode failure for a [`RefoundMap`](crate::metis::RefoundMap) wire frame
/// (S278).
///
/// Distinct from the rhapsody, snapshot, have-set, and metatheses errors
/// beside it: the rename map is a separate codec with its own version space
/// and its own canonical-form rules (the per-station compaction shape), so
/// it owns its own error rather than sharing any (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 RefoundMapDecodeError {
    /// The leading version byte is not a version this build understands.
    #[error("unknown refound-map wire version: {0:#04x}")]
    UnknownVersion(u8),
    /// The input was shorter than the declared station or entry counts
    /// require, or (for
    /// [`RefoundMap::from_bytes`](crate::metis::RefoundMap::from_bytes))
    /// carried trailing bytes past the frame.
    #[error("unexpected refound-map 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,
    },
    /// The declared station-row 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("refound-map frame declares {count} station rows, past the budget's {budget}")]
    TooManyStations {
        /// The declared station-row count.
        count: u64,
        /// The caller's station-row budget.
        budget: u64,
    },
    /// The translation entries declared so far exceed the caller's decode
    /// budget (checked cumulatively, row by row, before any entry of the
    /// offending row is materialized).
    #[error("refound-map frame declares {count} translation entries, past the budget's {budget}")]
    TooManyEntries {
        /// The cumulative declared entry count through the offending row.
        count: u64,
        /// The caller's translation-entry budget.
        budget: u64,
    },
    /// Station rows were not in strictly ascending station order (unsorted,
    /// or a duplicate station), so the frame is non-canonical: the map's
    /// ceilings enumerate ascending, and its encoding walks that
    /// enumeration.
    #[error("non-canonical refound map: station {found} is not strictly after {previous}")]
    NonAscendingStations {
        /// The preceding row's station.
        previous: u32,
        /// The offending station, not strictly greater than `previous`.
        found: u32,
    },
    /// A station row carried a zero ceiling. A station enters the map only
    /// by owning old-epoch identities at or below its ceiling, so every
    /// constructible row has a ceiling of at least one and no encoder
    /// produces a zero (the version vector's canonical form: a station at
    /// zero is absent).
    #[error("non-canonical refound map: zero ceiling for station {station}")]
    ZeroCeiling {
        /// The station of the offending row.
        station: u32,
    },
    /// A station row declared more live entries than its ceiling admits.
    /// Every live identity is a distinct old-epoch dot in `1..=ceiling`,
    /// so the live count never exceeds the ceiling (the fold's proven
    /// tally bound) and no encoder produces this shape.
    #[error(
        "non-canonical refound map: station {station} declares {live} live entries over ceiling {ceiling}"
    )]
    LiveExceedsCeiling {
        /// The station of the offending row.
        station: u32,
        /// The declared live-entry count.
        live: u64,
        /// The declared cut ceiling.
        ceiling: u64,
    },
    /// A translation entry named an old-epoch index outside the compacted
    /// domain `1..=ceiling`: zero (the non-dot), or above the station's
    /// ceiling (an identity the cut never witnessed, whose translation is
    /// the affine rule's, never the map's).
    #[error(
        "non-canonical refound map: station {station} entry {index} outside the compacted domain 1..={ceiling}"
    )]
    IndexOutOfRange {
        /// The station of the offending row.
        station: u32,
        /// The offending old-epoch index.
        index: u64,
        /// The declared cut ceiling bounding the compacted domain.
        ceiling: u64,
    },
    /// A station row named one old-epoch index twice. The compaction is a
    /// bijection onto `1..=live` (the fold's proven injectivity), so a
    /// duplicate would fold two old identities onto one new dot and no
    /// encoder produces it.
    #[error("non-canonical refound map: station {station} names old index {index} twice")]
    DuplicateIndex {
        /// The station of the offending row.
        station: u32,
        /// The old-epoch index named twice.
        index: u64,
    },
}