Skip to main content

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