use crate::constants::MAX_URL_DECODED_LEN;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum DecodeError {
TooLong,
}
impl std::fmt::Display for DecodeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::TooLong => write!(
f,
"url decoded output exceeds maximum length ({}KB limit)",
MAX_URL_DECODED_LEN / 1024
),
}
}
}
impl std::error::Error for DecodeError {}
#[inline]
pub(super) fn contains_ignore_ascii_case(haystack: &str, needle: &str) -> bool {
if needle.is_empty() {
return true;
}
if needle.len() > haystack.len() {
return false;
}
haystack
.as_bytes()
.windows(needle.len())
.any(|w| w.eq_ignore_ascii_case(needle.as_bytes()))
}
pub fn url_decode(s: &str) -> Result<String, DecodeError> {
let mut bytes = Vec::with_capacity(s.len());
let mut chars = s.bytes();
while let Some(b) = chars.next() {
if bytes.len() >= MAX_URL_DECODED_LEN {
return Err(DecodeError::TooLong);
}
match b {
b'%' => {
let h1 = chars.next();
let h2 = chars.next();
if let (Some(h1), Some(h2)) = (h1, h2) {
let hex_str = [h1, h2];
if let Ok(hex_str) = std::str::from_utf8(&hex_str)
&& let Ok(decoded) = u8::from_str_radix(hex_str, 16)
{
bytes.push(decoded);
continue;
}
bytes.push(b'%');
bytes.push(h1);
bytes.push(h2);
} else {
bytes.push(b'%');
if let Some(h1) = h1 {
bytes.push(h1);
}
}
},
b'+' => bytes.push(b' '),
_ => bytes.push(b),
}
}
Ok(String::from_utf8_lossy(&bytes).into_owned())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_url_decode() {
assert_eq!(url_decode("hello%20world").unwrap(), "hello world");
assert_eq!(url_decode("hello+world").unwrap(), "hello world");
assert_eq!(url_decode("a%2Fb").unwrap(), "a/b");
assert_eq!(url_decode("plain").unwrap(), "plain");
}
#[test]
fn test_url_decode_utf8() {
assert_eq!(url_decode("caf%C3%A9").unwrap(), "café");
assert_eq!(url_decode("%E4%B8%AD%E6%96%87").unwrap(), "中文");
}
#[test]
fn test_url_decode_double_encoding() {
assert_eq!(url_decode("%2520").unwrap(), "%20"); }
#[test]
fn test_url_decode_unicode() {
assert_eq!(url_decode("%E2%9C%93").unwrap(), "✓");
assert_eq!(url_decode("%E6%97%A5%E6%9C%AC%E8%AA%9E").unwrap(), "日本語");
}
#[test]
fn test_url_decode_invalid_sequences() {
assert_eq!(url_decode("%GG").unwrap(), "%GG");
assert_eq!(url_decode("%2").unwrap(), "%2");
assert_eq!(url_decode("%").unwrap(), "%");
assert_eq!(url_decode("a%20b%GGc%2").unwrap(), "a b%GGc%2");
}
#[test]
fn test_url_decode_plus_sign() {
assert_eq!(url_decode("hello+world").unwrap(), "hello world");
assert_eq!(url_decode("a+b+c").unwrap(), "a b c");
assert_eq!(url_decode("+++").unwrap(), " ");
}
#[test]
fn test_contains_ignore_ascii_case() {
assert!(contains_ignore_ascii_case("application/json", "json"));
assert!(contains_ignore_ascii_case("APPLICATION/JSON", "json"));
assert!(contains_ignore_ascii_case("Application/Json", "JSON"));
assert!(!contains_ignore_ascii_case("text/html", "json"));
assert!(contains_ignore_ascii_case("", ""));
assert!(contains_ignore_ascii_case("anything", ""));
assert!(!contains_ignore_ascii_case("", "something"));
}
}