1#[derive(Debug, PartialEq, Eq)]
5pub enum Resolved {
6 Whole,
9 Part { start: u64, end: u64 },
11 Unsatisfiable,
13}
14
15pub fn resolve(header: &str, if_range: Option<&str>, size: u64) -> Resolved {
29 if if_range.is_some() {
30 return Resolved::Whole;
31 }
32
33 let Some(spec) = header.trim().strip_prefix("bytes=") else {
36 return Resolved::Whole;
37 };
38 if spec.contains(',') {
39 return Resolved::Whole;
40 }
41 let Some((first, last)) = spec.split_once('-') else {
42 return Resolved::Whole;
43 };
44 let (first, last) = (first.trim(), last.trim());
45
46 if size == 0 {
48 return Resolved::Unsatisfiable;
49 }
50 let final_byte = size - 1;
51
52 match (first.is_empty(), last.is_empty()) {
53 (true, false) => {
56 let Ok(suffix) = last.parse::<u64>() else {
57 return Resolved::Whole;
58 };
59 if suffix == 0 {
60 return Resolved::Unsatisfiable;
61 }
62 Resolved::Part {
63 start: size.saturating_sub(suffix),
64 end: final_byte,
65 }
66 }
67 (false, true) => {
69 let Ok(start) = first.parse::<u64>() else {
70 return Resolved::Whole;
71 };
72 if start > final_byte {
73 return Resolved::Unsatisfiable;
74 }
75 Resolved::Part {
76 start,
77 end: final_byte,
78 }
79 }
80 (false, false) => {
82 let (Ok(start), Ok(end)) = (first.parse::<u64>(), last.parse::<u64>()) else {
83 return Resolved::Whole;
84 };
85 if start > end || start > final_byte {
86 return Resolved::Unsatisfiable;
87 }
88 Resolved::Part {
89 start,
90 end: end.min(final_byte),
91 }
92 }
93 (true, true) => Resolved::Whole,
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 fn part(start: u64, end: u64) -> Resolved {
103 Resolved::Part { start, end }
104 }
105
106 #[test]
107 fn both_ends_given() {
108 assert_eq!(resolve("bytes=0-9", None, 100), part(0, 9));
109 assert_eq!(resolve("bytes=10-19", None, 100), part(10, 19));
110 assert_eq!(resolve("bytes=99-99", None, 100), part(99, 99));
112 }
113
114 #[test]
115 fn an_end_past_the_file_is_clamped_rather_than_refused() {
116 assert_eq!(resolve("bytes=0-1000", None, 100), part(0, 99));
117 assert_eq!(resolve("bytes=50-1000", None, 100), part(50, 99));
118 }
119
120 #[test]
121 fn an_open_ended_range_runs_to_the_last_byte() {
122 assert_eq!(resolve("bytes=90-", None, 100), part(90, 99));
123 assert_eq!(resolve("bytes=0-", None, 100), part(0, 99));
124 }
125
126 #[test]
127 fn a_suffix_range_counts_back_from_the_end() {
128 assert_eq!(resolve("bytes=-10", None, 100), part(90, 99));
129 assert_eq!(resolve("bytes=-500", None, 100), part(0, 99));
131 }
132
133 #[test]
134 fn ranges_outside_the_file_are_unsatisfiable() {
135 assert_eq!(resolve("bytes=100-", None, 100), Resolved::Unsatisfiable);
136 assert_eq!(resolve("bytes=100-200", None, 100), Resolved::Unsatisfiable);
137 assert_eq!(resolve("bytes=5-3", None, 100), Resolved::Unsatisfiable);
139 assert_eq!(resolve("bytes=-0", None, 100), Resolved::Unsatisfiable);
141 }
142
143 #[test]
144 fn an_empty_file_satisfies_nothing() {
145 assert_eq!(resolve("bytes=0-0", None, 0), Resolved::Unsatisfiable);
146 assert_eq!(resolve("bytes=0-", None, 0), Resolved::Unsatisfiable);
147 assert_eq!(resolve("bytes=-1", None, 0), Resolved::Unsatisfiable);
148 }
149
150 #[test]
154 fn a_multi_range_request_is_answered_whole() {
155 assert_eq!(resolve("bytes=0-9,20-29", None, 100), Resolved::Whole);
156 }
157
158 #[test]
159 fn an_unknown_unit_or_malformed_spec_is_answered_whole() {
160 assert_eq!(resolve("items=0-9", None, 100), Resolved::Whole);
161 assert_eq!(resolve("bytes=abc-def", None, 100), Resolved::Whole);
162 assert_eq!(resolve("bytes=-", None, 100), Resolved::Whole);
163 assert_eq!(resolve("nonsense", None, 100), Resolved::Whole);
164 assert_eq!(resolve("", None, 100), Resolved::Whole);
165 }
166
167 #[test]
170 fn if_range_is_never_honoured_because_the_validator_is_weak() {
171 assert_eq!(
172 resolve("bytes=0-9", Some("W/\"64-7\""), 100),
173 Resolved::Whole
174 );
175 assert_eq!(resolve("bytes=0-9", Some("\"64-7\""), 100), Resolved::Whole);
178 }
179
180 #[test]
181 fn whitespace_around_the_spec_is_tolerated() {
182 assert_eq!(resolve(" bytes=0-9 ", None, 100), part(0, 9));
183 assert_eq!(resolve("bytes= 10 - 19 ", None, 100), part(10, 19));
184 }
185}