Skip to main content

range_requests/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2
3use std::ops::Range;
4
5use bytes::Bytes;
6
7pub mod headers;
8
9use crate::headers::{
10    content_range::{Bound, HttpContentRange, Unsatisfiable},
11    range::HttpRange,
12};
13
14/// Returns a [`BodyRange`] of [`Bytes`] if the provided [`HttpRange`] is satisfiable, otherwise it returns [`UnsatisfiableRange`].
15///
16/// [`HttpRange`]: crate::headers::range::HttpRange
17pub fn serve_file_with_http_range(
18    body: Bytes,
19    http_range: Option<HttpRange>,
20) -> Result<BodyRange<Bytes>, UnsatisfiableRange> {
21    let size = u64::try_from(body.len()).expect("we do not support 128bit usize");
22
23    let content_range = file_range(size, http_range)?;
24
25    let start = usize::try_from(content_range.range.start).expect("u64 doesn't fit usize");
26    let end = usize::try_from(content_range.range.end).expect("u64 doesn't fit usize");
27
28    Ok(BodyRange {
29        body: body.slice(start..end),
30        header: content_range.header,
31    })
32}
33
34/// Returns a [`ContentRange`] if the provided [`HttpRange`] is satisfiable, otherwise it returns [`UnsatisfiableRange`].
35///
36/// [`HttpRange`]: crate::headers::range::HttpRange
37pub fn file_range(
38    size: u64,
39    http_range: Option<HttpRange>,
40) -> Result<ContentRange, UnsatisfiableRange> {
41    let Some(http_range) = http_range else {
42        return Ok(ContentRange {
43            header: None,
44            range: 0..size,
45        });
46    };
47
48    let range = match http_range {
49        HttpRange::StartingPoint(start) if start < size => start..size,
50        HttpRange::Range(range) if range.start() < size => {
51            range.start()..range.end().saturating_add(1).min(size)
52        }
53        HttpRange::Suffix(suffix) if suffix > 0 && size > 0 => size.saturating_sub(suffix)..size,
54        // A non-zero suffix-range is satisfiable even when the representation
55        // is empty (RFC 9110 Section 14.1.2), but the `Content-Range` of a
56        // 206 cannot be expressed for an empty body. Ignore the range and
57        // serve the full (empty) representation instead, as permitted by
58        // RFC 9110 Section 14.2.
59        HttpRange::Suffix(suffix) if suffix > 0 => {
60            return Ok(ContentRange {
61                header: None,
62                range: 0..size,
63            });
64        }
65        _ => {
66            let content_range = HttpContentRange::Unsatisfiable(Unsatisfiable::new(size));
67            return Err(UnsatisfiableRange(content_range));
68        }
69    };
70
71    let content_range =
72        HttpContentRange::Bound(Bound::new(range.start..=range.end - 1, Some(size)).unwrap());
73
74    Ok(ContentRange {
75        header: Some(content_range),
76        range,
77    })
78}
79
80/// A container for the payload slice and the optional `Content-Range` header.
81///
82/// The header is `None` only if the body was not sliced.
83///
84/// If the `axum` feature is enabled this struct also implements `IntoResponse`.
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct BodyRange<T> {
87    body: T,
88    header: Option<HttpContentRange>,
89}
90
91impl<T> BodyRange<T> {
92    /// Returns the sliced body.
93    pub fn body(&self) -> &T {
94        &self.body
95    }
96
97    pub fn into_body(self) -> T {
98        self.body
99    }
100
101    /// Returns an option of [`HttpContentRange`].
102    /// It is `None` if no range was applied to the body: either no
103    /// [`HttpRange`] was provided, or the range was ignored because the
104    /// representation is empty.
105    pub fn header(&self) -> Option<HttpContentRange> {
106        self.header
107    }
108}
109
110/// A container for the payload range and the optional `Content-Range` header.
111///
112/// The header is `None` only if the body was not sliced.
113#[derive(Debug, Clone, PartialEq, Eq)]
114pub struct ContentRange {
115    header: Option<HttpContentRange>,
116    range: Range<u64>,
117}
118
119impl ContentRange {
120    /// Returns an option of [`HttpContentRange`].
121    /// It is `None` if no range was applied to the body: either no
122    /// [`HttpRange`] was provided, or the range was ignored because the
123    /// representation is empty.
124    pub fn header(&self) -> Option<HttpContentRange> {
125        self.header
126    }
127
128    /// Returns a [`Range`] of `u64` useful to manually slice the response body.
129    pub fn range(&self) -> &Range<u64> {
130        &self.range
131    }
132}
133
134/// An unsatisfiable range request.
135///
136/// If the `axum` feature is enabled this struct also implements `IntoResponse`.
137#[derive(Debug, Clone, PartialEq, Eq)]
138pub struct UnsatisfiableRange(HttpContentRange);
139
140impl UnsatisfiableRange {
141    /// Returns the [`HttpContentRange`] header.
142    pub fn header(&self) -> HttpContentRange {
143        self.0
144    }
145}
146
147#[cfg(feature = "axum")]
148mod axum {
149    use crate::{BodyRange, UnsatisfiableRange};
150
151    use axum_core::response::{IntoResponse, Response};
152    use bytes::Bytes;
153    use http::{HeaderValue, StatusCode, header::CONTENT_RANGE};
154
155    impl IntoResponse for BodyRange<Bytes> {
156        fn into_response(self) -> Response {
157            match self.header {
158                Some(range) => (
159                    StatusCode::PARTIAL_CONTENT,
160                    [(CONTENT_RANGE, HeaderValue::from(&range))],
161                    self.body,
162                )
163                    .into_response(),
164                None => (StatusCode::OK, self.body).into_response(),
165            }
166        }
167    }
168
169    impl IntoResponse for UnsatisfiableRange {
170        fn into_response(self) -> Response {
171            (
172                StatusCode::RANGE_NOT_SATISFIABLE,
173                [(CONTENT_RANGE, HeaderValue::from(&self.0))],
174            )
175                .into_response()
176        }
177    }
178}