Skip to main content

fast_pull/base/
progress.rs

1//! Progress range type and total-size computation.
2
3use core::ops::Range;
4
5/// A byte-range representing downloaded or to-be-downloaded progress.
6///
7/// Stored as a `Range<u64>` from `start` (inclusive) to `end` (exclusive).
8pub type ProgressEntry = Range<u64>;
9
10/// Trait for computing the total size from one or more [`ProgressEntry`] values.
11pub trait Total {
12    /// Total number of bytes represented by this progress value.
13    fn total(&self) -> u64;
14}
15
16impl Total for ProgressEntry {
17    #[allow(clippy::inline_always)]
18    #[inline(always)]
19    fn total(&self) -> u64 {
20        self.end.saturating_sub(self.start)
21    }
22}
23
24/// Total number of bytes across all entries, computed by saturating the sum of
25/// each entry's length at [`u64::MAX`].
26///
27/// # Preconditions
28///
29/// The entries must be **disjoint** (non-overlapping), as produced by
30/// [`Merge::merge_progress`](crate::Merge::merge_progress). Overlapping entries are
31/// silently counted twice, inflating the total. If the combined length exceeds
32/// [`u64::MAX`], the result is [`u64::MAX`].
33impl Total for [ProgressEntry] {
34    fn total(&self) -> u64 {
35        self.iter()
36            .fold(0, |total, entry| total.saturating_add(entry.total()))
37    }
38}
39
40impl Total for Vec<ProgressEntry> {
41    fn total(&self) -> u64 {
42        self.as_slice().total()
43    }
44}
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49
50    #[test]
51    fn progress_entry_total() {
52        assert_eq!((0..0).total(), 0);
53        assert_eq!((5..10).total(), 5);
54        assert_eq!((3..3).total(), 0);
55        // A reversed range must not underflow: saturating_sub yields 0.
56        let reversed = core::ops::Range { start: 10, end: 5 };
57        assert_eq!(reversed.total(), 0);
58    }
59
60    #[test]
61    fn vec_progress_total() {
62        #![allow(clippy::single_range_in_vec_init)]
63        let v: Vec<ProgressEntry> = vec![1..5, 8..10, 12..15];
64        assert_eq!(v.total(), 9);
65        let empty: Vec<ProgressEntry> = vec![];
66        assert_eq!(empty.total(), 0);
67    }
68
69    #[test]
70    fn slice_progress_total_matches_vec() {
71        #![allow(clippy::single_range_in_vec_init)]
72        let v: Vec<ProgressEntry> = vec![1..5, 8..10, 12..15];
73        assert_eq!(v.as_slice().total(), v.total());
74        let empty: &[ProgressEntry] = &[];
75        assert_eq!(empty.total(), 0);
76    }
77
78    #[test]
79    fn progress_entry_total_reversed_ranges_are_safe() {
80        // A reversed range must saturate to 0 rather than underflow.
81        assert_eq!((core::ops::Range { start: 10, end: 5 }).total(), 0);
82        assert_eq!(
83            (core::ops::Range {
84                start: u64::MAX,
85                end: 0
86            })
87            .total(),
88            0
89        );
90        assert_eq!(
91            (core::ops::Range {
92                start: 100,
93                end: 99
94            })
95            .total(),
96            0
97        );
98        assert_eq!((core::ops::Range { start: 1, end: 0 }).total(), 0);
99    }
100
101    #[test]
102    fn progress_entry_total_extreme_and_degenerate() {
103        // Extreme and empty ranges have well-defined lengths.
104        assert_eq!((0..u64::MAX).total(), u64::MAX);
105        assert_eq!((u64::MAX..u64::MAX).total(), 0);
106        assert_eq!((42..42).total(), 0);
107        assert_eq!((0..0).total(), 0);
108    }
109
110    #[test]
111    fn vec_progress_total_counts_overlap_twice() {
112        #![allow(clippy::single_range_in_vec_init)]
113        // `total` sums lengths naively, so overlapping entries are counted twice.
114        // This pins the documented behaviour: callers must pass disjoint entries.
115        let v: Vec<ProgressEntry> = vec![0..5, 3..10];
116        // (5 - 0) + (10 - 3) = 12, whereas the union 0..10 is only 10 bytes.
117        assert_eq!(v.total(), 12);
118    }
119
120    #[test]
121    fn vec_progress_total_overflow_saturates() {
122        let v: Vec<ProgressEntry> = vec![0..u64::MAX, 0..1];
123        assert_eq!(v.total(), u64::MAX);
124    }
125}