#
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}