Skip to main content

rustysnes_savestate/
lib.rs

1//! `rustysnes-savestate` — the shared save-state wire-format primitives (savestate).
2//!
3//! A leaf crate in the one-directional chip-crate graph (see `docs/architecture.md`): every
4//! chip crate (`rustysnes-cpu`/`-ppu`/`-apu`/`-cart`) and `rustysnes-core` depend on this for a
5//! single, versioned binary format, per `docs/adr/0006-save-state-format.md`. No dependents
6//! among the chip crates — this only ever gets DEPENDED ON, keeping the graph acyclic.
7//!
8//! Deliberately no `serde`/reflection: every `Board`/`Cpu`/`Ppu`/`Apu` implementation writes an
9//! explicit `save_state`/`load_state` pair using the primitives here (ADR 0006's "no derive
10//! magic" decision — keeps `#![no_std]` targets byte-identical to native and keeps each
11//! component's on-disk format change local + auditable, matching this project's existing style
12//! for the `Board` trait itself).
13//!
14//! # Format
15//!
16//! This crate defines the **section framing** a save-state's payload is built from — a full
17//! save-state additionally has a top-level header (magic bytes + format version + crate-version
18//! string, ADR 0006) that `rustysnes-core::System::save_state`/`load_state` writes/checks before
19//! ever touching a section; that envelope is out of scope here (see [`SaveStateError::BadMagic`]/
20//! [`SaveStateError::UnsupportedVersion`], which exist for that caller to use).
21//!
22//! A section is **tagged and length-prefixed** — a 4-byte ASCII tag, a `u32` little-endian byte
23//! length, then that many bytes of section-defined content. Wrapping a component's state as one
24//! such section (via [`SaveWriter::section`] / [`SaveReader::section`]) is what lets a newer
25//! format skip a section it doesn't recognize (a bumped-version load) rather than corrupting the
26//! rest of the load, and lets a struct that changed its own internal layout stay self-describing
27//! without a central schema registry.
28
29#![no_std]
30#![forbid(unsafe_code)]
31extern crate alloc;
32
33use alloc::string::String;
34use alloc::vec::Vec;
35
36use thiserror::Error;
37
38/// Errors from decoding a save-state.
39///
40/// `docs/adr/0006`'s "reject loudly on an unrecognized/corrupt format, never silently truncate
41/// or zero-fill" honesty posture — the same posture `docs/adr/0003` already applies to
42/// coprocessor accuracy.
43#[derive(Debug, Error, PartialEq, Eq)]
44#[non_exhaustive]
45pub enum SaveStateError {
46    /// The buffer ended before the expected number of bytes were available.
47    #[error("truncated save-state data (expected {expected} more bytes, {available} available)")]
48    Truncated {
49        /// Bytes the read attempted to consume.
50        expected: usize,
51        /// Bytes actually remaining in the buffer.
52        available: usize,
53    },
54    /// A section's declared tag didn't match what the caller expected at this point in the
55    /// format — either genuine corruption, or a format version whose section order changed.
56    #[error("unexpected section tag: expected {expected:?}, found {found:?}")]
57    UnexpectedTag {
58        /// The 4-byte ASCII tag the caller expected.
59        expected: [u8; 4],
60        /// The 4-byte ASCII tag actually present.
61        found: [u8; 4],
62    },
63    /// The blob's leading magic bytes didn't match — not a RustySNES save-state at all.
64    #[error("not a RustySNES save-state (bad magic)")]
65    BadMagic,
66    /// The blob's format-version major number is newer than this build understands.
67    #[error("save-state format version {found} is newer than this build supports (max {max})")]
68    UnsupportedVersion {
69        /// The format major version found in the blob.
70        found: u16,
71        /// The highest format major version this build can load.
72        max: u16,
73    },
74    /// A component rejected the decoded bytes as semantically invalid (e.g. an enum discriminant
75    /// with no matching variant) even though the section framing itself was well-formed.
76    #[error("invalid save-state content: {0}")]
77    Invalid(String),
78}
79
80/// A growable little-endian binary writer over an in-memory buffer (`#![no_std]` + `alloc`, no
81/// `std::io::Write` — every chip crate this feeds is `no_std`).
82#[derive(Debug, Default)]
83pub struct SaveWriter {
84    buf: Vec<u8>,
85}
86
87impl SaveWriter {
88    /// A fresh, empty writer.
89    #[must_use]
90    pub const fn new() -> Self {
91        Self { buf: Vec::new() }
92    }
93
94    /// Consume the writer, returning the assembled bytes.
95    #[must_use]
96    pub fn into_bytes(self) -> Vec<u8> {
97        self.buf
98    }
99
100    /// The bytes written so far (for a nested writer being spliced into a parent — see
101    /// [`Self::section`]).
102    #[must_use]
103    pub fn as_bytes(&self) -> &[u8] {
104        &self.buf
105    }
106
107    /// Append raw bytes verbatim (for a component embedding another's already-serialized bytes,
108    /// e.g. a coprocessor's own sub-engine).
109    pub fn write_bytes(&mut self, bytes: &[u8]) {
110        self.buf.extend_from_slice(bytes);
111    }
112
113    /// Write one byte.
114    pub fn write_u8(&mut self, v: u8) {
115        self.buf.push(v);
116    }
117
118    /// Write a `bool` as one byte (`0`/`1`).
119    pub fn write_bool(&mut self, v: bool) {
120        self.write_u8(u8::from(v));
121    }
122
123    /// Write a little-endian `u16`.
124    pub fn write_u16(&mut self, v: u16) {
125        self.buf.extend_from_slice(&v.to_le_bytes());
126    }
127
128    /// Write a little-endian `u32`.
129    pub fn write_u32(&mut self, v: u32) {
130        self.buf.extend_from_slice(&v.to_le_bytes());
131    }
132
133    /// Write a little-endian `u64`.
134    pub fn write_u64(&mut self, v: u64) {
135        self.buf.extend_from_slice(&v.to_le_bytes());
136    }
137
138    /// Write a byte slice prefixed with its `u32` length (for a variable-length payload, e.g. an
139    /// SRAM image — distinct from [`Self::section`], which additionally carries a 4-byte tag for
140    /// forward-compatible skipping; a length-prefixed blob has no tag because its meaning is
141    /// implied by its fixed position in the caller's own section).
142    pub fn write_len_prefixed(&mut self, bytes: &[u8]) {
143        #[allow(clippy::cast_possible_truncation)] // save-state payloads never approach 4 GiB
144        self.write_u32(bytes.len() as u32);
145        self.write_bytes(bytes);
146    }
147
148    /// Write a nested, self-describing section: a 4-byte ASCII `tag`, a `u32` byte length, then
149    /// whatever `body` writes. `tag` should be a short, stable, human-legible identifier (e.g.
150    /// `*b"CPU0"`, `*b"BRD1"`) — stable across releases even if the section's internal layout
151    /// changes, since [`SaveReader::section`] uses it to skip a section it doesn't recognize.
152    ///
153    /// `body` writes directly into this writer's own buffer (no nested `Vec` allocation — a
154    /// save-state gets created and restored repeatedly during rewind/run-ahead, so allocating one
155    /// throwaway buffer per (possibly nested) section on every such call would add up); the length
156    /// header is a zero placeholder written up front and patched in place once `body` returns and
157    /// the section's true length is known.
158    pub fn section(&mut self, tag: [u8; 4], body: impl FnOnce(&mut Self)) {
159        self.write_bytes(&tag);
160        let len_pos = self.buf.len();
161        self.write_u32(0); // placeholder, patched below
162        let start = self.buf.len();
163        body(self);
164        let len = self.buf.len() - start;
165        #[allow(clippy::cast_possible_truncation)] // save-state sections never approach 4 GiB
166        self.buf[len_pos..len_pos + 4].copy_from_slice(&(len as u32).to_le_bytes());
167    }
168}
169
170/// A little-endian binary cursor reader over a borrowed byte slice.
171///
172/// Bounds-checked reads return [`SaveStateError::Truncated`] instead of panicking on
173/// malformed/corrupt input (external, untrusted data — a save-state file the user can hand-edit
174/// or a corrupted disk write) — "validate at boundaries" / "never `unwrap` on untrusted data"
175/// (project-wide house rules, `master-core/modules/60-security.md`).
176#[derive(Debug, Clone, Copy)]
177pub struct SaveReader<'a> {
178    buf: &'a [u8],
179    pos: usize,
180}
181
182impl<'a> SaveReader<'a> {
183    /// A reader positioned at the start of `buf`.
184    #[must_use]
185    pub const fn new(buf: &'a [u8]) -> Self {
186        Self { buf, pos: 0 }
187    }
188
189    /// Bytes remaining unread.
190    #[must_use]
191    pub const fn remaining(&self) -> usize {
192        self.buf.len() - self.pos
193    }
194
195    fn take(&mut self, n: usize) -> Result<&'a [u8], SaveStateError> {
196        if self.remaining() < n {
197            return Err(SaveStateError::Truncated {
198                expected: n,
199                available: self.remaining(),
200            });
201        }
202        let out = &self.buf[self.pos..self.pos + n];
203        self.pos += n;
204        Ok(out)
205    }
206
207    /// Read raw bytes verbatim (the counterpart to [`SaveWriter::write_bytes`] for a component
208    /// that embeds another's already-serialized bytes at a caller-known fixed length).
209    ///
210    /// # Errors
211    /// [`SaveStateError::Truncated`] if fewer than `n` bytes remain.
212    pub fn read_bytes(&mut self, n: usize) -> Result<&'a [u8], SaveStateError> {
213        self.take(n)
214    }
215
216    /// Read one byte.
217    ///
218    /// # Errors
219    /// [`SaveStateError::Truncated`] if the buffer is exhausted.
220    pub fn read_u8(&mut self) -> Result<u8, SaveStateError> {
221        Ok(self.take(1)?[0])
222    }
223
224    /// Read a `bool` (any nonzero byte is `true`, matching how `write_bool` only ever emits 0/1
225    /// but a hand-corrupted file might not — never a hard error for either value).
226    ///
227    /// # Errors
228    /// [`SaveStateError::Truncated`] if the buffer is exhausted.
229    pub fn read_bool(&mut self) -> Result<bool, SaveStateError> {
230        Ok(self.read_u8()? != 0)
231    }
232
233    /// Read a little-endian `u16`.
234    ///
235    /// # Errors
236    /// [`SaveStateError::Truncated`] if fewer than 2 bytes remain.
237    pub fn read_u16(&mut self) -> Result<u16, SaveStateError> {
238        let b = self.take(2)?;
239        Ok(u16::from_le_bytes([b[0], b[1]]))
240    }
241
242    /// Read a little-endian `u32`.
243    ///
244    /// # Errors
245    /// [`SaveStateError::Truncated`] if fewer than 4 bytes remain.
246    pub fn read_u32(&mut self) -> Result<u32, SaveStateError> {
247        let b = self.take(4)?;
248        Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
249    }
250
251    /// Read a little-endian `u64`.
252    ///
253    /// # Errors
254    /// [`SaveStateError::Truncated`] if fewer than 8 bytes remain.
255    pub fn read_u64(&mut self) -> Result<u64, SaveStateError> {
256        let b = self.take(8)?;
257        Ok(u64::from_le_bytes([
258            b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7],
259        ]))
260    }
261
262    /// Read a `u32`-length-prefixed byte slice (the counterpart to
263    /// [`SaveWriter::write_len_prefixed`]).
264    ///
265    /// # Errors
266    /// [`SaveStateError::Truncated`] if the declared length exceeds what remains.
267    pub fn read_len_prefixed(&mut self) -> Result<&'a [u8], SaveStateError> {
268        let len = self.read_u32()? as usize;
269        self.take(len)
270    }
271
272    /// Read a nested section written by [`SaveWriter::section`]: consumes its 4-byte tag + `u32`
273    /// length, and returns `(tag, sub_reader)` scoped to exactly that section's bytes — reading
274    /// past the sub-reader's end fails with [`SaveStateError::Truncated`] even if the OUTER
275    /// buffer has more data, which is what makes an unrecognized/truncated-by-a-bug section fail
276    /// locally instead of desynchronizing every section after it.
277    ///
278    /// # Errors
279    /// [`SaveStateError::Truncated`] if the tag/length header or the declared body is truncated.
280    pub fn section(&mut self) -> Result<([u8; 4], Self), SaveStateError> {
281        let tag_bytes = self.take(4)?;
282        let tag = [tag_bytes[0], tag_bytes[1], tag_bytes[2], tag_bytes[3]];
283        let body = self.read_len_prefixed()?;
284        Ok((tag, Self::new(body)))
285    }
286
287    /// [`Self::section`], additionally checking the tag matches `expected` — the common case
288    /// where the caller knows exactly which section should come next (most of the format is a
289    /// fixed sequence; only the top-level `Board`-family dispatch genuinely branches on tag).
290    ///
291    /// # Errors
292    /// [`SaveStateError::Truncated`] per [`Self::section`], or [`SaveStateError::UnexpectedTag`]
293    /// if the next section's tag doesn't match `expected`.
294    pub fn expect_section(&mut self, expected: [u8; 4]) -> Result<Self, SaveStateError> {
295        let (found, reader) = self.section()?;
296        if found == expected {
297            Ok(reader)
298        } else {
299            Err(SaveStateError::UnexpectedTag { expected, found })
300        }
301    }
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307
308    #[test]
309    fn primitives_round_trip() {
310        let mut w = SaveWriter::new();
311        w.write_u8(0xAB);
312        w.write_bool(true);
313        w.write_u16(0x1234);
314        w.write_u32(0xDEAD_BEEF);
315        w.write_u64(0x1122_3344_5566_7788);
316        w.write_len_prefixed(&[1, 2, 3]);
317        let bytes = w.into_bytes();
318
319        let mut r = SaveReader::new(&bytes);
320        assert_eq!(r.read_u8().unwrap(), 0xAB);
321        assert!(r.read_bool().unwrap());
322        assert_eq!(r.read_u16().unwrap(), 0x1234);
323        assert_eq!(r.read_u32().unwrap(), 0xDEAD_BEEF);
324        assert_eq!(r.read_u64().unwrap(), 0x1122_3344_5566_7788);
325        assert_eq!(r.read_len_prefixed().unwrap(), &[1, 2, 3]);
326        assert_eq!(r.remaining(), 0);
327    }
328
329    #[test]
330    fn truncated_read_errors_instead_of_panicking() {
331        let mut r = SaveReader::new(&[0x01, 0x02]);
332        assert_eq!(
333            r.read_u32(),
334            Err(SaveStateError::Truncated {
335                expected: 4,
336                available: 2
337            })
338        );
339    }
340
341    #[test]
342    fn nested_sections_round_trip_and_stay_scoped() {
343        let mut w = SaveWriter::new();
344        w.section(*b"AAAA", |s| s.write_u32(1));
345        w.section(*b"BBBB", |s| s.write_u32(2));
346        let bytes = w.into_bytes();
347
348        let mut r = SaveReader::new(&bytes);
349        let mut a = r.expect_section(*b"AAAA").unwrap();
350        assert_eq!(a.read_u32().unwrap(), 1);
351        assert_eq!(a.remaining(), 0);
352        // Reading past a sub-section's own bound fails locally...
353        assert!(a.read_u8().is_err());
354        // ...without desynchronizing the outer reader's position for the next section.
355        let mut b = r.expect_section(*b"BBBB").unwrap();
356        assert_eq!(b.read_u32().unwrap(), 2);
357    }
358
359    #[test]
360    fn unexpected_tag_is_reported_not_silently_accepted() {
361        let mut w = SaveWriter::new();
362        w.section(*b"AAAA", |s| s.write_u8(1));
363        let bytes = w.into_bytes();
364        let mut r = SaveReader::new(&bytes);
365        assert_eq!(
366            r.expect_section(*b"ZZZZ").unwrap_err(),
367            SaveStateError::UnexpectedTag {
368                expected: *b"ZZZZ",
369                found: *b"AAAA"
370            }
371        );
372    }
373}