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)]
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: {0}")]
117 InvalidSyntax(String),
118 #[error("range header contained an invalid number: {0}")]
120 InvalidNumber(String),
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 "empty range suffix".to_owned(),
148 ));
149 }
150 if raw_suffix.contains(',') {
151 return Err(HttpRangeParseError::InvalidSyntax(
152 "multi-range not supported".to_owned(),
153 ));
154 }
155
156 let mut parts = raw_suffix.splitn(2, '-');
157 let Some(raw_start) = parts.next() else {
158 return Err(HttpRangeParseError::InvalidSyntax(
159 "missing range start".to_owned(),
160 ));
161 };
162 let Some(raw_end) = parts.next() else {
163 return Err(HttpRangeParseError::InvalidSyntax(
164 "missing range end".to_owned(),
165 ));
166 };
167 if raw_start.is_empty() {
168 let suffix_len = raw_end
170 .parse::<u64>()
171 .map_err(|e| HttpRangeParseError::InvalidNumber(e.to_string()))?;
172 if suffix_len == 0 {
173 return Err(HttpRangeParseError::InvalidSyntax(
174 "suffix length must be non-zero".to_owned(),
175 ));
176 }
177 let start = resource_length.saturating_sub(suffix_len);
178 let end = resource_length.saturating_sub(1);
179 return ByteRange::new(start, end)
180 .map_err(|err| HttpRangeParseError::InvalidSyntax(err.to_string()));
181 }
182
183 let start = raw_start
184 .parse::<u64>()
185 .map_err(|e| HttpRangeParseError::InvalidNumber(e.to_string()))?;
186 if start >= resource_length {
187 return Err(HttpRangeParseError::Unsatisfiable);
188 }
189
190 let last_byte = resource_length
191 .checked_sub(1)
192 .ok_or(HttpRangeParseError::Unsatisfiable)?;
193 let parsed_end = if raw_end.is_empty() {
194 last_byte
195 } else {
196 raw_end
197 .parse::<u64>()
198 .map_err(|e| HttpRangeParseError::InvalidNumber(e.to_string()))?
199 };
200 let end_inclusive = parsed_end.min(last_byte);
201
202 ByteRange::new(start, end_inclusive)
203 .map_err(|err| HttpRangeParseError::InvalidSyntax(err.to_string()))
204}
205
206#[cfg(test)]
207mod tests {
208 use super::{ByteRange, ChunkRange, HttpRangeParseError, RangeError, parse_http_byte_range};
209
210 #[test]
211 fn byte_range_is_inclusive() {
212 let range = ByteRange::new(10, 20);
213
214 assert!(range.is_ok());
215 if let Ok(value) = range {
216 assert_eq!(value.start(), 10);
217 assert_eq!(value.end_inclusive(), 20);
218 assert_eq!(value.len(), Some(11));
219 assert!(!value.is_empty());
220 }
221 }
222
223 #[test]
224 fn byte_range_rejects_inverted_input() {
225 let range = ByteRange::new(20, 10);
226
227 assert_eq!(range, Err(RangeError::Inverted));
228 }
229
230 #[test]
231 fn byte_range_reports_unrepresentable_full_u64_length() {
232 let range = ByteRange::new(0, u64::MAX);
233
234 assert!(range.is_ok());
235 if let Ok(value) = range {
236 assert_eq!(value.len(), None);
237 }
238 }
239
240 #[test]
241 fn chunk_range_rejects_empty_ranges() {
242 let range = ChunkRange::new(4, 4);
243
244 assert_eq!(range, Err(RangeError::Empty));
245 }
246
247 #[test]
248 fn chunk_range_rejects_inverted_ranges() {
249 let range = ChunkRange::new(5, 4);
250
251 assert_eq!(range, Err(RangeError::Inverted));
252 }
253
254 #[test]
255 fn chunk_range_is_end_exclusive() {
256 let range = ChunkRange::new(4, 9);
257
258 assert!(range.is_ok());
259 if let Ok(value) = range {
260 assert_eq!(value.start(), 4);
261 assert_eq!(value.end_exclusive(), 9);
262 }
263 }
264
265 #[test]
266 fn http_byte_range_parses_inclusive_range() {
267 let parsed = parse_http_byte_range("bytes=10-20", 100);
268 let expected = ByteRange::new(10, 20);
269
270 assert!(expected.is_ok());
271 assert_eq!(parsed.unwrap(), expected.unwrap());
272 }
273
274 #[test]
275 fn http_byte_range_clamps_open_or_oversized_end_to_resource() {
276 let open_ended = parse_http_byte_range("bytes=10-", 25);
277 let oversized = parse_http_byte_range("bytes=10-999", 25);
278 let expected = ByteRange::new(10, 24);
279
280 assert!(expected.is_ok());
281 let expected = expected.unwrap();
282 assert_eq!(open_ended.unwrap(), expected);
283 assert_eq!(oversized.unwrap(), expected);
284 }
285
286 #[test]
287 fn http_byte_range_rejects_invalid_syntax() {
288 assert!(matches!(
289 parse_http_byte_range("items=0-1", 10),
290 Err(HttpRangeParseError::MissingBytesUnit)
291 ));
292 assert!(matches!(
293 parse_http_byte_range("bytes=1-2,4-5", 10),
294 Err(HttpRangeParseError::InvalidSyntax(_))
295 ));
296 assert!(matches!(
297 parse_http_byte_range("bytes=2-1", 10),
298 Err(HttpRangeParseError::InvalidSyntax(_))
299 ));
300 assert!(matches!(
301 parse_http_byte_range("bytes= 1-2", 10),
302 Err(HttpRangeParseError::InvalidNumber(_))
303 ));
304 assert!(matches!(
305 parse_http_byte_range("bytes=1 -2", 10),
306 Err(HttpRangeParseError::InvalidNumber(_))
307 ));
308 }
309
310 #[test]
311 fn http_byte_range_accepts_suffix_and_rejects_multi_range_forms() {
312 let suffix = parse_http_byte_range("bytes=-1", 10);
313 let expected = ByteRange::new(9, 9);
314 assert!(expected.is_ok());
315 assert_eq!(suffix.unwrap(), expected.unwrap());
316
317 let suffix_large = parse_http_byte_range("bytes=-100", 50);
318 let expected_large = ByteRange::new(0, 49);
319 assert!(expected_large.is_ok());
320 assert_eq!(suffix_large.unwrap(), expected_large.unwrap());
321
322 assert!(matches!(
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!(matches!(
331 parse_http_byte_range("bytes=10-20", 10),
332 Err(HttpRangeParseError::Unsatisfiable)
333 ));
334 assert!(matches!(
335 parse_http_byte_range("bytes=0-0", 0),
336 Err(HttpRangeParseError::Unsatisfiable)
337 ));
338 }
339
340 #[test]
341 fn http_byte_range_rejects_empty_raw_suffix() {
342 assert!(matches!(
343 parse_http_byte_range("bytes=", 10),
344 Err(HttpRangeParseError::InvalidSyntax(_))
345 ));
346 }
347
348 #[test]
349 fn http_byte_range_rejects_empty_start_no_end() {
350 assert!(matches!(
352 parse_http_byte_range("bytes=-", 10),
353 Err(HttpRangeParseError::InvalidNumber(_))
354 ));
355 }
356
357 #[test]
358 fn http_byte_range_rejects_suffix_zero() {
359 assert!(matches!(
360 parse_http_byte_range("bytes=-0", 10),
361 Err(HttpRangeParseError::InvalidSyntax(_))
362 ));
363 }
364
365 #[test]
366 fn http_byte_range_suffix_larger_than_resource_clamps_to_start() {
367 let result = parse_http_byte_range("bytes=-999", 100);
368 let expected = ByteRange::new(0, 99);
369 assert!(expected.is_ok());
370 assert_eq!(result.unwrap(), expected.unwrap());
371 }
372
373 #[test]
376 fn range_error_display_inverted() {
377 let msg = RangeError::Inverted.to_string();
378 assert!(!msg.is_empty());
379 assert!(
380 msg.contains("smaller"),
381 "expected 'smaller' in display, got: {msg}"
382 );
383 }
384
385 #[test]
386 fn range_error_display_empty() {
387 let msg = RangeError::Empty.to_string();
388 assert!(!msg.is_empty());
389 assert!(
390 msg.contains("at least one chunk"),
391 "expected 'at least one chunk' in display, got: {msg}"
392 );
393 }
394
395 #[test]
396 fn http_range_parse_error_display_all_variants() {
397 let cases: &[(HttpRangeParseError, &str)] = &[
398 (HttpRangeParseError::MissingBytesUnit, "syntax"),
399 (
400 HttpRangeParseError::InvalidSyntax("test".to_owned()),
401 "syntax",
402 ),
403 (
404 HttpRangeParseError::InvalidNumber("test".to_owned()),
405 "invalid number",
406 ),
407 (HttpRangeParseError::Unsatisfiable, "satisfiable"),
408 ];
409 for (error, substring) in cases {
410 let msg = error.to_string();
411 assert!(!msg.is_empty(), "empty display for {error:?}");
412 assert!(
413 msg.contains(substring),
414 "expected '{substring}' in '{msg}' from {error:?}"
415 );
416 }
417 }
418
419 #[test]
422 fn byte_range_single_byte() {
423 let range = ByteRange::new(5, 5).unwrap();
424 assert_eq!(range.start(), 5);
425 assert_eq!(range.end_inclusive(), 5);
426 assert_eq!(range.len(), Some(1));
427 assert!(!range.is_empty());
428 }
429
430 #[test]
431 fn byte_range_zero_length_range() {
432 let range = ByteRange::new(0, 0).unwrap();
434 assert_eq!(range.len(), Some(1));
435 }
436
437 #[test]
438 fn byte_range_u64_max_start() {
439 let range = ByteRange::new(u64::MAX, u64::MAX);
440 assert!(range.is_ok());
441 let range = range.unwrap();
442 assert_eq!(range.len(), Some(1));
443 }
444
445 #[test]
446 fn byte_range_clone_copy_consistency() {
447 let range = ByteRange::new(10, 20).unwrap();
448 let cloned = range;
449 assert_eq!(range, cloned);
450 assert_eq!(range.start(), cloned.start());
451 assert_eq!(range.end_inclusive(), cloned.end_inclusive());
452 }
453
454 #[test]
457 fn chunk_range_single_chunk() {
458 let range = ChunkRange::new(3, 4).unwrap();
459 assert_eq!(range.start(), 3);
460 assert_eq!(range.end_exclusive(), 4);
461 }
462
463 #[test]
464 fn chunk_range_u32_bounds() {
465 let range = ChunkRange::new(0, u32::MAX).unwrap();
466 assert_eq!(range.start(), 0);
467 assert_eq!(range.end_exclusive(), u32::MAX);
468 }
469
470 #[test]
471 fn chunk_range_clone_copy_consistency() {
472 let range = ChunkRange::new(1, 5).unwrap();
473 let cloned = range;
474 assert_eq!(range, cloned);
475 assert_eq!(range.start(), cloned.start());
476 }
477
478 #[test]
481 fn http_byte_range_exact_resource_length_start_zero() {
482 let result = parse_http_byte_range("bytes=0-0", 1).unwrap();
484 assert_eq!(result.start(), 0);
485 assert_eq!(result.end_inclusive(), 0);
486 }
487
488 #[test]
489 fn http_byte_range_rejects_start_equals_resource_length() {
490 let result = parse_http_byte_range("bytes=5-10", 5);
492 assert!(matches!(result, Err(HttpRangeParseError::Unsatisfiable)));
493 }
494
495 #[test]
496 fn http_byte_range_zero_resource_length() {
497 let result = parse_http_byte_range("bytes=0-0", 0);
499 assert!(matches!(result, Err(HttpRangeParseError::Unsatisfiable)));
500 }
501
502 #[test]
503 fn http_byte_range_suffix_zero_resource_length() {
504 let result = parse_http_byte_range("bytes=-0", 0);
506 assert!(matches!(result, Err(HttpRangeParseError::InvalidSyntax(_))));
507 }
508
509 #[test]
510 fn http_byte_range_huge_numbers() {
511 let result = parse_http_byte_range("bytes=99999999999999999999-100000000000000000000", 100);
512 assert!(matches!(result, Err(HttpRangeParseError::InvalidNumber(_))));
513 }
514
515 #[test]
518 fn range_error_clone_copy_partial_eq() {
519 let a = RangeError::Inverted;
520 let b = RangeError::Empty;
521 let a2 = a;
522 assert_eq!(a, a2);
523 assert_ne!(a, b);
524 }
525
526 #[test]
527 fn http_range_parse_error_debug_non_empty() {
528 let err = HttpRangeParseError::InvalidSyntax("test".to_owned());
529 let debug = format!("{err:?}");
530 assert!(!debug.is_empty());
531 }
532}