1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct UriParts {
7 pub scheme: Option<String>,
9 pub authority: Option<String>,
11 pub path: String,
13 pub query: Option<String>,
15 pub fragment: Option<String>,
17}
18
19#[must_use]
21pub fn looks_like_uri(input: &str) -> bool {
22 let trimmed = input.trim();
23 !trimmed.is_empty() && (has_scheme(trimmed) || trimmed.starts_with("//"))
24}
25
26#[must_use]
28pub fn parse_uri_basic(input: &str) -> UriParts {
29 let trimmed = input.trim();
30 let scheme = extract_scheme(trimmed);
31 let mut remainder = if let Some(scheme) = scheme.as_deref() {
32 &trimmed[scheme.len() + 1..]
33 } else {
34 trimmed
35 };
36
37 let authority = if let Some(after_slashes) = remainder.strip_prefix("//") {
38 let end = after_slashes
39 .find(['/', '?', '#'])
40 .unwrap_or(after_slashes.len());
41 remainder = &after_slashes[end..];
42 let value = &after_slashes[..end];
43 (!value.is_empty()).then(|| value.to_string())
44 } else {
45 None
46 };
47
48 let fragment_index = remainder.find('#');
49 let without_fragment = &remainder[..fragment_index.unwrap_or(remainder.len())];
50 let query_index = without_fragment.find('?');
51
52 let path = without_fragment[..query_index.unwrap_or(without_fragment.len())].to_string();
53 let query = query_index.map(|index| without_fragment[index + 1..].to_string());
54 let fragment = fragment_index.map(|index| remainder[index + 1..].to_string());
55
56 UriParts {
57 scheme,
58 authority,
59 path,
60 query,
61 fragment,
62 }
63}
64
65#[must_use]
67pub fn has_scheme(input: &str) -> bool {
68 extract_scheme(input).is_some()
69}
70
71#[must_use]
73pub fn extract_scheme(input: &str) -> Option<String> {
74 let trimmed = input.trim();
75 let colon_index = trimmed.find(':')?;
76 let candidate = &trimmed[..colon_index];
77
78 if candidate.is_empty() {
79 return None;
80 }
81
82 let mut chars = candidate.chars();
83 let first = chars.next()?;
84 if !first.is_ascii_alphabetic() {
85 return None;
86 }
87
88 if chars
89 .all(|character| character.is_ascii_alphanumeric() || matches!(character, '+' | '-' | '.'))
90 {
91 Some(candidate.to_ascii_lowercase())
92 } else {
93 None
94 }
95}
96
97#[must_use]
99pub fn extract_fragment(input: &str) -> Option<String> {
100 let fragment_index = input.find('#')?;
101 Some(input[fragment_index + 1..].to_string())
102}
103
104#[must_use]
106pub fn strip_fragment(input: &str) -> &str {
107 match input.find('#') {
108 Some(index) => &input[..index],
109 None => input,
110 }
111}
112
113#[must_use]
115pub fn extract_query(input: &str) -> Option<String> {
116 let without_fragment = strip_fragment(input);
117 let query_index = without_fragment.find('?')?;
118 Some(without_fragment[query_index + 1..].to_string())
119}
120
121#[must_use]
123pub fn strip_query(input: &str) -> &str {
124 match input.find('?') {
125 Some(index) => &input[..index],
126 None => input,
127 }
128}