Skip to content

Commit f810234

Browse files
committed
factor out three hashmaps in Config into a new struct for better encapsulation
1 parent 35b53b8 commit f810234

File tree

3 files changed

+169
-97
lines changed

3 files changed

+169
-97
lines changed

src/cargo/util/config/de.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ impl<'config> ValueDeserializer<'config> {
424424
let definition = {
425425
let env = de.key.as_env_key();
426426
let env_def = Definition::Environment(env.to_string());
427-
match (de.config.env_has_key(env), de.config.get_cv(&de.key)?) {
427+
match (de.config.env.contains_key(env), de.config.get_cv(&de.key)?) {
428428
(true, Some(cv)) => {
429429
// Both, pick highest priority.
430430
if env_def.is_higher_priority(cv.definition()) {

src/cargo/util/config/environment.rs

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
//! Encapsulates snapshotting of environment variables.
2+
3+
use std::collections::HashMap;
4+
use std::ffi::{OsStr, OsString};
5+
6+
use crate::util::errors::CargoResult;
7+
use anyhow::{anyhow, bail};
8+
9+
/// Generate `case_insensitive_env` and `normalized_env` from the `env`.
10+
fn 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+
let normalized_env = env
19+
.iter()
20+
// Only keep entries where both the key and value are valid UTF-8
21+
.filter_map(|(k, v)| Some((k.to_str()?, v.to_str()?)))
22+
.map(|(k, _)| (k.to_uppercase().replace("-", "_"), k.to_owned()))
23+
.collect();
24+
(case_insensitive_env, normalized_env)
25+
}
26+
27+
#[derive(Debug)]
28+
pub struct Env {
29+
/// A snapshot of the process's environment variables.
30+
env: HashMap<OsString, OsString>,
31+
/// A map from normalized (upper case and with "-" replaced by "_") env keys to the actual key
32+
/// in the environment.
33+
/// The "normalized" key is the format expected by Cargo.
34+
/// This is used to warn users when env keys are not provided in this format.
35+
normalized_env: HashMap<String, String>,
36+
/// A map from uppercased env keys to the actual key in the environment.
37+
/// This is relevant on Windows, where env keys are case-insensitive.
38+
/// For example, this might hold a pair `("PATH", "Path")`.
39+
case_insensitive_env: HashMap<String, String>,
40+
}
41+
42+
impl Env {
43+
/// Create a new `Env` from process's environment variables.
44+
pub fn new() -> Self {
45+
let env: HashMap<_, _> = std::env::vars_os().collect();
46+
let (case_insensitive_env, normalized_env) = make_case_insensitive_and_normalized_env(&env);
47+
Self {
48+
env,
49+
case_insensitive_env,
50+
normalized_env,
51+
}
52+
}
53+
54+
/// Set the env directly from a `HashMap`.
55+
/// This should be used for debugging purposes only.
56+
pub(super) fn from_map(env: HashMap<String, String>) -> Self {
57+
let env = env.into_iter().map(|(k, v)| (k.into(), v.into())).collect();
58+
let (case_insensitive_env, normalized_env) = make_case_insensitive_and_normalized_env(&env);
59+
Self {
60+
env,
61+
case_insensitive_env,
62+
normalized_env,
63+
}
64+
}
65+
66+
/// Returns all environment variables as an iterator,
67+
/// keeping only entries where both the key and value are valid UTF-8.
68+
pub fn iter_str(&self) -> impl Iterator<Item = (&str, &str)> {
69+
self.env
70+
.iter()
71+
.filter_map(|(k, v)| Some((k.to_str()?, v.to_str()?)))
72+
}
73+
74+
/// Returns all environment variable keys, filtering out keys that are not valid UTF-8.
75+
pub fn keys_str(&self) -> impl Iterator<Item = &str> {
76+
self.env.keys().filter_map(|k| k.to_str())
77+
}
78+
79+
/// Get the value of environment variable `key` through the `Config` snapshot.
80+
///
81+
/// This can be used similarly to `std::env::var_os`.
82+
/// On Windows, we check for case mismatch since environment keys are case-insensitive.
83+
pub fn get_env_os(&self, key: impl AsRef<OsStr>) -> Option<OsString> {
84+
match self.env.get(key.as_ref()) {
85+
Some(s) => Some(s.clone()),
86+
None => {
87+
if cfg!(windows) {
88+
self.get_env_case_insensitive(key).cloned()
89+
} else {
90+
None
91+
}
92+
}
93+
}
94+
}
95+
96+
/// Get the value of environment variable `key` through the `self.env` snapshot.
97+
///
98+
/// This can be used similarly to `std::env::var`.
99+
/// On Windows, we check for case mismatch since environment keys are case-insensitive.
100+
pub fn get_env(&self, key: impl AsRef<OsStr>) -> CargoResult<String> {
101+
let key = key.as_ref();
102+
let s = self
103+
.get_env_os(key)
104+
.ok_or_else(|| anyhow!("{key:?} could not be found in the environment snapshot"))?;
105+
106+
match s.to_str() {
107+
Some(s) => Ok(s.to_owned()),
108+
None => bail!("environment variable value is not valid unicode: {s:?}"),
109+
}
110+
}
111+
112+
/// Performs a case-insensitive lookup of `key` in the environment.
113+
///
114+
/// This is relevant on Windows, where environment variables are case-insensitive.
115+
/// Note that this only works on keys that are valid UTF-8 and it uses Unicode uppercase,
116+
/// which may differ from the OS's notion of uppercase.
117+
fn get_env_case_insensitive(&self, key: impl AsRef<OsStr>) -> Option<&OsString> {
118+
let upper_case_key = key.as_ref().to_str()?.to_uppercase();
119+
let env_key: &OsStr = self.case_insensitive_env.get(&upper_case_key)?.as_ref();
120+
self.env.get(env_key)
121+
}
122+
123+
/// Get the value of environment variable `key` as a `&str`.
124+
/// Returns `None` if `key` is not in `self.env` or if the value is not valid UTF-8.
125+
///
126+
/// This is intended for use in private methods of `Config`,
127+
/// and does not check for env key case mismatch.
128+
pub(super) fn get_str(&self, key: impl AsRef<OsStr>) -> Option<&str> {
129+
self.env.get(key.as_ref()).and_then(|s| s.to_str())
130+
}
131+
132+
/// Check if the environment contains `key`.
133+
///
134+
/// This is intended for use in private methods of `Config`,
135+
/// and does not check for env key case mismatch.
136+
pub(super) fn contains_key(&self, key: impl AsRef<OsStr>) -> bool {
137+
self.env.contains_key(key.as_ref())
138+
}
139+
140+
/// Looks up a normalized `key` in the `normalized_env`.
141+
/// Returns the corresponding (non-normalized) env key if it exists, else `None`.
142+
///
143+
/// This is used by `Config::check_environment_key_mismatch`.
144+
pub(super) fn get_normalized(&self, key: &str) -> Option<&str> {
145+
self.normalized_env.get(key).map(|s| s.as_ref())
146+
}
147+
}

src/cargo/util/config/mod.rs

Lines changed: 21 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ pub use path::{ConfigRelativePath, PathAndArgs};
100100
mod target;
101101
pub use target::{TargetCfgConfig, TargetConfig};
102102

103+
mod environment;
104+
use environment::Env;
105+
103106
// Helper macro for creating typed access methods.
104107
macro_rules! get_value_typed {
105108
($name:ident, $ty:ty, $variant:ident, $expected:expr) => {
@@ -124,30 +127,6 @@ macro_rules! get_value_typed {
124127
};
125128
}
126129

127-
/// Generate `case_insensitive_env` and `normalized_env` from the `env`.
128-
fn make_case_insensitive_and_normalized_env(
129-
env: &HashMap<OsString, OsString>,
130-
) -> (HashMap<String, String>, HashMap<String, String>) {
131-
// See `Config.case_insensitive_env`.
132-
// Maps from uppercased key to actual environment key.
133-
// For example, `"PATH" => "Path"`.
134-
let case_insensitive_env: HashMap<_, _> = env
135-
.keys()
136-
.filter_map(|k| k.to_str())
137-
.map(|k| (k.to_uppercase(), k.to_owned()))
138-
.collect();
139-
// See `Config.normalized_env`.
140-
// Maps from normalized (uppercased with "-" replaced by "_") key
141-
// to actual environment key. For example, `"MY_KEY" => "my-key"`.
142-
let normalized_env = env
143-
.iter()
144-
// Only keep entries where both the key and value are valid UTF-8
145-
.filter_map(|(k, v)| Some((k.to_str()?, v.to_str()?)))
146-
.map(|(k, _)| (k.to_uppercase().replace("-", "_"), k.to_owned()))
147-
.collect();
148-
(case_insensitive_env, normalized_env)
149-
}
150-
151130
/// Indicates why a config value is being loaded.
152131
#[derive(Clone, Copy, Debug)]
153132
enum WhyLoad {
@@ -223,15 +202,8 @@ pub struct Config {
223202
creation_time: Instant,
224203
/// Target Directory via resolved Cli parameter
225204
target_dir: Option<Filesystem>,
226-
/// Environment variables, separated to assist testing.
227-
env: HashMap<OsString, OsString>,
228-
/// Environment variables converted to uppercase to check for case mismatch
229-
/// (relevant on Windows, where environment variables are case-insensitive).
230-
case_insensitive_env: HashMap<String, String>,
231-
/// Environment variables converted to uppercase and with "-" replaced by "_"
232-
/// (the format expected by Cargo). This only contains entries where the key and variable are
233-
/// both valid UTF-8.
234-
normalized_env: HashMap<String, String>,
205+
/// Environment variable snapshot.
206+
env: Env,
235207
/// Tracks which sources have been updated to avoid multiple updates.
236208
updated_sources: LazyCell<RefCell<HashSet<SourceId>>>,
237209
/// Cache of credentials from configuration or credential providers.
@@ -289,11 +261,10 @@ impl Config {
289261
}
290262
});
291263

292-
let env: HashMap<_, _> = env::vars_os().collect();
293-
let (case_insensitive_env, normalized_env) = make_case_insensitive_and_normalized_env(&env);
264+
let env = Env::new();
294265

295-
let cache_key: &OsStr = "CARGO_CACHE_RUSTC_INFO".as_ref();
296-
let cache_rustc_info = match env.get(cache_key) {
266+
let cache_key = "CARGO_CACHE_RUSTC_INFO";
267+
let cache_rustc_info = match env.get_env_os(cache_key) {
297268
Some(cache) => cache != "0",
298269
_ => true,
299270
};
@@ -327,8 +298,6 @@ impl Config {
327298
creation_time: Instant::now(),
328299
target_dir: None,
329300
env,
330-
case_insensitive_env,
331-
normalized_env,
332301
updated_sources: LazyCell::new(),
333302
credential_cache: LazyCell::new(),
334303
package_cache_lock: RefCell::new(None),
@@ -683,7 +652,7 @@ impl Config {
683652
// Root table can't have env value.
684653
return Ok(cv);
685654
}
686-
let env = self.get_env_str(key.as_env_key());
655+
let env = self.env.get_str(key.as_env_key());
687656
let env_def = Definition::Environment(key.as_env_key().to_string());
688657
let use_env = match (&cv, env) {
689658
// Lists are always merged.
@@ -754,32 +723,26 @@ impl Config {
754723

755724
/// Helper primarily for testing.
756725
pub fn set_env(&mut self, env: HashMap<String, String>) {
757-
self.env = env.into_iter().map(|(k, v)| (k.into(), v.into())).collect();
758-
let (case_insensitive_env, normalized_env) =
759-
make_case_insensitive_and_normalized_env(&self.env);
760-
self.case_insensitive_env = case_insensitive_env;
761-
self.normalized_env = normalized_env;
726+
self.env = Env::from_map(env);
762727
}
763728

764-
/// Returns all environment variables as an iterator, filtering out entries
765-
/// that are not valid UTF-8.
729+
/// Returns all environment variables as an iterator,
730+
/// keeping only entries where both the key and value are valid UTF-8.
766731
pub(crate) fn env(&self) -> impl Iterator<Item = (&str, &str)> {
767-
self.env
768-
.iter()
769-
.filter_map(|(k, v)| Some((k.to_str()?, v.to_str()?)))
732+
self.env.iter_str()
770733
}
771734

772-
/// Returns all environment variable keys, filtering out entries that are not valid UTF-8.
735+
/// Returns all environment variable keys, filtering out keys that are not valid UTF-8.
773736
fn env_keys(&self) -> impl Iterator<Item = &str> {
774-
self.env.iter().filter_map(|(k, _)| k.to_str())
737+
self.env.keys_str()
775738
}
776739

777740
fn get_config_env<T>(&self, key: &ConfigKey) -> Result<OptValue<T>, ConfigError>
778741
where
779742
T: FromStr,
780743
<T as FromStr>::Err: fmt::Display,
781744
{
782-
match self.get_env_str(key.as_env_key()) {
745+
match self.env.get_str(key.as_env_key()) {
783746
Some(value) => {
784747
let definition = Definition::Environment(key.as_env_key().to_string());
785748
Ok(Some(Value {
@@ -800,59 +763,21 @@ impl Config {
800763
///
801764
/// This can be used similarly to `std::env::var`.
802765
pub fn get_env(&self, key: impl AsRef<OsStr>) -> CargoResult<String> {
803-
let key = key.as_ref();
804-
let s = self
805-
.get_env_os(key)
806-
.ok_or_else(|| anyhow!("{key:?} could not be found in the environment snapshot"))?;
807-
808-
match s.to_str() {
809-
Some(s) => Ok(s.to_owned()),
810-
None => bail!("environment variable value is not valid unicode: {s:?}"),
811-
}
766+
self.env.get_env(key)
812767
}
813768

814769
/// Get the value of environment variable `key` through the `Config` snapshot.
815770
///
816771
/// This can be used similarly to `std::env::var_os`.
817772
pub fn get_env_os(&self, key: impl AsRef<OsStr>) -> Option<OsString> {
818-
match self.env.get(key.as_ref()) {
819-
Some(s) => Some(s.clone()),
820-
None => {
821-
if cfg!(windows) {
822-
self.get_env_case_insensitive(key).cloned()
823-
} else {
824-
None
825-
}
826-
}
827-
}
828-
}
829-
830-
/// Wrapper for `self.env.get` when `key` should be case-insensitive.
831-
/// This is relevant on Windows, where environment variables are case-insensitive.
832-
/// Note that this only works on keys that are valid UTF-8.
833-
fn get_env_case_insensitive(&self, key: impl AsRef<OsStr>) -> Option<&OsString> {
834-
let upper_case_key = key.as_ref().to_str()?.to_uppercase();
835-
// `self.case_insensitive_env` holds pairs like `("PATH", "Path")`
836-
// or `("MY-VAR", "my-var")`.
837-
let env_key: &OsStr = self.case_insensitive_env.get(&upper_case_key)?.as_ref();
838-
self.env.get(env_key)
839-
}
840-
841-
/// Get the value of environment variable `key`.
842-
/// Returns `None` if `key` is not in `self.env` or if the value is not valid UTF-8.
843-
fn get_env_str(&self, key: impl AsRef<OsStr>) -> Option<&str> {
844-
self.env.get(key.as_ref()).and_then(|s| s.to_str())
845-
}
846-
847-
fn env_has_key(&self, key: impl AsRef<OsStr>) -> bool {
848-
self.env.contains_key(key.as_ref())
773+
self.env.get_env_os(key)
849774
}
850775

851776
/// Check if the [`Config`] contains a given [`ConfigKey`].
852777
///
853778
/// See `ConfigMapAccess` for a description of `env_prefix_ok`.
854779
fn has_key(&self, key: &ConfigKey, env_prefix_ok: bool) -> CargoResult<bool> {
855-
if self.env_has_key(key.as_env_key()) {
780+
if self.env.contains_key(key.as_env_key()) {
856781
return Ok(true);
857782
}
858783
if env_prefix_ok {
@@ -870,7 +795,7 @@ impl Config {
870795
}
871796

872797
fn check_environment_key_case_mismatch(&self, key: &ConfigKey) {
873-
if let Some(env_key) = self.normalized_env.get(key.as_env_key()) {
798+
if let Some(env_key) = self.env.get_normalized(key.as_env_key()) {
874799
let _ = self.shell().warn(format!(
875800
"Environment variables are expected to use uppercase letters and underscores, \
876801
the variable `{}` will be ignored and have no effect",
@@ -969,7 +894,7 @@ impl Config {
969894
key: &ConfigKey,
970895
output: &mut Vec<(String, Definition)>,
971896
) -> CargoResult<()> {
972-
let env_val = match self.get_env_str(key.as_env_key()) {
897+
let env_val = match self.env.get_str(key.as_env_key()) {
973898
Some(v) => v,
974899
None => {
975900
self.check_environment_key_case_mismatch(key);

0 commit comments

Comments
 (0)