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