#[cfg(feature = "axum")]
use std::convert::Infallible;
use std::str::FromStr;
use http::HeaderValue;
use crate::headers::range::HttpRange;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IfRange {
Date(HeaderValue),
ETag(HeaderValue),
}
impl IfRange {
pub fn evaluate(
&self,
range: HttpRange,
last_modified: Option<&HeaderValue>,
etag: Option<&HeaderValue>,
) -> Option<HttpRange> {
let matches = match self {
IfRange::Date(date) => last_modified.is_some_and(|lm| lm == date),
IfRange::ETag(tag) => etag.is_some_and(|et| strong_etag_eq(tag, et)),
};
if matches { Some(range) } else { None }
}
}
fn strong_etag_eq(a: &HeaderValue, b: &HeaderValue) -> bool {
let a = a.as_bytes();
let b = b.as_bytes();
if a.starts_with(b"W/") || b.starts_with(b"W/") {
return false;
}
a == b
}
impl FromStr for IfRange {
type Err = InvalidIfRange;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim();
if s.is_empty() {
return Err(InvalidIfRange);
}
let value = HeaderValue::from_str(s).map_err(|_| InvalidIfRange)?;
if s.starts_with('"') || s.starts_with("W/\"") {
Ok(IfRange::ETag(value))
} else {
Ok(IfRange::Date(value))
}
}
}
impl TryFrom<&HeaderValue> for IfRange {
type Error = InvalidIfRange;
fn try_from(value: &HeaderValue) -> Result<Self, Self::Error> {
value.to_str().map_err(|_| InvalidIfRange)?.parse::<Self>()
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("Invalid If-Range header")]
pub struct InvalidIfRange;
#[cfg(feature = "axum")]
impl<S> axum_core::extract::OptionalFromRequestParts<S> for IfRange
where
S: Send + Sync,
{
type Rejection = Infallible;
async fn from_request_parts(
parts: &mut http::request::Parts,
_state: &S,
) -> Result<Option<Self>, Self::Rejection> {
let if_range = parts
.headers
.get(http::header::IF_RANGE)
.and_then(|v| IfRange::try_from(v).ok());
Ok(if_range)
}
}