cargo_lock/package/
source.rs

1//! Package source identifiers.
2//!
3//! Adapted from Cargo's `source_id.rs`:
4//!
5//! <https://github.com/rust-lang/cargo/blob/master/src/cargo/core/source/source_id.rs>
6//!
7//! Copyright (c) 2014 The Rust Project Developers
8//! Licensed under the same terms as the `cargo-lock` crate: Apache 2.0 + MIT
9
10use crate::error::{Error, Result};
11use serde::{de, ser, Deserialize, Serialize};
12use std::{fmt, str::FromStr};
13use url::Url;
14
15#[cfg(any(unix, windows))]
16use std::path::Path;
17
18/// Location of the crates.io index
19pub const CRATES_IO_INDEX: &str = "https://github.com/rust-lang/crates.io-index";
20/// Location of the crates.io sparse HTTP index
21pub const CRATES_IO_SPARSE_INDEX: &str = "sparse+https://index.crates.io/";
22
23/// Default branch name
24pub const DEFAULT_BRANCH: &str = "master";
25
26/// Unique identifier for a source of packages.
27#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
28pub struct SourceId {
29    /// The source URL.
30    url: Url,
31
32    /// The source kind.
33    kind: SourceKind,
34
35    /// For example, the exact Git revision of the specified branch for a Git Source.
36    precise: Option<String>,
37
38    /// Name of the registry source for alternative registries
39    name: Option<String>,
40}
41
42/// The possible kinds of code source.
43#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
44#[non_exhaustive]
45pub enum SourceKind {
46    /// A git repository.
47    Git(GitReference),
48
49    /// A local path..
50    Path,
51
52    /// A remote registry.
53    Registry,
54
55    /// A sparse registry.
56    SparseRegistry,
57
58    /// A local filesystem-based registry.
59    LocalRegistry,
60
61    /// A directory-based registry.
62    #[cfg(any(unix, windows))]
63    Directory,
64}
65
66impl SourceId {
67    /// Creates a `SourceId` object from the kind and URL.
68    fn new(kind: SourceKind, url: Url) -> Result<Self> {
69        Ok(Self {
70            kind,
71            url,
72            precise: None,
73            name: None,
74        })
75    }
76
77    /// SourceIds with git references used in package.source fields are subtly
78    /// different than those in parenthesized source URLs that appear in disambiguated
79    /// entries of package.dependencies: the former have the form ?rev=ABBREV#FULLHASH
80    /// whereas the latter have the form ?rev=FULLHASH. This method changes the former
81    /// into the latter, and is used in `impl From<&Package> for Dependency`.
82    pub fn normalize_git_source_for_dependency(&self) -> Self {
83        if let SourceKind::Git(GitReference::Rev(_abbrev)) = &self.kind {
84            if let Some(full) = &self.precise {
85                let mut url = self.url.clone();
86                url.set_fragment(None);
87                return Self {
88                    kind: SourceKind::Git(GitReference::Rev(full.clone())),
89                    precise: None,
90                    url,
91                    name: self.name.clone(),
92                };
93            }
94        } else if let SourceKind::Git(reference) = &self.kind {
95            if self.precise.is_some() {
96                return Self {
97                    kind: SourceKind::Git(reference.clone()),
98                    precise: None,
99                    url: self.url.clone(),
100                    name: self.name.clone(),
101                };
102            }
103        }
104        self.clone()
105    }
106
107    /// Parses a source URL and returns the corresponding ID.
108    ///
109    /// ## Example
110    ///
111    /// ```
112    /// use cargo_lock::SourceId;
113    /// SourceId::from_url("git+https://github.com/alexcrichton/\
114    ///                     libssh2-static-sys#80e71a3021618eb05\
115    ///                     656c58fb7c5ef5f12bc747f");
116    /// ```
117    pub fn from_url(string: &str) -> Result<Self> {
118        let mut parts = string.splitn(2, '+');
119        let kind = parts.next().unwrap();
120        let url = parts
121            .next()
122            .ok_or_else(|| Error::Parse(format!("invalid source `{string}`")))?;
123
124        match kind {
125            "git" => {
126                let mut url = url.into_url()?;
127                let mut reference = GitReference::Branch(DEFAULT_BRANCH.to_string());
128                for (k, v) in url.query_pairs() {
129                    match &k[..] {
130                        // Map older 'ref' to branch.
131                        "branch" | "ref" => reference = GitReference::Branch(v.into_owned()),
132
133                        "rev" => reference = GitReference::Rev(v.into_owned()),
134                        "tag" => reference = GitReference::Tag(v.into_owned()),
135                        _ => {}
136                    }
137                }
138                let precise = url.fragment().map(|s| s.to_owned());
139                url.set_fragment(None);
140                url.set_query(None);
141                Ok(Self::for_git(&url, reference)?.with_precise(precise))
142            }
143            "registry" => {
144                let url = url.into_url()?;
145                Ok(SourceId::new(SourceKind::Registry, url)?
146                    .with_precise(Some("locked".to_string())))
147            }
148            "sparse" => {
149                let url = url.into_url()?;
150                Ok(SourceId::new(SourceKind::SparseRegistry, url)?
151                    .with_precise(Some("locked".to_string())))
152            }
153            "path" => Self::new(SourceKind::Path, url.into_url()?),
154            kind => Err(Error::Parse(format!(
155                "unsupported source protocol: `{kind}` from `{string}`"
156            ))),
157        }
158    }
159
160    /// Creates a `SourceId` from a filesystem path.
161    ///
162    /// `path`: an absolute path.
163    #[cfg(any(unix, windows))]
164    pub fn for_path(path: &Path) -> Result<Self> {
165        Self::new(SourceKind::Path, path.into_url()?)
166    }
167
168    /// Creates a `SourceId` from a Git reference.
169    pub fn for_git(url: &Url, reference: GitReference) -> Result<Self> {
170        Self::new(SourceKind::Git(reference), url.clone())
171    }
172
173    /// Creates a SourceId from a remote registry URL.
174    pub fn for_registry(url: &Url) -> Result<Self> {
175        Self::new(SourceKind::Registry, url.clone())
176    }
177
178    /// Creates a SourceId from a local registry path.
179    #[cfg(any(unix, windows))]
180    pub fn for_local_registry(path: &Path) -> Result<Self> {
181        Self::new(SourceKind::LocalRegistry, path.into_url()?)
182    }
183
184    /// Creates a `SourceId` from a directory path.
185    #[cfg(any(unix, windows))]
186    pub fn for_directory(path: &Path) -> Result<Self> {
187        Self::new(SourceKind::Directory, path.into_url()?)
188    }
189
190    /// Gets this source URL.
191    pub fn url(&self) -> &Url {
192        &self.url
193    }
194
195    /// Get the kind of source.
196    pub fn kind(&self) -> &SourceKind {
197        &self.kind
198    }
199
200    /// Human-friendly description of an index
201    pub fn display_index(&self) -> String {
202        if self.is_default_registry() {
203            "crates.io index".to_string()
204        } else {
205            format!("`{}` index", self.url())
206        }
207    }
208
209    /// Human-friendly description of a registry name
210    pub fn display_registry_name(&self) -> String {
211        if self.is_default_registry() {
212            "crates.io".to_string()
213        } else if let Some(name) = &self.name {
214            name.clone()
215        } else {
216            self.url().to_string()
217        }
218    }
219
220    /// Returns `true` if this source is from a filesystem path.
221    pub fn is_path(&self) -> bool {
222        self.kind == SourceKind::Path
223    }
224
225    /// Returns `true` if this source is from a registry (either local or not).
226    pub fn is_registry(&self) -> bool {
227        matches!(
228            self.kind,
229            SourceKind::Registry | SourceKind::SparseRegistry | SourceKind::LocalRegistry
230        )
231    }
232
233    /// Returns `true` if this source is a "remote" registry.
234    ///
235    /// "remote" may also mean a file URL to a git index, so it is not
236    /// necessarily "remote". This just means it is not `local-registry`.
237    pub fn is_remote_registry(&self) -> bool {
238        matches!(self.kind, SourceKind::Registry | SourceKind::SparseRegistry)
239    }
240
241    /// Returns `true` if this source from a Git repository.
242    pub fn is_git(&self) -> bool {
243        matches!(self.kind, SourceKind::Git(_))
244    }
245
246    /// Gets the value of the precise field.
247    pub fn precise(&self) -> Option<&str> {
248        self.precise.as_ref().map(AsRef::as_ref)
249    }
250
251    /// Gets the Git reference if this is a git source, otherwise `None`.
252    pub fn git_reference(&self) -> Option<&GitReference> {
253        if let SourceKind::Git(ref s) = self.kind {
254            Some(s)
255        } else {
256            None
257        }
258    }
259
260    /// Creates a new `SourceId` from this source with the given `precise`.
261    pub fn with_precise(&self, v: Option<String>) -> Self {
262        Self {
263            precise: v,
264            ..self.clone()
265        }
266    }
267
268    /// Returns `true` if the remote registry is the standard <https://crates.io>.
269    pub fn is_default_registry(&self) -> bool {
270        self.kind == SourceKind::Registry && self.url.as_str() == CRATES_IO_INDEX
271            || self.kind == SourceKind::SparseRegistry
272                && self.url.as_str() == &CRATES_IO_SPARSE_INDEX[7..]
273    }
274
275    /// A view of the [`SourceId`] that can be `Display`ed as a URL.
276    pub(crate) fn as_url(&self, encoded: bool) -> SourceIdAsUrl<'_> {
277        SourceIdAsUrl { id: self, encoded }
278    }
279}
280
281impl Default for SourceId {
282    fn default() -> SourceId {
283        SourceId::for_registry(&CRATES_IO_INDEX.into_url().unwrap()).unwrap()
284    }
285}
286
287impl FromStr for SourceId {
288    type Err = Error;
289
290    fn from_str(s: &str) -> Result<Self> {
291        Self::from_url(s)
292    }
293}
294
295impl fmt::Display for SourceId {
296    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
297        self.as_url(false).fmt(f)
298    }
299}
300
301/// A `Display`able view into a `SourceId` that will write it as a url
302pub(crate) struct SourceIdAsUrl<'a> {
303    id: &'a SourceId,
304    encoded: bool,
305}
306
307impl<'a> fmt::Display for SourceIdAsUrl<'a> {
308    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
309        match self.id {
310            SourceId {
311                kind: SourceKind::Path,
312                ref url,
313                ..
314            } => write!(f, "path+{url}"),
315            SourceId {
316                kind: SourceKind::Git(ref reference),
317                ref url,
318                ref precise,
319                ..
320            } => {
321                write!(f, "git+{url}")?;
322                // TODO: set it to true when the default is lockfile v4,
323                if let Some(pretty) = reference.pretty_ref(self.encoded) {
324                    write!(f, "?{pretty}")?;
325                }
326                if let Some(precise) = precise.as_ref() {
327                    write!(f, "#{precise}")?;
328                }
329                Ok(())
330            }
331            SourceId {
332                kind: SourceKind::Registry,
333                ref url,
334                ..
335            } => write!(f, "registry+{url}"),
336            SourceId {
337                kind: SourceKind::SparseRegistry,
338                ref url,
339                ..
340            } => write!(f, "sparse+{url}"),
341            SourceId {
342                kind: SourceKind::LocalRegistry,
343                ref url,
344                ..
345            } => write!(f, "local-registry+{url}"),
346            #[cfg(any(unix, windows))]
347            SourceId {
348                kind: SourceKind::Directory,
349                ref url,
350                ..
351            } => write!(f, "directory+{url}"),
352        }
353    }
354}
355
356impl Serialize for SourceId {
357    fn serialize<S: ser::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
358        if self.is_path() {
359            None::<String>.serialize(s)
360        } else {
361            s.collect_str(&self.to_string())
362        }
363    }
364}
365
366impl<'de> Deserialize<'de> for SourceId {
367    fn deserialize<D: de::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
368        let string = String::deserialize(d)?;
369        SourceId::from_url(&string).map_err(de::Error::custom)
370    }
371}
372
373/// Information to find a specific commit in a Git repository.
374#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
375pub enum GitReference {
376    /// From a tag.
377    Tag(String),
378
379    /// From the HEAD of a branch.
380    Branch(String),
381
382    /// From a specific revision.
383    Rev(String),
384}
385
386impl GitReference {
387    /// Returns a `Display`able view of this git reference, or None if using
388    /// the head of the default branch
389    pub fn pretty_ref(&self, url_encoded: bool) -> Option<PrettyRef<'_>> {
390        match self {
391            GitReference::Branch(ref s) if *s == DEFAULT_BRANCH => None,
392            _ => Some(PrettyRef {
393                inner: self,
394                url_encoded,
395            }),
396        }
397    }
398}
399
400/// A git reference that can be `Display`ed
401pub struct PrettyRef<'a> {
402    inner: &'a GitReference,
403    url_encoded: bool,
404}
405
406impl<'a> fmt::Display for PrettyRef<'a> {
407    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
408        let value: &str = match self.inner {
409            GitReference::Branch(s) => {
410                write!(f, "branch=")?;
411                s
412            }
413            GitReference::Tag(s) => {
414                write!(f, "tag=")?;
415                s
416            }
417            GitReference::Rev(s) => {
418                write!(f, "rev=")?;
419                s
420            }
421        };
422        if self.url_encoded {
423            for value in url::form_urlencoded::byte_serialize(value.as_bytes()) {
424                write!(f, "{value}")?;
425            }
426        } else {
427            write!(f, "{value}")?;
428        }
429        Ok(())
430    }
431}
432
433/// A type that can be converted to a Url
434trait IntoUrl {
435    /// Performs the conversion
436    fn into_url(self) -> Result<Url>;
437}
438
439impl<'a> IntoUrl for &'a str {
440    fn into_url(self) -> Result<Url> {
441        Url::parse(self).map_err(|s| Error::Parse(format!("invalid url `{self}`: {s}")))
442    }
443}
444
445#[cfg(any(unix, windows))]
446impl<'a> IntoUrl for &'a Path {
447    fn into_url(self) -> Result<Url> {
448        Url::from_file_path(self)
449            .map_err(|_| Error::Parse(format!("invalid path url `{}`", self.display())))
450    }
451}
452
453#[cfg(test)]
454mod tests {
455    use super::SourceId;
456
457    #[test]
458    fn identifies_crates_io() {
459        assert!(SourceId::default().is_default_registry());
460        assert!(SourceId::from_url(super::CRATES_IO_SPARSE_INDEX)
461            .expect("failed to parse sparse URL")
462            .is_default_registry());
463    }
464}