#
cargo_util_schemas/core/
package_id_spec.rs

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