Skip to main content

api_bones/
range.rs

1//! `Range` request and `Content-Range` response header types (RFC 7233).
2//!
3//! [`ByteRange`] models a single byte range specifier.
4//! [`RangeHeader`] models the `Range` request header, which may contain one or
5//! more byte ranges or a suffix range.
6//! [`ContentRange`] models the `Content-Range` response header, indicating
7//! which portion of a resource's representation is being returned.
8//!
9//! # Example
10//!
11//! ```rust
12//! use api_bones::range::{ByteRange, ContentRange, RangeHeader};
13//!
14//! // Parse a Range request header.
15//! let range: RangeHeader = "bytes=0-499".parse().unwrap();
16//! assert_eq!(range, RangeHeader::Bytes(vec![ByteRange::FromTo(0, 499)]));
17//!
18//! // Build a Content-Range response header.
19//! let cr = ContentRange::bytes(0, 499, Some(1234));
20//! assert_eq!(cr.to_string(), "bytes 0-499/1234");
21//!
22//! // Validate the range against the resource length.
23//! assert!(ByteRange::FromTo(0, 499).is_valid(1234));
24//! assert!(!ByteRange::FromTo(1234, 9999).is_valid(1234)); // first >= length
25//! ```
26
27#[cfg(all(not(feature = "std"), feature = "alloc"))]
28use alloc::{
29    string::{String, ToString},
30    vec::Vec,
31};
32use core::{fmt, str::FromStr};
33#[cfg(feature = "serde")]
34use serde::{Deserialize, Serialize};
35
36// ---------------------------------------------------------------------------
37// ByteRange
38// ---------------------------------------------------------------------------
39
40/// A single byte range specifier as used in the `Range` header (RFC 7233 §2.1).
41///
42/// - `FromTo(first, last)` — a closed byte range `first-last` (both inclusive).
43/// - `From(first)` — an open-ended range starting at `first`.
44/// - `Suffix(n)` — the last `n` bytes of the representation.
45#[derive(Debug, Clone, PartialEq, Eq)]
46#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
47#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
48#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
49pub enum ByteRange {
50    /// A closed byte range: `<first>-<last>` (both inclusive).
51    FromTo(u64, u64),
52    /// An open-ended byte range: `<first>-` (from `first` to end).
53    From(u64),
54    /// A suffix byte range: `-<n>` (last `n` bytes).
55    Suffix(u64),
56}
57
58impl ByteRange {
59    /// Validate this range against the total length of the resource.
60    ///
61    /// Returns `false` if:
62    /// - `FromTo(first, last)`: `first > last` or `first >= length`.
63    /// - `From(first)`: `first >= length`.
64    /// - `Suffix(n)`: `n == 0`.
65    ///
66    /// ```
67    /// use api_bones::range::ByteRange;
68    ///
69    /// assert!(ByteRange::FromTo(0, 499).is_valid(1000));
70    /// assert!(!ByteRange::FromTo(500, 200).is_valid(1000)); // first > last
71    /// assert!(!ByteRange::FromTo(1000, 1999).is_valid(1000)); // first >= length
72    /// assert!(ByteRange::Suffix(100).is_valid(1000));
73    /// assert!(!ByteRange::Suffix(0).is_valid(1000));
74    /// ```
75    #[must_use]
76    pub fn is_valid(&self, length: u64) -> bool {
77        match self {
78            Self::FromTo(first, last) => first <= last && *first < length,
79            Self::From(first) => *first < length,
80            Self::Suffix(n) => *n > 0,
81        }
82    }
83
84    /// Resolve this range to a `(first, last)` byte range against the given
85    /// resource `length`. Returns `None` if the range is unsatisfiable.
86    ///
87    /// ```
88    /// use api_bones::range::ByteRange;
89    ///
90    /// assert_eq!(ByteRange::FromTo(0, 99).resolve(500), Some((0, 99)));
91    /// assert_eq!(ByteRange::From(400).resolve(500), Some((400, 499)));
92    /// assert_eq!(ByteRange::Suffix(100).resolve(500), Some((400, 499)));
93    /// assert_eq!(ByteRange::Suffix(600).resolve(500), Some((0, 499)));
94    /// assert_eq!(ByteRange::FromTo(0, 99).resolve(0), None);
95    /// ```
96    #[must_use]
97    pub fn resolve(&self, length: u64) -> Option<(u64, u64)> {
98        if length == 0 {
99            return None;
100        }
101        match self {
102            Self::FromTo(first, last) => {
103                if first > last || *first >= length {
104                    None
105                } else {
106                    Some((*first, (*last).min(length - 1)))
107                }
108            }
109            Self::From(first) => {
110                if *first >= length {
111                    None
112                } else {
113                    Some((*first, length - 1))
114                }
115            }
116            Self::Suffix(n) => {
117                if *n == 0 {
118                    None
119                } else {
120                    let first = length.saturating_sub(*n);
121                    Some((first, length - 1))
122                }
123            }
124        }
125    }
126}
127
128impl fmt::Display for ByteRange {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        match self {
131            Self::FromTo(first, last) => write!(f, "{first}-{last}"),
132            Self::From(first) => write!(f, "{first}-"),
133            Self::Suffix(n) => write!(f, "-{n}"),
134        }
135    }
136}
137
138// ---------------------------------------------------------------------------
139// RangeHeader
140// ---------------------------------------------------------------------------
141
142/// The `Range` request header (RFC 7233 §3.1).
143///
144/// The only range unit currently defined by the spec is `bytes`.
145/// Other range units are represented by the `Other` variant.
146#[derive(Debug, Clone, PartialEq, Eq)]
147#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
148#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
149#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
150pub enum RangeHeader {
151    /// `bytes=<range-set>` — one or more byte ranges.
152    Bytes(Vec<ByteRange>),
153    /// An unrecognised range unit, preserved as-is.
154    Other(String),
155}
156
157impl fmt::Display for RangeHeader {
158    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159        match self {
160            Self::Bytes(ranges) => {
161                f.write_str("bytes=")?;
162                for (i, r) in ranges.iter().enumerate() {
163                    if i > 0 {
164                        f.write_str(", ")?;
165                    }
166                    fmt::Display::fmt(r, f)?;
167                }
168                Ok(())
169            }
170            Self::Other(s) => f.write_str(s),
171        }
172    }
173}
174
175// ---------------------------------------------------------------------------
176// Parse errors
177// ---------------------------------------------------------------------------
178
179/// Error returned when parsing a `Range` or `Content-Range` header fails.
180#[derive(Debug, Clone, PartialEq, Eq)]
181pub enum ParseRangeError {
182    /// The input was empty.
183    Empty,
184    /// The range-unit or format was not recognised.
185    Malformed,
186}
187
188impl fmt::Display for ParseRangeError {
189    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190        match self {
191            Self::Empty => f.write_str("range header is empty"),
192            Self::Malformed => f.write_str("range header is malformed"),
193        }
194    }
195}
196
197#[cfg(feature = "std")]
198impl std::error::Error for ParseRangeError {}
199
200// ---------------------------------------------------------------------------
201// FromStr for ByteRange
202// ---------------------------------------------------------------------------
203
204impl FromStr for ByteRange {
205    type Err = ParseRangeError;
206
207    fn from_str(s: &str) -> Result<Self, Self::Err> {
208        let s = s.trim();
209        if s.is_empty() {
210            return Err(ParseRangeError::Empty);
211        }
212        if let Some(n) = s.strip_prefix('-') {
213            // suffix range: -N
214            let n: u64 = n.parse().map_err(|_| ParseRangeError::Malformed)?;
215            return Ok(Self::Suffix(n));
216        }
217        if let Some(pos) = s.find('-') {
218            let first: u64 = s[..pos].parse().map_err(|_| ParseRangeError::Malformed)?;
219            let rest = &s[pos + 1..];
220            if rest.trim().is_empty() {
221                return Ok(Self::From(first));
222            }
223            let last: u64 = rest.parse().map_err(|_| ParseRangeError::Malformed)?;
224            return Ok(Self::FromTo(first, last));
225        }
226        Err(ParseRangeError::Malformed)
227    }
228}
229
230// ---------------------------------------------------------------------------
231// FromStr for RangeHeader
232// ---------------------------------------------------------------------------
233
234impl FromStr for RangeHeader {
235    type Err = ParseRangeError;
236
237    /// Parse a `Range` header value such as `bytes=0-499` or `bytes=0-99, 200-299`.
238    ///
239    /// ```
240    /// use api_bones::range::{ByteRange, RangeHeader};
241    ///
242    /// let h: RangeHeader = "bytes=0-499".parse().unwrap();
243    /// assert_eq!(h, RangeHeader::Bytes(vec![ByteRange::FromTo(0, 499)]));
244    ///
245    /// let h2: RangeHeader = "bytes=0-99, 200-299, -50".parse().unwrap();
246    /// assert_eq!(h2, RangeHeader::Bytes(vec![
247    ///     ByteRange::FromTo(0, 99),
248    ///     ByteRange::FromTo(200, 299),
249    ///     ByteRange::Suffix(50),
250    /// ]));
251    /// ```
252    fn from_str(s: &str) -> Result<Self, Self::Err> {
253        let s = s.trim();
254        if s.is_empty() {
255            return Err(ParseRangeError::Empty);
256        }
257
258        if let Some(rest) = s.strip_prefix("bytes=") {
259            let ranges: Result<Vec<ByteRange>, _> = rest
260                .split(',')
261                .filter(|p| !p.trim().is_empty())
262                .map(|p| p.trim().parse::<ByteRange>())
263                .collect();
264            let ranges = ranges?;
265            if ranges.is_empty() {
266                return Err(ParseRangeError::Malformed);
267            }
268            return Ok(Self::Bytes(ranges));
269        }
270
271        Ok(Self::Other(s.to_string()))
272    }
273}
274
275// ---------------------------------------------------------------------------
276// ContentRange
277// ---------------------------------------------------------------------------
278
279/// The `Content-Range` response header (RFC 7233 §4.2).
280///
281/// Indicates which portion of a resource's representation is included in the
282/// response body.
283///
284/// ```
285/// use api_bones::range::ContentRange;
286///
287/// let cr = ContentRange::bytes(0, 999, Some(5000));
288/// assert_eq!(cr.to_string(), "bytes 0-999/5000");
289///
290/// let cr_unknown = ContentRange::bytes_unknown_length(200, 299);
291/// assert_eq!(cr_unknown.to_string(), "bytes 200-299/*");
292///
293/// let cr_unsatisfied = ContentRange::unsatisfiable(5000);
294/// assert_eq!(cr_unsatisfied.to_string(), "bytes */5000");
295/// ```
296#[derive(Debug, Clone, PartialEq, Eq)]
297#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
298#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
299#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
300pub enum ContentRange {
301    /// A satisfiable byte range: `bytes <first>-<last>/<complete-length or *>`.
302    Bytes {
303        /// First byte position (inclusive).
304        first: u64,
305        /// Last byte position (inclusive).
306        last: u64,
307        /// Total length of the representation, or `None` if unknown (`*`).
308        complete_length: Option<u64>,
309    },
310    /// An unsatisfiable range: `bytes */<complete-length>`.
311    Unsatisfiable {
312        /// Total length of the representation.
313        complete_length: u64,
314    },
315}
316
317impl ContentRange {
318    /// Construct a satisfiable byte range with a known total length.
319    #[must_use]
320    pub fn bytes(first: u64, last: u64, complete_length: Option<u64>) -> Self {
321        Self::Bytes {
322            first,
323            last,
324            complete_length,
325        }
326    }
327
328    /// Construct a satisfiable byte range where the total length is unknown.
329    #[must_use]
330    pub fn bytes_unknown_length(first: u64, last: u64) -> Self {
331        Self::Bytes {
332            first,
333            last,
334            complete_length: None,
335        }
336    }
337
338    /// Construct an unsatisfiable response (`bytes */<complete-length>`).
339    ///
340    /// Use this when the `Range` header cannot be satisfied; pair it with a
341    /// `416 Range Not Satisfiable` status code.
342    #[must_use]
343    pub fn unsatisfiable(complete_length: u64) -> Self {
344        Self::Unsatisfiable { complete_length }
345    }
346}
347
348impl fmt::Display for ContentRange {
349    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
350        match self {
351            Self::Bytes {
352                first,
353                last,
354                complete_length,
355            } => {
356                write!(f, "bytes {first}-{last}/")?;
357                match complete_length {
358                    Some(len) => write!(f, "{len}"),
359                    None => f.write_str("*"),
360                }
361            }
362            Self::Unsatisfiable { complete_length } => {
363                write!(f, "bytes */{complete_length}")
364            }
365        }
366    }
367}
368
369impl FromStr for ContentRange {
370    type Err = ParseRangeError;
371
372    /// Parse a `Content-Range` header value.
373    ///
374    /// Accepts `bytes <first>-<last>/<length>`, `bytes <first>-<last>/*`,
375    /// and `bytes */<length>`.
376    ///
377    /// ```
378    /// use api_bones::range::ContentRange;
379    ///
380    /// let cr: ContentRange = "bytes 0-999/5000".parse().unwrap();
381    /// assert_eq!(cr, ContentRange::bytes(0, 999, Some(5000)));
382    ///
383    /// let cr: ContentRange = "bytes */5000".parse().unwrap();
384    /// assert_eq!(cr, ContentRange::unsatisfiable(5000));
385    /// ```
386    fn from_str(s: &str) -> Result<Self, Self::Err> {
387        let s = s.trim();
388        let rest = s.strip_prefix("bytes ").ok_or(ParseRangeError::Malformed)?;
389
390        if let Some(len_str) = rest.strip_prefix("*/") {
391            let complete_length: u64 = len_str.parse().map_err(|_| ParseRangeError::Malformed)?;
392            return Ok(Self::Unsatisfiable { complete_length });
393        }
394
395        let slash = rest.find('/').ok_or(ParseRangeError::Malformed)?;
396        let range_part = &rest[..slash];
397        let len_part = &rest[slash + 1..];
398
399        let dash = range_part.find('-').ok_or(ParseRangeError::Malformed)?;
400        let first: u64 = range_part[..dash]
401            .parse()
402            .map_err(|_| ParseRangeError::Malformed)?;
403        let last: u64 = range_part[dash + 1..]
404            .parse()
405            .map_err(|_| ParseRangeError::Malformed)?;
406
407        let complete_length = if len_part == "*" {
408            None
409        } else {
410            Some(
411                len_part
412                    .parse::<u64>()
413                    .map_err(|_| ParseRangeError::Malformed)?,
414            )
415        };
416
417        Ok(Self::Bytes {
418            first,
419            last,
420            complete_length,
421        })
422    }
423}
424
425// ---------------------------------------------------------------------------
426// Tests
427// ---------------------------------------------------------------------------
428
429#[cfg(test)]
430mod tests {
431    use super::*;
432
433    // -----------------------------------------------------------------------
434    // ByteRange
435    // -----------------------------------------------------------------------
436
437    #[test]
438    fn byte_range_from_to_display() {
439        assert_eq!(ByteRange::FromTo(0, 499).to_string(), "0-499");
440    }
441
442    #[test]
443    fn byte_range_from_display() {
444        assert_eq!(ByteRange::From(100).to_string(), "100-");
445    }
446
447    #[test]
448    fn byte_range_suffix_display() {
449        assert_eq!(ByteRange::Suffix(50).to_string(), "-50");
450    }
451
452    #[test]
453    fn byte_range_parse_from_to() {
454        let r: ByteRange = "0-499".parse().unwrap();
455        assert_eq!(r, ByteRange::FromTo(0, 499));
456    }
457
458    #[test]
459    fn byte_range_parse_from() {
460        let r: ByteRange = "100-".parse().unwrap();
461        assert_eq!(r, ByteRange::From(100));
462    }
463
464    #[test]
465    fn byte_range_parse_suffix() {
466        let r: ByteRange = "-50".parse().unwrap();
467        assert_eq!(r, ByteRange::Suffix(50));
468    }
469
470    #[test]
471    fn byte_range_roundtrip() {
472        let ranges = [
473            ByteRange::FromTo(0, 99),
474            ByteRange::From(500),
475            ByteRange::Suffix(200),
476        ];
477        for r in &ranges {
478            let s = r.to_string();
479            let parsed: ByteRange = s.parse().unwrap();
480            assert_eq!(&parsed, r);
481        }
482    }
483
484    #[test]
485    fn byte_range_is_valid() {
486        assert!(ByteRange::FromTo(0, 499).is_valid(1000));
487        assert!(!ByteRange::FromTo(500, 200).is_valid(1000));
488        assert!(!ByteRange::FromTo(1000, 1999).is_valid(1000));
489        assert!(ByteRange::From(0).is_valid(1));
490        assert!(!ByteRange::From(1000).is_valid(1000));
491        assert!(ByteRange::Suffix(1).is_valid(1));
492        assert!(!ByteRange::Suffix(0).is_valid(1000));
493    }
494
495    #[test]
496    fn byte_range_resolve_from_to() {
497        assert_eq!(ByteRange::FromTo(0, 99).resolve(500), Some((0, 99)));
498        // clamp last to length-1
499        assert_eq!(ByteRange::FromTo(0, 999).resolve(500), Some((0, 499)));
500        // unsatisfiable
501        assert_eq!(ByteRange::FromTo(500, 999).resolve(500), None);
502        assert_eq!(ByteRange::FromTo(0, 99).resolve(0), None);
503    }
504
505    #[test]
506    fn byte_range_resolve_from() {
507        assert_eq!(ByteRange::From(400).resolve(500), Some((400, 499)));
508        assert_eq!(ByteRange::From(500).resolve(500), None);
509    }
510
511    #[test]
512    fn byte_range_resolve_suffix() {
513        assert_eq!(ByteRange::Suffix(100).resolve(500), Some((400, 499)));
514        // larger than length → starts at 0
515        assert_eq!(ByteRange::Suffix(600).resolve(500), Some((0, 499)));
516        assert_eq!(ByteRange::Suffix(0).resolve(500), None);
517    }
518
519    // -----------------------------------------------------------------------
520    // RangeHeader
521    // -----------------------------------------------------------------------
522
523    #[test]
524    fn range_header_parse_single() {
525        let h: RangeHeader = "bytes=0-499".parse().unwrap();
526        assert_eq!(h, RangeHeader::Bytes(vec![ByteRange::FromTo(0, 499)]));
527    }
528
529    #[test]
530    fn range_header_parse_multi() {
531        let h: RangeHeader = "bytes=0-99, 200-299".parse().unwrap();
532        assert_eq!(
533            h,
534            RangeHeader::Bytes(vec![ByteRange::FromTo(0, 99), ByteRange::FromTo(200, 299)])
535        );
536    }
537
538    #[test]
539    fn range_header_parse_suffix() {
540        let h: RangeHeader = "bytes=-500".parse().unwrap();
541        assert_eq!(h, RangeHeader::Bytes(vec![ByteRange::Suffix(500)]));
542    }
543
544    #[test]
545    fn range_header_parse_open() {
546        let h: RangeHeader = "bytes=9500-".parse().unwrap();
547        assert_eq!(h, RangeHeader::Bytes(vec![ByteRange::From(9500)]));
548    }
549
550    #[test]
551    fn range_header_roundtrip() {
552        let h = RangeHeader::Bytes(vec![ByteRange::FromTo(0, 499), ByteRange::Suffix(50)]);
553        let s = h.to_string();
554        let parsed: RangeHeader = s.parse().unwrap();
555        assert_eq!(parsed, h);
556    }
557
558    #[test]
559    fn range_header_other() {
560        let h: RangeHeader = "items=0-9".parse().unwrap();
561        assert_eq!(h, RangeHeader::Other("items=0-9".to_string()));
562    }
563
564    #[test]
565    fn range_header_empty_errors() {
566        assert_eq!("".parse::<RangeHeader>(), Err(ParseRangeError::Empty));
567    }
568
569    // -----------------------------------------------------------------------
570    // ContentRange
571    // -----------------------------------------------------------------------
572
573    #[test]
574    fn content_range_bytes_display() {
575        let cr = ContentRange::bytes(0, 999, Some(5000));
576        assert_eq!(cr.to_string(), "bytes 0-999/5000");
577    }
578
579    #[test]
580    fn content_range_bytes_unknown_length_display() {
581        let cr = ContentRange::bytes_unknown_length(200, 299);
582        assert_eq!(cr.to_string(), "bytes 200-299/*");
583    }
584
585    #[test]
586    fn content_range_unsatisfiable_display() {
587        let cr = ContentRange::unsatisfiable(5000);
588        assert_eq!(cr.to_string(), "bytes */5000");
589    }
590
591    #[test]
592    fn content_range_parse_known_length() {
593        let cr: ContentRange = "bytes 0-999/5000".parse().unwrap();
594        assert_eq!(cr, ContentRange::bytes(0, 999, Some(5000)));
595    }
596
597    #[test]
598    fn content_range_parse_unknown_length() {
599        let cr: ContentRange = "bytes 0-999/*".parse().unwrap();
600        assert_eq!(cr, ContentRange::bytes_unknown_length(0, 999));
601    }
602
603    #[test]
604    fn content_range_parse_unsatisfiable() {
605        let cr: ContentRange = "bytes */5000".parse().unwrap();
606        assert_eq!(cr, ContentRange::unsatisfiable(5000));
607    }
608
609    #[test]
610    fn content_range_roundtrip() {
611        let cases = [
612            ContentRange::bytes(0, 499, Some(1000)),
613            ContentRange::bytes_unknown_length(100, 199),
614            ContentRange::unsatisfiable(9999),
615        ];
616        for cr in &cases {
617            let s = cr.to_string();
618            let parsed: ContentRange = s.parse().unwrap();
619            assert_eq!(&parsed, cr);
620        }
621    }
622
623    // -----------------------------------------------------------------------
624    // Coverage gap: ByteRange::from_str empty string path
625    // -----------------------------------------------------------------------
626
627    #[test]
628    fn byte_range_parse_empty_is_error() {
629        assert_eq!("".parse::<ByteRange>(), Err(ParseRangeError::Empty));
630    }
631
632    // Coverage gap: ParseRangeError Display
633    #[test]
634    fn parse_range_error_display() {
635        assert!(!ParseRangeError::Empty.to_string().is_empty());
636        assert!(!ParseRangeError::Malformed.to_string().is_empty());
637    }
638
639    // Coverage gap: ByteRange parse malformed (no dash, non-empty)
640    #[test]
641    fn byte_range_parse_malformed_no_dash() {
642        assert_eq!("abc".parse::<ByteRange>(), Err(ParseRangeError::Malformed));
643    }
644}