cargo_util_schemas/core/
package_id_spec.rs

1use std::fmt;
2
3use semver::Version;
4use serde::{de, ser};
5use url::Url;
6
7use crate::core::GitReference;
8use crate::core::PartialVersion;
9use crate::core::PartialVersionError;
10use crate::core::SourceKind;
11use crate::manifest::PackageName;
12use crate::restricted_names::NameValidationError;
13
14type Result<T> = std::result::Result<T, PackageIdSpecError>;
15
16/// Some or all of the data required to identify a package:
17///
18///  1. the package name (a `String`, required)
19///  2. the package version (a `Version`, optional)
20///  3. the package source (a `Url`, optional)
21///
22/// If any of the optional fields are omitted, then the package ID may be ambiguous, there may be
23/// more than one package/version/url combo that will match. However, often just the name is
24/// sufficient to uniquely define a package ID.
25#[derive(Clone, PartialEq, Eq, Debug, Hash, Ord, PartialOrd)]
26pub struct PackageIdSpec {
27    name: String,
28    version: Option<PartialVersion>,
29    url: Option<Url>,
30    kind: Option<SourceKind>,
31}
32
33impl PackageIdSpec {
34    pub fn new(name: String) -> Self {
35        Self {
36            name,
37            version: None,
38            url: None,
39            kind: None,
40        }
41    }
42
43    pub fn with_version(mut self, version: PartialVersion) -> Self {
44        self.version = Some(version);
45        self
46    }
47
48    pub fn with_url(mut self, url: Url) -> Self {
49        self.url = Some(url);
50        self
51    }
52
53    pub fn with_kind(mut self, kind: SourceKind) -> Self {
54        self.kind = Some(kind);
55        self
56    }
57
58    /// Parses a spec string and returns a `PackageIdSpec` if the string was valid.
59    ///
60    /// # Examples
61    /// Some examples of valid strings
62    ///
63    /// ```
64    /// use cargo_util_schemas::core::PackageIdSpec;
65    ///
66    /// let specs = vec![
67    ///     "foo",
68    ///     "foo@1.4",
69    ///     "foo@1.4.3",
70    ///     "foo:1.2.3",
71    ///     "https://github.com/rust-lang/crates.io-index#foo",
72    ///     "https://github.com/rust-lang/crates.io-index#foo@1.4.3",
73    ///     "ssh://git@github.com/rust-lang/foo.git#foo@1.4.3",
74    ///     "file:///path/to/my/project/foo",
75    ///     "file:///path/to/my/project/foo#1.1.8"
76    /// ];
77    /// for spec in specs {
78    ///     assert!(PackageIdSpec::parse(spec).is_ok());
79    /// }
80    pub fn parse(spec: &str) -> Result<PackageIdSpec> {
81        if spec.contains("://") {
82            if let Ok(url) = Url::parse(spec) {
83                return PackageIdSpec::from_url(url);
84            }
85        } else if spec.contains('/') || spec.contains('\\') {
86            let abs = std::env::current_dir().unwrap_or_default().join(spec);
87            if abs.exists() {
88                let maybe_url = Url::from_file_path(abs)
89                    .map_or_else(|_| "a file:// URL".to_string(), |url| url.to_string());
90                return Err(ErrorKind::MaybeFilePath {
91                    spec: spec.into(),
92                    maybe_url,
93                }
94                .into());
95            }
96        }
97        let (name, version) = parse_spec(spec)?.unwrap_or_else(|| (spec.to_owned(), None));
98        PackageName::new(&name)?;
99        Ok(PackageIdSpec {
100            name: String::from(name),
101            version,
102            url: None,
103            kind: None,
104        })
105    }
106
107    /// Tries to convert a valid `Url` to a `PackageIdSpec`.
108    fn from_url(mut url: Url) -> Result<PackageIdSpec> {
109        let mut kind = None;
110        if let Some((kind_str, scheme)) = url.scheme().split_once('+') {
111            match kind_str {
112                "git" => {
113                    let git_ref = GitReference::from_query(url.query_pairs());
114                    url.set_query(None);
115                    kind = Some(SourceKind::Git(git_ref));
116                    url = strip_url_protocol(&url);
117                }
118                "registry" => {
119                    if url.query().is_some() {
120                        return Err(ErrorKind::UnexpectedQueryString(url).into());
121                    }
122                    kind = Some(SourceKind::Registry);
123                    url = strip_url_protocol(&url);
124                }
125                "sparse" => {
126                    if url.query().is_some() {
127                        return Err(ErrorKind::UnexpectedQueryString(url).into());
128                    }
129                    kind = Some(SourceKind::SparseRegistry);
130                    // Leave `sparse` as part of URL, see `SourceId::new`
131                    // url = strip_url_protocol(&url);
132                }
133                "path" => {
134                    if url.query().is_some() {
135                        return Err(ErrorKind::UnexpectedQueryString(url).into());
136                    }
137                    if scheme != "file" {
138                        return Err(ErrorKind::UnsupportedPathPlusScheme(scheme.into()).into());
139                    }
140                    kind = Some(SourceKind::Path);
141                    url = strip_url_protocol(&url);
142                }
143                kind => return Err(ErrorKind::UnsupportedProtocol(kind.into()).into()),
144            }
145        } else {
146            if url.query().is_some() {
147                return Err(ErrorKind::UnexpectedQueryString(url).into());
148            }
149        }
150
151        let frag = url.fragment().map(|s| s.to_owned());
152        url.set_fragment(None);
153
154        let (name, version) = {
155            let Some(path_name) = url.path_segments().and_then(|mut p| p.next_back()) else {
156                return Err(ErrorKind::MissingUrlPath(url).into());
157            };
158            match frag {
159                Some(fragment) => match parse_spec(&fragment)? {
160                    Some((name, ver)) => (name, ver),
161                    None => {
162                        if fragment.chars().next().unwrap().is_alphabetic() {
163                            (String::from(fragment.as_str()), None)
164                        } else {
165                            let version = fragment.parse::<PartialVersion>()?;
166                            (String::from(path_name), Some(version))
167                        }
168                    }
169                },
170                None => (String::from(path_name), None),
171            }
172        };
173        PackageName::new(&name)?;
174        Ok(PackageIdSpec {
175            name,
176            version,
177            url: Some(url),
178            kind,
179        })
180    }
181
182    pub fn name(&self) -> &str {
183        self.name.as_str()
184    }
185
186    /// Full `semver::Version`, if present
187    pub fn version(&self) -> Option<Version> {
188        self.version.as_ref().and_then(|v| v.to_version())
189    }
190
191    pub fn partial_version(&self) -> Option<&PartialVersion> {
192        self.version.as_ref()
193    }
194
195    pub fn url(&self) -> Option<&Url> {
196        self.url.as_ref()
197    }
198
199    pub fn set_url(&mut self, url: Url) {
200        self.url = Some(url);
201    }
202
203    pub fn kind(&self) -> Option<&SourceKind> {
204        self.kind.as_ref()
205    }
206
207    pub fn set_kind(&mut self, kind: SourceKind) {
208        self.kind = Some(kind);
209    }
210}
211
212fn parse_spec(spec: &str) -> Result<Option<(String, Option<PartialVersion>)>> {
213    let Some((name, ver)) = spec
214        .rsplit_once('@')
215        .or_else(|| spec.rsplit_once(':').filter(|(n, _)| !n.ends_with(':')))
216    else {
217        return Ok(None);
218    };
219    let name = name.to_owned();
220    let ver = ver.parse::<PartialVersion>()?;
221    Ok(Some((name, Some(ver))))
222}
223
224fn strip_url_protocol(url: &Url) -> Url {
225    // Ridiculous hoop because `Url::set_scheme` errors when changing to http/https
226    let raw = url.to_string();
227    raw.split_once('+').unwrap().1.parse().unwrap()
228}
229
230impl fmt::Display for PackageIdSpec {
231    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
232        let mut printed_name = false;
233        match self.url {
234            Some(ref url) => {
235                if let Some(protocol) = self.kind.as_ref().and_then(|k| k.protocol()) {
236                    write!(f, "{protocol}+")?;
237                }
238                write!(f, "{}", url)?;
239                if let Some(SourceKind::Git(git_ref)) = self.kind.as_ref() {
240                    if let Some(pretty) = git_ref.pretty_ref(true) {
241                        write!(f, "?{}", pretty)?;
242                    }
243                }
244                if url.path_segments().unwrap().next_back().unwrap() != &*self.name {
245                    printed_name = true;
246                    write!(f, "#{}", self.name)?;
247                }
248            }
249            None => {
250                printed_name = true;
251                write!(f, "{}", self.name)?;
252            }
253        }
254        if let Some(ref v) = self.version {
255            write!(f, "{}{}", if printed_name { "@" } else { "#" }, v)?;
256        }
257        Ok(())
258    }
259}
260
261impl ser::Serialize for PackageIdSpec {
262    fn serialize<S>(&self, s: S) -> std::result::Result<S::Ok, S::Error>
263    where
264        S: ser::Serializer,
265    {
266        self.to_string().serialize(s)
267    }
268}
269
270impl<'de> de::Deserialize<'de> for PackageIdSpec {
271    fn deserialize<D>(d: D) -> std::result::Result<PackageIdSpec, D::Error>
272    where
273        D: de::Deserializer<'de>,
274    {
275        let string = String::deserialize(d)?;
276        PackageIdSpec::parse(&string).map_err(de::Error::custom)
277    }
278}
279
280/// Error parsing a [`PackageIdSpec`].
281#[derive(Debug, thiserror::Error)]
282#[error(transparent)]
283pub struct PackageIdSpecError(#[from] ErrorKind);
284
285impl From<PartialVersionError> for PackageIdSpecError {
286    fn from(value: PartialVersionError) -> Self {
287        ErrorKind::PartialVersion(value).into()
288    }
289}
290
291impl From<NameValidationError> for PackageIdSpecError {
292    fn from(value: NameValidationError) -> Self {
293        ErrorKind::NameValidation(value).into()
294    }
295}
296
297/// Non-public error kind for [`PackageIdSpecError`].
298#[non_exhaustive]
299#[derive(Debug, thiserror::Error)]
300enum ErrorKind {
301    #[error("unsupported source protocol: {0}")]
302    UnsupportedProtocol(String),
303
304    #[error("`path+{0}` is unsupported; `path+file` and `file` schemes are supported")]
305    UnsupportedPathPlusScheme(String),
306
307    #[error("cannot have a query string in a pkgid: {0}")]
308    UnexpectedQueryString(Url),
309
310    #[error("pkgid urls must have at least one path component: {0}")]
311    MissingUrlPath(Url),
312
313    #[error("package ID specification `{spec}` looks like a file path, maybe try {maybe_url}")]
314    MaybeFilePath { spec: String, maybe_url: String },
315
316    #[error(transparent)]
317    NameValidation(#[from] crate::restricted_names::NameValidationError),
318
319    #[error(transparent)]
320    PartialVersion(#[from] crate::core::PartialVersionError),
321}
322
323#[cfg(test)]
324mod tests {
325    use super::ErrorKind;
326    use super::PackageIdSpec;
327    use crate::core::{GitReference, SourceKind};
328    use url::Url;
329
330    #[track_caller]
331    fn ok(spec: &str, expected: PackageIdSpec, expected_rendered: &str) {
332        let parsed = PackageIdSpec::parse(spec).unwrap();
333        assert_eq!(parsed, expected);
334        let rendered = parsed.to_string();
335        assert_eq!(rendered, expected_rendered);
336        let reparsed = PackageIdSpec::parse(&rendered).unwrap();
337        assert_eq!(reparsed, expected);
338    }
339
340    macro_rules! err {
341        ($spec:expr, $expected:pat) => {
342            let err = PackageIdSpec::parse($spec).unwrap_err();
343            let kind = err.0;
344            assert!(
345                matches!(kind, $expected),
346                "`{}` parse error mismatch, got {kind:?}",
347                $spec
348            );
349        };
350    }
351
352    #[test]
353    fn good_parsing() {
354        ok(
355            "https://crates.io/foo",
356            PackageIdSpec {
357                name: String::from("foo"),
358                version: None,
359                url: Some(Url::parse("https://crates.io/foo").unwrap()),
360                kind: None,
361            },
362            "https://crates.io/foo",
363        );
364        ok(
365            "https://crates.io/foo#1.2.3",
366            PackageIdSpec {
367                name: String::from("foo"),
368                version: Some("1.2.3".parse().unwrap()),
369                url: Some(Url::parse("https://crates.io/foo").unwrap()),
370                kind: None,
371            },
372            "https://crates.io/foo#1.2.3",
373        );
374        ok(
375            "https://crates.io/foo#1.2",
376            PackageIdSpec {
377                name: String::from("foo"),
378                version: Some("1.2".parse().unwrap()),
379                url: Some(Url::parse("https://crates.io/foo").unwrap()),
380                kind: None,
381            },
382            "https://crates.io/foo#1.2",
383        );
384        ok(
385            "https://crates.io/foo#bar:1.2.3",
386            PackageIdSpec {
387                name: String::from("bar"),
388                version: Some("1.2.3".parse().unwrap()),
389                url: Some(Url::parse("https://crates.io/foo").unwrap()),
390                kind: None,
391            },
392            "https://crates.io/foo#bar@1.2.3",
393        );
394        ok(
395            "https://crates.io/foo#bar@1.2.3",
396            PackageIdSpec {
397                name: String::from("bar"),
398                version: Some("1.2.3".parse().unwrap()),
399                url: Some(Url::parse("https://crates.io/foo").unwrap()),
400                kind: None,
401            },
402            "https://crates.io/foo#bar@1.2.3",
403        );
404        ok(
405            "https://crates.io/foo#bar@1.2",
406            PackageIdSpec {
407                name: String::from("bar"),
408                version: Some("1.2".parse().unwrap()),
409                url: Some(Url::parse("https://crates.io/foo").unwrap()),
410                kind: None,
411            },
412            "https://crates.io/foo#bar@1.2",
413        );
414        ok(
415            "registry+https://crates.io/foo#bar@1.2",
416            PackageIdSpec {
417                name: String::from("bar"),
418                version: Some("1.2".parse().unwrap()),
419                url: Some(Url::parse("https://crates.io/foo").unwrap()),
420                kind: Some(SourceKind::Registry),
421            },
422            "registry+https://crates.io/foo#bar@1.2",
423        );
424        ok(
425            "sparse+https://crates.io/foo#bar@1.2",
426            PackageIdSpec {
427                name: String::from("bar"),
428                version: Some("1.2".parse().unwrap()),
429                url: Some(Url::parse("sparse+https://crates.io/foo").unwrap()),
430                kind: Some(SourceKind::SparseRegistry),
431            },
432            "sparse+https://crates.io/foo#bar@1.2",
433        );
434        ok(
435            "foo",
436            PackageIdSpec {
437                name: String::from("foo"),
438                version: None,
439                url: None,
440                kind: None,
441            },
442            "foo",
443        );
444        ok(
445            "foo::bar",
446            PackageIdSpec {
447                name: String::from("foo::bar"),
448                version: None,
449                url: None,
450                kind: None,
451            },
452            "foo::bar",
453        );
454        ok(
455            "foo:1.2.3",
456            PackageIdSpec {
457                name: String::from("foo"),
458                version: Some("1.2.3".parse().unwrap()),
459                url: None,
460                kind: None,
461            },
462            "foo@1.2.3",
463        );
464        ok(
465            "foo::bar:1.2.3",
466            PackageIdSpec {
467                name: String::from("foo::bar"),
468                version: Some("1.2.3".parse().unwrap()),
469                url: None,
470                kind: None,
471            },
472            "foo::bar@1.2.3",
473        );
474        ok(
475            "foo@1.2.3",
476            PackageIdSpec {
477                name: String::from("foo"),
478                version: Some("1.2.3".parse().unwrap()),
479                url: None,
480                kind: None,
481            },
482            "foo@1.2.3",
483        );
484        ok(
485            "foo::bar@1.2.3",
486            PackageIdSpec {
487                name: String::from("foo::bar"),
488                version: Some("1.2.3".parse().unwrap()),
489                url: None,
490                kind: None,
491            },
492            "foo::bar@1.2.3",
493        );
494        ok(
495            "foo@1.2",
496            PackageIdSpec {
497                name: String::from("foo"),
498                version: Some("1.2".parse().unwrap()),
499                url: None,
500                kind: None,
501            },
502            "foo@1.2",
503        );
504
505        // pkgid-spec.md
506        ok(
507            "regex",
508            PackageIdSpec {
509                name: String::from("regex"),
510                version: None,
511                url: None,
512                kind: None,
513            },
514            "regex",
515        );
516        ok(
517            "regex@1.4",
518            PackageIdSpec {
519                name: String::from("regex"),
520                version: Some("1.4".parse().unwrap()),
521                url: None,
522                kind: None,
523            },
524            "regex@1.4",
525        );
526        ok(
527            "regex@1.4.3",
528            PackageIdSpec {
529                name: String::from("regex"),
530                version: Some("1.4.3".parse().unwrap()),
531                url: None,
532                kind: None,
533            },
534            "regex@1.4.3",
535        );
536        ok(
537            "https://github.com/rust-lang/crates.io-index#regex",
538            PackageIdSpec {
539                name: String::from("regex"),
540                version: None,
541                url: Some(Url::parse("https://github.com/rust-lang/crates.io-index").unwrap()),
542                kind: None,
543            },
544            "https://github.com/rust-lang/crates.io-index#regex",
545        );
546        ok(
547            "https://github.com/rust-lang/crates.io-index#regex@1.4.3",
548            PackageIdSpec {
549                name: String::from("regex"),
550                version: Some("1.4.3".parse().unwrap()),
551                url: Some(Url::parse("https://github.com/rust-lang/crates.io-index").unwrap()),
552                kind: None,
553            },
554            "https://github.com/rust-lang/crates.io-index#regex@1.4.3",
555        );
556        ok(
557            "sparse+https://github.com/rust-lang/crates.io-index#regex@1.4.3",
558            PackageIdSpec {
559                name: String::from("regex"),
560                version: Some("1.4.3".parse().unwrap()),
561                url: Some(
562                    Url::parse("sparse+https://github.com/rust-lang/crates.io-index").unwrap(),
563                ),
564                kind: Some(SourceKind::SparseRegistry),
565            },
566            "sparse+https://github.com/rust-lang/crates.io-index#regex@1.4.3",
567        );
568        ok(
569            "https://github.com/rust-lang/cargo#0.52.0",
570            PackageIdSpec {
571                name: String::from("cargo"),
572                version: Some("0.52.0".parse().unwrap()),
573                url: Some(Url::parse("https://github.com/rust-lang/cargo").unwrap()),
574                kind: None,
575            },
576            "https://github.com/rust-lang/cargo#0.52.0",
577        );
578        ok(
579            "https://github.com/rust-lang/cargo#cargo-platform@0.1.2",
580            PackageIdSpec {
581                name: String::from("cargo-platform"),
582                version: Some("0.1.2".parse().unwrap()),
583                url: Some(Url::parse("https://github.com/rust-lang/cargo").unwrap()),
584                kind: None,
585            },
586            "https://github.com/rust-lang/cargo#cargo-platform@0.1.2",
587        );
588        ok(
589            "ssh://git@github.com/rust-lang/regex.git#regex@1.4.3",
590            PackageIdSpec {
591                name: String::from("regex"),
592                version: Some("1.4.3".parse().unwrap()),
593                url: Some(Url::parse("ssh://git@github.com/rust-lang/regex.git").unwrap()),
594                kind: None,
595            },
596            "ssh://git@github.com/rust-lang/regex.git#regex@1.4.3",
597        );
598        ok(
599            "git+ssh://git@github.com/rust-lang/regex.git#regex@1.4.3",
600            PackageIdSpec {
601                name: String::from("regex"),
602                version: Some("1.4.3".parse().unwrap()),
603                url: Some(Url::parse("ssh://git@github.com/rust-lang/regex.git").unwrap()),
604                kind: Some(SourceKind::Git(GitReference::DefaultBranch)),
605            },
606            "git+ssh://git@github.com/rust-lang/regex.git#regex@1.4.3",
607        );
608        ok(
609            "git+ssh://git@github.com/rust-lang/regex.git?branch=dev#regex@1.4.3",
610            PackageIdSpec {
611                name: String::from("regex"),
612                version: Some("1.4.3".parse().unwrap()),
613                url: Some(Url::parse("ssh://git@github.com/rust-lang/regex.git").unwrap()),
614                kind: Some(SourceKind::Git(GitReference::Branch("dev".to_owned()))),
615            },
616            "git+ssh://git@github.com/rust-lang/regex.git?branch=dev#regex@1.4.3",
617        );
618        ok(
619            "file:///path/to/my/project/foo",
620            PackageIdSpec {
621                name: String::from("foo"),
622                version: None,
623                url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
624                kind: None,
625            },
626            "file:///path/to/my/project/foo",
627        );
628        ok(
629            "file:///path/to/my/project/foo::bar",
630            PackageIdSpec {
631                name: String::from("foo::bar"),
632                version: None,
633                url: Some(Url::parse("file:///path/to/my/project/foo::bar").unwrap()),
634                kind: None,
635            },
636            "file:///path/to/my/project/foo::bar",
637        );
638        ok(
639            "file:///path/to/my/project/foo#1.1.8",
640            PackageIdSpec {
641                name: String::from("foo"),
642                version: Some("1.1.8".parse().unwrap()),
643                url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
644                kind: None,
645            },
646            "file:///path/to/my/project/foo#1.1.8",
647        );
648        ok(
649            "path+file:///path/to/my/project/foo#1.1.8",
650            PackageIdSpec {
651                name: String::from("foo"),
652                version: Some("1.1.8".parse().unwrap()),
653                url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
654                kind: Some(SourceKind::Path),
655            },
656            "path+file:///path/to/my/project/foo#1.1.8",
657        );
658        ok(
659            "path+file:///path/to/my/project/foo#bar",
660            PackageIdSpec {
661                name: String::from("bar"),
662                version: None,
663                url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
664                kind: Some(SourceKind::Path),
665            },
666            "path+file:///path/to/my/project/foo#bar",
667        );
668        ok(
669            "path+file:///path/to/my/project/foo#foo::bar",
670            PackageIdSpec {
671                name: String::from("foo::bar"),
672                version: None,
673                url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
674                kind: Some(SourceKind::Path),
675            },
676            "path+file:///path/to/my/project/foo#foo::bar",
677        );
678        ok(
679            "path+file:///path/to/my/project/foo#bar:1.1.8",
680            PackageIdSpec {
681                name: String::from("bar"),
682                version: Some("1.1.8".parse().unwrap()),
683                url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
684                kind: Some(SourceKind::Path),
685            },
686            "path+file:///path/to/my/project/foo#bar@1.1.8",
687        );
688        ok(
689            "path+file:///path/to/my/project/foo#foo::bar:1.1.8",
690            PackageIdSpec {
691                name: String::from("foo::bar"),
692                version: Some("1.1.8".parse().unwrap()),
693                url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
694                kind: Some(SourceKind::Path),
695            },
696            "path+file:///path/to/my/project/foo#foo::bar@1.1.8",
697        );
698        ok(
699            "path+file:///path/to/my/project/foo#bar@1.1.8",
700            PackageIdSpec {
701                name: String::from("bar"),
702                version: Some("1.1.8".parse().unwrap()),
703                url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
704                kind: Some(SourceKind::Path),
705            },
706            "path+file:///path/to/my/project/foo#bar@1.1.8",
707        );
708        ok(
709            "path+file:///path/to/my/project/foo#foo::bar@1.1.8",
710            PackageIdSpec {
711                name: String::from("foo::bar"),
712                version: Some("1.1.8".parse().unwrap()),
713                url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
714                kind: Some(SourceKind::Path),
715            },
716            "path+file:///path/to/my/project/foo#foo::bar@1.1.8",
717        );
718    }
719
720    #[test]
721    fn bad_parsing() {
722        err!("baz:", ErrorKind::PartialVersion(_));
723        err!("baz:*", ErrorKind::PartialVersion(_));
724        err!("baz@", ErrorKind::PartialVersion(_));
725        err!("baz@*", ErrorKind::PartialVersion(_));
726        err!("baz@^1.0", ErrorKind::PartialVersion(_));
727        err!("https://baz:1.0", ErrorKind::NameValidation(_));
728        err!("https://#baz:1.0", ErrorKind::NameValidation(_));
729        err!(
730            "foobar+https://github.com/rust-lang/crates.io-index",
731            ErrorKind::UnsupportedProtocol(_)
732        );
733        err!(
734            "path+https://github.com/rust-lang/crates.io-index",
735            ErrorKind::UnsupportedPathPlusScheme(_)
736        );
737
738        // Only `git+` can use `?`
739        err!(
740            "file:///path/to/my/project/foo?branch=dev",
741            ErrorKind::UnexpectedQueryString(_)
742        );
743        err!(
744            "path+file:///path/to/my/project/foo?branch=dev",
745            ErrorKind::UnexpectedQueryString(_)
746        );
747        err!(
748            "registry+https://github.com/rust-lang/cargo?branch=dev#0.52.0",
749            ErrorKind::UnexpectedQueryString(_)
750        );
751        err!(
752            "sparse+https://github.com/rust-lang/cargo?branch=dev#0.52.0",
753            ErrorKind::UnexpectedQueryString(_)
754        );
755        err!("@1.2.3", ErrorKind::NameValidation(_));
756        err!("registry+https://github.com", ErrorKind::NameValidation(_));
757        err!("https://crates.io/1foo#1.2.3", ErrorKind::NameValidation(_));
758    }
759}