Skip to main content

core_api/
mask.rs

1use core_storage::fs::Fs;
2use std::collections::HashSet;
3
4use crate::db::GraphDb;
5
6/// Controls how hidden nodes are rendered when a [`NodeMask`] is used in
7/// [`GraphDb::node_info_masked`], [`GraphDb::node_edges_masked`], and
8/// [`GraphDb::neighborhood_masked`].
9///
10/// The default is [`MaskMode::Omit`], which preserves byte-identical behaviour
11/// with all pre-existing masked paths.  [`MaskMode::Stub`] is an explicit
12/// opt-in that discloses node *existence* — suitable only for full-token
13/// client masks.  Role-token paths are hard-coded to `Omit`.
14///
15/// **Existence-disclosure warning**: `Stub` mode intentionally tells the caller
16/// whether a node exists, even if its contents are hidden.  Only use this on
17/// client-mask (full-token) paths where the caller already has that knowledge
18/// implicitly.
19#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
20pub enum MaskMode {
21    /// Hidden nodes are silently omitted from every result — behaviour is
22    /// byte-identical to the pre-existing masked-query paths.  This is the
23    /// default.
24    #[default]
25    Omit,
26    /// Hidden nodes' existence is acknowledged via a restricted stub:
27    /// `{"key": "<key>", "restricted": true}`.  No label, props, or other
28    /// fields are included in the stub.
29    Stub,
30}
31
32/// Query-scoped node visibility filter (ACL primitive).
33///
34/// When a `NodeMask` is passed to `query_masked`, only nodes whose dense id
35/// appears in `visible` will be returned by label scans, key lookups, and
36/// neighbor expansions. Edges where either endpoint is hidden are silently
37/// dropped from the result.
38///
39/// Unknown keys in `from_keys` are silently ignored (they resolve to no id).
40/// An empty mask hides every node.
41#[derive(Clone, Debug)]
42pub struct NodeMask {
43    pub(crate) visible: HashSet<u32>,
44    mode: MaskMode,
45}
46
47impl NodeMask {
48    /// Resolve string keys to dense ids and build a mask.
49    ///
50    /// Keys that do not exist in the database are ignored.
51    /// The mask mode defaults to [`MaskMode::Omit`]; call [`NodeMask::with_mode`]
52    /// to opt into [`MaskMode::Stub`].
53    pub fn from_keys<'a, F: Fs>(db: &GraphDb<F>, keys: impl IntoIterator<Item = &'a str>) -> Self {
54        let visible = keys.into_iter().filter_map(|k| db.ids().get(k)).collect();
55        NodeMask {
56            visible,
57            mode: MaskMode::default(),
58        }
59    }
60
61    /// Build a mask from an already-resolved iterator of dense node ids.
62    ///
63    /// Used by `ReaderSnapshot` handlers that resolve keys against the frozen
64    /// state without a `GraphDb` reference.
65    pub fn from_ids(ids: impl IntoIterator<Item = u32>) -> Self {
66        NodeMask {
67            visible: ids.into_iter().collect(),
68            mode: MaskMode::default(),
69        }
70    }
71
72    /// Set the rendering mode, consuming `self` and returning a new mask.
73    ///
74    /// **SECURITY**: never call with [`MaskMode::Stub`] on role-token paths.
75    pub fn with_mode(self, mode: MaskMode) -> Self {
76        NodeMask { mode, ..self }
77    }
78
79    /// Return the current rendering mode.
80    pub fn mode(&self) -> MaskMode {
81        self.mode
82    }
83
84    pub fn len(&self) -> usize {
85        self.visible.len()
86    }
87
88    pub fn is_empty(&self) -> bool {
89        self.visible.is_empty()
90    }
91
92    /// Return a new mask that is the intersection of `self` and `other`.
93    ///
94    /// The result contains only nodes visible in both masks.  Used to enforce
95    /// the never-widen rule when a role token also supplies a client mask:
96    /// `effective = role_mask.intersect(&client_mask)`.
97    ///
98    /// The result always carries [`MaskMode::Omit`] — the role-path invariant
99    /// means stubs must never slip through an intersection.
100    pub fn intersect(&self, other: &NodeMask) -> NodeMask {
101        NodeMask {
102            visible: self.visible.intersection(&other.visible).copied().collect(),
103            mode: MaskMode::Omit,
104        }
105    }
106
107    /// Return `true` if the dense node id is visible in this mask.
108    ///
109    /// Used by `ReaderSnapshot` handlers where the key has already been resolved
110    /// to a dense id (avoids a second lookup into a `GraphDb`).
111    pub fn contains_id(&self, id: u32) -> bool {
112        self.visible.contains(&id)
113    }
114
115    /// Return `true` if the node identified by `key` is visible in this mask.
116    ///
117    /// Returns `false` for keys that do not exist in the database (unknown keys
118    /// are never visible), as well as for keys that exist but are not in the
119    /// visible set.  Used by node-endpoint handlers to produce the same
120    /// absent-key response for both missing and hidden nodes.
121    pub fn contains_node<F: core_storage::fs::Fs>(
122        &self,
123        db: &crate::db::GraphDb<F>,
124        key: &str,
125    ) -> bool {
126        db.ids()
127            .get(key)
128            .is_some_and(|id| self.visible.contains(&id))
129    }
130}