Skip to main content

mls_rs/group/
exported_tree.rs

1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// Copyright by contributors to this project.
3// SPDX-License-Identifier: (Apache-2.0 OR MIT)
4
5use alloc::{borrow::Cow, vec::Vec};
6use mls_rs_codec::{MlsDecode, MlsEncode, MlsSize};
7
8use crate::{
9    client::MlsError,
10    tree_kem::{
11        leaf_node::LeafNode,
12        node::{LeafIndex, Node, NodeIndex, NodeVec, Parent},
13    },
14};
15
16use super::Roster;
17
18#[derive(Debug, MlsSize, MlsEncode, MlsDecode, PartialEq, Clone)]
19pub struct ExportedTree<'a>(pub(crate) Cow<'a, NodeVec>);
20
21impl<'a> ExportedTree<'a> {
22    pub(crate) fn new(node_data: NodeVec) -> Self {
23        Self(Cow::Owned(node_data))
24    }
25
26    pub(crate) fn new_borrowed(node_data: &'a NodeVec) -> Self {
27        Self(Cow::Borrowed(node_data))
28    }
29
30    pub fn to_bytes(&self) -> Result<Vec<u8>, MlsError> {
31        self.mls_encode_to_vec().map_err(Into::into)
32    }
33
34    pub fn byte_size(&self) -> usize {
35        self.mls_encoded_len()
36    }
37
38    pub fn into_owned(self) -> ExportedTree<'static> {
39        ExportedTree(Cow::Owned(self.0.into_owned()))
40    }
41
42    pub fn roster(&'a self) -> Roster<'a> {
43        Roster {
44            public_tree: &self.0,
45        }
46    }
47
48    /// Returns a reference to the underlying vector of nodes in the tree.
49    ///
50    /// Each element is `None` for a blank node, or `Some(Node)` for an
51    /// occupied leaf or parent node. Nodes are indexed by `NodeIndex` where
52    /// even indices are leaves and odd indices are parent nodes.
53    pub fn nodes(&self) -> &[Option<Node>] {
54        &self.0
55    }
56
57    /// Returns the filtered direct path for a given leaf index as a vector
58    /// of `Option<&Parent>` nodes.
59    ///
60    /// Per RFC 9420 Section 8.4, the filtered direct path removes all nodes
61    /// whose child on the copath has an empty resolution. Each entry is `None`
62    /// if the parent node at that position is blank, or `Some(&Parent)` if
63    /// it is populated.
64    pub fn filtered_direct_path(&self, index: LeafIndex) -> Result<Vec<Option<&Parent>>, MlsError> {
65        let direct_copath = self.0.direct_copath(index);
66        let filtered = self.0.filtered(index)?;
67
68        let path = direct_copath
69            .into_iter()
70            .zip(filtered)
71            .filter_map(|(cp, is_filtered)| {
72                (!is_filtered).then(|| {
73                    self.0
74                        .get(cp.path as usize)
75                        .and_then(Option::as_ref)
76                        .and_then(|n| match n {
77                            Node::Parent(p) => Some(p),
78                            Node::Leaf(_) => None,
79                        })
80                })
81            })
82            .collect();
83
84        Ok(path)
85    }
86
87    /// Returns the parent node at the given `NodeIndex`, or `None` if the
88    /// node is blank or is a leaf node. Returns an error if the index is
89    /// out of range.
90    pub fn get_parent(&self, index: NodeIndex) -> Result<Option<&Parent>, MlsError> {
91        let parent = self.0.borrow_node(index)?.as_ref().and_then(|n| match n {
92            Node::Parent(p) => Some(p),
93            Node::Leaf(_) => None,
94        });
95
96        Ok(parent)
97    }
98
99    /// Returns the leaf node at the given `LeafIndex`, or `None` if the
100    /// leaf slot is blank. Returns an error if the index is out of range.
101    pub fn get_leaf(&self, index: LeafIndex) -> Result<Option<&LeafNode>, MlsError> {
102        let leaf = self
103            .0
104            .borrow_node(index.into())?
105            .as_ref()
106            .and_then(|n| match n {
107                Node::Leaf(l) => Some(l),
108                Node::Parent(_) => None,
109            });
110
111        Ok(leaf)
112    }
113}
114
115impl ExportedTree<'static> {
116    pub fn from_bytes(bytes: &[u8]) -> Result<Self, MlsError> {
117        Self::mls_decode(&mut &*bytes).map_err(Into::into)
118    }
119}
120
121impl From<ExportedTree<'_>> for NodeVec {
122    fn from(value: ExportedTree) -> Self {
123        value.0.into_owned()
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use crate::tree_kem::node::{test_utils::get_test_node_vec, NodeTypeResolver};
131
132    #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
133    async fn test_exported_tree_accessors() {
134        // The test tree (7 nodes, 4 leaf slots):
135        //
136        //        3
137        //       / \
138        //      1   5
139        //     / \ / \
140        //    0  2 4  6
141        //    A  _  C  D
142        //
143        // Node 0: Leaf "A"
144        // Node 1: None (blank parent)
145        // Node 2: None (blank leaf)
146        // Node 3: None (blank root)
147        // Node 4: Leaf "C"
148        // Node 5: Parent { key: "CD", unmerged_leaves: [2] }
149        // Node 6: Leaf "D"
150        let nodes = get_test_node_vec().await;
151        let tree = ExportedTree::new(nodes.clone());
152
153        assert_eq!(tree.nodes().len(), nodes.len());
154
155        let leaf_a = tree.get_leaf(LeafIndex::unchecked(0)).unwrap().unwrap();
156        assert_eq!(leaf_a, nodes[0].as_leaf().unwrap());
157
158        // get_leaf for blank leaf
159        let leaf_b = tree.get_leaf(LeafIndex::unchecked(1)).unwrap();
160        assert!(leaf_b.is_none());
161
162        // get_parent for occupied parent (node index 5)
163        let parent = tree.get_parent(5).unwrap().unwrap();
164        assert_eq!(parent, nodes[5].as_parent().unwrap());
165
166        // get_parent for blank parent (node index 1)
167        let blank_parent = tree.get_parent(1).unwrap();
168        assert!(blank_parent.is_none());
169
170        // get_parent on a leaf node returns None
171        let not_parent = tree.get_parent(0).unwrap();
172        assert!(not_parent.is_none());
173
174        // filtered_direct_path for leaf 0:
175        // Direct path is [1, 3], copath is [2, 5].
176        // Node at index 1 is filtered out (copath node 2 has empty resolution).
177        // Node at index 3 is kept (copath node 5 has non-empty resolution).
178        // Node 3 is blank, so result is [None].
179        let fdp = tree.filtered_direct_path(LeafIndex::unchecked(0)).unwrap();
180        assert_eq!(fdp.len(), 1);
181        assert!(fdp[0].is_none()); // node 3 is blank
182
183        // filtered_direct_path for leaf 2 (node index 4, leaf "C"):
184        // Direct path is [5, 3], copath is [6, 1].
185        // Node 6 is leaf "D" (non-empty resolution) → node 5 kept.
186        // Node 1 is blank, but leaf 0 "A" is in resolution of subtree → check.
187        // Actually copath of node 3 is node 1, resolution of node 1 is [0] (leaf A).
188        // So node 3 is NOT filtered. Both path nodes kept.
189        let fdp2 = tree.filtered_direct_path(LeafIndex::unchecked(2)).unwrap();
190        assert_eq!(fdp2.len(), 2);
191        // Node 5 is the occupied parent
192        assert_eq!(fdp2[0].unwrap().public_key.as_ref(), b"CD");
193        // Node 3 is blank
194        assert!(fdp2[1].is_none());
195    }
196}