use std::fmt;
pub const HASH_SIZE: usize = photodna_sys::PHOTODNA_HASH_SIZE_EDGE_V2;
pub const HASH_SIZE_MAX: usize = photodna_sys::PHOTODNA_HASH_SIZE_MAX;
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
pub struct Hash {
bytes: [u8; HASH_SIZE],
len: usize,
}
impl Hash {
#[inline]
pub const fn new(bytes: [u8; HASH_SIZE]) -> Self {
Self {
bytes,
len: HASH_SIZE,
}
}
pub fn from_slice(slice: &[u8]) -> Option<Self> {
if slice.len() > HASH_SIZE {
return None;
}
let mut bytes = [0u8; HASH_SIZE];
bytes[..slice.len()].copy_from_slice(slice);
Some(Self {
bytes,
len: slice.len(),
})
}
#[inline]
pub fn as_bytes(&self) -> &[u8] {
&self.bytes[..self.len]
}
#[inline]
pub const fn as_array(&self) -> &[u8; HASH_SIZE] {
&self.bytes
}
#[inline]
pub const fn len(&self) -> usize {
self.len
}
#[inline]
pub fn is_empty(&self) -> bool {
self.bytes[..self.len].iter().all(|&b| b == 0)
}
pub fn to_hex(&self) -> String {
let mut hex = String::with_capacity(self.len * 2);
for byte in &self.bytes[..self.len] {
use std::fmt::Write;
let _ = write!(hex, "{:02x}", byte);
}
hex
}
pub fn to_hex_upper(&self) -> String {
let mut hex = String::with_capacity(self.len * 2);
for byte in &self.bytes[..self.len] {
use std::fmt::Write;
let _ = write!(hex, "{:02X}", byte);
}
hex
}
pub fn from_hex(hex: &str) -> Option<Self> {
if hex.len() % 2 != 0 {
return None;
}
let byte_len = hex.len() / 2;
if byte_len > HASH_SIZE {
return None;
}
let mut bytes = [0u8; HASH_SIZE];
for (i, chunk) in hex.as_bytes().chunks(2).enumerate() {
let high = hex_digit_value(chunk[0])?;
let low = hex_digit_value(chunk[1])?;
bytes[i] = (high << 4) | low;
}
Some(Self {
bytes,
len: byte_len,
})
}
#[inline]
pub fn as_mut_bytes(&mut self) -> &mut [u8; HASH_SIZE] {
&mut self.bytes
}
#[inline]
pub fn set_len(&mut self, len: usize) {
assert!(len <= HASH_SIZE, "length exceeds maximum hash size");
self.len = len;
}
#[inline]
pub const fn zeroed() -> Self {
Self {
bytes: [0u8; HASH_SIZE],
len: 0,
}
}
}
impl Default for Hash {
fn default() -> Self {
Self {
bytes: [0u8; HASH_SIZE],
len: HASH_SIZE,
}
}
}
impl fmt::Debug for Hash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let preview_len = 16.min(self.len);
let preview: String = self.bytes[..preview_len]
.iter()
.map(|b| format!("{:02x}", b))
.collect();
if self.len > preview_len {
write!(f, "Hash({}..., {} bytes)", preview, self.len)
} else {
write!(f, "Hash({})", preview)
}
}
}
impl fmt::Display for Hash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_hex())
}
}
impl AsRef<[u8]> for Hash {
fn as_ref(&self) -> &[u8] {
self.as_bytes()
}
}
impl From<[u8; HASH_SIZE]> for Hash {
fn from(bytes: [u8; HASH_SIZE]) -> Self {
Self::new(bytes)
}
}
impl TryFrom<&[u8]> for Hash {
type Error = ();
fn try_from(slice: &[u8]) -> Result<Self, Self::Error> {
Self::from_slice(slice).ok_or(())
}
}
#[inline]
fn hex_digit_value(c: u8) -> Option<u8> {
match c {
b'0'..=b'9' => Some(c - b'0'),
b'a'..=b'f' => Some(c - b'a' + 10),
b'A'..=b'F' => Some(c - b'A' + 10),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hash_size_constant() {
assert_eq!(HASH_SIZE, 924);
}
#[test]
fn test_hash_new() {
let data = [0xAB; HASH_SIZE];
let hash = Hash::new(data);
assert_eq!(hash.len(), HASH_SIZE);
assert!(!hash.is_empty());
}
#[test]
fn test_hash_default() {
let hash = Hash::default();
assert!(hash.is_empty());
assert_eq!(hash.len(), HASH_SIZE);
}
#[test]
fn test_hash_from_slice() {
let data = [0xAB; 100];
let hash = Hash::from_slice(&data).unwrap();
assert_eq!(hash.len(), 100);
assert_eq!(&hash.as_bytes()[..100], &data);
}
#[test]
fn test_hash_from_slice_too_large() {
let data = [0xAB; HASH_SIZE + 1];
assert!(Hash::from_slice(&data).is_none());
}
#[test]
fn test_hash_to_hex() {
let data = [0xAB, 0xCD, 0xEF, 0x01];
let hash = Hash::from_slice(&data).unwrap();
assert_eq!(hash.to_hex(), "abcdef01");
assert_eq!(hash.to_hex_upper(), "ABCDEF01");
}
#[test]
fn test_hash_from_hex() {
let hash = Hash::from_hex("abcdef01").unwrap();
assert_eq!(hash.len(), 4);
assert_eq!(hash.as_bytes(), &[0xAB, 0xCD, 0xEF, 0x01]);
}
#[test]
fn test_hash_from_hex_invalid() {
assert!(Hash::from_hex("abc").is_none()); assert!(Hash::from_hex("ghij").is_none()); }
#[test]
fn test_hash_copy() {
let hash1 = Hash::from_slice(&[1, 2, 3, 4]).unwrap();
let hash2 = hash1; assert_eq!(hash1, hash2);
}
#[test]
fn test_hash_debug() {
let hash = Hash::from_slice(&[0xAB; 20]).unwrap();
let debug = format!("{:?}", hash);
assert!(debug.contains("Hash("));
assert!(debug.contains("20 bytes"));
}
#[test]
fn test_hash_display() {
let hash = Hash::from_slice(&[0xAB, 0xCD]).unwrap();
assert_eq!(format!("{}", hash), "abcd");
}
#[test]
fn test_hash_equality() {
let hash1 = Hash::from_slice(&[1, 2, 3, 4]).unwrap();
let hash2 = Hash::from_slice(&[1, 2, 3, 4]).unwrap();
let hash3 = Hash::from_slice(&[1, 2, 3, 5]).unwrap();
assert_eq!(hash1, hash2);
assert_ne!(hash1, hash3);
}
#[test]
fn test_hash_in_hashset() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(Hash::from_slice(&[1, 2, 3]).unwrap());
set.insert(Hash::from_slice(&[4, 5, 6]).unwrap());
assert!(set.contains(&Hash::from_slice(&[1, 2, 3]).unwrap()));
assert!(!set.contains(&Hash::from_slice(&[7, 8, 9]).unwrap()));
}
}