Skip to main content

cargo/workspace/
source_id.rs

1use crate::context;
2use crate::sources::registry::CRATES_IO_HTTP_INDEX;
3use crate::sources::source::Source;
4use crate::sources::{CRATES_IO_DOMAIN, CRATES_IO_INDEX, CRATES_IO_REGISTRY, DirectorySource};
5use crate::sources::{GitSource, PathSource, RegistrySource};
6use crate::util::interning::InternedString;
7use crate::util::{CanonicalUrl, CargoResult, GlobalContext, IntoUrl};
8use crate::workspace::GitReference;
9use crate::workspace::SourceKind;
10use anyhow::Context as _;
11use cargo_util::collections::HashSet;
12use serde::de;
13use serde::ser;
14use std::cmp::{self, Ordering};
15use std::fmt::{self, Formatter};
16use std::hash::{self, Hash};
17use std::path::{Path, PathBuf};
18use std::ptr;
19use std::sync::Mutex;
20use std::sync::OnceLock;
21use tracing::trace;
22use url::Url;
23
24static SOURCE_ID_CACHE: OnceLock<Mutex<HashSet<&'static SourceIdInner>>> = OnceLock::new();
25
26/// Unique identifier for a source of packages.
27///
28/// Cargo uniquely identifies packages using [`PackageId`], a combination of the
29/// package name, version, and the code source. `SourceId` exactly represents
30/// the "code source" in `PackageId`. See [`SourceId::hash`] to learn what are
31/// taken into account for the uniqueness of a source.
32///
33/// `SourceId` is usually associated with an instance of [`Source`], which is
34/// supposed to provide a `SourceId` via [`Source::source_id`] method.
35///
36/// [`Source`]: crate::sources::source::Source
37/// [`Source::source_id`]: crate::sources::source::Source::source_id
38/// [`PackageId`]: super::PackageId
39#[derive(Clone, Copy, Eq, Debug)]
40pub struct SourceId {
41    inner: &'static SourceIdInner,
42}
43
44/// The interned version of [`SourceId`] to avoid excessive clones and borrows.
45/// Values are cached in `SOURCE_ID_CACHE` once created.
46#[derive(Eq, Clone, Debug)]
47struct SourceIdInner {
48    /// The source URL.
49    url: Url,
50    /// The canonical version of the above url. See [`CanonicalUrl`] to learn
51    /// why it is needed and how it normalizes a URL.
52    canonical_url: CanonicalUrl,
53    /// The source kind.
54    kind: SourceKind,
55    /// For example, the exact Git revision of the specified branch for a Git Source.
56    precise: Option<Precise>,
57    /// Name of the remote registry.
58    ///
59    /// WARNING: this is not always set when the name is not known,
60    /// e.g. registry coming from `--index` or Cargo.lock
61    registry_key: Option<KeyOf>,
62}
63
64#[derive(Eq, PartialEq, Clone, Debug, Hash)]
65enum Precise {
66    Locked,
67    Updated {
68        name: InternedString,
69        from: semver::Version,
70        to: semver::Version,
71    },
72    GitUrlFragment(String),
73}
74
75impl fmt::Display for Precise {
76    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
77        match self {
78            Precise::Locked => "locked".fmt(f),
79            Precise::Updated { name, from, to } => {
80                write!(f, "{name}={from}->{to}")
81            }
82            Precise::GitUrlFragment(s) => s.fmt(f),
83        }
84    }
85}
86
87/// Where the remote source key is defined.
88///
89/// The purpose of this is to provide better diagnostics for different sources of keys.
90#[derive(Debug, Clone, PartialEq, Eq)]
91enum KeyOf {
92    /// Defined in the `[registries]` table or the built-in `crates-io` key.
93    Registry(String),
94    /// Defined in the `[source]` replacement table.
95    Source(String),
96}
97
98impl SourceId {
99    /// Creates a `SourceId` object from the kind and URL.
100    ///
101    /// The canonical url will be calculated, but the precise field will not
102    fn new(kind: SourceKind, url: Url, key: Option<KeyOf>) -> CargoResult<SourceId> {
103        if kind == SourceKind::SparseRegistry {
104            // Sparse URLs are different because they store the kind prefix (sparse+)
105            // in the URL. This is because the prefix is necessary to differentiate
106            // from regular registries (git-based). The sparse+ prefix is included
107            // everywhere, including user-facing locations such as the `config.toml`
108            // file that defines the registry, or whenever Cargo displays it to the user.
109            assert!(url.as_str().starts_with("sparse+"));
110        }
111        let source_id = SourceId::wrap(SourceIdInner {
112            kind,
113            canonical_url: CanonicalUrl::new(&url)?,
114            url,
115            precise: None,
116            registry_key: key,
117        });
118        Ok(source_id)
119    }
120
121    /// Interns the value and returns the wrapped type.
122    fn wrap(inner: SourceIdInner) -> SourceId {
123        let mut cache = SOURCE_ID_CACHE
124            .get_or_init(|| Default::default())
125            .lock()
126            .unwrap();
127        let inner = cache.get(&inner).cloned().unwrap_or_else(|| {
128            let inner = Box::leak(Box::new(inner));
129            cache.insert(inner);
130            inner
131        });
132        SourceId { inner }
133    }
134
135    fn remote_source_kind(url: &Url) -> SourceKind {
136        if url.as_str().starts_with("sparse+") {
137            SourceKind::SparseRegistry
138        } else {
139            SourceKind::Registry
140        }
141    }
142
143    /// Parses a source URL and returns the corresponding ID.
144    ///
145    /// ## Example
146    ///
147    /// ```
148    /// use cargo::workspace::SourceId;
149    /// SourceId::from_url("git+https://github.com/alexcrichton/\
150    ///                     libssh2-static-sys#80e71a3021618eb05\
151    ///                     656c58fb7c5ef5f12bc747f");
152    /// ```
153    pub fn from_url(string: &str) -> CargoResult<SourceId> {
154        let (kind, url) = string
155            .split_once('+')
156            .ok_or_else(|| anyhow::format_err!("invalid source `{}`", string))?;
157
158        match kind {
159            "git" => {
160                let mut url = url.into_url()?;
161                let reference = GitReference::from_query(url.query_pairs());
162                let precise = url.fragment().map(|s| s.to_owned());
163                url.set_fragment(None);
164                url.set_query(None);
165                Ok(SourceId::for_git(&url, reference)?.with_git_precise(precise))
166            }
167            "registry" => {
168                let url = url.into_url()?;
169                Ok(SourceId::new(SourceKind::Registry, url, None)?.with_locked_precise())
170            }
171            "sparse" => {
172                let url = string.into_url()?;
173                Ok(SourceId::new(SourceKind::SparseRegistry, url, None)?.with_locked_precise())
174            }
175            "path" => {
176                let url = url.into_url()?;
177                SourceId::new(SourceKind::Path, url, None)
178            }
179            "builtin" => SourceId::for_builtin(),
180            kind => Err(anyhow::format_err!("unsupported source protocol: {}", kind)),
181        }
182    }
183
184    /// A view of the [`SourceId`] that can be `Display`ed as a URL.
185    pub fn as_url(&self) -> SourceIdAsUrl<'_> {
186        SourceIdAsUrl {
187            inner: &*self.inner,
188            encoded: false,
189        }
190    }
191
192    /// Like [`Self::as_url`] but with URL parameters encoded.
193    pub fn as_encoded_url(&self) -> SourceIdAsUrl<'_> {
194        SourceIdAsUrl {
195            inner: &*self.inner,
196            encoded: true,
197        }
198    }
199
200    /// Creates a `SourceId` from a filesystem path.
201    ///
202    /// `path`: an absolute path.
203    pub fn for_path(path: &Path) -> CargoResult<SourceId> {
204        let url = path.into_url()?;
205        SourceId::new(SourceKind::Path, url, None)
206    }
207
208    /// Creates a `SourceId` for the builtin packages in the configured toolchain.
209    pub fn for_builtin() -> CargoResult<SourceId> {
210        let url = "builtin://.".into_url()?;
211        SourceId::new(SourceKind::Builtin, url, None)
212    }
213
214    /// Creates a `SourceId` from a filesystem path.
215    ///
216    /// `path`: an absolute path.
217    pub fn for_manifest_path(manifest_path: &Path) -> CargoResult<SourceId> {
218        if crate::workspace::parser::is_embedded(manifest_path) && manifest_path.is_file() {
219            Self::for_path(manifest_path)
220        } else {
221            Self::for_path(manifest_path.parent().unwrap())
222        }
223    }
224
225    /// Creates a `SourceId` from a Git reference.
226    pub fn for_git(url: &Url, reference: GitReference) -> CargoResult<SourceId> {
227        SourceId::new(SourceKind::Git(reference), url.clone(), None)
228    }
229
230    /// Creates a `SourceId` from a remote registry URL when the registry name
231    /// cannot be determined, e.g. a user passes `--index` directly from CLI.
232    ///
233    /// Use [`SourceId::for_alt_registry`] if a name can provided, which
234    /// generates better messages for cargo.
235    pub fn for_registry(url: &Url) -> CargoResult<SourceId> {
236        let kind = Self::remote_source_kind(url);
237        SourceId::new(kind, url.to_owned(), None)
238    }
239
240    /// Creates a `SourceId` for a remote registry from the `[registries]` table or crates.io.
241    pub fn for_alt_registry(url: &Url, key: &str) -> CargoResult<SourceId> {
242        let kind = Self::remote_source_kind(url);
243        let key = KeyOf::Registry(key.into());
244        SourceId::new(kind, url.to_owned(), Some(key))
245    }
246
247    /// Creates a `SourceId` for a remote registry from the `[source]` replacement table.
248    pub fn for_source_replacement_registry(url: &Url, key: &str) -> CargoResult<SourceId> {
249        let kind = Self::remote_source_kind(url);
250        let key = KeyOf::Source(key.into());
251        SourceId::new(kind, url.to_owned(), Some(key))
252    }
253
254    /// Creates a `SourceId` from a local registry path.
255    pub fn for_local_registry(path: &Path) -> CargoResult<SourceId> {
256        let url = path.into_url()?;
257        SourceId::new(SourceKind::LocalRegistry, url, None)
258    }
259
260    /// Creates a `SourceId` from a directory path.
261    pub fn for_directory(path: &Path) -> CargoResult<SourceId> {
262        let url = path.into_url()?;
263        SourceId::new(SourceKind::Directory, url, None)
264    }
265
266    /// Returns the `SourceId` corresponding to the main repository.
267    ///
268    /// This is the main cargo registry by default, but it can be overridden in
269    /// a `.cargo/config.toml`.
270    pub fn crates_io(gctx: &GlobalContext) -> CargoResult<SourceId> {
271        gctx.crates_io_source_id()
272    }
273
274    /// Returns the `SourceId` corresponding to the main repository, using the
275    /// sparse HTTP index if allowed.
276    pub fn crates_io_maybe_sparse_http(gctx: &GlobalContext) -> CargoResult<SourceId> {
277        if Self::crates_io_is_sparse(gctx)? {
278            gctx.check_registry_index_not_set()?;
279            let url = CRATES_IO_HTTP_INDEX.into_url().unwrap();
280            let key = KeyOf::Registry(CRATES_IO_REGISTRY.into());
281            SourceId::new(SourceKind::SparseRegistry, url, Some(key))
282        } else {
283            Self::crates_io(gctx)
284        }
285    }
286
287    /// Returns whether to access crates.io over the sparse protocol.
288    pub fn crates_io_is_sparse(gctx: &GlobalContext) -> CargoResult<bool> {
289        let proto: Option<context::Value<String>> =
290            gctx.get(["registries", "crates-io", "protocol"])?;
291        let is_sparse = match proto.as_ref().map(|v| v.val.as_str()) {
292            Some("sparse") => true,
293            Some("git") => false,
294            Some(unknown) => anyhow::bail!(
295                "unsupported registry protocol `{unknown}` (defined in {})",
296                proto.as_ref().unwrap().definition
297            ),
298            None => true,
299        };
300        Ok(is_sparse)
301    }
302
303    /// Gets the `SourceId` associated with given name of the remote registry.
304    pub fn alt_registry(gctx: &GlobalContext, key: &str) -> CargoResult<SourceId> {
305        if key == CRATES_IO_REGISTRY {
306            return Self::crates_io(gctx);
307        }
308        let url = gctx.get_registry_index(key)?;
309        Self::for_alt_registry(&url, key)
310    }
311
312    /// Gets this source URL.
313    pub fn url(&self) -> &Url {
314        &self.inner.url
315    }
316
317    /// Gets the canonical URL of this source, used for internal comparison
318    /// purposes.
319    pub fn canonical_url(&self) -> &CanonicalUrl {
320        &self.inner.canonical_url
321    }
322
323    /// Displays the text "crates.io index" for Cargo shell status output.
324    pub fn display_index(self) -> String {
325        if self.is_crates_io() {
326            format!("{} index", CRATES_IO_DOMAIN)
327        } else {
328            format!("`{}` index", self.display_registry_name())
329        }
330    }
331
332    /// Displays the name of a registry if it has one. Otherwise just the URL.
333    pub fn display_registry_name(self) -> String {
334        if let Some(key) = self.inner.registry_key.as_ref().map(|k| k.key()) {
335            key.into()
336        } else if self.has_precise() {
337            // We remove `precise` here to retrieve an permissive version of
338            // `SourceIdInner`, which may contain the registry name.
339            self.without_precise().display_registry_name()
340        } else {
341            url_display(self.url())
342        }
343    }
344
345    /// Gets the name of the remote registry as defined in the `[registries]` table,
346    /// or the built-in `crates-io` key.
347    pub fn alt_registry_key(&self) -> Option<&str> {
348        self.inner.registry_key.as_ref()?.alternative_registry()
349    }
350
351    /// Returns `true` if this source is from a filesystem path.
352    pub fn is_path(self) -> bool {
353        self.inner.kind == SourceKind::Path
354    }
355
356    /// Returns the local path if this is a path dependency.
357    pub fn local_path(self) -> Option<PathBuf> {
358        if self.inner.kind != SourceKind::Path {
359            return None;
360        }
361
362        Some(self.inner.url.to_file_path().unwrap())
363    }
364
365    pub fn kind(&self) -> &SourceKind {
366        &self.inner.kind
367    }
368
369    /// Returns `true` if this source is from a registry (either local or not).
370    pub fn is_registry(self) -> bool {
371        matches!(
372            self.inner.kind,
373            SourceKind::Registry | SourceKind::SparseRegistry | SourceKind::LocalRegistry
374        )
375    }
376
377    /// Returns `true` if this source is from a sparse registry.
378    pub fn is_sparse(self) -> bool {
379        matches!(self.inner.kind, SourceKind::SparseRegistry)
380    }
381
382    /// Returns `true` if this source is a "remote" registry.
383    ///
384    /// "remote" may also mean a file URL to a git index, so it is not
385    /// necessarily "remote". This just means it is not `local-registry`.
386    pub fn is_remote_registry(self) -> bool {
387        matches!(
388            self.inner.kind,
389            SourceKind::Registry | SourceKind::SparseRegistry
390        )
391    }
392
393    /// Returns `true` if this source from a Git repository.
394    pub fn is_git(self) -> bool {
395        matches!(self.inner.kind, SourceKind::Git(_))
396    }
397
398    /// Creates an implementation of `Source` corresponding to this ID.
399    pub fn load<'a>(self, gctx: &'a GlobalContext) -> CargoResult<Box<dyn Source + 'a>> {
400        trace!("loading SourceId; {}", self);
401        match self.inner.kind {
402            SourceKind::Git(..) => Ok(Box::new(GitSource::new(self, gctx)?)),
403            SourceKind::Path => {
404                let path = self
405                    .inner
406                    .url
407                    .to_file_path()
408                    .expect("path sources cannot be remote");
409                if crate::workspace::parser::is_embedded(&path) && path.is_file() {
410                    anyhow::bail!("single file packages cannot be used as dependencies")
411                }
412                Ok(Box::new(PathSource::new(&path, self, gctx)))
413            }
414            SourceKind::Builtin => todo!("builtin source"),
415            SourceKind::Registry | SourceKind::SparseRegistry => {
416                Ok(Box::new(RegistrySource::remote(self, gctx)?))
417            }
418            SourceKind::LocalRegistry => {
419                let path = self
420                    .inner
421                    .url
422                    .to_file_path()
423                    .expect("path sources cannot be remote");
424                Ok(Box::new(RegistrySource::local(self, &path, gctx)))
425            }
426            SourceKind::Directory => {
427                let path = self
428                    .inner
429                    .url
430                    .to_file_path()
431                    .expect("path sources cannot be remote");
432                Ok(Box::new(DirectorySource::new(&path, self, gctx)))
433            }
434        }
435    }
436
437    /// Gets the Git reference if this is a git source, otherwise `None`.
438    pub fn git_reference(self) -> Option<&'static GitReference> {
439        match self.inner.kind {
440            SourceKind::Git(ref s) => Some(s),
441            _ => None,
442        }
443    }
444
445    /// Check if the precise data field has bean set
446    pub fn has_precise(self) -> bool {
447        self.inner.precise.is_some()
448    }
449
450    /// Check if the precise data field has bean set to "locked"
451    pub fn has_locked_precise(self) -> bool {
452        self.inner.precise == Some(Precise::Locked)
453    }
454
455    /// Check if two sources have the same precise data field
456    pub fn has_same_precise_as(self, other: Self) -> bool {
457        self.inner.precise == other.inner.precise
458    }
459
460    /// Check if the precise data field stores information for this `name`
461    /// from a call to [`SourceId::with_precise_registry_version`].
462    ///
463    /// If so return the version currently in the lock file and the version to be updated to.
464    pub fn precise_registry_version(
465        self,
466        pkg: &str,
467    ) -> Option<(&semver::Version, &semver::Version)> {
468        match &self.inner.precise {
469            Some(Precise::Updated { name, from, to }) if name == pkg => Some((from, to)),
470            _ => None,
471        }
472    }
473
474    pub fn precise_git_fragment(self) -> Option<&'static str> {
475        match &self.inner.precise {
476            Some(Precise::GitUrlFragment(s)) => Some(&s),
477            _ => None,
478        }
479    }
480
481    /// Creates a new `SourceId` from this source with the given `precise`.
482    pub fn with_git_precise(self, fragment: Option<String>) -> SourceId {
483        self.with_precise(&fragment.map(|f| Precise::GitUrlFragment(f)))
484    }
485
486    /// Creates a new `SourceId` from this source without a `precise`.
487    pub fn without_precise(self) -> SourceId {
488        self.with_precise(&None)
489    }
490
491    /// Creates a new `SourceId` from this source without a `precise`.
492    pub fn with_locked_precise(self) -> SourceId {
493        self.with_precise(&Some(Precise::Locked))
494    }
495
496    /// Creates a new `SourceId` from this source with the `precise` from some other `SourceId`.
497    pub fn with_precise_from(self, v: Self) -> SourceId {
498        self.with_precise(&v.inner.precise)
499    }
500
501    fn with_precise(self, precise: &Option<Precise>) -> SourceId {
502        if &self.inner.precise == precise {
503            self
504        } else {
505            SourceId::wrap(SourceIdInner {
506                precise: precise.clone(),
507                ..(*self.inner).clone()
508            })
509        }
510    }
511
512    /// When updating a lock file on a version using `cargo update --precise`
513    /// the requested version is stored in the precise field.
514    /// On a registry dependency we also need to keep track of the package that
515    /// should be updated and even which of the versions should be updated.
516    /// All of this gets encoded in the precise field using this method.
517    /// The data can be read with [`SourceId::precise_registry_version`]
518    pub fn with_precise_registry_version(
519        self,
520        name: InternedString,
521        version: semver::Version,
522        precise: &str,
523    ) -> CargoResult<SourceId> {
524        let precise = semver::Version::parse(precise).with_context(|| {
525            if let Some(stripped) = precise.strip_prefix("v") {
526                return format!(
527                    "the version provided, `{precise}` is not a \
528                    valid SemVer version\n\n\
529                    help: try changing the version to `{stripped}`",
530                );
531            }
532            format!("invalid version format for precise version `{precise}`")
533        })?;
534
535        Ok(SourceId::wrap(SourceIdInner {
536            precise: Some(Precise::Updated {
537                name,
538                from: version,
539                to: precise,
540            }),
541            ..(*self.inner).clone()
542        }))
543    }
544
545    /// Returns `true` if the remote registry is the standard <https://crates.io>.
546    pub fn is_crates_io(self) -> bool {
547        match self.inner.kind {
548            SourceKind::Registry | SourceKind::SparseRegistry => {}
549            _ => return false,
550        }
551        let url = self.inner.url.as_str();
552        url == CRATES_IO_INDEX || url == CRATES_IO_HTTP_INDEX || is_overridden_crates_io_url(url)
553    }
554
555    /// Hashes `self` to be used in the name of some Cargo folders, so shouldn't vary.
556    ///
557    /// For git and url, `as_str` gives the serialisation of a url (which has a spec) and so
558    /// insulates against possible changes in how the url crate does hashing.
559    ///
560    /// For path dependencies, we do these to help reproducible builds:
561    ///
562    /// * Remove the workspace prefix,
563    ///   so the same source will give the same hash in different locations.
564    /// * Paths outside the workspace are hashed as absolute paths,
565    ///   unless `trim_paths` is set,
566    ///   in which case they are not hashed at all.
567    pub fn stable_hash<S: hash::Hasher>(self, workspace: &Path, trim_paths: bool, into: &mut S) {
568        if self.is_path() {
569            if let Ok(p) = self
570                .inner
571                .url
572                .to_file_path()
573                .unwrap()
574                .strip_prefix(workspace)
575            {
576                self.inner.kind.hash(into);
577                p.to_str().unwrap().hash(into);
578                return;
579            }
580
581            if trim_paths {
582                self.inner.kind.hash(into);
583                return;
584            }
585        }
586        self.inner.kind.hash(into);
587        match self.inner.kind {
588            SourceKind::Git(_) => (&self).inner.canonical_url.hash(into),
589            _ => (&self).inner.url.as_str().hash(into),
590        }
591    }
592
593    pub fn full_eq(self, other: SourceId) -> bool {
594        ptr::eq(self.inner, other.inner)
595    }
596
597    pub fn full_hash<S: hash::Hasher>(self, into: &mut S) {
598        ptr::NonNull::from(self.inner).hash(into)
599    }
600}
601
602impl PartialEq for SourceId {
603    fn eq(&self, other: &SourceId) -> bool {
604        self.cmp(other) == Ordering::Equal
605    }
606}
607
608impl PartialOrd for SourceId {
609    fn partial_cmp(&self, other: &SourceId) -> Option<Ordering> {
610        Some(self.cmp(other))
611    }
612}
613
614// Custom comparison defined as source kind and canonical URL equality,
615// ignoring the `precise` and `name` fields.
616impl Ord for SourceId {
617    fn cmp(&self, other: &SourceId) -> Ordering {
618        // If our interior pointers are to the exact same `SourceIdInner` then
619        // we're guaranteed to be equal.
620        if ptr::eq(self.inner, other.inner) {
621            return Ordering::Equal;
622        }
623
624        // Sort first based on `kind`, deferring to the URL comparison if
625        // the kinds are equal.
626        let ord_kind = self.inner.kind.cmp(&other.inner.kind);
627        ord_kind.then_with(|| self.inner.canonical_url.cmp(&other.inner.canonical_url))
628    }
629}
630
631impl ser::Serialize for SourceId {
632    fn serialize<S>(&self, s: S) -> Result<S::Ok, S::Error>
633    where
634        S: ser::Serializer,
635    {
636        if self.is_path() {
637            None::<String>.serialize(s)
638        } else {
639            s.collect_str(&self.as_url())
640        }
641    }
642}
643
644impl<'de> de::Deserialize<'de> for SourceId {
645    fn deserialize<D>(d: D) -> Result<SourceId, D::Error>
646    where
647        D: de::Deserializer<'de>,
648    {
649        let string = String::deserialize(d)?;
650        SourceId::from_url(&string).map_err(de::Error::custom)
651    }
652}
653
654fn url_display(url: &Url) -> String {
655    if url.scheme() == "file" {
656        if let Ok(path) = url.to_file_path() {
657            if let Some(path_str) = path.to_str() {
658                return path_str.to_string();
659            }
660        }
661    }
662
663    url.as_str().to_string()
664}
665
666impl fmt::Display for SourceId {
667    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
668        match self.inner.kind {
669            SourceKind::Git(ref reference) => {
670                // Don't replace the URL display for git references,
671                // because those are kind of expected to be URLs.
672                write!(f, "{}", self.inner.url)?;
673                if let Some(pretty) = reference.pretty_ref(true) {
674                    write!(f, "?{}", pretty)?;
675                }
676
677                if let Some(s) = &self.inner.precise {
678                    let s = s.to_string();
679                    let len = cmp::min(s.len(), 8);
680                    write!(f, "#{}", &s[..len])?;
681                }
682                Ok(())
683            }
684            SourceKind::Path => write!(f, "{}", url_display(&self.inner.url)),
685            SourceKind::Builtin => write!(f, "builtin {}", url_display(&self.inner.url)),
686            SourceKind::Registry | SourceKind::SparseRegistry => {
687                write!(f, "registry `{}`", self.display_registry_name())
688            }
689            SourceKind::LocalRegistry => write!(f, "registry `{}`", url_display(&self.inner.url)),
690            SourceKind::Directory => write!(f, "dir {}", url_display(&self.inner.url)),
691        }
692    }
693}
694
695impl Hash for SourceId {
696    fn hash<S: hash::Hasher>(&self, into: &mut S) {
697        self.inner.kind.hash(into);
698        self.inner.canonical_url.hash(into);
699    }
700}
701
702/// The hash of `SourceIdInner` is used to retrieve its interned value from
703/// `SOURCE_ID_CACHE`. We only care about fields that make `SourceIdInner`
704/// unique. Optional fields not affecting the uniqueness must be excluded,
705/// such as [`registry_key`]. That's why this is not derived.
706///
707/// [`registry_key`]: SourceIdInner::registry_key
708impl Hash for SourceIdInner {
709    fn hash<S: hash::Hasher>(&self, into: &mut S) {
710        self.kind.hash(into);
711        self.precise.hash(into);
712        self.canonical_url.hash(into);
713    }
714}
715
716/// This implementation must be synced with [`SourceIdInner::hash`].
717impl PartialEq for SourceIdInner {
718    fn eq(&self, other: &Self) -> bool {
719        self.kind == other.kind
720            && self.precise == other.precise
721            && self.canonical_url == other.canonical_url
722    }
723}
724
725/// A `Display`able view into a `SourceId` that will write it as a url
726pub struct SourceIdAsUrl<'a> {
727    inner: &'a SourceIdInner,
728    encoded: bool,
729}
730
731impl<'a> fmt::Display for SourceIdAsUrl<'a> {
732    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
733        if let Some(protocol) = self.inner.kind.protocol() {
734            write!(f, "{protocol}+")?;
735        }
736        write!(f, "{}", self.inner.url)?;
737        if let SourceIdInner {
738            kind: SourceKind::Git(ref reference),
739            ref precise,
740            ..
741        } = *self.inner
742        {
743            if let Some(pretty) = reference.pretty_ref(self.encoded) {
744                write!(f, "?{}", pretty)?;
745            }
746            if let Some(precise) = precise.as_ref() {
747                write!(f, "#{}", precise)?;
748            }
749        }
750        Ok(())
751    }
752}
753
754impl KeyOf {
755    /// Gets the underlying key.
756    fn key(&self) -> &str {
757        match self {
758            KeyOf::Registry(k) | KeyOf::Source(k) => k,
759        }
760    }
761
762    /// Gets the key if it's from an alternative registry.
763    fn alternative_registry(&self) -> Option<&str> {
764        match self {
765            KeyOf::Registry(k) => Some(k),
766            _ => None,
767        }
768    }
769}
770
771#[cfg(test)]
772mod tests {
773    use super::{GitReference, SourceId, SourceKind};
774    use crate::util::{GlobalContext, IntoUrl};
775
776    #[test]
777    fn github_sources_equal() {
778        let loc = "https://github.com/foo/bar".into_url().unwrap();
779        let default = SourceKind::Git(GitReference::DefaultBranch);
780        let s1 = SourceId::new(default.clone(), loc, None).unwrap();
781
782        let loc = "git://github.com/foo/bar".into_url().unwrap();
783        let s2 = SourceId::new(default, loc.clone(), None).unwrap();
784
785        assert_eq!(s1, s2);
786
787        let foo = SourceKind::Git(GitReference::Branch("foo".to_string()));
788        let s3 = SourceId::new(foo, loc, None).unwrap();
789        assert_ne!(s1, s3);
790    }
791
792    // This is a test that the hash of the `SourceId` for crates.io is a well-known
793    // value.
794    //
795    // Note that the hash value matches what the crates.io source id has hashed
796    // since Rust 1.84.0. We strive to keep this value the same across
797    // versions of Cargo because changing it means that users will need to
798    // redownload the index and all crates they use when using a new Cargo version.
799    //
800    // This isn't to say that this hash can *never* change, only that when changing
801    // this it should be explicitly done. If this hash changes accidentally and
802    // you're able to restore the hash to its original value, please do so!
803    // Otherwise please just leave a comment in your PR as to why the hash value is
804    // changing and why the old value can't be easily preserved.
805    // If it takes an ugly hack to restore it,
806    // then leave a link here so we can remove the hack next time we change the hash.
807    //
808    // Hacks to remove next time the hash changes:
809    // - (fill in your code here)
810    //
811    // The hash value should be stable across platforms, and doesn't depend on
812    // endianness and bit-width. One caveat is that absolute paths on Windows
813    // are inherently different than on Unix-like platforms. Unless we omit or
814    // strip the prefix components (e.g. `C:`), there is not way to have a true
815    // cross-platform stable hash for absolute paths.
816    #[test]
817    fn test_stable_hash() {
818        use std::hash::Hasher;
819        use std::path::Path;
820
821        use snapbox::IntoData as _;
822        use snapbox::assert_data_eq;
823        use snapbox::str;
824
825        use crate::util::StableHasher;
826        use crate::util::hex::short_hash;
827
828        #[cfg(not(windows))]
829        let ws_root = Path::new("/tmp/ws");
830        #[cfg(windows)]
831        let ws_root = Path::new(r"C:\\tmp\ws");
832
833        let gen_hash = |source_id: SourceId| {
834            let mut hasher = StableHasher::new();
835            source_id.stable_hash(ws_root, false, &mut hasher);
836            Hasher::finish(&hasher).to_string()
837        };
838
839        let source_id = SourceId::crates_io(&GlobalContext::default().unwrap()).unwrap();
840        assert_data_eq!(gen_hash(source_id), str!["7062945687441624357"].raw());
841        assert_data_eq!(short_hash(&source_id), str!["25cdd57fae9f0462"].raw());
842
843        let url = "https://my-crates.io".into_url().unwrap();
844        let source_id = SourceId::for_registry(&url).unwrap();
845        assert_data_eq!(gen_hash(source_id), str!["8310250053664888498"].raw());
846        assert_data_eq!(short_hash(&source_id), str!["b2d65deb64f05373"].raw());
847
848        let url = "https://your-crates.io".into_url().unwrap();
849        let source_id = SourceId::for_alt_registry(&url, "alt").unwrap();
850        assert_data_eq!(gen_hash(source_id), str!["14149534903000258933"].raw());
851        assert_data_eq!(short_hash(&source_id), str!["755952de063f5dc4"].raw());
852
853        let url = "sparse+https://my-crates.io".into_url().unwrap();
854        let source_id = SourceId::for_registry(&url).unwrap();
855        assert_data_eq!(gen_hash(source_id), str!["16249512552851930162"].raw());
856        assert_data_eq!(short_hash(&source_id), str!["327cfdbd92dd81e1"].raw());
857
858        let url = "sparse+https://your-crates.io".into_url().unwrap();
859        let source_id = SourceId::for_alt_registry(&url, "alt").unwrap();
860        assert_data_eq!(gen_hash(source_id), str!["6156697384053352292"].raw());
861        assert_data_eq!(short_hash(&source_id), str!["64a713b6a6fb7055"].raw());
862
863        let url = "file:///tmp/ws/crate".into_url().unwrap();
864        let source_id = SourceId::for_git(&url, GitReference::DefaultBranch).unwrap();
865        assert_data_eq!(gen_hash(source_id), str!["473480029881867801"].raw());
866        assert_data_eq!(short_hash(&source_id), str!["199e591d94239206"].raw());
867
868        let source_id = SourceId::for_builtin().unwrap();
869        assert_data_eq!(gen_hash(source_id), str!["5062137541645472653"].raw());
870        assert_data_eq!(short_hash(&source_id), str!["8d93f21546534046"].raw());
871
872        let path = &ws_root.join("crate");
873        let source_id = SourceId::for_local_registry(path).unwrap();
874        #[cfg(not(windows))]
875        {
876            assert_data_eq!(gen_hash(source_id), str!["11515846423845066584"].raw());
877            assert_data_eq!(short_hash(&source_id), str!["58d73c154f81d09f"].raw());
878        }
879        #[cfg(windows)]
880        {
881            assert_data_eq!(gen_hash(source_id), str!["6146331155906064276"].raw());
882            assert_data_eq!(short_hash(&source_id), str!["946fb2239f274c55"].raw());
883        }
884
885        let source_id = SourceId::for_path(path).unwrap();
886        assert_data_eq!(gen_hash(source_id), str!["215644081443634269"].raw());
887        #[cfg(not(windows))]
888        assert_data_eq!(short_hash(&source_id), str!["64bace89c92b101f"].raw());
889        #[cfg(windows)]
890        assert_data_eq!(short_hash(&source_id), str!["01e1e6c391813fb6"].raw());
891
892        let source_id = SourceId::for_directory(path).unwrap();
893        #[cfg(not(windows))]
894        {
895            assert_data_eq!(gen_hash(source_id), str!["6127590343904940368"].raw());
896            assert_data_eq!(short_hash(&source_id), str!["505191d1f3920955"].raw());
897        }
898        #[cfg(windows)]
899        {
900            assert_data_eq!(gen_hash(source_id), str!["10423446877655960172"].raw());
901            assert_data_eq!(short_hash(&source_id), str!["6c8ad69db585a790"].raw());
902        }
903    }
904
905    #[test]
906    fn test_stable_hash_trim_paths() {
907        use std::hash::Hasher;
908        use std::path::Path;
909
910        use crate::util::StableHasher;
911
912        #[cfg(not(windows))]
913        let (ws_root, outside, elsewhere) = (
914            Path::new("/tmp/ws"),
915            Path::new("/tmp/crate"),
916            Path::new("/opt/crate"),
917        );
918        #[cfg(windows)]
919        let (ws_root, outside, elsewhere) = (
920            Path::new(r"C:\\tmp\ws"),
921            Path::new(r"C:\\tmp\crate"),
922            Path::new(r"D:\\opt\crate"),
923        );
924
925        let inside = &ws_root.join("crate");
926
927        let gen_hash = |path: &Path, trim_outside_path| {
928            let source_id = SourceId::for_path(path).unwrap();
929            let mut hasher = StableHasher::new();
930            source_id.stable_hash(ws_root, trim_outside_path, &mut hasher);
931            Hasher::finish(&hasher)
932        };
933
934        // Path sources outside workspace can be distinguished by their path unless trimmed.
935        // Path sources inside workspace are hashed relatively regardlessly.
936        assert_ne!(gen_hash(outside, false), gen_hash(elsewhere, false));
937        assert_eq!(gen_hash(outside, true), gen_hash(elsewhere, true));
938        assert_eq!(gen_hash(inside, false), gen_hash(inside, true));
939        assert_ne!(gen_hash(inside, true), gen_hash(outside, true));
940        assert_ne!(gen_hash(inside, true), gen_hash(elsewhere, true));
941    }
942
943    #[test]
944    fn serde_roundtrip() {
945        let url = "sparse+https://my-crates.io/".into_url().unwrap();
946        let source_id = SourceId::for_registry(&url).unwrap();
947        let formatted = format!("{}", source_id.as_url());
948        let deserialized = SourceId::from_url(&formatted).unwrap();
949        assert_eq!(formatted, "sparse+https://my-crates.io/");
950        assert_eq!(source_id, deserialized);
951    }
952
953    #[test]
954    fn gitrefs_roundtrip() {
955        let base = "https://host/path".into_url().unwrap();
956        let branch = GitReference::Branch("*-._+20%30 Z/z#foo=bar&zap[]?to\\()'\"".to_string());
957        let s1 = SourceId::for_git(&base, branch).unwrap();
958        let ser1 = format!("{}", s1.as_encoded_url());
959        let s2 = SourceId::from_url(&ser1).expect("Failed to deserialize");
960        let ser2 = format!("{}", s2.as_encoded_url());
961        // Serializing twice should yield the same result
962        assert_eq!(ser1, ser2, "Serialized forms don't match");
963        // SourceId serializing the same should have the same semantics
964        // This used to not be the case (# was ambiguous)
965        assert_eq!(s1, s2, "SourceId doesn't round-trip");
966        // Freeze the format to match an x-www-form-urlencoded query string
967        // https://url.spec.whatwg.org/#application/x-www-form-urlencoded
968        assert_eq!(
969            ser1,
970            "git+https://host/path?branch=*-._%2B20%2530+Z%2Fz%23foo%3Dbar%26zap%5B%5D%3Fto%5C%28%29%27%22"
971        );
972    }
973}
974
975/// Check if `url` equals to the overridden crates.io URL.
976#[expect(
977    clippy::disallowed_methods,
978    reason = "testing only, no reason for config support"
979)]
980fn is_overridden_crates_io_url(url: &str) -> bool {
981    std::env::var("__CARGO_TEST_CRATES_IO_URL_DO_NOT_USE_THIS").map_or(false, |v| v == url)
982}