use axum::body::Bytes;
use serde_json::Value;
use std::collections::HashMap;
pub(crate) fn read_json_object(body: &Bytes) -> Value {
let raw = std::str::from_utf8(body).unwrap_or_default().trim();
if raw.is_empty() {
return Value::Object(Default::default());
}
match serde_json::from_str::<Value>(raw) {
Ok(value) if value.is_object() || value.is_array() => value,
_ => Value::Object(Default::default()),
}
}
pub(crate) fn string_field<'a>(body: &'a Value, key: &str) -> Option<&'a str> {
body.get(key).and_then(Value::as_str)
}
pub(crate) fn parse_form(body: &Bytes) -> HashMap<String, String> {
parse_pairs(&String::from_utf8_lossy(body))
}
pub(crate) fn parse_query(uri: &axum::http::Uri) -> HashMap<String, String> {
parse_pairs(uri.query().unwrap_or_default())
}
fn parse_pairs(raw: &str) -> HashMap<String, String> {
let mut form = HashMap::new();
for pair in raw.split('&').filter(|pair| !pair.is_empty()) {
let (key, value) = match pair.split_once('=') {
Some(split) => split,
None => (pair, ""),
};
form.entry(percent_decode(key))
.or_insert_with(|| percent_decode(value));
}
form
}
pub(crate) fn percent_decode(value: &str) -> String {
let bytes = value.as_bytes();
let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
let mut index = 0;
while index < bytes.len() {
match bytes[index] {
b'+' => {
out.push(b' ');
index += 1;
}
b'%' if index + 2 < bytes.len() => match hex_pair(bytes[index + 1], bytes[index + 2]) {
Some(decoded) => {
out.push(decoded);
index += 3;
}
None => {
out.push(b'%');
index += 1;
}
},
other => {
out.push(other);
index += 1;
}
}
}
String::from_utf8_lossy(&out).into_owned()
}
fn hex_pair(high: u8, low: u8) -> Option<u8> {
Some((hex_digit(high)? << 4) | hex_digit(low)?)
}
fn hex_digit(byte: u8) -> Option<u8> {
match byte {
b'0'..=b'9' => Some(byte - b'0'),
b'a'..=b'f' => Some(byte - b'a' + 10),
b'A'..=b'F' => Some(byte - b'A' + 10),
_ => None,
}
}
pub(crate) fn decode_uri_component(raw: &str) -> Option<String> {
let bytes = raw.as_bytes();
let mut decoded = Vec::with_capacity(bytes.len());
let mut index = 0;
while index < bytes.len() {
if bytes[index] == b'%' {
let hex = raw.get(index + 1..index + 3)?;
if !hex.bytes().all(|byte| byte.is_ascii_hexdigit()) {
return None;
}
decoded.push(u8::from_str_radix(hex, 16).ok()?);
index += 3;
} else {
decoded.push(bytes[index]);
index += 1;
}
}
String::from_utf8(decoded).ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decodes_escapes_and_plus() {
assert_eq!(percent_decode("a+b%2Fc"), "a b/c");
assert_eq!(percent_decode("%E2%9C%93"), "\u{2713}");
}
#[test]
fn leaves_a_stray_escape_alone_without_panicking() {
assert_eq!(percent_decode("%a\u{e9}"), "%a\u{e9}");
assert_eq!(percent_decode("%"), "%");
assert_eq!(percent_decode("%z1"), "%z1");
assert_eq!(percent_decode("100%"), "100%");
}
#[test]
fn a_repeated_key_keeps_the_first_value() {
let form = parse_form(&Bytes::from_static(b"name=one&name=two"));
assert_eq!(form.get("name").map(String::as_str), Some("one"));
}
}