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#[derive(Clone, Copy, Eq, Debug)]
40pub struct SourceId {
41 inner: &'static SourceIdInner,
42}
43
44#[derive(Eq, Clone, Debug)]
47struct SourceIdInner {
48 url: Url,
50 canonical_url: CanonicalUrl,
53 kind: SourceKind,
55 precise: Option<Precise>,
57 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#[derive(Debug, Clone, PartialEq, Eq)]
91enum KeyOf {
92 Registry(String),
94 Source(String),
96}
97
98impl SourceId {
99 fn new(kind: SourceKind, url: Url, key: Option<KeyOf>) -> CargoResult<SourceId> {
103 if kind == SourceKind::SparseRegistry {
104 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 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 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 pub fn as_url(&self) -> SourceIdAsUrl<'_> {
186 SourceIdAsUrl {
187 inner: &*self.inner,
188 encoded: false,
189 }
190 }
191
192 pub fn as_encoded_url(&self) -> SourceIdAsUrl<'_> {
194 SourceIdAsUrl {
195 inner: &*self.inner,
196 encoded: true,
197 }
198 }
199
200 pub fn for_path(path: &Path) -> CargoResult<SourceId> {
204 let url = path.into_url()?;
205 SourceId::new(SourceKind::Path, url, None)
206 }
207
208 pub fn for_builtin() -> CargoResult<SourceId> {
210 let url = "builtin://.".into_url()?;
211 SourceId::new(SourceKind::Builtin, url, None)
212 }
213
214 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 pub fn for_git(url: &Url, reference: GitReference) -> CargoResult<SourceId> {
227 SourceId::new(SourceKind::Git(reference), url.clone(), None)
228 }
229
230 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 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 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 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 pub fn for_directory(path: &Path) -> CargoResult<SourceId> {
262 let url = path.into_url()?;
263 SourceId::new(SourceKind::Directory, url, None)
264 }
265
266 pub fn crates_io(gctx: &GlobalContext) -> CargoResult<SourceId> {
271 gctx.crates_io_source_id()
272 }
273
274 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 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 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 pub fn url(&self) -> &Url {
314 &self.inner.url
315 }
316
317 pub fn canonical_url(&self) -> &CanonicalUrl {
320 &self.inner.canonical_url
321 }
322
323 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 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 self.without_precise().display_registry_name()
340 } else {
341 url_display(self.url())
342 }
343 }
344
345 pub fn alt_registry_key(&self) -> Option<&str> {
348 self.inner.registry_key.as_ref()?.alternative_registry()
349 }
350
351 pub fn is_path(self) -> bool {
353 self.inner.kind == SourceKind::Path
354 }
355
356 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 pub fn is_registry(self) -> bool {
371 matches!(
372 self.inner.kind,
373 SourceKind::Registry | SourceKind::SparseRegistry | SourceKind::LocalRegistry
374 )
375 }
376
377 pub fn is_sparse(self) -> bool {
379 matches!(self.inner.kind, SourceKind::SparseRegistry)
380 }
381
382 pub fn is_remote_registry(self) -> bool {
387 matches!(
388 self.inner.kind,
389 SourceKind::Registry | SourceKind::SparseRegistry
390 )
391 }
392
393 pub fn is_git(self) -> bool {
395 matches!(self.inner.kind, SourceKind::Git(_))
396 }
397
398 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 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 pub fn has_precise(self) -> bool {
447 self.inner.precise.is_some()
448 }
449
450 pub fn has_locked_precise(self) -> bool {
452 self.inner.precise == Some(Precise::Locked)
453 }
454
455 pub fn has_same_precise_as(self, other: Self) -> bool {
457 self.inner.precise == other.inner.precise
458 }
459
460 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 pub fn with_git_precise(self, fragment: Option<String>) -> SourceId {
483 self.with_precise(&fragment.map(|f| Precise::GitUrlFragment(f)))
484 }
485
486 pub fn without_precise(self) -> SourceId {
488 self.with_precise(&None)
489 }
490
491 pub fn with_locked_precise(self) -> SourceId {
493 self.with_precise(&Some(Precise::Locked))
494 }
495
496 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 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 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 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
614impl Ord for SourceId {
617 fn cmp(&self, other: &SourceId) -> Ordering {
618 if ptr::eq(self.inner, other.inner) {
621 return Ordering::Equal;
622 }
623
624 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 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
702impl 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
716impl 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
725pub 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 fn key(&self) -> &str {
757 match self {
758 KeyOf::Registry(k) | KeyOf::Source(k) => k,
759 }
760 }
761
762 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 #[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 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 assert_eq!(ser1, ser2, "Serialized forms don't match");
963 assert_eq!(s1, s2, "SourceId doesn't round-trip");
966 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#[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}