foundry_common/tempo/
tip20.rs1use std::fmt;
4
5pub const TIP20_MAX_LOGO_URI_BYTES: usize = 256;
6pub const TIP20_ALLOWED_LOGO_URI_SCHEMES: &[&str] = &["https", "http", "ipfs", "data"];
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum Tip20LogoUriValidationError {
10 LogoURITooLong,
11 InvalidLogoURI,
12}
13
14impl fmt::Display for Tip20LogoUriValidationError {
15 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16 match self {
17 Self::LogoURITooLong => f.write_str("LogoURITooLong"),
18 Self::InvalidLogoURI => f.write_str("InvalidLogoURI"),
19 }
20 }
21}
22
23impl std::error::Error for Tip20LogoUriValidationError {}
24
25pub fn validate_tip20_logo_uri(uri: &str) -> Result<(), Tip20LogoUriValidationError> {
26 if uri.len() > TIP20_MAX_LOGO_URI_BYTES {
27 return Err(Tip20LogoUriValidationError::LogoURITooLong);
28 }
29
30 if uri.is_empty() {
31 return Ok(());
32 }
33
34 let Some((scheme, _)) = uri.split_once(':') else {
35 return Err(Tip20LogoUriValidationError::InvalidLogoURI);
36 };
37
38 let mut bytes = scheme.bytes();
39 if !bytes.next().is_some_and(|b| b.is_ascii_alphabetic())
40 || !bytes.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'+' | b'-' | b'.'))
41 || !TIP20_ALLOWED_LOGO_URI_SCHEMES
42 .iter()
43 .any(|allowed| scheme.eq_ignore_ascii_case(allowed))
44 {
45 return Err(Tip20LogoUriValidationError::InvalidLogoURI);
46 }
47
48 Ok(())
49}
50
51#[cfg(test)]
52mod tests {
53 use super::*;
54
55 #[test]
56 fn validates_empty_and_allowed_schemes_case_insensitively() {
57 for uri in [
58 "",
59 "https://example.com/logo.png",
60 "HTTP://example.com/logo.png",
61 "ipfs://bafybeigdyrzt",
62 "DATA:image/png;base64,abcd",
63 ] {
64 validate_tip20_logo_uri(uri).unwrap();
65 }
66 }
67
68 #[test]
69 fn rejects_invalid_schemes_and_overlong_values() {
70 for uri in [
71 "ftp://example.com/logo.png",
72 "1https://example.com/logo.png",
73 "https+foo://example.com/logo.png",
74 "example.com/logo.png",
75 ] {
76 assert_eq!(
77 validate_tip20_logo_uri(uri).unwrap_err(),
78 Tip20LogoUriValidationError::InvalidLogoURI
79 );
80 }
81
82 assert_eq!(
83 validate_tip20_logo_uri(&format!("https://{}", "a".repeat(249))).unwrap_err(),
84 Tip20LogoUriValidationError::LogoURITooLong
85 );
86 }
87
88 #[test]
89 fn validates_logo_uri_byte_length_boundaries() {
90 validate_tip20_logo_uri(&format!("https://{}", "a".repeat(248))).unwrap();
91
92 let multibyte = format!("https://{}", "é".repeat(124));
93 assert_eq!(multibyte.len(), TIP20_MAX_LOGO_URI_BYTES);
94 validate_tip20_logo_uri(&multibyte).unwrap();
95
96 let too_long = format!("https://{}é", "a".repeat(247));
97 assert_eq!(too_long.len(), TIP20_MAX_LOGO_URI_BYTES + 1);
98 assert_eq!(
99 validate_tip20_logo_uri(&too_long).unwrap_err(),
100 Tip20LogoUriValidationError::LogoURITooLong
101 );
102 }
103}