Skip to main content

range_requests/headers/
range.rs

1#[cfg(feature = "axum")]
2use std::convert::Infallible;
3use std::{
4    fmt::{self, Display},
5    str::FromStr,
6};
7
8use http::HeaderValue;
9
10use crate::headers::{OrderedRange, ParseHttpRangeOrContentRangeError, UNIT, u64_unprefixed_parse};
11
12/// A typed HTTP `Range` header that only supports a __single__ range.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum HttpRange {
15    StartingPoint(u64),
16    Range(OrderedRange),
17    Suffix(u64),
18}
19
20impl FromStr for HttpRange {
21    type Err = ParseHttpRangeOrContentRangeError;
22
23    fn from_str(s: &str) -> Result<Self, Self::Err> {
24        let s = s.trim();
25        if s.is_empty() {
26            return Err(ParseHttpRangeOrContentRangeError::Empty);
27        }
28
29        let (unit_str, range_str) = s
30            .split_once("=")
31            .ok_or(ParseHttpRangeOrContentRangeError::Malformed)?;
32        if unit_str != UNIT {
33            return Err(ParseHttpRangeOrContentRangeError::InvalidUnit);
34        }
35
36        let (start_str, end_str) = range_str
37            .split_once("-")
38            .ok_or(ParseHttpRangeOrContentRangeError::MalformedRange)?;
39
40        match (start_str.is_empty(), end_str.is_empty()) {
41            (false, false) => {
42                let start = u64_unprefixed_parse(start_str)
43                    .map_err(ParseHttpRangeOrContentRangeError::InvalidRangePiece)?;
44                let end = u64_unprefixed_parse(end_str)
45                    .map_err(ParseHttpRangeOrContentRangeError::InvalidRangePiece)?;
46
47                let range = OrderedRange::new(start..=end)?;
48                Ok(Self::Range(range))
49            }
50            (false, true) => {
51                let start = u64_unprefixed_parse(start_str)
52                    .map_err(ParseHttpRangeOrContentRangeError::InvalidRangePiece)?;
53
54                Ok(Self::StartingPoint(start))
55            }
56            (true, false) => {
57                let suffix = u64_unprefixed_parse(end_str)
58                    .map_err(ParseHttpRangeOrContentRangeError::InvalidRangePiece)?;
59
60                Ok(Self::Suffix(suffix))
61            }
62            (true, true) => Err(ParseHttpRangeOrContentRangeError::Malformed),
63        }
64    }
65}
66
67impl From<&HttpRange> for HeaderValue {
68    fn from(value: &HttpRange) -> Self {
69        HeaderValue::from_maybe_shared(value.to_string())
70            .expect("`HttpRange` Display produced non-visible ASCII characters")
71    }
72}
73
74impl TryFrom<&HeaderValue> for HttpRange {
75    type Error = ParseHttpRangeOrContentRangeError;
76    fn try_from(value: &HeaderValue) -> Result<Self, Self::Error> {
77        value
78            .to_str()
79            .map_err(|_| ParseHttpRangeOrContentRangeError::ContainsNonVisibleASCII)?
80            .parse::<Self>()
81    }
82}
83
84#[cfg(feature = "axum")]
85impl<S> axum_core::extract::OptionalFromRequestParts<S> for HttpRange
86where
87    S: Send + Sync,
88{
89    type Rejection = Infallible;
90
91    /// Extracts an optional [`HttpRange`] from the request's `Range` header.
92    ///
93    /// Per [RFC 9110 Section 14.2], a server that receives a `Range` header it
94    /// cannot parse or does not support (unknown range unit, multiple ranges,
95    /// malformed values) **must** ignore the header and serve the full
96    /// representation. This extractor returns `Ok(None)` in all such cases
97    /// instead of rejecting the request.
98    ///
99    /// [RFC 9110 Section 14.2]: https://www.rfc-editor.org/rfc/rfc9110#section-14.2
100    async fn from_request_parts(
101        parts: &mut http::request::Parts,
102        _state: &S,
103    ) -> Result<Option<Self>, Self::Rejection> {
104        let range = parts
105            .headers
106            .get(http::header::RANGE)
107            .and_then(|range| HttpRange::try_from(range).ok());
108        Ok(range)
109    }
110}
111
112impl Display for HttpRange {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        match self {
115            HttpRange::StartingPoint(start) => write!(f, "{UNIT}={start}-"),
116            HttpRange::Range(range) => write!(f, "{UNIT}={range}"),
117            HttpRange::Suffix(suffix) => write!(f, "{UNIT}=-{suffix}"),
118        }
119    }
120}