http_range_header/
lib.rs

1#![warn(clippy::pedantic)]
2#![allow(clippy::uninlined_format_args)]
3use core::fmt::{Debug, Display, Formatter};
4use core::ops::RangeInclusive;
5
6const UNIT_SEP: &str = "bytes=";
7/// Function that parses the content of a range header.
8///
9/// Follows the [spec here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range)
10///
11/// And [here](https://www.ietf.org/rfc/rfc2616.txt)
12///
13/// Will only accept bytes ranges, will update when [this spec](https://www.iana.org/assignments/http-parameters/http-parameters.xhtml) changes to allow other units.
14///
15/// Parses ranges strictly, as in the examples contained in the above specifications.
16///
17/// Ranges such as `bytes=0-15, 16-20, abc` will be rejected immediately.
18///
19/// It preserves the range ordering, the specification leaves it open to the server to determine whether
20/// ranges that are out of order are correct or not, ie `bytes=20-30, 0-15`
21///
22/// # Example no trailing or leading whitespaces
23/// ```
24/// // Ok
25/// assert!(http_range_header::parse_range_header("bytes=0-15").is_ok());
26/// // Not allowed
27/// assert!(http_range_header::parse_range_header("bytes=0-15 ").is_err());
28/// // Also not allowed
29/// assert!(http_range_header::parse_range_header("bytes= 0-15").is_err());
30/// ```
31///
32/// # Example No leading whitespaces except in the case of separating multipart ranges
33/// ```
34/// // Ok, multipart with a leading whitespace after comma
35/// assert!(http_range_header::parse_range_header("bytes=0-15, 20-30").is_ok());
36/// // Ok multipart without leading whitespace after comma
37/// assert!(http_range_header::parse_range_header("bytes=0-15,20-30").is_ok());
38/// ```
39///
40/// # Example No negative values, no leading zeroes, no plus-sign
41/// ```
42/// // No negatives
43/// assert!(http_range_header::parse_range_header("bytes=-12-15").is_err());
44/// // No leading zeroes
45/// assert!(http_range_header::parse_range_header("bytes=00-15").is_err());
46/// // No plus sign
47/// assert!(http_range_header::parse_range_header("bytes=+0-15").is_err());
48/// ```
49///
50/// Makes two passes and parses ranges strictly. On the first pass, if any range is malformed returns an `Err`.
51///
52/// On the second pass if the ranges doesn't make sense (reversed range, range out of bounds, etc.) returns an `Err`.
53/// # Example with a standard valid range
54///
55/// ```
56/// let input = "bytes=0-15";
57/// let file_size_bytes = 512;
58/// let parsed_ranges = http_range_header::parse_range_header(input);
59///
60/// match parsed_ranges {
61///     Ok(ranges) => {
62///         match ranges.validate(file_size_bytes) {
63///             Ok(valid_ranges) => {
64///                 for range in valid_ranges {
65///                     // do something with ranges
66///                     assert_eq!(0..=15, range)
67///                 }
68///             }
69///             Err(_err) => {
70///                 // Do something when ranges doesn't make sense
71///                 panic!("Weird range!")
72///             }
73///         }
74///     }
75///     Err(_err) => {
76///         // Do something with malformed ranges
77///         panic!("Malformed range!")
78///     }
79/// }
80/// ```
81///
82/// The parser makes two passes, one without a known file-size, ensuring all ranges are syntactically correct.
83/// The returned struct will through its `validate` method accept a file-size and figure out whether or not the
84/// syntactically correct ranges actually makes sense in context
85///
86/// The range `bytes=0-20` on a file with 15 bytes will be accepted in the first pass as the content size is unknown.
87/// On the second pass (`validate`) it will be truncated to `file_size - 1` as per [the spec](https://httpwg.org/specs/rfc9110.html#rfc.section.14.1.2).
88/// # Example range truncates in `validate` because it exceedes
89/// ```
90/// let input = "bytes=0-20";
91/// let file_size_bytes = 15;
92/// let parsed_ranges = http_range_header::parse_range_header(input)
93///     // Is syntactically correct
94///     .unwrap();
95/// let validated = parsed_ranges.validate(file_size_bytes).unwrap();
96/// assert_eq!(vec![0..=14], validated);
97/// ```
98///
99/// Range reversal and overlap is also checked in the second pass, the range `bytes=0-20, 5-10`
100/// will become two syntactically correct ranges, but `validate` will return ann `Err`.
101///
102/// This is an opinionated implementation, [the spec](https://datatracker.ietf.org/doc/html/rfc7233)
103/// allows a server to determine its implementation of overlapping ranges, this api currently does not allow it.
104///
105/// # Example multipart-range fails `validate` because of an overlap
106/// ```
107/// let input = "bytes=0-15, 10-20, 30-50";
108/// let file_size_bytes = 512;
109/// let parsed_ranges = http_range_header::parse_range_header(input)
110///     // Is syntactically correct
111///     .unwrap();
112/// let validated = parsed_ranges.validate(file_size_bytes);
113/// // Some ranges overlap, all valid ranges get truncated to 1 Err
114/// assert!(validated.is_err());
115/// ```
116/// # Errors
117/// Will return an error if the `range_header_value` cannot be strictly parsed into a range
118/// per the http spec.
119pub fn parse_range_header(
120    range_header_value: &str,
121) -> Result<ParsedRanges, RangeUnsatisfiableError> {
122    const COMMA: char = ',';
123    if let Some((prefix, indicated_range)) = range_header_value.split_once(UNIT_SEP) {
124        if indicated_range.starts_with(char::is_whitespace) {
125            return Err(RangeUnsatisfiableError::StartsWithWhitespace);
126        }
127        if !prefix.is_empty() {
128            return Err(RangeUnsatisfiableError::DoesNotStartWithToken);
129        }
130        let mut last_err = None;
131        let ranges = indicated_range
132            .split(COMMA)
133            .filter_map(|range| {
134                if let Some(trimmed) = trim(range) {
135                    match parse_inner(trimmed) {
136                        Ok(parsed) => Some(parsed),
137                        Err(e) => {
138                            last_err = Some(e);
139                            None
140                        }
141                    }
142                } else {
143                    last_err = Some(RangeUnsatisfiableError::IllegalWhitespace);
144                    None
145                }
146            })
147            .collect::<Vec<SyntacticallyCorrectRange>>();
148        if let Some(last_err) = last_err {
149            return Err(last_err);
150        }
151        if ranges.is_empty() {
152            // Some other error should have been caught before we end up here
153            Err(RangeUnsatisfiableError::Empty)
154        } else {
155            Ok(ParsedRanges::new(ranges))
156        }
157    } else {
158        Err(RangeUnsatisfiableError::DoesNotStartWithToken)
159    }
160}
161
162fn trim(s: &str) -> Option<&str> {
163    if s.ends_with(char::is_whitespace) || s.match_indices(char::is_whitespace).count() > 1 {
164        None
165    } else {
166        Some(s.trim())
167    }
168}
169
170#[inline]
171fn parse_inner(range: &str) -> Result<SyntacticallyCorrectRange, RangeUnsatisfiableError> {
172    if let Some((start, end)) = range.split_once('-') {
173        if start.is_empty() {
174            if let Some(end) = strict_parse_u64(end) {
175                if end == 0 {
176                    return Err(RangeUnsatisfiableError::ZeroSuffix);
177                }
178                return Ok(SyntacticallyCorrectRange::new(
179                    StartPosition::FromLast(end),
180                    EndPosition::LastByte,
181                ));
182            }
183            return Err(RangeUnsatisfiableError::BadEndOfRange);
184        }
185        if let Some(start) = strict_parse_u64(start) {
186            if end.is_empty() {
187                return Ok(SyntacticallyCorrectRange::new(
188                    StartPosition::Index(start),
189                    EndPosition::LastByte,
190                ));
191            }
192            if let Some(end) = strict_parse_u64(end) {
193                return Ok(SyntacticallyCorrectRange::new(
194                    StartPosition::Index(start),
195                    EndPosition::Index(end),
196                ));
197            }
198            return Err(RangeUnsatisfiableError::BadEndOfRange);
199        }
200        return Err(RangeUnsatisfiableError::BadStartOfRange);
201    }
202    Err(RangeUnsatisfiableError::UnexpectedNumberOfDashes)
203}
204
205fn strict_parse_u64(s: &str) -> Option<u64> {
206    if !s.starts_with('+') && (s.len() == 1 || !s.starts_with('0')) {
207        return s.parse::<u64>().ok();
208    }
209    None
210}
211
212#[derive(Debug, Clone, Eq, PartialEq)]
213pub struct ParsedRanges {
214    pub ranges: Vec<SyntacticallyCorrectRange>,
215}
216
217impl ParsedRanges {
218    fn new(ranges: Vec<SyntacticallyCorrectRange>) -> Self {
219        ParsedRanges { ranges }
220    }
221
222    /// Validates a parsed range for a given file-size in bytes.
223    /// # Errors
224    /// If the range is invalid for the the file-size.
225    pub fn validate(
226        &self,
227        file_size_bytes: u64,
228    ) -> Result<Vec<RangeInclusive<u64>>, RangeUnsatisfiableError> {
229        let len = self.ranges.len();
230        let mut validated = Vec::with_capacity(len);
231        for parsed in &self.ranges {
232            let start = match parsed.start {
233                StartPosition::Index(i) => i,
234                StartPosition::FromLast(i) => {
235                    if i > file_size_bytes {
236                        return Err(RangeUnsatisfiableError::FileSuffixOutOfBounds);
237                    }
238                    file_size_bytes.saturating_sub(i)
239                }
240            };
241            let end = match parsed.end {
242                EndPosition::Index(i) => core::cmp::min(i, file_size_bytes.saturating_sub(1)),
243                EndPosition::LastByte => file_size_bytes.saturating_sub(1),
244            };
245
246            let valid = RangeInclusive::new(start, end);
247            validated.push(valid);
248        }
249        // False positive
250        #[allow(clippy::match_same_arms)]
251        match validate_ranges(validated.as_slice()) {
252            RangeValidationResult::Valid => Ok(validated),
253            RangeValidationResult::Overlapping => Err(RangeUnsatisfiableError::OverlappingRanges),
254            RangeValidationResult::Reversed => Err(RangeUnsatisfiableError::RangeReversed),
255        }
256    }
257}
258
259#[derive(Debug, Copy, Clone, Eq, PartialEq)]
260pub enum RangeUnsatisfiableError {
261    OverlappingRanges,
262    RangeReversed,
263    FileSuffixOutOfBounds,
264    IllegalWhitespace,
265    StartsWithWhitespace,
266    DoesNotStartWithToken,
267    ZeroSuffix,
268    BadStartOfRange,
269    BadEndOfRange,
270    UnexpectedNumberOfDashes,
271    Empty,
272}
273
274impl Display for RangeUnsatisfiableError {
275    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
276        match self {
277            RangeUnsatisfiableError::OverlappingRanges => {
278                f.write_str("RangeUnsatisfiable: Ranges overlap")
279            }
280            RangeUnsatisfiableError::RangeReversed => {
281                f.write_str("RangeUnsatisfiable: Reversed range")
282            }
283            RangeUnsatisfiableError::FileSuffixOutOfBounds => f.write_str(
284                "RangeUnsatisfiable: File suffix out of bounds (larger than file bytes)",
285            ),
286            RangeUnsatisfiableError::IllegalWhitespace => {
287                f.write_str("RangeUnsatisfiable: Illegal whitespaces in range")
288            }
289            RangeUnsatisfiableError::StartsWithWhitespace => {
290                f.write_str("RangeUnsatisfiable: Range starts with whitespace")
291            }
292            RangeUnsatisfiableError::DoesNotStartWithToken => f.write_fmt(format_args!(
293                "RangeUnsatisfiable: Range does not start with token '{UNIT_SEP}'"
294            )),
295            RangeUnsatisfiableError::ZeroSuffix => {
296                f.write_str("RangeUnsatisfiable: Range ends at 0")
297            }
298            RangeUnsatisfiableError::BadStartOfRange => {
299                f.write_str("RangeUnsatisfiable: Unparseable start of range")
300            }
301            RangeUnsatisfiableError::BadEndOfRange => {
302                f.write_str("RangeUnsatisfiable: Unparseable end of range")
303            }
304            RangeUnsatisfiableError::UnexpectedNumberOfDashes => {
305                f.write_str("RangeUnsatisfiable: Unexpected number of dashes")
306            }
307            RangeUnsatisfiableError::Empty => f.write_str(
308                "RangeUnsatisfiable: Failed to parse range fallback error, please file an issue",
309            ),
310        }
311    }
312}
313
314impl std::error::Error for RangeUnsatisfiableError {}
315
316enum RangeValidationResult {
317    Valid,
318    Overlapping,
319    Reversed,
320}
321
322fn validate_ranges(ranges: &[RangeInclusive<u64>]) -> RangeValidationResult {
323    let mut bounds = Vec::new();
324    for range in ranges {
325        let start = range.start();
326        let end = range.end();
327        if start > end {
328            return RangeValidationResult::Reversed;
329        } else if ranges.len() == 1 {
330            return RangeValidationResult::Valid;
331        }
332        bounds.push((range.start(), range.end()));
333    }
334    for i in 0..bounds.len() {
335        for j in i + 1..bounds.len() {
336            if bounds[i].0 <= bounds[j].1 && bounds[j].0 <= bounds[i].1 {
337                return RangeValidationResult::Overlapping;
338            }
339        }
340    }
341    RangeValidationResult::Valid
342}
343
344#[derive(Debug, Copy, Clone, Eq, PartialEq)]
345pub struct SyntacticallyCorrectRange {
346    pub start: StartPosition,
347    pub end: EndPosition,
348}
349
350impl SyntacticallyCorrectRange {
351    fn new(start: StartPosition, end: EndPosition) -> Self {
352        SyntacticallyCorrectRange { start, end }
353    }
354}
355
356#[derive(Debug, Copy, Clone, Eq, PartialEq)]
357pub enum StartPosition {
358    Index(u64),
359    FromLast(u64),
360}
361
362#[derive(Debug, Copy, Clone, Eq, PartialEq)]
363pub enum EndPosition {
364    Index(u64),
365    LastByte,
366}
367
368#[cfg(test)]
369mod tests {
370    use crate::{
371        parse_range_header, EndPosition, ParsedRanges, RangeUnsatisfiableError, StartPosition,
372        SyntacticallyCorrectRange,
373    };
374    use core::ops::RangeInclusive;
375
376    const TEST_FILE_LENGTH: u64 = 10_000;
377    /// Testing standard range compliance against <https://datatracker.ietf.org/doc/html/rfc7233>
378    #[test]
379    fn rfc_7233_standard_test1() {
380        let input = "bytes=0-499";
381        let expect =
382            SyntacticallyCorrectRange::new(StartPosition::Index(0), EndPosition::Index(499));
383        let actual = parse_range_header(input).unwrap();
384        assert_eq!(single_range(expect), actual);
385        let expect = RangeInclusive::new(0, 499);
386        let actual = actual.validate(TEST_FILE_LENGTH).unwrap()[0].clone();
387        assert_eq!(expect, actual);
388    }
389
390    #[test]
391    fn rfc_7233_standard_test2() {
392        let input = "bytes=500-999";
393        let expect =
394            SyntacticallyCorrectRange::new(StartPosition::Index(500), EndPosition::Index(999));
395        let actual = parse_range_header(input).unwrap();
396        assert_eq!(single_range(expect), actual);
397        let expect = RangeInclusive::new(500, 999);
398        let actual = actual.validate(TEST_FILE_LENGTH).unwrap()[0].clone();
399        assert_eq!(expect, actual);
400    }
401
402    /// Testing suffix compliance against <https://datatracker.ietf.org/doc/html/rfc7233>
403    #[test]
404    fn rfc_7233_suffixed_test() {
405        let input = "bytes=-500";
406        let expect =
407            SyntacticallyCorrectRange::new(StartPosition::FromLast(500), EndPosition::LastByte);
408        let actual = parse_range_header(input).unwrap();
409        assert_eq!(single_range(expect), actual);
410        let expect = RangeInclusive::new(9500, 9999);
411        let actual = actual.validate(10_000).unwrap()[0].clone();
412        assert_eq!(expect, actual);
413    }
414
415    /// Testing open range compliance against <https://datatracker.ietf.org/doc/html/rfc7233>
416    #[test]
417    fn rfc_7233_open_range_test() {
418        let input = "bytes=9500-";
419        let expect =
420            SyntacticallyCorrectRange::new(StartPosition::Index(9500), EndPosition::LastByte);
421        let actual = parse_range_header(input).unwrap();
422        assert_eq!(single_range(expect), actual);
423        let expect = RangeInclusive::new(9500, 9999);
424        let actual = actual.validate(10_000).unwrap()[0].clone();
425        assert_eq!(expect, actual);
426    }
427
428    /// Testing first and last bytes compliance against <https://datatracker.ietf.org/doc/html/rfc7233>
429    #[test]
430    fn rfc_7233_first_and_last() {
431        let input = "bytes=0-0, -1";
432        let expect = vec![
433            SyntacticallyCorrectRange::new(StartPosition::Index(0), EndPosition::Index(0)),
434            SyntacticallyCorrectRange::new(StartPosition::FromLast(1), EndPosition::LastByte),
435        ];
436        let actual = parse_range_header(input).unwrap();
437        assert_eq!(expect, actual.ranges);
438        let expect = vec![0..=0, 9999..=9999];
439        let actual = actual.validate(10_000).unwrap();
440        assert_eq!(expect, actual);
441    }
442
443    #[test]
444    fn parse_standard_range() {
445        let input = "bytes=0-1023";
446        let expect =
447            SyntacticallyCorrectRange::new(StartPosition::Index(0), EndPosition::Index(1023));
448        let actual = parse_range_header(input).unwrap();
449        assert_eq!(single_range(expect), actual);
450        let expect = RangeInclusive::new(0, 1023);
451        let actual = actual.validate(TEST_FILE_LENGTH).unwrap()[0].clone();
452        assert_eq!(expect, actual);
453    }
454
455    #[test]
456    fn parse_open_ended_range() {
457        let input = "bytes=0-";
458        let expect = SyntacticallyCorrectRange::new(StartPosition::Index(0), EndPosition::LastByte);
459        let actual = parse_range_header(input).unwrap();
460        assert_eq!(single_range(expect), actual);
461        let expect = RangeInclusive::new(0, TEST_FILE_LENGTH - 1);
462        let actual = actual.validate(TEST_FILE_LENGTH).unwrap()[0].clone();
463        assert_eq!(expect, actual);
464    }
465
466    #[test]
467    fn parse_suffix_range_edge() {
468        let input = &format!("bytes=-{}", TEST_FILE_LENGTH);
469        let expect = SyntacticallyCorrectRange::new(
470            StartPosition::FromLast(TEST_FILE_LENGTH),
471            EndPosition::LastByte,
472        );
473        let actual = parse_range_header(input).unwrap();
474        assert_eq!(single_range(expect), actual);
475        let expect = RangeInclusive::new(0, TEST_FILE_LENGTH - 1);
476        let actual = actual.validate(TEST_FILE_LENGTH).unwrap()[0].clone();
477        assert_eq!(expect, actual);
478    }
479
480    #[test]
481    fn parse_empty_as_invalid() {
482        let input = "";
483        let parsed = parse_range_header(input);
484        assert_eq!(parsed, Err(RangeUnsatisfiableError::DoesNotStartWithToken));
485    }
486
487    #[test]
488    fn parse_empty_range_as_invalid() {
489        let input = "bytes=";
490        let parsed = parse_range_header(input);
491        // 0 is unexpected
492        assert_eq!(
493            parsed,
494            Err(RangeUnsatisfiableError::UnexpectedNumberOfDashes)
495        );
496    }
497
498    #[test]
499    fn parse_range_starting_with_whitespace_as_invalid() {
500        let input = "bytes= 0-15";
501        let parsed = parse_range_header(input);
502        // 0 is unexpected
503        assert_eq!(parsed, Err(RangeUnsatisfiableError::StartsWithWhitespace));
504    }
505
506    #[test]
507    fn parse_range_token_starting_with_whitespace_as_invalid() {
508        let input = " bytes=0-15";
509        let parsed = parse_range_header(input);
510        // 0 is unexpected
511        assert_eq!(parsed, Err(RangeUnsatisfiableError::DoesNotStartWithToken));
512    }
513
514    #[test]
515    fn parse_range_strict_parse_numerical() {
516        let input = "bytes=+0-15";
517        let parsed = parse_range_header(input);
518        // 0 is unexpected
519        assert_eq!(parsed, Err(RangeUnsatisfiableError::BadStartOfRange));
520    }
521
522    #[test]
523    fn parse_bad_unit_as_invalid() {
524        let input = "abcde=0-10";
525        let parsed = parse_range_header(input);
526        assert_eq!(parsed, Err(RangeUnsatisfiableError::DoesNotStartWithToken));
527    }
528
529    #[test]
530    fn parse_missing_equals_as_malformed() {
531        let input = "bytes0-10";
532        let parsed = parse_range_header(input);
533        assert_eq!(parsed, Err(RangeUnsatisfiableError::DoesNotStartWithToken));
534    }
535
536    #[test]
537    fn parse_negative_bad_characters_in_range_as_malformed() {
538        let input = "bytes=1-10a";
539        let parsed = parse_range_header(input);
540        assert_eq!(parsed, Err(RangeUnsatisfiableError::BadEndOfRange));
541    }
542
543    #[test]
544    fn parse_negative_numbers_as_malformed() {
545        let input = "bytes=-1-10";
546        let parsed = parse_range_header(input);
547        // Becomes bad eor, since -1 signals suffixed range
548        assert_eq!(parsed, Err(RangeUnsatisfiableError::BadEndOfRange));
549    }
550
551    #[test]
552    fn parse_bad_characters_in_start_of_range() {
553        let input = "bytes=a1-10";
554        let parsed = parse_range_header(input);
555        // Becomes bad eor, since -1 signals suffixed range
556        assert_eq!(parsed, Err(RangeUnsatisfiableError::BadStartOfRange));
557    }
558
559    #[test]
560    fn parse_out_of_bounds_overrun_as_content_length() {
561        let input = &format!("bytes=0-{}", TEST_FILE_LENGTH);
562        let expect = vec![RangeInclusive::new(0, TEST_FILE_LENGTH - 1)];
563        let actual = parse_range_header(input)
564            .unwrap()
565            .validate(TEST_FILE_LENGTH)
566            .unwrap();
567        assert_eq!(expect, actual);
568    }
569
570    #[test]
571    fn parse_out_of_bounds_suffix_overrun_as_unsatisfiable() {
572        let input = &format!("bytes=-{}", TEST_FILE_LENGTH + 1);
573        let parsed = parse_range_header(input)
574            .unwrap()
575            .validate(TEST_FILE_LENGTH);
576        assert_eq!(parsed, Err(RangeUnsatisfiableError::FileSuffixOutOfBounds));
577    }
578
579    #[test]
580    fn parse_zero_length_suffix_as_unsatisfiable() {
581        let input = "bytes=-0";
582        let parsed = parse_range_header(input);
583        assert_eq!(parsed, Err(RangeUnsatisfiableError::ZeroSuffix));
584    }
585
586    #[test]
587    fn parse_single_reversed_as_invalid() {
588        let input = "bytes=15-0";
589        let parsed = parse_range_header(input).unwrap();
590        assert_eq!(
591            parsed.validate(TEST_FILE_LENGTH),
592            Err(RangeUnsatisfiableError::RangeReversed)
593        );
594    }
595
596    #[test]
597    fn parse_zero_range_as_invalid() {
598        let input = "bytes=15-";
599        let parsed = parse_range_header(input).unwrap();
600        assert_eq!(
601            parsed.validate(0),
602            Err(RangeUnsatisfiableError::RangeReversed)
603        );
604    }
605
606    #[test]
607    fn parse_zero_range_last_byte_valid_if_file_size_0() {
608        let input = "bytes=0-";
609        let expect = SyntacticallyCorrectRange::new(StartPosition::Index(0), EndPosition::LastByte);
610        let actual = parse_range_header(input).unwrap().ranges[0];
611        assert_eq!(actual, expect);
612    }
613
614    #[test]
615    fn parse_zero_range_closed_valid_if_file_size_0() {
616        let input = "bytes=0-0";
617        let expect = SyntacticallyCorrectRange::new(StartPosition::Index(0), EndPosition::Index(0));
618        let actual = parse_range_header(input).unwrap().ranges[0];
619        assert_eq!(actual, expect);
620    }
621
622    #[test]
623    fn parse_multi_range() {
624        let input = "bytes=0-1023, 2015-3000, 4000-4500, 8000-9999";
625        let expected_ranges = vec![
626            SyntacticallyCorrectRange::new(StartPosition::Index(0), EndPosition::Index(1023)),
627            SyntacticallyCorrectRange::new(StartPosition::Index(2015), EndPosition::Index(3000)),
628            SyntacticallyCorrectRange::new(StartPosition::Index(4000), EndPosition::Index(4500)),
629            SyntacticallyCorrectRange::new(StartPosition::Index(8000), EndPosition::Index(9999)),
630        ];
631        let parsed = parse_range_header(input).unwrap();
632        assert_eq!(expected_ranges, parsed.ranges);
633        let validated = parsed.validate(TEST_FILE_LENGTH).unwrap();
634        assert_eq!(
635            vec![0..=1023, 2015..=3000, 4000..=4500, 8000..=9999],
636            validated
637        );
638    }
639
640    #[test]
641    fn parse_multi_range_with_open() {
642        let input = "bytes=0-1023, 1024-";
643        let expected_ranges = vec![
644            SyntacticallyCorrectRange::new(StartPosition::Index(0), EndPosition::Index(1023)),
645            SyntacticallyCorrectRange::new(StartPosition::Index(1024), EndPosition::LastByte),
646        ];
647        let parsed = parse_range_header(input).unwrap();
648        assert_eq!(expected_ranges, parsed.ranges);
649        let validated = parsed.validate(TEST_FILE_LENGTH).unwrap();
650        assert_eq!(vec![0..=1023, 1024..=9999], validated);
651    }
652
653    #[test]
654    fn parse_multi_range_with_suffix() {
655        let input = "bytes=0-1023, -1000";
656        let expected_ranges = vec![
657            SyntacticallyCorrectRange::new(StartPosition::Index(0), EndPosition::Index(1023)),
658            SyntacticallyCorrectRange::new(StartPosition::FromLast(1000), EndPosition::LastByte),
659        ];
660        let parsed = parse_range_header(input).unwrap();
661        assert_eq!(expected_ranges, parsed.ranges);
662        assert_eq!(expected_ranges, parsed.ranges);
663        let validated = parsed.validate(TEST_FILE_LENGTH).unwrap();
664        assert_eq!(vec![0..=1023, 9000..=9999], validated);
665    }
666
667    #[test]
668    fn parse_overlapping_multi_range_as_unsatisfiable_standard() {
669        let input = "bytes=0-1023, 500-800";
670        assert_validation_err(input, RangeUnsatisfiableError::OverlappingRanges);
671        let input = "bytes=0-0, 0-15";
672        assert_validation_err(input, RangeUnsatisfiableError::OverlappingRanges);
673        let input = "bytes=0-20, 20-35";
674        assert_validation_err(input, RangeUnsatisfiableError::OverlappingRanges);
675    }
676
677    #[test]
678    fn parse_overlapping_multi_range_as_unsatisfiable_open() {
679        let input = "bytes=0-, 5000-6000";
680        assert_validation_err(input, RangeUnsatisfiableError::OverlappingRanges);
681    }
682
683    #[test]
684    fn parse_overlapping_multi_range_as_unsatisfiable_suffixed() {
685        let input = "bytes=8000-9000, -1001";
686        assert_validation_err(input, RangeUnsatisfiableError::OverlappingRanges);
687        let input = "bytes=8000-9000, -1000";
688        assert_validation_err(input, RangeUnsatisfiableError::OverlappingRanges);
689        // This doesn't overlap
690        let input = "bytes=8000-9000, -999";
691        parse_range_header(input)
692            .unwrap()
693            .validate(TEST_FILE_LENGTH)
694            .unwrap();
695    }
696
697    #[test]
698    fn parse_overlapping_multi_range_as_unsatisfiable_suffixed_open() {
699        let input = "bytes=0-, -1";
700        assert_validation_err(input, RangeUnsatisfiableError::OverlappingRanges);
701    }
702
703    #[test]
704    fn parse_multi_range_with_a_reversed_as_invalid() {
705        let input = "bytes=0-15, 30-20";
706        assert_validation_err(input, RangeUnsatisfiableError::RangeReversed);
707    }
708
709    fn assert_validation_err(input: &str, err: RangeUnsatisfiableError) {
710        let parsed = parse_range_header(input)
711            .unwrap()
712            .validate(TEST_FILE_LENGTH);
713        assert_eq!(Err(err), parsed);
714    }
715
716    #[test]
717    fn parse_multi_range_rejects_invalid() {
718        let input = "bytes=0-15, 25, 9, ";
719        let parsed = parse_range_header(input);
720        assert!(parsed.is_err());
721    }
722
723    #[quickcheck_macros::quickcheck]
724    #[allow(clippy::needless_pass_by_value)]
725    fn always_errs_on_random_input(input: String) -> quickcheck::TestResult {
726        // Basic regex matching most valid range headers
727        let acceptable = regex::Regex::new(
728            "^bytes=((\\d+-\\d+,\\s?)|(\\d+-,\\s?)|(-\\d+,\\s?))*((\\d+-\\d+)|(\\d+-)|(-\\d+))+$",
729        )
730        .unwrap();
731        if acceptable.is_match(&input) {
732            quickcheck::TestResult::discard()
733        } else if let Ok(passed_first_pass) = parse_range_header(&input) {
734            quickcheck::TestResult::from_bool(passed_first_pass.validate(u64::MAX).is_err())
735        } else {
736            quickcheck::TestResult::passed()
737        }
738    }
739
740    fn single_range(syntactically_correct: SyntacticallyCorrectRange) -> ParsedRanges {
741        ParsedRanges::new(vec![syntactically_correct])
742    }
743}