| //! 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()); |
| } |
| } |