Skip to main content

goose_http/range/
mod.rs

1//! Byte range parsing utilities.
2//!
3//! Implements structures for representing satisfiable and unsatisfiable byte
4//! ranges, following RFC 9110 Section 14.
5
6use std::cmp::{max, min};
7
8use thiserror::Error;
9
10/// Represents a single byte range specification from the Range header.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum RangeSpec {
13    /// `start-end`
14    FromTo { start: u64, end: u64 },
15    /// `start-`
16    From { start: u64 },
17    /// `-suffix-length`
18    Suffix { length: u64 },
19}
20
21/// Represents a satisfiable inclusive byte range once applied to an entity.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct SatisfiableRange {
24    pub start: u64,
25    pub end: u64,
26}
27
28impl SatisfiableRange {
29    pub fn len(self) -> u64 {
30        self.end - self.start + 1
31    }
32}
33
34/// Errors that can occur when parsing or applying range headers.
35#[derive(Debug, Error, PartialEq, Eq)]
36pub enum RangeParseError {
37    #[error("range unit must be bytes")]
38    InvalidUnit,
39    #[error("invalid range syntax")]
40    InvalidSyntax,
41    #[error("range start greater than end")]
42    InvalidBounds,
43    #[error("range component is not numeric")]
44    InvalidNumber,
45    #[error("empty range set")]
46    Empty,
47}
48
49/// Parse a Range header value into byte range specifications.
50pub fn parse_range_header(value: &str) -> Result<Vec<RangeSpec>, RangeParseError> {
51    let (unit, ranges) = value
52        .split_once('=')
53        .ok_or(RangeParseError::InvalidSyntax)?;
54    if !unit.trim().eq_ignore_ascii_case("bytes") {
55        return Err(RangeParseError::InvalidUnit);
56    }
57
58    let mut specs = Vec::new();
59    for part in ranges.split(',') {
60        let part = part.trim();
61        if part.is_empty() {
62            continue;
63        }
64        if let Some(stripped) = part.strip_prefix('-') {
65            let length = stripped
66                .trim()
67                .parse::<u64>()
68                .map_err(|_| RangeParseError::InvalidNumber)?;
69            specs.push(RangeSpec::Suffix { length });
70            continue;
71        }
72
73        let mut bounds = part.splitn(2, '-');
74        let start = bounds
75            .next()
76            .ok_or(RangeParseError::InvalidSyntax)?
77            .trim()
78            .parse::<u64>()
79            .map_err(|_| RangeParseError::InvalidNumber)?;
80
81        match bounds.next() {
82            Some("") | None => {
83                specs.push(RangeSpec::From { start });
84            }
85            Some(end_str) => {
86                let end = end_str
87                    .trim()
88                    .parse::<u64>()
89                    .map_err(|_| RangeParseError::InvalidNumber)?;
90                if start > end {
91                    return Err(RangeParseError::InvalidBounds);
92                }
93                specs.push(RangeSpec::FromTo { start, end });
94            }
95        }
96    }
97
98    if specs.is_empty() {
99        return Err(RangeParseError::Empty);
100    }
101
102    Ok(specs)
103}
104
105/// Compute satisfiable ranges given entity length. Returns an empty vector when
106/// no ranges overlap with the entity.
107pub fn compute_satisfiable_ranges(specs: &[RangeSpec], total_length: u64) -> Vec<SatisfiableRange> {
108    if total_length == 0 {
109        return Vec::new();
110    }
111
112    specs
113        .iter()
114        .filter_map(|spec| match spec {
115            RangeSpec::FromTo { start, end } => {
116                if *start >= total_length {
117                    None
118                } else {
119                    let bounded_end = min(*end, total_length - 1);
120                    Some(SatisfiableRange {
121                        start: *start,
122                        end: max(*start, bounded_end),
123                    })
124                }
125            }
126            RangeSpec::From { start } => {
127                if *start >= total_length {
128                    None
129                } else {
130                    Some(SatisfiableRange {
131                        start: *start,
132                        end: total_length - 1,
133                    })
134                }
135            }
136            RangeSpec::Suffix { length } => {
137                let length = min(*length, total_length);
138                if length == 0 {
139                    None
140                } else {
141                    Some(SatisfiableRange {
142                        start: total_length - length,
143                        end: total_length - 1,
144                    })
145                }
146            }
147        })
148        .collect()
149}
150
151/// Format a Content-Range header for a satisfiable range.
152pub fn format_content_range(range: SatisfiableRange, total_length: u64) -> String {
153    format!("bytes {}-{}/{}", range.start, range.end, total_length)
154}
155
156/// Format a Content-Range header for unsatisfied ranges.
157pub fn format_unsatisfied_range(total_length: u64) -> String {
158    format!("bytes */{}", total_length)
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[test]
166    fn parse_single_range() {
167        let parsed = parse_range_header("bytes=0-99").unwrap();
168        assert_eq!(parsed, vec![RangeSpec::FromTo { start: 0, end: 99 }]);
169    }
170
171    #[test]
172    fn parse_suffix_range() {
173        let parsed = parse_range_header("bytes=-500").unwrap();
174        assert_eq!(parsed, vec![RangeSpec::Suffix { length: 500 }]);
175    }
176
177    #[test]
178    fn compute_satisfiable_from_range() {
179        let specs = parse_range_header("bytes=100-").unwrap();
180        let ranges = compute_satisfiable_ranges(&specs, 1000);
181        assert_eq!(
182            ranges,
183            vec![SatisfiableRange {
184                start: 100,
185                end: 999
186            }]
187        );
188    }
189
190    #[test]
191    fn unsatisfiable_returns_empty() {
192        let specs = parse_range_header("bytes=2000-3000").unwrap();
193        let ranges = compute_satisfiable_ranges(&specs, 1024);
194        assert!(ranges.is_empty());
195    }
196}