Skip to main content

nu_protocol/engine/
env_name.rs

1//! Environment variable name handling with global case-insensitivity.
2//!
3//! This module defines `EnvName`, a wrapper around `String` that handles case sensitivity
4//! for environment variable names. All environment variables are now case-insensitive for lookup
5//! but case-preserving for storage across all platforms.
6//!
7//! ## Case Sensitivity Rules
8//!
9//! - **All platforms**: All environment variable names are case-insensitive but case-preserving.
10//!   This means `PATH`, `Path`, and `path` refer to the same variable, and so do `HOME` and `home`.
11//!
12//! This ensures that:
13//! - `$env.PATH`, `$env.path`, `$env.Path` all work on any platform
14//! - `$env.HOME`, `$env.home` all refer to the same variable
15//! - Case is preserved when storing the variable name
16//! - Existing scripts may need updates if they relied on case sensitivity
17
18use std::fmt;
19use std::fmt::Debug;
20use std::hash::{Hash, Hasher};
21
22/// A `String` that's case-insensitive for all environment variable names, used for environment variable names.
23#[derive(Clone)]
24pub struct EnvName(pub(crate) String);
25
26impl Debug for EnvName {
27    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28        self.0.fmt(f)
29    }
30}
31
32impl<T: Into<String>> From<T> for EnvName {
33    fn from(name: T) -> Self {
34        EnvName(name.into())
35    }
36}
37
38impl AsRef<str> for EnvName {
39    fn as_ref(&self) -> &str {
40        &self.0
41    }
42}
43
44impl PartialEq<Self> for EnvName {
45    fn eq(&self, other: &Self) -> bool {
46        // All environment variables are case-insensitive on all platforms
47        self.0.eq_ignore_ascii_case(&other.0)
48    }
49}
50
51impl Eq for EnvName {}
52
53impl Hash for EnvName {
54    fn hash<H: Hasher>(&self, state: &mut H) {
55        // All environment variables are hashed case-insensitively on all platforms
56        self.hash_case_insensitive(state);
57    }
58}
59
60impl EnvName {
61    /// Get the inner string
62    pub fn into_string(self) -> String {
63        self.0
64    }
65
66    /// Get a reference to the inner string
67    pub fn as_str(&self) -> &str {
68        &self.0
69    }
70
71    /// Hash the name case-insensitively by uppercasing each byte
72    fn hash_case_insensitive<H: Hasher>(&self, state: &mut H) {
73        for &b in self.0.as_bytes() {
74            b.to_ascii_uppercase().hash(state);
75        }
76    }
77}
78
79#[test]
80fn test_env_name_case_insensitive() {
81    // On all platforms, all environment variables are case-insensitive
82    let strings1 = [
83        EnvName::from("abc"),
84        EnvName::from("Abc"),
85        EnvName::from("aBc"),
86        EnvName::from("abC"),
87        EnvName::from("ABc"),
88        EnvName::from("aBC"),
89        EnvName::from("AbC"),
90        EnvName::from("ABC"),
91    ];
92    let strings2 = [
93        EnvName::from("xyz"),
94        EnvName::from("Xyz"),
95        EnvName::from("xYz"),
96        EnvName::from("xyZ"),
97        EnvName::from("XYz"),
98        EnvName::from("xYZ"),
99        EnvName::from("XyZ"),
100        EnvName::from("XYZ"),
101    ];
102    // All the strings in `strings1` compare equal to each other and hash the same.
103    for s1 in &strings1 {
104        for also_s1 in &strings1 {
105            assert_eq!(s1, also_s1);
106            let mut hash_set = std::collections::HashSet::new();
107            hash_set.insert(s1);
108            hash_set.insert(also_s1);
109            assert_eq!(hash_set.len(), 1);
110        }
111    }
112    // Same for `strings2`.
113    for s2 in &strings2 {
114        for also_s2 in &strings2 {
115            assert_eq!(s2, also_s2);
116            let mut hash_set = std::collections::HashSet::new();
117            hash_set.insert(s2);
118            hash_set.insert(also_s2);
119            assert_eq!(hash_set.len(), 1);
120        }
121    }
122    // But nothing in `strings1` compares equal to anything in `strings2`. We also assert that
123    // their hashes are distinct. In theory they could collide, but using DefaultHasher here (which
124    // is initialized with the zero key) should prevent that from happening randomly.
125    for s1 in &strings1 {
126        for s2 in &strings2 {
127            assert_ne!(s1, s2);
128            let mut hasher1 = std::hash::DefaultHasher::new();
129            s1.hash(&mut hasher1);
130            let mut hasher2 = std::hash::DefaultHasher::new();
131            s2.hash(&mut hasher2);
132            assert_ne!(hasher1.finish(), hasher2.finish());
133        }
134    }
135}