1use std::collections::HashMap;
8use std::path::PathBuf;
9
10pub const MAX_KEY: usize = 128;
11pub const MAX_VALUE: usize = 1 << 20;
12pub const QUOTA: usize = 16 << 20;
13
14pub struct Storage {
15 dir: Option<PathBuf>,
16 values: HashMap<String, Vec<u8>>,
17}
18
19pub fn valid_key(k: &str) -> bool {
20 !k.is_empty()
21 && k.len() <= MAX_KEY
22 && k.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'.' || b == b'_' || b == b'-')
23 && k != "."
24 && k != ".."
25}
26
27pub fn default_dir(game_id: &str) -> Option<PathBuf> {
30 let base = if cfg!(target_os = "macos") {
31 std::env::var_os("HOME").map(|h| PathBuf::from(h).join("Library/Application Support"))
32 } else if cfg!(windows) {
33 std::env::var_os("APPDATA").map(PathBuf::from)
34 } else {
35 std::env::var_os("XDG_DATA_HOME")
36 .map(PathBuf::from)
37 .or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".local/share")))
38 };
39 base.map(|b| b.join("gasm").join(game_id))
40}
41
42impl Storage {
43 pub fn memory() -> Storage {
44 Storage { dir: None, values: HashMap::new() }
45 }
46
47 pub fn open(dir: PathBuf) -> Result<Storage, String> {
49 std::fs::create_dir_all(&dir).map_err(|e| format!("{}: {e}", dir.display()))?;
50 let mut values = HashMap::new();
51 for entry in std::fs::read_dir(&dir).map_err(|e| e.to_string())?.flatten() {
52 let name = entry.file_name().to_string_lossy().into_owned();
53 if valid_key(&name) && entry.path().is_file() {
54 if let Ok(v) = std::fs::read(entry.path()) {
55 values.insert(name, v);
56 }
57 }
58 }
59 Ok(Storage { dir: Some(dir), values })
60 }
61
62 pub fn location(&self) -> String {
63 self.dir.as_ref().map_or("memory".into(), |d| d.display().to_string())
64 }
65
66 pub fn get(&self, key: &str) -> Option<&[u8]> {
67 self.values.get(key).map(Vec::as_slice)
68 }
69
70 pub fn set(&mut self, key: &str, value: &[u8]) -> Result<(), String> {
71 if !valid_key(key) {
72 return Err(format!("invalid key {key:?}"));
73 }
74 if value.len() > MAX_VALUE {
75 return Err(format!("value for {key:?} is {} bytes (max {MAX_VALUE})", value.len()));
76 }
77 let used: usize = self.values.iter().filter(|(k, _)| k.as_str() != key).map(|(k, v)| k.len() + v.len()).sum();
78 if used + key.len() + value.len() > QUOTA {
79 return Err(format!("storage quota of {QUOTA} bytes exceeded"));
80 }
81 if let Some(dir) = &self.dir {
82 let tmp = dir.join(format!(".{key}.tmp"));
83 std::fs::write(&tmp, value).map_err(|e| e.to_string())?;
84 std::fs::rename(&tmp, dir.join(key)).map_err(|e| e.to_string())?;
85 }
86 self.values.insert(key.to_owned(), value.to_vec());
87 Ok(())
88 }
89
90 pub fn delete(&mut self, key: &str) -> bool {
91 if self.values.remove(key).is_none() {
92 return false;
93 }
94 if let Some(dir) = &self.dir {
95 let _ = std::fs::remove_file(dir.join(key));
96 }
97 true
98 }
99}