1use 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#[derive(Clone)]
23pub struct SourceConfigMap<'gctx> {
24 cfgs: HashMap<String, SourceConfig>,
26 id2name: HashMap<SourceId, String>,
28 overlays: HashMap<SourceId, SourceId>,
30 gctx: &'gctx GlobalContext,
31}
32
33#[derive(Debug, serde::Deserialize)]
35#[serde(rename_all = "kebab-case")]
36struct SourceConfigDef {
37 replace_with: OptValue<String>,
39 directory: Option<ConfigRelativePath>,
41 registry: OptValue<String>,
43 local_registry: Option<ConfigRelativePath>,
45 git: OptValue<String>,
47 branch: OptValue<String>,
49 tag: OptValue<String>,
51 rev: OptValue<String>,
53}
54
55#[derive(Clone)]
63struct SourceConfig {
64 id: SourceId,
67
68 replace_with: Option<(String, String)>,
74}
75
76impl<'gctx> SourceConfigMap<'gctx> {
77 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 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 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 pub fn gctx(&self) -> &'gctx GlobalContext {
141 self.gctx
142 }
143
144 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 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 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 fn add(&mut self, name: &str, cfg: SourceConfig) -> CargoResult<()> {
253 if let Some(old_name) = self.id2name.insert(cfg.id, name.to_string()) {
254 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 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(®istry, &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}