Skip to main content

mkit_server/http_objects/
range.rs

1//! Conditional requests and ordinary byte ranges (`SPEC-HTTP-OBJECTS` ยง5.1).
2//! Pure functions over header text; proofs never reach them.
3
4/// What a `Range` header selects from a representation of `len` bytes.
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum Selection {
7    /// Serve the whole representation with 200: no `Range`, an invalid or
8    /// unsupported one, a multi-range request, or an `If-Range` that does
9    /// not match a strong `ETag`.
10    Full,
11    /// Serve the inclusive range with 206.
12    Partial {
13        /// First byte.
14        start: u64,
15        /// Last byte, already clamped to the representation.
16        end: u64,
17    },
18    /// 416 with `Content-Range: bytes */len`.
19    Unsatisfiable,
20}
21
22/// One entity-tag in an `If-None-Match` list, weak comparison: `W/` is
23/// ignored. Returns whether `etag` (a quoted strong tag) matches any member
24/// or the list is `*`.
25#[must_use]
26pub fn if_none_match(values: &[String], etag: &str) -> bool {
27    for value in values {
28        let mut rest = value.trim();
29        while !rest.is_empty() {
30            rest = rest.trim_start_matches([' ', '\t', ',']);
31            if let Some(star) = rest.strip_prefix('*') {
32                if star.trim().is_empty() || star.trim_start().starts_with(',') {
33                    return true;
34                }
35                return false;
36            }
37            let tag = rest.strip_prefix("W/").unwrap_or(rest);
38            let Some(body) = tag.strip_prefix('"') else {
39                // Malformed member: skip to the next comma.
40                rest = rest.find(',').map_or("", |i| &rest[i + 1..]);
41                continue;
42            };
43            let Some(close) = body.find('"') else {
44                break;
45            };
46            if &tag[..close + 2] == etag {
47                return true;
48            }
49            rest = &body[close + 1..];
50        }
51    }
52    false
53}
54
55fn parse_bound(text: &str) -> Option<u64> {
56    if text.is_empty() || !text.bytes().all(|b| b.is_ascii_digit()) {
57        return None;
58    }
59    // A value beyond u64 is a valid but unsatisfiable position.
60    Some(text.parse::<u64>().unwrap_or(u64::MAX))
61}
62
63/// Select the bytes for `Range` and `If-Range` against a representation of
64/// `len` bytes whose strong validator is `etag`. An invalid `Range` is
65/// ignored (never a 400), a multi-range request is a strict 200, and only a
66/// matching strong `If-Range` `ETag` enables slicing.
67#[must_use]
68pub fn select(range: Option<&str>, if_range: Option<&str>, etag: &str, len: u64) -> Selection {
69    let Some(spec) = range.map(str::trim) else {
70        return Selection::Full;
71    };
72    if if_range.is_some_and(|validator| validator.trim() != etag) {
73        return Selection::Full;
74    }
75    let Some(spec) = spec
76        .split_once('=')
77        .filter(|(unit, _)| unit.trim().eq_ignore_ascii_case("bytes"))
78        .map(|(_, spec)| spec.trim())
79    else {
80        return Selection::Full;
81    };
82    if spec.contains(',') {
83        return Selection::Full;
84    }
85    let Some((first, last)) = spec.split_once('-') else {
86        return Selection::Full;
87    };
88    let (first, last) = (first.trim(), last.trim());
89    match (first.is_empty(), last.is_empty()) {
90        // `-n`: the final n bytes.
91        (true, false) => match parse_bound(last) {
92            None => Selection::Full,
93            Some(0) => Selection::Unsatisfiable,
94            Some(_) if len == 0 => Selection::Unsatisfiable,
95            Some(n) => Selection::Partial {
96                start: len.saturating_sub(n),
97                end: len - 1,
98            },
99        },
100        // `a-` or `a-b`.
101        (false, _) => {
102            let Some(start) = parse_bound(first) else {
103                return Selection::Full;
104            };
105            let end = if last.is_empty() {
106                None
107            } else if let Some(end) = parse_bound(last) {
108                Some(end)
109            } else {
110                return Selection::Full;
111            };
112            if end.is_some_and(|end| end < start) {
113                return Selection::Full;
114            }
115            if start >= len {
116                return Selection::Unsatisfiable;
117            }
118            Selection::Partial {
119                start,
120                end: end.map_or(len - 1, |end| end.min(len - 1)),
121            }
122        }
123        (true, true) => Selection::Full,
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    const TAG: &str = "\"abc\"";
132
133    #[test]
134    fn range_table() {
135        use Selection::{Full, Partial, Unsatisfiable};
136        let p = |start, end| Partial { start, end };
137        for (range, len, want) in [
138            (Some("bytes=10-19"), 100, p(10, 19)),
139            (Some("bytes=10-"), 100, p(10, 99)),
140            (Some("bytes=-10"), 100, p(90, 99)),
141            (Some("bytes=-500"), 100, p(0, 99)),
142            (Some("bytes=90-500"), 100, p(90, 99)),
143            (Some("bytes=0-0"), 1, p(0, 0)),
144            (Some("bytes=100-200"), 100, Unsatisfiable),
145            (Some("bytes=100-"), 100, Unsatisfiable),
146            (Some("bytes=-0"), 100, Unsatisfiable),
147            (Some("bytes=0-0"), 0, Unsatisfiable),
148            (Some("bytes=-1"), 0, Unsatisfiable),
149            (Some("bytes=99999999999999999999-"), 100, Unsatisfiable),
150            (Some("bytes=0-1,5-6"), 100, Full),
151            (Some("bytes=0-1,5-6"), 0, Full),
152            (Some("bytes=5-2"), 100, Full),
153            (Some("bytes=a-b"), 100, Full),
154            (Some("bytes=-"), 100, Full),
155            (Some("bytes=5"), 100, Full),
156            (Some("items=1-2"), 100, Full),
157            (Some("BYTES=1-2"), 100, p(1, 2)),
158            (None, 100, Full),
159        ] {
160            assert_eq!(select(range, None, TAG, len), want, "{range:?} {len}");
161        }
162    }
163
164    #[test]
165    fn if_range_needs_a_matching_strong_tag() {
166        let range = Some("bytes=1-2");
167        assert_eq!(
168            select(range, Some(TAG), TAG, 10),
169            Selection::Partial { start: 1, end: 2 }
170        );
171        for validator in [
172            "W/\"abc\"",
173            "\"other\"",
174            "Wed, 21 Oct 2015 07:28:00 GMT",
175            "",
176        ] {
177            assert_eq!(select(range, Some(validator), TAG, 10), Selection::Full);
178        }
179        // An unsatisfiable range with a stale validator is a full 200.
180        assert_eq!(
181            select(Some("bytes=50-"), Some("\"x\""), TAG, 10),
182            Selection::Full
183        );
184    }
185
186    #[test]
187    fn if_none_match_uses_weak_comparison() {
188        let v = |s: &str| vec![s.to_owned()];
189        assert!(if_none_match(&v(TAG), TAG));
190        assert!(if_none_match(&v("W/\"abc\""), TAG));
191        assert!(if_none_match(&v("*"), TAG));
192        assert!(if_none_match(&v("\"x\", W/\"abc\" , \"y\""), TAG));
193        assert!(if_none_match(
194            &[v("\"x\"")[0].clone(), v(TAG)[0].clone()],
195            TAG
196        ));
197        assert!(!if_none_match(&v("\"x\", \"y\""), TAG));
198        assert!(!if_none_match(&v("\"abcd\""), TAG));
199        assert!(!if_none_match(&v("abc"), TAG));
200        assert!(!if_none_match(&v(""), TAG));
201        assert!(!if_none_match(&[], TAG));
202        assert!(!if_none_match(&v("*x"), TAG));
203    }
204}