Skip to main content

shuyi_playdate/
storage.rs

1//! The game's [`Storage`] on the Playdate's files: `<key>.ron` per key in the
2//! game's data folder (`Disk/Data/<bundle id>/` in the Simulator), kept as
3//! `shuyi-app`'s `FileStorage` keeps them (ticket 3045).
4//!
5//! A write puts the new value in `<key>.ron.tmp` and flushes it, copies the
6//! old `<key>.ron` (when it is a good copy: not empty, not all zero bytes,
7//! UTF-8) to `<key>.ron.bak` and flushes that, then renames the `.tmp` onto the
8//! `.ron`. A read of a `.ron` that is missing, empty, all zero bytes or not
9//! UTF-8 beside a good `.bak` reads the `.bak`. Where the SDK won't move a file
10//! onto another, the old `.ron` goes first: its `.bak` holds it meanwhile.
11
12use alloc::borrow::ToOwned;
13use alloc::collections::BTreeSet;
14use alloc::format;
15use alloc::string::String;
16use alloc::vec::Vec;
17
18use shuyi_ui::storage::{Storage, StorageError, conformance_suite, is_valid_key};
19
20use crate::sdk::{Sdk, backend};
21
22/// A [`Storage`] of one file per key in the game's data folder.
23#[derive(Debug)]
24pub struct SdkStorage<S> {
25    sdk: S,
26    /// The folder of the data folder its files are in: `""` for the data
27    /// folder itself, else ending in `/`.
28    folder: String,
29    /// Whether a write has removed the `.tmp` files a crash left.
30    swept: bool,
31}
32
33impl<S: Sdk> SdkStorage<S> {
34    /// The game's storage on `sdk`'s files.
35    pub fn new(sdk: S) -> Self {
36        Self {
37            sdk,
38            folder: String::new(),
39            swept: false,
40        }
41    }
42
43    /// The storage in `folder` (a name without `/`) of the game's data
44    /// folder, which a write makes.
45    pub fn in_folder(sdk: S, folder: &str) -> Self {
46        Self {
47            sdk,
48            folder: format!("{folder}/"),
49            swept: false,
50        }
51    }
52
53    fn path(&self, key: &str) -> String {
54        format!("{}{key}.ron", self.folder)
55    }
56
57    fn tmp_path(&self, key: &str) -> String {
58        format!("{}{key}.ron.tmp", self.folder)
59    }
60
61    fn bak_path(&self, key: &str) -> String {
62        format!("{}{key}.ron.bak", self.folder)
63    }
64
65    /// The names in its folder.
66    fn names(&self) -> Result<Vec<String>, StorageError> {
67        let folder = self.folder.trim_end_matches('/');
68        self.sdk.data_names(folder).map_err(|e| backend(folder, &e))
69    }
70
71    /// The backup's text, if it is a good copy.
72    fn good_backup(&self, key: &str) -> Option<String> {
73        let bak = self.bak_path(key);
74        if !self.sdk.is_file(&bak) {
75            return None;
76        }
77        let bytes = self.sdk.read_data(&bak).ok()?;
78        if is_good(&bytes) {
79            String::from_utf8(bytes).ok()
80        } else {
81            None
82        }
83    }
84
85    /// Removes the `.tmp` files a crash left.
86    fn sweep(&self) -> Result<(), StorageError> {
87        for name in self.names()?.iter().filter(|n| n.ends_with(".ron.tmp")) {
88            self.remove_if_there(&format!("{}{name}", self.folder))?;
89        }
90        Ok(())
91    }
92
93    fn remove_if_there(&self, path: &str) -> Result<(), StorageError> {
94        if self.sdk.is_file(path) {
95            self.sdk.remove_data(path).map_err(|e| backend(path, &e))?;
96        }
97        Ok(())
98    }
99
100    /// Moves the `.tmp` onto the `.ron`, the old one going first where the
101    /// SDK won't move a file onto another.
102    fn replace(&self, tmp: &str, dest: &str) -> Result<(), StorageError> {
103        if self.sdk.rename_data(tmp, dest).is_ok() {
104            return Ok(());
105        }
106        self.remove_if_there(dest)?;
107        self.sdk
108            .rename_data(tmp, dest)
109            .map_err(|e| backend(dest, &e))
110    }
111}
112
113/// Whether a file's bytes are a good copy of a value: not empty, not all
114/// zero bytes (what a power cut can leave), and UTF-8.
115fn is_good(bytes: &[u8]) -> bool {
116    !bytes.is_empty() && bytes.iter().any(|&b| b != 0) && core::str::from_utf8(bytes).is_ok()
117}
118
119fn check_key(key: &str) -> Result<(), StorageError> {
120    if is_valid_key(key) {
121        Ok(())
122    } else {
123        Err(StorageError::InvalidKey(key.to_owned()))
124    }
125}
126
127impl<S: Sdk> Storage for SdkStorage<S> {
128    fn read(&self, key: &str) -> Result<Option<String>, StorageError> {
129        check_key(key)?;
130        let path = self.path(key);
131        let main = if self.sdk.is_file(&path) {
132            Some(self.sdk.read_data(&path).map_err(|e| backend(&path, &e))?)
133        } else {
134            None
135        };
136        if !main.as_deref().is_some_and(is_good)
137            && let Some(backup) = self.good_backup(key)
138        {
139            return Ok(Some(backup));
140        }
141        main.map(String::from_utf8)
142            .transpose()
143            .map_err(|e| backend(&path, &format!("{e}")))
144    }
145
146    fn write(&mut self, key: &str, value: &str) -> Result<(), StorageError> {
147        check_key(key)?;
148        if !self.swept {
149            let folder = self.folder.trim_end_matches('/');
150            if !folder.is_empty() && self.sdk.data_names(folder).is_err() {
151                self.sdk
152                    .make_data_folder(folder)
153                    .map_err(|e| backend(folder, &e))?;
154            }
155            self.sweep()?;
156            self.swept = true;
157        }
158        let tmp = self.tmp_path(key);
159        self.sdk
160            .write_data(&tmp, value.as_bytes())
161            .map_err(|e| backend(&tmp, &e))?;
162        let dest = self.path(key);
163        let bak = self.bak_path(key);
164        if is_good(value.as_bytes()) {
165            let old = if self.sdk.is_file(&dest) {
166                Some(self.sdk.read_data(&dest).map_err(|e| backend(&dest, &e))?)
167            } else {
168                None
169            };
170            if let Some(old) = old.filter(|old| is_good(old)) {
171                self.sdk
172                    .write_data(&bak, &old)
173                    .map_err(|e| backend(&bak, &e))?;
174            }
175        } else {
176            self.remove_if_there(&bak)?;
177        }
178        self.replace(&tmp, &dest)
179    }
180
181    fn delete(&mut self, key: &str) -> Result<(), StorageError> {
182        check_key(key)?;
183        self.remove_if_there(&self.bak_path(key))?;
184        self.remove_if_there(&self.path(key))
185    }
186
187    fn list(&self) -> Result<Vec<String>, StorageError> {
188        let names = if self.folder.is_empty() {
189            self.names()?
190        } else {
191            // A folder not made yet holds no keys.
192            self.names().unwrap_or_default()
193        };
194        let mut keys = BTreeSet::new();
195        for name in &names {
196            if let Some(key) = name.strip_suffix(".ron") {
197                if is_valid_key(key) {
198                    keys.insert(key.to_owned());
199                }
200            } else if let Some(key) = name.strip_suffix(".ron.bak")
201                && is_valid_key(key)
202                && !names.contains(&format!("{key}.ron"))
203                && self.good_backup(key).is_some()
204            {
205                keys.insert(key.to_owned());
206            }
207        }
208        Ok(keys.into_iter().collect())
209    }
210}
211
212/// The folder of the data folder [`check`] runs in, away from the game's
213/// own keys.
214pub const CHECK_FOLDER: &str = "storage-check";
215
216/// Runs the engine's storage conformance suite on `sdk`'s files, in
217/// [`CHECK_FOLDER`], and leaves the folder empty; panics if it fails. A debug
218/// build on the device runs it at start, on the real files.
219pub fn check<S: Sdk>(sdk: S) {
220    let mut storage = SdkStorage::in_folder(sdk, CHECK_FOLDER);
221    let clear = |storage: &mut SdkStorage<S>| {
222        let keys = storage.list().unwrap_or_else(|e| panic!("{e}"));
223        for key in keys {
224            storage.delete(&key).unwrap_or_else(|e| panic!("{e}"));
225        }
226    };
227    clear(&mut storage);
228    conformance_suite(&mut storage);
229    clear(&mut storage);
230}
231
232#[cfg(test)]
233mod tests {
234    use alloc::string::ToString;
235
236    use shuyi_ui::storage::{PowerCutRig, power_cut_suite};
237
238    use super::*;
239    use crate::fake::{FakeSdk, Rename};
240
241    /// A storage over the fake SDK's files, which can lose the power.
242    #[derive(Debug)]
243    struct Rig {
244        sdk: FakeSdk,
245        storage: SdkStorage<FakeSdk>,
246    }
247
248    impl Rig {
249        fn new(rename: Rename) -> Self {
250            let sdk = FakeSdk::default();
251            sdk.set_rename(rename);
252            Self {
253                storage: SdkStorage::new(sdk.clone()),
254                sdk,
255            }
256        }
257    }
258
259    impl PowerCutRig for Rig {
260        fn storage(&mut self) -> &mut dyn Storage {
261            &mut self.storage
262        }
263
264        fn cut_after(&mut self, ops: u32) {
265            self.sdk.cut_power_after(ops);
266        }
267
268        fn restart(&mut self) {
269            self.sdk.restore_power();
270            self.storage = SdkStorage::new(self.sdk.clone());
271        }
272    }
273
274    #[test]
275    fn it_passes_the_conformance_suite() {
276        conformance_suite(&mut SdkStorage::new(FakeSdk::default()));
277    }
278
279    #[test]
280    fn it_passes_where_a_file_cannot_be_moved_onto_another() {
281        let sdk = FakeSdk::default();
282        sdk.set_rename(Rename::Refuses);
283        conformance_suite(&mut SdkStorage::new(sdk));
284    }
285
286    #[test]
287    fn it_passes_in_a_folder_and_the_check_leaves_nothing() {
288        conformance_suite(&mut SdkStorage::in_folder(FakeSdk::default(), "saves"));
289        let sdk = FakeSdk::default();
290        sdk.put_file("settings.ron", b"(a)");
291        check(sdk.clone());
292        check(sdk.clone());
293        assert_eq!(
294            sdk.data_names("").unwrap_or_default(),
295            ["settings.ron".to_string(), "storage-check/".to_string()]
296        );
297        assert_eq!(
298            sdk.data_names(CHECK_FOLDER).unwrap_or_default(),
299            [] as [String; 0]
300        );
301    }
302
303    #[test]
304    fn it_passes_the_power_cut_suite() {
305        power_cut_suite(&mut Rig::new(Rename::Replaces));
306    }
307
308    #[test]
309    fn it_passes_the_power_cut_suite_where_a_file_cannot_be_moved_onto_another() {
310        power_cut_suite(&mut Rig::new(Rename::Refuses));
311    }
312
313    #[test]
314    fn a_write_goes_tmp_then_bak_then_rename_each_flushed() {
315        let sdk = FakeSdk::default();
316        let mut storage = SdkStorage::new(sdk.clone());
317        storage
318            .write("slot", "(a)")
319            .unwrap_or_else(|e| panic!("{e}"));
320        sdk.take_file_calls();
321        storage
322            .write("slot", "(b)")
323            .unwrap_or_else(|e| panic!("{e}"));
324        assert_eq!(
325            sdk.take_file_calls(),
326            [
327                "write slot.ron.tmp",
328                "write slot.ron.bak",
329                "rename slot.ron.tmp slot.ron",
330            ]
331        );
332        assert_eq!(sdk.file("slot.ron.bak").as_deref(), Some(&b"(a)"[..]));
333        assert_eq!(
334            storage.read("slot").unwrap_or_default().as_deref(),
335            Some("(b)")
336        );
337    }
338
339    #[test]
340    fn damage_reads_the_backup() {
341        let sdk = FakeSdk::default();
342        let mut storage = SdkStorage::new(sdk.clone());
343        storage
344            .write("slot", "(a)")
345            .unwrap_or_else(|e| panic!("{e}"));
346        storage
347            .write("slot", "(b)")
348            .unwrap_or_else(|e| panic!("{e}"));
349        sdk.put_file("slot.ron", &[0, 0, 0]);
350        assert_eq!(
351            storage.read("slot").unwrap_or_default().as_deref(),
352            Some("(a)")
353        );
354        sdk.put_file("slot.ron", &[0xff]);
355        sdk.put_file("slot.ron.bak", &[0xff]);
356        assert!(matches!(
357            storage.read("slot"),
358            Err(StorageError::Backend(_))
359        ));
360    }
361
362    #[test]
363    fn only_keys_are_listed() {
364        let sdk = FakeSdk::default();
365        let mut storage = SdkStorage::new(sdk.clone());
366        storage
367            .write("settings", "()")
368            .unwrap_or_else(|e| panic!("{e}"));
369        sdk.put_file("load-report.txt", b"");
370        sdk.put_file("Bad Name.ron", b"");
371        sdk.put_file("slot.ron.tmp", b"");
372        let keys = storage.list().unwrap_or_else(|e| panic!("{e}"));
373        assert_eq!(keys, ["settings".to_string()]);
374    }
375
376    #[test]
377    fn the_first_write_removes_what_a_crash_left() {
378        let sdk = FakeSdk::default();
379        sdk.put_file("slot.ron.tmp", b"half");
380        let mut storage = SdkStorage::new(sdk.clone());
381        storage
382            .write("other", "(a)")
383            .unwrap_or_else(|e| panic!("{e}"));
384        assert_eq!(sdk.file("slot.ron.tmp"), None);
385    }
386
387    #[test]
388    fn a_data_folder_that_cant_be_listed_is_an_error_and_a_folder_not_made_holds_nothing() {
389        let sdk = FakeSdk::default();
390        assert_eq!(
391            SdkStorage::in_folder(sdk.clone(), "saves").list().ok(),
392            Some(Vec::new())
393        );
394        sdk.cut_power_after(0);
395        assert!(matches!(
396            SdkStorage::new(sdk).list(),
397            Err(StorageError::Backend(_))
398        ));
399    }
400
401    #[test]
402    fn a_bad_key_is_an_error() {
403        let mut storage = SdkStorage::new(FakeSdk::default());
404        assert!(matches!(
405            storage.read("a/b"),
406            Err(StorageError::InvalidKey(_))
407        ));
408        assert!(matches!(
409            storage.write("a/b", "x"),
410            Err(StorageError::InvalidKey(_))
411        ));
412        assert!(matches!(
413            storage.delete("a/b"),
414            Err(StorageError::InvalidKey(_))
415        ));
416    }
417}