Skip to main content

sim_kernel/
fact_store.rs

1//! The [`FactStore`] contract: storing and querying claims under visibility.
2//!
3//! This is protocol the libraries implement; the kernel ships a `BTreeMap`-
4//! backed store and defines the capability-gated read rules, not the
5//! persistence strategy.
6
7use std::{
8    collections::{BTreeMap, BTreeSet},
9    sync::OnceLock,
10};
11
12use crate::{
13    capability::{CapabilityName, CapabilitySet, fact_private_capability},
14    claim::{Claim, ClaimKind, ClaimPattern, Visibility},
15    datum::Datum,
16    datum_store::{BTreeDatumStore, DatumStore},
17    env::Cx,
18    error::{Error, Result},
19    id::Symbol,
20    ref_id::{ContentId, Ref},
21};
22
23/// Contract for storing and querying [`Claim`]s under visibility rules.
24///
25/// This is protocol the libraries implement; the kernel ships a [`BTreeMap`]-
26/// backed [`BTreeFactStore`] and defines the capability-gated read rules, not
27/// the persistence strategy.
28pub trait FactStore {
29    /// Inserts `claim` after checking the caller's `capabilities`, returning its
30    /// content [`Ref`]; private claims require the fact-private capability.
31    fn insert_authorized(
32        &mut self,
33        capabilities: &CapabilitySet,
34        data: &mut dyn DatumStore,
35        claim: Claim,
36    ) -> Result<Ref>;
37
38    /// Returns the claims matching `pattern` that are visible under `cx`'s
39    /// capabilities.
40    fn query_authorized(&self, cx: &Cx, pattern: ClaimPattern) -> Result<Vec<Claim>>;
41}
42
43/// In-memory [`FactStore`] backed by [`BTreeMap`]s with a triple index; the
44/// kernel default.
45#[derive(Clone, Debug, Default)]
46pub struct BTreeFactStore {
47    claims: BTreeMap<ContentId, Claim>,
48    index: BTreeMap<ClaimIndexKey, BTreeSet<ContentId>>,
49}
50
51#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
52struct ClaimIndexKey {
53    subject: Option<Ref>,
54    predicate: Option<Symbol>,
55    object: Option<Ref>,
56}
57
58impl BTreeFactStore {
59    /// Creates an empty store.
60    pub fn new() -> Self {
61        Self::default()
62    }
63
64    /// Returns the number of stored claims.
65    pub fn len(&self) -> usize {
66        self.claims.len()
67    }
68
69    /// Returns whether the store holds no claims.
70    pub fn is_empty(&self) -> bool {
71        self.claims.is_empty()
72    }
73
74    /// Returns the stored claim with content id `id`, if present.
75    pub fn get(&self, id: &ContentId) -> Option<&Claim> {
76        self.claims.get(id)
77    }
78
79    /// Removes the claim with content id `id`, updating every query index.
80    pub fn remove(&mut self, id: &ContentId) -> Option<Claim> {
81        let claim = self.claims.remove(id)?;
82        for key in index_keys_for_claim(&claim) {
83            let should_remove_key = if let Some(ids) = self.index.get_mut(&key) {
84                ids.remove(id);
85                ids.is_empty()
86            } else {
87                false
88            };
89            if should_remove_key {
90                self.index.remove(&key);
91            }
92        }
93        Some(claim)
94    }
95
96    /// Seeds the fixed core boot claims (the kernel class `kind` facts) into
97    /// this store and their datums into `data`.
98    pub fn insert_boot_claims(&mut self, data: &mut BTreeDatumStore) {
99        for record in boot_claim_records() {
100            data.insert_known(record.id.clone(), record.datum.clone());
101            self.insert_indexed(record.claim.clone());
102        }
103    }
104
105    fn insert_indexed(&mut self, claim: Claim) {
106        let Some(id) = claim.id.clone() else {
107            return;
108        };
109
110        if !self.claims.contains_key(&id) {
111            for key in index_keys_for_claim(&claim) {
112                self.index.entry(key).or_default().insert(id.clone());
113            }
114            self.claims.insert(id, claim);
115        }
116    }
117}
118
119impl FactStore for BTreeFactStore {
120    fn insert_authorized(
121        &mut self,
122        capabilities: &CapabilitySet,
123        data: &mut dyn DatumStore,
124        mut claim: Claim,
125    ) -> Result<Ref> {
126        authorize_insert(capabilities, &claim)?;
127
128        let id = claim.content_id(data)?;
129        claim.id = Some(id.clone());
130
131        self.insert_indexed(claim);
132
133        Ok(Ref::Content(id))
134    }
135
136    fn query_authorized(&self, cx: &Cx, pattern: ClaimPattern) -> Result<Vec<Claim>> {
137        let key = ClaimIndexKey {
138            subject: pattern.subject,
139            predicate: pattern.predicate,
140            object: pattern.object,
141        };
142
143        let Some(ids) = self.index.get(&key) else {
144            return Ok(Vec::new());
145        };
146
147        Ok(ids
148            .iter()
149            .filter_map(|id| self.claims.get(id))
150            .filter(|claim| visible_to(cx, claim, pattern.include_revoked))
151            .cloned()
152            .collect())
153    }
154}
155
156fn authorize_insert(capabilities: &CapabilitySet, claim: &Claim) -> Result<()> {
157    if claim.visibility == Visibility::Private {
158        require_capability(capabilities, &fact_private_capability())?;
159    }
160    Ok(())
161}
162
163fn visible_to(cx: &Cx, claim: &Claim, include_revoked: bool) -> bool {
164    if claim.kind == ClaimKind::Revoked && !include_revoked {
165        return false;
166    }
167
168    match claim.visibility {
169        Visibility::Public => true,
170        Visibility::CapabilityGated => has_all_requirements(cx.capabilities(), claim),
171        Visibility::Private => {
172            cx.capabilities().contains(&fact_private_capability())
173                && has_all_requirements(cx.capabilities(), claim)
174        }
175    }
176}
177
178fn has_all_requirements(capabilities: &CapabilitySet, claim: &Claim) -> bool {
179    claim
180        .requires
181        .iter()
182        .all(|capability| capabilities.contains(capability))
183}
184
185fn require_capability(capabilities: &CapabilitySet, capability: &CapabilityName) -> Result<()> {
186    if capabilities.contains(capability) {
187        Ok(())
188    } else {
189        Err(Error::CapabilityDenied {
190            capability: capability.clone(),
191        })
192    }
193}
194
195fn index_keys_for_claim(claim: &Claim) -> Vec<ClaimIndexKey> {
196    let subjects = [None, Some(claim.subject.clone())];
197    let predicates = [None, Some(claim.predicate.clone())];
198    let objects = [None, Some(claim.object.clone())];
199    let mut keys = Vec::with_capacity(8);
200
201    for subject in subjects {
202        for predicate in predicates.clone() {
203            for object in objects.clone() {
204                keys.push(ClaimIndexKey {
205                    subject: subject.clone(),
206                    predicate: predicate.clone(),
207                    object,
208                });
209            }
210        }
211    }
212
213    keys
214}
215
216fn core_boot_claims() -> Vec<Claim> {
217    core_class_names()
218        .iter()
219        .map(|name| {
220            Claim::public(
221                Ref::Symbol(core_symbol(name)),
222                core_symbol("kind"),
223                Ref::Symbol(core_symbol("class")),
224            )
225        })
226        .collect()
227}
228
229#[derive(Clone)]
230struct BootClaimRecord {
231    id: ContentId,
232    datum: Datum,
233    claim: Claim,
234}
235
236fn boot_claim_records() -> &'static [BootClaimRecord] {
237    static RECORDS: OnceLock<Vec<BootClaimRecord>> = OnceLock::new();
238    RECORDS.get_or_init(|| {
239        core_boot_claims()
240            .into_iter()
241            .map(|mut claim| {
242                let datum = claim.canonical_datum();
243                let id = datum.content_id().expect("core boot claim datum is valid");
244                claim.id = Some(id.clone());
245                BootClaimRecord { id, datum, claim }
246            })
247            .collect()
248    })
249}
250
251fn core_class_names() -> &'static [&'static str] {
252    &[
253        "Class",
254        "Nil",
255        "Bool",
256        "Number",
257        "Symbol",
258        "String",
259        "Bytes",
260        "List",
261        "Table",
262        "Expr",
263        "Function",
264        "Shape",
265        "Thunk",
266        "EvalRequest",
267        "EvalReply",
268        "Macro",
269        "ShapeMatch",
270        "Codec",
271        "Help",
272        "Test",
273        "NumberDomain",
274        "LocalEvalFabric",
275        "Card",
276    ]
277}
278
279fn core_symbol(name: &str) -> Symbol {
280    Symbol::qualified("core", name)
281}