hibana 0.3.0

Const-projected Affine Multiparty Session Types for choreography-first Rust protocols
Documentation
//! Built-in route/loop control kinds plus their public handle codecs.
//!
//! Built-in route/loop semantics are identified by descriptor control metadata,
//! never by numeric label reservations.
//!
//! This module is intentionally limited to the public built-ins:
//! - `LoopContinueKind`
//! - `LoopBreakKind`
//! - `RouteDecisionKind`
//!
//! Private atomic control codecs live in `control::cap::atomic_codecs`.

use crate::control::cap::mint::{
    CAP_HANDLE_LEN, CapError, CapShot, ControlOp, ControlPath, ControlResourceKind, ResourceKind,
};
use crate::global::const_dsl::{ControlScopeKind, ScopeId};
use crate::{
    control::types::{Lane, SessionId},
    observe::ids,
};

#[inline]
fn bytes_are_zero(bytes: &[u8]) -> bool {
    bytes.iter().all(|byte| *byte == 0)
}

/// Route decision handle carrying the selected arm and scope trace.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub struct RouteArmHandle {
    pub scope: ScopeId,
    pub arm: u8,
}

impl RouteArmHandle {
    pub fn encode(self) -> [u8; CAP_HANDLE_LEN] {
        let mut buf = [0u8; CAP_HANDLE_LEN];
        buf[0] = self.arm;
        buf[1..9].copy_from_slice(&self.scope.raw().to_le_bytes());
        buf
    }

    pub fn decode(data: [u8; CAP_HANDLE_LEN]) -> Result<Self, CapError> {
        if data[0] > 1 || !bytes_are_zero(&data[9..]) {
            return Err(CapError::Mismatch);
        }
        let mut scope_bytes = [0u8; 8];
        scope_bytes.copy_from_slice(&data[1..9]);
        Ok(Self {
            scope: ScopeId::from_raw(u64::from_le_bytes(scope_bytes)),
            arm: data[0],
        })
    }
}

/// Loop decision handle carrying session, lane, and scope information.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub struct LoopDecisionHandle {
    pub sid: u32,
    pub lane: u8,
    pub scope: ScopeId,
}

impl LoopDecisionHandle {
    pub fn encode(self) -> [u8; CAP_HANDLE_LEN] {
        let mut buf = [0u8; CAP_HANDLE_LEN];
        buf[0..4].copy_from_slice(&self.sid.to_le_bytes());
        buf[4..6].copy_from_slice(&u16::from(self.lane).to_le_bytes());
        buf[6..14].copy_from_slice(&self.scope.raw().to_le_bytes());
        buf
    }

    pub fn decode(data: [u8; CAP_HANDLE_LEN]) -> Result<Self, CapError> {
        if !bytes_are_zero(&data[14..]) {
            return Err(CapError::Mismatch);
        }
        let sid = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
        let lane_raw = u16::from_le_bytes([data[4], data[5]]);
        if lane_raw > u8::MAX as u16 {
            return Err(CapError::Mismatch);
        }
        let mut scope_bytes = [0u8; 8];
        scope_bytes.copy_from_slice(&data[6..14]);
        Ok(Self {
            sid,
            lane: lane_raw as u8,
            scope: ScopeId::from_raw(u64::from_le_bytes(scope_bytes)),
        })
    }
}

/// Built-in local loop-continue token.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct LoopContinueKind;

impl ResourceKind for LoopContinueKind {
    type Handle = LoopDecisionHandle;
    const TAG: u8 = 0x40;
    const NAME: &'static str = "LoopContinue";

    fn encode_handle(handle: &Self::Handle) -> [u8; CAP_HANDLE_LEN] {
        handle.encode()
    }

    fn decode_handle(data: [u8; CAP_HANDLE_LEN]) -> Result<Self::Handle, CapError> {
        LoopDecisionHandle::decode(data)
    }

    fn zeroize(_handle: &mut Self::Handle) {}
}

impl ControlResourceKind for LoopContinueKind {
    const SCOPE: ControlScopeKind = ControlScopeKind::Loop;
    const TAP_ID: u16 = ids::LOOP_DECISION;
    const SHOT: CapShot = CapShot::One;
    const PATH: ControlPath = ControlPath::Local;
    const OP: ControlOp = ControlOp::LoopContinue;
    const AUTO_MINT_WIRE: bool = false;

    fn mint_handle(sid: SessionId, lane: Lane, scope: ScopeId) -> Self::Handle {
        LoopDecisionHandle {
            sid: sid.raw(),
            lane: lane.as_wire(),
            scope,
        }
    }
}

/// Built-in local loop-break token.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct LoopBreakKind;

impl ResourceKind for LoopBreakKind {
    type Handle = LoopDecisionHandle;
    const TAG: u8 = 0x41;
    const NAME: &'static str = "LoopBreak";

    fn encode_handle(handle: &Self::Handle) -> [u8; CAP_HANDLE_LEN] {
        handle.encode()
    }

    fn decode_handle(data: [u8; CAP_HANDLE_LEN]) -> Result<Self::Handle, CapError> {
        LoopDecisionHandle::decode(data)
    }

    fn zeroize(_handle: &mut Self::Handle) {}
}

impl ControlResourceKind for LoopBreakKind {
    const SCOPE: ControlScopeKind = ControlScopeKind::Loop;
    const TAP_ID: u16 = ids::LOOP_DECISION;
    const SHOT: CapShot = CapShot::One;
    const PATH: ControlPath = ControlPath::Local;
    const OP: ControlOp = ControlOp::LoopBreak;
    const AUTO_MINT_WIRE: bool = false;

    fn mint_handle(sid: SessionId, lane: Lane, scope: ScopeId) -> Self::Handle {
        LoopDecisionHandle {
            sid: sid.raw(),
            lane: lane.as_wire(),
            scope,
        }
    }
}

/// Built-in local route decision token.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RouteDecisionKind;

impl ResourceKind for RouteDecisionKind {
    type Handle = RouteArmHandle;
    const TAG: u8 = 0x4E;
    const NAME: &'static str = "RouteDecision";

    fn encode_handle(handle: &Self::Handle) -> [u8; CAP_HANDLE_LEN] {
        handle.encode()
    }

    fn decode_handle(data: [u8; CAP_HANDLE_LEN]) -> Result<Self::Handle, CapError> {
        RouteArmHandle::decode(data)
    }

    fn zeroize(handle: &mut Self::Handle) {
        handle.arm = 0;
    }
}

impl ControlResourceKind for RouteDecisionKind {
    const SCOPE: ControlScopeKind = ControlScopeKind::Route;
    const TAP_ID: u16 = ids::ROUTE_PICK;
    const SHOT: CapShot = CapShot::One;
    const PATH: ControlPath = ControlPath::Local;
    const OP: ControlOp = ControlOp::RouteDecision;
    const AUTO_MINT_WIRE: bool = false;

    fn mint_handle(_sid: SessionId, _lane: Lane, scope: ScopeId) -> Self::Handle {
        RouteArmHandle { scope, arm: 0 }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn route_arm_handle_rejects_non_binary_arms_and_reserved_tail() {
        let handle = RouteArmHandle {
            scope: ScopeId::route(3),
            arm: 1,
        };
        let encoded = handle.encode();
        assert_eq!(RouteArmHandle::decode(encoded), Ok(handle));

        let mut non_binary = encoded;
        non_binary[0] = 2;
        assert_eq!(RouteArmHandle::decode(non_binary), Err(CapError::Mismatch));

        let mut trailing = encoded;
        trailing[9] = 0xA5;
        assert_eq!(RouteArmHandle::decode(trailing), Err(CapError::Mismatch));
    }

    #[test]
    fn loop_decision_handle_rejects_reserved_tail() {
        let handle = LoopDecisionHandle {
            sid: 9,
            lane: 4,
            scope: ScopeId::loop_scope(7),
        };
        let encoded = handle.encode();
        assert_eq!(LoopDecisionHandle::decode(encoded), Ok(handle));

        let mut trailing = encoded;
        trailing[14] = 0x5A;
        assert_eq!(
            LoopDecisionHandle::decode(trailing),
            Err(CapError::Mismatch)
        );

        let mut out_of_domain_lane = encoded;
        out_of_domain_lane[4..6].copy_from_slice(&256u16.to_le_bytes());
        assert_eq!(
            LoopDecisionHandle::decode(out_of_domain_lane),
            Err(CapError::Mismatch),
            "loop control handles must fail closed on lanes outside the wire domain"
        );
    }
}