http_request_target/
lib.rs

1//! HTTP/1.1 request-target (RFC 9112) parser.
2
3// #![no_std]
4
5extern crate alloc;
6
7use alloc::string::String;
8use core::str;
9
10mod error;
11
12use crate::error::ParseRequestTarget;
13
14/// See <https://datatracker.ietf.org/doc/html/rfc9112#name-request-target>.
15#[derive(Debug, Clone)]
16pub enum RequestTarget {
17    /// Origin form.
18    Origin(String),
19
20    /// ```plain
21    /// absolute-form = absolute-URI
22    /// GET http://www.example.org/pub/WWW/TheProject.html HTTP/1.1
23    /// ```
24    Absolute(String),
25
26    /// ```plain
27    /// authority-form = uri-host ":" port
28    /// CONNECT www.example.com:80 HTTP/1.1
29    /// ```
30    Authority(String),
31
32    /// Asterisk form.
33    ///
34    /// ```plain
35    /// asterisk-form = "*"
36    /// OPTIONS * HTTP/1.1
37    /// ```
38    Asterisk,
39}
40
41impl RequestTarget {
42    /// Parse request target from slice.
43    pub fn try_from_slice(slice: &[u8]) -> Result<Self, ParseRequestTarget> {
44        if slice.is_empty() {
45            return Err(ParseRequestTarget);
46        }
47
48        if slice == b"*" {
49            return Ok(Self::Asterisk);
50        }
51
52        let mut buf = String::new();
53
54        match () {
55            _ if memchr::memchr(b'/', slice).is_none() => {
56                let authority_form = parse_authority_form(slice, &mut buf)?;
57                Ok(Self::Authority(authority_form))
58            }
59
60            _ => {
61                parse_origin_form(slice, &mut buf).inspect_err(|_err| {
62                    eprintln!("so far: {buf}");
63                })?;
64
65                Ok(Self::Origin(buf))
66            }
67        }
68    }
69}
70
71/// ```plain
72/// GET /where?q=now HTTP/1.1
73///
74/// origin-form   = absolute-path [ "?" query ]
75/// absolute-path = 1*( "/" segment )
76/// segment       = *pchar
77/// pchar         = unreserved / pct-encoded / sub-delims / ":" / "@"
78/// unreserved    = ALPHA / DIGIT / "-" / "." / "_" / "~"
79/// pct-encoded   = "%" HEXDIG HEXDIG
80/// sub-delims    = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
81/// query         = *( pchar / "/" / "?" )
82/// ```
83fn parse_origin_form(slice: &[u8], buf: &mut String) -> Result<(), ParseRequestTarget> {
84    eprintln!("parse_origin_form");
85
86    match memchr::memchr(b'?', slice) {
87        // has query
88        Some(query_start) => {
89            eprintln!("parse_origin_form with query");
90            parse_path(&slice[..query_start], buf)?;
91            buf.push('?');
92            parse_query(&slice[query_start + 1..], buf)?;
93        }
94
95        // just a path
96        None => {
97            eprintln!("parse_origin_form just path");
98            parse_path(slice, buf)?;
99        }
100    };
101
102    Ok(())
103}
104
105fn parse_path(mut slice: &[u8], buf: &mut String) -> Result<(), ParseRequestTarget> {
106    eprintln!("parse_path");
107
108    if slice.is_empty() {
109        return Err(ParseRequestTarget);
110    }
111
112    while !slice.is_empty() {
113        if !slice.starts_with(b"/") {
114            return Err(ParseRequestTarget);
115        }
116
117        buf.push('/');
118        let seg_len = parse_segment(&slice[1..], buf)?;
119
120        // advance by (segment delimiter + contents)
121        slice = &slice[1 + seg_len..];
122    }
123
124    Ok(())
125}
126
127fn parse_segment(slice: &[u8], buf: &mut String) -> Result<usize, ParseRequestTarget> {
128    eprintln!("parse_segment buf={buf}");
129
130    if slice.is_empty() {
131        return Ok(0);
132    }
133
134    let mut iter = slice.iter().peekable();
135
136    let mut i = 0;
137
138    loop {
139        let Some(b) = iter.next_if(|&&b| b != b'/') else {
140            // stop consuming if next char is start-of-next-segment or EOL
141            break;
142        };
143
144        let consumed = match b {
145            // unreserved
146            b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'-' | b'.' | b'_' | b'~' => 1,
147
148            // pct-encoded
149            b'%' => {
150                // HEXDIGIT
151                match iter.next() {
152                    Some(b'0'..=b'9' | b'A'..=b'F') => {}
153                    Some(&b) => {
154                        eprintln!("invalid HEXDIGIT at {i} {}", b as char);
155                        return Err(ParseRequestTarget);
156                    }
157                    None => return Err(ParseRequestTarget),
158                }
159
160                // HEXDIGIT
161                match iter.next() {
162                    Some(b'0'..=b'9' | b'A'..=b'F') => {}
163                    Some(&b) => {
164                        eprintln!("invalid HEXDIGIT at {i} {}", b as char);
165                        return Err(ParseRequestTarget);
166                    }
167                    None => return Err(ParseRequestTarget),
168                }
169
170                3
171            }
172
173            // sub-delims
174            b'!' | b'$' | b'&' | b'\'' | b'(' | b')' | b'*' | b'+' | b',' | b';' | b'=' => 1,
175
176            _ => return Err(ParseRequestTarget),
177        };
178
179        i += consumed;
180    }
181
182    let segment = str::from_utf8(&slice[..i]).map_err(|_err| ParseRequestTarget)?;
183    buf.push_str(segment);
184
185    Ok(i)
186}
187
188fn parse_query(slice: &[u8], buf: &mut String) -> Result<(), ParseRequestTarget> {
189    eprintln!("parse_query");
190
191    let mut iter = slice.iter().peekable();
192
193    loop {
194        let Some(b) = iter.next_if(|&&b| b != b'#') else {
195            // stop consuming if next char is start-of-fragment or EOL
196            break;
197        };
198
199        match b {
200            // unreserved
201            b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'-' | b'.' | b'_' | b'~' => {}
202
203            // pct-encoded
204            b'%' => {
205                // HEXDIGIT
206                match iter.next() {
207                    Some(b'0'..=b'9' | b'A'..=b'F') => {}
208                    Some(&b) => {
209                        eprintln!("invalid HEXDIGIT {}", b as char);
210                        return Err(ParseRequestTarget);
211                    }
212                    None => return Err(ParseRequestTarget),
213                }
214
215                // HEXDIGIT
216                match iter.next() {
217                    Some(b'0'..=b'9' | b'A'..=b'F') => {}
218                    Some(&b) => {
219                        eprintln!("invalid HEXDIGIT {}", b as char);
220                        return Err(ParseRequestTarget);
221                    }
222                    None => return Err(ParseRequestTarget),
223                }
224            }
225
226            // sub-delims
227            b'!' | b'$' | b'&' | b'\'' | b'(' | b')' | b'*' | b'+' | b',' | b';' | b'=' => {}
228
229            // additional chars allowed in query
230            b'/' | b'?' => {}
231
232            _ => return Err(ParseRequestTarget),
233        }
234    }
235
236    let query = str::from_utf8(slice).map_err(|_err| ParseRequestTarget)?;
237    buf.push_str(query);
238
239    Ok(())
240}
241
242fn parse_authority_form(_slice: &[u8], _buf: &mut String) -> Result<String, ParseRequestTarget> {
243    todo!()
244}