ra2_mix/xcc_package/
reader.rs

1//! Reader module for RA2 MIX files
2
3use super::*;
4use crate::MixDatabase;
5use std::borrow::Cow;
6
7impl MixPackage {
8    /// # Arguments
9    ///
10    /// * `input`:
11    ///
12    /// returns: Result<XccPackage, MixError>
13    ///
14    /// # Examples
15    ///
16    /// ```
17    /// ```
18    pub fn load(mix_path: &Path, db: &MixDatabase) -> Result<Self, Ra2Error> {
19        let data = std::fs::read(mix_path)?;
20        MixPackage::decode(&data, db)
21    }
22    /// Reads a MIX file and returns a map of filenames to file data
23    ///
24    /// # Arguments
25    ///
26    /// * `input`:
27    ///
28    /// returns: Result<XccPackage, MixError>
29    ///
30    /// # Examples
31    ///
32    /// ```
33    /// ```
34    pub fn decode(mix_data: &[u8], db: &MixDatabase) -> Result<Self, Ra2Error> {
35        let (header, file_entries, mix_data_vec) = read_file_info(mix_data)?;
36        let map = get_file_map(&file_entries, &mix_data_vec, &header, db)?;
37        Ok(Self { game: Default::default(), files: map })
38    }
39}
40
41/// Checks if a MIX header is encrypted
42fn header_is_encrypted(header: &MixHeader) -> bool {
43    header.flags.is_some() && (header.flags.unwrap() & 0x20000) != 0
44}
45
46/// Parses file entries from index data
47fn get_file_entries(file_count: usize, index_data: &[u8]) -> Result<Vec<FileEntry>, Ra2Error> {
48    let mut file_entries = Vec::with_capacity(file_count);
49    let mut cursor = std::io::Cursor::new(index_data);
50
51    for _ in 0..file_count {
52        let id = cursor.read_u32::<LittleEndian>()?;
53        let offset = cursor.read_i32::<LittleEndian>()?;
54        let size = cursor.read_i32::<LittleEndian>()?;
55        file_entries.push(FileEntry { id, offset, size });
56    }
57
58    Ok(file_entries)
59}
60
61/// Extracts file data from MIX body
62fn get_file_data_from_mix_body(file_entry: &FileEntry, mix_body_data: &[u8]) -> Vec<u8> {
63    tracing::trace!("FileEntry: {:?}", file_entry);
64    let start = file_entry.offset as usize;
65    let end = start + file_entry.size as usize;
66
67    if end <= mix_body_data.len() { mix_body_data[start..end].to_vec() } else { Vec::new() }
68}
69
70/// Loads the global mix database
71#[cfg(feature = "serde_json")]
72pub fn load_global_mix_database() -> Result<HashMap<String, i32>, Ra2Error> {
73    // In a real implementation, this would load from an embedded resource
74    // For now, we'll return an empty map
75    Ok(HashMap::new())
76}
77
78/// Reads file information from a MIX file
79fn read_file_info(mix_data: &[u8]) -> Result<(MixHeader, Vec<FileEntry>, Vec<u8>), Ra2Error> {
80    let mut cursor = std::io::Cursor::new(mix_data);
81
82    // Check if this is an old format MIX file
83    let first_word = cursor.read_u16::<LittleEndian>()?;
84    cursor.seek(SeekFrom::Start(0))?; // Reset cursor position
85
86    let header: MixHeader;
87    let header_size: usize;
88
89    if first_word != 0 {
90        // Old format
91        let count = cursor.read_u16::<LittleEndian>()?;
92        let size = cursor.read_u32::<LittleEndian>()?;
93        header = MixHeader { flags: None, file_count: count, data_size: size };
94        header_size = MIN_HEADER_SIZE;
95    }
96    else {
97        // New format
98        let flags = cursor.read_u32::<LittleEndian>()?;
99        let count = cursor.read_u16::<LittleEndian>()?;
100        let size = cursor.read_u32::<LittleEndian>()?;
101        header = MixHeader { flags: Some(flags), file_count: count, data_size: size };
102        header_size = HEADER_SIZE;
103    }
104
105    let file_entries: Vec<FileEntry>;
106    let mut updated_header = header.clone();
107
108    if header_is_encrypted(&header) {
109        // Handle encrypted header
110        let encrypted_key_start = SIZE_OF_FLAGS;
111        let encrypted_key_end = encrypted_key_start + SIZE_OF_ENCRYPTED_KEY;
112
113        let encrypted_blowfish_key = &mix_data[encrypted_key_start..encrypted_key_end];
114        let decrypted_blowfish_key = decrypt_blowfish_key(encrypted_blowfish_key)?;
115
116        let (file_count, data_size, index_data) = decrypt_mix_header(mix_data, &decrypted_blowfish_key)?;
117
118        file_entries = get_file_entries(file_count as usize, &index_data)?;
119        updated_header.file_count = file_count;
120        updated_header.data_size = data_size;
121    }
122    else {
123        // Handle unencrypted header
124        let index_start = header_size;
125        let index_end = index_start + (header.file_count as usize * FILE_ENTRY_SIZE);
126
127        if index_end > mix_data.len() {
128            return Err(Ra2Error::InvalidFormat { message: "File too small for index".to_string() });
129        }
130
131        let index_data = &mix_data[index_start..index_end];
132        file_entries = get_file_entries(header.file_count as usize, index_data)?;
133    }
134
135    // Convert mix_data_ref to owned Vec<u8>
136    let mix_data_vec = mix_data.to_vec();
137
138    Ok((updated_header, file_entries, mix_data_vec))
139}
140
141/// Creates a file map from file entries and mix data
142fn get_file_map(
143    file_entries: &[FileEntry],
144    mix_data: &[u8],
145    header: &MixHeader,
146    db: &MixDatabase,
147) -> Result<HashMap<String, Vec<u8>>, Ra2Error> {
148    if file_entries.len() <= 1 {
149        return Ok(HashMap::new());
150    }
151
152    let mix_db_id = ra2_crc(MIX_DB_FILENAME);
153    debug_assert_eq!(mix_db_id, 0x366E051F);
154
155    // Calculate body start position
156    let mut body_start =
157        if header.flags.is_none() { MIN_HEADER_SIZE } else { HEADER_SIZE } + (FILE_ENTRY_SIZE * file_entries.len());
158
159    if header_is_encrypted(header) {
160        body_start += SIZE_OF_ENCRYPTED_KEY;
161        body_start += get_decryption_block_sizing(header.file_count).1;
162    }
163
164    // Find local mix database if it exists
165    let mut local_mix_db_file_entry = None;
166    for entry in file_entries {
167        if entry.id == mix_db_id {
168            local_mix_db_file_entry = Some(*entry);
169            break;
170        }
171    }
172
173    let mix_body_data = &mix_data[body_start..];
174
175    // Get filename to ID mapping
176    let id_filename_map = match local_mix_db_file_entry {
177        // Use local mix database
178        Some(db_entry) if db_entry.offset > 0 => {
179            let local_mix_db_data = get_file_data_from_mix_body(&db_entry, mix_body_data);
180            Cow::Owned(MixDatabase::decode(&local_mix_db_data)?)
181        }
182        // Use global mix database
183        _ => Cow::Borrowed(db),
184    };
185
186    // Create file map
187    let mut filemap = HashMap::new();
188
189    for entry in file_entries {
190        let file_data = get_file_data_from_mix_body(entry, mix_body_data);
191
192        if let Some(filename) = id_filename_map.get(entry.id) {
193            filemap.insert(filename.clone(), file_data);
194        }
195    }
196
197    Ok(filemap)
198}