Skip to main content

steel_utils/
hash.rs

1//! CRC32C hashing for component validation.
2//!
3//! Minecraft uses CRC32C (Castagnoli) checksums to validate component data
4//! in serverbound packets. This module provides a hasher that matches
5//! Minecraft's `HashOps` implementation exactly.
6//!
7//! ## Type Tags
8//!
9//! Minecraft prefixes each value with a type tag byte before hashing:
10//! - Primitives: TAG_BYTE, TAG_SHORT, TAG_INT, TAG_LONG, TAG_FLOAT, TAG_DOUBLE
11//! - Boolean: TAG_BOOLEAN followed by 0x00 or 0x01
12//! - String: TAG_STRING followed by length (i32 BE) and UTF-8 bytes
13//! - Collections use start/end markers: TAG_MAP_START/END, TAG_LIST_START/END
14//!
15//! All numeric values are little-endian (matching Guava's Hasher).
16
17use simdnbt::owned::NbtTag;
18
19use crate::nbt::nbt_list_values;
20
21/// Type tags matching Minecraft's `HashOps` implementation.
22#[repr(u8)]
23#[derive(Clone, Copy)]
24pub enum HashTag {
25    /// Empty/null value tag.
26    Empty = 1,
27    /// Start of a map/object.
28    MapStart = 2,
29    /// End of a map/object.
30    MapEnd = 3,
31    /// Start of a list/array.
32    ListStart = 4,
33    /// End of a list/array.
34    ListEnd = 5,
35    /// Byte (i8) value.
36    Byte = 6,
37    /// Short (i16) value.
38    Short = 7,
39    /// Int (i32) value.
40    Int = 8,
41    /// Long (i64) value.
42    Long = 9,
43    /// Float (f32) value.
44    Float = 10,
45    /// Double (f64) value.
46    Double = 11,
47    /// String value.
48    String = 12,
49    /// Boolean value.
50    Boolean = 13,
51    /// Start of a byte array.
52    ByteArrayStart = 14,
53    /// End of a byte array.
54    ByteArrayEnd = 15,
55    /// Start of an int array.
56    IntArrayStart = 16,
57    /// End of an int array.
58    IntArrayEnd = 17,
59    /// Start of a long array.
60    LongArrayStart = 18,
61    /// End of a long array.
62    LongArrayEnd = 19,
63}
64
65/// A CRC32C hasher for component values.
66///
67/// This hasher is designed to produce the same hashes as Minecraft's
68/// `HashOps` implementation using Guava's `Hashing.crc32c()`.
69///
70/// # Example
71///
72/// ```
73/// use steel_utils::hash::ComponentHasher;
74///
75/// let mut hasher = ComponentHasher::new();
76/// hasher.put_int(42);
77/// let hash = hasher.finish();
78/// ```
79#[derive(Default)]
80pub struct ComponentHasher {
81    data: Vec<u8>,
82}
83
84impl ComponentHasher {
85    /// Creates a new hasher.
86    #[must_use]
87    pub const fn new() -> Self {
88        Self { data: Vec::new() }
89    }
90
91    /// Writes a raw tag byte.
92    fn put_tag(&mut self, tag: HashTag) {
93        self.data.push(tag as u8);
94    }
95
96    /// Writes raw bytes without any tag or length prefix.
97    pub fn put_raw_bytes(&mut self, bytes: &[u8]) {
98        self.data.extend_from_slice(bytes);
99    }
100
101    /// Writes the four-byte hash of a nested codec value.
102    ///
103    /// Vanilla lists contain child hashes rather than each child's unhashed payload.
104    pub fn put_component_hash<T: HashComponent + ?Sized>(&mut self, value: &T) {
105        self.put_raw_bytes(&value.compute_hash().to_le_bytes());
106    }
107
108    /// Hashes an empty/null value.
109    pub fn put_empty(&mut self) {
110        self.put_tag(HashTag::Empty);
111    }
112
113    /// Hashes a byte (i8) value with tag.
114    pub fn put_byte(&mut self, value: i8) {
115        self.put_tag(HashTag::Byte);
116        self.data.push(value as u8);
117    }
118
119    /// Hashes an unsigned byte (u8) value with tag.
120    pub fn put_ubyte(&mut self, value: u8) {
121        self.put_tag(HashTag::Byte);
122        self.data.push(value);
123    }
124
125    /// Hashes a short (i16) value with tag.
126    /// Guava uses little-endian byte order.
127    pub fn put_short(&mut self, value: i16) {
128        self.put_tag(HashTag::Short);
129        self.data.extend_from_slice(&value.to_le_bytes());
130    }
131
132    /// Hashes an int (i32) value with tag.
133    /// Guava uses little-endian byte order.
134    pub fn put_int(&mut self, value: i32) {
135        self.put_tag(HashTag::Int);
136        self.data.extend_from_slice(&value.to_le_bytes());
137    }
138
139    /// Hashes a long (i64) value with tag.
140    /// Guava uses little-endian byte order.
141    pub fn put_long(&mut self, value: i64) {
142        self.put_tag(HashTag::Long);
143        self.data.extend_from_slice(&value.to_le_bytes());
144    }
145
146    /// Hashes a float (f32) value with tag.
147    /// Guava uses little-endian byte order.
148    pub fn put_float(&mut self, value: f32) {
149        self.put_tag(HashTag::Float);
150        self.data.extend_from_slice(&value.to_bits().to_le_bytes());
151    }
152
153    /// Hashes a double (f64) value with tag.
154    /// Guava uses little-endian byte order.
155    pub fn put_double(&mut self, value: f64) {
156        self.put_tag(HashTag::Double);
157        self.data.extend_from_slice(&value.to_bits().to_le_bytes());
158    }
159
160    /// Hashes a boolean value with tag.
161    pub fn put_bool(&mut self, value: bool) {
162        self.put_tag(HashTag::Boolean);
163        self.data.push(u8::from(value));
164    }
165
166    /// Hashes a string value with tag, length prefix, and UTF-16 LE characters.
167    ///
168    /// This matches Guava's Hasher which uses little-endian for all primitives:
169    /// - `putInt(length)` writes length as 4 bytes little-endian
170    /// - `putUnencodedChars` writes each char as 2 bytes little-endian
171    pub fn put_string(&mut self, value: &str) {
172        self.put_tag(HashTag::String);
173        // Length is the number of UTF-16 code units, not bytes
174        // Guava uses little-endian for putInt
175        let char_count: i32 = value.chars().map(|c| c.len_utf16() as i32).sum();
176        self.data.extend_from_slice(&char_count.to_le_bytes());
177        // Write each UTF-16 code unit as little-endian (low byte first, then high byte)
178        // This matches Guava's putUnencodedChars behavior
179        for c in value.chars() {
180            let mut buf = [0u16; 2];
181            let encoded = c.encode_utf16(&mut buf);
182            for code_unit in encoded {
183                self.data.extend_from_slice(&code_unit.to_le_bytes());
184            }
185        }
186    }
187
188    /// Starts a map/object. Call `end_map()` when done adding entries.
189    pub fn start_map(&mut self) {
190        self.put_tag(HashTag::MapStart);
191    }
192
193    /// Ends a map/object.
194    pub fn end_map(&mut self) {
195        self.put_tag(HashTag::MapEnd);
196    }
197
198    /// Starts a list. Call `end_list()` when done adding elements.
199    pub fn start_list(&mut self) {
200        self.put_tag(HashTag::ListStart);
201    }
202
203    /// Ends a list.
204    pub fn end_list(&mut self) {
205        self.put_tag(HashTag::ListEnd);
206    }
207
208    /// Starts a byte array. Call `end_byte_array()` when done.
209    pub fn start_byte_array(&mut self) {
210        self.put_tag(HashTag::ByteArrayStart);
211    }
212
213    /// Ends a byte array.
214    pub fn end_byte_array(&mut self) {
215        self.put_tag(HashTag::ByteArrayEnd);
216    }
217
218    /// Starts an int array. Call `end_int_array()` when done.
219    pub fn start_int_array(&mut self) {
220        self.put_tag(HashTag::IntArrayStart);
221    }
222
223    /// Writes an int value without tag (for use inside int arrays).
224    /// Guava uses little-endian byte order.
225    pub fn put_int_raw(&mut self, value: i32) {
226        self.data.extend_from_slice(&value.to_le_bytes());
227    }
228
229    /// Ends an int array.
230    pub fn end_int_array(&mut self) {
231        self.put_tag(HashTag::IntArrayEnd);
232    }
233
234    /// Starts a long array. Call `end_long_array()` when done.
235    pub fn start_long_array(&mut self) {
236        self.put_tag(HashTag::LongArrayStart);
237    }
238
239    /// Writes a long value without tag (for use inside long arrays).
240    /// Guava uses little-endian byte order.
241    pub fn put_long_raw(&mut self, value: i64) {
242        self.data.extend_from_slice(&value.to_le_bytes());
243    }
244
245    /// Ends a long array.
246    pub fn end_long_array(&mut self) {
247        self.put_tag(HashTag::LongArrayEnd);
248    }
249
250    /// Hashes a byte array with start/end markers.
251    pub fn put_byte_array(&mut self, bytes: &[u8]) {
252        self.start_byte_array();
253        self.data.extend_from_slice(bytes);
254        self.end_byte_array();
255    }
256
257    /// Hashes an int array with start/end markers.
258    pub fn put_int_array(&mut self, values: &[i32]) {
259        self.start_int_array();
260        for &v in values {
261            self.put_int_raw(v);
262        }
263        self.end_int_array();
264    }
265
266    /// Hashes a long array with start/end markers.
267    pub fn put_long_array(&mut self, values: &[i64]) {
268        self.start_long_array();
269        for &v in values {
270            self.put_long_raw(v);
271        }
272        self.end_long_array();
273    }
274
275    /// Returns the current hash data (for nested hashing).
276    #[must_use]
277    pub fn current_data(&self) -> &[u8] {
278        &self.data
279    }
280
281    /// Finishes hashing and returns the CRC32C checksum as i32.
282    #[must_use]
283    pub fn finish(self) -> i32 {
284        crc32c::crc32c(&self.data) as i32
285    }
286}
287
288/// A hash entry for map sorting.
289///
290/// Vanilla Minecraft hashes each key and value, then sorts by these hashes,
291/// and writes ONLY the 4-byte hash values (not the original encoded bytes) to the final hasher.
292#[derive(Clone)]
293pub struct HashEntry {
294    /// The hash of the key data (for sorting).
295    pub key_hash: i64,
296    /// The hash of the value data (for sorting).
297    pub value_hash: i64,
298    /// The 4-byte CRC32C hash of the key (to be written to the final hasher).
299    pub key_bytes: [u8; 4],
300    /// The 4-byte CRC32C hash of the value (to be written to the final hasher).
301    pub value_bytes: [u8; 4],
302}
303
304impl HashEntry {
305    /// Creates a new hash entry.
306    #[must_use]
307    pub fn new(key_hasher: ComponentHasher, value_hasher: ComponentHasher) -> Self {
308        let key_bytes = crc32c::crc32c(&key_hasher.data);
309        let value_bytes = crc32c::crc32c(&value_hasher.data);
310        Self::from_hashes(key_bytes, value_bytes)
311    }
312
313    /// Creates a map entry from child hashes already computed by dispatched codecs.
314    #[must_use]
315    pub const fn from_hashes(key_hash: u32, value_hash: u32) -> Self {
316        Self {
317            key_hash: key_hash as i64,
318            value_hash: value_hash as i64,
319            key_bytes: key_hash.to_le_bytes(),
320            value_bytes: value_hash.to_le_bytes(),
321        }
322    }
323}
324
325/// Sorts map entries according to Minecraft's ordering:
326/// First by key hash, then by value hash (both as padded longs).
327pub fn sort_map_entries(entries: &mut [HashEntry]) {
328    entries.sort_by(|a, b| {
329        a.key_hash
330            .cmp(&b.key_hash)
331            .then_with(|| a.value_hash.cmp(&b.value_hash))
332    });
333}
334
335/// Trait for types that can be hashed for component validation.
336pub trait HashComponent {
337    /// Hashes this value into the given hasher.
338    fn hash_component(&self, hasher: &mut ComponentHasher);
339
340    /// Computes the hash of this value.
341    fn compute_hash(&self) -> i32 {
342        let mut hasher = ComponentHasher::new();
343        self.hash_component(&mut hasher);
344        hasher.finish()
345    }
346}
347
348// Implement HashComponent for primitive types
349impl HashComponent for i8 {
350    fn hash_component(&self, hasher: &mut ComponentHasher) {
351        hasher.put_byte(*self);
352    }
353}
354
355impl HashComponent for u8 {
356    fn hash_component(&self, hasher: &mut ComponentHasher) {
357        hasher.put_ubyte(*self);
358    }
359}
360
361impl HashComponent for i16 {
362    fn hash_component(&self, hasher: &mut ComponentHasher) {
363        hasher.put_short(*self);
364    }
365}
366
367impl HashComponent for i32 {
368    fn hash_component(&self, hasher: &mut ComponentHasher) {
369        hasher.put_int(*self);
370    }
371}
372
373impl HashComponent for i64 {
374    fn hash_component(&self, hasher: &mut ComponentHasher) {
375        hasher.put_long(*self);
376    }
377}
378
379impl HashComponent for f32 {
380    fn hash_component(&self, hasher: &mut ComponentHasher) {
381        hasher.put_float(*self);
382    }
383}
384
385impl HashComponent for f64 {
386    fn hash_component(&self, hasher: &mut ComponentHasher) {
387        hasher.put_double(*self);
388    }
389}
390
391impl HashComponent for bool {
392    fn hash_component(&self, hasher: &mut ComponentHasher) {
393        hasher.put_bool(*self);
394    }
395}
396
397impl HashComponent for str {
398    fn hash_component(&self, hasher: &mut ComponentHasher) {
399        hasher.put_string(self);
400    }
401}
402
403impl HashComponent for String {
404    fn hash_component(&self, hasher: &mut ComponentHasher) {
405        hasher.put_string(self);
406    }
407}
408
409impl HashComponent for () {
410    fn hash_component(&self, hasher: &mut ComponentHasher) {
411        hasher.start_map();
412        hasher.end_map();
413    }
414}
415
416impl HashComponent for NbtTag {
417    fn hash_component(&self, hasher: &mut ComponentHasher) {
418        match self {
419            NbtTag::Byte(value) => hasher.put_byte(*value),
420            NbtTag::Short(value) => hasher.put_short(*value),
421            NbtTag::Int(value) => hasher.put_int(*value),
422            NbtTag::Long(value) => hasher.put_long(*value),
423            NbtTag::Float(value) => hasher.put_float(*value),
424            NbtTag::Double(value) => hasher.put_double(*value),
425            NbtTag::ByteArray(values) => hasher.put_byte_array(values),
426            NbtTag::String(value) => hasher.put_string(&value.to_string()),
427            NbtTag::List(values) => {
428                hasher.start_list();
429                for value in nbt_list_values(values) {
430                    hasher.put_component_hash(&value);
431                }
432                hasher.end_list();
433            }
434            NbtTag::Compound(values) => {
435                let mut entries = values
436                    .iter()
437                    .map(|(key, value)| {
438                        let mut key_hasher = ComponentHasher::new();
439                        key_hasher.put_string(&key.to_string());
440                        let mut value_hasher = ComponentHasher::new();
441                        value.hash_component(&mut value_hasher);
442                        HashEntry::new(key_hasher, value_hasher)
443                    })
444                    .collect::<Vec<_>>();
445                sort_map_entries(&mut entries);
446
447                hasher.start_map();
448                for entry in entries {
449                    hasher.put_raw_bytes(&entry.key_bytes);
450                    hasher.put_raw_bytes(&entry.value_bytes);
451                }
452                hasher.end_map();
453            }
454            NbtTag::IntArray(values) => hasher.put_int_array(values),
455            NbtTag::LongArray(values) => hasher.put_long_array(values),
456        }
457    }
458}
459
460#[cfg(test)]
461mod tests {
462    use super::*;
463
464    #[test]
465    fn test_int_hash() {
466        let mut hasher = ComponentHasher::new();
467        hasher.put_int(42);
468        let hash = hasher.finish();
469        // Verify format: [TAG_INT=8] [00 00 00 2A]
470        assert_ne!(hash, 0);
471    }
472
473    #[test]
474    fn test_string_hash() {
475        let mut hasher = ComponentHasher::new();
476        hasher.put_string("hello");
477        let hash = hasher.finish();
478        // Verify format: [TAG_STRING=12] [00 00 00 05] [h e l l o]
479        assert_ne!(hash, 0);
480    }
481
482    #[test]
483    fn test_bool_hash() {
484        let mut hasher_true = ComponentHasher::new();
485        hasher_true.put_bool(true);
486        let hash_true = hasher_true.finish();
487
488        let mut hasher_false = ComponentHasher::new();
489        hasher_false.put_bool(false);
490        let hash_false = hasher_false.finish();
491
492        // true and false should produce different hashes
493        assert_ne!(hash_true, hash_false);
494    }
495
496    #[test]
497    fn test_empty_map_hash() {
498        let mut hasher = ComponentHasher::new();
499        hasher.start_map();
500        hasher.end_map();
501        let hash = hasher.finish();
502        // Format: [TAG_MAP_START=2] [TAG_MAP_END=3]
503        assert_ne!(hash, 0);
504    }
505
506    #[test]
507    fn unit_codec_hashes_as_an_empty_map() {
508        let mut hasher = ComponentHasher::new();
509        hasher.start_map();
510        hasher.end_map();
511
512        assert_eq!(().compute_hash(), hasher.finish());
513    }
514
515    #[test]
516    fn test_empty_list_hash() {
517        let mut hasher = ComponentHasher::new();
518        hasher.start_list();
519        hasher.end_list();
520        let hash = hasher.finish();
521        // Format: [TAG_LIST_START=4] [TAG_LIST_END=5]
522        assert_ne!(hash, 0);
523    }
524
525    #[test]
526    fn test_byte_array_hash() {
527        let mut hasher = ComponentHasher::new();
528        hasher.put_byte_array(&[1, 2, 3, 4]);
529        let hash = hasher.finish();
530        // Format: [TAG_BYTE_ARRAY_START=14] [01 02 03 04] [TAG_BYTE_ARRAY_END=15]
531        assert_ne!(hash, 0);
532    }
533
534    #[test]
535    fn test_deterministic() {
536        // Same input should always produce same hash
537        let hash1 = {
538            let mut h = ComponentHasher::new();
539            h.put_int(12345);
540            h.put_string("test");
541            h.finish()
542        };
543        let hash2 = {
544            let mut h = ComponentHasher::new();
545            h.put_int(12345);
546            h.put_string("test");
547            h.finish()
548        };
549        assert_eq!(hash1, hash2);
550    }
551
552    #[test]
553    fn nbt_compound_hash_is_independent_of_entry_order() {
554        use simdnbt::owned::NbtCompound;
555
556        let first = NbtTag::Compound(NbtCompound::from_values(vec![
557            ("first".into(), NbtTag::Int(1)),
558            ("second".into(), NbtTag::String("two".into())),
559        ]));
560        let reversed = NbtTag::Compound(NbtCompound::from_values(vec![
561            ("second".into(), NbtTag::String("two".into())),
562            ("first".into(), NbtTag::Int(1)),
563        ]));
564
565        assert_eq!(first.compute_hash(), reversed.compute_hash());
566    }
567
568    #[test]
569    fn nbt_list_hash_unwraps_vanillas_heterogeneous_list_marker() {
570        use simdnbt::owned::{NbtCompound, NbtList};
571
572        let mut wrapper = NbtCompound::new();
573        wrapper.insert("", 7);
574        let encoded = NbtTag::List(NbtList::Compound(vec![wrapper]));
575
576        let mut expected = ComponentHasher::new();
577        expected.start_list();
578        expected.put_component_hash(&NbtTag::Int(7));
579        expected.end_list();
580
581        assert_eq!(encoded.compute_hash(), expected.finish());
582    }
583
584    #[test]
585    fn test_text_component_steel() {
586        use text_components::TextComponent;
587
588        // A simple text component with just "Steel" should collapse to a string
589        let component = TextComponent::from("Steel");
590        let hash = component.compute_hash();
591
592        // Expected hash from vanilla Minecraft client
593        assert_eq!(hash, -25_646_594, "Hash should match vanilla client");
594    }
595
596    #[test]
597    fn test_text_component_simple_styled() {
598        use text_components::TextComponent;
599        use text_components::{Modifier, format::Color};
600
601        // Simple styled component: {"text":"R","color":"red","bold":true}
602        // Expected hash from vanilla client 1.21.11: 1605556242
603        let component = TextComponent::plain("R").color(Color::Red).bold(true);
604        let hash = component.compute_hash();
605
606        assert_eq!(
607            hash, 1_605_556_242,
608            "Hash should match vanilla client 1.21.11 for simple styled text"
609        );
610    }
611
612    #[test]
613    fn test_text_component_rainbow() {
614        use text_components::TextComponent;
615
616        // Rainbow text from the issue
617        let json = r##"[{"text":"R","color":"red","bold":true},{"text":"a","color":"#ff5a00"},{"text":"i","color":"yellow","bold":true},{"text":"n","color":"green"},{"text":"b","color":"aqua","bold":true},{"text":"o","color":"blue"},{"text":"w","color":"light_purple","bold":true}]"##;
618        let component = TextComponent::from_snbt(json).expect("Failed to parse rainbow text");
619        let hash = component.compute_hash();
620
621        // Expected hash from vanilla Minecraft client (from the error message)
622        assert_eq!(
623            hash, 796_582_470,
624            "Hash should match vanilla client for rainbow text"
625        );
626    }
627}