Skip to main content

simple_someip/protocol/sd/
entry.rs

1use super::Error;
2use crate::protocol::byte_order::WriteBytesExt;
3use automotive_wire_codec::{Decode, DecodeIter, Encode, take};
4
5pub const ENTRY_SIZE: usize = 16;
6
7/// The type of an SD entry.
8#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub enum EntryType {
10    /// Find a service (0x00).
11    FindService,
12    /// Offer a service (0x01).
13    OfferService,
14    /// Stop offering a service (0x02).
15    StopOfferService,
16    /// Subscribe to an event group (0x06).
17    Subscribe,
18    /// Acknowledge an event group subscription (0x07).
19    SubscribeAck,
20}
21
22impl TryFrom<u8> for EntryType {
23    type Error = Error;
24    fn try_from(value: u8) -> Result<Self, Error> {
25        match value {
26            0x00 => Ok(EntryType::FindService),
27            0x01 => Ok(EntryType::OfferService),
28            0x02 => Ok(EntryType::StopOfferService),
29            0x06 => Ok(EntryType::Subscribe),
30            0x07 => Ok(EntryType::SubscribeAck),
31            _ => Err(Error::InvalidEntryType(value)),
32        }
33    }
34}
35
36impl From<EntryType> for u8 {
37    fn from(service_entry_type: EntryType) -> u8 {
38        match service_entry_type {
39            EntryType::FindService => 0x00,
40            EntryType::OfferService => 0x01,
41            EntryType::StopOfferService => 0x02,
42            EntryType::Subscribe => 0x06,
43            EntryType::SubscribeAck => 0x07,
44        }
45    }
46}
47
48/// Packed pair of 4-bit option run counts (first and second options run).
49#[derive(Clone, Copy, Debug, Eq, PartialEq)]
50pub struct OptionsCount {
51    /// Number of options in the first options run.
52    pub first_options_count: u8,
53    /// Number of options in the second options run.
54    pub second_options_count: u8,
55}
56
57impl From<u8> for OptionsCount {
58    fn from(value: u8) -> Self {
59        let first_options_count = (value & 0xf0) >> 4;
60        let second_options_count = value & 0x0f;
61
62        Self {
63            first_options_count,
64            second_options_count,
65        }
66    }
67}
68
69impl From<OptionsCount> for u8 {
70    fn from(options_count: OptionsCount) -> u8 {
71        ((options_count.first_options_count << 4) & 0xf0)
72            | (options_count.second_options_count & 0x0f)
73    }
74}
75
76impl OptionsCount {
77    /// # Panics
78    /// Panics if either count is >= 16 (each count must fit in a 4-bit nibble).
79    #[must_use]
80    pub const fn new(first_options_count: u8, second_options_count: u8) -> Self {
81        assert!(first_options_count < 16);
82        assert!(second_options_count < 16);
83        OptionsCount {
84            first_options_count,
85            second_options_count,
86        }
87    }
88}
89
90/// An SD entry for event group operations (subscribe / subscribe-ack).
91#[derive(Clone, Debug, Eq, PartialEq)]
92pub struct EventGroupEntry {
93    /// Index into the options array for the first options run.
94    pub index_first_options_run: u8,
95    /// Index into the options array for the second options run.
96    pub index_second_options_run: u8,
97    /// Number of options in each run.
98    pub options_count: OptionsCount,
99    /// The SOME/IP service ID.
100    pub service_id: u16,
101    /// The SOME/IP instance ID.
102    pub instance_id: u16,
103    /// The major version of the service interface.
104    pub major_version: u8,
105    /// Time-to-live in seconds (24-bit value).
106    pub ttl: u32,
107    /// Event group counter.
108    pub counter: u16,
109    /// The event group ID.
110    pub event_group_id: u16,
111}
112
113impl EventGroupEntry {
114    /// Creates a new event group entry with default option indices.
115    #[must_use]
116    pub const fn new(
117        service_id: u16,
118        instance_id: u16,
119        major_version: u8,
120        ttl: u32,
121        event_group_id: u16,
122    ) -> Self {
123        Self {
124            index_first_options_run: 0,
125            index_second_options_run: 0,
126            options_count: OptionsCount::new(1, 0),
127            service_id,
128            instance_id,
129            major_version,
130            ttl,
131            counter: 0,
132            event_group_id,
133        }
134    }
135}
136
137impl Encode for EventGroupEntry {
138    type Error = crate::protocol::Error;
139
140    fn encoded_size(&self) -> Result<usize, Self::Error> {
141        Ok(15)
142    }
143
144    fn encode(&self, writer: &mut impl embedded_io::Write) -> Result<usize, Self::Error> {
145        writer.write_u8(self.index_first_options_run)?;
146        writer.write_u8(self.index_second_options_run)?;
147        writer.write_u8(u8::from(self.options_count))?;
148        writer.write_u16_be(self.service_id)?;
149        writer.write_u16_be(self.instance_id)?;
150        writer.write_u8(self.major_version)?;
151        writer.write_u24_be(self.ttl)?;
152        writer.write_u16_be(self.counter)?;
153        writer.write_u16_be(self.event_group_id)?;
154        Ok(15)
155    }
156}
157
158/// An SD entry for service operations (find / offer / stop-offer).
159#[derive(Clone, Debug, Eq, PartialEq)]
160pub struct ServiceEntry {
161    /// Index into the options array for the first options run.
162    pub index_first_options_run: u8,
163    /// Index into the options array for the second options run.
164    pub index_second_options_run: u8,
165    /// Number of options in each run.
166    pub options_count: OptionsCount,
167    /// The SOME/IP service ID.
168    pub service_id: u16,
169    /// The SOME/IP instance ID.
170    pub instance_id: u16,
171    /// The major version of the service interface.
172    pub major_version: u8,
173    /// Time-to-live in seconds (24-bit value).
174    pub ttl: u32,
175    /// The minor version of the service interface.
176    pub minor_version: u32,
177}
178
179impl ServiceEntry {
180    /// Creates a `FindService` entry with wildcard instance/version fields.
181    #[must_use]
182    pub const fn find(service_id: u16) -> Self {
183        Self {
184            index_first_options_run: 0,
185            index_second_options_run: 0,
186            options_count: OptionsCount::new(1, 0),
187            service_id,
188            instance_id: 0xFFFF,
189            major_version: 0xFF,
190            ttl: 0x00FF_FFFF,
191            minor_version: 0xFFFF_FFFF,
192        }
193    }
194}
195
196impl Encode for ServiceEntry {
197    type Error = crate::protocol::Error;
198
199    fn encoded_size(&self) -> Result<usize, Self::Error> {
200        Ok(15)
201    }
202
203    fn encode(&self, writer: &mut impl embedded_io::Write) -> Result<usize, Self::Error> {
204        writer.write_u8(self.index_first_options_run)?;
205        writer.write_u8(self.index_second_options_run)?;
206        writer.write_u8(u8::from(self.options_count))?;
207        writer.write_u16_be(self.service_id)?;
208        writer.write_u16_be(self.instance_id)?;
209        writer.write_u8(self.major_version)?;
210        writer.write_u24_be(self.ttl)?;
211        writer.write_u32_be(self.minor_version)?;
212        Ok(15)
213    }
214}
215
216/// A decoded SD entry, wrapping a [`ServiceEntry`] or [`EventGroupEntry`].
217#[derive(Clone, Debug, Eq, PartialEq)]
218pub enum Entry {
219    /// Find a service.
220    FindService(ServiceEntry),
221    /// Offer a service.
222    OfferService(ServiceEntry),
223    /// Stop offering a service.
224    StopOfferService(ServiceEntry),
225    /// Subscribe to an event group.
226    SubscribeEventGroup(EventGroupEntry),
227    /// Acknowledge an event group subscription.
228    SubscribeAckEventGroup(EventGroupEntry),
229}
230
231impl Entry {
232    /// Returns the number of options in the first options run.
233    #[must_use]
234    pub fn first_options_count(&self) -> u8 {
235        match self {
236            Entry::FindService(service_entry)
237            | Entry::OfferService(service_entry)
238            | Entry::StopOfferService(service_entry) => {
239                service_entry.options_count.first_options_count
240            }
241            Entry::SubscribeEventGroup(event_group_entry)
242            | Entry::SubscribeAckEventGroup(event_group_entry) => {
243                event_group_entry.options_count.first_options_count
244            }
245        }
246    }
247
248    /// Returns the number of options in the second options run.
249    #[must_use]
250    pub fn second_options_count(&self) -> u8 {
251        match self {
252            Entry::FindService(service_entry)
253            | Entry::OfferService(service_entry)
254            | Entry::StopOfferService(service_entry) => {
255                service_entry.options_count.second_options_count
256            }
257            Entry::SubscribeEventGroup(event_group_entry)
258            | Entry::SubscribeAckEventGroup(event_group_entry) => {
259                event_group_entry.options_count.second_options_count
260            }
261        }
262    }
263
264    /// Returns the total number of options across both runs.
265    #[must_use]
266    pub fn total_options_count(&self) -> u8 {
267        self.first_options_count() + self.second_options_count()
268    }
269}
270
271impl Encode for Entry {
272    type Error = crate::protocol::Error;
273
274    fn encoded_size(&self) -> Result<usize, Self::Error> {
275        // 1 type byte + 15 body bytes = 16 (ENTRY_SIZE) for every variant.
276        Ok(ENTRY_SIZE)
277    }
278
279    fn encode(&self, writer: &mut impl embedded_io::Write) -> Result<usize, Self::Error> {
280        let body = match self {
281            Entry::FindService(service_entry) => {
282                writer.write_u8(u8::from(EntryType::FindService))?;
283                service_entry.encode(writer)?
284            }
285            Entry::OfferService(service_entry) => {
286                writer.write_u8(u8::from(EntryType::OfferService))?;
287                service_entry.encode(writer)?
288            }
289            Entry::StopOfferService(service_entry) => {
290                writer.write_u8(u8::from(EntryType::StopOfferService))?;
291                service_entry.encode(writer)?
292            }
293            Entry::SubscribeEventGroup(event_group_entry) => {
294                writer.write_u8(u8::from(EntryType::Subscribe))?;
295                event_group_entry.encode(writer)?
296            }
297            Entry::SubscribeAckEventGroup(event_group_entry) => {
298                writer.write_u8(u8::from(EntryType::SubscribeAck))?;
299                event_group_entry.encode(writer)?
300            }
301        };
302        // 1 type byte + `body` (15) = 16.
303        Ok(1 + body)
304    }
305}
306
307// --- Zero-copy view types ---
308
309/// Zero-copy view into a 16-byte SD entry in a buffer.
310///
311/// Wire layout (16 bytes total):
312/// - `[0]`: entry type
313/// - `[1]`: `index_first_options_run`
314/// - `[2]`: `index_second_options_run`
315/// - `[3]`: `options_count` (packed nibbles)
316/// - `[4..6]`: `service_id` (BE)
317/// - `[6..8]`: `instance_id` (BE)
318/// - `[8]`: `major_version`
319/// - `[9..12]`: ttl (24-bit BE)
320/// - `[12..16]`: `minor_version` (BE) for service entries,
321///   OR `[12..14]` counter + `[14..16]` `event_group_id` for eventgroup entries
322#[derive(Clone, Copy, Debug)]
323pub struct EntryView<'a>(&'a [u8; ENTRY_SIZE]);
324
325impl EntryView<'_> {
326    /// Returns the entry type.
327    ///
328    /// # Errors
329    ///
330    /// Returns [`Error::InvalidEntryType`] if the type byte is not recognized.
331    pub fn entry_type(&self) -> Result<EntryType, Error> {
332        EntryType::try_from(self.0[0])
333    }
334
335    /// Returns the index of the first options run.
336    #[must_use]
337    pub fn index_first_options_run(&self) -> u8 {
338        self.0[1]
339    }
340
341    /// Returns the index of the second options run.
342    #[must_use]
343    pub fn index_second_options_run(&self) -> u8 {
344        self.0[2]
345    }
346
347    /// Returns the packed options count.
348    #[must_use]
349    pub fn options_count(&self) -> OptionsCount {
350        OptionsCount::from(self.0[3])
351    }
352
353    /// Returns the service ID.
354    #[must_use]
355    pub fn service_id(&self) -> u16 {
356        u16::from_be_bytes([self.0[4], self.0[5]])
357    }
358
359    /// Returns the instance ID.
360    #[must_use]
361    pub fn instance_id(&self) -> u16 {
362        u16::from_be_bytes([self.0[6], self.0[7]])
363    }
364
365    /// Returns the major version.
366    #[must_use]
367    pub fn major_version(&self) -> u8 {
368        self.0[8]
369    }
370
371    /// Returns the TTL (24-bit value).
372    #[must_use]
373    pub fn ttl(&self) -> u32 {
374        u32::from_be_bytes([0, self.0[9], self.0[10], self.0[11]])
375    }
376
377    /// Minor version (only valid for service entries).
378    #[must_use]
379    pub fn minor_version(&self) -> u32 {
380        u32::from_be_bytes([self.0[12], self.0[13], self.0[14], self.0[15]])
381    }
382
383    /// Counter field (only valid for eventgroup entries). Masked to lower 4 bits.
384    #[must_use]
385    pub fn counter(&self) -> u16 {
386        u16::from_be_bytes([self.0[12], self.0[13]]) & 0x000f
387    }
388
389    /// Event group ID (only valid for eventgroup entries).
390    #[must_use]
391    pub fn event_group_id(&self) -> u16 {
392        u16::from_be_bytes([self.0[14], self.0[15]])
393    }
394
395    /// Converts this view into an owned [`Entry`].
396    ///
397    /// # Errors
398    ///
399    /// Returns [`Error::InvalidEntryType`] if the entry type byte is not recognized.
400    pub fn to_owned(&self) -> Result<Entry, Error> {
401        let entry_type = self.entry_type()?;
402        match entry_type {
403            EntryType::FindService => Ok(Entry::FindService(self.to_service_entry())),
404            EntryType::OfferService => Ok(Entry::OfferService(self.to_service_entry())),
405            EntryType::StopOfferService => Ok(Entry::StopOfferService(self.to_service_entry())),
406            EntryType::Subscribe => Ok(Entry::SubscribeEventGroup(self.to_event_group_entry())),
407            EntryType::SubscribeAck => {
408                Ok(Entry::SubscribeAckEventGroup(self.to_event_group_entry()))
409            }
410        }
411    }
412
413    fn to_service_entry(self) -> ServiceEntry {
414        ServiceEntry {
415            index_first_options_run: self.index_first_options_run(),
416            index_second_options_run: self.index_second_options_run(),
417            options_count: self.options_count(),
418            service_id: self.service_id(),
419            instance_id: self.instance_id(),
420            major_version: self.major_version(),
421            ttl: self.ttl(),
422            minor_version: self.minor_version(),
423        }
424    }
425
426    fn to_event_group_entry(self) -> EventGroupEntry {
427        EventGroupEntry {
428            index_first_options_run: self.index_first_options_run(),
429            index_second_options_run: self.index_second_options_run(),
430            options_count: self.options_count(),
431            service_id: self.service_id(),
432            instance_id: self.instance_id(),
433            major_version: self.major_version(),
434            ttl: self.ttl(),
435            counter: self.counter(),
436            event_group_id: self.event_group_id(),
437        }
438    }
439}
440
441impl<'a> Decode<'a> for EntryView<'a> {
442    type Error = crate::protocol::Error;
443
444    /// Decode a single 16-byte SD entry from the front of `buf`.
445    ///
446    /// This is a pure fixed-stride slice: it does NOT validate the entry-type
447    /// byte. Validation is deferred to [`EntryView::entry_type`] /
448    /// [`EntryView::to_owned`] (the L2 validation pass), keeping this a lazy
449    /// zero-copy view.
450    ///
451    /// # Errors
452    ///
453    /// Returns [`Incomplete`](automotive_wire_codec::Incomplete) if fewer than
454    /// `ENTRY_SIZE` (16) bytes remain.
455    ///
456    /// # Panics
457    ///
458    /// Cannot panic — `take` guarantees exactly `ENTRY_SIZE` bytes.
459    fn decode(buf: &'a [u8]) -> Result<(Self, &'a [u8]), Self::Error> {
460        let (head, rest) = take(buf, ENTRY_SIZE)?;
461        let entry_bytes: &'a [u8; ENTRY_SIZE] =
462            head.try_into().expect("take guarantees ENTRY_SIZE bytes");
463        Ok((EntryView(entry_bytes), rest))
464    }
465}
466
467impl<'a> DecodeIter<'a> for EntryView<'a> {
468    type Error = crate::protocol::Error;
469
470    /// SD entries have a fixed 16-byte (`ENTRY_SIZE`) stride, enabling
471    /// [`DecodeIterator::remaining_len`](automotive_wire_codec::DecodeIterator::remaining_len).
472    const WIRE_SIZE: Option<usize> = Some(ENTRY_SIZE);
473
474    /// Decode the next entry, or `Ok(None)` at a clean end of buffer.
475    ///
476    /// A partial (non-multiple-of-16) trailing element is surfaced as an
477    /// `Err` rather than silently dropped.
478    ///
479    /// # Errors
480    ///
481    /// Returns [`Incomplete`](automotive_wire_codec::Incomplete) if a partial
482    /// entry remains after a good start.
483    fn decode_next(buf: &'a [u8]) -> Result<Option<(Self, &'a [u8])>, Self::Error> {
484        if buf.is_empty() {
485            return Ok(None);
486        }
487        Self::decode(buf).map(Some)
488    }
489}
490
491/// Iterator over 16-byte SD entries in a validated buffer.
492/// Entries are guaranteed valid (validated upfront in `SdHeaderView::parse`).
493pub struct EntryIter<'a> {
494    remaining: &'a [u8],
495}
496
497impl<'a> EntryIter<'a> {
498    pub(crate) fn new(buf: &'a [u8]) -> Self {
499        Self { remaining: buf }
500    }
501}
502
503impl<'a> Iterator for EntryIter<'a> {
504    type Item = EntryView<'a>;
505
506    fn next(&mut self) -> Option<Self::Item> {
507        if self.remaining.len() < ENTRY_SIZE {
508            return None;
509        }
510        let entry_bytes: &[u8; ENTRY_SIZE] = self.remaining[..ENTRY_SIZE]
511            .try_into()
512            .expect("length checked above");
513        self.remaining = &self.remaining[ENTRY_SIZE..];
514        Some(EntryView(entry_bytes))
515    }
516
517    fn size_hint(&self) -> (usize, Option<usize>) {
518        let n = self.remaining.len() / ENTRY_SIZE;
519        (n, Some(n))
520    }
521}
522
523impl ExactSizeIterator for EntryIter<'_> {}
524
525#[cfg(test)]
526mod tests {
527    use super::*;
528
529    fn encode_entry(entry: &Entry) -> [u8; 17] {
530        let mut buf = [0u8; 17];
531        entry.encode(&mut buf.as_mut_slice()).unwrap();
532        buf
533    }
534
535    fn make_service_entry() -> ServiceEntry {
536        ServiceEntry {
537            index_first_options_run: 1,
538            index_second_options_run: 2,
539            options_count: OptionsCount::new(3, 4),
540            service_id: 0x1234,
541            instance_id: 0x5678,
542            major_version: 0x01,
543            ttl: 0x0000_00FF,
544            minor_version: 0x0000_0002,
545        }
546    }
547
548    fn make_event_group_entry() -> EventGroupEntry {
549        EventGroupEntry {
550            index_first_options_run: 1,
551            index_second_options_run: 2,
552            options_count: OptionsCount::new(3, 4),
553            service_id: 0xABCD,
554            instance_id: 0x0001,
555            major_version: 0x02,
556            ttl: 0x0000_0064,
557            counter: 0x0003,
558            event_group_id: 0x0010,
559        }
560    }
561
562    // --- EntryType ---
563
564    #[test]
565    fn entry_type_try_from_all_valid_values() {
566        assert_eq!(EntryType::try_from(0x00).unwrap(), EntryType::FindService);
567        assert_eq!(EntryType::try_from(0x01).unwrap(), EntryType::OfferService);
568        assert_eq!(
569            EntryType::try_from(0x02).unwrap(),
570            EntryType::StopOfferService
571        );
572        assert_eq!(EntryType::try_from(0x06).unwrap(), EntryType::Subscribe);
573        assert_eq!(EntryType::try_from(0x07).unwrap(), EntryType::SubscribeAck);
574    }
575
576    #[test]
577    fn entry_type_try_from_invalid_returns_error() {
578        assert!(matches!(
579            EntryType::try_from(0x03),
580            Err(Error::InvalidEntryType(0x03))
581        ));
582    }
583
584    #[test]
585    fn entry_type_into_u8_all_variants() {
586        assert_eq!(u8::from(EntryType::FindService), 0x00);
587        assert_eq!(u8::from(EntryType::OfferService), 0x01);
588        assert_eq!(u8::from(EntryType::StopOfferService), 0x02);
589        assert_eq!(u8::from(EntryType::Subscribe), 0x06);
590        assert_eq!(u8::from(EntryType::SubscribeAck), 0x07);
591    }
592
593    // --- OptionsCount ---
594
595    #[test]
596    fn options_count_round_trip() {
597        let oc = OptionsCount::new(3, 7);
598        let byte = u8::from(oc);
599        let decoded = OptionsCount::from(byte);
600        assert_eq!(decoded.first_options_count, 3);
601        assert_eq!(decoded.second_options_count, 7);
602    }
603
604    // --- required_size ---
605
606    #[test]
607    fn service_entry_encoded_size() {
608        // 15 body bytes (no leading type byte — that belongs to `Entry`).
609        assert_eq!(make_service_entry().encoded_size().unwrap(), 15);
610    }
611
612    #[test]
613    fn event_group_entry_encoded_size() {
614        assert_eq!(make_event_group_entry().encoded_size().unwrap(), 15);
615    }
616
617    #[test]
618    fn entry_encoded_size_all_variants() {
619        // 1 type byte + 15 body bytes = 16 (ENTRY_SIZE) for every variant.
620        assert_eq!(
621            Entry::FindService(make_service_entry())
622                .encoded_size()
623                .unwrap(),
624            16
625        );
626        assert_eq!(
627            Entry::OfferService(make_service_entry())
628                .encoded_size()
629                .unwrap(),
630            16
631        );
632        assert_eq!(
633            Entry::StopOfferService(make_service_entry())
634                .encoded_size()
635                .unwrap(),
636            16
637        );
638        assert_eq!(
639            Entry::SubscribeEventGroup(make_event_group_entry())
640                .encoded_size()
641                .unwrap(),
642            16
643        );
644        assert_eq!(
645            Entry::SubscribeAckEventGroup(make_event_group_entry())
646                .encoded_size()
647                .unwrap(),
648            16
649        );
650    }
651
652    // --- first/second/total options count ---
653
654    #[test]
655    fn entry_options_count_service_variants() {
656        let se = make_service_entry(); // first=3, second=4
657        for entry in [
658            Entry::FindService(se),
659            Entry::OfferService(make_service_entry()),
660            Entry::StopOfferService(make_service_entry()),
661        ] {
662            assert_eq!(entry.first_options_count(), 3);
663            assert_eq!(entry.second_options_count(), 4);
664            assert_eq!(entry.total_options_count(), 7);
665        }
666    }
667
668    #[test]
669    fn entry_options_count_event_group_variants() {
670        let eg = make_event_group_entry(); // first=3, second=4
671        for entry in [
672            Entry::SubscribeEventGroup(eg.clone()),
673            Entry::SubscribeAckEventGroup(eg),
674        ] {
675            assert_eq!(entry.first_options_count(), 3);
676            assert_eq!(entry.second_options_count(), 4);
677            assert_eq!(entry.total_options_count(), 7);
678        }
679    }
680
681    // --- Entry encode / EntryView round-trips ---
682
683    #[test]
684    fn find_service_entry_round_trips() {
685        let entry = Entry::FindService(make_service_entry());
686        let buf = encode_entry(&entry);
687        // EntryView works on 16 bytes (type byte is first byte of the 16-byte entry)
688        // But Entry::encode writes type(1) + data(15) = 16 bytes out of the 17-byte buffer
689        let entry_bytes: &[u8; ENTRY_SIZE] = buf[..ENTRY_SIZE].try_into().unwrap();
690        let view = EntryView(entry_bytes);
691        assert_eq!(view.to_owned().unwrap(), entry);
692    }
693
694    #[test]
695    fn offer_service_entry_round_trips() {
696        let entry = Entry::OfferService(make_service_entry());
697        let buf = encode_entry(&entry);
698        let entry_bytes: &[u8; ENTRY_SIZE] = buf[..ENTRY_SIZE].try_into().unwrap();
699        let view = EntryView(entry_bytes);
700        assert_eq!(view.to_owned().unwrap(), entry);
701    }
702
703    #[test]
704    fn stop_offer_service_entry_round_trips() {
705        let entry = Entry::StopOfferService(make_service_entry());
706        let buf = encode_entry(&entry);
707        let entry_bytes: &[u8; ENTRY_SIZE] = buf[..ENTRY_SIZE].try_into().unwrap();
708        let view = EntryView(entry_bytes);
709        assert_eq!(view.to_owned().unwrap(), entry);
710    }
711
712    #[test]
713    fn subscribe_event_group_entry_round_trips() {
714        let entry = Entry::SubscribeEventGroup(make_event_group_entry());
715        let buf = encode_entry(&entry);
716        let entry_bytes: &[u8; ENTRY_SIZE] = buf[..ENTRY_SIZE].try_into().unwrap();
717        let view = EntryView(entry_bytes);
718        assert_eq!(view.to_owned().unwrap(), entry);
719    }
720
721    #[test]
722    fn subscribe_ack_event_group_entry_round_trips() {
723        let entry = Entry::SubscribeAckEventGroup(make_event_group_entry());
724        let buf = encode_entry(&entry);
725        let entry_bytes: &[u8; ENTRY_SIZE] = buf[..ENTRY_SIZE].try_into().unwrap();
726        let view = EntryView(entry_bytes);
727        assert_eq!(view.to_owned().unwrap(), entry);
728    }
729
730    #[test]
731    fn entry_view_invalid_type_returns_error() {
732        let buf: [u8; ENTRY_SIZE] = [0x03; ENTRY_SIZE]; // 0x03 is not a valid EntryType
733        let view = EntryView(&buf);
734        assert!(matches!(
735            view.to_owned(),
736            Err(Error::InvalidEntryType(0x03))
737        ));
738    }
739
740    // --- EntryIter ---
741
742    #[test]
743    fn entry_iter_empty() {
744        let iter = EntryIter::new(&[]);
745        assert_eq!(iter.len(), 0);
746    }
747
748    #[test]
749    fn entry_iter_two_entries() {
750        let e1 = Entry::FindService(make_service_entry());
751        let e2 = Entry::SubscribeEventGroup(make_event_group_entry());
752        let buf1 = encode_entry(&e1);
753        let buf2 = encode_entry(&e2);
754        // Concatenate the 16-byte entries (first 16 bytes of each 17-byte encode)
755        let mut combined = [0u8; 32];
756        combined[..16].copy_from_slice(&buf1[..16]);
757        combined[16..32].copy_from_slice(&buf2[..16]);
758
759        let mut iter = EntryIter::new(&combined);
760        assert_eq!(iter.len(), 2);
761        assert_eq!(iter.next().unwrap().to_owned().unwrap(), e1);
762        assert_eq!(iter.next().unwrap().to_owned().unwrap(), e2);
763        assert!(iter.next().is_none());
764    }
765
766    // --- Decode / DecodeIter (Phase 3 lazy L1) ---
767
768    fn two_entry_buf(e1: &Entry, e2: &Entry) -> [u8; 32] {
769        let b1 = encode_entry(e1);
770        let b2 = encode_entry(e2);
771        let mut combined = [0u8; 32];
772        combined[..16].copy_from_slice(&b1[..16]);
773        combined[16..32].copy_from_slice(&b2[..16]);
774        combined
775    }
776
777    #[test]
778    fn decode_yields_entry_and_remainder() {
779        let e1 = Entry::FindService(make_service_entry());
780        let e2 = Entry::SubscribeEventGroup(make_event_group_entry());
781        let buf = two_entry_buf(&e1, &e2);
782        let (view, rest) = EntryView::decode(&buf).unwrap();
783        assert_eq!(view.to_owned().unwrap(), e1);
784        assert_eq!(rest.len(), 16);
785        let (view2, rest2) = EntryView::decode(rest).unwrap();
786        assert_eq!(view2.to_owned().unwrap(), e2);
787        assert!(rest2.is_empty());
788    }
789
790    #[test]
791    fn decode_truncated_is_incomplete() {
792        let e1 = Entry::FindService(make_service_entry());
793        let buf = encode_entry(&e1);
794        assert!(matches!(
795            EntryView::decode(&buf[..15]),
796            Err(crate::protocol::Error::Incomplete(
797                automotive_wire_codec::Incomplete {
798                    needed: 16,
799                    available: 15,
800                }
801            ))
802        ));
803    }
804
805    #[test]
806    fn decode_exact_rejects_trailing() {
807        let e1 = Entry::FindService(make_service_entry());
808        let e2 = Entry::OfferService(make_service_entry());
809        let buf = two_entry_buf(&e1, &e2);
810        assert!(matches!(
811            EntryView::decode_exact(&buf),
812            Err(crate::protocol::Error::Trailing(_))
813        ));
814        // Single entry consumes the whole buffer.
815        assert!(EntryView::decode_exact(&buf[..16]).is_ok());
816    }
817
818    #[test]
819    fn decode_iter_yields_all_then_none() {
820        let e1 = Entry::FindService(make_service_entry());
821        let e2 = Entry::SubscribeEventGroup(make_event_group_entry());
822        let buf = two_entry_buf(&e1, &e2);
823        let mut iter = EntryView::iter(&buf);
824        assert_eq!(iter.next().unwrap().unwrap().to_owned().unwrap(), e1);
825        assert_eq!(iter.next().unwrap().unwrap().to_owned().unwrap(), e2);
826        assert!(iter.next().is_none());
827    }
828
829    #[test]
830    fn decode_iter_surfaces_truncated_tail_as_err() {
831        let e1 = Entry::FindService(make_service_entry());
832        let buf = two_entry_buf(&e1, &Entry::OfferService(make_service_entry()));
833        // One full entry plus a partial (8-byte) tail.
834        let mut iter = EntryView::iter(&buf[..24]);
835        assert!(matches!(iter.next(), Some(Ok(_))));
836        assert!(matches!(
837            iter.next(),
838            Some(Err(crate::protocol::Error::Incomplete(_)))
839        ));
840        // Adapter fuses after the first error.
841        assert!(iter.next().is_none());
842    }
843
844    #[test]
845    fn decode_iter_empty_is_immediately_none() {
846        let mut iter = EntryView::iter(&[]);
847        assert!(iter.next().is_none());
848    }
849
850    #[test]
851    fn decode_iter_remaining_len_counts_entries() {
852        let e1 = Entry::FindService(make_service_entry());
853        let e2 = Entry::SubscribeEventGroup(make_event_group_entry());
854        let buf = two_entry_buf(&e1, &e2);
855        let mut iter = EntryView::iter(&buf);
856        assert_eq!(iter.remaining_len(), Some(2));
857        iter.next();
858        assert_eq!(iter.remaining_len(), Some(1));
859        iter.next();
860        iter.next(); // Ok(None) -> done
861        assert_eq!(iter.remaining_len(), Some(0));
862    }
863
864    #[test]
865    fn decode_iter_does_not_validate_entry_type() {
866        // An invalid entry-type byte (0x03) must still decode as a view — the
867        // lazy path defers type validation to `to_owned` / `entry_type`.
868        let buf = [0x03u8; ENTRY_SIZE];
869        let mut iter = EntryView::iter(&buf);
870        let view = iter.next().unwrap().unwrap();
871        assert!(matches!(
872            view.to_owned(),
873            Err(Error::InvalidEntryType(0x03))
874        ));
875    }
876
877    // --- Encode size-exactness invariant ---
878
879    #[test]
880    fn entry_encoded_size_matches_bytes_written() {
881        use automotive_wire_codec::CountingSink;
882        for entry in [
883            Entry::FindService(make_service_entry()),
884            Entry::SubscribeEventGroup(make_event_group_entry()),
885        ] {
886            let mut sink = CountingSink::new();
887            let written = entry.encode(&mut sink).unwrap();
888            assert_eq!(written, entry.encoded_size().unwrap());
889            assert_eq!(written, sink.count());
890        }
891    }
892
893    #[test]
894    fn service_entry_encoded_size_matches_bytes_written() {
895        use automotive_wire_codec::CountingSink;
896        let se = make_service_entry();
897        let mut sink = CountingSink::new();
898        let written = se.encode(&mut sink).unwrap();
899        assert_eq!(written, se.encoded_size().unwrap());
900        assert_eq!(written, sink.count());
901
902        let eg = make_event_group_entry();
903        let mut sink = CountingSink::new();
904        let written = eg.encode(&mut sink).unwrap();
905        assert_eq!(written, eg.encoded_size().unwrap());
906        assert_eq!(written, sink.count());
907    }
908
909    #[test]
910    fn entry_encode_to_slice_too_small_yields_insufficient_buffer() {
911        use automotive_wire_codec::{EncodeToSliceError, InsufficientBuffer};
912        let entry = Entry::FindService(make_service_entry());
913        let mut buf = [0u8; 4]; // far smaller than 16
914        let err = entry.encode_to_slice(&mut buf).unwrap_err();
915        assert!(matches!(
916            err,
917            EncodeToSliceError::InsufficientBuffer(InsufficientBuffer {
918                needed: 16,
919                available: 4,
920            })
921        ));
922    }
923}