pub mod backend;
pub mod memory;
pub mod native;
use std::collections::HashMap;
use std::sync::{Mutex, OnceLock};
pub use backend::EnvironmentBackend;
type Entry = (String, String);
struct EnvState {
entries: Vec<Entry>,
index: HashMap<String, usize>,
}
impl EnvState {
fn from_entries(entries: Vec<Entry>) -> Self {
let index = entries
.iter()
.enumerate()
.map(|(i, (name, _))| (normalize_key(name), i))
.collect();
Self { entries, index }
}
fn set(&mut self, name: &str, value: &str) {
let key = normalize_key(name);
match self.index.get(&key) {
Some(&i) => self.entries[i].1 = value.to_string(),
None => {
self.index.insert(key, self.entries.len());
self.entries.push((name.to_string(), value.to_string()));
}
}
}
fn get(&self, name: &str) -> Option<&str> {
let key = normalize_key(name);
self.index.get(&key).map(|&i| self.entries[i].1.as_str())
}
fn at(&self, position: usize) -> Option<String> {
self.entries
.get(position - 1)
.map(|(name, value)| format!("{name}={value}"))
}
}
fn normalize_key(name: &str) -> String {
name.to_ascii_lowercase()
}
static BACKEND: OnceLock<Mutex<Box<dyn EnvironmentBackend>>> = OnceLock::new();
static ENV: OnceLock<Mutex<EnvState>> = OnceLock::new();
fn backend() -> &'static Mutex<Box<dyn EnvironmentBackend>> {
BACKEND.get_or_init(|| Mutex::new(default_backend()))
}
fn default_backend() -> Box<dyn EnvironmentBackend> {
Box::new(native::NativeBackend::new())
}
fn snapshot() -> &'static Mutex<EnvState> {
ENV.get_or_init(|| {
let entries = backend()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.load();
Mutex::new(EnvState::from_entries(entries))
})
}
fn lock() -> std::sync::MutexGuard<'static, EnvState> {
snapshot()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
pub fn set_backend(new_backend: Box<dyn EnvironmentBackend>) {
let entries = new_backend.load();
*backend()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = new_backend;
*lock() = EnvState::from_entries(entries);
}
pub fn reset_backend() {
set_backend(default_backend());
}
pub fn reset() {
let entries = backend()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.load();
*lock() = EnvState::from_entries(entries);
}
pub fn set_env(name: &str, value: &str) {
lock().set(name, value);
}
pub fn remove_env(name: &str) {
let mut state = lock();
let key = normalize_key(name);
if let Some(&i) = state.index.get(&key) {
state.index.remove(&key);
state.entries.remove(i);
for position in state.index.values_mut() {
if *position > i {
*position -= 1;
}
}
}
}
pub fn get_env(name: &str) -> Option<String> {
lock().get(name).map(str::to_string)
}
pub fn env_at(position: usize) -> Option<String> {
lock().at(position)
}
pub fn entries() -> Vec<(String, String)> {
lock().entries.clone()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::state::test_support::{position_of, TEST_LOCK};
#[test]
fn set_env_overwrites_in_place() {
let _guard = TEST_LOCK.lock().unwrap();
reset();
set_env("VB6RUNTIME_TEST_ONE", "first");
let position = position_of("VB6RUNTIME_TEST_ONE");
set_env("vb6runtime_test_one", "second");
assert_eq!(get_env("VB6RUNTIME_TEST_ONE").as_deref(), Some("second"));
assert_eq!(
env_at(position).as_deref(),
Some("VB6RUNTIME_TEST_ONE=second")
);
assert_eq!(env_at(position + 1), None);
}
#[test]
fn get_env_is_case_insensitive() {
let _guard = TEST_LOCK.lock().unwrap();
reset();
set_env("VB6RUNTIME_TEST_CASE", "value");
assert_eq!(get_env("vb6runtIme_test_case").as_deref(), Some("value"));
assert_eq!(get_env("VB6RUNTIME_TEST_MISSING"), None);
}
#[test]
fn env_at_returns_entries_in_order_and_reports_ends() {
let _guard = TEST_LOCK.lock().unwrap();
reset();
set_env("VB6RUNTIME_TEST_A", "1");
set_env("VB6RUNTIME_TEST_B", "2");
set_env("VB6RUNTIME_TEST_C", "3");
let first = position_of("VB6RUNTIME_TEST_A");
assert_eq!(env_at(first).as_deref(), Some("VB6RUNTIME_TEST_A=1"));
assert_eq!(env_at(first + 1).as_deref(), Some("VB6RUNTIME_TEST_B=2"));
assert_eq!(env_at(first + 2).as_deref(), Some("VB6RUNTIME_TEST_C=3"));
assert_eq!(env_at(first + 3), None);
}
#[test]
fn reset_restores_the_process_environment() {
let _guard = TEST_LOCK.lock().unwrap();
set_env("VB6RUNTIME_TEST_RESET", "gone");
reset();
assert_eq!(get_env("VB6RUNTIME_TEST_RESET"), None);
}
#[test]
fn remove_env_deletes_and_compacts_positions() {
let _guard = TEST_LOCK.lock().unwrap();
reset();
set_env("VB6RUNTIME_TEST_A", "1");
set_env("VB6RUNTIME_TEST_B", "2");
let a = position_of("VB6RUNTIME_TEST_A");
remove_env("vb6runtime_test_b");
assert_eq!(get_env("VB6RUNTIME_TEST_B"), None);
assert_eq!(get_env("VB6RUNTIME_TEST_A").as_deref(), Some("1"));
assert_eq!(env_at(a + 1), None);
}
}