Skip to main content

axhash_core/
lib.rs

1#![cfg_attr(not(any(feature = "std", test)), no_std)]
2pub mod hash;
3
4mod bytes;
5mod constants;
6mod math;
7mod memory;
8
9// Convenience re-exports so users can write `use axhash_core::AxHasher`
10// instead of navigating the full module path.
11pub use hash::AxHasher;
12pub use hash::api::{axhash, axhash_of, axhash_of_seeded, axhash_seeded};
13pub use hash::build::AxBuildHasher;
14
15// The hardware acceleration backend selected at runtime.
16//
17// This enum is `#[non_exhaustive]`: future versions may add new variants
18// (e.g. RISC-V V, ARM SVE2, WASM SIMD). Always include a wildcard arm
19// when matching outside this crate.
20#[non_exhaustive]
21#[derive(Clone, Copy, Debug, Eq, PartialEq)]
22pub enum RuntimeBackend {
23    // Pure-Rust scalar path, available on all targets.
24    Scalar,
25    // AArch64 AES + NEON acceleration.
26    Aarch64AesNeon,
27    // x86-64 AES-NI + AVX2 acceleration.
28    X86_64AesAvx2,
29}
30
31// Returns the hardware acceleration backend active on this CPU.
32//
33// The result is determined once at the first call and then cached by the
34// OS/runtime. On `no_std` targets the result is determined at compile time
35// from `target_feature` flags.
36//
37// # Example
38//
39// ```rust
40// use axhash_core::{runtime_backend, RuntimeBackend};
41//
42// match runtime_backend() {
43//     RuntimeBackend::X86_64AesAvx2  => println!("AES-NI + AVX2"),
44//     RuntimeBackend::Aarch64AesNeon => println!("AES + NEON"),
45//     RuntimeBackend::Scalar         => println!("portable scalar"),
46//     _ => println!("unknown backend"),
47// }
48// ```
49#[inline(always)]
50pub fn runtime_backend() -> RuntimeBackend {
51    match bytes::selected_backend() {
52        bytes::Backend::Scalar => RuntimeBackend::Scalar,
53        #[cfg(target_arch = "aarch64")]
54        bytes::Backend::Aarch64AesNeon => RuntimeBackend::Aarch64AesNeon,
55        #[cfg(target_arch = "x86_64")]
56        bytes::Backend::X86_64AesAvx2 => RuntimeBackend::X86_64AesAvx2,
57    }
58}
59
60// Returns `true` when hardware AES acceleration is available.
61//
62// Equivalent to `runtime_backend() != RuntimeBackend::Scalar`.
63//
64// # Example
65//
66// ```rust
67// use axhash_core::runtime_has_aes;
68//
69// if runtime_has_aes() {
70//     println!("AES acceleration active");
71// }
72// ```
73#[inline(always)]
74pub fn runtime_has_aes() -> bool {
75    runtime_backend() != RuntimeBackend::Scalar
76}
77
78#[cfg(test)]
79mod tests {
80    use crate::RuntimeBackend;
81    use crate::hash::AxHasher;
82    use crate::hash::api::*;
83    use core::hash::{Hash, Hasher};
84
85    #[derive(Hash)]
86    struct DemoRecord {
87        id: u64,
88        shard: u32,
89        flags: u32,
90    }
91
92    #[test]
93    fn hash_is_deterministic_for_bytes() {
94        let data = b"axhash regression seed";
95        let a = axhash_seeded(data, 0x1234_5678_9abc_def0);
96        let b = axhash_seeded(data, 0x1234_5678_9abc_def0);
97        assert_eq!(a, b);
98    }
99
100    #[test]
101    fn hash_changes_when_seed_changes() {
102        let data = b"same payload different seed";
103        let a = axhash_seeded(data, 1);
104        let b = axhash_seeded(data, 2);
105        assert_ne!(a, b);
106    }
107
108    #[test]
109    fn hash_trait_path_is_deterministic() {
110        let record = DemoRecord {
111            id: 42,
112            shard: 7,
113            flags: 3,
114        };
115        let a = axhash_of_seeded(&record, 0xdead_beef);
116        let b = axhash_of_seeded(&record, 0xdead_beef);
117        assert_eq!(a, b);
118    }
119
120    #[test]
121    fn primitive_writes_produce_a_stable_finish() {
122        let mut hasher = AxHasher::new_with_seed(0x4444);
123        hasher.write_u64(0x0102_0304_0506_0708);
124        hasher.write_u32(0xaabb_ccdd);
125        hasher.write_u16(0xeeff);
126        hasher.write_u8(0x11);
127        let value = hasher.finish();
128        assert_ne!(value, 0);
129    }
130
131    #[test]
132    fn test_axhash_default_seed() {
133        let data = b"default seed test";
134        let a = axhash(data);
135        let b = axhash_seeded(data, 0);
136        assert_eq!(a, b);
137    }
138
139    #[test]
140    fn test_axhash_of_default_seed() {
141        let record = DemoRecord {
142            id: 1,
143            shard: 2,
144            flags: 3,
145        };
146        let a = axhash_of(&record);
147        let b = axhash_of_seeded(&record, 0);
148        assert_eq!(a, b);
149    }
150
151    #[test]
152    fn runtime_backend_smoke_test() {
153        match super::runtime_backend() {
154            RuntimeBackend::Scalar
155            | RuntimeBackend::Aarch64AesNeon
156            | RuntimeBackend::X86_64AesAvx2 => {}
157        }
158    }
159
160    // --- empty input ---
161
162    #[test]
163    fn empty_input_is_deterministic() {
164        assert_eq!(axhash(b""), axhash(b""));
165        assert_eq!(axhash_seeded(b"", 0), axhash_seeded(b"", 0));
166        assert_eq!(
167            axhash_seeded(b"", 0xdead_beef),
168            axhash_seeded(b"", 0xdead_beef)
169        );
170    }
171
172    #[test]
173    fn empty_input_changes_with_seed() {
174        assert_ne!(axhash_seeded(b"", 0), axhash_seeded(b"", 1));
175    }
176
177    #[test]
178    fn write_empty_bytes_is_noop() {
179        // Writing zero bytes must not change hasher output.
180        let seed = 0x1234_5678;
181
182        let mut h1 = AxHasher::new_with_seed(seed);
183        h1.write_u64(0xabcd);
184
185        let mut h2 = AxHasher::new_with_seed(seed);
186        h2.write(b"");
187        h2.write_u64(0xabcd);
188        h2.write(b"");
189
190        assert_eq!(h1.finish(), h2.finish());
191    }
192
193    // --- dispatch boundary lengths ---
194
195    #[test]
196    fn boundary_lengths_are_deterministic() {
197        // Covers every dispatch branch in hash_bytes_core:
198        // 0           → early return
199        // 1..=16      → hash_bytes_short
200        // 17..=32     → hash_bytes_17_32
201        // 33..=64     → hash_bytes_33_64
202        // 65..=128    → hash_bytes_65_128
203        // 129+        → hash_bytes_long
204        let data = vec![0xABu8; 300];
205        let boundaries = [
206            0usize, 1, 7, 8, 15, 16, 17, 24, 32, 33, 48, 64, 65, 96, 128, 129, 192, 256, 300,
207        ];
208        for &len in &boundaries {
209            let slice = &data[..len];
210            assert_eq!(
211                axhash(slice),
212                axhash(slice),
213                "non-deterministic at len={len}"
214            );
215        }
216    }
217
218    #[test]
219    fn adjacent_lengths_produce_different_hashes() {
220        // Changing length by one byte must change the hash.
221        let data = vec![0xCCu8; 300];
222        let boundaries = [1, 8, 16, 17, 32, 33, 64, 65, 128, 129];
223        for &len in &boundaries {
224            let h1 = axhash(&data[..len]);
225            let h2 = axhash(&data[..len + 1]);
226            assert_ne!(h1, h2, "collision at len={len} vs len={}", len + 1);
227        }
228    }
229
230    #[test]
231    fn finish_is_idempotent() {
232        // Calling finish() twice must return the same value and must not
233        // mutate the hasher (the clone-free implementation must uphold this).
234        let mut hasher = AxHasher::new_with_seed(0x9999);
235        hasher.write_u32(0x1234);
236        let a = hasher.finish();
237        let b = hasher.finish();
238        assert_eq!(a, b);
239    }
240
241    #[test]
242    fn default_equals_new() {
243        use core::hash::Hasher as _;
244        let mut h1 = AxHasher::default();
245        let mut h2 = AxHasher::new();
246        h1.write(b"hello");
247        h2.write(b"hello");
248        assert_eq!(h1.finish(), h2.finish());
249    }
250
251    // --- single-byte coverage ---
252
253    #[test]
254    fn all_single_bytes_are_distinct() {
255        // Every distinct single-byte input must produce a distinct hash.
256        // A collision here would indicate a catastrophic mixing failure.
257        let hashes: std::collections::HashSet<u64> = (0u8..=255).map(|b| axhash(&[b])).collect();
258        assert_eq!(hashes.len(), 256, "collision among single-byte inputs");
259    }
260
261    #[test]
262    fn single_byte_differs_from_empty() {
263        let empty = axhash(b"");
264        for b in 0u8..=255 {
265            assert_ne!(axhash(&[b]), empty, "byte {b} collides with empty input");
266        }
267    }
268
269    // --- seed independence ---
270
271    #[test]
272    fn distinct_seeds_produce_independent_families() {
273        // For a fixed input, 256 distinct seeds must yield 256 distinct hashes.
274        let input = b"seed independence check";
275        let hashes: std::collections::HashSet<u64> =
276            (0u64..256).map(|s| axhash_seeded(input, s)).collect();
277        assert_eq!(hashes.len(), 256, "seed collision detected");
278    }
279
280    // --- crate-root re-export consistency ---
281
282    #[test]
283    fn crate_root_reexports_match_hash_api() {
284        // Convenience re-exports at the crate root must produce the same
285        // result as the fully-qualified path.
286        use crate::{axhash as root_axhash, axhash_seeded as root_seeded};
287        let data = b"reexport consistency";
288        assert_eq!(root_axhash(data), axhash(data));
289        assert_eq!(root_seeded(data, 0xABCD), axhash_seeded(data, 0xABCD));
290    }
291}