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, .. } => 2 + size,
49 Node::File { .. } | Node::Symlink { .. } => 1,
50 })
51 .sum::<u64>()
52 }
53
54 pub fn digest(&self) -> B3Digest {
57 proto::Directory::from(self.clone()).digest()
58 }
59
60 pub fn nodes(&self) -> impl ExactSizeIterator<Item = (&PathComponent, &Node)> + '_ {
64 self.nodes.iter()
65 }
66
67 pub fn into_nodes(self) -> impl ExactSizeIterator<Item = (PathComponent, Node)> {
70 self.nodes.into_iter()
71 }
72
73 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
90fn check_insert_node(
95 current_size: u64,
96 nodes: &mut BTreeMap<PathComponent, Node>,
97 name: PathComponent,
98 node: Node,
99) -> Result<u64, DirectoryError> {
100 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 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 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 assert_eq!(d.size(), 1 + 1 + 2 + 4);
271 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}