ehttpd_querystring/
querystring.rs

1//! Implements query-string decoding
2
3use ehttpd::{bytes::Data, error, error::Error};
4use std::{borrow::Cow, collections::BTreeMap, ops::Deref};
5
6/// A query string
7///
8/// ## Important
9/// The query parser is pretty simple and basically parses any `key` or `key=` or `key=value` component without further
10/// validation.
11///
12/// The following rules apply:
13///  - the query string _MUST NOT_ begin with a `?` – it's not a bug, it's a feature: this allows the parser to parse
14///    raw query strings in the body (e.g. from HTML forms)
15///  - keys should be unique, non-unique keys are overwritten (i.e. `key0=ignored&key0=value` evaluates to
16///    `["key0": "value"]`)
17///  - keys don't need a value (i.e. `key0&key1` is valid)
18///  - keys can have an empty value (i.e. `key0=&key1=` is valid)
19///  - keys can have a non-empty value (i.e. `key0=value0&key1=value1` is valid)
20///  - empty keys/key-value pairs are ignored (i.e. `&` evaluates to `[]`, `key0&&key1` evaluates to
21///    `["key0": "", "key1": ""]` and `=value0&key1=value1&` evaluates to `["key1": "value1"]`)
22#[derive(Debug, Clone)]
23pub struct QueryString<'a> {
24    /// The request base URL
25    url: &'a [u8],
26    /// The querystring key-value pairs
27    fields: BTreeMap<Cow<'a, [u8]>, Cow<'a, [u8]>>,
28}
29impl<'a> QueryString<'a> {
30    /// Splits a request target into its base URL and the query string
31    #[allow(clippy::missing_panics_doc, reason = "Panic should never occur")]
32    pub fn decode(target: &'a Data) -> Result<Self, Error> {
33        // Split the URL
34        let mut target = target.splitn(2, |b| *b == b'?');
35        let url = target.next().expect("first element of split iterator is empty?!");
36        let querystring = target.next().unwrap_or_default();
37
38        // Parse the query string
39        let fields = Self::decode_raw(querystring)?;
40        Ok(Self { url, fields })
41    }
42    /// Decodes a raw query string without the leading `?` into its key-value pairs
43    #[allow(clippy::missing_panics_doc, reason = "Panic should never occur")]
44    #[allow(clippy::type_complexity, reason = "The type only looks complex but is not complex")]
45    pub fn decode_raw(querystring: &'a [u8]) -> Result<BTreeMap<Cow<'a, [u8]>, Cow<'a, [u8]>>, Error> {
46        // Parse the query components
47        let mut fields = BTreeMap::new();
48        for pair in querystring.split(|b| *b == b'&') {
49            // Read the next pair
50            let mut pair = pair.splitn(2, |b| *b == b'=');
51            let key = pair.next().map(Cow::Borrowed).expect("first element of split iterator is empty?!");
52            let value = pair.next().map(Cow::Borrowed).unwrap_or_default();
53
54            // Insert the key if it is not empty
55            if !key.is_empty() {
56                // Decode key and value and insert it
57                let key = Self::percent_decode(key)?;
58                let value = Self::percent_decode(value)?;
59                fields.insert(key, value);
60            }
61        }
62        Ok(fields)
63    }
64
65    /// The request base URL
66    pub fn url(&self) -> &[u8] {
67        self.url
68    }
69
70    /// Percent-decodes the encoded data
71    pub fn percent_decode(encoded: Cow<[u8]>) -> Result<Cow<[u8]>, Error> {
72        // Check if we need some decoding
73        let needs_decode = encoded.iter().any(|b| *b == b'%');
74        if !needs_decode {
75            return Ok(encoded);
76        }
77
78        // Perform decoding
79        let mut source = encoded.iter().copied();
80        let mut decoded = Vec::new();
81        while let Some(mut byte) = source.next() {
82            // Decode percent literal if necessary
83            if byte == b'%' {
84                // Get the encoded bytes
85                let high = source.next().ok_or(error!("Truncated hex literal"))?;
86                let low = source.next().ok_or(error!("Truncated hex literal"))?;
87                byte = Self::percent_decode_byte(high, low)?;
88            }
89
90            // Write byte
91            decoded.push(byte);
92        }
93        Ok(Cow::Owned(decoded))
94    }
95
96    /// Encodes a nibble into a hex char
97    fn percent_decode_nibble(nibble: u8) -> Result<u8, Error> {
98        // Note: All operations are safe since they are implicitly validated by the range comparisons
99        #[allow(clippy::arithmetic_side_effects, reason = "The range is validated by the match")]
100        match nibble {
101            b'0'..=b'9' => Ok(nibble - b'0'),
102            b'a'..=b'f' => Ok((nibble - b'a') + 0xA),
103            b'A'..=b'F' => Ok((nibble - b'A') + 0xA),
104            nibble => Err(error!("Invalid nibble 0x{nibble:01x}")),
105        }
106    }
107
108    /// Encodes a byte
109    fn percent_decode_byte(high: u8, low: u8) -> Result<u8, Error> {
110        Ok((Self::percent_decode_nibble(high)? << 4) | Self::percent_decode_nibble(low)?)
111    }
112}
113impl<'a> Deref for QueryString<'a> {
114    type Target = BTreeMap<Cow<'a, [u8]>, Cow<'a, [u8]>>;
115
116    fn deref(&self) -> &Self::Target {
117        &self.fields
118    }
119}
120impl<'a> IntoIterator for QueryString<'a> {
121    type Item = <BTreeMap<Cow<'a, [u8]>, Cow<'a, [u8]>> as IntoIterator>::Item;
122    type IntoIter = <BTreeMap<Cow<'a, [u8]>, Cow<'a, [u8]>> as IntoIterator>::IntoIter;
123
124    fn into_iter(self) -> Self::IntoIter {
125        self.fields.into_iter()
126    }
127}