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 summing each entry's length.
25///
26/// # Preconditions
27///
28/// The entries must be **disjoint** (non-overlapping), as produced by
29/// [`Merge::merge_progress`](crate::Merge::merge_progress). Overlapping entries are
30/// silently counted twice, inflating the total; and the naive `u64` sum is unchecked,
31/// so a combined length beyond `u64::MAX` panics in debug builds and wraps in release.
32impl Total for Vec<ProgressEntry> {
33    fn total(&self) -> u64 {
34        self.iter().map(Total::total).sum()
35    }
36}
37
38#[cfg(test)]
39mod tests {
40    use super::*;
41
42    #[test]
43    fn progress_entry_total() {
44        assert_eq!((0..0).total(), 0);
45        assert_eq!((5..10).total(), 5);
46        assert_eq!((3..3).total(), 0);
47        // A reversed range must not underflow: saturating_sub yields 0.
48        let reversed = core::ops::Range { start: 10, end: 5 };
49        assert_eq!(reversed.total(), 0);
50    }
51
52    #[test]
53    fn vec_progress_total() {
54        #![allow(clippy::single_range_in_vec_init)]
55        let v: Vec<ProgressEntry> = vec![1..5, 8..10, 12..15];
56        assert_eq!(v.total(), 9);
57        let empty: Vec<ProgressEntry> = vec![];
58        assert_eq!(empty.total(), 0);
59    }
60
61    #[test]
62    fn progress_entry_total_reversed_ranges_are_safe() {
63        // A reversed range must saturate to 0 rather than underflow.
64        assert_eq!((core::ops::Range { start: 10, end: 5 }).total(), 0);
65        assert_eq!(
66            (core::ops::Range {
67                start: u64::MAX,
68                end: 0
69            })
70            .total(),
71            0
72        );
73        assert_eq!(
74            (core::ops::Range {
75                start: 100,
76                end: 99
77            })
78            .total(),
79            0
80        );
81        assert_eq!((core::ops::Range { start: 1, end: 0 }).total(), 0);
82    }
83
84    #[test]
85    fn progress_entry_total_extreme_and_degenerate() {
86        // Extreme and empty ranges have well-defined lengths.
87        assert_eq!((0..u64::MAX).total(), u64::MAX);
88        assert_eq!((u64::MAX..u64::MAX).total(), 0);
89        assert_eq!((42..42).total(), 0);
90        assert_eq!((0..0).total(), 0);
91    }
92
93    #[test]
94    fn vec_progress_total_counts_overlap_twice() {
95        #![allow(clippy::single_range_in_vec_init)]
96        // `total` sums lengths naively, so overlapping entries are counted twice.
97        // This pins the documented behaviour: callers must pass disjoint entries.
98        let v: Vec<ProgressEntry> = vec![0..5, 3..10];
99        // (5 - 0) + (10 - 3) = 12, whereas the union 0..10 is only 10 bytes.
100        assert_eq!(v.total(), 12);
101    }
102
103    #[test]
104    #[cfg_attr(debug_assertions, should_panic)]
105    #[allow(clippy::should_panic_without_expect)]
106    fn vec_progress_total_overflow_is_unchecked() {
107        // `sum()` on u64 is unchecked: it panics in debug and wraps in release.
108        // Only reachable with pathological input whose total exceeds 16 EiB.
109        let v: Vec<ProgressEntry> = vec![0..u64::MAX, 0..1];
110        let _ = v.total(); // u64::MAX + 1 -> debug panic
111    }
112}