liminal-protocol 0.13.2

Shared participant-lifecycle protocol types for liminal
Documentation
//! The credential records a `Connect` may carry in its authentication bytes.
//!
//! ACCESS-001 R2: a Lys pass is admitted as a second credential kind beside
//! `WirePassV1`, as a new record and never as trailing fields on V1. The two
//! records are told apart by their first byte; everything after the Lys
//! record's tag is the pass exactly as Lys issued it, so the server hands it
//! to `lys-pass` unchanged and never parses it here.
//!
//! The bearer token the server may also accept is not a record: it has no
//! tag and is compared whole, before any record is read. Classification runs
//! only on bytes the bearer comparison refused.

use alloc::vec::Vec;

/// First byte of a `WirePassV1` record, owned by the server's pass checker.
pub const WIRE_PASS_V1_TAG: u8 = 1;
/// First byte of a Lys pass record; the rest is the pass as Lys issued it.
pub const LYS_PASS_V1_TAG: u8 = 2;

/// A credential record classified by its first byte.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum WireCredential<'a> {
    /// A `WirePassV1` record; the whole bytes, tag included, go to its checker.
    PassV1(&'a [u8]),
    /// A Lys pass; the bytes after the tag go to `lys-pass` unchanged.
    LysPass(&'a [u8]),
}

/// Why bytes are not a credential record.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CredentialKindError {
    /// No bytes at all.
    Empty,
    /// A first byte no record kind claims.
    UnknownTag(u8),
    /// A Lys record tag with nothing after it.
    EmptyLysPass,
}

impl core::fmt::Display for CredentialKindError {
    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self {
            Self::Empty => formatter.write_str("credential is empty"),
            Self::UnknownTag(tag) => write!(formatter, "credential tag {tag} is unknown"),
            Self::EmptyLysPass => formatter.write_str("lys pass record carries no pass"),
        }
    }
}

impl core::error::Error for CredentialKindError {}

impl<'a> WireCredential<'a> {
    /// Classifies credential bytes by their first byte.
    ///
    /// # Errors
    /// Empty bytes, an unknown tag, or a Lys tag with no pass behind it.
    pub const fn classify(bytes: &'a [u8]) -> Result<Self, CredentialKindError> {
        match bytes {
            [] => Err(CredentialKindError::Empty),
            [WIRE_PASS_V1_TAG, ..] => Ok(Self::PassV1(bytes)),
            [LYS_PASS_V1_TAG] => Err(CredentialKindError::EmptyLysPass),
            [LYS_PASS_V1_TAG, pass @ ..] => Ok(Self::LysPass(pass)),
            [tag, ..] => Err(CredentialKindError::UnknownTag(*tag)),
        }
    }

    /// Frames a Lys pass as the record a client sends.
    #[must_use]
    pub fn lys_record(pass: &[u8]) -> Vec<u8> {
        let mut record = Vec::with_capacity(pass.len() + 1);
        record.push(LYS_PASS_V1_TAG);
        record.extend_from_slice(pass);
        record
    }
}

#[cfg(test)]
mod tests {
    use super::{CredentialKindError, LYS_PASS_V1_TAG, WIRE_PASS_V1_TAG, WireCredential};

    #[test]
    fn a_v1_record_keeps_its_tag_for_the_pass_checker() {
        let bytes = [WIRE_PASS_V1_TAG, 7, 8];
        assert_eq!(
            WireCredential::classify(&bytes),
            Ok(WireCredential::PassV1(&bytes))
        );
    }

    #[test]
    fn a_lys_record_hands_over_the_pass_without_its_tag() {
        let record = WireCredential::lys_record(b"issued-by-lys");
        assert_eq!(record[0], LYS_PASS_V1_TAG);
        assert_eq!(
            WireCredential::classify(&record),
            Ok(WireCredential::LysPass(b"issued-by-lys"))
        );
    }

    #[test]
    fn bytes_outside_both_records_are_refused_by_name() {
        assert_eq!(
            WireCredential::classify(&[]),
            Err(CredentialKindError::Empty)
        );
        assert_eq!(
            WireCredential::classify(&[LYS_PASS_V1_TAG]),
            Err(CredentialKindError::EmptyLysPass)
        );
        assert_eq!(
            WireCredential::classify(&[9, 1]),
            Err(CredentialKindError::UnknownTag(9))
        );
    }
}