What this program does
Read and hash all 1024 card frames before enabling a write. Holding L1 and R1 while pressing Cross performs another identity scan, creates a reserved one-block test save and verifies it. This does write persistent card data when requested. It does not format the card or overwrite an existing file; use a test card or an emulator card image when learning.
Build and run
Use the documented SDK checkout and tool setup. Run these commands from the repository root.
make disc EXAMPLE=hello-memcard
Open build/examples/mipsel-sony-psx/release/hello-memcard.cue in the emulator with its sibling BIN in the same directory. The first-program walkthrough explains installation and loading.
For original hardware, read the burned-disc requirements and warning.
Complete source
Complete, unabridged files from SDK revision f2fec1c301f1. Build inside the repository checkout: the manifest uses relative SDK paths, and some programs embed assets from elsewhere in the repository.
Runtime dependencies: psx-font, psx-gpu, psx-mc, psx-pad, psx-rt, psx-vram.
Cargo.toml · 30 lines
# SPDX-License-Identifier: GPL-2.0-or-later
[package]
name = "hello-memcard"
version = "0.1.0"
edition = "2021"
license = "GPL-2.0-or-later"
[workspace]
[lib]
name = "hello_memcard_recovery"
path = "src/main.rs"
[[bin]]
name = "hello-memcard"
path = "src/bin.rs"
test = false
[profile.release]
panic = "abort"
codegen-units = 1
lto = true
[dependencies]
psx-gpu = { path = "../../crates/psx-gpu" }
psx-rt = { path = "../../crates/psx-rt" }
psx-font = { path = "../../crates/psx-font" }
psx-vram = { path = "../../crates/psx-vram" }
psx-mc = { path = "../../crates/psx-mc", features = ["compress"] }
psx-pad = { path = "../../crates/psx-pad" }
src/bin.rs · 10 lines
// SPDX-License-Identifier: GPL-2.0-or-later
#![no_std]
#![no_main]
extern crate psx_rt;
#[no_mangle]
fn main() -> ! {
hello_memcard_recovery::run_standalone()
}
src/main.rs · 977 lines
// SPDX-License-Identifier: GPL-2.0-or-later
//! `hello-memcard` -- non-destructive, burnable memory-card hardware diagnostic.
//!
//! The test reads every one of the card's 1024 frames and hashes their contents
//! before enabling any write. It never formats a card and never overwrites an
//! existing file. Holding L1+R1 and pressing Cross performs a second full-card
//! identity scan, creates one reserved one-block test save with a native icon,
//! verifies it immediately, and leaves it to be found after a real power cycle.
#![no_std]
extern crate psx_rt;
#[cfg(target_arch = "mips")]
use psx_font::fonts::BASIC;
use psx_font::FontAtlas;
#[cfg(target_arch = "mips")]
use psx_gpu::display::{DisplayConfig, DoubleBuffer, Resolution, VideoMode};
use psx_gpu::Gpu;
use psx_mc::{
Block, Card, ControllerPort, Entry, Error, HardwareCard, SaveIcon, Slot, TransportFault,
TransportTrace, DATA_BLOCKS, FRAME_COUNT, FRAME_SIZE, MAX_NAME_LEN,
};
#[cfg(target_arch = "mips")]
use psx_pad::{button, poll_on, ButtonState, Port};
#[cfg(target_arch = "mips")]
use psx_vram::{Clut, TextureDepth, TexturePage};
mod recovery;
pub use recovery::{exact_legacy_size_target, repair_exact_legacy_size};
#[cfg(target_arch = "mips")]
const FONT_TPAGE: TexturePage = TexturePage::new(320, 0, TextureDepth::Bit4);
#[cfg(target_arch = "mips")]
const FONT_CLUT: Clut = Clut::new(320, 256);
// Deliberately unique and within the standard 20-byte BIOS filename limit.
const TEST_NAME: &str = "BESLES-00000PSXMC01";
const TEST_TITLE: &str = "PSOXIDE MC HARDWARE TEST";
const MAGIC: &[u8; 8] = b"PSXMCHW1";
const PAYLOAD_LEN: usize = 64;
/// The old diagnostic incorrectly stored container+payload bytes in the BIOS
/// directory size field: 16 + 64 = 0x50.
const LEGACY_DIRECTORY_SIZE: u32 = 0x50;
const BIOS_BLOCK_SIZE: u32 = 0x2000;
const GREEN: (u8, u8, u8) = (80, 230, 110);
const RED: (u8, u8, u8) = (240, 75, 75);
const AMBER: (u8, u8, u8) = (245, 190, 65);
const WHITE: (u8, u8, u8) = (225, 228, 235);
const GREY: (u8, u8, u8) = (145, 150, 165);
#[derive(Copy, Clone, PartialEq, Eq)]
enum Phase {
Scan,
PreWriteScan,
Ready,
Failed,
}
#[derive(Copy, Clone, PartialEq, Eq)]
enum Prior {
Absent,
Valid(u32),
Foreign,
ReadError(Error),
}
#[derive(Copy, Clone, PartialEq, Eq)]
enum WriteResult {
NotRun,
VerifyingCard,
Pass(u32),
RepairPass(u32),
CardChanged,
SafetyRefused(Error),
WriteError(Error),
ReadError(Error),
Mismatch,
}
/// Reusable state machine for the non-destructive memory-card diagnostic.
///
/// The standalone example and the unified hardware-test suite both use this
/// exact implementation, so their protocol, safety checks, and repair rules
/// cannot drift apart.
pub struct Diagnostic {
card: Card<HardwareCard>,
phase: Phase,
scan_frame: u16,
scan_hash: u32,
baseline_hash: u32,
scan_bad: u16,
first_error: Option<Error>,
first_fault: TransportTrace,
formatted: bool,
free_blocks: u8,
entries: [Entry; DATA_BLOCKS],
entry_count: u8,
prior: Prior,
write_result: WriteResult,
repair_expected: u32,
last_trace: TransportTrace,
page: u8,
}
impl Diagnostic {
/// A diagnostic on a controller port token it takes for as long as it runs.
pub fn new_on(port: ControllerPort) -> Self {
Self {
card: Card::new(HardwareCard::on_port(port, Slot::One)),
phase: Phase::Scan,
scan_frame: 0,
scan_hash: 0x811c_9dc5,
baseline_hash: 0,
scan_bad: 0,
first_error: None,
first_fault: empty_trace(),
formatted: false,
free_blocks: 0,
entries: [empty_entry(); DATA_BLOCKS],
entry_count: 0,
prior: Prior::Absent,
write_result: WriteResult::NotRun,
repair_expected: 0,
last_trace: empty_trace(),
page: 0,
}
}
/// [`new_on`](Self::new_on) on a token the caller does not hold.
#[deprecated(note = "use `Diagnostic::new_on` with the `ControllerPort` token")]
pub fn new() -> Self {
// SAFETY: a token is a logic guard, not a memory-safety one, and this
// constructor never took one.
Self::new_on(unsafe { ControllerPort::steal() })
}
/// The port the card sits on, for polling the pad between card steps.
pub fn controller_port(&mut self) -> &mut ControllerPort {
self.card.device().controller_port()
}
/// Read two frames per video frame: fast enough to finish in about nine
/// seconds at 60 Hz without hiding the progress screen.
pub fn scan_step(&mut self) {
if !matches!(self.phase, Phase::Scan | Phase::PreWriteScan) {
return;
}
let scan_phase = self.phase;
let mut count = 0;
while count < 2 && (self.scan_frame as usize) < FRAME_COUNT {
let mut frame = [0u8; FRAME_SIZE];
let result = self.card.device().read_frame(self.scan_frame, &mut frame);
self.last_trace = self.card.device().last_trace();
match result {
Ok(()) => {
self.scan_hash = hash_bytes(self.scan_hash, &frame);
}
Err(e) => {
self.scan_bad += 1;
if self.first_error.is_none() {
self.first_error = Some(e);
self.first_fault = self.last_trace;
}
// A low-level timeout on every remaining frame would take
// minutes and cannot yield a trustworthy filesystem test.
if self.last_trace.fault != TransportFault::None {
self.phase = Phase::Failed;
return;
}
}
}
self.scan_frame += 1;
count += 1;
}
if (self.scan_frame as usize) == FRAME_COUNT {
if self.scan_bad == 0 {
match scan_phase {
Phase::Scan => {
self.baseline_hash = self.scan_hash;
self.finish_scan();
}
Phase::PreWriteScan if self.scan_hash == self.baseline_hash => {
self.preflight_and_write()
}
Phase::PreWriteScan => {
self.write_result = WriteResult::CardChanged;
self.phase = Phase::Ready;
}
_ => {}
}
} else {
self.phase = Phase::Failed;
}
}
}
fn finish_scan(&mut self) {
match self.card.is_formatted() {
Ok(true) => self.formatted = true,
Ok(false) => {
self.phase = Phase::Ready;
return;
}
Err(e) => {
self.fail(e);
return;
}
}
self.repair_expected = 0;
if let Err(e) = self.card.validate_filesystem() {
if e != Error::Corrupt {
self.fail(e);
return;
}
match exact_legacy_size_target(&mut self.card, TEST_NAME, LEGACY_DIRECTORY_SIZE) {
Ok(Some(expected)) if expected == BIOS_BLOCK_SIZE => {
self.repair_expected = expected;
}
Ok(_) => {
self.fail(Error::Corrupt);
return;
}
Err(repair_error) => {
self.fail(repair_error);
return;
}
}
}
match self.card.free_blocks() {
Ok(n) => self.free_blocks = n as u8,
Err(e) => {
self.fail(e);
return;
}
}
match self.card.list(&mut self.entries) {
Ok(n) => self.entry_count = n as u8,
Err(e) => {
self.fail(e);
return;
}
}
let mut payload = [0u8; PAYLOAD_LEN];
self.prior = match self.card.read(TEST_NAME, &mut payload) {
Ok(n) => match validate_payload(&payload, n) {
Some(generation) => Prior::Valid(generation),
None => Prior::Foreign,
},
Err(Error::NotFound) => Prior::Absent,
Err(e) => Prior::ReadError(e),
};
if self.repair_expected != 0 && !matches!(self.prior, Prior::Valid(_)) {
self.fail(Error::Corrupt);
return;
}
self.last_trace = self.card.device().last_trace();
self.phase = Phase::Ready;
}
fn fail(&mut self, error: Error) {
self.first_error = Some(error);
self.first_fault = self.card.device().last_trace();
self.last_trace = self.first_fault;
self.phase = Phase::Failed;
}
fn write_enabled(&self) -> bool {
self.phase == Phase::Ready
&& self.formatted
&& ((self.prior == Prior::Absent && self.free_blocks > 0)
|| (self.repair_expected == BIOS_BLOCK_SIZE
&& matches!(self.prior, Prior::Valid(_))))
}
fn begin_write_test(&mut self) {
if !self.write_enabled() {
return;
}
self.phase = Phase::PreWriteScan;
self.scan_frame = 0;
self.scan_hash = 0x811c_9dc5;
self.scan_bad = 0;
self.first_error = None;
self.first_fault = empty_trace();
self.write_result = WriteResult::VerifyingCard;
}
fn preflight_and_write(&mut self) {
if self.repair_expected != 0 {
self.preflight_and_repair();
return;
}
if let Err(e) = self.card.validate_filesystem() {
self.write_result = WriteResult::SafetyRefused(e);
self.phase = Phase::Ready;
return;
}
let mut existing = [0u8; PAYLOAD_LEN];
match self.card.read(TEST_NAME, &mut existing) {
Err(Error::NotFound) => {}
Ok(_) | Err(Error::BadContainer) => {
self.prior = Prior::Foreign;
self.write_result = WriteResult::SafetyRefused(Error::Exists);
self.phase = Phase::Ready;
return;
}
Err(e) => {
self.write_result = WriteResult::SafetyRefused(e);
self.phase = Phase::Ready;
return;
}
}
match self.card.free_blocks() {
Ok(0) => {
self.write_result = WriteResult::SafetyRefused(Error::NoSpace);
self.phase = Phase::Ready;
return;
}
Ok(_) => {}
Err(e) => {
self.write_result = WriteResult::SafetyRefused(e);
self.phase = Phase::Ready;
return;
}
}
let generation = 1;
let payload = make_payload(generation);
let icon = make_test_icon();
if let Err(e) = self
.card
.write_with_icon(TEST_NAME, TEST_TITLE, &payload, &icon)
{
self.write_result = WriteResult::WriteError(e);
self.last_trace = self.card.device().last_trace();
self.phase = Phase::Ready;
return;
}
let mut readback = [0u8; PAYLOAD_LEN];
match self.card.read(TEST_NAME, &mut readback) {
Ok(n) if validate_payload(&readback, n) == Some(generation) => {
self.write_result = WriteResult::Pass(generation);
self.prior = Prior::Valid(generation);
}
Ok(_) => self.write_result = WriteResult::Mismatch,
Err(e) => self.write_result = WriteResult::ReadError(e),
}
self.last_trace = self.card.device().last_trace();
if self.write_result == WriteResult::Pass(generation) {
if let Err(e) = self.card.validate_filesystem() {
self.write_result = WriteResult::SafetyRefused(e);
} else {
self.free_blocks = self.card.free_blocks().unwrap_or(0) as u8;
self.entry_count = self.card.list(&mut self.entries).unwrap_or(0) as u8;
}
}
self.phase = Phase::Ready;
}
fn preflight_and_repair(&mut self) {
let generation = match self.prior {
Prior::Valid(generation) => generation,
_ => {
self.write_result = WriteResult::SafetyRefused(Error::Corrupt);
self.phase = Phase::Ready;
return;
}
};
match exact_legacy_size_target(&mut self.card, TEST_NAME, LEGACY_DIRECTORY_SIZE) {
Ok(Some(expected)) if expected == self.repair_expected => {}
Ok(_) => {
self.write_result = WriteResult::SafetyRefused(Error::Corrupt);
self.phase = Phase::Ready;
return;
}
Err(e) => {
self.write_result = WriteResult::SafetyRefused(e);
self.phase = Phase::Ready;
return;
}
}
// The reserved name alone is not authority to repair: verify that its
// payload is exactly the diagnostic save observed before the second
// unchanged-card scan.
let mut payload = [0u8; PAYLOAD_LEN];
match self.card.read(TEST_NAME, &mut payload) {
Ok(n) if validate_payload(&payload, n) == Some(generation) => {}
Ok(_) => {
self.write_result = WriteResult::SafetyRefused(Error::BadContainer);
self.phase = Phase::Ready;
return;
}
Err(e) => {
self.write_result = WriteResult::SafetyRefused(e);
self.phase = Phase::Ready;
return;
}
}
match repair_exact_legacy_size(&mut self.card, TEST_NAME, LEGACY_DIRECTORY_SIZE) {
Ok(expected) if expected == BIOS_BLOCK_SIZE => {}
Ok(_) => {
self.write_result = WriteResult::SafetyRefused(Error::Corrupt);
self.phase = Phase::Ready;
return;
}
Err(e) => {
self.write_result = WriteResult::WriteError(e);
self.last_trace = self.card.device().last_trace();
self.phase = Phase::Ready;
return;
}
}
let mut readback = [0u8; PAYLOAD_LEN];
self.write_result = match self.card.read(TEST_NAME, &mut readback) {
Ok(n) if validate_payload(&readback, n) == Some(generation) => {
WriteResult::RepairPass(generation)
}
Ok(_) => WriteResult::Mismatch,
Err(e) => WriteResult::ReadError(e),
};
self.last_trace = self.card.device().last_trace();
if self.write_result == WriteResult::RepairPass(generation) {
if let Err(e) = self.card.validate_filesystem() {
self.write_result = WriteResult::SafetyRefused(e);
} else {
self.repair_expected = 0;
self.free_blocks = self.card.free_blocks().unwrap_or(0) as u8;
self.entry_count = self.card.list(&mut self.entries).unwrap_or(0) as u8;
}
}
self.phase = Phase::Ready;
}
pub fn page_left(&mut self) {
self.page = self.page.saturating_sub(1);
}
pub fn page_right(&mut self) {
self.page = (self.page + 1).min(2);
}
/// Attempt the guarded write/repair operation. Callers must pass the
/// deliberate L1+R1+Cross chord; a plain Cross can never write.
pub fn guarded_write(&mut self, l1: bool, r1: bool, cross_edge: bool) {
if l1 && r1 && cross_edge {
self.begin_write_test();
}
}
pub fn draw(&self, font: &FontAtlas) {
match self.page {
0 => draw_summary(font, self),
1 => draw_files(font, self),
_ => draw_transport(font, self),
}
draw_footer(font, self.page);
}
}
/// Run the diagnostic as its original standalone burnable example.
#[cfg(target_arch = "mips")]
pub fn run_standalone() -> ! {
let Some(peripherals) = psx_rt::Peripherals::take() else {
// This is the program's entry: nothing took the peripherals before
// it, so this branch cannot run.
loop {}
};
let mut gpu = Gpu::new(
peripherals.gpu_dma,
DisplayConfig::new(VideoMode::Ntsc, Resolution::R320X240),
);
let mut fb = DoubleBuffer::new(Resolution::R320X240);
gpu.set_draw_area((0, 0), (319, 239));
gpu.set_draw_offset((0, 0));
let font = FontAtlas::upload(&BASIC, FONT_TPAGE, FONT_CLUT);
let mut app = Diagnostic::new_on(peripherals.controller_port);
let mut previous = ButtonState::NONE;
loop {
app.scan_step();
// The card transaction is complete and /CS is high before polling the
// pad; the two devices share SIO0 and are never accessed concurrently.
let current = poll_on(app.controller_port(), Port::One).buttons;
if pressed(current, previous, button::LEFT) {
app.page_left();
}
if pressed(current, previous, button::RIGHT) {
app.page_right();
}
app.guarded_write(
current.is_held(button::L1),
current.is_held(button::R1),
pressed(current, previous, button::CROSS),
);
previous = current;
fb.clear(&mut gpu, (9, 11, 18));
app.draw(&font);
gpu.wait_idle();
psx_rt::interrupts::wait_vblank();
fb.swap(&mut gpu);
}
}
#[cfg(not(target_arch = "mips"))]
pub fn run_standalone() -> ! {
panic!("the memory-card hardware diagnostic only runs on a PS1 target")
}
fn draw_summary(font: &FontAtlas, app: &Diagnostic) {
font.draw_text(8, 6, "PSOXIDE MEMORY CARD HW TEST", WHITE);
font.draw_text(8, 20, "SLOT 1 - NEVER FORMATS THE CARD", AMBER);
let mut y = 42;
let mut b = Text::new();
b.push("RAW SCAN ");
b.dec(app.scan_frame as u32);
b.push("/1024");
line(
font,
y,
b.as_str(),
if app.phase == Phase::Failed {
RED
} else {
WHITE
},
);
y += 13;
b.clear();
b.push("READ ERRORS ");
b.dec(app.scan_bad as u32);
line(
font,
y,
b.as_str(),
if app.scan_bad == 0 { GREEN } else { RED },
);
y += 13;
b.clear();
b.push("CARD HASH ");
b.hex32(app.scan_hash);
line(font, y, b.as_str(), GREY);
y += 13;
match app.phase {
Phase::Scan => line(font, y, "STATUS: READING EVERY FRAME...", AMBER),
Phase::PreWriteScan => line(font, y, "STATUS: VERIFYING SAME CARD...", AMBER),
Phase::Failed => {
b.clear();
b.push("STATUS: FAIL ");
if let Some(e) = app.first_error {
b.push(error_name(e));
}
line(font, y, b.as_str(), RED);
}
Phase::Ready if !app.formatted => line(font, y, "STATUS: UNFORMATTED - NO WRITE", RED),
Phase::Ready if app.repair_expected != 0 => {
line(font, y, "STATUS: DIRECTORY REPAIR NEEDED", AMBER)
}
Phase::Ready => line(font, y, "STATUS: FULL READ PASS", GREEN),
}
y += 16;
if app.phase == Phase::Ready && app.formatted {
b.clear();
b.push("FILES ");
b.dec(app.entry_count as u32);
b.push(" FREE BLOCKS ");
b.dec(app.free_blocks as u32);
line(font, y, b.as_str(), WHITE);
y += 14;
match app.prior {
Prior::Absent => line(font, y, "POWER-CYCLE SAVE: NOT CREATED", GREY),
Prior::Valid(generation) if app.repair_expected != 0 => {
b.clear();
b.push("TEST SAVE VERIFIED GEN ");
b.dec(generation);
line(font, y, b.as_str(), GREEN);
}
Prior::Valid(generation) => {
b.clear();
b.push("POWER-CYCLE SAVE: PASS GEN ");
b.dec(generation);
line(font, y, b.as_str(), GREEN);
}
Prior::Foreign => line(font, y, "RESERVED NAME OCCUPIED - REFUSING", RED),
Prior::ReadError(e) => {
b.clear();
b.push("TEST SAVE READ FAIL ");
b.push(error_name(e));
line(font, y, b.as_str(), RED);
}
}
y += 14;
match app.write_result {
WriteResult::NotRun if app.repair_expected != 0 && app.write_enabled() => {
line(font, y, "SIZE 0050 -> 2000 (ONE ENTRY)", AMBER);
y += 14;
line(font, y, "HOLD L1+R1, PRESS X TO REPAIR", AMBER)
}
WriteResult::NotRun if app.write_enabled() => {
line(font, y, "HOLD L1+R1, PRESS X TO WRITE", AMBER)
}
WriteResult::NotRun if matches!(app.prior, Prior::Valid(_)) => {
line(font, y, "COMPLETE - TEST SAVE NEVER OVERWRITTEN", GREEN)
}
WriteResult::NotRun => line(font, y, "WRITE LOCKED FOR CARD SAFETY", RED),
WriteResult::VerifyingCard => line(font, y, "SECOND FULL SCAN IN PROGRESS", AMBER),
WriteResult::Pass(generation) => {
b.clear();
b.push("WRITE+READBACK PASS GEN ");
b.dec(generation);
line(font, y, b.as_str(), GREEN);
y += 14;
line(font, y, "POWER OFF, REBOOT, EXPECT PASS", AMBER);
}
WriteResult::RepairPass(generation) => {
b.clear();
b.push("DIRECTORY REPAIR PASS GEN ");
b.dec(generation);
line(font, y, b.as_str(), GREEN);
y += 14;
line(font, y, "POWER OFF, CHECK CARD MENU", AMBER);
}
WriteResult::CardChanged => line(font, y, "REFUSED: CARD CONTENTS CHANGED", RED),
WriteResult::SafetyRefused(e) => {
b.clear();
b.push("SAFETY REFUSAL ");
b.push(error_name(e));
line(font, y, b.as_str(), RED);
}
WriteResult::WriteError(e) => {
b.clear();
b.push("WRITE FAIL ");
b.push(error_name(e));
line(font, y, b.as_str(), RED);
}
WriteResult::ReadError(e) => {
b.clear();
b.push("READBACK FAIL ");
b.push(error_name(e));
line(font, y, b.as_str(), RED);
}
WriteResult::Mismatch => line(font, y, "READBACK FAIL MISMATCH", RED),
}
}
}
fn draw_files(font: &FontAtlas, app: &Diagnostic) {
font.draw_text(8, 6, "CARD DIRECTORY (READ ONLY)", WHITE);
if app.phase != Phase::Ready || !app.formatted {
font.draw_text(8, 30, "AVAILABLE AFTER A CLEAN FULL SCAN", AMBER);
return;
}
if app.entry_count == 0 {
font.draw_text(8, 30, "NO SAVE FILES", GREY);
return;
}
let mut y = 25;
let mut i = 0usize;
while i < app.entry_count as usize {
let entry = app.entries[i];
let mut b = Text::new();
b.dec((i + 1) as u32);
b.push(" ");
b.push(entry.name());
b.push(" [");
b.dec(entry.blocks as u32);
b.push("]");
line(font, y, b.as_str(), WHITE);
y += 12;
i += 1;
}
}
fn draw_transport(font: &FontAtlas, app: &Diagnostic) {
font.draw_text(8, 6, "LAST SIO0 TRANSACTION", WHITE);
let trace = if app.phase == Phase::Failed {
app.first_fault
} else {
app.last_trace
};
let mut b = Text::new();
b.push("EXCHANGES ");
b.dec(trace.exchanges as u32);
b.push(" ACKS ");
b.dec(trace.acknowledgements as u32);
line(font, 28, b.as_str(), WHITE);
b.clear();
b.push("FAULT ");
b.push(fault_name(trace.fault));
if trace.fault != TransportFault::None {
b.push(" @BYTE ");
b.dec(trace.fault_exchange as u32);
}
line(
font,
43,
b.as_str(),
if trace.fault == TransportFault::None {
GREEN
} else {
RED
},
);
font.draw_text(8, 64, "RX PREFIX (HEX)", GREY);
b.clear();
let mut i = 0;
while i < trace.response_prefix.len() {
b.hex8(trace.response_prefix[i]);
b.push(" ");
i += 1;
}
line(font, 78, b.as_str(), WHITE);
font.draw_text(8, 104, "EXPECTED READ PREFIX", GREY);
font.draw_text(8, 118, "FF FLAG 5A 5D 00 MSB 5C 5D MSB LSB", WHITE);
font.draw_text(8, 145, "REPORT: FAULT, BYTE, RX PREFIX,", GREY);
font.draw_text(8, 158, "SCAN ERRORS AND CARD HASH.", GREY);
}
fn draw_footer(font: &FontAtlas, page: u8) {
let label = match page {
0 => "< SUMMARY 1/3 FILES >",
1 => "< SUMMARY 2/3 SIO >",
_ => "< FILES 3/3 SIO >",
};
font.draw_text(8, 222, label, GREY);
}
fn line(font: &FontAtlas, y: i16, text: &str, colour: (u8, u8, u8)) {
font.draw_text(8, y, text, colour);
}
#[cfg(target_arch = "mips")]
fn pressed(now: ButtonState, previous: ButtonState, mask: u16) -> bool {
now.is_held(mask) && !previous.is_held(mask)
}
const fn empty_entry() -> Entry {
Entry {
name: [0; MAX_NAME_LEN + 1],
name_len: 0,
blocks: 0,
}
}
const fn empty_trace() -> TransportTrace {
TransportTrace {
exchanges: 0,
acknowledgements: 0,
fault: TransportFault::None,
fault_exchange: u16::MAX,
response_prefix: [0xff; 10],
}
}
fn make_test_icon() -> SaveIcon {
let mut palette = [0u16; 16];
palette[0] = 0x0000; // transparent
palette[1] = 0x1084; // dark blue-grey fill
palette[2] = 0x7fe0; // cyan outline
palette[3] = 0x7fff; // white
palette[4] = 0x23e8; // green confirmation mark
let mut pixels = [0u8; FRAME_SIZE];
let mut y = 1usize;
while y < 15 {
let mut x = 2usize;
while x < 14 {
let colour = if x == 2 || x == 13 || y == 1 || y == 14 {
2
} else {
1
};
set_icon_pixel(&mut pixels, x, y, colour);
x += 1;
}
y += 1;
}
// Memory-card label and notch.
let mut x = 5;
while x < 11 {
set_icon_pixel(&mut pixels, x, 3, 3);
x += 1;
}
set_icon_pixel(&mut pixels, 10, 1, 0);
set_icon_pixel(&mut pixels, 11, 1, 0);
// A compact check mark.
set_icon_pixel(&mut pixels, 5, 9, 4);
set_icon_pixel(&mut pixels, 6, 10, 4);
set_icon_pixel(&mut pixels, 7, 11, 4);
set_icon_pixel(&mut pixels, 8, 10, 4);
set_icon_pixel(&mut pixels, 9, 9, 4);
set_icon_pixel(&mut pixels, 10, 8, 4);
set_icon_pixel(&mut pixels, 11, 7, 4);
SaveIcon::new(palette, pixels)
}
fn set_icon_pixel(pixels: &mut [u8; FRAME_SIZE], x: usize, y: usize, colour: u8) {
let at = (y * 16 + x) / 2;
if x & 1 == 0 {
pixels[at] = (pixels[at] & 0xf0) | (colour & 0x0f);
} else {
pixels[at] = (pixels[at] & 0x0f) | ((colour & 0x0f) << 4);
}
}
fn make_payload(generation: u32) -> [u8; PAYLOAD_LEN] {
let mut out = [0u8; PAYLOAD_LEN];
out[..MAGIC.len()].copy_from_slice(MAGIC);
out[8] = 1;
out[9..13].copy_from_slice(&generation.to_le_bytes());
let mut i = 13;
while i < 60 {
out[i] = (i as u8)
.wrapping_mul(37)
.wrapping_add(generation as u8)
.rotate_left((generation & 7) as u32);
i += 1;
}
let checksum = hash_bytes(0x811c_9dc5, &out[..60]);
out[60..64].copy_from_slice(&checksum.to_le_bytes());
out
}
fn validate_payload(payload: &[u8; PAYLOAD_LEN], len: usize) -> Option<u32> {
if len != PAYLOAD_LEN || &payload[..8] != MAGIC || payload[8] != 1 {
return None;
}
let generation = u32::from_le_bytes([payload[9], payload[10], payload[11], payload[12]]);
if make_payload(generation) == *payload {
Some(generation)
} else {
None
}
}
fn hash_bytes(mut hash: u32, bytes: &[u8]) -> u32 {
let mut i = 0;
while i < bytes.len() {
hash ^= bytes[i] as u32;
hash = hash.wrapping_mul(0x0100_0193);
i += 1;
}
hash
}
fn error_name(error: Error) -> &'static str {
match error {
Error::NoCard => "NO-CARD",
Error::Protocol => "PROTOCOL",
Error::BadChecksum => "CHECKSUM",
Error::OutOfRange => "RANGE",
Error::NotFormatted => "UNFORMATTED",
Error::NotFound => "NOT-FOUND",
Error::NoSpace => "NO-SPACE",
Error::Exists => "EXISTS",
Error::Corrupt => "CORRUPT",
Error::BufferTooSmall => "BUFFER",
Error::BadContainer => "CONTAINER",
Error::Compression => "COMPRESS",
Error::BadName => "BAD-NAME",
}
}
fn fault_name(fault: TransportFault) -> &'static str {
match fault {
TransportFault::None => "NONE",
TransportFault::TxTimeout => "TX-TIMEOUT",
TransportFault::RxTimeout => "RX-TIMEOUT",
TransportFault::AckTimeout => "ACK-TIMEOUT",
TransportFault::AckReleaseTimeout => "ACK-RELEASE",
}
}
struct Text {
bytes: [u8; 64],
len: usize,
}
impl Text {
const fn new() -> Self {
Self {
bytes: [0; 64],
len: 0,
}
}
fn clear(&mut self) {
self.len = 0;
}
fn push(&mut self, text: &str) {
for &byte in text.as_bytes() {
if self.len < self.bytes.len() {
self.bytes[self.len] = byte;
self.len += 1;
}
}
}
fn dec(&mut self, value: u32) {
let mut digits = [0u8; 10];
let mut n = value;
let mut len = 0;
loop {
digits[len] = b'0' + (n % 10) as u8;
len += 1;
n /= 10;
if n == 0 {
break;
}
}
while len > 0 {
len -= 1;
self.push_byte(digits[len]);
}
}
fn hex8(&mut self, value: u8) {
self.push_byte(hex(value >> 4));
self.push_byte(hex(value & 15));
}
fn hex32(&mut self, value: u32) {
let mut shift = 28;
loop {
self.push_byte(hex(((value >> shift) & 15) as u8));
if shift == 0 {
break;
}
shift -= 4;
}
}
fn push_byte(&mut self, byte: u8) {
if self.len < self.bytes.len() {
self.bytes[self.len] = byte;
self.len += 1;
}
}
fn as_str(&self) -> &str {
// SAFETY: every append path emits ASCII.
unsafe { core::str::from_utf8_unchecked(&self.bytes[..self.len]) }
}
}
fn hex(value: u8) -> u8 {
if value < 10 {
b'0' + value
} else {
b'A' + value - 10
}
}
src/recovery.rs · 272 lines
// SPDX-License-Identifier: GPL-2.0-or-later
//! Recovery logic for the one malformed save created by the first hardware
//! diagnostic disc. This intentionally lives in the diagnostic, not `psx-mc`.
use psx_mc::{Block, Card, Error, Result, DATA_BLOCKS, FRAME_SIZE};
const E_STATE: usize = 0;
const E_SIZE: usize = 4;
const E_LINK: usize = 8;
const E_NAME: usize = 10;
const E_CHK: usize = 127;
const ST_FIRST: u8 = 0x51;
const ST_MIDDLE: u8 = 0x52;
const ST_LAST: u8 = 0x53;
const LINK_NONE: u16 = 0xffff;
const BLOCK_BYTES: u32 = 0x2000;
/// Return the correct allocated size only if the card has exactly one named
/// entry with `legacy_size`, every chain is structurally sound, and every
/// other entry already has a BIOS-compatible allocated size.
pub fn exact_legacy_size_target<B: Block>(
card: &mut Card<B>,
name: &str,
legacy_size: u32,
) -> Result<Option<u32>> {
let mut header = [0u8; FRAME_SIZE];
card.device().read_frame(0, &mut header)?;
if header[0] != b'M' || header[1] != b'C' {
return Err(Error::NotFormatted);
}
if checksum(&header) != header[E_CHK] {
return Err(Error::Corrupt);
}
let mut states = [0u8; DATA_BLOCKS];
let mut links = [LINK_NONE; DATA_BLOCKS];
let mut sizes = [0u32; DATA_BLOCKS];
let mut named = [false; DATA_BLOCKS];
let mut named_count = 0u8;
for index in 0..DATA_BLOCKS {
let entry = read_dir(card, index)?;
if checksum(&entry) != entry[E_CHK] {
return Err(Error::Corrupt);
}
let state = entry[E_STATE];
if !is_free(state) && !matches!(state, ST_FIRST | ST_MIDDLE | ST_LAST) {
return Err(Error::Corrupt);
}
states[index] = state;
links[index] = u16::from_le_bytes([entry[E_LINK], entry[E_LINK + 1]]);
sizes[index] = read_u32(&entry[E_SIZE..]);
if state == ST_FIRST && entry_name_eq(&entry, name.as_bytes()) {
named[index] = true;
named_count = named_count.saturating_add(1);
}
}
if named_count > 1 {
return Ok(None);
}
let mut visited = 0u16;
let mut target = None;
for first in 0..DATA_BLOCKS {
if states[first] != ST_FIRST {
continue;
}
let mut current = first;
let mut first_node = true;
let mut chain_len = 0u32;
loop {
let bit = 1u16 << current;
if visited & bit != 0 {
return Err(Error::Corrupt);
}
visited |= bit;
chain_len += 1;
let state = states[current];
if first_node {
if state != ST_FIRST {
return Err(Error::Corrupt);
}
first_node = false;
} else if !matches!(state, ST_MIDDLE | ST_LAST) {
return Err(Error::Corrupt);
}
let link = links[current];
if state == ST_LAST || (state == ST_FIRST && link == LINK_NONE) {
if link != LINK_NONE {
return Err(Error::Corrupt);
}
break;
}
if link == LINK_NONE || link as usize >= DATA_BLOCKS {
return Err(Error::Corrupt);
}
current = link as usize;
}
let expected = chain_len * BLOCK_BYTES;
if named[first] {
if sizes[first] != legacy_size || sizes[first] == expected {
return Ok(None);
}
target = Some(expected);
} else if sizes[first] != expected {
return Err(Error::Corrupt);
}
}
for index in 0..DATA_BLOCKS {
if matches!(states[index], ST_MIDDLE | ST_LAST) && visited & (1u16 << index) == 0 {
return Err(Error::Corrupt);
}
}
Ok(target)
}
/// Repair the exact legacy entry by writing one directory frame, read it back,
/// and run the SDK's strict validator. The caller remains responsible for
/// authenticating the diagnostic payload before calling this function.
pub fn repair_exact_legacy_size<B: Block>(
card: &mut Card<B>,
name: &str,
legacy_size: u32,
) -> Result<u32> {
let expected = exact_legacy_size_target(card, name, legacy_size)?.ok_or(Error::Corrupt)?;
let mut target = None;
for index in 0..DATA_BLOCKS {
let entry = read_dir(card, index)?;
if entry[E_STATE] == ST_FIRST && entry_name_eq(&entry, name.as_bytes()) {
if target.is_some() {
return Err(Error::Corrupt);
}
target = Some((index, entry));
}
}
let (index, mut entry) = target.ok_or(Error::NotFound)?;
if checksum(&entry) != entry[E_CHK] || read_u32(&entry[E_SIZE..]) != legacy_size {
return Err(Error::Corrupt);
}
entry[E_SIZE..E_SIZE + 4].copy_from_slice(&expected.to_le_bytes());
entry[E_CHK] = checksum(&entry);
card.device().write_frame(1 + index as u16, &entry)?;
if read_dir(card, index)? != entry {
return Err(Error::Corrupt);
}
card.validate_filesystem()?;
Ok(expected)
}
fn read_dir<B: Block>(card: &mut Card<B>, index: usize) -> Result<[u8; FRAME_SIZE]> {
let mut entry = [0u8; FRAME_SIZE];
card.device().read_frame(1 + index as u16, &mut entry)?;
Ok(entry)
}
fn checksum(frame: &[u8; FRAME_SIZE]) -> u8 {
frame[..E_CHK].iter().fold(0u8, |sum, byte| sum ^ byte)
}
fn read_u32(bytes: &[u8]) -> u32 {
u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
}
fn is_free(state: u8) -> bool {
state & 0xf0 == 0xa0
}
fn entry_name_eq(entry: &[u8; FRAME_SIZE], name: &[u8]) -> bool {
if name.len() > 20 || &entry[E_NAME..E_NAME + name.len()] != name {
return false;
}
name.len() == 20 || entry[E_NAME + name.len()] == 0
}
#[cfg(test)]
mod tests {
extern crate std;
use super::{exact_legacy_size_target, repair_exact_legacy_size};
use psx_mc::{Card, Entry, Error, RamCard, CARD_SIZE, DATA_BLOCKS, FRAME_SIZE, MAX_NAME_LEN};
use std::vec::Vec;
const TARGET: &str = "BESLES-00000PSXMC01";
const BEFORE: &str = "BESLES-00000BEFORE1";
const AFTER: &str = "BESLES-00000AFTER01";
fn card_with_legacy_entry() -> Vec<u8> {
let mut card = Card::new(RamCard::new());
card.format().unwrap();
card.write(BEFORE, "BEFORE", b"before").unwrap();
card.write(TARGET, "PSOXIDE MC HARDWARE TEST", &[0x5a; 64])
.unwrap();
card.write(AFTER, "AFTER", b"after").unwrap();
let mut image = card.into_inner().image().to_vec();
set_directory_size(&mut image, 1, 0x50);
image
}
#[test]
fn changes_only_target_size_and_checksum_then_survives_cold_boot() {
let image = card_with_legacy_entry();
let before = image.clone();
let mut card = Card::new(RamCard::from_image(&image).unwrap());
assert_eq!(card.validate_filesystem(), Err(Error::Corrupt));
assert_eq!(
exact_legacy_size_target(&mut card, TARGET, 0x50),
Ok(Some(0x2000))
);
assert_eq!(
repair_exact_legacy_size(&mut card, TARGET, 0x50),
Ok(0x2000)
);
let after = card.into_inner().image().to_vec();
let base = (1 + 1) * FRAME_SIZE;
let changed: Vec<_> = (0..CARD_SIZE)
.filter(|&index| before[index] != after[index])
.collect();
assert_eq!(changed, [base + 4, base + 5, base + 127]);
let mut cold_boot = Card::new(RamCard::from_image(&after).unwrap());
cold_boot.validate_filesystem().unwrap();
let mut entries = [blank_entry(); DATA_BLOCKS];
assert_eq!(cold_boot.list(&mut entries).unwrap(), 3);
assert_eq!(entries[0].name(), BEFORE);
assert_eq!(entries[1].name(), TARGET);
assert_eq!(entries[2].name(), AFTER);
}
#[test]
fn refuses_wrong_size_name_or_unrelated_corruption() {
let image = card_with_legacy_entry();
let mut card = Card::new(RamCard::from_image(&image).unwrap());
assert_eq!(exact_legacy_size_target(&mut card, TARGET, 0x60), Ok(None));
assert_eq!(
exact_legacy_size_target(&mut card, "BESLES-00000UNKNOWN", 0x50),
Err(Error::Corrupt)
);
let mut image = image;
set_directory_size(&mut image, 2, 0x60);
let mut card = Card::new(RamCard::from_image(&image).unwrap());
assert_eq!(
exact_legacy_size_target(&mut card, TARGET, 0x50),
Err(Error::Corrupt)
);
assert_eq!(
repair_exact_legacy_size(&mut card, TARGET, 0x50),
Err(Error::Corrupt)
);
}
fn set_directory_size(image: &mut [u8], index: usize, size: u32) {
let base = (1 + index) * FRAME_SIZE;
image[base + 4..base + 8].copy_from_slice(&size.to_le_bytes());
image[base + 127] = image[base..base + 127]
.iter()
.fold(0u8, |sum, byte| sum ^ byte);
}
const fn blank_entry() -> Entry {
Entry {
name: [0; MAX_NAME_LEN + 1],
name_len: 0,
blocks: 0,
}
}
}