1#![cfg_attr(docsrs, feature(doc_cfg))]
2
3use std::{
4 num::{NonZero, NonZeroU64},
5 ops::RangeInclusive,
6};
7
8use bytes::Bytes;
9
10pub mod headers;
11
12use crate::headers::{
13 content_range::{Bound, HttpContentRange, Unsatisfiable},
14 range::HttpRange,
15};
16
17pub fn serve_file_with_http_range(
21 body: Bytes,
22 http_range: Option<HttpRange>,
23) -> Result<BodyRange<Bytes>, UnsatisfiableRange> {
24 let size = u64::try_from(body.len()).expect("we do not support 128bit usize");
25 let size = NonZeroU64::try_from(size).map_err(|_| {
26 UnsatisfiableRange(HttpContentRange::Unsatisfiable(Unsatisfiable::new(size)))
27 })?;
28
29 let content_range = file_range(size, http_range)?;
30
31 let start = usize::try_from(*content_range.range.start()).expect("u64 doesn't fit usize");
32 let end = usize::try_from(*content_range.range.end()).expect("u64 doesn't fit usize");
33
34 Ok(BodyRange {
35 body: body.slice(start..=end),
36 header: content_range.header,
37 })
38}
39
40pub fn file_range(
44 size: NonZero<u64>,
45 http_range: Option<HttpRange>,
46) -> Result<ContentRange, UnsatisfiableRange> {
47 let size = size.get();
48
49 let Some(http_range) = http_range else {
50 return Ok(ContentRange {
51 header: None,
52 range: 0..=size - 1,
53 });
54 };
55
56 let range = match http_range {
57 HttpRange::StartingPoint(start) if start < size => start..=size - 1,
58 HttpRange::Range(range) if range.start() < size => {
59 range.start()..=range.end().min(size - 1)
60 }
61 HttpRange::Suffix(suffix) if suffix > 0 => size.saturating_sub(suffix)..=size - 1,
62 _ => {
63 let content_range = HttpContentRange::Unsatisfiable(Unsatisfiable::new(size));
64 return Err(UnsatisfiableRange(content_range));
65 }
66 };
67
68 let content_range = HttpContentRange::Bound(Bound::new(range.clone(), Some(size)).unwrap());
69
70 Ok(ContentRange {
71 header: Some(content_range),
72 range,
73 })
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct BodyRange<T> {
83 body: T,
84 header: Option<HttpContentRange>,
85}
86
87impl<T> BodyRange<T> {
88 pub fn body(&self) -> &T {
90 &self.body
91 }
92
93 pub fn into_body(self) -> T {
94 self.body
95 }
96
97 pub fn header(&self) -> Option<HttpContentRange> {
100 self.header
101 }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq)]
108pub struct ContentRange {
109 header: Option<HttpContentRange>,
110 range: RangeInclusive<u64>,
111}
112
113impl ContentRange {
114 pub fn header(&self) -> Option<HttpContentRange> {
117 self.header
118 }
119
120 pub fn range(&self) -> &RangeInclusive<u64> {
122 &self.range
123 }
124}
125
126#[derive(Debug, Clone, PartialEq, Eq)]
130pub struct UnsatisfiableRange(HttpContentRange);
131
132impl UnsatisfiableRange {
133 pub fn header(&self) -> HttpContentRange {
135 self.0
136 }
137}
138
139#[cfg(feature = "axum")]
140mod axum {
141 use crate::{BodyRange, UnsatisfiableRange};
142
143 use axum_core::response::{IntoResponse, Response};
144 use bytes::Bytes;
145 use http::{HeaderValue, StatusCode, header::CONTENT_RANGE};
146
147 impl IntoResponse for BodyRange<Bytes> {
148 fn into_response(self) -> Response {
149 match self.header {
150 Some(range) => (
151 StatusCode::PARTIAL_CONTENT,
152 [(CONTENT_RANGE, HeaderValue::from(&range))],
153 self.body,
154 )
155 .into_response(),
156 None => (StatusCode::OK, self.body).into_response(),
157 }
158 }
159 }
160
161 impl IntoResponse for UnsatisfiableRange {
162 fn into_response(self) -> Response {
163 (
164 StatusCode::RANGE_NOT_SATISFIABLE,
165 [(CONTENT_RANGE, HeaderValue::from(&self.0))],
166 )
167 .into_response()
168 }
169 }
170}