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