use super::{Backend, Error, Host, RawStore};
use crate::context::Context;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Fault {
FailWrite(String),
CorruptWrite(String),
Unreadable(String),
Vanish,
LocksOnWrite,
DeletedAfterWrite,
LocksAfterWrite,
}
#[derive(Debug)]
pub struct MemoryStore {
kind: Backend,
items: Mutex<HashMap<String, String>>,
faults: Mutex<HashMap<String, (usize, Fault)>>,
blanket: Mutex<Option<Fault>>,
locked: Mutex<std::collections::HashSet<String>>,
ceiling: Mutex<Option<usize>>,
}
impl MemoryStore {
pub fn of(kind: Backend) -> Arc<MemoryStore> {
Arc::new(MemoryStore {
kind,
items: Mutex::new(HashMap::new()),
faults: Mutex::new(HashMap::new()),
blanket: Mutex::new(None),
locked: Mutex::new(std::collections::HashSet::new()),
ceiling: Mutex::new(None),
})
}
pub fn takes_on_stdin(&self, limit: usize) {
*self
.ceiling
.lock()
.expect("a poisoned test store is a failed test") = Some(limit);
}
pub fn plant(&self, service: &str, contents: &str) {
self.items
.lock()
.expect("a poisoned test store is a failed test")
.insert(service.into(), contents.into());
}
pub fn peek(&self, service: &str) -> Option<String> {
self.items
.lock()
.expect("a poisoned test store is a failed test")
.get(service)
.cloned()
}
pub fn services(&self) -> Vec<String> {
let mut names: Vec<String> = self
.items
.lock()
.expect("a poisoned test store is a failed test")
.keys()
.cloned()
.collect();
names.sort();
names
}
pub fn fault(&self, service: &str, fault: Fault) {
self.fault_after(service, 0, fault);
}
pub fn fault_after(&self, service: &str, calls: usize, fault: Fault) {
self.faults
.lock()
.expect("a poisoned test store is a failed test")
.insert(service.into(), (calls, fault));
}
pub fn fault_all(&self, fault: Fault) {
*self
.blanket
.lock()
.expect("a poisoned test store is a failed test") = Some(fault);
}
pub fn delete_everything(&self) {
self.items
.lock()
.expect("a poisoned test store is a failed test")
.clear();
}
pub fn heal_all(&self) {
*self
.blanket
.lock()
.expect("a poisoned test store is a failed test") = None;
self.faults
.lock()
.expect("a poisoned test store is a failed test")
.clear();
self.locked
.lock()
.expect("a poisoned test store is a failed test")
.clear();
}
pub fn heal(&self, service: &str) {
self.faults
.lock()
.expect("a poisoned test store is a failed test")
.remove(service);
}
fn has_locked(&self, service: &str) -> bool {
self.locked
.lock()
.expect("a poisoned test store is a failed test")
.contains(service)
}
fn lock_now(&self, service: &str) {
self.locked
.lock()
.expect("a poisoned test store is a failed test")
.insert(service.to_string());
}
fn fault_for(&self, service: &str) -> Option<Fault> {
let mut faults = self
.faults
.lock()
.expect("a poisoned test store is a failed test");
if let Some((countdown, fault)) = faults.get_mut(service) {
if *countdown == 0 {
return Some(fault.clone());
}
*countdown -= 1;
return None;
}
drop(faults);
self.blanket
.lock()
.expect("a poisoned test store is a failed test")
.clone()
}
}
impl RawStore for Arc<MemoryStore> {
fn kind(&self) -> Backend {
self.kind
}
fn contains(&self, service: &str) -> Result<bool, Error> {
Ok(self.read(service)?.is_some())
}
fn read(&self, service: &str) -> Result<Option<String>, Error> {
if self.has_locked(service) {
return Err(Error::Unreadable("the keychain is locked".into()));
}
match self.fault_for(service) {
Some(Fault::Unreadable(why)) => Err(Error::Unreadable(why)),
Some(Fault::Vanish) => Ok(None),
_ => Ok(self.peek(service)),
}
}
fn write(&self, service: &str, contents: &str) -> Result<(), Error> {
if self.has_locked(service) {
return Err(Error::Write("the keychain is locked".into()));
}
match self.fault_for(service) {
Some(Fault::FailWrite(why)) => return Err(Error::Write(why)),
Some(Fault::Unreadable(why)) => return Err(Error::Unreadable(why)),
Some(Fault::LocksOnWrite) => {
self.lock_now(service);
return Err(Error::Write("the keychain is locked".into()));
}
Some(Fault::LocksAfterWrite) => {
self.plant(service, contents);
self.lock_now(service);
return Err(Error::Unreadable("the keychain is locked".into()));
}
Some(Fault::CorruptWrite(instead)) => self.plant(service, &instead),
Some(Fault::DeletedAfterWrite) => {
self.items
.lock()
.expect("a poisoned test store is a failed test")
.remove(service);
self.heal(service);
return Ok(());
}
_ => self.plant(service, contents),
}
match self.peek(service) {
Some(back) if back == contents => Ok(()),
_ => Err(Error::NotDurable(format!(
"{service} holds different bytes"
))),
}
}
fn delete(&self, service: &str) -> Result<(), Error> {
self.items
.lock()
.expect("a poisoned test store is a failed test")
.remove(service);
Ok(())
}
fn list(&self) -> Result<Option<Vec<String>>, Error> {
Ok(Some(
self.services()
.into_iter()
.filter(|s| crate::park::is_park_name(s))
.collect(),
))
}
fn cost(&self, _service: &str, contents: &str) -> Option<super::Cost> {
let limit = *self
.ceiling
.lock()
.expect("a poisoned test store is a failed test");
limit.map(|limit| super::Cost {
needs: contents.len(),
limit,
second_route: true,
})
}
}
struct Vault {
store: Arc<MemoryStore>,
argument_line: bool,
}
impl RawStore for Vault {
fn kind(&self) -> Backend {
RawStore::kind(&self.store)
}
fn contains(&self, service: &str) -> Result<bool, Error> {
RawStore::contains(&self.store, service)
}
fn read(&self, service: &str) -> Result<Option<String>, Error> {
RawStore::read(&self.store, service)
}
fn write(&self, service: &str, contents: &str) -> Result<(), Error> {
if let Some(cost) = self.cost(service, contents)
&& cost.refused()
{
return Err(Error::Write(format!(
"this credential is {} bytes, past the {}-byte command limit",
cost.needs, cost.limit
)));
}
RawStore::write(&self.store, service, contents)
}
fn delete(&self, service: &str) -> Result<(), Error> {
RawStore::delete(&self.store, service)
}
fn list(&self) -> Result<Option<Vec<String>>, Error> {
RawStore::list(&self.store)
}
fn cost(&self, service: &str, contents: &str) -> Option<super::Cost> {
RawStore::cost(&self.store, service, contents).map(|cost| super::Cost {
second_route: self.argument_line,
..cost
})
}
}
#[derive(Debug)]
pub struct MemoryHost {
keychain: Arc<MemoryStore>,
vault: Arc<MemoryStore>,
files: Mutex<HashMap<PathBuf, Arc<MemoryStore>>>,
running: Mutex<HashMap<String, usize>>,
shared_vault: std::sync::atomic::AtomicBool,
}
impl Default for MemoryHost {
fn default() -> MemoryHost {
MemoryHost {
keychain: MemoryStore::of(Backend::Keychain),
vault: MemoryStore::of(Backend::Keychain),
files: Mutex::new(HashMap::new()),
running: Mutex::new(HashMap::new()),
shared_vault: std::sync::atomic::AtomicBool::new(true),
}
}
}
impl MemoryHost {
pub fn new() -> Arc<MemoryHost> {
Arc::new(MemoryHost::default())
}
pub fn live(&self) -> &Arc<MemoryStore> {
&self.keychain
}
pub fn vault(&self) -> &Arc<MemoryStore> {
&self.vault
}
pub fn file_at(&self, path: PathBuf) -> Arc<MemoryStore> {
Arc::clone(
self.files
.lock()
.expect("a poisoned test host is a failed test")
.entry(path)
.or_insert_with(|| MemoryStore::of(Backend::File)),
)
}
pub fn vault_of_its_own(&self) {
self.shared_vault
.store(false, std::sync::atomic::Ordering::SeqCst);
}
pub fn runs(&self, program: &str, count: usize) {
self.running
.lock()
.expect("a poisoned test host is a failed test")
.insert(program.to_string(), count);
}
}
impl Host for MemoryHost {
fn foreign_keychain(&self, _ctx: &Context, _account: &str) -> Option<Box<dyn RawStore>> {
Some(Box::new(Arc::clone(&self.keychain)))
}
fn file(&self, path: PathBuf) -> Box<dyn RawStore> {
Box::new(self.file_at(path))
}
fn vault(&self, ctx: &Context) -> Box<dyn RawStore> {
Box::new(Vault {
store: Arc::clone(&self.vault),
argument_line: ctx.argv_fallback(),
})
}
fn vault_is_shared(&self) -> bool {
self.shared_vault.load(std::sync::atomic::Ordering::SeqCst)
}
fn running(&self, program: &str) -> Option<usize> {
Some(
self.running
.lock()
.expect("a poisoned test host is a failed test")
.get(program)
.copied()
.unwrap_or(0),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn store() -> Arc<MemoryStore> {
MemoryStore::of(Backend::Keychain)
}
#[test]
fn a_write_that_does_not_read_back_is_reported_rather_than_believed() {
let s = store();
s.fault("svc", Fault::CorruptWrite("something else".into()));
assert!(matches!(s.write("svc", "after"), Err(Error::NotDurable(_))));
assert_eq!(s.peek("svc").as_deref(), Some("something else"));
}
#[test]
fn a_refused_write_leaves_what_was_there() {
let s = store();
s.plant("svc", "before");
s.fault("svc", Fault::FailWrite("told to".into()));
assert!(matches!(s.write("svc", "after"), Err(Error::Write(_))));
assert_eq!(s.peek("svc").as_deref(), Some("before"));
}
#[test]
fn an_unreadable_store_never_answers_absent() {
let s = store();
s.plant("svc", "before");
s.fault("svc", Fault::Unreadable("the keychain is locked".into()));
assert!(matches!(s.read("svc"), Err(Error::Unreadable(_))));
assert!(matches!(s.contains("svc"), Err(Error::Unreadable(_))));
}
#[test]
fn something_that_vanished_reads_as_gone_but_is_still_there_to_assert_on() {
let s = store();
s.plant("svc", "before");
s.fault("svc", Fault::Vanish);
assert_eq!(s.read("svc").expect("vanishing is not a failure"), None);
assert_eq!(s.peek("svc").as_deref(), Some("before"));
s.heal("svc");
assert_eq!(s.read("svc").expect("healed"), Some("before".into()));
}
#[test]
fn a_store_that_locks_on_a_write_reads_fine_until_then() {
let s = store();
s.plant("svc", "before");
s.fault("svc", Fault::LocksOnWrite);
assert_eq!(
s.read("svc").expect("still readable"),
Some("before".into())
);
assert!(matches!(s.write("svc", "after"), Err(Error::Write(_))));
assert!(
matches!(s.read("svc"), Err(Error::Unreadable(_))),
"once it is locked it cannot answer at all"
);
assert_eq!(
s.peek("svc").as_deref(),
Some("before"),
"and nothing moved"
);
s.heal_all();
assert_eq!(s.read("svc").expect("unlocked"), Some("before".into()));
}
#[test]
fn a_fault_can_wait_for_a_few_calls_first() {
let s = store();
s.plant("svc", "before");
s.fault_after("svc", 2, Fault::Vanish);
assert_eq!(s.read("svc").expect("first"), Some("before".into()));
assert_eq!(s.read("svc").expect("second"), Some("before".into()));
assert_eq!(s.read("svc").expect("third"), None, "now it is gone");
}
#[test]
fn a_vault_can_be_enumerated_which_no_real_one_on_macos_can() {
let s = store();
s.plant("pitboard-park-b", "2");
s.plant("pitboard-park-a", "1");
assert_eq!(s.services(), vec!["pitboard-park-a", "pitboard-park-b"]);
}
}