#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct ContentRange {
pub first: u64,
pub last: u64,
pub complete_length: Option<u64>,
}
impl ContentRange {
#[must_use]
pub fn parse(value: &str) -> Option<Self> {
let value = value.trim_matches(is_ows);
let (unit, rest) = value.split_once(' ')?;
if !unit.eq_ignore_ascii_case("bytes") {
return None;
}
let (range, length) = rest.split_once('/')?;
let (first, last) = range.split_once('-')?;
let first = digits(first)?;
let last = digits(last)?;
let complete_length = match length {
"*" => None,
n => Some(digits(n)?),
};
if first > last || complete_length.is_some_and(|length| last >= length) {
return None;
}
Some(ContentRange {
first,
last,
complete_length,
})
}
#[must_use]
pub fn len(&self) -> Option<u64> {
(self.last - self.first).checked_add(1)
}
#[must_use]
pub fn is_empty(&self) -> bool {
false
}
}
fn is_ows(c: char) -> bool {
c == ' ' || c == '\t'
}
fn digits(s: &str) -> Option<u64> {
if s.is_empty() || !s.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
s.parse().ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_known_length_range() {
let r = ContentRange::parse("bytes 0-9/10").unwrap();
assert_eq!((r.first, r.last, r.complete_length), (0, 9, Some(10)));
assert_eq!(r.len(), Some(10));
}
#[test]
fn an_unknown_length_is_valid() {
let r = ContentRange::parse("bytes 0-9/*").unwrap();
assert_eq!((r.first, r.last, r.complete_length), (0, 9, None));
}
#[test]
fn a_last_position_at_or_past_the_length_is_invalid() {
assert!(ContentRange::parse("bytes 5-20/10").is_none());
assert!(ContentRange::parse("bytes 0-10/10").is_none());
assert!(ContentRange::parse("bytes 0-9/10").is_some());
}
#[test]
fn a_reversed_range_the_unsatisfied_form_and_other_units_are_none() {
for bad in [
"bytes 9-0/10",
"bytes */10",
"items 0-9/10",
"bytes 0-9",
"bytes -9/10",
"bytes 0-/10",
"bytes 0-9/",
"bytes +0-9/10",
] {
assert!(ContentRange::parse(bad).is_none(), "{bad}");
}
}
#[test]
fn stray_whitespace_inside_the_value_is_invalid() {
assert!(ContentRange::parse("bytes 0-9/10").is_none());
assert!(ContentRange::parse("bytes 0-9 / 10").is_none());
assert!(ContentRange::parse("bytes 0 - 9/10").is_none());
assert!(ContentRange::parse(" bytes 0-9/10 ").is_some());
}
#[test]
fn the_unit_is_case_insensitive_and_overflow_is_none() {
assert!(ContentRange::parse("BYTES 0-9/10").is_some());
assert!(ContentRange::parse("bytes 0-18446744073709551616/*").is_none());
assert_eq!(
ContentRange::parse("bytes 0-18446744073709551615/*")
.unwrap()
.len(),
None
);
}
}