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    ///     "builtin://.#core"
77    /// ];
78    /// for spec in specs {
79    ///     assert!(PackageIdSpec::parse(spec).is_ok());
80    /// }
81    pub fn parse(spec: &str) -> Result<PackageIdSpec> {
82        if spec.contains("://") {
83            if let Ok(url) = Url::parse(spec) {
84                return PackageIdSpec::from_url(url);
85            }
86        } else if spec.contains('/') || spec.contains('\\') {
87            let abs = std::env::current_dir().unwrap_or_default().join(spec);
88            if abs.exists() {
89                let maybe_url = Url::from_file_path(abs)
90                    .map_or_else(|_| "a file:// URL".to_string(), |url| url.to_string());
91                return Err(ErrorKind::MaybeFilePath {
92                    spec: spec.into(),
93                    maybe_url,
94                }
95                .into());
96            }
97        }
98        let (name, version) = match parse_spec(spec)? {
99            Some((name, ver)) => (name, Some(ver)),
100            None => (spec.to_owned(), None),
101        };
102        PackageName::new(&name)?;
103        Ok(PackageIdSpec {
104            name: String::from(name),
105            version,
106            url: None,
107            kind: None,
108        })
109    }
110
111    /// Tries to convert a valid `Url` to a `PackageIdSpec`.
112    fn from_url(mut url: Url) -> Result<PackageIdSpec> {
113        let mut kind = None;
114        if let Some((kind_str, scheme)) = url.scheme().split_once('+') {
115            match kind_str {
116                "git" => {
117                    let git_ref = GitReference::from_query(url.query_pairs());
118                    url.set_query(None);
119                    kind = Some(SourceKind::Git(git_ref));
120                    url = strip_url_protocol(&url)?;
121                }
122                "registry" => {
123                    if url.query().is_some() {
124                        return Err(ErrorKind::UnexpectedQueryString(url).into());
125                    }
126                    kind = Some(SourceKind::Registry);
127                    url = strip_url_protocol(&url)?;
128                }
129                "sparse" => {
130                    if url.query().is_some() {
131                        return Err(ErrorKind::UnexpectedQueryString(url).into());
132                    }
133                    kind = Some(SourceKind::SparseRegistry);
134                    // Leave `sparse` as part of URL, see `SourceId::new`
135                    // url = strip_url_protocol(&url);
136                }
137                "path" => {
138                    if url.query().is_some() {
139                        return Err(ErrorKind::UnexpectedQueryString(url).into());
140                    }
141                    if scheme != "file" {
142                        return Err(ErrorKind::UnsupportedPathPlusScheme(scheme.into()).into());
143                    }
144                    kind = Some(SourceKind::Path);
145                    url = strip_url_protocol(&url)?;
146                }
147                "builtin" => {
148                    if url.query().is_some() {
149                        return Err(ErrorKind::UnexpectedQueryString(url).into());
150                    }
151                    if scheme != "builtin" {
152                        return Err(ErrorKind::UnsupportedBuiltinScheme(scheme.into()).into());
153                    }
154                    kind = Some(SourceKind::Builtin);
155                    url = strip_url_protocol(&url)?;
156                }
157                kind => return Err(ErrorKind::UnsupportedProtocol(kind.into()).into()),
158            }
159        } else if url.scheme() == "builtin" {
160            if url.query().is_some() {
161                return Err(ErrorKind::UnexpectedQueryString(url).into());
162            }
163            kind = Some(SourceKind::Builtin)
164        } else if url.query().is_some() {
165            return Err(ErrorKind::UnexpectedQueryString(url).into());
166        }
167
168        let frag = url.fragment().map(|s| s.to_owned());
169        url.set_fragment(None);
170
171        if kind == Some(SourceKind::Builtin) {
172            // Builtins are different in that they require a fragment and cannot have path segments,
173            // so handle them early
174            if url.host_str() != Some(".") {
175                return Err(ErrorKind::InvalidHostname.into());
176            }
177            if url.path() != "" {
178                return Err(ErrorKind::InvalidPath.into());
179            }
180            let Some(frag) = frag else {
181                return Err(ErrorKind::FragmentRequired.into());
182            };
183
184            if frag.is_empty() {
185                return Err(ErrorKind::EmptyFragment.into());
186            }
187            let name = match parse_spec(&frag)? {
188                Some((name, ver)) => {
189                    if !ver.matches(&Version::new(0, 0, 0)) {
190                        return Err(ErrorKind::InvalidVersion(ver.to_string()).into());
191                    }
192                    name
193                }
194                None => frag,
195            };
196
197            PackageName::new(&name)?;
198            let version = PartialVersion {
199                major: 0,
200                minor: Some(0),
201                patch: Some(0),
202                pre: None,
203                build: None,
204            };
205            return Ok(PackageIdSpec {
206                name,
207                version: Some(version),
208                url: Some(url),
209                kind,
210            });
211        }
212
213        let (name, version) = {
214            let Some(path_name) = url.path_segments().and_then(|mut p| p.next_back()) else {
215                return Err(ErrorKind::MissingUrlPath(url).into());
216            };
217            match frag {
218                Some(fragment) => match parse_spec(&fragment)? {
219                    Some((name, ver)) => (name, Some(ver)),
220                    None => {
221                        let Some(f) = fragment.chars().next() else {
222                            return Err(PackageIdSpecError(ErrorKind::EmptyFragment));
223                        };
224
225                        if f.is_alphabetic() {
226                            (String::from(fragment.as_str()), None)
227                        } else {
228                            let version = fragment.parse::<PartialVersion>()?;
229                            (String::from(path_name), Some(version))
230                        }
231                    }
232                },
233                None => (String::from(path_name), None),
234            }
235        };
236        PackageName::new(&name)?;
237        Ok(PackageIdSpec {
238            name,
239            version,
240            url: Some(url),
241            kind,
242        })
243    }
244
245    pub fn name(&self) -> &str {
246        self.name.as_str()
247    }
248
249    /// Full `semver::Version`, if present
250    pub fn version(&self) -> Option<Version> {
251        self.version.as_ref().and_then(|v| v.to_version())
252    }
253
254    pub fn partial_version(&self) -> Option<&PartialVersion> {
255        self.version.as_ref()
256    }
257
258    pub fn url(&self) -> Option<&Url> {
259        self.url.as_ref()
260    }
261
262    pub fn set_url(&mut self, url: Url) {
263        self.url = Some(url);
264    }
265
266    pub fn kind(&self) -> Option<&SourceKind> {
267        self.kind.as_ref()
268    }
269
270    pub fn set_kind(&mut self, kind: SourceKind) {
271        self.kind = Some(kind);
272    }
273}
274
275fn parse_spec(spec: &str) -> Result<Option<(String, PartialVersion)>> {
276    let Some((name, ver)) = spec
277        .rsplit_once('@')
278        .or_else(|| spec.rsplit_once(':').filter(|(n, _)| !n.ends_with(':')))
279    else {
280        return Ok(None);
281    };
282    let name = name.to_owned();
283    let ver = ver.parse::<PartialVersion>()?;
284    Ok(Some((name, ver)))
285}
286
287fn strip_url_protocol(url: &Url) -> Result<Url> {
288    // Ridiculous hoop because `Url::set_scheme` errors when changing to http/https
289    let raw = url.to_string();
290    let (_, rest) = raw
291        .split_once('+')
292        .ok_or_else(|| ErrorKind::InvalidPkgIdUrl(raw.clone()))?;
293    rest.parse()
294        .map_err(|_| ErrorKind::InvalidPkgIdUrl(raw).into())
295}
296
297impl fmt::Display for PackageIdSpec {
298    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
299        let mut printed_name = false;
300        match self.url {
301            Some(ref url) => {
302                if let Some(protocol) = self.kind.as_ref().and_then(SourceKind::protocol) {
303                    write!(f, "{protocol}+")?;
304                }
305                write!(f, "{}", url)?;
306                if let Some(SourceKind::Git(git_ref)) = self.kind.as_ref() {
307                    if let Some(pretty) = git_ref.pretty_ref(true) {
308                        write!(f, "?{}", pretty)?;
309                    }
310                }
311                if self.kind() == Some(&SourceKind::Builtin)
312                    || url.path_segments().unwrap().next_back().unwrap() != &*self.name
313                {
314                    printed_name = true;
315                    write!(f, "#{}", self.name)?;
316                }
317            }
318            None => {
319                printed_name = true;
320                write!(f, "{}", self.name)?;
321            }
322        }
323        if let Some(ref v) = self.version
324            && self.kind != Some(SourceKind::Builtin)
325        {
326            write!(f, "{}{}", if printed_name { "@" } else { "#" }, v)?;
327        }
328        Ok(())
329    }
330}
331
332impl ser::Serialize for PackageIdSpec {
333    fn serialize<S>(&self, s: S) -> std::result::Result<S::Ok, S::Error>
334    where
335        S: ser::Serializer,
336    {
337        self.to_string().serialize(s)
338    }
339}
340
341impl<'de> de::Deserialize<'de> for PackageIdSpec {
342    fn deserialize<D>(d: D) -> std::result::Result<PackageIdSpec, D::Error>
343    where
344        D: de::Deserializer<'de>,
345    {
346        let string = String::deserialize(d)?;
347        PackageIdSpec::parse(&string).map_err(de::Error::custom)
348    }
349}
350
351#[cfg(feature = "unstable-schema")]
352impl schemars::JsonSchema for PackageIdSpec {
353    fn schema_name() -> std::borrow::Cow<'static, str> {
354        "PackageIdSpec".into()
355    }
356    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
357        <String as schemars::JsonSchema>::json_schema(generator)
358    }
359}
360
361#[derive(Debug, thiserror::Error)]
362#[error(transparent)]
363pub struct PackageIdSpecError(#[from] ErrorKind);
364
365impl From<PartialVersionError> for PackageIdSpecError {
366    fn from(value: PartialVersionError) -> Self {
367        ErrorKind::PartialVersion(value).into()
368    }
369}
370
371impl From<NameValidationError> for PackageIdSpecError {
372    fn from(value: NameValidationError) -> Self {
373        ErrorKind::NameValidation(value).into()
374    }
375}
376
377/// Non-public error kind for [`PackageIdSpecError`].
378#[non_exhaustive]
379#[derive(Debug, thiserror::Error)]
380enum ErrorKind {
381    #[error("unsupported source protocol: {0}")]
382    UnsupportedProtocol(String),
383
384    #[error("`path+{0}` is unsupported; `path+file` and `file` schemes are supported")]
385    UnsupportedPathPlusScheme(String),
386
387    #[error("cannot have a query string in a pkgid: {0}")]
388    UnexpectedQueryString(Url),
389
390    #[error("pkgid urls must have at least one path component: {0}")]
391    MissingUrlPath(Url),
392
393    #[error("package ID specification `{spec}` looks like a file path, maybe try {maybe_url}")]
394    MaybeFilePath { spec: String, maybe_url: String },
395
396    #[error("pkgid url cannot have an empty fragment")]
397    EmptyFragment,
398
399    #[error("builtin package ID specifications must specify the package name in the fragment")]
400    FragmentRequired,
401
402    #[error("version `{0}` is invalid for builtin packages, which are unversioned (or `0.0.0`)")]
403    InvalidVersion(String),
404
405    #[error("only `.` is permitted as a hostname for builtin package ID specifications")]
406    InvalidHostname,
407
408    #[error("only empty paths are permitted for builtin package ID specifications")]
409    InvalidPath,
410
411    #[error(
412        "`builtin+{0}` is unsupported - only the `builtin` protocol is supported for `builtin` kinds"
413    )]
414    UnsupportedBuiltinScheme(String),
415
416    #[error(transparent)]
417    NameValidation(#[from] crate::restricted_names::NameValidationError),
418
419    #[error(transparent)]
420    PartialVersion(#[from] crate::core::PartialVersionError),
421
422    #[error("invalid pkgid url: {0}")]
423    InvalidPkgIdUrl(String),
424}
425
426#[cfg(test)]
427mod tests {
428    use super::ErrorKind;
429    use super::PackageIdSpec;
430    use crate::core::{GitReference, SourceKind};
431    use url::Url;
432
433    #[track_caller]
434    fn ok(spec: &str, expected: PackageIdSpec, expected_rendered: &str) {
435        let parsed = PackageIdSpec::parse(spec).unwrap();
436        assert_eq!(parsed, expected);
437        let rendered = parsed.to_string();
438        assert_eq!(rendered, expected_rendered);
439        let reparsed = PackageIdSpec::parse(&rendered).unwrap();
440        assert_eq!(reparsed, expected);
441    }
442
443    macro_rules! err {
444        ($spec:expr, $expected:pat) => {
445            let err = PackageIdSpec::parse($spec).unwrap_err();
446            let kind = err.0;
447            assert!(
448                matches!(kind, $expected),
449                "`{}` parse error mismatch, got {kind:?}",
450                $spec
451            );
452        };
453    }
454
455    #[test]
456    fn good_parsing() {
457        ok(
458            "https://crates.io/foo",
459            PackageIdSpec {
460                name: String::from("foo"),
461                version: None,
462                url: Some(Url::parse("https://crates.io/foo").unwrap()),
463                kind: None,
464            },
465            "https://crates.io/foo",
466        );
467        ok(
468            "https://crates.io/foo#1.2.3",
469            PackageIdSpec {
470                name: String::from("foo"),
471                version: Some("1.2.3".parse().unwrap()),
472                url: Some(Url::parse("https://crates.io/foo").unwrap()),
473                kind: None,
474            },
475            "https://crates.io/foo#1.2.3",
476        );
477        ok(
478            "https://crates.io/foo#1.2",
479            PackageIdSpec {
480                name: String::from("foo"),
481                version: Some("1.2".parse().unwrap()),
482                url: Some(Url::parse("https://crates.io/foo").unwrap()),
483                kind: None,
484            },
485            "https://crates.io/foo#1.2",
486        );
487        ok(
488            "https://crates.io/foo#bar:1.2.3",
489            PackageIdSpec {
490                name: String::from("bar"),
491                version: Some("1.2.3".parse().unwrap()),
492                url: Some(Url::parse("https://crates.io/foo").unwrap()),
493                kind: None,
494            },
495            "https://crates.io/foo#bar@1.2.3",
496        );
497        ok(
498            "https://crates.io/foo#bar@1.2.3",
499            PackageIdSpec {
500                name: String::from("bar"),
501                version: Some("1.2.3".parse().unwrap()),
502                url: Some(Url::parse("https://crates.io/foo").unwrap()),
503                kind: None,
504            },
505            "https://crates.io/foo#bar@1.2.3",
506        );
507        ok(
508            "https://crates.io/foo#bar@1.2",
509            PackageIdSpec {
510                name: String::from("bar"),
511                version: Some("1.2".parse().unwrap()),
512                url: Some(Url::parse("https://crates.io/foo").unwrap()),
513                kind: None,
514            },
515            "https://crates.io/foo#bar@1.2",
516        );
517        ok(
518            "registry+https://crates.io/foo#bar@1.2",
519            PackageIdSpec {
520                name: String::from("bar"),
521                version: Some("1.2".parse().unwrap()),
522                url: Some(Url::parse("https://crates.io/foo").unwrap()),
523                kind: Some(SourceKind::Registry),
524            },
525            "registry+https://crates.io/foo#bar@1.2",
526        );
527        ok(
528            "sparse+https://crates.io/foo#bar@1.2",
529            PackageIdSpec {
530                name: String::from("bar"),
531                version: Some("1.2".parse().unwrap()),
532                url: Some(Url::parse("sparse+https://crates.io/foo").unwrap()),
533                kind: Some(SourceKind::SparseRegistry),
534            },
535            "sparse+https://crates.io/foo#bar@1.2",
536        );
537        ok(
538            "foo",
539            PackageIdSpec {
540                name: String::from("foo"),
541                version: None,
542                url: None,
543                kind: None,
544            },
545            "foo",
546        );
547        ok(
548            "foo::bar",
549            PackageIdSpec {
550                name: String::from("foo::bar"),
551                version: None,
552                url: None,
553                kind: None,
554            },
555            "foo::bar",
556        );
557        ok(
558            "foo:1.2.3",
559            PackageIdSpec {
560                name: String::from("foo"),
561                version: Some("1.2.3".parse().unwrap()),
562                url: None,
563                kind: None,
564            },
565            "foo@1.2.3",
566        );
567        ok(
568            "foo::bar:1.2.3",
569            PackageIdSpec {
570                name: String::from("foo::bar"),
571                version: Some("1.2.3".parse().unwrap()),
572                url: None,
573                kind: None,
574            },
575            "foo::bar@1.2.3",
576        );
577        ok(
578            "foo@1.2.3",
579            PackageIdSpec {
580                name: String::from("foo"),
581                version: Some("1.2.3".parse().unwrap()),
582                url: None,
583                kind: None,
584            },
585            "foo@1.2.3",
586        );
587        ok(
588            "foo::bar@1.2.3",
589            PackageIdSpec {
590                name: String::from("foo::bar"),
591                version: Some("1.2.3".parse().unwrap()),
592                url: None,
593                kind: None,
594            },
595            "foo::bar@1.2.3",
596        );
597        ok(
598            "foo@1.2",
599            PackageIdSpec {
600                name: String::from("foo"),
601                version: Some("1.2".parse().unwrap()),
602                url: None,
603                kind: None,
604            },
605            "foo@1.2",
606        );
607
608        // pkgid-spec.md
609        ok(
610            "regex",
611            PackageIdSpec {
612                name: String::from("regex"),
613                version: None,
614                url: None,
615                kind: None,
616            },
617            "regex",
618        );
619        ok(
620            "regex@1.4",
621            PackageIdSpec {
622                name: String::from("regex"),
623                version: Some("1.4".parse().unwrap()),
624                url: None,
625                kind: None,
626            },
627            "regex@1.4",
628        );
629        ok(
630            "regex@1.4.3",
631            PackageIdSpec {
632                name: String::from("regex"),
633                version: Some("1.4.3".parse().unwrap()),
634                url: None,
635                kind: None,
636            },
637            "regex@1.4.3",
638        );
639        ok(
640            "https://github.com/rust-lang/crates.io-index#regex",
641            PackageIdSpec {
642                name: String::from("regex"),
643                version: None,
644                url: Some(Url::parse("https://github.com/rust-lang/crates.io-index").unwrap()),
645                kind: None,
646            },
647            "https://github.com/rust-lang/crates.io-index#regex",
648        );
649        ok(
650            "https://github.com/rust-lang/crates.io-index#regex@1.4.3",
651            PackageIdSpec {
652                name: String::from("regex"),
653                version: Some("1.4.3".parse().unwrap()),
654                url: Some(Url::parse("https://github.com/rust-lang/crates.io-index").unwrap()),
655                kind: None,
656            },
657            "https://github.com/rust-lang/crates.io-index#regex@1.4.3",
658        );
659        ok(
660            "sparse+https://github.com/rust-lang/crates.io-index#regex@1.4.3",
661            PackageIdSpec {
662                name: String::from("regex"),
663                version: Some("1.4.3".parse().unwrap()),
664                url: Some(
665                    Url::parse("sparse+https://github.com/rust-lang/crates.io-index").unwrap(),
666                ),
667                kind: Some(SourceKind::SparseRegistry),
668            },
669            "sparse+https://github.com/rust-lang/crates.io-index#regex@1.4.3",
670        );
671        ok(
672            "https://github.com/rust-lang/cargo#0.52.0",
673            PackageIdSpec {
674                name: String::from("cargo"),
675                version: Some("0.52.0".parse().unwrap()),
676                url: Some(Url::parse("https://github.com/rust-lang/cargo").unwrap()),
677                kind: None,
678            },
679            "https://github.com/rust-lang/cargo#0.52.0",
680        );
681        ok(
682            "https://github.com/rust-lang/cargo#cargo-platform@0.1.2",
683            PackageIdSpec {
684                name: String::from("cargo-platform"),
685                version: Some("0.1.2".parse().unwrap()),
686                url: Some(Url::parse("https://github.com/rust-lang/cargo").unwrap()),
687                kind: None,
688            },
689            "https://github.com/rust-lang/cargo#cargo-platform@0.1.2",
690        );
691        ok(
692            "ssh://git@github.com/rust-lang/regex.git#regex@1.4.3",
693            PackageIdSpec {
694                name: String::from("regex"),
695                version: Some("1.4.3".parse().unwrap()),
696                url: Some(Url::parse("ssh://git@github.com/rust-lang/regex.git").unwrap()),
697                kind: None,
698            },
699            "ssh://git@github.com/rust-lang/regex.git#regex@1.4.3",
700        );
701        ok(
702            "git+ssh://git@github.com/rust-lang/regex.git#regex@1.4.3",
703            PackageIdSpec {
704                name: String::from("regex"),
705                version: Some("1.4.3".parse().unwrap()),
706                url: Some(Url::parse("ssh://git@github.com/rust-lang/regex.git").unwrap()),
707                kind: Some(SourceKind::Git(GitReference::DefaultBranch)),
708            },
709            "git+ssh://git@github.com/rust-lang/regex.git#regex@1.4.3",
710        );
711        ok(
712            "git+ssh://git@github.com/rust-lang/regex.git?branch=dev#regex@1.4.3",
713            PackageIdSpec {
714                name: String::from("regex"),
715                version: Some("1.4.3".parse().unwrap()),
716                url: Some(Url::parse("ssh://git@github.com/rust-lang/regex.git").unwrap()),
717                kind: Some(SourceKind::Git(GitReference::Branch("dev".to_owned()))),
718            },
719            "git+ssh://git@github.com/rust-lang/regex.git?branch=dev#regex@1.4.3",
720        );
721        ok(
722            "file:///path/to/my/project/foo",
723            PackageIdSpec {
724                name: String::from("foo"),
725                version: None,
726                url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
727                kind: None,
728            },
729            "file:///path/to/my/project/foo",
730        );
731        ok(
732            "file:///path/to/my/project/foo::bar",
733            PackageIdSpec {
734                name: String::from("foo::bar"),
735                version: None,
736                url: Some(Url::parse("file:///path/to/my/project/foo::bar").unwrap()),
737                kind: None,
738            },
739            "file:///path/to/my/project/foo::bar",
740        );
741        ok(
742            "file:///path/to/my/project/foo#1.1.8",
743            PackageIdSpec {
744                name: String::from("foo"),
745                version: Some("1.1.8".parse().unwrap()),
746                url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
747                kind: None,
748            },
749            "file:///path/to/my/project/foo#1.1.8",
750        );
751        ok(
752            "path+file:///path/to/my/project/foo#1.1.8",
753            PackageIdSpec {
754                name: String::from("foo"),
755                version: Some("1.1.8".parse().unwrap()),
756                url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
757                kind: Some(SourceKind::Path),
758            },
759            "path+file:///path/to/my/project/foo#1.1.8",
760        );
761        ok(
762            "path+file:///path/to/my/project/foo#bar",
763            PackageIdSpec {
764                name: String::from("bar"),
765                version: None,
766                url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
767                kind: Some(SourceKind::Path),
768            },
769            "path+file:///path/to/my/project/foo#bar",
770        );
771        ok(
772            "path+file:///path/to/my/project/foo#foo::bar",
773            PackageIdSpec {
774                name: String::from("foo::bar"),
775                version: None,
776                url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
777                kind: Some(SourceKind::Path),
778            },
779            "path+file:///path/to/my/project/foo#foo::bar",
780        );
781        ok(
782            "path+file:///path/to/my/project/foo#bar:1.1.8",
783            PackageIdSpec {
784                name: String::from("bar"),
785                version: Some("1.1.8".parse().unwrap()),
786                url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
787                kind: Some(SourceKind::Path),
788            },
789            "path+file:///path/to/my/project/foo#bar@1.1.8",
790        );
791        ok(
792            "path+file:///path/to/my/project/foo#foo::bar:1.1.8",
793            PackageIdSpec {
794                name: String::from("foo::bar"),
795                version: Some("1.1.8".parse().unwrap()),
796                url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
797                kind: Some(SourceKind::Path),
798            },
799            "path+file:///path/to/my/project/foo#foo::bar@1.1.8",
800        );
801        ok(
802            "path+file:///path/to/my/project/foo#bar@1.1.8",
803            PackageIdSpec {
804                name: String::from("bar"),
805                version: Some("1.1.8".parse().unwrap()),
806                url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
807                kind: Some(SourceKind::Path),
808            },
809            "path+file:///path/to/my/project/foo#bar@1.1.8",
810        );
811        ok(
812            "path+file:///path/to/my/project/foo#foo::bar@1.1.8",
813            PackageIdSpec {
814                name: String::from("foo::bar"),
815                version: Some("1.1.8".parse().unwrap()),
816                url: Some(Url::parse("file:///path/to/my/project/foo").unwrap()),
817                kind: Some(SourceKind::Path),
818            },
819            "path+file:///path/to/my/project/foo#foo::bar@1.1.8",
820        );
821        ok(
822            "builtin://.#core",
823            PackageIdSpec {
824                name: String::from("core"),
825                version: Some("0.0.0".parse().unwrap()),
826                url: Some(Url::parse("builtin://.").unwrap()),
827                kind: Some(SourceKind::Builtin),
828            },
829            "builtin://.#core",
830        );
831        ok(
832            "builtin+builtin://.#core",
833            PackageIdSpec {
834                name: String::from("core"),
835                version: Some("0.0.0".parse().unwrap()),
836                url: Some(Url::parse("builtin://.").unwrap()),
837                kind: Some(SourceKind::Builtin),
838            },
839            "builtin://.#core",
840        );
841        ok(
842            "builtin+builtin://.#core@0.0.0",
843            PackageIdSpec {
844                name: String::from("core"),
845                version: Some("0.0.0".parse().unwrap()),
846                url: Some(Url::parse("builtin://.").unwrap()),
847                kind: Some(SourceKind::Builtin),
848            },
849            "builtin://.#core",
850        );
851        ok(
852            "builtin+builtin://.#core@0",
853            PackageIdSpec {
854                name: String::from("core"),
855                version: Some("0.0.0".parse().unwrap()),
856                url: Some(Url::parse("builtin://.").unwrap()),
857                kind: Some(SourceKind::Builtin),
858            },
859            "builtin://.#core",
860        );
861    }
862
863    #[test]
864    fn bad_parsing() {
865        err!("baz:", ErrorKind::PartialVersion(_));
866        err!("baz:*", ErrorKind::PartialVersion(_));
867        err!("baz@", ErrorKind::PartialVersion(_));
868        err!("baz@*", ErrorKind::PartialVersion(_));
869        err!("baz@^1.0", ErrorKind::PartialVersion(_));
870        err!("https://baz:1.0", ErrorKind::NameValidation(_));
871        err!("https://#baz:1.0", ErrorKind::NameValidation(_));
872        err!(
873            "foobar+https://github.com/rust-lang/crates.io-index",
874            ErrorKind::UnsupportedProtocol(_)
875        );
876        err!(
877            "path+https://github.com/rust-lang/crates.io-index",
878            ErrorKind::UnsupportedPathPlusScheme(_)
879        );
880
881        // Only `git+` can use `?`
882        err!(
883            "file:///path/to/my/project/foo?branch=dev",
884            ErrorKind::UnexpectedQueryString(_)
885        );
886        err!(
887            "path+file:///path/to/my/project/foo?branch=dev",
888            ErrorKind::UnexpectedQueryString(_)
889        );
890        err!(
891            "registry+https://github.com/rust-lang/cargo?branch=dev#0.52.0",
892            ErrorKind::UnexpectedQueryString(_)
893        );
894        err!(
895            "sparse+https://github.com/rust-lang/cargo?branch=dev#0.52.0",
896            ErrorKind::UnexpectedQueryString(_)
897        );
898        err!("@1.2.3", ErrorKind::NameValidation(_));
899        err!("registry+https://github.com", ErrorKind::NameValidation(_));
900        err!("https://crates.io/1foo#1.2.3", ErrorKind::NameValidation(_));
901        err!("https://example.com/foo#", ErrorKind::EmptyFragment);
902        err!("git++://x:", ErrorKind::InvalidPkgIdUrl(_));
903        err!(
904            "git++https://github.com/rust-lang/cargo",
905            ErrorKind::InvalidPkgIdUrl(_)
906        );
907        err!("git+https://", ErrorKind::InvalidPkgIdUrl(_));
908        err!("builtin://.", ErrorKind::FragmentRequired);
909        err!(
910            "builtin+builtin://.#core@0.1.0",
911            ErrorKind::InvalidVersion(_)
912        );
913        err!("builtin://.#", ErrorKind::EmptyFragment);
914        err!("builtin://.#0.0.0", ErrorKind::NameValidation(_));
915        err!("builtin://wrong#core", ErrorKind::InvalidHostname);
916        err!("builtin://./invalid/path#core", ErrorKind::InvalidPath);
917        err!(
918            "builtin+invalid://.",
919            ErrorKind::UnsupportedBuiltinScheme(_)
920        );
921        err!(
922            "builtin://.?query=test#core",
923            ErrorKind::UnexpectedQueryString(_)
924        );
925        err!(
926            "builtin+builtin://.?query=test#core",
927            ErrorKind::UnexpectedQueryString(_)
928        );
929    }
930}