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#[cfg(feature = "unstable-schema")]
281impl schemars::JsonSchema for PackageIdSpec {
282    fn schema_name() -> std::borrow::Cow<'static, str> {
283        "PackageIdSpec".into()
284    }
285    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
286        <String as schemars::JsonSchema>::json_schema(generator)
287    }
288}
289
290#[derive(Debug, thiserror::Error)]
291#[error(transparent)]
292pub struct PackageIdSpecError(#[from] ErrorKind);
293
294impl From<PartialVersionError> for PackageIdSpecError {
295    fn from(value: PartialVersionError) -> Self {
296        ErrorKind::PartialVersion(value).into()
297    }
298}
299
300impl From<NameValidationError> for PackageIdSpecError {
301    fn from(value: NameValidationError) -> Self {
302        ErrorKind::NameValidation(value).into()
303    }
304}
305
306/// Non-public error kind for [`PackageIdSpecError`].
307#[non_exhaustive]
308#[derive(Debug, thiserror::Error)]
309enum ErrorKind {
310    #[error("unsupported source protocol: {0}")]
311    UnsupportedProtocol(String),
312
313    #[error("`path+{0}` is unsupported; `path+file` and `file` schemes are supported")]
314    UnsupportedPathPlusScheme(String),
315
316    #[error("cannot have a query string in a pkgid: {0}")]
317    UnexpectedQueryString(Url),
318
319    #[error("pkgid urls must have at least one path component: {0}")]
320    MissingUrlPath(Url),
321
322    #[error("package ID specification `{spec}` looks like a file path, maybe try {maybe_url}")]
323    MaybeFilePath { spec: String, maybe_url: String },
324
325    #[error(transparent)]
326    NameValidation(#[from] crate::restricted_names::NameValidationError),
327
328    #[error(transparent)]
329    PartialVersion(#[from] crate::core::PartialVersionError),
330}
331
332#[cfg(test)]
333mod tests {
334    use super::ErrorKind;
335    use super::PackageIdSpec;
336    use crate::core::{GitReference, SourceKind};
337    use url::Url;
338
339    #[track_caller]
340    fn ok(spec: &str, expected: PackageIdSpec, expected_rendered: &str) {
341        let parsed = PackageIdSpec::parse(spec).unwrap();
342        assert_eq!(parsed, expected);
343        let rendered = parsed.to_string();
344        assert_eq!(rendered, expected_rendered);
345        let reparsed = PackageIdSpec::parse(&rendered).unwrap();
346        assert_eq!(reparsed, expected);
347    }
348
349    macro_rules! err {
350        ($spec:expr, $expected:pat) => {
351            let err = PackageIdSpec::parse($spec).unwrap_err();
352            let kind = err.0;
353            assert!(
354                matches!(kind, $expected),
355                "`{}` parse error mismatch, got {kind:?}",
356                $spec
357            );
358        };
359    }
360
361    #[test]
362    fn good_parsing() {
363        ok(
364            "https://crates.io/foo",
365            PackageIdSpec {
366                name: String::from("foo"),
367                version: None,
368                url: Some(Url::parse("https://crates.io/foo").unwrap()),
369                kind: None,
370            },
371            "https://crates.io/foo",
372        );
373        ok(
374            "https://crates.io/foo#1.2.3",
375            PackageIdSpec {
376                name: String::from("foo"),
377                version: Some("1.2.3".parse().unwrap()),
378                url: Some(Url::parse("https://crates.io/foo").unwrap()),
379                kind: None,
380            },
381            "https://crates.io/foo#1.2.3",
382        );
383        ok(
384            "https://crates.io/foo#1.2",
385            PackageIdSpec {
386                name: String::from("foo"),
387                version: Some("1.2".parse().unwrap()),
388                url: Some(Url::parse("https://crates.io/foo").unwrap()),
389                kind: None,
390            },
391            "https://crates.io/foo#1.2",
392        );
393        ok(
394            "https://crates.io/foo#bar:1.2.3",
395            PackageIdSpec {
396                name: String::from("bar"),
397                version: Some("1.2.3".parse().unwrap()),
398                url: Some(Url::parse("https://crates.io/foo").unwrap()),
399                kind: None,
400            },
401            "https://crates.io/foo#bar@1.2.3",
402        );
403        ok(
404            "https://crates.io/foo#bar@1.2.3",
405            PackageIdSpec {
406                name: String::from("bar"),
407                version: Some("1.2.3".parse().unwrap()),
408                url: Some(Url::parse("https://crates.io/foo").unwrap()),
409                kind: None,
410            },
411            "https://crates.io/foo#bar@1.2.3",
412        );
413        ok(
414            "https://crates.io/foo#bar@1.2",
415            PackageIdSpec {
416                name: String::from("bar"),
417                version: Some("1.2".parse().unwrap()),
418                url: Some(Url::parse("https://crates.io/foo").unwrap()),
419                kind: None,
420            },
421            "https://crates.io/foo#bar@1.2",
422        );
423        ok(
424            "registry+https://crates.io/foo#bar@1.2",
425            PackageIdSpec {
426                name: String::from("bar"),
427                version: Some("1.2".parse().unwrap()),
428                url: Some(Url::parse("https://crates.io/foo").unwrap()),
429                kind: Some(SourceKind::Registry),
430            },
431            "registry+https://crates.io/foo#bar@1.2",
432        );
433        ok(
434            "sparse+https://crates.io/foo#bar@1.2",
435            PackageIdSpec {
436                name: String::from("bar"),
437                version: Some("1.2".parse().unwrap()),
438                url: Some(Url::parse("sparse+https://crates.io/foo").unwrap()),
439                kind: Some(SourceKind::SparseRegistry),
440            },
441            "sparse+https://crates.io/foo#bar@1.2",
442        );
443        ok(
444            "foo",
445            PackageIdSpec {
446                name: String::from("foo"),
447                version: None,
448                url: None,
449                kind: None,
450            },
451            "foo",
452        );
453        ok(
454            "foo::bar",
455            PackageIdSpec {
456                name: String::from("foo::bar"),
457                version: None,
458                url: None,
459                kind: None,
460            },
461            "foo::bar",
462        );
463        ok(
464            "foo:1.2.3",
465            PackageIdSpec {
466                name: String::from("foo"),
467                version: Some("1.2.3".parse().unwrap()),
468                url: None,
469                kind: None,
470            },
471            "foo@1.2.3",
472        );
473        ok(
474            "foo::bar:1.2.3",
475            PackageIdSpec {
476                name: String::from("foo::bar"),
477                version: Some("1.2.3".parse().unwrap()),
478                url: None,
479                kind: None,
480            },
481            "foo::bar@1.2.3",
482        );
483        ok(
484            "foo@1.2.3",
485            PackageIdSpec {
486                name: String::from("foo"),
487                version: Some("1.2.3".parse().unwrap()),
488                url: None,
489                kind: None,
490            },
491            "foo@1.2.3",
492        );
493        ok(
494            "foo::bar@1.2.3",
495            PackageIdSpec {
496                name: String::from("foo::bar"),
497                version: Some("1.2.3".parse().unwrap()),
498                url: None,
499                kind: None,
500            },
501            "foo::bar@1.2.3",
502        );
503        ok(
504            "foo@1.2",
505            PackageIdSpec {
506                name: String::from("foo"),
507                version: Some("1.2".parse().unwrap()),
508                url: None,
509                kind: None,
510            },
511            "foo@1.2",
512        );
513
514        // pkgid-spec.md
515        ok(
516            "regex",
517            PackageIdSpec {
518                name: String::from("regex"),
519                version: None,
520                url: None,
521                kind: None,
522            },
523            "regex",
524        );
525        ok(
526            "regex@1.4",
527            PackageIdSpec {
528                name: String::from("regex"),
529                version: Some("1.4".parse().unwrap()),
530                url: None,
531                kind: None,
532            },
533            "regex@1.4",
534        );
535        ok(
536            "regex@1.4.3",
537            PackageIdSpec {
538                name: String::from("regex"),
539                version: Some("1.4.3".parse().unwrap()),
540                url: None,
541                kind: None,
542            },
543            "regex@1.4.3",
544        );
545        ok(
546            "https://github.com/rust-lang/crates.io-index#regex",
547            PackageIdSpec {
548                name: String::from("regex"),
549                version: None,
550                url: Some(Url::parse("https://github.com/rust-lang/crates.io-index").unwrap()),
551                kind: None,
552            },
553            "https://github.com/rust-lang/crates.io-index#regex",
554        );
555        ok(
556            "https://github.com/rust-lang/crates.io-index#regex@1.4.3",
557            PackageIdSpec {
558                name: String::from("regex"),
559                version: Some("1.4.3".parse().unwrap()),
560                url: Some(Url::parse("https://github.com/rust-lang/crates.io-index").unwrap()),
561                kind: None,
562            },
563            "https://github.com/rust-lang/crates.io-index#regex@1.4.3",
564        );
565        ok(
566            "sparse+https://github.com/rust-lang/crates.io-index#regex@1.4.3",
567            PackageIdSpec {
568                name: String::from("regex"),
569                version: Some("1.4.3".parse().unwrap()),
570                url: Some(
571                    Url::parse("sparse+https://github.com/rust-lang/crates.io-index").unwrap(),
572                ),
573                kind: Some(SourceKind::SparseRegistry),
574            },
575            "sparse+https://github.com/rust-lang/crates.io-index#regex@1.4.3",
576        );
577        ok(
578            "https://github.com/rust-lang/cargo#0.52.0",
579            PackageIdSpec {
580                name: String::from("cargo"),
581                version: Some("0.52.0".parse().unwrap()),
582                url: Some(Url::parse("https://github.com/rust-lang/cargo").unwrap()),
583                kind: None,
584            },
585            "https://github.com/rust-lang/cargo#0.52.0",
586        );
587        ok(
588            "https://github.com/rust-lang/cargo#cargo-platform@0.1.2",
589            PackageIdSpec {
590                name: String::from("cargo-platform"),
591                version: Some("0.1.2".parse().unwrap()),
592                url: Some(Url::parse("https://github.com/rust-lang/cargo").unwrap()),
593                kind: None,
594            },
595            "https://github.com/rust-lang/cargo#cargo-platform@0.1.2",
596        );
597        ok(
598            "ssh://git@github.com/rust-lang/regex.git#regex@1.4.3",
599            PackageIdSpec {
600                name: String::from("regex"),
601                version: Some("1.4.3".parse().unwrap()),
602                url: Some(Url::parse("ssh://git@github.com/rust-lang/regex.git").unwrap()),
603                kind: None,
604            },
605            "ssh://git@github.com/rust-lang/regex.git#regex@1.4.3",
606        );
607        ok(
608            "git+ssh://git@github.com/rust-lang/regex.git#regex@1.4.3",
609            PackageIdSpec {
610                name: String::from("regex"),
611                version: Some("1.4.3".parse().unwrap()),
612                url: Some(Url::parse("ssh://git@github.com/rust-lang/regex.git").unwrap()),
613                kind: Some(SourceKind::Git(GitReference::DefaultBranch)),
614            },
615            "git+ssh://git@github.com/rust-lang/regex.git#regex@1.4.3",
616        );
617        ok(
618            "git+ssh://git@github.com/rust-lang/regex.git?branch=dev#regex@1.4.3",
619            PackageIdSpec {
620                name: String::from("regex"),
621                version: Some("1.4.3".parse().unwrap()),
622                url: Some(Url::parse("ssh://git@github.com/rust-lang/regex.git").unwrap()),
623                kind: Some(SourceKind::Git(GitReference::Branch("dev".to_owned()))),
624            },
625            "git+ssh://git@github.com/rust-lang/regex.git?branch=dev#regex@1.4.3",
626        );
627        ok(
628            "file:///path/to/my/project/foo",
629            PackageIdSpec {
630                name: String::from("foo"),
631                version: None,
632                url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
633                kind: None,
634            },
635            "file:///path/to/my/project/foo",
636        );
637        ok(
638            "file:///path/to/my/project/foo::bar",
639            PackageIdSpec {
640                name: String::from("foo::bar"),
641                version: None,
642                url: Some(Url::parse("file:///path/to/my/project/foo::bar").unwrap()),
643                kind: None,
644            },
645            "file:///path/to/my/project/foo::bar",
646        );
647        ok(
648            "file:///path/to/my/project/foo#1.1.8",
649            PackageIdSpec {
650                name: String::from("foo"),
651                version: Some("1.1.8".parse().unwrap()),
652                url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
653                kind: None,
654            },
655            "file:///path/to/my/project/foo#1.1.8",
656        );
657        ok(
658            "path+file:///path/to/my/project/foo#1.1.8",
659            PackageIdSpec {
660                name: String::from("foo"),
661                version: Some("1.1.8".parse().unwrap()),
662                url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
663                kind: Some(SourceKind::Path),
664            },
665            "path+file:///path/to/my/project/foo#1.1.8",
666        );
667        ok(
668            "path+file:///path/to/my/project/foo#bar",
669            PackageIdSpec {
670                name: String::from("bar"),
671                version: None,
672                url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
673                kind: Some(SourceKind::Path),
674            },
675            "path+file:///path/to/my/project/foo#bar",
676        );
677        ok(
678            "path+file:///path/to/my/project/foo#foo::bar",
679            PackageIdSpec {
680                name: String::from("foo::bar"),
681                version: None,
682                url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
683                kind: Some(SourceKind::Path),
684            },
685            "path+file:///path/to/my/project/foo#foo::bar",
686        );
687        ok(
688            "path+file:///path/to/my/project/foo#bar:1.1.8",
689            PackageIdSpec {
690                name: String::from("bar"),
691                version: Some("1.1.8".parse().unwrap()),
692                url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
693                kind: Some(SourceKind::Path),
694            },
695            "path+file:///path/to/my/project/foo#bar@1.1.8",
696        );
697        ok(
698            "path+file:///path/to/my/project/foo#foo::bar:1.1.8",
699            PackageIdSpec {
700                name: String::from("foo::bar"),
701                version: Some("1.1.8".parse().unwrap()),
702                url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
703                kind: Some(SourceKind::Path),
704            },
705            "path+file:///path/to/my/project/foo#foo::bar@1.1.8",
706        );
707        ok(
708            "path+file:///path/to/my/project/foo#bar@1.1.8",
709            PackageIdSpec {
710                name: String::from("bar"),
711                version: Some("1.1.8".parse().unwrap()),
712                url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
713                kind: Some(SourceKind::Path),
714            },
715            "path+file:///path/to/my/project/foo#bar@1.1.8",
716        );
717        ok(
718            "path+file:///path/to/my/project/foo#foo::bar@1.1.8",
719            PackageIdSpec {
720                name: String::from("foo::bar"),
721                version: Some("1.1.8".parse().unwrap()),
722                url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
723                kind: Some(SourceKind::Path),
724            },
725            "path+file:///path/to/my/project/foo#foo::bar@1.1.8",
726        );
727    }
728
729    #[test]
730    fn bad_parsing() {
731        err!("baz:", ErrorKind::PartialVersion(_));
732        err!("baz:*", ErrorKind::PartialVersion(_));
733        err!("baz@", ErrorKind::PartialVersion(_));
734        err!("baz@*", ErrorKind::PartialVersion(_));
735        err!("baz@^1.0", ErrorKind::PartialVersion(_));
736        err!("https://baz:1.0", ErrorKind::NameValidation(_));
737        err!("https://#baz:1.0", ErrorKind::NameValidation(_));
738        err!(
739            "foobar+https://github.com/rust-lang/crates.io-index",
740            ErrorKind::UnsupportedProtocol(_)
741        );
742        err!(
743            "path+https://github.com/rust-lang/crates.io-index",
744            ErrorKind::UnsupportedPathPlusScheme(_)
745        );
746
747        // Only `git+` can use `?`
748        err!(
749            "file:///path/to/my/project/foo?branch=dev",
750            ErrorKind::UnexpectedQueryString(_)
751        );
752        err!(
753            "path+file:///path/to/my/project/foo?branch=dev",
754            ErrorKind::UnexpectedQueryString(_)
755        );
756        err!(
757            "registry+https://github.com/rust-lang/cargo?branch=dev#0.52.0",
758            ErrorKind::UnexpectedQueryString(_)
759        );
760        err!(
761            "sparse+https://github.com/rust-lang/cargo?branch=dev#0.52.0",
762            ErrorKind::UnexpectedQueryString(_)
763        );
764        err!("@1.2.3", ErrorKind::NameValidation(_));
765        err!("registry+https://github.com", ErrorKind::NameValidation(_));
766        err!("https://crates.io/1foo#1.2.3", ErrorKind::NameValidation(_));
767    }
768}