From 472cf96ba0b21f3530b62ffc887017a2dabf014b Mon Sep 17 00:00:00 2001 From: diogo464 Date: Tue, 8 Feb 2022 09:49:16 +0000 Subject: split code into different files --- src/main.rs | 1561 +---------------------------------------------------------- 1 file changed, 3 insertions(+), 1558 deletions(-) (limited to 'src/main.rs') diff --git a/src/main.rs b/src/main.rs index 7b38357..f933f6f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,1563 +1,8 @@ #![feature(try_blocks)] -// TODO: rewrite all errors so they start with lower case - -mod depot { - use anyhow::Context; - use std::{ - collections::HashSet, - ffi::{OsStr, OsString}, - ops::Index, - path::{Path, PathBuf}, - }; - use thiserror::Error; - - use slotmap::{Key, SlotMap}; - - //pub type Result = std::result::Result; - pub use anyhow::Result; - pub use disk::{read, write}; - - slotmap::new_key_type! {pub struct LinkID;} - slotmap::new_key_type! {struct NodeID;} - - #[derive(Debug, Error)] - enum DepotError { - #[error("path must be relative")] - InvalidPath, - #[error("path must be relative and not empty")] - InvalidLinkPath, - } - - #[derive(Debug, Clone)] - struct Node { - comp: OsString, - parent: NodeID, - kind: NodeKind, - } - - #[derive(Debug, Clone)] - enum NodeKind { - Link(LinkID), - Directory(HashSet), - } - - #[derive(Debug, Clone, PartialEq, Eq)] - enum NodeSearchResult { - Found(NodeID), - /// the closest NodeID up the the search point. - NotFound(NodeID), - } - - #[derive(Debug, Clone, PartialEq, Eq)] - pub enum DirNode { - Link(LinkID), - Directory(PathBuf), - } - - #[derive(Debug, Clone, PartialEq, Eq)] - pub enum SearchResult { - Found(LinkID), - Ancestor(LinkID), - NotFound, - } - - #[derive(Debug, Clone)] - struct Link { - origin: PathBuf, - destination: PathBuf, - origin_id: NodeID, - } - - #[derive(Debug)] - pub struct LinkView<'a> { - link_id: LinkID, - depot: &'a Depot, - } - - impl<'a> LinkView<'a> { - pub fn origin(&self) -> &Path { - &self.depot.links[self.link_id].origin - } - - pub fn destination(&self) -> &Path { - &self.depot.links[self.link_id].destination - } - } - - #[derive(Debug, Clone)] - struct DepotTree { - root: NodeID, - nodes: SlotMap, - } - - impl Default for DepotTree { - fn default() -> Self { - let mut nodes = SlotMap::::default(); - let root = nodes.insert(Node { - comp: Default::default(), - parent: Default::default(), - kind: NodeKind::Directory(Default::default()), - }); - Self { root, nodes } - } - } - - impl Index for DepotTree { - type Output = Node; - - fn index(&self, index: NodeID) -> &Self::Output { - self.nodes.index(index) - } - } - - impl DepotTree { - /// create a node of kind [`NodeKind::Link`]. - pub fn link_create(&mut self, path: &Path, link_id: LinkID) -> Result { - debug_assert!(path_verify_link(path).is_ok()); - - let path_search_result = self.search(path); - - // handle the error cases - match path_search_result { - NodeSearchResult::Found(node_id) => { - let node = &self.nodes[node_id]; - match &node.kind { - NodeKind::Link(_) => Err(anyhow::anyhow!("link already exists")), - NodeKind::Directory(_) => { - Err(anyhow::anyhow!("path already has links under it")) - } - } - } - NodeSearchResult::NotFound(ancestor_node_id) => { - let ancestor_node = &self.nodes[ancestor_node_id]; - match &ancestor_node.kind { - NodeKind::Link(_) => Err(anyhow::anyhow!( - "an ancestor of this path is already linked" - )), - NodeKind::Directory(_) => Ok(()), - } - } - }?; - - // create the node - // unwrap: this is a verfied link path, it must have atleast one component - let filename = path.file_name().unwrap(); - let parent_path = path_parent_or_empty(path); - let node_id = self.nodes.insert(Node { - comp: filename.to_owned(), - parent: Default::default(), - kind: NodeKind::Link(link_id), - }); - let parent_id = self.directory_get_or_create(parent_path, node_id); - self.nodes[node_id].parent = parent_id; - Ok(node_id) - } - - pub fn link_update_id(&mut self, node_id: NodeID, link_id: LinkID) { - let node = &mut self.nodes[node_id]; - match &mut node.kind { - NodeKind::Link(lid) => *lid = link_id, - NodeKind::Directory(_) => unreachable!(), - } - } - - /// attempts to moves a node of kind [`NodeKind::Link`] to `destination`. - pub fn link_move(&mut self, node_id: NodeID, destination: &Path) -> Result<()> { - let parent_id = self.nodes[node_id].parent; - let parent = &mut self.nodes[parent_id]; - - // remove the node from its parent temporarily so that the search never returns this - // link and that way any link will find means an error. - // if an error does happen then we re-add this node to its parent to keep the data - // consistent. - match &mut parent.kind { - NodeKind::Link(_) => unreachable!(), - NodeKind::Directory(children) => children.remove(&node_id), - }; - - let search_result = self.search(destination); - // handle the error cases - match search_result { - NodeSearchResult::Found(found_id) => { - assert!(found_id != node_id); - self.directory_add_child(parent_id, node_id); - return Err(anyhow::anyhow!("link already exists at that path")); - } - NodeSearchResult::NotFound(ancestor_id) => { - let ancestor = &self.nodes[ancestor_id]; - match &ancestor.kind { - NodeKind::Link(_) => { - self.directory_add_child(parent_id, node_id); - return Err(anyhow::anyhow!("ancestor path is already linked")); - } - NodeKind::Directory(_) => {} - } - } - }; - - let destination_parent = path_parent_or_empty(destination); - let new_parent_id = self.directory_get_or_create(destination_parent, node_id); - if new_parent_id != parent_id { - self.nodes[node_id].parent = new_parent_id; - - // we have to re-add and call the remove function because it could lead to the removal - // of several directories if they become empty after this remove. - self.directory_add_child(parent_id, node_id); - self.directory_remove_child(parent_id, node_id); - } - - // unwrap: destination is a verified link path so it has atleast 1 component - let comp = destination.file_name().unwrap(); - let node = &mut self.nodes[node_id]; - if node.comp != comp { - node.comp = comp.to_owned(); - } - - Ok(()) - } - - pub fn link_search(&self, path: &Path) -> SearchResult { - match self.search(path) { - NodeSearchResult::Found(node_id) => match &self.nodes[node_id].kind { - NodeKind::Link(link_id) => SearchResult::Found(*link_id), - NodeKind::Directory(_) => SearchResult::NotFound, - }, - NodeSearchResult::NotFound(node_id) => match &self.nodes[node_id].kind { - NodeKind::Link(link_id) => SearchResult::Ancestor(*link_id), - NodeKind::Directory(_) => SearchResult::NotFound, - }, - } - } - - /// remove a node of kind [`NodeKind::Link`]. - pub fn link_remove(&mut self, node_id: NodeID) { - let node = &self.nodes[node_id]; - assert!(std::matches!(node.kind, NodeKind::Link(_))); - let parent_id = node.parent; - self.nodes.remove(node_id); - self.directory_remove_child(parent_id, node_id); - } - - pub fn links_under(&self, path: &Path) -> impl Iterator + '_ { - let links = match self.search(path) { - NodeSearchResult::Found(node_id) => { - let node = &self.nodes[node_id]; - match &node.kind { - NodeKind::Link(link_id) => vec![*link_id], - NodeKind::Directory(children) => { - let mut links = Vec::new(); - let mut node_ids = Vec::from_iter(children.iter().copied()); - while let Some(child_id) = node_ids.pop() { - let child = &self.nodes[child_id]; - match &child.kind { - NodeKind::Link(link_id) => links.push(*link_id), - NodeKind::Directory(extra_children) => { - node_ids.extend(extra_children.iter().copied()) - } - } - } - links - } - } - } - NodeSearchResult::NotFound(_) => vec![], - }; - links.into_iter() - } - - pub fn has_links_under(&self, path: &Path) -> bool { - // it does not matter what type of node is found. if a directory exists then there - // must be atleast one link under it. - match self.search(path) { - NodeSearchResult::Found(_) => true, - NodeSearchResult::NotFound(_) => false, - } - } - - pub fn read_dir(&self, path: &Path) -> Result + '_> { - match self.search(path) { - NodeSearchResult::Found(node_id) => match &self.nodes[node_id].kind { - NodeKind::Link(_) => Err(anyhow::anyhow!("read dir called on a link")), - NodeKind::Directory(children) => Ok(children.iter().map(|child_id| { - let child = &self.nodes[*child_id]; - match &child.kind { - NodeKind::Link(link_id) => DirNode::Link(*link_id), - NodeKind::Directory(_) => { - DirNode::Directory(self.build_path(*child_id)) - } - } - })), - }, - NodeSearchResult::NotFound(_) => Err(anyhow::anyhow!("directory not found")), - } - } - - pub fn build_path(&self, node_id: NodeID) -> PathBuf { - fn recursive_helper(nodes: &SlotMap, nid: NodeID, pbuf: &mut PathBuf) { - if nid.is_null() { - return; - } - let parent_id = nodes[nid].parent; - recursive_helper(nodes, parent_id, pbuf); - pbuf.push(&nodes[nid].comp); - } - - let mut node_path = PathBuf::default(); - recursive_helper(&self.nodes, node_id, &mut node_path); - node_path - } - - fn search(&self, path: &Path) -> NodeSearchResult { - debug_assert!(path_verify(path).is_ok()); - - let mut curr_node_id = self.root; - let mut comp_iter = path_iter_comps(path).peekable(); - while let Some(comp) = comp_iter.next() { - if let Some(child_id) = self.directory_search_children(curr_node_id, comp) { - let child = &self.nodes[child_id]; - match &child.kind { - NodeKind::Link(_) => { - if comp_iter.peek().is_some() { - return NodeSearchResult::NotFound(child_id); - } else { - return NodeSearchResult::Found(child_id); - } - } - NodeKind::Directory(_) => curr_node_id = child_id, - } - } else { - return NodeSearchResult::NotFound(curr_node_id); - } - } - NodeSearchResult::Found(curr_node_id) - } - - // creates directories all the way up to and including path. - // there cannot be any links up to `path`. - fn directory_get_or_create(&mut self, path: &Path, initial_child: NodeID) -> NodeID { - // TODO: this could be replaced if the search function also returned the depth of the - // node and we skip those components and just start creating directories up to the - // path. - let mut curr_node_id = self.root; - for comp in path_iter_comps(path) { - if let Some(child_id) = self.directory_search_children(curr_node_id, comp) { - debug_assert!(std::matches!( - self.nodes[child_id].kind, - NodeKind::Directory(_) - )); - curr_node_id = child_id; - } else { - let new_node_id = self.nodes.insert(Node { - comp: comp.to_owned(), - parent: curr_node_id, - kind: NodeKind::Directory(Default::default()), - }); - self.directory_add_child(curr_node_id, new_node_id); - curr_node_id = new_node_id; - } - } - self.directory_add_child(curr_node_id, initial_child); - curr_node_id - } - - fn directory_search_children(&self, node_id: NodeID, comp: &OsStr) -> Option { - let node = &self.nodes[node_id]; - match &node.kind { - NodeKind::Link(_) => unreachable!(), - NodeKind::Directory(children) => { - for &child_id in children { - let child = &self.nodes[child_id]; - if child.comp == comp { - return Some(child_id); - } - } - } - } - None - } - - fn directory_add_child(&mut self, node_id: NodeID, child_id: NodeID) { - let node = &mut self.nodes[node_id]; - match &mut node.kind { - NodeKind::Link(_) => unreachable!(), - NodeKind::Directory(children) => children.insert(child_id), - }; - } - - fn directory_remove_child(&mut self, node_id: NodeID, child_id: NodeID) { - let node = &mut self.nodes[node_id]; - match &mut node.kind { - NodeKind::Link(_) => unreachable!(), - NodeKind::Directory(children) => { - children.remove(&child_id); - if children.is_empty() && !node.parent.is_null() { - let parent_id = node.parent; - self.directory_remove_child(parent_id, node_id); - } - } - } - } - } - - #[derive(Debug, Default, Clone)] - pub struct Depot { - links: SlotMap, - origin: DepotTree, - } - - impl Depot { - pub fn link_create( - &mut self, - origin: impl AsRef, - destination: impl AsRef, - ) -> Result { - let origin = origin.as_ref(); - let destination = destination.as_ref(); - path_verify_link(origin)?; - path_verify_link(destination)?; - self.link_create_unchecked(origin, destination) - } - - pub fn link_remove(&mut self, link_id: LinkID) { - let node_id = self.links[link_id].origin_id; - self.links.remove(link_id); - self.origin.link_remove(node_id); - } - - /// moves the link specified by `link_id` to the path at `destination`. - /// if the link is already at the destination nothing is done. - /// if the destination is another link that that link is removed. - /// if the destination is under another link then an error is returned. - /// `destination` will be the link's new origin. - pub fn link_move(&mut self, link_id: LinkID, destination: impl AsRef) -> Result<()> { - let destination = destination.as_ref(); - path_verify_link(destination)?; - self.link_move_unchecked(link_id, destination) - } - - #[allow(unused)] - pub fn link_search(&self, path: impl AsRef) -> Result { - let path = path.as_ref(); - path_verify(path)?; - Ok(self.link_search_unchecked(path)) - } - - pub fn link_find(&self, path: impl AsRef) -> Result> { - let path = path.as_ref(); - path_verify(path)?; - Ok(self.link_find_unchecked(path)) - } - - pub fn links_under( - &self, - path: impl AsRef, - ) -> Result + '_> { - let path = path.as_ref(); - path_verify(path)?; - Ok(self.links_under_unchecked(path)) - } - - pub fn has_links_under(&self, path: impl AsRef) -> Result { - let path = path.as_ref(); - path_verify(path)?; - Ok(self.has_links_under_unchecked(path)) - } - - pub fn links_verify_install(&self, link_ids: impl Iterator) -> Result<()> { - let mut destination = DepotTree::default(); - for link_id in link_ids { - let link = &self.links[link_id]; - destination - .link_create(&link.destination, link_id) - .context("link destinations overlap")?; - } - Ok(()) - } - - pub fn link_view(&self, link_id: LinkID) -> LinkView { - LinkView { - link_id, - depot: self, - } - } - - pub fn read_dir( - &self, - path: impl AsRef, - ) -> Result + '_> { - let path = path.as_ref(); - path_verify(path)?; - self.read_dir_unchecked(path) - } - - fn link_create_unchecked(&mut self, origin: &Path, destination: &Path) -> Result { - let node_id = self.origin.link_create(origin, Default::default())?; - let link_id = self.links.insert(Link { - origin: origin.to_owned(), - destination: destination.to_owned(), - origin_id: node_id, - }); - self.origin.link_update_id(node_id, link_id); - Ok(link_id) - } - - fn link_move_unchecked(&mut self, link_id: LinkID, destination: &Path) -> Result<()> { - let link = &self.links[link_id]; - if link.origin == destination { - return Ok(()); - } - let node_id = link.origin_id; - self.origin.link_move(node_id, destination)?; - self.links[link_id].origin = destination.to_owned(); - Ok(()) - } - - fn link_search_unchecked(&self, path: &Path) -> SearchResult { - self.origin.link_search(path) - } - - fn link_find_unchecked(&self, path: &Path) -> Option { - match self.link_search_unchecked(path) { - SearchResult::Found(link_id) => Some(link_id), - _ => None, - } - } - - fn links_under_unchecked(&self, path: &Path) -> impl Iterator + '_ { - self.origin.links_under(path) - } - - fn has_links_under_unchecked(&self, path: &Path) -> bool { - self.origin.has_links_under(path) - } - - fn read_dir_unchecked(&self, path: &Path) -> Result + '_> { - self.origin.read_dir(path) - } - } - - /// a verified link path is a path that: - /// + is not empty - /// + is relative - /// + does not contain Prefix/RootDir/ParentDir - fn path_verify_link(path: &Path) -> Result<()> { - // make sure the path is not empty - if path.components().next().is_none() { - return Err(DepotError::InvalidLinkPath.into()); - } - path_verify(path).map_err(|_| DepotError::InvalidLinkPath.into()) - } - - /// a verified path is a path that: - /// + is not empty - /// + is relative - /// + does not contain Prefix/RootDir/ParentDir - fn path_verify(path: &Path) -> Result<()> { - // make sure the path is relative - // make sure the path does not contain '.' or '..' - for component in path.components() { - match component { - std::path::Component::Prefix(_) - | std::path::Component::RootDir - | std::path::Component::CurDir - | std::path::Component::ParentDir => return Err(DepotError::InvalidPath.into()), - std::path::Component::Normal(_) => {} - } - } - Ok(()) - } - - fn path_parent_or_empty(path: &Path) -> &Path { - path.parent().unwrap_or_else(|| Path::new("")) - } - - /// Iterate over the components of a path. - /// # Pre - /// The path can only have "Normal" components. - fn path_iter_comps(path: &Path) -> impl Iterator { - debug_assert!(path_verify(path).is_ok()); - path.components().map(|component| match component { - std::path::Component::Normal(comp) => comp, - _ => unreachable!(), - }) - } - - mod disk { - use std::path::{Path, PathBuf}; - - use anyhow::Context; - use serde::{Deserialize, Serialize}; - - use super::Depot; - - #[derive(Debug, Serialize, Deserialize)] - struct DiskLink { - origin: PathBuf, - destination: PathBuf, - } - - #[derive(Debug, Serialize, Deserialize)] - struct DiskLinks { - links: Vec, - } - - pub fn read(path: &Path) -> anyhow::Result { - let contents = std::fs::read_to_string(path).context("Failed to read depot file")?; - let disk_links = toml::from_str::(&contents) - .context("Failed to parse depot file")? - .links; - let mut depot = Depot::default(); - for disk_link in disk_links { - depot - .link_create(disk_link.origin, disk_link.destination) - .context("Failed to build depot from file. File is in an invalid state")?; - } - Ok(depot) - } - - pub fn write(path: &Path, depot: &Depot) -> anyhow::Result<()> { - let mut links = Vec::with_capacity(depot.links.len()); - for (_, link) in depot.links.iter() { - links.push(DiskLink { - origin: link.origin.clone(), - destination: link.destination.clone(), - }); - } - let contents = toml::to_string_pretty(&DiskLinks { links }) - .context("Failed to serialize depot")?; - std::fs::write(path, contents).context("Failed to write depot to file")?; - Ok(()) - } - } - - #[cfg(test)] - mod tests { - use super::*; - - #[test] - fn test_depot_link_create() { - let mut depot = Depot::default(); - let f1 = depot.link_create("f1", "f1").unwrap(); - let f2 = depot.link_create("f2", "f2").unwrap(); - let f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); - let f4 = depot.link_create("d1/d2/f4", "d1/d2/d4").unwrap(); - - assert_eq!(depot.link_find("f1").unwrap(), Some(f1)); - assert_eq!(depot.link_find("f2").unwrap(), Some(f2)); - assert_eq!(depot.link_find("d1/f3").unwrap(), Some(f3)); - assert_eq!(depot.link_find("d1/d2/f4").unwrap(), Some(f4)); - - depot.link_create("f2", "").unwrap_err(); - depot.link_create("", "d4").unwrap_err(); - depot.link_create("f1/f3", "f3").unwrap_err(); - } - - #[test] - fn test_depot_link_remove() { - let mut depot = Depot::default(); - let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); - let f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); - let _f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); - let f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); - let d3 = depot.link_create("d3", "d3").unwrap(); - - depot.link_remove(f2); - assert_eq!(depot.link_find("d1/f1").unwrap(), Some(f1)); - assert_eq!(depot.link_find("d1/f2").unwrap(), None); - depot.link_remove(f4); - assert_eq!(depot.link_find("d1/d2/f4").unwrap(), None); - depot.link_remove(d3); - assert_eq!(depot.link_find("d3").unwrap(), None); - } - - #[test] - fn test_depot_link_move() { - let mut depot = Depot::default(); - let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); - let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); - - depot.link_move(f1, "").unwrap_err(); - depot.link_move(f1, "d1/f2/f1").unwrap_err(); - depot.link_move(f1, "d1/f2").unwrap_err(); - - depot.link_move(f1, "f1").unwrap(); - assert_eq!(depot.link_view(f1).origin(), Path::new("f1")); - depot.link_move(f1, "f2").unwrap(); - assert_eq!(depot.link_view(f1).origin(), Path::new("f2")); - assert_eq!(depot.link_find("f2").unwrap(), Some(f1)); - } - - #[test] - fn test_depot_link_search() { - let mut depot = Depot::default(); - let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); - let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); - let _f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); - let _f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); - let _d3 = depot.link_create("d3", "d3").unwrap(); - - assert_eq!(depot.link_search("d1/f1").unwrap(), SearchResult::Found(f1)); - assert_eq!( - depot.link_search("d1/f1/f5").unwrap(), - SearchResult::Ancestor(f1) - ); - assert_eq!(depot.link_search("d1").unwrap(), SearchResult::NotFound); - assert_eq!( - depot.link_search("d1/d2/f5").unwrap(), - SearchResult::NotFound - ); - } - - #[test] - fn test_depot_link_find() { - let mut depot = Depot::default(); - let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); - let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); - let f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); - let f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); - let d3 = depot.link_create("d3", "d3").unwrap(); - - assert_eq!(depot.link_find("d1/f1").unwrap(), Some(f1)); - assert_eq!(depot.link_find("d1/f3").unwrap(), Some(f3)); - assert_eq!(depot.link_find("d1/d2/f4").unwrap(), Some(f4)); - assert_eq!(depot.link_find("d3").unwrap(), Some(d3)); - - assert_eq!(depot.link_find("d5").unwrap(), None); - assert_eq!(depot.link_find("d3/d5").unwrap(), None); - assert_eq!(depot.link_find("d1/d2/f5").unwrap(), None); - } - - #[test] - fn test_depot_links_under() { - let mut depot = Depot::default(); - let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); - let f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); - let f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); - let f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); - let d3 = depot.link_create("d3", "d3").unwrap(); - - let under_f1 = depot.links_under("d1/f1").unwrap().collect::>(); - assert_eq!(under_f1, vec![f1]); - - let under_d1 = depot.links_under("d1").unwrap().collect::>(); - let expected_under_d1 = vec![f1, f2, f3, f4]; - assert!( - under_d1.len() == expected_under_d1.len() - && expected_under_d1.iter().all(|x| under_d1.contains(x)) - ); - - let under_d2 = depot.links_under("d2").unwrap().collect::>(); - assert_eq!(under_d2, vec![]); - - let under_d3 = depot.links_under("d3").unwrap().collect::>(); - assert_eq!(under_d3, vec![d3]); - - let under_root = depot.links_under("").unwrap().collect::>(); - let expected_under_root = vec![f1, f2, f3, f4, d3]; - assert!( - under_root.len() == expected_under_root.len() - && expected_under_root.iter().all(|x| under_root.contains(x)) - ); - } - - #[test] - fn test_depot_has_links_under() { - let mut depot = Depot::default(); - let _f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); - let _f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); - let _f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); - let _f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); - let _d3 = depot.link_create("d3", "d3").unwrap(); - - assert!(depot.has_links_under("").unwrap()); - assert!(depot.has_links_under("d1").unwrap()); - assert!(depot.has_links_under("d3").unwrap()); - assert!(depot.has_links_under("d1/f1").unwrap()); - assert!(depot.has_links_under("d1/d2").unwrap()); - assert!(depot.has_links_under("d1/d2/f4").unwrap()); - - assert!(!depot.has_links_under("d2").unwrap()); - assert!(!depot.has_links_under("d4").unwrap()); - assert!(!depot.has_links_under("d1/d2/f4/f5").unwrap()); - } - - #[test] - fn test_depot_links_verify_install() { - let mut depot = Depot::default(); - let f1 = depot.link_create("nvim", ".config/nvim").unwrap(); - let f2 = depot.link_create("alacritty", ".config/alacritty").unwrap(); - let f3 = depot.link_create("bash/.bashrc", ".bashrc").unwrap(); - let f4 = depot.link_create("bash_laptop/.bashrc", ".bashrc").unwrap(); - - depot - .links_verify_install(vec![f1, f2, f3].into_iter()) - .unwrap(); - depot - .links_verify_install(vec![f1, f2, f3, f4].into_iter()) - .unwrap_err(); - } - - #[test] - fn test_depot_read_dir() { - let mut depot = Depot::default(); - let f1 = depot.link_create("d1/f1", "d1/f1").unwrap(); - let f2 = depot.link_create("d1/f2", "d1/f2").unwrap(); - let f3 = depot.link_create("d1/f3", "d1/f3").unwrap(); - let _f4 = depot.link_create("d1/d2/f4", "d2/f4").unwrap(); - let _d3 = depot.link_create("d3", "d3").unwrap(); - - let read_dir = depot.read_dir("d1").unwrap().collect::>(); - let expected_read_dir = vec![ - DirNode::Link(f1), - DirNode::Link(f2), - DirNode::Link(f3), - DirNode::Directory(PathBuf::from("d1/d2")), - ]; - assert!( - read_dir.len() == expected_read_dir.len() - && expected_read_dir.iter().all(|x| read_dir.contains(x)) - ); - } - - #[test] - fn test_path_verify() { - path_verify(Path::new("")).unwrap(); - path_verify(Path::new("f1")).unwrap(); - path_verify(Path::new("d1/f1")).unwrap(); - path_verify(Path::new("d1/f1.txt")).unwrap(); - path_verify(Path::new("d1/./f1.txt")).unwrap(); - - path_verify(Path::new("/")).unwrap_err(); - path_verify(Path::new("./f1")).unwrap_err(); - path_verify(Path::new("/d1/f1")).unwrap_err(); - path_verify(Path::new("d1/../f1.txt")).unwrap_err(); - path_verify(Path::new("/d1/../f1.txt")).unwrap_err(); - } - - #[test] - fn test_path_verify_link() { - path_verify_link(Path::new("f1")).unwrap(); - path_verify_link(Path::new("d1/f1")).unwrap(); - path_verify_link(Path::new("d1/f1.txt")).unwrap(); - path_verify_link(Path::new("d1/./f1.txt")).unwrap(); - - path_verify_link(Path::new("")).unwrap_err(); - path_verify_link(Path::new("/")).unwrap_err(); - path_verify_link(Path::new("./f1")).unwrap_err(); - path_verify_link(Path::new("/d1/f1")).unwrap_err(); - path_verify_link(Path::new("d1/../f1.txt")).unwrap_err(); - path_verify_link(Path::new("/d1/../f1.txt")).unwrap_err(); - } - - #[test] - fn test_path_iter_comps() { - let path = Path::new("comp1/comp2/./comp3/file.txt"); - let mut iter = path_iter_comps(path); - assert_eq!(iter.next(), Some(OsStr::new("comp1"))); - assert_eq!(iter.next(), Some(OsStr::new("comp2"))); - assert_eq!(iter.next(), Some(OsStr::new("comp3"))); - assert_eq!(iter.next(), Some(OsStr::new("file.txt"))); - assert_eq!(iter.next(), None); - } - } -} - -pub mod dotup { - use std::{ - cmp::Ordering, - collections::HashSet, - path::{Path, PathBuf}, - }; - - use ansi_term::Color; - use anyhow::Context; - - use crate::{ - depot::{self, Depot, DirNode, LinkID}, - utils, - }; - - #[derive(Debug)] - struct CanonicalPair { - origin: PathBuf, - destination: PathBuf, - } - - #[derive(Debug, Clone)] - enum StatusItem { - Link { - origin: PathBuf, - destination: PathBuf, - is_directory: bool, - }, - Directory { - origin: PathBuf, - items: Vec, - }, - Unlinked { - origin: PathBuf, - is_directory: bool, - }, - } - - impl StatusItem { - fn display_ord_cmp(&self, other: &Self) -> Ordering { - match (self, other) { - ( - StatusItem::Link { - origin: l_origin, .. - }, - StatusItem::Link { - origin: r_origin, .. - }, - ) => l_origin.cmp(r_origin), - (StatusItem::Link { .. }, StatusItem::Directory { .. }) => Ordering::Less, - ( - StatusItem::Link { - is_directory: l_is_dir, - .. - }, - StatusItem::Unlinked { - is_directory: u_is_dir, - .. - }, - ) => { - if *u_is_dir && !*l_is_dir { - Ordering::Less - } else { - Ordering::Greater - } - } - (StatusItem::Directory { .. }, StatusItem::Link { .. }) => Ordering::Greater, - ( - StatusItem::Directory { - origin: l_origin, .. - }, - StatusItem::Directory { - origin: r_origin, .. - }, - ) => l_origin.cmp(r_origin), - (StatusItem::Directory { .. }, StatusItem::Unlinked { .. }) => Ordering::Greater, - ( - StatusItem::Unlinked { - is_directory: u_is_dir, - .. - }, - StatusItem::Link { - is_directory: l_is_dir, - .. - }, - ) => { - if *u_is_dir && !*l_is_dir { - Ordering::Greater - } else { - Ordering::Less - } - } - (StatusItem::Unlinked { .. }, StatusItem::Directory { .. }) => Ordering::Less, - ( - StatusItem::Unlinked { - origin: l_origin, .. - }, - StatusItem::Unlinked { - origin: r_origin, .. - }, - ) => l_origin.cmp(r_origin), - } - } - } - - #[derive(Debug)] - pub struct Dotup { - depot: Depot, - depot_dir: PathBuf, - depot_path: PathBuf, - install_base: PathBuf, - } - - impl Dotup { - fn new(depot: Depot, depot_path: PathBuf, install_base: PathBuf) -> anyhow::Result { - assert!(depot_path.is_absolute()); - assert!(depot_path.is_file()); - assert!(install_base.is_absolute()); - assert!(install_base.is_dir()); - let depot_dir = { - let mut d = depot_path.clone(); - d.pop(); - d - }; - Ok(Self { - depot, - depot_dir, - depot_path, - install_base, - }) - } - - pub fn link(&mut self, origin: impl AsRef, destination: impl AsRef) { - let link_result: anyhow::Result<()> = try { - let origin = self.prepare_relative_path(origin.as_ref())?; - let destination = destination.as_ref(); - self.depot.link_create(origin, destination)?; - }; - match link_result { - Ok(_) => {} - Err(e) => println!("Failed to create link : {e}"), - } - } - - pub fn unlink(&mut self, paths: impl Iterator>, uninstall: bool) { - for origin in paths { - let unlink_result: anyhow::Result<()> = try { - let origin = self.prepare_relative_path(origin.as_ref())?; - let links_under: Vec<_> = self.depot.links_under(&origin)?.collect(); - for link_id in links_under { - if uninstall && self.symlink_is_installed_by_link_id(link_id)? { - self.symlink_uninstall_by_link_id(link_id)?; - } - self.depot.link_remove(link_id); - } - }; - match unlink_result { - Ok(_) => {} - Err(e) => println!("Failed to unlink {} : {e}", origin.as_ref().display()), - } - } - } - - pub fn install(&self, paths: impl Iterator>) { - let install_result: anyhow::Result<()> = try { - let link_ids = self.link_ids_from_paths_iter(paths)?; - self.depot.links_verify_install(link_ids.iter().copied())?; - - for link_id in link_ids { - self.symlink_install_by_link_id(link_id)?; - } - }; - if let Err(e) = install_result { - println!("error while installing : {e}"); - } - } - - pub fn uninstall(&self, paths: impl Iterator>) { - let uninstall_result: anyhow::Result<()> = try { - let link_ids = self.link_ids_from_paths_iter(paths)?; - for link_id in link_ids { - if self.symlink_is_installed_by_link_id(link_id)? { - self.symlink_uninstall_by_link_id(link_id)?; - } - } - }; - if let Err(e) = uninstall_result { - println!("error while uninstalling {e}",); - } - } - - pub fn mv( - &mut self, - origins: impl Iterator>, - destination: impl AsRef, - ) { - let origins = { - let mut v = Vec::new(); - for origin in origins { - match self.prepare_relative_path(origin.as_ref()) { - Ok(origin) => v.push(origin), - Err(e) => { - println!("invalid link {} : {e}", origin.as_ref().display()); - return; - } - } - } - v - }; - let destination = destination.as_ref(); - - // if we are moving multiple links then the destination must be a directory - if origins.len() > 1 && destination.is_dir() { - println!("destination must be a directory"); - return; - } - - for origin in origins { - if let Err(e) = self.mv_one(&origin, destination) { - println!("error moving link {} : {e}", origin.display()); - } - } - } - - fn mv_one(&mut self, origin: &Path, destination: &Path) -> anyhow::Result<()> { - let link_id = match self.depot.link_find(origin)? { - Some(link_id) => link_id, - None => { - return Err(anyhow::anyhow!(format!( - "{} is not a link", - origin.display() - ))) - } - }; - let is_installed = self.symlink_is_installed_by_link_id(link_id)?; - let original_origin = self.depot.link_view(link_id).origin().to_owned(); - self.depot.link_move(link_id, destination)?; - // move the actual file on disk - if let Err(e) = std::fs::rename(origin, destination).context("Failed to move file") { - // unwrap: moving the link back to its origin place has to work - self.depot.link_move(link_id, original_origin).unwrap(); - return Err(e); - } - // reinstall because we just moved the origin - if is_installed { - self.symlink_install_by_link_id(link_id) - .context("failed to reinstall link while moving")?; - } - Ok(()) - } - - pub fn status(&self) { - let status_result: anyhow::Result<()> = try { - let canonical_dir = utils::current_working_directory(); - let item = self.status_path_to_item(&canonical_dir)?; - self.status_print_item(item, 0)?; - }; - if let Err(e) = status_result { - println!("error while displaying status : {e}"); - } - } - fn status_path_to_item(&self, canonical_path: &Path) -> anyhow::Result { - debug_assert!(canonical_path.is_absolute()); - debug_assert!(canonical_path.exists()); - let relative_path = self.prepare_relative_path(canonical_path)?; - - let item = if canonical_path.is_dir() { - if let Some(link_id) = self.depot.link_find(&relative_path)? { - let destination = self.depot.link_view(link_id).destination().to_owned(); - StatusItem::Link { - origin: relative_path, - destination, - is_directory: true, - } - } else if self.depot.has_links_under(&relative_path)? { - let mut items = Vec::new(); - let mut collected_rel_paths = HashSet::::new(); - let directory_paths = utils::collect_paths_in_dir(&canonical_path)?; - for canonical_item_path in directory_paths { - let item = self.status_path_to_item(&canonical_item_path)?; - match &item { - StatusItem::Link { origin, .. } - | StatusItem::Directory { origin, .. } => { - collected_rel_paths.insert(origin.to_owned()); - } - _ => {} - } - items.push(item); - } - - for dir_node in self.depot.read_dir(&relative_path)? { - match dir_node { - DirNode::Link(link_id) => { - let link_view = self.depot.link_view(link_id); - let link_rel_path = link_view.origin(); - let link_rel_dest = link_view.destination(); - if !collected_rel_paths.contains(link_rel_path) { - items.push(StatusItem::Link { - origin: link_rel_path.to_owned(), - destination: link_rel_dest.to_owned(), - is_directory: false, - }); - } - } - DirNode::Directory(_) => {} - } - } - - StatusItem::Directory { - origin: relative_path, - items, - } - } else { - StatusItem::Unlinked { - origin: relative_path, - is_directory: true, - } - } - } else if let Some(link_id) = self.depot.link_find(&relative_path)? { - let destination = self.depot.link_view(link_id).destination().to_owned(); - StatusItem::Link { - origin: relative_path, - destination, - is_directory: false, - } - } else { - StatusItem::Unlinked { - origin: relative_path, - is_directory: false, - } - }; - Ok(item) - } - fn status_print_item(&self, item: StatusItem, depth: u32) -> anyhow::Result<()> { - fn print_depth(d: u32) { - for _ in 0..d.saturating_sub(1) { - print!(" "); - } - } - fn origin_color(exists: bool, is_installed: bool) -> Color { - if !exists { - Color::Red - } else if is_installed { - Color::Green - } else { - Color::RGB(255, 127, 0) - } - } - - let destination_color = Color::Blue; - - print_depth(depth); - match item { - StatusItem::Link { - origin, - destination, - is_directory, - } => { - let canonical_origin = self.depot_dir.join(&origin); - let canonical_destination = self.install_base.join(&destination); - let file_name = Self::status_get_filename(&canonical_origin); - let is_installed = - self.symlink_is_installed(&canonical_origin, &canonical_destination)?; - let exists = canonical_origin.exists(); - let origin_color = origin_color(exists, is_installed); - let directory_extra = if is_directory { "/" } else { "" }; - println!( - "{}{} -> {}", - origin_color.paint(file_name), - directory_extra, - destination_color.paint(destination.display().to_string()) - ); - } - StatusItem::Directory { origin, mut items } => { - items.sort_by(|a, b| StatusItem::display_ord_cmp(a, b).reverse()); - let directory_name = Self::status_get_filename(&origin); - if depth != 0 { - println!("{}/", directory_name); - } - for item in items { - self.status_print_item(item, depth + 1)?; - } - } - StatusItem::Unlinked { - origin, - is_directory, - } => { - let file_name = Self::status_get_filename(&origin); - let directory_extra = if is_directory { "/" } else { "" }; - println!("{}{}", file_name, directory_extra); - } - } - Ok(()) - } - fn status_get_filename(path: &Path) -> &str { - path.file_name() - .and_then(|s| s.to_str()) - .unwrap_or_default() - } - - fn prepare_relative_path(&self, origin: &Path) -> anyhow::Result { - let canonical = utils::weakly_canonical(origin); - let relative = canonical - .strip_prefix(&self.depot_dir) - .context("Invalid origin path, not under depot directory")?; - Ok(relative.to_owned()) - } - - fn link_ids_from_paths_iter( - &self, - paths: impl Iterator>, - ) -> anyhow::Result> { - let mut link_ids = HashSet::::default(); - for path in paths { - let path = self.prepare_relative_path(path.as_ref())?; - link_ids.extend(self.depot.links_under(&path)?); - } - Ok(Vec::from_iter(link_ids.into_iter())) - } - - fn symlink_is_installed_by_link_id(&self, link_id: LinkID) -> anyhow::Result { - let canonical_pair = self.canonical_pair_from_link_id(link_id); - self.symlink_is_installed(&canonical_pair.origin, &canonical_pair.destination) - } - - fn symlink_is_installed(&self, origin: &Path, destination: &Path) -> anyhow::Result { - debug_assert!(origin.is_absolute()); - debug_assert!(destination.is_absolute()); - - if destination.is_symlink() { - let symlink_destination = destination.read_link()?; - match symlink_destination.canonicalize() { - Ok(canonicalized) => Ok(origin == canonicalized), - Err(_) => Ok(false), - } - } else { - Ok(false) - } - } - - fn symlink_install_by_link_id(&self, link_id: LinkID) -> anyhow::Result<()> { - let canonical_pair = self.canonical_pair_from_link_id(link_id); - self.symlink_install(&canonical_pair.origin, &canonical_pair.destination) - } - - fn symlink_install(&self, origin: &Path, destination: &Path) -> anyhow::Result<()> { - debug_assert!(origin.is_absolute()); - debug_assert!(destination.is_absolute()); - log::debug!( - "symlink_install : {} -> {}", - origin.display(), - destination.display() - ); - - let destination_parent = destination - .parent() - .ok_or_else(|| anyhow::anyhow!("destination has no parent component"))?; - std::fs::create_dir_all(destination_parent).context("Failed to create directories")?; - // need to do this beacause if the destination path ends in '/' because the symlink - // functions will treat it as a directory but we want a file with that name. - let destination = destination.with_file_name(destination.file_name().unwrap()); - - let destination_exists = destination.exists(); - let destination_is_symlink = destination.is_symlink(); - - if destination_exists && !destination_is_symlink { - return Err(anyhow::anyhow!("destination already exists")); - } - - if destination_is_symlink { - log::debug!("symlink already exists, removing before recreating"); - std::fs::remove_file(&destination)?; - } - - log::debug!( - "creating filesystem symlink {} -> {}", - origin.display(), - destination.display() - ); - std::os::unix::fs::symlink(origin, destination).context("failed to create symlink")?; - - Ok(()) - } - - fn symlink_uninstall(&self, origin: &Path, destination: &Path) -> anyhow::Result<()> { - debug_assert!(origin.is_absolute()); - debug_assert!(destination.is_absolute()); - let destination = destination.with_file_name(destination.file_name().unwrap()); - - if destination.is_symlink() { - let symlink_destination = destination.read_link()?.canonicalize()?; - if symlink_destination == origin { - std::fs::remove_file(&destination)?; - } - } - - Ok(()) - } - - fn symlink_uninstall_by_link_id(&self, link_id: LinkID) -> anyhow::Result<()> { - let canonical_pair = self.canonical_pair_from_link_id(link_id); - self.symlink_uninstall(&canonical_pair.origin, &canonical_pair.destination) - } - - fn canonical_pair_from_link_id(&self, link_id: LinkID) -> CanonicalPair { - let link_view = self.depot.link_view(link_id); - let relative_origin = link_view.origin(); - let relative_destination = link_view.destination(); - let canonical_origin = self.depot_dir.join(relative_origin); - let canonical_destination = self.install_base.join(relative_destination); - CanonicalPair { - origin: canonical_origin, - destination: canonical_destination, - } - } - } - - pub fn read(depot_path: PathBuf, install_base: PathBuf) -> anyhow::Result { - let depot_path = depot_path - .canonicalize() - .context("Failed to canonicalize depot path")?; - let install_base = install_base - .canonicalize() - .context("Failed to canonicalize install base")?; - if !install_base.is_dir() { - return Err(anyhow::anyhow!("Install base must be a directory")); - } - let depot = depot::read(&depot_path)?; - Dotup::new(depot, depot_path, install_base) - } - - pub fn write(dotup: &Dotup) -> anyhow::Result<()> { - depot::write(&dotup.depot_path, &dotup.depot)?; - Ok(()) - } -} - -mod utils { - use std::{ - collections::VecDeque, - path::{Component, Path, PathBuf}, - }; - - use crate::{ - dotup::{self, Dotup}, - Flags, - }; - - pub const DEFAULT_DEPOT_FILE_NAME: &str = ".depot"; - - /// Returns a list of canonical paths to all the files in `dir`. This includes files in - /// subdirectories. - /// Fails if dir isnt a directory or if there is some other io error. - pub fn collect_files_in_dir_recursive(dir: impl Into) -> anyhow::Result> { - let mut paths = Vec::new(); - let mut dirs = VecDeque::new(); - dirs.push_back(dir.into()); - - while let Some(dir) = dirs.pop_front() { - for entry in std::fs::read_dir(dir)? { - let entry = entry?; - let filetype = entry.file_type()?; - if filetype.is_dir() { - dirs.push_back(entry.path()); - } else { - paths.push(entry.path()); - } - } - } - - Ok(paths) - } - - pub fn collect_paths_in_dir(dir: impl AsRef) -> anyhow::Result> { - Ok(std::fs::read_dir(dir)? - .filter_map(|e| e.ok()) - .map(|e| e.path()) - .collect()) - } - - pub fn read_dotup(flags: &Flags) -> anyhow::Result { - let depot_path = depot_path_from_flags(flags)?; - let install_base = install_base_from_flags(flags); - dotup::read(depot_path, install_base) - } - - pub fn write_dotup(dotup: &Dotup) -> anyhow::Result<()> { - dotup::write(dotup) - } - - pub fn depot_path_from_flags(flags: &Flags) -> anyhow::Result { - match flags.depot { - Some(ref path) => Ok(path.clone()), - None => find_depot_path().ok_or_else(|| anyhow::anyhow!("Failed to find depot path")), - } - } - - pub fn default_depot_path() -> PathBuf { - current_working_directory().join(DEFAULT_DEPOT_FILE_NAME) - } - - pub fn find_depot_path() -> Option { - let mut cwd = current_working_directory(); - loop { - let path = cwd.join(DEFAULT_DEPOT_FILE_NAME); - if path.exists() { - break Some(path); - } - if !cwd.pop() { - break None; - } - } - } - - pub fn install_base_from_flags(flags: &Flags) -> PathBuf { - match flags.install_base { - Some(ref path) => path.clone(), - None => default_install_base(), - } - } - - pub fn default_install_base() -> PathBuf { - PathBuf::from(std::env::var("HOME").expect("Failed to obtain HOME environment variable")) - } - pub fn weakly_canonical(path: impl AsRef) -> PathBuf { - let cwd = current_working_directory(); - weakly_canonical_cwd(path, cwd) - } - - fn weakly_canonical_cwd(path: impl AsRef, cwd: PathBuf) -> PathBuf { - // Adapated from - // https://github.com/rust-lang/cargo/blob/fede83ccf973457de319ba6fa0e36ead454d2e20/src/cargo/util/paths.rs#L61 - let path = path.as_ref(); - - let mut components = path.components().peekable(); - let mut canonical = cwd; - let prefix = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() { - components.next(); - PathBuf::from(c.as_os_str()) - } else { - PathBuf::new() - }; - - for component in components { - match component { - Component::Prefix(_) => unreachable!(), - Component::RootDir => { - canonical = prefix.clone(); - canonical.push(component.as_os_str()) - } - Component::CurDir => {} - Component::ParentDir => { - canonical.pop(); - } - Component::Normal(p) => canonical.push(p), - }; - } - - canonical - } - - pub fn current_working_directory() -> PathBuf { - std::env::current_dir().expect("Failed to obtain current working directory") - } - - #[cfg(test)] - mod tests { - use super::*; - - #[test] - fn weak_canonical_test() { - let cwd = PathBuf::from("/home/user"); - assert_eq!( - PathBuf::from("/home/dest"), - weakly_canonical_cwd("../dest", cwd.clone()) - ); - assert_eq!( - PathBuf::from("/home/dest/configs/init.vim"), - weakly_canonical_cwd("../dest/configs/init.vim", cwd.clone()) - ); - assert_eq!( - PathBuf::from("/dest/configs/init.vim"), - weakly_canonical_cwd("/dest/configs/init.vim", cwd.clone()) - ); - assert_eq!( - PathBuf::from("/home/user/configs/nvim/lua/setup.lua"), - weakly_canonical_cwd("./configs/nvim/lua/setup.lua", cwd.clone()) - ); - assert_eq!( - PathBuf::from("/home/user/configs/nvim/lua/setup.lua"), - weakly_canonical_cwd("configs/nvim/lua/setup.lua", cwd) - ); - } - } -} +mod depot; +mod dotup; +mod utils; use std::path::PathBuf; -- cgit