| 1 | //! Byte ranges: the `Content-Range` a chunk of an upload names, the |
| 2 | //! `Range` an upload's replies say it holds, and the `Range` a download |
| 3 | //! asks for. |
| 4 | |
| 5 | /// The bytes a chunk says it holds, `start` to `end` inclusive. |
| 6 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 7 | pub struct ChunkRange { |
| 8 | pub start: u64, |
| 9 | pub end: u64, |
| 10 | } |
| 11 | |
| 12 | /// A chunk's `Content-Range`: `<start>-<end>`, as the Distribution spec |
| 13 | /// writes it, or the HTTP form `bytes <start>-<end>/<total or *>`. |
| 14 | pub fn parse_content_range(header: &str) -> Option<ChunkRange> { |
| 15 | let text = header.trim(); |
| 16 | let text = text.strip_prefix("bytes ").unwrap_or(text).trim(); |
| 17 | let text = text.split_once('/').map_or(text, |(range, _)| range); |
| 18 | let (start, end) = text.split_once('-')?; |
| 19 | let (start, end) = (start.trim().parse().ok()?, end.trim().parse().ok()?); |
| 20 | (start <= end).then_some(ChunkRange { start, end }) |
| 21 | } |
| 22 | |
| 23 | /// What an upload's replies say it holds so far: `0-<last byte>`, and |
| 24 | /// `0-0` before the first byte, as clients expect. |
| 25 | pub fn upload_range(offset: u64) -> String { |
| 26 | format!("0-{}", offset.saturating_sub(1)) |
| 27 | } |
| 28 | |
| 29 | /// A part of a blob a download asks for, resolved against its size. |
| 30 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 31 | pub struct Wanted { |
| 32 | pub offset: u64, |
| 33 | pub length: u64, |
| 34 | } |
| 35 | |
| 36 | impl Wanted { |
| 37 | /// `bytes <first>-<last>/<size>`. |
| 38 | pub fn content_range(&self, size: u64) -> String { |
| 39 | format!("bytes {}-{}/{size}", self.offset, self.offset + self.length - 1) |
| 40 | } |
| 41 | } |
| 42 | |
| 43 | /// What a download's `Range` header asks for, against a blob of `size` |
| 44 | /// bytes. `Ok(None)`: the whole blob (no header, or one this does not |
| 45 | /// read, such as several ranges). `Err(())`: nothing of the blob is in it, |
| 46 | /// which is answered 416. |
| 47 | pub fn parse_range(header: Option<&str>, size: u64) -> Result<Option<Wanted>, ()> { |
| 48 | let Some(spec) = header.and_then(|h| h.trim().strip_prefix("bytes=")) else { |
| 49 | return Ok(None); |
| 50 | }; |
| 51 | if spec.contains(',') { |
| 52 | return Ok(None); |
| 53 | } |
| 54 | let Some((first, last)) = spec.trim().split_once('-') else { |
| 55 | return Ok(None); |
| 56 | }; |
| 57 | let (first, last) = (first.trim(), last.trim()); |
| 58 | let wanted = if first.is_empty() { |
| 59 | let Ok(suffix) = last.parse::<u64>() else { return Ok(None) }; |
| 60 | if suffix == 0 || size == 0 { |
| 61 | return Err(()); |
| 62 | } |
| 63 | let length = suffix.min(size); |
| 64 | Wanted { offset: size - length, length } |
| 65 | } else { |
| 66 | let Ok(offset) = first.parse::<u64>() else { return Ok(None) }; |
| 67 | if offset >= size { |
| 68 | return Err(()); |
| 69 | } |
| 70 | let end = if last.is_empty() { |
| 71 | size - 1 |
| 72 | } else { |
| 73 | match last.parse::<u64>() { |
| 74 | Ok(end) if end >= offset => end.min(size - 1), |
| 75 | _ => return Ok(None), |
| 76 | } |
| 77 | }; |
| 78 | Wanted { offset, length: end - offset + 1 } |
| 79 | }; |
| 80 | Ok(Some(wanted)) |
| 81 | } |
| 82 | |
| 83 | #[cfg(test)] |
| 84 | mod tests { |
| 85 | use super::*; |
| 86 | |
| 87 | #[test] |
| 88 | fn a_chunk_names_its_range_either_way() { |
| 89 | assert_eq!(parse_content_range("0-1023"), Some(ChunkRange { start: 0, end: 1023 })); |
| 90 | assert_eq!(parse_content_range("bytes 1024-2047/*"), Some(ChunkRange { start: 1024, end: 2047 })); |
| 91 | assert_eq!(parse_content_range(" 5-5 "), Some(ChunkRange { start: 5, end: 5 })); |
| 92 | assert_eq!(parse_content_range("9-3"), None); |
| 93 | assert_eq!(parse_content_range("x-3"), None); |
| 94 | assert_eq!(parse_content_range(""), None); |
| 95 | } |
| 96 | |
| 97 | #[test] |
| 98 | fn an_upload_says_what_it_holds() { |
| 99 | assert_eq!(upload_range(0), "0-0"); |
| 100 | assert_eq!(upload_range(1), "0-0"); |
| 101 | assert_eq!(upload_range(1024), "0-1023"); |
| 102 | } |
| 103 | |
| 104 | #[test] |
| 105 | fn a_download_range_is_resolved_against_the_size() { |
| 106 | let w = |offset, length| Ok(Some(Wanted { offset, length })); |
| 107 | assert_eq!(parse_range(None, 100), Ok(None)); |
| 108 | assert_eq!(parse_range(Some("bytes=0-9"), 100), w(0, 10)); |
| 109 | assert_eq!(parse_range(Some("bytes=90-"), 100), w(90, 10)); |
| 110 | assert_eq!(parse_range(Some("bytes=90-500"), 100), w(90, 10)); |
| 111 | assert_eq!(parse_range(Some("bytes=-10"), 100), w(90, 10)); |
| 112 | assert_eq!(parse_range(Some("bytes=-500"), 100), w(0, 100)); |
| 113 | assert_eq!(parse_range(Some("bytes=100-"), 100), Err(())); |
| 114 | assert_eq!(parse_range(Some("bytes=-0"), 100), Err(())); |
| 115 | assert_eq!(parse_range(Some("bytes=0-1,5-9"), 100), Ok(None)); |
| 116 | assert_eq!(parse_range(Some("items=0-1"), 100), Ok(None)); |
| 117 | assert_eq!(parse_range(Some("bytes=9-3"), 100), Ok(None)); |
| 118 | assert_eq!(Wanted { offset: 90, length: 10 }.content_range(100), "bytes 90-99/100"); |
| 119 | } |
| 120 | } |