use alloc::{boxed::Box, string::String, vec::Vec};
use io_replica::object::ReplicaHash;
use sha2::{Digest, Sha256};
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum PimdirHashAlgo {
#[default]
Blake3,
Sha256_128,
}
impl PimdirHashAlgo {
pub fn as_str(&self) -> &'static str {
match self {
Self::Blake3 => "blake3",
Self::Sha256_128 => "sha256-128",
}
}
pub fn parse(algo: &str) -> Option<Self> {
match algo {
"blake3" => Some(Self::Blake3),
"sha256-128" => Some(Self::Sha256_128),
_ => None,
}
}
pub fn hash(&self, bytes: &[u8]) -> ReplicaHash {
let mut hasher = self.hasher();
hasher.update(bytes);
hasher.finish()
}
pub fn hasher(&self) -> PimdirHasher {
match self {
Self::Blake3 => PimdirHasher::Blake3(Box::new(blake3::Hasher::new())),
Self::Sha256_128 => PimdirHasher::Sha256_128(Sha256::new()),
}
}
}
pub enum PimdirHasher {
Blake3(Box<blake3::Hasher>),
Sha256_128(Sha256),
}
impl PimdirHasher {
pub fn update(&mut self, bytes: &[u8]) {
match self {
Self::Blake3(hasher) => {
hasher.update(bytes);
}
Self::Sha256_128(hasher) => hasher.update(bytes),
}
}
pub fn finish(self) -> ReplicaHash {
let digest: Vec<u8> = match self {
Self::Blake3(hasher) => hasher.finalize().as_bytes().to_vec(),
Self::Sha256_128(hasher) => hasher.finalize()[..16].to_vec(),
};
ReplicaHash(base32(&digest))
}
}
fn base32(digest: &[u8]) -> String {
const ALPHABET: &[u8; 32] = b"abcdefghijklmnopqrstuvwxyz234567";
let mut name = String::with_capacity(digest.len().div_ceil(5) * 8);
let mut buffer: u16 = 0;
let mut bits = 0;
for byte in digest {
buffer = (buffer << 8) | u16::from(*byte);
bits += 8;
while bits >= 5 {
bits -= 5;
name.push(ALPHABET[usize::from((buffer >> bits) & 0x1f)] as char);
}
}
if bits > 0 {
name.push(ALPHABET[usize::from((buffer << (5 - bits)) & 0x1f)] as char);
}
name
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sha256_128_matches_the_shape_every_implementation_must_agree_on() {
let hash = PimdirHashAlgo::Sha256_128.hash(b"pimdir");
assert_eq!(hash.0.len(), 26);
assert!(hash.0.chars().all(|c| ALPHABET_CHARS.contains(c)));
}
const ALPHABET_CHARS: &str = "abcdefghijklmnopqrstuvwxyz234567";
#[test]
fn base32_encodes_rfc_4648_vectors_lowercased() {
assert_eq!(base32(b"f"), "my");
assert_eq!(base32(b"fo"), "mzxq");
assert_eq!(base32(b"foo"), "mzxw6");
assert_eq!(base32(b"foob"), "mzxw6yq");
assert_eq!(base32(b"fooba"), "mzxw6ytb");
assert_eq!(base32(b"foobar"), "mzxw6ytboi");
}
#[test]
fn a_streamed_body_hashes_like_a_whole_one() {
for algo in [PimdirHashAlgo::Blake3, PimdirHashAlgo::Sha256_128] {
let mut hasher = algo.hasher();
hasher.update(b"BEGIN:VCARD\r\n");
hasher.update(b"UID:x\r\nEND:VCARD\r\n");
assert_eq!(
hasher.finish(),
algo.hash(b"BEGIN:VCARD\r\nUID:x\r\nEND:VCARD\r\n")
);
}
}
#[test]
fn the_algorithms_round_trip_through_their_stored_spelling() {
for algo in [PimdirHashAlgo::Blake3, PimdirHashAlgo::Sha256_128] {
assert_eq!(PimdirHashAlgo::parse(algo.as_str()), Some(algo));
}
assert_eq!(PimdirHashAlgo::parse("md5"), None);
assert_eq!(PimdirHashAlgo::default(), PimdirHashAlgo::Blake3);
}
}