From 72e215062954d71b297d9349ecf4173aa82bce48 Mon Sep 17 00:00:00 2001 From: Justin Kovacich Date: Wed, 13 May 2026 15:16:49 -0400 Subject: [PATCH 1/2] chore: add MIT + Apache-2.0 LICENSE files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cargo.toml already declares `license = "MIT OR Apache-2.0"` via SPDX; this materializes the actual text in the repo so downstream consumers, distros, and license scanners see the canonical templates rather than relying on the SPDX line alone. Same content as simple_e2e's license files. Copyright © 2026 MicroVision, Inc. Co-Authored-By: Claude Opus 4.7 --- LICENSE-APACHE | 201 +++++++++++++++++++++++++++++++++++++++++++++++++ LICENSE-MIT | 21 ++++++ 2 files changed, 222 insertions(+) create mode 100644 LICENSE-APACHE create mode 100644 LICENSE-MIT 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. From 095b9c6b4066ccd27f4ec46b0eff78eb9c8da6ea Mon Sep 17 00:00:00 2001 From: Justin Kovacich Date: Wed, 13 May 2026 15:49:42 -0400 Subject: [PATCH 2/2] feat(e2e)!: fold module into simple_e2e crate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the ~2000 lines of E2E implementation (CRC tables, profile types, state machines, registry storage) with re-exports from the sibling `simple_e2e` crate. `simple_someip::e2e` becomes a thin shim keeping only what's genuinely SOME/IP-flavored: * `E2EKey` — `(service_id, method_or_event_id)` pair plus `from_message_id` constructor. * `E2ERegistry` — pre-bound `simple_e2e::registry::Registry`. * `CheckStatus` — owned snapshot of `simple_e2e::registry::CheckOutcome` that crosses async channels (the underlying `CheckOutcome` borrows the input buffer, so `ReceivedMessage` can't store it directly). * `to_return_code` — SOME/IP-specific E2E status → wire-byte mapping that previously lived on `E2ECheckStatus`. Everything else is `pub use simple_e2e::…`. # Motivation `simple_someip` and `simple_e2e` had grown parallel taxonomies for the same protocol — different enum names, different variant lists, different error types. The split forced `iris_someip_messages` to maintain a `compat` bridge with `got: 0, expected: 0` placeholder hazards (because `simple_someip::e2e::E2ECheckStatus::CrcError` was tagless, while `simple_e2e::profile4::ValidateError::CrcMismatch` carries real values). This fold ends the parallel taxonomy and unblocks deletion of that bridge in the downstream `dft` PR. # Breaking changes (intentional, will tag as 0.8.0) * `E2ECheckStatus` removed → use `CheckStatus`, which is profile-discriminated and carries the rich underlying status enums (real `got` / `expected` fields on `Invalid(...)`). * `E2EProfile::Profile5WithHeader(config)` removed → use `Profile5 { config, include_upper_header: IncludeUpperHeader::Yes }`. Header inclusion is a per-binding option, not a third protocol variant. * `e2e::Error` now aliases `simple_e2e::registry::ProtectError`. Wire-validation outcomes (`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` / `Server::register_e2e` now return `Result<(), E2ERegistryFull>`. The previous silent-drop-on-full was a footgun; capacity must be handled explicitly. # Tests * `e2e::tests::*` — the new shim's smoke tests round-trip P4 through the registry and verify `CheckStatus::to_return_code` mapping (Ok→1, CrcMismatch→2, TooShort→6 BadArgument). * `tests/client_server.rs::test_e2e_protect_on_publish_and_check_on_receive` passes — proves the wire-format protect → wire-format check round-trip through the SOME/IP transport still works end-to-end. The remaining `tests/client_server.rs` tests have intermittent UDP/SD port-conflict flakiness when run in parallel — pre-existing, unrelated to this fold; verified by isolating each test, which passes. # Dependency note `simple_e2e` is currently a path dep (`path = "../simple_e2e"`). Before publishing simple_someip 0.8.0 to crates.io, this must swap to `version = "0.1"` once simple_e2e is published. The CHANGELOG entry flags this. Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 13 + Cargo.lock | 10 + Cargo.toml | 5 + src/client/mod.rs | 20 +- src/client/socket_manager.rs | 15 +- src/e2e/config.rs | 55 --- src/e2e/crc.rs | 225 ------------ src/e2e/e2e_checker.rs | 670 ----------------------------------- src/e2e/e2e_protector.rs | 490 ------------------------- src/e2e/error.rs | 14 - src/e2e/mod.rs | 513 ++++++++------------------- src/e2e/registry.rs | 150 -------- src/e2e/state.rs | 184 ---------- src/lib.rs | 2 +- src/server/mod.rs | 14 +- tests/client_server.rs | 13 +- 16 files changed, 216 insertions(+), 2177 deletions(-) delete mode 100644 src/e2e/config.rs delete mode 100644 src/e2e/crc.rs delete mode 100644 src/e2e/e2e_checker.rs delete mode 100644 src/e2e/e2e_protector.rs delete mode 100644 src/e2e/error.rs delete mode 100644 src/e2e/registry.rs delete mode 100644 src/e2e/state.rs 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/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:?}"),