Skip to content

Commit af062ac

Browse files
ealasuseanmonstar
authored andcommitted
feat(headers): Content-Range header
1 parent 884fb1b commit af062ac

File tree

2 files changed

+191
-0
lines changed

2 files changed

+191
-0
lines changed

src/header/common/content_range.rs

+189
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
use std::fmt::{self, Display};
2+
use std::str::FromStr;
3+
4+
header! {
5+
#[doc="`Content-Range` header, defined in"]
6+
#[doc="[RFC7233](http://tools.ietf.org/html/rfc7233#section-4.2)"]
7+
(ContentRange, "Content-Range") => [ContentRangeSpec]
8+
9+
test_content_range {
10+
test_header!(test_bytes,
11+
vec![b"bytes 0-499/500"],
12+
Some(ContentRange(ContentRangeSpec::Bytes {
13+
range: Some((0, 499)),
14+
instance_length: Some(500)
15+
})));
16+
17+
test_header!(test_bytes_unknown_len,
18+
vec![b"bytes 0-499/*"],
19+
Some(ContentRange(ContentRangeSpec::Bytes {
20+
range: Some((0, 499)),
21+
instance_length: None
22+
})));
23+
24+
test_header!(test_bytes_unknown_range,
25+
vec![b"bytes */500"],
26+
Some(ContentRange(ContentRangeSpec::Bytes {
27+
range: None,
28+
instance_length: Some(500)
29+
})));
30+
31+
test_header!(test_unregistered,
32+
vec![b"seconds 1-2"],
33+
Some(ContentRange(ContentRangeSpec::Unregistered {
34+
unit: "seconds".to_string(),
35+
resp: "1-2".to_string()
36+
})));
37+
38+
test_header!(test_no_len,
39+
vec![b"bytes 0-499"],
40+
None::<ContentRange>);
41+
42+
test_header!(test_only_unit,
43+
vec![b"bytes"],
44+
None::<ContentRange>);
45+
46+
test_header!(test_end_less_than_start,
47+
vec![b"bytes 499-0/500"],
48+
None::<ContentRange>);
49+
50+
test_header!(test_blank,
51+
vec![b""],
52+
None::<ContentRange>);
53+
54+
test_header!(test_bytes_many_spaces,
55+
vec![b"bytes 1-2/500 3"],
56+
None::<ContentRange>);
57+
58+
test_header!(test_bytes_many_slashes,
59+
vec![b"bytes 1-2/500/600"],
60+
None::<ContentRange>);
61+
62+
test_header!(test_bytes_many_dashes,
63+
vec![b"bytes 1-2-3/500"],
64+
None::<ContentRange>);
65+
66+
}
67+
}
68+
69+
70+
/// Content-Range, described in [RFC7233](https://tools.ietf.org/html/rfc7233#section-4.2)
71+
///
72+
/// # ABNF
73+
/// ```plain
74+
/// Content-Range = byte-content-range
75+
/// / other-content-range
76+
///
77+
/// byte-content-range = bytes-unit SP
78+
/// ( byte-range-resp / unsatisfied-range )
79+
///
80+
/// byte-range-resp = byte-range "/" ( complete-length / "*" )
81+
/// byte-range = first-byte-pos "-" last-byte-pos
82+
/// unsatisfied-range = "*/" complete-length
83+
///
84+
/// complete-length = 1*DIGIT
85+
///
86+
/// other-content-range = other-range-unit SP other-range-resp
87+
/// other-range-resp = *CHAR
88+
/// ```
89+
#[derive(PartialEq, Clone, Debug)]
90+
pub enum ContentRangeSpec {
91+
/// Byte range
92+
Bytes {
93+
/// First and last bytes of the range, omitted if request could not be
94+
/// satisfied
95+
range: Option<(u64, u64)>,
96+
97+
/// Total length of the instance, can be omitted if unknown
98+
instance_length: Option<u64>
99+
},
100+
101+
/// Custom range, with unit not registered at IANA
102+
Unregistered {
103+
/// other-range-unit
104+
unit: String,
105+
106+
/// other-range-resp
107+
resp: String
108+
}
109+
}
110+
111+
fn split_in_two<'a>(s: &'a str, separator: char) -> Option<(&'a str, &'a str)> {
112+
let mut iter = s.splitn(2, separator);
113+
match (iter.next(), iter.next()) {
114+
(Some(a), Some(b)) => Some((a, b)),
115+
_ => None
116+
}
117+
}
118+
119+
impl FromStr for ContentRangeSpec {
120+
type Err = ::Error;
121+
122+
fn from_str(s: &str) -> ::Result<Self> {
123+
let res = match split_in_two(s, ' ') {
124+
Some(("bytes", resp)) => {
125+
let (range, instance_length) = try!(split_in_two(resp, '/').ok_or(::Error::Header));
126+
127+
let instance_length = if instance_length == "*" {
128+
None
129+
} else {
130+
Some(try!(instance_length.parse().map_err(|_| ::Error::Header)))
131+
};
132+
133+
let range = if range == "*" {
134+
None
135+
} else {
136+
let (first_byte, last_byte) = try!(split_in_two(range, '-').ok_or(::Error::Header));
137+
let first_byte = try!(first_byte.parse().map_err(|_| ::Error::Header));
138+
let last_byte = try!(last_byte.parse().map_err(|_| ::Error::Header));
139+
if last_byte < first_byte {
140+
return Err(::Error::Header);
141+
}
142+
Some((first_byte, last_byte))
143+
};
144+
145+
ContentRangeSpec::Bytes {
146+
range: range,
147+
instance_length: instance_length
148+
}
149+
}
150+
Some((unit, resp)) => {
151+
ContentRangeSpec::Unregistered {
152+
unit: unit.to_string(),
153+
resp: resp.to_string()
154+
}
155+
}
156+
_ => return Err(::Error::Header)
157+
};
158+
Ok(res)
159+
}
160+
}
161+
162+
impl Display for ContentRangeSpec {
163+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
164+
match *self {
165+
ContentRangeSpec::Bytes { range, instance_length } => {
166+
try!(f.write_str("bytes "));
167+
match range {
168+
Some((first_byte, last_byte)) => {
169+
try!(write!(f, "{}-{}", first_byte, last_byte));
170+
},
171+
None => {
172+
try!(f.write_str("*"));
173+
}
174+
};
175+
try!(f.write_str("/"));
176+
if let Some(v) = instance_length {
177+
write!(f, "{}", v)
178+
} else {
179+
f.write_str("*")
180+
}
181+
}
182+
ContentRangeSpec::Unregistered { ref unit, ref resp } => {
183+
try!(f.write_str(&unit));
184+
try!(f.write_str(" "));
185+
f.write_str(&resp)
186+
}
187+
}
188+
}
189+
}

src/header/common/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ pub use self::connection::{Connection, ConnectionOption};
2424
pub use self::content_length::ContentLength;
2525
pub use self::content_encoding::ContentEncoding;
2626
pub use self::content_language::ContentLanguage;
27+
pub use self::content_range::{ContentRange, ContentRangeSpec};
2728
pub use self::content_type::ContentType;
2829
pub use self::cookie::Cookie;
2930
pub use self::date::Date;
@@ -368,6 +369,7 @@ mod connection;
368369
mod content_encoding;
369370
mod content_language;
370371
mod content_length;
372+
mod content_range;
371373
mod content_type;
372374
mod date;
373375
mod etag;

0 commit comments

Comments
 (0)