use {
crate::{package_metadata::PythonPackageMetadata, resource::PythonResource},
anyhow::{anyhow, Context, Result},
spdx::{ExceptionId, Expression, LicenseId},
std::{
cmp::Ordering,
collections::{BTreeMap, BTreeSet},
fmt::{Display, Formatter},
},
};
pub const SAFE_SYSTEM_LIBRARIES: &[&str] = &[
"cabinet", "iphlpapi", "msi", "rpcrt4", "rt", "winmm", "ws2_32",
];
fn format_spdx(id: LicenseId, exception: Option<ExceptionId>, full: bool) -> String {
let name = if full { id.full_name } else { id.name };
if let Some(exception) = exception {
format!("{} WITH {}", name, exception.name)
} else {
name.to_string()
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum LicenseFlavor {
None,
Spdx(Expression),
OtherExpression(Expression),
PublicDomain,
Unknown(Vec<String>),
}
#[derive(Clone, Debug)]
pub enum ComponentFlavor {
PythonDistribution(String),
PythonStandardLibraryModule(String),
PythonStandardLibraryExtensionModule(String),
PythonExtensionModule(String),
PythonModule(String),
Library(String),
RustCrate(String),
}
impl Display for ComponentFlavor {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::PythonDistribution(name) => f.write_str(name),
Self::PythonStandardLibraryModule(name) => {
f.write_fmt(format_args!("Python stdlib module {}", name))
}
Self::PythonStandardLibraryExtensionModule(name) => {
f.write_fmt(format_args!("Python stdlib extension {}", name))
}
Self::PythonExtensionModule(name) => {
f.write_fmt(format_args!("Python extension module {}", name))
}
Self::PythonModule(name) => f.write_fmt(format_args!("Python module {}", name)),
Self::Library(name) => f.write_fmt(format_args!("library {}", name)),
Self::RustCrate(name) => f.write_fmt(format_args!("Rust crate {}", name)),
}
}
}
impl PartialEq for ComponentFlavor {
fn eq(&self, other: &Self) -> bool {
match (self.python_module_name(), other.python_module_name()) {
(Some(a), Some(b)) => a.eq(b),
(Some(_), None) => false,
(None, Some(_)) => false,
(None, None) => match (self, other) {
(Self::PythonDistribution(a), Self::PythonDistribution(b)) => a.eq(b),
(Self::Library(a), Self::Library(b)) => a.eq(b),
(Self::RustCrate(a), Self::RustCrate(b)) => a.eq(b),
_ => false,
},
}
}
}
impl Eq for ComponentFlavor {}
impl PartialOrd for ComponentFlavor {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
match (self.python_module_name(), other.python_module_name()) {
(Some(a), Some(b)) => a.partial_cmp(b),
_ => {
let a = (self.ordinal_value(), self.to_string());
let b = (other.ordinal_value(), other.to_string());
a.partial_cmp(&b)
}
}
}
}
impl Ord for ComponentFlavor {
fn cmp(&self, other: &Self) -> Ordering {
self.partial_cmp(other).unwrap()
}
}
impl ComponentFlavor {
fn ordinal_value(&self) -> u8 {
match self {
Self::PythonDistribution(_) => 0,
ComponentFlavor::PythonStandardLibraryModule(_) => 1,
ComponentFlavor::PythonStandardLibraryExtensionModule(_) => 2,
ComponentFlavor::PythonExtensionModule(_) => 3,
ComponentFlavor::PythonModule(_) => 4,
ComponentFlavor::Library(_) => 5,
ComponentFlavor::RustCrate(_) => 6,
}
}
pub fn is_python_standard_library(&self) -> bool {
match self {
Self::PythonDistribution(_) => false,
Self::PythonStandardLibraryModule(_) => true,
Self::PythonStandardLibraryExtensionModule(_) => true,
Self::PythonExtensionModule(_) => true,
Self::PythonModule(_) => false,
Self::Library(_) => false,
Self::RustCrate(_) => false,
}
}
pub fn python_module_name(&self) -> Option<&str> {
match self {
Self::PythonDistribution(_) => None,
Self::PythonStandardLibraryModule(name) => Some(name.as_str()),
Self::PythonStandardLibraryExtensionModule(name) => Some(name.as_str()),
Self::PythonExtensionModule(name) => Some(name.as_str()),
Self::PythonModule(name) => Some(name.as_str()),
Self::Library(_) => None,
Self::RustCrate(_) => None,
}
}
pub fn is_python_distribution_component(&self) -> bool {
matches!(
self,
Self::PythonDistribution(_)
| Self::PythonStandardLibraryModule(_)
| Self::PythonStandardLibraryExtensionModule(_)
)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SourceLocation {
NotSet,
Url(String),
}
#[derive(Clone, Debug)]
pub struct LicensedComponent {
flavor: ComponentFlavor,
license: LicenseFlavor,
source_location: SourceLocation,
homepage: Option<String>,
authors: Vec<String>,
license_texts: Vec<String>,
}
impl PartialEq for LicensedComponent {
fn eq(&self, other: &Self) -> bool {
self.flavor.eq(&other.flavor)
}
}
impl Eq for LicensedComponent {}
impl PartialOrd for LicensedComponent {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.flavor.partial_cmp(&other.flavor)
}
}
impl Ord for LicensedComponent {
fn cmp(&self, other: &Self) -> Ordering {
self.flavor.cmp(&other.flavor)
}
}
impl LicensedComponent {
pub fn new(flavor: ComponentFlavor, license: LicenseFlavor) -> Self {
Self {
flavor,
license,
source_location: SourceLocation::NotSet,
homepage: None,
authors: vec![],
license_texts: vec![],
}
}
pub fn new_spdx(flavor: ComponentFlavor, spdx_expression: &str) -> Result<Self> {
let spdx_expression = Expression::parse(spdx_expression).map_err(|e| anyhow!("{}", e))?;
let license = if spdx_expression.evaluate(|req| req.license.id().is_some()) {
LicenseFlavor::Spdx(spdx_expression)
} else {
LicenseFlavor::OtherExpression(spdx_expression)
};
Ok(Self::new(flavor, license))
}
pub fn flavor(&self) -> &ComponentFlavor {
&self.flavor
}
pub fn license(&self) -> &LicenseFlavor {
&self.license
}
pub fn spdx_expression(&self) -> Option<&Expression> {
match &self.license {
LicenseFlavor::Spdx(expression) => Some(expression),
LicenseFlavor::OtherExpression(expression) => Some(expression),
LicenseFlavor::None | LicenseFlavor::PublicDomain | LicenseFlavor::Unknown(_) => None,
}
}
pub fn is_simple_spdx_expression(&self) -> bool {
if let LicenseFlavor::Spdx(expression) = &self.license {
expression.iter().count() < 2
} else {
false
}
}
pub fn source_location(&self) -> &SourceLocation {
&self.source_location
}
pub fn set_source_location(&mut self, location: SourceLocation) {
self.source_location = location;
}
pub fn homepage(&self) -> Option<&str> {
self.homepage.as_deref()
}
pub fn set_homepage(&mut self, value: impl ToString) {
self.homepage = Some(value.to_string());
}
pub fn authors(&self) -> &[String] {
&self.authors
}
pub fn add_author(&mut self, value: impl ToString) {
self.authors.push(value.to_string());
}
pub fn license_texts(&self) -> &Vec<String> {
&self.license_texts
}
pub fn add_license_text(&mut self, text: impl ToString) {
self.license_texts.push(text.to_string());
}
pub fn is_spdx(&self) -> bool {
matches!(self.license, LicenseFlavor::Spdx(_))
}
pub fn all_spdx_licenses(&self) -> BTreeSet<(LicenseId, Option<ExceptionId>)> {
match &self.license {
LicenseFlavor::Spdx(expression) => expression
.requirements()
.map(|req| (req.req.license.id().unwrap(), req.req.exception))
.collect::<BTreeSet<_>>(),
LicenseFlavor::OtherExpression(expression) => expression
.requirements()
.filter_map(|req| req.req.license.id().map(|id| (id, req.req.exception)))
.collect::<BTreeSet<_>>(),
LicenseFlavor::None | LicenseFlavor::PublicDomain | LicenseFlavor::Unknown(_) => {
BTreeSet::new()
}
}
}
pub fn all_spdx_license_names(&self, full: bool) -> Vec<String> {
self.all_spdx_licenses()
.into_iter()
.map(|(id, exception)| format_spdx(id, exception, full))
.collect::<Vec<_>>()
}
pub fn all_spdx_license_ids(&self) -> BTreeSet<LicenseId> {
self.all_spdx_licenses()
.into_iter()
.map(|(lid, _)| lid)
.collect::<BTreeSet<_>>()
}
pub fn all_spdx_exception_ids(&self) -> BTreeSet<ExceptionId> {
self.all_spdx_licenses()
.into_iter()
.filter_map(|(_, id)| id)
.collect::<BTreeSet<_>>()
}
pub fn has_copyleft(&self) -> bool {
self.all_spdx_licenses()
.into_iter()
.any(|(id, _)| id.is_copyleft())
}
pub fn is_always_copyleft(&self) -> bool {
let licenses = self.all_spdx_licenses();
if licenses.is_empty() {
false
} else {
licenses.into_iter().all(|(id, _)| id.is_copyleft())
}
}
pub fn licensing_summary(&self) -> String {
let mut lines = vec![];
if !self.authors().is_empty() {
lines.push(format!("Authors: {}", self.authors().join(", ")));
}
if let Some(value) = self.homepage() {
lines.push(format!("Homepage: {}", value));
}
match self.source_location() {
SourceLocation::NotSet => {}
SourceLocation::Url(value) => {
lines.push(format!("Source location: {}", value));
}
}
match self.license() {
LicenseFlavor::None => {
lines.push("No licensing information available.".into());
}
LicenseFlavor::Spdx(expression) | LicenseFlavor::OtherExpression(expression) => {
lines.push(format!(
"Licensed according to SPDX expression: {}",
expression
));
}
LicenseFlavor::PublicDomain => {
lines.push("Licensed to the public domain.".into());
}
LicenseFlavor::Unknown(terms) => {
lines.push(format!("Licensed according to {}", terms.join(", ")));
}
}
lines.join("\n")
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct LicensedComponents {
components: BTreeMap<ComponentFlavor, LicensedComponent>,
}
impl LicensedComponents {
pub fn into_components(self) -> impl Iterator<Item = LicensedComponent> {
self.components.into_values()
}
pub fn iter_components(&self) -> impl Iterator<Item = &LicensedComponent> {
self.components.values()
}
pub fn add_component(&mut self, component: LicensedComponent) {
self.components.insert(component.flavor.clone(), component);
}
pub fn add_spdx_only_component(&mut self, component: LicensedComponent) -> Result<()> {
if component.is_spdx() {
self.add_component(component);
Ok(())
} else {
Err(anyhow!("component has non-SPDX license identifiers"))
}
}
pub fn has_python_module(&self, name: &str) -> bool {
self.components
.contains_key(&ComponentFlavor::PythonModule(name.into()))
}
pub fn normalize_python_modules(&self) -> Self {
let distribution = self
.components
.values()
.find(|c| matches!(c.flavor(), ComponentFlavor::PythonDistribution(_)));
let mut top_level_names = BTreeSet::new();
let mut components = Self::default();
let filtered = self.components.iter().filter(|(k, v)| {
if k.is_python_standard_library() {
if let Some(distribution) = distribution {
if v.license() == distribution.license() {
return false;
}
}
}
if let Some(name) = k.python_module_name() {
let top_level_name = if let Some((name, _)) = name.split_once('.') {
name
} else {
name
};
top_level_names.insert(top_level_name.to_string());
}
true
});
for (_, component) in filtered {
components.add_component(component.clone());
}
for name in top_level_names {
if !components.has_python_module(&name) {
components.add_component(LicensedComponent::new(
ComponentFlavor::PythonModule(name.to_string()),
LicenseFlavor::None,
));
}
}
components.components =
BTreeMap::from_iter(components.components.into_iter().filter(|(k, _)| {
if let Some(name) = k.python_module_name() {
if name.contains('.') {
return false;
}
}
true
}));
components
}
pub fn all_spdx_licenses(&self) -> BTreeSet<(LicenseId, Option<ExceptionId>)> {
self.components
.values()
.flat_map(|component| component.all_spdx_licenses())
.collect::<BTreeSet<_>>()
}
pub fn all_spdx_license_ids(&self) -> BTreeSet<LicenseId> {
self.components
.values()
.flat_map(|component| component.all_spdx_license_ids())
.collect::<BTreeSet<_>>()
}
pub fn all_spdx_license_names(&self, full: bool) -> Vec<String> {
self.iter_components()
.flat_map(|c| c.all_spdx_license_names(full))
.collect::<BTreeSet<_>>()
.into_iter()
.collect::<Vec<_>>()
}
pub fn components_by_spdx_license(
&self,
) -> BTreeMap<(LicenseId, Option<ExceptionId>), BTreeSet<&LicensedComponent>> {
let mut res = BTreeMap::new();
for component in self.iter_components() {
for key in component.all_spdx_licenses() {
res.entry(key)
.or_insert_with(BTreeSet::new)
.insert(component);
}
}
res
}
pub fn license_spdx_components(&self) -> impl Iterator<Item = &LicensedComponent> {
self.components
.values()
.filter(|c| matches!(c.license(), &LicenseFlavor::Spdx(_)))
}
pub fn license_missing_components(&self) -> impl Iterator<Item = &LicensedComponent> {
self.components
.values()
.filter(|c| c.license() == &LicenseFlavor::None)
}
pub fn license_public_domain_components(&self) -> impl Iterator<Item = &LicensedComponent> {
self.components
.values()
.filter(|c| c.license() == &LicenseFlavor::PublicDomain)
}
pub fn license_unknown_components(&self) -> impl Iterator<Item = &LicensedComponent> {
self.components.values().filter(|c| {
matches!(
c.license(),
&LicenseFlavor::Unknown(_) | &LicenseFlavor::OtherExpression(_)
)
})
}
pub fn license_copyleft_components(&self) -> impl Iterator<Item = &LicensedComponent> {
self.components.values().filter(|c| c.has_copyleft())
}
pub fn license_summary(&self) -> String {
let mut lines = vec![
"Software Licensing Summary".to_string(),
"==========================".to_string(),
"".to_string(),
];
lines.push(format!(
"{} distinct software components",
self.components.len()
));
lines.push(format!(
"{} lack a known software license",
self.license_missing_components().count()
));
lines.push(format!(
"{} have unknown license expressions",
self.license_unknown_components().count()
));
lines.push(format!(
"{} distinct SPDX licenses",
self.all_spdx_licenses().len()
));
lines.push(format!(
"{} components in the public domain",
self.license_public_domain_components().count()
));
lines.push(format!(
"{} have copyleft licenses",
self.license_copyleft_components().count()
));
let spdx_components = self.components_by_spdx_license();
if !spdx_components.is_empty() {
lines.push("".to_string());
lines.push("Count OSI FSF free Copyleft SPDX License".to_string());
for ((lid, exception), components) in spdx_components {
lines.push(format!(
"{:>5} [{}] [{}] [{}] {}",
components.len(),
if lid.is_osi_approved() { "x" } else { " " },
if lid.is_fsf_free_libre() { "x" } else { " " },
if lid.is_copyleft() { "x" } else { " " },
format_spdx(lid, exception, true)
));
}
}
lines.join("\n")
}
pub fn interesting_report(&self) -> Option<String> {
let mut lines = vec![
"Noteworthy Licensing Info".to_string(),
"=========================".to_string(),
"".to_string(),
];
let mut have_interesting = false;
for component in self.iter_components() {
match component.license() {
LicenseFlavor::None => {
lines.push(format!("* {} lacks a known license", component.flavor()));
have_interesting = true;
}
LicenseFlavor::Spdx(_) => {
let copyleft_names = component
.all_spdx_licenses()
.into_iter()
.filter(|(id, _)| id.is_copyleft())
.map(|(id, exception)| format_spdx(id, exception, true))
.collect::<Vec<_>>();
if component.is_always_copyleft() {
lines.push(format!(
"* {} has copyleft licenses exclusively ({})",
component.flavor(),
copyleft_names.join(", ")
));
have_interesting = true;
} else if component.has_copyleft() {
lines.push(format!(
"* {} has a copyleft license ({})",
component.flavor(),
copyleft_names.join(", ")
));
have_interesting = true;
}
}
LicenseFlavor::OtherExpression(expr) => {
lines.push(format!(
"* {} has an unknown SPDX license expression: {}",
component.flavor(),
expr
));
have_interesting = true;
}
LicenseFlavor::PublicDomain => {}
LicenseFlavor::Unknown(terms) => {
lines.push(format!(
"* {} has unknown license expression: {}",
component.flavor(),
terms.join(", ")
));
have_interesting = true;
}
}
}
if have_interesting {
Some(lines.join("\n"))
} else {
None
}
}
pub fn spdx_license_breakdown(&self) -> String {
let mut lines = vec![
"SPDX License Breakdown".to_string(),
"======================".to_string(),
"".to_string(),
];
for (license, exception) in self.all_spdx_licenses() {
lines.push(format_spdx(license, exception, true));
lines.push("-".repeat(format_spdx(license, exception, true).len()));
lines.push("".to_string());
lines.push(format!(
"[{}] OSI approved; [{}] FSF free libre; [{}] copyleft",
if license.is_osi_approved() { "*" } else { " " },
if license.is_fsf_free_libre() {
"*"
} else {
" "
},
if license.is_copyleft() { "*" } else { " " }
));
lines.push("".to_string());
for component in self.iter_components() {
if component
.all_spdx_licenses()
.contains(&(license, exception))
{
lines.push(format!("* {}", component.flavor()));
}
}
lines.push("".to_string());
}
lines.join("\n")
}
#[cfg(feature = "spdx-text")]
pub fn aggregate_license_document(&self, emit_interesting: bool) -> Result<String> {
let mut lines = vec![self.license_summary()];
lines.push("".into());
if emit_interesting {
if let Some(value) = self.interesting_report() {
lines.push(value);
lines.push("".into());
}
}
lines.push("Software Components".to_string());
lines.push("===================".to_string());
lines.push("".into());
for component in self.iter_components() {
lines.push(component.flavor().to_string());
lines.push("-".repeat(component.flavor().to_string().len()));
lines.push("".into());
lines.push(component.licensing_summary());
lines.push("".into());
if component.spdx_expression().is_some() && component.license_texts.is_empty() {
lines.push("The license texts for this component are reproduced elsewhere in this document.".into());
}
for exception in component.all_spdx_exception_ids() {
lines.push("".into());
lines.push(format!(
"In addition to the standard SPDX license, this component has the license exception: {}",
exception.name
));
lines.push("The text of that exception follows.".into());
lines.push("".into());
lines.push(exception.text().to_string());
lines.push(format!("(end of exception text for {})", exception.name));
}
if !component.license_texts().is_empty() {
lines.push("".into());
lines.push("The license text for this component is as follows.".into());
lines.push("".into());
lines.push("-".repeat(80).to_string());
for text in component.license_texts() {
lines.push(text.to_string());
}
lines.push("".into());
lines.push("-".repeat(80).to_string());
lines.push(format!("(end of license text for {})", component.flavor()));
}
lines.push("".into());
}
lines.push("SPDX License Texts".into());
lines.push("==================".into());
lines.push("".into());
lines.push("The following sections contain license texts for all SPDX licenses".into());
lines.push("referenced by software components listed above.".into());
lines.push("".into());
for license in self.all_spdx_license_ids() {
let header = format!("{} / {}", license.name, license.full_name);
lines.push(header.clone());
lines.push("-".repeat(header.len()));
lines.push("".into());
lines.push(license.text().to_string());
lines.push("".into());
}
let text = lines.join("\n");
Ok(text)
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct PackageLicenseInfo {
pub package: String,
pub version: String,
pub metadata_licenses: Vec<String>,
pub classifier_licenses: Vec<String>,
pub license_texts: Vec<String>,
pub notice_texts: Vec<String>,
pub is_public_domain: bool,
pub homepage: Option<String>,
pub authors: Vec<String>,
}
impl TryInto<LicensedComponent> for PackageLicenseInfo {
type Error = anyhow::Error;
fn try_into(self) -> Result<LicensedComponent, Self::Error> {
let component_flavor = ComponentFlavor::PythonModule(self.package.clone());
let mut component = if self.is_public_domain {
LicensedComponent::new(component_flavor, LicenseFlavor::PublicDomain)
} else if !self.metadata_licenses.is_empty() || !self.classifier_licenses.is_empty() {
let mut spdx_license_ids = BTreeSet::new();
let mut non_spdx_licenses = BTreeSet::new();
for s in self
.metadata_licenses
.into_iter()
.chain(self.classifier_licenses.into_iter())
{
if let Some(lid) = spdx::license_id(&s) {
spdx_license_ids.insert(format!("({})", lid.name));
} else if spdx::Expression::parse(&s).is_ok() {
spdx_license_ids.insert(format!("({})", s));
} else if let Some(name) = spdx::identifiers::LICENSES
.iter()
.find_map(|(name, full, _)| if &s == full { Some(name) } else { None })
{
spdx_license_ids.insert(name.to_string());
} else {
non_spdx_licenses.insert(s);
}
}
if non_spdx_licenses.is_empty() {
let expression = spdx_license_ids
.into_iter()
.collect::<Vec<_>>()
.join(" OR ");
LicensedComponent::new_spdx(component_flavor, &expression)?
} else {
LicensedComponent::new(
component_flavor,
LicenseFlavor::Unknown(non_spdx_licenses.into_iter().collect::<Vec<_>>()),
)
}
} else {
LicensedComponent::new(component_flavor, LicenseFlavor::None)
};
for text in self
.license_texts
.into_iter()
.chain(self.notice_texts.into_iter())
{
component.add_license_text(text);
}
if let Some(value) = self.homepage {
component.set_homepage(value);
}
for value in self.authors {
component.add_author(value);
}
Ok(component)
}
}
impl PartialOrd for PackageLicenseInfo {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
if self.package == other.package {
self.version.partial_cmp(&other.version)
} else {
self.package.partial_cmp(&other.package)
}
}
}
impl Ord for PackageLicenseInfo {
fn cmp(&self, other: &Self) -> Ordering {
if self.package == other.package {
self.version.cmp(&other.version)
} else {
self.package.cmp(&other.package)
}
}
}
pub fn derive_package_license_infos<'a>(
resources: impl Iterator<Item = &'a PythonResource<'a>>,
) -> Result<Vec<PackageLicenseInfo>> {
let mut packages = BTreeMap::new();
let resources = resources.filter_map(|resource| {
if let PythonResource::PackageDistributionResource(resource) = resource {
Some(resource)
} else {
None
}
});
for resource in resources {
let key = (resource.package.clone(), resource.version.clone());
let entry = packages.entry(key).or_insert(PackageLicenseInfo {
package: resource.package.clone(),
version: resource.version.clone(),
..Default::default()
});
if resource.name == "METADATA" || resource.name == "PKG-INFO" {
let metadata = PythonPackageMetadata::from_metadata(&resource.data.resolve_content()?)
.context("parsing package metadata")?;
if let Some(value) = metadata.find_first_header("Home-page") {
entry.homepage = Some(value.to_string());
}
for value in metadata.find_all_headers("Author") {
entry.authors.push(value.to_string());
}
for value in metadata.find_all_headers("Maintainer") {
entry.authors.push(value.to_string());
}
for value in metadata.find_all_headers("License") {
entry.metadata_licenses.push(value.to_string());
}
for value in metadata.find_all_headers("Classifier") {
if value.starts_with("License ") {
if let Some(license) = value.split(" :: ").last() {
if license != "OSI Approved" {
entry.classifier_licenses.push(license.to_string());
}
}
}
}
}
else if resource.name.starts_with("LICENSE")
|| resource.name.starts_with("LICENSE")
|| resource.name.starts_with("COPYING")
{
let data = resource.data.resolve_content()?;
let license_text = String::from_utf8_lossy(&data);
entry.license_texts.push(license_text.to_string());
}
else if resource.name.starts_with("NOTICE") {
let data = resource.data.resolve_content()?;
let notice_text = String::from_utf8_lossy(&data);
entry.notice_texts.push(notice_text.to_string());
}
}
Ok(packages.into_values().collect::<Vec<_>>())
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::resource::{
PythonPackageDistributionResource, PythonPackageDistributionResourceFlavor,
},
simple_file_manifest::FileData,
std::borrow::Cow,
};
#[test]
fn component_flavor_equivalence() {
assert_eq!(
ComponentFlavor::PythonDistribution("foo".to_string()),
ComponentFlavor::PythonDistribution("foo".to_string())
);
assert_ne!(
ComponentFlavor::PythonDistribution("foo".to_string()),
ComponentFlavor::PythonStandardLibraryModule("foo".into())
);
assert_eq!(
ComponentFlavor::PythonStandardLibraryModule("foo".into()),
ComponentFlavor::PythonStandardLibraryModule("foo".into())
);
assert_eq!(
ComponentFlavor::PythonStandardLibraryModule("foo".into()),
ComponentFlavor::PythonStandardLibraryExtensionModule("foo".into())
);
assert_eq!(
ComponentFlavor::PythonStandardLibraryModule("foo".into()),
ComponentFlavor::PythonExtensionModule("foo".into())
);
assert_eq!(
ComponentFlavor::PythonStandardLibraryModule("foo".into()),
ComponentFlavor::PythonModule("foo".into())
);
assert_ne!(
ComponentFlavor::PythonStandardLibraryModule("foo".into()),
ComponentFlavor::PythonStandardLibraryModule("bar".into())
);
assert_ne!(
ComponentFlavor::PythonStandardLibraryModule("foo".into()),
ComponentFlavor::PythonStandardLibraryExtensionModule("bar".into())
);
assert_ne!(
ComponentFlavor::PythonStandardLibraryModule("foo".into()),
ComponentFlavor::PythonExtensionModule("bar".into())
);
assert_ne!(
ComponentFlavor::PythonStandardLibraryModule("foo".into()),
ComponentFlavor::PythonModule("bar".into())
);
}
#[test]
fn parse_advanced() -> Result<()> {
LicensedComponent::new_spdx(
ComponentFlavor::PythonDistribution("foo".into()),
"Apache-2.0 OR MPL-2.0 OR 0BSD",
)?;
LicensedComponent::new_spdx(
ComponentFlavor::PythonDistribution("foo".into()),
"Apache-2.0 AND MPL-2.0 AND 0BSD",
)?;
LicensedComponent::new_spdx(
ComponentFlavor::PythonDistribution("foo".into()),
"Apache-2.0 AND MPL-2.0 OR 0BSD",
)?;
LicensedComponent::new_spdx(
ComponentFlavor::PythonDistribution("foo".into()),
"MIT AND (LGPL-2.1-or-later OR BSD-3-Clause)",
)?;
Ok(())
}
#[test]
fn test_derive_package_license_infos_empty() -> Result<()> {
let infos = derive_package_license_infos(vec![].iter())?;
assert!(infos.is_empty());
Ok(())
}
#[test]
fn test_derive_package_license_infos_license_file() -> Result<()> {
let resources = vec![PythonResource::PackageDistributionResource(Cow::Owned(
PythonPackageDistributionResource {
location: PythonPackageDistributionResourceFlavor::DistInfo,
package: "foo".to_string(),
version: "1.0".to_string(),
name: "LICENSE".to_string(),
data: FileData::Memory(vec![42]),
},
))];
let infos = derive_package_license_infos(resources.iter())?;
assert_eq!(infos.len(), 1);
assert_eq!(
infos[0],
PackageLicenseInfo {
package: "foo".to_string(),
version: "1.0".to_string(),
license_texts: vec!["*".to_string()],
..Default::default()
}
);
Ok(())
}
#[test]
fn test_derive_package_license_infos_metadata_licenses() -> Result<()> {
let resources = vec![PythonResource::PackageDistributionResource(Cow::Owned(
PythonPackageDistributionResource {
location: PythonPackageDistributionResourceFlavor::DistInfo,
package: "foo".to_string(),
version: "1.0".to_string(),
name: "METADATA".to_string(),
data: FileData::Memory(
"Name: foo\nLicense: BSD-1-Clause\nLicense: BSD-2-Clause\n"
.as_bytes()
.to_vec(),
),
},
))];
let infos = derive_package_license_infos(resources.iter())?;
assert_eq!(infos.len(), 1);
assert_eq!(
infos[0],
PackageLicenseInfo {
package: "foo".to_string(),
version: "1.0".to_string(),
metadata_licenses: vec!["BSD-1-Clause".to_string(), "BSD-2-Clause".to_string()],
..Default::default()
}
);
Ok(())
}
#[test]
fn test_derive_package_license_infos_metadata_classifiers() -> Result<()> {
let resources = vec![PythonResource::PackageDistributionResource(Cow::Owned(
PythonPackageDistributionResource {
location: PythonPackageDistributionResourceFlavor::DistInfo,
package: "foo".to_string(),
version: "1.0".to_string(),
name: "METADATA".to_string(),
data: FileData::Memory(
"Name: foo\nClassifier: License :: OSI Approved\nClassifier: License :: OSI Approved :: BSD-1-Clause\n"
.as_bytes()
.to_vec(),
),
},
))];
let infos = derive_package_license_infos(resources.iter())?;
assert_eq!(infos.len(), 1);
assert_eq!(
infos[0],
PackageLicenseInfo {
package: "foo".to_string(),
version: "1.0".to_string(),
classifier_licenses: vec!["BSD-1-Clause".to_string()],
..Default::default()
}
);
Ok(())
}
#[test]
fn license_info_to_component_empty() -> Result<()> {
let li = PackageLicenseInfo {
package: "foo".to_string(),
version: "0.1".to_string(),
..Default::default()
};
let c: LicensedComponent = li.try_into()?;
let wanted = LicensedComponent::new(
ComponentFlavor::PythonModule("foo".to_string()),
LicenseFlavor::None,
);
assert_eq!(c, wanted);
Ok(())
}
#[test]
fn license_info_to_component_single_metadata_spdx() -> Result<()> {
let li = PackageLicenseInfo {
package: "foo".to_string(),
version: "0.1".to_string(),
metadata_licenses: vec!["MIT".to_string()],
..Default::default()
};
let c: LicensedComponent = li.try_into()?;
let wanted =
LicensedComponent::new_spdx(ComponentFlavor::PythonModule("foo".to_string()), "MIT")?;
assert_eq!(c, wanted);
Ok(())
}
#[test]
fn license_info_to_component_single_classifier_spdx() -> Result<()> {
let li = PackageLicenseInfo {
package: "foo".to_string(),
version: "0.1".to_string(),
classifier_licenses: vec!["Apache-2.0".to_string()],
..Default::default()
};
let c: LicensedComponent = li.try_into()?;
let wanted = LicensedComponent::new_spdx(
ComponentFlavor::PythonModule("foo".to_string()),
"Apache-2.0",
)?;
assert_eq!(c, wanted);
Ok(())
}
#[test]
fn license_info_to_component_multiple_metadata_spdx() -> Result<()> {
let li = PackageLicenseInfo {
package: "foo".to_string(),
version: "0.1".to_string(),
metadata_licenses: vec!["MIT".to_string(), "Apache-2.0".to_string()],
..Default::default()
};
let c: LicensedComponent = li.try_into()?;
let wanted = LicensedComponent::new_spdx(
ComponentFlavor::PythonModule("foo".to_string()),
"Apache-2.0 OR MIT",
)?;
assert_eq!(c, wanted);
Ok(())
}
#[test]
fn license_info_to_component_multiple_classifier_spdx() -> Result<()> {
let li = PackageLicenseInfo {
package: "foo".to_string(),
version: "0.1".to_string(),
classifier_licenses: vec!["Apache-2.0".to_string(), "MIT".to_string()],
..Default::default()
};
let c: LicensedComponent = li.try_into()?;
let wanted = LicensedComponent::new_spdx(
ComponentFlavor::PythonModule("foo".to_string()),
"Apache-2.0 OR MIT",
)?;
assert_eq!(c, wanted);
Ok(())
}
#[test]
fn license_info_to_component_spdx_expression() -> Result<()> {
let li = PackageLicenseInfo {
package: "foo".to_string(),
version: "0.1".to_string(),
metadata_licenses: vec!["MIT OR Apache-2.0".to_string()],
..Default::default()
};
let c: LicensedComponent = li.try_into()?;
let wanted = LicensedComponent::new_spdx(
ComponentFlavor::PythonModule("foo".to_string()),
"MIT OR Apache-2.0",
)?;
assert_eq!(c, wanted);
Ok(())
}
#[test]
fn license_info_to_component_spdx_fullname() -> Result<()> {
let li = PackageLicenseInfo {
package: "foo".to_string(),
version: "0.1".to_string(),
metadata_licenses: vec!["MIT License".to_string()],
..Default::default()
};
let c: LicensedComponent = li.try_into()?;
let wanted =
LicensedComponent::new_spdx(ComponentFlavor::PythonModule("foo".to_string()), "MIT")?;
assert_eq!(c, wanted);
Ok(())
}
#[test]
fn license_info_to_component_unknown() -> Result<()> {
let terms = vec!["Unknown".to_string(), "Unknown 2".to_string()];
let li = PackageLicenseInfo {
package: "foo".to_string(),
version: "0.1".to_string(),
metadata_licenses: terms.clone(),
..Default::default()
};
let c: LicensedComponent = li.try_into()?;
let wanted = LicensedComponent::new(
ComponentFlavor::PythonModule("foo".to_string()),
LicenseFlavor::Unknown(terms),
);
assert_eq!(c, wanted);
Ok(())
}
}