Skip to main content

simple_someip/e2e/
mod.rs

1//! E2E (End-to-End) protection for SOME/IP payloads.
2//!
3//! This module implements E2E Profile 4 and Profile 5 protection as specified
4//! in the [Open SOME/IP Specification](https://github.com/some-ip-com/open-someip-spec).
5//!
6//! # Why E2E does not implement `Encode`/`Decode`
7//!
8//! Unlike the rest of the wire path (headers, SD entries/options, payloads),
9//! E2E deliberately stays on its own `protect`/`check` API instead of
10//! `automotive_wire_codec::{Encode, Decode}`. Two structural mismatches drive
11//! this, and both are intentional — not gaps to be closed later:
12//!
13//! - **In-place mutation, not a fresh encode.** `protect` writes a header in
14//!   front of an already-serialized payload and, at the call site (see
15//!   `server::event_publisher::EventPublisher::publish_event`), the
16//!   surrounding SOME/IP length field gets rewritten *after* protection
17//!   because the final length depends on protect's output size. `Encode` is
18//!   a single forward pass; it has no place to express "go back and patch
19//!   bytes already written based on bytes written later." The codec's own
20//!   README scopes this kind of size-changing, post-hoc transform out of
21//!   `Encode` and points to a two-phase, consumer-owned API for it — which
22//!   is exactly what `protect`/`check` are.
23//! - **Status results, not `Result<_, Error>`.** `check_profile4`/`check_profile5`
24//!   return an [`E2ECheckResult`](crate::e2e::E2ECheckResult) carrying an [`E2ECheckStatus`] (`Ok`,
25//!   `CrcError`, `Repeated`, `WrongSequence`, `OkSomeLost`, `BadArgument`,
26//!   `Unchecked`) rather than an error. Several of those statuses (e.g.
27//!   `OkSomeLost`) are still *successful* checks that also carry diagnostic
28//!   information — that doesn't fit `Decode`'s binary success/error split.
29//!
30//! [`crate::e2e::Error`] (used only by `protect`'s buffer-sizing failure) is
31//! bridged onto [`crate::protocol::Error`] via `impl From<e2e::Error> for
32//! protocol::Error` (see `protocol::error`) so callers that want one error
33//! type can still get it, without forcing E2E itself onto the codec traits.
34//!
35//! # Example
36//!
37//! ```
38//! use simple_someip::e2e::{
39//!     Profile4Config, Profile4State,
40//!     protect_profile4, check_profile4,
41//!     E2ECheckStatus,
42//! };
43//!
44//! let config = Profile4Config::new(0x1234_5678, 15);
45//! let mut protect_state = Profile4State::new();
46//! let mut check_state = Profile4State::new();
47//!
48//! let payload = b"Hello, SOME/IP!";
49//! let mut buf = [0u8; 128];
50//! let len = protect_profile4(&config, &mut protect_state, payload, &mut buf).unwrap();
51//!
52//! let result = check_profile4(&config, &mut check_state, &buf[..len]);
53//! assert!(matches!(result.status, E2ECheckStatus::Ok));
54//! ```
55
56mod config;
57mod crc;
58mod e2e_checker;
59mod e2e_protector;
60mod error;
61mod registry;
62mod state;
63
64pub use config::{Profile4Config, Profile5Config};
65pub use e2e_checker::{check_profile4, check_profile5, check_profile5_with_header};
66pub use e2e_protector::{
67    PROFILE4_HEADER_SIZE, PROFILE5_HEADER_SIZE, protect_profile4, protect_profile5,
68    protect_profile5_with_header,
69};
70pub use error::Error;
71pub use registry::{E2E_REGISTRY_CAP, E2E_RX_STATE_CAP, E2ERegistry, E2ERegistryFull};
72pub use state::{Profile4State, Profile5State};
73
74/// Status result from E2E check operations.
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub enum E2ECheckStatus {
77    /// Initial state, no check performed yet.
78    Unchecked,
79    /// Check passed successfully.
80    Ok,
81    /// CRC verification failed.
82    CrcError,
83    /// Counter value is repeated (same as last received).
84    Repeated,
85    /// Check passed but some messages were lost (counter gap within tolerance).
86    OkSomeLost,
87    /// Counter sequence error (gap exceeds `max_delta_counter`).
88    WrongSequence,
89    /// Invalid input arguments (e.g., message too short).
90    BadArgument,
91}
92
93impl E2ECheckStatus {
94    /// Convert to a numeric return code compatible with E2E.
95    #[must_use]
96    pub fn to_return_code(self) -> u8 {
97        match self {
98            E2ECheckStatus::Unchecked => 0,
99            E2ECheckStatus::Ok => 1,
100            E2ECheckStatus::CrcError => 2,
101            E2ECheckStatus::Repeated => 3,
102            E2ECheckStatus::OkSomeLost => 4,
103            E2ECheckStatus::WrongSequence => 5,
104            E2ECheckStatus::BadArgument => 6,
105        }
106    }
107}
108
109/// Result from an E2E check operation.
110#[derive(Debug, Clone)]
111pub struct E2ECheckResult<'a> {
112    /// Status of the E2E check.
113    pub status: E2ECheckStatus,
114    /// Counter value extracted from the header (if parsing succeeded).
115    pub counter: Option<u32>,
116    /// Extracted payload without E2E header (if check succeeded).
117    ///
118    /// This is a borrowed subslice of the input `protected` buffer and is only
119    /// valid as long as that buffer is kept alive.
120    pub payload: Option<&'a [u8]>,
121}
122
123impl<'a> E2ECheckResult<'a> {
124    pub(crate) fn error(status: E2ECheckStatus) -> Self {
125        Self {
126            status,
127            counter: None,
128            payload: None,
129        }
130    }
131
132    pub(crate) fn success(status: E2ECheckStatus, counter: u32, payload: &'a [u8]) -> Self {
133        Self {
134            status,
135            counter: Some(counter),
136            payload: Some(payload),
137        }
138    }
139
140    /// Copy the extracted payload into an owned `Vec<u8>`.
141    ///
142    /// Returns `None` if the check did not produce a payload (e.g. on error).
143    #[cfg(feature = "std")]
144    #[must_use]
145    pub fn to_owned_payload(&self) -> Option<std::vec::Vec<u8>> {
146        self.payload.map(<[u8]>::to_vec)
147    }
148}
149
150/// Describes which E2E profile to apply for a given data element.
151#[derive(Debug, Clone)]
152pub enum E2EProfile {
153    /// E2E Profile 4 (CRC-32, 12-byte header).
154    Profile4(Profile4Config),
155    /// E2E Profile 5 (CRC-16, 3-byte header, no upper-header in CRC).
156    Profile5(Profile5Config),
157    /// E2E Profile 5 with SOME/IP upper-header included in the CRC.
158    Profile5WithHeader(Profile5Config),
159}
160
161/// Identifies a data element for E2E protection lookup.
162#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
163pub struct E2EKey {
164    /// SOME/IP service ID.
165    pub service_id: u16,
166    /// SOME/IP method or event ID.
167    pub method_or_event_id: u16,
168}
169
170impl E2EKey {
171    /// Create a new key from explicit service and method/event IDs.
172    #[must_use]
173    pub const fn new(service_id: u16, method_or_event_id: u16) -> Self {
174        Self {
175            service_id,
176            method_or_event_id,
177        }
178    }
179
180    /// Derive a key from a [`MessageId`](crate::protocol::MessageId).
181    #[must_use]
182    pub fn from_message_id(message_id: crate::protocol::MessageId) -> Self {
183        Self {
184            service_id: message_id.service_id(),
185            method_or_event_id: message_id.method_id(),
186        }
187    }
188}
189
190/// Internal E2E state, one per registered key.
191#[derive(Debug, Clone)]
192pub(crate) enum E2EState {
193    /// State for Profile 4.
194    Profile4(Profile4State),
195    /// State for Profile 5 (used by both `Profile5` and `Profile5WithHeader`).
196    Profile5(Profile5State),
197}
198
199impl E2EState {
200    pub(crate) fn from_profile(profile: &E2EProfile) -> Self {
201        match profile {
202            E2EProfile::Profile4(_) => Self::Profile4(Profile4State::new()),
203            E2EProfile::Profile5(_) | E2EProfile::Profile5WithHeader(_) => {
204                Self::Profile5(Profile5State::new())
205            }
206        }
207    }
208}
209
210/// Run the appropriate E2E check for the given profile, returning the status
211/// and the best available payload slice (stripped on success, original on error).
212pub(crate) fn e2e_check<'a>(
213    profile: &E2EProfile,
214    state: &mut E2EState,
215    payload: &'a [u8],
216    upper_header: [u8; 8],
217) -> (E2ECheckStatus, &'a [u8]) {
218    let result = match (profile, state) {
219        (E2EProfile::Profile4(config), E2EState::Profile4(st)) => {
220            check_profile4(config, st, payload)
221        }
222        (E2EProfile::Profile5(config), E2EState::Profile5(st)) => {
223            check_profile5(config, st, payload)
224        }
225        (E2EProfile::Profile5WithHeader(config), E2EState::Profile5(st)) => {
226            check_profile5_with_header(config, st, payload, upper_header)
227        }
228        _ => return (E2ECheckStatus::BadArgument, payload),
229    };
230    let stripped = result.payload.unwrap_or(payload);
231    (result.status, stripped)
232}
233
234/// Run the appropriate E2E protect for the given profile.
235///
236/// # Errors
237///
238/// Returns [`Error::BufferTooSmall`] if `output` cannot hold the protected payload.
239pub(crate) fn e2e_protect(
240    profile: &E2EProfile,
241    state: &mut E2EState,
242    payload: &[u8],
243    upper_header: [u8; 8],
244    output: &mut [u8],
245) -> Result<usize, Error> {
246    match (profile, state) {
247        (E2EProfile::Profile4(config), E2EState::Profile4(st)) => {
248            protect_profile4(config, st, payload, output)
249        }
250        (E2EProfile::Profile5(config), E2EState::Profile5(st)) => {
251            protect_profile5(config, st, payload, output)
252        }
253        (E2EProfile::Profile5WithHeader(config), E2EState::Profile5(st)) => {
254            protect_profile5_with_header(config, st, payload, upper_header, output)
255        }
256        _ => unreachable!("E2EState is always created from E2EProfile"),
257    }
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263
264    #[test]
265    fn test_status_return_codes() {
266        assert_eq!(E2ECheckStatus::Unchecked.to_return_code(), 0);
267        assert_eq!(E2ECheckStatus::Ok.to_return_code(), 1);
268        assert_eq!(E2ECheckStatus::CrcError.to_return_code(), 2);
269        assert_eq!(E2ECheckStatus::Repeated.to_return_code(), 3);
270        assert_eq!(E2ECheckStatus::OkSomeLost.to_return_code(), 4);
271        assert_eq!(E2ECheckStatus::WrongSequence.to_return_code(), 5);
272        assert_eq!(E2ECheckStatus::BadArgument.to_return_code(), 6);
273    }
274
275    #[test]
276    fn test_profile4_roundtrip() {
277        let config = Profile4Config::new(0x1234_5678, 15);
278        let mut protect_state = Profile4State::new();
279        let mut check_state = Profile4State::new();
280
281        let payload = b"Test payload data";
282        let mut buf = [0u8; 256];
283        let len = protect_profile4(&config, &mut protect_state, payload, &mut buf).unwrap();
284        let protected = &buf[..len];
285
286        assert_eq!(len, payload.len() + 12); // 12-byte header
287
288        let result = check_profile4(&config, &mut check_state, protected);
289        assert_eq!(result.status, E2ECheckStatus::Ok);
290        assert_eq!(result.counter, Some(0));
291        assert_eq!(result.payload, Some(payload.as_slice()));
292    }
293
294    #[test]
295    fn test_profile5_roundtrip() {
296        let config = Profile5Config::new(0x1234, 20, 15);
297        let mut protect_state = Profile5State::new();
298        let mut check_state = Profile5State::new();
299
300        // Payload must be padded to data_length (20 bytes) for check_profile5
301        let mut payload = [0u8; 20];
302        payload[..17].copy_from_slice(b"Test payload data");
303        let mut buf = [0u8; 256];
304        let len = protect_profile5(&config, &mut protect_state, &payload, &mut buf).unwrap();
305        let protected = &buf[..len];
306
307        assert_eq!(len, payload.len() + 3); // 3-byte header
308
309        let result = check_profile5(&config, &mut check_state, protected);
310        assert_eq!(result.status, E2ECheckStatus::Ok);
311        assert_eq!(result.counter, Some(0));
312        assert_eq!(result.payload, Some(payload.as_slice()));
313    }
314
315    #[test]
316    fn test_profile4_sequence_detection() {
317        let config = Profile4Config::new(0x1234_5678, 5);
318        let mut protect_state = Profile4State::new();
319        let mut check_state = Profile4State::new();
320
321        let payload = b"Test";
322        let mut buf1 = [0u8; 256];
323        let mut buf2 = [0u8; 256];
324
325        // First message - should be Ok
326        let len1 = protect_profile4(&config, &mut protect_state, payload, &mut buf1).unwrap();
327        let result1 = check_profile4(&config, &mut check_state, &buf1[..len1]);
328        assert_eq!(result1.status, E2ECheckStatus::Ok);
329
330        // Second message - should be Ok
331        let len2 = protect_profile4(&config, &mut protect_state, payload, &mut buf2).unwrap();
332        let result2 = check_profile4(&config, &mut check_state, &buf2[..len2]);
333        assert_eq!(result2.status, E2ECheckStatus::Ok);
334
335        // Replay first message - should be Repeated or WrongSequence
336        let result3 = check_profile4(&config, &mut check_state, &buf1[..len1]);
337        assert!(matches!(
338            result3.status,
339            E2ECheckStatus::Repeated | E2ECheckStatus::WrongSequence
340        ));
341    }
342
343    #[test]
344    fn test_profile4_some_lost_detection() {
345        let config = Profile4Config::new(0x1234_5678, 5);
346        let mut protect_state = Profile4State::new();
347        let mut check_state = Profile4State::new();
348
349        let payload = b"Test";
350        let mut buf = [0u8; 256];
351
352        // First message
353        let len = protect_profile4(&config, &mut protect_state, payload, &mut buf).unwrap();
354        let result1 = check_profile4(&config, &mut check_state, &buf[..len]);
355        assert_eq!(result1.status, E2ECheckStatus::Ok);
356
357        // Skip a few messages by advancing protector counter
358        protect_profile4(&config, &mut protect_state, payload, &mut buf).unwrap();
359        protect_profile4(&config, &mut protect_state, payload, &mut buf).unwrap();
360        let len = protect_profile4(&config, &mut protect_state, payload, &mut buf).unwrap();
361
362        // Check skipped message - should be OkSomeLost (delta=3, within max_delta=5)
363        let result4 = check_profile4(&config, &mut check_state, &buf[..len]);
364        assert_eq!(result4.status, E2ECheckStatus::OkSomeLost);
365    }
366
367    #[test]
368    fn test_profile4_wrong_sequence_detection() {
369        let config = Profile4Config::new(0x1234_5678, 2);
370        let mut protect_state = Profile4State::new();
371        let mut check_state = Profile4State::new();
372
373        let payload = b"Test";
374        let mut buf = [0u8; 256];
375
376        // First message
377        let len = protect_profile4(&config, &mut protect_state, payload, &mut buf).unwrap();
378        let result1 = check_profile4(&config, &mut check_state, &buf[..len]);
379        assert_eq!(result1.status, E2ECheckStatus::Ok);
380
381        // Skip many messages (exceed max_delta)
382        for _ in 0..5 {
383            protect_profile4(&config, &mut protect_state, payload, &mut buf).unwrap();
384        }
385        let len = protect_profile4(&config, &mut protect_state, payload, &mut buf).unwrap();
386
387        // Check - should be WrongSequence (delta=6, exceeds max_delta=2)
388        let result = check_profile4(&config, &mut check_state, &buf[..len]);
389        assert_eq!(result.status, E2ECheckStatus::WrongSequence);
390    }
391
392    #[test]
393    fn test_profile4_crc_error() {
394        let config = Profile4Config::new(0x1234_5678, 15);
395        let mut protect_state = Profile4State::new();
396        let mut check_state = Profile4State::new();
397
398        let payload = b"Test";
399        let mut buf = [0u8; 256];
400        let len = protect_profile4(&config, &mut protect_state, payload, &mut buf).unwrap();
401
402        // Corrupt the CRC (last 4 bytes of header)
403        buf[8] ^= 0xFF;
404
405        let result = check_profile4(&config, &mut check_state, &buf[..len]);
406        assert_eq!(result.status, E2ECheckStatus::CrcError);
407    }
408
409    #[test]
410    fn test_profile5_crc_error() {
411        let config = Profile5Config::new(0x1234, 20, 15);
412        let mut protect_state = Profile5State::new();
413        let mut check_state = Profile5State::new();
414
415        let mut payload = [0u8; 20];
416        payload[..4].copy_from_slice(b"Test");
417        let mut buf = [0u8; 256];
418        let len = protect_profile5(&config, &mut protect_state, &payload, &mut buf).unwrap();
419
420        // Corrupt the CRC (bytes 1-2 of header)
421        buf[1] ^= 0xFF;
422
423        let result = check_profile5(&config, &mut check_state, &buf[..len]);
424        assert_eq!(result.status, E2ECheckStatus::CrcError);
425    }
426
427    #[test]
428    fn test_profile4_bad_argument_short_message() {
429        let config = Profile4Config::new(0x1234_5678, 15);
430        let mut check_state = Profile4State::new();
431
432        // Message too short (less than 12-byte header)
433        let short_message = [0u8; 8];
434        let result = check_profile4(&config, &mut check_state, &short_message);
435        assert_eq!(result.status, E2ECheckStatus::BadArgument);
436    }
437
438    #[test]
439    fn test_profile5_bad_argument_short_message() {
440        let config = Profile5Config::new(0x1234, 20, 15);
441        let mut check_state = Profile5State::new();
442
443        // Message too short (less than 3-byte header)
444        let short_message = [0u8; 2];
445        let result = check_profile5(&config, &mut check_state, &short_message);
446        assert_eq!(result.status, E2ECheckStatus::BadArgument);
447    }
448
449    #[cfg(feature = "std")]
450    #[test]
451    fn test_check_result_to_owned_payload() {
452        let data = b"hello";
453        let result = E2ECheckResult::success(E2ECheckStatus::Ok, 0, data);
454        let owned = result.to_owned_payload();
455        assert_eq!(owned, Some(b"hello".to_vec()));
456
457        let err_result = E2ECheckResult::error(E2ECheckStatus::CrcError);
458        assert_eq!(err_result.to_owned_payload(), None);
459    }
460
461    #[test]
462    fn test_e2e_key_from_message_id() {
463        let mid = crate::protocol::MessageId::new_from_service_and_method(0x1234, 0x0001);
464        let key = E2EKey::from_message_id(mid);
465        assert_eq!(key.service_id, 0x1234);
466        assert_eq!(key.method_or_event_id, 0x0001);
467    }
468}