use alloc::format;
use alloc::string::String;
use alloc::string::ToString;
use alloc::vec::Vec;
#[cfg(test)]
use alloc::vec;
fn is_unreserved(c: u8) -> bool {
c.is_ascii_alphanumeric() || matches!(c, b'-' | b'.' | b'_' | b'~')
}
fn is_sub_delim(c: u8) -> bool {
matches!(
c,
b'!' | b'$' | b'&' | b'\'' | b'(' | b')' | b'*' | b'+' | b',' | b';' | b'='
)
}
fn is_pchar(c: u8) -> bool {
is_unreserved(c) || is_sub_delim(c) || matches!(c, b':' | b'@')
}
fn is_path_segment_allowed(c: u8) -> bool {
is_pchar(c)
}
fn is_query_allowed(c: u8) -> bool {
is_pchar(c) || matches!(c, b'/' | b'?')
}
fn is_user_info_allowed(c: u8) -> bool {
is_unreserved(c) || is_sub_delim(c) || c == b':'
}
pub fn encode_path_segment(input: &str) -> String {
percent_encode(input, is_path_segment_allowed)
}
pub fn encode_query(input: &str) -> String {
percent_encode(input, is_query_allowed)
}
#[allow(dead_code)]
pub fn encode_form_query(input: &str) -> String {
let mut out = String::with_capacity(input.len());
for &b in input.as_bytes() {
match b {
b' ' => out.push('+'),
_ if is_unreserved(b) => out.push(b as char),
_ => percent_encode_byte(b, &mut out),
}
}
out
}
pub fn encode_user_info(input: &str) -> String {
percent_encode(input, is_user_info_allowed)
}
pub fn decode(input: &str) -> Result<String, String> {
let bytes = input.as_bytes();
let mut out = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'%' if i + 2 < bytes.len() => {
let hi = hex_val(bytes[i + 1]).ok_or_else(|| {
format!(
"invalid percent-encoding at byte {}: '%{}'",
i,
core::str::from_utf8(&bytes[i..(i + 3).min(bytes.len())]).unwrap_or("??")
)
})?;
let lo = hex_val(bytes[i + 2]).ok_or_else(|| {
format!(
"invalid percent-encoding at byte {}: '%{}'",
i,
core::str::from_utf8(&bytes[i..(i + 3).min(bytes.len())]).unwrap_or("??")
)
})?;
out.push((hi << 4) | lo);
i += 3;
continue;
}
b'%' => {
let tail =
core::str::from_utf8(&bytes[i..(i + 3).min(bytes.len())]).unwrap_or("??");
return Err(format!(
"truncated percent-encoding at byte {}: '{}'",
i, tail
));
}
b'+' => out.push(b' '), c => out.push(c),
}
i += 1;
}
String::from_utf8(out).map_err(|_| "invalid UTF-8 after decoding".to_string())
}
fn percent_encode(input: &str, allowed: fn(u8) -> bool) -> String {
let bytes = input.as_bytes();
let mut out = String::with_capacity(bytes.len());
for &b in bytes {
if allowed(b) {
out.push(b as char);
} else {
percent_encode_byte(b, &mut out);
}
}
out
}
fn percent_encode_byte(b: u8, out: &mut String) {
const HEX: &[u8; 16] = b"0123456789ABCDEF";
out.push('%');
out.push(HEX[(b >> 4) as usize] as char);
out.push(HEX[(b & 0x0F) as usize] as char);
}
fn hex_val(c: u8) -> Option<u8> {
match c {
b'0'..=b'9' => Some(c - b'0'),
b'A'..=b'F' => Some(c - b'A' + 10),
b'a'..=b'f' => Some(c - b'a' + 10),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encode_path_segment_simple() {
assert_eq!(encode_path_segment("hello"), "hello");
}
#[test]
fn test_encode_path_segment_space() {
assert_eq!(encode_path_segment("hello world"), "hello%20world");
}
#[test]
fn test_encode_path_segment_unicode() {
assert_eq!(encode_path_segment("中文"), "%E4%B8%AD%E6%96%87");
}
#[test]
fn test_encode_path_segment_special() {
assert_eq!(encode_path_segment("a/b"), "a%2Fb");
assert_eq!(encode_path_segment("a?b"), "a%3Fb");
assert_eq!(encode_path_segment("a#b"), "a%23b");
}
#[test]
fn test_decode_simple() {
assert_eq!(decode("hello").unwrap(), "hello");
}
#[test]
fn test_decode_percent() {
assert_eq!(decode("hello%20world").unwrap(), "hello world");
}
#[test]
fn test_decode_plus() {
assert_eq!(decode("hello+world").unwrap(), "hello world");
}
#[test]
fn test_decode_unicode() {
assert_eq!(decode("%E4%B8%AD%E6%96%87").unwrap(), "中文");
}
#[test]
fn test_decode_invalid() {
assert!(decode("%XX").is_err());
assert!(decode("%").is_err());
assert!(decode("%G").is_err());
}
#[test]
fn test_roundtrip() {
let cases = vec!["hello", "hello world", "a/b?c#d", "你好", "foo%bar"];
for s in &cases {
let encoded = encode_path_segment(s);
let decoded = decode(&encoded).unwrap();
assert_eq!(&decoded, s);
}
}
#[test]
fn test_encode_form_query() {
assert_eq!(encode_form_query("hello world"), "hello+world");
assert_eq!(encode_form_query("a & b"), "a+%26+b");
}
#[test]
fn test_encode_user_info() {
assert_eq!(encode_user_info("user"), "user");
assert_eq!(encode_user_info("user@host"), "user%40host");
assert_eq!(encode_user_info("user:pass"), "user:pass");
}
}