use std::borrow::Cow;
use url::Url;
const MAX_PERCENT_DECODING_DEPTH: usize = 8;
pub(in crate::formats::http) enum NestedUrl {
NotUrl,
Parsed(
Url,
),
Invalid,
LimitExceeded,
}
pub(in crate::formats::http) fn detect(value: &str) -> NestedUrl {
let mut candidate = Cow::Borrowed(value);
let mut malformed = false;
for depth in 0..=MAX_PERCENT_DECODING_DEPTH {
if let Ok(url) = Url::parse(candidate.as_ref()) {
return if matches!(url.scheme(), "http" | "https") {
if malformed {
NestedUrl::Invalid
} else {
NestedUrl::Parsed(url)
}
} else {
NestedUrl::NotUrl
};
}
if starts_with_http_scheme(candidate.as_ref()) {
return NestedUrl::Invalid;
}
if malformed && starts_with_http_name(candidate.as_ref()) {
return NestedUrl::Invalid;
}
if depth == MAX_PERCENT_DECODING_DEPTH {
return if candidate.as_bytes().contains(&b'%') {
NestedUrl::LimitExceeded
} else {
NestedUrl::NotUrl
};
}
let decoded = match percent_decode_once(candidate.as_ref()) {
Ok(Some(decoded)) => decoded,
Ok(None) => return NestedUrl::NotUrl,
Err(prefix) => {
if prefix.is_empty() {
return NestedUrl::NotUrl;
}
malformed = true;
prefix
}
};
candidate = Cow::Owned(decoded);
}
NestedUrl::LimitExceeded
}
fn starts_with_http_name(value: &str) -> bool {
value
.as_bytes()
.get(..b"http".len())
.is_some_and(|prefix| prefix.eq_ignore_ascii_case(b"http"))
}
fn starts_with_http_scheme(value: &str) -> bool {
[b"http://".as_slice(), b"https://".as_slice()]
.into_iter()
.any(|scheme| {
value
.as_bytes()
.get(..scheme.len())
.is_some_and(|prefix| prefix.eq_ignore_ascii_case(scheme))
})
}
fn percent_decode_once(value: &str) -> Result<Option<String>, String> {
let bytes = value.as_bytes();
let mut decoded = Vec::with_capacity(bytes.len());
let mut index = 0;
let mut changed = false;
while index < bytes.len() {
if bytes[index] == b'%' {
let Some(high) = bytes.get(index + 1).and_then(|byte| hex(*byte)) else {
return Err(valid_utf8_prefix(decoded));
};
let Some(low) = bytes.get(index + 2).and_then(|byte| hex(*byte)) else {
return Err(valid_utf8_prefix(decoded));
};
decoded.push((high << 4) | low);
index += 3;
changed = true;
} else {
decoded.push(bytes[index]);
index += 1;
}
}
if changed {
String::from_utf8(decoded).map(Some).map_err(|error| {
let valid_up_to = error.utf8_error().valid_up_to();
String::from_utf8_lossy(&error.into_bytes()[..valid_up_to]).into_owned()
})
} else {
Ok(None)
}
}
fn valid_utf8_prefix(decoded: Vec<u8>) -> String {
match String::from_utf8(decoded) {
Ok(text) => text,
Err(error) => {
let valid_up_to = error.utf8_error().valid_up_to();
String::from_utf8_lossy(&error.into_bytes()[..valid_up_to]).into_owned()
}
}
}
const fn hex(byte: u8) -> Option<u8> {
match byte {
b'0'..=b'9' => Some(byte - b'0'),
b'a'..=b'f' => Some(byte - b'a' + 10),
b'A'..=b'F' => Some(byte - b'A' + 10),
_ => None,
}
}