blob: 2d004bb1a6fe7a859f0e1e0c7aadec4a25a5c078 [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| {
match config
.pointer("/registries/crates-io/protocol")
.and_then(|p| p.as_str())?
{
"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_span::value::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_span::parse(&contents).map_err(Box::new)?;
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 fc = std::fs::read_to_string(&path)?;
let toml = toml_span::parse(&fc).map_err(Box::new)?;
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| {
let repw = config.pointer("/source/crates-io/replace-with")?.as_str()?;
let sources = config.pointer("/source")?.as_table()?;
let replace_src = sources.get(&repw.into())?.as_table()?;
if let Some(rr) = replace_src.get(&"registry".into()) {
rr.as_str()
.map(|r| IndexUrl::NonCratesIo(r.to_owned().into()))
} else if let Some(rlr) = replace_src.get(&"local-registry".into()) {
rlr.as_str()
.map(|l| IndexUrl::Local(PathBuf::from(l).into()))
} else {
None
}
})
}
#[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(_)
));
}
/// Verifies we can parse .cargo/config.toml files to either use the crates-io
/// protocol set, or source replacements
#[test]
fn parses_from_file() {
assert!(std::env::var_os("CARGO_REGISTRIES_CRATES_IO_PROTOCOL").is_none());
let td = tempfile::tempdir().unwrap();
let root = crate::PathBuf::from_path_buf(td.path().to_owned()).unwrap();
let cfg_toml = td.path().join(".cargo/config.toml");
std::fs::create_dir_all(cfg_toml.parent().unwrap()).unwrap();
const GIT: &str = r#"[registries.crates-io]
protocol = "git"
"#;
// First just set the protocol from the sparse default to git
std::fs::write(&cfg_toml, GIT).unwrap();
let iurl = super::IndexUrl::crates_io(Some(root.clone()), None, None).unwrap();
assert_eq!(iurl.as_str(), crate::CRATES_IO_INDEX);
assert!(!iurl.is_sparse());
// Next set replacement registries
for (i, (kind, url)) in [
(
"registry",
"sparse+https://sparse-registry-parses-from-file.com",
),
("registry", "https://sparse-registry-parses-from-file.git"),
("local-registry", root.as_str()),
]
.iter()
.enumerate()
{
std::fs::write(&cfg_toml, format!("{GIT}\n[source.crates-io]\nreplace-with = 'replacement'\n[source.replacement]\n{kind} = '{url}'")).unwrap();
let iurl = super::IndexUrl::crates_io(Some(root.clone()), None, None).unwrap();
assert_eq!(i == 0, iurl.is_sparse());
assert_eq!(iurl.as_str(), *url);
}
}
}