haem-frames 0.1.0

Shared framing and embedding messages for isolated workers
Documentation
use super::{Error, FailedCode};

const DIGITS: &[u8; 16] = b"0123456789abcdef";

pub fn encode_vector(vector: &[f32]) -> Result<String, Error> {
    let capacity = vector
        .len()
        .checked_mul(8)
        .ok_or_else(|| Error::new(FailedCode::EmbedLimits, "vector text length overflows"))?;
    let mut text = String::with_capacity(capacity);
    for member in vector {
        if !member.is_finite() {
            return Err(Error::new(
                FailedCode::EmbedOutputInvalid,
                "vector member is not finite",
            ));
        }
        for byte in member.to_le_bytes() {
            text.push(char::from(DIGITS[usize::from(byte >> 4)]));
            text.push(char::from(DIGITS[usize::from(byte & 15)]));
        }
    }
    Ok(text)
}

fn nibble(byte: u8) -> Result<u8, Error> {
    match byte {
        b'0'..=b'9' => Ok(byte - b'0'),
        b'a'..=b'f' => Ok(byte - b'a' + 10),
        _ => Err(Error::new(
            FailedCode::EmbedProtocol,
            "vector text must contain only lowercase hexadecimal",
        )),
    }
}

pub fn decode_vector(text: &str) -> Result<Vec<f32>, Error> {
    let mut words = text.as_bytes().chunks_exact(8);
    if !words.remainder().is_empty() {
        return Err(Error::new(
            FailedCode::EmbedProtocol,
            "vector text length must be a multiple of eight",
        ));
    }
    words
        .by_ref()
        .map(|word| {
            let mut bytes = [0; 4];
            for (byte, pair) in bytes.iter_mut().zip(word.chunks_exact(2)) {
                *byte = (nibble(pair[0])? << 4) | nibble(pair[1])?;
            }
            let member = f32::from_le_bytes(bytes);
            if !member.is_finite() {
                return Err(Error::new(
                    FailedCode::EmbedOutputInvalid,
                    "vector member is not finite",
                ));
            }
            Ok(member)
        })
        .collect()
}