mkit_server/http_objects/
range.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum Selection {
7 Full,
11 Partial {
13 start: u64,
15 end: u64,
17 },
18 Unsatisfiable,
20}
21
22#[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 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 Some(text.parse::<u64>().unwrap_or(u64::MAX))
61}
62
63#[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 (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 (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 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}