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