Skip to main content

libfw_core/
range.rs

1//! Byte-range representation shared by server and client.
2
3use serde::{Deserialize, Serialize};
4
5/// A byte range of a resource: `[start, end)`.
6///
7/// The transport layer uses *half-open* ranges internally; HTTP
8/// `Content-Range` values are emitted as `start-end` with inclusive end
9/// (see server helpers).
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11pub struct RangeSpec {
12    /// First byte (inclusive).
13    pub start: u64,
14    /// One past the last byte (exclusive).
15    pub end: u64,
16}
17
18impl RangeSpec {
19    /// Creates a range covering `len` bytes starting at `start`.
20    pub fn new(start: u64, len: u64) -> Self {
21        RangeSpec {
22            start,
23            end: start.saturating_add(len),
24        }
25    }
26
27    /// A range covering the whole resource.
28    pub fn full(len: u64) -> Self {
29        RangeSpec { start: 0, end: len }
30    }
31
32    /// Number of bytes in the range.
33    pub fn len(&self) -> u64 {
34        self.end.saturating_sub(self.start)
35    }
36
37    /// True when the range is empty.
38    pub fn is_empty(&self) -> bool {
39        self.len() == 0
40    }
41
42    /// Clamp this range to `file_size` bytes.
43    ///
44    /// Returns `None` when the range lies completely beyond the file
45    /// (HTTP 416 territory) or when `file_size` is zero.
46    pub fn clamp(&self, file_size: u64) -> Option<RangeSpec> {
47        if self.start >= file_size {
48            return None;
49        }
50        Some(RangeSpec {
51            start: self.start,
52            end: self.end.min(file_size),
53        })
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[test]
62    fn basic_range() {
63        let r = RangeSpec::new(100, 50);
64        assert_eq!((r.start, r.end, r.len()), (100, 150, 50));
65    }
66
67    #[test]
68    fn clamp_limits_end() {
69        let r = RangeSpec::new(0, 1000).clamp(100).unwrap();
70        assert_eq!((r.start, r.end), (0, 100));
71    }
72
73    #[test]
74    fn clamp_rejects_past_end() {
75        assert!(RangeSpec::new(100, 10).clamp(99).is_none());
76    }
77
78    #[test]
79    fn clamp_rejects_zero_file() {
80        assert!(RangeSpec::full(0).clamp(0).is_none());
81    }
82}