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(); }
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, }
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
138pub 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}