blob: cfd54ef0a8f27672a53a5db3fed925044afe6d89 [file] [log] [blame]
//! Helpers for initializing the remote and local disk location of an index
use crate::{Error, Path, PathBuf};
use std::borrow::Cow;
/// A remote index url
#[derive(Default, Debug)]
pub enum IndexUrl<'iu> {
/// The canonical crates.io HTTP sparse index.
///
/// See [`crate::CRATES_IO_HTTP_INDEX`]
#[default]
CratesIoSparse,
/// The canonical crates.io git index.
///
/// See [`crate::CRATES_IO_INDEX`]
CratesIoGit,
/// A non-crates.io index.
///
/// This variant uses the url to determine the index kind (sparse or git) by
/// inspecting the url's scheme. This is because sparse indices are required
/// to have the `sparse+` scheme modifier
NonCratesIo(Cow<'iu, str>),
/// A [local registry](crate::index::LocalRegistry)
Local(Cow<'iu, Path>),
}
impl<'iu> IndexUrl<'iu> {
/// Gets the url as a string
pub fn as_str(&'iu self) -> &'iu str {
match self {
Self::CratesIoSparse => crate::CRATES_IO_HTTP_INDEX,
Self::CratesIoGit => crate::CRATES_IO_INDEX,
Self::NonCratesIo(url) => url,
Self::Local(pb) => pb.as_str(),
}
}
/// Returns true if the url points to a sparse registry
pub fn is_sparse(&self) -> bool {
match self {
Self::CratesIoSparse => true,
Self::CratesIoGit | Self::Local(..) => false,
Self::NonCratesIo(url) => url.starts_with("sparse+http"),
}
}
/// Gets the [`IndexUrl`] for crates.io, depending on the local environment.
///
/// 1. Determines if the crates.io registry has been [replaced](https://doc.rust-lang.org/cargo/reference/source-replacement.html)
/// 2. Determines if the protocol was explicitly [configured](https://doc.rust-lang.org/cargo/reference/config.html#registriescrates-ioprotocol) by the user
/// 3. Otherwise, detects the version of cargo (see [`crate::utils::cargo_version`]), and uses that to determine the appropriate default
pub fn crates_io(
config_root: Option<PathBuf>,
cargo_home: Option<&Path>,
cargo_version: Option<&str>,
) -> Result<Self, Error> {
// If the crates.io registry has been replaced it doesn't matter what
// the protocol for it has been changed to
if let Some(replacement) = get_crates_io_replacement(config_root.clone(), cargo_home)? {
return Ok(replacement);
}
let sparse_index = match std::env::var("CARGO_REGISTRIES_CRATES_IO_PROTOCOL")
.ok()
.as_deref()
{
Some("sparse") => true,
Some("git") => false,
_ => {
let sparse_index = read_cargo_config(config_root, cargo_home, |config| {
config
.get("registries")
.and_then(|v| v.get("crates-io"))
.and_then(|v| v.get("protocol"))
.and_then(|v| v.as_str())
.and_then(|v| match v {
"sparse" => Some(true),
"git" => Some(false),
_ => None,
})
})?;
if let Some(si) = sparse_index {
si
} else {
let vers = match cargo_version {
Some(v) => v.trim().parse()?,
None => crate::utils::cargo_version(None)?,
};
vers >= semver::Version::new(1, 70, 0)
}
}
};
Ok(if sparse_index {
Self::CratesIoSparse
} else {
Self::CratesIoGit
})
}
}
impl<'iu> From<&'iu str> for IndexUrl<'iu> {
#[inline]
fn from(s: &'iu str) -> Self {
Self::NonCratesIo(s.into())
}
}
/// The local disk location to place an index
#[derive(Default)]
pub enum IndexPath {
/// The default cargo home root path
#[default]
CargoHome,
/// User-specified root path
UserSpecified(PathBuf),
/// An exact path on disk where an index is located.
///
/// Unlike the other two variants, this variant won't take the index's url
/// into account to calculate the unique url hash as part of the full path
Exact(PathBuf),
}
impl From<Option<PathBuf>> for IndexPath {
/// Converts an optional path to a rooted path.
///
/// This never constructs a [`Self::Exact`], that can only be done explicitly
fn from(pb: Option<PathBuf>) -> Self {
if let Some(pb) = pb {
Self::UserSpecified(pb)
} else {
Self::CargoHome
}
}
}
/// Helper for constructing an index location, consisting of the remote url for
/// the index and the local location on disk
#[derive(Default)]
pub struct IndexLocation<'il> {
/// The remote url of the registry index
pub url: IndexUrl<'il>,
/// The local disk path of the index
pub root: IndexPath,
}
impl<'il> IndexLocation<'il> {
/// Constructs an index with the specified url located in the default cargo
/// home
pub fn new(url: IndexUrl<'il>) -> Self {
Self {
url,
root: IndexPath::CargoHome,
}
}
/// Changes the root location of the index on the local disk.
///
/// If not called, or set to [`None`], the default cargo home disk location
/// is used as the root
pub fn with_root(mut self, root: Option<PathBuf>) -> Self {
self.root = root.into();
self
}
/// Obtains the full local disk path and URL of this index location
pub fn into_parts(self) -> Result<(PathBuf, String), Error> {
let url = self.url.as_str();
let root = match self.root {
IndexPath::CargoHome => crate::utils::cargo_home()?,
IndexPath::UserSpecified(root) => root,
IndexPath::Exact(path) => return Ok((path, url.to_owned())),
};
let (path, mut url) = crate::utils::get_index_details(url, Some(root))?;
if !url.ends_with('/') {
url.push('/');
}
Ok((path, url))
}
}
/// Calls the specified function for each cargo config located according to
/// cargo's standard hierarchical structure
///
/// Note that this only supports the use of `.cargo/config.toml`, which is not
/// supported below cargo 1.39.0
///
/// See <https://doc.rust-lang.org/cargo/reference/config.html#hierarchical-structure>
pub(crate) fn read_cargo_config<T>(
root: Option<PathBuf>,
cargo_home: Option<&Path>,
callback: impl Fn(&toml::Value) -> Option<T>,
) -> Result<Option<T>, Error> {
if let Some(mut path) = root.or_else(|| {
std::env::current_dir()
.ok()
.and_then(|pb| PathBuf::from_path_buf(pb).ok())
}) {
loop {
path.push(".cargo/config.toml");
if path.exists() {
let contents = match std::fs::read_to_string(&path) {
Ok(c) => c,
Err(err) => return Err(Error::IoPath(err, path)),
};
let toml: toml::Value = toml::from_str(&contents)?;
if let Some(value) = callback(&toml) {
return Ok(Some(value));
}
}
path.pop();
path.pop();
// Walk up to the next potential config root
if !path.pop() {
break;
}
}
}
if let Some(home) = cargo_home
.map(Cow::Borrowed)
.or_else(|| crate::utils::cargo_home().ok().map(Cow::Owned))
{
let path = home.join("config.toml");
if path.exists() {
let toml: toml::Value =
toml::from_str(&std::fs::read_to_string(&path)?).map_err(Error::Toml)?;
if let Some(value) = callback(&toml) {
return Ok(Some(value));
}
}
}
Ok(None)
}
/// Gets the url of a replacement registry for crates.io if one has been configured
///
/// See <https://doc.rust-lang.org/cargo/reference/source-replacement.html>
#[inline]
pub(crate) fn get_crates_io_replacement<'iu>(
root: Option<PathBuf>,
cargo_home: Option<&Path>,
) -> Result<Option<IndexUrl<'iu>>, Error> {
read_cargo_config(root, cargo_home, |config| {
config.get("source").and_then(|sources| {
sources
.get("crates-io")
.and_then(|v| v.get("replace-with"))
.and_then(|v| v.as_str())
.and_then(|v| sources.get(v))
.and_then(|v| {
v.get("registry")
.and_then(|reg| {
reg.as_str()
.map(|r| IndexUrl::NonCratesIo(r.to_owned().into()))
})
.or_else(|| {
v.get("local-registry").and_then(|l| {
l.as_str().map(|l| IndexUrl::Local(PathBuf::from(l).into()))
})
})
})
})
})
}
#[cfg(test)]
mod test {
// Current stable is 1.70.0
#[test]
fn opens_sparse() {
assert!(std::env::var_os("CARGO_REGISTRIES_CRATES_IO_PROTOCOL").is_none());
assert!(matches!(
crate::index::ComboIndexCache::new(super::IndexLocation::new(
super::IndexUrl::crates_io(None, None, None).unwrap()
))
.unwrap(),
crate::index::ComboIndexCache::Sparse(_)
));
}
}