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 Vec<ProgressEntry> {
34    fn total(&self) -> u64 {
35        self.iter()
36            .fold(0, |total, entry| total.saturating_add(entry.total()))
37    }
38}
39
40#[cfg(test)]
41mod tests {
42    use super::*;
43
44    #[test]
45    fn progress_entry_total() {
46        assert_eq!((0..0).total(), 0);
47        assert_eq!((5..10).total(), 5);
48        assert_eq!((3..3).total(), 0);
49        // A reversed range must not underflow: saturating_sub yields 0.
50        let reversed = core::ops::Range { start: 10, end: 5 };
51        assert_eq!(reversed.total(), 0);
52    }
53
54    #[test]
55    fn vec_progress_total() {
56        #![allow(clippy::single_range_in_vec_init)]
57        let v: Vec<ProgressEntry> = vec![1..5, 8..10, 12..15];
58        assert_eq!(v.total(), 9);
59        let empty: Vec<ProgressEntry> = vec![];
60        assert_eq!(empty.total(), 0);
61    }
62
63    #[test]
64    fn progress_entry_total_reversed_ranges_are_safe() {
65        // A reversed range must saturate to 0 rather than underflow.
66        assert_eq!((core::ops::Range { start: 10, end: 5 }).total(), 0);
67        assert_eq!(
68            (core::ops::Range {
69                start: u64::MAX,
70                end: 0
71            })
72            .total(),
73            0
74        );
75        assert_eq!(
76            (core::ops::Range {
77                start: 100,
78                end: 99
79            })
80            .total(),
81            0
82        );
83        assert_eq!((core::ops::Range { start: 1, end: 0 }).total(), 0);
84    }
85
86    #[test]
87    fn progress_entry_total_extreme_and_degenerate() {
88        // Extreme and empty ranges have well-defined lengths.
89        assert_eq!((0..u64::MAX).total(), u64::MAX);
90        assert_eq!((u64::MAX..u64::MAX).total(), 0);
91        assert_eq!((42..42).total(), 0);
92        assert_eq!((0..0).total(), 0);
93    }
94
95    #[test]
96    fn vec_progress_total_counts_overlap_twice() {
97        #![allow(clippy::single_range_in_vec_init)]
98        // `total` sums lengths naively, so overlapping entries are counted twice.
99        // This pins the documented behaviour: callers must pass disjoint entries.
100        let v: Vec<ProgressEntry> = vec![0..5, 3..10];
101        // (5 - 0) + (10 - 3) = 12, whereas the union 0..10 is only 10 bytes.
102        assert_eq!(v.total(), 12);
103    }
104
105    #[test]
106    fn vec_progress_total_overflow_saturates() {
107        let v: Vec<ProgressEntry> = vec![0..u64::MAX, 0..1];
108        assert_eq!(v.total(), u64::MAX);
109    }
110}