Skip to main content

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