Skip to main content

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                        let Some(f) = fragment.chars().next() else {
163                            return Err(PackageIdSpecError(ErrorKind::EmptyFragment));
164                        };
165
166                        if f.is_alphabetic() {
167                            (String::from(fragment.as_str()), None)
168                        } else {
169                            let version = fragment.parse::<PartialVersion>()?;
170                            (String::from(path_name), Some(version))
171                        }
172                    }
173                },
174                None => (String::from(path_name), None),
175            }
176        };
177        PackageName::new(&name)?;
178        Ok(PackageIdSpec {
179            name,
180            version,
181            url: Some(url),
182            kind,
183        })
184    }
185
186    pub fn name(&self) -> &str {
187        self.name.as_str()
188    }
189
190    /// Full `semver::Version`, if present
191    pub fn version(&self) -> Option<Version> {
192        self.version.as_ref().and_then(|v| v.to_version())
193    }
194
195    pub fn partial_version(&self) -> Option<&PartialVersion> {
196        self.version.as_ref()
197    }
198
199    pub fn url(&self) -> Option<&Url> {
200        self.url.as_ref()
201    }
202
203    pub fn set_url(&mut self, url: Url) {
204        self.url = Some(url);
205    }
206
207    pub fn kind(&self) -> Option<&SourceKind> {
208        self.kind.as_ref()
209    }
210
211    pub fn set_kind(&mut self, kind: SourceKind) {
212        self.kind = Some(kind);
213    }
214}
215
216fn parse_spec(spec: &str) -> Result<Option<(String, Option<PartialVersion>)>> {
217    let Some((name, ver)) = spec
218        .rsplit_once('@')
219        .or_else(|| spec.rsplit_once(':').filter(|(n, _)| !n.ends_with(':')))
220    else {
221        return Ok(None);
222    };
223    let name = name.to_owned();
224    let ver = ver.parse::<PartialVersion>()?;
225    Ok(Some((name, Some(ver))))
226}
227
228fn strip_url_protocol(url: &Url) -> Url {
229    // Ridiculous hoop because `Url::set_scheme` errors when changing to http/https
230    let raw = url.to_string();
231    raw.split_once('+').unwrap().1.parse().unwrap()
232}
233
234impl fmt::Display for PackageIdSpec {
235    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
236        let mut printed_name = false;
237        match self.url {
238            Some(ref url) => {
239                if let Some(protocol) = self.kind.as_ref().and_then(|k| k.protocol()) {
240                    write!(f, "{protocol}+")?;
241                }
242                write!(f, "{}", url)?;
243                if let Some(SourceKind::Git(git_ref)) = self.kind.as_ref() {
244                    if let Some(pretty) = git_ref.pretty_ref(true) {
245                        write!(f, "?{}", pretty)?;
246                    }
247                }
248                if url.path_segments().unwrap().next_back().unwrap() != &*self.name {
249                    printed_name = true;
250                    write!(f, "#{}", self.name)?;
251                }
252            }
253            None => {
254                printed_name = true;
255                write!(f, "{}", self.name)?;
256            }
257        }
258        if let Some(ref v) = self.version {
259            write!(f, "{}{}", if printed_name { "@" } else { "#" }, v)?;
260        }
261        Ok(())
262    }
263}
264
265impl ser::Serialize for PackageIdSpec {
266    fn serialize<S>(&self, s: S) -> std::result::Result<S::Ok, S::Error>
267    where
268        S: ser::Serializer,
269    {
270        self.to_string().serialize(s)
271    }
272}
273
274impl<'de> de::Deserialize<'de> for PackageIdSpec {
275    fn deserialize<D>(d: D) -> std::result::Result<PackageIdSpec, D::Error>
276    where
277        D: de::Deserializer<'de>,
278    {
279        let string = String::deserialize(d)?;
280        PackageIdSpec::parse(&string).map_err(de::Error::custom)
281    }
282}
283
284#[cfg(feature = "unstable-schema")]
285impl schemars::JsonSchema for PackageIdSpec {
286    fn schema_name() -> std::borrow::Cow<'static, str> {
287        "PackageIdSpec".into()
288    }
289    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
290        <String as schemars::JsonSchema>::json_schema(generator)
291    }
292}
293
294#[derive(Debug, thiserror::Error)]
295#[error(transparent)]
296pub struct PackageIdSpecError(#[from] ErrorKind);
297
298impl From<PartialVersionError> for PackageIdSpecError {
299    fn from(value: PartialVersionError) -> Self {
300        ErrorKind::PartialVersion(value).into()
301    }
302}
303
304impl From<NameValidationError> for PackageIdSpecError {
305    fn from(value: NameValidationError) -> Self {
306        ErrorKind::NameValidation(value).into()
307    }
308}
309
310/// Non-public error kind for [`PackageIdSpecError`].
311#[non_exhaustive]
312#[derive(Debug, thiserror::Error)]
313enum ErrorKind {
314    #[error("unsupported source protocol: {0}")]
315    UnsupportedProtocol(String),
316
317    #[error("`path+{0}` is unsupported; `path+file` and `file` schemes are supported")]
318    UnsupportedPathPlusScheme(String),
319
320    #[error("cannot have a query string in a pkgid: {0}")]
321    UnexpectedQueryString(Url),
322
323    #[error("pkgid urls must have at least one path component: {0}")]
324    MissingUrlPath(Url),
325
326    #[error("package ID specification `{spec}` looks like a file path, maybe try {maybe_url}")]
327    MaybeFilePath { spec: String, maybe_url: String },
328
329    #[error("pkgid url cannot have an empty fragment")]
330    EmptyFragment,
331
332    #[error(transparent)]
333    NameValidation(#[from] crate::restricted_names::NameValidationError),
334
335    #[error(transparent)]
336    PartialVersion(#[from] crate::core::PartialVersionError),
337}
338
339#[cfg(test)]
340mod tests {
341    use super::ErrorKind;
342    use super::PackageIdSpec;
343    use crate::core::{GitReference, SourceKind};
344    use url::Url;
345
346    #[track_caller]
347    fn ok(spec: &str, expected: PackageIdSpec, expected_rendered: &str) {
348        let parsed = PackageIdSpec::parse(spec).unwrap();
349        assert_eq!(parsed, expected);
350        let rendered = parsed.to_string();
351        assert_eq!(rendered, expected_rendered);
352        let reparsed = PackageIdSpec::parse(&rendered).unwrap();
353        assert_eq!(reparsed, expected);
354    }
355
356    macro_rules! err {
357        ($spec:expr, $expected:pat) => {
358            let err = PackageIdSpec::parse($spec).unwrap_err();
359            let kind = err.0;
360            assert!(
361                matches!(kind, $expected),
362                "`{}` parse error mismatch, got {kind:?}",
363                $spec
364            );
365        };
366    }
367
368    #[test]
369    fn good_parsing() {
370        ok(
371            "https://crates.io/foo",
372            PackageIdSpec {
373                name: String::from("foo"),
374                version: None,
375                url: Some(Url::parse("https://crates.io/foo").unwrap()),
376                kind: None,
377            },
378            "https://crates.io/foo",
379        );
380        ok(
381            "https://crates.io/foo#1.2.3",
382            PackageIdSpec {
383                name: String::from("foo"),
384                version: Some("1.2.3".parse().unwrap()),
385                url: Some(Url::parse("https://crates.io/foo").unwrap()),
386                kind: None,
387            },
388            "https://crates.io/foo#1.2.3",
389        );
390        ok(
391            "https://crates.io/foo#1.2",
392            PackageIdSpec {
393                name: String::from("foo"),
394                version: Some("1.2".parse().unwrap()),
395                url: Some(Url::parse("https://crates.io/foo").unwrap()),
396                kind: None,
397            },
398            "https://crates.io/foo#1.2",
399        );
400        ok(
401            "https://crates.io/foo#bar:1.2.3",
402            PackageIdSpec {
403                name: String::from("bar"),
404                version: Some("1.2.3".parse().unwrap()),
405                url: Some(Url::parse("https://crates.io/foo").unwrap()),
406                kind: None,
407            },
408            "https://crates.io/foo#bar@1.2.3",
409        );
410        ok(
411            "https://crates.io/foo#bar@1.2.3",
412            PackageIdSpec {
413                name: String::from("bar"),
414                version: Some("1.2.3".parse().unwrap()),
415                url: Some(Url::parse("https://crates.io/foo").unwrap()),
416                kind: None,
417            },
418            "https://crates.io/foo#bar@1.2.3",
419        );
420        ok(
421            "https://crates.io/foo#bar@1.2",
422            PackageIdSpec {
423                name: String::from("bar"),
424                version: Some("1.2".parse().unwrap()),
425                url: Some(Url::parse("https://crates.io/foo").unwrap()),
426                kind: None,
427            },
428            "https://crates.io/foo#bar@1.2",
429        );
430        ok(
431            "registry+https://crates.io/foo#bar@1.2",
432            PackageIdSpec {
433                name: String::from("bar"),
434                version: Some("1.2".parse().unwrap()),
435                url: Some(Url::parse("https://crates.io/foo").unwrap()),
436                kind: Some(SourceKind::Registry),
437            },
438            "registry+https://crates.io/foo#bar@1.2",
439        );
440        ok(
441            "sparse+https://crates.io/foo#bar@1.2",
442            PackageIdSpec {
443                name: String::from("bar"),
444                version: Some("1.2".parse().unwrap()),
445                url: Some(Url::parse("sparse+https://crates.io/foo").unwrap()),
446                kind: Some(SourceKind::SparseRegistry),
447            },
448            "sparse+https://crates.io/foo#bar@1.2",
449        );
450        ok(
451            "foo",
452            PackageIdSpec {
453                name: String::from("foo"),
454                version: None,
455                url: None,
456                kind: None,
457            },
458            "foo",
459        );
460        ok(
461            "foo::bar",
462            PackageIdSpec {
463                name: String::from("foo::bar"),
464                version: None,
465                url: None,
466                kind: None,
467            },
468            "foo::bar",
469        );
470        ok(
471            "foo:1.2.3",
472            PackageIdSpec {
473                name: String::from("foo"),
474                version: Some("1.2.3".parse().unwrap()),
475                url: None,
476                kind: None,
477            },
478            "foo@1.2.3",
479        );
480        ok(
481            "foo::bar:1.2.3",
482            PackageIdSpec {
483                name: String::from("foo::bar"),
484                version: Some("1.2.3".parse().unwrap()),
485                url: None,
486                kind: None,
487            },
488            "foo::bar@1.2.3",
489        );
490        ok(
491            "foo@1.2.3",
492            PackageIdSpec {
493                name: String::from("foo"),
494                version: Some("1.2.3".parse().unwrap()),
495                url: None,
496                kind: None,
497            },
498            "foo@1.2.3",
499        );
500        ok(
501            "foo::bar@1.2.3",
502            PackageIdSpec {
503                name: String::from("foo::bar"),
504                version: Some("1.2.3".parse().unwrap()),
505                url: None,
506                kind: None,
507            },
508            "foo::bar@1.2.3",
509        );
510        ok(
511            "foo@1.2",
512            PackageIdSpec {
513                name: String::from("foo"),
514                version: Some("1.2".parse().unwrap()),
515                url: None,
516                kind: None,
517            },
518            "foo@1.2",
519        );
520
521        // pkgid-spec.md
522        ok(
523            "regex",
524            PackageIdSpec {
525                name: String::from("regex"),
526                version: None,
527                url: None,
528                kind: None,
529            },
530            "regex",
531        );
532        ok(
533            "regex@1.4",
534            PackageIdSpec {
535                name: String::from("regex"),
536                version: Some("1.4".parse().unwrap()),
537                url: None,
538                kind: None,
539            },
540            "regex@1.4",
541        );
542        ok(
543            "regex@1.4.3",
544            PackageIdSpec {
545                name: String::from("regex"),
546                version: Some("1.4.3".parse().unwrap()),
547                url: None,
548                kind: None,
549            },
550            "regex@1.4.3",
551        );
552        ok(
553            "https://github.com/rust-lang/crates.io-index#regex",
554            PackageIdSpec {
555                name: String::from("regex"),
556                version: None,
557                url: Some(Url::parse("https://github.com/rust-lang/crates.io-index").unwrap()),
558                kind: None,
559            },
560            "https://github.com/rust-lang/crates.io-index#regex",
561        );
562        ok(
563            "https://github.com/rust-lang/crates.io-index#regex@1.4.3",
564            PackageIdSpec {
565                name: String::from("regex"),
566                version: Some("1.4.3".parse().unwrap()),
567                url: Some(Url::parse("https://github.com/rust-lang/crates.io-index").unwrap()),
568                kind: None,
569            },
570            "https://github.com/rust-lang/crates.io-index#regex@1.4.3",
571        );
572        ok(
573            "sparse+https://github.com/rust-lang/crates.io-index#regex@1.4.3",
574            PackageIdSpec {
575                name: String::from("regex"),
576                version: Some("1.4.3".parse().unwrap()),
577                url: Some(
578                    Url::parse("sparse+https://github.com/rust-lang/crates.io-index").unwrap(),
579                ),
580                kind: Some(SourceKind::SparseRegistry),
581            },
582            "sparse+https://github.com/rust-lang/crates.io-index#regex@1.4.3",
583        );
584        ok(
585            "https://github.com/rust-lang/cargo#0.52.0",
586            PackageIdSpec {
587                name: String::from("cargo"),
588                version: Some("0.52.0".parse().unwrap()),
589                url: Some(Url::parse("https://github.com/rust-lang/cargo").unwrap()),
590                kind: None,
591            },
592            "https://github.com/rust-lang/cargo#0.52.0",
593        );
594        ok(
595            "https://github.com/rust-lang/cargo#cargo-platform@0.1.2",
596            PackageIdSpec {
597                name: String::from("cargo-platform"),
598                version: Some("0.1.2".parse().unwrap()),
599                url: Some(Url::parse("https://github.com/rust-lang/cargo").unwrap()),
600                kind: None,
601            },
602            "https://github.com/rust-lang/cargo#cargo-platform@0.1.2",
603        );
604        ok(
605            "ssh://git@github.com/rust-lang/regex.git#regex@1.4.3",
606            PackageIdSpec {
607                name: String::from("regex"),
608                version: Some("1.4.3".parse().unwrap()),
609                url: Some(Url::parse("ssh://git@github.com/rust-lang/regex.git").unwrap()),
610                kind: None,
611            },
612            "ssh://git@github.com/rust-lang/regex.git#regex@1.4.3",
613        );
614        ok(
615            "git+ssh://git@github.com/rust-lang/regex.git#regex@1.4.3",
616            PackageIdSpec {
617                name: String::from("regex"),
618                version: Some("1.4.3".parse().unwrap()),
619                url: Some(Url::parse("ssh://git@github.com/rust-lang/regex.git").unwrap()),
620                kind: Some(SourceKind::Git(GitReference::DefaultBranch)),
621            },
622            "git+ssh://git@github.com/rust-lang/regex.git#regex@1.4.3",
623        );
624        ok(
625            "git+ssh://git@github.com/rust-lang/regex.git?branch=dev#regex@1.4.3",
626            PackageIdSpec {
627                name: String::from("regex"),
628                version: Some("1.4.3".parse().unwrap()),
629                url: Some(Url::parse("ssh://git@github.com/rust-lang/regex.git").unwrap()),
630                kind: Some(SourceKind::Git(GitReference::Branch("dev".to_owned()))),
631            },
632            "git+ssh://git@github.com/rust-lang/regex.git?branch=dev#regex@1.4.3",
633        );
634        ok(
635            "file:///path/to/my/project/foo",
636            PackageIdSpec {
637                name: String::from("foo"),
638                version: None,
639                url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
640                kind: None,
641            },
642            "file:///path/to/my/project/foo",
643        );
644        ok(
645            "file:///path/to/my/project/foo::bar",
646            PackageIdSpec {
647                name: String::from("foo::bar"),
648                version: None,
649                url: Some(Url::parse("file:///path/to/my/project/foo::bar").unwrap()),
650                kind: None,
651            },
652            "file:///path/to/my/project/foo::bar",
653        );
654        ok(
655            "file:///path/to/my/project/foo#1.1.8",
656            PackageIdSpec {
657                name: String::from("foo"),
658                version: Some("1.1.8".parse().unwrap()),
659                url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
660                kind: None,
661            },
662            "file:///path/to/my/project/foo#1.1.8",
663        );
664        ok(
665            "path+file:///path/to/my/project/foo#1.1.8",
666            PackageIdSpec {
667                name: String::from("foo"),
668                version: Some("1.1.8".parse().unwrap()),
669                url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
670                kind: Some(SourceKind::Path),
671            },
672            "path+file:///path/to/my/project/foo#1.1.8",
673        );
674        ok(
675            "path+file:///path/to/my/project/foo#bar",
676            PackageIdSpec {
677                name: String::from("bar"),
678                version: None,
679                url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
680                kind: Some(SourceKind::Path),
681            },
682            "path+file:///path/to/my/project/foo#bar",
683        );
684        ok(
685            "path+file:///path/to/my/project/foo#foo::bar",
686            PackageIdSpec {
687                name: String::from("foo::bar"),
688                version: None,
689                url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
690                kind: Some(SourceKind::Path),
691            },
692            "path+file:///path/to/my/project/foo#foo::bar",
693        );
694        ok(
695            "path+file:///path/to/my/project/foo#bar:1.1.8",
696            PackageIdSpec {
697                name: String::from("bar"),
698                version: Some("1.1.8".parse().unwrap()),
699                url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
700                kind: Some(SourceKind::Path),
701            },
702            "path+file:///path/to/my/project/foo#bar@1.1.8",
703        );
704        ok(
705            "path+file:///path/to/my/project/foo#foo::bar:1.1.8",
706            PackageIdSpec {
707                name: String::from("foo::bar"),
708                version: Some("1.1.8".parse().unwrap()),
709                url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
710                kind: Some(SourceKind::Path),
711            },
712            "path+file:///path/to/my/project/foo#foo::bar@1.1.8",
713        );
714        ok(
715            "path+file:///path/to/my/project/foo#bar@1.1.8",
716            PackageIdSpec {
717                name: String::from("bar"),
718                version: Some("1.1.8".parse().unwrap()),
719                url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
720                kind: Some(SourceKind::Path),
721            },
722            "path+file:///path/to/my/project/foo#bar@1.1.8",
723        );
724        ok(
725            "path+file:///path/to/my/project/foo#foo::bar@1.1.8",
726            PackageIdSpec {
727                name: String::from("foo::bar"),
728                version: Some("1.1.8".parse().unwrap()),
729                url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
730                kind: Some(SourceKind::Path),
731            },
732            "path+file:///path/to/my/project/foo#foo::bar@1.1.8",
733        );
734    }
735
736    #[test]
737    fn bad_parsing() {
738        err!("baz:", ErrorKind::PartialVersion(_));
739        err!("baz:*", ErrorKind::PartialVersion(_));
740        err!("baz@", ErrorKind::PartialVersion(_));
741        err!("baz@*", ErrorKind::PartialVersion(_));
742        err!("baz@^1.0", ErrorKind::PartialVersion(_));
743        err!("https://baz:1.0", ErrorKind::NameValidation(_));
744        err!("https://#baz:1.0", ErrorKind::NameValidation(_));
745        err!(
746            "foobar+https://github.com/rust-lang/crates.io-index",
747            ErrorKind::UnsupportedProtocol(_)
748        );
749        err!(
750            "path+https://github.com/rust-lang/crates.io-index",
751            ErrorKind::UnsupportedPathPlusScheme(_)
752        );
753
754        // Only `git+` can use `?`
755        err!(
756            "file:///path/to/my/project/foo?branch=dev",
757            ErrorKind::UnexpectedQueryString(_)
758        );
759        err!(
760            "path+file:///path/to/my/project/foo?branch=dev",
761            ErrorKind::UnexpectedQueryString(_)
762        );
763        err!(
764            "registry+https://github.com/rust-lang/cargo?branch=dev#0.52.0",
765            ErrorKind::UnexpectedQueryString(_)
766        );
767        err!(
768            "sparse+https://github.com/rust-lang/cargo?branch=dev#0.52.0",
769            ErrorKind::UnexpectedQueryString(_)
770        );
771        err!("@1.2.3", ErrorKind::NameValidation(_));
772        err!("registry+https://github.com", ErrorKind::NameValidation(_));
773        err!("https://crates.io/1foo#1.2.3", ErrorKind::NameValidation(_));
774        err!("https://example.com/foo#", ErrorKind::EmptyFragment);
775    }
776}