#![forbid(unsafe_code)]
pub fn encode_uri(s: impl AsRef<str>) -> String {
let mut encoded = String::with_capacity(s.as_ref().len());
for c in s.as_ref().as_bytes() {
match c {
b'A'..=b'Z'
| b'a'..=b'z'
| b'0'..=b'9'
| b'-'
| b'_'
| b'.'
| b'!'
| b'~'
| b'*'
| b'\''
| b'('
| b')'
| b';'
| b','
| b'/'
| b'?'
| b':'
| b'@'
| b'&'
| b'='
| b'+'
| b'$'
| b'#' => encoded.push(char::from_u32(*c as _).unwrap()),
c => {
encoded.push('%');
encoded.push_str(&format!("{:02x}", c));
}
}
}
encoded
}
pub fn encode_uri_component(s: impl AsRef<str>) -> String {
let mut encoded = String::with_capacity(s.as_ref().len());
for c in s.as_ref().as_bytes() {
match c {
b'A'..=b'Z'
| b'a'..=b'z'
| b'0'..=b'9'
| b'-'
| b'_'
| b'.'
| b'!'
| b'~'
| b'*'
| b'\''
| b'('
| b')' => encoded.push(char::from_u32(*c as _).unwrap()),
c => {
encoded.push('%');
encoded.push_str(&format!("{:02x}", c));
}
}
}
encoded
}
pub fn encode_query_param(s: impl AsRef<str>) -> String {
let mut encoded = String::with_capacity(s.as_ref().len());
for c in s.as_ref().as_bytes() {
match c {
b' ' => encoded.push('+'),
b'A'..=b'Z'
| b'a'..=b'z'
| b'0'..=b'9'
| b'-'
| b'_'
| b'.'
| b'!'
| b'~'
| b'*'
| b'\''
| b'('
| b')'
| b';'
| b','
| b'/'
| b'?'
| b':'
| b'@'
| b'&'
| b'='
| b'+'
| b'$'
| b'#' => encoded.push(char::from_u32(*c as _).unwrap()),
c => {
encoded.push('%');
encoded.push_str(&format!("{:02x}", c));
}
}
}
encoded
}
#[cfg(test)]
mod tests {
use super::*;
mod encode_uri_tests {
use super::*;
#[test]
fn empty_string() {
assert_eq!(encode_uri(""), "");
}
#[test]
fn alphanumeric_unchanged() {
assert_eq!(encode_uri("ABCxyz123"), "ABCxyz123");
}
#[test]
fn unreserved_chars_unchanged() {
assert_eq!(encode_uri("-_.!~*'()"), "-_.!~*'()");
}
#[test]
fn reserved_chars_unchanged() {
assert_eq!(encode_uri(";,/?:@&=+$#"), ";,/?:@&=+$#");
}
#[test]
fn spaces_encoded() {
assert_eq!(encode_uri("hello world"), "hello%20world");
}
#[test]
fn complete_url_structure_preserved() {
assert_eq!(
encode_uri("https://user:pass@example.com:8080/path?q=1&r=2#frag"),
"https://user:pass@example.com:8080/path?q=1&r=2#frag"
);
}
#[test]
fn url_with_spaces() {
assert_eq!(
encode_uri("https://example.com/hello world?name=foo bar"),
"https://example.com/hello%20world?name=foo%20bar"
);
}
#[test]
fn non_ascii_encoded() {
assert_eq!(encode_uri("café"), "caf%c3%a9");
assert_eq!(encode_uri("日本"), "%e6%97%a5%e6%9c%ac");
}
#[test]
fn special_chars_encoded() {
assert_eq!(encode_uri("<>\""), "%3c%3e%22");
assert_eq!(encode_uri("{}|\\^`"), "%7b%7d%7c%5c%5e%60");
}
#[test]
fn accepts_string_slice() {
let s = String::from("test value");
assert_eq!(encode_uri(&s), "test%20value");
assert_eq!(encode_uri(s), "test%20value");
}
}
mod encode_uri_component_tests {
use super::*;
#[test]
fn empty_string() {
assert_eq!(encode_uri_component(""), "");
}
#[test]
fn alphanumeric_unchanged() {
assert_eq!(encode_uri_component("ABCxyz123"), "ABCxyz123");
}
#[test]
fn unreserved_chars_unchanged() {
assert_eq!(encode_uri_component("-_.!~*'()"), "-_.!~*'()");
}
#[test]
fn reserved_chars_encoded() {
assert_eq!(
encode_uri_component(";,/?:@&=+$#"),
"%3b%2c%2f%3f%3a%40%26%3d%2b%24%23"
);
}
#[test]
fn spaces_encoded() {
assert_eq!(encode_uri_component("hello world"), "hello%20world");
}
#[test]
fn path_segment() {
assert_eq!(encode_uri_component("path/to/file"), "path%2fto%2ffile");
}
#[test]
fn query_value_with_special_chars() {
assert_eq!(encode_uri_component("a=1&b=2"), "a%3d1%26b%3d2");
}
#[test]
fn html_entities_encoded() {
assert_eq!(encode_uri_component("<script>"), "%3cscript%3e");
assert_eq!(encode_uri_component("\"alert\""), "%22alert%22");
}
#[test]
fn non_ascii_encoded() {
assert_eq!(encode_uri_component("é"), "%c3%a9");
assert_eq!(encode_uri_component("émoji"), "%c3%a9moji");
}
#[test]
fn all_bytes_handled() {
assert_eq!(encode_uri_component("\x00"), "%00");
assert_eq!(encode_uri_component("\x1f"), "%1f");
assert_eq!(encode_uri_component("\x7f"), "%7f");
}
}
mod encode_query_param_tests {
use super::*;
#[test]
fn empty_string() {
assert_eq!(encode_query_param(""), "");
}
#[test]
fn alphanumeric_unchanged() {
assert_eq!(encode_query_param("ABCxyz123"), "ABCxyz123");
}
#[test]
fn spaces_become_plus() {
assert_eq!(encode_query_param("hello world"), "hello+world");
assert_eq!(encode_query_param(" "), "++");
}
#[test]
fn reserved_chars_unchanged() {
assert_eq!(encode_query_param(";,/?:@&=+$#"), ";,/?:@&=+$#");
}
#[test]
fn form_data_encoding() {
assert_eq!(encode_query_param("John Doe"), "John+Doe");
assert_eq!(encode_query_param("New York"), "New+York");
}
#[test]
fn non_ascii_encoded() {
assert_eq!(encode_query_param("naïve"), "na%c3%afve");
}
#[test]
fn mixed_content() {
assert_eq!(
encode_query_param("value with spaces & special <chars>"),
"value+with+spaces+&+special+%3cchars%3e"
);
}
}
mod comparison_tests {
use super::*;
#[test]
fn encode_uri_vs_component_slash() {
assert_eq!(encode_uri("/"), "/");
assert_eq!(encode_uri_component("/"), "%2f");
}
#[test]
fn encode_uri_vs_component_question() {
assert_eq!(encode_uri("?"), "?");
assert_eq!(encode_uri_component("?"), "%3f");
}
#[test]
fn encode_uri_vs_component_ampersand() {
assert_eq!(encode_uri("&"), "&");
assert_eq!(encode_uri_component("&"), "%26");
}
#[test]
fn encode_uri_vs_query_param_space() {
assert_eq!(encode_uri(" "), "%20");
assert_eq!(encode_uri_component(" "), "%20");
assert_eq!(encode_query_param(" "), "+");
}
#[test]
fn all_functions_same_on_alphanumeric() {
let s = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
assert_eq!(encode_uri(s), s);
assert_eq!(encode_uri_component(s), s);
assert_eq!(encode_query_param(s), s);
}
#[test]
fn all_functions_same_on_unreserved() {
let s = "-_.!~*'()";
assert_eq!(encode_uri(s), s);
assert_eq!(encode_uri_component(s), s);
assert_eq!(encode_query_param(s), s);
}
}
mod edge_cases {
use super::*;
#[test]
fn percent_sign_encoded() {
assert_eq!(encode_uri("%"), "%25");
assert_eq!(encode_uri_component("%"), "%25");
assert_eq!(encode_query_param("%"), "%25");
}
#[test]
fn already_encoded_gets_double_encoded() {
assert_eq!(encode_uri("%20"), "%2520");
assert_eq!(encode_uri_component("%20"), "%2520");
}
#[test]
fn unicode_multi_byte() {
assert_eq!(encode_uri("€"), "%e2%82%ac");
assert_eq!(encode_uri("🦀"), "%f0%9f%a6%80");
}
#[test]
fn long_string() {
let input = "a".repeat(10000);
assert_eq!(encode_uri(&input), input);
}
#[test]
fn many_encoded_chars() {
let input = " ".repeat(1000);
let expected_uri = "%20".repeat(1000);
let expected_query = "+".repeat(1000);
assert_eq!(encode_uri(&input), expected_uri);
assert_eq!(encode_query_param(&input), expected_query);
}
}
}