1use std::collections::BTreeMap;
27
28use crate::platform::Store;
29
30use sealed::Kept;
31
32const HEADER: &str = "mirage-engine saves 1";
34
35pub trait SaveValue: Kept {}
41
42pub trait Saves {
48 fn name(&self) -> &'static str;
51}
52
53pub trait SaveKey: Saves {
61 type Value: SaveValue;
63
64 fn fallback(&self) -> Self::Value;
67}
68
69pub(crate) struct Saved {
72 entries: BTreeMap<String, String>,
74 dirty: bool,
75 store: Store,
76}
77
78impl Saved {
79 pub(crate) fn new(store: Store) -> Self {
82 Self {
83 entries: entries(store.read().as_deref()),
84 dirty: false,
85 store,
86 }
87 }
88
89 pub(crate) fn read<K: SaveKey>(&self, key: K) -> K::Value {
93 let Some(kept) = self.entries.get(key.name()) else {
94 return key.fallback();
95 };
96 K::Value::read(kept).unwrap_or_else(|| {
97 log::debug!(
98 "what the store kept for `{}` does not read as its value: {kept}",
99 key.name()
100 );
101 key.fallback()
102 })
103 }
104
105 pub(crate) fn write<K: SaveKey>(&mut self, key: K, value: K::Value) {
108 let written = value.written();
109 let kept = self.entries.get(key.name());
110 if kept.is_some_and(|kept| *kept == written) {
111 return;
112 }
113 self.entries.insert(key.name().to_owned(), written);
114 self.dirty = true;
115 }
116
117 pub(crate) fn flush(&mut self) {
120 if core::mem::take(&mut self.dirty) {
121 self.store.write(&self.written());
122 }
123 }
124
125 fn written(&self) -> String {
128 let mut out = String::from(HEADER);
129 out.push('\n');
130 for (name, value) in &self.entries {
131 escaped(name, &mut out);
132 out.push(' ');
133 out.push_str(value);
134 out.push('\n');
135 }
136 out
137 }
138}
139
140fn entries(text: Option<&str>) -> BTreeMap<String, String> {
143 let Some(text) = text else {
144 return BTreeMap::new();
145 };
146
147 let mut lines = text.lines();
148 if lines.next().map(str::trim) != Some(HEADER) {
149 log::debug!("what the store kept is not this version's; every key reads as its fallback");
150 return BTreeMap::new();
151 }
152
153 lines
154 .filter(|line| !line.is_empty())
155 .filter_map(|line| {
156 let read = line
157 .split_once(' ')
158 .and_then(|(name, value)| Some((unescaped(name)?, value.to_owned())));
159 if read.is_none() {
160 log::debug!("a kept entry was dropped: {line}");
161 }
162 read
163 })
164 .collect()
165}
166
167fn escaped(text: &str, out: &mut String) {
170 for letter in text.chars() {
171 match letter {
172 '\\' => out.push_str("\\\\"),
173 '\n' => out.push_str("\\n"),
174 '\r' => out.push_str("\\r"),
175 ' ' => out.push_str("\\s"),
176 _ => out.push(letter),
177 }
178 }
179}
180
181fn unescaped(text: &str) -> Option<String> {
184 let mut out = String::with_capacity(text.len());
185 let mut letters = text.chars();
186 while let Some(letter) = letters.next() {
187 match letter {
188 '\\' => out.push(match letters.next()? {
189 '\\' => '\\',
190 'n' => '\n',
191 'r' => '\r',
192 's' => ' ',
193 _ => return None,
194 }),
195 _ => out.push(letter),
196 }
197 }
198 Some(out)
199}
200
201macro_rules! parsed {
204 ($($value:ty),*) => {$(
205 impl Kept for $value {
206 fn written(&self) -> String {
207 self.to_string()
208 }
209
210 fn read(text: &str) -> Option<Self> {
211 text.parse().ok()
212 }
213 }
214
215 impl SaveValue for $value {}
216 )*};
217}
218
219parsed!(bool, i64, f64);
220
221impl Kept for String {
222 fn written(&self) -> String {
223 let mut out = String::with_capacity(self.len());
224 escaped(self, &mut out);
225 out
226 }
227
228 fn read(text: &str) -> Option<Self> {
229 unescaped(text)
230 }
231}
232
233impl SaveValue for String {}
234
235pub(crate) mod sealed {
238 pub trait Kept: Sized {
241 fn written(&self) -> String;
242 fn read(text: &str) -> Option<Self>;
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249
250 macro_rules! vocabulary {
252 ($name:ident, $value:ty, $fallback:expr) => {
253 #[derive(Clone, Copy)]
254 struct $name;
255
256 impl Saves for $name {
257 fn name(&self) -> &'static str {
258 stringify!($name)
259 }
260 }
261
262 impl SaveKey for $name {
263 type Value = $value;
264
265 fn fallback(&self) -> $value {
266 $fallback
267 }
268 }
269 };
270 }
271
272 vocabulary!(Score, i64, 0);
273 vocabulary!(Lap, f64, 0.0);
274 vocabulary!(Seen, bool, false);
275 vocabulary!(Player, String, "nobody".to_owned());
276
277 fn awkward() -> String {
279 " two\nlines \\ and a tab\t ".to_owned()
280 }
281
282 fn saves(kept: Option<&str>) -> Saved {
283 Saved {
284 entries: entries(kept),
285 dirty: false,
286 store: Store::saves(None),
287 }
288 }
289
290 #[test]
291 fn every_kind_of_value_reads_back_as_it_was_saved() {
292 let mut written = saves(None);
293 written.write(Score, 120);
294 written.write(Lap, -0.5);
295 written.write(Seen, true);
296 written.write(Player, awkward());
297
298 let read = saves(Some(&written.written()));
299
300 assert_eq!(read.read(Score), 120);
301 assert_eq!(read.read(Lap), -0.5);
302 assert!(read.read(Seen));
303 assert_eq!(read.read(Player), awkward());
304 assert_eq!(
305 read.written(),
306 written.written(),
307 "and writes the same store again"
308 );
309 }
310
311 #[test]
312 fn a_key_no_run_kept_reads_as_its_fallback_and_what_was_kept_reads_over_it() {
313 let mut written = saves(None);
314 written.write(Score, 7);
315
316 let read = saves(Some(&written.written()));
317
318 assert_eq!(read.read(Score), 7);
319 assert_eq!(read.read(Lap), 0.0, "which the store never kept");
320 assert_eq!(read.read(Player), "nobody");
321 }
322
323 #[test]
324 fn an_entry_no_key_of_this_run_names_is_carried_through_a_rewrite() {
325 let kept = format!("{HEADER}\nScore 3\nFurthest\\sLevel 9\n");
326
327 let mut read = saves(Some(&kept));
328 read.write(Score, 4);
329
330 assert_eq!(read.read(Score), 4);
331 assert_eq!(
332 read.written(),
333 format!("{HEADER}\nFurthest\\sLevel 9\nScore 4\n"),
334 "the entry this run knows nothing of is written back as it was"
335 );
336 }
337
338 #[test]
339 fn a_store_that_reads_as_nothing_leaves_every_fallback_standing() {
340 for broken in [
341 "",
342 "nonsense",
343 "mirage-engine saves 2\nScore 5\n",
344 &format!("{HEADER}\nScore\n"),
345 &format!("{HEADER}\nScore twelve\n"),
346 &format!("{HEADER}\nScore\\q 5\n"),
347 ] {
348 let read = saves(Some(broken));
349 assert_eq!(read.read(Score), 0, "`{broken}` left the fallback standing");
350 }
351 }
352
353 #[test]
354 fn saving_a_value_the_store_already_says_leaves_it_with_nothing_to_write() {
355 let mut written = saves(None);
356 written.write(Score, 42);
357 assert!(written.dirty);
358
359 let mut read = saves(Some(&written.written()));
360 read.write(Score, 42);
361
362 assert!(!read.dirty, "the same value again is not a change");
363 read.write(Score, 43);
364 assert!(read.dirty);
365 }
366
367 #[test]
368 fn a_store_mangled_any_which_way_still_reads_as_one_this_run_can_use() {
369 let mut written = saves(None);
370 written.write(Score, 120);
371 written.write(Player, awkward());
372 let kept = written.written();
373
374 for mangled in crate::platform::manglings(&kept) {
375 let mut read = saves(Some(&mangled));
376 read.write(Score, 7);
377
378 assert_eq!(read.read(Score), 7, "over {mangled:?}");
379 assert_eq!(
380 saves(Some(&read.written())).written(),
381 read.written(),
382 "and what it writes reads back the same, over {mangled:?}"
383 );
384 }
385 }
386
387 #[test]
388 fn a_run_with_no_title_keeps_nothing_and_still_reads_what_it_saved() {
389 let mut saves = Saved::new(Store::saves(None));
390 saves.write(Score, 5);
391 saves.flush();
392
393 assert_eq!(saves.read(Score), 5);
394 assert_eq!(Saved::new(Store::saves(None)).read(Score), 0);
395 }
396}