use core::fmt;
const ESCAPES: [&str; 256] = [
"%00", "%01", "%02", "%03", "%04", "%05", "%06", "%07", "%08", "%09", "%0A", "%0B", "%0C",
"%0D", "%0E", "%0F", "%10", "%11", "%12", "%13", "%14", "%15", "%16", "%17", "%18", "%19",
"%1A", "%1B", "%1C", "%1D", "%1E", "%1F", "%20", "%21", "%22", "%23", "%24", "%25", "%26",
"%27", "%28", "%29", "%2A", "%2B", "%2C", "%2D", "%2E", "%2F", "%30", "%31", "%32", "%33",
"%34", "%35", "%36", "%37", "%38", "%39", "%3A", "%3B", "%3C", "%3D", "%3E", "%3F", "%40",
"%41", "%42", "%43", "%44", "%45", "%46", "%47", "%48", "%49", "%4A", "%4B", "%4C", "%4D",
"%4E", "%4F", "%50", "%51", "%52", "%53", "%54", "%55", "%56", "%57", "%58", "%59", "%5A",
"%5B", "%5C", "%5D", "%5E", "%5F", "%60", "%61", "%62", "%63", "%64", "%65", "%66", "%67",
"%68", "%69", "%6A", "%6B", "%6C", "%6D", "%6E", "%6F", "%70", "%71", "%72", "%73", "%74",
"%75", "%76", "%77", "%78", "%79", "%7A", "%7B", "%7C", "%7D", "%7E", "%7F", "%80", "%81",
"%82", "%83", "%84", "%85", "%86", "%87", "%88", "%89", "%8A", "%8B", "%8C", "%8D", "%8E",
"%8F", "%90", "%91", "%92", "%93", "%94", "%95", "%96", "%97", "%98", "%99", "%9A", "%9B",
"%9C", "%9D", "%9E", "%9F", "%A0", "%A1", "%A2", "%A3", "%A4", "%A5", "%A6", "%A7", "%A8",
"%A9", "%AA", "%AB", "%AC", "%AD", "%AE", "%AF", "%B0", "%B1", "%B2", "%B3", "%B4", "%B5",
"%B6", "%B7", "%B8", "%B9", "%BA", "%BB", "%BC", "%BD", "%BE", "%BF", "%C0", "%C1", "%C2",
"%C3", "%C4", "%C5", "%C6", "%C7", "%C8", "%C9", "%CA", "%CB", "%CC", "%CD", "%CE", "%CF",
"%D0", "%D1", "%D2", "%D3", "%D4", "%D5", "%D6", "%D7", "%D8", "%D9", "%DA", "%DB", "%DC",
"%DD", "%DE", "%DF", "%E0", "%E1", "%E2", "%E3", "%E4", "%E5", "%E6", "%E7", "%E8", "%E9",
"%EA", "%EB", "%EC", "%ED", "%EE", "%EF", "%F0", "%F1", "%F2", "%F3", "%F4", "%F5", "%F6",
"%F7", "%F8", "%F9", "%FA", "%FB", "%FC", "%FD", "%FE", "%FF",
];
#[inline]
fn escape(b: u8) -> &'static str {
ESCAPES.get(usize::from(b)).copied().unwrap_or("%00")
}
#[allow(clippy::indexing_slicing, clippy::arithmetic_side_effects)]
const UNRESERVED: [bool; 256] = {
let mut t = [false; 256];
let mut i = 0u8;
loop {
t[i as usize] = i.is_ascii_alphanumeric() || matches!(i, b'*' | b'-' | b'.' | b'_');
if i == 255 {
break;
}
i += 1;
}
t
};
#[inline]
fn is_unreserved(b: u8) -> bool {
*UNRESERVED.get(usize::from(b)).unwrap_or(&false)
}
#[inline]
fn next_reserved(bytes: &[u8], mut i: usize) -> usize {
while let Some(&b) = bytes.get(i) {
if !is_unreserved(b) {
return i;
}
i = i.wrapping_add(1);
}
bytes.len()
}
pub fn encode_into<W: fmt::Write>(text: &str, out: &mut W) -> fmt::Result {
let bytes = text.as_bytes();
let mut i = 0usize;
while i < bytes.len() {
let end = next_reserved(bytes, i);
if let Some(run) = text.get(i..end) {
if !run.is_empty() {
out.write_str(run)?;
}
}
let Some(&b) = bytes.get(end) else { break };
if b == b' ' {
out.write_char('+')?;
} else {
out.write_str(escape(b))?;
}
i = end.wrapping_add(1);
}
Ok(())
}
#[cfg(feature = "alloc")]
mod owned {
use alloc::string::String;
use super::encode_into;
fn push_encoded(out: &mut String, text: &str) {
let _ = encode_into(text, out);
}
#[must_use]
pub fn serialize_urlencoded<I, N, V>(pairs: I) -> String
where
I: IntoIterator<Item = (N, V)>,
N: AsRef<str>,
V: AsRef<str>,
{
let mut out = String::with_capacity(256);
Serializer::new(&mut out).append_pairs(pairs);
out
}
#[derive(Debug)]
pub struct Serializer<'s> {
out: &'s mut String,
first: bool,
}
impl<'s> Serializer<'s> {
#[must_use]
pub fn new(out: &'s mut String) -> Serializer<'s> {
Serializer { out, first: true }
}
pub fn append_pair(&mut self, name: &str, value: &str) -> &mut Self {
self.out
.reserve(name.len().saturating_add(value.len()).saturating_add(2));
if !self.first {
self.out.push('&');
}
self.first = false;
push_encoded(self.out, name);
self.out.push('=');
push_encoded(self.out, value);
self
}
pub fn append_pairs<I, N, V>(&mut self, pairs: I) -> &mut Self
where
I: IntoIterator<Item = (N, V)>,
N: AsRef<str>,
V: AsRef<str>,
{
for (name, value) in pairs {
self.append_pair(name.as_ref(), value.as_ref());
}
self
}
pub fn finish(self) -> &'s mut String {
self.out
}
}
}
#[cfg(feature = "alloc")]
pub use owned::{Serializer, serialize_urlencoded};
#[cfg(test)]
mod tests {
use super::*;
use alloc::string::String;
#[test]
fn test_encode_every_ascii_byte_matches_whatwg() {
for b in 0u8..=0x7F {
let s = String::from(char::from(b));
let mut out = String::new();
encode_into(&s, &mut out).unwrap();
let expected = if b == b' ' {
String::from("+")
} else if b.is_ascii_alphanumeric() || b"*-._".contains(&b) {
s.clone()
} else {
alloc::format!("%{b:02X}")
};
assert_eq!(out, expected, "byte {b:#x}");
}
}
#[test]
fn test_encode_non_ascii_and_empty() {
let mut out = String::new();
encode_into("", &mut out).unwrap();
assert_eq!(out, "");
encode_into("€", &mut out).unwrap();
assert_eq!(out, "%E2%82%AC");
}
#[cfg(feature = "alloc")]
#[test]
fn test_serializer_appends_and_finishes() {
let mut s = String::from("x?");
let mut ser = Serializer::new(&mut s);
ser.append_pair("", "").append_pair("a", "b");
assert_eq!(ser.finish().as_str(), "x?=&a=b");
}
}