1use pf_core::cas::BlobStore;
5use pf_core::digest::Digest256;
6use regex::Regex;
7use serde::{Deserialize, Serialize};
8use std::collections::BTreeMap;
9use std::sync::Arc;
10
11#[derive(Clone, Debug, Serialize, Deserialize)]
13pub struct EnvSnapshot {
14 pub kind: String,
16 pub cwd: String,
18 pub vars: BTreeMap<String, String>,
21}
22
23pub struct EnvCapture {
25 scrub: Vec<Regex>,
26}
27
28impl EnvCapture {
29 #[must_use]
31 pub fn new() -> Self {
32 Self { scrub: Vec::new() }
33 }
34
35 pub fn scrub(mut self, pattern: &str) -> Result<Self, regex::Error> {
41 self.scrub.push(Regex::new(pattern)?);
42 Ok(self)
43 }
44
45 pub fn capture(&self, blobs: &Arc<dyn BlobStore>) -> pf_core::Result<Digest256> {
47 let cwd = match std::env::current_dir() {
48 Ok(p) => p.to_string_lossy().into_owned(),
49 Err(_) => String::new(),
50 };
51 let mut vars = BTreeMap::new();
52 for (k, v) in std::env::vars() {
53 let redacted = self.scrub.iter().any(|re| re.is_match(&k));
54 vars.insert(k, if redacted { "<redacted>".into() } else { v });
55 }
56 let snap = EnvSnapshot {
57 kind: "env.v1".into(),
58 cwd,
59 vars,
60 };
61 blobs.put(&serde_json::to_vec(&snap)?)
62 }
63}
64
65impl Default for EnvCapture {
66 fn default() -> Self {
67 Self::new()
68 }
69}
70
71#[cfg(test)]
72#[allow(unsafe_code)] mod tests {
74 use super::*;
75 use pf_core::cas::MemBlobStore;
76 use std::sync::Mutex;
80 static ENV_LOCK: Mutex<()> = Mutex::new(());
81
82 #[test]
83 fn captures_vars_and_cwd() {
84 let _g = ENV_LOCK.lock().unwrap();
85 unsafe {
87 std::env::set_var("PF_TEST_VAR_PLAIN", "value123");
88 }
89 let blobs: Arc<dyn BlobStore> = Arc::new(MemBlobStore::new());
90 let cid = EnvCapture::new().capture(&blobs).unwrap();
91 let snap: EnvSnapshot = serde_json::from_slice(&blobs.get(&cid).unwrap()).unwrap();
92 assert_eq!(snap.kind, "env.v1");
93 assert!(!snap.cwd.is_empty());
94 assert_eq!(snap.vars.get("PF_TEST_VAR_PLAIN").unwrap(), "value123");
95 }
96
97 #[test]
98 fn scrub_redacts_matching_keys() {
99 let _g = ENV_LOCK.lock().unwrap();
100 unsafe {
102 std::env::set_var("PF_TEST_SECRET_TOKEN", "do-not-leak");
103 std::env::set_var("PF_TEST_PUBLIC_INFO", "ok-to-share");
104 }
105 let blobs: Arc<dyn BlobStore> = Arc::new(MemBlobStore::new());
106 let cap = EnvCapture::new().scrub("(?i)secret|token").unwrap();
107 let cid = cap.capture(&blobs).unwrap();
108 let snap: EnvSnapshot = serde_json::from_slice(&blobs.get(&cid).unwrap()).unwrap();
109 assert_eq!(snap.vars.get("PF_TEST_SECRET_TOKEN").unwrap(), "<redacted>");
110 assert_eq!(snap.vars.get("PF_TEST_PUBLIC_INFO").unwrap(), "ok-to-share");
111 }
112
113 #[test]
114 fn vars_are_sorted_so_digest_is_deterministic() {
115 let _g = ENV_LOCK.lock().unwrap();
116 let blobs: Arc<dyn BlobStore> = Arc::new(MemBlobStore::new());
117 let c1 = EnvCapture::new().capture(&blobs).unwrap();
118 let c2 = EnvCapture::new().capture(&blobs).unwrap();
119 assert_eq!(c1, c2);
121 }
122}