wow_mpq/patch/
apply.rs

1//! Patch application logic
2//!
3//! This module implements patch application for both COPY and BSD0 patch types.
4
5use super::header::{PatchFile, PatchType};
6use crate::{Error, Result};
7
8/// Apply a patch file to base data
9///
10/// # Arguments
11///
12/// * `patch` - The parsed patch file containing header and patch data
13/// * `base_data` - The original file data to patch
14///
15/// # Returns
16///
17/// The patched file data
18///
19/// # Errors
20///
21/// Returns error if:
22/// - Base file MD5 doesn't match expected hash
23/// - Patch application fails
24/// - Patched result MD5 doesn't match expected hash
25/// - Unsupported patch type (BSD0 not yet implemented)
26pub fn apply_patch(patch: &PatchFile, base_data: &[u8]) -> Result<Vec<u8>> {
27    // Verify base file MD5 before patching
28    patch.verify_base(base_data)?;
29
30    // Apply patch based on type
31    let patched_data = match patch.header.patch_type {
32        PatchType::Copy => apply_copy_patch(patch, base_data)?,
33        PatchType::Bsd0 => apply_bsd0_patch(patch, base_data)?,
34    };
35
36    // Verify patched result MD5
37    patch.verify_patched(&patched_data)?;
38
39    Ok(patched_data)
40}
41
42/// Apply a COPY patch
43///
44/// COPY patches are simple file replacements - the patch data is the complete
45/// new file content. We ignore the base data entirely.
46///
47/// # Arguments
48///
49/// * `patch` - The parsed COPY patch file
50/// * `base_data` - The original file data (used for size verification only)
51///
52/// # Returns
53///
54/// The complete new file data (from patch.data)
55fn apply_copy_patch(patch: &PatchFile, base_data: &[u8]) -> Result<Vec<u8>> {
56    // Verify base file size matches expected
57    if base_data.len() != patch.header.size_before as usize {
58        return Err(Error::invalid_format(format!(
59            "Base file size mismatch: expected {}, got {}",
60            patch.header.size_before,
61            base_data.len()
62        )));
63    }
64
65    // Verify patch data size matches expected output size
66    if patch.data.len() != patch.header.size_after as usize {
67        return Err(Error::invalid_format(format!(
68            "Patch data size mismatch: expected {}, got {}",
69            patch.header.size_after,
70            patch.data.len()
71        )));
72    }
73
74    log::debug!(
75        "Applying COPY patch: {} -> {} bytes",
76        base_data.len(),
77        patch.data.len()
78    );
79
80    // COPY patch: patch data IS the complete new file
81    Ok(patch.data.clone())
82}
83
84/// Apply a BSD0 (bsdiff40) patch
85///
86/// BSD0 patches use binary diff algorithm to create space-efficient patches.
87/// The patch data is RLE-compressed and must be decompressed before applying.
88///
89/// # Arguments
90///
91/// * `patch` - The parsed BSD0 patch file
92/// * `base_data` - The original file data
93///
94/// # Returns
95///
96/// The patched file data
97fn apply_bsd0_patch(patch: &PatchFile, base_data: &[u8]) -> Result<Vec<u8>> {
98    use byteorder::{LittleEndian, ReadBytesExt};
99    use std::io::Cursor;
100
101    // Verify base file size
102    if base_data.len() != patch.header.size_before as usize {
103        return Err(Error::invalid_format(format!(
104            "Base file size mismatch: expected {}, got {}",
105            patch.header.size_before,
106            base_data.len()
107        )));
108    }
109
110    // BSD0 patch data is RLE-compressed - decompress it first
111    // The RLE data has a 4-byte size header that should be skipped
112    log::debug!(
113        "RLE decompressing BSD0 patch data: {} bytes compressed",
114        patch.data.len()
115    );
116
117    let bsdiff_data = crate::compression::rle::decompress(
118        &patch.data,
119        patch.header.patch_data_size as usize,
120        true, // skip 4-byte size header
121    )?;
122
123    log::debug!(
124        "RLE decompression complete: {} bytes → {} bytes",
125        patch.data.len(),
126        bsdiff_data.len()
127    );
128
129    let mut reader = Cursor::new(&bsdiff_data);
130
131    // Parse Bsdiff40 header (32 bytes)
132    let signature = reader.read_u64::<LittleEndian>()?;
133    if signature != 0x3034464649445342 {
134        // 'BSDIFF40'
135        return Err(Error::invalid_format(format!(
136            "Invalid BSDIFF40 signature: expected 0x3034464649445342, got 0x{signature:016X}"
137        )));
138    }
139
140    let ctrl_block_size = reader.read_u64::<LittleEndian>()? as usize;
141    let data_block_size = reader.read_u64::<LittleEndian>()? as usize;
142    let new_file_size = reader.read_u64::<LittleEndian>()? as usize;
143
144    // Verify new file size matches header
145    if new_file_size != patch.header.size_after as usize {
146        return Err(Error::invalid_format(format!(
147            "BSD0 new file size mismatch: header says {}, bsdiff says {new_file_size}",
148            patch.header.size_after
149        )));
150    }
151
152    log::debug!(
153        "BSD0 patch: {} -> {} bytes (ctrl: {}, data: {})",
154        base_data.len(),
155        new_file_size,
156        ctrl_block_size,
157        data_block_size
158    );
159
160    // Calculate block positions
161    let ctrl_start = 32; // After bsdiff header
162    let data_start = ctrl_start + ctrl_block_size;
163    let extra_start = data_start + data_block_size;
164
165    // Validate block sizes
166    if extra_start > bsdiff_data.len() {
167        return Err(Error::invalid_format(format!(
168            "BSD0 patch data too small: need {extra_start} bytes, have {}",
169            bsdiff_data.len()
170        )));
171    }
172
173    // Extract data blocks
174    let ctrl_block = &bsdiff_data[ctrl_start..data_start];
175    let data_block = &bsdiff_data[data_start..extra_start];
176    let extra_block = &bsdiff_data[extra_start..];
177
178    // Number of control blocks (each is 12 bytes: 3x u32)
179    let num_ctrl_blocks = ctrl_block_size / 12;
180
181    // Allocate output buffer
182    let mut new_data = vec![0u8; new_file_size];
183
184    // Process control blocks
185    let mut new_offset = 0usize;
186    let mut old_offset = 0usize;
187    let mut data_ptr = 0usize;
188    let mut extra_ptr = 0usize;
189
190    for i in 0..num_ctrl_blocks {
191        let ctrl_offset = i * 12;
192        if ctrl_offset + 12 > ctrl_block.len() {
193            return Err(Error::invalid_format(
194                "Control block extends beyond ctrl_block_size".to_string(),
195            ));
196        }
197
198        // Read control block
199        let mut ctrl_reader = Cursor::new(&ctrl_block[ctrl_offset..ctrl_offset + 12]);
200        let add_data_length = ctrl_reader.read_u32::<LittleEndian>()? as usize;
201        let mov_data_length = ctrl_reader.read_u32::<LittleEndian>()? as usize;
202        let old_move_length_raw = ctrl_reader.read_u32::<LittleEndian>()?;
203
204        // Step 1: Copy from data block and combine with old data
205        if new_offset + add_data_length > new_file_size {
206            return Err(Error::invalid_format(format!(
207                "BSD0: add overflow at ctrl {i}: new_offset {new_offset} + add {add_data_length} > size {new_file_size}"
208            )));
209        }
210
211        if data_ptr + add_data_length > data_block.len() {
212            return Err(Error::invalid_format(format!(
213                "BSD0: data block overflow at ctrl {i}: ptr {data_ptr} + len {add_data_length} > size {}",
214                data_block.len()
215            )));
216        }
217
218        // Copy from data block
219        new_data[new_offset..new_offset + add_data_length]
220            .copy_from_slice(&data_block[data_ptr..data_ptr + add_data_length]);
221        data_ptr += add_data_length;
222
223        // Combine with old data (wrapping addition)
224        let combine_size = if old_offset + add_data_length >= base_data.len() {
225            base_data.len().saturating_sub(old_offset)
226        } else {
227            add_data_length
228        };
229
230        for j in 0..combine_size {
231            new_data[new_offset + j] =
232                new_data[new_offset + j].wrapping_add(base_data[old_offset + j]);
233        }
234
235        new_offset += add_data_length;
236        old_offset += add_data_length;
237
238        // Step 2: Copy from extra block
239        if new_offset + mov_data_length > new_file_size {
240            return Err(Error::invalid_format(format!(
241                "BSD0: mov overflow at ctrl {i}: new_offset {new_offset} + mov {mov_data_length} > size {new_file_size}"
242            )));
243        }
244
245        if extra_ptr + mov_data_length > extra_block.len() {
246            return Err(Error::invalid_format(format!(
247                "BSD0: extra block overflow at ctrl {i}: ptr {extra_ptr} + len {mov_data_length} > size {}",
248                extra_block.len()
249            )));
250        }
251
252        new_data[new_offset..new_offset + mov_data_length]
253            .copy_from_slice(&extra_block[extra_ptr..extra_ptr + mov_data_length]);
254        extra_ptr += mov_data_length;
255        new_offset += mov_data_length;
256
257        // Step 3: Adjust old offset (signed!)
258        let old_move_length = if old_move_length_raw & 0x80000000 != 0 {
259            // Negative offset
260            let neg_val = 0x80000000u32.wrapping_sub(old_move_length_raw);
261            old_offset = old_offset.saturating_sub(neg_val as usize);
262            0
263        } else {
264            old_move_length_raw as usize
265        };
266
267        old_offset += old_move_length;
268    }
269
270    // Verify final offset matches expected size
271    if new_offset != new_file_size {
272        return Err(Error::invalid_format(format!(
273            "BSD0: final offset mismatch: got {new_offset}, expected {new_file_size}"
274        )));
275    }
276
277    log::debug!("BSD0 patch applied successfully: {} bytes", new_data.len());
278
279    Ok(new_data)
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285
286    /// Helper to create a test COPY patch
287    fn create_copy_patch(base_size: u32, new_data: Vec<u8>) -> PatchFile {
288        use md5::{Digest, Md5};
289
290        // Calculate MD5 of dummy base data
291        let base_data = vec![0u8; base_size as usize];
292        let mut hasher = Md5::new();
293        hasher.update(&base_data);
294        let md5_before: [u8; 16] = hasher.finalize().into();
295
296        // Calculate MD5 of new data
297        let mut hasher = Md5::new();
298        hasher.update(&new_data);
299        let md5_after: [u8; 16] = hasher.finalize().into();
300
301        // Build PTCH file
302        let mut data = Vec::new();
303
304        // PTCH Header
305        data.extend_from_slice(&0x48435450u32.to_le_bytes());
306        data.extend_from_slice(&(new_data.len() as u32).to_le_bytes());
307        data.extend_from_slice(&base_size.to_le_bytes());
308        data.extend_from_slice(&(new_data.len() as u32).to_le_bytes());
309
310        // MD5 Block
311        data.extend_from_slice(&0x5f35444du32.to_le_bytes());
312        data.extend_from_slice(&40u32.to_le_bytes());
313        data.extend_from_slice(&md5_before);
314        data.extend_from_slice(&md5_after);
315
316        // XFRM Block
317        data.extend_from_slice(&0x4d524658u32.to_le_bytes());
318        data.extend_from_slice(&(12 + new_data.len() as u32).to_le_bytes());
319        data.extend_from_slice(&0x59504f43u32.to_le_bytes()); // COPY
320
321        // Patch data
322        data.extend_from_slice(&new_data);
323
324        PatchFile::parse(&data).expect("Failed to create test patch")
325    }
326
327    #[test]
328    fn test_apply_copy_patch_simple() {
329        let base_data = vec![0u8; 100];
330        let new_data = b"Hello, Warcraft!".to_vec();
331
332        let patch = create_copy_patch(100, new_data.clone());
333        let result = apply_patch(&patch, &base_data).expect("Failed to apply patch");
334
335        assert_eq!(result, new_data);
336    }
337
338    #[test]
339    fn test_copy_patch_size_increase() {
340        let base_data = vec![0u8; 50];
341        let new_data = vec![0x42u8; 200]; // Larger file
342
343        let patch = create_copy_patch(50, new_data.clone());
344        let result = apply_patch(&patch, &base_data).expect("Failed to apply patch");
345
346        assert_eq!(result.len(), 200);
347        assert_eq!(result, new_data);
348    }
349
350    #[test]
351    fn test_copy_patch_size_decrease() {
352        let base_data = vec![0u8; 200];
353        let new_data = vec![0x42u8; 50]; // Smaller file
354
355        let patch = create_copy_patch(200, new_data.clone());
356        let result = apply_patch(&patch, &base_data).expect("Failed to apply patch");
357
358        assert_eq!(result.len(), 50);
359        assert_eq!(result, new_data);
360    }
361
362    #[test]
363    fn test_copy_patch_wrong_base_size() {
364        let base_data = vec![0u8; 100]; // Wrong size
365        let new_data = b"Test".to_vec();
366
367        let patch = create_copy_patch(50, new_data); // Expects 50 bytes
368        let result = apply_patch(&patch, &base_data);
369
370        assert!(result.is_err());
371    }
372
373    #[test]
374    fn test_copy_patch_wrong_base_md5() {
375        let base_data = vec![0x42u8; 100]; // Different content
376        let new_data = b"Test".to_vec();
377
378        let patch = create_copy_patch(100, new_data); // MD5 calculated for zeros
379        let result = apply_patch(&patch, &base_data);
380
381        assert!(result.is_err());
382    }
383
384    /// Simple RLE compress for test data
385    /// RLE format: if byte has high bit set (0x80), copy (byte & 0x7F) + 1 literal bytes
386    fn rle_compress_test(data: &[u8]) -> Vec<u8> {
387        let mut compressed = Vec::new();
388
389        // Add 4-byte size header (decompressed size)
390        compressed.extend_from_slice(&(data.len() as u32).to_le_bytes());
391
392        // Encode data in chunks of up to 128 bytes (0x7F + 1)
393        for chunk in data.chunks(128) {
394            let count = chunk.len() as u8;
395            compressed.push(0x80 | (count - 1)); // High bit set + (count-1)
396            compressed.extend_from_slice(chunk);
397        }
398
399        compressed
400    }
401
402    /// Helper to create a test BSD0 patch
403    fn create_bsd0_patch(base_data: &[u8], new_data: &[u8], patch_data: Vec<u8>) -> PatchFile {
404        use md5::{Digest, Md5};
405
406        // Calculate MD5 hashes
407        let mut hasher = Md5::new();
408        hasher.update(base_data);
409        let md5_before: [u8; 16] = hasher.finalize().into();
410
411        let mut hasher = Md5::new();
412        hasher.update(new_data);
413        let md5_after: [u8; 16] = hasher.finalize().into();
414
415        // RLE compress the patch data (BSD0 patches use RLE compression)
416        let compressed_patch_data = rle_compress_test(&patch_data);
417
418        // Build PTCH file with BSD0 type
419        let mut data = Vec::new();
420
421        // PTCH Header
422        data.extend_from_slice(&0x48435450u32.to_le_bytes());
423        data.extend_from_slice(&(patch_data.len() as u32).to_le_bytes()); // Decompressed size
424        data.extend_from_slice(&(base_data.len() as u32).to_le_bytes());
425        data.extend_from_slice(&(new_data.len() as u32).to_le_bytes());
426
427        // MD5 Block
428        data.extend_from_slice(&0x5f35444du32.to_le_bytes());
429        data.extend_from_slice(&40u32.to_le_bytes());
430        data.extend_from_slice(&md5_before);
431        data.extend_from_slice(&md5_after);
432
433        // XFRM Block
434        data.extend_from_slice(&0x4d524658u32.to_le_bytes());
435        data.extend_from_slice(&(12u32 + compressed_patch_data.len() as u32).to_le_bytes());
436        data.extend_from_slice(&0x30445342u32.to_le_bytes()); // BSD0
437
438        // Compressed patch data
439        data.extend_from_slice(&compressed_patch_data);
440
441        PatchFile::parse(&data).expect("Failed to create test BSD0 patch")
442    }
443
444    #[test]
445    fn test_apply_bsd0_simple() {
446        // Simple example: modify 3 bytes
447        // Old: [0x10, 0x20, 0x30]
448        // New: [0x15, 0x27, 0xFF]
449        let old_data = vec![0x10u8, 0x20, 0x30];
450        let new_data = vec![0x15u8, 0x27, 0xFF];
451
452        // Build BSD0 patch manually
453        let mut patch_data = Vec::new();
454
455        // Bsdiff40 header
456        patch_data.extend_from_slice(&0x3034464649445342u64.to_le_bytes()); // 'BSDIFF40'
457        patch_data.extend_from_slice(&12u64.to_le_bytes()); // ctrl_block_size (1 block)
458        patch_data.extend_from_slice(&2u64.to_le_bytes()); // data_block_size
459        patch_data.extend_from_slice(&3u64.to_le_bytes()); // new_file_size
460
461        // Control block: add=2, mov=1, old_move=1
462        patch_data.extend_from_slice(&2u32.to_le_bytes()); // add_data_length
463        patch_data.extend_from_slice(&1u32.to_le_bytes()); // mov_data_length
464        patch_data.extend_from_slice(&1u32.to_le_bytes()); // old_move_length
465
466        // Data block: [0x05, 0x07]
467        // These will be added to old data: 0x05+0x10=0x15, 0x07+0x20=0x27
468        patch_data.push(0x05);
469        patch_data.push(0x07);
470
471        // Extra block: [0xFF]
472        patch_data.push(0xFF);
473
474        let patch = create_bsd0_patch(&old_data, &new_data, patch_data);
475        let result = apply_patch(&patch, &old_data).expect("Failed to apply BSD0 patch");
476
477        assert_eq!(result, new_data);
478    }
479
480    #[test]
481    fn test_bsd0_wrapping_addition() {
482        // Test that addition wraps at 256
483        // Old: [0xFF]
484        // Diff: [0x02]
485        // New: [0x01] (0xFF + 0x02 = 0x101 = 0x01 mod 256)
486        let old_data = vec![0xFFu8];
487        let new_data = vec![0x01u8];
488
489        let mut patch_data = Vec::new();
490        patch_data.extend_from_slice(&0x3034464649445342u64.to_le_bytes());
491        patch_data.extend_from_slice(&12u64.to_le_bytes());
492        patch_data.extend_from_slice(&1u64.to_le_bytes());
493        patch_data.extend_from_slice(&1u64.to_le_bytes());
494
495        // Control block
496        patch_data.extend_from_slice(&1u32.to_le_bytes()); // add=1
497        patch_data.extend_from_slice(&0u32.to_le_bytes()); // mov=0
498        patch_data.extend_from_slice(&0u32.to_le_bytes()); // old_move=0
499
500        // Data block
501        patch_data.push(0x02); // Will wrap: 0xFF + 0x02 = 0x01
502
503        let patch = create_bsd0_patch(&old_data, &new_data, patch_data);
504        let result = apply_patch(&patch, &old_data).expect("Failed to apply BSD0 patch");
505
506        assert_eq!(result, new_data);
507        assert_eq!(result[0], 0x01);
508    }
509
510    #[test]
511    fn test_bsd0_multiple_control_blocks() {
512        // Test with 2 control blocks
513        // Old: [0x10, 0x20, 0x30, 0x40]
514        // New: [0x15, 0x27, 0xFF, 0xAA]
515        let old_data = vec![0x10u8, 0x20, 0x30, 0x40];
516        let new_data = vec![0x15u8, 0x27, 0xFF, 0xAA];
517
518        let mut patch_data = Vec::new();
519        patch_data.extend_from_slice(&0x3034464649445342u64.to_le_bytes());
520        patch_data.extend_from_slice(&24u64.to_le_bytes()); // 2 control blocks
521        patch_data.extend_from_slice(&2u64.to_le_bytes()); // data size
522        patch_data.extend_from_slice(&4u64.to_le_bytes()); // new size
523
524        // Control block 1
525        patch_data.extend_from_slice(&2u32.to_le_bytes()); // add=2
526        patch_data.extend_from_slice(&1u32.to_le_bytes()); // mov=1
527        patch_data.extend_from_slice(&1u32.to_le_bytes()); // old_move=1
528
529        // Control block 2
530        patch_data.extend_from_slice(&0u32.to_le_bytes()); // add=0
531        patch_data.extend_from_slice(&1u32.to_le_bytes()); // mov=1
532        patch_data.extend_from_slice(&0u32.to_le_bytes()); // old_move=0
533
534        // Data block
535        patch_data.push(0x05); // 0x10 + 0x05 = 0x15
536        patch_data.push(0x07); // 0x20 + 0x07 = 0x27
537
538        // Extra block
539        patch_data.push(0xFF);
540        patch_data.push(0xAA);
541
542        let patch = create_bsd0_patch(&old_data, &new_data, patch_data);
543        let result = apply_patch(&patch, &old_data).expect("Failed to apply BSD0 patch");
544
545        assert_eq!(result, new_data);
546    }
547
548    #[test]
549    fn test_bsd0_invalid_signature() {
550        let old_data = vec![0x10u8];
551        let new_data = vec![0x11u8];
552
553        let mut patch_data = Vec::new();
554        patch_data.extend_from_slice(&0xDEADBEEFDEADBEEFu64.to_le_bytes()); // Bad signature
555        patch_data.extend_from_slice(&12u64.to_le_bytes());
556        patch_data.extend_from_slice(&1u64.to_le_bytes());
557        patch_data.extend_from_slice(&1u64.to_le_bytes());
558        patch_data.extend_from_slice(&1u32.to_le_bytes());
559        patch_data.extend_from_slice(&0u32.to_le_bytes());
560        patch_data.extend_from_slice(&0u32.to_le_bytes());
561        patch_data.push(0x01);
562
563        let patch = create_bsd0_patch(&old_data, &new_data, patch_data);
564        let result = apply_patch(&patch, &old_data);
565
566        assert!(result.is_err());
567    }
568
569    #[test]
570    fn test_bsd0_size_mismatch() {
571        // Wrong base size
572        let old_data = vec![0x10u8, 0x20]; // 2 bytes
573        let new_data = vec![0x11u8];
574
575        let mut patch_data = Vec::new();
576        patch_data.extend_from_slice(&0x3034464649445342u64.to_le_bytes());
577        patch_data.extend_from_slice(&12u64.to_le_bytes());
578        patch_data.extend_from_slice(&1u64.to_le_bytes());
579        patch_data.extend_from_slice(&1u64.to_le_bytes()); // Says 1 byte result
580        patch_data.extend_from_slice(&1u32.to_le_bytes());
581        patch_data.extend_from_slice(&0u32.to_le_bytes());
582        patch_data.extend_from_slice(&0u32.to_le_bytes());
583        patch_data.push(0x01);
584
585        // Patch expects 1-byte base, but we give 2 bytes
586        let patch = create_bsd0_patch(&[0x10], &new_data, patch_data);
587        let result = apply_patch(&patch, &old_data);
588
589        assert!(result.is_err());
590    }
591}