1use crate::cartridge::{ConsoleType, Mirroring, Region, RomError, VsPpuType};
6use alloc::format;
7
8pub const MAGIC: [u8; 4] = [b'N', b'E', b'S', 0x1A];
10
11pub const HEADER_LEN: usize = 16;
13
14pub const PRG_UNIT: usize = 16 * 1024;
16
17pub const CHR_UNIT: usize = 8 * 1024;
19
20pub const TRAINER_LEN: usize = 512;
22
23#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
31#[non_exhaustive]
32pub enum VsHardwareType {
33 UniSystem,
35 UniSystemRbiBaseball,
37 UniSystemTkoBoxing,
39 UniSystemSuperXevious,
41 UniSystemIceClimberJapan,
43 DualSystem,
45 DualSystemRaidOnBungelingBay,
47 Reserved(u8),
49}
50
51impl VsHardwareType {
52 #[must_use]
54 pub const fn from_nibble(nibble: u8) -> Self {
55 match nibble & 0x0F {
56 0 => Self::UniSystem,
57 1 => Self::UniSystemRbiBaseball,
58 2 => Self::UniSystemTkoBoxing,
59 3 => Self::UniSystemSuperXevious,
60 4 => Self::UniSystemIceClimberJapan,
61 5 => Self::DualSystem,
62 6 => Self::DualSystemRaidOnBungelingBay,
63 n => Self::Reserved(n),
64 }
65 }
66
67 #[must_use]
70 pub const fn to_nibble(self) -> u8 {
71 match self {
72 Self::UniSystem => 0,
73 Self::UniSystemRbiBaseball => 1,
74 Self::UniSystemTkoBoxing => 2,
75 Self::UniSystemSuperXevious => 3,
76 Self::UniSystemIceClimberJapan => 4,
77 Self::DualSystem => 5,
78 Self::DualSystemRaidOnBungelingBay => 6,
79 Self::Reserved(n) => n & 0x0F,
80 }
81 }
82
83 #[must_use]
85 pub const fn is_dual_system(self) -> bool {
86 matches!(self, Self::DualSystem | Self::DualSystemRaidOnBungelingBay)
87 }
88}
89
90#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
99#[non_exhaustive]
100pub enum ExtendedConsoleType {
101 Regular,
103 VsSystem,
105 Playchoice10,
107 DecimalModeFamiclone,
109 Epsm,
111 Vt01,
113 Vt02,
115 Vt03,
117 Vt09,
119 Vt32,
121 Vt369,
123 Um6578,
125 FamicomNetworkSystem,
127 Reserved(u8),
129}
130
131impl ExtendedConsoleType {
132 #[must_use]
134 pub const fn from_nibble(nibble: u8) -> Self {
135 match nibble & 0x0F {
136 0x0 => Self::Regular,
137 0x1 => Self::VsSystem,
138 0x2 => Self::Playchoice10,
139 0x3 => Self::DecimalModeFamiclone,
140 0x4 => Self::Epsm,
141 0x5 => Self::Vt01,
142 0x6 => Self::Vt02,
143 0x7 => Self::Vt03,
144 0x8 => Self::Vt09,
145 0x9 => Self::Vt32,
146 0xA => Self::Vt369,
147 0xB => Self::Um6578,
148 0xC => Self::FamicomNetworkSystem,
149 n => Self::Reserved(n),
150 }
151 }
152
153 #[must_use]
155 pub const fn to_nibble(self) -> u8 {
156 match self {
157 Self::Regular => 0x0,
158 Self::VsSystem => 0x1,
159 Self::Playchoice10 => 0x2,
160 Self::DecimalModeFamiclone => 0x3,
161 Self::Epsm => 0x4,
162 Self::Vt01 => 0x5,
163 Self::Vt02 => 0x6,
164 Self::Vt03 => 0x7,
165 Self::Vt09 => 0x8,
166 Self::Vt32 => 0x9,
167 Self::Vt369 => 0xA,
168 Self::Um6578 => 0xB,
169 Self::FamicomNetworkSystem => 0xC,
170 Self::Reserved(n) => n & 0x0F,
171 }
172 }
173}
174
175macro_rules! expansion_devices {
178 ($( $(#[$doc:meta])* $variant:ident = $code:literal, )*) => {
179 #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
188 #[non_exhaustive]
189 pub enum ExpansionDevice {
190 $( $(#[$doc])* $variant, )*
191 Unassigned(u8),
193 }
194
195 impl ExpansionDevice {
196 #[must_use]
199 pub const fn from_code(code: u8) -> Self {
200 match code & 0x7F {
201 $( $code => Self::$variant, )*
202 n => Self::Unassigned(n),
203 }
204 }
205
206 #[must_use]
208 pub const fn code(self) -> u8 {
209 match self {
210 $( Self::$variant => $code, )*
211 Self::Unassigned(n) => n & 0x7F,
212 }
213 }
214 }
215 };
216}
217
218expansion_devices! {
219 Unspecified = 0x00,
221 StandardControllers = 0x01,
223 FourScore = 0x02,
225 FamicomFourPlayersAdapter = 0x03,
227 VsSystem4016 = 0x04,
229 VsSystem4017 = 0x05,
231 VsZapper = 0x07,
233 Zapper4017 = 0x08,
235 TwoZappers = 0x09,
237 BandaiHyperShot = 0x0A,
239 PowerPadSideA = 0x0B,
241 PowerPadSideB = 0x0C,
243 FamilyTrainerSideA = 0x0D,
245 FamilyTrainerSideB = 0x0E,
247 VausNes = 0x0F,
249 VausFamicom = 0x10,
251 TwoVausPlusDataRecorder = 0x11,
253 KonamiHyperShot = 0x12,
255 CoconutsPachinko = 0x13,
257 ExcitingBoxingPunchingBag = 0x14,
259 JissenMahjong = 0x15,
261 PartyTap = 0x16,
263 OekaKidsTablet = 0x17,
265 BarcodeBattler = 0x18,
267 MiraclePiano = 0x19,
269 PokkunMoguraa = 0x1A,
271 TopRider = 0x1B,
273 DoubleFisted = 0x1C,
275 Famicom3dSystem = 0x1D,
277 DoremikkoKeyboard = 0x1E,
279 RobGyromite = 0x1F,
281 FamicomDataRecorder = 0x20,
283 AsciiTurboFile = 0x21,
285 IgsBattleBox = 0x22,
287 FamilyBasicKeyboard = 0x23,
289 DongdaPecKeyboard = 0x24,
291 Bit79Keyboard = 0x25,
293 SuborKeyboard = 0x26,
295 SuborKeyboardMacroWinnersMouse = 0x27,
297 SuborKeyboardSuborMouse4016 = 0x28,
299 SnesMouse4016 = 0x29,
301 Multicart = 0x2A,
303 TwoSnesControllers = 0x2B,
305 RacerMateBicycle = 0x2C,
307 UForce = 0x2D,
309 RobStackUp = 0x2E,
311 CityPatrolmanLightgun = 0x2F,
313 SharpC1CassetteInterface = 0x30,
315 SwappedStandardController = 0x31,
317 ExcaliburSudokuPad = 0x32,
319 AblPinball = 0x33,
321 GoldenNuggetCasino = 0x34,
323 KedaKeyboard = 0x35,
325 SuborKeyboardSuborMouse4017 = 0x36,
327 PortTestController = 0x37,
329 BandaiMultiGamePlayer = 0x38,
331 VenomTvDanceMat = 0x39,
333 LgTvRemote = 0x3A,
335 FamicomNetworkController = 0x3B,
337 KingFishing = 0x3C,
339 CroakyKaraoke = 0x3D,
341 KingwonKeyboard = 0x3E,
343 ZechengKeyboard = 0x3F,
345 SuborKeyboardL90Ps2Mouse = 0x40,
347 Um6578Ps2KeyboardAndMouse = 0x41,
349 Um6578Ps2Mouse = 0x42,
351 YuxingMouse = 0x43,
353 SuborKeyboardYuxingMouse = 0x44,
355 GiggleTvPump = 0x45,
357 BbkKeyboardR90Ps2Mouse = 0x46,
359 MagicalCooking = 0x47,
361 SnesMouse4017 = 0x48,
363 Zapper4016 = 0x49,
365 VausPrototype = 0x4A,
367 TvMahjongGame = 0x4B,
369 MahjongGekitouDensetsu = 0x4C,
371 SuborKeyboardXInvertedPs2Mouse = 0x4D,
373 IbmPcXtKeyboard = 0x4E,
375 SuborKeyboardMegaBookMouse = 0x4F,
377}
378
379#[derive(Debug, Clone, Copy, PartialEq, Eq)]
402#[allow(clippy::struct_excessive_bools)]
403#[non_exhaustive]
404pub struct Header {
405 pub is_nes2: bool,
407 pub mapper_id: u16,
409 pub submapper: u8,
411 pub prg_size: usize,
413 pub chr_size: usize,
415 pub mirroring: Mirroring,
417 pub region: Region,
419 pub console_type: ConsoleType,
421 pub vs_ppu_type: VsPpuType,
428 pub vs_hardware_type: Option<VsHardwareType>,
433 pub extended_console_type: Option<ExtendedConsoleType>,
436 pub misc_rom_count: u8,
440 pub default_expansion_device: ExpansionDevice,
443 pub prg_ram_size: u32,
449 pub prg_nvram_size: u32,
453 pub chr_ram_size: u32,
456 pub chr_nvram_size: u32,
459 pub has_battery: bool,
461 pub has_trainer: bool,
463 pub four_screen: bool,
465}
466
467impl Default for Header {
468 fn default() -> Self {
473 Self {
474 is_nes2: false,
475 mapper_id: 0,
476 submapper: 0,
477 prg_size: 0,
478 chr_size: 0,
479 mirroring: Mirroring::Horizontal,
480 region: Region::Ntsc,
481 console_type: ConsoleType::Nes,
482 vs_ppu_type: VsPpuType::None,
483 vs_hardware_type: None,
484 extended_console_type: None,
485 misc_rom_count: 0,
486 default_expansion_device: ExpansionDevice::Unspecified,
487 prg_ram_size: INES1_NOMINAL_PRG_RAM,
488 prg_nvram_size: 0,
489 chr_ram_size: INES1_NOMINAL_CHR_RAM,
490 chr_nvram_size: 0,
491 has_battery: false,
492 has_trainer: false,
493 four_screen: false,
494 }
495 }
496}
497
498impl Header {
499 #[must_use]
504 pub const fn prg_ram_window(&self) -> u32 {
505 self.prg_ram_size.saturating_add(self.prg_nvram_size)
506 }
507
508 #[must_use]
511 pub const fn is_vs_dual_system(&self) -> bool {
512 match self.vs_hardware_type {
513 Some(t) => t.is_dual_system(),
514 None => false,
515 }
516 }
517}
518
519const INES1_NOMINAL_PRG_RAM: u32 = 8 * 1024;
522
523const INES1_NOMINAL_CHR_RAM: u32 = 8 * 1024;
525
526fn mapper_number(h: &[u8; HEADER_LEN], is_nes2: bool) -> u16 {
529 let ines1_dirty_tail = !is_nes2 && h[12..16].iter().any(|&b| b != 0);
546 let mapper_low = u16::from((h[6] >> 4) & 0x0F);
547 let mapper_mid = if ines1_dirty_tail {
548 0
549 } else {
550 u16::from(h[7] & 0xF0)
551 };
552 if is_nes2 {
553 let mapper_hi = u16::from(h[8] & 0x0F) << 8;
554 mapper_low | mapper_mid | mapper_hi
555 } else {
556 mapper_low | mapper_mid
557 }
558}
559
560pub fn parse_header(bytes: &[u8]) -> Result<Header, RomError> {
568 if bytes.len() < HEADER_LEN {
569 return Err(RomError::Truncated {
570 needed: HEADER_LEN,
571 got: bytes.len(),
572 });
573 }
574 if bytes[0..4] != MAGIC {
575 return Err(RomError::BadMagic);
576 }
577
578 let h: [u8; HEADER_LEN] = bytes[..HEADER_LEN].try_into().expect("checked length");
579 let is_nes2 = (h[7] & 0x0C) == 0x08;
580
581 let mapper_id = mapper_number(&h, is_nes2);
582 let submapper: u8 = if is_nes2 { (h[8] >> 4) & 0x0F } else { 0 };
583
584 let prg_size = if is_nes2 {
586 decoded_size(h[4], u16::from(h[9] & 0x0F), PRG_UNIT)?
587 } else {
588 usize::from(h[4]) * PRG_UNIT
589 };
590 let chr_size = if is_nes2 {
591 decoded_size(h[5], u16::from((h[9] >> 4) & 0x0F), CHR_UNIT)?
592 } else {
593 usize::from(h[5]) * CHR_UNIT
594 };
595
596 let four_screen = (h[6] & 0x08) != 0;
598 let mirroring = if four_screen {
599 Mirroring::FourScreen
600 } else if (h[6] & 0x01) != 0 {
601 Mirroring::Vertical
602 } else {
603 Mirroring::Horizontal
604 };
605
606 let region = if is_nes2 {
608 match h[12] & 0x03 {
609 0 => Region::Ntsc,
610 1 => Region::Pal,
611 2 => Region::Multi,
612 3 => Region::Dendy,
613 _ => unreachable!(),
614 }
615 } else {
616 Region::Ntsc
618 };
619
620 let console_type = if is_nes2 {
622 match h[7] & 0x03 {
623 0 => ConsoleType::Nes,
624 1 => ConsoleType::VsSystem,
625 2 => ConsoleType::Playchoice10,
626 3 => ConsoleType::Extended,
627 _ => unreachable!(),
628 }
629 } else {
630 ConsoleType::Nes
631 };
632
633 let vs_ppu_type = if is_nes2 && console_type == ConsoleType::VsSystem {
635 VsPpuType::from_byte13_low_nibble(h[13] & 0x0F)
636 } else {
637 VsPpuType::None
638 };
639
640 let tail = decode_tail(&h, is_nes2, console_type);
641
642 let (prg_ram_size, prg_nvram_size, chr_ram_size, chr_nvram_size) = if is_nes2 {
651 (
652 ram_size_from_shift(h[10] & 0x0F),
653 ram_size_from_shift(h[10] >> 4),
654 ram_size_from_shift(h[11] & 0x0F),
655 ram_size_from_shift(h[11] >> 4),
656 )
657 } else {
658 let chr_ram = if chr_size == 0 {
659 INES1_NOMINAL_CHR_RAM
660 } else {
661 0
662 };
663 (INES1_NOMINAL_PRG_RAM, 0, chr_ram, 0)
664 };
665
666 let has_battery = (h[6] & 0x02) != 0;
668 let has_trainer = (h[6] & 0x04) != 0;
669
670 Ok(Header {
671 is_nes2,
672 mapper_id,
673 submapper,
674 prg_size,
675 chr_size,
676 mirroring,
677 region,
678 console_type,
679 vs_ppu_type,
680 vs_hardware_type: tail.vs_hardware_type,
681 extended_console_type: tail.extended_console_type,
682 misc_rom_count: tail.misc_rom_count,
683 default_expansion_device: tail.default_expansion_device,
684 prg_ram_size,
685 prg_nvram_size,
686 chr_ram_size,
687 chr_nvram_size,
688 has_battery,
689 has_trainer,
690 four_screen,
691 })
692}
693
694struct Tail {
696 vs_hardware_type: Option<VsHardwareType>,
697 extended_console_type: Option<ExtendedConsoleType>,
698 misc_rom_count: u8,
699 default_expansion_device: ExpansionDevice,
700}
701
702fn decode_tail(h: &[u8; HEADER_LEN], is_nes2: bool, console_type: ConsoleType) -> Tail {
704 if !is_nes2 {
705 return Tail {
706 vs_hardware_type: None,
707 extended_console_type: None,
708 misc_rom_count: 0,
709 default_expansion_device: ExpansionDevice::Unspecified,
710 };
711 }
712 Tail {
713 vs_hardware_type: (console_type == ConsoleType::VsSystem)
716 .then(|| VsHardwareType::from_nibble(h[13] >> 4)),
717 extended_console_type: (console_type == ConsoleType::Extended)
719 .then(|| ExtendedConsoleType::from_nibble(h[13])),
720 misc_rom_count: h[14] & 0x03,
722 default_expansion_device: ExpansionDevice::from_code(h[15]),
723 }
724}
725
726fn decoded_size(lsb: u8, msb_nibble: u16, unit: usize) -> Result<usize, RomError> {
730 if msb_nibble == 0x0F {
731 let exponent = u32::from(lsb >> 2);
733 if exponent >= 32 {
734 return Err(RomError::InvalidConfig(format!(
735 "exponent-multiplier exponent {exponent} overflows usize"
736 )));
737 }
738 let multiplier_code = lsb & 0x03;
739 let multiplier = u64::from(multiplier_code) * 2 + 1;
740 let bytes = (1u64
741 .checked_shl(exponent)
742 .ok_or_else(|| RomError::InvalidConfig("exponent shift overflow".into()))?)
743 .checked_mul(multiplier)
744 .ok_or_else(|| RomError::InvalidConfig("multiplier overflow".into()))?;
745 usize::try_from(bytes).map_err(|_| {
746 RomError::InvalidConfig("exponent-multiplier size exceeds usize::MAX".into())
747 })
748 } else {
749 let count = (msb_nibble << 8) | u16::from(lsb);
750 let bytes = usize::from(count)
751 .checked_mul(unit)
752 .ok_or_else(|| RomError::InvalidConfig("rom size overflow".into()))?;
753 Ok(bytes)
754 }
755}
756
757const fn ram_size_from_shift(shift: u8) -> u32 {
759 if shift == 0 { 0 } else { 64u32 << shift }
760}
761
762#[must_use]
786pub fn serialize_header_preserving(h: &Header, original: &[u8; HEADER_LEN]) -> [u8; HEADER_LEN] {
787 let Ok(base) = parse_header(original) else {
788 return canonical_header(h);
789 };
790 if h.is_nes2 != base.is_nes2 {
791 return canonical_header(h);
792 }
793 let c = canonical_header(h);
796 let mut out = *original;
797 let mut take = |byte: usize, mask: u8| out[byte] = (out[byte] & !mask) | (c[byte] & mask);
798
799 if h.mapper_id != base.mapper_id {
800 take(6, 0xF0);
801 take(7, 0xF0);
802 if h.is_nes2 {
803 take(8, 0x0F);
804 }
805 }
806 let clear_dirty_tail = !h.is_nes2
810 && h.mapper_id != base.mapper_id
811 && h.mapper_id > 0x0F
812 && original[12..16].iter().any(|&b| b != 0);
813 if h.mirroring != base.mirroring {
814 take(6, 0x01);
815 }
816 if h.has_battery != base.has_battery {
817 take(6, 0x02);
818 }
819 if h.has_trainer != base.has_trainer {
820 take(6, 0x04);
821 }
822 if h.four_screen != base.four_screen {
823 take(6, 0x08);
824 }
825 if h.prg_size != base.prg_size {
826 take(4, 0xFF);
827 if h.is_nes2 {
828 take(9, 0x0F);
829 }
830 }
831 if h.chr_size != base.chr_size {
832 take(5, 0xFF);
833 if h.is_nes2 {
834 take(9, 0xF0);
835 }
836 }
837 if h.is_nes2 {
840 if h.submapper != base.submapper {
841 take(8, 0xF0);
842 }
843 if h.prg_ram_size != base.prg_ram_size {
844 take(10, 0x0F);
845 }
846 if h.prg_nvram_size != base.prg_nvram_size {
847 take(10, 0xF0);
848 }
849 if h.chr_ram_size != base.chr_ram_size {
850 take(11, 0x0F);
851 }
852 if h.chr_nvram_size != base.chr_nvram_size {
853 take(11, 0xF0);
854 }
855 if h.region != base.region {
856 take(12, 0x03);
857 }
858 if h.console_type != base.console_type {
859 take(7, 0x03);
862 take(13, 0xFF);
863 } else if h.console_type == ConsoleType::VsSystem {
864 if h.vs_ppu_type != base.vs_ppu_type {
865 take(13, 0x0F);
866 }
867 if h.vs_hardware_type != base.vs_hardware_type {
868 take(13, 0xF0);
869 }
870 } else if h.console_type == ConsoleType::Extended
871 && h.extended_console_type != base.extended_console_type
872 {
873 take(13, 0x0F);
874 }
875 if h.misc_rom_count != base.misc_rom_count {
876 take(14, 0x03);
877 }
878 if h.default_expansion_device != base.default_expansion_device {
879 take(15, 0x7F);
880 }
881 }
882 if clear_dirty_tail {
883 out[12..16].fill(0);
884 }
885 out
886}
887
888#[allow(clippy::cast_possible_truncation)]
906fn canonical_header(h: &Header) -> [u8; HEADER_LEN] {
907 let mut out = [0u8; HEADER_LEN];
908 out[0..4].copy_from_slice(&MAGIC);
909
910 let (prg_lsb, prg_msb_nibble) = encode_size(h.prg_size, PRG_UNIT, h.is_nes2);
912 let (chr_lsb, chr_msb_nibble) = encode_size(h.chr_size, CHR_UNIT, h.is_nes2);
913 out[4] = prg_lsb;
914 out[5] = chr_lsb;
915
916 let mut flags6 = ((h.mapper_id & 0x0F) as u8) << 4;
918 if matches!(h.mirroring, Mirroring::Vertical) {
919 flags6 |= 0x01;
920 }
921 if h.has_battery {
922 flags6 |= 0x02;
923 }
924 if h.has_trainer {
925 flags6 |= 0x04;
926 }
927 if h.four_screen {
928 flags6 |= 0x08;
929 }
930 out[6] = flags6;
931
932 let mut flags7 = (h.mapper_id as u8) & 0xF0;
935 if h.is_nes2 {
936 flags7 |= 0x08;
937 flags7 |= match h.console_type {
938 ConsoleType::Nes => 0,
939 ConsoleType::VsSystem => 1,
940 ConsoleType::Playchoice10 => 2,
941 ConsoleType::Extended => 3,
942 };
943 }
944 out[7] = flags7;
945
946 if h.is_nes2 {
947 out[8] = (((h.mapper_id >> 8) as u8) & 0x0F) | ((h.submapper & 0x0F) << 4);
949 out[9] = (prg_msb_nibble & 0x0F) | ((chr_msb_nibble & 0x0F) << 4);
950 out[10] = ram_shift_for(h.prg_ram_size) | (ram_shift_for(h.prg_nvram_size) << 4);
951 out[11] = ram_shift_for(h.chr_ram_size) | (ram_shift_for(h.chr_nvram_size) << 4);
952 out[12] = match h.region {
953 Region::Ntsc => 0,
954 Region::Pal => 1,
955 Region::Multi => 2,
956 Region::Dendy => 3,
957 };
958 out[13] = match h.console_type {
963 ConsoleType::VsSystem => {
964 let hw = h.vs_hardware_type.map_or(0, VsHardwareType::to_nibble);
965 vs_ppu_type_to_nibble(h.vs_ppu_type) | (hw << 4)
966 }
967 ConsoleType::Extended => h
968 .extended_console_type
969 .map_or(0, ExtendedConsoleType::to_nibble),
970 ConsoleType::Nes | ConsoleType::Playchoice10 => 0,
971 };
972 out[14] = h.misc_rom_count & 0x03;
973 out[15] = h.default_expansion_device.code();
974 }
975
976 out
977}
978
979#[allow(clippy::cast_possible_truncation)]
989const fn encode_size(bytes: usize, unit: usize, is_nes2: bool) -> (u8, u8) {
990 let count = bytes / unit;
991 if !is_nes2 {
992 return ((count & 0xFF) as u8, 0);
993 }
994 if bytes.is_multiple_of(unit) && count <= 0xEFF {
995 return ((count & 0xFF) as u8, ((count >> 8) & 0x0F) as u8);
996 }
997 if bytes != 0 {
998 let exponent = bytes.trailing_zeros();
1000 let odd = bytes >> exponent;
1001 if odd <= 7 && exponent < 64 {
1002 let multiplier = ((odd - 1) / 2) as u8;
1003 return (((exponent as u8) << 2) | multiplier, 0x0F);
1004 }
1005 }
1006 ((count & 0xFF) as u8, ((count >> 8) & 0x0F) as u8)
1007}
1008
1009const fn vs_ppu_type_to_nibble(t: VsPpuType) -> u8 {
1011 match t {
1012 VsPpuType::None | VsPpuType::Rp2C03 => 0x0,
1013 VsPpuType::Rp2C04_0001 => 0x2,
1014 VsPpuType::Rp2C04_0002 => 0x3,
1015 VsPpuType::Rp2C04_0003 => 0x4,
1016 VsPpuType::Rp2C04_0004 => 0x5,
1017 VsPpuType::Rc2C05_01 => 0x8,
1018 VsPpuType::Rc2C05_02 => 0x9,
1019 VsPpuType::Rc2C05_03 => 0xA,
1020 VsPpuType::Rc2C05_04 => 0xB,
1021 }
1022}
1023
1024const fn ram_shift_for(size: u32) -> u8 {
1025 if size == 0 {
1026 return 0;
1027 }
1028 let mut shift = 0u8;
1030 let mut v = size / 64;
1031 while v > 1 {
1032 v >>= 1;
1033 shift += 1;
1034 }
1035 shift & 0x0F
1036}
1037
1038#[cfg(test)]
1039mod tests {
1040 use super::*;
1041
1042 fn ines_header(prg_16k_units: u8, chr_8k_units: u8, mapper: u8, flags6: u8) -> [u8; 16] {
1043 let mut h = [0u8; 16];
1044 h[..4].copy_from_slice(&MAGIC);
1045 h[4] = prg_16k_units;
1046 h[5] = chr_8k_units;
1047 h[6] = (mapper << 4) | (flags6 & 0x0F);
1048 h[7] = mapper & 0xF0;
1049 h
1050 }
1051
1052 fn next(state: &mut u64) -> u8 {
1055 *state ^= *state << 13;
1056 *state ^= *state >> 7;
1057 *state ^= *state << 17;
1058 (*state >> 24).to_le_bytes()[0]
1059 }
1060
1061 fn random_header(state: &mut u64) -> [u8; 16] {
1062 let mut h = [0u8; 16];
1063 h[..4].copy_from_slice(&MAGIC);
1064 for b in &mut h[4..] {
1065 *b = next(state);
1066 }
1067 h
1068 }
1069
1070 #[test]
1071 fn preserving_round_trip_is_byte_identical_for_every_parsable_header() {
1072 let mut state = 0x9E37_79B9_7F4A_7C15;
1080 let mut checked = 0u32;
1081 let mut check = |h: &[u8; 16]| {
1082 if let Ok(parsed) = parse_header(h) {
1083 assert_eq!(&serialize_header_preserving(&parsed, h), h, "{h:02x?}");
1084 checked += 1;
1085 }
1086 };
1087 for b7 in 0..=255u8 {
1088 for b13 in 0..=255u8 {
1089 let mut h = random_header(&mut state);
1090 h[7] = b7;
1091 h[13] = b13;
1092 check(&h);
1093 }
1094 }
1095 for _ in 0..200_000 {
1096 check(&random_header(&mut state));
1097 }
1098 assert!(checked > 200_000, "only {checked} headers parsed");
1101 }
1102
1103 #[test]
1104 fn canonical_encoding_round_trips_every_parsed_header() {
1105 let mut state = 0x2545_F491_4F6C_DD1D;
1111 let mut checked = 0u32;
1112 let mut check = |h: &[u8; 16]| {
1113 if let Ok(parsed) = parse_header(h) {
1114 let again = parse_header(&canonical_header(&parsed))
1115 .unwrap_or_else(|e| panic!("{h:02x?}: canonical output did not parse: {e:?}"));
1116 assert_eq!(again, parsed, "{h:02x?}");
1117 checked += 1;
1118 }
1119 };
1120 for b7 in 0..=255u8 {
1121 for b13 in 0..=255u8 {
1122 let mut h = random_header(&mut state);
1123 h[7] = b7;
1124 h[13] = b13;
1125 check(&h);
1126 }
1127 }
1128 for b9 in [0x00, 0x0F, 0xF0, 0xFF, 0x3E, 0xE3] {
1129 for b4 in 0..=255u8 {
1130 let mut h = random_header(&mut state);
1131 h[7] = (h[7] & !0x0C) | 0x08;
1132 h[9] = b9;
1133 h[4] = b4;
1134 h[5] = b4.rotate_left(3);
1135 check(&h);
1136 }
1137 }
1138 for _ in 0..200_000 {
1139 check(&random_header(&mut state));
1140 }
1141 assert!(checked > 200_000, "only {checked} headers parsed");
1142 }
1143
1144 #[test]
1145 #[allow(clippy::field_reassign_with_default)]
1148 fn canonical_encoding_round_trips_constructed_nes2_headers() {
1149 let mut base = Header::default();
1154 base.is_nes2 = true;
1155 base.mapper_id = 0x123;
1156 base.submapper = 7;
1157 base.prg_size = 5 << 20; base.chr_size = 0xEFF * CHR_UNIT; base.region = Region::Multi;
1160 base.prg_ram_size = 0;
1161 base.chr_ram_size = 0;
1162 let mut headers = alloc::vec::Vec::new();
1163 for shift in 0..16u8 {
1164 let size = ram_size_from_shift(shift);
1165 for which in 0..4 {
1166 let mut h = base;
1167 match which {
1168 0 => h.prg_ram_size = size,
1169 1 => h.prg_nvram_size = size,
1170 2 => h.chr_ram_size = size,
1171 _ => h.chr_nvram_size = size,
1172 }
1173 headers.push(h);
1174 }
1175 }
1176 for nibble in 0..16u8 {
1177 let mut h = base;
1178 h.console_type = ConsoleType::VsSystem;
1179 h.vs_ppu_type = VsPpuType::Rc2C05_03;
1180 h.vs_hardware_type = Some(VsHardwareType::from_nibble(nibble));
1181 headers.push(h);
1182 let mut h = base;
1183 h.console_type = ConsoleType::Extended;
1184 h.extended_console_type = Some(ExtendedConsoleType::from_nibble(nibble));
1185 headers.push(h);
1186 }
1187 for count in 0..4u8 {
1188 let mut h = base;
1189 h.misc_rom_count = count;
1190 headers.push(h);
1191 }
1192 for code in 0..=0x7Fu8 {
1193 let mut h = base;
1194 h.default_expansion_device = ExpansionDevice::from_code(code);
1195 headers.push(h);
1196 }
1197 for h in headers {
1198 assert_eq!(parse_header(&canonical_header(&h)).unwrap(), h);
1199 }
1200 let d = Header::default();
1202 assert_eq!(parse_header(&canonical_header(&d)).unwrap(), d);
1203 }
1204
1205 #[test]
1206 fn enum_codes_round_trip() {
1207 for n in 0..16u8 {
1208 assert_eq!(VsHardwareType::from_nibble(n).to_nibble(), n);
1209 assert_eq!(ExtendedConsoleType::from_nibble(n).to_nibble(), n);
1210 }
1211 for code in 0..=0x7Fu8 {
1212 assert_eq!(ExpansionDevice::from_code(code).code(), code);
1213 }
1214 assert_eq!(
1216 ExpansionDevice::from_code(0x06),
1217 ExpansionDevice::Unassigned(0x06)
1218 );
1219 assert_eq!(
1220 ExpansionDevice::from_code(0x08),
1221 ExpansionDevice::Zapper4017
1222 );
1223 assert_eq!(
1224 ExpansionDevice::from_code(0x4F),
1225 ExpansionDevice::SuborKeyboardMegaBookMouse
1226 );
1227 assert_eq!(
1228 ExpansionDevice::from_code(0x50),
1229 ExpansionDevice::Unassigned(0x50)
1230 );
1231 assert_eq!(
1233 ExpansionDevice::from_code(0x88),
1234 ExpansionDevice::Zapper4017
1235 );
1236 assert_eq!(
1237 ExtendedConsoleType::from_nibble(0xC),
1238 ExtendedConsoleType::FamicomNetworkSystem
1239 );
1240 assert!(VsHardwareType::from_nibble(6).is_dual_system());
1241 assert!(!VsHardwareType::from_nibble(7).is_dual_system());
1242 }
1243
1244 #[test]
1245 fn ines1_reports_fixed_values_for_the_nes2_fields() {
1246 let mut h = ines_header(2, 0, 1, 0x02);
1250 h[7] |= 0x01; h[8..16].copy_from_slice(b"Dump3r!!");
1252 let p = parse_header(&h).unwrap();
1253 assert_eq!(p.vs_hardware_type, None);
1254 assert_eq!(p.extended_console_type, None);
1255 assert_eq!(p.misc_rom_count, 0);
1256 assert_eq!(p.default_expansion_device, ExpansionDevice::Unspecified);
1257 assert_eq!((p.prg_ram_size, p.prg_nvram_size), (8 * 1024, 0));
1258 assert_eq!((p.chr_ram_size, p.chr_nvram_size), (8 * 1024, 0));
1259 assert_eq!(p.prg_ram_window(), 8 * 1024);
1260 }
1261
1262 #[test]
1263 fn prg_ram_window_is_volatile_plus_nvram() {
1264 let mut h = ines_header(8, 16, 4, 0x02);
1268 h[7] = 0x08;
1269 h[10] = 0x70; let p = parse_header(&h).unwrap();
1271 assert_eq!((p.prg_ram_size, p.prg_nvram_size), (0, 8 * 1024));
1272 assert_eq!(p.prg_ram_window(), 8 * 1024);
1273 h[10] = 0x75; let p = parse_header(&h).unwrap();
1275 assert_eq!(p.prg_ram_window(), 10 * 1024);
1276 h[11] = 0x57; let p = parse_header(&h).unwrap();
1278 assert_eq!((p.chr_ram_size, p.chr_nvram_size), (8 * 1024, 2 * 1024));
1279 }
1280
1281 #[test]
1282 fn preserving_keeps_byte_13_high_nibble_and_bytes_14_15() {
1283 let mut h = ines_header(2, 1, 0, 0);
1288 h[7] = 0x08 | 0x01; h[14] = 0x02; h[15] = 0x2A; for hw in 0..16u8 {
1292 h[13] = (hw << 4) | 0x2; let mut p = parse_header(&h).unwrap();
1294 p.has_battery = true; let out = serialize_header_preserving(&p, &h);
1296 assert_eq!(out[13], h[13], "Vs. hardware type {hw}");
1297 assert_eq!(out[14..], h[14..], "bytes 14-15, hw {hw}");
1298 assert_eq!(out[6], h[6] | 0x02);
1299 }
1300 }
1301
1302 #[test]
1303 fn preserving_keeps_the_extended_console_type() {
1304 let mut h = ines_header(2, 1, 0, 0);
1308 h[7] = 0x08 | 0x03;
1309 for ext in 0..16u8 {
1310 h[13] = ext;
1311 let mut p = parse_header(&h).unwrap();
1312 p.mapper_id = 4;
1313 let out = serialize_header_preserving(&p, &h);
1314 assert_eq!(out[13], ext, "extended console type {ext}");
1315 assert_eq!(parse_header(&out).unwrap().mapper_id, 4);
1316 }
1317 }
1318
1319 #[test]
1320 fn editing_the_vs_hardware_type_rewrites_only_its_nibble() {
1321 let mut h = ines_header(2, 1, 0, 0);
1322 h[7] = 0x08 | 0x01;
1323 h[13] = 0x32; let mut p = parse_header(&h).unwrap();
1325 assert_eq!(
1326 p.vs_hardware_type,
1327 Some(VsHardwareType::UniSystemSuperXevious)
1328 );
1329 assert!(!p.is_vs_dual_system());
1330 p.vs_hardware_type = Some(VsHardwareType::DualSystem);
1331 assert_eq!(serialize_header_preserving(&p, &h)[13], 0x52);
1332 h[13] = 0x62;
1335 let mut p = parse_header(&h).unwrap();
1336 assert!(p.is_vs_dual_system());
1337 assert_eq!(canonical_header(&p)[13], 0x62);
1338 p.vs_hardware_type = Some(VsHardwareType::UniSystem);
1339 assert_eq!(serialize_header_preserving(&p, &h)[13], 0x02);
1340 h[13] = 0xB2;
1342 let p = parse_header(&h).unwrap();
1343 assert_eq!(p.vs_hardware_type, Some(VsHardwareType::Reserved(0xB)));
1344 assert_eq!(canonical_header(&p)[13], 0xB2);
1345 }
1346
1347 #[test]
1348 fn editing_the_extended_console_type_rewrites_only_its_nibble() {
1349 let mut h = ines_header(2, 1, 0, 0);
1350 h[7] = 0x08 | 0x03; h[13] = 0xF7; let mut p = parse_header(&h).unwrap();
1353 assert_eq!(p.extended_console_type, Some(ExtendedConsoleType::Vt03));
1354 assert_eq!(p.vs_hardware_type, None);
1355 p.extended_console_type = Some(ExtendedConsoleType::Um6578);
1356 assert_eq!(serialize_header_preserving(&p, &h)[13], 0xFB);
1357 assert_eq!(canonical_header(&p)[13], 0x0B);
1358 }
1359
1360 fn changed(a: &[u8; 16], b: &[u8; 16]) -> [u8; 16] {
1362 core::array::from_fn(|i| a[i] ^ b[i])
1363 }
1364
1365 #[test]
1366 fn each_edit_rewrites_only_its_own_bits() {
1367 let mut h = [0xFFu8; 16];
1370 h[..4].copy_from_slice(&MAGIC);
1371 h[4] = 0x02; h[5] = 0x01; h[6] = 0x10; h[7] = 0xF9; h[8] = 0x30; h[9] = 0x00; h[10] = 0x77; h[11] = 0x77; h[12] = 0xFD; h[13] = 0x31; h[14] = 0xFF; h[15] = 0xFF; let base = parse_header(&h).unwrap();
1384
1385 #[allow(clippy::type_complexity)]
1386 let edits: [(&str, fn(&mut Header), [u8; 16]); 16] = [
1387 (
1388 "mapper",
1389 |p| p.mapper_id = 0x2A4,
1390 mask(&[(6, 0xF0), (7, 0xF0), (8, 0x0F)]),
1391 ),
1392 (
1393 "mirroring",
1394 |p| p.mirroring = Mirroring::Vertical,
1395 mask(&[(6, 0x01)]),
1396 ),
1397 ("battery", |p| p.has_battery = true, mask(&[(6, 0x02)])),
1398 ("trainer", |p| p.has_trainer = true, mask(&[(6, 0x04)])),
1399 (
1400 "prg",
1401 |p| p.prg_size = 0x123 * PRG_UNIT,
1402 mask(&[(4, 0xFF), (9, 0x0F)]),
1403 ),
1404 (
1405 "chr",
1406 |p| p.chr_size = 0x201 * CHR_UNIT,
1407 mask(&[(5, 0xFF), (9, 0xF0)]),
1408 ),
1409 ("submapper", |p| p.submapper = 9, mask(&[(8, 0xF0)])),
1410 ("prg-ram", |p| p.prg_ram_size = 2048, mask(&[(10, 0x0F)])),
1411 (
1412 "prg-nvram",
1413 |p| p.prg_nvram_size = 1024,
1414 mask(&[(10, 0xF0)]),
1415 ),
1416 ("chr-ram", |p| p.chr_ram_size = 4096, mask(&[(11, 0x0F)])),
1417 ("chr-nvram", |p| p.chr_nvram_size = 0, mask(&[(11, 0xF0)])),
1418 ("region", |p| p.region = Region::Dendy, mask(&[(12, 0x03)])),
1419 (
1420 "vs-hardware",
1421 |p| p.vs_hardware_type = Some(VsHardwareType::DualSystem),
1422 mask(&[(13, 0xF0)]),
1423 ),
1424 ("misc-roms", |p| p.misc_rom_count = 1, mask(&[(14, 0x03)])),
1425 (
1426 "expansion",
1427 |p| p.default_expansion_device = ExpansionDevice::Zapper4017,
1428 mask(&[(15, 0x7F)]),
1429 ),
1430 (
1431 "prg-exponent",
1432 |p| p.prg_size = 3 << 12, mask(&[(4, 0xFF), (9, 0x0F)]),
1434 ),
1435 ];
1436 for (name, edit, allowed) in edits {
1437 let mut p = base;
1438 edit(&mut p);
1439 let out = serialize_header_preserving(&p, &h);
1440 let diff = changed(&h, &out);
1441 for i in 0..16 {
1442 assert_eq!(diff[i] & !allowed[i], 0, "{name}: byte {i} {out:02x?}");
1443 }
1444 assert_ne!(diff, [0; 16], "{name}: the edit was not written");
1445 let back = parse_header(&out).unwrap();
1447 assert_eq!(serialize_header_preserving(&p, &out), out, "{name}");
1448 assert_eq!(back, p, "{name}");
1449 }
1450 }
1451
1452 fn mask(bits: &[(usize, u8)]) -> [u8; 16] {
1453 let mut m = [0u8; 16];
1454 for &(i, b) in bits {
1455 m[i] |= b;
1456 }
1457 m
1458 }
1459
1460 #[test]
1461 fn preserving_leaves_nes2_only_fields_alone_on_ines() {
1462 let mut h = ines_header(2, 1, 1, 0);
1465 h[7] |= 0x01; h[8..].copy_from_slice(b"DiskDude");
1467 let mut p = parse_header(&h).unwrap();
1468 p.region = Region::Pal;
1469 p.submapper = 5;
1470 p.console_type = ConsoleType::Playchoice10;
1471 assert_eq!(serialize_header_preserving(&p, &h), h);
1472 p.has_battery = true;
1473 let out = serialize_header_preserving(&p, &h);
1474 assert_eq!(changed(&h, &out), mask(&[(6, 0x02)]));
1475 }
1476
1477 #[test]
1478 fn a_format_change_or_unparsable_original_encodes_canonically() {
1479 let h = ines_header(2, 1, 1, 0);
1480 let mut p = parse_header(&h).unwrap();
1481 p.is_nes2 = true;
1482 assert_eq!(serialize_header_preserving(&p, &h), canonical_header(&p));
1483 let mut bad = h;
1484 bad[0] = b'X';
1485 let p = parse_header(&h).unwrap();
1486 assert_eq!(serialize_header_preserving(&p, &bad), canonical_header(&p));
1487 }
1488
1489 #[test]
1490 fn canonical_encoding_round_trips_every_mapper_id() {
1491 for is_nes2 in [false, true] {
1497 let top = if is_nes2 { 4095 } else { 255 };
1498 for id in 0..=top {
1499 let mut h = ines_header(2, 1, 0, 0);
1500 if is_nes2 {
1501 h[7] = 0x08;
1502 }
1503 let mut p = parse_header(&h).unwrap();
1504 p.mapper_id = id;
1505 let back = parse_header(&canonical_header(&p)).unwrap();
1506 assert_eq!(back.mapper_id, id, "nes2={is_nes2}");
1507 }
1508 }
1509 }
1510
1511 #[test]
1512 fn a_console_change_re_encodes_byte_13() {
1513 let mut h = ines_header(2, 1, 0, 0);
1514 h[7] = 0x08 | 0x03; h[13] = 0x0B; let mut p = parse_header(&h).unwrap();
1517 p.console_type = ConsoleType::Nes;
1518 let out = serialize_header_preserving(&p, &h);
1519 assert_eq!((out[7] & 0x03, out[13]), (0, 0));
1520 }
1521
1522 #[test]
1523 fn rejects_bad_magic() {
1524 let mut h = ines_header(2, 1, 0, 0);
1525 h[0] = b'X';
1526 assert!(matches!(parse_header(&h), Err(RomError::BadMagic)));
1527 }
1528
1529 #[test]
1530 fn truncated_header() {
1531 let bytes = *b"NES";
1532 assert!(matches!(
1533 parse_header(&bytes),
1534 Err(RomError::Truncated { needed: 16, got: 3 })
1535 ));
1536 }
1537
1538 #[test]
1539 fn ines_basic_nrom_horizontal() {
1540 let h = ines_header(2, 1, 0, 0); let p = parse_header(&h).unwrap();
1542 assert!(!p.is_nes2);
1543 assert_eq!(p.mapper_id, 0);
1544 assert_eq!(p.prg_size, 32 * 1024);
1545 assert_eq!(p.chr_size, 8 * 1024);
1546 assert_eq!(p.mirroring, Mirroring::Horizontal);
1547 assert!(!p.has_battery);
1548 assert!(!p.has_trainer);
1549 assert_eq!(p.region, Region::Ntsc);
1550 }
1551
1552 #[test]
1553 fn ines_mapper_assembly() {
1554 let h = ines_header(1, 0, 1, 0);
1556 assert_eq!(parse_header(&h).unwrap().mapper_id, 1);
1557 let h = ines_header(1, 0, 4, 0);
1559 assert_eq!(parse_header(&h).unwrap().mapper_id, 4);
1560 let h = ines_header(1, 0, 0xCD, 0);
1562 assert_eq!(parse_header(&h).unwrap().mapper_id, 0xCD);
1563 }
1564
1565 #[test]
1566 fn nes2_vs_dualsystem_detected_from_byte13_high_nibble() {
1567 let mut h = ines_header(2, 1, 0, 0);
1569 h[7] = 0x08 | 0x01;
1570 h[13] = 0x50; let p = parse_header(&h).unwrap();
1573 assert!(p.is_nes2);
1574 assert_eq!(p.console_type, ConsoleType::VsSystem);
1575 assert!(p.is_vs_dual_system());
1576 h[13] = 0x60;
1578 assert!(parse_header(&h).unwrap().is_vs_dual_system());
1579 h[13] = 0x00;
1581 assert!(!parse_header(&h).unwrap().is_vs_dual_system());
1582 h[13] = 0x40;
1583 assert!(!parse_header(&h).unwrap().is_vs_dual_system());
1584 h[7] = 0x08; h[13] = 0x50;
1587 assert!(!parse_header(&h).unwrap().is_vs_dual_system());
1588 let mut h1 = ines_header(2, 1, 0, 0);
1590 h1[13] = 0x50;
1591 assert!(!parse_header(&h1).unwrap().is_vs_dual_system());
1592 }
1593
1594 #[test]
1595 fn vs_dualsystem_round_trips_through_serialize() {
1596 let mut h = ines_header(2, 1, 0, 0);
1597 h[7] = 0x08 | 0x01; h[13] = 0x60; let parsed = parse_header(&h).unwrap();
1600 assert!(parsed.is_vs_dual_system());
1601 let out = canonical_header(&parsed);
1602 let reparsed = parse_header(&out).unwrap();
1603 assert!(reparsed.is_vs_dual_system());
1605 assert_eq!(reparsed.console_type, ConsoleType::VsSystem);
1606 }
1607
1608 #[test]
1609 fn ines_vertical_battery_trainer() {
1610 let h = ines_header(2, 1, 0, 0b0111); let p = parse_header(&h).unwrap();
1612 assert_eq!(p.mirroring, Mirroring::Vertical);
1613 assert!(p.has_battery);
1614 assert!(p.has_trainer);
1615 }
1616
1617 #[test]
1618 fn ines_four_screen_overrides_mirroring_bit() {
1619 let h = ines_header(2, 1, 0, 0b1001);
1620 let p = parse_header(&h).unwrap();
1621 assert_eq!(p.mirroring, Mirroring::FourScreen);
1622 assert!(p.four_screen);
1623 }
1624
1625 #[test]
1626 fn ines_chr_ram_when_chr_size_zero() {
1627 let h = ines_header(2, 0, 0, 0);
1628 let p = parse_header(&h).unwrap();
1629 assert_eq!(p.chr_size, 0);
1630 assert_eq!(p.chr_ram_size, 8 * 1024);
1631 }
1632
1633 #[test]
1634 fn nes2_detection_and_extended_fields() {
1635 let mut h = [0u8; 16];
1636 h[..4].copy_from_slice(&MAGIC);
1637 h[4] = 1; h[5] = 1; h[6] = 0x10; h[7] = 0x08; h[8] = 0x21; h[9] = 0x00;
1643 h[10] = 0x07; h[11] = 0x00;
1645 h[12] = 0x01; let p = parse_header(&h).unwrap();
1647 assert!(p.is_nes2);
1648 assert_eq!(p.mapper_id, 0x101); assert_eq!(p.submapper, 2);
1650 assert_eq!(p.prg_ram_size, 8 * 1024);
1651 assert_eq!(p.region, Region::Pal);
1652 assert_eq!(p.console_type, ConsoleType::Nes);
1653 }
1654
1655 #[test]
1656 fn nes2_exponent_multiplier_sizing() {
1657 let mut h = [0u8; 16];
1658 h[..4].copy_from_slice(&MAGIC);
1659 h[4] = 16 << 2;
1661 h[5] = 0;
1662 h[6] = 0;
1663 h[7] = 0x08; h[8] = 0;
1665 h[9] = 0x0F; let p = parse_header(&h).unwrap();
1667 assert_eq!(p.prg_size, 65536);
1668 }
1669
1670 #[test]
1671 fn round_trip_ines_header() {
1672 let h = ines_header(2, 1, 4, 0b0011); let parsed = parse_header(&h).unwrap();
1674 let again = canonical_header(&parsed);
1675 assert_eq!(&h[0..8], &again[0..8]);
1676 }
1677
1678 #[test]
1679 fn round_trip_nes2_header() {
1680 let mut h = [0u8; 16];
1681 h[..4].copy_from_slice(&MAGIC);
1682 h[4] = 2;
1683 h[5] = 1;
1684 h[6] = 0x41; h[7] = 0x08; h[8] = 0x10; h[9] = 0x00;
1688 h[10] = 0x07;
1689 h[11] = 0x00;
1690 h[12] = 0x01;
1691 h[14] = 0x01; h[15] = 0x2A; let parsed = parse_header(&h).unwrap();
1694 assert_eq!(parsed.misc_rom_count, 1);
1695 assert_eq!(parsed.default_expansion_device, ExpansionDevice::Multicart);
1696 assert_eq!(canonical_header(&parsed), h);
1699 }
1700
1701 #[test]
1702 fn nes_cart_has_no_vs_ppu_type() {
1703 let mut h = [0u8; 16];
1705 h[..4].copy_from_slice(&MAGIC);
1706 h[4] = 1;
1707 h[5] = 1;
1708 h[7] = 0x08; h[13] = 0x09; let p = parse_header(&h).unwrap();
1711 assert_eq!(p.console_type, ConsoleType::Nes);
1712 assert_eq!(p.vs_ppu_type, VsPpuType::None);
1713 }
1714
1715 #[test]
1716 fn vs_byte13_parses_ppu_type() {
1717 let mk = |nibble: u8| {
1719 let mut h = [0u8; 16];
1720 h[..4].copy_from_slice(&MAGIC);
1721 h[4] = 1;
1722 h[5] = 1;
1723 h[7] = 0x09; h[13] = nibble;
1725 parse_header(&h).unwrap()
1726 };
1727 assert_eq!(mk(0x0).vs_ppu_type, VsPpuType::Rp2C03);
1728 assert_eq!(mk(0x2).vs_ppu_type, VsPpuType::Rp2C04_0001);
1729 assert_eq!(mk(0x5).vs_ppu_type, VsPpuType::Rp2C04_0004);
1730 assert_eq!(mk(0x9).vs_ppu_type, VsPpuType::Rc2C05_02);
1731 let t = mk(0x9).vs_ppu_type;
1733 assert_eq!(t.ppu_palette(), crate::cartridge::VsPpuPalette::Rgb2C05);
1734 assert!(t.is_2c05());
1735 assert_eq!(t.ppu_2c05_id(), 0x3D);
1736 let mut h = [0u8; 16];
1738 h[..4].copy_from_slice(&MAGIC);
1739 h[7] = 0x09;
1740 h[13] = 0x52; assert_eq!(
1742 parse_header(&h).unwrap().vs_ppu_type,
1743 VsPpuType::Rp2C04_0001
1744 );
1745 }
1746
1747 #[test]
1748 fn vs_byte13_round_trips() {
1749 let mut h = [0u8; 16];
1750 h[..4].copy_from_slice(&MAGIC);
1751 h[4] = 1;
1752 h[5] = 1;
1753 h[7] = 0x09; h[13] = 0x0A; let parsed = parse_header(&h).unwrap();
1756 assert_eq!(parsed.vs_ppu_type, VsPpuType::Rc2C05_03);
1757 let again = canonical_header(&parsed);
1758 assert_eq!(again[13] & 0x0F, 0x0A);
1759 }
1760
1761 #[test]
1762 fn ines1_garbage_tail_masks_the_mapper_high_nibble() {
1763 let mut h = ines_header(1, 1, 0, 0x01);
1773 h[7..16].copy_from_slice(b"@iskDude!");
1774 assert_eq!(parse_header(&h).unwrap().mapper_id, 0);
1775 let mut h = ines_header(8, 16, 1, 0);
1777 h[7..16].copy_from_slice(b"DiskDude!");
1778 assert_eq!(parse_header(&h).unwrap().mapper_id, 1);
1779 let h = ines_header(8, 8, 64, 0);
1781 assert_eq!(parse_header(&h).unwrap().mapper_id, 64);
1782 let mut h = ines_header(8, 8, 64, 0);
1785 h[8..12].copy_from_slice(b"junk");
1786 assert_eq!(parse_header(&h).unwrap().mapper_id, 64);
1787 let mut h = ines_header(8, 8, 64, 0);
1789 h[7] |= 0x08;
1790 h[12] = 0x01; h[15] = 0x01; assert_eq!(parse_header(&h).unwrap().mapper_id, 64);
1793 }
1794
1795 #[test]
1801 fn preserving_mapper_edit_reads_back_over_a_dirty_tail() {
1802 let mut h = ines_header(8, 1, 2, 0);
1803 h[7..16].copy_from_slice(b"DiskDude!");
1804 let mut edited = parse_header(&h).unwrap();
1805 assert_eq!(edited.mapper_id, 2);
1806 edited.mapper_id = 66;
1807 let out = serialize_header_preserving(&edited, &h);
1808 assert_eq!(parse_header(&out).unwrap().mapper_id, 66);
1809 edited.mapper_id = 3;
1811 let out = serialize_header_preserving(&edited, &h);
1812 assert_eq!(parse_header(&out).unwrap().mapper_id, 3);
1813 assert_eq!(&out[12..16], &h[12..16]);
1814 }
1815
1816 #[test]
1817 fn ram_shift_helper_zero_returns_zero() {
1818 assert_eq!(ram_size_from_shift(0), 0);
1819 }
1820
1821 #[test]
1822 fn ram_shift_helper_round_trip() {
1823 assert_eq!(ram_size_from_shift(7), 8192);
1825 assert_eq!(ram_shift_for(8192), 7);
1826 assert_eq!(ram_size_from_shift(10), 65536);
1828 assert_eq!(ram_shift_for(65536), 10);
1829 }
1830}