use std::borrow::Cow;
use std::fmt;
use std::str::FromStr;
use phf::phf_map;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use unicase::UniCase;
use crate::{
lowercase_in_place, GenericPurl, GenericPurlBuilder, ParseError, PurlField, PurlShape,
SmallString,
};
pub type Purl = GenericPurl<PackageType>;
pub type PurlBuilder = GenericPurlBuilder<PackageType>;
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[non_exhaustive]
pub enum PackageType {
Cargo,
Gem,
Golang,
Maven,
Npm,
NuGet,
PyPI,
}
static PACKAGE_TYPES: phf::Map<UniCase<&'static str>, PackageType> = phf_map! {
UniCase::ascii("cargo") => PackageType::Cargo,
UniCase::ascii("gem") => PackageType::Gem,
UniCase::ascii("golang") => PackageType::Golang,
UniCase::ascii("maven") => PackageType::Maven,
UniCase::ascii("npm") => PackageType::Npm,
UniCase::ascii("nuget") => PackageType::NuGet,
UniCase::ascii("pypi") => PackageType::PyPI,
};
impl PackageType {
#[must_use]
pub const fn name(&self) -> &'static str {
match self {
PackageType::Cargo => "cargo",
PackageType::Gem => "gem",
PackageType::Golang => "golang",
PackageType::Maven => "maven",
PackageType::Npm => "npm",
PackageType::NuGet => "nuget",
PackageType::PyPI => "pypi",
}
}
}
impl From<PackageType> for &'static str {
fn from(value: PackageType) -> Self {
value.name()
}
}
impl AsRef<str> for PackageType {
fn as_ref(&self) -> &str {
self.name()
}
}
impl fmt::Display for PackageType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
#[derive(Debug, thiserror::Error)]
#[error("Unsupported package type")]
pub struct UnsupportedPackageType;
impl FromStr for PackageType {
type Err = UnsupportedPackageType;
fn from_str(s: &str) -> Result<Self, Self::Err> {
PACKAGE_TYPES.get(&UniCase::new(s)).copied().ok_or(UnsupportedPackageType)
}
}
#[derive(Debug, thiserror::Error)]
pub enum PackageError {
#[error("The {0} field must be present")]
MissingRequiredField(PurlField),
#[error("{0}")]
Parse(#[from] ParseError),
#[error("Unsupported package type")]
UnsupportedType,
}
impl From<UnsupportedPackageType> for PackageError {
fn from(_: UnsupportedPackageType) -> Self {
PackageError::UnsupportedType
}
}
impl PurlShape for PackageType {
type Error = PackageError;
fn package_type(&self) -> Cow<str> {
self.name().into()
}
fn finish(&mut self, parts: &mut crate::PurlParts) -> Result<(), Self::Error> {
match self {
PackageType::Cargo | PackageType::Gem | PackageType::Npm | PackageType::Golang => {},
PackageType::Maven => {
if parts.namespace.is_empty() {
return Err(PackageError::MissingRequiredField(PurlField::Namespace));
}
},
PackageType::NuGet => {
lowercase_in_place(&mut parts.name);
},
PackageType::PyPI => {
fix_pypi_name(&mut parts.name);
},
}
Ok(())
}
}
fn fix_pypi_name(name: &mut SmallString) {
const DASH_CHARACTERS: &[char] = &['-', '_', '.'];
if name.contains(DASH_CHARACTERS) {
let mut result = SmallString::new();
let mut in_dash = false;
for c in name.chars() {
if DASH_CHARACTERS.contains(&c) {
if !in_dash {
result.push('-');
in_dash = true;
}
} else {
in_dash = false;
result.extend(c.to_lowercase());
}
}
*name = result;
} else {
lowercase_in_place(name)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn maven_requires_namespace() {
let error = Purl::new(PackageType::Maven, "invalid").unwrap_err();
assert!(
matches!(error, PackageError::MissingRequiredField(PurlField::Namespace)),
"Expected missing namespace error but got {error}",
);
}
}