Skip to main content

simple_someip/protocol/sd/
options.rs

1use core::net::{Ipv4Addr, Ipv6Addr};
2
3use super::Error;
4use crate::protocol::byte_order::WriteBytesExt;
5
6/// Maximum length of an SD configuration option string in bytes.
7pub const MAX_CONFIGURATION_STRING_LENGTH: usize = 256;
8
9// --- SD option wire-layout constants ---
10//
11// Every SD option begins with a 4-byte fixed header:
12//   [0..2]: length (u16 BE)        — value is `wire_size - OPTION_LENGTH_SIZE_DELTA`
13//   [2]:    option type (u8)
14//   [3]:    reserved/discard flag (u8)
15// Per-type payload follows starting at offset `OPTION_PAYLOAD_OFFSET`.
16
17/// Size of the fixed SD option header (length + type + discard flag).
18pub(crate) const OPTION_HEADER_SIZE: usize = 4;
19/// The SD option length field encodes `wire_size - OPTION_LENGTH_SIZE_DELTA`.
20pub(crate) const OPTION_LENGTH_SIZE_DELTA: usize = 3;
21/// Byte offset of the option type byte inside the fixed header.
22const OPTION_TYPE_OFFSET: usize = 2;
23/// Byte offset at which per-type payload begins.
24const OPTION_PAYLOAD_OFFSET: usize = 4;
25
26// IPv4 endpoint / multicast / SD options.
27/// Total wire size of an IPv4 endpoint/multicast/SD option.
28pub(crate) const IPV4_OPTION_WIRE_SIZE: usize = 12;
29/// Length-field value stored on the wire for an IPv4 option.
30pub(crate) const IPV4_OPTION_LENGTH_FIELD: u16 = 9;
31/// Byte offset of the 4-octet IPv4 address within the option.
32pub(crate) const IPV4_OPTION_IP_OFFSET: usize = OPTION_PAYLOAD_OFFSET;
33/// Byte offset of the transport protocol byte inside an IPv4 option.
34pub(crate) const IPV4_OPTION_PROTOCOL_OFFSET: usize = 9;
35/// Byte offset of the port (u16 BE) inside an IPv4 option.
36pub(crate) const IPV4_OPTION_PORT_OFFSET: usize = 10;
37
38// IPv6 endpoint / multicast / SD options.
39/// Total wire size of an IPv6 endpoint/multicast/SD option.
40pub(crate) const IPV6_OPTION_WIRE_SIZE: usize = 24;
41/// Length-field value stored on the wire for an IPv6 option.
42pub(crate) const IPV6_OPTION_LENGTH_FIELD: u16 = 21;
43/// Byte offset of the 16-octet IPv6 address within the option.
44const IPV6_OPTION_IP_OFFSET: usize = OPTION_PAYLOAD_OFFSET;
45/// Byte offset (exclusive) marking the end of the 16-octet IPv6 address.
46const IPV6_OPTION_IP_END: usize = IPV6_OPTION_IP_OFFSET + 16;
47/// Byte offset of the transport protocol byte inside an IPv6 option.
48pub(crate) const IPV6_OPTION_PROTOCOL_OFFSET: usize = 21;
49/// Byte offset of the port (u16 BE) inside an IPv6 option.
50const IPV6_OPTION_PORT_OFFSET: usize = 22;
51
52// Load-balancing option.
53/// Total wire size of a load-balancing option.
54const LOAD_BALANCING_OPTION_WIRE_SIZE: usize = 8;
55/// Length-field value stored on the wire for a load-balancing option.
56pub(crate) const LOAD_BALANCING_OPTION_LENGTH_FIELD: u16 = 5;
57
58// Configuration option.
59/// The configuration option's length field value is `1 + string_len`
60/// (the `+1` accounts for the trailing null terminator byte).
61const CONFIGURATION_OPTION_LENGTH_STRING_DELTA: u16 = 1;
62
63/// Transport protocol used in SD endpoint options.
64#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
65pub enum TransportProtocol {
66    /// UDP (0x11).
67    Udp,
68    /// TCP (0x06).
69    Tcp,
70}
71
72impl TryFrom<u8> for TransportProtocol {
73    type Error = Error;
74    fn try_from(value: u8) -> Result<Self, Error> {
75        match value {
76            0x11 => Ok(TransportProtocol::Udp),
77            0x06 => Ok(TransportProtocol::Tcp),
78            _ => Err(Error::InvalidOptionTransportProtocol(value)),
79        }
80    }
81}
82
83impl From<TransportProtocol> for u8 {
84    fn from(transport_protocol: TransportProtocol) -> u8 {
85        match transport_protocol {
86            TransportProtocol::Udp => 0x11,
87            TransportProtocol::Tcp => 0x06,
88        }
89    }
90}
91
92/// The type of an SD option.
93#[derive(Clone, Copy, Debug, Eq, PartialEq)]
94pub enum OptionType {
95    /// Configuration option (0x01).
96    Configuration,
97    /// Load balancing option (0x02).
98    LoadBalancing,
99    /// IPv4 endpoint option (0x04).
100    IpV4Endpoint,
101    /// IPv6 endpoint option (0x06).
102    IpV6Endpoint,
103    /// IPv4 multicast option (0x14).
104    IpV4Multicast,
105    /// IPv6 multicast option (0x16).
106    IpV6Multicast,
107    /// IPv4 SD option (0x24).
108    IpV4SD,
109    /// IPv6 SD option (0x26).
110    IpV6SD,
111}
112
113impl TryFrom<u8> for OptionType {
114    type Error = Error;
115    fn try_from(value: u8) -> Result<Self, Error> {
116        match value {
117            0x01 => Ok(OptionType::Configuration),
118            0x02 => Ok(OptionType::LoadBalancing),
119            0x04 => Ok(OptionType::IpV4Endpoint),
120            0x06 => Ok(OptionType::IpV6Endpoint),
121            0x14 => Ok(OptionType::IpV4Multicast),
122            0x16 => Ok(OptionType::IpV6Multicast),
123            0x24 => Ok(OptionType::IpV4SD),
124            0x26 => Ok(OptionType::IpV6SD),
125            _ => Err(Error::InvalidOptionType(value)),
126        }
127    }
128}
129
130impl From<OptionType> for u8 {
131    fn from(option_type: OptionType) -> u8 {
132        match option_type {
133            OptionType::Configuration => 0x01,
134            OptionType::LoadBalancing => 0x02,
135            OptionType::IpV4Endpoint => 0x04,
136            OptionType::IpV6Endpoint => 0x06,
137            OptionType::IpV4Multicast => 0x14,
138            OptionType::IpV6Multicast => 0x16,
139            OptionType::IpV4SD => 0x24,
140            OptionType::IpV6SD => 0x26,
141        }
142    }
143}
144
145// Boxing is not available in no_std, so allow the large variant.
146#[allow(clippy::large_enum_variant)]
147/// A decoded SD option.
148#[derive(Clone, Debug, Eq, PartialEq)]
149pub enum Options {
150    /// A configuration key-value string.
151    Configuration {
152        /// The raw configuration string bytes.
153        configuration_string: heapless::Vec<u8, MAX_CONFIGURATION_STRING_LENGTH>,
154    },
155    /// Load balancing parameters.
156    LoadBalancing {
157        /// The priority value.
158        priority: u16,
159        /// The weight value.
160        weight: u16,
161    },
162    /// An IPv4 endpoint.
163    IpV4Endpoint {
164        /// The IPv4 address.
165        ip: Ipv4Addr,
166        /// The transport protocol (UDP or TCP).
167        protocol: TransportProtocol,
168        /// The port number.
169        port: u16,
170    },
171    /// An IPv6 endpoint.
172    IpV6Endpoint {
173        /// The IPv6 address.
174        ip: Ipv6Addr,
175        /// The transport protocol (UDP or TCP).
176        protocol: TransportProtocol,
177        /// The port number.
178        port: u16,
179    },
180    /// An IPv4 multicast address.
181    IpV4Multicast {
182        /// The IPv4 multicast address.
183        ip: Ipv4Addr,
184        /// The transport protocol (UDP or TCP).
185        protocol: TransportProtocol,
186        /// The port number.
187        port: u16,
188    },
189    /// An IPv6 multicast address.
190    IpV6Multicast {
191        /// The IPv6 multicast address.
192        ip: Ipv6Addr,
193        /// The transport protocol (UDP or TCP).
194        protocol: TransportProtocol,
195        /// The port number.
196        port: u16,
197    },
198    /// An IPv4 SD endpoint.
199    IpV4SD {
200        /// The IPv4 address.
201        ip: Ipv4Addr,
202        /// The transport protocol (UDP or TCP).
203        protocol: TransportProtocol,
204        /// The port number.
205        port: u16,
206    },
207    /// An IPv6 SD endpoint.
208    IpV6SD {
209        /// The IPv6 address.
210        ip: Ipv6Addr,
211        /// The transport protocol (UDP or TCP).
212        protocol: TransportProtocol,
213        /// The port number.
214        port: u16,
215    },
216}
217
218impl Options {
219    /// Returns the total wire size of this option in bytes.
220    #[must_use]
221    pub fn size(&self) -> usize {
222        match self {
223            Options::Configuration {
224                configuration_string,
225            } => OPTION_HEADER_SIZE + configuration_string.len(),
226            Options::LoadBalancing { .. } => LOAD_BALANCING_OPTION_WIRE_SIZE,
227            Options::IpV4Endpoint { .. }
228            | Options::IpV4Multicast { .. }
229            | Options::IpV4SD { .. } => IPV4_OPTION_WIRE_SIZE,
230            Options::IpV6Endpoint { .. }
231            | Options::IpV6Multicast { .. }
232            | Options::IpV6SD { .. } => IPV6_OPTION_WIRE_SIZE,
233        }
234    }
235
236    /// Serializes this option to a writer.
237    ///
238    /// # Errors
239    ///
240    /// Returns an error if writing to the writer fails.
241    ///
242    /// # Panics
243    ///
244    /// Panics if the option size minus [`OPTION_LENGTH_SIZE_DELTA`] exceeds `u16::MAX`
245    /// (unreachable in practice).
246    pub fn write<T: embedded_io::Write>(
247        &self,
248        writer: &mut T,
249    ) -> Result<usize, crate::protocol::Error> {
250        writer.write_u16_be(
251            u16::try_from(self.size() - OPTION_LENGTH_SIZE_DELTA).expect("option size fits u16"),
252        )?;
253        match self {
254            Options::Configuration {
255                configuration_string,
256            } => {
257                writer.write_u8(u8::from(OptionType::Configuration))?;
258                writer.write_u8(0)?;
259                writer.write_bytes(configuration_string)?;
260                Ok(self.size())
261            }
262            Options::LoadBalancing { priority, weight } => {
263                writer.write_u8(u8::from(OptionType::LoadBalancing))?;
264                writer.write_u8(0)?;
265                writer.write_u16_be(*priority)?;
266                writer.write_u16_be(*weight)?;
267                Ok(LOAD_BALANCING_OPTION_WIRE_SIZE)
268            }
269            Options::IpV4Endpoint { ip, protocol, port } => {
270                write_ipv4_option(writer, OptionType::IpV4Endpoint, *ip, *protocol, *port)
271            }
272            Options::IpV6Endpoint { ip, protocol, port } => {
273                write_ipv6_option(writer, OptionType::IpV6Endpoint, *ip, *protocol, *port)
274            }
275            Options::IpV4Multicast { ip, protocol, port } => {
276                write_ipv4_option(writer, OptionType::IpV4Multicast, *ip, *protocol, *port)
277            }
278            Options::IpV6Multicast { ip, protocol, port } => {
279                write_ipv6_option(writer, OptionType::IpV6Multicast, *ip, *protocol, *port)
280            }
281            Options::IpV4SD { ip, protocol, port } => {
282                write_ipv4_option(writer, OptionType::IpV4SD, *ip, *protocol, *port)
283            }
284            Options::IpV6SD { ip, protocol, port } => {
285                write_ipv6_option(writer, OptionType::IpV6SD, *ip, *protocol, *port)
286            }
287        }
288    }
289}
290
291fn write_ipv4_option<T: embedded_io::Write>(
292    writer: &mut T,
293    option_type: OptionType,
294    ip: Ipv4Addr,
295    protocol: TransportProtocol,
296    port: u16,
297) -> Result<usize, crate::protocol::Error> {
298    writer.write_u8(u8::from(option_type))?;
299    writer.write_u8(0)?;
300    writer.write_u32_be(ip.to_bits())?;
301    writer.write_u8(0)?;
302    writer.write_u8(u8::from(protocol))?;
303    writer.write_u16_be(port)?;
304    Ok(IPV4_OPTION_WIRE_SIZE)
305}
306
307fn write_ipv6_option<T: embedded_io::Write>(
308    writer: &mut T,
309    option_type: OptionType,
310    ip: Ipv6Addr,
311    protocol: TransportProtocol,
312    port: u16,
313) -> Result<usize, crate::protocol::Error> {
314    writer.write_u8(u8::from(option_type))?;
315    writer.write_u8(0)?;
316    writer.write_bytes(&ip.octets())?;
317    writer.write_u8(0)?;
318    writer.write_u8(u8::from(protocol))?;
319    writer.write_u16_be(port)?;
320    Ok(IPV6_OPTION_WIRE_SIZE)
321}
322
323/// Extract the first `IpV4Endpoint` address from a slice of owned options.
324///
325/// Returns `None` if no `IpV4Endpoint` option is present.
326#[must_use]
327pub fn extract_ipv4_endpoint(options: &[Options]) -> Option<core::net::SocketAddrV4> {
328    options.iter().find_map(|opt| match opt {
329        Options::IpV4Endpoint { ip, port, .. } => Some(core::net::SocketAddrV4::new(*ip, *port)),
330        _ => None,
331    })
332}
333
334// --- Zero-copy view types ---
335
336/// Zero-copy view into a variable-length SD option in a buffer.
337///
338/// Wire layout:
339/// - `[0..2]`: length (u16 BE) = `total_size` - 3
340/// - `[2]`: option type (u8)
341/// - `[3]`: reserved/discard flag (u8)
342/// - `[4..]`: type-specific data
343#[derive(Clone, Copy, Debug)]
344pub struct OptionView<'a>(&'a [u8]);
345
346impl<'a> OptionView<'a> {
347    /// Returns the option type.
348    ///
349    /// # Errors
350    ///
351    /// Returns [`Error::InvalidOptionType`] if the type byte is unrecognized.
352    pub fn option_type(&self) -> Result<OptionType, Error> {
353        OptionType::try_from(self.0[OPTION_TYPE_OFFSET])
354    }
355
356    /// Total wire size of this option (length field value + [`OPTION_LENGTH_SIZE_DELTA`]).
357    #[must_use]
358    pub fn wire_size(&self) -> usize {
359        let length = u16::from_be_bytes([self.0[0], self.0[1]]);
360        usize::from(length) + OPTION_LENGTH_SIZE_DELTA
361    }
362
363    /// Parse as IPv4 endpoint/multicast/SD option.
364    /// Returns `(ip, protocol, port)`.
365    ///
366    /// # Errors
367    ///
368    /// Returns [`Error::InvalidOptionTransportProtocol`] if the protocol byte is unrecognized.
369    /// Views obtained via [`SdHeaderView::parse`](super::SdHeaderView::parse) have already
370    /// had their protocol byte validated, so this error cannot occur for those callers —
371    /// it is retained only to keep the API usable if an `OptionView` is ever constructed
372    /// outside the validated parse path.
373    pub fn as_ipv4(&self) -> Result<(Ipv4Addr, TransportProtocol, u16), Error> {
374        let ip = Ipv4Addr::from_bits(u32::from_be_bytes([
375            self.0[IPV4_OPTION_IP_OFFSET],
376            self.0[IPV4_OPTION_IP_OFFSET + 1],
377            self.0[IPV4_OPTION_IP_OFFSET + 2],
378            self.0[IPV4_OPTION_IP_OFFSET + 3],
379        ]));
380        let protocol = TransportProtocol::try_from(self.0[IPV4_OPTION_PROTOCOL_OFFSET])?;
381        let port = u16::from_be_bytes([
382            self.0[IPV4_OPTION_PORT_OFFSET],
383            self.0[IPV4_OPTION_PORT_OFFSET + 1],
384        ]);
385        Ok((ip, protocol, port))
386    }
387
388    /// Parse as IPv6 endpoint/multicast/SD option.
389    /// Returns `(ip, protocol, port)`.
390    ///
391    /// # Errors
392    ///
393    /// Returns [`Error::InvalidOptionTransportProtocol`] if the protocol byte is unrecognized.
394    /// Views obtained via [`SdHeaderView::parse`](super::SdHeaderView::parse) have already
395    /// had their protocol byte validated, so this error cannot occur for those callers —
396    /// it is retained only to keep the API usable if an `OptionView` is ever constructed
397    /// outside the validated parse path.
398    pub fn as_ipv6(&self) -> Result<(Ipv6Addr, TransportProtocol, u16), Error> {
399        let mut octets = [0u8; 16];
400        octets.copy_from_slice(&self.0[IPV6_OPTION_IP_OFFSET..IPV6_OPTION_IP_END]);
401        let ip = Ipv6Addr::from(octets);
402        let protocol = TransportProtocol::try_from(self.0[IPV6_OPTION_PROTOCOL_OFFSET])?;
403        let port = u16::from_be_bytes([
404            self.0[IPV6_OPTION_PORT_OFFSET],
405            self.0[IPV6_OPTION_PORT_OFFSET + 1],
406        ]);
407        Ok((ip, protocol, port))
408    }
409
410    /// Raw configuration bytes (for Configuration options).
411    #[must_use]
412    pub fn configuration_bytes(&self) -> &'a [u8] {
413        let length = u16::from_be_bytes([self.0[0], self.0[1]]);
414        let string_len = length.saturating_sub(CONFIGURATION_OPTION_LENGTH_STRING_DELTA);
415        &self.0[OPTION_PAYLOAD_OFFSET..OPTION_PAYLOAD_OFFSET + usize::from(string_len)]
416    }
417
418    /// Parse as load-balancing option. Returns `(priority, weight)`.
419    ///
420    /// # Errors
421    ///
422    /// Currently always succeeds; the `Result` return type is reserved for future validation.
423    pub fn as_load_balancing(&self) -> Result<(u16, u16), Error> {
424        let priority = u16::from_be_bytes([
425            self.0[OPTION_PAYLOAD_OFFSET],
426            self.0[OPTION_PAYLOAD_OFFSET + 1],
427        ]);
428        let weight = u16::from_be_bytes([
429            self.0[OPTION_PAYLOAD_OFFSET + 2],
430            self.0[OPTION_PAYLOAD_OFFSET + 3],
431        ]);
432        Ok((priority, weight))
433    }
434
435    /// Converts this view into an owned [`Options`].
436    ///
437    /// # Errors
438    ///
439    /// Returns an error if the option type is unrecognized, the transport protocol byte
440    /// is invalid, or the configuration string exceeds [`MAX_CONFIGURATION_STRING_LENGTH`].
441    ///
442    /// # Panics
443    ///
444    /// Panics if a configuration string passes the length check but fails to fit into the
445    /// heapless buffer (unreachable in practice).
446    pub fn to_owned(&self) -> Result<Options, Error> {
447        let option_type = self.option_type()?;
448        match option_type {
449            OptionType::Configuration => {
450                let config_bytes = self.configuration_bytes();
451                if config_bytes.len() > MAX_CONFIGURATION_STRING_LENGTH {
452                    return Err(Error::ConfigurationStringTooLong(config_bytes.len()));
453                }
454                let mut configuration_string =
455                    heapless::Vec::<u8, MAX_CONFIGURATION_STRING_LENGTH>::new();
456                configuration_string
457                    .extend_from_slice(config_bytes)
458                    .expect("length validated above");
459                Ok(Options::Configuration {
460                    configuration_string,
461                })
462            }
463            OptionType::LoadBalancing => {
464                let (priority, weight) = self.as_load_balancing()?;
465                Ok(Options::LoadBalancing { priority, weight })
466            }
467            OptionType::IpV4Endpoint => {
468                let (ip, protocol, port) = self.as_ipv4()?;
469                Ok(Options::IpV4Endpoint { ip, protocol, port })
470            }
471            OptionType::IpV6Endpoint => {
472                let (ip, protocol, port) = self.as_ipv6()?;
473                Ok(Options::IpV6Endpoint { ip, protocol, port })
474            }
475            OptionType::IpV4Multicast => {
476                let (ip, protocol, port) = self.as_ipv4()?;
477                Ok(Options::IpV4Multicast { ip, protocol, port })
478            }
479            OptionType::IpV6Multicast => {
480                let (ip, protocol, port) = self.as_ipv6()?;
481                Ok(Options::IpV6Multicast { ip, protocol, port })
482            }
483            OptionType::IpV4SD => {
484                let (ip, protocol, port) = self.as_ipv4()?;
485                Ok(Options::IpV4SD { ip, protocol, port })
486            }
487            OptionType::IpV6SD => {
488                let (ip, protocol, port) = self.as_ipv6()?;
489                Ok(Options::IpV6SD { ip, protocol, port })
490            }
491        }
492    }
493}
494
495/// Iterator over variable-length SD options in a validated buffer.
496/// Options are guaranteed valid (validated upfront in `SdHeaderView::parse`).
497///
498/// `OptionIter` is a thin wrapper around a borrowed byte slice and is
499/// `Clone`, so callers that need to walk the same options multiple
500/// times (e.g. to extract the subset referenced by a particular entry's
501/// options run) can explicitly clone the iterator. It is deliberately
502/// **not** `Copy` — making an iterator `Copy` is a footgun because
503/// advancing the original does not advance the hidden copies, which
504/// makes "this iterator is already exhausted" invariants easy to break
505/// accidentally. Clone when you mean to reuse; don't let the compiler
506/// duplicate for you.
507#[derive(Clone)]
508pub struct OptionIter<'a> {
509    remaining: &'a [u8],
510}
511
512impl<'a> OptionIter<'a> {
513    pub(crate) fn new(buf: &'a [u8]) -> Self {
514        Self { remaining: buf }
515    }
516}
517
518impl<'a> Iterator for OptionIter<'a> {
519    type Item = OptionView<'a>;
520
521    fn next(&mut self) -> Option<Self::Item> {
522        if self.remaining.len() < OPTION_HEADER_SIZE {
523            return None;
524        }
525        let length = u16::from_be_bytes([self.remaining[0], self.remaining[1]]);
526        let wire_size = usize::from(length) + OPTION_LENGTH_SIZE_DELTA;
527        if wire_size > self.remaining.len() {
528            return None;
529        }
530        let view = OptionView(&self.remaining[..wire_size]);
531        self.remaining = &self.remaining[wire_size..];
532        Some(view)
533    }
534}
535
536/// Validate a single option's wire format and return its wire size.
537/// Used during `SdHeaderView::parse` for upfront validation.
538///
539/// In addition to length/type checks, this validates the transport protocol
540/// byte of IP-bearing options so that `OptionView::as_ipv4` / `as_ipv6` on
541/// views obtained through `SdHeaderView::parse` cannot observe an unknown
542/// protocol byte.
543pub(crate) fn validate_option(buf: &[u8]) -> Result<usize, Error> {
544    if buf.len() < OPTION_HEADER_SIZE {
545        return Err(Error::IncorrectOptionsSize(buf.len()));
546    }
547    let length = u16::from_be_bytes([buf[0], buf[1]]);
548    let wire_size = usize::from(length) + OPTION_LENGTH_SIZE_DELTA;
549    if wire_size > buf.len() {
550        return Err(Error::IncorrectOptionsSize(buf.len()));
551    }
552    let option_type_byte = buf[OPTION_TYPE_OFFSET];
553    let option_type = OptionType::try_from(option_type_byte)?;
554    // Validate expected lengths for fixed-size options
555    match option_type {
556        OptionType::IpV4Endpoint | OptionType::IpV4Multicast | OptionType::IpV4SD => {
557            if length != IPV4_OPTION_LENGTH_FIELD {
558                return Err(Error::InvalidOptionLength {
559                    option_type: option_type_byte,
560                    expected: IPV4_OPTION_LENGTH_FIELD,
561                    actual: length,
562                });
563            }
564            TransportProtocol::try_from(buf[IPV4_OPTION_PROTOCOL_OFFSET])?;
565        }
566        OptionType::IpV6Endpoint | OptionType::IpV6Multicast | OptionType::IpV6SD => {
567            if length != IPV6_OPTION_LENGTH_FIELD {
568                return Err(Error::InvalidOptionLength {
569                    option_type: option_type_byte,
570                    expected: IPV6_OPTION_LENGTH_FIELD,
571                    actual: length,
572                });
573            }
574            TransportProtocol::try_from(buf[IPV6_OPTION_PROTOCOL_OFFSET])?;
575        }
576        OptionType::LoadBalancing => {
577            if length != LOAD_BALANCING_OPTION_LENGTH_FIELD {
578                return Err(Error::InvalidOptionLength {
579                    option_type: option_type_byte,
580                    expected: LOAD_BALANCING_OPTION_LENGTH_FIELD,
581                    actual: length,
582                });
583            }
584        }
585        OptionType::Configuration => {
586            // Configuration strings are variable length; just check it doesn't exceed max
587            let string_len = length.saturating_sub(CONFIGURATION_OPTION_LENGTH_STRING_DELTA);
588            if usize::from(string_len) > MAX_CONFIGURATION_STRING_LENGTH {
589                return Err(Error::ConfigurationStringTooLong(string_len.into()));
590            }
591        }
592    }
593    Ok(wire_size)
594}
595
596#[cfg(test)]
597mod tests {
598    use core::net::{Ipv4Addr, Ipv6Addr};
599
600    use super::*;
601
602    // --- TransportProtocol ---
603
604    #[test]
605    fn transport_protocol_tcp_round_trip() {
606        assert_eq!(
607            TransportProtocol::try_from(0x06).unwrap(),
608            TransportProtocol::Tcp
609        );
610        assert_eq!(u8::from(TransportProtocol::Tcp), 0x06);
611    }
612
613    #[test]
614    fn transport_protocol_invalid_returns_error() {
615        assert!(matches!(
616            TransportProtocol::try_from(0xFF),
617            Err(Error::InvalidOptionTransportProtocol(0xFF))
618        ));
619    }
620
621    // --- OptionView: parse from encoded bytes ---
622
623    #[test]
624    fn option_view_ipv4_endpoint_tcp() {
625        let buf: [u8; 12] = [
626            0x00, 0x09, // length = 9
627            0x04, // type = IpV4Endpoint
628            0x00, // discard flag
629            192, 168, 0, 1,    // ip
630            0x00, // reserved
631            0x06, // protocol = TCP
632            0x04, 0xD2, // port = 1234
633        ];
634        let view = OptionView(&buf);
635        assert_eq!(view.option_type().unwrap(), OptionType::IpV4Endpoint);
636        assert_eq!(view.wire_size(), 12);
637        let (ip, protocol, port) = view.as_ipv4().unwrap();
638        assert_eq!(ip, Ipv4Addr::new(192, 168, 0, 1));
639        assert_eq!(protocol, TransportProtocol::Tcp);
640        assert_eq!(port, 1234);
641    }
642
643    #[test]
644    fn option_view_to_owned_invalid_type() {
645        let buf: [u8; 4] = [0x00, 0x00, 0xFF, 0x00]; // type = 0xFF (invalid)
646        let view = OptionView(&buf);
647        assert!(matches!(
648            view.to_owned(),
649            Err(Error::InvalidOptionType(0xFF))
650        ));
651    }
652
653    // --- Round-trip tests for all option types ---
654
655    fn round_trip(option: &Options) {
656        let size = option.size();
657        let mut buf = [0u8; 4 + MAX_CONFIGURATION_STRING_LENGTH];
658        let written = option.write(&mut &mut buf[..size]).unwrap();
659        assert_eq!(written, size);
660        let view = OptionView(&buf[..size]);
661        let parsed = view.to_owned().unwrap();
662        assert_eq!(*option, parsed);
663    }
664
665    #[test]
666    fn configuration_round_trip() {
667        let mut config_string = heapless::Vec::<u8, MAX_CONFIGURATION_STRING_LENGTH>::new();
668        config_string.extend_from_slice(b"test=value").unwrap();
669        let option = Options::Configuration {
670            configuration_string: config_string,
671        };
672        round_trip(&option);
673    }
674
675    #[test]
676    fn configuration_empty_round_trip() {
677        let option = Options::Configuration {
678            configuration_string: heapless::Vec::new(),
679        };
680        round_trip(&option);
681    }
682
683    #[test]
684    fn load_balancing_round_trip() {
685        let option = Options::LoadBalancing {
686            priority: 100,
687            weight: 200,
688        };
689        round_trip(&option);
690    }
691
692    #[test]
693    fn ipv4_endpoint_round_trip() {
694        let option = Options::IpV4Endpoint {
695            ip: Ipv4Addr::new(10, 0, 0, 1),
696            protocol: TransportProtocol::Udp,
697            port: 30490,
698        };
699        round_trip(&option);
700    }
701
702    #[test]
703    fn ipv6_endpoint_round_trip() {
704        let option = Options::IpV6Endpoint {
705            ip: Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1),
706            protocol: TransportProtocol::Tcp,
707            port: 8080,
708        };
709        round_trip(&option);
710    }
711
712    #[test]
713    fn ipv4_multicast_round_trip() {
714        let option = Options::IpV4Multicast {
715            ip: Ipv4Addr::new(239, 0, 0, 1),
716            protocol: TransportProtocol::Udp,
717            port: 30490,
718        };
719        round_trip(&option);
720    }
721
722    #[test]
723    fn ipv6_multicast_round_trip() {
724        let option = Options::IpV6Multicast {
725            ip: Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 0, 1),
726            protocol: TransportProtocol::Udp,
727            port: 30490,
728        };
729        round_trip(&option);
730    }
731
732    #[test]
733    fn ipv4_sd_round_trip() {
734        let option = Options::IpV4SD {
735            ip: Ipv4Addr::new(172, 16, 0, 1),
736            protocol: TransportProtocol::Udp,
737            port: 30490,
738        };
739        round_trip(&option);
740    }
741
742    #[test]
743    fn ipv6_sd_round_trip() {
744        let option = Options::IpV6SD {
745            ip: Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1),
746            protocol: TransportProtocol::Tcp,
747            port: 9999,
748        };
749        round_trip(&option);
750    }
751
752    // --- Error cases ---
753
754    #[test]
755    fn load_balancing_invalid_length_returns_error() {
756        // length = 3 (wrong, should be 5), wire_size = 6
757        let mut buf = [0u8; 6];
758        buf[0] = 0x00;
759        buf[1] = 0x03; // length = 3
760        buf[2] = 0x02; // type = LoadBalancing
761        buf[3] = 0x00; // discard flag
762        assert!(matches!(
763            validate_option(&buf),
764            Err(Error::InvalidOptionLength {
765                option_type: 0x02,
766                expected: 5,
767                actual: 3,
768            })
769        ));
770    }
771
772    #[test]
773    fn ipv4_endpoint_invalid_length_returns_error() {
774        // length = 5 (wrong, should be 9), wire_size = 8
775        let mut buf = [0u8; 8];
776        buf[0] = 0x00;
777        buf[1] = 0x05; // length = 5
778        buf[2] = 0x04; // type = IpV4Endpoint
779        buf[3] = 0x00;
780        assert!(matches!(
781            validate_option(&buf),
782            Err(Error::InvalidOptionLength {
783                option_type: 0x04,
784                expected: 9,
785                actual: 5,
786            })
787        ));
788    }
789
790    #[test]
791    fn ipv6_endpoint_invalid_length_returns_error() {
792        // length = 9 (wrong, should be 21), wire_size = 12
793        let mut buf = [0u8; 12];
794        buf[0] = 0x00;
795        buf[1] = 0x09; // length = 9
796        buf[2] = 0x06; // type = IpV6Endpoint
797        buf[3] = 0x00;
798        assert!(matches!(
799            validate_option(&buf),
800            Err(Error::InvalidOptionLength {
801                option_type: 0x06,
802                expected: 21,
803                actual: 9,
804            })
805        ));
806    }
807
808    #[test]
809    fn ipv4_multicast_invalid_length_returns_error() {
810        // length = 5 (wrong, should be 9), wire_size = 8
811        let mut buf = [0u8; 8];
812        buf[0] = 0x00;
813        buf[1] = 0x05;
814        buf[2] = 0x14; // type = IpV4Multicast
815        buf[3] = 0x00;
816        assert!(matches!(
817            validate_option(&buf),
818            Err(Error::InvalidOptionLength {
819                option_type: 0x14,
820                expected: 9,
821                actual: 5,
822            })
823        ));
824    }
825
826    #[test]
827    fn ipv6_multicast_invalid_length_returns_error() {
828        // length = 9 (wrong, should be 21), wire_size = 12
829        let mut buf = [0u8; 12];
830        buf[0] = 0x00;
831        buf[1] = 0x09;
832        buf[2] = 0x16; // type = IpV6Multicast
833        buf[3] = 0x00;
834        assert!(matches!(
835            validate_option(&buf),
836            Err(Error::InvalidOptionLength {
837                option_type: 0x16,
838                expected: 21,
839                actual: 9,
840            })
841        ));
842    }
843
844    #[test]
845    fn ipv4_sd_invalid_length_returns_error() {
846        // length = 5 (wrong, should be 9), wire_size = 8
847        let mut buf = [0u8; 8];
848        buf[0] = 0x00;
849        buf[1] = 0x05;
850        buf[2] = 0x24; // type = IpV4SD
851        buf[3] = 0x00;
852        assert!(matches!(
853            validate_option(&buf),
854            Err(Error::InvalidOptionLength {
855                option_type: 0x24,
856                expected: 9,
857                actual: 5,
858            })
859        ));
860    }
861
862    /// Build a well-formed IPv4 option wire buffer (length + type correct) with a
863    /// caller-chosen transport protocol byte — used to exercise protocol-byte
864    /// validation without hand-rolling wire offsets.
865    fn ipv4_option_with_protocol(
866        option_type: OptionType,
867        protocol_byte: u8,
868    ) -> [u8; IPV4_OPTION_WIRE_SIZE] {
869        let mut buf = [0u8; IPV4_OPTION_WIRE_SIZE];
870        buf[0..2].copy_from_slice(&IPV4_OPTION_LENGTH_FIELD.to_be_bytes());
871        buf[OPTION_TYPE_OFFSET] = u8::from(option_type);
872        buf[IPV4_OPTION_PROTOCOL_OFFSET] = protocol_byte;
873        buf
874    }
875
876    /// Build a well-formed IPv6 option wire buffer (length + type correct) with a
877    /// caller-chosen transport protocol byte.
878    fn ipv6_option_with_protocol(
879        option_type: OptionType,
880        protocol_byte: u8,
881    ) -> [u8; IPV6_OPTION_WIRE_SIZE] {
882        let mut buf = [0u8; IPV6_OPTION_WIRE_SIZE];
883        buf[0..2].copy_from_slice(&IPV6_OPTION_LENGTH_FIELD.to_be_bytes());
884        buf[OPTION_TYPE_OFFSET] = u8::from(option_type);
885        buf[IPV6_OPTION_PROTOCOL_OFFSET] = protocol_byte;
886        buf
887    }
888
889    #[test]
890    fn ipv4_endpoint_invalid_transport_protocol_returns_error() {
891        let buf = ipv4_option_with_protocol(OptionType::IpV4Endpoint, 0xAB);
892        assert!(matches!(
893            validate_option(&buf),
894            Err(Error::InvalidOptionTransportProtocol(0xAB))
895        ));
896    }
897
898    #[test]
899    fn ipv4_multicast_invalid_transport_protocol_returns_error() {
900        let buf = ipv4_option_with_protocol(OptionType::IpV4Multicast, 0x42);
901        assert!(matches!(
902            validate_option(&buf),
903            Err(Error::InvalidOptionTransportProtocol(0x42))
904        ));
905    }
906
907    #[test]
908    fn ipv4_sd_invalid_transport_protocol_returns_error() {
909        let buf = ipv4_option_with_protocol(OptionType::IpV4SD, 0x01);
910        assert!(matches!(
911            validate_option(&buf),
912            Err(Error::InvalidOptionTransportProtocol(0x01))
913        ));
914    }
915
916    #[test]
917    fn ipv6_endpoint_invalid_transport_protocol_returns_error() {
918        let buf = ipv6_option_with_protocol(OptionType::IpV6Endpoint, 0x99);
919        assert!(matches!(
920            validate_option(&buf),
921            Err(Error::InvalidOptionTransportProtocol(0x99))
922        ));
923    }
924
925    #[test]
926    fn ipv6_multicast_invalid_transport_protocol_returns_error() {
927        let buf = ipv6_option_with_protocol(OptionType::IpV6Multicast, 0x00);
928        assert!(matches!(
929            validate_option(&buf),
930            Err(Error::InvalidOptionTransportProtocol(0x00))
931        ));
932    }
933
934    #[test]
935    fn ipv6_sd_invalid_transport_protocol_returns_error() {
936        let buf = ipv6_option_with_protocol(OptionType::IpV6SD, 0xFE);
937        assert!(matches!(
938            validate_option(&buf),
939            Err(Error::InvalidOptionTransportProtocol(0xFE))
940        ));
941    }
942
943    #[test]
944    fn ipv6_sd_invalid_length_returns_error() {
945        // length = 9 (wrong, should be 21), wire_size = 12
946        let mut buf = [0u8; 12];
947        buf[0] = 0x00;
948        buf[1] = 0x09;
949        buf[2] = 0x26; // type = IpV6SD
950        buf[3] = 0x00;
951        assert!(matches!(
952            validate_option(&buf),
953            Err(Error::InvalidOptionLength {
954                option_type: 0x26,
955                expected: 21,
956                actual: 9,
957            })
958        ));
959    }
960
961    // --- OptionIter ---
962
963    #[test]
964    fn option_iter_empty() {
965        let iter = OptionIter::new(&[]);
966        assert_eq!(iter.count(), 0);
967    }
968
969    #[test]
970    fn option_iter_two_options() {
971        let opt1 = Options::IpV4Endpoint {
972            ip: Ipv4Addr::new(10, 0, 0, 1),
973            protocol: TransportProtocol::Udp,
974            port: 30490,
975        };
976        let opt2 = Options::LoadBalancing {
977            priority: 100,
978            weight: 200,
979        };
980        let mut buf = [0u8; 24]; // 12 + 8 = 20
981        let n1 = opt1.write(&mut &mut buf[..12]).unwrap();
982        let n2 = opt2.write(&mut &mut buf[12..20]).unwrap();
983        let total = n1 + n2;
984
985        let mut iter = OptionIter::new(&buf[..total]);
986        let v1 = iter.next().unwrap();
987        assert_eq!(v1.to_owned().unwrap(), opt1);
988        let v2 = iter.next().unwrap();
989        assert_eq!(v2.to_owned().unwrap(), opt2);
990        assert!(iter.next().is_none());
991    }
992
993    #[test]
994    fn option_iter_clone_allows_reuse() {
995        // Cloning should snapshot the iterator state — advancing the
996        // original must not affect the clone, and the clone must be
997        // able to walk the full sequence independently.
998        let opt1 = Options::IpV4Endpoint {
999            ip: Ipv4Addr::new(10, 0, 0, 1),
1000            protocol: TransportProtocol::Udp,
1001            port: 30490,
1002        };
1003        let opt2 = Options::IpV4Endpoint {
1004            ip: Ipv4Addr::new(10, 0, 0, 2),
1005            protocol: TransportProtocol::Udp,
1006            port: 30491,
1007        };
1008        let mut buf = [0u8; 24];
1009        let n1 = opt1.write(&mut &mut buf[..12]).unwrap();
1010        let n2 = opt2.write(&mut &mut buf[12..24]).unwrap();
1011        let total = n1 + n2;
1012
1013        let iter = OptionIter::new(&buf[..total]);
1014        let clone = iter.clone();
1015
1016        // Walk the original: it should produce opt1 then opt2.
1017        let mut walker = iter;
1018        let a = walker.next().unwrap().to_owned().unwrap();
1019        let b = walker.next().unwrap().to_owned().unwrap();
1020        assert!(walker.next().is_none());
1021        assert_eq!(a, opt1);
1022        assert_eq!(b, opt2);
1023
1024        // The clone is untouched by the original's advance — it still
1025        // starts from the beginning and yields both options.
1026        let mut walker2 = clone;
1027        let a2 = walker2.next().unwrap().to_owned().unwrap();
1028        let b2 = walker2.next().unwrap().to_owned().unwrap();
1029        assert!(walker2.next().is_none());
1030        assert_eq!(a2, opt1);
1031        assert_eq!(b2, opt2);
1032    }
1033
1034    #[test]
1035    fn option_iter_clone_mid_walk_preserves_position() {
1036        // After partially walking the original iterator, cloning it
1037        // should yield a new iterator that starts from the current
1038        // position of the original — not from the beginning.
1039        let opt1 = Options::IpV4Endpoint {
1040            ip: Ipv4Addr::new(10, 0, 0, 1),
1041            protocol: TransportProtocol::Udp,
1042            port: 30490,
1043        };
1044        let opt2 = Options::IpV4Endpoint {
1045            ip: Ipv4Addr::new(10, 0, 0, 2),
1046            protocol: TransportProtocol::Udp,
1047            port: 30491,
1048        };
1049        let mut buf = [0u8; 24];
1050        let n1 = opt1.write(&mut &mut buf[..12]).unwrap();
1051        let n2 = opt2.write(&mut &mut buf[12..24]).unwrap();
1052        let total = n1 + n2;
1053
1054        let mut iter = OptionIter::new(&buf[..total]);
1055        // Advance past opt1.
1056        let _ = iter.next().unwrap();
1057
1058        // Clone from this mid-walk position; the clone should yield
1059        // only opt2 (and then end).
1060        let mut clone = iter.clone();
1061        let remaining = clone.next().unwrap().to_owned().unwrap();
1062        assert!(clone.next().is_none());
1063        assert_eq!(remaining, opt2);
1064    }
1065}