tame_index/index/
location.rs

1//! Helpers for initializing the remote and local disk location of an index
2
3use crate::{Error, Path, PathBuf};
4use std::borrow::Cow;
5
6/// A remote index url
7#[derive(Default, Debug)]
8pub enum IndexUrl<'iu> {
9    /// The canonical crates.io HTTP sparse index.
10    ///
11    /// See [`crate::CRATES_IO_HTTP_INDEX`]
12    #[default]
13    CratesIoSparse,
14    /// The canonical crates.io git index.
15    ///
16    /// See [`crate::CRATES_IO_INDEX`]
17    CratesIoGit,
18    /// A non-crates.io index.
19    ///
20    /// This variant uses the url to determine the index kind (sparse or git) by
21    /// inspecting the url's scheme. This is because sparse indices are required
22    /// to have the `sparse+` scheme modifier
23    NonCratesIo(Cow<'iu, str>),
24    /// A [local registry](crate::index::LocalRegistry)
25    Local(Cow<'iu, Path>),
26}
27
28impl<'iu> IndexUrl<'iu> {
29    /// Gets the url as a string
30    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    /// Returns true if the url points to a sparse registry
40    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    /// Gets the [`IndexUrl`] for crates.io, depending on the local environment.
49    ///
50    /// 1. Determines if the crates.io registry has been [replaced](https://doc.rust-lang.org/cargo/reference/source-replacement.html)
51    /// 2. Determines if the protocol was explicitly [configured](https://doc.rust-lang.org/cargo/reference/config.html#registriescrates-ioprotocol) by the user
52    /// 3. Otherwise, detects the version of cargo (see [`crate::utils::cargo_version`]), and uses that to determine the appropriate default
53    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 the crates.io registry has been replaced it doesn't matter what
59        // the protocol for it has been changed to
60        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    /// Creates an [`IndexUrl`] for the specified registry name
106    ///
107    /// 1. Checks if [`CARGO_REGISTRIES_<name>_INDEX`](https://doc.rust-lang.org/cargo/reference/config.html#registriesnameindex) is set
108    /// 2. Checks if the source for the registry has been [replaced](https://doc.rust-lang.org/cargo/reference/source-replacement.html)
109    /// 3. Uses the value of [`registries.<name>.index`](https://doc.rust-lang.org/cargo/reference/config.html#registriesnameindex) otherwise
110    pub fn for_registry_name(
111        config_root: Option<PathBuf>,
112        cargo_home: Option<&Path>,
113        registry_name: &str,
114    ) -> Result<Self, Error> {
115        // Check if the index was explicitly specified
116        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/// The local disk location to place an index
172#[derive(Default)]
173pub enum IndexPath {
174    /// The default cargo home root path
175    #[default]
176    CargoHome,
177    /// User-specified root path
178    UserSpecified(PathBuf),
179    /// An exact path on disk where an index is located.
180    ///
181    /// Unlike the other two variants, this variant won't take the index's url
182    /// into account to calculate the unique url hash as part of the full path
183    Exact(PathBuf),
184}
185
186impl From<Option<PathBuf>> for IndexPath {
187    /// Converts an optional path to a rooted path.
188    ///
189    /// This never constructs a [`Self::Exact`], that can only be done explicitly
190    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/// Helper for constructing an index location, consisting of the remote url for
200/// the index and the local location on disk
201#[derive(Default)]
202pub struct IndexLocation<'il> {
203    /// The remote url of the registry index
204    pub url: IndexUrl<'il>,
205    /// The local disk path of the index
206    pub root: IndexPath,
207    /// The index location depends on the version of cargo used, as 1.85.0
208    /// introduced a change to how the url is hashed. Not specifying the version
209    /// will acquire the cargo version pertaining to the current environment.
210    pub cargo_version: Option<crate::Version>,
211}
212
213impl<'il> IndexLocation<'il> {
214    /// Constructs an index with the specified url located in the default cargo
215    /// home
216    pub fn new(url: IndexUrl<'il>) -> Self {
217        Self {
218            url,
219            root: IndexPath::CargoHome,
220            cargo_version: None,
221        }
222    }
223
224    /// Changes the root location of the index on the local disk.
225    ///
226    /// If not called, or set to [`None`], the default cargo home disk location
227    /// is used as the root
228    pub fn with_root(mut self, root: Option<PathBuf>) -> Self {
229        self.root = root.into();
230        self
231    }
232
233    /// Obtains the full local disk path and URL of this index location
234    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
261/// Calls the specified function for each cargo config located according to
262/// cargo's standard hierarchical structure
263///
264/// Note that this only supports the use of `.cargo/config.toml`, which is not
265/// supported below cargo 1.39.0
266///
267/// See <https://doc.rust-lang.org/cargo/reference/config.html#hierarchical-structure>
268pub(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            // Walk up to the next potential config root
295            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/// Gets the url of a replacement registry for the specified registry if one has been configured
319///
320/// See <https://doc.rust-lang.org/cargo/reference/source-replacement.html>
321#[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    // Current stable is 1.70.0
348    #[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    /// Verifies we can parse .cargo/config.toml files to either use the crates-io
361    /// protocol set, or source replacements
362    #[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        // First just set the protocol from the sparse default to git
377        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        // Next set replacement registries
384        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}