#
cargo/sources/
config.rs

    1//! Implementation of configuration for various sources.
    2//!
    3//! This module will parse the various `source.*` TOML configuration keys into a
    4//! structure usable by Cargo itself. Currently this is primarily used to map
    5//! sources to one another via the `replace-with` key in `.cargo/config`.
    6
    7use crate::core::{GitReference, PackageId, SourceId};
    8use crate::sources::overlay::DependencyConfusionThreatOverlaySource;
    9use crate::sources::source::Source;
    10use crate::sources::{ReplacedSource, CRATES_IO_REGISTRY};
    11use crate::util::context::{self, ConfigRelativePath, OptValue};
    12use crate::util::errors::CargoResult;
    13use crate::util::{GlobalContext, IntoUrl};
    14use anyhow::{bail, Context as _};
    15use std::collections::{HashMap, HashSet};
    16use tracing::debug;
    17use url::Url;
    18
    19/// Represents the entire [`[source]` replacement table][1] in Cargo configuration.
    20///
    21/// [1]: https://doc.rust-lang.org/nightly/cargo/reference/config.html#source
    22#[derive(Clone)]
    23pub struct SourceConfigMap<'gctx> {
    24    /// Mapping of source name to the toml configuration.
    25    cfgs: HashMap<String, SourceConfig>,
    26    /// Mapping of [`SourceId`] to the source name.
    27    id2name: HashMap<SourceId, String>,
    28    /// Mapping of sources to local registries that will be overlaid on them.
    29    overlays: HashMap<SourceId, SourceId>,
    30    gctx: &'gctx GlobalContext,
    31}
    32
    33/// Definition of a source in a config file.
    34#[derive(Debug, serde::Deserialize)]
    35#[serde(rename_all = "kebab-case")]
    36struct SourceConfigDef {
    37    /// Indicates this source should be replaced with another of the given name.
    38    replace_with: OptValue<String>,
    39    /// A directory source.
    40    directory: Option<ConfigRelativePath>,
    41    /// A registry source. Value is a URL.
    42    registry: OptValue<String>,
    43    /// A local registry source.
    44    local_registry: Option<ConfigRelativePath>,
    45    /// A git source. Value is a URL.
    46    git: OptValue<String>,
    47    /// The git branch.
    48    branch: OptValue<String>,
    49    /// The git tag.
    50    tag: OptValue<String>,
    51    /// The git revision.
    52    rev: OptValue<String>,
    53}
    54
    55/// Configuration for a particular source, found in TOML looking like:
    56///
    57/// ```toml
    58/// [source.crates-io]
    59/// registry = 'https://github.com/rust-lang/crates.io-index'
    60/// replace-with = 'foo'    # optional
    61/// ```
    62#[derive(Clone)]
    63struct SourceConfig {
    64    /// `SourceId` this source corresponds to, inferred from the various
    65    /// defined keys in the configuration.
    66    id: SourceId,
    67
    68    /// Whether or not this source is replaced with another.
    69    ///
    70    /// This field is a tuple of `(name, location)` where `location` is where
    71    /// this configuration key was defined (such as the `.cargo/config` path
    72    /// or the environment variable name).
    73    replace_with: Option<(String, String)>,
    74}
    75
    76impl<'gctx> SourceConfigMap<'gctx> {
    77    /// Like [`SourceConfigMap::empty`] but includes sources from source
    78    /// replacement configurations.
    79    pub fn new(gctx: &'gctx GlobalContext) -> CargoResult<SourceConfigMap<'gctx>> {
    80        let mut base = SourceConfigMap::empty(gctx)?;
    81        let sources: Option<HashMap<String, SourceConfigDef>> = gctx.get("source")?;
    82        if let Some(sources) = sources {
    83            for (key, value) in sources.into_iter() {
    84                base.add_config(key, value)?;
    85            }
    86        }
    87
    88        Ok(base)
    89    }
    90
    91    /// Like [`SourceConfigMap::new`] but includes sources from source
    92    /// replacement configurations.
    93    pub fn new_with_overlays(
    94        gctx: &'gctx GlobalContext,
    95        overlays: impl IntoIterator<Item = (SourceId, SourceId)>,
    96    ) -> CargoResult<SourceConfigMap<'gctx>> {
    97        let mut base = SourceConfigMap::new(gctx)?;
    98        base.overlays = overlays.into_iter().collect();
    99        Ok(base)
    100    }
    101
    102    /// Creates the default set of sources that doesn't take `[source]`
    103    /// replacement into account.
    104    pub fn empty(gctx: &'gctx GlobalContext) -> CargoResult<SourceConfigMap<'gctx>> {
    105        let mut base = SourceConfigMap {
    106            cfgs: HashMap::new(),
    107            id2name: HashMap::new(),
    108            overlays: HashMap::new(),
    109            gctx,
    110        };
    111        base.add(
    112            CRATES_IO_REGISTRY,
    113            SourceConfig {
    114                id: SourceId::crates_io(gctx)?,
    115                replace_with: None,
    116            },
    117        )?;
    118        if SourceId::crates_io_is_sparse(gctx)? {
    119            base.add(
    120                CRATES_IO_REGISTRY,
    121                SourceConfig {
    122                    id: SourceId::crates_io_maybe_sparse_http(gctx)?,
    123                    replace_with: None,
    124                },
    125            )?;
    126        }
    127        if let Ok(url) = gctx.get_env("__CARGO_TEST_CRATES_IO_URL_DO_NOT_USE_THIS") {
    128            base.add(
    129                CRATES_IO_REGISTRY,
    130                SourceConfig {
    131                    id: SourceId::for_alt_registry(&url.parse()?, CRATES_IO_REGISTRY)?,
    132                    replace_with: None,
    133                },
    134            )?;
    135        }
    136        Ok(base)
    137    }
    138
    139    /// Returns the [`GlobalContext`] this source config map is associated with.
    140    pub fn gctx(&self) -> &'gctx GlobalContext {
    141        self.gctx
    142    }
    143
    144    /// Gets the [`Source`] for a given [`SourceId`].
    145    ///
    146    /// * `yanked_whitelist` --- Packages allowed to be used, even if they are yanked.
    147    pub fn load(
    148        &self,
    149        id: SourceId,
    150        yanked_whitelist: &HashSet<PackageId>,
    151    ) -> CargoResult<Box<dyn Source + 'gctx>> {
    152        debug!("loading: {}", id);
    153
    154        let Some(mut name) = self.id2name.get(&id) else {
    155            return self.load_overlaid(id, yanked_whitelist);
    156        };
    157        let mut cfg_loc = "";
    158        let orig_name = name;
    159        let new_id = loop {
    160            let Some(cfg) = self.cfgs.get(name) else {
    161                // Attempt to interpret the source name as an alt registry name
    162                if let Ok(alt_id) = SourceId::alt_registry(self.gctx, name) {
    163                    debug!("following pointer to registry {}", name);
    164                    break alt_id.with_precise_from(id);
    165                }
    166                bail!(
    167                    "could not find a configured source with the \
    168                     name `{}` when attempting to lookup `{}` \
    169                     (configuration in `{}`)",
    170                    name,
    171                    orig_name,
    172                    cfg_loc
    173                );
    174            };
    175            match &cfg.replace_with {
    176                Some((s, c)) => {
    177                    name = s;
    178                    cfg_loc = c;
    179                }
    180                None if id == cfg.id => return self.load_overlaid(id, yanked_whitelist),
    181                None => {
    182                    break cfg.id.with_precise_from(id);
    183                }
    184            }
    185            debug!("following pointer to {}", name);
    186            if name == orig_name {
    187                bail!(
    188                    "detected a cycle of `replace-with` sources, the source \
    189                     `{}` is eventually replaced with itself \
    190                     (configuration in `{}`)",
    191                    name,
    192                    cfg_loc
    193                )
    194            }
    195        };
    196
    197        let new_src = self.load_overlaid(
    198            new_id,
    199            &yanked_whitelist
    200                .iter()
    201                .map(|p| p.map_source(id, new_id))
    202                .collect(),
    203        )?;
    204        let old_src = id.load(self.gctx, yanked_whitelist)?;
    205        if !new_src.supports_checksums() && old_src.supports_checksums() {
    206            bail!(
    207                "\
    208cannot replace `{orig}` with `{name}`, the source `{orig}` supports \
    209checksums, but `{name}` does not
    210
    211a lock file compatible with `{orig}` cannot be generated in this situation
    212",
    213                orig = orig_name,
    214                name = name
    215            );
    216        }
    217
    218        if old_src.requires_precise() && !id.has_precise() {
    219            bail!(
    220                "\
    221the source {orig} requires a lock file to be present first before it can be
    222used against vendored source code
    223
    224remove the source replacement configuration, generate a lock file, and then
    225restore the source replacement configuration to continue the build
    226",
    227                orig = orig_name
    228            );
    229        }
    230
    231        Ok(Box::new(ReplacedSource::new(id, new_id, new_src)))
    232    }
    233
    234    /// Gets the [`Source`] for a given [`SourceId`] without performing any source replacement.
    235    fn load_overlaid(
    236        &self,
    237        id: SourceId,
    238        yanked_whitelist: &HashSet<PackageId>,
    239    ) -> CargoResult<Box<dyn Source + 'gctx>> {
    240        let src = id.load(self.gctx, yanked_whitelist)?;
    241        if let Some(overlay_id) = self.overlays.get(&id) {
    242            let overlay = overlay_id.load(self.gctx(), yanked_whitelist)?;
    243            Ok(Box::new(DependencyConfusionThreatOverlaySource::new(
    244                overlay, src,
    245            )))
    246        } else {
    247            Ok(src)
    248        }
    249    }
    250
    251    /// Adds a source config with an associated name.
    252    fn add(&mut self, name: &str, cfg: SourceConfig) -> CargoResult<()> {
    253        if let Some(old_name) = self.id2name.insert(cfg.id, name.to_string()) {
    254            // The user is allowed to redefine the built-in crates-io
    255            // definition from `empty()`.
    256            if name != CRATES_IO_REGISTRY {
    257                bail!(
    258                    "source `{}` defines source {}, but that source is already defined by `{}`\n\
    259                     note: Sources are not allowed to be defined multiple times.",
    260                    name,
    261                    cfg.id,
    262                    old_name
    263                );
    264            }
    265        }
    266        self.cfgs.insert(name.to_string(), cfg);
    267        Ok(())
    268    }
    269
    270    /// Adds a source config from TOML definition.
    271    fn add_config(&mut self, name: String, def: SourceConfigDef) -> CargoResult<()> {
    272        let mut srcs = Vec::new();
    273        if let Some(registry) = def.registry {
    274            let url = url(&registry, &format!("source.{}.registry", name))?;
    275            srcs.push(SourceId::for_source_replacement_registry(&url, &name)?);
    276        }
    277        if let Some(local_registry) = def.local_registry {
    278            let path = local_registry.resolve_path(self.gctx);
    279            srcs.push(SourceId::for_local_registry(&path)?);
    280        }
    281        if let Some(directory) = def.directory {
    282            let path = directory.resolve_path(self.gctx);
    283            srcs.push(SourceId::for_directory(&path)?);
    284        }
    285        if let Some(git) = def.git {
    286            let url = url(&git, &format!("source.{}.git", name))?;
    287            let reference = match def.branch {
    288                Some(b) => GitReference::Branch(b.val),
    289                None => match def.tag {
    290                    Some(b) => GitReference::Tag(b.val),
    291                    None => match def.rev {
    292                        Some(b) => GitReference::Rev(b.val),
    293                        None => GitReference::DefaultBranch,
    294                    },
    295                },
    296            };
    297            srcs.push(SourceId::for_git(&url, reference)?);
    298        } else {
    299            let check_not_set = |key, v: OptValue<String>| {
    300                if let Some(val) = v {
    301                    bail!(
    302                        "source definition `source.{}` specifies `{}`, \
    303                         but that requires a `git` key to be specified (in {})",
    304                        name,
    305                        key,
    306                        val.definition
    307                    );
    308                }
    309                Ok(())
    310            };
    311            check_not_set("branch", def.branch)?;
    312            check_not_set("tag", def.tag)?;
    313            check_not_set("rev", def.rev)?;
    314        }
    315        if name == CRATES_IO_REGISTRY && srcs.is_empty() {
    316            srcs.push(SourceId::crates_io_maybe_sparse_http(self.gctx)?);
    317        }
    318
    319        match srcs.len() {
    320            0 => bail!(
    321                "no source location specified for `source.{}`, need \
    322                 `registry`, `local-registry`, `directory`, or `git` defined",
    323                name
    324            ),
    325            1 => {}
    326            _ => bail!(
    327                "more than one source location specified for `source.{}`",
    328                name
    329            ),
    330        }
    331        let src = srcs[0];
    332
    333        let replace_with = def
    334            .replace_with
    335            .map(|val| (val.val, val.definition.to_string()));
    336
    337        self.add(
    338            &name,
    339            SourceConfig {
    340                id: src,
    341                replace_with,
    342            },
    343        )?;
    344
    345        return Ok(());
    346
    347        fn url(val: &context::Value<String>, key: &str) -> CargoResult<Url> {
    348            let url = val.val.into_url().with_context(|| {
    349                format!(
    350                    "configuration key `{}` specified an invalid \
    351                     URL (in {})",
    352                    key, val.definition
    353                )
    354            })?;
    355
    356            Ok(url)
    357        }
    358    }
    359}