Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_net/src/http/range.rs

21.7 KiB, 42 runs

created by r1870400018:17808, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! Byte ranges: the `Range` request field, and the `206`, `416` and
2//! `Accept-Ranges` answers to it (RFC 9110 §14).
3//!
4//! A player asked to jump to the middle of a two hour recording does not want the
5//! first hour and a half of it, and will not wait for them. It asks for the bytes
6//! around the point it was sent to, and it asks again as it plays. A server that
7//! cannot answer such a request either sends the whole representation every time
8//! the viewer moves the scrubber, or -- what browsers actually do -- refuses to
9//! offer seeking at all.
10//!
11//! # One range, never several
12//!
13//! The `Range` grammar admits a list, and a server may answer a list with a
14//! `multipart/byteranges` body. This module recognises such a request and declines
15//! to answer it that way: [`RangeRequest::Multiple`] is served as the whole
16//! representation with a plain `200`, which RFC 9110 §14.2 permits ("a server MAY
17//! ignore the Range header field"). Nothing that matters here asks for several
18//! ranges at once -- media players, download managers and browsers all ask for one
19//! window at a time -- and a multipart body costs a boundary generator, a second
20//! framing to get wrong, and a client population that would rather have the file.
21//!
22//! # Not yet: `If-Range`
23//!
24//! RFC 9110 §13.1.5 lets a client attach the validator it holds to a `Range`, so
25//! that a representation which changed underneath it is answered whole rather
26//! than as a window of something else. Nothing here reads that field, so a client
27//! resuming a download of a file that has since been replaced splices two
28//! different files together and does not find out. The fix is to compare the
29//! `If-Range` value against the entity tag before resolving, and to answer `200`
30//! rather than `206` when they disagree.
31//!
32//! # A field that cannot fail
33//!
34//! RFC 9110 §14.2 requires an unsatisfiable *unit* and a malformed field alike to
35//! be ignored rather than rejected, so parsing yields [`RangeRequest::Ignored`]
36//! instead of an error: a client that garbles its `Range` gets the representation,
37//! not a refusal. Only a well-formed byte range that falls entirely outside the
38//! representation earns a `416`, which is a statement about the representation
39//! rather than about the syntax.
40//!
41//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
42//! Anthropic Claude
43
44use crate::http::{
45 fields::{
46 HeaderFieldCategory,
47 HeaderFields,
48 HeaderFieldValue,
49 HeaderName,
50 },
51 msg::HttpMessage,
52 status::HttpStatus,
53};
54
55use oxedyne_fe2o3_core::prelude::*;
56
57
58pub const BYTES_UNIT: &str = "bytes"; // the only unit anyone implements, RFC 9110 §14.1
59
60pub const ACCEPT_RANGES_BYTES: &str = "bytes"; // what a range-answering resource advertises
61
62
63/// One byte-range specifier, as written in a `Range` field and before it has met
64/// the representation it names (RFC 9110 §14.1.1).
65#[derive(Clone, Copy, Debug, Eq, PartialEq)]
66pub enum ByteRangeSpec {
67 FromTo(u64, u64), // `bytes=s-e`, both ends inclusive
68 From(u64), // `bytes=s-`, on to the last byte
69 Suffix(u64), // `bytes=-n`, the last n bytes
70}
71
72/// What a `Range` field asked for.
73#[derive(Clone, Copy, Debug, Eq, PartialEq)]
74pub enum RangeRequest {
75 Single(ByteRangeSpec), // one window, what every player and download manager sends
76 Multiple, // several at once, answered whole; see the module header
77 Ignored, // an unimplemented unit, or a field that did not parse
78}
79
80impl RangeRequest {
81
82 /// Parse the value of a `Range` field, e.g. `bytes=0-499`.
83 ///
84 /// Never fails: anything this server cannot honour is [`Self::Ignored`], which
85 /// the caller serves as the whole representation.
86 pub fn parse(value: &str) -> Self {
87 let trimmed = value.trim();
88 let (unit, list) = match trimmed.split_once('=') {
89 Some(pair) => pair,
90 None => return Self::Ignored,
91 };
92 // The unit is a case-insensitive token, and only `bytes` is implemented.
93 if !unit.trim().eq_ignore_ascii_case(BYTES_UNIT) {
94 return Self::Ignored;
95 }
96
97 let mut specs = Vec::new();
98 for part in list.split(',') {
99 let part = part.trim();
100 if part.is_empty() {
101 // `bytes=0-1,,4-5` is malformed, and the whole field goes with it.
102 return Self::Ignored;
103 }
104 match Self::parse_spec(part) {
105 Some(spec) => specs.push(spec),
106 None => return Self::Ignored,
107 }
108 }
109
110 match specs.len() {
111 0 => Self::Ignored,
112 1 => Self::Single(specs[0]),
113 _ => Self::Multiple,
114 }
115 }
116
117 /// Parse one `first-last`, `first-` or `-suffix` specifier.
118 fn parse_spec(part: &str) -> Option<ByteRangeSpec> {
119 let (first, last) = match part.split_once('-') {
120 Some(pair) => pair,
121 None => return None, // No dash at all is not a range.
122 };
123 let first = first.trim();
124 let last = last.trim();
125
126 if first.is_empty() {
127 // `-n`, the last n bytes. `bytes=-` names nothing.
128 if last.is_empty() {
129 return None;
130 }
131 return match last.parse::<u64>() {
132 Ok(n) => Some(ByteRangeSpec::Suffix(n)),
133 Err(_) => None,
134 };
135 }
136
137 let start = match first.parse::<u64>() {
138 Ok(n) => n,
139 Err(_) => return None,
140 };
141
142 if last.is_empty() {
143 return Some(ByteRangeSpec::From(start));
144 }
145
146 match last.parse::<u64>() {
147 // A last position before the first is not a range at all, and the
148 // grammar of RFC 9110 §14.1.1 forbids it.
149 Ok(end) if end >= start => Some(ByteRangeSpec::FromTo(start, end)),
150 _ => None,
151 }
152 }
153
154 /// Read the `Range` field out of a request's header fields, if it carries one.
155 ///
156 /// The field is held as a `Generic` value rather than encapsulated, because a
157 /// malformed one must be ignored and not turned into a read error that fails
158 /// the whole message.
159 pub fn from_fields(fields: &HeaderFields) -> Option<Self> {
160 fields.get_one(&HeaderName::Range)
161 .map(|val| Self::parse(&fmt!("{}", val)))
162 }
163
164 /// Resolve against a representation of `total` bytes.
165 pub fn resolve(&self, total: u64) -> RangeOutcome {
166 match self {
167 Self::Single(spec) => spec.resolve(total),
168 Self::Multiple => RangeOutcome::Whole,
169 Self::Ignored => RangeOutcome::Whole,
170 }
171 }
172}
173
174impl ByteRangeSpec {
175
176 /// Resolve to a concrete window of a representation of `total` bytes
177 /// (RFC 9110 §14.1.2).
178 ///
179 /// A zero-length representation satisfies no range whatsoever, an end past the
180 /// last byte is clamped to it, and a suffix longer than the representation is
181 /// the whole of it.
182 pub fn resolve(&self, total: u64) -> RangeOutcome {
183 if total == 0 {
184 return RangeOutcome::NotSatisfiable;
185 }
186 let last = total - 1;
187 match *self {
188 Self::FromTo(start, end) => {
189 if start > last {
190 RangeOutcome::NotSatisfiable
191 } else {
192 RangeOutcome::Partial(ByteWindow {
193 start,
194 end: end.min(last),
195 total,
196 })
197 }
198 }
199 Self::From(start) => {
200 if start > last {
201 RangeOutcome::NotSatisfiable
202 } else {
203 RangeOutcome::Partial(ByteWindow { start, end: last, total })
204 }
205 }
206 Self::Suffix(n) => {
207 // `bytes=-0` asks for the last nothing, which no representation
208 // holds (RFC 9110 §14.1.2).
209 if n == 0 {
210 RangeOutcome::NotSatisfiable
211 } else {
212 RangeOutcome::Partial(ByteWindow {
213 start: total.saturating_sub(n),
214 end: last,
215 total,
216 })
217 }
218 }
219 }
220 }
221}
222
223/// A concrete window of a representation, with both ends inclusive as they are on
224/// the wire.
225#[derive(Clone, Copy, Debug, Eq, PartialEq)]
226pub struct ByteWindow {
227 pub start: u64,
228 pub end: u64, // inclusive
229 pub total: u64, // length of the whole representation it was cut from
230}
231
232impl ByteWindow {
233
234 /// How many bytes the window holds, which is the `Content-Length` of the
235 /// answer.
236 pub fn len(&self) -> u64 {
237 self.end - self.start + 1
238 }
239
240 /// Does the window cover the whole representation?
241 pub fn is_whole(&self) -> bool {
242 self.start == 0 && self.len() == self.total
243 }
244
245 /// The `Content-Range` field value naming this window, `bytes s-e/total`.
246 pub fn content_range(&self) -> String {
247 fmt!("{} {}-{}/{}", BYTES_UNIT, self.start, self.end, self.total)
248 }
249}
250
251/// What answering a `Range` field comes to, once the representation is known.
252#[derive(Clone, Copy, Debug, Eq, PartialEq)]
253pub enum RangeOutcome {
254 Whole, // send the whole representation, `200`
255 Partial(ByteWindow), // send this window, `206`, with a `Content-Range` naming it
256 NotSatisfiable, // send `416` with a `Content-Range` of `bytes */total`
257}
258
259/// Read a request's `Range` field and resolve it against a representation of
260/// `total` bytes, in one call.
261///
262/// A request carrying no `Range` at all gets [`RangeOutcome::Whole`], as does one
263/// whose field this server declines to honour.
264pub fn resolve(fields: &HeaderFields, total: u64) -> RangeOutcome {
265 match RangeRequest::from_fields(fields) {
266 Some(req) => req.resolve(total),
267 None => RangeOutcome::Whole,
268 }
269}
270
271/// The `Accept-Ranges: bytes` a resource advertises when it can be asked for in
272/// windows.
273///
274/// A browser will not offer a scrubber on a video the server has not said this
275/// about, however well the server would in fact answer the request.
276pub fn accept_ranges() -> HeaderFieldValue {
277 HeaderFieldValue::Generic(ACCEPT_RANGES_BYTES.to_string())
278}
279
280/// Stamp a response as answerable in byte ranges.
281pub fn with_accept_ranges(msg: HttpMessage) -> HttpMessage {
282 msg.with_field_with_order(
283 HeaderName::AcceptRanges,
284 accept_ranges(),
285 Some(HeaderFieldCategory::Response as u16),
286 )
287}
288
289/// The `Content-Range` field a `206` carries, naming the window sent.
290pub fn content_range_field(window: &ByteWindow) -> HeaderFieldValue {
291 HeaderFieldValue::Generic(window.content_range())
292}
293
294/// The `Content-Range` field a `416` carries, naming only the length the client's
295/// range missed (RFC 9110 §14.4).
296pub fn unsatisfied_range_field(total: u64) -> HeaderFieldValue {
297 HeaderFieldValue::Generic(fmt!("{} */{}", BYTES_UNIT, total))
298}
299
300/// A `416 Range Not Satisfiable`, telling the client how long the representation
301/// actually is so its next request can be a sensible one.
302pub fn not_satisfiable(total: u64) -> HttpMessage {
303 with_accept_ranges(
304 HttpMessage::new_response(HttpStatus::RangeNotSatisfiable)
305 .with_field_with_order(
306 HeaderName::ContentRange,
307 unsatisfied_range_field(total),
308 Some(HeaderFieldCategory::Entity as u16),
309 )
310 )
311}
312
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317
318 fn single(spec: ByteRangeSpec) -> RangeRequest {
319 RangeRequest::Single(spec)
320 }
321
322 // ┌───────────────────────────────────────────────────────────────────────┐
323 // │ PARSING │
324 // └───────────────────────────────────────────────────────────────────────┘
325
326 #[test]
327 fn test_the_three_shapes_of_a_byte_range() {
328 assert_eq!(RangeRequest::parse("bytes=0-499"), single(ByteRangeSpec::FromTo(0, 499)));
329 assert_eq!(RangeRequest::parse("bytes=500-"), single(ByteRangeSpec::From(500)));
330 assert_eq!(RangeRequest::parse("bytes=-500"), single(ByteRangeSpec::Suffix(500)));
331 }
332
333 #[test]
334 fn test_a_single_byte_is_a_range() {
335 assert_eq!(RangeRequest::parse("bytes=0-0"), single(ByteRangeSpec::FromTo(0, 0)));
336 }
337
338 /// The unit is a case-insensitive token, and the field tolerates whitespace
339 /// around what it names.
340 #[test]
341 fn test_the_unit_and_the_spacing_are_forgiving() {
342 assert_eq!(RangeRequest::parse("BYTES=0-9"), single(ByteRangeSpec::FromTo(0, 9)));
343 assert_eq!(RangeRequest::parse(" bytes = 0 - 9 "), single(ByteRangeSpec::FromTo(0, 9)));
344 }
345
346 /// Only `bytes` is implemented, and RFC 9110 §14.2 says an unknown unit is
347 /// ignored rather than refused -- so the client gets the file.
348 #[test]
349 fn test_a_unit_that_is_not_bytes_is_ignored() {
350 assert_eq!(RangeRequest::parse("items=0-9"), RangeRequest::Ignored);
351 assert_eq!(RangeRequest::parse("seconds=0-9"), RangeRequest::Ignored);
352 }
353
354 #[test]
355 fn test_a_field_that_does_not_parse_is_ignored_not_refused() {
356 for bad in [
357 "", // Nothing at all.
358 "bytes", // No `=`.
359 "bytes=", // No specifier.
360 "bytes=-", // Neither end.
361 "bytes=abc-def", // Not numbers.
362 "bytes=1x-2", // Not quite numbers.
363 "bytes=99-10", // Last before first, which the grammar forbids.
364 "bytes=0-1,,4-5", // An empty specifier in the list.
365 ] {
366 assert_eq!(RangeRequest::parse(bad), RangeRequest::Ignored,
367 "{:?} should have been ignored", bad);
368 }
369 }
370
371 /// Several ranges are recognised, and answered with the whole representation.
372 #[test]
373 fn test_several_ranges_are_recognised_and_answered_whole() {
374 assert_eq!(RangeRequest::parse("bytes=0-49,100-149"), RangeRequest::Multiple);
375 assert_eq!(RangeRequest::parse("bytes=0-49,100-149").resolve(1000),
376 RangeOutcome::Whole);
377 }
378
379 // ┌───────────────────────────────────────────────────────────────────────┐
380 // │ RESOLUTION │
381 // └───────────────────────────────────────────────────────────────────────┘
382
383 #[test]
384 fn test_a_window_inside_the_file() {
385 assert_eq!(
386 RangeRequest::parse("bytes=0-99").resolve(1000),
387 RangeOutcome::Partial(ByteWindow { start: 0, end: 99, total: 1000 }),
388 );
389 }
390
391 #[test]
392 fn test_an_open_ended_range_runs_to_the_last_byte() {
393 assert_eq!(
394 RangeRequest::parse("bytes=100-").resolve(1000),
395 RangeOutcome::Partial(ByteWindow { start: 100, end: 999, total: 1000 }),
396 );
397 }
398
399 #[test]
400 fn test_a_suffix_takes_the_last_bytes() {
401 assert_eq!(
402 RangeRequest::parse("bytes=-50").resolve(1000),
403 RangeOutcome::Partial(ByteWindow { start: 950, end: 999, total: 1000 }),
404 );
405 }
406
407 /// An end past the last byte is clamped rather than refused: the client asked
408 /// for more than there is, and gets what there is.
409 #[test]
410 fn test_an_end_past_the_last_byte_is_clamped() {
411 assert_eq!(
412 RangeRequest::parse("bytes=900-99999").resolve(1000),
413 RangeOutcome::Partial(ByteWindow { start: 900, end: 999, total: 1000 }),
414 );
415 }
416
417 /// A start past the last byte is a genuine `416`: there is nothing there to
418 /// clamp to.
419 #[test]
420 fn test_a_start_past_the_last_byte_is_not_satisfiable() {
421 assert_eq!(RangeRequest::parse("bytes=1000-").resolve(1000),
422 RangeOutcome::NotSatisfiable);
423 assert_eq!(RangeRequest::parse("bytes=1000-2000").resolve(1000),
424 RangeOutcome::NotSatisfiable);
425 assert_eq!(RangeRequest::parse("bytes=999999999-").resolve(1000),
426 RangeOutcome::NotSatisfiable);
427 }
428
429 /// The last byte is the last satisfiable start, and asking for it yields one
430 /// byte rather than nothing.
431 #[test]
432 fn test_the_last_byte_is_still_satisfiable() {
433 assert_eq!(
434 RangeRequest::parse("bytes=999-").resolve(1000),
435 RangeOutcome::Partial(ByteWindow { start: 999, end: 999, total: 1000 }),
436 );
437 }
438
439 /// A suffix longer than the representation is the whole of it, not an error.
440 #[test]
441 fn test_a_suffix_longer_than_the_file_is_the_whole_file() -> Outcome<()> {
442 let outcome = RangeRequest::parse("bytes=-5000").resolve(1000);
443 assert_eq!(outcome,
444 RangeOutcome::Partial(ByteWindow { start: 0, end: 999, total: 1000 }));
445 match outcome {
446 RangeOutcome::Partial(w) => assert!(w.is_whole()),
447 other => return Err(err!(
448 "The whole file was resolved as {:?}.", other; Test, Mismatch)),
449 }
450 Ok(())
451 }
452
453 /// `bytes=-0` asks for the last nothing, which no representation holds.
454 #[test]
455 fn test_a_zero_length_suffix_is_not_satisfiable() {
456 assert_eq!(RangeRequest::parse("bytes=-0").resolve(1000),
457 RangeOutcome::NotSatisfiable);
458 }
459
460 /// A zero-length representation satisfies no range at all, including the ones
461 /// that name byte zero -- there is no byte zero.
462 #[test]
463 fn test_an_empty_file_satisfies_nothing() {
464 for asked in ["bytes=0-", "bytes=0-0", "bytes=-1", "bytes=0-99"] {
465 assert_eq!(RangeRequest::parse(asked).resolve(0),
466 RangeOutcome::NotSatisfiable, "{:?} against an empty file", asked);
467 }
468 }
469
470 /// A one-byte file is the smallest thing a range can actually name.
471 #[test]
472 fn test_a_one_byte_file() {
473 assert_eq!(
474 RangeRequest::parse("bytes=0-").resolve(1),
475 RangeOutcome::Partial(ByteWindow { start: 0, end: 0, total: 1 }),
476 );
477 assert_eq!(RangeRequest::parse("bytes=1-").resolve(1),
478 RangeOutcome::NotSatisfiable);
479 }
480
481 /// A window's length counts both ends, and an off-by-one here is a truncated
482 /// video that plays to within a frame of the end and stops.
483 #[test]
484 fn test_a_window_counts_both_of_its_ends() {
485 assert_eq!(ByteWindow { start: 0, end: 99, total: 1000 }.len(), 100);
486 assert_eq!(ByteWindow { start: 0, end: 0, total: 1000 }.len(), 1);
487 assert_eq!(ByteWindow { start: 950, end: 999, total: 1000 }.len(), 50);
488 }
489
490 // ┌───────────────────────────────────────────────────────────────────────┐
491 // │ THE ANSWER ON THE WIRE │
492 // └───────────────────────────────────────────────────────────────────────┘
493
494 #[test]
495 fn test_the_content_range_of_a_window() {
496 assert_eq!(
497 ByteWindow { start: 0, end: 99, total: 1000 }.content_range(),
498 "bytes 0-99/1000",
499 );
500 }
501
502 /// A `416` names the length the client's range missed, and nothing else.
503 #[test]
504 fn test_a_refusal_states_the_length() -> Outcome<()> {
505 let msg = not_satisfiable(1000);
506 let held = res!(msg.header.fields.get_one(&HeaderName::ContentRange)
507 .ok_or_else(|| err!("A 416 carried no Content-Range."; Missing)));
508 assert_eq!(fmt!("{}", held), "bytes */1000");
509 let ranges = res!(msg.header.fields.get_one(&HeaderName::AcceptRanges)
510 .ok_or_else(|| err!("A 416 did not say what it does accept."; Missing)));
511 assert_eq!(fmt!("{}", ranges), "bytes");
512 Ok(())
513 }
514
515 /// The whole answer as bytes, because a field this server emits by hand is a
516 /// field it can render wrongly.
517 #[test]
518 fn test_a_refusal_on_the_wire() -> Outcome<()> {
519 let wire = not_satisfiable(1000).header.as_vec();
520 let text = String::from_utf8_lossy(&wire).to_string();
521 assert!(text.starts_with("HTTP/1.1 416 Range Not Satisfiable\r\n"),
522 "unexpected status line: {:?}", text);
523 assert!(text.contains("content-range: bytes */1000\r\n"),
524 "unexpected fields: {:?}", text);
525 assert!(text.contains("accept-ranges: bytes\r\n"),
526 "unexpected fields: {:?}", text);
527 Ok(())
528 }
529
530 /// A request with no `Range` at all asks for the whole thing.
531 #[test]
532 fn test_no_range_field_means_the_whole_representation() {
533 assert_eq!(resolve(&HeaderFields::default(), 1000), RangeOutcome::Whole);
534 assert_eq!(RangeRequest::from_fields(&HeaderFields::default()), None);
535 }
536
537 #[test]
538 fn test_a_range_field_is_read_off_the_request() -> Outcome<()> {
539 let mut fields = HeaderFields::default();
540 fields.insert(
541 HeaderName::Range,
542 res!(HeaderFieldValue::new(&HeaderName::Range, "bytes=10-19")),
543 None,
544 );
545 assert_eq!(
546 resolve(&fields, 1000),
547 RangeOutcome::Partial(ByteWindow { start: 10, end: 19, total: 1000 }),
548 );
549 Ok(())
550 }
551}