schemars/
encoding.rs

1use crate::_alloc_prelude::*;
2use alloc::borrow::Cow;
3use core::fmt::Write as _;
4
5/// Encodes a string for insertion into a JSON Pointer in URI fragment representation.
6pub fn encode_ref_name(name: &str) -> Cow<str> {
7    fn needs_encoding(byte: u8) -> bool {
8        match byte {
9            // `~` and `/` need encoding for JSON Pointer
10            // See https://datatracker.ietf.org/doc/html/rfc6901#section-3
11            b'~' | b'/' => true,
12            // These chars (and `~`) are valid in URL fragment
13            // See https://datatracker.ietf.org/doc/html/rfc3986/#section-3.5
14            b'!' | b'$' | b'&'..=b';' | b'=' | b'?'..=b'Z' | b'_' | b'a'..=b'z' => false,
15            // Everything else needs percent-encoding
16            _ => true,
17        }
18    }
19
20    if name.bytes().any(needs_encoding) {
21        let mut buf = String::new();
22
23        for byte in name.bytes() {
24            if byte == b'~' {
25                buf.push_str("~0");
26            } else if byte == b'/' {
27                buf.push_str("~1");
28            } else if needs_encoding(byte) {
29                write!(buf, "%{byte:2X}").unwrap();
30            } else {
31                buf.push(byte as char);
32            }
33        }
34
35        Cow::Owned(buf)
36    } else {
37        Cow::Borrowed(name)
38    }
39}
40
41/// Percent-decodes the given string, returning `None` if it results in invalid UTF-8.
42/// A `%` that is not followed by two hex digits is treated as a literal `%`.
43pub fn percent_decode(s: &str) -> Option<Cow<str>> {
44    if s.contains('%') {
45        let mut buf = Vec::<u8>::new();
46
47        let mut segments = s.split('%');
48        buf.extend(segments.next().unwrap_or_default().as_bytes());
49
50        for segment in segments {
51            if let Some(decoded_byte) = segment
52                .get(0..2)
53                .and_then(|p| u8::from_str_radix(p, 16).ok())
54            {
55                buf.push(decoded_byte);
56                buf.extend(&segment.as_bytes()[2..]);
57            } else {
58                buf.push(b'%');
59                buf.extend(segment.as_bytes());
60            }
61        }
62
63        String::from_utf8(buf).ok().map(Cow::Owned)
64    } else {
65        Some(Cow::Borrowed(s))
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[test]
74    fn test_encode_ref_name() {
75        assert_eq!(encode_ref_name("Simple!"), "Simple!");
76        assert_eq!(
77            encode_ref_name("Needs %-encoding 🚀"),
78            "Needs%20%25-encoding%20%F0%9F%9A%80"
79        );
80        assert_eq!(
81            encode_ref_name("aA0-._!$&'()*+,;=:@?"),
82            "aA0-._!$&'()*+,;=:@?",
83        );
84        assert_eq!(encode_ref_name("\"£%^\\~/"), "%22%C2%A3%25%5E%5C~0~1",);
85    }
86
87    #[test]
88    fn test_percent_decode() {
89        assert_eq!(percent_decode("Simple!"), Some("Simple!".into()));
90        assert_eq!(
91            percent_decode("Needs %-encoding 🚀"),
92            Some("Needs %-encoding 🚀".into())
93        );
94        assert_eq!(
95            percent_decode("Needs%20%25-encoding%20%F0%9F%9A%80"),
96            Some("Needs %-encoding 🚀".into())
97        );
98        assert_eq!(
99            percent_decode("aA0-._!$&'()*+,;=:@?"),
100            Some("aA0-._!$&'()*+,;=:@?".into())
101        );
102        assert_eq!(percent_decode("\"£%^\\~/"), Some("\"£%^\\~/".into()));
103        assert_eq!(
104            percent_decode("%22%C2%A3%25%5E%5C~0~1"),
105            Some("\"£%^\\~0~1".into())
106        );
107        assert_eq!(percent_decode("%%%2020%%%"), Some("%% 20%%%".into()));
108        assert_eq!(percent_decode("%f0%9F%9a%80"), Some("🚀".into()));
109        assert_eq!(percent_decode("%F0%9F%9A"), None);
110    }
111}