use super::{
Administered, Cookie, Elevation, Floor, Host, Jar, JarError, Listed, Process, Scheduler,
WindowsSettings,
};
use crate::context::Context;
use crate::provider::claude::credman::FORCE;
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,
piece: String,
}
impl Opened {
fn of(store: &Arc<MemoryStore>, ctx: &Context, account: &str) -> Opened {
Opened {
store: Arc::clone(store),
argument_line: ctx.argv_fallback(),
piece: account
.rfind('#')
.map_or_else(String::new, |at| account[at..].to_owned()),
}
}
fn key(&self, service: &str) -> String {
format!("{service}{}", self.piece)
}
}
impl RawStore for Opened {
fn kind(&self) -> Backend {
RawStore::kind(&self.store)
}
fn contains(&self, service: &str) -> Result<bool, Error> {
RawStore::contains(&self.store, &self.key(service))
}
fn read(&self, service: &str) -> Result<Option<String>, Error> {
RawStore::read(&self.store, &self.key(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(match self.kind() {
Backend::CredentialManager => format!(
"this item would hold {} bytes, past the {} a Credential Manager item holds",
cost.needs, cost.limit
),
_ => format!(
"this credential is {} bytes, past the {}-byte command limit",
cost.needs, cost.limit
),
}));
}
RawStore::write(&self.store, permit, &self.key(service), contents)
}
fn delete(&self, permit: Permit, service: &str) -> Result<(), Error> {
RawStore::delete(&self.store, permit, &self.key(service))
}
fn list(&self) -> Result<Option<Vec<String>>, Error> {
RawStore::list(&self.store)
}
fn cost(&self, service: &str, contents: &str) -> Option<Cost> {
if self.kind() == Backend::CredentialManager {
return Some(Cost {
needs: contents.len(),
limit: crate::provider::claude::pieces::BLOB,
second_route: false,
});
}
RawStore::cost(&self.store, &self.key(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>>,
policies: Mutex<HashMap<(super::Hive, String, String), Administered>>,
credentials: Mutex<Vec<String>>,
credentials_fault: Mutex<Option<Error>>,
user_environment_values: Mutex<HashMap<String, String>>,
machine_environment_values: Mutex<HashMap<String, String>>,
windows_settings: Mutex<WindowsSettings>,
kernel_is_wsl: AtomicBool,
windows_drives: Mutex<Vec<PathBuf>>,
}
impl Default for MemoryHost {
fn default() -> MemoryHost {
MemoryHost::parking_in(Backend::Keychain, true)
}
}
impl MemoryHost {
pub fn new() -> Arc<MemoryHost> {
Arc::new(MemoryHost::default())
}
pub fn sealed() -> Arc<MemoryHost> {
Arc::new(MemoryHost::parking_in(Backend::SealedFile, false))
}
#[cfg(test)]
pub(crate) fn scheduling_tasks(sid: &str) -> Arc<MemoryHost> {
let mut host = MemoryHost::default();
let pretend = super::task::Pretend {
refuse_start: Arc::clone(&host.refuse_start),
};
host.scheduler = Some(Box::new(super::task::Tasks::new(
sid.into(),
Arc::new(pretend),
)));
Arc::new(host)
}
fn parking_in(vault: Backend, shared: bool) -> MemoryHost {
let refuse_start = Arc::new(AtomicBool::new(false));
let trail = Arc::default();
let (live, gate) = match super::OS {
super::Os::MacOs | super::Os::Linux => (Backend::Keychain, HashMap::new()),
super::Os::Windows => (
Backend::CredentialManager,
HashMap::from([(FORCE.to_uppercase(), "1".to_owned())]),
),
};
MemoryHost {
keychain: MemoryStore::on_trail(live, "live".into(), Arc::clone(&trail)),
vault: MemoryStore::on_trail(vault, "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(shared),
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()),
policies: Mutex::new(HashMap::new()),
credentials: Mutex::new(Vec::new()),
credentials_fault: Mutex::new(None),
user_environment_values: Mutex::new(gate),
machine_environment_values: Mutex::new(HashMap::new()),
windows_settings: Mutex::new(WindowsSettings::default()),
kernel_is_wsl: AtomicBool::new(false),
windows_drives: Mutex::new(Vec::new()),
}
}
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]) {
self.runs_packaged(program, None, paths);
}
pub fn runs_in_package(&self, program: &str, family: &str, paths: &[&str]) {
self.runs_packaged(program, Some(family), paths);
}
fn runs_packaged(&self, program: &str, family: Option<&str>, paths: &[&str]) {
let processes = (1..)
.zip(paths)
.map(|(pid, path)| Process {
pid,
path: PathBuf::from(path),
package: family.map(str::to_owned),
})
.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);
}
#[cfg(test)]
pub(crate) fn sets_policy(&self, hive: super::Hive, key: &str, value: &str, text: &str) {
self.policies
.lock()
.expect("a poisoned test host is a failed test")
.insert(
(hive, key.to_string(), value.to_string()),
Administered::Set(text.into()),
);
}
#[cfg(test)]
pub(crate) fn policy_unreadable(&self, hive: super::Hive, key: &str, value: &str, why: &str) {
self.policies
.lock()
.expect("a poisoned test host is a failed test")
.insert(
(hive, key.to_string(), value.to_string()),
Administered::Unreadable(why.into()),
);
}
#[cfg(test)]
pub(crate) fn keeps_credentials(&self, names: &[&str]) {
locked(&self.credentials).extend(names.iter().map(|name| (*name).to_owned()));
}
#[cfg(test)]
pub(crate) fn credentials_unreadable(&self, why: &str) {
*locked(&self.credentials_fault) = Some(Error::Unreadable(why.into()));
}
#[cfg(test)]
pub(crate) fn credentials_unreachable(&self) {
*locked(&self.credentials_fault) = Some(Error::Locked);
}
#[cfg(test)]
pub(crate) fn sets_user_environment(&self, name: &str, value: &str) {
locked(&self.user_environment_values).insert(name.to_uppercase(), value.into());
}
#[cfg(test)]
pub(crate) fn forgets_user_environment(&self, name: &str) {
locked(&self.user_environment_values).remove(&name.to_uppercase());
}
#[cfg(test)]
pub(crate) fn sets_machine_environment(&self, name: &str, value: &str) {
locked(&self.machine_environment_values).insert(name.to_uppercase(), value.into());
}
#[cfg(test)]
pub(crate) fn sets_windows(&self, settings: WindowsSettings) {
*locked(&self.windows_settings) = settings;
}
#[cfg(test)]
pub(crate) fn runs_in_wsl(&self) {
self.kernel_is_wsl.store(true, Ordering::SeqCst);
}
#[cfg(test)]
pub(crate) fn mounts_a_windows_drive(&self, at: &Path) {
locked(&self.windows_drives).push(at.to_path_buf());
}
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::of(&self.keychain, ctx, account)))
}
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::of(&self.vault, ctx, ""))
}
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(),
package: None,
placed: true,
})
.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)
}
fn administered_folder(&self, dir: &Path) -> Vec<PathBuf> {
self.administered
.lock()
.expect("a poisoned test host is a failed test")
.keys()
.filter(|path| path.parent() == Some(dir))
.cloned()
.collect()
}
fn policy(&self, hive: super::Hive, key: &str, value: &str) -> Administered {
self.policies
.lock()
.expect("a poisoned test host is a failed test")
.get(&(hive, key.to_string(), value.to_string()))
.cloned()
.unwrap_or(Administered::Unset)
}
fn credential_names(&self, prefix: &str) -> Result<Vec<String>, String> {
let prefix = prefix.to_lowercase();
Ok(locked(&self.credentials)
.iter()
.filter(|name| name.to_lowercase().starts_with(&prefix))
.cloned()
.collect())
}
fn user_environment(&self, name: &str) -> Administered {
locked(&self.user_environment_values)
.get(&name.to_uppercase())
.map_or(Administered::Unset, |value| {
Administered::Set(value.clone())
})
}
fn machine_environment(&self, name: &str) -> Administered {
locked(&self.machine_environment_values)
.get(&name.to_uppercase())
.map_or(Administered::Unset, |value| {
Administered::Set(value.clone())
})
}
fn windows_settings(&self) -> WindowsSettings {
locked(&self.windows_settings).clone()
}
fn kernel_is_wsl(&self) -> bool {
self.kernel_is_wsl.load(Ordering::SeqCst)
}
fn on_a_windows_drive(&self, path: &Path) -> bool {
locked(&self.windows_drives)
.iter()
.any(|drive| path.starts_with(drive))
}
fn credential_held(&self, ctx: &Context, target: &str) -> Result<bool, Error> {
if let Some(fault) = locked(&self.credentials_fault).clone() {
return Err(fault);
}
let lower = target.to_lowercase();
if locked(&self.credentials)
.iter()
.any(|name| name.to_lowercase() == lower)
{
return Ok(true);
}
let Some((service, account)) = target.rsplit_once('/') else {
return Ok(false);
};
Opened::of(&self.keychain, ctx, account).contains(service)
}
}
#[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")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_credential_manager_item_takes_2560_bytes_and_each_piece_is_its_own() {
let store =
MemoryStore::on_trail(Backend::CredentialManager, "live".into(), Arc::default());
let ctx = Context::for_unit_test();
let (whole, piece) = (
Opened::of(&store, &ctx, "claude-code-user"),
Opened::of(&store, &ctx, "claude-code-user#0"),
);
let permit = Permit::for_a_test();
whole
.write(permit, "svc", &"x".repeat(2560))
.expect("it fits");
assert!(matches!(
whole.write(permit, "svc", &"x".repeat(2561)),
Err(Error::Write(_))
));
piece.write(permit, "svc", "a piece").expect("written");
assert_eq!(store.peek("svc").map(|held| held.len()), Some(2560));
assert_eq!(store.peek("svc#0").as_deref(), Some("a piece"));
assert_eq!(whole.cost("svc", "x").map(|cost| cost.limit), Some(2560));
}
}