use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct ByteRange {
start: u64,
end_inclusive: u64,
}
impl ByteRange {
pub const fn new(start: u64, end_inclusive: u64) -> Result<Self, RangeError> {
if end_inclusive < start {
return Err(RangeError::Inverted);
}
Ok(Self {
start,
end_inclusive,
})
}
#[must_use]
pub const fn start(&self) -> u64 {
self.start
}
#[must_use]
pub const fn end_inclusive(&self) -> u64 {
self.end_inclusive
}
#[must_use]
pub const fn len(&self) -> Option<u64> {
match self.end_inclusive.checked_sub(self.start) {
Some(offset) => offset.checked_add(1),
None => None,
}
}
#[must_use]
pub const fn is_empty(&self) -> bool {
false
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChunkRange {
start: u32,
end_exclusive: u32,
}
impl ChunkRange {
pub const fn new(start: u32, end_exclusive: u32) -> Result<Self, RangeError> {
if end_exclusive < start {
return Err(RangeError::Inverted);
}
if end_exclusive == start {
return Err(RangeError::Empty);
}
Ok(Self {
start,
end_exclusive,
})
}
#[must_use]
pub const fn start(self) -> u32 {
self.start
}
#[must_use]
pub const fn end_exclusive(self) -> u32 {
self.end_exclusive
}
}
#[derive(Debug, Clone, Copy, Error, PartialEq, Eq)]
pub enum RangeError {
#[error("range end must not be smaller than range start")]
Inverted,
#[error("chunk range must contain at least one chunk")]
Empty,
}
#[derive(Debug, Clone, Error, PartialEq, Eq)]
pub enum HttpRangeParseError {
#[error("range header must use bytes=<start>-<end> syntax")]
MissingBytesUnit,
#[error("range header must use bytes=<start>-<end> syntax")]
InvalidSyntax,
#[error("range header contained an invalid number")]
InvalidNumber,
#[error("requested range is not satisfiable")]
Unsatisfiable,
}
pub fn parse_http_byte_range(
value: &str,
resource_length: u64,
) -> Result<ByteRange, HttpRangeParseError> {
let Some(raw_suffix) = value.strip_prefix("bytes=") else {
return Err(HttpRangeParseError::MissingBytesUnit);
};
if raw_suffix.is_empty() {
return Err(HttpRangeParseError::InvalidSyntax);
}
if raw_suffix.contains(',') {
return Err(HttpRangeParseError::InvalidSyntax);
}
let mut parts = raw_suffix.splitn(2, '-');
let Some(raw_start) = parts.next() else {
return Err(HttpRangeParseError::InvalidSyntax);
};
let Some(raw_end) = parts.next() else {
return Err(HttpRangeParseError::InvalidSyntax);
};
if raw_start.is_empty() {
return Err(HttpRangeParseError::InvalidSyntax);
}
let start = raw_start
.parse::<u64>()
.map_err(|_error| HttpRangeParseError::InvalidNumber)?;
if start >= resource_length {
return Err(HttpRangeParseError::Unsatisfiable);
}
let last_byte = resource_length
.checked_sub(1)
.ok_or(HttpRangeParseError::Unsatisfiable)?;
let parsed_end = if raw_end.is_empty() {
last_byte
} else {
raw_end
.parse::<u64>()
.map_err(|_error| HttpRangeParseError::InvalidNumber)?
};
let end_inclusive = parsed_end.min(last_byte);
ByteRange::new(start, end_inclusive).map_err(|_error| HttpRangeParseError::InvalidSyntax)
}
#[cfg(test)]
mod tests {
use super::{ByteRange, ChunkRange, HttpRangeParseError, RangeError, parse_http_byte_range};
#[test]
fn byte_range_is_inclusive() {
let range = ByteRange::new(10, 20);
assert!(range.is_ok());
if let Ok(value) = range {
assert_eq!(value.start(), 10);
assert_eq!(value.end_inclusive(), 20);
assert_eq!(value.len(), Some(11));
assert!(!value.is_empty());
}
}
#[test]
fn byte_range_rejects_inverted_input() {
let range = ByteRange::new(20, 10);
assert_eq!(range, Err(RangeError::Inverted));
}
#[test]
fn byte_range_reports_unrepresentable_full_u64_length() {
let range = ByteRange::new(0, u64::MAX);
assert!(range.is_ok());
if let Ok(value) = range {
assert_eq!(value.len(), None);
}
}
#[test]
fn chunk_range_rejects_empty_ranges() {
let range = ChunkRange::new(4, 4);
assert_eq!(range, Err(RangeError::Empty));
}
#[test]
fn chunk_range_rejects_inverted_ranges() {
let range = ChunkRange::new(5, 4);
assert_eq!(range, Err(RangeError::Inverted));
}
#[test]
fn chunk_range_is_end_exclusive() {
let range = ChunkRange::new(4, 9);
assert!(range.is_ok());
if let Ok(value) = range {
assert_eq!(value.start(), 4);
assert_eq!(value.end_exclusive(), 9);
}
}
#[test]
fn http_byte_range_parses_inclusive_range() {
let parsed = parse_http_byte_range("bytes=10-20", 100);
let expected = ByteRange::new(10, 20);
assert!(expected.is_ok());
assert_eq!(
parsed,
expected.map_err(|_error| HttpRangeParseError::InvalidSyntax)
);
}
#[test]
fn http_byte_range_clamps_open_or_oversized_end_to_resource() {
let open_ended = parse_http_byte_range("bytes=10-", 25);
let oversized = parse_http_byte_range("bytes=10-999", 25);
let expected = ByteRange::new(10, 24);
assert!(expected.is_ok());
assert_eq!(
open_ended,
expected.map_err(|_error| HttpRangeParseError::InvalidSyntax)
);
assert_eq!(
oversized,
expected.map_err(|_error| HttpRangeParseError::InvalidSyntax)
);
}
#[test]
fn http_byte_range_accepts_single_byte_and_final_byte_vectors() {
let first_byte = parse_http_byte_range("bytes=0-0", 10);
let final_byte = parse_http_byte_range("bytes=9-9", 10);
assert_eq!(
first_byte,
ByteRange::new(0, 0).map_err(|_error| HttpRangeParseError::InvalidSyntax)
);
assert_eq!(
final_byte,
ByteRange::new(9, 9).map_err(|_error| HttpRangeParseError::InvalidSyntax)
);
}
#[test]
fn http_byte_range_rejects_invalid_syntax() {
assert_eq!(
parse_http_byte_range("items=0-1", 10),
Err(HttpRangeParseError::MissingBytesUnit)
);
assert_eq!(
parse_http_byte_range("bytes=-10", 10),
Err(HttpRangeParseError::InvalidSyntax)
);
assert_eq!(
parse_http_byte_range("bytes=1-2,4-5", 10),
Err(HttpRangeParseError::InvalidSyntax)
);
assert_eq!(
parse_http_byte_range("bytes=2-1", 10),
Err(HttpRangeParseError::InvalidSyntax)
);
assert_eq!(
parse_http_byte_range("bytes= 1-2", 10),
Err(HttpRangeParseError::InvalidNumber)
);
assert_eq!(
parse_http_byte_range("bytes=1 -2", 10),
Err(HttpRangeParseError::InvalidNumber)
);
}
#[test]
fn http_byte_range_rejects_suffix_and_multi_range_forms() {
assert_eq!(
parse_http_byte_range("bytes=-1", 10),
Err(HttpRangeParseError::InvalidSyntax)
);
assert_eq!(
parse_http_byte_range("bytes=0-0,1-1", 10),
Err(HttpRangeParseError::InvalidSyntax)
);
}
#[test]
fn http_byte_range_rejects_unsatisfiable_start() {
assert_eq!(
parse_http_byte_range("bytes=10-20", 10),
Err(HttpRangeParseError::Unsatisfiable)
);
assert_eq!(
parse_http_byte_range("bytes=0-0", 0),
Err(HttpRangeParseError::Unsatisfiable)
);
}
}