Skip to main content

iri_rs_core/
components.rs

1//! Helper views over path / authority components, and convenience accessors.
2
3use std::ops::Deref;
4
5use crate::types::{Iri, IriRef, Uri, UriRef};
6
7#[inline]
8pub fn path_segments(path: &str) -> PathSegments<'_> {
9    let rest = path.strip_prefix('/').unwrap_or(path);
10    PathSegments {
11        inner: if rest.is_empty() {
12            None
13        } else {
14            Some(rest.split('/'))
15        },
16    }
17}
18
19pub struct PathSegments<'a> {
20    inner: Option<std::str::Split<'a, char>>,
21}
22
23impl<'a> Iterator for PathSegments<'a> {
24    type Item = &'a str;
25    #[inline]
26    fn next(&mut self) -> Option<&'a str> {
27        self.inner.as_mut()?.next()
28    }
29}
30
31impl<'a> DoubleEndedIterator for PathSegments<'a> {
32    #[inline]
33    fn next_back(&mut self) -> Option<&'a str> {
34        self.inner.as_mut()?.next_back()
35    }
36}
37
38#[inline]
39pub fn path_is_absolute(path: &str) -> bool {
40    path.starts_with('/')
41}
42
43pub fn normalize_path(path: &str) -> String {
44    if memchr::memchr(b'.', path.as_bytes()).is_none() {
45        return path.to_owned();
46    }
47    let mut input = path;
48    let mut out = String::with_capacity(path.len());
49
50    while !input.is_empty() {
51        if let Some(rest) = input.strip_prefix("../") {
52            input = rest;
53        } else if let Some(rest) = input.strip_prefix("./") {
54            input = rest;
55        } else if input.starts_with("/./") {
56            input = &input[2..];
57        } else if input == "/." {
58            input = "/";
59        } else if input.starts_with("/../") {
60            input = &input[3..];
61            pop_to_slash(&mut out);
62        } else if input == "/.." {
63            input = "/";
64            pop_to_slash(&mut out);
65        } else if input == "." || input == ".." {
66            input = "";
67        } else {
68            let rest = if let Some(r) = input.strip_prefix('/') {
69                out.push('/');
70                r
71            } else {
72                input
73            };
74            let end = memchr::memchr(b'/', rest.as_bytes()).unwrap_or(rest.len());
75            out.push_str(&rest[..end]);
76            input = &rest[end..];
77        }
78    }
79    out
80}
81
82fn pop_to_slash(out: &mut String) {
83    if let Some(i) = memchr::memrchr(b'/', out.as_bytes()) {
84        out.truncate(i);
85    } else {
86        out.clear();
87    }
88}
89
90pub fn split_authority(authority: &str) -> (Option<&str>, &str, Option<&str>) {
91    let (user_info, rest) = match memchr::memchr(b'@', authority.as_bytes()) {
92        Some(i) => (Some(&authority[..i]), &authority[i + 1..]),
93        None => (None, authority),
94    };
95    let (host, port) = if let Some(rest_in) = rest.strip_prefix('[') {
96        if let Some(end) = memchr::memchr(b']', rest_in.as_bytes()) {
97            let host = &rest[..end + 2];
98            let tail = &rest[end + 2..];
99            match tail.strip_prefix(':') {
100                Some(p) => (host, Some(p)),
101                None => (host, None),
102            }
103        } else {
104            (rest, None)
105        }
106    } else {
107        match memchr::memchr(b':', rest.as_bytes()) {
108            Some(i) => (&rest[..i], Some(&rest[i + 1..])),
109            None => (rest, None),
110        }
111    };
112    (user_info, host, port)
113}
114
115impl<T: Deref<Target = str>> Iri<T> {
116    pub fn path_segments(&self) -> PathSegments<'_> {
117        path_segments(self.path())
118    }
119
120    pub fn authority_parts(&self) -> Option<(Option<&str>, &str, Option<&str>)> {
121        self.authority().map(split_authority)
122    }
123}
124
125impl<T: Deref<Target = str>> IriRef<T> {
126    pub fn path_segments(&self) -> PathSegments<'_> {
127        path_segments(self.path())
128    }
129    pub fn authority_parts(&self) -> Option<(Option<&str>, &str, Option<&str>)> {
130        self.authority().map(split_authority)
131    }
132}
133
134impl<T: Deref<Target = str>> Uri<T> {
135    pub fn path_segments(&self) -> PathSegments<'_> {
136        path_segments(self.path())
137    }
138    pub fn authority_parts(&self) -> Option<(Option<&str>, &str, Option<&str>)> {
139        self.authority().map(split_authority)
140    }
141}
142
143impl<T: Deref<Target = str>> UriRef<T> {
144    pub fn path_segments(&self) -> PathSegments<'_> {
145        path_segments(self.path())
146    }
147    pub fn authority_parts(&self) -> Option<(Option<&str>, &str, Option<&str>)> {
148        self.authority().map(split_authority)
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn segments_absolute() {
158        let segs: Vec<&str> = path_segments("/a/b/c").collect();
159        assert_eq!(segs, vec!["a", "b", "c"]);
160    }
161
162    #[test]
163    fn segments_relative() {
164        let segs: Vec<&str> = path_segments("a/b/c").collect();
165        assert_eq!(segs, vec!["a", "b", "c"]);
166    }
167
168    #[test]
169    fn segments_trailing_slash() {
170        let segs: Vec<&str> = path_segments("/a/b/").collect();
171        assert_eq!(segs, vec!["a", "b", ""]);
172    }
173
174    #[test]
175    fn split_auth_host_only() {
176        assert_eq!(split_authority("example.com"), (None, "example.com", None));
177    }
178
179    #[test]
180    fn split_auth_full() {
181        assert_eq!(
182            split_authority("user:pass@host:80"),
183            (Some("user:pass"), "host", Some("80"))
184        );
185    }
186
187    #[test]
188    fn split_auth_ipv6() {
189        assert_eq!(split_authority("[::1]:8080"), (None, "[::1]", Some("8080")));
190    }
191
192    #[test]
193    fn normalize_basic() {
194        assert_eq!(normalize_path("/a/b/../c"), "/a/c");
195        assert_eq!(normalize_path("a/./b/../c"), "a/c");
196    }
197}