#
cargo_util_schemas/core/
package_id_spec.rs

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