psx_mc/lib.rs
1// SPDX-License-Identifier: GPL-2.0-or-later
2//! PlayStation 1 memory-card driver.
3//!
4//! Three layers, each usable on its own:
5//!
6//! 1. **Transport** -- a [`Block`] device exposes the card as 1024 raw 128-byte
7//! frames. [`HardwareCard`] talks to a physical card over SIO0 (feature
8//! `hw`, on by default); [`RamCard`] is an in-memory image for host tests and
9//! virtual saves.
10//! 2. **Filesystem** -- [`Card`] wraps any [`Block`] and implements the standard
11//! PS1 directory: named files, block allocation with the real link-chain,
12//! BIOS-visible title + icon frames, and per-frame XOR checksums. Saves show
13//! up in the console's memory-card manager like any retail game.
14//! 3. **Container** -- [`Card::write`] wraps payloads in a small self-describing
15//! header so [`Card::read`] can transparently handle both plain and (feature
16//! `compress`) LZSS-compressed saves.
17//!
18//! ## Why talk to SIO0 directly
19//!
20//! Like [`psx_pad`](../psx_pad), this avoids BIOS syscalls so the same code runs
21//! under an HLE BIOS side-load or a real boot. The card protocol is a fixed
22//! request/response the [`hardware`] module drives byte by byte, over the same
23//! shared SIO0 transport the pad driver polls controllers with, so the port has
24//! one owner (the `ControllerPort` token) however many drivers use it.
25//!
26//! ## Example
27//!
28//! ```text
29//! use psx_mc::{Card, HardwareCard, Slot};
30//!
31//! // `port` is the `ControllerPort` token (or a `&mut` of it, to keep polling
32//! // pads between saves).
33//! let mut card = Card::new(HardwareCard::on_port(&mut port, Slot::One));
34//! if !card.is_formatted()? {
35//! card.format()?;
36//! }
37//! card.write("BESLES-00000MYGAME01", "MY GAME", &save_bytes)?;
38//!
39//! let mut buf = [0u8; 8192];
40//! let len = card.read("BESLES-00000MYGAME01", &mut buf)?;
41//! ```
42//!
43//! All logic below the transport is pure and covered by host unit tests
44//! (`cargo test --no-default-features`, optionally `--features compress`).
45
46#![no_std]
47#![allow(clippy::result_unit_err)]
48
49mod fs;
50mod ram;
51
52#[cfg(test)]
53mod tests;
54
55#[cfg(feature = "compress")]
56pub mod compress;
57
58#[cfg(feature = "hw")]
59#[doc(alias = "SIO0")]
60pub mod hardware;
61
62pub use ram::RamCard;
63
64#[cfg(feature = "hw")]
65pub use hardware::{HardwareCard, Slot, TransportFault, TransportTrace};
66/// The token a [`HardwareCard`] drives the port through.
67#[cfg(feature = "hw")]
68pub use psx_io::periph::ControllerPort;
69
70// --------------------------------------------------------------------------
71// Card geometry (fixed by the hardware).
72// --------------------------------------------------------------------------
73
74/// Bytes per frame (the transfer unit of the serial protocol).
75pub const FRAME_SIZE: usize = 128;
76/// Frames per 8 KiB block.
77pub const FRAMES_PER_BLOCK: usize = 64;
78/// Total blocks on a standard card (block 0 is the directory).
79pub const BLOCK_COUNT: usize = 16;
80/// Total frames on a standard card.
81pub const FRAME_COUNT: usize = BLOCK_COUNT * FRAMES_PER_BLOCK; // 1024
82/// Total card size in bytes (128 KiB).
83pub const CARD_SIZE: usize = FRAME_COUNT * FRAME_SIZE;
84/// Usable save blocks (blocks 1..=15; block 0 holds the directory).
85pub const DATA_BLOCKS: usize = BLOCK_COUNT - 1;
86/// Maximum file-name length (region+product code + name), excluding the NUL.
87pub const MAX_NAME_LEN: usize = 20;
88/// Renamed to [`MAX_NAME_LEN`].
89#[deprecated(note = "renamed to `MAX_NAME_LEN`")]
90pub const MAX_NAME: usize = MAX_NAME_LEN;
91
92/// A native PS1 memory-card save icon.
93///
94/// The BIOS displays a 16×16, 4bpp indexed image. `pixels` contains two
95/// left-to-right pixels per byte (low nibble first); `palette` contains the 16
96/// BGR555 colours. This type represents one static icon frame.
97#[derive(Copy, Clone, Debug, PartialEq, Eq)]
98pub struct SaveIcon {
99 /// Sixteen BGR555 palette entries. Palette index zero is conventionally
100 /// transparent.
101 pub palette: [u16; 16],
102 /// 16×16 pixels at 4bpp: exactly one 128-byte memory-card frame.
103 pub pixels: [u8; FRAME_SIZE],
104}
105
106impl SaveIcon {
107 /// Construct a static save icon from its native palette and packed pixels.
108 pub const fn new(palette: [u16; 16], pixels: [u8; FRAME_SIZE]) -> Self {
109 Self { palette, pixels }
110 }
111}
112
113// --------------------------------------------------------------------------
114// Errors.
115// --------------------------------------------------------------------------
116
117/// Everything that can go wrong talking to a card or its filesystem.
118#[derive(Copy, Clone, Debug, PartialEq, Eq)]
119pub enum Error {
120 /// No card responded on the port (no `/ACK`), or it was removed mid-transfer.
121 NoCard,
122 /// The card broke protocol (bad ID/ACK/terminator byte).
123 Protocol,
124 /// A frame's stored checksum did not match its data (read); likely a
125 /// corrupt or half-written card.
126 BadChecksum,
127 /// Frame index past the end of the card.
128 OutOfRange,
129 /// The card has no valid `MC` directory header (needs [`Card::format`]).
130 NotFormatted,
131 /// No file with the requested name exists.
132 NotFound,
133 /// Not enough free blocks for the save.
134 NoSpace,
135 /// A file with that name already exists and `overwrite` was not set.
136 Exists,
137 /// The directory link-chain is inconsistent (corrupt card).
138 Corrupt,
139 /// The caller's read buffer is smaller than the stored payload.
140 BufferTooSmall,
141 /// The save is not a `psx-mc` container, or its header is malformed.
142 BadContainer,
143 /// The payload is compressed but this build lacks the `compress` feature,
144 /// or decompression failed.
145 Compression,
146 /// A supplied name/title was empty or too long.
147 BadName,
148}
149
150/// Convenience alias.
151pub type Result<T> = core::result::Result<T, Error>;
152
153// --------------------------------------------------------------------------
154// Block transport abstraction.
155// --------------------------------------------------------------------------
156
157/// A card as a flat array of [`FRAME_COUNT`] 128-byte frames.
158///
159/// This is the seam between the hardware and the filesystem: implement it once
160/// per backing (real card, RAM image, a file on a host) and the whole [`Card`]
161/// filesystem works unchanged.
162pub trait Block {
163 /// Read frame `frame` (0..[`FRAME_COUNT`]) into `out`.
164 fn read_frame(&mut self, frame: u16, out: &mut [u8; FRAME_SIZE]) -> Result<()>;
165 /// Write `data` to frame `frame` (0..[`FRAME_COUNT`]).
166 fn write_frame(&mut self, frame: u16, data: &[u8; FRAME_SIZE]) -> Result<()>;
167}
168
169// --------------------------------------------------------------------------
170// Directory listing entry.
171// --------------------------------------------------------------------------
172
173/// One file as reported by [`Card::list`].
174///
175/// The fields stay public because games build `[Entry; N]` listing buffers
176/// with a struct literal (voxide's save menu, the hello-memcard example).
177/// Nothing relies on them for memory safety: [`Entry::name`] checks both the
178/// length and the UTF-8.
179#[derive(Copy, Clone, Debug, PartialEq, Eq)]
180pub struct Entry {
181 /// File name bytes (ASCII), `name_len` valid.
182 pub name: [u8; MAX_NAME_LEN + 1],
183 /// Valid length of `name`.
184 pub name_len: u8,
185 /// Number of 8 KiB blocks the file occupies.
186 pub blocks: u8,
187}
188
189impl Entry {
190 /// The file name as a `&str`.
191 ///
192 /// Names this crate writes are ASCII, but a listing reads whatever bytes
193 /// the card holds, so a corrupt or foreign card can carry anything. Such
194 /// a name, or a `name_len` past the buffer, reads as `"?"`.
195 pub fn name(&self) -> &str {
196 self.name
197 .get(..self.name_len as usize)
198 .and_then(|bytes| core::str::from_utf8(bytes).ok())
199 .unwrap_or("?")
200 }
201}
202
203// --------------------------------------------------------------------------
204// The filesystem handle. Method bodies live in `fs.rs`.
205// --------------------------------------------------------------------------
206
207/// A memory card with the standard PS1 filesystem on top of a [`Block`] device.
208pub struct Card<B: Block> {
209 pub(crate) dev: B,
210}
211
212impl<B: Block> Card<B> {
213 /// Wrap a block device. Does no I/O; call [`Card::is_formatted`] /
214 /// [`Card::format`] as needed.
215 pub fn new(dev: B) -> Self {
216 Card { dev }
217 }
218
219 /// Recover the underlying block device.
220 pub fn into_inner(self) -> B {
221 self.dev
222 }
223
224 /// Borrow the underlying block device.
225 pub fn device(&mut self) -> &mut B {
226 &mut self.dev
227 }
228}
229
230/// The save-container header written ahead of every payload so reads are
231/// self-describing. 16 bytes, little-endian.
232///
233/// ```text
234/// 0 "PMC1" magic
235/// 4 flags (u8) bit0 = payload is LZSS-compressed
236/// 5 reserved[3]
237/// 8 raw_len (u32) original payload length
238/// 12 stored_len(u32) bytes stored after this header
239/// ```
240pub(crate) const CONTAINER_MAGIC: [u8; 4] = *b"PMC1";
241pub(crate) const CONTAINER_LEN: usize = 16;
242pub(crate) const FLAG_COMPRESSED: u8 = 1 << 0;