1#[must_use]
5pub fn nwscript_string_hash_bytes(bytes: &[u8]) -> i32 {
6 let null_hash = xxh32(&[], 0);
7 let hash = xxh32(bytes, 0) ^ null_hash;
8 i32::from_ne_bytes(hash.to_ne_bytes())
9}
10
11#[must_use]
13pub fn nwscript_string_hash(input: &str) -> i32 {
14 nwscript_string_hash_bytes(input.as_bytes())
15}
16
17fn xxh32(input: &[u8], seed: u32) -> u32 {
18 const PRIME32_1: u32 = 2_654_435_761;
19 const PRIME32_2: u32 = 2_246_822_519;
20 const PRIME32_3: u32 = 3_266_489_917;
21 const PRIME32_4: u32 = 668_265_263;
22 const PRIME32_5: u32 = 374_761_393;
23
24 let len = input.len();
25 let mut index = 0usize;
26 let mut h32 = if len >= 16 {
27 let mut v1 = seed.wrapping_add(PRIME32_1).wrapping_add(PRIME32_2);
28 let mut v2 = seed.wrapping_add(PRIME32_2);
29 let mut v3 = seed;
30 let mut v4 = seed.wrapping_sub(PRIME32_1);
31
32 while index <= len - 16 {
33 v1 = xxh32_round(v1, read_le_u32(input, index));
34 index += 4;
35 v2 = xxh32_round(v2, read_le_u32(input, index));
36 index += 4;
37 v3 = xxh32_round(v3, read_le_u32(input, index));
38 index += 4;
39 v4 = xxh32_round(v4, read_le_u32(input, index));
40 index += 4;
41 }
42
43 rotate_left(v1, 1)
44 .wrapping_add(rotate_left(v2, 7))
45 .wrapping_add(rotate_left(v3, 12))
46 .wrapping_add(rotate_left(v4, 18))
47 } else {
48 seed.wrapping_add(PRIME32_5)
49 };
50
51 h32 = h32.wrapping_add(u32::try_from(len).ok().unwrap_or(u32::MAX));
52
53 while index + 4 <= len {
54 h32 = h32.wrapping_add(read_le_u32(input, index).wrapping_mul(PRIME32_3));
55 h32 = rotate_left(h32, 17).wrapping_mul(PRIME32_4);
56 index += 4;
57 }
58
59 while index < len {
60 let byte = input.get(index).copied().unwrap_or(0);
61 h32 = h32.wrapping_add(u32::from(byte).wrapping_mul(PRIME32_5));
62 h32 = rotate_left(h32, 11).wrapping_mul(PRIME32_1);
63 index += 1;
64 }
65
66 xxh32_avalanche(h32)
67}
68
69fn xxh32_round(seed: u32, input: u32) -> u32 {
70 const PRIME32_1: u32 = 2_654_435_761;
71 const PRIME32_2: u32 = 2_246_822_519;
72
73 let seed = seed.wrapping_add(input.wrapping_mul(PRIME32_2));
74 rotate_left(seed, 13).wrapping_mul(PRIME32_1)
75}
76
77fn xxh32_avalanche(mut hash: u32) -> u32 {
78 const PRIME32_2: u32 = 2_246_822_519;
79 const PRIME32_3: u32 = 3_266_489_917;
80
81 hash ^= hash >> 15;
82 hash = hash.wrapping_mul(PRIME32_2);
83 hash ^= hash >> 13;
84 hash = hash.wrapping_mul(PRIME32_3);
85 hash ^= hash >> 16;
86 hash
87}
88
89fn read_le_u32(bytes: &[u8], offset: usize) -> u32 {
90 let b0 = bytes.get(offset).copied().unwrap_or(0);
91 let b1 = bytes.get(offset + 1).copied().unwrap_or(0);
92 let b2 = bytes.get(offset + 2).copied().unwrap_or(0);
93 let b3 = bytes.get(offset + 3).copied().unwrap_or(0);
94 u32::from_le_bytes([b0, b1, b2, b3])
95}
96
97fn rotate_left(value: u32, amount: u32) -> u32 {
98 value.rotate_left(amount)
99}
100
101#[cfg(test)]
102mod tests {
103 use super::{nwscript_string_hash, nwscript_string_hash_bytes};
104
105 #[test]
106 fn matches_upstream_known_hash_values() {
107 assert_eq!(nwscript_string_hash(""), 0);
108 assert_eq!(nwscript_string_hash("hello"), -104060164);
109 }
110
111 #[test]
112 fn hashes_raw_byte_sequences_not_utf8_codepoints() {
113 let hash = nwscript_string_hash_bytes(&[b'"', b'\n', b'\\', 0xff, 0x80]);
114
115 assert_ne!(hash, 0);
116 }
117}