use crate::observability::LOG_TARGET;
use std::cell::RefCell;
use std::ffi::OsString;
use std::marker::PhantomData;
use std::rc::Rc;
use std::thread_local;
use tracing::{info, info_span};
#[cfg(all(test, feature = "loom-tests"))]
mod loom_tests;
mod state;
mod summary;
#[cfg(test)]
mod tests;
use state::ThreadState;
use summary::{MAX_ENV_CHANGES_SUMMARY_LEN, truncate_env_changes_summary};
thread_local! {
static THREAD_STATE: RefCell<ThreadState> = const { RefCell::new(ThreadState::new()) };
}
type EnterScopeFn = fn(Vec<(OsString, Option<OsString>)>) -> usize;
type ExitScopeFn = fn(usize);
fn with_state<F, R>(f: F) -> R
where
F: FnOnce(&mut ThreadState) -> R,
{
THREAD_STATE.with(|cell| {
let mut state = cell.borrow_mut();
f(&mut state)
})
}
fn enter_scope_std(vars: Vec<(OsString, Option<OsString>)>) -> usize {
with_state(|state| state.enter_scope(vars))
}
fn exit_scope_std(index: usize) {
with_state(|state| state.exit_scope(index));
}
#[derive(Debug)]
#[must_use = "Hold the guard until the end of the environment scope"]
pub struct ScopedEnv {
index: usize,
span: tracing::Span,
change_count: usize,
exit_scope: ExitScopeFn,
_not_send_or_sync: PhantomData<Rc<()>>,
}
impl ScopedEnv {
pub(crate) fn apply(vars: &[(String, Option<String>)]) -> Self {
let owned: Vec<(OsString, Option<OsString>)> = vars
.iter()
.map(|(key, value)| {
debug_assert!(
!key.is_empty() && !key.contains('='),
"invalid env var name"
);
let owned_value = value.as_ref().map(OsString::from);
(OsString::from(key), owned_value)
})
.collect();
Self::apply_owned_with_state(owned, enter_scope_std, exit_scope_std)
}
pub(crate) fn apply_os<I>(vars: I) -> Self
where
I: IntoIterator<Item = (OsString, Option<OsString>)>,
{
let owned: Vec<(OsString, Option<OsString>)> = vars.into_iter().collect();
Self::apply_owned_with_state(owned, enter_scope_std, exit_scope_std)
}
#[expect(
clippy::cognitive_complexity,
reason = "span entry plus bounded summary handling add structured branching"
)]
fn apply_owned_with_state(
vars: Vec<(OsString, Option<OsString>)>,
enter_scope: EnterScopeFn,
exit_scope: ExitScopeFn,
) -> Self {
let summary: Vec<String> = vars
.iter()
.map(|(key, value)| {
let status = if value.is_some() { "set" } else { "unset" };
format!("{}={status}", key.to_string_lossy())
})
.collect();
let change_count = summary.len();
let changes = summary.join(", ");
let truncated_changes =
truncate_env_changes_summary(&changes, MAX_ENV_CHANGES_SUMMARY_LEN, change_count);
let span = info_span!(
target: LOG_TARGET,
"scoped_env",
change_count,
changes = %truncated_changes
);
let index = {
let _entered = span.enter();
let index = enter_scope(vars);
info!(
target: LOG_TARGET,
change_count,
changes = %truncated_changes,
"applied scoped environment variables"
);
index
};
Self {
index,
span,
change_count,
exit_scope,
_not_send_or_sync: PhantomData,
}
}
}
impl Drop for ScopedEnv {
fn drop(&mut self) {
let _entered = self.span.enter();
info!(
target: LOG_TARGET,
change_count = self.change_count,
"restoring scoped environment variables"
);
(self.exit_scope)(self.index);
}
}