1use std::fmt;
6use std::hash::{Hash, Hasher};
7use std::rc::Rc;
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
11pub enum UriKind {
12 RelativeOrAbsolute,
14 Absolute,
16 Relative,
18}
19
20#[derive(Clone, Debug, PartialEq, Eq)]
22pub struct UriFormatError {
23 message: &'static str,
24}
25
26impl fmt::Display for UriFormatError {
27 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28 f.write_str(self.message)
29 }
30}
31
32impl std::error::Error for UriFormatError {}
33
34#[derive(Clone)]
38pub struct Uri {
39 original: Rc<str>,
40 absolute: Option<Rc<str>>,
42 scheme_length: usize,
44}
45
46fn scheme_length(s: &str) -> Option<usize> {
47 let colon = s.find(':')?;
48 let scheme = &s[..colon];
49 let mut chars = scheme.chars();
50 let first = chars.next()?;
51 if scheme.len() < 2 || !first.is_ascii_alphabetic() {
53 return None;
54 }
55 if chars.all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '-' || c == '.') {
56 Some(colon)
57 } else {
58 None
59 }
60}
61
62impl Uri {
63 pub fn new(uri_string: &str, kind: UriKind) -> Result<Uri, UriFormatError> {
65 Self::try_create(uri_string, kind).ok_or(UriFormatError {
66 message: "Invalid URI: The format of the URI could not be determined.",
67 })
68 }
69
70 pub fn absolute(uri_string: &str) -> Result<Uri, UriFormatError> {
72 Self::new(uri_string, UriKind::Absolute)
73 }
74
75 pub fn try_create(uri_string: &str, kind: UriKind) -> Option<Uri> {
77 let scheme = scheme_length(uri_string);
78
79 match (kind, scheme) {
80 (UriKind::Absolute, None) | (UriKind::Relative, Some(_)) => None,
81 (_, Some(length)) => {
82 if uri_string.len() == length + 1 {
83 return None;
85 }
86 let mut canonical = String::with_capacity(uri_string.len());
87 canonical.push_str(&uri_string[..length].to_ascii_lowercase());
88 let rest = &uri_string[length..];
89 match rest.strip_prefix("://") {
91 Some(after) => {
92 let authority_length = after.find(['/', '?', '#']).unwrap_or(after.len());
93 canonical.push_str("://");
94 canonical.push_str(&after[..authority_length].to_ascii_lowercase());
95 canonical.push_str(&after[authority_length..]);
96 }
97 None => canonical.push_str(rest),
98 }
99 Some(Uri { original: Rc::from(uri_string), absolute: Some(Rc::from(canonical)), scheme_length: length })
100 }
101 (_, None) => Some(Uri { original: Rc::from(uri_string), absolute: None, scheme_length: 0 }),
102 }
103 }
104
105 pub fn combine(base: &Uri, relative: &Uri) -> Uri {
109 if relative.is_absolute_uri() {
110 return relative.clone();
111 }
112
113 let base_text = base.absolute.as_deref().expect("The base URI must be absolute.");
114 let reference: &str = &relative.original;
115
116 let after_scheme = base.scheme_length + 1;
118 let (prefix_end, path_end) = {
119 let rest = &base_text[after_scheme..];
120 let authority_length = if let Some(stripped) = rest.strip_prefix("//") {
121 2 + stripped.find(['/', '?', '#']).unwrap_or(stripped.len())
122 } else {
123 0
124 };
125 let path_start = after_scheme + authority_length;
126 let path_length = base_text[path_start..].find(['?', '#']).unwrap_or(base_text.len() - path_start);
127 (path_start, path_start + path_length)
128 };
129 let prefix = &base_text[..prefix_end];
130 let base_path = &base_text[prefix_end..path_end];
131
132 let (reference_path, reference_suffix) = match reference.find(['?', '#']) {
133 Some(index) => (&reference[..index], &reference[index..]),
134 None => (reference, ""),
135 };
136
137 let merged = if reference_path.is_empty() {
138 base_path.to_owned()
139 } else if reference_path.starts_with('/') {
140 reference_path.to_owned()
141 } else {
142 let directory = match base_path.rfind('/') {
143 Some(index) => &base_path[..=index],
144 None => {
145 if prefix.ends_with(':') {
146 ""
147 } else {
148 "/"
149 }
150 }
151 };
152 format!("{directory}{reference_path}")
153 };
154
155 let mut result = String::with_capacity(prefix.len() + merged.len() + reference_suffix.len());
156 result.push_str(prefix);
157 result.push_str(&remove_dot_segments(&merged));
158 result.push_str(reference_suffix);
159
160 Uri::try_create(&result, UriKind::Absolute).expect("combining with an absolute base gives an absolute URI")
161 }
162
163 #[inline]
165 pub fn is_absolute_uri(&self) -> bool {
166 self.absolute.is_some()
167 }
168
169 #[inline]
171 pub fn original_string(&self) -> &str {
172 &self.original
173 }
174
175 pub fn scheme(&self) -> &str {
179 &self.expect_absolute()[..self.scheme_length]
180 }
181
182 pub fn absolute_uri(&self) -> &str {
186 self.expect_absolute()
187 }
188
189 pub fn absolute_path(&self) -> &str {
193 let text = self.expect_absolute();
194 let rest = &text[self.scheme_length + 1..];
195 let path = match rest.strip_prefix("//") {
196 Some(stripped) => &stripped[stripped.find(['/', '?', '#']).unwrap_or(stripped.len())..],
197 None => rest,
198 };
199 &path[..path.find(['?', '#']).unwrap_or(path.len())]
200 }
201
202 pub fn query(&self) -> &str {
204 let text = self.expect_absolute();
205 match text.find('?') {
206 Some(start) => {
207 let query = &text[start..];
208 &query[..query.find('#').unwrap_or(query.len())]
209 }
210 None => "",
211 }
212 }
213
214 pub fn local_path(&self) -> String {
221 use super::UriExtensions;
222
223 let text = self.expect_absolute();
224 if self.scheme() != "file" {
225 return UriExtensions::unescape_data_string(self.absolute_path());
226 }
227
228 let rest = &text[self.scheme_length + 1..];
229 let (authority, path) = match rest.strip_prefix("//") {
230 Some(stripped) => stripped.split_at(stripped.find(['/', '?', '#']).unwrap_or(stripped.len())),
231 None => ("", rest),
232 };
233 let path = &path[..path.find(['?', '#']).unwrap_or(path.len())];
234
235 if !authority.is_empty() {
236 return UriExtensions::unescape_data_string(&format!("\\\\{authority}{}", path.replace('/', "\\")));
237 }
238
239 let bytes = path.as_bytes();
240 let is_drive_path = bytes.len() >= 3
241 && bytes[0] == b'/'
242 && bytes[1].is_ascii_alphabetic()
243 && bytes[2] == b':'
244 && bytes.get(3).is_none_or(|b| *b == b'/');
245 if is_drive_path {
246 return UriExtensions::unescape_data_string(&path[1..].replace('/', "\\"));
247 }
248
249 UriExtensions::unescape_data_string(path)
250 }
251
252 pub fn ensure_absolute(&self, base_uri: Option<&Uri>) -> Uri {
257 if self.is_absolute_uri() {
258 return self.clone();
259 }
260 let Some(base_uri) = base_uri else {
261 panic!("Relative uri {self} without base url");
262 };
263 if !base_uri.is_absolute_uri() {
264 panic!("Base uri {base_uri} is relative");
265 }
266 Uri::combine(base_uri, self)
267 }
268
269 fn expect_absolute(&self) -> &str {
270 self.absolute.as_deref().expect("This operation is not supported for a relative URI.")
271 }
272}
273
274fn remove_dot_segments(path: &str) -> String {
275 let mut output: Vec<&str> = Vec::new();
276 let absolute = path.starts_with('/');
277 let trailing_slash = path.ends_with('/') || path.ends_with("/.") || path.ends_with("/..");
278
279 for segment in path.split('/') {
280 match segment {
281 "" | "." => {}
282 ".." => {
283 output.pop();
284 }
285 other => output.push(other),
286 }
287 }
288
289 let mut result = String::with_capacity(path.len());
290 if absolute {
291 result.push('/');
292 }
293 result.push_str(&output.join("/"));
294 if trailing_slash && !result.ends_with('/') {
295 result.push('/');
296 }
297 result
298}
299
300fn without_fragment(text: &str) -> &str {
303 &text[..text.find('#').unwrap_or(text.len())]
304}
305
306impl PartialEq for Uri {
307 fn eq(&self, other: &Self) -> bool {
308 match (&self.absolute, &other.absolute) {
309 (Some(a), Some(b)) => without_fragment(a) == without_fragment(b),
310 (None, None) => self.original == other.original,
311 _ => false,
312 }
313 }
314}
315
316impl Eq for Uri {}
317
318impl Hash for Uri {
319 fn hash<H: Hasher>(&self, state: &mut H) {
320 match &self.absolute {
321 Some(absolute) => without_fragment(absolute).hash(state),
322 None => self.original.hash(state),
323 }
324 }
325}
326
327impl fmt::Display for Uri {
328 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
329 match &self.absolute {
330 Some(absolute) => f.write_str(absolute),
331 None => f.write_str(&self.original),
332 }
333 }
334}
335
336impl fmt::Debug for Uri {
337 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
338 fmt::Display::fmt(self, f)
339 }
340}
341
342#[cfg(test)]
343mod tests {
344 use super::*;
345
346 #[test]
347 fn absolute_and_relative() {
348 let uri = Uri::absolute("Resm:Fonts.Assets?assembly=App#Name").unwrap();
349 assert!(uri.is_absolute_uri());
350 assert_eq!(uri.scheme(), "resm");
351 assert_eq!(uri.absolute_path(), "Fonts.Assets");
352 assert_eq!(uri.query(), "?assembly=App");
353
354 assert!(Uri::try_create("/Assets/Fonts", UriKind::Absolute).is_none());
355 assert!(Uri::try_create("/Assets/Fonts", UriKind::Relative).is_some());
356 assert!(Uri::try_create("fonts:Inter", UriKind::Relative).is_none());
357 assert!(Uri::try_create("C:x", UriKind::Absolute).is_none());
358 }
359
360 #[test]
361 fn combine_resolves_against_base() {
362 let base = Uri::absolute("app://Host/dir/page.xaml").unwrap();
363 let relative = Uri::new("/Assets/Fonts", UriKind::Relative).unwrap();
364 assert_eq!(Uri::combine(&base, &relative).to_string(), "app://host/Assets/Fonts");
365
366 let relative = Uri::new("../Fonts/#Name", UriKind::Relative).unwrap();
367 assert_eq!(Uri::combine(&base, &relative).to_string(), "app://host/Fonts/#Name");
368
369 let relative = Uri::new("Fonts", UriKind::Relative).unwrap();
370 assert_eq!(relative.ensure_absolute(Some(&base)).absolute_path(), "/dir/Fonts");
371 }
372
373 #[test]
374 fn equality_ignores_scheme_case() {
375 assert_eq!(Uri::absolute("FONTS:SystemFonts").unwrap(), Uri::absolute("fonts:SystemFonts").unwrap());
376 assert_ne!(Uri::absolute("fonts:a").unwrap(), Uri::absolute("fonts:b").unwrap());
377 assert_eq!(Uri::absolute("resm:A.B#MyFont").unwrap(), Uri::absolute("resm:A.B").unwrap());
378 assert_eq!(Uri::absolute("app://Host/Path").unwrap().absolute_uri(), "app://host/Path");
379 }
380}