1use serde::{Deserialize, Serialize};
2use thiserror::Error;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
6pub struct ByteRange {
7 start: u64,
8 end_inclusive: u64,
9}
10
11impl ByteRange {
12 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 #[must_use]
30 pub const fn start(&self) -> u64 {
31 self.start
32 }
33
34 #[must_use]
36 pub const fn end_inclusive(&self) -> u64 {
37 self.end_inclusive
38 }
39
40 #[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 #[must_use]
51 pub const fn is_empty(&self) -> bool {
52 false
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
58pub struct ChunkRange {
59 start: u32,
60 end_exclusive: u32,
61}
62
63impl ChunkRange {
64 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 #[must_use]
87 pub const fn start(self) -> u32 {
88 self.start
89 }
90
91 #[must_use]
93 pub const fn end_exclusive(self) -> u32 {
94 self.end_exclusive
95 }
96}
97
98#[derive(Debug, Clone, Copy, Error, PartialEq, Eq)]
100pub enum RangeError {
101 #[error("range end must not be smaller than range start")]
103 Inverted,
104 #[error("chunk range must contain at least one chunk")]
106 Empty,
107}
108
109#[derive(Debug, Clone, Error, PartialEq, Eq)]
111pub enum HttpRangeParseError {
112 #[error("range header must use bytes=<start>-<end> syntax")]
114 MissingBytesUnit,
115 #[error("range header must use bytes=<start>-<end> syntax")]
117 InvalidSyntax,
118 #[error("range header contained an invalid number")]
120 InvalidNumber,
121 #[error("requested range is not satisfiable")]
123 Unsatisfiable,
124}
125
126pub 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}