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.
41pub struct NodeMask {
42 pub(crate) visible: HashSet<u32>,
43 mode: MaskMode,
44}
45
46impl NodeMask {
47 /// Resolve string keys to dense ids and build a mask.
48 ///
49 /// Keys that do not exist in the database are ignored.
50 /// The mask mode defaults to [`MaskMode::Omit`]; call [`NodeMask::with_mode`]
51 /// to opt into [`MaskMode::Stub`].
52 pub fn from_keys<'a, F: Fs>(db: &GraphDb<F>, keys: impl IntoIterator<Item = &'a str>) -> Self {
53 let visible = keys.into_iter().filter_map(|k| db.ids().get(k)).collect();
54 NodeMask {
55 visible,
56 mode: MaskMode::default(),
57 }
58 }
59
60 /// Build a mask from an already-resolved iterator of dense node ids.
61 ///
62 /// Used by `ReaderSnapshot` handlers that resolve keys against the frozen
63 /// state without a `GraphDb` reference.
64 pub fn from_ids(ids: impl IntoIterator<Item = u32>) -> Self {
65 NodeMask {
66 visible: ids.into_iter().collect(),
67 mode: MaskMode::default(),
68 }
69 }
70
71 /// Set the rendering mode, consuming `self` and returning a new mask.
72 ///
73 /// **SECURITY**: never call with [`MaskMode::Stub`] on role-token paths.
74 pub fn with_mode(self, mode: MaskMode) -> Self {
75 NodeMask { mode, ..self }
76 }
77
78 /// Return the current rendering mode.
79 pub fn mode(&self) -> MaskMode {
80 self.mode
81 }
82
83 pub fn len(&self) -> usize {
84 self.visible.len()
85 }
86
87 pub fn is_empty(&self) -> bool {
88 self.visible.is_empty()
89 }
90
91 /// Return a new mask that is the intersection of `self` and `other`.
92 ///
93 /// The result contains only nodes visible in both masks. Used to enforce
94 /// the never-widen rule when a role token also supplies a client mask:
95 /// `effective = role_mask.intersect(&client_mask)`.
96 ///
97 /// The result always carries [`MaskMode::Omit`] — the role-path invariant
98 /// means stubs must never slip through an intersection.
99 pub fn intersect(&self, other: &NodeMask) -> NodeMask {
100 NodeMask {
101 visible: self.visible.intersection(&other.visible).copied().collect(),
102 mode: MaskMode::Omit,
103 }
104 }
105
106 /// Return `true` if the dense node id is visible in this mask.
107 ///
108 /// Used by `ReaderSnapshot` handlers where the key has already been resolved
109 /// to a dense id (avoids a second lookup into a `GraphDb`).
110 pub fn contains_id(&self, id: u32) -> bool {
111 self.visible.contains(&id)
112 }
113
114 /// Return `true` if the node identified by `key` is visible in this mask.
115 ///
116 /// Returns `false` for keys that do not exist in the database (unknown keys
117 /// are never visible), as well as for keys that exist but are not in the
118 /// visible set. Used by node-endpoint handlers to produce the same
119 /// absent-key response for both missing and hidden nodes.
120 pub fn contains_node<F: core_storage::fs::Fs>(
121 &self,
122 db: &crate::db::GraphDb<F>,
123 key: &str,
124 ) -> bool {
125 db.ids()
126 .get(key)
127 .is_some_and(|id| self.visible.contains(&id))
128 }
129}