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
26pub 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#[derive(Debug, Snafu)]
42pub enum RomExtractError {
43 #[snafu(transparent)]
45 RawHeader {
46 source: RawHeaderError,
48 },
49 #[snafu(transparent)]
51 Logo {
52 source: LogoError,
54 },
55 #[snafu(transparent)]
57 RawOverlay {
58 source: RawOverlayError,
60 },
61 #[snafu(transparent)]
63 RawFnt {
64 source: RawFntError,
66 },
67 #[snafu(transparent)]
69 RawFat {
70 source: RawFatError,
72 },
73 #[snafu(transparent)]
75 RawBanner {
76 source: RawBannerError,
78 },
79 #[snafu(transparent)]
81 FileParse {
82 source: FileParseError,
84 },
85 #[snafu(transparent)]
87 RawArm9 {
88 source: RawArm9Error,
90 },
91 #[snafu(transparent)]
93 Arm9Autoload {
94 source: Arm9AutoloadError,
96 },
97 #[snafu(transparent)]
99 RawBuildInfo {
100 source: RawBuildInfoError,
102 },
103 #[snafu(transparent)]
105 Arm9 {
106 source: Arm9Error,
108 },
109}
110
111#[derive(Snafu, Debug)]
113pub enum RomBuildError {
114 #[snafu(transparent)]
116 Io {
117 source: io::Error,
119 },
120 #[snafu(transparent)]
122 FileBuild {
123 source: FileBuildError,
125 },
126 #[snafu(transparent)]
128 Banner {
129 source: BannerError,
131 },
132 #[snafu(transparent)]
134 HeaderBuild {
135 source: HeaderBuildError,
137 },
138}
139
140#[derive(Snafu, Debug)]
142pub enum RomSaveError {
143 #[snafu(display("blowfish key is required because ARM9 program is encrypted"))]
145 BlowfishKeyNeeded,
146 #[snafu(transparent)]
148 Io {
149 source: io::Error,
151 },
152 #[snafu(transparent)]
154 File {
155 source: FileError,
157 },
158 #[snafu(transparent)]
160 SerdeJson {
161 source: serde_yml::Error,
163 },
164 #[snafu(transparent)]
166 LogoSave {
167 source: LogoSaveError,
169 },
170 #[snafu(transparent)]
172 LogoLoad {
173 source: LogoLoadError,
175 },
176 #[snafu(transparent)]
178 RawBuildInfo {
179 source: RawBuildInfoError,
181 },
182 #[snafu(transparent)]
184 Arm9 {
185 source: Arm9Error,
187 },
188 #[snafu(transparent)]
190 Arm9Autoload {
191 source: Arm9AutoloadError,
193 },
194 #[snafu(transparent)]
196 BannerImage {
197 source: BannerImageError,
199 },
200 #[snafu(transparent)]
202 Lz77Decompress {
203 source: Lz77DecompressError,
205 },
206}
207
208#[derive(Serialize, Deserialize)]
210pub struct Arm9BuildConfig {
211 #[serde(flatten)]
213 pub offsets: Arm9Offsets,
214 pub encrypted: bool,
216 pub compressed: bool,
218 #[serde(flatten)]
220 pub build_info: BuildInfo,
221}
222
223#[derive(Serialize, Deserialize)]
225pub struct OverlayConfig {
226 #[serde(flatten)]
228 pub info: OverlayInfo,
229 pub file_name: String,
231}
232
233impl<'a> Rom<'a> {
234 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 let header: Header = serde_yml::from_reader(open_file(path.join(&config.header))?)?;
248 let header_logo = Logo::from_png(path.join(&config.header_logo))?;
249
250 let arm9_build_config: Arm9BuildConfig = serde_yml::from_reader(open_file(path.join(&config.arm9_config))?)?;
252 let arm9 = read_file(path.join(&config.arm9_bin))?;
253
254 let mut autoloads = vec![];
256
257 let itcm = read_file(path.join(&config.itcm.bin))?;
258 let itcm_info = serde_yml::from_reader(open_file(path.join(&config.itcm.config))?)?;
259 let itcm = Autoload::new(itcm, itcm_info);
260 autoloads.push(itcm);
261
262 let dtcm = read_file(path.join(&config.dtcm.bin))?;
263 let dtcm_info = serde_yml::from_reader(open_file(path.join(&config.dtcm.config))?)?;
264 let dtcm = Autoload::new(dtcm, dtcm_info);
265 autoloads.push(dtcm);
266
267 for unknown_autoload in &config.unknown_autoloads {
268 let autoload = read_file(path.join(&unknown_autoload.bin))?;
269 let autoload_info = serde_yml::from_reader(open_file(path.join(&unknown_autoload.config))?)?;
270 let autoload = Autoload::new(autoload, autoload_info);
271 autoloads.push(autoload);
272 }
273
274 let mut arm9 = Arm9::with_autoloads(arm9, &autoloads, arm9_build_config.offsets, Arm9WithTcmsOptions {
276 originally_compressed: arm9_build_config.compressed,
277 originally_encrypted: arm9_build_config.encrypted,
278 })?;
279 arm9_build_config.build_info.assign_to_raw(arm9.build_info_mut()?);
280 if arm9_build_config.compressed && options.compress {
281 log::info!("Compressing ARM9 program");
282 arm9.compress()?;
283 }
284 if arm9_build_config.encrypted && options.encrypt {
285 let Some(key) = options.key else {
286 return BlowfishKeyNeededSnafu {}.fail();
287 };
288 log::info!("Encrypting ARM9 program");
289 arm9.encrypt(key, header.original.gamecode.to_le_u32())?;
290 }
291
292 let arm9_overlays = if let Some(arm9_overlays_config) = &config.arm9_overlays {
294 Self::load_overlays(&path.join(arm9_overlays_config), "arm9", &options)?
295 } else {
296 vec![]
297 };
298
299 let arm7 = read_file(path.join(&config.arm7_bin))?;
301 let arm7_config = serde_yml::from_reader(open_file(path.join(&config.arm7_config))?)?;
302 let arm7 = Arm7::new(arm7, arm7_config);
303
304 let arm7_overlays = if let Some(arm7_overlays_config) = &config.arm7_overlays {
306 Self::load_overlays(&path.join(arm7_overlays_config), "arm7", &options)?
307 } else {
308 vec![]
309 };
310
311 let banner_path = path.join(&config.banner);
313 let banner_dir = banner_path.parent().unwrap();
314 let mut banner: Banner = serde_yml::from_reader(open_file(&banner_path)?)?;
315 banner.images.load(banner_dir)?;
316
317 let num_overlays = arm9_overlays.len() + arm7_overlays.len();
319 let (files, path_order) = if options.load_files {
320 log::info!("Loading ROM assets");
321 let files = FileSystem::load(path.join(&config.files_dir), num_overlays)?;
322 let path_order =
323 read_to_string(path.join(&config.path_order))?.trim().lines().map(|l| l.to_string()).collect::<Vec<_>>();
324 (files, path_order)
325 } else {
326 (FileSystem::new(num_overlays), vec![])
327 };
328
329 Ok(Self { header, header_logo, arm9, arm9_overlays, arm7, arm7_overlays, banner, files, path_order, config })
330 }
331
332 fn load_overlays(config_path: &Path, processor: &str, options: &RomLoadOptions) -> Result<Vec<Overlay<'a>>, RomSaveError> {
333 let path = config_path.parent().unwrap();
334 let mut overlays = vec![];
335 let overlay_configs: Vec<OverlayConfig> = serde_yml::from_reader(open_file(config_path)?)?;
336 let num_overlays = overlay_configs.len();
337 for mut config in overlay_configs.into_iter() {
338 let data = read_file(path.join(config.file_name))?;
339 let compressed = config.info.compressed;
340 config.info.compressed = false;
341 let mut overlay = Overlay::new(data, config.info, compressed);
342 if compressed && options.compress {
343 log::info!("Compressing {processor} overlay {}/{}", overlay.id(), num_overlays - 1);
344 overlay.compress()?;
345 }
346 overlays.push(overlay);
347 }
348 Ok(overlays)
349 }
350
351 pub fn save<P: AsRef<Path>>(&self, path: P, key: Option<&BlowfishKey>) -> Result<(), RomSaveError> {
357 let path = path.as_ref();
358 create_dir_all(path)?;
359
360 log::info!("Saving ROM to directory {}", path.display());
361
362 serde_yml::to_writer(create_file_and_dirs(path.join("config.yaml"))?, &self.config)?;
364
365 serde_yml::to_writer(create_file_and_dirs(path.join(&self.config.header))?, &self.header)?;
367 self.header_logo.save_png(path.join(&self.config.header_logo))?;
368
369 let arm9_build_config = self.arm9_build_config()?;
371 serde_yml::to_writer(create_file_and_dirs(path.join(&self.config.arm9_config))?, &arm9_build_config)?;
372 let mut plain_arm9 = self.arm9.clone();
373 if plain_arm9.is_encrypted() {
374 let Some(key) = key else {
375 return BlowfishKeyNeededSnafu {}.fail();
376 };
377 log::info!("Decrypting ARM9 program");
378 plain_arm9.decrypt(key, self.header.original.gamecode.to_le_u32())?;
379 }
380 if plain_arm9.is_compressed()? {
381 log::info!("Decompressing ARM9 program");
382 plain_arm9.decompress()?;
383 }
384 create_file_and_dirs(path.join(&self.config.arm9_bin))?.write(plain_arm9.code()?)?;
385
386 let mut unknown_autoloads = self.config.unknown_autoloads.iter();
388 for autoload in plain_arm9.autoloads()?.iter() {
389 let (bin_path, config_path) = match autoload.kind() {
390 raw::AutoloadKind::Itcm => (path.join(&self.config.itcm.bin), path.join(&self.config.itcm.config)),
391 raw::AutoloadKind::Dtcm => (path.join(&self.config.dtcm.bin), path.join(&self.config.dtcm.config)),
392 raw::AutoloadKind::Unknown(_) => {
393 let unknown_autoload = unknown_autoloads.next().expect("no more autoloads in config, was it removed?");
394 (path.join(&unknown_autoload.bin), path.join(&unknown_autoload.config))
395 }
396 };
397 create_file_and_dirs(bin_path)?.write(autoload.code())?;
398 serde_yml::to_writer(create_file_and_dirs(config_path)?, autoload.info())?;
399 }
400
401 if let Some(arm9_overlays_config) = &self.config.arm9_overlays {
403 Self::save_overlays(&path.join(arm9_overlays_config), &self.arm9_overlays, "arm9")?;
404 }
405
406 create_file_and_dirs(path.join(&self.config.arm7_bin))?.write(self.arm7.full_data())?;
408 serde_yml::to_writer(create_file_and_dirs(path.join(&self.config.arm7_config))?, self.arm7.offsets())?;
409
410 if let Some(arm7_overlays_config) = &self.config.arm7_overlays {
412 Self::save_overlays(&path.join(arm7_overlays_config), &self.arm7_overlays, "arm7")?;
413 }
414
415 {
417 let banner_path = path.join(&self.config.banner);
418 let banner_dir = banner_path.parent().unwrap();
419 serde_yml::to_writer(create_file_and_dirs(&banner_path)?, &self.banner)?;
420 self.banner.images.save_bitmap_file(banner_dir)?;
421 }
422
423 {
425 log::info!("Saving ROM assets");
426 let files_path = path.join(&self.config.files_dir);
427 self.files.traverse_files(["/"], |file, path| {
428 let path = files_path.join(path);
429 create_dir_all(&path).expect("failed to create file directory");
431 create_file(&path.join(file.name()))
432 .expect("failed to create file")
433 .write(file.contents())
434 .expect("failed to write file");
435 });
436 }
437 let mut path_order_file = create_file_and_dirs(path.join(&self.config.path_order))?;
438 for path in &self.path_order {
439 path_order_file.write(path.as_bytes())?;
440 path_order_file.write("\n".as_bytes())?;
441 }
442
443 Ok(())
444 }
445
446 pub fn arm9_build_config(&self) -> Result<Arm9BuildConfig, RomSaveError> {
448 Ok(Arm9BuildConfig {
449 offsets: *self.arm9.offsets(),
450 encrypted: self.arm9.is_encrypted(),
451 compressed: self.arm9.is_compressed()?,
452 build_info: self.arm9.build_info()?.clone().into(),
453 })
454 }
455
456 fn save_overlays(config_path: &Path, overlays: &[Overlay], processor: &str) -> Result<(), RomSaveError> {
457 if !overlays.is_empty() {
458 let overlays_path = config_path.parent().unwrap();
459 create_dir_all(overlays_path)?;
460
461 let mut configs = vec![];
462 for overlay in overlays {
463 let name = format!("ov{:03}", overlay.id());
464
465 let mut plain_overlay = overlay.clone();
466 configs.push(OverlayConfig { info: plain_overlay.info().clone(), file_name: format!("{name}.bin") });
467
468 if plain_overlay.is_compressed() {
469 log::info!("Decompressing {processor} overlay {}/{}", overlay.id(), overlays.len() - 1);
470 plain_overlay.decompress()?;
471 }
472 create_file(overlays_path.join(format!("{name}.bin")))?.write(plain_overlay.code())?;
473 }
474 serde_yml::to_writer(create_file(config_path)?, &configs)?;
475 }
476 Ok(())
477 }
478
479 pub fn extract(rom: &'a raw::Rom) -> Result<Self, RomExtractError> {
485 let header = rom.header()?;
486 let fnt = rom.fnt()?;
487 let fat = rom.fat()?;
488 let banner = rom.banner()?;
489 let file_root = FileSystem::parse(&fnt, fat, rom)?;
490 let path_order = file_root.compute_path_order();
491
492 let arm9_overlays =
493 rom.arm9_overlay_table()?.iter().map(|ov| Overlay::parse(ov, fat, rom)).collect::<Result<Vec<_>, _>>()?;
494 let arm7_overlays =
495 rom.arm7_overlay_table()?.iter().map(|ov| Overlay::parse(ov, fat, rom)).collect::<Result<Vec<_>, _>>()?;
496
497 let arm9 = rom.arm9()?;
498
499 let num_unknown_autoloads = if arm9.is_compressed()? {
500 let mut decompressed_arm9 = arm9.clone();
501 decompressed_arm9.decompress()?;
502 decompressed_arm9.num_unknown_autoloads()?
503 } else {
504 arm9.num_unknown_autoloads()?
505 };
506 let unknown_autoloads = (0..num_unknown_autoloads)
507 .map(|index| RomConfigAutoload {
508 bin: format!("arm9/unk_autoload_{index}.bin").into(),
509 config: format!("arm9/unk_autoload_{index}.yaml").into(),
510 })
511 .collect();
512
513 let config = RomConfig {
514 padding_value: rom.padding_value()?,
515 header: "header.yaml".into(),
516 header_logo: "header_logo.png".into(),
517 arm9_bin: "arm9/arm9.bin".into(),
518 arm9_config: "arm9/arm9.yaml".into(),
519 arm7_bin: "arm7/arm7.bin".into(),
520 arm7_config: "arm7/arm7.yaml".into(),
521 itcm: RomConfigAutoload { bin: "arm9/itcm.bin".into(), config: "arm9/itcm.yaml".into() },
522 unknown_autoloads,
523 dtcm: RomConfigAutoload { bin: "arm9/dtcm.bin".into(), config: "arm9/dtcm.yaml".into() },
524 arm9_overlays: if arm9_overlays.is_empty() { None } else { Some("arm9_overlays/overlays.yaml".into()) },
525 arm7_overlays: if arm7_overlays.is_empty() { None } else { Some("arm7_overlays/overlays.yaml".into()) },
526 banner: "banner/banner.yaml".into(),
527 files_dir: "files/".into(),
528 path_order: "path_order.txt".into(),
529 };
530
531 Ok(Self {
532 header: Header::load_raw(&header),
533 header_logo: Logo::decompress(&header.logo)?,
534 arm9,
535 arm9_overlays,
536 arm7: rom.arm7()?,
537 arm7_overlays,
538 banner: Banner::load_raw(&banner),
539 files: file_root,
540 path_order,
541 config,
542 })
543 }
544
545 pub fn build(mut self, key: Option<&BlowfishKey>) -> Result<raw::Rom<'a>, RomBuildError> {
551 let mut context = BuildContext::default();
552 context.blowfish_key = key;
553
554 let mut cursor = Cursor::new(Vec::with_capacity(128 * 1024)); context.header_offset = Some(cursor.position() as u32);
558 cursor.write(&[0u8; size_of::<raw::Header>()])?;
559 self.align(&mut cursor)?;
560
561 context.arm9_offset = Some(cursor.position() as u32);
563 context.arm9_autoload_callback = Some(self.arm9.autoload_callback());
564 context.arm9_build_info_offset = Some(self.arm9.build_info_offset());
565 cursor.write(self.arm9.full_data())?;
566 let footer = Arm9Footer::new(self.arm9.build_info_offset());
567 cursor.write(bytemuck::bytes_of(&footer))?;
568 self.align(&mut cursor)?;
569
570 let max_file_id = self.files.max_file_id();
571 let mut file_allocs = vec![FileAlloc::default(); max_file_id as usize + 1];
572
573 if !self.arm9_overlays.is_empty() {
574 context.arm9_ovt_offset = Some(TableOffset {
576 offset: cursor.position() as u32,
577 size: (self.arm9_overlays.len() * size_of::<raw::Overlay>()) as u32,
578 });
579 for overlay in &self.arm9_overlays {
580 let raw = overlay.build();
581 cursor.write(bytemuck::bytes_of(&raw))?;
582 }
583 self.align(&mut cursor)?;
584
585 for overlay in &self.arm9_overlays {
587 let start = cursor.position() as u32;
588 let end = start + overlay.full_data().len() as u32;
589 file_allocs[overlay.file_id() as usize] = FileAlloc { start, end };
590
591 cursor.write(overlay.full_data())?;
592 self.align(&mut cursor)?;
593 }
594 }
595
596 context.arm7_offset = Some(cursor.position() as u32);
598 context.arm7_autoload_callback = Some(self.arm7.autoload_callback());
599 context.arm7_build_info_offset = None;
600 cursor.write(self.arm7.full_data())?;
601 self.align(&mut cursor)?;
602
603 if !self.arm7_overlays.is_empty() {
604 context.arm7_ovt_offset = Some(TableOffset {
606 offset: cursor.position() as u32,
607 size: (self.arm7_overlays.len() * size_of::<raw::Overlay>()) as u32,
608 });
609 for overlay in &self.arm7_overlays {
610 let raw = overlay.build();
611 cursor.write(bytemuck::bytes_of(&raw))?;
612 }
613 self.align(&mut cursor)?;
614
615 for overlay in &self.arm7_overlays {
617 let start = cursor.position() as u32;
618 let end = start + overlay.full_data().len() as u32;
619 file_allocs[overlay.file_id() as usize] = FileAlloc { start, end };
620
621 cursor.write(overlay.full_data())?;
622 self.align(&mut cursor)?;
623 }
624 }
625
626 self.files.sort_for_fnt();
628 let fnt = self.files.build_fnt()?.build()?;
629 context.fnt_offset = Some(TableOffset { offset: cursor.position() as u32, size: fnt.len() as u32 });
630 cursor.write(&fnt)?;
631 self.align(&mut cursor)?;
632
633 context.fat_offset =
635 Some(TableOffset { offset: cursor.position() as u32, size: (file_allocs.len() * size_of::<FileAlloc>()) as u32 });
636 cursor.write(bytemuck::cast_slice(&file_allocs))?;
637 self.align(&mut cursor)?;
638
639 let banner = self.banner.build()?;
641 context.banner_offset = Some(TableOffset { offset: cursor.position() as u32, size: banner.full_data().len() as u32 });
642 cursor.write(banner.full_data())?;
643 self.align(&mut cursor)?;
644
645 self.files.sort_for_rom();
647 self.files.traverse_files(self.path_order.iter().map(|s| s.as_str()), |file, _| {
648 self.align(&mut cursor).expect("failed to align before file");
650
651 let contents = file.contents();
652 let start = cursor.position() as u32;
653 let end = start + contents.len() as u32;
654 file_allocs[file.id() as usize] = FileAlloc { start, end };
655
656 cursor.write(contents).expect("failed to write file contents");
657 });
658
659 context.rom_size = Some(cursor.position() as u32);
661 while !cursor.position().is_power_of_two() && cursor.position() >= 128 * 1024 {
662 cursor.write(&[self.config.padding_value])?;
663 }
664
665 cursor.set_position(context.fat_offset.unwrap().offset as u64);
667 cursor.write(&bytemuck::cast_slice(&file_allocs))?;
668
669 cursor.set_position(context.header_offset.unwrap() as u64);
671 let header = self.header.build(&context, &self)?;
672 cursor.write(bytemuck::bytes_of(&header))?;
673
674 Ok(raw::Rom::new(cursor.into_inner()))
675 }
676
677 fn align(&self, cursor: &mut Cursor<Vec<u8>>) -> Result<(), RomBuildError> {
678 let padding = (!cursor.position() + 1) & 0x1ff;
679 for _ in 0..padding {
680 cursor.write(&[self.config.padding_value])?;
681 }
682 Ok(())
683 }
684
685 pub fn header_logo(&self) -> &Logo {
687 &self.header_logo
688 }
689
690 pub fn arm9(&self) -> &Arm9 {
692 &self.arm9
693 }
694
695 pub fn arm9_overlays(&self) -> &[Overlay] {
697 &self.arm9_overlays
698 }
699
700 pub fn arm7(&self) -> &Arm7 {
702 &self.arm7
703 }
704
705 pub fn arm7_overlays(&self) -> &[Overlay] {
707 &self.arm7_overlays
708 }
709
710 pub fn header(&self) -> &Header {
712 &self.header
713 }
714
715 pub fn config(&self) -> &RomConfig {
717 &self.config
718 }
719}
720
721#[derive(Default)]
723pub struct BuildContext<'a> {
724 pub header_offset: Option<u32>,
726 pub arm9_offset: Option<u32>,
728 pub arm7_offset: Option<u32>,
730 pub fnt_offset: Option<TableOffset>,
732 pub fat_offset: Option<TableOffset>,
734 pub arm9_ovt_offset: Option<TableOffset>,
736 pub arm7_ovt_offset: Option<TableOffset>,
738 pub banner_offset: Option<TableOffset>,
740 pub blowfish_key: Option<&'a BlowfishKey>,
742 pub arm9_autoload_callback: Option<u32>,
744 pub arm7_autoload_callback: Option<u32>,
746 pub arm9_build_info_offset: Option<u32>,
748 pub arm7_build_info_offset: Option<u32>,
750 pub rom_size: Option<u32>,
752}
753
754pub struct RomLoadOptions<'a> {
756 pub key: Option<&'a BlowfishKey>,
758 pub compress: bool,
760 pub encrypt: bool,
762 pub load_files: bool,
764}
765
766impl<'a> Default for RomLoadOptions<'a> {
767 fn default() -> Self {
768 Self { key: None, compress: true, encrypt: true, load_files: true }
769 }
770}