1#[derive(Clone, Copy)]
9pub struct NameHash {
10 pub md5: [u64; 2],
12 pub java_hash: i32,
14}
15
16impl NameHash {
17 #[must_use]
22 pub const fn new(name: &str) -> Self {
23 let digest = const_md5(name.as_bytes());
24 let lo = u64::from_be_bytes([
25 digest[0], digest[1], digest[2], digest[3], digest[4], digest[5], digest[6], digest[7],
26 ]);
27 let hi = u64::from_be_bytes([
28 digest[8], digest[9], digest[10], digest[11], digest[12], digest[13], digest[14],
29 digest[15],
30 ]);
31
32 Self {
33 md5: [lo, hi],
34 java_hash: java_hash_code(name),
35 }
36 }
37}
38
39const fn java_hash_code(s: &str) -> i32 {
43 let bytes = s.as_bytes();
44 let mut hash = 0_i32;
45 let mut i = 0;
46 while i < bytes.len() {
47 hash = hash.wrapping_mul(31).wrapping_add(bytes[i] as i32);
48 i += 1;
49 }
50 hash
51}
52
53const S: [u32; 64] = [
56 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9,
57 14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10, 15,
58 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
59];
60
61#[expect(
63 clippy::unreadable_literal,
64 reason = "RFC 1321 precomputed table; separators would obscure the standard values"
65)]
66const T: [u32; 64] = [
67 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
68 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
69 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
70 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
71 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
72 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
73 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
74 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391,
75];
76
77#[expect(
80 clippy::many_single_char_names,
81 reason = "matches RFC 1321 algorithm notation"
82)]
83#[expect(
85 clippy::unreadable_literal,
86 reason = "RFC 1321 initial hash values; underscores would obscure the standard constants"
87)]
88const fn const_md5(data: &[u8]) -> [u8; 16] {
89 assert!(
90 data.len() < 56,
91 "const_md5: messages >= 56 bytes not supported"
92 );
93
94 let mut block = [0u8; 64];
96 let mut i = 0;
97 while i < data.len() {
98 block[i] = data[i];
99 i += 1;
100 }
101 block[data.len()] = 0x80;
102
103 let bit_len = (data.len() as u64) * 8;
105 let len_bytes = bit_len.to_le_bytes();
106 i = 0;
107 while i < 8 {
108 block[56 + i] = len_bytes[i];
109 i += 1;
110 }
111
112 let mut m = [0u32; 16];
114 i = 0;
115 while i < 16 {
116 m[i] = u32::from_le_bytes([
117 block[i * 4],
118 block[i * 4 + 1],
119 block[i * 4 + 2],
120 block[i * 4 + 3],
121 ]);
122 i += 1;
123 }
124
125 let mut a: u32 = 0x67452301;
126 let mut b: u32 = 0xefcdab89;
127 let mut c: u32 = 0x98badcfe;
128 let mut d: u32 = 0x10325476;
129
130 i = 0;
131 while i < 64 {
132 let f;
133 let g;
134 if i < 16 {
135 f = (b & c) | (!b & d);
136 g = i;
137 } else if i < 32 {
138 f = (d & b) | (!d & c);
139 g = (5 * i + 1) % 16;
140 } else if i < 48 {
141 f = b ^ c ^ d;
142 g = (3 * i + 5) % 16;
143 } else {
144 f = c ^ (b | !d);
145 g = (7 * i) % 16;
146 }
147
148 let temp = d;
149 d = c;
150 c = b;
151 let x = a.wrapping_add(f).wrapping_add(T[i]).wrapping_add(m[g]);
152 b = b.wrapping_add(x.rotate_left(S[i]));
153 a = temp;
154
155 i += 1;
156 }
157
158 a = a.wrapping_add(0x67452301);
159 b = b.wrapping_add(0xefcdab89);
160 c = c.wrapping_add(0x98badcfe);
161 d = d.wrapping_add(0x10325476);
162
163 let ab = a.to_le_bytes();
164 let bb = b.to_le_bytes();
165 let cb = c.to_le_bytes();
166 let db = d.to_le_bytes();
167 [
168 ab[0], ab[1], ab[2], ab[3], bb[0], bb[1], bb[2], bb[3], cb[0], cb[1], cb[2], cb[3], db[0],
169 db[1], db[2], db[3],
170 ]
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn const_md5_matches_crate() {
179 let names = [
180 "minecraft:clay_bands",
181 "minecraft:offset",
182 "minecraft:aquifer",
183 "minecraft:ore",
184 "minecraft:terrain",
185 "minecraft:overworld",
186 "octave_0",
187 "octave_-7",
188 "TEST STRING",
189 "test_noise",
190 ];
191 for name in names {
192 let expected = md5::compute(name.as_bytes());
193 let actual = const_md5(name.as_bytes());
194 assert_eq!(
195 &actual, &*expected,
196 "MD5 mismatch for {name:?}: expected {expected:?}, got {actual:?}"
197 );
198 }
199 }
200
201 #[test]
202 fn java_hash_matches_legacy() {
203 assert_eq!(java_hash_code("minecraft:offset"), {
205 let mut hash = 0_i32;
206 for b in "minecraft:offset".encode_utf16() {
207 hash = hash.wrapping_mul(31).wrapping_add(i32::from(b));
208 }
209 hash
210 });
211 }
212
213 #[test]
214 fn name_hash_is_const() {
215 const HASH: NameHash = NameHash::new("minecraft:offset");
217 let _ = HASH;
218 }
219}