use std::fmt;
use std::str::FromStr;
use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize};
use crate::error::Error;
#[derive(
Archive,
RkyvSerialize,
RkyvDeserialize,
Clone,
Copy,
PartialEq,
Eq,
Hash,
PartialOrd,
Ord,
)]
#[rkyv(derive(Debug, PartialEq, Eq))]
pub struct GraphHash(pub [u8; 32]);
impl GraphHash {
#[must_use]
pub fn of(bytes: &[u8]) -> Self {
Self(blake3::hash(bytes).into())
}
#[must_use]
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
#[must_use]
pub fn display_short(&self) -> String {
let full = self.to_string();
full.chars().take(52).collect()
}
#[must_use]
pub fn shard_prefix(&self) -> (String, String) {
let hex = data_encoding::HEXLOWER.encode(&self.0[..2]);
(hex[0..2].to_string(), hex[2..4].to_string())
}
}
impl fmt::Debug for GraphHash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "GraphHash({})", self.display_short())
}
}
impl fmt::Display for GraphHash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let encoded = nix_base32_encode(&self.0);
f.write_str(&encoded)
}
}
impl FromStr for GraphHash {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let bytes = nix_base32_decode(s).ok_or(Error::BadHash {
input: s.to_string(),
reason: "not a valid nix-base32 encoding",
})?;
if bytes.len() != 32 {
return Err(Error::BadHash {
input: s.to_string(),
reason: "wrong digest length (expected 32 bytes)",
});
}
let mut out = [0u8; 32];
out.copy_from_slice(&bytes);
Ok(Self(out))
}
}
const NIX_BASE32_CHARS: &[u8; 32] = b"0123456789abcdfghijklmnpqrsvwxyz";
fn nix_base32_encode(bytes: &[u8]) -> String {
let out_len = (bytes.len() * 8 + 4) / 5;
let mut out = String::with_capacity(out_len);
for i in (0..out_len).rev() {
let b = i * 5;
let byte = b / 8;
let bit = b % 8;
let mut c = u16::from(bytes[byte]) >> bit;
if byte + 1 < bytes.len() {
c |= u16::from(bytes[byte + 1]) << (8 - bit);
}
out.push(NIX_BASE32_CHARS[(c & 0x1f) as usize] as char);
}
out
}
fn nix_base32_decode(s: &str) -> Option<Vec<u8>> {
let s = s.as_bytes();
let bytes_len = s.len() * 5 / 8;
let mut out = vec![0u8; bytes_len];
for (i, &c) in s.iter().rev().enumerate() {
let digit = NIX_BASE32_CHARS.iter().position(|&x| x == c)?;
let b = i * 5;
let byte = b / 8;
let bit = b % 8;
let lo = (digit as u16) << bit;
let hi = (digit as u16) >> (8 - bit);
out[byte] |= (lo & 0xff) as u8;
if hi != 0 && byte + 1 < bytes_len {
out[byte + 1] |= hi as u8;
}
}
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn hash_of_empty_is_blake3_empty() {
let h = GraphHash::of(&[]);
let expected: [u8; 32] = [
0xaf, 0x13, 0x49, 0xb9, 0xf5, 0xf9, 0xa1, 0xa6, 0xa0, 0x40, 0x4d, 0xea, 0x36, 0xdc,
0xc9, 0x49, 0x9b, 0xcb, 0x25, 0xc9, 0xad, 0xc1, 0x12, 0xb7, 0xcc, 0x9a, 0x93, 0xca,
0xe4, 0x1f, 0x32, 0x62,
];
assert_eq!(h.as_bytes(), &expected);
}
#[test]
fn base32_roundtrip() {
for input in [b"".as_slice(), b"hello", b"sui graph store"] {
let h = GraphHash::of(input);
let s = h.to_string();
let back: GraphHash = s.parse().unwrap();
assert_eq!(h, back);
}
}
#[test]
fn shard_prefix_is_two_two_hex() {
let h = GraphHash::of(b"shard test");
let (a, b) = h.shard_prefix();
assert_eq!(a.len(), 2);
assert_eq!(b.len(), 2);
assert!(a.chars().all(|c| c.is_ascii_hexdigit()));
assert!(b.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn display_short_is_52_chars() {
let h = GraphHash::of(b"display test");
assert_eq!(h.display_short().len(), 52);
}
#[test]
fn bad_hash_string_returns_error() {
let r: Result<GraphHash, _> = "not a real hash!@#".parse();
assert!(r.is_err());
}
}