1#![forbid(unsafe_code)] mod clean;
6
7use crate::err::{Action, ErrorSource, Resource};
8use crate::load_store;
9use crate::{Error, LockStatus, Result, StateMgr};
10use fs_mistrust::CheckedDir;
11use fs_mistrust::anon_home::PathExt as _;
12use futures::FutureExt;
13use oneshot_fused_workaround as oneshot;
14use serde::{Serialize, de::DeserializeOwned};
15use std::path::{Path, PathBuf};
16use std::sync::{Arc, Mutex};
17use tor_error::warn_report;
18use tracing::info;
19use web_time_compat::{SystemTime, SystemTimeExt};
20
21#[cfg_attr(docsrs, doc(cfg(not(target_arch = "wasm32"))))]
47#[derive(Clone, Debug)]
48pub struct FsStateMgr {
49 inner: Arc<FsStateMgrInner>,
51}
52
53#[derive(Debug)]
55struct FsStateMgrInner {
56 statepath: CheckedDir,
58 lockfile: Mutex<fslock::LockFile>,
60 #[allow(dead_code)] lock_dropped_tx: oneshot::Sender<void::Void>,
67 lock_dropped_rx: futures::future::Shared<oneshot::Receiver<void::Void>>,
69}
70
71impl FsStateMgr {
72 pub fn from_path_and_mistrust<P: AsRef<Path>>(
79 path: P,
80 mistrust: &fs_mistrust::Mistrust,
81 ) -> Result<Self> {
82 let path = path.as_ref();
83 let dir = path.join("state");
84
85 let statepath = mistrust
86 .verifier()
87 .check_content()
88 .make_secure_dir(&dir)
89 .map_err(|e| {
90 Error::new(
91 e,
92 Action::Initializing,
93 Resource::Directory { dir: dir.clone() },
94 )
95 })?;
96 let lockpath = statepath.join("state.lock").map_err(|e| {
97 Error::new(
98 e,
99 Action::Initializing,
100 Resource::Directory { dir: dir.clone() },
101 )
102 })?;
103
104 let lockfile = Mutex::new(fslock::LockFile::open(&lockpath).map_err(|e| {
105 Error::new(
106 e,
107 Action::Initializing,
108 Resource::File {
109 container: dir,
110 file: "state.lock".into(),
111 },
112 )
113 })?);
114
115 let (lock_dropped_tx, lock_dropped_rx) = oneshot::channel();
116 let lock_dropped_rx = lock_dropped_rx.shared();
117 Ok(FsStateMgr {
118 inner: Arc::new(FsStateMgrInner {
119 statepath,
120 lockfile,
121 lock_dropped_tx,
122 lock_dropped_rx,
123 }),
124 })
125 }
126 #[cfg(test)]
130 pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self> {
131 Self::from_path_and_mistrust(
132 path,
133 &fs_mistrust::Mistrust::new_dangerously_trust_everyone(),
134 )
135 }
136
137 fn rel_filename(&self, key: &str) -> PathBuf {
142 (sanitize_filename::sanitize(key) + ".json").into()
143 }
144 pub fn path(&self) -> &Path {
149 self.inner
150 .statepath
151 .as_path()
152 .parent()
153 .expect("No parent directory even after path.join?")
154 }
155
156 fn clean(&self, now: SystemTime) {
160 for fname in clean::files_to_delete(self.inner.statepath.as_path(), now) {
161 info!("Deleting obsolete file {}", fname.anonymize_home());
162 if let Err(e) = std::fs::remove_file(&fname) {
163 warn_report!(e, "Unable to delete {}", fname.anonymize_home(),);
164 }
165 }
166 }
167
168 fn with_load_store_target<T, F>(&self, key: &str, action: Action, f: F) -> Result<T>
170 where
171 F: FnOnce(load_store::Target<'_>) -> std::result::Result<T, ErrorSource>,
172 {
173 let rel_fname = self.rel_filename(key);
174 f(load_store::Target {
175 dir: &self.inner.statepath,
176 rel_fname: &rel_fname,
177 })
178 .map_err(|source| Error::new(source, action, self.err_resource(key)))
179 }
180
181 fn err_resource(&self, key: &str) -> Resource {
183 Resource::File {
184 container: self.path().to_path_buf(),
185 file: PathBuf::from("state").join(self.rel_filename(key)),
186 }
187 }
188
189 fn err_resource_lock(&self) -> Resource {
191 Resource::File {
192 container: self.path().to_path_buf(),
193 file: "state.lock".into(),
194 }
195 }
196
197 pub fn wait_for_unlock(
199 &self,
200 ) -> impl futures::Future<Output = ()> + Send + Sync + 'static + use<> {
201 self.inner.lock_dropped_rx.clone().map(|_| ())
202 }
203}
204
205impl StateMgr for FsStateMgr {
206 fn can_store(&self) -> bool {
207 let lockfile = self
208 .inner
209 .lockfile
210 .lock()
211 .expect("Poisoned lock on state lockfile");
212 lockfile.owns_lock()
213 }
214 fn try_lock(&self) -> Result<LockStatus> {
215 let mut lockfile = self
216 .inner
217 .lockfile
218 .lock()
219 .expect("Poisoned lock on state lockfile");
220 if lockfile.owns_lock() {
221 Ok(LockStatus::AlreadyHeld)
222 } else if lockfile
223 .try_lock()
224 .map_err(|e| Error::new(e, Action::Locking, self.err_resource_lock()))?
225 {
226 self.clean(SystemTime::get());
227 Ok(LockStatus::NewlyAcquired)
228 } else {
229 Ok(LockStatus::NoLock)
230 }
231 }
232 fn unlock(&self) -> Result<()> {
233 let mut lockfile = self
234 .inner
235 .lockfile
236 .lock()
237 .expect("Poisoned lock on state lockfile");
238 if lockfile.owns_lock() {
239 lockfile
240 .unlock()
241 .map_err(|e| Error::new(e, Action::Unlocking, self.err_resource_lock()))?;
242 }
243 Ok(())
244 }
245 fn load<D>(&self, key: &str) -> Result<Option<D>>
246 where
247 D: DeserializeOwned,
248 {
249 self.with_load_store_target(key, Action::Loading, |t| t.load())
250 }
251
252 fn store<S>(&self, key: &str, val: &S) -> Result<()>
253 where
254 S: Serialize,
255 {
256 if !self.can_store() {
257 return Err(Error::new(
258 ErrorSource::NoLock,
259 Action::Storing,
260 Resource::Manager,
261 ));
262 }
263
264 self.with_load_store_target(key, Action::Storing, |t| t.store(val))
265 }
266}
267
268#[cfg(all(test, not(miri) ))]
269mod test {
270 #![allow(clippy::bool_assert_comparison)]
272 #![allow(clippy::clone_on_copy)]
273 #![allow(clippy::dbg_macro)]
274 #![allow(clippy::mixed_attributes_style)]
275 #![allow(clippy::print_stderr)]
276 #![allow(clippy::print_stdout)]
277 #![allow(clippy::single_char_pattern)]
278 #![allow(clippy::unwrap_used)]
279 #![allow(clippy::unchecked_time_subtraction)]
280 #![allow(clippy::useless_vec)]
281 #![allow(clippy::needless_pass_by_value)]
282 use super::*;
284 use std::{collections::HashMap, time::Duration};
285
286 #[test]
287 fn simple() -> Result<()> {
288 let dir = tempfile::TempDir::new().unwrap();
289 let store = FsStateMgr::from_path(dir.path())?;
290
291 assert_eq!(store.try_lock()?, LockStatus::NewlyAcquired);
292 let stuff: HashMap<_, _> = vec![("hello".to_string(), "world".to_string())]
293 .into_iter()
294 .collect();
295 store.store("xyz", &stuff)?;
296
297 let stuff2: Option<HashMap<String, String>> = store.load("xyz")?;
298 let nothing: Option<HashMap<String, String>> = store.load("abc")?;
299
300 assert_eq!(Some(stuff), stuff2);
301 assert!(nothing.is_none());
302
303 assert_eq!(dir.path(), store.path());
304
305 drop(store); let store = FsStateMgr::from_path(dir.path())?;
307 let stuff3: Option<HashMap<String, String>> = store.load("xyz")?;
308 assert_eq!(stuff2, stuff3);
309
310 let stuff4: HashMap<_, _> = vec![("greetings".to_string(), "humans".to_string())]
311 .into_iter()
312 .collect();
313
314 assert!(matches!(
315 store.store("xyz", &stuff4).unwrap_err().source(),
316 ErrorSource::NoLock
317 ));
318
319 assert_eq!(store.try_lock()?, LockStatus::NewlyAcquired);
320 store.store("xyz", &stuff4)?;
321
322 let stuff5: Option<HashMap<String, String>> = store.load("xyz")?;
323 assert_eq!(Some(stuff4), stuff5);
324
325 Ok(())
326 }
327
328 #[test]
329 fn clean_successful() -> Result<()> {
330 let dir = tempfile::TempDir::new().unwrap();
331 let statedir = dir.path().join("state");
332 let store = FsStateMgr::from_path(dir.path())?;
333
334 assert_eq!(store.try_lock()?, LockStatus::NewlyAcquired);
335 let fname = statedir.join("numbat.toml");
336 let fname2 = statedir.join("quoll.json");
337 std::fs::write(fname, "we no longer use toml files.").unwrap();
338 std::fs::write(fname2, "{}").unwrap();
339
340 let count = statedir.read_dir().unwrap().count();
341 assert_eq!(count, 3); store.clean(SystemTime::get() + Duration::from_secs(365 * 86400));
345 let lst: Vec<_> = statedir.read_dir().unwrap().collect();
346 assert_eq!(lst.len(), 2); assert!(
348 lst.iter()
349 .any(|ent| ent.as_ref().unwrap().file_name() == "quoll.json")
350 );
351
352 Ok(())
353 }
354
355 #[cfg(target_family = "unix")]
356 #[test]
357 fn permissions() -> Result<()> {
358 use std::fs::Permissions;
359 use std::os::unix::fs::PermissionsExt;
360
361 let ro_dir = Permissions::from_mode(0o500);
362 let rw_dir = Permissions::from_mode(0o700);
363 let unusable = Permissions::from_mode(0o000);
364
365 let dir = tempfile::TempDir::new().unwrap();
366 let statedir = dir.path().join("state");
367 let store = FsStateMgr::from_path(dir.path())?;
368
369 assert_eq!(store.try_lock()?, LockStatus::NewlyAcquired);
370 let fname = statedir.join("numbat.toml");
371 let fname2 = statedir.join("quoll.json");
372 std::fs::write(fname, "we no longer use toml files.").unwrap();
373 std::fs::write(&fname2, "{}").unwrap();
374
375 std::fs::set_permissions(&statedir, ro_dir).unwrap();
377 store.clean(SystemTime::get() + Duration::from_secs(365 * 86400));
378 let lst: Vec<_> = statedir.read_dir().unwrap().collect();
379 if lst.len() == 2 {
380 return Ok(());
382 }
383 assert_eq!(lst.len(), 3); std::fs::set_permissions(&statedir, rw_dir).unwrap();
386 std::fs::set_permissions(fname2, unusable).unwrap();
387
388 let h: Result<Option<HashMap<String, u32>>> = store.load("quoll");
389 assert!(h.is_err());
390 assert!(matches!(h.unwrap_err().source(), ErrorSource::IoError(_)));
391
392 Ok(())
393 }
394
395 #[test]
396 fn locking() {
397 let dir = tempfile::TempDir::new().unwrap();
398 let store1 = FsStateMgr::from_path(dir.path()).unwrap();
399 let store2 = FsStateMgr::from_path(dir.path()).unwrap();
400
401 assert_eq!(store1.try_lock().unwrap(), LockStatus::NewlyAcquired);
403 assert_eq!(store1.try_lock().unwrap(), LockStatus::AlreadyHeld);
404 assert!(store1.can_store());
405
406 assert!(!store2.can_store());
408 assert_eq!(store2.try_lock().unwrap(), LockStatus::NoLock);
409 assert!(!store2.can_store());
410
411 store1.unlock().unwrap();
413 assert!(!store1.can_store());
414 assert!(!store2.can_store());
415
416 assert_eq!(store2.try_lock().unwrap(), LockStatus::NewlyAcquired);
418 assert!(store2.can_store());
419 assert!(!store1.can_store());
420 }
421
422 #[test]
423 fn errors() {
424 let dir = tempfile::TempDir::new().unwrap();
425 let store = FsStateMgr::from_path(dir.path()).unwrap();
426
427 let nonesuch: Result<Option<String>> = store.load("Hello");
429 assert!(matches!(nonesuch, Ok(None)));
430
431 let file: PathBuf = ["state", "Hello.json"].iter().collect();
433 std::fs::write(dir.path().join(&file), b"hello world \x00\xff").unwrap();
434 let bad_utf8: Result<Option<String>> = store.load("Hello");
435 assert!(bad_utf8.is_err());
436 assert_eq!(
437 bad_utf8.unwrap_err().to_string(),
438 format!(
439 "IO error while loading persistent data on {} in {}",
440 file.to_string_lossy(),
441 dir.path().anonymize_home(),
442 ),
443 );
444 }
445}