use headers::{Header, Range};
use std::ops::Bound;
#[derive(Debug)]
pub(crate) struct BadRangeError;
pub(crate) fn bytes_range(range: Option<Range>, max_len: u64) -> Result<(u64, u64), BadRangeError> {
let range = if let Some(range) = range {
range
} else {
return Ok((0, max_len));
};
for (start, end) in range.satisfiable_ranges(max_len) {
tracing::trace!("range request received, {:?}-{:?}-{}", start, end, max_len);
match normalize_byte_range(start, end, max_len)? {
Some((start, end)) => {
tracing::trace!("range request to return: {}-{}/{}", start, end, max_len);
return Ok((start, end));
}
None => tracing::trace!("unsatisfiable byte range for length {max_len}"),
}
}
if has_oversized_suffix_range(&range, max_len) {
tracing::trace!(
"suffix range exceeds file size, returning full content: 0-{}/{}",
max_len,
max_len
);
Ok((0, max_len))
} else {
Err(BadRangeError)
}
}
fn normalize_byte_range(
start: Bound<u64>,
end: Bound<u64>,
max_len: u64,
) -> Result<Option<(u64, u64)>, BadRangeError> {
match (start, end) {
(Bound::Included(start), Bound::Included(end)) => {
if start > end {
return Err(BadRangeError);
}
if start >= max_len {
return Ok(None);
}
let end = if end >= max_len { max_len } else { end + 1 };
Ok((start < end).then_some((start, end)))
}
(Bound::Included(start), Bound::Unbounded) => {
Ok((start < max_len).then_some((start, max_len)))
}
_ => Err(BadRangeError),
}
}
#[cold]
fn has_oversized_suffix_range(range: &Range, max_len: u64) -> bool {
if max_len == 0 {
return false;
}
let mut values = Vec::with_capacity(1);
range.encode(&mut values);
values
.iter()
.filter_map(|value| value.to_str().ok())
.filter_map(|value| value.strip_prefix("bytes="))
.flat_map(|specs| specs.split(','))
.any(|spec| suffix_len_exceeds(spec.trim(), max_len))
}
fn suffix_len_exceeds(spec: &str, max_len: u64) -> bool {
let Some(digits) = spec.strip_prefix('-') else {
return false;
};
if digits.is_empty() {
return false;
}
let mut value = 0_u64;
let mut non_zero = false;
for b in digits.bytes() {
if !b.is_ascii_digit() {
return false;
}
let digit = u64::from(b - b'0');
non_zero |= digit != 0;
let Some(next) = value.checked_mul(10).and_then(|v| v.checked_add(digit)) else {
return true;
};
value = next;
if value > max_len {
return true;
}
}
non_zero && value > max_len
}
#[cfg(test)]
mod tests {
use headers::{HeaderMap, HeaderMapExt, Range};
use super::bytes_range;
fn range(s: &str) -> Option<Range> {
let mut map = HeaderMap::new();
map.insert(http::header::RANGE, format!("bytes={s}").parse().unwrap());
map.typed_get::<Range>()
}
#[test]
fn no_range_returns_full_file() {
assert_eq!(bytes_range(None, 1000).unwrap(), (0, 1000));
}
#[test]
fn inclusive_range_within_bounds() {
assert_eq!(bytes_range(range("0-499"), 1000).unwrap(), (0, 500));
}
#[test]
fn inclusive_range_to_last_byte() {
assert_eq!(bytes_range(range("500-999"), 1000).unwrap(), (500, 1000));
}
#[test]
fn suffix_range_within_file() {
assert_eq!(bytes_range(range("-200"), 1000).unwrap(), (800, 1000));
}
#[test]
fn suffix_range_larger_than_file_returns_full() {
assert_eq!(bytes_range(range("-2000"), 1000).unwrap(), (0, 1000));
}
#[test]
fn enormous_suffix_range_returns_full() {
assert_eq!(
bytes_range(range("-18446744073709551616"), 1000).unwrap(),
(0, 1000)
);
}
#[test]
fn open_ended_range_from_offset() {
assert_eq!(bytes_range(range("100-"), 1000).unwrap(), (100, 1000));
}
#[test]
fn range_start_equals_end_is_single_byte() {
assert_eq!(bytes_range(range("5-5"), 1000).unwrap(), (5, 6));
}
#[test]
fn range_start_greater_than_end_is_error() {
assert!(bytes_range(range("100-50"), 1000).is_err());
}
#[test]
fn range_start_beyond_file_size_is_error() {
assert!(bytes_range(range("2000-3000"), 1000).is_err());
}
#[test]
fn open_ended_range_starting_at_file_size_is_error() {
assert!(bytes_range(range("1000-"), 1000).is_err());
}
#[test]
fn out_of_bounds_first_byte_ranges_are_errors() {
assert!(bytes_range(range("5000-"), 100).is_err());
assert!(bytes_range(range("5000-5999"), 100).is_err());
assert!(bytes_range(range("100-199"), 100).is_err());
}
#[test]
fn range_end_beyond_file_size_is_clamped() {
assert_eq!(bytes_range(range("50-999"), 100).unwrap(), (50, 100));
}
#[test]
fn huge_range_end_does_not_overflow() {
assert_eq!(
bytes_range(range("0-18446744073709551615"), 100).unwrap(),
(0, 100)
);
}
#[test]
fn invalid_empty_range_is_error() {
assert!(bytes_range(range("-"), 100).is_err());
}
#[test]
fn range_on_zero_byte_file_is_unsatisfiable() {
assert!(bytes_range(range("0-0"), 0).is_err());
assert!(bytes_range(range("-1"), 0).is_err());
assert!(bytes_range(range("0-"), 0).is_err());
}
#[test]
fn later_satisfiable_range_is_used() {
assert_eq!(bytes_range(range("200-300,0-9"), 100).unwrap(), (0, 10));
}
#[test]
fn suffix_range_equal_to_file_size_returns_full() {
assert_eq!(bytes_range(range("-1000"), 1000).unwrap(), (0, 1000));
}
#[test]
fn suffix_range_of_one_byte() {
assert_eq!(bytes_range(range("-1"), 1000).unwrap(), (999, 1000));
}
#[test]
fn zero_length_suffix_is_unsatisfiable() {
assert!(bytes_range(range("-0"), 1000).is_err());
}
#[test]
fn leading_zeros_in_suffix_are_parsed() {
assert_eq!(bytes_range(range("-00200"), 1000).unwrap(), (800, 1000));
assert_eq!(bytes_range(range("-02000"), 1000).unwrap(), (0, 1000));
}
#[test]
fn full_file_range_returns_full() {
assert_eq!(bytes_range(range("0-999"), 1000).unwrap(), (0, 1000));
}
#[test]
fn multi_range_first_satisfiable_wins() {
assert_eq!(bytes_range(range("0-9,50-59"), 100).unwrap(), (0, 10));
}
#[test]
fn multi_range_with_oversized_suffix_picks_first_valid() {
assert_eq!(bytes_range(range("10-19,-99999"), 100).unwrap(), (10, 20));
}
#[test]
fn range_at_u64_boundary_file_size() {
assert_eq!(bytes_range(range("0-"), u64::MAX).unwrap(), (0, u64::MAX));
}
use proptest::prelude::*;
proptest! {
#![proptest_config(ProptestConfig {
cases: 512, ..ProptestConfig::default()
})]
#[test]
fn prop_inclusive_range_roundtrip(
(max_len, a, b) in (1u64..=10_000)
.prop_flat_map(|m| (Just(m), 0u64..m, 0u64..m))
.prop_map(|(m, x, y)| (m, x.min(y), x.max(y))),
) {
let req = format!("{a}-{b}");
let (start, end) = bytes_range(range(&req), max_len).unwrap();
prop_assert_eq!((start, end), (a, b + 1));
}
#[test]
fn prop_open_ended_range_extends_to_eof(
(max_len, a) in (1u64..=10_000).prop_flat_map(|m| (Just(m), 0u64..m)),
) {
let req = format!("{a}-");
let (start, end) = bytes_range(range(&req), max_len).unwrap();
prop_assert_eq!((start, end), (a, max_len));
}
#[test]
fn prop_suffix_range_within_bounds(
(max_len, n) in (1u64..=10_000).prop_flat_map(|m| (Just(m), 1u64..=m)),
) {
let req = format!("-{n}");
let (start, end) = bytes_range(range(&req), max_len).unwrap();
prop_assert_eq!((start, end), (max_len - n, max_len));
}
#[test]
fn prop_oversized_suffix_returns_full_file(
(max_len, n) in (1u64..=10_000)
.prop_flat_map(|m| (Just(m), (m + 1)..=u64::MAX)),
) {
let req = format!("-{n}");
let (start, end) = bytes_range(range(&req), max_len).unwrap();
prop_assert_eq!((start, end), (0, max_len));
}
#[test]
fn prop_successful_slice_is_in_bounds(
max_len in 1u64..=10_000,
spec in "(\\PC{0,32})",
) {
let mut map = HeaderMap::new();
if let Ok(value) = format!("bytes={spec}").parse() {
map.insert(http::header::RANGE, value);
let parsed = map.typed_get::<Range>();
if let Ok((start, end)) = bytes_range(parsed, max_len) {
prop_assert!(start < end, "empty slice: {start}-{end}");
prop_assert!(end <= max_len, "end > max_len: {end} > {max_len}");
}
}
}
#[test]
fn prop_out_of_bounds_first_byte_is_unsatisfiable(
max_len in 1u64..=10_000,
offset in 0u64..u64::MAX / 2,
) {
let start = max_len.saturating_add(offset);
let req = format!("{start}-");
prop_assert!(bytes_range(range(&req), max_len).is_err());
}
}
}