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/// Transport protocol used in SD endpoint options.
10#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
11pub enum TransportProtocol {
12    /// UDP (0x11).
13    Udp,
14    /// TCP (0x06).
15    Tcp,
16}
17
18impl TryFrom<u8> for TransportProtocol {
19    type Error = Error;
20    fn try_from(value: u8) -> Result<Self, Error> {
21        match value {
22            0x11 => Ok(TransportProtocol::Udp),
23            0x06 => Ok(TransportProtocol::Tcp),
24            _ => Err(Error::InvalidOptionTransportProtocol(value)),
25        }
26    }
27}
28
29impl From<TransportProtocol> for u8 {
30    fn from(transport_protocol: TransportProtocol) -> u8 {
31        match transport_protocol {
32            TransportProtocol::Udp => 0x11,
33            TransportProtocol::Tcp => 0x06,
34        }
35    }
36}
37
38/// The type of an SD option.
39#[derive(Clone, Copy, Debug, Eq, PartialEq)]
40pub enum OptionType {
41    /// Configuration option (0x01).
42    Configuration,
43    /// Load balancing option (0x02).
44    LoadBalancing,
45    /// IPv4 endpoint option (0x04).
46    IpV4Endpoint,
47    /// IPv6 endpoint option (0x06).
48    IpV6Endpoint,
49    /// IPv4 multicast option (0x14).
50    IpV4Multicast,
51    /// IPv6 multicast option (0x16).
52    IpV6Multicast,
53    /// IPv4 SD option (0x24).
54    IpV4SD,
55    /// IPv6 SD option (0x26).
56    IpV6SD,
57}
58
59impl TryFrom<u8> for OptionType {
60    type Error = Error;
61    fn try_from(value: u8) -> Result<Self, Error> {
62        match value {
63            0x01 => Ok(OptionType::Configuration),
64            0x02 => Ok(OptionType::LoadBalancing),
65            0x04 => Ok(OptionType::IpV4Endpoint),
66            0x06 => Ok(OptionType::IpV6Endpoint),
67            0x14 => Ok(OptionType::IpV4Multicast),
68            0x16 => Ok(OptionType::IpV6Multicast),
69            0x24 => Ok(OptionType::IpV4SD),
70            0x26 => Ok(OptionType::IpV6SD),
71            _ => Err(Error::InvalidOptionType(value)),
72        }
73    }
74}
75
76impl From<OptionType> for u8 {
77    fn from(option_type: OptionType) -> u8 {
78        match option_type {
79            OptionType::Configuration => 0x01,
80            OptionType::LoadBalancing => 0x02,
81            OptionType::IpV4Endpoint => 0x04,
82            OptionType::IpV6Endpoint => 0x06,
83            OptionType::IpV4Multicast => 0x14,
84            OptionType::IpV6Multicast => 0x16,
85            OptionType::IpV4SD => 0x24,
86            OptionType::IpV6SD => 0x26,
87        }
88    }
89}
90
91// Boxing is not available in no_std, so allow the large variant.
92#[allow(clippy::large_enum_variant)]
93/// A decoded SD option.
94#[derive(Clone, Debug, Eq, PartialEq)]
95pub enum Options {
96    /// A configuration key-value string.
97    Configuration {
98        /// The raw configuration string bytes.
99        configuration_string: heapless::Vec<u8, MAX_CONFIGURATION_STRING_LENGTH>,
100    },
101    /// Load balancing parameters.
102    LoadBalancing {
103        /// The priority value.
104        priority: u16,
105        /// The weight value.
106        weight: u16,
107    },
108    /// An IPv4 endpoint.
109    IpV4Endpoint {
110        /// The IPv4 address.
111        ip: Ipv4Addr,
112        /// The transport protocol (UDP or TCP).
113        protocol: TransportProtocol,
114        /// The port number.
115        port: u16,
116    },
117    /// An IPv6 endpoint.
118    IpV6Endpoint {
119        /// The IPv6 address.
120        ip: Ipv6Addr,
121        /// The transport protocol (UDP or TCP).
122        protocol: TransportProtocol,
123        /// The port number.
124        port: u16,
125    },
126    /// An IPv4 multicast address.
127    IpV4Multicast {
128        /// The IPv4 multicast address.
129        ip: Ipv4Addr,
130        /// The transport protocol (UDP or TCP).
131        protocol: TransportProtocol,
132        /// The port number.
133        port: u16,
134    },
135    /// An IPv6 multicast address.
136    IpV6Multicast {
137        /// The IPv6 multicast address.
138        ip: Ipv6Addr,
139        /// The transport protocol (UDP or TCP).
140        protocol: TransportProtocol,
141        /// The port number.
142        port: u16,
143    },
144    /// An IPv4 SD endpoint.
145    IpV4SD {
146        /// The IPv4 address.
147        ip: Ipv4Addr,
148        /// The transport protocol (UDP or TCP).
149        protocol: TransportProtocol,
150        /// The port number.
151        port: u16,
152    },
153    /// An IPv6 SD endpoint.
154    IpV6SD {
155        /// The IPv6 address.
156        ip: Ipv6Addr,
157        /// The transport protocol (UDP or TCP).
158        protocol: TransportProtocol,
159        /// The port number.
160        port: u16,
161    },
162}
163
164impl Options {
165    /// Returns the total wire size of this option in bytes.
166    #[must_use]
167    pub fn size(&self) -> usize {
168        match self {
169            Options::Configuration {
170                configuration_string,
171            } => 4 + configuration_string.len(),
172            Options::LoadBalancing { .. } => 8,
173            Options::IpV4Endpoint { .. }
174            | Options::IpV4Multicast { .. }
175            | Options::IpV4SD { .. } => 12,
176            Options::IpV6Endpoint { .. }
177            | Options::IpV6Multicast { .. }
178            | Options::IpV6SD { .. } => 24,
179        }
180    }
181
182    /// Serializes this option to a writer.
183    ///
184    /// # Errors
185    ///
186    /// Returns an error if writing to the writer fails.
187    ///
188    /// # Panics
189    ///
190    /// Panics if the option size minus 3 exceeds `u16::MAX` (unreachable in practice).
191    pub fn write<T: embedded_io::Write>(
192        &self,
193        writer: &mut T,
194    ) -> Result<usize, crate::protocol::Error> {
195        writer.write_u16_be(u16::try_from(self.size() - 3).expect("option size fits u16"))?;
196        match self {
197            Options::Configuration {
198                configuration_string,
199            } => {
200                writer.write_u8(u8::from(OptionType::Configuration))?;
201                writer.write_u8(0)?;
202                writer.write_bytes(configuration_string)?;
203                Ok(self.size())
204            }
205            Options::LoadBalancing { priority, weight } => {
206                writer.write_u8(u8::from(OptionType::LoadBalancing))?;
207                writer.write_u8(0)?;
208                writer.write_u16_be(*priority)?;
209                writer.write_u16_be(*weight)?;
210                Ok(8)
211            }
212            Options::IpV4Endpoint { ip, protocol, port } => {
213                write_ipv4_option(writer, OptionType::IpV4Endpoint, *ip, *protocol, *port)
214            }
215            Options::IpV6Endpoint { ip, protocol, port } => {
216                write_ipv6_option(writer, OptionType::IpV6Endpoint, *ip, *protocol, *port)
217            }
218            Options::IpV4Multicast { ip, protocol, port } => {
219                write_ipv4_option(writer, OptionType::IpV4Multicast, *ip, *protocol, *port)
220            }
221            Options::IpV6Multicast { ip, protocol, port } => {
222                write_ipv6_option(writer, OptionType::IpV6Multicast, *ip, *protocol, *port)
223            }
224            Options::IpV4SD { ip, protocol, port } => {
225                write_ipv4_option(writer, OptionType::IpV4SD, *ip, *protocol, *port)
226            }
227            Options::IpV6SD { ip, protocol, port } => {
228                write_ipv6_option(writer, OptionType::IpV6SD, *ip, *protocol, *port)
229            }
230        }
231    }
232}
233
234fn write_ipv4_option<T: embedded_io::Write>(
235    writer: &mut T,
236    option_type: OptionType,
237    ip: Ipv4Addr,
238    protocol: TransportProtocol,
239    port: u16,
240) -> Result<usize, crate::protocol::Error> {
241    writer.write_u8(u8::from(option_type))?;
242    writer.write_u8(0)?;
243    writer.write_u32_be(ip.to_bits())?;
244    writer.write_u8(0)?;
245    writer.write_u8(u8::from(protocol))?;
246    writer.write_u16_be(port)?;
247    Ok(12)
248}
249
250fn write_ipv6_option<T: embedded_io::Write>(
251    writer: &mut T,
252    option_type: OptionType,
253    ip: Ipv6Addr,
254    protocol: TransportProtocol,
255    port: u16,
256) -> Result<usize, crate::protocol::Error> {
257    writer.write_u8(u8::from(option_type))?;
258    writer.write_u8(0)?;
259    writer.write_bytes(&ip.octets())?;
260    writer.write_u8(0)?;
261    writer.write_u8(u8::from(protocol))?;
262    writer.write_u16_be(port)?;
263    Ok(24)
264}
265
266/// Extract the first `IpV4Endpoint` address from a slice of owned options.
267///
268/// Returns `None` if no `IpV4Endpoint` option is present.
269#[must_use]
270pub fn extract_ipv4_endpoint(options: &[Options]) -> Option<core::net::SocketAddrV4> {
271    options.iter().find_map(|opt| match opt {
272        Options::IpV4Endpoint { ip, port, .. } => Some(core::net::SocketAddrV4::new(*ip, *port)),
273        _ => None,
274    })
275}
276
277// --- Zero-copy view types ---
278
279/// Zero-copy view into a variable-length SD option in a buffer.
280///
281/// Wire layout:
282/// - `[0..2]`: length (u16 BE) = `total_size` - 3
283/// - `[2]`: option type (u8)
284/// - `[3]`: reserved/discard flag (u8)
285/// - `[4..]`: type-specific data
286#[derive(Clone, Copy, Debug)]
287pub struct OptionView<'a>(&'a [u8]);
288
289impl<'a> OptionView<'a> {
290    /// Returns the option type.
291    ///
292    /// # Errors
293    ///
294    /// Returns [`Error::InvalidOptionType`] if the type byte is unrecognized.
295    pub fn option_type(&self) -> Result<OptionType, Error> {
296        OptionType::try_from(self.0[2])
297    }
298
299    /// Total wire size of this option (length field value + 3).
300    #[must_use]
301    pub fn wire_size(&self) -> usize {
302        let length = u16::from_be_bytes([self.0[0], self.0[1]]);
303        usize::from(length) + 3
304    }
305
306    /// Parse as IPv4 endpoint/multicast/SD option.
307    /// Returns `(ip, protocol, port)`.
308    ///
309    /// # Errors
310    ///
311    /// Returns [`Error::InvalidOptionTransportProtocol`] if the protocol byte is unrecognized.
312    pub fn as_ipv4(&self) -> Result<(Ipv4Addr, TransportProtocol, u16), Error> {
313        let ip = Ipv4Addr::from_bits(u32::from_be_bytes([
314            self.0[4], self.0[5], self.0[6], self.0[7],
315        ]));
316        // [8] is reserved
317        let protocol = TransportProtocol::try_from(self.0[9])?;
318        let port = u16::from_be_bytes([self.0[10], self.0[11]]);
319        Ok((ip, protocol, port))
320    }
321
322    /// Parse as IPv6 endpoint/multicast/SD option.
323    /// Returns `(ip, protocol, port)`.
324    ///
325    /// # Errors
326    ///
327    /// Returns [`Error::InvalidOptionTransportProtocol`] if the protocol byte is unrecognized.
328    pub fn as_ipv6(&self) -> Result<(Ipv6Addr, TransportProtocol, u16), Error> {
329        let mut octets = [0u8; 16];
330        octets.copy_from_slice(&self.0[4..20]);
331        let ip = Ipv6Addr::from(octets);
332        // [20] is reserved
333        let protocol = TransportProtocol::try_from(self.0[21])?;
334        let port = u16::from_be_bytes([self.0[22], self.0[23]]);
335        Ok((ip, protocol, port))
336    }
337
338    /// Raw configuration bytes (for Configuration options).
339    #[must_use]
340    pub fn configuration_bytes(&self) -> &'a [u8] {
341        let length = u16::from_be_bytes([self.0[0], self.0[1]]);
342        let string_len = length.saturating_sub(1);
343        &self.0[4..4 + usize::from(string_len)]
344    }
345
346    /// Parse as load-balancing option. Returns `(priority, weight)`.
347    ///
348    /// # Errors
349    ///
350    /// Currently always succeeds; the `Result` return type is reserved for future validation.
351    pub fn as_load_balancing(&self) -> Result<(u16, u16), Error> {
352        let priority = u16::from_be_bytes([self.0[4], self.0[5]]);
353        let weight = u16::from_be_bytes([self.0[6], self.0[7]]);
354        Ok((priority, weight))
355    }
356
357    /// Converts this view into an owned [`Options`].
358    ///
359    /// # Errors
360    ///
361    /// Returns an error if the option type is unrecognized, the transport protocol byte
362    /// is invalid, or the configuration string exceeds [`MAX_CONFIGURATION_STRING_LENGTH`].
363    ///
364    /// # Panics
365    ///
366    /// Panics if a configuration string passes the length check but fails to fit into the
367    /// heapless buffer (unreachable in practice).
368    pub fn to_owned(&self) -> Result<Options, Error> {
369        let option_type = self.option_type()?;
370        match option_type {
371            OptionType::Configuration => {
372                let config_bytes = self.configuration_bytes();
373                if config_bytes.len() > MAX_CONFIGURATION_STRING_LENGTH {
374                    return Err(Error::ConfigurationStringTooLong(config_bytes.len()));
375                }
376                let mut configuration_string =
377                    heapless::Vec::<u8, MAX_CONFIGURATION_STRING_LENGTH>::new();
378                configuration_string
379                    .extend_from_slice(config_bytes)
380                    .expect("length validated above");
381                Ok(Options::Configuration {
382                    configuration_string,
383                })
384            }
385            OptionType::LoadBalancing => {
386                let (priority, weight) = self.as_load_balancing()?;
387                Ok(Options::LoadBalancing { priority, weight })
388            }
389            OptionType::IpV4Endpoint => {
390                let (ip, protocol, port) = self.as_ipv4()?;
391                Ok(Options::IpV4Endpoint { ip, protocol, port })
392            }
393            OptionType::IpV6Endpoint => {
394                let (ip, protocol, port) = self.as_ipv6()?;
395                Ok(Options::IpV6Endpoint { ip, protocol, port })
396            }
397            OptionType::IpV4Multicast => {
398                let (ip, protocol, port) = self.as_ipv4()?;
399                Ok(Options::IpV4Multicast { ip, protocol, port })
400            }
401            OptionType::IpV6Multicast => {
402                let (ip, protocol, port) = self.as_ipv6()?;
403                Ok(Options::IpV6Multicast { ip, protocol, port })
404            }
405            OptionType::IpV4SD => {
406                let (ip, protocol, port) = self.as_ipv4()?;
407                Ok(Options::IpV4SD { ip, protocol, port })
408            }
409            OptionType::IpV6SD => {
410                let (ip, protocol, port) = self.as_ipv6()?;
411                Ok(Options::IpV6SD { ip, protocol, port })
412            }
413        }
414    }
415}
416
417/// Iterator over variable-length SD options in a validated buffer.
418/// Options are guaranteed valid (validated upfront in `SdHeaderView::parse`).
419pub struct OptionIter<'a> {
420    remaining: &'a [u8],
421}
422
423impl<'a> OptionIter<'a> {
424    pub(crate) fn new(buf: &'a [u8]) -> Self {
425        Self { remaining: buf }
426    }
427}
428
429impl<'a> Iterator for OptionIter<'a> {
430    type Item = OptionView<'a>;
431
432    fn next(&mut self) -> Option<Self::Item> {
433        if self.remaining.len() < 4 {
434            return None;
435        }
436        let length = u16::from_be_bytes([self.remaining[0], self.remaining[1]]);
437        let wire_size = usize::from(length) + 3;
438        if wire_size > self.remaining.len() {
439            return None;
440        }
441        let view = OptionView(&self.remaining[..wire_size]);
442        self.remaining = &self.remaining[wire_size..];
443        Some(view)
444    }
445}
446
447/// Validate a single option's wire format and return its wire size.
448/// Used during `SdHeaderView::parse` for upfront validation.
449pub(crate) fn validate_option(buf: &[u8]) -> Result<usize, Error> {
450    if buf.len() < 4 {
451        return Err(Error::IncorrectOptionsSize(buf.len()));
452    }
453    let length = u16::from_be_bytes([buf[0], buf[1]]);
454    let wire_size = usize::from(length) + 3;
455    if wire_size > buf.len() {
456        return Err(Error::IncorrectOptionsSize(buf.len()));
457    }
458    let option_type = OptionType::try_from(buf[2])?;
459    // Validate expected lengths for fixed-size options
460    match option_type {
461        OptionType::IpV4Endpoint | OptionType::IpV4Multicast | OptionType::IpV4SD => {
462            if length != 9 {
463                return Err(Error::InvalidOptionLength {
464                    option_type: buf[2],
465                    expected: 9,
466                    actual: length,
467                });
468            }
469        }
470        OptionType::IpV6Endpoint | OptionType::IpV6Multicast | OptionType::IpV6SD => {
471            if length != 21 {
472                return Err(Error::InvalidOptionLength {
473                    option_type: buf[2],
474                    expected: 21,
475                    actual: length,
476                });
477            }
478        }
479        OptionType::LoadBalancing => {
480            if length != 5 {
481                return Err(Error::InvalidOptionLength {
482                    option_type: buf[2],
483                    expected: 5,
484                    actual: length,
485                });
486            }
487        }
488        OptionType::Configuration => {
489            // Configuration strings are variable length; just check it doesn't exceed max
490            let string_len = length.saturating_sub(1);
491            if usize::from(string_len) > MAX_CONFIGURATION_STRING_LENGTH {
492                return Err(Error::ConfigurationStringTooLong(string_len.into()));
493            }
494        }
495    }
496    Ok(wire_size)
497}
498
499#[cfg(test)]
500mod tests {
501    use core::net::{Ipv4Addr, Ipv6Addr};
502
503    use super::*;
504
505    // --- TransportProtocol ---
506
507    #[test]
508    fn transport_protocol_tcp_round_trip() {
509        assert_eq!(
510            TransportProtocol::try_from(0x06).unwrap(),
511            TransportProtocol::Tcp
512        );
513        assert_eq!(u8::from(TransportProtocol::Tcp), 0x06);
514    }
515
516    #[test]
517    fn transport_protocol_invalid_returns_error() {
518        assert!(matches!(
519            TransportProtocol::try_from(0xFF),
520            Err(Error::InvalidOptionTransportProtocol(0xFF))
521        ));
522    }
523
524    // --- OptionView: parse from encoded bytes ---
525
526    #[test]
527    fn option_view_ipv4_endpoint_tcp() {
528        let buf: [u8; 12] = [
529            0x00, 0x09, // length = 9
530            0x04, // type = IpV4Endpoint
531            0x00, // discard flag
532            192, 168, 0, 1,    // ip
533            0x00, // reserved
534            0x06, // protocol = TCP
535            0x04, 0xD2, // port = 1234
536        ];
537        let view = OptionView(&buf);
538        assert_eq!(view.option_type().unwrap(), OptionType::IpV4Endpoint);
539        assert_eq!(view.wire_size(), 12);
540        let (ip, protocol, port) = view.as_ipv4().unwrap();
541        assert_eq!(ip, Ipv4Addr::new(192, 168, 0, 1));
542        assert_eq!(protocol, TransportProtocol::Tcp);
543        assert_eq!(port, 1234);
544    }
545
546    #[test]
547    fn option_view_to_owned_invalid_type() {
548        let buf: [u8; 4] = [0x00, 0x00, 0xFF, 0x00]; // type = 0xFF (invalid)
549        let view = OptionView(&buf);
550        assert!(matches!(
551            view.to_owned(),
552            Err(Error::InvalidOptionType(0xFF))
553        ));
554    }
555
556    // --- Round-trip tests for all option types ---
557
558    fn round_trip(option: &Options) {
559        let size = option.size();
560        let mut buf = [0u8; 4 + MAX_CONFIGURATION_STRING_LENGTH];
561        let written = option.write(&mut &mut buf[..size]).unwrap();
562        assert_eq!(written, size);
563        let view = OptionView(&buf[..size]);
564        let parsed = view.to_owned().unwrap();
565        assert_eq!(*option, parsed);
566    }
567
568    #[test]
569    fn configuration_round_trip() {
570        let mut config_string = heapless::Vec::<u8, MAX_CONFIGURATION_STRING_LENGTH>::new();
571        config_string.extend_from_slice(b"test=value").unwrap();
572        let option = Options::Configuration {
573            configuration_string: config_string,
574        };
575        round_trip(&option);
576    }
577
578    #[test]
579    fn configuration_empty_round_trip() {
580        let option = Options::Configuration {
581            configuration_string: heapless::Vec::new(),
582        };
583        round_trip(&option);
584    }
585
586    #[test]
587    fn load_balancing_round_trip() {
588        let option = Options::LoadBalancing {
589            priority: 100,
590            weight: 200,
591        };
592        round_trip(&option);
593    }
594
595    #[test]
596    fn ipv4_endpoint_round_trip() {
597        let option = Options::IpV4Endpoint {
598            ip: Ipv4Addr::new(10, 0, 0, 1),
599            protocol: TransportProtocol::Udp,
600            port: 30490,
601        };
602        round_trip(&option);
603    }
604
605    #[test]
606    fn ipv6_endpoint_round_trip() {
607        let option = Options::IpV6Endpoint {
608            ip: Ipv6Addr::new(0xfe80, 0, 0, 0, 0, 0, 0, 1),
609            protocol: TransportProtocol::Tcp,
610            port: 8080,
611        };
612        round_trip(&option);
613    }
614
615    #[test]
616    fn ipv4_multicast_round_trip() {
617        let option = Options::IpV4Multicast {
618            ip: Ipv4Addr::new(239, 0, 0, 1),
619            protocol: TransportProtocol::Udp,
620            port: 30490,
621        };
622        round_trip(&option);
623    }
624
625    #[test]
626    fn ipv6_multicast_round_trip() {
627        let option = Options::IpV6Multicast {
628            ip: Ipv6Addr::new(0xff02, 0, 0, 0, 0, 0, 0, 1),
629            protocol: TransportProtocol::Udp,
630            port: 30490,
631        };
632        round_trip(&option);
633    }
634
635    #[test]
636    fn ipv4_sd_round_trip() {
637        let option = Options::IpV4SD {
638            ip: Ipv4Addr::new(172, 16, 0, 1),
639            protocol: TransportProtocol::Udp,
640            port: 30490,
641        };
642        round_trip(&option);
643    }
644
645    #[test]
646    fn ipv6_sd_round_trip() {
647        let option = Options::IpV6SD {
648            ip: Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1),
649            protocol: TransportProtocol::Tcp,
650            port: 9999,
651        };
652        round_trip(&option);
653    }
654
655    // --- Error cases ---
656
657    #[test]
658    fn load_balancing_invalid_length_returns_error() {
659        // length = 3 (wrong, should be 5), wire_size = 6
660        let mut buf = [0u8; 6];
661        buf[0] = 0x00;
662        buf[1] = 0x03; // length = 3
663        buf[2] = 0x02; // type = LoadBalancing
664        buf[3] = 0x00; // discard flag
665        assert!(matches!(
666            validate_option(&buf),
667            Err(Error::InvalidOptionLength {
668                option_type: 0x02,
669                expected: 5,
670                actual: 3,
671            })
672        ));
673    }
674
675    #[test]
676    fn ipv4_endpoint_invalid_length_returns_error() {
677        // length = 5 (wrong, should be 9), wire_size = 8
678        let mut buf = [0u8; 8];
679        buf[0] = 0x00;
680        buf[1] = 0x05; // length = 5
681        buf[2] = 0x04; // type = IpV4Endpoint
682        buf[3] = 0x00;
683        assert!(matches!(
684            validate_option(&buf),
685            Err(Error::InvalidOptionLength {
686                option_type: 0x04,
687                expected: 9,
688                actual: 5,
689            })
690        ));
691    }
692
693    #[test]
694    fn ipv6_endpoint_invalid_length_returns_error() {
695        // length = 9 (wrong, should be 21), wire_size = 12
696        let mut buf = [0u8; 12];
697        buf[0] = 0x00;
698        buf[1] = 0x09; // length = 9
699        buf[2] = 0x06; // type = IpV6Endpoint
700        buf[3] = 0x00;
701        assert!(matches!(
702            validate_option(&buf),
703            Err(Error::InvalidOptionLength {
704                option_type: 0x06,
705                expected: 21,
706                actual: 9,
707            })
708        ));
709    }
710
711    #[test]
712    fn ipv4_multicast_invalid_length_returns_error() {
713        // length = 5 (wrong, should be 9), wire_size = 8
714        let mut buf = [0u8; 8];
715        buf[0] = 0x00;
716        buf[1] = 0x05;
717        buf[2] = 0x14; // type = IpV4Multicast
718        buf[3] = 0x00;
719        assert!(matches!(
720            validate_option(&buf),
721            Err(Error::InvalidOptionLength {
722                option_type: 0x14,
723                expected: 9,
724                actual: 5,
725            })
726        ));
727    }
728
729    #[test]
730    fn ipv6_multicast_invalid_length_returns_error() {
731        // length = 9 (wrong, should be 21), wire_size = 12
732        let mut buf = [0u8; 12];
733        buf[0] = 0x00;
734        buf[1] = 0x09;
735        buf[2] = 0x16; // type = IpV6Multicast
736        buf[3] = 0x00;
737        assert!(matches!(
738            validate_option(&buf),
739            Err(Error::InvalidOptionLength {
740                option_type: 0x16,
741                expected: 21,
742                actual: 9,
743            })
744        ));
745    }
746
747    #[test]
748    fn ipv4_sd_invalid_length_returns_error() {
749        // length = 5 (wrong, should be 9), wire_size = 8
750        let mut buf = [0u8; 8];
751        buf[0] = 0x00;
752        buf[1] = 0x05;
753        buf[2] = 0x24; // type = IpV4SD
754        buf[3] = 0x00;
755        assert!(matches!(
756            validate_option(&buf),
757            Err(Error::InvalidOptionLength {
758                option_type: 0x24,
759                expected: 9,
760                actual: 5,
761            })
762        ));
763    }
764
765    #[test]
766    fn ipv6_sd_invalid_length_returns_error() {
767        // length = 9 (wrong, should be 21), wire_size = 12
768        let mut buf = [0u8; 12];
769        buf[0] = 0x00;
770        buf[1] = 0x09;
771        buf[2] = 0x26; // type = IpV6SD
772        buf[3] = 0x00;
773        assert!(matches!(
774            validate_option(&buf),
775            Err(Error::InvalidOptionLength {
776                option_type: 0x26,
777                expected: 21,
778                actual: 9,
779            })
780        ));
781    }
782
783    // --- OptionIter ---
784
785    #[test]
786    fn option_iter_empty() {
787        let iter = OptionIter::new(&[]);
788        assert_eq!(iter.count(), 0);
789    }
790
791    #[test]
792    fn option_iter_two_options() {
793        let opt1 = Options::IpV4Endpoint {
794            ip: Ipv4Addr::new(10, 0, 0, 1),
795            protocol: TransportProtocol::Udp,
796            port: 30490,
797        };
798        let opt2 = Options::LoadBalancing {
799            priority: 100,
800            weight: 200,
801        };
802        let mut buf = [0u8; 24]; // 12 + 8 = 20
803        let n1 = opt1.write(&mut &mut buf[..12]).unwrap();
804        let n2 = opt2.write(&mut &mut buf[12..20]).unwrap();
805        let total = n1 + n2;
806
807        let mut iter = OptionIter::new(&buf[..total]);
808        let v1 = iter.next().unwrap();
809        assert_eq!(v1.to_owned().unwrap(), opt1);
810        let v2 = iter.next().unwrap();
811        assert_eq!(v2.to_owned().unwrap(), opt2);
812        assert!(iter.next().is_none());
813    }
814}