immortal_http/
util.rs

1
2use std::fmt::Display;
3use std::str::{self, Utf8Error, Chars};
4use std::error;
5
6use colored::{Colorize, ColoredString};
7
8#[derive(Debug)]
9pub enum ParseError {
10    ParamNameInvalid(String),
11    UrlDecodeNotUtf8(Utf8Error),
12    MalformedParams(String, String),
13}
14impl Display for ParseError {
15    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
16        write!(f, "{:?}", self)
17    }
18}
19impl error::Error for ParseError {}
20
21/// colours an HTTP code appropriately
22pub fn code_color(code: &str) -> ColoredString {
23    match code.as_bytes().first() {
24        Some(n) => match n {
25            b'1' => code.white().bold(),
26            b'2' => code.green(),
27            b'3' => code.cyan().bold(),
28            b'4' => code.yellow(),
29            b'5' => code.red().bold(),
30            _ => code.normal(),
31        },
32        None => {
33            "<no response code>".red().bold()
34        },
35    }
36}
37
38/// Performs html escaping on str
39pub fn escape_html(str: &str) -> String {
40    let mut out = String::new();
41    for ch in str.chars() {
42        match ch {
43            '&' => out.push_str("&amp;"),
44            '<' => out.push_str("&lt;"),
45            '>' => out.push_str("&gt;"),
46            '"' => out.push_str("&#34;"),
47            '\'' => out.push_str("&#39;"),
48            ';' => out.push_str("&#59;"),
49            _ => out.push(ch),
50        }
51    }
52    out
53}
54
55/// Accept a string, filter out the terminal control chars and return the clean string
56pub fn strip_for_terminal(to_strip: &str) -> String {
57    to_strip.chars()
58        .filter(|chr| !matches!(chr, '\x07'..='\x0D'))
59        .collect::<String>()
60}
61
62/// Accept a byte encoding a hex value and decompose it into its half-byte binary form
63fn from_hex(byte: u8) -> Option<u8> {
64    match byte {
65        b'a'..=b'f' => Some(byte - b'a' + 10),
66        b'A'..=b'F' => Some(byte - b'A' + 10),
67        b'0'..=b'9' => Some(byte - b'0'),
68        _ => None,
69    }
70}
71
72/// Accept a string, perform URL decoding on the string and return the result
73pub fn url_decode(to_decode: &str) -> Result<String, ParseError> {
74    let mut build: Vec<u8> = Vec::with_capacity(to_decode.len());
75    let mut bytes = to_decode.bytes();
76    while let Some(c) = bytes.next() {
77        match c {
78            b'%' => { // if % is found, take the next 2 characters and try to hex-decoe them
79                match bytes.next() {
80                    None => build.push(b'%'),
81                    Some(top) => match from_hex(top) {
82                        None => {
83                            build.push(b'%');
84                            build.push(top);
85                        },
86                        Some(t) => match bytes.next() {
87                            None => {
88                                build.push(b'%');
89                                build.push(top);
90                            },
91                            Some(bottom) => match from_hex(bottom) {
92                                None => { // fail, emit as-is
93                                    build.push(b'%');
94                                    build.push(top);
95                                    build.push(bottom);
96                                },
97                                Some(b) => {
98                                    // pack the top and bottom half of the byte then add it
99                                    build.push((t << 4) | b);
100                                },
101                            },
102                        },
103                    },
104                };
105            },
106            b'+' => build.push(b' '),
107            b'\0' => break,
108            other => build.push(other),
109        }
110    }
111
112    // validate if is still utf8
113    Ok(str::from_utf8(&build)
114        .map_err(ParseError::UrlDecodeNotUtf8)?.to_string())
115}
116
117const EOF_CHAR: char = '\0';
118/// Parser for Key-Value values delimited by '='
119pub(crate) struct KVParser<'buf> {
120    len_remaining: usize,
121    chars: Chars<'buf>,
122}
123
124impl<'buf> KVParser<'buf> {
125    pub(crate) fn new(input: &'buf str) -> KVParser<'buf> {
126        KVParser {
127            len_remaining: input.len(),
128            chars: input.chars(),
129        }
130    }
131
132    pub(crate) fn first(&self) -> char {
133        self.chars.clone().next().unwrap_or(EOF_CHAR)
134    }
135
136    pub(crate) fn is_eof(&self) -> bool {
137        self.chars.as_str().is_empty()
138    }
139
140    pub(crate) fn pos_within_token(&self) -> usize {
141        self.len_remaining - self.chars.as_str().len()
142    }
143
144    pub(crate) fn reset_pos_within_token(&mut self) {
145        self.len_remaining = self.chars.as_str().len();
146    }
147
148    pub(crate) fn advance(&mut self) -> Option<char> {
149        Some(self.chars.next()?)
150    }
151
152    pub(crate) fn consume_while(&mut self, mut predicate: impl FnMut(char) -> bool) {
153        while predicate(self.first()) && !self.is_eof() {
154            self.advance();
155        }
156    }
157
158    /// Advance token parser for query KV parsing
159    pub(crate) fn query_kv_pair(&mut self) -> Option<(&'buf str, &'buf str)> {
160        if !self.chars.as_str().is_ascii() {
161            return None;
162        }
163        let iter = self.chars.clone();
164        let first_char = match self.advance() {
165            Some(c) => c,
166            None => return None,
167        };
168
169        fn is_id_start(c: char) -> bool {
170            (c == '_' || c == '-' || c == '&')
171                || ('a' <= c && c <= 'z')
172                || ('A' <= c && c <= 'Z')
173        }
174        fn is_id_continue(c: char) -> bool {
175            (c == '_' || c == '-' || c == '&')
176                || ('a' <= c && c <= 'z')
177                || ('A' <= c && c <= 'Z')
178                || ('0' <= c && c <= '9')
179        }
180
181        // get key len
182        if is_id_start(first_char) {
183            self.consume_while(|c| is_id_continue(c) && c != '=');
184        } else {
185            return None;
186        }
187        let key_len = self.pos_within_token();
188
189        match self.advance() {
190            // skip =
191            Some('=') => (),
192            // anything else, just do an empty value key
193            _ => {
194                let key = &iter.as_str()[..key_len];
195                return Some((key, ""));
196            },
197        }
198        self.reset_pos_within_token();
199
200        let first_char = match self.advance() {
201            Some(c) => c,
202            // if no more, just do an empty value key
203            None => {
204                let key = &iter.as_str()[..key_len];
205                return Some((key, ""));
206            },
207        };
208
209        if first_char != '&' {
210            self.consume_while(|c| c != '&');
211        } else {
212            let key = &iter.as_str()[..key_len];
213            return Some((key, ""));
214        }
215        let val_len = self.pos_within_token();
216        self.advance();
217        self.reset_pos_within_token();
218
219        let iter_str = iter.as_str();
220        let key = &iter_str[..key_len];
221        let value = &iter_str[(key_len+1)..(key_len+1+val_len)];
222        return Some((key, value));
223    }
224
225    /// Advance token parser for cookie KV parsing
226    pub(crate) fn cookie_kv_pair(&mut self) -> Option<(&'buf str, &'buf str)> {
227        if !self.chars.as_str().is_ascii() {
228            return None;
229        }
230        let iter = self.chars.clone();
231        let first_char = match self.advance() {
232            Some(c) => c,
233            None => return None,
234        };
235
236        fn is_id_start(c: char) -> bool {
237            (c == '_')
238                || ('a' <= c && c <= 'z')
239                || ('A' <= c && c <= 'Z')
240        }
241        fn is_id_continue(c: char) -> bool {
242            (c == '_' || c == '-')
243                || ('a' <= c && c <= 'z')
244                || ('A' <= c && c <= 'Z')
245                || ('0' <= c && c <= '9')
246        }
247
248        if is_id_start(first_char) {
249            self.consume_while(|c| is_id_continue(c) && c != '=');
250        } else {
251            return None;
252        }
253        let key_len = self.pos_within_token();
254
255        match self.advance() {
256            // skip =
257            Some('=') => (),
258            // anything else, just do an empty value key
259            _ => {
260                let key = &iter.as_str()[..key_len];
261                return Some((key, ""));
262            },
263        }
264        self.reset_pos_within_token();
265
266        let first_char = match self.advance() {
267            Some(c) => c,
268            // if no more, just do an empty value key
269            None => {
270                let key = &iter.as_str()[..key_len];
271                return Some((key, ""));
272            },
273        };
274
275        if (first_char.is_ascii_alphanumeric() || first_char.is_ascii_punctuation()) && first_char != ';' {
276            self.consume_while(|c| c != ';');
277        } else {
278            let key = &iter.as_str()[..key_len];
279            return Some((key, ""));
280        }
281        let val_len = self.pos_within_token();
282        self.advance();
283        self.reset_pos_within_token();
284
285        let iter_str = iter.as_str();
286        let key = &iter_str[..key_len];
287        let value = &iter_str[(key_len+1)..(key_len+1+val_len)];
288        return Some((key, value));
289    }
290}
291
292/// Parses an HTTP query string into a key-value hashmap
293/// Note: Assumes there will be no whitespace characters.
294pub fn parse_parameters<'buf>(to_parse: &'buf str) -> Result<Vec<(&'buf str, &'buf str)>, ParseError> {
295    if to_parse.is_empty() {
296        return Ok(Vec::new());
297    }
298
299    let mut pp = KVParser::new(to_parse);
300    let mut params = Vec::new();
301
302    while let Some((key, value)) = pp.query_kv_pair() {
303        params.push((key, value));
304    }
305
306    return Ok(params);
307}
308
309/// Parses an arbitrary string slice containing an unparsed header straight from the request recieve buffer.
310pub fn parse_header<'buf>(raw_header: &'buf str) -> Option<(&'buf str, &'buf str)> {
311    if raw_header.is_empty() {
312        return None;
313    }
314
315    match raw_header.split_once(": ") {
316        None => return None,
317        Some((key, value)) => {
318            if value.is_empty() || !is_param_name_valid(key) {
319                return None;
320            } else {
321                return Some((key, value));
322            }
323        },
324    }
325}
326
327/// Accepts a string which is assumed to be a param name.
328/// Returns true if it's valid, valse if it's not valid.
329pub fn is_param_name_valid(param: &str) -> bool {
330    if param.is_empty() { return false }
331
332    for (idx, chr) in param.chars().enumerate() {
333        if idx == 0 { // first char can't be a number
334            if let '0'..='9' = chr { return false }
335        }
336        match chr { // can only be alphanumeric and '-' | '_'
337            'a'..='z' => continue,
338            'A'..='Z' => continue,
339            '0'..='9' => continue,
340            '-' => continue,
341            '_' => continue,
342            _ => return false,
343        }
344    }
345    true
346}
347
348/// Find the index of the first item `by`, and return a tuple of two mutable string slices 
349///
350/// The first being the slice content up to the first instance of item `by`, and the second being 
351/// the slice content after the first instance of `by`.
352/// 
353/// This exists because there is no stable split_once for u8 slices
354pub fn split_once(to_split: &[u8], by: u8) -> (&[u8], Option<&[u8]>) {
355    let mut found_idx = 0;
356
357    // iterate over the slice and and obtain the first instance of `by` in `to_split`
358    for (idx, byte) in to_split.iter().enumerate() {
359        if *byte == by {
360            found_idx = idx;
361            break;
362        }
363    }
364
365    // if `by` wasn't found in `to_split` or its at index 0, just return it
366    if found_idx == 0 {
367        return (to_split, None);
368    }
369
370    // build the returned tuple excluding the matched `by` in the data
371    let (item, rest) = to_split.split_at(found_idx);
372    let rest = rest.split_at(1).1;
373    if rest == b"" {
374        (item, None)
375    } else {
376        (item, Some(rest))
377    }
378}
379