ra2_mix/xcc_package/
writer.rs1use super::*;
4use std::{collections::VecDeque, fs::create_dir_all, path::PathBuf};
5
6impl MixPackage {
7 pub fn save(self, output: &Path) -> Result<usize, Ra2Error> {
16 if !output.is_file() {
17 return Err(Ra2Error::FileNotFound("must be a file".to_string()));
18 }
19 let data = self.encode()?;
20 std::fs::write(output, &data)?;
21 Ok(data.len())
22 }
23 pub fn dump(self, output: &Path) -> Result<usize, Ra2Error> {
24 if !output.exists() {
25 println!("Skipping file {}", output.display());
26 create_dir_all(output)?;
27 }
28 if !output.is_dir() {
29 return Err(Ra2Error::FileNotFound("file exists but not folder".to_string()));
30 }
31 unsafe { self.dump_unchecked(output) }
32 }
33 pub unsafe fn dump_unchecked(self, output: &Path) -> Result<usize, Ra2Error> {
34 for (filename, data) in self.files.iter() {
35 let mut file = File::create(output.join(&filename))?;
36 file.write_all(&data)?;
37 }
38 Ok(self.files.len())
39 }
40 pub fn encode(self) -> Result<Vec<u8>, Ra2Error> {
49 let file_map = coalesce_input_files(self.game, &self.files)?;
50
51 let mut file_information_list: Vec<FileInfo> =
53 file_map.iter().map(|(filename, data)| FileInfo { file_id: ra2_crc(filename), data: data.clone() }).collect();
54
55 file_information_list.sort_by_key(|file_info| file_info.file_id);
57
58 let mut offset = 0u32;
60 let mut file_entry_data = Vec::new();
61 let mut body_data = Vec::new();
62
63 for file_info in &file_information_list {
64 let size = file_info.data.len() as u32;
65
66 file_entry_data.write_u32::<LittleEndian>(file_info.file_id)?;
68 file_entry_data.write_u32::<LittleEndian>(offset)?;
69 file_entry_data.write_u32::<LittleEndian>(size)?;
70
71 body_data.extend_from_slice(&file_info.data);
73
74 offset += size;
75 }
76
77 let mut mix_data = create_mix_header(&file_map)?;
79 mix_data.extend_from_slice(&file_entry_data);
80 mix_data.extend_from_slice(&body_data);
81
82 Ok(mix_data)
83 }
84}
85
86fn get_mix_db_data(filenames: &[String], game: CncGame) -> Vec<u8> {
88 let num_files = filenames.len();
89 let db_size_in_bytes = XCC_HEADER_SIZE + filenames.iter().map(|filename| filename.len() + 1).sum::<usize>();
90
91 let mut bytes_data = Vec::with_capacity(db_size_in_bytes);
92
93 bytes_data.extend_from_slice(XCC_ID_BYTES);
95 bytes_data.resize(32, 0);
97
98 bytes_data.write_u32::<LittleEndian>(db_size_in_bytes as u32).unwrap();
100 bytes_data.write_u32::<LittleEndian>(XCC_FILE_TYPE).unwrap();
101 bytes_data.write_u32::<LittleEndian>(XCC_FILE_VERSION).unwrap();
102 bytes_data.write_u32::<LittleEndian>(game as u32).unwrap();
103 bytes_data.write_u32::<LittleEndian>(num_files as u32).unwrap();
104
105 for filename in filenames {
107 bytes_data.extend_from_slice(filename.as_bytes());
108 bytes_data.push(0); }
110
111 bytes_data
112}
113
114pub fn coalesce_input_files(game: CncGame, file_map: &HashMap<String, Vec<u8>>) -> Result<HashMap<String, Vec<u8>>, Ra2Error> {
116 let mut extra_file_map = file_map.clone();
117 let mut filenames: Vec<String> = extra_file_map.keys().cloned().collect();
119 filenames.push(MIX_DB_FILENAME.to_string());
120 let db_data = get_mix_db_data(&filenames, game);
122 extra_file_map.insert(MIX_DB_FILENAME.to_string(), db_data);
123 Ok(extra_file_map)
124}
125
126fn create_mix_header(file_map: &HashMap<String, Vec<u8>>) -> Result<Vec<u8>, Ra2Error> {
128 let flags = 0u32;
129 let file_count = file_map.len() as u16;
130 let data_size = file_map.values().map(|data| data.len() as u32).sum();
131
132 let mut header = Vec::with_capacity(HEADER_SIZE);
133 header.write_u32::<LittleEndian>(flags)?;
134 header.write_u16::<LittleEndian>(file_count)?;
135 header.write_u32::<LittleEndian>(data_size)?;
136
137 Ok(header)
138}
139
140pub fn decompress(folder: &Path, db: &MixDatabase) -> Result<(), Ra2Error> {
159 if !folder.exists() {
160 return Err(Ra2Error::FileNotFound("file not found".to_string()));
161 }
162 if !folder.is_dir() {
163 return Err(Ra2Error::FileNotFound("file exists but not folder".to_string()));
164 }
165 let mut queue = VecDeque::new();
166 task_append(&mut queue, folder)?;
167 while let Some(path) = queue.pop_front() {
168 let mix = MixPackage::load(&path, db)?;
169 let sub_folder = path.with_extension("");
170 create_dir_all(&sub_folder)?;
171 unsafe { mix.dump_unchecked(&sub_folder)? };
172 task_append(&mut queue, &sub_folder)?;
173 }
174 Ok(())
175}
176
177fn task_append(paths: &mut VecDeque<PathBuf>, folder: &Path) -> Result<(), Ra2Error> {
178 for entry in folder.read_dir()? {
179 let entry = entry?;
180 let path = entry.path();
181 match path.extension() {
182 Some(s) if s.eq("mix") => {
183 println!("Found mix file: {}", path.display());
184 paths.push_back(path);
185 }
186 _ => {}
187 }
188 }
189 Ok(())
190}