tower_http_cache/
range.rs

1//! HTTP Range request parsing and handling.
2//!
3//! This module provides utilities for working with HTTP Range requests (RFC 7233),
4//! which allow clients to request specific byte ranges of a resource. This is
5//! commonly used for video streaming, large file downloads, and resume capabilities.
6//!
7//! # Example
8//!
9//! ```
10//! use tower_http_cache::range::parse_range_header;
11//! use http::HeaderMap;
12//!
13//! let mut headers = HeaderMap::new();
14//! headers.insert("range", "bytes=0-1023".parse().unwrap());
15//!
16//! if let Some(range) = parse_range_header(&headers) {
17//!     println!("Requested bytes {}-{:?}", range.start, range.end);
18//! }
19//! ```
20
21use http::{HeaderMap, HeaderValue, StatusCode};
22
23/// A parsed byte range request.
24///
25/// Represents a request for a specific byte range of a resource,
26/// typically from the HTTP Range header.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub struct RangeRequest {
29    /// Starting byte offset (inclusive)
30    pub start: u64,
31
32    /// Ending byte offset (inclusive), or None for "to end of file"
33    pub end: Option<u64>,
34}
35
36impl RangeRequest {
37    /// Creates a new range request.
38    pub fn new(start: u64, end: Option<u64>) -> Self {
39        Self { start, end }
40    }
41
42    /// Returns the length of this range in bytes, if both start and end are known.
43    pub fn len(&self) -> Option<u64> {
44        self.end
45            .map(|end| end.saturating_sub(self.start).saturating_add(1))
46    }
47
48    /// Returns true if this is an empty range.
49    pub fn is_empty(&self) -> bool {
50        self.len() == Some(0)
51    }
52
53    /// Validates this range against a known total size.
54    ///
55    /// Returns true if the range is satisfiable (within bounds).
56    pub fn is_satisfiable(&self, total_size: u64) -> bool {
57        if self.start >= total_size {
58            return false;
59        }
60
61        if let Some(end) = self.end {
62            if end >= total_size || end < self.start {
63                return false;
64            }
65        }
66
67        true
68    }
69
70    /// Normalizes the range to have a definite end based on total size.
71    pub fn normalize(&self, total_size: u64) -> Option<Self> {
72        if !self.is_satisfiable(total_size) {
73            return None;
74        }
75
76        let end = self.end.unwrap_or(total_size.saturating_sub(1));
77        let end = end.min(total_size.saturating_sub(1));
78
79        Some(Self {
80            start: self.start,
81            end: Some(end),
82        })
83    }
84}
85
86/// Policy for handling range requests in the cache.
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
88pub enum RangeHandling {
89    /// Pass through range requests without caching (default, simplest)
90    #[default]
91    PassThrough,
92
93    /// Cache the full response on first request, serve ranges from cache
94    /// This requires fetching the complete resource once
95    CacheFullServeRanges,
96
97    /// Cache individual chunks independently (future feature)
98    /// This is the most complex but most efficient for large files
99    #[allow(dead_code)]
100    CacheChunks,
101}
102
103/// Parses a Range header value.
104///
105/// Supports the standard "bytes=start-end" format. Returns None if the header
106/// is missing, malformed, or uses a unit other than "bytes".
107///
108/// # Examples
109///
110/// ```
111/// use tower_http_cache::range::parse_range_header;
112/// use http::HeaderMap;
113///
114/// let mut headers = HeaderMap::new();
115///
116/// // Request bytes 0-1023
117/// headers.insert("range", "bytes=0-1023".parse().unwrap());
118/// let range = parse_range_header(&headers).unwrap();
119/// assert_eq!(range.start, 0);
120/// assert_eq!(range.end, Some(1023));
121///
122/// // Request from byte 1024 to end
123/// headers.insert("range", "bytes=1024-".parse().unwrap());
124/// let range = parse_range_header(&headers).unwrap();
125/// assert_eq!(range.start, 1024);
126/// assert_eq!(range.end, None);
127/// ```
128pub fn parse_range_header(headers: &HeaderMap) -> Option<RangeRequest> {
129    let range_header = headers.get(http::header::RANGE)?;
130    let range_str = range_header.to_str().ok()?;
131
132    // Must start with "bytes="
133    if !range_str.starts_with("bytes=") {
134        return None;
135    }
136
137    // Extract the range specification
138    let range_spec = &range_str[6..]; // Skip "bytes="
139
140    // Split on comma to get first range (we only support single ranges for now)
141    let first_range = range_spec.split(',').next()?;
142
143    // Parse "start-end" format
144    let parts: Vec<&str> = first_range.split('-').collect();
145    if parts.len() != 2 {
146        return None;
147    }
148
149    // Parse start
150    let start = parts[0].trim().parse::<u64>().ok()?;
151
152    // Parse end (optional)
153    let end = if parts[1].trim().is_empty() {
154        None
155    } else {
156        Some(parts[1].trim().parse::<u64>().ok()?)
157    };
158
159    Some(RangeRequest { start, end })
160}
161
162/// Checks if a response status code indicates a partial content response.
163///
164/// Returns true for HTTP 206 Partial Content.
165pub fn is_partial_content(status: StatusCode) -> bool {
166    status == StatusCode::PARTIAL_CONTENT
167}
168
169/// Builds a Content-Range header value.
170///
171/// Creates a header value in the format "bytes start-end/total".
172///
173/// # Example
174///
175/// ```
176/// use tower_http_cache::range::build_content_range_header;
177///
178/// let header = build_content_range_header(0, 1023, 10240);
179/// assert_eq!(header.to_str().unwrap(), "bytes 0-1023/10240");
180/// ```
181pub fn build_content_range_header(start: u64, end: u64, total: u64) -> HeaderValue {
182    let value = format!("bytes {}-{}/{}", start, end, total);
183    HeaderValue::from_str(&value).expect("Content-Range value should be valid")
184}
185
186/// Parses a Content-Range header from a response.
187///
188/// Returns the start, end, and total size if the header is present and valid.
189pub fn parse_content_range_header(headers: &HeaderMap) -> Option<(u64, u64, u64)> {
190    let content_range = headers.get(http::header::CONTENT_RANGE)?;
191    let range_str = content_range.to_str().ok()?;
192
193    // Format: "bytes start-end/total"
194    if !range_str.starts_with("bytes ") {
195        return None;
196    }
197
198    let range_spec = &range_str[6..]; // Skip "bytes "
199    let parts: Vec<&str> = range_spec.split('/').collect();
200    if parts.len() != 2 {
201        return None;
202    }
203
204    let range_parts: Vec<&str> = parts[0].split('-').collect();
205    if range_parts.len() != 2 {
206        return None;
207    }
208
209    let start = range_parts[0].trim().parse::<u64>().ok()?;
210    let end = range_parts[1].trim().parse::<u64>().ok()?;
211    let total = parts[1].trim().parse::<u64>().ok()?;
212
213    Some((start, end, total))
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn test_parse_range_with_start_and_end() {
222        let mut headers = HeaderMap::new();
223        headers.insert(http::header::RANGE, "bytes=0-1023".parse().unwrap());
224
225        let range = parse_range_header(&headers).unwrap();
226        assert_eq!(range.start, 0);
227        assert_eq!(range.end, Some(1023));
228    }
229
230    #[test]
231    fn test_parse_range_with_start_only() {
232        let mut headers = HeaderMap::new();
233        headers.insert(http::header::RANGE, "bytes=1024-".parse().unwrap());
234
235        let range = parse_range_header(&headers).unwrap();
236        assert_eq!(range.start, 1024);
237        assert_eq!(range.end, None);
238    }
239
240    #[test]
241    fn test_parse_range_missing_header() {
242        let headers = HeaderMap::new();
243        assert!(parse_range_header(&headers).is_none());
244    }
245
246    #[test]
247    fn test_parse_range_invalid_unit() {
248        let mut headers = HeaderMap::new();
249        headers.insert(http::header::RANGE, "items=0-10".parse().unwrap());
250
251        assert!(parse_range_header(&headers).is_none());
252    }
253
254    #[test]
255    fn test_parse_range_malformed() {
256        let mut headers = HeaderMap::new();
257        headers.insert(http::header::RANGE, "bytes=invalid".parse().unwrap());
258
259        assert!(parse_range_header(&headers).is_none());
260    }
261
262    #[test]
263    fn test_range_len() {
264        let range = RangeRequest::new(0, Some(1023));
265        assert_eq!(range.len(), Some(1024));
266
267        let range_open = RangeRequest::new(1024, None);
268        assert_eq!(range_open.len(), None);
269    }
270
271    #[test]
272    fn test_range_is_satisfiable() {
273        let range = RangeRequest::new(0, Some(1023));
274        assert!(range.is_satisfiable(10240));
275        assert!(!range.is_satisfiable(512));
276
277        let range_at_end = RangeRequest::new(10240, Some(10250));
278        assert!(!range_at_end.is_satisfiable(10240));
279    }
280
281    #[test]
282    fn test_range_normalize() {
283        let range = RangeRequest::new(0, None);
284        let normalized = range.normalize(10240).unwrap();
285        assert_eq!(normalized.start, 0);
286        assert_eq!(normalized.end, Some(10239));
287
288        let range_explicit = RangeRequest::new(0, Some(1023));
289        let normalized_explicit = range_explicit.normalize(10240).unwrap();
290        assert_eq!(normalized_explicit.start, 0);
291        assert_eq!(normalized_explicit.end, Some(1023));
292    }
293
294    #[test]
295    fn test_range_normalize_out_of_bounds() {
296        let range = RangeRequest::new(10240, Some(20000));
297        assert!(range.normalize(10240).is_none());
298    }
299
300    #[test]
301    fn test_is_partial_content() {
302        assert!(is_partial_content(StatusCode::PARTIAL_CONTENT));
303        assert!(!is_partial_content(StatusCode::OK));
304        assert!(!is_partial_content(StatusCode::NOT_FOUND));
305    }
306
307    #[test]
308    fn test_build_content_range_header() {
309        let header = build_content_range_header(0, 1023, 10240);
310        assert_eq!(header.to_str().unwrap(), "bytes 0-1023/10240");
311    }
312
313    #[test]
314    fn test_parse_content_range_header() {
315        let mut headers = HeaderMap::new();
316        headers.insert(
317            http::header::CONTENT_RANGE,
318            "bytes 0-1023/10240".parse().unwrap(),
319        );
320
321        let (start, end, total) = parse_content_range_header(&headers).unwrap();
322        assert_eq!(start, 0);
323        assert_eq!(end, 1023);
324        assert_eq!(total, 10240);
325    }
326
327    #[test]
328    fn test_parse_content_range_missing() {
329        let headers = HeaderMap::new();
330        assert!(parse_content_range_header(&headers).is_none());
331    }
332
333    #[test]
334    fn test_range_request_is_empty() {
335        let empty_range = RangeRequest::new(0, Some(0));
336        assert!(!empty_range.is_empty()); // 0-0 is actually 1 byte
337
338        let range = RangeRequest::new(100, Some(99)); // Invalid range
339        let len = range.len();
340        // This is actually an invalid range, len would be computed incorrectly
341        // In practice, is_satisfiable would catch this
342        assert!(len.is_some());
343    }
344
345    #[test]
346    fn test_parse_range_with_whitespace() {
347        let mut headers = HeaderMap::new();
348        headers.insert(http::header::RANGE, "bytes= 0 - 1023 ".parse().unwrap());
349
350        // Should handle whitespace gracefully
351        if let Some(range) = parse_range_header(&headers) {
352            assert_eq!(range.start, 0);
353            assert_eq!(range.end, Some(1023));
354        }
355    }
356
357    #[test]
358    fn test_range_multiple_ranges_first_only() {
359        let mut headers = HeaderMap::new();
360        headers.insert(
361            http::header::RANGE,
362            "bytes=0-1023,1024-2047".parse().unwrap(),
363        );
364
365        // We only support single ranges, should parse first one
366        let range = parse_range_header(&headers).unwrap();
367        assert_eq!(range.start, 0);
368        assert_eq!(range.end, Some(1023));
369    }
370}