vyre 0.4.0

GPU compute intermediate representation with a standard operation library
Documentation
//! Hex formatting helpers for hash tests.
//!
//! Per audit L.2.4 (perf): the prior implementations called
//! `format!("{byte:02x}")` per byte / per word, which allocates a fresh
//! `String` on every call. At internet-scale hashing (billions of
//! digests) that's a heap alloc per input byte. These helpers now use a
//! 512-byte lookup table and push two characters per byte directly.

/// Precomputed lookup table of every `u8` value as its 2-character
/// lowercase hex representation. Indexed by byte value.
static HEX_LUT: [[u8; 2]; 256] = build_hex_lut();

const fn build_hex_lut() -> [[u8; 2]; 256] {
    let mut table = [[0u8; 2]; 256];
    let hex = b"0123456789abcdef";
    let mut i = 0usize;
    while i < 256 {
        table[i][0] = hex[(i >> 4) & 0xF];
        table[i][1] = hex[i & 0xF];
        i += 1;
    }
    table
}

#[inline]
fn push_byte(out: &mut String, byte: u8) {
    let pair = HEX_LUT[byte as usize];
    // ASCII hex chars are single-byte UTF-8, so `push` keeps the
    // String valid without any unsafe block.
    out.push(pair[0] as char);
    out.push(pair[1] as char);
}

/// Format a byte slice as lowercase hex.
#[must_use]
pub fn bytes_to_hex(bytes: &[u8]) -> String {
    let mut out = String::with_capacity(bytes.len() * 2);
    for &byte in bytes {
        push_byte(&mut out, byte);
    }
    out
}

/// Format big-endian `u32` digest words as lowercase hex.
#[must_use]
pub fn u32_words_to_hex(words: &[u32]) -> String {
    let mut out = String::with_capacity(words.len() * 8);
    for &word in words {
        for byte in word.to_be_bytes() {
            push_byte(&mut out, byte);
        }
    }
    out
}

/// Format a single `u64` as 16 lowercase hex nibbles (big-endian word order).
#[must_use]
pub fn u64_to_hex(word: u64) -> String {
    let mut out = String::with_capacity(16);
    for byte in word.to_be_bytes() {
        push_byte(&mut out, byte);
    }
    out
}

/// Format big-endian `u64` digest words as lowercase hex.
#[must_use]
pub fn u64_words_to_hex(words: &[u64]) -> String {
    let mut out = String::with_capacity(words.len() * 16);
    for &word in words {
        for byte in word.to_be_bytes() {
            push_byte(&mut out, byte);
        }
    }
    out
}

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

    /// L.2.4: the lookup-table path must match the prior format!-based
    /// output bit-for-bit across the full byte and word domains.
    #[test]
    fn bytes_to_hex_matches_format() {
        let input: Vec<u8> = (0u16..=255u16).map(|b| b as u8).collect();
        let expected: String = input.iter().map(|b| format!("{b:02x}")).collect();
        assert_eq!(bytes_to_hex(&input), expected);
    }

    #[test]
    fn u32_words_to_hex_matches_format() {
        let words: [u32; 4] = [0, 1, 0xDEADBEEF, u32::MAX];
        let expected: String = words.iter().map(|w| format!("{w:08x}")).collect();
        assert_eq!(u32_words_to_hex(&words), expected);
    }

    #[test]
    fn u64_words_to_hex_matches_format() {
        let words: [u64; 3] = [0, u64::MAX, 0x0123_4567_89AB_CDEF];
        let expected: String = words.iter().map(|w| format!("{w:016x}")).collect();
        assert_eq!(u64_words_to_hex(&words), expected);
    }

    #[test]
    fn u64_to_hex_matches_format() {
        for word in [0u64, 1, 0xDEADBEEFDEADBEEF, u64::MAX] {
            assert_eq!(u64_to_hex(word), format!("{word:016x}"));
        }
    }
}