cmpa 0.1.6

Multiprecision arithmetic primitives commonly needed for asymmetric cryptography
Documentation
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023, 2026 SUSE LLC
// Author: Nicolai Stange <nstange@suse.de>

//! Conversion between byte arrays and hexadecimal strings.
//!
//! The byte arrays hold big-endian integers, as accepted e.g. by
//! [`MpBigEndianUIntByteSlice`](crate::MpBigEndianUIntByteSlice). The
//! conversions from hexadecimal strings can be evaluated in const contexts and
//! serve, among others, for the definition of multiprecision integer
//! constants. None of the primitives here is constant-time, they're not meant
//! to be used with sensitive data.

extern crate alloc;

use alloc::string::String;

/// Error returned by the conversions from hexadecimal strings.
///
/// Returned by [`be_bytes_from_hexstr()`] and [`bytes_from_hexstr()`].
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BytesFromHexStrError {
    /// The hexadecimal string's length doesn't fit the byte array's.
    InvalidHexStrLen,
    /// The hexadecimal string contains a character other than `0`-`9`,
    /// `a`-`f` and `A`-`F`.
    InvalidHexChar,
}

/// Convert a hexadecimal string into a big-endian byte array.
///
/// Parse `hexstr` as a big-endian integer, i.e. with its most significant
/// digit first, and return it as a big-endian byte array of `N` bytes. The
/// string may have fewer than `2 * N` digits, including an odd number of
/// them, in which case the integer gets zero-extended at its most significant
/// end. In particular, an empty string yields all zeroes. Both lower- and
/// uppercase digits are accepted, a prefix like `0x` is not.
///
/// This is the less strict variant of [`bytes_from_hexstr()`], which requires
/// exactly `2 * N` digits. Can be evaluated in const contexts.
///
/// This is not a constant-time primitive: the execution time depends on the
/// hexadecimal string's contents. It's not meant to be used with sensitive
/// data.
///
/// # Arguments
///
/// * `hexstr` - The hexadecimal string. Must have at most `2 * N` digits.
///
/// # Errors
///
/// - Returns [`BytesFromHexStrError::InvalidHexStrLen`] if `hexstr` has more
///   than `2 * N` digits.
/// - Returns [`BytesFromHexStrError::InvalidHexChar`] if `hexstr` contains a
///   character that isn't a hexadecimal digit.
pub const fn be_bytes_from_hexstr<const N: usize>(
    hexstr: &str,
) -> Result<[u8; N], BytesFromHexStrError> {
    const fn byte_from_hex(hexstr: &[u8; 2]) -> Result<u8, BytesFromHexStrError> {
        let mut result = 0u8;
        let mut i = 0;
        while i < 2 {
            let hex_char = hexstr[i];
            let nibble = hex_char
                - match hex_char {
                    b'0'..=b'9' => b'0',
                    b'a'..=b'f' => b'a' - 0xa,
                    b'A'..=b'F' => b'A' - 0xa,
                    _ => return Err(BytesFromHexStrError::InvalidHexChar),
                };
            result = result << 4 | nibble;
            i += 1;
        }
        Ok(result)
    }

    let hexstr = hexstr.as_bytes();
    if hexstr.len() > 2 * N {
        return Err(BytesFromHexStrError::InvalidHexStrLen);
    }

    let mut result: [u8; N] = [0; N];
    let result_offset = N - hexstr.len().div_ceil(2);
    let mut i = hexstr.len();
    while i > 1 {
        i -= 2;
        let hexstr: [u8; 2] = [hexstr[i], hexstr[i + 1]];
        result[result_offset + i.div_ceil(2)] = match byte_from_hex(&hexstr) {
            Ok(b) => b,
            Err(e) => return Err(e),
        };
    }
    if i == 1 {
        let hexstr: [u8; 2] = [b'0', hexstr[0]];
        result[result_offset] = match byte_from_hex(&hexstr) {
            Ok(b) => b,
            Err(e) => return Err(e),
        };
    }
    Ok(result)
}

/// Convert a hexadecimal string of exact length into a byte array.
///
/// Like [`be_bytes_from_hexstr()`], but require `hexstr` to have exactly two
/// digits per byte, i.e. `2 * N`. The bytes are returned in the order of the
/// string's digit pairs, i.e. big-endian if the string is read as an integer.
/// See [`bytes_from_hexstr_cnst()`] for a panicking variant for use in const
/// contexts. Can be evaluated in const contexts itself.
///
/// This is not a constant-time primitive: the execution time depends on the
/// hexadecimal string's contents. It's not meant to be used with sensitive
/// data.
///
/// # Arguments
///
/// * `hexstr` - The hexadecimal string. Must have exactly `2 * N` digits.
///
/// # Errors
///
/// - Returns [`BytesFromHexStrError::InvalidHexStrLen`] if `hexstr` doesn't
///   have exactly `2 * N` digits.
/// - Returns [`BytesFromHexStrError::InvalidHexChar`] if `hexstr` contains a
///   character that isn't a hexadecimal digit.
pub const fn bytes_from_hexstr<const N: usize>(
    hexstr: &str,
) -> Result<[u8; N], BytesFromHexStrError> {
    if hexstr.len() != 2 * N {
        return Err(BytesFromHexStrError::InvalidHexStrLen);
    }
    be_bytes_from_hexstr::<N>(hexstr)
}

/// Convert a hexadecimal string of exact length into a byte array, panicking
/// on error.
///
/// Like [`bytes_from_hexstr()`], but panic instead of returning an error. As
/// `Result::unwrap()` can't be used in const contexts, this provides a
/// convenient way of defining constants, where invalid input makes the
/// compilation fail.
///
/// This is not a constant-time primitive: the execution time depends on the
/// hexadecimal string's contents. It's not meant to be used with sensitive
/// data.
///
/// # Arguments
///
/// * `hexstr` - The hexadecimal string. Must have exactly `2 * N` digits, all
///   of them valid.
///
/// # Panics
///
/// Panics if `hexstr` doesn't have exactly `2 * N` digits or contains a
/// character that isn't a hexadecimal digit.
pub const fn bytes_from_hexstr_cnst<const N: usize>(hexstr: &str) -> [u8; N] {
    match bytes_from_hexstr::<N>(hexstr) {
        Ok(result) => result,
        Err(_) => panic!("invalid hex string"),
    }
}

#[test]
fn test_be_bytes_from_hexstr() {
    assert_eq!(
        be_bytes_from_hexstr::<11>("0123456789abcdefABCDEF").unwrap(),
        [
            0x01u8, 0x23u8, 0x45u8, 0x67u8, 0x89u8, 0xabu8, 0xcdu8, 0xefu8, 0xabu8, 0xcdu8, 0xefu8
        ],
    );
    assert_eq!(
        be_bytes_from_hexstr::<11>("123456789abcdefABCDEF").unwrap(),
        [
            0x01u8, 0x23u8, 0x45u8, 0x67u8, 0x89u8, 0xabu8, 0xcdu8, 0xefu8, 0xabu8, 0xcdu8, 0xefu8
        ],
    );
    assert_eq!(
        be_bytes_from_hexstr::<12>("0123456789abcdefABCDEF").unwrap(),
        [
            0x00u8, 0x01u8, 0x23u8, 0x45u8, 0x67u8, 0x89u8, 0xabu8, 0xcdu8, 0xefu8, 0xabu8, 0xcdu8,
            0xefu8
        ],
    );
    assert_eq!(
        be_bytes_from_hexstr::<12>("123456789abcdefABCDEF").unwrap(),
        [
            0x00u8, 0x01u8, 0x23u8, 0x45u8, 0x67u8, 0x89u8, 0xabu8, 0xcdu8, 0xefu8, 0xabu8, 0xcdu8,
            0xefu8
        ],
    );
    assert_eq!(
        be_bytes_from_hexstr::<13>("0123456789abcdefABCDEF").unwrap(),
        [
            0x00u8, 0x00u8, 0x01u8, 0x23u8, 0x45u8, 0x67u8, 0x89u8, 0xabu8, 0xcdu8, 0xefu8, 0xabu8,
            0xcdu8, 0xefu8
        ],
    );
    assert_eq!(
        be_bytes_from_hexstr::<13>("123456789abcdefABCDEF").unwrap(),
        [
            0x00u8, 0x00u8, 0x01u8, 0x23u8, 0x45u8, 0x67u8, 0x89u8, 0xabu8, 0xcdu8, 0xefu8, 0xabu8,
            0xcdu8, 0xefu8
        ],
    );
}

#[test]
fn test_bytes_from_hexstr() {
    assert_eq!(
        bytes_from_hexstr::<11>("0123456789abcdefABCDEF").unwrap(),
        [
            0x01u8, 0x23u8, 0x45u8, 0x67u8, 0x89u8, 0xabu8, 0xcdu8, 0xefu8, 0xabu8, 0xcdu8, 0xefu8
        ],
    );
    assert_eq!(
        bytes_from_hexstr::<11>("123456789abcdefABCDEF"),
        Err(BytesFromHexStrError::InvalidHexStrLen)
    );
}

/// Convert a byte array into a hexadecimal string.
///
/// Return a string with two lowercase hexadecimal digits per byte, in the
/// order of `bytes`, i.e. most significant digit first for a big-endian byte
/// array. No prefix like `0x` is prepended and leading zeroes are retained.
/// This is the inverse of [`bytes_from_hexstr()`].
///
/// This is not a constant-time primitive: the execution time might depend on
/// the bytes' values. It's not meant to be used with sensitive data.
///
/// # Arguments
///
/// * `bytes` - The bytes to convert.
///
/// # Errors
///
/// Returns a [`TryReserveError`](alloc::collections::TryReserveError) if the
/// memory for the string couldn't be allocated.
pub fn bytes_to_hexstr(bytes: &[u8]) -> Result<String, alloc::collections::TryReserveError> {
    fn nibble_to_hexchar(nibble: u8) -> char {
        let c = match nibble {
            0x0..=0x9 => b'0' + nibble,
            0xa..=0xf => b'a' + (nibble - 0xa),
            _ => unreachable!(),
        };
        c as char
    }

    let mut result = String::new();
    result.try_reserve_exact(2 * bytes.len())?;
    for b in bytes {
        result.push(nibble_to_hexchar(b >> 4));
        result.push(nibble_to_hexchar(b & 0xf))
    }
    Ok(result)
}

#[test]
fn test_bytes_to_hexstr() {
    assert_eq!(
        bytes_to_hexstr(&[
            0x01u8, 0x23u8, 0x45u8, 0x67u8, 0x89u8, 0xabu8, 0xcdu8, 0xefu8,
        ])
        .unwrap(),
        "0123456789abcdef",
    );
}