#[must_use]
pub fn mmh3_x86_32(data: &[u8], seed: u32) -> u32 {
const C1: u32 = 0xcc9e_2d51;
const C2: u32 = 0x1b87_3593;
let mut h1 = seed;
let mut chunks = data.chunks_exact(4);
for chunk in &mut chunks {
let mut k1 = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
k1 = k1.wrapping_mul(C1).rotate_left(15).wrapping_mul(C2);
h1 ^= k1;
h1 = h1.rotate_left(13).wrapping_mul(5).wrapping_add(0xe654_6b64);
}
let rem = chunks.remainder();
if !rem.is_empty() {
let mut k1: u32 = 0;
if rem.len() >= 3 {
k1 ^= u32::from(rem[2]) << 16;
}
if rem.len() >= 2 {
k1 ^= u32::from(rem[1]) << 8;
}
k1 ^= u32::from(rem[0]);
k1 = k1.wrapping_mul(C1).rotate_left(15).wrapping_mul(C2);
h1 ^= k1;
}
h1 ^= data.len() as u32;
h1 ^= h1 >> 16;
h1 = h1.wrapping_mul(0x85eb_ca6b);
h1 ^= h1 >> 13;
h1 = h1.wrapping_mul(0xc2b2_ae35);
h1 ^ (h1 >> 16)
}
fn base64_std(data: &[u8]) -> String {
const T: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let cap = data.len().saturating_mul(2).saturating_add(4);
let mut out = String::with_capacity(cap);
for chunk in data.chunks(3) {
let b0 = chunk[0] as u32;
let b1 = *chunk.get(1).unwrap_or(&0) as u32;
let b2 = *chunk.get(2).unwrap_or(&0) as u32;
let n = (b0 << 16) | (b1 << 8) | b2;
out.push(T[((n >> 18) & 63) as usize] as char);
out.push(T[((n >> 12) & 63) as usize] as char);
out.push(if chunk.len() > 1 {
T[((n >> 6) & 63) as usize] as char
} else {
'='
});
out.push(if chunk.len() > 2 {
T[(n & 63) as usize] as char
} else {
'='
});
}
out
}
#[must_use]
pub fn shodan_favicon_hash(raw_icon: &[u8]) -> i32 {
let b64 = base64_std(raw_icon);
let mut framed = String::with_capacity(b64.len() + b64.len() / 76 + 1);
for chunk in b64.as_bytes().chunks(76) {
framed.push_str(std::str::from_utf8(chunk).unwrap_or(""));
framed.push('\n');
}
mmh3_x86_32(framed.as_bytes(), 0) as i32
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mmh3_empty_seed_zero_is_zero() {
assert_eq!(mmh3_x86_32(b"", 0), 0);
}
#[test]
fn mmh3_matches_legacy_private_copies() {
fn legacy(data: &[u8], seed: u32) -> u32 {
let c1: u32 = 0xcc9e_2d51;
let c2: u32 = 0x1b87_3593;
let mut h1 = seed;
let chunks = data.chunks_exact(4);
let remainder = chunks.remainder();
for chunk in chunks {
let mut k1 = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
k1 = k1.wrapping_mul(c1).rotate_left(15).wrapping_mul(c2);
h1 ^= k1;
h1 = h1.rotate_left(13).wrapping_mul(5).wrapping_add(0xe654_6b64);
}
let mut k1: u32 = 0;
match remainder.len() {
3 => {
k1 ^= (remainder[2] as u32) << 16;
k1 ^= (remainder[1] as u32) << 8;
k1 ^= remainder[0] as u32;
}
2 => {
k1 ^= (remainder[1] as u32) << 8;
k1 ^= remainder[0] as u32;
}
1 => {
k1 ^= remainder[0] as u32;
}
_ => {}
}
if !remainder.is_empty() {
k1 = k1.wrapping_mul(c1).rotate_left(15).wrapping_mul(c2);
h1 ^= k1;
}
h1 ^= data.len() as u32;
h1 ^= h1 >> 16;
h1 = h1.wrapping_mul(0x85eb_ca6b);
h1 ^= h1 >> 13;
h1 = h1.wrapping_mul(0xc2b2_ae35);
h1 ^= h1 >> 16;
h1
}
for len in 0..200usize {
let data: Vec<u8> = (0..len).map(|i| (i * 31 + 7) as u8).collect();
assert_eq!(
mmh3_x86_32(&data, 0),
legacy(&data, 0),
"divergence at len {len}"
);
assert_eq!(mmh3_x86_32(&data, 0xdead_beef), legacy(&data, 0xdead_beef));
}
}
#[test]
fn shodan_favicon_hash_is_stable() {
let icon = b"\x00\x00\x01\x00\x01\x00\x10\x10fake-ico-bytes";
let h = shodan_favicon_hash(icon);
assert_eq!(h, shodan_favicon_hash(icon));
assert_ne!(h, 0);
}
#[test]
fn shodan_favicon_hash_large_input_does_not_panic() {
let big = vec![0xABu8; 10_000_000];
let _h = shodan_favicon_hash(&big);
}
}