zuzu-rust 0.4.0

Rust implementation of ZuzuScript
Documentation
use std::collections::HashMap;

use hmac::{Hmac, Mac};
use md5::Md5;
use sha1::Sha1;
use sha2::{Digest, Sha224, Sha256, Sha384, Sha512};

use super::super::Value;
use crate::error::{Result, ZuzuRustError};

const BASE64_ALPHABET: &[u8; 64] =
    b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

pub(super) fn md5_exports() -> HashMap<String, Value> {
    HashMap::from([
        ("md5".to_owned(), Value::native_function("md5".to_owned())),
        (
            "md5_hex".to_owned(),
            Value::native_function("md5_hex".to_owned()),
        ),
        (
            "md5_b64".to_owned(),
            Value::native_function("md5_b64".to_owned()),
        ),
    ])
}

pub(super) fn sha_exports() -> HashMap<String, Value> {
    let mut exports = HashMap::new();
    for name in [
        "sha1",
        "sha1_hex",
        "sha1_b64",
        "sha224",
        "sha224_hex",
        "sha224_b64",
        "sha256",
        "sha256_hex",
        "sha256_b64",
        "sha384",
        "sha384_hex",
        "sha384_b64",
        "sha512",
        "sha512_hex",
        "sha512_b64",
        "hmac_sha1",
        "hmac_sha1_hex",
        "hmac_sha1_b64",
        "hmac_sha224",
        "hmac_sha224_hex",
        "hmac_sha224_b64",
        "hmac_sha256",
        "hmac_sha256_hex",
        "hmac_sha256_b64",
        "hmac_sha384",
        "hmac_sha384_hex",
        "hmac_sha384_b64",
        "hmac_sha512",
        "hmac_sha512_hex",
        "hmac_sha512_b64",
    ] {
        exports.insert(name.to_owned(), Value::native_function(name.to_owned()));
    }
    exports
}

pub(super) fn call(name: &str, args: &[Value]) -> Option<Result<Value>> {
    Some(match name {
        "md5" => md5_binary(args.first(), "md5"),
        "md5_hex" => md5_hex(args.first(), "md5_hex"),
        "md5_b64" => md5_b64(args.first(), "md5_b64"),
        "sha1" => sha_binary::<Sha1>(args.first(), "sha1"),
        "sha1_hex" => sha_hex::<Sha1>(args.first(), "sha1_hex"),
        "sha1_b64" => sha_b64::<Sha1>(args.first(), "sha1_b64"),
        "sha224" => sha_binary::<Sha224>(args.first(), "sha224"),
        "sha224_hex" => sha_hex::<Sha224>(args.first(), "sha224_hex"),
        "sha224_b64" => sha_b64::<Sha224>(args.first(), "sha224_b64"),
        "sha256" => sha_binary::<Sha256>(args.first(), "sha256"),
        "sha256_hex" => sha_hex::<Sha256>(args.first(), "sha256_hex"),
        "sha256_b64" => sha_b64::<Sha256>(args.first(), "sha256_b64"),
        "sha384" => sha_binary::<Sha384>(args.first(), "sha384"),
        "sha384_hex" => sha_hex::<Sha384>(args.first(), "sha384_hex"),
        "sha384_b64" => sha_b64::<Sha384>(args.first(), "sha384_b64"),
        "sha512" => sha_binary::<Sha512>(args.first(), "sha512"),
        "sha512_hex" => sha_hex::<Sha512>(args.first(), "sha512_hex"),
        "sha512_b64" => sha_b64::<Sha512>(args.first(), "sha512_b64"),
        "hmac_sha1" => hmac_digest_sha1(args, "hmac_sha1").map(Value::BinaryString),
        "hmac_sha1_hex" => {
            hmac_digest_sha1(args, "hmac_sha1_hex").map(|value| Value::String(to_hex(&value)))
        }
        "hmac_sha1_b64" => hmac_digest_sha1(args, "hmac_sha1_b64")
            .map(|value| Value::String(to_base64_no_pad(&value))),
        "hmac_sha224" => hmac_digest_sha224(args, "hmac_sha224").map(Value::BinaryString),
        "hmac_sha224_hex" => {
            hmac_digest_sha224(args, "hmac_sha224_hex").map(|value| Value::String(to_hex(&value)))
        }
        "hmac_sha224_b64" => hmac_digest_sha224(args, "hmac_sha224_b64")
            .map(|value| Value::String(to_base64_no_pad(&value))),
        "hmac_sha256" => hmac_digest_sha256(args, "hmac_sha256").map(Value::BinaryString),
        "hmac_sha256_hex" => {
            hmac_digest_sha256(args, "hmac_sha256_hex").map(|value| Value::String(to_hex(&value)))
        }
        "hmac_sha256_b64" => hmac_digest_sha256(args, "hmac_sha256_b64")
            .map(|value| Value::String(to_base64_no_pad(&value))),
        "hmac_sha384" => hmac_digest_sha384(args, "hmac_sha384").map(Value::BinaryString),
        "hmac_sha384_hex" => {
            hmac_digest_sha384(args, "hmac_sha384_hex").map(|value| Value::String(to_hex(&value)))
        }
        "hmac_sha384_b64" => hmac_digest_sha384(args, "hmac_sha384_b64")
            .map(|value| Value::String(to_base64_no_pad(&value))),
        "hmac_sha512" => hmac_digest_sha512(args, "hmac_sha512").map(Value::BinaryString),
        "hmac_sha512_hex" => {
            hmac_digest_sha512(args, "hmac_sha512_hex").map(|value| Value::String(to_hex(&value)))
        }
        "hmac_sha512_b64" => hmac_digest_sha512(args, "hmac_sha512_b64")
            .map(|value| Value::String(to_base64_no_pad(&value))),
        _ => return None,
    })
}

fn md5_binary(value: Option<&Value>, label: &str) -> Result<Value> {
    let bytes = require_binary(value, label)?;
    Ok(Value::BinaryString(Md5::digest(bytes).to_vec()))
}

fn md5_hex(value: Option<&Value>, label: &str) -> Result<Value> {
    let bytes = require_binary(value, label)?;
    Ok(Value::String(to_hex(&Md5::digest(bytes))))
}

fn md5_b64(value: Option<&Value>, label: &str) -> Result<Value> {
    let bytes = require_binary(value, label)?;
    Ok(Value::String(to_base64_no_pad(&Md5::digest(bytes))))
}

fn sha_binary<D>(value: Option<&Value>, label: &str) -> Result<Value>
where
    D: Digest,
{
    let bytes = require_binary(value, label)?;
    Ok(Value::BinaryString(D::digest(bytes).to_vec()))
}

fn sha_hex<D>(value: Option<&Value>, label: &str) -> Result<Value>
where
    D: Digest,
{
    let bytes = require_binary(value, label)?;
    Ok(Value::String(to_hex(&D::digest(bytes))))
}

fn sha_b64<D>(value: Option<&Value>, label: &str) -> Result<Value>
where
    D: Digest,
{
    let bytes = require_binary(value, label)?;
    Ok(Value::String(to_base64_no_pad(&D::digest(bytes))))
}

fn require_binary<'a>(value: Option<&'a Value>, label: &str) -> Result<&'a [u8]> {
    match value {
        Some(Value::BinaryString(bytes)) => Ok(bytes.as_slice()),
        Some(other) => Err(ZuzuRustError::thrown(format!(
            "TypeException: {label} expects BinaryString, got {}",
            other.type_name()
        ))),
        None => Err(ZuzuRustError::thrown(format!(
            "TypeException: {label} expects BinaryString, got Null"
        ))),
    }
}

fn require_hmac_inputs<'a>(args: &'a [Value], label: &str) -> Result<(&'a [u8], &'a [u8])> {
    let value = require_binary(args.first(), label)?;
    let key = match args.get(1) {
        Some(Value::BinaryString(bytes)) => bytes.as_slice(),
        Some(other) => {
            return Err(ZuzuRustError::thrown(format!(
                "TypeException: {label} expects BinaryString key, got {}",
                other.type_name()
            )))
        }
        None => {
            return Err(ZuzuRustError::thrown(format!(
                "TypeException: {label} expects BinaryString key, got Null"
            )))
        }
    };
    Ok((value, key))
}

fn hmac_digest_sha1(args: &[Value], label: &str) -> Result<Vec<u8>> {
    let (value, key) = require_hmac_inputs(args, label)?;
    let mut mac = Hmac::<Sha1>::new_from_slice(key).map_err(|_| {
        ZuzuRustError::thrown(format!(
            "TypeException: {label} expects BinaryString key, got Null"
        ))
    })?;
    mac.update(value);
    Ok(mac.finalize().into_bytes().to_vec())
}

fn hmac_digest_sha224(args: &[Value], label: &str) -> Result<Vec<u8>> {
    let (value, key) = require_hmac_inputs(args, label)?;
    let mut mac = Hmac::<Sha224>::new_from_slice(key).map_err(|_| {
        ZuzuRustError::thrown(format!(
            "TypeException: {label} expects BinaryString key, got Null"
        ))
    })?;
    mac.update(value);
    Ok(mac.finalize().into_bytes().to_vec())
}

fn hmac_digest_sha256(args: &[Value], label: &str) -> Result<Vec<u8>> {
    let (value, key) = require_hmac_inputs(args, label)?;
    let mut mac = Hmac::<Sha256>::new_from_slice(key).map_err(|_| {
        ZuzuRustError::thrown(format!(
            "TypeException: {label} expects BinaryString key, got Null"
        ))
    })?;
    mac.update(value);
    Ok(mac.finalize().into_bytes().to_vec())
}

fn hmac_digest_sha384(args: &[Value], label: &str) -> Result<Vec<u8>> {
    let (value, key) = require_hmac_inputs(args, label)?;
    let mut mac = Hmac::<Sha384>::new_from_slice(key).map_err(|_| {
        ZuzuRustError::thrown(format!(
            "TypeException: {label} expects BinaryString key, got Null"
        ))
    })?;
    mac.update(value);
    Ok(mac.finalize().into_bytes().to_vec())
}

fn hmac_digest_sha512(args: &[Value], label: &str) -> Result<Vec<u8>> {
    let (value, key) = require_hmac_inputs(args, label)?;
    let mut mac = Hmac::<Sha512>::new_from_slice(key).map_err(|_| {
        ZuzuRustError::thrown(format!(
            "TypeException: {label} expects BinaryString key, got Null"
        ))
    })?;
    mac.update(value);
    Ok(mac.finalize().into_bytes().to_vec())
}

fn to_hex(bytes: &[u8]) -> String {
    let mut out = String::with_capacity(bytes.len() * 2);
    for byte in bytes {
        out.push_str(&format!("{byte:02x}"));
    }
    out
}

fn to_base64_no_pad(bytes: &[u8]) -> String {
    let mut out = String::new();
    let mut index = 0usize;
    while index < bytes.len() {
        let b0 = bytes[index];
        let b1 = bytes.get(index + 1).copied().unwrap_or(0);
        let b2 = bytes.get(index + 2).copied().unwrap_or(0);
        let triple = ((b0 as u32) << 16) | ((b1 as u32) << 8) | (b2 as u32);
        out.push(BASE64_ALPHABET[((triple >> 18) & 0x3f) as usize] as char);
        out.push(BASE64_ALPHABET[((triple >> 12) & 0x3f) as usize] as char);
        if index + 1 < bytes.len() {
            out.push(BASE64_ALPHABET[((triple >> 6) & 0x3f) as usize] as char);
        }
        if index + 2 < bytes.len() {
            out.push(BASE64_ALPHABET[(triple & 0x3f) as usize] as char);
        }
        index += 3;
    }
    out
}