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 {
43 (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 pub fn digest(&self) -> B3Digest {
58 proto::Directory::from(self.clone()).digest()
59 }
60
61 pub fn nodes(&self) -> impl Iterator<Item = (&PathComponent, &Node)> + Send + Sync + '_ {
65 self.nodes.iter()
66 }
67
68 pub fn into_nodes(self) -> impl Iterator<Item = (PathComponent, Node)> + Send + Sync {
71 self.nodes.into_iter()
72 }
73
74 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
91fn check_insert_node(
96 current_size: u64,
97 nodes: &mut BTreeMap<PathComponent, Node>,
98 name: PathComponent,
99 node: Node,
100) -> Result<u64, DirectoryError> {
101 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 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 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}