use core::{
fmt::{self, Debug},
hash::Hash,
};
use crate::std::borrow::Cow;
use super::component_input::IntoUriComponent;
use crate::uri::{
PathCaptures, PathPattern,
encode::{
encoded_path, encoded_segment, encoded_segment_cmp, encoded_segment_eq,
extend_encoded_path, hash_encoded_segment, write_encoded_path, write_encoded_segment,
},
};
use rama_core::bytes::BytesMut;
use itertools::Itertools;
use percent_encoding::percent_decode;
#[derive(Debug, Default, Clone, Copy)]
pub struct PathRef<'a> {
pub(crate) bytes: &'a [u8],
}
impl<'a> PathRef<'a> {
#[must_use]
#[inline]
pub(crate) const fn new(bytes: &'a [u8]) -> Self {
Self { bytes }
}
#[must_use]
#[inline]
pub fn from_raw_str(path: &'a str) -> Self {
Self::new(path.as_bytes())
}
#[must_use]
#[inline(always)]
pub fn as_encoded_str(self) -> Cow<'a, str> {
encoded_path(self.bytes)
}
pub(super) fn write_encoded_to(self, buf: &mut BytesMut) {
extend_encoded_path(buf, self.bytes);
}
#[must_use]
#[inline(always)]
pub fn is_empty(self) -> bool {
self.bytes.is_empty()
}
#[must_use]
#[inline]
pub fn trimmed_slashes(self) -> Self {
Self::new(trim_ascii_slashes(self.bytes))
}
#[must_use]
pub fn segment_range(self, start: usize, count: usize) -> Option<Self> {
let (start, end) = segment_range_bounds(self.bytes, start, count)?;
Some(Self::new(&self.bytes[start..end]))
}
#[must_use]
pub fn segments(self) -> PathSegments<'a> {
if self.bytes.is_empty() {
return PathSegments::empty();
}
let remaining = self.bytes.strip_prefix(b"/").unwrap_or(self.bytes);
PathSegments {
remaining,
exhausted: false,
}
}
#[must_use]
pub fn has_prefix(self, prefix: impl IntoUriComponent) -> bool {
self.has_prefix_with_opts(prefix, PathMatchOptions::default())
}
#[must_use]
#[expect(
clippy::needless_pass_by_value,
reason = "by-value matches IntoUriComponent's signature on sibling setters; this impl only borrows the input"
)]
pub fn has_prefix_with_opts(
self,
prefix: impl IntoUriComponent,
opts: PathMatchOptions,
) -> bool {
let prefix = prefix.as_uri_component_bytes();
match_prefix_in_body(strip_leading_slash(self.bytes), &prefix, opts).is_some()
}
#[must_use]
pub fn has_suffix(self, suffix: impl IntoUriComponent) -> bool {
self.has_suffix_with_opts(suffix, PathMatchOptions::default())
}
#[must_use]
#[expect(
clippy::needless_pass_by_value,
reason = "by-value matches IntoUriComponent's signature on sibling setters; this impl only borrows the input"
)]
pub fn has_suffix_with_opts(
self,
suffix: impl IntoUriComponent,
opts: PathMatchOptions,
) -> bool {
let suffix = suffix.as_uri_component_bytes();
match_suffix_in_body(strip_leading_slash(self.bytes), &suffix, opts).is_some()
}
#[must_use]
pub fn nth_segment(self, n: usize) -> Option<PathSegment<'a>> {
self.segments().nth(n)
}
#[must_use]
pub fn first_segment(self) -> Option<PathSegment<'a>> {
self.segments().next()
}
#[must_use]
pub fn last_segment(self) -> Option<PathSegment<'a>> {
self.segments().last()
}
#[must_use]
pub fn segment_count(self) -> usize {
self.segments().len()
}
#[must_use]
pub fn contains_segments(self, needle: impl IntoUriComponent) -> bool {
self.contains_segments_with_opts(needle, PathMatchOptions::default())
}
#[must_use]
#[expect(
clippy::needless_pass_by_value,
reason = "by-value matches IntoUriComponent's signature on sibling matchers; this impl only borrows the input"
)]
pub fn contains_segments_with_opts(
self,
needle: impl IntoUriComponent,
opts: PathMatchOptions,
) -> bool {
let needle = needle.as_uri_component_bytes();
if trim_ascii_slashes(&needle).is_empty() {
return true;
}
let body = strip_leading_slash(self.bytes);
let mut start = 0;
loop {
if match_prefix_in_body(&body[start..], &needle, opts).is_some() {
return true;
}
match memchr::memchr(b'/', &body[start..]) {
Some(i) => start += i + 1,
None => return false,
}
}
}
#[must_use]
#[inline(always)]
pub fn is_pattern_match(self, pattern: &PathPattern) -> bool {
pattern.is_match(self)
}
#[must_use]
#[inline(always)]
pub fn pattern_captures(self, pattern: &PathPattern) -> Option<PathCaptures<'_, 'a>> {
pattern.captures(self)
}
}
impl<'a> From<&'a str> for PathRef<'a> {
#[inline]
fn from(path: &'a str) -> Self {
Self::from_raw_str(path)
}
}
impl PartialEq for PathRef<'_> {
fn eq(&self, other: &Self) -> bool {
self.segments()
.zip_longest(other.segments())
.all(|segment_pair| {
let (segment_a, segment_b) = segment_pair.left_and_right();
segment_a == segment_b
})
}
}
impl Eq for PathRef<'_> {}
impl Ord for PathRef<'_> {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
for segment_pair in self.segments().zip_longest(other.segments()) {
match segment_pair.left_and_right() {
(None, None) => (),
(None, Some(_)) => return core::cmp::Ordering::Less,
(Some(_), None) => return core::cmp::Ordering::Greater,
(Some(segment_a), Some(segment_b)) => {
let ordering = segment_a.cmp(&segment_b);
if ordering != core::cmp::Ordering::Equal {
return ordering;
}
}
}
}
core::cmp::Ordering::Equal
}
}
impl PartialOrd for PathRef<'_> {
#[inline(always)]
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq<str> for PathRef<'_> {
#[inline(always)]
fn eq(&self, other: &str) -> bool {
self.eq(&PathRef::from_raw_str(other))
}
}
impl PartialEq<&str> for PathRef<'_> {
#[inline(always)]
fn eq(&self, other: &&str) -> bool {
self.eq(&PathRef::from_raw_str(other))
}
}
impl<'a> PartialEq<PathRef<'a>> for str {
#[inline(always)]
fn eq(&self, other: &PathRef<'a>) -> bool {
PathRef::from_raw_str(self).eq(other)
}
}
impl<'a> PartialEq<PathRef<'a>> for &str {
#[inline(always)]
fn eq(&self, other: &PathRef<'a>) -> bool {
PathRef::from_raw_str(self).eq(other)
}
}
impl Hash for PathRef<'_> {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
let mut separator = "";
for segment in self.segments() {
separator.hash(state);
separator = "/";
segment.hash(state);
}
}
}
impl core::fmt::Display for PathRef<'_> {
#[inline(always)]
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write_encoded_path(f, self.bytes)
}
}
#[derive(Debug, Clone, Copy)]
pub struct PathSegment<'a> {
raw: &'a [u8],
}
impl PartialEq for PathSegment<'_> {
#[inline(always)]
fn eq(&self, other: &Self) -> bool {
encoded_segment_eq(self.raw, other.raw)
}
}
impl Eq for PathSegment<'_> {}
impl PartialEq<str> for PathSegment<'_> {
#[inline(always)]
fn eq(&self, other: &str) -> bool {
self.eq(&PathSegment::new(other.as_bytes()))
}
}
impl PartialEq<&str> for PathSegment<'_> {
#[inline(always)]
fn eq(&self, other: &&str) -> bool {
self.eq(&PathSegment::new(other.as_bytes()))
}
}
impl<'a> PartialEq<PathSegment<'a>> for str {
#[inline(always)]
fn eq(&self, other: &PathSegment<'a>) -> bool {
PathSegment::new(self.as_bytes()).eq(other)
}
}
impl<'a> PartialEq<PathSegment<'a>> for &str {
#[inline(always)]
fn eq(&self, other: &PathSegment<'a>) -> bool {
PathSegment::new(self.as_bytes()).eq(other)
}
}
impl PartialOrd for PathSegment<'_> {
#[inline(always)]
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for PathSegment<'_> {
#[inline(always)]
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
encoded_segment_cmp(self.raw, other.raw)
}
}
impl Hash for PathSegment<'_> {
#[inline(always)]
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
hash_encoded_segment(state, self.raw);
}
}
impl fmt::Display for PathSegment<'_> {
#[inline(always)]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write_encoded_segment(f, self.raw)
}
}
impl<'a> PathSegment<'a> {
#[must_use]
#[inline]
pub(crate) const fn new(raw: &'a [u8]) -> Self {
Self { raw }
}
#[inline]
pub(super) fn write_encoded_to(self, buf: &mut BytesMut) {
super::encode::extend_encoded_segment_bytes(buf, self.raw);
}
#[inline]
pub(super) fn encoded_capacity_hint(self) -> usize {
self.raw.len()
}
#[must_use]
#[inline(always)]
pub fn as_encoded_str(self) -> Cow<'a, str> {
encoded_segment(self.raw)
}
#[must_use]
pub fn as_decoded_str(self) -> Cow<'a, str> {
percent_decode(self.raw).decode_utf8_lossy()
}
#[must_use]
pub fn is_empty(self) -> bool {
self.raw.is_empty()
}
#[must_use]
pub fn matches(self, other: impl IntoUriComponent) -> bool {
self.matches_with_opts(other, PathMatchOptions::default())
}
#[must_use]
#[expect(
clippy::needless_pass_by_value,
reason = "by-value matches IntoUriComponent's signature on sibling matchers; this impl only borrows the input"
)]
pub fn matches_with_opts(self, other: impl IntoUriComponent, opts: PathMatchOptions) -> bool {
segment_eq(self.raw, &other.as_uri_component_bytes(), opts)
}
#[must_use]
pub fn has_prefix(self, prefix: impl IntoUriComponent) -> bool {
self.has_prefix_with_opts(prefix, PathMatchOptions::default())
}
#[must_use]
#[expect(
clippy::needless_pass_by_value,
reason = "by-value matches IntoUriComponent's signature on sibling matchers; this impl only borrows the input"
)]
pub fn has_prefix_with_opts(
self,
prefix: impl IntoUriComponent,
opts: PathMatchOptions,
) -> bool {
let pat = prefix.as_uri_component_bytes();
let seg = maybe_decode(self.raw, opts.percent_decode);
let pat = maybe_decode(&pat, opts.percent_decode);
byte_starts_with(&seg, &pat, opts.ignore_ascii_case)
}
#[must_use]
pub fn has_suffix(self, suffix: impl IntoUriComponent) -> bool {
self.has_suffix_with_opts(suffix, PathMatchOptions::default())
}
#[must_use]
#[expect(
clippy::needless_pass_by_value,
reason = "by-value matches IntoUriComponent's signature on sibling matchers; this impl only borrows the input"
)]
pub fn has_suffix_with_opts(
self,
suffix: impl IntoUriComponent,
opts: PathMatchOptions,
) -> bool {
let pat = suffix.as_uri_component_bytes();
let seg = maybe_decode(self.raw, opts.percent_decode);
let pat = maybe_decode(&pat, opts.percent_decode);
byte_ends_with(&seg, &pat, opts.ignore_ascii_case)
}
}
#[derive(Debug, Clone)]
pub struct PathSegments<'a> {
remaining: &'a [u8],
exhausted: bool,
}
impl<'a> PathSegments<'a> {
fn empty() -> Self {
Self {
remaining: &[],
exhausted: true,
}
}
}
impl<'a> Iterator for PathSegments<'a> {
type Item = PathSegment<'a>;
fn next(&mut self) -> Option<Self::Item> {
if self.exhausted {
return None;
}
if let Some(i) = memchr::memchr(b'/', self.remaining) {
let seg = &self.remaining[..i];
self.remaining = &self.remaining[i + 1..];
Some(PathSegment::new(seg))
} else {
let seg = self.remaining;
self.remaining = &[];
self.exhausted = true;
Some(PathSegment::new(seg))
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
let n = if self.exhausted {
0
} else {
self.remaining.iter().filter(|&&b| b == b'/').count() + 1
};
(n, Some(n))
}
}
impl core::iter::FusedIterator for PathSegments<'_> {}
impl ExactSizeIterator for PathSegments<'_> {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct PathMatchOptions {
pub partial: bool,
pub ignore_ascii_case: bool,
pub percent_decode: bool,
}
impl Default for PathMatchOptions {
fn default() -> Self {
Self {
partial: false,
ignore_ascii_case: false,
percent_decode: true,
}
}
}
#[inline]
pub(super) fn strip_leading_slash(path: &[u8]) -> &[u8] {
path.strip_prefix(b"/").unwrap_or(path)
}
pub(super) fn trim_ascii_slashes(mut bytes: &[u8]) -> &[u8] {
while let Some(rest) = bytes.strip_prefix(b"/") {
bytes = rest;
}
while let Some(rest) = bytes.strip_suffix(b"/") {
bytes = rest;
}
bytes
}
pub(super) fn segment_range_bounds(
path: &[u8],
start: usize,
count: usize,
) -> Option<(usize, usize)> {
if path.is_empty() || count == 0 {
return None;
}
let has_leading_slash = path.first().copied() == Some(b'/');
let body_offset = usize::from(has_leading_slash);
let body = &path[body_offset..];
let first = segment_body_bounds(body, start)?;
let last = segment_body_bounds(body, start.checked_add(count - 1)?)?;
let abs_start = if has_leading_slash {
if start == 0 {
0
} else {
body_offset + first.0 - 1
}
} else if start == 0 {
0
} else {
first.0 - 1
};
let abs_end = if has_leading_slash && body.is_empty() && start == 0 {
1
} else {
body_offset + last.1
};
Some((abs_start, abs_end))
}
fn segment_body_bounds(body: &[u8], target: usize) -> Option<(usize, usize)> {
let mut index = 0;
let mut start = 0;
loop {
let end = body[start..]
.iter()
.position(|&b| b == b'/')
.map_or(body.len(), |pos| start + pos);
if index == target {
return Some((start, end));
}
if end == body.len() {
return None;
}
start = end + 1;
index += 1;
}
}
#[inline]
pub(super) fn maybe_decode(bytes: &[u8], decode: bool) -> Cow<'_, [u8]> {
if decode {
percent_decode(bytes).into()
} else {
Cow::Borrowed(bytes)
}
}
#[inline]
pub(super) fn byte_starts_with(hay: &[u8], needle: &[u8], ignore_case: bool) -> bool {
if ignore_case {
hay.len() >= needle.len() && hay[..needle.len()].eq_ignore_ascii_case(needle)
} else {
hay.starts_with(needle)
}
}
#[inline]
pub(super) fn byte_ends_with(hay: &[u8], needle: &[u8], ignore_case: bool) -> bool {
if ignore_case {
hay.len() >= needle.len() && hay[hay.len() - needle.len()..].eq_ignore_ascii_case(needle)
} else {
hay.ends_with(needle)
}
}
pub(super) fn segment_eq(seg: &[u8], pat: &[u8], opts: PathMatchOptions) -> bool {
if opts.percent_decode {
let seg: crate::std::borrow::Cow<'_, [u8]> = percent_decode(seg).into();
let pat: crate::std::borrow::Cow<'_, [u8]> = percent_decode(pat).into();
if opts.ignore_ascii_case {
seg.eq_ignore_ascii_case(&pat)
} else {
seg == pat
}
} else if opts.ignore_ascii_case {
seg.eq_ignore_ascii_case(pat)
} else {
seg == pat
}
}
pub(super) fn match_prefix_in_body(
body: &[u8],
pattern_raw: &[u8],
opts: PathMatchOptions,
) -> Option<usize> {
let pat = trim_ascii_slashes(pattern_raw);
if pat.is_empty() {
return Some(0);
}
if opts.partial {
let matches = if opts.ignore_ascii_case {
body.len() >= pat.len() && body[..pat.len()].eq_ignore_ascii_case(pat)
} else {
body.starts_with(pat)
};
return matches.then_some(pat.len());
}
let mut bi = 0;
let mut pi = 0;
loop {
let bend = body[bi..]
.iter()
.position(|&c| c == b'/')
.map_or(body.len(), |p| bi + p);
let pend = pat[pi..]
.iter()
.position(|&c| c == b'/')
.map_or(pat.len(), |p| pi + p);
if !segment_eq(&body[bi..bend], &pat[pi..pend], opts) {
return None;
}
if pend == pat.len() {
return Some(bend);
}
if bend >= body.len() {
return None;
}
bi = bend + 1;
pi = pend + 1;
}
}
pub(super) fn match_suffix_in_body(
body: &[u8],
pattern_raw: &[u8],
opts: PathMatchOptions,
) -> Option<usize> {
let pat = trim_ascii_slashes(pattern_raw);
if pat.is_empty() {
return Some(body.len());
}
if opts.partial {
let matches = if opts.ignore_ascii_case {
body.len() >= pat.len() && body[body.len() - pat.len()..].eq_ignore_ascii_case(pat)
} else {
body.ends_with(pat)
};
return matches.then(|| body.len() - pat.len());
}
let mut be = body.len();
let mut pe = pat.len();
loop {
let bstart = body[..be]
.iter()
.rposition(|&c| c == b'/')
.map_or(0, |p| p + 1);
let pstart = pat[..pe]
.iter()
.rposition(|&c| c == b'/')
.map_or(0, |p| p + 1);
if !segment_eq(&body[bstart..be], &pat[pstart..pe], opts) {
return None;
}
if pstart == 0 {
return Some(bstart.saturating_sub(1));
}
if bstart == 0 {
return None;
}
be = bstart - 1;
pe = pstart - 1;
}
}
#[cfg(test)]
mod segment_eq_fix_tests {
use super::*;
#[test]
fn distinct_invalid_utf8_segments_do_not_coalesce() {
let opts = PathMatchOptions::default(); assert!(!segment_eq(b"%ff", b"%fe", opts));
assert!(segment_eq(b"%2f", b"%2F", opts));
assert!(segment_eq(b"abc", b"abc", opts));
}
}
#[cfg(test)]
mod partial_ignore_case_boundary_tests {
use super::*;
const OPTS: PathMatchOptions = PathMatchOptions {
partial: true,
ignore_ascii_case: true,
percent_decode: true,
};
#[test]
fn prefix_partial_ignore_case_length_and_match() {
assert_eq!(match_prefix_in_body(b"abc", b"AB", OPTS), Some(2));
assert_eq!(match_prefix_in_body(b"abc", b"xy", OPTS), None);
assert_eq!(match_prefix_in_body(b"a", b"AB", OPTS), None);
}
#[test]
fn suffix_partial_ignore_case_length_and_offset() {
assert_eq!(match_suffix_in_body(b"abcde", b"DE", OPTS), Some(3));
assert_eq!(match_suffix_in_body(b"abc", b"xy", OPTS), None);
assert_eq!(match_suffix_in_body(b"a", b"DE", OPTS), None);
}
}