Skip to main content

simple_someip/protocol/sd/
header.rs

1use crate::protocol::byte_order::WriteBytesExt;
2
3use automotive_wire_codec::{Decode, DecodeIter, DecodeIterator, Encode};
4
5use super::{
6    Entry, EntryView, Flags, OptionView, Options,
7    entry::{ENTRY_SIZE, EntryIter},
8    options::OptionIter,
9};
10
11/// An SD header that borrows its entries and options slices.
12///
13/// Used for constructing and encoding outgoing SD messages. For zero-copy
14/// parsing of incoming SD messages, see [`SdHeaderView`].
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
16pub struct Header<'a> {
17    /// The SD flags byte (reboot + unicast).
18    pub flags: Flags,
19    /// The SD entries.
20    pub entries: &'a [Entry],
21    /// The SD options.
22    pub options: &'a [Options],
23}
24
25impl<'a> Header<'a> {
26    /// Creates a new SD header from the given flags, entries, and options.
27    #[must_use]
28    pub const fn new(flags: Flags, entries: &'a [Entry], options: &'a [Options]) -> Self {
29        Self {
30            flags,
31            entries,
32            options,
33        }
34    }
35}
36
37/// Zero-copy view into an SD header payload.
38///
39/// Created by [`SdHeaderView::parse`], which fully validates the SD header,
40/// entries, and options upfront. This makes the entry and option iterators
41/// infallible.
42///
43/// # Validation-proof invariant (candidate "c")
44///
45/// The type system carries no proof tying the cached `entry_count` /
46/// `option_count` to `entries_buf` / `options_buf`. `parse` runs ONE eager
47/// validating walk (draining the lazy L1 [`DecodeIterator`]s) and caches the
48/// element counts; the infallible accessors ([`entries`](SdHeaderView::entries)
49/// / [`options`](SdHeaderView::options)) then re-slice those *already-validated*
50/// buffers with purpose-built iterators that advance by stride/length WITHOUT
51/// re-running the type/length/transport checks. They TRUST the construction-time
52/// walk. Nothing but this `parse`-only construction path may populate the
53/// buffers, so the trust holds — the same invariant `OptionIter` has always
54/// relied on.
55#[derive(Clone, Copy, Debug)]
56pub struct SdHeaderView<'a> {
57    flags: Flags,
58    entries_buf: &'a [u8],
59    options_buf: &'a [u8],
60    /// Number of valid entries found during the construction-time walk.
61    entry_count: usize,
62    /// Number of valid options found during the construction-time walk.
63    option_count: usize,
64}
65
66impl<'a> SdHeaderView<'a> {
67    /// Parse and fully validate an SD header from `buf`.
68    ///
69    /// Validates:
70    /// - Buffer has enough data for flags + `entries_size` + entries + `options_size` + options
71    /// - `entries_size` is a multiple of `ENTRY_SIZE` (16)
72    /// - All entry type bytes are valid
73    /// - All options have valid types and lengths, and IP-bearing options have a
74    ///   recognized transport protocol byte
75    ///
76    /// # Errors
77    ///
78    /// Returns an error if the buffer is too short, `entries_size` is not a multiple of 16,
79    /// any entry type byte is invalid, or any option has an invalid type, length, or
80    /// transport protocol byte.
81    pub fn parse(buf: &'a [u8]) -> Result<Self, crate::protocol::Error> {
82        // The O(1) slicing + length checks (flags/reserved, entries_size,
83        // options_size, and the section bounds, incl. overflow hardening) live
84        // in the single decode source, `SdBody::decode`. This L2 path is
85        // re-founded (Phase 4) on top of that lazy L1 layer: it runs ONE eager
86        // validating walk by draining the L1 `DecodeIterator`s over the
87        // already-sliced entries and options sections, surfacing the first
88        // `Err` via `?`, and caches the element counts. The infallible
89        // accessors then re-slice these validated buffers without re-validating
90        // (candidate "c" — see the type-level docs for the trust invariant).
91        let (body, _rest) = SdBody::decode(buf)?;
92
93        // Eager validating walk over the entries section. `EntryView`'s L1
94        // decode only slices the fixed 16-byte stride (surfacing truncation);
95        // `entry_type()` validates the type byte. A partial trailing entry is
96        // surfaced here as an `Err`, not silently truncated at accessor time.
97        let mut entry_count = 0usize;
98        for entry in body.entries() {
99            entry?.entry_type()?;
100            entry_count += 1;
101        }
102
103        // Eager validating walk over the options section. `OptionView`'s L1
104        // decode only slices by the length field (surfacing truncation);
105        // `validate()` checks type / per-type length / transport-protocol byte.
106        let mut option_count = 0usize;
107        for option in body.options() {
108            option?.validate()?;
109            option_count += 1;
110        }
111
112        Ok(Self {
113            flags: body.flags,
114            entries_buf: body.entries_buf,
115            options_buf: body.options_buf,
116            entry_count,
117            option_count,
118        })
119    }
120
121    /// Returns the SD flags.
122    #[must_use]
123    pub fn flags(&self) -> Flags {
124        self.flags
125    }
126
127    /// Returns an infallible iterator over the SD entries.
128    ///
129    /// Re-slices the already-validated `entries_buf` at the fixed 16-byte
130    /// stride; it never re-runs entry-type validation (done once in
131    /// [`parse`](SdHeaderView::parse)).
132    /// The returned [`EntryIter`] is [`ExactSizeIterator`] — its length comes
133    /// for free from the fixed stride.
134    #[must_use]
135    pub fn entries(&self) -> EntryIter<'a> {
136        EntryIter::new(self.entries_buf)
137    }
138
139    /// Returns an infallible iterator over the SD options.
140    ///
141    /// Re-slices the already-validated `options_buf` by each option's length
142    /// field; it never re-runs option validation (done once in
143    /// [`parse`](SdHeaderView::parse)).
144    /// Options have no fixed stride, so [`OptionIter`] is not itself
145    /// [`ExactSizeIterator`]; use [`option_count`](SdHeaderView::option_count)
146    /// for the cached element count.
147    #[must_use]
148    pub fn options(&self) -> OptionIter<'a> {
149        OptionIter::new(self.options_buf)
150    }
151
152    /// Returns the number of entries in this SD header.
153    ///
154    /// This is the count cached by the construction-time validating walk.
155    #[must_use]
156    pub fn entry_count(&self) -> usize {
157        self.entry_count
158    }
159
160    /// Returns the number of options in this SD header.
161    ///
162    /// This is the count cached by the construction-time validating walk.
163    /// Because options have no fixed stride, this cached count is the analogue
164    /// of `EntryIter`'s free `ExactSizeIterator::len` for the options section.
165    #[must_use]
166    pub fn option_count(&self) -> usize {
167        self.option_count
168    }
169}
170
171/// Lazy zero-copy view over an SD payload body.
172///
173/// [`SdBody::decode`] performs only the O(1) flag decode and section slicing
174/// (with the accompanying length / `entries_size`-multiple checks); it does NOT
175/// walk the entries validating their type bytes, nor the options validating
176/// their type / length / transport-protocol bytes. That per-element validation
177/// is the job of the lazy [`DecodeIter`] adapters returned by [`SdBody::entries`]
178/// / [`SdBody::options`], or of the L2 validation pass ([`SdHeaderView::parse`]).
179///
180/// Contrast with [`SdHeaderView`], which validates everything upfront so its
181/// iterators are infallible.
182#[derive(Clone, Copy, Debug)]
183pub struct SdBody<'a> {
184    flags: Flags,
185    entries_buf: &'a [u8],
186    options_buf: &'a [u8],
187}
188
189impl<'a> SdBody<'a> {
190    /// Returns the SD flags.
191    #[must_use]
192    pub fn flags(&self) -> Flags {
193        self.flags
194    }
195
196    /// Returns a lazy iterator over the SD entries.
197    ///
198    /// Each item is a `Result<EntryView, Error>`; a malformed/truncated entry
199    /// surfaces as an `Err`. The entry-type byte is not validated here — call
200    /// [`EntryView::entry_type`] / [`EntryView::to_owned`] to validate it.
201    #[must_use]
202    pub fn entries(&self) -> DecodeIterator<'a, EntryView<'a>> {
203        EntryView::iter(self.entries_buf)
204    }
205
206    /// Returns a lazy iterator over the SD options.
207    ///
208    /// Each item is a `Result<OptionView, Error>`; a malformed/truncated option
209    /// surfaces as an `Err`. Option type / length / transport-protocol bytes
210    /// are not validated here — validate them via the `OptionView` accessors.
211    #[must_use]
212    pub fn options(&self) -> DecodeIterator<'a, OptionView<'a>> {
213        OptionView::iter(self.options_buf)
214    }
215}
216
217impl<'a> Decode<'a> for SdBody<'a> {
218    type Error = crate::protocol::Error;
219
220    /// Decode and slice an SD payload body from the front of `buf`.
221    ///
222    /// Performs only the flag decode and the section slicing / length checks
223    /// (buffer minimum, `entries_size` multiple-of-16, and section bounds). It
224    /// deliberately does NOT validate entry-type bytes or option contents —
225    /// see the type-level docs.
226    ///
227    /// # Errors
228    ///
229    /// Returns [`Incomplete`](automotive_wire_codec::Incomplete) if the buffer
230    /// is too short for the declared sections, or
231    /// [`IncorrectEntriesSize`](super::Error::IncorrectEntriesSize) if
232    /// `entries_size` is not a multiple of `ENTRY_SIZE` (16).
233    fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Self::Error> {
234        // Minimum: 4 (flags+reserved) + 4 (entries_size) + 4 (options_size) = 12
235        if buf.len() < 12 {
236            return Err(automotive_wire_codec::Incomplete {
237                needed: 12,
238                available: buf.len(),
239            }
240            .into());
241        }
242
243        let flags = Flags::from(buf[0]);
244        // bytes [1..4] are reserved
245
246        let entries_size = u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]) as usize;
247
248        if !entries_size.is_multiple_of(ENTRY_SIZE) {
249            return Err(super::Error::IncorrectEntriesSize(entries_size).into());
250        }
251
252        // All section-bound arithmetic is `checked_add`: on a 32-bit `usize`
253        // (`no_std` embedded targets) a hostile `entries_size` / `options_size`
254        // near `u32::MAX` would otherwise wrap `8 + entries_size + 4` or
255        // `options_start + options_size` and pass the length check with a bogus
256        // small bound. An overflow means the buffer cannot possibly hold the
257        // declared sections, so it is reported as `Incomplete`.
258        let overflow = || automotive_wire_codec::Incomplete {
259            needed: usize::MAX,
260            available: buf.len(),
261        };
262
263        // Need entries data + 4 bytes for options_size field.
264        let entries_end = 8usize.checked_add(entries_size).ok_or_else(overflow)?;
265        let options_size_offset = entries_end;
266        let entries_section_end = entries_end.checked_add(4).ok_or_else(overflow)?;
267        if buf.len() < entries_section_end {
268            return Err(automotive_wire_codec::Incomplete {
269                needed: entries_section_end,
270                available: buf.len(),
271            }
272            .into());
273        }
274
275        let entries_buf = &buf[8..options_size_offset];
276
277        let options_size = u32::from_be_bytes([
278            buf[options_size_offset],
279            buf[options_size_offset + 1],
280            buf[options_size_offset + 2],
281            buf[options_size_offset + 3],
282        ]) as usize;
283
284        let options_start = entries_section_end;
285        let options_end = options_start
286            .checked_add(options_size)
287            .ok_or_else(overflow)?;
288        if buf.len() < options_end {
289            return Err(automotive_wire_codec::Incomplete {
290                needed: options_end,
291                available: buf.len(),
292            }
293            .into());
294        }
295
296        let options_buf = &buf[options_start..options_end];
297        let rest = &buf[options_end..];
298
299        Ok((
300            Self {
301                flags,
302                entries_buf,
303                options_buf,
304            },
305            rest,
306        ))
307    }
308}
309
310impl Encode for Header<'_> {
311    type Error = crate::protocol::Error;
312
313    fn encoded_size(&self) -> Result<usize, Self::Error> {
314        let mut size = 12 + self.entries.len() * ENTRY_SIZE;
315        for option in self.options {
316            size += option.size();
317        }
318        Ok(size)
319    }
320
321    fn encode(&self, writer: &mut impl embedded_io::Write) -> Result<usize, Self::Error> {
322        writer.write_u8(u8::from(self.flags))?;
323        let reserved: [u8; 3] = [0; 3];
324        writer.write_bytes(&reserved)?;
325        let entries_size = u32::try_from(self.entries.len() * 16).expect("entries size fits u32");
326        writer.write_u32_be(entries_size)?;
327        for entry in self.entries {
328            entry.encode(writer)?;
329        }
330        let mut options_size = 0;
331        for option in self.options {
332            options_size += option.size();
333        }
334        writer.write_u32_be(u32::try_from(options_size).expect("options size fits u32"))?;
335        for option in self.options {
336            option.encode(writer)?;
337        }
338        Ok(12 + entries_size as usize + options_size)
339    }
340}
341
342#[cfg(test)]
343mod tests {
344    use core::net::Ipv4Addr;
345
346    use super::*;
347    use crate::protocol::sd::{
348        Error as SdError, EventGroupEntry, OptionType, OptionsCount, RebootFlag, ServiceEntry,
349        TransportProtocol,
350        options::{
351            IPV4_OPTION_IP_OFFSET, IPV4_OPTION_LENGTH_FIELD, IPV4_OPTION_PORT_OFFSET,
352            IPV4_OPTION_PROTOCOL_OFFSET, IPV4_OPTION_WIRE_SIZE,
353        },
354    };
355    use automotive_wire_codec::Encode;
356
357    fn ipv4_endpoint_bytes(ip: [u8; 4], protocol: u8, port: u16) -> [u8; IPV4_OPTION_WIRE_SIZE] {
358        let mut b = [0u8; IPV4_OPTION_WIRE_SIZE];
359        b[0..2].copy_from_slice(&IPV4_OPTION_LENGTH_FIELD.to_be_bytes());
360        b[2] = u8::from(OptionType::IpV4Endpoint);
361        // b[3] is the discard flag (0).
362        b[IPV4_OPTION_IP_OFFSET..IPV4_OPTION_IP_OFFSET + 4].copy_from_slice(&ip);
363        // b[IPV4_OPTION_IP_OFFSET + 4] is reserved (0).
364        b[IPV4_OPTION_PROTOCOL_OFFSET] = protocol;
365        b[IPV4_OPTION_PORT_OFFSET..IPV4_OPTION_PORT_OFFSET + 2]
366            .copy_from_slice(&port.to_be_bytes());
367        b
368    }
369
370    fn raw_header(entries_size: u32, options_size: u32) -> [u8; 12] {
371        let mut b = [0u8; 12];
372        // flags = 0, reserved = 0
373        b[4..8].copy_from_slice(&entries_size.to_be_bytes());
374        b[8..12].copy_from_slice(&options_size.to_be_bytes());
375        b
376    }
377
378    #[test]
379    fn header_new_stores_fields() {
380        let flags = Flags::new_sd(RebootFlag::RecentlyRebooted);
381        let entries: &[Entry] = &[];
382        let options: &[Options] = &[];
383        let h = Header::new(flags, entries, options);
384        assert_eq!(h.flags, flags);
385        assert!(h.entries.is_empty());
386        assert!(h.options.is_empty());
387    }
388
389    #[test]
390    fn service_offer_round_trips() {
391        let ip = Ipv4Addr::new(192, 168, 1, 10);
392        let entry = Entry::OfferService(ServiceEntry {
393            service_id: 0x1234,
394            instance_id: 0x0001,
395            major_version: 1,
396            ttl: 0xFF_FFFF,
397            index_first_options_run: 0,
398            index_second_options_run: 0,
399            options_count: OptionsCount::new(1, 0),
400            minor_version: 0,
401        });
402        let endpoint = Options::IpV4Endpoint {
403            ip,
404            protocol: TransportProtocol::Udp,
405            port: 30509,
406        };
407        let entries = [entry];
408        let options = [endpoint];
409        let h = Header::new(
410            Flags::new_sd(RebootFlag::RecentlyRebooted),
411            &entries,
412            &options,
413        );
414        assert_eq!(h.encoded_size().unwrap(), 40);
415        let mut buf = [0u8; 64];
416        h.encode(&mut buf.as_mut_slice()).unwrap();
417        let view = SdHeaderView::parse(&buf[..h.encoded_size().unwrap()]).unwrap();
418        assert_eq!(view.entry_count(), 1);
419        let entry_view = view.entries().next().unwrap();
420        assert_eq!(entry_view.service_id(), 0x1234);
421    }
422
423    #[test]
424    fn subscribe_ack_round_trips() {
425        let entry = Entry::SubscribeAckEventGroup(EventGroupEntry::new(
426            0xAAAA, 0x0001, 1, 0xFF_FFFF, 0x0010,
427        ));
428        let entries = [entry];
429        let h = Header::new(Flags::new_sd(RebootFlag::RecentlyRebooted), &entries, &[]);
430        assert_eq!(h.encoded_size().unwrap(), 28);
431        let mut buf = [0u8; 32];
432        h.encode(&mut buf.as_mut_slice()).unwrap();
433        let view = SdHeaderView::parse(&buf[..h.encoded_size().unwrap()]).unwrap();
434        assert_eq!(view.entry_count(), 1);
435    }
436
437    #[test]
438    fn parse_exact_size_slice_succeeds() {
439        let entry = Entry::OfferService(ServiceEntry {
440            service_id: 0x1234,
441            instance_id: 0x0001,
442            major_version: 1,
443            ttl: 0xFF_FFFF,
444            index_first_options_run: 0,
445            index_second_options_run: 0,
446            options_count: OptionsCount::new(1, 0),
447            minor_version: 0,
448        });
449        let endpoint = Options::IpV4Endpoint {
450            ip: Ipv4Addr::new(192, 168, 1, 10),
451            protocol: TransportProtocol::Udp,
452            port: 30509,
453        };
454        let entries = [entry];
455        let options = [endpoint];
456        let h = Header::new(
457            Flags::new_sd(RebootFlag::RecentlyRebooted),
458            &entries,
459            &options,
460        );
461        let mut buf = [0u8; 64];
462        let n = h.encode(&mut buf.as_mut_slice()).unwrap();
463        let view = SdHeaderView::parse(&buf[..n]).unwrap();
464        assert_eq!(view.entry_count(), 1);
465    }
466
467    #[test]
468    fn parse_options_size_below_minimum_returns_error() {
469        // The eager L2 walk now drains the L1 option `DecodeIterator`, so a
470        // truncated options section surfaces the L1 `Incomplete` (the needed /
471        // available byte counts are unchanged) rather than the old hand-rolled
472        // `IncorrectOptionsSize`.
473        let prefix = raw_header(0, 2);
474        let mut buf = [0u8; 14];
475        buf[..12].copy_from_slice(&prefix);
476        assert!(matches!(
477            SdHeaderView::parse(&buf),
478            Err(crate::protocol::Error::Incomplete(
479                automotive_wire_codec::Incomplete {
480                    needed: 4,
481                    available: 2,
482                }
483            ))
484        ));
485    }
486
487    #[test]
488    fn parse_option_size_exceeds_declared_remaining_returns_error() {
489        let prefix = raw_header(0, 5);
490        let option = ipv4_endpoint_bytes([127, 0, 0, 1], 0x11, 1234);
491        let mut buf = [0u8; 24];
492        buf[..12].copy_from_slice(&prefix);
493        buf[12..24].copy_from_slice(&option);
494        assert!(matches!(
495            SdHeaderView::parse(&buf),
496            Err(crate::protocol::Error::Incomplete(
497                automotive_wire_codec::Incomplete {
498                    needed: 12,
499                    available: 5,
500                }
501            ))
502        ));
503    }
504
505    // --- SdHeaderView accessors ---
506
507    #[test]
508    fn sd_header_view_entry_count() {
509        let entries = [
510            Entry::FindService(ServiceEntry::find(0x0001)),
511            Entry::FindService(ServiceEntry::find(0x0002)),
512        ];
513        let h = Header::new(Flags::new_sd(RebootFlag::RecentlyRebooted), &entries, &[]);
514        let mut buf = [0u8; 64];
515        h.encode(&mut buf.as_mut_slice()).unwrap();
516        let view = SdHeaderView::parse(&buf[..h.encoded_size().unwrap()]).unwrap();
517        assert_eq!(view.entry_count(), 2);
518    }
519
520    #[test]
521    fn sd_header_view_accessors_yield_cached_counts() {
522        // After a successful parse, the infallible accessors yield exactly the
523        // cached counts and never panic.
524        let ip = Ipv4Addr::new(192, 168, 1, 10);
525        let entries = [
526            Entry::FindService(ServiceEntry::find(0x0001)),
527            Entry::FindService(ServiceEntry::find(0x0002)),
528        ];
529        let options = [
530            Options::IpV4Endpoint {
531                ip,
532                protocol: TransportProtocol::Udp,
533                port: 30509,
534            },
535            Options::IpV4Endpoint {
536                ip,
537                protocol: TransportProtocol::Tcp,
538                port: 30510,
539            },
540        ];
541        let h = Header::new(
542            Flags::new_sd(RebootFlag::RecentlyRebooted),
543            &entries,
544            &options,
545        );
546        let mut buf = [0u8; 128];
547        let n = h.encode(&mut buf.as_mut_slice()).unwrap();
548        let view = SdHeaderView::parse(&buf[..n]).unwrap();
549        assert_eq!(view.entry_count(), 2);
550        assert_eq!(view.option_count(), 2);
551        // Infallible accessors walk without panicking and match the counts.
552        assert_eq!(view.entries().count(), view.entry_count());
553        assert_eq!(view.options().count(), view.option_count());
554        // EntryIter is ExactSizeIterator: its len matches the cached count.
555        assert_eq!(view.entries().len(), view.entry_count());
556    }
557
558    #[test]
559    fn parse_rejects_trailing_partial_option() {
560        // options_size declares 12 bytes, but the single option's length field
561        // claims a wire size of 16 (length = 13). The eager walk must reject
562        // this at parse rather than silently truncating at accessor time.
563        let prefix = raw_header(0, 12);
564        let mut option = ipv4_endpoint_bytes([10, 0, 0, 1], 0x11, 30490);
565        // Overwrite the length field to claim more bytes than are present.
566        option[0..2].copy_from_slice(&13u16.to_be_bytes());
567        let mut buf = [0u8; 24];
568        buf[..12].copy_from_slice(&prefix);
569        buf[12..24].copy_from_slice(&option);
570        assert!(matches!(
571            SdHeaderView::parse(&buf),
572            Err(crate::protocol::Error::Incomplete(
573                automotive_wire_codec::Incomplete {
574                    needed: 16,
575                    available: 12,
576                }
577            ))
578        ));
579    }
580
581    #[test]
582    fn sd_header_view_flags() {
583        let h = Header::new(Flags::new_sd(RebootFlag::RecentlyRebooted), &[], &[]);
584        let mut buf = [0u8; 16];
585        h.encode(&mut buf.as_mut_slice()).unwrap();
586        let view = SdHeaderView::parse(&buf[..h.encoded_size().unwrap()]).unwrap();
587        assert_eq!(view.flags(), h.flags);
588    }
589
590    #[test]
591    fn parse_incorrect_entries_size_returns_error() {
592        let mut buf = [0u8; 12];
593        buf[4..8].copy_from_slice(&5u32.to_be_bytes());
594        assert!(matches!(
595            SdHeaderView::parse(&buf),
596            Err(crate::protocol::Error::Sd(SdError::IncorrectEntriesSize(5)))
597        ));
598    }
599
600    #[test]
601    fn parse_rejects_ipv4_option_with_invalid_transport_protocol() {
602        // An IPv4 endpoint option with an otherwise-valid wire layout but a
603        // transport protocol byte that is neither UDP (0x11) nor TCP (0x06)
604        // must be rejected by parse, so downstream `as_ipv4()` calls cannot
605        // observe a bad protocol byte at walk time.
606        const SD_HEADER_PREFIX_SIZE: usize = 12;
607        let options_size = u32::try_from(IPV4_OPTION_WIRE_SIZE).expect("wire size fits u32");
608        let prefix = raw_header(0, options_size);
609        let option = ipv4_endpoint_bytes([10, 0, 0, 1], 0xAB, 30490);
610        let mut buf = [0u8; SD_HEADER_PREFIX_SIZE + IPV4_OPTION_WIRE_SIZE];
611        buf[..SD_HEADER_PREFIX_SIZE].copy_from_slice(&prefix);
612        buf[SD_HEADER_PREFIX_SIZE..].copy_from_slice(&option);
613        assert!(matches!(
614            SdHeaderView::parse(&buf),
615            Err(crate::protocol::Error::Sd(
616                SdError::InvalidOptionTransportProtocol(0xAB)
617            ))
618        ));
619    }
620
621    // --- SdBody (Phase 3 lazy L1 decode) ---
622
623    #[test]
624    fn sd_body_decode_slices_sections() {
625        let ip = Ipv4Addr::new(192, 168, 1, 10);
626        let entry = Entry::OfferService(ServiceEntry {
627            service_id: 0x1234,
628            instance_id: 0x0001,
629            major_version: 1,
630            ttl: 0xFF_FFFF,
631            index_first_options_run: 0,
632            index_second_options_run: 0,
633            options_count: OptionsCount::new(1, 0),
634            minor_version: 0,
635        });
636        let endpoint = Options::IpV4Endpoint {
637            ip,
638            protocol: TransportProtocol::Udp,
639            port: 30509,
640        };
641        let entries = [entry];
642        let options = [endpoint];
643        let h = Header::new(
644            Flags::new_sd(RebootFlag::RecentlyRebooted),
645            &entries,
646            &options,
647        );
648        let mut buf = [0u8; 64];
649        let n = h.encode(&mut buf.as_mut_slice()).unwrap();
650        let (body, rest) = SdBody::decode(&buf[..n]).unwrap();
651        assert!(rest.is_empty());
652        assert_eq!(body.flags(), h.flags);
653        // Lazy iterators recover the entry and option.
654        let entry_view = body.entries().next().unwrap().unwrap();
655        assert_eq!(entry_view.service_id(), 0x1234);
656        let opt_view = body.options().next().unwrap().unwrap();
657        assert_eq!(opt_view.as_ipv4().unwrap().0, ip);
658    }
659
660    #[test]
661    fn sd_body_decode_returns_trailing_remainder() {
662        let h = Header::new(Flags::new_sd(RebootFlag::RecentlyRebooted), &[], &[]);
663        let mut buf = [0u8; 32];
664        let n = h.encode(&mut buf.as_mut_slice()).unwrap();
665        // Append 3 extra trailing bytes past the SD body.
666        buf[n] = 0xDE;
667        buf[n + 1] = 0xAD;
668        buf[n + 2] = 0xBE;
669        let (_body, rest) = SdBody::decode(&buf[..n + 3]).unwrap();
670        assert_eq!(rest, &[0xDE, 0xAD, 0xBE]);
671    }
672
673    #[test]
674    fn sd_body_decode_defers_entry_type_validation() {
675        // A body whose single entry has an invalid entry-type byte (0x03)
676        // must still decode successfully — SdBody does NOT walk entry types.
677        // entries_size = 16 (valid multiple), options_size = 0.
678        let mut buf = [0u8; 28];
679        buf[4..8].copy_from_slice(&16u32.to_be_bytes());
680        buf[8] = 0x03; // invalid entry type byte
681        // bytes 24..28 = options_size = 0
682        let (body, rest) = SdBody::decode(&buf).unwrap();
683        assert!(rest.is_empty());
684        // The lazy iterator produces the view; validation only fails on to_owned.
685        let entry_view = body.entries().next().unwrap().unwrap();
686        assert!(matches!(
687            entry_view.to_owned(),
688            Err(SdError::InvalidEntryType(0x03))
689        ));
690        // But SdHeaderView::parse (the eager L2 walk) DOES reject it.
691        assert!(matches!(
692            SdHeaderView::parse(&buf),
693            Err(crate::protocol::Error::Sd(SdError::InvalidEntryType(0x03)))
694        ));
695    }
696
697    #[test]
698    fn sd_body_decode_defers_option_validation() {
699        // options_size = 12 with an IPv4 option carrying an invalid transport
700        // protocol byte. SdBody slices it without complaint.
701        const PREFIX: usize = 12;
702        let options_size = u32::try_from(IPV4_OPTION_WIRE_SIZE).unwrap();
703        let prefix = raw_header(0, options_size);
704        let option = ipv4_endpoint_bytes([10, 0, 0, 1], 0xAB, 30490);
705        let mut buf = [0u8; PREFIX + IPV4_OPTION_WIRE_SIZE];
706        buf[..PREFIX].copy_from_slice(&prefix);
707        buf[PREFIX..].copy_from_slice(&option);
708        let (body, rest) = SdBody::decode(&buf).unwrap();
709        assert!(rest.is_empty());
710        let opt_view = body.options().next().unwrap().unwrap();
711        assert!(matches!(
712            opt_view.as_ipv4(),
713            Err(SdError::InvalidOptionTransportProtocol(0xAB))
714        ));
715    }
716
717    #[test]
718    fn sd_body_decode_too_short_is_incomplete() {
719        let buf = [0u8; 8];
720        assert!(matches!(
721            SdBody::decode(&buf),
722            Err(crate::protocol::Error::Incomplete(
723                automotive_wire_codec::Incomplete {
724                    needed: 12,
725                    available: 8,
726                }
727            ))
728        ));
729    }
730
731    #[test]
732    fn sd_body_decode_rejects_non_multiple_entries_size() {
733        let mut buf = [0u8; 12];
734        buf[4..8].copy_from_slice(&5u32.to_be_bytes());
735        assert!(matches!(
736            SdBody::decode(&buf),
737            Err(crate::protocol::Error::Sd(SdError::IncorrectEntriesSize(5)))
738        ));
739    }
740
741    #[test]
742    fn sd_body_entries_remaining_len_reports_count() {
743        let entries = [
744            Entry::FindService(ServiceEntry::find(0x0001)),
745            Entry::FindService(ServiceEntry::find(0x0002)),
746        ];
747        let h = Header::new(Flags::new_sd(RebootFlag::RecentlyRebooted), &entries, &[]);
748        let mut buf = [0u8; 64];
749        let n = h.encode(&mut buf.as_mut_slice()).unwrap();
750        let (body, _rest) = SdBody::decode(&buf[..n]).unwrap();
751        assert_eq!(body.entries().remaining_len(), Some(2));
752    }
753}