#![doc = include_str!("../README.md")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
use std::borrow::Cow;
pub use builder::*;
#[cfg(feature = "package-type")]
pub use package_type::*;
pub use parse::*;
pub use qualifiers::Qualifiers;
#[cfg(feature = "smartstring")]
use smartstring::{LazyCompact, SmartString, SmartStringMode};
mod builder;
mod format;
#[cfg(feature = "package-type")]
mod package_type;
mod parse;
pub mod qualifiers;
#[cfg(feature = "smartstring")]
pub type SmallString = SmartString<LazyCompact>;
#[cfg(not(feature = "smartstring"))]
type SmallString = String;
pub trait PurlShape {
type Error: From<ParseError>;
#[must_use]
fn package_type(&self) -> Cow<str>;
fn finish(&mut self, parts: &mut PurlParts) -> Result<(), Self::Error>;
}
impl PurlShape for String {
type Error = ParseError;
fn package_type(&self) -> Cow<str> {
Cow::Borrowed(self)
}
fn finish(&mut self, _parts: &mut PurlParts) -> Result<(), Self::Error> {
str_preview_mut(self)?;
Ok(())
}
}
impl PurlShape for Cow<'_, str> {
type Error = ParseError;
fn package_type(&self) -> Cow<str> {
Cow::Borrowed(self)
}
fn finish(&mut self, _parts: &mut PurlParts) -> Result<(), Self::Error> {
match self {
Cow::Owned(v) => str_preview_mut(v)?,
Cow::Borrowed(v) => {
if !is_valid_package_type(v) {
return Err(ParseError::InvalidPackageType);
}
if !v.chars().all(|c| c.is_ascii_lowercase()) {
*self = Cow::Owned(v.to_ascii_lowercase())
}
},
}
Ok(())
}
}
#[cfg(feature = "smartstring")]
impl<M> PurlShape for SmartString<M>
where
M: SmartStringMode,
{
type Error = ParseError;
fn package_type(&self) -> Cow<str> {
Cow::Borrowed(self)
}
fn finish(&mut self, _parts: &mut PurlParts) -> Result<(), Self::Error> {
str_preview_mut(self)?;
Ok(())
}
}
fn str_preview_mut(s: &mut str) -> Result<(), ParseError> {
if !is_valid_package_type(s) {
return Err(ParseError::InvalidPackageType);
}
s.make_ascii_lowercase();
Ok(())
}
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq, PartialOrd, Ord)]
#[must_use]
pub struct PurlParts {
pub namespace: SmallString,
pub name: SmallString,
pub version: SmallString,
pub qualifiers: Qualifiers,
pub subpath: SmallString,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
#[must_use]
pub struct GenericPurl<T> {
package_type: T,
parts: PurlParts,
}
impl<T> GenericPurl<T> {
pub fn builder<S>(package_type: T, name: S) -> GenericPurlBuilder<T>
where
SmallString: From<S>,
T: PurlShape,
{
GenericPurlBuilder::new(package_type, name)
}
pub fn new<S>(package_type: T, name: S) -> Result<Self, T::Error>
where
SmallString: From<S>,
T: PurlShape,
{
Self::builder(package_type, name).build()
}
#[must_use]
pub fn package_type(&self) -> &T {
&self.package_type
}
#[must_use]
pub fn namespace(&self) -> Option<&str> {
Some(&*self.parts.namespace).filter(|v| !v.is_empty())
}
#[must_use]
pub fn name(&self) -> &str {
&self.parts.name
}
#[must_use]
pub fn version(&self) -> Option<&str> {
Some(&*self.parts.version).filter(|v| !v.is_empty())
}
#[must_use]
pub fn qualifiers(&self) -> &Qualifiers {
&self.parts.qualifiers
}
#[must_use]
pub fn subpath(&self) -> Option<&str> {
Some(&*self.parts.subpath).filter(|v| !v.is_empty())
}
pub fn into_builder(self) -> GenericPurlBuilder<T> {
let GenericPurl { package_type, parts } = self;
GenericPurlBuilder { package_type, parts }
}
}
#[cfg(feature = "package-type")]
impl Purl {
pub fn builder_with_combined_name<S>(
package_type: PackageType,
namespaced_name: S,
) -> PurlBuilder
where
S: AsRef<str>,
{
let namespaced_name = namespaced_name.as_ref();
let (namespace, name) = match package_type {
PackageType::Cargo | PackageType::Gem | PackageType::NuGet | PackageType::PyPI => {
(None, namespaced_name)
},
PackageType::Golang | PackageType::Npm => match namespaced_name.rsplit_once('/') {
Some((namespace, name)) => (Some(namespace), name),
None => (None, namespaced_name),
},
PackageType::Maven => match namespaced_name.split_once(':') {
Some((namespace, name)) => (Some(namespace), name),
None => (None, namespaced_name),
},
};
let mut builder = GenericPurlBuilder::new(package_type, name);
if let Some(namespace) = namespace {
builder = builder.with_namespace(namespace);
}
builder
}
pub fn combined_name(&self) -> Cow<'_, str> {
match self.package_type {
PackageType::Cargo | PackageType::Gem | PackageType::NuGet | PackageType::PyPI => {
self.name().into()
},
PackageType::Golang | PackageType::Npm => match self.namespace() {
Some(namespace) => Cow::Owned(format!("{}/{}", namespace, self.name())),
None => self.name().into(),
},
PackageType::Maven => match self.namespace() {
Some(namespace) => Cow::Owned(format!("{}:{}", namespace, self.name())),
None => self.name().into(),
},
}
}
}
#[must_use]
fn is_valid_package_type(package_type: &str) -> bool {
const ALLOWED_SPECIAL_CHARS: &[char] = &['.', '+', '-'];
!package_type.is_empty()
&& package_type.starts_with(|c: char| c.is_ascii_alphabetic())
&& package_type
.chars()
.skip(1)
.all(|c| c.is_ascii_alphanumeric() || ALLOWED_SPECIAL_CHARS.contains(&c))
}
fn lowercase_in_place(s: &mut SmallString) {
enum State {
Lower,
MixedAscii,
MixedUnicode,
}
let mut state = State::Lower;
for c in s.chars() {
if c.is_uppercase() {
if c.is_ascii() {
state = State::MixedAscii;
} else {
state = State::MixedUnicode;
break;
}
}
}
match state {
State::Lower => {},
State::MixedAscii => {
s.make_ascii_lowercase();
},
State::MixedUnicode => {
*s = s.chars().flat_map(|c| c.to_lowercase()).collect();
},
}
}
fn copy_as_lowercase(s: &str) -> SmallString {
enum State {
Lower,
MixedAscii,
MixedUnicode,
}
let mut state = State::Lower;
for c in s.chars() {
if c.is_uppercase() {
if c.is_ascii() {
state = State::MixedAscii;
} else {
state = State::MixedUnicode;
break;
}
}
}
match state {
State::Lower => SmallString::from(s),
State::MixedAscii => {
let mut v = SmallString::from(s);
v.make_ascii_lowercase();
v
},
State::MixedUnicode => s.chars().flat_map(|c| c.to_lowercase()).collect(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn string_shape_converts_to_lower() {
let purl = GenericPurlBuilder::new("TEST".to_owned(), "name")
.build()
.expect("Could not build PURL");
assert_eq!("test", purl.package_type());
assert_eq!("pkg:test/name", &purl.to_string());
}
#[test]
fn string_shape_disallows_invalid_names() {
let error = GenericPurlBuilder::new("!".to_owned(), "name")
.build()
.expect_err("Build with invalid type should have failed");
assert!(matches!(error, ParseError::InvalidPackageType), "Got unexpected error {}", error,);
}
#[test]
fn cow_shape_borrowed_converts_to_lower() {
let purl = GenericPurlBuilder::new(Cow::Borrowed("TEST"), "name")
.build()
.expect("Could not build PURL");
assert_eq!("test", purl.package_type());
assert_eq!("pkg:test/name", &purl.to_string());
}
#[test]
fn cow_shape_owned_converts_to_lower() {
let purl = GenericPurlBuilder::new(Cow::Owned("TEST".to_owned()), "name")
.build()
.expect("Could not build PURL");
assert_eq!("test", purl.package_type());
assert_eq!("pkg:test/name", &purl.to_string());
}
#[test]
fn cow_shape_does_not_clone_lower() {
let original = "test";
let purl = GenericPurlBuilder::new(Cow::Borrowed(original), "name")
.build()
.expect("Could not build PURL");
assert_eq!(original.as_ptr(), purl.package_type.as_ptr());
}
#[test]
fn cow_shape_does_not_clone_owned() {
let original = "TEST".to_owned();
let original_ptr = original.as_ptr();
let purl = GenericPurlBuilder::new(Cow::Owned(original), "name")
.build()
.expect("Could not build PURL");
assert_eq!("test", purl.package_type());
assert_eq!(original_ptr, purl.package_type.as_ptr());
}
#[test]
fn cow_shape_disallows_invalid_names() {
let error = GenericPurlBuilder::new(Cow::Borrowed("!"), "name")
.build()
.expect_err("Build with invalid type should have failed");
assert!(matches!(error, ParseError::InvalidPackageType), "Got unexpected error {}", error,);
}
#[test]
fn smallstring_shape_converts_to_lower() {
let purl = GenericPurlBuilder::new(SmallString::from("TEST"), "name")
.build()
.expect("Could not build PURL");
assert_eq!("test", purl.package_type());
assert_eq!("pkg:test/name", &purl.to_string());
}
#[test]
fn smallstring_shape_disallows_invalid_names() {
let error = GenericPurlBuilder::new(SmallString::from("!"), "name")
.build()
.expect_err("Build with invalid type should have failed");
assert!(matches!(error, ParseError::InvalidPackageType), "Got unexpected error {}", error,);
}
#[test]
fn into_builder_build_produces_same_purl() {
let original = GenericPurlBuilder::new(Cow::Borrowed("type"), "name")
.with_namespace("namespace")
.with_subpath("subpath")
.with_version("1.0")
.with_qualifier("key", "value")
.unwrap()
.build()
.unwrap();
let round_trip = original.clone().into_builder().build().unwrap();
assert_eq!(original, round_trip);
}
#[test]
fn lowercase_in_place_when_lower_does_nothing() {
let mut lower = SmallString::from("a");
lowercase_in_place(&mut lower);
assert_eq!("a", &lower);
}
#[test]
fn lowercase_in_place_when_upper_ascii_lowercases() {
let mut lower = SmallString::from("A");
lowercase_in_place(&mut lower);
assert_eq!("a", &lower);
}
#[test]
fn lowercase_in_place_when_upper_unicode_lowercases() {
let mut lower = SmallString::from("Æ");
lowercase_in_place(&mut lower);
assert_eq!("æ", &lower);
}
#[test]
fn copy_as_lowercase_when_lower_does_nothing() {
let upper = "a";
let lower = copy_as_lowercase(upper);
assert_eq!("a", &lower);
}
#[test]
fn copy_as_lowercase_when_upper_ascii_lowercases() {
let upper = "A";
let lower = copy_as_lowercase(upper);
assert_eq!("a", &lower);
}
#[test]
fn copy_as_lowercase_when_upper_unicode_lowercases() {
let upper = "Æ";
let lower = copy_as_lowercase(upper);
assert_eq!("æ", &lower);
}
#[test]
fn empty_package_type_is_invalid() {
let error = GenericPurl::new(Cow::Borrowed(""), "name").unwrap_err();
assert!(matches!(error, ParseError::InvalidPackageType));
}
#[test]
fn namespace_when_empty_is_none() {
let purl = GenericPurl::new(Cow::Borrowed("type"), "name").unwrap();
assert_eq!(None, purl.namespace());
}
#[test]
fn version_when_empty_is_none() {
let purl = GenericPurl::new(Cow::Borrowed("type"), "name").unwrap();
assert_eq!(None, purl.version());
}
#[test]
fn subpath_when_empty_is_none() {
let purl = GenericPurl::new(Cow::Borrowed("type"), "name").unwrap();
assert_eq!(None, purl.subpath());
}
#[cfg(feature = "package-type")]
#[test]
fn namespaced_name() {
let purl =
Purl::builder_with_combined_name(PackageType::Npm, "@angular/cli").build().unwrap();
assert_eq!(purl.namespace(), Some("@angular"));
assert_eq!(purl.name(), "cli");
assert_eq!(purl.combined_name(), "@angular/cli");
let purl = Purl::builder_with_combined_name(PackageType::Maven, "org.maven.plugins:pom")
.build()
.unwrap();
assert_eq!(purl.namespace(), Some("org.maven.plugins"));
assert_eq!(purl.name(), "pom");
assert_eq!(purl.combined_name(), "org.maven.plugins:pom");
let purl = Purl::builder_with_combined_name(PackageType::Cargo, "libc").build().unwrap();
assert_eq!(purl.namespace(), None);
assert_eq!(purl.name(), "libc");
assert_eq!(purl.combined_name(), "libc");
}
}