did_toolkit/
string.rs

1use anyhow::anyhow;
2
3/// Implements percent-encoding of byte arrays. It is not suggested, despite it's public access,
4/// that you use this function. Instead, feed the byte array directly to the member data for the
5/// type you wish to have encoded, it will do it automatically on output.
6///
7/// Encode portions of the URL according to <https://www.w3.org/TR/did-core/#did-syntax>
8#[inline]
9pub fn url_encoded(input: &[u8]) -> String {
10    url_encoded_internal(input, true)
11}
12
13#[inline]
14/// Encode the method_id, which has slightly different rules surrounding the colon.
15pub(crate) fn method_id_encoded(input: &[u8]) -> String {
16    url_encoded_internal(input, false)
17}
18
19#[inline]
20fn url_encoded_internal(input: &[u8], escape_colon: bool) -> String {
21    let mut ret: Vec<u8> = Vec::new();
22
23    for idx in input {
24        match *idx as char {
25            '0'..='9' | 'A'..='Z' | 'a'..='z' | '.' | '-' | '_' => ret.push(*idx),
26            ':' => {
27                if escape_colon {
28                    for i in format!("%{:02X}", idx).bytes() {
29                        ret.push(i)
30                    }
31                } else {
32                    ret.push(*idx)
33                }
34            }
35            _ => {
36                for i in format!("%{:02X}", idx).bytes() {
37                    ret.push(i)
38                }
39            }
40        }
41    }
42
43    String::from_utf8(ret).unwrap()
44}
45
46/// Decode portions of the URL according to <https://www.w3.org/TR/did-core/#did-syntax>
47#[inline]
48pub(crate) fn url_decoded(s: &[u8]) -> Vec<u8> {
49    let mut hexval: u8 = 0;
50    let mut hexleft = true;
51    let mut ret = Vec::new();
52    let mut in_pct = false;
53
54    for idx in s {
55        match *idx as char {
56            '%' => in_pct = true,
57            '0'..='9' | 'a'..='f' | 'A'..='F' => {
58                if in_pct {
59                    let val: u8 = (*idx as char).to_digit(16).unwrap() as u8;
60
61                    hexval |= if hexleft { val << 4 } else { val };
62
63                    if hexleft {
64                        hexleft = false;
65                    } else {
66                        ret.push(hexval);
67                        in_pct = false;
68                        hexleft = true;
69                        hexval = 0;
70                    }
71                } else {
72                    ret.push(*idx)
73                }
74            }
75            _ => ret.push(*idx),
76        }
77    }
78
79    ret
80}
81
82/// Validate method names fit within the proper ASCII range according to
83/// https://www.w3.org/TR/did-core/#did-syntax. Return an error if any characters fall outside of
84/// it.
85#[inline]
86pub(crate) fn validate_method_name(s: &[u8]) -> Result<(), anyhow::Error> {
87    for idx in s {
88        if !(&0x61..=&0x7a).contains(&idx) && !('0'..='9').contains(&(*idx as char)) {
89            return Err(anyhow!(
90                "Method name has invalid characters (not in 0x61 - 0x7a)"
91            ));
92        }
93    }
94
95    Ok(())
96}
97
98mod tests {
99    #[test]
100    fn test_encode_decode() {
101        let encoded = super::url_encoded("text with spaces".as_bytes());
102        assert_eq!(encoded, String::from("text%20with%20spaces"));
103        assert_eq!(
104            super::url_decoded(encoded.as_bytes()),
105            "text with spaces".as_bytes()
106        );
107    }
108
109    #[test]
110    fn test_battery_encode() {
111        use rand::Fill;
112
113        let mut rng = rand::rng();
114
115        for _ in 1..100000 {
116            let mut array: [u8; 100] = [0; 100];
117            array.fill(&mut rng);
118            let encoded = super::url_encoded(&array);
119            assert_eq!(super::url_decoded(encoded.as_bytes()), array, "{}", encoded);
120        }
121    }
122
123    #[test]
124    fn test_validate_method_name() {
125        assert!(super::validate_method_name("erik".as_bytes()).is_ok());
126        assert!(super::validate_method_name("not valid".as_bytes()).is_err());
127    }
128}