cdragon_hashes/
lib.rs

1//! Tools to work with hashes, as used by cdragon
2//!
3//! Actual hash values are created with [crate::define_hash_type!()], which implements [HashDef] and
4//! conversions.
5//!
6//! [HashMapper] manages a mapping to retrieve a string from a hash value.
7//! The type provides methods to load mapping files, check for known hashes, etc.
8//! update mapping files, etc.
9use std::fmt;
10use std::fs::File;
11use std::io::{BufReader, BufRead, BufWriter, Write};
12use std::collections::HashMap;
13use std::path::Path;
14use std::hash::Hash;
15use num_traits::Num;
16use thiserror::Error;
17use cdragon_utils::GuardedFile;
18
19#[cfg(feature = "bin")]
20pub mod bin;
21#[cfg(feature = "rst")]
22pub mod rst;
23#[cfg(feature = "wad")]
24pub mod wad;
25
26type Result<T, E = HashError> = std::result::Result<T, E>;
27
28
29/// Hash related error
30///
31/// For now, it is only used when parsing hash mappings.
32#[allow(missing_docs)]
33#[derive(Error, Debug)]
34pub enum HashError {
35    #[error(transparent)]
36    Io(#[from] std::io::Error),
37    #[error("invalid hash line: {0:?}")]
38    InvalidHashLine(String),
39    #[error("invalid hash value: {0:?}")]
40    InvalidHashValue(String),
41}
42
43
44/// Store hash-to-string association for a hash value
45///
46/// A hash mapping can be loaded from and written to files.
47/// Such files store one line per hash, formatted as `<hex-value> <string>`.
48#[derive(Default)]
49pub struct HashMapper<T, const NBITS: usize> where T: Hash {
50    map: HashMap<T, String>,
51}
52
53impl<T, const NBITS: usize> HashMapper<T, NBITS> where T: Hash {
54    /// Number of characters used to format the hash
55    const NCHARS: usize = NBITS.div_ceil(4);
56}
57
58impl<T, const N: usize> HashMapper<T, N> where T: Eq + Hash + Copy {
59    /// Create a new, empty mapping
60    pub fn new() -> Self {
61        Self { map: HashMap::<T, String>::new() }
62    }
63
64    /// Get a value from the mapping
65    pub fn get(&self, hash: T) -> Option<&str> {
66        self.map.get(&hash).map(|v| v.as_ref())
67    }
68
69    /// Return a matching string (if known) or the hash
70    ///
71    /// Use this method to get a string representation with a fallback for unknown hashes.
72    /// ```
73    /// # use cdragon_hashes::HashMapper;
74    /// let mut mapper = HashMapper::<u16>::new();
75    /// mapper.insert(42, "forty-two".to_string());
76    /// assert_eq!(format!("{}", mapper.seek(42)), "forty-two");
77    /// assert_eq!(format!("{}", mapper.seek(0x1234)), "{1234}");
78    /// ```
79    pub fn seek(&self, hash: T) -> HashOrStr<T, &str> {
80        match self.map.get(&hash) {
81            Some(s) => HashOrStr::Str(s.as_ref()),
82            None => HashOrStr::Hash(hash),
83        }
84    }
85
86    /// Return `true` if the mapping is empty
87    pub fn is_empty(&self) -> bool {
88        self.map.is_empty()
89    }
90
91    /// Return `true` if the given hash is known
92    pub fn is_known(&self, hash: T) -> bool {
93        self.map.contains_key(&hash)
94    }
95
96    /// Add a hash to the mapper
97    ///
98    /// **Important:** the caller must ensure the value matches the hash.
99    pub fn insert(&mut self, hash: T, value: String) {
100        self.map.insert(hash, value);
101    }
102}
103
104impl<T, const N: usize> HashMapper<T, N> where T: Num + Eq + Hash + Copy {
105    /// Create a new mapping, loaded from a reader
106    pub fn from_reader<R: BufRead>(reader: R) -> Result<Self> {
107        let mut this = Self::new();
108        this.load_reader(reader)?;
109        Ok(this)
110    }
111
112    /// Create a new mapping, loaded from a file
113    pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
114        let mut this = Self::new();
115        this.load_path(&path)?;
116        Ok(this)
117    }
118
119    /// Load hash mapping from a reader
120    pub fn load_reader<R: BufRead>(&mut self, reader: R) -> Result<(), HashError> {
121        for line in reader.lines() {
122            let l = line?;
123            if l.len() < Self::NCHARS + 2 {
124                return Err(HashError::InvalidHashLine(l));
125            }
126            let hash = T::from_str_radix(&l[..Self::NCHARS], 16).map_err(|_e| {
127                HashError::InvalidHashValue(l[..Self::NCHARS].to_string())
128            })?;
129            self.map.insert(hash, l[Self::NCHARS+1..].to_string());
130        }
131        Ok(())
132    }
133
134    /// Load hash mapping from a file
135    pub fn load_path<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
136        let file = File::open(&path)?;
137        self.load_reader(BufReader::new(file))?;
138        Ok(())
139    }
140}
141
142impl<T, const N: usize> HashMapper<T, N> where T: Eq + Hash + Copy + fmt::LowerHex {
143    /// Write hash mapping to a writer
144    pub fn write<W: Write>(&self, writer: &mut W) -> std::io::Result<()> {
145        let mut entries: Vec<_> = self.map.iter().collect();
146        entries.sort_by_key(|kv| kv.1);
147        for (h, s) in entries {
148            writeln!(writer, "{:0w$x} {}", h, s, w = Self::NCHARS)?;
149        }
150        Ok(())
151    }
152
153    /// Write hash map to a file
154    ///
155    /// File is upadeted atomically.
156    pub fn write_path<P: AsRef<Path>>(&self, path: P) -> std::io::Result<()> {
157        GuardedFile::for_scope(path, |file| {
158            self.write(&mut BufWriter::new(file))
159        })
160    }
161}
162
163impl<T, const N: usize> std::fmt::Debug for HashMapper<T, N> where T: Hash {
164    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165        f.debug_struct("HashMapper")
166            .field("BIT_SIZE", &N)
167            .field("len", &self.map.len())
168            .finish()
169    }
170}
171
172
173/// Trait for hash values types
174///
175/// This trait is implemented by types created with [crate::define_hash_type!()].
176pub trait HashDef: Sized {
177    /// Type of hash values (integer type)
178    type Hash: Sized;
179    /// Hashing method
180    const HASHER: fn(&str) -> Self::Hash;
181
182    /// Create a new hash value from an integer
183    fn new(hash: Self::Hash) -> Self;
184
185    /// Convert a string into a hash by hashing it
186    #[inline]
187    fn hashed(s: &str) -> Self {
188        Self::new(Self::HASHER(s))
189    }
190
191    /// Return true if hash is the null hash (0)
192    fn is_null(&self) -> bool;
193}
194
195
196/// Either a hash or its associated string
197///
198/// This enum is intended to be used along with a [HashMapper] for display.
199/// If string is unknown, the hash value is written as `{hex-value}`
200#[derive(Debug)]
201pub enum HashOrStr<H, S>
202where H: Copy, S: AsRef<str> {
203    /// Hash value, string is unknown
204    Hash(H),
205    /// String value matching the hash
206    Str(S),
207}
208
209impl<H, S> fmt::Display for HashOrStr<H, S>
210where H: Copy + fmt::LowerHex, S: AsRef<str> {
211    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
212        match self {
213            Self::Hash(h) => write!(f, "{{{:0w$x}}}", h, w = std::mem::size_of::<H>() * 2),
214            Self::Str(s) => write!(f, "{}", s.as_ref()),
215        }
216    }
217}
218
219
220/// Define a hash type wrapping an integer hash value
221///
222/// The created type provides
223/// - a `hash` field, with the hash numeric value
224/// - [HashDef] implementation
225/// - conversion from a string, using the hasher method (`From<&str>` implementation that calls the hasher method
226/// - implicit conversion from/to hash integer type (`From<T>`)
227/// - [std::fmt::Debug] implementation
228/// - [std::fmt::LowerHex] implementation
229#[macro_export]
230macro_rules! define_hash_type {
231    (
232        $(#[$meta:meta])*
233        $name:ident($T:ty) => $hasher:expr
234    ) => {
235        $(#[$meta])*
236        #[derive(Default, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)]
237        pub struct $name {
238            /// Hash value
239            pub hash: $T,
240        }
241
242        impl $crate::HashDef for $name {
243            type Hash = $T;
244            const HASHER: fn(&str) -> Self::Hash = $hasher;
245
246            #[inline]
247            fn new(hash: Self::Hash) -> Self {
248                Self { hash }
249            }
250
251            #[inline]
252            fn is_null(&self) -> bool {
253                self.hash == 0
254            }
255        }
256
257        impl From<$T> for $name {
258            fn from(v: $T) -> Self {
259                Self { hash: v }
260            }
261        }
262
263        impl std::fmt::Debug for $name {
264            fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
265                write!(f, concat!(stringify!($name), "({:x})"), self)
266            }
267        }
268
269        impl std::fmt::LowerHex for $name {
270            fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
271                write!(f, "{:0w$x}", self.hash, w = std::mem::size_of::<$T>() * 2)
272            }
273        }
274    }
275}
276
277
278/// Each kind of hash handled by CDragon
279///
280/// See also [bin::BinHashKind] for a kind limited to bin hashes.
281#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
282pub enum HashKind {
283    /// Hash for game WAD entries (`.wad.client`)
284    WadGame,
285    /// Hash for launcher WAD entries (`.wad`)
286    WadLcu,
287    /// Hash of an bin entry path
288    BinEntryPath,
289    /// Hash of a bin class name
290    BinClassName,
291    /// Hash of a bin field name
292    BinFieldName,
293    /// Hash of a bin hash value
294    BinHashValue,
295    /// Hash of RST files (translation files)
296    Rst,
297}
298
299impl HashKind {
300    /// Return filename used by CDragon to store the mapping this kind of hash
301    ///
302    /// ```
303    /// use cdragon_hashes::HashKind;
304    /// assert_eq!(HashKind::WadLcu.mapping_path(), "hashes.lcu.txt");
305    /// assert_eq!(HashKind::BinEntryPath.mapping_path(), "hashes.binentries.txt");
306    /// ```
307    pub fn mapping_path(&self) -> &'static str {
308        match self {
309            Self::WadGame => "hashes.game.txt",
310            Self::WadLcu => "hashes.lcu.txt",
311            Self::BinEntryPath => "hashes.binentries.txt",
312            Self::BinClassName => "hashes.bintypes.txt",
313            Self::BinFieldName => "hashes.binfields.txt",
314            Self::BinHashValue => "hashes.binhashes.txt",
315            Self::Rst => "hashes.rst.txt",
316        }
317    }
318
319    /// Return WAD hash kind from a WAD path
320    ///
321    /// The path is assumed to be a "regular" WAD path that follows Riot conventions.
322    /// ```
323    /// use cdragon_hashes::HashKind;
324    /// assert_eq!(HashKind::from_wad_path("Global.wad.client"), Some(HashKind::WadGame));
325    /// assert_eq!(HashKind::from_wad_path("assets.wad"), Some(HashKind::WadLcu));
326    /// assert_eq!(HashKind::from_wad_path("unknown"), None);
327    /// ```
328    pub fn from_wad_path<P: AsRef<Path>>(path: P) -> Option<Self> {
329        let path = path.as_ref().to_str()?;
330        if path.ends_with(".wad.client") {
331            Some(Self::WadGame)
332        } else if path.ends_with(".wad") {
333            Some(Self::WadLcu)
334        } else {
335            None
336        }
337    }
338}
339