blob: bad679b0f6813a14a1e70f1e0850d41d152346e9 [file] [log] [blame]
use crate::Error;
use semver::Version;
/// A package specification. See
/// [cargo pkgid](https://doc.rust-lang.org/cargo/commands/cargo-pkgid.html)
/// for more information on this.
#[derive(Debug, Clone)]
pub struct PkgSpec {
pub name: String,
pub version: Option<Version>,
pub url: Option<String>,
}
impl PkgSpec {
pub fn matches(&self, krate: &crate::cm::Package) -> bool {
if self.name != krate.name {
return false;
}
if let Some(ref vers) = self.version {
if vers != &krate.version {
return false;
}
}
let Some((url, src)) = self
.url
.as_ref()
.zip(krate.source.as_ref().map(|s| s.repr.as_str()))
else {
return true;
};
let begin = src.find('+').map_or(0, |i| i + 1);
let end = src.find('?').or_else(|| src.find('#')).unwrap_or(src.len());
url == &src[begin..end]
}
}
impl std::str::FromStr for PkgSpec {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let name_and_or_version = |nv: Option<&str>,
url: Option<&str>|
-> Result<(String, Option<Version>), Self::Err> {
let validate_name = |n: &str| -> Result<String, Self::Err> {
if n.find(|c: char| c != '-' && c != '_' && !c.is_ascii_alphanumeric())
.is_some()
{
Err(Error::InvalidPkgSpec(
"found an invalid character for the package name",
))
} else {
Ok(n.to_owned())
}
};
// We validate the url portion regardless, unlike cargo
let path_name = match url {
Some(url) => {
// Cargo is actually more lenient than this and will allow an end slash,
// even though that means it will never actually match a package due to
// how it retrieves a name from the url if the name isn't explicitly provided
if url.ends_with('/') {
return Err(Error::InvalidPkgSpec("url ends with /"));
}
match url.rfind("://") {
Some(ind) => match url.rfind('/') {
Some(pind) => {
if pind == ind + 2 {
return Err(Error::InvalidPkgSpec("path required for urls"));
}
let path = &url[pind + 1..];
Some(path)
}
None => return Err(Error::InvalidPkgSpec("path required for urls")),
},
None => return Err(Error::InvalidPkgSpec("missing url scheme")),
}
}
None => None,
};
match nv {
Some(nv) => {
match nv.find(':') {
Some(ind) => {
if ind == nv.len() - 1 {
return Err(Error::InvalidPkgSpec(
"package spec cannot end with ':'",
));
}
Ok((
validate_name(&nv[..ind])?,
Some(Version::parse(&nv[ind + 1..]).map_err(|_e| {
Error::InvalidPkgSpec("failed to parse version")
})?),
))
}
None => {
// If we have a url, this could be either a name and/or a version
match path_name {
Some(name) => {
// This is the same way that cargo itself parses
if nv.chars().next().unwrap().is_alphabetic() {
Ok((validate_name(nv)?, None))
} else {
let version = Version::parse(nv).map_err(|_e| {
Error::InvalidPkgSpec("failed to parse version")
})?;
Ok((validate_name(name)?, Some(version)))
}
}
None => Ok((validate_name(nv)?, None)),
}
}
}
}
None => Ok((validate_name(path_name.unwrap())?, None)),
}
};
if s.contains('/') {
let url = if s.contains("://") {
std::borrow::Cow::Borrowed(s)
} else {
std::borrow::Cow::Owned(format!("cargo://{}", s))
};
if let Some(ind) = url.find('#') {
if ind == url.len() - 1 {
return Err(Error::InvalidPkgSpec("package spec cannot end with '#'"));
}
let url_no_frag = url[..ind].to_owned();
let (name, version) =
name_and_or_version(Some(&url[ind + 1..]), Some(&url_no_frag))?;
Ok(Self {
url: Some(url_no_frag),
name,
version,
})
} else {
let (name, version) = name_and_or_version(None, Some(&url))?;
Ok(Self {
url: Some(url.into_owned()),
name,
version,
})
}
} else {
let (name, version) = name_and_or_version(Some(s), None)?;
Ok(Self {
url: None,
name,
version,
})
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn name() {
let spec: PkgSpec = "bitflags".parse().unwrap();
assert_eq!("bitflags", spec.name);
assert!(spec.version.is_none());
assert!(spec.url.is_none());
}
#[test]
fn name_and_version() {
let spec: PkgSpec = "bitflags:1.0.4".parse().unwrap();
assert_eq!("bitflags", spec.name);
assert_eq!(Version::parse("1.0.4").unwrap(), spec.version.unwrap());
assert!(spec.url.is_none());
}
#[test]
fn url() {
let spec: PkgSpec = "https://github.com/rust-lang/cargo".parse().unwrap();
assert_eq!("cargo", spec.name);
assert!(spec.version.is_none());
assert_eq!("https://github.com/rust-lang/cargo", spec.url.unwrap());
}
#[test]
fn url_and_version() {
let spec: PkgSpec = "https://github.com/rust-lang/cargo#0.33.0".parse().unwrap();
assert_eq!("cargo", spec.name);
assert_eq!(Version::parse("0.33.0").unwrap(), spec.version.unwrap());
assert_eq!("https://github.com/rust-lang/cargo", spec.url.unwrap());
}
#[test]
fn url_and_name() {
let spec: PkgSpec = "https://github.com/rust-lang/crates.io-index#bitflags"
.parse()
.unwrap();
assert_eq!("bitflags", spec.name);
assert!(spec.version.is_none());
assert_eq!(
"https://github.com/rust-lang/crates.io-index",
spec.url.unwrap()
);
}
#[test]
fn url_and_name_and_version() {
let spec: PkgSpec = "https://github.com/rust-lang/cargo#crates-io:0.21.0"
.parse()
.unwrap();
assert_eq!("crates-io", spec.name);
assert_eq!(Version::parse("0.21.0").unwrap(), spec.version.unwrap());
assert_eq!("https://github.com/rust-lang/cargo", spec.url.unwrap());
}
#[test]
fn no_proto() {
let spec: PkgSpec = "crates.io/foo".parse().unwrap();
assert_eq!("foo", spec.name);
assert!(spec.version.is_none());
assert_eq!("cargo://crates.io/foo", spec.url.unwrap());
}
#[test]
fn no_proto_and_version() {
let spec: PkgSpec = "crates.io/foo#1.2.3".parse().unwrap();
assert_eq!("foo", spec.name);
assert_eq!(Version::parse("1.2.3").unwrap(), spec.version.unwrap());
assert_eq!("cargo://crates.io/foo", spec.url.unwrap());
}
#[test]
fn no_proto_and_name_and_version() {
let spec: PkgSpec = "crates.io/foo#1.2.3".parse().unwrap();
assert_eq!("foo", spec.name);
assert_eq!(Version::parse("1.2.3").unwrap(), spec.version.unwrap());
assert_eq!("cargo://crates.io/foo", spec.url.unwrap());
}
#[test]
fn disallow_no_path() {
let nopes = [
"https://crates.io#1.2.3",
"https://crates.io",
"https://crates.io#foo",
];
for nope in &nopes {
match nope.parse::<PkgSpec>().unwrap_err() {
Error::InvalidPkgSpec(err) => assert_eq!(err, "path required for urls"),
nope => panic!("didn't expect {:?}", nope),
}
}
for nope in &["https://crates.io/", "crates.io/#1.2.3"] {
match nope.parse::<PkgSpec>().unwrap_err() {
Error::InvalidPkgSpec(err) => assert_eq!(err, "url ends with /"),
nope => panic!("didn't expect {:?}", nope),
}
}
for nope in &["crates.io#foo", "crates.io#1.2.3"] {
match nope.parse::<PkgSpec>().unwrap_err() {
Error::InvalidPkgSpec(err) => {
assert_eq!(err, "found an invalid character for the package name");
}
nope => panic!("didn't expect {:?}", nope),
}
}
}
}