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