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