cargo_edit/
index.rs

1use tame_index::krate::IndexKrate;
2use tame_index::utils::flock::FileLock;
3
4use url::Url;
5
6use super::errors::{CargoResult, Context};
7
8#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
9pub enum CertsSource {
10    /// Use certs from Mozilla's root certificate store.
11    #[default]
12    Webpki,
13    /// Use certs from the system root certificate store.
14    Native,
15}
16
17pub struct IndexCache {
18    certs_source: CertsSource,
19    index: std::collections::HashMap<Url, AnyIndexCache>,
20}
21
22impl IndexCache {
23    #[inline]
24    pub fn new(certs_source: CertsSource) -> Self {
25        Self {
26            certs_source,
27            index: Default::default(),
28        }
29    }
30
31    /// Determines if the specified crate exists in the crates.io index
32    #[inline]
33    pub fn has_krate(&mut self, registry: &Url, name: &str) -> CargoResult<bool> {
34        self.index(registry)
35            .with_context(|| format!("failed to look up {name}"))?
36            .has_krate(name)
37    }
38
39    /// Determines if the specified crate version exists in the crates.io index
40    #[inline]
41    pub fn has_krate_version(
42        &mut self,
43        registry: &Url,
44        name: &str,
45        version: &str,
46    ) -> CargoResult<Option<bool>> {
47        self.index(registry)
48            .with_context(|| format!("failed to look up {name}@{version}"))?
49            .has_krate_version(name, version)
50    }
51
52    #[inline]
53    pub fn update_krate(&mut self, registry: &Url, name: &str) -> CargoResult<()> {
54        self.index(registry)
55            .with_context(|| format!("failed to look up {name}"))?
56            .update_krate(name);
57        Ok(())
58    }
59
60    pub fn krate(&mut self, registry: &Url, name: &str) -> CargoResult<Option<IndexKrate>> {
61        self.index(registry)
62            .with_context(|| format!("failed to look up {name}"))?
63            .krate(name)
64    }
65
66    fn index<'s>(&'s mut self, registry: &Url) -> CargoResult<&'s mut AnyIndexCache> {
67        if !self.index.contains_key(registry) {
68            let index = AnyIndex::open(registry, self.certs_source)?;
69            let index = AnyIndexCache::new(index);
70            self.index.insert(registry.clone(), index);
71        }
72        Ok(self.index.get_mut(registry).unwrap())
73    }
74}
75
76struct AnyIndexCache {
77    index: AnyIndex,
78    cache: std::collections::HashMap<String, Option<IndexKrate>>,
79}
80
81impl AnyIndexCache {
82    #[inline]
83    fn new(index: AnyIndex) -> Self {
84        Self {
85            index,
86            cache: std::collections::HashMap::new(),
87        }
88    }
89
90    /// Determines if the specified crate exists in the crates.io index
91    #[inline]
92    fn has_krate(&mut self, name: &str) -> CargoResult<bool> {
93        Ok(self.krate(name)?.map(|_| true).unwrap_or(false))
94    }
95
96    /// Determines if the specified crate version exists in the crates.io index
97    #[inline]
98    fn has_krate_version(&mut self, name: &str, version: &str) -> CargoResult<Option<bool>> {
99        let krate = self.krate(name)?;
100        Ok(krate.map(|ik| ik.versions.iter().any(|iv| iv.version == version)))
101    }
102
103    #[inline]
104    fn update_krate(&mut self, name: &str) {
105        self.cache.remove(name);
106    }
107
108    fn krate(&mut self, name: &str) -> CargoResult<Option<IndexKrate>> {
109        if let Some(entry) = self.cache.get(name) {
110            return Ok(entry.clone());
111        }
112
113        let entry = self.index.krate(name)?;
114        self.cache.insert(name.to_owned(), entry.clone());
115        Ok(entry)
116    }
117}
118
119enum AnyIndex {
120    Local(LocalIndex),
121    Remote(RemoteIndex),
122}
123
124impl AnyIndex {
125    fn open(url: &Url, certs_source: CertsSource) -> CargoResult<Self> {
126        if url.scheme() == "file" {
127            LocalIndex::open(url)
128                .map(Self::Local)
129                .with_context(|| format!("invalid local registry {url:?}"))
130        } else {
131            RemoteIndex::open(url, certs_source)
132                .map(Self::Remote)
133                .with_context(|| format!("invalid registry {url:?}"))
134        }
135    }
136
137    fn krate(&mut self, name: &str) -> CargoResult<Option<IndexKrate>> {
138        match self {
139            Self::Local(index) => index.krate(name),
140            Self::Remote(index) => index.krate(name),
141        }
142    }
143}
144
145struct LocalIndex {
146    index: tame_index::index::LocalRegistry,
147    root: tame_index::PathBuf,
148}
149
150impl LocalIndex {
151    fn open(url: &Url) -> CargoResult<Self> {
152        let path = url
153            .to_file_path()
154            .map_err(|_err| anyhow::format_err!("invalid file path {url:?}"))?;
155        let path = tame_index::PathBuf::from_path_buf(path)
156            .map_err(|_err| anyhow::format_err!("invalid file path {url:?}"))?;
157        let index = tame_index::index::LocalRegistry::open(path.clone(), false)?;
158        Ok(Self { index, root: path })
159    }
160
161    fn krate(&mut self, name: &str) -> CargoResult<Option<IndexKrate>> {
162        let name = tame_index::KrateName::cargo(name)?;
163        // HACK: for some reason, `tame_index` puts `index` in the middle
164        let entry_path = self.index.krate_path(name);
165        let rel_path = entry_path
166            .strip_prefix(&self.root)
167            .map_err(|_err| anyhow::format_err!("invalid index path {entry_path:?}"))?;
168        let rel_path = rel_path
169            .strip_prefix("index")
170            .map_err(|_err| anyhow::format_err!("invalid index path {entry_path:?}"))?;
171        let entry_path = self.root.join(rel_path);
172        let Ok(entry) = std::fs::read(&entry_path) else {
173            return Ok(None);
174        };
175        let results = IndexKrate::from_slice(&entry)?;
176        Ok(Some(results))
177    }
178}
179
180struct RemoteIndex {
181    index: tame_index::SparseIndex,
182    client: tame_index::external::reqwest::blocking::Client,
183    lock: FileLock,
184    etags: Vec<(String, String)>,
185}
186
187impl RemoteIndex {
188    fn open(url: &Url, certs_source: CertsSource) -> CargoResult<Self> {
189        log::trace!("opening index entry for {url}");
190        let url = url.to_string();
191        let url = tame_index::IndexUrl::NonCratesIo(std::borrow::Cow::Owned(url));
192        let index = tame_index::SparseIndex::new(tame_index::IndexLocation::new(url))?;
193
194        let client = {
195            let builder = tame_index::external::reqwest::blocking::ClientBuilder::new();
196
197            let builder = match certs_source {
198                CertsSource::Webpki => builder.tls_built_in_webpki_certs(true),
199                CertsSource::Native => builder.tls_built_in_native_certs(true),
200            };
201
202            builder.build()?
203        };
204
205        let lock = FileLock::unlocked();
206
207        Ok(Self {
208            index,
209            client,
210            lock,
211            etags: Vec::new(),
212        })
213    }
214
215    fn krate(&mut self, name: &str) -> CargoResult<Option<IndexKrate>> {
216        log::trace!("krate {name}");
217        let etag = self
218            .etags
219            .iter()
220            .find_map(|(krate, etag)| (krate == name).then_some(etag.as_str()))
221            .unwrap_or("");
222
223        let krate_name = name.try_into()?;
224        let req = self
225            .index
226            .make_remote_request(krate_name, Some(etag), &self.lock)?;
227        let (
228            tame_index::external::http::request::Parts {
229                method,
230                uri,
231                version,
232                headers,
233                ..
234            },
235            _,
236        ) = req.into_parts();
237        let mut req = self.client.request(method, uri.to_string());
238        req = req.version(version);
239        req = req.headers(headers);
240        let res = self.client.execute(req.build()?)?;
241
242        // Grab the etag if it exists for future requests
243        if let Some(etag) = res
244            .headers()
245            .get(tame_index::external::reqwest::header::ETAG)
246            && let Ok(etag) = etag.to_str()
247        {
248            if let Some(i) = self.etags.iter().position(|(krate, _)| krate == name) {
249                etag.clone_into(&mut self.etags[i].1);
250            } else {
251                self.etags.push((name.to_owned(), etag.to_owned()));
252            }
253        }
254
255        let mut builder = tame_index::external::http::Response::builder()
256            .status(res.status())
257            .version(res.version());
258
259        builder
260            .headers_mut()
261            .unwrap()
262            .extend(res.headers().iter().map(|(k, v)| (k.clone(), v.clone())));
263
264        let body = res.bytes()?;
265        let response = builder
266            .body(body.to_vec())
267            .map_err(|e| tame_index::Error::from(tame_index::error::HttpError::from(e)))?;
268
269        self.index
270            .parse_remote_response(krate_name, response, false, &self.lock)
271            .map_err(Into::into)
272    }
273}