blob: cf0b43d09d0786eb7baa0c00e8ce35f27580dafe [file] [log] [blame]
//! Package source identifiers.
//!
//! Adapted from Cargo's `source_id.rs`:
//!
//! <https://github.com/rust-lang/cargo/blob/master/src/cargo/core/source/source_id.rs>
//!
//! Copyright (c) 2014 The Rust Project Developers
//! Licensed under the same terms as the `cargo-lock` crate: Apache 2.0 + MIT
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;
/// Location of the crates.io index
pub const CRATES_IO_INDEX: &str = "https://github.com/rust-lang/crates.io-index";
/// Location of the crates.io sparse HTTP index
pub const CRATES_IO_SPARSE_INDEX: &str = "sparse+https://index.crates.io/";
/// Default branch name
pub const DEFAULT_BRANCH: &str = "master";
/// Unique identifier for a source of packages.
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub struct SourceId {
/// The source URL.
url: Url,
/// The source kind.
kind: SourceKind,
/// For example, the exact Git revision of the specified branch for a Git Source.
precise: Option<String>,
/// Name of the registry source for alternative registries
name: Option<String>,
}
/// The possible kinds of code source.
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
#[non_exhaustive]
pub enum SourceKind {
/// A git repository.
Git(GitReference),
/// A local path..
Path,
/// A remote registry.
Registry,
/// A sparse registry.
SparseRegistry,
/// A local filesystem-based registry.
LocalRegistry,
/// A directory-based registry.
#[cfg(any(unix, windows))]
Directory,
}
impl SourceId {
/// Creates a `SourceId` object from the kind and URL.
fn new(kind: SourceKind, url: Url) -> Result<Self> {
Ok(Self {
kind,
url,
precise: None,
name: None,
})
}
/// Parses a source URL and returns the corresponding ID.
///
/// ## Example
///
/// ```
/// use cargo_lock::SourceId;
/// SourceId::from_url("git+https://github.com/alexcrichton/\
/// libssh2-static-sys#80e71a3021618eb05\
/// 656c58fb7c5ef5f12bc747f");
/// ```
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[..] {
// Map older 'ref' to branch.
"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
))),
}
}
/// Creates a `SourceId` from a filesystem path.
///
/// `path`: an absolute path.
#[cfg(any(unix, windows))]
pub fn for_path(path: &Path) -> Result<Self> {
Self::new(SourceKind::Path, path.into_url()?)
}
/// Creates a `SourceId` from a Git reference.
pub fn for_git(url: &Url, reference: GitReference) -> Result<Self> {
Self::new(SourceKind::Git(reference), url.clone())
}
/// Creates a SourceId from a remote registry URL.
pub fn for_registry(url: &Url) -> Result<Self> {
Self::new(SourceKind::Registry, url.clone())
}
/// Creates a SourceId from a local registry path.
#[cfg(any(unix, windows))]
pub fn for_local_registry(path: &Path) -> Result<Self> {
Self::new(SourceKind::LocalRegistry, path.into_url()?)
}
/// Creates a `SourceId` from a directory path.
#[cfg(any(unix, windows))]
pub fn for_directory(path: &Path) -> Result<Self> {
Self::new(SourceKind::Directory, path.into_url()?)
}
/// Gets this source URL.
pub fn url(&self) -> &Url {
&self.url
}
/// Get the kind of source.
pub fn kind(&self) -> &SourceKind {
&self.kind
}
/// Human-friendly description of an index
pub fn display_index(&self) -> String {
if self.is_default_registry() {
"crates.io index".to_string()
} else {
format!("`{}` index", self.url())
}
}
/// Human-friendly description of a registry name
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()
}
}
/// Returns `true` if this source is from a filesystem path.
pub fn is_path(&self) -> bool {
self.kind == SourceKind::Path
}
/// Returns `true` if this source is from a registry (either local or not).
pub fn is_registry(&self) -> bool {
matches!(
self.kind,
SourceKind::Registry | SourceKind::SparseRegistry | SourceKind::LocalRegistry
)
}
/// Returns `true` if this source is a "remote" registry.
///
/// "remote" may also mean a file URL to a git index, so it is not
/// necessarily "remote". This just means it is not `local-registry`.
pub fn is_remote_registry(&self) -> bool {
matches!(self.kind, SourceKind::Registry | SourceKind::SparseRegistry)
}
/// Returns `true` if this source from a Git repository.
pub fn is_git(&self) -> bool {
matches!(self.kind, SourceKind::Git(_))
}
/// Gets the value of the precise field.
pub fn precise(&self) -> Option<&str> {
self.precise.as_ref().map(AsRef::as_ref)
}
/// Gets the Git reference if this is a git source, otherwise `None`.
pub fn git_reference(&self) -> Option<&GitReference> {
if let SourceKind::Git(ref s) = self.kind {
Some(s)
} else {
None
}
}
/// Creates a new `SourceId` from this source with the given `precise`.
pub fn with_precise(&self, v: Option<String>) -> Self {
Self {
precise: v,
..self.clone()
}
}
/// Returns `true` if the remote registry is the standard <https://crates.io>.
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)
}
}
/// Information to find a specific commit in a Git repository.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum GitReference {
/// From a tag.
Tag(String),
/// From the HEAD of a branch.
Branch(String),
/// From a specific revision.
Rev(String),
}
impl GitReference {
/// Returns a `Display`able view of this git reference, or None if using
/// the head of the default branch
pub fn pretty_ref(&self) -> Option<PrettyRef<'_>> {
match *self {
GitReference::Branch(ref s) if *s == DEFAULT_BRANCH => None,
_ => Some(PrettyRef { inner: self }),
}
}
}
/// A git reference that can be `Display`ed
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),
}
}
}
/// A type that can be converted to a Url
trait IntoUrl {
/// Performs the conversion
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());
}
}