ra2_mix/xcc_package/
reader.rs1use super::*;
4use crate::MixDatabase;
5use std::borrow::Cow;
6
7impl MixPackage {
8 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 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
41fn header_is_encrypted(header: &MixHeader) -> bool {
43 header.flags.is_some() && (header.flags.unwrap() & 0x20000) != 0
44}
45
46fn 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
61fn 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#[cfg(feature = "serde_json")]
72pub fn load_global_mix_database() -> Result<HashMap<String, i32>, Ra2Error> {
73 Ok(HashMap::new())
76}
77
78fn 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 let first_word = cursor.read_u16::<LittleEndian>()?;
84 cursor.seek(SeekFrom::Start(0))?; let header: MixHeader;
87 let header_size: usize;
88
89 if first_word != 0 {
90 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 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 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 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 let mix_data_vec = mix_data.to_vec();
137
138 Ok((updated_header, file_entries, mix_data_vec))
139}
140
141fn 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 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 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 let id_filename_map = match local_mix_db_file_entry {
177 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 _ => Cow::Borrowed(db),
184 };
185
186 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}