agb_gbafix/
lib.rs

1use anyhow::{Result, anyhow, bail, ensure};
2use std::{collections::HashMap, io::Write};
3
4const GBA_HEADER_SIZE: usize = 192;
5
6const NINTENDO_LOGO: &[u8] = &[
7    0x24, 0xFF, 0xAE, 0x51, 0x69, 0x9A, 0xA2, 0x21, 0x3D, 0x84, 0x82, 0x0A, 0x84, 0xE4, 0x09, 0xAD,
8    0x11, 0x24, 0x8B, 0x98, 0xC0, 0x81, 0x7F, 0x21, 0xA3, 0x52, 0xBE, 0x19, 0x93, 0x09, 0xCE, 0x20,
9    0x10, 0x46, 0x4A, 0x4A, 0xF8, 0x27, 0x31, 0xEC, 0x58, 0xC7, 0xE8, 0x33, 0x82, 0xE3, 0xCE, 0xBF,
10    0x85, 0xF4, 0xDF, 0x94, 0xCE, 0x4B, 0x09, 0xC1, 0x94, 0x56, 0x8A, 0xC0, 0x13, 0x72, 0xA7, 0xFC,
11    0x9F, 0x84, 0x4D, 0x73, 0xA3, 0xCA, 0x9A, 0x61, 0x58, 0x97, 0xA3, 0x27, 0xFC, 0x03, 0x98, 0x76,
12    0x23, 0x1D, 0xC7, 0x61, 0x03, 0x04, 0xAE, 0x56, 0xBF, 0x38, 0x84, 0x00, 0x40, 0xA7, 0x0E, 0xFD,
13    0xFF, 0x52, 0xFE, 0x03, 0x6F, 0x95, 0x30, 0xF1, 0x97, 0xFB, 0xC0, 0x85, 0x60, 0xD6, 0x80, 0x25,
14    0xA9, 0x63, 0xBE, 0x03, 0x01, 0x4E, 0x38, 0xE2, 0xF9, 0xA2, 0x34, 0xFF, 0xBB, 0x3E, 0x03, 0x44,
15    0x78, 0x00, 0x90, 0xCB, 0x88, 0x11, 0x3A, 0x94, 0x65, 0xC0, 0x7C, 0x63, 0x87, 0xF0, 0x3C, 0xAF,
16    0xD6, 0x25, 0xE4, 0x8B, 0x38, 0x0A, 0xAC, 0x72, 0x21, 0xD4, 0xF8, 0x07,
17];
18
19#[derive(Debug, Default)]
20pub struct GbaHeader {
21    pub start_code: [u8; 4],
22    pub game_title: [u8; 12],
23    pub game_code: [u8; 4],
24    pub maker_code: [u8; 2],
25    pub software_version: u8,
26}
27
28impl GbaHeader {
29    fn produce_header(&self) -> Vec<u8> {
30        let mut header_result = vec![];
31
32        header_result.extend_from_slice(&self.start_code);
33        header_result.extend_from_slice(NINTENDO_LOGO);
34        header_result.extend_from_slice(&self.game_title);
35        header_result.extend_from_slice(&self.game_code);
36        header_result.extend_from_slice(&self.maker_code);
37        header_result.push(0x96); // must be 96
38        header_result.push(0); // main unit code (0 for current GBA models)
39        header_result.push(0); // device type, usually 0
40        header_result.extend_from_slice(&[0; 7]); // reserved area, should be zero filled
41        header_result.push(self.software_version);
42
43        let checksum = Self::calculate_checksum(&header_result);
44        header_result.push(checksum); // checksum
45        header_result.extend_from_slice(&[0; 2]); // reserved area, should be zero filled
46
47        assert_eq!(header_result.len(), GBA_HEADER_SIZE);
48
49        header_result
50    }
51
52    fn calculate_checksum(header: &[u8]) -> u8 {
53        let mut chk = 0u8;
54        for value in header.iter().take(0xBC).skip(0xA0) {
55            chk = chk.wrapping_sub(*value);
56        }
57
58        chk = chk.wrapping_sub(0x19);
59        chk
60    }
61}
62
63#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
64pub enum PaddingBehaviour {
65    Pad,
66    #[default]
67    DoNotPad,
68}
69
70pub fn write_gba_file<W: Write>(
71    input: &[u8],
72    mut header: GbaHeader,
73    padding_behaviour: PaddingBehaviour,
74    include_debug: bool,
75    output: &mut W,
76) -> Result<()> {
77    let elf_file = elf::ElfBytes::<elf::endian::AnyEndian>::minimal_parse(input)?;
78
79    let section_headers = elf_file
80        .section_headers()
81        .ok_or_else(|| anyhow!("Failed to parse as elf file"))?;
82
83    let mut bytes_written = 0;
84    for section_header in section_headers {
85        const SHT_NOBITS: u32 = 8;
86        const SHT_NULL: u32 = 0;
87        const SHF_ALLOC: u64 = 2;
88
89        if (section_header.sh_type == SHT_NOBITS || section_header.sh_type == SHT_NULL)
90            || section_header.sh_flags & SHF_ALLOC == 0
91        {
92            continue;
93        }
94
95        let align = bytes_written % section_header.sh_addralign;
96        if align != 0 {
97            for _ in 0..(section_header.sh_addralign - align) {
98                output.write_all(&[0])?;
99                bytes_written += 1;
100            }
101        }
102
103        let (mut data, compression) = elf_file.section_data(&section_header)?;
104        if let Some(compression) = compression {
105            bail!("Cannot decompress elf content, but got compression header {compression:?}");
106        }
107
108        if bytes_written == 0 {
109            ensure!(
110                data.len() > GBA_HEADER_SIZE,
111                "first section must be at least as big as the gba header"
112            );
113
114            header.start_code = data[0..4].try_into().unwrap();
115
116            let header_bytes = header.produce_header();
117            output.write_all(&header_bytes)?;
118
119            data = &data[GBA_HEADER_SIZE..];
120            bytes_written += GBA_HEADER_SIZE as u64;
121        }
122
123        output.write_all(data)?;
124        bytes_written += data.len() as u64;
125    }
126
127    if include_debug {
128        bytes_written += write_debug(&elf_file, output)?;
129    }
130
131    if !bytes_written.is_power_of_two() && padding_behaviour == PaddingBehaviour::Pad {
132        let required_padding = bytes_written.next_power_of_two() - bytes_written;
133
134        for _ in 0..required_padding {
135            output.write_all(&[0])?;
136        }
137    }
138
139    Ok(())
140}
141
142fn write_debug<W: Write>(
143    elf_file: &elf::ElfBytes<'_, elf::endian::AnyEndian>,
144    output: &mut W,
145) -> Result<u64> {
146    let (Some(section_headers), Some(string_table)) = elf_file.section_headers_with_strtab()?
147    else {
148        bail!("Could not find either the section headers or the string table");
149    };
150
151    let mut debug_sections = HashMap::new();
152
153    for section_header in section_headers {
154        let section_name = string_table.get(section_header.sh_name as usize)?;
155        if !section_name.starts_with(".debug") {
156            continue;
157        }
158
159        let (data, compression) = elf_file.section_data(&section_header)?;
160        if compression.is_some() {
161            bail!("Do not support compression in elf files, section {section_name} was compressed");
162        }
163
164        debug_sections.insert(section_name.to_owned(), data);
165    }
166
167    let mut debug_data = vec![];
168    {
169        let mut compressed_writer = lz4_flex::frame::FrameEncoder::new(&mut debug_data);
170        rmp_serde::encode::write(&mut compressed_writer, &debug_sections)?;
171        compressed_writer.flush()?;
172    }
173
174    output.write_all(&debug_data)?;
175    output.write_all(&(debug_data.len() as u32).to_le_bytes())?;
176    output.write_all(b"agb1")?;
177
178    Ok(debug_data.len() as u64 + 4)
179}