// dirs_multiset.rs // // Copyright 2019 Raphaël Gomès // // This software may be used and distributed according to the terms of the // GNU General Public License version 2 or any later version. //! A multiset of directory names. //! //! Used to counts the references to directories in a manifest or dirstate. use crate::{utils::files, DirsIterable, DirstateEntry, DirstateMapError}; use std::collections::hash_map::{Entry, Iter}; use std::collections::HashMap; #[derive(PartialEq, Debug)] pub struct DirsMultiset { inner: HashMap, u32>, } impl DirsMultiset { /// Initializes the multiset from a dirstate or a manifest. /// /// If `skip_state` is provided, skips dirstate entries with equal state. pub fn new(iterable: DirsIterable, skip_state: Option) -> Self { let mut multiset = DirsMultiset { inner: HashMap::new(), }; match iterable { DirsIterable::Dirstate(vec) => { for (ref filename, DirstateEntry { state, .. }) in vec { // This `if` is optimized out of the loop if let Some(skip) = skip_state { if skip != state { multiset.add_path(filename); } } else { multiset.add_path(filename); } } } DirsIterable::Manifest(vec) => { for ref filename in vec { multiset.add_path(filename); } } } multiset } /// Increases the count of deepest directory contained in the path. /// /// If the directory is not yet in the map, adds its parents. pub fn add_path(&mut self, path: &[u8]) { for subpath in files::find_dirs(path) { if let Some(val) = self.inner.get_mut(subpath) { *val += 1; break; } self.inner.insert(subpath.to_owned(), 1); } } /// Decreases the count of deepest directory contained in the path. /// /// If it is the only reference, decreases all parents until one is /// removed. /// If the directory is not in the map, something horrible has happened. pub fn delete_path( &mut self, path: &[u8], ) -> Result<(), DirstateMapError> { for subpath in files::find_dirs(path) { match self.inner.entry(subpath.to_owned()) { Entry::Occupied(mut entry) => { let val = entry.get().clone(); if val > 1 { entry.insert(val - 1); break; } entry.remove(); } Entry::Vacant(_) => { return Err(DirstateMapError::PathNotFound( path.to_owned(), )) } }; } Ok(()) } pub fn contains_key(&self, key: &[u8]) -> bool { self.inner.contains_key(key) } pub fn iter(&self) -> Iter, u32> { self.inner.iter() } pub fn len(&self) -> usize { self.inner.len() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_delete_path_path_not_found() { let mut map = DirsMultiset::new(DirsIterable::Manifest(vec![]), None); let path = b"doesnotexist/"; assert_eq!( Err(DirstateMapError::PathNotFound(path.to_vec())), map.delete_path(path) ); } #[test] fn test_delete_path_empty_path() { let mut map = DirsMultiset::new(DirsIterable::Manifest(vec![vec![]]), None); let path = b""; assert_eq!(Ok(()), map.delete_path(path)); assert_eq!( Err(DirstateMapError::PathNotFound(path.to_vec())), map.delete_path(path) ); } #[test] fn test_delete_path_successful() { let mut map = DirsMultiset { inner: [("", 5), ("a", 3), ("a/b", 2), ("a/c", 1)] .iter() .map(|(k, v)| (k.as_bytes().to_vec(), *v)) .collect(), }; assert_eq!(Ok(()), map.delete_path(b"a/b/")); assert_eq!(Ok(()), map.delete_path(b"a/b/")); assert_eq!( Err(DirstateMapError::PathNotFound(b"a/b/".to_vec())), map.delete_path(b"a/b/") ); assert_eq!(2, *map.inner.get(&b"a".to_vec()).unwrap()); assert_eq!(1, *map.inner.get(&b"a/c".to_vec()).unwrap()); eprintln!("{:?}", map); assert_eq!(Ok(()), map.delete_path(b"a/")); eprintln!("{:?}", map); assert_eq!(Ok(()), map.delete_path(b"a/c/")); assert_eq!( Err(DirstateMapError::PathNotFound(b"a/c/".to_vec())), map.delete_path(b"a/c/") ); } #[test] fn test_add_path_empty_path() { let mut map = DirsMultiset::new(DirsIterable::Manifest(vec![]), None); let path = b""; map.add_path(path); assert_eq!(1, map.len()); } #[test] fn test_add_path_successful() { let mut map = DirsMultiset::new(DirsIterable::Manifest(vec![]), None); map.add_path(b"a/"); assert_eq!(1, *map.inner.get(&b"a".to_vec()).unwrap()); assert_eq!(1, *map.inner.get(&Vec::new()).unwrap()); assert_eq!(2, map.len()); // Non directory should be ignored map.add_path(b"a"); assert_eq!(1, *map.inner.get(&b"a".to_vec()).unwrap()); assert_eq!(2, map.len()); // Non directory will still add its base map.add_path(b"a/b"); assert_eq!(2, *map.inner.get(&b"a".to_vec()).unwrap()); assert_eq!(2, map.len()); // Duplicate path works map.add_path(b"a/"); assert_eq!(3, *map.inner.get(&b"a".to_vec()).unwrap()); // Nested dir adds to its base map.add_path(b"a/b/"); assert_eq!(4, *map.inner.get(&b"a".to_vec()).unwrap()); assert_eq!(1, *map.inner.get(&b"a/b".to_vec()).unwrap()); // but not its base's base, because it already existed map.add_path(b"a/b/c/"); assert_eq!(4, *map.inner.get(&b"a".to_vec()).unwrap()); assert_eq!(2, *map.inner.get(&b"a/b".to_vec()).unwrap()); map.add_path(b"a/c/"); assert_eq!(1, *map.inner.get(&b"a/c".to_vec()).unwrap()); let expected = DirsMultiset { inner: [("", 2), ("a", 5), ("a/b", 2), ("a/b/c", 1), ("a/c", 1)] .iter() .map(|(k, v)| (k.as_bytes().to_vec(), *v)) .collect(), }; assert_eq!(map, expected); } #[test] fn test_dirsmultiset_new_empty() { use DirsIterable::{Dirstate, Manifest}; let new = DirsMultiset::new(Manifest(vec![]), None); let expected = DirsMultiset { inner: HashMap::new(), }; assert_eq!(expected, new); let new = DirsMultiset::new(Dirstate(vec![]), None); let expected = DirsMultiset { inner: HashMap::new(), }; assert_eq!(expected, new); } #[test] fn test_dirsmultiset_new_no_skip() { use DirsIterable::{Dirstate, Manifest}; let input_vec = ["a/", "b/", "a/c", "a/d/"] .iter() .map(|e| e.as_bytes().to_vec()) .collect(); let expected_inner = [("", 2), ("a", 3), ("b", 1), ("a/d", 1)] .iter() .map(|(k, v)| (k.as_bytes().to_vec(), *v)) .collect(); let new = DirsMultiset::new(Manifest(input_vec), None); let expected = DirsMultiset { inner: expected_inner, }; assert_eq!(expected, new); let input_map = ["a/", "b/", "a/c", "a/d/"] .iter() .map(|f| { ( f.as_bytes().to_vec(), DirstateEntry { state: 0, mode: 0, mtime: 0, size: 0, }, ) }) .collect(); let expected_inner = [("", 2), ("a", 3), ("b", 1), ("a/d", 1)] .iter() .map(|(k, v)| (k.as_bytes().to_vec(), *v)) .collect(); let new = DirsMultiset::new(Dirstate(input_map), None); let expected = DirsMultiset { inner: expected_inner, }; assert_eq!(expected, new); } #[test] fn test_dirsmultiset_new_skip() { use DirsIterable::{Dirstate, Manifest}; let input_vec = ["a/", "b/", "a/c", "a/d/"] .iter() .map(|e| e.as_bytes().to_vec()) .collect(); let expected_inner = [("", 2), ("a", 3), ("b", 1), ("a/d", 1)] .iter() .map(|(k, v)| (k.as_bytes().to_vec(), *v)) .collect(); let new = DirsMultiset::new(Manifest(input_vec), Some('n' as i8)); let expected = DirsMultiset { inner: expected_inner, }; // Skip does not affect a manifest assert_eq!(expected, new); let input_map = [("a/", 'n'), ("a/b/", 'n'), ("a/c", 'r'), ("a/d/", 'm')] .iter() .map(|(f, state)| { ( f.as_bytes().to_vec(), DirstateEntry { state: *state as i8, mode: 0, mtime: 0, size: 0, }, ) }) .collect(); // "a" incremented with "a/c" and "a/d/" let expected_inner = [("", 1), ("a", 2), ("a/d", 1)] .iter() .map(|(k, v)| (k.as_bytes().to_vec(), *v)) .collect(); let new = DirsMultiset::new(Dirstate(input_map), Some('n' as i8)); let expected = DirsMultiset { inner: expected_inner, }; assert_eq!(expected, new); } }