g1t/services/packages/src/range.rs

120 lines4,615 bytesCodeBlame
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)]
7pub 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 *>`.
14pub 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.
25pub 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)]
31pub struct Wanted {
32 pub offset: u64,
33 pub length: u64,
34}
35
36impl 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.
47pub 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)]
84mod 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}