diff --git a/CHANGELOG.md b/CHANGELOG.md index 128fece..38f9b4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ - **`std` is now the default feature** — the crate enables `std` (with `thiserror` and `tracing`) by default. Users targeting `no_std` environments must set `default-features = false` in their `Cargo.toml`. - **`thiserror` and `tracing` use `default-features = false`** — both dependencies are always included but their `std` features are only enabled when the crate's `std` feature is active. This removes the need for `#[cfg(feature = "std")]` gating on error types and logging macros. +- **E2E module folded into the `simple_e2e` crate.** The CRC tables, profile types, state machines, and registry storage all now live in `simple_e2e`; `simple_someip::e2e` becomes a thin shim re-exporting them, retaining only the SOME/IP-specific [`E2EKey`] (service+method-or-event) and the [`CheckStatus`] owned snapshot type used by `ReceivedMessage`. Eliminates ~2000 lines of duplicated implementation and ends the parallel taxonomy that forced downstream crates (notably `iris_someip_messages`) to ship a `compat` bridge with `got: 0, expected: 0` placeholder hazards. +- Add MIT + Apache-2.0 LICENSE files materializing the SPDX declaration. + +### Breaking + +- `simple_someip::e2e::E2ECheckStatus` is removed. Use [`simple_someip::e2e::CheckStatus`] (profile-discriminated, carries the rich underlying `simple_e2e::profile4::CheckStatus` / `profile5::CheckStatus` including real `got` / `expected` fields for CRC and length mismatches). +- `simple_someip::e2e::E2EProfile::Profile5WithHeader(config)` is replaced by `E2EProfile::Profile5 { config, include_upper_header: IncludeUpperHeader::Yes }`. The header-inclusion choice is now a per-binding option, not a third protocol variant. +- `simple_someip::e2e::Error` now aliases `simple_e2e::registry::ProtectError`. Wire-validation errors (`TooShort`, `LengthMismatch`, `DataIdMismatch`, `CrcMismatch`) surface via [`CheckStatus::Invalid(_)`] rather than the error chain — they aren't failures of the check operation itself, they're outcomes of it. +- `Client::register_e2e` and `Server::register_e2e` now return `Result<(), E2ERegistryFull>` instead of `()`. The previous silent-drop-on-full behavior was a footgun; callers must handle capacity explicitly. + +### Added + +- `simple_e2e` is a new path/version dependency (currently `path = "../simple_e2e"` during development; this will be swapped to `version = "0.1"` before publishing simple_someip 0.8.0). ## [0.6.0](https://github.com/luminartech/simple_someip/compare/v0.5.3...v0.6.0) - 2026-04-20 diff --git a/Cargo.lock b/Cargo.lock index 3fd7af0..261ed9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -159,6 +159,7 @@ dependencies = [ "crc", "embedded-io", "heapless", + "simple_e2e", "socket2 0.5.10", "thiserror", "tokio", @@ -166,6 +167,15 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "simple_e2e" +version = "0.1.0" +dependencies = [ + "crc", + "heapless", + "thiserror", +] + [[package]] name = "smallvec" version = "1.15.1" diff --git a/Cargo.toml b/Cargo.toml index 45bde7f..979fba2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,11 @@ repository = "https://github.com/luminartech/simple_someip" crc = "3.4" embedded-io = { version = "0.7" } heapless = "0.9" +# Path dep during development; swap to `version = "0.1"` from crates.io +# before publishing simple_someip 0.8.0. The `registry` feature provides +# `simple_e2e::registry::Registry` which simple_someip wraps as +# `E2ERegistry` keyed on `E2EKey`. +simple_e2e = { path = "../simple_e2e", default-features = false, features = ["registry"] } socket2 = { version = "0.5", optional = true, features = ["all"] } thiserror = { version = "2", default-features = false } tokio = { version = "1", default-features = false, features = [ diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..dab3b34 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for describing the origin of the Work and + reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Support. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or support. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 MicroVision, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied. See the License for the specific language governing permissions + and limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..aeff7c1 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 MicroVision, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/client/mod.rs b/src/client/mod.rs index 026f2b7..7951d9f 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -6,7 +6,7 @@ mod socket_manager; pub use error::Error; -use crate::e2e::{E2ECheckStatus, E2EKey, E2EProfile, E2ERegistry}; +use crate::e2e::{CheckStatus, E2EKey, E2EProfile, E2ERegistry}; use crate::{protocol, protocol::Message, traits::PayloadWireFormat}; use inner::{ControlMessage, Inner}; use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; @@ -79,7 +79,7 @@ pub enum ClientUpdate { /// The received SOME/IP message. message: Message

, /// E2E check status, if E2E was configured for this message. - e2e_status: Option, + e2e_status: Option, }, /// The client encountered an error. Error(Error), @@ -608,14 +608,24 @@ where /// header checked and stripped, and outgoing messages will have E2E /// protection applied automatically. /// + /// # Errors + /// + /// Returns [`crate::e2e::E2ERegistryFull`] if the registry is at capacity + /// and `key` is not already registered. Replacing an existing key's + /// profile always succeeds. + /// /// # Panics /// /// Panics if the E2E registry mutex is poisoned. - pub fn register_e2e(&self, key: E2EKey, profile: E2EProfile) { + pub fn register_e2e( + &self, + key: E2EKey, + profile: E2EProfile, + ) -> Result<(), crate::e2e::E2ERegistryFull> { self.e2e_registry .lock() .expect("e2e registry lock poisoned") - .register(key, profile); + .register(key, profile) } /// Remove E2E configuration for the given key. @@ -844,7 +854,7 @@ mod tests { method_or_event_id: 0x0001, }; let profile = E2EProfile::Profile4(crate::e2e::Profile4Config::new(42, 10)); - client.register_e2e(key, profile); + client.register_e2e(key, profile).expect("register"); client.unregister_e2e(&key); client.shut_down(); } diff --git a/src/client/socket_manager.rs b/src/client/socket_manager.rs index 13a8f6c..b67aa1c 100644 --- a/src/client/socket_manager.rs +++ b/src/client/socket_manager.rs @@ -1,5 +1,5 @@ use crate::{ - e2e::{E2ECheckStatus, E2EKey, E2ERegistry, PROFILE4_HEADER_SIZE}, + e2e::{CheckStatus, E2EKey, E2ERegistry, PROFILE4_HEADER_SIZE}, protocol::{Message, MessageView, sd}, traits::{PayloadWireFormat, WireFormat}, }; @@ -19,7 +19,7 @@ use tracing::{error, info, trace}; pub struct ReceivedMessage

{ pub message: Message

, pub source: SocketAddr, - pub e2e_status: Option, + pub e2e_status: Option, } /// Structure representing a request to send a message @@ -225,11 +225,18 @@ where let key = E2EKey::from_message_id(header.message_id()); let payload_bytes = view.payload_bytes(); - // Apply E2E check if configured + // Apply E2E check if configured. + // CheckOutcome borrows the input slice for the stripped + // payload, so we extract the owned status + payload slice + // here and drop the outcome inside this block. let (e2e_status, effective_payload) = { let mut registry = e2e_registry.lock().expect("e2e registry lock poisoned"); match registry.check(key, payload_bytes, upper_header) { - Some((status, stripped)) => (Some(status), stripped), + Some(outcome) => { + let status = CheckStatus::from_outcome(&outcome); + let stripped = outcome.payload().unwrap_or(payload_bytes); + (Some(status), stripped) + } None => (None, payload_bytes), } }; 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 2eb08d7..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 { - tracing::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(); - tracing::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 { - tracing::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(); - tracing::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, 0x12345678, b"test"); - let crc2 = compute_crc32_p4(10, 1, 0x12345678, b"test"); - let crc3 = compute_crc32_p4(10, 0, 0x12345679, b"test"); - let crc4 = compute_crc32_p4(10, 0, 0x12345678, 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, 0xABCDEF01, b"payload data"); - let crc2 = compute_crc32_p4(20, 5, 0xABCDEF01, 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, 0x12345678, 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 549f744..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 { - tracing::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 { - tracing::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(0x12345678, 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(0x12345678, 15); - let config2 = Profile4Config::new(0xDEADBEEF, 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(0x12345678, 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(0x12345678, 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(0x12345678, 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(0x12345678, 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(0x12345678, 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(0x12345678, 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(0x12345678, 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(0x12345678, 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(0x12345678, 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.as_deref(), 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 90122f8..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(0x12345678, 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, 0x12345678); - - // Check payload at end - assert_eq!(&protected[12..], b"test"); - } - - #[test] - fn test_protect_profile4_counter_increment() { - let config = Profile4Config::new(0x12345678, 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(0x12345678, 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(0x12345678, 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(0x12345678, 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(0x12345678, 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 deleted file mode 100644 index 9966009..0000000 --- a/src/e2e/error.rs +++ /dev/null @@ -1,14 +0,0 @@ -use thiserror::Error; - -/// Errors that can occur during E2E protection or checking. -#[derive(Error, Debug)] -pub enum Error { - /// The output buffer is too small to hold the protected payload. - #[error("output buffer too small: need {needed} bytes, got {actual}")] - BufferTooSmall { - /// The number of bytes required. - needed: usize, - /// The number of bytes available. - actual: usize, - }, -} diff --git a/src/e2e/mod.rs b/src/e2e/mod.rs index 233db20..1c186df 100644 --- a/src/e2e/mod.rs +++ b/src/e2e/mod.rs @@ -1,137 +1,54 @@ -//! E2E (End-to-End) protection for SOME/IP payloads. +//! SOME/IP-specific keying for the `simple_e2e` registry. //! -//! 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). +//! Following the simple_e2e migration (0.8.0), this module is a thin +//! shim over `simple_e2e::registry`. The only SOME/IP-flavored type is +//! [`E2EKey`] — service+method-or-event keying derived from a SOME/IP +//! [`MessageId`](crate::protocol::MessageId). Everything else is +//! re-exported from `simple_e2e`. //! //! # Example //! -//! ``` +//! ```ignore //! use simple_someip::e2e::{ -//! Profile4Config, Profile4State, -//! protect_profile4, check_profile4, -//! E2ECheckStatus, +//! E2ERegistry, E2EProfile, E2EKey, Profile4Config, //! }; //! +//! let mut registry = E2ERegistry::new(); +//! let key = E2EKey::new(0x1234, 0x5678); //! let config = Profile4Config::new(0x12345678, 15); -//! let mut protect_state = Profile4State::new(); -//! let mut check_state = Profile4State::new(); +//! registry +//! .register(key, E2EProfile::Profile4(config)) +//! .expect("registry not full"); //! //! let payload = b"Hello, SOME/IP!"; //! let mut buf = [0u8; 128]; -//! let len = protect_profile4(&config, &mut protect_state, payload, &mut buf).unwrap(); +//! let len = registry +//! .protect(key, payload, [0; 8], &mut buf) +//! .expect("key registered") +//! .expect("buffer large enough"); //! -//! let result = check_profile4(&config, &mut check_state, &buf[..len]); -//! assert!(matches!(result.status, E2ECheckStatus::Ok)); +//! let outcome = registry +//! .check(key, &buf[..len], [0; 8]) +//! .expect("key registered"); +//! assert!(outcome.is_ok()); //! ``` -mod config; -mod crc; -mod e2e_checker; -mod e2e_protector; -mod error; -#[cfg(feature = "std")] -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 simple_e2e::profile4::{ + Config as Profile4Config, HEADER_SIZE as PROFILE4_HEADER_SIZE, State as Profile4State, +}; +pub use simple_e2e::profile5::{ + Config as Profile5Config, HEADER_SIZE as PROFILE5_HEADER_SIZE, State as Profile5State, +}; +pub use simple_e2e::registry::{ + CheckOutcome, IncludeUpperHeader, Profile as E2EProfile, ProtectError as Error, + RegistryFull as E2ERegistryFull, }; -pub use error::Error; -#[cfg(feature = "std")] -pub use registry::E2ERegistry; -pub use state::{Profile4State, Profile5State}; - -/// Status result from E2E check operations. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum E2ECheckStatus { - /// Initial state, no check performed yet. - Unchecked, - /// Check passed successfully. - 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). - OkSomeLost, - /// Counter sequence error (gap exceeds `max_delta_counter`). - WrongSequence, - /// Invalid input arguments (e.g., message too short). - BadArgument, -} - -impl E2ECheckStatus { - /// Convert to a numeric return code compatible with E2E. - #[must_use] - pub fn to_return_code(self) -> u8 { - match self { - E2ECheckStatus::Unchecked => 0, - E2ECheckStatus::Ok => 1, - E2ECheckStatus::CrcError => 2, - E2ECheckStatus::Repeated => 3, - E2ECheckStatus::OkSomeLost => 4, - E2ECheckStatus::WrongSequence => 5, - E2ECheckStatus::BadArgument => 6, - } - } -} - -/// Result from an E2E check operation. -#[derive(Debug, Clone)] -pub struct E2ECheckResult<'a> { - /// Status of the E2E check. - pub status: E2ECheckStatus, - /// Counter value extracted from the header (if parsing succeeded). - pub counter: Option, - /// Extracted payload without E2E header (if check succeeded). - /// - /// This is a borrowed subslice of the input `protected` buffer and is only - /// valid as long as that buffer is kept alive. - 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), - } - } - - /// Copy the extracted payload into an owned `Vec`. - /// - /// Returns `None` if the check did not produce a payload (e.g. on error). - #[cfg(feature = "std")] - #[must_use] - pub fn to_owned_payload(&self) -> Option> { - self.payload.map(<[u8]>::to_vec) - } -} - -/// Describes which E2E profile to apply for a given data element. -#[derive(Debug, Clone)] -pub enum E2EProfile { - /// E2E Profile 4 (CRC-32, 12-byte header). - Profile4(Profile4Config), - /// E2E Profile 5 (CRC-16, 3-byte header, no upper-header in CRC). - Profile5(Profile5Config), - /// E2E Profile 5 with SOME/IP upper-header included in the CRC. - Profile5WithHeader(Profile5Config), -} -/// Identifies a data element for E2E protection lookup. +/// SOME/IP-specific lookup key for E2E configuration. +/// +/// A `(service_id, method_or_event_id)` pair uniquely identifies a data +/// element on the SOME/IP wire, and that's exactly what's keyed in the +/// registry. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct E2EKey { /// SOME/IP service ID. @@ -160,77 +77,80 @@ impl E2EKey { } } -/// Internal E2E state, one per registered key. -#[cfg(feature = "std")] -#[derive(Debug, Clone)] -pub(crate) enum E2EState { - /// State for Profile 4. - Profile4(Profile4State), - /// State for Profile 5 (used by both `Profile5` and `Profile5WithHeader`). - Profile5(Profile5State), +/// E2E registry pre-bound to [`E2EKey`] with capacity 32 — sized for +/// typical SOME/IP workloads (a service instance with up to a few dozen +/// E2E-protected message types). +pub type E2ERegistry = simple_e2e::registry::Registry; + +/// SOME/IP-specific owned snapshot of a check outcome. +/// +/// `simple_e2e::registry::CheckOutcome` borrows the input buffer for the +/// stripped-payload slice, which makes it unsuitable for cross-task +/// storage on `ReceivedMessage`. This owned variant captures just the +/// status half — profile-discriminated, no payload — so it can travel +/// through async channels. +/// +/// Construct via [`Self::from_outcome`] right after `registry.check(...)` +/// returns, while the borrowed payload is still in scope. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CheckStatus { + /// Profile 4 result. + Profile4(simple_e2e::profile4::CheckStatus), + /// Profile 5 result (covers both plain and with-upper-header bindings). + Profile5(simple_e2e::profile5::CheckStatus), } -#[cfg(feature = "std")] -impl E2EState { - pub(crate) fn from_profile(profile: &E2EProfile) -> Self { - match profile { - E2EProfile::Profile4(_) => Self::Profile4(Profile4State::new()), - E2EProfile::Profile5(_) | E2EProfile::Profile5WithHeader(_) => { - Self::Profile5(Profile5State::new()) - } +impl CheckStatus { + /// Copy the status half of a [`CheckOutcome`] into the owned form. + #[must_use] + pub fn from_outcome(outcome: &CheckOutcome<'_>) -> Self { + match outcome { + CheckOutcome::Profile4 { status, .. } => Self::Profile4(status.clone()), + CheckOutcome::Profile5 { status, .. } => Self::Profile5(status.clone()), } } -} -/// Run the appropriate E2E check for the given profile, returning the status -/// and the best available payload slice (stripped on success, original on error). -#[cfg(feature = "std")] -pub(crate) fn e2e_check<'a>( - profile: &E2EProfile, - state: &mut E2EState, - payload: &'a [u8], - upper_header: [u8; 8], -) -> (E2ECheckStatus, &'a [u8]) { - let result = match (profile, state) { - (E2EProfile::Profile4(config), E2EState::Profile4(st)) => { - check_profile4(config, st, payload) - } - (E2EProfile::Profile5(config), E2EState::Profile5(st)) => { - check_profile5(config, st, payload) - } - (E2EProfile::Profile5WithHeader(config), E2EState::Profile5(st)) => { - check_profile5_with_header(config, st, payload, upper_header) + /// `true` only for status `Ok` — not `OkSomeLost`, `Repeated`, + /// `WrongSequence`, or `Invalid`. Use for "did exactly the next-in- + /// sequence valid message arrive?" checks. + #[must_use] + pub fn is_ok(&self) -> bool { + match self { + Self::Profile4(s) => matches!(s, simple_e2e::profile4::CheckStatus::Ok), + Self::Profile5(s) => matches!(s, simple_e2e::profile5::CheckStatus::Ok), } - _ => return (E2ECheckStatus::BadArgument, payload), - }; - let stripped = result.payload.unwrap_or(payload); - (result.status, stripped) -} + } -/// Run the appropriate E2E protect for the given profile. -/// -/// # Errors -/// -/// Returns [`Error::BufferTooSmall`] if `output` cannot hold the protected payload. -#[cfg(feature = "std")] -pub(crate) fn e2e_protect( - profile: &E2EProfile, - state: &mut E2EState, - payload: &[u8], - upper_header: [u8; 8], - output: &mut [u8], -) -> Result { - match (profile, state) { - (E2EProfile::Profile4(config), E2EState::Profile4(st)) => { - protect_profile4(config, st, payload, output) - } - (E2EProfile::Profile5(config), E2EState::Profile5(st)) => { - protect_profile5(config, st, payload, output) - } - (E2EProfile::Profile5WithHeader(config), E2EState::Profile5(st)) => { - protect_profile5_with_header(config, st, payload, upper_header, output) + /// Map to the AUTOSAR E2E return code byte used on the SOME/IP wire. + /// + /// `Ok` → 1, `OkSomeLost` → 4, `Repeated` → 3, `WrongSequence` → 5. + /// `Invalid(CrcMismatch)` → 2 (CrcError); other `Invalid(_)` → + /// 6 (BadArgument). 0 (Unchecked) is reserved for the "no profile + /// registered" case the caller signals separately by passing `None`. + #[must_use] + pub fn to_return_code(&self) -> u8 { + match self { + Self::Profile4(s) => match s { + simple_e2e::profile4::CheckStatus::Ok => 1, + simple_e2e::profile4::CheckStatus::OkSomeLost => 4, + simple_e2e::profile4::CheckStatus::Repeated => 3, + simple_e2e::profile4::CheckStatus::WrongSequence => 5, + simple_e2e::profile4::CheckStatus::Invalid( + simple_e2e::profile4::ValidateError::CrcMismatch { .. }, + ) => 2, + simple_e2e::profile4::CheckStatus::Invalid(_) => 6, + }, + Self::Profile5(s) => match s { + simple_e2e::profile5::CheckStatus::Ok => 1, + simple_e2e::profile5::CheckStatus::OkSomeLost => 4, + simple_e2e::profile5::CheckStatus::Repeated => 3, + simple_e2e::profile5::CheckStatus::WrongSequence => 5, + simple_e2e::profile5::CheckStatus::Invalid( + simple_e2e::profile5::ValidateError::CrcMismatch { .. }, + ) => 2, + simple_e2e::profile5::CheckStatus::Invalid(_) => 6, + }, } - _ => unreachable!("E2EState is always created from E2EProfile"), } } @@ -239,207 +159,60 @@ mod tests { use super::*; #[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); - 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); + fn e2e_key_from_message_id() { + let message_id = crate::protocol::MessageId::new_from_service_and_method(0x1234, 0x5678); + let key = E2EKey::from_message_id(message_id); + assert_eq!(key.service_id, 0x1234); + assert_eq!(key.method_or_event_id, 0x5678); } #[test] - fn test_profile4_roundtrip() { - let config = Profile4Config::new(0x12345678, 15); - let mut protect_state = Profile4State::new(); - let mut check_state = Profile4State::new(); - - let payload = b"Test payload data"; - let mut buf = [0u8; 256]; - let len = protect_profile4(&config, &mut protect_state, payload, &mut buf).unwrap(); - let protected = &buf[..len]; - - assert_eq!(len, payload.len() + 12); // 12-byte header - - 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())); + fn registry_round_trip_profile4() { + let mut registry = E2ERegistry::new(); + let key = E2EKey::new(0x1234, 0x5678); + registry + .register( + key, + E2EProfile::Profile4(Profile4Config::new(0x12345678, 15)), + ) + .expect("register fits within capacity"); + + let payload = b"hello"; + let mut buf = [0u8; 64]; + let len = registry + .protect(key, payload, [0; 8], &mut buf) + .expect("registered") + .expect("buffer large"); + + let outcome = registry + .check(key, &buf[..len], [0; 8]) + .expect("registered"); + assert!(outcome.is_ok()); } #[test] - fn test_profile5_roundtrip() { - 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[..17].copy_from_slice(b"Test payload data"); - let mut buf = [0u8; 256]; - let len = protect_profile5(&config, &mut protect_state, &payload, &mut buf).unwrap(); - let protected = &buf[..len]; - - assert_eq!(len, payload.len() + 3); // 3-byte header - - 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())); + fn check_status_to_return_code_ok() { + let status = CheckStatus::Profile4(simple_e2e::profile4::CheckStatus::Ok); + assert_eq!(status.to_return_code(), 1); } #[test] - fn test_profile4_sequence_detection() { - let config = Profile4Config::new(0x12345678, 5); - let mut protect_state = Profile4State::new(); - let mut check_state = Profile4State::new(); - - let payload = b"Test"; - let mut buf1 = [0u8; 256]; - let mut buf2 = [0u8; 256]; - - // First message - should be Ok - let len1 = protect_profile4(&config, &mut protect_state, payload, &mut buf1).unwrap(); - let result1 = check_profile4(&config, &mut check_state, &buf1[..len1]); - assert_eq!(result1.status, E2ECheckStatus::Ok); - - // Second message - should be Ok - let len2 = protect_profile4(&config, &mut protect_state, payload, &mut buf2).unwrap(); - let result2 = check_profile4(&config, &mut check_state, &buf2[..len2]); - assert_eq!(result2.status, E2ECheckStatus::Ok); - - // Replay first message - should be Repeated or WrongSequence - let result3 = check_profile4(&config, &mut check_state, &buf1[..len1]); - assert!(matches!( - result3.status, - E2ECheckStatus::Repeated | E2ECheckStatus::WrongSequence + fn check_status_to_return_code_crc_mismatch() { + let status = CheckStatus::Profile4(simple_e2e::profile4::CheckStatus::Invalid( + simple_e2e::profile4::ValidateError::CrcMismatch { + got: 0xDEAD_BEEF, + expected: 0xCAFE_BABE, + }, )); + assert_eq!(status.to_return_code(), 2); } #[test] - fn test_profile4_some_lost_detection() { - let config = Profile4Config::new(0x12345678, 5); - 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 a few messages by advancing protector counter - protect_profile4(&config, &mut protect_state, payload, &mut buf).unwrap(); - protect_profile4(&config, &mut protect_state, payload, &mut buf).unwrap(); - let len = protect_profile4(&config, &mut protect_state, payload, &mut buf).unwrap(); - - // Check skipped message - should be OkSomeLost (delta=3, within max_delta=5) - let result4 = check_profile4(&config, &mut check_state, &buf[..len]); - assert_eq!(result4.status, E2ECheckStatus::OkSomeLost); - } - - #[test] - fn test_profile4_wrong_sequence_detection() { - let config = Profile4Config::new(0x12345678, 2); - 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 (exceed max_delta) - for _ in 0..5 { - protect_profile4(&config, &mut protect_state, payload, &mut buf).unwrap(); - } - let len = protect_profile4(&config, &mut protect_state, payload, &mut buf).unwrap(); - - // Check - should be WrongSequence (delta=6, exceeds max_delta=2) - let result = check_profile4(&config, &mut check_state, &buf[..len]); - assert_eq!(result.status, E2ECheckStatus::WrongSequence); - } - - #[test] - fn test_profile4_crc_error() { - let config = Profile4Config::new(0x12345678, 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 the CRC (last 4 bytes of header) - buf[8] ^= 0xFF; - - let result = check_profile4(&config, &mut check_state, &buf[..len]); - assert_eq!(result.status, E2ECheckStatus::CrcError); - } - - #[test] - fn test_profile5_crc_error() { - 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 the CRC (bytes 1-2 of header) - buf[1] ^= 0xFF; - - let result = check_profile5(&config, &mut check_state, &buf[..len]); - assert_eq!(result.status, E2ECheckStatus::CrcError); - } - - #[test] - fn test_profile4_bad_argument_short_message() { - let config = Profile4Config::new(0x12345678, 15); - let mut check_state = Profile4State::new(); - - // 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); - } - - #[test] - fn test_profile5_bad_argument_short_message() { - let config = Profile5Config::new(0x1234, 20, 15); - let mut check_state = Profile5State::new(); - - // 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); - } - - #[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); - assert_eq!(err_result.to_owned_payload(), None); - } - - #[test] - fn test_e2e_key_from_message_id() { - let mid = crate::protocol::MessageId::new_from_service_and_method(0x1234, 0x0001); - let key = E2EKey::from_message_id(mid); - assert_eq!(key.service_id, 0x1234); - assert_eq!(key.method_or_event_id, 0x0001); + fn check_status_to_return_code_bad_argument() { + // TooShort / LengthMismatch / DataIdMismatch all collapse to 6. + let too_short = CheckStatus::Profile4(simple_e2e::profile4::CheckStatus::Invalid( + simple_e2e::profile4::ValidateError::TooShort { actual: 5 }, + )); + assert_eq!(too_short.to_return_code(), 6); } } diff --git a/src/e2e/registry.rs b/src/e2e/registry.rs deleted file mode 100644 index 0dcd8c8..0000000 --- a/src/e2e/registry.rs +++ /dev/null @@ -1,150 +0,0 @@ -//! E2E configuration registry for runtime E2E management. - -use std::collections::HashMap; - -use super::{E2ECheckStatus, E2EKey, E2EProfile, E2EState, Error, e2e_check, e2e_protect}; - -/// Registry mapping message keys to E2E profile configurations and state. -#[derive(Debug)] -pub struct E2ERegistry { - map: HashMap, -} - -impl E2ERegistry { - /// Create an empty registry. - #[must_use] - pub fn new() -> Self { - Self { - map: HashMap::new(), - } - } - - /// Register an E2E profile for the given key, creating fresh state. - pub fn register(&mut self, key: E2EKey, profile: E2EProfile) { - let state = E2EState::from_profile(&profile); - self.map.insert(key, (profile, state)); - } - - /// Remove E2E configuration for the given key. - pub fn unregister(&mut self, key: &E2EKey) { - self.map.remove(key); - } - - /// Returns `true` if a profile is registered for `key`. - #[must_use] - pub fn contains_key(&self, key: &E2EKey) -> bool { - self.map.contains_key(key) - } - - /// Run E2E check for `key` if configured. - /// - /// Returns `None` if no profile is registered for `key`. - /// Otherwise returns the check status and the best available payload - /// (stripped E2E header on success, original bytes on check failure). - pub fn check<'a>( - &mut self, - key: E2EKey, - payload: &'a [u8], - upper_header: [u8; 8], - ) -> Option<(E2ECheckStatus, &'a [u8])> { - let (profile, state) = self.map.get_mut(&key)?; - Some(e2e_check(profile, state, payload, upper_header)) - } - - /// Run E2E protect for `key` if configured. - /// - /// Returns `None` if no profile is registered for `key`. - pub fn protect( - &mut self, - key: E2EKey, - payload: &[u8], - upper_header: [u8; 8], - output: &mut [u8], - ) -> Option> { - let (profile, state) = self.map.get_mut(&key)?; - Some(e2e_protect(profile, state, payload, upper_header, output)) - } -} - -impl Default for E2ERegistry { - fn default() -> Self { - Self::new() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::e2e::{Profile4Config, Profile5Config}; - - fn make_key() -> E2EKey { - E2EKey::new(0x1234, 0x5678) - } - - #[test] - fn register_and_check_profile4() { - let mut reg = E2ERegistry::new(); - let key = make_key(); - let config = Profile4Config::new(0x12345678, 15); - reg.register(key, E2EProfile::Profile4(config.clone())); - assert!(reg.contains_key(&key)); - - // Protect a payload - let payload = b"Hello"; - let mut out = [0u8; 64]; - let len = reg - .protect(key, payload, [0; 8], &mut out) - .unwrap() - .unwrap(); - - // Check it - let (status, stripped) = reg.check(key, &out[..len], [0; 8]).unwrap(); - assert_eq!(status, E2ECheckStatus::Ok); - assert_eq!(stripped, payload); - } - - #[test] - fn register_and_check_profile5() { - let mut reg = E2ERegistry::new(); - let key = make_key(); - let config = Profile5Config::new(0x1234, 20, 15); - reg.register(key, E2EProfile::Profile5(config)); - - let mut payload = [0u8; 20]; - payload[..5].copy_from_slice(b"Hello"); - let mut out = [0u8; 64]; - let len = reg - .protect(key, &payload, [0; 8], &mut out) - .unwrap() - .unwrap(); - - let (status, stripped) = reg.check(key, &out[..len], [0; 8]).unwrap(); - assert_eq!(status, E2ECheckStatus::Ok); - assert_eq!(stripped, &payload); - } - - #[test] - fn unregistered_key_returns_none() { - let mut reg = E2ERegistry::new(); - let key = make_key(); - assert!(!reg.contains_key(&key)); - assert!(reg.check(key, b"test", [0; 8]).is_none()); - assert!(reg.protect(key, b"test", [0; 8], &mut [0; 64]).is_none()); - } - - #[test] - fn unregister_removes_key() { - let mut reg = E2ERegistry::new(); - let key = make_key(); - reg.register(key, E2EProfile::Profile4(Profile4Config::new(0, 15))); - assert!(reg.contains_key(&key)); - reg.unregister(&key); - assert!(!reg.contains_key(&key)); - } - - #[test] - fn default_is_empty() { - let reg = E2ERegistry::default(); - assert!(!reg.contains_key(&make_key())); - } -} 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 78877b3..1bb399a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -114,6 +114,6 @@ pub use traits::{PayloadWireFormat, WireFormat}; #[cfg(feature = "client")] pub use client::{Client, ClientUpdate, ClientUpdates, DiscoveryMessage, PendingResponse}; -pub use e2e::{E2ECheckStatus, E2EKey, E2EProfile}; +pub use e2e::{CheckStatus, E2EKey, E2EProfile}; #[cfg(feature = "server")] pub use server::Server; diff --git a/src/server/mod.rs b/src/server/mod.rs index 1532b98..731e7e9 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -481,14 +481,24 @@ impl Server { /// Once registered, outgoing events published via [`EventPublisher::publish_event`] /// will have E2E protection applied automatically. /// + /// # Errors + /// + /// Returns [`crate::e2e::E2ERegistryFull`] if the registry is at capacity + /// and `key` is not already registered. Replacing an existing key's + /// profile always succeeds. + /// /// # Panics /// /// Panics if the E2E registry mutex is poisoned. - pub fn register_e2e(&self, key: E2EKey, profile: E2EProfile) { + pub fn register_e2e( + &self, + key: E2EKey, + profile: E2EProfile, + ) -> Result<(), crate::e2e::E2ERegistryFull> { self.e2e_registry .lock() .expect("e2e registry lock poisoned") - .register(key, profile); + .register(key, profile) } /// Remove E2E configuration for the given key. diff --git a/tests/client_server.rs b/tests/client_server.rs index ffd6d34..cfa0b8f 100644 --- a/tests/client_server.rs +++ b/tests/client_server.rs @@ -1,6 +1,6 @@ //! Integration tests exercising the Client and Server together on localhost. -use simple_someip::e2e::{E2ECheckStatus, E2EKey, E2EProfile, Profile4Config}; +use simple_someip::e2e::{CheckStatus, E2EKey, E2EProfile, Profile4Config}; use simple_someip::protocol::{Header, Message, MessageId, sd}; use simple_someip::server::ServerConfig; use simple_someip::{Client, ClientUpdate, PayloadWireFormat, RawPayload, Server, VecSdHeader}; @@ -317,14 +317,14 @@ async fn test_e2e_protect_on_publish_and_check_on_receive() { method_or_event_id: 0x0001, }; let profile = E2EProfile::Profile4(Profile4Config::new(0x12345678, 15)); - server.register_e2e(key, profile.clone()); + server.register_e2e(key, profile.clone()).expect("register"); let server_handle = tokio::spawn(async move { server.run().await }); let (client, mut updates) = TestClient::new(Ipv4Addr::LOCALHOST); // Register matching E2E profile on client - client.register_e2e(key, profile); + client.register_e2e(key, profile).expect("register"); let server_addr = SocketAddrV4::new(Ipv4Addr::LOCALHOST, server_port); client.add_endpoint(0x5B, 1, server_addr, 0).await.unwrap(); @@ -361,10 +361,9 @@ async fn test_e2e_protect_on_publish_and_check_on_receive() { e2e_status.is_some(), "expected e2e_status to be populated when E2E is configured" ); - assert_eq!( - e2e_status.unwrap(), - E2ECheckStatus::Ok, - "E2E check should pass for correctly protected message" + assert!( + e2e_status.as_ref().is_some_and(CheckStatus::is_ok), + "E2E check should pass for correctly protected message, got {e2e_status:?}", ); } other => panic!("expected Unicast with e2e_status, got {other:?}"),