supreme_fs/game_storage/
local_ops.rs1use super::GameStorage;
6use sha2::{Digest, Sha256};
7use std::fs::{self, File};
8use std::io::{self, Read};
9use std::path::{Path, PathBuf};
10
11impl GameStorage {
12 pub fn calculate_hash(&self, path: &Path) -> io::Result<String> {
18 let mut file = File::open(path)?;
19 let mut hasher = Sha256::new();
20 let mut buffer = [0; 1024];
21 loop {
22 let count = file.read(&mut buffer)?;
23 if count == 0 {
24 break;
25 }
26 hasher.update(&buffer[..count]);
27 }
28 Ok(hex::encode(hasher.finalize()))
29 }
30
31 pub fn list_snapshots_profiles(&self) -> io::Result<Vec<String>> {
37 let mut profiles = Vec::new();
38 if let Ok(entries) = fs::read_dir(self.profiles_path()) {
39 for entry in entries.flatten() {
40 let path = entry.path();
41 if path.extension().and_then(|s| s.to_str()) == Some("json")
42 && let Some(stem) = path.file_stem().and_then(|s| s.to_str())
43 {
44 profiles.push(stem.to_string());
45 }
46 }
47 }
48 profiles.sort();
49 Ok(profiles)
50 }
51
52 pub fn clean_up_empty_dirs(&self, start_dir: &Path, limit_dir: &Path) {
54 let mut current = start_dir;
55 while current.starts_with(limit_dir) && current != limit_dir {
56 if fs::remove_dir(current).is_ok() {
57 if let Some(p) = current.parent() {
58 current = p;
59 } else {
60 break;
61 }
62 } else {
63 break;
64 }
65 }
66 }
67
68 pub fn write_report<T: serde::Serialize>(&self, name: &str, value: &T) -> io::Result<()> {
74 let report_dir = self.root_path.join(".supreme");
75 fs::create_dir_all(&report_dir)?;
76
77 let report_path = report_dir.join(format!("{name}.json"));
78 let file = File::create(report_path)?;
79 serde_json::to_writer_pretty(file, value).map_err(io::Error::other)
80 }
81
82 #[must_use]
84 pub fn get_report_path(&self, name: &str) -> PathBuf {
85 self.root_path.join(".supreme").join(format!("{name}.json"))
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92 use std::fs;
93 use tempfile::tempdir;
94
95 fn make_storage() -> (tempfile::TempDir, GameStorage) {
96 let dir = tempdir().expect("tempdir");
97 let storage = GameStorage::new(dir.path());
98 (dir, storage)
99 }
100
101 #[test]
102 fn calculate_hash_matches_known_sha256() {
103 let (dir, storage) = make_storage();
104 let file_path = dir.path().join("hello.txt");
105 fs::write(&file_path, b"hello world").unwrap();
106
107 let hash = storage.calculate_hash(&file_path).expect("hash");
108 assert_eq!(
110 hash,
111 "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
112 );
113 }
114
115 #[test]
116 fn calculate_hash_of_empty_file_matches_known_sha256() {
117 let (dir, storage) = make_storage();
118 let file_path = dir.path().join("empty.txt");
119 fs::write(&file_path, b"").unwrap();
120
121 let hash = storage.calculate_hash(&file_path).expect("hash");
122 assert_eq!(
123 hash,
124 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
125 );
126 }
127
128 #[test]
129 fn calculate_hash_errors_on_missing_file() {
130 let (dir, storage) = make_storage();
131 let result = storage.calculate_hash(&dir.path().join("missing.txt"));
132 assert!(result.is_err());
133 }
134
135 #[test]
136 fn list_snapshots_profiles_returns_sorted_json_stems() {
137 let (_dir, storage) = make_storage();
138 fs::create_dir_all(storage.profiles_path()).unwrap();
139 fs::write(storage.profiles_path().join("zeta.json"), b"{}").unwrap();
140 fs::write(storage.profiles_path().join("alpha.json"), b"{}").unwrap();
141 fs::write(storage.profiles_path().join("notes.txt"), b"ignore me").unwrap();
143
144 let profiles = storage.list_snapshots_profiles().expect("list");
145 assert_eq!(profiles, vec!["alpha".to_string(), "zeta".to_string()]);
146 }
147
148 #[test]
149 fn list_snapshots_profiles_on_missing_dir_returns_empty() {
150 let (_dir, storage) = make_storage();
151 let profiles = storage.list_snapshots_profiles().expect("list");
152 assert!(profiles.is_empty());
153 }
154
155 #[test]
156 fn clean_up_empty_dirs_removes_empty_chain_but_stops_at_limit() {
157 let (dir, storage) = make_storage();
158 let nested = dir.path().join("a/b/c");
159 fs::create_dir_all(&nested).unwrap();
160
161 storage.clean_up_empty_dirs(&nested, dir.path());
162
163 assert!(!nested.exists());
164 assert!(!dir.path().join("a/b").exists());
165 assert!(!dir.path().join("a").exists());
166 assert!(dir.path().exists());
168 }
169
170 #[test]
171 fn clean_up_empty_dirs_stops_when_a_directory_is_not_empty() {
172 let (dir, storage) = make_storage();
173 let nested = dir.path().join("a/b/c");
174 fs::create_dir_all(&nested).unwrap();
175 fs::write(dir.path().join("a/b/keep.txt"), b"keep").unwrap();
177
178 storage.clean_up_empty_dirs(&nested, dir.path());
179
180 assert!(!nested.exists());
181 assert!(dir.path().join("a/b").exists());
182 assert!(dir.path().join("a/b/keep.txt").exists());
183 }
184
185 #[test]
186 fn write_report_creates_json_file_readable_back() {
187 let (_dir, storage) = make_storage();
188 #[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug)]
189 struct Report {
190 ok: bool,
191 count: u32,
192 }
193 let report = Report { ok: true, count: 3 };
194
195 storage
196 .write_report("summary", &report)
197 .expect("write_report");
198
199 let path = storage.get_report_path("summary");
200 assert!(path.exists());
201 let contents = fs::read_to_string(path).unwrap();
202 let parsed: Report = serde_json::from_str(&contents).unwrap();
203 assert_eq!(parsed, report);
204 }
205
206 #[test]
207 fn get_report_path_is_under_dot_supreme() {
208 let (dir, storage) = make_storage();
209 let path = storage.get_report_path("mysummary");
210 assert_eq!(path, dir.path().join(".supreme").join("mysummary.json"));
211 }
212}