diff --git a/Cargo.lock b/Cargo.lock index b22ef89..b46d44f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -663,6 +663,13 @@ dependencies = [ "tokio", ] +[[package]] +name = "size_probe" +version = "0.0.0" +dependencies = [ + "simple-someip", +] + [[package]] name = "slab" version = "0.4.12" diff --git a/Cargo.toml b/Cargo.toml index 80e5e1f..b8078b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "examples/discovery_client", "examples/embassy_net_client", "simple-someip-embassy-net", + "tools/size_probe", ] [package] diff --git a/tests/data/vsomeip-offerer/CMakeLists.txt b/tests/data/vsomeip-offerer/CMakeLists.txt new file mode 100644 index 0000000..f0f144a --- /dev/null +++ b/tests/data/vsomeip-offerer/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 3.13) +project(vsomeip_offerer CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# vsomeip's exported config (installed by `make install` in the build +# stage of the Dockerfile) provides the imported targets we need. +find_package(vsomeip3 REQUIRED) + +# Offerer binary: tiny, links libvsomeip3. +add_executable(offerer offerer.cpp) + +target_link_libraries(offerer + PRIVATE + vsomeip3 +) + +# vsomeip publishes its headers under . +target_include_directories(offerer + PRIVATE + ${VSOMEIP_INCLUDE_DIRS} +) diff --git a/tests/data/vsomeip-offerer/Dockerfile b/tests/data/vsomeip-offerer/Dockerfile new file mode 100644 index 0000000..2020c3c --- /dev/null +++ b/tests/data/vsomeip-offerer/Dockerfile @@ -0,0 +1,94 @@ +# vsomeip 3.4.10 + a minimal offerer that advertises service 0x1234 +# instance 0x0001 via SD multicast. Used by phase 20f's host-side +# conformance test (`tests/vsomeip_sd_compat.rs`). +# +# Build: +# docker build -t vsomeip-offerer tests/data/vsomeip-offerer/ +# +# Run (host network mode so SD multicast 224.0.23.0:30490 reaches the +# host's listener — required for the cargo test to receive the +# OfferService broadcast): +# docker run --rm -d --name vsomeip-offerer --network host \ +# vsomeip-offerer +# +# Verify it's emitting: +# docker logs vsomeip-offerer +# +# Stop: +# docker stop vsomeip-offerer +# +# Pinning to vsomeip 3.4.10 specifically because that's the version +# LumPDK / EnVision use (see LumPDK/packages/thirdparty/vsomeip/ +# vsomeip.MODULE.bazel). Keeping wire-version-aligned with production +# avoids interop quirks during CI conformance testing. + +FROM ubuntu:22.04 AS build + +# vsomeip's CMake build needs: gcc, cmake, boost, and a few utilities +# for the patch-and-build flow. dlt is optional and we skip it. +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + cmake \ + git \ + ca-certificates \ + wget \ + libboost-system-dev \ + libboost-filesystem-dev \ + libboost-thread-dev \ + libboost-log-dev \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /src + +# Pull vsomeip 3.4.10 from upstream. +RUN wget -q https://github.com/COVESA/vsomeip/archive/refs/tags/3.4.10.tar.gz \ + && tar xzf 3.4.10.tar.gz \ + && rm 3.4.10.tar.gz + +WORKDIR /src/vsomeip-3.4.10 + +# Build vsomeip as shared libs (default). Skip building the test/ +# example trees — we'll compile our own offerer against the installed +# library. +RUN mkdir build && cd build \ + && cmake .. \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX=/usr/local \ + && make -j"$(nproc)" \ + && make install \ + && ldconfig + +# Build our offerer. It links against the just-installed libvsomeip3. +COPY offerer.cpp /src/offerer/offerer.cpp +COPY CMakeLists.txt /src/offerer/CMakeLists.txt + +WORKDIR /src/offerer +RUN mkdir build && cd build \ + && cmake .. \ + -DCMAKE_BUILD_TYPE=Release \ + && make -j"$(nproc)" + +# ── Runtime image ──────────────────────────────────────────────────── +FROM ubuntu:22.04 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + libboost-system1.74.0 \ + libboost-filesystem1.74.0 \ + libboost-thread1.74.0 \ + libboost-log1.74.0 \ + && rm -rf /var/lib/apt/lists/* + +# Copy installed vsomeip libs + the offerer binary + the config + entrypoint. +COPY --from=build /usr/local/lib/libvsomeip3*.so* /usr/local/lib/ +COPY --from=build /src/offerer/build/offerer /usr/local/bin/offerer +COPY offerer.json /etc/vsomeip-offerer.json +COPY entrypoint.sh /usr/local/bin/entrypoint.sh + +RUN ldconfig && chmod +x /usr/local/bin/entrypoint.sh + +# Entrypoint script templates VSOMEIP_UNICAST into the JSON config +# before launching the offerer. Caller MUST pass `-e VSOMEIP_UNICAST= +# ` on `docker run`; the script exits +# loudly otherwise. See entrypoint.sh for the rationale (lo's lack of +# MULTICAST flag is the gotcha). +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] diff --git a/tests/data/vsomeip-offerer/README.md b/tests/data/vsomeip-offerer/README.md new file mode 100644 index 0000000..2e8c346 --- /dev/null +++ b/tests/data/vsomeip-offerer/README.md @@ -0,0 +1,113 @@ +# vsomeip offerer for phase-20f conformance testing + +A Docker image that builds vsomeip 3.4.10 (the version LumPDK / +EnVision pin) and runs a tiny C++ offerer advertising service +`0x1234` instance `0x0001` via SOME/IP-SD. The companion +`tests/vsomeip_sd_compat.rs` test on the host listens for that +broadcast. + +## Build + +```sh +docker build -t vsomeip-offerer tests/data/vsomeip-offerer/ +``` + +First build pulls vsomeip from upstream and compiles it in the +container — expect 5–10 minutes on a typical workstation. +Subsequent builds use Docker's layer cache. + +## Run + +First, find a multicast-capable interface IP on your host: + +```sh +ip route get 224.0.23.0 +# Expected output: +# multicast 224.0.23.0 dev wlp0s20f3 src 192.168.1.42 uid 1000 +# ^^^^^^^^^^^^ +# That last IP is what you pass below. Lo (127.0.0.1) does NOT +# work — Linux's loopback interface lacks the MULTICAST flag by +# default, so SD multicast never leaves the host. +``` + +Then launch the offerer: + +```sh +docker run --rm -d --name vsomeip-offerer --network host \ + -e VSOMEIP_UNICAST=192.168.1.42 \ + vsomeip-offerer +``` + +`--network host` is required so SD multicast (`224.0.23.0:30490`) +flows on the actual host interface. The `VSOMEIP_UNICAST` env var +gets templated into the JSON config at container start by +`entrypoint.sh`. + +Verify it's up: + +```sh +docker logs vsomeip-offerer +# Expected (debug level): "Joining to multicast group 224.0.23.0 from " +# and "OFFER(1277): [1234.0001:1.0] (true)" +``` + +## Test against it + +In another terminal: + +```sh +SIMPLE_SOMEIP_TEST_INTERFACE=192.168.1.42 \ + cargo test --features client-tokio,server-tokio \ + --test vsomeip_sd_compat -- --ignored --nocapture +``` + +Use the **same IP** you passed via `VSOMEIP_UNICAST`. Expected: +`client_sees_vsomeip_offer_service ... ok` in well under a second +once vsomeip's first SD broadcast fires (~100 ms after offer +registration, then every 1 s thereafter). + +## Stop + +```sh +docker stop vsomeip-offerer +``` + +## Files + +- `Dockerfile` — multi-stage: builds vsomeip + the offerer in stage 1, + copies the runtime artifacts into a slim runtime stage. +- `offerer.cpp` — ~50 LOC vsomeip-based offerer; calls + `application->offer_service(0x1234, 0x0001, 1, 0)` and idles + while vsomeip emits SD broadcasts. +- `CMakeLists.txt` — builds `offerer` against installed `libvsomeip3`. +- `offerer.json` — vsomeip configuration. `unicast` is templated + via `VSOMEIP_UNICAST` env var at container start (see + `entrypoint.sh`). Standard SD multicast `224.0.23.0:30490`. +- `entrypoint.sh` — substitutes `VSOMEIP_UNICAST` into the JSON + config before launching the offerer; bails loudly if the env + var isn't set. + +## Why these specific values + +- vsomeip 3.4.10: matches `LumPDK/packages/thirdparty/vsomeip/vsomeip.MODULE.bazel` + so CI conformance tests run against the same wire-version + production validation does. +- Service `0x1234` instance `0x0001`: hardcoded in both this + config and `tests/vsomeip_sd_compat.rs`. Change one, change the + other. +- Multicast `224.0.23.0:30490`: SOME/IP-SD spec default. (LumPDK's + production config uses `239.255.0.5:30491` but that's a + Luminar-network-specific choice; for the host-side conformance + test, sticking to spec defaults removes a configuration knob.) +- `unicast: "127.0.0.1"`: works under Docker host-network mode + because the host and container share the loopback interface. + For real-NIC testing, set this to the host's interface IP and + set `SIMPLE_SOMEIP_TEST_INTERFACE` to match. + +## Future (phase 20g+) + +- Wire this Dockerfile into CI via TestContainers-rs (or + equivalent) so `cargo test ... -- --ignored` runs in a + CI runner with Docker available. +- Apply LumPDK's vsomeip patches to the build (especially the + E2E Profile 5 patch) once we add E2E-conformance tests. diff --git a/tests/data/vsomeip-offerer/entrypoint.sh b/tests/data/vsomeip-offerer/entrypoint.sh new file mode 100755 index 0000000..e652115 --- /dev/null +++ b/tests/data/vsomeip-offerer/entrypoint.sh @@ -0,0 +1,31 @@ +#!/bin/sh +# Templates VSOMEIP_UNICAST into /etc/vsomeip-offerer.json then exec +# the offerer. The env var MUST be set on docker run; vsomeip 3.4.10 +# does not honor a VSOMEIP_UNICAST_ADDRESS-style env var directly, +# and `unicast: 127.0.0.1` doesn't work on Linux (lo lacks the +# MULTICAST flag, so SD multicast never reaches the wire). Pick the +# IP of an actual multicast-capable interface on the host: +# +# ip route get 224.0.23.0 +# +# returns "multicast 224.0.23.0 dev src ..." — use +# that . + +set -eu + +if [ -z "${VSOMEIP_UNICAST:-}" ]; then + echo "ERROR: set VSOMEIP_UNICAST= on docker run." 1>&2 + echo " e.g. 'docker run -e VSOMEIP_UNICAST=192.168.1.10 ...'" 1>&2 + echo " Find your interface IP via 'ip route get 224.0.23.0'." 1>&2 + exit 1 +fi + +# Templated config goes to a writable location since /etc/ in the +# image is read-only-ish from the build's COPY. +sed "s/VSOMEIP_UNICAST_PLACEHOLDER/${VSOMEIP_UNICAST}/" \ + /etc/vsomeip-offerer.json > /tmp/vsomeip-offerer.json + +export VSOMEIP_CONFIGURATION=/tmp/vsomeip-offerer.json +export VSOMEIP_APPLICATION_NAME=offerer + +exec /usr/local/bin/offerer diff --git a/tests/data/vsomeip-offerer/offerer.cpp b/tests/data/vsomeip-offerer/offerer.cpp new file mode 100644 index 0000000..197edc2 --- /dev/null +++ b/tests/data/vsomeip-offerer/offerer.cpp @@ -0,0 +1,95 @@ +// Minimal vsomeip offerer for phase-20f conformance testing. +// +// Offers service 0x1234 instance 0x0001 via vsomeip's SD subsystem. +// vsomeip emits OfferService SD broadcasts on the configured +// multicast group/port (per offerer.json's "service-discovery" +// section) until the process exits. That's the broadcast our +// `tests/vsomeip_sd_compat.rs` test on the host listens for. +// +// Hardcoded service+instance to keep this trivial; if the test's +// constants change, change them here too. See +// tests/vsomeip_sd_compat.rs:SERVICE_ID / INSTANCE_ID. + +#include + +#include +#include +#include +#include +#include + +namespace { + +constexpr vsomeip::service_t kServiceId = 0x1234; +constexpr vsomeip::instance_t kInstanceId = 0x0001; +// Major.Minor version vsomeip advertises in OfferService entries. +// Defaults; doesn't have to match anything specific test-side. +constexpr vsomeip::major_version_t kMajor = 1; +constexpr vsomeip::minor_version_t kMinor = 0; + +std::atomic g_shutdown{false}; + +void on_signal(int /*signum*/) { + g_shutdown.store(true, std::memory_order_release); +} + +} // namespace + +int main() { + std::signal(SIGINT, on_signal); + std::signal(SIGTERM, on_signal); + + auto runtime = vsomeip::runtime::get(); + if (!runtime) { + std::cerr << "[offerer] vsomeip::runtime::get() returned null" << std::endl; + return 1; + } + + // Application name matches "applications" / "routing" entries in + // offerer.json (and the VSOMEIP_APPLICATION_NAME env var the + // Dockerfile sets). vsomeip uses this to look up the routing + // configuration. + auto app = runtime->create_application("offerer"); + if (!app) { + std::cerr << "[offerer] runtime->create_application() returned null" << std::endl; + return 1; + } + + // init() reads the JSON config (VSOMEIP_CONFIGURATION) and + // registers the SD subsystem. + if (!app->init()) { + std::cerr << "[offerer] application->init() failed; " + << "check VSOMEIP_CONFIGURATION and JSON validity" << std::endl; + return 1; + } + + // Spawn vsomeip's main loop on a worker thread. start() blocks + // for the lifetime of the application; we drive it from a thread + // so this main loop can monitor the shutdown signal. + std::thread vsomeip_thread([&app]() { app->start(); }); + + // Wait for vsomeip to be ready, then advertise the service. + // 200 ms is more than enough for vsomeip's startup on any + // x86 host. + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + std::cout << "[offerer] offering service 0x" << std::hex << kServiceId + << " instance 0x" << kInstanceId + << " (major " << std::dec << static_cast(kMajor) + << ", minor " << kMinor << ")" << std::endl; + + app->offer_service(kServiceId, kInstanceId, kMajor, kMinor); + + // Spin until SIGINT/SIGTERM. vsomeip's SD subsystem emits + // periodic OfferService broadcasts in the background; we just + // need to keep the process alive. + while (!g_shutdown.load(std::memory_order_acquire)) { + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + } + + std::cout << "[offerer] shutdown requested; stopping vsomeip" << std::endl; + app->stop_offer_service(kServiceId, kInstanceId, kMajor, kMinor); + app->stop(); + vsomeip_thread.join(); + return 0; +} diff --git a/tests/data/vsomeip-offerer/offerer.json b/tests/data/vsomeip-offerer/offerer.json new file mode 100644 index 0000000..21008f5 --- /dev/null +++ b/tests/data/vsomeip-offerer/offerer.json @@ -0,0 +1,34 @@ +{ + "_comment": "vsomeip configuration for the phase-20f host-side conformance test offerer. Defaults follow the SOME/IP-SD spec (multicast 224.0.23.0:30490) so simple-someip's test-side Client picks up the broadcast without any non-default routing. The 'unicast' field is the vsomeip-side IP — 127.0.0.1 works on Linux Docker host-network mode because both sides share the loopback. For real-NIC testing, set unicast to the host interface IP and adjust the test's SIMPLE_SOMEIP_TEST_INTERFACE env var to match.", + + "_unicast_comment": "Templated at container start by entrypoint.sh from VSOMEIP_UNICAST env var. Must be a non-loopback interface IP that has the MULTICAST flag (lo doesn't on Linux by default), so SD multicast 224.0.23.0 actually leaves the host. Pass via -e VSOMEIP_UNICAST= on docker run.", + "unicast": "VSOMEIP_UNICAST_PLACEHOLDER", + "netmask": "255.255.255.0", + "logging": { + "level": "debug", + "console": "true" + }, + "applications": [ + { "name": "offerer", "id": "0x1277" } + ], + "services": [ + { + "service": "0x1234", + "instance": "0x0001", + "unreliable": "30509" + } + ], + "routing": "offerer", + "service-discovery": { + "enable": "true", + "multicast": "224.0.23.0", + "port": "30490", + "protocol": "udp", + "initial_delay_min": "10", + "initial_delay_max": "100", + "repetitions_base_delay": "200", + "repetitions_max": "3", + "ttl": "5", + "cyclic_offer_delay": "1000" + } +} diff --git a/tests/vsomeip_sd_compat.rs b/tests/vsomeip_sd_compat.rs index 3c9de94..4c47b4b 100644 --- a/tests/vsomeip_sd_compat.rs +++ b/tests/vsomeip_sd_compat.rs @@ -20,72 +20,55 @@ //! //! # Running locally //! -//! 1. Pull or build a vsomeip container. The COVESA project doesn't -//! publish a "ready-to-go" image; the simplest path is a small -//! Dockerfile around vsomeip's cmake build. The image needs -//! `routingmanagerd` (the SD daemon) plus a JSON config that -//! declares an "offerer" application with the service we want -//! advertised. Phase 20g will add a reference Dockerfile under -//! `tests/data/vsomeip-offerer/` once the manual setup is -//! proven; until then, hand-rolled is fine. -//! -//! 2. Save the config below as `vsomeip-offerer.json` and start -//! the container in host-network mode so SD multicast (224.0.23.0) -//! flows between the host and the container: +//! 1. Build the offerer image (one-time, ~5-10 min): //! //! ```text -//! docker run --rm -d \ -//! --name vsomeip-offerer \ -//! --network host \ -//! -v $(pwd)/vsomeip-offerer.json:/etc/vsomeip.json:ro \ -//! -e VSOMEIP_CONFIGURATION=/etc/vsomeip.json \ -//! -e VSOMEIP_APPLICATION_NAME=offerer \ -//! +//! docker build --network=host -t vsomeip-offerer \ +//! tests/data/vsomeip-offerer/ //! ``` //! -//! Sample vsomeip-offerer.json that offers service 0x1234 -//! instance 0x0001 over UDP port 30509: -//! -//! ```json -//! { -//! "unicast": "127.0.0.1", -//! "logging": { "level": "info", "console": "true" }, -//! "applications": [ -//! { "name": "offerer", "id": "0x1277" } -//! ], -//! "services": [ -//! { -//! "service": "0x1234", -//! "instance": "0x0001", -//! "unreliable": "30509" -//! } -//! ], -//! "routing": "offerer", -//! "service-discovery": { -//! "enable": "true", -//! "multicast": "224.0.23.0", -//! "port": "30490", -//! "protocol": "udp", -//! "initial_delay_min": "10", -//! "initial_delay_max": "100", -//! "repetitions_base_delay": "200", -//! "repetitions_max": "3", -//! "ttl": "5" -//! } -//! } +//! 2. Find a multicast-capable interface IP on your host. **Do not +//! use 127.0.0.1** — Linux's `lo` interface lacks the `MULTICAST` +//! flag by default, so SD multicast (`224.0.23.0`) never leaves +//! the host: +//! +//! ```text +//! ip route get 224.0.23.0 +//! # multicast 224.0.23.0 dev wlp0s20f3 src 192.168.1.42 ... +//! # ^^^^^^^^^^^^ +//! ``` +//! +//! The `src` IP is what you pass on both sides below. +//! +//! 3. Start the offerer (host-network mode so SD multicast flows on +//! the actual interface): +//! +//! ```text +//! docker run --rm -d --name vsomeip-offerer --network host \ +//! -e VSOMEIP_UNICAST=192.168.1.42 \ +//! vsomeip-offerer +//! ``` +//! +//! Verify it's emitting: +//! +//! ```text +//! docker logs vsomeip-offerer | grep -E "Joining|OFFER" +//! # Joining to multicast group 224.0.23.0 from 192.168.1.42 +//! # OFFER(1277): [1234.0001:1.0] (true) //! ``` //! -//! 3. Set the test's listening interface via env var to whatever IP -//! vsomeip is announcing on. For host-network Docker, that's -//! typically `127.0.0.1` (matches `unicast` in the config above): +//! 4. Run the test (use the same interface IP): //! //! ```text -//! SIMPLE_SOMEIP_TEST_INTERFACE=127.0.0.1 \ +//! SIMPLE_SOMEIP_TEST_INTERFACE=192.168.1.42 \ //! cargo test --features client-tokio,server-tokio \ //! --test vsomeip_sd_compat -- --ignored --nocapture //! ``` //! -//! 4. Tear down: `docker stop vsomeip-offerer`. +//! Expected: `client_sees_vsomeip_offer_service ... ok` in well +//! under a second. +//! +//! 5. Tear down: `docker stop vsomeip-offerer`. //! //! # Why `#[ignore]`? //! diff --git a/tools/size_probe/Cargo.toml b/tools/size_probe/Cargo.toml new file mode 100644 index 0000000..b86c585 --- /dev/null +++ b/tools/size_probe/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "size_probe" +version = "0.0.0" +edition = "2024" +publish = false + +# Phase-20-pre flash-size measurement probe. Builds a `staticlib` +# that exposes `extern "C"` shims around simple-someip's +# Option-A-relevant entry points, so post-link dead-code-elimination +# only keeps what an actual halo-style FFI consumer would call. +# +# Build: +# cargo build -p size_probe --release --target thumbv7em-none-eabihf +# +# Measure: +# llvm-size target/thumbv7em-none-eabihf/release/libsize_probe.a +# +# NOT a real production crate — exists purely to give us a flash-size +# floor on the cortex-m4f target, since we don't have access to the +# actual proxy LLVM-IR-TriCore toolchain locally. + +[lib] +name = "size_probe" +crate-type = ["staticlib"] + +[dependencies] +# `bare_metal` only — no `server` (pulls `extern crate alloc` per +# the lib.rs feature table). Codec-only FFI doesn't need server's +# Server actor or Arc-shared state. `client` would be alloc-free +# but not needed here either. Matches halo PR #4429's surface. +simple-someip = { path = "../..", default-features = false, features = ["bare_metal"] } + +[profile.release] +opt-level = "z" # optimize for size +lto = true +codegen-units = 1 +panic = "abort" +strip = "symbols" diff --git a/tools/size_probe/src/lib.rs b/tools/size_probe/src/lib.rs new file mode 100644 index 0000000..19d44ae --- /dev/null +++ b/tools/size_probe/src/lib.rs @@ -0,0 +1,203 @@ +//! Phase-20-pre flash-size measurement probe. +//! +//! Mirrors halo PR #4429's `rust_simple_someip` C-callable FFI +//! surface (header encode/decode + E2E protect/check round-trips) +//! to get a realistic post-link flash-size floor on +//! `thumbv7em-none-eabihf` for what a Halo TC4D `rust_simple_someip` +//! staticlib would cost. +//! +//! NOT production code. Exposes `#[no_mangle] extern "C"` entry +//! points only so post-link DCE keeps what an actual FFI consumer +//! would reach, and discards everything else. + +#![no_std] + +use core::alloc::{GlobalAlloc, Layout}; +use core::panic::PanicInfo; +use core::ptr; +use core::slice; + +/// Stub allocator. Some transitive dep pulls `extern crate alloc` +/// even with simple-someip's `default-features = false`, requiring a +/// `#[global_allocator]` link target. The codec-only FFI surface +/// (header encode + E2E protect/check) never actually allocates, so +/// this stub returning null on alloc is sound for the probe; if any +/// path it fronts ever does allocate, that's an explicit FFI-design +/// bug surfaced at link time, not silent corruption at runtime. +struct PanicAllocator; + +unsafe impl GlobalAlloc for PanicAllocator { + unsafe fn alloc(&self, _: Layout) -> *mut u8 { + ptr::null_mut() + } + unsafe fn dealloc(&self, _: *mut u8, _: Layout) {} +} + +#[global_allocator] +static ALLOC: PanicAllocator = PanicAllocator; + +use simple_someip::WireFormat; +use simple_someip::e2e::{ + Profile4Config, Profile4State, Profile5Config, Profile5State, check_profile4, check_profile5, + protect_profile4, protect_profile5, +}; +use simple_someip::protocol::{Header, MessageId, MessageType, MessageTypeField, ReturnCode}; + +/// Required for no_std staticlib targeting thumbv7em. +#[panic_handler] +fn panic(_: &PanicInfo) -> ! { + loop {} +} + +// ── SOME/IP header encode ─────────────────────────────────────────── + +#[repr(C)] +pub struct CSomeIpHeader { + pub service_id: u16, + pub method_id: u16, + pub length: u32, + pub client_id: u16, + pub session_id: u16, + pub protocol_version: u8, + pub interface_version: u8, + pub message_type: u8, + pub return_code: u8, +} + +/// # Safety +/// Caller must ensure `header` points to a valid `CSomeIpHeader` and +/// `buf` points to at least `buf_len` writable bytes. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn someip_header_encode( + header: *const CSomeIpHeader, + buf: *mut u8, + buf_len: usize, +) -> usize { + if header.is_null() || buf.is_null() || buf_len < 16 { + return 0; + } + let h = unsafe { &*header }; + let message_id = MessageId::new_from_service_and_method(h.service_id, h.method_id); + let request_id = (u32::from(h.client_id) << 16) | u32::from(h.session_id); + let Ok(msg_type_raw) = MessageType::try_from(h.message_type & 0xBF) else { + return 0; + }; + let msg_type = MessageTypeField::new(msg_type_raw, (h.message_type & 0x20) != 0); + let Ok(ret_code) = ReturnCode::try_from(h.return_code) else { + return 0; + }; + let header = Header::new( + message_id, + request_id, + h.protocol_version, + h.interface_version, + msg_type, + ret_code, + 0, + ); + let out = unsafe { slice::from_raw_parts_mut(buf, buf_len) }; + header.encode(&mut &mut out[..]).unwrap_or(0) +} + +// ── E2E Profile 4 protect + check ─────────────────────────────────── + +#[repr(C)] +pub struct E2eRoundTripResult { + pub ok: i32, + pub protected_len: u32, + pub check_status: u8, + pub counter: u32, + pub payload_match: i32, +} + +/// # Safety +/// Caller must ensure `payload` points to at least `payload_len` +/// readable bytes. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn e2e_profile4_round_trip( + payload: *const u8, + payload_len: usize, + initial_counter: u16, +) -> E2eRoundTripResult { + let mut out = E2eRoundTripResult { + ok: 0, + protected_len: 0, + check_status: 0, + counter: 0, + payload_match: 0, + }; + if payload.is_null() { + return out; + } + let payload = unsafe { slice::from_raw_parts(payload, payload_len) }; + + let config = Profile4Config::new(0x1234_5678, 15); + let mut protect_state = Profile4State::with_initial_counter(initial_counter); + + // Probe-only stack buffer; production code uses caller-supplied storage. + let mut buf = [0u8; 1500]; + if buf.len() < payload_len + 12 { + return out; + } + let Ok(protected_len) = protect_profile4(&config, &mut protect_state, payload, &mut buf) else { + return out; + }; + + let mut check_state = Profile4State::with_initial_counter(initial_counter); + let result = check_profile4(&config, &mut check_state, &buf[..protected_len]); + + out.ok = 1; + out.protected_len = protected_len as u32; + out.check_status = result.status as u8; + out.counter = result.counter.unwrap_or(0); + out.payload_match = i32::from(result.payload == Some(payload)); + out +} + +// ── E2E Profile 5 protect + check ─────────────────────────────────── + +/// # Safety +/// Caller must ensure `payload` points to at least `payload_len` +/// readable bytes. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn e2e_profile5_round_trip( + payload: *const u8, + payload_len: usize, + initial_counter: u16, +) -> E2eRoundTripResult { + let mut out = E2eRoundTripResult { + ok: 0, + protected_len: 0, + check_status: 0, + counter: 0, + payload_match: 0, + }; + if payload.is_null() { + return out; + } + let payload = unsafe { slice::from_raw_parts(payload, payload_len) }; + + let Ok(payload_len_u16) = u16::try_from(payload_len) else { + return out; + }; + let config = Profile5Config::new(0x1234, payload_len_u16, 15); + let mut protect_state = Profile5State::with_initial_counter((initial_counter & 0xFF) as u8); + + let mut buf = [0u8; 1500]; + if buf.len() < payload_len + 4 { + return out; + } + let Ok(protected_len) = protect_profile5(&config, &mut protect_state, payload, &mut buf) else { + return out; + }; + + let mut check_state = Profile5State::with_initial_counter((initial_counter & 0xFF) as u8); + let result = check_profile5(&config, &mut check_state, &buf[..protected_len]); + + out.ok = 1; + out.protected_len = protected_len as u32; + out.check_status = result.status as u8; + out.counter = result.counter.unwrap_or(0); + out.payload_match = i32::from(result.payload == Some(payload)); + out +}