Skip to main content

gfarch/
lib.rs

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    /// Errors for various GfArch problems. 
10    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    /// Allows the user to specify a custom GFCP offset.
25    pub enum GFCPOffset {
26        Default,
27        Custom(usize)
28    }
29
30    #[derive(PartialEq)]
31    /// The version of a GfArch archive.
32    pub enum Version {
33        V2,
34        V3,
35        V3_1,
36    }
37
38    #[derive(PartialEq)]
39    /// The compression type of a GfArch archive.
40    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    /// Calculates a checksum from a string, most commonly a filename.
68    /// 
69    /// ### Parameters
70    /// `input`: The input string.
71    /// 
72    /// ### Returns
73    /// The output checksum as a `u32`.
74    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    /// Extracts the contents of a GfArch archive.
99    /// 
100    /// ### Parameters
101    /// `input`: The archive contents to be extracted.
102    /// 
103    /// ### Returns
104    /// A `Vec<(String, Vec<u8>)>`, containing the contents of the archive.
105    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        // read file entries
115        
116        entries.extend(
117            input[0x30..]
118            .chunks(0x10)
119            .take(file_count as usize)
120            .map(FileEntry::from_bytes)
121        );
122
123        // read filenames
124        
125        filenames.extend(
126            entries.iter().map(|entry|
127                read_string(input, entry.name_offset)
128            )
129        );
130
131        // read compression header
132
133        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        // decompress files
140
141        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                // nintendo_lz works with headered chunks but GfArch does not.
159                // construct a 4-byte header
160                let mut lz_chunk = vec![0x10]; // LZ10
161                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    /// Creates a GfArch archive from given files and filenames.
189    /// 
190    /// ### Parameters
191    /// `input`: The files to be put in the archive.
192    /// 
193    /// `filenames`: The names of each file in the archive.
194    /// 
195    /// `version`: The archive version.
196    /// 
197    /// `compression_type`: The compression type.
198    /// 
199    /// `offset`: An offset for the GFCP header, if specified.
200    /// For Yoshi's Woolly World, use `0x2000`.
201    /// 
202    /// ### Returns
203    /// A `Vec<u8>`, containing the archive.
204    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    /// Creates a GfArch archive from given files.
223    /// 
224    /// ### Parameters
225    /// `input`: The filenames and contents to be put in the archive,
226    /// 
227    /// `version`: The archive version.
228    /// 
229    /// `compression_type`: The compression type.
230    /// 
231    /// `offset`: An offset for the GFCP header, if specified.
232    /// For Yoshi's Woolly World, use `0x2000`.
233    /// ### Returns
234    /// A `Vec<u8>`, containing the archive.
235    pub fn pack_from_files(
236        input: &[(String, Vec<u8>)],
237        version: Version,
238        compression_type: CompressionType,
239        offset: GFCPOffset
240    ) -> Vec<u8> {
241        // Yoshi's Woolly World is the only known game
242        // that consistently picks the same offset
243
244        let file_count = input.len();
245
246        // concatenate all data
247        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        // compress all data
258        let compressed_chunk = match compression_type {
259            CompressionType::BPE => bpe::encode(&decompressed_chunk),
260            CompressionType::LZ10 => {
261                // create a cursor so we can specify LZ10
262                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                // nintendo_lz works with headered chunks but GfArch does not.
267                // the 4-byte header must be removed here
268
269                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 + // archive header
282                (file_count * 0x10) + // file entries
283                file_name_section_length.next_multiple_of(0x10) + // filenames
284                0x14 + // compression header
285                compressed_chunk.len() // compressed data
286            }
287
288            GFCPOffset::Custom(offs) => {
289                offs + 0x14 + compressed_chunk.len()
290            }
291        };
292        
293        // write archive header
294        let mut output = vec![0u8; archive_size];
295        
296        // magic
297        output[0] = b'G';
298        output[1] = b'F';
299        output[2] = b'A';
300        output[3] = b'C';
301
302        // version
303        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        // is compressed
310        output[0x8] = 1;
311
312        // file entry offset
313        LittleEndian::write_u32(&mut output[0xC..0x10], 0x2C);
314
315        // file info size
316        let file_info_size: u32 =
317            4 + // the actual beginning of the file info
318            (file_count * 0x10) as u32 + // file entries
319            file_name_section_length as u32 + // length of all strings
320            file_count as u32; // (plus null terminators)
321
322
323        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        // gfcp offset
328        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        // payload size
336        LittleEndian::write_u32(
337            &mut output[0x18..0x1C],
338            {
339                0x14 + // gfcp header
340                compressed_chunk.len() as u32
341            }
342        );
343
344        // file count
345        LittleEndian::write_u32(&mut output[0x2C..0x30], file_count as u32);
346
347        // write file entries
348        let mut cur_name_offset =
349            0x30 + // header size
350            (file_count * 0x10); // file entries
351
352        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                // if last entry, apply a flag to indicate so
357                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            // checksum
369            LittleEndian::write_u32(&mut output[offset..offset + 4], checksum);
370            // name offset
371            LittleEndian::write_u32(&mut output[offset + 4..offset + 8], name_offset);
372            // size
373            LittleEndian::write_u32(&mut output[offset + 8..offset + 0xC], input[i].1.len() as u32);
374            // offset
375            LittleEndian::write_u32(&mut output[offset + 0xC..offset + 0x10], data_offset);
376
377            // update offsets
378            cur_name_offset += input[i].0.len() + 1;
379            decompressed_offset += (input[i].1.len() as u32).next_multiple_of(0x10);
380        }
381
382        // write strings
383        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; // null terminator
391            name_offs += 1;
392        }
393
394        // write compression header
395        // magic
396
397        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        // "version" -- this value is always 1
405        LittleEndian::write_u32(&mut output[gfcp_offset + 4..gfcp_offset + 8], 1);
406
407        // write compression type
408        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        // decompressed size
418        LittleEndian::write_u32(
419            &mut output[gfcp_offset + 0xC..gfcp_offset + 0x10],
420            decompressed_chunk.len() as u32
421        );
422
423        // compressed size
424        LittleEndian::write_u32(
425            &mut output[gfcp_offset + 0x10..gfcp_offset + 0x14],
426            compressed_chunk.len() as u32
427        );
428
429        // write the compressed data
430
431        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}