use core::{
cell::Cell,
cmp::Ordering,
hash::{Hash, Hasher},
};
mod segment;
pub use segment::*;
#[cfg(feature = "std")]
mod r#mut;
#[cfg(feature = "std")]
pub use r#mut::*;
const CURRENT_SEGMENT: &[u8] = b".";
const PARENT_SEGMENT: &[u8] = b"..";
#[derive(static_automata::Validate, str_newtype::StrNewType)]
#[automaton(super::grammar::Path)]
#[newtype(ord([u8], &[u8], str, &str))]
#[cfg_attr(
feature = "std",
newtype(ord(Vec<u8>, String), owned(PathBuf, derive(Default, PartialEq, Eq, PartialOrd, Ord, Hash)))
)]
#[cfg_attr(feature = "serde", newtype(serde))]
pub struct Path(str);
impl Default for &Path {
fn default() -> Self {
Path::EMPTY_RELATIVE
}
}
impl Path {
pub const EMPTY_RELATIVE: &'static Self = unsafe { Self::new_unchecked("") };
pub const EMPTY_ABSOLUTE: &'static Self = unsafe { Self::new_unchecked("/") };
pub fn len(&self) -> usize {
self.as_bytes().len()
}
#[inline(always)]
pub fn is_empty(&self) -> bool {
self.as_bytes().is_empty() || self.as_bytes() == *b"/"
}
#[inline(always)]
pub fn is_absolute(&self) -> bool {
self.as_bytes().starts_with(b"/")
}
#[inline(always)]
pub fn is_relative(&self) -> bool {
!self.is_absolute()
}
#[inline]
pub fn segment_count(&self) -> usize {
if self.is_empty() {
0
} else {
1 + self.as_bytes()[1..].iter().filter(|&&b| b == b'/').count()
}
}
fn first_segment_offset(&self) -> usize {
if self.is_absolute() { 1 } else { 0 }
}
pub fn first(&self) -> Option<&Segment> {
if self.is_empty() {
None
} else {
Some(unsafe { self.segment_at(self.first_segment_offset()).0 })
}
}
pub fn last(&self) -> Option<&Segment> {
if self.is_empty() {
None
} else {
unsafe {
self.previous_segment_from(self.as_bytes().len() + 1)
.map(|(segment, _)| segment)
}
}
}
unsafe fn segment_at(&self, offset: usize) -> (&Segment, usize) {
let mut i = offset;
let bytes = self.as_bytes();
while i < bytes.len() && !matches!(bytes[i], b'/' | b'?' | b'#') {
i += 1
}
(
unsafe { Segment::new_unchecked_from_bytes(&bytes[offset..i]) },
i + 1,
)
}
unsafe fn next_segment_from(&self, offset: usize) -> Option<(&Segment, usize)> {
let bytes = self.as_bytes();
if offset <= bytes.len() {
Some(unsafe { self.segment_at(offset) })
} else {
None
}
}
unsafe fn previous_segment_from(&self, offset: usize) -> Option<(&Segment, usize)> {
if offset >= 2 {
let first_offset = self.first_segment_offset();
let bytes = self.as_bytes();
let mut i = offset - 2;
while i > first_offset && bytes[i] != b'/' {
i -= 1
}
if bytes[i] == b'/' {
let j = i + 1;
Some((unsafe { self.segment_at(j) }.0, j))
} else {
Some((unsafe { self.segment_at(first_offset) }.0, first_offset))
}
} else {
None
}
}
#[inline]
pub fn segments(&self) -> Segments<'_> {
if self.is_empty() {
Segments::Empty
} else {
Segments::NonEmpty {
path: self,
offset: self.first_segment_offset(),
back_offset: self.as_bytes().len() + 1,
consumed: 0,
total: Cell::new(None),
}
}
}
#[inline]
pub fn normalized_segments(&self) -> NormalizedSegments<'_> {
NormalizedSegments::new(self)
}
#[inline]
#[cfg(feature = "std")]
pub fn normalized(&self) -> PathBuf {
let mut result: PathBuf = if self.is_absolute() {
Self::EMPTY_ABSOLUTE.to_owned()
} else {
Self::EMPTY_RELATIVE.to_owned()
};
let mut open = false;
for segment in self.segments() {
open = result.as_path_mut().push_inner(segment)
}
if open && !result.is_empty() {
result.as_path_mut().lazy_push(Segment::EMPTY);
}
result
}
#[inline]
pub fn file_name(&self) -> Option<&Segment> {
self.segments().next_back().filter(|s| !s.is_empty())
}
pub fn directory(&self) -> &Self {
let bytes = self.as_bytes();
if bytes.is_empty() {
self
} else {
let mut i = bytes.len() - 1;
while i > 0 && bytes[i] != b'/' {
i -= 1
}
if i == 0 && bytes[i] != b'/' {
Self::EMPTY_RELATIVE
} else {
unsafe { Self::new_unchecked_from_bytes(&bytes[..=i]) }
}
}
}
#[inline]
pub fn parent(&self) -> Option<&Self> {
if self.is_empty() {
None
} else {
let bytes = self.as_bytes();
let mut end = bytes.len() - 1;
loop {
if bytes[end] == b'/' {
if end == 0 {
return Some(Self::EMPTY_ABSOLUTE);
}
break;
}
if end == 0 {
return None;
}
end -= 1;
}
if end == 1 && bytes[0] == b'/' && bytes[1] == b'/' {
unsafe { Some(Self::new_unchecked_from_bytes(b"/./")) }
} else {
unsafe { Some(Self::new_unchecked_from_bytes(&bytes[..end])) }
}
}
}
#[inline]
pub fn parent_or_empty(&self) -> &Self {
self.parent().unwrap_or_else(|| {
if self.is_absolute() {
Self::EMPTY_ABSOLUTE
} else {
Self::EMPTY_RELATIVE
}
})
}
#[inline]
#[cfg(feature = "std")]
pub fn suffix(&self, prefix: &Self) -> Option<PathBuf> {
if self.is_absolute() != prefix.is_absolute() {
return None;
}
let mut buf: PathBuf = Default::default();
let mut self_it = self.normalized_segments();
let mut prefix_it = prefix.normalized_segments();
loop {
match (self_it.next(), prefix_it.next()) {
(Some(self_seg), Some(prefix_seg))
if self_seg.as_pct_str() == prefix_seg.as_pct_str() => {}
(_, Some(_)) => return None,
(Some(seg), None) => {
buf.as_path_mut().lazy_push(seg);
}
(None, None) => break,
}
}
Some(buf)
}
pub fn looks_like_scheme(&self) -> bool {
crate::common::parse::looks_like_scheme(self.as_bytes())
}
}
impl<'a> IntoIterator for &'a Path {
type Item = &'a Segment;
type IntoIter = Segments<'a>;
#[inline]
fn into_iter(self) -> Segments<'a> {
self.segments()
}
}
impl PartialEq for Path {
#[inline]
fn eq(&self, other: &Path) -> bool {
if self.is_absolute() == other.is_absolute() {
let self_segments = self.normalized_segments();
let other_segments = other.normalized_segments();
self_segments.len() == other_segments.len()
&& self_segments.zip(other_segments).all(|(a, b)| a == b)
} else {
false
}
}
}
impl Eq for Path {}
impl PartialOrd for Path {
#[inline]
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Path {
#[inline]
fn cmp(&self, other: &Self) -> Ordering {
if self.is_absolute() == other.is_absolute() {
let mut self_segments = self.normalized_segments();
let mut other_segments = other.normalized_segments();
loop {
match (self_segments.next(), other_segments.next()) {
(None, None) => return Ordering::Equal,
(Some(_), None) => return Ordering::Greater,
(None, Some(_)) => return Ordering::Less,
(Some(a), Some(b)) => match a.cmp(b) {
Ordering::Greater => return Ordering::Greater,
Ordering::Less => return Ordering::Less,
Ordering::Equal => (),
},
}
}
} else if self.is_absolute() {
Ordering::Greater
} else {
Ordering::Less
}
}
}
impl Hash for Path {
#[inline]
fn hash<H: Hasher>(&self, hasher: &mut H) {
self.is_absolute().hash(hasher);
self.normalized_segments().for_each(move |s| s.hash(hasher))
}
}
pub enum Segments<'a> {
Empty,
NonEmpty {
path: &'a Path,
offset: usize,
back_offset: usize,
consumed: usize,
total: Cell<Option<usize>>,
},
}
impl<'a> Iterator for Segments<'a> {
type Item = &'a Segment;
fn next(&mut self) -> Option<Self::Item> {
match self {
Self::Empty => None,
Self::NonEmpty {
path,
offset,
back_offset,
consumed,
..
} => {
if offset < back_offset {
match unsafe { path.next_segment_from(*offset) } {
Some((segment, i)) => {
*offset = i;
*consumed += 1;
Some(segment)
}
None => None,
}
} else {
None
}
}
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
let len = match self {
Self::Empty => 0,
Self::NonEmpty {
path,
consumed,
total,
..
} => {
let t = match total.get() {
Some(t) => t,
None => {
let t = path.segment_count();
total.set(Some(t));
t
}
};
t - *consumed
}
};
(len, Some(len))
}
}
impl<'a> DoubleEndedIterator for Segments<'a> {
fn next_back(&mut self) -> Option<Self::Item> {
match self {
Segments::Empty => None,
Segments::NonEmpty {
path,
offset,
back_offset,
consumed,
..
} => {
if offset < back_offset {
match unsafe { path.previous_segment_from(*back_offset) } {
Some((segment, i)) => {
*back_offset = i;
*consumed += 1;
Some(segment)
}
None => None,
}
} else {
None
}
}
}
}
}
impl<'a> ExactSizeIterator for Segments<'a> {}
const NORMALIZE_STACK_SIZE: usize = 16;
pub struct NormalizedSegments<'a>(smallvec::IntoIter<[&'a Segment; NORMALIZE_STACK_SIZE]>);
impl<'a> NormalizedSegments<'a> {
fn new(path: &'a Path) -> NormalizedSegments<'a> {
let relative = path.is_relative();
let mut stack = smallvec::SmallVec::<[&'a Segment; NORMALIZE_STACK_SIZE]>::new();
let mut open = false;
for segment in path.segments() {
open = match segment.as_bytes() {
CURRENT_SEGMENT => true,
PARENT_SEGMENT => {
if stack
.last()
.map(|s| s.as_bytes() == PARENT_SEGMENT)
.unwrap_or(relative)
{
stack.push(segment)
} else {
stack.pop();
};
true
}
_ => {
stack.push(segment);
false
}
};
}
if open && !stack.is_empty() {
stack.push(Segment::EMPTY);
}
NormalizedSegments(stack.into_iter())
}
}
impl<'a> Iterator for NormalizedSegments<'a> {
type Item = &'a Segment;
fn size_hint(&self) -> (usize, Option<usize>) {
self.0.size_hint()
}
#[inline]
fn next(&mut self) -> Option<&'a Segment> {
self.0.next()
}
}
impl<'a> DoubleEndedIterator for NormalizedSegments<'a> {
#[inline]
fn next_back(&mut self) -> Option<Self::Item> {
self.0.next_back()
}
}
impl<'a> ExactSizeIterator for NormalizedSegments<'a> {}
#[cfg(feature = "std")]
impl PathBuf {
pub fn from_segments<'a>(
absolute: bool,
segments: impl IntoIterator<Item = &'a Segment>,
) -> Self {
let mut path = if absolute {
Path::EMPTY_ABSOLUTE
} else {
Path::EMPTY_RELATIVE
}
.to_owned();
for segment in segments {
path.push(segment);
}
path
}
pub unsafe fn as_mut_vec(&mut self) -> &mut Vec<u8> {
unsafe { self.0.as_mut_vec() }
}
pub fn as_path_mut(&mut self) -> PathMut<'_> {
PathMut::from_path(self)
}
pub fn lazy_push(&mut self, segment: &Segment) -> &mut Self {
self.as_path_mut().lazy_push(segment);
self
}
pub fn try_lazy_push<'s>(
&mut self,
segment: &'s str,
) -> Result<&mut Self, InvalidSegment<&'s str>> {
self.as_path_mut().try_lazy_push(segment)?;
Ok(self)
}
#[inline]
pub fn push(&mut self, segment: &Segment) -> &mut Self {
self.as_path_mut().push(segment);
self
}
#[inline]
pub fn try_push<'s>(&mut self, segment: &'s str) -> Result<&mut Self, InvalidSegment<&'s str>> {
self.as_path_mut().try_push(segment)?;
Ok(self)
}
#[inline]
pub fn append<'s, P: IntoIterator<Item = &'s Segment>>(&mut self, path: P) -> &mut Self {
self.as_path_mut().append(path);
self
}
#[inline]
pub fn try_append<'s, P: IntoIterator<Item = &'s str>>(
&mut self,
path: P,
) -> Result<&mut Self, InvalidSegment<&'s str>> {
self.as_path_mut().try_append(path)?;
Ok(self)
}
pub fn join(&mut self, path: &Path) -> &mut Self {
self.as_path_mut().join(path);
self
}
pub fn try_join<'s>(&mut self, path: &'s str) -> Result<&mut Self, InvalidPath<&'s str>> {
self.as_path_mut().try_join(path)?;
Ok(self)
}
pub fn pop(&mut self) -> &mut Self {
self.as_path_mut().try_pop();
self
}
pub fn try_pop(&mut self) -> bool {
self.as_path_mut().try_pop()
}
pub fn clear(&mut self) -> &mut Self {
self.as_path_mut().clear();
self
}
pub fn replace(&mut self, path: &Path) -> &mut Self {
self.as_path_mut().replace(path);
self
}
pub fn try_replace<'p>(&mut self, path: &'p str) -> Result<&mut Self, InvalidPath<&'p str>> {
self.as_path_mut().try_replace(path)?;
Ok(self)
}
#[inline]
pub fn normalize(&mut self) -> &mut Self {
self.as_path_mut().normalize();
self
}
}
#[cfg(feature = "std")]
impl<'a> FromIterator<&'a Segment> for PathBuf {
fn from_iter<I: IntoIterator<Item = &'a Segment>>(iter: I) -> Self {
let mut path = PathBuf::default();
for segment in iter {
path.push(segment);
}
path
}
}
#[macro_export]
macro_rules! path {
($value:literal) => {
match $crate::uri::Path::from_str($value) {
Ok(value) => value,
Err(_) => panic!("invalid URI path"),
}
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty() {
let path = Path::EMPTY_RELATIVE;
assert!(path.is_empty());
assert!(!path.is_absolute());
assert!(path.segments().next().is_none());
}
#[test]
fn empty_absolute() {
let path = Path::EMPTY_ABSOLUTE;
assert!(path.is_empty());
assert!(path.is_absolute());
assert!(path.segments().next().is_none());
}
#[test]
fn non_empty() {
let path = Path::new(b"a/b").unwrap();
assert!(!path.is_empty());
assert!(!path.is_absolute());
let mut segments = path.segments();
assert!(segments.next().unwrap().as_str() == "a");
assert!(segments.next().unwrap().as_str() == "b");
assert!(segments.next().is_none());
}
#[test]
fn non_empty_absolute() {
let path = Path::new(b"/foo/bar").unwrap();
assert!(!path.is_empty());
assert!(path.is_absolute());
let mut segments = path.segments();
assert!(segments.next().unwrap().as_bytes() == b"foo");
assert!(segments.next().unwrap().as_bytes() == b"bar");
assert!(segments.next().is_none());
}
#[test]
fn next_segment() {
let vectors: [(&[u8], usize, Option<(&[u8], usize)>); 6] = [
(b"foo/bar", 0, Some((b"foo", 4))),
(b"foo/bar", 4, Some((b"bar", 8))),
(b"foo/bar", 8, None),
(b"foo/bar/", 8, Some((b"", 9))),
(b"foo/bar/", 9, None),
(b"//foo", 1, Some((b"", 2))),
];
for (input, offset, expected) in vectors {
unsafe {
assert_eq!(
Path::new(input).unwrap().next_segment_from(offset),
expected.map(|(e, i)| (Segment::new(e).unwrap(), i))
)
}
}
}
#[test]
fn previous_segment() {
let vectors: [(&[u8], usize, Option<(&[u8], usize)>); 7] = [
(b"/foo/bar", 1, None),
(b"foo/bar", 0, None),
(b"foo/bar", 4, Some((b"foo", 0))),
(b"foo/bar", 8, Some((b"bar", 4))),
(b"foo/bar/", 8, Some((b"bar", 4))),
(b"foo/bar/", 9, Some((b"", 8))),
(b"//a/b", 4, Some((b"a", 2))),
];
for (input, offset, expected) in vectors {
unsafe {
assert_eq!(
Path::new(input).unwrap().previous_segment_from(offset),
expected.map(|(e, i)| (Segment::new(e).unwrap(), i))
)
}
}
}
#[test]
fn first_segment() {
let vectors: [(&[u8], Option<&[u8]>); 4] = [
(b"", None),
(b"/", None),
(b"//", Some(b"")),
(b"/foo/bar", Some(b"foo")),
];
for (input, expected) in vectors {
assert_eq!(
Path::new(input).unwrap().first(),
expected.map(|e| Segment::new(e).unwrap())
)
}
}
#[test]
fn segments() {
let vectors: [(&[u8], &[&[u8]]); 8] = [
(b"", &[]),
(b"foo", &[b"foo"]),
(b"/foo", &[b"foo"]),
(b"foo/", &[b"foo", b""]),
(b"/foo/", &[b"foo", b""]),
(b"a/b/c/d", &[b"a", b"b", b"c", b"d"]),
(b"a/b//c/d", &[b"a", b"b", b"", b"c", b"d"]),
(
b"//a/b/foo//bar/",
&[b"", b"a", b"b", b"foo", b"", b"bar", b""],
),
];
for (input, expected) in vectors {
let path = Path::new(input).unwrap();
let segments: Vec<_> = path.segments().collect();
assert_eq!(segments.len(), expected.len());
assert!(
segments
.into_iter()
.zip(expected)
.all(|(a, b)| a.as_bytes() == *b)
)
}
}
#[test]
fn segments_rev() {
let vectors: [(&[u8], &[&[u8]]); 8] = [
(b"", &[]),
(b"foo", &[b"foo"]),
(b"/foo", &[b"foo"]),
(b"foo/", &[b"foo", b""]),
(b"/foo/", &[b"foo", b""]),
(b"a/b/c/d", &[b"a", b"b", b"c", b"d"]),
(b"a/b//c/d", &[b"a", b"b", b"", b"c", b"d"]),
(
b"//a/b/foo//bar/",
&[b"", b"a", b"b", b"foo", b"", b"bar", b""],
),
];
for (input, expected) in vectors {
let path = Path::new(input).unwrap();
let segments: Vec<_> = path.segments().rev().collect();
assert_eq!(segments.len(), expected.len());
assert!(
segments
.into_iter()
.zip(expected.into_iter().rev())
.all(|(a, b)| a.as_bytes() == *b)
)
}
}
#[test]
fn normalized() {
let vectors: [(&[u8], &[u8]); 9] = [
(b"", b""),
(b"a/b/c", b"a/b/c"),
(b"a/..", b""),
(b"a/b/..", b"a/"),
(b"a/b/../", b"a/"),
(b"a/b/c/..", b"a/b/"),
(b"a/b/c/.", b"a/b/c/"),
(b"a/../..", b"../"),
(b"/a/../..", b"/"),
];
for (input, expected) in vectors {
let path = Path::new(input).unwrap();
let output = path.normalized();
assert_eq!(output.as_bytes(), expected);
}
}
#[test]
fn eq() {
let vectors: [(&str, &str); _] = [
("a/b/c", "a/b/c"),
("a/b/c/", "a/b/c/."),
("a/b/c/", "a/b/c/./"),
("a/b/c", "a/b/../b/c"),
("a/b/c/..", "a/b/"),
("a/..", ""),
("/a/..", "/"),
("a/../../", "../"),
("/a/../../", "/../"),
("a/b/c/./", "a/b/c/"),
("a/b/c/../", "a/b/"),
];
for (a, b) in vectors {
let a = Path::new(a).unwrap();
let b = Path::new(b).unwrap();
assert_eq!(a, b)
}
}
#[test]
fn eq_percent_encoding() {
let vectors: [(&str, &str); _] = [
("/%2a", "/%2A"),
("/a/%2b/c", "/a/%2B/c"),
("/%2D", "/-"),
("/%5F", "/_"),
("/%7E", "/~"),
("/a%2Db", "/a-b"),
("/%61", "/a"),
("/%7A", "/z"),
("/%41", "/A"),
("/%5A", "/Z"),
("/%30", "/0"),
("/%39", "/9"),
];
for (a, b) in vectors {
let a = Path::new(a).unwrap();
let b = Path::new(b).unwrap();
assert_eq!(a, b, "{:?} should equal {:?}", a.as_str(), b.as_str());
}
}
#[test]
fn ne_percent_encoding() {
let vectors: [(&str, &str); _] = [
("/%2E", "/."),
("/%2E%2E", "/.."),
("/%2F", "//"),
];
for (a, b) in vectors {
let a = Path::new(a).unwrap();
let b = Path::new(b).unwrap();
assert_ne!(a, b, "{:?} should not equal {:?}", a.as_str(), b.as_str());
}
}
#[test]
fn ne() {
let vectors: [(&str, &str); _] = [
("a/b/c", "a/b/c/"),
("a/b/c", "a/b/c/."),
("a/b/c/../", "a/b"),
];
for (a, b) in vectors {
let a = Path::new(a).unwrap();
let b = Path::new(b).unwrap();
assert_ne!(a, b)
}
}
#[test]
fn file_name() {
let vectors: [(&[u8], Option<&[u8]>); 2] = [
(b"//a/b/foo//bar/", None),
(b"//a/b/foo//bar", Some(b"bar")),
];
for (input, expected) in vectors {
let input = Path::new(input).unwrap();
assert_eq!(input.file_name().map(|s| s.as_bytes()), expected)
}
}
#[test]
fn parent() {
let vectors: [(&[u8], Option<&[u8]>); 11] = [
(b"", None),
(b"/", None),
(b".", None),
(b"//a/b/foo//bar", Some(b"//a/b/foo/")),
(b"//a/b/foo//", Some(b"//a/b/foo/")),
(b"//a/b/foo/", Some(b"//a/b/foo")),
(b"//a/b/foo", Some(b"//a/b")),
(b"//a/b", Some(b"//a")),
(b"//a", Some(b"/./")),
(b"/./", Some(b"/.")),
(b"/.", Some(b"/")),
];
for (input, expected) in vectors {
let input = Path::new(input).unwrap();
assert_eq!(input.parent().map(Path::as_bytes), expected)
}
}
#[test]
fn segments_double_ended() {
let vectors: [(&str, &[&str]); _] = [
("/a/b/c/d", &["a", "b", "c", "d"]),
("a/b", &["a", "b"]),
("/", &[]),
("", &[]),
("/a", &["a"]),
("a", &["a"]),
("/a/b/c/", &["a", "b", "c", ""]),
("a//b", &["a", "", "b"]),
];
for (input, expected) in vectors {
let path = Path::new(input).unwrap();
let fwd: Vec<&str> = path.segments().map(|s| s.as_str()).collect();
assert_eq!(fwd, expected, "forward segments of {input:?}");
let rev: Vec<&str> = path.segments().rev().map(|s| s.as_str()).collect();
let mut expected_rev: Vec<&str> = expected.to_vec();
expected_rev.reverse();
assert_eq!(rev, expected_rev, "reverse segments of {input:?}");
}
}
#[test]
fn segments_interleaved() {
let path = Path::new("/a/b/c/d").unwrap();
let mut it = path.segments();
assert_eq!(it.next().unwrap().as_str(), "a");
assert_eq!(it.next_back().unwrap().as_str(), "d");
assert_eq!(it.next().unwrap().as_str(), "b");
assert_eq!(it.next_back().unwrap().as_str(), "c");
assert!(it.next().is_none());
assert!(it.next_back().is_none());
}
#[test]
fn segments_exact_size() {
let vectors: [(&str, usize); _] = [
("/a/b/c", 3),
("", 0),
("/", 0),
("a", 1),
("/a/b/", 3),
("a//b", 3),
];
for (input, expected_len) in vectors {
let path = Path::new(input).unwrap();
let mut it = path.segments();
assert_eq!(it.len(), expected_len, "len() of segments({input:?})");
if expected_len > 0 {
it.next();
assert_eq!(it.len(), expected_len - 1);
}
}
}
#[test]
fn suffix() {
let vectors: [(&str, &str, Option<&str>); _] = [
("/foo/bar/baz", "/foo/bar", Some("baz")),
("//foo", "/", Some(".//foo")),
("/a/b/baz", "/foo/bar", None),
("/foo/bar/baz", "/foo", Some("bar/baz")),
];
for (path, prefix, expected_suffix) in vectors {
let path = Path::new(path).unwrap();
let suffix = path.suffix(Path::new(prefix).unwrap());
assert_eq!(suffix.as_deref().map(Path::as_str), expected_suffix)
}
}
}