1use std::fmt;
16use std::str::FromStr;
17
18use http::header::HeaderValue;
19use thiserror::Error;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum ByteRange {
29 Bounded(u64, u64),
31 From(u64),
33 Last(u64),
35}
36
37impl ByteRange {
38 pub fn to_header_value(&self) -> HeaderValue {
40 let s = match self {
41 ByteRange::Bounded(a, b) => format!("bytes={a}-{b}"),
42 ByteRange::From(n) => format!("bytes={n}-"),
43 ByteRange::Last(n) => format!("bytes=-{n}"),
44 };
45 HeaderValue::from_str(&s).expect("always a valid header value")
46 }
47
48 pub fn resolve(self, total: u64) -> Option<ContentRange> {
51 if total == 0 {
52 return None;
53 }
54
55 let (start, end) = match self {
56 ByteRange::Bounded(start, end) => {
57 if start >= total {
58 return None;
59 }
60 (start, end.min(total - 1)) }
62 ByteRange::From(start) => {
63 if start >= total {
64 return None;
65 }
66 (start, total - 1)
67 }
68 ByteRange::Last(negative_start) => {
69 let start = total.saturating_sub(negative_start);
70 (start, total - 1)
71 }
72 };
73
74 Some(ContentRange { start, end, total })
75 }
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Error)]
80pub enum RangeError {
81 #[error("invalid byte range")]
83 Invalid,
84 #[error("expected single byte range, found multipart range")]
86 MultiRange,
87 #[error("invalid range unit: {0}, expected: bytes")]
89 InvalidUnit(String),
90}
91
92impl FromStr for ByteRange {
97 type Err = RangeError;
98
99 fn from_str(value: &str) -> Result<Self, Self::Err> {
100 let lower = value.to_ascii_lowercase();
101 let Some(spec) = lower.strip_prefix("bytes=") else {
102 let unit = lower.split_once('=').map_or(&*lower, |(u, _)| u);
103 return Err(RangeError::InvalidUnit(unit.to_owned()));
104 };
105 if spec.contains(',') {
106 return Err(RangeError::MultiRange);
107 }
108
109 let (start, end) = spec.split_once('-').ok_or(RangeError::Invalid)?;
110 if end.is_empty() {
111 let start: u64 = start.parse().map_err(|_| RangeError::Invalid)?;
112 Ok(ByteRange::From(start))
113 } else if start.is_empty() {
114 let last: u64 = end.parse().map_err(|_| RangeError::Invalid)?;
115 if last == 0 {
116 return Err(RangeError::Invalid);
117 }
118 Ok(ByteRange::Last(last))
119 } else {
120 let start: u64 = start.parse().map_err(|_| RangeError::Invalid)?;
121 let end: u64 = end.parse().map_err(|_| RangeError::Invalid)?;
122 if start > end {
123 return Err(RangeError::Invalid);
124 }
125 Ok(ByteRange::Bounded(start, end))
126 }
127 }
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub struct ContentRange {
138 pub start: u64,
140 pub end: u64,
142 pub total: u64,
144}
145
146impl FromStr for ContentRange {
148 type Err = RangeError;
149
150 fn from_str(s: &str) -> Result<Self, Self::Err> {
151 let parse = || {
152 let rest = s.strip_prefix("bytes ")?;
153 let (range_part, total_str) = rest.split_once('/')?;
154 let total: u64 = total_str.parse().ok()?;
155 let (start_str, end_str) = range_part.split_once('-')?;
156 let start: u64 = start_str.parse().ok()?;
157 let end: u64 = end_str.parse().ok()?;
158 if start > end || end >= total {
159 return None;
160 }
161 Some(Self { start, end, total })
162 };
163 parse().ok_or(RangeError::Invalid)
164 }
165}
166
167#[expect(
168 clippy::len_without_is_empty,
169 reason = "A valid ContentRange is never empty"
170)]
171impl ContentRange {
172 pub fn len(&self) -> u64 {
174 self.end - self.start + 1
175 }
176
177 pub fn to_header_value(&self) -> HeaderValue {
182 HeaderValue::from_str(&self.to_string()).expect("always a valid header value")
183 }
184
185 pub fn len_to_header_value(&self) -> HeaderValue {
187 HeaderValue::from_str(&self.len().to_string()).expect("always a valid header value")
188 }
189
190 pub fn parse_unsatisfiable_total(header: &str) -> Option<u64> {
197 let rest = header.strip_prefix("bytes */")?;
198 rest.parse().ok()
199 }
200
201 pub fn unsatisfiable_total_to_header_value(total: u64) -> HeaderValue {
203 HeaderValue::from_str(format!("bytes */{total}").as_str())
204 .expect("always a valid header value")
205 }
206}
207
208impl fmt::Display for ContentRange {
209 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
210 write!(f, "bytes {}-{}/{}", self.start, self.end, self.total)
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn parse_valid_ranges() {
220 assert_eq!(
221 "bytes=0-499".parse::<ByteRange>(),
222 Ok(ByteRange::Bounded(0, 499))
223 );
224 assert_eq!("bytes=500-".parse::<ByteRange>(), Ok(ByteRange::From(500)));
225 assert_eq!("bytes=-100".parse::<ByteRange>(), Ok(ByteRange::Last(100)));
226 assert_eq!(
228 "Bytes=0-499".parse::<ByteRange>(),
229 Ok(ByteRange::Bounded(0, 499))
230 );
231 assert_eq!("BYTES=100-".parse::<ByteRange>(), Ok(ByteRange::From(100)));
232 }
233
234 #[test]
235 fn parse_invalid_ranges() {
236 assert_eq!(
237 "bytes=0-10, 20-30".parse::<ByteRange>(),
238 Err(RangeError::MultiRange)
239 );
240 assert_eq!(
241 "items=0-10".parse::<ByteRange>(),
242 Err(RangeError::InvalidUnit("items".into()))
243 );
244 assert_eq!(
245 "bytes=500-100".parse::<ByteRange>(),
246 Err(RangeError::Invalid)
247 );
248 assert_eq!("bytes=-0".parse::<ByteRange>(), Err(RangeError::Invalid));
249 }
250
251 #[test]
252 fn resolve_satisfiable() {
253 let cr = |start, end, total| Some(ContentRange { start, end, total });
254 assert_eq!(ByteRange::Bounded(0, 499).resolve(1000), cr(0, 499, 1000));
255 assert_eq!(ByteRange::Bounded(0, 9999).resolve(500), cr(0, 499, 500));
256 assert_eq!(ByteRange::From(500).resolve(1000), cr(500, 999, 1000));
257 assert_eq!(ByteRange::Last(100).resolve(1000), cr(900, 999, 1000));
258 assert_eq!(ByteRange::Last(2000).resolve(1000), cr(0, 999, 1000));
259 }
260
261 #[test]
262 fn resolve_unsatisfiable() {
263 assert_eq!(ByteRange::Bounded(1000, 2000).resolve(500), None);
264 assert_eq!(ByteRange::From(500).resolve(500), None);
265 assert_eq!(ByteRange::Bounded(0, 0).resolve(0), None);
266 }
267
268 #[test]
269 fn content_range_len() {
270 let full = ContentRange {
271 start: 0,
272 end: 999,
273 total: 1000,
274 };
275 assert_eq!(full.len(), 1000);
276
277 let partial = ContentRange {
278 start: 0,
279 end: 499,
280 total: 1000,
281 };
282 assert_eq!(partial.len(), 500);
283 }
284
285 #[test]
286 fn parse_unsatisfiable_total() {
287 assert_eq!(
288 ContentRange::parse_unsatisfiable_total("bytes */1234"),
289 Some(1234)
290 );
291 assert_eq!(
292 ContentRange::parse_unsatisfiable_total("bytes 0-499/1234"),
293 None
294 );
295 assert_eq!(ContentRange::parse_unsatisfiable_total("invalid"), None);
296 }
297
298 #[test]
299 fn header_value_roundtrips() {
300 assert_eq!(ByteRange::Bounded(0, 499).to_header_value(), "bytes=0-499");
301 assert_eq!(ByteRange::From(500).to_header_value(), "bytes=500-");
302 assert_eq!(ByteRange::Last(100).to_header_value(), "bytes=-100");
303
304 let cr = ContentRange {
305 start: 0,
306 end: 499,
307 total: 1234,
308 };
309 assert_eq!(cr.to_header_value(), "bytes 0-499/1234");
310 assert_eq!("bytes 0-499/1234".parse::<ContentRange>(), Ok(cr));
311 assert!("bytes */1234".parse::<ContentRange>().is_err());
312 assert!("invalid".parse::<ContentRange>().is_err());
313 assert!("bytes 499-0/1234".parse::<ContentRange>().is_err());
315 assert!("bytes 0-1234/1234".parse::<ContentRange>().is_err());
317 assert_eq!(
318 ContentRange::unsatisfiable_total_to_header_value(1234),
319 "bytes */1234"
320 );
321 }
322}