Skip to main content

range_requests/headers/
content_range.rs

1use std::{
2    fmt::{self, Display},
3    ops::RangeInclusive,
4    str::FromStr,
5};
6
7use http::HeaderValue;
8
9use crate::headers::{
10    InvalidHttpU64, InvalidOrderedRange, OrderedRange, ParseHttpRangeOrContentRangeError, UNIT,
11    range::HttpRange, u64_unprefixed_parse,
12};
13
14/// A typed HTTP `Content-Range` header that only supports a __single__ range.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum HttpContentRange {
17    Bound(Bound),
18    Unsatisfiable(Unsatisfiable),
19}
20
21impl HttpContentRange {
22    /// Checks whether this `Content-Range` matches the expected [`HttpRange`].
23    ///
24    /// [`HttpRange`]: crate::headers::range::HttpRange
25    pub fn matches_requested_range(&self, expected_range: HttpRange) -> bool {
26        match (expected_range, self) {
27            (HttpRange::StartingPoint(start), HttpContentRange::Bound(Bound { range, .. })) => {
28                start == range.start()
29            }
30            (
31                HttpRange::Range(OrderedRange { start, end }),
32                HttpContentRange::Bound(Bound { range, .. }),
33            ) => start == range.start() && end >= range.end(),
34            (HttpRange::Suffix(suffix), HttpContentRange::Bound(Bound { range, size })) => {
35                let length = (range.end() - range.start()).checked_add(1);
36                let length_matches = length.is_some_and(|len| len <= suffix);
37                let ends_at_boundary = size.is_none_or(|size| range.end() + 1 == size);
38                length_matches && ends_at_boundary
39            }
40            // Per RFC 9110 Section 14.1.2 an int-range is satisfiable iff its
41            // first-pos is less than the representation length, so a 416 is
42            // only consistent with the request when the start is at or past
43            // the claimed size.
44            (
45                HttpRange::StartingPoint(n),
46                HttpContentRange::Unsatisfiable(Unsatisfiable { size }),
47            )
48            | (
49                HttpRange::Range(OrderedRange { start: n, .. }),
50                HttpContentRange::Unsatisfiable(Unsatisfiable { size }),
51            ) => n >= *size,
52            (HttpRange::Suffix(suffix), HttpContentRange::Unsatisfiable(Unsatisfiable { .. })) => {
53                suffix == 0
54            }
55        }
56    }
57}
58
59impl FromStr for HttpContentRange {
60    type Err = ParseHttpRangeOrContentRangeError;
61
62    fn from_str(s: &str) -> Result<Self, Self::Err> {
63        let s = s.trim();
64        if s.is_empty() {
65            return Err(ParseHttpRangeOrContentRangeError::Empty);
66        }
67
68        let (unit_str, range_and_size_str) = s
69            .split_once(" ")
70            .ok_or(ParseHttpRangeOrContentRangeError::Malformed)?;
71
72        // Range unit names are case-insensitive (RFC 9110 Section 14.1).
73        if !unit_str.eq_ignore_ascii_case(UNIT) {
74            return Err(ParseHttpRangeOrContentRangeError::InvalidUnit);
75        }
76
77        let (range_str, size_str) = range_and_size_str
78            .split_once('/')
79            .ok_or(ParseHttpRangeOrContentRangeError::Malformed)?;
80
81        let range = range_str.parse::<ParsedRange>()?;
82        let size = size_str
83            .parse::<ParsedSize>()
84            .map_err(ParseHttpRangeOrContentRangeError::InvalidSize)?;
85
86        match (range, size) {
87            (ParsedRange::Star, ParsedSize::Star) => {
88                Err(ParseHttpRangeOrContentRangeError::Malformed)
89            }
90            (ParsedRange::Star, ParsedSize::Value(size)) => {
91                Ok(Self::Unsatisfiable(Unsatisfiable { size }))
92            }
93            (ParsedRange::Range(range), ParsedSize::Star) => {
94                Ok(Self::Bound(Bound { range, size: None }))
95            }
96            (ParsedRange::Range(range), ParsedSize::Value(size)) if range.end() < size => {
97                Ok(Self::Bound(Bound {
98                    range,
99                    size: Some(size),
100                }))
101            }
102            (ParsedRange::Range(_), ParsedSize::Value(_)) => {
103                Err(ParseHttpRangeOrContentRangeError::MalformedRange)
104            }
105        }
106    }
107}
108
109/// The Errors that may occur when creating a [`Bound`].
110#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
111pub enum InvalidBound {
112    #[error(transparent)]
113    InvalidRange(#[from] InvalidOrderedRange),
114    #[error("The provided range `end`: {} is greater than or equal to `size`: {size}", range.end)]
115    InvalidSize { range: OrderedRange, size: u64 },
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub struct Bound {
120    range: OrderedRange,
121    size: Option<u64>,
122}
123
124impl Bound {
125    // Creates a new [`Bound`].
126    pub fn new(range: RangeInclusive<u64>, size: Option<u64>) -> Result<Self, InvalidBound> {
127        let range = OrderedRange::new(range)?;
128
129        if let Some(size) = size
130            && range.end() >= size
131        {
132            return Err(InvalidBound::InvalidSize { range, size });
133        }
134
135        Ok(Self { range, size })
136    }
137
138    // Returns a copy of the [`Bound`] range.
139    pub fn range(&self) -> OrderedRange {
140        self.range
141    }
142
143    // Returns the size of the [`Bound`], if present.
144    pub fn size(&self) -> Option<u64> {
145        self.size
146    }
147}
148
149// An unsatisfiable `Content-Range`.
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub struct Unsatisfiable {
152    size: u64,
153}
154
155impl Unsatisfiable {
156    // Creates a new [`Unsatisfiable`].
157    pub fn new(size: u64) -> Self {
158        Self { size }
159    }
160}
161
162#[derive(Debug, Clone, Copy)]
163enum ParsedRange {
164    Star,
165    Range(OrderedRange),
166}
167
168impl FromStr for ParsedRange {
169    type Err = ParseHttpRangeOrContentRangeError;
170
171    fn from_str(s: &str) -> Result<Self, Self::Err> {
172        if s == "*" {
173            return Ok(ParsedRange::Star);
174        }
175
176        let (start_str, end_str) = s
177            .split_once('-')
178            .ok_or(ParseHttpRangeOrContentRangeError::MalformedRange)?;
179
180        let start = u64_unprefixed_parse(start_str)
181            .map_err(ParseHttpRangeOrContentRangeError::InvalidRangePiece)?;
182        let end = u64_unprefixed_parse(end_str)
183            .map_err(ParseHttpRangeOrContentRangeError::InvalidRangePiece)?;
184
185        let range = OrderedRange::new(start..=end)?;
186        Ok(ParsedRange::Range(range))
187    }
188}
189
190#[derive(Debug, Clone, Copy)]
191enum ParsedSize {
192    Star,
193    Value(u64),
194}
195
196impl FromStr for ParsedSize {
197    type Err = InvalidHttpU64;
198
199    fn from_str(s: &str) -> Result<Self, Self::Err> {
200        Ok(if s == "*" {
201            ParsedSize::Star
202        } else {
203            let size = u64_unprefixed_parse(s)?;
204            ParsedSize::Value(size)
205        })
206    }
207}
208
209impl From<&HttpContentRange> for HeaderValue {
210    fn from(value: &HttpContentRange) -> Self {
211        HeaderValue::from_maybe_shared(value.to_string())
212            .expect("`HttpContentRange` Display produced non-visible ASCII characters")
213    }
214}
215
216impl TryFrom<&HeaderValue> for HttpContentRange {
217    type Error = ParseHttpRangeOrContentRangeError;
218    fn try_from(value: &HeaderValue) -> Result<Self, Self::Error> {
219        value
220            .to_str()
221            .map_err(|_| ParseHttpRangeOrContentRangeError::ContainsNonVisibleASCII)?
222            .parse::<Self>()
223    }
224}
225
226#[cfg(feature = "axum")]
227impl<S> axum_core::extract::OptionalFromRequestParts<S> for HttpContentRange
228where
229    S: Send + Sync,
230{
231    type Rejection = ParseHttpRangeOrContentRangeError;
232
233    async fn from_request_parts(
234        parts: &mut http::request::Parts,
235        _state: &S,
236    ) -> Result<Option<Self>, Self::Rejection> {
237        match parts.headers.get(http::header::CONTENT_RANGE) {
238            Some(content_range) => {
239                let content_range = HttpContentRange::try_from(content_range)?;
240                Ok(Some(content_range))
241            }
242            None => Ok(None),
243        }
244    }
245}
246
247impl Display for HttpContentRange {
248    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
249        match self {
250            HttpContentRange::Bound(Bound { range, size }) => match size {
251                Some(size) => write!(f, "{UNIT} {range}/{size}"),
252                None => write!(f, "{UNIT} {range}/*"),
253            },
254            HttpContentRange::Unsatisfiable(Unsatisfiable { size }) => write!(f, "{UNIT} */{size}"),
255        }
256    }
257}