1use core::{
5 fmt::{self, Debug},
6 hash::Hash,
7};
8
9use crate::std::borrow::Cow;
10
11use super::component_input::IntoUriComponent;
12use crate::uri::{
13 PathCaptures, PathPattern,
14 encode::{
15 encoded_path, encoded_segment, encoded_segment_cmp, encoded_segment_eq,
16 extend_encoded_path, hash_encoded_segment, write_encoded_path, write_encoded_segment,
17 },
18};
19
20use rama_core::bytes::BytesMut;
21
22use itertools::Itertools;
23use percent_encoding::percent_decode;
24
25#[derive(Debug, Default, Clone, Copy)]
33pub struct PathRef<'a> {
34 pub(crate) bytes: &'a [u8],
35}
36
37impl<'a> PathRef<'a> {
38 #[must_use]
39 #[inline]
40 pub(crate) const fn new(bytes: &'a [u8]) -> Self {
41 Self { bytes }
42 }
43
44 #[must_use]
48 #[inline]
49 pub fn from_raw_str(path: &'a str) -> Self {
50 Self::new(path.as_bytes())
51 }
52
53 #[must_use]
55 #[inline(always)]
56 pub fn as_encoded_str(self) -> Cow<'a, str> {
57 encoded_path(self.bytes)
58 }
59
60 pub(super) fn write_encoded_to(self, buf: &mut BytesMut) {
61 extend_encoded_path(buf, self.bytes);
62 }
63
64 #[must_use]
66 #[inline(always)]
67 pub fn is_empty(self) -> bool {
68 self.bytes.is_empty()
69 }
70
71 #[must_use]
73 #[inline]
74 pub fn trimmed_slashes(self) -> Self {
75 Self::new(trim_ascii_slashes(self.bytes))
76 }
77
78 #[must_use]
87 pub fn segment_range(self, start: usize, count: usize) -> Option<Self> {
88 let (start, end) = segment_range_bounds(self.bytes, start, count)?;
89 Some(Self::new(&self.bytes[start..end]))
90 }
91
92 #[must_use]
106 pub fn segments(self) -> PathSegments<'a> {
107 if self.bytes.is_empty() {
108 return PathSegments::empty();
109 }
110 let remaining = self.bytes.strip_prefix(b"/").unwrap_or(self.bytes);
114 PathSegments {
115 remaining,
116 exhausted: false,
117 }
118 }
119
120 #[must_use]
125 pub fn has_prefix(self, prefix: impl IntoUriComponent) -> bool {
126 self.has_prefix_with_opts(prefix, PathMatchOptions::default())
127 }
128
129 #[must_use]
131 #[expect(
132 clippy::needless_pass_by_value,
133 reason = "by-value matches IntoUriComponent's signature on sibling setters; this impl only borrows the input"
134 )]
135 pub fn has_prefix_with_opts(
136 self,
137 prefix: impl IntoUriComponent,
138 opts: PathMatchOptions,
139 ) -> bool {
140 let prefix = prefix.as_uri_component_bytes();
141 match_prefix_in_body(strip_leading_slash(self.bytes), &prefix, opts).is_some()
142 }
143
144 #[must_use]
149 pub fn has_suffix(self, suffix: impl IntoUriComponent) -> bool {
150 self.has_suffix_with_opts(suffix, PathMatchOptions::default())
151 }
152
153 #[must_use]
155 #[expect(
156 clippy::needless_pass_by_value,
157 reason = "by-value matches IntoUriComponent's signature on sibling setters; this impl only borrows the input"
158 )]
159 pub fn has_suffix_with_opts(
160 self,
161 suffix: impl IntoUriComponent,
162 opts: PathMatchOptions,
163 ) -> bool {
164 let suffix = suffix.as_uri_component_bytes();
165 match_suffix_in_body(strip_leading_slash(self.bytes), &suffix, opts).is_some()
166 }
167
168 #[must_use]
171 pub fn nth_segment(self, n: usize) -> Option<PathSegment<'a>> {
172 self.segments().nth(n)
173 }
174
175 #[must_use]
177 pub fn first_segment(self) -> Option<PathSegment<'a>> {
178 self.segments().next()
179 }
180
181 #[must_use]
184 pub fn last_segment(self) -> Option<PathSegment<'a>> {
185 self.segments().last()
186 }
187
188 #[must_use]
190 pub fn segment_count(self) -> usize {
191 self.segments().len()
192 }
193
194 #[must_use]
199 pub fn contains_segments(self, needle: impl IntoUriComponent) -> bool {
200 self.contains_segments_with_opts(needle, PathMatchOptions::default())
201 }
202
203 #[must_use]
206 #[expect(
207 clippy::needless_pass_by_value,
208 reason = "by-value matches IntoUriComponent's signature on sibling matchers; this impl only borrows the input"
209 )]
210 pub fn contains_segments_with_opts(
211 self,
212 needle: impl IntoUriComponent,
213 opts: PathMatchOptions,
214 ) -> bool {
215 let needle = needle.as_uri_component_bytes();
216 if trim_ascii_slashes(&needle).is_empty() {
217 return true;
218 }
219 let body = strip_leading_slash(self.bytes);
222 let mut start = 0;
223 loop {
224 if match_prefix_in_body(&body[start..], &needle, opts).is_some() {
225 return true;
226 }
227 match memchr::memchr(b'/', &body[start..]) {
228 Some(i) => start += i + 1,
229 None => return false,
230 }
231 }
232 }
233
234 #[must_use]
238 #[inline(always)]
239 pub fn is_pattern_match(self, pattern: &PathPattern) -> bool {
240 pattern.is_match(self)
241 }
242
243 #[must_use]
249 #[inline(always)]
250 pub fn pattern_captures(self, pattern: &PathPattern) -> Option<PathCaptures<'_, 'a>> {
251 pattern.captures(self)
252 }
253}
254
255impl<'a> From<&'a str> for PathRef<'a> {
256 #[inline]
259 fn from(path: &'a str) -> Self {
260 Self::from_raw_str(path)
261 }
262}
263
264impl PartialEq for PathRef<'_> {
265 fn eq(&self, other: &Self) -> bool {
266 self.segments()
267 .zip_longest(other.segments())
268 .all(|segment_pair| {
269 let (segment_a, segment_b) = segment_pair.left_and_right();
270 segment_a == segment_b
271 })
272 }
273}
274
275impl Eq for PathRef<'_> {}
276
277impl Ord for PathRef<'_> {
278 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
279 for segment_pair in self.segments().zip_longest(other.segments()) {
280 match segment_pair.left_and_right() {
281 (None, None) => (),
282 (None, Some(_)) => return core::cmp::Ordering::Less,
283 (Some(_), None) => return core::cmp::Ordering::Greater,
284 (Some(segment_a), Some(segment_b)) => {
285 let ordering = segment_a.cmp(&segment_b);
286 if ordering != core::cmp::Ordering::Equal {
287 return ordering;
288 }
289 }
290 }
291 }
292 core::cmp::Ordering::Equal
293 }
294}
295
296impl PartialOrd for PathRef<'_> {
297 #[inline(always)]
298 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
299 Some(self.cmp(other))
300 }
301}
302
303impl PartialEq<str> for PathRef<'_> {
304 #[inline(always)]
305 fn eq(&self, other: &str) -> bool {
306 self.eq(&PathRef::from_raw_str(other))
307 }
308}
309
310impl PartialEq<&str> for PathRef<'_> {
311 #[inline(always)]
312 fn eq(&self, other: &&str) -> bool {
313 self.eq(&PathRef::from_raw_str(other))
314 }
315}
316
317impl<'a> PartialEq<PathRef<'a>> for str {
318 #[inline(always)]
319 fn eq(&self, other: &PathRef<'a>) -> bool {
320 PathRef::from_raw_str(self).eq(other)
321 }
322}
323
324impl<'a> PartialEq<PathRef<'a>> for &str {
325 #[inline(always)]
326 fn eq(&self, other: &PathRef<'a>) -> bool {
327 PathRef::from_raw_str(self).eq(other)
328 }
329}
330
331impl Hash for PathRef<'_> {
332 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
333 let mut separator = "";
334 for segment in self.segments() {
335 separator.hash(state);
336 separator = "/";
337 segment.hash(state);
338 }
339 }
340}
341
342impl core::fmt::Display for PathRef<'_> {
343 #[inline(always)]
345 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
346 write_encoded_path(f, self.bytes)
347 }
348}
349
350#[derive(Debug, Clone, Copy)]
356pub struct PathSegment<'a> {
357 raw: &'a [u8],
358}
359
360impl PartialEq for PathSegment<'_> {
361 #[inline(always)]
362 fn eq(&self, other: &Self) -> bool {
363 encoded_segment_eq(self.raw, other.raw)
364 }
365}
366
367impl Eq for PathSegment<'_> {}
368
369impl PartialEq<str> for PathSegment<'_> {
370 #[inline(always)]
371 fn eq(&self, other: &str) -> bool {
372 self.eq(&PathSegment::new(other.as_bytes()))
373 }
374}
375
376impl PartialEq<&str> for PathSegment<'_> {
377 #[inline(always)]
378 fn eq(&self, other: &&str) -> bool {
379 self.eq(&PathSegment::new(other.as_bytes()))
380 }
381}
382
383impl<'a> PartialEq<PathSegment<'a>> for str {
384 #[inline(always)]
385 fn eq(&self, other: &PathSegment<'a>) -> bool {
386 PathSegment::new(self.as_bytes()).eq(other)
387 }
388}
389
390impl<'a> PartialEq<PathSegment<'a>> for &str {
391 #[inline(always)]
392 fn eq(&self, other: &PathSegment<'a>) -> bool {
393 PathSegment::new(self.as_bytes()).eq(other)
394 }
395}
396
397impl PartialOrd for PathSegment<'_> {
398 #[inline(always)]
399 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
400 Some(self.cmp(other))
401 }
402}
403
404impl Ord for PathSegment<'_> {
405 #[inline(always)]
406 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
407 encoded_segment_cmp(self.raw, other.raw)
408 }
409}
410
411impl Hash for PathSegment<'_> {
412 #[inline(always)]
413 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
414 hash_encoded_segment(state, self.raw);
415 }
416}
417
418impl fmt::Display for PathSegment<'_> {
419 #[inline(always)]
420 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
421 write_encoded_segment(f, self.raw)
422 }
423}
424
425impl<'a> PathSegment<'a> {
426 #[must_use]
427 #[inline]
428 pub(crate) const fn new(raw: &'a [u8]) -> Self {
429 Self { raw }
430 }
431
432 #[inline]
433 pub(super) fn write_encoded_to(self, buf: &mut BytesMut) {
434 super::encode::extend_encoded_segment_bytes(buf, self.raw);
435 }
436
437 #[inline]
438 pub(super) fn encoded_capacity_hint(self) -> usize {
439 self.raw.len()
440 }
441
442 #[must_use]
446 #[inline(always)]
447 pub fn as_encoded_str(self) -> Cow<'a, str> {
448 encoded_segment(self.raw)
449 }
450
451 #[must_use]
458 pub fn as_decoded_str(self) -> Cow<'a, str> {
459 percent_decode(self.raw).decode_utf8_lossy()
460 }
461
462 #[must_use]
465 pub fn is_empty(self) -> bool {
466 self.raw.is_empty()
467 }
468
469 #[must_use]
474 pub fn matches(self, other: impl IntoUriComponent) -> bool {
475 self.matches_with_opts(other, PathMatchOptions::default())
476 }
477
478 #[must_use]
481 #[expect(
482 clippy::needless_pass_by_value,
483 reason = "by-value matches IntoUriComponent's signature on sibling matchers; this impl only borrows the input"
484 )]
485 pub fn matches_with_opts(self, other: impl IntoUriComponent, opts: PathMatchOptions) -> bool {
486 segment_eq(self.raw, &other.as_uri_component_bytes(), opts)
487 }
488
489 #[must_use]
492 pub fn has_prefix(self, prefix: impl IntoUriComponent) -> bool {
493 self.has_prefix_with_opts(prefix, PathMatchOptions::default())
494 }
495
496 #[must_use]
499 #[expect(
500 clippy::needless_pass_by_value,
501 reason = "by-value matches IntoUriComponent's signature on sibling matchers; this impl only borrows the input"
502 )]
503 pub fn has_prefix_with_opts(
504 self,
505 prefix: impl IntoUriComponent,
506 opts: PathMatchOptions,
507 ) -> bool {
508 let pat = prefix.as_uri_component_bytes();
509 let seg = maybe_decode(self.raw, opts.percent_decode);
510 let pat = maybe_decode(&pat, opts.percent_decode);
511 byte_starts_with(&seg, &pat, opts.ignore_ascii_case)
512 }
513
514 #[must_use]
518 pub fn has_suffix(self, suffix: impl IntoUriComponent) -> bool {
519 self.has_suffix_with_opts(suffix, PathMatchOptions::default())
520 }
521
522 #[must_use]
525 #[expect(
526 clippy::needless_pass_by_value,
527 reason = "by-value matches IntoUriComponent's signature on sibling matchers; this impl only borrows the input"
528 )]
529 pub fn has_suffix_with_opts(
530 self,
531 suffix: impl IntoUriComponent,
532 opts: PathMatchOptions,
533 ) -> bool {
534 let pat = suffix.as_uri_component_bytes();
535 let seg = maybe_decode(self.raw, opts.percent_decode);
536 let pat = maybe_decode(&pat, opts.percent_decode);
537 byte_ends_with(&seg, &pat, opts.ignore_ascii_case)
538 }
539}
540
541#[derive(Debug, Clone)]
544pub struct PathSegments<'a> {
545 remaining: &'a [u8],
548 exhausted: bool,
550}
551
552impl<'a> PathSegments<'a> {
553 fn empty() -> Self {
555 Self {
556 remaining: &[],
557 exhausted: true,
558 }
559 }
560}
561
562impl<'a> Iterator for PathSegments<'a> {
563 type Item = PathSegment<'a>;
564
565 fn next(&mut self) -> Option<Self::Item> {
566 if self.exhausted {
567 return None;
568 }
569 if let Some(i) = memchr::memchr(b'/', self.remaining) {
570 let seg = &self.remaining[..i];
571 self.remaining = &self.remaining[i + 1..];
572 Some(PathSegment::new(seg))
573 } else {
574 let seg = self.remaining;
576 self.remaining = &[];
577 self.exhausted = true;
578 Some(PathSegment::new(seg))
579 }
580 }
581
582 fn size_hint(&self) -> (usize, Option<usize>) {
583 let n = if self.exhausted {
586 0
587 } else {
588 self.remaining.iter().filter(|&&b| b == b'/').count() + 1
589 };
590 (n, Some(n))
591 }
592}
593
594impl core::iter::FusedIterator for PathSegments<'_> {}
595
596impl ExactSizeIterator for PathSegments<'_> {}
597
598#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
605pub struct PathMatchOptions {
606 pub partial: bool,
611 pub ignore_ascii_case: bool,
613 pub percent_decode: bool,
616}
617
618impl Default for PathMatchOptions {
619 fn default() -> Self {
620 Self {
621 partial: false,
622 ignore_ascii_case: false,
623 percent_decode: true,
624 }
625 }
626}
627
628#[inline]
630pub(super) fn strip_leading_slash(path: &[u8]) -> &[u8] {
631 path.strip_prefix(b"/").unwrap_or(path)
632}
633
634pub(super) fn trim_ascii_slashes(mut bytes: &[u8]) -> &[u8] {
636 while let Some(rest) = bytes.strip_prefix(b"/") {
637 bytes = rest;
638 }
639 while let Some(rest) = bytes.strip_suffix(b"/") {
640 bytes = rest;
641 }
642 bytes
643}
644
645pub(super) fn segment_range_bounds(
646 path: &[u8],
647 start: usize,
648 count: usize,
649) -> Option<(usize, usize)> {
650 if path.is_empty() || count == 0 {
651 return None;
652 }
653
654 let has_leading_slash = path.first().copied() == Some(b'/');
655 let body_offset = usize::from(has_leading_slash);
656 let body = &path[body_offset..];
657
658 let first = segment_body_bounds(body, start)?;
659 let last = segment_body_bounds(body, start.checked_add(count - 1)?)?;
660
661 let abs_start = if has_leading_slash {
662 if start == 0 {
663 0
664 } else {
665 body_offset + first.0 - 1
666 }
667 } else if start == 0 {
668 0
669 } else {
670 first.0 - 1
671 };
672
673 let abs_end = if has_leading_slash && body.is_empty() && start == 0 {
674 1
675 } else {
676 body_offset + last.1
677 };
678
679 Some((abs_start, abs_end))
680}
681
682fn segment_body_bounds(body: &[u8], target: usize) -> Option<(usize, usize)> {
683 let mut index = 0;
684 let mut start = 0;
685 loop {
686 let end = body[start..]
687 .iter()
688 .position(|&b| b == b'/')
689 .map_or(body.len(), |pos| start + pos);
690 if index == target {
691 return Some((start, end));
692 }
693 if end == body.len() {
694 return None;
695 }
696 start = end + 1;
697 index += 1;
698 }
699}
700
701#[inline]
702pub(super) fn maybe_decode(bytes: &[u8], decode: bool) -> Cow<'_, [u8]> {
703 if decode {
704 percent_decode(bytes).into()
705 } else {
706 Cow::Borrowed(bytes)
707 }
708}
709
710#[inline]
711pub(super) fn byte_starts_with(hay: &[u8], needle: &[u8], ignore_case: bool) -> bool {
712 if ignore_case {
713 hay.len() >= needle.len() && hay[..needle.len()].eq_ignore_ascii_case(needle)
714 } else {
715 hay.starts_with(needle)
716 }
717}
718
719#[inline]
720pub(super) fn byte_ends_with(hay: &[u8], needle: &[u8], ignore_case: bool) -> bool {
721 if ignore_case {
722 hay.len() >= needle.len() && hay[hay.len() - needle.len()..].eq_ignore_ascii_case(needle)
723 } else {
724 hay.ends_with(needle)
725 }
726}
727
728pub(super) fn segment_eq(seg: &[u8], pat: &[u8], opts: PathMatchOptions) -> bool {
730 if opts.percent_decode {
731 let seg: crate::std::borrow::Cow<'_, [u8]> = percent_decode(seg).into();
735 let pat: crate::std::borrow::Cow<'_, [u8]> = percent_decode(pat).into();
736 if opts.ignore_ascii_case {
737 seg.eq_ignore_ascii_case(&pat)
738 } else {
739 seg == pat
740 }
741 } else if opts.ignore_ascii_case {
742 seg.eq_ignore_ascii_case(pat)
743 } else {
744 seg == pat
745 }
746}
747
748pub(super) fn match_prefix_in_body(
753 body: &[u8],
754 pattern_raw: &[u8],
755 opts: PathMatchOptions,
756) -> Option<usize> {
757 let pat = trim_ascii_slashes(pattern_raw);
758 if pat.is_empty() {
759 return Some(0);
760 }
761
762 if opts.partial {
763 let matches = if opts.ignore_ascii_case {
764 body.len() >= pat.len() && body[..pat.len()].eq_ignore_ascii_case(pat)
765 } else {
766 body.starts_with(pat)
767 };
768 return matches.then_some(pat.len());
769 }
770
771 let mut bi = 0;
772 let mut pi = 0;
773 loop {
774 let bend = body[bi..]
775 .iter()
776 .position(|&c| c == b'/')
777 .map_or(body.len(), |p| bi + p);
778 let pend = pat[pi..]
779 .iter()
780 .position(|&c| c == b'/')
781 .map_or(pat.len(), |p| pi + p);
782 if !segment_eq(&body[bi..bend], &pat[pi..pend], opts) {
783 return None;
784 }
785 if pend == pat.len() {
786 return Some(bend);
787 }
788 if bend >= body.len() {
790 return None;
791 }
792 bi = bend + 1;
793 pi = pend + 1;
794 }
795}
796
797pub(super) fn match_suffix_in_body(
802 body: &[u8],
803 pattern_raw: &[u8],
804 opts: PathMatchOptions,
805) -> Option<usize> {
806 let pat = trim_ascii_slashes(pattern_raw);
807 if pat.is_empty() {
808 return Some(body.len());
809 }
810
811 if opts.partial {
812 let matches = if opts.ignore_ascii_case {
813 body.len() >= pat.len() && body[body.len() - pat.len()..].eq_ignore_ascii_case(pat)
814 } else {
815 body.ends_with(pat)
816 };
817 return matches.then(|| body.len() - pat.len());
818 }
819
820 let mut be = body.len();
821 let mut pe = pat.len();
822 loop {
823 let bstart = body[..be]
824 .iter()
825 .rposition(|&c| c == b'/')
826 .map_or(0, |p| p + 1);
827 let pstart = pat[..pe]
828 .iter()
829 .rposition(|&c| c == b'/')
830 .map_or(0, |p| p + 1);
831 if !segment_eq(&body[bstart..be], &pat[pstart..pe], opts) {
832 return None;
833 }
834 if pstart == 0 {
835 return Some(bstart.saturating_sub(1));
837 }
838 if bstart == 0 {
840 return None;
841 }
842 be = bstart - 1;
843 pe = pstart - 1;
844 }
845}
846
847#[cfg(test)]
848mod segment_eq_fix_tests {
849 use super::*;
850
851 #[test]
852 fn distinct_invalid_utf8_segments_do_not_coalesce() {
853 let opts = PathMatchOptions::default(); assert!(!segment_eq(b"%ff", b"%fe", opts));
857 assert!(segment_eq(b"%2f", b"%2F", opts));
859 assert!(segment_eq(b"abc", b"abc", opts));
860 }
861}
862
863#[cfg(test)]
864mod partial_ignore_case_boundary_tests {
865 use super::*;
866
867 const OPTS: PathMatchOptions = PathMatchOptions {
871 partial: true,
872 ignore_ascii_case: true,
873 percent_decode: true,
874 };
875
876 #[test]
877 fn prefix_partial_ignore_case_length_and_match() {
878 assert_eq!(match_prefix_in_body(b"abc", b"AB", OPTS), Some(2));
880 assert_eq!(match_prefix_in_body(b"abc", b"xy", OPTS), None);
883 assert_eq!(match_prefix_in_body(b"a", b"AB", OPTS), None);
885 }
886
887 #[test]
888 fn suffix_partial_ignore_case_length_and_offset() {
889 assert_eq!(match_suffix_in_body(b"abcde", b"DE", OPTS), Some(3));
892 assert_eq!(match_suffix_in_body(b"abc", b"xy", OPTS), None);
894 assert_eq!(match_suffix_in_body(b"a", b"DE", OPTS), None);
896 }
897}