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}