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