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#[derive(Debug, Clone, Copy, Eq, PartialEq)]
13pub enum ContentRange {
14 Bytes(ContentRangeBytes),
16 UnboundBytes(ContentRangeUnbound),
18 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 #[must_use]
87 #[inline]
88 pub fn parse(header: &str) -> Option<ContentRange> {
89 Self::parse_bytes(header.as_bytes())
90 }
91
92 #[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 fail_if(!is_whitespace(*iter.next()?))?;
125 let res = if iter.skip_spaces()? == b'*' {
126 iter.next()?; iter.parse_separator(b'/')?;
129 ContentRange::Unsatisfied(ContentRangeUnsatisfied {
130 complete_length: iter.parse_u64()?,
131 })
132 } else {
133 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 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 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 ("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 ("", 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}