azure_core/request_options/
content_range.rs

1use crate::error::{Error, ErrorKind, ResultExt};
2use std::fmt;
3use std::str::FromStr;
4
5const PREFIX: &str = "bytes ";
6
7#[derive(Debug, Copy, Clone, PartialEq, Eq)]
8pub struct ContentRange {
9    start: u64,
10    end: u64,
11    total_length: u64,
12}
13
14impl ContentRange {
15    pub fn new(start: u64, end: u64, total_length: u64) -> ContentRange {
16        ContentRange {
17            start,
18            end,
19            total_length,
20        }
21    }
22
23    pub fn start(&self) -> u64 {
24        self.start
25    }
26
27    pub fn end(&self) -> u64 {
28        self.end
29    }
30
31    pub fn total_length(&self) -> u64 {
32        self.total_length
33    }
34
35    pub fn is_empty(&self) -> bool {
36        self.end == self.start
37    }
38}
39
40impl FromStr for ContentRange {
41    type Err = Error;
42    fn from_str(s: &str) -> crate::Result<ContentRange> {
43        let remaining = s.strip_prefix(PREFIX).ok_or_else(|| {
44            Error::with_message(ErrorKind::Other, || {
45                format!(
46                    "expected token \"{PREFIX}\" not found when parsing ContentRange from \"{s}\""
47                )
48            })
49        })?;
50
51        // when requesting zero byte files from azurite, it can generate invalid content-range
52        // headers.  See Azure/Azurite#1682 for more information.
53        if cfg!(feature = "azurite_workaround") && remaining == "0--1/0" {
54            return Ok(ContentRange {
55                start: 0,
56                end: 0,
57                total_length: 0,
58            });
59        }
60
61        let mut split_at_dash = remaining.split('-');
62        let start = split_at_dash
63            .next()
64            .ok_or_else(|| {
65                Error::with_message(ErrorKind::Other, || {
66                    format!(
67                        "expected token \"{}\" not found when parsing ContentRange from \"{}\"",
68                        "-", s
69                    )
70                })
71            })?
72            .parse()
73            .map_kind(ErrorKind::DataConversion)?;
74
75        let mut split_at_slash = split_at_dash
76            .next()
77            .ok_or_else(|| {
78                Error::with_message(ErrorKind::Other, || {
79                    format!(
80                        "expected token \"{}\" not found when parsing ContentRange from \"{}\"",
81                        "-", s
82                    )
83                })
84            })?
85            .split('/');
86
87        let end = split_at_slash
88            .next()
89            .ok_or_else(|| {
90                Error::with_message(ErrorKind::Other, || {
91                    format!(
92                        "expected token \"{}\" not found when parsing ContentRange from \"{}\"",
93                        "/", s
94                    )
95                })
96            })?
97            .parse()
98            .map_kind(ErrorKind::DataConversion)?;
99
100        let total_length = split_at_slash
101            .next()
102            .ok_or_else(|| {
103                Error::with_message(ErrorKind::Other, || {
104                    format!(
105                        "expected token \"{}\" not found when parsing ContentRange from \"{}\"",
106                        "/", s
107                    )
108                })
109            })?
110            .parse()
111            .map_kind(ErrorKind::DataConversion)?;
112
113        Ok(ContentRange {
114            start,
115            end,
116            total_length,
117        })
118    }
119}
120
121impl fmt::Display for ContentRange {
122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123        write!(
124            f,
125            "{}{}-{}/{}",
126            PREFIX,
127            self.start(),
128            self.end(),
129            self.total_length()
130        )
131    }
132}
133
134#[cfg(test)]
135mod test {
136    use super::*;
137
138    #[cfg(feature = "azurite_workaround")]
139    #[test]
140    fn test_azurite_workaround() {
141        let range = "bytes 0--1/0".parse::<ContentRange>().unwrap();
142
143        assert_eq!(range.start(), 0);
144        assert_eq!(range.end(), 0);
145        assert_eq!(range.total_length(), 0);
146    }
147
148    #[test]
149    fn test_parse() {
150        let range = "bytes 172032-172489/172490"
151            .parse::<ContentRange>()
152            .unwrap();
153
154        assert_eq!(range.start(), 172032);
155        assert_eq!(range.end(), 172489);
156        assert_eq!(range.total_length(), 172490);
157    }
158
159    #[test]
160    fn test_parse_no_starting_token() {
161        "something else".parse::<ContentRange>().unwrap_err();
162    }
163
164    #[test]
165    fn test_parse_no_dash() {
166        "bytes 100".parse::<ContentRange>().unwrap_err();
167    }
168
169    #[test]
170    fn test_parse_no_slash() {
171        "bytes 100-500".parse::<ContentRange>().unwrap_err();
172    }
173
174    #[test]
175    fn test_display() {
176        let range = ContentRange {
177            start: 100,
178            end: 501,
179            total_length: 5000,
180        };
181
182        let txt = format!("{range}");
183
184        assert_eq!(txt, "bytes 100-501/5000");
185    }
186}