1use crate::EnvxError;
2use chrono::{DateTime, Utc};
3use color_eyre::Result;
4use indexmap::IndexMap;
5use regex::Regex;
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9pub enum EnvVarSource {
10 System,
11 User,
12 Process,
13 Shell,
14 Application(String),
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct EnvVar {
19 pub name: String,
20 pub value: String,
21 pub source: EnvVarSource,
22 pub modified: DateTime<Utc>,
23 pub original_value: Option<String>,
24}
25
26pub struct EnvVarManager {
27 pub vars: IndexMap<String, EnvVar>,
28 pub history: Vec<crate::history::HistoryEntry>,
29}
30
31impl Default for EnvVarManager {
32 fn default() -> Self {
33 Self {
34 vars: IndexMap::new(),
35 history: Vec::new(),
36 }
37 }
38}
39
40impl EnvVarManager {
41 #[must_use]
42 pub fn new() -> Self {
43 Self::default()
44 }
45
46 pub fn load_all(&mut self) -> Result<()> {
58 for (key, value) in std::env::vars() {
60 self.vars.insert(
61 key.clone(),
62 EnvVar {
63 name: key,
64 value,
65 source: EnvVarSource::Process,
66 modified: Utc::now(),
67 original_value: None,
68 },
69 );
70 }
71
72 #[cfg(windows)]
73 self.load_windows_vars();
74
75 #[cfg(unix)]
76 self.load_unix_vars();
77
78 Ok(())
79 }
80
81 #[cfg(windows)]
82 fn load_windows_vars(&mut self) {
83 use winreg::RegKey;
84 use winreg::enums::{HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE};
85
86 let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
88 if let Ok(env_key) = hklm.open_subkey("System\\CurrentControlSet\\Control\\Session Manager\\Environment") {
89 for (name, value) in env_key.enum_values().filter_map(std::result::Result::ok) {
90 let val_str = value.to_string();
91 self.vars.insert(
92 name.clone(),
93 EnvVar {
94 name,
95 value: val_str,
96 source: EnvVarSource::System,
97 modified: Utc::now(),
98 original_value: None,
99 },
100 );
101 }
102 }
103
104 let hkcu = RegKey::predef(HKEY_CURRENT_USER);
105 if let Ok(env_key) = hkcu.open_subkey("Environment") {
106 for (name, value) in env_key.enum_values().filter_map(std::result::Result::ok) {
107 let val_str = value.to_string();
108 self.vars.insert(
109 name.clone(),
110 EnvVar {
111 name,
112 value: val_str,
113 source: EnvVarSource::User,
114 modified: Utc::now(),
115 original_value: None,
116 },
117 );
118 }
119 }
120 }
121
122 #[cfg(unix)]
123 fn load_unix_vars(&mut self) {
124 for (key, value) in std::env::vars() {
127 let source = if key.starts_with("BASH_") || key.starts_with("ZSH_") {
128 EnvVarSource::Shell
129 } else {
130 EnvVarSource::Process
131 };
132
133 self.vars.insert(
134 key.clone(),
135 EnvVar {
136 name: key,
137 value,
138 source,
139 modified: Utc::now(),
140 original_value: None,
141 },
142 );
143 }
144 }
145
146 #[must_use]
147 pub fn get(&self, name: &str) -> Option<&EnvVar> {
148 self.vars.get(name)
149 }
150
151 #[must_use]
153 pub fn get_pattern(&self, pattern: &str) -> Vec<&EnvVar> {
154 if pattern.starts_with('/') && pattern.ends_with('/') && pattern.len() > 2 {
156 self.get_regex(&pattern[1..pattern.len() - 1])
158 } else if pattern.contains('*') || pattern.contains('?') {
159 self.get_wildcard(pattern)
161 } else {
162 self.get(pattern).into_iter().collect()
164 }
165 }
166
167 #[must_use]
169 pub fn get_wildcard(&self, pattern: &str) -> Vec<&EnvVar> {
170 let regex_pattern = wildcard_to_regex(pattern);
171 self.get_regex(®ex_pattern)
172 }
173
174 #[must_use]
176 pub fn get_regex(&self, pattern: &str) -> Vec<&EnvVar> {
177 match Regex::new(pattern) {
178 Ok(re) => self.vars.values().filter(|v| re.is_match(&v.name)).collect(),
179 Err(_) => vec![],
180 }
181 }
182
183 #[must_use]
185 pub fn get_prefix(&self, prefix: &str) -> Vec<&EnvVar> {
186 self.vars.values().filter(|v| v.name.starts_with(prefix)).collect()
187 }
188
189 #[must_use]
191 pub fn get_suffix(&self, suffix: &str) -> Vec<&EnvVar> {
192 self.vars.values().filter(|v| v.name.ends_with(suffix)).collect()
193 }
194
195 #[must_use]
197 pub fn get_containing(&self, substring: &str) -> Vec<&EnvVar> {
198 let lower = substring.to_lowercase();
199 self.vars
200 .values()
201 .filter(|v| v.name.to_lowercase().contains(&lower))
202 .collect()
203 }
204
205 pub fn set(&mut self, name: &str, value: &str, permanent: bool) -> Result<()> {
218 if name.is_empty() {
219 return Err(EnvxError::InvalidName("Variable name cannot be empty".to_string()).into());
220 }
221
222 if name.contains('=') {
223 return Err(EnvxError::InvalidName(format!("Variable name '{name}' cannot contain '='")).into());
224 }
225
226 #[cfg(windows)]
227 {
228 if name.contains('\0') {
230 return Err(EnvxError::InvalidName("Variable name cannot contain null character".to_string()).into());
231 }
232 }
233
234 let old_var = self.vars.get(name).cloned();
235
236 self.history
238 .push(crate::history::HistoryEntry::new(crate::history::HistoryAction::Set {
239 name: name.to_string(),
240 old_value: old_var.as_ref().map(|v| v.value.clone()),
241 new_value: value.to_string(),
242 }));
243
244 let var = EnvVar {
246 name: name.to_string(),
247 value: value.to_string(),
248 source: if permanent {
249 EnvVarSource::User
250 } else {
251 EnvVarSource::Process
252 },
253 modified: Utc::now(),
254 original_value: old_var.map(|v| v.value),
255 };
256 self.vars.insert(name.to_string(), var);
257
258 unsafe { std::env::set_var(name, value) };
260
261 if permanent {
262 #[cfg(windows)]
263 Self::set_windows_var(name, value, false)?;
264
265 #[cfg(unix)]
266 Self::set_unix_var(name, value);
267 }
268
269 Ok(())
270 }
271
272 #[cfg(windows)]
273 fn set_windows_var(name: &str, value: &str, system: bool) -> Result<()> {
274 use winreg::RegKey;
275 use winreg::enums::{HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_SET_VALUE};
276
277 let (key, subkey) = if system {
278 (
279 HKEY_LOCAL_MACHINE,
280 "System\\CurrentControlSet\\Control\\Session Manager\\Environment",
281 )
282 } else {
283 (HKEY_CURRENT_USER, "Environment")
284 };
285
286 let hkey = RegKey::predef(key);
287 let env_key = hkey.open_subkey_with_flags(subkey, KEY_SET_VALUE)?;
288 env_key.set_value(name, &value)?;
289
290 Ok(())
307 }
308
309 #[cfg(unix)]
310 fn set_unix_var(name: &str, value: &str) {
311 println!("Note: To make this permanent on Unix, add to your shell config:");
315 println!("export {name}=\"{value}\"");
316 }
317
318 pub fn delete(&mut self, name: &str) -> Result<()> {
328 let old_var = self
329 .vars
330 .swap_remove(name)
331 .ok_or_else(|| EnvxError::VarNotFound(name.to_string()))?;
332
333 self.history.push(crate::history::HistoryEntry::new(
334 crate::history::HistoryAction::Delete {
335 name: name.to_string(),
336 old_value: old_var.value.clone(),
337 },
338 ));
339
340 unsafe { std::env::remove_var(name) };
342
343 match old_var.source {
345 EnvVarSource::System | EnvVarSource::User => {
346 #[cfg(windows)]
347 delete_windows_var(name, matches!(old_var.source, EnvVarSource::System));
348
349 #[cfg(unix)]
350 delete_unix_var(name);
351 }
352 _ => {
353 }
355 }
356
357 Ok(())
358 }
359
360 #[must_use]
361 pub fn list(&self) -> Vec<&EnvVar> {
362 self.vars.values().collect()
363 }
364
365 #[must_use]
366 pub fn filter_by_source(&self, source: &EnvVarSource) -> Vec<&EnvVar> {
367 self.vars.values().filter(|v| v.source == *source).collect()
368 }
369
370 #[must_use]
371 pub fn search(&self, query: &str) -> Vec<&EnvVar> {
372 let query_lower = query.to_lowercase();
373 self.vars
374 .values()
375 .filter(|v| v.name.to_lowercase().contains(&query_lower) || v.value.to_lowercase().contains(&query_lower))
376 .collect()
377 }
378
379 pub fn undo(&mut self) -> Result<()> {
392 if let Some(entry) = self.history.pop() {
393 match entry.action {
395 crate::history::HistoryAction::Set { name, old_value, .. } => {
396 if let Some(old) = old_value {
397 let var = EnvVar {
399 name: name.clone(),
400 value: old.clone(),
401 source: EnvVarSource::Process,
402 modified: Utc::now(),
403 original_value: self.vars.get(&name).map(|v| v.value.clone()),
404 };
405 self.vars.insert(name.clone(), var);
406 unsafe { std::env::set_var(&name, &old) };
407 } else {
408 self.vars.swap_remove(&name);
410 unsafe { std::env::remove_var(&name) };
411 }
412 }
413 crate::history::HistoryAction::Delete { name, old_value } => {
414 let var = EnvVar {
416 name: name.clone(),
417 value: old_value.clone(),
418 source: EnvVarSource::Process,
419 modified: Utc::now(),
420 original_value: None,
421 };
422 self.vars.insert(name.clone(), var);
423 unsafe { std::env::set_var(&name, &old_value) };
424 }
425 crate::history::HistoryAction::BatchUpdate { .. } => {}
426 }
427 }
428 Ok(())
429 }
430
431 pub fn clear(&mut self) {
432 self.vars.clear();
433 }
434
435 pub fn rename(&mut self, pattern: &str, replacement: &str) -> Result<Vec<(String, String)>> {
446 let mut renamed = Vec::new();
447
448 if pattern.contains('*') {
449 let (prefix, suffix) = split_wildcard_pattern(pattern)?;
451 let (new_prefix, new_suffix) = split_wildcard_pattern(replacement)?;
452
453 let matching_vars: Vec<(String, EnvVar)> = self
455 .vars
456 .iter()
457 .filter(|(name, _)| {
458 name.starts_with(&prefix) && name.ends_with(&suffix) && name.len() >= prefix.len() + suffix.len()
459 })
460 .map(|(k, v)| (k.clone(), v.clone()))
461 .collect();
462
463 for (old_name, var) in matching_vars {
464 let middle = &old_name[prefix.len()..old_name.len() - suffix.len()];
466 let new_name = format!("{new_prefix}{middle}{new_suffix}");
467
468 if self.vars.contains_key(&new_name) {
470 return Err(EnvxError::Other(format!(
471 "Cannot rename '{old_name}' to '{new_name}': target variable already exists"
472 ))
473 .into());
474 }
475
476 self.set(&new_name, &var.value, true)?;
478
479 self.delete(&old_name)?;
481
482 renamed.push((old_name, new_name));
483 }
484 } else {
485 if let Some(var) = self.vars.get(pattern).cloned() {
487 if self.vars.contains_key(replacement) {
488 return Err(EnvxError::Other(format!(
489 "Cannot rename '{pattern}' to '{replacement}': target variable already exists"
490 ))
491 .into());
492 }
493
494 self.set(replacement, &var.value, true)?;
496
497 self.delete(pattern)?;
499
500 renamed.push((pattern.to_string(), replacement.to_string()));
501 } else {
502 return Err(EnvxError::Other(format!("Variable '{pattern}' not found")).into());
503 }
504 }
505
506 Ok(renamed)
507 }
508
509 pub fn replace(&mut self, pattern: &str, new_value: &str) -> Result<Vec<(String, String, String)>> {
521 let mut replaced = Vec::new();
522
523 if pattern.contains('*') {
524 let (prefix, suffix) = split_wildcard_pattern(pattern)?;
526
527 let matching_vars: Vec<(String, String)> = self
529 .vars
530 .iter()
531 .filter(|(name, _)| {
532 name.starts_with(&prefix) && name.ends_with(&suffix) && name.len() >= prefix.len() + suffix.len()
533 })
534 .map(|(name, var)| (name.clone(), var.value.clone()))
535 .collect();
536
537 for (name, old_value) in matching_vars {
538 self.set(&name, new_value, true)?;
539 replaced.push((name, old_value, new_value.to_string()));
540 }
541 } else {
542 if let Some(var) = self.vars.get(pattern).cloned() {
544 let old_value = var.value;
545 self.set(pattern, new_value, true)?;
546 replaced.push((pattern.to_string(), old_value, new_value.to_string()));
547 } else {
548 return Err(EnvxError::VarNotFound(pattern.to_string()).into());
549 }
550 }
551
552 Ok(replaced)
553 }
554
555 pub fn find_replace(
568 &mut self,
569 search: &str,
570 replacement: &str,
571 pattern: Option<&str>,
572 ) -> Result<Vec<(String, String, String)>> {
573 let mut replaced = Vec::new();
574
575 let vars_to_update: Vec<(String, EnvVar)> = if let Some(pat) = pattern {
576 if pat.contains('*') {
578 let (prefix, suffix) = split_wildcard_pattern(pat)?;
579 self.vars
580 .iter()
581 .filter(|(name, _)| {
582 name.starts_with(&prefix)
583 && name.ends_with(&suffix)
584 && name.len() >= prefix.len() + suffix.len()
585 })
586 .filter(|(_, var)| var.value.contains(search))
587 .map(|(k, v)| (k.clone(), v.clone()))
588 .collect()
589 } else {
590 self.vars
592 .iter()
593 .filter(|(name, _)| name == &pat)
594 .filter(|(_, var)| var.value.contains(search))
595 .map(|(k, v)| (k.clone(), v.clone()))
596 .collect()
597 }
598 } else {
599 self.vars
601 .iter()
602 .filter(|(_, var)| var.value.contains(search))
603 .map(|(k, v)| (k.clone(), v.clone()))
604 .collect()
605 };
606
607 for (name, var) in vars_to_update {
608 let old_value = var.value.clone();
609 let new_value = var.value.replace(search, replacement);
610
611 self.set(&name, &new_value, true)?;
613
614 replaced.push((name, old_value, new_value));
615 }
616
617 Ok(replaced)
618 }
619}
620fn wildcard_to_regex(pattern: &str) -> String {
621 let mut regex = String::new();
622 regex.push('^');
623
624 for ch in pattern.chars() {
625 match ch {
626 '*' => regex.push_str(".*"),
627 '?' => regex.push('.'),
628 '.' | '+' | '^' | '$' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '\\' => {
629 regex.push('\\');
630 regex.push(ch);
631 }
632 _ => regex.push(ch),
633 }
634 }
635
636 regex.push('$');
637 regex
638}
639
640pub fn split_wildcard_pattern(pattern: &str) -> Result<(String, String)> {
646 if let Some(pos) = pattern.find('*') {
647 let prefix = pattern[..pos].to_string();
648 let suffix = pattern[pos + 1..].to_string();
649
650 if suffix.contains('*') {
652 return Err(EnvxError::Other("Multiple wildcards not supported".to_string()).into());
653 }
654
655 Ok((prefix, suffix))
656 } else {
657 Ok((pattern.to_string(), String::new()))
658 }
659}
660
661#[cfg(windows)]
662fn delete_windows_var(name: &str, system: bool) {
663 use winreg::RegKey;
664 use winreg::enums::{HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, KEY_SET_VALUE};
665
666 let (key, subkey) = if system {
667 (
668 HKEY_LOCAL_MACHINE,
669 "System\\CurrentControlSet\\Control\\Session Manager\\Environment",
670 )
671 } else {
672 (HKEY_CURRENT_USER, "Environment")
673 };
674
675 let hkey = RegKey::predef(key);
676 match hkey.open_subkey_with_flags(subkey, KEY_SET_VALUE) {
677 Ok(env_key) => {
678 match env_key.delete_value(name) {
679 Ok(()) => {
680 }
683 Err(e) => {
684 tracing::debug!("Could not delete {} from registry: {}", name, e);
687 }
688 }
689 }
690 Err(e) => {
691 if system {
692 tracing::warn!(
694 "Cannot delete system variable {} (requires admin privileges): {}",
695 name,
696 e
697 );
698 }
699 }
701 }
702}
703
704#[cfg(unix)]
705fn delete_unix_var(name: &str) {
706 println!("Note: To permanently remove this on Unix, remove from your shell config:");
710 println!("unset {name}");
711 println!("And remove any 'export {name}=...' lines from .bashrc/.zshrc/etc.");
712}
713
714#[cfg(test)]
715mod tests {
716 #![allow(clippy::unwrap_used)]
717 #![allow(clippy::expect_used)]
718 #![allow(clippy::panic)]
719 use chrono::Utc;
720
721 use crate::{EnvVarManager, EnvVarSource};
722
723 use super::*;
724
725 fn create_test_var(name: &str, value: &str, source: EnvVarSource) -> EnvVar {
727 EnvVar {
728 name: name.to_string(),
729 value: value.to_string(),
730 source,
731 modified: Utc::now(),
732 original_value: None,
733 }
734 }
735
736 fn create_test_manager() -> EnvVarManager {
738 let mut manager = EnvVarManager::new();
739
740 manager.vars.insert(
742 "PATH".to_string(),
743 create_test_var("PATH", "/usr/bin:/usr/local/bin", EnvVarSource::System),
744 );
745 manager.vars.insert(
746 "HOME".to_string(),
747 create_test_var("HOME", "/home/user", EnvVarSource::User),
748 );
749 manager.vars.insert(
750 "RUST_LOG".to_string(),
751 create_test_var("RUST_LOG", "debug", EnvVarSource::Process),
752 );
753 manager.vars.insert(
754 "API_KEY".to_string(),
755 create_test_var("API_KEY", "secret123", EnvVarSource::User),
756 );
757 manager.vars.insert(
758 "API_SECRET".to_string(),
759 create_test_var("API_SECRET", "supersecret", EnvVarSource::User),
760 );
761 manager.vars.insert(
762 "DATABASE_URL".to_string(),
763 create_test_var("DATABASE_URL", "postgres://localhost", EnvVarSource::Process),
764 );
765 manager.vars.insert(
766 "APP_VERSION".to_string(),
767 create_test_var("APP_VERSION", "1.0.0", EnvVarSource::Application("myapp".to_string())),
768 );
769
770 manager
771 }
772
773 #[test]
774 fn test_new() {
775 let manager = EnvVarManager::new();
776 assert!(manager.vars.is_empty());
777 assert!(manager.history.is_empty());
778 }
779
780 #[test]
781 fn test_get() {
782 let manager = create_test_manager();
783
784 let var = manager.get("PATH").unwrap();
786 assert_eq!(var.name, "PATH");
787 assert_eq!(var.value, "/usr/bin:/usr/local/bin");
788 assert_eq!(var.source, EnvVarSource::System);
789
790 assert!(manager.get("NON_EXISTENT").is_none());
792 }
793
794 #[test]
795 fn test_get_pattern_exact_match() {
796 let manager = create_test_manager();
797
798 let vars = manager.get_pattern("PATH");
799 assert_eq!(vars.len(), 1);
800 assert_eq!(vars[0].name, "PATH");
801 }
802
803 #[test]
804 fn test_get_pattern_wildcard() {
805 let manager = create_test_manager();
806
807 let vars = manager.get_pattern("API_*");
809 assert_eq!(vars.len(), 2);
810 let names: Vec<&str> = vars.iter().map(|v| v.name.as_str()).collect();
811 assert!(names.contains(&"API_KEY"));
812 assert!(names.contains(&"API_SECRET"));
813
814 let vars = manager.get_pattern("HOM?");
816 assert_eq!(vars.len(), 1);
817 assert_eq!(vars[0].name, "HOME");
818
819 let vars = manager.get_pattern("*_*");
821 assert!(vars.len() >= 4); }
823
824 #[test]
825 fn test_get_pattern_regex() {
826 let manager = create_test_manager();
827
828 let vars = manager.get_pattern("/^API.*/");
830 assert_eq!(vars.len(), 2);
831
832 let vars = manager.get_pattern("/(?i)^api.*/");
834 assert_eq!(vars.len(), 2);
835 }
836
837 #[test]
838 fn test_get_wildcard() {
839 let manager = create_test_manager();
840
841 assert_eq!(manager.get_wildcard("*").len(), 7); assert_eq!(manager.get_wildcard("A*").len(), 3); assert_eq!(manager.get_wildcard("*URL").len(), 1); assert_eq!(manager.get_wildcard("????").len(), 2); }
847
848 #[test]
849 fn test_get_regex() {
850 let manager = create_test_manager();
851
852 assert_eq!(manager.get_regex("^API.*").len(), 2);
854 assert_eq!(manager.get_regex(".*URL$").len(), 1);
855 assert_eq!(manager.get_regex("^[A-Z]+$").len(), 2); assert_eq!(manager.get_regex("[").len(), 0);
859 }
860
861 #[test]
862 fn test_get_prefix() {
863 let manager = create_test_manager();
864
865 assert_eq!(manager.get_prefix("API_").len(), 2);
866 assert_eq!(manager.get_prefix("DATA").len(), 1);
867 assert_eq!(manager.get_prefix("NON").len(), 0);
868 }
869
870 #[test]
871 fn test_get_suffix() {
872 let manager = create_test_manager();
873
874 assert_eq!(manager.get_suffix("_URL").len(), 1);
875 assert_eq!(manager.get_suffix("KEY").len(), 1);
876 assert_eq!(manager.get_suffix("SECRET").len(), 1);
877 assert_eq!(manager.get_suffix("XYZ").len(), 0);
878 }
879
880 #[test]
881 fn test_get_containing() {
882 let manager = create_test_manager();
883
884 assert_eq!(manager.get_containing("api").len(), 2);
886 assert_eq!(manager.get_containing("API").len(), 2);
887 assert_eq!(manager.get_containing("_").len(), 5);
888 assert_eq!(manager.get_containing("URL").len(), 1);
889 }
890
891 #[test]
892 fn test_set_temporary() {
893 let mut manager = EnvVarManager::new();
894
895 manager.set("TEST_VAR", "test_value", false).unwrap();
897
898 let var = manager.get("TEST_VAR").unwrap();
899 assert_eq!(var.value, "test_value");
900 assert_eq!(var.source, EnvVarSource::Process);
901 assert!(var.original_value.is_none());
902
903 assert_eq!(std::env::var("TEST_VAR").unwrap(), "test_value");
905
906 unsafe { std::env::remove_var("TEST_VAR") };
908 }
909
910 #[test]
911 fn test_set_overwrite_existing() {
912 let mut manager = create_test_manager();
913
914 manager.set("RUST_LOG", "info", false).unwrap();
916
917 let var = manager.get("RUST_LOG").unwrap();
918 assert_eq!(var.value, "info");
919 assert_eq!(var.original_value, Some("debug".to_string()));
920 }
921
922 #[test]
923 fn test_delete() {
924 let mut manager = create_test_manager();
925
926 assert!(manager.delete("RUST_LOG").is_ok());
928 assert!(manager.get("RUST_LOG").is_none());
929
930 assert!(manager.delete("NON_EXISTENT").is_err());
932 }
933
934 #[test]
935 fn test_list() {
936 let manager = create_test_manager();
937 let vars = manager.list();
938 assert_eq!(vars.len(), 7);
939 }
940
941 #[test]
942 fn test_filter_by_source() {
943 let manager = create_test_manager();
944
945 assert_eq!(manager.filter_by_source(&EnvVarSource::System).len(), 1);
946 assert_eq!(manager.filter_by_source(&EnvVarSource::User).len(), 3);
947 assert_eq!(manager.filter_by_source(&EnvVarSource::Process).len(), 2);
948 assert_eq!(manager.filter_by_source(&EnvVarSource::Shell).len(), 0);
949 assert_eq!(
950 manager
951 .filter_by_source(&EnvVarSource::Application("myapp".to_string()))
952 .len(),
953 1
954 );
955 }
956
957 #[test]
958 fn test_search() {
959 let manager = create_test_manager();
960
961 assert_eq!(manager.search("api").len(), 2);
963 assert_eq!(manager.search("PATH").len(), 1);
964
965 assert_eq!(manager.search("secret").len(), 2);
967 assert_eq!(manager.search("localhost").len(), 1);
968
969 assert_eq!(manager.search("API").len(), 2);
971 assert_eq!(manager.search("SECRET").len(), 2);
972 }
973
974 #[test]
975 fn test_history_tracking() {
976 let mut manager = EnvVarManager::new();
977
978 manager.set("VAR1", "value1", false).unwrap();
980 assert_eq!(manager.history.len(), 1);
981
982 manager.set("VAR1", "value2", false).unwrap();
984 assert_eq!(manager.history.len(), 2);
985
986 manager.delete("VAR1").unwrap();
988 assert_eq!(manager.history.len(), 3);
989
990 if let crate::history::HistoryAction::Delete { name, old_value } = &manager.history[2].action {
992 assert_eq!(name, "VAR1");
993 assert_eq!(old_value, "value2");
994 } else {
995 panic!("Expected Delete action");
996 }
997 }
998
999 #[test]
1000 fn test_undo_set() {
1001 let mut manager = EnvVarManager::new();
1002
1003 manager.set("UNDO_TEST", "value1", false).unwrap();
1005
1006 manager.set("UNDO_TEST", "value2", false).unwrap();
1008
1009 manager.undo().unwrap();
1011 assert_eq!(manager.get("UNDO_TEST").unwrap().value, "value1");
1012
1013 manager.undo().unwrap();
1015 assert!(manager.get("UNDO_TEST").is_none());
1016 }
1017
1018 #[test]
1019 fn test_undo_delete() {
1020 let mut manager = EnvVarManager::new();
1021
1022 manager.set("DELETE_TEST", "value", false).unwrap();
1024 manager.delete("DELETE_TEST").unwrap();
1025 assert!(manager.get("DELETE_TEST").is_none());
1026
1027 manager.undo().unwrap();
1029 assert_eq!(manager.get("DELETE_TEST").unwrap().value, "value");
1030 }
1031
1032 #[test]
1033 fn test_wildcard_to_regex() {
1034 assert_eq!(wildcard_to_regex("API_*"), "^API_.*$");
1036 assert_eq!(wildcard_to_regex("*_KEY"), "^.*_KEY$");
1037 assert_eq!(wildcard_to_regex("*TEST*"), "^.*TEST.*$");
1038
1039 assert_eq!(wildcard_to_regex("HOM?"), "^HOM.$");
1041 assert_eq!(wildcard_to_regex("??ST"), "^..ST$");
1042
1043 assert_eq!(wildcard_to_regex("TEST.VAR"), "^TEST\\.VAR$");
1045 assert_eq!(wildcard_to_regex("VAR[1]"), "^VAR\\[1\\]$");
1046 assert_eq!(wildcard_to_regex("A+B"), "^A\\+B$");
1047 assert_eq!(wildcard_to_regex("^START"), "^\\^START$");
1048 assert_eq!(wildcard_to_regex("END$"), "^END\\$$");
1049 assert_eq!(wildcard_to_regex("(GROUP)"), "^\\(GROUP\\)$");
1050 assert_eq!(wildcard_to_regex("{BRACE}"), "^\\{BRACE\\}$");
1051 assert_eq!(wildcard_to_regex("A|B"), "^A\\|B$");
1052 assert_eq!(wildcard_to_regex("C\\D"), "^C\\\\D$");
1053
1054 assert_eq!(wildcard_to_regex("*.txt"), "^.*\\.txt$");
1056 assert_eq!(wildcard_to_regex("file?.log"), "^file.\\.log$");
1057 }
1058
1059 #[test]
1060 fn test_env_var_source_equality() {
1061 assert_eq!(EnvVarSource::System, EnvVarSource::System);
1062 assert_eq!(EnvVarSource::User, EnvVarSource::User);
1063 assert_eq!(EnvVarSource::Process, EnvVarSource::Process);
1064 assert_eq!(EnvVarSource::Shell, EnvVarSource::Shell);
1065 assert_eq!(
1066 EnvVarSource::Application("app1".to_string()),
1067 EnvVarSource::Application("app1".to_string())
1068 );
1069 assert_ne!(
1070 EnvVarSource::Application("app1".to_string()),
1071 EnvVarSource::Application("app2".to_string())
1072 );
1073 assert_ne!(EnvVarSource::System, EnvVarSource::User);
1074 }
1075
1076 #[test]
1077 fn test_load_all() {
1078 let mut manager = EnvVarManager::new();
1079
1080 unsafe { std::env::set_var("TEST_LOAD_VAR1", "value1") };
1082 unsafe { std::env::set_var("TEST_LOAD_VAR2", "value2") };
1083
1084 manager.load_all().unwrap();
1086
1087 assert!(manager.get("TEST_LOAD_VAR1").is_some());
1089 assert!(manager.get("TEST_LOAD_VAR2").is_some());
1090
1091 assert_eq!(manager.get("TEST_LOAD_VAR1").unwrap().source, EnvVarSource::Process);
1093
1094 unsafe { std::env::remove_var("TEST_LOAD_VAR1") };
1096 unsafe { std::env::remove_var("TEST_LOAD_VAR2") };
1097 }
1098
1099 #[test]
1100 #[cfg(unix)]
1101 fn test_unix_shell_detection() {
1102 let mut manager = EnvVarManager::new();
1103
1104 unsafe { std::env::set_var("BASH_VERSION", "5.0.0") };
1106
1107 manager.load_unix_vars();
1108
1109 if let Some(var) = manager.get("BASH_VERSION") {
1110 assert_eq!(var.source, EnvVarSource::Shell);
1111 }
1112
1113 unsafe { std::env::remove_var("BASH_VERSION") };
1115 }
1116
1117 #[test]
1118 fn test_special_characters_in_values() {
1119 let mut manager = EnvVarManager::new();
1120
1121 let special_values = vec![
1123 ("NEWLINE_VAR", "line1\nline2"),
1124 ("TAB_VAR", "col1\tcol2"),
1125 ("QUOTE_VAR", "value with \"quotes\""),
1126 ("BACKSLASH_VAR", "C:\\path\\to\\file"),
1127 ("UNICODE_VAR", "Hello δΈη π"),
1128 ("EMPTY_VAR", ""),
1129 ("SPACE_VAR", " spaces around "),
1130 ];
1131
1132 for (name, value) in special_values {
1133 manager.set(name, value, false).unwrap();
1134 assert_eq!(manager.get(name).unwrap().value, value);
1135 assert_eq!(std::env::var(name).unwrap(), value);
1136 unsafe { std::env::remove_var(name) };
1137 }
1138 }
1139
1140 #[test]
1141 fn test_variable_ordering() {
1142 let mut manager = EnvVarManager::new();
1143
1144 let vars = vec!["ZETA", "ALPHA", "GAMMA", "BETA"];
1146 for var in &vars {
1147 manager.set(var, "value", false).unwrap();
1148 }
1149
1150 let list: Vec<&str> = manager.list().iter().map(|v| v.name.as_str()).collect();
1152 assert_eq!(list, vars);
1153 }
1154
1155 #[test]
1156 fn test_concurrent_modification_safety() {
1157 let mut manager = create_test_manager();
1158
1159 let initial_count = manager.list().len();
1161
1162 let vars_to_modify: Vec<String> = manager.get_prefix("API_").iter().map(|v| v.name.clone()).collect();
1164
1165 for name in vars_to_modify {
1166 manager.set(&name, "modified", false).unwrap();
1167 }
1168
1169 assert_eq!(manager.list().len(), initial_count);
1171 }
1172
1173 #[test]
1174 fn test_empty_operations() {
1175 let manager = EnvVarManager::new();
1176
1177 assert_eq!(manager.list().len(), 0);
1179 assert_eq!(manager.get_pattern("*").len(), 0);
1180 assert_eq!(manager.get_prefix("").len(), 0);
1181 assert_eq!(manager.get_suffix("").len(), 0);
1182 assert_eq!(manager.get_containing("").len(), 0);
1183 assert_eq!(manager.search("anything").len(), 0);
1184 }
1185
1186 #[test]
1187 fn test_case_sensitivity() {
1188 let mut manager = EnvVarManager::new();
1189
1190 manager.set("test_var", "lower", false).unwrap();
1192 manager.set("TEST_VAR", "upper", false).unwrap();
1193
1194 assert_eq!(manager.get("test_var").unwrap().value, "lower");
1195 assert_eq!(manager.get("TEST_VAR").unwrap().value, "upper");
1196 assert!(manager.get("Test_Var").is_none());
1197
1198 assert_eq!(manager.search("test_var").len(), 2);
1200 assert_eq!(manager.get_containing("test_var").len(), 2);
1201 }
1202
1203 #[test]
1204 fn test_original_value_tracking() {
1205 let mut manager = EnvVarManager::new();
1206
1207 manager.set("TRACK_VAR", "v1", false).unwrap();
1209 assert!(manager.get("TRACK_VAR").unwrap().original_value.is_none());
1210
1211 manager.set("TRACK_VAR", "v2", false).unwrap();
1213 assert_eq!(manager.get("TRACK_VAR").unwrap().original_value, Some("v1".to_string()));
1214
1215 manager.set("TRACK_VAR", "v3", false).unwrap();
1217 assert_eq!(manager.get("TRACK_VAR").unwrap().original_value, Some("v2".to_string()));
1218 }
1219
1220 #[test]
1221 fn test_rename_exact_match() {
1222 let mut manager = EnvVarManager::new();
1223 manager.set("OLD_VAR", "value", false).unwrap();
1224
1225 let renamed = manager.rename("OLD_VAR", "NEW_VAR").unwrap();
1226
1227 assert_eq!(renamed.len(), 1);
1228 assert_eq!(renamed[0], ("OLD_VAR".to_string(), "NEW_VAR".to_string()));
1229
1230 assert!(manager.get("OLD_VAR").is_none());
1231 assert_eq!(manager.get("NEW_VAR").unwrap().value, "value");
1232 }
1233
1234 #[test]
1235 fn test_rename_wildcard_prefix() {
1236 let mut manager = EnvVarManager::new();
1237 manager.set("APP_VAR1", "value1", false).unwrap();
1238 manager.set("APP_VAR2", "value2", false).unwrap();
1239 manager.set("OTHER_VAR", "other", false).unwrap();
1240
1241 let renamed = manager.rename("APP_*", "MY_APP_*").unwrap();
1242
1243 assert_eq!(renamed.len(), 2);
1244 assert!(manager.get("MY_APP_VAR1").is_some());
1245 assert!(manager.get("MY_APP_VAR2").is_some());
1246 assert!(manager.get("APP_VAR1").is_none());
1247 assert!(manager.get("APP_VAR2").is_none());
1248 assert!(manager.get("OTHER_VAR").is_some());
1249 }
1250
1251 #[test]
1252 fn test_rename_target_exists_error() {
1253 let mut manager = EnvVarManager::new();
1254 manager.set("VAR1", "value1", false).unwrap();
1255 manager.set("VAR2", "value2", false).unwrap();
1256
1257 let result = manager.rename("VAR1", "VAR2");
1258 assert!(result.is_err());
1259 assert!(result.unwrap_err().to_string().contains("already exists"));
1260 }
1261
1262 #[test]
1263 fn test_rename_not_found_error() {
1264 let mut manager = EnvVarManager::new();
1265
1266 let result = manager.rename("NONEXISTENT", "NEW_VAR");
1267 assert!(result.is_err());
1268 assert!(result.unwrap_err().to_string().contains("not found"));
1269 }
1270
1271 #[test]
1272 fn test_replace_single_variable() {
1273 let mut manager = EnvVarManager::new();
1274 manager.set("MY_VAR", "old_value", false).unwrap();
1275
1276 let replaced = manager.replace("MY_VAR", "new_value").unwrap();
1277
1278 assert_eq!(replaced.len(), 1);
1279 assert_eq!(
1280 replaced[0],
1281 ("MY_VAR".to_string(), "old_value".to_string(), "new_value".to_string())
1282 );
1283 assert_eq!(manager.get("MY_VAR").unwrap().value, "new_value");
1284 }
1285
1286 #[test]
1287 fn test_replace_wildcard_pattern() {
1288 let mut manager = EnvVarManager::new();
1289 manager.set("API_KEY", "old_key", false).unwrap();
1290 manager.set("API_SECRET", "old_secret", false).unwrap();
1291 manager.set("OTHER_VAR", "other", false).unwrap();
1292
1293 let replaced = manager.replace("API_*", "REDACTED").unwrap();
1294
1295 assert_eq!(replaced.len(), 2);
1296 assert_eq!(manager.get("API_KEY").unwrap().value, "REDACTED");
1297 assert_eq!(manager.get("API_SECRET").unwrap().value, "REDACTED");
1298 assert_eq!(manager.get("OTHER_VAR").unwrap().value, "other");
1299 }
1300
1301 #[test]
1302 fn test_replace_not_found() {
1303 let mut manager = EnvVarManager::new();
1304
1305 let result = manager.replace("NONEXISTENT", "value");
1306 assert!(result.is_err());
1307 }
1308
1309 #[test]
1310 fn test_find_replace_in_values() {
1311 let mut manager = EnvVarManager::new();
1312 manager.set("DATABASE_URL", "localhost:5432", false).unwrap();
1313 manager.set("API_URL", "localhost:8080", false).unwrap();
1314
1315 let replaced = manager
1316 .find_replace("localhost", "production.server.com", None)
1317 .unwrap();
1318
1319 assert_eq!(replaced.len(), 2);
1320 assert_eq!(manager.get("DATABASE_URL").unwrap().value, "production.server.com:5432");
1321 assert_eq!(manager.get("API_URL").unwrap().value, "production.server.com:8080");
1322 }
1323
1324 #[test]
1325 fn test_split_wildcard_pattern() {
1326 assert_eq!(
1327 split_wildcard_pattern("APP_*").unwrap(),
1328 ("APP_".to_string(), String::new())
1329 );
1330 assert_eq!(
1331 split_wildcard_pattern("*_SUFFIX").unwrap(),
1332 (String::new(), "_SUFFIX".to_string())
1333 );
1334 assert_eq!(
1335 split_wildcard_pattern("PREFIX_*_SUFFIX").unwrap(),
1336 ("PREFIX_".to_string(), "_SUFFIX".to_string())
1337 );
1338 assert_eq!(
1339 split_wildcard_pattern("NO_WILDCARD").unwrap(),
1340 ("NO_WILDCARD".to_string(), String::new())
1341 );
1342
1343 assert!(split_wildcard_pattern("*_*").is_err());
1345 }
1346
1347 #[test]
1348 fn test_delete_permanent_variable() {
1349 let mut manager = EnvVarManager::new();
1350
1351 manager.set("DELETE_PERM_TEST", "value", true).unwrap();
1353
1354 assert!(manager.get("DELETE_PERM_TEST").is_some());
1356 assert_eq!(std::env::var("DELETE_PERM_TEST").unwrap(), "value");
1357
1358 manager.delete("DELETE_PERM_TEST").unwrap();
1360
1361 assert!(manager.get("DELETE_PERM_TEST").is_none());
1363
1364 assert!(std::env::var("DELETE_PERM_TEST").is_err());
1366
1367 }
1370
1371 #[test]
1372 fn test_delete_tracks_source() {
1373 let mut manager = EnvVarManager::new();
1374
1375 manager.vars.insert(
1377 "SYS_VAR".to_string(),
1378 create_test_var("SYS_VAR", "sys_value", EnvVarSource::System),
1379 );
1380 manager.vars.insert(
1381 "USER_VAR".to_string(),
1382 create_test_var("USER_VAR", "user_value", EnvVarSource::User),
1383 );
1384 manager.vars.insert(
1385 "PROC_VAR".to_string(),
1386 create_test_var("PROC_VAR", "proc_value", EnvVarSource::Process),
1387 );
1388
1389 assert!(manager.delete("SYS_VAR").is_ok());
1391 assert!(manager.delete("USER_VAR").is_ok());
1392 assert!(manager.delete("PROC_VAR").is_ok());
1393
1394 assert!(manager.get("SYS_VAR").is_none());
1396 assert!(manager.get("USER_VAR").is_none());
1397 assert!(manager.get("PROC_VAR").is_none());
1398 }
1399}