Skip to main content
← PSoXide SDK guides and examples · SDK f2fec1c301f1 · All Cargo features enabled; check feature gates before use.

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;