#[cfg(not(target_arch = "wasm32"))]
use std::env;
#[cfg(not(target_arch = "wasm32"))]
use std::collections::HashMap;
#[cfg(not(target_arch = "wasm32"))]
use std::sync::{OnceLock, RwLock};
#[cfg(target_arch = "wasm32")]
use {wasm_bindgen::prelude::*, web_sys::window};
#[cfg(not(target_arch = "wasm32"))]
static ENV_OVERRIDES: OnceLock<RwLock<HashMap<String, String>>> = OnceLock::new();
#[cfg(not(target_arch = "wasm32"))]
fn get_overrides() -> &'static RwLock<HashMap<String, String>> {
ENV_OVERRIDES.get_or_init(|| RwLock::new(HashMap::new()))
}
#[derive(Debug)]
pub enum EnvError {
NotFound(String),
Empty(String),
#[cfg(target_arch = "wasm32")]
WasmError(String),
}
impl std::fmt::Display for EnvError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EnvError::NotFound(key) => {
let hint = match key.as_str() {
"JACS_PRIVATE_KEY_PASSWORD" => {
" Set this to the password used to encrypt your private key."
}
"JACS_KEY_DIRECTORY" => {
" Set this to the directory containing your key files (e.g., './keys')."
}
"JACS_DATA_DIRECTORY" => {
" Set this to the directory for JACS data files (e.g., './data')."
}
"JACS_AGENT_PRIVATE_KEY_FILENAME" => {
" Set this to your private key filename (e.g., 'agent.private.pem.enc')."
}
"JACS_AGENT_PUBLIC_KEY_FILENAME" => {
" Set this to your public key filename (e.g., 'agent.public.pem')."
}
"JACS_AGENT_KEY_ALGORITHM" => {
" Set this to your key algorithm (e.g., 'ring-Ed25519', 'RSA-PSS', 'pq-dilithium')."
}
_ => "",
};
write!(
f,
"Required environment variable '{}' is not set.{}",
key, hint
)
}
EnvError::Empty(key) => {
write!(
f,
"Environment variable '{}' is set but empty. Please provide a non-empty value.",
key
)
}
#[cfg(target_arch = "wasm32")]
EnvError::WasmError(msg) => write!(f, "WASM environment error: {}", msg),
}
}
}
impl std::error::Error for EnvError {}
#[cfg(target_arch = "wasm32")]
fn get_local_storage() -> Result<web_sys::Storage, EnvError> {
window()
.ok_or_else(|| EnvError::WasmError("No global window exists".to_string()))?
.local_storage()
.map_err(|e| EnvError::WasmError(e.as_string().unwrap_or_default()))?
.ok_or_else(|| EnvError::WasmError("localStorage is not available".to_string()))
}
pub fn get_env_var(key: &str, required_non_empty: bool) -> Result<Option<String>, EnvError> {
#[cfg(not(target_arch = "wasm32"))]
{
if let Ok(overrides) = get_overrides().read()
&& let Some(value) = overrides.get(key)
{
if required_non_empty && value.trim().is_empty() {
return Err(EnvError::Empty(key.to_string()));
}
return Ok(Some(value.clone()));
}
match env::var(key) {
Ok(value) => {
if required_non_empty && value.trim().is_empty() {
Err(EnvError::Empty(key.to_string()))
} else {
Ok(Some(value))
}
}
Err(_) => Ok(None),
}
}
#[cfg(target_arch = "wasm32")]
{
match get_local_storage()?
.get_item(key)
.map_err(|e| EnvError::WasmError(e.as_string().unwrap_or_default()))?
{
Some(value) => {
if required_non_empty && value.trim().is_empty() {
Err(EnvError::Empty(key.to_string()))
} else {
Ok(Some(value))
}
}
None => Ok(None),
}
}
}
pub fn get_required_env_var(key: &str, required_non_empty: bool) -> Result<String, EnvError> {
match get_env_var(key, required_non_empty)? {
Some(value) => Ok(value),
None => Err(EnvError::NotFound(key.to_string())),
}
}
pub fn set_env_var_override(key: &str, value: &str, do_override: bool) -> Result<(), EnvError> {
if get_env_var(key, false)?.is_none() || do_override {
set_env_var(key, value)
} else {
Ok(())
}
}
#[cfg(target_arch = "wasm32")]
pub fn set_env_var(key: &str, value: &str) -> Result<(), EnvError> {
get_local_storage()?
.set_item(key, value)
.map_err(|e| EnvError::WasmError(e.as_string().unwrap_or_default()))
}
#[cfg(not(target_arch = "wasm32"))]
pub fn set_env_var(key: &str, value: &str) -> Result<(), EnvError> {
if let Ok(mut overrides) = get_overrides().write() {
overrides.insert(key.to_string(), value.to_string());
Ok(())
} else {
Ok(())
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn clear_env_var(key: &str) -> Result<(), EnvError> {
if let Ok(mut overrides) = get_overrides().write() {
overrides.remove(key);
}
Ok(())
}
#[cfg(test)]
#[cfg(not(target_arch = "wasm32"))]
mod tests {
use super::*;
use std::thread;
#[test]
fn test_set_and_get_env_var() {
let key = "JACS_TEST_SET_GET";
set_env_var(key, "test_value").unwrap();
let result = get_env_var(key, false).unwrap();
assert_eq!(result, Some("test_value".to_string()));
clear_env_var(key).unwrap();
}
#[test]
fn test_override_takes_precedence() {
let key = "JACS_TEST_OVERRIDE";
set_env_var(key, "override_value").unwrap();
let result = get_env_var(key, false).unwrap();
assert_eq!(result, Some("override_value".to_string()));
clear_env_var(key).unwrap();
}
#[test]
fn test_required_env_var_not_found() {
let key = "JACS_TEST_NOT_EXISTS_12345";
let result = get_required_env_var(key, false);
assert!(result.is_err());
match result {
Err(EnvError::NotFound(k)) => assert_eq!(k, key),
_ => panic!("Expected NotFound error"),
}
}
#[test]
fn test_empty_value_with_required_non_empty() {
let key = "JACS_TEST_EMPTY";
set_env_var(key, " ").unwrap();
let result = get_env_var(key, true);
assert!(result.is_err());
match result {
Err(EnvError::Empty(k)) => assert_eq!(k, key),
_ => panic!("Expected Empty error"),
}
clear_env_var(key).unwrap();
}
#[test]
fn test_concurrent_access() {
let handles: Vec<_> = (0..10)
.map(|i| {
thread::spawn(move || {
let key = format!("JACS_TEST_CONCURRENT_{}", i);
for j in 0..100 {
set_env_var(&key, &format!("value_{}", j)).unwrap();
let _ = get_env_var(&key, false);
}
clear_env_var(&key).unwrap();
})
})
.collect();
for handle in handles {
handle.join().expect("Thread panicked");
}
}
#[test]
fn test_clear_env_var() {
let key = "JACS_TEST_CLEAR";
set_env_var(key, "to_be_cleared").unwrap();
assert_eq!(
get_env_var(key, false).unwrap(),
Some("to_be_cleared".to_string())
);
clear_env_var(key).unwrap();
let _ = get_env_var(key, false);
}
}