actix-router 0.5.1

Resource path matching and router
Documentation
use crate::ResourcePath;

use crate::Quoter;

thread_local! {
    static DEFAULT_QUOTER: Quoter = Quoter::new(b"", b"%/+");
}

#[derive(Debug, Clone, Default)]
pub struct Url {
    uri: http::Uri,
    path: Option<String>,
}

impl Url {
    #[inline]
    pub fn new(uri: http::Uri) -> Url {
        let path = DEFAULT_QUOTER.with(|q| q.requote_str_lossy(uri.path()));
        Url { uri, path }
    }

    #[inline]
    pub fn new_with_quoter(uri: http::Uri, quoter: &Quoter) -> Url {
        Url {
            path: quoter.requote_str_lossy(uri.path()),
            uri,
        }
    }

    /// Returns URI.
    #[inline]
    pub fn uri(&self) -> &http::Uri {
        &self.uri
    }

    /// Returns path.
    #[inline]
    pub fn path(&self) -> &str {
        match self.path {
            Some(ref path) => path,
            _ => self.uri.path(),
        }
    }

    #[inline]
    pub fn update(&mut self, uri: &http::Uri) {
        self.uri = uri.clone();
        self.path = DEFAULT_QUOTER.with(|q| q.requote_str_lossy(uri.path()));
    }

    #[inline]
    pub fn update_with_quoter(&mut self, uri: &http::Uri, quoter: &Quoter) {
        self.uri = uri.clone();
        self.path = quoter.requote_str_lossy(uri.path());
    }
}

impl ResourcePath for Url {
    #[inline]
    fn path(&self) -> &str {
        self.path()
    }
}

#[cfg(test)]
mod tests {
    use http::Uri;
    use std::convert::TryFrom;

    use super::*;
    use crate::{Path, ResourceDef};

    const PROTECTED: &[u8] = b"%/+";

    fn match_url(pattern: &'static str, url: impl AsRef<str>) -> Path<Url> {
        let re = ResourceDef::new(pattern);
        let uri = Uri::try_from(url.as_ref()).unwrap();
        let mut path = Path::new(Url::new(uri));
        assert!(re.capture_match_info(&mut path));
        path
    }

    fn percent_encode(data: &[u8]) -> String {
        data.iter().map(|c| format!("%{:02X}", c)).collect()
    }

    #[test]
    fn parse_url() {
        let re = "/user/{id}/test";

        let path = match_url(re, "/user/2345/test");
        assert_eq!(path.get("id").unwrap(), "2345");
    }

    #[test]
    fn protected_chars() {
        let re = "/user/{id}/test";

        let encoded = percent_encode(PROTECTED);
        let path = match_url(re, format!("/user/{}/test", encoded));
        // characters in captured segment remain unencoded
        assert_eq!(path.get("id").unwrap(), &encoded);

        // "%25" should never be decoded into '%' to guarantee the output is a valid
        // percent-encoded format
        let path = match_url(re, "/user/qwe%25/test");
        assert_eq!(path.get("id").unwrap(), "qwe%25");

        let path = match_url(re, "/user/qwe%25rty/test");
        assert_eq!(path.get("id").unwrap(), "qwe%25rty");
    }

    #[test]
    fn non_protected_ascii() {
        let non_protected_ascii = ('\u{0}'..='\u{7F}')
            .filter(|&c| c.is_ascii() && !PROTECTED.contains(&(c as u8)))
            .collect::<String>();
        let encoded = percent_encode(non_protected_ascii.as_bytes());
        let path = match_url("/user/{id}/test", format!("/user/{}/test", encoded));
        assert_eq!(path.get("id").unwrap(), &non_protected_ascii);
    }

    #[test]
    fn valid_utf8_multi_byte() {
        let test = ('\u{FF00}'..='\u{FFFF}').collect::<String>();
        let encoded = percent_encode(test.as_bytes());
        let path = match_url("/a/{id}/b", format!("/a/{}/b", &encoded));
        assert_eq!(path.get("id").unwrap(), &test);
    }

    #[test]
    fn invalid_utf8() {
        let invalid_utf8 = percent_encode((0x80..=0xff).collect::<Vec<_>>().as_slice());
        let uri = Uri::try_from(format!("/{}", invalid_utf8)).unwrap();
        let path = Path::new(Url::new(uri));

        // We should always get a valid utf8 string
        assert!(String::from_utf8(path.as_str().as_bytes().to_owned()).is_ok());
    }
}