rama-net 0.3.0

rama network types and utilities
Documentation
#![expect(
    clippy::allow_attributes,
    reason = "macro-emitted `#[allow(dead_code)]` whose underlying lint fires only for some macro instantiations"
)]

use core::fmt;

use crate::std::borrow::ToOwned;
use crate::std::string::String;
use crate::std::vec;
use crate::std::vec::Vec;

use rama_core::error::BoxErrorExt as _;
use rama_core::error::{BoxError, ErrorContext};
use rama_utils::str::smol_str::SmolStr;

macro_rules! create_obf_type {
    ($name:ident, $val_fn:expr, $fix_lossy:expr) => {
        #[doc = concat!(stringify!($name), "used by Forwarded extension")]
        #[doc = ""]
        #[doc = "See <https://datatracker.ietf.org/doc/html/rfc7239#section-6>."]
        #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
        pub struct $name(SmolStr);

        impl $name {
            #[doc = concat!("Creates a [`", stringify!($name), "`] at compile time.")]
            #[doc = ""]
            #[doc = concat!("This function requires the static string to be a valid [`", stringify!($name), "`]")]
            ///
            /// # Panics
            ///
            /// This function panics at **compile time** when the static string is not a valid.
            pub const fn from_static(s: &'static str) -> Self {
                if !$val_fn(s.as_bytes()) {
                    panic!(concat!("static str is an invalid ", stringify!($name)));
                }
                Self(SmolStr::new_static(s))
            }

            #[doc = concat!("Try to convert a vector of bytes to a [`", stringify!($name), "`].")]
            pub fn try_from_bytes(vec: Vec<u8>) -> Result<Self, BoxError> {
                vec.try_into()
            }

            #[doc = concat!("Try to convert a string slice to a [`", stringify!($name), "`].")]
            pub fn try_from_str(s: &str) -> Result<Self, BoxError> {
                s.to_owned().try_into()
            }

            #[doc = concat!("Converts a vector of bytes to a [`", stringify!($name), "`], converting invalid characters to underscore.")]
            pub fn from_bytes_lossy(mut vec: Vec<u8>) -> Self {
                vec = $fix_lossy(vec);

                if vec.len() > OBF_MAX_LEN {
                    vec = vec.into_iter().take(OBF_MAX_LEN).collect();
                }

                for b in vec.iter_mut() {
                    if OBF_CHARS[*b as usize] == 0 {
                        *b = b'_'
                    }
                }

                vec.try_into().expect("sanitized bytes vec should always be correct")
            }

            #[doc = concat!("Converts a string slice to a [`", stringify!($name), "`], converting invalid characters to underscore.")]
            pub fn from_str_lossy(s: &str) -> Self {
                let vec = s.to_owned().into_bytes();
                Self::from_bytes_lossy(vec)
            }

            #[doc = concat!("Gets the [`", stringify!($name), "`] as reference.")]
            pub fn as_str(&self) -> &str {
                self.as_ref()
            }

            /// easier creation for other locs in this codebase where we are certain that data is pre-validated
            #[allow(dead_code, reason = "macro-emitted: used only by some instantiations; kept for symmetry across all generated obf types")]
            pub(super) fn from_inner(inner: SmolStr) -> Self {
                debug_assert!($val_fn(inner.as_bytes()));
                Self(inner)
            }
        }

        impl AsRef<str> for $name {
            fn as_ref(&self) -> &str {
                self.0.as_ref()
            }
        }

        impl fmt::Display for $name {
            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> core::fmt::Result {
                self.0.fmt(f)
            }
        }

        impl core::str::FromStr for $name {
            type Err = BoxError;

            fn from_str(s: &str) -> Result<Self, Self::Err> {
                $name::try_from(s.to_owned())
            }
        }

        impl TryFrom<String> for $name {
            type Error = BoxError;

            fn try_from(s: String) -> Result<Self, Self::Error> {
                if $val_fn(s.as_bytes()) {
                    Ok(Self(SmolStr::new(s)))
                } else {
                    Err(BoxError::from_static_str(concat!("invalid ", stringify!($name))))
                }
            }
        }

        impl TryFrom<Vec<u8>> for $name {
            type Error = BoxError;

            fn try_from(s: Vec<u8>) -> Result<Self, Self::Error> {
                if $val_fn(s.as_slice()) {
                    Ok(Self(SmolStr::new(
                        String::from_utf8(s).context(concat!("convert ", stringify!($name), "bytes to utf-8 string"))?,
                    )))
                } else {
                    Err(BoxError::from_static_str(concat!("invalid ", stringify!($name))))
                }
            }
        }

        impl PartialEq<str> for $name {
            fn eq(&self, other: &str) -> bool {
                self.0 == other
            }
        }

        impl PartialEq<&str> for $name {
            fn eq(&self, other: &&str) -> bool {
                self.0 == *other
            }
        }

        impl PartialEq<$name> for str {
            fn eq(&self, other: &$name) -> bool {
                other == self
            }
        }

        impl PartialEq<$name> for &str {
            #[inline(always)]
            fn eq(&self, other: &$name) -> bool {
                other == *self
            }
        }

        impl PartialEq<String> for $name {
            #[inline(always)]
            fn eq(&self, other: &String) -> bool {
                self.as_str() == other
            }
        }

        impl PartialEq<$name> for String {
            #[inline(always)]
            fn eq(&self, other: &$name) -> bool {
                other == self
            }
        }

        impl serde::Serialize for $name {
            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
            where
                S: serde::Serializer,
            {
                self.0.serialize(serializer)
            }
        }

        impl<'de> serde::Deserialize<'de> for $name {
            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
            where
                D: serde::Deserializer<'de>,
            {
                let s = <crate::std::borrow::Cow<'de, str>>::deserialize(deserializer)?;
                s.parse().map_err(serde::de::Error::custom)
            }
        }
    };
}

create_obf_type!(ObfNode, is_valid_obf_node, fix_obf_node);
create_obf_type!(ObfPort, is_valid_obf_port, fix_obf_port);

const fn is_valid_obf_port(s: &[u8]) -> bool {
    is_valid_obf_node(s) && s[0] == b'_'
}

fn fix_obf_port(mut vec: Vec<u8>) -> Vec<u8> {
    if vec.is_empty() {
        vec![b'_']
    } else if vec[0] != b'_' {
        vec.insert(0, b'_');
        vec
    } else {
        vec
    }
}

const fn is_valid_obf_node(s: &[u8]) -> bool {
    if s.is_empty() || s.len() > OBF_MAX_LEN {
        false
    } else {
        let mut i = 0;
        while i < s.len() {
            if OBF_CHARS[s[i] as usize] == 0 {
                return false;
            }
            i += 1;
        }
        true
    }
}

fn fix_obf_node(vec: Vec<u8>) -> Vec<u8> {
    if vec.is_empty() { vec![b'_'] } else { vec }
}

/// The maximum length of an obf string.
///
/// Not defined by spec, but might as well put a limit on it
const OBF_MAX_LEN: usize = 256;

// obfnode = 1*( ALPHA / DIGIT / "." / "_" / "-")
// obfport = "_" 1*(ALPHA / DIGIT / "." / "_" / "-")
//
// https://datatracker.ietf.org/doc/html/rfc7239#section-6
#[rustfmt::skip]
const OBF_CHARS: [u8; 256] = [
    //  0      1      2      3      4      5      6      7      8      9
        0,     0,     0,     0,     0,     0,     0,     0,     0,     0, //   x
        0,     0,     0,     0,     0,     0,     0,     0,     0,     0, //  1x
        0,     0,     0,     0,     0,     0,     0,     0,     0,     0, //  2x
        0,     0,     0,     0,     0,     0,     0,     0,     0,     0, //  3x
        0,     0,     0,     0,     0,  b'-',  b'.',     0,  b'0',  b'1', //  4x
     b'2',  b'3',  b'4',  b'5',  b'6',  b'7',  b'8',  b'9',     0,     0, //  5x
        0,     0,     0,     0,     0,  b'A',  b'B',  b'C',  b'D',  b'E', //  6x
     b'F',  b'G',  b'H',  b'I',  b'J',  b'K',  b'L',  b'M',  b'N',  b'O', //  7x
     b'P',  b'Q',  b'R',  b'S',  b'T',  b'U',  b'V',  b'W',  b'X',  b'Y', //  8x
     b'Z',     0,     0,     0,     0,  b'_',     0,  b'a',  b'b',  b'c', //  9x
     b'd',  b'e',  b'f',  b'g',  b'h',  b'i',  b'j',  b'k',  b'l',  b'm', // 10x
     b'n',  b'o',  b'p',  b'q',  b'r',  b's',  b't',  b'u',  b'v',  b'w', // 11x
     b'x',  b'y',  b'z',     0,     0,     0,     0,     0,     0,     0, // 12x
        0,     0,     0,     0,     0,     0,     0,     0,     0,     0, // 13x
        0,     0,     0,     0,     0,     0,     0,     0,     0,     0, // 14x
        0,     0,     0,     0,     0,     0,     0,     0,     0,     0, // 15x
        0,     0,     0,     0,     0,     0,     0,     0,     0,     0, // 16x
        0,     0,     0,     0,     0,     0,     0,     0,     0,     0, // 17x
        0,     0,     0,     0,     0,     0,     0,     0,     0,     0, // 18x
        0,     0,     0,     0,     0,     0,     0,     0,     0,     0, // 19x
        0,     0,     0,     0,     0,     0,     0,     0,     0,     0, // 20x
        0,     0,     0,     0,     0,     0,     0,     0,     0,     0, // 21x
        0,     0,     0,     0,     0,     0,     0,     0,     0,     0, // 22x
        0,     0,     0,     0,     0,     0,     0,     0,     0,     0, // 23x
        0,     0,     0,     0,     0,     0,     0,     0,     0,     0, // 24x
        0,     0,     0,     0,     0,     0                              // 25x
];

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_obf_node_parse_valid() {
        for str in [
            "_gazonk",
            "foo",
            "_foo-bar.baz",
            "-",
            "_",
            ".",
            "1",
            "a",
            "A",
            "-FoA-F-sdada_321A---",
        ] {
            let msg = format!("to parse: {str}");
            assert_eq!(ObfNode::try_from(str.to_owned()).expect(msg.as_str()), str);
            assert_eq!(
                ObfNode::try_from(str.as_bytes().to_vec()).expect(msg.as_str()),
                str
            );
        }
    }

    #[test]
    fn test_obf_node_parse_lossy() {
        for (str, expected) in [
            ("_gazonk", "_gazonk"),
            ("foo", "foo"),
            ("", "_"),
            ("@", "_"),
            ("wh@t", "wh_t"),
            ("😀", "____"),
            (
                "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz",
                "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuv",
            ),
        ] {
            assert_eq!(ObfNode::from_str_lossy(str), expected);
            assert_eq!(ObfNode::from_bytes_lossy(str.as_bytes().to_vec()), expected);
        }
    }

    #[test]
    fn test_obf_node_parse_invalid() {
        for str in [
            "",
            "@",
            "😀",
            "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz",
        ] {
            ObfNode::try_from(str.to_owned()).unwrap_err();
            ObfNode::try_from(str.as_bytes().to_vec()).unwrap_err();
        }
    }

    #[test]
    fn test_obf_port_parse_valid() {
        for str in [
            "_gazonk",
            "_83",
            "_foo-bar.baz",
            "_-",
            "_",
            "_.",
            "_1",
            "_a",
            "_A",
            "_-FoA-F-sdada_321A---",
        ] {
            let msg = format!("to parse: {str}");
            assert_eq!(ObfPort::try_from(str.to_owned()).expect(msg.as_str()), str);
            assert_eq!(
                ObfPort::try_from(str.as_bytes().to_vec()).expect(msg.as_str()),
                str
            );
        }
    }

    #[test]
    fn test_obf_port_parse_lossy() {
        for (str, expected) in [
            ("_gazonk", "_gazonk"),
            ("_83", "_83"),
            ("83", "_83"),
            ("-", "_-"),
            ("", "_"),
            ("@", "__"),
            ("wh@t", "_wh_t"),
            ("😀", "_____"),
            (
                "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz",
                "_abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstu",
            ),
        ] {
            assert_eq!(ObfPort::from_str_lossy(str), expected);
            assert_eq!(ObfPort::from_bytes_lossy(str.as_bytes().to_vec()), expected);
        }
    }

    #[test]
    fn test_obf_port_parse_invalid() {
        for str in [
            "",
            "-",
            "a",
            "1",
            "@",
            "😀",
            "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz",
        ] {
            ObfPort::try_from(str.to_owned()).unwrap_err();
            ObfPort::try_from(str.as_bytes().to_vec()).unwrap_err();
        }
    }
}