minerva 0.2.0

Causal ordering for distributed systems
extern crate alloc;

use alloc::vec::Vec;

use core::num::NonZeroU64;

use crate::kairos::{KAIROS_WIRE_LEN, Kairos};

use super::super::placement::Dot;
use super::super::{Anchor, Locus};
use super::RhapsodyDecodeError;
use crate::metis::dot::RawDot;

/// The fixed prefix of one locus record before its variable-width anchor: a
/// `u32` `station` and a `u64` `index` (the woven dot, 12 bytes) then a one-byte
/// anchor tag. `O(1)`-checkable before the anchor branch.
const RHAPSODY_WIRE_LOCUS_PREFIX_LEN: usize = 13;

/// The width of an anchor dot when present: a `u32` `station` and a `u64` `index`.
const RHAPSODY_WIRE_ANCHOR_LEN: usize = 12;

/// The smallest a whole locus record can be: the fixed prefix, an origin anchor
/// (tag only), and the 17-byte rank frame. A dot anchor (after or before) adds
/// [`RHAPSODY_WIRE_ANCHOR_LEN`]. Used as the per-locus lower bound so a frame
/// claiming `u32::MAX` loci is refused in `O(1)` against the buffer it cannot
/// fill.
const RHAPSODY_WIRE_LOCUS_MIN_LEN: usize = RHAPSODY_WIRE_LOCUS_PREFIX_LEN + KAIROS_WIRE_LEN;

/// The anchor tag for a locus anchored at the origin ([`Anchor::Origin`]),
/// carrying no dot.
const ANCHOR_TAG_ORIGIN: u8 = 0x00;
/// The anchor tag for a locus in the region AFTER a dot ([`Anchor::After`]); a
/// dot follows.
const ANCHOR_TAG_AFTER: u8 = 0x01;
/// The anchor tag for a locus in the region BEFORE a dot ([`Anchor::Before`]); a
/// dot follows. The side rides the tag, so a `Before` record is the same width as
/// an `After` one (S127).
const ANCHOR_TAG_BEFORE: u8 = 0x02;

/// Encodes one skeleton locus record in canonical woven-dot order.
pub(super) fn encode_locus(out: &mut Vec<u8>, dot: Dot, locus: &Locus) {
    out.extend_from_slice(&dot.station().to_be_bytes());
    out.extend_from_slice(&dot.counter().to_be_bytes());
    match locus.anchor {
        Anchor::Origin => out.push(ANCHOR_TAG_ORIGIN),
        Anchor::After(RawDot {
            station: astation,
            counter: aindex,
        }) => {
            out.push(ANCHOR_TAG_AFTER);
            out.extend_from_slice(&astation.to_be_bytes());
            out.extend_from_slice(&aindex.to_be_bytes());
        }
        Anchor::Before(RawDot {
            station: astation,
            counter: aindex,
        }) => {
            out.push(ANCHOR_TAG_BEFORE);
            out.extend_from_slice(&astation.to_be_bytes());
            out.extend_from_slice(&aindex.to_be_bytes());
        }
    }
    out.extend_from_slice(&locus.rank.to_bytes());
}

/// One decoded locus record: its woven dot, the locus, and the unconsumed tail.
type DecodedLocus<'a> = (Dot, Locus, &'a [u8]);

/// Decodes one locus record off the front of `rest`, returning the woven dot, its
/// locus, and the unconsumed tail. Length-checks the fixed prefix (against the
/// per-locus minimum, so a large locus count cannot force an unbacked read), then
/// the anchor's variable body, then the rank frame; each is a total, panic-free
/// slice conversion. Validates the woven dot is non-zero and the anchor tag is
/// known; the ascending-order and structural checks are the caller's.
pub(super) fn decode_locus<'a>(
    bytes: &[u8],
    rest: &'a [u8],
) -> Result<DecodedLocus<'a>, RhapsodyDecodeError> {
    let prefix: [u8; RHAPSODY_WIRE_LOCUS_PREFIX_LEN] = rest
        .get(..RHAPSODY_WIRE_LOCUS_PREFIX_LEN)
        .and_then(|p| p.try_into().ok())
        .ok_or(RhapsodyDecodeError::UnexpectedLength {
            // The bytes consumed so far plus one whole minimal locus is a sound
            // lower bound on the frame the count requires.
            expected: bytes.len() - rest.len() + RHAPSODY_WIRE_LOCUS_MIN_LEN,
            found: bytes.len(),
        })?;
    let [s0, s1, s2, s3, i0, i1, i2, i3, i4, i5, i6, i7, tag] = prefix;
    let station = u32::from_be_bytes([s0, s1, s2, s3]);
    let index = u64::from_be_bytes([i0, i1, i2, i3, i4, i5, i6, i7]);
    // The non-dot `0`; `weave` refuses it, so no encoder produces one. The
    // refusal IS the identity's construction, so the decoded dot needs no
    // second proof (ruling R-91); acceptance is byte-identical.
    let Some(counter) = NonZeroU64::new(index) else {
        return Err(RhapsodyDecodeError::ZeroDot { station });
    };
    let mut rest = &rest[RHAPSODY_WIRE_LOCUS_PREFIX_LEN..];

    let anchor = match tag {
        ANCHOR_TAG_ORIGIN => Anchor::Origin,
        ANCHOR_TAG_AFTER | ANCHOR_TAG_BEFORE => {
            let anchor: [u8; RHAPSODY_WIRE_ANCHOR_LEN] = rest
                .get(..RHAPSODY_WIRE_ANCHOR_LEN)
                .and_then(|a| a.try_into().ok())
                .ok_or(RhapsodyDecodeError::UnexpectedLength {
                    expected: bytes.len() - rest.len() + RHAPSODY_WIRE_ANCHOR_LEN + KAIROS_WIRE_LEN,
                    found: bytes.len(),
                })?;
            rest = &rest[RHAPSODY_WIRE_ANCHOR_LEN..];
            let [a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11] = anchor;
            let astation = u32::from_be_bytes([a0, a1, a2, a3]);
            let aindex = u64::from_be_bytes([a4, a5, a6, a7, a8, a9, a10, a11]);
            let dot = (astation, aindex);
            // The side rides the tag: an after tag hangs in the region after the
            // dot, a before tag in the region before it (S127).
            if tag == ANCHOR_TAG_AFTER {
                Anchor::After(dot.into())
            } else {
                Anchor::Before(dot.into())
            }
        }
        other => return Err(RhapsodyDecodeError::BadAnchorTag { tag: other }),
    };

    let rank_bytes: [u8; KAIROS_WIRE_LEN] = rest
        .get(..KAIROS_WIRE_LEN)
        .and_then(|r| r.try_into().ok())
        .ok_or(RhapsodyDecodeError::UnexpectedLength {
            expected: bytes.len() - rest.len() + KAIROS_WIRE_LEN,
            found: bytes.len(),
        })?;
    let rank = Kairos::from_bytes(&rank_bytes).map_err(RhapsodyDecodeError::Rank)?;
    rest = &rest[KAIROS_WIRE_LEN..];

    Ok((Dot::new(station, counter), Locus { anchor, rank }, rest))
}