#![cfg_attr(docsrs, doc(include = "../../docs/licenses/cfg.md"))]
use crate::{
cfg::{deprecated, PackageSpec, ValidationContext},
diag::{Diagnostic, FileId, Label},
LintLevel, PathBuf, Span, Spanned,
};
use toml_span::{de_helpers::TableHelper, value::Value, DeserError, Deserialize};
const DEFAULT_CONFIDENCE_THRESHOLD: f32 = 0.8;
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, strum::VariantArray, strum::VariantNames)]
#[cfg_attr(test, derive(serde::Serialize))]
#[strum(serialize_all = "kebab-case")]
pub enum BlanketAgreement {
Both,
Either,
Osi,
Fsf,
OsiOnly,
FsfOnly,
#[default]
Neither,
}
crate::enum_deser!(BlanketAgreement);
#[derive(Default)]
#[cfg_attr(test, derive(serde::Serialize))]
pub struct Private {
pub ignore: bool,
pub ignore_sources: Vec<Spanned<String>>,
pub registries: Vec<String>,
}
impl<'de> Deserialize<'de> for Private {
fn deserialize(value: &mut Value<'de>) -> Result<Self, DeserError> {
let mut th = TableHelper::new(value)?;
let ignore = th.optional("ignore").unwrap_or_default();
let ignore_sources = th.optional("ignore-sources").unwrap_or_default();
let registries = th.optional("registries").unwrap_or_default();
th.finalize(None)?;
Ok(Self {
ignore,
ignore_sources,
registries,
})
}
}
#[derive(PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize))]
pub struct FileSource {
pub path: Spanned<PathBuf>,
pub hash: u32,
}
impl<'de> Deserialize<'de> for FileSource {
fn deserialize(value: &mut Value<'de>) -> Result<Self, DeserError> {
let mut th = TableHelper::new(value)?;
let path: Spanned<String> = th.required("path")?;
let hash = th.required("hash")?;
th.finalize(None)?;
Ok(Self {
path: path.map(),
hash,
})
}
}
pub struct Clarification {
pub spec: PackageSpec,
pub expression: Spanned<String>,
pub license_files: Vec<FileSource>,
}
impl<'de> Deserialize<'de> for Clarification {
fn deserialize(value: &mut Value<'de>) -> Result<Self, DeserError> {
let spec = PackageSpec::deserialize(value)?;
let mut th = TableHelper::new(value)?;
let expression = th.required("expression")?;
let license_files = th.required("license-files")?;
th.finalize(None)?;
Ok(Self {
spec,
expression,
license_files,
})
}
}
pub struct Exception {
pub spec: PackageSpec,
pub allow: Vec<Licensee>,
}
impl<'de> Deserialize<'de> for Exception {
fn deserialize(value: &mut Value<'de>) -> Result<Self, DeserError> {
let spec = PackageSpec::deserialize(value)?;
let mut th = TableHelper::new(value)?;
let allow = th.required("allow")?;
th.finalize(None)?;
Ok(Self { spec, allow })
}
}
#[derive(Debug, PartialEq, Eq, Ord, PartialOrd)]
pub struct Licensee(pub Spanned<spdx::Licensee>);
impl<'de> Deserialize<'de> for Licensee {
fn deserialize(value: &mut Value<'de>) -> Result<Self, DeserError> {
let val = value.take_string(Some("an SPDX licensee string"))?;
match spdx::Licensee::parse(&val) {
Ok(licensee) => Ok(Self(Spanned::with_span(licensee, value.span))),
Err(pe) => {
let offset = value.span.start;
Err(toml_span::Error {
kind: toml_span::ErrorKind::Custom(pe.reason.to_string().into()),
span: (pe.span.start + offset..pe.span.end + offset).into(),
line_info: None,
}
.into())
}
}
}
}
#[cfg(test)]
impl serde::Serialize for Licensee {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.0.value.to_string().serialize(serializer)
}
}
#[cfg_attr(test, derive(serde::Serialize))]
pub(crate) struct Deprecated {
pub unlicensed: LintLevel,
pub allow_osi_fsf_free: BlanketAgreement,
pub copyleft: LintLevel,
pub default: LintLevel,
pub deny: Vec<Licensee>,
}
pub struct Config {
pub private: Private,
pub confidence_threshold: f32,
pub allow: Vec<Licensee>,
pub unused_allowed_license: LintLevel,
pub clarify: Vec<Clarification>,
pub exceptions: Vec<Exception>,
pub include_dev: bool,
deprecated: Option<Deprecated>,
deprecated_spans: Vec<Span>,
}
impl Default for Config {
fn default() -> Self {
Self {
private: Private::default(),
unused_allowed_license: LintLevel::Warn,
confidence_threshold: DEFAULT_CONFIDENCE_THRESHOLD,
allow: Vec::new(),
clarify: Vec::new(),
exceptions: Vec::new(),
include_dev: false,
deprecated: None,
deprecated_spans: Vec::new(),
}
}
}
impl<'de> Deserialize<'de> for Config {
fn deserialize(value: &mut Value<'de>) -> Result<Self, DeserError> {
let mut th = TableHelper::new(value)?;
let version = th.optional("version").unwrap_or(1);
let mut fdeps = Vec::new();
let private = th.optional("private").unwrap_or_default();
let unlicensed = deprecated(&mut th, "unlicensed", &mut fdeps).unwrap_or(LintLevel::Deny);
let allow_osi_fsf_free =
deprecated(&mut th, "allow-osi-fsf-free", &mut fdeps).unwrap_or_default();
let copyleft = deprecated(&mut th, "copyleft", &mut fdeps).unwrap_or(LintLevel::Warn);
let default = deprecated(&mut th, "default", &mut fdeps).unwrap_or(LintLevel::Deny);
let confidence_threshold = th
.optional("confidence-threshold")
.unwrap_or(DEFAULT_CONFIDENCE_THRESHOLD);
let deny = deprecated(&mut th, "deny", &mut fdeps).unwrap_or_default();
let allow = th.optional("allow").unwrap_or_default();
let unused_allowed_license = th
.optional("unused-allowed-license")
.unwrap_or(LintLevel::Warn);
let clarify = th.optional("clarify").unwrap_or_default();
let exceptions = th.optional("exceptions").unwrap_or_default();
let include_dev = th.optional("include-dev").unwrap_or_default();
th.finalize(None)?;
let deprecated = if version <= 1 {
Some(Deprecated {
unlicensed,
allow_osi_fsf_free,
copyleft,
default,
deny,
})
} else {
None
};
Ok(Self {
private,
confidence_threshold,
allow,
unused_allowed_license,
clarify,
exceptions,
include_dev,
deprecated,
deprecated_spans: fdeps,
})
}
}
impl crate::cfg::UnvalidatedConfig for Config {
type ValidCfg = ValidConfig;
fn validate(self, mut ctx: ValidationContext<'_>) -> 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::normalize_git_url(&mut url);
ignore_sources.push(url);
}
Err(pe) => {
ctx.push(
Diagnostic::error()
.with_message("failed to parse url")
.with_labels(vec![
Label::primary(ctx.cfg_id, aurl.span).with_message(pe.to_string())
]),
);
}
}
}
let mut deprecated = self.deprecated;
let mut denied = deprecated
.as_mut()
.map_or(Vec::new(), |d| std::mem::take(&mut d.deny));
let mut allowed = self.allow;
denied.par_sort();
allowed.par_sort();
let mut exceptions = Vec::with_capacity(self.exceptions.len());
exceptions.extend(self.exceptions.into_iter().map(|exc| ValidException {
spec: exc.spec,
allowed: exc.allow,
file_id: ctx.cfg_id,
}));
for (di, d) in denied.iter().enumerate() {
if let Ok(ai) = allowed.binary_search(d) {
ctx.push(
Diagnostic::error()
.with_message("a license id was specified in both `allow` and `deny`")
.with_labels(vec![
Label::secondary(ctx.cfg_id, denied[di].0.span).with_message("deny"),
Label::secondary(ctx.cfg_id, allowed[ai].0.span).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;
let expr_span = offset + err.span.start..offset + err.span.end;
ctx.push(
Diagnostic::error()
.with_message("unable to parse license expression")
.with_labels(vec![Label::primary(ctx.cfg_id, expr_span)
.with_message(err.reason.to_string())]),
);
continue;
}
};
let mut license_files = c.license_files;
license_files.sort_by(|a, b| a.path.cmp(&b.path));
clarifications.push(ValidClarification {
spec: c.spec,
expr_offset: c.expression.span.start,
expression: expr,
license_files,
});
}
use crate::diag::general::{Deprecated, DeprecationReason};
for dep in self.deprecated_spans {
ctx.push(
Deprecated {
reason: DeprecationReason::WillBeRemoved(Some(
"https://github.com/EmbarkStudios/cargo-deny/pull/611",
)),
key: dep,
file_id: ctx.cfg_id,
}
.into(),
);
}
ValidConfig {
file_id: ctx.cfg_id,
private: self.private,
unused_allowed_license: self.unused_allowed_license,
confidence_threshold: self.confidence_threshold,
clarifications,
exceptions,
denied,
allowed,
ignore_sources,
deprecated,
include_dev: self.include_dev,
}
}
}
pub fn load_exceptions(
cfg: &mut ValidConfig,
path: crate::PathBuf,
files: &mut crate::diag::Files,
diags: &mut Vec<Diagnostic>,
) {
let content = match std::fs::read_to_string(&path) {
Ok(c) => c,
Err(err) => {
diags.push(
Diagnostic::error()
.with_message("failed to read exceptions override")
.with_notes(vec![format!("path = '{path}'"), format!("error = {err:#}")]),
);
return;
}
};
let file_id = files.add(path, content);
let get_exceptions = || -> Result<Vec<Exception>, DeserError> {
let mut parsed = toml_span::parse(files.source(file_id))?;
let mut th = TableHelper::new(&mut parsed)?;
let exceptions = th.required("exceptions")?;
th.finalize(None)?;
Ok(exceptions)
};
match get_exceptions() {
Ok(exceptions) => {
cfg.exceptions.reserve(exceptions.len());
for exc in exceptions {
cfg.exceptions.push(ValidException {
spec: exc.spec,
allowed: exc.allow,
file_id,
});
}
}
Err(err) => {
diags.extend(err.errors.into_iter().map(|err| err.to_diagnostic(file_id)));
}
}
}
#[doc(hidden)]
#[cfg_attr(test, derive(PartialEq))]
pub struct ValidClarification {
pub spec: PackageSpec,
pub expr_offset: usize,
pub expression: spdx::Expression,
pub license_files: Vec<FileSource>,
}
#[cfg(test)]
impl serde::Serialize for ValidClarification {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeMap;
let mut map = serializer.serialize_map(Some(4))?;
map.serialize_entry("spec", &self.spec)?;
map.serialize_entry("expression", self.expression.as_ref())?;
map.serialize_entry("license-files", &self.license_files)?;
map.end()
}
}
#[doc(hidden)]
#[derive(Debug, PartialEq, Eq)]
#[cfg_attr(test, derive(serde::Serialize))]
pub struct ValidException {
pub spec: PackageSpec,
pub allowed: Vec<Licensee>,
pub file_id: FileId,
}
#[doc(hidden)]
#[cfg_attr(test, derive(serde::Serialize))]
pub struct ValidConfig {
pub file_id: FileId,
pub private: Private,
pub unused_allowed_license: 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>,
pub(crate) deprecated: Option<Deprecated>,
pub include_dev: bool,
}
#[cfg(test)]
mod test {
use super::*;
use crate::test_utils::{write_diagnostics, ConfigData};
struct Licenses {
licenses: Config,
}
impl<'de> toml_span::Deserialize<'de> for Licenses {
fn deserialize(
value: &mut toml_span::value::Value<'de>,
) -> Result<Self, toml_span::DeserError> {
let mut th = toml_span::de_helpers::TableHelper::new(value)?;
let licenses = th.required("licenses").unwrap();
th.finalize(None)?;
Ok(Self { licenses })
}
}
#[test]
fn deserializes_licenses_cfg() {
let cd = ConfigData::<Licenses>::load("tests/cfg/licenses.toml");
let validated = cd.validate_with_diags(
|l| l.licenses,
|files, diags| {
let diags = write_diagnostics(files, diags.into_iter());
insta::assert_snapshot!(diags);
},
);
insta::assert_json_snapshot!(validated);
}
#[test]
fn correct_duplicate_license_spans() {
let cfg = r#"[licenses]
allow = [
"MIT",
"Apache-2.0",
"BSD-3-Clause",
"ISC",
"CC0-1.0",
"Unicode-DFS-2016",
]
deny = [
"MIT",
"GPL-1.0",
"GPL-2.0",
"GPL-3.0",
"AGPL-3.0",
]"#;
let cd = ConfigData::<Licenses>::load_str("license-in-allow-and-deny", cfg);
let _validated = cd.validate_with_diags(
|l| l.licenses,
|files, diags| {
let diags = write_diagnostics(files, diags.into_iter());
insta::assert_snapshot!(diags);
},
);
}
}