1use std::fmt;
4use std::str::FromStr;
5
6use http::header::HeaderValue;
7use thiserror::Error;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ByteRange {
12 Bounded(u64, u64),
14 From(u64),
16 Last(u64),
18}
19
20impl ByteRange {
21 pub fn to_header_value(&self) -> HeaderValue {
23 let s = match self {
24 ByteRange::Bounded(a, b) => format!("bytes={a}-{b}"),
25 ByteRange::From(n) => format!("bytes={n}-"),
26 ByteRange::Last(n) => format!("bytes=-{n}"),
27 };
28 HeaderValue::from_str(&s).expect("always a valid header value")
29 }
30
31 pub fn resolve(self, total: u64) -> Option<ContentRange> {
34 if total == 0 {
35 return None;
36 }
37
38 let (start, end) = match self {
39 ByteRange::Bounded(start, end) => {
40 if start >= total {
41 return None;
42 }
43 (start, end.min(total - 1)) }
45 ByteRange::From(start) => {
46 if start >= total {
47 return None;
48 }
49 (start, total - 1)
50 }
51 ByteRange::Last(negative_start) => {
52 let start = total.saturating_sub(negative_start);
53 (start, total - 1)
54 }
55 };
56
57 Some(ContentRange { start, end, total })
58 }
59}
60
61#[derive(Debug, Clone, PartialEq, Eq, Error)]
63pub enum RangeError {
64 #[error("invalid byte range")]
66 Invalid,
67 #[error("expected single byte range, found multipart range")]
69 MultiRange,
70 #[error("invalid range unit: {0}, expected: bytes")]
72 InvalidUnit(String),
73}
74
75impl FromStr for ByteRange {
80 type Err = RangeError;
81
82 fn from_str(value: &str) -> Result<Self, Self::Err> {
83 let lower = value.to_ascii_lowercase();
84 let Some(spec) = lower.strip_prefix("bytes=") else {
85 let unit = lower.split_once('=').map_or(&*lower, |(u, _)| u);
86 return Err(RangeError::InvalidUnit(unit.to_owned()));
87 };
88 if spec.contains(',') {
89 return Err(RangeError::MultiRange);
90 }
91
92 let (start, end) = spec.split_once('-').ok_or(RangeError::Invalid)?;
93 if end.is_empty() {
94 let start: u64 = start.parse().map_err(|_| RangeError::Invalid)?;
95 Ok(ByteRange::From(start))
96 } else if start.is_empty() {
97 let last: u64 = end.parse().map_err(|_| RangeError::Invalid)?;
98 if last == 0 {
99 return Err(RangeError::Invalid);
100 }
101 Ok(ByteRange::Last(last))
102 } else {
103 let start: u64 = start.parse().map_err(|_| RangeError::Invalid)?;
104 let end: u64 = end.parse().map_err(|_| RangeError::Invalid)?;
105 if start > end {
106 return Err(RangeError::Invalid);
107 }
108 Ok(ByteRange::Bounded(start, end))
109 }
110 }
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub struct ContentRange {
116 pub start: u64,
118 pub end: u64,
120 pub total: u64,
122}
123
124impl FromStr for ContentRange {
126 type Err = RangeError;
127
128 fn from_str(s: &str) -> Result<Self, Self::Err> {
129 let parse = || {
130 let rest = s.strip_prefix("bytes ")?;
131 let (range_part, total_str) = rest.split_once('/')?;
132 let total: u64 = total_str.parse().ok()?;
133 let (start_str, end_str) = range_part.split_once('-')?;
134 let start: u64 = start_str.parse().ok()?;
135 let end: u64 = end_str.parse().ok()?;
136 if start > end || end >= total {
137 return None;
138 }
139 Some(Self { start, end, total })
140 };
141 parse().ok_or(RangeError::Invalid)
142 }
143}
144
145#[expect(
146 clippy::len_without_is_empty,
147 reason = "A valid ContentRange is never empty"
148)]
149impl ContentRange {
150 pub fn len(&self) -> u64 {
152 self.end - self.start + 1
153 }
154
155 pub fn to_header_value(&self) -> HeaderValue {
160 HeaderValue::from_str(&self.to_string()).expect("always a valid header value")
161 }
162
163 pub fn len_to_header_value(&self) -> HeaderValue {
165 HeaderValue::from_str(&self.len().to_string()).expect("always a valid header value")
166 }
167
168 pub fn parse_unsatisfiable_total(header: &str) -> Option<u64> {
175 let rest = header.strip_prefix("bytes */")?;
176 rest.parse().ok()
177 }
178
179 pub fn unsatisfiable_total_to_header_value(total: u64) -> HeaderValue {
181 HeaderValue::from_str(format!("bytes */{total}").as_str())
182 .expect("always a valid header value")
183 }
184}
185
186impl fmt::Display for ContentRange {
187 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188 write!(f, "bytes {}-{}/{}", self.start, self.end, self.total)
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 #[test]
197 fn parse_valid_ranges() {
198 assert_eq!(
199 "bytes=0-499".parse::<ByteRange>(),
200 Ok(ByteRange::Bounded(0, 499))
201 );
202 assert_eq!("bytes=500-".parse::<ByteRange>(), Ok(ByteRange::From(500)));
203 assert_eq!("bytes=-100".parse::<ByteRange>(), Ok(ByteRange::Last(100)));
204 assert_eq!(
206 "Bytes=0-499".parse::<ByteRange>(),
207 Ok(ByteRange::Bounded(0, 499))
208 );
209 assert_eq!("BYTES=100-".parse::<ByteRange>(), Ok(ByteRange::From(100)));
210 }
211
212 #[test]
213 fn parse_invalid_ranges() {
214 assert_eq!(
215 "bytes=0-10, 20-30".parse::<ByteRange>(),
216 Err(RangeError::MultiRange)
217 );
218 assert_eq!(
219 "items=0-10".parse::<ByteRange>(),
220 Err(RangeError::InvalidUnit("items".into()))
221 );
222 assert_eq!(
223 "bytes=500-100".parse::<ByteRange>(),
224 Err(RangeError::Invalid)
225 );
226 assert_eq!("bytes=-0".parse::<ByteRange>(), Err(RangeError::Invalid));
227 }
228
229 #[test]
230 fn resolve_satisfiable() {
231 let cr = |start, end, total| Some(ContentRange { start, end, total });
232 assert_eq!(ByteRange::Bounded(0, 499).resolve(1000), cr(0, 499, 1000));
233 assert_eq!(ByteRange::Bounded(0, 9999).resolve(500), cr(0, 499, 500));
234 assert_eq!(ByteRange::From(500).resolve(1000), cr(500, 999, 1000));
235 assert_eq!(ByteRange::Last(100).resolve(1000), cr(900, 999, 1000));
236 assert_eq!(ByteRange::Last(2000).resolve(1000), cr(0, 999, 1000));
237 }
238
239 #[test]
240 fn resolve_unsatisfiable() {
241 assert_eq!(ByteRange::Bounded(1000, 2000).resolve(500), None);
242 assert_eq!(ByteRange::From(500).resolve(500), None);
243 assert_eq!(ByteRange::Bounded(0, 0).resolve(0), None);
244 }
245
246 #[test]
247 fn content_range_len() {
248 let full = ContentRange {
249 start: 0,
250 end: 999,
251 total: 1000,
252 };
253 assert_eq!(full.len(), 1000);
254
255 let partial = ContentRange {
256 start: 0,
257 end: 499,
258 total: 1000,
259 };
260 assert_eq!(partial.len(), 500);
261 }
262
263 #[test]
264 fn parse_unsatisfiable_total() {
265 assert_eq!(
266 ContentRange::parse_unsatisfiable_total("bytes */1234"),
267 Some(1234)
268 );
269 assert_eq!(
270 ContentRange::parse_unsatisfiable_total("bytes 0-499/1234"),
271 None
272 );
273 assert_eq!(ContentRange::parse_unsatisfiable_total("invalid"), None);
274 }
275
276 #[test]
277 fn header_value_roundtrips() {
278 assert_eq!(ByteRange::Bounded(0, 499).to_header_value(), "bytes=0-499");
279 assert_eq!(ByteRange::From(500).to_header_value(), "bytes=500-");
280 assert_eq!(ByteRange::Last(100).to_header_value(), "bytes=-100");
281
282 let cr = ContentRange {
283 start: 0,
284 end: 499,
285 total: 1234,
286 };
287 assert_eq!(cr.to_header_value(), "bytes 0-499/1234");
288 assert_eq!("bytes 0-499/1234".parse::<ContentRange>(), Ok(cr));
289 assert!("bytes */1234".parse::<ContentRange>().is_err());
290 assert!("invalid".parse::<ContentRange>().is_err());
291 assert!("bytes 499-0/1234".parse::<ContentRange>().is_err());
293 assert!("bytes 0-1234/1234".parse::<ContentRange>().is_err());
295 assert_eq!(
296 ContentRange::unsatisfiable_total_to_header_value(1234),
297 "bytes */1234"
298 );
299 }
300}