use crate::storage::Storage;
use axum::body::Body;
use axum::http::{header, HeaderMap, HeaderName, StatusCode};
use axum::response::{IntoResponse, Response};
use tokio_util::io::ReaderStream;
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum ParsedRange {
Satisfiable(u64, u64),
Unsatisfiable,
None,
}
pub(crate) fn parse_byte_range(value: &str, size: u64) -> ParsedRange {
parse_spec(value, size).unwrap_or(ParsedRange::None)
}
fn parse_spec(value: &str, size: u64) -> Option<ParsedRange> {
let spec = value.strip_prefix("bytes=")?.trim();
if spec.contains(',') {
return None;
}
let (s, e) = spec.split_once('-')?;
if s.is_empty() {
let n: u64 = e.trim().parse().ok()?;
Some(byte_range_core(true, 0, false, n, size))
} else {
let start: u64 = s.trim().parse().ok()?;
let (end_empty, end_in) = if e.trim().is_empty() {
(true, 0)
} else {
(false, e.trim().parse::<u64>().ok()?)
};
Some(byte_range_core(false, start, end_empty, end_in, size))
}
}
fn byte_range_core(
suffix: bool,
start_in: u64,
end_empty: bool,
end_in: u64,
size: u64,
) -> ParsedRange {
if suffix {
if end_in == 0 {
return ParsedRange::None;
}
if size == 0 {
return ParsedRange::Unsatisfiable;
}
return ParsedRange::Satisfiable(size.saturating_sub(end_in), size - 1);
}
if start_in >= size {
return ParsedRange::Unsatisfiable;
}
let end = if end_empty {
size - 1
} else {
end_in.min(size - 1)
};
if start_in > end {
return ParsedRange::None;
}
ParsedRange::Satisfiable(start_in, end)
}
#[cfg(kani)]
#[kani::proof]
fn byte_range_core_is_bounds_safe() {
let suffix: bool = kani::any();
let start_in: u64 = kani::any();
let end_empty: bool = kani::any();
let end_in: u64 = kani::any();
let size: u64 = kani::any();
if let ParsedRange::Satisfiable(start, end) =
byte_range_core(suffix, start_in, end_empty, end_in, size)
{
assert!(start <= end, "Range start must never exceed end");
assert!(end < size, "Range end must stay within the object size");
assert!(start < size, "Range start must stay within the object size");
}
}
pub(crate) async fn range_response(
storage: &Storage,
keys: &[&str],
headers: &HeaderMap,
size: u64,
content_type: &str,
extra: &[(HeaderName, String)],
) -> Option<Response> {
let (start, end) = match parse_byte_range(headers.get(header::RANGE)?.to_str().ok()?, size) {
ParsedRange::Satisfiable(start, end) => (start, end),
ParsedRange::Unsatisfiable => {
return Some(
Response::builder()
.status(StatusCode::RANGE_NOT_SATISFIABLE)
.header(header::CONTENT_RANGE, format!("bytes */{}", size))
.header(header::ACCEPT_RANGES, "bytes")
.body(Body::empty())
.unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response()),
)
}
ParsedRange::None => return None,
};
let mut reader = None;
for key in keys {
if let Ok((_, r)) = storage.get_range(key, start, end).await {
reader = Some(r);
break;
}
}
let mut response = Response::builder()
.status(StatusCode::PARTIAL_CONTENT)
.header(header::CONTENT_TYPE, content_type)
.header(header::CONTENT_LENGTH, end - start + 1)
.header(
header::CONTENT_RANGE,
format!("bytes {}-{}/{}", start, end, size),
)
.header(header::ACCEPT_RANGES, "bytes");
for (name, value) in extra {
response = response.header(name, value.as_str());
}
Some(
response
.body(Body::from_stream(ReaderStream::new(reader?)))
.unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR.into_response()),
)
}
#[cfg(test)]
mod tests {
use super::{parse_byte_range, ParsedRange};
use ParsedRange::{None as Ignore, Satisfiable, Unsatisfiable};
#[test]
fn test_parse_byte_range() {
assert_eq!(parse_byte_range("bytes=0-3", 10), Satisfiable(0, 3));
assert_eq!(parse_byte_range("bytes=5-", 10), Satisfiable(5, 9)); assert_eq!(parse_byte_range("bytes=-4", 10), Satisfiable(6, 9)); assert_eq!(parse_byte_range("bytes=8-100", 10), Satisfiable(8, 9)); assert_eq!(parse_byte_range("bytes=10-12", 10), Unsatisfiable); assert_eq!(parse_byte_range("bytes=5-3", 10), Ignore); assert_eq!(parse_byte_range("nonsense", 10), Ignore); assert_eq!(parse_byte_range("bytes=0-1,4-5", 10), Ignore); assert_eq!(parse_byte_range("bytes=0-3", 0), Unsatisfiable);
assert_eq!(parse_byte_range("bytes=10-", 10), Unsatisfiable);
assert_eq!(parse_byte_range("bytes=5-5", 10), Satisfiable(5, 5)); assert_eq!(parse_byte_range("bytes=-5", 0), Unsatisfiable); }
proptest::proptest! {
#[test]
fn parse_byte_range_lexer_invariant(
value in "bytes=-?[0-9]{0,9}-?[0-9]{0,9}",
size in proptest::prelude::any::<u64>(),
) {
if let Satisfiable(s, e) = parse_byte_range(&value, size) {
proptest::prop_assert!(s <= e, "inverted: {} > {}", s, e);
proptest::prop_assert!(e < size, "oob end: {} >= {}", e, size);
proptest::prop_assert!(s < size, "oob start: {} >= {}", s, size);
}
}
}
}