rama_http_headers/common/
range.rs1use std::fmt;
2use std::ops::{Bound, RangeBounds};
3
4use rama_core::telemetry::tracing;
5use rama_http_types::{HeaderName, HeaderValue};
6
7use crate::{Error, HeaderDecode, HeaderEncode, TypedHeader, util};
8
9#[derive(Clone, Debug, PartialEq, Eq, Hash)]
53pub struct Range(HeaderValue);
54
55rama_utils::macros::error::static_str_error! {
56 #[doc = "range is not valid"]
57 pub struct InvalidRange;
58}
59
60impl Range {
61 pub fn bytes(bounds: impl RangeBounds<u64>) -> Result<Self, InvalidRange> {
66 let v = match (bounds.start_bound(), bounds.end_bound()) {
67 (Bound::Included(start), Bound::Included(end)) => format!("bytes={start}-{end}"),
68 (Bound::Included(start), Bound::Excluded(&end)) => {
69 let Some(last) = end.checked_sub(1) else {
71 return Err(InvalidRange);
72 };
73 format!("bytes={start}-{last}")
74 }
75 (Bound::Included(start), Bound::Unbounded) => format!("bytes={start}-"),
76 _ => return Err(InvalidRange),
78 };
79
80 match HeaderValue::try_from(v) {
81 Ok(v) => Ok(Self(v)),
82 Err(err) => {
83 tracing::debug!("failed to create Range header from bytes: {err}");
84 Err(InvalidRange)
85 }
86 }
87 }
88
89 #[must_use]
92 pub fn suffix(n: u64) -> Self {
93 Self(util::fmt(format_args!("bytes=-{n}")))
94 }
95
96 pub fn iter(&self) -> impl Iterator<Item = ByteRangeSpec> + '_ {
101 self.range_set().split(',').filter_map(parse_spec)
102 }
103
104 pub fn satisfiable_ranges(&self, content_length: u64) -> impl Iterator<Item = (u64, u64)> + '_ {
110 self.iter()
111 .filter_map(move |spec| spec.to_satisfiable_range(content_length))
112 }
113
114 #[must_use]
120 pub fn first_satisfiable_range(&self, content_length: u64) -> Option<(u64, u64)> {
121 self.iter()
122 .find_map(|spec| spec.to_satisfiable_range(content_length))
123 }
124
125 fn range_set(&self) -> &str {
127 #[expect(
128 clippy::expect_used,
129 reason = "value is validated as a UTF-8 `bytes=…` string in HeaderDecode::decode"
130 )]
131 let s = self
132 .0
133 .to_str()
134 .expect("valid string checked in HeaderDecode::decode()");
135 s.strip_prefix("bytes=").unwrap_or("")
136 }
137}
138
139#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
142pub enum ByteRangeSpec {
143 FromTo(u64, u64),
145 AllFrom(u64),
147 Last(u64),
149}
150
151impl ByteRangeSpec {
152 #[must_use]
163 pub fn to_satisfiable_range(&self, content_length: u64) -> Option<(u64, u64)> {
164 if content_length == 0 {
165 return None;
167 }
168 let last = content_length - 1;
169 match *self {
170 Self::FromTo(first, to) if first <= to && first < content_length => {
171 Some((first, to.min(last)))
172 }
173 Self::AllFrom(first) if first < content_length => Some((first, last)),
174 Self::Last(suffix) if suffix > 0 => Some((content_length.saturating_sub(suffix), last)),
175 _ => None,
176 }
177 }
178}
179
180impl fmt::Display for ByteRangeSpec {
181 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182 match *self {
183 Self::FromTo(first, last) => write!(f, "{first}-{last}"),
184 Self::AllFrom(first) => write!(f, "{first}-"),
185 Self::Last(suffix) => write!(f, "-{suffix}"),
186 }
187 }
188}
189
190fn parse_spec(spec: &str) -> Option<ByteRangeSpec> {
191 let (first, last) = spec.trim().split_once('-')?;
192 match (first, last) {
193 ("", "") => None,
195 ("", suffix) => Some(ByteRangeSpec::Last(suffix.parse().ok()?)),
196 (first, "") => Some(ByteRangeSpec::AllFrom(first.parse().ok()?)),
197 (first, last) => {
198 let first = first.parse().ok()?;
199 let last = last.parse().ok()?;
200 (first <= last).then_some(ByteRangeSpec::FromTo(first, last))
201 }
202 }
203}
204
205impl TypedHeader for Range {
206 fn name() -> &'static HeaderName {
207 &::rama_http_types::header::RANGE
208 }
209}
210
211impl HeaderDecode for Range {
212 fn decode<'i, I: Iterator<Item = &'i HeaderValue>>(values: &mut I) -> Result<Self, Error> {
213 values
214 .next()
215 .and_then(|val| {
216 if val.to_str().ok()?.starts_with("bytes=") {
217 Some(Self(val.clone()))
218 } else {
219 None
220 }
221 })
222 .ok_or_else(Error::invalid)
223 }
224}
225
226impl HeaderEncode for Range {
227 fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
228 values.extend(::std::iter::once(self.0.clone()));
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::super::{test_decode, test_encode};
235 use super::*;
236
237 fn range(s: &str) -> Range {
238 test_decode(&[s]).unwrap()
239 }
240
241 fn specs(s: &str) -> Vec<ByteRangeSpec> {
242 range(s).iter().collect()
243 }
244
245 #[test]
246 fn decode_rejects_non_bytes_unit() {
247 assert!(test_decode::<Range>(&["seconds=1-2"]).is_none());
248 assert!(test_decode::<Range>(&["1-2"]).is_none());
249 assert!(test_decode::<Range>(&["bytes"]).is_none());
250 }
251
252 #[test]
253 fn iter_parses_specs() {
254 assert_eq!(specs("bytes=1-100"), [ByteRangeSpec::FromTo(1, 100)]);
255 assert_eq!(
256 specs("bytes=1-100,200-"),
257 [ByteRangeSpec::FromTo(1, 100), ByteRangeSpec::AllFrom(200)],
258 );
259 assert_eq!(
260 specs("bytes=0-10,20-90,-100"),
261 [
262 ByteRangeSpec::FromTo(0, 10),
263 ByteRangeSpec::FromTo(20, 90),
264 ByteRangeSpec::Last(100),
265 ],
266 );
267 }
268
269 #[test]
270 fn iter_trims_and_skips_invalid() {
271 assert_eq!(
273 specs("bytes= 1-100 , 101-xxx , 200- , ,, -100 , 5-2"),
274 [
275 ByteRangeSpec::FromTo(1, 100),
276 ByteRangeSpec::AllFrom(200),
277 ByteRangeSpec::Last(100),
278 ],
279 );
280 assert!(specs("bytes=1-2-3").is_empty());
281 assert!(specs("bytes=").is_empty());
282 assert!(specs("bytes=-").is_empty());
283 }
284
285 #[test]
286 fn spec_from_to_resolution() {
287 use ByteRangeSpec::FromTo;
288 assert_eq!(FromTo(0, 0).to_satisfiable_range(3), Some((0, 0)));
289 assert_eq!(FromTo(1, 2).to_satisfiable_range(3), Some((1, 2)));
290 assert_eq!(FromTo(1, 5).to_satisfiable_range(3), Some((1, 2))); assert_eq!(FromTo(3, 3).to_satisfiable_range(3), None); assert_eq!(FromTo(2, 1).to_satisfiable_range(3), None); assert_eq!(FromTo(0, 0).to_satisfiable_range(0), None); }
295
296 #[test]
297 fn spec_all_from_resolution() {
298 use ByteRangeSpec::AllFrom;
299 assert_eq!(AllFrom(0).to_satisfiable_range(3), Some((0, 2)));
300 assert_eq!(AllFrom(2).to_satisfiable_range(3), Some((2, 2)));
301 assert_eq!(AllFrom(3).to_satisfiable_range(3), None);
302 assert_eq!(AllFrom(5).to_satisfiable_range(3), None);
303 assert_eq!(AllFrom(0).to_satisfiable_range(0), None);
304 }
305
306 #[test]
307 fn spec_last_resolution() {
308 use ByteRangeSpec::Last;
309 assert_eq!(Last(2).to_satisfiable_range(3), Some((1, 2)));
310 assert_eq!(Last(1).to_satisfiable_range(3), Some((2, 2)));
311 assert_eq!(Last(3).to_satisfiable_range(3), Some((0, 2)));
313 assert_eq!(Last(5).to_satisfiable_range(3), Some((0, 2)));
314 assert_eq!(Last(0).to_satisfiable_range(3), None);
315 assert_eq!(Last(2).to_satisfiable_range(0), None);
316 }
317
318 #[test]
319 fn first_satisfiable_range_suffix() {
320 assert_eq!(
321 range("bytes=-100").first_satisfiable_range(350),
322 Some((250, 349)),
323 );
324 assert_eq!(
326 range("bytes=-350").first_satisfiable_range(100),
327 Some((0, 99)),
328 );
329 }
330
331 #[test]
332 fn first_satisfiable_range_skips_unsatisfiable() {
333 assert_eq!(
335 range("bytes=500-600,0-1").first_satisfiable_range(100),
336 Some((0, 1)),
337 );
338 assert_eq!(range("bytes=500-,600-").first_satisfiable_range(100), None);
340 }
341
342 #[test]
343 fn satisfiable_ranges_resolves_and_clamps() {
344 let resolved: Vec<_> = range("bytes=0-1,30-40,500-600,-5")
345 .satisfiable_ranges(100)
346 .collect();
347 assert_eq!(resolved, [(0, 1), (30, 40), (95, 99)]);
349 }
350
351 #[test]
352 fn bytes_constructor_encodes() {
353 assert_eq!(
354 test_encode(Range::bytes(0..1234).unwrap())["range"],
355 "bytes=0-1233"
356 );
357 assert_eq!(
358 test_encode(Range::bytes(0..=99).unwrap())["range"],
359 "bytes=0-99"
360 );
361 assert_eq!(
362 test_encode(Range::bytes(100..).unwrap())["range"],
363 "bytes=100-"
364 );
365 }
366
367 #[test]
368 fn bytes_constructor_rejects_unrepresentable() {
369 Range::bytes(..).unwrap_err(); Range::bytes(..100).unwrap_err(); Range::bytes(0..0).unwrap_err(); }
373
374 #[test]
375 fn suffix_constructor() {
376 assert_eq!(test_encode(Range::suffix(500))["range"], "bytes=-500");
377 assert_eq!(
378 Range::suffix(500).first_satisfiable_range(2000),
379 Some((1500, 1999))
380 );
381 }
382
383 #[test]
384 fn spec_display_roundtrips_through_iter() {
385 let rendered: Vec<String> = range("bytes=0-10,20-,-100")
386 .iter()
387 .map(|s| s.to_string())
388 .collect();
389 assert_eq!(rendered, ["0-10", "20-", "-100"]);
390 }
391}