Skip to main content

simple_someip/
transport.rs

1//! Executor-agnostic transport abstraction.
2//!
3//! [`TransportSocket`] is the minimum UDP surface `simple-someip` needs from
4//! its networking backend: unicast and multicast send/recv plus a few
5//! socket-level knobs. [`TransportFactory`] constructs bound and configured
6//! sockets at startup. [`Timer`] provides async sleep.
7//!
8//! # Why a trait, and why like this
9//!
10//! The crate's `client` and `server` modules today use a tokio-based UDP
11//! backend, with sockets created/configured via `socket2` (for reuse /
12//! multicast-interface / multicast-loop options) and then handed off as
13//! `tokio::net::UdpSocket` for the async I/O loop. That works on
14//! `std + tokio` but makes no-`std` / non-tokio embedded use impossible.
15//! These traits are the integration point for alternative backends (lwIP,
16//! smoltcp, etc.).
17//!
18//! Three explicit design choices:
19//!
20//! 1. **Executor-agnostic for socket / timer I/O.** [`TransportSocket`]
21//!    and [`Timer`] methods return `impl Future`, not `async fn`, and
22//!    those traits make no statement about `Send` or `'static` bounds on
23//!    their returned futures. Callers that need those bounds (e.g. to
24//!    `tokio::spawn`) require them at the consumer site. Bare-metal
25//!    callers driving the future on a single executor task pay no `Send`
26//!    tax for socket I/O. **[`Spawner::spawn`] is the deliberate
27//!    exception:** it is a multi-task abstraction by definition, so it
28//!    requires `Send + 'static` on its argument. Single-core executors
29//!    that need a `!Send` variant (embassy with `task_arena_size = 0`,
30//!    `LocalSet`-style models) need either a future `spawn_local` shim
31//!    or a hand-rolled adapter; the `Send + 'static` bound is documented
32//!    on the trait method itself.
33//! 2. **IPv4-only address type.** This transport abstraction currently
34//!    uses [`core::net::SocketAddrV4`] directly rather than `SocketAddr`,
35//!    matching the crate's present transport-layer reach for unicast and
36//!    the standard SD IPv4 multicast address
37//!    ([`crate::protocol::sd::MULTICAST_IP`], `239.255.0.255`). This
38//!    saves every backend from writing a `SocketAddr::V6(_) =>
39//!    Unsupported` arm, and documents the crate's actual reach at this
40//!    layer. (The protocol layer parses IPv6 SD option endpoints too;
41//!    only the transport bind / send is IPv4-today.)
42//! 3. **No object safety.** Because `impl Future` is used in method return
43//!    positions, the traits cannot be made into trait objects
44//!    (`Box<dyn TransportSocket>` will not compile). This is intentional:
45//!    there is exactly one transport implementation per build, selected at
46//!    compile time, and monomorphization eliminates any dispatch overhead.
47//!    Consumers carry a generic `<T: TransportSocket>`.
48//!
49//! # `Send` and multithreaded executors
50//!
51//! Neither [`TransportSocket`] nor [`Timer`] method signatures require
52//! their returned futures to be `Send`. This is on purpose: single-threaded
53//! executors (embassy, smol's `LocalSet`, and any bare-metal task loop)
54//! benefit from the relaxation and can hold `!Send` state across yield
55//! points.
56//!
57//! Implementations targeting multithreaded executors such as `tokio::spawn`
58//! are expected to produce `Send + 'static` futures in practice. Consumers
59//! that require `Send` should enforce it through how they use the
60//! transport, not by naming the hidden future type returned by the trait
61//! methods — with RPITIT that type is anonymous and cannot be named, and
62//! there is no `TransportSocketSendFut`-style associated-type escape
63//! hatch here. Instead, wrap the call in an `async move` block and
64//! require `T: Send + 'static` on the captured state:
65//!
66//! ```ignore
67//! fn spawn_loop<T>(sock: T)
68//! where
69//!     T: TransportSocket + Send + 'static,
70//! {
71//!     tokio::spawn(async move {
72//!         let mut sock = sock;
73//!         /* use sock here */
74//!     });
75//! }
76//! ```
77//!
78//! A tokio-backed implementation where the underlying `UdpSocket` is
79//! already `Send + Sync` will produce `Send` futures automatically via
80//! `async` block capture inference, so the pattern above works without
81//! any extra trait-level future bound. Implementations that hold
82//! `!Send` state internally simply won't satisfy the `T: Send` bound
83//! — the compiler catches the mismatch at the `tokio::spawn` call
84//! site rather than inside the trait definition.
85//!
86//! # Status
87//!
88//! A default `std + tokio` implementation
89//! (`crate::tokio_transport::TokioTransport`,
90//! `crate::tokio_transport::TokioSocket`, `crate::tokio_transport::TokioTimer`)
91//! ships under the `client` and `server` features and is re-exported at the
92//! crate root. The paths are rendered as code literals rather than
93//! intra-doc links because the `tokio_transport` module is feature-gated,
94//! and links would otherwise break default-feature rustdoc builds. Other
95//! backends (for example `smoltcp::UdpSocket` + `embassy-time` on embedded)
96//! are the consumer's responsibility — the traits here are the integration
97//! point.
98//!
99//! # Minimal adapter sketch
100//!
101//! ```
102//! # #[cfg(feature = "client-tokio")]
103//! # fn wrapper() {
104//! use core::future::Future;
105//! use core::net::{Ipv4Addr, SocketAddrV4};
106//! use core::pin::Pin;
107//! use core::time::Duration;
108//! use simple_someip::transport::{
109//!     IoErrorKind, ReceivedDatagram, SocketOptions, Timer, TransportError,
110//!     TransportFactory, TransportSocket,
111//! };
112//!
113//! // A boxed future alias keeps this sketch short without pulling in the
114//! // `futures` crate (the engine itself depends only on `futures-util`).
115//! type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
116//!
117//! struct TokioTransport;
118//!
119//! struct TokioSocket {
120//!     inner: tokio::net::UdpSocket,
121//! }
122//!
123//! impl TransportFactory for TokioTransport {
124//!     type Socket = TokioSocket;
125//!     type BindFuture<'a> = BoxFuture<'a, Result<Self::Socket, TransportError>>;
126//!     fn bind<'a>(
127//!         &'a self,
128//!         addr: SocketAddrV4,
129//!         _options: &'a SocketOptions,
130//!     ) -> Self::BindFuture<'a> {
131//!         Box::pin(async move {
132//!             let inner = tokio::net::UdpSocket::bind(addr)
133//!                 .await
134//!                 .map_err(|_| TransportError::Io(IoErrorKind::Other))?;
135//!             Ok(TokioSocket { inner })
136//!         })
137//!     }
138//! }
139//!
140//! impl TransportSocket for TokioSocket {
141//!     // `BoxFuture` keeps this sketch short. The real `TokioSocket`
142//!     // shipped under the `client` / `server` features uses named
143//!     // future structs that wrap `poll_send_to` / `poll_recv_from`
144//!     // for zero-allocation per datagram — see `tokio_transport.rs`.
145//!     type SendFuture<'a> = BoxFuture<'a, Result<(), TransportError>>;
146//!     type RecvFuture<'a> = BoxFuture<'a, Result<ReceivedDatagram, TransportError>>;
147//!
148//!     fn send_to<'a>(
149//!         &'a self,
150//!         buf: &'a [u8],
151//!         target: SocketAddrV4,
152//!     ) -> Self::SendFuture<'a> {
153//!         Box::pin(async move {
154//!             self.inner
155//!                 .send_to(buf, target)
156//!                 .await
157//!                 .map(|_| ())
158//!                 .map_err(|_| TransportError::Io(IoErrorKind::Other))
159//!         })
160//!     }
161//!     fn recv_from<'a>(
162//!         &'a self,
163//!         buf: &'a mut [u8],
164//!     ) -> Self::RecvFuture<'a> {
165//!         Box::pin(async move {
166//!             let (n, src) = self
167//!                 .inner
168//!                 .recv_from(buf)
169//!                 .await
170//!                 .map_err(|_| TransportError::Io(IoErrorKind::Other))?;
171//!             let source = match src {
172//!                 std::net::SocketAddr::V4(v4) => v4,
173//!                 std::net::SocketAddr::V6(_) => return Err(TransportError::Unsupported),
174//!             };
175//!             Ok(ReceivedDatagram {
176//!                 bytes_received: n,
177//!                 source,
178//!                 truncated: false,
179//!             })
180//!         })
181//!     }
182//!     fn local_addr(&self) -> Result<SocketAddrV4, TransportError> {
183//!         match self.inner.local_addr() {
184//!             Ok(std::net::SocketAddr::V4(v4)) => Ok(v4),
185//!             Ok(_) => Err(TransportError::Unsupported),
186//!             Err(_) => Err(TransportError::Io(IoErrorKind::Other)),
187//!         }
188//!     }
189//!     fn join_multicast_v4(
190//!         &self,
191//!         group: Ipv4Addr,
192//!         iface: Ipv4Addr,
193//!     ) -> Result<(), TransportError> {
194//!         self.inner
195//!             .join_multicast_v4(group, iface)
196//!             .map_err(|_| TransportError::Io(IoErrorKind::Other))
197//!     }
198//!     fn leave_multicast_v4(
199//!         &self,
200//!         group: Ipv4Addr,
201//!         iface: Ipv4Addr,
202//!     ) -> Result<(), TransportError> {
203//!         self.inner
204//!             .leave_multicast_v4(group, iface)
205//!             .map_err(|_| TransportError::Io(IoErrorKind::Other))
206//!     }
207//! }
208//!
209//! struct TokioTimer;
210//! impl Timer for TokioTimer {
211//!     // `tokio::time::Sleep` is `!Send`; box it behind a non-`Send`
212//!     // future so this sketch stays backend-agnostic.
213//!     type SleepFuture<'a> = Pin<Box<dyn Future<Output = ()> + 'a>>;
214//!     fn sleep(&self, duration: Duration) -> Self::SleepFuture<'_> {
215//!         Box::pin(tokio::time::sleep(duration))
216//!     }
217//! }
218//! # }
219//! ```
220//!
221//! # Lifecycle
222//!
223//! Sockets are dropped to close. There is no explicit `shutdown` method —
224//! implementations should release kernel / stack resources in `Drop`.
225//! Implementations that need graceful shutdown (flushing an outgoing queue,
226//! for example) should perform it in `Drop` or expose an inherent method
227//! outside this trait.
228
229use core::future::Future;
230use core::net::{IpAddr, Ipv4Addr, SocketAddrV4};
231use core::time::Duration;
232
233use crate::e2e::Error as E2EError;
234use crate::e2e::{E2ECheckStatus, E2EKey, E2EProfile};
235
236/// Portable I/O error kinds surfaced by transport implementations.
237///
238/// This is a deliberately small vocabulary — anything that does not fit
239/// maps to [`IoErrorKind::Other`]. The enum is `#[non_exhaustive]` so new
240/// kinds can be added without a breaking change. Kept local to this crate
241/// (rather than re-exporting `embedded_io::ErrorKind`) so our public API
242/// does not move when `embedded_io` bumps major versions.
243#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
244#[non_exhaustive]
245pub enum IoErrorKind {
246    /// The operation timed out.
247    #[error("operation timed out")]
248    TimedOut,
249    /// The operation was interrupted and can be retried.
250    #[error("operation interrupted")]
251    Interrupted,
252    /// The caller lacks permission for the operation.
253    #[error("permission denied")]
254    PermissionDenied,
255    /// A remote peer actively refused the connection / destination was
256    /// unreachable.
257    #[error("connection refused")]
258    ConnectionRefused,
259    /// The network layer rejected the operation (routing, MTU, etc.).
260    #[error("network unreachable")]
261    NetworkUnreachable,
262    /// A non-blocking call would have blocked. Transient — caller
263    /// should retry or wait for readiness rather than treating as
264    /// fatal.
265    #[error("would block")]
266    WouldBlock,
267    /// An inbound datagram was truncated because it exceeded the receive
268    /// buffer. The datagram is discarded; the socket loop survives.
269    ///
270    /// Backends that receive this signal MUST drop the datagram and continue
271    /// polling — it does NOT count toward the consecutive-error kill cap.
272    /// This variant is distinct from [`Self::Other`] so that genuine I/O
273    /// errors are still counted as potentially-fatal.
274    #[error("inbound datagram truncated (exceeded buffer)")]
275    Truncated,
276    /// Any error that does not fit a more specific variant.
277    #[error("i/o error")]
278    Other,
279}
280
281impl IoErrorKind {
282    /// Returns `true` if a recv-loop error of this kind is a transient
283    /// condition that should not count toward a "kill the loop after N
284    /// consecutive errors" cap. Includes:
285    /// - [`Self::ConnectionRefused`] — a peer's ICMP port-unreachable
286    ///   reply is normal noise on a SOME/IP host that probes services
287    ///   that are not yet available;
288    /// - [`Self::NetworkUnreachable`] — a routing blip during
289    ///   interface migration is recoverable;
290    /// - [`Self::WouldBlock`] — by definition, retry-on-readiness;
291    /// - [`Self::Interrupted`] — a signal interrupted the syscall;
292    /// - [`Self::TimedOut`] — caller-driven timeout, not a socket
293    ///   failure;
294    /// - [`Self::Truncated`] — an inbound datagram was truncated because
295    ///   it exceeded the receive buffer; the datagram is dropped and the
296    ///   loop continues (distinct from [`Self::Other`] so genuine I/O
297    ///   errors are still counted as potentially-fatal).
298    ///
299    /// All other kinds (including [`Self::Other`]) are treated as
300    /// potentially-fatal and DO count toward the cap.
301    #[must_use]
302    pub fn is_transient_recv(self) -> bool {
303        matches!(
304            self,
305            Self::ConnectionRefused
306                | Self::NetworkUnreachable
307                | Self::WouldBlock
308                | Self::Interrupted
309                | Self::TimedOut
310                | Self::Truncated,
311        )
312    }
313}
314
315/// Errors returned by [`TransportSocket`] and [`TransportFactory`]
316/// operations.
317///
318/// `#[non_exhaustive]` so that backend-specific conditions can be added in
319/// future releases without a breaking change. Implementations map their
320/// native error types into one of these variants; anything that does not
321/// fit a specific variant should use [`TransportError::Io`] with an
322/// appropriate [`IoErrorKind`].
323#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
324#[non_exhaustive]
325pub enum TransportError {
326    /// Bind failed because the address or port is already in use.
327    #[error("address in use")]
328    AddressInUse,
329    /// The operation is not supported by this transport (for example,
330    /// multicast on a backend that has none, or an IPv6 address on an
331    /// IPv4-only stack).
332    #[error("unsupported transport operation")]
333    Unsupported,
334    /// A generic I/O error, classified by a portable [`IoErrorKind`].
335    #[error("transport i/o: {0}")]
336    Io(IoErrorKind),
337}
338
339/// Socket-level options applied by [`TransportFactory::bind`].
340///
341/// The fields mirror the BSD / `socket2` options that `simple-someip`
342/// needs for its Service Discovery socket layout. A default-constructed
343/// [`SocketOptions`] requests a plain unicast socket.
344///
345/// `#[non_exhaustive]` so additional knobs (TTL, buffer sizes) can be
346/// introduced later without breaking downstream construction.
347#[derive(Debug, Clone, Copy)]
348#[non_exhaustive]
349pub struct SocketOptions {
350    /// Enable `SO_REUSEADDR`. Required on the SD port 30490 when more
351    /// than one SOME/IP endpoint runs on the same interface; on Linux,
352    /// callers binding 30490 should set BOTH this and [`Self::reuse_port`]
353    /// because Linux ties multicast-group membership to the
354    /// `SO_REUSEPORT` group rather than `SO_REUSEADDR` alone — without
355    /// REUSEPORT a second binder may fail or silently steal datagrams.
356    pub reuse_address: bool,
357    /// Enable `SO_REUSEPORT` where supported (Linux, BSD). Ignored on
358    /// platforms that do not expose it. See [`Self::reuse_address`] for
359    /// the Linux-specific reason both are required on the SD socket.
360    pub reuse_port: bool,
361    /// Outbound multicast interface (`IP_MULTICAST_IF`). `None` lets the
362    /// backend choose.
363    pub multicast_if_v4: Option<Ipv4Addr>,
364    /// Loop multicast traffic back to sockets on the same host
365    /// (`IP_MULTICAST_LOOP`). Tri-state:
366    /// - `None` — the OS default applies (Linux: enabled by default).
367    ///   Use this when you have no opinion on loopback.
368    /// - `Some(true)` — explicitly enable. Required when running a
369    ///   SOME/IP server and client on the same machine for testing.
370    /// - `Some(false)` — explicitly disable.
371    ///
372    /// Backends call `setsockopt(IP_MULTICAST_LOOP)` only for
373    /// `Some(_)`. A previous bool-typed field caused
374    /// `multicast_if_v4: Some(_), multicast_loop_v4: false` to silently
375    /// turn loopback OFF on hosts where the OS default was ON, even
376    /// when the caller had no opinion on loopback.
377    pub multicast_loop_v4: Option<bool>,
378}
379
380impl SocketOptions {
381    /// A plain unicast socket with no multicast configuration.
382    #[must_use]
383    pub const fn new() -> Self {
384        Self {
385            reuse_address: false,
386            reuse_port: false,
387            multicast_if_v4: None,
388            multicast_loop_v4: None,
389        }
390    }
391}
392
393impl Default for SocketOptions {
394    fn default() -> Self {
395        Self::new()
396    }
397}
398
399/// The result of a successful [`TransportSocket::recv_from`].
400///
401/// `truncated` is set if the backend delivered only a prefix of the
402/// incoming datagram because it did not fit in the caller's buffer. If
403/// callers use a buffer sized to [`crate::UDP_BUFFER_SIZE`], truncation is
404/// generally not expected on backends whose delivered datagrams are
405/// bounded by that configured application-level cap. Backends that may
406/// deliver larger datagrams should surface this explicitly instead of
407/// silently dropping the fact that data was discarded.
408///
409/// Note: the default Tokio backend currently always reports
410/// `truncated: false` because `tokio::net::UdpSocket::recv_from` does not
411/// expose `MSG_TRUNC` (or equivalent). Reliable truncation detection
412/// requires a backend that does — e.g. a `recvmsg`-based backend, or a
413/// `no_std` stack like smoltcp / embassy-net that surfaces the original
414/// datagram length.
415#[derive(Debug, Clone, Copy, PartialEq, Eq)]
416pub struct ReceivedDatagram {
417    /// Number of bytes written to the caller's buffer.
418    pub bytes_received: usize,
419    /// Source address of the datagram.
420    pub source: SocketAddrV4,
421    /// `true` if the incoming datagram was larger than the caller's
422    /// buffer and the tail was discarded. See the type-level docs for
423    /// the default Tokio backend's caveat.
424    pub truncated: bool,
425}
426
427/// A bound, configured UDP socket usable for SOME/IP message exchange.
428///
429/// Implementations are obtained via [`TransportFactory::bind`]. The
430/// send/receive methods return associated future types so callers can
431/// require `Send` bounds when spawning socket loops on multithreaded
432/// executors. The smaller socket-level queries ([`Self::local_addr`],
433/// [`Self::join_multicast_v4`], [`Self::leave_multicast_v4`]) are
434/// synchronous because they are typically O(1) lookups on a backend's
435/// internal handle and do not benefit from yielding to the executor.
436///
437/// Multicast group membership is joined *after* bind via
438/// [`TransportSocket::join_multicast_v4`]; the bind-time
439/// [`SocketOptions::multicast_if_v4`] only selects the *outbound*
440/// multicast interface.
441///
442/// # Associated future types
443///
444/// The [`SendFuture`](Self::SendFuture) and [`RecvFuture`](Self::RecvFuture)
445/// associated types let consumers express `Send` bounds on the futures
446/// returned by `send_to` and `recv_from` without requiring nightly-only
447/// Return-Type Notation (RTN, RFC 3654). This enables:
448///
449/// ```ignore
450/// fn spawn_loop<T: TransportSocket>(sock: T, spawner: impl Spawner)
451/// where
452///     T: Send + Sync + 'static,
453///     for<'a> T::SendFuture<'a>: Send,
454///     for<'a> T::RecvFuture<'a>: Send,
455/// {
456///     spawner.spawn(async move { /* use sock */ });
457/// }
458/// ```
459///
460/// `TokioSocket` implements these with `Send` futures; bare-metal
461/// implementations must do the same if they want to be used with
462/// multithreaded spawners.
463pub trait TransportSocket {
464    /// Future returned by [`Self::send_to`].
465    type SendFuture<'a>: Future<Output = Result<(), TransportError>>
466    where
467        Self: 'a;
468
469    /// Future returned by [`Self::recv_from`].
470    type RecvFuture<'a>: Future<Output = Result<ReceivedDatagram, TransportError>>
471    where
472        Self: 'a;
473
474    /// Send `buf` to `target`. UDP is atomic — either the whole datagram
475    /// is transmitted or an error is returned; there is no short-write
476    /// case, which is why this method returns `()` on success rather than
477    /// a byte count.
478    ///
479    /// Takes `&self` so a single-task socket loop can hold a pending
480    /// [`Self::recv_from`] future and still call `send_to` in another
481    /// `select!` branch. Backends that need to mutate their socket
482    /// handle on send — e.g. direct smoltcp — must provide interior
483    /// mutability (typically `RefCell<_>` on single-threaded `no_std`, or
484    /// `critical_section::Mutex<RefCell<_>>` on multi-core HAL). The
485    /// `tokio::net::UdpSocket` and `embassy_net::udp::UdpSocket` APIs
486    /// are already `&self`, so adapters over those backends need no
487    /// extra wrapping.
488    ///
489    /// # Errors
490    ///
491    /// Returns:
492    /// - [`TransportError::Io`] with the appropriate [`IoErrorKind`] for
493    ///   transport-level send failures (e.g. the peer is unreachable,
494    ///   the interface is down, the datagram exceeds the link MTU, or a
495    ///   platform-level send error).
496    /// - [`TransportError::Unsupported`] if `target` is not representable
497    ///   on a backend that only speaks a subset of IPv4 (rare; most
498    ///   backends surface addressing issues as [`TransportError::Io`]).
499    fn send_to<'a>(&'a self, buf: &'a [u8], target: SocketAddrV4) -> Self::SendFuture<'a>;
500
501    /// Receive the next datagram into `buf`, returning a
502    /// [`ReceivedDatagram`] carrying byte count, source, and a truncation
503    /// flag.
504    ///
505    /// Takes `&self` for the same reason as [`Self::send_to`]: the
506    /// pending receive future must not hold an exclusive borrow of the
507    /// socket, or the concurrent send branch of a `select!` cannot
508    /// compile.
509    ///
510    /// # Cancel safety
511    ///
512    /// The returned [`Self::RecvFuture`] **must be cancel-safe**:
513    /// dropping it before completion (the typical outcome inside a
514    /// `select!` / `select_biased!` where another arm wins) must not
515    /// lose any datagram that the kernel had already delivered to the
516    /// socket. The server run-loop and the client socket-manager both
517    /// race this future against other arms and rely on the
518    /// drop-and-retry pattern; a backend whose recv-future commits
519    /// kernel state before yielding (and loses it on drop) would
520    /// silently drop datagrams. The default `TokioSocket` impl
521    /// satisfies this via tokio's documented cancel-safety on
522    /// `UdpSocket::recv_from`.
523    ///
524    /// # Errors
525    ///
526    /// Returns:
527    /// - [`TransportError::Io`] with the appropriate [`IoErrorKind`] for
528    ///   transport-level receive failures (e.g. the socket was closed,
529    ///   the interface went down, or a platform-level recv error).
530    /// - [`TransportError::Unsupported`] if the backend surfaces a
531    ///   non-IPv4 source address that cannot be represented as
532    ///   [`SocketAddrV4`].
533    ///
534    /// A datagram whose payload exceeds `buf` is **not** an error; it is
535    /// returned with [`ReceivedDatagram::truncated`] set to `true`. The
536    /// caller decides whether to treat truncation as fatal.
537    fn recv_from<'a>(&'a self, buf: &'a mut [u8]) -> Self::RecvFuture<'a>;
538
539    /// Return the local address this socket is bound to. Useful for
540    /// discovering the ephemeral port chosen by `bind(port: 0, ..)`.
541    ///
542    /// # Errors
543    ///
544    /// Returns [`TransportError`] if the backend cannot report the address.
545    fn local_addr(&self) -> Result<SocketAddrV4, TransportError>;
546
547    /// Join IPv4 multicast group `group` on interface `iface`. Required
548    /// before the socket will receive multicast traffic for that group.
549    ///
550    /// Called once per group per socket; joining twice is allowed and a
551    /// no-op on most backends.
552    ///
553    /// # Errors
554    ///
555    /// Returns [`TransportError::Unsupported`] if the backend has no
556    /// multicast support; otherwise [`TransportError::Io`] with an
557    /// appropriate kind.
558    fn join_multicast_v4(&self, group: Ipv4Addr, iface: Ipv4Addr) -> Result<(), TransportError>;
559
560    /// Leave IPv4 multicast group `group` on interface `iface`. Symmetric
561    /// to [`Self::join_multicast_v4`]. Most backends implicitly leave on
562    /// drop, so this is optional for simple lifetimes but required for
563    /// long-lived sockets that rotate group membership.
564    ///
565    /// # Errors
566    ///
567    /// Returns [`TransportError::Unsupported`] if the backend has no
568    /// multicast support; otherwise [`TransportError::Io`] with an
569    /// appropriate kind.
570    fn leave_multicast_v4(&self, group: Ipv4Addr, iface: Ipv4Addr) -> Result<(), TransportError>;
571
572    /// Upper bound, in bytes, on datagrams this socket will successfully
573    /// accept in `send_to` or return via `recv_from`. The default returns
574    /// [`crate::UDP_BUFFER_SIZE`], the crate's default application-level
575    /// UDP payload cap (currently 1500 bytes — note that this is *not*
576    /// MTU-safe; see [`crate::UDP_BUFFER_SIZE`]'s own docs for the
577    /// IPv4/IPv6 header overhead).
578    ///
579    /// Backends with a smaller effective MTU (for example, some
580    /// resource-constrained embedded stacks) should override this to
581    /// advertise the real limit so callers can size buffers accordingly.
582    #[must_use]
583    fn max_datagram_size(&self) -> usize {
584        crate::UDP_BUFFER_SIZE
585    }
586}
587
588/// Constructs [`TransportSocket`] instances from a bind address and
589/// [`SocketOptions`]. The factory carries whatever state the backend needs
590/// (for example, an lwIP network-interface handle) so that `bind` itself
591/// is a pure data operation.
592///
593/// On `std + tokio`, a unit-struct `TokioTransport;` factory is all that's
594/// needed — the runtime is implicit.
595pub trait TransportFactory {
596    /// The socket type produced by this factory.
597    type Socket: TransportSocket;
598
599    /// Future returned by [`Self::bind`].
600    ///
601    /// As an associated GAT (matching [`TransportSocket::SendFuture`] /
602    /// [`TransportSocket::RecvFuture`]), consumers can express a `Send`
603    /// bound at use sites that need it without forcing every backend
604    /// to produce a `Send` bind future. Multi-threaded callers add
605    /// `where for<'a> F::BindFuture<'a>: Send`; single-threaded callers
606    /// (`Client::new_with_deps_local`) drop that bound and accept a
607    /// `!Send` bind future from a backend like embassy-net.
608    type BindFuture<'a>: Future<Output = Result<Self::Socket, TransportError>>
609    where
610        Self: 'a;
611
612    /// Bind a new socket to `addr` with the requested `options`.
613    ///
614    /// `addr.port() == 0` requests an ephemeral port; call
615    /// [`TransportSocket::local_addr`] afterwards to discover what was
616    /// assigned.
617    ///
618    /// # Errors
619    ///
620    /// Returns [`TransportError::AddressInUse`] if the requested address
621    /// and port pair is already bound (and `reuse_*` was not enabled).
622    /// Other backend-level failures surface as [`TransportError::Io`].
623    fn bind<'a>(&'a self, addr: SocketAddrV4, options: &'a SocketOptions) -> Self::BindFuture<'a>;
624}
625
626/// Executor-agnostic sleep primitive.
627///
628/// `simple-someip` needs timed waits in two places: the Service Discovery
629/// announcement tick (1 s) and the client event-loop idle timeout
630/// (125 ms). Consumers provide a `Timer` at startup; on `std + tokio` this
631/// is a one-line wrapper around `tokio::time::sleep`, on embedded it is a
632/// one-line wrapper around `embassy_time::Timer::after` or similar.
633pub trait Timer {
634    /// Future returned by [`Self::sleep`].
635    ///
636    /// As an associated GAT, consumers can require `Send` at use sites
637    /// (`where for<'a> Tm::SleepFuture<'a>: Send`) without forcing every
638    /// backend's sleep future to be `Send`. Multi-threaded callers
639    /// (`Server::announcement_loop`, the tokio Client) add the bound;
640    /// single-threaded callers do not, accepting a `!Send` future from
641    /// a backend like `embassy_time`.
642    type SleepFuture<'a>: Future<Output = ()>
643    where
644        Self: 'a;
645
646    /// Wait for at least `duration` before resolving. Implementations MAY
647    /// overshoot but MUST NOT undershoot.
648    fn sleep(&self, duration: Duration) -> Self::SleepFuture<'_>;
649}
650
651/// Executor-agnostic task-spawning primitive.
652///
653/// `simple-someip`'s per-socket I/O loops need to run concurrently with
654/// the client's main event loop — otherwise `SocketManager::send`'s
655/// internal oneshot wait deadlocks (the send future parks the main
656/// loop, which is the only thing that would drive the socket loop to
657/// produce its response). The `Spawner` trait lets std+tokio callers
658/// pass a one-line `TokioSpawner` and bare-metal callers wrap their own
659/// executor's task-spawning primitive.
660///
661/// # Design rationale
662///
663/// The transport-trait design deliberately avoided wrapping spawn to
664/// prevent "reinventing embassy" and trait-object dispatch in the hot
665/// path. However, without a spawn abstraction, `Inner::bind_*` has to
666/// call `tokio::spawn` directly — making the whole crate tokio-only.
667/// The revised rule: spawn DOES need a trait, but we avoid the
668/// concerns by (1) keeping the trait generic (monomorphized, no
669/// `dyn Spawner`) and (2) scoping it narrowly — just spawn, not
670/// select/sleep which have other solutions.
671///
672/// # Usage
673///
674/// On `std + tokio`, use `crate::tokio_transport::TokioSpawner`
675/// (available when the `client` or `server` feature is enabled) —
676/// a zero-size unit struct whose `spawn` is a thin wrapper around
677/// `tokio::spawn`. The path is rendered as a code literal rather
678/// than an intra-doc link because the target module is feature-gated
679/// and would break default-feature rustdoc builds. On embedded:
680///
681/// ```ignore
682/// struct EmbassySpawner(embassy_executor::Spawner);
683/// impl simple_someip::Spawner for EmbassySpawner {
684///     fn spawn(&self, fut: impl core::future::Future<Output = ()> + Send + 'static) {
685///         // embassy's Spawner has its own task-registration model;
686///         // the adapter layer depends on how the user defined their tasks
687///         todo!("call self.0.spawn(...)");
688///     }
689/// }
690/// ```
691/// Local-executor counterpart to [`Spawner`].
692///
693/// Where [`Spawner::spawn`] requires its future to be `Send + 'static`
694/// (matching multi-threaded executors like tokio), `LocalSpawner::spawn_local`
695/// drops the `Send` bound and is the trait that single-threaded
696/// executors — embassy with `task-arena = 0`, tokio's `LocalSet`, async-std
697/// `LocalExecutor`, etc. — implement directly.
698///
699/// The two traits are independent: an executor MAY implement both
700/// (`current_thread` tokio with `LocalSet`), only [`Spawner`]
701/// (multi-threaded tokio default), or only [`LocalSpawner`]
702/// (single-task embassy).
703///
704/// Use `crate::client::Client::new_with_deps_local` (under `client`) to
705/// construct a Client whose run-loop and per-socket loops are submitted
706/// through a
707/// `LocalSpawner` (and whose `TransportFactory::Socket` is therefore
708/// allowed to be `!Send`).
709pub trait LocalSpawner {
710    /// Submit `future` to the local executor. Must not block; must
711    /// arrange for the future to be polled to completion on some
712    /// single-threaded task.
713    ///
714    /// The future is **not** required to be `Send` — it may capture
715    /// `Rc`, `RefCell`, raw `*mut` pointers, etc.
716    fn spawn_local(&self, future: impl Future<Output = ()> + 'static);
717}
718
719pub trait Spawner {
720    /// Submit `future` to the executor. Must not block; must arrange
721    /// for the future to be polled to completion on some task.
722    ///
723    /// # Correctness requirement
724    ///
725    /// Implementations MUST poll the submitted future. Dropping it
726    /// without polling — or holding it in a queue that never drains —
727    /// will deadlock `crate::client::Client` (available when the
728    /// `client` feature is enabled): `SocketManager::send`
729    /// `await`s an internal mpsc→oneshot round-trip whose only driver
730    /// is the per-socket loop future submitted here. No poll, no
731    /// progress, no oneshot resolution; the caller's `send` hangs
732    /// forever.
733    ///
734    /// The mock spawners in `tests/bare_metal_*.rs` demonstrate
735    /// correct integration patterns; callers that simply drop the
736    /// future will deadlock on any operation that requires a socket
737    /// round-trip.
738    ///
739    /// # Fire-and-forget by design
740    ///
741    /// `spawn` returns `()`, not a join-handle. The rest of the crate
742    /// observes `tokio::JoinHandle`s wherever it spawns work directly
743    /// (commit `d92c5a3`); this trait is the deliberate exception. The
744    /// per-socket loops have no observable result — they run forever and
745    /// only exit when their owning `SocketManager` drops its channel
746    /// ends — so a join-handle would just be storage with no callers.
747    /// A future revision MAY add an associated `Handle` type if a
748    /// concrete shutdown / cancellation use case appears; today there is
749    /// none.
750    ///
751    /// # Bound rationale
752    ///
753    /// The `Send + 'static` bound matches multi-threaded executors like
754    /// tokio, async-std, and smol — the captured per-socket loop is
755    /// already `Send + 'static` because its underlying `TokioSocket` is.
756    /// Embassy and other `no_alloc` / single-core executors typically need
757    /// additional adapter scaffolding (a typed `SpawnToken`, a static
758    /// task arena, hardware-specific waker plumbing) to satisfy
759    /// `Send + 'static`; the example at the top of this docstring has a
760    /// `todo!()` precisely because the adapter is not one-line. A future
761    /// release MAY add a `spawn_local`-style variant gated on a cargo
762    /// feature for those targets.
763    fn spawn(&self, future: impl Future<Output = ()> + Send + 'static);
764}
765
766/// Shared handle to the runtime E2E configuration registry.
767///
768/// Abstracts over `Arc<Mutex<E2ERegistry>>` on `std` and over
769/// critical-section-backed primitives (e.g. `embassy_sync::blocking_mutex`)
770/// on bare metal. All methods take `&self` and provide interior-mutable
771/// access. Implementations are required to be `Clone` so the handle can be
772/// cheaply shared between the `Client` (or `Server`) handle and its inner
773/// event loop.
774pub trait E2ERegistryHandle: Clone + Send + Sync + 'static {
775    /// Register an E2E profile for the given key, replacing any prior entry.
776    ///
777    /// # Errors
778    ///
779    /// Returns [`crate::e2e::E2ERegistryFull`] when the underlying registry has no
780    /// capacity for a new key. Replacing an already-registered key
781    /// always succeeds (the existing slot is reused). Implementations
782    /// that wrap [`crate::e2e::E2ERegistry`] forward this error
783    /// directly; backends with their own storage should pick an
784    /// equivalent overflow contract.
785    fn register(&self, key: E2EKey, profile: E2EProfile)
786    -> Result<(), crate::e2e::E2ERegistryFull>;
787
788    /// Remove the E2E configuration for the given key. No-op if absent.
789    fn unregister(&self, key: &E2EKey);
790
791    /// Returns `true` if a profile is registered for `key`.
792    fn contains_key(&self, key: &E2EKey) -> bool;
793
794    /// Run E2E protect for `key` if configured, writing to `output`.
795    ///
796    /// Returns `None` if no profile is registered for `key`.
797    /// Returns `Some(Err(_))` if protection fails (e.g. buffer too small).
798    /// Returns `Some(Ok(len))` on success; `len` is the number of bytes
799    /// written to `output`.
800    fn protect(
801        &self,
802        key: E2EKey,
803        payload: &[u8],
804        upper_header: [u8; 8],
805        output: &mut [u8],
806    ) -> Option<Result<usize, E2EError>>;
807
808    /// Run E2E check for `key` against `source`'s receive counter state,
809    /// if configured.
810    ///
811    /// Returns `None` if no profile is registered for `key`. Otherwise
812    /// returns the check status and the effective payload slice — the
813    /// E2E header is stripped on success; the original bytes are returned
814    /// on check failure so the caller can decide how to handle it.
815    ///
816    /// `source` keys the receive counter state: on a shared subnet several
817    /// devices send the same `(service, method)` under one instance id, so
818    /// each sender's sequence counter must be tracked independently. See
819    /// [`crate::e2e::E2ERegistry`].
820    ///
821    /// The returned slice borrows from `payload`, not from this handle.
822    fn check<'a>(
823        &self,
824        source: IpAddr,
825        key: E2EKey,
826        payload: &'a [u8],
827        upper_header: [u8; 8],
828    ) -> Option<(E2ECheckStatus, &'a [u8])>;
829
830    /// Drop all per-source receive counter state for `source` (e.g. when
831    /// its reboot is detected via Service Discovery), so its next frame
832    /// starts a fresh sequence. Configuration and transmit state are
833    /// untouched.
834    fn reset_source(&self, source: IpAddr);
835}
836
837/// Shared handle to the local interface address.
838///
839/// Abstracts over `Arc<RwLock<Ipv4Addr>>` on `std`. All clones of a
840/// `Client` share the same handle, so writes from one clone (e.g.
841/// `Client::set_interface`) are visible to all others.
842///
843/// On bare metal, where `Client` is not `Clone`, a trivial implementation
844/// wrapping a `core::cell::Cell<Ipv4Addr>` suffices.
845pub trait InterfaceHandle: Clone + Send + Sync + 'static {
846    /// Returns the current interface address.
847    fn get(&self) -> Ipv4Addr;
848
849    /// Updates the stored interface address.
850    fn set(&self, addr: Ipv4Addr);
851}
852
853/// Shared handle to a single owned-or-borrowed `T`.
854///
855/// One trait covering every "Server holds an `Arc<T>` for sharing
856/// between its run loop and consumer-side tasks" pattern in this
857/// crate. Replaces the three separate handle traits this crate
858/// shipped earlier (`SocketHandle`, `SdStateHandle`,
859/// `EventPublisherHandle`), each of which had the same shape with
860/// a different concrete `T`.
861///
862/// Two impls ship out of the box, both via blanket impls so any
863/// consumer-defined type wrapped in `Arc<T>` or `&'static T`
864/// satisfies the bound automatically:
865///
866/// - `Arc<T>: SharedHandle<T>` on alloc-using builds (`std` or
867///   `bare_metal`-with-alloc). `Arc::clone` increments the
868///   refcount; `get` returns the inner reference.
869/// - `&'static T: SharedHandle<T>` on bare-metal-no-alloc. The
870///   reference is `Copy + Clone + 'static`; the user declares the
871///   underlying `static` storage at boot.
872///
873/// `Clone + 'static` only — neither `Send` nor `Sync` at the
874/// trait level. Method-level `where` clauses on `Server` add
875/// Send bounds at the use sites that need them
876/// (`announcement_loop`'s `+ Send` return type, etc.).
877///
878/// `T: 'static` because both blanket impls require it: an `Arc<T>`
879/// is `'static` only when `T: 'static`, and `&'static T` requires
880/// `T: 'static` by definition.
881///
882/// `?Sized` is intentionally NOT supported — the inline-construction
883/// path ([`WrappableSharedHandle::wrap`]) needs an owned `T`, which
884/// requires `Sized`.
885pub trait SharedHandle<T: 'static>: Clone + 'static {
886    /// Borrow the underlying `T`. Both blanket impls return a
887    /// reference into the underlying storage; consumers should
888    /// not assume more than a fresh borrow's worth of lifetime.
889    fn get(&self) -> &T;
890}
891
892/// Extension of [`SharedHandle`] for handles that can be
893/// constructed inline from an owned `T`.
894///
895/// Required by `Server` constructors that build the underlying
896/// `T` internally (the alloc-using path —
897/// e.g., `Server::new_with_deps` calls `factory.bind(...).await?`
898/// to get an `F::Socket`, then `H::wrap(socket)` to place it
899/// behind the caller's chosen shared-storage). The no-alloc
900/// counterpart constructors (`Server::new_with_handles`) take
901/// pre-built handles directly and don't need this trait.
902///
903/// `&'static T` deliberately does NOT implement this trait —
904/// materializing a `&'static T` from an owned `T` inside a trait
905/// method's body requires an allocator (`Box::leak`) or a
906/// slot-based init pattern (`StaticCell::init`) that the trait
907/// method's signature can't express. No-alloc consumers declare
908/// their `static` storage themselves and pass `&STATIC` into the
909/// no-wrap constructor.
910pub trait WrappableSharedHandle<T: 'static>: SharedHandle<T> {
911    /// Place an owned `T` behind this handle's shared storage.
912    fn wrap(value: T) -> Self;
913}
914
915// `&'static T` is the no-alloc handle. `&'static T: Copy + Clone +
916// 'static` for any `T: 'static`, so the trait bounds are met
917// without further work.
918impl<T: 'static> SharedHandle<T> for &'static T {
919    fn get(&self) -> &T {
920        self
921    }
922}
923
924// `Arc<T>` is the alloc-using handle. `Arc::clone` is the
925// reference-count increment; `wrap` is `Arc::new`. Gated on the
926// internal `_alloc` feature, which is also what gates the
927// crate-root `extern crate alloc` declaration — server,
928// embassy_channels, and std all imply it.
929#[cfg(feature = "_alloc")]
930impl<T: 'static> SharedHandle<T> for alloc::sync::Arc<T> {
931    fn get(&self) -> &T {
932        self
933    }
934}
935
936#[cfg(feature = "_alloc")]
937impl<T: 'static> WrappableSharedHandle<T> for alloc::sync::Arc<T> {
938    fn wrap(value: T) -> Self {
939        alloc::sync::Arc::new(value)
940    }
941}
942
943/// Default `std`-flavoured impls of [`E2ERegistryHandle`] /
944/// [`InterfaceHandle`] / [`SocketHandle`] backed by
945/// `std::sync::{Arc, Mutex, RwLock}`. Pure std — no tokio
946/// dependency — so they live in the executor-agnostic transport
947/// module rather than the tokio backend.
948#[cfg(feature = "std")]
949mod std_handle_impls {
950    use super::{E2ERegistryHandle, InterfaceHandle};
951    use crate::e2e::Error as E2EError;
952    use crate::e2e::{E2ECheckStatus, E2EKey, E2EProfile, E2ERegistry, E2ERegistryFull};
953    use core::net::{IpAddr, Ipv4Addr};
954    use std::sync::{Arc, Mutex, RwLock};
955
956    impl E2ERegistryHandle for Arc<Mutex<E2ERegistry>> {
957        fn register(&self, key: E2EKey, profile: E2EProfile) -> Result<(), E2ERegistryFull> {
958            self.lock()
959                .expect("e2e registry lock poisoned")
960                .register(key, profile)
961        }
962
963        fn unregister(&self, key: &E2EKey) {
964            self.lock()
965                .expect("e2e registry lock poisoned")
966                .unregister(key);
967        }
968
969        fn contains_key(&self, key: &E2EKey) -> bool {
970            self.lock()
971                .expect("e2e registry lock poisoned")
972                .contains_key(key)
973        }
974
975        fn protect(
976            &self,
977            key: E2EKey,
978            payload: &[u8],
979            upper_header: [u8; 8],
980            output: &mut [u8],
981        ) -> Option<Result<usize, E2EError>> {
982            self.lock().expect("e2e registry lock poisoned").protect(
983                key,
984                payload,
985                upper_header,
986                output,
987            )
988        }
989
990        fn check<'a>(
991            &self,
992            source: IpAddr,
993            key: E2EKey,
994            payload: &'a [u8],
995            upper_header: [u8; 8],
996        ) -> Option<(E2ECheckStatus, &'a [u8])> {
997            self.lock().expect("e2e registry lock poisoned").check(
998                source,
999                key,
1000                payload,
1001                upper_header,
1002            )
1003        }
1004
1005        fn reset_source(&self, source: IpAddr) {
1006            self.lock()
1007                .expect("e2e registry lock poisoned")
1008                .reset_source(source);
1009        }
1010    }
1011
1012    impl InterfaceHandle for Arc<RwLock<Ipv4Addr>> {
1013        fn get(&self) -> Ipv4Addr {
1014            *self.read().expect("interface lock poisoned")
1015        }
1016
1017        fn set(&self, addr: Ipv4Addr) {
1018            *self.write().expect("interface lock poisoned") = addr;
1019        }
1020    }
1021}
1022
1023/// Bare-metal no-alloc impls of [`E2ERegistryHandle`] and [`InterfaceHandle`].
1024///
1025/// These types satisfy `Clone + Send + Sync + 'static` without any heap
1026/// allocation. The backing storage lives in a caller-owned `static`; the
1027/// handles are thin `&'static` pointers that are trivially `Copy`.
1028///
1029/// # Production pattern
1030///
1031/// ```ignore
1032/// use core::cell::RefCell;
1033/// use core::sync::atomic::{AtomicU32, Ordering};
1034/// use embassy_sync::blocking_mutex::Mutex;
1035/// use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
1036/// use simple_someip::e2e::E2ERegistry;
1037/// use simple_someip::transport::{StaticE2EHandle, AtomicInterfaceHandle};
1038///
1039/// // Initialize once in main() before spawning tasks.
1040/// fn init() -> (StaticE2EHandle, AtomicInterfaceHandle) {
1041///     static IFACE_ADDR: AtomicU32 = AtomicU32::new(0);
1042///     // E2ERegistry::new() is not const so the storage is heap-placed once.
1043///     let registry_storage: &'static _ = Box::leak(Box::new(
1044///         Mutex::<CriticalSectionRawMutex, RefCell<E2ERegistry>>::new(
1045///             RefCell::new(E2ERegistry::new()),
1046///         ),
1047///     ));
1048///     (StaticE2EHandle::new(registry_storage), AtomicInterfaceHandle::new(&IFACE_ADDR))
1049/// }
1050/// ```
1051///
1052/// # No-allocator targets
1053///
1054/// The example above uses `Box::leak` because [`crate::e2e::E2ERegistry::new()`] is not
1055/// currently `const`. On a target with no allocator, swap that for a
1056/// `static`-cell pattern (e.g. `static_cell::StaticCell::init`) once the
1057/// registry constructor becomes `const`-friendly. The handle layer itself
1058/// never allocates — only the one-time storage materialization does.
1059#[cfg(feature = "bare_metal")]
1060pub mod bare_metal_handle_impls {
1061    use super::InterfaceHandle;
1062    use core::net::Ipv4Addr;
1063    use core::sync::atomic::{AtomicU32, Ordering};
1064
1065    // `StaticE2EHandle` wraps `E2ERegistry`, which currently requires
1066    // `feature = "std"` because its backing storage is `HashMap`. Ported
1067    // separately below so the rest of this module — in particular
1068    // `AtomicInterfaceHandle` — is available in pure `no_std` bare-metal
1069    // builds.
1070
1071    /// No-alloc [`InterfaceHandle`] backed by a `&'static AtomicU32`.
1072    ///
1073    /// IPv4 addresses are encoded as big-endian `u32` (`Ipv4Addr::into::<u32>`).
1074    /// All clones are the same thin pointer. Declare the backing storage in a
1075    /// `static`:
1076    ///
1077    /// ```ignore
1078    /// static IFACE_ADDR: AtomicU32 = AtomicU32::new(0);
1079    /// let handle = AtomicInterfaceHandle::new(&IFACE_ADDR);
1080    /// ```
1081    ///
1082    /// # Memory ordering
1083    ///
1084    /// `set` uses [`Ordering::Release`] and `get` uses
1085    /// [`Ordering::Acquire`] so a reader on a weakly-ordered core sees
1086    /// updates promptly. Cheap on x86-TSO (free) and inexpensive on
1087    /// aarch64 (one `dmb ish`).
1088    #[derive(Clone, Copy)]
1089    pub struct AtomicInterfaceHandle(&'static AtomicU32);
1090
1091    impl AtomicInterfaceHandle {
1092        /// Wraps a static reference to the backing atomic.
1093        pub const fn new(addr: &'static AtomicU32) -> Self {
1094            Self(addr)
1095        }
1096    }
1097
1098    // Send + Sync are derived automatically: `&'static AtomicU32` is
1099    // `Send + Sync` because `AtomicU32` is `Sync`.
1100
1101    impl InterfaceHandle for AtomicInterfaceHandle {
1102        fn get(&self) -> Ipv4Addr {
1103            // `Acquire` ordering pairs with the `Release` store below
1104            // so a reader sees the most recent address promptly even
1105            // on weakly-ordered hardware. The cost over `Relaxed` is
1106            // a `dmb ish` on aarch64; on x86-TSO it is free.
1107            Ipv4Addr::from(self.0.load(Ordering::Acquire))
1108        }
1109
1110        fn set(&self, addr: Ipv4Addr) {
1111            self.0.store(u32::from(addr), Ordering::Release);
1112        }
1113    }
1114    // `StaticSocketHandle<T>(&'static T)` was collapsed into a
1115    // direct `impl SharedHandle<T> for &'static T` blanket — the
1116    // wrapper type's only role was carrying the `'static` lifetime,
1117    // which the blanket impl achieves without a wrapper. Consumers
1118    // pass `&SOCKET` directly into Server's no-wrap constructors.
1119}
1120
1121/// `StaticE2EHandle` — no-alloc `E2ERegistryHandle` backed by a
1122/// `&'static` critical-section mutex.
1123///
1124/// Available in pure `no_std` builds: [`crate::e2e::E2ERegistry`] is
1125/// backed by [`heapless::index_map::FnvIndexMap`], so no allocator is
1126/// required.
1127#[cfg(feature = "bare_metal")]
1128pub mod bare_metal_e2e_impl {
1129    use super::E2ERegistryHandle;
1130    use crate::e2e::{
1131        E2ECheckStatus, E2EKey, E2EProfile, E2ERegistry, E2ERegistryFull, Error as E2EError,
1132    };
1133    use core::cell::RefCell;
1134    use core::net::IpAddr;
1135    use embassy_sync::blocking_mutex::Mutex;
1136    use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
1137
1138    /// Convenience type alias for the embassy-sync critical-section mutex
1139    /// backing [`StaticE2EHandle`].
1140    pub type StaticE2EStorage = Mutex<CriticalSectionRawMutex, RefCell<E2ERegistry>>;
1141
1142    /// No-alloc [`E2ERegistryHandle`] backed by a `&'static` critical-section
1143    /// mutex.
1144    ///
1145    /// All clones are the same thin pointer. Construct via [`StaticE2EHandle::new`]
1146    /// and supply a `&'static StaticE2EStorage` (typically obtained via
1147    /// `Box::leak` during system init, since [`E2ERegistry::new`] is not const).
1148    #[derive(Clone, Copy)]
1149    pub struct StaticE2EHandle(&'static StaticE2EStorage);
1150
1151    impl StaticE2EHandle {
1152        /// Wraps a static reference to the backing mutex.
1153        pub const fn new(storage: &'static StaticE2EStorage) -> Self {
1154            Self(storage)
1155        }
1156    }
1157
1158    impl E2ERegistryHandle for StaticE2EHandle {
1159        fn register(&self, key: E2EKey, profile: E2EProfile) -> Result<(), E2ERegistryFull> {
1160            self.0.lock(|cell| cell.borrow_mut().register(key, profile))
1161        }
1162
1163        fn unregister(&self, key: &E2EKey) {
1164            self.0.lock(|cell| cell.borrow_mut().unregister(key));
1165        }
1166
1167        fn contains_key(&self, key: &E2EKey) -> bool {
1168            self.0.lock(|cell| cell.borrow().contains_key(key))
1169        }
1170
1171        fn protect(
1172            &self,
1173            key: E2EKey,
1174            payload: &[u8],
1175            upper_header: [u8; 8],
1176            output: &mut [u8],
1177        ) -> Option<Result<usize, E2EError>> {
1178            self.0.lock(|cell| {
1179                cell.borrow_mut()
1180                    .protect(key, payload, upper_header, output)
1181            })
1182        }
1183
1184        fn check<'a>(
1185            &self,
1186            source: IpAddr,
1187            key: E2EKey,
1188            payload: &'a [u8],
1189            upper_header: [u8; 8],
1190        ) -> Option<(E2ECheckStatus, &'a [u8])> {
1191            self.0
1192                .lock(|cell| cell.borrow_mut().check(source, key, payload, upper_header))
1193        }
1194
1195        fn reset_source(&self, source: IpAddr) {
1196            self.0.lock(|cell| cell.borrow_mut().reset_source(source));
1197        }
1198    }
1199}
1200
1201#[cfg(feature = "bare_metal")]
1202pub use bare_metal_handle_impls::AtomicInterfaceHandle;
1203
1204#[cfg(feature = "bare_metal")]
1205pub use bare_metal_e2e_impl::{StaticE2EHandle, StaticE2EStorage};
1206
1207// ── Channel-handle abstraction ────────────────────────────────────────────
1208//
1209// `ChannelFactory` and its associated sender / receiver traits abstract over
1210// the channel primitive used by the client. `TokioChannels` (in
1211// `tokio_transport`) is the default for `std + tokio` builds;
1212// `EmbassySyncChannels` (in `crate::embassy_channels`, gated behind
1213// `embassy_channels` feature) is a heap-backed alternative for no-tokio builds;
1214// `static_channels` (gated behind `bare_metal`) is the no-alloc alternative.
1215
1216/// Returned by [`OneshotRecv::recv`] when the sender was dropped before
1217/// sending a value.
1218#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1219pub struct OneshotCancelled;
1220
1221impl core::fmt::Display for OneshotCancelled {
1222    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1223        f.write_str("oneshot sender dropped before sending a value")
1224    }
1225}
1226
1227/// The send half of a oneshot channel. Consuming: a value can be sent exactly
1228/// once.
1229pub trait OneshotSend<T: Send + 'static>: Send + 'static {
1230    /// Send `value` through the channel.
1231    ///
1232    /// # Errors
1233    ///
1234    /// Returns `Err(value)` if the receiver was already dropped.
1235    fn send(self, value: T) -> Result<(), T>;
1236}
1237
1238/// The receive half of a oneshot channel. Resolves once the sender delivers a
1239/// value, or returns [`OneshotCancelled`] if the sender is dropped first.
1240pub trait OneshotRecv<T: Send + 'static>: Send + 'static {
1241    /// Await the value. Consumes self — a oneshot receiver can only be awaited
1242    /// once.
1243    fn recv(self) -> impl core::future::Future<Output = Result<T, OneshotCancelled>> + Send;
1244}
1245
1246/// The send half of a bounded MPSC channel.
1247///
1248/// Implementations must be [`Clone`] so that multiple producers can share the
1249/// same channel (e.g. the `Client` handle is `Clone` and every clone must be
1250/// able to send control messages to `Inner`).
1251pub trait MpscSend<T: Send + 'static>: Clone + Send + 'static {
1252    /// Send `value`, waiting if the channel is full. Returns `Err(())` if the
1253    /// receiver was dropped.
1254    fn send(&self, value: T) -> impl core::future::Future<Output = Result<(), ()>> + Send + '_;
1255}
1256
1257/// The receive half of a bounded MPSC channel.
1258pub trait MpscRecv<T: Send + 'static>: Send + 'static {
1259    /// Receive the next value, waiting if the channel is empty. Returns `None`
1260    /// if all senders were dropped and the channel is empty.
1261    fn recv(&mut self) -> impl core::future::Future<Output = Option<T>> + Send + '_;
1262
1263    /// Poll the channel without blocking. Used by `receive_any_unicast` to
1264    /// multiplex across several socket channels in a single `poll_fn` pass.
1265    fn poll_recv(&mut self, cx: &mut core::task::Context<'_>) -> core::task::Poll<Option<T>>;
1266}
1267
1268/// The send half of an unbounded MPSC channel.
1269///
1270/// Unlike [`MpscSend`], sending never blocks — the implementation must buffer
1271/// arbitrarily many values (or, for embassy-sync, use a large finite capacity
1272/// that is treated as effectively unbounded).
1273pub trait UnboundedSend<T: Send + 'static>: Clone + Send + 'static {
1274    /// Send `value` without blocking.
1275    ///
1276    /// # Errors
1277    ///
1278    /// Returns `Err(value)` if the receiver was dropped.
1279    fn send_now(&self, value: T) -> Result<(), T>;
1280}
1281
1282/// The receive half of an unbounded MPSC channel.
1283pub trait UnboundedRecv<T: Send + 'static>: Send + 'static {
1284    /// Receive the next value, waiting if the channel is empty. Returns `None`
1285    /// if all senders were dropped and the channel is empty.
1286    fn recv(&mut self) -> impl core::future::Future<Output = Option<T>> + Send + '_;
1287}
1288
1289/// A zero-sized factory that creates channel pairs used by the client's
1290/// internal transport.
1291///
1292/// Abstracting over both `tokio::sync::mpsc` / `oneshot` (std path) and
1293/// `embassy-sync::channel::Channel` (bare-metal path) behind a single trait
1294/// lets `Client` / `Inner` / `SocketManager` compile without a tokio
1295/// dependency when `bare_metal` is active and `tokio` is not.
1296///
1297/// The three channel families:
1298/// - **oneshot** — single-shot rendezvous, capacity 1. Used for command
1299///   completion callbacks inside `crate::client::ControlMessage`.
1300/// - **bounded** — finite-capacity MPSC queue. Used for the control channel
1301///   and per-socket send / receive queues.
1302/// - **unbounded** — notionally unbounded MPSC queue (embassy-sync
1303///   implementations use a large-capacity channel). Used for the
1304///   `ClientUpdate` stream from `Inner` to `Client`.
1305///
1306/// # Per-`T` opt-in via the `*Pooled<Self>` traits
1307///
1308/// The three constructor methods are generic over the channeled type
1309/// `T`, but a heap-free static-pool implementation needs to map each `T`
1310/// to a pre-declared `static` storage area. To make that mapping
1311/// type-safe — and to surface "you forgot to declare a pool for this
1312/// type" as a compile error rather than a runtime panic — each method
1313/// requires the channeled type to implement the corresponding
1314/// `*Pooled<Self>` trait and delegates the actual construction to it:
1315///
1316/// ```ignore
1317/// fn oneshot<T>() -> (...) where T: OneshotPooled<Self> { T::oneshot_pair() }
1318/// ```
1319///
1320/// Backends that have a single shared allocator (Tokio, embassy-sync)
1321/// publish a blanket `impl<T: Send + 'static> OneshotPooled<Self> for T`
1322/// (and its bounded / unbounded peers), so existing user code does not
1323/// notice the change. A static-pool backend instead publishes per-`T`
1324/// impls (typically generated by a `define_static_channels!` macro) that wire
1325/// each `T` to its declared pool. Calling `oneshot::<NotDeclared>()`
1326/// against such a backend fails at the call site with
1327/// `OneshotPooled<MyChannels> is not implemented for NotDeclared`.
1328pub trait ChannelFactory: Clone + Send + Sync + 'static {
1329    /// Oneshot sender type.
1330    type OneshotSender<T: Send + 'static>: OneshotSend<T>;
1331    /// Oneshot receiver type.
1332    type OneshotReceiver<T: Send + 'static>: OneshotRecv<T>;
1333    /// Create a oneshot channel pair.
1334    ///
1335    /// Default body delegates to [`OneshotPooled::oneshot_pair`]; impls
1336    /// rarely need to override this, they just publish the appropriate
1337    /// `OneshotPooled<Self>` impls for the types they support.
1338    #[must_use]
1339    fn oneshot<T>() -> (Self::OneshotSender<T>, Self::OneshotReceiver<T>)
1340    where
1341        T: OneshotPooled<Self>,
1342    {
1343        T::oneshot_pair()
1344    }
1345
1346    /// Bounded-channel sender type. The `const N: usize` parameter is
1347    /// the channel capacity; it must match the `N` passed to
1348    /// [`Self::bounded`]. Backends that store the capacity at
1349    /// construction time (`tokio::sync::mpsc`) ignore it for storage
1350    /// purposes; backends that bake it into the type (`embassy-sync`)
1351    /// use it directly.
1352    type BoundedSender<T: Send + 'static, const N: usize>: MpscSend<T>;
1353    /// Bounded-channel receiver type. See [`Self::BoundedSender`].
1354    type BoundedReceiver<T: Send + 'static, const N: usize>: MpscRecv<T>;
1355    /// Create a bounded channel with capacity `N`.
1356    ///
1357    /// Default body delegates to [`BoundedPooled::bounded_pair`].
1358    #[must_use]
1359    fn bounded<T, const N: usize>() -> (Self::BoundedSender<T, N>, Self::BoundedReceiver<T, N>)
1360    where
1361        T: BoundedPooled<Self, N>,
1362    {
1363        T::bounded_pair()
1364    }
1365
1366    /// Unbounded-channel sender type.
1367    type UnboundedSender<T: Send + 'static>: UnboundedSend<T>;
1368    /// Unbounded-channel receiver type.
1369    type UnboundedReceiver<T: Send + 'static>: UnboundedRecv<T>;
1370    /// Create an unbounded channel.
1371    ///
1372    /// Default body delegates to [`UnboundedPooled::unbounded_pair`].
1373    #[must_use]
1374    fn unbounded<T>() -> (Self::UnboundedSender<T>, Self::UnboundedReceiver<T>)
1375    where
1376        T: UnboundedPooled<Self>,
1377    {
1378        T::unbounded_pair()
1379    }
1380}
1381
1382/// Per-`T` opt-in for [`ChannelFactory::oneshot`].
1383///
1384/// Implementors declare "this `T` may be channeled through `C`'s oneshot
1385/// family" and provide the construction. Backends with a single shared
1386/// allocator (Tokio, embassy-sync) publish a blanket
1387/// `impl<T: Send + 'static> OneshotPooled<Self> for T`. Static-pool
1388/// backends publish per-`T` impls — typically via a macro — each
1389/// pointing at a declared `static` pool slot.
1390///
1391/// The trait is parameterized over the channel factory `C` so a single
1392/// `T` may participate in multiple backends without conflicting impls.
1393pub trait OneshotPooled<C: ChannelFactory>: Send + Sized + 'static {
1394    /// Build a `(sender, receiver)` pair through `C`'s oneshot family.
1395    fn oneshot_pair() -> (C::OneshotSender<Self>, C::OneshotReceiver<Self>);
1396}
1397
1398/// Per-`(T, N)` opt-in for [`ChannelFactory::bounded`]. See
1399/// [`OneshotPooled`] for the design rationale; this is the bounded peer
1400/// with capacity baked into the type.
1401pub trait BoundedPooled<C: ChannelFactory, const N: usize>: Send + Sized + 'static {
1402    /// Build a `(sender, receiver)` pair through `C`'s bounded family
1403    /// with capacity `N`.
1404    fn bounded_pair() -> (C::BoundedSender<Self, N>, C::BoundedReceiver<Self, N>);
1405}
1406
1407/// Per-`T` opt-in for [`ChannelFactory::unbounded`]. See
1408/// [`OneshotPooled`] for the design rationale.
1409pub trait UnboundedPooled<C: ChannelFactory>: Send + Sized + 'static {
1410    /// Build a `(sender, receiver)` pair through `C`'s unbounded family.
1411    fn unbounded_pair() -> (C::UnboundedSender<Self>, C::UnboundedReceiver<Self>);
1412}
1413
1414// ── BufferProvider ────────────────────────────────────────────────────────
1415
1416use crate::buffer_pool::{BufferLease, BufferPool};
1417
1418/// Source of `&'static mut [u8]` receive/scratch buffers for the client's
1419/// socket loops. Mirrors [`ChannelFactory`]'s role for channels: the
1420/// bare-metal path is backed by a consumer-declared `static BufferPool`;
1421/// the tokio path is heap-backed and provisioned internally.
1422pub trait BufferProvider: Clone + Send + Sync + 'static {
1423    /// Claim one buffer, or `None` when the pool is exhausted.
1424    fn claim(&self) -> Option<BufferLease>;
1425}
1426
1427/// `BufferProvider` backed by a `'static` [`BufferPool`] (bare-metal path).
1428#[derive(Clone, Copy, Debug)]
1429pub struct StaticBufferProvider<const SLOTS: usize, const LEN: usize>(
1430    pub &'static BufferPool<SLOTS, LEN>,
1431);
1432
1433impl<const SLOTS: usize, const LEN: usize> BufferProvider for StaticBufferProvider<SLOTS, LEN> {
1434    fn claim(&self) -> Option<BufferLease> {
1435        self.0.claim()
1436    }
1437}
1438
1439/// Zero-behavior implementations of the client- and server-side
1440/// dependency traits. Two uses: (1) compile-time proof the trait
1441/// signatures are implementable without async machinery, (2)
1442/// **layout probing** — `tools/size_probe` instantiates `Client`
1443/// with these on `thumbv7em-none-eabihf` so `-Zprint-type-sizes`
1444/// reports the real on-target future layouts (see
1445/// `docs/simple_someip/plans/2026-06-09-phase22-125-memory-reduction-design.md`).
1446///
1447/// NOT for production use: sockets error, and the spawner panics
1448/// outright — probe code is compiled, never executed, and a loud
1449/// failure beats the silent deadlock a future-dropping spawner
1450/// would cause in a driven `Client`.
1451#[cfg(any(test, feature = "bare_metal"))]
1452pub mod probe {
1453    use super::{
1454        E2ERegistryHandle, InterfaceHandle, ReceivedDatagram, SocketOptions, Spawner, Timer,
1455        TransportError, TransportFactory, TransportSocket,
1456    };
1457    use crate::e2e::{E2ECheckStatus, E2EKey, E2EProfile, Error as E2EError};
1458    use core::future::Future;
1459    use core::net::{IpAddr, Ipv4Addr, SocketAddrV4};
1460    use core::time::Duration;
1461
1462    /// Socket whose I/O futures resolve immediately with
1463    /// `TransportError::Unsupported`.
1464    pub struct NullSocket {
1465        addr: SocketAddrV4,
1466    }
1467
1468    impl NullSocket {
1469        #[must_use]
1470        pub const fn new(addr: SocketAddrV4) -> Self {
1471            Self { addr }
1472        }
1473    }
1474
1475    impl TransportSocket for NullSocket {
1476        type SendFuture<'a> = core::future::Ready<Result<(), TransportError>>;
1477        type RecvFuture<'a> = core::future::Ready<Result<ReceivedDatagram, TransportError>>;
1478
1479        fn send_to<'a>(&'a self, _buf: &'a [u8], _target: SocketAddrV4) -> Self::SendFuture<'a> {
1480            core::future::ready(Err(TransportError::Unsupported))
1481        }
1482
1483        fn recv_from<'a>(&'a self, _buf: &'a mut [u8]) -> Self::RecvFuture<'a> {
1484            core::future::ready(Err(TransportError::Unsupported))
1485        }
1486
1487        fn local_addr(&self) -> Result<SocketAddrV4, TransportError> {
1488            Ok(self.addr)
1489        }
1490
1491        fn join_multicast_v4(
1492            &self,
1493            _group: Ipv4Addr,
1494            _iface: Ipv4Addr,
1495        ) -> Result<(), TransportError> {
1496            Err(TransportError::Unsupported)
1497        }
1498
1499        fn leave_multicast_v4(
1500            &self,
1501            _group: Ipv4Addr,
1502            _iface: Ipv4Addr,
1503        ) -> Result<(), TransportError> {
1504            Err(TransportError::Unsupported)
1505        }
1506    }
1507
1508    /// Factory that "binds" a [`NullSocket`] at the requested addr.
1509    pub struct NullFactory;
1510
1511    impl TransportFactory for NullFactory {
1512        type Socket = NullSocket;
1513        type BindFuture<'a> = core::future::Ready<Result<Self::Socket, TransportError>>;
1514
1515        fn bind<'a>(
1516            &'a self,
1517            addr: SocketAddrV4,
1518            _options: &'a SocketOptions,
1519        ) -> Self::BindFuture<'a> {
1520            core::future::ready(Ok(NullSocket::new(addr)))
1521        }
1522    }
1523
1524    /// Timer whose sleeps resolve immediately.
1525    pub struct NullTimer;
1526
1527    impl Timer for NullTimer {
1528        type SleepFuture<'a> = core::future::Ready<()>;
1529
1530        fn sleep(&self, _duration: Duration) -> Self::SleepFuture<'_> {
1531            core::future::ready(())
1532        }
1533    }
1534
1535    /// E2E registry handle that registers nothing and checks nothing.
1536    #[derive(Clone)]
1537    pub struct NullE2ERegistry;
1538
1539    impl E2ERegistryHandle for NullE2ERegistry {
1540        fn register(
1541            &self,
1542            _key: E2EKey,
1543            _profile: E2EProfile,
1544        ) -> Result<(), crate::e2e::E2ERegistryFull> {
1545            Ok(())
1546        }
1547        fn unregister(&self, _key: &E2EKey) {}
1548        fn contains_key(&self, _key: &E2EKey) -> bool {
1549            false
1550        }
1551        fn protect(
1552            &self,
1553            _key: E2EKey,
1554            _payload: &[u8],
1555            _upper_header: [u8; 8],
1556            _output: &mut [u8],
1557        ) -> Option<Result<usize, E2EError>> {
1558            None
1559        }
1560        fn check<'a>(
1561            &self,
1562            _source: IpAddr,
1563            _key: E2EKey,
1564            _payload: &'a [u8],
1565            _upper_header: [u8; 8],
1566        ) -> Option<(E2ECheckStatus, &'a [u8])> {
1567            None
1568        }
1569        fn reset_source(&self, _source: IpAddr) {}
1570    }
1571
1572    /// Interface handle pinned to a fixed address.
1573    #[derive(Clone)]
1574    pub struct NullInterface(pub Ipv4Addr);
1575
1576    impl InterfaceHandle for NullInterface {
1577        fn get(&self) -> Ipv4Addr {
1578            self.0
1579        }
1580        fn set(&self, _addr: Ipv4Addr) {}
1581    }
1582
1583    /// Spawner that PANICS if asked to spawn. Probe code only
1584    /// constructs futures, never drives them — failing loudly beats
1585    /// violating [`Spawner`]'s poll-to-completion contract by
1586    /// silently dropping the future.
1587    pub struct NullSpawner;
1588
1589    impl Spawner for NullSpawner {
1590        fn spawn(&self, _future: impl Future<Output = ()> + Send + 'static) {
1591            panic!("NullSpawner is layout-probe-only; it never polls");
1592        }
1593    }
1594}
1595
1596#[cfg(test)]
1597mod tests {
1598    //! The traits are pure interfaces — these tests only verify that
1599    //! trivial mock implementations compile and that defaults behave as
1600    //! documented.
1601
1602    use super::probe::{NullE2ERegistry, NullFactory, NullInterface, NullSocket, NullTimer};
1603    use super::*;
1604
1605    /// `IoErrorKind::is_transient_recv` must classify the well-known
1606    /// transient kinds as `true` (so they do not count toward
1607    /// `MAX_CONSECUTIVE_RECV_ERRORS` in the per-socket loop) and
1608    /// everything else — including the catch-all `Other` — as `false`.
1609    /// Regression for H10: an inbound ICMP storm
1610    /// (`ConnectionRefused`) was wrongly counted as fatal and tore
1611    /// down healthy sockets after 16 transient blips.
1612    #[test]
1613    fn io_error_kind_transient_classification() {
1614        // Transient kinds — must NOT count toward fatal-error cap.
1615        assert!(IoErrorKind::ConnectionRefused.is_transient_recv());
1616        assert!(IoErrorKind::NetworkUnreachable.is_transient_recv());
1617        assert!(IoErrorKind::WouldBlock.is_transient_recv());
1618        assert!(IoErrorKind::Interrupted.is_transient_recv());
1619        assert!(IoErrorKind::TimedOut.is_transient_recv());
1620
1621        // Fatal-class kinds — DO count toward the cap.
1622        assert!(!IoErrorKind::PermissionDenied.is_transient_recv());
1623        assert!(!IoErrorKind::Other.is_transient_recv());
1624    }
1625
1626    /// Drive a Future to completion on the test thread, assuming it never
1627    /// yields (as with [`core::future::ready`] and its sync-in-disguise
1628    /// peers). Panics if the future returns `Poll::Pending`.
1629    fn block_on_ready<F: Future>(fut: F) -> F::Output {
1630        use core::pin::pin;
1631        use core::task::{Context, Poll, Waker};
1632        let waker = Waker::noop();
1633        let mut cx = Context::from_waker(waker);
1634        let mut fut = pin!(fut);
1635        match fut.as_mut().poll(&mut cx) {
1636            Poll::Ready(v) => v,
1637            Poll::Pending => panic!("future yielded Pending; use a real executor"),
1638        }
1639    }
1640
1641    #[test]
1642    fn socket_options_default_is_plain_unicast() {
1643        let opts = SocketOptions::default();
1644        assert!(!opts.reuse_address);
1645        assert!(!opts.reuse_port);
1646        assert!(opts.multicast_if_v4.is_none());
1647        assert!(opts.multicast_loop_v4.is_none());
1648    }
1649
1650    #[test]
1651    fn socket_options_new_matches_default() {
1652        let a = SocketOptions::new();
1653        let b = SocketOptions::default();
1654        assert_eq!(a.reuse_address, b.reuse_address);
1655        assert_eq!(a.reuse_port, b.reuse_port);
1656        assert_eq!(a.multicast_if_v4, b.multicast_if_v4);
1657        assert_eq!(a.multicast_loop_v4, b.multicast_loop_v4);
1658    }
1659
1660    #[test]
1661    fn null_factory_bind_resolves_with_addr() {
1662        let factory = NullFactory;
1663        let addr = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0);
1664        let options = SocketOptions::default();
1665        let sock = block_on_ready(factory.bind(addr, &options)).expect("bind");
1666        assert_eq!(sock.local_addr().unwrap(), addr);
1667    }
1668
1669    #[test]
1670    fn max_datagram_size_default_is_udp_buffer_size() {
1671        let sock = NullSocket::new(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0));
1672        assert_eq!(sock.max_datagram_size(), crate::UDP_BUFFER_SIZE);
1673    }
1674
1675    #[test]
1676    fn null_timer_sleep_resolves_immediately() {
1677        let timer = NullTimer;
1678        block_on_ready(timer.sleep(Duration::from_secs(1)));
1679    }
1680
1681    #[test]
1682    fn received_datagram_construct_and_field_access() {
1683        let d = ReceivedDatagram {
1684            bytes_received: 42,
1685            source: SocketAddrV4::new(Ipv4Addr::LOCALHOST, 9999),
1686            truncated: false,
1687        };
1688        assert_eq!(d.bytes_received, 42);
1689        assert!(!d.truncated);
1690    }
1691
1692    #[test]
1693    fn io_error_kind_variants_are_distinct() {
1694        // Compile-time check that all variants are constructible and
1695        // distinguishable — Eq is derived, so assert some inequalities.
1696        assert_ne!(IoErrorKind::TimedOut, IoErrorKind::Interrupted);
1697        assert_ne!(IoErrorKind::PermissionDenied, IoErrorKind::Other);
1698        assert_ne!(
1699            IoErrorKind::ConnectionRefused,
1700            IoErrorKind::NetworkUnreachable
1701        );
1702    }
1703
1704    #[test]
1705    fn transport_error_io_wraps_kind() {
1706        let e = TransportError::Io(IoErrorKind::TimedOut);
1707        assert_eq!(e, TransportError::Io(IoErrorKind::TimedOut));
1708        assert_ne!(e, TransportError::AddressInUse);
1709    }
1710
1711    #[test]
1712    fn null_e2e_registry_compiles() {
1713        let r = NullE2ERegistry;
1714        let key = E2EKey::new(0, 0);
1715        r.register(
1716            key,
1717            crate::e2e::E2EProfile::Profile4(crate::e2e::Profile4Config::new(0, 8)),
1718        )
1719        .expect("NullE2ERegistry::register is infallible");
1720        assert!(!r.contains_key(&key));
1721        assert!(
1722            r.check(Ipv4Addr::LOCALHOST.into(), key, b"hello", [0; 8])
1723                .is_none()
1724        );
1725        r.reset_source(Ipv4Addr::LOCALHOST.into()); // no-op in null impl
1726    }
1727
1728    #[test]
1729    fn null_interface_get_set() {
1730        let h = NullInterface(Ipv4Addr::LOCALHOST);
1731        assert_eq!(h.get(), Ipv4Addr::LOCALHOST);
1732        h.set(Ipv4Addr::UNSPECIFIED); // no-op in null impl
1733        assert_eq!(h.get(), Ipv4Addr::LOCALHOST); // unchanged
1734    }
1735}