opendal_core/raw/http_util/
bytes_range.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use std::fmt::Debug;
19use std::fmt::Display;
20use std::fmt::Formatter;
21use std::ops::Bound;
22use std::ops::RangeBounds;
23use std::str::FromStr;
24
25use crate::*;
26
27/// BytesRange(offset, size) carries a range of content.
28///
29/// BytesRange implements `ToString` which can be used as `Range` HTTP header directly.
30///
31/// `<unit>` should always be `bytes`.
32///
33/// ```text
34/// Range: bytes=<range-start>-
35/// Range: bytes=<range-start>-<range-end>
36/// ```
37///
38/// # Notes
39///
40/// We don't support tailing read like `Range: bytes=-<range-end>`
41#[derive(Default, Debug, Clone, Copy, Eq, PartialEq)]
42pub struct BytesRange(
43    /// Offset of the range.
44    u64,
45    /// Size of the range.
46    Option<u64>,
47);
48
49impl BytesRange {
50    /// Create a new `BytesRange`
51    ///
52    /// It better to use `BytesRange::from(1024..2048)` to construct.
53    ///
54    /// # Note
55    ///
56    /// The behavior for `None` and `Some` of `size` is different.
57    ///
58    /// - size=None => `bytes=<offset>-`, read from `<offset>` until the end
59    /// - size=Some(1024) => `bytes=<offset>-<offset + 1024>`, read 1024 bytes starting from the `<offset>`
60    pub fn new(offset: u64, size: Option<u64>) -> Self {
61        BytesRange(offset, size)
62    }
63
64    /// Get offset of BytesRange.
65    pub fn offset(&self) -> u64 {
66        self.0
67    }
68
69    /// Get size of BytesRange.
70    pub fn size(&self) -> Option<u64> {
71        self.1
72    }
73
74    /// Advance the range by `n` bytes.
75    ///
76    /// # Panics
77    ///
78    /// Panic if advancing the offset would overflow or if input `n` is larger
79    /// than the size of the range.
80    pub fn advance(&mut self, n: u64) {
81        self.0 = self
82            .0
83            .checked_add(n)
84            .expect("BytesRange::advance overflow: offset + n exceeds u64::MAX");
85        self.1 = self.1.map(|size| {
86            size.checked_sub(n)
87                .expect("BytesRange::advance underflow: n exceeds range size")
88        });
89    }
90
91    /// Check if this range is full of this content.
92    ///
93    /// If this range is full, we don't need to specify it in http request.
94    pub fn is_full(&self) -> bool {
95        self.0 == 0 && self.1.is_none()
96    }
97
98    /// Convert bytes range into Range header.
99    pub fn to_header(&self) -> String {
100        format!("bytes={self}")
101    }
102
103    /// Convert bytes range into rust range.
104    pub fn to_range(&self) -> impl RangeBounds<u64> {
105        (
106            Bound::Included(self.0),
107            match self.1 {
108                Some(size) => Bound::Excluded(
109                    self.0
110                        .checked_add(size)
111                        .expect("BytesRange::to_range overflow: offset + size exceeds u64::MAX"),
112                ),
113                None => Bound::Unbounded,
114            },
115        )
116    }
117
118    /// Convert bytes range into rust range with usize.
119    pub fn to_range_as_usize(self) -> impl RangeBounds<usize> {
120        let offset: usize = self
121            .0
122            .try_into()
123            .expect("BytesRange::to_range_as_usize: offset exceeds usize::MAX");
124        (
125            Bound::Included(offset),
126            match self.1 {
127                Some(size) => {
128                    let end: usize = self
129                        .0
130                        .checked_add(size)
131                        .expect(
132                            "BytesRange::to_range_as_usize overflow: offset + size exceeds u64::MAX",
133                        )
134                        .try_into()
135                        .expect(
136                            "BytesRange::to_range_as_usize: offset + size exceeds usize::MAX",
137                        );
138                    Bound::Excluded(end)
139                }
140                None => Bound::Unbounded,
141            },
142        )
143    }
144}
145
146impl Display for BytesRange {
147    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
148        match self.1 {
149            None => write!(f, "{}-", self.0),
150            // A zero-size range can't be represented as a valid HTTP Range.
151            Some(0) => Err(std::fmt::Error),
152            Some(size) => write!(
153                f,
154                "{}-{}",
155                self.0,
156                self.0.checked_add(size - 1).ok_or(std::fmt::Error)?
157            ),
158        }
159    }
160}
161
162impl FromStr for BytesRange {
163    type Err = Error;
164
165    fn from_str(value: &str) -> Result<Self> {
166        let s = value.strip_prefix("bytes=").ok_or_else(|| {
167            Error::new(ErrorKind::Unexpected, "header range is invalid")
168                .with_operation("BytesRange::from_str")
169                .with_context("value", value)
170        })?;
171
172        if s.contains(',') {
173            return Err(Error::new(ErrorKind::Unexpected, "header range is invalid")
174                .with_operation("BytesRange::from_str")
175                .with_context("value", value));
176        }
177
178        let v = s.split('-').collect::<Vec<_>>();
179        if v.len() != 2 {
180            return Err(Error::new(ErrorKind::Unexpected, "header range is invalid")
181                .with_operation("BytesRange::from_str")
182                .with_context("value", value));
183        }
184
185        let parse_int_error = |e: std::num::ParseIntError| {
186            Error::new(ErrorKind::Unexpected, "header range is invalid")
187                .with_operation("BytesRange::from_str")
188                .with_context("value", value)
189                .set_source(e)
190        };
191
192        if v[1].is_empty() {
193            // <range-start>-
194            Ok(BytesRange::new(
195                v[0].parse().map_err(parse_int_error)?,
196                None,
197            ))
198        } else if v[0].is_empty() {
199            // -<suffix-length>
200            Err(Error::new(
201                ErrorKind::Unexpected,
202                "header range with tailing is not supported",
203            )
204            .with_operation("BytesRange::from_str")
205            .with_context("value", value))
206        } else {
207            // <range-start>-<range-end>
208            let start: u64 = v[0].parse().map_err(parse_int_error)?;
209            let end: u64 = v[1].parse().map_err(parse_int_error)?;
210            if end < start {
211                return Err(Error::new(
212                    ErrorKind::Unexpected,
213                    "header range is invalid: end is less than start",
214                )
215                .with_operation("BytesRange::from_str")
216                .with_context("value", value));
217            }
218            Ok(BytesRange::new(start, Some(end - start + 1)))
219        }
220    }
221}
222
223impl<T> From<T> for BytesRange
224where
225    T: RangeBounds<u64>,
226{
227    fn from(range: T) -> Self {
228        let offset = match range.start_bound().cloned() {
229            Bound::Included(n) => n,
230            Bound::Excluded(n) => n.saturating_add(1),
231            Bound::Unbounded => 0,
232        };
233        let size = match range.end_bound().cloned() {
234            Bound::Included(n) => Some(n.saturating_add(1).saturating_sub(offset)),
235            Bound::Excluded(n) => Some(n.saturating_sub(offset)),
236            Bound::Unbounded => None,
237        };
238
239        BytesRange(offset, size)
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    #[test]
248    fn test_bytes_range_display_zero_size() {
249        // Zero-size at offset 0
250        let range = BytesRange::new(0, Some(0));
251        assert!(std::fmt::write(&mut String::new(), format_args!("{}", range)).is_err());
252
253        // Zero-size at nonzero offset
254        let range = BytesRange::new(5, Some(0));
255        assert!(std::fmt::write(&mut String::new(), format_args!("{}", range)).is_err());
256    }
257
258    #[test]
259    fn test_bytes_range_to_string() {
260        let h = BytesRange::new(0, Some(1024));
261        assert_eq!(h.to_string(), "0-1023");
262
263        let h = BytesRange::new(1024, None);
264        assert_eq!(h.to_string(), "1024-");
265
266        let h = BytesRange::new(1024, Some(1024));
267        assert_eq!(h.to_string(), "1024-2047");
268    }
269
270    #[test]
271    fn test_bytes_range_to_header() {
272        let h = BytesRange::new(0, Some(1024));
273        assert_eq!(h.to_header(), "bytes=0-1023");
274
275        let h = BytesRange::new(1024, None);
276        assert_eq!(h.to_header(), "bytes=1024-");
277
278        let h = BytesRange::new(1024, Some(1024));
279        assert_eq!(h.to_header(), "bytes=1024-2047");
280    }
281
282    #[test]
283    fn test_bytes_range_from_range_bounds() {
284        assert_eq!(BytesRange::new(0, None), BytesRange::from(..));
285        assert_eq!(BytesRange::new(10, None), BytesRange::from(10..));
286        assert_eq!(BytesRange::new(0, Some(11)), BytesRange::from(..=10));
287        assert_eq!(BytesRange::new(0, Some(10)), BytesRange::from(..10));
288        assert_eq!(BytesRange::new(10, Some(10)), BytesRange::from(10..20));
289        assert_eq!(BytesRange::new(10, Some(11)), BytesRange::from(10..=20));
290    }
291
292    #[test]
293    fn test_bytes_range_from_str() -> Result<()> {
294        let cases = vec![
295            ("range-start", "bytes=123-", BytesRange::new(123, None)),
296            ("range", "bytes=123-124", BytesRange::new(123, Some(2))),
297            ("one byte", "bytes=0-0", BytesRange::new(0, Some(1))),
298            (
299                "lower case header",
300                "bytes=0-0",
301                BytesRange::new(0, Some(1)),
302            ),
303        ];
304
305        for (name, input, expected) in cases {
306            let actual = input.parse()?;
307
308            assert_eq!(expected, actual, "{name}")
309        }
310
311        Ok(())
312    }
313
314    #[test]
315    fn test_bytes_range_from_str_invalid_end_less_than_start() {
316        let cases = vec!["bytes=100-50", "bytes=10-9", "bytes=1-0"];
317
318        for input in cases {
319            let result: Result<BytesRange> = input.parse();
320            assert!(
321                result.is_err(),
322                "expected error for invalid range {input}, got {result:?}"
323            );
324        }
325    }
326
327    #[allow(clippy::reversed_empty_ranges)]
328    #[test]
329    fn test_bytes_range_from_range_bounds_underflow() {
330        // Invalid ranges where end < start should produce zero-size ranges
331        // rather than underflowing.
332        assert_eq!(BytesRange::new(100, Some(0)), BytesRange::from(100..50));
333        assert_eq!(BytesRange::new(10, Some(0)), BytesRange::from(10..=5));
334        assert_eq!(BytesRange::new(5, Some(0)), BytesRange::from(5..0));
335        assert_eq!(BytesRange::new(5, Some(0)), BytesRange::from(5..=0));
336    }
337
338    #[test]
339    fn test_bytes_range_from_range_bounds_u64_max() {
340        // Boundary cases near u64::MAX must not overflow.
341        assert_eq!(
342            BytesRange::new(0, Some(u64::MAX)),
343            BytesRange::from(..=u64::MAX)
344        );
345        assert_eq!(
346            BytesRange::new(0, Some(u64::MAX)),
347            BytesRange::from(..u64::MAX)
348        );
349        assert_eq!(
350            BytesRange::new(1, Some(u64::MAX.saturating_sub(1))),
351            BytesRange::from(1..=u64::MAX)
352        );
353        // Excluded start at u64::MAX must not overflow.
354        assert_eq!(
355            BytesRange::new(u64::MAX, None),
356            BytesRange::from((u64::MAX)..)
357        );
358    }
359
360    #[test]
361    fn test_bytes_range_display_overflow() {
362        // offset=u64::MAX, size=2 would overflow in Display (u64::MAX + 1)
363        let range = BytesRange::new(u64::MAX, Some(2));
364        assert!(std::fmt::write(&mut String::new(), format_args!("{}", range)).is_err());
365    }
366
367    #[test]
368    #[should_panic(expected = "BytesRange::to_range overflow")]
369    fn test_bytes_range_to_range_overflow() {
370        let range = BytesRange::new(u64::MAX, Some(1));
371        let _ = range.to_range();
372    }
373
374    #[test]
375    #[should_panic(expected = "BytesRange::to_range_as_usize")]
376    fn test_bytes_range_to_range_as_usize_overflow() {
377        let range = BytesRange::new(u64::MAX, Some(1));
378        let _ = range.to_range_as_usize();
379    }
380
381    #[test]
382    #[should_panic(expected = "BytesRange::advance overflow")]
383    fn test_bytes_range_advance_offset_overflow() {
384        let mut range = BytesRange::new(u64::MAX, None);
385        range.advance(1);
386    }
387
388    #[test]
389    #[should_panic(expected = "BytesRange::advance underflow")]
390    fn test_bytes_range_advance_size_underflow() {
391        let mut range = BytesRange::new(0, Some(1));
392        range.advance(2);
393    }
394}