use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct LicenseEvidence {
pub spdx: String,
pub source: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub archived_proof: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub attribution: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
}
pub fn license_allowed(spdx: &str) -> Result<(), String> {
let s = spdx.trim().to_ascii_uppercase();
if s.is_empty() {
return Err("empty license".to_string());
}
if s.contains("-NC") || s.contains("NONCOMMERCIAL") {
return Err("NonCommercial (NC) is forbidden".to_string());
}
if s.contains("-ND") || s.contains("NODERIV") {
return Err("NoDerivatives (ND) is forbidden".to_string());
}
if s.contains("-SA") || s.contains("SHAREALIKE") {
return Err("ShareAlike (SA) not admitted for assets".to_string());
}
let ok = s == "ORIGINAL"
|| s == "CC0"
|| s.starts_with("CC0-")
|| s == "CC-BY"
|| s.starts_with("CC-BY-") || s == "MIT"
|| s == "APACHE-2.0"
|| s == "BSD-2-CLAUSE"
|| s == "BSD-3-CLAUSE"
|| s == "GPL-3.0-ONLY"
|| s == "GPL-3.0-OR-LATER"
|| s == "LGPL-3.0-ONLY"
|| s == "LGPL-3.0-OR-LATER";
if ok {
Ok(())
} else {
Err(format!("`{spdx}` is not in the ADR-0013 allowlist"))
}
}
pub fn image_license_refusals(ev: &LicenseEvidence) -> Vec<String> {
let mut out = Vec::new();
if let Err(reason) = license_allowed(&ev.spdx) {
out.push(format!(
"license `{}` is refused: {reason} — only CC0, CC-BY, MIT, Apache-2.0, \
GPL-3.0-compatible, or `original` images may ship (ADR-0013); use an image \
under one of those, or draw one",
ev.spdx
));
return out;
}
let spdx = ev.spdx.trim().to_ascii_uppercase();
let blank = |v: &Option<String>| v.as_deref().unwrap_or("").trim().is_empty();
if spdx == "ORIGINAL" {
if !ev.source.trim().eq_ignore_ascii_case("original") {
out.push(format!(
"license `original` with `source` `{}` — an original image is made for this \
project and has no other source; set `source` to `original`, or record the \
licence the image was taken under",
ev.source
));
}
return out;
}
if blank(&ev.url) {
out.push(format!(
"license `{}` has no `url` — a licence must be verifiable, not just 'free to \
download'; add `license.url` pointing at the licence page",
ev.spdx
));
}
if (spdx == "CC-BY" || spdx.starts_with("CC-BY-")) && blank(&ev.attribution) {
out.push(format!(
"license `{}` has no `attribution` — a CC BY licence obliges a credit line, and \
the release's attribution is built from this field; add `license.attribution`",
ev.spdx
));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn ev(spdx: &str, source: &str) -> LicenseEvidence {
LicenseEvidence {
spdx: spdx.into(),
source: source.into(),
url: None,
archived_proof: None,
attribution: None,
note: None,
}
}
#[test]
fn an_original_image_needs_only_its_source() {
assert!(image_license_refusals(&ev("original", "original")).is_empty());
assert_eq!(image_license_refusals(&ev("original", "a site")).len(), 1);
}
#[test]
fn a_third_party_image_needs_its_url_and_a_cc_by_one_its_credit() {
let mut e = ev("CC-BY-4.0", "a site");
assert_eq!(image_license_refusals(&e).len(), 2);
e.url = Some("https://example.org/licence".into());
assert_eq!(image_license_refusals(&e).len(), 1);
e.attribution = Some("Artist — Title".into());
assert!(image_license_refusals(&e).is_empty());
let mut m = ev("MIT", "a repo");
m.url = Some("https://example.org/licence".into());
assert!(image_license_refusals(&m).is_empty());
}
#[test]
fn the_allowlist_refuses_first_and_alone() {
let r = image_license_refusals(&ev("CC-BY-NC-4.0", "a site"));
assert_eq!(r.len(), 1);
assert!(r[0].contains("NonCommercial"), "{r:?}");
}
}