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