use std::collections::BTreeMap;
use crate::error::{err, index, ErrorCode, Result};
use crate::json::Value;
use crate::keys::KeyDocument;
use crate::lexical::{self, limits};
use crate::receipt::Receipt;
use crate::schema;
const MAX_NAMES: usize = limits::DECLARATIONS;
const MAX_ENTRIES: usize = limits::DECLARATION_ENTRIES;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct Declaration {
pub licence: String,
pub valid_from: String,
pub valid_to: Option<String>,
}
impl Declaration {
pub fn new(licence: &str, valid_from: &str, valid_to: Option<&str>) -> Result<Declaration> {
entry_fields(licence, valid_from, valid_to, "$")
}
pub fn open_at(&self, issued: &str) -> bool {
self.valid_from.as_str() <= issued && self.valid_to.as_deref().is_none_or(|to| issued < to)
}
fn to_value(&self) -> Value {
let s = |x: &str| Value::String(x.into());
let mut m = vec![("licence".into(), s(&self.licence)), ("valid_from".into(), s(&self.valid_from))];
if let Some(t) = &self.valid_to {
m.push(("valid_to".into(), s(t)));
}
Value::Object(m)
}
}
fn entry_fields(licence: &str, from: &str, to: Option<&str>, p: &str) -> Result<Declaration> {
let kd = ErrorCode::KeyDoc;
lexical::check_licence(licence, &format!("{p}.licence")).map_err(|e| e.recode(kd))?;
if !lexical::is_instant(from) {
return err(kd, &format!("{p}.valid_from"), "valid_from must be an instant");
}
if let Some(t) = to {
if !lexical::is_instant(t) {
return err(kd, &format!("{p}.valid_to"), "valid_to must be an instant");
}
if t <= from {
return err(kd, &format!("{p}.valid_to"), "valid_to must be after valid_from");
}
}
Ok(Declaration { licence: licence.into(), valid_from: from.into(), valid_to: to.map(String::from) })
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct Declarations {
names: BTreeMap<String, Vec<Declaration>>,
}
impl Declarations {
pub fn new() -> Declarations {
Declarations::default()
}
pub fn from_value(v: &Value) -> Result<Declarations> {
parse(v, "$")
}
pub fn insert(&mut self, name: &str, entry: Declaration) -> Result<()> {
lexical::check_function_name(name, "$").map_err(|e| e.recode(ErrorCode::KeyDoc))?;
if !self.names.contains_key(name) && self.names.len() >= MAX_NAMES {
return err(ErrorCode::KeyDoc, "$", "at most 64 declared names");
}
let entries = self.names.entry(name.into()).or_default();
if entries.len() >= MAX_ENTRIES {
return err(ErrorCode::KeyDoc, "$", "at most 16 entries for one name");
}
entries.push(entry);
Ok(())
}
pub fn get(&self, name: &str) -> &[Declaration] {
self.names.get(name).map_or(&[], Vec::as_slice)
}
pub fn names(&self) -> impl Iterator<Item = &str> {
self.names.keys().map(String::as_str)
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &[Declaration])> {
self.names.iter().map(|(k, v)| (k.as_str(), v.as_slice()))
}
pub fn len(&self) -> usize {
self.names.len()
}
pub fn is_empty(&self) -> bool {
self.names.is_empty()
}
pub fn to_value(&self) -> Value {
Value::Object(
self.names
.iter()
.map(|(k, es)| (k.clone(), Value::Array(es.iter().map(Declaration::to_value).collect())))
.collect(),
)
}
pub(crate) fn check_host(&self, host: &str, path: &str) -> Result<()> {
if self.names.is_empty() {
return err(ErrorCode::KeyDoc, path, "declarations holds 1 to 64 names");
}
for name in self.names.keys() {
if lexical::host_of(name) != host {
return err(
ErrorCode::KeyDoc,
&format!("{path}.{name}"),
format!("a declared name is under the document's own host {host}"),
);
}
}
Ok(())
}
}
fn parse(v: &Value, path: &str) -> Result<Declarations> {
let kd = ErrorCode::KeyDoc;
let Value::Object(m) = v else {
return err(kd, path, "declarations must be an object of names");
};
if m.len() > MAX_NAMES {
return err(kd, path, "declarations holds at most 64 names");
}
let mut names = BTreeMap::new();
for (name, entries) in m {
let p = format!("{path}.{name}");
lexical::check_function_name(name, &p).map_err(|e| e.recode(kd))?;
let items = match entries {
Value::Array(a) if !a.is_empty() && a.len() <= MAX_ENTRIES => a,
_ => return err(kd, &p, "a declared name has 1 to 16 entries"),
};
let mut out = Vec::with_capacity(items.len());
for (i, item) in items.iter().enumerate() {
let ep = index(&p, i);
let em = schema::object_or(item, &ep, kd, "an entry")?;
schema::exact(em, &["licence", "valid_from"], &["valid_to"], &ep, kd)?;
let licence = schema::string_or(em, "licence", &ep, kd)?;
let from = schema::string_or(em, "valid_from", &ep, kd)?;
let to = match schema::opt(em, "valid_to") {
None => None,
Some(_) => Some(schema::string_or(em, "valid_to", &ep, kd)?),
};
out.push(entry_fields(licence, from, to, &ep)?);
}
names.insert(name.clone(), out);
}
Ok(Declarations { names })
}
pub(crate) fn declarations_member(v: &Value, host: &str) -> Result<Declarations> {
let d = parse(v, "$.declarations")?;
d.check_host(host, "$.declarations")?;
Ok(d)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum LicenceReason {
NoInputs,
Open,
Declared,
ShareAlikeConflict,
RestrictedConflict,
NotDeclared,
DeclarationNotOpen,
LicenceMismatch,
}
impl LicenceReason {
pub const ALL: &'static [LicenceReason] = &[
LicenceReason::NoInputs,
LicenceReason::Open,
LicenceReason::Declared,
LicenceReason::ShareAlikeConflict,
LicenceReason::RestrictedConflict,
LicenceReason::NotDeclared,
LicenceReason::DeclarationNotOpen,
LicenceReason::LicenceMismatch,
];
pub const fn as_str(self) -> &'static str {
match self {
LicenceReason::NoInputs => "no_inputs",
LicenceReason::Open => "open",
LicenceReason::Declared => "declared",
LicenceReason::ShareAlikeConflict => "share_alike_conflict",
LicenceReason::RestrictedConflict => "restricted_conflict",
LicenceReason::NotDeclared => "not_declared",
LicenceReason::DeclarationNotOpen => "declaration_not_open",
LicenceReason::LicenceMismatch => "licence_mismatch",
}
}
pub fn from_name(text: &str) -> Option<LicenceReason> {
LicenceReason::ALL.iter().copied().find(|r| r.as_str() == text)
}
pub const fn entitled(self) -> bool {
matches!(self, LicenceReason::NoInputs | LicenceReason::Open | LicenceReason::Declared)
}
}
impl core::fmt::Display for LicenceReason {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct Inheritance {
pub licence: Option<String>,
pub reason: LicenceReason,
}
impl Inheritance {
pub fn refused(&self) -> bool {
self.licence.is_none()
}
}
const TIERS: [&[&str]; 3] =
[&["CC0-1.0", "PDDL-1.0"], &["MIT", "Apache-2.0", "CC-BY-4.0", "OGL-UK-3.0"], &["CC-BY-SA-4.0", "ODbL-1.0"]];
fn open_rank(licence: &str) -> Option<(usize, usize)> {
TIERS.iter().enumerate().find_map(|(t, ids)| ids.iter().position(|x| *x == licence).map(|i| (t, i)))
}
pub fn inherit_licence<I, S>(
licences: I,
declarations: Option<&Declarations>,
function: &str,
issued: &str,
mesh_licence: &str,
) -> Inheritance
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let all: Vec<S> = licences.into_iter().collect();
let give = |l: &str, reason| Inheritance { licence: Some(l.into()), reason };
let refuse = |reason| Inheritance { licence: None, reason };
if all.is_empty() {
return give(mesh_licence, LicenceReason::NoInputs);
}
let mut share_alike: Vec<&str> = Vec::new();
let mut restricted: Vec<&str> = Vec::new();
let mut best: Option<(usize, usize)> = None;
for l in &all {
let l = l.as_ref();
match open_rank(l) {
Some((2, _)) if !share_alike.contains(&l) => share_alike.push(l),
None if !restricted.contains(&l) => restricted.push(l),
_ => {}
}
if let Some(r) = open_rank(l) {
best = best.max(Some(r));
}
}
if share_alike.len() > 1 || (!share_alike.is_empty() && !restricted.is_empty()) {
return refuse(LicenceReason::ShareAlikeConflict);
}
if restricted.len() > 1 {
return refuse(LicenceReason::RestrictedConflict);
}
if let Some(r) = restricted.first() {
let entries: Vec<&Declaration> =
declarations.map_or(&[][..], |d| d.get(function)).iter().filter(|e| e.licence == *r).collect();
if entries.is_empty() {
return refuse(LicenceReason::NotDeclared);
}
if !entries.iter().any(|e| e.open_at(issued)) {
return refuse(LicenceReason::DeclarationNotOpen);
}
return give(r, LicenceReason::Declared);
}
match best {
Some((t, i)) => give(TIERS[t][i], LicenceReason::Open),
None => refuse(LicenceReason::NotDeclared), }
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct Entitlement {
pub entitled: bool,
pub reason: LicenceReason,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct WrongPublisher {
expected: String,
got: String,
}
impl WrongPublisher {
pub fn expected(&self) -> &str {
&self.expected
}
pub fn got(&self) -> &str {
&self.got
}
}
impl core::fmt::Display for WrongPublisher {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "the entitlement check needs the key document of {}, not of {}", self.expected, self.got)
}
}
impl std::error::Error for WrongPublisher {}
pub fn check_entitlement<I, S>(
receipt: &Receipt,
input_licences: I,
key_document: &KeyDocument,
) -> core::result::Result<Entitlement, WrongPublisher>
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let cell = receipt.cell();
let publisher = lexical::host_of(&cell.ns);
if key_document.host() != publisher {
return Err(WrongPublisher { expected: publisher.into(), got: key_document.host().into() });
}
let function = format!("{}/{}", cell.ns, cell.fn_name);
let rule = inherit_licence(
input_licences,
key_document.declarations.as_ref(),
&function,
receipt.issued(),
receipt.licence(),
);
Ok(match rule.licence {
None => Entitlement { entitled: false, reason: rule.reason },
Some(l) if l != receipt.licence() => Entitlement { entitled: false, reason: LicenceReason::LicenceMismatch },
Some(_) => Entitlement { entitled: true, reason: rule.reason },
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn open_ranks_order_by_tier_then_place() {
assert_eq!(open_rank("CC0-1.0"), Some((0, 0)));
assert_eq!(open_rank("OGL-UK-3.0"), Some((1, 3)));
assert_eq!(open_rank("ODbL-1.0"), Some((2, 1)));
assert_eq!(open_rank("cc0-1.0"), None);
assert!(Some((1, 0)) > Some((0, 1)));
}
}