radix_engine_interface/object_modules/metadata/models/
url.rs

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