cargo/util/context/environment.rs
1//! Encapsulates snapshotting of environment variables.
2
3use std::collections::HashMap;
4use std::ffi::{OsStr, OsString};
5
6use crate::util::errors::CargoResult;
7use anyhow::{anyhow, bail};
8
9/// Generate `case_insensitive_env` and `normalized_env` from the `env`.
10fn make_case_insensitive_and_normalized_env(
11 env: &HashMap<OsString, OsString>,
12) -> (HashMap<String, String>, HashMap<String, String>) {
13 let case_insensitive_env: HashMap<_, _> = env
14 .keys()
15 .filter_map(|k| k.to_str())
16 .map(|k| (k.to_uppercase(), k.to_owned()))
17 .collect();
18
19 let normalized_env = env
20 .iter()
21 // Only keep entries where both the key and value are valid UTF-8,
22 // since the config env vars only support UTF-8 keys and values.
23 // Otherwise, the normalized map warning could incorrectly warn about entries that can't be
24 // read by the config system.
25 // Please see the docs for `Env` for more context.
26 .filter_map(|(k, v)| Some((k.to_str()?, v.to_str()?)))
27 .map(|(k, _)| (k.to_uppercase().replace("-", "_"), k.to_owned()))
28 .collect();
29 (case_insensitive_env, normalized_env)
30}
31
32/// A snapshot of the environment variables available to [`GlobalContext`].
33///
34/// Currently, the [`GlobalContext`] supports lookup of environment variables
35/// through two different means:
36///
37/// - [`GlobalContext::get_env`](super::GlobalContext::get_env)
38/// and [`GlobalContext::get_env_os`](super::GlobalContext::get_env_os)
39/// for process environment variables (similar to [`std::env::var`] and [`std::env::var_os`]),
40/// - Typed Config Value API via [`GlobalContext::get`](super::GlobalContext::get).
41/// This is only available for `CARGO_` prefixed environment keys.
42///
43/// This type contains the env var snapshot and helper methods for both APIs.
44///
45/// [`GlobalContext`]: super::GlobalContext
46#[derive(Debug)]
47pub struct Env {
48 /// A snapshot of the process's environment variables.
49 env: HashMap<OsString, OsString>,
50 /// Used in the typed Config value API for warning messages when config keys are
51 /// given in the wrong format.
52 ///
53 /// Maps from "normalized" (upper case and with "-" replaced by "_") env keys
54 /// to the actual keys in the environment.
55 /// The normalized format is the one expected by Cargo.
56 ///
57 /// This only holds env keys that are valid UTF-8, since [`super::ConfigKey`] only supports UTF-8 keys.
58 /// In addition, this only holds env keys whose value in the environment is also valid UTF-8,
59 /// since the typed Config value API only supports UTF-8 values.
60 normalized_env: HashMap<String, String>,
61 /// Used to implement `get_env` and `get_env_os` on Windows, where env keys are case-insensitive.
62 ///
63 /// Maps from uppercased env keys to the actual key in the environment.
64 /// For example, this might hold a pair `("PATH", "Path")`.
65 /// Currently only supports UTF-8 keys and values.
66 case_insensitive_env: HashMap<String, String>,
67}
68
69impl Env {
70 /// Create a new `Env` from process's environment variables.
71 pub fn new() -> Self {
72 // ALLOWED: This is the only permissible usage of `std::env::vars{_os}`
73 // within cargo. If you do need access to individual variables without
74 // interacting with the config system in [`GlobalContext`], please use
75 // `std::env::var{_os}` and justify the validity of the usage.
76 #[allow(clippy::disallowed_methods)]
77 let env: HashMap<_, _> = std::env::vars_os().collect();
78 let (case_insensitive_env, normalized_env) = make_case_insensitive_and_normalized_env(&env);
79 Self {
80 env,
81 case_insensitive_env,
82 normalized_env,
83 }
84 }
85
86 /// Set the env directly from a `HashMap`.
87 /// This should be used for debugging purposes only.
88 pub(super) fn from_map(env: HashMap<String, String>) -> Self {
89 let env = env.into_iter().map(|(k, v)| (k.into(), v.into())).collect();
90 let (case_insensitive_env, normalized_env) = make_case_insensitive_and_normalized_env(&env);
91 Self {
92 env,
93 case_insensitive_env,
94 normalized_env,
95 }
96 }
97
98 /// Returns all environment variables as an iterator,
99 /// keeping only entries where both the key and value are valid UTF-8.
100 pub fn iter_str(&self) -> impl Iterator<Item = (&str, &str)> {
101 self.env
102 .iter()
103 .filter_map(|(k, v)| Some((k.to_str()?, v.to_str()?)))
104 }
105
106 /// Returns all environment variable keys, filtering out keys that are not valid UTF-8.
107 pub fn keys_str(&self) -> impl Iterator<Item = &str> {
108 self.env.keys().filter_map(|k| k.to_str())
109 }
110
111 /// Get the value of environment variable `key` through the snapshot in
112 /// [`GlobalContext`](super::GlobalContext).
113 ///
114 /// This can be used similarly to [`std::env::var_os`].
115 /// On Windows, we check for case mismatch since environment keys are case-insensitive.
116 pub fn get_env_os(&self, key: impl AsRef<OsStr>) -> Option<&OsStr> {
117 match self.env.get(key.as_ref()) {
118 Some(s) => Some(s),
119 None => {
120 if cfg!(windows) {
121 self.get_env_case_insensitive(key)
122 } else {
123 None
124 }
125 }
126 }
127 }
128
129 /// Get the value of environment variable `key` through the `self.env` snapshot.
130 ///
131 /// This can be used similarly to `std::env::var`.
132 /// On Windows, we check for case mismatch since environment keys are case-insensitive.
133 pub fn get_env(&self, key: impl AsRef<OsStr>) -> CargoResult<&str> {
134 let key = key.as_ref();
135 let s = self
136 .get_env_os(key)
137 .ok_or_else(|| anyhow!("{key:?} could not be found in the environment snapshot"))?;
138
139 match s.to_str() {
140 Some(s) => Ok(s),
141 None => bail!("environment variable value is not valid unicode: {s:?}"),
142 }
143 }
144
145 /// Performs a case-insensitive lookup of `key` in the environment.
146 ///
147 /// This is relevant on Windows, where environment variables are case-insensitive.
148 /// Note that this only works on keys that are valid UTF-8 and it uses Unicode uppercase,
149 /// which may differ from the OS's notion of uppercase.
150 fn get_env_case_insensitive(&self, key: impl AsRef<OsStr>) -> Option<&OsStr> {
151 let upper_case_key = key.as_ref().to_str()?.to_uppercase();
152 let env_key: &OsStr = self.case_insensitive_env.get(&upper_case_key)?.as_ref();
153 self.env.get(env_key).map(|v| v.as_ref())
154 }
155
156 /// Get the value of environment variable `key` as a `&str`.
157 /// Returns `None` if `key` is not in `self.env` or if the value is not valid UTF-8.
158 ///
159 /// This is intended for use in private methods of [`GlobalContext`](super::GlobalContext),
160 /// and does not check for env key case mismatch.
161 ///
162 /// This is case-sensitive on Windows (even though environment keys on Windows are usually
163 /// case-insensitive) due to an unintended regression in 1.28 (via #5552).
164 /// This should only affect keys used for cargo's config-system env variables (`CARGO_`
165 /// prefixed ones), which are currently all uppercase.
166 /// We may want to consider rectifying it if users report issues.
167 /// One thing that adds a wrinkle here is the unstable advanced-env option that *requires*
168 /// case-sensitive keys.
169 ///
170 /// Do not use this for any other purposes.
171 /// Use [`Env::get_env_os`] or [`Env::get_env`] instead, which properly handle case
172 /// insensitivity on Windows.
173 pub(super) fn get_str(&self, key: impl AsRef<OsStr>) -> Option<&str> {
174 self.env.get(key.as_ref()).and_then(|s| s.to_str())
175 }
176
177 /// Check if the environment contains `key`.
178 ///
179 /// This is intended for use in private methods of [`GlobalContext`](super::GlobalContext),
180 /// and does not check for env key case mismatch.
181 /// See the docstring of [`Env::get_str`] for more context.
182 pub(super) fn contains_key(&self, key: impl AsRef<OsStr>) -> bool {
183 self.env.contains_key(key.as_ref())
184 }
185
186 /// Looks up a normalized `key` in the `normalized_env`.
187 /// Returns the corresponding (non-normalized) env key if it exists, else `None`.
188 ///
189 /// This is used by [`super::GlobalContext::check_environment_key_case_mismatch`].
190 pub(super) fn get_normalized(&self, key: &str) -> Option<&str> {
191 self.normalized_env.get(key).map(|s| s.as_ref())
192 }
193}