Skip to main content

ds_rom/rom/
header.rs

1use std::{
2    mem::{offset_of, size_of},
3    str::FromStr,
4};
5
6use serde::{Deserialize, Serialize};
7use snafu::Snafu;
8
9use super::{
10    raw::{
11        self, AccessControl, Capacity, Delay, DsFlags, DsiFlags, DsiFlags2, HeaderVersion, ProgramOffset, RegionFlags,
12        TableOffset,
13    },
14    BuildContext, Rom,
15};
16use crate::{
17    crc::CRC_16_MODBUS,
18    str::{AsciiArray, AsciiArrayError},
19};
20/// ROM header.
21#[derive(Serialize, Deserialize, Default)]
22pub struct Header {
23    /// Values for the original header version, [`HeaderVersion::Original`].
24    #[serde(flatten)]
25    pub original: HeaderOriginal,
26    /// Values for DS games after DSi release, [`HeaderVersion::DsPostDsi`].
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub ds_post_dsi: Option<HeaderDsPostDsi>,
29}
30
31/// Values for the original header version, [`HeaderVersion::Original`].
32#[derive(Serialize, Deserialize, Default)]
33pub struct HeaderOriginal {
34    /// Short game title, normally in uppercase letters.
35    pub title: String,
36    /// 4-character game code in uppercase letters.
37    pub gamecode: AsciiArray<4>,
38    /// 2-character maker code, normally "01".
39    pub makercode: AsciiArray<2>,
40    /// Unit code, depends on which platform (DS, DSi) this game is for.
41    pub unitcode: u8,
42    /// Encryption seed select.
43    pub seed_select: u8,
44    /// Flags for both DS and DSi.
45    pub ds_flags: DsFlags,
46    /// ROM version, usually zero.
47    pub rom_version: u8,
48    /// Autostart, can skip "Health and Safety" screen.
49    pub autostart: u8,
50    /// Port 0x40001a4 setting for normal commands.
51    pub normal_cmd_setting: u32,
52    /// Port 0x40001a4 setting for KEY1 commands.
53    pub key1_cmd_setting: u32,
54    /// Delay to wait for secure area.
55    pub secure_area_delay: Delay,
56    /// NAND end of ROM area in multiples of 0x20000 (0x80000 on DSi).
57    pub rom_nand_end: u16,
58    /// NAND end of RW area in multiples of 0x20000 (0x80000 on DSi).
59    pub rw_nand_end: u16,
60    /// Whether the header has the ARM9 build info offset.
61    pub has_arm9_build_info_offset: bool,
62}
63
64/// Values for DS games after DSi release, [`HeaderVersion::DsPostDsi`].
65#[derive(Serialize, Deserialize)]
66pub struct HeaderDsPostDsi {
67    /// DSi-exclusive flags.
68    pub dsi_flags_2: DsiFlags2,
69    /// SHA1-HMAC of banner.
70    pub sha1_hmac_banner: [u8; 0x14],
71    /// Unknown SHA1-HMAC, defined by some games.
72    pub sha1_hmac_unk1: [u8; 0x14],
73    /// Unknown SHA1-HMAC, defined by some games.
74    pub sha1_hmac_unk2: [u8; 0x14],
75    /// RSA-SHA1 signature up to [`raw::Header::debug_args`].
76    pub rsa_sha1: Box<[u8]>,
77}
78
79/// Errors related to [`Header::build`].
80#[derive(Snafu, Debug)]
81pub enum HeaderBuildError {
82    /// See [`AsciiArrayError`].
83    #[snafu(transparent)]
84    AsciiArray {
85        /// Source error.
86        source: AsciiArrayError,
87    },
88}
89
90impl Header {
91    /// Loads from a raw header.
92    pub fn load_raw(header: &raw::Header) -> Self {
93        let version = header.version();
94        Self {
95            original: HeaderOriginal {
96                title: header.title.to_string(),
97                gamecode: header.gamecode,
98                makercode: header.makercode,
99                unitcode: header.unitcode,
100                seed_select: header.seed_select,
101                ds_flags: header.ds_flags,
102                rom_version: header.rom_version,
103                autostart: header.autostart,
104                normal_cmd_setting: header.normal_cmd_setting,
105                key1_cmd_setting: header.key1_cmd_setting,
106                secure_area_delay: header.secure_area_delay,
107                rom_nand_end: header.rom_nand_end,
108                rw_nand_end: header.rw_nand_end,
109                has_arm9_build_info_offset: header.arm9_build_info_offset != 0,
110            },
111            ds_post_dsi: (version >= HeaderVersion::DsPostDsi).then_some(HeaderDsPostDsi {
112                dsi_flags_2: header.dsi_flags_2,
113                sha1_hmac_banner: header.sha1_hmac_banner,
114                sha1_hmac_unk1: header.sha1_hmac_unk1,
115                sha1_hmac_unk2: header.sha1_hmac_unk2,
116                rsa_sha1: Box::new(header.rsa_sha1),
117            }),
118        }
119    }
120
121    /// Builds a raw header.
122    ///
123    /// # Panics
124    ///
125    /// Panics if a value is missing in the `context`.
126    ///
127    /// # Errors
128    ///
129    /// This function will return an error if the title contains a non-ASCII character.
130    pub fn build(&self, context: &BuildContext, rom: &Rom) -> Result<raw::Header, HeaderBuildError> {
131        let logo = rom.header_logo().compress();
132        let arm9 = rom.arm9();
133        let arm7 = rom.arm7();
134        let arm9_offset = context.arm9_offset.expect("ARM9 offset must be known");
135        let arm7_offset = context.arm7_offset.expect("ARM7 offset must be known");
136        let mut header = raw::Header {
137            title: AsciiArray::from_str(&self.original.title)?,
138            gamecode: self.original.gamecode,
139            makercode: self.original.makercode,
140            unitcode: self.original.unitcode,
141            seed_select: self.original.seed_select,
142            capacity: Capacity::from_size(context.rom_size.expect("ROM size must be known")),
143            reserved0: [0; 7],
144            dsi_flags: DsiFlags::new(),
145            ds_flags: self.original.ds_flags,
146            rom_version: self.original.rom_version,
147            autostart: self.original.autostart,
148            arm9: ProgramOffset {
149                offset: arm9_offset,
150                entry: arm9.entry_function(),
151                base_addr: arm9.base_address(),
152                size: arm9.full_data().len() as u32,
153            },
154            arm7: ProgramOffset {
155                offset: arm7_offset,
156                entry: arm7.entry_function(),
157                base_addr: arm7.base_address(),
158                size: arm7.full_data().len() as u32,
159            },
160            file_names: context.fnt_offset.expect("FNT offset must be known"),
161            file_allocs: context.fat_offset.expect("FAT offset must be known"),
162            arm9_overlays: context.arm9_ovt_offset.unwrap_or_default(),
163            arm7_overlays: context.arm7_ovt_offset.unwrap_or_default(),
164            normal_cmd_setting: self.original.normal_cmd_setting,
165            key1_cmd_setting: self.original.key1_cmd_setting,
166            banner_offset: context.banner_offset.map(|b| b.offset).expect("Banner offset must be known"),
167            secure_area_crc: if let Some(key) = context.blowfish_key {
168                arm9.secure_area_crc(key, self.original.gamecode.to_le_u32())
169            } else {
170                0
171            },
172            secure_area_delay: self.original.secure_area_delay,
173            arm9_autoload_callback: context.arm9_autoload_callback.expect("ARM9 autoload callback must be known"),
174            arm7_autoload_callback: context.arm7_autoload_callback.expect("ARM7 autoload callback must be known"),
175            secure_area_disable: 0,
176            rom_size_ds: context.rom_size.expect("ROM size must be known"),
177            header_size: size_of::<raw::Header>() as u32,
178            arm9_build_info_offset: if self.original.has_arm9_build_info_offset {
179                context.arm9_build_info_offset.map(|offset| offset + arm9_offset).unwrap_or(0)
180            } else {
181                0
182            },
183            arm7_build_info_offset: context.arm7_build_info_offset.map(|offset| offset + arm7_offset).unwrap_or(0),
184            ds_rom_region_end: 0,
185            dsi_rom_region_end: 0,
186            rom_nand_end: self.original.rom_nand_end,
187            rw_nand_end: self.original.rw_nand_end,
188            reserved1: [0; 0x18],
189            reserved2: [0; 0x10],
190            logo,
191            logo_crc: CRC_16_MODBUS.checksum(&logo),
192            header_crc: 0, // gets updated below
193            debug_rom_offset: 0,
194            debug_size: 0,
195            debug_ram_addr: 0,
196            reserved3: [0; 0x4],
197            reserved4: [0; 0x10],
198            // The below fields are for DSi only and are not supported yet
199            memory_banks_wram: [0; 5],
200            memory_banks_arm9: [0; 3],
201            memory_banks_arm7: [0; 3],
202            memory_bank_9: 0,
203            region_flags: RegionFlags::new(),
204            access_control: AccessControl::new(),
205            arm7_scfg_ext7_setting: 0,
206            dsi_flags_2: DsiFlags2::new(),
207            arm9i: ProgramOffset::default(),
208            arm7i: ProgramOffset::default(),
209            digest_ds_area: TableOffset::default(),
210            digest_dsi_area: TableOffset::default(),
211            digest_sector_hashtable: TableOffset::default(),
212            digest_block_hashtable: TableOffset::default(),
213            digest_sector_size: 0,
214            digest_sector_count: 0,
215            banner_size: 0,
216            sd_shared2_0000_size: 0,
217            sd_shared2_0001_size: 0,
218            eula_version: 0,
219            use_ratings: false,
220            rom_size_dsi: 0,
221            sd_shared2_0002_size: 0,
222            sd_shared2_0003_size: 0,
223            sd_shared2_0004_size: 0,
224            sd_shared2_0005_size: 0,
225            arm9i_build_info_offset: 0,
226            arm7i_build_info_offset: 0,
227            modcrypt_area_1: TableOffset::default(),
228            modcrypt_area_2: TableOffset::default(),
229            gamecode_rev: AsciiArray([0; 4]),
230            file_type: 0,
231            sd_public_sav_size: 0,
232            sd_private_sav_size: 0,
233            reserved5: [0; 0xb0],
234            age_ratings: [0; 0x10],
235            sha1_hmac_arm9_with_secure_area: [0; 0x14],
236            sha1_hmac_arm7: [0; 0x14],
237            sha1_hmac_digest: [0; 0x14],
238            sha1_hmac_banner: [0; 0x14],
239            sha1_hmac_arm9i: [0; 0x14],
240            sha1_hmac_arm7i: [0; 0x14],
241            sha1_hmac_unk1: [0; 0x14],
242            sha1_hmac_unk2: [0; 0x14],
243            sha1_hmac_arm9: [0; 0x14],
244            reserved6: [0; 0xa4c],
245            debug_args: [0; 0x180],
246            rsa_sha1: [0; 0x80],
247            reserved7: [0; 0x3000],
248        };
249
250        if let Some(ds_post_dsi) = &self.ds_post_dsi {
251            header.dsi_flags_2 = ds_post_dsi.dsi_flags_2;
252            header.sha1_hmac_banner = ds_post_dsi.sha1_hmac_banner;
253            header.sha1_hmac_unk1 = ds_post_dsi.sha1_hmac_unk1;
254            header.sha1_hmac_unk2 = ds_post_dsi.sha1_hmac_unk2;
255            header.rsa_sha1.copy_from_slice(&ds_post_dsi.rsa_sha1);
256        }
257
258        header.header_crc = CRC_16_MODBUS.checksum(&bytemuck::bytes_of(&header)[0..offset_of!(raw::Header, header_crc)]);
259        Ok(header)
260    }
261
262    /// Returns the version of this [`Header`].
263    pub fn version(&self) -> HeaderVersion {
264        if self.ds_post_dsi.is_some() {
265            HeaderVersion::DsPostDsi
266        } else {
267            HeaderVersion::Original
268        }
269    }
270}