Skip to main content

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