e2e-protection 0.4.0

End-to-End protection core with pluggable profiles. AUTOSAR profile family is optional via feature
Documentation
//! Profile 11 (CAN-like)
//!
//! **Intent (simplified)**:
//! - 4-bit **counter** in the header (value range: 0x0..0xE; 0xF is reserved/invalid).
//! - **Data ID**: either a full 16-bit *implicit* value (not transmitted, used in CRC),
//!   or a **12-bit nibble-mode** where 4 high bits are explicit in the header byte
//!   and the low 8 bits are implicit (in CRC).
//! - **CRC**: 8-bit **CRC-8/SAE-J1850** (poly 0x1D).
//!
//! **Frame layout used here (educational & compact)**:
//! ```text
//! [ payload ... | CRC8 (1B) | HDR (1B) ]
//!   HDR (bits 7..4) : DI_hi_nibble (nibble mode) OR counter (both-mode)
//!   HDR (bits 3..0) : counter (nibble mode)      OR 0x0 (both-mode)
//! ```
//! - This keeps the CAN 8-byte budget friendly. Real deployments may use bit-level
//!   positioning per network mapping; we encapsulate only the *semantic* pieces.
//!
//! **CRC input** (this implementation):
//! - both-mode:   `payload || DataID[15:0] (LE byte order) || HDR_byte(with counter in high nibble)`
//! - nibble-mode: `payload || DataID_low[7:0] || HDR_byte(explicit hi-nibble + counter)`
//!
//! The `crc` crate is used for the CRC8 computation.

use crc::{Crc, CRC_8_SAE_J1850};

use crate::e2e::{CheckInfo, E2eError, E2eProfile};

const CRC8P1: Crc<u8> = Crc::<u8>::new(&CRC_8_SAE_J1850);

/// Data-ID mode for Profile 11.
///
/// - `Both`: full 16-bit Data-ID is implicit (only used in CRC).
/// - `Nibble`: high 4-bit is explicit in the header (1..=0xE recommended),
///             low 8-bit is implicit (in CRC).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum P11DataIdMode {
    Both { data_id: u16 },
    Nibble { data_id12: u16 }, // 0x000..=0xFFF (only low 12 bits used)
}

/// Profile 11 object containing the chosen Data-ID mode.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct P11 {
    pub mode: P11DataIdMode,
}

impl P11 {
    #[inline]
    fn is_counter_valid(c4: u8) -> bool {
        c4 <= 0x0E // 0x0F is forbidden
    }
}

impl E2eProfile for P11 {
    fn protect(&self, payload: &[u8], counter: u32) -> Result<Vec<u8>, E2eError> {
        let c4 = (counter & 0x0F) as u8;
        if !Self::is_counter_valid(c4) { return Err(E2eError::Counter); }

        // frame := [payload | CRC | HDR]
        let mut out = Vec::with_capacity(payload.len() + 2);
        out.extend_from_slice(payload);
        out.resize(payload.len() + 2, 0u8);

        // Build HDR byte & CRC input depending on mode
        let hdr = match self.mode {
            P11DataIdMode::Both { data_id } => {
                // HDR.high = counter, HDR.low = 0
                let hdr = (c4 << 4) | 0x0;
                let mut crc_in = Vec::with_capacity(payload.len() + 3);
                crc_in.extend_from_slice(payload);
                // Data-ID low, then high (LE byte order for CRC input here)
                crc_in.push((data_id & 0xFF) as u8);
                crc_in.push((data_id >> 8) as u8);
                crc_in.push(hdr);
                out[payload.len()] = CRC8P1.checksum(&crc_in);
                out[payload.len() + 1] = hdr;
                hdr
            }
            P11DataIdMode::Nibble { data_id12 } => {
                // HDR.high = DI_hi_nibble, HDR.low = counter
                let di_hi = ((data_id12 >> 8) & 0x0F) as u8;
                let hdr = ((di_hi & 0x0F) << 4) | c4;
                let mut crc_in = Vec::with_capacity(payload.len() + 2);
                crc_in.extend_from_slice(payload);
                // Only low 8 bits of Data-ID participate implicitly in CRC.
                crc_in.push((data_id12 & 0xFF) as u8);
                crc_in.push(hdr);
                out[payload.len()] = CRC8P1.checksum(&crc_in);
                out[payload.len() + 1] = hdr;
                hdr
            }
        };

        debug_assert_eq!(out[payload.len() + 1], hdr);
        Ok(out)
    }

    fn check(&self, frame: &[u8]) -> Result<CheckInfo, E2eError> {
        if frame.len() < 2 { return Err(E2eError::Length); }
        let (crc_rx, hdr) = (frame[frame.len()-2], frame[frame.len()-1]);
        let cnt = hdr & 0x0F; // in both-mode we stored 0; in nibble-mode it's real

        // In both-mode, counter is HDR.high; in nibble-mode, counter is HDR.low.
        let counter = match self.mode {
            P11DataIdMode::Both { .. } => (hdr >> 4) & 0x0F,
            P11DataIdMode::Nibble { .. } => cnt,
        };
        if !Self::is_counter_valid(counter) {
            return Err(E2eError::Counter);
        }

        // Rebuild the CRC input and verify.
        let payload = &frame[..frame.len()-2];
        let crc_in = match self.mode {
            P11DataIdMode::Both { data_id } => {
                let mut crc_in = Vec::with_capacity(payload.len() + 3);
                crc_in.extend_from_slice(payload);
                crc_in.push((data_id & 0xFF) as u8);
                crc_in.push((data_id >> 8) as u8);
                crc_in.push(hdr); // hdr carries the counter in high nibble
                crc_in
            }
            P11DataIdMode::Nibble { data_id12 } => {
                let mut crc_in = Vec::with_capacity(payload.len() + 2);
                crc_in.extend_from_slice(payload);
                crc_in.push((data_id12 & 0xFF) as u8);
                crc_in.push(hdr);
                crc_in
            }
        };
        let want = CRC8P1.checksum(&crc_in);
        if want != crc_rx { return Err(E2eError::Crc); }

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

    fn payload_len(&self, frame: &[u8]) -> Option<usize> {
        frame.len().checked_sub(2)
    }
}

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

    #[test]
    fn p11_both_roundtrip() {
        let mut tx = E2eSession::new(
            P11 { mode: P11DataIdMode::Both { data_id: 0x1234 } },
            CounterPolicy::Rollover { bits: 4, step: 1 },
        );
        let f = tx.wrap(&[1,2,3]).unwrap();

        let mut rx = E2eSession::new(
            P11 { mode: P11DataIdMode::Both { data_id: 0x1234 } },
            CounterPolicy::Rollover { bits: 4, step: 1 },
        );
        let pl = rx.unwrap(&f).unwrap();
        assert_eq!(pl, &[1,2,3]);
    }

    #[test]
    fn p11_nibble_roundtrip() {
        let mut tx = E2eSession::new(
            P11 { mode: P11DataIdMode::Nibble { data_id12: 0x0A7 } }, // high nibble=0x0, low byte=0xA7
            CounterPolicy::Rollover { bits: 4, step: 1 },
        );
        let f = tx.wrap(&[0xAA, 0xBB]).unwrap();

        let mut rx = E2eSession::new(
            P11 { mode: P11DataIdMode::Nibble { data_id12: 0x0A7 } },
            CounterPolicy::Rollover { bits: 4, step: 1 },
        );
        let pl = rx.unwrap(&f).unwrap();
        assert_eq!(pl, &[0xAA, 0xBB]);
    }

    #[test]
    fn p11_detect_crc_error() {
        let p = P11 { mode: P11DataIdMode::Both { data_id: 0xBEEF } };
        let mut f = p.protect(&[1,2,3,4], 1).unwrap();
        f[0] ^= 0x01; // flip one bit in payload
        assert!(matches!(p.check(&f), Err(E2eError::Crc)));
    }

    #[test]
    fn p11_illegal_counter() {
        let p = P11 { mode: P11DataIdMode::Both { data_id: 0x55AA } };
        // 0x0F must be rejected
        assert!(matches!(p.protect(&[], 0x0F), Err(E2eError::Counter)));
    }
}