Skip to main content

rustynes_mappers/
header.rs

1//! Header parser shared between iNES 1.0 and NES 2.0 paths.
2//!
3//! Encoding rules follow `docs/cartridge-format.md` §Header layout.
4
5use crate::cartridge::{ConsoleType, Mirroring, Region, RomError, VsPpuType};
6use alloc::format;
7
8/// Magic bytes of an iNES / NES 2.0 file: `"NES\x1A"`.
9pub const MAGIC: [u8; 4] = [b'N', b'E', b'S', 0x1A];
10
11/// Header length in bytes.
12pub const HEADER_LEN: usize = 16;
13
14/// 16 KiB PRG-ROM unit size.
15pub const PRG_UNIT: usize = 16 * 1024;
16
17/// 8 KiB CHR-ROM unit size.
18pub const CHR_UNIT: usize = 8 * 1024;
19
20/// 512-byte trainer block size (when present).
21pub const TRAINER_LEN: usize = 512;
22
23/// Vs. System hardware type: NES 2.0 header byte 13, high nibble, when the
24/// console type (byte 7 bits 0-1) is 1.
25///
26/// Values per the Nesdev wiki's "NES 2.0" page, §"Vs. System Type". Types 0-4 are
27/// the single-board Vs. `UniSystem` with its non-PPU copy-protection variant;
28/// 5 and 6 are the two-CPU / two-PPU Vs. `DualSystem`. Values 7-15 are not
29/// assigned and are kept, unaltered, in [`VsHardwareType::Reserved`].
30#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
31#[non_exhaustive]
32pub enum VsHardwareType {
33    /// `$0`: Vs. `UniSystem` (normal).
34    UniSystem,
35    /// `$1`: Vs. `UniSystem`, RBI Baseball protection.
36    UniSystemRbiBaseball,
37    /// `$2`: Vs. `UniSystem`, TKO Boxing protection.
38    UniSystemTkoBoxing,
39    /// `$3`: Vs. `UniSystem`, Super Xevious protection.
40    UniSystemSuperXevious,
41    /// `$4`: Vs. `UniSystem`, Vs. Ice Climber Japan protection.
42    UniSystemIceClimberJapan,
43    /// `$5`: Vs. `DualSystem` (normal).
44    DualSystem,
45    /// `$6`: Vs. `DualSystem`, Raid on Bungeling Bay protection.
46    DualSystemRaidOnBungelingBay,
47    /// `$7-$F`: unassigned. The value is the nibble itself (`7..=15`).
48    Reserved(u8),
49}
50
51impl VsHardwareType {
52    /// Decode the byte-13 high nibble (`nibble` is masked to four bits).
53    #[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    /// The byte-13 high nibble this type encodes to (`0..=15`). A
68    /// [`Self::Reserved`] value outside `7..=15` is masked to four bits.
69    #[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    /// True for the two Vs. `DualSystem` types (5 and 6).
84    #[must_use]
85    pub const fn is_dual_system(self) -> bool {
86        matches!(self, Self::DualSystem | Self::DualSystemRaidOnBungelingBay)
87    }
88}
89
90/// Extended console type: NES 2.0 header byte 13, low nibble, when the
91/// console type (byte 7 bits 0-1) is 3.
92///
93/// Values per the Nesdev wiki's "NES 2.0" page, §"Extended Console Type". `$0-$2`
94/// duplicate what byte 7 can already say and exist so a console-type variable
95/// can fold both fields together; they are kept distinct here so a header that
96/// uses them round-trips. `$D-$F` are reserved and kept in
97/// [`ExtendedConsoleType::Reserved`].
98#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
99#[non_exhaustive]
100pub enum ExtendedConsoleType {
101    /// `$0`: regular NES / Famicom / Dendy.
102    Regular,
103    /// `$1`: Nintendo Vs. System.
104    VsSystem,
105    /// `$2`: PlayChoice-10.
106    Playchoice10,
107    /// `$3`: regular Famiclone with a CPU that supports decimal mode.
108    DecimalModeFamiclone,
109    /// `$4`: regular NES / Famicom with an EPSM module or plug-through cartridge.
110    Epsm,
111    /// `$5`: V.R. Technology VT01 with red/cyan STN palette.
112    Vt01,
113    /// `$6`: V.R. Technology VT02.
114    Vt02,
115    /// `$7`: V.R. Technology VT03.
116    Vt03,
117    /// `$8`: V.R. Technology VT09.
118    Vt09,
119    /// `$9`: V.R. Technology VT32.
120    Vt32,
121    /// `$A`: V.R. Technology VT369.
122    Vt369,
123    /// `$B`: UMC UM6578.
124    Um6578,
125    /// `$C`: Famicom Network System.
126    FamicomNetworkSystem,
127    /// `$D-$F`: reserved. The value is the nibble itself (`13..=15`).
128    Reserved(u8),
129}
130
131impl ExtendedConsoleType {
132    /// Decode the byte-13 low nibble (`nibble` is masked to four bits).
133    #[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    /// The byte-13 low nibble this type encodes to (`0..=15`).
154    #[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
175/// Generates [`ExpansionDevice`] and its two code conversions from one table,
176/// so the variant list, the decoder and the encoder cannot drift apart.
177macro_rules! expansion_devices {
178    ($( $(#[$doc:meta])* $variant:ident = $code:literal, )*) => {
179        /// Default expansion device: NES 2.0 header byte 15, bits 0-6.
180        ///
181        /// The device the game expects at CPU `$4016`/`$4017`, per the Nesdev
182        /// wiki's "NES 2.0" page, §"Default Expansion Device" (codes `$00-$4F`). An
183        /// unassigned code -- `$06` (withdrawn) or `$50-$7F` -- is kept,
184        /// unaltered, in [`ExpansionDevice::Unassigned`]. The header only
185        /// *describes* the device: nothing in the core selects an input device
186        /// from it.
187        #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
188        #[non_exhaustive]
189        pub enum ExpansionDevice {
190            $( $(#[$doc])* $variant, )*
191            /// A code the page does not assign (`$06`, `$50-$7F`).
192            Unassigned(u8),
193        }
194
195        impl ExpansionDevice {
196            /// Decode header byte 15 (bit 7 is not part of the field and is
197            /// ignored).
198            #[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            /// The 7-bit byte-15 code this device encodes to.
207            #[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    /// `$00`: unspecified (no information).
220    Unspecified = 0x00,
221    /// `$01`: standard NES / Famicom controllers.
222    StandardControllers = 0x01,
223    /// `$02`: NES Four Score / Satellite with two more standard controllers.
224    FourScore = 0x02,
225    /// `$03`: Famicom Four Players Adapter, "simple" protocol.
226    FamicomFourPlayersAdapter = 0x03,
227    /// `$04`: Vs. System, 1P via `$4016`.
228    VsSystem4016 = 0x04,
229    /// `$05`: Vs. System, 1P via `$4017`.
230    VsSystem4017 = 0x05,
231    /// `$07`: Vs. Zapper.
232    VsZapper = 0x07,
233    /// `$08`: Zapper (`$4017`).
234    Zapper4017 = 0x08,
235    /// `$09`: two Zappers.
236    TwoZappers = 0x09,
237    /// `$0A`: Bandai Hyper Shot light gun.
238    BandaiHyperShot = 0x0A,
239    /// `$0B`: Power Pad side A.
240    PowerPadSideA = 0x0B,
241    /// `$0C`: Power Pad side B.
242    PowerPadSideB = 0x0C,
243    /// `$0D`: Family Trainer side A.
244    FamilyTrainerSideA = 0x0D,
245    /// `$0E`: Family Trainer side B.
246    FamilyTrainerSideB = 0x0E,
247    /// `$0F`: Arkanoid Vaus controller (NES).
248    VausNes = 0x0F,
249    /// `$10`: Arkanoid Vaus controller (Famicom).
250    VausFamicom = 0x10,
251    /// `$11`: two Vaus controllers plus Famicom Data Recorder.
252    TwoVausPlusDataRecorder = 0x11,
253    /// `$12`: Konami Hyper Shot controller.
254    KonamiHyperShot = 0x12,
255    /// `$13`: Coconuts Pachinko controller.
256    CoconutsPachinko = 0x13,
257    /// `$14`: Exciting Boxing punching bag.
258    ExcitingBoxingPunchingBag = 0x14,
259    /// `$15`: Jissen Mahjong controller.
260    JissenMahjong = 0x15,
261    /// `$16`: Yonezawa Party Tap.
262    PartyTap = 0x16,
263    /// `$17`: Oeka Kids tablet.
264    OekaKidsTablet = 0x17,
265    /// `$18`: Sunsoft Barcode Battler.
266    BarcodeBattler = 0x18,
267    /// `$19`: Miracle Piano keyboard.
268    MiraclePiano = 0x19,
269    /// `$1A`: Pokkun Moguraa tap-tap mat.
270    PokkunMoguraa = 0x1A,
271    /// `$1B`: Top Rider handlebars.
272    TopRider = 0x1B,
273    /// `$1C`: double-fisted (two controllers per player).
274    DoubleFisted = 0x1C,
275    /// `$1D`: Famicom 3D System.
276    Famicom3dSystem = 0x1D,
277    /// `$1E`: Doremikko keyboard.
278    DoremikkoKeyboard = 0x1E,
279    /// `$1F`: R.O.B. Gyromite.
280    RobGyromite = 0x1F,
281    /// `$20`: Famicom Data Recorder ("silent" keyboard).
282    FamicomDataRecorder = 0x20,
283    /// `$21`: ASCII Turbo File.
284    AsciiTurboFile = 0x21,
285    /// `$22`: IGS Storage Battle Box.
286    IgsBattleBox = 0x22,
287    /// `$23`: Family BASIC keyboard plus Famicom Data Recorder.
288    FamilyBasicKeyboard = 0x23,
289    /// `$24`: Dongda PEC keyboard.
290    DongdaPecKeyboard = 0x24,
291    /// `$25`: Bit Corp. Bit-79 keyboard.
292    Bit79Keyboard = 0x25,
293    /// `$26`: Subor keyboard.
294    SuborKeyboard = 0x26,
295    /// `$27`: Subor keyboard plus Macro Winners mouse.
296    SuborKeyboardMacroWinnersMouse = 0x27,
297    /// `$28`: Subor keyboard plus Subor mouse via `$4016`.
298    SuborKeyboardSuborMouse4016 = 0x28,
299    /// `$29`: SNES mouse (`$4016`).
300    SnesMouse4016 = 0x29,
301    /// `$2A`: multicart.
302    Multicart = 0x2A,
303    /// `$2B`: two SNES controllers replacing the two standard controllers.
304    TwoSnesControllers = 0x2B,
305    /// `$2C`: `RacerMate` bicycle.
306    RacerMateBicycle = 0x2C,
307    /// `$2D`: U-Force.
308    UForce = 0x2D,
309    /// `$2E`: R.O.B. Stack-Up.
310    RobStackUp = 0x2E,
311    /// `$2F`: City Patrolman light gun.
312    CityPatrolmanLightgun = 0x2F,
313    /// `$30`: Sharp C1 cassette interface.
314    SharpC1CassetteInterface = 0x30,
315    /// `$31`: standard controller with swapped Left-Right / Up-Down / B-A.
316    SwappedStandardController = 0x31,
317    /// `$32`: Excalibur Sudoku pad.
318    ExcaliburSudokuPad = 0x32,
319    /// `$33`: ABL Pinball.
320    AblPinball = 0x33,
321    /// `$34`: Golden Nugget Casino extra buttons.
322    GoldenNuggetCasino = 0x34,
323    /// `$35`: Keda keyboard.
324    KedaKeyboard = 0x35,
325    /// `$36`: Subor keyboard plus Subor mouse via `$4017`.
326    SuborKeyboardSuborMouse4017 = 0x36,
327    /// `$37`: port test controller.
328    PortTestController = 0x37,
329    /// `$38`: Bandai Multi Game Player gamepad buttons.
330    BandaiMultiGamePlayer = 0x38,
331    /// `$39`: Venom TV Dance Mat.
332    VenomTvDanceMat = 0x39,
333    /// `$3A`: LG TV remote control.
334    LgTvRemote = 0x3A,
335    /// `$3B`: Famicom Network Controller.
336    FamicomNetworkController = 0x3B,
337    /// `$3C`: King Fishing controller.
338    KingFishing = 0x3C,
339    /// `$3D`: Croaky Karaoke controller.
340    CroakyKaraoke = 0x3D,
341    /// `$3E`: Kingwon keyboard.
342    KingwonKeyboard = 0x3E,
343    /// `$3F`: Zecheng keyboard.
344    ZechengKeyboard = 0x3F,
345    /// `$40`: Subor keyboard plus L90-rotated PS/2 mouse in `$4017`.
346    SuborKeyboardL90Ps2Mouse = 0x40,
347    /// `$41`: PS/2 keyboard in the UM6578 PS/2 port, PS/2 mouse via `$4017`.
348    Um6578Ps2KeyboardAndMouse = 0x41,
349    /// `$42`: PS/2 mouse in the UM6578 PS/2 port.
350    Um6578Ps2Mouse = 0x42,
351    /// `$43`: Yuxing mouse via `$4016`.
352    YuxingMouse = 0x43,
353    /// `$44`: Subor keyboard plus Yuxing mouse in `$4016`.
354    SuborKeyboardYuxingMouse = 0x44,
355    /// `$45`: Gigggle TV Pump.
356    GiggleTvPump = 0x45,
357    /// `$46`: BBK keyboard plus R90-rotated PS/2 mouse in `$4017`.
358    BbkKeyboardR90Ps2Mouse = 0x46,
359    /// `$47`: Magical Cooking.
360    MagicalCooking = 0x47,
361    /// `$48`: SNES mouse (`$4017`).
362    SnesMouse4017 = 0x48,
363    /// `$49`: Zapper (`$4016`).
364    Zapper4016 = 0x49,
365    /// `$4A`: Arkanoid Vaus controller (prototype).
366    VausPrototype = 0x4A,
367    /// `$4B`: TV Mahjong Game controller.
368    TvMahjongGame = 0x4B,
369    /// `$4C`: Mahjong Gekitou Densetsu controller.
370    MahjongGekitouDensetsu = 0x4C,
371    /// `$4D`: Subor keyboard plus X-inverted PS/2 mouse in `$4017`.
372    SuborKeyboardXInvertedPs2Mouse = 0x4D,
373    /// `$4E`: IBM PC/XT keyboard.
374    IbmPcXtKeyboard = 0x4E,
375    /// `$4F`: Subor keyboard plus Mega Book mouse.
376    SuborKeyboardMegaBookMouse = 0x4F,
377}
378
379/// Parsed header view, format-detected.
380///
381/// Every field a 16-byte iNES / NES 2.0 header defines is modelled (since
382/// v2.9.8): what is left over are the reserved bits (byte 12 bits 2-7, byte 13
383/// for console types 0 and 2, byte 13 bits 4-7 for console type 3, byte 14
384/// bits 2-7, byte 15 bit 7) and, on an iNES 1.0 header, bytes 8-15, which are
385/// not part of that format. [`serialize_header_preserving`] keeps all of those.
386///
387/// **iNES 1.0.** The NES 2.0-only fields hold a fixed value on an iNES 1.0
388/// header, whatever its bytes 8-15 contain (old dumpers wrote signatures
389/// there): `submapper` 0, `region` NTSC, `console_type` NES,
390/// `vs_hardware_type` / `extended_console_type` `None`, `vs_ppu_type`
391/// [`VsPpuType::None`], `misc_rom_count` 0, `default_expansion_device`
392/// [`ExpansionDevice::Unspecified`], both NVRAM sizes 0, and the two RAM sizes
393/// the nominal values described on each field.
394///
395/// `#[non_exhaustive]`: outside this crate, build one with [`Header::default`]
396/// and field assignment (or [`parse_header`]), so a field added later is not
397/// an API break.
398///
399/// The 5 boolean flags directly mirror the iNES / NES 2.0 wire format and so
400/// are not refactorable into an enum without losing parser fidelity.
401#[derive(Debug, Clone, Copy, PartialEq, Eq)]
402#[allow(clippy::struct_excessive_bools)]
403#[non_exhaustive]
404pub struct Header {
405    /// True if the file is NES 2.0 (header byte 7 bits 2-3 == `10`).
406    pub is_nes2: bool,
407    /// 12-bit mapper id (iNES 1.0 fills only the low 8 bits).
408    pub mapper_id: u16,
409    /// 4-bit submapper id (NES 2.0 only; 0 on iNES 1.0).
410    pub submapper: u8,
411    /// PRG-ROM size in bytes.
412    pub prg_size: usize,
413    /// CHR-ROM size in bytes (0 if cart uses CHR-RAM).
414    pub chr_size: usize,
415    /// Effective initial mirroring.
416    pub mirroring: Mirroring,
417    /// Region from NES 2.0 byte 12; defaults to NTSC for iNES 1.0.
418    pub region: Region,
419    /// Console type from NES 2.0 byte 7; always [`ConsoleType::Nes`] for iNES 1.0.
420    pub console_type: ConsoleType,
421    /// Vs. System PPU type from NES 2.0 byte 13 low nibble, valid only when
422    /// `console_type == ConsoleType::VsSystem` (otherwise [`VsPpuType::None`]).
423    /// Resolves to the output palette + 2C05 quirks via [`VsPpuType::ppu_palette`]
424    /// / [`VsPpuType::is_2c05`]. The reserved nibbles (`$1`, `$6`, `$7`,
425    /// `$C-$F`) decode as [`VsPpuType::Rp2C03`], so they do not survive a
426    /// canonical re-encode; [`serialize_header_preserving`] keeps them.
427    pub vs_ppu_type: VsPpuType,
428    /// Vs. hardware type from NES 2.0 byte 13 high nibble: `Some` exactly when
429    /// the header is NES 2.0 and `console_type == ConsoleType::VsSystem`.
430    /// Types 5 and 6 are the Vs. `DualSystem` boards; see
431    /// [`Header::is_vs_dual_system`].
432    pub vs_hardware_type: Option<VsHardwareType>,
433    /// Extended console type from NES 2.0 byte 13 low nibble: `Some` exactly
434    /// when the header is NES 2.0 and `console_type == ConsoleType::Extended`.
435    pub extended_console_type: Option<ExtendedConsoleType>,
436    /// Number of miscellaneous ROMs present (NES 2.0 byte 14 bits 0-1, so
437    /// `0..=3`; 0 on iNES 1.0). The miscellaneous ROM area itself is whatever
438    /// follows CHR-ROM in the file.
439    pub misc_rom_count: u8,
440    /// Default expansion device (NES 2.0 byte 15 bits 0-6;
441    /// [`ExpansionDevice::Unspecified`] on iNES 1.0).
442    pub default_expansion_device: ExpansionDevice,
443    /// Volatile PRG-RAM size in bytes: NES 2.0 byte 10 low nibble. iNES 1.0
444    /// has no size field, so it reports a nominal 8 KiB.
445    ///
446    /// Until v2.9.8 this field held the volatile **and** non-volatile sizes
447    /// summed; that total is now [`Header::prg_ram_window`].
448    pub prg_ram_size: u32,
449    /// Non-volatile (battery-backed) PRG-RAM / EEPROM size in bytes: NES 2.0
450    /// byte 10 high nibble. 0 on iNES 1.0, which has no NVRAM split (whether
451    /// its nominal RAM is battery-backed is [`Header::has_battery`]).
452    pub prg_nvram_size: u32,
453    /// Volatile CHR-RAM size in bytes: NES 2.0 byte 11 low nibble. iNES 1.0
454    /// reports a nominal 8 KiB when there is no CHR-ROM, otherwise 0.
455    pub chr_ram_size: u32,
456    /// Non-volatile CHR-RAM size in bytes: NES 2.0 byte 11 high nibble. 0 on
457    /// iNES 1.0. No board in this crate allocates CHR-NVRAM from it.
458    pub chr_nvram_size: u32,
459    /// True when battery-backed PRG-RAM is present (`header[6]` bit 1).
460    pub has_battery: bool,
461    /// True when a 512-byte trainer follows the header (`header[6]` bit 2).
462    pub has_trainer: bool,
463    /// True when bit 3 of `header[6]` forces four-screen mode.
464    pub four_screen: bool,
465}
466
467impl Default for Header {
468    /// An iNES 1.0 header for mapper 0 with no ROM, horizontal mirroring, and
469    /// every other field at the value [`parse_header`] gives an iNES 1.0 file
470    /// with all-zero bytes 4-15, so `parse_header(&canonical(default))` is
471    /// the default again.
472    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    /// The whole PRG-RAM window a board allocates at `$6000-$7FFF`: the
500    /// volatile and non-volatile sizes together. Some carts (*`StarTropics`* /
501    /// MMC6) declare their save RAM only in the NVRAM nibble, so reading the
502    /// volatile size alone would leave them with none.
503    #[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    /// True when the header names a Vs. `DualSystem` board (Vs. hardware type
509    /// 5 or 6). Drives DUAL-system *detection*; see `docs/cartridge-format.md`.
510    #[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
519/// The PRG-RAM size an iNES 1.0 header reports, having no field for it: 8 KiB,
520/// so the common save-RAM mappers (MMC1, MMC3, MMC5) get a plausible window.
521const INES1_NOMINAL_PRG_RAM: u32 = 8 * 1024;
522
523/// The CHR-RAM size an iNES 1.0 header reports when it has no CHR-ROM.
524const INES1_NOMINAL_CHR_RAM: u32 = 8 * 1024;
525
526/// Assemble the mapper number from header bytes 6-8 (see the comment
527/// inside for the iNES 1.0 dirty-tail rule).
528fn mapper_number(h: &[u8; HEADER_LEN], is_nes2: bool) -> u16 {
529    // Mapper assembly:
530    //   bits 0..=3 from header[6] high nibble,
531    //   bits 4..=7 from header[7] high nibble,
532    //   bits 8..=11 from header[8] low nibble (NES 2.0 only).
533    //
534    // iNES 1.0 only: old ROM tools wrote signatures ("DiskDude!" and its
535    // variants) into bytes 7-15, which the original iNES emulator ignored.
536    // Byte 7's high nibble then reads as mapper bits 4-7 and adds 64 (for
537    // 'D' = 0x44) to the mapper number. The NESdev "iNES" page gives the rule
538    // applied here: if bytes 12-15 are not all zero and the header is not NES
539    // 2.0, mask off the upper four bits of the mapper number. A clean iNES 1.0
540    // header always has zeros there, so a well-formed dump of any mapper from
541    // 16 to 255 is unaffected; NES 2.0 headers are exempt because bytes 12-15
542    // carry real fields. Byte 7's low nibble is already ignored on the iNES
543    // 1.0 path (console type and the NES 2.0 marker), so nothing else in the
544    // tail is read.
545    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
560/// Parse a 16-byte header into a [`Header`].
561///
562/// # Errors
563///
564/// Returns [`RomError::Truncated`] if `bytes` is < 16 bytes; [`RomError::BadMagic`]
565/// if the magic does not match. Header-internal inconsistencies are returned as
566/// [`RomError::InvalidConfig`].
567pub 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    // PRG / CHR sizing.
585    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    // Mirroring.
597    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    // Region (NES 2.0 byte 12 bits 0-1).
607    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        // iNES 1.0 has only the unreliable byte 9 bit 0; assume NTSC.
617        Region::Ntsc
618    };
619
620    // Console type (NES 2.0 byte 7 bits 0-1).
621    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    // Vs. System PPU type (NES 2.0 byte 13 low nibble, only when console = Vs).
634    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    // RAM sizes. NES 2.0 bytes 10-11: low nibble = volatile shift, high nibble
643    // = non-volatile shift, each `64 << shift` bytes (0 = none). A board's
644    // PRG-RAM window is the two together (`Header::prg_ram_window`).
645    //
646    // iNES 1.0 has no reliable PRG-RAM size. We report 8 KiB so the common
647    // mappers that use save RAM (MMC1, MMC3, MMC5) get a plausible window
648    // allocated; mappers that override this on construction may. It has no
649    // NVRAM split, so both NVRAM sizes are 0.
650    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    // Battery / trainer.
667    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
694/// The NES 2.0 fields of bytes 13-15 that `vs_ppu_type` does not cover.
695struct 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
702/// Decode bytes 13-15 (see [`Header`] for the fixed iNES 1.0 values).
703fn 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        // Byte 13 HIGH nibble for a Vs. System: types 5 and 6 are the Vs.
714        // DualSystem boards (two CPUs / two PPUs).
715        vs_hardware_type: (console_type == ConsoleType::VsSystem)
716            .then(|| VsHardwareType::from_nibble(h[13] >> 4)),
717        // Byte 13 LOW nibble for console type 3.
718        extended_console_type: (console_type == ConsoleType::Extended)
719            .then(|| ExtendedConsoleType::from_nibble(h[13])),
720        // Byte 14 bits 0-1 and byte 15 bits 0-6.
721        misc_rom_count: h[14] & 0x03,
722        default_expansion_device: ExpansionDevice::from_code(h[15]),
723    }
724}
725
726/// Standard / exponent-multiplier sizing per NES 2.0.
727///
728/// `lsb` is header byte 4 or 5; `msb_nibble` is the matching nibble of byte 9.
729fn decoded_size(lsb: u8, msb_nibble: u16, unit: usize) -> Result<usize, RomError> {
730    if msb_nibble == 0x0F {
731        // Exponent-multiplier: lsb = EEEEEEMM.
732        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
757/// NES 2.0 RAM-shift encoding: 0 → 0 bytes, otherwise `64 << shift`.
758const fn ram_size_from_shift(shift: u8) -> u32 {
759    if shift == 0 { 0 } else { 64u32 << shift }
760}
761
762/// Write the edits in `h` over `original`, the 16 header bytes `h` was parsed
763/// from, and return the result.
764///
765/// Only the bits of fields whose value differs from `parse_header(original)`
766/// are rewritten; every other bit of `original` comes back unchanged. So an
767/// unedited header round-trips byte for byte, whatever it holds --
768/// exponent-notation sizes, reserved bits, reserved Vs. PPU nibbles, and the
769/// junk some dumpers left in bytes 8-15 of iNES 1.0 headers. This is the
770/// public header writer (since v2.9.8 the only one) and what the header editor
771/// writes to disk.
772///
773/// One edit reaches outside its own field: on an iNES 1.0 header, setting a
774/// mapper of 16 or more zeroes a non-zero tail in bytes 12-15, which would
775/// otherwise mask the new mapper's bits 4-7 (the `"DiskDude!"` rule).
776///
777/// An edited field is written in its canonical encoding: a size in the
778/// standard notation when that can express it and in exponent-multiplier
779/// notation otherwise, a RAM size as its `64 << shift` nibble. Toggling
780/// `is_nes2` changes what bytes 7-15 mean, so that edit re-encodes the whole
781/// header canonically, which writes every reserved bit as zero. So does an
782/// `original` that does not parse. To build a header from nothing, pass the
783/// 16 bytes of an empty iNES 1.0 header (`"NES\x1A"` and twelve zeros) as
784/// `original`.
785#[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    // Every byte the canonical encoding produces for the edited header; each
794    // changed field copies only its own bits from here.
795    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    // A non-zero byte in 12-15 of an iNES 1.0 header masks mapper bits 4-7
807    // (`mapper_number`'s dirty-tail rule), so a mapper edit to 16 or more
808    // clears them or it does not read back. iNES 1.0 reads nothing else there.
809    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    // Bytes 7 (console bits) and 8-13 carry these fields in NES 2.0 only;
838    // `parse_header` ignores them in iNES 1.0, and so does this.
839    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            // Byte 13 means something else for each console type, so a
860            // console change re-encodes it.
861            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/// Encode a [`Header`] from scratch: the edited fields of
889/// [`serialize_header_preserving`], and its whole output when the format
890/// changes or the original does not parse.
891///
892/// Every field `parse_header` reads is written, so for any header it produced,
893/// `parse_header(&canonical_header(&h)) == Ok(h)`
894/// (`canonical_encoding_round_trips_every_parsed_header`). What is not a field
895/// is written as zero: the reserved bits, the exact reserved Vs. PPU nibble,
896/// and all of bytes 8-15 on iNES 1.0. A field iNES 1.0 cannot express (a size
897/// above 255 units, any NES 2.0-only field) is dropped there.
898///
899/// Private since v2.9.8; until then `serialize_header` exposed it, and it lost
900/// the exponent size notation, the NVRAM nibbles, the Vs. hardware type, the
901/// extended console type and bytes 14-15.
902// Serialization performs nibble extraction by mask + cast; the truncation is
903// the documented encoding (not a bug), so we allow the cast lints narrowly on
904// this function.
905#[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    // Sizing.
911    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    // Flags 6.
917    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    // Flags 7.
933    // Mapper bits 4-7 sit in byte 7's high nibble as-is (no shift).
934    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        // Mapper hi nibble + submapper.
948        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        // Byte 13 means something different per console type: the Vs. PPU
959        // type (low nibble) and Vs. hardware type (high nibble) for a Vs.
960        // System, the extended console type (low nibble) for console type 3,
961        // nothing otherwise.
962        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/// Encode a ROM size as (byte 4/5, byte-9 nibble).
980///
981/// NES 2.0 uses the standard notation (a 12-bit count of `unit`s, nibble
982/// `$0-$E`) when it can express `bytes` exactly, and the exponent-multiplier
983/// notation (nibble `$F`, `bytes = 2^E * (2*MM + 1)`, E < 64) otherwise, which
984/// is the order the Nesdev page prescribes. A size neither can express (not a
985/// whole number of units above `$EFF` units, with an odd factor above 7) falls
986/// back to the standard notation, truncated. iNES 1.0 has only the 8-bit count.
987// Truncating cast: count is masked to 8 / 4 bits before the cast.
988#[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        // bytes = 2^E * odd; the odd factor must be 1, 3, 5 or 7.
999        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
1009/// Encode a [`VsPpuType`] back to its NES 2.0 byte-13 low nibble.
1010const 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    // shift = log2(size / 64).
1029    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    /// A small deterministic PRNG (xorshift64) for header sweeps, so the
1053    /// sampled headers are the same on every run.
1054    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        // The header editor writes `serialize_header_preserving` output over
1073        // the ROM file, so an unedited header must come back exactly -- every
1074        // bit, modelled or not. Bytes 7 and 13 (format, console type, Vs.
1075        // PPU / hardware type, extended console type) are swept exhaustively
1076        // against sampled values of the rest; then 200,000 fully random
1077        // headers cover exponent sizes, NVRAM nibbles, reserved bits and iNES
1078        // 1.0 junk in bytes 8-15.
1079        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        // Most random headers parse; a sweep that silently checked nothing
1099        // would pass, so say how much it covered.
1100        assert!(checked > 200_000, "only {checked} headers parsed");
1101    }
1102
1103    #[test]
1104    fn canonical_encoding_round_trips_every_parsed_header() {
1105        // parse(canonical(h)) == h for every header `parse_header` can
1106        // produce: the canonical encoder must write back every field the
1107        // parser reads. Until v2.9.8 it lost exponent-notation sizes, the
1108        // NVRAM nibbles, the Vs. hardware type, the extended console type
1109        // and bytes 14-15.
1110        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    // Field assignment after `Default` is deliberate: it is the construction
1146    // pattern `#[non_exhaustive]` leaves other crates, so the test uses it.
1147    #[allow(clippy::field_reassign_with_default)]
1148    fn canonical_encoding_round_trips_constructed_nes2_headers() {
1149        // The same property over headers built with `Default` + field
1150        // assignment (the only way outside this crate, `Header` being
1151        // `#[non_exhaustive]`), sweeping every value of each byte 10-15
1152        // field in turn.
1153        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; // 5 MiB: exponent notation
1158        base.chr_size = 0xEFF * CHR_UNIT; // the largest standard count
1159        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        // And the default round-trips as the iNES 1.0 header it describes.
1201        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        // Spot checks against the NESdev table.
1215        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        // Bit 7 is not part of the field.
1232        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        // iNES 1.0 bytes 8-15 are not part of the format; a dumper's
1247        // signature there must not turn into a Vs. type or an expansion
1248        // device. The decision is documented on `Header`.
1249        let mut h = ines_header(2, 0, 1, 0x02);
1250        h[7] |= 0x01; // a Vs. bit iNES 1.0 parsing ignores
1251        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        // The window every board allocates is what `prg_ram_size` alone held
1265        // before v2.9.8 split it, so ROM loading is unchanged. StarTropics
1266        // (MMC6) declares its save RAM only in the NVRAM nibble.
1267        let mut h = ines_header(8, 16, 4, 0x02);
1268        h[7] = 0x08;
1269        h[10] = 0x70; // 8 KiB PRG-NVRAM, no volatile PRG-RAM
1270        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; // 2 KiB volatile + 8 KiB NVRAM
1274        let p = parse_header(&h).unwrap();
1275        assert_eq!(p.prg_ram_window(), 10 * 1024);
1276        h[11] = 0x57; // 8 KiB CHR-RAM, 2 KiB CHR-NVRAM
1277        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        // Before v2.9.3 the editor wrote the canonical encoding, which
1284        // collapsed the Vs. hardware type (byte 13 high nibble) to 5 or 0 --
1285        // losing UniSystem protection types 1-4 and the 5/6 distinction --
1286        // and always wrote bytes 14-15 as zero.
1287        let mut h = ines_header(2, 1, 0, 0);
1288        h[7] = 0x08 | 0x01; // NES 2.0, Vs. System
1289        h[14] = 0x02; // two miscellaneous ROMs
1290        h[15] = 0x2A; // a default expansion device
1291        for hw in 0..16u8 {
1292            h[13] = (hw << 4) | 0x2; // PPU type 2 (RP2C04-0001)
1293            let mut p = parse_header(&h).unwrap();
1294            p.has_battery = true; // an unrelated edit
1295            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        // Console type 3 (Extended): byte 13's LOW nibble is the extended
1305        // console type (VT01-VT32, EPSM, ...), which `Header` does not model
1306        // (CodeRabbit on #571).
1307        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; // UniSystem with protection type 3, PPU type 2
1324        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        // Type 6 is kept as 6: until v2.9.8 the header carried only a
1333        // DualSystem flag, and a canonical encode wrote every dual board as 5.
1334        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        // Reserved types 7-15 keep their value too.
1341        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; // NES 2.0, Extended
1351        h[13] = 0xF7; // reserved high nibble, VT03
1352        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    /// Every bit that differs between `a` and `b`, as a 16-byte mask.
1361    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        // A NES 2.0 Vs. System header with every unmodelled bit set, so a
1368        // stray write anywhere shows up in the changed-bit mask.
1369        let mut h = [0xFFu8; 16];
1370        h[..4].copy_from_slice(&MAGIC);
1371        h[4] = 0x02; // 2 x 16 KiB PRG
1372        h[5] = 0x01; // 1 x 8 KiB CHR
1373        h[6] = 0x10; // mapper 1, horizontal
1374        h[7] = 0xF9; // mapper 0xF1 high nibble, NES 2.0, Vs. System
1375        h[8] = 0x30; // submapper 3
1376        h[9] = 0x00; // standard size notation
1377        h[10] = 0x77; // 8 KiB volatile + 8 KiB NV PRG-RAM
1378        h[11] = 0x77; // 8 KiB CHR-RAM, 8 KiB CHR-NVRAM
1379        h[12] = 0xFD; // PAL, reserved bits set
1380        h[13] = 0x31; // hardware type 3, PPU type 1 (reserved)
1381        h[14] = 0xFF; // 3 miscellaneous ROMs, reserved bits set
1382        h[15] = 0xFF; // unassigned device $7F, bit 7 set
1383        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, // 12 KiB: 2^12 * 3, exponent notation only
1433                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            // And the edit reads back -- every field, not only the edited one.
1446            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        // iNES 1.0 bytes 8-15 are not part of the format and often hold a
1463        // dumper's signature; edits to NES 2.0-only fields must not touch them.
1464        let mut h = ines_header(2, 1, 1, 0);
1465        h[7] |= 0x01; // a Vs. bit iNES 1.0 parsing ignores
1466        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        // Byte 7's high nibble is mapper bits 4-7. Until v2.9.3 the encoder
1492        // wrote bits 8-11 there (`(mapper >> 4) & 0xF0`), so every mapper
1493        // from 16 up came back wrong -- mapper 66 (GxROM) as 2 -- and the
1494        // header editor wrote that to disk. Found by
1495        // `each_edit_rewrites_only_its_own_bits`.
1496        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; // Extended
1515        h[13] = 0x0B; // an extended console type
1516        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); // 32K PRG, 8K CHR, mapper 0, horizontal
1541        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        // Mapper 1 (MMC1): low nibble 1, high nibble 0.
1555        let h = ines_header(1, 0, 1, 0);
1556        assert_eq!(parse_header(&h).unwrap().mapper_id, 1);
1557        // Mapper 4 (MMC3): low nibble 4, high nibble 0.
1558        let h = ines_header(1, 0, 4, 0);
1559        assert_eq!(parse_header(&h).unwrap().mapper_id, 4);
1560        // Mapper 0xCD: low nibble D, high nibble C.
1561        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        // NES 2.0 (h[7] bits 2-3 = 0b10) + Vs. System console (bits 0-1 = 01).
1568        let mut h = ines_header(2, 1, 0, 0);
1569        h[7] = 0x08 | 0x01;
1570        // byte 13: high nibble = Vs. hardware type, low nibble = Vs. PPU type.
1571        h[13] = 0x50; // hardware type 5 (DualSystem), 2C03 PPU
1572        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        // Hardware type 6 is also a DualSystem board.
1577        h[13] = 0x60;
1578        assert!(parse_header(&h).unwrap().is_vs_dual_system());
1579        // A normal Vs. UniSystem (hardware type 0-4) is NOT dual.
1580        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        // A non-Vs. NES 2.0 cart is never dual, even with a stray high nibble.
1585        h[7] = 0x08; // NES 2.0, console type = Nes
1586        h[13] = 0x50;
1587        assert!(!parse_header(&h).unwrap().is_vs_dual_system());
1588        // An iNES-1.0 cart (no NES 2.0 flag) is never dual.
1589        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; // NES 2.0 + Vs. System
1598        h[13] = 0x60; // DualSystem hardware type 6 + 2C03 PPU
1599        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        // The bool round-trips (type 6 re-encodes as type 5, both DualSystem).
1604        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); // V mirroring + battery + trainer
1611        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; // PRG LSB
1638        h[5] = 1; // CHR LSB
1639        h[6] = 0x10; // mapper low nibble = 1
1640        h[7] = 0x08; // NES 2.0 marker, console NES, mapper hi nibble 0
1641        h[8] = 0x21; // submapper 2, mapper hi 1
1642        h[9] = 0x00;
1643        h[10] = 0x07; // PRG RAM shift 7 -> 64<<7 = 8 KiB
1644        h[11] = 0x00;
1645        h[12] = 0x01; // PAL
1646        let p = parse_header(&h).unwrap();
1647        assert!(p.is_nes2);
1648        assert_eq!(p.mapper_id, 0x101); // bits: low=1, hi=1<<8
1649        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        // exponent = 16, multiplier = 1: 2^16 = 65536 bytes
1660        h[4] = 16 << 2;
1661        h[5] = 0;
1662        h[6] = 0;
1663        h[7] = 0x08; // NES 2.0
1664        h[8] = 0;
1665        h[9] = 0x0F; // PRG MSB nibble = $F (exponent path)
1666        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); // mapper 4, V + battery
1673        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; // mapper low 4, vertical
1685        h[7] = 0x08; // NES 2.0, console NES, mapper mid 0
1686        h[8] = 0x10; // submapper 1
1687        h[9] = 0x00;
1688        h[10] = 0x07;
1689        h[11] = 0x00;
1690        h[12] = 0x01;
1691        h[14] = 0x01; // one miscellaneous ROM
1692        h[15] = 0x2A; // multicart
1693        let parsed = parse_header(&h).unwrap();
1694        assert_eq!(parsed.misc_rom_count, 1);
1695        assert_eq!(parsed.default_expansion_device, ExpansionDevice::Multicart);
1696        // Since v2.9.8 every byte of a header with no reserved bits set
1697        // round-trips through the canonical encoding.
1698        assert_eq!(canonical_header(&parsed), h);
1699    }
1700
1701    #[test]
1702    fn nes_cart_has_no_vs_ppu_type() {
1703        // A standard NES 2.0 cart (console type Nes) parses to VsPpuType::None.
1704        let mut h = [0u8; 16];
1705        h[..4].copy_from_slice(&MAGIC);
1706        h[4] = 1;
1707        h[5] = 1;
1708        h[7] = 0x08; // NES 2.0, console = Nes
1709        h[13] = 0x09; // would be 2C05-02 IF this were a Vs. cart
1710        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        // Console type Vs. System (byte 7 bits 0-1 = 1) + byte 13 low nibble.
1718        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; // NES 2.0 (bits 2-3 = 10) + console Vs (bits 0-1 = 01)
1724            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        // The 2C05-02 resolves to the 2C03 palette + 2C05 quirks + $3D id.
1732        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        // High nibble (Vs. hardware type) does not change the PPU type.
1737        let mut h = [0u8; 16];
1738        h[..4].copy_from_slice(&MAGIC);
1739        h[7] = 0x09;
1740        h[13] = 0x52; // hw type 5 (Dual System), PPU type 2 = 2C04-0001
1741        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; // NES 2.0 + Vs. System
1754        h[13] = 0x0A; // 2C05-03
1755        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        // NESdev "iNES", Flags 7-15: old tools wrote signatures such as
1764        // "DiskDude!" into bytes 7-15, which adds 64 to the mapper number,
1765        // and "if the last 4 bytes are not all zero, and the header is not
1766        // marked for NES 2.0 format, an emulator should either mask off the
1767        // upper 4 bits of the mapper number or simply refuse to load the ROM."
1768        //
1769        // The staged Russian Balloon Fight translation carries "@iskDude!"
1770        // (byte 7 = 0x40): an NROM image that loaded as mapper 64 and filled
1771        // the sky with RAMBO-1-banked tiles.
1772        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        // The textbook "DiskDude!" form on an MMC1 image: 65 -> 1.
1776        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        // A clean iNES 1.0 tail keeps the full 8-bit mapper number...
1780        let h = ines_header(8, 8, 64, 0);
1781        assert_eq!(parse_header(&h).unwrap().mapper_id, 64);
1782        // ...and garbage confined to bytes 8-11 does not trigger the rule,
1783        // which keys on bytes 12-15 only.
1784        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        // NES 2.0 headers are exempt: bytes 12-15 are real fields there.
1788        let mut h = ines_header(8, 8, 64, 0);
1789        h[7] |= 0x08;
1790        h[12] = 0x01; // PAL
1791        h[15] = 0x01; // default expansion device
1792        assert_eq!(parse_header(&h).unwrap().mapper_id, 64);
1793    }
1794
1795    /// v2.9.8 (`CodeRabbit` on the review slice #580) — the header editor's
1796    /// mapper edit reads back on a `"DiskDude!"` dump. The preserving writer
1797    /// copies the untouched tail back, and a non-zero byte in 12-15 masks
1798    /// mapper bits 4-7, so setting mapper 66 used to save a header that
1799    /// parsed as mapper 2.
1800    #[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        // A mapper below 16 has no bits 4-7 to lose: the tail stays.
1810        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        // shift=7 => 8 KiB
1824        assert_eq!(ram_size_from_shift(7), 8192);
1825        assert_eq!(ram_shift_for(8192), 7);
1826        // shift=10 => 64 KiB
1827        assert_eq!(ram_size_from_shift(10), 65536);
1828        assert_eq!(ram_shift_for(65536), 10);
1829    }
1830}