Skip to content

Commit

Permalink
fix: Range suffixes are not Rust RangeTo
Browse files Browse the repository at this point in the history
An HTTP Range of `bytes=-100` means a suffix, the last 100 bytes. This
was wrongly parsed as the Rust range `..100`, which means the first 100
bytes.

This has been fixed, but doing so required change `Range::iter` to
accept a length argument, to determine if the ranges are satisfiable.

BREAKING CHANGE: Change `.iter()` calls to `.satisfiable_ranges(len)`.
  Also, the `Range::bytes()` constructor will now return an error if
  pass a `RangeTo` (e.g. `Range::bytes(..100)`).
  • Loading branch information
seanmonstar committed Nov 23, 2023
1 parent 7d784cd commit fb9a4e0
Showing 1 changed file with 35 additions and 6 deletions.
41 changes: 35 additions & 6 deletions src/common/range.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,29 +48,50 @@ impl Range {
/// Creates a `Range` header from bounds.
pub fn bytes(bounds: impl RangeBounds<u64>) -> Result<Self, InvalidRange> {
let v = match (bounds.start_bound(), bounds.end_bound()) {
(Bound::Unbounded, Bound::Included(end)) => format!("bytes=-{}", end),
(Bound::Unbounded, Bound::Excluded(&end)) => format!("bytes=-{}", end - 1),
(Bound::Included(start), Bound::Included(end)) => format!("bytes={}-{}", start, end),
(Bound::Included(start), Bound::Excluded(&end)) => {
format!("bytes={}-{}", start, end - 1)
}
(Bound::Included(start), Bound::Unbounded) => format!("bytes={}-", start),
// These do not directly translate.
//(Bound::Unbounded, Bound::Included(end)) => format!("bytes=-{}", end),
//(Bound::Unbounded, Bound::Excluded(&end)) => format!("bytes=-{}", end - 1),

_ => return Err(InvalidRange { _inner: () }),
};

Ok(Range(::HeaderValue::from_str(&v).unwrap()))
}

/// Iterate the range sets as a tuple of bounds.
pub fn iter<'a>(&'a self) -> impl Iterator<Item = (Bound<u64>, Bound<u64>)> + 'a {
/// Iterate the range sets as a tuple of bounds, if valid with length.
///
/// The length of the content is passed as an argument, and all ranges
/// that can be satisfied will be iterated.
pub fn satisfiable_ranges<'a>(&'a self, len: u64) -> impl Iterator<Item = (Bound<u64>, Bound<u64>)> + 'a {
let s = self
.0
.to_str()
.expect("valid string checked in Header::decode()");

s["bytes=".len()..].split(',').filter_map(|spec| {
s["bytes=".len()..].split(',').filter_map(move |spec| {
let mut iter = spec.trim().splitn(2, '-');
Some((parse_bound(iter.next()?)?, parse_bound(iter.next()?)?))
let start = parse_bound(iter.next()?)?;
let end = parse_bound(iter.next()?)?;

// Unbounded ranges in HTTP are actually a suffix
// For example, `-100` means the last 100 bytes.
if let Bound::Unbounded = start {
if let Bound::Included(end) = end {
if len < end {
// Last N bytes is larger than available!
return None;
}
return Some((Bound::Included(len - end), Bound::Unbounded));
}
// else fall through
}

Some((start, end))
})
}
}
Expand Down Expand Up @@ -416,3 +437,11 @@ fn test_byte_range_spec_to_satisfiable_range() {
bench_header!(bytes_multi, Range, { vec![b"bytes=1-1001,2001-3001,10001-".to_vec()]});
bench_header!(custom_unit, Range, { vec![b"other=0-100000".to_vec()]});
*/

#[test]
fn test_to_satisfiable_range_suffix() {
let range = super::test_decode::<Range>(&["bytes=-100"]).unwrap();
let bounds = range.satisfiable_ranges(350).next().unwrap();
assert_eq!(bounds, (Bound::Included(250), Bound::Unbounded));
}

0 comments on commit fb9a4e0

Please sign in to comment.