use crate::error::{Error, Result};
use serde::{Deserialize, Serialize, de, ser};
use std::{
cmp::{Ord, Ordering},
fmt,
hash::Hash,
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/";
#[derive(Clone, Debug)]
pub struct SourceId {
url: Url,
kind: SourceKind,
precise: Option<String>,
name: Option<String>,
}
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::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(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: `{kind}` from `{string}`"
))),
}
}
#[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(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..]
}
pub(crate) fn as_url(&self, encoded: bool) -> SourceIdAsUrl<'_> {
SourceIdAsUrl { id: self, encoded }
}
}
impl Ord for SourceId {
fn cmp(&self, other: &Self) -> Ordering {
match self.url.cmp(&other.url) {
Ordering::Equal => {}
non_eq => return non_eq,
}
match self.name.cmp(&other.name) {
Ordering::Equal => {}
non_eq => return non_eq,
}
match (&self.kind, &other.kind) {
(SourceKind::Git(s), SourceKind::Git(o)) => (s, o),
(a, b) => return a.cmp(b),
};
if let (Some(s), Some(o)) = (&self.precise, &other.precise) {
return s.cmp(o);
}
Ordering::Equal
}
}
impl PartialOrd for SourceId {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Hash for SourceId {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.url.hash(state);
self.kind.hash(state);
self.precise.hash(state);
self.name.hash(state);
}
}
impl PartialEq for SourceId {
fn eq(&self, other: &Self) -> bool {
self.cmp(other) == Ordering::Equal
}
}
impl Eq for SourceId {}
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)
}
}
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 {
self.as_url(false).fmt(f)
}
}
impl Default for SourceId {
fn default() -> SourceId {
SourceId::for_registry(&CRATES_IO_INDEX.into_url().unwrap()).unwrap()
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
#[non_exhaustive]
pub enum SourceKind {
Git(GitReference),
Path,
Registry,
SparseRegistry,
LocalRegistry,
#[cfg(any(unix, windows))]
Directory,
}
pub(crate) struct SourceIdAsUrl<'a> {
id: &'a SourceId,
encoded: bool,
}
impl fmt::Display for SourceIdAsUrl<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.id {
SourceId {
kind: SourceKind::Path,
url,
..
} => write!(f, "path+{url}"),
SourceId {
kind: SourceKind::Git(reference),
url,
precise,
..
} => {
write!(f, "git+{url}")?;
if let Some(pretty) = reference.pretty_ref(self.encoded) {
write!(f, "?{pretty}")?;
}
if let Some(precise) = precise.as_ref() {
write!(f, "#{precise}")?;
}
Ok(())
}
SourceId {
kind: SourceKind::Registry,
url,
..
} => write!(f, "registry+{url}"),
SourceId {
kind: SourceKind::SparseRegistry,
url,
..
} => write!(f, "sparse+{url}"),
SourceId {
kind: SourceKind::LocalRegistry,
url,
..
} => write!(f, "local-registry+{url}"),
#[cfg(any(unix, windows))]
SourceId {
kind: SourceKind::Directory,
url,
..
} => write!(f, "directory+{url}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum GitReference {
DefaultBranch,
Tag(String),
Branch(String),
Rev(String),
}
impl GitReference {
pub fn pretty_ref(&self, url_encoded: bool) -> Option<PrettyRef<'_>> {
match self {
Self::DefaultBranch => None,
_ => Some(PrettyRef {
inner: self,
url_encoded,
}),
}
}
}
pub struct PrettyRef<'a> {
inner: &'a GitReference,
url_encoded: bool,
}
impl fmt::Display for PrettyRef<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let value: &str = match self.inner {
GitReference::DefaultBranch => return Ok(()),
GitReference::Branch(s) => {
write!(f, "branch=")?;
s
}
GitReference::Tag(s) => {
write!(f, "tag=")?;
s
}
GitReference::Rev(s) => {
write!(f, "rev=")?;
s
}
};
if self.url_encoded {
for value in url::form_urlencoded::byte_serialize(value.as_bytes()) {
write!(f, "{value}")?;
}
} else {
write!(f, "{value}")?;
}
Ok(())
}
}
trait IntoUrl {
fn into_url(self) -> Result<Url>;
}
impl IntoUrl for &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 IntoUrl for &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()
);
}
}