#![cfg_attr(docsrs, doc(include = "../../docs/licenses/cfg.md"))]
use crate::{
diag::{Diagnostic, FileId, Label},
LintLevel, Spanned,
};
use semver::VersionReq;
use serde::Deserialize;
use std::path::PathBuf;
const fn confidence_threshold() -> f32 {
0.8
}
#[derive(Deserialize, Debug, PartialEq, Eq)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub enum BlanketAgreement {
Both,
Either,
OsiOnly,
FsfOnly,
Neither,
}
impl Default for BlanketAgreement {
fn default() -> Self {
BlanketAgreement::Neither
}
}
#[derive(Deserialize, Default)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct Private {
#[serde(default)]
pub ignore: bool,
#[serde(default)]
pub ignore_sources: Vec<Spanned<String>>,
#[serde(default)]
pub registries: Vec<String>,
}
#[derive(PartialEq, Eq, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct FileSource {
pub path: Spanned<PathBuf>,
pub hash: u32,
}
#[derive(Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct Clarification {
pub name: String,
pub version: Option<VersionReq>,
pub expression: Spanned<String>,
pub license_files: Vec<FileSource>,
}
#[derive(Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct Exception {
pub name: Spanned<String>,
pub version: Option<VersionReq>,
pub allow: Vec<Spanned<String>>,
}
#[derive(Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct Config {
#[serde(default)]
pub private: Private,
#[serde(default = "crate::lint_deny")]
pub unlicensed: LintLevel,
#[serde(default)]
pub allow_osi_fsf_free: BlanketAgreement,
#[serde(default = "crate::lint_warn")]
pub copyleft: LintLevel,
#[serde(default = "crate::lint_deny")]
pub default: LintLevel,
#[serde(default = "confidence_threshold")]
pub confidence_threshold: f32,
#[serde(default)]
pub deny: Vec<Spanned<String>>,
#[serde(default)]
pub allow: Vec<Spanned<String>>,
#[serde(default = "crate::lint_warn")]
pub unused_allowed_license: LintLevel,
#[serde(default)]
pub clarify: Vec<Clarification>,
#[serde(default)]
pub exceptions: Vec<Exception>,
}
impl Default for Config {
fn default() -> Self {
Self {
private: Private::default(),
unlicensed: LintLevel::Deny,
allow_osi_fsf_free: BlanketAgreement::default(),
copyleft: LintLevel::Warn,
default: LintLevel::Deny,
unused_allowed_license: LintLevel::Warn,
confidence_threshold: confidence_threshold(),
deny: Vec::new(),
allow: Vec::new(),
clarify: Vec::new(),
exceptions: Vec::new(),
}
}
}
impl crate::cfg::UnvalidatedConfig for Config {
type ValidCfg = ValidConfig;
fn validate(self, cfg_file: FileId, diags: &mut Vec<Diagnostic>) -> Self::ValidCfg {
use rayon::prelude::*;
let mut ignore_sources = Vec::with_capacity(self.private.ignore_sources.len());
for aurl in &self.private.ignore_sources {
match url::Url::parse(aurl.as_ref()) {
Ok(mut url) => {
crate::sources::normalize_url(&mut url);
ignore_sources.push(url);
}
Err(pe) => {
diags.push(
Diagnostic::error()
.with_message("failed to parse url")
.with_labels(vec![Label::primary(cfg_file, aurl.span.clone())
.with_message(pe.to_string())]),
);
}
}
}
let mut parse_license = |ls: &Spanned<String>, v: &mut Vec<Licensee>| {
match spdx::Licensee::parse(ls.as_ref()) {
Ok(licensee) => {
v.push(Licensee::new(licensee, ls.span.clone()));
}
Err(pe) => {
let offset = ls.span.start + 1;
let span = pe.span.start + offset..pe.span.end + offset;
diags.push(
Diagnostic::error()
.with_message("invalid licensee")
.with_labels(vec![Label::primary(cfg_file, span)
.with_message(format!("{}", pe.reason))]),
);
}
}
};
let mut denied = Vec::with_capacity(self.deny.len());
for d in &self.deny {
parse_license(d, &mut denied);
}
let mut allowed: Vec<Licensee> = Vec::with_capacity(self.allow.len());
for a in &self.allow {
parse_license(a, &mut allowed);
}
denied.par_sort();
allowed.par_sort();
let mut exceptions = Vec::with_capacity(self.exceptions.len());
for exc in self.exceptions {
let mut allowed = Vec::with_capacity(exc.allow.len());
for allow in &exc.allow {
parse_license(allow, &mut allowed);
}
exceptions.push(ValidException {
name: exc.name,
version: exc.version,
allowed,
});
}
for (di, d) in denied.iter().enumerate() {
if let Ok(ai) = allowed.binary_search(d) {
diags.push(
Diagnostic::error()
.with_message("a license id was specified in both `allow` and `deny`")
.with_labels(vec![
Label::secondary(cfg_file, self.deny[di].span.clone())
.with_message("deny"),
Label::secondary(cfg_file, self.allow[ai].span.clone())
.with_message("allow"),
]),
);
}
}
let mut clarifications = Vec::with_capacity(self.clarify.len());
for c in self.clarify {
let expr = match spdx::Expression::parse(c.expression.as_ref()) {
Ok(validated) => validated,
Err(err) => {
let offset = c.expression.span.start + 1;
let expr_span = offset + err.span.start..offset + err.span.end;
diags.push(
Diagnostic::error()
.with_message("unable to parse license expression")
.with_labels(vec![Label::primary(cfg_file, expr_span)
.with_message(format!("{}", err.reason))]),
);
continue;
}
};
let mut license_files = c.license_files;
license_files.sort_by(|a, b| a.path.cmp(&b.path));
clarifications.push(ValidClarification {
name: c.name,
version: c.version,
expr_offset: (c.expression.span.start + 1),
expression: expr,
license_files,
});
}
ValidConfig {
file_id: cfg_file,
private: self.private,
unlicensed: self.unlicensed,
copyleft: self.copyleft,
default: self.default,
unused_allowed_license: self.unused_allowed_license,
allow_osi_fsf_free: self.allow_osi_fsf_free,
confidence_threshold: self.confidence_threshold,
clarifications,
exceptions,
denied,
allowed,
ignore_sources,
}
}
}
#[doc(hidden)]
#[cfg_attr(test, derive(Debug, PartialEq))]
pub struct ValidClarification {
pub name: String,
pub version: Option<VersionReq>,
pub expr_offset: usize,
pub expression: spdx::Expression,
pub license_files: Vec<FileSource>,
}
#[doc(hidden)]
#[derive(Debug, PartialEq, Eq)]
pub struct ValidException {
pub name: crate::Spanned<String>,
pub version: Option<VersionReq>,
pub allowed: Vec<Licensee>,
}
pub type Licensee = Spanned<spdx::Licensee>;
#[doc(hidden)]
pub struct ValidConfig {
pub file_id: FileId,
pub private: Private,
pub unlicensed: LintLevel,
pub copyleft: LintLevel,
pub unused_allowed_license: LintLevel,
pub allow_osi_fsf_free: BlanketAgreement,
pub default: LintLevel,
pub confidence_threshold: f32,
pub denied: Vec<Licensee>,
pub allowed: Vec<Licensee>,
pub clarifications: Vec<ValidClarification>,
pub exceptions: Vec<ValidException>,
pub ignore_sources: Vec<url::Url>,
}
#[cfg(test)]
mod test {
use super::*;
use crate::cfg::{test::*, *};
#[test]
fn deserializes_licenses_cfg() {
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Licenses {
licenses: Config,
}
let cd: ConfigData<Licenses> = load("tests/cfg/licenses.toml");
let mut diags = Vec::new();
let validated = cd.config.licenses.validate(cd.id, &mut diags);
assert!(diags.is_empty());
assert_eq!(validated.file_id, cd.id);
assert!(validated.private.ignore);
assert_eq!(validated.private.registries, vec!["sekrets".to_owned()]);
assert_eq!(validated.unlicensed, LintLevel::Warn);
assert_eq!(validated.copyleft, LintLevel::Deny);
assert_eq!(validated.unused_allowed_license, LintLevel::Warn);
assert_eq!(validated.default, LintLevel::Warn);
assert_eq!(validated.allow_osi_fsf_free, BlanketAgreement::Both);
assert_eq!(
validated.allowed,
vec![
spdx::Licensee::parse("Apache-2.0 WITH LLVM-exception").unwrap(),
spdx::Licensee::parse("EUPL-1.2").unwrap(),
]
);
assert_eq!(
validated.denied,
vec![
spdx::Licensee::parse("BSD-2-Clause").unwrap(),
spdx::Licensee::parse("Nokia").unwrap(),
]
);
assert_eq!(
validated.exceptions,
vec![ValidException {
name: "adler32".to_owned().fake(),
allowed: vec![spdx::Licensee::parse("Zlib").unwrap().fake()],
version: Some(semver::VersionReq::parse("0.1.1").unwrap()),
}]
);
let p: PathBuf = "LICENSE".into();
assert_eq!(
validated.clarifications,
vec![ValidClarification {
name: "ring".to_owned(),
version: None,
expression: spdx::Expression::parse("MIT AND ISC AND OpenSSL").unwrap(),
license_files: vec![FileSource {
path: p.fake(),
hash: 0xbd0e_ed23,
}],
expr_offset: 450,
}]
);
}
}