1use serde::{Deserialize, Serialize};
15use std::path::PathBuf;
16use std::time::Duration;
17
18use crate::cdp_protection::CdpFixMode;
19
20#[cfg(feature = "stealth")]
21use crate::webrtc::WebRtcConfig;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
48#[serde(rename_all = "lowercase")]
49pub enum HeadlessMode {
50 #[default]
53 New,
54 Legacy,
56}
57
58impl HeadlessMode {
59 pub fn from_env() -> Self {
61 match std::env::var("STYGIAN_HEADLESS_MODE")
62 .unwrap_or_default()
63 .to_lowercase()
64 .as_str()
65 {
66 "legacy" => Self::Legacy,
67 _ => Self::New,
68 }
69 }
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
87#[serde(rename_all = "lowercase")]
88pub enum StealthLevel {
89 None,
91 Basic,
93 #[default]
95 Advanced,
96}
97
98impl StealthLevel {
99 #[must_use]
101 pub fn is_active(self) -> bool {
102 self != Self::None
103 }
104
105 pub fn from_env() -> Self {
107 match std::env::var("STYGIAN_STEALTH_LEVEL")
108 .unwrap_or_default()
109 .to_lowercase()
110 .as_str()
111 {
112 "none" => Self::None,
113 "basic" => Self::Basic,
114 _ => Self::Advanced,
115 }
116 }
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct PoolConfig {
133 pub min_size: usize,
137
138 pub max_size: usize,
142
143 #[serde(with = "duration_secs")]
147 pub idle_timeout: Duration,
148
149 #[serde(with = "duration_secs")]
154 pub acquire_timeout: Duration,
155}
156
157impl Default for PoolConfig {
158 fn default() -> Self {
159 Self {
160 min_size: env_usize("STYGIAN_POOL_MIN", 2),
161 max_size: env_usize("STYGIAN_POOL_MAX", 10),
162 idle_timeout: Duration::from_secs(env_u64("STYGIAN_POOL_IDLE_SECS", 300)),
163 acquire_timeout: Duration::from_secs(env_u64("STYGIAN_POOL_ACQUIRE_SECS", 5)),
164 }
165 }
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct BrowserConfig {
186 pub chrome_path: Option<PathBuf>,
190
191 pub args: Vec<String>,
193
194 pub headless: bool,
198
199 pub user_data_dir: Option<PathBuf>,
201
202 pub headless_mode: HeadlessMode,
208
209 pub window_size: Option<(u32, u32)>,
211
212 pub devtools: bool,
214
215 pub proxy: Option<String>,
217
218 pub proxy_bypass_list: Option<String>,
222
223 #[cfg(feature = "stealth")]
227 pub webrtc: WebRtcConfig,
228
229 pub stealth_level: StealthLevel,
231
232 pub disable_sandbox: bool,
244
245 pub cdp_fix_mode: CdpFixMode,
249
250 pub source_url: Option<String>,
257
258 pub pool: PoolConfig,
260
261 #[serde(with = "duration_secs")]
265 pub launch_timeout: Duration,
266
267 #[serde(with = "duration_secs")]
271 pub cdp_timeout: Duration,
272}
273
274impl Default for BrowserConfig {
275 fn default() -> Self {
276 Self {
277 chrome_path: std::env::var("STYGIAN_CHROME_PATH").ok().map(PathBuf::from),
278 args: vec![],
279 headless: env_bool("STYGIAN_HEADLESS", true),
280 user_data_dir: None,
281 headless_mode: HeadlessMode::from_env(),
282 window_size: Some((1920, 1080)),
283 devtools: false,
284 proxy: std::env::var("STYGIAN_PROXY").ok(),
285 proxy_bypass_list: std::env::var("STYGIAN_PROXY_BYPASS").ok(),
286 #[cfg(feature = "stealth")]
287 webrtc: WebRtcConfig::default(),
288 disable_sandbox: env_bool("STYGIAN_DISABLE_SANDBOX", is_containerized()),
289 stealth_level: StealthLevel::from_env(),
290 cdp_fix_mode: CdpFixMode::from_env(),
291 source_url: std::env::var("STYGIAN_SOURCE_URL").ok(),
292 pool: PoolConfig::default(),
293 launch_timeout: Duration::from_secs(env_u64("STYGIAN_LAUNCH_TIMEOUT_SECS", 10)),
294 cdp_timeout: Duration::from_secs(env_u64("STYGIAN_CDP_TIMEOUT_SECS", 30)),
295 }
296 }
297}
298
299impl BrowserConfig {
300 pub fn builder() -> BrowserConfigBuilder {
302 BrowserConfigBuilder {
303 config: Self::default(),
304 }
305 }
306
307 pub fn effective_args(&self) -> Vec<String> {
312 let mut args = vec![
313 "--disable-blink-features=AutomationControlled".to_string(),
314 "--disable-dev-shm-usage".to_string(),
315 "--disable-infobars".to_string(),
316 "--disable-background-timer-throttling".to_string(),
317 "--disable-backgrounding-occluded-windows".to_string(),
318 "--disable-renderer-backgrounding".to_string(),
319 ];
320
321 if self.disable_sandbox {
322 args.push("--no-sandbox".to_string());
323 }
324
325 if let Some(proxy) = &self.proxy {
326 args.push(format!("--proxy-server={proxy}"));
327 }
328
329 if let Some(bypass) = &self.proxy_bypass_list {
330 args.push(format!("--proxy-bypass-list={bypass}"));
331 }
332
333 #[cfg(feature = "stealth")]
334 args.extend(self.webrtc.chrome_args());
335
336 if let Some((w, h)) = self.window_size {
337 args.push(format!("--window-size={w},{h}"));
338 }
339
340 args.extend_from_slice(&self.args);
341 args
342 }
343
344 pub fn validate(&self) -> Result<(), Vec<String>> {
362 let mut errors: Vec<String> = Vec::new();
363
364 if self.pool.min_size > self.pool.max_size {
365 errors.push(format!(
366 "pool.min_size ({}) must be <= pool.max_size ({})",
367 self.pool.min_size, self.pool.max_size
368 ));
369 }
370 if self.pool.max_size == 0 {
371 errors.push("pool.max_size must be >= 1".to_string());
372 }
373 if self.launch_timeout.is_zero() {
374 errors.push("launch_timeout must be positive".to_string());
375 }
376 if self.cdp_timeout.is_zero() {
377 errors.push("cdp_timeout must be positive".to_string());
378 }
379 if let Some(proxy) = &self.proxy
380 && !proxy.starts_with("http://")
381 && !proxy.starts_with("https://")
382 && !proxy.starts_with("socks4://")
383 && !proxy.starts_with("socks5://")
384 {
385 errors.push(format!(
386 "proxy URL must start with http://, https://, socks4:// or socks5://; got: {proxy}"
387 ));
388 }
389
390 if errors.is_empty() {
391 Ok(())
392 } else {
393 Err(errors)
394 }
395 }
396
397 pub fn to_json(&self) -> Result<String, serde_json::Error> {
412 serde_json::to_string_pretty(self)
413 }
414
415 pub fn from_json_str(s: &str) -> Result<Self, serde_json::Error> {
436 serde_json::from_str(s)
437 }
438
439 pub fn from_json_file(path: impl AsRef<std::path::Path>) -> crate::error::Result<Self> {
453 use crate::error::BrowserError;
454 let content = std::fs::read_to_string(path.as_ref()).map_err(|e| {
455 BrowserError::ConfigError(format!(
456 "cannot read config file {}: {e}",
457 path.as_ref().display()
458 ))
459 })?;
460 serde_json::from_str(&content).map_err(|e| {
461 BrowserError::ConfigError(format!(
462 "invalid JSON in config file {}: {e}",
463 path.as_ref().display()
464 ))
465 })
466 }
467}
468
469pub struct BrowserConfigBuilder {
473 config: BrowserConfig,
474}
475
476impl BrowserConfigBuilder {
477 #[must_use]
479 pub fn chrome_path(mut self, path: PathBuf) -> Self {
480 self.config.chrome_path = Some(path);
481 self
482 }
483
484 #[must_use]
500 pub fn user_data_dir(mut self, path: impl Into<std::path::PathBuf>) -> Self {
501 self.config.user_data_dir = Some(path.into());
502 self
503 }
504
505 #[must_use]
507 pub const fn headless(mut self, headless: bool) -> Self {
508 self.config.headless = headless;
509 self
510 }
511
512 #[must_use]
528 pub const fn headless_mode(mut self, mode: HeadlessMode) -> Self {
529 self.config.headless_mode = mode;
530 self
531 }
532
533 #[must_use]
535 pub const fn window_size(mut self, width: u32, height: u32) -> Self {
536 self.config.window_size = Some((width, height));
537 self
538 }
539
540 #[must_use]
542 pub const fn devtools(mut self, enabled: bool) -> Self {
543 self.config.devtools = enabled;
544 self
545 }
546
547 #[must_use]
549 pub fn proxy(mut self, proxy: String) -> Self {
550 self.config.proxy = Some(proxy);
551 self
552 }
553
554 #[must_use]
566 pub fn proxy_bypass_list(mut self, bypass: String) -> Self {
567 self.config.proxy_bypass_list = Some(bypass);
568 self
569 }
570
571 #[cfg(feature = "stealth")]
583 #[must_use]
584 pub fn webrtc(mut self, webrtc: WebRtcConfig) -> Self {
585 self.config.webrtc = webrtc;
586 self
587 }
588
589 #[must_use]
591 pub fn arg(mut self, arg: String) -> Self {
592 self.config.args.push(arg);
593 self
594 }
595
596 #[must_use]
598 pub const fn stealth_level(mut self, level: StealthLevel) -> Self {
599 self.config.stealth_level = level;
600 self
601 }
602
603 #[must_use]
617 pub const fn disable_sandbox(mut self, disable: bool) -> Self {
618 self.config.disable_sandbox = disable;
619 self
620 }
621
622 #[must_use]
635 pub const fn cdp_fix_mode(mut self, mode: CdpFixMode) -> Self {
636 self.config.cdp_fix_mode = mode;
637 self
638 }
639
640 #[must_use]
653 pub fn source_url(mut self, url: Option<String>) -> Self {
654 self.config.source_url = url;
655 self
656 }
657
658 #[must_use]
660 pub const fn pool(mut self, pool: PoolConfig) -> Self {
661 self.config.pool = pool;
662 self
663 }
664
665 pub fn build(self) -> BrowserConfig {
667 self.config
668 }
669}
670
671mod duration_secs {
675 use serde::{Deserialize, Deserializer, Serialize, Serializer};
676 use std::time::Duration;
677
678 pub fn serialize<S: Serializer>(d: &Duration, s: S) -> std::result::Result<S::Ok, S::Error> {
679 d.as_secs().serialize(s)
680 }
681
682 pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> std::result::Result<Duration, D::Error> {
683 Ok(Duration::from_secs(u64::deserialize(d)?))
684 }
685}
686
687fn env_bool(key: &str, default: bool) -> bool {
690 std::env::var(key)
691 .map(|v| !matches!(v.to_lowercase().as_str(), "false" | "0" | "no"))
692 .unwrap_or(default)
693}
694
695#[allow(clippy::missing_const_for_fn)] fn is_containerized() -> bool {
707 #[cfg(target_os = "linux")]
708 {
709 if std::path::Path::new("/.dockerenv").exists() {
710 return true;
711 }
712 if let Ok(cgroup) = std::fs::read_to_string("/proc/1/cgroup")
713 && (cgroup.contains("docker") || cgroup.contains("kubepods"))
714 {
715 return true;
716 }
717 false
718 }
719 #[cfg(not(target_os = "linux"))]
720 {
721 false
722 }
723}
724
725fn env_u64(key: &str, default: u64) -> u64 {
726 std::env::var(key)
727 .ok()
728 .and_then(|v| v.parse().ok())
729 .unwrap_or(default)
730}
731
732fn env_usize(key: &str, default: usize) -> usize {
733 std::env::var(key)
734 .ok()
735 .and_then(|v| v.parse().ok())
736 .unwrap_or(default)
737}
738
739#[cfg(test)]
742mod tests {
743 use super::*;
744
745 #[test]
746 fn default_config_is_headless() {
747 let cfg = BrowserConfig::default();
748 assert!(cfg.headless);
749 }
750
751 #[test]
752 fn builder_roundtrip() {
753 let cfg = BrowserConfig::builder()
754 .headless(false)
755 .window_size(1280, 720)
756 .stealth_level(StealthLevel::Basic)
757 .build();
758
759 assert!(!cfg.headless);
760 assert_eq!(cfg.window_size, Some((1280, 720)));
761 assert_eq!(cfg.stealth_level, StealthLevel::Basic);
762 }
763
764 #[test]
765 fn effective_args_include_anti_detection_flag() {
766 let cfg = BrowserConfig::default();
767 let args = cfg.effective_args();
768 assert!(args.iter().any(|a| a.contains("AutomationControlled")));
769 }
770
771 #[test]
772 fn no_sandbox_only_when_explicitly_enabled() {
773 let with_sandbox_disabled = BrowserConfig::builder().disable_sandbox(true).build();
774 assert!(
775 with_sandbox_disabled
776 .effective_args()
777 .iter()
778 .any(|a| a == "--no-sandbox")
779 );
780
781 let with_sandbox_enabled = BrowserConfig::builder().disable_sandbox(false).build();
782 assert!(
783 !with_sandbox_enabled
784 .effective_args()
785 .iter()
786 .any(|a| a == "--no-sandbox")
787 );
788 }
789
790 #[test]
791 fn pool_config_defaults() {
792 let p = PoolConfig::default();
793 assert_eq!(p.min_size, 2);
794 assert_eq!(p.max_size, 10);
795 }
796
797 #[test]
798 fn stealth_level_none_not_active() {
799 assert!(!StealthLevel::None.is_active());
800 assert!(StealthLevel::Basic.is_active());
801 assert!(StealthLevel::Advanced.is_active());
802 }
803
804 #[test]
805 fn config_serialization() -> Result<(), Box<dyn std::error::Error>> {
806 let cfg = BrowserConfig::default();
807 let json = serde_json::to_string(&cfg)?;
808 let back: BrowserConfig = serde_json::from_str(&json)?;
809 assert_eq!(back.headless, cfg.headless);
810 assert_eq!(back.stealth_level, cfg.stealth_level);
811 Ok(())
812 }
813
814 #[test]
815 fn validate_default_config_is_valid() {
816 let cfg = BrowserConfig::default();
817 assert!(cfg.validate().is_ok(), "default config must be valid");
818 }
819
820 #[test]
821 fn validate_detects_pool_size_inversion() {
822 let cfg = BrowserConfig {
823 pool: PoolConfig {
824 min_size: 10,
825 max_size: 5,
826 ..PoolConfig::default()
827 },
828 ..BrowserConfig::default()
829 };
830 let result = cfg.validate();
831 assert!(result.is_err());
832 if let Err(errors) = result {
833 assert!(errors.iter().any(|e| e.contains("min_size")));
834 }
835 }
836
837 #[test]
838 fn validate_detects_zero_max_pool() {
839 let cfg = BrowserConfig {
840 pool: PoolConfig {
841 max_size: 0,
842 ..PoolConfig::default()
843 },
844 ..BrowserConfig::default()
845 };
846 let result = cfg.validate();
847 assert!(result.is_err());
848 if let Err(errors) = result {
849 assert!(errors.iter().any(|e| e.contains("max_size")));
850 }
851 }
852
853 #[test]
854 fn validate_detects_zero_timeouts() {
855 let cfg = BrowserConfig {
856 launch_timeout: std::time::Duration::ZERO,
857 cdp_timeout: std::time::Duration::ZERO,
858 ..BrowserConfig::default()
859 };
860 let result = cfg.validate();
861 assert!(result.is_err());
862 if let Err(errors) = result {
863 assert_eq!(errors.len(), 2);
864 }
865 }
866
867 #[test]
868 fn validate_detects_bad_proxy_scheme() {
869 let cfg = BrowserConfig {
870 proxy: Some("ftp://bad.proxy:1234".to_string()),
871 ..BrowserConfig::default()
872 };
873 let result = cfg.validate();
874 assert!(result.is_err());
875 if let Err(errors) = result {
876 assert!(errors.iter().any(|e| e.contains("proxy URL")));
877 }
878 }
879
880 #[test]
881 fn validate_accepts_valid_proxy() {
882 let cfg = BrowserConfig {
883 proxy: Some("socks5://user:pass@127.0.0.1:1080".to_string()),
884 ..BrowserConfig::default()
885 };
886 assert!(cfg.validate().is_ok());
887 }
888
889 #[test]
890 fn to_json_and_from_json_str_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
891 let cfg = BrowserConfig::builder()
892 .headless(false)
893 .stealth_level(StealthLevel::Basic)
894 .build();
895 let json = cfg.to_json()?;
896 assert!(json.contains("headless"));
897 let back = BrowserConfig::from_json_str(&json)?;
898 assert!(!back.headless);
899 assert_eq!(back.stealth_level, StealthLevel::Basic);
900 Ok(())
901 }
902
903 #[test]
904 fn from_json_str_error_on_invalid_json() {
905 let err = BrowserConfig::from_json_str("not json at all");
906 assert!(err.is_err());
907 }
908
909 #[test]
910 fn builder_cdp_fix_mode_and_source_url() {
911 use crate::cdp_protection::CdpFixMode;
912 let cfg = BrowserConfig::builder()
913 .cdp_fix_mode(CdpFixMode::IsolatedWorld)
914 .source_url(Some("stealth.js".to_string()))
915 .build();
916 assert_eq!(cfg.cdp_fix_mode, CdpFixMode::IsolatedWorld);
917 assert_eq!(cfg.source_url.as_deref(), Some("stealth.js"));
918 }
919
920 #[test]
921 fn builder_source_url_none_disables_sourceurl() {
922 let cfg = BrowserConfig::builder().source_url(None).build();
923 assert!(cfg.source_url.is_none());
924 }
925
926 #[test]
934 fn stealth_level_from_env_none() {
935 temp_env::with_var("STYGIAN_STEALTH_LEVEL", Some("none"), || {
938 let level = StealthLevel::from_env();
939 assert_eq!(level, StealthLevel::None);
940 });
941 }
942
943 #[test]
944 fn stealth_level_from_env_basic() {
945 temp_env::with_var("STYGIAN_STEALTH_LEVEL", Some("basic"), || {
946 assert_eq!(StealthLevel::from_env(), StealthLevel::Basic);
947 });
948 }
949
950 #[test]
951 fn stealth_level_from_env_advanced_is_default() {
952 temp_env::with_var("STYGIAN_STEALTH_LEVEL", Some("anything_else"), || {
953 assert_eq!(StealthLevel::from_env(), StealthLevel::Advanced);
954 });
955 }
956
957 #[test]
958 fn stealth_level_from_env_missing_defaults_to_advanced() {
959 temp_env::with_var("STYGIAN_STEALTH_LEVEL", None::<&str>, || {
961 assert_eq!(StealthLevel::from_env(), StealthLevel::Advanced);
962 });
963 }
964
965 #[test]
966 fn cdp_fix_mode_from_env_variants() {
967 use crate::cdp_protection::CdpFixMode;
968 let cases = [
969 ("add_binding", CdpFixMode::AddBinding),
970 ("isolatedworld", CdpFixMode::IsolatedWorld),
971 ("enable_disable", CdpFixMode::EnableDisable),
972 ("none", CdpFixMode::None),
973 ("unknown_value", CdpFixMode::AddBinding), ];
975 for (val, expected) in cases {
976 temp_env::with_var("STYGIAN_CDP_FIX_MODE", Some(val), || {
977 assert_eq!(
978 CdpFixMode::from_env(),
979 expected,
980 "STYGIAN_CDP_FIX_MODE={val}"
981 );
982 });
983 }
984 }
985
986 #[test]
987 fn pool_config_from_env_min_max() {
988 temp_env::with_vars(
989 [
990 ("STYGIAN_POOL_MIN", Some("3")),
991 ("STYGIAN_POOL_MAX", Some("15")),
992 ],
993 || {
994 let p = PoolConfig::default();
995 assert_eq!(p.min_size, 3);
996 assert_eq!(p.max_size, 15);
997 },
998 );
999 }
1000
1001 #[test]
1002 fn headless_from_env_false() {
1003 temp_env::with_var("STYGIAN_HEADLESS", Some("false"), || {
1004 assert!(!env_bool("STYGIAN_HEADLESS", true));
1006 });
1007 }
1008
1009 #[test]
1010 fn headless_from_env_zero_means_false() {
1011 temp_env::with_var("STYGIAN_HEADLESS", Some("0"), || {
1012 assert!(!env_bool("STYGIAN_HEADLESS", true));
1013 });
1014 }
1015
1016 #[test]
1017 fn headless_from_env_no_means_false() {
1018 temp_env::with_var("STYGIAN_HEADLESS", Some("no"), || {
1019 assert!(!env_bool("STYGIAN_HEADLESS", true));
1020 });
1021 }
1022
1023 #[test]
1024 fn validate_accepts_socks4_proxy() {
1025 let cfg = BrowserConfig {
1026 proxy: Some("socks4://127.0.0.1:1080".to_string()),
1027 ..BrowserConfig::default()
1028 };
1029 assert!(cfg.validate().is_ok());
1030 }
1031
1032 #[test]
1033 fn validate_multiple_errors_returned_together() {
1034 let cfg = BrowserConfig {
1035 pool: PoolConfig {
1036 min_size: 10,
1037 max_size: 5,
1038 ..PoolConfig::default()
1039 },
1040 launch_timeout: std::time::Duration::ZERO,
1041 proxy: Some("ftp://bad".to_string()),
1042 ..BrowserConfig::default()
1043 };
1044 let result = cfg.validate();
1045 assert!(result.is_err());
1046 if let Err(errors) = result {
1047 assert!(errors.len() >= 3, "expected ≥3 errors, got: {errors:?}");
1048 }
1049 }
1050
1051 #[test]
1052 fn json_file_error_on_missing_file() {
1053 let result = BrowserConfig::from_json_file("/nonexistent/path/config.json");
1054 assert!(result.is_err());
1055 if let Err(e) = result {
1056 let err_str = e.to_string();
1057 assert!(err_str.contains("cannot read config file") || err_str.contains("config"));
1058 }
1059 }
1060
1061 #[test]
1062 fn json_roundtrip_preserves_cdp_fix_mode() -> Result<(), Box<dyn std::error::Error>> {
1063 use crate::cdp_protection::CdpFixMode;
1064 let cfg = BrowserConfig::builder()
1065 .cdp_fix_mode(CdpFixMode::EnableDisable)
1066 .build();
1067 let json = cfg.to_json()?;
1068 let back = BrowserConfig::from_json_str(&json)?;
1069 assert_eq!(back.cdp_fix_mode, CdpFixMode::EnableDisable);
1070 Ok(())
1071 }
1072}
1073
1074#[cfg(test)]
1080#[allow(unsafe_code)] mod temp_env {
1082 use std::env;
1083 use std::ffi::OsStr;
1084 use std::sync::Mutex;
1085
1086 static ENV_LOCK: Mutex<()> = Mutex::new(());
1088
1089 pub fn with_var<K, V, F>(key: K, value: Option<V>, f: F)
1092 where
1093 K: AsRef<OsStr>,
1094 V: AsRef<OsStr>,
1095 F: FnOnce(),
1096 {
1097 let _guard = ENV_LOCK
1098 .lock()
1099 .unwrap_or_else(std::sync::PoisonError::into_inner);
1100 let key = key.as_ref();
1101 let prev = env::var_os(key);
1102 match value {
1103 Some(v) => unsafe { env::set_var(key, v.as_ref()) },
1104 None => unsafe { env::remove_var(key) },
1105 }
1106 f();
1107 match prev {
1108 Some(v) => unsafe { env::set_var(key, v) },
1109 None => unsafe { env::remove_var(key) },
1110 }
1111 }
1112
1113 pub fn with_vars<K, V, F>(pairs: impl IntoIterator<Item = (K, Option<V>)>, f: F)
1115 where
1116 K: AsRef<OsStr>,
1117 V: AsRef<OsStr>,
1118 F: FnOnce(),
1119 {
1120 let _guard = ENV_LOCK
1121 .lock()
1122 .unwrap_or_else(std::sync::PoisonError::into_inner);
1123 let pairs: Vec<_> = pairs
1124 .into_iter()
1125 .map(|(k, v)| {
1126 let key = k.as_ref().to_os_string();
1127 let prev = env::var_os(&key);
1128 let new_val = v.map(|v| v.as_ref().to_os_string());
1129 (key, prev, new_val)
1130 })
1131 .collect();
1132
1133 for (key, _, new_val) in &pairs {
1134 match new_val {
1135 Some(v) => unsafe { env::set_var(key, v) },
1136 None => unsafe { env::remove_var(key) },
1137 }
1138 }
1139
1140 f();
1141
1142 for (key, prev, _) in &pairs {
1143 match prev {
1144 Some(v) => unsafe { env::set_var(key, v) },
1145 None => unsafe { env::remove_var(key) },
1146 }
1147 }
1148 }
1149}