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 serde::{Deserialize, Serialize};
19use std::hash::{Hash, Hasher};
20
21/// A `String` that's case-insensitive for all environment variable names, used for environment variable names.
22#[derive(Clone, derive_more::Debug, Serialize, Deserialize)]
23#[debug("{_0:?}")]
24pub struct EnvName(pub(crate) String);
25
26impl<T: Into<String>> From<T> for EnvName {
27    fn from(name: T) -> Self {
28        EnvName(name.into())
29    }
30}
31
32impl AsRef<str> for EnvName {
33    fn as_ref(&self) -> &str {
34        &self.0
35    }
36}
37
38impl PartialEq<Self> for EnvName {
39    fn eq(&self, other: &Self) -> bool {
40        // All environment variables are case-insensitive on all platforms
41        self.0.eq_ignore_ascii_case(&other.0)
42    }
43}
44
45impl Eq for EnvName {}
46
47impl Hash for EnvName {
48    fn hash<H: Hasher>(&self, state: &mut H) {
49        // All environment variables are hashed case-insensitively on all platforms
50        self.hash_case_insensitive(state);
51    }
52}
53
54impl EnvName {
55    /// Get the inner string
56    pub fn into_string(self) -> String {
57        self.0
58    }
59
60    /// Get a reference to the inner string
61    pub fn as_str(&self) -> &str {
62        &self.0
63    }
64
65    /// Hash the name case-insensitively by uppercasing each byte
66    fn hash_case_insensitive<H: Hasher>(&self, state: &mut H) {
67        for &b in self.0.as_bytes() {
68            b.to_ascii_uppercase().hash(state);
69        }
70    }
71}
72
73#[test]
74fn test_env_name_case_insensitive() {
75    // On all platforms, all environment variables are case-insensitive
76    let strings1 = [
77        EnvName::from("abc"),
78        EnvName::from("Abc"),
79        EnvName::from("aBc"),
80        EnvName::from("abC"),
81        EnvName::from("ABc"),
82        EnvName::from("aBC"),
83        EnvName::from("AbC"),
84        EnvName::from("ABC"),
85    ];
86    let strings2 = [
87        EnvName::from("xyz"),
88        EnvName::from("Xyz"),
89        EnvName::from("xYz"),
90        EnvName::from("xyZ"),
91        EnvName::from("XYz"),
92        EnvName::from("xYZ"),
93        EnvName::from("XyZ"),
94        EnvName::from("XYZ"),
95    ];
96    // All the strings in `strings1` compare equal to each other and hash the same.
97    for s1 in &strings1 {
98        for also_s1 in &strings1 {
99            assert_eq!(s1, also_s1);
100            let mut hash_set = std::collections::HashSet::new();
101            hash_set.insert(s1);
102            hash_set.insert(also_s1);
103            assert_eq!(hash_set.len(), 1);
104        }
105    }
106    // Same for `strings2`.
107    for s2 in &strings2 {
108        for also_s2 in &strings2 {
109            assert_eq!(s2, also_s2);
110            let mut hash_set = std::collections::HashSet::new();
111            hash_set.insert(s2);
112            hash_set.insert(also_s2);
113            assert_eq!(hash_set.len(), 1);
114        }
115    }
116    // But nothing in `strings1` compares equal to anything in `strings2`. We also assert that
117    // their hashes are distinct. In theory they could collide, but using DefaultHasher here (which
118    // is initialized with the zero key) should prevent that from happening randomly.
119    for s1 in &strings1 {
120        for s2 in &strings2 {
121            assert_ne!(s1, s2);
122            let mut hasher1 = std::hash::DefaultHasher::new();
123            s1.hash(&mut hasher1);
124            let mut hasher2 = std::hash::DefaultHasher::new();
125            s2.hash(&mut hasher2);
126            assert_ne!(hasher1.finish(), hasher2.finish());
127        }
128    }
129}