use git2::{self, ErrorCode as GitErrorCode, Config as GitConfig, Error as GitError, Cred as GitCred, RemoteCallbacks, CredentialType, FetchOptions,
ProxyOptions, Repository, Blob, Tree, Oid};
use curl::easy::{WriteError as CurlWriteError, Handler as CurlHandler, SslOpt as CurlSslOpt, Easy2 as CurlEasy, List as CurlList};
use semver::{VersionReq as SemverReq, Version as Semver};
#[cfg(target_vendor = "apple")]
use security_framework::os::macos::keychain::SecKeychain;
#[cfg(target_os = "windows")]
use windows::Win32::Security::Credentials as WinCred;
use std::io::{self, ErrorKind as IoErrorKind, BufWriter, BufReader, BufRead, Write};
use std::collections::{BTreeMap, BTreeSet};
use std::{slice, cmp, env, mem, str, fs};
use chrono::{FixedOffset, DateTime, Utc};
use curl::multi::{Multi as CurlMulti, Easy2Handle as CurlEasyHandle};
use std::process::{Command, Stdio};
use std::ffi::{OsString, OsStr};
use std::path::{PathBuf, Path};
use std::hash::{Hasher, Hash};
use std::iter::FromIterator;
#[cfg(target_os = "windows")]
use windows::core::PCSTR;
use std::time::Duration;
#[cfg(all(unix, not(target_vendor = "apple")))]
use std::sync::LazyLock;
use serde_json as json;
use std::borrow::Cow;
use std::sync::Mutex;
#[cfg(any(target_os = "windows", all(unix, not(target_vendor = "apple"))))]
use std::ptr;
use url::Url;
use toml;
use hex;
mod config;
pub use self::config::*;
fn parse_registry_package_ident(ident: &str) -> Option<(&str, &str, &str)> {
let mut idx = ident.splitn(3, ' ');
let (name, version, mut reg) = (idx.next()?, idx.next()?, idx.next()?);
reg = reg.strip_prefix('(')?.strip_suffix(')')?;
Some((name, version, reg.strip_prefix("registry+").or_else(|| reg.strip_prefix("sparse+"))?))
}
fn parse_git_package_ident(ident: &str) -> Option<(&str, &str, &str)> {
let mut idx = ident.splitn(3, ' ');
let (name, _, blob) = (idx.next()?, idx.next()?, idx.next()?);
let (url, sha) = blob.strip_prefix("(git+")?.strip_suffix(')')?.split_once('#')?;
if sha.len() != 40 {
return None;
}
Some((name, url, sha))
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct RegistryPackage {
pub name: String,
pub registry: Cow<'static, str>,
pub version: Option<Semver>,
pub newest_version: Option<Semver>,
pub alternative_version: Option<Semver>,
pub max_version: Option<Semver>,
pub executables: Vec<String>,
}
#[derive(Debug, PartialEq)]
pub struct GitRepoPackage {
pub name: String,
pub url: String,
pub branch: Option<String>,
pub id: Oid,
pub newest_id: Result<Oid, GitError>,
pub executables: Vec<String>,
}
impl Hash for GitRepoPackage {
fn hash<H: Hasher>(&self, state: &mut H) {
self.name.hash(state);
self.url.hash(state);
self.branch.hash(state);
self.id.hash(state);
match &self.newest_id {
Ok(nid) => nid.hash(state),
Err(err) => {
err.raw_code().hash(state);
err.raw_class().hash(state);
err.message().hash(state);
}
}
self.executables.hash(state);
}
}
impl RegistryPackage {
pub fn parse(what: &str, executables: Vec<String>) -> Option<RegistryPackage> {
parse_registry_package_ident(what).map(|(name, version, registry)| {
RegistryPackage {
name: name.to_string(),
registry: registry.to_string().into(),
version: Some(Semver::parse(version).unwrap()),
newest_version: None,
alternative_version: None,
max_version: None,
executables: executables,
}
})
}
fn want_to_install_prerelease(&self, version_to_install: &Semver, install_prereleases: Option<bool>) -> bool {
if install_prereleases.unwrap_or(false) {
return true;
}
self.version
.as_ref()
.map(|cur| {
cur.is_prerelease() && cur.major == version_to_install.major && cur.minor == version_to_install.minor && cur.patch == version_to_install.patch
})
.unwrap_or(false)
}
pub fn pull_version(&mut self, registry: &RegistryTree, registry_parent: &Registry, install_prereleases: Option<bool>,
released_after: Option<DateTime<Utc>>) {
let mut vers_git;
let vers = match (registry, registry_parent) {
(RegistryTree::Git(registry), Registry::Git(registry_parent)) => {
vers_git = find_package_data(&self.name, registry, registry_parent)
.ok_or_else(|| format!("package {} not found", self.name))
.and_then(|pd| crate_versions(pd.content()).map_err(|e| format!("package {}: {}", self.name, e)))
.unwrap();
vers_git.sort();
&vers_git
}
(RegistryTree::Sparse, Registry::Sparse(registry_parent)) => ®istry_parent[&self.name],
_ => unreachable!(),
};
self.newest_version = None;
self.alternative_version = None;
let mut vers = vers.iter()
.rev()
.filter(|(_, dt)| match (dt, released_after) {
(_, None) => true,
(None, Some(_)) => false,
(Some(dt), Some(ra)) => ra > *dt,
})
.map(|(v, _)| v);
if let Some(newest) = vers.next() {
self.newest_version = Some(newest.clone());
if self.newest_version.as_ref().unwrap().is_prerelease() &&
!self.want_to_install_prerelease(self.newest_version.as_ref().unwrap(), install_prereleases) {
if let Some(newest_nonpre) = vers.find(|v| !v.is_prerelease()) {
mem::swap(&mut self.alternative_version, &mut self.newest_version);
self.newest_version = Some(newest_nonpre.clone());
}
}
}
}
pub fn needs_update(&self, req: Option<&SemverReq>, install_prereleases: Option<bool>, downdate: bool) -> bool {
fn criterion(fromver: &Semver, tover: &Semver, downdate: bool) -> bool {
if downdate {
fromver != tover
} else {
fromver < tover
}
}
let update_to_version = self.update_to_version();
(req.into_iter().zip(self.version.as_ref()).map(|(sr, cv)| !sr.matches(cv)).next().unwrap_or(true) ||
req.into_iter().zip(update_to_version).map(|(sr, uv)| sr.matches(uv)).next().unwrap_or(true)) &&
update_to_version.map(|upd_v| {
(!upd_v.is_prerelease() || self.want_to_install_prerelease(upd_v, install_prereleases)) &&
(self.version.is_none() || criterion(self.version.as_ref().unwrap(), upd_v, downdate))
})
.unwrap_or(false)
}
pub fn update_to_version(&self) -> Option<&Semver> {
self.newest_version.as_ref().map(|new_v| cmp::min(new_v, self.max_version.as_ref().unwrap_or(new_v)))
}
}
impl GitRepoPackage {
pub fn parse(what: &str, executables: Vec<String>) -> Option<GitRepoPackage> {
parse_git_package_ident(what).map(|(name, url, sha)| {
let mut url = Url::parse(url).unwrap();
let branch = url.query_pairs().find(|&(ref name, _)| name == "branch").map(|(_, value)| value.to_string());
url.set_query(None);
GitRepoPackage {
name: name.to_string(),
url: url.into(),
branch: branch,
id: Oid::from_str(sha).unwrap(),
newest_id: Err(GitError::from_str("")),
executables: executables,
}
})
}
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) {
self.pull_version_impl(temp_dir.as_ref(), git_db_dir.as_ref(), http_proxy, fork_git)
}
fn pull_version_impl(&mut self, temp_dir: &Path, git_db_dir: &Path, http_proxy: Option<&str>, fork_git: bool) {
let clone_dir = find_git_db_repo(git_db_dir, &self.url).unwrap_or_else(|| temp_dir.join(&self.name));
if !clone_dir.exists() {
self.newest_id = if fork_git {
Command::new(env::var_os("GIT").as_ref().map(OsString::as_os_str).unwrap_or(OsStr::new("git")))
.args(&["ls-remote", "--", &self.url, self.branch.as_ref().map(String::as_str).unwrap_or("HEAD")])
.arg(&clone_dir)
.stderr(Stdio::inherit())
.output()
.ok()
.filter(|s| s.status.success())
.map(|s| s.stdout)
.and_then(|o| String::from_utf8(o).ok())
.and_then(|o| o.split('\t').next().and_then(|o| Oid::from_str(o).ok()))
.ok_or(GitError::from_str(""))
} else {
with_authentication(&self.url, |creds| {
git2::Remote::create_detached(self.url.clone()).and_then(|mut r| {
let mut cb = RemoteCallbacks::new();
cb.credentials(|a, b, c| creds(a, b, c));
r.connect_auth(git2::Direction::Fetch,
Some(cb),
http_proxy.map(|http_proxy| proxy_options_from_proxy_url(&self.url, http_proxy)))
.and_then(|rc| {
rc.list()?
.into_iter()
.find(|rh| match self.branch.as_ref() {
Some(b) => {
if rh.name().starts_with("refs/heads/") {
rh.name()["refs/heads/".len()..] == b[..]
} else if rh.name().starts_with("refs/tags/") {
rh.name()["refs/tags/".len()..] == b[..]
} else {
false
}
}
None => rh.name() == "HEAD",
})
.map(|rh| rh.oid())
.ok_or(git2::Error::from_str(""))
})
})
})
};
if self.newest_id.is_ok() {
return;
}
}
let repo = self.pull_version_repo(&clone_dir, http_proxy, fork_git);
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"))));
}
fn pull_version_fresh_clone(&self, clone_dir: &Path, http_proxy: Option<&str>, fork_git: bool) -> Result<Repository, GitError> {
if fork_git {
Command::new(env::var_os("GIT").as_ref().map(OsString::as_os_str).unwrap_or(OsStr::new("git")))
.arg("clone")
.args(self.branch.as_ref().map(|_| "-b"))
.args(self.branch.as_ref())
.args(&["--bare", "--", &self.url])
.arg(clone_dir)
.status()
.map_err(|e| GitError::from_str(&e.to_string()))
.and_then(|e| if e.success() {
Repository::open(clone_dir)
} else {
Err(GitError::from_str(&e.to_string()))
})
} else {
with_authentication(&self.url, |creds| {
let mut bldr = git2::build::RepoBuilder::new();
let mut cb = RemoteCallbacks::new();
cb.credentials(|a, b, c| creds(a, b, c));
bldr.fetch_options(fetch_options_from_proxy_url_and_callbacks(&self.url, http_proxy, cb));
if let Some(ref b) = self.branch.as_ref() {
bldr.branch(b);
}
bldr.bare(true);
bldr.clone(&self.url, &clone_dir)
})
}
}
fn pull_version_repo(&self, clone_dir: &Path, http_proxy: Option<&str>, fork_git: bool) -> Result<Repository, GitError> {
if let Ok(r) = Repository::open(clone_dir) {
let (branch, tofetch) = match self.branch.as_ref() {
Some(b) => {
r.set_head(&format!("refs/heads/{}", b)).map_err(|e| panic!("Couldn't set HEAD to chosen branch {}: {}", b, e)).unwrap();
(Cow::from(b), Cow::from(b))
}
None => {
match r.find_reference("HEAD")
.map_err(|e| panic!("No HEAD in {}: {}", clone_dir.display(), e))
.unwrap()
.symbolic_target() {
Some(ht) => (ht["refs/heads/".len()..].to_string().into(), "+HEAD:refs/remotes/origin/HEAD".into()),
None => {
fs::remove_dir_all(clone_dir).unwrap();
return self.pull_version_fresh_clone(clone_dir, http_proxy, fork_git);
}
}
}
};
let mut remote = "origin";
r.find_remote("origin")
.or_else(|_| {
remote = &self.url;
r.remote_anonymous(&self.url)
})
.and_then(|mut rm| if fork_git {
Command::new(env::var_os("GIT").as_ref().map(OsString::as_os_str).unwrap_or(OsStr::new("git")))
.arg("-C")
.arg(r.path())
.args(&["fetch", remote, &tofetch])
.status()
.map_err(|e| GitError::from_str(&e.to_string()))
.and_then(|e| if e.success() {
Ok(())
} else {
Err(GitError::from_str(&e.to_string()))
})
} else {
with_authentication(&self.url, |creds| {
let mut cb = RemoteCallbacks::new();
cb.credentials(|a, b, c| creds(a, b, c));
rm.fetch(&[&tofetch[..]],
Some(&mut fetch_options_from_proxy_url_and_callbacks(&self.url, http_proxy, cb)),
None)
})
})
.map_err(|e| panic!("Fetching {} from {}: {}", clone_dir.display(), self.url, e))
.unwrap();
r.branch(&branch,
&r.find_reference("FETCH_HEAD")
.map_err(|e| panic!("No FETCH_HEAD in {}: {}", clone_dir.display(), e))
.unwrap()
.peel_to_commit()
.map_err(|e| panic!("FETCH_HEAD not a commit in {}: {}", clone_dir.display(), e))
.unwrap(),
true)
.map_err(|e| panic!("Setting local branch {} in {}: {}", branch, clone_dir.display(), e))
.unwrap();
Ok(r)
} else {
let _ = fs::remove_dir_all(&clone_dir).or_else(|_| fs::remove_file(&clone_dir));
self.pull_version_fresh_clone(clone_dir, http_proxy, fork_git)
}
}
pub fn needs_update(&self) -> bool {
self.newest_id.is_ok() && self.id != *self.newest_id.as_ref().unwrap()
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub enum PackageFilterElement {
Toolchain(String),
}
impl PackageFilterElement {
pub fn parse(from: &str) -> Result<PackageFilterElement, String> {
let (key, value) = from.split_at(from.find('=').ok_or_else(|| format!(r#"Filter string "{}" does not contain the key/value separator "=""#, from))?);
let value = &value[1..];
Ok(match key {
"toolchain" => PackageFilterElement::Toolchain(value.to_string()),
_ => return Err(format!(r#"Unrecognised filter key "{}""#, key)),
})
}
pub fn matches(&self, cfg: &PackageConfig) -> bool {
match *self {
PackageFilterElement::Toolchain(ref chain) => Some(chain) == cfg.toolchain.as_ref(),
}
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct CargoConfig {
pub net_git_fetch_with_cli: bool,
pub registries_crates_io_protocol_sparse: bool,
pub http: HttpCargoConfig,
pub sparse_registries: SparseRegistryConfig,
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct HttpCargoConfig {
pub cainfo: Option<PathBuf>,
pub check_revoke: bool,
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct SparseRegistryConfig {
pub global_credential_providers: Vec<SparseRegistryAuthProvider>,
pub crates_io_credential_provider: Option<SparseRegistryAuthProvider>,
pub crates_io_token_env: Option<String>,
pub crates_io_token: Option<String>,
pub registry_tokens_env: BTreeMap<CargoConfigEnvironmentNormalisedString, String>,
pub registry_tokens: BTreeMap<String, String>,
pub credential_aliases: BTreeMap<CargoConfigEnvironmentNormalisedString, Vec<String>>,
}
impl SparseRegistryConfig {
pub fn credential_provider(&self, v: toml::Value) -> Option<SparseRegistryAuthProvider> {
SparseRegistryConfig::credential_provider_impl(&self.credential_aliases, v)
}
fn credential_provider_impl(credential_aliases: &BTreeMap<CargoConfigEnvironmentNormalisedString, Vec<String>>, v: toml::Value)
-> Option<SparseRegistryAuthProvider> {
match v {
toml::Value::String(s) => Some(CargoConfig::string_provider(s, &credential_aliases)),
toml::Value::Array(a) => Some(SparseRegistryAuthProvider::from_config(CargoConfig::string_array(a))),
_ => None,
}
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub enum SparseRegistryAuthProvider {
TokenNoEnvironment,
Token,
Wincred,
MacosKeychain,
Libsecret,
TokenFromStdout(Vec<String>),
Provider(Vec<String>),
}
impl SparseRegistryAuthProvider {
pub fn from_config(mut toks: Vec<String>) -> SparseRegistryAuthProvider {
match toks.get(0).map(String::as_str).unwrap_or("") {
"cargo:token" => SparseRegistryAuthProvider::Token,
"cargo:wincred" => SparseRegistryAuthProvider::Wincred,
"cargo:macos-keychain" => SparseRegistryAuthProvider::MacosKeychain,
"cargo:libsecret" => SparseRegistryAuthProvider::Libsecret,
"cargo:token-from-stdout" => {
toks.remove(0);
SparseRegistryAuthProvider::TokenFromStdout(toks)
}
_ => SparseRegistryAuthProvider::Provider(toks),
}
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct CargoConfigEnvironmentNormalisedString(pub String);
impl CargoConfigEnvironmentNormalisedString {
pub fn normalise(mut s: String) -> CargoConfigEnvironmentNormalisedString {
s.make_ascii_uppercase();
while let Some(i) = s.find(['.', '-']) {
s.replace_range(i..i + 1, "_");
}
CargoConfigEnvironmentNormalisedString(s)
}
}
impl CargoConfig {
pub fn load(crates_file: &Path) -> CargoConfig {
let mut cfg = fs::read_to_string(crates_file.with_file_name("config"))
.or_else(|_| fs::read_to_string(crates_file.with_file_name("config.toml")))
.ok()
.and_then(|s| s.parse::<toml::Value>().ok());
let mut creds = fs::read_to_string(crates_file.with_file_name("credentials"))
.or_else(|_| fs::read_to_string(crates_file.with_file_name("credentials.toml")))
.ok()
.and_then(|s| s.parse::<toml::Value>().ok());
let credential_aliases = None.or_else(|| match cfg.as_mut()?.as_table_mut()?.remove("credential-alias")? {
toml::Value::Table(t) => Some(t),
_ => None,
})
.unwrap_or_default()
.into_iter()
.flat_map(|(k, v)| {
match v {
toml::Value::String(s) => Some(s.split(' ').map(String::from).collect()),
toml::Value::Array(a) => Some(CargoConfig::string_array(a)),
_ => None,
}
.map(|v| (CargoConfigEnvironmentNormalisedString::normalise(k), v))
})
.chain(env::vars_os()
.map(|(k, v)| (k.into_encoded_bytes(), v))
.filter(|(k, _)| k.starts_with(b"CARGO_CREDENTIAL_ALIAS_"))
.filter(|(k, _)| k["CARGO_CREDENTIAL_ALIAS_".len()..].iter().all(|&b| !(b.is_ascii_lowercase() || b == b'.' || b == b'-')))
.flat_map(|(mut k, v)| {
let k = String::from_utf8(k.drain("CARGO_CREDENTIAL_ALIAS_".len()..).collect()).ok()?;
let v = v.into_string().ok()?;
Some((CargoConfigEnvironmentNormalisedString(k), v.split(' ').map(String::from).collect()))
}))
.collect();
CargoConfig {
net_git_fetch_with_cli: env::var("CARGO_NET_GIT_FETCH_WITH_CLI")
.ok()
.and_then(|e| if e.is_empty() {
Some(toml::Value::String(String::new()))
} else {
e.parse::<toml::Value>().ok()
})
.or_else(|| {
cfg.as_mut()?
.as_table_mut()?
.get_mut("net")?
.as_table_mut()?
.remove("git-fetch-with-cli")
})
.map(CargoConfig::truthy)
.unwrap_or(false),
registries_crates_io_protocol_sparse: env::var("CARGO_REGISTRIES_CRATES_IO_PROTOCOL")
.map(|s| s == "sparse")
.ok()
.or_else(|| {
Some(cfg.as_mut()?
.as_table_mut()?
.get_mut("registries")?
.as_table_mut()?
.get_mut("crates-io")?
.as_table_mut()?
.remove("protocol")?
.as_str()? == "sparse")
})
.unwrap_or(true),
http: HttpCargoConfig {
cainfo: env::var_os("CARGO_HTTP_CAINFO")
.map(PathBuf::from)
.or_else(|| {
CargoConfig::string(cfg.as_mut()?
.as_table_mut()?
.get_mut("http")?
.as_table_mut()?
.remove("cainfo")?)
.map(PathBuf::from)
}),
check_revoke: env::var("CARGO_HTTP_CHECK_REVOKE")
.ok()
.map(toml::Value::String)
.or_else(|| {
cfg.as_mut()?
.as_table_mut()?
.get_mut("http")?
.as_table_mut()?
.remove("check-revoke")
})
.map(CargoConfig::truthy)
.unwrap_or(cfg!(target_os = "windows")),
},
sparse_registries: SparseRegistryConfig {
global_credential_providers: None.or_else(|| {
CargoConfig::string_array_v(cfg.as_mut()?
.as_table_mut()?
.get_mut("registry")?
.as_table_mut()?
.remove("global-credential-providers")?)
})
.map(|a| a.into_iter().map(|s| CargoConfig::string_provider(s, &credential_aliases)).collect())
.unwrap_or_else(|| vec![SparseRegistryAuthProvider::TokenNoEnvironment]),
crates_io_credential_provider: env::var("CARGO_REGISTRY_CREDENTIAL_PROVIDER")
.ok()
.map(toml::Value::String)
.or_else(|| {
cfg.as_mut()?
.as_table_mut()?
.get_mut("registry")?
.as_table_mut()?
.remove("credential-provider")
})
.and_then(|v| SparseRegistryConfig::credential_provider_impl(&credential_aliases, v)),
crates_io_token_env: env::var("CARGO_REGISTRY_TOKEN").ok(),
crates_io_token: None.or_else(|| {
CargoConfig::string(creds.as_mut()?
.as_table_mut()?
.get_mut("registry")?
.as_table_mut()?
.remove("token")?)
})
.or_else(|| {
CargoConfig::string(cfg.as_mut()?
.as_table_mut()?
.get_mut("registry")?
.as_table_mut()?
.remove("token")?)
}),
registry_tokens_env: env::vars_os()
.map(|(k, v)| (k.into_encoded_bytes(), v))
.filter(|(k, _)| k.starts_with(b"CARGO_REGISTRIES_") && k.ends_with(b"_TOKEN"))
.filter(|(k, _)| {
k["CARGO_REGISTRIES_".len()..k.len() - b"_TOKEN".len()].iter().all(|&b| !(b.is_ascii_lowercase() || b == b'.' || b == b'-'))
})
.flat_map(|(mut k, v)| {
let k = String::from_utf8(k.drain("CARGO_REGISTRIES_".len()..k.len() - b"_TOKEN".len()).collect()).ok()?;
Some((CargoConfigEnvironmentNormalisedString(k), v.into_string().ok()?))
})
.collect(),
registry_tokens: cfg.as_mut()
.into_iter()
.chain(creds.as_mut())
.flat_map(|c| {
c.as_table_mut()?
.get_mut("registries")?
.as_table_mut()
})
.flat_map(|r| r.into_iter().flat_map(|(name, v)| Some((name.clone(), CargoConfig::string(v.as_table_mut()?.remove("token")?)?))))
.collect(),
credential_aliases: credential_aliases,
},
}
}
fn truthy(v: toml::Value) -> bool {
match v {
toml::Value::String(ref s) if s == "" => false,
toml::Value::Float(0.) => false,
toml::Value::Integer(0) |
toml::Value::Boolean(false) => false,
_ => true,
}
}
fn string(v: toml::Value) -> Option<String> {
match v {
toml::Value::String(s) => Some(s),
_ => None,
}
}
fn string_array(a: Vec<toml::Value>) -> Vec<String> {
a.into_iter().flat_map(CargoConfig::string).collect()
}
fn string_array_v(v: toml::Value) -> Option<Vec<String>> {
match v {
toml::Value::Array(s) => Some(CargoConfig::string_array(s)),
_ => None,
}
}
fn string_provider(s: String, credential_aliases: &BTreeMap<CargoConfigEnvironmentNormalisedString, Vec<String>>) -> SparseRegistryAuthProvider {
match credential_aliases.get(&CargoConfigEnvironmentNormalisedString::normalise(s.clone())) {
Some(av) => SparseRegistryAuthProvider::Provider(av.clone()),
None => {
SparseRegistryAuthProvider::from_config(if s.contains(' ') {
s.split(' ').map(String::from).collect()
} else {
vec![s]
})
}
}
}
}
pub fn crates_file_in(cargo_dir: &Path) -> PathBuf {
crates_file_in_impl(cargo_dir, BTreeSet::new())
}
fn crates_file_in_impl<'cd>(cargo_dir: &'cd Path, mut seen: BTreeSet<&'cd Path>) -> PathBuf {
if !seen.insert(cargo_dir) {
panic!("Cargo config install.root loop at {:?} (saw {:?})", cargo_dir.display(), seen);
}
let mut config_file = cargo_dir.join("config");
let mut config_data = fs::read_to_string(&config_file);
if config_data.is_err() {
config_file.set_file_name("config.toml");
config_data = fs::read_to_string(&config_file);
}
if let Ok(config_data) = config_data {
if let Some(idir) = toml::from_str::<toml::Value>(&config_data)
.unwrap()
.get("install")
.and_then(|t| t.as_table())
.and_then(|t| t.get("root"))
.and_then(|t| t.as_str()) {
return crates_file_in_impl(Path::new(idir), seen);
}
}
config_file.set_file_name(".crates.toml");
config_file
}
fn installed_packages_table(crates_file: &Path) -> Option<toml::Table> {
let crates_data = fs::read_to_string(crates_file).ok()?;
Some(toml::from_str::<toml::Value>(&crates_data).unwrap().get_mut("v1")?.as_table_mut().map(mem::take).unwrap())
}
pub fn installed_registry_packages(crates_file: &Path) -> Vec<RegistryPackage> {
let mut res = Vec::<RegistryPackage>::new();
for pkg in installed_packages_table(crates_file)
.into_iter()
.flatten()
.flat_map(|(s, x)| CargoConfig::string_array_v(x).and_then(|x| RegistryPackage::parse(&s, x))) {
if let Some(saved) = res.iter_mut().find(|p| p.name == pkg.name) {
if saved.version.is_none() || saved.version.as_ref().unwrap() < pkg.version.as_ref().unwrap() {
saved.version = pkg.version;
}
continue;
}
res.push(pkg);
}
res
}
pub fn installed_git_repo_packages(crates_file: &Path) -> Vec<GitRepoPackage> {
let mut res = Vec::<GitRepoPackage>::new();
for pkg in installed_packages_table(crates_file)
.into_iter()
.flatten()
.flat_map(|(s, x)| CargoConfig::string_array_v(x).and_then(|x| GitRepoPackage::parse(&s, x))) {
if let Some(saved) = res.iter_mut().find(|p| p.name == pkg.name) {
saved.id = pkg.id;
continue;
}
res.push(pkg);
}
res
}
pub fn intersect_packages(installed: &[RegistryPackage], to_update: &[(String, Option<Semver>, Cow<'static, str>)], allow_installs: bool,
installed_git: &[GitRepoPackage])
-> Vec<RegistryPackage> {
installed.iter()
.filter(|p| to_update.iter().any(|u| p.name == u.0))
.cloned()
.map(|p| RegistryPackage { max_version: to_update.iter().find(|u| p.name == u.0).and_then(|u| u.1.clone()), ..p })
.chain(to_update.iter()
.filter(|p| allow_installs && !installed.iter().any(|i| i.name == p.0) && !installed_git.iter().any(|i| i.name == p.0))
.map(|p| {
RegistryPackage {
name: p.0.clone(),
registry: p.2.clone(),
version: None,
newest_version: None,
alternative_version: None,
max_version: p.1.clone(),
executables: vec![],
}
}))
.collect()
}
pub fn crate_versions(buf: &[u8]) -> Result<Vec<(Semver, Option<DateTime<FixedOffset>>)>, Cow<'static, str>> {
buf.split_inclusive(|&b| b == b'\n').map(crate_version_line).flat_map(Result::transpose).collect()
}
fn crate_version_line(line: &[u8]) -> Result<Option<(Semver, Option<DateTime<FixedOffset>>)>, Cow<'static, str>> {
if line == b"\n" {
return Ok(None);
}
match json::from_slice(line).map_err(|e| e.to_string())? {
json::Value::Object(o) => {
if matches!(o.get("yanked"), Some(&json::Value::Bool(true))) {
return Ok(None);
}
let v = match o.get("vers").ok_or("no \"vers\" key")? {
json::Value::String(ref v) => Semver::parse(&v).map_err(|e| e.to_string())?,
_ => return Err("\"vers\" not string".into()),
};
let pt = match o.get("pubtime") {
None => None,
Some(json::Value::String(ref pt)) => Some(DateTime::parse_from_rfc3339(pt).map_err(|e| e.to_string())?),
Some(_) => return Err("\"pubtime\" not string".into()),
};
Ok(Some((v, pt)))
}
_ => Err("line not object".into()),
}
}
pub fn assert_index_path(cargo_dir: &Path, registry_url: &str, sparse: bool) -> Result<PathBuf, Cow<'static, str>> {
if sparse {
return Ok(PathBuf::from("/ENOENT"));
}
let path = cargo_dir.join("registry").join("index").join(registry_shortname(registry_url));
match path.metadata() {
Ok(meta) => {
if meta.is_dir() {
Ok(path)
} else {
Err(format!("{} (index directory for {}) not a directory", path.display(), registry_url).into())
}
}
Err(ref e) if e.kind() == IoErrorKind::NotFound => {
fs::create_dir_all(&path).map_err(|e| format!("Couldn't create {} (index directory for {}): {}", path.display(), registry_url, e))?;
Ok(path)
}
Err(e) => Err(format!("Couldn't read {} (index directory for {}): {}", path.display(), registry_url, e).into()),
}
}
pub fn open_index_repository(registry: &Path, sparse: bool) -> Result<Registry, (bool, GitError)> {
match sparse {
false => {
Repository::open(®istry).map(Registry::Git).or_else(|e| if e.code() == GitErrorCode::NotFound {
Repository::init(®istry).map(Registry::Git).map_err(|e| (true, e))
} else {
Err((false, e))
})
}
true => Ok(Registry::Sparse(BTreeMap::new())),
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct SparseRegistryAuthProviderBundle<'sr>(pub Cow<'sr, [SparseRegistryAuthProvider]>,
pub &'sr OsStr,
pub &'sr str,
pub Cow<'sr, str>,
pub Option<&'sr str>,
pub Option<&'sr str>);
impl<'sr> SparseRegistryAuthProviderBundle<'sr> {
pub fn try(&self) -> Option<Cow<'sr, str>> {
let (install_cargo, repo_name, repo_url, token_env, token) = (self.1, self.2, &self.3, self.4, self.5);
self.0
.iter()
.rev()
.find_map(|p| match p {
SparseRegistryAuthProvider::TokenNoEnvironment => token.map(Cow::from),
SparseRegistryAuthProvider::Token => token_env.or(token).map(Cow::from),
SparseRegistryAuthProvider::Wincred => {
#[allow(unused_mut)]
let mut ret = None;
#[cfg(target_os="windows")]
unsafe {
let mut cred = ptr::null_mut();
if WinCred::CredReadA(PCSTR(format!("cargo-registry:{}\0", repo_url).as_ptr()),
WinCred::CRED_TYPE_GENERIC,
None,
&mut cred)
.is_ok() {
ret = str::from_utf8(slice::from_raw_parts((*cred).CredentialBlob, (*cred).CredentialBlobSize as usize))
.map(str::to_string)
.map(Cow::from)
.ok();
WinCred::CredFree(cred as _);
}
}
ret
}
SparseRegistryAuthProvider::MacosKeychain => {
#[allow(unused_mut, unused_assignments)]
let mut ret = None;
#[cfg(target_vendor = "apple")]
{
ret = SecKeychain::default()
.and_then(|k| k.find_generic_password(&format!("cargo-registry:{}", repo_url), ""))
.ok()
.and_then(|(p, _)| str::from_utf8(&*p).map(str::to_string).map(Cow::from).ok());
}
ret
}
SparseRegistryAuthProvider::Libsecret => {
#[allow(unused_mut)]
let mut ret = None;
#[cfg(all(unix, not(target_vendor = "apple")))]
#[allow(non_camel_case_types)]
unsafe {
#[repr(C)]
struct SecretSchemaAttribute {
name: *const u8,
flags: libc::c_int, }
#[repr(C)]
struct SecretSchema {
name: *const u8,
flags: libc::c_int,
attributes: [SecretSchemaAttribute; 32],
reserved: libc::c_int,
reserved1: *const (),
reserved2: *const (),
reserved3: *const (),
reserved4: *const (),
reserved5: *const (),
reserved6: *const (),
reserved7: *const (),
}
unsafe impl Sync for SecretSchema {}
type secret_password_lookup_sync_t = extern "C" fn(*const SecretSchema, *mut (), *mut (), ...) -> *mut u8;
type secret_password_free_t = extern "C" fn(*mut u8);
static LIBSECRET: LazyLock<Option<(secret_password_lookup_sync_t, secret_password_free_t)>> = LazyLock::new(|| unsafe {
let libsecret = libc::dlopen(b"libsecret-1.so.0\0".as_ptr() as _, libc::RTLD_LAZY);
if libsecret.is_null() {
return None;
}
let lookup = libc::dlsym(libsecret, b"secret_password_lookup_sync\0".as_ptr() as _);
let free = libc::dlsym(libsecret, b"secret_password_free\0".as_ptr() as _);
if lookup.is_null() || free.is_null() {
libc::dlclose(libsecret);
return None;
}
Some((mem::transmute(lookup), mem::transmute(free)))
});
static SCHEMA: SecretSchema = unsafe {
let mut schema: SecretSchema = mem::zeroed();
schema.name = b"org.rust-lang.cargo.registry\0".as_ptr() as _;
schema.attributes[0].name = b"url\0".as_ptr() as _;
schema
};
if let Some((lookup, free)) = *LIBSECRET {
let pass = lookup(&SCHEMA,
ptr::null_mut(),
ptr::null_mut(),
b"url\0".as_ptr(),
format!("{}\0", repo_url).as_ptr(),
ptr::null() as *const u8);
if !pass.is_null() {
ret = str::from_utf8(slice::from_raw_parts(pass, libc::strlen(pass as _))).map(str::to_string).map(Cow::from).ok();
free(pass);
}
}
}
ret
}
SparseRegistryAuthProvider::TokenFromStdout(args) => {
Command::new(&args[0])
.args(&args[1..])
.env("CARGO", install_cargo)
.env("CARGO_REGISTRY_INDEX_URL", &repo_url[..])
.env("CARGO_REGISTRY_NAME_OPT", repo_name)
.stdin(Stdio::inherit())
.stderr(Stdio::inherit())
.output()
.ok()
.filter(|o| o.status.success())
.map(|o| o.stdout)
.and_then(|o| String::from_utf8(o).ok())
.map(|mut o| {
o.replace_range(o.rfind(|c| c != '\n').unwrap_or(o.len()) + 1..o.len(), "");
o.replace_range(0..o.find(|c| c != '\n').unwrap_or(0), "");
o.into()
})
}
SparseRegistryAuthProvider::Provider(args) => {
Command::new(&args[0])
.arg("--cargo-plugin")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.ok()
.and_then(|mut child| {
let mut stdin = BufWriter::new(child.stdin.take().unwrap());
let mut stdout = BufReader::new(child.stdout.take().unwrap());
let mut l = String::new();
stdout.read_line(&mut l).map_err(|_| child.kill()).ok()?;
{
let mut hello: json::Value = json::from_str(&l).map_err(|_| child.kill()).ok()?;
hello.as_object_mut()
.and_then(|h| h.remove("v"))
.and_then(|mut v| v.as_array_mut().filter(|vs| vs.contains(&json::Value::Number(1.into()))).map(drop))
.ok_or_else(|| child.kill())
.ok()?;
}
let req = json::Value::Object({
let mut kv = json::Map::new();
kv.insert("v".to_string(), json::Value::Number(1.into()));
kv.insert("registry".to_string(),
json::Value::Object({
let mut kv = json::Map::new();
kv.insert("index-url".to_string(), json::Value::String(repo_url.to_string()));
kv.insert("name".to_string(), json::Value::String(repo_name.to_string()));
kv
}));
kv.insert("kind".to_string(), json::Value::String("get".to_string()));
kv.insert("operation".to_string(), json::Value::String("read".to_string()));
kv.insert("args".to_string(),
json::Value::Array(args.into_iter().skip(1).cloned().map(json::Value::String).collect()));
kv
});
json::to_writer(&mut stdin, &req).map_err(|_| child.kill()).ok()?;
stdin.write_all(b"\n").map_err(|_| child.kill()).ok()?;
stdin.flush().map_err(|_| child.kill()).ok()?;
l.clear();
stdout.read_line(&mut l).map_err(|_| child.kill()).ok()?;
let mut res: json::Value = json::from_str(&l).map_err(|_| child.kill()).ok()?;
match res.as_object_mut()
.and_then(|h| h.remove("Ok"))
.and_then(|mut ok| ok.as_object_mut().and_then(|ok| ok.remove("token"))) {
Some(json::Value::String(tok)) => Some(tok.into()),
Some(_) => {
let _ = child.kill();
None
}
None => {
let _ = io::stderr()
.write_all(b"\n")
.ok()
.and_then(|_| json::to_writer(&mut io::stderr(), &res).ok().and_then(|_| io::stderr().write_all(b"\n").ok()));
None
}
}
})
}
})
}
}
pub fn auth_providers<'sr>(crates_file: &Path, install_cargo: Option<&'sr OsStr>, sparse_registries: &'sr SparseRegistryConfig, sparse: bool,
repo_name: &'sr str, repo_url: &'sr str)
-> SparseRegistryAuthProviderBundle<'sr> {
let cargo = install_cargo.unwrap_or(OsStr::new("cargo"));
if !sparse {
return SparseRegistryAuthProviderBundle(vec![].into(), cargo, "!sparse", "!sparse".into(), None, None);
}
if repo_name == "crates-io" {
let ret = match sparse_registries.crates_io_credential_provider.as_ref() {
Some(prov) => slice::from_ref(prov).into(),
None => sparse_registries.global_credential_providers[..].into(),
};
return SparseRegistryAuthProviderBundle(ret,
cargo,
repo_name,
format!("sparse+{}", repo_url).into(),
sparse_registries.crates_io_token_env.as_deref(),
sparse_registries.crates_io_token.as_deref());
}
let ret: Cow<'sr, [SparseRegistryAuthProvider]> = match fs::read_to_string(crates_file.with_file_name("config"))
.or_else(|_| fs::read_to_string(crates_file.with_file_name("config.toml")))
.ok()
.and_then(|s| s.parse::<toml::Value>().ok())
.and_then(|mut c| {
sparse_registries.credential_provider(c.as_table_mut()?
.remove("registries")?
.as_table_mut()?
.remove(repo_name)?
.as_table_mut()?
.remove("credential-provider")?)
}) {
Some(prov) => vec![prov].into(),
None => sparse_registries.global_credential_providers[..].into(),
};
let token_env = if ret.contains(&SparseRegistryAuthProvider::Token) {
sparse_registries.registry_tokens_env.get(&CargoConfigEnvironmentNormalisedString::normalise(repo_name.to_string())).map(String::as_str)
} else {
None
};
SparseRegistryAuthProviderBundle(ret,
cargo,
repo_name,
format!("sparse+{}", repo_url).into(),
token_env,
sparse_registries.registry_tokens.get(repo_name).map(String::as_str))
}
pub 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>,
fork_git: bool, http: &HttpCargoConfig, auth_token: Option<&str>, out: &mut W)
-> Result<(), String> {
write!(out,
" {} registry '{}'{}",
["Updating", "Polling"][matches!(index_repo, Registry::Sparse(_)) as usize],
repo_url,
["\n", ""][matches!(index_repo, Registry::Sparse(_)) as usize]).and_then(|_| out.flush())
.map_err(|e| format!("failed to write updating message: {}", e))?;
match index_repo {
Registry::Git(index_repo) => {
if fork_git {
Command::new(env::var_os("GIT").as_ref().map(OsString::as_os_str).unwrap_or(OsStr::new("git"))).arg("-C")
.arg(index_repo.path())
.args(&["fetch", "-f", repo_url, "HEAD:refs/remotes/origin/HEAD"])
.status()
.map_err(|e| e.to_string())
.and_then(|e| if e.success() {
Ok(())
} else {
Err(e.to_string())
})?;
} else {
index_repo.remote_anonymous(repo_url)
.and_then(|mut r| {
with_authentication(repo_url, |creds| {
let mut cb = RemoteCallbacks::new();
cb.credentials(|a, b, c| creds(a, b, c));
r.fetch(&["HEAD:refs/remotes/origin/HEAD"],
Some(&mut fetch_options_from_proxy_url_and_callbacks(repo_url, http_proxy, cb)),
None)
})
})
.map_err(|e| e.message().to_string())?;
}
}
Registry::Sparse(registry) => {
let mut sucker = CurlMulti::new();
sucker.pipelining(true, true).map_err(|e| format!("pipelining: {}", e))?;
let writussy = Mutex::new(&mut *out);
let mut conns: Vec<_> = Result::from_iter(packages.map(|pkg| {
sucker.add2(CurlEasy::new(SparseHandler(pkg.as_ref().to_string(), Err("init".into()), &writussy, Some(b'.'))))
.map(|h| (Some(h), Ok(())))
.map_err(|e| format!("add2: {}", e))
}))?;
const ATTEMPTS: u8 = 4;
for attempt in 0..ATTEMPTS {
if conns.is_empty() {
break;
}
std::thread::sleep(Duration::from_secs((1 << attempt) - 1));
for c in &mut conns {
let mut conn = sucker.remove2(c.0.take().unwrap()).map_err(|e| format!("remove2: {}", e))?;
conn.get_mut().1 = Ok((vec![], vec![]));
conn.get_mut().3.get_or_insert(b'0' + attempt);
conn.reset();
conn.url(&split_package_path(&conn.get_ref().0).into_iter().fold(repo_url.to_string(), |mut u, s| {
if !u.ends_with('/') {
u.push('/');
}
u.push_str(&s);
u
}))
.map_err(|e| format!("url: {}", e))?;
if let Some(auth_token) = auth_token.as_ref() {
let mut headers = CurlList::new();
headers.append(&format!("Authorization: {}", auth_token)).map_err(|e| format!("append: {}", e))?;
conn.http_headers(headers).map_err(|e| format!("http_headers: {}", e))?;
}
if let Some(http_proxy) = http_proxy {
conn.proxy(http_proxy).map_err(|e| format!("proxy: {}", e))?;
}
conn.pipewait(true).map_err(|e| format!("pipewait: {}", e))?;
conn.progress(true).map_err(|e| format!("progress: {}", e))?;
if let Some(cainfo) = http.cainfo.as_ref() {
conn.cainfo(cainfo).map_err(|e| format!("cainfo: {}", e))?;
}
conn.ssl_options(CurlSslOpt::new().no_revoke(!http.check_revoke)).map_err(|e| format!("ssl_options: {}", e))?;
c.0 = Some(sucker.add2(conn).map_err(|e| format!("add2: {}", e))?);
}
while sucker.perform().map_err(|e| format!("perform: {}", e))? > 0 {
sucker.wait(&mut [], Duration::from_millis(200)).map_err(|e| format!("wait: {}", e))?;
}
sucker.messages(|m| {
for c in &mut conns {
if let Some(err) = m.result_for2(&c.0.as_ref().unwrap()) {
c.1 = err;
}
}
});
let mut retainer = |c: &mut (Option<CurlEasyHandle<SparseHandler<'_, '_, _>>>, Result<(), _>)| {
let pkg = mem::take(&mut c.0.as_mut().unwrap().get_mut().0);
match c.0.as_mut().unwrap().response_code().map_err(|e| format!("response_code: {}", e))? {
200 => {
let (mut resp, buf) =
mem::replace(&mut c.0.as_mut().unwrap().get_mut().1, Err("taken".into())).map_err(|e| format!("package {}: {}", pkg, e))?;
mem::replace(&mut c.1, Ok(())).map_err(|e| format!("package {}: {}", pkg, e))?;
if !buf.is_empty() {
return Err(format!("package {}: {} bytes of trailing garbage", pkg, buf.len()))?;
}
resp.sort();
sucker.remove2(c.0.take().unwrap()).map_err(|e| format!("remove2: {}", e))?;
registry.insert(pkg, resp);
Ok(false)
}
rc @ 404 | rc @ 410 | rc @ 451 => Err(format!("package {} doesn't exist: HTTP {}", pkg, rc)),
rc @ 408 | rc @ 429 | rc @ 503 | rc @ 504 => {
if attempt == ATTEMPTS - 1 {
Err(format!("package {}: HTTP {} after {} attempts", pkg, rc, ATTEMPTS))
} else {
c.0.as_mut().unwrap().get_mut().0 = pkg;
Ok(true)
}
}
rc => Err(format!("package {}: HTTP {}", pkg, rc)),
}
};
let mut err = Ok(());
conns.retain_mut(|c| {
if err.is_err() {
return false;
}
match retainer(c) {
Ok(r) => r,
Err(e) => {
if let Ok(mut out) = writussy.lock() {
let _ = writeln!(out);
}
err = Err(e);
false
}
}
});
err?;
}
if let Ok(mut out) = writussy.lock() {
let _ = writeln!(out);
};
}
}
writeln!(out).map_err(|e| format!("failed to write post-update newline: {}", e))?;
Ok(())
}
struct SparseHandler<'m, 'w: 'm, W: Write>(String,
Result<(Vec<(Semver, Option<DateTime<FixedOffset>>)>, Vec<u8>), Cow<'static, str>>,
&'m Mutex<&'w mut W>,
Option<u8>);
impl<'m, 'w: 'm, W: Write> CurlHandler for SparseHandler<'m, 'w, W> {
fn write(&mut self, data: &[u8]) -> Result<usize, CurlWriteError> {
let mut consumed = 0;
self.1 = mem::replace(&mut self.1, Err("write".into())).and_then(|(mut vers, mut buf)| {
for l in data.split_inclusive(|&b| b == b'\n') {
if !l.ends_with(b"\n") {
buf.extend(l);
consumed += l.len();
continue;
}
let line = if buf.is_empty() {
l
} else {
buf.extend(l);
&buf[..]
};
vers.extend(crate_version_line(line)?);
buf.clear();
consumed += l.len();
}
Ok((vers, buf))
});
Ok(consumed)
}
fn progress(&mut self, dltotal: f64, dlnow: f64, _: f64, _: f64) -> bool {
if dltotal != 0.0 && dltotal == dlnow {
if let Some(status) = self.3.take() {
if let Ok(mut out) = self.2.lock() {
let _ = out.write_all(&[status]).and_then(|_| out.flush());
}
}
}
true
}
}
pub enum Registry {
Git(Repository),
Sparse(BTreeMap<String, Vec<(Semver, Option<DateTime<FixedOffset>>)>>),
}
pub enum RegistryTree<'a> {
Git(Tree<'a>),
Sparse,
}
pub fn parse_registry_head(registry_repo: &Registry) -> Result<RegistryTree<'_>, GitError> {
match registry_repo {
Registry::Git(registry_repo) => {
registry_repo.revparse_single("FETCH_HEAD")
.or_else(|_| registry_repo.revparse_single("origin/HEAD"))
.map(|h| h.as_commit().unwrap().tree().unwrap())
.map(RegistryTree::Git)
}
Registry::Sparse(_) => Ok(RegistryTree::Sparse),
}
}
fn proxy_options_from_proxy_url<'a>(repo_url: &str, proxy_url: &str) -> ProxyOptions<'a> {
let mut prx = ProxyOptions::new();
let mut url = Cow::from(proxy_url);
if Url::parse(proxy_url).is_err() {
if let Ok(rurl) = Url::parse(repo_url) {
let replacement_proxy_url = format!("{}://{}", rurl.scheme(), proxy_url);
if Url::parse(&replacement_proxy_url).is_ok() {
url = Cow::from(replacement_proxy_url);
}
}
}
prx.url(&url);
prx
}
fn fetch_options_from_proxy_url_and_callbacks<'a>(repo_url: &str, proxy_url: Option<&str>, callbacks: RemoteCallbacks<'a>) -> FetchOptions<'a> {
let mut ret = FetchOptions::new();
if let Some(proxy_url) = proxy_url {
ret.proxy_options(proxy_options_from_proxy_url(repo_url, proxy_url));
}
ret.remote_callbacks(callbacks);
ret
}
pub fn get_index_url(crates_file: &Path, registry: &str, registries_crates_io_protocol_sparse: bool)
-> Result<(Cow<'static, str>, bool, Cow<'static, str>), Cow<'static, str>> {
let mut config_file = crates_file.with_file_name("config");
let config = if let Ok(cfg) = fs::read_to_string(&config_file).or_else(|_| {
config_file.set_file_name("config.toml");
fs::read_to_string(&config_file)
}) {
toml::from_str::<toml::Value>(&cfg).map_err(|e| format!("{} not TOML: {}", config_file.display(), e))?
} else {
if registry == "https://github.com/rust-lang/crates.io-index" {
if registries_crates_io_protocol_sparse {
return Ok(("https://index.crates.io/".into(), true, "crates-io".into()));
} else {
return Ok((registry.to_string().into(), false, "crates-io".into()));
}
} else {
Err(format!("Non-crates.io registry specified and no config file found at {} or {}. \
Due to a Cargo limitation we will not be able to install from there \
until it's given a [source.NAME] in that file!",
config_file.with_file_name("config").display(),
config_file.display()))?
}
};
let mut replacements = BTreeMap::new();
let mut registries = BTreeMap::new();
let mut cur_source = Cow::from(registry);
registries.insert("crates-io",
Cow::from(if registries_crates_io_protocol_sparse {
"sparse+https://index.crates.io/"
} else {
"https://github.com/rust-lang/crates.io-index"
}));
if cur_source == "https://github.com/rust-lang/crates.io-index" || cur_source == "sparse+https://index.crates.io/" {
cur_source = "crates-io".into();
}
if let Some(source) = config.get("source") {
for (name, v) in source.as_table().ok_or("source not table")? {
if let Some(replacement) = v.get("replace-with") {
replacements.insert(&name[..],
replacement.as_str().ok_or_else(|| format!("source.{}.replacement not string", name))?);
}
if let Some(url) = v.get("registry") {
let url = url.as_str().ok_or_else(|| format!("source.{}.registry not string", name))?.to_string().into();
if cur_source == url {
cur_source = name.into();
}
registries.insert(&name[..], url);
}
}
}
if let Some(registries_tabls) = config.get("registries") {
let table = registries_tabls.as_table().ok_or("registries is not a table")?;
for (name, url) in table.iter().flat_map(|(name, val)| val.as_table()?.get("index")?.as_str().map(|v| (name, v))) {
if cur_source == url.strip_prefix("sparse+").unwrap_or(url) {
cur_source = name.into()
}
registries.insert(name, url.into());
}
}
if Url::parse(&cur_source).is_ok() {
Err(format!("Non-crates.io registry specified and {} couldn't be found in the config file at {}. \
Due to a Cargo limitation we will not be able to install from there \
until it's given a [source.NAME] in that file!",
cur_source,
config_file.display()))?
}
while let Some(repl) = replacements.get(&cur_source[..]) {
cur_source = Cow::from(&repl[..]);
}
registries.get(&cur_source[..])
.map(|reg| (reg.strip_prefix("sparse+").unwrap_or(reg).to_string().into(), reg.starts_with("sparse+"), cur_source.to_string().into()))
.ok_or_else(|| {
format!("Couldn't find appropriate source URL for {} in {} (resolved to {:?})",
registry,
config_file.display(),
cur_source)
.into()
})
}
fn with_authentication<T, F>(url: &str, mut f: F) -> Result<T, GitError>
where F: FnMut(&mut git2::Credentials) -> Result<T, GitError>
{
let cfg = GitConfig::open_default().unwrap();
let mut cred_helper = git2::CredentialHelper::new(url);
cred_helper.config(&cfg);
let mut ssh_username_requested = false;
let mut cred_helper_bad = None;
let mut ssh_agent_attempts = Vec::new();
let mut any_attempts = false;
let mut tried_ssh_key = false;
let mut res = f(&mut |url, username, allowed| {
any_attempts = true;
if allowed.contains(CredentialType::USERNAME) {
ssh_username_requested = true;
Err(GitError::from_str("username to be tried later"))
} else if allowed.contains(CredentialType::SSH_KEY) && !tried_ssh_key {
tried_ssh_key = true;
let username = username.unwrap();
ssh_agent_attempts.push(username.to_string());
GitCred::ssh_key_from_agent(username)
} else if allowed.contains(CredentialType::USER_PASS_PLAINTEXT) && cred_helper_bad.is_none() {
let ret = GitCred::credential_helper(&cfg, url, username);
cred_helper_bad = Some(ret.is_err());
ret
} else if allowed.contains(CredentialType::DEFAULT) {
GitCred::default()
} else {
Err(GitError::from_str("no authentication available"))
}
});
if ssh_username_requested {
for uname in cred_helper.username
.into_iter()
.chain(cfg.get_string("user.name"))
.chain(["USERNAME", "USER"].iter().flat_map(env::var))
.chain(Some("git".to_string())) {
let mut ssh_attempts = 0;
res = f(&mut |_, _, allowed| {
if allowed.contains(CredentialType::USERNAME) {
return GitCred::username(&uname);
} else if allowed.contains(CredentialType::SSH_KEY) {
ssh_attempts += 1;
if ssh_attempts == 1 {
ssh_agent_attempts.push(uname.to_string());
return GitCred::ssh_key_from_agent(&uname);
}
}
Err(GitError::from_str("no authentication available"))
});
if ssh_attempts != 2 {
break;
}
}
}
if res.is_ok() || !any_attempts {
res
} else {
let err = res.err().map(|e| format!("{}: ", e)).unwrap_or_default();
let mut msg = format!("{}failed to authenticate when downloading repository {}", err, url);
if !ssh_agent_attempts.is_empty() {
msg.push_str(" (tried ssh-agent, but none of the following usernames worked: ");
for (i, uname) in ssh_agent_attempts.into_iter().enumerate() {
if i != 0 {
msg.push_str(", ");
}
msg.push('\"');
msg.push_str(&uname);
msg.push('\"');
}
msg.push(')');
}
if let Some(failed_cred_helper) = cred_helper_bad {
msg.push_str(" (tried to find username+password via ");
if failed_cred_helper {
msg.push_str("git's credential.helper support, but failed)");
} else {
msg.push_str("credential.helper, but found credentials were incorrect)");
}
}
Err(GitError::from_str(&msg))
}
}
pub fn split_package_path(cratename: &str) -> Vec<Cow<'_, str>> {
let mut elems = Vec::new();
if cratename.is_empty() {
panic!("0-length cratename");
}
if cratename.len() <= 3 {
elems.push(["1", "2", "3"][cratename.len() - 1].into())
}
match cratename.len() {
1 | 2 => {}
3 => elems.push(lcase(&cratename[0..1])),
_ => {
elems.push(lcase(&cratename[0..2]));
elems.push(lcase(&cratename[2..4]));
}
}
elems.push(lcase(cratename));
elems
}
fn lcase(s: &str) -> Cow<'_, str> {
if s.bytes().any(|b| b.is_ascii_uppercase()) {
s.to_ascii_lowercase().into()
} else {
s.into()
}
}
pub fn find_package_data<'t>(cratename: &str, registry: &Tree<'t>, registry_parent: &'t Repository) -> Option<Blob<'t>> {
let elems = split_package_path(cratename);
let ent = registry.get_name(&elems[0])?;
let obj = ent.to_object(registry_parent).ok()?;
let ent = obj.as_tree()?.get_name(&elems[1])?;
let obj = ent.to_object(registry_parent).ok()?;
let obj = if elems.len() == 3 {
let ent = obj.as_tree()?.get_name(&elems[2])?;
ent.to_object(registry_parent).ok()?
} else {
obj
};
obj.into_blob().ok()
}
pub fn find_proxy(crates_file: &Path) -> Option<String> {
if let Ok(crates_file) = fs::read_to_string(crates_file) {
if let Some(toml::Value::String(proxy)) =
toml::from_str::<toml::Value>(&crates_file)
.unwrap()
.get_mut("http")
.and_then(|t| t.as_table_mut())
.and_then(|t| t.remove("proxy")) {
if !proxy.is_empty() {
return Some(proxy);
}
}
}
if let Ok(cfg) = GitConfig::open_default() {
if let Ok(proxy) = cfg.get_string("http.proxy") {
if !proxy.is_empty() {
return Some(proxy);
}
}
}
["http_proxy", "HTTP_PROXY", "https_proxy", "HTTPS_PROXY"].iter().flat_map(env::var).filter(|proxy| !proxy.is_empty()).next()
}
pub fn find_git_db_repo(git_db_dir: &Path, url: &str) -> Option<PathBuf> {
let path = git_db_dir.join(format!("{}-{}",
match Url::parse(url)
.ok()?
.path_segments()
.and_then(|mut segs| segs.next_back())
.unwrap_or("") {
"" => "_empty",
url => url,
},
cargo_hash(url)));
if path.is_dir() { Some(path) } else { None }
}
pub fn registry_shortname(url: &str) -> String {
struct RegistryHash<'u>(&'u str);
impl<'u> Hash for RegistryHash<'u> {
fn hash<S: Hasher>(&self, hasher: &mut S) {
SourceKind::Registry.hash(hasher);
self.0.hash(hasher);
}
}
format!("{}-{}",
Url::parse(url).map_err(|e| format!("{} not an URL: {}", url, e)).unwrap().host_str().unwrap_or(""),
cargo_hash(RegistryHash(url)))
}
#[allow(deprecated)]
pub fn cargo_hash<T: Hash>(whom: T) -> String {
use std::hash::SipHasher;
let mut hasher = SipHasher::new_with_keys(0, 0);
whom.hash(&mut hasher);
let hash = hasher.finish();
hex::encode(&[(hash >> 0) as u8,
(hash >> 8) as u8,
(hash >> 16) as u8,
(hash >> 24) as u8,
(hash >> 32) as u8,
(hash >> 40) as u8,
(hash >> 48) as u8,
(hash >> 56) as u8])
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[allow(unused)]
enum SourceKind {
Git(GitReference),
Path,
Registry,
LocalRegistry,
Directory,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[allow(unused)]
enum GitReference {
Tag(String),
Branch(String),
Rev(String),
}
trait SemverExt {
fn is_prerelease(&self) -> bool;
}
impl SemverExt for Semver {
fn is_prerelease(&self) -> bool {
!self.pre.is_empty()
}
}