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}