Skip to main content

actix_cloud_extra/
utils.rs

1use base64::{Engine as _, prelude::BASE64_STANDARD};
2use infer::Type;
3
4pub trait StringUtil {
5    /// Check whether given `s` is a hex string.
6    fn is_hex(&self) -> bool;
7
8    /// Escape `like` SQL in `s`.
9    /// Replace `\` to `\\`, `%` to `\%` and `_` to `\_`.
10    fn like_escape(&self) -> String;
11}
12
13impl<S> StringUtil for S
14where
15    S: AsRef<str>,
16{
17    /// Check whether given string is a hex string.
18    fn is_hex(&self) -> bool {
19        self.as_ref().chars().all(|c| c.is_ascii_hexdigit())
20    }
21
22    /// Escape `like` SQL in string.
23    fn like_escape(&self) -> String {
24        let mut s = self.as_ref().replace('\\', "\\\\"); // first replace \
25        s = s.replace('%', "\\%");
26        s.replace('_', "\\_")
27    }
28}
29
30pub struct DataUrl {
31    pub data: Vec<u8>,
32    pub mime: Type,
33}
34
35impl DataUrl {
36    pub fn encode(&self) -> String {
37        let data = BASE64_STANDARD.encode(&self.data);
38        format!("data:{};base64,{data}", self.mime)
39    }
40
41    pub fn decode<S: AsRef<str>>(data: S) -> Option<Self> {
42        let data: Vec<_> = data.as_ref().split(',').collect();
43        if data.len() == 2 {
44            BASE64_STANDARD.decode(data[1]).map_or(None, Self::new)
45        } else {
46            None
47        }
48    }
49
50    pub fn new(data: Vec<u8>) -> Option<Self> {
51        let mime = infer::get(&data);
52        mime.map(|mime| Self { data, mime })
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn is_hex_valid() {
62        assert!("0123456789abcdefABCDEF".is_hex());
63        assert!("".is_hex());
64    }
65
66    #[test]
67    fn is_hex_invalid() {
68        assert!(!"0x123".is_hex());
69        assert!(!"hello".is_hex());
70        assert!(!"12g4".is_hex());
71    }
72
73    #[test]
74    fn like_escape_basic() {
75        assert_eq!("plain".like_escape(), "plain");
76    }
77
78    #[test]
79    fn like_escape_special() {
80        assert_eq!("a%b_c\\d".like_escape(), "a\\%b\\_c\\\\d");
81    }
82
83    #[test]
84    fn like_escape_backslash_first() {
85        // `\` must be escaped first so it does not double-escape later inserts.
86        assert_eq!("%\\".like_escape(), "\\%\\\\");
87    }
88
89    #[test]
90    fn dataurl_roundtrip() {
91        // 8-byte PNG signature is enough for `infer` to detect image/png.
92        let png = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
93        let url = DataUrl::new(png.clone()).unwrap();
94        let encoded = url.encode();
95        let decoded = DataUrl::decode(&encoded).unwrap();
96        assert_eq!(decoded.data, png);
97        assert_eq!(decoded.mime.mime_type(), "image/png");
98    }
99
100    #[test]
101    fn dataurl_decode_bad() {
102        assert!(DataUrl::decode("no-comma-here").is_none());
103    }
104
105    #[test]
106    fn dataurl_new_unknown() {
107        // Empty / random bytes have no detectable mime.
108        assert!(DataUrl::new(Vec::new()).is_none());
109        assert!(DataUrl::new(vec![0x01, 0x02, 0x03]).is_none());
110    }
111}