cargo_update/ops/
mod.rs

1//! Main functions doing actual work.
2//!
3//! Use `installed_registry_packages()` to list the installed packages,
4//! then use `intersect_packages()` to confirm which ones should be updated,
5//! poll the packages' latest versions by calling `RegistryPackage::pull_version()` on them,
6//! continue with doing whatever you wish.
7
8
9use git2::{self, ErrorCode as GitErrorCode, Config as GitConfig, Error as GitError, Cred as GitCred, RemoteCallbacks, CredentialType, FetchOptions,
10           ProxyOptions, Repository, Tree, Oid};
11use curl::easy::{WriteError as CurlWriteError, Handler as CurlHandler, SslOpt as CurlSslOpt, Easy2 as CurlEasy, List as CurlList};
12use semver::{VersionReq as SemverReq, Version as Semver};
13#[cfg(target_vendor = "apple")]
14use security_framework::os::macos::keychain::SecKeychain;
15#[cfg(target_os = "windows")]
16use windows::Win32::Security::Credentials as WinCred;
17use std::io::{self, ErrorKind as IoErrorKind, BufWriter, BufReader, BufRead, Write};
18use std::collections::{BTreeMap, BTreeSet};
19use std::{slice, cmp, env, mem, str, fs};
20use curl::multi::Multi as CurlMulti;
21use std::process::{Command, Stdio};
22use std::ffi::{OsString, OsStr};
23use std::path::{PathBuf, Path};
24use std::hash::{Hasher, Hash};
25use std::iter::FromIterator;
26#[cfg(target_os = "windows")]
27use windows::core::PCSTR;
28use std::time::Duration;
29#[cfg(all(unix, not(target_vendor = "apple")))]
30use std::sync::LazyLock;
31use serde_json as json;
32use std::borrow::Cow;
33use std::sync::Mutex;
34#[cfg(any(target_os = "windows", all(unix, not(target_vendor = "apple"))))]
35use std::ptr;
36use url::Url;
37use toml;
38use hex;
39
40mod config;
41
42pub use self::config::*;
43
44
45// cargo-audit 0.17.5 (registry+https://github.com/rust-lang/crates.io-index)
46// cargo-audit 0.17.5 (sparse+https://index.crates.io/)
47// -> (name, version, registry)
48//    ("cargo-audit", "0.17.5", "https://github.com/rust-lang/crates.io-index")
49//    ("cargo-audit", "0.17.5", "https://index.crates.io/")
50fn parse_registry_package_ident(ident: &str) -> Option<(&str, &str, &str)> {
51    let mut idx = ident.splitn(3, ' ');
52    let (name, version, mut reg) = (idx.next()?, idx.next()?, idx.next()?);
53    reg = reg.strip_prefix('(')?.strip_suffix(')')?;
54    Some((name, version, reg.strip_prefix("registry+").or_else(|| reg.strip_prefix("sparse+"))?))
55}
56// alacritty 0.1.0 (git+https://github.com/jwilm/alacritty#eb231b3e70b87875df4bdd1974d5e94704024d70)
57// chattium-oxide-client 0.1.0
58// (git+https://github.com/nabijaczleweli/chattium-oxide-client?branch=master#108a7b94f0e0dcb2a875f70fc0459d5a682df14c)
59// -> (name, url, sha)
60//    ("alacritty", "https://github.com/jwilm/alacritty", "eb231b3e70b87875df4bdd1974d5e94704024d70")
61// ("chattium-oxide-client", "https://github.com/nabijaczleweli/chattium-oxide-client?branch=master",
62//                           "108a7b94f0e0dcb2a875f70fc0459d5a682df14c")
63fn parse_git_package_ident(ident: &str) -> Option<(&str, &str, &str)> {
64    let mut idx = ident.splitn(3, ' ');
65    let (name, _, blob) = (idx.next()?, idx.next()?, idx.next()?);
66    let (url, sha) = blob.strip_prefix("(git+")?.strip_suffix(')')?.split_once('#')?;
67    if sha.len() != 40 {
68        return None;
69    }
70    Some((name, url, sha))
71}
72
73
74/// A representation of a package from the main [`crates.io`](https://crates.io) repository.
75///
76/// The newest version of a package is pulled from [`crates.io`](https://crates.io) via `pull_version()`.
77///
78/// The `parse()` function parses the format used in `$HOME/.cargo/.crates.toml`.
79///
80/// # Examples
81///
82/// ```
83/// # extern crate cargo_update;
84/// # extern crate semver;
85/// # use cargo_update::ops::RegistryPackage;
86/// # use semver::Version as Semver;
87/// # fn main() {
88/// let package_s = "racer 1.2.10 (registry+https://github.com/rust-lang/crates.io-index)";
89/// let mut package = RegistryPackage::parse(package_s, vec!["racer.exe".to_string()]).unwrap();
90/// assert_eq!(package,
91///            RegistryPackage {
92///                name: "racer".to_string(),
93///                registry: "https://github.com/rust-lang/crates.io-index".into(),
94///                version: Some(Semver::parse("1.2.10").unwrap()),
95///                newest_version: None,
96///                alternative_version: None,
97///                max_version: None,
98///                executables: vec!["racer.exe".to_string()],
99///            });
100///
101/// # /*
102/// package.pull_version(&registry_tree, &registry);
103/// # */
104/// # package.newest_version = Some(Semver::parse("1.2.11").unwrap());
105/// assert!(package.newest_version.is_some());
106/// # }
107/// ```
108#[derive(Debug, Clone, Hash, PartialEq, Eq)]
109pub struct RegistryPackage {
110    /// The package's name.
111    ///
112    /// Go to `https://crates.io/crates/{name}` to get the crate info, if available on the main repository.
113    pub name: String,
114    /// The registry the package is available from.
115    ///
116    /// Can be a name from ~/.cargo/config.
117    ///
118    /// The main repository is `https://github.com/rust-lang/crates.io-index`, or `sparse+https://index.crates.io/`.
119    pub registry: Cow<'static, str>,
120    /// The package's locally installed version.
121    pub version: Option<Semver>,
122    /// The latest version of the package, available at [`crates.io`](https://crates.io), if in main repository.
123    ///
124    /// `None` by default, acquire via `RegistryPackage::pull_version()`.
125    pub newest_version: Option<Semver>,
126    /// If present, the alternative newest version not chosen because of unfulfilled requirements like (not) being a prerelease.
127    pub alternative_version: Option<Semver>,
128    /// User-bounded maximum version to update up to.
129    pub max_version: Option<Semver>,
130    /// Executables currently installed for this package.
131    pub executables: Vec<String>,
132}
133
134/// A representation of a package a remote git repository.
135///
136/// The newest commit is pulled from that repo via `pull_version()`.
137///
138/// The `parse()` function parses the format used in `$HOME/.cargo/.crates.toml`.
139///
140/// # Examples
141///
142/// ```
143/// # extern crate cargo_update;
144/// # extern crate git2;
145/// # use cargo_update::ops::GitRepoPackage;
146/// # fn main() {
147/// let package_s = "alacritty 0.1.0 (git+https://github.com/jwilm/alacritty#eb231b3e70b87875df4bdd1974d5e94704024d70)";
148/// let mut package = GitRepoPackage::parse(package_s, vec!["alacritty".to_string()]).unwrap();
149/// assert_eq!(package,
150///            GitRepoPackage {
151///                name: "alacritty".to_string(),
152///                url: "https://github.com/jwilm/alacritty".to_string(),
153///                branch: None,
154///                id: git2::Oid::from_str("eb231b3e70b87875df4bdd1974d5e94704024d70").unwrap(),
155///                newest_id: Err(git2::Error::from_str("")),
156///                executables: vec!["alacritty".to_string()],
157///            });
158///
159/// # /*
160/// package.pull_version(&registry_tree, &registry);
161/// # */
162/// # package.newest_id = git2::Oid::from_str("5f7885749c4d7e48869b1fc0be4d430601cdbbfa");
163/// assert!(package.newest_id.is_ok());
164/// # }
165/// ```
166#[derive(Debug, PartialEq)]
167pub struct GitRepoPackage {
168    /// The package's name.
169    pub name: String,
170    /// The remote git repo URL.
171    pub url: String,
172    /// The installed branch, or `None` for default.
173    pub branch: Option<String>,
174    /// The package's locally installed version's object hash.
175    pub id: Oid,
176    /// The latest version of the package available at the main [`crates.io`](https://crates.io) repository.
177    ///
178    /// `None` by default, acquire via `GitRepoPackage::pull_version()`.
179    pub newest_id: Result<Oid, GitError>,
180    /// Executables currently installed for this package.
181    pub executables: Vec<String>,
182}
183impl Hash for GitRepoPackage {
184    fn hash<H: Hasher>(&self, state: &mut H) {
185        self.name.hash(state);
186        self.url.hash(state);
187        self.branch.hash(state);
188        self.id.hash(state);
189        match &self.newest_id {
190            Ok(nid) => nid.hash(state),
191            Err(err) => {
192                err.raw_code().hash(state);
193                err.raw_class().hash(state);
194                err.message().hash(state);
195            }
196        }
197        self.executables.hash(state);
198    }
199}
200
201
202impl RegistryPackage {
203    /// Try to decypher a package descriptor into a `RegistryPackage`.
204    ///
205    /// Will return `None` if the given package descriptor is invalid.
206    ///
207    /// In the returned instance, `newest_version` is always `None`, get it via `RegistryPackage::pull_version()`.
208    ///
209    /// The executable list is used as-is.
210    ///
211    /// # Examples
212    ///
213    /// Main repository packages:
214    ///
215    /// ```
216    /// # extern crate cargo_update;
217    /// # extern crate semver;
218    /// # use cargo_update::ops::RegistryPackage;
219    /// # use semver::Version as Semver;
220    /// # fn main() {
221    /// let package_s = "racer 1.2.10 (registry+https://github.com/rust-lang/crates.io-index)";
222    /// assert_eq!(RegistryPackage::parse(package_s, vec!["racer.exe".to_string()]).unwrap(),
223    ///            RegistryPackage {
224    ///                name: "racer".to_string(),
225    ///                registry: "https://github.com/rust-lang/crates.io-index".into(),
226    ///                version: Some(Semver::parse("1.2.10").unwrap()),
227    ///                newest_version: None,
228    ///                alternative_version: None,
229    ///                max_version: None,
230    ///                executables: vec!["racer.exe".to_string()],
231    ///            });
232    ///
233    /// let package_s = "cargo-outdated 0.2.0 (registry+file:///usr/local/share/cargo)";
234    /// assert_eq!(RegistryPackage::parse(package_s, vec!["cargo-outdated".to_string()]).unwrap(),
235    ///            RegistryPackage {
236    ///                name: "cargo-outdated".to_string(),
237    ///                registry: "file:///usr/local/share/cargo".into(),
238    ///                version: Some(Semver::parse("0.2.0").unwrap()),
239    ///                newest_version: None,
240    ///                alternative_version: None,
241    ///                max_version: None,
242    ///                executables: vec!["cargo-outdated".to_string()],
243    ///            });
244    /// # }
245    /// ```
246    ///
247    /// Git repository:
248    ///
249    /// ```
250    /// # use cargo_update::ops::RegistryPackage;
251    /// let package_s = "treesize 0.2.1 (git+https://github.com/melak47/treesize-rs#v0.2.1)";
252    /// assert!(RegistryPackage::parse(package_s, vec!["treesize".to_string()]).is_none());
253    /// ```
254    pub fn parse(what: &str, executables: Vec<String>) -> Option<RegistryPackage> {
255        parse_registry_package_ident(what).map(|(name, version, registry)| {
256            RegistryPackage {
257                name: name.to_string(),
258                registry: registry.to_string().into(),
259                version: Some(Semver::parse(version).unwrap()),
260                newest_version: None,
261                alternative_version: None,
262                max_version: None,
263                executables: executables,
264            }
265        })
266    }
267
268    fn want_to_install_prerelease(&self, version_to_install: &Semver, install_prereleases: Option<bool>) -> bool {
269        if install_prereleases.unwrap_or(false) {
270            return true;
271        }
272
273        // otherwise only want to install prerelease if the current version is a prerelease with the same maj.min.patch
274        self.version
275            .as_ref()
276            .map(|cur| {
277                cur.is_prerelease() && cur.major == version_to_install.major && cur.minor == version_to_install.minor && cur.patch == version_to_install.patch
278            })
279            .unwrap_or(false)
280    }
281
282    /// Read the version list for this crate off the specified repository tree and set the latest and alternative versions.
283    pub fn pull_version(&mut self, registry: &RegistryTree, registry_parent: &Registry, install_prereleases: Option<bool>) {
284        let mut vers_git;
285        let vers = match (registry, registry_parent) {
286            (RegistryTree::Git(registry), Registry::Git(registry_parent)) => {
287                vers_git = find_package_data(&self.name, registry, registry_parent)
288                    .ok_or_else(|| format!("package {} not found", self.name))
289                    .and_then(|pd| crate_versions(&pd).map_err(|e| format!("package {}: {}", self.name, e)))
290                    .unwrap();
291                vers_git.sort();
292                &vers_git
293            }
294            (RegistryTree::Sparse, Registry::Sparse(registry_parent)) => &registry_parent[&self.name],
295            _ => unreachable!(),
296        };
297
298        self.newest_version = None;
299        self.alternative_version = None;
300
301        let mut vers = vers.iter().rev();
302        if let Some(newest) = vers.next() {
303            self.newest_version = Some(newest.clone());
304
305            if self.newest_version.as_ref().unwrap().is_prerelease() &&
306               !self.want_to_install_prerelease(self.newest_version.as_ref().unwrap(), install_prereleases) {
307                if let Some(newest_nonpre) = vers.find(|v| !v.is_prerelease()) {
308                    mem::swap(&mut self.alternative_version, &mut self.newest_version);
309                    self.newest_version = Some(newest_nonpre.clone());
310                }
311            }
312        }
313    }
314
315    /// Check whether this package needs to be installed
316    ///
317    /// # Examples
318    ///
319    /// ```
320    /// # extern crate cargo_update;
321    /// # extern crate semver;
322    /// # use semver::{VersionReq as SemverReq, Version as Semver};
323    /// # use cargo_update::ops::RegistryPackage;
324    /// # use std::str::FromStr;
325    /// # fn main() {
326    /// assert!(RegistryPackage {
327    ///             name: "racer".to_string(),
328    ///             registry: "https://github.com/rust-lang/crates.io-index".into(),
329    ///             version: Some(Semver::parse("1.7.2").unwrap()),
330    ///             newest_version: Some(Semver::parse("2.0.6").unwrap()),
331    ///             alternative_version: None,
332    ///             max_version: None,
333    ///             executables: vec!["racer".to_string()],
334    ///         }.needs_update(None, None, false));
335    /// assert!(RegistryPackage {
336    ///             name: "racer".to_string(),
337    ///             registry: "https://github.com/rust-lang/crates.io-index".into(),
338    ///             version: None,
339    ///             newest_version: Some(Semver::parse("2.0.6").unwrap()),
340    ///             alternative_version: None,
341    ///             max_version: None,
342    ///             executables: vec!["racer".to_string()],
343    ///         }.needs_update(None, None, false));
344    /// assert!(RegistryPackage {
345    ///             name: "racer".to_string(),
346    ///             registry: "https://github.com/rust-lang/crates.io-index".into(),
347    ///             version: Some(Semver::parse("2.0.7").unwrap()),
348    ///             newest_version: Some(Semver::parse("2.0.6").unwrap()),
349    ///             alternative_version: None,
350    ///             max_version: None,
351    ///             executables: vec!["racer".to_string()],
352    ///         }.needs_update(None, None, true));
353    /// assert!(!RegistryPackage {
354    ///             name: "racer".to_string(),
355    ///             registry: "https://github.com/rust-lang/crates.io-index".into(),
356    ///             version: Some(Semver::parse("2.0.6").unwrap()),
357    ///             newest_version: Some(Semver::parse("2.0.6").unwrap()),
358    ///             alternative_version: None,
359    ///             max_version: None,
360    ///             executables: vec!["racer".to_string()],
361    ///         }.needs_update(None, None, false));
362    /// assert!(!RegistryPackage {
363    ///             name: "racer".to_string(),
364    ///             registry: "https://github.com/rust-lang/crates.io-index".into(),
365    ///             version: Some(Semver::parse("2.0.6").unwrap()),
366    ///             newest_version: None,
367    ///             alternative_version: None,
368    ///             max_version: None,
369    ///             executables: vec!["racer".to_string()],
370    ///         }.needs_update(None, None, false));
371    ///
372    /// let req = SemverReq::from_str("^1.7").unwrap();
373    /// assert!(RegistryPackage {
374    ///             name: "racer".to_string(),
375    ///             registry: "https://github.com/rust-lang/crates.io-index".into(),
376    ///             version: Some(Semver::parse("1.7.2").unwrap()),
377    ///             newest_version: Some(Semver::parse("1.7.3").unwrap()),
378    ///             alternative_version: None,
379    ///             max_version: None,
380    ///             executables: vec!["racer".to_string()],
381    ///         }.needs_update(Some(&req), None, false));
382    /// assert!(RegistryPackage {
383    ///             name: "racer".to_string(),
384    ///             registry: "https://github.com/rust-lang/crates.io-index".into(),
385    ///             version: None,
386    ///             newest_version: Some(Semver::parse("2.0.6").unwrap()),
387    ///             alternative_version: None,
388    ///             max_version: None,
389    ///             executables: vec!["racer".to_string()],
390    ///         }.needs_update(Some(&req), None, false));
391    /// assert!(!RegistryPackage {
392    ///             name: "racer".to_string(),
393    ///             registry: "https://github.com/rust-lang/crates.io-index".into(),
394    ///             version: Some(Semver::parse("1.7.2").unwrap()),
395    ///             newest_version: Some(Semver::parse("2.0.6").unwrap()),
396    ///             alternative_version: None,
397    ///             max_version: None,
398    ///             executables: vec!["racer".to_string()],
399    ///         }.needs_update(Some(&req), None, false));
400    ///
401    /// assert!(!RegistryPackage {
402    ///             name: "cargo-audit".to_string(),
403    ///             registry: "https://github.com/rust-lang/crates.io-index".into(),
404    ///             version: None,
405    ///             newest_version: Some(Semver::parse("0.9.0-beta2").unwrap()),
406    ///             alternative_version: None,
407    ///             max_version: None,
408    ///             executables: vec!["racer".to_string()],
409    ///         }.needs_update(Some(&req), None, false));
410    /// assert!(RegistryPackage {
411    ///             name: "cargo-audit".to_string(),
412    ///             registry: "https://github.com/rust-lang/crates.io-index".into(),
413    ///             version: None,
414    ///             newest_version: Some(Semver::parse("0.9.0-beta2").unwrap()),
415    ///             alternative_version: None,
416    ///             max_version: None,
417    ///             executables: vec!["racer".to_string()],
418    ///         }.needs_update(Some(&req), Some(true), false));
419    /// # }
420    /// ```
421    pub fn needs_update(&self, req: Option<&SemverReq>, install_prereleases: Option<bool>, downdate: bool) -> bool {
422        fn criterion(fromver: &Semver, tover: &Semver, downdate: bool) -> bool {
423            if downdate {
424                fromver != tover
425            } else {
426                fromver < tover
427            }
428        }
429
430        let update_to_version = self.update_to_version();
431
432        (req.into_iter().zip(self.version.as_ref()).map(|(sr, cv)| !sr.matches(cv)).next().unwrap_or(true) ||
433         req.into_iter().zip(update_to_version).map(|(sr, uv)| sr.matches(uv)).next().unwrap_or(true)) &&
434        update_to_version.map(|upd_v| {
435                (!upd_v.is_prerelease() || self.want_to_install_prerelease(upd_v, install_prereleases)) &&
436                (self.version.is_none() || criterion(self.version.as_ref().unwrap(), upd_v, downdate))
437            })
438            .unwrap_or(false)
439    }
440
441    /// Get package version to update to, or `None` if the crate has no newest version (was yanked)
442    ///
443    /// # Examples
444    ///
445    /// ```
446    /// # extern crate cargo_update;
447    /// # extern crate semver;
448    /// # use cargo_update::ops::RegistryPackage;
449    /// # use semver::Version as Semver;
450    /// # fn main() {
451    /// assert_eq!(RegistryPackage {
452    ///                name: "racer".to_string(),
453    ///                registry: "https://github.com/rust-lang/crates.io-index".into(),
454    ///                version: Some(Semver::parse("1.7.2").unwrap()),
455    ///                newest_version: Some(Semver::parse("2.0.6").unwrap()),
456    ///                alternative_version: None,
457    ///                max_version: Some(Semver::parse("2.0.5").unwrap()),
458    ///                executables: vec!["racer".to_string()],
459    ///            }.update_to_version(),
460    ///            Some(&Semver::parse("2.0.5").unwrap()));
461    /// assert_eq!(RegistryPackage {
462    ///                name: "gutenberg".to_string(),
463    ///                registry: "https://github.com/rust-lang/crates.io-index".into(),
464    ///                version: Some(Semver::parse("0.0.7").unwrap()),
465    ///                newest_version: None,
466    ///                alternative_version: None,
467    ///                max_version: None,
468    ///                executables: vec!["gutenberg".to_string()],
469    ///            }.update_to_version(),
470    ///            None);
471    /// # }
472    /// ```
473    pub fn update_to_version(&self) -> Option<&Semver> {
474        self.newest_version.as_ref().map(|new_v| cmp::min(new_v, self.max_version.as_ref().unwrap_or(new_v)))
475    }
476}
477
478impl GitRepoPackage {
479    /// Try to decypher a package descriptor into a `GitRepoPackage`.
480    ///
481    /// Will return `None` if:
482    ///
483    ///   * the given package descriptor is invalid, or
484    ///   * the package descriptor is not from a git repository.
485    ///
486    /// In the returned instance, `newest_version` is always `None`, get it via `GitRepoPackage::pull_version()`.
487    ///
488    /// The executable list is used as-is.
489    ///
490    /// # Examples
491    ///
492    /// Remote git repo packages:
493    ///
494    /// ```
495    /// # extern crate cargo_update;
496    /// # extern crate git2;
497    /// # use cargo_update::ops::GitRepoPackage;
498    /// # fn main() {
499    /// let package_s = "alacritty 0.1.0 (git+https://github.com/jwilm/alacritty#eb231b3e70b87875df4bdd1974d5e94704024d70)";
500    /// assert_eq!(GitRepoPackage::parse(package_s, vec!["alacritty".to_string()]).unwrap(),
501    ///            GitRepoPackage {
502    ///                name: "alacritty".to_string(),
503    ///                url: "https://github.com/jwilm/alacritty".to_string(),
504    ///                branch: None,
505    ///                id: git2::Oid::from_str("eb231b3e70b87875df4bdd1974d5e94704024d70").unwrap(),
506    ///                newest_id: Err(git2::Error::from_str("")),
507    ///                executables: vec!["alacritty".to_string()],
508    ///            });
509    ///
510    /// let package_s = "chattium-oxide-client 0.1.0 \
511    ///                  (git+https://github.com/nabijaczleweli/chattium-oxide-client\
512    ///                       ?branch=master#108a7b94f0e0dcb2a875f70fc0459d5a682df14c)";
513    /// assert_eq!(GitRepoPackage::parse(package_s, vec!["chattium-oxide-client.exe".to_string()]).unwrap(),
514    ///            GitRepoPackage {
515    ///                name: "chattium-oxide-client".to_string(),
516    ///                url: "https://github.com/nabijaczleweli/chattium-oxide-client".to_string(),
517    ///                branch: Some("master".to_string()),
518    ///                id: git2::Oid::from_str("108a7b94f0e0dcb2a875f70fc0459d5a682df14c").unwrap(),
519    ///                newest_id: Err(git2::Error::from_str("")),
520    ///                executables: vec!["chattium-oxide-client.exe".to_string()],
521    ///            });
522    /// # }
523    /// ```
524    ///
525    /// Main repository package:
526    ///
527    /// ```
528    /// # use cargo_update::ops::GitRepoPackage;
529    /// let package_s = "racer 1.2.10 (registry+https://github.com/rust-lang/crates.io-index)";
530    /// assert!(GitRepoPackage::parse(package_s, vec!["racer".to_string()]).is_none());
531    /// ```
532    pub fn parse(what: &str, executables: Vec<String>) -> Option<GitRepoPackage> {
533        parse_git_package_ident(what).map(|(name, url, sha)| {
534            let mut url = Url::parse(url).unwrap();
535            let branch = url.query_pairs().find(|&(ref name, _)| name == "branch").map(|(_, value)| value.to_string());
536            url.set_query(None);
537            GitRepoPackage {
538                name: name.to_string(),
539                url: url.into(),
540                branch: branch,
541                id: Oid::from_str(sha).unwrap(),
542                newest_id: Err(GitError::from_str("")),
543                executables: executables,
544            }
545        })
546    }
547
548    /// Clone the repo and check what the latest commit's hash is.
549    pub fn pull_version<Pt: AsRef<Path>, Pg: AsRef<Path>>(&mut self, temp_dir: Pt, git_db_dir: Pg, http_proxy: Option<&str>, fork_git: bool) {
550        self.pull_version_impl(temp_dir.as_ref(), git_db_dir.as_ref(), http_proxy, fork_git)
551    }
552
553    fn pull_version_impl(&mut self, temp_dir: &Path, git_db_dir: &Path, http_proxy: Option<&str>, fork_git: bool) {
554        let clone_dir = find_git_db_repo(git_db_dir, &self.url).unwrap_or_else(|| temp_dir.join(&self.name));
555        if !clone_dir.exists() {
556            self.newest_id = if fork_git {
557                Command::new(env::var_os("GIT").as_ref().map(OsString::as_os_str).unwrap_or(OsStr::new("git")))
558                    .args(&["ls-remote", "--", &self.url, self.branch.as_ref().map(String::as_str).unwrap_or("HEAD")])
559                    .arg(&clone_dir)
560                    .stderr(Stdio::inherit())
561                    .output()
562                    .ok()
563                    .filter(|s| s.status.success())
564                    .map(|s| s.stdout)
565                    .and_then(|o| String::from_utf8(o).ok())
566                    .and_then(|o| o.split('\t').next().and_then(|o| Oid::from_str(o).ok()))
567                    .ok_or(GitError::from_str(""))
568            } else {
569                with_authentication(&self.url, |creds| {
570                    git2::Remote::create_detached(self.url.clone()).and_then(|mut r| {
571                        let mut cb = RemoteCallbacks::new();
572                        cb.credentials(|a, b, c| creds(a, b, c));
573                        r.connect_auth(git2::Direction::Fetch,
574                                          Some(cb),
575                                          http_proxy.map(|http_proxy| proxy_options_from_proxy_url(&self.url, http_proxy)))
576                            .and_then(|rc| {
577                                rc.list()?
578                                    .into_iter()
579                                    .find(|rh| match self.branch.as_ref() {
580                                        Some(b) => {
581                                            if rh.name().starts_with("refs/heads/") {
582                                                rh.name()["refs/heads/".len()..] == b[..]
583                                            } else if rh.name().starts_with("refs/tags/") {
584                                                rh.name()["refs/tags/".len()..] == b[..]
585                                            } else {
586                                                false
587                                            }
588                                        }
589                                        None => rh.name() == "HEAD",
590                                    })
591                                    .map(|rh| rh.oid())
592                                    .ok_or(git2::Error::from_str(""))
593                            })
594                    })
595                })
596            };
597            if self.newest_id.is_ok() {
598                return;
599            }
600        }
601
602        let repo = self.pull_version_repo(&clone_dir, http_proxy, fork_git);
603
604        self.newest_id = repo.and_then(|r| r.head().and_then(|h| h.target().ok_or_else(|| GitError::from_str("HEAD not a direct reference"))));
605    }
606
607    fn pull_version_fresh_clone(&self, clone_dir: &Path, http_proxy: Option<&str>, fork_git: bool) -> Result<Repository, GitError> {
608        if fork_git {
609            Command::new(env::var_os("GIT").as_ref().map(OsString::as_os_str).unwrap_or(OsStr::new("git")))
610                .arg("clone")
611                .args(self.branch.as_ref().map(|_| "-b"))
612                .args(self.branch.as_ref())
613                .args(&["--bare", "--", &self.url])
614                .arg(clone_dir)
615                .status()
616                .map_err(|e| GitError::from_str(&e.to_string()))
617                .and_then(|e| if e.success() {
618                    Repository::open(clone_dir)
619                } else {
620                    Err(GitError::from_str(&e.to_string()))
621                })
622        } else {
623            with_authentication(&self.url, |creds| {
624                let mut bldr = git2::build::RepoBuilder::new();
625
626                let mut cb = RemoteCallbacks::new();
627                cb.credentials(|a, b, c| creds(a, b, c));
628                bldr.fetch_options(fetch_options_from_proxy_url_and_callbacks(&self.url, http_proxy, cb));
629                if let Some(ref b) = self.branch.as_ref() {
630                    bldr.branch(b);
631                }
632
633                bldr.bare(true);
634                bldr.clone(&self.url, &clone_dir)
635            })
636        }
637    }
638
639    fn pull_version_repo(&self, clone_dir: &Path, http_proxy: Option<&str>, fork_git: bool) -> Result<Repository, GitError> {
640        if let Ok(r) = Repository::open(clone_dir) {
641            // If `Repository::open` is successful, both `clone_dir` exists *and* points to a valid repository.
642            //
643            // Fetch the specified or default branch, reset it to the remote HEAD.
644
645            let (branch, tofetch) = match self.branch.as_ref() {
646                Some(b) => {
647                    // Cargo doesn't point the HEAD at the chosen (via "--branch") branch when installing
648                    // https://github.com/nabijaczleweli/cargo-update/issues/143
649                    r.set_head(&format!("refs/heads/{}", b)).map_err(|e| panic!("Couldn't set HEAD to chosen branch {}: {}", b, e)).unwrap();
650                    (Cow::from(b), Cow::from(b))
651                }
652
653                None => {
654                    match r.find_reference("HEAD")
655                        .map_err(|e| panic!("No HEAD in {}: {}", clone_dir.display(), e))
656                        .unwrap()
657                        .symbolic_target() {
658                        Some(ht) => (ht["refs/heads/".len()..].to_string().into(), "+HEAD:refs/remotes/origin/HEAD".into()),
659                        None => {
660                            // Versions up to v4.0.0 (well, 59be1c0de283dabce320a860a3d533d00910a6a9, but who's counting)
661                            // called r.set_head("FETCH_HEAD"), which made HEAD a direct SHA reference.
662                            // This is obviously problematic when trying to read the default branch, and these checkouts can persist
663                            // (https://github.com/nabijaczleweli/cargo-update/issues/139#issuecomment-665847290);
664                            // yeeting them shouldn't be a problem, since that's what we *would* do anyway,
665                            // and we set up for the non-pessimised path in later runs.
666                            fs::remove_dir_all(clone_dir).unwrap();
667                            return self.pull_version_fresh_clone(clone_dir, http_proxy, fork_git);
668                        }
669                    }
670
671                }
672            };
673
674            let mut remote = "origin";
675            r.find_remote("origin")
676                .or_else(|_| {
677                    remote = &self.url;
678                    r.remote_anonymous(&self.url)
679                })
680                .and_then(|mut rm| if fork_git {
681                    Command::new(env::var_os("GIT").as_ref().map(OsString::as_os_str).unwrap_or(OsStr::new("git")))
682                        .arg("-C")
683                        .arg(r.path())
684                        .args(&["fetch", remote, &tofetch])
685                        .status()
686                        .map_err(|e| GitError::from_str(&e.to_string()))
687                        .and_then(|e| if e.success() {
688                            Ok(())
689                        } else {
690                            Err(GitError::from_str(&e.to_string()))
691                        })
692                } else {
693                    with_authentication(&self.url, |creds| {
694                        let mut cb = RemoteCallbacks::new();
695                        cb.credentials(|a, b, c| creds(a, b, c));
696
697                        rm.fetch(&[&tofetch[..]],
698                                 Some(&mut fetch_options_from_proxy_url_and_callbacks(&self.url, http_proxy, cb)),
699                                 None)
700                    })
701                })
702                .map_err(|e| panic!("Fetching {} from {}: {}", clone_dir.display(), self.url, e))
703                .unwrap();
704            r.branch(&branch,
705                        &r.find_reference("FETCH_HEAD")
706                            .map_err(|e| panic!("No FETCH_HEAD in {}: {}", clone_dir.display(), e))
707                            .unwrap()
708                            .peel_to_commit()
709                            .map_err(|e| panic!("FETCH_HEAD not a commit in {}: {}", clone_dir.display(), e))
710                            .unwrap(),
711                        true)
712                .map_err(|e| panic!("Setting local branch {} in {}: {}", branch, clone_dir.display(), e))
713                .unwrap();
714            Ok(r)
715        } else {
716            // If we could not open the repository either it does not exist, or exists but is invalid,
717            // in which case remove it to trigger a fresh clone.
718            let _ = fs::remove_dir_all(&clone_dir).or_else(|_| fs::remove_file(&clone_dir));
719
720            self.pull_version_fresh_clone(clone_dir, http_proxy, fork_git)
721        }
722    }
723
724    /// Check whether this package needs to be installed
725    ///
726    /// # Examples
727    ///
728    /// ```
729    /// # extern crate cargo_update;
730    /// # extern crate git2;
731    /// # use cargo_update::ops::GitRepoPackage;
732    /// # fn main() {
733    /// assert!(GitRepoPackage {
734    ///             name: "alacritty".to_string(),
735    ///             url: "https://github.com/jwilm/alacritty".to_string(),
736    ///             branch: None,
737    ///             id: git2::Oid::from_str("eb231b3e70b87875df4bdd1974d5e94704024d70").unwrap(),
738    ///             newest_id: git2::Oid::from_str("5f7885749c4d7e48869b1fc0be4d430601cdbbfa"),
739    ///             executables: vec!["alacritty".to_string()],
740    ///         }.needs_update());
741    /// assert!(!GitRepoPackage {
742    ///             name: "alacritty".to_string(),
743    ///             url: "https://github.com/jwilm/alacritty".to_string(),
744    ///             branch: None,
745    ///             id: git2::Oid::from_str("5f7885749c4d7e48869b1fc0be4d430601cdbbfa").unwrap(),
746    ///             newest_id: git2::Oid::from_str("5f7885749c4d7e48869b1fc0be4d430601cdbbfa"),
747    ///             executables: vec!["alacritty".to_string()],
748    ///         }.needs_update());
749    /// # }
750    /// ```
751    pub fn needs_update(&self) -> bool {
752        self.newest_id.is_ok() && self.id != *self.newest_id.as_ref().unwrap()
753    }
754}
755
756
757/// One of elements with which to filter required packages.
758#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
759pub enum PackageFilterElement {
760    /// Requires toolchain to be specified to the specified toolchain.
761    ///
762    /// Parsed name: `"toolchain"`.
763    Toolchain(String),
764}
765
766impl PackageFilterElement {
767    /// Parse one filter specifier into up to one package filter
768    ///
769    /// # Examples
770    ///
771    /// ```
772    /// # use cargo_update::ops::PackageFilterElement;
773    /// assert_eq!(PackageFilterElement::parse("toolchain=nightly"),
774    ///            Ok(PackageFilterElement::Toolchain("nightly".to_string())));
775    ///
776    /// assert!(PackageFilterElement::parse("capitalism").is_err());
777    /// assert!(PackageFilterElement::parse("communism=good").is_err());
778    /// ```
779    pub fn parse(from: &str) -> Result<PackageFilterElement, String> {
780        let (key, value) = from.split_at(from.find('=').ok_or_else(|| format!(r#"Filter string "{}" does not contain the key/value separator "=""#, from))?);
781        let value = &value[1..];
782
783        Ok(match key {
784            "toolchain" => PackageFilterElement::Toolchain(value.to_string()),
785            _ => return Err(format!(r#"Unrecognised filter key "{}""#, key)),
786        })
787    }
788
789    /// Check if the specified package config matches this filter element.
790    ///
791    /// # Examples
792    ///
793    /// ```
794    /// # use cargo_update::ops::{PackageFilterElement, ConfigOperation, PackageConfig};
795    /// assert!(PackageFilterElement::Toolchain("nightly".to_string())
796    ///     .matches(&PackageConfig::from(&[ConfigOperation::SetToolchain("nightly".to_string())])));
797    ///
798    /// assert!(!PackageFilterElement::Toolchain("nightly".to_string()).matches(&PackageConfig::from(&[])));
799    /// ```
800    pub fn matches(&self, cfg: &PackageConfig) -> bool {
801        match *self {
802            PackageFilterElement::Toolchain(ref chain) => Some(chain) == cfg.toolchain.as_ref(),
803        }
804    }
805}
806
807
808/// `cargo` configuration, as obtained from `.cargo/config[.toml]`
809#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
810pub struct CargoConfig {
811    pub net_git_fetch_with_cli: bool,
812    /// https://blog.rust-lang.org/2023/03/09/Rust-1.68.0.html#cargos-sparse-protocol
813    /// https://doc.rust-lang.org/stable/cargo/reference/registry-index.html#sparse-protocol
814    pub registries_crates_io_protocol_sparse: bool,
815    pub http: HttpCargoConfig,
816    pub sparse_registries: SparseRegistryConfig,
817}
818
819#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
820pub struct HttpCargoConfig {
821    pub cainfo: Option<PathBuf>,
822    pub check_revoke: bool,
823}
824
825/// https://github.com/nabijaczleweli/cargo-update/issues/300
826#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
827pub struct SparseRegistryConfig {
828    pub global_credential_providers: Vec<SparseRegistryAuthProvider>,
829    pub crates_io_credential_provider: Option<SparseRegistryAuthProvider>,
830    pub crates_io_token_env: Option<String>,
831    pub crates_io_token: Option<String>,
832    pub registry_tokens_env: BTreeMap<CargoConfigEnvironmentNormalisedString, String>,
833    pub registry_tokens: BTreeMap<String, String>,
834    pub credential_aliases: BTreeMap<CargoConfigEnvironmentNormalisedString, Vec<String>>,
835}
836
837impl SparseRegistryConfig {
838    pub fn credential_provider(&self, v: toml::Value) -> Option<SparseRegistryAuthProvider> {
839        SparseRegistryConfig::credential_provider_impl(&self.credential_aliases, v)
840    }
841
842    fn credential_provider_impl(credential_aliases: &BTreeMap<CargoConfigEnvironmentNormalisedString, Vec<String>>, v: toml::Value)
843                                -> Option<SparseRegistryAuthProvider> {
844        match v {
845            toml::Value::String(s) => Some(CargoConfig::string_provider(s, &credential_aliases)),
846            toml::Value::Array(a) => Some(SparseRegistryAuthProvider::from_config(CargoConfig::string_array(a))),
847            _ => None,
848        }
849    }
850}
851
852/// https://doc.rust-lang.org/cargo/reference/registry-authentication.html
853#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
854pub enum SparseRegistryAuthProvider {
855    /// The default; does not read `CARGO_REGISTRY_TOKEN` or `CARGO_REGISTRIES_{}_TOKEN` environment variables.
856    TokenNoEnvironment,
857    /// `cargo:token`
858    Token,
859    /// `cargo:wincred` (Win32)
860    Wincred,
861    /// `cargo:macos-keychain` (Apple)
862    MacosKeychain,
863    /// `cargo:libsecret`; this `dlopen()`s `libsecret-1.so.0` on non-Apple UNIX.
864    Libsecret,
865    /// `cargo:token-from-stdout prog arg arg`
866    TokenFromStdout(Vec<String>),
867    /// Not `cargo:`-prefixed
868    ///
869    /// https://doc.rust-lang.org/cargo/reference/credential-provider-protocol.html
870    ///
871    /// We do *not* care about `"cache"`, `"expiration"`, or `"operation_independent"`,
872    /// always behaving as-if `"never"`/`_`/`true`.
873    ///
874    /// We don't provide the optional `{"registry": {"headers": ...}}` field.
875    Provider(Vec<String>),
876}
877
878impl SparseRegistryAuthProvider {
879    /// Parses a `["cargo:token-from-stdout", "whatever"]`-style entry
880    pub fn from_config(mut toks: Vec<String>) -> SparseRegistryAuthProvider {
881        match toks.get(0).map(String::as_str).unwrap_or("") {
882            "cargo:token" => SparseRegistryAuthProvider::Token,
883            "cargo:wincred" => SparseRegistryAuthProvider::Wincred,
884            "cargo:macos-keychain" => SparseRegistryAuthProvider::MacosKeychain,
885            "cargo:libsecret" => SparseRegistryAuthProvider::Libsecret,
886            "cargo:token-from-stdout" => {
887                toks.remove(0);
888                SparseRegistryAuthProvider::TokenFromStdout(toks)
889            }
890            _ => SparseRegistryAuthProvider::Provider(toks),
891        }
892    }
893}
894
895/// https://doc.rust-lang.org/cargo/reference/config.html#environment-variables
896#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
897pub struct CargoConfigEnvironmentNormalisedString(pub String);
898impl CargoConfigEnvironmentNormalisedString {
899    /// `tr a-z.- A-Z__`
900    pub fn normalise(mut s: String) -> CargoConfigEnvironmentNormalisedString {
901        s.make_ascii_uppercase();
902        while let Some(i) = s.find(['.', '-']) {
903            s.replace_range(i..i + 1, "_");
904        }
905        CargoConfigEnvironmentNormalisedString(s)
906    }
907}
908
909impl CargoConfig {
910    pub fn load(crates_file: &Path) -> CargoConfig {
911        let mut cfg = fs::read_to_string(crates_file.with_file_name("config"))
912            .or_else(|_| fs::read_to_string(crates_file.with_file_name("config.toml")))
913            .ok()
914            .and_then(|s| s.parse::<toml::Value>().ok());
915        let mut creds = fs::read_to_string(crates_file.with_file_name("credentials"))
916            .or_else(|_| fs::read_to_string(crates_file.with_file_name("credentials.toml")))
917            .ok()
918            .and_then(|s| s.parse::<toml::Value>().ok());
919
920        let credential_aliases = None.or_else(|| match cfg.as_mut()?.as_table_mut()?.remove("credential-alias")? {
921                toml::Value::Table(t) => Some(t),
922                _ => None,
923            })
924            .unwrap_or_default()
925            .into_iter()
926            .flat_map(|(k, v)| {
927                match v {
928                        toml::Value::String(s) => Some(s.split(' ').map(String::from).collect()),
929                        toml::Value::Array(a) => Some(CargoConfig::string_array(a)),
930                        _ => None,
931                    }
932                    .map(|v| (CargoConfigEnvironmentNormalisedString::normalise(k), v))
933            })
934            .chain(env::vars_os()
935                .map(|(k, v)| (k.into_encoded_bytes(), v))
936                .filter(|(k, _)| k.starts_with(b"CARGO_CREDENTIAL_ALIAS_"))
937                .filter(|(k, _)| k["CARGO_CREDENTIAL_ALIAS_".len()..].iter().all(|&b| !(b.is_ascii_lowercase() || b == b'.' || b == b'-')))
938                .flat_map(|(mut k, v)| {
939                    let k = String::from_utf8(k.drain("CARGO_CREDENTIAL_ALIAS_".len()..).collect()).ok()?;
940                    let v = v.into_string().ok()?;
941                    Some((CargoConfigEnvironmentNormalisedString(k), v.split(' ').map(String::from).collect()))
942                }))
943            .collect();
944
945        CargoConfig {
946            net_git_fetch_with_cli: env::var("CARGO_NET_GIT_FETCH_WITH_CLI")
947                .ok()
948                .and_then(|e| if e.is_empty() {
949                    Some(toml::Value::String(String::new()))
950                } else {
951                    e.parse::<toml::Value>().ok()
952                })
953                .or_else(|| {
954                    cfg.as_mut()?
955                        .as_table_mut()?
956                        .get_mut("net")?
957                        .as_table_mut()?
958                        .remove("git-fetch-with-cli")
959                })
960                .map(CargoConfig::truthy)
961                .unwrap_or(false),
962            registries_crates_io_protocol_sparse: env::var("CARGO_REGISTRIES_CRATES_IO_PROTOCOL")
963                .map(|s| s == "sparse")
964                .ok()
965                .or_else(|| {
966                    Some(cfg.as_mut()?
967                        .as_table_mut()?
968                        .get_mut("registries")?
969                        .as_table_mut()?
970                        .get_mut("crates-io")?
971                        .as_table_mut()?
972                        .remove("protocol")?
973                        .as_str()? == "sparse")
974                })
975                // // Horrifically expensive (82-93ms end-to-end) and largely unnecessary
976                // .or_else(|| {
977                //     let mut l = String::new();
978                //     // let before = std::time::Instant::now();
979                //     BufReader::new(Command::new(cargo).arg("version").stdout(Stdio::piped()).spawn().ok()?.stdout?).read_line(&mut l).ok()?;
980                //     // let after = std::time::Instant::now();
981                //
982                //     // cargo 1.63.0 (fd9c4297c 2022-07-01)
983                //     Some(Semver::parse(l.split_whitespace().nth(1)?).ok()? >= Semver::new(1, 70, 0))
984                // })
985                // .unwrap_or(false),
986                .unwrap_or(true),
987            http: HttpCargoConfig {
988                cainfo: env::var_os("CARGO_HTTP_CAINFO")
989                    .map(PathBuf::from)
990                    .or_else(|| {
991                        CargoConfig::string(cfg.as_mut()?
992                                .as_table_mut()?
993                                .get_mut("http")?
994                                .as_table_mut()?
995                                .remove("cainfo")?)
996                            .map(PathBuf::from)
997                    }),
998                check_revoke: env::var("CARGO_HTTP_CHECK_REVOKE")
999                    .ok()
1000                    .map(toml::Value::String)
1001                    .or_else(|| {
1002                        cfg.as_mut()?
1003                            .as_table_mut()?
1004                            .get_mut("http")?
1005                            .as_table_mut()?
1006                            .remove("check-revoke")
1007                    })
1008                    .map(CargoConfig::truthy)
1009                    .unwrap_or(cfg!(target_os = "windows")),
1010            },
1011            sparse_registries: SparseRegistryConfig {
1012                // Supposedly this is CARGO_REGISTRY_GLOBAL_CREDENTIAL_PROVIDERS but they don't specify how they serialise arrays so
1013                global_credential_providers: None.or_else(|| {
1014                        CargoConfig::string_array_v(cfg.as_mut()?
1015                            .as_table_mut()?
1016                            .get_mut("registry")?
1017                            .as_table_mut()?
1018                            .remove("global-credential-providers")?)
1019                    })
1020                    .map(|a| a.into_iter().map(|s| CargoConfig::string_provider(s, &credential_aliases)).collect())
1021                    .unwrap_or_else(|| vec![SparseRegistryAuthProvider::TokenNoEnvironment]),
1022                crates_io_credential_provider: env::var("CARGO_REGISTRY_CREDENTIAL_PROVIDER")
1023                    .ok()
1024                    .map(toml::Value::String)
1025                    .or_else(|| {
1026                        cfg.as_mut()?
1027                            .as_table_mut()?
1028                            .get_mut("registry")?
1029                            .as_table_mut()?
1030                            .remove("credential-provider")
1031                    })
1032                    .and_then(|v| SparseRegistryConfig::credential_provider_impl(&credential_aliases, v)),
1033                crates_io_token_env: env::var("CARGO_REGISTRY_TOKEN").ok(),
1034                crates_io_token: None.or_else(|| {
1035                        CargoConfig::string(creds.as_mut()?
1036                            .as_table_mut()?
1037                            .get_mut("registry")?
1038                            .as_table_mut()?
1039                            .remove("token")?)
1040                    })
1041                    .or_else(|| {
1042                        CargoConfig::string(cfg.as_mut()?
1043                            .as_table_mut()?
1044                            .get_mut("registry")?
1045                            .as_table_mut()?
1046                            .remove("token")?)
1047                    }),
1048                registry_tokens_env: env::vars_os()
1049                    .map(|(k, v)| (k.into_encoded_bytes(), v))
1050                    .filter(|(k, _)| k.starts_with(b"CARGO_REGISTRIES_") && k.ends_with(b"_TOKEN"))
1051                    .filter(|(k, _)| {
1052                        k["CARGO_REGISTRIES_".len()..k.len() - b"_TOKEN".len()].iter().all(|&b| !(b.is_ascii_lowercase() || b == b'.' || b == b'-'))
1053                    })
1054                    .flat_map(|(mut k, v)| {
1055                        let k = String::from_utf8(k.drain("CARGO_REGISTRIES_".len()..k.len() - b"_TOKEN".len()).collect()).ok()?;
1056                        Some((CargoConfigEnvironmentNormalisedString(k), v.into_string().ok()?))
1057                    })
1058                    .collect(),
1059                registry_tokens: cfg.as_mut()
1060                    .into_iter()
1061                    .chain(creds.as_mut())
1062                    .flat_map(|c| {
1063                        c.as_table_mut()?
1064                            .get_mut("registries")?
1065                            .as_table_mut()
1066                    })
1067                    .flat_map(|r| r.into_iter().flat_map(|(name, v)| Some((name.clone(), CargoConfig::string(v.as_table_mut()?.remove("token")?)?))))
1068                    .collect(),
1069                credential_aliases: credential_aliases,
1070            },
1071        }
1072    }
1073
1074    fn truthy(v: toml::Value) -> bool {
1075        match v {
1076            toml::Value::String(ref s) if s == "" => false,
1077            toml::Value::Float(0.) => false,
1078            toml::Value::Integer(0) |
1079            toml::Value::Boolean(false) => false,
1080            _ => true,
1081        }
1082    }
1083
1084    fn string(v: toml::Value) -> Option<String> {
1085        match v {
1086            toml::Value::String(s) => Some(s),
1087            _ => None,
1088        }
1089    }
1090
1091    fn string_array(a: Vec<toml::Value>) -> Vec<String> {
1092        a.into_iter().flat_map(CargoConfig::string).collect()
1093    }
1094
1095    fn string_array_v(v: toml::Value) -> Option<Vec<String>> {
1096        match v {
1097            toml::Value::Array(s) => Some(CargoConfig::string_array(s)),
1098            _ => None,
1099        }
1100    }
1101
1102    fn string_provider(s: String, credential_aliases: &BTreeMap<CargoConfigEnvironmentNormalisedString, Vec<String>>) -> SparseRegistryAuthProvider {
1103        match credential_aliases.get(&CargoConfigEnvironmentNormalisedString::normalise(s.clone())) {
1104            Some(av) => SparseRegistryAuthProvider::Provider(av.clone()),
1105            None => {
1106                SparseRegistryAuthProvider::from_config(if s.contains(' ') {
1107                    s.split(' ').map(String::from).collect()
1108                } else {
1109                    vec![s]
1110                })
1111            }
1112        }
1113    }
1114}
1115
1116
1117/// [Follow `install.root`](https://github.com/nabijaczleweli/cargo-update/issues/23) in the `config` or `config.toml` file
1118/// in the cargo directory specified.
1119///
1120/// # Examples
1121///
1122/// ```
1123/// # use cargo_update::ops::crates_file_in;
1124/// # use std::env::temp_dir;
1125/// # let cargo_dir = temp_dir();
1126/// let cargo_dir = crates_file_in(&cargo_dir);
1127/// # let _ = cargo_dir;
1128/// ```
1129pub fn crates_file_in(cargo_dir: &Path) -> PathBuf {
1130    crates_file_in_impl(cargo_dir, BTreeSet::new())
1131}
1132fn crates_file_in_impl<'cd>(cargo_dir: &'cd Path, mut seen: BTreeSet<&'cd Path>) -> PathBuf {
1133    if !seen.insert(cargo_dir) {
1134        panic!("Cargo config install.root loop at {:?} (saw {:?})", cargo_dir.display(), seen);
1135    }
1136
1137    let mut config_file = cargo_dir.join("config");
1138    let mut config_data = fs::read_to_string(&config_file);
1139    if config_data.is_err() {
1140        config_file.set_file_name("config.toml");
1141        config_data = fs::read_to_string(&config_file);
1142    }
1143    if let Ok(config_data) = config_data {
1144        if let Some(idir) = toml::from_str::<toml::Value>(&config_data)
1145            .unwrap()
1146            .get("install")
1147            .and_then(|t| t.as_table())
1148            .and_then(|t| t.get("root"))
1149            .and_then(|t| t.as_str()) {
1150            return crates_file_in_impl(Path::new(idir), seen);
1151        }
1152    }
1153
1154    config_file.set_file_name(".crates.toml");
1155    config_file
1156}
1157
1158fn installed_packages_table(crates_file: &Path) -> Option<toml::Table> {
1159    let crates_data = fs::read_to_string(crates_file).ok()?;
1160    Some(toml::from_str::<toml::Value>(&crates_data).unwrap().get_mut("v1")?.as_table_mut().map(mem::take).unwrap())
1161}
1162
1163/// List the installed packages at the specified location that originate
1164/// from the a cargo registry.
1165///
1166/// If the `.crates.toml` file doesn't exist an empty vector is returned.
1167///
1168/// This also deduplicates packages and assumes the latest version as the correct one to work around
1169/// [#44](https://github.com/nabijaczleweli/cargo-update/issues/44) a.k.a.
1170/// [rust-lang/cargo#4321](https://github.com/rust-lang/cargo/issues/4321).
1171///
1172/// # Examples
1173///
1174/// ```
1175/// # use cargo_update::ops::installed_registry_packages;
1176/// # use std::env::temp_dir;
1177/// # let cargo_dir = temp_dir().join(".crates.toml");
1178/// let packages = installed_registry_packages(&cargo_dir);
1179/// for package in &packages {
1180///     println!("{} v{}", package.name, package.version.as_ref().unwrap());
1181/// }
1182/// ```
1183pub fn installed_registry_packages(crates_file: &Path) -> Vec<RegistryPackage> {
1184    let mut res = Vec::<RegistryPackage>::new();
1185    for pkg in installed_packages_table(crates_file)
1186        .into_iter()
1187        .flatten()
1188        .flat_map(|(s, x)| CargoConfig::string_array_v(x).and_then(|x| RegistryPackage::parse(&s, x))) {
1189        if let Some(saved) = res.iter_mut().find(|p| p.name == pkg.name) {
1190            if saved.version.is_none() || saved.version.as_ref().unwrap() < pkg.version.as_ref().unwrap() {
1191                saved.version = pkg.version;
1192            }
1193            continue;
1194        }
1195
1196        res.push(pkg);
1197    }
1198    res
1199}
1200
1201/// List the installed packages at the specified location that originate
1202/// from a  remote git repository.
1203///
1204/// If the `.crates.toml` file doesn't exist an empty vector is returned.
1205///
1206/// This also deduplicates packages and assumes the latest-mentioned version as the most correct.
1207///
1208/// # Examples
1209///
1210/// ```
1211/// # use cargo_update::ops::installed_git_repo_packages;
1212/// # use std::env::temp_dir;
1213/// # let cargo_dir = temp_dir().join(".crates.toml");
1214/// let packages = installed_git_repo_packages(&cargo_dir);
1215/// for package in &packages {
1216///     println!("{} v{}", package.name, package.id);
1217/// }
1218/// ```
1219pub fn installed_git_repo_packages(crates_file: &Path) -> Vec<GitRepoPackage> {
1220    let mut res = Vec::<GitRepoPackage>::new();
1221    for pkg in installed_packages_table(crates_file)
1222        .into_iter()
1223        .flatten()
1224        .flat_map(|(s, x)| CargoConfig::string_array_v(x).and_then(|x| GitRepoPackage::parse(&s, x))) {
1225        if let Some(saved) = res.iter_mut().find(|p| p.name == pkg.name) {
1226            saved.id = pkg.id;
1227            continue;
1228        }
1229
1230        res.push(pkg);
1231    }
1232    res
1233}
1234
1235/// Filter out the installed packages not specified to be updated and add the packages you specify to install,
1236/// if they aren't already installed via git.
1237///
1238/// List installed packages with `installed_registry_packages()`.
1239///
1240/// # Examples
1241///
1242/// ```
1243/// # use cargo_update::ops::{RegistryPackage, intersect_packages};
1244/// # fn installed_registry_packages(_: &()) {}
1245/// # let cargo_dir = ();
1246/// # let packages_to_update = [("racer".to_string(), None,
1247/// #                            "registry+https://github.com/rust-lang/crates.io-index".into()),
1248/// #                           ("cargo-outdated".to_string(), None,
1249/// #                            "registry+https://github.com/rust-lang/crates.io-index".into())];
1250/// let mut installed_packages = installed_registry_packages(&cargo_dir);
1251/// # let mut installed_packages =
1252/// #     vec![RegistryPackage::parse("cargo-outdated 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
1253/// #     vec!["cargo-outdated".to_string()]).unwrap(),
1254/// #          RegistryPackage::parse("racer 1.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
1255/// #     vec!["racer.exe".to_string()]).unwrap(),
1256/// #          RegistryPackage::parse("rustfmt 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
1257/// #     vec!["rustfmt".to_string(), "cargo-format".to_string()]).unwrap()];
1258/// installed_packages = intersect_packages(&installed_packages, &packages_to_update, false, &[]);
1259/// # assert_eq!(&installed_packages,
1260/// #   &[RegistryPackage::parse("cargo-outdated 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
1261/// #                            vec!["cargo-outdated".to_string()]).unwrap(),
1262/// #     RegistryPackage::parse("racer 1.2.10 (registry+https://github.com/rust-lang/crates.io-index)",
1263/// #                            vec!["racer.exe".to_string()]).unwrap()]);
1264/// ```
1265pub fn intersect_packages(installed: &[RegistryPackage], to_update: &[(String, Option<Semver>, Cow<'static, str>)], allow_installs: bool,
1266                          installed_git: &[GitRepoPackage])
1267                          -> Vec<RegistryPackage> {
1268    installed.iter()
1269        .filter(|p| to_update.iter().any(|u| p.name == u.0))
1270        .cloned()
1271        .map(|p| RegistryPackage { max_version: to_update.iter().find(|u| p.name == u.0).and_then(|u| u.1.clone()), ..p })
1272        .chain(to_update.iter()
1273            .filter(|p| allow_installs && !installed.iter().any(|i| i.name == p.0) && !installed_git.iter().any(|i| i.name == p.0))
1274            .map(|p| {
1275                RegistryPackage {
1276                    name: p.0.clone(),
1277                    registry: p.2.clone(),
1278                    version: None,
1279                    newest_version: None,
1280                    alternative_version: None,
1281                    max_version: p.1.clone(),
1282                    executables: vec![],
1283                }
1284            }))
1285        .collect()
1286}
1287
1288/// Parse the raw crate descriptor from the repository into a collection of `Semver`s.
1289///
1290/// # Examples
1291///
1292/// ```
1293/// # use cargo_update::ops::crate_versions;
1294/// # use std::fs;
1295/// # let desc_path = "test-data/checksums-versions.json";
1296/// # let package = "checksums";
1297/// let versions = crate_versions(&fs::read(desc_path).unwrap()).expect(package);
1298///
1299/// println!("Released versions of checksums:");
1300/// for ver in &versions {
1301///     println!("  {}", ver);
1302/// }
1303/// ```
1304pub fn crate_versions(buf: &[u8]) -> Result<Vec<Semver>, Cow<'static, str>> {
1305    buf.split(|&b| b == b'\n').filter(|l| !l.is_empty()).try_fold(vec![], |mut acc, p| match json::from_slice(p).map_err(|e| e.to_string())? {
1306        json::Value::Object(o) => {
1307            if !matches!(o.get("yanked"), Some(&json::Value::Bool(true))) {
1308                match o.get("vers").ok_or("no \"vers\" key")? {
1309                    json::Value::String(ref v) => acc.push(Semver::parse(&v).map_err(|e| e.to_string())?),
1310                    _ => Err("\"vers\" not string")?,
1311                }
1312            }
1313            Ok(acc)
1314        }
1315        _ => Err(Cow::from("line not object")),
1316    })
1317}
1318
1319/// Get the location of the registry index corresponding ot the given URL; if not present – make it and its parents.
1320///
1321/// As odd as it may be, this [can happen (if rarely) and is a supported
1322/// configuration](https://github.com/nabijaczleweli/cargo-update/issues/150).
1323///
1324/// Sparse registries do nothing and return a meaningless value.
1325///
1326/// # Examples
1327///
1328/// ```
1329/// # #[cfg(all(target_pointer_width="64", target_endian="little"))] // github.com/nabijaczleweli/cargo-update/issues/235
1330/// # {
1331/// # use cargo_update::ops::assert_index_path;
1332/// # use std::env::temp_dir;
1333/// # use std::path::Path;
1334/// # let cargo_dir = temp_dir().join("cargo_update-doctest").join("assert_index_path-0");
1335/// # let idx_dir = cargo_dir.join("registry").join("index").join("github.com-1ecc6299db9ec823");
1336/// let index = assert_index_path(&cargo_dir, "https://github.com/rust-lang/crates.io-index", false).unwrap();
1337///
1338/// // Use find_package_data() to look for packages
1339/// # assert_eq!(index, idx_dir);
1340/// # assert_eq!(assert_index_path(&cargo_dir, "https://index.crates.io/", true).unwrap(), Path::new("/ENOENT"));
1341/// # }
1342/// ```
1343pub fn assert_index_path(cargo_dir: &Path, registry_url: &str, sparse: bool) -> Result<PathBuf, Cow<'static, str>> {
1344    if sparse {
1345        return Ok(PathBuf::from("/ENOENT"));
1346    }
1347
1348    let path = cargo_dir.join("registry").join("index").join(registry_shortname(registry_url));
1349    match path.metadata() {
1350        Ok(meta) => {
1351            if meta.is_dir() {
1352                Ok(path)
1353            } else {
1354                Err(format!("{} (index directory for {}) not a directory", path.display(), registry_url).into())
1355            }
1356        }
1357        Err(ref e) if e.kind() == IoErrorKind::NotFound => {
1358            fs::create_dir_all(&path).map_err(|e| format!("Couldn't create {} (index directory for {}): {}", path.display(), registry_url, e))?;
1359            Ok(path)
1360        }
1361        Err(e) => Err(format!("Couldn't read {} (index directory for {}): {}", path.display(), registry_url, e).into()),
1362    }
1363}
1364
1365/// Opens or initialises a git repository at `registry`, or returns a blank sparse registry.
1366///
1367/// Error type distinguishes init error from open error.
1368pub fn open_index_repository(registry: &Path, sparse: bool) -> Result<Registry, (bool, GitError)> {
1369    match sparse {
1370        false => {
1371            Repository::open(&registry).map(Registry::Git).or_else(|e| if e.code() == GitErrorCode::NotFound {
1372                Repository::init(&registry).map(Registry::Git).map_err(|e| (true, e))
1373            } else {
1374                Err((false, e))
1375            })
1376        }
1377        true => Ok(Registry::Sparse(BTreeMap::new())),
1378    }
1379}
1380
1381#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
1382pub struct SparseRegistryAuthProviderBundle<'sr>(pub Cow<'sr, [SparseRegistryAuthProvider]>,
1383                                                 pub &'sr OsStr,
1384                                                 pub &'sr str,
1385                                                 pub Cow<'sr, str>,
1386                                                 pub Option<&'sr str>,
1387                                                 pub Option<&'sr str>);
1388impl<'sr> SparseRegistryAuthProviderBundle<'sr> {
1389    pub fn try(&self) -> Option<Cow<'sr, str>> {
1390        let (install_cargo, repo_name, repo_url, token_env, token) = (self.1, self.2, &self.3, self.4, self.5);
1391        self.0
1392            .iter()
1393            .rev()
1394            .find_map(|p| match p {
1395                SparseRegistryAuthProvider::TokenNoEnvironment => token.map(Cow::from),
1396                SparseRegistryAuthProvider::Token => token_env.or(token).map(Cow::from),
1397                SparseRegistryAuthProvider::Wincred => {
1398                    #[allow(unused_mut)]
1399                    let mut ret = None;
1400                    #[cfg(target_os="windows")]
1401                    unsafe {
1402                        let mut cred = ptr::null_mut();
1403                        if WinCred::CredReadA(PCSTR(format!("cargo-registry:{}\0", repo_url).as_ptr()),
1404                                              WinCred::CRED_TYPE_GENERIC,
1405                                              None,
1406                                              &mut cred)
1407                            .is_ok() {
1408                            ret = str::from_utf8(slice::from_raw_parts((*cred).CredentialBlob, (*cred).CredentialBlobSize as usize))
1409                                .map(str::to_string)
1410                                .map(Cow::from)
1411                                .ok();
1412                            WinCred::CredFree(cred as _);
1413                        }
1414                    }
1415                    ret
1416                }
1417                SparseRegistryAuthProvider::MacosKeychain => {
1418                    #[allow(unused_mut, unused_assignments)]
1419                    let mut ret = None;
1420                    #[cfg(target_vendor = "apple")]
1421                    {
1422                        ret = SecKeychain::default()
1423                            .and_then(|k| k.find_generic_password(&format!("cargo-registry:{}", repo_url), ""))
1424                            .ok()
1425                            .and_then(|(p, _)| str::from_utf8(&*p).map(str::to_string).map(Cow::from).ok());
1426                    }
1427                    ret
1428                }
1429                SparseRegistryAuthProvider::Libsecret => {
1430                    #[allow(unused_mut)]
1431                    let mut ret = None;
1432                    #[cfg(all(unix, not(target_vendor = "apple")))]
1433                    #[allow(non_camel_case_types)]
1434                    unsafe {
1435                        #[repr(C)]
1436                        struct SecretSchemaAttribute {
1437                            name: *const u8,
1438                            flags: libc::c_int, // SECRET_SCHEMA_ATTRIBUTE_STRING = 0
1439                        }
1440                        #[repr(C)]
1441                        struct SecretSchema {
1442                            name: *const u8,
1443                            flags: libc::c_int,
1444                            attributes: [SecretSchemaAttribute; 32],
1445                            reserved: libc::c_int,
1446                            reserved1: *const (),
1447                            reserved2: *const (),
1448                            reserved3: *const (),
1449                            reserved4: *const (),
1450                            reserved5: *const (),
1451                            reserved6: *const (),
1452                            reserved7: *const (),
1453                        }
1454                        unsafe impl Sync for SecretSchema {}
1455                        type secret_password_lookup_sync_t = extern "C" fn(*const SecretSchema, *mut (), *mut (), ...) -> *mut u8;
1456                        type secret_password_free_t = extern "C" fn(*mut u8);
1457
1458                        static LIBSECRET: LazyLock<Option<(secret_password_lookup_sync_t, secret_password_free_t)>> = LazyLock::new(|| unsafe {
1459                            let libsecret = libc::dlopen(b"libsecret-1.so.0\0".as_ptr() as _, libc::RTLD_LAZY);
1460                            if libsecret.is_null() {
1461                                return None;
1462                            }
1463                            let lookup = libc::dlsym(libsecret, b"secret_password_lookup_sync\0".as_ptr() as _);
1464                            let free = libc::dlsym(libsecret, b"secret_password_free\0".as_ptr() as _);
1465                            if lookup.is_null() || free.is_null() {
1466                                libc::dlclose(libsecret);
1467                                return None;
1468                            }
1469                            Some((mem::transmute(lookup), mem::transmute(free)))
1470                        });
1471                        static SCHEMA: SecretSchema = unsafe {
1472                            let mut schema: SecretSchema = mem::zeroed();
1473                            schema.name = b"org.rust-lang.cargo.registry\0".as_ptr() as _;
1474                            schema.attributes[0].name = b"url\0".as_ptr() as _;
1475                            schema
1476                        };
1477
1478                        if let Some((lookup, free)) = *LIBSECRET {
1479                            let pass = lookup(&SCHEMA,
1480                                              ptr::null_mut(),
1481                                              ptr::null_mut(),
1482                                              b"url\0".as_ptr(),
1483                                              format!("{}\0", repo_url).as_ptr(),
1484                                              ptr::null() as *const u8);
1485                            if !pass.is_null() {
1486                                ret = str::from_utf8(slice::from_raw_parts(pass, libc::strlen(pass as _))).map(str::to_string).map(Cow::from).ok();
1487                                free(pass);
1488                            }
1489                        }
1490                    }
1491                    ret
1492                }
1493                SparseRegistryAuthProvider::TokenFromStdout(args) => {
1494                    Command::new(&args[0])
1495                        .args(&args[1..])
1496                        .env("CARGO", install_cargo)
1497                        .env("CARGO_REGISTRY_INDEX_URL", &repo_url[..])
1498                        .env("CARGO_REGISTRY_NAME_OPT", repo_name)
1499                        .stdin(Stdio::inherit())
1500                        .stderr(Stdio::inherit())
1501                        .output()
1502                        .ok()
1503                        .filter(|o| o.status.success())
1504                        .map(|o| o.stdout)
1505                        .and_then(|o| String::from_utf8(o).ok())
1506                        .map(|mut o| {
1507                            o.replace_range(o.rfind(|c| c != '\n').unwrap_or(o.len()) + 1..o.len(), "");
1508                            o.replace_range(0..o.find(|c| c != '\n').unwrap_or(0), "");
1509                            o.into()
1510                        })
1511                }
1512                SparseRegistryAuthProvider::Provider(args) => {
1513                    Command::new(&args[0])
1514                        .arg("--cargo-plugin")
1515                        .stdin(Stdio::piped())
1516                        .stdout(Stdio::piped())
1517                        .spawn()
1518                        .ok()
1519                        .and_then(|mut child| {
1520                            let mut stdin = BufWriter::new(child.stdin.take().unwrap());
1521                            let mut stdout = BufReader::new(child.stdout.take().unwrap());
1522
1523                            let mut l = String::new();
1524                            stdout.read_line(&mut l).map_err(|_| child.kill()).ok()?;
1525                            {
1526                                let mut hello: json::Value = json::from_str(&l).map_err(|_| child.kill()).ok()?;
1527                                hello.as_object_mut()
1528                                    .and_then(|h| h.remove("v"))
1529                                    .and_then(|mut v| v.as_array_mut().filter(|vs| vs.contains(&json::Value::Number(1.into()))).map(drop))
1530                                    .ok_or_else(|| child.kill())
1531                                    .ok()?;
1532                            }
1533
1534                            let req = json::Value::Object({
1535                                let mut kv = json::Map::new();
1536                                kv.insert("v".to_string(), json::Value::Number(1.into()));
1537                                kv.insert("registry".to_string(),
1538                                          json::Value::Object({
1539                                              let mut kv = json::Map::new();
1540                                              kv.insert("index-url".to_string(), json::Value::String(repo_url.to_string()));
1541                                              kv.insert("name".to_string(), json::Value::String(repo_name.to_string()));
1542                                              kv
1543                                          }));
1544                                kv.insert("kind".to_string(), json::Value::String("get".to_string()));
1545                                kv.insert("operation".to_string(), json::Value::String("read".to_string()));
1546                                kv.insert("args".to_string(),
1547                                          json::Value::Array(args.into_iter().skip(1).cloned().map(json::Value::String).collect()));
1548                                kv
1549                            });
1550                            json::to_writer(&mut stdin, &req).map_err(|_| child.kill()).ok()?;
1551                            stdin.write_all(b"\n").map_err(|_| child.kill()).ok()?;
1552                            stdin.flush().map_err(|_| child.kill()).ok()?;
1553
1554                            l.clear();
1555                            stdout.read_line(&mut l).map_err(|_| child.kill()).ok()?;
1556                            let mut res: json::Value = json::from_str(&l).map_err(|_| child.kill()).ok()?;
1557                            match res.as_object_mut()
1558                                .and_then(|h| h.remove("Ok"))
1559                                .and_then(|mut ok| ok.as_object_mut().and_then(|ok| ok.remove("token"))) {
1560                                Some(json::Value::String(tok)) => Some(tok.into()),
1561                                Some(_) => {
1562                                    let _ = child.kill();
1563                                    None
1564                                }
1565                                None => {
1566                                    let _ = io::stderr()
1567                                        .write_all(b"\n")
1568                                        .ok()
1569                                        .and_then(|_| json::to_writer(&mut io::stderr(), &res).ok().and_then(|_| io::stderr().write_all(b"\n").ok()));
1570                                    None
1571                                }
1572                            }
1573                        })
1574                }
1575            })
1576    }
1577}
1578
1579/// Collect everything needed to get an authentication token for the given registry.
1580pub fn auth_providers<'sr>(crates_file: &Path, install_cargo: Option<&'sr OsStr>, sparse_registries: &'sr SparseRegistryConfig, sparse: bool,
1581                           repo_name: &'sr str, repo_url: &'sr str)
1582                           -> SparseRegistryAuthProviderBundle<'sr> {
1583    let cargo = install_cargo.unwrap_or(OsStr::new("cargo"));
1584    if !sparse {
1585        return SparseRegistryAuthProviderBundle(vec![].into(), cargo, "!sparse", "!sparse".into(), None, None);
1586    }
1587
1588    if repo_name == "crates-io" {
1589        let ret = match sparse_registries.crates_io_credential_provider.as_ref() {
1590            Some(prov) => slice::from_ref(prov).into(),
1591            None => sparse_registries.global_credential_providers[..].into(),
1592        };
1593        return SparseRegistryAuthProviderBundle(ret,
1594                                                cargo,
1595                                                repo_name,
1596                                                format!("sparse+{}", repo_url).into(),
1597                                                sparse_registries.crates_io_token_env.as_deref(),
1598                                                sparse_registries.crates_io_token.as_deref());
1599    }
1600
1601    // Supposedly this is
1602    //   format!("CARGO_REGISTRIES_{}_CREDENTIAL_PROVIDER",
1603    //           CargoConfigEnvironmentNormalisedString::normalise(repo_name.to_string()).0)
1604    // but they don't specify how they serialise arrays so
1605    let ret: Cow<'sr, [SparseRegistryAuthProvider]> = match fs::read_to_string(crates_file.with_file_name("config"))
1606        .or_else(|_| fs::read_to_string(crates_file.with_file_name("config.toml")))
1607        .ok()
1608        .and_then(|s| s.parse::<toml::Value>().ok())
1609        .and_then(|mut c| {
1610            sparse_registries.credential_provider(c.as_table_mut()?
1611                .remove("registries")?
1612                .as_table_mut()?
1613                .remove(repo_name)?
1614                .as_table_mut()?
1615                .remove("credential-provider")?)
1616        }) {
1617        Some(prov) => vec![prov].into(),
1618        None => sparse_registries.global_credential_providers[..].into(),
1619    };
1620    let token_env = if ret.contains(&SparseRegistryAuthProvider::Token) {
1621        sparse_registries.registry_tokens_env.get(&CargoConfigEnvironmentNormalisedString::normalise(repo_name.to_string())).map(String::as_str)
1622    } else {
1623        None
1624    };
1625    SparseRegistryAuthProviderBundle(ret,
1626                                     cargo,
1627                                     repo_name,
1628                                     format!("sparse+{}", repo_url).into(),
1629                                     token_env,
1630                                     sparse_registries.registry_tokens.get(repo_name).map(String::as_str))
1631}
1632
1633/// Update the specified index repository from the specified URL.
1634///
1635/// Historically, `cargo search` was used, first of an
1636/// [empty string](https://github.com/nabijaczleweli/cargo-update/commit/aa090b4a38a486654cd73b173c3f49f6a56aa059#diff-639fbc4ef05b315af92b4d836c31b023R24),
1637/// then a [ZWNJ](https://github.com/nabijaczleweli/cargo-update/commit/aeccbd6252a2ddc90dc796117cefe327fbd7fb58#diff-639fbc4ef05b315af92b4d836c31b023R48)
1638/// ([why?](https://github.com/nabijaczleweli/cargo-update/commit/08a7111831c6397b7d67a51f9b77bee0a3bbbed4#diff-639fbc4ef05b315af92b4d836c31b023R47)).
1639///
1640/// The need for this in-house has first emerged with [#93](https://github.com/nabijaczleweli/cargo-update/issues/93): since
1641/// [`cargo` v1.29.0-nightly](https://github.com/rust-lang/cargo/pull/5621/commits/5e680f2849e44ce9dfe44416c3284a3b30747e74),
1642/// the registry was no longer updated.
1643/// So a [two-year-old `cargo` issue](https://github.com/rust-lang/cargo/issues/3377#issuecomment-417950125) was dug up,
1644/// asking for a `cargo update-registry` command, followed by a [PR](https://github.com/rust-lang/cargo/pull/5961) implementing
1645/// this.
1646/// Up to this point, there was no good substitute: `cargo install lazy_static`, the poster-child of replacements errored out
1647/// and left garbage in the console, making it unsuitable.
1648///
1649/// But then, a [man of steel eyes and hawk will](https://github.com/Eh2406) has emerged, seemingly from nowhere, remarking:
1650///
1651/// > [21:09] Eh2406:
1652/// https://github.com/rust-lang/cargo/blob/1ee1ef0ea7ab47d657ca675e3b1bd2fcd68b5aab/src/cargo/sources/registry/remote.rs#L204
1653/// <br />
1654/// > [21:10] Eh2406: looks like it is a git fetch of "refs/heads/master:refs/remotes/origin/master"<br />
1655/// > [21:11] Eh2406: You are already poking about in cargos internal representation of the index, is this so much more?
1656///
1657/// It, well, isn't. And with some `cargo` maintainers being firmly against blind-merging that `cargo update-registry` PR,
1658/// here I go recycling <del>the same old song</del> that implementation (but simpler, and badlier).
1659///
1660/// Honourable mentions:
1661/// * [**@joshtriplett**](https://github.com/joshtriplett), for being a bastion for the people and standing with me in
1662///   advocacy for `cargo update-registry`
1663///   (NB: it was *his* issue from 2016 requesting it, funny how things turn around)
1664/// * [**@alexcrichton**](https://github.com/alexcrichton), for not getting overly too fed up with me while managing that PR
1665///   and producing a brilliant
1666///   argument list for doing it in-house (as well as suggesting I write another crate for this)
1667/// * And lastly, because mostly, [**@Eh2406**](https://github.com/Eh2406), for swooping in and saving me in my hour of
1668///   <del>need</del> not having a good replacement.
1669///
1670/// Most of this would have been impossible, of course, without the [`rust-lang` Discord server](https://discord.gg/rust-lang),
1671/// so shoutout to whoever convinced people that Discord is actually good.
1672///
1673/// Sometimes, however, even this isn't enough (see https://github.com/nabijaczleweli/cargo-update/issues/163),
1674/// hence `fork_git`, which actually runs `$GIT` (default: `git`).
1675///
1676/// # Sparse indices
1677///
1678/// Have a `.cache` under the obvious path, then the usual `ca/rg/cargo-update`, but *the file is different than the standard
1679/// format*: it starts with a ^A or ^C (I'm assuming these are versions, and if I looked at more files I would also've seen
1680/// ^C), then Some Binary Data, then the ETag(?), then {NUL, version, NUL, usual JSON blob line} repeats.
1681///
1682/// I do not wanna be touching that shit. Just suck off all the files.<br />
1683/// Shoulda stored the blobs verbatim and used `If-Modified-Since`. Too me.
1684///
1685/// Only in this mode is the package list used.
1686pub fn update_index<W: Write, A: AsRef<str>, I: Iterator<Item = A>>(index_repo: &mut Registry, repo_url: &str, packages: I, http_proxy: Option<&str>,
1687                                                                    fork_git: bool, http: &HttpCargoConfig, auth_token: Option<&str>, out: &mut W)
1688                                                                    -> Result<(), String> {
1689    write!(out,
1690           "    {} registry '{}'{}",
1691           ["Updating", "Polling"][matches!(index_repo, Registry::Sparse(_)) as usize],
1692           repo_url,
1693           ["\n", ""][matches!(index_repo, Registry::Sparse(_)) as usize]).and_then(|_| out.flush())
1694        .map_err(|e| format!("failed to write updating message: {}", e))?;
1695    match index_repo {
1696        Registry::Git(index_repo) => {
1697            if fork_git {
1698                Command::new(env::var_os("GIT").as_ref().map(OsString::as_os_str).unwrap_or(OsStr::new("git"))).arg("-C")
1699                    .arg(index_repo.path())
1700                    .args(&["fetch", "-f", repo_url, "HEAD:refs/remotes/origin/HEAD"])
1701                    .status()
1702                    .map_err(|e| e.to_string())
1703                    .and_then(|e| if e.success() {
1704                        Ok(())
1705                    } else {
1706                        Err(e.to_string())
1707                    })?;
1708            } else {
1709                index_repo.remote_anonymous(repo_url)
1710                    .and_then(|mut r| {
1711                        with_authentication(repo_url, |creds| {
1712                            let mut cb = RemoteCallbacks::new();
1713                            cb.credentials(|a, b, c| creds(a, b, c));
1714
1715                            r.fetch(&["HEAD:refs/remotes/origin/HEAD"],
1716                                    Some(&mut fetch_options_from_proxy_url_and_callbacks(repo_url, http_proxy, cb)),
1717                                    None)
1718                        })
1719                    })
1720                    .map_err(|e| e.message().to_string())?;
1721            }
1722        }
1723        Registry::Sparse(registry) => {
1724            let mut sucker = CurlMulti::new();
1725            sucker.pipelining(true, true).map_err(|e| format!("pipelining: {}", e))?;
1726
1727            let writussy = Mutex::new(&mut *out);
1728            let mut conns: Vec<_> = Result::from_iter(packages.map(|pkg| {
1729                let mut conn = CurlEasy::new(SparseHandler(pkg.as_ref().to_string(), vec![], Some(&writussy)));
1730                conn.url(&split_package_path(pkg.as_ref()).into_iter().fold(repo_url.to_string(), |mut u, s| {
1731                        if !u.ends_with('/') {
1732                            u.push('/');
1733                        }
1734                        u.push_str(&s);
1735                        u
1736                    }))
1737                    .map_err(|e| format!("url: {}", e))?;
1738                if let Some(auth_token) = auth_token.as_ref() {
1739                    let mut headers = CurlList::new();
1740                    headers.append(&format!("Authorization: {}", auth_token)).map_err(|e| format!("append: {}", e))?;
1741                    conn.http_headers(headers).map_err(|e| format!("http_headers: {}", e))?;
1742                }
1743                if let Some(http_proxy) = http_proxy {
1744                    conn.proxy(http_proxy).map_err(|e| format!("proxy: {}", e))?;
1745                }
1746                conn.pipewait(true).map_err(|e| format!("pipewait: {}", e))?;
1747                conn.progress(true).map_err(|e| format!("progress: {}", e))?;
1748                if let Some(cainfo) = http.cainfo.as_ref() {
1749                    conn.cainfo(cainfo).map_err(|e| format!("cainfo: {}", e))?;
1750                }
1751                conn.ssl_options(CurlSslOpt::new().no_revoke(!http.check_revoke)).map_err(|e| format!("ssl_options: {}", e))?;
1752                sucker.add2(conn).map(|h| (h, Ok(()))).map_err(|e| format!("add2: {}", e))
1753            }))?;
1754
1755            while sucker.perform().map_err(|e| format!("perform: {}", e))? > 0 {
1756                sucker.wait(&mut [], Duration::from_millis(200)).map_err(|e| format!("wait: {}", e))?;
1757            }
1758
1759            writussy.lock()
1760                .map_err(|e| e.to_string())
1761                .and_then(|mut out| writeln!(out).map_err(|e| e.to_string()))
1762                .map_err(|e| format!("failed to write post-update newline: {}", e))?;
1763
1764            sucker.messages(|m| {
1765                for c in &mut conns {
1766                    // Yes, a linear search; this is much faster than adding 2+n sets of CURLINFO_PRIVATE calls
1767                    if let Some(err) = m.result_for2(&c.0) {
1768                        c.1 = err;
1769                    }
1770                }
1771            });
1772
1773            for mut c in conns {
1774                let pkg = mem::take(&mut c.0.get_mut().0);
1775                if let Err(e) = c.1 {
1776                    return Err(format!("package {}: {}", pkg, e));
1777                }
1778                match c.0.response_code().map_err(|e| format!("response_code: {}", e))? {
1779                    200 => {
1780                        let mut resp = crate_versions(&c.0.get_ref().1).map_err(|e| format!("package {}: {}", pkg, e))?;
1781                        resp.sort();
1782                        registry.insert(pkg, resp);
1783                    }
1784                    rc @ 404 | rc @ 410 | rc @ 451 => return Err(format!("package {} doesn't exist: HTTP {}", pkg, rc)),
1785                    rc => return Err(format!("package {}: HTTP {}", pkg, rc)),
1786                }
1787            }
1788        }
1789    }
1790    writeln!(out).map_err(|e| format!("failed to write post-update newline: {}", e))?;
1791
1792    Ok(())
1793}
1794
1795// Could we theoretically parse the semvers on the fly? Yes. Is it more trouble than it's worth? Also probably yes; there
1796// doesn't appear to be a good way to bubble errors.
1797// Same applies to just waiting instead of processing via .messages()
1798struct SparseHandler<'m, 'w: 'm, W: Write>(String, Vec<u8>, Option<&'m Mutex<&'w mut W>>);
1799
1800impl<'m, 'w: 'm, W: Write> CurlHandler for SparseHandler<'m, 'w, W> {
1801    fn write(&mut self, data: &[u8]) -> Result<usize, CurlWriteError> {
1802        self.1.extend(data);
1803        Ok(data.len())
1804    }
1805    fn progress(&mut self, dltotal: f64, dlnow: f64, _: f64, _: f64) -> bool {
1806        if dltotal != 0.0 && dltotal == dlnow {
1807            if let Some(mut out) = self.2.take().and_then(|m| m.lock().ok()) {
1808                let _ = out.write_all(b".").and_then(|_| out.flush());
1809            }
1810        }
1811        true
1812    }
1813}
1814
1815
1816/// Either an open git repository with a git registry, or a map of (package, sorted versions), populated by
1817/// [`update_index()`](fn.update_index.html)
1818pub enum Registry {
1819    Git(Repository),
1820    Sparse(BTreeMap<String, Vec<Semver>>),
1821}
1822
1823/// A git tree corresponding to the latest revision of a git registry.
1824pub enum RegistryTree<'a> {
1825    Git(Tree<'a>),
1826    Sparse,
1827}
1828
1829/// Get `FETCH_HEAD` or `origin/HEAD`, then unwrap it to the tree it points to.
1830pub fn parse_registry_head(registry_repo: &Registry) -> Result<RegistryTree<'_>, GitError> {
1831    match registry_repo {
1832        Registry::Git(registry_repo) => {
1833            registry_repo.revparse_single("FETCH_HEAD")
1834                .or_else(|_| registry_repo.revparse_single("origin/HEAD"))
1835                .map(|h| h.as_commit().unwrap().tree().unwrap())
1836                .map(RegistryTree::Git)
1837        }
1838        Registry::Sparse(_) => Ok(RegistryTree::Sparse),
1839    }
1840}
1841
1842
1843fn proxy_options_from_proxy_url<'a>(repo_url: &str, proxy_url: &str) -> ProxyOptions<'a> {
1844    let mut prx = ProxyOptions::new();
1845    let mut url = Cow::from(proxy_url);
1846
1847    // Cargo allows [protocol://]host[:port], but git needs the protocol, try to crudely add it here if missing;
1848    // confer https://github.com/nabijaczleweli/cargo-update/issues/144.
1849    if Url::parse(proxy_url).is_err() {
1850        if let Ok(rurl) = Url::parse(repo_url) {
1851            let replacement_proxy_url = format!("{}://{}", rurl.scheme(), proxy_url);
1852            if Url::parse(&replacement_proxy_url).is_ok() {
1853                url = Cow::from(replacement_proxy_url);
1854            }
1855        }
1856    }
1857
1858    prx.url(&url);
1859    prx
1860}
1861
1862fn fetch_options_from_proxy_url_and_callbacks<'a>(repo_url: &str, proxy_url: Option<&str>, callbacks: RemoteCallbacks<'a>) -> FetchOptions<'a> {
1863    let mut ret = FetchOptions::new();
1864    if let Some(proxy_url) = proxy_url {
1865        ret.proxy_options(proxy_options_from_proxy_url(repo_url, proxy_url));
1866    }
1867    ret.remote_callbacks(callbacks);
1868    ret
1869}
1870
1871/// Get the URL to update index from, whether it's "sparse", and the cargo name for it from the config file parallel to the
1872/// specified crates file
1873///
1874/// First gets the source name corresponding to the given URL, if appropriate,
1875/// then chases the `source.$SRCNAME.replace-with` chain,
1876/// then retrieves the URL from `source.$SRCNAME.registry` of the final source.
1877///
1878/// Prepopulates with `source.crates-io.registry = "https://github.com/rust-lang/crates.io-index"`,
1879/// as specified in the book
1880///
1881/// If `registries_crates_io_protocol_sparse`, `https://github.com/rust-lang/crates.io-index` is replaced with
1882/// `sparse+https://index.crates.io/`.
1883///
1884/// Consult [#107](https://github.com/nabijaczleweli/cargo-update/issues/107) and
1885/// the Cargo Book for details: https://doc.rust-lang.org/cargo/reference/source-replacement.html,
1886/// https://doc.rust-lang.org/cargo/reference/registries.html.
1887pub fn get_index_url(crates_file: &Path, registry: &str, registries_crates_io_protocol_sparse: bool)
1888                     -> Result<(Cow<'static, str>, bool, Cow<'static, str>), Cow<'static, str>> {
1889    let mut config_file = crates_file.with_file_name("config");
1890    let config = if let Ok(cfg) = fs::read_to_string(&config_file).or_else(|_| {
1891        config_file.set_file_name("config.toml");
1892        fs::read_to_string(&config_file)
1893    }) {
1894        toml::from_str::<toml::Value>(&cfg).map_err(|e| format!("{} not TOML: {}", config_file.display(), e))?
1895    } else {
1896        if registry == "https://github.com/rust-lang/crates.io-index" {
1897            if registries_crates_io_protocol_sparse {
1898                return Ok(("https://index.crates.io/".into(), true, "crates-io".into()));
1899            } else {
1900                return Ok((registry.to_string().into(), false, "crates-io".into()));
1901            }
1902        } else {
1903            Err(format!("Non-crates.io registry specified and no config file found at {} or {}. \
1904                         Due to a Cargo limitation we will not be able to install from there \
1905                         until it's given a [source.NAME] in that file!",
1906                        config_file.with_file_name("config").display(),
1907                        config_file.display()))?
1908        }
1909    };
1910
1911    let mut replacements = BTreeMap::new();
1912    let mut registries = BTreeMap::new();
1913    let mut cur_source = Cow::from(registry);
1914
1915    // Special case, always present
1916    registries.insert("crates-io",
1917                      Cow::from(if registries_crates_io_protocol_sparse {
1918                          "sparse+https://index.crates.io/"
1919                      } else {
1920                          "https://github.com/rust-lang/crates.io-index"
1921                      }));
1922    if cur_source == "https://github.com/rust-lang/crates.io-index" || cur_source == "sparse+https://index.crates.io/" {
1923        cur_source = "crates-io".into();
1924    }
1925
1926    if let Some(source) = config.get("source") {
1927        for (name, v) in source.as_table().ok_or("source not table")? {
1928            if let Some(replacement) = v.get("replace-with") {
1929                replacements.insert(&name[..],
1930                                    replacement.as_str().ok_or_else(|| format!("source.{}.replacement not string", name))?);
1931            }
1932
1933            if let Some(url) = v.get("registry") {
1934                let url = url.as_str().ok_or_else(|| format!("source.{}.registry not string", name))?.to_string().into();
1935                if cur_source == url {
1936                    cur_source = name.into();
1937                }
1938
1939                registries.insert(&name[..], url);
1940            }
1941        }
1942    }
1943
1944    if let Some(registries_tabls) = config.get("registries") {
1945        let table = registries_tabls.as_table().ok_or("registries is not a table")?;
1946        for (name, url) in table.iter().flat_map(|(name, val)| val.as_table()?.get("index")?.as_str().map(|v| (name, v))) {
1947            if cur_source == url.strip_prefix("sparse+").unwrap_or(url) {
1948                cur_source = name.into()
1949            }
1950            registries.insert(name, url.into());
1951        }
1952    }
1953
1954    if Url::parse(&cur_source).is_ok() {
1955        Err(format!("Non-crates.io registry specified and {} couldn't be found in the config file at {}. \
1956                     Due to a Cargo limitation we will not be able to install from there \
1957                     until it's given a [source.NAME] in that file!",
1958                    cur_source,
1959                    config_file.display()))?
1960    }
1961
1962    while let Some(repl) = replacements.get(&cur_source[..]) {
1963        cur_source = Cow::from(&repl[..]);
1964    }
1965
1966    registries.get(&cur_source[..])
1967        .map(|reg| (reg.strip_prefix("sparse+").unwrap_or(reg).to_string().into(), reg.starts_with("sparse+"), cur_source.to_string().into()))
1968        .ok_or_else(|| {
1969            format!("Couldn't find appropriate source URL for {} in {} (resolved to {:?})",
1970                    registry,
1971                    config_file.display(),
1972                    cur_source)
1973                .into()
1974        })
1975}
1976
1977/// Based on
1978/// https://github.com/rust-lang/cargo/blob/bb28e71202260180ecff658cd0fa0c7ba86d0296/src/cargo/sources/git/utils.rs#L344
1979/// and
1980/// https://github.com/rust-lang/cargo/blob/5102de2b7de997b03181063417f20874a06a67c0/src/cargo/sources/git/utils.rs#L644,
1981/// then
1982/// https://github.com/rust-lang/cargo/blob/5102de2b7de997b03181063417f20874a06a67c0/src/cargo/sources/git/utils.rs#L437
1983/// (see that link for full comments)
1984fn with_authentication<T, F>(url: &str, mut f: F) -> Result<T, GitError>
1985    where F: FnMut(&mut git2::Credentials) -> Result<T, GitError>
1986{
1987    let cfg = GitConfig::open_default().unwrap();
1988
1989    let mut cred_helper = git2::CredentialHelper::new(url);
1990    cred_helper.config(&cfg);
1991
1992    let mut ssh_username_requested = false;
1993    let mut cred_helper_bad = None;
1994    let mut ssh_agent_attempts = Vec::new();
1995    let mut any_attempts = false;
1996    let mut tried_ssh_key = false;
1997
1998    let mut res = f(&mut |url, username, allowed| {
1999        any_attempts = true;
2000
2001        if allowed.contains(CredentialType::USERNAME) {
2002            ssh_username_requested = true;
2003
2004            Err(GitError::from_str("username to be tried later"))
2005        } else if allowed.contains(CredentialType::SSH_KEY) && !tried_ssh_key {
2006            tried_ssh_key = true;
2007
2008            let username = username.unwrap();
2009            ssh_agent_attempts.push(username.to_string());
2010
2011            GitCred::ssh_key_from_agent(username)
2012        } else if allowed.contains(CredentialType::USER_PASS_PLAINTEXT) && cred_helper_bad.is_none() {
2013            let ret = GitCred::credential_helper(&cfg, url, username);
2014            cred_helper_bad = Some(ret.is_err());
2015            ret
2016        } else if allowed.contains(CredentialType::DEFAULT) {
2017            GitCred::default()
2018        } else {
2019            Err(GitError::from_str("no authentication available"))
2020        }
2021    });
2022
2023    if ssh_username_requested {
2024        // NOTE: this is the only divergence from the original cargo code: we also try cfg["user.name"]
2025        //       see https://github.com/nabijaczleweli/cargo-update/issues/110#issuecomment-533091965 for explanation
2026        for uname in cred_helper.username
2027            .into_iter()
2028            .chain(cfg.get_string("user.name"))
2029            .chain(["USERNAME", "USER"].iter().flat_map(env::var))
2030            .chain(Some("git".to_string())) {
2031            let mut ssh_attempts = 0;
2032
2033            res = f(&mut |_, _, allowed| {
2034                if allowed.contains(CredentialType::USERNAME) {
2035                    return GitCred::username(&uname);
2036                } else if allowed.contains(CredentialType::SSH_KEY) {
2037                    ssh_attempts += 1;
2038                    if ssh_attempts == 1 {
2039                        ssh_agent_attempts.push(uname.to_string());
2040                        return GitCred::ssh_key_from_agent(&uname);
2041                    }
2042                }
2043
2044                Err(GitError::from_str("no authentication available"))
2045            });
2046
2047            if ssh_attempts != 2 {
2048                break;
2049            }
2050        }
2051    }
2052
2053    if res.is_ok() || !any_attempts {
2054        res
2055    } else {
2056        let err = res.err().map(|e| format!("{}: ", e)).unwrap_or_default();
2057
2058        let mut msg = format!("{}failed to authenticate when downloading repository {}", err, url);
2059        if !ssh_agent_attempts.is_empty() {
2060            msg.push_str(" (tried ssh-agent, but none of the following usernames worked: ");
2061            for (i, uname) in ssh_agent_attempts.into_iter().enumerate() {
2062                if i != 0 {
2063                    msg.push_str(", ");
2064                }
2065                msg.push('\"');
2066                msg.push_str(&uname);
2067                msg.push('\"');
2068            }
2069            msg.push(')');
2070        }
2071
2072        if let Some(failed_cred_helper) = cred_helper_bad {
2073            msg.push_str(" (tried to find username+password via ");
2074            if failed_cred_helper {
2075                msg.push_str("git's credential.helper support, but failed)");
2076            } else {
2077                msg.push_str("credential.helper, but found credentials were incorrect)");
2078            }
2079        }
2080
2081        Err(GitError::from_str(&msg))
2082    }
2083}
2084
2085
2086/// Split and lower-case `cargo-update` into `[ca, rg, cargo-update]`, `jot` into `[3, j, jot]`, &c.
2087pub fn split_package_path(cratename: &str) -> Vec<Cow<'_, str>> {
2088    let mut elems = Vec::new();
2089    if cratename.is_empty() {
2090        panic!("0-length cratename");
2091    }
2092    if cratename.len() <= 3 {
2093        elems.push(["1", "2", "3"][cratename.len() - 1].into())
2094    }
2095    match cratename.len() {
2096        1 | 2 => {}
2097        3 => elems.push(lcase(&cratename[0..1])),
2098        _ => {
2099            elems.push(lcase(&cratename[0..2]));
2100            elems.push(lcase(&cratename[2..4]));
2101        }
2102    }
2103    elems.push(lcase(cratename));
2104    elems
2105}
2106
2107fn lcase(s: &str) -> Cow<'_, str> {
2108    if s.bytes().any(|b| b.is_ascii_uppercase()) {
2109        s.to_ascii_lowercase().into()
2110    } else {
2111        s.into()
2112    }
2113}
2114
2115/// Find package data in the specified cargo git index tree.
2116pub fn find_package_data<'t>(cratename: &str, registry: &Tree<'t>, registry_parent: &'t Repository) -> Option<Vec<u8>> {
2117    let elems = split_package_path(cratename);
2118
2119    let ent = registry.get_name(&elems[0])?;
2120    let obj = ent.to_object(registry_parent).ok()?;
2121    let ent = obj.as_tree()?.get_name(&elems[1])?;
2122    let obj = ent.to_object(registry_parent).ok()?;
2123    if elems.len() == 3 {
2124        let ent = obj.as_tree()?.get_name(&elems[2])?;
2125        let obj = ent.to_object(registry_parent).ok()?;
2126        Some(obj.as_blob()?.content().into())
2127    } else {
2128        Some(obj.as_blob()?.content().into())
2129    }
2130}
2131
2132/// Check if there's a proxy specified to be used.
2133///
2134/// Look for `http.proxy` key in the `config` file parallel to the specified crates file.
2135///
2136/// Then look for `git`'s `http.proxy`.
2137///
2138/// Then for the `http_proxy`, `HTTP_PROXY`, `https_proxy`, and `HTTPS_PROXY` environment variables, in that order.
2139///
2140/// Based on Cargo's [`http_proxy_exists()` and
2141/// `http_proxy()`](https://github.com/rust-lang/cargo/blob/eebd1da3a89e9c7788d109b3e615e1e25dc2cfcd/src/cargo/ops/registry.rs)
2142///
2143/// If a proxy is specified, but an empty string, treat it as unspecified.
2144///
2145/// # Examples
2146///
2147/// ```
2148/// # use cargo_update::ops::find_proxy;
2149/// # use std::env::temp_dir;
2150/// # let crates_file = temp_dir().join(".crates.toml");
2151/// match find_proxy(&crates_file) {
2152///     Some(proxy) => println!("Proxy found at {}", proxy),
2153///     None => println!("No proxy detected"),
2154/// }
2155/// ```
2156pub fn find_proxy(crates_file: &Path) -> Option<String> {
2157    if let Ok(crates_file) = fs::read_to_string(crates_file) {
2158        if let Some(toml::Value::String(proxy)) =
2159            toml::from_str::<toml::Value>(&crates_file)
2160                .unwrap()
2161                .get_mut("http")
2162                .and_then(|t| t.as_table_mut())
2163                .and_then(|t| t.remove("proxy")) {
2164            if !proxy.is_empty() {
2165                return Some(proxy);
2166            }
2167        }
2168    }
2169
2170    if let Ok(cfg) = GitConfig::open_default() {
2171        if let Ok(proxy) = cfg.get_string("http.proxy") {
2172            if !proxy.is_empty() {
2173                return Some(proxy);
2174            }
2175        }
2176    }
2177
2178    ["http_proxy", "HTTP_PROXY", "https_proxy", "HTTPS_PROXY"].iter().flat_map(env::var).filter(|proxy| !proxy.is_empty()).next()
2179}
2180
2181/// Find the bare git repository in the specified directory for the specified crate
2182///
2183/// The db directory is usually `$HOME/.cargo/git/db/`
2184///
2185/// The resulting paths are children of this directory in the format
2186/// [`{last_url_segment || "_empty"}-{hash(url)}`]
2187/// (https://github.com/rust-lang/cargo/blob/74f2b400d2be43da798f99f94957d359bc223988/src/cargo/sources/git/source.rs#L62-L73)
2188pub fn find_git_db_repo(git_db_dir: &Path, url: &str) -> Option<PathBuf> {
2189    let path = git_db_dir.join(format!("{}-{}",
2190                                       match Url::parse(url)
2191                                           .ok()?
2192                                           .path_segments()
2193                                           .and_then(|mut segs| segs.next_back())
2194                                           .unwrap_or("") {
2195                                           "" => "_empty",
2196                                           url => url,
2197                                       },
2198                                       cargo_hash(url)));
2199
2200    if path.is_dir() { Some(path) } else { None }
2201}
2202
2203
2204/// The short filesystem name for the repository, as used by `cargo`
2205///
2206/// Must be equivalent to
2207/// https://github.com/rust-lang/cargo/blob/74f2b400d2be43da798f99f94957d359bc223988/src/cargo/sources/registry/mod.rs#L387-L402
2208/// and
2209/// https://github.com/rust-lang/cargo/blob/74f2b400d2be43da798f99f94957d359bc223988/src/cargo/util/hex.rs
2210///
2211/// For main repository it's `github.com-1ecc6299db9ec823`
2212pub fn registry_shortname(url: &str) -> String {
2213    struct RegistryHash<'u>(&'u str);
2214    impl<'u> Hash for RegistryHash<'u> {
2215        fn hash<S: Hasher>(&self, hasher: &mut S) {
2216            SourceKind::Registry.hash(hasher);
2217            self.0.hash(hasher);
2218        }
2219    }
2220
2221    format!("{}-{}",
2222            Url::parse(url).map_err(|e| format!("{} not an URL: {}", url, e)).unwrap().host_str().unwrap_or(""),
2223            cargo_hash(RegistryHash(url)))
2224}
2225
2226/// Stolen from and equivalent to `short_hash()` from
2227/// https://github.com/rust-lang/cargo/blob/74f2b400d2be43da798f99f94957d359bc223988/src/cargo/util/hex.rs
2228#[allow(deprecated)]
2229pub fn cargo_hash<T: Hash>(whom: T) -> String {
2230    use std::hash::SipHasher;
2231
2232    let mut hasher = SipHasher::new_with_keys(0, 0);
2233    whom.hash(&mut hasher);
2234    let hash = hasher.finish();
2235    hex::encode(&[(hash >> 0) as u8,
2236                  (hash >> 8) as u8,
2237                  (hash >> 16) as u8,
2238                  (hash >> 24) as u8,
2239                  (hash >> 32) as u8,
2240                  (hash >> 40) as u8,
2241                  (hash >> 48) as u8,
2242                  (hash >> 56) as u8])
2243}
2244
2245/// These two are stolen verbatim from
2246/// https://github.com/rust-lang/cargo/blob/74f2b400d2be43da798f99f94957d359bc223988/src/cargo/core/source/source_id.rs#L48-L73
2247/// in order to match our hash with
2248/// https://github.com/rust-lang/cargo/blob/74f2b400d2be43da798f99f94957d359bc223988/src/cargo/core/source/source_id.rs#L510
2249#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2250#[allow(unused)]
2251enum SourceKind {
2252    Git(GitReference),
2253    Path,
2254    Registry,
2255    LocalRegistry,
2256    Directory,
2257}
2258
2259#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2260#[allow(unused)]
2261enum GitReference {
2262    Tag(String),
2263    Branch(String),
2264    Rev(String),
2265}
2266
2267
2268trait SemverExt {
2269    fn is_prerelease(&self) -> bool;
2270}
2271impl SemverExt for Semver {
2272    fn is_prerelease(&self) -> bool {
2273        !self.pre.is_empty()
2274    }
2275}