Skip to main content

datumesh_core/
licence.rs

1//! Licences of computed receipts (`spec-0.4.3`): a publisher's declarations (9.2), licence
2//! inheritance (8.5) and the entitlement check (10.3).
3//!
4//! Inheritance is the rule the engine applies when it signs a computed receipt; the
5//! entitlement check is the same rule run by a verifier on a receipt it has verified. Both are
6//! pure functions: no network, no clock, no verification.
7
8use 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/// One dated entry of a publisher's declarations (9.2, `spec-0.4.3`): its function may be
21/// computed over inputs under `licence` while the entry is open, the result carrying that
22/// licence (8.5). Entries are dated as keys are, and never removed, only closed.
23#[derive(Clone, Debug, PartialEq, Eq, Hash)]
24#[non_exhaustive]
25pub struct Declaration {
26    /// The declared licence, by the grammar and limits of 8.1, compared byte for byte.
27    pub licence: String,
28    /// Start of the entry's window (inclusive), an instant.
29    pub valid_from: String,
30    /// End of the window (exclusive), later than `valid_from`; `None` is open-ended.
31    pub valid_to: Option<String>,
32}
33
34impl Declaration {
35    /// An entry, checked as 9.2 checks one in a key document: `licence` by 8.1, `valid_from` an
36    /// instant, `valid_to` an instant later than it. Every failure is `E_KEY_DOC`.
37    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    /// Whether the entry is open at `issued`: `valid_from <= issued`, and `issued < valid_to`
42    /// when `valid_to` is present (the window of a key, 9.5).
43    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/// A publisher's declarations (9.2, `spec-0.4.3`): per full function name (2.3), the dated
75/// entries naming the restricted licences the publisher permits as inputs of that function.
76/// Read from the key document of the host of the function's namespace ([`KeyDocument`]).
77/// Names are kept in ascending order; each name's entries in document order.
78#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
79pub struct Declarations {
80    names: BTreeMap<String, Vec<Declaration>>,
81}
82
83impl Declarations {
84    /// No declarations.
85    pub fn new() -> Declarations {
86        Declarations::default()
87    }
88
89    /// Declarations from the JSON value of a `declarations` member: an object of at most 64 full
90    /// function names, each to an array of 1 to 16 entries with exactly `licence`, `valid_from`
91    /// and optionally `valid_to`, all strings, checked as in 9.2 (`E_KEY_DOC`). Two rules are
92    /// the key document's and not checked here, because the value alone cannot tell them: that
93    /// every name is under the document's host, and that the object is not empty (`{}` gives no
94    /// declarations, as the `licence` vectors write none). [`KeyDocument::from_json`] checks
95    /// both.
96    pub fn from_value(v: &Value) -> Result<Declarations> {
97        parse(v, "$")
98    }
99
100    /// Adds an entry for the function `name`, a full function name (2.3), keeping the limits
101    /// of 4.5 (64 names, 16 entries for one name). Every failure is `E_KEY_DOC`.
102    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    /// The entries for the function `name` (`cell.ns + "/" + cell.fn`); empty when none.
116    pub fn get(&self, name: &str) -> &[Declaration] {
117        self.names.get(name).map_or(&[], Vec::as_slice)
118    }
119
120    /// The declared function names, in ascending order.
121    pub fn names(&self) -> impl Iterator<Item = &str> {
122        self.names.keys().map(String::as_str)
123    }
124
125    /// Each declared name with its entries, names in ascending order.
126    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    /// The number of declared names.
131    pub fn len(&self) -> usize {
132        self.names.len()
133    }
134
135    /// Whether nothing is declared.
136    pub fn is_empty(&self) -> bool {
137        self.names.is_empty()
138    }
139
140    /// The JSON form of the `declarations` member (members in ascending name order).
141    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    /// 9.2: not empty, and every name's namespace's host is `host` exactly (a subdomain is
151    /// another host).
152    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
203/// The `declarations` member of a key document fetched from `host` (9.2).
204pub(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/// The named outcome of licence inheritance (8.5) and of the entitlement check (10.3): a
211/// string, not an error code. Three reasons give a licence (entitled), five refuse (not
212/// entitled).
213#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
214#[non_exhaustive]
215pub enum LicenceReason {
216    /// No input receipt: the mesh's own licence (a stored receipt, a constant, only empty
217    /// slices).
218    NoInputs,
219    /// Open inputs only: the highest tier, and within it the identifier listed last.
220    Open,
221    /// One restricted licence, declared for the function and open at `issued`.
222    Declared,
223    /// A share-alike licence with a different share-alike licence or with any restricted one.
224    ShareAlikeConflict,
225    /// Two or more different restricted licences.
226    RestrictedConflict,
227    /// One restricted licence, with no declaration entry for the function naming it.
228    NotDeclared,
229    /// One restricted licence, declared for the function, but no such entry open at `issued`.
230    DeclarationNotOpen,
231    /// The entitlement check only: the rule gives a licence other than the receipt's.
232    LicenceMismatch,
233}
234
235impl LicenceReason {
236    /// Every reason, in the order of SPEC 10.3.
237    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    /// The reason as written, e.g. `"share_alike_conflict"`.
249    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    /// Looks a reason up by its name.
263    pub fn from_name(text: &str) -> Option<LicenceReason> {
264        LicenceReason::ALL.iter().copied().find(|r| r.as_str() == text)
265    }
266
267    /// Whether the reason is one of an entitled receipt (`no_inputs`, `open`, `declared`): the
268    /// rule gave a licence.
269    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/// The result of licence inheritance (8.5): the licence of the receipt to be issued, or a
281/// refusal (`licence` `None`: the mesh answers `unlicensed` and signs nothing), with the reason.
282#[derive(Clone, Debug, PartialEq, Eq, Hash)]
283#[non_exhaustive]
284pub struct Inheritance {
285    /// The result's licence; `None` when the mesh may not compute the cell.
286    pub licence: Option<String>,
287    /// Why: `no_inputs`, `open` or `declared` with a licence; `share_alike_conflict`,
288    /// `restricted_conflict`, `not_declared` or `declaration_not_open` without.
289    pub reason: LicenceReason,
290}
291
292impl Inheritance {
293    /// Whether the mesh may not compute the cell (it answers `unlicensed`, 5.5).
294    pub fn refused(&self) -> bool {
295        self.licence.is_none()
296    }
297}
298
299/// The open tiers of 8.5, least to most strict, each in its listed order.
300const 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
303/// The tier of a licence (0 public domain, 1 attribution, 2 share-alike) and its place in it;
304/// `None` for a restricted licence. Byte for byte: `cc0-1.0` is restricted.
305fn 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
309/// Licence inheritance (SPEC 8.5, `spec-0.4.3`): the licence of a computed receipt for the
310/// function `function` (its full name, `cell.ns + "/" + cell.fn`) to be issued at `issued`,
311/// from `licences`, the licences of its input receipts (every receipt hash in `inputs` and
312/// every member of each slice entry's manifest, one per receipt, repeats allowed),
313/// `declarations`, those of the function's publisher (the key document of the host of
314/// `cell.ns`, whatever host signs; `None` or empty when it has none), and `mesh_licence`, the
315/// mesh's own licence for a cell with no input receipt.
316///
317/// The five steps of 8.5, in order: no inputs (`no_inputs`); a share-alike licence with a
318/// different share-alike one or with any restricted one (`share_alike_conflict`, declared or
319/// not); two different restricted licences (`restricted_conflict`); one restricted licence,
320/// which needs an entry for `function` naming it exactly and open at `issued` (`declared`,
321/// else `not_declared` or `declaration_not_open`); open inputs only, the highest tier present
322/// and within it the identifier listed last (`open`). Licences are compared byte for byte and
323/// not checked against 8.1's grammar: any string not among the eight open identifiers is
324/// restricted. `issued` is compared as text, as instants order.
325///
326/// It lives in this crate because the engine applies it when it signs (and refuses with
327/// `unlicensed`), while [`check_entitlement`] applies it on the verifier's side; both must give
328/// the same answer for the same receipt.
329pub 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    // 1.
344    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    // 2.
362    if share_alike.len() > 1 || (!share_alike.is_empty() && !restricted.is_empty()) {
363        return refuse(LicenceReason::ShareAlikeConflict);
364    }
365    // 3.
366    if restricted.len() > 1 {
367        return refuse(LicenceReason::RestrictedConflict);
368    }
369    // 4.
370    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    // 5. Open inputs only; (tier, place) orders by tier first, then by the place listed.
382    match best {
383        Some((t, i)) => give(TIERS[t][i], LicenceReason::Open),
384        None => refuse(LicenceReason::NotDeclared), // unreachable: an input is open or restricted
385    }
386}
387
388/// The answer of the entitlement check (10.3): whether the receipt's licence is the one the
389/// inheritance rule gives, with the reason. It is not a verification result.
390#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
391#[non_exhaustive]
392pub struct Entitlement {
393    /// Entitled or not.
394    pub entitled: bool,
395    /// One of the eight reasons; [`LicenceReason::entitled`] agrees with `entitled`.
396    pub reason: LicenceReason,
397}
398
399/// The key document given to [`check_entitlement`] is not that of the receipt's publisher (the
400/// host of `cell.ns`): the caller's error (10.3), not a reason.
401#[derive(Clone, Debug, PartialEq, Eq, Hash)]
402pub struct WrongPublisher {
403    expected: String,
404    got: String,
405}
406
407impl WrongPublisher {
408    /// The publisher's host: the host of the receipt's `cell.ns`.
409    pub fn expected(&self) -> &str {
410        &self.expected
411    }
412
413    /// The host of the key document given.
414    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
427/// The entitlement check (SPEC 10.3, `spec-0.4.3`), a pure function of R, L and K: `receipt`
428/// (R, which the caller has verified through step 6 of 10.1), `input_licences` (L, the licences
429/// of R's input receipts, slice members included, one per receipt, as step 7.5 obtains them)
430/// and `key_document` (K, the checked key document of R's publisher, the host of
431/// `R.cell.ns`, even when `computed_by` names another host).
432///
433/// Applies [`inherit_licence`] to L, K's declarations, `R.cell.ns + "/" + R.cell.fn` and
434/// `R.issued`, with `R.licence` as the mesh's own licence: a refusal is not entitled with the
435/// refusal's reason; a licence other than `R.licence` (byte for byte) is not entitled,
436/// `licence_mismatch`; otherwise entitled with the rule's reason. A K of another host is
437/// [`WrongPublisher`].
438pub 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}