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::ip::IpAddress;
16use net_types::MulticastAddr;
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() {
153            Ok(res)
154        } else {
155            Err(ExactConversionError::NotExact)
156        }
157    }
158}
159
160create_protocol_enum!(
161    /// Group/Multicast Record Types as defined in [RFC 3376 section 4.2.12] and
162    /// [RFC 3810 section 5.2.12].
163    ///
164    /// [RFC 3376 section 4.2.12]:
165    ///     https://tools.ietf.org/html/rfc3376#section-4.2.12
166    /// [RFC 3810 section 5.2.12]:
167    ///     https://www.rfc-editor.org/rfc/rfc3810#section-5.2.12
168    #[allow(missing_docs)]
169    #[derive(PartialEq, Eq, Copy, Clone, PartialOrd, Ord)]
170    pub enum GroupRecordType: u8 {
171        ModeIsInclude, 0x01, "Mode Is Include";
172        ModeIsExclude, 0x02, "Mode Is Exclude";
173        ChangeToIncludeMode, 0x03, "Change To Include Mode";
174        ChangeToExcludeMode, 0x04, "Change To Exclude Mode";
175        AllowNewSources, 0x05, "Allow New Sources";
176        BlockOldSources, 0x06, "Block Old Sources";
177    }
178);
179
180impl GroupRecordType {
181    /// Returns `true` if this record type allows the record to be split into
182    /// multiple reports.
183    ///
184    /// If `false`, then the list of sources should be truncated instead.
185    ///
186    /// From [RFC 3810 section 5.2.15]:
187    ///
188    /// > if its Type is not IS_EX or TO_EX, it is split into multiple Multicast
189    /// > Address Records; each such record contains a different subset of the
190    /// > source addresses, and is sent in a separate Report.
191    ///
192    /// > if its Type is IS_EX or TO_EX, a single Multicast Address Record is
193    /// > sent, with as many source addresses as can fit; the remaining source
194    /// > addresses are not reported.
195    ///
196    /// Text is equivalent in [RFC 3376 section 4.2.16]:
197    ///
198    /// > If a single Group Record contains so many source addresses that it
199    /// > does not fit within the size limit of a single Report message, if its
200    /// > Type is not MODE_IS_EXCLUDE or CHANGE_TO_EXCLUDE_MODE, it is split
201    /// > into multiple Group Records, each containing a different subset of the
202    /// > source addresses and each sent in a separate Report message.  If its
203    /// > Type is MODE_IS_EXCLUDE or CHANGE_TO_EXCLUDE_MODE, a single Group
204    /// > Record is sent, containing as many source addresses as can fit, and
205    /// > the remaining source addresses are not reported;
206    ///
207    /// [RFC 3810 section 5.2.15]:
208    ///     https://datatracker.ietf.org/doc/html/rfc3810#section-5.2.15
209    /// [RFC 3376 section 4.2.16]:
210    ///     https://datatracker.ietf.org/doc/html/rfc3376#section-4.2.16
211    fn allow_split(&self) -> bool {
212        match self {
213            GroupRecordType::ModeIsInclude
214            | GroupRecordType::ChangeToIncludeMode
215            | GroupRecordType::AllowNewSources
216            | GroupRecordType::BlockOldSources => true,
217            GroupRecordType::ModeIsExclude | GroupRecordType::ChangeToExcludeMode => false,
218        }
219    }
220}
221
222/// QQIC (Querier's Query Interval Code) used in IGMPv3/MLDv2 messages, defined
223/// in [RFC 3376 section 4.1.7] and [RFC 3810 section 5.1.9].
224///
225/// [RFC 3376 section 4.1.7]:
226///     https://datatracker.ietf.org/doc/html/rfc3376#section-4.1.7
227/// [RFC 3810 section 5.1.9]:
228///     https://datatracker.ietf.org/doc/html/rfc3810#section-5.1.9
229#[derive(PartialEq, Eq, Debug, Clone, Copy, Default)]
230pub struct QQIC(u8);
231
232impl QQIC {
233    /// Creates a new `QQIC` allowing lossy conversion from `value`.
234    pub fn new_lossy(value: Duration) -> Result<Self, OverflowError> {
235        Self::lossy_try_from(value)
236    }
237
238    /// Creates a new `QQIC` rejecting lossy conversion from `value`.
239    pub fn new_exact(value: Duration) -> Result<Self, ExactConversionError> {
240        Self::exact_try_from(value)
241    }
242}
243
244impl LinExpConversion<Duration> for QQIC {
245    const NUM_MANT_BITS: u8 = 4;
246    const NUM_EXP_BITS: u8 = 3;
247
248    fn lossy_try_from(value: Duration) -> Result<Self, OverflowError> {
249        let secs: u32 = value.as_secs().try_into().map_err(|_| OverflowError)?;
250        let code = Self::lossy_try_from_expanded(secs)?.try_into().map_err(|_| OverflowError)?;
251        Ok(Self(code))
252    }
253}
254
255impl From<QQIC> for Duration {
256    fn from(code: QQIC) -> Self {
257        let secs: u64 = QQIC::to_expanded(code.0.into()).into();
258        Duration::from_secs(secs)
259    }
260}
261
262impl From<QQIC> for u8 {
263    fn from(QQIC(v): QQIC) -> Self {
264        v
265    }
266}
267
268impl From<u8> for QQIC {
269    fn from(value: u8) -> Self {
270        Self(value)
271    }
272}
273
274/// QRV (Querier's Robustness Variable) used in IGMPv3/MLDv2 messages, defined
275/// in [RFC 3376 section 4.1.6] and [RFC 3810 section 5.1.8].
276///
277/// [RFC 3376 section 4.1.6]:
278///     https://datatracker.ietf.org/doc/html/rfc3376#section-4.1.6
279/// [RFC 3810 section 5.1.8]:
280///     https://datatracker.ietf.org/doc/html/rfc3810#section-5.1.8
281#[derive(PartialEq, Eq, Debug, Clone, Copy, Default)]
282pub struct QRV(u8);
283
284impl QRV {
285    const QRV_MAX: u8 = 7;
286
287    /// Returns the Querier's Robustness Variable.
288    ///
289    /// From [RFC 3376 section 4.1.6]: If the querier's [Robustness Variable]
290    /// exceeds 7, the maximum value of the QRV field, the QRV is set to zero.
291    ///
292    /// From [RFC 3810 section 5.1.8]: If the Querier's [Robustness Variable]
293    /// exceeds 7 (the maximum value of the QRV field), the QRV field is set to
294    /// zero.
295    ///
296    /// [RFC 3376 section 4.1.6]:
297    ///     https://datatracker.ietf.org/doc/html/rfc3376#section-4.1.6
298    ///
299    /// [RFC 3810 section 5.1.8]:
300    ///     https://datatracker.ietf.org/doc/html/rfc3810#section-5.1.8
301    pub fn new(robustness_value: u8) -> Self {
302        if robustness_value > Self::QRV_MAX {
303            return QRV(0);
304        }
305        QRV(robustness_value)
306    }
307}
308
309impl From<QRV> for u8 {
310    fn from(qrv: QRV) -> u8 {
311        qrv.0
312    }
313}
314
315/// A trait abstracting a multicast group record in MLDv2 or IGMPv3.
316///
317/// This trait facilitates the nested iterators required for implementing group
318/// records (iterator of groups, each of which with an iterator of sources)
319/// without propagating the inner iterator types far up.
320///
321/// An implementation for tuples of `(group, record_type, iterator)` is
322/// provided.
323pub trait GmpReportGroupRecord<A: IpAddress> {
324    /// Returns the multicast group this report refers to.
325    fn group(&self) -> MulticastAddr<A>;
326
327    /// Returns record type to insert in the record entry.
328    fn record_type(&self) -> GroupRecordType;
329
330    /// Returns an iterator over the sources in the report.
331    fn sources(&self) -> impl Iterator<Item: Borrow<A>> + '_;
332}
333
334impl<A, I> GmpReportGroupRecord<A> for (MulticastAddr<A>, GroupRecordType, I)
335where
336    A: IpAddress,
337    I: Iterator<Item: Borrow<A>> + Clone,
338{
339    fn group(&self) -> MulticastAddr<A> {
340        self.0
341    }
342
343    fn record_type(&self) -> GroupRecordType {
344        self.1
345    }
346
347    fn sources(&self) -> impl Iterator<Item: Borrow<A>> + '_ {
348        self.2.clone()
349    }
350}
351
352#[derive(Clone)]
353struct OverrideGroupRecordSources<R> {
354    record: R,
355    limit: NonZeroUsize,
356    skip: usize,
357}
358
359impl<R, A> GmpReportGroupRecord<A> for OverrideGroupRecordSources<R>
360where
361    A: IpAddress,
362    R: GmpReportGroupRecord<A>,
363{
364    fn group(&self) -> MulticastAddr<A> {
365        self.record.group()
366    }
367
368    fn record_type(&self) -> GroupRecordType {
369        self.record.record_type()
370    }
371
372    fn sources(&self) -> impl Iterator<Item: Borrow<A>> + '_ {
373        self.record.sources().skip(self.skip).take(self.limit.get())
374    }
375}
376
377/// The error returned when size constraints can't fit records.
378#[derive(Debug, Eq, PartialEq)]
379pub struct InvalidConstraintsError;
380
381pub(crate) fn group_record_split_iterator<A, I>(
382    max_len: usize,
383    group_header: usize,
384    groups: I,
385) -> Result<
386    impl Iterator<Item: Iterator<Item: GmpReportGroupRecord<A>> + Clone>,
387    InvalidConstraintsError,
388>
389where
390    A: IpAddress,
391    I: Iterator<Item: GmpReportGroupRecord<A> + Clone> + Clone,
392{
393    // We need a maximum length that can fit at least one group with one source.
394    if group_header + core::mem::size_of::<A>() > max_len {
395        return Err(InvalidConstraintsError);
396    }
397    // These are the mutable state given to the iterator.
398    //
399    // `groups` is the main iterator that is moved forward whenever we've fully
400    // yielded a group out on a `next` call.
401    let mut groups = groups.peekable();
402    // `skip` is saved in case the first group of a next iteration needs to skip
403    // sources entries.
404    let mut skip = 0;
405    Ok(core::iter::from_fn(move || {
406        let start = groups.clone();
407        let mut take = 0;
408        let mut len = 0;
409        loop {
410            let group = match groups.peek() {
411                Some(group) => group,
412                None => break,
413            };
414            len += group_header;
415            // Can't even fit the header.
416            if len > max_len {
417                break;
418            }
419
420            // `skip` is only going to be valid for the first group we look at,
421            // so always reset it to zero.
422            let skipped = core::mem::replace(&mut skip, 0);
423            let sources = group.sources();
424            if take == 0 {
425                // If this is the first group, we should be able to split this
426                // into multiple reports as necessary. Alternatively, if we have
427                // skipped records from a previous yield we should produce the
428                // rest of the records here.
429                let mut sources = sources.skip(skipped).enumerate();
430                loop {
431                    // NB: This is not written as a `while` or `for` loop so we
432                    // don't create temporaries that are holding on to borrows
433                    // of groups, which then allows us to drive the main
434                    // iterator before exiting here.
435                    let Some((i, _)) = sources.next() else { break };
436
437                    len += core::mem::size_of::<A>();
438                    if len > max_len {
439                        // We're ensured to always be able to fit at least one
440                        // group with one source per report, so we should never
441                        // hit max length on the first source.
442                        let limit = NonZeroUsize::new(i).expect("can't fit a single source");
443                        let record = if group.record_type().allow_split() {
444                            // Update skip so we yield the rest of the message
445                            // on the next iteration.
446                            skip = skipped + i;
447                            group.clone()
448                        } else {
449                            // Use the current limit and just ignore any further
450                            // sources. We known unwrap is okay here we just
451                            // peeked.
452                            drop(sources);
453                            groups.next().unwrap()
454                        };
455                        return Some(either::Either::Left(core::iter::once(
456                            OverrideGroupRecordSources { record, limit, skip: skipped },
457                        )));
458                    }
459                }
460                // If we need to skip any records, yield a single entry. It's a
461                // bit too complicated to insert this group in a report with
462                // other groups, so let's just issue the rest of its sources in
463                // its own report.
464                if skipped != 0 {
465                    // Consume this current group. Unwrap is safe we just
466                    // peeked.
467                    drop(sources);
468                    let group = groups.next().unwrap();
469                    return Some(either::Either::Left(core::iter::once(
470                        OverrideGroupRecordSources {
471                            record: group,
472                            limit: NonZeroUsize::MAX,
473                            skip: skipped,
474                        },
475                    )));
476                }
477            } else {
478                // We can't handle skipped sources here.
479                assert_eq!(skipped, 0);
480                // If not the first group only account for it if we can take all
481                // sources.
482                len += sources.count() * core::mem::size_of::<A>();
483                if len > max_len {
484                    break;
485                }
486            }
487
488            // This entry fits account for it.
489            let _: Option<_> = groups.next();
490            take += 1;
491        }
492
493        if take == 0 {
494            None
495        } else {
496            Some(either::Either::Right(start.take(take).map(|record| OverrideGroupRecordSources {
497                record,
498                limit: NonZeroUsize::MAX,
499                skip: 0,
500            })))
501        }
502    }))
503}
504
505#[cfg(test)]
506mod tests {
507    use core::ops::Range;
508
509    use super::*;
510
511    use ip_test_macro::ip_test;
512    use net_types::ip::{Ip, Ipv4Addr, Ipv6Addr};
513
514    fn empty_iter<A: IpAddress>() -> impl Iterator<Item: GmpReportGroupRecord<A> + Clone> + Clone {
515        core::iter::empty::<(MulticastAddr<A>, GroupRecordType, core::iter::Empty<A>)>()
516    }
517
518    fn addr<I: Ip>(i: u8) -> I::Addr {
519        I::map_ip_out(
520            i,
521            |i| Ipv4Addr::new([0, 0, 0, i]),
522            |i| Ipv6Addr::from_bytes([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, i]),
523        )
524    }
525
526    fn mcast_addr<I: Ip>(i: u8) -> MulticastAddr<I::Addr> {
527        MulticastAddr::new(I::map_ip_out(
528            i,
529            |i| Ipv4Addr::new([224, 0, 0, i]),
530            |i| Ipv6Addr::from_bytes([0xFF, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, i]),
531        ))
532        .unwrap()
533    }
534
535    fn addr_iter_range<I: Ip>(range: Range<u8>) -> impl Iterator<Item = I::Addr> + Clone {
536        range.into_iter().map(|i| addr::<I>(i))
537    }
538
539    fn collect<I, A>(iter: I) -> Vec<Vec<(MulticastAddr<A>, GroupRecordType, Vec<A>)>>
540    where
541        I: Iterator<Item: Iterator<Item: GmpReportGroupRecord<A>>>,
542        A: IpAddress,
543    {
544        iter.map(|groups| {
545            groups
546                .map(|g| {
547                    (
548                        g.group(),
549                        g.record_type(),
550                        g.sources().map(|b| b.borrow().clone()).collect::<Vec<_>>(),
551                    )
552                })
553                .collect::<Vec<_>>()
554        })
555        .collect::<Vec<_>>()
556    }
557
558    const GROUP_RECORD_HEADER: usize = 1;
559
560    #[ip_test(I)]
561    fn split_rejects_small_lengths<I: Ip>() {
562        assert_eq!(
563            group_record_split_iterator(
564                GROUP_RECORD_HEADER,
565                GROUP_RECORD_HEADER,
566                empty_iter::<I::Addr>()
567            )
568            .map(collect),
569            Err(InvalidConstraintsError)
570        );
571        assert_eq!(
572            group_record_split_iterator(
573                GROUP_RECORD_HEADER + core::mem::size_of::<I::Addr>() - 1,
574                GROUP_RECORD_HEADER,
575                empty_iter::<I::Addr>()
576            )
577            .map(collect),
578            Err(InvalidConstraintsError)
579        );
580        // Works, doesn't yield anything because of empty iterator.
581        assert_eq!(
582            group_record_split_iterator(
583                GROUP_RECORD_HEADER + core::mem::size_of::<I::Addr>(),
584                GROUP_RECORD_HEADER,
585                empty_iter::<I::Addr>()
586            )
587            .map(collect),
588            Ok(vec![])
589        );
590    }
591
592    #[ip_test(I)]
593    fn basic_split<I: Ip>() {
594        let iter = group_record_split_iterator(
595            GROUP_RECORD_HEADER + core::mem::size_of::<I::Addr>() * 2,
596            GROUP_RECORD_HEADER,
597            [
598                (mcast_addr::<I>(1), GroupRecordType::ModeIsInclude, addr_iter_range::<I>(1..2)),
599                (mcast_addr::<I>(2), GroupRecordType::ModeIsExclude, addr_iter_range::<I>(2..4)),
600                (
601                    mcast_addr::<I>(3),
602                    GroupRecordType::ChangeToIncludeMode,
603                    addr_iter_range::<I>(0..0),
604                ),
605                (
606                    mcast_addr::<I>(4),
607                    GroupRecordType::ChangeToExcludeMode,
608                    addr_iter_range::<I>(0..0),
609                ),
610            ]
611            .into_iter(),
612        )
613        .unwrap();
614
615        let report1 = vec![(
616            mcast_addr::<I>(1),
617            GroupRecordType::ModeIsInclude,
618            addr_iter_range::<I>(1..2).collect::<Vec<_>>(),
619        )];
620        let report2 = vec![(
621            mcast_addr::<I>(2),
622            GroupRecordType::ModeIsExclude,
623            addr_iter_range::<I>(2..4).collect::<Vec<_>>(),
624        )];
625        let report3 = vec![
626            (mcast_addr::<I>(3), GroupRecordType::ChangeToIncludeMode, vec![]),
627            (mcast_addr::<I>(4), GroupRecordType::ChangeToExcludeMode, vec![]),
628        ];
629        assert_eq!(collect(iter), vec![report1, report2, report3]);
630    }
631
632    #[ip_test(I)]
633    fn sources_split<I: Ip>() {
634        let iter = group_record_split_iterator(
635            GROUP_RECORD_HEADER + core::mem::size_of::<I::Addr>(),
636            GROUP_RECORD_HEADER,
637            [
638                (mcast_addr::<I>(1), GroupRecordType::ModeIsInclude, addr_iter_range::<I>(0..0)),
639                (mcast_addr::<I>(2), GroupRecordType::ModeIsInclude, addr_iter_range::<I>(0..3)),
640                (mcast_addr::<I>(3), GroupRecordType::ModeIsInclude, addr_iter_range::<I>(0..0)),
641            ]
642            .into_iter(),
643        )
644        .unwrap();
645
646        let report1 = vec![(mcast_addr::<I>(1), GroupRecordType::ModeIsInclude, vec![])];
647        let report2 = vec![(
648            mcast_addr::<I>(2),
649            GroupRecordType::ModeIsInclude,
650            addr_iter_range::<I>(0..1).collect::<Vec<_>>(),
651        )];
652        let report3 = vec![(
653            mcast_addr::<I>(2),
654            GroupRecordType::ModeIsInclude,
655            addr_iter_range::<I>(1..2).collect::<Vec<_>>(),
656        )];
657        let report4 = vec![(
658            mcast_addr::<I>(2),
659            GroupRecordType::ModeIsInclude,
660            addr_iter_range::<I>(2..3).collect::<Vec<_>>(),
661        )];
662        let report5 = vec![(mcast_addr::<I>(3), GroupRecordType::ModeIsInclude, vec![])];
663        assert_eq!(collect(iter), vec![report1, report2, report3, report4, report5]);
664    }
665
666    #[ip_test(I)]
667    fn sources_truncate<I: Ip>() {
668        let iter = group_record_split_iterator(
669            GROUP_RECORD_HEADER + core::mem::size_of::<I::Addr>(),
670            GROUP_RECORD_HEADER,
671            [
672                (mcast_addr::<I>(1), GroupRecordType::ModeIsInclude, addr_iter_range::<I>(0..0)),
673                (mcast_addr::<I>(2), GroupRecordType::ModeIsExclude, addr_iter_range::<I>(0..2)),
674                (mcast_addr::<I>(3), GroupRecordType::ModeIsInclude, addr_iter_range::<I>(2..3)),
675            ]
676            .into_iter(),
677        )
678        .unwrap();
679
680        let report1 = vec![(mcast_addr::<I>(1), GroupRecordType::ModeIsInclude, vec![])];
681        // Only one report for the exclude mode is generated, sources are
682        // truncated.
683        let report2 = vec![(
684            mcast_addr::<I>(2),
685            GroupRecordType::ModeIsExclude,
686            addr_iter_range::<I>(0..1).collect::<Vec<_>>(),
687        )];
688        let report3 = vec![(
689            mcast_addr::<I>(3),
690            GroupRecordType::ModeIsInclude,
691            addr_iter_range::<I>(2..3).collect::<Vec<_>>(),
692        )];
693        assert_eq!(collect(iter), vec![report1, report2, report3]);
694    }
695
696    /// Tests for a current limitation of the iterator. We don't attempt to pack
697    /// split sources, but rather possibly generate a short report.
698    #[ip_test(I)]
699    fn odd_split<I: Ip>() {
700        let iter = group_record_split_iterator(
701            GROUP_RECORD_HEADER + core::mem::size_of::<I::Addr>() * 4,
702            GROUP_RECORD_HEADER,
703            [
704                (mcast_addr::<I>(1), GroupRecordType::ModeIsInclude, addr_iter_range::<I>(0..5)),
705                (mcast_addr::<I>(2), GroupRecordType::ModeIsExclude, addr_iter_range::<I>(5..6)),
706            ]
707            .into_iter(),
708        )
709        .unwrap();
710
711        let report1 = vec![(
712            mcast_addr::<I>(1),
713            GroupRecordType::ModeIsInclude,
714            addr_iter_range::<I>(0..4).collect::<Vec<_>>(),
715        )];
716        let report2 = vec![(
717            mcast_addr::<I>(1),
718            GroupRecordType::ModeIsInclude,
719            addr_iter_range::<I>(4..5).collect::<Vec<_>>(),
720        )];
721        let report3 = vec![(
722            mcast_addr::<I>(2),
723            GroupRecordType::ModeIsExclude,
724            addr_iter_range::<I>(5..6).collect::<Vec<_>>(),
725        )];
726        assert_eq!(collect(iter), vec![report1, report2, report3]);
727    }
728
729    /// Tests that we prefer to keep a group together if we can, i.e., avoid
730    /// splitting off a group that is not the first in a message.
731    #[ip_test(I)]
732    fn split_off_large_group<I: Ip>() {
733        let iter = group_record_split_iterator(
734            (GROUP_RECORD_HEADER + core::mem::size_of::<I::Addr>()) * 2,
735            GROUP_RECORD_HEADER,
736            [
737                (mcast_addr::<I>(1), GroupRecordType::ModeIsInclude, addr_iter_range::<I>(0..1)),
738                // The beginning of this group should be in its own message.
739                (mcast_addr::<I>(2), GroupRecordType::ModeIsInclude, addr_iter_range::<I>(1..3)),
740                (mcast_addr::<I>(3), GroupRecordType::ModeIsInclude, addr_iter_range::<I>(3..4)),
741                // This group should be in its own message as opposed to
742                // truncating together with the previous one.
743                (mcast_addr::<I>(4), GroupRecordType::ModeIsExclude, addr_iter_range::<I>(4..6)),
744            ]
745            .into_iter(),
746        )
747        .unwrap();
748
749        let report1 = vec![(
750            mcast_addr::<I>(1),
751            GroupRecordType::ModeIsInclude,
752            addr_iter_range::<I>(0..1).collect::<Vec<_>>(),
753        )];
754        let report2 = vec![(
755            mcast_addr::<I>(2),
756            GroupRecordType::ModeIsInclude,
757            addr_iter_range::<I>(1..3).collect::<Vec<_>>(),
758        )];
759        let report3 = vec![(
760            mcast_addr::<I>(3),
761            GroupRecordType::ModeIsInclude,
762            addr_iter_range::<I>(3..4).collect::<Vec<_>>(),
763        )];
764        let report4 = vec![(
765            mcast_addr::<I>(4),
766            GroupRecordType::ModeIsExclude,
767            addr_iter_range::<I>(4..6).collect::<Vec<_>>(),
768        )];
769        assert_eq!(collect(iter), vec![report1, report2, report3, report4]);
770    }
771}