1- //! HTTP Range request types for partial content retrieval.
2- //!
3- //! [`ByteRange`] represents a parsed single byte-range from a `Range` HTTP
4- //! header. [`ContentRange`] describes which portion of an object is present in
5- //! a response body, used to build `Content-Range` response headers and choose
6- //! between 200 and 206 status codes.
1+ //! Types for HTTP range requests.
72
83use std:: fmt;
94
10- /// A single byte-range parsed from the `Range` HTTP request header.
11- ///
12- /// Only the `bytes` range unit is supported, and only a single range specifier
13- /// (multi-range requests are rejected at parse time).
5+ use http:: header:: HeaderValue ;
6+
7+ /// A byte range.
148#[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
159pub enum ByteRange {
16- /// `bytes=N-M` — from byte offset N to byte offset M (both inclusive).
17- FromTo ( u64 , u64 ) ,
18- /// `bytes=N-` — from byte offset N to the end of the representation.
10+ /// Bounded range with start and end, inclusive
11+ Inclusive ( u64 , u64 ) ,
12+ /// From offset X onwards
1913 From ( u64 ) ,
20- /// `bytes=-N` — the last N bytes of the representation.
21- Suffix ( u64 ) ,
14+ /// Last X bytes
15+ Last ( u64 ) ,
2216}
2317
2418impl ByteRange {
25- /// Parses a `Range` header value into a [`ByteRange`] .
19+ /// Formats this range as a `Range` request header value (e.g. `bytes=0-499`) .
2620 ///
27- /// Only `bytes=` ranges with a single specifier are accepted. Multi-range
28- /// requests (containing commas) and non-`bytes` units are rejected.
29- pub fn parse ( header : & str ) -> Result < Self , RangeError > {
30- let lower = header. to_ascii_lowercase ( ) ;
31- let spec = lower
32- . strip_prefix ( "bytes=" )
33- . ok_or ( RangeError :: UnknownUnit ) ?;
34-
35- if spec. contains ( ',' ) {
36- return Err ( RangeError :: MultiRangeNotSupported ) ;
37- }
38-
39- let ( start_str, end_str) = spec. split_once ( '-' ) . ok_or ( RangeError :: InvalidRange ) ?;
40-
41- if start_str. is_empty ( ) {
42- // bytes=-N (suffix)
43- let n: u64 = end_str. parse ( ) . map_err ( |_| RangeError :: InvalidRange ) ?;
44- if n == 0 {
45- return Err ( RangeError :: InvalidRange ) ;
46- }
47- Ok ( ByteRange :: Suffix ( n) )
48- } else if end_str. is_empty ( ) {
49- // bytes=N- (from offset to end)
50- let start: u64 = start_str. parse ( ) . map_err ( |_| RangeError :: InvalidRange ) ?;
51- Ok ( ByteRange :: From ( start) )
52- } else {
53- // bytes=N-M
54- let start: u64 = start_str. parse ( ) . map_err ( |_| RangeError :: InvalidRange ) ?;
55- let end: u64 = end_str. parse ( ) . map_err ( |_| RangeError :: InvalidRange ) ?;
56- if start > end {
57- return Err ( RangeError :: InvalidRange ) ;
58- }
59- Ok ( ByteRange :: FromTo ( start, end) )
60- }
61- }
62-
63- /// Formats this range as a `Range` header value (e.g. `bytes=0-499`).
64- pub fn to_header_value ( & self ) -> String {
65- match self {
66- ByteRange :: FromTo ( s, e) => format ! ( "bytes={s}-{e}" ) ,
21+ /// The returned value is always valid ASCII and can be inserted directly
22+ /// into an HTTP header map.
23+ pub fn to_header_value ( & self ) -> HeaderValue {
24+ let s = match self {
25+ ByteRange :: Inclusive ( s, e) => format ! ( "bytes={s}-{e}" ) ,
6726 ByteRange :: From ( s) => format ! ( "bytes={s}-" ) ,
68- ByteRange :: Suffix ( n) => format ! ( "bytes=-{n}" ) ,
69- }
27+ ByteRange :: Last ( n) => format ! ( "bytes=-{n}" ) ,
28+ } ;
29+ // SAFETY: the format only contains ASCII digits, hyphens, and the
30+ // literal prefix "bytes=", which are all valid header value bytes.
31+ HeaderValue :: from_str ( & s) . expect ( "ByteRange always produces a valid header value" )
7032 }
7133
7234 /// Resolves this range against a known total size, returning the concrete
73- /// byte offsets and total, or `None` if the range is unsatisfiable.
35+ /// byte offsets and total, or [ `None`] if the range is unsatisfiable.
7436 pub fn resolve ( self , total : u64 ) -> Option < ContentRange > {
7537 if total == 0 {
7638 return None ;
7739 }
7840
7941 let ( start, end) = match self {
80- ByteRange :: FromTo ( s, e) => {
42+ ByteRange :: Inclusive ( s, e) => {
8143 if s >= total {
8244 return None ;
8345 }
@@ -89,7 +51,7 @@ impl ByteRange {
8951 }
9052 ( s, total - 1 )
9153 }
92- ByteRange :: Suffix ( n) => {
54+ ByteRange :: Last ( n) => {
9355 let start = total. saturating_sub ( n) ;
9456 ( start, total - 1 )
9557 }
@@ -99,6 +61,52 @@ impl ByteRange {
9961 }
10062}
10163
64+ /// Parses a `Range` request header string into a [`ByteRange`].
65+ ///
66+ /// Only `bytes=` ranges with a single specifier are accepted. Multi-range
67+ /// requests (containing commas) and non-`bytes` units are rejected.
68+ ///
69+ /// Converting a [`http::header::HeaderValue`] to `&str` via
70+ /// [`HeaderValue::to_str`] is the caller's responsibility, since that
71+ /// conversion can fail when the value contains non-visible-ASCII bytes.
72+ impl TryFrom < & str > for ByteRange {
73+ type Error = RangeError ;
74+
75+ fn try_from ( header : & str ) -> Result < Self , Self :: Error > {
76+ let lower = header. to_ascii_lowercase ( ) ;
77+ let spec = lower
78+ . strip_prefix ( "bytes=" )
79+ . ok_or ( RangeError :: UnknownUnit ) ?;
80+
81+ if spec. contains ( ',' ) {
82+ return Err ( RangeError :: MultiRangeNotSupported ) ;
83+ }
84+
85+ let ( start_str, end_str) = spec. split_once ( '-' ) . ok_or ( RangeError :: InvalidRange ) ?;
86+
87+ if start_str. is_empty ( ) {
88+ // bytes=-N (suffix / last N bytes)
89+ let n: u64 = end_str. parse ( ) . map_err ( |_| RangeError :: InvalidRange ) ?;
90+ if n == 0 {
91+ return Err ( RangeError :: InvalidRange ) ;
92+ }
93+ Ok ( ByteRange :: Last ( n) )
94+ } else if end_str. is_empty ( ) {
95+ // bytes=N- (from offset to end)
96+ let start: u64 = start_str. parse ( ) . map_err ( |_| RangeError :: InvalidRange ) ?;
97+ Ok ( ByteRange :: From ( start) )
98+ } else {
99+ // bytes=N-M (inclusive range)
100+ let start: u64 = start_str. parse ( ) . map_err ( |_| RangeError :: InvalidRange ) ?;
101+ let end: u64 = end_str. parse ( ) . map_err ( |_| RangeError :: InvalidRange ) ?;
102+ if start > end {
103+ return Err ( RangeError :: InvalidRange ) ;
104+ }
105+ Ok ( ByteRange :: Inclusive ( start, end) )
106+ }
107+ }
108+ }
109+
102110/// Describes which bytes of the full object are present in the response body.
103111#[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
104112pub struct ContentRange {
@@ -156,8 +164,14 @@ impl ContentRange {
156164 }
157165
158166 /// Formats the value for a `Content-Range` response header.
159- pub fn to_header_value ( & self ) -> String {
160- format ! ( "bytes {}-{}/{}" , self . start, self . end, self . total)
167+ ///
168+ /// The returned value is always valid ASCII and can be inserted directly
169+ /// into an HTTP header map.
170+ pub fn to_header_value ( & self ) -> HeaderValue {
171+ let s = format ! ( "bytes {}-{}/{}" , self . start, self . end, self . total) ;
172+ // SAFETY: the format only contains ASCII digits, spaces, hyphens,
173+ // and slashes, which are all valid header value bytes.
174+ HeaderValue :: from_str ( & s) . expect ( "ContentRange always produces a valid header value" )
161175 }
162176}
163177
@@ -200,59 +214,59 @@ mod tests {
200214 #[ test]
201215 fn parse_from_to ( ) {
202216 assert_eq ! (
203- ByteRange :: parse ( "bytes=0-499" ) ,
204- Ok ( ByteRange :: FromTo ( 0 , 499 ) )
217+ ByteRange :: try_from ( "bytes=0-499" ) ,
218+ Ok ( ByteRange :: Inclusive ( 0 , 499 ) )
205219 ) ;
206220 }
207221
208222 #[ test]
209223 fn parse_from ( ) {
210- assert_eq ! ( ByteRange :: parse ( "bytes=500-" ) , Ok ( ByteRange :: From ( 500 ) ) ) ;
224+ assert_eq ! ( ByteRange :: try_from ( "bytes=500-" ) , Ok ( ByteRange :: From ( 500 ) ) ) ;
211225 }
212226
213227 #[ test]
214228 fn parse_suffix ( ) {
215- assert_eq ! ( ByteRange :: parse ( "bytes=-100" ) , Ok ( ByteRange :: Suffix ( 100 ) ) ) ;
229+ assert_eq ! ( ByteRange :: try_from ( "bytes=-100" ) , Ok ( ByteRange :: Last ( 100 ) ) ) ;
216230 }
217231
218232 #[ test]
219233 fn parse_rejects_multi_range ( ) {
220234 assert_eq ! (
221- ByteRange :: parse ( "bytes=0-10, 20-30" ) ,
235+ ByteRange :: try_from ( "bytes=0-10, 20-30" ) ,
222236 Err ( RangeError :: MultiRangeNotSupported )
223237 ) ;
224238 }
225239
226240 #[ test]
227241 fn parse_case_insensitive ( ) {
228242 assert_eq ! (
229- ByteRange :: parse ( "Bytes=0-499" ) ,
230- Ok ( ByteRange :: FromTo ( 0 , 499 ) )
243+ ByteRange :: try_from ( "Bytes=0-499" ) ,
244+ Ok ( ByteRange :: Inclusive ( 0 , 499 ) )
231245 ) ;
232- assert_eq ! ( ByteRange :: parse ( "BYTES=100-" ) , Ok ( ByteRange :: From ( 100 ) ) ) ;
246+ assert_eq ! ( ByteRange :: try_from ( "BYTES=100-" ) , Ok ( ByteRange :: From ( 100 ) ) ) ;
233247 }
234248
235249 #[ test]
236250 fn parse_returns_unknown_unit_for_non_bytes ( ) {
237- assert_eq ! ( ByteRange :: parse ( "items=0-10" ) , Err ( RangeError :: UnknownUnit ) ) ;
251+ assert_eq ! ( ByteRange :: try_from ( "items=0-10" ) , Err ( RangeError :: UnknownUnit ) ) ;
238252 }
239253
240254 #[ test]
241255 fn parse_rejects_inverted_range ( ) {
242256 assert_eq ! (
243- ByteRange :: parse ( "bytes=500-100" ) ,
257+ ByteRange :: try_from ( "bytes=500-100" ) ,
244258 Err ( RangeError :: InvalidRange )
245259 ) ;
246260 }
247261
248262 #[ test]
249263 fn parse_rejects_zero_suffix ( ) {
250- assert_eq ! ( ByteRange :: parse ( "bytes=-0" ) , Err ( RangeError :: InvalidRange ) ) ;
264+ assert_eq ! ( ByteRange :: try_from ( "bytes=-0" ) , Err ( RangeError :: InvalidRange ) ) ;
251265 }
252266
253267 #[ test]
254268 fn resolve_from_to ( ) {
255- let range = ByteRange :: FromTo ( 0 , 499 ) . resolve ( 1000 ) ;
269+ let range = ByteRange :: Inclusive ( 0 , 499 ) . resolve ( 1000 ) ;
256270 assert_eq ! (
257271 range,
258272 Some ( ContentRange {
@@ -265,7 +279,7 @@ mod tests {
265279
266280 #[ test]
267281 fn resolve_from_to_clamped ( ) {
268- let range = ByteRange :: FromTo ( 0 , 9999 ) . resolve ( 500 ) ;
282+ let range = ByteRange :: Inclusive ( 0 , 9999 ) . resolve ( 500 ) ;
269283 assert_eq ! (
270284 range,
271285 Some ( ContentRange {
@@ -291,7 +305,7 @@ mod tests {
291305
292306 #[ test]
293307 fn resolve_suffix ( ) {
294- let range = ByteRange :: Suffix ( 100 ) . resolve ( 1000 ) ;
308+ let range = ByteRange :: Last ( 100 ) . resolve ( 1000 ) ;
295309 assert_eq ! (
296310 range,
297311 Some ( ContentRange {
@@ -304,7 +318,7 @@ mod tests {
304318
305319 #[ test]
306320 fn resolve_suffix_larger_than_total ( ) {
307- let range = ByteRange :: Suffix ( 2000 ) . resolve ( 1000 ) ;
321+ let range = ByteRange :: Last ( 2000 ) . resolve ( 1000 ) ;
308322 assert_eq ! (
309323 range,
310324 Some ( ContentRange {
@@ -317,9 +331,9 @@ mod tests {
317331
318332 #[ test]
319333 fn resolve_unsatisfiable ( ) {
320- assert_eq ! ( ByteRange :: FromTo ( 1000 , 2000 ) . resolve( 500 ) , None ) ;
334+ assert_eq ! ( ByteRange :: Inclusive ( 1000 , 2000 ) . resolve( 500 ) , None ) ;
321335 assert_eq ! ( ByteRange :: From ( 500 ) . resolve( 500 ) , None ) ;
322- assert_eq ! ( ByteRange :: FromTo ( 0 , 0 ) . resolve( 0 ) , None ) ;
336+ assert_eq ! ( ByteRange :: Inclusive ( 0 , 0 ) . resolve( 0 ) , None ) ;
323337 }
324338
325339 #[ test]
@@ -353,9 +367,12 @@ mod tests {
353367
354368 #[ test]
355369 fn byte_range_to_header_value ( ) {
356- assert_eq ! ( ByteRange :: FromTo ( 0 , 499 ) . to_header_value( ) , "bytes=0-499" ) ;
370+ assert_eq ! (
371+ ByteRange :: Inclusive ( 0 , 499 ) . to_header_value( ) ,
372+ "bytes=0-499"
373+ ) ;
357374 assert_eq ! ( ByteRange :: From ( 500 ) . to_header_value( ) , "bytes=500-" ) ;
358- assert_eq ! ( ByteRange :: Suffix ( 100 ) . to_header_value( ) , "bytes=-100" ) ;
375+ assert_eq ! ( ByteRange :: Last ( 100 ) . to_header_value( ) , "bytes=-100" ) ;
359376 }
360377
361378 #[ test]
0 commit comments