range-requests 0.2.0

Various utilities to deal with `Range Requests`
Documentation
#[cfg(feature = "axum")]
use std::convert::Infallible;
use std::{
    fmt::{self, Display},
    str::FromStr,
};

use http::HeaderValue;

use crate::headers::{OrderedRange, ParseHttpRangeOrContentRangeError, UNIT, u64_unprefixed_parse};

/// A typed HTTP `Range` header that only supports a __single__ range.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HttpRange {
    StartingPoint(u64),
    Range(OrderedRange),
    Suffix(u64),
}

impl FromStr for HttpRange {
    type Err = ParseHttpRangeOrContentRangeError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let s = s.trim();
        if s.is_empty() {
            return Err(ParseHttpRangeOrContentRangeError::Empty);
        }

        let (unit_str, range_str) = s
            .split_once("=")
            .ok_or(ParseHttpRangeOrContentRangeError::Malformed)?;
        if unit_str != UNIT {
            return Err(ParseHttpRangeOrContentRangeError::InvalidUnit);
        }

        let (start_str, end_str) = range_str
            .split_once("-")
            .ok_or(ParseHttpRangeOrContentRangeError::MalformedRange)?;

        match (start_str.is_empty(), end_str.is_empty()) {
            (false, false) => {
                let start = u64_unprefixed_parse(start_str)
                    .map_err(ParseHttpRangeOrContentRangeError::InvalidRangePiece)?;
                let end = u64_unprefixed_parse(end_str)
                    .map_err(ParseHttpRangeOrContentRangeError::InvalidRangePiece)?;

                let range = OrderedRange::new(start..=end)?;
                Ok(Self::Range(range))
            }
            (false, true) => {
                let start = u64_unprefixed_parse(start_str)
                    .map_err(ParseHttpRangeOrContentRangeError::InvalidRangePiece)?;

                Ok(Self::StartingPoint(start))
            }
            (true, false) => {
                let suffix = u64_unprefixed_parse(end_str)
                    .map_err(ParseHttpRangeOrContentRangeError::InvalidRangePiece)?;

                Ok(Self::Suffix(suffix))
            }
            (true, true) => Err(ParseHttpRangeOrContentRangeError::Malformed),
        }
    }
}

impl From<&HttpRange> for HeaderValue {
    fn from(value: &HttpRange) -> Self {
        HeaderValue::from_maybe_shared(value.to_string())
            .expect("`HttpRange` Display produced non-visible ASCII characters")
    }
}

impl TryFrom<&HeaderValue> for HttpRange {
    type Error = ParseHttpRangeOrContentRangeError;
    fn try_from(value: &HeaderValue) -> Result<Self, Self::Error> {
        value
            .to_str()
            .map_err(|_| ParseHttpRangeOrContentRangeError::ContainsNonVisibleASCII)?
            .parse::<Self>()
    }
}

#[cfg(feature = "axum")]
impl<S> axum_core::extract::OptionalFromRequestParts<S> for HttpRange
where
    S: Send + Sync,
{
    type Rejection = Infallible;

    /// Extracts an optional [`HttpRange`] from the request's `Range` header.
    ///
    /// Per [RFC 9110 Section 14.2], a server that receives a `Range` header it
    /// cannot parse or does not support (unknown range unit, multiple ranges,
    /// malformed values) **must** ignore the header and serve the full
    /// representation. This extractor returns `Ok(None)` in all such cases
    /// instead of rejecting the request.
    ///
    /// [RFC 9110 Section 14.2]: https://www.rfc-editor.org/rfc/rfc9110#section-14.2
    async fn from_request_parts(
        parts: &mut http::request::Parts,
        _state: &S,
    ) -> Result<Option<Self>, Self::Rejection> {
        let range = parts
            .headers
            .get(http::header::RANGE)
            .and_then(|range| HttpRange::try_from(range).ok());
        Ok(range)
    }
}

impl Display for HttpRange {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            HttpRange::StartingPoint(start) => write!(f, "{UNIT}={start}-"),
            HttpRange::Range(range) => write!(f, "{UNIT}={range}"),
            HttpRange::Suffix(suffix) => write!(f, "{UNIT}=-{suffix}"),
        }
    }
}