use crate::core::PackageId;
use crate::sources::DirectorySource;
use crate::sources::{GitSource, PathSource, RegistrySource, CRATES_IO_INDEX};
use crate::util::{CanonicalUrl, CargoResult, Config, IntoUrl};
use log::trace;
use serde::de;
use serde::ser;
use std::cmp::{self, Ordering};
use std::collections::HashSet;
use std::fmt::{self, Formatter};
use std::hash::{self, Hash};
use std::path::Path;
use std::ptr;
use std::sync::Mutex;
use url::Url;
lazy_static::lazy_static! {
static ref SOURCE_ID_CACHE: Mutex<HashSet<&'static SourceIdInner>> = Default::default();
}
#[derive(Clone, Copy, Eq, Debug)]
pub struct SourceId {
inner: &'static SourceIdInner,
}
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
struct SourceIdInner {
url: Url,
canonical_url: CanonicalUrl,
kind: SourceKind,
precise: Option<String>,
name: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum SourceKind {
Git(GitReference),
Path,
Registry,
LocalRegistry,
Directory,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum GitReference {
Tag(String),
Branch(String),
Rev(String),
DefaultBranch,
}
impl SourceId {
fn new(kind: SourceKind, url: Url) -> CargoResult<SourceId> {
let source_id = SourceId::wrap(SourceIdInner {
kind,
canonical_url: CanonicalUrl::new(&url)?,
url,
precise: None,
name: None,
});
Ok(source_id)
}
fn wrap(inner: SourceIdInner) -> SourceId {
let mut cache = SOURCE_ID_CACHE.lock().unwrap();
let inner = cache.get(&inner).cloned().unwrap_or_else(|| {
let inner = Box::leak(Box::new(inner));
cache.insert(inner);
inner
});
SourceId { inner }
}
pub fn from_url(string: &str) -> CargoResult<SourceId> {
let mut parts = string.splitn(2, '+');
let kind = parts.next().unwrap();
let url = parts
.next()
.ok_or_else(|| anyhow::format_err!("invalid source `{}`", string))?;
match kind {
"git" => {
let mut url = url.into_url()?;
let mut reference = GitReference::DefaultBranch;
for (k, v) in url.query_pairs() {
match &k[..] {
"branch" | "ref" => reference = GitReference::Branch(v.into_owned()),
"rev" => reference = GitReference::Rev(v.into_owned()),
"tag" => reference = GitReference::Tag(v.into_owned()),
_ => {}
}
}
let precise = url.fragment().map(|s| s.to_owned());
url.set_fragment(None);
url.set_query(None);
Ok(SourceId::for_git(&url, reference)?.with_precise(precise))
}
"registry" => {
let url = url.into_url()?;
Ok(SourceId::new(SourceKind::Registry, url)?
.with_precise(Some("locked".to_string())))
}
"path" => {
let url = url.into_url()?;
SourceId::new(SourceKind::Path, url)
}
kind => Err(anyhow::format_err!("unsupported source protocol: {}", kind)),
}
}
pub fn as_url(&self) -> SourceIdAsUrl<'_> {
SourceIdAsUrl {
inner: &*self.inner,
}
}
pub fn for_path(path: &Path) -> CargoResult<SourceId> {
let url = path.into_url()?;
SourceId::new(SourceKind::Path, url)
}
pub fn for_git(url: &Url, reference: GitReference) -> CargoResult<SourceId> {
SourceId::new(SourceKind::Git(reference), url.clone())
}
pub fn for_registry(url: &Url) -> CargoResult<SourceId> {
SourceId::new(SourceKind::Registry, url.clone())
}
pub fn for_local_registry(path: &Path) -> CargoResult<SourceId> {
let url = path.into_url()?;
SourceId::new(SourceKind::LocalRegistry, url)
}
pub fn for_directory(path: &Path) -> CargoResult<SourceId> {
let url = path.into_url()?;
SourceId::new(SourceKind::Directory, url)
}
pub fn crates_io(config: &Config) -> CargoResult<SourceId> {
config.crates_io_source_id(|| {
config.check_registry_index_not_set()?;
let url = CRATES_IO_INDEX.into_url().unwrap();
SourceId::for_registry(&url)
})
}
pub fn alt_registry(config: &Config, key: &str) -> CargoResult<SourceId> {
let url = config.get_registry_index(key)?;
Ok(SourceId::wrap(SourceIdInner {
kind: SourceKind::Registry,
canonical_url: CanonicalUrl::new(&url)?,
url,
precise: None,
name: Some(key.to_string()),
}))
}
pub fn url(&self) -> &Url {
&self.inner.url
}
pub fn canonical_url(&self) -> &CanonicalUrl {
&self.inner.canonical_url
}
pub fn display_index(self) -> String {
if self.is_default_registry() {
"crates.io index".to_string()
} else {
format!("`{}` index", url_display(self.url()))
}
}
pub fn display_registry_name(self) -> String {
if self.is_default_registry() {
"crates.io".to_string()
} else if let Some(name) = &self.inner.name {
name.clone()
} else {
url_display(self.url())
}
}
pub fn is_path(self) -> bool {
self.inner.kind == SourceKind::Path
}
pub fn is_registry(self) -> bool {
matches!(
self.inner.kind,
SourceKind::Registry | SourceKind::LocalRegistry
)
}
pub fn is_remote_registry(self) -> bool {
matches!(self.inner.kind, SourceKind::Registry)
}
pub fn is_git(self) -> bool {
matches!(self.inner.kind, SourceKind::Git(_))
}
pub fn load<'a>(
self,
config: &'a Config,
yanked_whitelist: &HashSet<PackageId>,
) -> CargoResult<Box<dyn super::Source + 'a>> {
trace!("loading SourceId; {}", self);
match self.inner.kind {
SourceKind::Git(..) => Ok(Box::new(GitSource::new(self, config)?)),
SourceKind::Path => {
let path = match self.inner.url.to_file_path() {
Ok(p) => p,
Err(()) => panic!("path sources cannot be remote"),
};
Ok(Box::new(PathSource::new(&path, self, config)))
}
SourceKind::Registry => Ok(Box::new(RegistrySource::remote(
self,
yanked_whitelist,
config,
))),
SourceKind::LocalRegistry => {
let path = match self.inner.url.to_file_path() {
Ok(p) => p,
Err(()) => panic!("path sources cannot be remote"),
};
Ok(Box::new(RegistrySource::local(
self,
&path,
yanked_whitelist,
config,
)))
}
SourceKind::Directory => {
let path = match self.inner.url.to_file_path() {
Ok(p) => p,
Err(()) => panic!("path sources cannot be remote"),
};
Ok(Box::new(DirectorySource::new(&path, self, config)))
}
}
}
pub fn precise(self) -> Option<&'static str> {
self.inner.precise.as_deref()
}
pub fn git_reference(self) -> Option<&'static GitReference> {
match self.inner.kind {
SourceKind::Git(ref s) => Some(s),
_ => None,
}
}
pub fn with_precise(self, v: Option<String>) -> SourceId {
SourceId::wrap(SourceIdInner {
precise: v,
..(*self.inner).clone()
})
}
pub fn is_default_registry(self) -> bool {
match self.inner.kind {
SourceKind::Registry => {}
_ => return false,
}
self.inner.url.as_str() == CRATES_IO_INDEX
}
pub fn stable_hash<S: hash::Hasher>(self, workspace: &Path, into: &mut S) {
if self.is_path() {
if let Ok(p) = self
.inner
.url
.to_file_path()
.unwrap()
.strip_prefix(workspace)
{
self.inner.kind.hash(into);
p.to_str().unwrap().hash(into);
return;
}
}
self.hash(into)
}
pub fn full_eq(self, other: SourceId) -> bool {
ptr::eq(self.inner, other.inner)
}
pub fn full_hash<S: hash::Hasher>(self, into: &mut S) {
ptr::NonNull::from(self.inner).hash(into)
}
}
impl PartialEq for SourceId {
fn eq(&self, other: &SourceId) -> bool {
self.cmp(other) == Ordering::Equal
}
}
impl PartialOrd for SourceId {
fn partial_cmp(&self, other: &SourceId) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for SourceId {
fn cmp(&self, other: &SourceId) -> Ordering {
if ptr::eq(self.inner, other.inner) {
return Ordering::Equal;
}
match (&self.inner.kind, &other.inner.kind) {
(SourceKind::Path, SourceKind::Path) => {}
(SourceKind::Path, _) => return Ordering::Less,
(_, SourceKind::Path) => return Ordering::Greater,
(SourceKind::Registry, SourceKind::Registry) => {}
(SourceKind::Registry, _) => return Ordering::Less,
(_, SourceKind::Registry) => return Ordering::Greater,
(SourceKind::LocalRegistry, SourceKind::LocalRegistry) => {}
(SourceKind::LocalRegistry, _) => return Ordering::Less,
(_, SourceKind::LocalRegistry) => return Ordering::Greater,
(SourceKind::Directory, SourceKind::Directory) => {}
(SourceKind::Directory, _) => return Ordering::Less,
(_, SourceKind::Directory) => return Ordering::Greater,
(SourceKind::Git(a), SourceKind::Git(b)) => {
use GitReference::*;
let ord = match (a, b) {
(Tag(a), Tag(b)) => a.cmp(b),
(Tag(_), _) => Ordering::Less,
(_, Tag(_)) => Ordering::Greater,
(Rev(a), Rev(b)) => a.cmp(b),
(Rev(_), _) => Ordering::Less,
(_, Rev(_)) => Ordering::Greater,
(Branch(a), DefaultBranch) => a.as_str().cmp("master"),
(DefaultBranch, Branch(b)) => "master".cmp(b),
(Branch(a), Branch(b)) => a.cmp(b),
(DefaultBranch, DefaultBranch) => Ordering::Equal,
};
if ord != Ordering::Equal {
return ord;
}
}
}
match (&self.inner.kind, &other.inner.kind) {
(SourceKind::Git(_), SourceKind::Git(_)) => {
self.inner.canonical_url.cmp(&other.inner.canonical_url)
}
_ => self.inner.url.cmp(&other.inner.url),
}
}
}
impl ser::Serialize for SourceId {
fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
where
S: ser::Serializer,
{
if self.is_path() {
None::<String>.serialize(s)
} else {
s.collect_str(&self.as_url())
}
}
}
impl<'de> de::Deserialize<'de> for SourceId {
fn deserialize<D>(d: D) -> Result<SourceId, D::Error>
where
D: de::Deserializer<'de>,
{
let string = String::deserialize(d)?;
SourceId::from_url(&string).map_err(de::Error::custom)
}
}
fn url_display(url: &Url) -> String {
if url.scheme() == "file" {
if let Ok(path) = url.to_file_path() {
if let Some(path_str) = path.to_str() {
return path_str.to_string();
}
}
}
url.as_str().to_string()
}
impl fmt::Display for SourceId {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self.inner.kind {
SourceKind::Git(ref reference) => {
write!(f, "{}", self.inner.url)?;
if let Some(pretty) = reference.pretty_ref() {
write!(f, "?{}", pretty)?;
}
if let Some(ref s) = self.inner.precise {
let len = cmp::min(s.len(), 8);
write!(f, "#{}", &s[..len])?;
}
Ok(())
}
SourceKind::Path => write!(f, "{}", url_display(&self.inner.url)),
SourceKind::Registry => write!(f, "registry `{}`", url_display(&self.inner.url)),
SourceKind::LocalRegistry => write!(f, "registry `{}`", url_display(&self.inner.url)),
SourceKind::Directory => write!(f, "dir {}", url_display(&self.inner.url)),
}
}
}
impl Hash for SourceId {
fn hash<S: hash::Hasher>(&self, into: &mut S) {
match &self.inner.kind {
SourceKind::Git(GitReference::Tag(a)) => {
0usize.hash(into);
0usize.hash(into);
a.hash(into);
}
SourceKind::Git(GitReference::Branch(a)) => {
0usize.hash(into);
1usize.hash(into);
a.hash(into);
}
SourceKind::Git(GitReference::DefaultBranch) => {
0usize.hash(into);
1usize.hash(into);
"master".hash(into);
}
SourceKind::Git(GitReference::Rev(a)) => {
0usize.hash(into);
2usize.hash(into);
a.hash(into);
}
SourceKind::Path => 1usize.hash(into),
SourceKind::Registry => 2usize.hash(into),
SourceKind::LocalRegistry => 3usize.hash(into),
SourceKind::Directory => 4usize.hash(into),
}
match self.inner.kind {
SourceKind::Git(_) => self.inner.canonical_url.hash(into),
_ => self.inner.url.as_str().hash(into),
}
}
}
pub struct SourceIdAsUrl<'a> {
inner: &'a SourceIdInner,
}
impl<'a> fmt::Display for SourceIdAsUrl<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self.inner {
SourceIdInner {
kind: SourceKind::Path,
ref url,
..
} => write!(f, "path+{}", url),
SourceIdInner {
kind: SourceKind::Git(ref reference),
ref url,
ref precise,
..
} => {
write!(f, "git+{}", url)?;
if let Some(pretty) = reference.pretty_ref() {
write!(f, "?{}", pretty)?;
}
if let Some(precise) = precise.as_ref() {
write!(f, "#{}", precise)?;
}
Ok(())
}
SourceIdInner {
kind: SourceKind::Registry,
ref url,
..
} => write!(f, "registry+{}", url),
SourceIdInner {
kind: SourceKind::LocalRegistry,
ref url,
..
} => write!(f, "local-registry+{}", url),
SourceIdInner {
kind: SourceKind::Directory,
ref url,
..
} => write!(f, "directory+{}", url),
}
}
}
impl GitReference {
pub fn pretty_ref(&self) -> Option<PrettyRef<'_>> {
match self {
GitReference::DefaultBranch => None,
_ => Some(PrettyRef { inner: self }),
}
}
}
pub struct PrettyRef<'a> {
inner: &'a GitReference,
}
impl<'a> fmt::Display for PrettyRef<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self.inner {
GitReference::Branch(ref b) => write!(f, "branch={}", b),
GitReference::Tag(ref s) => write!(f, "tag={}", s),
GitReference::Rev(ref s) => write!(f, "rev={}", s),
GitReference::DefaultBranch => unreachable!(),
}
}
}
#[cfg(test)]
mod tests {
use super::{GitReference, SourceId, SourceKind};
use crate::util::IntoUrl;
#[test]
fn github_sources_equal() {
let loc = "https://github.com/foo/bar".into_url().unwrap();
let default = SourceKind::Git(GitReference::DefaultBranch);
let s1 = SourceId::new(default.clone(), loc).unwrap();
let loc = "git://github.com/foo/bar".into_url().unwrap();
let s2 = SourceId::new(default, loc.clone()).unwrap();
assert_eq!(s1, s2);
let foo = SourceKind::Git(GitReference::Branch("foo".to_string()));
let s3 = SourceId::new(foo, loc).unwrap();
assert_ne!(s1, s3);
}
}