e2e-protection 0.2.1

Pluggable End-to-End (E2E) protection profiles. First class support for AUTOSAR P11 (CAN) and P22 (CAN FD).
Documentation
//! E2E Profile 22 (CAN FD):
//!   frame = [payload | length(2) | data_id(2) | counter(1) | crc(N)]
//! - Include `length` to detect truncation/injection.
//! - v0.1 supports CRC-16/CCITT-FALSE and CRC-32/ISO-HDLC.

use crate::crc::{crc_compute, params_for, CrcParams, CrcPreset};
use crate::e2e::{CheckInfo, E2eError, E2eProfile};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum P22Crc {
    Crc16CcittFalse,
    Crc32IsoHdlc,
    Custom(CrcParams),
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct P22Config {
    pub data_id: u16,
    pub crc: P22Crc,
}

pub struct Profile22 { cfg: P22Config }

impl Profile22 {
    pub fn preset_crc16(data_id: u16) -> Self {
        Self::new(P22Config { data_id, crc: P22Crc::Crc16CcittFalse })
    }
    pub fn preset_crc32(data_id: u16) -> Self {
        Self::new(P22Config { data_id, crc: P22Crc::Crc32IsoHdlc })
    }
    pub fn preset_custom(data_id: u16, params: CrcParams) -> Self {
        Self::new(P22Config { data_id, crc: P22Crc::Custom(params) })
    }
    pub fn new(cfg: P22Config) -> Self { Self { cfg } }

    fn crc_params(&self) -> (CrcParams, usize) {
        match self.cfg.crc {
            P22Crc::Crc16CcittFalse => (params_for(CrcPreset::Crc16CcittFalse), 2),
            P22Crc::Crc32IsoHdlc    => (params_for(CrcPreset::Crc32IsoHdlc), 4),
            P22Crc::Custom(p)       => (p, (p.width / 8) as usize),
        }
    }
}

impl E2eProfile for Profile22 {
    fn protect(&self, payload: &[u8], counter: u32) -> Result<Vec<u8>, E2eError> {
        let (params, crc_bytes) = self.crc_params();
        let length = payload.len() as u16;
        let tail = 2 + 2 + 1 + crc_bytes;
        if payload.len() + tail > 64 { return Err(E2eError::LengthError); }

        let mut buf = Vec::with_capacity(payload.len() + tail);
        buf.extend_from_slice(payload);
        buf.extend_from_slice(&length.to_be_bytes());
        buf.extend_from_slice(&self.cfg.data_id.to_be_bytes());
        buf.push((counter & 0xFF) as u8);

        let crc = crc_compute(params, &buf);
        for i in (0..crc_bytes).rev() {
            buf.push(((crc >> (i * 8)) & 0xFF) as u8);
        }
        Ok(buf)
    }

    fn check(&self, frame: &[u8]) -> Result<CheckInfo, E2eError> {
        let (params, crc_bytes) = self.crc_params();
        if frame.len() < (2 + 2 + 1 + crc_bytes) { return Err(E2eError::LengthError); }

        let len = frame.len();
        let crc_recv = &frame[len - crc_bytes..];
        let counter = frame[len - crc_bytes - 1];
        let data_id = u16::from_be_bytes([frame[len - crc_bytes - 3], frame[len - crc_bytes - 2]]);
        let length  = u16::from_be_bytes([frame[len - crc_bytes - 5], frame[len - crc_bytes - 4]]) as usize;

        let payload = &frame[..len - (2 + 2 + 1 + crc_bytes)];
        if payload.len() != length { return Err(E2eError::LengthError); }

        let want = crc_compute(params, &frame[..len - crc_bytes]);
        for i in 0..crc_bytes {
            let b = ((want >> ((crc_bytes - 1 - i) * 8)) & 0xFF) as u8;
            if b != crc_recv[i] { return Err(E2eError::CrcMismatch); }
        }
        if data_id != self.cfg.data_id { return Err(E2eError::DataIdMismatch); }

        Ok(CheckInfo { counter: counter as u32, ok: true })
    }

    fn payload_len(&self, frame: &[u8]) -> Option<usize> {
        let (params, crc_bytes) = self.crc_params();
        let _ = params; // silence
        if frame.len() < (2 + 2 + 1 + crc_bytes) { return None; }
        let len = frame.len();
        let length = u16::from_be_bytes([frame[len - crc_bytes - 5], frame[len - crc_bytes - 4]]) as usize;
        (frame.len() >= length + (2 + 2 + 1 + crc_bytes)).then_some(length)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{E2eSession, CounterPolicy, E2eProfile, E2eError};

    #[test]
    fn p22_roundtrip_crc16() {
        let mut tx = E2eSession::new(
            Profile22::preset_crc16(0xBEEF),
            CounterPolicy::MonotonicRollover { bits: 8, step: 1 },
        );
        let payload = vec![0xAA; 20];
        let f = tx.wrap(&payload).unwrap();

        let mut rx = E2eSession::new(
            Profile22::preset_crc16(0xBEEF),
            CounterPolicy::MonotonicRollover { bits: 8, step: 1 },
        );
        let pl = rx.unwrap(&f).unwrap();
        assert_eq!(pl, &payload[..]);
    }

    #[test]
    fn p22_roundtrip_crc32() {
        let mut tx = E2eSession::new(
            Profile22::preset_crc32(0x00FE),
            CounterPolicy::MonotonicRollover { bits: 8, step: 1 },
        );
        let payload = (0u8..40).collect::<Vec<_>>();
        let f = tx.wrap(&payload).unwrap();

        let mut rx = E2eSession::new(
            Profile22::preset_crc32(0x00FE),
            CounterPolicy::MonotonicRollover { bits: 8, step: 1 },
        );
        let pl = rx.unwrap(&f).unwrap();
        assert_eq!(pl, &payload[..]);
    }

    #[test]
    fn p22_detect_length_and_crc() {
        let p = Profile22::preset_crc16(0x0102);
        let payload = vec![1,2,3,4,5,6];
        let mut f = p.protect(&payload, 3).unwrap();
        // Truncate → length mismatch
        f.remove(0);
        assert!(matches!(p.check(&f), Err(E2eError::LengthError)));

        // CRC mismatch case
        let mut f2 = Profile22::preset_crc32(0x0102).protect(&payload, 7).unwrap();
        f2[0] ^= 0x01;
        assert!(matches!(Profile22::preset_crc32(0x0102).check(&f2), Err(E2eError::CrcMismatch)));
    }
}