Skip to main content

http/uri/
path.rs

1use std::convert::TryFrom;
2use std::str::FromStr;
3use std::{cmp, fmt, hash, str};
4
5use bytes::Bytes;
6
7use super::{ErrorKind, InvalidUri, MAX_LEN};
8use crate::byte_str::ByteStr;
9
10/// Represents the path component of a URI
11#[derive(Clone)]
12pub struct PathAndQuery {
13    pub(super) data: ByteStr,
14    pub(super) query: u16,
15}
16
17const NONE: u16 = u16::MAX;
18
19impl PathAndQuery {
20    // Not public while `bytes` is unstable.
21    pub(super) fn from_shared(mut src: Bytes) -> Result<Self, InvalidUri> {
22        let Scanned {
23            query,
24            fragment,
25            is_maybe_not_utf8,
26        } = scan_path_and_query(&src)?;
27
28        if let Some(i) = fragment {
29            src.truncate(i as usize);
30        }
31
32        let data = if is_maybe_not_utf8 {
33            ByteStr::from_utf8(src).map_err(|_| ErrorKind::InvalidUriChar)?
34        } else {
35            unsafe { ByteStr::from_utf8_unchecked(src) }
36        };
37
38        Ok(PathAndQuery { data, query })
39    }
40
41    /// Convert a `PathAndQuery` from a static string.
42    ///
43    /// This function will not perform any copying, however the string is
44    /// checked to ensure that it is valid.
45    ///
46    /// # Panics
47    ///
48    /// This function panics if the argument is an invalid path and query.
49    ///
50    /// # Examples
51    ///
52    /// ```
53    /// # use http::uri::*;
54    /// let v = PathAndQuery::from_static("/hello?world");
55    ///
56    /// assert_eq!(v.path(), "/hello");
57    /// assert_eq!(v.query(), Some("world"));
58    /// ```
59    #[inline]
60    pub const fn from_static(src: &'static str) -> Self {
61        match scan_path_and_query(src.as_bytes()) {
62            Ok(Scanned {
63                query,
64                fragment: None,
65                is_maybe_not_utf8: false,
66            }) => PathAndQuery {
67                data: ByteStr::from_static(src),
68                query,
69            },
70            // Yes, we reject fragments and non-utf8
71            _ => panic!("static str is not valid path"),
72        }
73    }
74
75    /// Attempt to convert a `Bytes` buffer to a `PathAndQuery`.
76    ///
77    /// This will try to prevent a copy if the type passed is the type used
78    /// internally, and will copy the data if it is not.
79    pub fn from_maybe_shared<T>(src: T) -> Result<Self, InvalidUri>
80    where
81        T: AsRef<[u8]> + 'static,
82    {
83        if_downcast_into!(T, Bytes, src, {
84            return PathAndQuery::from_shared(src);
85        });
86
87        PathAndQuery::try_from(src.as_ref())
88    }
89
90    pub(super) fn empty() -> Self {
91        PathAndQuery {
92            data: ByteStr::new(),
93            query: NONE,
94        }
95    }
96
97    pub(super) fn slash() -> Self {
98        PathAndQuery {
99            data: ByteStr::from_static("/"),
100            query: NONE,
101        }
102    }
103
104    pub(super) fn star() -> Self {
105        PathAndQuery {
106            data: ByteStr::from_static("*"),
107            query: NONE,
108        }
109    }
110
111    /// Returns the path component
112    ///
113    /// The path component is **case sensitive**.
114    ///
115    /// ```notrust
116    /// abc://username:password@example.com:123/path/data?key=value&key2=value2#fragid1
117    ///                                        |--------|
118    ///                                             |
119    ///                                           path
120    /// ```
121    ///
122    /// If the URI is `*` then the path component is equal to `*`.
123    ///
124    /// # Examples
125    ///
126    /// ```
127    /// # use http::uri::*;
128    ///
129    /// let path_and_query: PathAndQuery = "/hello/world".parse().unwrap();
130    ///
131    /// assert_eq!(path_and_query.path(), "/hello/world");
132    /// ```
133    #[inline]
134    pub fn path(&self) -> &str {
135        let ret = if self.query == NONE {
136            &self.data[..]
137        } else {
138            &self.data[..self.query as usize]
139        };
140
141        if ret.is_empty() {
142            return "/";
143        }
144
145        ret
146    }
147
148    /// Returns the query string component
149    ///
150    /// The query component contains non-hierarchical data that, along with data
151    /// in the path component, serves to identify a resource within the scope of
152    /// the URI's scheme and naming authority (if any). The query component is
153    /// indicated by the first question mark ("?") character and terminated by a
154    /// number sign ("#") character or by the end of the URI.
155    ///
156    /// ```notrust
157    /// abc://username:password@example.com:123/path/data?key=value&key2=value2#fragid1
158    ///                                                   |-------------------|
159    ///                                                             |
160    ///                                                           query
161    /// ```
162    ///
163    /// # Examples
164    ///
165    /// With a query string component
166    ///
167    /// ```
168    /// # use http::uri::*;
169    /// let path_and_query: PathAndQuery = "/hello/world?key=value&foo=bar".parse().unwrap();
170    ///
171    /// assert_eq!(path_and_query.query(), Some("key=value&foo=bar"));
172    /// ```
173    ///
174    /// Without a query string component
175    ///
176    /// ```
177    /// # use http::uri::*;
178    /// let path_and_query: PathAndQuery = "/hello/world".parse().unwrap();
179    ///
180    /// assert!(path_and_query.query().is_none());
181    /// ```
182    #[inline]
183    pub fn query(&self) -> Option<&str> {
184        if self.query == NONE {
185            None
186        } else {
187            let i = self.query + 1;
188            Some(&self.data[i as usize..])
189        }
190    }
191
192    /// Returns the path and query as a string component.
193    ///
194    /// # Examples
195    ///
196    /// With a query string component
197    ///
198    /// ```
199    /// # use http::uri::*;
200    /// let path_and_query: PathAndQuery = "/hello/world?key=value&foo=bar".parse().unwrap();
201    ///
202    /// assert_eq!(path_and_query.as_str(), "/hello/world?key=value&foo=bar");
203    /// ```
204    ///
205    /// Without a query string component
206    ///
207    /// ```
208    /// # use http::uri::*;
209    /// let path_and_query: PathAndQuery = "/hello/world".parse().unwrap();
210    ///
211    /// assert_eq!(path_and_query.as_str(), "/hello/world");
212    /// ```
213    #[inline]
214    pub fn as_str(&self) -> &str {
215        let ret = &self.data[..];
216        if ret.is_empty() {
217            return "/";
218        }
219        ret
220    }
221}
222
223impl TryFrom<&[u8]> for PathAndQuery {
224    type Error = InvalidUri;
225    #[inline]
226    fn try_from(s: &[u8]) -> Result<Self, Self::Error> {
227        PathAndQuery::from_shared(Bytes::copy_from_slice(s))
228    }
229}
230
231impl TryFrom<&str> for PathAndQuery {
232    type Error = InvalidUri;
233    #[inline]
234    fn try_from(s: &str) -> Result<Self, Self::Error> {
235        TryFrom::try_from(s.as_bytes())
236    }
237}
238
239impl TryFrom<Vec<u8>> for PathAndQuery {
240    type Error = InvalidUri;
241    #[inline]
242    fn try_from(vec: Vec<u8>) -> Result<Self, Self::Error> {
243        PathAndQuery::from_shared(vec.into())
244    }
245}
246
247impl TryFrom<String> for PathAndQuery {
248    type Error = InvalidUri;
249    #[inline]
250    fn try_from(s: String) -> Result<Self, Self::Error> {
251        PathAndQuery::from_shared(s.into())
252    }
253}
254
255impl TryFrom<&String> for PathAndQuery {
256    type Error = InvalidUri;
257    #[inline]
258    fn try_from(s: &String) -> Result<Self, Self::Error> {
259        TryFrom::try_from(s.as_bytes())
260    }
261}
262
263impl FromStr for PathAndQuery {
264    type Err = InvalidUri;
265    #[inline]
266    fn from_str(s: &str) -> Result<Self, InvalidUri> {
267        TryFrom::try_from(s)
268    }
269}
270
271impl fmt::Debug for PathAndQuery {
272    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
273        fmt::Display::fmt(self, f)
274    }
275}
276
277impl fmt::Display for PathAndQuery {
278    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
279        if !self.data.is_empty() {
280            match self.data.as_bytes()[0] {
281                b'/' | b'*' => fmt.write_str(&self.data),
282                _ => {
283                    fmt.write_str("/")?;
284                    fmt.write_str(&self.data)
285                }
286            }
287        } else {
288            fmt.write_str("/")
289        }
290    }
291}
292
293impl hash::Hash for PathAndQuery {
294    fn hash<H: hash::Hasher>(&self, state: &mut H) {
295        self.data.hash(state);
296    }
297}
298
299// ===== PartialEq / PartialOrd =====
300
301impl PartialEq for PathAndQuery {
302    #[inline]
303    fn eq(&self, other: &PathAndQuery) -> bool {
304        self.data == other.data
305    }
306}
307
308impl Eq for PathAndQuery {}
309
310impl PartialEq<str> for PathAndQuery {
311    #[inline]
312    fn eq(&self, other: &str) -> bool {
313        self.as_str() == other
314    }
315}
316
317impl PartialEq<PathAndQuery> for &str {
318    #[inline]
319    fn eq(&self, other: &PathAndQuery) -> bool {
320        self == &other.as_str()
321    }
322}
323
324impl PartialEq<&str> for PathAndQuery {
325    #[inline]
326    fn eq(&self, other: &&str) -> bool {
327        self.as_str() == *other
328    }
329}
330
331impl PartialEq<PathAndQuery> for str {
332    #[inline]
333    fn eq(&self, other: &PathAndQuery) -> bool {
334        self == other.as_str()
335    }
336}
337
338impl PartialEq<String> for PathAndQuery {
339    #[inline]
340    fn eq(&self, other: &String) -> bool {
341        self.as_str() == other.as_str()
342    }
343}
344
345impl PartialEq<PathAndQuery> for String {
346    #[inline]
347    fn eq(&self, other: &PathAndQuery) -> bool {
348        self.as_str() == other.as_str()
349    }
350}
351
352impl PartialOrd for PathAndQuery {
353    #[inline]
354    fn partial_cmp(&self, other: &PathAndQuery) -> Option<cmp::Ordering> {
355        self.as_str().partial_cmp(other.as_str())
356    }
357}
358
359impl PartialOrd<str> for PathAndQuery {
360    #[inline]
361    fn partial_cmp(&self, other: &str) -> Option<cmp::Ordering> {
362        self.as_str().partial_cmp(other)
363    }
364}
365
366impl PartialOrd<PathAndQuery> for str {
367    #[inline]
368    fn partial_cmp(&self, other: &PathAndQuery) -> Option<cmp::Ordering> {
369        self.partial_cmp(other.as_str())
370    }
371}
372
373impl PartialOrd<&str> for PathAndQuery {
374    #[inline]
375    fn partial_cmp(&self, other: &&str) -> Option<cmp::Ordering> {
376        self.as_str().partial_cmp(*other)
377    }
378}
379
380impl PartialOrd<PathAndQuery> for &str {
381    #[inline]
382    fn partial_cmp(&self, other: &PathAndQuery) -> Option<cmp::Ordering> {
383        self.partial_cmp(&other.as_str())
384    }
385}
386
387impl PartialOrd<String> for PathAndQuery {
388    #[inline]
389    fn partial_cmp(&self, other: &String) -> Option<cmp::Ordering> {
390        self.as_str().partial_cmp(other.as_str())
391    }
392}
393
394impl PartialOrd<PathAndQuery> for String {
395    #[inline]
396    fn partial_cmp(&self, other: &PathAndQuery) -> Option<cmp::Ordering> {
397        self.as_str().partial_cmp(other.as_str())
398    }
399}
400
401// Scanner implementation that is `const fn`, usable by both `from_static`
402// and `from_shared`.
403// =====
404
405struct Scanned {
406    query: u16,
407    fragment: Option<u16>,
408    is_maybe_not_utf8: bool,
409}
410
411// Per-byte character classes for the path and query scanners.
412const CLASS_VALID: u8 = 0;
413const CLASS_QUERY: u8 = 1;
414const CLASS_FRAGMENT: u8 = 2;
415const CLASS_HIGH: u8 = 3;
416const CLASS_INVALID: u8 = 4;
417
418const fn build_path_map() -> [u8; 256] {
419    let mut t = [CLASS_INVALID; 256];
420    let mut i = 0;
421    while i < 256 {
422        // See https://url.spec.whatwg.org/#path-state
423        t[i] = match i as u8 {
424            b'?' => CLASS_QUERY,
425            b'#' => CLASS_FRAGMENT,
426
427            // Bytes that don't need to be percent-encoded in the path.
428            0x21 | 0x24..=0x3B | 0x3D | 0x40..=0x5F | 0x61..=0x7A | 0x7C | 0x7E => CLASS_VALID,
429
430            // Potentially utf8, checked later.
431            0x80..=0xFF => CLASS_HIGH,
432
433            // Should be percent-encoded, but accepted for parity with clients
434            // that send them as-is (e.g. JSON embedded in the path).
435            b'"' | b'{' | b'}' => CLASS_VALID,
436
437            _ => CLASS_INVALID,
438        };
439        i += 1;
440    }
441    t
442}
443
444const fn build_query_map() -> [u8; 256] {
445    let mut t = [CLASS_INVALID; 256];
446    let mut i = 0;
447    while i < 256 {
448        // See https://url.spec.whatwg.org/#query-state
449        t[i] = match i as u8 {
450            b'#' => CLASS_FRAGMENT,
451
452            // Allowed: 0x21 / 0x24 - 0x3B / 0x3D / 0x3F - 0x7E
453            0x21 | 0x24..=0x3B | 0x3D | 0x3F..=0x7E => CLASS_VALID,
454
455            0x80..=0xFF => CLASS_HIGH,
456
457            _ => CLASS_INVALID,
458        };
459        i += 1;
460    }
461    t
462}
463
464const PATH_MAP: [u8; 256] = build_path_map();
465const QUERY_MAP: [u8; 256] = build_query_map();
466
467const fn scan_path_and_query(bytes: &[u8]) -> Result<Scanned, ErrorKind> {
468    let mut i = 0;
469    let mut query = NONE;
470    let mut fragment = None;
471
472    let mut is_maybe_not_utf8 = false;
473
474    if bytes.is_empty() {
475        return Err(ErrorKind::Empty);
476    }
477
478    if bytes.len() > MAX_LEN {
479        return Err(ErrorKind::TooLong);
480    }
481
482    if bytes.len() == 1 && bytes[0] == b'*' {
483        return Ok(Scanned {
484            query,
485            fragment,
486            is_maybe_not_utf8: false,
487        });
488    }
489
490    if !matches!(bytes[0], b'/' | b'?' | b'#') {
491        return Err(ErrorKind::PathDoesNotStartWithSlash);
492    }
493
494    while i < bytes.len() {
495        match PATH_MAP[bytes[i] as usize] {
496            CLASS_VALID => {}
497            CLASS_QUERY => {
498                debug_assert!(query == NONE);
499                query = i as u16;
500                i += 1;
501                break;
502            }
503            CLASS_FRAGMENT => {
504                fragment = Some(i as u16);
505                break;
506            }
507            CLASS_HIGH => {
508                is_maybe_not_utf8 = true;
509            }
510            _ => return Err(ErrorKind::InvalidUriChar),
511        }
512        i += 1;
513    }
514
515    // query ...
516    if query != NONE {
517        while i < bytes.len() {
518            match QUERY_MAP[bytes[i] as usize] {
519                CLASS_VALID => {}
520                CLASS_HIGH => {
521                    is_maybe_not_utf8 = true;
522                }
523                CLASS_FRAGMENT => {
524                    fragment = Some(i as u16);
525                    break;
526                }
527                _ => return Err(ErrorKind::InvalidUriChar),
528            }
529            i += 1;
530        }
531    }
532
533    Ok(Scanned {
534        query,
535        fragment,
536        is_maybe_not_utf8,
537    })
538}
539
540#[cfg(test)]
541mod tests {
542    use super::*;
543
544    #[test]
545    fn equal_to_self_of_same_path() {
546        let p1: PathAndQuery = "/hello/world&foo=bar".parse().unwrap();
547        let p2: PathAndQuery = "/hello/world&foo=bar".parse().unwrap();
548        assert_eq!(p1, p2);
549        assert_eq!(p2, p1);
550    }
551
552    #[test]
553    fn not_equal_to_self_of_different_path() {
554        let p1: PathAndQuery = "/hello/world&foo=bar".parse().unwrap();
555        let p2: PathAndQuery = "/world&foo=bar".parse().unwrap();
556        assert_ne!(p1, p2);
557        assert_ne!(p2, p1);
558    }
559
560    #[test]
561    fn equates_with_a_str() {
562        let path_and_query: PathAndQuery = "/hello/world&foo=bar".parse().unwrap();
563        assert_eq!(&path_and_query, "/hello/world&foo=bar");
564        assert_eq!("/hello/world&foo=bar", &path_and_query);
565        assert_eq!(path_and_query, "/hello/world&foo=bar");
566        assert_eq!("/hello/world&foo=bar", path_and_query);
567    }
568
569    #[test]
570    fn not_equal_with_a_str_of_a_different_path() {
571        let path_and_query: PathAndQuery = "/hello/world&foo=bar".parse().unwrap();
572        // as a reference
573        assert_ne!(&path_and_query, "/hello&foo=bar");
574        assert_ne!("/hello&foo=bar", &path_and_query);
575        // without reference
576        assert_ne!(path_and_query, "/hello&foo=bar");
577        assert_ne!("/hello&foo=bar", path_and_query);
578    }
579
580    #[test]
581    fn equates_with_a_string() {
582        let path_and_query: PathAndQuery = "/hello/world&foo=bar".parse().unwrap();
583        assert_eq!(path_and_query, "/hello/world&foo=bar".to_string());
584        assert_eq!("/hello/world&foo=bar".to_string(), path_and_query);
585    }
586
587    #[test]
588    fn not_equal_with_a_string_of_a_different_path() {
589        let path_and_query: PathAndQuery = "/hello/world&foo=bar".parse().unwrap();
590        assert_ne!(path_and_query, "/hello&foo=bar".to_string());
591        assert_ne!("/hello&foo=bar".to_string(), path_and_query);
592    }
593
594    #[test]
595    fn compares_to_self() {
596        let p1: PathAndQuery = "/a/world&foo=bar".parse().unwrap();
597        let p2: PathAndQuery = "/b/world&foo=bar".parse().unwrap();
598        assert!(p1 < p2);
599        assert!(p2 > p1);
600    }
601
602    #[test]
603    fn compares_with_a_str() {
604        let path_and_query: PathAndQuery = "/b/world&foo=bar".parse().unwrap();
605        // by ref
606        assert!(&path_and_query < "/c/world&foo=bar");
607        assert!("/c/world&foo=bar" > &path_and_query);
608        assert!(&path_and_query > "/a/world&foo=bar");
609        assert!("/a/world&foo=bar" < &path_and_query);
610
611        // by val
612        assert!(path_and_query < "/c/world&foo=bar");
613        assert!("/c/world&foo=bar" > path_and_query);
614        assert!(path_and_query > "/a/world&foo=bar");
615        assert!("/a/world&foo=bar" < path_and_query);
616    }
617
618    #[test]
619    fn compares_with_a_string() {
620        let path_and_query: PathAndQuery = "/b/world&foo=bar".parse().unwrap();
621        assert!(path_and_query < "/c/world&foo=bar".to_string());
622        assert!("/c/world&foo=bar".to_string() > path_and_query);
623        assert!(path_and_query > "/a/world&foo=bar".to_string());
624        assert!("/a/world&foo=bar".to_string() < path_and_query);
625    }
626
627    #[test]
628    fn ignores_valid_percent_encodings() {
629        assert_eq!("/a%20b", pq("/a%20b?r=1").path());
630        assert_eq!("qr=%31", pq("/a/b?qr=%31").query().unwrap());
631    }
632
633    #[test]
634    fn ignores_invalid_percent_encodings() {
635        assert_eq!("/a%%b", pq("/a%%b?r=1").path());
636        assert_eq!("/aaa%", pq("/aaa%").path());
637        assert_eq!("/aaa%", pq("/aaa%?r=1").path());
638        assert_eq!("/aa%2", pq("/aa%2").path());
639        assert_eq!("/aa%2", pq("/aa%2?r=1").path());
640        assert_eq!("qr=%3", pq("/a/b?qr=%3").query().unwrap());
641    }
642
643    #[test]
644    fn allow_utf8_in_path() {
645        assert_eq!("/🍕", pq("/🍕").path());
646    }
647
648    #[test]
649    fn allow_utf8_in_query() {
650        assert_eq!(Some("pizza=🍕"), pq("/test?pizza=🍕").query());
651    }
652
653    #[test]
654    fn rejects_invalid_utf8_in_path() {
655        PathAndQuery::try_from(&[b'/', 0xFF][..]).expect_err("reject invalid utf8");
656    }
657
658    #[test]
659    fn rejects_invalid_utf8_in_query() {
660        PathAndQuery::try_from(&[b'/', b'a', b'?', 0xFF][..]).expect_err("reject invalid utf8");
661    }
662
663    #[test]
664    fn rejects_empty_string() {
665        PathAndQuery::try_from("").expect_err("reject empty str");
666    }
667
668    #[test]
669    fn requires_starting_with_slash() {
670        PathAndQuery::try_from("sneaky").expect_err("reject missing slash");
671    }
672
673    #[test]
674    fn rejects_del_in_path() {
675        PathAndQuery::try_from(&[b'/', 0x7F][..]).expect_err("reject DEL");
676    }
677
678    #[test]
679    fn rejects_del_in_query() {
680        PathAndQuery::try_from(&[b'/', b'a', b'?', 0x7F][..]).expect_err("reject DEL");
681    }
682
683    #[test]
684    fn rejects_too_long_path_and_query() {
685        let path = format!("/{}?query", "a".repeat(MAX_LEN));
686        let err = PathAndQuery::try_from(path).expect_err("reject overly long path and query");
687        assert_eq!(err.0, ErrorKind::TooLong);
688    }
689
690    #[test]
691    fn accepts_max_length_path_and_query() {
692        let path = format!("/{}?", "a".repeat(MAX_LEN - 2));
693        let path_and_query = PathAndQuery::try_from(path).expect("accept maximum length");
694        assert_eq!(path_and_query.as_str().len(), MAX_LEN);
695        assert_eq!(path_and_query.query(), Some(""));
696    }
697
698    #[test]
699    fn json_is_fine() {
700        assert_eq!(
701            r#"/{"bread":"baguette"}"#,
702            pq(r#"/{"bread":"baguette"}"#).path()
703        );
704    }
705
706    fn pq(s: &str) -> PathAndQuery {
707        s.parse().expect(&format!("parsing {}", s))
708    }
709}