1use std::collections::BTreeSet;
8use std::fmt;
9use std::fs;
10use std::io::{self, Write as _};
11use std::panic::{RefUnwindSafe, UnwindSafe};
12use std::path::{Path, PathBuf};
13
14use shuyi_ui::storage::{Storage, StorageError, is_valid_key};
15
16#[cfg(test)]
17mod fake_disk;
18
19#[derive(Debug)]
52pub struct FileStorage {
53 dir: PathBuf,
54 disk: Box<dyn Disk>,
55 warn: fn(&str),
57 swept: bool,
59}
60
61impl FileStorage {
62 pub fn new(name: &str) -> Result<Self, StorageError> {
66 let dir = data_dir(name).ok_or_else(|| {
67 StorageError::Backend("could not determine an OS data directory".to_owned())
68 })?;
69 Ok(Self::at(dir, Box::new(StdDisk), log_warning))
70 }
71
72 #[cfg(test)]
74 pub(crate) fn in_dir(dir: PathBuf) -> Self {
75 Self::at(dir, Box::new(StdDisk), log_warning)
76 }
77
78 fn at(dir: PathBuf, disk: Box<dyn Disk>, warn: fn(&str)) -> Self {
80 Self {
81 dir,
82 disk,
83 warn,
84 swept: false,
85 }
86 }
87
88 fn path(&self, key: &str) -> PathBuf {
89 self.dir.join(format!("{key}.ron"))
90 }
91
92 fn tmp_path(&self, key: &str) -> PathBuf {
93 self.dir.join(format!("{key}.ron.tmp"))
94 }
95
96 fn bak_path(&self, key: &str) -> PathBuf {
97 self.dir.join(format!("{key}.ron.bak"))
98 }
99
100 fn good_backup(&self, key: &str) -> Option<String> {
102 let bytes = self.disk.read(&self.bak_path(key)).ok()?;
103 if is_good(&bytes) {
104 String::from_utf8(bytes).ok()
105 } else {
106 None
107 }
108 }
109
110 fn sweep(&self) -> Result<(), StorageError> {
112 let names = self
113 .disk
114 .names(&self.dir)
115 .map_err(|e| backend_error(&self.dir, &e))?;
116 for name in names.iter().filter(|name| name.ends_with(".ron.tmp")) {
117 remove_if_there(&*self.disk, &self.dir.join(name))?;
118 }
119 Ok(())
120 }
121}
122
123fn log_warning(message: &str) {
125 macroquad::prelude::warn!("storage: {}", message);
127}
128
129pub fn data_dir(name: &str) -> Option<PathBuf> {
135 #[cfg(target_os = "windows")]
136 {
137 std::env::var_os("APPDATA").map(|dir| PathBuf::from(dir).join(name))
138 }
139 #[cfg(target_os = "macos")]
140 {
141 std::env::var_os("HOME").map(|home| {
142 PathBuf::from(home)
143 .join("Library/Application Support")
144 .join(name)
145 })
146 }
147 #[cfg(all(unix, not(target_os = "macos")))]
148 {
149 if let Some(xdg_data_home) = std::env::var_os("XDG_DATA_HOME") {
150 Some(PathBuf::from(xdg_data_home).join(name))
151 } else {
152 std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".local/share").join(name))
153 }
154 }
155}
156
157impl Storage for FileStorage {
158 fn read(&self, key: &str) -> Result<Option<String>, StorageError> {
159 check_key(key)?;
160 let path = self.path(key);
161 let main = match self.disk.read(&path) {
162 Ok(bytes) => Some(bytes),
163 Err(e) if e.kind() == io::ErrorKind::NotFound => None,
164 Err(e) => return Err(backend_error(&path, &e)),
165 };
166 if !main.as_deref().is_some_and(is_good)
167 && let Some(backup) = self.good_backup(key)
168 {
169 (self.warn)(&format!(
171 "{key}: {}, read its backup instead",
172 damage(main.as_deref())
173 ));
174 return Ok(Some(backup));
175 }
176 main.map(String::from_utf8)
177 .transpose()
178 .map_err(|e| StorageError::Backend(format!("{}: {e}", path.display())))
179 }
180
181 fn write(&mut self, key: &str, value: &str) -> Result<(), StorageError> {
182 check_key(key)?;
183 self.disk
184 .create_dir_all(&self.dir)
185 .map_err(|e| backend_error(&self.dir, &e))?;
186 if !self.swept {
187 self.sweep()?;
188 self.swept = true;
189 }
190 let tmp = self.tmp_path(key);
191 self.disk
192 .write_synced(&tmp, value.as_bytes())
193 .map_err(|e| backend_error(&tmp, &e))?;
194 let dest = self.path(key);
195 let bak = self.bak_path(key);
196 if is_good(value.as_bytes()) {
197 let old = match self.disk.read(&dest) {
198 Ok(bytes) => Some(bytes),
199 Err(e) if e.kind() == io::ErrorKind::NotFound => None,
200 Err(e) => return Err(backend_error(&dest, &e)),
201 };
202 if let Some(old) = old.filter(|old| is_good(old)) {
203 self.disk
204 .write_synced(&bak, &old)
205 .map_err(|e| backend_error(&bak, &e))?;
206 }
207 } else {
208 remove_if_there(&*self.disk, &bak)?;
209 }
210 self.disk
211 .rename(&tmp, &dest)
212 .map_err(|e| backend_error(&dest, &e))?;
213 self.disk
214 .sync_dir(&self.dir)
215 .map_err(|e| backend_error(&self.dir, &e))
216 }
217
218 fn delete(&mut self, key: &str) -> Result<(), StorageError> {
219 check_key(key)?;
220 remove_if_there(&*self.disk, &self.bak_path(key))?;
221 remove_if_there(&*self.disk, &self.path(key))?;
222 match self.disk.sync_dir(&self.dir) {
223 Err(e) if e.kind() != io::ErrorKind::NotFound => Err(backend_error(&self.dir, &e)),
224 _ => Ok(()),
225 }
226 }
227
228 fn list(&self) -> Result<Vec<String>, StorageError> {
229 let names = match self.disk.names(&self.dir) {
230 Ok(names) => names,
231 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
232 Err(e) => return Err(backend_error(&self.dir, &e)),
233 };
234 let mut keys = BTreeSet::new();
235 for name in &names {
236 if let Some(key) = name.strip_suffix(".ron") {
237 keys.insert(key.to_owned());
238 } else if let Some(key) = name.strip_suffix(".ron.bak")
239 && !names.contains(&format!("{key}.ron"))
240 && self.good_backup(key).is_some()
241 {
242 keys.insert(key.to_owned());
243 }
244 }
245 Ok(keys.into_iter().collect())
246 }
247}
248
249fn is_good(bytes: &[u8]) -> bool {
252 !bytes.is_empty() && bytes.iter().any(|&b| b != 0) && std::str::from_utf8(bytes).is_ok()
253}
254
255fn damage(main: Option<&[u8]>) -> &'static str {
259 match main {
260 None => "the file is missing",
261 Some([]) => "the file is empty",
262 Some(bytes) if bytes.iter().all(|&b| b == 0) => "the file is all zero bytes",
263 Some(_) => "the file is not UTF-8",
264 }
265}
266
267fn remove_if_there(disk: &dyn Disk, path: &Path) -> Result<(), StorageError> {
269 match disk.remove(path) {
270 Err(e) if e.kind() != io::ErrorKind::NotFound => Err(backend_error(path, &e)),
271 _ => Ok(()),
272 }
273}
274
275fn check_key(key: &str) -> Result<(), StorageError> {
276 if is_valid_key(key) {
277 Ok(())
278 } else {
279 Err(StorageError::InvalidKey(key.to_owned()))
280 }
281}
282
283fn backend_error(path: &Path, e: &io::Error) -> StorageError {
284 StorageError::Backend(format!("{}: {e}", path.display()))
285}
286
287trait Disk: fmt::Debug + Send + Sync + UnwindSafe + RefUnwindSafe {
291 fn read(&self, path: &Path) -> io::Result<Vec<u8>>;
293 fn write_synced(&self, path: &Path, bytes: &[u8]) -> io::Result<()>;
296 fn rename(&self, from: &Path, to: &Path) -> io::Result<()>;
297 fn remove(&self, path: &Path) -> io::Result<()>;
298 fn sync_dir(&self, dir: &Path) -> io::Result<()>;
301 fn names(&self, dir: &Path) -> io::Result<Vec<String>>;
303 fn create_dir_all(&self, dir: &Path) -> io::Result<()>;
304}
305
306#[derive(Debug)]
308struct StdDisk;
309
310impl Disk for StdDisk {
311 fn read(&self, path: &Path) -> io::Result<Vec<u8>> {
312 fs::read(path)
313 }
314
315 fn write_synced(&self, path: &Path, bytes: &[u8]) -> io::Result<()> {
316 let mut file = fs::File::create(path)?;
317 file.write_all(bytes)?;
318 file.sync_all()
319 }
320
321 fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
322 fs::rename(from, to)
323 }
324
325 fn remove(&self, path: &Path) -> io::Result<()> {
326 fs::remove_file(path)
327 }
328
329 #[cfg(unix)]
332 fn sync_dir(&self, dir: &Path) -> io::Result<()> {
333 match fs::File::open(dir).and_then(|folder| folder.sync_all()) {
334 Err(e)
335 if matches!(
336 e.kind(),
337 io::ErrorKind::Unsupported | io::ErrorKind::InvalidInput
338 ) =>
339 {
340 Ok(())
341 }
342 result => result,
343 }
344 }
345
346 #[cfg(not(unix))]
349 fn sync_dir(&self, _dir: &Path) -> io::Result<()> {
350 Ok(())
351 }
352
353 fn names(&self, dir: &Path) -> io::Result<Vec<String>> {
354 let mut names = Vec::new();
355 for entry in fs::read_dir(dir)? {
356 if let Ok(name) = entry?.file_name().into_string() {
357 names.push(name);
358 }
359 }
360 Ok(names)
361 }
362
363 fn create_dir_all(&self, dir: &Path) -> io::Result<()> {
364 fs::create_dir_all(dir)
365 }
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371 use shuyi_ui::storage::conformance_suite;
372 use std::cell::RefCell;
373
374 thread_local! {
375 static WARNINGS: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
376 }
377
378 fn catch(message: &str) {
380 WARNINGS.with(|warnings| warnings.borrow_mut().push(message.to_owned()));
381 }
382
383 fn warnings() -> Vec<String> {
385 WARNINGS.with(RefCell::take)
386 }
387
388 fn storage_in(dir: PathBuf) -> FileStorage {
389 FileStorage::at(dir, Box::new(StdDisk), catch)
390 }
391
392 fn storage() -> (FileStorage, tempfile::TempDir) {
393 let tmp = tempfile::tempdir().unwrap();
394 (storage_in(tmp.path().to_owned()), tmp)
395 }
396
397 fn files(dir: &Path) -> Vec<String> {
398 let mut names: Vec<_> = fs::read_dir(dir)
399 .unwrap()
400 .map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
401 .collect();
402 names.sort();
403 names
404 }
405
406 #[test]
407 fn passes_the_conformance_suite() {
408 let (mut storage, _tmp) = storage();
409 conformance_suite(&mut storage);
410 }
411
412 #[test]
413 fn the_storage_stays_send_sync_and_unwind_safe() {
414 fn auto_traits<T: Send + Sync + UnwindSafe + RefUnwindSafe>() {}
415 auto_traits::<FileStorage>();
416 }
417
418 #[test]
419 fn writes_are_atomic_and_do_not_leave_tmp_files_behind() {
420 let (mut storage, tmp) = storage();
421 storage.write("save-slot-1", "abc").unwrap();
422 assert_eq!(files(tmp.path()), ["save-slot-1.ron"]);
423 }
424
425 #[test]
426 fn a_second_write_keeps_the_first_value_as_the_backup() {
427 let (mut storage, tmp) = storage();
428 storage.write("save-slot-1", "first").unwrap();
429 storage.write("save-slot-1", "second").unwrap();
430 assert_eq!(
431 files(tmp.path()),
432 ["save-slot-1.ron", "save-slot-1.ron.bak"]
433 );
434 let bak = fs::read_to_string(tmp.path().join("save-slot-1.ron.bak")).unwrap();
435 assert_eq!(bak, "first");
436 assert_eq!(
437 storage.read("save-slot-1").unwrap(),
438 Some("second".to_owned())
439 );
440 }
441
442 #[test]
443 fn a_damaged_main_file_reads_its_backup_and_warns_why() {
444 let damages: [(&str, Option<&[u8]>); 4] = [
445 ("the file is missing", None),
446 ("the file is empty", Some(b"")),
447 ("the file is all zero bytes", Some(b"\0\0\0\0")),
448 ("the file is not UTF-8", Some(b"\xff\xfe(gold")),
449 ];
450 for (why, bytes) in damages {
451 let (mut storage, tmp) = storage();
452 storage.write("save-slot-1", "first").unwrap();
453 storage.write("save-slot-1", "second").unwrap();
454 let main = tmp.path().join("save-slot-1.ron");
455 match bytes {
456 Some(bytes) => fs::write(&main, bytes).unwrap(),
457 None => fs::remove_file(&main).unwrap(),
458 }
459 assert_eq!(
460 storage.read("save-slot-1").unwrap(),
461 Some("first".to_owned()),
462 "{why}"
463 );
464 assert_eq!(
465 warnings(),
466 [format!("save-slot-1: {why}, read its backup instead")]
467 );
468 }
469 }
470
471 #[test]
472 fn without_a_backup_a_damaged_main_file_reads_as_it_is() {
473 let (storage, tmp) = storage();
474 let main = tmp.path().join("save-slot-1.ron");
475 assert_eq!(storage.read("save-slot-1").unwrap(), None);
476 fs::write(&main, b"").unwrap();
477 assert_eq!(storage.read("save-slot-1").unwrap(), Some(String::new()));
478 fs::write(&main, b"\0\0").unwrap();
479 assert_eq!(
480 storage.read("save-slot-1").unwrap(),
481 Some("\0\0".to_owned())
482 );
483 fs::write(&main, b"\xff\xfe").unwrap();
484 assert!(matches!(
485 storage.read("save-slot-1"),
486 Err(StorageError::Backend(_))
487 ));
488 assert_eq!(warnings(), Vec::<String>::new());
489 }
490
491 #[test]
492 fn a_damaged_backup_is_not_read() {
493 let (storage, tmp) = storage();
494 fs::write(tmp.path().join("save-slot-1.ron"), b"").unwrap();
495 fs::write(tmp.path().join("save-slot-1.ron.bak"), b"\0\0").unwrap();
496 assert_eq!(storage.read("save-slot-1").unwrap(), Some(String::new()));
497 assert_eq!(warnings(), Vec::<String>::new());
498 }
499
500 #[test]
501 fn writing_an_empty_or_zero_value_removes_the_backup_and_reads_back() {
502 for value in ["", "\0\0"] {
503 let (mut storage, tmp) = storage();
504 storage.write("save-slot-1", "first").unwrap();
505 storage.write("save-slot-1", "second").unwrap();
506 storage.write("save-slot-1", value).unwrap();
507 assert_eq!(files(tmp.path()), ["save-slot-1.ron"], "{value:?}");
508 assert_eq!(storage.read("save-slot-1").unwrap(), Some(value.to_owned()));
509 assert_eq!(warnings(), Vec::<String>::new());
510 }
511 }
512
513 #[test]
514 fn a_tmp_file_a_crash_left_is_ignored_and_gone_after_the_next_write() {
515 let (mut storage, tmp) = storage();
516 fs::write(tmp.path().join("save-slot-1.ron.tmp"), "(gold: 2").unwrap();
517 assert_eq!(storage.read("save-slot-1").unwrap(), None);
518 assert_eq!(storage.list().unwrap(), Vec::<String>::new());
519 storage.write("settings", "{}").unwrap();
520 assert_eq!(files(tmp.path()), ["settings.ron"]);
521 }
522
523 #[test]
524 fn delete_removes_the_backup_too() {
525 let (mut storage, tmp) = storage();
526 storage.write("save-slot-1", "first").unwrap();
527 storage.write("save-slot-1", "second").unwrap();
528 storage.delete("save-slot-1").unwrap();
529 assert_eq!(files(tmp.path()), Vec::<String>::new());
530 assert_eq!(storage.read("save-slot-1").unwrap(), None);
531 }
532
533 #[test]
534 fn list_names_keys_and_good_lone_backups_but_never_bak_or_tmp_files() {
535 let (storage, tmp) = storage();
536 for (name, bytes) in [
537 ("main.ron", &b"1"[..]),
538 ("main.ron.bak", b"0"),
539 ("lone-backup.ron.bak", b"1"),
540 ("empty-backup.ron.bak", b""),
541 ("left.ron.tmp", b"1"),
542 ] {
543 fs::write(tmp.path().join(name), bytes).unwrap();
544 }
545 assert_eq!(storage.list().unwrap(), ["lone-backup", "main"]);
546 }
547
548 #[test]
549 fn creates_the_directory_on_first_write() {
550 let tmp = tempfile::tempdir().unwrap();
551 let dir = tmp.path().join("nested").join("data");
552 let mut storage = storage_in(dir.clone());
553 assert!(!dir.exists());
554 storage.write("settings", "{}").unwrap();
555 assert!(dir.exists());
556 assert_eq!(storage.read("settings").unwrap(), Some("{}".to_owned()));
557 }
558
559 #[test]
560 fn missing_directory_lists_as_empty_not_an_error() {
561 let tmp = tempfile::tempdir().unwrap();
562 let mut storage = storage_in(tmp.path().join("never-created"));
563 assert_eq!(storage.list().unwrap(), Vec::<String>::new());
564 storage.delete("settings").unwrap();
565 }
566
567 #[test]
568 fn the_data_directory_is_the_storage_name_in_the_os_data_folder() {
569 let dir = data_dir("some-game");
570 assert!(
571 dir.as_ref().is_some_and(|d| d.ends_with("some-game")),
572 "{dir:?}"
573 );
574 #[cfg(target_os = "windows")]
575 assert_eq!(
576 dir,
577 std::env::var_os("APPDATA").map(|d| PathBuf::from(d).join("some-game"))
578 );
579 }
580}