cnfy-uint 0.2.3

Zero-dependency 256-bit unsigned integer arithmetic for cryptographic applications
Documentation
//! Hex string parsing via the [`FromStr`] trait.
use super::U512;
use core::fmt;
use core::str::FromStr;

/// Error type returned when parsing a hex string into a [`U512`] fails.
///
/// Possible failure reasons: empty input, invalid hex character, or
/// value exceeds 512 bits (more than 128 hex digits).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseU512Error;

impl fmt::Display for ParseU512Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("invalid hex string for U512")
    }
}

/// Parses a hexadecimal string into a [`U512`].
///
/// Accepts lowercase and uppercase hex digits (`0-9`, `a-f`, `A-F`).
/// An optional `0x` or `0X` prefix is stripped before parsing. Leading
/// zeros are permitted. The string must represent a value that fits in
/// 512 bits (at most 128 hex digits after stripping the prefix and
/// leading zeros).
///
/// # Examples
///
/// ```
/// use cnfy_uint::u512::U512;
///
/// let v: U512 = "ff".parse().unwrap();
/// assert_eq!(v, U512::from_be_limbs([0, 0, 0, 0, 0, 0, 0, 255]));
///
/// let v: U512 = "0xFF".parse().unwrap();
/// assert_eq!(v, U512::from_be_limbs([0, 0, 0, 0, 0, 0, 0, 255]));
/// ```
impl FromStr for U512 {
    type Err = ParseU512Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let hex = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")).unwrap_or(s);

        if hex.is_empty() {
            return Err(ParseU512Error);
        }

        // Strip leading zeros, but keep at least one digit
        let hex = hex.trim_start_matches('0');
        let hex = if hex.is_empty() { "0" } else { hex };

        if hex.len() > 128 {
            return Err(ParseU512Error);
        }

        // Pad to 128 hex chars and parse into bytes
        let mut bytes = [0u8; 64];
        let hex_bytes = hex.as_bytes();
        let offset = 128 - hex_bytes.len();

        let mut i = 0;
        while i < hex_bytes.len() {
            let nibble = match hex_bytes[i] {
                b'0'..=b'9' => hex_bytes[i] - b'0',
                b'a'..=b'f' => hex_bytes[i] - b'a' + 10,
                b'A'..=b'F' => hex_bytes[i] - b'A' + 10,
                _ => return Err(ParseU512Error),
            };

            let byte_idx = (offset + i) / 2;
            if (offset + i) % 2 == 0 {
                bytes[byte_idx] = nibble << 4;
            } else {
                bytes[byte_idx] |= nibble;
            }
            i += 1;
        }

        Ok(U512::from_be_bytes(bytes))
    }
}

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

    /// Parse "0" yields zero.
    #[test]
    fn zero() {
        assert_eq!("0".parse::<U512>().unwrap(), U512::ZERO);
    }

    /// Parse "1" yields one.
    #[test]
    fn one() {
        assert_eq!("1".parse::<U512>().unwrap(), U512::ONE);
    }

    /// Parse "ff" yields 255.
    #[test]
    fn lowercase_ff() {
        assert_eq!(
            "ff".parse::<U512>().unwrap(),
            U512::from_be_limbs([0, 0, 0, 0, 0, 0, 0, 255]),
        );
    }

    /// Parse "FF" yields 255.
    #[test]
    fn uppercase_ff() {
        assert_eq!(
            "FF".parse::<U512>().unwrap(),
            U512::from_be_limbs([0, 0, 0, 0, 0, 0, 0, 255]),
        );
    }

    /// Parse with 0x prefix.
    #[test]
    fn hex_prefix_lower() {
        assert_eq!(
            "0xff".parse::<U512>().unwrap(),
            U512::from_be_limbs([0, 0, 0, 0, 0, 0, 0, 255]),
        );
    }

    /// Parse with 0X prefix.
    #[test]
    fn hex_prefix_upper() {
        assert_eq!(
            "0XFF".parse::<U512>().unwrap(),
            U512::from_be_limbs([0, 0, 0, 0, 0, 0, 0, 255]),
        );
    }

    /// Leading zeros are stripped.
    #[test]
    fn leading_zeros() {
        assert_eq!(
            "000ff".parse::<U512>().unwrap(),
            U512::from_be_limbs([0, 0, 0, 0, 0, 0, 0, 255]),
        );
    }

    /// Parse all zeros.
    #[test]
    fn all_zeros() {
        assert_eq!("0000".parse::<U512>().unwrap(), U512::ZERO);
    }

    /// Parse MAX value (128 f's).
    #[test]
    fn max_value() {
        let hex = "f".repeat(128);
        assert_eq!(hex.parse::<U512>().unwrap(), U512::MAX);
    }

    /// 129 hex digits (without leading zeros) exceeds 512 bits.
    #[test]
    fn too_large() {
        let hex = format!("1{}", "0".repeat(128));
        assert_eq!(hex.parse::<U512>(), Err(ParseU512Error));
    }

    /// Empty string is an error.
    #[test]
    fn empty_string() {
        assert_eq!("".parse::<U512>(), Err(ParseU512Error));
    }

    /// Just "0x" with no digits is an error.
    #[test]
    fn prefix_only() {
        assert_eq!("0x".parse::<U512>(), Err(ParseU512Error));
    }

    /// Invalid character is an error.
    #[test]
    fn invalid_char() {
        assert_eq!("0xGG".parse::<U512>(), Err(ParseU512Error));
    }

    /// Round-trip with LowerHex formatting.
    #[test]
    fn round_trip_lower_hex() {
        let original = U512::from_be_limbs([0x1234, 0x5678, 0x9ABC, 0xDEF0, 1, 2, 3, 4]);
        let hex = format!("{:x}", original);
        let parsed: U512 = hex.parse().unwrap();
        assert_eq!(parsed, original);
    }

    /// Round-trip with 0x prefix formatting.
    #[test]
    fn round_trip_prefixed() {
        let original = U512::from_be_limbs([0x1234, 0x5678, 0x9ABC, 0xDEF0, 1, 2, 3, 4]);
        let hex = format!("{:#x}", original);
        let parsed: U512 = hex.parse().unwrap();
        assert_eq!(parsed, original);
    }

    /// Single digit parses correctly.
    #[test]
    fn single_digit() {
        assert_eq!(
            "a".parse::<U512>().unwrap(),
            U512::from_be_limbs([0, 0, 0, 0, 0, 0, 0, 10]),
        );
    }
}