diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bf7c3c..044332c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,62 @@ # Changelog +## [0.13.0] + +Contains a breaking change to a public enum, so this release takes the major +position under this crate's 0.x convention. As in 0.12.0, the `version` in +`Cargo.toml` is bumped here rather than left to release-plz so that +`cargo-semver-checks` compares against the version this change actually lands as. + +### Breaking — `E2ECheckStatus` reports the real validation error + +The E2E Profile 4 and Profile 5 algorithms now come from the `simple-e2e` +crate; this crate keeps only the SOME/IP-specific glue (the registry, `E2EKey`, +`E2EProfile`, and the return-code mapping). The visible consequence is that a +rejected message tells you *why*: + +```rust +// Before — two tags, no detail. A downstream logger wanting the CRC values +// had to fabricate them. +E2ECheckStatus::CrcError +E2ECheckStatus::BadArgument + +// After — the profile's own error, with the received and computed values. +E2ECheckStatus::Invalid(E2EValidateError::Profile5( + profile5::ValidateError::CrcMismatch { got: 0x1A2B, expected: 0x1A2C }, +)) +E2ECheckStatus::Invalid(E2EValidateError::Profile4( + profile4::ValidateError::DataIdMismatch { got: .., expected: .. }, +)) +``` + +`E2ECheckStatus::to_return_code` (and `sd_codec::e2e_status_code`) are +unchanged on the wire: a CRC mismatch is still `2`, every other validation +failure is still `6`. `E2EValidateError::is_crc_mismatch` is the predicate +behind that split. `E2EValidateError` is `#[non_exhaustive]`, `Copy`, and +implements `Display` through the inner error. + +Migration for a `match` on `E2ECheckStatus`: + +| 0.12 | 0.13 | +|---|---| +| `CrcError` | `Invalid(e) if e.is_crc_mismatch()` | +| `BadArgument` | `Invalid(_)` (after the CRC arm) | +| `CrcError \| BadArgument` | `Invalid(_)` | + +Everything else in `e2e` keeps its name. `Profile4Config`, `Profile5Config`, +`Profile4State`, `Profile5State` are now type aliases of +`simple_e2e::profile{4,5}::{Config, State}`; `PROFILE4_HEADER_SIZE` / +`PROFILE5_HEADER_SIZE` alias the profile modules' `HEADER_SIZE`; and the six +`check_*` / `protect_*` functions are thin adapters that still return +`E2ECheckResult` / `e2e::Error`. `E2ECheckResult::counter` stays `Option`. +The direct `crc` dependency is gone (it arrives through `simple-e2e`). + +### Removed + +- `E2ECheckStatus::CrcError`, `E2ECheckStatus::BadArgument` (see above). +- The crate-private CRC and profile modules and their unit tests, which + duplicated `simple-e2e`'s. + ## [0.12.0] Contains a breaking change to the public error enums, so this release takes the diff --git a/Cargo.lock b/Cargo.lock index 601d404..5ac8ff5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -632,16 +632,24 @@ dependencies = [ ] [[package]] -name = "simple-someip" -version = "0.12.0" +name = "simple-e2e" +version = "0.1.0" dependencies = [ "crc", + "thiserror", +] + +[[package]] +name = "simple-someip" +version = "0.13.0" +dependencies = [ "critical-section", "embassy-executor", "embassy-sync 0.6.2", "embedded-io 0.7.1", "futures-util", "heapless 0.9.2", + "simple-e2e", "socket2 0.5.10", "thiserror", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 3105fc3..6e76b12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,14 +24,13 @@ exclude = ["tools/size_probe"] [package] name = "simple-someip" -version = "0.12.0" +version = "0.13.0" edition = "2024" license = "MIT OR Apache-2.0" description = "A lightweight SOME/IP serialization and communication library" repository = "https://github.com/luminartech/simple_someip" [dependencies] -crc = "3.4" # embassy-sync provides no_std-compatible bounded channels used as the # channel backend when the `bare_metal` feature is active. The # `critical-section` and `portable-atomic` deps ship with embassy-sync and @@ -64,6 +63,9 @@ futures-util = { version = "0.3", default-features = false, features = [ "async-await-macro", ], optional = true } heapless = "0.9" +# The E2E Profile 4 / Profile 5 algorithms. This crate keeps only the +# SOME/IP-specific glue (registry, key, profile selection, return codes). +simple-e2e = { version = "0.1", default-features = false } socket2 = { version = "0.5", optional = true, features = ["all"] } thiserror = { version = "2", default-features = false } tokio = { version = "1", default-features = false, features = [ @@ -94,7 +96,7 @@ default = ["std"] # tracing behind a feature so those builds can opt out; std users # pick it up automatically through the `std` feature below. tracing = ["dep:tracing"] -std = ["embedded-io/std", "thiserror/std", "tracing", "tracing/std", "_alloc"] +std = ["embedded-io/std", "thiserror/std", "tracing", "tracing/std", "_alloc", "simple-e2e/std"] # Feature split: `client` exposes the protocol/trait-surface client # (no tokio, no socket2); `client-tokio` layers the tokio + socket2 # convenience defaults on top. Consumers of the bare-metal trait surface @@ -203,3 +205,6 @@ required-features = ["client", "server", "bare_metal"] [[test]] name = "buffer_pool" + +[patch.crates-io] +simple-e2e = { path = "../simple_e2e" } diff --git a/simple-someip-embassy-net/Cargo.toml b/simple-someip-embassy-net/Cargo.toml index 7cacaae..faf31a6 100644 --- a/simple-someip-embassy-net/Cargo.toml +++ b/simple-someip-embassy-net/Cargo.toml @@ -19,7 +19,7 @@ readme = "README.md" # [dependencies] -simple-someip = { path = "..", version = "0.12", default-features = false, features = [ +simple-someip = { path = "..", version = "0.13", default-features = false, features = [ "client", "server", "bare_metal", diff --git a/src/e2e/config.rs b/src/e2e/config.rs deleted file mode 100644 index 91d8a73..0000000 --- a/src/e2e/config.rs +++ /dev/null @@ -1,55 +0,0 @@ -//! Configuration structures for E2E profiles. - -/// Configuration for E2E Profile 4. -#[derive(Debug, Clone)] -pub struct Profile4Config { - /// Unique identifier for this data element (included in CRC calculation). - pub data_id: u32, - /// Maximum allowed counter delta before reporting `WrongSequence`. - /// A delta of 1 means consecutive messages, delta > 1 means some lost. - pub max_delta_counter: u16, -} - -impl Profile4Config { - /// Create a new Profile 4 configuration. - /// - /// # Arguments - /// * `data_id` - Unique identifier for this data element - /// * `max_delta_counter` - Maximum allowed gap in counter sequence - #[must_use] - pub fn new(data_id: u32, max_delta_counter: u16) -> Self { - Self { - data_id, - max_delta_counter, - } - } -} - -/// Configuration for E2E Profile 5. -#[derive(Debug, Clone)] -pub struct Profile5Config { - /// Unique identifier for this data element (included in CRC calculation). - /// Profile 5 uses a 16-bit `DataID`. - pub data_id: u16, - /// Expected data length (used in CRC calculation). - pub data_length: u16, - /// Maximum allowed counter delta before reporting `WrongSequence`. - pub max_delta_counter: u8, -} - -impl Profile5Config { - /// Create a new Profile 5 configuration. - /// - /// # Arguments - /// * `data_id` - Unique identifier for this data element - /// * `data_length` - Expected length of protected data - /// * `max_delta_counter` - Maximum allowed gap in counter sequence - #[must_use] - pub fn new(data_id: u16, data_length: u16, max_delta_counter: u8) -> Self { - Self { - data_id, - data_length, - max_delta_counter, - } - } -} diff --git a/src/e2e/crc.rs b/src/e2e/crc.rs deleted file mode 100644 index a72854a..0000000 --- a/src/e2e/crc.rs +++ /dev/null @@ -1,225 +0,0 @@ -//! CRC computation helpers for E2E profiles. - -use crc::{CRC_16_IBM_3740, CRC_32_AUTOSAR, Crc}; - -/// CRC-32P4 algorithm used by E2E Profile 4. -/// Polynomial: 0xF4ACFB13 -const CRC32_P4: Crc = Crc::::new(&CRC_32_AUTOSAR); - -/// CRC-16-CCITT algorithm used by E2E Profile 5. -/// Polynomial: 0x1021, Init: 0xFFFF (IBM 3740 variant, also known as CRC-16-CCITT-FALSE) -const CRC16_CCITT: Crc = Crc::::new(&CRC_16_IBM_3740); - -/// Compute CRC-32P4 for Profile 4. -/// -/// The CRC is computed over: Length (2) + Counter (2) + `DataID` (4) + Payload -/// Note: CRC field itself is not included in the calculation. -pub fn compute_crc32_p4(length: u16, counter: u16, data_id: u32, payload: &[u8]) -> u32 { - let mut digest = CRC32_P4.digest(); - - // Length (big-endian) - digest.update(&length.to_be_bytes()); - - // Counter (big-endian) - digest.update(&counter.to_be_bytes()); - - // DataID (big-endian) - digest.update(&data_id.to_be_bytes()); - - // Payload - digest.update(payload); - - digest.finalize() -} - -/// Compute CRC-16-CCITT for Profile 5. -/// -/// Per E2E Profile 5, the CRC is computed over all data bytes except the -/// CRC field itself, plus the `DataID`. Specifically: -/// - Counter (1 byte) + Payload (N bytes) + `DataID` (2 bytes, little-endian) -/// -/// Note: CRC field itself is not included in the calculation. -/// Note: `DataLength` is NOT included in the CRC calculation. -pub fn compute_crc16_p5(data_id: u16, counter: u8, payload: &[u8]) -> u16 { - crate::log::trace!( - "CRC-16 Profile5: data_id=0x{:04X}, counter={}, payload_len={}, payload={:02X?}", - data_id, - counter, - payload.len(), - payload - ); - - let mut digest = CRC16_CCITT.digest(); - - // Counter (single byte) - digest.update(&[counter]); - - // Payload - digest.update(payload); - - // DataID (little-endian) - let data_id_bytes = data_id.to_le_bytes(); - digest.update(&data_id_bytes); - - let crc = digest.finalize(); - crate::log::trace!( - "CRC-16 Profile5: computed CRC = 0x{:04X} (bytes: {:02X?})", - crc, - crc.to_le_bytes() - ); - - crc -} - -/// Compute CRC-16-CCITT for Profile 5 with SOME/IP upper-header prefix. -/// -/// The 8-byte upper header (UPPER-HEADER-BITS-TO-SHIFT = 64 bits) is prepended -/// to the CRC input before Counter + Payload + `DataID`. -/// -/// CRC input order: `upper_header(8)` + Counter(1) + Payload(N) + DataID(2 LE) -pub fn compute_crc16_p5_with_header( - data_id: u16, - counter: u8, - payload: &[u8], - upper_header: [u8; 8], -) -> u16 { - crate::log::trace!( - "CRC-16 Profile5 (with header): data_id=0x{:04X}, counter={}, payload_len={}, upper_header={:02X?}, payload={:02X?}", - data_id, - counter, - payload.len(), - upper_header, - payload - ); - - let mut digest = CRC16_CCITT.digest(); - digest.update(&upper_header); - digest.update(&[counter]); - digest.update(payload); - digest.update(&data_id.to_le_bytes()); - - let crc = digest.finalize(); - crate::log::trace!( - "CRC-16 Profile5 (with header): computed CRC = 0x{:04X} (bytes: {:02X?})", - crc, - crc.to_le_bytes() - ); - - crc -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_crc32_p4_basic() { - // Basic smoke test - verify CRC changes with different inputs - let crc1 = compute_crc32_p4(10, 0, 0x1234_5678, b"test"); - let crc2 = compute_crc32_p4(10, 1, 0x1234_5678, b"test"); - let crc3 = compute_crc32_p4(10, 0, 0x1234_5679, b"test"); - let crc4 = compute_crc32_p4(10, 0, 0x1234_5678, b"Test"); - - assert_ne!(crc1, crc2, "Different counter should produce different CRC"); - assert_ne!(crc1, crc3, "Different data_id should produce different CRC"); - assert_ne!(crc1, crc4, "Different payload should produce different CRC"); - } - - #[test] - fn test_crc16_p5_basic() { - // Basic smoke test - verify CRC changes with different inputs - let crc1 = compute_crc16_p5(0x1234, 0, b"test"); - let crc2 = compute_crc16_p5(0x1234, 1, b"test"); - let crc3 = compute_crc16_p5(0x1235, 0, b"test"); - let crc4 = compute_crc16_p5(0x1234, 0, b"Test"); - - assert_ne!(crc1, crc2, "Different counter should produce different CRC"); - assert_ne!(crc1, crc3, "Different data_id should produce different CRC"); - assert_ne!(crc1, crc4, "Different payload should produce different CRC"); - } - - #[test] - fn test_crc32_p4_deterministic() { - // Same inputs should always produce same output - let crc1 = compute_crc32_p4(20, 5, 0xABCD_EF01, b"payload data"); - let crc2 = compute_crc32_p4(20, 5, 0xABCD_EF01, b"payload data"); - assert_eq!(crc1, crc2); - } - - #[test] - fn test_crc16_p5_deterministic() { - // Same inputs should always produce same output - let crc1 = compute_crc16_p5(0xABCD, 5, b"payload data"); - let crc2 = compute_crc16_p5(0xABCD, 5, b"payload data"); - assert_eq!(crc1, crc2); - } - - #[test] - fn test_crc32_p4_empty_payload() { - // Should work with empty payload - let crc = compute_crc32_p4(8, 0, 0x1234_5678, b""); - assert_ne!(crc, 0); // CRC should be non-trivial even for empty payload - } - - #[test] - fn test_crc16_p5_empty_payload() { - // Should work with empty payload - let crc = compute_crc16_p5(0x1234, 0, b""); - assert_ne!(crc, 0); // CRC should be non-trivial even for empty payload - } - - #[test] - fn test_crc16_p5_with_header_nonzero_header_changes_crc() { - // A non-zero upper header must produce a different CRC than the headerless variant - let header = [0x00, 0x01, 0x00, 0x05, 0x01, 0x03, 0x02, 0x00]; - let crc_no_header = compute_crc16_p5(0x1234, 0, b"test"); - let crc_with_header = compute_crc16_p5_with_header(0x1234, 0, b"test", header); - assert_ne!( - crc_no_header, crc_with_header, - "Non-zero upper_header should change CRC vs headerless path" - ); - } - - #[test] - fn test_crc16_p5_with_header_different_headers_differ() { - let header_a = [0x00, 0x01, 0x00, 0x05, 0x01, 0x03, 0x02, 0x00]; - let header_b = [0x00, 0x02, 0x00, 0x05, 0x01, 0x03, 0x02, 0x00]; // single byte changed - let crc_a = compute_crc16_p5_with_header(0x1234, 0, b"test", header_a); - let crc_b = compute_crc16_p5_with_header(0x1234, 0, b"test", header_b); - assert_ne!( - crc_a, crc_b, - "Different upper_header should produce different CRC" - ); - } - - #[test] - fn test_crc16_p5_with_header_deterministic() { - let header = [0x00, 0x01, 0x00, 0x05, 0x01, 0x03, 0x02, 0x00]; - let crc1 = compute_crc16_p5_with_header(0x1234, 0, b"test", header); - let crc2 = compute_crc16_p5_with_header(0x1234, 0, b"test", header); - assert_eq!(crc1, crc2, "Same inputs should always produce same CRC"); - } - - #[test] - fn test_crc16_p5_with_header_each_field_matters() { - let header = [0x00, 0x01, 0x00, 0x05, 0x01, 0x03, 0x02, 0x00]; - let baseline = compute_crc16_p5_with_header(0x1234, 0, b"test", header); - - let crc_diff_data_id = compute_crc16_p5_with_header(0x1235, 0, b"test", header); - let crc_diff_counter = compute_crc16_p5_with_header(0x1234, 1, b"test", header); - let crc_diff_payload = compute_crc16_p5_with_header(0x1234, 0, b"Test", header); - - assert_ne!( - baseline, crc_diff_data_id, - "Different data_id should change CRC" - ); - assert_ne!( - baseline, crc_diff_counter, - "Different counter should change CRC" - ); - assert_ne!( - baseline, crc_diff_payload, - "Different payload should change CRC" - ); - } -} diff --git a/src/e2e/e2e_checker.rs b/src/e2e/e2e_checker.rs deleted file mode 100644 index 1921273..0000000 --- a/src/e2e/e2e_checker.rs +++ /dev/null @@ -1,670 +0,0 @@ -//! E2E checking functions for validating E2E-protected payloads. - -use super::config::{Profile4Config, Profile5Config}; -use super::crc::{compute_crc16_p5, compute_crc16_p5_with_header, compute_crc32_p4}; -use super::e2e_protector::{PROFILE4_HEADER_SIZE, PROFILE5_HEADER_SIZE}; -use super::state::{Profile4State, Profile5State}; -use super::{E2ECheckResult, E2ECheckStatus}; - -/// Check E2E Profile 4 protected data. -/// -/// Validates the 12-byte header: -/// - Length (2 bytes): Verifies against actual message length -/// - Counter (2 bytes): Checks sequence continuity -/// - `DataID` (4 bytes): Must match configuration -/// - CRC (4 bytes): Verified against computed CRC-32P4 -/// -/// # Arguments -/// * `config` - Profile 4 configuration -/// * `state` - Mutable state for counter tracking -/// * `protected` - The protected message (header + payload) -/// -/// # Returns -/// An `E2ECheckResult` containing the status, counter, and extracted payload. -pub fn check_profile4<'a>( - config: &Profile4Config, - state: &mut Profile4State, - protected: &'a [u8], -) -> E2ECheckResult<'a> { - // Check minimum length - if protected.len() < PROFILE4_HEADER_SIZE { - return E2ECheckResult::error(E2ECheckStatus::BadArgument); - } - - // Parse header - let length = u16::from_be_bytes([protected[0], protected[1]]); - let counter = u16::from_be_bytes([protected[2], protected[3]]); - let data_id = u32::from_be_bytes([protected[4], protected[5], protected[6], protected[7]]); - let received_crc = - u32::from_be_bytes([protected[8], protected[9], protected[10], protected[11]]); - - // Verify length field matches actual message length - if length as usize != protected.len() { - return E2ECheckResult::error(E2ECheckStatus::BadArgument); - } - - // Verify DataID matches configuration - if data_id != config.data_id { - return E2ECheckResult::error(E2ECheckStatus::BadArgument); - } - - // Extract payload - let payload = &protected[PROFILE4_HEADER_SIZE..]; - - // Compute and verify CRC - let computed_crc = compute_crc32_p4(length, counter, data_id, payload); - if computed_crc != received_crc { - return E2ECheckResult::error(E2ECheckStatus::CrcError); - } - - // Check sequence - let status = check_sequence_profile4(state, counter, config.max_delta_counter); - - // Update state - state.last_counter = Some(counter); - - E2ECheckResult::success(status, u32::from(counter), payload) -} - -/// Check E2E Profile 5 protected data. -/// -/// Validates the 3-byte header: -/// - CRC (2 bytes, little-endian): Verified against computed CRC-16-CCITT -/// - Counter (1 byte): Checks sequence continuity -/// -/// # Arguments -/// * `config` - Profile 5 configuration -/// * `state` - Mutable state for counter tracking -/// * `protected` - The protected message (header + payload) -/// -/// # Returns -/// An `E2ECheckResult` containing the status, counter, and extracted payload. -pub fn check_profile5<'a>( - config: &Profile5Config, - state: &mut Profile5State, - protected: &'a [u8], -) -> E2ECheckResult<'a> { - // Check minimum length - if protected.len() < PROFILE5_HEADER_SIZE { - return E2ECheckResult::error(E2ECheckStatus::BadArgument); - } - - // Verify data length matches configuration (header + payload = config.data_length) - let expected_total_length = PROFILE5_HEADER_SIZE + config.data_length as usize; - if protected.len() != expected_total_length { - crate::log::warn!( - "E2E Profile 5 length mismatch: expected {} bytes (3 header + {} payload), got {} bytes", - expected_total_length, - config.data_length, - protected.len() - ); - return E2ECheckResult::error(E2ECheckStatus::BadArgument); - } - - // Parse header: CRC (2, little-endian) + Counter (1) - let received_crc = u16::from_le_bytes([protected[0], protected[1]]); - let counter = protected[2]; - - // Extract payload - let payload = &protected[PROFILE5_HEADER_SIZE..]; - - // Compute and verify CRC - let computed_crc = compute_crc16_p5(config.data_id, counter, payload); - if computed_crc != received_crc { - return E2ECheckResult::error(E2ECheckStatus::CrcError); - } - - // Check sequence - let status = check_sequence_profile5(state, counter, config.max_delta_counter); - - // Update state - state.last_counter = Some(counter); - - E2ECheckResult::success(status, u32::from(counter), payload) -} - -/// Check E2E Profile 5 protected data with SOME/IP upper-header in the CRC. -/// -/// Validates the 3-byte header: -/// - CRC (2 bytes, little-endian): Verified against CRC-16-CCITT computed over -/// `upper_header(8) + Counter(1) + Payload(N) + DataID(2 LE)` -/// - Counter (1 byte): Checks sequence continuity -/// -/// The 8-byte `upper_header` (UPPER-HEADER-BITS-TO-SHIFT = 64 bits) is the -/// second half of the SOME/IP header: `[request_id:4 BE, proto_ver:1, -/// iface_ver:1, msg_type:1, return_code:1]`. It must match exactly what the -/// sender included in its CRC computation, otherwise a `CrcError` is returned. -/// -/// # Arguments -/// * `config` - Profile 5 configuration (data ID, data length, max delta counter) -/// * `state` - Mutable state for counter tracking -/// * `protected` - The protected message (3-byte E2E header + payload) -/// * `upper_header` - 8-byte SOME/IP upper header included in the CRC -/// -/// # Returns -/// An [`E2ECheckResult`] containing the status, counter, and extracted payload. -pub fn check_profile5_with_header<'a>( - config: &Profile5Config, - state: &mut Profile5State, - protected: &'a [u8], - upper_header: [u8; 8], -) -> E2ECheckResult<'a> { - if protected.len() < PROFILE5_HEADER_SIZE { - return E2ECheckResult::error(E2ECheckStatus::BadArgument); - } - - let expected_total_length = PROFILE5_HEADER_SIZE + config.data_length as usize; - if protected.len() != expected_total_length { - crate::log::warn!( - "E2E Profile 5 length mismatch: expected {} bytes (3 header + {} payload), got {} bytes", - expected_total_length, - config.data_length, - protected.len() - ); - return E2ECheckResult::error(E2ECheckStatus::BadArgument); - } - - let received_crc = u16::from_le_bytes([protected[0], protected[1]]); - let counter = protected[2]; - let payload = &protected[PROFILE5_HEADER_SIZE..]; - - let computed_crc = compute_crc16_p5_with_header(config.data_id, counter, payload, upper_header); - if computed_crc != received_crc { - return E2ECheckResult::error(E2ECheckStatus::CrcError); - } - - let status = check_sequence_profile5(state, counter, config.max_delta_counter); - state.last_counter = Some(counter); - - E2ECheckResult::success(status, u32::from(counter), payload) -} - -/// Check sequence continuity for Profile 4 (16-bit counter). -fn check_sequence_profile4( - state: &Profile4State, - received_counter: u16, - max_delta: u16, -) -> E2ECheckStatus { - match state.last_counter { - None => { - // First message received - always Ok - E2ECheckStatus::Ok - } - Some(last_counter) => { - // Calculate delta with wraparound handling - let delta = received_counter.wrapping_sub(last_counter); - - if delta == 0 { - // Same counter value - repeated message - E2ECheckStatus::Repeated - } else if delta == 1 { - // Consecutive message - perfect - E2ECheckStatus::Ok - } else if delta <= max_delta { - // Some messages lost but within tolerance - E2ECheckStatus::OkSomeLost - } else { - // Too many messages lost or counter went backwards - E2ECheckStatus::WrongSequence - } - } - } -} - -/// Check sequence continuity for Profile 5 (8-bit counter). -fn check_sequence_profile5( - state: &Profile5State, - received_counter: u8, - max_delta: u8, -) -> E2ECheckStatus { - match state.last_counter { - None => { - // First message received - always Ok - E2ECheckStatus::Ok - } - Some(last_counter) => { - // Calculate delta with wraparound handling - let delta = received_counter.wrapping_sub(last_counter); - - if delta == 0 { - // Same counter value - repeated message - E2ECheckStatus::Repeated - } else if delta == 1 { - // Consecutive message - perfect - E2ECheckStatus::Ok - } else if delta <= max_delta { - // Some messages lost but within tolerance - E2ECheckStatus::OkSomeLost - } else { - // Too many messages lost or counter went backwards - E2ECheckStatus::WrongSequence - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::e2e::{protect_profile4, protect_profile5, protect_profile5_with_header}; - - #[test] - fn test_check_profile4_valid() { - let config = Profile4Config::new(0x1234_5678, 15); - let mut protect_state = Profile4State::new(); - let mut check_state = Profile4State::new(); - - let payload = b"Hello, World!"; - let mut buf = [0u8; 256]; - let len = protect_profile4(&config, &mut protect_state, payload, &mut buf).unwrap(); - let protected = &buf[..len]; - - let result = check_profile4(&config, &mut check_state, protected); - assert_eq!(result.status, E2ECheckStatus::Ok); - assert_eq!(result.counter, Some(0)); - assert_eq!(result.payload, Some(payload.as_slice())); - } - - #[test] - fn test_check_profile4_wrong_data_id() { - let config1 = Profile4Config::new(0x1234_5678, 15); - let config2 = Profile4Config::new(0xDEAD_BEEF, 15); - let mut protect_state = Profile4State::new(); - let mut check_state = Profile4State::new(); - - let payload = b"test"; - let mut buf = [0u8; 256]; - let len = protect_profile4(&config1, &mut protect_state, payload, &mut buf).unwrap(); - - // Check with different data_id - let result = check_profile4(&config2, &mut check_state, &buf[..len]); - assert_eq!(result.status, E2ECheckStatus::BadArgument); - } - - #[test] - fn test_check_profile4_corrupted_crc() { - let config = Profile4Config::new(0x1234_5678, 15); - let mut protect_state = Profile4State::new(); - let mut check_state = Profile4State::new(); - - let payload = b"test"; - let mut buf = [0u8; 256]; - let len = protect_profile4(&config, &mut protect_state, payload, &mut buf).unwrap(); - - // Corrupt CRC (bytes 8-11) - buf[8] ^= 0xFF; - - let result = check_profile4(&config, &mut check_state, &buf[..len]); - assert_eq!(result.status, E2ECheckStatus::CrcError); - } - - #[test] - fn test_check_profile4_corrupted_payload() { - let config = Profile4Config::new(0x1234_5678, 15); - let mut protect_state = Profile4State::new(); - let mut check_state = Profile4State::new(); - - let payload = b"test"; - let mut buf = [0u8; 256]; - let len = protect_profile4(&config, &mut protect_state, payload, &mut buf).unwrap(); - - // Corrupt payload - buf[12] ^= 0xFF; - - let result = check_profile4(&config, &mut check_state, &buf[..len]); - assert_eq!(result.status, E2ECheckStatus::CrcError); - } - - #[test] - fn test_check_profile4_wrong_length() { - let config = Profile4Config::new(0x1234_5678, 15); - let mut protect_state = Profile4State::new(); - let mut check_state = Profile4State::new(); - - let payload = b"test"; - let mut buf = [0u8; 256]; - protect_profile4(&config, &mut protect_state, payload, &mut buf).unwrap(); - - // Truncate message (header says 16 but we only pass 14) - let result = check_profile4(&config, &mut check_state, &buf[..14]); - assert_eq!(result.status, E2ECheckStatus::BadArgument); - } - - #[test] - fn test_check_profile4_too_short() { - let config = Profile4Config::new(0x1234_5678, 15); - let mut check_state = Profile4State::new(); - - let short = [0u8; 11]; // Less than 12-byte header - let result = check_profile4(&config, &mut check_state, &short); - assert_eq!(result.status, E2ECheckStatus::BadArgument); - } - - #[test] - fn test_check_profile5_valid() { - let config = Profile5Config::new(0x1234, 20, 15); - let mut protect_state = Profile5State::new(); - let mut check_state = Profile5State::new(); - - // Payload must be padded to data_length (20 bytes) for check_profile5 - let mut payload = [0u8; 20]; - payload[..13].copy_from_slice(b"Hello, World!"); - let mut buf = [0u8; 256]; - let len = protect_profile5(&config, &mut protect_state, &payload, &mut buf).unwrap(); - let protected = &buf[..len]; - - let result = check_profile5(&config, &mut check_state, protected); - assert_eq!(result.status, E2ECheckStatus::Ok); - assert_eq!(result.counter, Some(0)); - assert_eq!(result.payload, Some(payload.as_slice())); - } - - #[test] - fn test_check_profile5_corrupted_crc() { - let config = Profile5Config::new(0x1234, 20, 15); - let mut protect_state = Profile5State::new(); - let mut check_state = Profile5State::new(); - - let mut payload = [0u8; 20]; - payload[..4].copy_from_slice(b"test"); - let mut buf = [0u8; 256]; - let len = protect_profile5(&config, &mut protect_state, &payload, &mut buf).unwrap(); - - // Corrupt CRC (bytes 1-2) - buf[1] ^= 0xFF; - - let result = check_profile5(&config, &mut check_state, &buf[..len]); - assert_eq!(result.status, E2ECheckStatus::CrcError); - } - - #[test] - fn test_check_profile5_too_short() { - let config = Profile5Config::new(0x1234, 20, 15); - let mut check_state = Profile5State::new(); - - let short = [0u8; 2]; // Less than 3-byte header - let result = check_profile5(&config, &mut check_state, &short); - assert_eq!(result.status, E2ECheckStatus::BadArgument); - } - - #[test] - fn test_sequence_repeated() { - let config = Profile4Config::new(0x1234_5678, 15); - let mut protect_state = Profile4State::new(); - let mut check_state = Profile4State::new(); - - let payload = b"test"; - let mut buf = [0u8; 256]; - let len = protect_profile4(&config, &mut protect_state, payload, &mut buf).unwrap(); - let protected = &buf[..len]; - - // First check - let result1 = check_profile4(&config, &mut check_state, protected); - assert_eq!(result1.status, E2ECheckStatus::Ok); - - // Replay same message - let result2 = check_profile4(&config, &mut check_state, protected); - assert_eq!(result2.status, E2ECheckStatus::Repeated); - } - - #[test] - fn test_sequence_consecutive() { - let config = Profile4Config::new(0x1234_5678, 15); - let mut protect_state = Profile4State::new(); - let mut check_state = Profile4State::new(); - - let payload = b"test"; - let mut buf = [0u8; 256]; - - for _ in 0..5 { - let len = protect_profile4(&config, &mut protect_state, payload, &mut buf).unwrap(); - let result = check_profile4(&config, &mut check_state, &buf[..len]); - assert_eq!(result.status, E2ECheckStatus::Ok); - } - } - - #[test] - fn test_sequence_some_lost() { - let config = Profile4Config::new(0x1234_5678, 10); - let mut protect_state = Profile4State::new(); - let mut check_state = Profile4State::new(); - - let payload = b"test"; - let mut buf = [0u8; 256]; - - // First message - let len = protect_profile4(&config, &mut protect_state, payload, &mut buf).unwrap(); - let result1 = check_profile4(&config, &mut check_state, &buf[..len]); - assert_eq!(result1.status, E2ECheckStatus::Ok); - - // Skip some messages - for _ in 0..5 { - protect_profile4(&config, &mut protect_state, payload, &mut buf).unwrap(); - } - - // Check with gap of 6 (within max_delta of 10) - let len = protect_profile4(&config, &mut protect_state, payload, &mut buf).unwrap(); - let result2 = check_profile4(&config, &mut check_state, &buf[..len]); - assert_eq!(result2.status, E2ECheckStatus::OkSomeLost); - } - - #[test] - fn test_sequence_wrong_sequence() { - let config = Profile4Config::new(0x1234_5678, 3); - let mut protect_state = Profile4State::new(); - let mut check_state = Profile4State::new(); - - let payload = b"test"; - let mut buf = [0u8; 256]; - - // First message - let len = protect_profile4(&config, &mut protect_state, payload, &mut buf).unwrap(); - let result1 = check_profile4(&config, &mut check_state, &buf[..len]); - assert_eq!(result1.status, E2ECheckStatus::Ok); - - // Skip many messages - for _ in 0..10 { - protect_profile4(&config, &mut protect_state, payload, &mut buf).unwrap(); - } - - // Check with gap of 11 (exceeds max_delta of 3) - let len = protect_profile4(&config, &mut protect_state, payload, &mut buf).unwrap(); - let result2 = check_profile4(&config, &mut check_state, &buf[..len]); - assert_eq!(result2.status, E2ECheckStatus::WrongSequence); - } - - #[test] - fn test_sequence_wraparound() { - let config = Profile4Config::new(0x1234_5678, 5); - let mut protect_state = Profile4State::with_initial_counter(u16::MAX - 2); - let mut check_state = Profile4State::new(); - - let payload = b"test"; - let mut buf = [0u8; 256]; - - // Messages around counter wraparound - for _ in 0..5 { - let len = protect_profile4(&config, &mut protect_state, payload, &mut buf).unwrap(); - let result = check_profile4(&config, &mut check_state, &buf[..len]); - assert_eq!(result.status, E2ECheckStatus::Ok); - } - } - - #[test] - fn test_profile5_sequence_wraparound() { - let config = Profile5Config::new(0x1234, 20, 5); - let mut protect_state = Profile5State::with_initial_counter(u8::MAX - 2); - let mut check_state = Profile5State::new(); - - let mut payload = [0u8; 20]; - payload[..4].copy_from_slice(b"test"); - let mut buf = [0u8; 256]; - - // Messages around counter wraparound - for _ in 0..5 { - let len = protect_profile5(&config, &mut protect_state, &payload, &mut buf).unwrap(); - let result = check_profile5(&config, &mut check_state, &buf[..len]); - assert_eq!(result.status, E2ECheckStatus::Ok); - } - } - - #[test] - fn test_check_profile5_with_header_roundtrip() { - let config = Profile5Config::new(0x1234, 20, 15); - let mut protect_state = Profile5State::new(); - let mut check_state = Profile5State::new(); - - let upper_header: [u8; 8] = [0x00, 0x01, 0x00, 0x05, 0x01, 0x03, 0x02, 0x00]; - - let mut payload = [0u8; 20]; - payload[..5].copy_from_slice(b"Hello"); - - let mut buf = [0u8; 256]; - let len = protect_profile5_with_header( - &config, - &mut protect_state, - &payload, - upper_header, - &mut buf, - ) - .unwrap(); - let result = - check_profile5_with_header(&config, &mut check_state, &buf[..len], upper_header); - - assert_eq!(result.status, E2ECheckStatus::Ok); - assert_eq!(result.counter, Some(0)); - assert_eq!(result.payload, Some(payload.as_slice())); - } - - #[test] - fn test_check_profile5_length_mismatch() { - // Config expects data_length=20, so total = 3 + 20 = 23 bytes. - // Pass a buffer that's >= 3 bytes but != 23 to hit the length mismatch path. - let config = Profile5Config::new(0x1234, 20, 15); - let mut check_state = Profile5State::new(); - - let buf = [0u8; 10]; // 10 != 23 - let result = check_profile5(&config, &mut check_state, &buf); - assert_eq!(result.status, E2ECheckStatus::BadArgument); - } - - #[test] - fn test_check_profile5_with_header_too_short() { - let config = Profile5Config::new(0x1234, 20, 15); - let mut check_state = Profile5State::new(); - let upper_header: [u8; 8] = [0; 8]; - - let buf = [0u8; 2]; // Less than 3-byte header - let result = check_profile5_with_header(&config, &mut check_state, &buf, upper_header); - assert_eq!(result.status, E2ECheckStatus::BadArgument); - } - - #[test] - fn test_check_profile5_with_header_length_mismatch() { - let config = Profile5Config::new(0x1234, 20, 15); - let mut check_state = Profile5State::new(); - let upper_header: [u8; 8] = [0; 8]; - - let buf = [0u8; 10]; // >= 3 but != 23 - let result = check_profile5_with_header(&config, &mut check_state, &buf, upper_header); - assert_eq!(result.status, E2ECheckStatus::BadArgument); - } - - #[test] - fn test_profile5_sequence_some_lost() { - let config = Profile5Config::new(0x1234, 20, 10); - let mut protect_state = Profile5State::new(); - let mut check_state = Profile5State::new(); - - let mut payload = [0u8; 20]; - payload[..4].copy_from_slice(b"test"); - let mut buf = [0u8; 256]; - - // First message - let len = protect_profile5(&config, &mut protect_state, &payload, &mut buf).unwrap(); - let result = check_profile5(&config, &mut check_state, &buf[..len]); - assert_eq!(result.status, E2ECheckStatus::Ok); - - // Skip 5 messages - for _ in 0..5 { - protect_profile5(&config, &mut protect_state, &payload, &mut buf).unwrap(); - } - - // Check with gap of 6 (within max_delta of 10) - let len = protect_profile5(&config, &mut protect_state, &payload, &mut buf).unwrap(); - let result = check_profile5(&config, &mut check_state, &buf[..len]); - assert_eq!(result.status, E2ECheckStatus::OkSomeLost); - } - - #[test] - fn test_profile5_sequence_wrong_sequence() { - let config = Profile5Config::new(0x1234, 20, 3); - let mut protect_state = Profile5State::new(); - let mut check_state = Profile5State::new(); - - let mut payload = [0u8; 20]; - payload[..4].copy_from_slice(b"test"); - let mut buf = [0u8; 256]; - - // First message - let len = protect_profile5(&config, &mut protect_state, &payload, &mut buf).unwrap(); - let result = check_profile5(&config, &mut check_state, &buf[..len]); - assert_eq!(result.status, E2ECheckStatus::Ok); - - // Skip 10 messages (exceeds max_delta of 3) - for _ in 0..10 { - protect_profile5(&config, &mut protect_state, &payload, &mut buf).unwrap(); - } - - let len = protect_profile5(&config, &mut protect_state, &payload, &mut buf).unwrap(); - let result = check_profile5(&config, &mut check_state, &buf[..len]); - assert_eq!(result.status, E2ECheckStatus::WrongSequence); - } - - #[test] - fn test_profile5_sequence_repeated() { - let config = Profile5Config::new(0x1234, 20, 15); - let mut protect_state = Profile5State::new(); - let mut check_state = Profile5State::new(); - - let mut payload = [0u8; 20]; - payload[..4].copy_from_slice(b"test"); - let mut buf = [0u8; 256]; - - let len = protect_profile5(&config, &mut protect_state, &payload, &mut buf).unwrap(); - - // First check - let result = check_profile5(&config, &mut check_state, &buf[..len]); - assert_eq!(result.status, E2ECheckStatus::Ok); - - // Replay same message - let result = check_profile5(&config, &mut check_state, &buf[..len]); - assert_eq!(result.status, E2ECheckStatus::Repeated); - } - - #[test] - fn test_check_profile5_with_header_mismatch_is_crc_error() { - let config = Profile5Config::new(0x1234, 20, 15); - let mut protect_state = Profile5State::new(); - let mut check_state = Profile5State::new(); - - let tx_header: [u8; 8] = [0x00, 0x01, 0x00, 0x05, 0x01, 0x03, 0x02, 0x00]; - let rx_header: [u8; 8] = [0x00, 0x02, 0x00, 0x05, 0x01, 0x03, 0x02, 0x00]; - - let mut payload = [0u8; 20]; - payload[..5].copy_from_slice(b"Hello"); - - let mut buf = [0u8; 256]; - let len = protect_profile5_with_header( - &config, - &mut protect_state, - &payload, - tx_header, - &mut buf, - ) - .unwrap(); - let result = check_profile5_with_header(&config, &mut check_state, &buf[..len], rx_header); - - assert_eq!(result.status, E2ECheckStatus::CrcError); - } -} diff --git a/src/e2e/e2e_protector.rs b/src/e2e/e2e_protector.rs deleted file mode 100644 index 9a3d48d..0000000 --- a/src/e2e/e2e_protector.rs +++ /dev/null @@ -1,490 +0,0 @@ -//! E2E protection functions for adding E2E headers to payloads. - -use super::Error; -use super::config::{Profile4Config, Profile5Config}; -use super::crc::{compute_crc16_p5, compute_crc16_p5_with_header, compute_crc32_p4}; -use super::state::{Profile4State, Profile5State}; - -/// Profile 4 header size in bytes. -pub const PROFILE4_HEADER_SIZE: usize = 12; - -/// Profile 5 header size in bytes. -pub const PROFILE5_HEADER_SIZE: usize = 3; - -/// Add E2E Profile 4 protection to a payload. -/// -/// Writes a protected message into `output` with a 12-byte header prepended: -/// - Length (2 bytes): Total length including header -/// - Counter (2 bytes): Sequence counter from state -/// - `DataID` (4 bytes): From configuration -/// - CRC (4 bytes): CRC-32P4 over Length + Counter + `DataID` + Payload -/// -/// The state counter is incremented after each call. -/// -/// # Arguments -/// * `config` - Profile 4 configuration -/// * `state` - Mutable state for counter tracking -/// * `payload` - The payload data to protect -/// * `output` - Buffer to write the protected message into; must be at least -/// `PROFILE4_HEADER_SIZE + payload.len()` bytes -/// -/// # Returns -/// The number of bytes written to `output`, or an error if the buffer is too -/// small. -/// -/// # Errors -/// Returns [`Error::BufferTooSmall`] if `output` is too small to hold the -/// protected message, or if the total message length (header + payload) -/// exceeds 65 535 bytes (the Profile 4 length field is `u16`). -pub fn protect_profile4( - config: &Profile4Config, - state: &mut Profile4State, - payload: &[u8], - output: &mut [u8], -) -> Result { - let total_length = PROFILE4_HEADER_SIZE + payload.len(); - - if output.len() < total_length { - return Err(Error::BufferTooSmall { - needed: total_length, - actual: output.len(), - }); - } - - // Profile 4 length field is u16; if total_length > u16::MAX the buffer - // requirement already exceeds what the protocol can encode, so report it - // as a buffer-too-small error (needed would be > 65535). - let length = u16::try_from(total_length).map_err(|_| Error::BufferTooSmall { - needed: total_length, - actual: output.len(), - })?; - - let counter = state.protect_counter; - - // Compute CRC over: Length + Counter + DataID + Payload - let crc = compute_crc32_p4(length, counter, config.data_id, payload); - - // Header: Length (2) + Counter (2) + DataID (4) + CRC (4) - output[0..2].copy_from_slice(&length.to_be_bytes()); - output[2..4].copy_from_slice(&counter.to_be_bytes()); - output[4..8].copy_from_slice(&config.data_id.to_be_bytes()); - output[8..12].copy_from_slice(&crc.to_be_bytes()); - - // Payload - output[PROFILE4_HEADER_SIZE..total_length].copy_from_slice(payload); - - // Increment counter (wraps at u16::MAX) - state.protect_counter = state.protect_counter.wrapping_add(1); - - Ok(total_length) -} - -/// Add E2E Profile 5 protection to a payload. -/// -/// Writes a protected message into `output` with a 3-byte header prepended: -/// - CRC (2 bytes, little-endian): CRC-16-CCITT over Counter + Payload + DataID(LE) -/// - Counter (1 byte): Sequence counter from state -/// -/// The state counter is incremented after each call. -/// -/// # Arguments -/// * `config` - Profile 5 configuration -/// * `state` - Mutable state for counter tracking -/// * `payload` - The payload data to protect -/// * `output` - Buffer to write the protected message into; must be at least -/// `PROFILE5_HEADER_SIZE + payload.len()` bytes -/// -/// # Returns -/// The number of bytes written to `output`, or an error if the buffer is too -/// small. -/// -/// # Errors -/// Returns [`Error::BufferTooSmall`] if `output` is too small to hold the -/// protected message. -pub fn protect_profile5( - config: &Profile5Config, - state: &mut Profile5State, - payload: &[u8], - output: &mut [u8], -) -> Result { - let total_length = PROFILE5_HEADER_SIZE + payload.len(); - - if output.len() < total_length { - return Err(Error::BufferTooSmall { - needed: total_length, - actual: output.len(), - }); - } - - let counter = state.protect_counter; - - // Compute CRC over: Counter + Payload + DataID (LE) - let crc = compute_crc16_p5(config.data_id, counter, payload); - - // Header: CRC (2, little-endian) + Counter (1) - output[0..2].copy_from_slice(&crc.to_le_bytes()); - output[2] = counter; - - // Payload - output[PROFILE5_HEADER_SIZE..total_length].copy_from_slice(payload); - - // Increment counter (wraps at u8::MAX) - state.protect_counter = state.protect_counter.wrapping_add(1); - - Ok(total_length) -} - -/// Add E2E Profile 5 protection with SOME/IP upper-header in the CRC. -/// -/// Creates a protected message with a 3-byte header prepended: -/// - CRC (2 bytes, little-endian): CRC-16-CCITT over -/// `upper_header(8) + Counter(1) + Payload(N) + DataID(2 LE)` -/// - Counter (1 byte): Sequence counter from state -/// -/// The 8-byte `upper_header` (UPPER-HEADER-BITS-TO-SHIFT = 64 bits) is the -/// second half of the SOME/IP header: `[request_id:4 BE, proto_ver:1, -/// iface_ver:1, msg_type:1, return_code:1]`. The state counter is incremented -/// after each call. -/// -/// # Arguments -/// * `config` - Profile 5 configuration (data ID, data length, max delta counter) -/// * `state` - Mutable state for counter tracking -/// * `payload` - The payload data to protect -/// * `upper_header` - 8-byte SOME/IP upper header included in the CRC -/// -/// # Returns -/// The number of bytes written to `output`, or an error if the buffer is too -/// small. -/// -/// # Errors -/// Returns [`Error::BufferTooSmall`] if `output` is too small to hold the -/// protected message. -pub fn protect_profile5_with_header( - config: &Profile5Config, - state: &mut Profile5State, - payload: &[u8], - upper_header: [u8; 8], - output: &mut [u8], -) -> Result { - let total_length = PROFILE5_HEADER_SIZE + payload.len(); - - if output.len() < total_length { - return Err(Error::BufferTooSmall { - needed: total_length, - actual: output.len(), - }); - } - - let counter = state.protect_counter; - let crc = compute_crc16_p5_with_header(config.data_id, counter, payload, upper_header); - - // Header: CRC (2, little-endian) + Counter (1) - output[0..2].copy_from_slice(&crc.to_le_bytes()); - output[2] = counter; - - // Payload - output[PROFILE5_HEADER_SIZE..total_length].copy_from_slice(payload); - - state.protect_counter = state.protect_counter.wrapping_add(1); - - Ok(total_length) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_protect_profile4_header_format() { - let config = Profile4Config::new(0x1234_5678, 15); - let mut state = Profile4State::new(); - - let payload = b"test"; - let mut buf = [0u8; 256]; - let len = protect_profile4(&config, &mut state, payload, &mut buf).unwrap(); - let protected = &buf[..len]; - - // Check total length - assert_eq!(len, 12 + 4); // header + payload - - // Check length field (first 2 bytes) - let length = u16::from_be_bytes([protected[0], protected[1]]); - assert_eq!(length, 16); // 12 + 4 - - // Check counter field (bytes 2-3) - let counter = u16::from_be_bytes([protected[2], protected[3]]); - assert_eq!(counter, 0); - - // Check data_id field (bytes 4-7) - let data_id = u32::from_be_bytes([protected[4], protected[5], protected[6], protected[7]]); - assert_eq!(data_id, 0x1234_5678); - - // Check payload at end - assert_eq!(&protected[12..], b"test"); - } - - #[test] - fn test_protect_profile4_counter_increment() { - let config = Profile4Config::new(0x1234_5678, 15); - let mut state = Profile4State::new(); - - let payload = b"test"; - let mut buf = [0u8; 256]; - - for i in 0..5 { - let len = protect_profile4(&config, &mut state, payload, &mut buf).unwrap(); - let counter = u16::from_be_bytes([buf[2], buf[3]]); - assert_eq!(counter, i); - assert_eq!(len, 16); - } - } - - #[test] - fn test_protect_profile4_counter_wraps() { - let config = Profile4Config::new(0x1234_5678, 15); - let mut state = Profile4State::with_initial_counter(u16::MAX); - - let payload = b"test"; - let mut buf = [0u8; 256]; - - protect_profile4(&config, &mut state, payload, &mut buf).unwrap(); - let counter1 = u16::from_be_bytes([buf[2], buf[3]]); - assert_eq!(counter1, u16::MAX); - - protect_profile4(&config, &mut state, payload, &mut buf).unwrap(); - let counter2 = u16::from_be_bytes([buf[2], buf[3]]); - assert_eq!(counter2, 0); // Wrapped - } - - #[test] - fn test_protect_profile5_header_format() { - let config = Profile5Config::new(0x1234, 20, 15); - let mut state = Profile5State::new(); - - let payload = b"test"; - let mut buf = [0u8; 256]; - let len = protect_profile5(&config, &mut state, payload, &mut buf).unwrap(); - let protected = &buf[..len]; - - // Check total length - assert_eq!(len, 3 + 4); // header + payload - - // Header layout: [CRC_lo, CRC_hi, Counter] - // Check counter field (third byte) - assert_eq!(protected[2], 0); - - // Check payload at end - assert_eq!(&protected[3..], b"test"); - } - - #[test] - fn test_protect_profile5_counter_increment() { - let config = Profile5Config::new(0x1234, 20, 15); - let mut state = Profile5State::new(); - - let payload = b"test"; - let mut buf = [0u8; 256]; - - for i in 0..5u8 { - protect_profile5(&config, &mut state, payload, &mut buf).unwrap(); - assert_eq!(buf[2], i); // Counter is at byte 2 - } - } - - #[test] - fn test_protect_profile5_counter_wraps() { - let config = Profile5Config::new(0x1234, 20, 15); - let mut state = Profile5State::with_initial_counter(u8::MAX); - - let payload = b"test"; - let mut buf = [0u8; 256]; - - protect_profile5(&config, &mut state, payload, &mut buf).unwrap(); - assert_eq!(buf[2], u8::MAX); // Counter is at byte 2 - - protect_profile5(&config, &mut state, payload, &mut buf).unwrap(); - assert_eq!(buf[2], 0); // Wrapped - } - - #[test] - fn test_protect_profile5_with_header_format() { - let config = Profile5Config::new(0x1234, 20, 15); - let mut state = Profile5State::new(); - - let payload = b"test"; - let upper_header: [u8; 8] = [0x00, 0x01, 0x00, 0x05, 0x01, 0x03, 0x02, 0x00]; - let mut buf = [0u8; 256]; - let len = - protect_profile5_with_header(&config, &mut state, payload, upper_header, &mut buf) - .unwrap(); - let protected = &buf[..len]; - - // Check total length - assert_eq!(len, 3 + 4); // header + payload - - // Check counter field (third byte) - assert_eq!(protected[2], 0); - - // Check payload at end - assert_eq!(&protected[3..], b"test"); - } - - #[test] - fn test_protect_profile5_with_header_counter_increment() { - let config = Profile5Config::new(0x1234, 20, 15); - let mut state = Profile5State::new(); - - let payload = b"test"; - let upper_header: [u8; 8] = [0x00, 0x01, 0x00, 0x05, 0x01, 0x03, 0x02, 0x00]; - let mut buf = [0u8; 256]; - - for i in 0..5u8 { - protect_profile5_with_header(&config, &mut state, payload, upper_header, &mut buf) - .unwrap(); - assert_eq!(buf[2], i); - } - } - - #[test] - fn test_protect_profile5_with_header_counter_wraps() { - let config = Profile5Config::new(0x1234, 20, 15); - let mut state = Profile5State::with_initial_counter(u8::MAX); - - let payload = b"test"; - let upper_header: [u8; 8] = [0x00, 0x01, 0x00, 0x05, 0x01, 0x03, 0x02, 0x00]; - let mut buf = [0u8; 256]; - - protect_profile5_with_header(&config, &mut state, payload, upper_header, &mut buf).unwrap(); - assert_eq!(buf[2], u8::MAX); - - protect_profile5_with_header(&config, &mut state, payload, upper_header, &mut buf).unwrap(); - assert_eq!(buf[2], 0); // Wrapped - } - - #[test] - fn test_protect_profile5_with_header_empty_payload() { - let config = Profile5Config::new(0x1234, 3, 15); - let mut state = Profile5State::new(); - - let upper_header: [u8; 8] = [0x00, 0x01, 0x00, 0x05, 0x01, 0x03, 0x02, 0x00]; - let mut buf = [0u8; 256]; - let len = - protect_profile5_with_header(&config, &mut state, b"", upper_header, &mut buf).unwrap(); - assert_eq!(len, 3); // Just header - } - - #[test] - fn test_protect_profile5_with_header_differs_from_no_header() { - let config = Profile5Config::new(0x1234, 20, 15); - let mut state_a = Profile5State::new(); - let mut state_b = Profile5State::new(); - - let payload = b"test"; - let upper_header: [u8; 8] = [0x00, 0x01, 0x00, 0x05, 0x01, 0x03, 0x02, 0x00]; - - let mut buf_a = [0u8; 256]; - let len_a = protect_profile5(&config, &mut state_a, payload, &mut buf_a).unwrap(); - let without_header_crc = u16::from_le_bytes([buf_a[0], buf_a[1]]); - - let mut buf_b = [0u8; 256]; - let len_b = - protect_profile5_with_header(&config, &mut state_b, payload, upper_header, &mut buf_b) - .unwrap(); - let with_header_crc = u16::from_le_bytes([buf_b[0], buf_b[1]]); - - // Same counter and payload but different CRC due to upper_header - assert_eq!(buf_a[2], buf_b[2]); // same counter - assert_eq!(&buf_a[3..len_a], &buf_b[3..len_b]); // same payload - assert_ne!(without_header_crc, with_header_crc); // different CRC - } - - #[test] - fn test_protect_profile4_buffer_too_small() { - let config = Profile4Config::new(0x1234_5678, 15); - let mut state = Profile4State::new(); - - let payload = b"test"; - // Need 12 (header) + 4 (payload) = 16 bytes, provide only 10 - let mut buf = [0u8; 10]; - let err = protect_profile4(&config, &mut state, payload, &mut buf).unwrap_err(); - assert!(matches!( - err, - crate::e2e::Error::BufferTooSmall { - needed: 16, - actual: 10, - } - )); - } - - #[test] - fn test_protect_profile5_buffer_too_small() { - let config = Profile5Config::new(0x1234, 20, 15); - let mut state = Profile5State::new(); - - let payload = b"test"; - // Need 3 (header) + 4 (payload) = 7 bytes, provide only 5 - let mut buf = [0u8; 5]; - let err = protect_profile5(&config, &mut state, payload, &mut buf).unwrap_err(); - assert!(matches!( - err, - crate::e2e::Error::BufferTooSmall { - needed: 7, - actual: 5, - } - )); - } - - #[test] - fn test_protect_profile5_with_header_buffer_too_small() { - let config = Profile5Config::new(0x1234, 20, 15); - let mut state = Profile5State::new(); - - let payload = b"test"; - let upper_header: [u8; 8] = [0x00, 0x01, 0x00, 0x05, 0x01, 0x03, 0x02, 0x00]; - // Need 3 (header) + 4 (payload) = 7 bytes, provide only 5 - let mut buf = [0u8; 5]; - let err = - protect_profile5_with_header(&config, &mut state, payload, upper_header, &mut buf) - .unwrap_err(); - assert!(matches!( - err, - crate::e2e::Error::BufferTooSmall { - needed: 7, - actual: 5, - } - )); - } - - #[test] - #[cfg(feature = "std")] - fn test_protect_profile4_length_overflow() { - let config = Profile4Config::new(0x1234_5678, 15); - let mut state = Profile4State::new(); - - // payload of 65536 bytes => total = 12 + 65536 = 65548 > u16::MAX - let payload = std::vec![0u8; 65536]; - let mut buf = std::vec![0u8; 65536 + 12]; - let err = protect_profile4(&config, &mut state, &payload, &mut buf).unwrap_err(); - assert!(matches!(err, crate::e2e::Error::BufferTooSmall { .. })); - } - - #[test] - fn test_protect_profile4_empty_payload() { - let config = Profile4Config::new(0x1234_5678, 15); - let mut state = Profile4State::new(); - - let mut buf = [0u8; 256]; - let len = protect_profile4(&config, &mut state, b"", &mut buf).unwrap(); - assert_eq!(len, 12); // Just header - } - - #[test] - fn test_protect_profile5_empty_payload() { - let config = Profile5Config::new(0x1234, 3, 15); - let mut state = Profile5State::new(); - - let mut buf = [0u8; 256]; - let len = protect_profile5(&config, &mut state, b"", &mut buf).unwrap(); - assert_eq!(len, 3); // Just header - } -} diff --git a/src/e2e/error.rs b/src/e2e/error.rs index 7a804e8..64e1967 100644 --- a/src/e2e/error.rs +++ b/src/e2e/error.rs @@ -13,3 +13,23 @@ pub enum Error { actual: usize, }, } + +impl From for Error { + fn from(err: simple_e2e::profile4::ProtectError) -> Self { + match err { + simple_e2e::profile4::ProtectError::BufferTooSmall { needed, actual } => { + Self::BufferTooSmall { needed, actual } + } + } + } +} + +impl From for Error { + fn from(err: simple_e2e::profile5::ProtectError) -> Self { + match err { + simple_e2e::profile5::ProtectError::BufferTooSmall { needed, actual } => { + Self::BufferTooSmall { needed, actual } + } + } + } +} diff --git a/src/e2e/mod.rs b/src/e2e/mod.rs index 7196a3f..6c4fa48 100644 --- a/src/e2e/mod.rs +++ b/src/e2e/mod.rs @@ -1,7 +1,10 @@ //! E2E (End-to-End) protection for SOME/IP payloads. //! -//! This module implements E2E Profile 4 and Profile 5 protection as specified -//! in the [Open SOME/IP Specification](https://github.com/some-ip-com/open-someip-spec). +//! The Profile 4 and Profile 5 algorithms live in [`simple_e2e`]; this module +//! adds the SOME/IP glue: [`E2EKey`] (service + method/event), [`E2EProfile`] +//! (which profile a key uses, and whether the SOME/IP upper header is folded +//! into the Profile 5 CRC), the per-source [`E2ERegistry`](crate::e2e::E2ERegistry), and the mapping +//! from [`E2ECheckStatus`] to the wire return code. //! //! # Example //! @@ -24,55 +27,208 @@ //! assert!(matches!(result.status, E2ECheckStatus::Ok)); //! ``` -mod config; -mod crc; -mod e2e_checker; -mod e2e_protector; mod error; mod registry; -mod state; - -pub use config::{Profile4Config, Profile5Config}; -pub use e2e_checker::{check_profile4, check_profile5, check_profile5_with_header}; -pub use e2e_protector::{ - PROFILE4_HEADER_SIZE, PROFILE5_HEADER_SIZE, protect_profile4, protect_profile5, - protect_profile5_with_header, -}; + pub use error::Error; pub use registry::{E2E_REGISTRY_CAP, E2E_RX_STATE_CAP, E2ERegistry, E2ERegistryFull}; -pub use state::{Profile4State, Profile5State}; + +/// Profile 4 configuration (`data_id`, `max_delta_counter`). +pub type Profile4Config = simple_e2e::profile4::Config; +/// Profile 4 per-direction counter state. +pub type Profile4State = simple_e2e::profile4::State; +/// Profile 5 configuration (`data_id`, `data_length`, `max_delta_counter`). +pub type Profile5Config = simple_e2e::profile5::Config; +/// Profile 5 per-direction counter state. +pub type Profile5State = simple_e2e::profile5::State; +/// Profile 4 header length in bytes (length, counter, data ID, CRC-32). +pub const PROFILE4_HEADER_SIZE: usize = simple_e2e::profile4::HEADER_SIZE; +/// Profile 5 header length in bytes (CRC-16, counter). +pub const PROFILE5_HEADER_SIZE: usize = simple_e2e::profile5::HEADER_SIZE; + +/// Check a Profile 4 protected message, tracking the counter in `state`. +pub fn check_profile4<'a>( + config: &Profile4Config, + state: &mut Profile4State, + protected: &'a [u8], +) -> E2ECheckResult<'a> { + let result = simple_e2e::profile4::check(config, state, protected); + E2ECheckResult { + status: result.status.into(), + counter: result.counter.map(u32::from), + payload: result.payload, + } +} + +/// Check a Profile 5 protected message, tracking the counter in `state`. +pub fn check_profile5<'a>( + config: &Profile5Config, + state: &mut Profile5State, + protected: &'a [u8], +) -> E2ECheckResult<'a> { + let result = simple_e2e::profile5::check(config, state, protected); + E2ECheckResult { + status: result.status.into(), + counter: result.counter.map(u32::from), + payload: result.payload, + } +} + +/// Check a Profile 5 protected message whose CRC also covers the 8-byte +/// SOME/IP upper header. +pub fn check_profile5_with_header<'a>( + config: &Profile5Config, + state: &mut Profile5State, + protected: &'a [u8], + upper_header: [u8; 8], +) -> E2ECheckResult<'a> { + let result = simple_e2e::profile5::check_with_header(config, state, protected, upper_header); + E2ECheckResult { + status: result.status.into(), + counter: result.counter.map(u32::from), + payload: result.payload, + } +} + +/// Protect `payload` with a Profile 4 header into `output`; returns the +/// number of bytes written. +/// +/// # Errors +/// [`Error::BufferTooSmall`] if `output` cannot hold header + payload. +pub fn protect_profile4( + config: &Profile4Config, + state: &mut Profile4State, + payload: &[u8], + output: &mut [u8], +) -> Result { + simple_e2e::profile4::protect(config, state, payload, output).map_err(Error::from) +} + +/// Protect `payload` with a Profile 5 header into `output`; returns the +/// number of bytes written. +/// +/// # Errors +/// [`Error::BufferTooSmall`] if `output` cannot hold header + payload. +pub fn protect_profile5( + config: &Profile5Config, + state: &mut Profile5State, + payload: &[u8], + output: &mut [u8], +) -> Result { + simple_e2e::profile5::protect(config, state, payload, output).map_err(Error::from) +} + +/// Protect `payload` with a Profile 5 header whose CRC also covers the +/// 8-byte SOME/IP upper header. +/// +/// # Errors +/// [`Error::BufferTooSmall`] if `output` cannot hold header + payload. +pub fn protect_profile5_with_header( + config: &Profile5Config, + state: &mut Profile5State, + payload: &[u8], + upper_header: [u8; 8], + output: &mut [u8], +) -> Result { + simple_e2e::profile5::protect_with_header(config, state, payload, upper_header, output) + .map_err(Error::from) +} /// Status result from E2E check operations. +/// +/// `Unchecked` is a registry-level outcome (no profile is registered for the +/// message's [`E2EKey`]); the remaining variants come from the profile +/// algorithm in `simple_e2e`. A wire-level rejection carries the concrete +/// [`E2EValidateError`], so a CRC mismatch reports the received and computed +/// values instead of a bare tag. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum E2ECheckStatus { - /// Initial state, no check performed yet. + /// No E2E profile is registered for this message; nothing was checked. Unchecked, - /// Check passed successfully. + /// Valid CRC and the counter advanced by exactly one (or first message). Ok, - /// CRC verification failed. - CrcError, - /// Counter value is repeated (same as last received). - Repeated, - /// Check passed but some messages were lost (counter gap within tolerance). + /// Valid CRC; some messages were lost, within `max_delta_counter`. OkSomeLost, - /// Counter sequence error (gap exceeds `max_delta_counter`). + /// Valid CRC; the counter did not advance (duplicate). + Repeated, + /// Valid CRC; the counter jumped past `max_delta_counter`. WrongSequence, - /// Invalid input arguments (e.g., message too short). - BadArgument, + /// The message failed wire-level validation (length, data ID, or CRC). + Invalid(E2EValidateError), +} + +/// Wire-level validation failure, tagged with the profile that produced it. +/// +/// Profile 4 uses a CRC-32 and Profile 5 a CRC-16, so the two error types +/// differ in width; this enum keeps each profile's real values rather than +/// widening them into a lossy common shape. +#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] +#[non_exhaustive] +pub enum E2EValidateError { + /// Profile 4 (CRC-32) validation failure. + #[error("profile 4: {0}")] + Profile4(simple_e2e::profile4::ValidateError), + /// Profile 5 (CRC-16) validation failure. + #[error("profile 5: {0}")] + Profile5(simple_e2e::profile5::ValidateError), +} + +impl E2EValidateError { + /// `true` when the failure is a CRC mismatch (as opposed to a length or + /// data-ID problem). Selects the wire return code in + /// [`E2ECheckStatus::to_return_code`]. + #[must_use] + pub const fn is_crc_mismatch(self) -> bool { + matches!( + self, + Self::Profile4(simple_e2e::profile4::ValidateError::CrcMismatch { .. }) + | Self::Profile5(simple_e2e::profile5::ValidateError::CrcMismatch { .. }) + ) + } } impl E2ECheckStatus { - /// Convert to a numeric return code compatible with E2E. + /// Convert to a numeric return code. + /// + /// The codes are the pre-0.13 values: a CRC mismatch is `2` (formerly + /// `CrcError`) and every other validation failure is `6` (formerly + /// `BadArgument`), so the wire mapping is unchanged. #[must_use] pub fn to_return_code(self) -> u8 { match self { E2ECheckStatus::Unchecked => 0, E2ECheckStatus::Ok => 1, - E2ECheckStatus::CrcError => 2, + E2ECheckStatus::Invalid(err) if err.is_crc_mismatch() => 2, E2ECheckStatus::Repeated => 3, E2ECheckStatus::OkSomeLost => 4, E2ECheckStatus::WrongSequence => 5, - E2ECheckStatus::BadArgument => 6, + E2ECheckStatus::Invalid(_) => 6, + } + } +} + +impl From for E2ECheckStatus { + fn from(status: simple_e2e::profile4::CheckStatus) -> Self { + use simple_e2e::profile4::CheckStatus as S; + match status { + S::Ok => Self::Ok, + S::OkSomeLost => Self::OkSomeLost, + S::Repeated => Self::Repeated, + S::WrongSequence => Self::WrongSequence, + S::Invalid(err) => Self::Invalid(E2EValidateError::Profile4(err)), + } + } +} + +impl From for E2ECheckStatus { + fn from(status: simple_e2e::profile5::CheckStatus) -> Self { + use simple_e2e::profile5::CheckStatus as S; + match status { + S::Ok => Self::Ok, + S::OkSomeLost => Self::OkSomeLost, + S::Repeated => Self::Repeated, + S::WrongSequence => Self::WrongSequence, + S::Invalid(err) => Self::Invalid(E2EValidateError::Profile5(err)), } } } @@ -91,23 +247,7 @@ pub struct E2ECheckResult<'a> { pub payload: Option<&'a [u8]>, } -impl<'a> E2ECheckResult<'a> { - pub(crate) fn error(status: E2ECheckStatus) -> Self { - Self { - status, - counter: None, - payload: None, - } - } - - pub(crate) fn success(status: E2ECheckStatus, counter: u32, payload: &'a [u8]) -> Self { - Self { - status, - counter: Some(counter), - payload: Some(payload), - } - } - +impl E2ECheckResult<'_> { /// Copy the extracted payload into an owned `Vec`. /// /// Returns `None` if the check did not produce a payload (e.g. on error). @@ -196,7 +336,7 @@ pub(crate) fn e2e_check<'a>( (E2EProfile::Profile5WithHeader(config), E2EState::Profile5(st)) => { check_profile5_with_header(config, st, payload, upper_header) } - _ => return (E2ECheckStatus::BadArgument, payload), + _ => unreachable!("E2EState is always created from E2EProfile"), }; let stripped = result.payload.unwrap_or(payload); (result.status, stripped) @@ -232,15 +372,75 @@ pub(crate) fn e2e_protect( mod tests { use super::*; + use simple_e2e::{profile4, profile5}; + #[test] fn test_status_return_codes() { assert_eq!(E2ECheckStatus::Unchecked.to_return_code(), 0); assert_eq!(E2ECheckStatus::Ok.to_return_code(), 1); - assert_eq!(E2ECheckStatus::CrcError.to_return_code(), 2); + let p5_crc = E2ECheckStatus::Invalid(E2EValidateError::Profile5( + profile5::ValidateError::CrcMismatch { + got: 1, + expected: 2, + }, + )); + assert_eq!(p5_crc.to_return_code(), 2); + let p4_crc = E2ECheckStatus::Invalid(E2EValidateError::Profile4( + profile4::ValidateError::CrcMismatch { + got: 1, + expected: 2, + }, + )); + assert_eq!(p4_crc.to_return_code(), 2); assert_eq!(E2ECheckStatus::Repeated.to_return_code(), 3); assert_eq!(E2ECheckStatus::OkSomeLost.to_return_code(), 4); assert_eq!(E2ECheckStatus::WrongSequence.to_return_code(), 5); - assert_eq!(E2ECheckStatus::BadArgument.to_return_code(), 6); + // Every non-CRC validation failure is the wire's "bad argument". + for err in [ + E2EValidateError::Profile5(profile5::ValidateError::TooShort { actual: 1 }), + E2EValidateError::Profile5(profile5::ValidateError::LengthMismatch { + expected: 4, + actual: 3, + }), + E2EValidateError::Profile4(profile4::ValidateError::TooShort { actual: 1 }), + E2EValidateError::Profile4(profile4::ValidateError::LengthMismatch { + header_length: 4, + actual: 3, + }), + E2EValidateError::Profile4(profile4::ValidateError::DataIdMismatch { + got: 1, + expected: 2, + }), + ] { + assert_eq!(E2ECheckStatus::Invalid(err).to_return_code(), 6, "{err:?}"); + } + } + + #[test] + fn test_check_status_from_profile_status() { + assert_eq!( + E2ECheckStatus::from(profile4::CheckStatus::OkSomeLost), + E2ECheckStatus::OkSomeLost + ); + assert_eq!( + E2ECheckStatus::from(profile5::CheckStatus::WrongSequence), + E2ECheckStatus::WrongSequence + ); + let e = profile5::ValidateError::CrcMismatch { + got: 0xBEEF, + expected: 0xCAFE, + }; + assert_eq!( + E2ECheckStatus::from(profile5::CheckStatus::Invalid(e)), + E2ECheckStatus::Invalid(E2EValidateError::Profile5(e)) + ); + } + + #[test] + fn test_check_status_is_copy() { + fn assert_copy() {} + assert_copy::(); + assert_copy::(); } #[test] @@ -374,7 +574,18 @@ mod tests { buf[8] ^= 0xFF; let result = check_profile4(&config, &mut check_state, &buf[..len]); - assert_eq!(result.status, E2ECheckStatus::CrcError); + let E2ECheckStatus::Invalid(E2EValidateError::Profile4( + profile4::ValidateError::CrcMismatch { got, expected }, + )) = result.status + else { + panic!("expected a Profile 4 CRC mismatch, got {:?}", result.status); + }; + // Profile 4 stores the CRC big-endian at bytes 8..12; we flipped byte 8. + assert_eq!(got, u32::from_be_bytes([buf[8], buf[9], buf[10], buf[11]])); + assert_eq!( + expected, + u32::from_be_bytes([buf[8] ^ 0xFF, buf[9], buf[10], buf[11]]) + ); } #[test] @@ -392,7 +603,15 @@ mod tests { buf[1] ^= 0xFF; let result = check_profile5(&config, &mut check_state, &buf[..len]); - assert_eq!(result.status, E2ECheckStatus::CrcError); + let E2ECheckStatus::Invalid(E2EValidateError::Profile5( + profile5::ValidateError::CrcMismatch { got, expected }, + )) = result.status + else { + panic!("expected a Profile 5 CRC mismatch, got {:?}", result.status); + }; + // Profile 5 stores the CRC little-endian at bytes 0..2; we flipped byte 1. + assert_eq!(got, u16::from_le_bytes([buf[0], buf[1]])); + assert_eq!(expected, u16::from_le_bytes([buf[0], buf[1] ^ 0xFF])); } #[test] @@ -403,7 +622,12 @@ mod tests { // Message too short (less than 12-byte header) let short_message = [0u8; 8]; let result = check_profile4(&config, &mut check_state, &short_message); - assert_eq!(result.status, E2ECheckStatus::BadArgument); + assert_eq!( + result.status, + E2ECheckStatus::Invalid(E2EValidateError::Profile4( + profile4::ValidateError::TooShort { actual: 8 } + )) + ); } #[test] @@ -414,18 +638,32 @@ mod tests { // Message too short (less than 3-byte header) let short_message = [0u8; 2]; let result = check_profile5(&config, &mut check_state, &short_message); - assert_eq!(result.status, E2ECheckStatus::BadArgument); + assert_eq!( + result.status, + E2ECheckStatus::Invalid(E2EValidateError::Profile5( + profile5::ValidateError::TooShort { actual: 2 } + )) + ); } #[cfg(feature = "std")] #[test] fn test_check_result_to_owned_payload() { let data = b"hello"; - let result = E2ECheckResult::success(E2ECheckStatus::Ok, 0, data); - let owned = result.to_owned_payload(); - assert_eq!(owned, Some(b"hello".to_vec())); - - let err_result = E2ECheckResult::error(E2ECheckStatus::CrcError); + let result = E2ECheckResult { + status: E2ECheckStatus::Ok, + counter: Some(0), + payload: Some(data), + }; + assert_eq!(result.to_owned_payload(), Some(b"hello".to_vec())); + + let err_result = E2ECheckResult { + status: E2ECheckStatus::Invalid(E2EValidateError::Profile5( + profile5::ValidateError::TooShort { actual: 1 }, + )), + counter: None, + payload: None, + }; assert_eq!(err_result.to_owned_payload(), None); } diff --git a/src/e2e/state.rs b/src/e2e/state.rs deleted file mode 100644 index 8c2bcd8..0000000 --- a/src/e2e/state.rs +++ /dev/null @@ -1,184 +0,0 @@ -//! State tracking structures for E2E profiles. - -/// State for E2E Profile 4 protection/checking. -#[derive(Debug, Clone)] -pub struct Profile4State { - /// Counter for protection (incremented on each protect call). - pub(crate) protect_counter: u16, - /// Last received counter for checking. - pub(crate) last_counter: Option, -} - -impl Profile4State { - /// Create a new Profile 4 state with initial counter value of 0. - #[must_use] - pub fn new() -> Self { - Self { - protect_counter: 0, - last_counter: None, - } - } - - /// Create a new Profile 4 state with a specific initial counter. - #[must_use] - pub fn with_initial_counter(counter: u16) -> Self { - Self { - protect_counter: counter, - last_counter: None, - } - } - - /// Returns the current protection counter value. - #[must_use] - pub const fn protect_counter(&self) -> u16 { - self.protect_counter - } - - /// Returns the last received counter value, or `None` if no message - /// has been checked yet. - #[must_use] - pub const fn last_counter(&self) -> Option { - self.last_counter - } - - /// Reset the state to initial values. - pub fn reset(&mut self) { - self.protect_counter = 0; - self.last_counter = None; - } -} - -impl Default for Profile4State { - fn default() -> Self { - Self::new() - } -} - -/// State for E2E Profile 5 protection/checking. -#[derive(Debug, Clone)] -pub struct Profile5State { - /// Counter for protection (incremented on each protect call). - pub(crate) protect_counter: u8, - /// Last received counter for checking. - pub(crate) last_counter: Option, -} - -impl Profile5State { - /// Create a new Profile 5 state with initial counter value of 0. - #[must_use] - pub fn new() -> Self { - Self { - protect_counter: 0, - last_counter: None, - } - } - - /// Create a new Profile 5 state with a specific initial counter. - #[must_use] - pub fn with_initial_counter(counter: u8) -> Self { - Self { - protect_counter: counter, - last_counter: None, - } - } - - /// Returns the current protection counter value. - #[must_use] - pub const fn protect_counter(&self) -> u8 { - self.protect_counter - } - - /// Returns the last received counter value, or `None` if no message - /// has been checked yet. - #[must_use] - pub const fn last_counter(&self) -> Option { - self.last_counter - } - - /// Reset the state to initial values. - pub fn reset(&mut self) { - self.protect_counter = 0; - self.last_counter = None; - } -} - -impl Default for Profile5State { - fn default() -> Self { - Self::new() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn profile4_reset_clears_state() { - let mut state = Profile4State::with_initial_counter(42); - state.last_counter = Some(10); - state.reset(); - assert_eq!(state.protect_counter, 0); - assert!(state.last_counter.is_none()); - } - - #[test] - fn profile4_default_matches_new() { - let from_new = Profile4State::new(); - let from_default = Profile4State::default(); - assert_eq!(from_new.protect_counter, from_default.protect_counter); - assert_eq!(from_new.last_counter, from_default.last_counter); - } - - #[test] - fn profile5_reset_clears_state() { - let mut state = Profile5State::with_initial_counter(42); - state.last_counter = Some(10); - state.reset(); - assert_eq!(state.protect_counter, 0); - assert!(state.last_counter.is_none()); - } - - #[test] - fn profile5_default_matches_new() { - let from_new = Profile5State::new(); - let from_default = Profile5State::default(); - assert_eq!(from_new.protect_counter, from_default.protect_counter); - assert_eq!(from_new.last_counter, from_default.last_counter); - } - - #[test] - fn profile4_with_initial_counter_sets_counter() { - let state = Profile4State::with_initial_counter(42); - assert_eq!(state.protect_counter(), 42); - assert_eq!(state.last_counter(), None); - } - - #[test] - fn profile4_accessors() { - let mut state = Profile4State::new(); - assert_eq!(state.protect_counter(), 0); - assert_eq!(state.last_counter(), None); - state.protect_counter = 5; - state.last_counter = Some(3); - assert_eq!(state.protect_counter(), 5); - assert_eq!(state.last_counter(), Some(3)); - } - - #[test] - fn profile5_with_initial_counter_sets_counter() { - let state = Profile5State::with_initial_counter(42); - assert_eq!(state.protect_counter(), 42); - assert_eq!(state.last_counter(), None); - } - - #[test] - fn profile5_accessors() { - let mut state = Profile5State::new(); - assert_eq!(state.protect_counter(), 0); - assert_eq!(state.last_counter(), None); - state.protect_counter = 5; - state.last_counter = Some(3); - assert_eq!(state.protect_counter(), 5); - assert_eq!(state.last_counter(), Some(3)); - } -} diff --git a/src/lib.rs b/src/lib.rs index aaca103..bac5554 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,7 +17,7 @@ //! | Module | `no_std` | Description | //! |--------|----------|-------------| //! | [`protocol`] | Yes | Wire format: headers, messages, message types, return codes, and service discovery (SD) entries/options | -//! | [`e2e`] | Yes | End-to-End protection — Profile 4 (CRC-32) and Profile 5 (CRC-16) | +//! | [`e2e`] | Yes | End-to-End protection — Profile 4 (CRC-32) and Profile 5 (CRC-16), via `simple-e2e` | //! | [`WireFormat`] / [`PayloadWireFormat`] | Yes | Traits for serializing messages and defining custom payload types | //! | `client` | No | Async client trait surface — service discovery, subscriptions, request/response (feature `client`; add `client-tokio` for `Client::new`) | //! | `server` | No | Async server trait surface — service offering, event publishing, subscription management (feature `server`; add `server-tokio` for `Server::new`) | @@ -278,7 +278,7 @@ pub use client::{ // the public-API contract and tempt generic users into hitting the // `ClientChannelTypes` elaboration limit at the wrong call site. pub use capacity::CapacityKind; -pub use e2e::{E2ECheckStatus, E2EKey, E2EProfile}; +pub use e2e::{E2ECheckStatus, E2EKey, E2EProfile, E2EValidateError}; #[cfg(feature = "server")] pub use server::{ NonSdRequestCallback, Server, ServerDeps, ServerHandles, ServerStorage, SubscriptionHandle, diff --git a/src/sd_codec.rs b/src/sd_codec.rs index 71cbab5..0435ff7 100644 --- a/src/sd_codec.rs +++ b/src/sd_codec.rs @@ -428,19 +428,12 @@ pub fn check_parsed_e2e<'a, R: E2ERegistryHandle>( } /// Map an [`E2ECheckStatus`] to the 1-byte code the firmware dispatch -/// expects (`0` = unchecked / none). Shared so the library and firmware -/// agree on the wire mapping. +/// expects (`0` = unchecked / none). Delegates to +/// [`E2ECheckStatus::to_return_code`]; kept as a named function because the +/// firmware dispatch table cites it. #[must_use] pub fn e2e_status_code(status: E2ECheckStatus) -> u8 { - match status { - E2ECheckStatus::Ok => 1, - E2ECheckStatus::CrcError => 2, - E2ECheckStatus::Repeated => 3, - E2ECheckStatus::OkSomeLost => 4, - E2ECheckStatus::WrongSequence => 5, - E2ECheckStatus::BadArgument => 6, - E2ECheckStatus::Unchecked => 0, - } + status.to_return_code() } #[cfg(test)] @@ -448,6 +441,36 @@ mod tests { use super::*; use crate::protocol::sd::EntryType; + #[test] + fn e2e_status_code_matches_return_code_for_every_status() { + use crate::e2e::{E2ECheckStatus, E2EValidateError}; + use simple_e2e::{profile4, profile5}; + let statuses = [ + E2ECheckStatus::Unchecked, + E2ECheckStatus::Ok, + E2ECheckStatus::OkSomeLost, + E2ECheckStatus::Repeated, + E2ECheckStatus::WrongSequence, + E2ECheckStatus::Invalid(E2EValidateError::Profile5( + profile5::ValidateError::CrcMismatch { + got: 1, + expected: 2, + }, + )), + E2ECheckStatus::Invalid(E2EValidateError::Profile4( + profile4::ValidateError::DataIdMismatch { + got: 1, + expected: 2, + }, + )), + ]; + for s in statuses { + assert_eq!(e2e_status_code(s), s.to_return_code(), "{s:?}"); + } + assert_eq!(e2e_status_code(statuses[5]), 2); + assert_eq!(e2e_status_code(statuses[6]), 6); + } + fn req(service_id: u16, port: u16) -> OfferServiceRequest { OfferServiceRequest { service_id,