range_requests/
lib.rs

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
17/// Returns a [`BodyRange`] of [`Bytes`] if the provided [`HttpRange`] is satisfiable, otherwise it returns [`UnsatisfiableRange`].
18///
19/// [`HttpRange`]: crate::headers::range::HttpRange
20pub 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
40/// Returns a [`ContentRange`] if the provided [`HttpRange`] is satisfiable, otherwise it returns [`UnsatisfiableRange`].
41///
42/// [`HttpRange`]: crate::headers::range::HttpRange
43pub 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        let end = size - 1;
51        return Ok(ContentRange {
52            header: None,
53            range: 0..=end,
54        });
55    };
56
57    match http_range {
58        HttpRange::StartingPoint(start) if size > start => {
59            let end = size - 1;
60
61            let content_range =
62                HttpContentRange::Bound(Bound::new(start..=end, Some(size)).unwrap());
63
64            Ok(ContentRange {
65                header: Some(content_range),
66                range: start..=end,
67            })
68        }
69        HttpRange::Range(ordered_range) if size > ordered_range.end() => {
70            let start = ordered_range.start();
71            let end = ordered_range.end();
72
73            let content_range =
74                HttpContentRange::Bound(Bound::new(start..=end, Some(size)).unwrap());
75
76            Ok(ContentRange {
77                header: Some(content_range),
78                range: start..=end,
79            })
80        }
81        HttpRange::Suffix(suffix) if size.checked_sub(suffix).is_some() => {
82            let start = size - suffix;
83            let end = size - 1;
84            let content_range =
85                HttpContentRange::Bound(Bound::new(start..=end, Some(size)).unwrap());
86
87            Ok(ContentRange {
88                header: Some(content_range),
89                range: start..=end,
90            })
91        }
92        _ => {
93            let content_range = HttpContentRange::Unsatisfiable(
94                crate::headers::content_range::Unsatisfiable::new(size),
95            );
96
97            Err(UnsatisfiableRange(content_range))
98        }
99    }
100}
101
102/// A container for the payload slice and the optional `Content-Range` header.
103///
104/// The header is `None` only if the body was not sliced.
105///
106/// If the `axum` feature is enabled this struct also implements `IntoResponse`.
107#[derive(Debug, Clone, PartialEq, Eq)]
108pub struct BodyRange<T> {
109    body: T,
110    header: Option<HttpContentRange>,
111}
112
113impl<T> BodyRange<T> {
114    /// Returns the sliced body.
115    pub fn body(&self) -> &T {
116        &self.body
117    }
118
119    pub fn into_body(self) -> T {
120        self.body
121    }
122
123    /// Returns an option of [`HttpContentRange`].
124    /// If it's None the provided [`HttpRange`] was None too.
125    pub fn header(&self) -> Option<HttpContentRange> {
126        self.header
127    }
128}
129
130/// A container for the payload range and the optional `Content-Range` header.
131///
132/// The header is `None` only if the body was not sliced.
133#[derive(Debug, Clone, PartialEq, Eq)]
134pub struct ContentRange {
135    header: Option<HttpContentRange>,
136    range: RangeInclusive<u64>,
137}
138
139impl ContentRange {
140    /// Returns an option of [`HttpContentRange`].
141    /// If it's None the provided [`HttpRange`] was None too.
142    pub fn header(&self) -> Option<HttpContentRange> {
143        self.header
144    }
145
146    /// Returns a [`RangeInclusive`] of `u64` useful to manually slice the response body.
147    pub fn range(&self) -> &RangeInclusive<u64> {
148        &self.range
149    }
150}
151
152/// An unsatisfiable range request.
153///
154/// If the `axum` feature is enabled this struct also implements `IntoResponse`.
155#[derive(Debug, Clone, PartialEq, Eq)]
156pub struct UnsatisfiableRange(HttpContentRange);
157
158impl UnsatisfiableRange {
159    /// Returns the [`HttpContentRange`] header.
160    pub fn header(&self) -> HttpContentRange {
161        self.0
162    }
163}
164
165#[cfg(feature = "axum")]
166mod axum {
167    use crate::{BodyRange, UnsatisfiableRange};
168
169    use axum_core::response::{IntoResponse, Response};
170    use bytes::Bytes;
171    use http::{HeaderValue, StatusCode, header::CONTENT_RANGE};
172
173    impl IntoResponse for BodyRange<Bytes> {
174        fn into_response(self) -> Response {
175            match self.header {
176                Some(range) => (
177                    StatusCode::PARTIAL_CONTENT,
178                    [(CONTENT_RANGE, HeaderValue::from(&range))],
179                    self.body,
180                )
181                    .into_response(),
182                None => (StatusCode::OK, self.body).into_response(),
183            }
184        }
185    }
186
187    impl IntoResponse for UnsatisfiableRange {
188        fn into_response(self) -> Response {
189            (
190                StatusCode::RANGE_NOT_SATISFIABLE,
191                [(CONTENT_RANGE, HeaderValue::from(&self.0))],
192            )
193                .into_response()
194        }
195    }
196}