use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use super::config::EnvCacheConfig;
use super::entry::CacheEntry;
use super::maintenance;
use super::stats::EnvCacheStats;
use super::validation;
pub struct EnvironmentCache {
cache: Arc<RwLock<HashMap<String, CacheEntry>>>,
config: EnvCacheConfig,
stats: Arc<RwLock<EnvCacheStats>>,
safe_variables: std::collections::HashSet<&'static str>,
}
impl EnvironmentCache {
pub fn new() -> Self {
Self::with_config(EnvCacheConfig::default())
}
pub fn with_config(config: EnvCacheConfig) -> Self {
let stats = EnvCacheStats {
max_entries: config.max_entries,
..Default::default()
};
let safe_variables = validation::create_safe_variables();
Self {
cache: Arc::new(RwLock::new(HashMap::new())),
config,
stats: Arc::new(RwLock::new(stats)),
safe_variables,
}
}
pub fn get_env_var(&self, var_name: &str) -> Result<Option<String>, anyhow::Error> {
if !self.config.enabled {
return if self.safe_variables.contains(var_name) {
Ok(std::env::var(var_name).ok())
} else {
tracing::warn!(
"Blocked access to non-whitelisted environment variable '{}' (cache disabled)",
var_name
);
Ok(None)
};
}
if !self.safe_variables.contains(var_name) {
tracing::warn!(
"Blocked access to non-whitelisted environment variable '{}'",
var_name
);
return Ok(None);
}
if let Some(value) = self.try_get_cached(var_name)? {
return Ok(value);
}
let value = std::env::var(var_name).ok();
self.put(var_name.to_string(), value.clone());
{
let mut stats = self.stats.write().unwrap();
stats.misses += 1;
}
tracing::trace!("Environment variable cache miss: {}", var_name);
Ok(value)
}
fn try_get_cached(&self, var_name: &str) -> Result<Option<Option<String>>, anyhow::Error> {
let mut cache = self.cache.write().unwrap();
if let Some(entry) = cache.get_mut(var_name) {
if entry.is_expired(self.config.ttl) {
tracing::trace!("Environment variable cache entry expired: {}", var_name);
cache.remove(var_name);
let mut stats = self.stats.write().unwrap();
stats.ttl_evictions += 1;
return Ok(None);
}
let value = entry.access().clone();
{
let mut stats = self.stats.write().unwrap();
stats.hits += 1;
}
tracing::trace!("Environment variable cache hit: {}", var_name);
return Ok(Some(value));
}
Ok(None)
}
fn put(&self, var_name: String, value: Option<String>) {
let mut cache = self.cache.write().unwrap();
if cache.len() >= self.config.max_entries {
if let Some(oldest_key) = cache
.iter()
.min_by_key(|(_, entry)| entry.cached_at())
.map(|(k, _)| k.clone())
{
cache.remove(&oldest_key);
tracing::debug!(
"Evicted environment variable from cache due to size limit: {}",
oldest_key
);
}
}
let entry = CacheEntry::new(value);
cache.insert(var_name.clone(), entry);
{
let mut stats = self.stats.write().unwrap();
stats.current_entries = cache.len();
}
tracing::trace!("Environment variable cached: {}", var_name);
}
#[allow(dead_code)]
pub fn clear(&self) {
maintenance::clear_cache(&self.cache, &self.stats);
}
#[allow(dead_code)]
pub fn remove(&self, var_name: &str) -> Option<String> {
maintenance::remove_entry(&self.cache, &self.stats, var_name)
}
#[allow(dead_code)]
pub fn stats(&self) -> EnvCacheStats {
self.stats.read().unwrap().clone()
}
#[allow(dead_code)]
pub fn config(&self) -> &EnvCacheConfig {
&self.config
}
#[allow(dead_code)]
pub fn maintain(&self) -> usize {
maintenance::maintain_cache(
&self.cache,
&self.stats,
self.config.ttl,
self.config.enabled,
)
}
#[allow(dead_code)]
pub fn refresh(&self) {
self.clear();
tracing::debug!("Environment variable cache refreshed");
}
#[allow(dead_code)]
pub fn debug_info(&self) -> HashMap<String, String> {
maintenance::get_debug_info(&self.cache, self.config.ttl)
}
#[allow(dead_code)]
pub fn is_safe_variable(&self, var_name: &str) -> bool {
self.safe_variables.contains(var_name)
}
#[allow(dead_code)]
pub fn safe_variables(&self) -> Vec<&'static str> {
self.safe_variables.iter().copied().collect()
}
}
impl Default for EnvironmentCache {
fn default() -> Self {
Self::new()
}
}