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