use std::fmt;
pub fn for_uri_component(input: &str) -> String {
let mut out = String::with_capacity(input.len());
write_uri_component(&mut out, input).expect("writing to string cannot fail");
out
}
pub fn write_uri_component<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
let bytes = input.as_bytes();
let mut last_written = 0;
for (i, &byte) in bytes.iter().enumerate() {
if !is_unreserved(byte) {
if last_written < i {
out.write_str(&input[last_written..i])?;
}
write!(out, "%{:02X}", byte)?;
last_written = i + 1;
}
}
if last_written < bytes.len() {
out.write_str(&input[last_written..])?;
}
Ok(())
}
pub fn for_uri_path(input: &str) -> String {
let mut out = String::with_capacity(input.len());
write_uri_path(&mut out, input).expect("writing to string cannot fail");
out
}
pub fn write_uri_path<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
let bytes = input.as_bytes();
let mut last_written = 0;
for (i, &byte) in bytes.iter().enumerate() {
if !is_unreserved(byte) && byte != b'/' {
if last_written < i {
out.write_str(&input[last_written..i])?;
}
write!(out, "%{:02X}", byte)?;
last_written = i + 1;
}
}
if last_written < bytes.len() {
out.write_str(&input[last_written..])?;
}
Ok(())
}
pub fn for_form_urlencoded(input: &str) -> String {
let mut out = String::with_capacity(input.len());
write_form_urlencoded(&mut out, input).expect("writing to string cannot fail");
out
}
pub fn write_form_urlencoded<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
let bytes = input.as_bytes();
let mut last_written = 0;
for (i, &byte) in bytes.iter().enumerate() {
if byte == b' ' {
if last_written < i {
out.write_str(&input[last_written..i])?;
}
out.write_char('+')?;
last_written = i + 1;
} else if !is_form_safe(byte) {
if last_written < i {
out.write_str(&input[last_written..i])?;
}
write!(out, "%{:02X}", byte)?;
last_written = i + 1;
}
}
if last_written < bytes.len() {
out.write_str(&input[last_written..])?;
}
Ok(())
}
fn is_unreserved(b: u8) -> bool {
matches!(b, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~')
}
fn is_form_safe(b: u8) -> bool {
matches!(b, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'*' | b'-' | b'.' | b'_')
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn uri_component_no_encoding_needed() {
assert_eq!(for_uri_component("hello"), "hello");
assert_eq!(for_uri_component(""), "");
assert_eq!(for_uri_component("ABCxyz019"), "ABCxyz019");
assert_eq!(for_uri_component("-._~"), "-._~");
}
#[test]
fn uri_component_encodes_space() {
assert_eq!(for_uri_component("a b"), "a%20b");
}
#[test]
fn uri_component_encodes_reserved_chars() {
assert_eq!(for_uri_component("a=b"), "a%3Db");
assert_eq!(for_uri_component("a&b"), "a%26b");
assert_eq!(for_uri_component("a+b"), "a%2Bb");
assert_eq!(for_uri_component("a?b"), "a%3Fb");
assert_eq!(for_uri_component("a#b"), "a%23b");
assert_eq!(for_uri_component("a/b"), "a%2Fb");
}
#[test]
fn uri_component_encodes_html_significant() {
assert_eq!(for_uri_component("<script>"), "%3Cscript%3E");
assert_eq!(for_uri_component(r#""quoted""#), "%22quoted%22");
}
#[test]
fn uri_component_encodes_two_byte_utf8() {
assert_eq!(for_uri_component("\u{00A0}"), "%C2%A0");
assert_eq!(for_uri_component("é"), "%C3%A9");
}
#[test]
fn uri_component_encodes_three_byte_utf8() {
assert_eq!(for_uri_component("\u{0800}"), "%E0%A0%80");
assert_eq!(for_uri_component("世"), "%E4%B8%96");
}
#[test]
fn uri_component_encodes_four_byte_utf8() {
assert_eq!(for_uri_component("\u{10000}"), "%F0%90%80%80");
assert_eq!(for_uri_component("😀"), "%F0%9F%98%80");
}
#[test]
fn uri_component_encodes_control_chars() {
assert_eq!(for_uri_component("\x00"), "%00");
assert_eq!(for_uri_component("\x1F"), "%1F");
assert_eq!(for_uri_component("\x7F"), "%7F");
}
#[test]
fn uri_component_mixed() {
assert_eq!(
for_uri_component("key=hello world&foo=bar"),
"key%3Dhello%20world%26foo%3Dbar"
);
}
#[test]
fn uri_component_writer_variant() {
let mut out = String::new();
write_uri_component(&mut out, "a b").unwrap();
assert_eq!(out, "a%20b");
}
#[test]
fn uri_path_no_encoding_needed() {
assert_eq!(for_uri_path("hello"), "hello");
assert_eq!(for_uri_path(""), "");
assert_eq!(for_uri_path("-._~"), "-._~");
}
#[test]
fn uri_path_preserves_slashes() {
assert_eq!(for_uri_path("/a/b/c"), "/a/b/c");
assert_eq!(for_uri_path("/"), "/");
assert_eq!(for_uri_path("//"), "//");
assert_eq!(for_uri_path("a/b"), "a/b");
}
#[test]
fn uri_path_encodes_reserved_except_slash() {
assert_eq!(for_uri_path("a=b"), "a%3Db");
assert_eq!(for_uri_path("a&b"), "a%26b");
assert_eq!(for_uri_path("a?b"), "a%3Fb");
assert_eq!(for_uri_path("a#b"), "a%23b");
}
#[test]
fn uri_path_encodes_space() {
assert_eq!(for_uri_path("/a b/c d"), "/a%20b/c%20d");
}
#[test]
fn uri_path_encodes_multibyte() {
assert_eq!(for_uri_path("/café"), "/caf%C3%A9");
assert_eq!(for_uri_path("/世界"), "/%E4%B8%96%E7%95%8C");
assert_eq!(for_uri_path("/😀"), "/%F0%9F%98%80");
}
#[test]
fn uri_path_writer_variant() {
let mut out = String::new();
write_uri_path(&mut out, "/a b/c").unwrap();
assert_eq!(out, "/a%20b/c");
}
#[test]
fn form_no_encoding_needed() {
assert_eq!(for_form_urlencoded("hello"), "hello");
assert_eq!(for_form_urlencoded(""), "");
assert_eq!(for_form_urlencoded("ABCxyz019"), "ABCxyz019");
assert_eq!(for_form_urlencoded("-._*"), "-._*");
}
#[test]
fn form_space_becomes_plus() {
assert_eq!(for_form_urlencoded("a b"), "a+b");
assert_eq!(for_form_urlencoded(" "), "+++");
}
#[test]
fn form_tilde_encoded() {
assert_eq!(for_form_urlencoded("a~b"), "a%7Eb");
}
#[test]
fn form_asterisk_safe() {
assert_eq!(for_form_urlencoded("a*b"), "a*b");
}
#[test]
fn form_encodes_reserved_chars() {
assert_eq!(for_form_urlencoded("a=b"), "a%3Db");
assert_eq!(for_form_urlencoded("a&b"), "a%26b");
assert_eq!(for_form_urlencoded("a+b"), "a%2Bb");
assert_eq!(for_form_urlencoded("a?b"), "a%3Fb");
assert_eq!(for_form_urlencoded("a#b"), "a%23b");
assert_eq!(for_form_urlencoded("a/b"), "a%2Fb");
}
#[test]
fn form_encodes_multibyte() {
assert_eq!(for_form_urlencoded("é"), "%C3%A9");
assert_eq!(for_form_urlencoded("世"), "%E4%B8%96");
assert_eq!(for_form_urlencoded("😀"), "%F0%9F%98%80");
assert_eq!(for_form_urlencoded("café"), "caf%C3%A9");
}
#[test]
fn form_encodes_control_chars() {
assert_eq!(for_form_urlencoded("\x00"), "%00");
assert_eq!(for_form_urlencoded("\x1F"), "%1F");
assert_eq!(for_form_urlencoded("\x7F"), "%7F");
}
#[test]
fn form_mixed() {
assert_eq!(
for_form_urlencoded("key=hello world&foo=bar"),
"key%3Dhello+world%26foo%3Dbar"
);
}
#[test]
fn form_writer_variant() {
let mut out = String::new();
write_form_urlencoded(&mut out, "a b").unwrap();
assert_eq!(out, "a+b");
}
}