blob: da9cd5e641c37a246cb95d17247b094e8f783831 [file] [log] [blame]
//! Encapsulates snapshotting of environment variables.
use std::collections::HashMap;
use std::ffi::{OsStr, OsString};
use crate::util::errors::CargoResult;
use anyhow::{anyhow, bail};
/// Generate `case_insensitive_env` and `normalized_env` from the `env`.
fn make_case_insensitive_and_normalized_env(
env: &HashMap<OsString, OsString>,
) -> (HashMap<String, String>, HashMap<String, String>) {
let case_insensitive_env: HashMap<_, _> = env
.keys()
.filter_map(|k| k.to_str())
.map(|k| (k.to_uppercase(), k.to_owned()))
.collect();
let normalized_env = env
.iter()
// Only keep entries where both the key and value are valid UTF-8,
// since the config env vars only support UTF-8 keys and values.
// Otherwise, the normalized map warning could incorrectly warn about entries that can't be
// read by the config system.
// Please see the docs for `Env` for more context.
.filter_map(|(k, v)| Some((k.to_str()?, v.to_str()?)))
.map(|(k, _)| (k.to_uppercase().replace("-", "_"), k.to_owned()))
.collect();
(case_insensitive_env, normalized_env)
}
/// A snapshot of the environment variables available to [`GlobalContext`].
///
/// Currently, the [`GlobalContext`] supports lookup of environment variables
/// through two different means:
///
/// - [`GlobalContext::get_env`](super::GlobalContext::get_env)
/// and [`GlobalContext::get_env_os`](super::GlobalContext::get_env_os)
/// for process environment variables (similar to [`std::env::var`] and [`std::env::var_os`]),
/// - Typed Config Value API via [`GlobalContext::get`](super::GlobalContext::get).
/// This is only available for `CARGO_` prefixed environment keys.
///
/// This type contains the env var snapshot and helper methods for both APIs.
///
/// [`GlobalContext`]: super::GlobalContext
#[derive(Debug)]
pub struct Env {
/// A snapshot of the process's environment variables.
env: HashMap<OsString, OsString>,
/// Used in the typed Config value API for warning messages when config keys are
/// given in the wrong format.
///
/// Maps from "normalized" (upper case and with "-" replaced by "_") env keys
/// to the actual keys in the environment.
/// The normalized format is the one expected by Cargo.
///
/// This only holds env keys that are valid UTF-8, since [`super::ConfigKey`] only supports UTF-8 keys.
/// In addition, this only holds env keys whose value in the environment is also valid UTF-8,
/// since the typed Config value API only supports UTF-8 values.
normalized_env: HashMap<String, String>,
/// Used to implement `get_env` and `get_env_os` on Windows, where env keys are case-insensitive.
///
/// Maps from uppercased env keys to the actual key in the environment.
/// For example, this might hold a pair `("PATH", "Path")`.
/// Currently only supports UTF-8 keys and values.
case_insensitive_env: HashMap<String, String>,
}
impl Env {
/// Create a new `Env` from process's environment variables.
pub fn new() -> Self {
// ALLOWED: This is the only permissible usage of `std::env::vars{_os}`
// within cargo. If you do need access to individual variables without
// interacting with the config system in [`GlobalContext`], please use
// `std::env::var{_os}` and justify the validity of the usage.
#[allow(clippy::disallowed_methods)]
let env: HashMap<_, _> = std::env::vars_os().collect();
let (case_insensitive_env, normalized_env) = make_case_insensitive_and_normalized_env(&env);
Self {
env,
case_insensitive_env,
normalized_env,
}
}
/// Set the env directly from a `HashMap`.
/// This should be used for debugging purposes only.
pub(super) fn from_map(env: HashMap<String, String>) -> Self {
let env = env.into_iter().map(|(k, v)| (k.into(), v.into())).collect();
let (case_insensitive_env, normalized_env) = make_case_insensitive_and_normalized_env(&env);
Self {
env,
case_insensitive_env,
normalized_env,
}
}
/// Returns all environment variables as an iterator,
/// keeping only entries where both the key and value are valid UTF-8.
pub fn iter_str(&self) -> impl Iterator<Item = (&str, &str)> {
self.env
.iter()
.filter_map(|(k, v)| Some((k.to_str()?, v.to_str()?)))
}
/// Returns all environment variable keys, filtering out keys that are not valid UTF-8.
pub fn keys_str(&self) -> impl Iterator<Item = &str> {
self.env.keys().filter_map(|k| k.to_str())
}
/// Get the value of environment variable `key` through the snapshot in
/// [`GlobalContext`](super::GlobalContext).
///
/// This can be used similarly to [`std::env::var_os`].
/// On Windows, we check for case mismatch since environment keys are case-insensitive.
pub fn get_env_os(&self, key: impl AsRef<OsStr>) -> Option<OsString> {
match self.env.get(key.as_ref()) {
Some(s) => Some(s.clone()),
None => {
if cfg!(windows) {
self.get_env_case_insensitive(key).cloned()
} else {
None
}
}
}
}
/// Get the value of environment variable `key` through the `self.env` snapshot.
///
/// This can be used similarly to `std::env::var`.
/// On Windows, we check for case mismatch since environment keys are case-insensitive.
pub fn get_env(&self, key: impl AsRef<OsStr>) -> CargoResult<String> {
let key = key.as_ref();
let s = self
.get_env_os(key)
.ok_or_else(|| anyhow!("{key:?} could not be found in the environment snapshot"))?;
match s.to_str() {
Some(s) => Ok(s.to_owned()),
None => bail!("environment variable value is not valid unicode: {s:?}"),
}
}
/// Performs a case-insensitive lookup of `key` in the environment.
///
/// This is relevant on Windows, where environment variables are case-insensitive.
/// Note that this only works on keys that are valid UTF-8 and it uses Unicode uppercase,
/// which may differ from the OS's notion of uppercase.
fn get_env_case_insensitive(&self, key: impl AsRef<OsStr>) -> Option<&OsString> {
let upper_case_key = key.as_ref().to_str()?.to_uppercase();
let env_key: &OsStr = self.case_insensitive_env.get(&upper_case_key)?.as_ref();
self.env.get(env_key)
}
/// Get the value of environment variable `key` as a `&str`.
/// Returns `None` if `key` is not in `self.env` or if the value is not valid UTF-8.
///
/// This is intended for use in private methods of [`GlobalContext`](super::GlobalContext),
/// and does not check for env key case mismatch.
///
/// This is case-sensitive on Windows (even though environment keys on Windows are usually
/// case-insensitive) due to an unintended regression in 1.28 (via #5552).
/// This should only affect keys used for cargo's config-system env variables (`CARGO_`
/// prefixed ones), which are currently all uppercase.
/// We may want to consider rectifying it if users report issues.
/// One thing that adds a wrinkle here is the unstable advanced-env option that *requires*
/// case-sensitive keys.
///
/// Do not use this for any other purposes.
/// Use [`Env::get_env_os`] or [`Env::get_env`] instead, which properly handle case
/// insensitivity on Windows.
pub(super) fn get_str(&self, key: impl AsRef<OsStr>) -> Option<&str> {
self.env.get(key.as_ref()).and_then(|s| s.to_str())
}
/// Check if the environment contains `key`.
///
/// This is intended for use in private methods of [`GlobalContext`](super::GlobalContext),
/// and does not check for env key case mismatch.
/// See the docstring of [`Env::get_str`] for more context.
pub(super) fn contains_key(&self, key: impl AsRef<OsStr>) -> bool {
self.env.contains_key(key.as_ref())
}
/// Looks up a normalized `key` in the `normalized_env`.
/// Returns the corresponding (non-normalized) env key if it exists, else `None`.
///
/// This is used by [`super::GlobalContext::check_environment_key_case_mismatch`].
pub(super) fn get_normalized(&self, key: &str) -> Option<&str> {
self.normalized_env.get(key).map(|s| s.as_ref())
}
}