1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub struct HttpByteRange {
6 start: u64,
7 end: u64,
8 length: u64,
9 total_size: u64,
10}
11
12impl HttpByteRange {
13 pub fn new(start: u64, end: u64, total_size: u64) -> Result<Self, HttpRangeError> {
15 if total_size == 0 {
16 return Err(HttpRangeError::EmptyRepresentation);
17 }
18 if start > end || end >= total_size {
19 return Err(HttpRangeError::Unsatisfiable);
20 }
21 Ok(Self {
22 start,
23 end,
24 length: end - start + 1,
25 total_size,
26 })
27 }
28
29 #[must_use]
30 pub const fn start(self) -> u64 {
31 self.start
32 }
33
34 #[must_use]
35 pub const fn end(self) -> u64 {
36 self.end
37 }
38
39 #[must_use]
40 pub const fn length(self) -> u64 {
41 self.length
42 }
43
44 #[must_use]
45 pub const fn total_size(self) -> u64 {
46 self.total_size
47 }
48
49 #[must_use]
51 pub fn content_range_header(self) -> String {
52 format!("bytes {}-{}/{}", self.start, self.end, self.total_size)
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
58pub enum HttpRangeError {
59 #[error("range header must use the bytes unit")]
60 UnsupportedUnit,
61 #[error("multiple range requests are not supported")]
62 MultipleRangesUnsupported,
63 #[error("range header is malformed")]
64 Malformed,
65 #[error("range bound must be a valid unsigned integer")]
66 InvalidNumber,
67 #[error("range cannot be requested for an empty representation")]
68 EmptyRepresentation,
69 #[error("range is not satisfiable for the current representation")]
70 Unsatisfiable,
71}
72
73pub fn parse_single_byte_range(
79 raw: &str,
80 total_size: u64,
81) -> Result<HttpByteRange, HttpRangeError> {
82 let raw = raw.trim_start();
83 let (unit, range) = raw.split_once('=').ok_or(HttpRangeError::UnsupportedUnit)?;
84 if !unit.eq_ignore_ascii_case("bytes") {
85 return Err(HttpRangeError::UnsupportedUnit);
86 }
87 if range.contains(',') {
88 return Err(HttpRangeError::MultipleRangesUnsupported);
89 }
90
91 let (start_raw, end_raw) = range.split_once('-').ok_or(HttpRangeError::Malformed)?;
92 if start_raw.is_empty() && end_raw.is_empty() {
93 return Err(HttpRangeError::Malformed);
94 }
95 if total_size == 0 {
96 return Err(HttpRangeError::EmptyRepresentation);
97 }
98
99 if start_raw.is_empty() {
100 let suffix_length = parse_bound(end_raw)?;
101 if suffix_length == 0 {
102 return Err(HttpRangeError::Unsatisfiable);
103 }
104 let length = suffix_length.min(total_size);
105 return HttpByteRange::new(total_size - length, total_size - 1, total_size);
106 }
107
108 let start = parse_bound(start_raw)?;
109 if start >= total_size {
110 return Err(HttpRangeError::Unsatisfiable);
111 }
112 let end = if end_raw.is_empty() {
113 total_size - 1
114 } else {
115 parse_bound(end_raw)?
116 };
117 if end < start {
118 return Err(HttpRangeError::Unsatisfiable);
119 }
120 HttpByteRange::new(start, end.min(total_size - 1), total_size)
121}
122
123fn parse_bound(value: &str) -> Result<u64, HttpRangeError> {
124 value
125 .parse::<u64>()
126 .map_err(|_| HttpRangeError::InvalidNumber)
127}
128
129#[cfg(test)]
130mod tests {
131 use super::{HttpByteRange, HttpRangeError, parse_single_byte_range};
132
133 #[test]
134 fn resolves_bounded_open_and_suffix_ranges() {
135 assert_eq!(
136 parse_single_byte_range("bytes=5-9", 20),
137 HttpByteRange::new(5, 9, 20)
138 );
139 assert_eq!(
140 parse_single_byte_range("bytes=7-", 20),
141 HttpByteRange::new(7, 19, 20)
142 );
143 assert_eq!(
144 parse_single_byte_range("bytes=-6", 20),
145 HttpByteRange::new(14, 19, 20)
146 );
147 assert_eq!(
148 parse_single_byte_range("bytes=-50", 20),
149 HttpByteRange::new(0, 19, 20)
150 );
151 assert_eq!(
152 parse_single_byte_range(" BYTES=0-1", 20),
153 HttpByteRange::new(0, 1, 20)
154 );
155 }
156
157 #[test]
158 fn clamps_end_beyond_the_representation() {
159 assert_eq!(
160 parse_single_byte_range("bytes=17-99", 20),
161 HttpByteRange::new(17, 19, 20)
162 );
163 }
164
165 #[test]
166 fn preserves_u64_boundaries_without_overflow() {
167 let total_size = u64::MAX;
168 let range = parse_single_byte_range("bytes=0-18446744073709551615", total_size)
169 .expect("maximum end should clamp safely");
170 assert_eq!(range.start(), 0);
171 assert_eq!(range.end(), u64::MAX - 1);
172 assert_eq!(range.length(), u64::MAX);
173 assert_eq!(range.total_size(), total_size);
174 }
175
176 #[test]
177 fn renders_content_range_and_exposes_bounds() {
178 let range = HttpByteRange::new(2, 6, 10).expect("valid range");
179 assert_eq!(range.start(), 2);
180 assert_eq!(range.end(), 6);
181 assert_eq!(range.length(), 5);
182 assert_eq!(range.total_size(), 10);
183 assert_eq!(range.content_range_header(), "bytes 2-6/10");
184 }
185
186 #[test]
187 fn constructor_rejects_empty_inverted_and_out_of_bounds_ranges() {
188 assert_eq!(
189 HttpByteRange::new(0, 0, 0),
190 Err(HttpRangeError::EmptyRepresentation)
191 );
192 assert_eq!(
193 HttpByteRange::new(5, 4, 10),
194 Err(HttpRangeError::Unsatisfiable)
195 );
196 assert_eq!(
197 HttpByteRange::new(5, 10, 10),
198 Err(HttpRangeError::Unsatisfiable)
199 );
200 }
201
202 #[test]
203 fn classifies_every_rejected_range_shape() {
204 let cases = [
205 ("items=0-1", HttpRangeError::UnsupportedUnit),
206 ("bytes=0-1,3-4", HttpRangeError::MultipleRangesUnsupported),
207 ("bytes=-", HttpRangeError::Malformed),
208 ("bytes=abc-", HttpRangeError::InvalidNumber),
209 ("bytes=-0", HttpRangeError::Unsatisfiable),
210 ("bytes=9-5", HttpRangeError::Unsatisfiable),
211 ("bytes=20-", HttpRangeError::Unsatisfiable),
212 ];
213 for (raw, expected) in cases {
214 assert_eq!(parse_single_byte_range(raw, 20), Err(expected), "{raw}");
215 }
216 assert_eq!(
217 parse_single_byte_range("bytes=0-0", 0),
218 Err(HttpRangeError::EmptyRepresentation)
219 );
220 }
221}