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        // Range unit names are case-insensitive (RFC 9110 Section 14.1).
33        if !unit_str.eq_ignore_ascii_case(UNIT) {
34            return Err(ParseHttpRangeOrContentRangeError::InvalidUnit);
35        }
36
37        let (start_str, end_str) = range_str
38            .split_once("-")
39            .ok_or(ParseHttpRangeOrContentRangeError::MalformedRange)?;
40
41        match (start_str.is_empty(), end_str.is_empty()) {
42            (false, false) => {
43                let start = u64_unprefixed_parse(start_str)
44                    .map_err(ParseHttpRangeOrContentRangeError::InvalidRangePiece)?;
45                let end = u64_unprefixed_parse(end_str)
46                    .map_err(ParseHttpRangeOrContentRangeError::InvalidRangePiece)?;
47
48                let range = OrderedRange::new(start..=end)?;
49                Ok(Self::Range(range))
50            }
51            (false, true) => {
52                let start = u64_unprefixed_parse(start_str)
53                    .map_err(ParseHttpRangeOrContentRangeError::InvalidRangePiece)?;
54
55                Ok(Self::StartingPoint(start))
56            }
57            (true, false) => {
58                let suffix = u64_unprefixed_parse(end_str)
59                    .map_err(ParseHttpRangeOrContentRangeError::InvalidRangePiece)?;
60
61                Ok(Self::Suffix(suffix))
62            }
63            (true, true) => Err(ParseHttpRangeOrContentRangeError::Malformed),
64        }
65    }
66}
67
68impl From<&HttpRange> for HeaderValue {
69    fn from(value: &HttpRange) -> Self {
70        HeaderValue::from_maybe_shared(value.to_string())
71            .expect("`HttpRange` Display produced non-visible ASCII characters")
72    }
73}
74
75impl TryFrom<&HeaderValue> for HttpRange {
76    type Error = ParseHttpRangeOrContentRangeError;
77    fn try_from(value: &HeaderValue) -> Result<Self, Self::Error> {
78        value
79            .to_str()
80            .map_err(|_| ParseHttpRangeOrContentRangeError::ContainsNonVisibleASCII)?
81            .parse::<Self>()
82    }
83}
84
85#[cfg(feature = "axum")]
86impl<S> axum_core::extract::OptionalFromRequestParts<S> for HttpRange
87where
88    S: Send + Sync,
89{
90    type Rejection = Infallible;
91
92    /// Extracts an optional [`HttpRange`] from the request's `Range` header.
93    ///
94    /// Per [RFC 9110 Section 14.2], range handling is only defined for the
95    /// GET method, and a server may ignore a `Range` header it cannot parse
96    /// or does not support (unknown range unit, multiple ranges, malformed
97    /// values). This extractor returns `Ok(None)` for non-GET requests and
98    /// in all such cases instead of rejecting the request, so that the full
99    /// representation is served.
100    ///
101    /// [RFC 9110 Section 14.2]: https://www.rfc-editor.org/rfc/rfc9110#section-14.2
102    async fn from_request_parts(
103        parts: &mut http::request::Parts,
104        _state: &S,
105    ) -> Result<Option<Self>, Self::Rejection> {
106        if parts.method != http::Method::GET {
107            return Ok(None);
108        }
109
110        let range = parts
111            .headers
112            .get(http::header::RANGE)
113            .and_then(|range| HttpRange::try_from(range).ok());
114        Ok(range)
115    }
116}
117
118impl Display for HttpRange {
119    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120        match self {
121            HttpRange::StartingPoint(start) => write!(f, "{UNIT}={start}-"),
122            HttpRange::Range(range) => write!(f, "{UNIT}={range}"),
123            HttpRange::Suffix(suffix) => write!(f, "{UNIT}=-{suffix}"),
124        }
125    }
126}