1use std::collections::HashMap;
8
9use crate::core::GitReference;
10use crate::core::SourceId;
11use crate::sources::overlay::DependencyConfusionThreatOverlaySource;
12use crate::sources::source::Source;
13use crate::sources::{CRATES_IO_REGISTRY, ReplacedSource};
14use crate::util::context::{self, ConfigRelativePath, OptValue};
15use crate::util::errors::CargoResult;
16use crate::util::{GlobalContext, IntoUrl};
17
18use anyhow::{Context as _, bail};
19use tracing::debug;
20use url::Url;
21
22#[derive(Clone)]
26pub struct SourceConfigMap<'gctx> {
27 cfgs: HashMap<String, SourceConfig>,
29 id2name: HashMap<SourceId, String>,
31 overlays: HashMap<SourceId, SourceId>,
33 gctx: &'gctx GlobalContext,
34}
35
36#[derive(Debug, serde::Deserialize)]
38#[serde(rename_all = "kebab-case")]
39struct SourceConfigDef {
40 replace_with: OptValue<String>,
42 directory: Option<ConfigRelativePath>,
44 registry: OptValue<String>,
46 local_registry: Option<ConfigRelativePath>,
48 git: OptValue<String>,
50 branch: OptValue<String>,
52 tag: OptValue<String>,
54 rev: OptValue<String>,
56}
57
58#[derive(Clone)]
66struct SourceConfig {
67 id: SourceId,
70
71 replace_with: Option<(String, String)>,
77}
78
79impl<'gctx> SourceConfigMap<'gctx> {
80 pub fn new(gctx: &'gctx GlobalContext) -> CargoResult<SourceConfigMap<'gctx>> {
83 let mut base = SourceConfigMap::empty(gctx)?;
84 let sources: Option<HashMap<String, SourceConfigDef>> = gctx.get("source")?;
85 if let Some(sources) = sources {
86 for (key, value) in sources.into_iter() {
87 base.add_config(key, value)?;
88 }
89 }
90
91 Ok(base)
92 }
93
94 pub fn new_with_overlays(
97 gctx: &'gctx GlobalContext,
98 overlays: impl IntoIterator<Item = (SourceId, SourceId)>,
99 ) -> CargoResult<SourceConfigMap<'gctx>> {
100 let mut base = SourceConfigMap::new(gctx)?;
101 base.overlays = overlays.into_iter().collect();
102 Ok(base)
103 }
104
105 pub fn empty(gctx: &'gctx GlobalContext) -> CargoResult<SourceConfigMap<'gctx>> {
108 let mut base = SourceConfigMap {
109 cfgs: HashMap::new(),
110 id2name: HashMap::new(),
111 overlays: HashMap::new(),
112 gctx,
113 };
114 base.add(
115 CRATES_IO_REGISTRY,
116 SourceConfig {
117 id: SourceId::crates_io(gctx)?,
118 replace_with: None,
119 },
120 )?;
121 if SourceId::crates_io_is_sparse(gctx)? {
122 base.add(
123 CRATES_IO_REGISTRY,
124 SourceConfig {
125 id: SourceId::crates_io_maybe_sparse_http(gctx)?,
126 replace_with: None,
127 },
128 )?;
129 }
130 if let Ok(url) = gctx.get_env("__CARGO_TEST_CRATES_IO_URL_DO_NOT_USE_THIS") {
131 base.add(
132 CRATES_IO_REGISTRY,
133 SourceConfig {
134 id: SourceId::for_alt_registry(&url.parse()?, CRATES_IO_REGISTRY)?,
135 replace_with: None,
136 },
137 )?;
138 }
139 Ok(base)
140 }
141
142 pub fn gctx(&self) -> &'gctx GlobalContext {
144 self.gctx
145 }
146
147 pub fn load(&self, id: SourceId) -> CargoResult<Box<dyn Source + 'gctx>> {
149 debug!("loading: {}", id);
150
151 let Some(mut name) = self.id2name.get(&id) else {
152 return self.load_overlaid(id);
153 };
154 let mut cfg_loc = "";
155 let orig_name = name;
156 let new_id = loop {
157 let Some(cfg) = self.cfgs.get(name) else {
158 if let Ok(alt_id) = SourceId::alt_registry(self.gctx, name) {
160 debug!("following pointer to registry {}", name);
161 break alt_id.with_precise_from(id);
162 }
163 bail!(
164 "could not find a configured source with the \
165 name `{}` when attempting to lookup `{}` \
166 (configuration in `{}`)",
167 name,
168 orig_name,
169 cfg_loc
170 );
171 };
172 match &cfg.replace_with {
173 Some((s, c)) => {
174 name = s;
175 cfg_loc = c;
176 }
177 None if id == cfg.id => return self.load_overlaid(id),
178 None => {
179 break cfg.id.with_precise_from(id);
180 }
181 }
182 debug!("following pointer to {}", name);
183 if name == orig_name {
184 bail!(
185 "detected a cycle of `replace-with` sources, the source \
186 `{}` is eventually replaced with itself \
187 (configuration in `{}`)",
188 name,
189 cfg_loc
190 )
191 }
192 };
193
194 let new_src = self.load_overlaid(new_id)?;
195 let old_src = id.load(self.gctx)?;
196 if !new_src.supports_checksums() && old_src.supports_checksums() {
197 bail!(
198 "\
199cannot replace `{orig}` with `{name}`, the source `{orig}` supports \
200checksums, but `{name}` does not
201
202a lock file compatible with `{orig}` cannot be generated in this situation
203",
204 orig = orig_name,
205 name = name
206 );
207 }
208
209 if old_src.requires_precise() && !id.has_precise() {
210 bail!(
211 "\
212the source {orig} requires a lock file to be present first before it can be
213used against vendored source code
214
215remove the source replacement configuration, generate a lock file, and then
216restore the source replacement configuration to continue the build
217",
218 orig = orig_name
219 );
220 }
221
222 Ok(Box::new(ReplacedSource::new(id, new_id, new_src)))
223 }
224
225 fn load_overlaid(&self, id: SourceId) -> CargoResult<Box<dyn Source + 'gctx>> {
227 let src = id.load(self.gctx)?;
228 if let Some(overlay_id) = self.overlays.get(&id) {
229 let overlay = overlay_id.load(self.gctx())?;
230 Ok(Box::new(DependencyConfusionThreatOverlaySource::new(
231 overlay, src,
232 )))
233 } else {
234 Ok(src)
235 }
236 }
237
238 fn add(&mut self, name: &str, cfg: SourceConfig) -> CargoResult<()> {
240 if let Some(old_name) = self.id2name.insert(cfg.id, name.to_string()) {
241 if name != CRATES_IO_REGISTRY {
244 bail!(
245 "source `{}` defines source {}, but that source is already defined by `{}`\n\
246 note: Sources are not allowed to be defined multiple times.",
247 name,
248 cfg.id,
249 old_name
250 );
251 }
252 }
253 self.cfgs.insert(name.to_string(), cfg);
254 Ok(())
255 }
256
257 fn add_config(&mut self, name: String, def: SourceConfigDef) -> CargoResult<()> {
259 let mut srcs = Vec::new();
260 if let Some(registry) = def.registry {
261 let url = url(®istry, &format!("source.{}.registry", name))?;
262 srcs.push(SourceId::for_source_replacement_registry(&url, &name)?);
263 }
264 if let Some(local_registry) = def.local_registry {
265 let path = local_registry.resolve_path(self.gctx);
266 srcs.push(SourceId::for_local_registry(&path)?);
267 }
268 if let Some(directory) = def.directory {
269 let path = directory.resolve_path(self.gctx);
270 srcs.push(SourceId::for_directory(&path)?);
271 }
272 if let Some(git) = def.git {
273 let url = url(&git, &format!("source.{}.git", name))?;
274 let reference = match def.branch {
275 Some(b) => GitReference::Branch(b.val),
276 None => match def.tag {
277 Some(b) => GitReference::Tag(b.val),
278 None => match def.rev {
279 Some(b) => GitReference::Rev(b.val),
280 None => GitReference::DefaultBranch,
281 },
282 },
283 };
284 srcs.push(SourceId::for_git(&url, reference)?);
285 } else {
286 let check_not_set = |key, v: OptValue<String>| {
287 if let Some(val) = v {
288 bail!(
289 "source definition `source.{}` specifies `{}`, \
290 but that requires a `git` key to be specified (in {})",
291 name,
292 key,
293 val.definition
294 );
295 }
296 Ok(())
297 };
298 check_not_set("branch", def.branch)?;
299 check_not_set("tag", def.tag)?;
300 check_not_set("rev", def.rev)?;
301 }
302 if name == CRATES_IO_REGISTRY && srcs.is_empty() {
303 srcs.push(SourceId::crates_io_maybe_sparse_http(self.gctx)?);
304 }
305
306 match srcs.len() {
307 0 => bail!(
308 "no source location specified for `source.{}`, need \
309 `registry`, `local-registry`, `directory`, or `git` defined",
310 name
311 ),
312 1 => {}
313 _ => bail!(
314 "more than one source location specified for `source.{}`",
315 name
316 ),
317 }
318 let src = srcs[0];
319
320 let replace_with = def
321 .replace_with
322 .map(|val| (val.val, val.definition.to_string()));
323
324 self.add(
325 &name,
326 SourceConfig {
327 id: src,
328 replace_with,
329 },
330 )?;
331
332 return Ok(());
333
334 fn url(val: &context::Value<String>, key: &str) -> CargoResult<Url> {
335 let url = val.val.into_url().with_context(|| {
336 format!(
337 "configuration key `{}` specified an invalid \
338 URL (in {})",
339 key, val.definition
340 )
341 })?;
342
343 Ok(url)
344 }
345 }
346}