1use std::collections::BTreeMap;
9
10use crate::error::{err, index, ErrorCode, Result};
11use crate::json::Value;
12use crate::keys::KeyDocument;
13use crate::lexical::{self, limits};
14use crate::receipt::Receipt;
15use crate::schema;
16
17const MAX_NAMES: usize = limits::DECLARATIONS;
18const MAX_ENTRIES: usize = limits::DECLARATION_ENTRIES;
19
20#[derive(Clone, Debug, PartialEq, Eq, Hash)]
24#[non_exhaustive]
25pub struct Declaration {
26 pub licence: String,
28 pub valid_from: String,
30 pub valid_to: Option<String>,
32}
33
34impl Declaration {
35 pub fn new(licence: &str, valid_from: &str, valid_to: Option<&str>) -> Result<Declaration> {
38 entry_fields(licence, valid_from, valid_to, "$")
39 }
40
41 pub fn open_at(&self, issued: &str) -> bool {
44 self.valid_from.as_str() <= issued && self.valid_to.as_deref().is_none_or(|to| issued < to)
45 }
46
47 fn to_value(&self) -> Value {
48 let s = |x: &str| Value::String(x.into());
49 let mut m = vec![("licence".into(), s(&self.licence)), ("valid_from".into(), s(&self.valid_from))];
50 if let Some(t) = &self.valid_to {
51 m.push(("valid_to".into(), s(t)));
52 }
53 Value::Object(m)
54 }
55}
56
57fn entry_fields(licence: &str, from: &str, to: Option<&str>, p: &str) -> Result<Declaration> {
58 let kd = ErrorCode::KeyDoc;
59 lexical::check_licence(licence, &format!("{p}.licence")).map_err(|e| e.recode(kd))?;
60 if !lexical::is_instant(from) {
61 return err(kd, &format!("{p}.valid_from"), "valid_from must be an instant");
62 }
63 if let Some(t) = to {
64 if !lexical::is_instant(t) {
65 return err(kd, &format!("{p}.valid_to"), "valid_to must be an instant");
66 }
67 if t <= from {
68 return err(kd, &format!("{p}.valid_to"), "valid_to must be after valid_from");
69 }
70 }
71 Ok(Declaration { licence: licence.into(), valid_from: from.into(), valid_to: to.map(String::from) })
72}
73
74#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
79pub struct Declarations {
80 names: BTreeMap<String, Vec<Declaration>>,
81}
82
83impl Declarations {
84 pub fn new() -> Declarations {
86 Declarations::default()
87 }
88
89 pub fn from_value(v: &Value) -> Result<Declarations> {
97 parse(v, "$")
98 }
99
100 pub fn insert(&mut self, name: &str, entry: Declaration) -> Result<()> {
103 lexical::check_function_name(name, "$").map_err(|e| e.recode(ErrorCode::KeyDoc))?;
104 if !self.names.contains_key(name) && self.names.len() >= MAX_NAMES {
105 return err(ErrorCode::KeyDoc, "$", "at most 64 declared names");
106 }
107 let entries = self.names.entry(name.into()).or_default();
108 if entries.len() >= MAX_ENTRIES {
109 return err(ErrorCode::KeyDoc, "$", "at most 16 entries for one name");
110 }
111 entries.push(entry);
112 Ok(())
113 }
114
115 pub fn get(&self, name: &str) -> &[Declaration] {
117 self.names.get(name).map_or(&[], Vec::as_slice)
118 }
119
120 pub fn names(&self) -> impl Iterator<Item = &str> {
122 self.names.keys().map(String::as_str)
123 }
124
125 pub fn iter(&self) -> impl Iterator<Item = (&str, &[Declaration])> {
127 self.names.iter().map(|(k, v)| (k.as_str(), v.as_slice()))
128 }
129
130 pub fn len(&self) -> usize {
132 self.names.len()
133 }
134
135 pub fn is_empty(&self) -> bool {
137 self.names.is_empty()
138 }
139
140 pub fn to_value(&self) -> Value {
142 Value::Object(
143 self.names
144 .iter()
145 .map(|(k, es)| (k.clone(), Value::Array(es.iter().map(Declaration::to_value).collect())))
146 .collect(),
147 )
148 }
149
150 pub(crate) fn check_host(&self, host: &str, path: &str) -> Result<()> {
153 if self.names.is_empty() {
154 return err(ErrorCode::KeyDoc, path, "declarations holds 1 to 64 names");
155 }
156 for name in self.names.keys() {
157 if lexical::host_of(name) != host {
158 return err(
159 ErrorCode::KeyDoc,
160 &format!("{path}.{name}"),
161 format!("a declared name is under the document's own host {host}"),
162 );
163 }
164 }
165 Ok(())
166 }
167}
168
169fn parse(v: &Value, path: &str) -> Result<Declarations> {
170 let kd = ErrorCode::KeyDoc;
171 let Value::Object(m) = v else {
172 return err(kd, path, "declarations must be an object of names");
173 };
174 if m.len() > MAX_NAMES {
175 return err(kd, path, "declarations holds at most 64 names");
176 }
177 let mut names = BTreeMap::new();
178 for (name, entries) in m {
179 let p = format!("{path}.{name}");
180 lexical::check_function_name(name, &p).map_err(|e| e.recode(kd))?;
181 let items = match entries {
182 Value::Array(a) if !a.is_empty() && a.len() <= MAX_ENTRIES => a,
183 _ => return err(kd, &p, "a declared name has 1 to 16 entries"),
184 };
185 let mut out = Vec::with_capacity(items.len());
186 for (i, item) in items.iter().enumerate() {
187 let ep = index(&p, i);
188 let em = schema::object_or(item, &ep, kd, "an entry")?;
189 schema::exact(em, &["licence", "valid_from"], &["valid_to"], &ep, kd)?;
190 let licence = schema::string_or(em, "licence", &ep, kd)?;
191 let from = schema::string_or(em, "valid_from", &ep, kd)?;
192 let to = match schema::opt(em, "valid_to") {
193 None => None,
194 Some(_) => Some(schema::string_or(em, "valid_to", &ep, kd)?),
195 };
196 out.push(entry_fields(licence, from, to, &ep)?);
197 }
198 names.insert(name.clone(), out);
199 }
200 Ok(Declarations { names })
201}
202
203pub(crate) fn declarations_member(v: &Value, host: &str) -> Result<Declarations> {
205 let d = parse(v, "$.declarations")?;
206 d.check_host(host, "$.declarations")?;
207 Ok(d)
208}
209
210#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
214#[non_exhaustive]
215pub enum LicenceReason {
216 NoInputs,
219 Open,
221 Declared,
223 ShareAlikeConflict,
225 RestrictedConflict,
227 NotDeclared,
229 DeclarationNotOpen,
231 LicenceMismatch,
233}
234
235impl LicenceReason {
236 pub const ALL: &'static [LicenceReason] = &[
238 LicenceReason::NoInputs,
239 LicenceReason::Open,
240 LicenceReason::Declared,
241 LicenceReason::ShareAlikeConflict,
242 LicenceReason::RestrictedConflict,
243 LicenceReason::NotDeclared,
244 LicenceReason::DeclarationNotOpen,
245 LicenceReason::LicenceMismatch,
246 ];
247
248 pub const fn as_str(self) -> &'static str {
250 match self {
251 LicenceReason::NoInputs => "no_inputs",
252 LicenceReason::Open => "open",
253 LicenceReason::Declared => "declared",
254 LicenceReason::ShareAlikeConflict => "share_alike_conflict",
255 LicenceReason::RestrictedConflict => "restricted_conflict",
256 LicenceReason::NotDeclared => "not_declared",
257 LicenceReason::DeclarationNotOpen => "declaration_not_open",
258 LicenceReason::LicenceMismatch => "licence_mismatch",
259 }
260 }
261
262 pub fn from_name(text: &str) -> Option<LicenceReason> {
264 LicenceReason::ALL.iter().copied().find(|r| r.as_str() == text)
265 }
266
267 pub const fn entitled(self) -> bool {
270 matches!(self, LicenceReason::NoInputs | LicenceReason::Open | LicenceReason::Declared)
271 }
272}
273
274impl core::fmt::Display for LicenceReason {
275 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
276 f.write_str(self.as_str())
277 }
278}
279
280#[derive(Clone, Debug, PartialEq, Eq, Hash)]
283#[non_exhaustive]
284pub struct Inheritance {
285 pub licence: Option<String>,
287 pub reason: LicenceReason,
290}
291
292impl Inheritance {
293 pub fn refused(&self) -> bool {
295 self.licence.is_none()
296 }
297}
298
299const TIERS: [&[&str]; 3] =
301 [&["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"]];
302
303fn open_rank(licence: &str) -> Option<(usize, usize)> {
306 TIERS.iter().enumerate().find_map(|(t, ids)| ids.iter().position(|x| *x == licence).map(|i| (t, i)))
307}
308
309pub fn inherit_licence<I, S>(
330 licences: I,
331 declarations: Option<&Declarations>,
332 function: &str,
333 issued: &str,
334 mesh_licence: &str,
335) -> Inheritance
336where
337 I: IntoIterator<Item = S>,
338 S: AsRef<str>,
339{
340 let all: Vec<S> = licences.into_iter().collect();
341 let give = |l: &str, reason| Inheritance { licence: Some(l.into()), reason };
342 let refuse = |reason| Inheritance { licence: None, reason };
343 if all.is_empty() {
345 return give(mesh_licence, LicenceReason::NoInputs);
346 }
347 let mut share_alike: Vec<&str> = Vec::new();
348 let mut restricted: Vec<&str> = Vec::new();
349 let mut best: Option<(usize, usize)> = None;
350 for l in &all {
351 let l = l.as_ref();
352 match open_rank(l) {
353 Some((2, _)) if !share_alike.contains(&l) => share_alike.push(l),
354 None if !restricted.contains(&l) => restricted.push(l),
355 _ => {}
356 }
357 if let Some(r) = open_rank(l) {
358 best = best.max(Some(r));
359 }
360 }
361 if share_alike.len() > 1 || (!share_alike.is_empty() && !restricted.is_empty()) {
363 return refuse(LicenceReason::ShareAlikeConflict);
364 }
365 if restricted.len() > 1 {
367 return refuse(LicenceReason::RestrictedConflict);
368 }
369 if let Some(r) = restricted.first() {
371 let entries: Vec<&Declaration> =
372 declarations.map_or(&[][..], |d| d.get(function)).iter().filter(|e| e.licence == *r).collect();
373 if entries.is_empty() {
374 return refuse(LicenceReason::NotDeclared);
375 }
376 if !entries.iter().any(|e| e.open_at(issued)) {
377 return refuse(LicenceReason::DeclarationNotOpen);
378 }
379 return give(r, LicenceReason::Declared);
380 }
381 match best {
383 Some((t, i)) => give(TIERS[t][i], LicenceReason::Open),
384 None => refuse(LicenceReason::NotDeclared), }
386}
387
388#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
391#[non_exhaustive]
392pub struct Entitlement {
393 pub entitled: bool,
395 pub reason: LicenceReason,
397}
398
399#[derive(Clone, Debug, PartialEq, Eq, Hash)]
402pub struct WrongPublisher {
403 expected: String,
404 got: String,
405}
406
407impl WrongPublisher {
408 pub fn expected(&self) -> &str {
410 &self.expected
411 }
412
413 pub fn got(&self) -> &str {
415 &self.got
416 }
417}
418
419impl core::fmt::Display for WrongPublisher {
420 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
421 write!(f, "the entitlement check needs the key document of {}, not of {}", self.expected, self.got)
422 }
423}
424
425impl std::error::Error for WrongPublisher {}
426
427pub fn check_entitlement<I, S>(
439 receipt: &Receipt,
440 input_licences: I,
441 key_document: &KeyDocument,
442) -> core::result::Result<Entitlement, WrongPublisher>
443where
444 I: IntoIterator<Item = S>,
445 S: AsRef<str>,
446{
447 let cell = receipt.cell();
448 let publisher = lexical::host_of(&cell.ns);
449 if key_document.host() != publisher {
450 return Err(WrongPublisher { expected: publisher.into(), got: key_document.host().into() });
451 }
452 let function = format!("{}/{}", cell.ns, cell.fn_name);
453 let rule = inherit_licence(
454 input_licences,
455 key_document.declarations.as_ref(),
456 &function,
457 receipt.issued(),
458 receipt.licence(),
459 );
460 Ok(match rule.licence {
461 None => Entitlement { entitled: false, reason: rule.reason },
462 Some(l) if l != receipt.licence() => Entitlement { entitled: false, reason: LicenceReason::LicenceMismatch },
463 Some(_) => Entitlement { entitled: true, reason: rule.reason },
464 })
465}
466
467#[cfg(test)]
468mod tests {
469 use super::*;
470
471 #[test]
472 fn open_ranks_order_by_tier_then_place() {
473 assert_eq!(open_rank("CC0-1.0"), Some((0, 0)));
474 assert_eq!(open_rank("OGL-UK-3.0"), Some((1, 3)));
475 assert_eq!(open_rank("ODbL-1.0"), Some((2, 1)));
476 assert_eq!(open_rank("cc0-1.0"), None);
477 assert!(Some((1, 0)) > Some((0, 1)));
478 }
479}