use pf_core::cas::BlobStore;
use pf_core::digest::Digest256;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::sync::Arc;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct EnvSnapshot {
pub kind: String,
pub cwd: String,
pub vars: BTreeMap<String, String>,
}
pub struct EnvCapture {
scrub: Vec<Regex>,
}
impl EnvCapture {
#[must_use]
pub fn new() -> Self {
Self { scrub: Vec::new() }
}
pub fn scrub(mut self, pattern: &str) -> Result<Self, regex::Error> {
self.scrub.push(Regex::new(pattern)?);
Ok(self)
}
pub fn capture(&self, blobs: &Arc<dyn BlobStore>) -> pf_core::Result<Digest256> {
let cwd = match std::env::current_dir() {
Ok(p) => p.to_string_lossy().into_owned(),
Err(_) => String::new(),
};
let mut vars = BTreeMap::new();
for (k, v) in std::env::vars() {
let redacted = self.scrub.iter().any(|re| re.is_match(&k));
vars.insert(k, if redacted { "<redacted>".into() } else { v });
}
let snap = EnvSnapshot {
kind: "env.v1".into(),
cwd,
vars,
};
blobs.put(&serde_json::to_vec(&snap)?)
}
}
impl Default for EnvCapture {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[allow(unsafe_code)] mod tests {
use super::*;
use pf_core::cas::MemBlobStore;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
#[test]
fn captures_vars_and_cwd() {
let _g = ENV_LOCK.lock().unwrap();
unsafe {
std::env::set_var("PF_TEST_VAR_PLAIN", "value123");
}
let blobs: Arc<dyn BlobStore> = Arc::new(MemBlobStore::new());
let cid = EnvCapture::new().capture(&blobs).unwrap();
let snap: EnvSnapshot = serde_json::from_slice(&blobs.get(&cid).unwrap()).unwrap();
assert_eq!(snap.kind, "env.v1");
assert!(!snap.cwd.is_empty());
assert_eq!(snap.vars.get("PF_TEST_VAR_PLAIN").unwrap(), "value123");
}
#[test]
fn scrub_redacts_matching_keys() {
let _g = ENV_LOCK.lock().unwrap();
unsafe {
std::env::set_var("PF_TEST_SECRET_TOKEN", "do-not-leak");
std::env::set_var("PF_TEST_PUBLIC_INFO", "ok-to-share");
}
let blobs: Arc<dyn BlobStore> = Arc::new(MemBlobStore::new());
let cap = EnvCapture::new().scrub("(?i)secret|token").unwrap();
let cid = cap.capture(&blobs).unwrap();
let snap: EnvSnapshot = serde_json::from_slice(&blobs.get(&cid).unwrap()).unwrap();
assert_eq!(snap.vars.get("PF_TEST_SECRET_TOKEN").unwrap(), "<redacted>");
assert_eq!(snap.vars.get("PF_TEST_PUBLIC_INFO").unwrap(), "ok-to-share");
}
#[test]
fn vars_are_sorted_so_digest_is_deterministic() {
let _g = ENV_LOCK.lock().unwrap();
let blobs: Arc<dyn BlobStore> = Arc::new(MemBlobStore::new());
let c1 = EnvCapture::new().capture(&blobs).unwrap();
let c2 = EnvCapture::new().capture(&blobs).unwrap();
assert_eq!(c1, c2);
}
}