Skip to main content

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        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/// A container for the payload slice and the optional `Content-Range` header.
77///
78/// The header is `None` only if the body was not sliced.
79///
80/// If the `axum` feature is enabled this struct also implements `IntoResponse`.
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct BodyRange<T> {
83    body: T,
84    header: Option<HttpContentRange>,
85}
86
87impl<T> BodyRange<T> {
88    /// Returns the sliced body.
89    pub fn body(&self) -> &T {
90        &self.body
91    }
92
93    pub fn into_body(self) -> T {
94        self.body
95    }
96
97    /// Returns an option of [`HttpContentRange`].
98    /// If it's None the provided [`HttpRange`] was None too.
99    pub fn header(&self) -> Option<HttpContentRange> {
100        self.header
101    }
102}
103
104/// A container for the payload range and the optional `Content-Range` header.
105///
106/// The header is `None` only if the body was not sliced.
107#[derive(Debug, Clone, PartialEq, Eq)]
108pub struct ContentRange {
109    header: Option<HttpContentRange>,
110    range: RangeInclusive<u64>,
111}
112
113impl ContentRange {
114    /// Returns an option of [`HttpContentRange`].
115    /// If it's None the provided [`HttpRange`] was None too.
116    pub fn header(&self) -> Option<HttpContentRange> {
117        self.header
118    }
119
120    /// Returns a [`RangeInclusive`] of `u64` useful to manually slice the response body.
121    pub fn range(&self) -> &RangeInclusive<u64> {
122        &self.range
123    }
124}
125
126/// An unsatisfiable range request.
127///
128/// If the `axum` feature is enabled this struct also implements `IntoResponse`.
129#[derive(Debug, Clone, PartialEq, Eq)]
130pub struct UnsatisfiableRange(HttpContentRange);
131
132impl UnsatisfiableRange {
133    /// Returns the [`HttpContentRange`] header.
134    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}