Skip to main content

notedthat_core/
range.rs

1//! RFC 7233 byte range parsing and conversion helpers.
2
3use std::ops::Range;
4
5/// RFC 7233 §2.1 byte range spec. All bounds are inclusive HTTP semantics.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum ByteRange {
8    /// `bytes=first-last` (both bounds present, inclusive)
9    FromStart {
10        /// The first byte position (inclusive).
11        first: u64,
12        /// The last byte position (inclusive).
13        last: u64,
14    },
15    /// `bytes=first-` (no upper bound; from first to end)
16    FromStartOpen {
17        /// The first byte position (inclusive).
18        first: u64,
19    },
20    /// `bytes=-length` (last N bytes)
21    Suffix {
22        /// The number of bytes from the end.
23        length: u64,
24    },
25}
26
27/// Line range spec. All bounds are 1-based inclusive line semantics.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum LineRange {
30    /// `lines=first-last` (both bounds present, inclusive)
31    FromStart {
32        /// The first line number (inclusive, 1-based).
33        first: u64,
34        /// The last line number (inclusive, 1-based).
35        last: u64,
36    },
37    /// `lines=first-` (no upper bound; from first to end)
38    FromStartOpen {
39        /// The first line number (inclusive, 1-based).
40        first: u64,
41    },
42    /// `lines=-length` (last N lines)
43    Suffix {
44        /// The number of lines from the end.
45        length: u64,
46    },
47    /// `lines=before-before-1` (zero-width insert at `before`)
48    Insert {
49        /// The line number to insert before (1-based).
50        before: u64,
51    },
52}
53
54/// Errors produced while parsing an RFC 7233 `Range:` header value.
55#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
56pub enum RangeParseError {
57    /// Header value is empty.
58    #[error("empty Range header value")]
59    Empty,
60    /// Header value is missing the `=` separator between unit and range set.
61    #[error("Range header missing '=' separator")]
62    MissingEquals,
63    /// Header value has no range specs after `=`.
64    #[error("Range header has no range specs after '='")]
65    NoRanges,
66    /// A single range spec is syntactically invalid.
67    #[error("invalid range spec: {0}")]
68    InvalidSpec(String),
69    /// A numeric range component does not fit in `u64`.
70    #[error("range spec overflows u64")]
71    OverflowU64,
72}
73
74impl ByteRange {
75    /// Return this range in single-range HTTP header form.
76    pub fn to_http_string(&self) -> String {
77        match self {
78            Self::FromStart { first, last } => format!("bytes={first}-{last}"),
79            Self::FromStartOpen { first } => format!("bytes={first}-"),
80            Self::Suffix { length } => format!("bytes=-{length}"),
81        }
82    }
83
84    /// Convert inclusive HTTP range semantics into a zero-based exclusive Rust range for `total_size`.
85    pub fn to_exclusive_range(&self, total_size: u64) -> Option<Range<u64>> {
86        match *self {
87            Self::FromStart { first, last } => {
88                if first > last || first >= total_size {
89                    return None;
90                }
91
92                let end_inclusive = last.min(total_size - 1);
93                Some(first..end_inclusive + 1)
94            }
95            Self::FromStartOpen { first } => {
96                if first >= total_size {
97                    None
98                } else {
99                    Some(first..total_size)
100                }
101            }
102            Self::Suffix { length } => {
103                if length == 0 || total_size == 0 {
104                    None
105                } else {
106                    Some(total_size.saturating_sub(length)..total_size)
107                }
108            }
109        }
110    }
111
112    /// Return whether this range can be satisfied for `total_size`.
113    pub fn is_satisfiable(&self, total_size: u64) -> bool {
114        self.to_exclusive_range(total_size).is_some()
115    }
116}
117
118impl LineRange {
119    /// Return this range in single-range HTTP header form.
120    pub fn to_http_string(&self) -> String {
121        match self {
122            Self::FromStart { first, last } => format!("lines={first}-{last}"),
123            Self::FromStartOpen { first } => format!("lines={first}-"),
124            Self::Suffix { length } => format!("lines=-{length}"),
125            Self::Insert { before } => format!("lines={before}-{}", before - 1),
126        }
127    }
128}
129
130/// Per-request line index built from raw bytes.
131/// `line_starts[i]` is the byte offset at which line `i+1` begins (0-indexed into the array).
132/// `line_starts[0] = 0` always.
133#[derive(Debug, Clone)]
134pub struct LineIndex {
135    /// Byte offset at which each line begins. Length equals `total_lines`.
136    pub line_starts: Vec<u64>,
137    /// Total number of lines (including trailing partial line without a `\n`).
138    pub total_lines: u64,
139    /// Total number of bytes in the buffer.
140    pub total_bytes: u64,
141}
142
143impl LineIndex {
144    /// Build a line index from raw bytes without decoding text.
145    pub fn from_bytes(bytes: &[u8]) -> Self {
146        if bytes.is_empty() {
147            return Self {
148                line_starts: Vec::new(),
149                total_lines: 0,
150                total_bytes: 0,
151            };
152        }
153
154        let mut line_starts = vec![0];
155        let mut i = 0;
156        while i < bytes.len() {
157            if bytes[i] == b'\r' && i + 1 < bytes.len() && bytes[i + 1] == b'\n' {
158                i += 2;
159                if i < bytes.len() {
160                    line_starts.push(i as u64);
161                }
162            } else if bytes[i] == b'\n' {
163                i += 1;
164                if i < bytes.len() {
165                    line_starts.push(i as u64);
166                }
167            } else {
168                i += 1;
169            }
170        }
171
172        Self {
173            total_lines: line_starts.len() as u64,
174            total_bytes: bytes.len() as u64,
175            line_starts,
176        }
177    }
178
179    /// Convert a line range into an exclusive byte range.
180    pub fn byte_range(&self, line_range: &LineRange) -> Option<Range<u64>> {
181        match *line_range {
182            LineRange::FromStart { first, last } => {
183                if first == 0 || first > self.total_lines || first > last.saturating_add(1) {
184                    return None;
185                }
186
187                let clamped_last = last.min(self.total_lines);
188                let start = self.line_start(first - 1)?;
189                let end = if clamped_last == self.total_lines {
190                    self.total_bytes
191                } else {
192                    self.line_start(clamped_last)?
193                };
194                Some(start..end)
195            }
196            LineRange::FromStartOpen { first } => {
197                if first == 0 || first > self.total_lines {
198                    return None;
199                }
200
201                let start = self.line_start(first - 1)?;
202                Some(start..self.total_bytes)
203            }
204            LineRange::Suffix { length } => {
205                if length == 0 || self.total_lines == 0 {
206                    return None;
207                }
208
209                let clamped = length.min(self.total_lines);
210                let start_line = self.total_lines - clamped;
211                let start = self.line_start(start_line)?;
212                Some(start..self.total_bytes)
213            }
214            LineRange::Insert { before } => {
215                if before == 0 || before > self.total_lines + 1 {
216                    return None;
217                }
218
219                if before == self.total_lines + 1 {
220                    return Some(self.total_bytes..self.total_bytes);
221                }
222
223                let offset = self.line_start(before - 1)?;
224                Some(offset..offset)
225            }
226        }
227    }
228
229    fn line_start(&self, zero_based_line: u64) -> Option<u64> {
230        let index = usize::try_from(zero_based_line).ok()?;
231        self.line_starts.get(index).copied()
232    }
233
234    /// Return the `Content-Range` header value for a line-mode response.
235    pub fn content_range_string(&self, line_range: &LineRange) -> String {
236        match *line_range {
237            LineRange::FromStart { first, last } => {
238                let clamped_last = last.min(self.total_lines);
239                format!("lines {first}-{clamped_last}/{}", self.total_lines)
240            }
241            LineRange::FromStartOpen { first } => {
242                format!("lines {first}-{}/{}", self.total_lines, self.total_lines)
243            }
244            LineRange::Suffix { length } => {
245                let actual_first = self
246                    .total_lines
247                    .saturating_sub(length.min(self.total_lines))
248                    + 1;
249                format!(
250                    "lines {actual_first}-{}/{}",
251                    self.total_lines, self.total_lines
252                )
253            }
254            LineRange::Insert { before } => {
255                format!("lines {before}-{}/{}", before - 1, self.total_lines)
256            }
257        }
258    }
259}
260
261/// Parsed `Range:` header. Unit is preserved so callers can ignore non-`bytes` units per RFC 7233 §2.1.
262#[derive(Debug, Clone, PartialEq, Eq)]
263pub struct ParsedRanges {
264    /// Range unit token, such as `bytes`.
265    pub unit: String,
266    /// Parsed byte ranges. Empty when `unit` is not `bytes`.
267    pub ranges: Vec<ByteRange>,
268}
269
270/// Parse an RFC 7233 §2.1 `Range:` header value.
271pub fn parse_range_header(value: &str) -> Result<ParsedRanges, RangeParseError> {
272    if value.is_empty() {
273        return Err(RangeParseError::Empty);
274    }
275
276    let (unit, range_set) = value
277        .split_once('=')
278        .ok_or(RangeParseError::MissingEquals)?;
279    if range_set.is_empty() {
280        return Err(RangeParseError::NoRanges);
281    }
282
283    let unit = unit.to_string();
284    if unit != "bytes" {
285        return Ok(ParsedRanges {
286            unit,
287            ranges: Vec::new(),
288        });
289    }
290
291    let ranges = range_set
292        .split(',')
293        .map(|spec| parse_byte_range_spec(spec.trim()))
294        .collect::<Result<Vec<_>, _>>()?;
295
296    Ok(ParsedRanges { unit, ranges })
297}
298
299/// Parse a single `lines=` `Range:` header value into a line range.
300///
301/// # Errors
302///
303/// Returns [`RangeParseError`] when the value is empty, misses `=`, has no
304/// range set, uses unsupported multi-range line syntax, or contains an invalid
305/// line range component.
306pub fn parse_line_range_header(value: &str) -> Result<LineRange, RangeParseError> {
307    if value.is_empty() {
308        return Err(RangeParseError::Empty);
309    }
310
311    let (_unit, range_set) = value
312        .split_once('=')
313        .ok_or(RangeParseError::MissingEquals)?;
314    if range_set.is_empty() {
315        return Err(RangeParseError::NoRanges);
316    }
317    if range_set.contains(',') {
318        return Err(RangeParseError::InvalidSpec(
319            "multi-range lines= not supported".into(),
320        ));
321    }
322
323    let (start, end) = range_set
324        .split_once('-')
325        .ok_or_else(|| RangeParseError::InvalidSpec(range_set.to_string()))?;
326
327    if start.is_empty() && end.is_empty() {
328        return Err(RangeParseError::InvalidSpec(range_set.to_string()));
329    }
330
331    if start.is_empty() {
332        let length = parse_u64(end, range_set)?;
333        return Ok(LineRange::Suffix { length });
334    }
335
336    let first = parse_u64(start, range_set)?;
337    if end.is_empty() {
338        return Ok(LineRange::FromStartOpen { first });
339    }
340
341    let last = parse_u64(end, range_set)?;
342    if first == 0 {
343        return Err(RangeParseError::InvalidSpec(range_set.to_string()));
344    }
345    if last == first - 1 {
346        Ok(LineRange::Insert { before: first })
347    } else {
348        Ok(LineRange::FromStart { first, last })
349    }
350}
351
352fn parse_byte_range_spec(spec: &str) -> Result<ByteRange, RangeParseError> {
353    let (start, end) = spec
354        .split_once('-')
355        .ok_or_else(|| RangeParseError::InvalidSpec(spec.to_string()))?;
356
357    if end.contains('-') || (start.is_empty() && end.is_empty()) {
358        return Err(RangeParseError::InvalidSpec(spec.to_string()));
359    }
360
361    if start.is_empty() {
362        let length = parse_u64(end, spec)?;
363        return Ok(ByteRange::Suffix { length });
364    }
365
366    let first = parse_u64(start, spec)?;
367    if end.is_empty() {
368        Ok(ByteRange::FromStartOpen { first })
369    } else {
370        let last = parse_u64(end, spec)?;
371        Ok(ByteRange::FromStart { first, last })
372    }
373}
374
375fn parse_u64(component: &str, spec: &str) -> Result<u64, RangeParseError> {
376    if component.is_empty() || !component.bytes().all(|byte| byte.is_ascii_digit()) {
377        return Err(RangeParseError::InvalidSpec(spec.to_string()));
378    }
379
380    component.parse().map_err(|_| RangeParseError::OverflowU64)
381}
382
383#[cfg(test)]
384mod tests {
385    use super::*;
386
387    mod line_range {
388        use super::*;
389
390        #[test]
391        fn constructs_each_variant() {
392            assert_eq!(
393                LineRange::FromStart { first: 1, last: 10 },
394                LineRange::FromStart { first: 1, last: 10 }
395            );
396            assert_eq!(
397                LineRange::FromStartOpen { first: 7 },
398                LineRange::FromStartOpen { first: 7 }
399            );
400            assert_eq!(
401                LineRange::Suffix { length: 3 },
402                LineRange::Suffix { length: 3 }
403            );
404            assert_eq!(
405                LineRange::Insert { before: 5 },
406                LineRange::Insert { before: 5 }
407            );
408        }
409
410        #[test]
411        fn from_start_to_http_string() {
412            assert_eq!(
413                LineRange::FromStart { first: 1, last: 10 }.to_http_string(),
414                "lines=1-10"
415            );
416        }
417
418        #[test]
419        fn insert_to_http_string() {
420            assert_eq!(
421                LineRange::Insert { before: 5 }.to_http_string(),
422                "lines=5-4"
423            );
424        }
425
426        #[test]
427        fn suffix_to_http_string() {
428            assert_eq!(LineRange::Suffix { length: 3 }.to_http_string(), "lines=-3");
429        }
430
431        #[test]
432        fn is_send_sync_clone() {
433            let _: fn() = || {
434                fn f<T: Send + Sync + Clone>() {}
435                f::<LineRange>();
436            };
437        }
438    }
439
440    mod line_index {
441        use super::*;
442
443        fn ten_line_index() -> LineIndex {
444            LineIndex::from_bytes(b"1\n2\n3\n4\n5\n6\n7\n8\n9\n10")
445        }
446
447        #[test]
448        fn empty_buffer_has_no_lines() {
449            let index = LineIndex::from_bytes(b"");
450
451            assert_eq!(index.total_lines, 0);
452            assert_eq!(index.line_starts, Vec::<u64>::new());
453            assert_eq!(index.total_bytes, 0);
454            assert_eq!(
455                index.byte_range(&LineRange::FromStart { first: 1, last: 1 }),
456                None
457            );
458        }
459
460        #[test]
461        fn trailing_newline_belongs_to_last_line() {
462            let index = LineIndex::from_bytes(b"one\n");
463
464            assert_eq!(index.total_lines, 1);
465            assert_eq!(index.line_starts, vec![0]);
466            assert_eq!(index.total_bytes, 4);
467            assert_eq!(
468                index.byte_range(&LineRange::FromStart { first: 1, last: 1 }),
469                Some(0..4)
470            );
471        }
472
473        #[test]
474        fn trailing_partial_line_is_counted() {
475            let index = LineIndex::from_bytes(b"one");
476
477            assert_eq!(index.total_lines, 1);
478            assert_eq!(index.line_starts, vec![0]);
479            assert_eq!(index.total_bytes, 3);
480            assert_eq!(
481                index.byte_range(&LineRange::FromStart { first: 1, last: 1 }),
482                Some(0..3)
483            );
484        }
485
486        #[test]
487        fn crlf_is_one_line_boundary() {
488            let index = LineIndex::from_bytes(b"a\r\nb\n");
489
490            assert_eq!(index.total_lines, 2);
491            assert_eq!(index.line_starts, vec![0, 3]);
492            assert_eq!(index.total_bytes, 5);
493        }
494
495        #[test]
496        fn from_start_clamps_past_eof_end() {
497            let index = ten_line_index();
498
499            assert_eq!(
500                index.byte_range(&LineRange::FromStart {
501                    first: 1,
502                    last: 999,
503                }),
504                Some(0..index.total_bytes)
505            );
506        }
507
508        #[test]
509        fn from_start_past_eof_is_unsatisfiable() {
510            let index = ten_line_index();
511
512            assert_eq!(
513                index.byte_range(&LineRange::FromStart {
514                    first: 999,
515                    last: 1000,
516                }),
517                None
518            );
519        }
520
521        #[test]
522        fn insert_before_existing_line_is_zero_width() {
523            let index = ten_line_index();
524            let offset = index.line_starts[4];
525
526            assert_eq!(
527                index.byte_range(&LineRange::Insert { before: 5 }),
528                Some(offset..offset)
529            );
530        }
531
532        #[test]
533        fn content_range_strings_render_line_bounds() {
534            let index = ten_line_index();
535
536            assert_eq!(
537                index.content_range_string(&LineRange::FromStart { first: 2, last: 4 }),
538                "lines 2-4/10"
539            );
540            assert_eq!(
541                index.content_range_string(&LineRange::Insert { before: 5 }),
542                "lines 5-4/10"
543            );
544        }
545
546        #[test]
547        fn empty_file_insert_allows_position_one_only() {
548            let index = LineIndex::from_bytes(b"");
549
550            assert_eq!(
551                index.byte_range(&LineRange::Insert { before: 1 }),
552                Some(0..0)
553            );
554            assert_eq!(index.byte_range(&LineRange::Insert { before: 2 }), None);
555        }
556
557        #[test]
558        fn lone_cr_and_bom_are_ordinary_bytes() {
559            let lone_cr = LineIndex::from_bytes(b"a\rb");
560            let bom = LineIndex::from_bytes(b"\xef\xbb\xbffoo\n");
561
562            assert_eq!(lone_cr.total_lines, 1);
563            assert_eq!(bom.total_lines, 1);
564            assert_eq!(bom.line_starts, vec![0]);
565        }
566    }
567
568    mod parse_line_range {
569        use super::*;
570
571        #[test]
572        fn closed_range_parses_as_from_start() {
573            assert_eq!(
574                parse_line_range_header("lines=1-10"),
575                Ok(LineRange::FromStart { first: 1, last: 10 })
576            );
577        }
578
579        #[test]
580        fn open_ended_range_parses_as_from_start_open() {
581            assert_eq!(
582                parse_line_range_header("lines=100-"),
583                Ok(LineRange::FromStartOpen { first: 100 })
584            );
585        }
586
587        #[test]
588        fn suffix_range_parses_as_suffix() {
589            assert_eq!(
590                parse_line_range_header("lines=-5"),
591                Ok(LineRange::Suffix { length: 5 })
592            );
593        }
594
595        #[test]
596        fn zero_width_range_parses_as_insert() {
597            assert_eq!(
598                parse_line_range_header("lines=5-4"),
599                Ok(LineRange::Insert { before: 5 })
600            );
601        }
602
603        #[test]
604        fn single_line_range_is_not_insert() {
605            assert_eq!(
606                parse_line_range_header("lines=1-1"),
607                Ok(LineRange::FromStart { first: 1, last: 1 })
608            );
609        }
610
611        #[test]
612        fn empty_range_set_is_no_ranges_error() {
613            assert_eq!(
614                parse_line_range_header("lines="),
615                Err(RangeParseError::NoRanges)
616            );
617        }
618
619        #[test]
620        fn zero_start_is_invalid_spec() {
621            assert_eq!(
622                parse_line_range_header("lines=0-10"),
623                Err(RangeParseError::InvalidSpec("0-10".into()))
624            );
625        }
626
627        #[test]
628        fn multi_range_is_invalid_spec() {
629            assert_eq!(
630                parse_line_range_header("lines=1-10,20-30"),
631                Err(RangeParseError::InvalidSpec(
632                    "multi-range lines= not supported".into()
633                ))
634            );
635        }
636    }
637
638    #[test]
639    fn parse_closed_range() {
640        assert_eq!(
641            parse_range_header("bytes=0-499"),
642            Ok(ParsedRanges {
643                unit: "bytes".into(),
644                ranges: vec![ByteRange::FromStart {
645                    first: 0,
646                    last: 499,
647                }],
648            })
649        );
650    }
651
652    #[test]
653    fn parse_open_ended_range() {
654        assert_eq!(
655            parse_range_header("bytes=500-"),
656            Ok(ParsedRanges {
657                unit: "bytes".into(),
658                ranges: vec![ByteRange::FromStartOpen { first: 500 }],
659            })
660        );
661    }
662
663    #[test]
664    fn parse_suffix_range() {
665        assert_eq!(
666            parse_range_header("bytes=-500"),
667            Ok(ParsedRanges {
668                unit: "bytes".into(),
669                ranges: vec![ByteRange::Suffix { length: 500 }],
670            })
671        );
672    }
673
674    #[test]
675    fn parse_single_byte_range() {
676        assert_eq!(
677            parse_range_header("bytes=0-0"),
678            Ok(ParsedRanges {
679                unit: "bytes".into(),
680                ranges: vec![ByteRange::FromStart { first: 0, last: 0 }],
681            })
682        );
683    }
684
685    #[test]
686    fn parse_multiple_ranges() {
687        let parsed = parse_range_header("bytes=0-499,1000-1499").unwrap();
688        assert_eq!(parsed.unit, "bytes");
689        assert_eq!(
690            parsed.ranges,
691            vec![
692                ByteRange::FromStart {
693                    first: 0,
694                    last: 499,
695                },
696                ByteRange::FromStart {
697                    first: 1000,
698                    last: 1499,
699                },
700            ]
701        );
702    }
703
704    #[test]
705    fn parse_multiple_ranges_with_space_after_comma() {
706        let parsed = parse_range_header("bytes=0-499, 1000-1499").unwrap();
707        assert_eq!(parsed.unit, "bytes");
708        assert_eq!(parsed.ranges.len(), 2);
709    }
710
711    #[test]
712    fn parse_start_greater_than_end() {
713        assert_eq!(
714            parse_range_header("bytes=100-50"),
715            Ok(ParsedRanges {
716                unit: "bytes".into(),
717                ranges: vec![ByteRange::FromStart {
718                    first: 100,
719                    last: 50,
720                }],
721            })
722        );
723    }
724
725    #[test]
726    fn parse_unknown_unit_as_empty_ranges() {
727        assert_eq!(
728            parse_range_header("items=0-10"),
729            Ok(ParsedRanges {
730                unit: "items".into(),
731                ranges: vec![],
732            })
733        );
734    }
735
736    #[test]
737    fn parse_empty_value_is_error() {
738        assert_eq!(parse_range_header(""), Err(RangeParseError::Empty));
739    }
740
741    #[test]
742    fn parse_missing_equals_is_error() {
743        assert_eq!(
744            parse_range_header("bytes"),
745            Err(RangeParseError::MissingEquals)
746        );
747    }
748
749    #[test]
750    fn parse_no_ranges_is_error() {
751        assert_eq!(parse_range_header("bytes="), Err(RangeParseError::NoRanges));
752    }
753
754    #[test]
755    fn parse_alpha_spec_is_error() {
756        assert_eq!(
757            parse_range_header("bytes=abc"),
758            Err(RangeParseError::InvalidSpec("abc".into()))
759        );
760    }
761
762    #[test]
763    fn parse_bare_dash_is_error() {
764        assert_eq!(
765            parse_range_header("bytes=-"),
766            Err(RangeParseError::InvalidSpec("-".into()))
767        );
768    }
769
770    #[test]
771    fn parse_double_dash_is_error() {
772        assert_eq!(
773            parse_range_header("bytes=0--5"),
774            Err(RangeParseError::InvalidSpec("0--5".into()))
775        );
776    }
777
778    #[test]
779    fn parse_overflow_is_error() {
780        assert_eq!(
781            parse_range_header("bytes=99999999999999999999999-"),
782            Err(RangeParseError::OverflowU64)
783        );
784    }
785
786    #[test]
787    fn closed_range_converts_to_exclusive() {
788        assert_eq!(
789            ByteRange::FromStart {
790                first: 0,
791                last: 499,
792            }
793            .to_exclusive_range(1000),
794            Some(0..500)
795        );
796    }
797
798    #[test]
799    fn closed_range_clamps_last_to_total_size() {
800        assert_eq!(
801            ByteRange::FromStart {
802                first: 0,
803                last: 999,
804            }
805            .to_exclusive_range(500),
806            Some(0..500)
807        );
808    }
809
810    #[test]
811    fn closed_range_start_past_total_is_unsatisfiable() {
812        assert_eq!(
813            ByteRange::FromStart {
814                first: 500,
815                last: 999,
816            }
817            .to_exclusive_range(100),
818            None
819        );
820    }
821
822    #[test]
823    fn open_range_converts_to_exclusive() {
824        assert_eq!(
825            ByteRange::FromStartOpen { first: 100 }.to_exclusive_range(500),
826            Some(100..500)
827        );
828    }
829
830    #[test]
831    fn open_range_start_past_total_is_unsatisfiable() {
832        assert_eq!(
833            ByteRange::FromStartOpen { first: 500 }.to_exclusive_range(100),
834            None
835        );
836    }
837
838    #[test]
839    fn suffix_range_converts_to_exclusive() {
840        assert_eq!(
841            ByteRange::Suffix { length: 100 }.to_exclusive_range(500),
842            Some(400..500)
843        );
844    }
845
846    #[test]
847    fn suffix_range_clamps_to_total_size() {
848        assert_eq!(
849            ByteRange::Suffix { length: 9999 }.to_exclusive_range(100),
850            Some(0..100)
851        );
852    }
853
854    #[test]
855    fn suffix_zero_is_unsatisfiable() {
856        assert_eq!(
857            ByteRange::Suffix { length: 0 }.to_exclusive_range(500),
858            None
859        );
860    }
861
862    #[test]
863    fn is_satisfiable_true_when_exclusive_range_exists() {
864        assert!(ByteRange::Suffix { length: 100 }.is_satisfiable(500));
865    }
866
867    #[test]
868    fn is_satisfiable_false_when_exclusive_range_is_none() {
869        assert!(!ByteRange::Suffix { length: 0 }.is_satisfiable(500));
870    }
871
872    #[test]
873    fn closed_range_start_greater_than_end_is_unsatisfiable() {
874        assert_eq!(
875            ByteRange::FromStart {
876                first: 100,
877                last: 50,
878            }
879            .to_exclusive_range(1000),
880            None
881        );
882    }
883
884    #[test]
885    fn closed_range_to_http_string() {
886        assert_eq!(
887            ByteRange::FromStart {
888                first: 0,
889                last: 499,
890            }
891            .to_http_string(),
892            "bytes=0-499"
893        );
894    }
895
896    #[test]
897    fn open_range_to_http_string() {
898        assert_eq!(
899            ByteRange::FromStartOpen { first: 500 }.to_http_string(),
900            "bytes=500-"
901        );
902    }
903
904    #[test]
905    fn suffix_range_to_http_string() {
906        assert_eq!(
907            ByteRange::Suffix { length: 500 }.to_http_string(),
908            "bytes=-500"
909        );
910    }
911}