1use std::collections::btree_map::{self, BTreeMap};
2
3use crate::{B3Digest, Node, errors::DirectoryError, path::PathComponent, proto};
4
5#[derive(Default, Debug, Clone, PartialEq, Eq)]
12pub struct Directory {
13 nodes: BTreeMap<PathComponent, Node>,
14}
15
16impl Directory {
17 pub fn new() -> Self {
19 Directory {
20 nodes: BTreeMap::new(),
21 }
22 }
23
24 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 pub fn size(&self) -> u64 {
44 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 #[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 pub fn digest(&self) -> B3Digest {
72 proto::Directory::from(self.clone()).digest()
73 }
74
75 pub fn nodes(&self) -> impl ExactSizeIterator<Item = (&PathComponent, &Node)> + '_ {
79 self.nodes.iter()
80 }
81
82 pub fn into_nodes(self) -> impl ExactSizeIterator<Item = (PathComponent, Node)> {
85 self.nodes.into_iter()
86 }
87
88 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
105fn check_insert_node(
110 current_size: u64,
111 nodes: &mut BTreeMap<PathComponent, Node>,
112 name: PathComponent,
113 node: Node,
114) -> Result<u64, DirectoryError> {
115 let new_size = checked_sum([
118 current_size,
119 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 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 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}