1#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum RangeDecision {
15 Full,
18 Partial { start: u64, end: u64 },
20 Unsatisfiable,
22}
23
24pub fn etag_of(sha256: &str) -> String {
26 format!("\"{sha256}\"")
27}
28
29pub fn decide(range: Option<&str>, if_range: Option<&str>, etag: &str, size: u64) -> RangeDecision {
32 let Some(range) = range else {
33 return RangeDecision::Full;
34 };
35 if let Some(validator) = if_range {
37 if validator.trim() != etag {
38 return RangeDecision::Full;
39 }
40 }
41 let Some(spec) = range.trim().strip_prefix("bytes=") else {
42 return RangeDecision::Full;
43 };
44 if spec.contains(',') {
45 return RangeDecision::Full;
46 }
47 let Some((first, last)) = spec.trim().split_once('-') else {
48 return RangeDecision::Full;
49 };
50 let (first, last) = (first.trim(), last.trim());
51 if first.is_empty() {
52 let Ok(n) = last.parse::<u64>() else {
54 return RangeDecision::Full;
55 };
56 if n == 0 || size == 0 {
57 return RangeDecision::Unsatisfiable;
58 }
59 let start = size.saturating_sub(n);
60 return RangeDecision::Partial {
61 start,
62 end: size - 1,
63 };
64 }
65 let Ok(start) = first.parse::<u64>() else {
66 return RangeDecision::Full;
67 };
68 let end = if last.is_empty() {
69 size.saturating_sub(1)
70 } else {
71 match last.parse::<u64>() {
72 Ok(e) if e >= start => e.min(size.saturating_sub(1)),
73 _ => return RangeDecision::Full,
74 }
75 };
76 if start >= size {
77 return RangeDecision::Unsatisfiable;
78 }
79 RangeDecision::Partial { start, end }
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85
86 const TAG: &str = "\"abc\"";
87
88 #[test]
89 fn without_a_range_the_whole_file() {
90 assert_eq!(decide(None, None, TAG, 100), RangeDecision::Full);
91 assert_eq!(decide(None, Some(TAG), TAG, 100), RangeDecision::Full);
92 }
93
94 #[test]
95 fn open_and_closed_ranges() {
96 assert_eq!(
97 decide(Some("bytes=40-"), None, TAG, 100),
98 RangeDecision::Partial { start: 40, end: 99 }
99 );
100 assert_eq!(
101 decide(Some("bytes=0-9"), None, TAG, 100),
102 RangeDecision::Partial { start: 0, end: 9 }
103 );
104 assert_eq!(
106 decide(Some("bytes=90-500"), None, TAG, 100),
107 RangeDecision::Partial { start: 90, end: 99 }
108 );
109 assert_eq!(
110 decide(Some("bytes=-10"), None, TAG, 100),
111 RangeDecision::Partial { start: 90, end: 99 }
112 );
113 assert_eq!(
114 decide(Some("bytes=-500"), None, TAG, 100),
115 RangeDecision::Partial { start: 0, end: 99 }
116 );
117 }
118
119 #[test]
120 fn past_the_end_is_unsatisfiable() {
121 assert_eq!(
122 decide(Some("bytes=100-"), None, TAG, 100),
123 RangeDecision::Unsatisfiable
124 );
125 assert_eq!(
126 decide(Some("bytes=0-"), None, TAG, 0),
127 RangeDecision::Unsatisfiable
128 );
129 assert_eq!(
130 decide(Some("bytes=-0"), None, TAG, 100),
131 RangeDecision::Unsatisfiable
132 );
133 }
134
135 #[test]
136 fn if_range_must_name_this_content() {
137 assert_eq!(
138 decide(Some("bytes=40-"), Some(TAG), TAG, 100),
139 RangeDecision::Partial { start: 40, end: 99 }
140 );
141 assert_eq!(
142 decide(Some("bytes=40-"), Some("\"other\""), TAG, 100),
143 RangeDecision::Full,
144 "the file changed: start over"
145 );
146 assert_eq!(
147 decide(
148 Some("bytes=40-"),
149 Some("Wed, 21 Oct 2015 07:28:00 GMT"),
150 TAG,
151 100
152 ),
153 RangeDecision::Full,
154 "dates are not supported"
155 );
156 }
157
158 #[test]
159 fn unsupported_or_malformed_ranges_get_the_whole_file() {
160 for range in [
161 "bytes=0-9,20-29",
162 "items=0-9",
163 "bytes=x-",
164 "bytes=9-3",
165 "bytes=",
166 "bytes=-x",
167 ] {
168 assert_eq!(
169 decide(Some(range), None, TAG, 100),
170 RangeDecision::Full,
171 "{range}"
172 );
173 }
174 assert_eq!(etag_of("ab12"), "\"ab12\"");
175 }
176}