http-range-header-0.4.0/.cargo_vcs_info.json0000644000000001360000000000100143340ustar { "git": { "sha1": "194fad909405c8cba9b1f1e69b90760186cc727c" }, "path_in_vcs": "" }http-range-header-0.4.0/.gitignore000064400000000000000000000000401046102023000151060ustar 00000000000000/target /.idea *.iml Cargo.lock http-range-header-0.4.0/Cargo.toml0000644000000024050000000000100123330ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "http-range-header" version = "0.4.0" exclude = [ "/.github", "CONTRIBUTING.md", ] description = "No-dep range header parser" homepage = "https://github.com/MarcusGrass/parse-range-headers" readme = "./README.md" keywords = [ "http", "parser", "http-headers", "headers", "range", ] categories = [ "parser-implementations", "network-programming", "web-programming", ] license = "MIT" repository = "https://github.com/MarcusGrass/parse-range-headers" [package.metadata] msrv = "1.60.0" [[bench]] name = "benchmark" harness = false [dependencies] [dev-dependencies.criterion] version = "0.5.1" [dev-dependencies.quickcheck] version = "1.0.3" [dev-dependencies.quickcheck_macros] version = "1.0.0" [dev-dependencies.regex] version = "1.8.3" http-range-header-0.4.0/Cargo.toml.orig000064400000000000000000000014061046102023000160140ustar 00000000000000[package] name = "http-range-header" version = "0.4.0" edition = "2018" license = "MIT" readme = "./README.md" description = "No-dep range header parser" repository = "https://github.com/MarcusGrass/parse-range-headers" homepage = "https://github.com/MarcusGrass/parse-range-headers" categories = ["parser-implementations", "network-programming", "web-programming"] keywords = ["http", "parser", "http-headers", "headers", "range"] exclude = ["/.github", "CONTRIBUTING.md"] [package.metadata] msrv = "1.60.0" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] [dev-dependencies] criterion = "0.5.1" quickcheck = "1.0.3" quickcheck_macros = "1.0.0" regex = "1.8.3" [[bench]] name = "benchmark" harness = false http-range-header-0.4.0/Changelog.md000064400000000000000000000024471046102023000153440ustar 00000000000000 # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] - ReleaseDate ### Added ### Changed ### Fixed ## [0.4.0] - ReleaseDate ### Added - Bench error performance ### Changed - Msrv set to 1.60.0 to match [tower-http](https://github.com/tower-rs/tower-http) - Use higher `Rust` version features to improve performance - Remove feature `with_error_cause` - Convert Error into an enum ### Fixed ## [0.3.1] - 2023-07-21 ### Fixed - Now accepts ranges that are out of bounds, but truncates them down to an in-range value, according to the spec, thanks @jfaust! - Clean up with clippy pedantic, update docs, format, etc. Resulted in a bench improvement of almost 5%. ## [0.3.0] - 2021-11-25 ### Changed - Only expose a single error-type to make usage more ergonomic ## [0.2.1] - 2021-11-25 ### Added - Make some optimizations ## [0.2.0] - 2021-11-25 ### Changed - Rename to http-range-header ## [0.1.0] - 2021-11-25 ### Added - Released first version under parse_range_headers http-range-header-0.4.0/LICENSE000064400000000000000000000020541046102023000141320ustar 00000000000000MIT License Copyright (c) 2021 MarcusGrass Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. http-range-header-0.4.0/README.md000064400000000000000000000015051046102023000144040ustar 00000000000000# Range header parsing [![Latest workflow](https://github.com/MarcusGrass/parse-range-headers/workflows/check_commit/badge.svg)](https://github.com/MarcusGrass/parse-range-headers/actions) [![CratesIo](https://shields.io/crates/v/http-range-header)](https://crates.io/crates/http-range-header) The main goals of this parser is: * Follow specification [RFC-2616](https://www.ietf.org/rfc/rfc2616.txt) * Behave as expected [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range) * Accuracy - parses headers strictly * Security - Never panics, ensured by fuzzing * Stability * No dependecies Secondary goals are: * Speed * Information on why the header was rejected The parser is strict. Any range where all parts are not syntactically correct and makes sense in the context of the underlying resource will be rejected. http-range-header-0.4.0/benches/benchmark.rs000064400000000000000000000026051046102023000170360ustar 00000000000000use criterion::{black_box, criterion_group, criterion_main}; use http_range_header::RangeUnsatisfiableError; pub fn bench(c: &mut criterion::Criterion) { c.bench_function("Standard range", |b| { b.iter(|| { http_range_header::parse_range_header(black_box("bytes=0-15")) .unwrap() .validate(black_box(10_000)) }) }); c.bench_function("Multipart range", |b| { b.iter(|| { http_range_header::parse_range_header(black_box("bytes=0-15, 20-30, 40-60")) .unwrap() .validate(black_box(10_000)) }) }); c.bench_function("Suffix range", |b| { b.iter(|| { http_range_header::parse_range_header(black_box("bytes=-500")) .unwrap() .validate(black_box(10_000)) }) }); c.bench_function("Open range", |b| { b.iter(|| { http_range_header::parse_range_header(black_box("bytes=0-")) .unwrap() .validate(black_box(10_000)) }) }); c.bench_function("Bad multipart range", |b| { b.iter(|| { assert_eq!( Err(RangeUnsatisfiableError::ZeroSuffix), http_range_header::parse_range_header(black_box("bytes=0-19, -0")) ); }) }); } criterion_group!(benches, bench); criterion_main!(benches); http-range-header-0.4.0/src/lib.rs000064400000000000000000000641371046102023000150420ustar 00000000000000#![warn(clippy::pedantic)] use core::fmt::{Debug, Display, Formatter}; use core::ops::RangeInclusive; const UNIT_SEP: &str = "bytes="; /// Function that parses the content of a range header. /// /// Follows the [spec here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range) /// /// And [here](https://www.ietf.org/rfc/rfc2616.txt) /// /// 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. /// /// Parses ranges strictly, as in the examples contained in the above specifications. /// /// Ranges such as `bytes=0-15, 16-20, abc` will be rejected immediately. /// /// It preserves the range ordering, the specification leaves it open to the server to determine whether /// ranges that are out of order are correct or not, ie `bytes=20-30, 0-15` /// /// # Example no trailing or leading whitespaces /// ``` /// // Ok /// assert!(http_range_header::parse_range_header("bytes=0-15").is_ok()); /// // Not allowed /// assert!(http_range_header::parse_range_header("bytes=0-15 ").is_err()); /// // Also not allowed /// assert!(http_range_header::parse_range_header("bytes= 0-15").is_err()); /// ``` /// /// # Example No leading whitespaces except in the case of separating multipart ranges /// ``` /// // Ok, multipart with a leading whitespace after comma /// assert!(http_range_header::parse_range_header("bytes=0-15, 20-30").is_ok()); /// // Ok multipart without leading whitespace after comma /// assert!(http_range_header::parse_range_header("bytes=0-15,20-30").is_ok()); /// ``` /// /// # Example No negative values, no leading zeroes, no plus-sign /// ``` /// // No negatives /// assert!(http_range_header::parse_range_header("bytes=-12-15").is_err()); /// // No leading zeroes /// assert!(http_range_header::parse_range_header("bytes=00-15").is_err()); /// // No plus sign /// assert!(http_range_header::parse_range_header("bytes=+0-15").is_err()); /// ``` /// /// Makes two passes and parses ranges strictly. On the first pass, if any range is malformed returns an `Err`. /// /// On the second pass if the ranges doesn't make sense (reversed range, range out of bounds, etc.) returns an `Err`. /// # Example with a standard valid range /// /// ``` /// let input = "bytes=0-15"; /// let file_size_bytes = 512; /// let parsed_ranges = http_range_header::parse_range_header(input); /// /// match parsed_ranges { /// Ok(ranges) => { /// match ranges.validate(file_size_bytes) { /// Ok(valid_ranges) => { /// for range in valid_ranges { /// // do something with ranges /// assert_eq!(0..=15, range) /// } /// } /// Err(_err) => { /// // Do something when ranges doesn't make sense /// panic!("Weird range!") /// } /// } /// } /// Err(_err) => { /// // Do something with malformed ranges /// panic!("Malformed range!") /// } /// } /// ``` /// /// The parser makes two passes, one without a known file-size, ensuring all ranges are syntactically correct. /// The returned struct will through its `validate` method accept a file-size and figure out whether or not the /// syntactically correct ranges actually makes sense in context /// /// The range `bytes=0-20` on a file with 15 bytes will be accepted in the first pass as the content size is unknown. /// 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). /// # Example range truncates in `validate` because it exceedes /// ``` /// let input = "bytes=0-20"; /// let file_size_bytes = 15; /// let parsed_ranges = http_range_header::parse_range_header(input) /// // Is syntactically correct /// .unwrap(); /// let validated = parsed_ranges.validate(file_size_bytes).unwrap(); /// assert_eq!(vec![0..=14], validated); /// ``` /// /// Range reversal and overlap is also checked in the second pass, the range `bytes=0-20, 5-10` /// will become two syntactically correct ranges, but `validate` will return ann `Err`. /// /// This is an opinionated implementation, [the spec](https://datatracker.ietf.org/doc/html/rfc7233) /// allows a server to determine its implementation of overlapping ranges, this api currently does not allow it. /// /// # Example multipart-range fails `validate` because of an overlap /// ``` /// let input = "bytes=0-15, 10-20, 30-50"; /// let file_size_bytes = 512; /// let parsed_ranges = http_range_header::parse_range_header(input) /// // Is syntactically correct /// .unwrap(); /// let validated = parsed_ranges.validate(file_size_bytes); /// // Some ranges overlap, all valid ranges get truncated to 1 Err /// assert!(validated.is_err()); /// ``` /// # Errors /// Will return an error if the `range_header_value` cannot be strictly parsed into a range /// per the http spec. pub fn parse_range_header( range_header_value: &str, ) -> Result { const COMMA: char = ','; if let Some((prefix, indicated_range)) = range_header_value.split_once(UNIT_SEP) { if indicated_range.starts_with(char::is_whitespace) { return Err(RangeUnsatisfiableError::StartsWithWhitespace); } if !prefix.is_empty() { return Err(RangeUnsatisfiableError::DoesNotStartWithToken); } let mut last_err = None; let ranges = indicated_range .split(COMMA) .filter_map(|range| { if let Some(trimmed) = trim(range) { match parse_inner(trimmed) { Ok(parsed) => Some(parsed), Err(e) => { last_err = Some(e); None } } } else { last_err = Some(RangeUnsatisfiableError::IllegalWhitespace); None } }) .collect::>(); if let Some(last_err) = last_err { return Err(last_err); } if ranges.is_empty() { // Some other error should have been caught before we end up here Err(RangeUnsatisfiableError::Empty) } else { Ok(ParsedRanges::new(ranges)) } } else { Err(RangeUnsatisfiableError::DoesNotStartWithToken) } } fn trim(s: &str) -> Option<&str> { if s.ends_with(char::is_whitespace) || s.match_indices(char::is_whitespace).count() > 1 { None } else { Some(s.trim()) } } #[inline] fn parse_inner(range: &str) -> Result { if let Some((start, end)) = range.split_once('-') { if start.is_empty() { if let Some(end) = strict_parse_u64(end) { if end == 0 { return Err(RangeUnsatisfiableError::ZeroSuffix); } return Ok(SyntacticallyCorrectRange::new( StartPosition::FromLast(end), EndPosition::LastByte, )); } return Err(RangeUnsatisfiableError::BadEndOfRange); } if let Some(start) = strict_parse_u64(start) { if end.is_empty() { return Ok(SyntacticallyCorrectRange::new( StartPosition::Index(start), EndPosition::LastByte, )); } if let Some(end) = strict_parse_u64(end) { return Ok(SyntacticallyCorrectRange::new( StartPosition::Index(start), EndPosition::Index(end), )); } return Err(RangeUnsatisfiableError::BadEndOfRange); } return Err(RangeUnsatisfiableError::BadStartOfRange); } Err(RangeUnsatisfiableError::UnexpectedNumberOfDashes) } fn strict_parse_u64(s: &str) -> Option { if !s.starts_with('+') && (s.len() == 1 || !s.starts_with('0')) { return s.parse::().ok(); } None } #[derive(Debug, Clone, Eq, PartialEq)] pub struct ParsedRanges { ranges: Vec, } impl ParsedRanges { fn new(ranges: Vec) -> Self { ParsedRanges { ranges } } /// Validates a parsed range for a given file-size in bytes. /// # Errors /// If the range is invalid for the the file-size. pub fn validate( &self, file_size_bytes: u64, ) -> Result>, RangeUnsatisfiableError> { let len = self.ranges.len(); let mut validated = Vec::with_capacity(len); for parsed in &self.ranges { let start = match parsed.start { StartPosition::Index(i) => i, StartPosition::FromLast(i) => { if i > file_size_bytes { return Err(RangeUnsatisfiableError::FileSuffixOutOfBounds); } file_size_bytes - i } }; let end = match parsed.end { EndPosition::Index(i) => core::cmp::min(i, file_size_bytes - 1), EndPosition::LastByte => file_size_bytes - 1, }; let valid = RangeInclusive::new(start, end); validated.push(valid); } // False positive #[allow(clippy::match_same_arms)] match validate_ranges(validated.as_slice()) { RangeValidationResult::Valid => Ok(validated), RangeValidationResult::Overlapping => Err(RangeUnsatisfiableError::OverlappingRanges), RangeValidationResult::Reversed => Err(RangeUnsatisfiableError::RangeReversed), } } } #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum RangeUnsatisfiableError { OverlappingRanges, RangeReversed, FileSuffixOutOfBounds, IllegalWhitespace, StartsWithWhitespace, DoesNotStartWithToken, ZeroSuffix, BadStartOfRange, BadEndOfRange, UnexpectedNumberOfDashes, Empty, } impl Display for RangeUnsatisfiableError { fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { match self { RangeUnsatisfiableError::OverlappingRanges => { f.write_str("RangeUnsatisfiable: Ranges overlap") } RangeUnsatisfiableError::RangeReversed => { f.write_str("RangeUnsatisfiable: Reversed range") } RangeUnsatisfiableError::FileSuffixOutOfBounds => f.write_str( "RangeUnsatisfiable: File suffix out of bounds (larger than file bytes)", ), RangeUnsatisfiableError::IllegalWhitespace => { f.write_str("RangeUnsatisfiable: Illegal whitespaces in range") } RangeUnsatisfiableError::StartsWithWhitespace => { f.write_str("RangeUnsatisfiable: Range starts with whitespace") } RangeUnsatisfiableError::DoesNotStartWithToken => f.write_fmt(format_args!( "RangeUnsatisfiable: Range does not start with token '{UNIT_SEP}'" )), RangeUnsatisfiableError::ZeroSuffix => { f.write_str("RangeUnsatisfiable: Range ends at 0") } RangeUnsatisfiableError::BadStartOfRange => { f.write_str("RangeUnsatisfiable: Unparseable start of range") } RangeUnsatisfiableError::BadEndOfRange => { f.write_str("RangeUnsatisfiable: Unparseable end of range") } RangeUnsatisfiableError::UnexpectedNumberOfDashes => { f.write_str("RangeUnsatisfiable: Unexpected number of dashes") } RangeUnsatisfiableError::Empty => f.write_str( "RangeUnsatisfiable: Failed to parse range fallback error, please file an issue", ), } } } impl std::error::Error for RangeUnsatisfiableError {} enum RangeValidationResult { Valid, Overlapping, Reversed, } fn validate_ranges(ranges: &[RangeInclusive]) -> RangeValidationResult { let mut bounds = Vec::new(); for range in ranges { let start = range.start(); let end = range.end(); if start > end { return RangeValidationResult::Reversed; } else if ranges.len() == 1 { return RangeValidationResult::Valid; } bounds.push((range.start(), range.end())); } for i in 0..bounds.len() { for j in i + 1..bounds.len() { if bounds[i].0 <= bounds[j].1 && bounds[j].0 <= bounds[i].1 { return RangeValidationResult::Overlapping; } } } RangeValidationResult::Valid } #[derive(Debug, Copy, Clone, Eq, PartialEq)] struct SyntacticallyCorrectRange { start: StartPosition, end: EndPosition, } impl SyntacticallyCorrectRange { fn new(start: StartPosition, end: EndPosition) -> Self { SyntacticallyCorrectRange { start, end } } } #[derive(Debug, Copy, Clone, Eq, PartialEq)] enum StartPosition { Index(u64), FromLast(u64), } #[derive(Debug, Copy, Clone, Eq, PartialEq)] enum EndPosition { Index(u64), LastByte, } #[cfg(test)] mod tests { use crate::{ parse_range_header, EndPosition, ParsedRanges, RangeUnsatisfiableError, StartPosition, SyntacticallyCorrectRange, }; use core::ops::RangeInclusive; const TEST_FILE_LENGTH: u64 = 10_000; /// Testing standard range compliance against https://datatracker.ietf.org/doc/html/rfc7233 #[test] fn rfc_7233_standard_test1() { let input = "bytes=0-499"; let expect = SyntacticallyCorrectRange::new(StartPosition::Index(0), EndPosition::Index(499)); let actual = parse_range_header(input).unwrap(); assert_eq!(single_range(expect), actual); let expect = RangeInclusive::new(0, 499); let actual = actual.validate(TEST_FILE_LENGTH).unwrap()[0].clone(); assert_eq!(expect, actual) } #[test] fn rfc_7233_standard_test2() { let input = "bytes=500-999"; let expect = SyntacticallyCorrectRange::new(StartPosition::Index(500), EndPosition::Index(999)); let actual = parse_range_header(input).unwrap(); assert_eq!(single_range(expect), actual); let expect = RangeInclusive::new(500, 999); let actual = actual.validate(TEST_FILE_LENGTH).unwrap()[0].clone(); assert_eq!(expect, actual) } /// Testing suffix compliance against https://datatracker.ietf.org/doc/html/rfc7233 #[test] fn rfc_7233_suffixed_test() { let input = "bytes=-500"; let expect = SyntacticallyCorrectRange::new(StartPosition::FromLast(500), EndPosition::LastByte); let actual = parse_range_header(input).unwrap(); assert_eq!(single_range(expect), actual); let expect = RangeInclusive::new(9500, 9999); let actual = actual.validate(10_000).unwrap()[0].clone(); assert_eq!(expect, actual) } /// Testing open range compliance against https://datatracker.ietf.org/doc/html/rfc7233 #[test] fn rfc_7233_open_range_test() { let input = "bytes=9500-"; let expect = SyntacticallyCorrectRange::new(StartPosition::Index(9500), EndPosition::LastByte); let actual = parse_range_header(input).unwrap(); assert_eq!(single_range(expect), actual); let expect = RangeInclusive::new(9500, 9999); let actual = actual.validate(10_000).unwrap()[0].clone(); assert_eq!(expect, actual) } /// Testing first and last bytes compliance against https://datatracker.ietf.org/doc/html/rfc7233 #[test] fn rfc_7233_first_and_last() { let input = "bytes=0-0, -1"; let expect = vec![ SyntacticallyCorrectRange::new(StartPosition::Index(0), EndPosition::Index(0)), SyntacticallyCorrectRange::new(StartPosition::FromLast(1), EndPosition::LastByte), ]; let actual = parse_range_header(input).unwrap(); assert_eq!(expect, actual.ranges); let expect = vec![0..=0, 9999..=9999]; let actual = actual.validate(10_000).unwrap(); assert_eq!(expect, actual) } #[test] fn parse_standard_range() { let input = "bytes=0-1023"; let expect = SyntacticallyCorrectRange::new(StartPosition::Index(0), EndPosition::Index(1023)); let actual = parse_range_header(input).unwrap(); assert_eq!(single_range(expect), actual); let expect = RangeInclusive::new(0, 1023); let actual = actual.validate(TEST_FILE_LENGTH).unwrap()[0].clone(); assert_eq!(expect, actual) } #[test] fn parse_open_ended_range() { let input = "bytes=0-"; let expect = SyntacticallyCorrectRange::new(StartPosition::Index(0), EndPosition::LastByte); let actual = parse_range_header(input).unwrap(); assert_eq!(single_range(expect), actual); let expect = RangeInclusive::new(0, TEST_FILE_LENGTH - 1); let actual = actual.validate(TEST_FILE_LENGTH).unwrap()[0].clone(); assert_eq!(expect, actual); } #[test] fn parse_suffix_range_edge() { let input = &format!("bytes=-{}", TEST_FILE_LENGTH); let expect = SyntacticallyCorrectRange::new( StartPosition::FromLast(TEST_FILE_LENGTH), EndPosition::LastByte, ); let actual = parse_range_header(input).unwrap(); assert_eq!(single_range(expect), actual); let expect = RangeInclusive::new(0, TEST_FILE_LENGTH - 1); let actual = actual.validate(TEST_FILE_LENGTH).unwrap()[0].clone(); assert_eq!(expect, actual); } #[test] fn parse_empty_as_invalid() { let input = ""; let parsed = parse_range_header(input); assert_eq!(parsed, Err(RangeUnsatisfiableError::DoesNotStartWithToken)); } #[test] fn parse_empty_range_as_invalid() { let input = "bytes="; let parsed = parse_range_header(input); // 0 is unexpected assert_eq!( parsed, Err(RangeUnsatisfiableError::UnexpectedNumberOfDashes) ); } #[test] fn parse_range_starting_with_whitespace_as_invalid() { let input = "bytes= 0-15"; let parsed = parse_range_header(input); // 0 is unexpected assert_eq!(parsed, Err(RangeUnsatisfiableError::StartsWithWhitespace)); } #[test] fn parse_range_token_starting_with_whitespace_as_invalid() { let input = " bytes=0-15"; let parsed = parse_range_header(input); // 0 is unexpected assert_eq!(parsed, Err(RangeUnsatisfiableError::DoesNotStartWithToken)); } #[test] fn parse_range_strict_parse_numerical() { let input = "bytes=+0-15"; let parsed = parse_range_header(input); // 0 is unexpected assert_eq!(parsed, Err(RangeUnsatisfiableError::BadStartOfRange)); } #[test] fn parse_bad_unit_as_invalid() { let input = "abcde=0-10"; let parsed = parse_range_header(input); assert_eq!(parsed, Err(RangeUnsatisfiableError::DoesNotStartWithToken)); } #[test] fn parse_missing_equals_as_malformed() { let input = "bytes0-10"; let parsed = parse_range_header(input); assert_eq!(parsed, Err(RangeUnsatisfiableError::DoesNotStartWithToken)); } #[test] fn parse_negative_bad_characters_in_range_as_malformed() { let input = "bytes=1-10a"; let parsed = parse_range_header(input); assert_eq!(parsed, Err(RangeUnsatisfiableError::BadEndOfRange)); } #[test] fn parse_negative_numbers_as_malformed() { let input = "bytes=-1-10"; let parsed = parse_range_header(input); // Becomes bad eor, since -1 signals suffixed range assert_eq!(parsed, Err(RangeUnsatisfiableError::BadEndOfRange)); } #[test] fn parse_bad_characters_in_start_of_range() { let input = "bytes=a1-10"; let parsed = parse_range_header(input); // Becomes bad eor, since -1 signals suffixed range assert_eq!(parsed, Err(RangeUnsatisfiableError::BadStartOfRange)); } #[test] fn parse_out_of_bounds_overrun_as_content_length() { let input = &format!("bytes=0-{}", TEST_FILE_LENGTH); let expect = vec![RangeInclusive::new(0, TEST_FILE_LENGTH - 1)]; let actual = parse_range_header(input) .unwrap() .validate(TEST_FILE_LENGTH) .unwrap(); assert_eq!(expect, actual); } #[test] fn parse_out_of_bounds_suffix_overrun_as_unsatisfiable() { let input = &format!("bytes=-{}", TEST_FILE_LENGTH + 1); let parsed = parse_range_header(input) .unwrap() .validate(TEST_FILE_LENGTH); assert_eq!(parsed, Err(RangeUnsatisfiableError::FileSuffixOutOfBounds)); } #[test] fn parse_zero_length_suffix_as_unsatisfiable() { let input = &format!("bytes=-0"); let parsed = parse_range_header(input); assert_eq!(parsed, Err(RangeUnsatisfiableError::ZeroSuffix)); } #[test] fn parse_single_reversed_as_invalid() { let input = &format!("bytes=15-0"); let parsed = parse_range_header(input).unwrap(); assert_eq!( parsed.validate(TEST_FILE_LENGTH), Err(RangeUnsatisfiableError::RangeReversed) ); } #[test] fn parse_multi_range() { let input = "bytes=0-1023, 2015-3000, 4000-4500, 8000-9999"; let expected_ranges = vec![ SyntacticallyCorrectRange::new(StartPosition::Index(0), EndPosition::Index(1023)), SyntacticallyCorrectRange::new(StartPosition::Index(2015), EndPosition::Index(3000)), SyntacticallyCorrectRange::new(StartPosition::Index(4000), EndPosition::Index(4500)), SyntacticallyCorrectRange::new(StartPosition::Index(8000), EndPosition::Index(9999)), ]; let parsed = parse_range_header(input).unwrap(); assert_eq!(expected_ranges, parsed.ranges); let validated = parsed.validate(TEST_FILE_LENGTH).unwrap(); assert_eq!( vec![0..=1023, 2015..=3000, 4000..=4500, 8000..=9999], validated ) } #[test] fn parse_multi_range_with_open() { let input = "bytes=0-1023, 1024-"; let expected_ranges = vec![ SyntacticallyCorrectRange::new(StartPosition::Index(0), EndPosition::Index(1023)), SyntacticallyCorrectRange::new(StartPosition::Index(1024), EndPosition::LastByte), ]; let parsed = parse_range_header(input).unwrap(); assert_eq!(expected_ranges, parsed.ranges); let validated = parsed.validate(TEST_FILE_LENGTH).unwrap(); assert_eq!(vec![0..=1023, 1024..=9999], validated); } #[test] fn parse_multi_range_with_suffix() { let input = "bytes=0-1023, -1000"; let expected_ranges = vec![ SyntacticallyCorrectRange::new(StartPosition::Index(0), EndPosition::Index(1023)), SyntacticallyCorrectRange::new(StartPosition::FromLast(1000), EndPosition::LastByte), ]; let parsed = parse_range_header(input).unwrap(); assert_eq!(expected_ranges, parsed.ranges); assert_eq!(expected_ranges, parsed.ranges); let validated = parsed.validate(TEST_FILE_LENGTH).unwrap(); assert_eq!(vec![0..=1023, 9000..=9999], validated); } #[test] fn parse_overlapping_multi_range_as_unsatisfiable_standard() { let input = "bytes=0-1023, 500-800"; assert_validation_err(input, RangeUnsatisfiableError::OverlappingRanges); let input = "bytes=0-0, 0-15"; assert_validation_err(input, RangeUnsatisfiableError::OverlappingRanges); let input = "bytes=0-20, 20-35"; assert_validation_err(input, RangeUnsatisfiableError::OverlappingRanges); } #[test] fn parse_overlapping_multi_range_as_unsatisfiable_open() { let input = "bytes=0-, 5000-6000"; assert_validation_err(input, RangeUnsatisfiableError::OverlappingRanges); } #[test] fn parse_overlapping_multi_range_as_unsatisfiable_suffixed() { let input = "bytes=8000-9000, -1001"; assert_validation_err(input, RangeUnsatisfiableError::OverlappingRanges); let input = "bytes=8000-9000, -1000"; assert_validation_err(input, RangeUnsatisfiableError::OverlappingRanges); // This doesn't overlap let input = "bytes=8000-9000, -999"; parse_range_header(input) .unwrap() .validate(TEST_FILE_LENGTH) .unwrap(); } #[test] fn parse_overlapping_multi_range_as_unsatisfiable_suffixed_open() { let input = "bytes=0-, -1"; assert_validation_err(input, RangeUnsatisfiableError::OverlappingRanges); } #[test] fn parse_multi_range_with_a_reversed_as_invalid() { let input = "bytes=0-15, 30-20"; assert_validation_err(input, RangeUnsatisfiableError::RangeReversed); } fn assert_validation_err(input: &str, err: RangeUnsatisfiableError) { let parsed = parse_range_header(input) .unwrap() .validate(TEST_FILE_LENGTH); assert_eq!(Err(err), parsed); } #[test] fn parse_multi_range_rejects_invalid() { let input = "bytes=0-15, 25, 9, "; let parsed = parse_range_header(input); assert!(parsed.is_err()) } #[quickcheck_macros::quickcheck] fn always_errs_on_random_input(input: String) -> quickcheck::TestResult { // Basic regex matching most valid range headers let acceptable = regex::Regex::new( "^bytes=((\\d+-\\d+,\\s?)|(\\d+-,\\s?)|(-\\d+,\\s?))*((\\d+-\\d+)|(\\d+-)|(-\\d+))+$", ) .unwrap(); if acceptable.is_match(&input) { quickcheck::TestResult::discard() } else { if let Ok(passed_first_pass) = parse_range_header(&input) { quickcheck::TestResult::from_bool(passed_first_pass.validate(u64::MAX).is_err()) } else { quickcheck::TestResult::passed() } } } fn single_range(syntactically_correct: SyntacticallyCorrectRange) -> ParsedRanges { ParsedRanges::new(vec![syntactically_correct]) } }