http_content_range/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::str::FromStr;
4
5use crate::utils::{fail_if, is_whitespace, IterExt};
6
7mod utils;
8
9const PREFIX: &[u8] = b"bytes";
10
11/// HTTP Content-Range response header representation.
12#[derive(Debug, Clone, Copy, Eq, PartialEq)]
13pub enum ContentRange {
14    /// Regular bytes range response with status 206
15    Bytes(ContentRangeBytes),
16    /// Regular bytes range response with status 206
17    UnboundBytes(ContentRangeUnbound),
18    /// Server response with status 416
19    Unsatisfied(ContentRangeUnsatisfied),
20}
21
22#[derive(Debug, Clone, Copy, Eq, PartialEq)]
23pub struct ContentRangeBytes {
24    pub first_byte: u64,
25    pub last_byte: u64,
26    pub complete_length: u64,
27}
28
29#[derive(Debug, Clone, Copy, Eq, PartialEq)]
30pub struct ContentRangeUnbound {
31    pub first_byte: u64,
32    pub last_byte: u64,
33}
34
35#[derive(Debug, Clone, Copy, Eq, PartialEq)]
36pub struct ContentRangeUnsatisfied {
37    pub complete_length: u64,
38}
39
40impl TryFrom<&str> for ContentRange {
41    type Error = ();
42
43    fn try_from(value: &str) -> Result<Self, Self::Error> {
44        Self::parse(value).ok_or(())
45    }
46}
47
48impl TryFrom<&[u8]> for ContentRange {
49    type Error = ();
50
51    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
52        Self::parse_bytes(value).ok_or(())
53    }
54}
55
56impl FromStr for ContentRange {
57    type Err = ();
58
59    fn from_str(s: &str) -> Result<Self, Self::Err> {
60        Self::try_from(s)
61    }
62}
63
64impl ContentRange {
65    /// Parses Content-Range HTTP header string as per
66    /// [RFC 7233](https://httpwg.org/specs/rfc7233.html#header.content-range).
67    ///
68    /// `header` is the HTTP Content-Range header (e.g. `bytes 0-9/30`).
69    ///
70    /// This parser is a bit more lenient than the official RFC, it allows spaces and tabs between everything.
71    /// See <https://httpwg.org/specs/rfc7233.html#rfc.section.4.2>
72    ///
73    /// ```
74    /// # use http_content_range::{ContentRange, ContentRangeBytes, ContentRangeUnbound, ContentRangeUnsatisfied};
75    /// assert_eq!(ContentRange::parse("bytes 42-69/420").unwrap(),
76    ///     ContentRange::Bytes(ContentRangeBytes{first_byte: 42, last_byte: 69, complete_length: 420}));
77    ///
78    /// // complete_length is unknown
79    /// assert_eq!(ContentRange::parse("bytes 42-69/*").unwrap(),
80    ///    ContentRange::UnboundBytes(ContentRangeUnbound{first_byte: 42, last_byte: 69}));
81    ///
82    /// // response is unsatisfied
83    /// assert_eq!(ContentRange::parse("bytes */420").unwrap(),
84    ///   ContentRange::Unsatisfied(ContentRangeUnsatisfied{complete_length: 420}));
85    /// ```
86    #[must_use]
87    #[inline]
88    pub fn parse(header: &str) -> Option<ContentRange> {
89        Self::parse_bytes(header.as_bytes())
90    }
91
92    /// From <https://httpwg.org/specs/rfc7233.html#rfc.section.4.2>
93    /// Valid bytes responses:
94    ///   Content-Range: bytes 42-1233/1234
95    ///   Content-Range: bytes 42-1233/*
96    ///   Content-Range: bytes */1233
97    ///
98    /// ```none
99    ///   Content-Range       = byte-content-range
100    ///                       / other-content-range
101    ///
102    ///   byte-content-range  = bytes-unit SP
103    ///                         ( byte-range-resp / unsatisfied-range )
104    ///
105    ///   byte-range-resp     = byte-range "/" ( complete-length / "*" )
106    ///   byte-range          = first-byte-pos "-" last-byte-pos
107    ///   unsatisfied-range   = "*/" complete-length
108    ///
109    ///   complete-length     = 1*DIGIT
110    ///
111    ///   other-content-range = other-range-unit SP other-range-resp
112    ///   other-range-resp    = *CHAR
113    /// ```
114    /// Same as [`parse`](Self::parse) but parses directly from the byte array
115    #[must_use]
116    pub fn parse_bytes(header: &[u8]) -> Option<ContentRange> {
117        if !header.starts_with(PREFIX) {
118            return None;
119        }
120
121        let mut iter = header[PREFIX.len()..].iter().peekable();
122
123        // must start with a space
124        fail_if(!is_whitespace(*iter.next()?))?;
125        let res = if iter.skip_spaces()? == b'*' {
126            // Unsatisfied range
127            iter.next()?; // consume '*'
128            iter.parse_separator(b'/')?;
129            ContentRange::Unsatisfied(ContentRangeUnsatisfied {
130                complete_length: iter.parse_u64()?,
131            })
132        } else {
133            // byte range
134            let first_byte = iter.parse_u64()?;
135            iter.parse_separator(b'-')?;
136            let last_byte = iter.parse_u64()?;
137            fail_if(first_byte > last_byte)?;
138            if iter.parse_separator(b'/')? == b'*' {
139                // unbound byte range, consume '*'
140                iter.next()?;
141                ContentRange::UnboundBytes(ContentRangeUnbound {
142                    first_byte,
143                    last_byte,
144                })
145            } else {
146                let complete_length = iter.parse_u64()?;
147                fail_if(last_byte >= complete_length)?;
148                ContentRange::Bytes(ContentRangeBytes {
149                    first_byte,
150                    last_byte,
151                    complete_length,
152                })
153            }
154        };
155
156        // verify there is nothing left
157        match iter.skip_spaces() {
158            None => Some(res),
159            Some(_) => None,
160        }
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    #![allow(clippy::unnecessary_wraps)]
167
168    use super::*;
169
170    fn bytes(first_byte: u64, last_byte: u64, complete_length: u64) -> Option<ContentRange> {
171        Some(ContentRange::Bytes(ContentRangeBytes {
172            first_byte,
173            last_byte,
174            complete_length,
175        }))
176    }
177
178    fn unbound(first_byte: u64, last_byte: u64) -> Option<ContentRange> {
179        Some(ContentRange::UnboundBytes(ContentRangeUnbound {
180            first_byte,
181            last_byte,
182        }))
183    }
184
185    fn unsatisfied(complete_length: u64) -> Option<ContentRange> {
186        Some(ContentRange::Unsatisfied(ContentRangeUnsatisfied {
187            complete_length,
188        }))
189    }
190
191    #[test]
192    fn test_parse() {
193        for (header, expected) in vec![
194            // Valid
195            ("bytes 0-9/20", bytes(0, 9, 20)),
196            ("bytes\t 0 \t -\t \t  \t9 / 20   ", bytes(0, 9, 20)),
197            ("bytes */20", unsatisfied(20)),
198            ("bytes   *\t\t/  20    ", unsatisfied(20)),
199            ("bytes 0-9/*", unbound(0, 9)),
200            ("bytes   0  -    9  /  *   ", unbound(0, 9)),
201            //
202            // Errors
203            //
204            ("", None),
205            ("b", None),
206            ("foo", None),
207            ("foo 1-2/3", None),
208            (" bytes 1-2/3", None),
209            ("bytes -2/3", None),
210            ("bytes 1-/3", None),
211            ("bytes 1-2/", None),
212            ("bytes 1-2/a", None),
213            ("bytes1-2/3", None),
214            ("bytes=1-2/3", None),
215            ("bytes a-2/3", None),
216            ("bytes 1-a/3", None),
217            ("bytes 0x01-0x02/3", None),
218            ("bytes 1-2/a", None),
219            (
220                "bytes 1111111111111111111111111111111111111111111-2/1",
221                None,
222            ),
223            ("bytes 1-3/20 1", None),
224            ("bytes 1-3/* 1", None),
225            ("bytes */1 1", None),
226            ("bytes 1-0/20", None),
227            ("bytes 1-20/20", None),
228            ("bytes 1-21/20", None),
229        ] {
230            assert_eq!(ContentRange::parse(header), expected);
231            assert_eq!(ContentRange::try_from(header).ok(), expected);
232            assert_eq!(ContentRange::from_str(header).ok(), expected);
233            assert_eq!(ContentRange::try_from(header.as_bytes()).ok(), expected);
234        }
235    }
236}