1use crate::{Error, Path, PathBuf};
4use std::borrow::Cow;
5
6#[derive(Default, Debug)]
8pub enum IndexUrl<'iu> {
9 #[default]
13 CratesIoSparse,
14 CratesIoGit,
18 NonCratesIo(Cow<'iu, str>),
24 Local(Cow<'iu, Path>),
26}
27
28impl<'iu> IndexUrl<'iu> {
29 pub fn as_str(&'iu self) -> &'iu str {
31 match self {
32 Self::CratesIoSparse => crate::CRATES_IO_HTTP_INDEX,
33 Self::CratesIoGit => crate::CRATES_IO_INDEX,
34 Self::NonCratesIo(url) => url,
35 Self::Local(pb) => pb.as_str(),
36 }
37 }
38
39 pub fn is_sparse(&self) -> bool {
41 match self {
42 Self::CratesIoSparse => true,
43 Self::CratesIoGit | Self::Local(..) => false,
44 Self::NonCratesIo(url) => url.starts_with("sparse+http"),
45 }
46 }
47
48 pub fn crates_io(
54 config_root: Option<PathBuf>,
55 cargo_home: Option<&Path>,
56 cargo_version: Option<&str>,
57 ) -> Result<Self, Error> {
58 if let Some(replacement) =
61 get_source_replacement(config_root.clone(), cargo_home, "crates-io")?
62 {
63 return Ok(replacement);
64 }
65
66 let sparse_index = match std::env::var("CARGO_REGISTRIES_CRATES_IO_PROTOCOL")
67 .ok()
68 .as_deref()
69 {
70 Some("sparse") => true,
71 Some("git") => false,
72 _ => {
73 let sparse_index =
74 read_cargo_config(config_root, cargo_home, |config| {
75 match config
76 .pointer("/registries/crates-io/protocol")
77 .and_then(|p| p.as_str())?
78 {
79 "sparse" => Some(true),
80 "git" => Some(false),
81 _ => None,
82 }
83 })?;
84
85 if let Some(si) = sparse_index {
86 si
87 } else {
88 let vers = match cargo_version {
89 Some(v) => v.trim().parse()?,
90 None => crate::utils::cargo_version(None)?,
91 };
92
93 vers >= semver::Version::new(1, 70, 0)
94 }
95 }
96 };
97
98 Ok(if sparse_index {
99 Self::CratesIoSparse
100 } else {
101 Self::CratesIoGit
102 })
103 }
104
105 pub fn for_registry_name(
111 config_root: Option<PathBuf>,
112 cargo_home: Option<&Path>,
113 registry_name: &str,
114 ) -> Result<Self, Error> {
115 let mut env = String::with_capacity(17 + registry_name.len() + 6);
117 env.push_str("CARGO_REGISTRIES_");
118
119 if registry_name.is_ascii() {
120 for c in registry_name.chars() {
121 if c == '-' {
122 env.push('_');
123 } else {
124 env.push(c.to_ascii_uppercase());
125 }
126 }
127 } else {
128 let mut upper = registry_name.to_uppercase();
129 if upper.contains('-') {
130 upper = upper.replace('-', "_");
131 }
132
133 env.push_str(&upper);
134 }
135
136 env.push_str("_INDEX");
137
138 match std::env::var(&env) {
139 Ok(index) => return Ok(Self::NonCratesIo(index.into())),
140 Err(err) => {
141 if let std::env::VarError::NotUnicode(_nu) = err {
142 return Err(Error::NonUtf8EnvVar(env.into()));
143 }
144 }
145 }
146
147 if let Some(replacement) =
148 get_source_replacement(config_root.clone(), cargo_home, registry_name)?
149 {
150 return Ok(replacement);
151 }
152
153 read_cargo_config(config_root, cargo_home, |config| {
154 let path = format!("/registries/{registry_name}/index");
155 config
156 .pointer(&path)?
157 .as_str()
158 .map(|si| Self::NonCratesIo(si.to_owned().into()))
159 })?
160 .ok_or_else(|| Error::UnknownRegistry(registry_name.into()))
161 }
162}
163
164impl<'iu> From<&'iu str> for IndexUrl<'iu> {
165 #[inline]
166 fn from(s: &'iu str) -> Self {
167 Self::NonCratesIo(s.into())
168 }
169}
170
171#[derive(Default)]
173pub enum IndexPath {
174 #[default]
176 CargoHome,
177 UserSpecified(PathBuf),
179 Exact(PathBuf),
184}
185
186impl From<Option<PathBuf>> for IndexPath {
187 fn from(pb: Option<PathBuf>) -> Self {
191 if let Some(pb) = pb {
192 Self::UserSpecified(pb)
193 } else {
194 Self::CargoHome
195 }
196 }
197}
198
199#[derive(Default)]
202pub struct IndexLocation<'il> {
203 pub url: IndexUrl<'il>,
205 pub root: IndexPath,
207 pub cargo_version: Option<crate::Version>,
211}
212
213impl<'il> IndexLocation<'il> {
214 pub fn new(url: IndexUrl<'il>) -> Self {
217 Self {
218 url,
219 root: IndexPath::CargoHome,
220 cargo_version: None,
221 }
222 }
223
224 pub fn with_root(mut self, root: Option<PathBuf>) -> Self {
229 self.root = root.into();
230 self
231 }
232
233 pub fn into_parts(self) -> Result<(PathBuf, String), Error> {
235 let url = self.url.as_str();
236
237 let root = match self.root {
238 IndexPath::CargoHome => crate::utils::cargo_home()?,
239 IndexPath::UserSpecified(root) => root,
240 IndexPath::Exact(path) => return Ok((path, url.to_owned())),
241 };
242
243 let vers = if let Some(v) = self.cargo_version {
244 v
245 } else {
246 crate::utils::cargo_version(None)?
247 };
248
249 let stable = vers >= semver::Version::new(1, 85, 0);
250
251 let (path, mut url) = crate::utils::get_index_details(url, Some(root), stable)?;
252
253 if !url.ends_with('/') {
254 url.push('/');
255 }
256
257 Ok((path, url))
258 }
259}
260
261pub(crate) fn read_cargo_config<T>(
269 root: Option<PathBuf>,
270 cargo_home: Option<&Path>,
271 callback: impl Fn(&toml_span::value::Value<'_>) -> Option<T>,
272) -> Result<Option<T>, Error> {
273 if let Some(mut path) = root.or_else(|| {
274 std::env::current_dir()
275 .ok()
276 .and_then(|pb| PathBuf::from_path_buf(pb).ok())
277 }) {
278 loop {
279 path.push(".cargo/config.toml");
280 if path.exists() {
281 let contents = match std::fs::read_to_string(&path) {
282 Ok(c) => c,
283 Err(err) => return Err(Error::IoPath(err, path)),
284 };
285
286 let toml = toml_span::parse(&contents).map_err(Box::new)?;
287 if let Some(value) = callback(&toml) {
288 return Ok(Some(value));
289 }
290 }
291 path.pop();
292 path.pop();
293
294 if !path.pop() {
296 break;
297 }
298 }
299 }
300
301 if let Some(home) = cargo_home
302 .map(Cow::Borrowed)
303 .or_else(|| crate::utils::cargo_home().ok().map(Cow::Owned))
304 {
305 let path = home.join("config.toml");
306 if path.exists() {
307 let fc = std::fs::read_to_string(&path)?;
308 let toml = toml_span::parse(&fc).map_err(Box::new)?;
309 if let Some(value) = callback(&toml) {
310 return Ok(Some(value));
311 }
312 }
313 }
314
315 Ok(None)
316}
317
318#[inline]
322pub(crate) fn get_source_replacement<'iu>(
323 root: Option<PathBuf>,
324 cargo_home: Option<&Path>,
325 registry_name: &str,
326) -> Result<Option<IndexUrl<'iu>>, Error> {
327 read_cargo_config(root, cargo_home, |config| {
328 let path = format!("/source/{registry_name}/replace-with");
329 let repw = config.pointer(&path)?.as_str()?;
330 let sources = config.pointer("/source")?.as_table()?;
331 let replace_src = sources.get(repw)?.as_table()?;
332
333 if let Some(rr) = replace_src.get("registry") {
334 rr.as_str()
335 .map(|r| IndexUrl::NonCratesIo(r.to_owned().into()))
336 } else if let Some(rlr) = replace_src.get("local-registry") {
337 rlr.as_str()
338 .map(|l| IndexUrl::Local(PathBuf::from(l).into()))
339 } else {
340 None
341 }
342 })
343}
344
345#[cfg(test)]
346mod test {
347 #[test]
349 fn opens_sparse() {
350 assert!(std::env::var_os("CARGO_REGISTRIES_CRATES_IO_PROTOCOL").is_none());
351 assert!(matches!(
352 crate::index::ComboIndexCache::new(super::IndexLocation::new(
353 super::IndexUrl::crates_io(None, None, None).unwrap()
354 ))
355 .unwrap(),
356 crate::index::ComboIndexCache::Sparse(_)
357 ));
358 }
359
360 #[test]
363 fn parses_from_file() {
364 assert!(std::env::var_os("CARGO_REGISTRIES_CRATES_IO_PROTOCOL").is_none());
365
366 let td = tempfile::tempdir().unwrap();
367 let root = crate::PathBuf::from_path_buf(td.path().to_owned()).unwrap();
368 let cfg_toml = td.path().join(".cargo/config.toml");
369
370 std::fs::create_dir_all(cfg_toml.parent().unwrap()).unwrap();
371
372 const GIT: &str = r#"[registries.crates-io]
373protocol = "git"
374"#;
375
376 std::fs::write(&cfg_toml, GIT).unwrap();
378
379 let iurl = super::IndexUrl::crates_io(Some(root.clone()), None, None).unwrap();
380 assert_eq!(iurl.as_str(), crate::CRATES_IO_INDEX);
381 assert!(!iurl.is_sparse());
382
383 for (i, (kind, url)) in [
385 (
386 "registry",
387 "sparse+https://sparse-registry-parses-from-file.com",
388 ),
389 ("registry", "https://sparse-registry-parses-from-file.git"),
390 ("local-registry", root.as_str()),
391 ]
392 .iter()
393 .enumerate()
394 {
395 std::fs::write(&cfg_toml, format!("{GIT}\n[source.crates-io]\nreplace-with = 'replacement'\n[source.replacement]\n{kind} = '{url}'")).unwrap();
396
397 let iurl = super::IndexUrl::crates_io(Some(root.clone()), None, None).unwrap();
398 assert_eq!(i == 0, iurl.is_sparse());
399 assert_eq!(iurl.as_str(), *url);
400 }
401 }
402
403 #[test]
404 fn custom() {
405 assert!(std::env::var_os("CARGO_REGISTRIES_TAME_INDEX_TEST_INDEX").is_none());
406
407 let td = tempfile::tempdir().unwrap();
408 let root = crate::PathBuf::from_path_buf(td.path().to_owned()).unwrap();
409 let cfg_toml = td.path().join(".cargo/config.toml");
410
411 std::fs::create_dir_all(cfg_toml.parent().unwrap()).unwrap();
412
413 const SPARSE: &str = r#"[registries.tame-index-test]
414index = "sparse+https://some-url.com"
415"#;
416
417 const GIT: &str = r#"[registries.tame-index-test]
418 index = "https://some-url.com"
419 "#;
420
421 {
422 std::fs::write(&cfg_toml, SPARSE).unwrap();
423
424 let iurl =
425 super::IndexUrl::for_registry_name(Some(root.clone()), None, "tame-index-test")
426 .unwrap();
427 assert_eq!(iurl.as_str(), "sparse+https://some-url.com");
428 assert!(iurl.is_sparse());
429
430 std::env::set_var(
431 "CARGO_REGISTRIES_TAME_INDEX_TEST_INDEX",
432 "sparse+https://some-other-url.com",
433 );
434
435 let iurl =
436 super::IndexUrl::for_registry_name(Some(root.clone()), None, "tame-index-test")
437 .unwrap();
438 assert_eq!(iurl.as_str(), "sparse+https://some-other-url.com");
439 assert!(iurl.is_sparse());
440
441 std::env::remove_var("CARGO_REGISTRIES_TAME_INDEX_TEST_INDEX");
442 }
443
444 {
445 std::fs::write(&cfg_toml, GIT).unwrap();
446
447 let iurl =
448 super::IndexUrl::for_registry_name(Some(root.clone()), None, "tame-index-test")
449 .unwrap();
450 assert_eq!(iurl.as_str(), "https://some-url.com");
451 assert!(!iurl.is_sparse());
452
453 std::env::set_var(
454 "CARGO_REGISTRIES_TAME_INDEX_TEST_INDEX",
455 "https://some-other-url.com",
456 );
457
458 let iurl =
459 super::IndexUrl::for_registry_name(Some(root.clone()), None, "tame-index-test")
460 .unwrap();
461 assert_eq!(iurl.as_str(), "https://some-other-url.com");
462 assert!(!iurl.is_sparse());
463
464 std::env::remove_var("CARGO_REGISTRIES_TAME_INDEX_TEST_INDEX");
465 }
466
467 #[allow(unused_variables)]
468 {
469 let err = crate::Error::UnknownRegistry("non-existant".to_owned());
470 assert!(matches!(
471 super::IndexUrl::for_registry_name(Some(root.clone()), None, "non-existant"),
472 Err(err),
473 ));
474 }
475 }
476}