#[derive(Debug, PartialEq, Eq)]
pub enum Resolved {
Whole,
Part { start: u64, end: u64 },
Unsatisfiable,
}
pub fn resolve(header: &str, if_range: Option<&str>, size: u64) -> Resolved {
if if_range.is_some() {
return Resolved::Whole;
}
let Some(spec) = header.trim().strip_prefix("bytes=") else {
return Resolved::Whole;
};
if spec.contains(',') {
return Resolved::Whole;
}
let Some((first, last)) = spec.split_once('-') else {
return Resolved::Whole;
};
let (first, last) = (first.trim(), last.trim());
if size == 0 {
return Resolved::Unsatisfiable;
}
let final_byte = size - 1;
match (first.is_empty(), last.is_empty()) {
(true, false) => {
let Ok(suffix) = last.parse::<u64>() else {
return Resolved::Whole;
};
if suffix == 0 {
return Resolved::Unsatisfiable;
}
Resolved::Part {
start: size.saturating_sub(suffix),
end: final_byte,
}
}
(false, true) => {
let Ok(start) = first.parse::<u64>() else {
return Resolved::Whole;
};
if start > final_byte {
return Resolved::Unsatisfiable;
}
Resolved::Part {
start,
end: final_byte,
}
}
(false, false) => {
let (Ok(start), Ok(end)) = (first.parse::<u64>(), last.parse::<u64>()) else {
return Resolved::Whole;
};
if start > end || start > final_byte {
return Resolved::Unsatisfiable;
}
Resolved::Part {
start,
end: end.min(final_byte),
}
}
(true, true) => Resolved::Whole,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn part(start: u64, end: u64) -> Resolved {
Resolved::Part { start, end }
}
#[test]
fn both_ends_given() {
assert_eq!(resolve("bytes=0-9", None, 100), part(0, 9));
assert_eq!(resolve("bytes=10-19", None, 100), part(10, 19));
assert_eq!(resolve("bytes=99-99", None, 100), part(99, 99));
}
#[test]
fn an_end_past_the_file_is_clamped_rather_than_refused() {
assert_eq!(resolve("bytes=0-1000", None, 100), part(0, 99));
assert_eq!(resolve("bytes=50-1000", None, 100), part(50, 99));
}
#[test]
fn an_open_ended_range_runs_to_the_last_byte() {
assert_eq!(resolve("bytes=90-", None, 100), part(90, 99));
assert_eq!(resolve("bytes=0-", None, 100), part(0, 99));
}
#[test]
fn a_suffix_range_counts_back_from_the_end() {
assert_eq!(resolve("bytes=-10", None, 100), part(90, 99));
assert_eq!(resolve("bytes=-500", None, 100), part(0, 99));
}
#[test]
fn ranges_outside_the_file_are_unsatisfiable() {
assert_eq!(resolve("bytes=100-", None, 100), Resolved::Unsatisfiable);
assert_eq!(resolve("bytes=100-200", None, 100), Resolved::Unsatisfiable);
assert_eq!(resolve("bytes=5-3", None, 100), Resolved::Unsatisfiable);
assert_eq!(resolve("bytes=-0", None, 100), Resolved::Unsatisfiable);
}
#[test]
fn an_empty_file_satisfies_nothing() {
assert_eq!(resolve("bytes=0-0", None, 0), Resolved::Unsatisfiable);
assert_eq!(resolve("bytes=0-", None, 0), Resolved::Unsatisfiable);
assert_eq!(resolve("bytes=-1", None, 0), Resolved::Unsatisfiable);
}
#[test]
fn a_multi_range_request_is_answered_whole() {
assert_eq!(resolve("bytes=0-9,20-29", None, 100), Resolved::Whole);
}
#[test]
fn an_unknown_unit_or_malformed_spec_is_answered_whole() {
assert_eq!(resolve("items=0-9", None, 100), Resolved::Whole);
assert_eq!(resolve("bytes=abc-def", None, 100), Resolved::Whole);
assert_eq!(resolve("bytes=-", None, 100), Resolved::Whole);
assert_eq!(resolve("nonsense", None, 100), Resolved::Whole);
assert_eq!(resolve("", None, 100), Resolved::Whole);
}
#[test]
fn if_range_is_never_honoured_because_the_validator_is_weak() {
assert_eq!(
resolve("bytes=0-9", Some("W/\"64-7\""), 100),
Resolved::Whole
);
assert_eq!(resolve("bytes=0-9", Some("\"64-7\""), 100), Resolved::Whole);
}
#[test]
fn whitespace_around_the_spec_is_tolerated() {
assert_eq!(resolve(" bytes=0-9 ", None, 100), part(0, 9));
assert_eq!(resolve("bytes= 10 - 19 ", None, 100), part(10, 19));
}
}