1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub struct HttpByteRange {
6 start: u64,
7 end: u64,
8 length: u64,
9 total_size: u64,
10}
11
12#[derive(Debug, PartialEq, Eq)]
14pub struct HttpByteRangeSet {
15 requested_count: usize,
16 ranges: Vec<HttpByteRange>,
17}
18
19impl HttpByteRangeSet {
20 #[must_use]
22 pub const fn requested_count(&self) -> usize {
23 self.requested_count
24 }
25
26 #[must_use]
28 pub fn ranges(&self) -> &[HttpByteRange] {
29 &self.ranges
30 }
31
32 #[must_use]
34 pub fn into_ranges(self) -> Vec<HttpByteRange> {
35 self.ranges
36 }
37}
38
39impl HttpByteRange {
40 pub fn new(start: u64, end: u64, total_size: u64) -> Result<Self, HttpRangeError> {
47 if total_size == 0 {
48 return Err(HttpRangeError::EmptyRepresentation);
49 }
50 if start > end || end >= total_size {
51 return Err(HttpRangeError::Unsatisfiable);
52 }
53 Ok(Self {
54 start,
55 end,
56 length: end - start + 1,
57 total_size,
58 })
59 }
60
61 #[must_use]
62 pub const fn start(self) -> u64 {
63 self.start
64 }
65
66 #[must_use]
67 pub const fn end(self) -> u64 {
68 self.end
69 }
70
71 #[must_use]
72 pub const fn length(self) -> u64 {
73 self.length
74 }
75
76 #[must_use]
77 pub const fn total_size(self) -> u64 {
78 self.total_size
79 }
80
81 #[must_use]
83 pub fn content_range_header(self) -> String {
84 format!("bytes {}-{}/{}", self.start, self.end, self.total_size)
85 }
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
90pub enum HttpRangeError {
91 #[error("range header must use the bytes unit")]
92 UnsupportedUnit,
93 #[error("multiple range requests are not supported")]
94 MultipleRangesUnsupported,
95 #[error("range header exceeds the configured byte length")]
96 HeaderTooLong,
97 #[error("range request exceeds the configured number of range specs")]
98 TooManyRanges,
99 #[error("range header is malformed")]
100 Malformed,
101 #[error("range bound must be a valid unsigned integer")]
102 InvalidNumber,
103 #[error("range cannot be requested for an empty representation")]
104 EmptyRepresentation,
105 #[error("range is not satisfiable for the current representation")]
106 Unsatisfiable,
107}
108
109pub fn parse_single_byte_range(
120 raw: &str,
121 total_size: u64,
122) -> Result<HttpByteRange, HttpRangeError> {
123 let set = parse_byte_ranges(raw, total_size, raw.len(), 1).map_err(|error| match error {
124 HttpRangeError::TooManyRanges => HttpRangeError::MultipleRangesUnsupported,
125 other => other,
126 })?;
127 set.into_ranges()
128 .into_iter()
129 .next()
130 .ok_or(HttpRangeError::Unsatisfiable)
131}
132
133pub fn parse_byte_ranges(
147 raw: &str,
148 total_size: u64,
149 maximum_raw_bytes: usize,
150 maximum_specs: usize,
151) -> Result<HttpByteRangeSet, HttpRangeError> {
152 if raw.len() > maximum_raw_bytes {
153 return Err(HttpRangeError::HeaderTooLong);
154 }
155 let raw = raw.trim_start();
156 let (unit, range_set) = raw.split_once('=').ok_or(HttpRangeError::UnsupportedUnit)?;
157 if !unit.eq_ignore_ascii_case("bytes") {
158 return Err(HttpRangeError::UnsupportedUnit);
159 }
160
161 let requested_count = range_set
162 .split(',')
163 .filter(|spec| !spec.trim().is_empty())
164 .count();
165 if requested_count == 0 {
166 return Err(HttpRangeError::Malformed);
167 }
168 if requested_count > maximum_specs {
169 return Err(HttpRangeError::TooManyRanges);
170 }
171
172 let mut ranges = Vec::with_capacity(requested_count);
173 for spec in range_set
174 .split(',')
175 .map(str::trim)
176 .filter(|spec| !spec.is_empty())
177 {
178 if let Some(range) = parse_byte_range_spec(spec, total_size)? {
179 ranges.push(range);
180 }
181 }
182 if ranges.is_empty() {
183 return Err(if total_size == 0 {
184 HttpRangeError::EmptyRepresentation
185 } else {
186 HttpRangeError::Unsatisfiable
187 });
188 }
189
190 Ok(HttpByteRangeSet {
191 requested_count,
192 ranges,
193 })
194}
195
196fn parse_byte_range_spec(
197 spec: &str,
198 total_size: u64,
199) -> Result<Option<HttpByteRange>, HttpRangeError> {
200 let (start_raw, end_raw) = spec.split_once('-').ok_or(HttpRangeError::Malformed)?;
201 if start_raw.is_empty() && end_raw.is_empty() {
202 return Err(HttpRangeError::Malformed);
203 }
204
205 if start_raw.is_empty() {
206 let suffix_length = parse_bound(end_raw)?;
207 if suffix_length == 0 || total_size == 0 {
208 return Ok(None);
209 }
210 let length = suffix_length.min(total_size);
211 return HttpByteRange::new(total_size - length, total_size - 1, total_size).map(Some);
212 }
213
214 let start = parse_bound(start_raw)?;
215 let end = if end_raw.is_empty() {
216 None
217 } else {
218 Some(parse_bound(end_raw)?)
219 };
220 if end.is_some_and(|end| end < start) {
221 return Err(HttpRangeError::Malformed);
222 }
223 if total_size == 0 || start >= total_size {
224 return Ok(None);
225 }
226 let end = end.unwrap_or(total_size - 1).min(total_size - 1);
227 HttpByteRange::new(start, end, total_size).map(Some)
228}
229
230fn parse_bound(value: &str) -> Result<u64, HttpRangeError> {
231 value
232 .parse::<u64>()
233 .map_err(|_| HttpRangeError::InvalidNumber)
234}
235
236#[cfg(test)]
237mod tests {
238 use super::{HttpByteRange, HttpRangeError, parse_byte_ranges, parse_single_byte_range};
239
240 #[test]
241 fn resolves_bounded_open_and_suffix_ranges() {
242 assert_eq!(
243 parse_single_byte_range("bytes=5-9", 20),
244 HttpByteRange::new(5, 9, 20)
245 );
246 assert_eq!(
247 parse_single_byte_range("bytes=7-", 20),
248 HttpByteRange::new(7, 19, 20)
249 );
250 assert_eq!(
251 parse_single_byte_range("bytes=-6", 20),
252 HttpByteRange::new(14, 19, 20)
253 );
254 assert_eq!(
255 parse_single_byte_range("bytes=-50", 20),
256 HttpByteRange::new(0, 19, 20)
257 );
258 assert_eq!(
259 parse_single_byte_range(" BYTES=0-1", 20),
260 HttpByteRange::new(0, 1, 20)
261 );
262 }
263
264 #[test]
265 fn clamps_end_beyond_the_representation() {
266 assert_eq!(
267 parse_single_byte_range("bytes=17-99", 20),
268 HttpByteRange::new(17, 19, 20)
269 );
270 }
271
272 #[test]
273 fn preserves_u64_boundaries_without_overflow() {
274 let total_size = u64::MAX;
275 let range = parse_single_byte_range("bytes=0-18446744073709551615", total_size)
276 .expect("maximum end should clamp safely");
277 assert_eq!(range.start(), 0);
278 assert_eq!(range.end(), u64::MAX - 1);
279 assert_eq!(range.length(), u64::MAX);
280 assert_eq!(range.total_size(), total_size);
281 }
282
283 #[test]
284 fn multi_range_parser_preserves_order_and_removes_only_unsatisfiable_specs() {
285 let set = parse_byte_ranges("bytes=10-12, 50-, -5, 0-4", 20, 32, 4)
286 .expect("mixed range-set should keep satisfiable specs");
287 assert_eq!(set.requested_count(), 4);
288 assert_eq!(
289 set.ranges(),
290 [
291 HttpByteRange::new(10, 12, 20).expect("range"),
292 HttpByteRange::new(15, 19, 20).expect("range"),
293 HttpByteRange::new(0, 4, 20).expect("range"),
294 ]
295 );
296 }
297
298 #[test]
299 fn multi_range_parser_tolerates_empty_list_members_and_clamps_suffixes() {
300 let set = parse_byte_ranges("bytes=, 0-99, , -100,", 20, 24, 2)
301 .expect("empty list members are recipient-tolerated");
302 assert_eq!(set.requested_count(), 2);
303 assert_eq!(
304 set.into_ranges(),
305 vec![
306 HttpByteRange::new(0, 19, 20).expect("range"),
307 HttpByteRange::new(0, 19, 20).expect("range"),
308 ]
309 );
310 }
311
312 #[test]
313 fn multi_range_parser_clamps_mixed_ranges_at_u64_max() {
314 let set = parse_byte_ranges(
315 "bytes=-2,18446744073709551613-18446744073709551615",
316 u64::MAX,
317 58,
318 2,
319 )
320 .expect("maximum-sized representation ranges should clamp without overflow");
321 assert_eq!(set.requested_count(), 2);
322 assert_eq!(
323 set.ranges(),
324 [
325 HttpByteRange::new(u64::MAX - 2, u64::MAX - 1, u64::MAX).expect("suffix range"),
326 HttpByteRange::new(u64::MAX - 2, u64::MAX - 1, u64::MAX).expect("closed range"),
327 ]
328 );
329 }
330
331 #[test]
332 fn multi_range_parser_enforces_spec_limit_before_normalization() {
333 assert_eq!(
334 parse_byte_ranges("bytes=0-1,100-200", 20, 19, 1),
335 Err(HttpRangeError::TooManyRanges)
336 );
337 assert_eq!(
338 parse_byte_ranges("bytes=100-200,300-400", 20, 25, 2),
339 Err(HttpRangeError::Unsatisfiable)
340 );
341 assert_eq!(
342 parse_byte_ranges("bytes=-0,20-", 20, 13, 2),
343 Err(HttpRangeError::Unsatisfiable)
344 );
345 }
346
347 #[test]
348 fn multi_range_parser_rejects_invalid_members_and_empty_representations() {
349 for (raw, expected) in [
350 ("bytes=", HttpRangeError::Malformed),
351 ("bytes=, ,", HttpRangeError::Malformed),
352 ("bytes=0-1,broken", HttpRangeError::Malformed),
353 ("bytes=9-5,0-1", HttpRangeError::Malformed),
354 ("bytes=0-1,2-x", HttpRangeError::InvalidNumber),
355 ("items=0-1", HttpRangeError::UnsupportedUnit),
356 ] {
357 assert_eq!(
358 parse_byte_ranges(raw, 20, raw.len(), 8),
359 Err(expected),
360 "{raw}"
361 );
362 }
363 assert_eq!(
364 parse_byte_ranges("bytes=-1,0-", 0, 12, 2),
365 Err(HttpRangeError::EmptyRepresentation)
366 );
367 }
368
369 #[test]
370 fn multi_range_parser_separates_raw_byte_and_spec_limits() {
371 let exact = "bytes=0-1";
372 assert!(parse_byte_ranges(exact, 20, exact.len(), 1).is_ok());
373 assert_eq!(
374 parse_byte_ranges(exact, 20, exact.len() - 1, 1),
375 Err(HttpRangeError::HeaderTooLong)
376 );
377
378 let comma_padded = "bytes=,,,,,,,,0-1,,,,,,,,";
379 assert_eq!(
380 parse_byte_ranges(comma_padded, 20, 16, 1),
381 Err(HttpRangeError::HeaderTooLong)
382 );
383 let too_many_specs = "bytes=0-1,2-3";
384 assert_eq!(
385 parse_byte_ranges(too_many_specs, 20, too_many_specs.len(), 1),
386 Err(HttpRangeError::TooManyRanges)
387 );
388 }
389
390 #[test]
391 fn renders_content_range_and_exposes_bounds() {
392 let range = HttpByteRange::new(2, 6, 10).expect("valid range");
393 assert_eq!(range.start(), 2);
394 assert_eq!(range.end(), 6);
395 assert_eq!(range.length(), 5);
396 assert_eq!(range.total_size(), 10);
397 assert_eq!(range.content_range_header(), "bytes 2-6/10");
398 }
399
400 #[test]
401 fn constructor_rejects_empty_inverted_and_out_of_bounds_ranges() {
402 assert_eq!(
403 HttpByteRange::new(0, 0, 0),
404 Err(HttpRangeError::EmptyRepresentation)
405 );
406 assert_eq!(
407 HttpByteRange::new(5, 4, 10),
408 Err(HttpRangeError::Unsatisfiable)
409 );
410 assert_eq!(
411 HttpByteRange::new(5, 10, 10),
412 Err(HttpRangeError::Unsatisfiable)
413 );
414 }
415
416 #[test]
417 fn classifies_every_rejected_range_shape() {
418 let cases = [
419 ("items=0-1", HttpRangeError::UnsupportedUnit),
420 ("bytes=0-1,3-4", HttpRangeError::MultipleRangesUnsupported),
421 ("bytes=-", HttpRangeError::Malformed),
422 ("bytes=abc-", HttpRangeError::InvalidNumber),
423 ("bytes=-0", HttpRangeError::Unsatisfiable),
424 ("bytes=9-5", HttpRangeError::Malformed),
425 ("bytes=20-", HttpRangeError::Unsatisfiable),
426 ];
427 for (raw, expected) in cases {
428 assert_eq!(parse_single_byte_range(raw, 20), Err(expected), "{raw}");
429 }
430 assert_eq!(
431 parse_single_byte_range("bytes=0-0", 0),
432 Err(HttpRangeError::EmptyRepresentation)
433 );
434 }
435}