tame_index/
index.rs

1//! Provides functionality for interacting with both local and remote registry
2//! indices
3
4pub mod cache;
5#[cfg(all(feature = "__git", feature = "sparse"))]
6mod combo;
7#[allow(missing_docs)]
8pub mod git;
9#[cfg(feature = "__git")]
10pub(crate) mod git_remote;
11#[cfg(feature = "local")]
12pub mod local;
13pub mod location;
14#[allow(missing_docs)]
15pub mod sparse;
16#[cfg(feature = "sparse")]
17mod sparse_remote;
18
19pub use cache::IndexCache;
20#[cfg(all(feature = "__git", feature = "sparse"))]
21pub use combo::ComboIndex;
22pub use git::GitIndex;
23#[cfg(feature = "__git")]
24pub use git_remote::RemoteGitIndex;
25#[cfg(feature = "local")]
26pub use local::LocalRegistry;
27pub use location::{IndexLocation, IndexPath, IndexUrl};
28pub use sparse::SparseIndex;
29#[cfg(feature = "sparse")]
30pub use sparse_remote::{AsyncRemoteSparseIndex, RemoteSparseIndex};
31
32pub use crate::utils::flock::FileLock;
33
34/// Global configuration of an index, reflecting the [contents of config.json](https://doc.rust-lang.org/cargo/reference/registries.html#index-format).
35#[derive(Eq, PartialEq, PartialOrd, Ord, Clone, Debug, serde::Deserialize, serde::Serialize)]
36pub struct IndexConfig {
37    /// Pattern for creating download URLs. See [`Self::download_url`].
38    pub dl: String,
39    /// Base URL for publishing, etc.
40    pub api: Option<String>,
41}
42
43impl IndexConfig {
44    /// Gets the download url for the specified crate version
45    ///
46    /// See <https://doc.rust-lang.org/cargo/reference/registries.html#index-format>
47    /// for more info
48    pub fn download_url(&self, name: crate::KrateName<'_>, version: &str) -> String {
49        // Special case crates.io which will easily be the most common case in
50        // almost all scenarios, we just use the _actual_ url directly, which
51        // avoids a 301 redirect, though obviously this will be bad if crates.io
52        // ever changes the redirect, this has been stable since 1.0 (at least)
53        // so it's unlikely to ever change, and if it does, it would be easy to
54        // update, though obviously would be broken on previously published versions
55        if self.dl == "https://crates.io/api/v1/crates" {
56            return format!("https://static.crates.io/crates/{name}/{name}-{version}.crate");
57        }
58
59        let mut dl = self.dl.clone();
60
61        if dl.contains('{') {
62            while let Some(start) = dl.find("{crate}") {
63                dl.replace_range(start..start + 7, name.0);
64            }
65
66            while let Some(start) = dl.find("{version}") {
67                dl.replace_range(start..start + 9, version);
68            }
69
70            if dl.contains("{prefix}") || dl.contains("{lowerprefix}") {
71                let mut prefix = String::with_capacity(6);
72                name.prefix(&mut prefix, '/');
73
74                while let Some(start) = dl.find("{prefix}") {
75                    dl.replace_range(start..start + 8, &prefix);
76                }
77
78                if dl.contains("{lowerprefix}") {
79                    prefix.make_ascii_lowercase();
80
81                    while let Some(start) = dl.find("{lowerprefix}") {
82                        dl.replace_range(start..start + 13, &prefix);
83                    }
84                }
85            }
86        } else {
87            // If none of the markers are present, then the value /{crate}/{version}/download is appended to the end
88            if !dl.ends_with('/') {
89                dl.push('/');
90            }
91
92            dl.push_str(name.0);
93            dl.push('/');
94            dl.push_str(version);
95            dl.push('/');
96            dl.push_str("download");
97        }
98
99        dl
100    }
101}
102
103use crate::Error;
104
105/// Provides simpler access to the cache for an index, regardless of the registry kind
106#[non_exhaustive]
107pub enum ComboIndexCache {
108    /// A git index
109    Git(GitIndex),
110    /// A sparse HTTP index
111    Sparse(SparseIndex),
112    /// A local registry
113    #[cfg(feature = "local")]
114    Local(LocalRegistry),
115}
116
117impl ComboIndexCache {
118    /// Retrieves the index metadata for the specified crate name
119    #[inline]
120    pub fn cached_krate(
121        &self,
122        name: crate::KrateName<'_>,
123        lock: &FileLock,
124    ) -> Result<Option<crate::IndexKrate>, Error> {
125        match self {
126            Self::Git(index) => index.cached_krate(name, lock),
127            Self::Sparse(index) => index.cached_krate(name, lock),
128            #[cfg(feature = "local")]
129            Self::Local(lr) => lr.cached_krate(name, lock),
130        }
131    }
132
133    /// Gets the path to the cache entry for the specified crate
134    pub fn cache_path(&self, name: crate::KrateName<'_>) -> crate::PathBuf {
135        match self {
136            Self::Git(index) => index.cache.cache_path(name),
137            Self::Sparse(index) => index.cache().cache_path(name),
138            #[cfg(feature = "local")]
139            Self::Local(lr) => lr.krate_path(name),
140        }
141    }
142
143    /// Constructs a [`Self`] for the specified index.
144    ///
145    /// See [`Self::crates_io`] if you want to create a crates.io index based
146    /// upon other information in the user's environment
147    pub fn new(il: IndexLocation<'_>) -> Result<Self, Error> {
148        #[cfg(feature = "local")]
149        {
150            if let IndexUrl::Local(path) = il.url {
151                return Ok(Self::Local(LocalRegistry::open(path.into(), true)?));
152            }
153        }
154
155        let index = if il.url.is_sparse() {
156            let sparse = SparseIndex::new(il)?;
157            Self::Sparse(sparse)
158        } else {
159            let git = GitIndex::new(il)?;
160            Self::Git(git)
161        };
162
163        Ok(index)
164    }
165}
166
167impl From<SparseIndex> for ComboIndexCache {
168    #[inline]
169    fn from(si: SparseIndex) -> Self {
170        Self::Sparse(si)
171    }
172}
173
174impl From<GitIndex> for ComboIndexCache {
175    #[inline]
176    fn from(gi: GitIndex) -> Self {
177        Self::Git(gi)
178    }
179}
180
181#[cfg(test)]
182mod test {
183    use super::IndexConfig;
184    use crate::kn;
185
186    /// Validates we get the non-redirect url for crates.io downloads
187    #[test]
188    fn download_url_crates_io() {
189        let crates_io = IndexConfig {
190            dl: "https://crates.io/api/v1/crates".into(),
191            api: Some("https://crates.io".into()),
192        };
193
194        assert_eq!(
195            crates_io.download_url(kn!("a"), "1.0.0"),
196            "https://static.crates.io/crates/a/a-1.0.0.crate"
197        );
198        assert_eq!(
199            crates_io.download_url(kn!("aB"), "0.1.0"),
200            "https://static.crates.io/crates/aB/aB-0.1.0.crate"
201        );
202        assert_eq!(
203            crates_io.download_url(kn!("aBc"), "0.1.0"),
204            "https://static.crates.io/crates/aBc/aBc-0.1.0.crate"
205        );
206        assert_eq!(
207            crates_io.download_url(kn!("aBc-123"), "0.1.0"),
208            "https://static.crates.io/crates/aBc-123/aBc-123-0.1.0.crate"
209        );
210    }
211
212    /// Validates we get a simple non-crates.io download
213    #[test]
214    fn download_url_non_crates_io() {
215        let ic = IndexConfig {
216            dl: "https://dl.cloudsmith.io/public/embark/deny/cargo/{crate}-{version}.crate".into(),
217            api: Some("https://cargo.cloudsmith.io/embark/deny".into()),
218        };
219
220        assert_eq!(
221            ic.download_url(kn!("a"), "1.0.0"),
222            "https://dl.cloudsmith.io/public/embark/deny/cargo/a-1.0.0.crate"
223        );
224        assert_eq!(
225            ic.download_url(kn!("aB"), "0.1.0"),
226            "https://dl.cloudsmith.io/public/embark/deny/cargo/aB-0.1.0.crate"
227        );
228        assert_eq!(
229            ic.download_url(kn!("aBc"), "0.1.0"),
230            "https://dl.cloudsmith.io/public/embark/deny/cargo/aBc-0.1.0.crate"
231        );
232        assert_eq!(
233            ic.download_url(kn!("aBc-123"), "0.1.0"),
234            "https://dl.cloudsmith.io/public/embark/deny/cargo/aBc-123-0.1.0.crate"
235        );
236    }
237
238    /// Validates we get a more complicated non-crates.io download, exercising all
239    /// of the possible replacement components
240    #[test]
241    fn download_url_complex() {
242        let ic = IndexConfig {
243            dl: "https://complex.io/ohhi/embark/rust/cargo/{lowerprefix}/{crate}/{crate}/{prefix}-{version}".into(),
244            api: None,
245        };
246
247        assert_eq!(
248            ic.download_url(kn!("a"), "1.0.0"),
249            "https://complex.io/ohhi/embark/rust/cargo/1/a/a/1-1.0.0"
250        );
251        assert_eq!(
252            ic.download_url(kn!("aB"), "0.1.0"),
253            "https://complex.io/ohhi/embark/rust/cargo/2/aB/aB/2-0.1.0"
254        );
255        assert_eq!(
256            ic.download_url(kn!("ABc"), "0.1.0"),
257            "https://complex.io/ohhi/embark/rust/cargo/3/a/ABc/ABc/3/A-0.1.0"
258        );
259        assert_eq!(
260            ic.download_url(kn!("aBc-123"), "0.1.0"),
261            "https://complex.io/ohhi/embark/rust/cargo/ab/c-/aBc-123/aBc-123/aB/c--0.1.0"
262        );
263    }
264}