http_lite/
lib.rs

1#![no_std]
2
3use heapless::String;
4use thiserror::Error;
5
6#[derive(Error, Debug, PartialEq)]
7pub enum ParseError {
8    #[error("query string did not contain '='")]
9    QueryNoEquals,
10    #[error("buffer was not large enough")]
11    BufferTooSmall,
12    #[error("only ascii characters are supported for decoding (<=128)")]
13    EncodedNonAscii,
14    #[error("malformed http request")]
15    BadRequest,
16    #[error("only GET and POST are supported")]
17    UnsupportedMethod,
18    #[error("only http/1.1 is supported")]
19    UnsupportedProtocol,
20}
21
22impl From<heapless::CapacityError> for ParseError {
23    fn from(_value: heapless::CapacityError) -> Self {
24        Self::BufferTooSmall
25    }
26}
27
28pub struct QueryParams<'a, const KN: usize, const VN: usize> {
29    rest: &'a str,
30}
31
32impl<'a, const KN: usize, const VN: usize> Iterator for QueryParams<'a, KN, VN> {
33    type Item = Result<QueryParam<KN, VN>, ParseError>;
34
35    fn next(&mut self) -> Option<Self::Item> {
36        if self.rest.is_empty() {
37            return None;
38        }
39
40        let (segment, tail) = self.rest.split_once('&').unwrap_or((self.rest, ""));
41        self.rest = tail;
42
43        if segment.is_empty() {
44            return self.next(); // skip empty segments like "&&"
45        }
46
47        Some(segment.parse())
48    }
49}
50
51pub struct QueryParam<const KN: usize, const VN: usize> {
52    pub k: String<KN>,
53    pub v: String<VN>,
54}
55
56impl<const KN: usize, const VN: usize> QueryParam<KN, VN> {
57    pub fn entry(&self) -> (&String<KN>, &String<VN>) {
58        (&self.k, &self.v)
59    }
60}
61
62impl<const KN: usize, const VN: usize> core::str::FromStr for QueryParam<KN, VN> {
63    type Err = ParseError;
64
65    fn from_str(s: &str) -> Result<Self, Self::Err> {
66        let (k, v) = s.split_once('=').ok_or(ParseError::QueryNoEquals)?;
67
68        Ok(QueryParam {
69            k: unescape::<KN>(k)?,
70            v: unescape::<VN>(v)?,
71        })
72    }
73}
74
75#[derive(Debug, Clone, PartialEq)]
76pub struct RequestLine<const N: usize> {
77    pub method: Method,
78    pub target: String<N>,
79    pub protocol: Protocol,
80}
81
82#[derive(Debug, Clone, PartialEq)]
83pub enum Method {
84    GET,
85    POST,
86}
87
88#[derive(Debug, Clone, PartialEq)]
89pub enum Protocol {
90    HTTP1, // HTTP1.1
91}
92
93impl<const N: usize> RequestLine<N> {
94    pub fn query_params<'a, const KN: usize, const VN: usize>(&'a self) -> QueryParams<'a, KN, VN> {
95        let rest = self
96            .target
97            .as_str()
98            .split_once('?')
99            .map(|(_, q)| q)
100            .unwrap_or("");
101
102        QueryParams { rest }
103    }
104}
105
106impl<const N: usize> core::str::FromStr for RequestLine<N> {
107    type Err = ParseError;
108
109    fn from_str(s: &str) -> Result<Self, Self::Err> {
110        let line = s.lines().next().ok_or(ParseError::BadRequest)?;
111        let mut parts = line.split_ascii_whitespace();
112        let method_str = parts.next().ok_or(ParseError::BadRequest)?;
113        let target_str = parts.next().ok_or(ParseError::BadRequest)?;
114        let protocol_str = parts.next().ok_or(ParseError::BadRequest)?;
115
116        let method = match method_str {
117            "GET" => Method::GET,
118            "POST" => Method::POST,
119            _ => return Err(ParseError::UnsupportedMethod),
120        };
121
122        let target: String<N> = unescape(target_str)?;
123
124        let protocol = if protocol_str == "HTTP/1.1" {
125            Protocol::HTTP1
126        } else {
127            return Err(ParseError::UnsupportedProtocol);
128        };
129
130        Ok(RequestLine {
131            method,
132            target,
133            protocol,
134        })
135    }
136}
137
138/// `N` must be greater than or equal to the length of `escaped`.
139/// All escaped characters _must_ decode to valid __ascii__.
140/// The rest of Utf8 is not implemented.
141pub fn unescape<const N: usize>(escaped: &str) -> Result<String<N>, ParseError> {
142    let mut out = String::<N>::new();
143    let bytes = escaped.as_bytes();
144    let mut i = 0;
145    while i < bytes.len() {
146        match bytes[i] {
147            b'+' => {
148                out.push(' ')?;
149                i += 1;
150            }
151            b'%' if i + 2 < bytes.len() => {
152                if let (Some(hi), Some(lo)) =
153                    (hex_char_to_dec(bytes[i + 1]), hex_char_to_dec(bytes[i + 2]))
154                {
155                    let c = (hi << 4 | lo) as char;
156                    if !c.is_ascii() {
157                        return Err(ParseError::EncodedNonAscii);
158                    }
159                    out.push((hi << 4 | lo) as char)?;
160                    i += 3;
161                } else {
162                    out.push('%')?;
163                    i += 1;
164                }
165            }
166            b => {
167                out.push(b as char)?;
168                i += 1;
169            }
170        }
171    }
172
173    Ok(out)
174}
175
176fn hex_char_to_dec(b: u8) -> Option<u8> {
177    match b {
178        b'0'..=b'9' => Some(b - b'0'),
179        b'a'..=b'f' => Some(10 + b - b'a'),
180        b'A'..=b'F' => Some(10 + b - b'A'),
181        _ => None,
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use heapless::String;
188
189    use crate::{ParseError, RequestLine, hex_char_to_dec, unescape};
190
191    #[test]
192    fn test_unescape() {
193        let escaped = "%21%40%23%24%25%5E%26%2A%28%29123asd";
194        let unescaped: String<32> = unescape(escaped).unwrap();
195        assert_eq!(unescaped, "!@#$%^&*()123asd");
196
197        let non_ascii_str = "%C3B3";
198        let non_ascii = unescape::<32>(non_ascii_str);
199        assert_eq!(non_ascii, Err(ParseError::EncodedNonAscii));
200
201        let percent_near_end_str = "123abc%20987%f";
202        let percent_near_end: String<32> = unescape(percent_near_end_str).unwrap();
203        assert_eq!(percent_near_end, "123abc 987%f");
204    }
205
206    #[test]
207    fn test_hex_to_dec() {
208        assert_eq!(hex_char_to_dec(b'F'), Some(15));
209        assert_eq!(hex_char_to_dec(b'0'), Some(0));
210        assert_eq!(hex_char_to_dec(b'A'), Some(10));
211        assert_eq!(hex_char_to_dec(b'H'), None);
212        assert_eq!(hex_char_to_dec(0x0), None);
213    }
214
215    #[test]
216    fn request_line_get() {
217        let line = "GET /submit HTTP/1.1";
218
219        let parsed = line.parse::<RequestLine<32>>().unwrap();
220
221        let mut target: String<32> = String::new();
222        target.push_str("/submit").unwrap();
223        let expected = RequestLine {
224            method: crate::Method::GET,
225            target,
226            protocol: crate::Protocol::HTTP1,
227        };
228
229        assert_eq!(parsed, expected);
230    }
231
232    #[test]
233    fn request_line_post() {
234        let line = "POST / HTTP/1.1";
235
236        let parsed = line.parse::<RequestLine<32>>().unwrap();
237
238        let mut target: String<32> = String::new();
239        target.push_str("/").unwrap();
240        let expected = RequestLine {
241            method: crate::Method::POST,
242            target,
243            protocol: crate::Protocol::HTTP1,
244        };
245
246        assert_eq!(parsed, expected);
247    }
248
249    #[test]
250    fn request_line_with_params() {
251        let line = "GET /submit?name=http%20lite HTTP/1.1";
252
253        let parsed = line.parse::<RequestLine<32>>().unwrap();
254
255        let mut target: String<32> = String::new();
256        target.push_str("/submit?name=http lite").unwrap();
257        let expected = RequestLine {
258            method: crate::Method::GET,
259            target,
260            protocol: crate::Protocol::HTTP1,
261        };
262
263        assert_eq!(parsed, expected);
264    }
265
266    #[test]
267    fn iterate_query_params() {
268        let line: RequestLine<64> = "GET /search?q=hi&lang=en HTTP/1.1".parse().unwrap();
269        let mut params = line.query_params::<16, 16>();
270
271        let first = params.next().unwrap().unwrap();
272        assert_eq!(first.k.as_str(), "q");
273        assert_eq!(first.v.as_str(), "hi");
274
275        let second = params.next().unwrap().unwrap();
276        assert_eq!(second.k.as_str(), "lang");
277        assert_eq!(second.v.as_str(), "en");
278
279        assert!(params.next().is_none());
280    }
281}