wow_mpq/patch/
header.rs

1//! PTCH format header parsing
2
3use crate::{Error, Result};
4use byteorder::{LittleEndian, ReadBytesExt};
5use std::io::Read;
6
7/// Magic signature for PTCH header
8const PTCH_SIGNATURE: u32 = 0x48435450; // 'PTCH'
9
10/// Magic signature for MD5 block
11const MD5_SIGNATURE: u32 = 0x5f35444d; // 'MD5_'
12
13/// Magic signature for XFRM block
14const XFRM_SIGNATURE: u32 = 0x4d524658; // 'XFRM'
15
16/// Patch file type
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum PatchType {
19    /// Simple file replacement - patch data is the complete new file
20    Copy,
21    /// Binary diff using bsdiff40 algorithm
22    Bsd0,
23}
24
25impl PatchType {
26    /// Parse patch type from u32 magic value
27    fn from_magic(magic: u32) -> Result<Self> {
28        match magic {
29            0x59504f43 => Ok(PatchType::Copy), // 'COPY'
30            0x30445342 => Ok(PatchType::Bsd0), // 'BSD0'
31            _ => Err(Error::invalid_format(format!(
32                "Unknown patch type: 0x{magic:08X}"
33            ))),
34        }
35    }
36
37    /// Get magic value for this patch type
38    pub fn to_magic(self) -> u32 {
39        match self {
40            PatchType::Copy => 0x59504f43,
41            PatchType::Bsd0 => 0x30445342,
42        }
43    }
44}
45
46/// PTCH file header containing all metadata
47#[derive(Debug, Clone)]
48pub struct PatchHeader {
49    /// Total size of patch data (decompressed)
50    pub patch_data_size: u32,
51    /// Size of original file before patching
52    pub size_before: u32,
53    /// Size of file after patching
54    pub size_after: u32,
55    /// MD5 hash of original file (before patch)
56    pub md5_before: [u8; 16],
57    /// MD5 hash of patched file (after patch)
58    pub md5_after: [u8; 16],
59    /// Type of patch (COPY or BSD0)
60    pub patch_type: PatchType,
61    /// Size of XFRM block data (excludes 12-byte XFRM header)
62    pub xfrm_data_size: u32,
63}
64
65impl PatchHeader {
66    /// Parse PTCH header from reader
67    ///
68    /// Reads and validates:
69    /// - PTCH header (12 bytes)
70    /// - MD5 block (40 bytes)
71    /// - XFRM header (12 bytes)
72    ///
73    /// Returns the parsed header. Caller must read remaining patch data.
74    pub fn parse<R: Read>(reader: &mut R) -> Result<Self> {
75        // --- PTCH Header (12 bytes) ---
76        let ptch_sig = reader.read_u32::<LittleEndian>()?;
77        if ptch_sig != PTCH_SIGNATURE {
78            return Err(Error::invalid_format(format!(
79                "Invalid PTCH signature: expected 0x{PTCH_SIGNATURE:08X}, got 0x{ptch_sig:08X}"
80            )));
81        }
82
83        let patch_data_size = reader.read_u32::<LittleEndian>()?;
84        let size_before = reader.read_u32::<LittleEndian>()?;
85        let size_after = reader.read_u32::<LittleEndian>()?;
86
87        log::debug!(
88            "PTCH header: patch_size={patch_data_size}, before={size_before}, after={size_after}"
89        );
90
91        // --- MD5 Block (40 bytes) ---
92        let md5_sig = reader.read_u32::<LittleEndian>()?;
93        if md5_sig != MD5_SIGNATURE {
94            return Err(Error::invalid_format(format!(
95                "Invalid MD5 signature: expected 0x{MD5_SIGNATURE:08X}, got 0x{md5_sig:08X}"
96            )));
97        }
98
99        let md5_block_size = reader.read_u32::<LittleEndian>()?;
100        if md5_block_size != 40 {
101            return Err(Error::invalid_format(format!(
102                "Invalid MD5 block size: expected 40, got {md5_block_size}"
103            )));
104        }
105
106        let mut md5_before = [0u8; 16];
107        reader.read_exact(&mut md5_before)?;
108
109        let mut md5_after = [0u8; 16];
110        reader.read_exact(&mut md5_after)?;
111
112        log::debug!(
113            "MD5 before: {}, after: {}",
114            hex::encode(md5_before),
115            hex::encode(md5_after)
116        );
117
118        // --- XFRM Block Header (12 bytes) ---
119        let xfrm_sig = reader.read_u32::<LittleEndian>()?;
120        if xfrm_sig != XFRM_SIGNATURE {
121            return Err(Error::invalid_format(format!(
122                "Invalid XFRM signature: expected 0x{XFRM_SIGNATURE:08X}, got 0x{xfrm_sig:08X}"
123            )));
124        }
125
126        let xfrm_block_size = reader.read_u32::<LittleEndian>()?;
127        let patch_type_magic = reader.read_u32::<LittleEndian>()?;
128
129        let patch_type = PatchType::from_magic(patch_type_magic)?;
130
131        // XFRM block size includes the 12-byte header
132        let xfrm_data_size = xfrm_block_size.saturating_sub(12);
133
134        log::debug!(
135            "XFRM header: type={:?}, block_size={xfrm_block_size}, data_size={xfrm_data_size}",
136            patch_type
137        );
138
139        Ok(PatchHeader {
140            patch_data_size,
141            size_before,
142            size_after,
143            md5_before,
144            md5_after,
145            patch_type,
146            xfrm_data_size,
147        })
148    }
149
150    /// Total header size in bytes (PTCH + MD5 + XFRM headers)
151    pub const HEADER_SIZE: usize = 12 + 40 + 12; // 64 bytes
152}
153
154/// Complete patch file with header and data
155#[derive(Debug, Clone)]
156pub struct PatchFile {
157    /// Parsed header
158    pub header: PatchHeader,
159    /// Raw patch data (after XFRM header, decompressed if needed)
160    pub data: Vec<u8>,
161}
162
163impl PatchFile {
164    /// Parse patch file from complete data
165    pub fn parse(data: &[u8]) -> Result<Self> {
166        if data.len() < PatchHeader::HEADER_SIZE {
167            return Err(Error::invalid_format(format!(
168                "Patch file too small: {} bytes, need at least {}",
169                data.len(),
170                PatchHeader::HEADER_SIZE
171            )));
172        }
173
174        let mut reader = std::io::Cursor::new(data);
175        let header = PatchHeader::parse(&mut reader)?;
176
177        // Read remaining data as patch payload
178        let mut patch_data = Vec::new();
179        reader.read_to_end(&mut patch_data)?;
180
181        log::debug!(
182            "Parsed patch file: type={:?}, data_size={}",
183            header.patch_type,
184            patch_data.len()
185        );
186
187        Ok(PatchFile {
188            header,
189            data: patch_data,
190        })
191    }
192
193    /// Verify if base file MD5 matches expected
194    pub fn verify_base(&self, base_data: &[u8]) -> Result<()> {
195        use md5::{Digest, Md5};
196
197        let mut hasher = Md5::new();
198        hasher.update(base_data);
199        let result = hasher.finalize();
200
201        if result.as_slice() != self.header.md5_before {
202            return Err(Error::invalid_format(format!(
203                "Base file MD5 mismatch: expected {}, got {}",
204                hex::encode(self.header.md5_before),
205                hex::encode(result)
206            )));
207        }
208
209        Ok(())
210    }
211
212    /// Verify if patched result MD5 matches expected
213    pub fn verify_patched(&self, patched_data: &[u8]) -> Result<()> {
214        use md5::{Digest, Md5};
215
216        let mut hasher = Md5::new();
217        hasher.update(patched_data);
218        let result = hasher.finalize();
219
220        if result.as_slice() != self.header.md5_after {
221            return Err(Error::invalid_format(format!(
222                "Patched file MD5 mismatch: expected {}, got {}",
223                hex::encode(self.header.md5_after),
224                hex::encode(result)
225            )));
226        }
227
228        Ok(())
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    #[test]
237    fn test_patch_type_magic() {
238        assert_eq!(PatchType::Copy.to_magic(), 0x59504f43);
239        assert_eq!(PatchType::Bsd0.to_magic(), 0x30445342);
240
241        assert_eq!(PatchType::from_magic(0x59504f43).unwrap(), PatchType::Copy);
242        assert_eq!(PatchType::from_magic(0x30445342).unwrap(), PatchType::Bsd0);
243
244        assert!(PatchType::from_magic(0xDEADBEEF).is_err());
245    }
246
247    #[test]
248    fn test_header_size() {
249        assert_eq!(PatchHeader::HEADER_SIZE, 64);
250    }
251}