use ehttpd::bytes::{Data, Parse};
use ehttpd::err;
use ehttpd::error::Error;
use std::borrow::{Borrow, Cow};
use std::collections::BTreeMap;
use std::ops::Deref;
use std::str::FromStr;
#[derive(Debug, Clone)]
pub struct QueryString<'a> {
url: &'a [u8],
fields: BTreeMap<Cow<'a, [u8]>, Cow<'a, [u8]>>,
}
impl<'a> QueryString<'a> {
#[allow(clippy::missing_panics_doc, reason = "Panic should never occur")]
pub fn decode(target: &'a Data) -> Result<Self, Error> {
let mut target = target.splitn(2, |b| *b == b'?');
let url = target.next().expect("first element of split iterator is empty?!");
let querystring = target.next().unwrap_or_default();
let fields = Self::decode_raw(querystring)?;
Ok(Self { url, fields })
}
#[allow(clippy::missing_panics_doc, reason = "Panic should never occur")]
#[allow(clippy::type_complexity, reason = "The type only looks complex but is not complex")]
pub fn decode_raw(querystring: &'a [u8]) -> Result<BTreeMap<Cow<'a, [u8]>, Cow<'a, [u8]>>, Error> {
let mut fields = BTreeMap::new();
for pair in querystring.split(|b| *b == b'&') {
let mut pair = pair.splitn(2, |b| *b == b'=');
let key = pair.next().map(Cow::Borrowed).expect("first element of split iterator is empty?!");
let value = pair.next().map(Cow::Borrowed).unwrap_or_default();
if !key.is_empty() {
let key = Self::percent_decode(key)?;
let value = Self::percent_decode(value)?;
fields.insert(key, value);
}
}
Ok(fields)
}
pub fn url(&self) -> &[u8] {
self.url
}
pub fn get_str<Key>(&self, key: &Key) -> Result<Option<&str>, Error>
where
Key: Ord + ?Sized,
Cow<'a, [u8]>: Borrow<Key> + Ord,
{
let Some(value) = self.get(key) else {
return Ok(None);
};
let value = str::from_utf8(value)?;
Ok(Some(value))
}
pub fn get_as<Type, Key>(&self, key: &Key) -> Result<Option<Type>, Error>
where
Type: FromStr,
Type::Err: std::error::Error + Send + 'static,
Key: Ord + ?Sized,
Cow<'a, [u8]>: Borrow<Key> + Ord,
{
let Some(value) = self.get(key) else {
return Ok(None);
};
let parsed = value.as_ref().parse()?;
Ok(Some(parsed))
}
pub fn percent_decode(encoded: Cow<[u8]>) -> Result<Cow<[u8]>, Error> {
let needs_decode = encoded.contains(&b'%');
if !needs_decode {
return Ok(encoded);
}
let mut source = encoded.iter().copied();
let mut decoded = Vec::new();
while let Some(mut byte) = source.next() {
if byte == b'%' {
let high = source.next().ok_or(err!("Truncated hex literal"))?;
let low = source.next().ok_or(err!("Truncated hex literal"))?;
byte = Self::percent_decode_byte(high, low)?;
}
decoded.push(byte);
}
Ok(Cow::Owned(decoded))
}
fn percent_decode_nibble(nibble: u8) -> Result<u8, Error> {
#[allow(clippy::arithmetic_side_effects, reason = "The range is validated by the match")]
match nibble {
b'0'..=b'9' => Ok(nibble - b'0'),
b'a'..=b'f' => Ok((nibble - b'a') + 0xA),
b'A'..=b'F' => Ok((nibble - b'A') + 0xA),
nibble => Err(err!("Invalid nibble 0x{nibble:01x}")),
}
}
fn percent_decode_byte(high: u8, low: u8) -> Result<u8, Error> {
Ok((Self::percent_decode_nibble(high)? << 4) | Self::percent_decode_nibble(low)?)
}
}
impl<'a> Deref for QueryString<'a> {
type Target = BTreeMap<Cow<'a, [u8]>, Cow<'a, [u8]>>;
fn deref(&self) -> &Self::Target {
&self.fields
}
}
impl<'a> IntoIterator for QueryString<'a> {
type Item = <BTreeMap<Cow<'a, [u8]>, Cow<'a, [u8]>> as IntoIterator>::Item;
type IntoIter = <BTreeMap<Cow<'a, [u8]>, Cow<'a, [u8]>> as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.fields.into_iter()
}
}