Skip to main content

use_uri/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4/// Basic URI parts extracted with lightweight string splitting.
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct UriParts {
7    /// The URI scheme when a valid scheme prefix exists.
8    pub scheme: Option<String>,
9    /// The authority component when the URI uses `//authority` syntax.
10    pub authority: Option<String>,
11    /// The path component, which may be empty.
12    pub path: String,
13    /// The query without the leading `?` when present.
14    pub query: Option<String>,
15    /// The fragment without the leading `#` when present.
16    pub fragment: Option<String>,
17}
18
19/// Returns `true` when the input starts with a valid URI scheme or authority marker.
20#[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/// Parses URI-like input with lightweight splitting and graceful fallback behavior.
27#[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/// Returns `true` when the input begins with a valid URI scheme.
66#[must_use]
67pub fn has_scheme(input: &str) -> bool {
68    extract_scheme(input).is_some()
69}
70
71/// Extracts a lowercase URI scheme when the input starts with one.
72#[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/// Extracts the fragment without the leading `#` when present.
98#[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/// Returns the input without the fragment portion.
105#[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/// Extracts the query without the leading `?` and excluding any fragment.
114#[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/// Returns the input without the query portion.
122#[must_use]
123pub fn strip_query(input: &str) -> &str {
124    match input.find('?') {
125        Some(index) => &input[..index],
126        None => input,
127    }
128}