Skip to main content

wow_mpq/special_files/
attributes.rs

1//! Support for the MPQ (attributes) special file.
2//!
3//! The (attributes) file stores extended metadata about files in the archive,
4//! including CRC32 checksums, MD5 hashes, file timestamps, and patch information.
5
6use crate::error::{Error, Result};
7use byteorder::{LittleEndian, ReadBytesExt};
8use bytes::Bytes;
9use std::io::{Cursor, Read};
10
11/// Flags indicating which attributes are present in the file
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct AttributeFlags(u32);
14
15impl AttributeFlags {
16    /// CRC32 checksums are present
17    pub const CRC32: u32 = 0x00000001;
18    /// File timestamps are present
19    pub const FILETIME: u32 = 0x00000002;
20    /// MD5 hashes are present
21    pub const MD5: u32 = 0x00000004;
22    /// Patch bit indicators are present
23    pub const PATCH_BIT: u32 = 0x00000008;
24    /// All attributes are present
25    pub const ALL: u32 = 0x0000000F;
26
27    /// Create new attribute flags
28    pub fn new(value: u32) -> Self {
29        Self(value)
30    }
31
32    /// Check if CRC32 checksums are present
33    pub fn has_crc32(&self) -> bool {
34        self.0 & Self::CRC32 != 0
35    }
36
37    /// Check if file timestamps are present
38    pub fn has_filetime(&self) -> bool {
39        self.0 & Self::FILETIME != 0
40    }
41
42    /// Check if MD5 hashes are present
43    pub fn has_md5(&self) -> bool {
44        self.0 & Self::MD5 != 0
45    }
46
47    /// Check if patch bits are present
48    pub fn has_patch_bit(&self) -> bool {
49        self.0 & Self::PATCH_BIT != 0
50    }
51
52    /// Get the raw flags value
53    pub fn as_u32(&self) -> u32 {
54        self.0
55    }
56}
57
58/// File attributes for a single file in the archive
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct FileAttributes {
61    /// CRC32 checksum of the uncompressed file data
62    pub crc32: Option<u32>,
63    /// Windows FILETIME timestamp (100-nanosecond intervals since 1601-01-01)
64    pub filetime: Option<u64>,
65    /// MD5 hash of the uncompressed file data
66    pub md5: Option<[u8; 16]>,
67    /// Whether this file is a patch file
68    pub is_patch: Option<bool>,
69}
70
71impl FileAttributes {
72    /// Create empty attributes
73    pub fn new() -> Self {
74        Self {
75            crc32: None,
76            filetime: None,
77            md5: None,
78            is_patch: None,
79        }
80    }
81}
82
83impl Default for FileAttributes {
84    fn default() -> Self {
85        Self::new()
86    }
87}
88
89/// Parsed (attributes) file data
90#[derive(Debug, Clone)]
91pub struct Attributes {
92    /// Version of the attributes file (should be 100)
93    pub version: u32,
94    /// Flags indicating which attributes are present
95    pub flags: AttributeFlags,
96    /// Attributes for each file in the block table
97    pub file_attributes: Vec<FileAttributes>,
98    /// CRC32 checksum (Phase 1 stub)
99    pub crc32: Option<u32>,
100    /// MD5 checksum (Phase 1 stub)  
101    pub md5: Option<[u8; 16]>,
102    /// File time (Phase 1 stub)
103    pub filetime: Option<u64>,
104}
105
106impl Attributes {
107    /// Expected version for the attributes file
108    pub const EXPECTED_VERSION: u32 = 100;
109
110    /// Parse attributes from raw data
111    pub fn parse(data: &Bytes, block_count: usize) -> Result<Self> {
112        if data.len() < 8 {
113            return Err(Error::invalid_format(
114                "Attributes file too small for header",
115            ));
116        }
117
118        let mut cursor = Cursor::new(data);
119
120        // Read header
121        let version = cursor.read_u32::<LittleEndian>().map_err(Error::Io)?;
122        if version != Self::EXPECTED_VERSION {
123            return Err(Error::invalid_format(format!(
124                "Unsupported attributes version: {} (expected {})",
125                version,
126                Self::EXPECTED_VERSION
127            )));
128        }
129
130        let flags = AttributeFlags::new(cursor.read_u32::<LittleEndian>().map_err(Error::Io)?);
131
132        // Calculate expected size
133        let mut expected_size = 8; // header
134        if flags.has_crc32() {
135            expected_size += block_count * 4;
136        }
137        if flags.has_filetime() {
138            expected_size += block_count * 8;
139        }
140        if flags.has_md5() {
141            expected_size += block_count * 16;
142        }
143        if flags.has_patch_bit() {
144            expected_size += block_count.div_ceil(8);
145        }
146
147        // Be more lenient with size validation to handle real-world MPQ variations
148        // Some MPQ files may have slightly different patch bit calculations
149        let min_required_size = 8 + // header
150            if flags.has_crc32() { block_count * 4 } else { 0 } +
151            if flags.has_filetime() { block_count * 8 } else { 0 } +
152            if flags.has_md5() { block_count * 16 } else { 0 } +
153            if flags.has_patch_bit() {
154                // Allow for off-by-one variations in patch bit calculations
155                let ideal_patch_bytes = block_count.div_ceil(8);
156                if ideal_patch_bytes > 0 { ideal_patch_bytes - 1 } else { 0 }
157            } else { 0 };
158
159        if data.len() < min_required_size {
160            return Err(Error::invalid_format(format!(
161                "Attributes file too small: {} bytes (expected at least {}, ideally {})",
162                data.len(),
163                min_required_size,
164                expected_size
165            )));
166        }
167
168        // Log when we encounter size discrepancies for debugging
169        if data.len() != expected_size {
170            log::warn!(
171                "Attributes file size mismatch: actual={}, expected={}, difference={} (tolerating for compatibility)",
172                data.len(),
173                expected_size,
174                expected_size as i32 - data.len() as i32
175            );
176        }
177
178        // Parse attributes for each file
179        let mut file_attributes = Vec::with_capacity(block_count);
180
181        // Parse CRC32 array if present
182        let crc32_values = if flags.has_crc32() {
183            let mut values = Vec::with_capacity(block_count);
184            for _ in 0..block_count {
185                values.push(cursor.read_u32::<LittleEndian>().map_err(Error::Io)?);
186            }
187            Some(values)
188        } else {
189            None
190        };
191
192        // Parse timestamp array if present
193        let filetime_values = if flags.has_filetime() {
194            let mut values = Vec::with_capacity(block_count);
195            for _ in 0..block_count {
196                values.push(cursor.read_u64::<LittleEndian>().map_err(Error::Io)?);
197            }
198            Some(values)
199        } else {
200            None
201        };
202
203        // Parse MD5 array if present
204        let md5_values = if flags.has_md5() {
205            let mut values = Vec::with_capacity(block_count);
206            for _ in 0..block_count {
207                let mut hash = [0u8; 16];
208                cursor.read_exact(&mut hash).map_err(Error::Io)?;
209                values.push(hash);
210            }
211            Some(values)
212        } else {
213            None
214        };
215
216        // Parse patch bits if present
217        let patch_bits = if flags.has_patch_bit() {
218            let ideal_byte_count = block_count.div_ceil(8);
219            // Calculate how many bytes are actually available for patch bits
220            let position = cursor.position() as usize;
221            let available_bytes = if data.len() > position {
222                data.len() - position
223            } else {
224                0
225            };
226            let actual_byte_count = available_bytes.min(ideal_byte_count);
227
228            log::debug!(
229                "Patch bits: ideal={ideal_byte_count} bytes, available={available_bytes} bytes, reading={actual_byte_count} bytes"
230            );
231
232            let mut bits = vec![0u8; ideal_byte_count]; // Always allocate the ideal size
233            if actual_byte_count > 0 {
234                let mut actual_bits = vec![0u8; actual_byte_count];
235                cursor.read_exact(&mut actual_bits).map_err(Error::Io)?;
236                bits[..actual_byte_count].copy_from_slice(&actual_bits);
237                // Remaining bytes in bits stay as 0, which is safe for patch bit interpretation
238            }
239            Some(bits)
240        } else {
241            None
242        };
243
244        // Combine into FileAttributes structs
245        for i in 0..block_count {
246            let mut attrs = FileAttributes::new();
247
248            if let Some(ref values) = crc32_values {
249                attrs.crc32 = Some(values[i]);
250            }
251
252            if let Some(ref values) = filetime_values {
253                attrs.filetime = Some(values[i]);
254            }
255
256            if let Some(ref values) = md5_values {
257                attrs.md5 = Some(values[i]);
258            }
259
260            if let Some(ref bits) = patch_bits {
261                let byte_index = i / 8;
262                let bit_index = i % 8;
263                attrs.is_patch = Some((bits[byte_index] & (1 << bit_index)) != 0);
264            }
265
266            file_attributes.push(attrs);
267        }
268
269        Ok(Self {
270            version,
271            flags,
272            file_attributes,
273            crc32: None,    // Phase 1 stub
274            md5: None,      // Phase 1 stub
275            filetime: None, // Phase 1 stub
276        })
277    }
278
279    /// Get attributes for a specific block index
280    pub fn get_file_attributes(&self, block_index: usize) -> Option<&FileAttributes> {
281        self.file_attributes.get(block_index)
282    }
283
284    /// Create attributes data for writing to an archive
285    pub fn to_bytes(&self) -> Result<Vec<u8>> {
286        let block_count = self.file_attributes.len();
287        let mut data = Vec::new();
288
289        // Write header
290        data.extend_from_slice(&self.version.to_le_bytes());
291        data.extend_from_slice(&self.flags.as_u32().to_le_bytes());
292
293        // Write CRC32 array if present
294        if self.flags.has_crc32() {
295            for attrs in &self.file_attributes {
296                let crc = attrs.crc32.unwrap_or(0);
297                data.extend_from_slice(&crc.to_le_bytes());
298            }
299        }
300
301        // Write timestamp array if present
302        if self.flags.has_filetime() {
303            for attrs in &self.file_attributes {
304                let time = attrs.filetime.unwrap_or(0);
305                data.extend_from_slice(&time.to_le_bytes());
306            }
307        }
308
309        // Write MD5 array if present
310        if self.flags.has_md5() {
311            for attrs in &self.file_attributes {
312                let hash = attrs.md5.unwrap_or([0u8; 16]);
313                data.extend_from_slice(&hash);
314            }
315        }
316
317        // Write patch bits if present
318        if self.flags.has_patch_bit() {
319            let byte_count = block_count.div_ceil(8);
320            let mut bits = vec![0u8; byte_count];
321
322            for (i, attrs) in self.file_attributes.iter().enumerate() {
323                if attrs.is_patch.unwrap_or(false) {
324                    let byte_index = i / 8;
325                    let bit_index = i % 8;
326                    bits[byte_index] |= 1 << bit_index;
327                }
328            }
329
330            data.extend_from_slice(&bits);
331        }
332
333        Ok(data)
334    }
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340
341    #[test]
342    fn test_attribute_flags() {
343        let flags = AttributeFlags::new(AttributeFlags::ALL);
344        assert!(flags.has_crc32());
345        assert!(flags.has_filetime());
346        assert!(flags.has_md5());
347        assert!(flags.has_patch_bit());
348
349        let flags = AttributeFlags::new(AttributeFlags::CRC32 | AttributeFlags::MD5);
350        assert!(flags.has_crc32());
351        assert!(!flags.has_filetime());
352        assert!(flags.has_md5());
353        assert!(!flags.has_patch_bit());
354    }
355
356    #[test]
357    fn test_parse_empty_attributes() {
358        // Create minimal attributes file with no attributes
359        let mut data = Vec::new();
360        data.extend_from_slice(&100u32.to_le_bytes()); // version
361        data.extend_from_slice(&0u32.to_le_bytes()); // flags (no attributes)
362
363        let bytes = Bytes::from(data);
364        let attrs = Attributes::parse(&bytes, 0).unwrap();
365
366        assert_eq!(attrs.version, 100);
367        assert_eq!(attrs.flags.as_u32(), 0);
368        assert_eq!(attrs.file_attributes.len(), 0);
369    }
370
371    #[test]
372    fn test_parse_crc32_only() {
373        let mut data = Vec::new();
374        data.extend_from_slice(&100u32.to_le_bytes()); // version
375        data.extend_from_slice(&AttributeFlags::CRC32.to_le_bytes()); // flags
376
377        // Add CRC32 values for 2 files
378        data.extend_from_slice(&0x12345678u32.to_le_bytes());
379        data.extend_from_slice(&0x9ABCDEF0u32.to_le_bytes());
380
381        let bytes = Bytes::from(data);
382        let attrs = Attributes::parse(&bytes, 2).unwrap();
383
384        assert_eq!(attrs.version, 100);
385        assert!(attrs.flags.has_crc32());
386        assert!(!attrs.flags.has_filetime());
387        assert!(!attrs.flags.has_md5());
388        assert!(!attrs.flags.has_patch_bit());
389
390        assert_eq!(attrs.file_attributes.len(), 2);
391        assert_eq!(attrs.file_attributes[0].crc32, Some(0x12345678));
392        assert_eq!(attrs.file_attributes[1].crc32, Some(0x9ABCDEF0));
393    }
394
395    #[test]
396    fn test_roundtrip() {
397        // Create attributes with all fields
398        let mut file_attrs = Vec::new();
399
400        let mut attr1 = FileAttributes::new();
401        attr1.crc32 = Some(0x12345678);
402        attr1.filetime = Some(0x01234567_89ABCDEF);
403        attr1.md5 = Some([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
404        attr1.is_patch = Some(false);
405        file_attrs.push(attr1);
406
407        let mut attr2 = FileAttributes::new();
408        attr2.crc32 = Some(0x9ABCDEF0);
409        attr2.filetime = Some(0xFEDCBA98_76543210);
410        attr2.md5 = Some([16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]);
411        attr2.is_patch = Some(true);
412        file_attrs.push(attr2);
413
414        let original = Attributes {
415            version: 100,
416            flags: AttributeFlags::new(AttributeFlags::ALL),
417            file_attributes: file_attrs,
418            crc32: None,    // Phase 1 stub
419            md5: None,      // Phase 1 stub
420            filetime: None, // Phase 1 stub
421        };
422
423        // Convert to bytes and back
424        let bytes = original.to_bytes().unwrap();
425        let parsed = Attributes::parse(&Bytes::from(bytes), 2).unwrap();
426
427        assert_eq!(parsed.version, original.version);
428        assert_eq!(parsed.flags.as_u32(), original.flags.as_u32());
429        assert_eq!(parsed.file_attributes.len(), original.file_attributes.len());
430
431        for i in 0..2 {
432            assert_eq!(parsed.file_attributes[i], original.file_attributes[i]);
433        }
434    }
435}