lib_bcsv_jmap/hash.rs
1use std::collections::HashMap;
2use std::fs::File;
3use std::io::{BufRead, BufReader};
4use std::path::Path;
5
6use crate::error::{JMapError, Result};
7
8/// The hash function used by Super Mario Galaxy 1
9///
10/// # Arguments
11/// - `field_name` - The ASCII field name to hash
12///
13/// # Returns
14/// A 32-bit hash value
15pub fn calc_hash(field_name: &str) -> u32 {
16 let mut hash: u32 = 0;
17
18 for byte in field_name.bytes() {
19 let ch = if byte & 0x80 != 0 {
20 byte as i8 as i32
21 } else {
22 byte as i32
23 };
24
25 hash = hash.wrapping_mul(31).wrapping_add(ch as u32);
26 }
27
28 hash
29}
30
31/// Trait for hash table implementations
32pub trait HashTable {
33 /// Calculate the hash for a field name
34 ///
35 /// # Arguments
36 /// - `field_name` - The field name to hash
37 ///
38 /// # Returns
39 /// The hash value corresponding to the given field name
40 fn calc(&self, field_name: &str) -> u32;
41
42 /// Find the field name for a given hash
43 /// Returns a hex representation like `[DEADBEEF]` if not found
44 /// # Arguments
45 /// - `hash` - The hash value to look up
46 ///
47 /// # Returns
48 /// The field name corresponding to the given hash, or a hex string if not found
49 fn find(&self, hash: u32) -> String;
50
51 /// Add a field name to the lookup table and return its hash
52 ///
53 /// # Arguments
54 /// - `field_name` - The field name to add to the lookup table
55 ///
56 /// # Returns
57 /// The hash value corresponding to the added field name
58 fn add(&mut self, field_name: &str) -> u32;
59}
60
61/// Type of hash algorithm to use
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum HashAlgorithm {
64 SMG,
65}
66
67impl HashAlgorithm {
68 /// Calculate hash using this algorithm
69 ///
70 /// # Arguments
71 /// - `field_name` - The field name to hash
72 ///
73 /// # Returns
74 /// The calculated hash value base of the hash algorithm
75 pub fn calc(&self, field_name: &str) -> u32 {
76 match self {
77 HashAlgorithm::SMG => calc_hash(field_name),
78 }
79 }
80}
81
82/// A hash lookup table backed by a file of known field names
83#[derive(Debug, Clone)]
84pub struct FileHashTable {
85 algorithm: HashAlgorithm,
86 lookup: HashMap<u32, String>,
87}
88
89impl FileHashTable {
90 /// Create a new empty hash table with the given algorithm
91 ///
92 /// # Arguments
93 /// - `algorithm` - The hash algorithm to use for calculating hashes
94 ///
95 /// # Returns
96 /// A new `FileHashTable` instance with the specified algorithm and an empty lookup table
97 pub fn new(algorithm: HashAlgorithm) -> Self {
98 Self {
99 algorithm,
100 lookup: HashMap::new(),
101 }
102 }
103
104 /// Create a new hash table with the given algorithm and lookup file
105 ///
106 /// The lookup file should contain one field name per line
107 /// Lines starting with '#' are treated as comments
108 ///
109 /// # Arguments
110 /// - `algorithm` - The hash algorithm to use for calculating hashes
111 /// - `path` - The path to the lookup file containing field names
112 ///
113 /// # Types
114 /// - `P` - A type that can be converted to a `Path` reference, such as `&str` or `String`
115 ///
116 /// # Errors
117 /// - If the file cannot be opened, a `JMapError::LookupFileNotFound` error is returned with the file path
118 ///
119 /// # Returns
120 /// A `Result` containing the new `FileHashTable` instance if successful, or a `JMapError` if the file cannot be read
121 pub fn from_file<P: AsRef<Path>>(algorithm: HashAlgorithm, path: P) -> Result<Self> {
122 let path = path.as_ref();
123 let file = File::open(path).map_err(|_| {
124 JMapError::LookupFileNotFound(path.display().to_string())
125 })?;
126
127 let reader = BufReader::new(file);
128 let mut lookup = HashMap::new();
129
130 for line in reader.lines() {
131 let line = line?;
132 let line = line.trim();
133
134 // Skip comments and empty lines
135 if line.is_empty() || line.starts_with('#') {
136 continue;
137 }
138
139 let hash = algorithm.calc(line);
140 lookup.insert(hash, line.to_string());
141 }
142
143 Ok(Self { algorithm, lookup })
144 }
145
146 /// Get the hash algorithm used by this table
147 ///
148 /// # Returns
149 /// The `HashAlgorithm` instance representing the hash algorithm used by this `FileHashTable`
150 pub fn algorithm(&self) -> HashAlgorithm {
151 self.algorithm
152 }
153}
154
155/// Implementation of the `HashTable` trait for `FileHashTable`
156/// This allows `FileHashTable` to be used wherever a `HashTable` is expected, providing methods to calculate hashes, find field names by hash, and add new field names to the lookup table
157impl HashTable for FileHashTable {
158 fn calc(&self, field_name: &str) -> u32 {
159 self.algorithm.calc(field_name)
160 }
161
162 fn find(&self, hash: u32) -> String {
163 self.lookup
164 .get(&hash)
165 .cloned()
166 .unwrap_or_else(|| format!("[{:08X}]", hash))
167 }
168
169 fn add(&mut self, field_name: &str) -> u32 {
170 let hash = self.calc(field_name);
171 self.lookup.entry(hash).or_insert_with(|| field_name.to_string());
172 hash
173 }
174}
175
176/// Create a hash table configured for Super Mario Galaxy 1/2
177///
178/// This uses the JGadget hash algorithm and loads the lookup file
179/// from the default location if available
180pub fn smg_hash_table() -> FileHashTable {
181 FileHashTable::new(HashAlgorithm::SMG)
182}
183
184/// Create a hash table for Super Mario Galaxy with a custom lookup file
185pub fn smg_hash_table_with_lookup<P: AsRef<Path>>(path: P) -> Result<FileHashTable> {
186 FileHashTable::from_file(HashAlgorithm::SMG, path)
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn test_hash() {
195 /// Known hash values from Super Mario Galaxy (verified with this [hash calculator](https://mariogalaxy.org/hash))
196 assert_eq!(calc_hash("ScenarioNo"), 0xED08B591);
197 assert_eq!(calc_hash("ZoneName"), 0x3666C077);
198 }
199
200 #[test]
201 fn test_hash_table() {
202 let mut table = smg_hash_table();
203
204 // Add a field and verify lookup
205 let hash = table.add("TestField");
206 assert_eq!(table.find(hash), "TestField");
207
208 // Unknown hash should return hex representation
209 let unknown = table.find(0xDEADBEEF);
210 assert_eq!(unknown, "[DEADBEEF]");
211 }
212}