Skip to main content

simple_someip/e2e/
registry.rs

1//! E2E configuration registry for runtime E2E management.
2//!
3//! Backed by [`heapless::index_map::FnvIndexMap`] so the registry is
4//! `no_std`-compatible and allocates no heap memory after construction.
5//! The capacity is bounded at compile time to [`E2E_REGISTRY_CAP`]; the
6//! registry rejects further registrations once that cap is reached
7//! rather than silently dropping or growing — see [`E2ERegistry::register`]
8//! and [`E2ERegistryFull`].
9
10use core::net::IpAddr;
11
12use heapless::index_map::{Entry, FnvIndexMap};
13
14use super::{E2ECheckStatus, E2EKey, E2EProfile, E2EState, Error, e2e_check, e2e_protect};
15
16/// Maximum number of distinct `(key → profile)` bindings the registry
17/// can hold. Sized for typical workloads where a single service
18/// instance has at most a few dozen E2E-protected message types.
19///
20/// Must be a power of two for [`FnvIndexMap`]; the `const _` assertion
21/// below catches any future change that would violate the requirement.
22pub const E2E_REGISTRY_CAP: usize = 32;
23
24const _: () = assert!(
25    E2E_REGISTRY_CAP.is_power_of_two(),
26    "E2E_REGISTRY_CAP must be a power of two for heapless::FnvIndexMap"
27);
28
29/// Maximum number of distinct `(source, key)` **receive** counter slots
30/// the registry can hold at once.
31///
32/// On a shared subnet the receive state is keyed per source (see
33/// [`E2ERegistry`]), so this bounds *sources × keys*, not just keys —
34/// size it for the high-water mark of distinct senders the node expects
35/// to demux concurrently. Once full, [`E2ERegistry::check`] still runs
36/// (CRC is always validated) but a brand-new source falls back to a
37/// transient per-call counter, so its *sequence* continuity is not
38/// tracked until a slot frees via [`E2ERegistry::reset_source`] /
39/// [`E2ERegistry::unregister`]. A one-shot `warn!` fires the first time
40/// this happens.
41///
42/// Must be a power of two for [`FnvIndexMap`].
43pub const E2E_RX_STATE_CAP: usize = 64;
44
45const _: () = assert!(
46    E2E_RX_STATE_CAP.is_power_of_two(),
47    "E2E_RX_STATE_CAP must be a power of two for heapless::FnvIndexMap"
48);
49
50/// Returned by [`E2ERegistry::register`] when the registry is at
51/// capacity.
52///
53/// The contained value is the cap that was hit (i.e.
54/// [`E2E_REGISTRY_CAP`]); kept in the error so log lines and panic
55/// messages name the constant the user can adjust.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
57#[error("e2e registry at capacity ({0})")]
58pub struct E2ERegistryFull(pub usize);
59
60/// Registry mapping message keys to E2E profile configurations and the
61/// per-source / per-key counter state.
62///
63/// On a shared subnet several devices send the same `(service, method)` under
64/// the same fixed instance id. The profile *configuration* is endpoint-agnostic
65/// (one per [`E2EKey`]), but the **receive** counter state must be independent
66/// per device — otherwise two senders' interleaved counters collide into
67/// spurious `WrongSequence` results. Receive state is therefore keyed by
68/// `(source, key)` and created lazily the first time a source is seen.
69///
70/// Transmit (protect) counter state stays per-key: a fan-out publish sends the
71/// same protected bytes (one counter) to every subscriber, and per-recipient
72/// transmit counters are handled a layer up (e.g. `iris_someip_client`).
73///
74/// `no_std`-friendly: every map is a fixed-capacity [`FnvIndexMap`], so
75/// construction and the entire lifetime of the registry are heap-free.
76/// Construction is `const`, so a `static` instance can be declared in
77/// firmware boot code. Profile/transmit slots are bounded by
78/// [`E2E_REGISTRY_CAP`]; receive slots by [`E2E_RX_STATE_CAP`].
79#[derive(Debug)]
80pub struct E2ERegistry {
81    /// Endpoint-agnostic profile configuration, keyed by data element.
82    configs: FnvIndexMap<E2EKey, E2EProfile, E2E_REGISTRY_CAP>,
83    /// Receive counter state, per `(source, key)`.
84    rx_states: FnvIndexMap<(IpAddr, E2EKey), E2EState, E2E_RX_STATE_CAP>,
85    /// Transmit counter state, per key.
86    tx_states: FnvIndexMap<E2EKey, E2EState, E2E_REGISTRY_CAP>,
87    /// Latches the one-shot `warn!` emitted when `rx_states` first
88    /// saturates, so an over-capacity subnet doesn't flood the logs.
89    rx_saturation_warned: bool,
90}
91
92impl E2ERegistry {
93    /// Create an empty registry. `const`-constructible so it can live
94    /// in `static` storage on bare-metal targets.
95    #[must_use]
96    pub const fn new() -> Self {
97        Self {
98            configs: FnvIndexMap::new(),
99            rx_states: FnvIndexMap::new(),
100            tx_states: FnvIndexMap::new(),
101            rx_saturation_warned: false,
102        }
103    }
104
105    /// Register an E2E profile for the given key, creating fresh transmit
106    /// state and clearing any prior per-source receive state for the key.
107    ///
108    /// Replacing the profile of an already-registered key always
109    /// succeeds (the existing slots are reused). Adding a new key when
110    /// the registry already holds [`E2E_REGISTRY_CAP`] entries returns
111    /// [`Err(E2ERegistryFull)`](E2ERegistryFull); the caller is
112    /// responsible for sizing the cap to its workload's high-water
113    /// mark.
114    ///
115    /// # Errors
116    ///
117    /// [`E2ERegistryFull`] when the registry is full and `key` is not
118    /// already present.
119    pub fn register(&mut self, key: E2EKey, profile: E2EProfile) -> Result<(), E2ERegistryFull> {
120        let state = E2EState::from_profile(&profile);
121        // `FnvIndexMap::insert` returns `Err((K, V))` only when the map is
122        // full AND `key` is not already present (replacing an existing
123        // entry never overflows). `configs` and `tx_states` share both the
124        // key set and `E2E_REGISTRY_CAP`, so we gate on `configs` first and
125        // the `tx_states` insert below can only ever replace-in-place.
126        if self.configs.insert(key, profile).is_err() {
127            return Err(E2ERegistryFull(E2E_REGISTRY_CAP));
128        }
129        let _ = self.tx_states.insert(key, state);
130        // A re-register restarts the counter, so drop stale per-source
131        // receive state for this key.
132        self.rx_states.retain(|(_, k), _| *k != key);
133        Ok(())
134    }
135
136    /// Remove E2E configuration (and all state) for the given key.
137    pub fn unregister(&mut self, key: &E2EKey) {
138        self.configs.remove(key);
139        self.tx_states.remove(key);
140        self.rx_states.retain(|(_, k), _| k != key);
141    }
142
143    /// Returns `true` if a profile is registered for `key`.
144    #[must_use]
145    pub fn contains_key(&self, key: &E2EKey) -> bool {
146        self.configs.contains_key(key)
147    }
148
149    /// Run E2E check for `key` against `source`'s receive counter state, if
150    /// configured.
151    ///
152    /// Returns `None` if no profile is registered for `key`. Otherwise returns
153    /// the check status and the best available payload (stripped E2E header on
154    /// success, original bytes on check failure).
155    pub fn check<'a>(
156        &mut self,
157        source: IpAddr,
158        key: E2EKey,
159        payload: &'a [u8],
160        upper_header: [u8; 8],
161    ) -> Option<(E2ECheckStatus, &'a [u8])> {
162        let profile = self.configs.get(&key)?;
163        // Per-source receive state, created lazily the first time a
164        // `(source, key)` pair is seen. When `rx_states` is at
165        // [`E2E_RX_STATE_CAP`] a brand-new source can't claim a slot; fall
166        // back to a transient counter so the CRC is still validated (only
167        // sequence continuity is lost) and warn once.
168        match self.rx_states.entry((source, key)) {
169            Entry::Occupied(occupied) => {
170                let state = occupied.into_mut();
171                Some(e2e_check(profile, state, payload, upper_header))
172            }
173            Entry::Vacant(vacant) => match vacant.insert(E2EState::from_profile(profile)) {
174                Ok(state) => Some(e2e_check(profile, state, payload, upper_header)),
175                Err(_full) => {
176                    if !self.rx_saturation_warned {
177                        self.rx_saturation_warned = true;
178                        crate::log::warn!(
179                            "E2E rx_states at capacity ({}); source {} falls back to a \
180                             transient counter — sequence continuity untracked until a slot frees",
181                            E2E_RX_STATE_CAP,
182                            source
183                        );
184                    }
185                    let mut transient = E2EState::from_profile(profile);
186                    Some(e2e_check(profile, &mut transient, payload, upper_header))
187                }
188            },
189        }
190    }
191
192    /// Run E2E protect for `key` if configured.
193    ///
194    /// Returns `None` if no profile is registered for `key`.
195    pub fn protect(
196        &mut self,
197        key: E2EKey,
198        payload: &[u8],
199        upper_header: [u8; 8],
200        output: &mut [u8],
201    ) -> Option<Result<usize, Error>> {
202        let profile = self.configs.get(&key)?;
203        let state = self.tx_states.get_mut(&key)?;
204        Some(e2e_protect(profile, state, payload, upper_header, output))
205    }
206
207    /// Drop all per-source receive state for `source` (e.g. on its reboot), so
208    /// its next frame starts a fresh counter sequence. Configuration and
209    /// transmit state are untouched.
210    pub fn reset_source(&mut self, source: IpAddr) {
211        self.rx_states.retain(|(s, _), _| *s != source);
212    }
213}
214
215impl Default for E2ERegistry {
216    fn default() -> Self {
217        Self::new()
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224    use crate::e2e::{Profile4Config, Profile5Config};
225    use core::net::Ipv4Addr;
226
227    fn make_key() -> E2EKey {
228        E2EKey::new(0x1234, 0x5678)
229    }
230
231    fn src() -> IpAddr {
232        IpAddr::V4(Ipv4Addr::LOCALHOST)
233    }
234
235    fn make_profile5() -> E2EProfile {
236        E2EProfile::Profile5(Profile5Config::new(0x1234, 20, 15))
237    }
238
239    /// Protect a 20-byte "Hello" frame with `sender`'s next transmit counter,
240    /// writing into `out` and returning the protected length. Avoids `Vec`
241    /// because the crate's prelude is `core` (no_std-compatible).
242    fn protect_next(sender: &mut E2ERegistry, key: E2EKey, out: &mut [u8; 64]) -> usize {
243        let mut payload = [0u8; 20];
244        payload[..5].copy_from_slice(b"Hello");
245        sender.protect(key, &payload, [0; 8], out).unwrap().unwrap()
246    }
247
248    #[test]
249    fn register_and_check_profile4() {
250        let mut reg = E2ERegistry::new();
251        let key = make_key();
252        let config = Profile4Config::new(0x12345678, 15);
253        reg.register(key, E2EProfile::Profile4(config.clone()))
254            .expect("register fits within E2E_REGISTRY_CAP");
255        assert!(reg.contains_key(&key));
256
257        // Protect a payload
258        let payload = b"Hello";
259        let mut out = [0u8; 64];
260        let len = reg
261            .protect(key, payload, [0; 8], &mut out)
262            .unwrap()
263            .unwrap();
264
265        // Check it
266        let (status, stripped) = reg.check(src(), key, &out[..len], [0; 8]).unwrap();
267        assert_eq!(status, E2ECheckStatus::Ok);
268        assert_eq!(stripped, payload);
269    }
270
271    #[test]
272    fn register_and_check_profile5() {
273        let mut reg = E2ERegistry::new();
274        let key = make_key();
275        reg.register(key, make_profile5())
276            .expect("register fits within E2E_REGISTRY_CAP");
277
278        let mut payload = [0u8; 20];
279        payload[..5].copy_from_slice(b"Hello");
280        let mut out = [0u8; 64];
281        let len = reg
282            .protect(key, &payload, [0; 8], &mut out)
283            .unwrap()
284            .unwrap();
285
286        let (status, stripped) = reg.check(src(), key, &out[..len], [0; 8]).unwrap();
287        assert_eq!(status, E2ECheckStatus::Ok);
288        assert_eq!(stripped, &payload);
289    }
290
291    #[test]
292    fn distinct_sources_have_independent_e2e_state() {
293        let a = IpAddr::V4(Ipv4Addr::new(192, 168, 11, 101));
294        let b = IpAddr::V4(Ipv4Addr::new(192, 168, 11, 102));
295        let key = make_key();
296
297        // A sender produces two frames carrying counters 0 then 1.
298        let mut sender = E2ERegistry::new();
299        sender
300            .register(key, make_profile5())
301            .expect("register fits within E2E_REGISTRY_CAP");
302        let mut b0 = [0u8; 64];
303        let l0 = protect_next(&mut sender, key, &mut b0);
304        let mut b1 = [0u8; 64];
305        let l1 = protect_next(&mut sender, key, &mut b1);
306
307        let mut recv = E2ERegistry::new();
308        recv.register(key, make_profile5())
309            .expect("register fits within E2E_REGISTRY_CAP");
310
311        // Source A consumes counters 0 then 1.
312        assert_eq!(
313            recv.check(a, key, &b0[..l0], [0; 8]).unwrap().0,
314            E2ECheckStatus::Ok
315        );
316        assert_eq!(
317            recv.check(a, key, &b1[..l1], [0; 8]).unwrap().0,
318            E2ECheckStatus::Ok
319        );
320        // Source B, interleaved AFTER A, starts its own counter sequence at 0.
321        // With shared (per-key) receive state this would flag b0 as
322        // out-of-sequence because A already advanced the single counter past 0.
323        assert_eq!(
324            recv.check(b, key, &b0[..l0], [0; 8]).unwrap().0,
325            E2ECheckStatus::Ok,
326            "source B's receive counter must be independent of source A's"
327        );
328        assert_eq!(
329            recv.check(b, key, &b1[..l1], [0; 8]).unwrap().0,
330            E2ECheckStatus::Ok
331        );
332    }
333
334    #[test]
335    fn reset_source_clears_only_that_source() {
336        let a = IpAddr::V4(Ipv4Addr::new(192, 168, 11, 101));
337        let key = make_key();
338
339        let mut sender = E2ERegistry::new();
340        sender
341            .register(key, make_profile5())
342            .expect("register fits within E2E_REGISTRY_CAP");
343        let mut b0 = [0u8; 64];
344        let l0 = protect_next(&mut sender, key, &mut b0);
345        let mut b1 = [0u8; 64];
346        let l1 = protect_next(&mut sender, key, &mut b1);
347
348        let mut recv = E2ERegistry::new();
349        recv.register(key, make_profile5())
350            .expect("register fits within E2E_REGISTRY_CAP");
351        recv.check(a, key, &b0[..l0], [0; 8]);
352        recv.check(a, key, &b1[..l1], [0; 8]);
353
354        // After a reboot, source A starts fresh — its counter-0 frame is Ok
355        // again.
356        recv.reset_source(a);
357        assert_eq!(
358            recv.check(a, key, &b0[..l0], [0; 8]).unwrap().0,
359            E2ECheckStatus::Ok,
360            "reset_source(a) restarts A's receive counter sequence"
361        );
362    }
363
364    #[test]
365    fn unregistered_key_returns_none() {
366        let mut reg = E2ERegistry::new();
367        let key = make_key();
368        assert!(!reg.contains_key(&key));
369        assert!(reg.check(src(), key, b"test", [0; 8]).is_none());
370        assert!(reg.protect(key, b"test", [0; 8], &mut [0; 64]).is_none());
371    }
372
373    #[test]
374    fn unregister_removes_key() {
375        let mut reg = E2ERegistry::new();
376        let key = make_key();
377        reg.register(key, E2EProfile::Profile4(Profile4Config::new(0, 15)))
378            .expect("register fits within E2E_REGISTRY_CAP");
379        assert!(reg.contains_key(&key));
380        reg.unregister(&key);
381        assert!(!reg.contains_key(&key));
382    }
383
384    #[test]
385    fn default_is_empty() {
386        let reg = E2ERegistry::default();
387        assert!(!reg.contains_key(&make_key()));
388    }
389
390    /// Replacing the profile of an already-registered key MUST succeed
391    /// even when the registry is at capacity — the slot is reused, not
392    /// added. Regression guard for the FnvIndexMap "full + missing key"
393    /// branch.
394    #[test]
395    fn register_replacement_succeeds_when_full() {
396        let mut reg = E2ERegistry::new();
397        for i in 0..E2E_REGISTRY_CAP {
398            let key = E2EKey::new(0x1000 + u16::try_from(i).unwrap(), 0);
399            reg.register(key, E2EProfile::Profile4(Profile4Config::new(0, 15)))
400                .expect("filling to cap");
401        }
402        // Re-register the first key with a different profile — must succeed.
403        let key0 = E2EKey::new(0x1000, 0);
404        let result = reg.register(key0, E2EProfile::Profile4(Profile4Config::new(42, 15)));
405        assert!(
406            result.is_ok(),
407            "replacing an existing entry must succeed even at capacity"
408        );
409    }
410
411    /// Adding a new key beyond the cap MUST return
412    /// `Err(E2ERegistryFull(E2E_REGISTRY_CAP))` and leave the registry
413    /// otherwise unchanged. Regression test that locks in the
414    /// capacity contract documented on `register`.
415    #[test]
416    fn register_overflow_returns_err_and_does_not_mutate() {
417        let mut reg = E2ERegistry::new();
418        for i in 0..E2E_REGISTRY_CAP {
419            reg.register(
420                E2EKey::new(0x2000 + u16::try_from(i).unwrap(), 0),
421                E2EProfile::Profile4(Profile4Config::new(0, 15)),
422            )
423            .expect("filling to cap");
424        }
425        // The (cap+1)-th distinct key must be rejected.
426        let overflow_key = E2EKey::new(0xFFFE, 0);
427        let err = reg
428            .register(
429                overflow_key,
430                E2EProfile::Profile4(Profile4Config::new(0, 15)),
431            )
432            .expect_err("registering the (cap+1)-th key must overflow");
433        assert_eq!(err, E2ERegistryFull(E2E_REGISTRY_CAP));
434        // And the rejected key must NOT be present.
435        assert!(!reg.contains_key(&overflow_key));
436    }
437}