use crate::hashes::Hasher;
const WIDTH_BITS: u64 = 160;
const SHIFT: u64 = 11;
pub struct QuickXorHasher {
state: [u8; 20],
length: u64,
bit_offset: u64,
}
impl QuickXorHasher {
pub fn new() -> Self {
Self {
state: [0u8; 20],
length: 0,
bit_offset: 0,
}
}
}
impl Default for QuickXorHasher {
fn default() -> Self {
Self::new()
}
}
impl Hasher for QuickXorHasher {
fn update(&mut self, data: &[u8]) {
for &byte in data {
let start_bit = self.bit_offset % WIDTH_BITS;
let byte_index = (start_bit / 8) as usize;
let bit_in_byte = (start_bit % 8) as u8;
self.state[byte_index % 20] ^= byte << bit_in_byte;
if bit_in_byte > 0 {
self.state[(byte_index + 1) % 20] ^= byte >> (8 - bit_in_byte);
}
self.bit_offset += SHIFT;
}
self.length += data.len() as u64;
}
fn finalize_hex(mut self: Box<Self>) -> String {
for (i, b) in self.length.to_le_bytes().iter().enumerate() {
self.state[i] ^= b;
}
hex::encode(self.state)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hashes::Hasher;
fn hash(data: &[u8]) -> String {
let mut h = QuickXorHasher::new();
h.update(data);
Box::new(h).finalize_hex()
}
#[test]
fn quickxor_empty() {
assert_eq!(hash(b""), "0000000000000000000000000000000000000000");
}
#[test]
fn quickxor_output_is_40_hex_chars() {
assert_eq!(hash(b"test").len(), 40);
}
#[test]
fn chunked_matches_single() {
let data = b"the quick brown fox jumps over the lazy dog";
let single = hash(data);
let mut h = QuickXorHasher::new();
for chunk in data.chunks(7) {
h.update(chunk);
}
assert_eq!(Box::new(h).finalize_hex(), single);
}
#[test]
fn default_equals_new() {
let mut h = QuickXorHasher::default();
h.update(b"abc");
assert_eq!(Box::new(h).finalize_hex(), hash(b"abc"));
}
#[test]
fn single_byte_a() {
let result = hash(b"a");
assert_eq!(&result[0..2], "60", "first byte should be 0x60");
assert_eq!(
&result[2..],
"00".repeat(19),
"remaining bytes should be zero"
);
}
}