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