use std::ops::Deref;
use crate::types::{Iri, IriRef, Uri, UriRef};
#[inline]
#[must_use]
pub fn path_segments(path: &str) -> PathSegments<'_> {
let rest = path.strip_prefix('/').unwrap_or(path);
PathSegments {
inner: if rest.is_empty() { None } else { Some(rest.split('/')) },
}
}
pub struct PathSegments<'a> {
inner: Option<std::str::Split<'a, char>>,
}
impl<'a> Iterator for PathSegments<'a> {
type Item = &'a str;
#[inline]
fn next(&mut self) -> Option<&'a str> {
self.inner.as_mut()?.next()
}
}
impl<'a> DoubleEndedIterator for PathSegments<'a> {
#[inline]
fn next_back(&mut self) -> Option<&'a str> {
self.inner.as_mut()?.next_back()
}
}
#[inline]
#[must_use]
pub fn path_is_absolute(path: &str) -> bool {
path.starts_with('/')
}
#[must_use]
pub fn normalize_path(path: &str) -> String {
if path_is_normalized(path) {
return path.to_owned();
}
let absolute = path.starts_with('/');
let mut input = path;
let mut out = String::with_capacity(path.len());
let mut floor = 0usize;
while !input.is_empty() {
if input.starts_with("../") {
input = &input[2..];
pop_segment(&mut out, &mut floor, absolute);
} else if let Some(rest) = input.strip_prefix("./") {
input = rest;
} else if input.starts_with("/./") {
input = &input[2..];
} else if input == "/." {
input = "/";
} else if input.starts_with("/../") {
input = &input[3..];
pop_segment(&mut out, &mut floor, absolute);
} else if input == "/.." || input == ".." {
pop_segment(&mut out, &mut floor, absolute);
input = close_directory(&out, absolute);
} else if input == "." {
input = "";
} else {
let rest = if let Some(r) = input.strip_prefix('/') {
if absolute || !out.is_empty() {
out.push('/');
}
r
} else {
input
};
let end = memchr::memchr(b'/', rest.as_bytes()).unwrap_or(rest.len());
out.push_str(&rest[..end]);
input = &rest[end..];
}
}
out
}
#[inline]
fn close_directory(out: &str, absolute: bool) -> &'static str {
if out.is_empty() && !absolute { "" } else { "/" }
}
#[inline]
pub(crate) fn path_is_normalized(path: &str) -> bool {
memchr::memchr(b'.', path.as_bytes()).is_none()
}
#[inline]
pub(crate) fn first_segment_has_colon(path: &str) -> bool {
if path.starts_with('/') {
return false;
}
let first_segment = match memchr::memchr(b'/', path.as_bytes()) {
Some(slash) => &path[..slash],
None => path,
};
memchr::memchr(b':', first_segment.as_bytes()).is_some()
}
fn pop_segment(out: &mut String, floor: &mut usize, absolute: bool) {
if out.len() > *floor {
match memchr::memrchr(b'/', &out.as_bytes()[*floor..]) {
Some(slash) => out.truncate(*floor + slash),
None => out.truncate(*floor),
}
return;
}
if !absolute {
if *floor > 0 {
out.push('/');
}
out.push_str("..");
*floor = out.len();
}
}
#[must_use]
pub fn split_authority(authority: &str) -> (Option<&str>, &str, Option<&str>) {
let (user_info, rest) = match memchr::memchr(b'@', authority.as_bytes()) {
Some(i) => (Some(&authority[..i]), &authority[i + 1..]),
None => (None, authority),
};
let (host, port) = if let Some(rest_in) = rest.strip_prefix('[') {
if let Some(end) = memchr::memchr(b']', rest_in.as_bytes()) {
let host = &rest[..end + 2];
let tail = &rest[end + 2..];
match tail.strip_prefix(':') {
Some(p) => (host, Some(p)),
None => (host, None),
}
} else {
(rest, None)
}
} else {
match memchr::memchr(b':', rest.as_bytes()) {
Some(i) => (&rest[..i], Some(&rest[i + 1..])),
None => (rest, None),
}
};
(user_info, host, port)
}
impl<T: Deref<Target = str>> Iri<T> {
pub fn path_segments(&self) -> PathSegments<'_> {
path_segments(self.path())
}
pub fn authority_parts(&self) -> Option<(Option<&str>, &str, Option<&str>)> {
self.authority().map(split_authority)
}
}
impl<T: Deref<Target = str>> IriRef<T> {
pub fn path_segments(&self) -> PathSegments<'_> {
path_segments(self.path())
}
pub fn authority_parts(&self) -> Option<(Option<&str>, &str, Option<&str>)> {
self.authority().map(split_authority)
}
}
impl<T: Deref<Target = str>> Uri<T> {
pub fn path_segments(&self) -> PathSegments<'_> {
path_segments(self.path())
}
pub fn authority_parts(&self) -> Option<(Option<&str>, &str, Option<&str>)> {
self.authority().map(split_authority)
}
}
impl<T: Deref<Target = str>> UriRef<T> {
pub fn path_segments(&self) -> PathSegments<'_> {
path_segments(self.path())
}
pub fn authority_parts(&self) -> Option<(Option<&str>, &str, Option<&str>)> {
self.authority().map(split_authority)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn segments_absolute() {
let segs: Vec<&str> = path_segments("/a/b/c").collect();
assert_eq!(segs, vec!["a", "b", "c"]);
}
#[test]
fn segments_relative() {
let segs: Vec<&str> = path_segments("a/b/c").collect();
assert_eq!(segs, vec!["a", "b", "c"]);
}
#[test]
fn segments_trailing_slash() {
let segs: Vec<&str> = path_segments("/a/b/").collect();
assert_eq!(segs, vec!["a", "b", ""]);
}
#[test]
fn split_auth_host_only() {
assert_eq!(split_authority("example.com"), (None, "example.com", None));
}
#[test]
fn split_auth_full() {
assert_eq!(split_authority("user:pass@host:80"), (Some("user:pass"), "host", Some("80")));
}
#[test]
fn split_auth_ipv6() {
assert_eq!(split_authority("[::1]:8080"), (None, "[::1]", Some("8080")));
}
#[test]
fn normalize_basic() {
assert_eq!(normalize_path("/a/b/../c"), "/a/c");
assert_eq!(normalize_path("a/./b/../c"), "a/c");
}
#[test]
fn normalize_absolute_paths() {
for (input, expected) in [
("/", "/"),
("/.", "/"),
("/./", "/"),
("/..", "/"),
("/a/..", "/"),
("/a/./", "/a/"),
("/a/../..", "/"),
("/../..", "/"),
("/a/b/c/../../d", "/a/d"),
] {
assert_eq!(normalize_path(input), expected, "path `{input}`");
}
}
#[test]
fn normalize_keeps_unresolvable_parent_segments_on_relative_paths() {
for (input, expected) in [
("..", "../"),
("../", "../"),
("../a", "../a"),
("../..", "../../"),
("../../", "../../"),
("a/../..", "../"),
("../a/..", "../"),
("a/../../b", "../b"),
] {
assert_eq!(normalize_path(input), expected, "path `{input}`");
}
}
#[test]
fn normalize_relative_paths() {
for (input, expected) in [
("", ""),
(".", ""),
("./", ""),
("./a", "a"),
("a/..", ""),
("a/.", "a/"),
("a/b/..", "a/"),
("a/b/../", "a/"),
("a/b/c/..", "a/b/"),
("a/b/c/.", "a/b/c/"),
("a/../b", "b"),
("x/./y/.././z", "x/z"),
] {
assert_eq!(normalize_path(input), expected, "path `{input}`");
}
}
#[test]
fn normalize_preserves_empty_segments() {
for (input, expected) in [
("a//b", "a//b"),
("a//../b", "a/b"),
("//a", "//a"),
("/.//a", "//a"),
("/.//", "//"),
("/a/..//b", "//b"),
] {
assert_eq!(normalize_path(input), expected, "path `{input}`");
}
}
#[test]
fn normalize_leaves_a_first_segment_colon_alone() {
for (input, expected) in [
("a:b", "a:b"),
("./a:b", "a:b"),
("x/../a:b", "a:b"),
("a/b:c", "a/b:c"),
("./a/b:c", "a/b:c"),
("/a:b", "/a:b"),
("isbn:0451450523", "isbn:0451450523"),
("example.com,2026-01-01:foo/bar", "example.com,2026-01-01:foo/bar"),
] {
assert_eq!(normalize_path(input), expected, "path `{input}`");
}
}
#[test]
fn first_segment_colon_is_detectable() {
for (path, expected) in [("a:b", true), ("a/b:c", false), ("/a:b", false), ("", false), ("a", false)] {
assert_eq!(first_segment_has_colon(path), expected, "path `{path}`");
}
}
#[test]
fn normalize_is_idempotent() {
for input in [
"",
"/",
".",
"..",
"./",
"../",
"./a",
"../a",
"a/..",
"a/../..",
"/a/../..",
"/..",
"/./",
"a/b/..",
"a//b",
"/.//",
"a:b",
"./a:b",
"x/./y/.././z",
] {
let once = normalize_path(input);
assert_eq!(normalize_path(&once), once, "path `{input}`");
}
}
}