Skip to main content

nwnrs_nwscript/
hash.rs

1/// Returns the exact `NWScript` runtime hash for a cooked byte string.
2///
3/// Upstream computes this as `XXH32(bytes, 0) ^ XXH32("", 0, 0)`.
4#[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/// Returns the exact `NWScript` runtime hash for a UTF-8 string slice.
12#[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}