snix_castore/nodes/
directory.rs

1use std::collections::btree_map::{self, BTreeMap};
2
3use crate::{B3Digest, Node, errors::DirectoryError, path::PathComponent, proto};
4
5/// A Directory contains nodes, which can be Directory, File or Symlink nodes.
6/// It attaches names to these nodes, which is the basename in that directory.
7/// These names:
8///  - MUST not contain slashes or null bytes
9///  - MUST not be '.' or '..'
10///  - MUST be unique across all three lists
11#[derive(Default, Debug, Clone, PartialEq, Eq)]
12pub struct Directory {
13    nodes: BTreeMap<PathComponent, Node>,
14}
15
16impl Directory {
17    /// Constructs a new, empty Directory.
18    pub fn new() -> Self {
19        Directory {
20            nodes: BTreeMap::new(),
21        }
22    }
23
24    /// Construct a [Directory] from tuples of name and [Node].
25    ///
26    /// Inserting multiple elements with the same name will yield an error, as
27    /// well as exceeding the maximum size.
28    pub fn try_from_iter<T: IntoIterator<Item = (PathComponent, Node)>>(
29        iter: T,
30    ) -> Result<Directory, DirectoryError> {
31        let mut nodes = BTreeMap::new();
32
33        iter.into_iter().try_fold(0u64, |size, (name, node)| {
34            check_insert_node(size, &mut nodes, name, node)
35        })?;
36
37        Ok(Self { nodes })
38    }
39
40    /// The size of a directory is the number of all regular and symlink elements,
41    /// the number of directory elements, and their size fields.
42    pub fn size(&self) -> u64 {
43        // It's impossible to create a Directory where the size overflows, because we
44        // check before every add() that the size won't overflow.
45        (self.nodes.len() as u64)
46            + self
47                .nodes()
48                .map(|(_name, n)| match n {
49                    Node::Directory { size, .. } => 1 + size,
50                    Node::File { .. } | Node::Symlink { .. } => 1,
51                })
52                .sum::<u64>()
53    }
54
55    /// Calculates the digest of a Directory, which is the blake3 hash of a
56    /// Directory protobuf message, serialized in protobuf canonical form.
57    pub fn digest(&self) -> B3Digest {
58        proto::Directory::from(self.clone()).digest()
59    }
60
61    /// Allows iterating over all nodes (directories, files and symlinks)
62    /// For each, it returns a tuple of its name and node.
63    /// The elements are sorted by their names.
64    pub fn nodes(&self) -> impl Iterator<Item = (&PathComponent, &Node)> + Send + Sync + '_ {
65        self.nodes.iter()
66    }
67
68    /// Dissolves a Directory into its individual names and nodes.
69    /// The elements are sorted by their names.
70    pub fn into_nodes(self) -> impl Iterator<Item = (PathComponent, Node)> + Send + Sync {
71        self.nodes.into_iter()
72    }
73
74    /// Adds the specified [Node] to the [Directory] with a given name.
75    ///
76    /// Inserting a node that already exists with the same name in the directory
77    /// will yield an error, as well as exceeding the maximum size.
78    ///
79    /// In case you want to construct a [Directory] from multiple elements, use
80    /// [Directory::try_from_iter] instead.
81    pub fn add(&mut self, name: PathComponent, node: Node) -> Result<(), DirectoryError> {
82        check_insert_node(self.size(), &mut self.nodes, name, node)?;
83        Ok(())
84    }
85}
86
87fn checked_sum(iter: impl IntoIterator<Item = u64>) -> Option<u64> {
88    iter.into_iter().try_fold(0u64, |acc, i| acc.checked_add(i))
89}
90
91/// Helper function dealing with inserting nodes into the nodes [BTreeMap],
92/// after ensuring the new size doesn't overlow and the key doesn't exist already.
93///
94/// Returns the new total size, or an error.
95fn check_insert_node(
96    current_size: u64,
97    nodes: &mut BTreeMap<PathComponent, Node>,
98    name: PathComponent,
99    node: Node,
100) -> Result<u64, DirectoryError> {
101    // Check that the even after adding this new directory entry, the size calculation will not
102    // overflow
103    let new_size = checked_sum([
104        current_size,
105        1,
106        match node {
107            Node::Directory { size, .. } => size,
108            _ => 0,
109        },
110    ])
111    .ok_or(DirectoryError::SizeOverflow)?;
112
113    match nodes.entry(name) {
114        btree_map::Entry::Vacant(e) => {
115            e.insert(node);
116        }
117        btree_map::Entry::Occupied(occupied) => {
118            return Err(DirectoryError::DuplicateName(occupied.key().to_owned()));
119        }
120    }
121
122    Ok(new_size)
123}
124
125#[cfg(test)]
126mod test {
127    use super::{Directory, Node};
128    use crate::fixtures::DUMMY_DIGEST;
129    use crate::{DirectoryError, PathComponent};
130
131    #[test]
132    fn from_iter_single() {
133        Directory::try_from_iter([(
134            PathComponent::try_from("b").unwrap(),
135            Node::Directory {
136                digest: DUMMY_DIGEST.clone(),
137                size: 1,
138            },
139        )])
140        .unwrap();
141    }
142
143    #[test]
144    fn from_iter_multiple() {
145        let d = Directory::try_from_iter([
146            (
147                "b".try_into().unwrap(),
148                Node::Directory {
149                    digest: DUMMY_DIGEST.clone(),
150                    size: 1,
151                },
152            ),
153            (
154                "a".try_into().unwrap(),
155                Node::Directory {
156                    digest: DUMMY_DIGEST.clone(),
157                    size: 1,
158                },
159            ),
160            (
161                "z".try_into().unwrap(),
162                Node::Directory {
163                    digest: DUMMY_DIGEST.clone(),
164                    size: 1,
165                },
166            ),
167            (
168                "f".try_into().unwrap(),
169                Node::File {
170                    digest: DUMMY_DIGEST.clone(),
171                    size: 1,
172                    executable: true,
173                },
174            ),
175            (
176                "c".try_into().unwrap(),
177                Node::File {
178                    digest: DUMMY_DIGEST.clone(),
179                    size: 1,
180                    executable: true,
181                },
182            ),
183            (
184                "g".try_into().unwrap(),
185                Node::File {
186                    digest: DUMMY_DIGEST.clone(),
187                    size: 1,
188                    executable: true,
189                },
190            ),
191            (
192                "t".try_into().unwrap(),
193                Node::Symlink {
194                    target: "a".try_into().unwrap(),
195                },
196            ),
197            (
198                "o".try_into().unwrap(),
199                Node::Symlink {
200                    target: "a".try_into().unwrap(),
201                },
202            ),
203            (
204                "e".try_into().unwrap(),
205                Node::Symlink {
206                    target: "a".try_into().unwrap(),
207                },
208            ),
209        ])
210        .unwrap();
211
212        // Convert to proto struct and back to ensure we are not generating any invalid structures
213        crate::Directory::try_from(crate::proto::Directory::from(d))
214            .expect("directory should be valid");
215    }
216
217    #[test]
218    fn add_nodes_to_directory() {
219        let mut d = Directory::new();
220
221        d.add(
222            "b".try_into().unwrap(),
223            Node::Directory {
224                digest: DUMMY_DIGEST.clone(),
225                size: 1,
226            },
227        )
228        .unwrap();
229        d.add(
230            "a".try_into().unwrap(),
231            Node::Directory {
232                digest: DUMMY_DIGEST.clone(),
233                size: 1,
234            },
235        )
236        .unwrap();
237
238        // Convert to proto struct and back to ensure we are not generating any invalid structures
239        crate::Directory::try_from(crate::proto::Directory::from(d))
240            .expect("directory should be valid");
241    }
242
243    #[test]
244    fn validate_overflow() {
245        let mut d = Directory::new();
246
247        assert_eq!(
248            d.add(
249                "foo".try_into().unwrap(),
250                Node::Directory {
251                    digest: DUMMY_DIGEST.clone(),
252                    size: u64::MAX
253                }
254            ),
255            Err(DirectoryError::SizeOverflow)
256        );
257    }
258
259    #[test]
260    fn add_duplicate_node_to_directory() {
261        let mut d = Directory::new();
262
263        d.add(
264            "a".try_into().unwrap(),
265            Node::Directory {
266                digest: DUMMY_DIGEST.clone(),
267                size: 1,
268            },
269        )
270        .unwrap();
271        assert_eq!(
272            format!(
273                "{}",
274                d.add(
275                    "a".try_into().unwrap(),
276                    Node::File {
277                        digest: DUMMY_DIGEST.clone(),
278                        size: 1,
279                        executable: true
280                    }
281                )
282                .expect_err("adding duplicate dir entry must fail")
283            ),
284            "\"a\" is a duplicate name"
285        );
286    }
287}