/// HTTP Range header representation. #[derive(Debug, Clone, Copy)] pub struct HttpRange { pub start: u64, pub length: u64, } static PREFIX: &str = "bytes="; const PREFIX_LEN: usize = 6; impl HttpRange { /// Parses Range HTTP header string as per RFC 2616. /// /// `header` is HTTP Range header (e.g. `bytes=bytes=0-9`). /// `size` is full size of response (file). pub fn parse(header: &str, size: u64) -> Result, ()> { if header.is_empty() { return Ok(Vec::new()); } if !header.starts_with(PREFIX) { return Err(()); } let size_sig = size as i64; let mut no_overlap = false; let all_ranges: Vec> = header[PREFIX_LEN..] .split(',') .map(|x| x.trim()) .filter(|x| !x.is_empty()) .map(|ra| { let mut start_end_iter = ra.split('-'); let start_str = start_end_iter.next().ok_or(())?.trim(); let end_str = start_end_iter.next().ok_or(())?.trim(); if start_str.is_empty() { // If no start is specified, end specifies the // range start relative to the end of the file. let mut length: i64 = end_str.parse().map_err(|_| ())?; if length > size_sig { length = size_sig; } Ok(Some(HttpRange { start: (size_sig - length) as u64, length: length as u64, })) } else { let start: i64 = start_str.parse().map_err(|_| ())?; if start < 0 { return Err(()); } if start >= size_sig { no_overlap = true; return Ok(None); } let length = if end_str.is_empty() { // If no end is specified, range extends to end of the file. size_sig - start } else { let mut end: i64 = end_str.parse().map_err(|_| ())?; if start > end { return Err(()); } if end >= size_sig { end = size_sig - 1; } end - start + 1 }; Ok(Some(HttpRange { start: start as u64, length: length as u64, })) } }) .collect::>()?; let ranges: Vec = all_ranges.into_iter().filter_map(|x| x).collect(); if no_overlap && ranges.is_empty() { return Err(()); } Ok(ranges) } } #[cfg(test)] mod tests { use super::*; struct T(&'static str, u64, Vec); #[test] fn test_parse() { let tests = vec![ T("", 0, vec![]), T("", 1000, vec![]), T("foo", 0, vec![]), T("bytes=", 0, vec![]), T("bytes=7", 10, vec![]), T("bytes= 7 ", 10, vec![]), T("bytes=1-", 0, vec![]), T("bytes=5-4", 10, vec![]), T("bytes=0-2,5-4", 10, vec![]), T("bytes=2-5,4-3", 10, vec![]), T("bytes=--5,4--3", 10, vec![]), T("bytes=A-", 10, vec![]), T("bytes=A- ", 10, vec![]), T("bytes=A-Z", 10, vec![]), T("bytes= -Z", 10, vec![]), T("bytes=5-Z", 10, vec![]), T("bytes=Ran-dom, garbage", 10, vec![]), T("bytes=0x01-0x02", 10, vec![]), T("bytes= ", 10, vec![]), T("bytes= , , , ", 10, vec![]), T( "bytes=0-9", 10, vec![HttpRange { start: 0, length: 10, }], ), T( "bytes=0-", 10, vec![HttpRange { start: 0, length: 10, }], ), T( "bytes=5-", 10, vec![HttpRange { start: 5, length: 5, }], ), T( "bytes=0-20", 10, vec![HttpRange { start: 0, length: 10, }], ), T( "bytes=15-,0-5", 10, vec![HttpRange { start: 0, length: 6, }], ), T( "bytes=1-2,5-", 10, vec![ HttpRange { start: 1, length: 2, }, HttpRange { start: 5, length: 5, }, ], ), T( "bytes=-2 , 7-", 11, vec![ HttpRange { start: 9, length: 2, }, HttpRange { start: 7, length: 4, }, ], ), T( "bytes=0-0 ,2-2, 7-", 11, vec![ HttpRange { start: 0, length: 1, }, HttpRange { start: 2, length: 1, }, HttpRange { start: 7, length: 4, }, ], ), T( "bytes=-5", 10, vec![HttpRange { start: 5, length: 5, }], ), T( "bytes=-15", 10, vec![HttpRange { start: 0, length: 10, }], ), T( "bytes=0-499", 10000, vec![HttpRange { start: 0, length: 500, }], ), T( "bytes=500-999", 10000, vec![HttpRange { start: 500, length: 500, }], ), T( "bytes=-500", 10000, vec![HttpRange { start: 9500, length: 500, }], ), T( "bytes=9500-", 10000, vec![HttpRange { start: 9500, length: 500, }], ), T( "bytes=0-0,-1", 10000, vec![ HttpRange { start: 0, length: 1, }, HttpRange { start: 9999, length: 1, }, ], ), T( "bytes=500-600,601-999", 10000, vec![ HttpRange { start: 500, length: 101, }, HttpRange { start: 601, length: 399, }, ], ), T( "bytes=500-700,601-999", 10000, vec![ HttpRange { start: 500, length: 201, }, HttpRange { start: 601, length: 399, }, ], ), // Match Apache laxity: T( "bytes= 1 -2 , 4- 5, 7 - 8 , ,,", 11, vec![ HttpRange { start: 1, length: 2, }, HttpRange { start: 4, length: 2, }, HttpRange { start: 7, length: 2, }, ], ), ]; for t in tests { let header = t.0; let size = t.1; let expected = t.2; let res = HttpRange::parse(header, size); if res.is_err() { if expected.is_empty() { continue; } else { assert!( false, "parse({}, {}) returned error {:?}", header, size, res.unwrap_err() ); } } let got = res.unwrap(); if got.len() != expected.len() { assert!( false, "len(parseRange({}, {})) = {}, want {}", header, size, got.len(), expected.len() ); continue; } for i in 0..expected.len() { if got[i].start != expected[i].start { assert!( false, "parseRange({}, {})[{}].start = {}, want {}", header, size, i, got[i].start, expected[i].start ) } if got[i].length != expected[i].length { assert!( false, "parseRange({}, {})[{}].length = {}, want {}", header, size, i, got[i].length, expected[i].length ) } } } } }