Skip to main content

azure_storage_blob/models/
http_ranges.rs

1// Copyright (c) Microsoft Corporation. All rights reserved.
2// Licensed under the MIT License.
3
4use azure_core::error::{Error, ErrorKind, ResultExt};
5use azure_core::http::headers::{Header, HeaderName, HeaderValue};
6use std::fmt;
7use std::ops::{Range, RangeFrom, RangeInclusive, RangeTo, RangeToInclusive};
8use std::str::FromStr;
9
10const PREFIX: &str = "bytes ";
11const WILDCARD: &str = "*";
12const CONTENT_RANGE_ID: HeaderName = HeaderName::from_static("content-range");
13
14type Result<T> = azure_core::Result<T>;
15
16/// Represents the `Content-Range` HTTP response header.
17#[derive(Debug, Copy, Clone, PartialEq, Eq)]
18pub(crate) struct ContentRange {
19    /// Inclusive start and exclusive end of the range.
20    pub range: Option<(usize, usize)>,
21    /// Total length of the remote resource.
22    pub total_len: Option<usize>,
23}
24
25impl Header for ContentRange {
26    fn name(&self) -> azure_core::http::headers::HeaderName {
27        CONTENT_RANGE_ID
28    }
29
30    fn value(&self) -> azure_core::http::headers::HeaderValue {
31        let range_str = match self.range {
32            Some(range) => format!("{}-{}", range.0, range.1),
33            None => WILDCARD.to_string(),
34        };
35        let len_str = match self.total_len {
36            Some(len) => len.to_string(),
37            None => WILDCARD.to_string(),
38        };
39        format!("{}{}/{}", PREFIX, range_str, len_str).into()
40    }
41}
42
43impl FromStr for ContentRange {
44    type Err = Error;
45    fn from_str(s: &str) -> Result<ContentRange> {
46        let remaining = s.strip_prefix(PREFIX).ok_or_else(|| {
47            Error::with_message_fn(ErrorKind::Other, || {
48                format!(
49                    "expected token \"{PREFIX}\" not found when parsing ContentRange from \"{s}\""
50                )
51            })
52        })?;
53
54        let mut split_at_slash = remaining.split('/');
55
56        let range = parse_range(split_at_slash.next().ok_or_else(|| {
57            Error::with_message(ErrorKind::Other, "Unexpected end of Content-Range.")
58        })?)?;
59
60        let total_len = parse_total_length(split_at_slash.next().ok_or_else(|| {
61            Error::with_message_fn(ErrorKind::Other, || {
62                format!(
63                    "expected token \"{}\" not found when parsing ContentRange from \"{}\"",
64                    "/", s
65                )
66            })
67        })?)?;
68
69        Ok(ContentRange { range, total_len })
70    }
71}
72
73/// Parses the range portion of the Content-Range header: `<unit> <range>/<size>`.
74/// The range portion can be of the format `<start>-<end>` or a wildcard `*`.
75/// `start` and `end` are both serialized as inclusive values, but we return a
76/// half-open range (inclusive start, exclusive end).
77fn parse_range(s: &str) -> Result<Option<(usize, usize)>> {
78    let s = s.trim();
79    if s == WILDCARD {
80        return Ok(None);
81    }
82
83    let mut split_at_dash = s.split('-');
84    let start = split_at_dash
85        .next()
86        .ok_or_else(|| Error::with_message(ErrorKind::Other, "Unexpected end of Content-Range."))?
87        .parse::<usize>()
88        .with_kind(ErrorKind::DataConversion)?;
89    let end = split_at_dash
90        .next()
91        .ok_or_else(|| {
92            Error::with_message_fn(ErrorKind::Other, || {
93                format!(
94                    "expected token \"{}\" not found when parsing ContentRange from \"{}\"",
95                    "-", s
96                )
97            })
98        })?
99        .parse::<usize>()
100        .with_kind(ErrorKind::DataConversion)?;
101
102    Ok(Some((start, end + 1)))
103}
104
105fn parse_total_length(s: &str) -> Result<Option<usize>> {
106    let s = s.trim();
107    if s == WILDCARD {
108        return Ok(None);
109    }
110    Ok(Some(s.parse().with_kind(ErrorKind::DataConversion)?))
111}
112
113impl fmt::Display for ContentRange {
114    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115        write!(
116            f,
117            "{}{}/{}",
118            PREFIX,
119            self.range
120                .map(|range| format!("{}-{}", range.0, range.1 - 1))
121                .unwrap_or(WILDCARD.into()),
122            self.total_len
123                .map(|len| len.to_string())
124                .unwrap_or(WILDCARD.into()),
125        )
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn parse() {
135        let range = "bytes 172032-172489/172490"
136            .parse::<ContentRange>()
137            .unwrap();
138
139        assert_eq!(range.range.unwrap().0, 172032);
140        assert_eq!(range.range.unwrap().1, 172490);
141        assert_eq!(range.total_len.unwrap(), 172490);
142    }
143
144    #[test]
145    fn parse_no_starting_token() {
146        "something else".parse::<ContentRange>().unwrap_err();
147    }
148
149    #[test]
150    fn parse_no_dash() {
151        "bytes 100".parse::<ContentRange>().unwrap_err();
152    }
153
154    #[test]
155    fn parse_no_slash() {
156        "bytes 100-500".parse::<ContentRange>().unwrap_err();
157    }
158
159    #[test]
160    fn display() {
161        let range = ContentRange {
162            range: Some((100, 500)),
163            total_len: Some(5000),
164        };
165
166        let txt = format!("{range}");
167
168        assert_eq!(txt, "bytes 100-499/5000");
169    }
170}
171
172/// Represents an HTTP Range header value for blob operations.
173///
174/// Defines a range of bytes within an HTTP resource, starting at an offset and
175/// ending at `offset + length - 1` inclusively. This matches the semantics of .NET's
176/// `Azure.HttpRange`.
177///
178/// # Examples
179///
180/// Range of 512 bytes starting at offset 0:
181///
182/// ```
183/// use azure_storage_blob::models::HttpRange;
184///
185/// let range = HttpRange::new(0, 512);
186/// assert_eq!(range.to_string(), "bytes=0-511");
187/// ```
188///
189/// Open-ended range starting at offset 255:
190///
191/// ```
192/// use azure_storage_blob::models::HttpRange;
193///
194/// let range = HttpRange::from_offset(255);
195/// assert_eq!(range.to_string(), "bytes=255-");
196/// ```
197///
198/// Convert from standard Rust range types:
199///
200/// ```
201/// use azure_storage_blob::models::HttpRange;
202///
203/// let range: HttpRange = (0u64..100).into();
204/// assert_eq!(range.to_string(), "bytes=0-99");
205///
206/// let range: HttpRange = (100u64..).into();
207/// assert_eq!(range.to_string(), "bytes=100-");
208///
209/// let range: HttpRange = (0u64..=99).into();
210/// assert_eq!(range.to_string(), "bytes=0-99");
211/// ```
212#[derive(Debug, Clone, PartialEq, Eq)]
213pub struct HttpRange {
214    /// The starting byte offset.
215    offset: u64,
216    /// The length of the range. If `None`, the range extends to the end of the resource.
217    length: Option<u64>,
218}
219
220impl HttpRange {
221    /// Creates a new `HttpRange` with the specified offset and length.
222    ///
223    /// The range will cover bytes from `offset` to `offset + length - 1` inclusive.
224    ///
225    /// # Arguments
226    ///
227    /// * `offset` - The starting byte offset.
228    /// * `length` - The number of bytes in the range.
229    pub fn new(offset: u64, length: u64) -> Self {
230        Self {
231            offset,
232            length: Some(length),
233        }
234    }
235
236    /// Creates a new `HttpRange` that starts at the specified offset and extends to the end.
237    ///
238    /// # Arguments
239    ///
240    /// * `offset` - The starting byte offset.
241    pub fn from_offset(offset: u64) -> Self {
242        Self {
243            offset,
244            length: None,
245        }
246    }
247
248    pub(crate) fn offset(&self) -> u64 {
249        self.offset
250    }
251
252    pub(crate) fn length(&self) -> Option<u64> {
253        self.length
254    }
255}
256
257impl fmt::Display for HttpRange {
258    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
259        match self.length {
260            Some(length) => write!(
261                f,
262                "bytes={}-{}",
263                self.offset,
264                self.offset.saturating_add(length).saturating_sub(1)
265            ),
266            None => write!(f, "bytes={}-", self.offset),
267        }
268    }
269}
270
271impl From<HttpRange> for HeaderValue {
272    fn from(range: HttpRange) -> Self {
273        HeaderValue::from(range.to_string())
274    }
275}
276
277// From<Range*<u64>> impls
278
279impl From<Range<u64>> for HttpRange {
280    fn from(range: Range<u64>) -> Self {
281        Self::new(range.start, range.end - range.start)
282    }
283}
284
285impl From<RangeFrom<u64>> for HttpRange {
286    fn from(range: RangeFrom<u64>) -> Self {
287        Self::from_offset(range.start)
288    }
289}
290
291impl From<RangeInclusive<u64>> for HttpRange {
292    fn from(range: RangeInclusive<u64>) -> Self {
293        Self::new(*range.start(), range.end() - range.start() + 1)
294    }
295}
296
297impl From<RangeTo<u64>> for HttpRange {
298    fn from(range: RangeTo<u64>) -> Self {
299        Self::new(0, range.end)
300    }
301}
302
303impl From<RangeToInclusive<u64>> for HttpRange {
304    fn from(range: RangeToInclusive<u64>) -> Self {
305        Self::new(0, range.end + 1)
306    }
307}
308
309// From<Range*<usize>> impls
310
311impl From<Range<usize>> for HttpRange {
312    fn from(range: Range<usize>) -> Self {
313        Self::new(range.start as u64, (range.end - range.start) as u64)
314    }
315}
316
317impl From<RangeFrom<usize>> for HttpRange {
318    fn from(range: RangeFrom<usize>) -> Self {
319        Self::from_offset(range.start as u64)
320    }
321}
322
323impl From<RangeInclusive<usize>> for HttpRange {
324    fn from(range: RangeInclusive<usize>) -> Self {
325        Self::new(
326            *range.start() as u64,
327            (range.end() - range.start() + 1) as u64,
328        )
329    }
330}
331
332impl From<RangeTo<usize>> for HttpRange {
333    fn from(range: RangeTo<usize>) -> Self {
334        Self::new(0, range.end as u64)
335    }
336}
337
338impl From<RangeToInclusive<usize>> for HttpRange {
339    fn from(range: RangeToInclusive<usize>) -> Self {
340        Self::new(0, (range.end + 1) as u64)
341    }
342}
343
344#[cfg(test)]
345mod http_range_tests {
346    use super::*;
347
348    #[test]
349    fn new_creates_bounded_range() {
350        let range = HttpRange::new(0, 512);
351        assert_eq!(range.to_string(), "bytes=0-511");
352    }
353
354    #[test]
355    fn from_offset_creates_open_ended_range() {
356        let range = HttpRange::from_offset(255);
357        assert_eq!(range.to_string(), "bytes=255-");
358    }
359
360    #[test]
361    fn display_bounded_range() {
362        let range = HttpRange::new(0, 512);
363        assert_eq!(range.to_string(), "bytes=0-511");
364    }
365
366    #[test]
367    fn display_open_ended_range() {
368        let range = HttpRange::from_offset(255);
369        assert_eq!(range.to_string(), "bytes=255-");
370    }
371
372    #[test]
373    fn to_string_bounded_range() {
374        let range = HttpRange::new(100, 101);
375        assert_eq!(range.to_string(), "bytes=100-200");
376    }
377
378    #[test]
379    fn into_header_value() {
380        let range = HttpRange::new(0, 512);
381        let header_value: HeaderValue = range.into();
382        assert_eq!(header_value.as_str(), "bytes=0-511");
383    }
384
385    #[test]
386    fn display_zero_length_does_not_panic() {
387        // length == 0 would underflow without saturating arithmetic; must not panic
388        let range = HttpRange::new(0, 0);
389        // saturating_add(0).saturating_sub(1) on offset 0 saturates to 0
390        let _ = range.to_string();
391    }
392
393    #[test]
394    fn display_overflow_does_not_panic() {
395        // offset + length would overflow u64 without saturating arithmetic; must not panic
396        let range = HttpRange::new(u64::MAX, u64::MAX);
397        let _ = range.to_string();
398    }
399
400    // From<Range*<u64>> tests
401
402    #[test]
403    fn from_range_u64() {
404        let range: HttpRange = (0u64..100).into();
405        assert_eq!(range.to_string(), "bytes=0-99");
406    }
407
408    #[test]
409    fn from_range_from_u64() {
410        let range: HttpRange = (100u64..).into();
411        assert_eq!(range.to_string(), "bytes=100-");
412    }
413
414    #[test]
415    fn from_range_inclusive_u64() {
416        let range: HttpRange = (0u64..=99).into();
417        assert_eq!(range.to_string(), "bytes=0-99");
418    }
419
420    #[test]
421    fn from_range_to_u64() {
422        let range: HttpRange = (..100u64).into();
423        assert_eq!(range.to_string(), "bytes=0-99");
424    }
425
426    #[test]
427    fn from_range_to_inclusive_u64() {
428        let range: HttpRange = (..=99u64).into();
429        assert_eq!(range.to_string(), "bytes=0-99");
430    }
431
432    // From<Range*<usize>> tests
433
434    #[test]
435    fn from_range_usize() {
436        let range: HttpRange = (0usize..100).into();
437        assert_eq!(range.to_string(), "bytes=0-99");
438    }
439
440    #[test]
441    fn from_range_from_usize() {
442        let range: HttpRange = (100usize..).into();
443        assert_eq!(range.to_string(), "bytes=100-");
444    }
445
446    #[test]
447    fn from_range_inclusive_usize() {
448        let range: HttpRange = (0usize..=99).into();
449        assert_eq!(range.to_string(), "bytes=0-99");
450    }
451
452    #[test]
453    fn from_range_to_usize() {
454        let range: HttpRange = (..100usize).into();
455        assert_eq!(range.to_string(), "bytes=0-99");
456    }
457
458    #[test]
459    fn from_range_to_inclusive_usize() {
460        let range: HttpRange = (..=99usize).into();
461        assert_eq!(range.to_string(), "bytes=0-99");
462    }
463
464    #[test]
465    fn from_range_nonzero_offset() {
466        // Verify no off-by-one when start != 0
467        let exclusive: HttpRange = (50u64..150).into();
468        let inclusive: HttpRange = (50u64..=149).into();
469        assert_eq!(exclusive.to_string(), "bytes=50-149");
470        assert_eq!(inclusive.to_string(), "bytes=50-149");
471        assert_eq!(exclusive, inclusive);
472    }
473
474    #[test]
475    fn from_range_single_byte() {
476        // A 1-byte range must not produce an off-by-one
477        let exclusive: HttpRange = (42u64..43).into();
478        let inclusive: HttpRange = (42u64..=42).into();
479        assert_eq!(exclusive.to_string(), "bytes=42-42");
480        assert_eq!(inclusive.to_string(), "bytes=42-42");
481        assert_eq!(exclusive, inclusive);
482    }
483}