use crate::error::{Error, Result};
use serde::{de, ser, Deserialize, Serialize};
use std::{fmt, str::FromStr};
use url::Url;
#[cfg(any(unix, windows))]
use std::path::Path;
pub const CRATES_IO_INDEX: &str = "https://github.com/rust-lang/crates.io-index";
pub const CRATES_IO_SPARSE_INDEX: &str = "sparse+https://index.crates.io/";
pub const DEFAULT_BRANCH: &str = "master";
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub struct SourceId {
url: Url,
kind: SourceKind,
precise: Option<String>,
name: Option<String>,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
#[non_exhaustive]
pub enum SourceKind {
Git(GitReference),
Path,
Registry,
SparseRegistry,
LocalRegistry,
#[cfg(any(unix, windows))]
Directory,
}
impl SourceId {
fn new(kind: SourceKind, url: Url) -> Result<Self> {
Ok(Self {
kind,
url,
precise: None,
name: None,
})
}
pub fn from_url(string: &str) -> Result<Self> {
let mut parts = string.splitn(2, '+');
let kind = parts.next().unwrap();
let url = parts
.next()
.ok_or_else(|| Error::Parse(format!("invalid source `{}`", string)))?;
match kind {
"git" => {
let mut url = url.into_url()?;
let mut reference = GitReference::Branch(DEFAULT_BRANCH.to_string());
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(Self::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())))
}
"sparse" => {
let url = url.into_url()?;
Ok(SourceId::new(SourceKind::SparseRegistry, url)?
.with_precise(Some("locked".to_string())))
}
"path" => Self::new(SourceKind::Path, url.into_url()?),
kind => Err(Error::Parse(format!(
"unsupported source protocol: `{}` from `{string}`",
kind
))),
}
}
#[cfg(any(unix, windows))]
pub fn for_path(path: &Path) -> Result<Self> {
Self::new(SourceKind::Path, path.into_url()?)
}
pub fn for_git(url: &Url, reference: GitReference) -> Result<Self> {
Self::new(SourceKind::Git(reference), url.clone())
}
pub fn for_registry(url: &Url) -> Result<Self> {
Self::new(SourceKind::Registry, url.clone())
}
#[cfg(any(unix, windows))]
pub fn for_local_registry(path: &Path) -> Result<Self> {
Self::new(SourceKind::LocalRegistry, path.into_url()?)
}
#[cfg(any(unix, windows))]
pub fn for_directory(path: &Path) -> Result<Self> {
Self::new(SourceKind::Directory, path.into_url()?)
}
pub fn url(&self) -> &Url {
&self.url
}
pub fn kind(&self) -> &SourceKind {
&self.kind
}
pub fn display_index(&self) -> String {
if self.is_default_registry() {
"crates.io index".to_string()
} else {
format!("`{}` index", self.url())
}
}
pub fn display_registry_name(&self) -> String {
if self.is_default_registry() {
"crates.io".to_string()
} else if let Some(name) = &self.name {
name.clone()
} else {
self.url().to_string()
}
}
pub fn is_path(&self) -> bool {
self.kind == SourceKind::Path
}
pub fn is_registry(&self) -> bool {
matches!(
self.kind,
SourceKind::Registry | SourceKind::SparseRegistry | SourceKind::LocalRegistry
)
}
pub fn is_remote_registry(&self) -> bool {
matches!(self.kind, SourceKind::Registry | SourceKind::SparseRegistry)
}
pub fn is_git(&self) -> bool {
matches!(self.kind, SourceKind::Git(_))
}
pub fn precise(&self) -> Option<&str> {
self.precise.as_ref().map(AsRef::as_ref)
}
pub fn git_reference(&self) -> Option<&GitReference> {
if let SourceKind::Git(ref s) = self.kind {
Some(s)
} else {
None
}
}
pub fn with_precise(&self, v: Option<String>) -> Self {
Self {
precise: v,
..self.clone()
}
}
pub fn is_default_registry(&self) -> bool {
self.kind == SourceKind::Registry && self.url.as_str() == CRATES_IO_INDEX
|| self.kind == SourceKind::SparseRegistry
&& self.url.as_str() == &CRATES_IO_SPARSE_INDEX[7..]
}
}
impl Default for SourceId {
fn default() -> SourceId {
SourceId::for_registry(&CRATES_IO_INDEX.into_url().unwrap()).unwrap()
}
}
impl FromStr for SourceId {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
Self::from_url(s)
}
}
impl fmt::Display for SourceId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SourceId {
kind: SourceKind::Path,
ref url,
..
} => write!(f, "path+{}", url),
SourceId {
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(())
}
SourceId {
kind: SourceKind::Registry,
ref url,
..
} => write!(f, "registry+{}", url),
SourceId {
kind: SourceKind::SparseRegistry,
ref url,
..
} => write!(f, "sparse+{}", url),
SourceId {
kind: SourceKind::LocalRegistry,
ref url,
..
} => write!(f, "local-registry+{}", url),
#[cfg(any(unix, windows))]
SourceId {
kind: SourceKind::Directory,
ref url,
..
} => write!(f, "directory+{}", url),
}
}
}
impl Serialize for SourceId {
fn serialize<S: ser::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
if self.is_path() {
None::<String>.serialize(s)
} else {
s.collect_str(&self.to_string())
}
}
}
impl<'de> Deserialize<'de> for SourceId {
fn deserialize<D: de::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
let string = String::deserialize(d)?;
SourceId::from_url(&string).map_err(de::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum GitReference {
Tag(String),
Branch(String),
Rev(String),
}
impl GitReference {
pub fn pretty_ref(&self) -> Option<PrettyRef<'_>> {
match *self {
GitReference::Branch(ref s) if *s == DEFAULT_BRANCH => 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),
}
}
}
trait IntoUrl {
fn into_url(self) -> Result<Url>;
}
impl<'a> IntoUrl for &'a str {
fn into_url(self) -> Result<Url> {
Url::parse(self).map_err(|s| Error::Parse(format!("invalid url `{}`: {}", self, s)))
}
}
#[cfg(any(unix, windows))]
impl<'a> IntoUrl for &'a Path {
fn into_url(self) -> Result<Url> {
Url::from_file_path(self)
.map_err(|_| Error::Parse(format!("invalid path url `{}`", self.display())))
}
}
#[cfg(test)]
mod tests {
use super::SourceId;
#[test]
fn identifies_crates_io() {
assert!(SourceId::default().is_default_registry());
assert!(SourceId::from_url(super::CRATES_IO_SPARSE_INDEX)
.expect("failed to parse sparse URL")
.is_default_registry());
}
}