Skip to content

Commit f2c4aba

Browse files
authored
fix: Range suffixes are not Rust RangeTo (hyperium#155)
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)`).
1 parent 7d784cd commit f2c4aba

File tree

1 file changed

+43
-6
lines changed

1 file changed

+43
-6
lines changed

src/common/range.rs

+43-6
Original file line numberDiff line numberDiff line change
@@ -48,29 +48,52 @@ impl Range {
4848
/// Creates a `Range` header from bounds.
4949
pub fn bytes(bounds: impl RangeBounds<u64>) -> Result<Self, InvalidRange> {
5050
let v = match (bounds.start_bound(), bounds.end_bound()) {
51-
(Bound::Unbounded, Bound::Included(end)) => format!("bytes=-{}", end),
52-
(Bound::Unbounded, Bound::Excluded(&end)) => format!("bytes=-{}", end - 1),
5351
(Bound::Included(start), Bound::Included(end)) => format!("bytes={}-{}", start, end),
5452
(Bound::Included(start), Bound::Excluded(&end)) => {
5553
format!("bytes={}-{}", start, end - 1)
5654
}
5755
(Bound::Included(start), Bound::Unbounded) => format!("bytes={}-", start),
56+
// These do not directly translate.
57+
//(Bound::Unbounded, Bound::Included(end)) => format!("bytes=-{}", end),
58+
//(Bound::Unbounded, Bound::Excluded(&end)) => format!("bytes=-{}", end - 1),
5859
_ => return Err(InvalidRange { _inner: () }),
5960
};
6061

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

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

71-
s["bytes=".len()..].split(',').filter_map(|spec| {
78+
s["bytes=".len()..].split(',').filter_map(move |spec| {
7279
let mut iter = spec.trim().splitn(2, '-');
73-
Some((parse_bound(iter.next()?)?, parse_bound(iter.next()?)?))
80+
let start = parse_bound(iter.next()?)?;
81+
let end = parse_bound(iter.next()?)?;
82+
83+
// Unbounded ranges in HTTP are actually a suffix
84+
// For example, `-100` means the last 100 bytes.
85+
if let Bound::Unbounded = start {
86+
if let Bound::Included(end) = end {
87+
if len < end {
88+
// Last N bytes is larger than available!
89+
return None;
90+
}
91+
return Some((Bound::Included(len - end), Bound::Unbounded));
92+
}
93+
// else fall through
94+
}
95+
96+
Some((start, end))
7497
})
7598
}
7699
}
@@ -416,3 +439,17 @@ fn test_byte_range_spec_to_satisfiable_range() {
416439
bench_header!(bytes_multi, Range, { vec![b"bytes=1-1001,2001-3001,10001-".to_vec()]});
417440
bench_header!(custom_unit, Range, { vec![b"other=0-100000".to_vec()]});
418441
*/
442+
443+
#[test]
444+
fn test_to_satisfiable_range_suffix() {
445+
let range = super::test_decode::<Range>(&["bytes=-100"]).unwrap();
446+
let bounds = range.satisfiable_ranges(350).next().unwrap();
447+
assert_eq!(bounds, (Bound::Included(250), Bound::Unbounded));
448+
}
449+
450+
#[test]
451+
fn test_to_unsatisfiable_range_suffix() {
452+
let range = super::test_decode::<Range>(&["bytes=-350"]).unwrap();
453+
let bounds = range.satisfiable_ranges(100).next();
454+
assert_eq!(bounds, None);
455+
}

0 commit comments

Comments
 (0)