radix_engine_interface/object_modules/metadata/models/
url.rs

1use crate::internal_prelude::*;
2#[cfg(feature = "fuzzing")]
3use arbitrary::Arbitrary;
4use lazy_static::lazy_static;
5use regex::Regex;
6
7lazy_static! {
8    /// This regular expressions only cover the most commonly used types of URLs.
9    ///
10    /// Based on https://en.wikipedia.org/wiki/URL#/media/File:URI_syntax_diagram.svg
11    ///
12    static ref URL_REGEX: Regex = Regex::new(
13        concat!(
14            // 1. Start
15            "^",
16            // 2. Schema, http or https only
17            "https?",
18            // 3. ://
19            ":\\/\\/",
20            // 4. Userinfo, not allowed
21            // 5. Host, ip address or host name
22            //    From https://stackoverflow.com/questions/106179/regular-expression-to-match-dns-hostname-or-ip-address
23            "(",
24                "((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))",
25                "|",
26                "((([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9]))",
27            ")",
28            // 6. Port number, optional
29            //    From https://stackoverflow.com/questions/12968093/regex-to-validate-port-number
30            "(:([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?",
31            // 7. Path, optional
32            //    * -+
33            //    * a-zA-Z0-9
34            //    * ()
35            //    * []
36            //    * @ : % _ . ~ & =
37            "(\\/[-\\+a-zA-Z0-9\\(\\)\\[\\]@:%_.~&=]*)*",
38            // 8. Query, optional
39            //    * -+
40            //    * a-zA-Z0-9
41            //    * ()
42            //    * []
43            //    * @ : % _ . ~ & =
44            //    * /
45            "(\\?[-\\+a-zA-Z0-9\\(\\)\\[\\]@:%_.~&=\\/]*)?",
46            // 9. Fragment, optional
47            //    * -+
48            //    * a-zA-Z0-9
49            //    * ()
50            //    * []
51            //    * @ : % _ . ~ & =
52            //    * /
53            "(#[-\\+a-zA-Z0-9\\(\\)\\[\\]@:%_.~&=\\/]*)?",
54            // 10. End
55            "$"
56        )
57    ).unwrap();
58}
59
60#[cfg_attr(feature = "fuzzing", derive(Arbitrary))]
61#[derive(
62    Debug, Clone, Eq, PartialEq, ManifestSbor, ScryptoCategorize, ScryptoEncode, ScryptoDecode,
63)]
64#[sbor(transparent)]
65pub struct UncheckedUrl(pub String);
66
67impl Describe<ScryptoCustomTypeKind> for UncheckedUrl {
68    const TYPE_ID: RustTypeId = RustTypeId::WellKnown(well_known_scrypto_custom_types::URL_TYPE);
69
70    fn type_data() -> ScryptoTypeData<RustTypeId> {
71        well_known_scrypto_custom_types::url_type_data()
72    }
73}
74
75impl UncheckedUrl {
76    pub fn of(value: impl AsRef<str>) -> Self {
77        Self(value.as_ref().to_owned())
78    }
79
80    pub fn as_str(&self) -> &str {
81        self.0.as_str()
82    }
83}
84
85pub struct CheckedUrl(String);
86
87impl CheckedUrl {
88    pub fn of(value: impl AsRef<str>) -> Option<Self> {
89        let s = value.as_ref();
90        if s.len() <= MAX_URL_LENGTH && URL_REGEX.is_match(s) {
91            Some(Self(s.to_owned()))
92        } else {
93            None
94        }
95    }
96
97    pub fn as_str(&self) -> &str {
98        self.0.as_str()
99    }
100}
101
102impl TryFrom<UncheckedUrl> for CheckedUrl {
103    type Error = ();
104
105    fn try_from(value: UncheckedUrl) -> Result<Self, Self::Error> {
106        CheckedUrl::of(value.as_str()).ok_or(())
107    }
108}
109
110impl From<CheckedUrl> for UncheckedUrl {
111    fn from(value: CheckedUrl) -> Self {
112        Self(value.0)
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn test_url() {
122        assert!(CheckedUrl::of("https://66.123.1.255:9999").is_some());
123        assert!(CheckedUrl::of("https://66.123.1.255:9999/hi").is_some());
124        assert!(CheckedUrl::of("https://www.google.com").is_some());
125        assert!(CheckedUrl::of("https://www.google.com/").is_some());
126        assert!(CheckedUrl::of("https://www.google.com/test/_abc/path").is_some());
127        assert!(CheckedUrl::of("https://www.google.com/test/_abc/path?").is_some());
128        assert!(CheckedUrl::of("https://www.google.com/test/_abc/path?abc=%12&def=test").is_some());
129        assert!(CheckedUrl::of("https://www.google.com/q?-+a-zA-Z0-9()[]@:%_.~&=/").is_some());
130        assert!(CheckedUrl::of("https://www.google.com/ /q").is_none());
131        assert!(CheckedUrl::of("https://username:password@www.google.com").is_none()); // not supported
132        assert!(CheckedUrl::of("https://www.google.com/path?#").is_some());
133        assert!(CheckedUrl::of("https://www.google.com/path?#-+a-zA-Z0-9()[]@:%_.~&=/").is_some());
134    }
135}