packet_formats/
gmp.rs

1// Copyright 2024 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5//! Common types and utilities between MLDv2 and IGMPv3.
6//!
7//! See [`crate::igmp`] and [`crate::icmp::mld`] for implementations.
8
9use core::borrow::Borrow;
10use core::fmt::Debug;
11use core::num::NonZeroUsize;
12use core::time::Duration;
13use core::usize;
14
15use net_types::MulticastAddr;
16use net_types::ip::IpAddress;
17
18/// Creates a bitmask of [n] bits, [n] must be <= 31.
19/// E.g. for n = 12 yields 0xFFF.
20const fn bitmask(n: u8) -> u32 {
21    assert!((n as u32) < u32::BITS);
22    (1 << n) - 1
23}
24
25/// Requested value doesn't fit the representation.
26#[derive(Debug, Eq, PartialEq)]
27pub struct OverflowError;
28
29/// Exact conversion failed.
30#[derive(Debug, Eq, PartialEq)]
31pub enum ExactConversionError {
32    /// Equivalent to [`OverflowError`].
33    Overflow,
34    /// An exact representation is not possible.
35    NotExact,
36}
37
38impl From<OverflowError> for ExactConversionError {
39    fn from(OverflowError: OverflowError) -> Self {
40        Self::Overflow
41    }
42}
43
44/// The trait converts a code to a floating point value: in a linear fashion up
45/// to `SWITCHPOINT` and then using a floating point representation to allow the
46/// conversion of larger values. In MLD and IGMP there are different codes that
47/// follow this pattern, e.g. QQIC, ResponseDelay ([RFC 3376 section 4.1], [RFC
48/// 3810 section 5.1]), which all convert a code with the following underlying
49/// structure:
50///
51///       0    NUM_EXP_BITS       NUM_MANT_BITS
52///      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
53///      |X|      exp      |          mant         |
54///      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
55///
56/// This trait simplifies the implementation by providing methods to perform the
57/// conversion.
58///
59/// [RFC 3376 section 4.1]:
60///     https://datatracker.ietf.org/doc/html/rfc3376#section-4.1
61/// [RFC 3810 section 5.1]:
62///     https://datatracker.ietf.org/doc/html/rfc3810#section-5.1
63pub(crate) trait LinExpConversion<C: Debug + PartialEq + Copy + Clone>:
64    Into<C> + Copy + Clone + Sized
65{
66    // Specified by Implementors
67    /// Number of bits used for the mantissa.
68    const NUM_MANT_BITS: u8;
69    /// Number of bits used for the exponent.
70    const NUM_EXP_BITS: u8;
71    /// Perform a lossy conversion from the `C` type.
72    ///
73    /// Not all values in `C` can be exactly represented using the code and they
74    /// will be rounded to a code that represents a value close the provided
75    /// one.
76    fn lossy_try_from(value: C) -> Result<Self, OverflowError>;
77
78    // Provided for Implementors.
79    /// How much the exponent needs to be incremented when performing the
80    /// exponential conversion.
81    const EXP_INCR: u32 = 3;
82    /// Bitmask for the mantissa.
83    const MANT_BITMASK: u32 = bitmask(Self::NUM_MANT_BITS);
84    /// Bitmask for the exponent.
85    const EXP_BITMASK: u32 = bitmask(Self::NUM_EXP_BITS);
86    /// First value for which we start the exponential conversion.
87    const SWITCHPOINT: u32 = 0x1 << (Self::NUM_MANT_BITS + Self::NUM_EXP_BITS);
88    /// Prefix for capturing the mantissa.
89    const MANT_PREFIX: u32 = 0x1 << Self::NUM_MANT_BITS;
90    /// Maximum value the code supports.
91    const MAX_VALUE: u32 =
92        (Self::MANT_BITMASK | Self::MANT_PREFIX) << (Self::EXP_INCR + Self::EXP_BITMASK);
93
94    /// Converts the provided code to a value: in a linear way until
95    /// [Self::SWITCHPOINT] and using a floating representation for larger
96    /// values.
97    fn to_expanded(code: u16) -> u32 {
98        let code = code.into();
99        if code < Self::SWITCHPOINT {
100            code
101        } else {
102            let mant = code & Self::MANT_BITMASK;
103            let exp = (code >> Self::NUM_MANT_BITS) & Self::EXP_BITMASK;
104            (mant | Self::MANT_PREFIX) << (Self::EXP_INCR + exp)
105        }
106    }
107
108    /// Performs a lossy conversion from `value`.
109    ///
110    /// The function will always succeed for values within the valid range.
111    /// However, the code might not exactly represent the provided input. E.g. a
112    /// value of `MAX_VALUE - 1` cannot be exactly represented with a
113    /// corresponding code, due the exponential representation. However, the
114    /// function will be able to provide a code representing a value close to
115    /// the provided one.
116    ///
117    /// If stronger guarantees are needed consider using
118    /// [`LinExpConversion::exact_try_from`].
119    fn lossy_try_from_expanded(value: u32) -> Result<u16, OverflowError> {
120        if value > Self::MAX_VALUE {
121            Err(OverflowError)
122        } else if value < Self::SWITCHPOINT {
123            // Given that Value is < Self::SWITCHPOINT, unwrapping here is safe.
124            let code = value.try_into().unwrap();
125            Ok(code)
126        } else {
127            let msb = (u32::BITS - value.leading_zeros()) - 1;
128            let exp = msb - u32::from(Self::NUM_MANT_BITS);
129            let mant = (value >> exp) & Self::MANT_BITMASK;
130            // Unwrap guaranteed by the structure of the built int:
131            let code = (Self::SWITCHPOINT | ((exp - Self::EXP_INCR) << Self::NUM_MANT_BITS) | mant)
132                .try_into()
133                .unwrap();
134            Ok(code)
135        }
136    }
137
138    /// Attempts an exact conversion from `value`.
139    ///
140    /// The function will succeed only for values within the valid range that
141    /// can be exactly represented by the produced code. E.g. a value of
142    /// `FLOATING_POINT_MAX_VALUE - 1` cannot be exactly represented with a
143    /// corresponding, code due the exponential representation. In this case,
144    /// the function will return an error.
145    ///
146    /// If a lossy conversion can be tolerated consider using
147    /// [`LinExpConversion::lossy_try_from_expanded`].
148    ///
149    /// If the conversion is attempt is lossy, returns `Ok(None)`.
150    fn exact_try_from(value: C) -> Result<Self, ExactConversionError> {
151        let res = Self::lossy_try_from(value)?;
152        if value == res.into() { Ok(res) } else { Err(ExactConversionError::NotExact) }
153    }
154}
155
156create_protocol_enum!(
157    /// Group/Multicast Record Types as defined in [RFC 3376 section 4.2.12] and
158    /// [RFC 3810 section 5.2.12].
159    ///
160    /// [RFC 3376 section 4.2.12]:
161    ///     https://tools.ietf.org/html/rfc3376#section-4.2.12
162    /// [RFC 3810 section 5.2.12]:
163    ///     https://www.rfc-editor.org/rfc/rfc3810#section-5.2.12
164    #[allow(missing_docs)]
165    #[derive(PartialEq, Eq, Copy, Clone, PartialOrd, Ord)]
166    pub enum GroupRecordType: u8 {
167        ModeIsInclude, 0x01, "Mode Is Include";
168        ModeIsExclude, 0x02, "Mode Is Exclude";
169        ChangeToIncludeMode, 0x03, "Change To Include Mode";
170        ChangeToExcludeMode, 0x04, "Change To Exclude Mode";
171        AllowNewSources, 0x05, "Allow New Sources";
172        BlockOldSources, 0x06, "Block Old Sources";
173    }
174);
175
176impl GroupRecordType {
177    /// Returns `true` if this record type allows the record to be split into
178    /// multiple reports.
179    ///
180    /// If `false`, then the list of sources should be truncated instead.
181    ///
182    /// From [RFC 3810 section 5.2.15]:
183    ///
184    /// > if its Type is not IS_EX or TO_EX, it is split into multiple Multicast
185    /// > Address Records; each such record contains a different subset of the
186    /// > source addresses, and is sent in a separate Report.
187    ///
188    /// > if its Type is IS_EX or TO_EX, a single Multicast Address Record is
189    /// > sent, with as many source addresses as can fit; the remaining source
190    /// > addresses are not reported.
191    ///
192    /// Text is equivalent in [RFC 3376 section 4.2.16]:
193    ///
194    /// > If a single Group Record contains so many source addresses that it
195    /// > does not fit within the size limit of a single Report message, if its
196    /// > Type is not MODE_IS_EXCLUDE or CHANGE_TO_EXCLUDE_MODE, it is split
197    /// > into multiple Group Records, each containing a different subset of the
198    /// > source addresses and each sent in a separate Report message.  If its
199    /// > Type is MODE_IS_EXCLUDE or CHANGE_TO_EXCLUDE_MODE, a single Group
200    /// > Record is sent, containing as many source addresses as can fit, and
201    /// > the remaining source addresses are not reported;
202    ///
203    /// [RFC 3810 section 5.2.15]:
204    ///     https://datatracker.ietf.org/doc/html/rfc3810#section-5.2.15
205    /// [RFC 3376 section 4.2.16]:
206    ///     https://datatracker.ietf.org/doc/html/rfc3376#section-4.2.16
207    fn allow_split(&self) -> bool {
208        match self {
209            GroupRecordType::ModeIsInclude
210            | GroupRecordType::ChangeToIncludeMode
211            | GroupRecordType::AllowNewSources
212            | GroupRecordType::BlockOldSources => true,
213            GroupRecordType::ModeIsExclude | GroupRecordType::ChangeToExcludeMode => false,
214        }
215    }
216}
217
218/// QQIC (Querier's Query Interval Code) used in IGMPv3/MLDv2 messages, defined
219/// in [RFC 3376 section 4.1.7] and [RFC 3810 section 5.1.9].
220///
221/// [RFC 3376 section 4.1.7]:
222///     https://datatracker.ietf.org/doc/html/rfc3376#section-4.1.7
223/// [RFC 3810 section 5.1.9]:
224///     https://datatracker.ietf.org/doc/html/rfc3810#section-5.1.9
225#[derive(PartialEq, Eq, Debug, Clone, Copy, Default)]
226pub struct QQIC(u8);
227
228impl QQIC {
229    /// Creates a new `QQIC` allowing lossy conversion from `value`.
230    pub fn new_lossy(value: Duration) -> Result<Self, OverflowError> {
231        Self::lossy_try_from(value)
232    }
233
234    /// Creates a new `QQIC` rejecting lossy conversion from `value`.
235    pub fn new_exact(value: Duration) -> Result<Self, ExactConversionError> {
236        Self::exact_try_from(value)
237    }
238}
239
240impl LinExpConversion<Duration> for QQIC {
241    const NUM_MANT_BITS: u8 = 4;
242    const NUM_EXP_BITS: u8 = 3;
243
244    fn lossy_try_from(value: Duration) -> Result<Self, OverflowError> {
245        let secs: u32 = value.as_secs().try_into().map_err(|_| OverflowError)?;
246        let code = Self::lossy_try_from_expanded(secs)?.try_into().map_err(|_| OverflowError)?;
247        Ok(Self(code))
248    }
249}
250
251impl From<QQIC> for Duration {
252    fn from(code: QQIC) -> Self {
253        let secs: u64 = QQIC::to_expanded(code.0.into()).into();
254        Duration::from_secs(secs)
255    }
256}
257
258impl From<QQIC> for u8 {
259    fn from(QQIC(v): QQIC) -> Self {
260        v
261    }
262}
263
264impl From<u8> for QQIC {
265    fn from(value: u8) -> Self {
266        Self(value)
267    }
268}
269
270/// QRV (Querier's Robustness Variable) used in IGMPv3/MLDv2 messages, defined
271/// in [RFC 3376 section 4.1.6] and [RFC 3810 section 5.1.8].
272///
273/// [RFC 3376 section 4.1.6]:
274///     https://datatracker.ietf.org/doc/html/rfc3376#section-4.1.6
275/// [RFC 3810 section 5.1.8]:
276///     https://datatracker.ietf.org/doc/html/rfc3810#section-5.1.8
277#[derive(PartialEq, Eq, Debug, Clone, Copy, Default)]
278pub struct QRV(u8);
279
280impl QRV {
281    const QRV_MAX: u8 = 7;
282
283    /// Returns the Querier's Robustness Variable.
284    ///
285    /// From [RFC 3376 section 4.1.6]: If the querier's [Robustness Variable]
286    /// exceeds 7, the maximum value of the QRV field, the QRV is set to zero.
287    ///
288    /// From [RFC 3810 section 5.1.8]: If the Querier's [Robustness Variable]
289    /// exceeds 7 (the maximum value of the QRV field), the QRV field is set to
290    /// zero.
291    ///
292    /// [RFC 3376 section 4.1.6]:
293    ///     https://datatracker.ietf.org/doc/html/rfc3376#section-4.1.6
294    ///
295    /// [RFC 3810 section 5.1.8]:
296    ///     https://datatracker.ietf.org/doc/html/rfc3810#section-5.1.8
297    pub fn new(robustness_value: u8) -> Self {
298        if robustness_value > Self::QRV_MAX {
299            return QRV(0);
300        }
301        QRV(robustness_value)
302    }
303}
304
305impl From<QRV> for u8 {
306    fn from(qrv: QRV) -> u8 {
307        qrv.0
308    }
309}
310
311/// A trait abstracting a multicast group record in MLDv2 or IGMPv3.
312///
313/// This trait facilitates the nested iterators required for implementing group
314/// records (iterator of groups, each of which with an iterator of sources)
315/// without propagating the inner iterator types far up.
316///
317/// An implementation for tuples of `(group, record_type, iterator)` is
318/// provided.
319pub trait GmpReportGroupRecord<A: IpAddress> {
320    /// Returns the multicast group this report refers to.
321    fn group(&self) -> MulticastAddr<A>;
322
323    /// Returns record type to insert in the record entry.
324    fn record_type(&self) -> GroupRecordType;
325
326    /// Returns an iterator over the sources in the report.
327    fn sources(&self) -> impl Iterator<Item: Borrow<A>> + '_;
328}
329
330impl<A, I> GmpReportGroupRecord<A> for (MulticastAddr<A>, GroupRecordType, I)
331where
332    A: IpAddress,
333    I: Iterator<Item: Borrow<A>> + Clone,
334{
335    fn group(&self) -> MulticastAddr<A> {
336        self.0
337    }
338
339    fn record_type(&self) -> GroupRecordType {
340        self.1
341    }
342
343    fn sources(&self) -> impl Iterator<Item: Borrow<A>> + '_ {
344        self.2.clone()
345    }
346}
347
348#[derive(Clone)]
349struct OverrideGroupRecordSources<R> {
350    record: R,
351    limit: NonZeroUsize,
352    skip: usize,
353}
354
355impl<R, A> GmpReportGroupRecord<A> for OverrideGroupRecordSources<R>
356where
357    A: IpAddress,
358    R: GmpReportGroupRecord<A>,
359{
360    fn group(&self) -> MulticastAddr<A> {
361        self.record.group()
362    }
363
364    fn record_type(&self) -> GroupRecordType {
365        self.record.record_type()
366    }
367
368    fn sources(&self) -> impl Iterator<Item: Borrow<A>> + '_ {
369        self.record.sources().skip(self.skip).take(self.limit.get())
370    }
371}
372
373/// The error returned when size constraints can't fit records.
374#[derive(Debug, Eq, PartialEq)]
375pub struct InvalidConstraintsError;
376
377pub(crate) fn group_record_split_iterator<A, I>(
378    max_len: usize,
379    group_header: usize,
380    groups: I,
381) -> Result<
382    impl Iterator<Item: Iterator<Item: GmpReportGroupRecord<A>> + Clone>,
383    InvalidConstraintsError,
384>
385where
386    A: IpAddress,
387    I: Iterator<Item: GmpReportGroupRecord<A> + Clone> + Clone,
388{
389    // We need a maximum length that can fit at least one group with one source.
390    if group_header + core::mem::size_of::<A>() > max_len {
391        return Err(InvalidConstraintsError);
392    }
393    // These are the mutable state given to the iterator.
394    //
395    // `groups` is the main iterator that is moved forward whenever we've fully
396    // yielded a group out on a `next` call.
397    let mut groups = groups.peekable();
398    // `skip` is saved in case the first group of a next iteration needs to skip
399    // sources entries.
400    let mut skip = 0;
401    Ok(core::iter::from_fn(move || {
402        let start = groups.clone();
403        let mut take = 0;
404        let mut len = 0;
405        loop {
406            let group = match groups.peek() {
407                Some(group) => group,
408                None => break,
409            };
410            len += group_header;
411            // Can't even fit the header.
412            if len > max_len {
413                break;
414            }
415
416            // `skip` is only going to be valid for the first group we look at,
417            // so always reset it to zero.
418            let skipped = core::mem::replace(&mut skip, 0);
419            let sources = group.sources();
420            if take == 0 {
421                // If this is the first group, we should be able to split this
422                // into multiple reports as necessary. Alternatively, if we have
423                // skipped records from a previous yield we should produce the
424                // rest of the records here.
425                let mut sources = sources.skip(skipped).enumerate();
426                loop {
427                    // NB: This is not written as a `while` or `for` loop so we
428                    // don't create temporaries that are holding on to borrows
429                    // of groups, which then allows us to drive the main
430                    // iterator before exiting here.
431                    let Some((i, _)) = sources.next() else { break };
432
433                    len += core::mem::size_of::<A>();
434                    if len > max_len {
435                        // We're ensured to always be able to fit at least one
436                        // group with one source per report, so we should never
437                        // hit max length on the first source.
438                        let limit = NonZeroUsize::new(i).expect("can't fit a single source");
439                        let record = if group.record_type().allow_split() {
440                            // Update skip so we yield the rest of the message
441                            // on the next iteration.
442                            skip = skipped + i;
443                            group.clone()
444                        } else {
445                            // Use the current limit and just ignore any further
446                            // sources. We known unwrap is okay here we just
447                            // peeked.
448                            drop(sources);
449                            groups.next().unwrap()
450                        };
451                        return Some(either::Either::Left(core::iter::once(
452                            OverrideGroupRecordSources { record, limit, skip: skipped },
453                        )));
454                    }
455                }
456                // If we need to skip any records, yield a single entry. It's a
457                // bit too complicated to insert this group in a report with
458                // other groups, so let's just issue the rest of its sources in
459                // its own report.
460                if skipped != 0 {
461                    // Consume this current group. Unwrap is safe we just
462                    // peeked.
463                    drop(sources);
464                    let group = groups.next().unwrap();
465                    return Some(either::Either::Left(core::iter::once(
466                        OverrideGroupRecordSources {
467                            record: group,
468                            limit: NonZeroUsize::MAX,
469                            skip: skipped,
470                        },
471                    )));
472                }
473            } else {
474                // We can't handle skipped sources here.
475                assert_eq!(skipped, 0);
476                // If not the first group only account for it if we can take all
477                // sources.
478                len += sources.count() * core::mem::size_of::<A>();
479                if len > max_len {
480                    break;
481                }
482            }
483
484            // This entry fits account for it.
485            let _: Option<_> = groups.next();
486            take += 1;
487        }
488
489        if take == 0 {
490            None
491        } else {
492            Some(either::Either::Right(start.take(take).map(|record| OverrideGroupRecordSources {
493                record,
494                limit: NonZeroUsize::MAX,
495                skip: 0,
496            })))
497        }
498    }))
499}
500
501#[cfg(test)]
502mod tests {
503    use core::ops::Range;
504
505    use super::*;
506
507    use ip_test_macro::ip_test;
508    use net_types::ip::{Ip, Ipv4Addr, Ipv6Addr};
509
510    fn empty_iter<A: IpAddress>() -> impl Iterator<Item: GmpReportGroupRecord<A> + Clone> + Clone {
511        core::iter::empty::<(MulticastAddr<A>, GroupRecordType, core::iter::Empty<A>)>()
512    }
513
514    fn addr<I: Ip>(i: u8) -> I::Addr {
515        I::map_ip_out(
516            i,
517            |i| Ipv4Addr::new([0, 0, 0, i]),
518            |i| Ipv6Addr::from_bytes([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, i]),
519        )
520    }
521
522    fn mcast_addr<I: Ip>(i: u8) -> MulticastAddr<I::Addr> {
523        MulticastAddr::new(I::map_ip_out(
524            i,
525            |i| Ipv4Addr::new([224, 0, 0, i]),
526            |i| Ipv6Addr::from_bytes([0xFF, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, i]),
527        ))
528        .unwrap()
529    }
530
531    fn addr_iter_range<I: Ip>(range: Range<u8>) -> impl Iterator<Item = I::Addr> + Clone {
532        range.into_iter().map(|i| addr::<I>(i))
533    }
534
535    fn collect<I, A>(iter: I) -> Vec<Vec<(MulticastAddr<A>, GroupRecordType, Vec<A>)>>
536    where
537        I: Iterator<Item: Iterator<Item: GmpReportGroupRecord<A>>>,
538        A: IpAddress,
539    {
540        iter.map(|groups| {
541            groups
542                .map(|g| {
543                    (
544                        g.group(),
545                        g.record_type(),
546                        g.sources().map(|b| b.borrow().clone()).collect::<Vec<_>>(),
547                    )
548                })
549                .collect::<Vec<_>>()
550        })
551        .collect::<Vec<_>>()
552    }
553
554    const GROUP_RECORD_HEADER: usize = 1;
555
556    #[ip_test(I)]
557    fn split_rejects_small_lengths<I: Ip>() {
558        assert_eq!(
559            group_record_split_iterator(
560                GROUP_RECORD_HEADER,
561                GROUP_RECORD_HEADER,
562                empty_iter::<I::Addr>()
563            )
564            .map(collect),
565            Err(InvalidConstraintsError)
566        );
567        assert_eq!(
568            group_record_split_iterator(
569                GROUP_RECORD_HEADER + core::mem::size_of::<I::Addr>() - 1,
570                GROUP_RECORD_HEADER,
571                empty_iter::<I::Addr>()
572            )
573            .map(collect),
574            Err(InvalidConstraintsError)
575        );
576        // Works, doesn't yield anything because of empty iterator.
577        assert_eq!(
578            group_record_split_iterator(
579                GROUP_RECORD_HEADER + core::mem::size_of::<I::Addr>(),
580                GROUP_RECORD_HEADER,
581                empty_iter::<I::Addr>()
582            )
583            .map(collect),
584            Ok(vec![])
585        );
586    }
587
588    #[ip_test(I)]
589    fn basic_split<I: Ip>() {
590        let iter = group_record_split_iterator(
591            GROUP_RECORD_HEADER + core::mem::size_of::<I::Addr>() * 2,
592            GROUP_RECORD_HEADER,
593            [
594                (mcast_addr::<I>(1), GroupRecordType::ModeIsInclude, addr_iter_range::<I>(1..2)),
595                (mcast_addr::<I>(2), GroupRecordType::ModeIsExclude, addr_iter_range::<I>(2..4)),
596                (
597                    mcast_addr::<I>(3),
598                    GroupRecordType::ChangeToIncludeMode,
599                    addr_iter_range::<I>(0..0),
600                ),
601                (
602                    mcast_addr::<I>(4),
603                    GroupRecordType::ChangeToExcludeMode,
604                    addr_iter_range::<I>(0..0),
605                ),
606            ]
607            .into_iter(),
608        )
609        .unwrap();
610
611        let report1 = vec![(
612            mcast_addr::<I>(1),
613            GroupRecordType::ModeIsInclude,
614            addr_iter_range::<I>(1..2).collect::<Vec<_>>(),
615        )];
616        let report2 = vec![(
617            mcast_addr::<I>(2),
618            GroupRecordType::ModeIsExclude,
619            addr_iter_range::<I>(2..4).collect::<Vec<_>>(),
620        )];
621        let report3 = vec![
622            (mcast_addr::<I>(3), GroupRecordType::ChangeToIncludeMode, vec![]),
623            (mcast_addr::<I>(4), GroupRecordType::ChangeToExcludeMode, vec![]),
624        ];
625        assert_eq!(collect(iter), vec![report1, report2, report3]);
626    }
627
628    #[ip_test(I)]
629    fn sources_split<I: Ip>() {
630        let iter = group_record_split_iterator(
631            GROUP_RECORD_HEADER + core::mem::size_of::<I::Addr>(),
632            GROUP_RECORD_HEADER,
633            [
634                (mcast_addr::<I>(1), GroupRecordType::ModeIsInclude, addr_iter_range::<I>(0..0)),
635                (mcast_addr::<I>(2), GroupRecordType::ModeIsInclude, addr_iter_range::<I>(0..3)),
636                (mcast_addr::<I>(3), GroupRecordType::ModeIsInclude, addr_iter_range::<I>(0..0)),
637            ]
638            .into_iter(),
639        )
640        .unwrap();
641
642        let report1 = vec![(mcast_addr::<I>(1), GroupRecordType::ModeIsInclude, vec![])];
643        let report2 = vec![(
644            mcast_addr::<I>(2),
645            GroupRecordType::ModeIsInclude,
646            addr_iter_range::<I>(0..1).collect::<Vec<_>>(),
647        )];
648        let report3 = vec![(
649            mcast_addr::<I>(2),
650            GroupRecordType::ModeIsInclude,
651            addr_iter_range::<I>(1..2).collect::<Vec<_>>(),
652        )];
653        let report4 = vec![(
654            mcast_addr::<I>(2),
655            GroupRecordType::ModeIsInclude,
656            addr_iter_range::<I>(2..3).collect::<Vec<_>>(),
657        )];
658        let report5 = vec![(mcast_addr::<I>(3), GroupRecordType::ModeIsInclude, vec![])];
659        assert_eq!(collect(iter), vec![report1, report2, report3, report4, report5]);
660    }
661
662    #[ip_test(I)]
663    fn sources_truncate<I: Ip>() {
664        let iter = group_record_split_iterator(
665            GROUP_RECORD_HEADER + core::mem::size_of::<I::Addr>(),
666            GROUP_RECORD_HEADER,
667            [
668                (mcast_addr::<I>(1), GroupRecordType::ModeIsInclude, addr_iter_range::<I>(0..0)),
669                (mcast_addr::<I>(2), GroupRecordType::ModeIsExclude, addr_iter_range::<I>(0..2)),
670                (mcast_addr::<I>(3), GroupRecordType::ModeIsInclude, addr_iter_range::<I>(2..3)),
671            ]
672            .into_iter(),
673        )
674        .unwrap();
675
676        let report1 = vec![(mcast_addr::<I>(1), GroupRecordType::ModeIsInclude, vec![])];
677        // Only one report for the exclude mode is generated, sources are
678        // truncated.
679        let report2 = vec![(
680            mcast_addr::<I>(2),
681            GroupRecordType::ModeIsExclude,
682            addr_iter_range::<I>(0..1).collect::<Vec<_>>(),
683        )];
684        let report3 = vec![(
685            mcast_addr::<I>(3),
686            GroupRecordType::ModeIsInclude,
687            addr_iter_range::<I>(2..3).collect::<Vec<_>>(),
688        )];
689        assert_eq!(collect(iter), vec![report1, report2, report3]);
690    }
691
692    /// Tests for a current limitation of the iterator. We don't attempt to pack
693    /// split sources, but rather possibly generate a short report.
694    #[ip_test(I)]
695    fn odd_split<I: Ip>() {
696        let iter = group_record_split_iterator(
697            GROUP_RECORD_HEADER + core::mem::size_of::<I::Addr>() * 4,
698            GROUP_RECORD_HEADER,
699            [
700                (mcast_addr::<I>(1), GroupRecordType::ModeIsInclude, addr_iter_range::<I>(0..5)),
701                (mcast_addr::<I>(2), GroupRecordType::ModeIsExclude, addr_iter_range::<I>(5..6)),
702            ]
703            .into_iter(),
704        )
705        .unwrap();
706
707        let report1 = vec![(
708            mcast_addr::<I>(1),
709            GroupRecordType::ModeIsInclude,
710            addr_iter_range::<I>(0..4).collect::<Vec<_>>(),
711        )];
712        let report2 = vec![(
713            mcast_addr::<I>(1),
714            GroupRecordType::ModeIsInclude,
715            addr_iter_range::<I>(4..5).collect::<Vec<_>>(),
716        )];
717        let report3 = vec![(
718            mcast_addr::<I>(2),
719            GroupRecordType::ModeIsExclude,
720            addr_iter_range::<I>(5..6).collect::<Vec<_>>(),
721        )];
722        assert_eq!(collect(iter), vec![report1, report2, report3]);
723    }
724
725    /// Tests that we prefer to keep a group together if we can, i.e., avoid
726    /// splitting off a group that is not the first in a message.
727    #[ip_test(I)]
728    fn split_off_large_group<I: Ip>() {
729        let iter = group_record_split_iterator(
730            (GROUP_RECORD_HEADER + core::mem::size_of::<I::Addr>()) * 2,
731            GROUP_RECORD_HEADER,
732            [
733                (mcast_addr::<I>(1), GroupRecordType::ModeIsInclude, addr_iter_range::<I>(0..1)),
734                // The beginning of this group should be in its own message.
735                (mcast_addr::<I>(2), GroupRecordType::ModeIsInclude, addr_iter_range::<I>(1..3)),
736                (mcast_addr::<I>(3), GroupRecordType::ModeIsInclude, addr_iter_range::<I>(3..4)),
737                // This group should be in its own message as opposed to
738                // truncating together with the previous one.
739                (mcast_addr::<I>(4), GroupRecordType::ModeIsExclude, addr_iter_range::<I>(4..6)),
740            ]
741            .into_iter(),
742        )
743        .unwrap();
744
745        let report1 = vec![(
746            mcast_addr::<I>(1),
747            GroupRecordType::ModeIsInclude,
748            addr_iter_range::<I>(0..1).collect::<Vec<_>>(),
749        )];
750        let report2 = vec![(
751            mcast_addr::<I>(2),
752            GroupRecordType::ModeIsInclude,
753            addr_iter_range::<I>(1..3).collect::<Vec<_>>(),
754        )];
755        let report3 = vec![(
756            mcast_addr::<I>(3),
757            GroupRecordType::ModeIsInclude,
758            addr_iter_range::<I>(3..4).collect::<Vec<_>>(),
759        )];
760        let report4 = vec![(
761            mcast_addr::<I>(4),
762            GroupRecordType::ModeIsExclude,
763            addr_iter_range::<I>(4..6).collect::<Vec<_>>(),
764        )];
765        assert_eq!(collect(iter), vec![report1, report2, report3, report4]);
766    }
767}