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)? + 1 };
56 if end <= start {
57 return Err(RangeParseError::Malformed);
58 }
59 return Ok(Some(ParsedRange::Bytes(RangeSpec { start, end })));
60 }
61 Err(RangeParseError::Malformed)
62}
63
64fn parse_u64(s: &str) -> Result<u64, RangeParseError> {
65 if s.is_empty() {
66 return Err(RangeParseError::Malformed);
67 }
68 u64::from_str(s).map_err(|_| RangeParseError::Malformed)
69}
70
71pub fn content_range_value(spec: &RangeSpec, total: u64) -> String {
73 format!("bytes {}-{}/{}", spec.start, spec.end - 1, total)
74}
75
76pub fn content_range_none_value(total: u64) -> String {
78 format!("bytes */{total}")
79}
80
81pub fn etag_matches_if_none_match(if_none_match: &str, etag: &str) -> bool {
86 let strong_etag = etag.strip_prefix("W/").unwrap_or(etag);
87 if_none_match.split(',').any(|tag| {
88 let tag = tag.trim().strip_prefix("W/").unwrap_or(tag.trim());
89 tag == "*" || tag == strong_etag
90 })
91}
92
93pub fn if_range_matches(if_range: &str, etag: &str) -> bool {
97 let candidate = if_range.trim();
98 if let Some(tag) = candidate.strip_prefix('"') {
99 return tag.trim_end_matches('"') == etag.trim_matches('"');
101 }
102 false
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[test]
111 fn parses_closed_range() {
112 let ParsedRange::Bytes(r) = parse_range_header("bytes=0-499").unwrap().unwrap() else {
113 panic!("expected bytes range");
114 };
115 assert_eq!((r.start, r.end), (0, 500));
116 }
117
118 #[test]
119 fn parses_open_ended_range() {
120 let ParsedRange::Bytes(r) = parse_range_header("bytes=500-").unwrap().unwrap() else {
121 panic!("expected bytes range");
122 };
123 assert_eq!((r.start, r.end), (500, u64::MAX));
124 }
125
126 #[test]
127 fn parses_suffix_range() {
128 let ParsedRange::Suffix(n) = parse_range_header("bytes=-500").unwrap().unwrap() else {
129 panic!("expected suffix range");
130 };
131 assert_eq!(n, 500);
132 }
133
134 #[test]
135 fn ignores_other_units_and_multi_ranges() {
136 assert_eq!(parse_range_header("items=0-1").unwrap(), None);
137 assert_eq!(parse_range_header("bytes=0-1,3-4").unwrap(), None);
138 assert_eq!(parse_range_header("").unwrap(), None);
139 }
140
141 #[test]
142 fn rejects_malformed() {
143 assert_eq!(parse_range_header("bytes=abc"), Err(RangeParseError::Malformed));
144 assert_eq!(parse_range_header("bytes=5-2"), Err(RangeParseError::Malformed));
145 assert_eq!(parse_range_header("bytes=-"), Err(RangeParseError::Malformed));
146 }
147
148 #[test]
149 fn if_none_match_matching() {
150 assert!(etag_matches_if_none_match("\"abc\"", "\"abc\""));
151 assert!(etag_matches_if_none_match("W/\"abc\"", "\"abc\""));
152 assert!(etag_matches_if_none_match("*", "\"abc\""));
153 assert!(etag_matches_if_none_match("\"x\", \"abc\"", "\"abc\""));
154 assert!(!etag_matches_if_none_match("\"xyz\"", "\"abc\""));
155 }
156
157 #[test]
158 fn if_range_matching() {
159 assert!(if_range_matches("\"abc\"", "\"abc\""));
160 assert!(!if_range_matches("W/\"abc\"", "\"abc\""));
161 assert!(!if_range_matches("\"xyz\"", "\"abc\""));
162 assert!(!if_range_matches("Wed, 01 Jan 2025 00:00:00 GMT", "\"abc\""));
163 }
164}