1use std::ops::Range;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum ByteRange {
8 FromStart {
10 first: u64,
12 last: u64,
14 },
15 FromStartOpen {
17 first: u64,
19 },
20 Suffix {
22 length: u64,
24 },
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum LineRange {
30 FromStart {
32 first: u64,
34 last: u64,
36 },
37 FromStartOpen {
39 first: u64,
41 },
42 Suffix {
44 length: u64,
46 },
47 Insert {
49 before: u64,
51 },
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
56pub enum RangeParseError {
57 #[error("empty Range header value")]
59 Empty,
60 #[error("Range header missing '=' separator")]
62 MissingEquals,
63 #[error("Range header has no range specs after '='")]
65 NoRanges,
66 #[error("invalid range spec: {0}")]
68 InvalidSpec(String),
69 #[error("range spec overflows u64")]
71 OverflowU64,
72}
73
74impl ByteRange {
75 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 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 pub fn is_satisfiable(&self, total_size: u64) -> bool {
114 self.to_exclusive_range(total_size).is_some()
115 }
116}
117
118impl LineRange {
119 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#[derive(Debug, Clone)]
134pub struct LineIndex {
135 pub line_starts: Vec<u64>,
137 pub total_lines: u64,
139 pub total_bytes: u64,
141}
142
143impl LineIndex {
144 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 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 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#[derive(Debug, Clone, PartialEq, Eq)]
263pub struct ParsedRanges {
264 pub unit: String,
266 pub ranges: Vec<ByteRange>,
268}
269
270pub 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
299pub 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}