Skip to main content

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 (counted twice for historical reasons),
42    /// and their size fields.
43    pub fn size(&self) -> u64 {
44        // It's impossible to create a Directory where the size overflows, because we
45        // check before every add() that the size won't overflow.
46        self.nodes()
47            .map(|(_name, n)| match n {
48                Node::Directory { size, .. } => 2 + size,
49                Node::File { .. } | Node::Symlink { .. } => 1,
50            })
51            .sum::<u64>()
52    }
53
54    /// Calculates the digest of a Directory, which is the blake3 hash of a
55    /// Directory protobuf message, serialized in protobuf canonical form.
56    pub fn digest(&self) -> B3Digest {
57        proto::Directory::from(self.clone()).digest()
58    }
59
60    /// Allows iterating over all nodes (directories, files and symlinks)
61    /// For each, it returns a tuple of its name and node.
62    /// The elements are sorted by their names.
63    pub fn nodes(&self) -> impl ExactSizeIterator<Item = (&PathComponent, &Node)> + '_ {
64        self.nodes.iter()
65    }
66
67    /// Dissolves a Directory into its individual names and nodes.
68    /// The elements are sorted by their names.
69    pub fn into_nodes(self) -> impl ExactSizeIterator<Item = (PathComponent, Node)> {
70        self.nodes.into_iter()
71    }
72
73    /// Adds the specified [Node] to the [Directory] with a given name.
74    ///
75    /// Inserting a node that already exists with the same name in the directory
76    /// will yield an error, as well as exceeding the maximum size.
77    ///
78    /// In case you want to construct a [Directory] from multiple elements, use
79    /// [Directory::try_from_iter] instead.
80    pub fn add(&mut self, name: PathComponent, node: Node) -> Result<(), DirectoryError> {
81        check_insert_node(self.size(), &mut self.nodes, name, node)?;
82        Ok(())
83    }
84}
85
86fn checked_sum(iter: impl IntoIterator<Item = u64>) -> Option<u64> {
87    iter.into_iter().try_fold(0u64, |acc, i| acc.checked_add(i))
88}
89
90/// Helper function dealing with inserting nodes into the nodes [BTreeMap],
91/// after ensuring the new size doesn't overlow and the key doesn't exist already.
92///
93/// Returns the new total size, or an error.
94fn check_insert_node(
95    current_size: u64,
96    nodes: &mut BTreeMap<PathComponent, Node>,
97    name: PathComponent,
98    node: Node,
99) -> Result<u64, DirectoryError> {
100    // Check that the even after adding this new directory entry, the size calculation will not
101    // overflow
102    let new_size = checked_sum([
103        current_size,
104        2,
105        match node {
106            Node::Directory { size, .. } => size,
107            _ => 0,
108        },
109    ])
110    .ok_or(DirectoryError::SizeOverflow)?;
111
112    match nodes.entry(name) {
113        btree_map::Entry::Vacant(e) => {
114            e.insert(node);
115        }
116        btree_map::Entry::Occupied(occupied) => {
117            return Err(DirectoryError::DuplicateName(occupied.key().to_owned()));
118        }
119    }
120
121    Ok(new_size)
122}
123
124#[cfg(test)]
125mod test {
126    use super::{Directory, Node};
127    use crate::fixtures::DUMMY_DIGEST;
128    use crate::{DirectoryError, PathComponent};
129
130    #[test]
131    fn from_iter_single() {
132        Directory::try_from_iter([(
133            PathComponent::try_from("b").unwrap(),
134            Node::Directory {
135                digest: *DUMMY_DIGEST,
136                size: 1,
137            },
138        )])
139        .unwrap();
140    }
141
142    #[test]
143    fn from_iter_multiple() {
144        let d = Directory::try_from_iter([
145            (
146                "b".try_into().unwrap(),
147                Node::Directory {
148                    digest: *DUMMY_DIGEST,
149                    size: 1,
150                },
151            ),
152            (
153                "a".try_into().unwrap(),
154                Node::Directory {
155                    digest: *DUMMY_DIGEST,
156                    size: 1,
157                },
158            ),
159            (
160                "z".try_into().unwrap(),
161                Node::Directory {
162                    digest: *DUMMY_DIGEST,
163                    size: 1,
164                },
165            ),
166            (
167                "f".try_into().unwrap(),
168                Node::File {
169                    digest: *DUMMY_DIGEST,
170                    size: 1,
171                    executable: true,
172                },
173            ),
174            (
175                "c".try_into().unwrap(),
176                Node::File {
177                    digest: *DUMMY_DIGEST,
178                    size: 1,
179                    executable: true,
180                },
181            ),
182            (
183                "g".try_into().unwrap(),
184                Node::File {
185                    digest: *DUMMY_DIGEST,
186                    size: 1,
187                    executable: true,
188                },
189            ),
190            (
191                "t".try_into().unwrap(),
192                Node::Symlink {
193                    target: "a".try_into().unwrap(),
194                },
195            ),
196            (
197                "o".try_into().unwrap(),
198                Node::Symlink {
199                    target: "a".try_into().unwrap(),
200                },
201            ),
202            (
203                "e".try_into().unwrap(),
204                Node::Symlink {
205                    target: "a".try_into().unwrap(),
206                },
207            ),
208        ])
209        .unwrap();
210
211        // Convert to proto struct and back to ensure we are not generating any invalid structures
212        crate::Directory::try_from(crate::proto::Directory::from(d))
213            .expect("directory should be valid");
214    }
215
216    #[test]
217    fn add_nodes_to_directory() {
218        let mut d = Directory::new();
219
220        d.add(
221            "b".try_into().unwrap(),
222            Node::Directory {
223                digest: *DUMMY_DIGEST,
224                size: 1,
225            },
226        )
227        .unwrap();
228        d.add(
229            "a".try_into().unwrap(),
230            Node::Directory {
231                digest: *DUMMY_DIGEST,
232                size: 1,
233            },
234        )
235        .unwrap();
236
237        // Convert to proto struct and back to ensure we are not generating any invalid structures
238        crate::Directory::try_from(crate::proto::Directory::from(d))
239            .expect("directory should be valid");
240    }
241
242    #[test]
243    fn size() {
244        let d = Directory::try_from_iter([
245            (
246                "a".try_into().unwrap(),
247                Node::Directory {
248                    digest: *DUMMY_DIGEST,
249                    size: 4,
250                },
251            ),
252            (
253                "b".try_into().unwrap(),
254                Node::File {
255                    digest: *DUMMY_DIGEST,
256                    size: 42,
257                    executable: false,
258                },
259            ),
260            (
261                "c".try_into().unwrap(),
262                Node::Symlink {
263                    target: "a".try_into().unwrap(),
264                },
265            ),
266        ])
267        .unwrap();
268        // One file, one symlink, one directory node (counted twice for historical reasons),
269        // plus the size field of the single child directory.
270        assert_eq!(d.size(), 1 + 1 + 2 + 4);
271        // Must agree with the proto implementation.
272        assert_eq!(d.size(), crate::proto::Directory::from(d).size());
273    }
274
275    #[test]
276    fn validate_overflow() {
277        let mut d = Directory::new();
278
279        assert_eq!(
280            d.add(
281                "foo".try_into().unwrap(),
282                Node::Directory {
283                    digest: *DUMMY_DIGEST,
284                    size: u64::MAX
285                }
286            ),
287            Err(DirectoryError::SizeOverflow)
288        );
289    }
290
291    #[test]
292    fn add_duplicate_node_to_directory() {
293        let mut d = Directory::new();
294
295        d.add(
296            "a".try_into().unwrap(),
297            Node::Directory {
298                digest: *DUMMY_DIGEST,
299                size: 1,
300            },
301        )
302        .unwrap();
303        assert_eq!(
304            format!(
305                "{}",
306                d.add(
307                    "a".try_into().unwrap(),
308                    Node::File {
309                        digest: *DUMMY_DIGEST,
310                        size: 1,
311                        executable: true
312                    }
313                )
314                .expect_err("adding duplicate dir entry must fail")
315            ),
316            "\"a\" is a duplicate name"
317        );
318    }
319}