netstack3_tcp/
seq_ranges.rs

1// Copyright 2025 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
5use alloc::collections::VecDeque;
6use core::ops::Range;
7
8use derivative::Derivative;
9use netstack3_base::SeqNum;
10
11/// A generic data structure that keeps track of ordered sequence number ranges.
12///
13/// Each kept range has associated metadata of type `M`.
14#[derive(Debug, Derivative)]
15#[derivative(Default(bound = ""))]
16#[cfg_attr(test, derive(PartialEq, Eq))]
17pub(crate) struct SeqRanges<M> {
18    blocks: VecDeque<SeqRange<M>>,
19}
20
21impl<M> SeqRanges<M> {
22    pub(crate) fn is_empty(&self) -> bool {
23        self.blocks.is_empty()
24    }
25
26    pub(crate) fn pop_front_if<F: FnOnce(&SeqRange<M>) -> bool>(
27        &mut self,
28        f: F,
29    ) -> Option<SeqRange<M>> {
30        let front = self.blocks.front()?;
31        if f(front) {
32            self.blocks.pop_front()
33        } else {
34            None
35        }
36    }
37
38    /// Inserts `range` into this tracking structure.
39    ///
40    /// No-op if the range is empty.
41    ///
42    /// `meta` is attached to the newly created range and all the ranges it
43    /// touches, including if `range` is a subset of a currently tracked range.
44    pub(crate) fn insert(&mut self, range: Range<SeqNum>, meta: M)
45    where
46        M: Clone,
47    {
48        let Range { mut start, mut end } = range;
49        let Self { blocks } = self;
50
51        if start == end {
52            return;
53        }
54
55        if blocks.is_empty() {
56            blocks.push_back(SeqRange { range: Range { start, end }, meta });
57            return;
58        }
59
60        // Search for the first segment whose `start` is greater.
61        let first_after = match blocks.binary_search_by(|block| {
62            if block.range.start == start {
63                return core::cmp::Ordering::Equal;
64            }
65            if block.range.start.before(start) {
66                core::cmp::Ordering::Less
67            } else {
68                core::cmp::Ordering::Greater
69            }
70        }) {
71            Ok(r) => {
72                // We found the exact same start point, so the first segment
73                // whose start is greater must be the next one.
74                r + 1
75            }
76            Err(e) => {
77                // When binary search doesn't find the exact place it returns
78                // the index where this block should be in, which should be the
79                // next greater range.
80                e
81            }
82        };
83
84        let mut merge_right = 0;
85        for block in blocks.range(first_after..blocks.len()) {
86            if end.before(block.range.start) {
87                break;
88            }
89            merge_right += 1;
90            if end.before(block.range.end) {
91                end = block.range.end;
92                break;
93            }
94        }
95
96        let mut merge_left = 0;
97        for block in blocks.range(0..first_after).rev() {
98            if start.after(block.range.end) {
99                break;
100            }
101            // There is no guarantee that `end.after(range.end)`, not doing
102            // the following may shrink existing coverage. For example:
103            // range.start = 0, range.end = 10, start = 0, end = 1, will result
104            // in only 0..1 being tracked in the resulting assembler. We didn't
105            // do the symmetrical thing above when merging to the right because
106            // the search guarantees that `start.before(range.start)`, thus the
107            // problem doesn't exist there. The asymmetry rose from the fact
108            // that we used `start` to perform the search.
109            if end.before(block.range.end) {
110                end = block.range.end;
111            }
112            merge_left += 1;
113            if start.after(block.range.start) {
114                start = block.range.start;
115                break;
116            }
117        }
118
119        if merge_left == 0 && merge_right == 0 {
120            // If the new segment cannot merge with any of its neighbors, we
121            // add a new entry for it.
122            blocks.insert(first_after, SeqRange { range: Range { start, end }, meta });
123        } else {
124            // Otherwise, we put the new segment at the left edge of the merge
125            // window and remove all other existing segments.
126            let left_edge = first_after - merge_left;
127            let right_edge = first_after + merge_right;
128            blocks[left_edge] = SeqRange { range: Range { start, end }, meta };
129            for i in right_edge..blocks.len() {
130                blocks[i - merge_left - merge_right + 1] = blocks[i].clone();
131            }
132            blocks.truncate(blocks.len() - merge_left - merge_right + 1);
133        }
134    }
135
136    pub(crate) fn iter(&self) -> impl Iterator<Item = &SeqRange<M>> + '_ {
137        self.blocks.iter()
138    }
139}
140
141impl<M: Clone> FromIterator<SeqRange<M>> for SeqRanges<M> {
142    fn from_iter<T: IntoIterator<Item = SeqRange<M>>>(iter: T) -> Self {
143        let mut ranges = SeqRanges::default();
144        for SeqRange { range, meta } in iter {
145            ranges.insert(range, meta)
146        }
147        ranges
148    }
149}
150
151/// A range kept in [`SeqRanges`].
152#[derive(Debug, Clone)]
153#[cfg_attr(test, derive(PartialEq, Eq))]
154pub(crate) struct SeqRange<M> {
155    pub(crate) range: Range<SeqNum>,
156    pub(crate) meta: M,
157}
158
159#[cfg(test)]
160mod test {
161    use super::*;
162
163    use alloc::format;
164
165    use netstack3_base::WindowSize;
166    use proptest::strategy::{Just, Strategy};
167    use proptest::test_runner::Config;
168    use proptest::{prop_assert, prop_assert_eq, proptest};
169    use proptest_support::failed_seeds_no_std;
170
171    proptest! {
172        #![proptest_config(Config {
173            // Add all failed seeds here.
174            failure_persistence: failed_seeds_no_std!(
175                "cc f621ca7d3a2b108e0dc41f7169ad028f4329b79e90e73d5f68042519a9f63999",
176                "cc c449aebed201b4ec4f137f3c224f20325f4cfee0b7fd596d9285176b6d811aa9"
177            ),
178            ..Config::default()
179        })]
180
181        #[test]
182        fn seq_ranges_insert(insertions in proptest::collection::vec(insertions(), 200)) {
183            let mut seq_ranges = SeqRanges::<()>::default();
184            let mut num_insertions_performed = 0;
185            let mut min_seq = SeqNum::new(WindowSize::MAX.into());
186            let mut max_seq = SeqNum::new(0);
187            for Range { start, end } in insertions {
188                if min_seq.after(start) {
189                    min_seq = start;
190                }
191                if max_seq.before(end) {
192                    max_seq = end;
193                }
194                // assert that it's impossible to have more entries than the
195                // number of insertions performed.
196                prop_assert!(seq_ranges.blocks.len() <= num_insertions_performed);
197                seq_ranges.insert(start..end, ());
198                num_insertions_performed += 1;
199
200                // assert that the ranges are sorted and don't overlap with
201                // each other.
202                for i in 1..seq_ranges.blocks.len() {
203                    prop_assert!(
204                        seq_ranges.blocks[i-1].range.end.before(seq_ranges.blocks[i].range.start)
205                    );
206                }
207            }
208            prop_assert_eq!(seq_ranges.blocks.front().unwrap().range.start, min_seq);
209            prop_assert_eq!(seq_ranges.blocks.back().unwrap().range.end, max_seq);
210        }
211    }
212
213    fn insertions() -> impl Strategy<Value = Range<SeqNum>> {
214        (0..u32::from(WindowSize::MAX)).prop_flat_map(|start| {
215            (start + 1..=u32::from(WindowSize::MAX)).prop_flat_map(move |end| {
216                Just(Range { start: SeqNum::new(start), end: SeqNum::new(end) })
217            })
218        })
219    }
220}