use std::time::SystemTime;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RangeSpec {
FromTo(u64, u64),
From(u64),
Suffix(u64),
}
pub(crate) fn parse(header: &str) -> Option<RangeSpec> {
let bytes_part = header.strip_prefix("bytes=")?.trim();
if bytes_part.contains(',') {
return None;
}
let (start, end) = bytes_part.split_once('-')?;
match (start.trim(), end.trim()) {
("", "") => None,
("", suffix) => suffix.parse().ok().map(RangeSpec::Suffix),
(start, "") => start.parse().ok().map(RangeSpec::From),
(start, end) => {
let s: u64 = start.parse().ok()?;
let e: u64 = end.parse().ok()?;
(s <= e).then_some(RangeSpec::FromTo(s, e))
}
}
}
pub(crate) fn resolve(spec: RangeSpec, total: u64) -> Option<(u64, u64)> {
if total == 0 {
return None;
}
match spec {
RangeSpec::FromTo(s, e) => (s < total).then(|| (s, e.min(total - 1))),
RangeSpec::From(s) => (s < total).then_some((s, total - 1)),
RangeSpec::Suffix(n) => {
if n == 0 {
None
} else {
let n = n.min(total);
Some((total - n, total - 1))
}
}
}
}
pub(crate) fn if_range_matches(
if_range: &str,
etag: Option<&str>,
last_modified: Option<SystemTime>,
) -> bool {
let trimmed = if_range.trim();
if trimmed.starts_with("W/") {
return false;
}
if let Some(et) = etag
&& trimmed == et
{
return true;
}
if let Some(modified) = last_modified
&& let Ok(date) = httpdate::parse_http_date(trimmed)
&& let (Ok(m), Ok(d)) = (
modified.duration_since(SystemTime::UNIX_EPOCH),
date.duration_since(SystemTime::UNIX_EPOCH),
)
&& m.as_secs() == d.as_secs()
{
return true;
}
false
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_basic_forms() {
assert_eq!(parse("bytes=0-499"), Some(RangeSpec::FromTo(0, 499)));
assert_eq!(parse("bytes=500-"), Some(RangeSpec::From(500)));
assert_eq!(parse("bytes=-50"), Some(RangeSpec::Suffix(50)));
}
#[test]
fn parse_rejects() {
assert_eq!(parse("bytes=0-100,200-300"), None);
assert_eq!(parse("seconds=0-10"), None);
assert_eq!(parse("bytes=500-100"), None);
assert_eq!(parse("bytes=abc-def"), None);
assert_eq!(parse("bytes=-"), None);
}
#[test]
fn resolve_basics() {
assert_eq!(resolve(RangeSpec::FromTo(0, 99), 1000), Some((0, 99)));
assert_eq!(resolve(RangeSpec::FromTo(0, 99999), 1000), Some((0, 999)));
assert_eq!(resolve(RangeSpec::FromTo(1000, 2000), 1000), None);
assert_eq!(resolve(RangeSpec::From(500), 1000), Some((500, 999)));
assert_eq!(resolve(RangeSpec::From(1000), 1000), None);
assert_eq!(resolve(RangeSpec::Suffix(100), 1000), Some((900, 999)));
assert_eq!(resolve(RangeSpec::Suffix(2000), 1000), Some((0, 999)));
assert_eq!(resolve(RangeSpec::Suffix(0), 1000), None);
}
#[test]
fn if_range_etag() {
assert!(if_range_matches("\"abc\"", Some("\"abc\""), None));
assert!(!if_range_matches("\"xyz\"", Some("\"abc\""), None));
assert!(!if_range_matches("W/\"abc\"", Some("W/\"abc\""), None));
}
#[test]
fn if_range_date() {
let modified = httpdate::parse_http_date("Wed, 21 Oct 2026 07:28:00 GMT").unwrap();
assert!(if_range_matches(
"Wed, 21 Oct 2026 07:28:00 GMT",
None,
Some(modified)
));
assert!(!if_range_matches(
"Wed, 21 Oct 2026 07:28:01 GMT",
None,
Some(modified)
));
}
}