e2e-protection 0.4.0

End-to-End protection core with pluggable profiles. AUTOSAR profile family is optional via feature
Documentation
//! Profile 4m (Method-call style, CAN FD / SOME-IP RPC)
//!
//! **Intent (simplified)**:
//! - **Counter**: 16-bit
//! - **DataID**: 32-bit explicit
//! - **Meta**: 32-bit (2 bits Type, 2 bits Result, 28 bits SourceID)
//! - **CRC**: 32-bit CRC-32P4
//! - **Length**: 16-bit
//!
//! **Frame layout (educational mapping)**:
//! ```text
//! [ payload ... | Length(2B) | Counter(2B) | DataID(4B) | Meta(4B) | CRC32(4B) ]
//! ```
//!
//! **Meta field**: `(Type << 30) | (Result << 28) | (SourceID & 0x0FFF_FFFF)`

use crc::{Crc, CRC_32_AUTOSAR};

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

const CRC32P4: Crc<u32> = Crc::<u32>::new(&CRC_32_AUTOSAR);

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct P4m {
    pub data_id: u32,
    pub msg_type: u8,   // 2 bits
    pub msg_result: u8, // 2 bits
    pub source_id: u32, // 28 bits
}

impl E2eProfile for P4m {
    fn protect(&self, payload: &[u8], counter: u32) -> Result<Vec<u8>, E2eError> {
        let meta = ((self.msg_type as u32 & 0x3) << 30)
                 | ((self.msg_result as u32 & 0x3) << 28)
                 | (self.source_id & 0x0FFF_FFFF);

        let mut out = Vec::with_capacity(payload.len() + 16);
        out.extend_from_slice(payload);
        out.extend_from_slice(&(payload.len() as u16).to_be_bytes());
        out.extend_from_slice(&(counter as u16).to_be_bytes());
        out.extend_from_slice(&self.data_id.to_be_bytes());
        out.extend_from_slice(&meta.to_be_bytes());
        let crc = CRC32P4.checksum(&out);
        out.extend_from_slice(&crc.to_be_bytes());
        Ok(out)
    }

    fn check(&self, frame: &[u8]) -> Result<CheckInfo, E2eError> {
        if frame.len() < 16 { return Err(E2eError::Length); }
        let crc_recv = u32::from_be_bytes(frame[frame.len()-4..].try_into().unwrap());
        let without_crc = &frame[..frame.len()-4];
        let want = CRC32P4.checksum(without_crc);
        if want != crc_recv { return Err(E2eError::Crc); }

        let tail = &without_crc[without_crc.len()-12..];
        let len = u16::from_be_bytes(tail[0..2].try_into().unwrap()) as usize;
        let counter = u16::from_be_bytes(tail[2..4].try_into().unwrap());
        let data_id = u32::from_be_bytes(tail[4..8].try_into().unwrap());
        let meta = u32::from_be_bytes(tail[8..12].try_into().unwrap());

        if data_id != self.data_id { return Err(E2eError::DataId); }
        if (meta >> 30) != (self.msg_type & 0x3) as u32 { return Err(E2eError::DataId); }
        if ((meta >> 28) & 0x3) != (self.msg_result & 0x3) as u32 { return Err(E2eError::DataId); }
        if (meta & 0x0FFF_FFFF) != (self.source_id & 0x0FFF_FFFF) { return Err(E2eError::DataId); }

        let payload_len = without_crc.len() - 12;
        if len != payload_len { return Err(E2eError::Length); }

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

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

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

    #[test]
    fn p4m_roundtrip() {
        let mut tx = E2eSession::new(P4m{data_id:0x99, msg_type:1, msg_result:0, source_id:0xABCDE},
            CounterPolicy::Rollover{bits:16,step:1});
        let f = tx.wrap(&[7,7,7]).unwrap();

        let mut rx = E2eSession::new(P4m{data_id:0x99, msg_type:1, msg_result:0, source_id:0xABCDE},
            CounterPolicy::Rollover{bits:16,step:1});
        let pl = rx.unwrap(&f).unwrap();
        assert_eq!(pl, &[7,7,7]);
    }

    #[test]
    fn p4m_crc_detects_error() {
        let p = P4m{data_id:1, msg_type:0, msg_result:0, source_id:0};
        let mut f = p.protect(&[1,2,3], 1).unwrap();
        f[0] ^= 0x80;
        assert!(matches!(p.check(&f), Err(E2eError::Crc)));
    }
}