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