Skip to main content

shardline_protocol/
ranges.rs

1use serde::{Deserialize, Serialize};
2use thiserror::Error;
3
4/// Inclusive byte range.
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
6pub struct ByteRange {
7    start: u64,
8    end_inclusive: u64,
9}
10
11impl ByteRange {
12    /// Creates an inclusive byte range.
13    ///
14    /// # Errors
15    ///
16    /// Returns [`RangeError::Inverted`] when `end_inclusive` is smaller than `start`.
17    pub const fn new(start: u64, end_inclusive: u64) -> Result<Self, RangeError> {
18        if end_inclusive < start {
19            return Err(RangeError::Inverted);
20        }
21
22        Ok(Self {
23            start,
24            end_inclusive,
25        })
26    }
27
28    /// Returns the first byte offset in the range.
29    #[must_use]
30    pub const fn start(&self) -> u64 {
31        self.start
32    }
33
34    /// Returns the inclusive final byte offset in the range.
35    #[must_use]
36    pub const fn end_inclusive(&self) -> u64 {
37        self.end_inclusive
38    }
39
40    /// Returns the number of bytes in the range.
41    #[must_use]
42    pub const fn len(&self) -> Option<u64> {
43        match self.end_inclusive.checked_sub(self.start) {
44            Some(offset) => offset.checked_add(1),
45            None => None,
46        }
47    }
48
49    /// Returns false because validated inclusive byte ranges always contain at least one byte.
50    #[must_use]
51    pub const fn is_empty(&self) -> bool {
52        false
53    }
54}
55
56/// End-exclusive chunk index range.
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
58pub struct ChunkRange {
59    start: u32,
60    end_exclusive: u32,
61}
62
63impl ChunkRange {
64    /// Creates an end-exclusive chunk index range.
65    ///
66    /// # Errors
67    ///
68    /// Returns [`RangeError::Empty`] when `end_exclusive` is equal to `start`.
69    /// Returns [`RangeError::Inverted`] when `end_exclusive` is smaller than `start`.
70    pub const fn new(start: u32, end_exclusive: u32) -> Result<Self, RangeError> {
71        if end_exclusive < start {
72            return Err(RangeError::Inverted);
73        }
74
75        if end_exclusive == start {
76            return Err(RangeError::Empty);
77        }
78
79        Ok(Self {
80            start,
81            end_exclusive,
82        })
83    }
84
85    /// Returns the first chunk index in the range.
86    #[must_use]
87    pub const fn start(self) -> u32 {
88        self.start
89    }
90
91    /// Returns the end-exclusive chunk index.
92    #[must_use]
93    pub const fn end_exclusive(self) -> u32 {
94        self.end_exclusive
95    }
96}
97
98/// Range construction failure.
99#[derive(Debug, Clone, Copy, Error, PartialEq, Eq)]
100pub enum RangeError {
101    /// The range end was smaller than the range start.
102    #[error("range end must not be smaller than range start")]
103    Inverted,
104    /// The range contained no chunks.
105    #[error("chunk range must contain at least one chunk")]
106    Empty,
107}
108
109/// Reconstruction request range parse failure.
110#[derive(Debug, Clone, Error, PartialEq, Eq)]
111pub enum HttpRangeParseError {
112    /// The header did not start with the expected unit token.
113    #[error("range header must use bytes=<start>-<end> syntax")]
114    MissingBytesUnit,
115    /// The header contained unsupported or malformed syntax.
116    #[error("range header must use bytes=<start>-<end> syntax")]
117    InvalidSyntax,
118    /// The numeric range could not be parsed.
119    #[error("range header contained an invalid number")]
120    InvalidNumber,
121    /// The requested start exceeded the represented resource length.
122    #[error("requested range is not satisfiable")]
123    Unsatisfiable,
124}
125
126/// Parses a reconstruction `Range` header into an inclusive byte range.
127///
128/// The Xet reconstruction API uses `bytes=<start>-<end>` syntax with an inclusive end.
129/// When the requested end exceeds the resource length, the returned range is clamped to
130/// the last byte of the resource.
131///
132/// # Errors
133///
134/// Returns [`HttpRangeParseError::InvalidSyntax`] when the header uses unsupported
135/// syntax, [`HttpRangeParseError::InvalidNumber`] when parsing fails, and
136/// [`HttpRangeParseError::Unsatisfiable`] when the requested start exceeds the last byte
137/// of the resource.
138pub fn parse_http_byte_range(
139    value: &str,
140    resource_length: u64,
141) -> Result<ByteRange, HttpRangeParseError> {
142    let Some(raw_suffix) = value.strip_prefix("bytes=") else {
143        return Err(HttpRangeParseError::MissingBytesUnit);
144    };
145    if raw_suffix.is_empty() {
146        return Err(HttpRangeParseError::InvalidSyntax);
147    }
148    if raw_suffix.contains(',') {
149        return Err(HttpRangeParseError::InvalidSyntax);
150    }
151
152    let mut parts = raw_suffix.splitn(2, '-');
153    let Some(raw_start) = parts.next() else {
154        return Err(HttpRangeParseError::InvalidSyntax);
155    };
156    let Some(raw_end) = parts.next() else {
157        return Err(HttpRangeParseError::InvalidSyntax);
158    };
159    if raw_start.is_empty() {
160        return Err(HttpRangeParseError::InvalidSyntax);
161    }
162
163    let start = raw_start
164        .parse::<u64>()
165        .map_err(|_error| HttpRangeParseError::InvalidNumber)?;
166    if start >= resource_length {
167        return Err(HttpRangeParseError::Unsatisfiable);
168    }
169
170    let last_byte = resource_length
171        .checked_sub(1)
172        .ok_or(HttpRangeParseError::Unsatisfiable)?;
173    let parsed_end = if raw_end.is_empty() {
174        last_byte
175    } else {
176        raw_end
177            .parse::<u64>()
178            .map_err(|_error| HttpRangeParseError::InvalidNumber)?
179    };
180    let end_inclusive = parsed_end.min(last_byte);
181
182    ByteRange::new(start, end_inclusive).map_err(|_error| HttpRangeParseError::InvalidSyntax)
183}
184
185#[cfg(test)]
186mod tests {
187    use super::{ByteRange, ChunkRange, HttpRangeParseError, RangeError, parse_http_byte_range};
188
189    #[test]
190    fn byte_range_is_inclusive() {
191        let range = ByteRange::new(10, 20);
192
193        assert!(range.is_ok());
194        if let Ok(value) = range {
195            assert_eq!(value.start(), 10);
196            assert_eq!(value.end_inclusive(), 20);
197            assert_eq!(value.len(), Some(11));
198            assert!(!value.is_empty());
199        }
200    }
201
202    #[test]
203    fn byte_range_rejects_inverted_input() {
204        let range = ByteRange::new(20, 10);
205
206        assert_eq!(range, Err(RangeError::Inverted));
207    }
208
209    #[test]
210    fn byte_range_reports_unrepresentable_full_u64_length() {
211        let range = ByteRange::new(0, u64::MAX);
212
213        assert!(range.is_ok());
214        if let Ok(value) = range {
215            assert_eq!(value.len(), None);
216        }
217    }
218
219    #[test]
220    fn chunk_range_rejects_empty_ranges() {
221        let range = ChunkRange::new(4, 4);
222
223        assert_eq!(range, Err(RangeError::Empty));
224    }
225
226    #[test]
227    fn chunk_range_rejects_inverted_ranges() {
228        let range = ChunkRange::new(5, 4);
229
230        assert_eq!(range, Err(RangeError::Inverted));
231    }
232
233    #[test]
234    fn chunk_range_is_end_exclusive() {
235        let range = ChunkRange::new(4, 9);
236
237        assert!(range.is_ok());
238        if let Ok(value) = range {
239            assert_eq!(value.start(), 4);
240            assert_eq!(value.end_exclusive(), 9);
241        }
242    }
243
244    #[test]
245    fn http_byte_range_parses_inclusive_range() {
246        let parsed = parse_http_byte_range("bytes=10-20", 100);
247        let expected = ByteRange::new(10, 20);
248
249        assert!(expected.is_ok());
250        assert_eq!(
251            parsed,
252            expected.map_err(|_error| HttpRangeParseError::InvalidSyntax)
253        );
254    }
255
256    #[test]
257    fn http_byte_range_clamps_open_or_oversized_end_to_resource() {
258        let open_ended = parse_http_byte_range("bytes=10-", 25);
259        let oversized = parse_http_byte_range("bytes=10-999", 25);
260        let expected = ByteRange::new(10, 24);
261
262        assert!(expected.is_ok());
263        assert_eq!(
264            open_ended,
265            expected.map_err(|_error| HttpRangeParseError::InvalidSyntax)
266        );
267        assert_eq!(
268            oversized,
269            expected.map_err(|_error| HttpRangeParseError::InvalidSyntax)
270        );
271    }
272
273    #[test]
274    fn http_byte_range_accepts_single_byte_and_final_byte_vectors() {
275        let first_byte = parse_http_byte_range("bytes=0-0", 10);
276        let final_byte = parse_http_byte_range("bytes=9-9", 10);
277
278        assert_eq!(
279            first_byte,
280            ByteRange::new(0, 0).map_err(|_error| HttpRangeParseError::InvalidSyntax)
281        );
282        assert_eq!(
283            final_byte,
284            ByteRange::new(9, 9).map_err(|_error| HttpRangeParseError::InvalidSyntax)
285        );
286    }
287
288    #[test]
289    fn http_byte_range_rejects_invalid_syntax() {
290        assert_eq!(
291            parse_http_byte_range("items=0-1", 10),
292            Err(HttpRangeParseError::MissingBytesUnit)
293        );
294        assert_eq!(
295            parse_http_byte_range("bytes=-10", 10),
296            Err(HttpRangeParseError::InvalidSyntax)
297        );
298        assert_eq!(
299            parse_http_byte_range("bytes=1-2,4-5", 10),
300            Err(HttpRangeParseError::InvalidSyntax)
301        );
302        assert_eq!(
303            parse_http_byte_range("bytes=2-1", 10),
304            Err(HttpRangeParseError::InvalidSyntax)
305        );
306        assert_eq!(
307            parse_http_byte_range("bytes= 1-2", 10),
308            Err(HttpRangeParseError::InvalidNumber)
309        );
310        assert_eq!(
311            parse_http_byte_range("bytes=1 -2", 10),
312            Err(HttpRangeParseError::InvalidNumber)
313        );
314    }
315
316    #[test]
317    fn http_byte_range_rejects_suffix_and_multi_range_forms() {
318        assert_eq!(
319            parse_http_byte_range("bytes=-1", 10),
320            Err(HttpRangeParseError::InvalidSyntax)
321        );
322        assert_eq!(
323            parse_http_byte_range("bytes=0-0,1-1", 10),
324            Err(HttpRangeParseError::InvalidSyntax)
325        );
326    }
327
328    #[test]
329    fn http_byte_range_rejects_unsatisfiable_start() {
330        assert_eq!(
331            parse_http_byte_range("bytes=10-20", 10),
332            Err(HttpRangeParseError::Unsatisfiable)
333        );
334        assert_eq!(
335            parse_http_byte_range("bytes=0-0", 0),
336            Err(HttpRangeParseError::Unsatisfiable)
337        );
338    }
339}