rustyn64_cart/lib.rs
1//! `rustyn64-cart` — PI cart interface + PIF/CIC boot + cartridge saves.
2//!
3//! Models the Peripheral Interface (PI) DMA path to the cartridge ROM, the PIF
4//! boot ROM + CIC seed handshake (SI side), and the on-cart save backends
5//! (EEPROM 4k/16k, SRAM, `FlashRAM`, Controller Pak). This is a **skeleton** —
6//! the real PI/SI DMA engines, the CIC challenge/response, and the `FlashRAM`
7//! state machine are roadmap phases left as marked TODOs.
8//!
9//! Part of the one-directional chip-crate graph (see `docs/architecture.md`):
10//! this crate does NOT depend on any other chip crate. The RDP depends on it
11//! ONLY for the shared RDRAM memory-bus trait ([`RdramBus`]) — the N64 analog
12//! shared RDRAM access path that the RDP also borrows.
13//! `#![no_std]` + `alloc`; only the frontend carries `std` + `unsafe`.
14
15#![no_std]
16#![forbid(unsafe_code)]
17#![warn(missing_docs)]
18#![allow(clippy::cast_possible_truncation, clippy::cast_lossless)]
19// Skeleton `tick`/hook methods are deliberately non-`const` (they will drive
20// PI/SI DMA and the FlashRAM state machine).
21#![allow(clippy::missing_const_for_fn)]
22
23extern crate alloc;
24
25/// The PI (Peripheral Interface) DMA engine + BSD domain timing registers.
26pub mod pi;
27/// The PIF RAM + the SI joybus frame executor (controllers, EEPROM, Pak).
28pub mod pif;
29/// The four on-cartridge save backends (SRAM, FlashRAM, EEPROM, Controller Pak).
30pub mod save;
31
32use alloc::vec::Vec;
33
34use serde::{Deserialize, Serialize};
35
36/// The shared RDRAM memory bus, as seen by chips that DMA into/out of main RAM.
37///
38/// The RDP (framebuffer + texture fetches via the RDRAM) and the PI/SI DMA
39/// engines all read and write RDRAM through this narrow trait. `rustyn64-core`
40/// owns the concrete 8 MiB (4 MiB base + 4 MiB Expansion Pak) backing store and
41/// implements this; keeping the trait in `rustyn64-cart` lets `rustyn64-rdp`
42/// depend on exactly one chip crate, preserving the one-directional graph.
43pub trait RdramBus {
44 /// Read a byte from RDRAM at a physical address.
45 fn rdram_read(&self, addr: u32) -> u8;
46 /// Write a byte to RDRAM at a physical address.
47 fn rdram_write(&mut self, addr: u32, val: u8);
48
49 /// Read a big-endian 32-bit word from RDRAM. Default composes four byte
50 /// reads; `rustyn64-core` overrides with a fast slice path.
51 fn rdram_read_u32(&self, addr: u32) -> u32 {
52 u32::from_be_bytes([
53 self.rdram_read(addr),
54 self.rdram_read(addr.wrapping_add(1)),
55 self.rdram_read(addr.wrapping_add(2)),
56 self.rdram_read(addr.wrapping_add(3)),
57 ])
58 }
59
60 /// Read the RDRAM "hidden" bits for the 16-bit halfword at `addr` — the 9th
61 /// bit RDRAM carries per byte, which the RDP Z-buffer uses for the low 2 bits
62 /// of the per-pixel `dz`. Returns the 2-bit value (`0..=3`).
63 ///
64 /// Default returns 0, so an impl that does not model the hidden bits behaves
65 /// as if they read back clear (which is also the power-on state).
66 fn rdram_read_hidden(&self, addr: u32) -> u8 {
67 let _ = addr;
68 0
69 }
70
71 /// Write the RDRAM hidden bits (`0..=3`) for the halfword at `addr`. Default
72 /// no-op for impls that do not model them.
73 fn rdram_write_hidden(&mut self, addr: u32, val: u8) {
74 let _ = (addr, val);
75 }
76}
77
78/// On-cartridge non-volatile save backend type.
79///
80/// Detected from the per-game database (the IPL/ROM has no reliable in-header
81/// save-type field, unlike the iNES mapper byte) — keyed off the cart serial /
82/// CRC. `None` means the title saves only to the Controller Pak (or not at all).
83#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Default, Serialize, Deserialize)]
84pub enum SaveType {
85 /// No on-cart save chip.
86 #[default]
87 None,
88 /// 4 Kbit serial EEPROM (512 bytes).
89 Eeprom4k,
90 /// 16 Kbit serial EEPROM (2 KiB).
91 Eeprom16k,
92 /// 256 Kbit battery-backed SRAM (32 KiB).
93 Sram,
94 /// 1 Mbit `FlashRAM` (128 KiB).
95 FlashRam,
96 /// Removable Controller Pak / Memory Pak (32 KiB, via the SI joybus).
97 ControllerPak,
98}
99
100impl SaveType {
101 /// Backing-store size in bytes (`0` for [`SaveType::None`]).
102 #[must_use]
103 pub const fn size_bytes(self) -> usize {
104 match self {
105 Self::None => 0,
106 Self::Eeprom4k => 512,
107 Self::Eeprom16k => 2 * 1024,
108 Self::Sram | Self::ControllerPak => 32 * 1024,
109 Self::FlashRam => 128 * 1024,
110 }
111 }
112}
113
114/// The CIC (boot-security copy-protection) lockout-chip variant.
115///
116/// The PIF and the CIC exchange a seeded challenge/response at boot; the variant
117/// fixes the seed + checksum the IPL3 expects. Skeleton — the handshake itself
118/// is a roadmap phase.
119#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Default, Serialize, Deserialize)]
120pub enum Cic {
121 /// 6101 (early NTSC: Star Fox 64).
122 Cic6101,
123 /// 6102 / 7101 (the common NTSC/PAL variant).
124 #[default]
125 Cic6102,
126 /// 6103 / 7103.
127 Cic6103,
128 /// 6105 / 7105 (uses the X105 IPL2 ramp).
129 Cic6105,
130 /// 6106 / 7106.
131 Cic6106,
132}
133
134/// The boot secrets a CIC hands the PIF at power-on.
135///
136/// From `n64brew_wiki/markdown/PIF-NUS.md` §checksum table: the two 8-bit seeds
137/// and the 6-byte IPL2 checksum. On the real-PIF path the PIF writes the seeds
138/// into PIF RAM (IPL2 reads them to run its own checksum) and keeps the checksum
139/// to compare against the value IPL2 computes — a mismatch NMI-halts the CPU.
140/// These are documented constants, not the SM5 firmware; the seed bytes
141/// cross-check the per-CIC seed values the HLE boot path already injects.
142#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
143pub struct CicBootSecrets {
144 /// The 8-bit IPL2 seed (used by IPL2's checksum over the cart's IPL3).
145 pub ipl2_seed: u8,
146 /// The 8-bit IPL3 seed (used by IPL3's checksum over the first MiB).
147 pub ipl3_seed: u8,
148 /// The 6-byte IPL2 checksum the PIF expects the CPU to reproduce.
149 pub ipl2_checksum: [u8; 6],
150}
151
152/// Standard CRC-32 (reflected, poly `0xEDB8_8320`) — the fingerprint used to
153/// identify a cartridge's CIC from its IPL3 (cen64 `si/cic.c` `si_crc32`).
154fn crc32(data: &[u8]) -> u32 {
155 let mut crc = 0xFFFF_FFFF_u32;
156 for &byte in data {
157 crc ^= u32::from(byte);
158 for _ in 0..8 {
159 crc = if crc & 1 != 0 {
160 0xEDB8_8320 ^ (crc >> 1)
161 } else {
162 crc >> 1
163 };
164 }
165 }
166 !crc
167}
168
169impl Cic {
170 /// Identify the CIC from the cartridge IPL3 (`rom[0x40..0x1000]`) by its
171 /// CRC-32 — the standard fingerprint (cen64 `si/cic.c`, N64brew *CIC-NUS*).
172 /// The core never consults a per-game database (ADR 0003/0004); this reads
173 /// only the ROM's own boot code. An unknown IPL3 (homebrew, e.g.
174 /// n64-systemtest, which ships its own) falls back to 6102 — the seed only
175 /// feeds the boot handshake, and a custom IPL3 does not depend on the stock
176 /// checksum path (the same fallback cen64 documents).
177 #[must_use]
178 pub fn from_ipl3(rom: &[u8]) -> Self {
179 let Some(ipl3) = rom.get(0x40..0x1000) else {
180 return Self::Cic6102;
181 };
182 // CRC values: cen64 `si/cic.c`. 7102 and the three iQue variants share
183 // the 6101 seed; 8303 (64DD), the common 6102 fingerprint `0x90BB_6CB5`,
184 // and every unknown/homebrew IPL3 all resolve to 6102 (the last arm).
185 match crc32(ipl3) {
186 0x6170_A4A1 | 0x009E_9EA3 | 0xCD19_FEF1 | 0xB98C_ED9A | 0xE71C_2766 => Self::Cic6101,
187 0x0B05_0EE0 => Self::Cic6103,
188 0x98BC_2C86 => Self::Cic6105,
189 0xACC8_580A => Self::Cic6106,
190 // `0x90BB_6CB5` (6102) folds into this default — the same variant.
191 _ => Self::Cic6102,
192 }
193 }
194
195 /// The [`CicBootSecrets`] for this variant. All modeled variants are the
196 /// NTSC members; the 7xxx PAL twins share the same seeds and checksums, so
197 /// region is a separate axis (the PIF SM5 ROM, not the CIC, is region-locked).
198 #[must_use]
199 pub const fn boot_secrets(self) -> CicBootSecrets {
200 let (ipl2_seed, ipl3_seed, ipl2_checksum) = match self {
201 Self::Cic6101 => (0x3F, 0x3F, [0x45, 0xCC, 0x73, 0xEE, 0x31, 0x7A]),
202 Self::Cic6102 => (0x3F, 0x3F, [0xA5, 0x36, 0xC0, 0xF1, 0xD8, 0x59]),
203 Self::Cic6103 => (0x78, 0x78, [0x58, 0x6F, 0xD4, 0x70, 0x98, 0x67]),
204 Self::Cic6105 => (0x91, 0x91, [0x86, 0x18, 0xA4, 0x5B, 0xC2, 0xD3]),
205 Self::Cic6106 => (0x85, 0x85, [0x2B, 0xBA, 0xD4, 0xE6, 0xEB, 0x74]),
206 };
207 CicBootSecrets {
208 ipl2_seed,
209 ipl3_seed,
210 ipl2_checksum,
211 }
212 }
213}
214
215/// ROM image byte order, derived from the magic in the first four bytes.
216///
217/// `.z64` is big-endian (native), `.n64` is little-endian (byte-swapped),
218/// `.v64` is byte-swapped within each 16-bit halfword. The loader normalizes
219/// everything to big-endian internally.
220#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
221pub enum RomFormat {
222 /// `.z64` — big-endian, the canonical internal order.
223 Z64BigEndian,
224 /// `.n64` — little-endian (32-bit word swap).
225 N64LittleEndian,
226 /// `.v64` — byte-swapped halfwords.
227 V64ByteSwapped,
228}
229
230impl RomFormat {
231 /// Detect the format from the leading four magic bytes, or `None` if the
232 /// header is too short / unrecognized.
233 #[must_use]
234 pub fn detect(magic: &[u8]) -> Option<Self> {
235 match magic {
236 [0x80, 0x37, 0x12, 0x40, ..] => Some(Self::Z64BigEndian),
237 [0x40, 0x12, 0x37, 0x80, ..] => Some(Self::N64LittleEndian),
238 [0x37, 0x80, 0x40, 0x12, ..] => Some(Self::V64ByteSwapped),
239 _ => None,
240 }
241 }
242}
243
244/// Parsed cartridge header (the first 0x40 bytes of the ROM image).
245#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
246pub struct RomHeader {
247 /// Internal game title (0x20..0x34, space-padded ASCII).
248 pub title: [u8; 20],
249 /// Cartridge serial / game code (0x3B..0x3F, e.g. `NSME`).
250 pub game_code: [u8; 4],
251 /// Detected save backend (resolved via the per-game DB, not the header).
252 pub save_type: SaveType,
253 /// CIC lockout-chip variant (resolved from the IPL3 checksum / DB).
254 pub cic: Cic,
255}
256
257impl RomHeader {
258 /// Parse a normalized big-endian header. Skeleton: only the title / game
259 /// code are extracted; `save_type` + `cic` are DB-resolved elsewhere.
260 ///
261 /// # Errors
262 /// Returns [`CartError::ShortHeader`] if `rom` is shorter than 0x40 bytes.
263 pub fn parse(rom: &[u8]) -> Result<Self, CartError> {
264 if rom.len() < 0x40 {
265 return Err(CartError::ShortHeader);
266 }
267 let mut title = [0u8; 20];
268 title.copy_from_slice(&rom[0x20..0x34]);
269 let mut game_code = [0u8; 4];
270 game_code.copy_from_slice(&rom[0x3B..0x3F]);
271 // The CIC is fingerprinted from the ROM's own IPL3 (not a per-game DB).
272 // TODO(T-CART-02): resolve save_type from the per-game DB by serial/CRC.
273 Ok(Self {
274 title,
275 game_code,
276 save_type: SaveType::None,
277 cic: Cic::from_ipl3(rom),
278 })
279 }
280}
281
282/// Error type for cartridge loading / parsing.
283#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
284pub enum CartError {
285 /// The ROM image is shorter than a 0x40-byte header.
286 ShortHeader,
287 /// The leading magic bytes matched no known `.z64`/`.n64`/`.v64` order.
288 UnknownFormat,
289}
290
291/// The cartridge trait. One cart model, parameterized by save type, CIC, and
292/// region (ADR 0003) — the N64 has no mapper equivalent.
293///
294/// Board-specific behavior — PI/SI-mediated reads and writes, save backing,
295/// optional bootstrap — lives behind it, not in the CPU. All hooks default to
296/// no-op so a board implements only what it uses.
297pub trait Cartridge {
298 /// PI-side read from the cartridge address space (`$1000_0000..`).
299 fn pi_read(&mut self, addr: u32) -> u8 {
300 let _ = addr;
301 0
302 }
303 /// PI-side write into the cartridge address space (save SRAM/FlashRAM, regs).
304 fn pi_write(&mut self, addr: u32, val: u8) {
305 let _ = (addr, val);
306 }
307 /// SI-side joybus exchange (controllers + Controller Pak). Default no-op.
308 fn si_exchange(&mut self, _channel: u8, _tx: &[u8], _rx: &mut [u8]) {}
309 /// Per-CPU-cycle hook (counter-driven cart hardware). Default no-op.
310 fn notify_cpu_cycle(&mut self) {}
311 /// The active save backend for this cartridge.
312 fn save_type(&self) -> SaveType {
313 SaveType::None
314 }
315}
316
317/// PI cart + PIF/CIC + save state — the concrete board.
318#[derive(Debug, Default, Serialize, Deserialize)]
319pub struct Cart {
320 /// The normalized (big-endian) ROM image.
321 ///
322 /// **Excluded from save-states** (`#[serde(skip)]`): the ROM is immutable and
323 /// up to 64 MiB, so serializing it in every snapshot would make the rewind
324 /// ring enormous. A restore deserializes an empty ROM; the frontend re-attaches
325 /// the currently-loaded image via [`Cart::reattach_rom`]. A save-state is thus
326 /// only valid alongside the same ROM — the normal emulator contract.
327 #[serde(skip)]
328 rom: Vec<u8>,
329 /// Parsed header (title / game code / save+CIC selection).
330 header: RomHeader,
331 /// The active save backend (PI-bus SRAM/FlashRAM or joybus EEPROM/Pak).
332 save: save::SaveDevice,
333 /// The PIF: its 64-byte RAM + the joybus executor for controllers/accessories.
334 pif: pif::Pif,
335}
336
337/// PI domain-1 base (the cartridge ROM window).
338const ROM_PI_BASE: u32 = 0x1000_0000;
339
340impl Cart {
341 /// Construct an empty cart at power-on.
342 #[must_use]
343 pub fn new() -> Self {
344 Self::default()
345 }
346
347 /// Load + normalize a raw ROM image of any supported byte order.
348 ///
349 /// # Errors
350 /// [`CartError::UnknownFormat`] for an unrecognized magic, or
351 /// [`CartError::ShortHeader`] for a truncated header.
352 pub fn load(raw: &[u8]) -> Result<Self, CartError> {
353 let format = RomFormat::detect(raw).ok_or(CartError::UnknownFormat)?;
354 let rom = normalize_to_big_endian(raw, format);
355 let header = RomHeader::parse(&rom)?;
356 let save = save::SaveDevice::new(header.save_type);
357 let mut pif = pif::Pif::new();
358 // A Controller-Pak cart advertises its pak on port 0 so the game's
359 // accessory probe (`0x00` info → status 1) finds it.
360 pif.set_pak_present(0, header.save_type == SaveType::ControllerPak);
361 Ok(Self {
362 rom,
363 header,
364 save,
365 pif,
366 })
367 }
368
369 /// Re-attach a ROM image after a save-state restore (which deserializes an
370 /// empty ROM — the ROM field is `#[serde(skip)]`'d). The `rom` bytes are the
371 /// normalized big-endian image (what `Cart::load` stores, i.e. what
372 /// `rom_image` returns); the frontend keeps the loaded image and re-inserts it
373 /// here so cart/PI reads resolve again. All other cart state (save, PIF,
374 /// header) came from the snapshot and is left untouched.
375 pub fn reattach_rom(&mut self, rom: alloc::vec::Vec<u8>) {
376 self.rom = rom;
377 }
378
379 /// The normalized (big-endian) ROM image, for the frontend to stash so it can
380 /// [`Cart::reattach_rom`] after a restore.
381 #[must_use]
382 pub fn rom_image(&self) -> &[u8] {
383 &self.rom
384 }
385
386 /// The PIF's 64-byte RAM (the SI DMA copies it to/from RDRAM).
387 #[must_use]
388 pub const fn pif_ram(&self) -> &[u8; pif::PIF_RAM_LEN] {
389 self.pif.ram()
390 }
391
392 /// Load the whole PIF RAM (an SI 64-byte write from RDRAM).
393 pub fn pif_load(&mut self, bytes: &[u8; pif::PIF_RAM_LEN]) {
394 self.pif.load(bytes);
395 }
396
397 /// A CPU direct read/write byte of PIF RAM.
398 #[must_use]
399 pub fn pif_read(&self, off: usize) -> u8 {
400 self.pif.read(off)
401 }
402
403 /// Write a CPU direct byte of PIF RAM.
404 pub const fn pif_write(&mut self, off: usize, val: u8) {
405 self.pif.write(off, val);
406 }
407
408 /// Execute the pending joybus frame (on an SI read), using the four packed
409 /// controller port words and the cart's save backend.
410 pub fn pif_execute(&mut self, controllers: &[u32; 4]) {
411 self.pif.execute(controllers, &mut self.save);
412 }
413
414 /// Install the PIF boot ROM (IPL1/IPL2) for the real-PIF boot path.
415 pub fn pif_load_boot_rom(&mut self, bytes: &[u8]) {
416 self.pif.load_boot_rom(bytes);
417 }
418
419 /// Read a byte of the PIF boot ROM (`0x1FC0_0000 + off`); 0 when absent (HLE).
420 #[must_use]
421 pub fn pif_boot_rom_read(&self, off: usize) -> u8 {
422 self.pif.boot_rom_read(off)
423 }
424
425 /// Is a real PIF boot ROM installed (real-PIF boot path active)?
426 #[must_use]
427 pub const fn pif_has_boot_rom(&self) -> bool {
428 self.pif.has_boot_rom()
429 }
430
431 /// Register the CIC's IPL2 checksum for the real-PIF boot verify.
432 pub const fn pif_set_boot_checksum(&mut self, checksum: [u8; 6]) {
433 self.pif.set_boot_checksum(checksum);
434 }
435
436 /// Process a reset-mode PIF command-byte write (real-PIF boot); returns `true`
437 /// if the checksum verify failed and the CPU must be NMI-halted.
438 pub fn pif_boot_command(&mut self) -> bool {
439 self.pif.boot_command()
440 }
441
442 /// Warm-reset the transient PIF boot state (unlock the ROM, drop the latched
443 /// checksum) so a reset can re-run IPL1→IPL2.
444 pub const fn pif_reset_boot(&mut self) {
445 self.pif.reset_boot();
446 }
447
448 /// The parsed cartridge header.
449 #[must_use]
450 pub const fn header(&self) -> &RomHeader {
451 &self.header
452 }
453
454 /// The persistable save backing bytes (empty for [`SaveType::None`]).
455 #[must_use]
456 pub fn save(&self) -> &[u8] {
457 self.save.backing()
458 }
459
460 /// Mutable access to the save device (the joybus module drives EEPROM/Pak).
461 #[must_use]
462 pub fn save_device_mut(&mut self) -> &mut save::SaveDevice {
463 &mut self.save
464 }
465
466 /// A whole-word PI write (direct-I/O or DMA). Routes the `FlashRAM` Command
467 /// Internal Register (`0x0801_0000`) as a 32-bit command; other DOM2-window
468 /// writes fall to the byte path. ROM-window writes are ignored (read-only).
469 pub fn pi_write_word(&mut self, addr: u32, word: u32) {
470 if addr & !3 == save::FLASH_CIR {
471 self.save.flash_cir(word);
472 } else {
473 for (i, b) in word.to_be_bytes().into_iter().enumerate() {
474 self.pi_write(addr.wrapping_add(i as u32), b);
475 }
476 }
477 }
478
479 /// Advance one unit of cart time (PI/SI DMA progress).
480 pub fn tick(&mut self) {
481 // TODO(T-CART-01): step any in-flight PI/SI DMA transfer.
482 }
483}
484
485impl Cartridge for Cart {
486 fn pi_read(&mut self, addr: u32) -> u8 {
487 // The DOM2 save window (SRAM / FlashRAM) takes priority, but **only**
488 // within that window — otherwise a SRAM/FlashRAM cart's `pi_read` would
489 // answer for the ROM window too (the save's flat store returns `0` past
490 // its end), and the game could never read its own ROM through PI DMA.
491 if (save::SAVE_PI_BASE..ROM_PI_BASE).contains(&addr)
492 && let Some(b) = self.save.pi_read(addr)
493 {
494 return b;
495 }
496 let off = (addr as usize).wrapping_sub(ROM_PI_BASE as usize);
497 self.rom.get(off).copied().unwrap_or(0)
498 }
499
500 fn pi_write(&mut self, addr: u32, val: u8) {
501 // Byte writes reach SRAM and the FlashRAM page buffer; the ROM is
502 // read-only (writes ignored). The FlashRAM CIR is word-only — see
503 // `pi_write_word`.
504 if (save::SAVE_PI_BASE..ROM_PI_BASE).contains(&addr) && addr & !3 != save::FLASH_CIR {
505 self.save.pi_write(addr, val);
506 }
507 }
508
509 fn save_type(&self) -> SaveType {
510 self.header.save_type
511 }
512}
513
514/// Normalize a raw ROM image to internal big-endian (`.z64`) order.
515fn normalize_to_big_endian(raw: &[u8], format: RomFormat) -> Vec<u8> {
516 match format {
517 RomFormat::Z64BigEndian => raw.to_vec(),
518 RomFormat::V64ByteSwapped => {
519 let mut out = raw.to_vec();
520 for pair in out.chunks_exact_mut(2) {
521 pair.swap(0, 1);
522 }
523 out
524 }
525 RomFormat::N64LittleEndian => {
526 let mut out = raw.to_vec();
527 for word in out.chunks_exact_mut(4) {
528 word.swap(0, 3);
529 word.swap(1, 2);
530 }
531 out
532 }
533 }
534}
535
536/// Returns the crate version string.
537#[must_use]
538pub const fn version() -> &'static str {
539 env!("CARGO_PKG_VERSION")
540}
541
542#[cfg(test)]
543mod tests {
544 use super::*;
545
546 #[test]
547 fn detects_rom_formats() {
548 assert_eq!(
549 RomFormat::detect(&[0x80, 0x37, 0x12, 0x40]),
550 Some(RomFormat::Z64BigEndian)
551 );
552 assert_eq!(
553 RomFormat::detect(&[0x40, 0x12, 0x37, 0x80]),
554 Some(RomFormat::N64LittleEndian)
555 );
556 assert_eq!(
557 RomFormat::detect(&[0x37, 0x80, 0x40, 0x12]),
558 Some(RomFormat::V64ByteSwapped)
559 );
560 assert_eq!(RomFormat::detect(&[0, 0, 0, 0]), None);
561 }
562
563 #[test]
564 fn save_sizes() {
565 assert_eq!(SaveType::Eeprom4k.size_bytes(), 512);
566 assert_eq!(SaveType::FlashRam.size_bytes(), 128 * 1024);
567 assert_eq!(SaveType::None.size_bytes(), 0);
568 }
569
570 #[test]
571 fn short_header_errors() {
572 assert_eq!(RomHeader::parse(&[0u8; 8]), Err(CartError::ShortHeader));
573 }
574
575 #[test]
576 fn constructs() {
577 let cart = Cart::new();
578 assert_eq!(cart.save_type(), SaveType::None);
579 }
580
581 #[test]
582 fn version_is_non_empty() {
583 assert!(!version().is_empty());
584 }
585
586 #[test]
587 fn crc32_matches_the_standard_check_vector() {
588 // The canonical CRC-32 check value for the ASCII string "123456789".
589 assert_eq!(crc32(b"123456789"), 0xCBF4_3926);
590 }
591
592 #[test]
593 fn an_unknown_ipl3_falls_back_to_6102() {
594 // An all-zero IPL3 is not in the fingerprint table — homebrew ships its
595 // own boot code, so 6102 is the documented fallback (cen64).
596 let rom = alloc::vec![0u8; 0x1000];
597 assert_eq!(Cic::from_ipl3(&rom), Cic::Cic6102);
598 // Too short to hold an IPL3 → same fallback, no panic.
599 assert_eq!(Cic::from_ipl3(&[0u8; 0x40]), Cic::Cic6102);
600 }
601
602 #[test]
603 fn boot_secrets_match_the_pif_nus_table() {
604 // N64brew PIF-NUS §checksum table — the IPL2 seeds differ per CIC (the
605 // real IPL2 consumes them), and the 6-byte checksum is the authenticator.
606 assert_eq!(
607 Cic::Cic6102.boot_secrets().ipl2_checksum,
608 [0xA5, 0x36, 0xC0, 0xF1, 0xD8, 0x59]
609 );
610 assert_eq!(Cic::Cic6102.boot_secrets().ipl2_seed, 0x3F);
611 assert_eq!(Cic::Cic6103.boot_secrets().ipl2_seed, 0x78);
612 assert_eq!(Cic::Cic6105.boot_secrets().ipl2_seed, 0x91);
613 assert_eq!(Cic::Cic6106.boot_secrets().ipl2_seed, 0x85);
614 // In every known CIC the IPL2 and IPL3 seeds coincide.
615 for cic in [
616 Cic::Cic6101,
617 Cic::Cic6102,
618 Cic::Cic6103,
619 Cic::Cic6105,
620 Cic::Cic6106,
621 ] {
622 let s = cic.boot_secrets();
623 assert_eq!(s.ipl2_seed, s.ipl3_seed, "seeds coincide for {cic:?}");
624 }
625 }
626}