1use std::str::FromStr;
4
5use libfw_core::RangeSpec;
6
7#[derive(Debug, PartialEq, Eq)]
9pub enum RangeParseError {
10 Malformed,
12}
13
14#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum ParsedRange {
17 Bytes(RangeSpec),
19 Suffix(u64),
21}
22
23pub fn parse_range_header(header: &str) -> Result<Option<ParsedRange>, RangeParseError> {
31 let header = header.trim();
32 if header.is_empty() {
33 return Ok(None);
34 }
35 let Some((unit, spec)) = header.split_once('=') else {
36 return Ok(None); };
38 if !unit.trim().eq_ignore_ascii_case("bytes") {
39 return Ok(None);
40 }
41 let spec = spec.trim();
42 if spec.contains(',') {
43 return Ok(None); }
45 if let Some(suffix) = spec.strip_prefix('-') {
46 let n = parse_u64(suffix)?;
47 return Ok(Some(ParsedRange::Suffix(n)));
48 }
49 if let Some((start_s, end_s)) = spec.split_once('-') {
50 let start = parse_u64(start_s)?;
51 let end = if end_s.is_empty() {
52 u64::MAX
53 } else {
54 parse_u64(end_s)?
57 .checked_add(1)
58 .ok_or(RangeParseError::Malformed)?
59 };
60 if end <= start {
61 return Err(RangeParseError::Malformed);
62 }
63 return Ok(Some(ParsedRange::Bytes(RangeSpec { start, end })));
64 }
65 Err(RangeParseError::Malformed)
66}
67
68fn parse_u64(s: &str) -> Result<u64, RangeParseError> {
69 if s.is_empty() {
70 return Err(RangeParseError::Malformed);
71 }
72 u64::from_str(s).map_err(|_| RangeParseError::Malformed)
73}
74
75pub fn content_range_value(spec: &RangeSpec, total: u64) -> String {
77 format!("bytes {}-{}/{}", spec.start, spec.end - 1, total)
78}
79
80pub fn content_range_none_value(total: u64) -> String {
82 format!("bytes */{total}")
83}
84
85pub fn etag_matches_if_none_match(if_none_match: &str, etag: &str) -> bool {
90 let strong_etag = etag.strip_prefix("W/").unwrap_or(etag);
91 if_none_match.split(',').any(|tag| {
92 let tag = tag.trim().strip_prefix("W/").unwrap_or(tag.trim());
93 tag == "*" || tag == strong_etag
94 })
95}
96
97pub fn if_range_matches(if_range: &str, etag: &str) -> bool {
101 let candidate = if_range.trim();
102 if let Some(tag) = candidate.strip_prefix('"') {
103 return tag.trim_end_matches('"') == etag.trim_matches('"');
105 }
106 false
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn parses_closed_range() {
116 let ParsedRange::Bytes(r) = parse_range_header("bytes=0-499").unwrap().unwrap() else {
117 panic!("expected bytes range");
118 };
119 assert_eq!((r.start, r.end), (0, 500));
120 }
121
122 #[test]
123 fn parses_open_ended_range() {
124 let ParsedRange::Bytes(r) = parse_range_header("bytes=500-").unwrap().unwrap() else {
125 panic!("expected bytes range");
126 };
127 assert_eq!((r.start, r.end), (500, u64::MAX));
128 }
129
130 #[test]
131 fn parses_suffix_range() {
132 let ParsedRange::Suffix(n) = parse_range_header("bytes=-500").unwrap().unwrap() else {
133 panic!("expected suffix range");
134 };
135 assert_eq!(n, 500);
136 }
137
138 #[test]
139 fn ignores_other_units_and_multi_ranges() {
140 assert_eq!(parse_range_header("items=0-1").unwrap(), None);
141 assert_eq!(parse_range_header("bytes=0-1,3-4").unwrap(), None);
142 assert_eq!(parse_range_header("").unwrap(), None);
143 }
144
145 #[test]
146 fn rejects_malformed() {
147 assert_eq!(parse_range_header("bytes=abc"), Err(RangeParseError::Malformed));
148 assert_eq!(parse_range_header("bytes=5-2"), Err(RangeParseError::Malformed));
149 assert_eq!(parse_range_header("bytes=-"), Err(RangeParseError::Malformed));
150 }
151
152 #[test]
153 fn rejects_overflowing_end_instead_of_panicking() {
154 assert_eq!(
156 parse_range_header("bytes=0-18446744073709551615"),
157 Err(RangeParseError::Malformed)
158 );
159 }
160
161 #[test]
162 fn if_none_match_matching() {
163 assert!(etag_matches_if_none_match("\"abc\"", "\"abc\""));
164 assert!(etag_matches_if_none_match("W/\"abc\"", "\"abc\""));
165 assert!(etag_matches_if_none_match("*", "\"abc\""));
166 assert!(etag_matches_if_none_match("\"x\", \"abc\"", "\"abc\""));
167 assert!(!etag_matches_if_none_match("\"xyz\"", "\"abc\""));
168 }
169
170 #[test]
171 fn if_range_matching() {
172 assert!(if_range_matches("\"abc\"", "\"abc\""));
173 assert!(!if_range_matches("W/\"abc\"", "\"abc\""));
174 assert!(!if_range_matches("\"xyz\"", "\"abc\""));
175 assert!(!if_range_matches("Wed, 01 Jan 2025 00:00:00 GMT", "\"abc\""));
176 }
177}