assembly_pack/md5/
mod.rs

1//! # `md5` hashsum of files
2
3use std::{fmt, io::Write, str::FromStr};
4
5use serde::{ser::SerializeTuple, Deserialize, Serialize};
6
7mod fs;
8#[cfg(feature = "md5sum")]
9pub mod io;
10pub mod padded;
11
12#[cfg(feature = "md5sum")]
13pub use fs::md5sum;
14
15/// # MD5 hashsum of a file
16///
17/// The game uses md5 hashes of the content to identify a specific version of a file.
18///
19/// *Note*: Currently, this doesn't actually provide an implementation of that hash. Please use a tool
20/// like `md5sum` for that.
21#[allow(clippy::upper_case_acronyms)]
22#[derive(PartialEq, Eq, Copy, Clone)]
23pub struct MD5Sum(pub [u8; 16]);
24
25impl MD5Sum {
26    /// Compute an MD5 Sum
27    #[cfg(feature = "md5sum")]
28    pub fn compute<B: AsRef<[u8]> + ?Sized>(data: &B) -> Self {
29        Self(md5::compute(data).0)
30    }
31
32    /// Read an MD5 sum from bytes
33    pub fn from_hex_bytes(bytes: &[u8]) -> Result<Self, Error> {
34        if bytes.len() != 32 {
35            return Err(Error::InvalidLength(bytes.len()));
36        }
37
38        let mut arr = [0u8; 16];
39        let mut k = 4;
40
41        for (bi, b) in bytes.iter().copied().enumerate() {
42            let v = match b {
43                b'0'..=b'9' => b - b'0',
44                b'a'..=b'f' => b - b'a' + 10,
45                b'A'..=b'F' => b - b'A' + 10,
46                _ => return Err(Error::InvalidByte { index: bi as u8, b }),
47            };
48            arr[bi >> 1] += v << k;
49            k = 4 - k;
50        }
51
52        Ok(Self(arr))
53    }
54}
55
56impl fmt::Debug for MD5Sum {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        for i in 0..16 {
59            write!(f, "{:02x}", self.0[i])?;
60        }
61        Ok(())
62    }
63}
64
65impl fmt::Display for MD5Sum {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        <Self as fmt::Debug>::fmt(self, f)
68    }
69}
70
71#[derive(Debug)]
72/// Failure to parse an MD5 hash
73pub enum Error {
74    /// Not 32 hex bytes
75    InvalidLength(usize),
76    /// Byte is not in `0-9a-fA-F`
77    InvalidByte {
78        /// Index of the failing byte
79        index: u8,
80        /// value of the failing byte
81        b: u8,
82    },
83}
84
85impl std::error::Error for Error {}
86impl fmt::Display for Error {
87    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88        match self {
89            Self::InvalidLength(v) => write!(f, "Invalid byte count for md5sum: {}", v),
90            Self::InvalidByte { index, b } => {
91                write!(f, "Invalid byte {} at {} for md5sum", b, index)
92            }
93        }
94    }
95}
96
97impl FromStr for MD5Sum {
98    type Err = Error;
99
100    fn from_str(input: &str) -> Result<Self, Self::Err> {
101        Self::from_hex_bytes(input.as_bytes())
102    }
103}
104
105struct MD5Visitor;
106
107impl<'de> serde::de::Visitor<'de> for MD5Visitor {
108    type Value = MD5Sum;
109
110    fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
111        write!(f, "md5sum hex string")
112    }
113
114    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
115    where
116        E: serde::de::Error,
117    {
118        MD5Sum::from_str(v).map_err(|e| E::custom(e.to_string()))
119    }
120}
121
122impl Serialize for MD5Sum {
123    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
124    where
125        S: serde::Serializer,
126    {
127        let mut bytes = [0u8; 32];
128        write!(bytes.as_mut(), "{:?}", self).unwrap();
129        if serializer.is_human_readable() {
130            let str = unsafe { std::str::from_utf8_unchecked(&bytes[..]) };
131            serializer.serialize_str(str)
132        } else {
133            let mut m = serializer.serialize_tuple(32)?;
134            for b in bytes {
135                m.serialize_element(&b)?;
136            }
137            m.end()
138        }
139    }
140}
141
142impl<'de> Deserialize<'de> for MD5Sum {
143    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
144    where
145        D: serde::Deserializer<'de>,
146    {
147        if deserializer.is_human_readable() {
148            deserializer.deserialize_str(MD5Visitor)
149        } else {
150            let bytes = <[u8; 32]>::deserialize(deserializer)?;
151            match MD5Sum::from_hex_bytes(&bytes) {
152                Ok(sum) => Ok(sum),
153                Err(e) => Err(<D::Error as serde::de::Error>::custom(e.to_string())),
154            }
155        }
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::MD5Sum;
162
163    const ARR: [u8; 16] = [
164        0x33, 0x7e, 0x24, 0xd7, 0x26, 0xfd, 0x72, 0x8f, //
165        0x92, 0x95, 0x7a, 0x2c, 0x90, 0x8d, 0xde, 0xe6,
166    ];
167    const STR: &str = "337e24d726fd728f92957a2c908ddee6";
168
169    #[test]
170    fn test() {
171        let s = MD5Sum::from_hex_bytes(STR.as_bytes()).unwrap();
172        assert_eq!(s.0, ARR);
173        let f = format!("{:?}", s);
174        assert_eq!(f, STR);
175    }
176
177    #[test]
178    fn test_bincode() {
179        let s: MD5Sum = bincode::deserialize(STR.as_bytes()).unwrap();
180        assert_eq!(s.0, ARR);
181        let bytes = bincode::serialize(&s).unwrap();
182        assert_eq!(&bytes, STR.as_bytes());
183    }
184
185    #[test]
186    fn test_json() {
187        let input = format!("\"{}\"", STR);
188        let s: MD5Sum = serde_json::from_str(&input).unwrap();
189        assert_eq!(s.0, ARR);
190        let output = serde_json::to_string(&s).unwrap();
191        assert_eq!(output, input);
192    }
193}