uhttp_uri/
lib.rs

1//! This crate provides a barebone, zero-allocation parser for [HTTP
2//! URIs](https://tools.ietf.org/html/rfc7230#section-2.7) as they appear in a request
3//! header.
4//!
5//! In general, components are extracted along defined delimiters, but further validation
6//! and processing (such as percent decoding, query decoding, and punycode decoding) is
7//! left to higher layers. In the pursuit of simplicity, this crate also contains no
8//! support for generic and non-http URIs such as `file:` and `ftp://` – only the reduced
9//! syntax for [`http://`](https://tools.ietf.org/html/rfc7230#section-2.7.1) and
10//! [`https://`](https://tools.ietf.org/html/rfc7230#section-2.7.2) schemes is
11//! implemented.
12//!
13//! ## Example
14//!
15//! ```rust
16//! use uhttp_uri::{HttpUri, HttpResource, HttpScheme};
17//!
18//! let uri = HttpUri::new("https://example.com:443/r/rarepuppers?k=v&v=k#top").unwrap();
19//! assert_eq!(uri.scheme, HttpScheme::Https);
20//! assert_eq!(uri.authority, "example.com:443");
21//! assert_eq!(uri.resource.path, "/r/rarepuppers");
22//! assert_eq!(uri.resource.query, Some("k=v&v=k"));
23//! assert_eq!(uri.resource.fragment, Some("top"));
24//!
25//! let res = HttpResource::new("/shittydogs?lang=en");
26//! assert_eq!(res.path, "/shittydogs");
27//! assert_eq!(res.query, Some("lang=en"));
28//! assert_eq!(res.fragment, None);
29//! ```
30
31/// Components in an HTTP Request Line URI.
32#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
33pub struct HttpUri<'a> {
34    /// HTTP scheme of request.
35    ///
36    /// This is automatically parsed to an `HttpScheme` since RFC7230 only gives syntax for
37    /// the two http schemes.
38    pub scheme: HttpScheme,
39
40    /// Authority for the URI's target resource.
41    ///
42    /// This should typically be a domain name or IP address and may also contain a
43    /// username and port.
44    pub authority: &'a str,
45
46    /// Path and parameters for requested target resource.
47    pub resource: HttpResource<'a>,
48}
49
50impl<'a> HttpUri<'a> {
51    /// Try to parse the given string into `HttpUri` components.
52    ///
53    /// The string must contain no whitespace, as required by
54    /// [RFC7230§3.1.1](https://tools.ietf.org/html/rfc7230#section-3.1.1).
55    pub fn new(s: &'a str) -> Result<Self, ()> {
56        let (scheme, rest) = match s.find("://") {
57            Some(idx) => s.split_at(idx),
58            None => return Err(()),
59        };
60
61        let scheme = scheme.parse()?;
62        let rest = &rest[3..];
63
64        let (authority, rest) = match rest.find('/') {
65            Some(idx) => rest.split_at(idx),
66            None => (rest, ""),
67        };
68
69        if authority.is_empty() {
70            return Err(());
71        }
72
73        Ok(HttpUri {
74            scheme: scheme,
75            authority: authority,
76            resource: HttpResource::new(rest),
77        })
78    }
79
80}
81
82/// Writes the URI in the format required by [RFC7230§2.7.1]/[RFC7230§2.7.2].
83impl<'a> std::fmt::Display for HttpUri<'a> {
84    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
85        write!(f, "{}://{}{}", self.scheme, self.authority, self.resource)
86    }
87}
88
89/// Components in an HTTP URI resource.
90#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
91pub struct HttpResource<'a> {
92    /// Path to the resource.
93    ///
94    /// This is guaranteed to be nonempty (it will contain at least `"/"`.)
95    pub path: &'a str,
96
97    /// Parameters used to further identify the resource.
98    pub query: Option<&'a str>,
99
100    /// Identifier and parameters for a subresource.
101    pub fragment: Option<&'a str>,
102}
103
104impl<'a> HttpResource<'a> {
105    /// Parse the given string into a new `HttpResource`.
106    pub fn new(s: &'a str) -> Self {
107        let (path, query, fragment) = parts(s, s.find('?'), s.find('#'));
108
109        HttpResource {
110            path: if path.is_empty() {
111                "/"
112            } else {
113                path
114            },
115            query: if query.is_empty() {
116                None
117            } else {
118                Some(query)
119            },
120            fragment: if fragment.is_empty() {
121                None
122            } else {
123                Some(fragment)
124            }
125        }
126    }
127}
128
129impl<'a> std::fmt::Display for HttpResource<'a> {
130    fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
131        try!(fmt.write_str(self.path));
132
133        if let Some(q) = self.query {
134            try!(write!(fmt, "?{}", q));
135        }
136
137        if let Some(f) = self.fragment {
138            try!(write!(fmt, "#{}", f));
139        }
140
141        Ok(())
142    }
143}
144
145/// Split URI into path, query, and fragment [RFC3986§3].
146fn parts<'a>(s: &'a str, qidx: Option<usize>, fidx: Option<usize>)
147    -> (&'a str, &'a str, &'a str)
148{
149    match (qidx, fidx) {
150        (Some(q), Some(f)) => if q < f {
151            let (path, query) = (&s[..f]).split_at(q);
152            let (_, frag) = s.split_at(f);
153
154            (path, &query[1..], &frag[1..])
155        } else {
156            parts(s, None, Some(f))
157        },
158        (Some(q), None) => {
159            let (path, query) = s.split_at(q);
160            (path, &query[1..], "")
161        },
162        (None, Some(f)) => {
163            let (path, frag) = s.split_at(f);
164            (path, "", &frag[1..])
165        },
166        (None, None) => {
167            (s, "", "")
168        },
169    }
170}
171
172/// Schemes for HTTP requests.
173#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
174pub enum HttpScheme {
175    /// Plaintext http.
176    Http,
177    /// Secure http.
178    Https,
179}
180
181impl std::str::FromStr for HttpScheme {
182    type Err = ();
183
184    fn from_str(s: &str) -> Result<Self, Self::Err> {
185        match s {
186            "http" => Ok(HttpScheme::Http),
187            "https" => Ok(HttpScheme::Https),
188            _ => Err(()),
189        }
190    }
191}
192
193impl std::fmt::Display for HttpScheme {
194    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
195        f.write_str(match *self {
196            HttpScheme::Http => "http",
197            HttpScheme::Https => "https",
198        })
199    }
200}
201
202#[cfg(test)]
203mod test {
204    use super::*;
205
206    #[test]
207    fn test_http_resource() {
208        assert_eq!(HttpResource::new("/a/b/c"),
209            HttpResource {
210                path: "/a/b/c",
211                query: None,
212                fragment: None,
213            });
214
215        assert_eq!(HttpResource::new("/a/b/c?key=val"),
216            HttpResource {
217                path: "/a/b/c",
218                query: Some("key=val"),
219                fragment: None,
220            });
221
222        assert_eq!(HttpResource::new("/a/b/c#frag"),
223            HttpResource {
224                path: "/a/b/c",
225                query: None,
226                fragment: Some("frag"),
227            });
228
229        assert_eq!(HttpResource::new("/a/b/c#frag?frag-param"),
230            HttpResource {
231                path: "/a/b/c",
232                query: None,
233                fragment: Some("frag?frag-param"),
234            });
235
236        assert_eq!(HttpResource::new("/a/b/c?key=val&param#frag"),
237            HttpResource {
238                path: "/a/b/c",
239                query: Some("key=val&param"),
240                fragment: Some("frag"),
241            });
242
243        assert_eq!(HttpResource::new("/a/b/c/?key=val&param#frag"),
244            HttpResource {
245                path: "/a/b/c/",
246                query: Some("key=val&param"),
247                fragment: Some("frag"),
248            });
249
250        assert_eq!(HttpResource::new("/a/b/c?key=d/e#frag/ment?param"),
251            HttpResource {
252                path: "/a/b/c",
253                query: Some("key=d/e"),
254                fragment: Some("frag/ment?param"),
255            });
256
257        assert_eq!(HttpResource::new("/a/b/c#frag?param&key=val"),
258            HttpResource {
259                path: "/a/b/c",
260                query: None,
261                fragment: Some("frag?param&key=val"),
262            });
263
264        assert_eq!(HttpResource::new("/%02/%03/%04#frag"),
265            HttpResource {
266                path: "/%02/%03/%04",
267                query: None,
268                fragment: Some("frag"),
269            });
270
271        assert_eq!(HttpResource::new("/"),
272            HttpResource {
273                path: "/",
274                query: None,
275                fragment: None,
276            });
277
278        assert_eq!(HttpResource::new(""),
279            HttpResource {
280                path: "/",
281                query: None,
282                fragment: None,
283            });
284
285        assert_eq!(HttpResource::new("?#"),
286            HttpResource {
287                path: "/",
288                query: None,
289                fragment: None,
290            });
291
292        assert_eq!(HttpResource::new("?key=val#"),
293            HttpResource {
294                path: "/",
295                query: Some("key=val"),
296                fragment: None,
297            });
298
299        assert_eq!(HttpResource::new("?#frag"),
300            HttpResource {
301                path: "/",
302                query: None,
303                fragment: Some("frag"),
304            });
305    }
306
307    #[test]
308    fn test_http_uri() {
309        assert_eq!(HttpUri::new("http://example.com").unwrap(),
310            HttpUri {
311                scheme: HttpScheme::Http,
312                authority: "example.com",
313                resource: HttpResource {
314                    path: "/",
315                    query: None,
316                    fragment: None,
317                }
318            });
319
320        assert_eq!(HttpUri::new("http://127.0.0.1:61761/chunks").unwrap(),
321            HttpUri {
322                scheme: HttpScheme::Http,
323                authority: "127.0.0.1:61761",
324                resource: HttpResource {
325                    path: "/chunks",
326                    query: None,
327                    fragment: None,
328                }
329            });
330
331        assert_eq!(HttpUri::new("https://127.0.0.1:61761").unwrap(),
332            HttpUri {
333                scheme: HttpScheme::Https,
334                authority:  "127.0.0.1:61761",
335                resource: HttpResource {
336                    path: "/",
337                    query: None,
338                    fragment: None,
339                }
340            });
341
342        assert!(HttpUri::new("http://").is_err());
343        assert!(HttpUri::new("http:///").is_err());
344        assert!(HttpUri::new("://example.com").is_err());
345        assert!(HttpUri::new("ftp://example.com").is_err());
346        assert!(HttpUri::new("file:example").is_err());
347        assert!(HttpUri::new("htt:p//host").is_err());
348        assert!(HttpUri::new("hyper.rs/").is_err());
349        assert!(HttpUri::new("hyper.rs?key=val").is_err());
350
351        assert_eq!(HttpUri::new("http://test.com/nazghul?test=3").unwrap(),
352            HttpUri {
353                scheme: HttpScheme::Http,
354                authority: "test.com",
355                resource: HttpResource {
356                    path: "/nazghul",
357                    query: Some("test=3"),
358                    fragment: None,
359                }
360            });
361    }
362}