1use std::ops::Deref;
4
5use crate::types::{Iri, IriRef, Uri, UriRef};
6
7#[inline]
8#[must_use]
9pub fn path_segments(path: &str) -> PathSegments<'_> {
10 let rest = path.strip_prefix('/').unwrap_or(path);
11 PathSegments {
12 inner: if rest.is_empty() { None } else { Some(rest.split('/')) },
13 }
14}
15
16pub struct PathSegments<'a> {
17 inner: Option<std::str::Split<'a, char>>,
18}
19
20impl<'a> Iterator for PathSegments<'a> {
21 type Item = &'a str;
22 #[inline]
23 fn next(&mut self) -> Option<&'a str> {
24 self.inner.as_mut()?.next()
25 }
26}
27
28impl<'a> DoubleEndedIterator for PathSegments<'a> {
29 #[inline]
30 fn next_back(&mut self) -> Option<&'a str> {
31 self.inner.as_mut()?.next_back()
32 }
33}
34
35#[inline]
36#[must_use]
37pub fn path_is_absolute(path: &str) -> bool {
38 path.starts_with('/')
39}
40
41#[must_use]
64pub fn normalize_path(path: &str) -> String {
65 if path_is_normalized(path) {
66 return path.to_owned();
67 }
68 let absolute = path.starts_with('/');
69 let mut input = path;
70 let mut out = String::with_capacity(path.len());
71 let mut floor = 0usize;
74
75 while !input.is_empty() {
79 if input.starts_with("../") {
80 input = &input[2..];
84 pop_segment(&mut out, &mut floor, absolute);
85 } else if let Some(rest) = input.strip_prefix("./") {
86 input = rest;
87 } else if input.starts_with("/./") {
88 input = &input[2..];
89 } else if input == "/." {
90 input = "/";
91 } else if input.starts_with("/../") {
92 input = &input[3..];
93 pop_segment(&mut out, &mut floor, absolute);
94 } else if input == "/.." || input == ".." {
95 pop_segment(&mut out, &mut floor, absolute);
96 input = close_directory(&out, absolute);
97 } else if input == "." {
98 input = "";
99 } else {
100 let rest = if let Some(r) = input.strip_prefix('/') {
101 if absolute || !out.is_empty() {
104 out.push('/');
105 }
106 r
107 } else {
108 input
109 };
110 let end = memchr::memchr(b'/', rest.as_bytes()).unwrap_or(rest.len());
111 out.push_str(&rest[..end]);
112 input = &rest[end..];
113 }
114 }
115 out
116}
117
118#[inline]
121fn close_directory(out: &str, absolute: bool) -> &'static str {
122 if out.is_empty() && !absolute { "" } else { "/" }
123}
124
125#[inline]
131pub(crate) fn path_is_normalized(path: &str) -> bool {
132 memchr::memchr(b'.', path.as_bytes()).is_none()
133}
134
135#[inline]
141pub(crate) fn first_segment_has_colon(path: &str) -> bool {
142 if path.starts_with('/') {
143 return false;
144 }
145 let first_segment = match memchr::memchr(b'/', path.as_bytes()) {
146 Some(slash) => &path[..slash],
147 None => path,
148 };
149 memchr::memchr(b':', first_segment.as_bytes()).is_some()
150}
151
152fn pop_segment(out: &mut String, floor: &mut usize, absolute: bool) {
153 if out.len() > *floor {
154 match memchr::memrchr(b'/', &out.as_bytes()[*floor..]) {
155 Some(slash) => out.truncate(*floor + slash),
156 None => out.truncate(*floor),
157 }
158 return;
159 }
160 if !absolute {
164 if *floor > 0 {
165 out.push('/');
166 }
167 out.push_str("..");
168 *floor = out.len();
169 }
170}
171
172#[must_use]
173pub fn split_authority(authority: &str) -> (Option<&str>, &str, Option<&str>) {
174 let (user_info, rest) = match memchr::memchr(b'@', authority.as_bytes()) {
175 Some(i) => (Some(&authority[..i]), &authority[i + 1..]),
176 None => (None, authority),
177 };
178 let (host, port) = if let Some(rest_in) = rest.strip_prefix('[') {
179 if let Some(end) = memchr::memchr(b']', rest_in.as_bytes()) {
180 let host = &rest[..end + 2];
181 let tail = &rest[end + 2..];
182 match tail.strip_prefix(':') {
183 Some(p) => (host, Some(p)),
184 None => (host, None),
185 }
186 } else {
187 (rest, None)
188 }
189 } else {
190 match memchr::memchr(b':', rest.as_bytes()) {
191 Some(i) => (&rest[..i], Some(&rest[i + 1..])),
192 None => (rest, None),
193 }
194 };
195 (user_info, host, port)
196}
197
198impl<T: Deref<Target = str>> Iri<T> {
199 pub fn path_segments(&self) -> PathSegments<'_> {
200 path_segments(self.path())
201 }
202
203 pub fn authority_parts(&self) -> Option<(Option<&str>, &str, Option<&str>)> {
204 self.authority().map(split_authority)
205 }
206}
207
208impl<T: Deref<Target = str>> IriRef<T> {
209 pub fn path_segments(&self) -> PathSegments<'_> {
210 path_segments(self.path())
211 }
212 pub fn authority_parts(&self) -> Option<(Option<&str>, &str, Option<&str>)> {
213 self.authority().map(split_authority)
214 }
215}
216
217impl<T: Deref<Target = str>> Uri<T> {
218 pub fn path_segments(&self) -> PathSegments<'_> {
219 path_segments(self.path())
220 }
221 pub fn authority_parts(&self) -> Option<(Option<&str>, &str, Option<&str>)> {
222 self.authority().map(split_authority)
223 }
224}
225
226impl<T: Deref<Target = str>> UriRef<T> {
227 pub fn path_segments(&self) -> PathSegments<'_> {
228 path_segments(self.path())
229 }
230 pub fn authority_parts(&self) -> Option<(Option<&str>, &str, Option<&str>)> {
231 self.authority().map(split_authority)
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 #[test]
240 fn segments_absolute() {
241 let segs: Vec<&str> = path_segments("/a/b/c").collect();
242 assert_eq!(segs, vec!["a", "b", "c"]);
243 }
244
245 #[test]
246 fn segments_relative() {
247 let segs: Vec<&str> = path_segments("a/b/c").collect();
248 assert_eq!(segs, vec!["a", "b", "c"]);
249 }
250
251 #[test]
252 fn segments_trailing_slash() {
253 let segs: Vec<&str> = path_segments("/a/b/").collect();
254 assert_eq!(segs, vec!["a", "b", ""]);
255 }
256
257 #[test]
258 fn split_auth_host_only() {
259 assert_eq!(split_authority("example.com"), (None, "example.com", None));
260 }
261
262 #[test]
263 fn split_auth_full() {
264 assert_eq!(split_authority("user:pass@host:80"), (Some("user:pass"), "host", Some("80")));
265 }
266
267 #[test]
268 fn split_auth_ipv6() {
269 assert_eq!(split_authority("[::1]:8080"), (None, "[::1]", Some("8080")));
270 }
271
272 #[test]
273 fn normalize_basic() {
274 assert_eq!(normalize_path("/a/b/../c"), "/a/c");
275 assert_eq!(normalize_path("a/./b/../c"), "a/c");
276 }
277
278 #[test]
279 fn normalize_absolute_paths() {
280 for (input, expected) in [
281 ("/", "/"),
282 ("/.", "/"),
283 ("/./", "/"),
284 ("/..", "/"),
285 ("/a/..", "/"),
286 ("/a/./", "/a/"),
287 ("/a/../..", "/"),
288 ("/../..", "/"),
289 ("/a/b/c/../../d", "/a/d"),
290 ] {
291 assert_eq!(normalize_path(input), expected, "path `{input}`");
292 }
293 }
294
295 #[test]
298 fn normalize_keeps_unresolvable_parent_segments_on_relative_paths() {
299 for (input, expected) in [
300 ("..", "../"),
301 ("../", "../"),
302 ("../a", "../a"),
303 ("../..", "../../"),
304 ("../../", "../../"),
305 ("a/../..", "../"),
306 ("../a/..", "../"),
307 ("a/../../b", "../b"),
308 ] {
309 assert_eq!(normalize_path(input), expected, "path `{input}`");
310 }
311 }
312
313 #[test]
314 fn normalize_relative_paths() {
315 for (input, expected) in [
316 ("", ""),
317 (".", ""),
318 ("./", ""),
319 ("./a", "a"),
320 ("a/..", ""),
321 ("a/.", "a/"),
322 ("a/b/..", "a/"),
323 ("a/b/../", "a/"),
324 ("a/b/c/..", "a/b/"),
325 ("a/b/c/.", "a/b/c/"),
326 ("a/../b", "b"),
327 ("x/./y/.././z", "x/z"),
328 ] {
329 assert_eq!(normalize_path(input), expected, "path `{input}`");
330 }
331 }
332
333 #[test]
336 fn normalize_preserves_empty_segments() {
337 for (input, expected) in [
338 ("a//b", "a//b"),
339 ("a//../b", "a/b"),
340 ("//a", "//a"),
341 ("/.//a", "//a"),
342 ("/.//", "//"),
343 ("/a/..//b", "//b"),
344 ] {
345 assert_eq!(normalize_path(input), expected, "path `{input}`");
346 }
347 }
348
349 #[test]
355 fn normalize_leaves_a_first_segment_colon_alone() {
356 for (input, expected) in [
357 ("a:b", "a:b"),
358 ("./a:b", "a:b"),
359 ("x/../a:b", "a:b"),
360 ("a/b:c", "a/b:c"),
361 ("./a/b:c", "a/b:c"),
362 ("/a:b", "/a:b"),
363 ("isbn:0451450523", "isbn:0451450523"),
365 ("example.com,2026-01-01:foo/bar", "example.com,2026-01-01:foo/bar"),
366 ] {
367 assert_eq!(normalize_path(input), expected, "path `{input}`");
368 }
369 }
370
371 #[test]
373 fn first_segment_colon_is_detectable() {
374 for (path, expected) in [("a:b", true), ("a/b:c", false), ("/a:b", false), ("", false), ("a", false)] {
375 assert_eq!(first_segment_has_colon(path), expected, "path `{path}`");
376 }
377 }
378
379 #[test]
380 fn normalize_is_idempotent() {
381 for input in [
382 "",
383 "/",
384 ".",
385 "..",
386 "./",
387 "../",
388 "./a",
389 "../a",
390 "a/..",
391 "a/../..",
392 "/a/../..",
393 "/..",
394 "/./",
395 "a/b/..",
396 "a//b",
397 "/.//",
398 "a:b",
399 "./a:b",
400 "x/./y/.././z",
401 ] {
402 let once = normalize_path(input);
403 assert_eq!(normalize_path(&once), once, "path `{input}`");
404 }
405 }
406}