assetpack-core 0.3.0

Content-addressed asset packing, chunking, recipe, and SQLite storage primitives.
Documentation
use std::{fmt, io::Read, str::FromStr};

use sha3::{Digest, Sha3_256};

use crate::error::{Error, Result};

pub const HASH_LEN: usize = 32;

#[derive(Clone, Copy, Eq, PartialEq, Hash, Ord, PartialOrd)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(try_from = "String", into = "String"))]
pub struct Hash32(pub [u8; HASH_LEN]);

impl Hash32 {
  pub fn new(bytes: [u8; HASH_LEN]) -> Self {
    Self(bytes)
  }

  pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
    if bytes.len() != HASH_LEN {
      return Err(Error::InvalidHashLength(bytes.len()));
    }
    let mut arr = [0u8; HASH_LEN];
    arr.copy_from_slice(bytes);
    Ok(Self(arr))
  }

  pub fn sha3_256(data: impl AsRef<[u8]>) -> Self {
    let mut hasher = Sha3_256::new();
    hasher.update(data.as_ref());
    let digest = hasher.finalize();
    Hash32::new(digest.into())
  }

  pub fn sha3_256_reader(mut reader: impl Read) -> Result<Self> {
    let mut hasher = Sha3_256::new();
    let mut buf = [0u8; 8192];
    loop {
      let n = reader.read(&mut buf)?;
      if n == 0 {
        break;
      }
      hasher.update(&buf[..n]);
    }
    let digest = hasher.finalize();
    Ok(Hash32::new(digest.into()))
  }

  pub fn to_hex(&self) -> String {
    hex::encode(self.0)
  }

  pub fn from_hex(s: &str) -> Result<Self> {
    let bytes = hex::decode(s).map_err(|_| Error::InvalidHex(s.to_string()))?;
    Self::from_bytes(&bytes)
  }

  pub fn as_bytes(&self) -> &[u8; HASH_LEN] {
    &self.0
  }
}

impl fmt::Display for Hash32 {
  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    write!(f, "{}", self.to_hex())
  }
}

impl fmt::Debug for Hash32 {
  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    write!(f, "Hash32({})", self.to_hex())
  }
}

impl FromStr for Hash32 {
  type Err = Error;

  fn from_str(s: &str) -> Result<Self> {
    Hash32::from_hex(s)
  }
}

#[cfg(feature = "serde")]
impl From<Hash32> for String {
  fn from(value: Hash32) -> Self {
    value.to_hex()
  }
}

#[cfg(feature = "serde")]
impl TryFrom<String> for Hash32 {
  type Error = Error;

  fn try_from(value: String) -> Result<Self> {
    Hash32::from_hex(&value)
  }
}

#[cfg(test)]
mod tests {
  use rand::Rng;

  use super::*;

  #[test]
  fn sha3_known_value() {
    let hash = Hash32::sha3_256("hello");
    assert_eq!(hash.to_hex(), "3338be694f50c5f338814986cdf0686453a888b84f424d792af4b9202398f392");
  }

  #[test]
  fn from_hex_roundtrip() {
    let mut bytes = [0u8; HASH_LEN];
    rand::rng().fill_bytes(&mut bytes);
    let h = Hash32::new(bytes);
    let encoded = h.to_hex();
    let decoded = Hash32::from_hex(&encoded).unwrap();
    assert_eq!(decoded, h);
  }

  #[test]
  fn invalid_hex_rejected() {
    let err = Hash32::from_hex("abcd").unwrap_err();
    assert!(matches!(err, Error::InvalidHashLength(2)));
    let err2 = Hash32::from_hex(&"zz".repeat(32)).unwrap_err();
    assert!(matches!(err2, Error::InvalidHex(_)));
  }

  #[test]
  fn odd_length_hex_is_rejected() {
    let err = Hash32::from_hex("abc").unwrap_err();
    assert!(matches!(err, Error::InvalidHex(_)));
  }

  #[test]
  fn from_bytes_rejects_wrong_length() {
    let err = Hash32::from_bytes(&[0u8; HASH_LEN - 1]).unwrap_err();
    assert!(matches!(err, Error::InvalidHashLength(_)));
  }

  #[test]
  fn reader_digest_matches() {
    let data = vec![42u8; 1024];
    let expected = Hash32::sha3_256(&data);
    let via_reader = Hash32::sha3_256_reader(&data[..]).unwrap();
    assert_eq!(expected, via_reader);
  }
}