Byte ranges (RFC 9110 section 14): which part of a file a Range header asks for.
Link-preview players (Discord, Twitter) and iOS fetch a video in pieces, so the file that
carries it must answer Range. This is the whole decision, as a pure function of the header
and the file's length; the Worker only slices and sets headers from the answer.
Only a single bytes range is honoured. A header that is absent, in another unit, malformed,
reversed (5-2) or asks for several ranges is ignored and the whole file is sent, which
RFC 9110 permits ("a server MAY ignore the Range header field") and is always correct.
What to send for a request, given the file's length.
The whole file, 200.
15 Whole,
Bytes start..=end of the file, 206. Always start <= end < len.
17 Partial { start: u64, end: u64 },
A valid range that no byte of the file satisfies, 416 with Content-Range: bytes */len.
A number made only of digits. One too large for a u64 saturates, so it behaves as "past
the end of any file" rather than as a syntax error.
The part of a file of len bytes that a Range header (None if the request had none)
selects.
33pub fn select(header: Option<&str>, len: u64) -> Selection { 34 let Some(header) = header else { return Selection::Whole }; 35 let Some((unit, specs)) = header.split_once('=') else { return Selection::Whole }; 36 if !unit.trim().eq_ignore_ascii_case("bytes") { 37 return Selection::Whole; 38 } 39 // Several ranges, or none: not served as a multipart answer, so the whole file. 40 if specs.contains(',') { 41 return Selection::Whole; 42 } 43 let Some((first, last)) = specs.trim().split_once('-') else { return Selection::Whole }; 44 45 match (number(first), number(last)) { 46 // `-N`: the last N bytes. `-0` asks for nothing, which no file satisfies. 47 (None, Some(0)) if first.is_empty() => Selection::Unsatisfiable, 48 (None, Some(n)) if first.is_empty() => { 49 if len == 0 { 50 Selection::Unsatisfiable 51 } else { 52 Selection::Partial { start: len.saturating_sub(n), end: len - 1 } 53 } 54 } 55 // `N-`: from N to the end. 56 (Some(start), None) if last.is_empty() => from(start, u64::MAX, len), 57 // `N-M`: reversed is a syntax error (ignored); an end past the file is clamped. 58 (Some(start), Some(end)) => { 59 if end < start { 60 Selection::Whole 61 } else { 62 from(start, end, len) 63 } 64 } 65 _ => Selection::Whole, 66 } 67}
start..=end clamped to a file of len bytes; unsatisfiable when start is past the end.
78#[cfg(test)] 79mod tests { 80 use super::*; 81 82 const LEN: u64 = 1000; 83 84 fn part(start: u64, end: u64) -> Selection { 85 Selection::Partial { start, end } 86 } 87 88 fn pick(header: &str) -> Selection { 89 select(Some(header), LEN) 90 } 91 92 #[test] 93 fn no_header_is_the_whole_file() { 94 assert_eq!(select(None, LEN), Selection::Whole); 95 } 96 97 #[test] 98 fn a_closed_range() { 99 assert_eq!(pick("bytes=0-99"), part(0, 99)); 100 assert_eq!(pick("bytes=500-500"), part(500, 500)); 101 assert_eq!(pick("bytes=0-0"), part(0, 0)); 102 assert_eq!(pick("bytes=900-999"), part(900, 999)); 103 } 104 105 #[test] 106 fn an_open_ended_range_runs_to_the_end() { 107 assert_eq!(pick("bytes=0-"), part(0, 999)); 108 assert_eq!(pick("bytes=100-"), part(100, 999)); 109 assert_eq!(pick("bytes=999-"), part(999, 999)); 110 } 111 112 #[test] 113 fn a_suffix_range_is_the_last_bytes() { 114 assert_eq!(pick("bytes=-1"), part(999, 999)); 115 assert_eq!(pick("bytes=-100"), part(900, 999)); 116 assert_eq!(pick("bytes=-1000"), part(0, 999)); 117 } 118 119 #[test] 120 fn a_suffix_longer_than_the_file_is_the_whole_file_as_a_range() { 121 assert_eq!(pick("bytes=-1001"), part(0, 999)); 122 assert_eq!(pick("bytes=-99999999999999999999999999"), part(0, 999)); 123 } 124 125 #[test] 126 fn an_end_past_the_file_is_clamped() { 127 assert_eq!(pick("bytes=900-5000"), part(900, 999)); 128 assert_eq!(pick("bytes=0-1000"), part(0, 999)); 129 assert_eq!(pick("bytes=0-99999999999999999999999999"), part(0, 999)); 130 } 131 132 #[test] 133 fn a_start_at_or_past_the_end_cannot_be_satisfied() { 134 assert_eq!(pick("bytes=1000-"), Selection::Unsatisfiable); 135 assert_eq!(pick("bytes=1000-1010"), Selection::Unsatisfiable); 136 assert_eq!(pick("bytes=5000-"), Selection::Unsatisfiable); 137 assert_eq!(pick("bytes=99999999999999999999999999-"), Selection::Unsatisfiable); 138 assert_eq!(pick("bytes=99999999999999999999999999-99999999999999999999999999"), Selection::Unsatisfiable); 139 } 140 141 #[test] 142 fn a_suffix_of_nothing_cannot_be_satisfied() { 143 assert_eq!(pick("bytes=-0"), Selection::Unsatisfiable); 144 } 145 146 #[test] 147 fn an_empty_file_satisfies_no_range() { 148 for header in ["bytes=0-", "bytes=0-0", "bytes=-1", "bytes=0-10", "bytes=5-"] { 149 assert_eq!(select(Some(header), 0), Selection::Unsatisfiable, "{header}"); 150 } 151 assert_eq!(select(None, 0), Selection::Whole); 152 } 153 154 #[test] 155 fn a_reversed_range_is_ignored() { 156 assert_eq!(pick("bytes=5-2"), Selection::Whole); 157 assert_eq!(pick("bytes=999-0"), Selection::Whole); 158 } 159 160 #[test] 161 fn several_ranges_are_ignored() { 162 assert_eq!(pick("bytes=0-10,20-30"), Selection::Whole); 163 assert_eq!(pick("bytes=0-10, 20-30"), Selection::Whole); 164 assert_eq!(pick("bytes=0-10,5000-6000"), Selection::Whole); 165 assert_eq!(pick("bytes=0-0,"), Selection::Whole); 166 } 167 168 #[test] 169 fn anything_else_is_ignored() { 170 for header in [ 171 "", "bytes", "bytes=", "bytes=-", "bytes= - ", "bytes=abc", "bytes=1-2-3", "bytes=a-b", "bytes=1-b", "bytes=a-2", 172 "bytes=+1-2", "bytes=1.5-2", "bytes=0x10-20", "bytes=--5", "bytes=1--5", "bytes=-5-", "=0-10", "0-10", "items=0-10", 173 "bytes 0-10", "bytes=\u{661}-5", 174 ] { 175 assert_eq!(pick(header), Selection::Whole, "{header:?}"); 176 } 177 } 178 179 #[test] 180 fn the_unit_is_case_blind_and_space_around_it_is_allowed() { 181 assert_eq!(pick("Bytes=0-9"), part(0, 9)); 182 assert_eq!(pick("BYTES=0-9"), part(0, 9)); 183 assert_eq!(pick(" bytes =0-9"), part(0, 9)); 184 assert_eq!(pick("bytes= 0-9 "), part(0, 9)); 185 } 186 187 #[test] 188 fn a_partial_is_always_inside_the_file() { 189 let numbers = ["0", "1", "2", "9", "10", "11", "99", "100", "101", "99999999999999999999999"]; 190 for len in [0, 1, 2, 10, 100] { 191 for a in numbers { 192 for b in numbers { 193 for header in [format!("bytes={a}-{b}"), format!("bytes={a}-"), format!("bytes=-{b}")] { 194 if let Selection::Partial { start, end } = select(Some(&header), len) { 195 assert!(start <= end && end < len, "{header} on {len}: {start}-{end}"); 196 } 197 } 198 } 199 } 200 } 201 } 202}