lsdj/sram/
mod.rs

1//! LittleSoundDJ SRAM/`.sav` file handling
2//!
3//! This module contains code for manipulating [`SRam`], which is where LSDJ stores our
4//! songs (compressed and uncompressed). Usually people work with `.sav` files, which gameboy
5//! emulators use to store the SRAM tied to a ROM. You can also download/upload `.sav`
6//! files to flashcarts for playback on real hardware.
7
8use crate::{
9    fs::{self, Filesystem},
10    song::{self, SongMemory},
11};
12use std::{
13    fs::{File, create_dir_all},
14    io::{self, Read, Write},
15    path::Path,
16};
17use thiserror::Error;
18
19/// A full representation of LittleSoundDJ SRAM
20///
21/// Every LSDJ save file consists of the same amount of bytes, in which both the song you're
22/// currently working on is stored (uncompressed), as well as a filesystem containing at max
23/// 32 (compressed) songs.
24///
25/// The first time you boot LSDJ it formats the SRAM to the expected structure, setting some
26/// magic bytes for later verification as well. This crate allows you to do the same, but also
27/// to deserialize [`SRam`] from disk or an arbitrary reader.
28///
29/// ```no_run
30/// # use lsdj::sram::SRam;
31/// # use std::fs::File;
32/// // Construct valid SRAM with the default/empty song and an empty filesystem
33/// let sram = SRam::new();
34///
35/// // Load SRAM from a path on disk
36/// let sram = SRam::from_path("bangers.sav")?;
37///
38/// // Load SRAM from an arbitrary reader
39/// let sram = SRam::from_reader(File::open("bangers.sav")?)?;
40/// # Ok::<(), anyhow::Error>(())
41/// ```
42///
43/// In the same way, SRAM can be serialized back to the underlying byte structure:
44///
45/// ```no_run
46/// # use lsdj::sram::SRam;
47/// # use std::fs::File;
48/// # let sram = SRam::new();
49/// // Load SRAM from a path on disk
50/// sram.to_path("bangers.sav")?;
51///
52/// // Load SRAM from an arbitrary reader
53/// sram.to_writer(File::create("bangers.sav")?)?;
54/// # Ok::<(), std::io::Error>(())
55/// ```
56pub struct SRam {
57    /// The song that's currently being worked on in LSDJ
58    pub working_memory_song: SongMemory,
59
60    /// Compressed storage for songs not currently worked on
61    pub filesystem: Filesystem,
62}
63
64impl SRam {
65    /// Construct a new SRAM, with a default song and empty filesystem
66    ///
67    /// This function also sets some necessary verification bytes which LSDJ uses to check
68    /// for corrupted memory
69    pub fn new() -> Self {
70        Self {
71            working_memory_song: SongMemory::new(),
72            filesystem: Filesystem::new(),
73        }
74    }
75
76    /// Deserialize SRAM from an arbitrary I/O reader
77    pub fn from_reader<R>(mut reader: R) -> Result<Self, FromReaderError>
78    where
79        R: Read,
80    {
81        let working_memory_song = SongMemory::from_reader(&mut reader)?;
82        let filesystem = Filesystem::from_reader(&mut reader)?;
83
84        Ok(Self {
85            working_memory_song,
86            filesystem,
87        })
88    }
89
90    /// Deserialize SRAM from a path on disk (.sav)
91    pub fn from_path<P>(path: P) -> Result<Self, FromPathError>
92    where
93        P: AsRef<Path>,
94    {
95        let file = File::open(path)?;
96        let sram = Self::from_reader(file)?;
97
98        Ok(sram)
99    }
100
101    /// Serialize SRAM to an arbitrary I/O writer
102    pub fn to_writer<W>(&self, mut writer: W) -> Result<(), io::Error>
103    where
104        W: Write,
105    {
106        self.working_memory_song.to_writer(&mut writer)?;
107        self.filesystem.to_writer(writer)
108    }
109
110    /// Serialize SRAM to a path on disk (.sav)
111    pub fn to_path<P>(&self, path: P) -> Result<(), io::Error>
112    where
113        P: AsRef<Path>,
114    {
115        let path = path.as_ref();
116        create_dir_all(path.parent().unwrap())?;
117        self.to_writer(File::create(path)?)
118    }
119}
120
121impl Default for SRam {
122    fn default() -> Self {
123        Self::new()
124    }
125}
126
127/// Errors that might be returned from [`SRam::from_reader()`]
128#[derive(Debug, Error)]
129pub enum FromReaderError {
130    /// Deserializing the working memory song from I/O failed
131    #[error("Reading the working memory song failed")]
132    WorkingSong(#[from] song::FromReaderError),
133
134    /// Deserializing the file system from I/O failed
135    #[error("Reading the filesystem failed")]
136    Filesystem(#[from] fs::FromReaderError),
137}
138
139/// Errors that might be returned from [`SRam::from_path()`]
140#[derive(Debug, Error)]
141pub enum FromPathError {
142    /// Opening the file itself failed
143    #[error("Opening the file failed")]
144    FileOpen(#[from] io::Error),
145
146    /// Deserialization itself somehow failed
147    #[error("Reading the SRAM from file failed")]
148    Read(#[from] FromReaderError),
149}