ds_rom/rom/
rom.rs

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