use azure_core::error::{Error, ErrorKind, ResultExt};
use azure_core::http::headers::{Header, HeaderName, HeaderValue};
use std::fmt;
use std::ops::{Range, RangeFrom, RangeInclusive, RangeTo, RangeToInclusive};
use std::str::FromStr;
const PREFIX: &str = "bytes ";
const WILDCARD: &str = "*";
const CONTENT_RANGE_ID: HeaderName = HeaderName::from_static("content-range");
type Result<T> = azure_core::Result<T>;
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub(crate) struct ContentRange {
pub range: Option<(usize, usize)>,
pub total_len: Option<usize>,
}
impl Header for ContentRange {
fn name(&self) -> azure_core::http::headers::HeaderName {
CONTENT_RANGE_ID
}
fn value(&self) -> azure_core::http::headers::HeaderValue {
let range_str = match self.range {
Some(range) => format!("{}-{}", range.0, range.1),
None => WILDCARD.to_string(),
};
let len_str = match self.total_len {
Some(len) => len.to_string(),
None => WILDCARD.to_string(),
};
format!("{}{}/{}", PREFIX, range_str, len_str).into()
}
}
impl FromStr for ContentRange {
type Err = Error;
fn from_str(s: &str) -> Result<ContentRange> {
let remaining = s.strip_prefix(PREFIX).ok_or_else(|| {
Error::with_message_fn(ErrorKind::Other, || {
format!(
"expected token \"{PREFIX}\" not found when parsing ContentRange from \"{s}\""
)
})
})?;
let mut split_at_slash = remaining.split('/');
let range = parse_range(split_at_slash.next().ok_or_else(|| {
Error::with_message(ErrorKind::Other, "Unexpected end of Content-Range.")
})?)?;
let total_len = parse_total_length(split_at_slash.next().ok_or_else(|| {
Error::with_message_fn(ErrorKind::Other, || {
format!(
"expected token \"{}\" not found when parsing ContentRange from \"{}\"",
"/", s
)
})
})?)?;
Ok(ContentRange { range, total_len })
}
}
fn parse_range(s: &str) -> Result<Option<(usize, usize)>> {
let s = s.trim();
if s == WILDCARD {
return Ok(None);
}
let mut split_at_dash = s.split('-');
let start = split_at_dash
.next()
.ok_or_else(|| Error::with_message(ErrorKind::Other, "Unexpected end of Content-Range."))?
.parse::<usize>()
.with_kind(ErrorKind::DataConversion)?;
let end = split_at_dash
.next()
.ok_or_else(|| {
Error::with_message_fn(ErrorKind::Other, || {
format!(
"expected token \"{}\" not found when parsing ContentRange from \"{}\"",
"-", s
)
})
})?
.parse::<usize>()
.with_kind(ErrorKind::DataConversion)?;
Ok(Some((start, end + 1)))
}
fn parse_total_length(s: &str) -> Result<Option<usize>> {
let s = s.trim();
if s == WILDCARD {
return Ok(None);
}
Ok(Some(s.parse().with_kind(ErrorKind::DataConversion)?))
}
impl fmt::Display for ContentRange {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}{}/{}",
PREFIX,
self.range
.map(|range| format!("{}-{}", range.0, range.1 - 1))
.unwrap_or(WILDCARD.into()),
self.total_len
.map(|len| len.to_string())
.unwrap_or(WILDCARD.into()),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse() {
let range = "bytes 172032-172489/172490"
.parse::<ContentRange>()
.unwrap();
assert_eq!(range.range.unwrap().0, 172032);
assert_eq!(range.range.unwrap().1, 172490);
assert_eq!(range.total_len.unwrap(), 172490);
}
#[test]
fn parse_no_starting_token() {
"something else".parse::<ContentRange>().unwrap_err();
}
#[test]
fn parse_no_dash() {
"bytes 100".parse::<ContentRange>().unwrap_err();
}
#[test]
fn parse_no_slash() {
"bytes 100-500".parse::<ContentRange>().unwrap_err();
}
#[test]
fn display() {
let range = ContentRange {
range: Some((100, 500)),
total_len: Some(5000),
};
let txt = format!("{range}");
assert_eq!(txt, "bytes 100-499/5000");
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HttpRange {
offset: u64,
length: Option<u64>,
}
impl HttpRange {
pub fn new(offset: u64, length: u64) -> Self {
Self {
offset,
length: Some(length),
}
}
pub fn from_offset(offset: u64) -> Self {
Self {
offset,
length: None,
}
}
pub(crate) fn offset(&self) -> u64 {
self.offset
}
pub(crate) fn length(&self) -> Option<u64> {
self.length
}
}
impl fmt::Display for HttpRange {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.length {
Some(length) => write!(
f,
"bytes={}-{}",
self.offset,
self.offset.saturating_add(length).saturating_sub(1)
),
None => write!(f, "bytes={}-", self.offset),
}
}
}
impl From<HttpRange> for HeaderValue {
fn from(range: HttpRange) -> Self {
HeaderValue::from(range.to_string())
}
}
impl From<Range<u64>> for HttpRange {
fn from(range: Range<u64>) -> Self {
Self::new(range.start, range.end - range.start)
}
}
impl From<RangeFrom<u64>> for HttpRange {
fn from(range: RangeFrom<u64>) -> Self {
Self::from_offset(range.start)
}
}
impl From<RangeInclusive<u64>> for HttpRange {
fn from(range: RangeInclusive<u64>) -> Self {
Self::new(*range.start(), range.end() - range.start() + 1)
}
}
impl From<RangeTo<u64>> for HttpRange {
fn from(range: RangeTo<u64>) -> Self {
Self::new(0, range.end)
}
}
impl From<RangeToInclusive<u64>> for HttpRange {
fn from(range: RangeToInclusive<u64>) -> Self {
Self::new(0, range.end + 1)
}
}
impl From<Range<usize>> for HttpRange {
fn from(range: Range<usize>) -> Self {
Self::new(range.start as u64, (range.end - range.start) as u64)
}
}
impl From<RangeFrom<usize>> for HttpRange {
fn from(range: RangeFrom<usize>) -> Self {
Self::from_offset(range.start as u64)
}
}
impl From<RangeInclusive<usize>> for HttpRange {
fn from(range: RangeInclusive<usize>) -> Self {
Self::new(
*range.start() as u64,
(range.end() - range.start() + 1) as u64,
)
}
}
impl From<RangeTo<usize>> for HttpRange {
fn from(range: RangeTo<usize>) -> Self {
Self::new(0, range.end as u64)
}
}
impl From<RangeToInclusive<usize>> for HttpRange {
fn from(range: RangeToInclusive<usize>) -> Self {
Self::new(0, (range.end + 1) as u64)
}
}
#[cfg(test)]
mod http_range_tests {
use super::*;
#[test]
fn new_creates_bounded_range() {
let range = HttpRange::new(0, 512);
assert_eq!(range.to_string(), "bytes=0-511");
}
#[test]
fn from_offset_creates_open_ended_range() {
let range = HttpRange::from_offset(255);
assert_eq!(range.to_string(), "bytes=255-");
}
#[test]
fn display_bounded_range() {
let range = HttpRange::new(0, 512);
assert_eq!(range.to_string(), "bytes=0-511");
}
#[test]
fn display_open_ended_range() {
let range = HttpRange::from_offset(255);
assert_eq!(range.to_string(), "bytes=255-");
}
#[test]
fn to_string_bounded_range() {
let range = HttpRange::new(100, 101);
assert_eq!(range.to_string(), "bytes=100-200");
}
#[test]
fn into_header_value() {
let range = HttpRange::new(0, 512);
let header_value: HeaderValue = range.into();
assert_eq!(header_value.as_str(), "bytes=0-511");
}
#[test]
fn display_zero_length_does_not_panic() {
let range = HttpRange::new(0, 0);
let _ = range.to_string();
}
#[test]
fn display_overflow_does_not_panic() {
let range = HttpRange::new(u64::MAX, u64::MAX);
let _ = range.to_string();
}
#[test]
fn from_range_u64() {
let range: HttpRange = (0u64..100).into();
assert_eq!(range.to_string(), "bytes=0-99");
}
#[test]
fn from_range_from_u64() {
let range: HttpRange = (100u64..).into();
assert_eq!(range.to_string(), "bytes=100-");
}
#[test]
fn from_range_inclusive_u64() {
let range: HttpRange = (0u64..=99).into();
assert_eq!(range.to_string(), "bytes=0-99");
}
#[test]
fn from_range_to_u64() {
let range: HttpRange = (..100u64).into();
assert_eq!(range.to_string(), "bytes=0-99");
}
#[test]
fn from_range_to_inclusive_u64() {
let range: HttpRange = (..=99u64).into();
assert_eq!(range.to_string(), "bytes=0-99");
}
#[test]
fn from_range_usize() {
let range: HttpRange = (0usize..100).into();
assert_eq!(range.to_string(), "bytes=0-99");
}
#[test]
fn from_range_from_usize() {
let range: HttpRange = (100usize..).into();
assert_eq!(range.to_string(), "bytes=100-");
}
#[test]
fn from_range_inclusive_usize() {
let range: HttpRange = (0usize..=99).into();
assert_eq!(range.to_string(), "bytes=0-99");
}
#[test]
fn from_range_to_usize() {
let range: HttpRange = (..100usize).into();
assert_eq!(range.to_string(), "bytes=0-99");
}
#[test]
fn from_range_to_inclusive_usize() {
let range: HttpRange = (..=99usize).into();
assert_eq!(range.to_string(), "bytes=0-99");
}
#[test]
fn from_range_nonzero_offset() {
let exclusive: HttpRange = (50u64..150).into();
let inclusive: HttpRange = (50u64..=149).into();
assert_eq!(exclusive.to_string(), "bytes=50-149");
assert_eq!(inclusive.to_string(), "bytes=50-149");
assert_eq!(exclusive, inclusive);
}
#[test]
fn from_range_single_byte() {
let exclusive: HttpRange = (42u64..43).into();
let inclusive: HttpRange = (42u64..=42).into();
assert_eq!(exclusive.to_string(), "bytes=42-42");
assert_eq!(inclusive.to_string(), "bytes=42-42");
assert_eq!(exclusive, inclusive);
}
}