Skip to main content

fslite_server/
range.rs

1//! Pure-function resolution of the HTTP `Range: bytes=...` header against a
2//! known content length. No axum/HTTP types are involved here — this module
3//! is unit-tested standalone in `tests/range.rs`.
4
5use fslite_core::ByteRange;
6
7/// Why an HTTP `Range` header could not be resolved to a concrete `ByteRange`.
8#[derive(Debug, Eq, PartialEq)]
9pub enum RangeError {
10    /// The header was not a well-formed single `bytes=` range.
11    Malformed,
12    /// The header requested more than one range; unsupported.
13    MultiRangeUnsupported,
14    /// The requested range starts at or beyond the content length.
15    Unsatisfiable,
16}
17
18/// Resolves a single-range `Range: bytes=...` header value (without the
19/// leading header name) against a known content length. Supports
20/// `start-end` (inclusive end), `start-` (open-ended), and `-suffix_len`
21/// (last `suffix_len` bytes, clamped to the content length).
22pub fn resolve_range(header: &str, logical_size: u64) -> Result<ByteRange, RangeError> {
23    let spec = header.strip_prefix("bytes=").ok_or(RangeError::Malformed)?;
24    if spec.contains(',') {
25        return Err(RangeError::MultiRangeUnsupported);
26    }
27
28    let (start_str, end_str) = spec.split_once('-').ok_or(RangeError::Malformed)?;
29
30    if start_str.is_empty() {
31        // Suffix range: "-N" = last N bytes.
32        let suffix_len: u64 = end_str.parse().map_err(|_| RangeError::Malformed)?;
33        if suffix_len == 0 {
34            return Err(RangeError::Malformed);
35        }
36        let start = logical_size.saturating_sub(suffix_len);
37        return Ok(ByteRange::new(start, logical_size));
38    }
39
40    let start: u64 = start_str.parse().map_err(|_| RangeError::Malformed)?;
41    if start >= logical_size {
42        return Err(RangeError::Unsatisfiable);
43    }
44
45    let end = if end_str.is_empty() {
46        logical_size
47    } else {
48        let inclusive_end: u64 = end_str.parse().map_err(|_| RangeError::Malformed)?;
49        // RFC 9110 requires the range's last-byte-pos to be >= first-byte-pos;
50        // e.g. `bytes=5-1` is not a valid range, not an empty/unsatisfiable one.
51        if inclusive_end < start {
52            return Err(RangeError::Malformed);
53        }
54        inclusive_end.saturating_add(1).min(logical_size)
55    };
56
57    Ok(ByteRange::new(start, end))
58}