zero-trust-rps 0.0.5

Online Multiplayer Rock Paper Scissors
Documentation
use std::{
    fmt::{Debug, Display},
    str,
};

use serde::{de::Visitor, Deserialize, Serialize};

pub const HEX_DIGITS: [&str; 16] = [
    "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f",
];

#[expect(clippy::char_lit_as_u8)]
pub const fn parse_ascii_hex_digit(ch: char) -> Option<u8> {
    match ch {
        i if i.is_ascii_digit() => Some(i as u8 - ('0' as u8)),
        i if i.is_ascii_hexdigit() => Some(10 + i.to_ascii_lowercase() as u8 - ('a' as u8)),
        _ => None,
    }
}
mod test_hex_digit_parsing {
    use super::parse_ascii_hex_digit;
    const _: () = assert!(parse_ascii_hex_digit('g').is_none());
    const _: () = assert!(parse_ascii_hex_digit('ß').is_none());
    const _: () = assert!(matches!(parse_ascii_hex_digit('0'), Some(0)));
    const _: () = assert!(matches!(parse_ascii_hex_digit('1'), Some(1)));
    const _: () = assert!(matches!(parse_ascii_hex_digit('2'), Some(2)));
    const _: () = assert!(matches!(parse_ascii_hex_digit('3'), Some(3)));
    const _: () = assert!(matches!(parse_ascii_hex_digit('4'), Some(4)));
    const _: () = assert!(matches!(parse_ascii_hex_digit('5'), Some(5)));
    const _: () = assert!(matches!(parse_ascii_hex_digit('6'), Some(6)));
    const _: () = assert!(matches!(parse_ascii_hex_digit('7'), Some(7)));
    const _: () = assert!(matches!(parse_ascii_hex_digit('8'), Some(8)));
    const _: () = assert!(matches!(parse_ascii_hex_digit('9'), Some(9)));
    const _: () = assert!(matches!(parse_ascii_hex_digit('A'), Some(10)));
    const _: () = assert!(matches!(parse_ascii_hex_digit('B'), Some(11)));
    const _: () = assert!(matches!(parse_ascii_hex_digit('C'), Some(12)));
    const _: () = assert!(matches!(parse_ascii_hex_digit('D'), Some(13)));
    const _: () = assert!(matches!(parse_ascii_hex_digit('E'), Some(14)));
    const _: () = assert!(matches!(parse_ascii_hex_digit('F'), Some(15)));
    const _: () = assert!(matches!(parse_ascii_hex_digit('a'), Some(10)));
    const _: () = assert!(matches!(parse_ascii_hex_digit('b'), Some(11)));
    const _: () = assert!(matches!(parse_ascii_hex_digit('c'), Some(12)));
    const _: () = assert!(matches!(parse_ascii_hex_digit('d'), Some(13)));
    const _: () = assert!(matches!(parse_ascii_hex_digit('e'), Some(14)));
    const _: () = assert!(matches!(parse_ascii_hex_digit('f'), Some(15)));
    use super::HEX_DIGITS;
    #[allow(dead_code)]
    const fn verify_hex_digits() {
        let mut i = 0;
        loop {
            assert!(HEX_DIGITS[i].len() == 1);
            let bytes = HEX_DIGITS[i].as_bytes();
            assert!(bytes.len() == 1);
            let ch = bytes[0] as char;
            if let Some(val) = parse_ascii_hex_digit(ch) {
                assert!(val as usize == i);
            } else {
                panic!("could not parse")
            }
            assert!(ch.is_ascii_hexdigit());
            assert!(ch.is_ascii_digit() || ch.is_ascii_lowercase());

            i += 1;
            if i == HEX_DIGITS.len() {
                break;
            }
        }
    }
    const _: () = verify_hex_digits();
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub struct OwnedHexStr<const LEN: usize, const HLEN: usize> {
    hex_str: [u8; HLEN],
}

impl<const LEN: usize, const HLEN: usize> OwnedHexStr<LEN, HLEN> {
    const __76560: () = assert!(2 * LEN == HLEN);

    #[expect(clippy::char_lit_as_u8)]
    pub const fn from_bytes(value: [u8; LEN]) -> Self {
        let mut hex_str = [0; HLEN];

        let mut i = 0;

        loop {
            hex_str[i * 2] = match value[i] / 16 {
                i if i < 10 => '0' as u8 + i,
                i => 'a' as u8 - 10 + i,
            };
            hex_str[i * 2 + 1] = match value[i] % 16 {
                i if i < 10 => '0' as u8 + i,
                i => 'a' as u8 - 10 + i,
            };
            i += 1;
            if i == LEN {
                break;
            }
        }

        OwnedHexStr { hex_str }
    }

    #[expect(clippy::char_lit_as_u8)]
    #[allow(clippy::wrong_self_convention)]
    pub const fn into_original_bytes(&self) -> [u8; LEN] {
        let mut bytes = [0; LEN];

        let mut i = 0;

        loop {
            let upper: u8 = match self.hex_str[i * 2] {
                i if i.is_ascii_digit() => i - ('0' as u8),
                i => 10 + i - ('a' as u8),
            };
            let lower: u8 = match self.hex_str[i * 2 + 1] {
                i if i.is_ascii_digit() => i - ('0' as u8),
                i => 10 + i - ('a' as u8),
            };
            bytes[i] = (upper << 4) + lower;
            i += 1;
            if i == LEN {
                break;
            }
        }

        bytes
    }

    #[allow(dead_code)]
    pub(crate) const fn from_hex_str(string: &str) -> Self {
        let string = string.as_bytes();
        assert!(HLEN == string.len());

        let mut hex_str = [0; HLEN];

        let mut i = 0;

        loop {
            assert!(string[i].is_ascii_hexdigit());
            hex_str[i] = string[i];
            i += 1;
            if i == HLEN {
                break;
            }
        }

        OwnedHexStr { hex_str }
    }

    pub const fn as_str(&self) -> &str {
        #[cfg(debug_assertions)]
        {
            assert!(self.hex_str.is_ascii());
            let mut i = 0;
            loop {
                assert!(self.hex_str[i].is_ascii_hexdigit());
                i += 1;
                if i == HLEN {
                    break;
                }
            }
        }
        unsafe { str::from_utf8_unchecked(&self.hex_str) }
    }

    #[expect(clippy::needless_lifetimes)]
    #[allow(unused)]
    pub fn chars<'a>(&'a self) -> impl DoubleEndedIterator<Item = char> + 'a {
        self.as_str().chars()
    }
}

impl<I: Into<[u8; LEN]>, const LEN: usize, const HLEN: usize> From<I> for OwnedHexStr<LEN, HLEN> {
    fn from(value: I) -> Self {
        Self::from_bytes(value.into())
    }
}

mod test {
    use super::OwnedHexStr;
    use const_format::assertcp_eq;

    macro_rules! owned_hex_str {
        ($arr:expr) => {{
            assertcp_eq!(
                OwnedHexStr::<{ $arr.len() }, { $arr.len() * 2 }>::from_bytes($arr)
                    .into_original_bytes()[0],
                $arr[0]
            );
            OwnedHexStr::<{ $arr.len() }, { $arr.len() * 2 }>::from_bytes($arr)
        }};
    }

    assertcp_eq!(owned_hex_str!([175, 254]).as_str(), "affe");
    assertcp_eq!(owned_hex_str!([222, 173]).as_str(), "dead");
    assertcp_eq!(owned_hex_str!([42, 233]).as_str(), "2ae9");
    assertcp_eq!(owned_hex_str!([0, 255]).as_str(), "00ff");
    assertcp_eq!(owned_hex_str!([10, 9]).as_str(), "0a09");
    assertcp_eq!(owned_hex_str!([16, 15]).as_str(), "100f");
}

impl<const LEN: usize, const HLEN: usize> AsRef<str> for OwnedHexStr<LEN, HLEN> {
    fn as_ref(&self) -> &str {
        self.as_str()
    }
}

impl<const LEN: usize, const HLEN: usize> Display for OwnedHexStr<LEN, HLEN> {
    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        Display::fmt(&self.as_str(), fmt)
    }
}

impl<const LEN: usize, const HLEN: usize> Debug for OwnedHexStr<LEN, HLEN> {
    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        Debug::fmt(self.as_str(), fmt)
    }
}

impl<const LEN: usize, const HLEN: usize> Serialize for OwnedHexStr<LEN, HLEN> {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        serializer.serialize_str(self.as_str())
    }
}

impl<'de, const LEN: usize, const HLEN: usize> Deserialize<'de> for OwnedHexStr<LEN, HLEN> {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        struct VisitorImpl<const LEN: usize, const HLEN: usize>;
        #[expect(clippy::needless_lifetimes)]
        impl<'de, const LEN: usize, const HLEN: usize> Visitor<'de> for VisitorImpl<LEN, HLEN> {
            type Value = OwnedHexStr<LEN, HLEN>;

            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
                write!(formatter, "a string containing {} hex digits", HLEN)
            }

            fn visit_str<E>(self, val: &str) -> Result<Self::Value, E>
            where
                E: serde::de::Error,
            {
                if val.len() != HLEN {
                    Err(E::custom(format!(
                        "Expected length {HLEN} got length {}",
                        val.len()
                    )))
                } else if !val.is_ascii() {
                    Err(E::custom("Expected ASCII string"))
                } else if let Some(ch) = val.chars().find(|ch| !ch.is_ascii_hexdigit()) {
                    Err(E::custom(format!("Expected hex digit, got {ch:?}")))
                } else {
                    let mut hex_str = [0; HLEN];

                    for (i, b) in val.bytes().enumerate() {
                        hex_str[i] = b;
                    }

                    Ok(OwnedHexStr { hex_str })
                }
            }
        }
        deserializer.deserialize_string(VisitorImpl::<LEN, HLEN> {})
    }
}