use std::borrow::Cow;
use std::fmt::{self, Debug, Formatter};
use std::ops::Range;
use super::{PathParams, decode_url_path};
#[derive(Clone, Debug, Eq, PartialEq)]
enum PathPart {
Raw(Range<usize>),
Decoded(String),
}
impl PathPart {
#[inline]
fn as_str<'a>(&'a self, source: &'a str) -> &'a str {
match self {
Self::Raw(range) => &source[range.clone()],
Self::Decoded(value) => value,
}
}
#[inline]
fn len(&self, source: &str) -> usize {
self.as_str(source).len()
}
}
#[doc(hidden)]
#[derive(Clone)]
pub struct PathState<'a> {
path: Cow<'a, str>,
parts: Vec<PathPart>,
pub(crate) cursor: (usize, usize),
pub(crate) params: PathParams,
#[cfg(feature = "matched-path")]
pub(crate) matched_parts: Vec<String>,
pub(crate) end_slash: bool, pub(crate) once_ended: bool,
}
impl<'a> Debug for PathState<'a> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let parts = self.parts_iter().collect::<Vec<_>>();
let mut debug = f.debug_struct("PathState");
debug
.field("parts", &parts)
.field("cursor", &self.cursor)
.field("params", &self.params);
#[cfg(feature = "matched-path")]
debug.field("matched_parts", &self.matched_parts);
debug
.field("end_slash", &self.end_slash)
.field("once_ended", &self.once_ended)
.finish()
}
}
impl<'a, 'b> PartialEq<PathState<'b>> for PathState<'a> {
fn eq(&self, other: &PathState<'b>) -> bool {
if !self.parts_iter().eq(other.parts_iter()) {
return false;
}
if self.cursor != other.cursor
|| self.params != other.params
|| self.end_slash != other.end_slash
|| self.once_ended != other.once_ended
{
return false;
}
#[cfg(feature = "matched-path")]
if self.matched_parts != other.matched_parts {
return false;
}
true
}
}
impl<'a> Eq for PathState<'a> {}
impl<'a> PathState<'a> {
#[inline]
#[must_use]
#[deprecated(
since = "0.93.0",
note = "use PathState::from_owned_path or PathState::from_borrowed_path to make ownership explicit"
)]
pub fn new(url_path: &str) -> Self {
Self::from_owned_path(url_path.to_owned())
}
#[inline]
#[must_use]
pub fn from_borrowed_path(path: &'a str) -> Self {
Self::from_cow_path(Cow::Borrowed(path))
}
#[inline]
#[must_use]
pub fn from_owned_path(path: String) -> Self {
Self::from_cow_path(Cow::Owned(path))
}
#[inline]
#[must_use]
pub fn from_cow_path(path: Cow<'a, str>) -> Self {
let end_slash = path.ends_with('/');
let parts = parse_path_parts(path.as_ref());
Self {
path,
parts,
cursor: (0, 0),
params: PathParams::new(),
end_slash,
once_ended: false,
#[cfg(feature = "matched-path")]
matched_parts: vec![],
}
}
#[inline]
#[must_use]
pub fn pick(&self) -> Option<&str> {
match self.parts.get(self.cursor.0) {
None => None,
Some(part) => {
let part = part.as_str(&self.path);
if self.cursor.1 >= part.len() {
let row = self.cursor.0 + 1;
self.parts.get(row).map(|s| s.as_str(&self.path))
} else {
part.get(self.cursor.1..)
}
}
}
}
#[inline]
#[must_use]
pub fn all_rest(&self) -> Option<Cow<'_, str>> {
if let Some(picked) = self.pick() {
if self.cursor.0 >= self.parts.len() - 1 {
if self.end_slash {
Some(Cow::Owned(format!("{picked}/")))
} else {
Some(Cow::Borrowed(picked))
}
} else {
let rest = &self.parts[self.cursor.0 + 1..];
let trailing = usize::from(self.end_slash);
let cap = picked.len()
+ rest.iter().map(|s| s.len(&self.path) + 1).sum::<usize>()
+ trailing;
let mut buf = String::with_capacity(cap);
buf.push_str(picked);
for part in rest {
buf.push('/');
buf.push_str(part.as_str(&self.path));
}
if self.end_slash {
buf.push('/');
}
Some(Cow::Owned(buf))
}
} else {
None
}
}
#[inline]
pub fn forward(&mut self, steps: usize) {
let mut steps = steps + self.cursor.1;
while let Some(part) = self.parts.get(self.cursor.0) {
let len = part.len(&self.path);
if len > steps {
self.cursor.1 = steps;
return;
} else {
steps -= len;
self.cursor = (self.cursor.0 + 1, 0);
}
}
}
#[inline]
#[must_use]
pub fn is_ended(&self) -> bool {
self.cursor.0 >= self.parts.len()
}
#[inline]
pub(crate) fn parts_len(&self) -> usize {
self.parts.len()
}
#[inline]
fn parts_iter(&self) -> impl Iterator<Item = &str> + '_ {
self.parts.iter().map(|part| part.as_str(&self.path))
}
#[inline]
#[cfg(test)]
pub(crate) fn parts(&self) -> impl Iterator<Item = &str> + '_ {
self.parts_iter()
}
}
fn parse_path_parts(path: &str) -> Vec<PathPart> {
let mut parts = Vec::new();
let end = path.trim_end_matches('/').len();
let mut cursor = path.len() - path.trim_start_matches('/').len();
while cursor < end {
while cursor < end && path.as_bytes()[cursor] == b'/' {
cursor += 1;
}
let start = cursor;
while cursor < end && path.as_bytes()[cursor] != b'/' {
cursor += 1;
}
if start == cursor {
continue;
}
match decode_url_path(&path[start..cursor]) {
Cow::Borrowed(_) => parts.push(PathPart::Raw(start..cursor)),
Cow::Owned(decoded) => parts.push(PathPart::Decoded(decoded)),
}
}
parts
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use super::PathState;
#[test]
fn constructors_make_path_ownership_explicit() {
let borrowed = PathState::from_borrowed_path("plain");
assert!(matches!(borrowed.path, Cow::Borrowed("plain")));
let owned = PathState::from_owned_path("plain".to_owned());
assert!(matches!(owned.path, Cow::Owned(ref path) if path == "plain"));
let cow = PathState::from_cow_path(Cow::Borrowed("plain"));
assert!(matches!(cow.path, Cow::Borrowed("plain")));
}
#[test]
fn raw_utf8_ranges_stay_on_char_boundaries() {
let user = "\u{7528}\u{6237}";
let emoji = "\u{1f600}";
let e_accent = "\u{e9}";
let path = format!("/{user}/{emoji}/{e_accent}/rest/");
let mut state = PathState::from_borrowed_path(&path);
assert_eq!(
state.parts().collect::<Vec<_>>(),
vec![user, emoji, e_accent, "rest"]
);
assert_eq!(state.pick(), Some(user));
state.forward(user.len());
assert_eq!(state.pick(), Some(emoji));
state.forward(emoji.len());
assert_eq!(state.pick(), Some(e_accent));
assert_eq!(state.all_rest().as_deref(), Some("\u{e9}/rest/"));
}
#[test]
fn decoded_utf8_parts_advance_by_bytes() {
let user = "\u{7528}\u{6237}";
let emoji = "\u{1f600}";
let mut state =
PathState::from_borrowed_path("/%E7%94%A8%E6%88%B7/%F0%9F%98%80/a%2Fb/rest");
assert_eq!(
state.parts().collect::<Vec<_>>(),
vec![user, emoji, "a/b", "rest"]
);
assert_eq!(state.pick(), Some(user));
state.forward(user.len());
assert_eq!(state.pick(), Some(emoji));
state.forward(emoji.len());
assert_eq!(state.pick(), Some("a/b"));
state.forward("a/b".len());
assert_eq!(state.pick(), Some("rest"));
}
#[test]
fn equality_uses_decoded_parts_not_internal_storage() {
let user = "\u{7528}\u{6237}";
assert_eq!(
PathState::from_borrowed_path(user),
PathState::from_owned_path(format!("/{user}"))
);
assert_eq!(
PathState::from_borrowed_path(user),
PathState::from_borrowed_path("%E7%94%A8%E6%88%B7")
);
}
}