1use axum::body::Body;
54use axum::response::Response;
55use bytes::Bytes;
56use http::header::{ACCEPT_RANGES, CONTENT_LENGTH, CONTENT_RANGE, IF_RANGE, RANGE};
57use http::{HeaderMap, HeaderValue, StatusCode};
58
59use crate::etag::ETag;
60
61#[derive(Debug, Clone, PartialEq, Eq)]
66pub enum RangeResolution {
67 Full,
71 Partial {
74 start: u64,
76 end: u64,
78 total: u64,
80 },
81 Unsatisfiable {
85 total: u64,
87 },
88}
89
90#[derive(Debug, Clone, Copy, Default)]
97pub struct Validator<'a> {
98 etag: Option<&'a ETag>,
100 last_modified: Option<&'a str>,
102}
103
104impl<'a> Validator<'a> {
105 #[must_use]
107 pub const fn new() -> Self {
108 Self {
109 etag: None,
110 last_modified: None,
111 }
112 }
113
114 #[must_use]
116 pub const fn with_etag(mut self, etag: &'a ETag) -> Self {
117 self.etag = Some(etag);
118 self
119 }
120
121 #[must_use]
123 pub const fn with_last_modified(mut self, last_modified: &'a str) -> Self {
124 self.last_modified = Some(last_modified);
125 self
126 }
127}
128
129#[must_use]
151pub fn resolve(
152 req_headers: &HeaderMap,
153 total: u64,
154 validator: Option<Validator<'_>>,
155) -> RangeResolution {
156 let Some(range_value) = req_headers.get(RANGE) else {
157 return RangeResolution::Full;
158 };
159 let Ok(range_str) = range_value.to_str() else {
160 return RangeResolution::Full;
161 };
162
163 if let Some(if_range) = req_headers.get(IF_RANGE).and_then(|v| v.to_str().ok())
167 && !if_range_matches(if_range, validator.as_ref())
168 {
169 return RangeResolution::Full;
170 }
171
172 parse_range(range_str, total)
173}
174
175enum SpecOutcome {
177 Invalid,
179 Satisfiable { start: u64, end: u64 },
181 Unsatisfiable,
183}
184
185fn parse_range(range_str: &str, total: u64) -> RangeResolution {
187 let Some(rest) = range_str.trim().strip_prefix("bytes=") else {
188 return RangeResolution::Full;
190 };
191
192 let mut saw_unsatisfiable = false;
193
194 for spec in rest.split(',') {
195 let spec = spec.trim();
196 if spec.is_empty() {
197 continue;
198 }
199 match parse_single_spec(spec, total) {
200 SpecOutcome::Invalid => {}
201 SpecOutcome::Satisfiable { start, end } => {
202 return RangeResolution::Partial { start, end, total };
204 }
205 SpecOutcome::Unsatisfiable => {
206 saw_unsatisfiable = true;
207 }
208 }
209 }
210
211 if saw_unsatisfiable {
212 RangeResolution::Unsatisfiable { total }
213 } else {
214 RangeResolution::Full
216 }
217}
218
219fn parse_single_spec(spec: &str, total: u64) -> SpecOutcome {
221 if let Some(suffix) = spec.strip_prefix('-') {
223 let Ok(n) = suffix.trim().parse::<u64>() else {
224 return SpecOutcome::Invalid;
225 };
226 if n == 0 || total == 0 {
227 return SpecOutcome::Unsatisfiable;
229 }
230 let start = total.saturating_sub(n);
232 return SpecOutcome::Satisfiable {
233 start,
234 end: total - 1,
235 };
236 }
237
238 let Some((start_s, end_s)) = spec.split_once('-') else {
239 return SpecOutcome::Invalid;
240 };
241 let Ok(start) = start_s.trim().parse::<u64>() else {
242 return SpecOutcome::Invalid;
243 };
244
245 if end_s.trim().is_empty() {
247 if total == 0 || start >= total {
248 return SpecOutcome::Unsatisfiable;
249 }
250 return SpecOutcome::Satisfiable {
251 start,
252 end: total - 1,
253 };
254 }
255
256 let Ok(end) = end_s.trim().parse::<u64>() else {
258 return SpecOutcome::Invalid;
259 };
260 if start > end {
261 return SpecOutcome::Invalid;
263 }
264 if total == 0 || start >= total {
265 return SpecOutcome::Unsatisfiable;
266 }
267 SpecOutcome::Satisfiable {
268 start,
269 end: end.min(total - 1),
270 }
271}
272
273fn if_range_matches(if_range: &str, validator: Option<&Validator<'_>>) -> bool {
279 let Some(validator) = validator else {
280 return false;
281 };
282 let trimmed = if_range.trim();
283
284 if trimmed.starts_with('"') || trimmed.starts_with("W/") {
286 if trimmed.starts_with("W/") {
288 return false;
289 }
290 let Some(etag) = validator.etag else {
291 return false;
292 };
293 if etag.is_weak() {
294 return false;
295 }
296 trimmed.trim_matches('"') == etag.tag()
297 } else {
298 validator
302 .last_modified
303 .is_some_and(|lm| lm.trim() == trimmed)
304 }
305}
306
307fn slice_inclusive(full: &Bytes, start: u64, end: u64) -> Bytes {
315 if full.is_empty() {
316 return Bytes::new();
317 }
318 let last = full.len() - 1;
319 let lo = usize::try_from(start).unwrap_or(usize::MAX).min(last);
320 let hi = usize::try_from(end).unwrap_or(usize::MAX).min(last);
321 if lo > hi {
322 return Bytes::new();
323 }
324 full.slice(lo..=hi)
325}
326
327#[must_use]
339pub fn partial_bytes_response(resolution: &RangeResolution, full: Bytes) -> Response<Body> {
340 match *resolution {
341 RangeResolution::Full => {
342 let len = full.len();
343 let mut response = Response::new(Body::from(full));
344 set_accept_ranges(response.headers_mut());
345 response
346 .headers_mut()
347 .insert(CONTENT_LENGTH, HeaderValue::from(len));
348 response
349 }
350 RangeResolution::Partial { start, end, total } => {
351 let slice = slice_inclusive(&full, start, end);
352 let len = slice.len();
353 let mut response = Response::new(Body::from(slice));
354 *response.status_mut() = StatusCode::PARTIAL_CONTENT;
355 let headers = response.headers_mut();
356 set_accept_ranges(headers);
357 if let Ok(v) = HeaderValue::from_str(&content_range_value(start, end, total)) {
358 headers.insert(CONTENT_RANGE, v);
359 }
360 headers.insert(CONTENT_LENGTH, HeaderValue::from(len));
361 response
362 }
363 RangeResolution::Unsatisfiable { total } => {
364 let mut response = Response::new(Body::empty());
365 *response.status_mut() = StatusCode::RANGE_NOT_SATISFIABLE;
366 let headers = response.headers_mut();
367 set_accept_ranges(headers);
368 if let Ok(v) = HeaderValue::from_str(&unsatisfied_content_range(total)) {
369 headers.insert(CONTENT_RANGE, v);
370 }
371 headers.insert(CONTENT_LENGTH, HeaderValue::from(0));
372 response
373 }
374 }
375}
376
377#[must_use]
380pub fn content_range_value(start: u64, end: u64, total: u64) -> String {
381 format!("bytes {start}-{end}/{total}")
382}
383
384#[must_use]
386pub fn unsatisfied_content_range(total: u64) -> String {
387 format!("bytes */{total}")
388}
389
390pub fn set_accept_ranges(headers: &mut HeaderMap) {
392 headers.insert(ACCEPT_RANGES, HeaderValue::from_static("bytes"));
393}
394
395#[cfg(test)]
396mod tests {
397 use super::*;
398
399 fn headers_with_range(value: &str) -> HeaderMap {
400 let mut h = HeaderMap::new();
401 h.insert(RANGE, HeaderValue::from_str(value).unwrap());
402 h
403 }
404
405 #[test]
408 fn no_range_header_is_full() {
409 assert_eq!(resolve(&HeaderMap::new(), 10, None), RangeResolution::Full);
410 }
411
412 #[test]
413 fn simple_closed_range() {
414 assert_eq!(
415 resolve(&headers_with_range("bytes=0-3"), 10, None),
416 RangeResolution::Partial {
417 start: 0,
418 end: 3,
419 total: 10
420 }
421 );
422 }
423
424 #[test]
425 fn open_ended_range_extends_to_eof() {
426 assert_eq!(
427 resolve(&headers_with_range("bytes=5-"), 10, None),
428 RangeResolution::Partial {
429 start: 5,
430 end: 9,
431 total: 10
432 }
433 );
434 }
435
436 #[test]
437 fn suffix_range_takes_last_n_bytes() {
438 assert_eq!(
439 resolve(&headers_with_range("bytes=-4"), 10, None),
440 RangeResolution::Partial {
441 start: 6,
442 end: 9,
443 total: 10
444 }
445 );
446 }
447
448 #[test]
449 fn suffix_range_clamps_when_larger_than_total() {
450 assert_eq!(
451 resolve(&headers_with_range("bytes=-100"), 10, None),
452 RangeResolution::Partial {
453 start: 0,
454 end: 9,
455 total: 10
456 }
457 );
458 }
459
460 #[test]
461 fn closed_end_is_clamped_to_last_byte() {
462 assert_eq!(
463 resolve(&headers_with_range("bytes=2-999"), 10, None),
464 RangeResolution::Partial {
465 start: 2,
466 end: 9,
467 total: 10
468 }
469 );
470 }
471
472 #[test]
473 fn multi_range_collapses_to_first_satisfiable() {
474 assert_eq!(
475 resolve(&headers_with_range("bytes=0-3,5-7"), 10, None),
476 RangeResolution::Partial {
477 start: 0,
478 end: 3,
479 total: 10
480 }
481 );
482 }
483
484 #[test]
485 fn multi_range_skips_leading_unsatisfiable() {
486 assert_eq!(
488 resolve(&headers_with_range("bytes=100-200,2-4"), 10, None),
489 RangeResolution::Partial {
490 start: 2,
491 end: 4,
492 total: 10
493 }
494 );
495 }
496
497 #[test]
498 fn start_beyond_eof_is_unsatisfiable() {
499 assert_eq!(
500 resolve(&headers_with_range("bytes=100-"), 10, None),
501 RangeResolution::Unsatisfiable { total: 10 }
502 );
503 }
504
505 #[test]
506 fn non_numeric_range_is_full() {
507 assert_eq!(
508 resolve(&headers_with_range("bytes=abc"), 10, None),
509 RangeResolution::Full
510 );
511 }
512
513 #[test]
514 fn backwards_range_is_full() {
515 assert_eq!(
516 resolve(&headers_with_range("bytes=5-2"), 10, None),
517 RangeResolution::Full
518 );
519 }
520
521 #[test]
522 fn non_bytes_unit_is_full() {
523 assert_eq!(
524 resolve(&headers_with_range("items=0-3"), 10, None),
525 RangeResolution::Full
526 );
527 }
528
529 #[test]
530 fn zero_total_range_is_unsatisfiable() {
531 assert_eq!(
532 resolve(&headers_with_range("bytes=0-3"), 0, None),
533 RangeResolution::Unsatisfiable { total: 0 }
534 );
535 }
536
537 #[test]
538 fn zero_suffix_is_unsatisfiable() {
539 assert_eq!(
540 resolve(&headers_with_range("bytes=-0"), 10, None),
541 RangeResolution::Unsatisfiable { total: 10 }
542 );
543 }
544
545 #[test]
548 fn if_range_matching_etag_honours_range() {
549 let etag = ETag::strong("v1");
550 let mut h = headers_with_range("bytes=0-3");
551 h.insert(IF_RANGE, etag.header_value());
552 let validator = Validator::new().with_etag(&etag);
553 assert_eq!(
554 resolve(&h, 10, Some(validator)),
555 RangeResolution::Partial {
556 start: 0,
557 end: 3,
558 total: 10
559 }
560 );
561 }
562
563 #[test]
564 fn if_range_stale_etag_falls_back_to_full() {
565 let current = ETag::strong("v2");
566 let mut h = headers_with_range("bytes=0-3");
567 h.insert(IF_RANGE, HeaderValue::from_static("\"v1\""));
568 let validator = Validator::new().with_etag(¤t);
569 assert_eq!(resolve(&h, 10, Some(validator)), RangeResolution::Full);
570 }
571
572 #[test]
573 fn if_range_weak_etag_never_matches() {
574 let weak = ETag::weak("v1");
575 let mut h = headers_with_range("bytes=0-3");
576 h.insert(IF_RANGE, HeaderValue::from_static("W/\"v1\""));
577 let validator = Validator::new().with_etag(&weak);
578 assert_eq!(resolve(&h, 10, Some(validator)), RangeResolution::Full);
579 }
580
581 #[test]
582 fn if_range_matching_last_modified_honours_range() {
583 let lm = "Wed, 21 Oct 2015 07:28:00 GMT";
584 let mut h = headers_with_range("bytes=0-3");
585 h.insert(
586 IF_RANGE,
587 HeaderValue::from_static("Wed, 21 Oct 2015 07:28:00 GMT"),
588 );
589 let validator = Validator::new().with_last_modified(lm);
590 assert_eq!(
591 resolve(&h, 10, Some(validator)),
592 RangeResolution::Partial {
593 start: 0,
594 end: 3,
595 total: 10
596 }
597 );
598 }
599
600 #[test]
601 fn if_range_stale_last_modified_falls_back_to_full() {
602 let lm = "Wed, 21 Oct 2015 07:28:00 GMT";
603 let mut h = headers_with_range("bytes=0-3");
604 h.insert(
605 IF_RANGE,
606 HeaderValue::from_static("Tue, 20 Oct 2015 00:00:00 GMT"),
607 );
608 let validator = Validator::new().with_last_modified(lm);
609 assert_eq!(resolve(&h, 10, Some(validator)), RangeResolution::Full);
610 }
611
612 #[test]
613 fn if_range_without_validator_falls_back_to_full() {
614 let mut h = headers_with_range("bytes=0-3");
615 h.insert(IF_RANGE, HeaderValue::from_static("\"v1\""));
616 assert_eq!(resolve(&h, 10, None), RangeResolution::Full);
617 }
618
619 #[tokio::test]
622 async fn partial_response_slices_body_and_sets_headers() {
623 use http_body_util::BodyExt as _;
624
625 let full = Bytes::from_static(b"0123456789");
626 let resolution = RangeResolution::Partial {
627 start: 2,
628 end: 5,
629 total: 10,
630 };
631 let resp = partial_bytes_response(&resolution, full);
632 assert_eq!(resp.status(), StatusCode::PARTIAL_CONTENT);
633 assert_eq!(resp.headers().get(CONTENT_RANGE).unwrap(), "bytes 2-5/10");
634 assert_eq!(resp.headers().get(CONTENT_LENGTH).unwrap(), "4");
635 assert_eq!(resp.headers().get(ACCEPT_RANGES).unwrap(), "bytes");
636 let body = resp.into_body().collect().await.unwrap().to_bytes();
637 assert_eq!(&body[..], b"2345");
638 }
639
640 #[tokio::test]
641 async fn full_response_sets_accept_ranges_and_length() {
642 let full = Bytes::from_static(b"0123456789");
643 let resp = partial_bytes_response(&RangeResolution::Full, full);
644 assert_eq!(resp.status(), StatusCode::OK);
645 assert_eq!(resp.headers().get(ACCEPT_RANGES).unwrap(), "bytes");
646 assert_eq!(resp.headers().get(CONTENT_LENGTH).unwrap(), "10");
647 }
648
649 #[tokio::test]
650 async fn unsatisfiable_response_is_416_with_star_content_range() {
651 let full = Bytes::from_static(b"0123456789");
652 let resp = partial_bytes_response(&RangeResolution::Unsatisfiable { total: 10 }, full);
653 assert_eq!(resp.status(), StatusCode::RANGE_NOT_SATISFIABLE);
654 assert_eq!(resp.headers().get(CONTENT_RANGE).unwrap(), "bytes */10");
655 }
656
657 #[test]
658 fn content_range_helpers_format_correctly() {
659 assert_eq!(content_range_value(0, 3, 10), "bytes 0-3/10");
660 assert_eq!(unsatisfied_content_range(10), "bytes */10");
661 }
662}