use super::{
Administered, Cookie, Elevation, Floor, Host, Jar, JarError, Listed, Process, Scheduler,
};
use crate::context::Context;
use crate::service::Permit;
use crate::store::memory::{Fault, MemoryStore, Touch};
use crate::store::{Backend, Cost, Error, RawStore};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
struct Opened {
store: Arc<MemoryStore>,
argument_line: bool,
}
impl RawStore for Opened {
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, permit: Permit, 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, permit, service, contents)
}
fn delete(&self, permit: Permit, service: &str) -> Result<(), Error> {
RawStore::delete(&self.store, permit, service)
}
fn list(&self) -> Result<Option<Vec<String>>, Error> {
RawStore::list(&self.store)
}
fn cost(&self, service: &str, contents: &str) -> Option<Cost> {
RawStore::cost(&self.store, service, contents).map(|cost| Cost {
second_route: self.argument_line,
..cost
})
}
}
#[derive(Debug)]
pub struct MemoryHost {
keychain: Arc<MemoryStore>,
vault: Arc<MemoryStore>,
files: Mutex<HashMap<PathBuf, Arc<MemoryStore>>>,
trail: Arc<Mutex<Vec<Touch>>>,
running: Mutex<HashMap<String, Vec<Process>>>,
listless: AtomicBool,
parents: Mutex<HashMap<PathBuf, PathBuf>>,
later: Mutex<Vec<(usize, PathBuf)>>,
listings: AtomicUsize,
jars_asked: Mutex<Vec<PathBuf>>,
jar_faults: Mutex<HashMap<PathBuf, JarFault>>,
versions: Mutex<HashMap<PathBuf, String>>,
shared_vault: AtomicBool,
scheduler: Option<Box<dyn Scheduler>>,
refuse_start: Arc<AtomicBool>,
unscheduled: AtomicBool,
elevation: Mutex<Elevation>,
floor: Mutex<Floor>,
administered: Mutex<HashMap<PathBuf, Administered>>,
forced: Mutex<HashMap<(String, String), Administered>>,
}
impl Default for MemoryHost {
fn default() -> MemoryHost {
let refuse_start = Arc::new(AtomicBool::new(false));
let trail = Arc::default();
MemoryHost {
keychain: MemoryStore::on_trail(Backend::Keychain, "live".into(), Arc::clone(&trail)),
vault: MemoryStore::on_trail(Backend::Keychain, "vault".into(), Arc::clone(&trail)),
files: Mutex::new(HashMap::new()),
trail,
running: Mutex::new(HashMap::new()),
listless: AtomicBool::new(false),
parents: Mutex::new(HashMap::new()),
later: Mutex::new(Vec::new()),
listings: AtomicUsize::new(0),
jars_asked: Mutex::new(Vec::new()),
jar_faults: Mutex::new(HashMap::new()),
versions: Mutex::new(HashMap::new()),
shared_vault: AtomicBool::new(true),
scheduler: super::os::pretend_scheduler(Arc::clone(&refuse_start)),
refuse_start,
unscheduled: AtomicBool::new(false),
elevation: Mutex::new(Elevation::Normal),
floor: Mutex::new(Floor::Met),
administered: Mutex::new(HashMap::new()),
forced: Mutex::new(HashMap::new()),
}
}
}
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> {
let name = format!("file {}", path.display());
Arc::clone(
self.files
.lock()
.expect("a poisoned test host is a failed test")
.entry(path)
.or_insert_with(|| {
MemoryStore::on_trail(Backend::File, name, Arc::clone(&self.trail))
}),
)
}
pub fn trail(&self) -> Vec<Touch> {
self.trail
.lock()
.expect("a poisoned test host is a failed test")
.clone()
}
pub fn lock_keychain(&self) {
self.keychain.fault_all(Fault::Locked);
self.vault.fault_all(Fault::Locked);
}
pub fn unlock_keychain(&self) {
self.keychain.heal_all();
self.vault.heal_all();
}
pub fn vault_of_its_own(&self) {
self.shared_vault.store(false, Ordering::SeqCst);
}
pub fn runs(&self, program: &str, count: usize) {
self.runs_at(program, &vec![program; count]);
}
pub fn runs_at(&self, program: &str, paths: &[&str]) {
let processes = (1..)
.zip(paths)
.map(|(pid, path)| Process {
pid,
path: PathBuf::from(path),
})
.collect();
self.running
.lock()
.expect("a poisoned test host is a failed test")
.insert(program.to_string(), processes);
}
pub fn without_a_process_list(&self) {
self.listless.store(true, Ordering::SeqCst);
}
pub fn child_of(&self, child: &str, parent: &str) {
locked(&self.parents).insert(child.into(), parent.into());
}
pub fn listed_from(&self, n: usize, path: &str) {
let from = self.listings() + n;
locked(&self.later).push((from, path.into()));
}
pub fn listed_no_longer(&self, path: &str) {
locked(&self.later).retain(|(_, listed)| listed != Path::new(path));
}
pub fn listings(&self) -> usize {
self.listings.load(Ordering::SeqCst)
}
pub fn jars_asked(&self) -> Vec<PathBuf> {
locked(&self.jars_asked).clone()
}
pub fn jar_unsettled(&self, path: &Path) {
locked(&self.jar_faults).insert(path.into(), JarFault::Unsettled);
}
pub fn jar_unknown(&self, path: &Path, why: &str) {
locked(&self.jar_faults).insert(path.into(), JarFault::Unknown(why.into()));
}
pub fn app_at(&self, bundle: &Path, version: &str) {
locked(&self.versions).insert(bundle.into(), version.into());
}
pub fn refuse_next_start(&self) {
self.refuse_start.store(true, Ordering::SeqCst);
}
pub fn without_a_scheduler(&self) {
self.unscheduled.store(true, Ordering::SeqCst);
}
pub fn runs_with(&self, elevation: Elevation) {
*self
.elevation
.lock()
.expect("a poisoned test host is a failed test") = elevation;
}
pub fn runs_on(&self, floor: Floor) {
*self
.floor
.lock()
.expect("a poisoned test host is a failed test") = floor;
}
pub fn administers(&self, path: impl Into<PathBuf>, contents: &str) {
self.administer(path.into(), Administered::Set(contents.into()));
}
pub fn administers_unreadably(&self, path: impl Into<PathBuf>, why: &str) {
self.administer(path.into(), Administered::Unreadable(why.into()));
}
fn administer(&self, path: PathBuf, holds: Administered) {
self.administered
.lock()
.expect("a poisoned test host is a failed test")
.insert(path, holds);
}
pub fn forces(&self, domain: &str, key: &str, value: &str) {
self.forced
.lock()
.expect("a poisoned test host is a failed test")
.insert(
(domain.to_string(), key.to_string()),
Administered::Set(value.into()),
);
}
}
impl Host for MemoryHost {
fn foreign_secrets(&self, ctx: &Context, _account: &str) -> Option<Box<dyn RawStore>> {
Some(Box::new(Opened {
store: Arc::clone(&self.keychain),
argument_line: ctx.argv_fallback(),
}))
}
fn file(&self, path: PathBuf) -> Box<dyn RawStore> {
Box::new(self.file_at(path))
}
fn vault(&self, ctx: &Context) -> Box<dyn RawStore> {
Box::new(Opened {
store: Arc::clone(&self.vault),
argument_line: ctx.argv_fallback(),
})
}
fn vault_is_shared(&self) -> bool {
self.shared_vault.load(Ordering::SeqCst)
}
fn listing(&self) -> Result<Vec<Listed>, String> {
if self.listless.load(Ordering::SeqCst) {
self.listings.fetch_add(1, Ordering::SeqCst);
return Err("a test withheld the process list".into());
}
let (call, mut paths) = {
let running = locked(&self.running);
let mut programs: Vec<&String> = running.keys().collect();
programs.sort();
let paths: Vec<PathBuf> = programs
.into_iter()
.flat_map(|program| running[program].iter().map(|p| p.path.clone()))
.collect();
(self.listings.fetch_add(1, Ordering::SeqCst) + 1, paths)
};
paths.extend(
locked(&self.later)
.iter()
.filter(|(from, _)| call >= *from)
.map(|(_, path)| path.clone()),
);
let pid = |index: usize| 1000 + u32::try_from(index).unwrap_or(u32::MAX - 1000);
let parents = locked(&self.parents);
Ok(paths
.iter()
.enumerate()
.map(|(index, path)| Listed {
pid: pid(index),
parent: parents
.get(path)
.and_then(|parent| paths.iter().position(|p| p == parent))
.map_or(1, pid),
path: path.clone(),
})
.collect())
}
fn cookies(&self, jar: &Path, hosts: &[&str], names: &[&str]) -> Result<Jar, JarError> {
locked(&self.jars_asked).push(jar.to_path_buf());
match locked(&self.jar_faults).get(jar) {
Some(JarFault::Unsettled) => {
return Err(JarError::Unsettled("a test held the jar".into()));
}
Some(JarFault::Unknown(why)) => return Err(JarError::Unknown(why.clone())),
None => {}
}
let raw = match std::fs::read(jar) {
Ok(raw) => raw,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Err(JarError::Absent),
Err(e) => return Err(JarError::Unreadable(e.to_string())),
};
let stand_in: StandInJar =
serde_json::from_slice(&raw).map_err(|e| JarError::Unknown(e.to_string()))?;
Ok(Jar {
version: stand_in.version,
compatible: stand_in.compatible,
cookies: stand_in
.cookies
.into_iter()
.filter(|c| hosts.contains(&c.host.as_str()) || names.contains(&c.name.as_str()))
.map(|c| {
Ok(Cookie {
stored: hex::decode(&c.stored)
.map_err(|e| JarError::Unknown(e.to_string()))?,
host: c.host,
name: c.name,
path: c.path,
expires_utc: c.expires_utc,
})
})
.collect::<Result<_, _>>()?,
})
}
fn app_version(&self, bundle: &Path) -> Option<String> {
locked(&self.versions).get(bundle).cloned()
}
fn app_id(&self, _bundle: &Path) -> Option<String> {
None
}
fn processes(&self, program: &str) -> Option<Vec<Process>> {
if self.listless.load(Ordering::SeqCst) {
return None;
}
Some(
self.running
.lock()
.expect("a poisoned test host is a failed test")
.get(program)
.cloned()
.unwrap_or_default(),
)
}
fn scheduler(&self) -> Option<&dyn Scheduler> {
self.scheduler
.as_deref()
.filter(|_| !self.unscheduled.load(Ordering::SeqCst))
}
fn elevation(&self, _ctx: &Context) -> Elevation {
*self
.elevation
.lock()
.expect("a poisoned test host is a failed test")
}
fn floor(&self) -> Floor {
*self
.floor
.lock()
.expect("a poisoned test host is a failed test")
}
fn administered_file(&self, path: &Path) -> Administered {
self.administered
.lock()
.expect("a poisoned test host is a failed test")
.get(path)
.cloned()
.unwrap_or(Administered::Unset)
}
fn managed_preference(&self, domain: &str, key: &str) -> Administered {
self.forced
.lock()
.expect("a poisoned test host is a failed test")
.get(&(domain.to_string(), key.to_string()))
.cloned()
.unwrap_or(Administered::Unset)
}
}
#[derive(Debug)]
enum JarFault {
Unsettled,
Unknown(String),
}
#[derive(serde::Serialize, serde::Deserialize)]
pub(crate) struct StandInJar {
pub version: i64,
pub compatible: i64,
pub cookies: Vec<StandInCookie>,
}
#[derive(serde::Serialize, serde::Deserialize)]
pub(crate) struct StandInCookie {
pub host: String,
pub name: String,
pub path: String,
pub stored: String,
pub expires_utc: i64,
}
fn locked<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
mutex.lock().expect("a poisoned test host is a failed test")
}