use http::{HeaderMap, HeaderValue, StatusCode};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RangeRequest {
pub start: u64,
pub end: Option<u64>,
}
impl RangeRequest {
pub fn new(start: u64, end: Option<u64>) -> Self {
Self { start, end }
}
pub fn len(&self) -> Option<u64> {
self.end
.map(|end| end.saturating_sub(self.start).saturating_add(1))
}
pub fn is_empty(&self) -> bool {
self.len() == Some(0)
}
pub fn is_satisfiable(&self, total_size: u64) -> bool {
if self.start >= total_size {
return false;
}
if let Some(end) = self.end {
if end >= total_size || end < self.start {
return false;
}
}
true
}
pub fn normalize(&self, total_size: u64) -> Option<Self> {
if !self.is_satisfiable(total_size) {
return None;
}
let end = self.end.unwrap_or(total_size.saturating_sub(1));
let end = end.min(total_size.saturating_sub(1));
Some(Self {
start: self.start,
end: Some(end),
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RangeHandling {
#[default]
PassThrough,
CacheFullServeRanges,
#[allow(dead_code)]
CacheChunks,
}
pub fn parse_range_header(headers: &HeaderMap) -> Option<RangeRequest> {
let range_header = headers.get(http::header::RANGE)?;
let range_str = range_header.to_str().ok()?;
if !range_str.starts_with("bytes=") {
return None;
}
let range_spec = &range_str[6..];
let first_range = range_spec.split(',').next()?;
let parts: Vec<&str> = first_range.split('-').collect();
if parts.len() != 2 {
return None;
}
let start = parts[0].trim().parse::<u64>().ok()?;
let end = if parts[1].trim().is_empty() {
None
} else {
Some(parts[1].trim().parse::<u64>().ok()?)
};
Some(RangeRequest { start, end })
}
pub fn is_partial_content(status: StatusCode) -> bool {
status == StatusCode::PARTIAL_CONTENT
}
pub fn build_content_range_header(start: u64, end: u64, total: u64) -> HeaderValue {
let value = format!("bytes {}-{}/{}", start, end, total);
HeaderValue::from_str(&value).expect("Content-Range value should be valid")
}
pub fn parse_content_range_header(headers: &HeaderMap) -> Option<(u64, u64, u64)> {
let content_range = headers.get(http::header::CONTENT_RANGE)?;
let range_str = content_range.to_str().ok()?;
if !range_str.starts_with("bytes ") {
return None;
}
let range_spec = &range_str[6..]; let parts: Vec<&str> = range_spec.split('/').collect();
if parts.len() != 2 {
return None;
}
let range_parts: Vec<&str> = parts[0].split('-').collect();
if range_parts.len() != 2 {
return None;
}
let start = range_parts[0].trim().parse::<u64>().ok()?;
let end = range_parts[1].trim().parse::<u64>().ok()?;
let total = parts[1].trim().parse::<u64>().ok()?;
Some((start, end, total))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_range_with_start_and_end() {
let mut headers = HeaderMap::new();
headers.insert(http::header::RANGE, "bytes=0-1023".parse().unwrap());
let range = parse_range_header(&headers).unwrap();
assert_eq!(range.start, 0);
assert_eq!(range.end, Some(1023));
}
#[test]
fn test_parse_range_with_start_only() {
let mut headers = HeaderMap::new();
headers.insert(http::header::RANGE, "bytes=1024-".parse().unwrap());
let range = parse_range_header(&headers).unwrap();
assert_eq!(range.start, 1024);
assert_eq!(range.end, None);
}
#[test]
fn test_parse_range_missing_header() {
let headers = HeaderMap::new();
assert!(parse_range_header(&headers).is_none());
}
#[test]
fn test_parse_range_invalid_unit() {
let mut headers = HeaderMap::new();
headers.insert(http::header::RANGE, "items=0-10".parse().unwrap());
assert!(parse_range_header(&headers).is_none());
}
#[test]
fn test_parse_range_malformed() {
let mut headers = HeaderMap::new();
headers.insert(http::header::RANGE, "bytes=invalid".parse().unwrap());
assert!(parse_range_header(&headers).is_none());
}
#[test]
fn test_range_len() {
let range = RangeRequest::new(0, Some(1023));
assert_eq!(range.len(), Some(1024));
let range_open = RangeRequest::new(1024, None);
assert_eq!(range_open.len(), None);
}
#[test]
fn test_range_is_satisfiable() {
let range = RangeRequest::new(0, Some(1023));
assert!(range.is_satisfiable(10240));
assert!(!range.is_satisfiable(512));
let range_at_end = RangeRequest::new(10240, Some(10250));
assert!(!range_at_end.is_satisfiable(10240));
}
#[test]
fn test_range_normalize() {
let range = RangeRequest::new(0, None);
let normalized = range.normalize(10240).unwrap();
assert_eq!(normalized.start, 0);
assert_eq!(normalized.end, Some(10239));
let range_explicit = RangeRequest::new(0, Some(1023));
let normalized_explicit = range_explicit.normalize(10240).unwrap();
assert_eq!(normalized_explicit.start, 0);
assert_eq!(normalized_explicit.end, Some(1023));
}
#[test]
fn test_range_normalize_out_of_bounds() {
let range = RangeRequest::new(10240, Some(20000));
assert!(range.normalize(10240).is_none());
}
#[test]
fn test_is_partial_content() {
assert!(is_partial_content(StatusCode::PARTIAL_CONTENT));
assert!(!is_partial_content(StatusCode::OK));
assert!(!is_partial_content(StatusCode::NOT_FOUND));
}
#[test]
fn test_build_content_range_header() {
let header = build_content_range_header(0, 1023, 10240);
assert_eq!(header.to_str().unwrap(), "bytes 0-1023/10240");
}
#[test]
fn test_parse_content_range_header() {
let mut headers = HeaderMap::new();
headers.insert(
http::header::CONTENT_RANGE,
"bytes 0-1023/10240".parse().unwrap(),
);
let (start, end, total) = parse_content_range_header(&headers).unwrap();
assert_eq!(start, 0);
assert_eq!(end, 1023);
assert_eq!(total, 10240);
}
#[test]
fn test_parse_content_range_missing() {
let headers = HeaderMap::new();
assert!(parse_content_range_header(&headers).is_none());
}
#[test]
fn test_range_request_is_empty() {
let empty_range = RangeRequest::new(0, Some(0));
assert!(!empty_range.is_empty());
let range = RangeRequest::new(100, Some(99)); let len = range.len();
assert!(len.is_some());
}
#[test]
fn test_parse_range_with_whitespace() {
let mut headers = HeaderMap::new();
headers.insert(http::header::RANGE, "bytes= 0 - 1023 ".parse().unwrap());
if let Some(range) = parse_range_header(&headers) {
assert_eq!(range.start, 0);
assert_eq!(range.end, Some(1023));
}
}
#[test]
fn test_range_multiple_ranges_first_only() {
let mut headers = HeaderMap::new();
headers.insert(
http::header::RANGE,
"bytes=0-1023,1024-2047".parse().unwrap(),
);
let range = parse_range_header(&headers).unwrap();
assert_eq!(range.start, 0);
assert_eq!(range.end, Some(1023));
}
}