e2e-protection 0.2.0

Pluggable End-to-End (E2E) protection profiles. First class support for AUTOSAR P11 (CAN) and P22 (CAN FD).
Documentation
//! E2E Profile 11 (CAN): frame = [payload | data_id(2) | counter(1) | crc8(1)]
//! - 8-byte CAN limit: `payload.len() + 4 <= 8`
//! - v0.1: 8-bit counter (4-bit packing can be added later)

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

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct P11Config {
    pub data_id: u16,
    pub crc: CrcPreset, // CRC-8: SAE-J1850 or H2F
}

pub struct Profile11 { cfg: P11Config }

impl Profile11 {
    pub fn preset_sae_j1850(data_id: u16) -> Self {
        Self::new(P11Config { data_id, crc: CrcPreset::Crc8SaeJ1850 })
    }
    pub fn preset_h2f(data_id: u16) -> Self {
        Self::new(P11Config { data_id, crc: CrcPreset::Crc8H2F })
    }
    pub fn new(cfg: P11Config) -> Self { Self { cfg } }
}

impl E2eProfile for Profile11 {
    fn protect(&self, payload: &[u8], counter: u32) -> Result<Vec<u8>, E2eError> {
        if payload.len() + 4 > 8 { return Err(E2eError::LengthError); }
        let mut buf = Vec::with_capacity(payload.len() + 4);
        buf.extend_from_slice(payload);
        buf.extend_from_slice(&self.cfg.data_id.to_be_bytes());
        buf.push((counter & 0xFF) as u8);

        let crc = crc_compute(params_for(self.cfg.crc), &buf) & 0xFF;
        buf.push(crc as u8);
        Ok(buf)
    }
    fn check(&self, frame: &[u8]) -> Result<CheckInfo, E2eError> {
        if frame.len() < 4 { return Err(E2eError::LengthError); }
        let len = frame.len();
        let crc_recv = frame[len - 1];
        let counter  = frame[len - 2];
        let data_id  = u16::from_be_bytes([frame[len - 4], frame[len - 3]]);
        let want = (crc_compute(params_for(self.cfg.crc), &frame[..len - 1]) & 0xFF) as u8;
        if want != crc_recv { 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> {
        frame.len().checked_sub(4)
    }
}

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

    #[test]
    fn p11_roundtrip() {
        let mut tx = E2eSession::new(
            Profile11::preset_sae_j1850(0x0042),
            CounterPolicy::MonotonicRollover { bits: 8, step: 1 },
        );
        let frame = tx.wrap(&[1,2,3,4]).unwrap();

        let mut rx = E2eSession::new(
            Profile11::preset_sae_j1850(0x0042),
            CounterPolicy::MonotonicRollover { bits: 8, step: 1 },
        );
        let pl = rx.unwrap(&frame).unwrap();
        assert_eq!(pl, &[1,2,3,4]);
    }

    #[test]
    fn p11_detect_crc_and_dataid() {
        let p = Profile11::preset_h2f(0xABCD);
        let mut f = p.protect(&[7,7,7], 10).unwrap();
        // flip one bit → CRC mismatch
        f[0] ^= 0x01;
        assert!(matches!(p.check(&f), Err(E2eError::CrcMismatch)));

        // fix, then corrupt data_id → DataIdMismatch
        let mut f2 = p.protect(&[7,7,7], 10).unwrap();
        let len = f2.len();
        f2[len - 3] ^= 0xFF; // high byte of data_id
        assert!(matches!(p.check(&f2), Err(E2eError::DataIdMismatch)));
    }

    #[test]
    fn p11_counter_policy() {
        let mut tx = E2eSession::new(
            Profile11::preset_sae_j1850(0x0101),
            CounterPolicy::MonotonicRollover { bits: 8, step: 1 },
        );
        let f1 = tx.wrap(&[]).unwrap();
        let mut f2 = tx.wrap(&[]).unwrap();

        // Break step by adding 2 to counter field.
        let len = f2.len();
        f2[len - 2] = f2[len - 2].wrapping_add(2);

        let mut rx = E2eSession::new(
            Profile11::preset_sae_j1850(0x0101),
            CounterPolicy::MonotonicRollover { bits: 8, step: 1 },
        );
        rx.unwrap(&f1).unwrap();
        assert!(matches!(rx.unwrap(&f2), Err(E2eError::CounterError)));
    }
}