Files
zstd-rs/rust/src/xxhash.rs
T
ddidderr 089f8e5b4d feat(rust): add common primitive compatibility layer
Introduce the Rust static library and move the shared byte, bitstream, CPU,
error, xxHash, debug, and public-common implementations into it. Thin C shims
preserve the existing header-driven C build while original tests link the Rust
archive.

This establishes the ABI-safe foundation for later codec and CLI ports;
entropy coding, runtime support, codecs, dictionaries, and the CLI remain C.
The top-down migration map documents that boundary and its validation path.

Test Plan:
- cargo fmt --check
- cargo test --all-targets
- cargo clippy --all-targets -- -D warnings
- cargo build --release

Refs: rust/README.md
2026-07-10 20:02:17 +02:00

689 lines
20 KiB
Rust

#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
use std::os::raw::c_void;
pub type XXH32_hash_t = u32;
pub type XXH64_hash_t = u64;
pub type XXH_errorcode = i32;
pub const XXH_OK: XXH_errorcode = 0;
pub const XXH_ERROR: XXH_errorcode = 1;
const PRIME32_1: u32 = 0x9e37_79b1;
const PRIME32_2: u32 = 0x85eb_ca77;
const PRIME32_3: u32 = 0xc2b2_ae3d;
const PRIME32_4: u32 = 0x27d4_eb2f;
const PRIME32_5: u32 = 0x1656_67b1;
const PRIME64_1: u64 = 0x9e37_79b1_85eb_ca87;
const PRIME64_2: u64 = 0xc2b2_ae3d_27d4_eb4f;
const PRIME64_3: u64 = 0x1656_67b1_9e37_79f9;
const PRIME64_4: u64 = 0x85eb_ca77_c2b2_ae63;
const PRIME64_5: u64 = 0x27d4_eb2f_1656_67c5;
#[inline]
fn XXH64_round(mut acc: u64, input: u64) -> u64 {
acc = acc.wrapping_add(input.wrapping_mul(PRIME64_2));
acc = acc.rotate_left(31);
acc.wrapping_mul(PRIME64_1)
}
#[inline]
fn XXH64_mergeRound(mut acc: u64, val: u64) -> u64 {
acc ^= XXH64_round(0, val);
acc.wrapping_mul(PRIME64_1).wrapping_add(PRIME64_4)
}
#[inline]
fn XXH64_avalanche(mut hash: u64) -> u64 {
hash ^= hash >> 33;
hash = hash.wrapping_mul(PRIME64_2);
hash ^= hash >> 29;
hash = hash.wrapping_mul(PRIME64_3);
hash ^= hash >> 32;
hash
}
#[inline]
fn read_le64(input: &[u8]) -> u64 {
u64::from_le_bytes(input[..8].try_into().unwrap())
}
#[inline]
fn read_le32(input: &[u8]) -> u32 {
u32::from_le_bytes(input[..4].try_into().unwrap())
}
#[inline]
fn XXH32_round(mut acc: u32, input: u32) -> u32 {
acc = acc.wrapping_add(input.wrapping_mul(PRIME32_2));
acc = acc.rotate_left(13);
acc.wrapping_mul(PRIME32_1)
}
#[inline]
fn XXH32_avalanche(mut hash: u32) -> u32 {
hash ^= hash >> 15;
hash = hash.wrapping_mul(PRIME32_2);
hash ^= hash >> 13;
hash = hash.wrapping_mul(PRIME32_3);
hash ^= hash >> 16;
hash
}
#[inline]
fn process_stripe32(accumulators: &mut [u32; 4], stripe: &[u8]) {
accumulators[0] = XXH32_round(accumulators[0], read_le32(&stripe[0..]));
accumulators[1] = XXH32_round(accumulators[1], read_le32(&stripe[4..]));
accumulators[2] = XXH32_round(accumulators[2], read_le32(&stripe[8..]));
accumulators[3] = XXH32_round(accumulators[3], read_le32(&stripe[12..]));
}
#[inline]
fn finalize32(mut hash: u32, mut input: &[u8]) -> u32 {
while input.len() >= 4 {
hash = hash.wrapping_add(read_le32(input).wrapping_mul(PRIME32_3));
hash = hash.rotate_left(17).wrapping_mul(PRIME32_4);
input = &input[4..];
}
for &byte in input {
hash = hash.wrapping_add(u32::from(byte).wrapping_mul(PRIME32_5));
hash = hash.rotate_left(11).wrapping_mul(PRIME32_1);
}
XXH32_avalanche(hash)
}
fn hash32_bytes(input: &[u8], seed: u32) -> u32 {
let mut offset = 0;
let mut hash;
if input.len() >= 16 {
let mut accumulators = [
seed.wrapping_add(PRIME32_1).wrapping_add(PRIME32_2),
seed.wrapping_add(PRIME32_2),
seed,
seed.wrapping_sub(PRIME32_1),
];
while offset + 16 <= input.len() {
process_stripe32(&mut accumulators, &input[offset..offset + 16]);
offset += 16;
}
hash = accumulators[0]
.rotate_left(1)
.wrapping_add(accumulators[1].rotate_left(7))
.wrapping_add(accumulators[2].rotate_left(12))
.wrapping_add(accumulators[3].rotate_left(18));
} else {
hash = seed.wrapping_add(PRIME32_5);
}
hash = hash.wrapping_add(input.len() as u32);
finalize32(hash, &input[offset..])
}
#[inline]
fn process_stripe(accumulators: &mut [u64; 4], stripe: &[u8]) {
accumulators[0] = XXH64_round(accumulators[0], read_le64(&stripe[0..]));
accumulators[1] = XXH64_round(accumulators[1], read_le64(&stripe[8..]));
accumulators[2] = XXH64_round(accumulators[2], read_le64(&stripe[16..]));
accumulators[3] = XXH64_round(accumulators[3], read_le64(&stripe[24..]));
}
#[inline]
fn finalize(mut hash: u64, mut input: &[u8]) -> u64 {
while input.len() >= 8 {
let lane = XXH64_round(0, read_le64(input));
hash ^= lane;
hash = hash
.rotate_left(27)
.wrapping_mul(PRIME64_1)
.wrapping_add(PRIME64_4);
input = &input[8..];
}
if input.len() >= 4 {
hash ^= u64::from(read_le32(input)).wrapping_mul(PRIME64_1);
hash = hash
.rotate_left(23)
.wrapping_mul(PRIME64_2)
.wrapping_add(PRIME64_3);
input = &input[4..];
}
for &byte in input {
hash ^= u64::from(byte).wrapping_mul(PRIME64_5);
hash = hash.rotate_left(11).wrapping_mul(PRIME64_1);
}
XXH64_avalanche(hash)
}
fn hash_bytes(input: &[u8], seed: u64) -> u64 {
let mut offset = 0;
let mut hash;
if input.len() >= 32 {
let mut accumulators = [
seed.wrapping_add(PRIME64_1).wrapping_add(PRIME64_2),
seed.wrapping_add(PRIME64_2),
seed,
seed.wrapping_sub(PRIME64_1),
];
while offset + 32 <= input.len() {
process_stripe(&mut accumulators, &input[offset..offset + 32]);
offset += 32;
}
hash = accumulators[0]
.rotate_left(1)
.wrapping_add(accumulators[1].rotate_left(7))
.wrapping_add(accumulators[2].rotate_left(12))
.wrapping_add(accumulators[3].rotate_left(18));
for accumulator in accumulators {
hash = XXH64_mergeRound(hash, accumulator);
}
} else {
hash = seed.wrapping_add(PRIME64_5);
}
hash = hash.wrapping_add(input.len() as u64);
finalize(hash, &input[offset..])
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct XXH32_state_t {
pub total_len_32: u32,
pub large_len: u32,
pub v: [u32; 4],
pub mem32: [u32; 4],
pub memsize: u32,
pub reserved: u32,
}
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct XXH32_canonical_t {
pub digest: [u8; 4],
}
#[export_name = "ZSTD_XXH32"]
pub unsafe extern "C" fn XXH32(
input: *const c_void,
length: usize,
seed: XXH32_hash_t,
) -> XXH32_hash_t {
if input.is_null() {
if length != 0 {
return 0;
}
return hash32_bytes(&[], seed);
}
hash32_bytes(std::slice::from_raw_parts(input.cast::<u8>(), length), seed)
}
#[export_name = "ZSTD_XXH32_createState"]
pub unsafe extern "C" fn XXH32_createState() -> *mut XXH32_state_t {
libc::malloc(std::mem::size_of::<XXH32_state_t>()).cast::<XXH32_state_t>()
}
#[export_name = "ZSTD_XXH32_freeState"]
pub unsafe extern "C" fn XXH32_freeState(state: *mut XXH32_state_t) -> XXH_errorcode {
libc::free(state.cast::<c_void>());
XXH_OK
}
#[export_name = "ZSTD_XXH32_copyState"]
pub unsafe extern "C" fn XXH32_copyState(
destination: *mut XXH32_state_t,
source: *const XXH32_state_t,
) {
std::ptr::copy_nonoverlapping(source, destination, 1);
}
#[export_name = "ZSTD_XXH32_reset"]
pub unsafe extern "C" fn XXH32_reset(
state: *mut XXH32_state_t,
seed: XXH32_hash_t,
) -> XXH_errorcode {
if state.is_null() {
return XXH_ERROR;
}
std::ptr::write_bytes(state, 0, 1);
let state = &mut *state;
state.v[0] = seed.wrapping_add(PRIME32_1).wrapping_add(PRIME32_2);
state.v[1] = seed.wrapping_add(PRIME32_2);
state.v[2] = seed;
state.v[3] = seed.wrapping_sub(PRIME32_1);
XXH_OK
}
#[export_name = "ZSTD_XXH32_update"]
pub unsafe extern "C" fn XXH32_update(
state: *mut XXH32_state_t,
input: *const c_void,
length: usize,
) -> XXH_errorcode {
if state.is_null() {
return XXH_ERROR;
}
if input.is_null() {
return XXH_OK;
}
let state = &mut *state;
let input = std::slice::from_raw_parts(input.cast::<u8>(), length);
state.total_len_32 = state.total_len_32.wrapping_add(length as u32);
state.large_len |= u32::from(length >= 16 || state.total_len_32 >= 16);
let buffered = state.memsize as usize;
if buffered + length < 16 {
std::ptr::copy_nonoverlapping(
input.as_ptr(),
state.mem32.as_mut_ptr().cast::<u8>().add(buffered),
length,
);
state.memsize += length as u32;
return XXH_OK;
}
let mut offset = 0;
if buffered != 0 {
let fill = 16 - buffered;
std::ptr::copy_nonoverlapping(
input.as_ptr(),
state.mem32.as_mut_ptr().cast::<u8>().add(buffered),
fill,
);
let stripe = std::slice::from_raw_parts(state.mem32.as_ptr().cast::<u8>(), 16);
process_stripe32(&mut state.v, stripe);
offset = fill;
state.memsize = 0;
}
while offset + 16 <= length {
process_stripe32(&mut state.v, &input[offset..offset + 16]);
offset += 16;
}
let remaining = length - offset;
if remaining != 0 {
std::ptr::copy_nonoverlapping(
input.as_ptr().add(offset),
state.mem32.as_mut_ptr().cast::<u8>(),
remaining,
);
state.memsize = remaining as u32;
}
XXH_OK
}
#[export_name = "ZSTD_XXH32_digest"]
pub unsafe extern "C" fn XXH32_digest(state: *const XXH32_state_t) -> XXH32_hash_t {
if state.is_null() {
return 0;
}
let state = &*state;
let mut hash = if state.large_len != 0 {
state.v[0]
.rotate_left(1)
.wrapping_add(state.v[1].rotate_left(7))
.wrapping_add(state.v[2].rotate_left(12))
.wrapping_add(state.v[3].rotate_left(18))
} else {
state.v[2].wrapping_add(PRIME32_5)
};
hash = hash.wrapping_add(state.total_len_32);
let buffered =
std::slice::from_raw_parts(state.mem32.as_ptr().cast::<u8>(), state.memsize as usize);
finalize32(hash, buffered)
}
#[export_name = "ZSTD_XXH32_canonicalFromHash"]
pub unsafe extern "C" fn XXH32_canonicalFromHash(
destination: *mut XXH32_canonical_t,
hash: XXH32_hash_t,
) {
(*destination).digest = hash.to_be_bytes();
}
#[export_name = "ZSTD_XXH32_hashFromCanonical"]
pub unsafe extern "C" fn XXH32_hashFromCanonical(source: *const XXH32_canonical_t) -> XXH32_hash_t {
u32::from_be_bytes((*source).digest)
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct XXH64_state_t {
pub total_len: u64,
pub v: [u64; 4],
pub mem64: [u64; 4],
pub memsize: u32,
pub reserved32: u32,
pub reserved64: u64,
}
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct XXH64_canonical_t {
pub digest: [u8; 8],
}
#[export_name = "ZSTD_XXH64"]
pub unsafe extern "C" fn XXH64(
input: *const c_void,
length: usize,
seed: XXH64_hash_t,
) -> XXH64_hash_t {
if input.is_null() {
if length != 0 {
return 0;
}
return hash_bytes(&[], seed);
}
hash_bytes(std::slice::from_raw_parts(input.cast::<u8>(), length), seed)
}
#[export_name = "ZSTD_XXH64_createState"]
pub unsafe extern "C" fn XXH64_createState() -> *mut XXH64_state_t {
libc::malloc(std::mem::size_of::<XXH64_state_t>()).cast::<XXH64_state_t>()
}
#[export_name = "ZSTD_XXH64_freeState"]
pub unsafe extern "C" fn XXH64_freeState(state: *mut XXH64_state_t) -> XXH_errorcode {
libc::free(state.cast::<c_void>());
XXH_OK
}
#[export_name = "ZSTD_XXH64_copyState"]
pub unsafe extern "C" fn XXH64_copyState(
destination: *mut XXH64_state_t,
source: *const XXH64_state_t,
) {
std::ptr::copy_nonoverlapping(source, destination, 1);
}
#[export_name = "ZSTD_XXH64_reset"]
pub unsafe extern "C" fn XXH64_reset(
state: *mut XXH64_state_t,
seed: XXH64_hash_t,
) -> XXH_errorcode {
if state.is_null() {
return XXH_ERROR;
}
std::ptr::write_bytes(state, 0, 1);
let state = &mut *state;
state.v[0] = seed.wrapping_add(PRIME64_1).wrapping_add(PRIME64_2);
state.v[1] = seed.wrapping_add(PRIME64_2);
state.v[2] = seed;
state.v[3] = seed.wrapping_sub(PRIME64_1);
XXH_OK
}
#[export_name = "ZSTD_XXH64_update"]
pub unsafe extern "C" fn XXH64_update(
state: *mut XXH64_state_t,
input: *const c_void,
length: usize,
) -> XXH_errorcode {
if state.is_null() {
return XXH_ERROR;
}
if input.is_null() {
return XXH_OK;
}
let state = &mut *state;
let input = std::slice::from_raw_parts(input.cast::<u8>(), length);
state.total_len = state.total_len.wrapping_add(length as u64);
let buffered = state.memsize as usize;
if buffered + length < 32 {
std::ptr::copy_nonoverlapping(
input.as_ptr(),
state.mem64.as_mut_ptr().cast::<u8>().add(buffered),
length,
);
state.memsize += length as u32;
return XXH_OK;
}
let mut offset = 0;
if buffered != 0 {
let fill = 32 - buffered;
std::ptr::copy_nonoverlapping(
input.as_ptr(),
state.mem64.as_mut_ptr().cast::<u8>().add(buffered),
fill,
);
let stripe = std::slice::from_raw_parts(state.mem64.as_ptr().cast::<u8>(), 32);
process_stripe(&mut state.v, stripe);
offset = fill;
state.memsize = 0;
}
while offset + 32 <= length {
process_stripe(&mut state.v, &input[offset..offset + 32]);
offset += 32;
}
let remaining = length - offset;
if remaining != 0 {
std::ptr::copy_nonoverlapping(
input.as_ptr().add(offset),
state.mem64.as_mut_ptr().cast::<u8>(),
remaining,
);
state.memsize = remaining as u32;
}
XXH_OK
}
#[export_name = "ZSTD_XXH64_digest"]
pub unsafe extern "C" fn XXH64_digest(state: *const XXH64_state_t) -> XXH64_hash_t {
if state.is_null() {
return 0;
}
let state = &*state;
let mut hash = if state.total_len >= 32 {
let mut hash = state.v[0]
.rotate_left(1)
.wrapping_add(state.v[1].rotate_left(7))
.wrapping_add(state.v[2].rotate_left(12))
.wrapping_add(state.v[3].rotate_left(18));
for accumulator in state.v {
hash = XXH64_mergeRound(hash, accumulator);
}
hash
} else {
state.v[2].wrapping_add(PRIME64_5)
};
hash = hash.wrapping_add(state.total_len);
let buffered =
std::slice::from_raw_parts(state.mem64.as_ptr().cast::<u8>(), state.memsize as usize);
finalize(hash, buffered)
}
#[export_name = "ZSTD_XXH64_canonicalFromHash"]
pub unsafe extern "C" fn XXH64_canonicalFromHash(
destination: *mut XXH64_canonical_t,
hash: XXH64_hash_t,
) {
(*destination).digest = hash.to_be_bytes();
}
#[export_name = "ZSTD_XXH64_hashFromCanonical"]
pub unsafe extern "C" fn XXH64_hashFromCanonical(source: *const XXH64_canonical_t) -> XXH64_hash_t {
u64::from_be_bytes((*source).digest)
}
#[cfg(test)]
mod tests {
use super::*;
use std::mem::{offset_of, size_of, zeroed};
fn hash(input: &[u8], seed: u64) -> u64 {
unsafe { XXH64(input.as_ptr().cast(), input.len(), seed) }
}
fn hash32(input: &[u8], seed: u32) -> u32 {
unsafe { XXH32(input.as_ptr().cast(), input.len(), seed) }
}
#[test]
fn state_layout_matches_xxhash_h() {
assert_eq!(size_of::<XXH32_state_t>(), 48);
assert_eq!(offset_of!(XXH32_state_t, total_len_32), 0);
assert_eq!(offset_of!(XXH32_state_t, v), 8);
assert_eq!(offset_of!(XXH32_state_t, mem32), 24);
assert_eq!(offset_of!(XXH32_state_t, memsize), 40);
assert_eq!(size_of::<XXH64_state_t>(), 88);
assert_eq!(offset_of!(XXH64_state_t, total_len), 0);
assert_eq!(offset_of!(XXH64_state_t, v), 8);
assert_eq!(offset_of!(XXH64_state_t, mem64), 40);
assert_eq!(offset_of!(XXH64_state_t, memsize), 72);
assert_eq!(offset_of!(XXH64_state_t, reserved64), 80);
}
#[test]
fn reference_vectors_match_xxhash_0_8() {
let vectors: &[(&[u8], u64)] = &[
(b"", 0xef46_db37_51d8_e999),
(b"a", 0xd24e_c4f1_a98c_6e5b),
(b"abc", 0x44bc_2cf5_ad77_0999),
(b"message digest", 0x066e_d728_fcee_b3be),
(b"abcdefghijklmnopqrstuvwxyz", 0xcfe1_f278_fa89_835c),
(
b"1234567890123456789012345678901234567890",
0x5f3a_f5e2_3eeb_431d,
),
];
for &(input, expected) in vectors {
assert_eq!(hash(input, 0), expected, "input={input:?}");
}
let vectors32: &[(&[u8], u32)] = &[
(b"", 0x02cc_5d05),
(b"a", 0x550d_7456),
(b"abc", 0x32d1_53ff),
(b"message digest", 0x7c94_8494),
(b"abcdefghijklmnopqrstuvwxyz", 0x63a1_4d5f),
(b"1234567890123456789012345678901234567890", 0x765d_8c05),
];
for &(input, expected) in vectors32 {
assert_eq!(hash32(input, 0), expected, "input={input:?}");
}
}
#[test]
fn streaming_matches_one_shot_across_every_buffer_boundary() {
let input: Vec<u8> = (0..257)
.map(|index| (index as u8).wrapping_mul(101).wrapping_add(17))
.collect();
for &seed in &[0, 1, u64::MAX, 0x0123_4567_89ab_cdef] {
let expected = hash(&input, seed);
for chunk_size in 1..=65 {
let mut state: XXH64_state_t = unsafe { zeroed() };
assert_eq!(unsafe { XXH64_reset(&mut state, seed) }, XXH_OK);
for chunk in input.chunks(chunk_size) {
assert_eq!(
unsafe { XXH64_update(&mut state, chunk.as_ptr().cast(), chunk.len()) },
XXH_OK
);
}
assert_eq!(
unsafe { XXH64_digest(&state) },
expected,
"chunk={chunk_size}"
);
}
}
for &seed in &[0, 1, u32::MAX, 0x89ab_cdef] {
let expected = hash32(&input, seed);
for chunk_size in 1..=33 {
let mut state: XXH32_state_t = unsafe { zeroed() };
assert_eq!(unsafe { XXH32_reset(&mut state, seed) }, XXH_OK);
for chunk in input.chunks(chunk_size) {
assert_eq!(
unsafe { XXH32_update(&mut state, chunk.as_ptr().cast(), chunk.len()) },
XXH_OK
);
}
assert_eq!(
unsafe { XXH32_digest(&state) },
expected,
"chunk={chunk_size}"
);
}
}
}
#[test]
fn intermediate_digests_do_not_modify_state() {
let input: Vec<u8> = (0..96).map(|value| value as u8).collect();
let mut state: XXH64_state_t = unsafe { zeroed() };
assert_eq!(unsafe { XXH64_reset(&mut state, 7) }, XXH_OK);
for end in 0..=input.len() {
if end != 0 {
assert_eq!(
unsafe { XXH64_update(&mut state, input[end - 1..].as_ptr().cast(), 1) },
XXH_OK
);
}
assert_eq!(unsafe { XXH64_digest(&state) }, hash(&input[..end], 7));
}
}
#[test]
fn canonical_form_is_big_endian() {
let mut canonical32 = XXH32_canonical_t { digest: [0; 4] };
unsafe { XXH32_canonicalFromHash(&mut canonical32, 0x0123_4567) };
assert_eq!(canonical32.digest, [1, 0x23, 0x45, 0x67]);
assert_eq!(
unsafe { XXH32_hashFromCanonical(&canonical32) },
0x0123_4567
);
let mut canonical = XXH64_canonical_t { digest: [0; 8] };
unsafe { XXH64_canonicalFromHash(&mut canonical, 0x0123_4567_89ab_cdef) };
assert_eq!(
canonical.digest,
[1, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]
);
assert_eq!(
unsafe { XXH64_hashFromCanonical(&canonical) },
0x0123_4567_89ab_cdef
);
}
#[test]
fn null_empty_input_is_supported() {
assert_eq!(unsafe { XXH32(std::ptr::null(), 0, 0) }, 0x02cc_5d05);
let mut state32: XXH32_state_t = unsafe { zeroed() };
assert_eq!(unsafe { XXH32_reset(&mut state32, 0) }, XXH_OK);
assert_eq!(
unsafe { XXH32_update(&mut state32, std::ptr::null(), 0) },
XXH_OK
);
assert_eq!(unsafe { XXH32_digest(&state32) }, 0x02cc_5d05);
assert_eq!(
unsafe { XXH64(std::ptr::null(), 0, 0) },
0xef46_db37_51d8_e999
);
let mut state: XXH64_state_t = unsafe { zeroed() };
assert_eq!(unsafe { XXH64_reset(&mut state, 0) }, XXH_OK);
assert_eq!(
unsafe { XXH64_update(&mut state, std::ptr::null(), 0) },
XXH_OK
);
assert_eq!(unsafe { XXH64_digest(&state) }, 0xef46_db37_51d8_e999);
}
}