1pub mod gfarch {
2 use std::io::Cursor;
3 use bpe_rs::bpe;
4 use nintendo_lz;
5 use byteorder::{ByteOrder, LittleEndian};
6 use thiserror;
7
8 #[derive(thiserror::Error, Debug)]
9 pub enum GfArchError {
11 #[error("Archive header was not valid")]
12 ArchiveHeaderError,
13
14 #[error("Compression header was not valid")]
15 CompressionHeaderError,
16
17 #[error("Unsupported compression type, found type with value {0}")]
18 UnsupportedCompressionTypeError(u32),
19
20 #[error("Failed to decompress LZ10")]
21 LZ10DecompressError
22 }
23
24 pub enum GFCPOffset {
26 Default,
27 Custom(usize)
28 }
29
30 #[derive(PartialEq)]
31 pub enum Version {
33 V2,
34 V3,
35 V3_1,
36 }
37
38 #[derive(PartialEq)]
39 pub enum CompressionType {
41 BPE,
42 LZ10
43 }
44
45 struct FileEntry {
46 name_offset: usize,
47 decompressed_size: usize,
48 decompressed_offset: usize,
49 }
50
51 impl FileEntry {
52 fn from_bytes(input: &[u8]) -> Self {
53 assert_eq!(0x10, input.len());
54
55 let name_offset = (LittleEndian::read_u32(&input[4..8]) & 0x00FFFFFF) as usize;
56 let decompressed_size = LittleEndian::read_u32(&input[8..0xC]) as usize;
57 let decompressed_offset = LittleEndian::read_u32(&input[0xC..0x10]) as usize;
58
59 Self {
60 name_offset,
61 decompressed_size,
62 decompressed_offset
63 }
64 }
65 }
66
67 pub fn calculate_checksum(input: &str) -> u32 {
75 let mut result: u32 = 0;
76
77 for c in input.bytes() {
78 result = c as u32 + result.wrapping_mul(137);
79 }
80
81 result
82 }
83
84 fn read_string(input: &[u8], offset: usize) -> String {
85 let mut result = String::new();
86
87 for &byte in &input[offset..] {
88 if byte == 0 {
89 break;
90 }
91
92 result.push(byte as char);
93 }
94
95 result
96 }
97
98 pub fn extract(input: &[u8]) -> Result<Vec<(String, Vec<u8>)>, GfArchError> {
106 if &input[..4] != b"GFAC" {
107 return Err(GfArchError::ArchiveHeaderError);
108 }
109
110 let file_count = LittleEndian::read_u32(&input[0x2C..0x30]);
111 let mut entries = Vec::new();
112 let mut filenames = Vec::<String>::new();
113
114 entries.extend(
117 input[0x30..]
118 .chunks(0x10)
119 .take(file_count as usize)
120 .map(FileEntry::from_bytes)
121 );
122
123 filenames.extend(
126 entries.iter().map(|entry|
127 read_string(input, entry.name_offset)
128 )
129 );
130
131 let gfcp_offset = LittleEndian::read_u32(&input[0x14..0x18]) as usize;
134
135 if &input[gfcp_offset..gfcp_offset + 4] != b"GFCP" {
136 return Err(GfArchError::CompressionHeaderError);
137 }
138
139 let raw_compression_type = LittleEndian::read_u32(&input[gfcp_offset + 0x8..gfcp_offset + 0xC]);
142 let compression_type = match raw_compression_type {
143 1 => CompressionType::BPE,
144 3 => CompressionType::LZ10,
145 _ => {
146 return Err(GfArchError::UnsupportedCompressionTypeError(raw_compression_type))
147 }
148 };
149
150
151 let decompressed_chunk = match compression_type {
152 CompressionType::BPE => bpe::decode(&input[gfcp_offset + 0x14..], bpe::DEFAULT_STACK_SIZE),
153 CompressionType::LZ10 => {
154 let decompressed_size = LittleEndian::read_u32(
155 &input[gfcp_offset + 0xC..gfcp_offset + 0x10]
156 );
157
158 let mut lz_chunk = vec![0x10]; lz_chunk.extend_from_slice(&decompressed_size.to_le_bytes()[..3]);
162 lz_chunk.extend_from_slice(&input[gfcp_offset + 0x14..]);
163
164
165 let result = nintendo_lz::decompress_arr(&lz_chunk);
166
167 if let Ok(decompressed) = result {
168 decompressed
169 } else {
170 return Err(GfArchError::LZ10DecompressError);
171 }
172 }
173 };
174
175 let files: Vec<(String, Vec<u8>)> = (0..file_count as usize)
176 .map(|i|{
177 let offset = entries[i].decompressed_offset - gfcp_offset;
178 let size = entries[i].decompressed_size;
179
180 (filenames[i].clone(), decompressed_chunk[offset..offset + size].to_vec())
181 }).collect();
182
183 Ok(files)
184 }
185
186
187
188 pub fn pack_from_bytes(
205 input: &[Vec<u8>],
206 filenames: &[String],
207 version: Version,
208 compression_type: CompressionType,
209 offset: GFCPOffset
210 ) -> Vec<u8> {
211 assert_eq!(input.len(), filenames.len());
212
213 let files: Vec::<(String, Vec<u8>)> = (0..input.len())
214 .map(|i| {
215 (filenames[i].clone(), input[i].to_vec())
216 }).collect();
217
218
219 pack_from_files(&files, version, compression_type, offset)
220 }
221
222 pub fn pack_from_files(
236 input: &[(String, Vec<u8>)],
237 version: Version,
238 compression_type: CompressionType,
239 offset: GFCPOffset
240 ) -> Vec<u8> {
241 let file_count = input.len();
245
246 let mut decompressed_chunk = Vec::new();
248
249 for file in input.iter() {
250 decompressed_chunk.extend_from_slice(&file.1);
251 decompressed_chunk.resize(
252 decompressed_chunk.len().next_multiple_of(0x10),
253 0
254 );
255 }
256
257 let compressed_chunk = match compression_type {
259 CompressionType::BPE => bpe::encode(&decompressed_chunk),
260 CompressionType::LZ10 => {
261 let mut compressed: Vec<u8> = Vec::new();
263 let mut writer = Cursor::new(&mut compressed);
264 nintendo_lz::compress(&decompressed_chunk, &mut writer, nintendo_lz::CompressionLevel::LZ10).unwrap();
265
266 compressed[4..].to_vec()
270 }
271 };
272
273 let mut file_name_section_length = 0usize;
274
275 for file in input.iter() {
276 file_name_section_length += file.0.len();
277 }
278
279 let archive_size = match offset {
280 GFCPOffset::Default => {
281 0x30 + (file_count * 0x10) + file_name_section_length.next_multiple_of(0x10) + 0x14 + compressed_chunk.len() }
287
288 GFCPOffset::Custom(offs) => {
289 offs + 0x14 + compressed_chunk.len()
290 }
291 };
292
293 let mut output = vec![0u8; archive_size];
295
296 output[0] = b'G';
298 output[1] = b'F';
299 output[2] = b'A';
300 output[3] = b'C';
301
302 LittleEndian::write_u32(&mut output[0x4..0x8], match version {
304 Version::V2 => 0x0200,
305 Version::V3 => 0x0300,
306 Version::V3_1 => 0x0301,
307 });
308
309 output[0x8] = 1;
311
312 LittleEndian::write_u32(&mut output[0xC..0x10], 0x2C);
314
315 let file_info_size: u32 =
317 4 + (file_count * 0x10) as u32 + file_name_section_length as u32 + file_count as u32; LittleEndian::write_u32(&mut output[0x10..0x14], file_info_size);
324
325 let file_info_size = file_info_size.next_multiple_of(0x10);
326
327 let gfcp_offset: u32 = match offset {
329 GFCPOffset::Default => 0x30 + file_info_size,
330 GFCPOffset::Custom(offs) => offs as u32
331 };
332
333 LittleEndian::write_u32(&mut output[0x14..0x18], gfcp_offset);
334
335 LittleEndian::write_u32(
337 &mut output[0x18..0x1C],
338 {
339 0x14 + compressed_chunk.len() as u32
341 }
342 );
343
344 LittleEndian::write_u32(&mut output[0x2C..0x30], file_count as u32);
346
347 let mut cur_name_offset =
349 0x30 + (file_count * 0x10); let mut decompressed_offset = 0x30 + file_info_size;
353 for i in 0..file_count {
354 let checksum = calculate_checksum(&input[i].0);
355 let name_offset = if i == file_count - 1 {
356 cur_name_offset as u32 | 0x80000000
358 } else {
359 cur_name_offset as u32
360 };
361
362
363 let data_offset = decompressed_offset;
364
365 let offset = 0x30 + (i * 0x10);
366
367
368 LittleEndian::write_u32(&mut output[offset..offset + 4], checksum);
370 LittleEndian::write_u32(&mut output[offset + 4..offset + 8], name_offset);
372 LittleEndian::write_u32(&mut output[offset + 8..offset + 0xC], input[i].1.len() as u32);
374 LittleEndian::write_u32(&mut output[offset + 0xC..offset + 0x10], data_offset);
376
377 cur_name_offset += input[i].0.len() + 1;
379 decompressed_offset += (input[i].1.len() as u32).next_multiple_of(0x10);
380 }
381
382 let mut name_offs = 0x30 + (file_count * 0x10);
384
385 for file in input.iter() {
386 let filename_bytes = file.0.as_bytes();
387 output[name_offs..name_offs + filename_bytes.len()].copy_from_slice(filename_bytes);
388 name_offs += filename_bytes.len();
389
390 output[name_offs] = 0; name_offs += 1;
392 }
393
394 let gfcp_offset = gfcp_offset as usize;
398 output[gfcp_offset] = b'G';
399 output[gfcp_offset + 1] = b'F';
400 output[gfcp_offset + 2] = b'C';
401 output[gfcp_offset + 3] = b'P';
402
403
404 LittleEndian::write_u32(&mut output[gfcp_offset + 4..gfcp_offset + 8], 1);
406
407 LittleEndian::write_u32(
409 &mut output[gfcp_offset + 8..gfcp_offset + 0xC],
410
411 match compression_type {
412 CompressionType::BPE => 1,
413 CompressionType::LZ10 => 3
414 }
415 );
416
417 LittleEndian::write_u32(
419 &mut output[gfcp_offset + 0xC..gfcp_offset + 0x10],
420 decompressed_chunk.len() as u32
421 );
422
423 LittleEndian::write_u32(
425 &mut output[gfcp_offset + 0x10..gfcp_offset + 0x14],
426 compressed_chunk.len() as u32
427 );
428
429 let target_offset = gfcp_offset + 0x14;
432 let target_len = compressed_chunk.len();
433 if target_offset + target_len > output.len() {
434 output.resize(target_offset + target_len, 0);
435 }
436
437 output[target_offset..target_offset + target_len]
438 .copy_from_slice(&compressed_chunk);
439 output
440 }
441
442
443}
444
445
446#[cfg(test)]
447mod tests {
448 use super::*;
449
450 #[test]
451 fn validate_checksum() {
452 let sample = "sea_turtle_01.brres";
453 let checksum = gfarch::calculate_checksum(sample);
454 assert_eq!(0xCC91B7B8, checksum.swap_bytes());
455 }
456}