ds_rom/rom/
rom.rs

1use std::{
2    backtrace::Backtrace,
3    io::{self, Cursor, Write},
4    mem::size_of,
5    path::Path,
6};
7
8use serde::{Deserialize, Serialize};
9use snafu::Snafu;
10
11use super::{
12    raw::{
13        self, Arm9Footer, HmacSha1Signature, RawArm9Error, RawBannerError, RawBuildInfoError, RawFatError, RawFntError,
14        RawHeaderError, RawOverlayError, RomAlignmentsError, TableOffset,
15    },
16    Arm7, Arm9, Arm9AutoloadError, Arm9Error, Arm9HmacSha1KeyError, Arm9Offsets, Arm9OverlaySignaturesError, Autoload, Banner,
17    BannerError, BannerImageError, BuildInfo, FileBuildError, FileParseError, FileSystem, Header, HeaderBuildError, Logo,
18    LogoError, LogoLoadError, LogoSaveError, Overlay, OverlayError, OverlayInfo, OverlayOptions, OverlayTable,
19    RomConfigAutoload, RomConfigUnknownAutoload,
20};
21use crate::{
22    compress::lz77::Lz77DecompressError,
23    crypto::{
24        blowfish::BlowfishKey,
25        hmac_sha1::{HmacSha1, HmacSha1FromBytesError},
26    },
27    io::{create_dir_all, create_file, create_file_and_dirs, open_file, read_file, read_to_string, FileError},
28    rom::{raw::FileAlloc, Arm9WithTcmsOptions, RomConfig},
29};
30
31/// A plain ROM.
32pub struct Rom<'a> {
33    header: Header,
34    header_logo: Logo,
35    arm9: Arm9<'a>,
36    arm9_overlay_table: OverlayTable<'a>,
37    arm7: Arm7<'a>,
38    arm7_overlay_table: OverlayTable<'a>,
39    banner: Banner,
40    files: FileSystem<'a>,
41    path_order: Vec<String>,
42    config: RomConfig,
43}
44
45/// Errors related to [`Rom::extract`].
46#[derive(Debug, Snafu)]
47pub enum RomExtractError {
48    /// See [`RawHeaderError`].
49    #[snafu(transparent)]
50    RawHeader {
51        /// Source error.
52        source: RawHeaderError,
53    },
54    /// See [`LogoError`].
55    #[snafu(transparent)]
56    Logo {
57        /// Source error.
58        source: LogoError,
59    },
60    /// See [`RawOverlayError`].
61    #[snafu(transparent)]
62    RawOverlay {
63        /// Source error.
64        source: RawOverlayError,
65    },
66    /// See [`RawFntError`].
67    #[snafu(transparent)]
68    RawFnt {
69        /// Source error.
70        source: RawFntError,
71    },
72    /// See [`RawFatError`].
73    #[snafu(transparent)]
74    RawFat {
75        /// Source error.
76        source: RawFatError,
77    },
78    /// See [`RawBannerError`].
79    #[snafu(transparent)]
80    RawBanner {
81        /// Source error.
82        source: RawBannerError,
83    },
84    /// See [`FileParseError`].
85    #[snafu(transparent)]
86    FileParse {
87        /// Source error.
88        source: FileParseError,
89    },
90    /// See [`RawArm9Error`].
91    #[snafu(transparent)]
92    RawArm9 {
93        /// Source error.
94        source: RawArm9Error,
95    },
96    /// See [`Arm9AutoloadError`]
97    #[snafu(transparent)]
98    Arm9Autoload {
99        /// Source error.
100        source: Arm9AutoloadError,
101    },
102    /// See [`RawBuildInfoError`].
103    #[snafu(transparent)]
104    RawBuildInfo {
105        /// Source error.
106        source: RawBuildInfoError,
107    },
108    /// See [`Arm9Error`].
109    #[snafu(transparent)]
110    Arm9 {
111        /// Source error.
112        source: Arm9Error,
113    },
114    /// See [`RomAlignmentsError`].
115    #[snafu(transparent)]
116    RomAlignments {
117        /// Source error.
118        source: RomAlignmentsError,
119    },
120    /// See [`OverlayError`].
121    #[snafu(transparent)]
122    Overlay {
123        /// Source error.
124        source: OverlayError,
125    },
126    /// See [`Arm9HmacSha1KeyError`].
127    #[snafu(transparent)]
128    Arm9HmacSha1Key {
129        /// Source error.
130        source: Arm9HmacSha1KeyError,
131    },
132}
133
134/// Errors related to [`Rom::build`].
135#[derive(Snafu, Debug)]
136pub enum RomBuildError {
137    /// See [`io::Error`].
138    #[snafu(transparent)]
139    Io {
140        /// Source error.
141        source: io::Error,
142    },
143    /// See [`FileBuildError`].
144    #[snafu(transparent)]
145    FileBuild {
146        /// Source error.
147        source: FileBuildError,
148    },
149    /// See [`BannerError`].
150    #[snafu(transparent)]
151    Banner {
152        /// Source error.
153        source: BannerError,
154    },
155    /// See [`HeaderBuildError`].
156    #[snafu(transparent)]
157    HeaderBuild {
158        /// Source error.
159        source: HeaderBuildError,
160    },
161}
162
163/// Errors related to [`Rom::save`] and [`Rom::load`].
164#[derive(Snafu, Debug)]
165pub enum RomSaveError {
166    /// Occurs when the ROM is encrypted but no Blowfish key was provided.
167    #[snafu(display("blowfish key is required because ARM9 program is encrypted"))]
168    BlowfishKeyNeeded,
169    /// See [`io::Error`].
170    #[snafu(transparent)]
171    Io {
172        /// Source error.
173        source: io::Error,
174    },
175    /// See [`FileError`].
176    #[snafu(transparent)]
177    File {
178        /// Source error.
179        source: FileError,
180    },
181    /// See [`serde_yml::Error`].
182    #[snafu(transparent)]
183    SerdeJson {
184        /// Source error.
185        source: serde_yml::Error,
186    },
187    /// See [`LogoSaveError`].
188    #[snafu(transparent)]
189    LogoSave {
190        /// Source error.
191        source: LogoSaveError,
192    },
193    /// See [`LogoLoadError`].
194    #[snafu(transparent)]
195    LogoLoad {
196        /// Source error.
197        source: LogoLoadError,
198    },
199    /// See [`RawBuildInfoError`].
200    #[snafu(transparent)]
201    RawBuildInfo {
202        /// Source error.
203        source: RawBuildInfoError,
204    },
205    /// See [`Arm9Error`].
206    #[snafu(transparent)]
207    Arm9 {
208        /// Source error.
209        source: Arm9Error,
210    },
211    /// See [`Arm9AutoloadError`].
212    #[snafu(transparent)]
213    Arm9Autoload {
214        /// Source error.
215        source: Arm9AutoloadError,
216    },
217    /// See [`BannerImageError`].
218    #[snafu(transparent)]
219    BannerImage {
220        /// Source error.
221        source: BannerImageError,
222    },
223    /// See [`Lz77DecompressError`].
224    #[snafu(transparent)]
225    Lz77Decompress {
226        /// Source error.
227        source: Lz77DecompressError,
228    },
229    /// See [`OverlayError`].
230    #[snafu(transparent)]
231    Overlay {
232        /// Source error.
233        source: OverlayError,
234    },
235    /// See [`Arm9OverlaySignaturesError`].
236    #[snafu(transparent)]
237    HmacSha1FromBytes {
238        /// Source error.
239        source: HmacSha1FromBytesError,
240    },
241    /// See [`Arm9HmacSha1KeyError`].
242    #[snafu(transparent)]
243    Arm9HmacSha1Key {
244        /// Source error.
245        source: Arm9HmacSha1KeyError,
246    },
247    /// See [`Arm9OverlaySignaturesError`].
248    #[snafu(transparent)]
249    Arm9OverlaySignatures {
250        /// Source error.
251        source: Arm9OverlaySignaturesError,
252    },
253    /// Occurs when the HMAC-SHA1 key was not provided for a signed overlay.
254    #[snafu(display("HMAC-SHA1 key was not provided for a signed overlay:\n{backtrace}"))]
255    NoHmacSha1Key {
256        /// Backtrace to the source of the error.
257        backtrace: Backtrace,
258    },
259    /// Occurs when an autoload was not found in the config.
260    #[snafu(display("autoload index {index} not found in config:\n{backtrace}"))]
261    AutoloadNotFound {
262        /// The index of the autoload that was missing.
263        index: u32,
264        /// Backtrace to the source of the error.
265        backtrace: Backtrace,
266    },
267}
268
269/// Config file for the ARM9 main module.
270#[derive(Serialize, Deserialize)]
271pub struct Arm9BuildConfig {
272    /// Various offsets within the ARM9 module.
273    #[serde(flatten)]
274    pub offsets: Arm9Offsets,
275    /// Whether this module is encrypted in the ROM.
276    pub encrypted: bool,
277    /// Whether this module is compressed in the ROM.
278    pub compressed: bool,
279    /// Build info for this module.
280    #[serde(flatten)]
281    pub build_info: BuildInfo,
282}
283
284/// Overlay configuration, extending [`OverlayInfo`] with more fields.
285#[derive(Serialize, Deserialize)]
286pub struct OverlayConfig {
287    /// See [`OverlayInfo`].
288    #[serde(flatten)]
289    pub info: OverlayInfo,
290    /// Whether this overlay is signed.
291    pub signed: bool,
292    /// Name of binary file.
293    pub file_name: String,
294}
295
296/// Configuration for the overlay table, used for both ARM9 and ARM7 overlays.
297#[derive(Serialize, Deserialize)]
298pub struct OverlayTableConfig {
299    /// Whether the overlay table has an HMAC-SHA1 signature.
300    pub table_signed: bool,
301    /// Overlay table HMAC-SHA1 signature. NOTE: This field is temporary! A bug in the DS standard library causes this
302    /// signature to be computed incorrectly, and we haven't replicated this bug in our code yet.
303    #[serde(skip_serializing_if = "Option::is_none")]
304    pub table_signature: Option<HmacSha1Signature>,
305    /// List of overlays.
306    pub overlays: Vec<OverlayConfig>,
307}
308
309impl<'a> Rom<'a> {
310    /// Loads a ROM from a path generated by [`Self::save`].
311    ///
312    /// # Errors
313    ///
314    /// This function will return an error if there's a file missing or the file has an invalid format.
315    pub fn load<P: AsRef<Path>>(config_path: P, options: RomLoadOptions) -> Result<Self, RomSaveError> {
316        let config_path = config_path.as_ref();
317        log::info!("Loading ROM from {}", config_path.display());
318
319        let config: RomConfig = serde_yml::from_reader(open_file(config_path)?)?;
320        let path = config_path.parent().unwrap();
321
322        // --------------------- Load header ---------------------
323        let (header, header_logo) = if options.load_header {
324            let header: Header = serde_yml::from_reader(open_file(path.join(&config.header))?)?;
325            let header_logo = Logo::from_png(path.join(&config.header_logo))?;
326            (header, header_logo)
327        } else {
328            Default::default()
329        };
330
331        // --------------------- Load ARM9 program ---------------------
332        let arm9_build_config: Arm9BuildConfig = serde_yml::from_reader(open_file(path.join(&config.arm9_config))?)?;
333        let arm9 = read_file(path.join(&config.arm9_bin))?;
334
335        // --------------------- Load autoloads ---------------------
336        let mut autoloads = vec![];
337
338        let itcm = read_file(path.join(&config.itcm.bin))?;
339        let itcm_info = serde_yml::from_reader(open_file(path.join(&config.itcm.config))?)?;
340        let itcm = Autoload::new(itcm, itcm_info);
341        autoloads.push(itcm);
342
343        let dtcm = read_file(path.join(&config.dtcm.bin))?;
344        let dtcm_info = serde_yml::from_reader(open_file(path.join(&config.dtcm.config))?)?;
345        let dtcm = Autoload::new(dtcm, dtcm_info);
346        autoloads.push(dtcm);
347
348        for unknown_autoload in &config.unknown_autoloads {
349            let autoload = read_file(path.join(&unknown_autoload.files.bin))?;
350            let autoload_info = serde_yml::from_reader(open_file(path.join(&unknown_autoload.files.config))?)?;
351            let autoload = Autoload::new(autoload, autoload_info);
352            autoloads.push(autoload);
353        }
354
355        autoloads.sort_by_key(|autoload| autoload.kind());
356
357        // --------------------- Load HMAC SHA1 key ---------------------
358        let arm9_hmac_sha1 = if let Some(hmac_sha1_key_file) = &config.arm9_hmac_sha1_key {
359            let hmac_sha1_key = read_file(path.join(hmac_sha1_key_file))?;
360            Some(HmacSha1::try_from(hmac_sha1_key.as_ref())?)
361        } else {
362            None
363        };
364
365        // --------------------- Load ARM9 overlays ---------------------
366        let arm9_overlays = if let Some(arm9_overlays_config) = &config.arm9_overlays {
367            Self::load_overlays(&path.join(arm9_overlays_config), "arm9", arm9_hmac_sha1, &options)?
368        } else {
369            Default::default()
370        };
371
372        // --------------------- Build ARM9 program ---------------------
373        let mut arm9 = Arm9::with_autoloads(
374            arm9,
375            &autoloads,
376            arm9_build_config.offsets,
377            Arm9WithTcmsOptions {
378                originally_compressed: arm9_build_config.compressed,
379                originally_encrypted: arm9_build_config.encrypted,
380            },
381        )?;
382        arm9_build_config.build_info.assign_to_raw(arm9.build_info_mut()?);
383        arm9.update_overlay_signatures(&arm9_overlays)?;
384        if arm9_build_config.compressed && options.compress {
385            log::info!("Compressing ARM9 program");
386            arm9.compress()?;
387        }
388        if arm9_build_config.encrypted && options.encrypt {
389            let Some(key) = options.key else {
390                return BlowfishKeyNeededSnafu {}.fail();
391            };
392            log::info!("Encrypting ARM9 program");
393            arm9.encrypt(key, header.original.gamecode.to_le_u32())?;
394        }
395
396        // --------------------- Load ARM7 overlays ---------------------
397        let arm7_overlays = if let Some(arm7_overlays_config) = &config.arm7_overlays {
398            Self::load_overlays(&path.join(arm7_overlays_config), "arm7", None, &options)?
399        } else {
400            Default::default()
401        };
402
403        // --------------------- Load ARM7 program ---------------------
404        let arm7 = read_file(path.join(&config.arm7_bin))?;
405        let arm7_config = serde_yml::from_reader(open_file(path.join(&config.arm7_config))?)?;
406        let arm7 = Arm7::new(arm7, arm7_config);
407
408        // --------------------- Load banner ---------------------
409        let banner = if options.load_banner {
410            let banner_path = path.join(&config.banner);
411            let banner_dir = banner_path.parent().unwrap();
412            let mut banner: Banner = serde_yml::from_reader(open_file(&banner_path)?)?;
413            banner.images.load(banner_dir)?;
414            banner
415        } else {
416            Default::default()
417        };
418
419        // --------------------- Load files ---------------------
420        let num_overlays = arm9_overlays.overlays().len() + arm7_overlays.overlays().len();
421        let (files, path_order) = if options.load_files {
422            log::info!("Loading ROM assets");
423            let files = FileSystem::load(path.join(&config.files_dir), num_overlays)?;
424            let path_order =
425                read_to_string(path.join(&config.path_order))?.trim().lines().map(|l| l.to_string()).collect::<Vec<_>>();
426            (files, path_order)
427        } else {
428            (FileSystem::new(num_overlays), vec![])
429        };
430
431        Ok(Self {
432            header,
433            header_logo,
434            arm9,
435            arm9_overlay_table: arm9_overlays,
436            arm7,
437            arm7_overlay_table: arm7_overlays,
438            banner,
439            files,
440            path_order,
441            config,
442        })
443    }
444
445    fn load_overlays(
446        config_path: &Path,
447        processor: &str,
448        hmac_sha1: Option<HmacSha1>,
449        options: &RomLoadOptions,
450    ) -> Result<OverlayTable<'a>, RomSaveError> {
451        let path = config_path.parent().unwrap();
452        let mut overlays = vec![];
453        let overlay_table_config: OverlayTableConfig = serde_yml::from_reader(open_file(config_path)?)?;
454        let num_overlays = overlay_table_config.overlays.len();
455        for mut config in overlay_table_config.overlays.into_iter() {
456            let data = read_file(path.join(config.file_name))?;
457            let compressed = config.info.compressed;
458            config.info.compressed = false;
459            let mut overlay = Overlay::new(data, OverlayOptions { info: config.info, originally_compressed: compressed })?;
460
461            if options.compress {
462                if compressed {
463                    log::info!("Compressing {processor} overlay {}/{}", overlay.id(), num_overlays - 1);
464                    overlay.compress()?;
465                }
466
467                if config.signed {
468                    let Some(ref hmac_sha1) = hmac_sha1 else {
469                        return NoHmacSha1KeySnafu {}.fail();
470                    };
471                    overlay.sign(hmac_sha1)?;
472                }
473            }
474
475            overlays.push(overlay);
476        }
477
478        let mut overlay_table = OverlayTable::new(overlays);
479        if overlay_table_config.table_signed {
480            let Some(ref hmac_sha1) = hmac_sha1 else {
481                return NoHmacSha1KeySnafu {}.fail();
482            };
483            if let Some(signature) = overlay_table_config.table_signature {
484                overlay_table.set_signature(signature);
485            } else {
486                overlay_table.sign(hmac_sha1);
487            }
488        }
489
490        Ok(overlay_table)
491    }
492
493    /// Saves this ROM to a path as separate files.
494    ///
495    /// # Errors
496    ///
497    /// This function will return an error if a file could not be created or the a component of the ROM has an invalid format.
498    pub fn save<P: AsRef<Path>>(&self, path: P, key: Option<&BlowfishKey>) -> Result<(), RomSaveError> {
499        let path = path.as_ref();
500        create_dir_all(path)?;
501
502        log::info!("Saving ROM to directory {}", path.display());
503
504        // --------------------- Save config ---------------------
505        serde_yml::to_writer(create_file_and_dirs(path.join("config.yaml"))?, &self.config)?;
506
507        // --------------------- Save header ---------------------
508        serde_yml::to_writer(create_file_and_dirs(path.join(&self.config.header))?, &self.header)?;
509        self.header_logo.save_png(path.join(&self.config.header_logo))?;
510
511        // --------------------- Save ARM9 program ---------------------
512        let arm9_build_config = self.arm9_build_config()?;
513        serde_yml::to_writer(create_file_and_dirs(path.join(&self.config.arm9_config))?, &arm9_build_config)?;
514        let mut plain_arm9 = self.arm9.clone();
515        if plain_arm9.is_encrypted() {
516            let Some(key) = key else {
517                return BlowfishKeyNeededSnafu {}.fail();
518            };
519            log::info!("Decrypting ARM9 program");
520            plain_arm9.decrypt(key, self.header.original.gamecode.to_le_u32())?;
521        }
522        if plain_arm9.is_compressed()? {
523            log::info!("Decompressing ARM9 program");
524            plain_arm9.decompress()?;
525        }
526        create_file_and_dirs(path.join(&self.config.arm9_bin))?.write_all(plain_arm9.code()?)?;
527
528        // --------------------- Save ARM9 HMAC-SHA1 key ---------------------
529        if let Some(arm9_hmac_sha1_key) = plain_arm9.hmac_sha1_key()? {
530            if let Some(key_file) = &self.config.arm9_hmac_sha1_key {
531                create_file_and_dirs(path.join(key_file))?.write_all(arm9_hmac_sha1_key.as_ref())?;
532            }
533        } else if self.config.arm9_hmac_sha1_key.is_some() {
534            log::warn!("ARM9 HMAC-SHA1 key not found, but config requested it to be saved");
535        }
536
537        // --------------------- Save autoloads ---------------------
538        for autoload in plain_arm9.autoloads()?.iter() {
539            let (bin_path, config_path) = match autoload.kind() {
540                raw::AutoloadKind::Itcm => (path.join(&self.config.itcm.bin), path.join(&self.config.itcm.config)),
541                raw::AutoloadKind::Dtcm => (path.join(&self.config.dtcm.bin), path.join(&self.config.dtcm.config)),
542                raw::AutoloadKind::Unknown(index) => {
543                    let unknown_autoload = self
544                        .config
545                        .unknown_autoloads
546                        .iter()
547                        .find(|autoload| autoload.index == index)
548                        .ok_or_else(|| AutoloadNotFoundSnafu { index }.build())?;
549                    (path.join(&unknown_autoload.files.bin), path.join(&unknown_autoload.files.config))
550                }
551            };
552            create_file_and_dirs(bin_path)?.write_all(autoload.code())?;
553            serde_yml::to_writer(create_file_and_dirs(config_path)?, autoload.info())?;
554        }
555
556        // --------------------- Save ARM9 overlays ---------------------
557        if let Some(arm9_overlays_config) = &self.config.arm9_overlays {
558            Self::save_overlays(&path.join(arm9_overlays_config), &self.arm9_overlay_table, "arm9")?;
559        }
560
561        // --------------------- Save ARM7 program ---------------------
562        create_file_and_dirs(path.join(&self.config.arm7_bin))?.write_all(self.arm7.full_data())?;
563        serde_yml::to_writer(create_file_and_dirs(path.join(&self.config.arm7_config))?, self.arm7.offsets())?;
564
565        // --------------------- Save ARM7 overlays ---------------------
566        if let Some(arm7_overlays_config) = &self.config.arm7_overlays {
567            Self::save_overlays(&path.join(arm7_overlays_config), &self.arm7_overlay_table, "arm7")?;
568        }
569
570        // --------------------- Save banner ---------------------
571        {
572            let banner_path = path.join(&self.config.banner);
573            let banner_dir = banner_path.parent().unwrap();
574            serde_yml::to_writer(create_file_and_dirs(&banner_path)?, &self.banner)?;
575            self.banner.images.save_bitmap_file(banner_dir)?;
576        }
577
578        // --------------------- Save files ---------------------
579        {
580            log::info!("Saving ROM assets");
581            let files_path = path.join(&self.config.files_dir);
582            self.files.traverse_files(["/"], |file, path| {
583                let path = files_path.join(path);
584                // TODO: Rewrite traverse_files as an iterator so these errors can be returned
585                create_dir_all(&path).expect("failed to create file directory");
586                create_file(path.join(file.name()))
587                    .expect("failed to create file")
588                    .write_all(file.contents())
589                    .expect("failed to write file");
590            });
591        }
592        let mut path_order_file = create_file_and_dirs(path.join(&self.config.path_order))?;
593        for path in &self.path_order {
594            path_order_file.write_all(path.as_bytes())?;
595            path_order_file.write_all("\n".as_bytes())?;
596        }
597
598        Ok(())
599    }
600
601    /// Generates a build config for ARM9, which normally goes into arm9.yaml.
602    pub fn arm9_build_config(&self) -> Result<Arm9BuildConfig, RomSaveError> {
603        Ok(Arm9BuildConfig {
604            offsets: *self.arm9.offsets(),
605            encrypted: self.arm9.is_encrypted(),
606            compressed: self.arm9.is_compressed()?,
607            build_info: (*self.arm9.build_info()?).into(),
608        })
609    }
610
611    fn save_overlays(config_path: &Path, overlay_table: &OverlayTable, processor: &str) -> Result<(), RomSaveError> {
612        let overlays = overlay_table.overlays();
613        if !overlays.is_empty() {
614            let overlays_path = config_path.parent().unwrap();
615            create_dir_all(overlays_path)?;
616
617            let mut configs = vec![];
618            for overlay in overlays {
619                let name = format!("ov{:03}", overlay.id());
620
621                let mut plain_overlay = overlay.clone();
622                configs.push(OverlayConfig {
623                    info: plain_overlay.info().clone(),
624                    file_name: format!("{name}.bin"),
625                    signed: overlay.is_signed(),
626                });
627
628                if plain_overlay.is_compressed() {
629                    log::info!("Decompressing {processor} overlay {}/{}", overlay.id(), overlays.len() - 1);
630                    plain_overlay.decompress()?;
631                }
632                create_file(overlays_path.join(format!("{name}.bin")))?.write_all(plain_overlay.code())?;
633            }
634
635            let overlay_table_config = OverlayTableConfig {
636                table_signed: overlay_table.is_signed(),
637                table_signature: overlay_table.signature(),
638                overlays: configs,
639            };
640            serde_yml::to_writer(create_file(config_path)?, &overlay_table_config)?;
641        }
642        Ok(())
643    }
644
645    /// Extracts from a raw ROM.
646    ///
647    /// # Errors
648    ///
649    /// This function will return an error if a component is missing from the raw ROM.
650    pub fn extract(rom: &'a raw::Rom) -> Result<Self, RomExtractError> {
651        let header = rom.header()?;
652        let fnt = rom.fnt()?;
653        let fat = rom.fat()?;
654        let banner = rom.banner()?;
655        let file_root = FileSystem::parse(&fnt, fat, rom)?;
656        let path_order = file_root.compute_path_order();
657
658        let arm9 = rom.arm9()?;
659        let mut decompressed_arm9 = arm9.clone();
660        decompressed_arm9.decompress()?;
661
662        let arm9_overlays = rom.arm9_overlay_table_with(&decompressed_arm9)?;
663        let arm9_overlays = OverlayTable::parse_arm9(arm9_overlays, rom, &decompressed_arm9)?;
664        let arm7_overlays = rom.arm7_overlay_table()?;
665        let arm7_overlays = OverlayTable::parse_arm7(arm7_overlays, rom)?;
666
667        let autoloads = decompressed_arm9.autoloads()?;
668        let unknown_autoloads = autoloads
669            .iter()
670            .filter_map(|autoload| {
671                let raw::AutoloadKind::Unknown(index) = autoload.kind() else {
672                    return None;
673                };
674                Some(RomConfigUnknownAutoload {
675                    index,
676                    files: RomConfigAutoload {
677                        bin: format!("arm9/unk_autoload_{index}.bin").into(),
678                        config: format!("arm9/unk_autoload_{index}.yaml").into(),
679                    },
680                })
681            })
682            .collect();
683
684        let has_arm9_hmac_sha1 = decompressed_arm9.hmac_sha1_key()?.is_some();
685
686        let alignment = rom.alignments()?;
687
688        let config = RomConfig {
689            padding_value: rom.padding_value()?,
690            header: "header.yaml".into(),
691            header_logo: "header_logo.png".into(),
692            arm9_bin: "arm9/arm9.bin".into(),
693            arm9_config: "arm9/arm9.yaml".into(),
694            arm7_bin: "arm7/arm7.bin".into(),
695            arm7_config: "arm7/arm7.yaml".into(),
696            itcm: RomConfigAutoload { bin: "arm9/itcm.bin".into(), config: "arm9/itcm.yaml".into() },
697            unknown_autoloads,
698            dtcm: RomConfigAutoload { bin: "arm9/dtcm.bin".into(), config: "arm9/dtcm.yaml".into() },
699            arm9_overlays: if arm9_overlays.is_empty() { None } else { Some("arm9_overlays/overlays.yaml".into()) },
700            arm7_overlays: if arm7_overlays.is_empty() { None } else { Some("arm7_overlays/overlays.yaml".into()) },
701            banner: "banner/banner.yaml".into(),
702            files_dir: "files/".into(),
703            path_order: "path_order.txt".into(),
704            arm9_hmac_sha1_key: has_arm9_hmac_sha1.then_some("arm9/hmac_sha1_key.bin".into()),
705            alignment,
706        };
707
708        Ok(Self {
709            header: Header::load_raw(header),
710            header_logo: Logo::decompress(&header.logo)?,
711            arm9,
712            arm9_overlay_table: arm9_overlays,
713            arm7: rom.arm7()?,
714            arm7_overlay_table: arm7_overlays,
715            banner: Banner::load_raw(&banner),
716            files: file_root,
717            path_order,
718            config,
719        })
720    }
721
722    /// Builds a raw ROM.
723    ///
724    /// # Errors
725    ///
726    /// This function will return an error if an I/O operation fails or a component fails to build.
727    pub fn build(mut self, key: Option<&BlowfishKey>) -> Result<raw::Rom<'a>, RomBuildError> {
728        let mut context = BuildContext { blowfish_key: key, ..Default::default() };
729
730        let mut cursor = Cursor::new(Vec::with_capacity(128 * 1024)); // smallest possible ROM
731
732        // --------------------- Write header placeholder ---------------------
733        context.header_offset = Some(cursor.position() as u32);
734        cursor.write_all(&[0u8; size_of::<raw::Header>()])?;
735
736        // --------------------- Write ARM9 program ---------------------
737        self.align(&mut cursor, self.config.alignment.arm9)?;
738        context.arm9_offset = Some(cursor.position() as u32);
739        context.arm9_autoload_callback = Some(self.arm9.autoload_callback());
740        context.arm9_build_info_offset = Some(self.arm9.build_info_offset());
741        cursor.write_all(self.arm9.full_data())?;
742        let footer = Arm9Footer::new(self.arm9.build_info_offset(), self.arm9.overlay_signatures_offset());
743        cursor.write_all(bytemuck::bytes_of(&footer))?;
744
745        let max_file_id = self.files.max_file_id();
746        let mut file_allocs = vec![FileAlloc::default(); max_file_id as usize + 1];
747
748        if !self.arm9_overlay_table.is_empty() {
749            // --------------------- Write ARM9 overlay table ---------------------
750            self.align(&mut cursor, self.config.alignment.arm9_overlay_table)?;
751            context.arm9_ovt_offset = Some(TableOffset {
752                offset: cursor.position() as u32,
753                size: (self.arm9_overlay_table.len() * size_of::<raw::Overlay>()) as u32,
754            });
755            let raw_table = self.arm9_overlay_table.build();
756            cursor.write_all(raw_table.as_bytes())?;
757
758            // --------------------- Write ARM9 overlays ---------------------
759            for overlay in self.arm9_overlay_table.overlays() {
760                self.align(&mut cursor, self.config.alignment.arm9_overlay)?;
761                let start = cursor.position() as u32;
762                let end = start + overlay.full_data().len() as u32;
763                file_allocs[overlay.file_id() as usize] = FileAlloc { start, end };
764
765                cursor.write_all(overlay.full_data())?;
766            }
767        }
768
769        // --------------------- Write ARM7 program ---------------------
770        self.align(&mut cursor, self.config.alignment.arm7)?;
771        context.arm7_offset = Some(cursor.position() as u32);
772        context.arm7_autoload_callback = Some(self.arm7.autoload_callback());
773        context.arm7_build_info_offset = None;
774        cursor.write_all(self.arm7.full_data())?;
775
776        if !self.arm7_overlay_table.is_empty() {
777            // --------------------- Write ARM7 overlay table ---------------------
778            self.align(&mut cursor, self.config.alignment.arm7_overlay_table)?;
779            context.arm7_ovt_offset = Some(TableOffset {
780                offset: cursor.position() as u32,
781                size: (self.arm7_overlay_table.len() * size_of::<raw::Overlay>()) as u32,
782            });
783            let raw_table = self.arm7_overlay_table.build();
784            cursor.write_all(raw_table.as_bytes())?;
785
786            // --------------------- Write ARM7 overlays ---------------------
787            for overlay in self.arm7_overlay_table.overlays() {
788                self.align(&mut cursor, self.config.alignment.arm7_overlay)?;
789                let start = cursor.position() as u32;
790                let end = start + overlay.full_data().len() as u32;
791                file_allocs[overlay.file_id() as usize] = FileAlloc { start, end };
792
793                cursor.write_all(overlay.full_data())?;
794            }
795        }
796
797        // --------------------- Write file name table (FNT) ---------------------
798        self.align(&mut cursor, self.config.alignment.file_name_table)?;
799        self.files.sort_for_fnt();
800        let fnt = self.files.build_fnt()?.build()?;
801        context.fnt_offset = Some(TableOffset { offset: cursor.position() as u32, size: fnt.len() as u32 });
802        cursor.write_all(&fnt)?;
803
804        // --------------------- Write file allocation table (FAT) placeholder ---------------------
805        self.align(&mut cursor, self.config.alignment.file_allocation_table)?;
806        context.fat_offset =
807            Some(TableOffset { offset: cursor.position() as u32, size: (file_allocs.len() * size_of::<FileAlloc>()) as u32 });
808        cursor.write_all(bytemuck::cast_slice(&file_allocs))?;
809
810        // --------------------- Write banner ---------------------
811        self.align(&mut cursor, self.config.alignment.banner)?;
812        let banner = self.banner.build()?;
813        context.banner_offset = Some(TableOffset { offset: cursor.position() as u32, size: banner.full_data().len() as u32 });
814        cursor.write_all(banner.full_data())?;
815
816        // --------------------- Write files ---------------------
817        self.align(&mut cursor, self.config.alignment.file_image_block)?;
818        self.files.sort_for_rom();
819        self.files.traverse_files(self.path_order.iter().map(|s| s.as_str()), |file, _| {
820            // TODO: Rewrite traverse_files as an iterator so these errors can be returned
821            self.align(&mut cursor, self.config.alignment.file).expect("failed to align after file");
822
823            let contents = file.contents();
824            let start = cursor.position() as u32;
825            let end = start + contents.len() as u32;
826            file_allocs[file.id() as usize] = FileAlloc { start, end };
827
828            cursor.write_all(contents).expect("failed to write file contents");
829        });
830
831        // --------------------- Write padding ---------------------
832        context.rom_size = Some(cursor.position() as u32);
833        let padded_rom_size = cursor.position().next_power_of_two().max(128 * 1024) as u32;
834        self.align(&mut cursor, padded_rom_size)?;
835
836        // --------------------- Update FAT ---------------------
837        cursor.set_position(context.fat_offset.unwrap().offset as u64);
838        cursor.write_all(bytemuck::cast_slice(&file_allocs))?;
839
840        // --------------------- Update header ---------------------
841        cursor.set_position(context.header_offset.unwrap() as u64);
842        let header = self.header.build(&context, &self)?;
843        cursor.write_all(bytemuck::bytes_of(&header))?;
844
845        Ok(raw::Rom::new(cursor.into_inner()))
846    }
847
848    fn align(&self, cursor: &mut Cursor<Vec<u8>>, alignment: u32) -> Result<(), RomBuildError> {
849        assert!(alignment.is_power_of_two(), "alignment must be a power of two");
850        let mask = alignment - 1;
851        let padding = (!cursor.position() as u32 + 1) & mask;
852        for _ in 0..padding {
853            cursor.write_all(&[self.config.padding_value])?;
854        }
855        Ok(())
856    }
857
858    /// Returns a reference to the header logo of this [`Rom`].
859    pub fn header_logo(&self) -> &Logo {
860        &self.header_logo
861    }
862
863    /// Returns a reference to the ARM9 program of this [`Rom`].
864    pub fn arm9(&self) -> &Arm9 {
865        &self.arm9
866    }
867
868    /// Returns a reference to the ARM9 overlay table of this [`Rom`].
869    pub fn arm9_overlay_table(&self) -> &OverlayTable {
870        &self.arm9_overlay_table
871    }
872
873    /// Returns a reference to the ARM9 overlays of this [`Rom`].
874    pub fn arm9_overlays(&self) -> &[Overlay] {
875        self.arm9_overlay_table.overlays()
876    }
877
878    /// Returns a reference to the ARM7 program of this [`Rom`].
879    pub fn arm7(&self) -> &Arm7 {
880        &self.arm7
881    }
882
883    /// Returns a reference to the ARM7 overlay table of this [`Rom`].
884    pub fn arm7_overlay_table(&self) -> &OverlayTable {
885        &self.arm7_overlay_table
886    }
887
888    /// Returns a reference to the ARM7 overlays of this [`Rom`].
889    pub fn arm7_overlays(&self) -> &[Overlay] {
890        self.arm7_overlay_table.overlays()
891    }
892
893    /// Returns a reference to the header of this [`Rom`].
894    pub fn header(&self) -> &Header {
895        &self.header
896    }
897
898    /// Returns the [`RomConfig`] consisting of paths to extracted files.
899    pub fn config(&self) -> &RomConfig {
900        &self.config
901    }
902}
903
904/// Build context, generated during [`Rom::build`] and later passed to [`Header::build`] to fill in the header.
905#[derive(Default)]
906pub struct BuildContext<'a> {
907    /// Header offset.
908    pub header_offset: Option<u32>,
909    /// ARM9 program offset.
910    pub arm9_offset: Option<u32>,
911    /// ARM7 program offset.
912    pub arm7_offset: Option<u32>,
913    /// FNT offset.
914    pub fnt_offset: Option<TableOffset>,
915    /// FAT offset.
916    pub fat_offset: Option<TableOffset>,
917    /// ARM9 overlay table offset.
918    pub arm9_ovt_offset: Option<TableOffset>,
919    /// ARM7 overlay table offset.
920    pub arm7_ovt_offset: Option<TableOffset>,
921    /// Banner offset.
922    pub banner_offset: Option<TableOffset>,
923    /// Blowfish key.
924    pub blowfish_key: Option<&'a BlowfishKey>,
925    /// ARM9 autoload callback.
926    pub arm9_autoload_callback: Option<u32>,
927    /// ARM7 autoload callback.
928    pub arm7_autoload_callback: Option<u32>,
929    /// ARM9 build info offset.
930    pub arm9_build_info_offset: Option<u32>,
931    /// ARM7 build info offset.
932    pub arm7_build_info_offset: Option<u32>,
933    /// Total ROM size.
934    pub rom_size: Option<u32>,
935}
936
937/// Options for [`Rom::load`].
938pub struct RomLoadOptions<'a> {
939    /// Blowfish encryption key.
940    pub key: Option<&'a BlowfishKey>,
941    /// If true (default), compress ARM9 and overlays if they are configured with `compressed: true`.
942    pub compress: bool,
943    /// If true (default), encrypt ARM9 if it's configured with `encrypted: true`.
944    pub encrypt: bool,
945    /// If true (default), load asset files.
946    pub load_files: bool,
947    /// If true (default), load header and header logo.
948    pub load_header: bool,
949    /// If true (default), load banner.
950    pub load_banner: bool,
951}
952
953impl Default for RomLoadOptions<'_> {
954    fn default() -> Self {
955        Self { key: None, compress: true, encrypt: true, load_files: true, load_header: true, load_banner: true }
956    }
957}